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
211#[derive(Debug, Clone)]
212pub struct OpenedArchive {
213 options: ReaderOptions,
214 observed_archive_bytes: u64,
215 observed_volume_count: u32,
216 subkeys: Subkeys,
217 blocks: BTreeMap<u64, BlockRecord>,
218 lazy_blocks: Option<Arc<SeekableBlockSource>>,
219 crypto_header_bytes: Vec<u8>,
220 pub volume_header: VolumeHeader,
221 pub crypto_header: CryptoHeaderFixed,
222 pub manifest_footer: ManifestFooter,
223 pub volume_trailer: Option<VolumeTrailer>,
224 pub root_auth_footer: Option<RootAuthFooterV1>,
225 pub index_root: IndexRoot,
226 payload_dictionary: Option<Vec<u8>>,
227}
228
229#[derive(Debug)]
230pub struct ArchiveContentVerification<'a> {
231 archive: &'a OpenedArchive,
232}
233
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub struct ArchiveRepairPatch {
236 pub volume_index: u32,
237 pub block_index: u64,
238 pub record_offset: u64,
239 pub record_bytes: Vec<u8>,
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243pub enum RootAuthDiagnostic {
244 RootAuthContentVerified,
245 RootAuthDeferredFullArchiveScanRequired,
246 AuthenticatedMetadataNotRootSigned,
247 RecoveryMarginNotRootAuthenticated,
248 ReplicatedGlobalCopyUncheckedDueToVolumeLoss,
249 RecoveryMarginChecked,
250 RecoveryMarginFailed,
251 RecoveryMarginUnchecked,
252}
253
254impl RootAuthDiagnostic {
255 pub const fn label(self) -> &'static str {
256 match self {
257 Self::RootAuthContentVerified => "root_auth_content_verified",
258 Self::RootAuthDeferredFullArchiveScanRequired => {
259 "root_auth_deferred_full_archive_scan_required"
260 }
261 Self::AuthenticatedMetadataNotRootSigned => "authenticated_metadata_not_root_signed",
262 Self::RecoveryMarginNotRootAuthenticated => "recovery_margin_not_root_authenticated",
263 Self::ReplicatedGlobalCopyUncheckedDueToVolumeLoss => {
264 "replicated_global_copy_unchecked_due_to_volume_loss"
265 }
266 Self::RecoveryMarginChecked => "recovery_margin_checked",
267 Self::RecoveryMarginFailed => "recovery_margin_failed",
268 Self::RecoveryMarginUnchecked => "recovery_margin_unchecked",
269 }
270 }
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub enum PublicNoKeyDiagnostic {
275 PublicDataBlockCommitmentVerified,
276 PublicPhysicalCompletenessUnverified,
277 PublicRecoveryMarginUnchecked,
278}
279
280impl PublicNoKeyDiagnostic {
281 pub const fn label(self) -> &'static str {
282 match self {
283 Self::PublicDataBlockCommitmentVerified => "public_data_block_commitment_verified",
284 Self::PublicPhysicalCompletenessUnverified => "public_physical_completeness_unverified",
285 Self::PublicRecoveryMarginUnchecked => "public_recovery_margin_unchecked",
286 }
287 }
288}
289
290#[derive(Debug, Clone, PartialEq, Eq)]
291pub struct RootAuthVerification {
292 pub archive_root: [u8; 32],
293 pub authenticator_id: u16,
294 pub signer_identity_type: u16,
295 pub signer_identity_bytes: Vec<u8>,
296 pub total_data_block_count: u64,
297 pub diagnostics: Vec<RootAuthDiagnostic>,
298}
299
300#[derive(Debug, Clone, PartialEq, Eq)]
301pub struct PublicNoKeyVerification {
302 pub archive_root: [u8; 32],
303 pub authenticator_id: u16,
304 pub signer_identity_type: u16,
305 pub signer_identity_bytes: Vec<u8>,
306 pub total_data_block_count: u64,
307 pub diagnostics: Vec<PublicNoKeyDiagnostic>,
308}
309
310#[derive(Debug, Clone, Copy, PartialEq, Eq)]
311struct RootAuthMaterial {
312 critical_metadata_digest: [u8; 32],
313 index_digest: [u8; 32],
314 fec_layout_digest: [u8; 32],
315 data_block_merkle_root: [u8; 32],
316 signer_identity_digest: [u8; 32],
317 archive_root: [u8; 32],
318 total_data_block_count: u64,
319}
320
321#[derive(Debug, Clone, Copy)]
322struct ObjectExtent {
323 first_block_index: u64,
324 data_block_count: u32,
325 parity_block_count: u32,
326 encrypted_size: u32,
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
330enum ParityReadPolicy {
331 Always,
332 RepairOnly,
333}
334
335pub(crate) struct StreamedArchiveOpenParts {
336 pub(crate) options: ReaderOptions,
337 pub(crate) observed_archive_bytes: u64,
338 pub(crate) subkeys: Subkeys,
339 pub(crate) blocks: BTreeMap<u64, BlockRecord>,
340 pub(crate) crypto_header_bytes: Vec<u8>,
341 pub(crate) volume_header: VolumeHeader,
342 pub(crate) crypto_header: CryptoHeaderFixed,
343 pub(crate) manifest_footer: ManifestFooter,
344 pub(crate) volume_trailer: VolumeTrailer,
345 pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
346}
347
348#[derive(Clone, Copy)]
349struct WinningIndexEntry {
350 start: u64,
351 file_data_size: u64,
352 shard_index: usize,
353 file_index: usize,
354}
355
356struct LocatedIndexFile {
357 shard: IndexShard,
358 file_index: usize,
359 start: u64,
360}
361
362struct DecodedTarMemberGroupReader<'a> {
363 archive: &'a OpenedArchive,
364 shard: &'a IndexShard,
365 file: &'a FileEntry,
366 decompressor: zstd::bulk::Decompressor<'static>,
367 next_frame_offset: u64,
368 cached_envelope_index: Option<u64>,
369 cached_envelope_plaintext: Vec<u8>,
370 current_frame: Vec<u8>,
371 current_frame_offset: usize,
372 remaining_group_bytes: u64,
373}
374
375struct SeekableVolumeSource {
376 reader: Arc<dyn ArchiveReadAt>,
377 volume_index: u32,
378 crypto_end: u64,
379 block_count: u64,
380 record_len: u64,
381 block_size: usize,
382}
383
384impl std::fmt::Debug for SeekableVolumeSource {
385 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386 f.debug_struct("SeekableVolumeSource")
387 .field("volume_index", &self.volume_index)
388 .field("crypto_end", &self.crypto_end)
389 .field("block_count", &self.block_count)
390 .field("record_len", &self.record_len)
391 .field("block_size", &self.block_size)
392 .finish_non_exhaustive()
393 }
394}
395
396#[derive(Debug)]
397struct SeekableBlockSource {
398 stripe_width: u32,
399 volumes: Vec<Option<SeekableVolumeSource>>,
400}
401
402trait BlockProvider {
403 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError>;
404}
405
406struct OpenedBlockProvider<'a> {
407 memory_blocks: &'a BTreeMap<u64, BlockRecord>,
408 lazy_blocks: Option<&'a SeekableBlockSource>,
409}
410
411impl SeekableBlockSource {
412 fn record_location(&self, block_index: u64) -> Result<(u32, u64), FormatError> {
413 if self.stripe_width == 0 {
414 return Err(FormatError::ZeroStripeWidth);
415 }
416 let volume_index = u32::try_from(block_index % self.stripe_width as u64)
417 .map_err(|_| FormatError::InvalidArchive("BlockRecord volume index overflow"))?;
418 let Some(volume) = self
419 .volumes
420 .get(volume_index as usize)
421 .and_then(Option::as_ref)
422 else {
423 return Err(FormatError::InvalidArchive(
424 "repair output requires all archive volumes",
425 ));
426 };
427 let slot = block_index / self.stripe_width as u64;
428 if slot >= volume.block_count {
429 return Err(FormatError::InvalidArchive(
430 "BlockRecord global coverage has a gap",
431 ));
432 }
433 Ok((volume_index, volume.record_offset(slot)?))
434 }
435
436 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
437 if self.stripe_width == 0 {
438 return Err(FormatError::ZeroStripeWidth);
439 }
440 let volume_index = u32::try_from(block_index % self.stripe_width as u64)
441 .map_err(|_| FormatError::InvalidArchive("BlockRecord volume index overflow"))?;
442 let Some(volume) = self
443 .volumes
444 .get(volume_index as usize)
445 .and_then(Option::as_ref)
446 else {
447 return Ok(None);
448 };
449 let slot = block_index / self.stripe_width as u64;
450 if slot >= volume.block_count {
451 return Ok(None);
452 }
453 match volume.read_slot(slot, block_index) {
454 Ok(record) => Ok(Some(record)),
455 Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
456 Err(err) => Err(err),
457 }
458 }
459
460 fn is_complete_volume_set(&self) -> bool {
461 self.volumes.iter().all(Option::is_some)
462 }
463
464 fn total_block_count(&self) -> Result<u64, FormatError> {
465 self.volumes
466 .iter()
467 .map(|volume| {
468 volume
469 .as_ref()
470 .map(|volume| volume.block_count)
471 .ok_or(FormatError::InvalidArchive(
472 "missing volume in complete set",
473 ))
474 })
475 .try_fold(0u64, |sum, count| {
476 checked_u64_add(sum, count?, "BlockRecord count overflow")
477 })
478 }
479}
480
481impl SeekableVolumeSource {
482 fn record_offset(&self, slot: u64) -> Result<u64, FormatError> {
483 self.crypto_end
484 .checked_add(checked_u64_mul(
485 slot,
486 self.record_len,
487 "BlockRecord offset overflow",
488 )?)
489 .ok_or(FormatError::InvalidArchive("BlockRecord offset overflow"))
490 }
491
492 fn read_slot(&self, slot: u64, expected_block_index: u64) -> Result<BlockRecord, FormatError> {
493 let record_offset = self.record_offset(slot)?;
494 let raw = read_at_vec(
495 self.reader.as_ref(),
496 record_offset,
497 usize::try_from(self.record_len)
498 .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?,
499 "BlockRecord",
500 )?;
501 let record = BlockRecord::parse(&raw, self.block_size)?;
502 if record.block_index != expected_block_index {
503 return Err(FormatError::InvalidArchive(
504 "BlockRecord index does not match volume position",
505 ));
506 }
507 Ok(record)
508 }
509}
510
511impl BlockProvider for BTreeMap<u64, BlockRecord> {
512 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
513 Ok(self.get(&block_index).cloned())
514 }
515}
516
517impl BlockProvider for OpenedBlockProvider<'_> {
518 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
519 if let Some(record) = self.memory_blocks.get(&block_index) {
520 return Ok(Some(record.clone()));
521 }
522 match self.lazy_blocks {
523 Some(source) => source.block(block_index),
524 None => Ok(None),
525 }
526 }
527}
528
529fn subkeys_for_open(
530 master_key: Option<&MasterKey>,
531 aead_algo: AeadAlgo,
532 archive_uuid: &[u8; 16],
533 session_id: &[u8; 16],
534) -> Result<Subkeys, FormatError> {
535 if aead_algo.is_encrypted() {
536 Subkeys::derive(
537 master_key.ok_or(FormatError::KeyMaterialMismatch)?,
538 archive_uuid,
539 session_id,
540 )
541 } else {
542 Ok(Subkeys::unencrypted_placeholder())
543 }
544}
545
546type DirectoryHintMap = BTreeMap<Vec<u8>, BTreeSet<u32>>;
547pub type ExtractedRegularFile = (Vec<u8>, Vec<MetadataDiagnostic>);
548const FAST_FULL_EXTRACT_UNIQUE_PATHS_UNSUPPORTED: &str =
549 "fast full extract requires unique archive paths";
550
551#[derive(Debug)]
552struct PayloadIndexTables {
553 shards: Vec<IndexShard>,
554 file_count: u64,
555 frames: BTreeMap<u64, FrameEntry>,
556 envelopes: BTreeMap<u64, EnvelopeEntry>,
557}
558
559pub fn open_archive(bytes: &[u8], master_key: &MasterKey) -> Result<OpenedArchive, FormatError> {
560 OpenedArchive::open_with_options(bytes, master_key, ReaderOptions::default())
561}
562
563pub fn open_archive_unencrypted(bytes: &[u8]) -> Result<OpenedArchive, FormatError> {
564 require_unencrypted_volume_profile(bytes)?;
565 let placeholder = MasterKey::from_raw_key(&[0; 32])?;
566 OpenedArchive::open_with_options(bytes, &placeholder, ReaderOptions::default())
567}
568
569pub fn open_archive_volumes(
570 volumes: &[&[u8]],
571 master_key: &MasterKey,
572) -> Result<OpenedArchive, FormatError> {
573 OpenedArchive::open_volumes_with_options(volumes, master_key, ReaderOptions::default())
574}
575
576pub fn open_archive_volumes_unencrypted(volumes: &[&[u8]]) -> Result<OpenedArchive, FormatError> {
577 for volume in volumes {
578 require_unencrypted_volume_profile(volume)?;
579 }
580 let placeholder = MasterKey::from_raw_key(&[0; 32])?;
581 OpenedArchive::open_volumes_with_options(volumes, &placeholder, ReaderOptions::default())
582}
583
584pub fn open_archive_with_bootstrap_sidecar(
585 bytes: &[u8],
586 bootstrap_sidecar: &[u8],
587 master_key: &MasterKey,
588) -> Result<OpenedArchive, FormatError> {
589 OpenedArchive::open_with_bootstrap_sidecar_options(
590 bytes,
591 bootstrap_sidecar,
592 master_key,
593 ReaderOptions::default(),
594 )
595}
596
597fn require_unencrypted_volume_profile(bytes: &[u8]) -> Result<(), FormatError> {
598 if bytes.len() < VOLUME_HEADER_LEN {
599 return Err(FormatError::InvalidLength {
600 structure: "archive",
601 expected: VOLUME_HEADER_LEN,
602 actual: bytes.len(),
603 });
604 }
605 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
606 let crypto_start = volume_header.crypto_header_offset as usize;
607 let crypto_len = volume_header.crypto_header_length as usize;
608 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
609 let crypto_header = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
610 if crypto_header.fixed.aead_algo == AeadAlgo::None
611 && crypto_header.fixed.kdf_algo == KdfAlgo::None
612 {
613 Ok(())
614 } else {
615 Err(FormatError::KeyMaterialMismatch)
616 }
617}
618
619pub fn open_seekable_archive<R: ArchiveReadAt>(
620 reader: R,
621 master_key: &MasterKey,
622) -> Result<OpenedArchive, FormatError> {
623 OpenedArchive::open_seekable_volumes_with_options(
624 vec![reader],
625 master_key,
626 ReaderOptions::default(),
627 )
628}
629
630pub fn open_seekable_archive_volumes<R: ArchiveReadAt>(
631 readers: Vec<R>,
632 master_key: &MasterKey,
633) -> Result<OpenedArchive, FormatError> {
634 OpenedArchive::open_seekable_volumes_with_options(readers, master_key, ReaderOptions::default())
635}
636
637pub fn open_seekable_archive_with_bootstrap_sidecar<R: ArchiveReadAt>(
638 reader: R,
639 bootstrap_sidecar: &[u8],
640 master_key: &MasterKey,
641) -> Result<OpenedArchive, FormatError> {
642 open_seekable_archive_with_bootstrap_sidecar_options(
643 reader,
644 bootstrap_sidecar,
645 master_key,
646 ReaderOptions::default(),
647 )
648}
649
650pub fn open_seekable_archive_with_bootstrap_sidecar_options<R: ArchiveReadAt>(
651 reader: R,
652 bootstrap_sidecar: &[u8],
653 master_key: &MasterKey,
654 options: ReaderOptions,
655) -> Result<OpenedArchive, FormatError> {
656 OpenedArchive::open_seekable_volumes_with_options_for_mode(
657 vec![Arc::new(reader) as Arc<dyn ArchiveReadAt>],
658 master_key,
659 options,
660 Some(bootstrap_sidecar),
661 )
662}
663
664pub fn open_non_seekable_archive(
665 bytes: &[u8],
666 master_key: &MasterKey,
667 bootstrap_sidecar: Option<&[u8]>,
668) -> Result<OpenedArchive, FormatError> {
669 match bootstrap_sidecar {
670 Some(sidecar) => OpenedArchive::open_with_bootstrap_sidecar_options_for_mode(
671 bytes,
672 sidecar,
673 master_key,
674 ReaderOptions::default(),
675 BootstrapSidecarUse::NonSeekableRandomAccess,
676 ),
677 None => Err(FormatError::ReaderUnsupported(
678 "non-seekable random access requires a bootstrap sidecar",
679 )),
680 }
681}
682
683pub fn public_no_key_verify_archive_with<F>(
684 bytes: &[u8],
685 verifier: F,
686) -> Result<PublicNoKeyVerification, FormatError>
687where
688 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
689{
690 public_no_key_verify_volumes_with_options(&[bytes], verifier, ReaderOptions::default())
691}
692
693pub fn public_no_key_verify_volumes_with<F>(
694 volumes: &[&[u8]],
695 verifier: F,
696) -> Result<PublicNoKeyVerification, FormatError>
697where
698 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
699{
700 public_no_key_verify_volumes_with_options(volumes, verifier, ReaderOptions::default())
701}
702
703pub fn sequential_extract_tar_stream(
709 bytes: &[u8],
710 master_key: &MasterKey,
711) -> Result<Vec<u8>, FormatError> {
712 sequential_extract_tar_stream_with_options(bytes, master_key, ReaderOptions::default())
713}
714
715impl OpenedArchive {
716 fn block_provider(&self) -> OpenedBlockProvider<'_> {
717 OpenedBlockProvider {
718 memory_blocks: &self.blocks,
719 lazy_blocks: self.lazy_blocks.as_deref(),
720 }
721 }
722
723 fn missing_volume_count(&self) -> u32 {
724 self.crypto_header
725 .stripe_width
726 .saturating_sub(self.observed_volume_count)
727 }
728
729 fn root_auth_success_diagnostics(&self) -> Vec<RootAuthDiagnostic> {
730 let mut diagnostics = vec![
731 RootAuthDiagnostic::RootAuthContentVerified,
732 RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
733 RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
734 ];
735 if self.missing_volume_count() > 0 {
736 diagnostics.push(RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss);
737 }
738 diagnostics.push(RootAuthDiagnostic::RecoveryMarginUnchecked);
739 diagnostics
740 }
741
742 pub fn open_with_options(
743 bytes: &[u8],
744 master_key: &MasterKey,
745 options: ReaderOptions,
746 ) -> Result<Self, FormatError> {
747 Self::open_volumes_with_options(&[bytes], master_key, options)
748 }
749
750 pub fn open_volumes_with_options(
751 volumes: &[&[u8]],
752 master_key: &MasterKey,
753 options: ReaderOptions,
754 ) -> Result<Self, FormatError> {
755 validate_reader_options(options)?;
756 if volumes.is_empty() {
757 return Err(FormatError::InvalidArchive("no volumes supplied"));
758 }
759
760 let observed_archive_bytes =
761 observed_archive_size(volumes.iter().map(|volume| volume.len() as u64))?;
762 let mut first: Option<ParsedSeekableVolume> = None;
763 let mut manifest_authority: Option<ManifestFooter> = None;
764 let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
765 let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
766 let mut root_auth_authority: Option<RootAuthFooterV1> = None;
767 let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
768 let mut saw_root_auth_absent = false;
769 let mut first_manifest_footer_error: Option<FormatError> = None;
770 let mut seen_volume_indexes = BTreeSet::new();
771 let mut blocks = BTreeMap::new();
772 let mut erased_block_indices = BTreeSet::new();
773
774 for volume_bytes in volumes {
775 let mut parsed = parse_seekable_volume(volume_bytes, master_key, options)?;
776 if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
777 return Err(FormatError::InvalidArchive(
778 "duplicate authenticated volume index",
779 ));
780 }
781
782 if let Some(first) = &first {
783 validate_volume_set_member(first, &parsed)?;
784 }
785
786 if let Some(footer) = &parsed.manifest_footer {
787 if let Some(authority) = &manifest_authority {
788 if !manifest_bootstrap_fields_match(authority, footer) {
789 return Err(FormatError::InvalidArchive(
790 "ManifestFooter bootstrap fields differ",
791 ));
792 }
793 } else {
794 manifest_authority = Some(footer.clone());
795 manifest_authority_volume_header = Some(parsed.volume_header.clone());
796 manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
797 }
798 } else if first_manifest_footer_error.is_none() {
799 first_manifest_footer_error = parsed.manifest_footer_error.take();
800 }
801
802 match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
803 (Some(footer), Some(bytes)) => {
804 if saw_root_auth_absent {
805 return Err(FormatError::InvalidArchive(
806 "root-auth footer presence differs across volumes",
807 ));
808 }
809 if let Some(authority_bytes) = &root_auth_authority_bytes {
810 if authority_bytes != bytes {
811 return Err(FormatError::InvalidArchive(
812 "RootAuthFooter copies differ",
813 ));
814 }
815 } else {
816 root_auth_authority = Some(footer.clone());
817 root_auth_authority_bytes = Some(bytes.clone());
818 }
819 }
820 (None, None) => {
821 if root_auth_authority_bytes.is_some() {
822 return Err(FormatError::InvalidArchive(
823 "root-auth footer presence differs across volumes",
824 ));
825 }
826 saw_root_auth_absent = true;
827 }
828 _ => {
829 return Err(FormatError::InvalidArchive(
830 "root-auth footer terminal state is inconsistent",
831 ));
832 }
833 }
834
835 for (block_index, record) in &parsed.blocks {
836 if blocks.insert(*block_index, record.clone()).is_some() {
837 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
838 }
839 }
840 for block_index in &parsed.erased_block_indices {
841 erased_block_indices.insert(*block_index);
842 }
843
844 if first.is_none() {
845 first = Some(parsed);
846 }
847 }
848
849 let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
850 let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
851 Some(err) => err,
852 None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
853 })?;
854 let authority_volume_header = manifest_authority_volume_header.ok_or(
855 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
856 )?;
857 let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
858 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
859 )?;
860 let observed_volume_count = u32::try_from(seen_volume_indexes.len())
861 .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
862 let missing_volume_count = first
863 .crypto_header
864 .stripe_width
865 .checked_sub(observed_volume_count)
866 .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
867 if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
868 return Err(FormatError::InvalidArchive(
869 "missing volume count exceeds volume_loss_tolerance",
870 ));
871 }
872 if seen_volume_indexes.len() == first.crypto_header.stripe_width as usize {
873 validate_complete_global_block_coverage(&blocks, &erased_block_indices)?;
874 }
875
876 let limits = metadata_limits(&first.crypto_header);
877 let index_root_plaintext = load_metadata_object_from_parts(
878 &blocks,
879 ObjectLoadContext::index_root(
880 &first.volume_header,
881 &first.crypto_header,
882 &first.subkeys,
883 ObjectExtent {
884 first_block_index: manifest_footer.index_root_first_block,
885 data_block_count: manifest_footer.index_root_data_block_count,
886 parity_block_count: manifest_footer.index_root_parity_block_count,
887 encrypted_size: manifest_footer.index_root_encrypted_size,
888 },
889 ),
890 manifest_footer.index_root_decompressed_size,
891 )?;
892 let index_root = IndexRoot::parse(
893 &index_root_plaintext,
894 first.crypto_header.has_dictionary != 0,
895 limits,
896 )?;
897 let payload_dictionary = load_archive_dictionary(
898 &blocks,
899 &first.subkeys,
900 &first.volume_header,
901 &first.crypto_header,
902 &index_root,
903 )?;
904
905 Ok(Self {
906 options,
907 observed_archive_bytes,
908 observed_volume_count,
909 subkeys: first.subkeys,
910 blocks,
911 lazy_blocks: None,
912 crypto_header_bytes: first.crypto_header_bytes,
913 volume_header: authority_volume_header,
914 crypto_header: first.crypto_header,
915 manifest_footer,
916 volume_trailer: Some(authority_volume_trailer),
917 root_auth_footer: root_auth_authority,
918 index_root,
919 payload_dictionary,
920 })
921 }
922
923 pub fn open_seekable_volumes_with_options<R: ArchiveReadAt>(
924 readers: Vec<R>,
925 master_key: &MasterKey,
926 options: ReaderOptions,
927 ) -> Result<Self, FormatError> {
928 let readers = readers
929 .into_iter()
930 .map(|reader| Arc::new(reader) as Arc<dyn ArchiveReadAt>)
931 .collect::<Vec<_>>();
932 Self::open_seekable_volumes_with_options_for_mode(readers, master_key, options, None)
933 }
934
935 fn open_seekable_volumes_with_options_for_mode(
936 readers: Vec<Arc<dyn ArchiveReadAt>>,
937 master_key: &MasterKey,
938 options: ReaderOptions,
939 bootstrap_sidecar: Option<&[u8]>,
940 ) -> Result<Self, FormatError> {
941 validate_reader_options(options)?;
942 if readers.is_empty() {
943 return Err(FormatError::InvalidArchive("no volumes supplied"));
944 }
945 if bootstrap_sidecar.is_some() && readers.len() > 1 {
946 return Err(FormatError::ReaderUnsupported(
947 "multi-volume inputs with bootstrap sidecar are not supported",
948 ));
949 }
950
951 let observed_archive_bytes = observed_archive_size(
952 readers
953 .iter()
954 .map(|reader| reader.len())
955 .collect::<Result<Vec<_>, _>>()?
956 .into_iter()
957 .chain(bootstrap_sidecar.map(|sidecar| sidecar.len() as u64)),
958 )?;
959 let mut first: Option<ParsedSeekableReadAtVolume> = None;
960 let mut manifest_authority: Option<ManifestFooter> = None;
961 let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
962 let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
963 let mut root_auth_authority: Option<RootAuthFooterV1> = None;
964 let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
965 let mut saw_root_auth_absent = false;
966 let mut first_manifest_footer_error: Option<FormatError> = None;
967 let mut seen_volume_indexes = BTreeSet::new();
968 let mut lazy_volume_slots: Vec<Option<SeekableVolumeSource>> = Vec::new();
969
970 for reader in readers {
971 let mut parsed = parse_seekable_read_at_volume(reader, master_key, options)?;
972 if bootstrap_sidecar.is_some() {
973 validate_bootstrap_single_volume_input(
974 &parsed.volume_header,
975 &parsed.crypto_header,
976 )?;
977 }
978 if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
979 return Err(FormatError::InvalidArchive(
980 "duplicate authenticated volume index",
981 ));
982 }
983
984 if let Some(first) = &first {
985 validate_volume_set_member_metadata(
986 &first.volume_header,
987 &first.crypto_header,
988 &first.crypto_header_bytes,
989 &parsed.volume_header,
990 &parsed.crypto_header,
991 &parsed.crypto_header_bytes,
992 )?;
993 } else {
994 lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
995 }
996
997 if let Some(footer) = &parsed.manifest_footer {
998 if let Some(authority) = &manifest_authority {
999 if !manifest_bootstrap_fields_match(authority, footer) {
1000 return Err(FormatError::InvalidArchive(
1001 "ManifestFooter bootstrap fields differ",
1002 ));
1003 }
1004 } else {
1005 manifest_authority = Some(footer.clone());
1006 manifest_authority_volume_header = Some(parsed.volume_header.clone());
1007 manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
1008 }
1009 } else if first_manifest_footer_error.is_none() {
1010 first_manifest_footer_error = parsed.manifest_footer_error.take();
1011 }
1012
1013 match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
1014 (Some(footer), Some(bytes)) => {
1015 if saw_root_auth_absent {
1016 return Err(FormatError::InvalidArchive(
1017 "root-auth footer presence differs across volumes",
1018 ));
1019 }
1020 if let Some(authority_bytes) = &root_auth_authority_bytes {
1021 if authority_bytes != bytes {
1022 return Err(FormatError::InvalidArchive(
1023 "RootAuthFooter copies differ",
1024 ));
1025 }
1026 } else {
1027 root_auth_authority = Some(footer.clone());
1028 root_auth_authority_bytes = Some(bytes.clone());
1029 }
1030 }
1031 (None, None) => {
1032 if root_auth_authority_bytes.is_some() {
1033 return Err(FormatError::InvalidArchive(
1034 "root-auth footer presence differs across volumes",
1035 ));
1036 }
1037 saw_root_auth_absent = true;
1038 }
1039 _ => {
1040 return Err(FormatError::InvalidArchive(
1041 "root-auth footer terminal state is inconsistent",
1042 ));
1043 }
1044 }
1045
1046 let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
1047 let source = SeekableVolumeSource {
1048 reader: parsed.reader.clone(),
1049 volume_index: parsed.volume_header.volume_index,
1050 crypto_end: parsed.crypto_end,
1051 block_count: parsed.volume_trailer.block_count,
1052 record_len,
1053 block_size: parsed.crypto_header.block_size as usize,
1054 };
1055 let slot = parsed.volume_header.volume_index as usize;
1056 if slot >= lazy_volume_slots.len() || lazy_volume_slots[slot].replace(source).is_some()
1057 {
1058 return Err(FormatError::InvalidArchive(
1059 "duplicate authenticated volume index",
1060 ));
1061 }
1062
1063 if first.is_none() {
1064 first = Some(parsed);
1065 }
1066 }
1067
1068 let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
1069 let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
1070 Some(err) => err,
1071 None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1072 })?;
1073 let authority_volume_header = manifest_authority_volume_header.ok_or(
1074 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1075 )?;
1076 let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
1077 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1078 )?;
1079 let observed_volume_count = u32::try_from(seen_volume_indexes.len())
1080 .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
1081 let missing_volume_count = first
1082 .crypto_header
1083 .stripe_width
1084 .checked_sub(observed_volume_count)
1085 .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1086 if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
1087 return Err(FormatError::InvalidArchive(
1088 "missing volume count exceeds volume_loss_tolerance",
1089 ));
1090 }
1091
1092 let mut blocks = BTreeMap::new();
1093 let sidecar = if let Some(bytes) = bootstrap_sidecar {
1094 let sidecar = parse_bootstrap_sidecar(
1095 bytes,
1096 &first.volume_header,
1097 &first.crypto_header,
1098 &first.subkeys,
1099 )?;
1100 sidecar
1101 .require_sections_for(BootstrapSidecarUse::SeekableAssist, &first.crypto_header)?;
1102 if let Some(sidecar_manifest) = &sidecar.manifest_footer {
1103 if !manifest_bootstrap_fields_match(&manifest_footer, sidecar_manifest) {
1104 return Err(FormatError::InvalidArchive(
1105 "bootstrap sidecar conflicts with terminal ManifestFooter",
1106 ));
1107 }
1108 }
1109 Some((bytes, sidecar))
1110 } else {
1111 None
1112 };
1113
1114 if let Some((sidecar_bytes, sidecar)) = &sidecar {
1115 if let Some((offset, length)) = sidecar.index_root_records_section {
1116 let index_root_records = parse_sidecar_block_records(
1117 sidecar_bytes,
1118 first.crypto_header.block_size as usize,
1119 SidecarBlockRecordsSection {
1120 offset,
1121 length,
1122 extent: index_root_extent_from_manifest(&manifest_footer),
1123 data_kind: BlockKind::IndexRootData,
1124 parity_kind: BlockKind::IndexRootParity,
1125 structure: "IndexRoot",
1126 },
1127 )?;
1128 insert_sidecar_records(&mut blocks, index_root_records)?;
1129 }
1130 }
1131
1132 let lazy_source = Arc::new(SeekableBlockSource {
1133 stripe_width: first.crypto_header.stripe_width,
1134 volumes: lazy_volume_slots,
1135 });
1136 let block_provider = OpenedBlockProvider {
1137 memory_blocks: &blocks,
1138 lazy_blocks: Some(lazy_source.as_ref()),
1139 };
1140 let limits = metadata_limits(&first.crypto_header);
1141 let index_root_plaintext = load_metadata_object_from_parts(
1142 &block_provider,
1143 ObjectLoadContext::index_root(
1144 &first.volume_header,
1145 &first.crypto_header,
1146 &first.subkeys,
1147 index_root_extent_from_manifest(&manifest_footer),
1148 ),
1149 manifest_footer.index_root_decompressed_size,
1150 )?;
1151 let index_root = IndexRoot::parse(
1152 &index_root_plaintext,
1153 first.crypto_header.has_dictionary != 0,
1154 limits,
1155 )?;
1156 if first.crypto_header.has_dictionary != 0 {
1157 if let Some((sidecar_bytes, sidecar)) = &sidecar {
1158 if let Some((offset, length)) = sidecar.dictionary_records_section {
1159 let dictionary_records = parse_sidecar_block_records(
1160 sidecar_bytes,
1161 first.crypto_header.block_size as usize,
1162 SidecarBlockRecordsSection {
1163 offset,
1164 length,
1165 extent: dictionary_extent_from_index_root(&index_root)?,
1166 data_kind: BlockKind::DictionaryData,
1167 parity_kind: BlockKind::DictionaryParity,
1168 structure: "Dictionary",
1169 },
1170 )?;
1171 insert_sidecar_records(&mut blocks, dictionary_records)?;
1172 }
1173 }
1174 }
1175 let block_provider = OpenedBlockProvider {
1176 memory_blocks: &blocks,
1177 lazy_blocks: Some(lazy_source.as_ref()),
1178 };
1179 let payload_dictionary = load_archive_dictionary(
1180 &block_provider,
1181 &first.subkeys,
1182 &first.volume_header,
1183 &first.crypto_header,
1184 &index_root,
1185 )?;
1186
1187 Ok(Self {
1188 options,
1189 observed_archive_bytes,
1190 observed_volume_count,
1191 subkeys: first.subkeys,
1192 blocks,
1193 lazy_blocks: Some(lazy_source),
1194 crypto_header_bytes: first.crypto_header_bytes,
1195 volume_header: authority_volume_header,
1196 crypto_header: first.crypto_header,
1197 manifest_footer,
1198 volume_trailer: Some(authority_volume_trailer),
1199 root_auth_footer: root_auth_authority,
1200 index_root,
1201 payload_dictionary,
1202 })
1203 }
1204
1205 pub fn open_with_bootstrap_sidecar_options(
1206 bytes: &[u8],
1207 bootstrap_sidecar: &[u8],
1208 master_key: &MasterKey,
1209 options: ReaderOptions,
1210 ) -> Result<Self, FormatError> {
1211 Self::open_with_bootstrap_sidecar_options_for_mode(
1212 bytes,
1213 bootstrap_sidecar,
1214 master_key,
1215 options,
1216 BootstrapSidecarUse::SeekableAssist,
1217 )
1218 }
1219
1220 fn open_with_bootstrap_sidecar_options_for_mode(
1221 bytes: &[u8],
1222 bootstrap_sidecar: &[u8],
1223 master_key: &MasterKey,
1224 options: ReaderOptions,
1225 sidecar_use: BootstrapSidecarUse,
1226 ) -> Result<Self, FormatError> {
1227 let observed_archive_bytes =
1228 observed_archive_size([bytes.len() as u64, bootstrap_sidecar.len() as u64])?;
1229 if bytes.len() < VOLUME_HEADER_LEN {
1230 return Err(FormatError::InvalidLength {
1231 structure: "archive",
1232 expected: VOLUME_HEADER_LEN,
1233 actual: bytes.len(),
1234 });
1235 }
1236
1237 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
1238 let crypto_start = volume_header.crypto_header_offset as usize;
1239 let crypto_len = volume_header.crypto_header_length as usize;
1240 let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
1241 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
1242 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
1243 let subkeys = subkeys_for_open(
1244 Some(master_key),
1245 parsed_crypto.fixed.aead_algo,
1246 &volume_header.archive_uuid,
1247 &volume_header.session_id,
1248 )?;
1249 verify_integrity_tag(
1250 HmacDomain::CryptoHeader,
1251 parsed_crypto.fixed.aead_algo,
1252 Some(&subkeys.mac_key),
1253 &volume_header.archive_uuid,
1254 &volume_header.session_id,
1255 parsed_crypto.hmac_covered_bytes,
1256 &parsed_crypto.header_hmac,
1257 )?;
1258 parsed_crypto.validate_extension_semantics()?;
1259 reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
1260 validate_bootstrap_single_volume_input(&volume_header, &parsed_crypto.fixed)?;
1261 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
1262
1263 let sidecar = parse_bootstrap_sidecar(
1264 bootstrap_sidecar,
1265 &volume_header,
1266 &parsed_crypto.fixed,
1267 &subkeys,
1268 )?;
1269 sidecar.require_sections_for(sidecar_use, &parsed_crypto.fixed)?;
1270
1271 let (mut blocks, terminal_offset, observed_block_count) = parse_stream_block_prefix(
1272 bytes,
1273 crypto_end,
1274 parsed_crypto.fixed.block_size as usize,
1275 &volume_header,
1276 )?;
1277 let terminal_material = match sidecar_use {
1278 BootstrapSidecarUse::SeekableAssist => Some(parse_terminal_material(
1279 bytes,
1280 terminal_offset,
1281 observed_block_count,
1282 KeyHoldingTerminalContext {
1283 subkeys: &subkeys,
1284 volume_header: &volume_header,
1285 crypto_header: &parsed_crypto.fixed,
1286 crypto_header_bytes: crypto_bytes,
1287 },
1288 options,
1289 )?),
1290 BootstrapSidecarUse::NonSeekableRandomAccess => parse_terminal_material(
1291 bytes,
1292 terminal_offset,
1293 observed_block_count,
1294 KeyHoldingTerminalContext {
1295 subkeys: &subkeys,
1296 volume_header: &volume_header,
1297 crypto_header: &parsed_crypto.fixed,
1298 crypto_header_bytes: crypto_bytes,
1299 },
1300 options,
1301 )
1302 .ok(),
1303 };
1304 let terminal_manifest = terminal_material.as_ref().map(|(manifest, _, _)| manifest);
1305 let manifest_authority = match sidecar_use {
1306 BootstrapSidecarUse::SeekableAssist => {
1307 let terminal_manifest = terminal_manifest.ok_or(FormatError::InvalidArchive(
1308 "terminal ManifestFooter/VolumeTrailer is required",
1309 ))?;
1310 if let Some(sidecar_manifest) = &sidecar.manifest_footer {
1311 if !manifest_bootstrap_fields_match(terminal_manifest, sidecar_manifest) {
1312 return Err(FormatError::InvalidArchive(
1313 "bootstrap sidecar conflicts with terminal ManifestFooter",
1314 ));
1315 }
1316 }
1317 terminal_manifest.clone()
1318 }
1319 BootstrapSidecarUse::NonSeekableRandomAccess => {
1320 let sidecar_manifest = sidecar
1321 .manifest_footer
1322 .as_ref()
1323 .ok_or(FormatError::ReaderUnsupported(
1324 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
1325 ))?;
1326 if let Some(terminal_manifest) = terminal_manifest {
1327 if !manifest_bootstrap_fields_match(terminal_manifest, sidecar_manifest) {
1328 return Err(FormatError::InvalidArchive(
1329 "bootstrap sidecar conflicts with terminal ManifestFooter",
1330 ));
1331 }
1332 }
1333 sidecar_manifest.clone()
1334 }
1335 };
1336 manifest_authority.validate_index_root_extent(parsed_crypto.fixed.block_size)?;
1337
1338 if let Some((offset, length)) = sidecar.index_root_records_section {
1339 let index_root_records = parse_sidecar_block_records(
1340 bootstrap_sidecar,
1341 parsed_crypto.fixed.block_size as usize,
1342 SidecarBlockRecordsSection {
1343 offset,
1344 length,
1345 extent: index_root_extent_from_manifest(&manifest_authority),
1346 data_kind: BlockKind::IndexRootData,
1347 parity_kind: BlockKind::IndexRootParity,
1348 structure: "IndexRoot",
1349 },
1350 )?;
1351 insert_sidecar_records(&mut blocks, index_root_records)?;
1352 }
1353
1354 let limits = metadata_limits(&parsed_crypto.fixed);
1355 let index_root_plaintext = load_metadata_object_from_parts(
1356 &blocks,
1357 ObjectLoadContext::index_root(
1358 &volume_header,
1359 &parsed_crypto.fixed,
1360 &subkeys,
1361 index_root_extent_from_manifest(&manifest_authority),
1362 ),
1363 manifest_authority.index_root_decompressed_size,
1364 )?;
1365 let index_root = IndexRoot::parse(
1366 &index_root_plaintext,
1367 parsed_crypto.fixed.has_dictionary != 0,
1368 limits,
1369 )?;
1370 if parsed_crypto.fixed.has_dictionary != 0 {
1371 if let Some((offset, length)) = sidecar.dictionary_records_section {
1372 let dictionary_records = parse_sidecar_block_records(
1373 bootstrap_sidecar,
1374 parsed_crypto.fixed.block_size as usize,
1375 SidecarBlockRecordsSection {
1376 offset,
1377 length,
1378 extent: dictionary_extent_from_index_root(&index_root)?,
1379 data_kind: BlockKind::DictionaryData,
1380 parity_kind: BlockKind::DictionaryParity,
1381 structure: "dictionary",
1382 },
1383 )?;
1384 insert_sidecar_records(&mut blocks, dictionary_records)?;
1385 }
1386 }
1387 let payload_dictionary = load_archive_dictionary(
1388 &blocks,
1389 &subkeys,
1390 &volume_header,
1391 &parsed_crypto.fixed,
1392 &index_root,
1393 )?;
1394
1395 Ok(Self {
1396 options,
1397 observed_archive_bytes,
1398 observed_volume_count: 1,
1399 subkeys,
1400 blocks,
1401 lazy_blocks: None,
1402 crypto_header_bytes: crypto_bytes.to_vec(),
1403 volume_header,
1404 crypto_header: parsed_crypto.fixed,
1405 manifest_footer: manifest_authority,
1406 volume_trailer: terminal_material
1407 .as_ref()
1408 .map(|(_, trailer, _)| trailer.clone()),
1409 root_auth_footer: terminal_material.and_then(|(_, _, root_auth)| root_auth),
1410 index_root,
1411 payload_dictionary,
1412 })
1413 }
1414
1415 pub fn list_index_entries(&self) -> Result<Vec<ArchiveIndexEntry>, FormatError> {
1421 let shards = self.load_all_index_shards()?;
1422 final_index_entry_winners(&shards)?
1423 .into_iter()
1424 .map(|(path, winner)| {
1425 Ok(ArchiveIndexEntry {
1426 path,
1427 file_data_size: winner.file_data_size,
1428 })
1429 })
1430 .collect()
1431 }
1432
1433 pub fn lookup_index_entry(&self, path: &str) -> Result<Option<ArchiveIndexEntry>, FormatError> {
1435 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
1436 self.locate_index_file(&normalized)?
1437 .map(|located| archive_index_entry_from_loaded_file(&located.shard, located.file_index))
1438 .transpose()
1439 }
1440
1441 pub fn list_files(&self) -> Result<Vec<ArchiveEntry>, FormatError> {
1442 let shards = self.load_all_index_shards()?;
1443 final_index_entry_winners(&shards)?
1444 .into_iter()
1445 .map(|(path, winner)| {
1446 let shard = &shards[winner.shard_index];
1447 let member =
1448 self.decode_loaded_owned_tar_member(shard, winner.file_index, false)?;
1449 Ok(ArchiveEntry {
1450 path,
1451 file_data_size: winner.file_data_size,
1452 kind: member.kind,
1453 mode: member.mode,
1454 mtime: member.mtime,
1455 diagnostics: member.diagnostics,
1456 })
1457 })
1458 .collect()
1459 }
1460
1461 pub fn extract_file(&self, path: &str) -> Result<Option<Vec<u8>>, FormatError> {
1468 self.extract_member(path)?
1469 .map(|member| {
1470 if member.kind != TarEntryKind::Regular {
1471 return Err(FormatError::ReaderUnsupported(
1472 "extract_file returns only regular file payloads",
1473 ));
1474 }
1475 Ok(member.data)
1476 })
1477 .transpose()
1478 }
1479
1480 pub fn extract_file_with_diagnostics(
1483 &self,
1484 path: &str,
1485 ) -> Result<Option<ExtractedRegularFile>, FormatError> {
1486 self.extract_member(path)?
1487 .map(|member| {
1488 if member.kind != TarEntryKind::Regular {
1489 return Err(FormatError::ReaderUnsupported(
1490 "extract_file_with_diagnostics returns only regular file payloads",
1491 ));
1492 }
1493 Ok((member.data, member.diagnostics))
1494 })
1495 .transpose()
1496 }
1497
1498 pub fn extract_file_to_writer<W: Write>(
1504 &self,
1505 path: &str,
1506 writer: &mut W,
1507 ) -> Result<Option<Vec<MetadataDiagnostic>>, ExtractError> {
1508 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
1509 self.locate_index_file(&normalized)?
1510 .map(|located| {
1511 self.stream_loaded_file_to_writer(&located.shard, located.file_index, writer)
1512 })
1513 .transpose()
1514 }
1515
1516 pub fn extract_member(
1517 &self,
1518 path: &str,
1519 ) -> Result<Option<ExtractedArchiveMember>, FormatError> {
1520 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
1521 self.locate_index_file(&normalized)?
1522 .map(|located| self.extract_loaded_member(&located.shard, located.file_index))
1523 .transpose()
1524 }
1525
1526 pub fn extract_file_to(
1527 &self,
1528 path: &str,
1529 root: &std::path::Path,
1530 options: SafeExtractionOptions,
1531 ) -> Result<Option<Vec<MetadataDiagnostic>>, FormatError> {
1532 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
1533 self.locate_index_file(&normalized)?
1534 .map(|located| {
1535 self.stream_loaded_file_to_path(&located.shard, located.file_index, root, options)
1536 })
1537 .transpose()
1538 }
1539
1540 pub fn extract_indexed_files_to(
1541 &self,
1542 root: &std::path::Path,
1543 options: SafeExtractionOptions,
1544 jobs: usize,
1545 ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
1546 if jobs == 0 {
1547 return Err(FormatError::ReaderUnsupported("jobs must be at least 1"));
1548 }
1549
1550 let shards = self.load_all_index_shards()?;
1551 let entries = final_index_entry_winners(&shards)?.into_iter().collect();
1552 self.extract_winning_index_entries_to(&shards, entries, root, options, jobs)
1553 }
1554
1555 pub fn verify(&self) -> Result<(), FormatError> {
1556 self.verify_content().map(|_| ())
1557 }
1558
1559 pub fn verify_content(&self) -> Result<ArchiveContentVerification<'_>, FormatError> {
1560 let tables = self.load_payload_index_tables()?;
1561 let streamed = self.scan_seekable_payload(
1562 &tables,
1563 u64::MAX,
1564 NoopTarStreamObserver,
1565 true,
1566 ParityReadPolicy::Always,
1567 )?;
1568 self.validate_streamed_payload_summary(&tables, &streamed, false, true)?;
1569 Ok(ArchiveContentVerification { archive: self })
1570 }
1571
1572 pub fn repair_patches(&self) -> Result<Vec<ArchiveRepairPatch>, FormatError> {
1573 let lazy_source = self
1574 .lazy_blocks
1575 .as_ref()
1576 .ok_or(FormatError::ReaderUnsupported(
1577 "repair output requires seekable archive input",
1578 ))?;
1579 if !lazy_source.is_complete_volume_set() {
1580 return Err(FormatError::ReaderUnsupported(
1581 "repair output requires all archive volumes",
1582 ));
1583 }
1584
1585 let shards = self.load_all_index_shards()?;
1586 let rows = self.root_auth_fec_layout_rows(&shards)?;
1587 let block_provider = self.block_provider();
1588 let mut patches = BTreeMap::<u64, ArchiveRepairPatch>::new();
1589 for row in rows.into_iter().filter(|row| row.present) {
1590 self.collect_repair_patches_for_object(
1591 &block_provider,
1592 lazy_source,
1593 row,
1594 &mut patches,
1595 )?;
1596 }
1597 Ok(patches.into_values().collect())
1598 }
1599
1600 pub fn extract_all_to(
1601 &self,
1602 root: &std::path::Path,
1603 options: SafeExtractionOptions,
1604 ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
1605 let tables = self.load_payload_index_tables()?;
1606 if final_index_entry_winners(&tables.shards)?.len() as u64 != tables.file_count {
1607 return Err(FormatError::ReaderUnsupported(
1608 FAST_FULL_EXTRACT_UNIQUE_PATHS_UNSUPPORTED,
1609 ));
1610 }
1611
1612 let observer = TarStreamFilesystemRestoreObserver::new(root, options);
1613 let streamed = self.scan_seekable_payload(
1614 &tables,
1615 total_extraction_size_cap(self.options, self.observed_archive_bytes),
1616 observer,
1617 false,
1618 ParityReadPolicy::RepairOnly,
1619 )?;
1620 self.validate_streamed_payload_summary(&tables, &streamed, true, false)?;
1621 streamed
1622 .tar
1623 .members
1624 .into_iter()
1625 .map(|member| Ok((utf8_path(&member.path)?, member.diagnostics)))
1626 .collect()
1627 }
1628
1629 fn collect_repair_patches_for_object(
1630 &self,
1631 blocks: &impl BlockProvider,
1632 source: &SeekableBlockSource,
1633 row: FecLayoutObjectRow,
1634 patches: &mut BTreeMap<u64, ArchiveRepairPatch>,
1635 ) -> Result<(), FormatError> {
1636 let (data_kind, parity_kind, data_max, parity_max) =
1637 self.fec_object_class_shape(row.object_class)?;
1638 let extent = ObjectExtent {
1639 first_block_index: row.first_block_index,
1640 data_block_count: row.data_block_count,
1641 parity_block_count: row.parity_block_count,
1642 encrypted_size: row.encrypted_size,
1643 };
1644 validate_object_extent(extent, &self.crypto_header, data_max, parity_max)?;
1645
1646 let block_size = self.crypto_header.block_size as usize;
1647 let data_count = extent.data_block_count as usize;
1648 let parity_count = extent.parity_block_count as usize;
1649 let mut data_shards = Vec::with_capacity(data_count);
1650 let mut parity_shards = Vec::with_capacity(parity_count);
1651
1652 for offset in 0..data_count {
1653 let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
1654 match blocks.block(block_index)? {
1655 Some(record) => {
1656 if record.kind != data_kind {
1657 return Err(FormatError::InvalidArchive(
1658 "object data block has unexpected kind",
1659 ));
1660 }
1661 let should_be_last = offset + 1 == data_count;
1662 if record.is_last_data() != should_be_last {
1663 return Err(FormatError::InvalidArchive(
1664 "object last-data flag is not on the final data block",
1665 ));
1666 }
1667 data_shards.push(Some(record.payload.clone()));
1668 }
1669 None => data_shards.push(None),
1670 }
1671 }
1672
1673 for offset in 0..parity_count {
1674 let block_index = checked_u64_add(
1675 extent.first_block_index,
1676 data_count as u64 + offset as u64,
1677 "object",
1678 )?;
1679 match blocks.block(block_index)? {
1680 Some(record) => {
1681 if record.kind != parity_kind {
1682 return Err(FormatError::InvalidArchive(
1683 "object parity block has unexpected kind",
1684 ));
1685 }
1686 if record.is_last_data() {
1687 return Err(FormatError::InvalidArchive(
1688 "object parity block has last-data flag",
1689 ));
1690 }
1691 parity_shards.push(Some(record.payload.clone()));
1692 }
1693 None => parity_shards.push(None),
1694 }
1695 }
1696
1697 let repaired_data = repair_data_gf16(&data_shards, &parity_shards, block_size)?;
1698 for (offset, payload) in repaired_data.iter().enumerate() {
1699 if data_shards[offset].is_none() {
1700 let block_index =
1701 checked_u64_add(extent.first_block_index, offset as u64, "object")?;
1702 let flags = if offset + 1 == data_count { 0x01 } else { 0 };
1703 self.insert_repair_patch(
1704 patches,
1705 source,
1706 block_index,
1707 data_kind,
1708 flags,
1709 payload.clone(),
1710 )?;
1711 }
1712 }
1713
1714 if parity_count > 0 {
1715 let repaired_parity = encode_parity_gf16(&repaired_data, parity_count)?;
1716 for (offset, payload) in repaired_parity.into_iter().enumerate() {
1717 if parity_shards[offset].as_ref() != Some(&payload) {
1718 let block_index = checked_u64_add(
1719 extent.first_block_index,
1720 data_count as u64 + offset as u64,
1721 "object",
1722 )?;
1723 self.insert_repair_patch(
1724 patches,
1725 source,
1726 block_index,
1727 parity_kind,
1728 0,
1729 payload,
1730 )?;
1731 }
1732 }
1733 }
1734
1735 Ok(())
1736 }
1737
1738 fn insert_repair_patch(
1739 &self,
1740 patches: &mut BTreeMap<u64, ArchiveRepairPatch>,
1741 source: &SeekableBlockSource,
1742 block_index: u64,
1743 kind: BlockKind,
1744 flags: u8,
1745 payload: Vec<u8>,
1746 ) -> Result<(), FormatError> {
1747 let (volume_index, record_offset) = source.record_location(block_index)?;
1748 let record = BlockRecord {
1749 block_index,
1750 kind,
1751 flags,
1752 payload,
1753 record_crc32c: 0,
1754 };
1755 let patch = ArchiveRepairPatch {
1756 volume_index,
1757 block_index,
1758 record_offset,
1759 record_bytes: record.to_bytes(),
1760 };
1761 if let Some(existing) = patches.insert(block_index, patch.clone()) {
1762 if existing != patch {
1763 return Err(FormatError::InvalidArchive(
1764 "conflicting repair patch for BlockRecord",
1765 ));
1766 }
1767 }
1768 Ok(())
1769 }
1770
1771 fn load_payload_index_tables(&self) -> Result<PayloadIndexTables, FormatError> {
1772 if self.index_root.header.file_count > DIRECTORY_HINT_REQUIRED_FILE_COUNT
1773 && self.index_root.directory_hint_shards.is_empty()
1774 {
1775 return Err(FormatError::InvalidArchive(
1776 "IndexRoot file_count requires directory hints",
1777 ));
1778 }
1779
1780 let shards = self.load_all_index_shards()?;
1781 let mut file_count = 0u64;
1782 let mut frames = BTreeMap::<u64, FrameEntry>::new();
1783 let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
1784
1785 for shard in &shards {
1786 file_count = file_count
1787 .checked_add(shard.files.len() as u64)
1788 .ok_or(FormatError::InvalidArchive("file count overflow"))?;
1789 for frame in &shard.frames {
1790 if let Some(existing) = frames.insert(frame.frame_index, frame.clone()) {
1791 if existing != *frame {
1792 return Err(FormatError::InvalidArchive(
1793 "duplicate FrameEntry rows do not match",
1794 ));
1795 }
1796 }
1797 }
1798 for envelope in &shard.envelopes {
1799 if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
1800 {
1801 if existing != *envelope {
1802 return Err(FormatError::InvalidArchive(
1803 "duplicate EnvelopeEntry rows do not match",
1804 ));
1805 }
1806 }
1807 }
1808 }
1809 validate_global_file_table_order(&shards)?;
1810
1811 if file_count != self.index_root.header.file_count {
1812 return Err(FormatError::InvalidArchive(
1813 "IndexRoot file_count does not match decoded shards",
1814 ));
1815 }
1816 verify_dense_keys(&frames, self.index_root.header.frame_count, "FrameEntry")?;
1817 verify_dense_keys(
1818 &envelopes,
1819 self.index_root.header.envelope_count,
1820 "EnvelopeEntry",
1821 )?;
1822 validate_envelope_frame_coverage(&frames, &envelopes)?;
1823 self.validate_encrypted_object_block_ranges(&envelopes)?;
1824
1825 let payload_block_count = envelopes.values().try_fold(0u64, |sum, envelope| {
1826 sum.checked_add(envelope.data_block_count as u64)
1827 .ok_or(FormatError::InvalidArchive("payload block count overflow"))
1828 })?;
1829 if payload_block_count != self.index_root.header.payload_block_count {
1830 return Err(FormatError::InvalidArchive(
1831 "IndexRoot payload_block_count does not match envelopes",
1832 ));
1833 }
1834
1835 Ok(PayloadIndexTables {
1836 shards,
1837 file_count,
1838 frames,
1839 envelopes,
1840 })
1841 }
1842
1843 fn scan_seekable_payload<O: TarStreamObserver>(
1844 &self,
1845 tables: &PayloadIndexTables,
1846 extraction_cap: u64,
1847 observer: O,
1848 hash_content: bool,
1849 parity_policy: ParityReadPolicy,
1850 ) -> Result<StreamedPayloadSummary, FormatError> {
1851 let mut tar = TarStreamSummaryValidator::with_observer(
1852 self.crypto_header.max_path_length,
1853 extraction_cap,
1854 usize::MAX,
1855 self.index_root.header.file_count,
1856 observer,
1857 );
1858 let mut content_hasher = hash_content.then(Sha256::new);
1859 let mut streamed_frames = Vec::with_capacity(tables.frames.len());
1860 let streamed_envelopes = tables
1861 .envelopes
1862 .values()
1863 .map(|envelope| StreamedEnvelopeSummary {
1864 envelope_index: envelope.envelope_index,
1865 first_block_index: envelope.first_block_index,
1866 data_block_count: envelope.data_block_count,
1867 parity_block_count: envelope.parity_block_count,
1868 encrypted_size: envelope.encrypted_size,
1869 plaintext_size: envelope.plaintext_size,
1870 first_frame_index: envelope.first_frame_index,
1871 frame_count: envelope.frame_count,
1872 })
1873 .collect::<Vec<_>>();
1874 let mut cached_envelope_index = None;
1875 let mut cached_envelope_plaintext = Vec::new();
1876 let mut decompressor = self.new_payload_decompressor()?;
1877
1878 for frame in tables.frames.values() {
1879 let envelope =
1880 tables
1881 .envelopes
1882 .get(&frame.envelope_index)
1883 .ok_or(FormatError::InvalidArchive(
1884 "FrameEntry references missing EnvelopeEntry",
1885 ))?;
1886 if cached_envelope_index != Some(envelope.envelope_index) {
1887 cached_envelope_plaintext = self.load_payload_envelope(envelope, parity_policy)?;
1888 cached_envelope_index = Some(envelope.envelope_index);
1889 }
1890 let compressed = slice(
1891 &cached_envelope_plaintext,
1892 frame.offset_in_envelope as usize,
1893 frame.compressed_size as usize,
1894 "FrameEntry",
1895 )?;
1896 let tar_stream_offset = tar.tar_total_size();
1897 let decoded = self.decompress_payload_frame_with(
1898 &mut decompressor,
1899 compressed,
1900 frame.decompressed_size,
1901 )?;
1902 if decoded.is_empty() {
1903 return Err(FormatError::InvalidArchive(
1904 "zstd payload frame decompressed to zero bytes",
1905 ));
1906 }
1907 if let Some(hasher) = &mut content_hasher {
1908 hasher.update(&decoded);
1909 }
1910 tar.observe(&decoded)?;
1911 streamed_frames.push(StreamedFrameSummary {
1912 frame_index: frame.frame_index,
1913 envelope_index: frame.envelope_index,
1914 offset_in_envelope: frame.offset_in_envelope,
1915 compressed_size: u32::try_from(compressed.len()).map_err(|_| {
1916 FormatError::InvalidArchive("FrameEntry.compressed_size overflow")
1917 })?,
1918 decompressed_size: u32::try_from(decoded.len()).map_err(|_| {
1919 FormatError::InvalidArchive("FrameEntry.decompressed_size overflow")
1920 })?,
1921 tar_stream_offset,
1922 });
1923 }
1924
1925 let mut content_sha256 = [0u8; 32];
1926 if let Some(hasher) = content_hasher {
1927 let digest = hasher.finalize();
1928 content_sha256.copy_from_slice(&digest);
1929 }
1930 Ok(StreamedPayloadSummary {
1931 tar: tar.finish()?,
1932 content_sha256,
1933 envelopes: streamed_envelopes,
1934 frames: streamed_frames,
1935 })
1936 }
1937
1938 fn validate_streamed_payload_summary(
1939 &self,
1940 tables: &PayloadIndexTables,
1941 streamed: &StreamedPayloadSummary,
1942 enforce_total_extraction_cap: bool,
1943 enforce_content_sha256: bool,
1944 ) -> Result<(), FormatError> {
1945 if enforce_total_extraction_cap
1946 && streamed.tar.total_extraction_size
1947 > total_extraction_size_cap(self.options, self.observed_archive_bytes)
1948 {
1949 return Err(FormatError::ReaderUnsupported(
1950 "total extraction size exceeds configured cap",
1951 ));
1952 }
1953
1954 let streamed_payload_block_count =
1955 streamed.envelopes.iter().try_fold(0u64, |sum, envelope| {
1956 sum.checked_add(envelope.data_block_count as u64)
1957 .ok_or(FormatError::InvalidArchive("payload block count overflow"))
1958 })?;
1959 if streamed_payload_block_count != self.index_root.header.payload_block_count {
1960 return Err(FormatError::InvalidArchive(
1961 "streamed payload block count does not match IndexRoot",
1962 ));
1963 }
1964
1965 if streamed.tar.tar_total_size != self.index_root.header.tar_total_size {
1966 return Err(FormatError::InvalidArchive(
1967 "IndexRoot tar_total_size does not match streamed tar stream",
1968 ));
1969 }
1970 if enforce_content_sha256
1971 && streamed.content_sha256 != self.index_root.header.content_sha256
1972 {
1973 return Err(FormatError::InvalidArchive(
1974 "IndexRoot content_sha256 does not match decoded tar stream",
1975 ));
1976 }
1977
1978 let streamed_envelopes = streamed.envelope_map()?;
1979 for envelope in tables.envelopes.values() {
1980 let actual = streamed_envelopes.get(&envelope.envelope_index).ok_or(
1981 FormatError::InvalidArchive(
1982 "metadata references missing streamed payload envelope",
1983 ),
1984 )?;
1985 if actual.first_block_index != envelope.first_block_index
1986 || actual.data_block_count != envelope.data_block_count
1987 || actual.parity_block_count != envelope.parity_block_count
1988 || actual.encrypted_size != envelope.encrypted_size
1989 || actual.plaintext_size != envelope.plaintext_size
1990 || actual.first_frame_index != envelope.first_frame_index
1991 || actual.frame_count != envelope.frame_count
1992 {
1993 return Err(FormatError::InvalidArchive(
1994 "EnvelopeEntry does not match streamed payload envelope",
1995 ));
1996 }
1997 }
1998
1999 let streamed_frames = streamed.frame_map()?;
2000 for frame in tables.frames.values() {
2001 let actual =
2002 streamed_frames
2003 .get(&frame.frame_index)
2004 .ok_or(FormatError::InvalidArchive(
2005 "metadata references missing streamed payload frame",
2006 ))?;
2007 if actual.envelope_index != frame.envelope_index
2008 || actual.offset_in_envelope != frame.offset_in_envelope
2009 || actual.compressed_size != frame.compressed_size
2010 || actual.decompressed_size != frame.decompressed_size
2011 || actual.tar_stream_offset != frame.tar_stream_offset
2012 || streamed.frame_flags(actual)? != frame.flags
2013 {
2014 return Err(FormatError::InvalidArchive(
2015 "FrameEntry does not match streamed payload frame",
2016 ));
2017 }
2018 }
2019
2020 let streamed_members = streamed.member_start_map()?;
2021 if streamed.tar.members.len() as u64 != tables.file_count {
2022 return Err(FormatError::InvalidArchive(
2023 "streamed tar member count does not match decoded shards",
2024 ));
2025 }
2026 let mut file_extents = Vec::new();
2027 let mut directory_hint_map = DirectoryHintMap::new();
2028 for (shard_row_index, shard) in tables.shards.iter().enumerate() {
2029 let shard_row_index = u32::try_from(shard_row_index)
2030 .map_err(|_| FormatError::InvalidArchive("shard row index overflow"))?;
2031 for idx in 0..shard.files.len() {
2032 let file = &shard.files[idx];
2033 let start =
2034 shard
2035 .tar_member_group_start(idx)
2036 .ok_or(FormatError::InvalidArchive(
2037 "FileEntry tar member start is missing",
2038 ))?;
2039 file_extents.push((start, file.tar_member_group_size));
2040 let path = shard
2041 .file_path(idx)
2042 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2043 let member = streamed_members
2044 .get(&start)
2045 .ok_or(FormatError::InvalidArchive(
2046 "FileEntry tar member start is missing from streamed tar",
2047 ))?;
2048 if member.path != path {
2049 return Err(FormatError::InvalidArchive(
2050 "tar member path does not match FileEntry path",
2051 ));
2052 }
2053 if member.logical_size != file.file_data_size {
2054 return Err(FormatError::InvalidArchive(
2055 "tar member size does not match FileEntry file_data_size",
2056 ));
2057 }
2058 if member.group_size != file.tar_member_group_size {
2059 return Err(FormatError::InvalidArchive(
2060 "FileEntry does not match streamed tar member",
2061 ));
2062 }
2063 add_expected_directory_hint_rows(
2064 &mut directory_hint_map,
2065 shard_row_index,
2066 path,
2067 member.kind,
2068 );
2069 }
2070 }
2071 validate_file_extent_coverage_ranges(&file_extents, self.index_root.header.tar_total_size)?;
2072 if !self.index_root.directory_hint_shards.is_empty() {
2073 let hint_tables = self.load_all_directory_hint_tables()?;
2074 validate_directory_hint_tables_against_expected(&hint_tables, &directory_hint_map)?;
2075 }
2076
2077 Ok(())
2078 }
2079
2080 pub(crate) fn from_streamed_parts(
2081 parts: StreamedArchiveOpenParts,
2082 ) -> Result<Self, FormatError> {
2083 let limits = metadata_limits(&parts.crypto_header);
2084 let index_root_plaintext = load_metadata_object_from_parts(
2085 &parts.blocks,
2086 ObjectLoadContext::index_root(
2087 &parts.volume_header,
2088 &parts.crypto_header,
2089 &parts.subkeys,
2090 ObjectExtent {
2091 first_block_index: parts.manifest_footer.index_root_first_block,
2092 data_block_count: parts.manifest_footer.index_root_data_block_count,
2093 parity_block_count: parts.manifest_footer.index_root_parity_block_count,
2094 encrypted_size: parts.manifest_footer.index_root_encrypted_size,
2095 },
2096 ),
2097 parts.manifest_footer.index_root_decompressed_size,
2098 )?;
2099 let index_root = IndexRoot::parse(
2100 &index_root_plaintext,
2101 parts.crypto_header.has_dictionary != 0,
2102 limits,
2103 )?;
2104 let payload_dictionary = load_archive_dictionary(
2105 &parts.blocks,
2106 &parts.subkeys,
2107 &parts.volume_header,
2108 &parts.crypto_header,
2109 &index_root,
2110 )?;
2111
2112 Ok(Self {
2113 options: parts.options,
2114 observed_archive_bytes: parts.observed_archive_bytes,
2115 observed_volume_count: 1,
2116 subkeys: parts.subkeys,
2117 blocks: parts.blocks,
2118 lazy_blocks: None,
2119 crypto_header_bytes: parts.crypto_header_bytes,
2120 volume_header: parts.volume_header,
2121 crypto_header: parts.crypto_header,
2122 manifest_footer: parts.manifest_footer,
2123 volume_trailer: Some(parts.volume_trailer),
2124 root_auth_footer: parts.root_auth_footer,
2125 index_root,
2126 payload_dictionary,
2127 })
2128 }
2129
2130 pub(crate) fn verify_streamed_payload_summary(
2131 &self,
2132 streamed: &StreamedPayloadSummary,
2133 ) -> Result<(), FormatError> {
2134 let tables = self.load_payload_index_tables()?;
2135 self.validate_streamed_payload_summary(&tables, streamed, true, true)
2136 }
2137
2138 pub fn verify_root_auth_with<F>(&self, verifier: F) -> Result<RootAuthVerification, FormatError>
2139 where
2140 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
2141 {
2142 let content_verification = self.verify_content()?;
2143 self.verify_root_auth_with_verified_content(&content_verification, verifier)
2144 }
2145
2146 pub fn verify_root_auth_with_verified_content<F>(
2147 &self,
2148 content_verification: &ArchiveContentVerification<'_>,
2149 mut verifier: F,
2150 ) -> Result<RootAuthVerification, FormatError>
2151 where
2152 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
2153 {
2154 if !std::ptr::eq(content_verification.archive, self) {
2155 return Err(FormatError::InvalidArchive(
2156 "content verification does not match archive",
2157 ));
2158 }
2159 let footer = self
2160 .root_auth_footer
2161 .as_ref()
2162 .ok_or(FormatError::ReaderUnsupported("root-auth footer is absent"))?;
2163 let material = self.recompute_root_auth_material(footer)?;
2164 if material.critical_metadata_digest != footer.critical_metadata_digest
2165 || material.index_digest != footer.index_digest
2166 || material.fec_layout_digest != footer.fec_layout_digest
2167 || material.data_block_merkle_root != footer.data_block_merkle_root
2168 || material.signer_identity_digest != footer.signer_identity_digest
2169 || material.archive_root != footer.archive_root
2170 || material.total_data_block_count != footer.total_data_block_count
2171 {
2172 return Err(FormatError::InvalidArchive(
2173 "RootAuthFooter commitments do not match recomputed archive root",
2174 ));
2175 }
2176 if !verifier(footer, &material.archive_root)? {
2177 return Err(FormatError::InvalidArchive(
2178 "root-auth authenticator verification failed",
2179 ));
2180 }
2181 Ok(RootAuthVerification {
2182 archive_root: material.archive_root,
2183 authenticator_id: footer.authenticator_id,
2184 signer_identity_type: footer.signer_identity_type,
2185 signer_identity_bytes: footer.signer_identity_bytes.clone(),
2186 total_data_block_count: footer.total_data_block_count,
2187 diagnostics: self.root_auth_success_diagnostics(),
2188 })
2189 }
2190
2191 fn load_all_index_shards(&self) -> Result<Vec<IndexShard>, FormatError> {
2192 parallel_map_ref(&self.index_root.shards, self.options.jobs, |entry| {
2193 self.load_index_shard(entry)
2194 })
2195 }
2196
2197 fn load_index_shard(&self, entry: &ShardEntry) -> Result<IndexShard, FormatError> {
2198 let block_provider = self.block_provider();
2199 let plaintext = load_metadata_object_from_parts(
2200 &block_provider,
2201 ObjectLoadContext::index_shard(
2202 &self.volume_header,
2203 &self.crypto_header,
2204 &self.subkeys,
2205 entry,
2206 ),
2207 entry.decompressed_size,
2208 )?;
2209 IndexShard::parse(&plaintext, entry, self.metadata_limits())
2210 }
2211
2212 fn load_all_directory_hint_tables(&self) -> Result<Vec<DirectoryHintTable>, FormatError> {
2213 parallel_map_ref(
2214 &self.index_root.directory_hint_shards,
2215 self.options.jobs,
2216 |entry| self.load_directory_hint_table(entry),
2217 )
2218 }
2219
2220 fn load_directory_hint_table(
2221 &self,
2222 entry: &DirectoryHintShardEntry,
2223 ) -> Result<DirectoryHintTable, FormatError> {
2224 let block_provider = self.block_provider();
2225 let plaintext = load_metadata_object_from_parts(
2226 &block_provider,
2227 ObjectLoadContext::directory_hint(
2228 &self.volume_header,
2229 &self.crypto_header,
2230 &self.subkeys,
2231 entry,
2232 ),
2233 entry.decompressed_size,
2234 )?;
2235 DirectoryHintTable::parse(
2236 &plaintext,
2237 entry,
2238 self.index_root.header.shard_count,
2239 self.metadata_limits(),
2240 )
2241 }
2242
2243 fn load_payload_envelope(
2244 &self,
2245 envelope: &EnvelopeEntry,
2246 parity_policy: ParityReadPolicy,
2247 ) -> Result<Vec<u8>, FormatError> {
2248 let block_provider = self.block_provider();
2249 let plaintext = load_decrypted_object_from_parts_with_parity_policy(
2250 &block_provider,
2251 ObjectLoadContext::payload(
2252 &self.volume_header,
2253 &self.crypto_header,
2254 &self.subkeys,
2255 envelope,
2256 ),
2257 parity_policy,
2258 )?;
2259 if plaintext.len() != envelope.plaintext_size as usize {
2260 return Err(FormatError::InvalidArchive(
2261 "payload envelope plaintext_size mismatch",
2262 ));
2263 }
2264 Ok(plaintext)
2265 }
2266
2267 fn locate_index_file(
2268 &self,
2269 normalized: &[u8],
2270 ) -> Result<Option<LocatedIndexFile>, FormatError> {
2271 let candidate_indexes = self
2272 .index_root
2273 .candidate_shards_for_path(normalized, self.metadata_limits())?;
2274 let mut winner: Option<LocatedIndexFile> = None;
2275
2276 for row_index in candidate_indexes {
2277 let locating =
2278 self.index_root
2279 .shards
2280 .get(row_index)
2281 .ok_or(FormatError::InvalidArchive(
2282 "candidate shard row is out of bounds",
2283 ))?;
2284 let shard = self.load_index_shard(locating)?;
2285 if let Some(file_index) = shard.lookup_file_index(normalized) {
2286 let start =
2287 shard
2288 .tar_member_group_start(file_index)
2289 .ok_or(FormatError::InvalidArchive(
2290 "FileEntry tar member start is missing",
2291 ))?;
2292 if winner
2293 .as_ref()
2294 .map(|existing| start > existing.start)
2295 .unwrap_or(true)
2296 {
2297 winner = Some(LocatedIndexFile {
2298 shard,
2299 file_index,
2300 start,
2301 });
2302 }
2303 }
2304 }
2305
2306 Ok(winner)
2307 }
2308
2309 fn extract_loaded_member(
2310 &self,
2311 shard: &IndexShard,
2312 file_index: usize,
2313 ) -> Result<ExtractedArchiveMember, FormatError> {
2314 let member = self.extract_loaded_owned_tar_member(shard, file_index)?;
2315 Ok(ExtractedArchiveMember {
2316 path: utf8_path(&member.path)?,
2317 kind: member.kind,
2318 data: member.data,
2319 link_target: member
2320 .link_target
2321 .map(|target| utf8_path(&target))
2322 .transpose()?,
2323 diagnostics: member.diagnostics,
2324 })
2325 }
2326
2327 fn extract_loaded_owned_tar_member(
2328 &self,
2329 shard: &IndexShard,
2330 file_index: usize,
2331 ) -> Result<OwnedTarMember, FormatError> {
2332 self.decode_loaded_owned_tar_member(shard, file_index, true)
2333 }
2334
2335 fn stream_loaded_file_to_writer<W: Write>(
2336 &self,
2337 shard: &IndexShard,
2338 file_index: usize,
2339 writer: &mut W,
2340 ) -> Result<Vec<MetadataDiagnostic>, ExtractError> {
2341 let file = shard
2342 .files
2343 .get(file_index)
2344 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
2345 self.validate_total_extraction_size(file.file_data_size)?;
2346 let expected_path = shard
2347 .file_path(file_index)
2348 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2349 let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
2350 stream_regular_tar_member_group_to_writer(
2351 &mut reader,
2352 expected_path,
2353 file.file_data_size,
2354 file.tar_member_group_size,
2355 self.crypto_header.max_path_length,
2356 writer,
2357 )
2358 }
2359
2360 fn stream_loaded_file_to_path(
2361 &self,
2362 shard: &IndexShard,
2363 file_index: usize,
2364 root: &std::path::Path,
2365 options: SafeExtractionOptions,
2366 ) -> Result<Vec<MetadataDiagnostic>, FormatError> {
2367 let file = shard
2368 .files
2369 .get(file_index)
2370 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
2371 self.validate_total_extraction_size(file.file_data_size)?;
2372 let expected_path = shard
2373 .file_path(file_index)
2374 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2375 let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
2376 restore_streaming_tar_member_group(
2377 root,
2378 expected_path,
2379 file.file_data_size,
2380 file.tar_member_group_size,
2381 self.crypto_header.max_path_length,
2382 options,
2383 &mut reader,
2384 )
2385 .map_err(format_error_from_extract_error)
2386 }
2387
2388 fn extract_winning_index_entries_to(
2389 &self,
2390 shards: &[IndexShard],
2391 entries: Vec<(String, WinningIndexEntry)>,
2392 root: &std::path::Path,
2393 options: SafeExtractionOptions,
2394 jobs: usize,
2395 ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
2396 if entries.is_empty() {
2397 return Ok(Vec::new());
2398 }
2399 if jobs <= 1 || entries.len() <= 1 {
2400 return entries
2401 .into_iter()
2402 .map(|(path, entry)| {
2403 let shard =
2404 shards
2405 .get(entry.shard_index)
2406 .ok_or(FormatError::InvalidArchive(
2407 "winning FileEntry shard is out of bounds",
2408 ))?;
2409 let diagnostics =
2410 self.stream_loaded_file_to_path(shard, entry.file_index, root, options)?;
2411 Ok((path, diagnostics))
2412 })
2413 .collect();
2414 }
2415
2416 let worker_count = jobs.min(entries.len());
2417 let chunk_size = entries.len().div_ceil(worker_count);
2418 std::thread::scope(|scope| {
2419 let handles = entries
2420 .chunks(chunk_size)
2421 .map(|chunk| {
2422 scope.spawn(move || {
2423 let mut out = Vec::with_capacity(chunk.len());
2424 for (path, entry) in chunk {
2425 let shard = shards.get(entry.shard_index).ok_or(
2426 FormatError::InvalidArchive(
2427 "winning FileEntry shard is out of bounds",
2428 ),
2429 )?;
2430 let diagnostics = self.stream_loaded_file_to_path(
2431 shard,
2432 entry.file_index,
2433 root,
2434 options,
2435 )?;
2436 out.push((path.clone(), diagnostics));
2437 }
2438 Ok(out)
2439 })
2440 })
2441 .collect::<Vec<_>>();
2442 let mut out = Vec::new();
2443 for handle in handles {
2444 let mut chunk = handle
2445 .join()
2446 .map_err(|_| FormatError::ReaderUnsupported("extract worker panicked"))??;
2447 out.append(&mut chunk);
2448 }
2449 Ok(out)
2450 })
2451 }
2452
2453 fn decode_loaded_owned_tar_member(
2454 &self,
2455 shard: &IndexShard,
2456 file_index: usize,
2457 enforce_extraction_cap: bool,
2458 ) -> Result<OwnedTarMember, FormatError> {
2459 let file = shard
2460 .files
2461 .get(file_index)
2462 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
2463 if enforce_extraction_cap {
2464 self.validate_total_extraction_size(file.file_data_size)?;
2465 }
2466 let expected_path = shard
2467 .file_path(file_index)
2468 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2469 let frames = frame_range_for_file(shard, file)?;
2470 let mut envelope_cache = HashMap::<u64, Vec<u8>>::new();
2471 let mut decoded = Vec::new();
2472
2473 for frame in frames {
2474 let envelope = shard
2475 .envelopes
2476 .iter()
2477 .find(|entry| entry.envelope_index == frame.envelope_index)
2478 .ok_or(FormatError::InvalidArchive(
2479 "FrameEntry references missing EnvelopeEntry",
2480 ))?;
2481 if let Entry::Vacant(entry) = envelope_cache.entry(envelope.envelope_index) {
2482 entry.insert(self.load_payload_envelope(envelope, ParityReadPolicy::RepairOnly)?);
2483 }
2484 let envelope_plaintext = envelope_cache
2485 .get(&envelope.envelope_index)
2486 .expect("inserted above");
2487 let compressed = slice(
2488 envelope_plaintext,
2489 frame.offset_in_envelope as usize,
2490 frame.compressed_size as usize,
2491 "FrameEntry",
2492 )?;
2493 decoded.extend_from_slice(
2494 &self.decompress_payload_frame(compressed, frame.decompressed_size)?,
2495 );
2496 }
2497
2498 let offset = file.offset_in_first_frame_plaintext as usize;
2499 let group_len = to_usize(file.tar_member_group_size, "FileEntry")?;
2500 let group = slice(&decoded, offset, group_len, "FileEntry")?;
2501 let member = parse_tar_member_group(group, self.crypto_header.max_path_length)?;
2502 if member.path != expected_path {
2503 return Err(FormatError::InvalidArchive(
2504 "tar member path does not match FileEntry path",
2505 ));
2506 }
2507 if member.logical_size != file.file_data_size {
2508 return Err(FormatError::InvalidArchive(
2509 "tar member size does not match FileEntry file_data_size",
2510 ));
2511 }
2512 Ok(member.to_owned_member())
2513 }
2514
2515 fn metadata_limits(&self) -> MetadataLimits {
2516 metadata_limits(&self.crypto_header)
2517 }
2518
2519 fn recompute_root_auth_material(
2520 &self,
2521 footer: &RootAuthFooterV1,
2522 ) -> Result<RootAuthMaterial, FormatError> {
2523 let footer_length = footer.footer_length()?;
2524 let root_auth_descriptor_digest = root_auth_descriptor_digest(
2525 footer.authenticator_id,
2526 footer.signer_identity_type,
2527 &footer.signer_identity_bytes,
2528 u32::try_from(footer.authenticator_value.len()).map_err(|_| {
2529 FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
2530 })?,
2531 footer_length,
2532 )?;
2533 let signer_identity_digest =
2534 signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
2535 let manifest_pre_hmac = manifest_footer_global_pre_hmac_bytes(&self.manifest_footer);
2536 let crypto_pre_hmac_len = self
2537 .crypto_header_bytes
2538 .len()
2539 .checked_sub(CRYPTO_HEADER_HMAC_LEN)
2540 .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
2541 let critical_metadata_digest = critical_metadata_digest(CriticalMetadataDigestInputs {
2542 archive_uuid: self.volume_header.archive_uuid,
2543 session_id: self.volume_header.session_id,
2544 stripe_width: self.crypto_header.stripe_width,
2545 total_volumes: self.manifest_footer.total_volumes,
2546 compression_algo: self.crypto_header.compression_algo,
2547 aead_algo: self.crypto_header.aead_algo,
2548 fec_algo: self.crypto_header.fec_algo,
2549 kdf_algo: self.crypto_header.kdf_algo,
2550 crypto_header_pre_hmac_bytes: &self.crypto_header_bytes[..crypto_pre_hmac_len],
2551 chunk_size: self.crypto_header.chunk_size,
2552 envelope_target_size: self.crypto_header.envelope_target_size,
2553 block_size: self.crypto_header.block_size,
2554 fec_data_shards: self.crypto_header.fec_data_shards,
2555 fec_parity_shards: self.crypto_header.fec_parity_shards,
2556 index_fec_data_shards: self.crypto_header.index_fec_data_shards,
2557 index_fec_parity_shards: self.crypto_header.index_fec_parity_shards,
2558 index_root_fec_data_shards: self.crypto_header.index_root_fec_data_shards,
2559 index_root_fec_parity_shards: self.crypto_header.index_root_fec_parity_shards,
2560 volume_loss_tolerance: self.crypto_header.volume_loss_tolerance,
2561 bit_rot_buffer_pct: self.crypto_header.bit_rot_buffer_pct,
2562 has_dictionary: self.crypto_header.has_dictionary,
2563 manifest_footer_global_pre_hmac_bytes: &manifest_pre_hmac,
2564 index_root_first_block: self.manifest_footer.index_root_first_block,
2565 index_root_data_block_count: self.manifest_footer.index_root_data_block_count,
2566 index_root_parity_block_count: self.manifest_footer.index_root_parity_block_count,
2567 index_root_encrypted_size: self.manifest_footer.index_root_encrypted_size,
2568 index_root_decompressed_size: self.manifest_footer.index_root_decompressed_size,
2569 root_auth_descriptor_digest,
2570 })?;
2571 let index_root_plaintext = self.index_root.to_bytes();
2572 let index_digest = index_digest(&index_root_plaintext);
2573 let shards = self.load_all_index_shards()?;
2574 let fec_layout_rows = self.root_auth_fec_layout_rows(&shards)?;
2575 let fec_layout_digest = fec_layout_digest(&fec_layout_rows)?;
2576 let data_leaves = self.root_auth_data_block_leaves(&fec_layout_rows)?;
2577 let total_data_block_count = u64::try_from(data_leaves.len())
2578 .map_err(|_| FormatError::InvalidArchive("root-auth data block count overflow"))?;
2579 let data_block_merkle_root = data_block_merkle_root(&data_leaves);
2580 let archive_root = archive_root(ArchiveRootInputs {
2581 archive_uuid: self.volume_header.archive_uuid,
2582 session_id: self.volume_header.session_id,
2583 format_version: FORMAT_VERSION,
2584 volume_format_rev: VOLUME_FORMAT_REV,
2585 compression_algo: self.crypto_header.compression_algo,
2586 aead_algo: self.crypto_header.aead_algo,
2587 fec_algo: self.crypto_header.fec_algo,
2588 kdf_algo: self.crypto_header.kdf_algo,
2589 critical_metadata_digest,
2590 index_digest,
2591 fec_layout_digest,
2592 total_data_block_count,
2593 data_block_merkle_root,
2594 root_auth_descriptor_digest,
2595 signer_identity_digest,
2596 });
2597 Ok(RootAuthMaterial {
2598 critical_metadata_digest,
2599 index_digest,
2600 fec_layout_digest,
2601 data_block_merkle_root,
2602 signer_identity_digest,
2603 archive_root,
2604 total_data_block_count,
2605 })
2606 }
2607
2608 fn root_auth_fec_layout_rows(
2609 &self,
2610 shards: &[IndexShard],
2611 ) -> Result<Vec<FecLayoutObjectRow>, FormatError> {
2612 let mut rows = Vec::new();
2613 rows.push(FecLayoutObjectRow {
2614 object_class: 1,
2615 present: true,
2616 object_id: 0,
2617 first_block_index: self.manifest_footer.index_root_first_block,
2618 data_block_count: self.manifest_footer.index_root_data_block_count,
2619 parity_block_count: self.manifest_footer.index_root_parity_block_count,
2620 encrypted_size: self.manifest_footer.index_root_encrypted_size,
2621 plain_size: self.manifest_footer.index_root_decompressed_size,
2622 });
2623 if self.crypto_header.has_dictionary != 0 {
2624 rows.push(FecLayoutObjectRow {
2625 object_class: 2,
2626 present: true,
2627 object_id: 0,
2628 first_block_index: self.index_root.header.dictionary_first_block,
2629 data_block_count: self.index_root.header.dictionary_data_block_count,
2630 parity_block_count: self.index_root.header.dictionary_parity_block_count,
2631 encrypted_size: self.index_root.header.dictionary_encrypted_size,
2632 plain_size: self.index_root.header.dictionary_decompressed_size,
2633 });
2634 } else {
2635 rows.push(FecLayoutObjectRow {
2636 object_class: 2,
2637 present: false,
2638 object_id: 0,
2639 first_block_index: 0,
2640 data_block_count: 0,
2641 parity_block_count: 0,
2642 encrypted_size: 0,
2643 plain_size: 0,
2644 });
2645 }
2646 for entry in &self.index_root.shards {
2647 rows.push(FecLayoutObjectRow {
2648 object_class: 3,
2649 present: true,
2650 object_id: entry.shard_index,
2651 first_block_index: entry.first_block_index,
2652 data_block_count: entry.data_block_count,
2653 parity_block_count: entry.parity_block_count,
2654 encrypted_size: entry.encrypted_size,
2655 plain_size: entry.decompressed_size,
2656 });
2657 }
2658 let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
2659 for shard in shards {
2660 for envelope in &shard.envelopes {
2661 if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
2662 {
2663 if existing != *envelope {
2664 return Err(FormatError::InvalidArchive(
2665 "duplicate EnvelopeEntry rows do not match",
2666 ));
2667 }
2668 }
2669 }
2670 }
2671 for envelope in envelopes.values() {
2672 rows.push(FecLayoutObjectRow {
2673 object_class: 4,
2674 present: true,
2675 object_id: envelope.envelope_index,
2676 first_block_index: envelope.first_block_index,
2677 data_block_count: envelope.data_block_count,
2678 parity_block_count: envelope.parity_block_count,
2679 encrypted_size: envelope.encrypted_size,
2680 plain_size: envelope.plaintext_size,
2681 });
2682 }
2683 for entry in &self.index_root.directory_hint_shards {
2684 rows.push(FecLayoutObjectRow {
2685 object_class: 5,
2686 present: true,
2687 object_id: entry.hint_shard_index,
2688 first_block_index: entry.first_block_index,
2689 data_block_count: entry.data_block_count,
2690 parity_block_count: entry.parity_block_count,
2691 encrypted_size: entry.encrypted_size,
2692 plain_size: entry.decompressed_size,
2693 });
2694 }
2695 Ok(rows)
2696 }
2697
2698 fn fec_object_class_shape(
2699 &self,
2700 object_class: u8,
2701 ) -> Result<(BlockKind, BlockKind, u16, u16), FormatError> {
2702 match object_class {
2703 1 => Ok((
2704 BlockKind::IndexRootData,
2705 BlockKind::IndexRootParity,
2706 self.crypto_header.index_root_fec_data_shards,
2707 self.crypto_header.index_root_fec_parity_shards,
2708 )),
2709 2 => Ok((
2710 BlockKind::DictionaryData,
2711 BlockKind::DictionaryParity,
2712 self.crypto_header.index_root_fec_data_shards,
2713 self.crypto_header.index_root_fec_parity_shards,
2714 )),
2715 3 => Ok((
2716 BlockKind::IndexShardData,
2717 BlockKind::IndexShardParity,
2718 self.crypto_header.index_fec_data_shards,
2719 self.crypto_header.index_fec_parity_shards,
2720 )),
2721 4 => Ok((
2722 BlockKind::PayloadData,
2723 BlockKind::PayloadParity,
2724 self.crypto_header.fec_data_shards,
2725 self.crypto_header.fec_parity_shards,
2726 )),
2727 5 => Ok((
2728 BlockKind::DirectoryHintData,
2729 BlockKind::DirectoryHintParity,
2730 self.crypto_header.index_fec_data_shards,
2731 self.crypto_header.index_fec_parity_shards,
2732 )),
2733 _ => Err(FormatError::InvalidArchive(
2734 "unknown root-auth FEC row class",
2735 )),
2736 }
2737 }
2738
2739 fn root_auth_data_block_leaves(
2740 &self,
2741 rows: &[FecLayoutObjectRow],
2742 ) -> Result<Vec<DataBlockMerkleLeaf>, FormatError> {
2743 let block_provider = self.block_provider();
2744 let present_rows = rows.iter().filter(|row| row.present).collect::<Vec<_>>();
2745 let chunks = parallel_map_ref(&present_rows, self.options.jobs, |row| {
2746 let row = **row;
2747 let (data_kind, parity_kind, data_max, parity_max) =
2748 self.fec_object_class_shape(row.object_class)?;
2749 let extent = ObjectExtent {
2750 first_block_index: row.first_block_index,
2751 data_block_count: row.data_block_count,
2752 parity_block_count: row.parity_block_count,
2753 encrypted_size: row.encrypted_size,
2754 };
2755 let repaired = load_repaired_object_data_shards_from_parts(
2756 &block_provider,
2757 &self.crypto_header,
2758 extent,
2759 data_kind,
2760 parity_kind,
2761 data_max,
2762 parity_max,
2763 )?;
2764 let mut leaves = Vec::new();
2765 for (offset, payload) in repaired.into_iter().enumerate() {
2766 leaves.push(DataBlockMerkleLeaf {
2767 block_index: checked_u64_add(
2768 row.first_block_index,
2769 offset as u64,
2770 "root-auth data block",
2771 )?,
2772 kind: data_kind,
2773 flags: if offset + 1 == row.data_block_count as usize {
2774 0x01
2775 } else {
2776 0
2777 },
2778 payload,
2779 });
2780 }
2781 Ok(leaves)
2782 })?;
2783 let mut leaves = Vec::new();
2784 for mut chunk in chunks {
2785 leaves.append(&mut chunk);
2786 }
2787 leaves.sort_by_key(|leaf| leaf.block_index);
2788 Ok(leaves)
2789 }
2790
2791 fn validate_total_extraction_size(&self, logical_size: u64) -> Result<(), FormatError> {
2792 let cap = total_extraction_size_cap(self.options, self.observed_archive_bytes);
2793 if logical_size > cap {
2794 return Err(FormatError::ReaderUnsupported(
2795 "total extraction size exceeds configured cap",
2796 ));
2797 }
2798 Ok(())
2799 }
2800
2801 fn decompress_payload_frame(
2802 &self,
2803 compressed: &[u8],
2804 decompressed_size: u32,
2805 ) -> Result<Vec<u8>, FormatError> {
2806 let mut decompressor = self.new_payload_decompressor()?;
2807 self.decompress_payload_frame_with(&mut decompressor, compressed, decompressed_size)
2808 }
2809
2810 fn new_payload_decompressor(&self) -> Result<zstd::bulk::Decompressor<'static>, FormatError> {
2811 match &self.payload_dictionary {
2812 Some(dictionary) => zstd::bulk::Decompressor::with_dictionary(dictionary),
2813 None => zstd::bulk::Decompressor::new(),
2814 }
2815 .map_err(|_| FormatError::ZstdDecompressionFailure)
2816 }
2817
2818 fn decompress_payload_frame_with(
2819 &self,
2820 decompressor: &mut zstd::bulk::Decompressor<'static>,
2821 compressed: &[u8],
2822 decompressed_size: u32,
2823 ) -> Result<Vec<u8>, FormatError> {
2824 validate_exact_zstd_frame(compressed)?;
2825 let expected = decompressed_size as usize;
2826 let decoded = decompressor
2827 .decompress(compressed, expected)
2828 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
2829 if decoded.len() != expected {
2830 return Err(FormatError::ZstdDecompressedSizeMismatch {
2831 expected,
2832 actual: decoded.len(),
2833 });
2834 }
2835 Ok(decoded)
2836 }
2837
2838 fn validate_encrypted_object_block_ranges(
2839 &self,
2840 envelopes: &BTreeMap<u64, EnvelopeEntry>,
2841 ) -> Result<(), FormatError> {
2842 let mut ranges = Vec::new();
2843 ranges.push(object_block_range(
2844 self.manifest_footer.index_root_first_block,
2845 self.manifest_footer.index_root_data_block_count,
2846 self.manifest_footer.index_root_parity_block_count,
2847 "IndexRoot",
2848 )?);
2849 for shard in &self.index_root.shards {
2850 ranges.push(object_block_range(
2851 shard.first_block_index,
2852 shard.data_block_count,
2853 shard.parity_block_count,
2854 "IndexShard",
2855 )?);
2856 }
2857 for hint in &self.index_root.directory_hint_shards {
2858 ranges.push(object_block_range(
2859 hint.first_block_index,
2860 hint.data_block_count,
2861 hint.parity_block_count,
2862 "DirectoryHintShardEntry",
2863 )?);
2864 }
2865 if self.crypto_header.has_dictionary != 0 {
2866 ranges.push(object_block_range(
2867 self.index_root.header.dictionary_first_block,
2868 self.index_root.header.dictionary_data_block_count,
2869 self.index_root.header.dictionary_parity_block_count,
2870 "dictionary",
2871 )?);
2872 }
2873 for envelope in envelopes.values() {
2874 ranges.push(object_block_range(
2875 envelope.first_block_index,
2876 envelope.data_block_count,
2877 envelope.parity_block_count,
2878 "EnvelopeEntry",
2879 )?);
2880 }
2881 validate_non_overlapping_object_ranges(&mut ranges)?;
2882 if let Some(source) = &self.lazy_blocks {
2883 if source.is_complete_volume_set() {
2884 validate_exact_coverage_ranges_u64(
2885 &mut ranges,
2886 source.total_block_count()?,
2887 "encrypted object block ranges do not cover complete archive exactly",
2888 )?;
2889 }
2890 }
2891 Ok(())
2892 }
2893}
2894
2895impl<'a> DecodedTarMemberGroupReader<'a> {
2896 fn new(
2897 archive: &'a OpenedArchive,
2898 shard: &'a IndexShard,
2899 file: &'a FileEntry,
2900 ) -> Result<Self, FormatError> {
2901 Ok(Self {
2902 archive,
2903 shard,
2904 file,
2905 decompressor: archive.new_payload_decompressor()?,
2906 next_frame_offset: 0,
2907 cached_envelope_index: None,
2908 cached_envelope_plaintext: Vec::new(),
2909 current_frame: Vec::new(),
2910 current_frame_offset: 0,
2911 remaining_group_bytes: file.tar_member_group_size,
2912 })
2913 }
2914
2915 fn ensure_frame_available(&mut self) -> Result<(), ExtractError> {
2916 while self.current_frame_offset >= self.current_frame.len() {
2917 if self.next_frame_offset >= self.file.frame_count as u64 {
2918 return Err(
2919 FormatError::InvalidArchive("tar member group exceeds frame range").into(),
2920 );
2921 }
2922 let frame_index = self
2923 .file
2924 .first_frame_index
2925 .checked_add(self.next_frame_offset)
2926 .ok_or(FormatError::InvalidArchive(
2927 "FileEntry frame range overflow",
2928 ))?;
2929 let frame = frame_by_index(self.shard, frame_index)?;
2930 let envelope = envelope_by_index(self.shard, frame.envelope_index)?;
2931 if self.cached_envelope_index != Some(envelope.envelope_index) {
2932 self.cached_envelope_plaintext = self
2933 .archive
2934 .load_payload_envelope(envelope, ParityReadPolicy::RepairOnly)?;
2935 self.cached_envelope_index = Some(envelope.envelope_index);
2936 }
2937 let compressed = slice(
2938 &self.cached_envelope_plaintext,
2939 frame.offset_in_envelope as usize,
2940 frame.compressed_size as usize,
2941 "FrameEntry",
2942 )?;
2943 let decoded = self.archive.decompress_payload_frame_with(
2944 &mut self.decompressor,
2945 compressed,
2946 frame.decompressed_size,
2947 )?;
2948 let offset = if self.next_frame_offset == 0 {
2949 self.file.offset_in_first_frame_plaintext as usize
2950 } else {
2951 0
2952 };
2953 if offset > decoded.len() {
2954 return Err(FormatError::InvalidArchive(
2955 "offset in first frame is outside the first referenced frame",
2956 )
2957 .into());
2958 }
2959 self.next_frame_offset += 1;
2960 self.current_frame = decoded;
2961 self.current_frame_offset = offset;
2962 }
2963 Ok(())
2964 }
2965}
2966
2967impl TarMemberGroupReader for DecodedTarMemberGroupReader<'_> {
2968 fn read_some_member_bytes(&mut self, buf: &mut [u8]) -> Result<usize, ExtractError> {
2969 if buf.is_empty() {
2970 return Ok(0);
2971 }
2972 if self.remaining_group_bytes == 0 {
2973 return Ok(0);
2974 }
2975 self.ensure_frame_available()?;
2976 let available = self.current_frame.len() - self.current_frame_offset;
2977 let len = available
2978 .min(buf.len())
2979 .min(to_usize(self.remaining_group_bytes, "FileEntry")?);
2980 if len == 0 {
2981 return Err(FormatError::InvalidArchive("tar member group exceeds frame range").into());
2982 }
2983 buf[..len].copy_from_slice(
2984 &self.current_frame[self.current_frame_offset..self.current_frame_offset + len],
2985 );
2986 self.current_frame_offset += len;
2987 self.remaining_group_bytes -= len as u64;
2988 Ok(len)
2989 }
2990}
2991
2992fn frame_by_index(shard: &IndexShard, frame_index: u64) -> Result<&FrameEntry, FormatError> {
2993 shard
2994 .frames
2995 .binary_search_by_key(&frame_index, |entry| entry.frame_index)
2996 .map(|idx| &shard.frames[idx])
2997 .map_err(|_| FormatError::InvalidArchive("FileEntry references missing FrameEntry"))
2998}
2999
3000fn envelope_by_index(
3001 shard: &IndexShard,
3002 envelope_index: u64,
3003) -> Result<&EnvelopeEntry, FormatError> {
3004 shard
3005 .envelopes
3006 .binary_search_by_key(&envelope_index, |entry| entry.envelope_index)
3007 .map(|idx| &shard.envelopes[idx])
3008 .map_err(|_| FormatError::InvalidArchive("FrameEntry references missing EnvelopeEntry"))
3009}
3010
3011fn format_error_from_extract_error(err: ExtractError) -> FormatError {
3012 match err {
3013 ExtractError::Format(err) => err,
3014 ExtractError::Output(_) => {
3015 FormatError::FilesystemExtractionFailed("failed to write regular file")
3016 }
3017 }
3018}
3019
3020fn final_index_entry_winners(
3021 shards: &[IndexShard],
3022) -> Result<BTreeMap<String, WinningIndexEntry>, FormatError> {
3023 let mut final_entries = BTreeMap::<String, WinningIndexEntry>::new();
3024 for (shard_index, shard) in shards.iter().enumerate() {
3025 for (idx, file) in shard.files.iter().enumerate() {
3026 let path = utf8_path(
3027 shard
3028 .file_path(idx)
3029 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
3030 )?;
3031 let start = shard
3032 .tar_member_group_start(idx)
3033 .ok_or(FormatError::InvalidArchive(
3034 "FileEntry tar member start is missing",
3035 ))?;
3036 if let Some(winner) = final_entries.get_mut(&path) {
3037 if start >= winner.start {
3038 winner.start = start;
3039 winner.file_data_size = file.file_data_size;
3040 winner.shard_index = shard_index;
3041 winner.file_index = idx;
3042 }
3043 } else {
3044 final_entries.insert(
3045 path,
3046 WinningIndexEntry {
3047 start,
3048 file_data_size: file.file_data_size,
3049 shard_index,
3050 file_index: idx,
3051 },
3052 );
3053 }
3054 }
3055 }
3056 Ok(final_entries)
3057}
3058
3059fn archive_index_entry_from_loaded_file(
3060 shard: &IndexShard,
3061 file_index: usize,
3062) -> Result<ArchiveIndexEntry, FormatError> {
3063 let file = shard
3064 .files
3065 .get(file_index)
3066 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3067 let path = utf8_path(
3068 shard
3069 .file_path(file_index)
3070 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
3071 )?;
3072 Ok(ArchiveIndexEntry {
3073 path,
3074 file_data_size: file.file_data_size,
3075 })
3076}
3077
3078#[derive(Debug)]
3079struct ParsedSeekableVolume {
3080 volume_header: VolumeHeader,
3081 crypto_header: CryptoHeaderFixed,
3082 crypto_header_bytes: Vec<u8>,
3083 subkeys: Subkeys,
3084 manifest_footer: Option<ManifestFooter>,
3085 manifest_footer_error: Option<FormatError>,
3086 root_auth_footer: Option<RootAuthFooterV1>,
3087 root_auth_footer_bytes: Option<Vec<u8>>,
3088 volume_trailer: VolumeTrailer,
3089 blocks: BTreeMap<u64, BlockRecord>,
3090 erased_block_indices: BTreeSet<u64>,
3091}
3092
3093struct ParsedSeekableReadAtVolume {
3094 reader: Arc<dyn ArchiveReadAt>,
3095 volume_header: VolumeHeader,
3096 crypto_header: CryptoHeaderFixed,
3097 crypto_header_bytes: Vec<u8>,
3098 subkeys: Subkeys,
3099 manifest_footer: Option<ManifestFooter>,
3100 manifest_footer_error: Option<FormatError>,
3101 root_auth_footer: Option<RootAuthFooterV1>,
3102 root_auth_footer_bytes: Option<Vec<u8>>,
3103 volume_trailer: VolumeTrailer,
3104 crypto_end: u64,
3105}
3106
3107fn parse_seekable_volume(
3108 bytes: &[u8],
3109 master_key: &MasterKey,
3110 options: ReaderOptions,
3111) -> Result<ParsedSeekableVolume, FormatError> {
3112 if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
3113 return Err(FormatError::InvalidLength {
3114 structure: "archive",
3115 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
3116 actual: bytes.len(),
3117 });
3118 }
3119
3120 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
3121 let crypto_start = volume_header.crypto_header_offset as usize;
3122 let crypto_len = volume_header.crypto_header_length as usize;
3123 let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
3124 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
3125 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
3126 let subkeys = subkeys_for_open(
3127 Some(master_key),
3128 parsed_crypto.fixed.aead_algo,
3129 &volume_header.archive_uuid,
3130 &volume_header.session_id,
3131 )?;
3132 verify_integrity_tag(
3133 HmacDomain::CryptoHeader,
3134 parsed_crypto.fixed.aead_algo,
3135 Some(&subkeys.mac_key),
3136 &volume_header.archive_uuid,
3137 &volume_header.session_id,
3138 parsed_crypto.hmac_covered_bytes,
3139 &parsed_crypto.header_hmac,
3140 )?;
3141 parsed_crypto.validate_extension_semantics()?;
3142 validate_seekable_supported_volume(
3143 &volume_header,
3144 &parsed_crypto.fixed,
3145 &parsed_crypto.extensions,
3146 )?;
3147 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
3148
3149 let terminal = locate_v41_terminal(
3150 bytes,
3151 KeyHoldingTerminalContext {
3152 subkeys: &subkeys,
3153 volume_header: &volume_header,
3154 crypto_header: &parsed_crypto.fixed,
3155 crypto_header_bytes: crypto_bytes,
3156 },
3157 options,
3158 )?;
3159 let trailer_offset = to_usize(terminal.image.volume_trailer_offset, "VolumeTrailer")?;
3160 let volume_trailer = terminal.volume_trailer.clone();
3161 validate_trailer_identity(&volume_header, &volume_trailer)?;
3162
3163 let manifest_offset = to_usize(volume_trailer.manifest_footer_offset, "ManifestFooter")?;
3164 let manifest_end = checked_add(manifest_offset, MANIFEST_FOOTER_LEN, "ManifestFooter")?;
3165 if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
3166 if to_usize(volume_trailer.root_auth_footer_offset, "RootAuthFooter")? != manifest_end
3167 || volume_trailer
3168 .root_auth_footer_offset
3169 .checked_add(volume_trailer.root_auth_footer_length as u64)
3170 .ok_or(FormatError::InvalidArchive(
3171 "RootAuthFooter terminal boundary overflow",
3172 ))?
3173 != trailer_offset as u64
3174 {
3175 return Err(FormatError::InvalidArchive(
3176 "RootAuthFooter does not sit before selected trailer",
3177 ));
3178 }
3179 } else if manifest_end != trailer_offset {
3180 return Err(FormatError::InvalidArchive(
3181 "ManifestFooter does not end at selected trailer",
3182 ));
3183 }
3184 let manifest_bytes = &terminal.manifest_footer_bytes;
3185 let (manifest_footer, manifest_footer_error) = match parse_valid_manifest_footer(
3186 &volume_header,
3187 &parsed_crypto.fixed,
3188 &subkeys,
3189 manifest_bytes,
3190 ) {
3191 Ok(footer) => (Some(footer), None),
3192 Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
3193 Err(err) => return Err(err),
3194 };
3195
3196 let block_region = parse_block_region(
3197 bytes,
3198 crypto_end,
3199 manifest_offset,
3200 parsed_crypto.fixed.block_size as usize,
3201 &volume_header,
3202 &volume_trailer,
3203 )?;
3204
3205 Ok(ParsedSeekableVolume {
3206 volume_header,
3207 crypto_header: parsed_crypto.fixed,
3208 crypto_header_bytes: crypto_bytes.to_vec(),
3209 subkeys,
3210 manifest_footer,
3211 manifest_footer_error,
3212 root_auth_footer: terminal.root_auth_footer,
3213 root_auth_footer_bytes: terminal.root_auth_footer_bytes,
3214 volume_trailer,
3215 blocks: block_region.blocks,
3216 erased_block_indices: block_region.erased_block_indices,
3217 })
3218}
3219
3220fn parse_seekable_read_at_volume(
3221 reader: Arc<dyn ArchiveReadAt>,
3222 master_key: &MasterKey,
3223 options: ReaderOptions,
3224) -> Result<ParsedSeekableReadAtVolume, FormatError> {
3225 let observed_len = reader.len()?;
3226 if observed_len < (VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN) as u64 {
3227 return Err(FormatError::InvalidLength {
3228 structure: "archive",
3229 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
3230 actual: to_usize(observed_len, "archive")?,
3231 });
3232 }
3233
3234 let volume_header_bytes = read_at_vec(reader.as_ref(), 0, VOLUME_HEADER_LEN, "archive")?;
3235 let volume_header = VolumeHeader::parse(&volume_header_bytes)?;
3236 let crypto_start = volume_header.crypto_header_offset as u64;
3237 let crypto_len = volume_header.crypto_header_length as u64;
3238 let crypto_end = checked_u64_add(crypto_start, crypto_len, "CryptoHeader")?;
3239 let crypto_bytes = read_at_vec(
3240 reader.as_ref(),
3241 crypto_start,
3242 to_usize(crypto_len, "CryptoHeader")?,
3243 "CryptoHeader",
3244 )?;
3245 let parsed_crypto = CryptoHeader::parse(&crypto_bytes, volume_header.crypto_header_length)?;
3246 let subkeys = subkeys_for_open(
3247 Some(master_key),
3248 parsed_crypto.fixed.aead_algo,
3249 &volume_header.archive_uuid,
3250 &volume_header.session_id,
3251 )?;
3252 verify_integrity_tag(
3253 HmacDomain::CryptoHeader,
3254 parsed_crypto.fixed.aead_algo,
3255 Some(&subkeys.mac_key),
3256 &volume_header.archive_uuid,
3257 &volume_header.session_id,
3258 parsed_crypto.hmac_covered_bytes,
3259 &parsed_crypto.header_hmac,
3260 )?;
3261 parsed_crypto.validate_extension_semantics()?;
3262 validate_seekable_supported_volume(
3263 &volume_header,
3264 &parsed_crypto.fixed,
3265 &parsed_crypto.extensions,
3266 )?;
3267 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
3268
3269 let terminal = locate_v41_terminal_read_at(
3270 reader.as_ref(),
3271 observed_len,
3272 KeyHoldingTerminalContext {
3273 subkeys: &subkeys,
3274 volume_header: &volume_header,
3275 crypto_header: &parsed_crypto.fixed,
3276 crypto_header_bytes: &crypto_bytes,
3277 },
3278 options,
3279 )?;
3280 let volume_trailer = terminal.volume_trailer.clone();
3281 validate_trailer_identity(&volume_header, &volume_trailer)?;
3282
3283 let manifest_offset = volume_trailer.manifest_footer_offset;
3284 let manifest_end = checked_u64_add(
3285 manifest_offset,
3286 MANIFEST_FOOTER_LEN as u64,
3287 "ManifestFooter",
3288 )?;
3289 if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
3290 if volume_trailer.root_auth_footer_offset != manifest_end
3291 || volume_trailer
3292 .root_auth_footer_offset
3293 .checked_add(volume_trailer.root_auth_footer_length as u64)
3294 .ok_or(FormatError::InvalidArchive(
3295 "RootAuthFooter terminal boundary overflow",
3296 ))?
3297 != terminal.image.volume_trailer_offset
3298 {
3299 return Err(FormatError::InvalidArchive(
3300 "RootAuthFooter does not sit before selected trailer",
3301 ));
3302 }
3303 } else if manifest_end != terminal.image.volume_trailer_offset {
3304 return Err(FormatError::InvalidArchive(
3305 "ManifestFooter does not end at selected trailer",
3306 ));
3307 }
3308 validate_seekable_block_region_layout(
3309 crypto_end,
3310 manifest_offset,
3311 parsed_crypto.fixed.block_size as usize,
3312 &volume_trailer,
3313 )?;
3314
3315 let manifest_bytes = &terminal.manifest_footer_bytes;
3316 let (manifest_footer, manifest_footer_error) = match parse_valid_manifest_footer(
3317 &volume_header,
3318 &parsed_crypto.fixed,
3319 &subkeys,
3320 manifest_bytes,
3321 ) {
3322 Ok(footer) => (Some(footer), None),
3323 Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
3324 Err(err) => return Err(err),
3325 };
3326
3327 Ok(ParsedSeekableReadAtVolume {
3328 reader,
3329 volume_header,
3330 crypto_header: parsed_crypto.fixed,
3331 crypto_header_bytes: crypto_bytes,
3332 subkeys,
3333 manifest_footer,
3334 manifest_footer_error,
3335 root_auth_footer: terminal.root_auth_footer,
3336 root_auth_footer_bytes: terminal.root_auth_footer_bytes,
3337 volume_trailer,
3338 crypto_end,
3339 })
3340}
3341
3342#[derive(Debug)]
3343struct ParsedPublicNoKeyVolume {
3344 volume_header: VolumeHeader,
3345 crypto_header: CryptoHeaderFixed,
3346 root_auth_footer: RootAuthFooterV1,
3347 root_auth_footer_bytes: Vec<u8>,
3348 blocks: BTreeMap<u64, BlockRecord>,
3349}
3350
3351pub fn public_no_key_verify_volumes_with_options<F>(
3352 volumes: &[&[u8]],
3353 mut verifier: F,
3354 options: ReaderOptions,
3355) -> Result<PublicNoKeyVerification, FormatError>
3356where
3357 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
3358{
3359 validate_reader_options(options)?;
3360 if volumes.is_empty() {
3361 return Err(FormatError::InvalidArchive("no volumes supplied"));
3362 }
3363 let mut parsed = Vec::with_capacity(volumes.len());
3364 for volume in volumes {
3365 parsed.push(parse_public_no_key_volume(volume, options)?);
3366 }
3367 let first = parsed
3368 .first()
3369 .ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
3370 if parsed.len() != first.crypto_header.stripe_width as usize {
3371 return Err(FormatError::ReaderUnsupported(
3372 "public no-key verification requires a complete volume set",
3373 ));
3374 }
3375
3376 let mut seen_volume_indexes = BTreeSet::new();
3377 let mut blocks = BTreeMap::new();
3378 for volume in &parsed {
3379 if volume.volume_header.archive_uuid != first.volume_header.archive_uuid
3380 || volume.volume_header.session_id != first.volume_header.session_id
3381 || !public_crypto_headers_agree(&volume.crypto_header, &first.crypto_header)
3382 {
3383 return Err(FormatError::InvalidArchive(
3384 "public no-key volume global metadata differs",
3385 ));
3386 }
3387 if volume.root_auth_footer_bytes != first.root_auth_footer_bytes {
3388 return Err(FormatError::InvalidArchive(
3389 "public no-key RootAuthFooter copies differ",
3390 ));
3391 }
3392 if !seen_volume_indexes.insert(volume.volume_header.volume_index) {
3393 return Err(FormatError::InvalidArchive(
3394 "duplicate public no-key volume index",
3395 ));
3396 }
3397 for (block_index, record) in &volume.blocks {
3398 if blocks.insert(*block_index, record.clone()).is_some() {
3399 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
3400 }
3401 }
3402 }
3403 validate_complete_global_block_coverage(&blocks, &BTreeSet::new())?;
3404
3405 let footer = &first.root_auth_footer;
3406 let mut data_leaves = blocks
3407 .values()
3408 .filter(|record| record.kind.is_data())
3409 .map(|record| DataBlockMerkleLeaf {
3410 block_index: record.block_index,
3411 kind: record.kind,
3412 flags: record.flags,
3413 payload: record.payload.clone(),
3414 })
3415 .collect::<Vec<_>>();
3416 data_leaves.sort_by_key(|leaf| leaf.block_index);
3417 let total_data_block_count = u64::try_from(data_leaves.len())
3418 .map_err(|_| FormatError::InvalidArchive("public no-key data block count overflow"))?;
3419 let observed_data_root = data_block_merkle_root(&data_leaves);
3420 if total_data_block_count != footer.total_data_block_count
3421 || observed_data_root != footer.data_block_merkle_root
3422 {
3423 return Err(FormatError::InvalidArchive(
3424 "public no-key data-block commitment mismatch",
3425 ));
3426 }
3427 let archive_root = recompute_public_archive_root(footer, &first.crypto_header)?;
3428 if archive_root != footer.archive_root {
3429 return Err(FormatError::InvalidArchive(
3430 "public no-key archive_root mismatch",
3431 ));
3432 }
3433 if !verifier(footer, &archive_root)? {
3434 return Err(FormatError::InvalidArchive(
3435 "public no-key authenticator verification failed",
3436 ));
3437 }
3438 Ok(PublicNoKeyVerification {
3439 archive_root,
3440 authenticator_id: footer.authenticator_id,
3441 signer_identity_type: footer.signer_identity_type,
3442 signer_identity_bytes: footer.signer_identity_bytes.clone(),
3443 total_data_block_count,
3444 diagnostics: vec![
3445 PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
3446 PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
3447 PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
3448 ],
3449 })
3450}
3451
3452fn parse_public_no_key_volume(
3453 bytes: &[u8],
3454 options: ReaderOptions,
3455) -> Result<ParsedPublicNoKeyVolume, FormatError> {
3456 if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
3457 return Err(FormatError::InvalidLength {
3458 structure: "archive",
3459 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
3460 actual: bytes.len(),
3461 });
3462 }
3463 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
3464 let crypto_start = volume_header.crypto_header_offset as usize;
3465 let crypto_len = volume_header.crypto_header_length as usize;
3466 let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
3467 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
3468 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
3469 parsed_crypto.validate_extension_semantics()?;
3470 validate_seekable_supported_volume(
3471 &volume_header,
3472 &parsed_crypto.fixed,
3473 &parsed_crypto.extensions,
3474 )?;
3475 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
3476
3477 let terminal =
3478 locate_v41_public_terminal(bytes, &volume_header, &parsed_crypto.fixed, options)?;
3479 let block_region = parse_public_block_observation(
3480 bytes,
3481 crypto_end,
3482 &terminal.image,
3483 parsed_crypto.fixed.block_size as usize,
3484 &volume_header,
3485 )?;
3486 Ok(ParsedPublicNoKeyVolume {
3487 volume_header,
3488 crypto_header: parsed_crypto.fixed,
3489 root_auth_footer: terminal.root_auth_footer,
3490 root_auth_footer_bytes: terminal.root_auth_footer_bytes,
3491 blocks: block_region,
3492 })
3493}
3494
3495fn public_crypto_headers_agree(left: &CryptoHeaderFixed, right: &CryptoHeaderFixed) -> bool {
3496 left.length == right.length
3497 && left.stripe_width == right.stripe_width
3498 && left.block_size == right.block_size
3499 && left.compression_algo == right.compression_algo
3500 && left.aead_algo == right.aead_algo
3501 && left.fec_algo == right.fec_algo
3502 && left.kdf_algo == right.kdf_algo
3503}
3504
3505fn recompute_public_archive_root(
3506 footer: &RootAuthFooterV1,
3507 crypto_header: &CryptoHeaderFixed,
3508) -> Result<[u8; 32], FormatError> {
3509 let descriptor_digest = root_auth_descriptor_digest(
3510 footer.authenticator_id,
3511 footer.signer_identity_type,
3512 &footer.signer_identity_bytes,
3513 u32::try_from(footer.authenticator_value.len()).map_err(|_| {
3514 FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
3515 })?,
3516 footer.footer_length()?,
3517 )?;
3518 let signer_digest =
3519 signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
3520 if signer_digest != footer.signer_identity_digest {
3521 return Err(FormatError::InvalidArchive(
3522 "public no-key signer identity digest mismatch",
3523 ));
3524 }
3525 Ok(archive_root(ArchiveRootInputs {
3526 archive_uuid: footer.archive_uuid,
3527 session_id: footer.session_id,
3528 format_version: FORMAT_VERSION,
3529 volume_format_rev: VOLUME_FORMAT_REV,
3530 compression_algo: crypto_header.compression_algo,
3531 aead_algo: crypto_header.aead_algo,
3532 fec_algo: crypto_header.fec_algo,
3533 kdf_algo: crypto_header.kdf_algo,
3534 critical_metadata_digest: footer.critical_metadata_digest,
3535 index_digest: footer.index_digest,
3536 fec_layout_digest: footer.fec_layout_digest,
3537 total_data_block_count: footer.total_data_block_count,
3538 data_block_merkle_root: footer.data_block_merkle_root,
3539 root_auth_descriptor_digest: descriptor_digest,
3540 signer_identity_digest: signer_digest,
3541 }))
3542}
3543
3544fn parse_valid_manifest_footer(
3545 volume_header: &VolumeHeader,
3546 crypto_header: &CryptoHeaderFixed,
3547 subkeys: &Subkeys,
3548 manifest_bytes: &[u8],
3549) -> Result<ManifestFooter, FormatError> {
3550 let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
3551 validate_manifest_footer(
3552 volume_header,
3553 crypto_header,
3554 &manifest_footer,
3555 subkeys,
3556 manifest_bytes,
3557 )?;
3558 manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
3559 Ok(manifest_footer)
3560}
3561
3562fn manifest_footer_copy_error_is_recoverable(error: &FormatError) -> bool {
3563 matches!(
3564 error,
3565 FormatError::BadMagic {
3566 structure: "ManifestFooter",
3567 } | FormatError::NonZeroReserved {
3568 structure: "ManifestFooter",
3569 } | FormatError::InvalidAuthoritativeFlag(_)
3570 | FormatError::HmacMismatch {
3571 structure: "ManifestFooter",
3572 }
3573 | FormatError::IntegrityDigestMismatch {
3574 structure: "ManifestFooter",
3575 }
3576 )
3577}
3578
3579fn validate_seekable_supported_volume(
3580 volume_header: &VolumeHeader,
3581 crypto_header: &CryptoHeaderFixed,
3582 extensions: &[ExtensionTlv<'_>],
3583) -> Result<(), FormatError> {
3584 reject_unsupported_raw_stream_profile(extensions)?;
3585 if crypto_header.stripe_width != volume_header.stripe_width {
3586 return Err(FormatError::InvalidArchive(
3587 "VolumeHeader and CryptoHeader stripe_width differ",
3588 ));
3589 }
3590 Ok(())
3591}
3592
3593pub(crate) fn validate_crypto_class_parity_exactness(
3594 crypto_header: &CryptoHeaderFixed,
3595) -> Result<(), FormatError> {
3596 let fec = required_object_parity(crypto_header.fec_data_shards as u64, crypto_header)?;
3597 if crypto_header.fec_parity_shards as u32 != fec {
3598 return Err(FormatError::InvalidArchive(
3599 "fec_parity_shards does not match v41 compute_parity",
3600 ));
3601 }
3602 let index = required_object_parity(crypto_header.index_fec_data_shards as u64, crypto_header)?;
3603 if crypto_header.index_fec_parity_shards as u32 != index {
3604 return Err(FormatError::InvalidArchive(
3605 "index_fec_parity_shards does not match v41 compute_parity",
3606 ));
3607 }
3608 let index_root = required_object_parity(
3609 crypto_header.index_root_fec_data_shards as u64,
3610 crypto_header,
3611 )?;
3612 if crypto_header.index_root_fec_parity_shards as u32 != index_root {
3613 return Err(FormatError::InvalidArchive(
3614 "index_root_fec_parity_shards does not match v41 compute_parity",
3615 ));
3616 }
3617 Ok(())
3618}
3619
3620fn validate_volume_set_member(
3621 first: &ParsedSeekableVolume,
3622 candidate: &ParsedSeekableVolume,
3623) -> Result<(), FormatError> {
3624 validate_volume_set_member_metadata(
3625 &first.volume_header,
3626 &first.crypto_header,
3627 &first.crypto_header_bytes,
3628 &candidate.volume_header,
3629 &candidate.crypto_header,
3630 &candidate.crypto_header_bytes,
3631 )
3632}
3633
3634fn validate_volume_set_member_metadata(
3635 first_volume_header: &VolumeHeader,
3636 first_crypto_header: &CryptoHeaderFixed,
3637 first_crypto_header_bytes: &[u8],
3638 candidate_volume_header: &VolumeHeader,
3639 candidate_crypto_header: &CryptoHeaderFixed,
3640 candidate_crypto_header_bytes: &[u8],
3641) -> Result<(), FormatError> {
3642 if candidate_volume_header.archive_uuid != first_volume_header.archive_uuid
3643 || candidate_volume_header.session_id != first_volume_header.session_id
3644 {
3645 return Err(FormatError::InvalidArchive(
3646 "mixed archive or session IDs in volume set",
3647 ));
3648 }
3649 if candidate_crypto_header_bytes != first_crypto_header_bytes
3650 || candidate_crypto_header != first_crypto_header
3651 {
3652 return Err(FormatError::InvalidArchive("CryptoHeader copies differ"));
3653 }
3654 Ok(())
3655}
3656
3657pub(crate) fn manifest_bootstrap_fields_match(
3658 left: &ManifestFooter,
3659 right: &ManifestFooter,
3660) -> bool {
3661 left.archive_uuid == right.archive_uuid
3662 && left.session_id == right.session_id
3663 && left.is_authoritative == right.is_authoritative
3664 && left.total_volumes == right.total_volumes
3665 && left.index_root_first_block == right.index_root_first_block
3666 && left.index_root_data_block_count == right.index_root_data_block_count
3667 && left.index_root_parity_block_count == right.index_root_parity_block_count
3668 && left.index_root_encrypted_size == right.index_root_encrypted_size
3669 && left.index_root_decompressed_size == right.index_root_decompressed_size
3670}
3671
3672fn validate_complete_global_block_coverage(
3673 blocks: &BTreeMap<u64, BlockRecord>,
3674 erased_block_indices: &BTreeSet<u64>,
3675) -> Result<(), FormatError> {
3676 let mut expected = 0u64;
3677 let mut block_iter = blocks.keys().copied().peekable();
3678 let mut erasure_iter = erased_block_indices.iter().copied().peekable();
3679
3680 loop {
3681 let next_block = block_iter.peek().copied();
3682 let next_erasure = erasure_iter.peek().copied();
3683 let next = match (next_block, next_erasure) {
3684 (Some(block), Some(erasure)) if block == erasure => {
3685 return Err(FormatError::InvalidArchive(
3686 "BlockRecord index is both present and erased",
3687 ));
3688 }
3689 (Some(block), Some(erasure)) => block.min(erasure),
3690 (Some(block), None) => block,
3691 (None, Some(erasure)) => erasure,
3692 (None, None) => return Ok(()),
3693 };
3694
3695 if next != expected {
3696 return Err(FormatError::InvalidArchive(
3697 "complete volume set has missing global blocks",
3698 ));
3699 }
3700 if next_block == Some(next) {
3701 block_iter.next();
3702 }
3703 if next_erasure == Some(next) {
3704 erasure_iter.next();
3705 }
3706 expected = expected
3707 .checked_add(1)
3708 .ok_or(FormatError::InvalidArchive("global block index overflow"))?;
3709 }
3710}
3711
3712#[derive(Debug)]
3713struct V41Terminal {
3714 image: CriticalMetadataImage,
3715 manifest_footer_bytes: Vec<u8>,
3716 root_auth_footer_bytes: Option<Vec<u8>>,
3717 root_auth_footer: Option<RootAuthFooterV1>,
3718 volume_trailer: VolumeTrailer,
3719}
3720
3721pub(crate) struct SequentialTerminalMaterial {
3722 pub(crate) manifest_footer: ManifestFooter,
3723 pub(crate) volume_trailer: VolumeTrailer,
3724 pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
3725}
3726
3727#[derive(Debug)]
3728struct V41PublicTerminal {
3729 image: CriticalMetadataImage,
3730 root_auth_footer_bytes: Vec<u8>,
3731 root_auth_footer: RootAuthFooterV1,
3732}
3733
3734#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3735struct CmraDecoderTuple {
3736 shard_size: u32,
3737 data_shard_count: u16,
3738 parity_shard_count: u16,
3739 image_length: u32,
3740 image_sha256: [u8; 32],
3741}
3742
3743#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3744struct CmraIdentityHints {
3745 archive_uuid: [u8; 16],
3746 session_id: [u8; 16],
3747 volume_index: u32,
3748}
3749
3750impl From<CriticalMetadataRecoveryHeader> for CmraDecoderTuple {
3751 fn from(header: CriticalMetadataRecoveryHeader) -> Self {
3752 Self {
3753 shard_size: header.shard_size,
3754 data_shard_count: header.data_shard_count,
3755 parity_shard_count: header.parity_shard_count,
3756 image_length: header.image_length,
3757 image_sha256: header.image_sha256,
3758 }
3759 }
3760}
3761
3762impl From<CriticalMetadataRecoveryHeader> for CmraIdentityHints {
3763 fn from(header: CriticalMetadataRecoveryHeader) -> Self {
3764 Self {
3765 archive_uuid: header.archive_uuid_hint,
3766 session_id: header.session_id_hint,
3767 volume_index: header.volume_index_hint,
3768 }
3769 }
3770}
3771
3772impl From<CriticalRecoveryLocator> for CmraDecoderTuple {
3773 fn from(locator: CriticalRecoveryLocator) -> Self {
3774 Self {
3775 shard_size: locator.cmra_shard_size,
3776 data_shard_count: locator.cmra_data_shard_count,
3777 parity_shard_count: locator.cmra_parity_shard_count,
3778 image_length: locator.cmra_image_length,
3779 image_sha256: locator.cmra_image_sha256,
3780 }
3781 }
3782}
3783
3784impl From<CriticalRecoveryLocator> for CmraIdentityHints {
3785 fn from(locator: CriticalRecoveryLocator) -> Self {
3786 Self {
3787 archive_uuid: locator.archive_uuid_hint,
3788 session_id: locator.session_id_hint,
3789 volume_index: locator.volume_index_hint,
3790 }
3791 }
3792}
3793
3794#[derive(Debug)]
3795struct RecoveredCmra {
3796 image: CriticalMetadataImage,
3797 tuple: CmraDecoderTuple,
3798 header_hints: Option<CmraIdentityHints>,
3799 cmra_length: u64,
3800}
3801
3802#[derive(Debug)]
3803struct TerminalCandidate {
3804 terminal: V41Terminal,
3805 anchor: usize,
3806 locator_sequence: Option<u32>,
3807 cmra_offset: u64,
3808 cmra_length: u64,
3809}
3810
3811#[derive(Debug)]
3812struct PublicTerminalCandidate {
3813 terminal: V41PublicTerminal,
3814 anchor: usize,
3815 cmra_offset: u64,
3816 cmra_length: u64,
3817}
3818
3819#[derive(Debug, Clone, Copy)]
3820enum CmraRecoveryMode {
3821 KeyHolding,
3822 PublicNoKey,
3823}
3824
3825#[derive(Clone, Copy)]
3826pub(crate) struct KeyHoldingTerminalContext<'a> {
3827 pub(crate) subkeys: &'a Subkeys,
3828 pub(crate) volume_header: &'a VolumeHeader,
3829 pub(crate) crypto_header: &'a CryptoHeaderFixed,
3830 pub(crate) crypto_header_bytes: &'a [u8],
3831}
3832
3833fn locate_v41_terminal(
3834 bytes: &[u8],
3835 context: KeyHoldingTerminalContext<'_>,
3836 options: ReaderOptions,
3837) -> Result<V41Terminal, FormatError> {
3838 locate_v41_terminal_candidate(bytes, context, options).map(|candidate| candidate.terminal)
3839}
3840
3841fn locate_v41_terminal_read_at(
3842 reader: &dyn ArchiveReadAt,
3843 len: u64,
3844 context: KeyHoldingTerminalContext<'_>,
3845 options: ReaderOptions,
3846) -> Result<V41Terminal, FormatError> {
3847 let mut candidates = Vec::new();
3848 if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
3849 let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
3850 collect_v41_locator_candidate_read_at(reader, final_offset, 0, context, &mut candidates);
3851 }
3852 if len >= LOCATOR_PAIR_LEN as u64 {
3853 let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
3854 collect_v41_locator_candidate_read_at(reader, mirror_offset, 1, context, &mut candidates);
3855 }
3856
3857 if candidates.is_empty() {
3858 let scan = max_critical_recovery_scan(options)? as u64;
3859 let scan_start = len.saturating_sub(scan);
3860 let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
3861 let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
3862 let mut offset = tail.len().saturating_sub(4);
3863 while offset < tail.len() {
3864 let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
3865 if tail.get(offset..offset + 4) == Some(b"TZCL") {
3866 collect_v41_locator_candidate_read_at(
3867 reader,
3868 absolute_offset,
3869 2,
3870 context,
3871 &mut candidates,
3872 );
3873 } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
3874 if let Ok(candidate) =
3875 parse_locatorless_cmra_candidate_read_at(reader, absolute_offset, context)
3876 {
3877 candidates.push(candidate);
3878 }
3879 }
3880 if offset == 0 {
3881 break;
3882 }
3883 offset -= 1;
3884 }
3885 }
3886
3887 choose_v41_terminal_candidate(candidates).map(|candidate| candidate.terminal)
3888}
3889
3890fn locate_v41_terminal_candidate(
3891 bytes: &[u8],
3892 context: KeyHoldingTerminalContext<'_>,
3893 options: ReaderOptions,
3894) -> Result<TerminalCandidate, FormatError> {
3895 let mut candidates = Vec::new();
3896 if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
3897 let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
3898 collect_v41_locator_candidate(bytes, final_offset, 0, context, &mut candidates);
3899 }
3900 if bytes.len() >= LOCATOR_PAIR_LEN {
3901 let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
3902 collect_v41_locator_candidate(bytes, mirror_offset, 1, context, &mut candidates);
3903 }
3904
3905 if candidates.is_empty() {
3906 let scan = max_critical_recovery_scan(options)?;
3907 let scan_start = bytes.len().saturating_sub(scan);
3908 let mut offset = bytes.len().saturating_sub(4);
3909 while offset >= scan_start {
3910 if bytes.get(offset..offset + 4) == Some(b"TZCL") {
3911 collect_v41_locator_candidate(bytes, offset, 2, context, &mut candidates);
3912 } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
3913 if let Ok(candidate) = parse_locatorless_cmra_candidate(bytes, offset, context) {
3914 candidates.push(candidate);
3915 }
3916 }
3917 if offset == 0 {
3918 break;
3919 }
3920 offset -= 1;
3921 }
3922 }
3923
3924 choose_v41_terminal_candidate(candidates)
3925}
3926
3927fn locate_v41_public_terminal(
3928 bytes: &[u8],
3929 volume_header: &VolumeHeader,
3930 crypto_header: &CryptoHeaderFixed,
3931 options: ReaderOptions,
3932) -> Result<V41PublicTerminal, FormatError> {
3933 let mut candidates = Vec::new();
3934 if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
3935 let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
3936 collect_v41_public_locator_candidate(
3937 bytes,
3938 final_offset,
3939 0,
3940 volume_header,
3941 crypto_header,
3942 &mut candidates,
3943 );
3944 }
3945 if bytes.len() >= LOCATOR_PAIR_LEN {
3946 let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
3947 collect_v41_public_locator_candidate(
3948 bytes,
3949 mirror_offset,
3950 1,
3951 volume_header,
3952 crypto_header,
3953 &mut candidates,
3954 );
3955 }
3956
3957 if candidates.is_empty() {
3958 let scan = max_critical_recovery_scan(options)?;
3959 let scan_start = bytes.len().saturating_sub(scan);
3960 let mut offset = bytes.len().saturating_sub(4);
3961 while offset >= scan_start {
3962 if bytes.get(offset..offset + 4) == Some(b"TZCL") {
3963 collect_v41_public_locator_candidate(
3964 bytes,
3965 offset,
3966 2,
3967 volume_header,
3968 crypto_header,
3969 &mut candidates,
3970 );
3971 } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
3972 if let Ok(candidate) = parse_public_locatorless_cmra_candidate(
3973 bytes,
3974 offset,
3975 volume_header,
3976 crypto_header,
3977 ) {
3978 candidates.push(candidate);
3979 }
3980 }
3981 if offset == 0 {
3982 break;
3983 }
3984 offset -= 1;
3985 }
3986 }
3987
3988 choose_v41_public_terminal_candidate(candidates).map(|candidate| candidate.terminal)
3989}
3990
3991fn collect_v41_locator_candidate(
3992 bytes: &[u8],
3993 offset: usize,
3994 expected_sequence: u32,
3995 context: KeyHoldingTerminalContext<'_>,
3996 candidates: &mut Vec<TerminalCandidate>,
3997) {
3998 let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
3999 return;
4000 };
4001 let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
4002 return;
4003 };
4004 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4005 return;
4006 }
4007 if let Ok(candidate) = parse_locator_cmra_candidate(bytes, offset, locator, context) {
4008 candidates.push(candidate);
4009 }
4010}
4011
4012fn collect_v41_locator_candidate_read_at(
4013 reader: &dyn ArchiveReadAt,
4014 offset: u64,
4015 expected_sequence: u32,
4016 context: KeyHoldingTerminalContext<'_>,
4017 candidates: &mut Vec<TerminalCandidate>,
4018) {
4019 let Ok(raw) = read_at_vec(
4020 reader,
4021 offset,
4022 CRITICAL_RECOVERY_LOCATOR_LEN,
4023 "CriticalRecoveryLocator",
4024 ) else {
4025 return;
4026 };
4027 let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
4028 return;
4029 };
4030 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4031 return;
4032 }
4033 if let Ok(candidate) = parse_locator_cmra_candidate_read_at(reader, offset, locator, context) {
4034 candidates.push(candidate);
4035 }
4036}
4037
4038fn collect_v41_public_locator_candidate(
4039 bytes: &[u8],
4040 offset: usize,
4041 expected_sequence: u32,
4042 volume_header: &VolumeHeader,
4043 crypto_header: &CryptoHeaderFixed,
4044 candidates: &mut Vec<PublicTerminalCandidate>,
4045) {
4046 let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
4047 return;
4048 };
4049 let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
4050 return;
4051 };
4052 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4053 return;
4054 }
4055 if let Ok(candidate) =
4056 parse_public_locator_cmra_candidate(bytes, offset, locator, volume_header, crypto_header)
4057 {
4058 candidates.push(candidate);
4059 }
4060}
4061
4062fn choose_v41_terminal_candidate(
4063 mut candidates: Vec<TerminalCandidate>,
4064) -> Result<TerminalCandidate, FormatError> {
4065 candidates.sort_by_key(|candidate| candidate.anchor);
4066 let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
4067 "no valid v41 CMRA candidate found",
4068 ))?;
4069 if let Some(previous) = candidates.last() {
4070 if previous.anchor == winner.anchor
4071 && (previous.cmra_offset != winner.cmra_offset
4072 || previous.cmra_length != winner.cmra_length)
4073 {
4074 return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
4075 }
4076 }
4077 Ok(winner)
4078}
4079
4080fn choose_v41_public_terminal_candidate(
4081 mut candidates: Vec<PublicTerminalCandidate>,
4082) -> Result<PublicTerminalCandidate, FormatError> {
4083 candidates.sort_by_key(|candidate| candidate.anchor);
4084 let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
4085 "no valid v41 public CMRA candidate found",
4086 ))?;
4087 if let Some(previous) = candidates.last() {
4088 if previous.anchor == winner.anchor
4089 && (previous.cmra_offset != winner.cmra_offset
4090 || previous.cmra_length != winner.cmra_length)
4091 {
4092 return Err(FormatError::InvalidArchive(
4093 "ambiguous v41 public CMRA candidates",
4094 ));
4095 }
4096 }
4097 Ok(winner)
4098}
4099
4100fn parse_locator_cmra_candidate(
4101 bytes: &[u8],
4102 locator_offset: usize,
4103 locator: CriticalRecoveryLocator,
4104 context: KeyHoldingTerminalContext<'_>,
4105) -> Result<TerminalCandidate, FormatError> {
4106 let tuple = CmraDecoderTuple::from(locator);
4107 validate_cmra_decoder_tuple(tuple)?;
4108 let expected_cmra_length = cmra_serialized_length(tuple)?;
4109 if locator.cmra_length as u64 != expected_cmra_length {
4110 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4111 }
4112 validate_locator_position(locator_offset, locator)?;
4113 let recovered = recover_cmra(
4114 bytes,
4115 locator.cmra_offset,
4116 Some(tuple),
4117 CmraRecoveryMode::KeyHolding,
4118 )?;
4119 if recovered.tuple != tuple {
4120 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4121 }
4122 if expected_cmra_length != recovered.cmra_length {
4123 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4124 }
4125 validate_locator_image_boundary(locator, &recovered.image)?;
4126 validate_cmra_identity_hints(
4127 recovered.header_hints,
4128 Some(CmraIdentityHints::from(locator)),
4129 &recovered.image,
4130 )?;
4131 let terminal = validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context)?;
4132 Ok(TerminalCandidate {
4133 terminal,
4134 anchor: locator_offset
4135 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
4136 .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
4137 locator_sequence: Some(locator.locator_sequence),
4138 cmra_offset: locator.cmra_offset,
4139 cmra_length: recovered.cmra_length,
4140 })
4141}
4142
4143fn parse_locator_cmra_candidate_read_at(
4144 reader: &dyn ArchiveReadAt,
4145 locator_offset: u64,
4146 locator: CriticalRecoveryLocator,
4147 context: KeyHoldingTerminalContext<'_>,
4148) -> Result<TerminalCandidate, FormatError> {
4149 let tuple = CmraDecoderTuple::from(locator);
4150 validate_cmra_decoder_tuple(tuple)?;
4151 let expected_cmra_length = cmra_serialized_length(tuple)?;
4152 if locator.cmra_length as u64 != expected_cmra_length {
4153 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4154 }
4155 validate_locator_position(
4156 to_usize(locator_offset, "CriticalRecoveryLocator")?,
4157 locator,
4158 )?;
4159 let recovered = recover_cmra_read_at(
4160 reader,
4161 locator.cmra_offset,
4162 Some(tuple),
4163 CmraRecoveryMode::KeyHolding,
4164 )?;
4165 if recovered.tuple != tuple {
4166 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4167 }
4168 if expected_cmra_length != recovered.cmra_length {
4169 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4170 }
4171 validate_locator_image_boundary(locator, &recovered.image)?;
4172 validate_cmra_identity_hints(
4173 recovered.header_hints,
4174 Some(CmraIdentityHints::from(locator)),
4175 &recovered.image,
4176 )?;
4177 let terminal =
4178 validate_recovered_terminal_read_at(recovered.image, recovered.tuple, reader, context)?;
4179 Ok(TerminalCandidate {
4180 terminal,
4181 anchor: to_usize(
4182 checked_u64_add(
4183 locator_offset,
4184 CRITICAL_RECOVERY_LOCATOR_LEN as u64,
4185 "locator anchor overflow",
4186 )?,
4187 "locator anchor overflow",
4188 )?,
4189 locator_sequence: Some(locator.locator_sequence),
4190 cmra_offset: locator.cmra_offset,
4191 cmra_length: recovered.cmra_length,
4192 })
4193}
4194
4195fn parse_public_locator_cmra_candidate(
4196 bytes: &[u8],
4197 locator_offset: usize,
4198 locator: CriticalRecoveryLocator,
4199 volume_header: &VolumeHeader,
4200 crypto_header: &CryptoHeaderFixed,
4201) -> Result<PublicTerminalCandidate, FormatError> {
4202 let tuple = CmraDecoderTuple::from(locator);
4203 validate_cmra_decoder_tuple(tuple)?;
4204 let expected_cmra_length = cmra_serialized_length(tuple)?;
4205 if locator.cmra_length as u64 != expected_cmra_length {
4206 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4207 }
4208 validate_locator_position(locator_offset, locator)?;
4209 let recovered = recover_cmra(
4210 bytes,
4211 locator.cmra_offset,
4212 Some(tuple),
4213 CmraRecoveryMode::PublicNoKey,
4214 )?;
4215 if recovered.tuple != tuple {
4216 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4217 }
4218 if expected_cmra_length != recovered.cmra_length {
4219 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4220 }
4221 validate_locator_image_boundary(locator, &recovered.image)?;
4222 validate_cmra_identity_hints(
4223 recovered.header_hints,
4224 Some(CmraIdentityHints::from(locator)),
4225 &recovered.image,
4226 )?;
4227 let terminal =
4228 validate_recovered_public_terminal(recovered.image, bytes, volume_header, crypto_header)?;
4229 Ok(PublicTerminalCandidate {
4230 terminal,
4231 anchor: locator_offset
4232 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
4233 .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
4234 cmra_offset: locator.cmra_offset,
4235 cmra_length: recovered.cmra_length,
4236 })
4237}
4238
4239fn parse_locatorless_cmra_candidate(
4240 bytes: &[u8],
4241 cmra_offset: usize,
4242 context: KeyHoldingTerminalContext<'_>,
4243) -> Result<TerminalCandidate, FormatError> {
4244 let recovered = recover_cmra(
4245 bytes,
4246 cmra_offset as u64,
4247 None,
4248 CmraRecoveryMode::KeyHolding,
4249 )?;
4250 if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
4251 return Err(FormatError::InvalidArchive(
4252 "locatorless CMRA boundary mismatch",
4253 ));
4254 }
4255 if recovered
4256 .image
4257 .volume_trailer_offset
4258 .checked_add(VOLUME_TRAILER_LEN as u64)
4259 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
4260 != cmra_offset as u64
4261 {
4262 return Err(FormatError::InvalidArchive(
4263 "locatorless trailer boundary mismatch",
4264 ));
4265 }
4266 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
4267 let terminal = validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context)?;
4268 Ok(TerminalCandidate {
4269 terminal,
4270 anchor: cmra_offset
4271 .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
4272 .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
4273 locator_sequence: None,
4274 cmra_offset: cmra_offset as u64,
4275 cmra_length: recovered.cmra_length,
4276 })
4277}
4278
4279fn parse_locatorless_cmra_candidate_read_at(
4280 reader: &dyn ArchiveReadAt,
4281 cmra_offset: u64,
4282 context: KeyHoldingTerminalContext<'_>,
4283) -> Result<TerminalCandidate, FormatError> {
4284 let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
4285 if recovered.image.body_bytes_before_cmra != cmra_offset {
4286 return Err(FormatError::InvalidArchive(
4287 "locatorless CMRA boundary mismatch",
4288 ));
4289 }
4290 if recovered
4291 .image
4292 .volume_trailer_offset
4293 .checked_add(VOLUME_TRAILER_LEN as u64)
4294 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
4295 != cmra_offset
4296 {
4297 return Err(FormatError::InvalidArchive(
4298 "locatorless trailer boundary mismatch",
4299 ));
4300 }
4301 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
4302 let terminal =
4303 validate_recovered_terminal_read_at(recovered.image, recovered.tuple, reader, context)?;
4304 Ok(TerminalCandidate {
4305 terminal,
4306 anchor: to_usize(
4307 checked_u64_add(cmra_offset, recovered.cmra_length, "CMRA anchor overflow")?,
4308 "CMRA anchor overflow",
4309 )?,
4310 locator_sequence: None,
4311 cmra_offset,
4312 cmra_length: recovered.cmra_length,
4313 })
4314}
4315
4316fn parse_public_locatorless_cmra_candidate(
4317 bytes: &[u8],
4318 cmra_offset: usize,
4319 volume_header: &VolumeHeader,
4320 crypto_header: &CryptoHeaderFixed,
4321) -> Result<PublicTerminalCandidate, FormatError> {
4322 let recovered = recover_cmra(
4323 bytes,
4324 cmra_offset as u64,
4325 None,
4326 CmraRecoveryMode::PublicNoKey,
4327 )?;
4328 if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
4329 return Err(FormatError::InvalidArchive(
4330 "locatorless CMRA boundary mismatch",
4331 ));
4332 }
4333 if recovered
4334 .image
4335 .volume_trailer_offset
4336 .checked_add(VOLUME_TRAILER_LEN as u64)
4337 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
4338 != cmra_offset as u64
4339 {
4340 return Err(FormatError::InvalidArchive(
4341 "locatorless trailer boundary mismatch",
4342 ));
4343 }
4344 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
4345 let terminal =
4346 validate_recovered_public_terminal(recovered.image, bytes, volume_header, crypto_header)?;
4347 Ok(PublicTerminalCandidate {
4348 terminal,
4349 anchor: cmra_offset
4350 .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
4351 .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
4352 cmra_offset: cmra_offset as u64,
4353 cmra_length: recovered.cmra_length,
4354 })
4355}
4356
4357fn validate_locator_position(
4358 locator_offset: usize,
4359 locator: CriticalRecoveryLocator,
4360) -> Result<(), FormatError> {
4361 if locator.cmra_offset != locator.body_bytes_before_cmra {
4362 return Err(FormatError::InvalidArchive(
4363 "locator CMRA boundary mismatch",
4364 ));
4365 }
4366 if locator
4367 .volume_trailer_offset
4368 .checked_add(VOLUME_TRAILER_LEN as u64)
4369 .ok_or(FormatError::InvalidArchive("locator trailer overflow"))?
4370 != locator.cmra_offset
4371 {
4372 return Err(FormatError::InvalidArchive(
4373 "locator trailer boundary mismatch",
4374 ));
4375 }
4376 let expected_offset = match locator.locator_sequence {
4377 1 => locator.cmra_offset.checked_add(locator.cmra_length as u64),
4378 0 => locator
4379 .cmra_offset
4380 .checked_add(locator.cmra_length as u64)
4381 .and_then(|value| value.checked_add(CRITICAL_RECOVERY_LOCATOR_LEN as u64)),
4382 _ => None,
4383 }
4384 .ok_or(FormatError::InvalidArchive("locator position overflow"))?;
4385 if expected_offset != locator_offset as u64 {
4386 return Err(FormatError::InvalidArchive(
4387 "locator position does not match sequence",
4388 ));
4389 }
4390 Ok(())
4391}
4392
4393fn validate_locator_image_boundary(
4394 locator: CriticalRecoveryLocator,
4395 image: &CriticalMetadataImage,
4396) -> Result<(), FormatError> {
4397 if locator.volume_trailer_offset != image.volume_trailer_offset
4398 || locator.body_bytes_before_cmra != image.body_bytes_before_cmra
4399 || image
4400 .volume_trailer_offset
4401 .checked_add(VOLUME_TRAILER_LEN as u64)
4402 .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
4403 != locator.cmra_offset
4404 {
4405 return Err(FormatError::InvalidArchive(
4406 "locator and CMRA image boundaries differ",
4407 ));
4408 }
4409 Ok(())
4410}
4411
4412fn validate_cmra_identity_hints(
4413 header_hints: Option<CmraIdentityHints>,
4414 locator_hints: Option<CmraIdentityHints>,
4415 image: &CriticalMetadataImage,
4416) -> Result<(), FormatError> {
4417 if let (Some(header), Some(locator)) = (header_hints, locator_hints) {
4418 if header != locator {
4419 return Err(FormatError::InvalidArchive(
4420 "CMRA header and locator identity hints differ",
4421 ));
4422 }
4423 }
4424 for hints in [header_hints, locator_hints].into_iter().flatten() {
4425 if hints.archive_uuid != image.archive_uuid
4426 || hints.session_id != image.session_id
4427 || hints.volume_index != image.volume_index
4428 {
4429 return Err(FormatError::InvalidArchive(
4430 "CMRA identity hints do not match recovered image",
4431 ));
4432 }
4433 }
4434 Ok(())
4435}
4436
4437fn recover_cmra(
4438 bytes: &[u8],
4439 cmra_offset: u64,
4440 locator_tuple: Option<CmraDecoderTuple>,
4441 mode: CmraRecoveryMode,
4442) -> Result<RecoveredCmra, FormatError> {
4443 let offset = to_usize(cmra_offset, "CMRA")?;
4444 let header_bytes = slice(
4445 bytes,
4446 offset,
4447 CRITICAL_METADATA_RECOVERY_HEADER_LEN,
4448 "CriticalMetadataRecoveryHeader",
4449 )?;
4450 let (tuple, header_hints) = recover_cmra_header_tuple(header_bytes, locator_tuple)?;
4451 validate_cmra_decoder_tuple(tuple)?;
4452 let cmra_length = cmra_serialized_length(tuple)?;
4453 let cmra_len = to_usize(cmra_length, "CMRA")?;
4454 let cmra_bytes = slice(bytes, offset, cmra_len, "CMRA")?;
4455 recover_cmra_from_bytes(cmra_bytes, tuple, header_hints, cmra_length, mode)
4456}
4457
4458fn recover_cmra_read_at(
4459 reader: &dyn ArchiveReadAt,
4460 cmra_offset: u64,
4461 locator_tuple: Option<CmraDecoderTuple>,
4462 mode: CmraRecoveryMode,
4463) -> Result<RecoveredCmra, FormatError> {
4464 let header_bytes = read_at_vec(
4465 reader,
4466 cmra_offset,
4467 CRITICAL_METADATA_RECOVERY_HEADER_LEN,
4468 "CriticalMetadataRecoveryHeader",
4469 )?;
4470 let (tuple, header_hints) = recover_cmra_header_tuple(&header_bytes, locator_tuple)?;
4471 validate_cmra_decoder_tuple(tuple)?;
4472 let cmra_length = cmra_serialized_length(tuple)?;
4473 let cmra_bytes = read_at_vec(reader, cmra_offset, to_usize(cmra_length, "CMRA")?, "CMRA")?;
4474 recover_cmra_from_bytes(&cmra_bytes, tuple, header_hints, cmra_length, mode)
4475}
4476
4477fn recover_cmra_header_tuple(
4478 header_bytes: &[u8],
4479 locator_tuple: Option<CmraDecoderTuple>,
4480) -> Result<(CmraDecoderTuple, Option<CmraIdentityHints>), FormatError> {
4481 let parsed_header = CriticalMetadataRecoveryHeader::parse(header_bytes);
4482 Ok(match (parsed_header, locator_tuple) {
4483 (Ok(header), Some(locator_tuple)) => {
4484 let header_tuple = CmraDecoderTuple::from(header);
4485 if header_tuple != locator_tuple {
4486 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4487 }
4488 (locator_tuple, Some(CmraIdentityHints::from(header)))
4489 }
4490 (Ok(header), None) => (
4491 CmraDecoderTuple::from(header),
4492 Some(CmraIdentityHints::from(header)),
4493 ),
4494 (
4495 Err(FormatError::BadCrc {
4496 structure: "CriticalMetadataRecoveryHeader",
4497 }),
4498 Some(tuple),
4499 ) => (tuple, None),
4500 (Err(err), _) => return Err(err),
4501 })
4502}
4503
4504fn recover_cmra_from_bytes(
4505 cmra_bytes: &[u8],
4506 tuple: CmraDecoderTuple,
4507 header_hints: Option<CmraIdentityHints>,
4508 cmra_length: u64,
4509 mode: CmraRecoveryMode,
4510) -> Result<RecoveredCmra, FormatError> {
4511 let shard_size = tuple.shard_size as usize;
4512 let mut data_shards = vec![None; tuple.data_shard_count as usize];
4513 let mut parity_shards = vec![None; tuple.parity_shard_count as usize];
4514 let mut cursor = CRITICAL_METADATA_RECOVERY_HEADER_LEN;
4515 for idx in 0..(tuple.data_shard_count as usize + tuple.parity_shard_count as usize) {
4516 let raw = slice(
4517 cmra_bytes,
4518 cursor,
4519 CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
4520 "CriticalMetadataRecoveryShard",
4521 )?;
4522 let shard = match CriticalMetadataRecoveryShard::parse(raw, shard_size) {
4523 Ok(shard) => Some(shard),
4524 Err(FormatError::BadCrc {
4525 structure: "CriticalMetadataRecoveryShard",
4526 }) => None,
4527 Err(err) => return Err(err),
4528 };
4529 if let Some(shard) = shard {
4530 validate_cmra_shard(&shard, idx, tuple)?;
4531 if shard.shard_role == 0 {
4532 let data_slot = data_shards
4533 .get_mut(idx)
4534 .ok_or(FormatError::InvalidArchive("CMRA data shard out of range"))?;
4535 *data_slot = Some(shard.payload);
4536 } else {
4537 let parity_idx = idx - tuple.data_shard_count as usize;
4538 let parity_slot =
4539 parity_shards
4540 .get_mut(parity_idx)
4541 .ok_or(FormatError::InvalidArchive(
4542 "CMRA parity shard out of range",
4543 ))?;
4544 *parity_slot = Some(shard.payload);
4545 }
4546 }
4547 cursor = checked_add(
4548 cursor,
4549 CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
4550 "CriticalMetadataRecoveryShard",
4551 )?;
4552 }
4553 let repaired = repair_data_gf16(&data_shards, &parity_shards, shard_size)?;
4554 let mut image_bytes = Vec::with_capacity(tuple.image_length as usize);
4555 for shard in repaired {
4556 image_bytes.extend_from_slice(&shard);
4557 }
4558 image_bytes.truncate(tuple.image_length as usize);
4559 if sha256_bytes(&image_bytes) != tuple.image_sha256 {
4560 return Err(FormatError::InvalidArchive("CMRA image SHA-256 mismatch"));
4561 }
4562 let image = CriticalMetadataImage::parse(&image_bytes)?;
4563 validate_critical_metadata_image(&image, mode)?;
4564 Ok(RecoveredCmra {
4565 image,
4566 tuple,
4567 header_hints,
4568 cmra_length,
4569 })
4570}
4571
4572fn validate_cmra_decoder_tuple(tuple: CmraDecoderTuple) -> Result<(), FormatError> {
4573 let shard_size = tuple.shard_size as u64;
4574 if !(512..=4096).contains(&shard_size) || shard_size % 2 != 0 {
4575 return Err(FormatError::InvalidArchive("CMRA shard_size is invalid"));
4576 }
4577 let image_length = tuple.image_length as u64;
4578 let min = critical_image_min();
4579 let cap = critical_image_cap()?;
4580 if image_length < min || image_length > cap {
4581 return Err(FormatError::InvalidArchive(
4582 "CMRA image_length is outside bounds",
4583 ));
4584 }
4585 let expected_data_shards = ceil_div_u64(image_length, shard_size)?;
4586 if expected_data_shards == 0 || expected_data_shards != tuple.data_shard_count as u64 {
4587 return Err(FormatError::InvalidArchive(
4588 "CMRA data_shard_count does not match image length",
4589 ));
4590 }
4591 let max_parity = 2u64.max(ceil_div_u64(
4592 checked_u64_mul(
4593 expected_data_shards,
4594 READER_MAX_CMRA_PARITY_PCT as u64,
4595 "CMRA parity overflow",
4596 )?,
4597 100,
4598 )?);
4599 if tuple.parity_shard_count as u64 > max_parity {
4600 return Err(FormatError::ReaderResourceLimitExceeded {
4601 field: "CMRA parity shard count",
4602 cap: max_parity,
4603 actual: tuple.parity_shard_count as u64,
4604 });
4605 }
4606 let total = expected_data_shards
4607 .checked_add(tuple.parity_shard_count as u64)
4608 .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
4609 if total > 65_535 {
4610 return Err(FormatError::FecTooManyShards(total as usize));
4611 }
4612 Ok(())
4613}
4614
4615fn validate_cmra_writer_parity_lower_bound(
4616 tuple: CmraDecoderTuple,
4617 bit_rot_buffer_pct: u8,
4618) -> Result<(), FormatError> {
4619 let min_parity = 2u64.max(ceil_div_u64(
4620 checked_u64_mul(
4621 tuple.data_shard_count as u64,
4622 bit_rot_buffer_pct as u64,
4623 "CMRA parity lower-bound overflow",
4624 )?,
4625 100,
4626 )?);
4627 if (tuple.parity_shard_count as u64) < min_parity {
4628 return Err(FormatError::InvalidArchive(
4629 "CMRA parity shard count is below authenticated bit-rot lower bound",
4630 ));
4631 }
4632 Ok(())
4633}
4634
4635fn validate_cmra_shard(
4636 shard: &CriticalMetadataRecoveryShard,
4637 serialized_idx: usize,
4638 tuple: CmraDecoderTuple,
4639) -> Result<(), FormatError> {
4640 if shard.shard_index as usize != serialized_idx {
4641 return Err(FormatError::InvalidArchive(
4642 "CMRA shards are not in canonical order",
4643 ));
4644 }
4645 let data_count = tuple.data_shard_count as usize;
4646 let shard_size = tuple.shard_size as usize;
4647 if serialized_idx < data_count {
4648 if shard.shard_role != 0 {
4649 return Err(FormatError::InvalidArchive(
4650 "CMRA data shard has wrong role",
4651 ));
4652 }
4653 let expected_len = if serialized_idx + 1 == data_count {
4654 let used = tuple.image_length as usize - serialized_idx * shard_size;
4655 if used == 0 {
4656 shard_size
4657 } else {
4658 used
4659 }
4660 } else {
4661 shard_size
4662 };
4663 if shard.shard_payload_length as usize != expected_len {
4664 return Err(FormatError::InvalidArchive(
4665 "CMRA data shard payload length is non-canonical",
4666 ));
4667 }
4668 if serialized_idx + 1 == data_count
4669 && shard.payload[expected_len..].iter().any(|byte| *byte != 0)
4670 {
4671 return Err(FormatError::InvalidArchive(
4672 "CMRA final data shard padding is non-zero",
4673 ));
4674 }
4675 } else {
4676 if shard.shard_role != 1 {
4677 return Err(FormatError::InvalidArchive(
4678 "CMRA parity shard has wrong role",
4679 ));
4680 }
4681 if shard.shard_payload_length as usize != shard_size {
4682 return Err(FormatError::InvalidArchive(
4683 "CMRA parity shard payload length is non-canonical",
4684 ));
4685 }
4686 }
4687 Ok(())
4688}
4689
4690fn validate_critical_metadata_image(
4691 image: &CriticalMetadataImage,
4692 mode: CmraRecoveryMode,
4693) -> Result<(), FormatError> {
4694 let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
4695 if image.volume_header_offset != 0
4696 || image.volume_header_length != VOLUME_HEADER_LEN as u32
4697 || image.crypto_header_offset != VOLUME_HEADER_LEN as u64
4698 || image.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
4699 || image.volume_trailer_length != VOLUME_TRAILER_LEN as u32
4700 || image.body_bytes_before_cmra
4701 != image
4702 .volume_trailer_offset
4703 .checked_add(VOLUME_TRAILER_LEN as u64)
4704 .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
4705 {
4706 return Err(FormatError::InvalidArchive(
4707 "CriticalMetadataImage fixed layout is invalid",
4708 ));
4709 }
4710 if root_auth_present {
4711 if image.root_auth_footer_offset == 0
4712 || image.root_auth_footer_length == 0
4713 || image.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
4714 {
4715 return Err(FormatError::InvalidArchive(
4716 "CriticalMetadataImage root-auth range is invalid",
4717 ));
4718 }
4719 } else if image.root_auth_footer_offset != 0
4720 || image.root_auth_footer_length != 0
4721 || image.root_auth_footer_sha256 != [0u8; 32]
4722 {
4723 return Err(FormatError::InvalidArchive(
4724 "CriticalMetadataImage root-auth fields must be zero when absent",
4725 ));
4726 }
4727 let block_record_len = image_block_record_len_from_region(image)?;
4728 let block_record_len_u64 = u64::try_from(block_record_len)
4729 .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?;
4730 match mode {
4731 CmraRecoveryMode::KeyHolding => {
4732 let expected_len = image.block_count.checked_mul(block_record_len_u64).ok_or(
4733 FormatError::InvalidArchive("BlockRecord region length overflow"),
4734 )?;
4735 if image.block_records_length != expected_len {
4736 return Err(FormatError::InvalidArchive(
4737 "CriticalMetadataImage terminal equations are invalid",
4738 ));
4739 }
4740 }
4741 CmraRecoveryMode::PublicNoKey => {
4742 if image.block_records_length % block_record_len_u64 != 0 {
4743 return Err(FormatError::InvalidArchive(
4744 "CriticalMetadataImage BlockRecord region is not aligned",
4745 ));
4746 }
4747 }
4748 }
4749 if image.block_records_offset
4750 != image
4751 .crypto_header_offset
4752 .checked_add(image.crypto_header_length as u64)
4753 .ok_or(FormatError::InvalidArchive(
4754 "CryptoHeader boundary overflow",
4755 ))?
4756 || image.manifest_footer_offset
4757 != image
4758 .block_records_offset
4759 .checked_add(image.block_records_length)
4760 .ok_or(FormatError::InvalidArchive(
4761 "ManifestFooter boundary overflow",
4762 ))?
4763 {
4764 return Err(FormatError::InvalidArchive(
4765 "CriticalMetadataImage terminal equations are invalid",
4766 ));
4767 }
4768 let manifest_end = image
4769 .manifest_footer_offset
4770 .checked_add(MANIFEST_FOOTER_LEN as u64)
4771 .ok_or(FormatError::InvalidArchive(
4772 "RootAuthFooter boundary overflow",
4773 ))?;
4774 if root_auth_present {
4775 if image.root_auth_footer_offset != manifest_end
4776 || image
4777 .root_auth_footer_offset
4778 .checked_add(image.root_auth_footer_length as u64)
4779 .ok_or(FormatError::InvalidArchive(
4780 "VolumeTrailer boundary overflow",
4781 ))?
4782 != image.volume_trailer_offset
4783 {
4784 return Err(FormatError::InvalidArchive(
4785 "CriticalMetadataImage root-auth terminal equations are invalid",
4786 ));
4787 }
4788 } else if image.volume_trailer_offset != manifest_end {
4789 return Err(FormatError::InvalidArchive(
4790 "CriticalMetadataImage unsigned terminal equations are invalid",
4791 ));
4792 }
4793 let expected_types: &[u16] = if root_auth_present {
4794 &[1, 2, 3, 4, 5]
4795 } else {
4796 &[1, 2, 3, 5]
4797 };
4798 if image.regions.len() != expected_types.len()
4799 || image
4800 .regions
4801 .iter()
4802 .map(|region| region.region_type)
4803 .ne(expected_types.iter().copied())
4804 {
4805 return Err(FormatError::InvalidArchive(
4806 "CriticalMetadataImage regions are not canonical",
4807 ));
4808 }
4809 validate_image_region(
4810 image,
4811 1,
4812 image.volume_header_offset,
4813 image.volume_header_length,
4814 )?;
4815 validate_image_region(
4816 image,
4817 2,
4818 image.crypto_header_offset,
4819 image.crypto_header_length,
4820 )?;
4821 validate_image_region(
4822 image,
4823 3,
4824 image.manifest_footer_offset,
4825 image.manifest_footer_length,
4826 )?;
4827 if root_auth_present {
4828 validate_image_region(
4829 image,
4830 4,
4831 image.root_auth_footer_offset,
4832 image.root_auth_footer_length,
4833 )?;
4834 }
4835 validate_image_region(
4836 image,
4837 5,
4838 image.volume_trailer_offset,
4839 image.volume_trailer_length,
4840 )?;
4841 if sha256_region(image, 1)? != image.volume_header_sha256
4842 || sha256_region(image, 2)? != image.crypto_header_sha256
4843 || sha256_region(image, 3)? != image.manifest_footer_sha256
4844 || (root_auth_present && sha256_region(image, 4)? != image.root_auth_footer_sha256)
4845 || (!root_auth_present && image.root_auth_footer_sha256 != [0u8; 32])
4846 || sha256_region(image, 5)? != image.volume_trailer_sha256
4847 {
4848 return Err(FormatError::InvalidArchive(
4849 "CriticalMetadataImage region digest mismatch",
4850 ));
4851 }
4852 Ok(())
4853}
4854
4855fn image_block_record_len_from_region(image: &CriticalMetadataImage) -> Result<usize, FormatError> {
4856 let crypto_region = image
4857 .region(2)
4858 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
4859 let crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
4860 crypto.fixed.validate_supported_profile()?;
4861 Ok(crypto.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN)
4862}
4863
4864fn validate_image_region(
4865 image: &CriticalMetadataImage,
4866 region_type: u16,
4867 offset: u64,
4868 length: u32,
4869) -> Result<(), FormatError> {
4870 let region = image
4871 .region(region_type)
4872 .ok_or(FormatError::InvalidArchive(
4873 "missing CriticalMetadataImage region",
4874 ))?;
4875 if region.offset != offset || region.bytes.len() != length as usize {
4876 return Err(FormatError::InvalidArchive(
4877 "CriticalMetadataImage region range mismatch",
4878 ));
4879 }
4880 Ok(())
4881}
4882
4883fn validate_image_identity(
4884 image: &CriticalMetadataImage,
4885 volume_header: &VolumeHeader,
4886 crypto_header: &CryptoHeaderFixed,
4887) -> Result<(), FormatError> {
4888 if image.archive_uuid != volume_header.archive_uuid
4889 || image.session_id != volume_header.session_id
4890 || image.volume_index != volume_header.volume_index
4891 || image.stripe_width != volume_header.stripe_width
4892 || image.stripe_width != crypto_header.stripe_width
4893 {
4894 return Err(FormatError::InvalidArchive(
4895 "CriticalMetadataImage identity does not match selected volume",
4896 ));
4897 }
4898 Ok(())
4899}
4900
4901fn sha256_region(image: &CriticalMetadataImage, region_type: u16) -> Result<[u8; 32], FormatError> {
4902 Ok(sha256_bytes(
4903 &image
4904 .region(region_type)
4905 .ok_or(FormatError::InvalidArchive(
4906 "missing CriticalMetadataImage region",
4907 ))?
4908 .bytes,
4909 ))
4910}
4911
4912fn validate_recovered_terminal(
4913 image: CriticalMetadataImage,
4914 tuple: CmraDecoderTuple,
4915 bytes: &[u8],
4916 context: KeyHoldingTerminalContext<'_>,
4917) -> Result<V41Terminal, FormatError> {
4918 let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
4919 let cmra_boundary_magic_ok = bytes.get(cmra_offset..cmra_offset + 4) == Some(b"TZCR");
4920 validate_recovered_terminal_inner(image, tuple, cmra_boundary_magic_ok, context)
4921}
4922
4923fn validate_recovered_terminal_read_at(
4924 image: CriticalMetadataImage,
4925 tuple: CmraDecoderTuple,
4926 reader: &dyn ArchiveReadAt,
4927 context: KeyHoldingTerminalContext<'_>,
4928) -> Result<V41Terminal, FormatError> {
4929 let mut magic = [0u8; 4];
4930 reader.read_exact_at(image.body_bytes_before_cmra, &mut magic)?;
4931 validate_recovered_terminal_inner(image, tuple, magic == *b"TZCR", context)
4932}
4933
4934fn validate_recovered_terminal_inner(
4935 image: CriticalMetadataImage,
4936 tuple: CmraDecoderTuple,
4937 cmra_boundary_magic_ok: bool,
4938 context: KeyHoldingTerminalContext<'_>,
4939) -> Result<V41Terminal, FormatError> {
4940 let subkeys = context.subkeys;
4941 let volume_header = context.volume_header;
4942 let crypto_header = context.crypto_header;
4943 let volume_header_region = image
4944 .region(1)
4945 .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
4946 let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
4947 if &recovered_volume_header != volume_header {
4948 return Err(FormatError::InvalidArchive(
4949 "CMRA VolumeHeader differs from parsed VolumeHeader",
4950 ));
4951 }
4952 validate_image_identity(&image, volume_header, crypto_header)?;
4953 let crypto_region = image
4954 .region(2)
4955 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
4956 let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
4957 if recovered_crypto.fixed != *crypto_header {
4958 return Err(FormatError::InvalidArchive(
4959 "CMRA CryptoHeader differs from parsed CryptoHeader",
4960 ));
4961 }
4962 let recovered_pre_hmac_len = crypto_region
4963 .bytes
4964 .len()
4965 .checked_sub(CRYPTO_HEADER_HMAC_LEN)
4966 .ok_or(FormatError::InvalidArchive(
4967 "CMRA CryptoHeader is too short",
4968 ))?;
4969 let parsed_pre_hmac_len = context
4970 .crypto_header_bytes
4971 .len()
4972 .checked_sub(CRYPTO_HEADER_HMAC_LEN)
4973 .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
4974 if recovered_pre_hmac_len != parsed_pre_hmac_len
4975 || crypto_region.bytes[..recovered_pre_hmac_len]
4976 != context.crypto_header_bytes[..parsed_pre_hmac_len]
4977 {
4978 return Err(FormatError::InvalidArchive(
4979 "CMRA CryptoHeader differs from parsed CryptoHeader",
4980 ));
4981 }
4982 verify_integrity_tag(
4983 HmacDomain::CryptoHeader,
4984 recovered_crypto.fixed.aead_algo,
4985 Some(&subkeys.mac_key),
4986 &volume_header.archive_uuid,
4987 &volume_header.session_id,
4988 recovered_crypto.hmac_covered_bytes,
4989 &recovered_crypto.header_hmac,
4990 )?;
4991 validate_cmra_writer_parity_lower_bound(tuple, recovered_crypto.fixed.bit_rot_buffer_pct)?;
4992 recovered_crypto.validate_extension_semantics()?;
4993
4994 let manifest_region = image
4995 .region(3)
4996 .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
4997 let manifest_footer = ManifestFooter::parse(&manifest_region.bytes)?;
4998 validate_manifest_footer(
4999 volume_header,
5000 crypto_header,
5001 &manifest_footer,
5002 subkeys,
5003 &manifest_region.bytes,
5004 )?;
5005 manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
5006
5007 let root_auth_footer = if image.layout_flags & 0x0000_0001 != 0 {
5008 let root_auth_region = image
5009 .region(4)
5010 .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
5011 let footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
5012 if footer.archive_uuid != volume_header.archive_uuid
5013 || footer.session_id != volume_header.session_id
5014 || footer.footer_length()? != image.root_auth_footer_length
5015 {
5016 return Err(FormatError::InvalidArchive(
5017 "RootAuthFooter identity or length does not match terminal image",
5018 ));
5019 }
5020 Some(footer)
5021 } else {
5022 None
5023 };
5024
5025 let trailer_region = image
5026 .region(5)
5027 .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
5028 let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
5029 verify_integrity_tag(
5030 HmacDomain::VolumeTrailer,
5031 crypto_header.aead_algo,
5032 Some(&subkeys.mac_key),
5033 &volume_header.archive_uuid,
5034 &volume_header.session_id,
5035 &trailer_region.bytes[..TRAILER_HMAC_COVERED_LEN],
5036 &trailer.trailer_hmac,
5037 )?;
5038 validate_trailer_identity(volume_header, &trailer)?;
5039 validate_v41_trailer_equations(&image, &trailer)?;
5040
5041 if !cmra_boundary_magic_ok {
5042 return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
5043 }
5044
5045 let manifest_footer_bytes = manifest_region.bytes.clone();
5046 let root_auth_footer_bytes = image.region(4).map(|region| region.bytes.clone());
5047 Ok(V41Terminal {
5048 image,
5049 manifest_footer_bytes,
5050 root_auth_footer_bytes,
5051 root_auth_footer,
5052 volume_trailer: trailer,
5053 })
5054}
5055
5056fn validate_recovered_public_terminal(
5057 image: CriticalMetadataImage,
5058 bytes: &[u8],
5059 volume_header: &VolumeHeader,
5060 crypto_header: &CryptoHeaderFixed,
5061) -> Result<V41PublicTerminal, FormatError> {
5062 if image.layout_flags & 0x0000_0001 == 0 {
5063 return Err(FormatError::ReaderUnsupported(
5064 "public no-key verification requires root-auth",
5065 ));
5066 }
5067 let volume_header_region = image
5068 .region(1)
5069 .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
5070 let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
5071 if &recovered_volume_header != volume_header {
5072 return Err(FormatError::InvalidArchive(
5073 "CMRA VolumeHeader differs from parsed VolumeHeader",
5074 ));
5075 }
5076 validate_image_identity(&image, volume_header, crypto_header)?;
5077 let crypto_region = image
5078 .region(2)
5079 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
5080 let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
5081 if recovered_crypto.fixed != *crypto_header {
5082 return Err(FormatError::InvalidArchive(
5083 "CMRA CryptoHeader differs from parsed CryptoHeader",
5084 ));
5085 }
5086 recovered_crypto.validate_extension_semantics()?;
5087
5088 image
5089 .region(3)
5090 .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
5091
5092 let root_auth_region = image
5093 .region(4)
5094 .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
5095 let root_auth_footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
5096 if root_auth_footer.archive_uuid != volume_header.archive_uuid
5097 || root_auth_footer.session_id != volume_header.session_id
5098 || root_auth_footer.footer_length()? != image.root_auth_footer_length
5099 {
5100 return Err(FormatError::InvalidArchive(
5101 "public RootAuthFooter identity or length does not match terminal image",
5102 ));
5103 }
5104
5105 let trailer_region = image
5106 .region(5)
5107 .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
5108 let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
5109 validate_trailer_identity(volume_header, &trailer)?;
5110 validate_v41_public_trailer_profile(&image, &trailer)?;
5111
5112 let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
5113 if bytes.get(cmra_offset..cmra_offset + 4) != Some(b"TZCR") {
5114 return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
5115 }
5116
5117 let root_auth_footer_bytes = root_auth_region.bytes.clone();
5118 Ok(V41PublicTerminal {
5119 image,
5120 root_auth_footer_bytes,
5121 root_auth_footer,
5122 })
5123}
5124
5125fn validate_v41_trailer_equations(
5126 image: &CriticalMetadataImage,
5127 trailer: &VolumeTrailer,
5128) -> Result<(), FormatError> {
5129 let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
5130 if trailer.bytes_written != image.volume_trailer_offset
5131 || trailer.manifest_footer_offset != image.manifest_footer_offset
5132 || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
5133 || trailer.block_count != image.block_count
5134 {
5135 return Err(FormatError::InvalidArchive(
5136 "VolumeTrailer does not match v41 terminal layout",
5137 ));
5138 }
5139 if root_auth_present {
5140 if trailer.root_auth_flags != 0x0000_0001
5141 || trailer.root_auth_footer_offset != image.root_auth_footer_offset
5142 || trailer.root_auth_footer_length != image.root_auth_footer_length
5143 || image.root_auth_footer_offset
5144 != image
5145 .manifest_footer_offset
5146 .checked_add(MANIFEST_FOOTER_LEN as u64)
5147 .ok_or(FormatError::InvalidArchive(
5148 "RootAuthFooter trailer boundary overflow",
5149 ))?
5150 || image
5151 .root_auth_footer_offset
5152 .checked_add(image.root_auth_footer_length as u64)
5153 .ok_or(FormatError::InvalidArchive(
5154 "RootAuthFooter trailer boundary overflow",
5155 ))?
5156 != image.volume_trailer_offset
5157 {
5158 return Err(FormatError::InvalidArchive(
5159 "VolumeTrailer root-auth fields do not match v41 terminal layout",
5160 ));
5161 }
5162 } else if trailer.root_auth_footer_offset != 0
5163 || trailer.root_auth_footer_length != 0
5164 || trailer.root_auth_flags != 0
5165 {
5166 return Err(FormatError::InvalidArchive(
5167 "VolumeTrailer root-auth fields must be zero when absent",
5168 ));
5169 }
5170 Ok(())
5171}
5172
5173fn validate_v41_public_trailer_profile(
5174 image: &CriticalMetadataImage,
5175 trailer: &VolumeTrailer,
5176) -> Result<(), FormatError> {
5177 if trailer.bytes_written != image.volume_trailer_offset
5178 || trailer.manifest_footer_offset != image.manifest_footer_offset
5179 || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
5180 {
5181 return Err(FormatError::InvalidArchive(
5182 "VolumeTrailer does not match v41 public terminal layout",
5183 ));
5184 }
5185 if trailer.root_auth_flags != 0x0000_0001
5186 || trailer.root_auth_footer_offset == 0
5187 || trailer.root_auth_footer_length == 0
5188 || trailer.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
5189 || trailer.root_auth_footer_offset != image.root_auth_footer_offset
5190 || trailer.root_auth_footer_length != image.root_auth_footer_length
5191 || image.root_auth_footer_offset
5192 != image
5193 .manifest_footer_offset
5194 .checked_add(MANIFEST_FOOTER_LEN as u64)
5195 .ok_or(FormatError::InvalidArchive(
5196 "RootAuthFooter trailer boundary overflow",
5197 ))?
5198 || image
5199 .root_auth_footer_offset
5200 .checked_add(image.root_auth_footer_length as u64)
5201 .ok_or(FormatError::InvalidArchive(
5202 "RootAuthFooter trailer boundary overflow",
5203 ))?
5204 != image.volume_trailer_offset
5205 {
5206 return Err(FormatError::InvalidArchive(
5207 "VolumeTrailer root-auth fields do not match v41 public terminal layout",
5208 ));
5209 }
5210 Ok(())
5211}
5212
5213fn critical_image_min() -> u64 {
5214 const MIN_CRYPTO_HEADER_LEN: u64 = 116;
5215 CRITICAL_METADATA_IMAGE_FIXED_LEN as u64
5216 + 4 * SERIALIZED_REGION_HEADER_LEN as u64
5217 + VOLUME_HEADER_LEN as u64
5218 + MIN_CRYPTO_HEADER_LEN
5219 + MANIFEST_FOOTER_LEN as u64
5220 + VOLUME_TRAILER_LEN as u64
5221 + IMAGE_CRC_LEN as u64
5222}
5223
5224fn critical_image_cap() -> Result<u64, FormatError> {
5225 [
5226 CRITICAL_METADATA_IMAGE_FIXED_LEN as u64,
5227 5 * SERIALIZED_REGION_HEADER_LEN as u64,
5228 VOLUME_HEADER_LEN as u64,
5229 READER_MAX_CRYPTO_HEADER_LEN as u64,
5230 MANIFEST_FOOTER_LEN as u64,
5231 READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
5232 VOLUME_TRAILER_LEN as u64,
5233 IMAGE_CRC_LEN as u64,
5234 ]
5235 .into_iter()
5236 .try_fold(0u64, |total, value| {
5237 total
5238 .checked_add(value)
5239 .ok_or(FormatError::InvalidArchive("critical image cap overflow"))
5240 })
5241}
5242
5243fn cmra_serialized_length(tuple: CmraDecoderTuple) -> Result<u64, FormatError> {
5244 let shard_total = (tuple.data_shard_count as u64)
5245 .checked_add(tuple.parity_shard_count as u64)
5246 .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
5247 let row_len = (CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN as u64)
5248 .checked_add(tuple.shard_size as u64)
5249 .ok_or(FormatError::InvalidArchive("CMRA row length overflow"))?;
5250 checked_u64_mul(shard_total, row_len, "CMRA length overflow")?
5251 .checked_add(CRITICAL_METADATA_RECOVERY_HEADER_LEN as u64)
5252 .ok_or(FormatError::InvalidArchive("CMRA length overflow"))
5253}
5254
5255fn cmra_worst_case_cap() -> Result<u64, FormatError> {
5256 let cap = critical_image_cap()?;
5257 let mut worst = 0u64;
5258 let mut shard_size = 512u64;
5259 while shard_size <= 4096 {
5260 let data = ceil_div_u64(cap, shard_size)?;
5261 let parity = 2u64.max(ceil_div_u64(
5262 checked_u64_mul(data, READER_MAX_CMRA_PARITY_PCT as u64, "CMRA cap overflow")?,
5263 100,
5264 )?);
5265 let tuple = CmraDecoderTuple {
5266 shard_size: shard_size as u32,
5267 data_shard_count: u16::try_from(data)
5268 .map_err(|_| FormatError::InvalidArchive("CMRA cap data shard overflow"))?,
5269 parity_shard_count: u16::try_from(parity)
5270 .map_err(|_| FormatError::InvalidArchive("CMRA cap parity shard overflow"))?,
5271 image_length: u32::try_from(cap)
5272 .map_err(|_| FormatError::InvalidArchive("CMRA cap image overflow"))?,
5273 image_sha256: [0u8; 32],
5274 };
5275 worst = worst.max(cmra_serialized_length(tuple)?);
5276 shard_size += 2;
5277 }
5278 Ok(worst)
5279}
5280
5281pub(crate) fn v41_terminal_tail_cap() -> Result<usize, FormatError> {
5282 let total = [
5283 MANIFEST_FOOTER_LEN as u64,
5284 READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
5285 VOLUME_TRAILER_LEN as u64,
5286 cmra_worst_case_cap()?,
5287 LOCATOR_PAIR_LEN as u64,
5288 ]
5289 .into_iter()
5290 .try_fold(0u64, |sum, value| {
5291 sum.checked_add(value)
5292 .ok_or(FormatError::InvalidArchive("terminal tail cap overflow"))
5293 })?;
5294 usize::try_from(total).map_err(|_| FormatError::InvalidArchive("terminal tail cap overflow"))
5295}
5296
5297fn max_critical_recovery_scan(options: ReaderOptions) -> Result<usize, FormatError> {
5298 let worst = cmra_worst_case_cap()?;
5299 let total = options
5300 .max_trailing_garbage_scan
5301 .try_into()
5302 .map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
5303 .and_then(|scan: u64| {
5304 scan.checked_add(worst)
5305 .and_then(|value| value.checked_add(LOCATOR_PAIR_LEN as u64))
5306 .ok_or(FormatError::InvalidArchive("scan cap overflow"))
5307 })?;
5308 usize::try_from(total).map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
5309}
5310
5311fn validate_bootstrap_single_volume_input(
5312 volume_header: &VolumeHeader,
5313 crypto_header: &CryptoHeaderFixed,
5314) -> Result<(), FormatError> {
5315 if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
5316 return Err(FormatError::ReaderUnsupported(
5317 "bootstrap sidecar reader supports only single-volume archive input",
5318 ));
5319 }
5320 if crypto_header.stripe_width != volume_header.stripe_width {
5321 return Err(FormatError::InvalidArchive(
5322 "VolumeHeader and CryptoHeader stripe_width differ",
5323 ));
5324 }
5325 Ok(())
5326}
5327
5328#[derive(Debug)]
5329struct ParsedBootstrapSidecar {
5330 manifest_footer: Option<ManifestFooter>,
5331 index_root_records_section: Option<(u64, u64)>,
5332 dictionary_records_section: Option<(u64, u64)>,
5333}
5334
5335pub(crate) struct NonSeekableBootstrapMaterial {
5336 pub(crate) manifest_footer: ManifestFooter,
5337 pub(crate) payload_dictionary: Option<Vec<u8>>,
5338}
5339
5340#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5341enum BootstrapSidecarUse {
5342 SeekableAssist,
5343 NonSeekableRandomAccess,
5344}
5345
5346impl ParsedBootstrapSidecar {
5347 fn require_sections_for(
5348 &self,
5349 sidecar_use: BootstrapSidecarUse,
5350 crypto_header: &CryptoHeaderFixed,
5351 ) -> Result<(), FormatError> {
5352 if sidecar_use == BootstrapSidecarUse::NonSeekableRandomAccess {
5353 if self.manifest_footer.is_none() || self.index_root_records_section.is_none() {
5354 return Err(FormatError::ReaderUnsupported(
5355 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
5356 ));
5357 }
5358 if crypto_header.has_dictionary != 0 && self.dictionary_records_section.is_none() {
5359 return Err(FormatError::ReaderUnsupported(
5360 "dictionary bootstrap required",
5361 ));
5362 }
5363 }
5364 Ok(())
5365 }
5366}
5367
5368pub(crate) fn parse_non_seekable_bootstrap_material(
5369 bootstrap_sidecar: &[u8],
5370 volume_header: &VolumeHeader,
5371 crypto_header: &CryptoHeaderFixed,
5372 subkeys: &Subkeys,
5373) -> Result<NonSeekableBootstrapMaterial, FormatError> {
5374 validate_bootstrap_single_volume_input(volume_header, crypto_header)?;
5375 let sidecar =
5376 parse_bootstrap_sidecar(bootstrap_sidecar, volume_header, crypto_header, subkeys)?;
5377 sidecar.require_sections_for(BootstrapSidecarUse::NonSeekableRandomAccess, crypto_header)?;
5378 let manifest_footer = sidecar
5379 .manifest_footer
5380 .clone()
5381 .ok_or(FormatError::ReaderUnsupported(
5382 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
5383 ))?;
5384
5385 let mut blocks = BTreeMap::new();
5386 let (offset, length) =
5387 sidecar
5388 .index_root_records_section
5389 .ok_or(FormatError::ReaderUnsupported(
5390 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
5391 ))?;
5392 let index_root_records = parse_sidecar_block_records(
5393 bootstrap_sidecar,
5394 crypto_header.block_size as usize,
5395 SidecarBlockRecordsSection {
5396 offset,
5397 length,
5398 extent: index_root_extent_from_manifest(&manifest_footer),
5399 data_kind: BlockKind::IndexRootData,
5400 parity_kind: BlockKind::IndexRootParity,
5401 structure: "IndexRoot",
5402 },
5403 )?;
5404 insert_sidecar_records(&mut blocks, index_root_records)?;
5405
5406 let limits = metadata_limits(crypto_header);
5407 let index_root_plaintext = load_metadata_object_from_parts(
5408 &blocks,
5409 ObjectLoadContext::index_root(
5410 volume_header,
5411 crypto_header,
5412 subkeys,
5413 index_root_extent_from_manifest(&manifest_footer),
5414 ),
5415 manifest_footer.index_root_decompressed_size,
5416 )?;
5417 let index_root = IndexRoot::parse(
5418 &index_root_plaintext,
5419 crypto_header.has_dictionary != 0,
5420 limits,
5421 )?;
5422
5423 if crypto_header.has_dictionary != 0 {
5424 let (offset, length) =
5425 sidecar
5426 .dictionary_records_section
5427 .ok_or(FormatError::ReaderUnsupported(
5428 "dictionary bootstrap required",
5429 ))?;
5430 let dictionary_records = parse_sidecar_block_records(
5431 bootstrap_sidecar,
5432 crypto_header.block_size as usize,
5433 SidecarBlockRecordsSection {
5434 offset,
5435 length,
5436 extent: dictionary_extent_from_index_root(&index_root)?,
5437 data_kind: BlockKind::DictionaryData,
5438 parity_kind: BlockKind::DictionaryParity,
5439 structure: "dictionary",
5440 },
5441 )?;
5442 insert_sidecar_records(&mut blocks, dictionary_records)?;
5443 }
5444 let payload_dictionary =
5445 load_archive_dictionary(&blocks, subkeys, volume_header, crypto_header, &index_root)?;
5446
5447 Ok(NonSeekableBootstrapMaterial {
5448 manifest_footer,
5449 payload_dictionary,
5450 })
5451}
5452
5453fn parse_bootstrap_sidecar(
5454 bytes: &[u8],
5455 volume_header: &VolumeHeader,
5456 crypto_header: &CryptoHeaderFixed,
5457 subkeys: &Subkeys,
5458) -> Result<ParsedBootstrapSidecar, FormatError> {
5459 let header_bytes = slice(
5460 bytes,
5461 0,
5462 BOOTSTRAP_SIDECAR_HEADER_LEN,
5463 "BootstrapSidecarHeader",
5464 )?;
5465 let header = BootstrapSidecarHeader::parse(header_bytes)?;
5466 if header.archive_uuid != volume_header.archive_uuid
5467 || header.session_id != volume_header.session_id
5468 {
5469 return Err(FormatError::InvalidArchive(
5470 "bootstrap sidecar identity does not match VolumeHeader",
5471 ));
5472 }
5473 verify_integrity_tag(
5474 HmacDomain::BootstrapSidecar,
5475 crypto_header.aead_algo,
5476 Some(&subkeys.mac_key),
5477 &volume_header.archive_uuid,
5478 &volume_header.session_id,
5479 &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
5480 &header.sidecar_hmac,
5481 )?;
5482 header.validate_packed_layout(bytes.len() as u64)?;
5483 validate_sidecar_size_cap(&header, crypto_header, bytes.len() as u64)?;
5484
5485 if header.has_dictionary_records() && crypto_header.has_dictionary == 0 {
5486 return Err(FormatError::InvalidArchive(
5487 "bootstrap sidecar has dictionary records while has_dictionary is false",
5488 ));
5489 }
5490
5491 let manifest_footer = if header.has_manifest_footer() {
5492 let manifest_offset = to_usize(header.manifest_footer_offset, "BootstrapSidecarHeader")?;
5493 let manifest_bytes = slice(
5494 bytes,
5495 manifest_offset,
5496 MANIFEST_FOOTER_LEN,
5497 "ManifestFooter",
5498 )?;
5499 let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
5500 validate_sidecar_manifest_footer(
5501 volume_header,
5502 crypto_header,
5503 &manifest_footer,
5504 subkeys,
5505 manifest_bytes,
5506 )?;
5507 manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
5508 Some(manifest_footer)
5509 } else {
5510 None
5511 };
5512
5513 Ok(ParsedBootstrapSidecar {
5514 manifest_footer,
5515 index_root_records_section: header.has_index_root_records().then_some((
5516 header.index_root_records_offset,
5517 header.index_root_records_length,
5518 )),
5519 dictionary_records_section: header.has_dictionary_records().then_some((
5520 header.dictionary_records_offset,
5521 header.dictionary_records_length,
5522 )),
5523 })
5524}
5525
5526fn index_root_extent_from_manifest(manifest_footer: &ManifestFooter) -> ObjectExtent {
5527 ObjectExtent {
5528 first_block_index: manifest_footer.index_root_first_block,
5529 data_block_count: manifest_footer.index_root_data_block_count,
5530 parity_block_count: manifest_footer.index_root_parity_block_count,
5531 encrypted_size: manifest_footer.index_root_encrypted_size,
5532 }
5533}
5534
5535fn insert_sidecar_records(
5536 blocks: &mut BTreeMap<u64, BlockRecord>,
5537 records: Vec<BlockRecord>,
5538) -> Result<(), FormatError> {
5539 for record in records {
5540 if let Some(existing) = blocks.insert(record.block_index, record.clone()) {
5541 if existing != record {
5542 return Err(FormatError::InvalidArchive(
5543 "bootstrap sidecar conflicts with volume BlockRecord",
5544 ));
5545 }
5546 }
5547 }
5548 Ok(())
5549}
5550
5551fn validate_sidecar_manifest_footer(
5552 volume_header: &VolumeHeader,
5553 crypto_header: &CryptoHeaderFixed,
5554 footer: &ManifestFooter,
5555 subkeys: &Subkeys,
5556 raw: &[u8],
5557) -> Result<(), FormatError> {
5558 if footer.archive_uuid != volume_header.archive_uuid
5559 || footer.session_id != volume_header.session_id
5560 {
5561 return Err(FormatError::InvalidArchive(
5562 "sidecar ManifestFooter identity does not match VolumeHeader",
5563 ));
5564 }
5565 if footer.volume_index != 0 {
5566 return Err(FormatError::InvalidArchive(
5567 "sidecar ManifestFooter volume_index must be zero",
5568 ));
5569 }
5570 if footer.total_volumes != crypto_header.stripe_width {
5571 return Err(FormatError::InvalidArchive(
5572 "sidecar ManifestFooter total_volumes does not match stripe_width",
5573 ));
5574 }
5575 if footer.is_authoritative != 1 {
5576 return Err(FormatError::InvalidArchive(
5577 "sidecar ManifestFooter is not authoritative",
5578 ));
5579 }
5580 verify_integrity_tag(
5581 HmacDomain::ManifestFooter,
5582 crypto_header.aead_algo,
5583 Some(&subkeys.mac_key),
5584 &volume_header.archive_uuid,
5585 &volume_header.session_id,
5586 &raw[..MANIFEST_HMAC_COVERED_LEN],
5587 &footer.manifest_hmac,
5588 )
5589}
5590
5591fn validate_sidecar_size_cap(
5592 header: &BootstrapSidecarHeader,
5593 crypto_header: &CryptoHeaderFixed,
5594 file_size: u64,
5595) -> Result<(), FormatError> {
5596 let record_len = checked_u64_add(
5597 crypto_header.block_size as u64,
5598 BLOCK_RECORD_FRAMING_LEN as u64,
5599 "bootstrap sidecar cap overflow",
5600 )?;
5601 let max_index_records = crypto_header.index_root_fec_data_shards as u64
5602 + crypto_header.index_root_fec_parity_shards as u64;
5603 let max_record_section_bytes = checked_u64_mul(
5604 max_index_records,
5605 record_len,
5606 "bootstrap sidecar cap overflow",
5607 )?;
5608 if header.index_root_records_length % record_len != 0 {
5609 return Err(FormatError::InvalidArchive(
5610 "bootstrap sidecar IndexRoot records length is not aligned",
5611 ));
5612 }
5613 if header.index_root_records_length / record_len > max_index_records {
5614 return Err(FormatError::InvalidArchive(
5615 "bootstrap sidecar IndexRoot records exceed resource cap",
5616 ));
5617 }
5618 if header.dictionary_records_length % record_len != 0 {
5619 return Err(FormatError::InvalidArchive(
5620 "bootstrap sidecar dictionary records length is not aligned",
5621 ));
5622 }
5623 if header.dictionary_records_length / record_len > max_index_records {
5624 return Err(FormatError::InvalidArchive(
5625 "bootstrap sidecar dictionary records exceed resource cap",
5626 ));
5627 }
5628
5629 let mut cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64;
5630 if header.has_manifest_footer() {
5631 cap = cap
5632 .checked_add(MANIFEST_FOOTER_LEN as u64)
5633 .ok_or(FormatError::InvalidArchive(
5634 "bootstrap sidecar cap overflow",
5635 ))?;
5636 }
5637 if header.has_index_root_records() {
5638 cap = checked_u64_add(
5639 cap,
5640 max_record_section_bytes,
5641 "bootstrap sidecar cap overflow",
5642 )?;
5643 }
5644 if header.has_dictionary_records() {
5645 cap = checked_u64_add(
5646 cap,
5647 max_record_section_bytes,
5648 "bootstrap sidecar cap overflow",
5649 )?;
5650 }
5651 if file_size > cap {
5652 return Err(FormatError::InvalidArchive(
5653 "bootstrap sidecar exceeds resource cap",
5654 ));
5655 }
5656 Ok(())
5657}
5658
5659#[derive(Debug, Clone, Copy)]
5660struct SidecarBlockRecordsSection {
5661 offset: u64,
5662 length: u64,
5663 extent: ObjectExtent,
5664 data_kind: BlockKind,
5665 parity_kind: BlockKind,
5666 structure: &'static str,
5667}
5668
5669fn parse_sidecar_block_records(
5670 sidecar_bytes: &[u8],
5671 block_size: usize,
5672 section: SidecarBlockRecordsSection,
5673) -> Result<Vec<BlockRecord>, FormatError> {
5674 let record_len = block_size
5675 .checked_add(BLOCK_RECORD_FRAMING_LEN)
5676 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
5677 if section.length % record_len as u64 != 0 {
5678 return Err(FormatError::InvalidArchive(
5679 "sidecar BlockRecord section is not aligned",
5680 ));
5681 }
5682 let expected_count =
5683 section.extent.data_block_count as usize + section.extent.parity_block_count as usize;
5684 let actual_count = usize::try_from(section.length / record_len as u64)
5685 .map_err(|_| FormatError::InvalidArchive("sidecar BlockRecord count overflow"))?;
5686 if actual_count != expected_count {
5687 return Err(FormatError::InvalidArchive(
5688 "sidecar BlockRecord section does not match declared extent",
5689 ));
5690 }
5691 let start = to_usize(section.offset, "BootstrapSidecarHeader")?;
5692 let raw = slice(
5693 sidecar_bytes,
5694 start,
5695 to_usize(section.length, "BootstrapSidecarHeader")?,
5696 "BootstrapSidecarHeader",
5697 )?;
5698 let mut records = Vec::with_capacity(expected_count);
5699
5700 for idx in 0..expected_count {
5701 let record = BlockRecord::parse(
5702 slice(raw, idx * record_len, record_len, "BlockRecord")?,
5703 block_size,
5704 )?;
5705 let expected_block_index = checked_u64_add(
5706 section.extent.first_block_index,
5707 idx as u64,
5708 section.structure,
5709 )?;
5710 if record.block_index != expected_block_index {
5711 return Err(FormatError::InvalidArchive(
5712 "sidecar BlockRecord section has missing or duplicate blocks",
5713 ));
5714 }
5715 let expected_kind = if idx < section.extent.data_block_count as usize {
5716 section.data_kind
5717 } else {
5718 section.parity_kind
5719 };
5720 if record.kind != expected_kind {
5721 return Err(FormatError::InvalidArchive(
5722 "sidecar BlockRecord section has wrong kind",
5723 ));
5724 }
5725 let should_be_last = idx + 1 == section.extent.data_block_count as usize;
5726 if idx < section.extent.data_block_count as usize && record.is_last_data() != should_be_last
5727 {
5728 return Err(FormatError::InvalidArchive(
5729 "sidecar BlockRecord section has wrong last-data flag",
5730 ));
5731 }
5732 records.push(record);
5733 }
5734
5735 Ok(records)
5736}
5737
5738fn validate_trailer_identity(
5739 volume_header: &VolumeHeader,
5740 trailer: &VolumeTrailer,
5741) -> Result<(), FormatError> {
5742 if trailer.archive_uuid != volume_header.archive_uuid
5743 || trailer.session_id != volume_header.session_id
5744 || trailer.volume_index != volume_header.volume_index
5745 {
5746 return Err(FormatError::InvalidArchive(
5747 "VolumeTrailer identity does not match VolumeHeader",
5748 ));
5749 }
5750 Ok(())
5751}
5752
5753fn validate_manifest_footer(
5754 volume_header: &VolumeHeader,
5755 crypto_header: &CryptoHeaderFixed,
5756 footer: &ManifestFooter,
5757 subkeys: &Subkeys,
5758 raw: &[u8],
5759) -> Result<(), FormatError> {
5760 if footer.archive_uuid != volume_header.archive_uuid
5761 || footer.session_id != volume_header.session_id
5762 || footer.volume_index != volume_header.volume_index
5763 {
5764 return Err(FormatError::InvalidArchive(
5765 "ManifestFooter identity does not match VolumeHeader",
5766 ));
5767 }
5768 if footer.total_volumes != volume_header.stripe_width {
5769 return Err(FormatError::InvalidArchive(
5770 "ManifestFooter total_volumes does not match stripe_width",
5771 ));
5772 }
5773 if footer.is_authoritative != 1 {
5774 return Err(FormatError::InvalidArchive(
5775 "ManifestFooter is not authoritative",
5776 ));
5777 }
5778 verify_integrity_tag(
5779 HmacDomain::ManifestFooter,
5780 crypto_header.aead_algo,
5781 Some(&subkeys.mac_key),
5782 &volume_header.archive_uuid,
5783 &volume_header.session_id,
5784 &raw[..MANIFEST_HMAC_COVERED_LEN],
5785 &footer.manifest_hmac,
5786 )
5787}
5788
5789#[derive(Debug)]
5790struct ParsedBlockRegion {
5791 blocks: BTreeMap<u64, BlockRecord>,
5792 erased_block_indices: BTreeSet<u64>,
5793}
5794
5795fn parse_block_region(
5796 bytes: &[u8],
5797 start: usize,
5798 end: usize,
5799 block_size: usize,
5800 volume_header: &VolumeHeader,
5801 trailer: &VolumeTrailer,
5802) -> Result<ParsedBlockRegion, FormatError> {
5803 if end < start {
5804 return Err(FormatError::InvalidArchive(
5805 "ManifestFooter starts before BlockRecord region",
5806 ));
5807 }
5808 let record_len = block_size
5809 .checked_add(BLOCK_RECORD_FRAMING_LEN)
5810 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
5811 let region_len = end - start;
5812 if region_len % record_len != 0 {
5813 return Err(FormatError::InvalidArchive(
5814 "BlockRecord region length is not aligned",
5815 ));
5816 }
5817 let observed_count = region_len / record_len;
5818 if observed_count as u64 != trailer.block_count {
5819 return Err(FormatError::InvalidArchive(
5820 "VolumeTrailer block_count does not match BlockRecord region",
5821 ));
5822 }
5823
5824 let mut blocks = BTreeMap::new();
5825 let mut erased_block_indices = BTreeSet::new();
5826 for idx in 0..observed_count {
5827 let offset = start + idx * record_len;
5828 let expected_block_index = checked_u64_add(
5829 volume_header.volume_index as u64,
5830 checked_u64_mul(
5831 idx as u64,
5832 volume_header.stripe_width as u64,
5833 "BlockRecord index overflow",
5834 )?,
5835 "BlockRecord index overflow",
5836 )?;
5837 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
5838 match BlockRecord::parse(raw, block_size) {
5839 Ok(record) => {
5840 if record.block_index != expected_block_index {
5841 return Err(FormatError::InvalidArchive(
5842 "BlockRecord index does not match volume position",
5843 ));
5844 }
5845 if blocks.insert(record.block_index, record).is_some() {
5846 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
5847 }
5848 }
5849 Err(err) if block_record_error_is_recoverable_erasure(&err) => {
5850 if !erased_block_indices.insert(expected_block_index) {
5851 return Err(FormatError::InvalidArchive(
5852 "duplicate erased BlockRecord index",
5853 ));
5854 }
5855 }
5856 Err(err) => return Err(err),
5857 }
5858 }
5859
5860 Ok(ParsedBlockRegion {
5861 blocks,
5862 erased_block_indices,
5863 })
5864}
5865
5866fn validate_seekable_block_region_layout(
5867 start: u64,
5868 end: u64,
5869 block_size: usize,
5870 trailer: &VolumeTrailer,
5871) -> Result<(), FormatError> {
5872 if end < start {
5873 return Err(FormatError::InvalidArchive(
5874 "ManifestFooter starts before BlockRecord region",
5875 ));
5876 }
5877 let record_len = block_record_len(block_size)?;
5878 let region_len = end - start;
5879 if region_len % record_len != 0 {
5880 return Err(FormatError::InvalidArchive(
5881 "BlockRecord region length is not aligned",
5882 ));
5883 }
5884 let observed_count = region_len / record_len;
5885 if observed_count != trailer.block_count {
5886 return Err(FormatError::InvalidArchive(
5887 "VolumeTrailer block_count does not match BlockRecord region",
5888 ));
5889 }
5890 Ok(())
5891}
5892
5893fn parse_public_block_observation(
5894 bytes: &[u8],
5895 start: usize,
5896 image: &CriticalMetadataImage,
5897 block_size: usize,
5898 volume_header: &VolumeHeader,
5899) -> Result<BTreeMap<u64, BlockRecord>, FormatError> {
5900 let image_start = to_usize(image.block_records_offset, "BlockRecord")?;
5901 if start != image_start {
5902 return Err(FormatError::InvalidArchive(
5903 "public BlockRecord observation start mismatch",
5904 ));
5905 }
5906 let scan_limit_u64 = image
5907 .block_records_offset
5908 .checked_add(image.block_records_length)
5909 .ok_or(FormatError::InvalidArchive(
5910 "public BlockRecord observation limit overflow",
5911 ))?;
5912 if scan_limit_u64 != image.manifest_footer_offset {
5913 return Err(FormatError::InvalidArchive(
5914 "public BlockRecord observation limit mismatch",
5915 ));
5916 }
5917 let scan_limit = to_usize(scan_limit_u64, "BlockRecord")?;
5918 if scan_limit < start {
5919 return Err(FormatError::InvalidArchive(
5920 "public BlockRecord observation limit before start",
5921 ));
5922 }
5923 let record_len = block_size
5924 .checked_add(BLOCK_RECORD_FRAMING_LEN)
5925 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
5926 let region_len = scan_limit - start;
5927 if region_len % record_len != 0 {
5928 return Err(FormatError::InvalidArchive(
5929 "public BlockRecord observation window is not aligned",
5930 ));
5931 }
5932
5933 let mut blocks = BTreeMap::new();
5934 let mut offset = start;
5935 let mut observed_slot = 0u64;
5936 while offset < scan_limit {
5937 let magic_end = checked_add(offset, 4, "BlockRecord")?;
5938 if magic_end > scan_limit || bytes.get(offset..magic_end) != Some(b"TZBK") {
5939 break;
5940 }
5941 let record_end = checked_add(offset, record_len, "BlockRecord")?;
5942 if record_end > scan_limit {
5943 return Err(FormatError::InvalidArchive(
5944 "public BlockRecord observation slot is incomplete",
5945 ));
5946 }
5947 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
5948 let record = BlockRecord::parse(raw, block_size)?;
5949 let expected_block_index = checked_u64_add(
5950 volume_header.volume_index as u64,
5951 checked_u64_mul(
5952 observed_slot,
5953 volume_header.stripe_width as u64,
5954 "BlockRecord index overflow",
5955 )?,
5956 "BlockRecord index overflow",
5957 )?;
5958 if record.block_index != expected_block_index {
5959 return Err(FormatError::InvalidArchive(
5960 "public BlockRecord index does not match volume position",
5961 ));
5962 }
5963 if blocks.insert(record.block_index, record).is_some() {
5964 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
5965 }
5966 offset = record_end;
5967 observed_slot = observed_slot
5968 .checked_add(1)
5969 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
5970 }
5971
5972 let mut scan = if offset < scan_limit {
5973 checked_add(offset, record_len, "BlockRecord")?
5974 } else {
5975 scan_limit
5976 };
5977 while scan < scan_limit {
5978 let magic_end = checked_add(scan, 4, "BlockRecord")?;
5979 let record_end = checked_add(scan, record_len, "BlockRecord")?;
5980 if record_end <= scan_limit && bytes.get(scan..magic_end) == Some(b"TZBK") {
5981 let raw = slice(bytes, scan, record_len, "BlockRecord")?;
5982 if BlockRecord::parse(raw, block_size).is_ok() {
5983 return Err(FormatError::InvalidArchive(
5984 "public observation has ambiguous extra BlockRecord",
5985 ));
5986 }
5987 }
5988 scan = record_end;
5989 }
5990
5991 Ok(blocks)
5992}
5993
5994pub(crate) fn block_record_error_is_recoverable_erasure(error: &FormatError) -> bool {
5995 matches!(
5996 error,
5997 FormatError::BadCrc {
5998 structure: "BlockRecord",
5999 }
6000 )
6001}
6002
6003fn block_record_len(block_size: usize) -> Result<u64, FormatError> {
6004 let len = block_size
6005 .checked_add(BLOCK_RECORD_FRAMING_LEN)
6006 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6007 u64::try_from(len).map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))
6008}
6009
6010fn checked_u64_mul(lhs: u64, rhs: u64, reason: &'static str) -> Result<u64, FormatError> {
6011 lhs.checked_mul(rhs)
6012 .ok_or(FormatError::InvalidArchive(reason))
6013}
6014
6015fn parse_stream_block_prefix(
6016 bytes: &[u8],
6017 start: usize,
6018 block_size: usize,
6019 volume_header: &VolumeHeader,
6020) -> Result<(BTreeMap<u64, BlockRecord>, usize, u64), FormatError> {
6021 let record_len = block_size
6022 .checked_add(BLOCK_RECORD_FRAMING_LEN)
6023 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6024 let mut blocks = BTreeMap::new();
6025 let mut offset = start;
6026 let mut observed_block_count = 0u64;
6027
6028 while bytes.get(offset..offset + 4) == Some(b"TZBK") {
6029 let expected_block_index =
6030 expected_stream_block_index(volume_header, observed_block_count)?;
6031 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
6032 match BlockRecord::parse(raw, block_size) {
6033 Ok(record) => {
6034 if record.block_index != expected_block_index {
6035 return Err(FormatError::InvalidArchive(
6036 "BlockRecord index does not match stream position",
6037 ));
6038 }
6039 if blocks.insert(record.block_index, record).is_some() {
6040 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
6041 }
6042 }
6043 Err(err) if block_record_error_is_recoverable_erasure(&err) => {}
6044 Err(err) => return Err(err),
6045 }
6046 offset = checked_add(offset, record_len, "BlockRecord")?;
6047 observed_block_count = observed_block_count
6048 .checked_add(1)
6049 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
6050 }
6051
6052 Ok((blocks, offset, observed_block_count))
6053}
6054
6055pub(crate) fn expected_stream_block_index(
6056 volume_header: &VolumeHeader,
6057 observed_block_count: u64,
6058) -> Result<u64, FormatError> {
6059 checked_u64_add(
6060 volume_header.volume_index as u64,
6061 checked_u64_mul(
6062 observed_block_count,
6063 volume_header.stripe_width as u64,
6064 "BlockRecord index overflow",
6065 )?,
6066 "BlockRecord index overflow",
6067 )
6068}
6069
6070fn parse_sequential_block_or_erasure(
6071 bytes: &[u8],
6072 offset: usize,
6073 record_len: usize,
6074 block_size: usize,
6075 volume_header: &VolumeHeader,
6076 observed_block_count: u64,
6077) -> Result<Option<BlockRecord>, FormatError> {
6078 let expected_block_index = expected_stream_block_index(volume_header, observed_block_count)?;
6079 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
6080 match BlockRecord::parse(raw, block_size) {
6081 Ok(record) => {
6082 if record.block_index != expected_block_index {
6083 return Err(FormatError::InvalidArchive(
6084 "BlockRecord index does not match stream position",
6085 ));
6086 }
6087 Ok(Some(record))
6088 }
6089 Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
6090 Err(err) => Err(err),
6091 }
6092}
6093
6094fn parse_terminal_material(
6095 bytes: &[u8],
6096 manifest_offset: usize,
6097 observed_block_count: u64,
6098 context: KeyHoldingTerminalContext<'_>,
6099 options: ReaderOptions,
6100) -> Result<(ManifestFooter, VolumeTrailer, Option<RootAuthFooterV1>), FormatError> {
6101 let candidate = locate_v41_terminal_candidate(bytes, context, options)?;
6102 if !terminal_candidate_reaches_eof(&candidate, bytes.len())? {
6103 return Err(FormatError::InvalidArchive(
6104 "sequential terminal does not end at EOF",
6105 ));
6106 }
6107 let terminal = candidate.terminal;
6108 if terminal.image.manifest_footer_offset != manifest_offset as u64 {
6109 return Err(FormatError::InvalidArchive(
6110 "VolumeTrailer ManifestFooter offset does not match observed stream offset",
6111 ));
6112 }
6113 if terminal.volume_trailer.block_count != observed_block_count {
6114 return Err(FormatError::InvalidArchive(
6115 "VolumeTrailer block_count does not match observed stream",
6116 ));
6117 }
6118 let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
6119 Ok((
6120 manifest_footer,
6121 terminal.volume_trailer,
6122 terminal.root_auth_footer,
6123 ))
6124}
6125
6126pub(crate) fn parse_terminal_material_read_at(
6127 reader: &dyn ArchiveReadAt,
6128 input_len: u64,
6129 manifest_offset: u64,
6130 observed_block_count: u64,
6131 context: KeyHoldingTerminalContext<'_>,
6132) -> Result<SequentialTerminalMaterial, FormatError> {
6133 let mut candidates = Vec::new();
6134 if input_len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
6135 collect_v41_locator_candidate_read_at(
6136 reader,
6137 input_len - CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6138 0,
6139 context,
6140 &mut candidates,
6141 );
6142 }
6143 if input_len >= LOCATOR_PAIR_LEN as u64 {
6144 collect_v41_locator_candidate_read_at(
6145 reader,
6146 input_len - LOCATOR_PAIR_LEN as u64,
6147 1,
6148 context,
6149 &mut candidates,
6150 );
6151 }
6152
6153 let candidate = choose_v41_terminal_candidate(candidates)?;
6154 if !terminal_candidate_reaches_eof(&candidate, to_usize(input_len, "terminal EOF")?)? {
6155 return Err(FormatError::InvalidArchive(
6156 "sequential terminal does not end at EOF",
6157 ));
6158 }
6159 let terminal = candidate.terminal;
6160 if terminal.image.manifest_footer_offset != manifest_offset {
6161 return Err(FormatError::InvalidArchive(
6162 "VolumeTrailer ManifestFooter offset does not match observed stream offset",
6163 ));
6164 }
6165 if terminal.volume_trailer.block_count != observed_block_count {
6166 return Err(FormatError::InvalidArchive(
6167 "VolumeTrailer block_count does not match observed stream",
6168 ));
6169 }
6170 let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
6171 Ok(SequentialTerminalMaterial {
6172 manifest_footer,
6173 volume_trailer: terminal.volume_trailer,
6174 root_auth_footer: terminal.root_auth_footer,
6175 })
6176}
6177
6178fn terminal_candidate_reaches_eof(
6179 candidate: &TerminalCandidate,
6180 input_len: usize,
6181) -> Result<bool, FormatError> {
6182 let expected_end =
6183 match candidate.locator_sequence {
6184 Some(0) => candidate.anchor,
6185 Some(1) => candidate
6186 .anchor
6187 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6188 .ok_or(FormatError::InvalidArchive(
6189 "terminal EOF boundary overflow",
6190 ))?,
6191 None => candidate.anchor.checked_add(LOCATOR_PAIR_LEN).ok_or(
6192 FormatError::InvalidArchive("terminal EOF boundary overflow"),
6193 )?,
6194 Some(_) => {
6195 return Err(FormatError::InvalidArchive(
6196 "invalid terminal locator sequence",
6197 ))
6198 }
6199 };
6200 Ok(expected_end == input_len)
6201}
6202
6203#[derive(Debug, Default)]
6204struct PendingSequentialEnvelope {
6205 data_shards: Vec<Option<Vec<u8>>>,
6206 parity_shards: Vec<Option<Vec<u8>>>,
6207 saw_last_data: bool,
6208 awaiting_tentative_parity: bool,
6209}
6210
6211impl PendingSequentialEnvelope {
6212 fn is_empty(&self) -> bool {
6213 self.data_shards.is_empty() && self.parity_shards.is_empty()
6214 }
6215}
6216
6217fn handle_sequential_payload_erasure(
6218 pending: &mut PendingSequentialEnvelope,
6219 crypto_header: &CryptoHeaderFixed,
6220 metadata_seen: bool,
6221) -> Result<(), FormatError> {
6222 if metadata_seen || pending.saw_last_data {
6223 return Err(FormatError::BadCrc {
6224 structure: "BlockRecord",
6225 });
6226 }
6227 if !sequential_payload_parity_is_guaranteed(crypto_header) {
6228 return Err(FormatError::BadCrc {
6229 structure: "BlockRecord",
6230 });
6231 }
6232 pending.data_shards.push(None);
6233 pending.awaiting_tentative_parity = true;
6234 if pending.data_shards.len() > crypto_header.fec_data_shards as usize {
6235 return Err(FormatError::InvalidArchive(
6236 "sequential payload envelope exceeds data-shard cap",
6237 ));
6238 }
6239 Ok(())
6240}
6241
6242fn sequential_payload_parity_is_guaranteed(crypto_header: &CryptoHeaderFixed) -> bool {
6243 crypto_header.fec_parity_shards > 0
6244 && (crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0)
6245}
6246
6247fn sequential_extract_tar_stream_with_options(
6248 bytes: &[u8],
6249 master_key: &MasterKey,
6250 options: ReaderOptions,
6251) -> Result<Vec<u8>, FormatError> {
6252 validate_reader_options(options)?;
6253 if bytes.len() < VOLUME_HEADER_LEN {
6254 return Err(FormatError::InvalidLength {
6255 structure: "archive",
6256 expected: VOLUME_HEADER_LEN,
6257 actual: bytes.len(),
6258 });
6259 }
6260
6261 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
6262 let crypto_start = volume_header.crypto_header_offset as usize;
6263 let crypto_len = volume_header.crypto_header_length as usize;
6264 let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
6265 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
6266 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
6267 let subkeys = subkeys_for_open(
6268 Some(master_key),
6269 parsed_crypto.fixed.aead_algo,
6270 &volume_header.archive_uuid,
6271 &volume_header.session_id,
6272 )?;
6273 verify_integrity_tag(
6274 HmacDomain::CryptoHeader,
6275 parsed_crypto.fixed.aead_algo,
6276 Some(&subkeys.mac_key),
6277 &volume_header.archive_uuid,
6278 &volume_header.session_id,
6279 parsed_crypto.hmac_covered_bytes,
6280 &parsed_crypto.header_hmac,
6281 )?;
6282 parsed_crypto.validate_extension_semantics()?;
6283 validate_sequential_supported_volume(
6284 &volume_header,
6285 &parsed_crypto.fixed,
6286 &parsed_crypto.extensions,
6287 )?;
6288 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
6289
6290 let block_size = parsed_crypto.fixed.block_size as usize;
6291 let record_len = block_size
6292 .checked_add(BLOCK_RECORD_FRAMING_LEN)
6293 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6294 let mut offset = crypto_end;
6295 let mut observed_block_count = 0u64;
6296 let mut metadata_seen = false;
6297 let mut pending = PendingSequentialEnvelope::default();
6298 let mut next_envelope_index = 0u64;
6299 let mut tar_stream = Vec::new();
6300 let max_tar_stream_size = options.max_verify_tar_size;
6301 let observed_archive_bytes = observed_archive_size([bytes.len() as u64])?;
6302 let total_extraction_cap = total_extraction_size_cap(options, observed_archive_bytes);
6303 let mut tar_stream_total_validator = TarStreamTotalExtractionSizeValidator::new(
6304 parsed_crypto.fixed.max_path_length,
6305 total_extraction_cap,
6306 );
6307
6308 while bytes.get(offset..offset + 4) == Some(b"TZBK") {
6309 let record = parse_sequential_block_or_erasure(
6310 bytes,
6311 offset,
6312 record_len,
6313 block_size,
6314 &volume_header,
6315 observed_block_count,
6316 )?;
6317 observed_block_count = observed_block_count
6318 .checked_add(1)
6319 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
6320 let Some(record) = record else {
6321 handle_sequential_payload_erasure(&mut pending, &parsed_crypto.fixed, metadata_seen)?;
6322 offset = checked_add(offset, record_len, "BlockRecord")?;
6323 continue;
6324 };
6325
6326 match record.kind {
6327 BlockKind::PayloadData => {
6328 if metadata_seen {
6329 return Err(FormatError::InvalidArchive(
6330 "payload BlockRecord appears after metadata",
6331 ));
6332 }
6333 if pending.awaiting_tentative_parity {
6334 return Err(FormatError::InvalidArchive(
6335 "sequential payload envelope boundary is ambiguous after CRC erasure",
6336 ));
6337 }
6338 if pending.saw_last_data {
6339 finalize_sequential_envelope(
6340 &mut pending,
6341 SequentialEnvelopeDecodeContext {
6342 crypto_header: &parsed_crypto.fixed,
6343 subkeys: &subkeys,
6344 volume_header: &volume_header,
6345 next_envelope_index: &mut next_envelope_index,
6346 tar_stream: &mut tar_stream,
6347 max_tar_stream_size,
6348 tar_stream_total_validator: &mut tar_stream_total_validator,
6349 },
6350 )?;
6351 }
6352 let is_last_data = record.is_last_data();
6353 pending.data_shards.push(Some(record.payload));
6354 if is_last_data {
6355 pending.saw_last_data = true;
6356 }
6357 if pending.data_shards.len() > parsed_crypto.fixed.fec_data_shards as usize {
6358 return Err(FormatError::InvalidArchive(
6359 "sequential payload envelope exceeds data-shard cap",
6360 ));
6361 }
6362 }
6363 BlockKind::PayloadParity => {
6364 if metadata_seen {
6365 return Err(FormatError::InvalidArchive(
6366 "payload parity BlockRecord appears after metadata",
6367 ));
6368 }
6369 if pending.awaiting_tentative_parity {
6370 pending.awaiting_tentative_parity = false;
6371 pending.saw_last_data = true;
6372 } else if pending.data_shards.is_empty() || !pending.saw_last_data {
6373 return Err(FormatError::InvalidArchive(
6374 "payload parity appears before envelope data is complete",
6375 ));
6376 }
6377 pending.parity_shards.push(Some(record.payload));
6378 if pending.parity_shards.len() > parsed_crypto.fixed.fec_parity_shards as usize {
6379 return Err(FormatError::InvalidArchive(
6380 "sequential payload envelope exceeds parity-shard cap",
6381 ));
6382 }
6383 }
6384 _ => {
6385 if !pending.is_empty() {
6386 finalize_sequential_envelope(
6387 &mut pending,
6388 SequentialEnvelopeDecodeContext {
6389 crypto_header: &parsed_crypto.fixed,
6390 subkeys: &subkeys,
6391 volume_header: &volume_header,
6392 next_envelope_index: &mut next_envelope_index,
6393 tar_stream: &mut tar_stream,
6394 max_tar_stream_size,
6395 tar_stream_total_validator: &mut tar_stream_total_validator,
6396 },
6397 )?;
6398 }
6399 metadata_seen = true;
6400 }
6401 }
6402
6403 offset = checked_add(offset, record_len, "BlockRecord")?;
6404 }
6405
6406 if !pending.is_empty() {
6407 finalize_sequential_envelope(
6408 &mut pending,
6409 SequentialEnvelopeDecodeContext {
6410 crypto_header: &parsed_crypto.fixed,
6411 subkeys: &subkeys,
6412 volume_header: &volume_header,
6413 next_envelope_index: &mut next_envelope_index,
6414 tar_stream: &mut tar_stream,
6415 max_tar_stream_size,
6416 tar_stream_total_validator: &mut tar_stream_total_validator,
6417 },
6418 )?;
6419 }
6420
6421 parse_terminal_material(
6422 bytes,
6423 offset,
6424 observed_block_count,
6425 KeyHoldingTerminalContext {
6426 subkeys: &subkeys,
6427 volume_header: &volume_header,
6428 crypto_header: &parsed_crypto.fixed,
6429 crypto_header_bytes: crypto_bytes,
6430 },
6431 options,
6432 )?;
6433 validate_tar_stream_total_extraction_size(
6436 &tar_stream,
6437 parsed_crypto.fixed.max_path_length,
6438 total_extraction_cap,
6439 )?;
6440 Ok(tar_stream)
6441}
6442
6443fn validate_sequential_supported_volume(
6444 volume_header: &VolumeHeader,
6445 crypto_header: &CryptoHeaderFixed,
6446 extensions: &[ExtensionTlv<'_>],
6447) -> Result<(), FormatError> {
6448 reject_unsupported_raw_stream_profile(extensions)?;
6449 if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
6450 return Err(FormatError::ReaderUnsupported(
6451 "sequential reader supports only single-volume archive input",
6452 ));
6453 }
6454 if crypto_header.stripe_width != volume_header.stripe_width {
6455 return Err(FormatError::InvalidArchive(
6456 "VolumeHeader and CryptoHeader stripe_width differ",
6457 ));
6458 }
6459 if crypto_header.has_dictionary != 0 {
6460 return Err(FormatError::ReaderUnsupported(
6461 "dictionary bootstrap required for non-seekable sequential extraction",
6462 ));
6463 }
6464 Ok(())
6465}
6466
6467struct SequentialEnvelopeDecodeContext<'a> {
6468 crypto_header: &'a CryptoHeaderFixed,
6469 subkeys: &'a Subkeys,
6470 volume_header: &'a VolumeHeader,
6471 next_envelope_index: &'a mut u64,
6472 tar_stream: &'a mut Vec<u8>,
6473 max_tar_stream_size: usize,
6474 tar_stream_total_validator: &'a mut TarStreamTotalExtractionSizeValidator,
6475}
6476
6477fn finalize_sequential_envelope(
6478 pending: &mut PendingSequentialEnvelope,
6479 context: SequentialEnvelopeDecodeContext<'_>,
6480) -> Result<(), FormatError> {
6481 if !pending.saw_last_data {
6482 return Err(FormatError::InvalidArchive(
6483 "sequential payload envelope is missing last-data flag",
6484 ));
6485 }
6486 if pending.data_shards.len() > context.crypto_header.fec_data_shards as usize {
6487 return Err(FormatError::InvalidArchive(
6488 "sequential payload envelope exceeds data-shard cap",
6489 ));
6490 }
6491 if pending.parity_shards.len() > context.crypto_header.fec_parity_shards as usize {
6492 return Err(FormatError::InvalidArchive(
6493 "sequential payload envelope exceeds parity-shard cap",
6494 ));
6495 }
6496 let required_parity =
6497 required_object_parity(pending.data_shards.len() as u64, context.crypto_header)?;
6498 if pending.parity_shards.len() < required_parity as usize {
6499 return Err(FormatError::InvalidArchive(
6500 "sequential payload envelope has insufficient parity for recovery settings",
6501 ));
6502 }
6503
6504 let repaired = repair_data_gf16(
6505 &pending.data_shards,
6506 &pending.parity_shards,
6507 context.crypto_header.block_size as usize,
6508 )?;
6509 let mut encrypted =
6510 Vec::with_capacity(repaired.len() * context.crypto_header.block_size as usize);
6511 for shard in repaired {
6512 encrypted.extend_from_slice(&shard);
6513 }
6514 let plaintext = decrypt_padded_aead_object(
6515 AeadObjectContext {
6516 algo: context.crypto_header.aead_algo,
6517 key: &context.subkeys.enc_key,
6518 nonce_seed: &context.subkeys.nonce_seed,
6519 domain: b"envelope",
6520 archive_uuid: &context.volume_header.archive_uuid,
6521 session_id: &context.volume_header.session_id,
6522 counter: *context.next_envelope_index,
6523 },
6524 &encrypted,
6525 )?;
6526 decode_concatenated_zstd_frames_with_cap(
6527 &plaintext,
6528 None,
6529 context.tar_stream,
6530 context.max_tar_stream_size,
6531 Some(context.tar_stream_total_validator),
6532 )?;
6533 *context.next_envelope_index = (*context.next_envelope_index)
6534 .checked_add(1)
6535 .ok_or(FormatError::InvalidArchive("envelope counter overflow"))?;
6536 *pending = PendingSequentialEnvelope::default();
6537 Ok(())
6538}
6539
6540fn decode_concatenated_zstd_frames_with_cap(
6541 plaintext: &[u8],
6542 dictionary: Option<&[u8]>,
6543 output: &mut Vec<u8>,
6544 max_output_len: usize,
6545 mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
6546) -> Result<(), FormatError> {
6547 let mut cursor = 0usize;
6548 while cursor < plaintext.len() {
6549 let frame_len = zstd_safe::find_frame_compressed_size(&plaintext[cursor..])
6550 .map_err(|_| FormatError::InvalidZstdFrame)?;
6551 if frame_len == 0 {
6552 return Err(FormatError::InvalidZstdFrame);
6553 }
6554 let end = checked_add(cursor, frame_len, "zstd frame")?;
6555 validate_exact_zstd_frame(&plaintext[cursor..end])?;
6556 if let Some(dictionary) = dictionary {
6557 let mut decoder =
6558 zstd::stream::Decoder::with_dictionary(&plaintext[cursor..end], dictionary)
6559 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
6560 read_zstd_frame_to_capped_output(
6561 &mut decoder,
6562 output,
6563 max_output_len,
6564 tar_stream_total_validator.as_deref_mut(),
6565 )?;
6566 } else {
6567 let mut decoder = zstd::stream::Decoder::new(&plaintext[cursor..end])
6568 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
6569 read_zstd_frame_to_capped_output(
6570 &mut decoder,
6571 output,
6572 max_output_len,
6573 tar_stream_total_validator.as_deref_mut(),
6574 )?;
6575 }
6576 cursor = end;
6577 }
6578 Ok(())
6579}
6580
6581fn read_zstd_frame_to_capped_output<R: Read>(
6582 decoder: &mut R,
6583 output: &mut Vec<u8>,
6584 max_output_len: usize,
6585 mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
6586) -> Result<(), FormatError> {
6587 let mut buf = [0u8; 64 * 1024];
6588 loop {
6589 let read = decoder
6590 .read(&mut buf)
6591 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
6592 if read == 0 {
6593 return Ok(());
6594 }
6595 let next_len = output
6596 .len()
6597 .checked_add(read)
6598 .ok_or(FormatError::ReaderUnsupported(
6599 "sequential tar stream exceeds configured verification cap",
6600 ))?;
6601 if next_len > max_output_len {
6602 return Err(FormatError::ReaderUnsupported(
6603 "sequential tar stream exceeds configured verification cap",
6604 ));
6605 }
6606 output.extend_from_slice(&buf[..read]);
6607 if let Some(validator) = tar_stream_total_validator.as_mut() {
6608 validator.observe(output)?;
6609 }
6610 }
6611}
6612
6613fn load_archive_dictionary(
6614 blocks: &impl BlockProvider,
6615 subkeys: &Subkeys,
6616 volume_header: &VolumeHeader,
6617 crypto_header: &CryptoHeaderFixed,
6618 index_root: &IndexRoot,
6619) -> Result<Option<Vec<u8>>, FormatError> {
6620 if crypto_header.has_dictionary == 0 {
6621 return Ok(None);
6622 }
6623 let plaintext = load_metadata_object_from_parts(
6624 blocks,
6625 ObjectLoadContext::dictionary(volume_header, crypto_header, subkeys, index_root)?,
6626 index_root.header.dictionary_decompressed_size,
6627 )?;
6628 Ok(Some(plaintext))
6629}
6630
6631#[derive(Clone, Copy)]
6632struct ObjectLoadContext<'a> {
6633 volume_header: &'a VolumeHeader,
6634 crypto_header: &'a CryptoHeaderFixed,
6635 extent: ObjectExtent,
6636 data_kind: BlockKind,
6637 parity_kind: BlockKind,
6638 key: &'a [u8; 32],
6639 nonce_seed: &'a [u8; 32],
6640 domain: &'a [u8],
6641 counter: u64,
6642 class_data_shard_max: u16,
6643 class_parity_shard_max: u16,
6644}
6645
6646impl<'a> ObjectLoadContext<'a> {
6647 fn index_root(
6648 volume_header: &'a VolumeHeader,
6649 crypto_header: &'a CryptoHeaderFixed,
6650 subkeys: &'a Subkeys,
6651 extent: ObjectExtent,
6652 ) -> Self {
6653 Self {
6654 volume_header,
6655 crypto_header,
6656 extent,
6657 data_kind: BlockKind::IndexRootData,
6658 parity_kind: BlockKind::IndexRootParity,
6659 key: &subkeys.index_root_key,
6660 nonce_seed: &subkeys.index_nonce_seed,
6661 domain: b"idxroot",
6662 counter: 0,
6663 class_data_shard_max: crypto_header.index_root_fec_data_shards,
6664 class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
6665 }
6666 }
6667
6668 fn index_shard(
6669 volume_header: &'a VolumeHeader,
6670 crypto_header: &'a CryptoHeaderFixed,
6671 subkeys: &'a Subkeys,
6672 entry: &ShardEntry,
6673 ) -> Self {
6674 Self {
6675 volume_header,
6676 crypto_header,
6677 extent: ObjectExtent {
6678 first_block_index: entry.first_block_index,
6679 data_block_count: entry.data_block_count,
6680 parity_block_count: entry.parity_block_count,
6681 encrypted_size: entry.encrypted_size,
6682 },
6683 data_kind: BlockKind::IndexShardData,
6684 parity_kind: BlockKind::IndexShardParity,
6685 key: &subkeys.index_shard_key,
6686 nonce_seed: &subkeys.index_nonce_seed,
6687 domain: b"idxshard",
6688 counter: entry.shard_index,
6689 class_data_shard_max: crypto_header.index_fec_data_shards,
6690 class_parity_shard_max: crypto_header.index_fec_parity_shards,
6691 }
6692 }
6693
6694 fn directory_hint(
6695 volume_header: &'a VolumeHeader,
6696 crypto_header: &'a CryptoHeaderFixed,
6697 subkeys: &'a Subkeys,
6698 entry: &DirectoryHintShardEntry,
6699 ) -> Self {
6700 Self {
6701 volume_header,
6702 crypto_header,
6703 extent: ObjectExtent {
6704 first_block_index: entry.first_block_index,
6705 data_block_count: entry.data_block_count,
6706 parity_block_count: entry.parity_block_count,
6707 encrypted_size: entry.encrypted_size,
6708 },
6709 data_kind: BlockKind::DirectoryHintData,
6710 parity_kind: BlockKind::DirectoryHintParity,
6711 key: &subkeys.dir_hint_key,
6712 nonce_seed: &subkeys.index_nonce_seed,
6713 domain: b"dirhint",
6714 counter: entry.hint_shard_index,
6715 class_data_shard_max: crypto_header.index_fec_data_shards,
6716 class_parity_shard_max: crypto_header.index_fec_parity_shards,
6717 }
6718 }
6719
6720 fn dictionary(
6721 volume_header: &'a VolumeHeader,
6722 crypto_header: &'a CryptoHeaderFixed,
6723 subkeys: &'a Subkeys,
6724 index_root: &IndexRoot,
6725 ) -> Result<Self, FormatError> {
6726 Ok(Self {
6727 volume_header,
6728 crypto_header,
6729 extent: dictionary_extent_from_index_root(index_root)?,
6730 data_kind: BlockKind::DictionaryData,
6731 parity_kind: BlockKind::DictionaryParity,
6732 key: &subkeys.dictionary_key,
6733 nonce_seed: &subkeys.index_nonce_seed,
6734 domain: b"dict",
6735 counter: 0,
6736 class_data_shard_max: crypto_header.index_root_fec_data_shards,
6737 class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
6738 })
6739 }
6740
6741 fn payload(
6742 volume_header: &'a VolumeHeader,
6743 crypto_header: &'a CryptoHeaderFixed,
6744 subkeys: &'a Subkeys,
6745 envelope: &EnvelopeEntry,
6746 ) -> Self {
6747 Self {
6748 volume_header,
6749 crypto_header,
6750 extent: ObjectExtent {
6751 first_block_index: envelope.first_block_index,
6752 data_block_count: envelope.data_block_count,
6753 parity_block_count: envelope.parity_block_count,
6754 encrypted_size: envelope.encrypted_size,
6755 },
6756 data_kind: BlockKind::PayloadData,
6757 parity_kind: BlockKind::PayloadParity,
6758 key: &subkeys.enc_key,
6759 nonce_seed: &subkeys.nonce_seed,
6760 domain: b"envelope",
6761 counter: envelope.envelope_index,
6762 class_data_shard_max: crypto_header.fec_data_shards,
6763 class_parity_shard_max: crypto_header.fec_parity_shards,
6764 }
6765 }
6766}
6767
6768fn dictionary_extent_from_index_root(index_root: &IndexRoot) -> Result<ObjectExtent, FormatError> {
6769 if index_root.header.dictionary_data_block_count == 0
6770 || index_root.header.dictionary_encrypted_size == 0
6771 || index_root.header.dictionary_decompressed_size == 0
6772 {
6773 return Err(FormatError::InvalidArchive("dictionary bootstrap required"));
6774 }
6775 Ok(ObjectExtent {
6776 first_block_index: index_root.header.dictionary_first_block,
6777 data_block_count: index_root.header.dictionary_data_block_count,
6778 parity_block_count: index_root.header.dictionary_parity_block_count,
6779 encrypted_size: index_root.header.dictionary_encrypted_size,
6780 })
6781}
6782
6783fn load_metadata_object_from_parts(
6784 blocks: &impl BlockProvider,
6785 context: ObjectLoadContext<'_>,
6786 decompressed_size: u32,
6787) -> Result<Vec<u8>, FormatError> {
6788 let compressed = load_decrypted_object_from_parts(blocks, context)?;
6789 decompress_exact_zstd_frame(&compressed, decompressed_size as usize)
6790}
6791
6792fn load_decrypted_object_from_parts(
6793 blocks: &impl BlockProvider,
6794 context: ObjectLoadContext<'_>,
6795) -> Result<Vec<u8>, FormatError> {
6796 load_decrypted_object_from_parts_with_parity_policy(blocks, context, ParityReadPolicy::Always)
6797}
6798
6799fn load_decrypted_object_from_parts_with_parity_policy(
6800 blocks: &impl BlockProvider,
6801 context: ObjectLoadContext<'_>,
6802 parity_policy: ParityReadPolicy,
6803) -> Result<Vec<u8>, FormatError> {
6804 let repaired = load_repaired_object_data_shards_from_parts_with_parity_policy(
6805 blocks,
6806 context.crypto_header,
6807 context.extent,
6808 context.data_kind,
6809 context.parity_kind,
6810 context.class_data_shard_max,
6811 context.class_parity_shard_max,
6812 parity_policy,
6813 )?;
6814 let mut encrypted = Vec::with_capacity(context.extent.encrypted_size as usize);
6815 for shard in repaired {
6816 encrypted.extend_from_slice(&shard);
6817 }
6818 if encrypted.len() != context.extent.encrypted_size as usize {
6819 return Err(FormatError::InvalidArchive(
6820 "object encrypted size does not match repaired shards",
6821 ));
6822 }
6823
6824 decrypt_padded_aead_object(
6825 AeadObjectContext {
6826 algo: context.crypto_header.aead_algo,
6827 key: context.key,
6828 nonce_seed: context.nonce_seed,
6829 domain: context.domain,
6830 archive_uuid: &context.volume_header.archive_uuid,
6831 session_id: &context.volume_header.session_id,
6832 counter: context.counter,
6833 },
6834 &encrypted,
6835 )
6836}
6837
6838fn load_repaired_object_data_shards_from_parts(
6839 blocks: &impl BlockProvider,
6840 crypto_header: &CryptoHeaderFixed,
6841 extent: ObjectExtent,
6842 data_kind: BlockKind,
6843 parity_kind: BlockKind,
6844 class_data_shard_max: u16,
6845 class_parity_shard_max: u16,
6846) -> Result<Vec<Vec<u8>>, FormatError> {
6847 load_repaired_object_data_shards_from_parts_with_parity_policy(
6848 blocks,
6849 crypto_header,
6850 extent,
6851 data_kind,
6852 parity_kind,
6853 class_data_shard_max,
6854 class_parity_shard_max,
6855 ParityReadPolicy::Always,
6856 )
6857}
6858
6859#[allow(clippy::too_many_arguments)]
6860fn load_repaired_object_data_shards_from_parts_with_parity_policy(
6861 blocks: &impl BlockProvider,
6862 crypto_header: &CryptoHeaderFixed,
6863 extent: ObjectExtent,
6864 data_kind: BlockKind,
6865 parity_kind: BlockKind,
6866 class_data_shard_max: u16,
6867 class_parity_shard_max: u16,
6868 parity_policy: ParityReadPolicy,
6869) -> Result<Vec<Vec<u8>>, FormatError> {
6870 validate_object_extent(
6871 extent,
6872 crypto_header,
6873 class_data_shard_max,
6874 class_parity_shard_max,
6875 )?;
6876 let block_size = crypto_header.block_size as usize;
6877 let data_count = extent.data_block_count as usize;
6878 let parity_count = extent.parity_block_count as usize;
6879 let mut data_shards = Vec::with_capacity(data_count);
6880 let mut parity_shards = Vec::with_capacity(parity_count);
6881
6882 for offset in 0..data_count {
6883 let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
6884 if let Some(record) = blocks.block(block_index)? {
6885 if record.kind != data_kind {
6886 return Err(FormatError::InvalidArchive(
6887 "object data block has unexpected kind",
6888 ));
6889 }
6890 let should_be_last = offset + 1 == data_count;
6891 if record.is_last_data() != should_be_last {
6892 return Err(FormatError::InvalidArchive(
6893 "object last-data flag is not on the final data block",
6894 ));
6895 }
6896 data_shards.push(Some(record.payload.clone()));
6897 } else {
6898 data_shards.push(None);
6899 }
6900 }
6901
6902 if parity_policy == ParityReadPolicy::RepairOnly && data_shards.iter().all(Option::is_some) {
6903 return repair_data_gf16(&data_shards, &[], block_size);
6904 }
6905
6906 for offset in 0..parity_count {
6907 let block_index = checked_u64_add(
6908 extent.first_block_index,
6909 data_count as u64 + offset as u64,
6910 "object",
6911 )?;
6912 if let Some(record) = blocks.block(block_index)? {
6913 if record.kind != parity_kind {
6914 return Err(FormatError::InvalidArchive(
6915 "object parity block has unexpected kind",
6916 ));
6917 }
6918 if record.is_last_data() {
6919 return Err(FormatError::InvalidArchive(
6920 "object parity block has last-data flag",
6921 ));
6922 }
6923 parity_shards.push(Some(record.payload.clone()));
6924 } else {
6925 parity_shards.push(None);
6926 }
6927 }
6928
6929 repair_data_gf16(&data_shards, &parity_shards, block_size)
6930}
6931
6932fn validate_object_extent(
6933 extent: ObjectExtent,
6934 crypto_header: &CryptoHeaderFixed,
6935 class_data_shard_max: u16,
6936 class_parity_shard_max: u16,
6937) -> Result<(), FormatError> {
6938 if extent.data_block_count == 0 || extent.encrypted_size == 0 {
6939 return Err(FormatError::InvalidArchive(
6940 "encrypted object has zero data blocks or size",
6941 ));
6942 }
6943 if extent.data_block_count > class_data_shard_max as u32 {
6944 return Err(FormatError::InvalidArchive(
6945 "encrypted object exceeds its class data-shard maximum",
6946 ));
6947 }
6948 if extent.parity_block_count > class_parity_shard_max as u32 {
6949 return Err(FormatError::InvalidArchive(
6950 "encrypted object exceeds its class parity-shard maximum",
6951 ));
6952 }
6953 let required_parity = required_object_parity(extent.data_block_count as u64, crypto_header)?;
6954 if extent.parity_block_count != required_parity {
6955 return Err(FormatError::InvalidArchive(
6956 "encrypted object parity does not match v41 compute_parity",
6957 ));
6958 }
6959 let total = checked_u64_add(
6960 extent.data_block_count as u64,
6961 extent.parity_block_count as u64,
6962 "encrypted object shard count overflow",
6963 )?;
6964 if total > 65_535 {
6965 return Err(FormatError::FecTooManyShards(total as usize));
6966 }
6967 let expected = checked_u64_mul(
6968 extent.data_block_count as u64,
6969 crypto_header.block_size as u64,
6970 "encrypted object size overflow",
6971 )?;
6972 if expected != extent.encrypted_size as u64 {
6973 return Err(FormatError::InvalidArchive(
6974 "encrypted object size is not data_block_count * block_size",
6975 ));
6976 }
6977 if extent.encrypted_size as usize <= crypto_header.aead_algo.tag_len() {
6978 return Err(FormatError::InvalidArchive(
6979 "encrypted object is too small for AEAD tag",
6980 ));
6981 }
6982 Ok(())
6983}
6984
6985pub(crate) fn required_object_parity(
6986 data_block_count: u64,
6987 crypto_header: &CryptoHeaderFixed,
6988) -> Result<u32, FormatError> {
6989 let min_parity =
6990 if crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0 {
6991 1
6992 } else {
6993 0
6994 };
6995 let mut parity = 0u64;
6996 for _ in 0..100 {
6997 let total = data_block_count
6998 .checked_add(parity)
6999 .ok_or(FormatError::InvalidArchive("parity total overflow"))?;
7000 let by_volume = checked_u64_mul(
7001 crypto_header.volume_loss_tolerance as u64,
7002 ceil_div_u64(total, crypto_header.stripe_width as u64)?,
7003 "volume-loss parity overflow",
7004 )?;
7005 let by_bitrot = ceil_div_u64(
7006 checked_u64_mul(
7007 total,
7008 crypto_header.bit_rot_buffer_pct as u64,
7009 "bit-rot parity overflow",
7010 )?,
7011 100,
7012 )?;
7013 let next = by_volume
7014 .checked_add(by_bitrot)
7015 .ok_or(FormatError::InvalidArchive("parity overflow"))?
7016 .max(min_parity);
7017 if next == parity {
7018 return u32::try_from(next)
7019 .map_err(|_| FormatError::InvalidArchive("parity count overflow"));
7020 }
7021 parity = next;
7022 }
7023 Err(FormatError::InvalidArchive(
7024 "parity calculation did not converge",
7025 ))
7026}
7027
7028fn ceil_div_u64(numerator: u64, denominator: u64) -> Result<u64, FormatError> {
7029 if denominator == 0 {
7030 return Err(FormatError::InvalidArchive("division by zero"));
7031 }
7032 numerator
7033 .checked_add(denominator - 1)
7034 .ok_or(FormatError::InvalidArchive("ceiling division overflow"))
7035 .map(|value| value / denominator)
7036}
7037
7038fn frame_range_for_file<'b>(
7039 shard: &'b IndexShard,
7040 file: &FileEntry,
7041) -> Result<Vec<&'b FrameEntry>, FormatError> {
7042 let mut frames = Vec::with_capacity(file.frame_count as usize);
7043 for offset in 0..file.frame_count as u64 {
7044 let frame_index =
7045 file.first_frame_index
7046 .checked_add(offset)
7047 .ok_or(FormatError::InvalidArchive(
7048 "FileEntry frame range overflow",
7049 ))?;
7050 let frame = shard
7051 .frames
7052 .iter()
7053 .find(|entry| entry.frame_index == frame_index)
7054 .ok_or(FormatError::InvalidArchive(
7055 "FileEntry references missing FrameEntry",
7056 ))?;
7057 frames.push(frame);
7058 }
7059 Ok(frames)
7060}
7061
7062fn metadata_limits(crypto_header: &CryptoHeaderFixed) -> MetadataLimits {
7063 MetadataLimits {
7064 block_size: crypto_header.block_size,
7065 max_path_length: crypto_header.max_path_length,
7066 max_payload_data_shards: crypto_header.fec_data_shards,
7067 max_payload_parity_shards: crypto_header.fec_parity_shards,
7068 max_index_data_shards: crypto_header.index_fec_data_shards,
7069 max_index_parity_shards: crypto_header.index_fec_parity_shards,
7070 max_index_root_data_shards: crypto_header.index_root_fec_data_shards,
7071 max_index_root_parity_shards: crypto_header.index_root_fec_parity_shards,
7072 ..MetadataLimits::default()
7073 }
7074}
7075
7076fn verify_dense_keys<T>(
7077 entries: &BTreeMap<u64, T>,
7078 expected_count: u64,
7079 structure: &'static str,
7080) -> Result<(), FormatError> {
7081 if entries.len() as u64 != expected_count {
7082 return Err(FormatError::InvalidArchive(
7083 "decoded table count does not match IndexRoot",
7084 ));
7085 }
7086 for expected in 0..expected_count {
7087 if !entries.contains_key(&expected) {
7088 return Err(FormatError::InvalidMetadata {
7089 structure,
7090 reason: "global index coverage has a gap",
7091 });
7092 }
7093 }
7094 Ok(())
7095}
7096
7097fn validate_envelope_frame_coverage(
7098 frames: &BTreeMap<u64, FrameEntry>,
7099 envelopes: &BTreeMap<u64, EnvelopeEntry>,
7100) -> Result<(), FormatError> {
7101 let mut accounted_frames = BTreeSet::new();
7102 for envelope in envelopes.values() {
7103 let first = envelope.first_frame_index;
7104 let end =
7105 first
7106 .checked_add(envelope.frame_count as u64)
7107 .ok_or(FormatError::InvalidArchive(
7108 "EnvelopeEntry frame range overflow",
7109 ))?;
7110 let mut ranges = Vec::with_capacity(envelope.frame_count as usize);
7111 for frame_index in first..end {
7112 let frame = frames.get(&frame_index).ok_or(FormatError::InvalidArchive(
7113 "EnvelopeEntry references missing FrameEntry",
7114 ))?;
7115 if frame.envelope_index != envelope.envelope_index {
7116 return Err(FormatError::InvalidArchive(
7117 "FrameEntry envelope_index does not match containing EnvelopeEntry",
7118 ));
7119 }
7120 if !accounted_frames.insert(frame_index) {
7121 return Err(FormatError::InvalidArchive(
7122 "FrameEntry is covered by multiple EnvelopeEntries",
7123 ));
7124 }
7125 let start = frame.offset_in_envelope as usize;
7126 let end = checked_add(start, frame.compressed_size as usize, "FrameEntry")?;
7127 if end > envelope.plaintext_size as usize {
7128 return Err(FormatError::InvalidArchive(
7129 "FrameEntry exceeds EnvelopeEntry plaintext_size",
7130 ));
7131 }
7132 ranges.push((start, end));
7133 }
7134 validate_exact_coverage_ranges(
7135 &mut ranges,
7136 envelope.plaintext_size as usize,
7137 "EnvelopeEntry frame coverage has a gap or overlap",
7138 )?;
7139 }
7140
7141 for frame_index in frames.keys() {
7142 if !accounted_frames.contains(frame_index) {
7143 return Err(FormatError::InvalidArchive(
7144 "FrameEntry is not covered by any EnvelopeEntry",
7145 ));
7146 }
7147 }
7148 Ok(())
7149}
7150
7151fn validate_global_file_table_order(shards: &[IndexShard]) -> Result<(), FormatError> {
7152 let mut previous = None::<([u8; 8], Vec<u8>, u64)>;
7153 for shard in shards {
7154 for (idx, file) in shard.files.iter().enumerate() {
7155 let path = shard
7156 .file_path(idx)
7157 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?
7158 .to_vec();
7159 let start = shard
7160 .tar_member_group_start(idx)
7161 .ok_or(FormatError::InvalidArchive(
7162 "FileEntry tar member start is missing",
7163 ))?;
7164 let key = (file.path_hash, path, start);
7165 validate_global_file_table_key_step(previous.as_ref(), &key)?;
7166 previous = Some(key);
7167 }
7168 }
7169 Ok(())
7170}
7171
7172fn validate_global_file_table_key_step(
7173 previous: Option<&([u8; 8], Vec<u8>, u64)>,
7174 current: &([u8; 8], Vec<u8>, u64),
7175) -> Result<(), FormatError> {
7176 if let Some(previous) = previous {
7177 if previous >= current {
7178 return Err(FormatError::InvalidArchive(
7179 "global FileEntry rows are not sorted and unique",
7180 ));
7181 }
7182 }
7183 Ok(())
7184}
7185
7186fn validate_file_extent_coverage_ranges(
7187 extents: &[(u64, u64)],
7188 tar_len: u64,
7189) -> Result<(), FormatError> {
7190 let mut ranges = Vec::with_capacity(extents.len());
7191 for (start, len) in extents {
7192 let end = checked_u64_add(*start, *len, "FileEntry")?;
7193 if end > tar_len {
7194 return Err(FormatError::InvalidArchive(
7195 "FileEntry extent exceeds IndexRoot tar_total_size",
7196 ));
7197 }
7198 ranges.push((*start, end));
7199 }
7200 validate_exact_coverage_ranges_u64(
7201 &mut ranges,
7202 tar_len,
7203 "FileEntry extents do not cover tar stream exactly",
7204 )
7205}
7206
7207fn add_expected_directory_hint_rows(
7208 map: &mut DirectoryHintMap,
7209 shard_row_index: u32,
7210 path: &[u8],
7211 kind: TarEntryKind,
7212) {
7213 map.entry(Vec::new()).or_default().insert(shard_row_index);
7214 for (idx, byte) in path.iter().enumerate() {
7215 if *byte == b'/' {
7216 map.entry(path[..idx].to_vec())
7217 .or_default()
7218 .insert(shard_row_index);
7219 }
7220 }
7221 if kind == TarEntryKind::Directory {
7222 map.entry(path.to_vec())
7223 .or_default()
7224 .insert(shard_row_index);
7225 }
7226}
7227
7228fn validate_directory_hint_tables_against_expected(
7229 tables: &[DirectoryHintTable],
7230 expected: &DirectoryHintMap,
7231) -> Result<(), FormatError> {
7232 let mut actual = Vec::new();
7233 let mut previous_key: Option<([u8; 8], Vec<u8>)> = None;
7234
7235 for table in tables {
7236 for entry_index in 0..table.entries.len() {
7237 let path = table
7238 .entry_path(entry_index)
7239 .ok_or(FormatError::InvalidArchive(
7240 "DirectoryHintEntry path is missing",
7241 ))?;
7242 let key = (hash_prefix(path), path.to_vec());
7243 if let Some(previous) = &previous_key {
7244 if previous >= &key {
7245 return Err(FormatError::InvalidArchive(
7246 "DirectoryHintEntry rows are not globally sorted",
7247 ));
7248 }
7249 }
7250 previous_key = Some(key);
7251
7252 let rows =
7253 table
7254 .shard_rows_for_entry(entry_index)
7255 .ok_or(FormatError::InvalidArchive(
7256 "DirectoryHintEntry shard rows are missing",
7257 ))?;
7258 actual.push((path.to_vec(), rows.to_vec()));
7259 }
7260 }
7261
7262 if actual != sorted_directory_hint_rows(expected) {
7263 return Err(FormatError::InvalidArchive(
7264 "directory hint map does not match decoded files",
7265 ));
7266 }
7267 Ok(())
7268}
7269
7270fn sorted_directory_hint_rows(map: &DirectoryHintMap) -> Vec<(Vec<u8>, Vec<u32>)> {
7271 let mut rows = map
7272 .iter()
7273 .map(|(path, shard_rows)| {
7274 (
7275 path.clone(),
7276 shard_rows.iter().copied().collect::<Vec<u32>>(),
7277 )
7278 })
7279 .collect::<Vec<_>>();
7280 rows.sort_by(|(left_path, _), (right_path, _)| {
7281 hash_prefix(left_path)
7282 .cmp(&hash_prefix(right_path))
7283 .then_with(|| left_path.cmp(right_path))
7284 });
7285 rows
7286}
7287
7288fn validate_exact_coverage_ranges(
7289 ranges: &mut [(usize, usize)],
7290 expected_end: usize,
7291 reason: &'static str,
7292) -> Result<(), FormatError> {
7293 ranges.sort_unstable();
7294 let mut cursor = 0usize;
7295 for (start, end) in ranges.iter().copied() {
7296 if start != cursor || end < start {
7297 return Err(FormatError::InvalidArchive(reason));
7298 }
7299 cursor = end;
7300 }
7301 if cursor != expected_end {
7302 return Err(FormatError::InvalidArchive(reason));
7303 }
7304 Ok(())
7305}
7306
7307fn validate_exact_coverage_ranges_u64(
7308 ranges: &mut [(u64, u64)],
7309 expected_end: u64,
7310 reason: &'static str,
7311) -> Result<(), FormatError> {
7312 ranges.sort_unstable();
7313 let mut cursor = 0u64;
7314 for (start, end) in ranges.iter().copied() {
7315 if start != cursor || end < start {
7316 return Err(FormatError::InvalidArchive(reason));
7317 }
7318 cursor = end;
7319 }
7320 if cursor != expected_end {
7321 return Err(FormatError::InvalidArchive(reason));
7322 }
7323 Ok(())
7324}
7325
7326fn object_block_range(
7327 first_block_index: u64,
7328 data_block_count: u32,
7329 parity_block_count: u32,
7330 structure: &'static str,
7331) -> Result<(u64, u64), FormatError> {
7332 let total = data_block_count as u64 + parity_block_count as u64;
7333 if total == 0 {
7334 return Err(FormatError::InvalidArchive(structure));
7335 }
7336 let end = checked_u64_add(first_block_index, total, structure)?;
7337 Ok((first_block_index, end))
7338}
7339
7340fn validate_non_overlapping_object_ranges(ranges: &mut [(u64, u64)]) -> Result<(), FormatError> {
7341 ranges.sort_unstable();
7342 for pair in ranges.windows(2) {
7343 if pair[0].1 > pair[1].0 {
7344 return Err(FormatError::InvalidArchive(
7345 "encrypted object block ranges overlap",
7346 ));
7347 }
7348 }
7349 Ok(())
7350}
7351
7352pub(crate) fn observed_archive_size(
7353 sizes: impl IntoIterator<Item = u64>,
7354) -> Result<u64, FormatError> {
7355 sizes.into_iter().try_fold(0u64, |sum, size| {
7356 sum.checked_add(size).ok_or(FormatError::InvalidArchive(
7357 "observed archive size overflow",
7358 ))
7359 })
7360}
7361
7362pub(crate) fn total_extraction_size_cap(
7363 options: ReaderOptions,
7364 observed_archive_bytes: u64,
7365) -> u64 {
7366 options
7367 .max_total_extraction_size
7368 .min(observed_archive_bytes.saturating_mul(10))
7369}
7370
7371fn utf8_path(bytes: &[u8]) -> Result<String, FormatError> {
7372 std::str::from_utf8(bytes)
7373 .map(|path| path.to_owned())
7374 .map_err(|_| FormatError::UnsafeArchivePath)
7375}
7376
7377fn manifest_footer_global_pre_hmac_bytes(manifest_footer: &ManifestFooter) -> [u8; 104] {
7378 let mut bytes = [0u8; 104];
7379 bytes.copy_from_slice(&manifest_footer.to_bytes()[..104]);
7380 bytes[36..40].fill(0);
7381 bytes
7382}
7383
7384fn sha256_bytes(bytes: &[u8]) -> [u8; 32] {
7385 let digest = Sha256::digest(bytes);
7386 let mut out = [0u8; 32];
7387 out.copy_from_slice(&digest);
7388 out
7389}
7390
7391fn slice<'b>(
7392 bytes: &'b [u8],
7393 offset: usize,
7394 len: usize,
7395 structure: &'static str,
7396) -> Result<&'b [u8], FormatError> {
7397 let end = checked_add(offset, len, structure)?;
7398 bytes.get(offset..end).ok_or(FormatError::InvalidLength {
7399 structure,
7400 expected: end,
7401 actual: bytes.len(),
7402 })
7403}
7404
7405fn read_at_vec(
7406 reader: &dyn ArchiveReadAt,
7407 offset: u64,
7408 len: usize,
7409 structure: &'static str,
7410) -> Result<Vec<u8>, FormatError> {
7411 let expected_end = offset
7412 .checked_add(len as u64)
7413 .ok_or(FormatError::InvalidArchive("archive read range overflow"))?;
7414 let observed_len = reader.len()?;
7415 if expected_end > observed_len {
7416 return Err(FormatError::InvalidLength {
7417 structure,
7418 expected: to_usize(expected_end, structure)?,
7419 actual: to_usize(observed_len, structure)?,
7420 });
7421 }
7422 let mut out = vec![0u8; len];
7423 reader.read_exact_at(offset, &mut out)?;
7424 Ok(out)
7425}
7426
7427fn parallel_map_ref<T, U, F>(items: &[T], jobs: usize, f: F) -> Result<Vec<U>, FormatError>
7428where
7429 T: Sync,
7430 U: Send,
7431 F: Fn(&T) -> Result<U, FormatError> + Sync,
7432{
7433 if jobs <= 1 || items.len() <= 1 {
7434 return items.iter().map(f).collect();
7435 }
7436 let worker_count = jobs.min(items.len());
7437 let chunk_size = items.len().div_ceil(worker_count);
7438 let mut out = Vec::with_capacity(items.len());
7439 thread::scope(|scope| {
7440 let handles = items
7441 .chunks(chunk_size)
7442 .map(|chunk| scope.spawn(|| chunk.iter().map(&f).collect::<Result<Vec<_>, _>>()))
7443 .collect::<Vec<_>>();
7444 for handle in handles {
7445 let mut chunk = handle
7446 .join()
7447 .map_err(|_| FormatError::InvalidArchive("reader worker panicked"))??;
7448 out.append(&mut chunk);
7449 }
7450 Ok(out)
7451 })
7452}
7453
7454fn checked_add(lhs: usize, rhs: usize, structure: &'static str) -> Result<usize, FormatError> {
7455 lhs.checked_add(rhs)
7456 .ok_or(FormatError::InvalidArchive(structure))
7457}
7458
7459fn checked_u64_add(lhs: u64, rhs: u64, structure: &'static str) -> Result<u64, FormatError> {
7460 lhs.checked_add(rhs)
7461 .ok_or(FormatError::InvalidArchive(structure))
7462}
7463
7464fn to_usize(value: u64, structure: &'static str) -> Result<usize, FormatError> {
7465 usize::try_from(value).map_err(|_| FormatError::InvalidArchive(structure))
7466}
7467
7468#[cfg(test)]
7469mod tests {
7470 use std::fs;
7471
7472 use super::*;
7473 use crate::compression::compress_zstd_frame;
7474 use crate::crypto::{compute_hmac, encrypt_padded_aead_object};
7475 use crate::fec::encode_parity_gf16;
7476 use crate::format::{
7477 AeadAlgo, CompressionAlgo, FecAlgo, KdfAlgo, CRYPTO_HEADER_FIXED_LEN, FORMAT_VERSION,
7478 VOLUME_FORMAT_REV,
7479 };
7480 use crate::metadata::{
7481 DirectoryHintEntry, DirectoryHintTableHeader, IndexRootHeader, IndexShardHeader,
7482 ENVELOPE_ENTRY_LEN, FILE_ENTRY_LEN, FRAME_ENTRY_LEN, INDEX_SHARD_HEADER_LEN,
7483 };
7484 use crate::non_seekable_reader::{
7485 extract_non_seekable_stream_to_dir, list_non_seekable_stream, verify_non_seekable_stream,
7486 verify_non_seekable_stream_with_bootstrap_sidecar, verify_non_seekable_stream_with_options,
7487 NonSeekableReaderOptions, SequentialRootAuthStatus,
7488 };
7489 use crate::writer::{
7490 write_archive, write_archive_with_dictionary, write_archive_with_kdf,
7491 write_archive_with_root_auth, RegularFile, RootAuthSigningRequest, RootAuthWriterConfig,
7492 WriterOptions,
7493 };
7494
7495 fn master_key() -> MasterKey {
7496 MasterKey::from_raw_key(&[0x42; 32]).unwrap()
7497 }
7498
7499 #[test]
7500 fn reader_defaults_use_available_parallelism_jobs() {
7501 let options = ReaderOptions::default();
7502
7503 assert_eq!(options.jobs, default_jobs());
7504 assert!(options.jobs >= 1);
7505 }
7506
7507 #[test]
7508 fn reader_options_reject_zero_jobs() {
7509 let err = OpenedArchive::open_with_options(
7510 &[],
7511 &master_key(),
7512 ReaderOptions {
7513 jobs: 0,
7514 ..ReaderOptions::default()
7515 },
7516 )
7517 .unwrap_err();
7518
7519 assert_eq!(
7520 err,
7521 FormatError::ReaderUnsupported("jobs must be at least 1")
7522 );
7523 }
7524
7525 const TEST_ROOT_AUTH_ID: u16 = 0xe001;
7526 const TEST_ROOT_AUTH_VALUE_LEN: u32 = 32;
7527
7528 fn test_root_auth_config() -> RootAuthWriterConfig<'static> {
7529 RootAuthWriterConfig {
7530 authenticator_id: TEST_ROOT_AUTH_ID,
7531 signer_identity_type: 0,
7532 signer_identity: &[],
7533 authenticator_value_length: TEST_ROOT_AUTH_VALUE_LEN,
7534 }
7535 }
7536
7537 fn test_root_auth_value(request: &RootAuthSigningRequest) -> Vec<u8> {
7538 request.archive_root.to_vec()
7539 }
7540
7541 fn test_root_auth_verifies(footer: &RootAuthFooterV1, archive_root: &[u8; 32]) -> bool {
7542 footer.authenticator_id == TEST_ROOT_AUTH_ID
7543 && footer.signer_identity_type == 0
7544 && footer.signer_identity_bytes.is_empty()
7545 && footer.authenticator_value.as_slice() == archive_root
7546 }
7547
7548 fn dictionary() -> &'static [u8] {
7549 b"dir/dict.txt common words common words common words dictionary payload"
7550 }
7551
7552 #[derive(Clone)]
7553 struct CountingReadAt {
7554 bytes: std::sync::Arc<Vec<u8>>,
7555 reads: std::sync::Arc<std::sync::Mutex<Vec<(u64, u64)>>>,
7556 denied_ranges: std::sync::Arc<Vec<(u64, u64)>>,
7557 }
7558
7559 impl CountingReadAt {
7560 fn new(bytes: Vec<u8>, denied_ranges: Vec<(u64, u64)>) -> Self {
7561 Self {
7562 bytes: std::sync::Arc::new(bytes),
7563 reads: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
7564 denied_ranges: std::sync::Arc::new(denied_ranges),
7565 }
7566 }
7567
7568 fn reads(&self) -> Vec<(u64, u64)> {
7569 self.reads.lock().unwrap().clone()
7570 }
7571 }
7572
7573 impl ArchiveReadAt for CountingReadAt {
7574 fn len(&self) -> Result<u64, FormatError> {
7575 u64::try_from(self.bytes.as_ref().len())
7576 .map_err(|_| FormatError::InvalidArchive("archive length overflow"))
7577 }
7578
7579 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
7580 let end = checked_u64_add(offset, buf.len() as u64, "archive read range overflow")?;
7581 self.reads.lock().unwrap().push((offset, end));
7582 if self
7583 .denied_ranges
7584 .iter()
7585 .any(|(start, limit)| ranges_overlap(offset, end, *start, *limit))
7586 {
7587 return Err(FormatError::InvalidArchive("denied test read"));
7588 }
7589 let start = to_usize(offset, "archive")?;
7590 let end_usize = checked_add(start, buf.len(), "archive")?;
7591 let source = self
7592 .bytes
7593 .get(start..end_usize)
7594 .ok_or(FormatError::InvalidLength {
7595 structure: "archive",
7596 expected: end_usize,
7597 actual: self.bytes.as_ref().len(),
7598 })?;
7599 buf.copy_from_slice(source);
7600 Ok(())
7601 }
7602 }
7603
7604 fn ranges_overlap(left_start: u64, left_end: u64, right_start: u64, right_end: u64) -> bool {
7605 left_start < right_end && right_start < left_end
7606 }
7607
7608 fn single_stream_options() -> WriterOptions {
7609 WriterOptions {
7610 stripe_width: 1,
7611 volume_loss_tolerance: 0,
7612 ..WriterOptions::default()
7613 }
7614 }
7615
7616 struct ChunkedReader {
7617 bytes: Vec<u8>,
7618 cursor: usize,
7619 max_chunk: usize,
7620 }
7621
7622 impl ChunkedReader {
7623 fn new(bytes: Vec<u8>, max_chunk: usize) -> Self {
7624 Self {
7625 bytes,
7626 cursor: 0,
7627 max_chunk,
7628 }
7629 }
7630 }
7631
7632 impl std::io::Read for ChunkedReader {
7633 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
7634 if self.cursor >= self.bytes.len() {
7635 return Ok(0);
7636 }
7637 let available = self.bytes.len() - self.cursor;
7638 let len = available.min(buf.len()).min(self.max_chunk);
7639 buf[..len].copy_from_slice(&self.bytes[self.cursor..self.cursor + len]);
7640 self.cursor += len;
7641 Ok(len)
7642 }
7643 }
7644
7645 #[test]
7646 fn global_file_table_key_step_rejects_distinct_path_regression() {
7647 let previous = ([1u8; 8], b"b.txt".to_vec(), 0);
7648 let current = ([1u8; 8], b"a.txt".to_vec(), 0);
7649
7650 assert_eq!(
7651 validate_global_file_table_key_step(Some(&previous), ¤t).unwrap_err(),
7652 FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
7653 );
7654 }
7655
7656 #[test]
7657 fn global_file_table_key_step_rejects_duplicate_full_key() {
7658 let previous = ([1u8; 8], b"a.txt".to_vec(), 7);
7659 let current = ([1u8; 8], b"a.txt".to_vec(), 7);
7660
7661 assert_eq!(
7662 validate_global_file_table_key_step(Some(&previous), ¤t).unwrap_err(),
7663 FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
7664 );
7665 }
7666
7667 fn small_block_recovery_options() -> WriterOptions {
7668 WriterOptions {
7669 block_size: 4096,
7670 chunk_size: 32 * 1024,
7671 envelope_target_size: 32 * 1024,
7672 stripe_width: 1,
7673 volume_loss_tolerance: 0,
7674 bit_rot_buffer_pct: 1,
7675 fec_data_shards: 16,
7676 fec_parity_shards: 1,
7677 index_fec_data_shards: 4,
7678 index_fec_parity_shards: 1,
7679 index_root_fec_data_shards: 16,
7680 index_root_fec_parity_shards: 1,
7681 ..WriterOptions::default()
7682 }
7683 }
7684
7685 fn parity_rich_recovery_options() -> WriterOptions {
7686 WriterOptions {
7687 block_size: 4096,
7688 chunk_size: 32 * 1024,
7689 envelope_target_size: 32 * 1024,
7690 stripe_width: 1,
7691 volume_loss_tolerance: 0,
7692 bit_rot_buffer_pct: 40,
7693 fec_data_shards: 16,
7694 fec_parity_shards: 16,
7695 index_fec_data_shards: 4,
7696 index_fec_parity_shards: 4,
7697 index_root_fec_data_shards: 16,
7698 index_root_fec_parity_shards: 16,
7699 ..WriterOptions::default()
7700 }
7701 }
7702
7703 fn pseudo_random_bytes(len: usize) -> Vec<u8> {
7704 let mut state = 0x1234_5678u32;
7705 (0..len)
7706 .map(|_| {
7707 state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
7708 (state >> 24) as u8
7709 })
7710 .collect()
7711 }
7712
7713 #[test]
7714 fn opens_lists_verifies_and_extracts_one_file_archive() {
7715 let archive = write_archive(
7716 &[RegularFile::new("dir/hello.txt", b"hello m7")],
7717 &master_key(),
7718 single_stream_options(),
7719 )
7720 .unwrap();
7721 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
7722
7723 assert_eq!(
7724 opened.list_files().unwrap(),
7725 vec![ArchiveEntry {
7726 path: "dir/hello.txt".to_string(),
7727 file_data_size: 8,
7728 kind: TarEntryKind::Regular,
7729 mode: 0o644,
7730 mtime: 0,
7731 diagnostics: Vec::new(),
7732 }]
7733 );
7734 opened.verify().unwrap();
7735 assert_eq!(
7736 opened.extract_file("dir/hello.txt").unwrap(),
7737 Some(b"hello m7".to_vec())
7738 );
7739 assert_eq!(opened.extract_file("missing.txt").unwrap(), None);
7740 }
7741
7742 #[test]
7743 fn root_auth_archive_round_trips_and_verifies_with_callback() {
7744 let archive = write_archive_with_root_auth(
7745 &[RegularFile::new("signed.txt", b"root-auth payload")],
7746 &master_key(),
7747 single_stream_options(),
7748 RootAuthWriterConfig {
7749 authenticator_id: 0x7777,
7750 signer_identity_type: 1,
7751 signer_identity: b"test signer",
7752 authenticator_value_length: 32,
7753 },
7754 |request| Ok(request.archive_root.to_vec()),
7755 )
7756 .unwrap();
7757
7758 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
7759 opened.verify().unwrap();
7760 let verified = opened
7761 .verify_root_auth_with(|footer, archive_root| {
7762 Ok(footer.authenticator_value == archive_root.as_slice())
7763 })
7764 .unwrap();
7765
7766 assert_eq!(verified.authenticator_id, 0x7777);
7767 assert_eq!(verified.signer_identity_type, 1);
7768 assert_eq!(verified.signer_identity_bytes, b"test signer");
7769 assert_eq!(
7770 verified.archive_root,
7771 opened.root_auth_footer.as_ref().unwrap().archive_root
7772 );
7773 assert_eq!(
7774 verified.diagnostics,
7775 vec![
7776 RootAuthDiagnostic::RootAuthContentVerified,
7777 RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
7778 RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
7779 RootAuthDiagnostic::RecoveryMarginUnchecked,
7780 ]
7781 );
7782 }
7783
7784 #[test]
7785 fn root_auth_verification_requires_authenticator_success() {
7786 let archive = write_archive_with_root_auth(
7787 &[RegularFile::new("signed.txt", b"root-auth payload")],
7788 &master_key(),
7789 single_stream_options(),
7790 RootAuthWriterConfig {
7791 authenticator_id: 9,
7792 signer_identity_type: 1,
7793 signer_identity: b"test signer",
7794 authenticator_value_length: 32,
7795 },
7796 |request| Ok(request.archive_root.to_vec()),
7797 )
7798 .unwrap();
7799 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
7800
7801 assert_eq!(
7802 opened.verify_root_auth_with(|_, _| Ok(false)).unwrap_err(),
7803 FormatError::InvalidArchive("root-auth authenticator verification failed")
7804 );
7805 }
7806
7807 #[test]
7808 fn public_no_key_verifies_encrypted_data_block_commitment_with_callback() {
7809 let archive = write_archive_with_root_auth(
7810 &[RegularFile::new("public.txt", b"public commitment")],
7811 &master_key(),
7812 single_stream_options(),
7813 RootAuthWriterConfig {
7814 authenticator_id: 0x2222,
7815 signer_identity_type: 1,
7816 signer_identity: b"public verifier",
7817 authenticator_value_length: 32,
7818 },
7819 |request| Ok(request.archive_root.to_vec()),
7820 )
7821 .unwrap();
7822
7823 let verified = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
7824 Ok(footer.authenticator_value == archive_root.as_slice())
7825 })
7826 .unwrap();
7827
7828 assert_eq!(verified.authenticator_id, 0x2222);
7829 assert_eq!(verified.signer_identity_bytes, b"public verifier");
7830 assert!(verified.total_data_block_count > 0);
7831 }
7832
7833 #[test]
7834 fn public_no_key_ignores_untrusted_manifest_and_trailer_block_count_fields() {
7835 let archive = write_archive_with_root_auth(
7836 &[RegularFile::new(
7837 "public-fields.txt",
7838 b"public source authority",
7839 )],
7840 &master_key(),
7841 single_stream_options(),
7842 RootAuthWriterConfig {
7843 authenticator_id: 0x2222,
7844 signer_identity_type: 1,
7845 signer_identity: b"public verifier",
7846 authenticator_value_length: 32,
7847 },
7848 |request| Ok(request.archive_root.to_vec()),
7849 )
7850 .unwrap();
7851 let mut bytes = archive.bytes.clone();
7852
7853 rewrite_public_cmra_image(&mut bytes, |image| {
7854 let manifest_region = image
7855 .regions
7856 .iter_mut()
7857 .find(|region| region.region_type == 3)
7858 .unwrap();
7859 manifest_region.bytes[44..48].copy_from_slice(&99u32.to_le_bytes());
7860
7861 let trailer_region = image
7862 .regions
7863 .iter_mut()
7864 .find(|region| region.region_type == 5)
7865 .unwrap();
7866 let mut trailer = VolumeTrailer::parse(&trailer_region.bytes).unwrap();
7867 trailer.block_count += 7;
7868 trailer_region.bytes = trailer.to_bytes().to_vec();
7869 });
7870
7871 public_no_key_verify_archive_with(&bytes, |footer, archive_root| {
7872 Ok(footer.authenticator_value == archive_root.as_slice())
7873 })
7874 .unwrap();
7875 }
7876
7877 #[test]
7878 fn public_no_key_compares_only_public_crypto_profile_across_volumes() {
7879 let archive = write_archive_with_root_auth(
7880 &[RegularFile::new(
7881 "public-crypto.txt",
7882 b"cross-volume public profile",
7883 )],
7884 &master_key(),
7885 WriterOptions {
7886 stripe_width: 2,
7887 volume_loss_tolerance: 0,
7888 ..WriterOptions::default()
7889 },
7890 RootAuthWriterConfig {
7891 authenticator_id: 0x3333,
7892 signer_identity_type: 1,
7893 signer_identity: b"public verifier",
7894 authenticator_value_length: 32,
7895 },
7896 |request| Ok(request.archive_root.to_vec()),
7897 )
7898 .unwrap();
7899 let mut volumes = archive.volumes.clone();
7900 let volume_header = VolumeHeader::parse(&volumes[1][..VOLUME_HEADER_LEN]).unwrap();
7901 let crypto_offset = volume_header.crypto_header_offset as usize;
7902 let expected_volume_size = 123_456_789u64;
7903 volumes[1][crypto_offset + 52..crypto_offset + 60]
7904 .copy_from_slice(&expected_volume_size.to_le_bytes());
7905 rewrite_public_cmra_image(&mut volumes[1], |image| {
7906 let crypto_region = image
7907 .regions
7908 .iter_mut()
7909 .find(|region| region.region_type == 2)
7910 .unwrap();
7911 crypto_region.bytes[52..60].copy_from_slice(&expected_volume_size.to_le_bytes());
7912 });
7913
7914 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
7915 public_no_key_verify_volumes_with(&volume_refs, |footer, archive_root| {
7916 Ok(footer.authenticator_value == archive_root.as_slice())
7917 })
7918 .unwrap();
7919 }
7920
7921 #[test]
7922 fn locator_based_cmra_recovery_only_ignores_header_crc_failures() {
7923 let archive = write_archive_with_root_auth(
7924 &[RegularFile::new("cmra-header.txt", b"header fallback")],
7925 &master_key(),
7926 single_stream_options(),
7927 RootAuthWriterConfig {
7928 authenticator_id: 0x4444,
7929 signer_identity_type: 1,
7930 signer_identity: b"public verifier",
7931 authenticator_value_length: 32,
7932 },
7933 |request| Ok(request.archive_root.to_vec()),
7934 )
7935 .unwrap();
7936 let final_locator = final_recovery_locator(&archive.bytes);
7937
7938 let mut bad_crc = archive.bytes.clone();
7939 let crc_offset =
7940 final_locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN - 1;
7941 bad_crc[crc_offset] ^= 0x55;
7942 public_no_key_verify_archive_with(&bad_crc, |footer, archive_root| {
7943 Ok(footer.authenticator_value == archive_root.as_slice())
7944 })
7945 .unwrap();
7946
7947 let mut bad_magic = archive.bytes.clone();
7948 bad_magic[final_locator.cmra_offset as usize] ^= 0x55;
7949 assert_eq!(
7950 public_no_key_verify_archive_with(&bad_magic, |_, _| Ok(true)).unwrap_err(),
7951 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
7952 );
7953
7954 let mut bad_hint = archive.bytes.clone();
7955 bad_hint[crc_offset] ^= 0xAA;
7956 for offset in [
7957 bad_hint.len() - LOCATOR_PAIR_LEN,
7958 bad_hint.len() - CRITICAL_RECOVERY_LOCATOR_LEN,
7959 ] {
7960 let mut locator = CriticalRecoveryLocator::parse(
7961 &bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN],
7962 )
7963 .unwrap();
7964 locator.volume_index_hint += 1;
7965 bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN]
7966 .copy_from_slice(&locator.to_bytes());
7967 }
7968 assert_eq!(
7969 public_no_key_verify_archive_with(&bad_hint, |_, _| Ok(true)).unwrap_err(),
7970 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
7971 );
7972 }
7973
7974 #[test]
7975 fn key_holding_rejects_cmra_below_authenticated_parity_floor() {
7976 let archive = write_archive(
7977 &[RegularFile::new(
7978 "cmra-floor.txt",
7979 b"authenticated CMRA floor",
7980 )],
7981 &master_key(),
7982 single_stream_options(),
7983 )
7984 .unwrap();
7985 let malformed = rewrite_cmra_parity_count(&archive.bytes, 1);
7986 let final_offset = malformed.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
7987 let locator = CriticalRecoveryLocator::parse(
7988 &malformed[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
7989 )
7990 .unwrap();
7991 let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
7992 let crypto_start = volume_header.crypto_header_offset as usize;
7993 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
7994 let crypto_header = CryptoHeader::parse(
7995 &malformed[crypto_start..crypto_end],
7996 volume_header.crypto_header_length,
7997 )
7998 .unwrap();
7999 let subkeys = Subkeys::derive(
8000 &master_key(),
8001 &volume_header.archive_uuid,
8002 &volume_header.session_id,
8003 )
8004 .unwrap();
8005
8006 assert_eq!(
8007 parse_locator_cmra_candidate(
8008 &malformed,
8009 final_offset,
8010 locator,
8011 KeyHoldingTerminalContext {
8012 subkeys: &subkeys,
8013 volume_header: &volume_header,
8014 crypto_header: &crypto_header.fixed,
8015 crypto_header_bytes: &malformed[crypto_start..crypto_end],
8016 },
8017 )
8018 .unwrap_err(),
8019 FormatError::InvalidArchive(
8020 "CMRA parity shard count is below authenticated bit-rot lower bound"
8021 )
8022 );
8023 assert!(open_archive(&malformed, &master_key()).is_err());
8024 }
8025
8026 #[test]
8027 fn locator_tuple_bounds_are_checked_before_locator_position_fields() {
8028 let archive = write_archive(
8029 &[RegularFile::new(
8030 "locator-order.txt",
8031 b"locator tuple first",
8032 )],
8033 &master_key(),
8034 single_stream_options(),
8035 )
8036 .unwrap();
8037 let final_offset = archive.bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
8038 let mut locator = final_recovery_locator(&archive.bytes);
8039 locator.cmra_shard_size = 513;
8040 locator.body_bytes_before_cmra = locator.cmra_offset + 1;
8041 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
8042 let crypto_start = volume_header.crypto_header_offset as usize;
8043 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
8044 let crypto_header = CryptoHeader::parse(
8045 &archive.bytes[crypto_start..crypto_end],
8046 volume_header.crypto_header_length,
8047 )
8048 .unwrap();
8049 let subkeys = Subkeys::derive(
8050 &master_key(),
8051 &volume_header.archive_uuid,
8052 &volume_header.session_id,
8053 )
8054 .unwrap();
8055
8056 assert_eq!(
8057 parse_locator_cmra_candidate(
8058 &archive.bytes,
8059 final_offset,
8060 locator,
8061 KeyHoldingTerminalContext {
8062 subkeys: &subkeys,
8063 volume_header: &volume_header,
8064 crypto_header: &crypto_header.fixed,
8065 crypto_header_bytes: &archive.bytes[crypto_start..crypto_end],
8066 },
8067 )
8068 .unwrap_err(),
8069 FormatError::InvalidArchive("CMRA shard_size is invalid")
8070 );
8071 }
8072
8073 #[test]
8074 fn sequential_extract_rejects_bytes_after_terminal_locator() {
8075 let archive = write_archive(
8076 &[RegularFile::new("seq.txt", b"sequential EOF")],
8077 &master_key(),
8078 single_stream_options(),
8079 )
8080 .unwrap();
8081 let mut appended = archive.bytes.clone();
8082 appended.extend_from_slice(&[0xAA; 32]);
8083
8084 assert_eq!(
8085 sequential_extract_tar_stream(&appended, &master_key()).unwrap_err(),
8086 FormatError::InvalidArchive("sequential terminal does not end at EOF")
8087 );
8088 }
8089
8090 #[test]
8091 fn global_file_table_order_rejects_cross_shard_duplicate_reversal() {
8092 let first = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 2048);
8093 let second = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 1024);
8094
8095 assert_eq!(
8096 validate_global_file_table_key_step(Some(&first), &second).unwrap_err(),
8097 FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
8098 );
8099 }
8100
8101 #[test]
8102 fn root_auth_verifies_key_holding_and_public_no_key_modes() {
8103 let archive = write_archive_with_root_auth(
8104 &[RegularFile::new("signed.txt", b"ed25519 payload")],
8105 &master_key(),
8106 single_stream_options(),
8107 test_root_auth_config(),
8108 |request| Ok(test_root_auth_value(request)),
8109 )
8110 .unwrap();
8111
8112 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8113 let root_auth = opened
8114 .verify_root_auth_with(|footer, archive_root| {
8115 Ok(test_root_auth_verifies(footer, archive_root))
8116 })
8117 .unwrap();
8118 assert_eq!(
8119 root_auth.archive_root,
8120 opened.root_auth_footer.as_ref().unwrap().archive_root
8121 );
8122
8123 let public = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
8124 Ok(test_root_auth_verifies(footer, archive_root))
8125 })
8126 .unwrap();
8127 assert_eq!(public.archive_root, root_auth.archive_root);
8128 assert_eq!(
8129 public.diagnostics,
8130 vec![
8131 PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
8132 PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
8133 PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
8134 ]
8135 );
8136 }
8137
8138 #[test]
8139 fn root_auth_verifies_with_tolerated_missing_volume_after_fec_repair() {
8140 let options = WriterOptions {
8141 block_size: 4096,
8142 chunk_size: 16 * 1024,
8143 envelope_target_size: 16 * 1024,
8144 stripe_width: 2,
8145 volume_loss_tolerance: 1,
8146 bit_rot_buffer_pct: 0,
8147 fec_data_shards: 16,
8148 fec_parity_shards: 1,
8149 index_fec_data_shards: 4,
8150 index_fec_parity_shards: 1,
8151 index_root_fec_data_shards: 16,
8152 index_root_fec_parity_shards: 1,
8153 ..WriterOptions::default()
8154 };
8155 let archive = write_archive_with_root_auth(
8156 &[RegularFile::new("missing-volume.txt", b"recover me")],
8157 &master_key(),
8158 options,
8159 test_root_auth_config(),
8160 |request| Ok(test_root_auth_value(request)),
8161 )
8162 .unwrap();
8163
8164 let opened = open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap();
8165 let root_auth = opened
8166 .verify_root_auth_with(|footer, archive_root| {
8167 Ok(test_root_auth_verifies(footer, archive_root))
8168 })
8169 .unwrap();
8170 assert!(root_auth
8171 .diagnostics
8172 .contains(&RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss));
8173 }
8174
8175 #[test]
8176 fn public_no_key_rejects_unsigned_archives() {
8177 let archive = write_archive(
8178 &[RegularFile::new("plain.txt", b"unsigned")],
8179 &master_key(),
8180 single_stream_options(),
8181 )
8182 .unwrap();
8183
8184 assert_eq!(
8185 public_no_key_verify_archive_with(&archive.bytes, |_, _| Ok(true)).unwrap_err(),
8186 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
8187 );
8188 }
8189
8190 #[test]
8191 fn unsigned_archive_reports_root_auth_absent() {
8192 let archive = write_archive(
8193 &[RegularFile::new("plain.txt", b"unsigned")],
8194 &master_key(),
8195 single_stream_options(),
8196 )
8197 .unwrap();
8198 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8199
8200 assert_eq!(
8201 opened.verify_root_auth_with(|_, _| Ok(true)).unwrap_err(),
8202 FormatError::ReaderUnsupported("root-auth footer is absent")
8203 );
8204 }
8205
8206 #[test]
8207 fn safe_extract_writes_regular_file_under_root() {
8208 let archive = write_archive(
8209 &[RegularFile::new("dir/hello.txt", b"safe m8")],
8210 &master_key(),
8211 single_stream_options(),
8212 )
8213 .unwrap();
8214 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8215 let tmp = tempfile::tempdir().unwrap();
8216
8217 opened
8218 .extract_file_to(
8219 "dir/hello.txt",
8220 tmp.path(),
8221 SafeExtractionOptions::default(),
8222 )
8223 .unwrap()
8224 .unwrap();
8225
8226 assert_eq!(
8227 std::fs::read(tmp.path().join("dir").join("hello.txt")).unwrap(),
8228 b"safe m8"
8229 );
8230 }
8231
8232 #[test]
8233 fn seekable_extract_all_to_streams_unique_archive() {
8234 let archive = write_archive(
8235 &[
8236 RegularFile::new("alpha.txt", b"alpha"),
8237 RegularFile::new("dir/beta.txt", b"beta"),
8238 ],
8239 &master_key(),
8240 single_stream_options(),
8241 )
8242 .unwrap();
8243 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8244 let tmp = tempfile::tempdir().unwrap();
8245
8246 let diagnostics = opened
8247 .extract_all_to(tmp.path(), SafeExtractionOptions::default())
8248 .unwrap();
8249
8250 assert_eq!(diagnostics.len(), 2);
8251 assert_eq!(fs::read(tmp.path().join("alpha.txt")).unwrap(), b"alpha");
8252 assert_eq!(
8253 fs::read(tmp.path().join("dir").join("beta.txt")).unwrap(),
8254 b"beta"
8255 );
8256 }
8257
8258 #[test]
8259 fn seekable_extract_all_to_rejects_duplicate_paths_for_cli_fallback() {
8260 let archive = write_archive(
8261 &[
8262 RegularFile::new("same.txt", b"old"),
8263 RegularFile::new("same.txt", b"new"),
8264 ],
8265 &master_key(),
8266 single_stream_options(),
8267 )
8268 .unwrap();
8269 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8270 let tmp = tempfile::tempdir().unwrap();
8271
8272 assert_eq!(
8273 opened
8274 .extract_all_to(tmp.path(), SafeExtractionOptions::default())
8275 .unwrap_err(),
8276 FormatError::ReaderUnsupported("fast full extract requires unique archive paths")
8277 );
8278 }
8279
8280 #[test]
8281 fn seekable_extract_indexed_files_to_restores_final_duplicate_winner() {
8282 let archive = write_archive(
8283 &[
8284 RegularFile::new("same.txt", b"old"),
8285 RegularFile::new("same.txt", b"new"),
8286 ],
8287 &master_key(),
8288 single_stream_options(),
8289 )
8290 .unwrap();
8291 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8292 let tmp = tempfile::tempdir().unwrap();
8293
8294 let diagnostics = opened
8295 .extract_indexed_files_to(tmp.path(), SafeExtractionOptions::default(), 2)
8296 .unwrap();
8297
8298 assert_eq!(diagnostics.len(), 1);
8299 assert_eq!(diagnostics[0].0, "same.txt");
8300 assert_eq!(fs::read(tmp.path().join("same.txt")).unwrap(), b"new");
8301 }
8302
8303 #[test]
8304 fn safe_extract_rejects_overwriting_existing_file_by_default() {
8305 let archive = write_archive(
8306 &[RegularFile::new("hello.txt", b"new")],
8307 &master_key(),
8308 single_stream_options(),
8309 )
8310 .unwrap();
8311 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8312 let tmp = tempfile::tempdir().unwrap();
8313 std::fs::write(tmp.path().join("hello.txt"), b"old").unwrap();
8314
8315 assert_eq!(
8316 opened
8317 .extract_file_to("hello.txt", tmp.path(), SafeExtractionOptions::default())
8318 .unwrap_err(),
8319 FormatError::UnsafeOverwrite
8320 );
8321 assert_eq!(std::fs::read(tmp.path().join("hello.txt")).unwrap(), b"old");
8322 }
8323
8324 #[test]
8325 fn opens_and_verifies_empty_archive() {
8326 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
8327 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8328
8329 assert!(opened.list_files().unwrap().is_empty());
8330 opened.verify().unwrap();
8331 }
8332
8333 #[test]
8334 fn default_reader_options_allow_v36_trailing_garbage_scan() {
8335 let archive = write_archive(
8336 &[RegularFile::new("garbage-tolerant.txt", b"still intact")],
8337 &master_key(),
8338 single_stream_options(),
8339 )
8340 .unwrap();
8341 let mut with_trailing_garbage = archive.bytes.clone();
8342 with_trailing_garbage.extend_from_slice(b"ignored trailing bytes");
8343
8344 let opened = open_archive(&with_trailing_garbage, &master_key()).unwrap();
8345 assert_eq!(
8346 opened.extract_file("garbage-tolerant.txt").unwrap(),
8347 Some(b"still intact".to_vec())
8348 );
8349 }
8350
8351 #[test]
8352 fn seekable_open_rejects_too_small_and_unavailable_header_crypto_bytes() {
8353 assert_eq!(
8354 open_archive(
8355 &[0u8; VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1],
8356 &master_key()
8357 )
8358 .unwrap_err(),
8359 FormatError::InvalidLength {
8360 structure: "archive",
8361 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
8362 actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1,
8363 }
8364 );
8365
8366 let mut header = test_volume_header();
8367 header.crypto_header_length = 512;
8368 let mut unavailable_crypto = header.to_bytes().to_vec();
8369 unavailable_crypto.resize(VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN, 0);
8370
8371 assert_eq!(
8372 open_archive(&unavailable_crypto, &master_key()).unwrap_err(),
8373 FormatError::InvalidLength {
8374 structure: "CryptoHeader",
8375 expected: VOLUME_HEADER_LEN + 512,
8376 actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
8377 }
8378 );
8379 }
8380
8381 #[test]
8382 fn seekable_open_rejects_in_bounds_noncanonical_crypto_header_offset() {
8383 let archive = write_archive(
8384 &[RegularFile::new("offset.txt", b"offset")],
8385 &master_key(),
8386 single_stream_options(),
8387 )
8388 .unwrap();
8389 let mut mutated = archive.bytes;
8390 let mut header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
8391 header.crypto_header_offset = VOLUME_HEADER_LEN as u32 + 1;
8392 mutated[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
8393
8394 assert_eq!(
8395 open_archive(&mutated, &master_key()).unwrap_err(),
8396 FormatError::NonCanonicalCryptoHeaderOffset(VOLUME_HEADER_LEN as u32 + 1)
8397 );
8398 }
8399
8400 #[test]
8401 fn rejects_wrong_key_before_metadata_release() {
8402 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
8403 let wrong = MasterKey::from_raw_key(&[0x43; 32]).unwrap();
8404
8405 assert_eq!(
8406 open_archive(&archive.bytes, &wrong).unwrap_err(),
8407 FormatError::HmacMismatch {
8408 structure: "CryptoHeader"
8409 }
8410 );
8411 }
8412
8413 #[test]
8414 fn rejects_payload_tamper_even_with_recomputed_block_crc() {
8415 let mut archive = write_archive(
8416 &[RegularFile::new("file.txt", b"authenticated")],
8417 &master_key(),
8418 single_stream_options(),
8419 )
8420 .unwrap()
8421 .bytes;
8422 let volume = VolumeHeader::parse(&archive[..VOLUME_HEADER_LEN]).unwrap();
8423 let crypto_end = VOLUME_HEADER_LEN + usize::try_from(volume.crypto_header_length).unwrap();
8424 let crypto = CryptoHeader::parse(
8425 &archive[VOLUME_HEADER_LEN..crypto_end],
8426 volume.crypto_header_length,
8427 )
8428 .unwrap();
8429 let block_size = crypto.fixed.block_size as usize;
8430 archive[crypto_end + 16] ^= 1;
8431 let crc_offset = crypto_end + 16 + block_size;
8432 let crc = crc32c::crc32c(&archive[crypto_end..crc_offset]);
8433 archive[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
8434
8435 let opened = open_archive(&archive, &master_key()).unwrap();
8436 assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
8437 }
8438
8439 #[test]
8440 fn list_and_extract_use_final_view_for_duplicate_paths() {
8441 let archive = write_archive(
8442 &[
8443 RegularFile::new("same.txt", b"old"),
8444 RegularFile::new("same.txt", b"newer"),
8445 ],
8446 &master_key(),
8447 single_stream_options(),
8448 )
8449 .unwrap();
8450 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8451
8452 assert_eq!(
8453 opened.list_index_entries().unwrap(),
8454 vec![ArchiveIndexEntry {
8455 path: "same.txt".to_string(),
8456 file_data_size: 5,
8457 }]
8458 );
8459 assert_eq!(
8460 opened.lookup_index_entry("same.txt").unwrap(),
8461 Some(ArchiveIndexEntry {
8462 path: "same.txt".to_string(),
8463 file_data_size: 5,
8464 })
8465 );
8466 assert_eq!(opened.lookup_index_entry("missing.txt").unwrap(), None);
8467 assert_eq!(
8468 opened.list_files().unwrap(),
8469 vec![ArchiveEntry {
8470 path: "same.txt".to_string(),
8471 file_data_size: 5,
8472 kind: TarEntryKind::Regular,
8473 mode: 0o644,
8474 mtime: 0,
8475 diagnostics: Vec::new(),
8476 }]
8477 );
8478 assert_eq!(
8479 opened.extract_file("same.txt").unwrap(),
8480 Some(b"newer".to_vec())
8481 );
8482 opened.verify().unwrap();
8483 }
8484
8485 #[test]
8486 fn index_entries_do_not_decrypt_payload_envelopes() {
8487 let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
8488 corrupt_payload_record(&mut opened.blocks, broken_payload_block);
8489
8490 assert_eq!(
8491 opened.list_index_entries().unwrap(),
8492 vec![
8493 ArchiveIndexEntry {
8494 path: "broken.txt".to_string(),
8495 file_data_size: b"broken payload\n".len() as u64,
8496 },
8497 ArchiveIndexEntry {
8498 path: "healthy.txt".to_string(),
8499 file_data_size: b"healthy payload\n".len() as u64,
8500 },
8501 ]
8502 );
8503 assert_eq!(
8504 opened.lookup_index_entry("broken.txt").unwrap(),
8505 Some(ArchiveIndexEntry {
8506 path: "broken.txt".to_string(),
8507 file_data_size: b"broken payload\n".len() as u64,
8508 })
8509 );
8510 assert_eq!(opened.list_files().unwrap_err(), FormatError::AeadFailure);
8511 }
8512
8513 #[test]
8514 fn extract_file_does_not_decrypt_unselected_payload_envelope() {
8515 let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
8518 corrupt_payload_record(&mut opened.blocks, broken_payload_block);
8519
8520 assert_eq!(
8521 opened.extract_file("healthy.txt").unwrap(),
8522 Some(b"healthy payload\n".to_vec())
8523 );
8524 assert_eq!(
8525 opened.extract_file("broken.txt").unwrap_err(),
8526 FormatError::AeadFailure
8527 );
8528 assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
8529 }
8530
8531 #[test]
8532 fn seekable_extract_does_not_read_unselected_payload_envelope() {
8533 let healthy = pseudo_random_bytes(64 * 1024);
8534 let broken = pseudo_random_bytes(64 * 1024);
8535 let options = WriterOptions {
8536 block_size: 4096,
8537 chunk_size: 4096,
8538 envelope_target_size: 8192,
8539 stripe_width: 1,
8540 volume_loss_tolerance: 0,
8541 bit_rot_buffer_pct: 0,
8542 fec_data_shards: 4,
8543 fec_parity_shards: 0,
8544 index_fec_data_shards: 4,
8545 index_fec_parity_shards: 0,
8546 index_root_fec_data_shards: 4,
8547 index_root_fec_parity_shards: 0,
8548 ..WriterOptions::default()
8549 };
8550 let archive = write_archive(
8551 &[
8552 RegularFile::new("healthy.bin", &healthy),
8553 RegularFile::new("broken.bin", &broken),
8554 ],
8555 &master_key(),
8556 options,
8557 )
8558 .unwrap();
8559 let eager = open_archive(&archive.bytes, &master_key()).unwrap();
8560 let healthy_envelopes = envelope_indices_for_path(&eager, "healthy.bin");
8561 let broken_envelopes = envelope_entries_for_path(&eager, "broken.bin");
8562 let denied_block_indices = broken_envelopes
8563 .iter()
8564 .filter(|envelope| !healthy_envelopes.contains(&envelope.envelope_index))
8565 .flat_map(|envelope| {
8566 let block_count =
8567 envelope.data_block_count as u64 + envelope.parity_block_count as u64;
8568 envelope.first_block_index..envelope.first_block_index + block_count
8569 })
8570 .collect::<BTreeSet<_>>();
8571 assert!(
8572 !denied_block_indices.is_empty(),
8573 "fixture must place broken.bin in at least one unshared envelope"
8574 );
8575 let denied_ranges = block_record_slots(&archive.bytes)
8576 .into_iter()
8577 .filter_map(|(offset, len, record)| {
8578 denied_block_indices
8579 .contains(&record.block_index)
8580 .then_some((offset as u64, (offset + len) as u64))
8581 })
8582 .collect::<Vec<_>>();
8583 assert!(!denied_ranges.is_empty());
8584
8585 let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
8586 let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
8587
8588 assert_eq!(opened.extract_file("healthy.bin").unwrap(), Some(healthy));
8589 for (read_start, read_end) in reader.reads() {
8590 assert!(
8591 denied_ranges
8592 .iter()
8593 .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
8594 "targeted extract read an unrelated payload BlockRecord range"
8595 );
8596 }
8597 assert_eq!(
8598 opened.extract_file("broken.bin").unwrap_err(),
8599 FormatError::InvalidArchive("denied test read")
8600 );
8601 }
8602
8603 #[test]
8604 fn extract_file_to_writer_streams_before_reading_later_envelopes() {
8605 struct FailOnFirstWrite;
8606
8607 impl std::io::Write for FailOnFirstWrite {
8608 fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
8609 Err(std::io::Error::other("sink stopped"))
8610 }
8611
8612 fn flush(&mut self) -> std::io::Result<()> {
8613 Ok(())
8614 }
8615 }
8616
8617 let payload = pseudo_random_bytes(128 * 1024);
8618 let options = WriterOptions {
8619 block_size: 4096,
8620 chunk_size: 4096,
8621 envelope_target_size: 8192,
8622 stripe_width: 1,
8623 volume_loss_tolerance: 0,
8624 bit_rot_buffer_pct: 0,
8625 fec_data_shards: 4,
8626 fec_parity_shards: 0,
8627 index_fec_data_shards: 4,
8628 index_fec_parity_shards: 0,
8629 index_root_fec_data_shards: 4,
8630 index_root_fec_parity_shards: 0,
8631 ..WriterOptions::default()
8632 };
8633 let archive = write_archive(
8634 &[RegularFile::new("large.bin", &payload)],
8635 &master_key(),
8636 options,
8637 )
8638 .unwrap();
8639 let eager = open_archive(&archive.bytes, &master_key()).unwrap();
8640 let envelopes = envelope_entries_for_path(&eager, "large.bin");
8641 let first_envelope = envelopes
8642 .first()
8643 .expect("large fixture should have at least one envelope")
8644 .envelope_index;
8645 let later_envelope_blocks = envelopes
8646 .iter()
8647 .filter(|entry| entry.envelope_index != first_envelope)
8648 .flat_map(|entry| {
8649 let block_count = entry.data_block_count as u64 + entry.parity_block_count as u64;
8650 entry.first_block_index..entry.first_block_index + block_count
8651 })
8652 .collect::<BTreeSet<_>>();
8653 assert!(
8654 !later_envelope_blocks.is_empty(),
8655 "fixture must span more than one payload envelope"
8656 );
8657 let denied_ranges = block_record_slots(&archive.bytes)
8658 .into_iter()
8659 .filter_map(|(offset, len, record)| {
8660 later_envelope_blocks
8661 .contains(&record.block_index)
8662 .then_some((offset as u64, (offset + len) as u64))
8663 })
8664 .collect::<Vec<_>>();
8665 assert!(!denied_ranges.is_empty());
8666
8667 let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
8668 let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
8669 let mut writer = FailOnFirstWrite;
8670
8671 let err = opened
8672 .extract_file_to_writer("large.bin", &mut writer)
8673 .unwrap_err();
8674 assert_eq!(err.to_string(), "extraction output write failed");
8675 for (read_start, read_end) in reader.reads() {
8676 assert!(
8677 denied_ranges
8678 .iter()
8679 .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
8680 "streaming writer read a later payload envelope before surfacing writer failure"
8681 );
8682 }
8683 }
8684
8685 #[test]
8686 fn extract_file_to_writer_writes_bounded_chunks() {
8687 struct ChunkRecorder {
8688 total: usize,
8689 max_write: usize,
8690 writes: usize,
8691 }
8692
8693 impl std::io::Write for ChunkRecorder {
8694 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
8695 self.total += buf.len();
8696 self.max_write = self.max_write.max(buf.len());
8697 self.writes += 1;
8698 Ok(buf.len())
8699 }
8700
8701 fn flush(&mut self) -> std::io::Result<()> {
8702 Ok(())
8703 }
8704 }
8705
8706 let payload = pseudo_random_bytes(128 * 1024);
8707 let options = WriterOptions {
8708 block_size: 4096,
8709 chunk_size: 4096,
8710 envelope_target_size: 8192,
8711 stripe_width: 1,
8712 volume_loss_tolerance: 0,
8713 bit_rot_buffer_pct: 0,
8714 fec_data_shards: 4,
8715 fec_parity_shards: 0,
8716 index_fec_data_shards: 4,
8717 index_fec_parity_shards: 0,
8718 index_root_fec_data_shards: 4,
8719 index_root_fec_parity_shards: 0,
8720 ..WriterOptions::default()
8721 };
8722 let archive = write_archive(
8723 &[RegularFile::new("large.bin", &payload)],
8724 &master_key(),
8725 options,
8726 )
8727 .unwrap();
8728 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8729 let mut writer = ChunkRecorder {
8730 total: 0,
8731 max_write: 0,
8732 writes: 0,
8733 };
8734
8735 opened
8736 .extract_file_to_writer("large.bin", &mut writer)
8737 .unwrap()
8738 .unwrap();
8739
8740 assert_eq!(writer.total, payload.len());
8741 assert!(writer.writes > 1);
8742 assert!(
8743 writer.max_write <= options.chunk_size as usize,
8744 "writer saw a {} byte chunk, larger than the {} byte frame target",
8745 writer.max_write,
8746 options.chunk_size
8747 );
8748 }
8749
8750 #[test]
8751 fn streaming_filesystem_extract_does_not_publish_partial_file_on_late_payload_error() {
8752 let payload = pseudo_random_bytes(128 * 1024);
8753 let options = WriterOptions {
8754 block_size: 4096,
8755 chunk_size: 4096,
8756 envelope_target_size: 8192,
8757 stripe_width: 1,
8758 volume_loss_tolerance: 0,
8759 bit_rot_buffer_pct: 0,
8760 fec_data_shards: 4,
8761 fec_parity_shards: 0,
8762 index_fec_data_shards: 4,
8763 index_fec_parity_shards: 0,
8764 index_root_fec_data_shards: 4,
8765 index_root_fec_parity_shards: 0,
8766 ..WriterOptions::default()
8767 };
8768 let archive = write_archive(
8769 &[RegularFile::new("large.bin", &payload)],
8770 &master_key(),
8771 options,
8772 )
8773 .unwrap();
8774 let eager = open_archive(&archive.bytes, &master_key()).unwrap();
8775 let envelopes = envelope_entries_for_path(&eager, "large.bin");
8776 let last_envelope = envelopes
8777 .last()
8778 .expect("large fixture should have at least one envelope");
8779 assert_ne!(
8780 envelopes.first().unwrap().envelope_index,
8781 last_envelope.envelope_index,
8782 "fixture must span more than one payload envelope"
8783 );
8784 let corrupt_slot = block_record_slots(&archive.bytes)
8785 .into_iter()
8786 .enumerate()
8787 .find_map(|(slot, (_, _, record))| {
8788 (record.block_index == last_envelope.first_block_index).then_some(slot)
8789 })
8790 .unwrap();
8791 let mut corrupted = archive.bytes;
8792 corrupt_block_record_payload_at_slot(&mut corrupted, corrupt_slot);
8793 let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
8794 let tmp = tempfile::tempdir().unwrap();
8795
8796 assert!(matches!(
8797 opened
8798 .extract_file_to("large.bin", tmp.path(), SafeExtractionOptions::default())
8799 .unwrap_err(),
8800 FormatError::AeadFailure | FormatError::FecTooFewAvailableShards
8801 ));
8802 assert!(!tmp.path().join("large.bin").exists());
8803 assert_eq!(std::fs::read_dir(tmp.path()).unwrap().count(), 0);
8804 }
8805
8806 #[test]
8807 fn bootstrap_sidecar_opens_lists_verifies_and_extracts() {
8808 let archive = write_archive(
8809 &[RegularFile::new("dir/sidecar.txt", b"hello sidecar")],
8810 &master_key(),
8811 single_stream_options(),
8812 )
8813 .unwrap();
8814 let opened = open_archive_with_bootstrap_sidecar(
8815 &archive.bytes,
8816 &archive.bootstrap_sidecar,
8817 &master_key(),
8818 )
8819 .unwrap();
8820
8821 assert_eq!(
8822 opened.list_files().unwrap(),
8823 vec![ArchiveEntry {
8824 path: "dir/sidecar.txt".to_string(),
8825 file_data_size: 13,
8826 kind: TarEntryKind::Regular,
8827 mode: 0o644,
8828 mtime: 0,
8829 diagnostics: Vec::new(),
8830 }]
8831 );
8832 assert_eq!(
8833 opened.extract_file("dir/sidecar.txt").unwrap(),
8834 Some(b"hello sidecar".to_vec())
8835 );
8836 opened.verify().unwrap();
8837 }
8838
8839 #[test]
8840 fn dictionary_archive_opens_lists_verifies_and_extracts_seekable() {
8841 let archive = write_archive_with_dictionary(
8842 &[RegularFile::new(
8843 "dir/dict.txt",
8844 b"common words common words dictionary payload",
8845 )],
8846 &master_key(),
8847 single_stream_options(),
8848 dictionary(),
8849 )
8850 .unwrap();
8851 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8852
8853 assert_eq!(opened.crypto_header.has_dictionary, 1);
8854 assert!(opened.index_root.header.dictionary_data_block_count > 0);
8855 assert_eq!(
8856 opened.list_files().unwrap(),
8857 vec![ArchiveEntry {
8858 path: "dir/dict.txt".to_string(),
8859 file_data_size: 44,
8860 kind: TarEntryKind::Regular,
8861 mode: 0o644,
8862 mtime: 0,
8863 diagnostics: Vec::new(),
8864 }]
8865 );
8866 assert_eq!(
8867 opened.extract_file("dir/dict.txt").unwrap(),
8868 Some(b"common words common words dictionary payload".to_vec())
8869 );
8870 opened.verify().unwrap();
8871 }
8872
8873 #[test]
8874 fn dictionary_object_tamper_fails_before_payload_decompression() {
8875 let archive = write_archive_with_dictionary(
8876 &[RegularFile::new(
8877 "dir/dict.txt",
8878 b"common words common words dictionary payload",
8879 )],
8880 &master_key(),
8881 single_stream_options(),
8882 dictionary(),
8883 )
8884 .unwrap();
8885 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8886 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
8887 let crypto_end = VOLUME_HEADER_LEN + volume_header.crypto_header_length as usize;
8888 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
8889 let dictionary_offset =
8890 crypto_end + opened.index_root.header.dictionary_first_block as usize * record_len;
8891
8892 let mut tampered = archive.bytes.clone();
8893 tampered[dictionary_offset + 16] ^= 0x01;
8894 let crc_offset = dictionary_offset + 16 + opened.crypto_header.block_size as usize;
8895 let crc = crc32c::crc32c(&tampered[dictionary_offset..crc_offset]);
8896 tampered[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
8897
8898 assert_eq!(
8899 open_archive(&tampered, &master_key()).unwrap_err(),
8900 FormatError::AeadFailure
8901 );
8902 }
8903
8904 #[test]
8905 fn dictionary_archive_bootstraps_from_sidecar_for_non_seekable_open() {
8906 let archive = write_archive_with_dictionary(
8907 &[RegularFile::new(
8908 "dict-sidecar.txt",
8909 b"common words common words sidecar payload",
8910 )],
8911 &master_key(),
8912 single_stream_options(),
8913 dictionary(),
8914 )
8915 .unwrap();
8916 let opened = open_non_seekable_archive(
8917 &archive.bytes,
8918 &master_key(),
8919 Some(&archive.bootstrap_sidecar),
8920 )
8921 .unwrap();
8922
8923 assert_eq!(
8924 opened.extract_file("dict-sidecar.txt").unwrap(),
8925 Some(b"common words common words sidecar payload".to_vec())
8926 );
8927 opened.verify().unwrap();
8928 }
8929
8930 #[test]
8931 fn non_seekable_full_sidecar_bootstraps_when_terminal_trailer_is_corrupt() {
8932 let archive = write_archive(
8933 &[RegularFile::new(
8934 "sidecar-terminal.txt",
8935 b"sidecar authority",
8936 )],
8937 &master_key(),
8938 single_stream_options(),
8939 )
8940 .unwrap();
8941 let mut corrupted = archive.bytes.clone();
8942 corrupt_v41_terminal_recovery(&mut corrupted);
8943 assert!(open_archive(&corrupted, &master_key()).is_err());
8944
8945 let opened =
8946 open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
8947 .unwrap();
8948
8949 assert!(opened.volume_trailer.is_none());
8950 assert_eq!(
8951 opened.extract_file("sidecar-terminal.txt").unwrap(),
8952 Some(b"sidecar authority".to_vec())
8953 );
8954 opened.verify().unwrap();
8955 }
8956
8957 #[test]
8958 fn dictionary_full_sidecar_bootstraps_when_terminal_material_is_absent() {
8959 let archive = write_archive_with_dictionary(
8960 &[RegularFile::new(
8961 "dict-no-terminal.txt",
8962 b"common words common words without terminal",
8963 )],
8964 &master_key(),
8965 single_stream_options(),
8966 dictionary(),
8967 )
8968 .unwrap();
8969 let terminal_offset = terminal_material_offset(&archive.bytes);
8970 let truncated = archive.bytes[..terminal_offset].to_vec();
8971 assert!(open_archive(&truncated, &master_key()).is_err());
8972
8973 let opened =
8974 open_non_seekable_archive(&truncated, &master_key(), Some(&archive.bootstrap_sidecar))
8975 .unwrap();
8976
8977 assert!(opened.volume_trailer.is_none());
8978 assert_eq!(
8979 opened.extract_file("dict-no-terminal.txt").unwrap(),
8980 Some(b"common words common words without terminal".to_vec())
8981 );
8982 opened.verify().unwrap();
8983 }
8984
8985 #[test]
8986 fn bootstrap_sidecar_treats_crc_failed_payload_block_as_erasure() {
8987 let archive = write_archive(
8988 &[RegularFile::new(
8989 "sidecar-erasure.txt",
8990 b"repair through sidecar",
8991 )],
8992 &master_key(),
8993 single_stream_options(),
8994 )
8995 .unwrap();
8996 let mut corrupted = archive.bytes.clone();
8997 corrupt_first_block_record_payload(&mut corrupted);
8998
8999 let opened = open_archive_with_bootstrap_sidecar(
9000 &corrupted,
9001 &archive.bootstrap_sidecar,
9002 &master_key(),
9003 )
9004 .unwrap();
9005 assert_eq!(
9006 opened.extract_file("sidecar-erasure.txt").unwrap(),
9007 Some(b"repair through sidecar".to_vec())
9008 );
9009 }
9010
9011 #[test]
9012 fn extraction_rejects_logical_payload_above_total_size_cap() {
9013 let archive = write_archive(
9014 &[RegularFile::new("cap.txt", b"payload")],
9015 &master_key(),
9016 single_stream_options(),
9017 )
9018 .unwrap();
9019 let options = ReaderOptions {
9020 max_total_extraction_size: 3,
9021 ..ReaderOptions::default()
9022 };
9023 let opened =
9024 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
9025
9026 assert_eq!(
9027 opened.extract_file("cap.txt").unwrap_err(),
9028 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
9029 );
9030 }
9031
9032 #[test]
9033 fn verify_does_not_apply_extraction_payload_cap() {
9034 let archive = write_archive(
9035 &[RegularFile::new("verify-cap.txt", b"payload")],
9036 &master_key(),
9037 single_stream_options(),
9038 )
9039 .unwrap();
9040 let options = ReaderOptions {
9041 max_total_extraction_size: 3,
9042 ..ReaderOptions::default()
9043 };
9044 let opened =
9045 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
9046
9047 opened.verify().unwrap();
9048 assert_eq!(
9049 opened.extract_file("verify-cap.txt").unwrap_err(),
9050 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
9051 );
9052 }
9053
9054 #[test]
9055 fn verify_streams_past_legacy_in_memory_tar_cap() {
9056 let data = vec![0x5a; 4096];
9057 let archive = write_archive(
9058 &[RegularFile::new("verify-large.txt", &data)],
9059 &master_key(),
9060 single_stream_options(),
9061 )
9062 .unwrap();
9063 let options = ReaderOptions {
9064 max_verify_tar_size: 1,
9065 ..ReaderOptions::default()
9066 };
9067 let opened =
9068 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
9069
9070 opened.verify().unwrap();
9071 }
9072
9073 #[test]
9074 fn dictionary_sidecar_requires_dictionary_record_section() {
9075 let archive = write_archive_with_dictionary(
9076 &[RegularFile::new("dict-missing.txt", b"common words")],
9077 &master_key(),
9078 single_stream_options(),
9079 dictionary(),
9080 )
9081 .unwrap();
9082 let header = BootstrapSidecarHeader::parse(
9083 &archive.bootstrap_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN],
9084 )
9085 .unwrap();
9086 let mut missing_dictionary =
9087 archive.bootstrap_sidecar[..header.dictionary_records_offset as usize].to_vec();
9088 rewrite_sidecar_header(&mut missing_dictionary, &master_key(), |header| {
9089 header.flags &= !0x04;
9090 header.dictionary_records_offset = 0;
9091 header.dictionary_records_length = 0;
9092 });
9093
9094 assert_eq!(
9095 open_non_seekable_archive(&archive.bytes, &master_key(), Some(&missing_dictionary))
9096 .unwrap_err(),
9097 FormatError::ReaderUnsupported("dictionary bootstrap required")
9098 );
9099 }
9100
9101 #[test]
9102 fn dictionary_sidecar_records_are_validated_against_dictionary_extent() {
9103 let archive = write_archive_with_dictionary(
9104 &[RegularFile::new("dict-sidecar-kind.txt", b"common words")],
9105 &master_key(),
9106 single_stream_options(),
9107 dictionary(),
9108 )
9109 .unwrap();
9110
9111 let mut wrong_kind = archive.bootstrap_sidecar.clone();
9112 mutate_sidecar_dictionary_record(&mut wrong_kind, 0, |record| {
9113 record.kind = BlockKind::IndexRootData;
9114 });
9115 assert_eq!(
9116 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
9117 .unwrap_err(),
9118 FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
9119 );
9120
9121 let mut wrong_last = archive.bootstrap_sidecar.clone();
9122 mutate_sidecar_dictionary_record(&mut wrong_last, 0, |record| {
9123 record.flags = 0;
9124 });
9125 assert_eq!(
9126 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
9127 .unwrap_err(),
9128 FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
9129 );
9130 }
9131
9132 #[test]
9133 fn non_seekable_random_access_requires_sidecar() {
9134 let archive = write_archive(
9135 &[RegularFile::new("file.txt", b"payload")],
9136 &master_key(),
9137 single_stream_options(),
9138 )
9139 .unwrap();
9140
9141 assert_eq!(
9142 open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
9143 FormatError::ReaderUnsupported(
9144 "non-seekable random access requires a bootstrap sidecar"
9145 )
9146 );
9147 assert!(open_non_seekable_archive(
9148 &archive.bytes,
9149 &master_key(),
9150 Some(&archive.bootstrap_sidecar)
9151 )
9152 .is_ok());
9153 }
9154
9155 #[test]
9156 fn non_seekable_bootstrap_rejects_index_root_only_sidecar() {
9157 let archive = write_archive(
9158 &[RegularFile::new("sparse.txt", b"sparse sidecar")],
9159 &master_key(),
9160 single_stream_options(),
9161 )
9162 .unwrap();
9163 let index_root_only = sparse_bootstrap_sidecar(
9164 &archive.bootstrap_sidecar,
9165 &master_key(),
9166 false,
9167 true,
9168 false,
9169 );
9170
9171 assert_eq!(
9172 open_non_seekable_archive(&archive.bytes, &master_key(), Some(&index_root_only))
9173 .unwrap_err(),
9174 FormatError::ReaderUnsupported(
9175 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
9176 )
9177 );
9178 }
9179
9180 #[test]
9181 fn seekable_sidecar_uses_index_root_records_after_terminal_manifest_authority() {
9182 let archive = write_archive(
9183 &[RegularFile::new("sparse-index.txt", b"recover index root")],
9184 &master_key(),
9185 single_stream_options(),
9186 )
9187 .unwrap();
9188 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9189 let mut corrupted = archive.bytes.clone();
9190 corrupt_object_extent_records(
9191 &mut corrupted,
9192 index_root_extent_from_manifest(&opened.manifest_footer),
9193 );
9194 assert!(open_archive(&corrupted, &master_key()).is_err());
9195
9196 let index_root_only = sparse_bootstrap_sidecar(
9197 &archive.bootstrap_sidecar,
9198 &master_key(),
9199 false,
9200 true,
9201 false,
9202 );
9203 let recovered =
9204 open_archive_with_bootstrap_sidecar(&corrupted, &index_root_only, &master_key())
9205 .unwrap();
9206
9207 assert_eq!(
9208 recovered.extract_file("sparse-index.txt").unwrap(),
9209 Some(b"recover index root".to_vec())
9210 );
9211 recovered.verify().unwrap();
9212 }
9213
9214 #[test]
9215 fn seekable_sidecar_uses_dictionary_records_after_index_root_authority() {
9216 let archive = write_archive_with_dictionary(
9217 &[RegularFile::new(
9218 "sparse-dict.txt",
9219 b"common words common words sparse dictionary",
9220 )],
9221 &master_key(),
9222 single_stream_options(),
9223 dictionary(),
9224 )
9225 .unwrap();
9226 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9227 let mut corrupted = archive.bytes.clone();
9228 corrupt_object_extent_records(
9229 &mut corrupted,
9230 dictionary_extent_from_index_root(&opened.index_root).unwrap(),
9231 );
9232 assert!(open_archive(&corrupted, &master_key()).is_err());
9233
9234 let dictionary_only = sparse_bootstrap_sidecar(
9235 &archive.bootstrap_sidecar,
9236 &master_key(),
9237 false,
9238 false,
9239 true,
9240 );
9241 assert_eq!(
9242 open_non_seekable_archive(&archive.bytes, &master_key(), Some(&dictionary_only))
9243 .unwrap_err(),
9244 FormatError::ReaderUnsupported(
9245 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
9246 )
9247 );
9248
9249 let recovered =
9250 open_archive_with_bootstrap_sidecar(&corrupted, &dictionary_only, &master_key())
9251 .unwrap();
9252 assert_eq!(
9253 recovered.extract_file("sparse-dict.txt").unwrap(),
9254 Some(b"common words common words sparse dictionary".to_vec())
9255 );
9256 recovered.verify().unwrap();
9257 }
9258
9259 #[test]
9260 fn sequential_extracts_dictionary_free_tar_stream() {
9261 let archive = write_archive(
9262 &[RegularFile::new("seq.txt", b"streaming")],
9263 &master_key(),
9264 single_stream_options(),
9265 )
9266 .unwrap();
9267
9268 let tar_stream = sequential_extract_tar_stream(&archive.bytes, &master_key()).unwrap();
9269 let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
9270 assert_eq!(member.path, b"seq.txt");
9271 assert_eq!(member.data, b"streaming");
9272 }
9273
9274 #[test]
9275 fn sequential_rejects_logical_payload_above_total_size_cap() {
9276 let archive = write_archive(
9277 &[RegularFile::new("seq-cap.txt", b"payload")],
9278 &master_key(),
9279 single_stream_options(),
9280 )
9281 .unwrap();
9282 let options = ReaderOptions {
9283 max_total_extraction_size: 3,
9284 ..ReaderOptions::default()
9285 };
9286
9287 assert_eq!(
9288 sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
9289 .unwrap_err(),
9290 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
9291 );
9292 }
9293
9294 #[test]
9295 fn sequential_rejects_tar_stream_above_buffer_cap_during_decode() {
9296 let archive = write_archive(
9297 &[RegularFile::new("seq-buffer-cap.txt", b"payload")],
9298 &master_key(),
9299 single_stream_options(),
9300 )
9301 .unwrap();
9302 let options = ReaderOptions {
9303 max_verify_tar_size: 512,
9304 ..ReaderOptions::default()
9305 };
9306
9307 assert_eq!(
9308 sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
9309 .unwrap_err(),
9310 FormatError::ReaderUnsupported(
9311 "sequential tar stream exceeds configured verification cap"
9312 )
9313 );
9314 }
9315
9316 #[test]
9317 fn sequential_repairs_crc_failed_payload_data_when_parity_is_guaranteed() {
9318 let archive = write_archive(
9319 &[RegularFile::new("seq-erasure.txt", b"stream repair")],
9320 &master_key(),
9321 single_stream_options(),
9322 )
9323 .unwrap();
9324 let mut corrupted = archive.bytes;
9325 corrupt_first_block_record_payload(&mut corrupted);
9326
9327 let tar_stream = sequential_extract_tar_stream(&corrupted, &master_key()).unwrap();
9328 let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
9329 assert_eq!(member.path, b"seq-erasure.txt");
9330 assert_eq!(member.data, b"stream repair");
9331 }
9332
9333 #[test]
9334 fn sequential_rejects_crc_failed_payload_data_without_guaranteed_parity() {
9335 let archive = write_archive(
9336 &[RegularFile::new("seq-no-parity.txt", b"no repair")],
9337 &master_key(),
9338 WriterOptions {
9339 bit_rot_buffer_pct: 0,
9340 fec_parity_shards: 0,
9341 index_fec_parity_shards: 0,
9342 index_root_fec_parity_shards: 0,
9343 ..single_stream_options()
9344 },
9345 )
9346 .unwrap();
9347 let mut corrupted = archive.bytes;
9348 corrupt_first_block_record_payload(&mut corrupted);
9349
9350 assert_eq!(
9351 sequential_extract_tar_stream(&corrupted, &master_key()).unwrap_err(),
9352 FormatError::BadCrc {
9353 structure: "BlockRecord"
9354 }
9355 );
9356 }
9357
9358 #[test]
9359 fn sequential_rejects_when_terminal_authentication_fails_without_returning_bytes() {
9360 let archive = write_archive(
9361 &[RegularFile::new(
9362 "seq.txt",
9363 b"payload must not be returned after terminal auth failure",
9364 )],
9365 &master_key(),
9366 single_stream_options(),
9367 )
9368 .unwrap();
9369 let mut corrupted = archive.bytes;
9370 corrupt_v41_terminal_recovery(&mut corrupted);
9371
9372 match sequential_extract_tar_stream(&corrupted, &master_key()) {
9373 Ok(bytes) => panic!(
9374 "sequential helper returned {} decoded byte(s) despite terminal HMAC failure",
9375 bytes.len()
9376 ),
9377 Err(err) => assert_eq!(
9378 err,
9379 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
9380 ),
9381 }
9382 }
9383
9384 #[test]
9385 fn sequential_rejects_dictionary_archive_without_bootstrap_before_payload_release() {
9386 let archive = write_archive_with_dictionary(
9387 &[RegularFile::new(
9388 "seq-dict.txt",
9389 b"common words common words dictionary payload",
9390 )],
9391 &master_key(),
9392 single_stream_options(),
9393 b"common words dictionary",
9394 )
9395 .unwrap();
9396
9397 match sequential_extract_tar_stream(&archive.bytes, &master_key()) {
9398 Ok(bytes) => panic!(
9399 "sequential helper returned {} decoded byte(s) for dictionary archive without bootstrap",
9400 bytes.len()
9401 ),
9402 Err(err) => assert_eq!(
9403 err,
9404 FormatError::ReaderUnsupported(
9405 "dictionary bootstrap required for non-seekable sequential extraction"
9406 )
9407 ),
9408 }
9409 }
9410
9411 #[test]
9412 fn non_seekable_dictionary_error_keeps_missing_bootstrap_wording() {
9413 let archive = write_archive_with_dictionary(
9414 &[RegularFile::new(
9415 "seq-dict-open.txt",
9416 b"common words common words bootstrap required",
9417 )],
9418 &master_key(),
9419 single_stream_options(),
9420 b"common words bootstrap",
9421 )
9422 .unwrap();
9423
9424 assert_eq!(
9425 open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
9426 FormatError::ReaderUnsupported(
9427 "non-seekable random access requires a bootstrap sidecar"
9428 )
9429 );
9430 }
9431
9432 #[test]
9433 fn sequential_zstd_stream_rejects_skippable_frame_segments() {
9434 let skippable = [0x50, 0x2a, 0x4d, 0x18, 0, 0, 0, 0];
9435 let mut output = Vec::new();
9436
9437 assert_eq!(
9438 decode_concatenated_zstd_frames_with_cap(
9439 &skippable,
9440 None,
9441 &mut output,
9442 usize::MAX,
9443 None,
9444 )
9445 .unwrap_err(),
9446 FormatError::NotStandardZstdFrame
9447 );
9448 assert!(output.is_empty());
9449 }
9450
9451 #[test]
9452 fn live_non_seekable_verify_stream_accepts_single_volume_archive() {
9453 let archive = write_archive(
9454 &[RegularFile::new("live.txt", b"stream verify")],
9455 &master_key(),
9456 single_stream_options(),
9457 )
9458 .unwrap();
9459
9460 let report =
9461 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
9462
9463 assert_eq!(report.file_count, 1);
9464 assert_eq!(report.total_volumes, 1);
9465 assert_eq!(report.root_auth, SequentialRootAuthStatus::Absent);
9466 assert!(report.payload_block_count > 0);
9467 }
9468
9469 #[test]
9470 fn live_non_seekable_verify_stream_accepts_tiny_read_chunks() {
9471 let archive = write_archive(
9472 &[RegularFile::new("tiny-chunks.txt", b"one byte at a time")],
9473 &master_key(),
9474 single_stream_options(),
9475 )
9476 .unwrap();
9477
9478 let report =
9479 verify_non_seekable_stream(ChunkedReader::new(archive.bytes, 1), &master_key())
9480 .unwrap();
9481
9482 assert_eq!(report.file_count, 1);
9483 assert_eq!(report.tar_total_size % 512, 0);
9484 }
9485
9486 #[test]
9487 fn live_non_seekable_verify_stream_accepts_empty_archive() {
9488 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9489
9490 let report =
9491 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
9492
9493 assert_eq!(report.file_count, 0);
9494 assert_eq!(report.payload_block_count, 0);
9495 assert_eq!(report.tar_total_size, 0);
9496 }
9497
9498 #[test]
9499 fn live_non_seekable_verify_rejects_dictionary_archive_without_bootstrap() {
9500 let archive = write_archive_with_dictionary(
9501 &[RegularFile::new(
9502 "live-dict.txt",
9503 b"common words common words dictionary payload",
9504 )],
9505 &master_key(),
9506 single_stream_options(),
9507 b"common words dictionary",
9508 )
9509 .unwrap();
9510
9511 assert_eq!(
9512 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key())
9513 .unwrap_err(),
9514 FormatError::ReaderUnsupported(
9515 "dictionary bootstrap required for non-seekable sequential verification"
9516 )
9517 );
9518 }
9519
9520 #[test]
9521 fn live_non_seekable_verify_accepts_dictionary_archive_with_bootstrap() {
9522 let archive = write_archive_with_dictionary(
9523 &[RegularFile::new(
9524 "live-dict-sidecar.txt",
9525 b"common words common words dictionary payload",
9526 )],
9527 &master_key(),
9528 single_stream_options(),
9529 b"common words dictionary",
9530 )
9531 .unwrap();
9532
9533 let report = verify_non_seekable_stream_with_bootstrap_sidecar(
9534 std::io::Cursor::new(archive.bytes),
9535 &archive.bootstrap_sidecar,
9536 &master_key(),
9537 NonSeekableReaderOptions::default(),
9538 )
9539 .unwrap();
9540
9541 assert_eq!(report.file_count, 1);
9542 assert_eq!(report.total_volumes, 1);
9543 }
9544
9545 #[test]
9546 fn live_non_seekable_verify_rejects_terminal_tail_above_cap() {
9547 let archive = write_archive(
9548 &[RegularFile::new("tail-cap.txt", b"payload")],
9549 &master_key(),
9550 single_stream_options(),
9551 )
9552 .unwrap();
9553 let options = NonSeekableReaderOptions {
9554 max_terminal_tail_size: 8,
9555 ..NonSeekableReaderOptions::default()
9556 };
9557
9558 assert_eq!(
9559 verify_non_seekable_stream_with_options(
9560 std::io::Cursor::new(archive.bytes),
9561 &master_key(),
9562 options
9563 )
9564 .unwrap_err(),
9565 FormatError::ReaderUnsupported("terminal tail exceeds configured cap")
9566 );
9567 }
9568
9569 #[test]
9570 fn live_non_seekable_verify_rejects_metadata_above_retention_cap() {
9571 let archive = write_archive(
9572 &[RegularFile::new("metadata-cap.txt", b"payload")],
9573 &master_key(),
9574 single_stream_options(),
9575 )
9576 .unwrap();
9577 let options = NonSeekableReaderOptions {
9578 max_retained_metadata_bytes: 1,
9579 ..NonSeekableReaderOptions::default()
9580 };
9581
9582 assert_eq!(
9583 verify_non_seekable_stream_with_options(
9584 std::io::Cursor::new(archive.bytes),
9585 &master_key(),
9586 options
9587 )
9588 .unwrap_err(),
9589 FormatError::ReaderUnsupported("retained metadata exceeds configured streaming cap")
9590 );
9591 }
9592
9593 #[test]
9594 fn live_non_seekable_verify_repairs_crc_failed_metadata_block() {
9595 let archive = write_archive(
9596 &[RegularFile::new("metadata-erasure.txt", b"payload")],
9597 &master_key(),
9598 single_stream_options(),
9599 )
9600 .unwrap();
9601 let mut corrupted = archive.bytes;
9602 let slot = first_block_record_slot_with_kind(&corrupted, BlockKind::IndexRootData).unwrap();
9603 corrupt_block_record_payload_at_slot(&mut corrupted, slot);
9604
9605 let report =
9606 verify_non_seekable_stream(std::io::Cursor::new(corrupted), &master_key()).unwrap();
9607
9608 assert_eq!(report.file_count, 1);
9609 }
9610
9611 #[test]
9612 fn live_non_seekable_verify_rejects_member_count_above_cap() {
9613 let archive = write_archive(
9614 &[RegularFile::new("member-cap.txt", b"payload")],
9615 &master_key(),
9616 single_stream_options(),
9617 )
9618 .unwrap();
9619 let options = NonSeekableReaderOptions {
9620 max_streamed_member_count: 0,
9621 ..NonSeekableReaderOptions::default()
9622 };
9623
9624 assert_eq!(
9625 verify_non_seekable_stream_with_options(
9626 std::io::Cursor::new(archive.bytes),
9627 &master_key(),
9628 options
9629 )
9630 .unwrap_err(),
9631 FormatError::ReaderUnsupported("tar member count exceeds configured streaming cap")
9632 );
9633 }
9634
9635 #[test]
9636 fn live_non_seekable_verify_rejects_total_extraction_cap_during_decode() {
9637 let archive = write_archive(
9638 &[RegularFile::new("live-total-cap.txt", b"payload")],
9639 &master_key(),
9640 single_stream_options(),
9641 )
9642 .unwrap();
9643 let mut options = NonSeekableReaderOptions::default();
9644 options.reader.max_total_extraction_size = 3;
9645
9646 assert_eq!(
9647 verify_non_seekable_stream_with_options(
9648 std::io::Cursor::new(archive.bytes),
9649 &master_key(),
9650 options
9651 )
9652 .unwrap_err(),
9653 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
9654 );
9655 }
9656
9657 #[test]
9658 fn live_non_seekable_verify_reports_root_auth_wire_only() {
9659 let archive = write_archive_with_root_auth(
9660 &[RegularFile::new("signed-live.txt", b"root-auth stream")],
9661 &master_key(),
9662 single_stream_options(),
9663 test_root_auth_config(),
9664 |request| Ok(test_root_auth_value(request)),
9665 )
9666 .unwrap();
9667
9668 let report =
9669 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
9670
9671 assert_eq!(report.root_auth, SequentialRootAuthStatus::WireValidOnly);
9672 }
9673
9674 #[test]
9675 fn live_non_seekable_extract_stream_commits_after_terminal_verify() {
9676 let archive = write_archive(
9677 &[
9678 RegularFile::new("alpha.txt", b"alpha"),
9679 RegularFile::new("nested/beta.txt", b"beta"),
9680 ],
9681 &master_key(),
9682 single_stream_options(),
9683 )
9684 .unwrap();
9685 let tmp = tempfile::tempdir().unwrap();
9686 let out = tmp.path().join("out");
9687
9688 let report = extract_non_seekable_stream_to_dir(
9689 std::io::Cursor::new(archive.bytes),
9690 &master_key(),
9691 &out,
9692 NonSeekableReaderOptions::default(),
9693 SafeExtractionOptions::default(),
9694 )
9695 .unwrap();
9696
9697 assert_eq!(report.verification.file_count, 2);
9698 assert_eq!(report.extracted_member_count, 2);
9699 assert_eq!(fs::read(out.join("alpha.txt")).unwrap(), b"alpha");
9700 assert_eq!(fs::read(out.join("nested/beta.txt")).unwrap(), b"beta");
9701 }
9702
9703 #[test]
9704 fn live_non_seekable_extract_stream_accepts_tiny_read_chunks() {
9705 let archive = write_archive(
9706 &[RegularFile::new("tiny-extract.txt", b"chunked extraction")],
9707 &master_key(),
9708 single_stream_options(),
9709 )
9710 .unwrap();
9711 let tmp = tempfile::tempdir().unwrap();
9712 let out = tmp.path().join("out");
9713
9714 extract_non_seekable_stream_to_dir(
9715 ChunkedReader::new(archive.bytes, 1),
9716 &master_key(),
9717 &out,
9718 NonSeekableReaderOptions::default(),
9719 SafeExtractionOptions::default(),
9720 )
9721 .unwrap();
9722
9723 assert_eq!(
9724 fs::read(out.join("tiny-extract.txt")).unwrap(),
9725 b"chunked extraction"
9726 );
9727 }
9728
9729 #[test]
9730 fn live_non_seekable_extract_stream_terminal_failure_leaves_no_final_output() {
9731 let archive = write_archive(
9732 &[RegularFile::new("late-fail.txt", b"must remain staged")],
9733 &master_key(),
9734 single_stream_options(),
9735 )
9736 .unwrap();
9737 let mut corrupted = archive.bytes;
9738 corrupt_v41_terminal_recovery(&mut corrupted);
9739 let tmp = tempfile::tempdir().unwrap();
9740 let out = tmp.path().join("out");
9741
9742 match extract_non_seekable_stream_to_dir(
9743 std::io::Cursor::new(corrupted),
9744 &master_key(),
9745 &out,
9746 NonSeekableReaderOptions::default(),
9747 SafeExtractionOptions::default(),
9748 )
9749 .unwrap_err()
9750 {
9751 ExtractError::Format(err) => assert_eq!(
9752 err,
9753 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
9754 ),
9755 ExtractError::Output(err) => panic!("unexpected output error: {err}"),
9756 }
9757 assert!(!out.exists());
9758 }
9759
9760 #[test]
9761 fn live_non_seekable_extract_stream_existing_destination_obeys_overwrite_policy() {
9762 let archive = write_archive(
9763 &[RegularFile::new("same.txt", b"new")],
9764 &master_key(),
9765 single_stream_options(),
9766 )
9767 .unwrap();
9768 let tmp = tempfile::tempdir().unwrap();
9769 let out = tmp.path().join("out");
9770 fs::create_dir(&out).unwrap();
9771 fs::write(out.join("same.txt"), b"old").unwrap();
9772
9773 match extract_non_seekable_stream_to_dir(
9774 std::io::Cursor::new(archive.bytes.clone()),
9775 &master_key(),
9776 &out,
9777 NonSeekableReaderOptions::default(),
9778 SafeExtractionOptions::default(),
9779 )
9780 .unwrap_err()
9781 {
9782 ExtractError::Format(err) => assert_eq!(err, FormatError::UnsafeOverwrite),
9783 ExtractError::Output(err) => panic!("unexpected output error: {err}"),
9784 }
9785 assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"old");
9786
9787 extract_non_seekable_stream_to_dir(
9788 std::io::Cursor::new(archive.bytes),
9789 &master_key(),
9790 &out,
9791 NonSeekableReaderOptions::default(),
9792 SafeExtractionOptions {
9793 overwrite_existing: true,
9794 },
9795 )
9796 .unwrap();
9797 assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"new");
9798 }
9799
9800 #[test]
9801 fn live_non_seekable_list_stream_matches_seekable_final_view() {
9802 let archive = write_archive(
9803 &[
9804 RegularFile::new("a.txt", b"a"),
9805 RegularFile::new("b.txt", b"bb"),
9806 ],
9807 &master_key(),
9808 single_stream_options(),
9809 )
9810 .unwrap();
9811 let seekable = open_archive(&archive.bytes, &master_key()).unwrap();
9812 let expected = seekable.list_files().unwrap();
9813
9814 let report = list_non_seekable_stream(
9815 std::io::Cursor::new(archive.bytes),
9816 &master_key(),
9817 NonSeekableReaderOptions::default(),
9818 )
9819 .unwrap();
9820
9821 assert_eq!(report.verification.file_count, 2);
9822 assert_eq!(report.entries, expected);
9823 }
9824
9825 #[test]
9826 fn bootstrap_sidecar_rejects_bad_flags_and_trailing_bytes() {
9827 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9828 let mut bad_flags = archive.bootstrap_sidecar.clone();
9829 rewrite_sidecar_header(&mut bad_flags, &master_key(), |header| {
9830 header.flags |= 0x08;
9831 });
9832 assert_eq!(
9833 open_archive_with_bootstrap_sidecar(&archive.bytes, &bad_flags, &master_key())
9834 .unwrap_err(),
9835 FormatError::UnknownBootstrapSidecarFlags(0x0b)
9836 );
9837
9838 let mut trailing = archive.bootstrap_sidecar.clone();
9839 trailing.push(0);
9840 assert_eq!(
9841 open_archive_with_bootstrap_sidecar(&archive.bytes, &trailing, &master_key())
9842 .unwrap_err(),
9843 FormatError::NonCanonicalBootstrapSidecarLayout
9844 );
9845 }
9846
9847 #[test]
9848 fn bootstrap_sidecar_rejects_bad_manifest_footer_semantics() {
9849 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9850 let mut wrong_volume = archive.bootstrap_sidecar.clone();
9851 mutate_sidecar_manifest(&mut wrong_volume, &master_key(), |footer| {
9852 footer.volume_index = 1;
9853 });
9854 assert_eq!(
9855 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_volume, &master_key())
9856 .unwrap_err(),
9857 FormatError::InvalidArchive("sidecar ManifestFooter volume_index must be zero")
9858 );
9859
9860 let mut non_authoritative = archive.bootstrap_sidecar.clone();
9861 mutate_sidecar_manifest(&mut non_authoritative, &master_key(), |footer| {
9862 footer.is_authoritative = 0;
9863 });
9864 assert_eq!(
9865 open_archive_with_bootstrap_sidecar(&archive.bytes, &non_authoritative, &master_key())
9866 .unwrap_err(),
9867 FormatError::InvalidArchive("sidecar ManifestFooter is not authoritative")
9868 );
9869 }
9870
9871 #[test]
9872 fn sidecar_manifest_validation_does_not_compare_opened_volume_index() {
9873 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9874 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
9875 let crypto_start = volume_header.crypto_header_offset as usize;
9876 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
9877 let crypto_header = CryptoHeader::parse(
9878 &archive.bytes[crypto_start..crypto_end],
9879 volume_header.crypto_header_length,
9880 )
9881 .unwrap();
9882 let subkeys = Subkeys::derive(
9883 &master_key(),
9884 &volume_header.archive_uuid,
9885 &volume_header.session_id,
9886 )
9887 .unwrap();
9888 let mut opened_header = volume_header;
9889 opened_header.volume_index = 1;
9890
9891 let parsed = parse_bootstrap_sidecar(
9892 &archive.bootstrap_sidecar,
9893 &opened_header,
9894 &crypto_header.fixed,
9895 &subkeys,
9896 )
9897 .unwrap();
9898
9899 assert_eq!(parsed.manifest_footer.unwrap().volume_index, 0);
9900 }
9901
9902 #[test]
9903 fn bootstrap_sidecar_rejects_conflicting_manifest_bootstrap_fields() {
9904 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9905 let mut conflicting = archive.bootstrap_sidecar.clone();
9906 mutate_sidecar_manifest(&mut conflicting, &master_key(), |footer| {
9907 footer.index_root_first_block += 1;
9908 });
9909
9910 assert_eq!(
9911 open_archive_with_bootstrap_sidecar(&archive.bytes, &conflicting, &master_key())
9912 .unwrap_err(),
9913 FormatError::InvalidArchive("bootstrap sidecar conflicts with terminal ManifestFooter")
9914 );
9915 }
9916
9917 #[test]
9918 fn sidecar_size_cap_counts_only_present_sparse_sections() {
9919 let mut crypto_header = test_crypto_header();
9920 crypto_header.has_dictionary = 1;
9921 crypto_header.index_root_fec_data_shards = 1;
9922 crypto_header.index_root_fec_parity_shards = 0;
9923 let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
9924 let header = BootstrapSidecarHeader {
9925 archive_uuid: [0x31; 16],
9926 session_id: [0x42; 16],
9927 flags: 0x04,
9928 manifest_footer_offset: 0,
9929 manifest_footer_length: 0,
9930 index_root_records_offset: 0,
9931 index_root_records_length: 0,
9932 dictionary_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
9933 dictionary_records_length: record_len,
9934 sidecar_hmac: [0u8; 32],
9935 header_crc32c: 0,
9936 };
9937
9938 validate_sidecar_size_cap(
9939 &header,
9940 &crypto_header,
9941 BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len,
9942 )
9943 .unwrap();
9944 assert_eq!(
9945 validate_sidecar_size_cap(
9946 &header,
9947 &crypto_header,
9948 BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len + 1,
9949 )
9950 .unwrap_err(),
9951 FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
9952 );
9953 }
9954
9955 #[test]
9956 fn sidecar_size_cap_rejects_sparse_section_above_class_max() {
9957 let mut crypto_header = test_crypto_header();
9958 crypto_header.index_root_fec_data_shards = 1;
9959 crypto_header.index_root_fec_parity_shards = 0;
9960 let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
9961 let header = BootstrapSidecarHeader {
9962 archive_uuid: [0x31; 16],
9963 session_id: [0x42; 16],
9964 flags: 0x02,
9965 manifest_footer_offset: 0,
9966 manifest_footer_length: 0,
9967 index_root_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
9968 index_root_records_length: record_len * 2,
9969 dictionary_records_offset: 0,
9970 dictionary_records_length: 0,
9971 sidecar_hmac: [0u8; 32],
9972 header_crc32c: 0,
9973 };
9974
9975 assert_eq!(
9976 validate_sidecar_size_cap(
9977 &header,
9978 &crypto_header,
9979 BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len * 2,
9980 )
9981 .unwrap_err(),
9982 FormatError::InvalidArchive("bootstrap sidecar IndexRoot records exceed resource cap")
9983 );
9984 }
9985
9986 #[test]
9987 fn sidecar_size_cap_uses_wide_arithmetic_for_large_record_classes() {
9988 let mut crypto_header = test_crypto_header();
9989 crypto_header.block_size = u32::MAX;
9990 crypto_header.index_root_fec_data_shards = u16::MAX;
9991 crypto_header.index_root_fec_parity_shards = u16::MAX;
9992 let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
9993 let max_records = crypto_header.index_root_fec_data_shards as u64
9994 + crypto_header.index_root_fec_parity_shards as u64;
9995 let max_section_len = max_records * record_len;
9996 let cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64
9997 + MANIFEST_FOOTER_LEN as u64
9998 + max_section_len
9999 + max_section_len;
10000 let header = BootstrapSidecarHeader {
10001 archive_uuid: [0x31; 16],
10002 session_id: [0x42; 16],
10003 flags: 0x01 | 0x02 | 0x04,
10004 manifest_footer_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
10005 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
10006 index_root_records_offset: 0,
10007 index_root_records_length: max_section_len,
10008 dictionary_records_offset: 0,
10009 dictionary_records_length: max_section_len,
10010 sidecar_hmac: [0u8; 32],
10011 header_crc32c: 0,
10012 };
10013
10014 validate_sidecar_size_cap(&header, &crypto_header, cap).unwrap();
10015 assert_eq!(
10016 validate_sidecar_size_cap(&header, &crypto_header, cap + 1).unwrap_err(),
10017 FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
10018 );
10019 }
10020
10021 #[test]
10022 fn bootstrap_sidecar_rejects_dictionary_section_for_no_dictionary_archive() {
10023 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10024 let mut with_dictionary = archive.bootstrap_sidecar.clone();
10025 let header =
10026 BootstrapSidecarHeader::parse(&with_dictionary[..BOOTSTRAP_SIDECAR_HEADER_LEN])
10027 .unwrap();
10028 let record_len = sidecar_record_len(&with_dictionary);
10029 let first_record = header.index_root_records_offset as usize;
10030 let copied_record = with_dictionary[first_record..first_record + record_len].to_vec();
10031 let dictionary_offset = with_dictionary.len() as u64;
10032 with_dictionary.extend_from_slice(&copied_record);
10033 rewrite_sidecar_header(&mut with_dictionary, &master_key(), |header| {
10034 header.flags |= 0x04;
10035 header.dictionary_records_offset = dictionary_offset;
10036 header.dictionary_records_length = record_len as u64;
10037 });
10038
10039 assert_eq!(
10040 open_archive_with_bootstrap_sidecar(&archive.bytes, &with_dictionary, &master_key())
10041 .unwrap_err(),
10042 FormatError::InvalidArchive(
10043 "bootstrap sidecar has dictionary records while has_dictionary is false"
10044 )
10045 );
10046 }
10047
10048 #[test]
10049 fn bootstrap_sidecar_rejects_missing_duplicate_wrong_kind_and_wrong_last_flag() {
10050 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10051 let mut missing = archive.bootstrap_sidecar.clone();
10052 let record_len = sidecar_record_len(&missing);
10053 let new_len = missing.len() - record_len;
10054 missing.truncate(new_len);
10055 rewrite_sidecar_header(&mut missing, &master_key(), |header| {
10056 header.index_root_records_length -= record_len as u64;
10057 });
10058 assert_eq!(
10059 open_archive_with_bootstrap_sidecar(&archive.bytes, &missing, &master_key())
10060 .unwrap_err(),
10061 FormatError::InvalidArchive(
10062 "sidecar BlockRecord section does not match declared extent"
10063 )
10064 );
10065
10066 let mut duplicate = archive.bootstrap_sidecar.clone();
10067 mutate_sidecar_index_record(&mut duplicate, 1, |record| {
10068 record.block_index -= 1;
10069 });
10070 assert_eq!(
10071 open_archive_with_bootstrap_sidecar(&archive.bytes, &duplicate, &master_key())
10072 .unwrap_err(),
10073 FormatError::InvalidArchive(
10074 "sidecar BlockRecord section has missing or duplicate blocks"
10075 )
10076 );
10077
10078 let mut misordered = archive.bootstrap_sidecar.clone();
10079 swap_sidecar_index_records(&mut misordered, 0, 1);
10080 assert_eq!(
10081 open_archive_with_bootstrap_sidecar(&archive.bytes, &misordered, &master_key())
10082 .unwrap_err(),
10083 FormatError::InvalidArchive(
10084 "sidecar BlockRecord section has missing or duplicate blocks"
10085 )
10086 );
10087
10088 let mut wrong_kind = archive.bootstrap_sidecar.clone();
10089 mutate_sidecar_index_record(&mut wrong_kind, 0, |record| {
10090 record.kind = BlockKind::PayloadData;
10091 });
10092 assert_eq!(
10093 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
10094 .unwrap_err(),
10095 FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
10096 );
10097
10098 let mut wrong_last = archive.bootstrap_sidecar.clone();
10099 mutate_sidecar_index_record(&mut wrong_last, 0, |record| {
10100 record.flags = 0;
10101 });
10102 assert_eq!(
10103 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
10104 .unwrap_err(),
10105 FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
10106 );
10107 }
10108
10109 #[test]
10110 fn verify_helper_rejects_envelope_frame_coverage_gap() {
10111 let frames = BTreeMap::from([(
10112 0,
10113 FrameEntry {
10114 frame_index: 0,
10115 envelope_index: 0,
10116 offset_in_envelope: 0,
10117 compressed_size: 10,
10118 decompressed_size: 512,
10119 flags: 0,
10120 tar_stream_offset: 0,
10121 },
10122 )]);
10123 let envelopes = BTreeMap::from([(
10124 0,
10125 EnvelopeEntry {
10126 envelope_index: 0,
10127 first_block_index: 0,
10128 data_block_count: 1,
10129 parity_block_count: 1,
10130 encrypted_size: 4096,
10131 plaintext_size: 11,
10132 first_frame_index: 0,
10133 frame_count: 1,
10134 },
10135 )]);
10136
10137 assert_eq!(
10138 validate_envelope_frame_coverage(&frames, &envelopes).unwrap_err(),
10139 FormatError::InvalidArchive("EnvelopeEntry frame coverage has a gap or overlap")
10140 );
10141 }
10142
10143 #[test]
10144 fn verify_helper_rejects_file_extent_gaps_and_overlaps() {
10145 assert!(validate_file_extent_coverage_ranges(&[(512, 512), (0, 512)], 1024).is_ok());
10146 assert_eq!(
10147 validate_file_extent_coverage_ranges(&[(0, 512), (1024, 512)], 1536).unwrap_err(),
10148 FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
10149 );
10150 assert_eq!(
10151 validate_file_extent_coverage_ranges(&[(0, 1024), (512, 512)], 1024).unwrap_err(),
10152 FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
10153 );
10154 }
10155
10156 #[test]
10157 fn verify_rejects_authenticated_content_hash_mismatch() {
10158 let options = WriterOptions {
10159 index_root_fec_parity_shards: 0,
10160 ..single_stream_options()
10161 };
10162 let archive = write_archive(
10163 &[RegularFile::new("content-hash.txt", b"hash covered")],
10164 &master_key(),
10165 options,
10166 )
10167 .unwrap();
10168 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10169
10170 let mut root = opened.index_root.clone();
10171 root.header.content_sha256 = [0xa5; 32];
10172 let root_plaintext = root.to_bytes();
10173 IndexRoot::parse(
10174 &root_plaintext,
10175 false,
10176 metadata_limits(&opened.crypto_header),
10177 )
10178 .unwrap();
10179 assert_eq!(
10180 root_plaintext.len() as u32,
10181 opened.manifest_footer.index_root_decompressed_size
10182 );
10183
10184 let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
10185 let mut next_block_index = opened.manifest_footer.index_root_first_block;
10186 let replacement = encrypt_test_object(
10187 &compressed_root,
10188 &opened.subkeys.index_root_key,
10189 &opened.subkeys.index_nonce_seed,
10190 b"idxroot",
10191 0,
10192 BlockKind::IndexRootData,
10193 &mut next_block_index,
10194 &opened.crypto_header,
10195 &opened.volume_header,
10196 );
10197 assert_eq!(
10198 replacement.extent.data_block_count,
10199 opened.manifest_footer.index_root_data_block_count
10200 );
10201 assert_eq!(
10202 replacement.extent.encrypted_size,
10203 opened.manifest_footer.index_root_encrypted_size
10204 );
10205
10206 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
10207 let crypto_end = volume_header.crypto_header_offset as usize
10208 + volume_header.crypto_header_length as usize;
10209 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
10210 let mut malformed = archive.bytes.clone();
10211 for record in replacement.records {
10212 let offset = crypto_end + record.block_index as usize * record_len;
10213 malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
10214 }
10215
10216 let reopened = open_archive(&malformed, &master_key()).unwrap();
10217 assert_eq!(
10218 reopened.verify().unwrap_err(),
10219 FormatError::InvalidArchive(
10220 "IndexRoot content_sha256 does not match decoded tar stream"
10221 )
10222 );
10223 }
10224
10225 #[test]
10226 fn verify_rejects_file_entry_tar_path_and_size_mismatches() {
10227 let (mut path_mismatch, _) = multi_envelope_reader_fixture();
10228 rewrite_as_single_healthy_file(&mut path_mismatch, |_file, path| {
10229 path[0] = b'x';
10230 });
10231 assert_eq!(
10232 path_mismatch.verify().unwrap_err(),
10233 FormatError::InvalidArchive("tar member path does not match FileEntry path")
10234 );
10235
10236 let (mut size_mismatch, _) = multi_envelope_reader_fixture();
10237 rewrite_as_single_healthy_file(&mut size_mismatch, |file, _path| {
10238 file.file_data_size += 1;
10239 });
10240 assert_eq!(
10241 size_mismatch.verify().unwrap_err(),
10242 FormatError::InvalidArchive("tar member size does not match FileEntry file_data_size")
10243 );
10244 }
10245
10246 #[test]
10247 fn verify_rejects_inconsistent_duplicate_local_frame_rows_across_shards() {
10248 let (mut opened, _) = multi_envelope_reader_fixture();
10249 let locating = opened.index_root.shards[0].clone();
10250 let mut duplicate = opened.load_index_shard(&locating).unwrap();
10251 duplicate.header.shard_index = 1;
10252 duplicate.frames[0].flags ^= 0x0000_0001;
10253 let duplicate_plaintext = duplicate.to_bytes();
10254 let mut next_block_index = opened
10255 .blocks
10256 .keys()
10257 .last()
10258 .copied()
10259 .map(|index| index + 1)
10260 .unwrap_or(0);
10261 let duplicate_object = encrypt_test_object(
10262 &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
10263 &opened.subkeys.index_shard_key,
10264 &opened.subkeys.index_nonce_seed,
10265 b"idxshard",
10266 1,
10267 BlockKind::IndexShardData,
10268 &mut next_block_index,
10269 &opened.crypto_header,
10270 &opened.volume_header,
10271 );
10272 insert_records(&mut opened.blocks, &duplicate_object.records);
10273 opened.index_root.shards.push(ShardEntry {
10274 shard_index: 1,
10275 first_block_index: duplicate_object.extent.first_block_index,
10276 data_block_count: duplicate_object.extent.data_block_count,
10277 parity_block_count: 0,
10278 encrypted_size: duplicate_object.extent.encrypted_size,
10279 decompressed_size: duplicate_plaintext.len() as u32,
10280 file_count: locating.file_count,
10281 first_path_hash: locating.first_path_hash,
10282 last_path_hash: locating.last_path_hash,
10283 });
10284 opened.index_root.header.file_count += locating.file_count as u64;
10285
10286 assert_eq!(
10287 opened.verify().unwrap_err(),
10288 FormatError::InvalidArchive("duplicate FrameEntry rows do not match")
10289 );
10290 }
10291
10292 #[test]
10293 fn verify_rejects_inconsistent_duplicate_local_envelope_rows_across_shards() {
10294 let (mut opened, _) = multi_envelope_reader_fixture();
10295 let locating = opened.index_root.shards[0].clone();
10296 let mut duplicate = opened.load_index_shard(&locating).unwrap();
10297 duplicate.header.shard_index = 1;
10298 duplicate.envelopes[0].first_block_index += 1;
10299 let duplicate_plaintext = duplicate.to_bytes();
10300 let mut next_block_index = opened
10301 .blocks
10302 .keys()
10303 .last()
10304 .copied()
10305 .map(|index| index + 1)
10306 .unwrap_or(0);
10307 let duplicate_object = encrypt_test_object(
10308 &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
10309 &opened.subkeys.index_shard_key,
10310 &opened.subkeys.index_nonce_seed,
10311 b"idxshard",
10312 1,
10313 BlockKind::IndexShardData,
10314 &mut next_block_index,
10315 &opened.crypto_header,
10316 &opened.volume_header,
10317 );
10318 insert_records(&mut opened.blocks, &duplicate_object.records);
10319 opened.index_root.shards.push(ShardEntry {
10320 shard_index: 1,
10321 first_block_index: duplicate_object.extent.first_block_index,
10322 data_block_count: duplicate_object.extent.data_block_count,
10323 parity_block_count: 0,
10324 encrypted_size: duplicate_object.extent.encrypted_size,
10325 decompressed_size: duplicate_plaintext.len() as u32,
10326 file_count: locating.file_count,
10327 first_path_hash: locating.first_path_hash,
10328 last_path_hash: locating.last_path_hash,
10329 });
10330 opened.index_root.header.file_count += locating.file_count as u64;
10331
10332 assert_eq!(
10333 opened.verify().unwrap_err(),
10334 FormatError::InvalidArchive("duplicate EnvelopeEntry rows do not match")
10335 );
10336 }
10337
10338 #[test]
10339 fn verify_rejects_non_contiguous_global_envelope_indexes() {
10340 let (mut opened, _) = multi_envelope_reader_fixture();
10341 replace_first_index_shard(&mut opened, |shard| {
10342 let frame = shard
10343 .frames
10344 .iter_mut()
10345 .find(|entry| entry.frame_index == 1)
10346 .unwrap();
10347 frame.envelope_index = 2;
10348
10349 let envelope = shard
10350 .envelopes
10351 .iter_mut()
10352 .find(|entry| entry.envelope_index == 1)
10353 .unwrap();
10354 envelope.envelope_index = 2;
10355 });
10356
10357 assert_eq!(
10358 opened.verify().unwrap_err(),
10359 FormatError::InvalidMetadata {
10360 structure: "EnvelopeEntry",
10361 reason: "global index coverage has a gap",
10362 }
10363 );
10364 }
10365
10366 #[test]
10367 fn verify_rejects_payload_object_extent_overlap() {
10368 let (mut opened, _) = multi_envelope_reader_fixture();
10369 replace_first_index_shard(&mut opened, |shard| {
10370 let first_block_index = shard.envelopes[0].first_block_index;
10371 shard.envelopes[1].first_block_index = first_block_index;
10372 });
10373
10374 assert_eq!(
10375 opened.verify().unwrap_err(),
10376 FormatError::InvalidArchive("encrypted object block ranges overlap")
10377 );
10378 }
10379
10380 #[test]
10381 fn verify_accepts_cross_shard_shared_envelope_frame_union() {
10382 let volume_header = test_volume_header();
10383 let crypto_header = test_crypto_header();
10384 let subkeys = Subkeys::derive(
10385 &master_key(),
10386 &volume_header.archive_uuid,
10387 &volume_header.session_id,
10388 )
10389 .unwrap();
10390 let mut next_block_index = 0u64;
10391 let mut blocks = BTreeMap::new();
10392
10393 let alpha = test_member(b"alpha.txt", b"alpha cross shard\n");
10394 let beta = test_member(b"beta.txt", b"beta cross shard\n");
10395 let tar_stream = [alpha.as_slice(), beta.as_slice()].concat();
10396 let frame0_plaintext = compress_zstd_frame(&alpha, 1).unwrap();
10397 let frame1_plaintext = compress_zstd_frame(&beta, 1).unwrap();
10398 let envelope_plaintext =
10399 [frame0_plaintext.as_slice(), frame1_plaintext.as_slice()].concat();
10400 let payload = encrypt_test_object(
10401 &envelope_plaintext,
10402 &subkeys.enc_key,
10403 &subkeys.nonce_seed,
10404 b"envelope",
10405 0,
10406 BlockKind::PayloadData,
10407 &mut next_block_index,
10408 &crypto_header,
10409 &volume_header,
10410 );
10411 insert_records(&mut blocks, &payload.records);
10412
10413 let envelope = EnvelopeEntry {
10414 envelope_index: 0,
10415 first_block_index: payload.extent.first_block_index,
10416 data_block_count: payload.extent.data_block_count,
10417 parity_block_count: 0,
10418 encrypted_size: payload.extent.encrypted_size,
10419 plaintext_size: envelope_plaintext.len() as u32,
10420 first_frame_index: 0,
10421 frame_count: 2,
10422 };
10423 let frame0 = FrameEntry {
10424 frame_index: 0,
10425 envelope_index: 0,
10426 offset_in_envelope: 0,
10427 compressed_size: frame0_plaintext.len() as u32,
10428 decompressed_size: alpha.len() as u32,
10429 flags: 0x0000_0003,
10430 tar_stream_offset: 0,
10431 };
10432 let frame1 = FrameEntry {
10433 frame_index: 1,
10434 envelope_index: 0,
10435 offset_in_envelope: frame0_plaintext.len() as u32,
10436 compressed_size: frame1_plaintext.len() as u32,
10437 decompressed_size: beta.len() as u32,
10438 flags: 0x0000_0003,
10439 tar_stream_offset: alpha.len() as u64,
10440 };
10441
10442 let (shard0_plaintext, first0, last0) = build_test_index_shard(
10443 &[TestFileMeta {
10444 path: b"alpha.txt".to_vec(),
10445 frame_index: 0,
10446 tar_stream_offset: 0,
10447 member_group_size: alpha.len() as u64,
10448 file_data_size: b"alpha cross shard\n".len() as u64,
10449 }],
10450 &[frame0],
10451 std::slice::from_ref(&envelope),
10452 );
10453 let (mut shard1_plaintext, first1, last1) = build_test_index_shard(
10454 &[TestFileMeta {
10455 path: b"beta.txt".to_vec(),
10456 frame_index: 1,
10457 tar_stream_offset: alpha.len() as u64,
10458 member_group_size: beta.len() as u64,
10459 file_data_size: b"beta cross shard\n".len() as u64,
10460 }],
10461 &[frame1],
10462 std::slice::from_ref(&envelope),
10463 );
10464 shard1_plaintext[8..16].copy_from_slice(&1u64.to_le_bytes());
10465
10466 let shard0 = encrypt_test_object(
10467 &compress_zstd_frame(&shard0_plaintext, 1).unwrap(),
10468 &subkeys.index_shard_key,
10469 &subkeys.index_nonce_seed,
10470 b"idxshard",
10471 0,
10472 BlockKind::IndexShardData,
10473 &mut next_block_index,
10474 &crypto_header,
10475 &volume_header,
10476 );
10477 let shard1 = encrypt_test_object(
10478 &compress_zstd_frame(&shard1_plaintext, 1).unwrap(),
10479 &subkeys.index_shard_key,
10480 &subkeys.index_nonce_seed,
10481 b"idxshard",
10482 1,
10483 BlockKind::IndexShardData,
10484 &mut next_block_index,
10485 &crypto_header,
10486 &volume_header,
10487 );
10488 insert_records(&mut blocks, &shard0.records);
10489 insert_records(&mut blocks, &shard1.records);
10490
10491 let index_root = IndexRoot {
10492 header: IndexRootHeader {
10493 frame_count: 2,
10494 envelope_count: 1,
10495 file_count: 2,
10496 payload_block_count: payload.extent.data_block_count as u64,
10497 tar_total_size: tar_stream.len() as u64,
10498 content_sha256: sha256_bytes(&tar_stream),
10499 ..IndexRootHeader::empty()
10500 },
10501 shards: vec![
10502 ShardEntry {
10503 shard_index: 0,
10504 first_block_index: shard0.extent.first_block_index,
10505 data_block_count: shard0.extent.data_block_count,
10506 parity_block_count: 0,
10507 encrypted_size: shard0.extent.encrypted_size,
10508 decompressed_size: shard0_plaintext.len() as u32,
10509 file_count: 1,
10510 first_path_hash: first0,
10511 last_path_hash: last0,
10512 },
10513 ShardEntry {
10514 shard_index: 1,
10515 first_block_index: shard1.extent.first_block_index,
10516 data_block_count: shard1.extent.data_block_count,
10517 parity_block_count: 0,
10518 encrypted_size: shard1.extent.encrypted_size,
10519 decompressed_size: shard1_plaintext.len() as u32,
10520 file_count: 1,
10521 first_path_hash: first1,
10522 last_path_hash: last1,
10523 },
10524 ],
10525 directory_hint_shards: Vec::new(),
10526 };
10527
10528 let index_root_plaintext = index_root.to_bytes();
10529 let index_root_object = encrypt_test_object(
10530 &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
10531 &subkeys.index_root_key,
10532 &subkeys.index_nonce_seed,
10533 b"idxroot",
10534 0,
10535 BlockKind::IndexRootData,
10536 &mut next_block_index,
10537 &crypto_header,
10538 &volume_header,
10539 );
10540 insert_records(&mut blocks, &index_root_object.records);
10541
10542 let archive_uuid = volume_header.archive_uuid;
10543 let session_id = volume_header.session_id;
10544 let opened = OpenedArchive {
10545 options: ReaderOptions::default(),
10546 observed_archive_bytes: 1_000_000,
10547 observed_volume_count: 1,
10548 subkeys,
10549 blocks,
10550 lazy_blocks: None,
10551 crypto_header_bytes: Vec::new(),
10552 volume_header,
10553 crypto_header,
10554 manifest_footer: ManifestFooter {
10555 archive_uuid,
10556 session_id,
10557 volume_index: 0,
10558 is_authoritative: 1,
10559 total_volumes: 1,
10560 index_root_first_block: index_root_object.extent.first_block_index,
10561 index_root_data_block_count: index_root_object.extent.data_block_count,
10562 index_root_parity_block_count: 0,
10563 index_root_encrypted_size: index_root_object.extent.encrypted_size,
10564 index_root_decompressed_size: index_root_plaintext.len() as u32,
10565 manifest_hmac: [0u8; 32],
10566 },
10567 volume_trailer: Some(VolumeTrailer {
10568 archive_uuid,
10569 session_id,
10570 volume_index: 0,
10571 block_count: next_block_index,
10572 bytes_written: 0,
10573 manifest_footer_offset: 0,
10574 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
10575 closed_at_ns: 0,
10576 root_auth_footer_offset: 0,
10577 root_auth_footer_length: 0,
10578 root_auth_flags: 0,
10579 trailer_hmac: [0u8; 32],
10580 }),
10581 root_auth_footer: None,
10582 index_root,
10583 payload_dictionary: None,
10584 };
10585
10586 opened.verify().unwrap();
10587 }
10588
10589 #[test]
10590 fn verify_rejects_authenticated_archive_missing_required_directory_hints() {
10591 let options = WriterOptions {
10592 index_root_fec_parity_shards: 0,
10593 ..single_stream_options()
10594 };
10595 let archive = write_archive(
10596 &[RegularFile::new("only.txt", b"only payload")],
10597 &master_key(),
10598 options,
10599 )
10600 .unwrap();
10601 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10602 assert!(opened.index_root.directory_hint_shards.is_empty());
10603
10604 let mut root = opened.index_root.clone();
10605 root.header.file_count = DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1;
10606 root.shards[0].file_count = (DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1) as u32;
10607 let root_plaintext = root.to_bytes();
10608 IndexRoot::parse(
10609 &root_plaintext,
10610 false,
10611 metadata_limits(&opened.crypto_header),
10612 )
10613 .unwrap();
10614 assert_eq!(
10615 root_plaintext.len() as u32,
10616 opened.manifest_footer.index_root_decompressed_size
10617 );
10618
10619 let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
10620 let mut next_block_index = opened.manifest_footer.index_root_first_block;
10621 let replacement = encrypt_test_object(
10622 &compressed_root,
10623 &opened.subkeys.index_root_key,
10624 &opened.subkeys.index_nonce_seed,
10625 b"idxroot",
10626 0,
10627 BlockKind::IndexRootData,
10628 &mut next_block_index,
10629 &opened.crypto_header,
10630 &opened.volume_header,
10631 );
10632 assert_eq!(
10633 replacement.extent.first_block_index,
10634 opened.manifest_footer.index_root_first_block
10635 );
10636 assert_eq!(
10637 replacement.extent.data_block_count,
10638 opened.manifest_footer.index_root_data_block_count
10639 );
10640 assert_eq!(
10641 replacement.extent.encrypted_size,
10642 opened.manifest_footer.index_root_encrypted_size
10643 );
10644
10645 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
10646 let crypto_end = volume_header.crypto_header_offset as usize
10647 + volume_header.crypto_header_length as usize;
10648 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
10649 let mut malformed = archive.bytes.clone();
10650 for record in replacement.records {
10651 let offset = crypto_end + record.block_index as usize * record_len;
10652 malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
10653 }
10654
10655 let reopened = open_archive(&malformed, &master_key()).unwrap();
10656 assert_eq!(
10657 reopened.index_root.header.file_count,
10658 DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1
10659 );
10660 assert!(reopened.index_root.directory_hint_shards.is_empty());
10661
10662 assert_eq!(
10663 reopened.verify().unwrap_err(),
10664 FormatError::InvalidArchive("IndexRoot file_count requires directory hints")
10665 );
10666 }
10667
10668 #[test]
10669 fn expected_directory_hint_rows_include_ancestors_and_directory_entries() {
10670 let mut map = DirectoryHintMap::new();
10671 add_expected_directory_hint_rows(&mut map, 2, b"foo/bar/baz.txt", TarEntryKind::Regular);
10672 add_expected_directory_hint_rows(&mut map, 4, b"foo/bar", TarEntryKind::Directory);
10673
10674 assert_eq!(map.get(&Vec::new()), Some(&BTreeSet::from([2, 4])));
10675 assert_eq!(map.get(b"foo".as_slice()), Some(&BTreeSet::from([2, 4])));
10676 assert_eq!(
10677 map.get(b"foo/bar".as_slice()),
10678 Some(&BTreeSet::from([2, 4]))
10679 );
10680 assert!(!map.contains_key(b"foo/bar/baz.txt".as_slice()));
10681 assert!(!map.contains_key(b"foobar".as_slice()));
10682 }
10683
10684 #[test]
10685 fn directory_hint_validation_requires_exact_global_map() {
10686 let mut expected = DirectoryHintMap::new();
10687 add_expected_directory_hint_rows(&mut expected, 0, b"foo/bar.txt", TarEntryKind::Regular);
10688 add_expected_directory_hint_rows(&mut expected, 1, b"foo", TarEntryKind::Directory);
10689 let rows = sorted_directory_hint_rows(&expected);
10690 let table = directory_hint_table_from_rows(7, &rows, 2);
10691
10692 validate_directory_hint_tables_against_expected(std::slice::from_ref(&table), &expected)
10693 .unwrap();
10694
10695 let mut missing_root = expected.clone();
10696 missing_root.remove(&Vec::new());
10697 let missing_root_rows = sorted_directory_hint_rows(&missing_root);
10698 let missing_root_table = directory_hint_table_from_rows(8, &missing_root_rows, 2);
10699 assert_eq!(
10700 validate_directory_hint_tables_against_expected(&[missing_root_table], &expected)
10701 .unwrap_err(),
10702 FormatError::InvalidArchive("directory hint map does not match decoded files")
10703 );
10704
10705 let mut expected_missing_directory_entry = expected.clone();
10706 expected_missing_directory_entry
10707 .get_mut(b"foo".as_slice())
10708 .unwrap()
10709 .remove(&1);
10710 assert_eq!(
10711 validate_directory_hint_tables_against_expected(
10712 std::slice::from_ref(&table),
10713 &expected_missing_directory_entry,
10714 )
10715 .unwrap_err(),
10716 FormatError::InvalidArchive("directory hint map does not match decoded files")
10717 );
10718
10719 let mut extra = expected.clone();
10720 extra.insert(b"foo/extra".to_vec(), BTreeSet::from([0]));
10721 let extra_rows = sorted_directory_hint_rows(&extra);
10722 let extra_table = directory_hint_table_from_rows(9, &extra_rows, 2);
10723 assert_eq!(
10724 validate_directory_hint_tables_against_expected(&[extra_table], &expected).unwrap_err(),
10725 FormatError::InvalidArchive("directory hint map does not match decoded files")
10726 );
10727 }
10728
10729 #[test]
10730 fn directory_hint_validation_rejects_global_order_mismatch() {
10731 let mut expected = DirectoryHintMap::new();
10732 expected.insert(Vec::new(), BTreeSet::from([0]));
10733 expected.insert(b"alpha".to_vec(), BTreeSet::from([0]));
10734 let rows = sorted_directory_hint_rows(&expected);
10735 let first = directory_hint_table_from_rows(8, &rows[..1], 1);
10736 let second = directory_hint_table_from_rows(9, &rows[1..], 1);
10737
10738 assert_eq!(
10739 validate_directory_hint_tables_against_expected(&[second, first], &expected)
10740 .unwrap_err(),
10741 FormatError::InvalidArchive("DirectoryHintEntry rows are not globally sorted")
10742 );
10743 }
10744
10745 #[test]
10746 fn object_extent_rejects_parity_above_class_cap() {
10747 let crypto_header = CryptoHeaderFixed {
10748 length: 0,
10749 compression_algo: CompressionAlgo::ZstdFramed,
10750 aead_algo: AeadAlgo::AesGcmSiv256,
10751 fec_algo: FecAlgo::ReedSolomonGF16,
10752 kdf_algo: KdfAlgo::Raw,
10753 chunk_size: 1024,
10754 envelope_target_size: 4096,
10755 block_size: 4096,
10756 fec_data_shards: 1,
10757 fec_parity_shards: 1,
10758 index_fec_data_shards: 1,
10759 index_fec_parity_shards: 1,
10760 index_root_fec_data_shards: 1,
10761 index_root_fec_parity_shards: 1,
10762 stripe_width: 1,
10763 volume_loss_tolerance: 0,
10764 bit_rot_buffer_pct: 0,
10765 has_dictionary: 0,
10766 max_path_length: 4096,
10767 expected_volume_size: 0,
10768 };
10769 let extent = ObjectExtent {
10770 first_block_index: 0,
10771 data_block_count: 1,
10772 parity_block_count: 2,
10773 encrypted_size: 4096,
10774 };
10775
10776 assert_eq!(
10777 validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
10778 FormatError::InvalidArchive("encrypted object exceeds its class parity-shard maximum")
10779 );
10780 }
10781
10782 #[test]
10783 fn object_extent_rejects_parity_below_recoverability_requirement() {
10784 let crypto_header = CryptoHeaderFixed {
10785 length: 0,
10786 compression_algo: CompressionAlgo::ZstdFramed,
10787 aead_algo: AeadAlgo::AesGcmSiv256,
10788 fec_algo: FecAlgo::ReedSolomonGF16,
10789 kdf_algo: KdfAlgo::Raw,
10790 chunk_size: 1024,
10791 envelope_target_size: 4096,
10792 block_size: 4096,
10793 fec_data_shards: 1,
10794 fec_parity_shards: 1,
10795 index_fec_data_shards: 1,
10796 index_fec_parity_shards: 1,
10797 index_root_fec_data_shards: 1,
10798 index_root_fec_parity_shards: 1,
10799 stripe_width: 2,
10800 volume_loss_tolerance: 1,
10801 bit_rot_buffer_pct: 0,
10802 has_dictionary: 0,
10803 max_path_length: 4096,
10804 expected_volume_size: 0,
10805 };
10806 let extent = ObjectExtent {
10807 first_block_index: 0,
10808 data_block_count: 1,
10809 parity_block_count: 0,
10810 encrypted_size: 4096,
10811 };
10812
10813 assert_eq!(
10814 validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
10815 FormatError::InvalidArchive(
10816 "encrypted object parity does not match v41 compute_parity"
10817 )
10818 );
10819 }
10820
10821 #[test]
10822 fn encrypted_object_extent_matrix_rejects_overlaps() {
10823 let (opened, _) = multi_envelope_reader_fixture();
10824 let loaded_shard = opened
10825 .load_index_shard(&opened.index_root.shards[0])
10826 .unwrap();
10827 let base_envelopes = loaded_shard
10828 .envelopes
10829 .iter()
10830 .map(|entry| (entry.envelope_index, entry.clone()))
10831 .collect::<BTreeMap<_, _>>();
10832 let payload_start = loaded_shard.envelopes[0].first_block_index;
10833 let overlap = FormatError::InvalidArchive("encrypted object block ranges overlap");
10834
10835 let mut payload_overlap = base_envelopes.clone();
10836 payload_overlap
10837 .get_mut(&loaded_shard.envelopes[1].envelope_index)
10838 .unwrap()
10839 .first_block_index = payload_start;
10840 assert_eq!(
10841 opened
10842 .validate_encrypted_object_block_ranges(&payload_overlap)
10843 .unwrap_err(),
10844 overlap
10845 );
10846
10847 let mut shard_overlap = opened.clone();
10848 let shard = shard_overlap.index_root.shards[0].clone();
10849 shard_overlap.index_root.shards.push(ShardEntry {
10850 shard_index: 1,
10851 ..shard
10852 });
10853 assert_eq!(
10854 shard_overlap
10855 .validate_encrypted_object_block_ranges(&base_envelopes)
10856 .unwrap_err(),
10857 overlap
10858 );
10859
10860 let mut dictionary_overlap = opened.clone();
10861 dictionary_overlap.crypto_header.has_dictionary = 1;
10862 dictionary_overlap.index_root.header.dictionary_first_block = payload_start;
10863 dictionary_overlap
10864 .index_root
10865 .header
10866 .dictionary_data_block_count = 1;
10867 dictionary_overlap
10868 .index_root
10869 .header
10870 .dictionary_parity_block_count = 0;
10871 dictionary_overlap
10872 .index_root
10873 .header
10874 .dictionary_encrypted_size = 4096;
10875 dictionary_overlap
10876 .index_root
10877 .header
10878 .dictionary_decompressed_size = 128;
10879 assert_eq!(
10880 dictionary_overlap
10881 .validate_encrypted_object_block_ranges(&base_envelopes)
10882 .unwrap_err(),
10883 overlap
10884 );
10885
10886 let mut hint_overlap = opened.clone();
10887 hint_overlap
10888 .index_root
10889 .directory_hint_shards
10890 .push(DirectoryHintShardEntry {
10891 hint_shard_index: 0,
10892 first_dir_hash: [0; 8],
10893 last_dir_hash: [0; 8],
10894 first_block_index: payload_start,
10895 data_block_count: 1,
10896 parity_block_count: 0,
10897 encrypted_size: 4096,
10898 decompressed_size: 128,
10899 entry_count: 1,
10900 });
10901 assert_eq!(
10902 hint_overlap
10903 .validate_encrypted_object_block_ranges(&base_envelopes)
10904 .unwrap_err(),
10905 overlap
10906 );
10907 }
10908
10909 #[test]
10910 fn load_metadata_object_rejects_per_object_zstd_frame_exactness_mutations() {
10911 let volume_header = test_volume_header();
10912 let crypto_header = test_crypto_header();
10913 let subkeys = Subkeys::derive(
10914 &master_key(),
10915 &volume_header.archive_uuid,
10916 &volume_header.session_id,
10917 )
10918 .unwrap();
10919 let mut next_block_index = 0u64;
10920
10921 let index_root_payload = b"index root metadata object";
10922 let index_root_compressed = compress_zstd_frame(index_root_payload, 1).unwrap();
10923 assert_metadata_object_from_compressed(
10924 &{
10925 let mut bytes = index_root_compressed.clone();
10926 bytes.push(0);
10927 bytes
10928 },
10929 index_root_payload.len(),
10930 &subkeys,
10931 &volume_header,
10932 &crypto_header,
10933 &subkeys.index_root_key,
10934 &subkeys.index_nonce_seed,
10935 b"idxroot",
10936 0,
10937 BlockKind::IndexRootData,
10938 BlockKind::IndexRootParity,
10939 crypto_header.index_root_fec_data_shards,
10940 crypto_header.index_root_fec_parity_shards,
10941 &mut next_block_index,
10942 FormatError::TrailingBytesAfterZstdFrame,
10943 );
10944 assert_metadata_object_from_compressed(
10945 &index_root_compressed,
10946 index_root_payload.len() + 1,
10947 &subkeys,
10948 &volume_header,
10949 &crypto_header,
10950 &subkeys.index_root_key,
10951 &subkeys.index_nonce_seed,
10952 b"idxroot",
10953 0,
10954 BlockKind::IndexRootData,
10955 BlockKind::IndexRootParity,
10956 crypto_header.index_root_fec_data_shards,
10957 crypto_header.index_root_fec_parity_shards,
10958 &mut next_block_index,
10959 FormatError::ZstdDecompressedSizeMismatch {
10960 expected: index_root_payload.len() + 1,
10961 actual: index_root_payload.len(),
10962 },
10963 );
10964
10965 let index_shard_payload = b"index shard metadata object";
10966 let index_shard_compressed = compress_zstd_frame(index_shard_payload, 1).unwrap();
10967 assert_metadata_object_from_compressed(
10968 &{
10969 let mut bytes = index_shard_compressed.clone();
10970 bytes.push(0);
10971 bytes
10972 },
10973 index_shard_payload.len(),
10974 &subkeys,
10975 &volume_header,
10976 &crypto_header,
10977 &subkeys.index_shard_key,
10978 &subkeys.index_nonce_seed,
10979 b"idxshard",
10980 1,
10981 BlockKind::IndexShardData,
10982 BlockKind::IndexShardParity,
10983 crypto_header.index_fec_data_shards,
10984 crypto_header.index_fec_parity_shards,
10985 &mut next_block_index,
10986 FormatError::TrailingBytesAfterZstdFrame,
10987 );
10988 assert_metadata_object_from_compressed(
10989 &index_shard_compressed,
10990 index_shard_payload.len() + 1,
10991 &subkeys,
10992 &volume_header,
10993 &crypto_header,
10994 &subkeys.index_shard_key,
10995 &subkeys.index_nonce_seed,
10996 b"idxshard",
10997 1,
10998 BlockKind::IndexShardData,
10999 BlockKind::IndexShardParity,
11000 crypto_header.index_fec_data_shards,
11001 crypto_header.index_fec_parity_shards,
11002 &mut next_block_index,
11003 FormatError::ZstdDecompressedSizeMismatch {
11004 expected: index_shard_payload.len() + 1,
11005 actual: index_shard_payload.len(),
11006 },
11007 );
11008
11009 let directory_hint_payload = b"directory hint metadata object";
11010 let directory_hint_compressed = compress_zstd_frame(directory_hint_payload, 1).unwrap();
11011 assert_metadata_object_from_compressed(
11012 &{
11013 let mut bytes = directory_hint_compressed.clone();
11014 bytes.push(0);
11015 bytes
11016 },
11017 directory_hint_payload.len(),
11018 &subkeys,
11019 &volume_header,
11020 &crypto_header,
11021 &subkeys.dir_hint_key,
11022 &subkeys.index_nonce_seed,
11023 b"dirhint",
11024 0,
11025 BlockKind::DirectoryHintData,
11026 BlockKind::DirectoryHintParity,
11027 crypto_header.index_fec_data_shards,
11028 crypto_header.index_fec_parity_shards,
11029 &mut next_block_index,
11030 FormatError::TrailingBytesAfterZstdFrame,
11031 );
11032 assert_metadata_object_from_compressed(
11033 &directory_hint_compressed,
11034 directory_hint_payload.len() + 1,
11035 &subkeys,
11036 &volume_header,
11037 &crypto_header,
11038 &subkeys.dir_hint_key,
11039 &subkeys.index_nonce_seed,
11040 b"dirhint",
11041 0,
11042 BlockKind::DirectoryHintData,
11043 BlockKind::DirectoryHintParity,
11044 crypto_header.index_fec_data_shards,
11045 crypto_header.index_fec_parity_shards,
11046 &mut next_block_index,
11047 FormatError::ZstdDecompressedSizeMismatch {
11048 expected: directory_hint_payload.len() + 1,
11049 actual: directory_hint_payload.len(),
11050 },
11051 );
11052
11053 let dictionary_payload = b"dictionary metadata object";
11054 let dictionary_compressed = compress_zstd_frame(dictionary_payload, 1).unwrap();
11055 assert_metadata_object_from_compressed(
11056 &{
11057 let mut bytes = dictionary_compressed.clone();
11058 bytes.push(0);
11059 bytes
11060 },
11061 dictionary_payload.len(),
11062 &subkeys,
11063 &volume_header,
11064 &crypto_header,
11065 &subkeys.dictionary_key,
11066 &subkeys.index_nonce_seed,
11067 b"dict",
11068 0,
11069 BlockKind::DictionaryData,
11070 BlockKind::DictionaryParity,
11071 crypto_header.index_root_fec_data_shards,
11072 crypto_header.index_root_fec_parity_shards,
11073 &mut next_block_index,
11074 FormatError::TrailingBytesAfterZstdFrame,
11075 );
11076 assert_metadata_object_from_compressed(
11077 &dictionary_compressed,
11078 dictionary_payload.len() + 1,
11079 &subkeys,
11080 &volume_header,
11081 &crypto_header,
11082 &subkeys.dictionary_key,
11083 &subkeys.index_nonce_seed,
11084 b"dict",
11085 0,
11086 BlockKind::DictionaryData,
11087 BlockKind::DictionaryParity,
11088 crypto_header.index_root_fec_data_shards,
11089 crypto_header.index_root_fec_parity_shards,
11090 &mut next_block_index,
11091 FormatError::ZstdDecompressedSizeMismatch {
11092 expected: dictionary_payload.len() + 1,
11093 actual: dictionary_payload.len(),
11094 },
11095 );
11096 }
11097
11098 #[test]
11099 fn load_metadata_object_extent_rejects_encrypted_size_not_data_block_count_times_block_size() {
11100 let volume_header = test_volume_header();
11101 let crypto_header = test_crypto_header();
11102 let subkeys = Subkeys::derive(
11103 &master_key(),
11104 &volume_header.archive_uuid,
11105 &volume_header.session_id,
11106 )
11107 .unwrap();
11108 let mut next_block_index = 0u64;
11109
11110 let index_root_payload = b"index root metadata object";
11111 let (index_root_extent, index_root_records) = build_metadata_object_from_payload(
11112 index_root_payload,
11113 &subkeys,
11114 &volume_header,
11115 &crypto_header,
11116 &subkeys.index_root_key,
11117 &subkeys.index_nonce_seed,
11118 b"idxroot",
11119 0,
11120 BlockKind::IndexRootData,
11121 &mut next_block_index,
11122 );
11123 let mut index_root_extent = index_root_extent;
11124 index_root_extent.encrypted_size = index_root_extent
11125 .encrypted_size
11126 .saturating_add(crypto_header.block_size);
11127 assert_eq!(
11128 load_metadata_object_from_parts(
11129 &index_root_records,
11130 ObjectLoadContext::index_root(
11131 &volume_header,
11132 &crypto_header,
11133 &subkeys,
11134 index_root_extent,
11135 ),
11136 index_root_payload.len() as u32,
11137 )
11138 .unwrap_err(),
11139 FormatError::InvalidArchive(
11140 "encrypted object size is not data_block_count * block_size"
11141 )
11142 );
11143
11144 let index_shard_payload = b"index shard metadata object";
11145 let (index_shard_extent, index_shard_records) = build_metadata_object_from_payload(
11146 index_shard_payload,
11147 &subkeys,
11148 &volume_header,
11149 &crypto_header,
11150 &subkeys.index_shard_key,
11151 &subkeys.index_nonce_seed,
11152 b"idxshard",
11153 1,
11154 BlockKind::IndexShardData,
11155 &mut next_block_index,
11156 );
11157 let mut index_shard_extent = index_shard_extent;
11158 index_shard_extent.encrypted_size = index_shard_extent
11159 .encrypted_size
11160 .saturating_add(crypto_header.block_size);
11161 assert_eq!(
11162 load_metadata_object_from_parts(
11163 &index_shard_records,
11164 ObjectLoadContext {
11165 volume_header: &volume_header,
11166 crypto_header: &crypto_header,
11167 extent: index_shard_extent,
11168 data_kind: BlockKind::IndexShardData,
11169 parity_kind: BlockKind::IndexShardParity,
11170 key: &subkeys.index_shard_key,
11171 nonce_seed: &subkeys.index_nonce_seed,
11172 domain: b"idxshard",
11173 counter: 1,
11174 class_data_shard_max: crypto_header.index_fec_data_shards,
11175 class_parity_shard_max: crypto_header.index_fec_parity_shards,
11176 },
11177 index_shard_payload.len() as u32,
11178 )
11179 .unwrap_err(),
11180 FormatError::InvalidArchive(
11181 "encrypted object size is not data_block_count * block_size"
11182 )
11183 );
11184
11185 let directory_hint_payload = b"directory hint metadata object";
11186 let (directory_hint_extent, directory_hint_records) = build_metadata_object_from_payload(
11187 directory_hint_payload,
11188 &subkeys,
11189 &volume_header,
11190 &crypto_header,
11191 &subkeys.dir_hint_key,
11192 &subkeys.index_nonce_seed,
11193 b"dirhint",
11194 0,
11195 BlockKind::DirectoryHintData,
11196 &mut next_block_index,
11197 );
11198 let mut directory_hint_extent = directory_hint_extent;
11199 directory_hint_extent.encrypted_size = directory_hint_extent
11200 .encrypted_size
11201 .saturating_add(crypto_header.block_size);
11202 assert_eq!(
11203 load_metadata_object_from_parts(
11204 &directory_hint_records,
11205 ObjectLoadContext {
11206 volume_header: &volume_header,
11207 crypto_header: &crypto_header,
11208 extent: directory_hint_extent,
11209 data_kind: BlockKind::DirectoryHintData,
11210 parity_kind: BlockKind::DirectoryHintParity,
11211 key: &subkeys.dir_hint_key,
11212 nonce_seed: &subkeys.index_nonce_seed,
11213 domain: b"dirhint",
11214 counter: 0,
11215 class_data_shard_max: crypto_header.index_fec_data_shards,
11216 class_parity_shard_max: crypto_header.index_fec_parity_shards,
11217 },
11218 directory_hint_payload.len() as u32,
11219 )
11220 .unwrap_err(),
11221 FormatError::InvalidArchive(
11222 "encrypted object size is not data_block_count * block_size"
11223 )
11224 );
11225
11226 let dictionary_payload = b"dictionary metadata object";
11227 let (dictionary_extent, dictionary_records) = build_metadata_object_from_payload(
11228 dictionary_payload,
11229 &subkeys,
11230 &volume_header,
11231 &crypto_header,
11232 &subkeys.dictionary_key,
11233 &subkeys.index_nonce_seed,
11234 b"dict",
11235 0,
11236 BlockKind::DictionaryData,
11237 &mut next_block_index,
11238 );
11239 let mut dictionary_extent = dictionary_extent;
11240 dictionary_extent.encrypted_size = dictionary_extent
11241 .encrypted_size
11242 .saturating_add(crypto_header.block_size);
11243 assert_eq!(
11244 load_metadata_object_from_parts(
11245 &dictionary_records,
11246 ObjectLoadContext {
11247 volume_header: &volume_header,
11248 crypto_header: &crypto_header,
11249 extent: dictionary_extent,
11250 data_kind: BlockKind::DictionaryData,
11251 parity_kind: BlockKind::DictionaryParity,
11252 key: &subkeys.dictionary_key,
11253 nonce_seed: &subkeys.index_nonce_seed,
11254 domain: b"dict",
11255 counter: 0,
11256 class_data_shard_max: crypto_header.index_root_fec_data_shards,
11257 class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
11258 },
11259 dictionary_payload.len() as u32,
11260 )
11261 .unwrap_err(),
11262 FormatError::InvalidArchive(
11263 "encrypted object size is not data_block_count * block_size"
11264 )
11265 );
11266 }
11267
11268 #[test]
11269 fn opens_complete_multi_volume_archive() {
11270 let files = [RegularFile::new("alpha.txt", b"hello from volume stripes")];
11271 let archive = write_archive(
11272 &files,
11273 &master_key(),
11274 WriterOptions {
11275 stripe_width: 2,
11276 volume_loss_tolerance: 1,
11277 ..single_stream_options()
11278 },
11279 )
11280 .unwrap();
11281 assert_eq!(archive.volumes.len(), 2);
11282
11283 let volume_refs = archive
11284 .volumes
11285 .iter()
11286 .map(Vec::as_slice)
11287 .collect::<Vec<_>>();
11288 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
11289
11290 assert_eq!(opened.volume_header.stripe_width, 2);
11291 assert_eq!(opened.list_files().unwrap()[0].path, "alpha.txt");
11292 assert_eq!(
11293 opened.extract_file("alpha.txt").unwrap(),
11294 Some(b"hello from volume stripes".to_vec())
11295 );
11296 opened.verify().unwrap();
11297 }
11298
11299 #[test]
11300 fn recovers_from_one_missing_volume_when_parity_allows() {
11301 let files = [RegularFile::new("alpha.txt", b"recover me")];
11302 let archive = write_archive(
11303 &files,
11304 &master_key(),
11305 WriterOptions {
11306 stripe_width: 2,
11307 volume_loss_tolerance: 1,
11308 ..single_stream_options()
11309 },
11310 )
11311 .unwrap();
11312
11313 let recovered =
11314 open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap();
11315 assert_eq!(
11316 recovered.extract_file("alpha.txt").unwrap(),
11317 Some(b"recover me".to_vec())
11318 );
11319 recovered.verify().unwrap();
11320 }
11321
11322 #[test]
11323 fn recovers_from_crc_corrupted_block_when_parity_allows() {
11324 let files = [RegularFile::new("alpha.txt", b"repair corrupt block")];
11325 let archive = write_archive(
11326 &files,
11327 &master_key(),
11328 WriterOptions {
11329 stripe_width: 2,
11330 volume_loss_tolerance: 1,
11331 ..single_stream_options()
11332 },
11333 )
11334 .unwrap();
11335 let mut volumes = archive.volumes.clone();
11336 corrupt_first_block_record_payload(&mut volumes[0]);
11337
11338 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
11339 let recovered = open_archive_volumes(&volume_refs, &master_key()).unwrap();
11340
11341 assert_eq!(
11342 recovered.extract_file("alpha.txt").unwrap(),
11343 Some(b"repair corrupt block".to_vec())
11344 );
11345 recovered.verify().unwrap();
11346 }
11347
11348 #[test]
11349 fn rejects_multi_volume_count_mismatch_without_tolerance() {
11350 let files = [RegularFile::new("alpha.txt", b"count check")];
11351 let archive = write_archive(
11352 &files,
11353 &master_key(),
11354 WriterOptions {
11355 stripe_width: 3,
11356 volume_loss_tolerance: 0,
11357 ..single_stream_options()
11358 },
11359 )
11360 .unwrap();
11361
11362 assert_eq!(
11363 open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap_err(),
11364 FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
11365 );
11366 }
11367
11368 #[test]
11369 fn rejects_multi_volume_manifest_bootstrap_field_mismatch() {
11370 let files = [RegularFile::new("alpha.txt", b"footer mismatch")];
11371 let archive = write_archive(
11372 &files,
11373 &master_key(),
11374 WriterOptions {
11375 stripe_width: 2,
11376 volume_loss_tolerance: 1,
11377 ..single_stream_options()
11378 },
11379 )
11380 .unwrap();
11381
11382 let mut bad_first = archive.volumes[0].clone();
11383 rewrite_manifest_footer(&mut bad_first, &master_key(), |footer| {
11384 footer.index_root_first_block = footer.index_root_first_block.wrapping_add(1);
11385 });
11386
11387 open_archive_volumes(
11388 &[bad_first.as_slice(), archive.volumes[1].as_slice()],
11389 &master_key(),
11390 )
11391 .unwrap();
11392 }
11393
11394 #[test]
11395 fn repairs_corrupted_index_root_block_in_multi_volume_archive() {
11396 let files = [RegularFile::new("alpha.txt", b"repair meta root")];
11397 let archive = write_archive(
11398 &files,
11399 &master_key(),
11400 WriterOptions {
11401 stripe_width: 2,
11402 volume_loss_tolerance: 1,
11403 ..single_stream_options()
11404 },
11405 )
11406 .unwrap();
11407 let mut volumes = archive.volumes.clone();
11408
11409 let mut corrupted = false;
11410 for volume in &mut volumes {
11411 if let Some(slot) =
11412 block_record_slots_with_kind(volume, BlockKind::IndexRootData).first()
11413 {
11414 corrupt_block_record_payload_at_slot(volume, *slot);
11415 corrupted = true;
11416 break;
11417 }
11418 }
11419 assert!(corrupted, "expected an IndexRootData record");
11420
11421 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
11422 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
11423 assert_eq!(
11424 opened.extract_file("alpha.txt").unwrap(),
11425 Some(b"repair meta root".to_vec())
11426 );
11427 opened.verify().unwrap();
11428 }
11429
11430 #[test]
11431 fn repairs_corrupted_index_shard_block_in_multi_volume_archive() {
11432 let files = [RegularFile::new("alpha.txt", b"repair meta shard")];
11433 let archive = write_archive(
11434 &files,
11435 &master_key(),
11436 WriterOptions {
11437 stripe_width: 2,
11438 volume_loss_tolerance: 1,
11439 ..single_stream_options()
11440 },
11441 )
11442 .unwrap();
11443 let mut volumes = archive.volumes.clone();
11444
11445 let mut corrupted = false;
11446 for volume in &mut volumes {
11447 if let Some(slot) =
11448 block_record_slots_with_kind(volume, BlockKind::IndexShardData).first()
11449 {
11450 corrupt_block_record_payload_at_slot(volume, *slot);
11451 corrupted = true;
11452 break;
11453 }
11454 }
11455 assert!(corrupted, "expected an IndexShardData record");
11456
11457 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
11458 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
11459 assert_eq!(
11460 opened.extract_file("alpha.txt").unwrap(),
11461 Some(b"repair meta shard".to_vec())
11462 );
11463 opened.verify().unwrap();
11464 }
11465
11466 #[test]
11467 fn rejects_missing_volume_when_loss_tolerance_zero_even_with_bitrot_parity() {
11468 let files = [RegularFile::new(
11469 "alpha.txt",
11470 b"bitrot parity is not volume loss",
11471 )];
11472 let archive = write_archive(
11473 &files,
11474 &master_key(),
11475 WriterOptions {
11476 stripe_width: 2,
11477 volume_loss_tolerance: 0,
11478 bit_rot_buffer_pct: 1,
11479 ..single_stream_options()
11480 },
11481 )
11482 .unwrap();
11483
11484 assert_eq!(
11485 open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap_err(),
11486 FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
11487 );
11488 }
11489
11490 #[test]
11491 fn repairs_crc_erasure_only_within_parity_budget() {
11492 let payload = pseudo_random_bytes(12_000);
11493 let archive = write_archive(
11494 &[RegularFile::new("rot.bin", &payload)],
11495 &master_key(),
11496 small_block_recovery_options(),
11497 )
11498 .unwrap();
11499 let payload_slots = first_payload_data_run_slots(&archive.bytes);
11500 assert!(
11501 payload_slots.len() >= 2,
11502 "fixture must contain a multi-block payload object"
11503 );
11504
11505 let mut one_erasure = archive.bytes.clone();
11506 corrupt_block_record_payload_at_slot(&mut one_erasure, payload_slots[0]);
11507 let repaired = open_archive(&one_erasure, &master_key()).unwrap();
11508 assert_eq!(
11509 repaired.extract_file("rot.bin").unwrap(),
11510 Some(payload.clone())
11511 );
11512
11513 let mut two_erasures = archive.bytes.clone();
11514 corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[0]);
11515 corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[1]);
11516 let unrepaired = open_archive(&two_erasures, &master_key()).unwrap();
11517 assert_eq!(
11518 unrepaired.extract_file("rot.bin").unwrap_err(),
11519 FormatError::FecTooFewAvailableShards
11520 );
11521 }
11522
11523 #[test]
11524 fn verify_rejects_missing_required_object_block_extent() {
11525 let (mut opened, missing_block) = multi_envelope_reader_fixture();
11526 assert!(opened.blocks.remove(&missing_block).is_some());
11527
11528 assert_eq!(
11529 opened.verify().unwrap_err(),
11530 FormatError::FecTooFewAvailableShards
11531 );
11532 }
11533
11534 #[test]
11535 fn parity_crc_erasure_does_not_hide_authenticated_data() {
11536 let payload = pseudo_random_bytes(12_000);
11537 let archive = write_archive(
11538 &[RegularFile::new("parity-erasure.bin", &payload)],
11539 &master_key(),
11540 parity_rich_recovery_options(),
11541 )
11542 .unwrap();
11543 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
11544 let parity_slots = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity);
11545 assert!(
11546 parity_slots.len() >= 2,
11547 "fixture must contain redundant parity shards"
11548 );
11549 let mut corrupted = archive.bytes;
11550 corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
11551 corrupt_block_record_payload_at_slot(&mut corrupted, parity_slots[0]);
11552
11553 let opened = open_archive(&corrupted, &master_key()).unwrap();
11554 assert_eq!(
11555 opened.extract_file("parity-erasure.bin").unwrap(),
11556 Some(payload)
11557 );
11558 opened.verify().unwrap();
11559 }
11560
11561 #[test]
11562 fn repair_patches_restore_crc_erased_payload_block() {
11563 let payload = pseudo_random_bytes(12_000);
11564 let archive = write_archive(
11565 &[RegularFile::new("rot.bin", &payload)],
11566 &master_key(),
11567 small_block_recovery_options(),
11568 )
11569 .unwrap();
11570 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
11571 let mut corrupted = archive.bytes.clone();
11572 corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
11573
11574 let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
11575 opened.verify().unwrap();
11576 let patches = opened.repair_patches().unwrap();
11577 assert_eq!(patches.len(), 1);
11578 apply_repair_patches(&mut corrupted, &patches);
11579
11580 let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
11581 repaired.verify().unwrap();
11582 assert!(repaired.repair_patches().unwrap().is_empty());
11583 }
11584
11585 #[test]
11586 fn repair_patches_restore_crc_erased_payload_parity_block() {
11587 let payload = pseudo_random_bytes(12_000);
11588 let archive = write_archive(
11589 &[RegularFile::new("parity-erasure.bin", &payload)],
11590 &master_key(),
11591 parity_rich_recovery_options(),
11592 )
11593 .unwrap();
11594 let parity_slot = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity)[0];
11595 let mut corrupted = archive.bytes.clone();
11596 corrupt_block_record_payload_at_slot(&mut corrupted, parity_slot);
11597
11598 let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
11599 opened.verify().unwrap();
11600 let patches = opened.repair_patches().unwrap();
11601 assert_eq!(patches.len(), 1);
11602 apply_repair_patches(&mut corrupted, &patches);
11603
11604 let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
11605 repaired.verify().unwrap();
11606 assert!(repaired.repair_patches().unwrap().is_empty());
11607 }
11608
11609 #[test]
11610 fn rejects_odd_block_size_before_fec_repair() {
11611 let archive = write_archive(
11612 &[RegularFile::new("odd-block.txt", b"payload")],
11613 &master_key(),
11614 small_block_recovery_options(),
11615 )
11616 .unwrap();
11617 let mut malformed = archive.bytes;
11618 let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
11619 let block_size_offset = volume_header.crypto_header_offset as usize + 24;
11620 malformed[block_size_offset..block_size_offset + 4].copy_from_slice(&4097u32.to_le_bytes());
11621
11622 assert_eq!(
11623 open_archive(&malformed, &master_key()).unwrap_err(),
11624 FormatError::OddBlockSize(4097)
11625 );
11626 }
11627
11628 #[test]
11629 fn rejects_structurally_malformed_block_records_instead_of_repairing() {
11630 let archive = write_archive(
11631 &[RegularFile::new("structural-block.txt", b"payload")],
11632 &master_key(),
11633 small_block_recovery_options(),
11634 )
11635 .unwrap();
11636 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
11637
11638 let mut bad_magic = archive.bytes.clone();
11639 corrupt_block_record_magic_at_slot(&mut bad_magic, payload_slot);
11640 assert_eq!(
11641 open_archive(&bad_magic, &master_key()).unwrap_err(),
11642 FormatError::BadMagic {
11643 structure: "BlockRecord"
11644 }
11645 );
11646
11647 let mut bad_reserved = archive.bytes;
11648 corrupt_block_record_reserved_at_slot(&mut bad_reserved, payload_slot);
11649 assert_eq!(
11650 open_archive(&bad_reserved, &master_key()).unwrap_err(),
11651 FormatError::NonZeroReserved {
11652 structure: "BlockRecord"
11653 }
11654 );
11655 }
11656
11657 #[test]
11658 fn rejects_parity_block_with_last_data_flag() {
11659 let archive = write_archive(
11660 &[RegularFile::new("parity-flag.txt", b"payload")],
11661 &master_key(),
11662 small_block_recovery_options(),
11663 )
11664 .unwrap();
11665 let parity_slot =
11666 first_block_record_slot_with_kind(&archive.bytes, BlockKind::PayloadParity).unwrap();
11667 let mut malformed = archive.bytes;
11668 mutate_block_record_at_slot(&mut malformed, parity_slot, |record| {
11669 record.flags = 0x01;
11670 });
11671
11672 assert_eq!(
11673 open_archive(&malformed, &master_key()).unwrap_err(),
11674 FormatError::ParityBlockHasLastDataFlag
11675 );
11676 }
11677
11678 #[test]
11679 fn rejects_missing_and_duplicate_payload_last_data_flags() {
11680 let payload = pseudo_random_bytes(12_000);
11681 let archive = write_archive(
11682 &[RegularFile::new("flags.bin", &payload)],
11683 &master_key(),
11684 small_block_recovery_options(),
11685 )
11686 .unwrap();
11687 let payload_slots = first_payload_data_run_slots(&archive.bytes);
11688 assert!(
11689 payload_slots.len() >= 2,
11690 "fixture must contain a multi-block payload object"
11691 );
11692
11693 let mut duplicate_last = archive.bytes.clone();
11694 mutate_block_record_at_slot(&mut duplicate_last, payload_slots[0], |record| {
11695 record.flags = 0x01;
11696 });
11697 let opened = open_archive(&duplicate_last, &master_key()).unwrap();
11698 assert_eq!(
11699 opened.extract_file("flags.bin").unwrap_err(),
11700 FormatError::InvalidArchive("object last-data flag is not on the final data block")
11701 );
11702
11703 let mut missing_last = archive.bytes;
11704 mutate_block_record_at_slot(
11705 &mut missing_last,
11706 *payload_slots.last().unwrap(),
11707 |record| {
11708 record.flags = 0;
11709 },
11710 );
11711 let opened = open_archive(&missing_last, &master_key()).unwrap();
11712 assert_eq!(
11713 opened.extract_file("flags.bin").unwrap_err(),
11714 FormatError::InvalidArchive("object last-data flag is not on the final data block")
11715 );
11716 }
11717
11718 #[test]
11719 fn recovers_from_one_corrupt_manifest_footer_copy_when_another_volume_authenticates() {
11720 let files = [RegularFile::new(
11721 "footer-copy.txt",
11722 b"survives one bad footer",
11723 )];
11724 let archive = write_archive(
11725 &files,
11726 &master_key(),
11727 WriterOptions {
11728 stripe_width: 2,
11729 volume_loss_tolerance: 1,
11730 ..single_stream_options()
11731 },
11732 )
11733 .unwrap();
11734 let mut volumes = archive.volumes.clone();
11735 corrupt_manifest_footer_hmac(&mut volumes[0]);
11736
11737 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
11738 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
11739 assert_eq!(opened.manifest_footer.volume_index, 0);
11740 assert_eq!(opened.volume_header.volume_index, 0);
11741 assert_eq!(opened.volume_trailer.as_ref().unwrap().volume_index, 0);
11742 assert_eq!(
11743 opened.extract_file("footer-copy.txt").unwrap(),
11744 Some(b"survives one bad footer".to_vec())
11745 );
11746 opened.verify().unwrap();
11747 }
11748
11749 #[test]
11750 fn manifest_footer_corruption_requires_trusted_sidecar() {
11751 let archive = write_archive(
11752 &[RegularFile::new("footer.txt", b"sidecar authority")],
11753 &master_key(),
11754 single_stream_options(),
11755 )
11756 .unwrap();
11757 let manifest_offset = terminal_material_offset(&archive.bytes);
11758 let mut corrupted = archive.bytes.clone();
11759 corrupted[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
11760 corrupt_v41_terminal_recovery(&mut corrupted);
11761
11762 assert!(open_archive(&corrupted, &master_key()).is_err());
11763
11764 let opened =
11765 open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
11766 .unwrap();
11767 assert!(opened.volume_trailer.is_none());
11768 assert_eq!(
11769 opened.extract_file("footer.txt").unwrap(),
11770 Some(b"sidecar authority".to_vec())
11771 );
11772 opened.verify().unwrap();
11773 }
11774
11775 #[test]
11776 fn authenticated_footer_trailer_and_sidecar_hmac_boundaries_are_enforced() {
11777 let archive = write_archive(
11778 &[RegularFile::new("hmac-boundary.txt", b"boundary bytes")],
11779 &master_key(),
11780 single_stream_options(),
11781 )
11782 .unwrap();
11783 let strict_options = ReaderOptions {
11784 max_trailing_garbage_scan: 0,
11785 ..ReaderOptions::default()
11786 };
11787
11788 let manifest_offset = terminal_material_offset(&archive.bytes);
11789 for offset in [
11790 manifest_offset + 71,
11791 manifest_offset + MANIFEST_HMAC_COVERED_LEN,
11792 ] {
11793 let mut corrupted = archive.bytes.clone();
11794 corrupted[offset] ^= 0x01;
11795 open_archive(&corrupted, &master_key()).unwrap();
11796 }
11797
11798 let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
11799 for offset in [
11800 trailer_offset + 75,
11801 trailer_offset + TRAILER_HMAC_COVERED_LEN,
11802 ] {
11803 let mut corrupted = archive.bytes.clone();
11804 corrupted[offset] ^= 0x01;
11805 OpenedArchive::open_with_options(&corrupted, &master_key(), strict_options).unwrap();
11806 }
11807
11808 let mut covered_sidecar = archive.bootstrap_sidecar.clone();
11809 let mut header =
11810 BootstrapSidecarHeader::parse(&covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
11811 .unwrap();
11812 header.manifest_footer_offset += 1;
11813 covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
11814 assert_eq!(
11815 open_archive_with_bootstrap_sidecar(&archive.bytes, &covered_sidecar, &master_key())
11816 .unwrap_err(),
11817 FormatError::HmacMismatch {
11818 structure: "BootstrapSidecarHeader"
11819 }
11820 );
11821
11822 let mut tag_sidecar = archive.bootstrap_sidecar.clone();
11823 let mut header =
11824 BootstrapSidecarHeader::parse(&tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
11825 header.sidecar_hmac[0] ^= 1;
11826 tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
11827 assert_eq!(
11828 open_archive_with_bootstrap_sidecar(&archive.bytes, &tag_sidecar, &master_key())
11829 .unwrap_err(),
11830 FormatError::HmacMismatch {
11831 structure: "BootstrapSidecarHeader"
11832 }
11833 );
11834
11835 let mut non_covered_sidecar = archive.bootstrap_sidecar.clone();
11836 let header =
11837 BootstrapSidecarHeader::parse(&non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
11838 .unwrap();
11839 let mut header_bytes = header.to_bytes();
11840 header_bytes[124] ^= 0x01;
11841 let crc = crc32c::crc32c(&header_bytes[..124]);
11842 header_bytes[124..128].copy_from_slice(&crc.to_le_bytes());
11843 non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
11844 let opened = open_archive_with_bootstrap_sidecar(
11845 &archive.bytes,
11846 &non_covered_sidecar,
11847 &master_key(),
11848 )
11849 .unwrap();
11850 assert_eq!(
11851 opened.extract_file("hmac-boundary.txt").unwrap(),
11852 Some(b"boundary bytes".to_vec())
11853 );
11854 }
11855
11856 #[test]
11857 fn rejects_authenticated_footer_and_trailer_volume_index_mismatches() {
11858 let archive = write_archive(
11859 &[RegularFile::new("volume-index.txt", b"identity")],
11860 &master_key(),
11861 single_stream_options(),
11862 )
11863 .unwrap();
11864
11865 let mut bad_trailer = archive.bytes.clone();
11866 rewrite_volume_trailer(&mut bad_trailer, &master_key(), |trailer| {
11867 trailer.volume_index = 1;
11868 });
11869 open_archive(&bad_trailer, &master_key()).unwrap();
11870
11871 let mut bad_manifest = archive.bytes;
11872 rewrite_manifest_footer(&mut bad_manifest, &master_key(), |footer| {
11873 footer.volume_index = 1;
11874 });
11875 open_archive(&bad_manifest, &master_key()).unwrap();
11876 }
11877
11878 #[test]
11879 fn rejects_same_key_header_terminal_material_splice() {
11880 let first = write_archive(
11881 &[RegularFile::new("splice.txt", b"same shape")],
11882 &master_key(),
11883 single_stream_options(),
11884 )
11885 .unwrap();
11886 let second = write_archive(
11887 &[RegularFile::new("splice.txt", b"same shape")],
11888 &master_key(),
11889 single_stream_options(),
11890 )
11891 .unwrap();
11892 assert_ne!(first.archive_uuid, second.archive_uuid);
11893 assert_eq!(
11894 terminal_material_offset(&first.bytes),
11895 terminal_material_offset(&second.bytes)
11896 );
11897 assert_eq!(first.bytes.len(), second.bytes.len());
11898
11899 let terminal_offset = terminal_material_offset(&first.bytes);
11900 let mut spliced = first.bytes.clone();
11901 spliced[terminal_offset..].copy_from_slice(&second.bytes[terminal_offset..]);
11902
11903 assert_eq!(
11904 open_archive(&spliced, &master_key()).unwrap_err(),
11905 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
11906 );
11907 }
11908
11909 #[test]
11910 fn rejects_cmra_crypto_header_pre_hmac_mismatch() {
11911 let kdf_params = crate::crypto::KdfParams::Argon2id {
11912 t_cost: 1,
11913 m_cost_kib: 8,
11914 parallelism: 1,
11915 salt: b"0123456789abcdef".to_vec(),
11916 };
11917 let archive = write_archive_with_kdf(
11918 &[RegularFile::new("cmra-crypto.txt", b"same fixed header")],
11919 &master_key(),
11920 single_stream_options(),
11921 &kdf_params,
11922 )
11923 .unwrap();
11924 let mut mutated = archive.bytes.clone();
11925 let volume_header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
11926 let subkeys = Subkeys::derive(
11927 &master_key(),
11928 &volume_header.archive_uuid,
11929 &volume_header.session_id,
11930 )
11931 .unwrap();
11932
11933 rewrite_cmra_image(&mut mutated, CmraRecoveryMode::KeyHolding, |image| {
11934 let crypto_region = image
11935 .regions
11936 .iter_mut()
11937 .find(|region| region.region_type == 2)
11938 .unwrap();
11939 let hmac_offset = crypto_region.bytes.len() - CRYPTO_HEADER_HMAC_LEN;
11940 let salt_start = CRYPTO_HEADER_FIXED_LEN + 16;
11941 crypto_region.bytes[salt_start] ^= 0x01;
11942 let hmac = compute_hmac(
11943 HmacDomain::CryptoHeader,
11944 &subkeys.mac_key,
11945 &volume_header.archive_uuid,
11946 &volume_header.session_id,
11947 &crypto_region.bytes[..hmac_offset],
11948 );
11949 crypto_region.bytes[hmac_offset..].copy_from_slice(&hmac);
11950 });
11951
11952 let final_offset = mutated.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11953 let locator = final_recovery_locator(&mutated);
11954 let crypto_start = volume_header.crypto_header_offset as usize;
11955 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
11956 let parsed_crypto = CryptoHeader::parse(
11957 &mutated[crypto_start..crypto_end],
11958 volume_header.crypto_header_length,
11959 )
11960 .unwrap();
11961 assert_eq!(
11962 parse_locator_cmra_candidate(
11963 &mutated,
11964 final_offset,
11965 locator,
11966 KeyHoldingTerminalContext {
11967 subkeys: &subkeys,
11968 volume_header: &volume_header,
11969 crypto_header: &parsed_crypto.fixed,
11970 crypto_header_bytes: &mutated[crypto_start..crypto_end],
11971 },
11972 )
11973 .unwrap_err(),
11974 FormatError::InvalidArchive("CMRA CryptoHeader differs from parsed CryptoHeader")
11975 );
11976 assert!(open_archive(&mutated, &master_key()).is_err());
11977 }
11978
11979 #[test]
11980 fn rejects_same_key_crypto_header_splice_with_session_mismatch() {
11981 let base = WriterOptions {
11982 archive_uuid: Some([0x11; 16]),
11983 session_id: Some([0x22; 16]),
11984 ..single_stream_options()
11985 };
11986 let same_archive = WriterOptions {
11987 archive_uuid: Some([0x11; 16]),
11988 session_id: Some([0x33; 16]),
11989 ..single_stream_options()
11990 };
11991
11992 let first = write_archive(
11993 &[RegularFile::new("splice.txt", b"same shape")],
11994 &master_key(),
11995 base,
11996 )
11997 .unwrap();
11998 let second = write_archive(
11999 &[RegularFile::new("splice.txt", b"same shape")],
12000 &master_key(),
12001 same_archive,
12002 )
12003 .unwrap();
12004
12005 let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
12006 let second_volume_header = VolumeHeader::parse(&second.bytes[..VOLUME_HEADER_LEN]).unwrap();
12007 let crypto_start = volume_header.crypto_header_offset as usize;
12008 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
12009 let second_crypto_end = second_volume_header.crypto_header_offset as usize
12010 + second_volume_header.crypto_header_length as usize;
12011 assert_eq!(crypto_end, second_crypto_end);
12012
12013 let mut spliced = first.bytes.clone();
12014 spliced[crypto_start..crypto_end].copy_from_slice(&second.bytes[crypto_start..crypto_end]);
12015
12016 assert_eq!(
12017 open_archive(&spliced, &master_key()).unwrap_err(),
12018 FormatError::HmacMismatch {
12019 structure: "CryptoHeader"
12020 }
12021 );
12022 }
12023
12024 #[test]
12025 fn rejects_same_key_object_splice_with_session_mismatch() {
12026 let first = write_archive(
12027 &[RegularFile::new("splice.txt", b"same shape")],
12028 &master_key(),
12029 WriterOptions {
12030 archive_uuid: Some([0x11; 16]),
12031 session_id: Some([0x22; 16]),
12032 ..single_stream_options()
12033 },
12034 )
12035 .unwrap();
12036 let second = write_archive(
12037 &[RegularFile::new("splice.txt", b"same shape")],
12038 &master_key(),
12039 WriterOptions {
12040 archive_uuid: Some([0x11; 16]),
12041 session_id: Some([0x33; 16]),
12042 ..single_stream_options()
12043 },
12044 )
12045 .unwrap();
12046
12047 let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
12048 let crypto_end = volume_header.crypto_header_offset as usize
12049 + volume_header.crypto_header_length as usize;
12050 let terminal_offset = terminal_material_offset(&first.bytes);
12051 let second_terminal_offset = terminal_material_offset(&second.bytes);
12052 assert_eq!(terminal_offset, second_terminal_offset);
12053
12054 let mut spliced = first.bytes.clone();
12055 spliced[crypto_end..terminal_offset]
12056 .copy_from_slice(&second.bytes[crypto_end..terminal_offset]);
12057
12058 assert_eq!(
12059 open_archive(&spliced, &master_key()).unwrap_err(),
12060 FormatError::AeadFailure
12061 );
12062 }
12063
12064 #[test]
12065 fn rejects_authenticated_trailer_pointer_and_count_mutations() {
12066 let archive = write_archive(
12067 &[RegularFile::new(
12068 "trailer-range.txt",
12069 b"authenticated ranges",
12070 )],
12071 &master_key(),
12072 single_stream_options(),
12073 )
12074 .unwrap();
12075 let strict_options = ReaderOptions {
12076 max_trailing_garbage_scan: 0,
12077 ..ReaderOptions::default()
12078 };
12079 let bytes = archive.bytes;
12080 let manifest_offset = terminal_material_offset(&bytes);
12081 let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
12082
12083 let mut wrong_footer_length = bytes.clone();
12084 rewrite_volume_trailer(&mut wrong_footer_length, &master_key(), |trailer| {
12085 trailer.manifest_footer_length = 42;
12086 });
12087 OpenedArchive::open_with_options(&wrong_footer_length, &master_key(), strict_options)
12088 .unwrap();
12089
12090 for (label, offset) in [
12091 (
12092 "offset before trailer by 1",
12093 manifest_offset.saturating_sub(1),
12094 ),
12095 ("offset after trailer", manifest_offset + 1),
12096 ("offset at stream start", 0),
12097 ("offset at trailer", trailer_offset),
12098 ("offset beyond trailer", trailer_offset + 4),
12099 ] {
12100 let mut wrong_footer_offset = bytes.clone();
12101 rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
12102 trailer.manifest_footer_offset = offset as u64;
12103 });
12104 open_archive(&wrong_footer_offset, &master_key())
12105 .unwrap_or_else(|err| panic!("manifest offset case {label}: {err:?}"));
12106 }
12107
12108 let mut wrong_bytes_written = bytes.clone();
12109 rewrite_volume_trailer(&mut wrong_bytes_written, &master_key(), |trailer| {
12110 trailer.bytes_written += 1;
12111 });
12112 open_archive(&wrong_bytes_written, &master_key()).unwrap();
12113
12114 let mut wrong_block_count = bytes.clone();
12115 rewrite_volume_trailer(&mut wrong_block_count, &master_key(), |trailer| {
12116 trailer.block_count += 1;
12117 });
12118 open_archive(&wrong_block_count, &master_key()).unwrap();
12119
12120 let mut wrong_footer_offset = bytes.clone();
12121 rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
12122 trailer.manifest_footer_offset = bytes.len() as u64 + 1024;
12123 });
12124 open_archive(&wrong_footer_offset, &master_key()).unwrap();
12125 }
12126
12127 #[test]
12128 fn rejects_authenticated_trailer_outside_trailing_scan_cap() {
12129 let archive = write_archive(
12130 &[RegularFile::new(
12131 "trailer-trailing-scan.txt",
12132 b"trailer scan boundaries",
12133 )],
12134 &master_key(),
12135 single_stream_options(),
12136 )
12137 .unwrap();
12138 let options = ReaderOptions {
12139 max_trailing_garbage_scan: 8,
12140 ..ReaderOptions::default()
12141 };
12142
12143 let mut within_scan = archive.bytes.clone();
12144 within_scan.resize(within_scan.len() + options.max_trailing_garbage_scan, 0xAA);
12145 let opened =
12146 OpenedArchive::open_with_options(&within_scan, &master_key(), options).unwrap();
12147 assert_eq!(
12148 opened.extract_file("trailer-trailing-scan.txt").unwrap(),
12149 Some(b"trailer scan boundaries".to_vec())
12150 );
12151
12152 let mut beyond_scan = archive.bytes.clone();
12153 beyond_scan.resize(
12154 beyond_scan.len() + max_critical_recovery_scan(options).unwrap() + 1,
12155 0xAA,
12156 );
12157 assert_eq!(
12158 OpenedArchive::open_with_options(&beyond_scan, &master_key(), options).unwrap_err(),
12159 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
12160 );
12161 }
12162
12163 #[test]
12164 fn rejects_authenticated_index_root_extent_size_mismatch_at_open() {
12165 let archive = write_archive(
12166 &[RegularFile::new("index-root-size.txt", b"extent size")],
12167 &master_key(),
12168 single_stream_options(),
12169 )
12170 .unwrap();
12171 let mut malformed = archive.bytes;
12172 let slot = first_block_record_slot_with_kind(&malformed, BlockKind::IndexRootData)
12173 .expect("archive should contain IndexRootData");
12174 mutate_block_record_at_slot(&mut malformed, slot, |record| {
12175 record.payload[0] ^= 0x55;
12176 });
12177
12178 assert_eq!(
12179 open_archive(&malformed, &master_key()).unwrap_err(),
12180 FormatError::AeadFailure
12181 );
12182 }
12183
12184 #[test]
12185 fn rejects_block_record_at_wrong_stripe_position() {
12186 let files = [RegularFile::new("alpha.txt", b"wrong stripe")];
12187 let archive = write_archive(
12188 &files,
12189 &master_key(),
12190 WriterOptions {
12191 stripe_width: 2,
12192 volume_loss_tolerance: 1,
12193 ..single_stream_options()
12194 },
12195 )
12196 .unwrap();
12197 let mut volumes = archive.volumes.clone();
12198 mutate_first_block_record(&mut volumes[0], |record| {
12199 record.block_index += 2;
12200 });
12201
12202 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
12203 assert_eq!(
12204 open_archive_volumes(&volume_refs, &master_key()).unwrap_err(),
12205 FormatError::InvalidArchive("BlockRecord index does not match volume position")
12206 );
12207 }
12208
12209 #[test]
12210 fn rejects_decreasing_block_record_index_in_required_region() {
12211 let archive = write_archive(
12212 &[RegularFile::new("alpha.txt", b"decreasing block index")],
12213 &master_key(),
12214 single_stream_options(),
12215 )
12216 .unwrap();
12217 assert!(block_record_slots(&archive.bytes).len() >= 2);
12218
12219 let mut malformed = archive.bytes;
12220 mutate_block_record_at_slot(&mut malformed, 1, |record| {
12221 record.block_index = 0;
12222 });
12223
12224 assert_eq!(
12225 open_archive(&malformed, &master_key()).unwrap_err(),
12226 FormatError::InvalidArchive("BlockRecord index does not match volume position")
12227 );
12228 }
12229
12230 #[test]
12231 fn rejects_duplicate_authenticated_volume_indexes() {
12232 let files = [RegularFile::new("alpha.txt", b"duplicates")];
12233 let archive = write_archive(
12234 &files,
12235 &master_key(),
12236 WriterOptions {
12237 stripe_width: 2,
12238 volume_loss_tolerance: 1,
12239 ..single_stream_options()
12240 },
12241 )
12242 .unwrap();
12243
12244 assert_eq!(
12245 open_archive_volumes(
12246 &[archive.volumes[0].as_slice(), archive.volumes[0].as_slice()],
12247 &master_key()
12248 )
12249 .unwrap_err(),
12250 FormatError::InvalidArchive("duplicate authenticated volume index")
12251 );
12252 }
12253
12254 #[test]
12255 fn rejects_conflicting_duplicate_authenticated_volume_indexes_by_default() {
12256 let files = [RegularFile::new("alpha.txt", b"conflicting duplicates")];
12257 let archive = write_archive(
12258 &files,
12259 &master_key(),
12260 WriterOptions {
12261 stripe_width: 2,
12262 volume_loss_tolerance: 1,
12263 ..single_stream_options()
12264 },
12265 )
12266 .unwrap();
12267 let mut conflicting = archive.volumes[0].clone();
12268 corrupt_first_block_record_payload(&mut conflicting);
12269
12270 assert_eq!(
12271 open_archive_volumes(
12272 &[archive.volumes[0].as_slice(), conflicting.as_slice()],
12273 &master_key()
12274 )
12275 .unwrap_err(),
12276 FormatError::InvalidArchive("duplicate authenticated volume index")
12277 );
12278 }
12279
12280 fn directory_hint_table_from_rows(
12281 hint_shard_index: u64,
12282 rows: &[(Vec<u8>, Vec<u32>)],
12283 shard_count: u32,
12284 ) -> DirectoryHintTable {
12285 let mut entries = Vec::new();
12286 let mut shard_row_indexes = Vec::new();
12287 let mut string_pool = Vec::new();
12288
12289 for (path, rows) in rows {
12290 let path_offset = if path.is_empty() {
12291 0
12292 } else {
12293 let offset = string_pool.len() as u64;
12294 string_pool.extend_from_slice(path);
12295 offset
12296 };
12297 let shard_list_start_index = shard_row_indexes.len() as u32;
12298 shard_row_indexes.extend_from_slice(rows);
12299 entries.push(DirectoryHintEntry {
12300 dir_hash: hash_prefix(path),
12301 path_offset,
12302 path_length: path.len() as u32,
12303 shard_list_start_index,
12304 shard_count: rows.len() as u32,
12305 });
12306 }
12307
12308 let table_bytes =
12309 directory_hint_table_bytes(hint_shard_index, entries, shard_row_indexes, string_pool);
12310 let locating = DirectoryHintShardEntry {
12311 hint_shard_index,
12312 first_dir_hash: hash_prefix(&rows.first().unwrap().0),
12313 last_dir_hash: hash_prefix(&rows.last().unwrap().0),
12314 first_block_index: 0,
12315 data_block_count: 1,
12316 parity_block_count: 0,
12317 encrypted_size: 4096,
12318 decompressed_size: table_bytes.len() as u32,
12319 entry_count: rows.len() as u64,
12320 };
12321 DirectoryHintTable::parse(
12322 &table_bytes,
12323 &locating,
12324 shard_count,
12325 MetadataLimits::default(),
12326 )
12327 .unwrap()
12328 }
12329
12330 fn directory_hint_table_bytes(
12331 hint_shard_index: u64,
12332 entries: Vec<DirectoryHintEntry>,
12333 shard_row_indexes: Vec<u32>,
12334 string_pool: Vec<u8>,
12335 ) -> Vec<u8> {
12336 let header_len = DirectoryHintTableHeader {
12337 version: 1,
12338 hint_shard_index,
12339 entry_count: 0,
12340 entry_table_offset: 0,
12341 shard_list_offset: 0,
12342 string_pool_offset: 0,
12343 string_pool_size: 0,
12344 }
12345 .to_bytes()
12346 .len();
12347 let entry_len = entries
12348 .first()
12349 .map(|entry| entry.to_bytes().len())
12350 .unwrap_or(0);
12351 let shard_list_offset = if entries.is_empty() {
12352 0
12353 } else {
12354 header_len + entries.len() * entry_len
12355 };
12356 let string_pool_offset = if string_pool.is_empty() {
12357 0
12358 } else {
12359 shard_list_offset + shard_row_indexes.len() * 4
12360 };
12361
12362 let header = DirectoryHintTableHeader {
12363 version: 1,
12364 hint_shard_index,
12365 entry_count: entries.len() as u64,
12366 entry_table_offset: if entries.is_empty() {
12367 0
12368 } else {
12369 header_len as u64
12370 },
12371 shard_list_offset: shard_list_offset as u64,
12372 string_pool_offset: string_pool_offset as u64,
12373 string_pool_size: string_pool.len() as u64,
12374 };
12375
12376 let mut out = Vec::new();
12377 out.extend_from_slice(&header.to_bytes());
12378 for entry in entries {
12379 out.extend_from_slice(&entry.to_bytes());
12380 }
12381 for row in shard_row_indexes {
12382 out.extend_from_slice(&row.to_le_bytes());
12383 }
12384 out.extend_from_slice(&string_pool);
12385 out
12386 }
12387
12388 fn corrupt_first_block_record_payload(volume: &mut [u8]) {
12389 let (record_offset, _) = first_block_record(volume);
12390 volume[record_offset + 16] ^= 0x55;
12391 }
12392
12393 fn corrupt_block_record_payload_at_slot(volume: &mut [u8], slot: usize) {
12394 let (record_offset, _) = block_record_at_slot(volume, slot);
12395 volume[record_offset + 16] ^= 0x55;
12396 }
12397
12398 fn apply_repair_patches(volume: &mut [u8], patches: &[ArchiveRepairPatch]) {
12399 for patch in patches {
12400 let offset = patch.record_offset as usize;
12401 let end = offset + patch.record_bytes.len();
12402 volume[offset..end].copy_from_slice(&patch.record_bytes);
12403 }
12404 }
12405
12406 fn corrupt_block_record_magic_at_slot(volume: &mut [u8], slot: usize) {
12407 let (record_offset, _) = block_record_at_slot(volume, slot);
12408 volume[record_offset] ^= 0x55;
12409 }
12410
12411 fn corrupt_block_record_reserved_at_slot(volume: &mut [u8], slot: usize) {
12412 let (record_offset, _) = block_record_at_slot(volume, slot);
12413 volume[record_offset + 14] = 0x01;
12414 }
12415
12416 fn corrupt_manifest_footer_hmac(volume: &mut [u8]) {
12417 let manifest_offset = terminal_material_offset(volume);
12418 volume[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
12419 }
12420
12421 fn final_recovery_locator(volume: &[u8]) -> CriticalRecoveryLocator {
12422 let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
12423 CriticalRecoveryLocator::parse(
12424 &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
12425 )
12426 .unwrap()
12427 }
12428
12429 fn rewrite_cmra_parity_count(volume: &[u8], parity_shard_count: u16) -> Vec<u8> {
12430 let locator = final_recovery_locator(volume);
12431 let tuple = CmraDecoderTuple::from(locator);
12432 assert!(parity_shard_count < tuple.parity_shard_count);
12433 let cmra_offset = locator.cmra_offset as usize;
12434 let shard_size = tuple.shard_size as usize;
12435 let row_len = CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size;
12436 let kept_rows = tuple.data_shard_count as usize + parity_shard_count as usize;
12437 let mut header = CriticalMetadataRecoveryHeader::parse(
12438 &volume[cmra_offset..cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN],
12439 )
12440 .unwrap();
12441 header.parity_shard_count = parity_shard_count;
12442
12443 let mut cmra =
12444 Vec::with_capacity(CRITICAL_METADATA_RECOVERY_HEADER_LEN + kept_rows * row_len);
12445 cmra.extend_from_slice(&header.to_bytes());
12446 let rows_start = cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
12447 for row in 0..kept_rows {
12448 let start = rows_start + row * row_len;
12449 cmra.extend_from_slice(&volume[start..start + row_len]);
12450 }
12451
12452 let mut out = Vec::with_capacity(cmra_offset + cmra.len() + LOCATOR_PAIR_LEN);
12453 out.extend_from_slice(&volume[..cmra_offset]);
12454 out.extend_from_slice(&cmra);
12455 let mut mirror = locator;
12456 mirror.locator_sequence = 1;
12457 mirror.cmra_length = cmra.len() as u32;
12458 mirror.cmra_parity_shard_count = parity_shard_count;
12459 out.extend_from_slice(&mirror.to_bytes());
12460 let final_locator = CriticalRecoveryLocator {
12461 locator_sequence: 0,
12462 ..mirror
12463 };
12464 out.extend_from_slice(&final_locator.to_bytes());
12465 out
12466 }
12467
12468 fn rewrite_public_cmra_image(
12469 volume: &mut [u8],
12470 mutate: impl FnOnce(&mut CriticalMetadataImage),
12471 ) {
12472 rewrite_cmra_image(volume, CmraRecoveryMode::PublicNoKey, mutate);
12473 }
12474
12475 fn rewrite_cmra_image(
12476 volume: &mut [u8],
12477 mode: CmraRecoveryMode,
12478 mutate: impl FnOnce(&mut CriticalMetadataImage),
12479 ) {
12480 let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
12481 let locator = final_recovery_locator(volume);
12482 let tuple = CmraDecoderTuple::from(locator);
12483 let recovered = recover_cmra(volume, locator.cmra_offset, Some(tuple), mode).unwrap();
12484 let mut image = recovered.image;
12485 mutate(&mut image);
12486 refresh_critical_image_region_digests(&mut image);
12487 let image_bytes = image.to_bytes().unwrap();
12488 assert_eq!(image_bytes.len(), tuple.image_length as usize);
12489
12490 let shard_size = tuple.shard_size as usize;
12491 let data_shard_count = tuple.data_shard_count as usize;
12492 let parity_shard_count = tuple.parity_shard_count as usize;
12493 assert!(image_bytes.len() <= data_shard_count * shard_size);
12494
12495 let mut data_shards = Vec::with_capacity(data_shard_count);
12496 for idx in 0..data_shard_count {
12497 let start = idx * shard_size;
12498 let end = (start + shard_size).min(image_bytes.len());
12499 let mut payload = vec![0u8; shard_size];
12500 if start < image_bytes.len() {
12501 payload[..end - start].copy_from_slice(&image_bytes[start..end]);
12502 }
12503 data_shards.push(payload);
12504 }
12505 let parity_shards = encode_parity_gf16(&data_shards, parity_shard_count).unwrap();
12506 let image_sha256 = sha256_bytes(&image_bytes);
12507
12508 let header = CriticalMetadataRecoveryHeader {
12509 shard_size: tuple.shard_size,
12510 data_shard_count: tuple.data_shard_count,
12511 parity_shard_count: tuple.parity_shard_count,
12512 image_length: tuple.image_length,
12513 archive_uuid_hint: locator.archive_uuid_hint,
12514 session_id_hint: locator.session_id_hint,
12515 volume_index_hint: locator.volume_index_hint,
12516 image_sha256,
12517 header_crc32c: 0,
12518 };
12519 let mut cmra = Vec::new();
12520 cmra.extend_from_slice(&header.to_bytes());
12521 for (idx, payload) in data_shards.into_iter().enumerate() {
12522 let payload_len = if idx + 1 == data_shard_count {
12523 image_bytes.len() - idx * shard_size
12524 } else {
12525 shard_size
12526 };
12527 cmra.extend_from_slice(
12528 &CriticalMetadataRecoveryShard {
12529 shard_index: idx as u16,
12530 shard_role: 0,
12531 shard_payload_length: payload_len as u32,
12532 payload,
12533 shard_crc32c: 0,
12534 }
12535 .to_bytes(shard_size)
12536 .unwrap(),
12537 );
12538 }
12539 for (idx, payload) in parity_shards.into_iter().enumerate() {
12540 cmra.extend_from_slice(
12541 &CriticalMetadataRecoveryShard {
12542 shard_index: (data_shard_count + idx) as u16,
12543 shard_role: 1,
12544 shard_payload_length: shard_size as u32,
12545 payload,
12546 shard_crc32c: 0,
12547 }
12548 .to_bytes(shard_size)
12549 .unwrap(),
12550 );
12551 }
12552 assert_eq!(cmra.len() as u64, recovered.cmra_length);
12553 let cmra_offset = locator.cmra_offset as usize;
12554 volume[cmra_offset..cmra_offset + cmra.len()].copy_from_slice(&cmra);
12555
12556 rewrite_locator_image_sha(volume, final_offset, image_sha256);
12557 let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
12558 rewrite_locator_image_sha(volume, mirror_offset, image_sha256);
12559 }
12560
12561 fn refresh_critical_image_region_digests(image: &mut CriticalMetadataImage) {
12562 image.volume_header_sha256 = sha256_bytes(
12563 &image
12564 .regions
12565 .iter()
12566 .find(|region| region.region_type == 1)
12567 .unwrap()
12568 .bytes,
12569 );
12570 image.crypto_header_sha256 = sha256_bytes(
12571 &image
12572 .regions
12573 .iter()
12574 .find(|region| region.region_type == 2)
12575 .unwrap()
12576 .bytes,
12577 );
12578 image.manifest_footer_sha256 = sha256_bytes(
12579 &image
12580 .regions
12581 .iter()
12582 .find(|region| region.region_type == 3)
12583 .unwrap()
12584 .bytes,
12585 );
12586 image.root_auth_footer_sha256 = image
12587 .regions
12588 .iter()
12589 .find(|region| region.region_type == 4)
12590 .map(|region| sha256_bytes(®ion.bytes))
12591 .unwrap_or([0u8; 32]);
12592 image.volume_trailer_sha256 = sha256_bytes(
12593 &image
12594 .regions
12595 .iter()
12596 .find(|region| region.region_type == 5)
12597 .unwrap()
12598 .bytes,
12599 );
12600 }
12601
12602 fn rewrite_locator_image_sha(volume: &mut [u8], offset: usize, image_sha256: [u8; 32]) {
12603 let mut locator =
12604 CriticalRecoveryLocator::parse(&volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN])
12605 .unwrap();
12606 locator.cmra_image_sha256 = image_sha256;
12607 volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN].copy_from_slice(&locator.to_bytes());
12608 }
12609
12610 fn corrupt_v41_terminal_recovery(volume: &mut [u8]) {
12611 let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
12612 let final_locator = CriticalRecoveryLocator::parse(
12613 &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
12614 )
12615 .unwrap();
12616 let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
12617 volume[final_locator.cmra_offset as usize] ^= 0x55;
12618 volume[mirror_offset] ^= 0x55;
12619 volume[final_offset] ^= 0x55;
12620 }
12621
12622 fn mutate_first_block_record(volume: &mut [u8], mutate: impl FnOnce(&mut BlockRecord)) {
12623 let (record_offset, record_len) = first_block_record(volume);
12624 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
12625 let mut record = BlockRecord::parse(
12626 &volume[record_offset..record_offset + record_len],
12627 block_size,
12628 )
12629 .unwrap();
12630 mutate(&mut record);
12631 volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
12632 }
12633
12634 fn mutate_block_record_at_slot(
12635 volume: &mut [u8],
12636 slot: usize,
12637 mutate: impl FnOnce(&mut BlockRecord),
12638 ) {
12639 let (record_offset, record_len) = block_record_at_slot(volume, slot);
12640 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
12641 let mut record = BlockRecord::parse(
12642 &volume[record_offset..record_offset + record_len],
12643 block_size,
12644 )
12645 .unwrap();
12646 mutate(&mut record);
12647 volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
12648 }
12649
12650 fn first_block_record(volume: &[u8]) -> (usize, usize) {
12651 block_record_at_slot(volume, 0)
12652 }
12653
12654 fn block_record_at_slot(volume: &[u8], slot: usize) -> (usize, usize) {
12655 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
12656 let crypto_start = volume_header.crypto_header_offset as usize;
12657 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
12658 let crypto_header = CryptoHeader::parse(
12659 &volume[crypto_start..crypto_end],
12660 volume_header.crypto_header_length,
12661 )
12662 .unwrap();
12663 let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
12664 let record_offset = crypto_end + slot * record_len;
12665 assert!(volume.len() >= record_offset + record_len);
12666 (record_offset, record_len)
12667 }
12668
12669 fn first_block_record_slot_with_kind(volume: &[u8], kind: BlockKind) -> Option<usize> {
12670 block_record_slots(volume)
12671 .into_iter()
12672 .enumerate()
12673 .find_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
12674 }
12675
12676 fn block_record_slots_with_kind(volume: &[u8], kind: BlockKind) -> Vec<usize> {
12677 block_record_slots(volume)
12678 .into_iter()
12679 .enumerate()
12680 .filter_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
12681 .collect()
12682 }
12683
12684 fn first_payload_data_run_slots(volume: &[u8]) -> Vec<usize> {
12685 let mut slots = Vec::new();
12686 for (slot, (_, _, record)) in block_record_slots(volume).into_iter().enumerate() {
12687 if record.kind == BlockKind::PayloadData {
12688 slots.push(slot);
12689 } else if !slots.is_empty() {
12690 break;
12691 }
12692 }
12693 slots
12694 }
12695
12696 fn envelope_indices_for_path(opened: &OpenedArchive, path: &str) -> BTreeSet<u64> {
12697 envelope_entries_for_path(opened, path)
12698 .into_iter()
12699 .map(|entry| entry.envelope_index)
12700 .collect()
12701 }
12702
12703 fn envelope_entries_for_path(opened: &OpenedArchive, path: &str) -> Vec<EnvelopeEntry> {
12704 let normalized =
12705 normalize_lookup_file_path(path, opened.crypto_header.max_path_length).unwrap();
12706 let located = opened.locate_index_file(&normalized).unwrap().unwrap();
12707 let file = &located.shard.files[located.file_index];
12708 frame_range_for_file(&located.shard, file)
12709 .unwrap()
12710 .into_iter()
12711 .map(|frame| {
12712 located
12713 .shard
12714 .envelopes
12715 .iter()
12716 .find(|entry| entry.envelope_index == frame.envelope_index)
12717 .unwrap()
12718 .clone()
12719 })
12720 .collect()
12721 }
12722
12723 fn block_record_slots(volume: &[u8]) -> Vec<(usize, usize, BlockRecord)> {
12724 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
12725 let crypto_start = volume_header.crypto_header_offset as usize;
12726 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
12727 let crypto_header = CryptoHeader::parse(
12728 &volume[crypto_start..crypto_end],
12729 volume_header.crypto_header_length,
12730 )
12731 .unwrap();
12732 let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
12733 let manifest_offset = terminal_material_offset(volume);
12734 assert_eq!((manifest_offset - crypto_end) % record_len, 0);
12735 let record_count = (manifest_offset - crypto_end) / record_len;
12736 (0..record_count)
12737 .map(|slot| {
12738 let offset = crypto_end + slot * record_len;
12739 let record = BlockRecord::parse(
12740 &volume[offset..offset + record_len],
12741 record_len - BLOCK_RECORD_FRAMING_LEN,
12742 )
12743 .unwrap();
12744 (offset, record_len, record)
12745 })
12746 .collect()
12747 }
12748
12749 fn rewrite_manifest_footer(
12750 volume: &mut [u8],
12751 master_key: &MasterKey,
12752 mutate: impl FnOnce(&mut ManifestFooter),
12753 ) {
12754 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
12755 let offset = terminal_material_offset(volume);
12756 let mut footer =
12757 ManifestFooter::parse(&volume[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
12758 mutate(&mut footer);
12759 footer.manifest_hmac = [0u8; 32];
12760 let mut footer_bytes = footer.to_bytes();
12761 let subkeys = Subkeys::derive(
12762 master_key,
12763 &volume_header.archive_uuid,
12764 &volume_header.session_id,
12765 )
12766 .unwrap();
12767 footer.manifest_hmac = compute_hmac(
12768 HmacDomain::ManifestFooter,
12769 &subkeys.mac_key,
12770 &volume_header.archive_uuid,
12771 &volume_header.session_id,
12772 &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
12773 );
12774 footer_bytes = footer.to_bytes();
12775 volume[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
12776 }
12777
12778 fn rewrite_volume_trailer(
12779 volume: &mut [u8],
12780 master_key: &MasterKey,
12781 mutate: impl FnOnce(&mut VolumeTrailer),
12782 ) {
12783 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
12784 let offset = terminal_material_offset(volume) + MANIFEST_FOOTER_LEN;
12785 let mut trailer =
12786 VolumeTrailer::parse(&volume[offset..offset + VOLUME_TRAILER_LEN]).unwrap();
12787 mutate(&mut trailer);
12788 trailer.trailer_hmac = [0u8; 32];
12789 let mut trailer_bytes = trailer.to_bytes();
12790 let subkeys = Subkeys::derive(
12791 master_key,
12792 &volume_header.archive_uuid,
12793 &volume_header.session_id,
12794 )
12795 .unwrap();
12796 trailer.trailer_hmac = compute_hmac(
12797 HmacDomain::VolumeTrailer,
12798 &subkeys.mac_key,
12799 &volume_header.archive_uuid,
12800 &volume_header.session_id,
12801 &trailer_bytes[..TRAILER_HMAC_COVERED_LEN],
12802 );
12803 trailer_bytes = trailer.to_bytes();
12804 volume[offset..offset + VOLUME_TRAILER_LEN].copy_from_slice(&trailer_bytes);
12805 }
12806
12807 fn rewrite_sidecar_header(
12808 sidecar: &mut [u8],
12809 master_key: &MasterKey,
12810 mutate: impl FnOnce(&mut BootstrapSidecarHeader),
12811 ) {
12812 let mut header =
12813 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12814 mutate(&mut header);
12815 write_signed_sidecar_header(sidecar, master_key, &mut header);
12816 }
12817
12818 fn write_signed_sidecar_header(
12819 sidecar: &mut [u8],
12820 master_key: &MasterKey,
12821 header: &mut BootstrapSidecarHeader,
12822 ) {
12823 header.sidecar_hmac = [0u8; 32];
12824 let mut header_bytes = header.to_bytes();
12825 let subkeys =
12826 Subkeys::derive(master_key, &header.archive_uuid, &header.session_id).unwrap();
12827 header.sidecar_hmac = compute_hmac(
12828 HmacDomain::BootstrapSidecar,
12829 &subkeys.mac_key,
12830 &header.archive_uuid,
12831 &header.session_id,
12832 &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
12833 );
12834 header_bytes = header.to_bytes();
12835 sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
12836 }
12837
12838 fn sparse_bootstrap_sidecar(
12839 source: &[u8],
12840 master_key: &MasterKey,
12841 include_manifest: bool,
12842 include_index_root: bool,
12843 include_dictionary: bool,
12844 ) -> Vec<u8> {
12845 let source_header =
12846 BootstrapSidecarHeader::parse(&source[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12847 let mut sidecar = vec![0u8; BOOTSTRAP_SIDECAR_HEADER_LEN];
12848 let mut header = BootstrapSidecarHeader {
12849 archive_uuid: source_header.archive_uuid,
12850 session_id: source_header.session_id,
12851 flags: 0,
12852 manifest_footer_offset: 0,
12853 manifest_footer_length: 0,
12854 index_root_records_offset: 0,
12855 index_root_records_length: 0,
12856 dictionary_records_offset: 0,
12857 dictionary_records_length: 0,
12858 sidecar_hmac: [0u8; 32],
12859 header_crc32c: 0,
12860 };
12861
12862 if include_manifest {
12863 assert!(source_header.has_manifest_footer());
12864 let (offset, length) = append_sidecar_section(
12865 source,
12866 &mut sidecar,
12867 source_header.manifest_footer_offset,
12868 source_header.manifest_footer_length as u64,
12869 );
12870 header.flags |= 0x01;
12871 header.manifest_footer_offset = offset;
12872 header.manifest_footer_length = length as u32;
12873 }
12874 if include_index_root {
12875 assert!(source_header.has_index_root_records());
12876 let (offset, length) = append_sidecar_section(
12877 source,
12878 &mut sidecar,
12879 source_header.index_root_records_offset,
12880 source_header.index_root_records_length,
12881 );
12882 header.flags |= 0x02;
12883 header.index_root_records_offset = offset;
12884 header.index_root_records_length = length;
12885 }
12886 if include_dictionary {
12887 assert!(source_header.has_dictionary_records());
12888 let (offset, length) = append_sidecar_section(
12889 source,
12890 &mut sidecar,
12891 source_header.dictionary_records_offset,
12892 source_header.dictionary_records_length,
12893 );
12894 header.flags |= 0x04;
12895 header.dictionary_records_offset = offset;
12896 header.dictionary_records_length = length;
12897 }
12898
12899 write_signed_sidecar_header(&mut sidecar, master_key, &mut header);
12900 sidecar
12901 }
12902
12903 fn append_sidecar_section(
12904 source: &[u8],
12905 sidecar: &mut Vec<u8>,
12906 source_offset: u64,
12907 length: u64,
12908 ) -> (u64, u64) {
12909 let source_offset = source_offset as usize;
12910 let length = length as usize;
12911 let offset = sidecar.len() as u64;
12912 sidecar.extend_from_slice(&source[source_offset..source_offset + length]);
12913 (offset, length as u64)
12914 }
12915
12916 fn mutate_sidecar_manifest(
12917 sidecar: &mut [u8],
12918 master_key: &MasterKey,
12919 mutate: impl FnOnce(&mut ManifestFooter),
12920 ) {
12921 let header =
12922 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12923 let offset = header.manifest_footer_offset as usize;
12924 let mut footer =
12925 ManifestFooter::parse(&sidecar[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
12926 mutate(&mut footer);
12927 footer.manifest_hmac = [0u8; 32];
12928 let mut footer_bytes = footer.to_bytes();
12929 let subkeys =
12930 Subkeys::derive(master_key, &footer.archive_uuid, &footer.session_id).unwrap();
12931 footer.manifest_hmac = compute_hmac(
12932 HmacDomain::ManifestFooter,
12933 &subkeys.mac_key,
12934 &footer.archive_uuid,
12935 &footer.session_id,
12936 &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
12937 );
12938 footer_bytes = footer.to_bytes();
12939 sidecar[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
12940 }
12941
12942 fn mutate_sidecar_index_record(
12943 sidecar: &mut [u8],
12944 record_index: usize,
12945 mutate: impl FnOnce(&mut BlockRecord),
12946 ) {
12947 let header =
12948 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12949 let record_len = sidecar_record_len(sidecar);
12950 let offset = header.index_root_records_offset as usize + record_index * record_len;
12951 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
12952 let mut record =
12953 BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
12954 mutate(&mut record);
12955 sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
12956 }
12957
12958 fn mutate_sidecar_dictionary_record(
12959 sidecar: &mut [u8],
12960 record_index: usize,
12961 mutate: impl FnOnce(&mut BlockRecord),
12962 ) {
12963 let header =
12964 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12965 let record_len = sidecar_record_len(sidecar);
12966 let offset = header.dictionary_records_offset as usize + record_index * record_len;
12967 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
12968 let mut record =
12969 BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
12970 mutate(&mut record);
12971 sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
12972 }
12973
12974 fn swap_sidecar_index_records(sidecar: &mut [u8], left: usize, right: usize) {
12975 let header =
12976 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12977 let record_len = sidecar_record_len(sidecar);
12978 let left_offset = header.index_root_records_offset as usize + left * record_len;
12979 let right_offset = header.index_root_records_offset as usize + right * record_len;
12980 for idx in 0..record_len {
12981 sidecar.swap(left_offset + idx, right_offset + idx);
12982 }
12983 }
12984
12985 fn sidecar_record_len(sidecar: &[u8]) -> usize {
12986 let header =
12987 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12988 let footer_offset = header.manifest_footer_offset as usize;
12989 let footer =
12990 ManifestFooter::parse(&sidecar[footer_offset..footer_offset + MANIFEST_FOOTER_LEN])
12991 .unwrap();
12992 let index_record_count = footer.index_root_data_block_count as usize
12993 + footer.index_root_parity_block_count as usize;
12994 header.index_root_records_length as usize / index_record_count
12995 }
12996
12997 fn corrupt_object_extent_records(volume: &mut [u8], extent: ObjectExtent) {
12998 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
12999 assert_eq!(volume_header.volume_index, 0);
13000 assert_eq!(volume_header.stripe_width, 1);
13001 let crypto_start = volume_header.crypto_header_offset as usize;
13002 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
13003 let crypto_header = CryptoHeader::parse(
13004 &volume[crypto_start..crypto_end],
13005 volume_header.crypto_header_length,
13006 )
13007 .unwrap();
13008 let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
13009 let record_count = extent.data_block_count as u64 + extent.parity_block_count as u64;
13010 for offset in 0..record_count {
13011 let block_index = extent.first_block_index + offset;
13012 let record_offset = crypto_end + block_index as usize * record_len;
13013 volume[record_offset + 16] ^= 0x55;
13014 }
13015 }
13016
13017 fn terminal_material_offset(volume: &[u8]) -> usize {
13018 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
13019 let crypto_start = volume_header.crypto_header_offset as usize;
13020 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
13021 let crypto_header = CryptoHeader::parse(
13022 &volume[crypto_start..crypto_end],
13023 volume_header.crypto_header_length,
13024 )
13025 .unwrap();
13026 let (_, offset, _) = parse_stream_block_prefix(
13027 volume,
13028 crypto_end,
13029 crypto_header.fixed.block_size as usize,
13030 &volume_header,
13031 )
13032 .unwrap();
13033 offset
13034 }
13035
13036 #[derive(Debug)]
13037 struct TestObject {
13038 extent: ObjectExtent,
13039 records: Vec<BlockRecord>,
13040 }
13041
13042 #[derive(Debug)]
13043 struct TestFileMeta {
13044 path: Vec<u8>,
13045 frame_index: u64,
13046 tar_stream_offset: u64,
13047 member_group_size: u64,
13048 file_data_size: u64,
13049 }
13050
13051 fn multi_envelope_reader_fixture() -> (OpenedArchive, u64) {
13052 let volume_header = test_volume_header();
13053 let crypto_header = test_crypto_header();
13054 let subkeys = Subkeys::derive(
13055 &master_key(),
13056 &volume_header.archive_uuid,
13057 &volume_header.session_id,
13058 )
13059 .unwrap();
13060 let mut next_block_index = 0u64;
13061 let mut blocks = BTreeMap::new();
13062
13063 let healthy = test_member(b"healthy.txt", b"healthy payload\n");
13064 let broken = test_member(b"broken.txt", b"broken payload\n");
13065 let tar_stream = [healthy.as_slice(), broken.as_slice()].concat();
13066
13067 let healthy_frame = compress_zstd_frame(&healthy, 1).unwrap();
13068 let broken_frame = compress_zstd_frame(&broken, 1).unwrap();
13069
13070 let healthy_payload = encrypt_test_object(
13071 &healthy_frame,
13072 &subkeys.enc_key,
13073 &subkeys.nonce_seed,
13074 b"envelope",
13075 0,
13076 BlockKind::PayloadData,
13077 &mut next_block_index,
13078 &crypto_header,
13079 &volume_header,
13080 );
13081 let broken_payload = encrypt_test_object(
13082 &broken_frame,
13083 &subkeys.enc_key,
13084 &subkeys.nonce_seed,
13085 b"envelope",
13086 1,
13087 BlockKind::PayloadData,
13088 &mut next_block_index,
13089 &crypto_header,
13090 &volume_header,
13091 );
13092 let broken_payload_block = broken_payload.extent.first_block_index;
13093 insert_records(&mut blocks, &healthy_payload.records);
13094 insert_records(&mut blocks, &broken_payload.records);
13095
13096 let frames = vec![
13097 FrameEntry {
13098 frame_index: 0,
13099 envelope_index: 0,
13100 offset_in_envelope: 0,
13101 compressed_size: healthy_frame.len() as u32,
13102 decompressed_size: healthy.len() as u32,
13103 flags: 0x0000_0003,
13104 tar_stream_offset: 0,
13105 },
13106 FrameEntry {
13107 frame_index: 1,
13108 envelope_index: 1,
13109 offset_in_envelope: 0,
13110 compressed_size: broken_frame.len() as u32,
13111 decompressed_size: broken.len() as u32,
13112 flags: 0x0000_0003,
13113 tar_stream_offset: healthy.len() as u64,
13114 },
13115 ];
13116 let envelopes = vec![
13117 EnvelopeEntry {
13118 envelope_index: 0,
13119 first_block_index: healthy_payload.extent.first_block_index,
13120 data_block_count: healthy_payload.extent.data_block_count,
13121 parity_block_count: 0,
13122 encrypted_size: healthy_payload.extent.encrypted_size,
13123 plaintext_size: healthy_frame.len() as u32,
13124 first_frame_index: 0,
13125 frame_count: 1,
13126 },
13127 EnvelopeEntry {
13128 envelope_index: 1,
13129 first_block_index: broken_payload.extent.first_block_index,
13130 data_block_count: broken_payload.extent.data_block_count,
13131 parity_block_count: 0,
13132 encrypted_size: broken_payload.extent.encrypted_size,
13133 plaintext_size: broken_frame.len() as u32,
13134 first_frame_index: 1,
13135 frame_count: 1,
13136 },
13137 ];
13138 let files = vec![
13139 TestFileMeta {
13140 path: b"healthy.txt".to_vec(),
13141 frame_index: 0,
13142 tar_stream_offset: 0,
13143 member_group_size: healthy.len() as u64,
13144 file_data_size: b"healthy payload\n".len() as u64,
13145 },
13146 TestFileMeta {
13147 path: b"broken.txt".to_vec(),
13148 frame_index: 1,
13149 tar_stream_offset: healthy.len() as u64,
13150 member_group_size: broken.len() as u64,
13151 file_data_size: b"broken payload\n".len() as u64,
13152 },
13153 ];
13154
13155 let (index_shard_plaintext, first_path_hash, last_path_hash) =
13156 build_test_index_shard(&files, &frames, &envelopes);
13157 let index_shard = encrypt_test_object(
13158 &compress_zstd_frame(&index_shard_plaintext, 1).unwrap(),
13159 &subkeys.index_shard_key,
13160 &subkeys.index_nonce_seed,
13161 b"idxshard",
13162 0,
13163 BlockKind::IndexShardData,
13164 &mut next_block_index,
13165 &crypto_header,
13166 &volume_header,
13167 );
13168 insert_records(&mut blocks, &index_shard.records);
13169
13170 let shard_entry = ShardEntry {
13171 shard_index: 0,
13172 first_block_index: index_shard.extent.first_block_index,
13173 data_block_count: index_shard.extent.data_block_count,
13174 parity_block_count: 0,
13175 encrypted_size: index_shard.extent.encrypted_size,
13176 decompressed_size: index_shard_plaintext.len() as u32,
13177 file_count: files.len() as u32,
13178 first_path_hash,
13179 last_path_hash,
13180 };
13181 let mut root_header = IndexRootHeader::empty();
13182 root_header.frame_count = frames.len() as u64;
13183 root_header.envelope_count = envelopes.len() as u64;
13184 root_header.file_count = files.len() as u64;
13185 root_header.payload_block_count = healthy_payload.extent.data_block_count as u64
13186 + broken_payload.extent.data_block_count as u64;
13187 root_header.tar_total_size = tar_stream.len() as u64;
13188 root_header.content_sha256 = sha256_bytes(&tar_stream);
13189 let index_root = IndexRoot {
13190 header: root_header,
13191 shards: vec![shard_entry],
13192 directory_hint_shards: Vec::new(),
13193 };
13194
13195 let index_root_plaintext = index_root.to_bytes();
13196 let index_root_object = encrypt_test_object(
13197 &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
13198 &subkeys.index_root_key,
13199 &subkeys.index_nonce_seed,
13200 b"idxroot",
13201 0,
13202 BlockKind::IndexRootData,
13203 &mut next_block_index,
13204 &crypto_header,
13205 &volume_header,
13206 );
13207 insert_records(&mut blocks, &index_root_object.records);
13208
13209 let archive_uuid = volume_header.archive_uuid;
13210 let session_id = volume_header.session_id;
13211 let opened = OpenedArchive {
13212 options: ReaderOptions::default(),
13213 observed_archive_bytes: 1_000_000,
13214 observed_volume_count: 1,
13215 subkeys,
13216 blocks,
13217 lazy_blocks: None,
13218 crypto_header_bytes: Vec::new(),
13219 volume_header,
13220 crypto_header,
13221 manifest_footer: ManifestFooter {
13222 archive_uuid,
13223 session_id,
13224 volume_index: 0,
13225 is_authoritative: 1,
13226 total_volumes: 1,
13227 index_root_first_block: index_root_object.extent.first_block_index,
13228 index_root_data_block_count: index_root_object.extent.data_block_count,
13229 index_root_parity_block_count: 0,
13230 index_root_encrypted_size: index_root_object.extent.encrypted_size,
13231 index_root_decompressed_size: index_root_plaintext.len() as u32,
13232 manifest_hmac: [0u8; 32],
13233 },
13234 volume_trailer: Some(VolumeTrailer {
13235 archive_uuid,
13236 session_id,
13237 volume_index: 0,
13238 block_count: next_block_index,
13239 bytes_written: 0,
13240 manifest_footer_offset: 0,
13241 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
13242 closed_at_ns: 0,
13243 root_auth_footer_offset: 0,
13244 root_auth_footer_length: 0,
13245 root_auth_flags: 0,
13246 trailer_hmac: [0u8; 32],
13247 }),
13248 root_auth_footer: None,
13249 index_root,
13250 payload_dictionary: None,
13251 };
13252 (opened, broken_payload_block)
13253 }
13254
13255 fn replace_first_index_shard(opened: &mut OpenedArchive, mutate: impl FnOnce(&mut IndexShard)) {
13256 let locating = opened.index_root.shards[0].clone();
13257 let mut shard = opened.load_index_shard(&locating).unwrap();
13258 mutate(&mut shard);
13259 let plaintext = shard.to_bytes();
13260 let mut next_block_index = opened
13261 .blocks
13262 .keys()
13263 .last()
13264 .copied()
13265 .map(|index| index + 1)
13266 .unwrap_or(0);
13267 let replacement = encrypt_test_object(
13268 &compress_zstd_frame(&plaintext, 1).unwrap(),
13269 &opened.subkeys.index_shard_key,
13270 &opened.subkeys.index_nonce_seed,
13271 b"idxshard",
13272 locating.shard_index,
13273 BlockKind::IndexShardData,
13274 &mut next_block_index,
13275 &opened.crypto_header,
13276 &opened.volume_header,
13277 );
13278 insert_records(&mut opened.blocks, &replacement.records);
13279 opened.index_root.shards[0] = ShardEntry {
13280 shard_index: locating.shard_index,
13281 first_block_index: replacement.extent.first_block_index,
13282 data_block_count: replacement.extent.data_block_count,
13283 parity_block_count: 0,
13284 encrypted_size: replacement.extent.encrypted_size,
13285 decompressed_size: plaintext.len() as u32,
13286 file_count: shard.files.len() as u32,
13287 first_path_hash: shard.files.first().unwrap().path_hash,
13288 last_path_hash: shard.files.last().unwrap().path_hash,
13289 };
13290 }
13291
13292 fn rewrite_as_single_healthy_file(
13293 opened: &mut OpenedArchive,
13294 mutate: impl FnOnce(&mut FileEntry, &mut Vec<u8>),
13295 ) {
13296 let healthy_path = b"healthy.txt";
13297 let healthy_payload = b"healthy payload\n";
13298 let healthy_member = test_member(healthy_path, healthy_payload);
13299 replace_first_index_shard(opened, |shard| {
13300 let file_index = (0..shard.files.len())
13301 .find(|idx| shard.file_path(*idx) == Some(healthy_path.as_slice()))
13302 .unwrap();
13303 let mut file = shard.files[file_index].clone();
13304 let frame = shard
13305 .frames
13306 .iter()
13307 .find(|entry| entry.frame_index == 0)
13308 .unwrap()
13309 .clone();
13310 let envelope = shard
13311 .envelopes
13312 .iter()
13313 .find(|entry| entry.envelope_index == 0)
13314 .unwrap()
13315 .clone();
13316 let mut path = healthy_path.to_vec();
13317
13318 file.path_offset = 0;
13319 file.path_length = path.len() as u32;
13320 file.first_frame_index = 0;
13321 file.frame_count = 1;
13322 file.offset_in_first_frame_plaintext = 0;
13323 file.tar_member_group_size = healthy_member.len() as u64;
13324 file.file_data_size = healthy_payload.len() as u64;
13325 file.flags = 0;
13326 mutate(&mut file, &mut path);
13327 file.path_offset = 0;
13328 file.path_length = path.len() as u32;
13329 file.path_hash = hash_prefix(&path);
13330
13331 shard.files = vec![file];
13332 shard.frames = vec![frame];
13333 shard.envelopes = vec![envelope];
13334 shard.string_pool = path;
13335 });
13336
13337 opened.index_root.header.file_count = 1;
13338 opened.index_root.header.frame_count = 1;
13339 opened.index_root.header.envelope_count = 1;
13340 opened.index_root.header.payload_block_count = 1;
13341 opened.index_root.header.tar_total_size = healthy_member.len() as u64;
13342 opened.index_root.header.content_sha256 = sha256_bytes(&healthy_member);
13343 }
13344
13345 fn test_volume_header() -> VolumeHeader {
13346 VolumeHeader {
13347 format_version: FORMAT_VERSION,
13348 volume_format_rev: VOLUME_FORMAT_REV,
13349 volume_index: 0,
13350 stripe_width: 1,
13351 archive_uuid: [0x31; 16],
13352 session_id: [0x42; 16],
13353 crypto_header_offset: VOLUME_HEADER_LEN as u32,
13354 crypto_header_length: CRYPTO_HEADER_FIXED_LEN as u32,
13355 header_crc32c: 0,
13356 }
13357 }
13358
13359 fn test_crypto_header() -> CryptoHeaderFixed {
13360 CryptoHeaderFixed {
13361 length: CRYPTO_HEADER_FIXED_LEN as u32,
13362 compression_algo: CompressionAlgo::ZstdFramed,
13363 aead_algo: AeadAlgo::AesGcmSiv256,
13364 fec_algo: FecAlgo::ReedSolomonGF16,
13365 kdf_algo: KdfAlgo::Raw,
13366 chunk_size: 4096,
13367 envelope_target_size: 8192,
13368 block_size: 4096,
13369 fec_data_shards: 4,
13370 fec_parity_shards: 0,
13371 index_fec_data_shards: 4,
13372 index_fec_parity_shards: 0,
13373 index_root_fec_data_shards: 4,
13374 index_root_fec_parity_shards: 0,
13375 stripe_width: 1,
13376 volume_loss_tolerance: 0,
13377 bit_rot_buffer_pct: 0,
13378 has_dictionary: 0,
13379 max_path_length: 4096,
13380 expected_volume_size: 0,
13381 }
13382 }
13383
13384 #[allow(clippy::too_many_arguments)]
13385 fn encrypt_test_object(
13386 plaintext: &[u8],
13387 key: &[u8; 32],
13388 nonce_seed: &[u8; 32],
13389 domain: &[u8],
13390 counter: u64,
13391 data_kind: BlockKind,
13392 next_block_index: &mut u64,
13393 crypto_header: &CryptoHeaderFixed,
13394 volume_header: &VolumeHeader,
13395 ) -> TestObject {
13396 let block_size = crypto_header.block_size as usize;
13397 let encrypted = encrypt_padded_aead_object(
13398 AeadObjectContext {
13399 algo: crypto_header.aead_algo,
13400 key,
13401 nonce_seed,
13402 domain,
13403 archive_uuid: &volume_header.archive_uuid,
13404 session_id: &volume_header.session_id,
13405 counter,
13406 },
13407 block_size,
13408 plaintext,
13409 )
13410 .unwrap();
13411 assert_eq!(encrypted.len() % block_size, 0);
13412
13413 let first_block_index = *next_block_index;
13414 let data_block_count = encrypted.len() / block_size;
13415 let records = encrypted
13416 .chunks(block_size)
13417 .enumerate()
13418 .map(|(index, payload)| BlockRecord {
13419 block_index: first_block_index + index as u64,
13420 kind: data_kind,
13421 flags: if index + 1 == data_block_count {
13422 0x01
13423 } else {
13424 0
13425 },
13426 payload: payload.to_vec(),
13427 record_crc32c: 0,
13428 })
13429 .collect::<Vec<_>>();
13430 *next_block_index += data_block_count as u64;
13431
13432 TestObject {
13433 extent: ObjectExtent {
13434 first_block_index,
13435 data_block_count: data_block_count as u32,
13436 parity_block_count: 0,
13437 encrypted_size: encrypted.len() as u32,
13438 },
13439 records,
13440 }
13441 }
13442
13443 fn insert_records(blocks: &mut BTreeMap<u64, BlockRecord>, records: &[BlockRecord]) {
13444 for record in records {
13445 assert!(blocks.insert(record.block_index, record.clone()).is_none());
13446 }
13447 }
13448
13449 #[allow(clippy::too_many_arguments)]
13450 fn build_metadata_object_from_payload(
13451 payload: &[u8],
13452 _subkeys: &Subkeys,
13453 volume_header: &VolumeHeader,
13454 crypto_header: &CryptoHeaderFixed,
13455 key: &[u8; 32],
13456 nonce_seed: &[u8; 32],
13457 domain: &[u8],
13458 counter: u64,
13459 data_kind: BlockKind,
13460 next_block_index: &mut u64,
13461 ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
13462 let compressed = compress_zstd_frame(payload, 1).unwrap();
13463 build_metadata_object_from_compressed(
13464 &compressed,
13465 key,
13466 nonce_seed,
13467 domain,
13468 counter,
13469 data_kind,
13470 next_block_index,
13471 crypto_header,
13472 volume_header,
13473 )
13474 }
13475
13476 #[allow(clippy::too_many_arguments)]
13477 fn build_metadata_object_from_compressed(
13478 compressed: &[u8],
13479 key: &[u8; 32],
13480 nonce_seed: &[u8; 32],
13481 domain: &[u8],
13482 counter: u64,
13483 data_kind: BlockKind,
13484 next_block_index: &mut u64,
13485 crypto_header: &CryptoHeaderFixed,
13486 volume_header: &VolumeHeader,
13487 ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
13488 let object = encrypt_test_object(
13489 compressed,
13490 key,
13491 nonce_seed,
13492 domain,
13493 counter,
13494 data_kind,
13495 next_block_index,
13496 crypto_header,
13497 volume_header,
13498 );
13499
13500 let mut blocks = BTreeMap::new();
13501 for record in object.records {
13502 blocks.insert(record.block_index, record);
13503 }
13504 (object.extent, blocks)
13505 }
13506
13507 #[allow(clippy::too_many_arguments)]
13508 fn assert_metadata_object_from_compressed(
13509 compressed: &[u8],
13510 decompressed_size: usize,
13511 _subkeys: &Subkeys,
13512 volume_header: &VolumeHeader,
13513 crypto_header: &CryptoHeaderFixed,
13514 key: &[u8; 32],
13515 nonce_seed: &[u8; 32],
13516 domain: &[u8],
13517 counter: u64,
13518 data_kind: BlockKind,
13519 parity_kind: BlockKind,
13520 class_data_shards: u16,
13521 class_parity_shards: u16,
13522 next_block_index: &mut u64,
13523 expected: FormatError,
13524 ) {
13525 let (extent, blocks) = build_metadata_object_from_compressed(
13526 compressed,
13527 key,
13528 nonce_seed,
13529 domain,
13530 counter,
13531 data_kind,
13532 next_block_index,
13533 crypto_header,
13534 volume_header,
13535 );
13536 let error = load_metadata_object_from_parts(
13537 &blocks,
13538 ObjectLoadContext {
13539 volume_header,
13540 crypto_header,
13541 extent,
13542 data_kind,
13543 parity_kind,
13544 key,
13545 nonce_seed,
13546 domain,
13547 counter,
13548 class_data_shard_max: class_data_shards,
13549 class_parity_shard_max: class_parity_shards,
13550 },
13551 decompressed_size as u32,
13552 )
13553 .unwrap_err();
13554 assert_eq!(error, expected);
13555 }
13556
13557 fn corrupt_payload_record(blocks: &mut BTreeMap<u64, BlockRecord>, block_index: u64) {
13558 let record = blocks.get_mut(&block_index).unwrap();
13559 assert_eq!(record.kind, BlockKind::PayloadData);
13560 record.payload[0] ^= 0x55;
13561 }
13562
13563 fn build_test_index_shard(
13564 files: &[TestFileMeta],
13565 frames: &[FrameEntry],
13566 envelopes: &[EnvelopeEntry],
13567 ) -> (Vec<u8>, [u8; 8], [u8; 8]) {
13568 let mut sorted = files
13569 .iter()
13570 .map(|file| (hash_prefix(&file.path), file))
13571 .collect::<Vec<_>>();
13572 sorted.sort_by(|left, right| {
13573 (left.0, left.1.path.as_slice(), left.1.tar_stream_offset).cmp(&(
13574 right.0,
13575 right.1.path.as_slice(),
13576 right.1.tar_stream_offset,
13577 ))
13578 });
13579
13580 let mut string_pool = Vec::new();
13581 let mut file_entries = Vec::with_capacity(sorted.len());
13582 for (path_hash, file) in &sorted {
13583 let path_offset = string_pool.len() as u32;
13584 string_pool.extend_from_slice(&file.path);
13585 file_entries.push(FileEntry {
13586 path_hash: *path_hash,
13587 path_offset,
13588 path_length: file.path.len() as u32,
13589 first_frame_index: file.frame_index,
13590 frame_count: 1,
13591 offset_in_first_frame_plaintext: 0,
13592 tar_member_group_size: file.member_group_size,
13593 file_data_size: file.file_data_size,
13594 flags: 0,
13595 });
13596 }
13597
13598 let header = IndexShardHeader {
13599 version: 1,
13600 shard_index: 0,
13601 file_count: file_entries.len() as u32,
13602 frame_count: frames.len() as u32,
13603 envelope_count: envelopes.len() as u32,
13604 file_table_offset: INDEX_SHARD_HEADER_LEN as u32,
13605 frame_table_offset: (INDEX_SHARD_HEADER_LEN + file_entries.len() * FILE_ENTRY_LEN)
13606 as u32,
13607 envelope_table_offset: (INDEX_SHARD_HEADER_LEN
13608 + file_entries.len() * FILE_ENTRY_LEN
13609 + frames.len() * FRAME_ENTRY_LEN) as u32,
13610 string_pool_offset: (INDEX_SHARD_HEADER_LEN
13611 + file_entries.len() * FILE_ENTRY_LEN
13612 + frames.len() * FRAME_ENTRY_LEN
13613 + envelopes.len() * ENVELOPE_ENTRY_LEN) as u32,
13614 string_pool_size: string_pool.len() as u32,
13615 };
13616
13617 let mut bytes = Vec::new();
13618 bytes.extend_from_slice(&header.to_bytes());
13619 for entry in &file_entries {
13620 bytes.extend_from_slice(&entry.to_bytes());
13621 }
13622 for entry in frames {
13623 bytes.extend_from_slice(&entry.to_bytes());
13624 }
13625 for entry in envelopes {
13626 bytes.extend_from_slice(&entry.to_bytes());
13627 }
13628 bytes.extend_from_slice(&string_pool);
13629
13630 (bytes, sorted.first().unwrap().0, sorted.last().unwrap().0)
13631 }
13632
13633 fn test_member(path: &[u8], data: &[u8]) -> Vec<u8> {
13634 let mut out = Vec::new();
13635 out.extend_from_slice(&test_tar_header(path, data.len() as u64));
13636 out.extend_from_slice(data);
13637 out.resize(out.len() + padding_to_512(data.len()), 0);
13638 out
13639 }
13640
13641 fn test_tar_header(path: &[u8], size: u64) -> [u8; 512] {
13642 let mut header = [0u8; 512];
13643 header[..path.len()].copy_from_slice(path);
13644 write_test_tar_octal(&mut header[100..108], 0o644);
13645 write_test_tar_octal(&mut header[108..116], 0);
13646 write_test_tar_octal(&mut header[116..124], 0);
13647 write_test_tar_octal(&mut header[124..136], size);
13648 write_test_tar_octal(&mut header[136..148], 0);
13649 header[148..156].fill(b' ');
13650 header[156] = b'0';
13651 header[257..263].copy_from_slice(b"ustar\0");
13652 header[263..265].copy_from_slice(b"00");
13653 let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
13654 write_test_tar_checksum(&mut header[148..156], checksum);
13655 header
13656 }
13657
13658 fn write_test_tar_octal(field: &mut [u8], value: u64) {
13659 let digits = format!("{value:o}");
13660 field.fill(0);
13661 let start = field.len() - 1 - digits.len();
13662 field[..start].fill(b'0');
13663 field[start..start + digits.len()].copy_from_slice(digits.as_bytes());
13664 }
13665
13666 fn write_test_tar_checksum(field: &mut [u8], value: u64) {
13667 let digits = format!("{value:06o}");
13668 field[0..6].copy_from_slice(digits.as_bytes());
13669 field[6] = 0;
13670 field[7] = b' ';
13671 }
13672
13673 fn padding_to_512(len: usize) -> usize {
13674 let remainder = len % 512;
13675 if remainder == 0 {
13676 0
13677 } else {
13678 512 - remainder
13679 }
13680 }
13681}