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
3107struct ParsedOpenPrefix {
3108 volume_header: VolumeHeader,
3109 crypto_header: CryptoHeaderFixed,
3110 crypto_header_bytes: Vec<u8>,
3111 crypto_end: usize,
3112 subkeys: Subkeys,
3113}
3114
3115struct ParsedReadAtOpenPrefix {
3116 volume_header: VolumeHeader,
3117 crypto_header: CryptoHeaderFixed,
3118 crypto_header_bytes: Vec<u8>,
3119 crypto_end: u64,
3120 subkeys: Subkeys,
3121}
3122
3123fn parse_seekable_volume(
3124 bytes: &[u8],
3125 master_key: &MasterKey,
3126 options: ReaderOptions,
3127) -> Result<ParsedSeekableVolume, FormatError> {
3128 if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
3129 return Err(FormatError::InvalidLength {
3130 structure: "archive",
3131 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
3132 actual: bytes.len(),
3133 });
3134 }
3135
3136 let prefix = match parse_open_prefix(bytes, master_key) {
3137 Ok(prefix) => prefix,
3138 Err(prefix_err) => {
3139 return parse_seekable_volume_from_recovered_terminal(bytes, master_key, options)
3140 .or(Err(prefix_err));
3141 }
3142 };
3143 let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
3144 match parse_seekable_volume_with_prefix(bytes, prefix, options) {
3145 Ok(parsed) => Ok(parsed),
3146 Err(prefix_err) => {
3147 match parse_seekable_volume_from_recovered_terminal(bytes, master_key, options) {
3148 Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
3149 Ok(recovered)
3150 }
3151 Ok(_) | Err(_) => Err(prefix_err),
3152 }
3153 }
3154 }
3155}
3156
3157fn parse_seekable_volume_with_prefix(
3158 bytes: &[u8],
3159 prefix: ParsedOpenPrefix,
3160 options: ReaderOptions,
3161) -> Result<ParsedSeekableVolume, FormatError> {
3162 let ParsedOpenPrefix {
3163 volume_header,
3164 crypto_header,
3165 crypto_header_bytes,
3166 crypto_end,
3167 subkeys,
3168 } = prefix;
3169 let crypto_bytes = crypto_header_bytes.as_slice();
3170
3171 let terminal = locate_v41_terminal(
3172 bytes,
3173 KeyHoldingTerminalContext {
3174 subkeys: &subkeys,
3175 volume_header: &volume_header,
3176 crypto_header: &crypto_header,
3177 crypto_header_bytes: crypto_bytes,
3178 },
3179 options,
3180 )?;
3181 finish_parse_seekable_volume(
3182 bytes,
3183 volume_header,
3184 crypto_header,
3185 crypto_header_bytes,
3186 crypto_end,
3187 subkeys,
3188 terminal,
3189 )
3190}
3191
3192fn parse_seekable_volume_from_recovered_terminal(
3193 bytes: &[u8],
3194 master_key: &MasterKey,
3195 options: ReaderOptions,
3196) -> Result<ParsedSeekableVolume, FormatError> {
3197 let authority = locate_v41_terminal_authority(bytes, master_key, options)?;
3198 let crypto_end = checked_add(
3199 authority.volume_header.crypto_header_offset as usize,
3200 authority.volume_header.crypto_header_length as usize,
3201 "CryptoHeader",
3202 )?;
3203 finish_parse_seekable_volume(
3204 bytes,
3205 authority.volume_header,
3206 authority.crypto_header,
3207 authority.crypto_header_bytes,
3208 crypto_end,
3209 authority.subkeys,
3210 authority.terminal,
3211 )
3212}
3213
3214fn parse_open_prefix(
3215 bytes: &[u8],
3216 master_key: &MasterKey,
3217) -> Result<ParsedOpenPrefix, FormatError> {
3218 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
3219 let crypto_start = volume_header.crypto_header_offset as usize;
3220 let crypto_len = volume_header.crypto_header_length as usize;
3221 let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
3222 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
3223 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
3224 let subkeys = subkeys_for_open(
3225 Some(master_key),
3226 parsed_crypto.fixed.aead_algo,
3227 &volume_header.archive_uuid,
3228 &volume_header.session_id,
3229 )?;
3230 verify_integrity_tag(
3231 HmacDomain::CryptoHeader,
3232 parsed_crypto.fixed.aead_algo,
3233 Some(&subkeys.mac_key),
3234 &volume_header.archive_uuid,
3235 &volume_header.session_id,
3236 parsed_crypto.hmac_covered_bytes,
3237 &parsed_crypto.header_hmac,
3238 )?;
3239 parsed_crypto.validate_extension_semantics()?;
3240 validate_seekable_supported_volume(
3241 &volume_header,
3242 &parsed_crypto.fixed,
3243 &parsed_crypto.extensions,
3244 )?;
3245 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
3246 let crypto_header = parsed_crypto.fixed.clone();
3247 Ok(ParsedOpenPrefix {
3248 volume_header,
3249 crypto_header,
3250 crypto_header_bytes: crypto_bytes.to_vec(),
3251 crypto_end,
3252 subkeys,
3253 })
3254}
3255
3256fn finish_parse_seekable_volume(
3257 bytes: &[u8],
3258 volume_header: VolumeHeader,
3259 crypto_header: CryptoHeaderFixed,
3260 crypto_header_bytes: Vec<u8>,
3261 crypto_end: usize,
3262 subkeys: Subkeys,
3263 terminal: V41Terminal,
3264) -> Result<ParsedSeekableVolume, FormatError> {
3265 let trailer_offset = to_usize(terminal.image.volume_trailer_offset, "VolumeTrailer")?;
3266 let volume_trailer = terminal.volume_trailer.clone();
3267 validate_trailer_identity(&volume_header, &volume_trailer)?;
3268
3269 let manifest_offset = to_usize(volume_trailer.manifest_footer_offset, "ManifestFooter")?;
3270 let manifest_end = checked_add(manifest_offset, MANIFEST_FOOTER_LEN, "ManifestFooter")?;
3271 if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
3272 if to_usize(volume_trailer.root_auth_footer_offset, "RootAuthFooter")? != manifest_end
3273 || volume_trailer
3274 .root_auth_footer_offset
3275 .checked_add(volume_trailer.root_auth_footer_length as u64)
3276 .ok_or(FormatError::InvalidArchive(
3277 "RootAuthFooter terminal boundary overflow",
3278 ))?
3279 != trailer_offset as u64
3280 {
3281 return Err(FormatError::InvalidArchive(
3282 "RootAuthFooter does not sit before selected trailer",
3283 ));
3284 }
3285 } else if manifest_end != trailer_offset {
3286 return Err(FormatError::InvalidArchive(
3287 "ManifestFooter does not end at selected trailer",
3288 ));
3289 }
3290 let manifest_bytes = &terminal.manifest_footer_bytes;
3291 let (manifest_footer, manifest_footer_error) =
3292 match parse_valid_manifest_footer(&volume_header, &crypto_header, &subkeys, manifest_bytes)
3293 {
3294 Ok(footer) => (Some(footer), None),
3295 Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
3296 Err(err) => return Err(err),
3297 };
3298
3299 let block_region = parse_block_region(
3300 bytes,
3301 crypto_end,
3302 manifest_offset,
3303 crypto_header.block_size as usize,
3304 &volume_header,
3305 &volume_trailer,
3306 )?;
3307
3308 Ok(ParsedSeekableVolume {
3309 volume_header,
3310 crypto_header,
3311 crypto_header_bytes,
3312 subkeys,
3313 manifest_footer,
3314 manifest_footer_error,
3315 root_auth_footer: terminal.root_auth_footer,
3316 root_auth_footer_bytes: terminal.root_auth_footer_bytes,
3317 volume_trailer,
3318 blocks: block_region.blocks,
3319 erased_block_indices: block_region.erased_block_indices,
3320 })
3321}
3322
3323fn parse_seekable_read_at_volume(
3324 reader: Arc<dyn ArchiveReadAt>,
3325 master_key: &MasterKey,
3326 options: ReaderOptions,
3327) -> Result<ParsedSeekableReadAtVolume, FormatError> {
3328 let observed_len = reader.len()?;
3329 if observed_len < (VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN) as u64 {
3330 return Err(FormatError::InvalidLength {
3331 structure: "archive",
3332 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
3333 actual: to_usize(observed_len, "archive")?,
3334 });
3335 }
3336
3337 let prefix = match parse_read_at_open_prefix(reader.as_ref(), master_key) {
3338 Ok(prefix) => prefix,
3339 Err(prefix_err) => {
3340 return parse_seekable_read_at_volume_from_recovered_terminal(
3341 reader,
3342 observed_len,
3343 master_key,
3344 options,
3345 )
3346 .or(Err(prefix_err));
3347 }
3348 };
3349 let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
3350 match parse_seekable_read_at_volume_with_prefix(reader.clone(), observed_len, prefix, options) {
3351 Ok(parsed) => Ok(parsed),
3352 Err(prefix_err) => match parse_seekable_read_at_volume_from_recovered_terminal(
3353 reader,
3354 observed_len,
3355 master_key,
3356 options,
3357 ) {
3358 Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
3359 Ok(recovered)
3360 }
3361 Ok(_) | Err(_) => Err(prefix_err),
3362 },
3363 }
3364}
3365
3366fn parse_seekable_read_at_volume_with_prefix(
3367 reader: Arc<dyn ArchiveReadAt>,
3368 observed_len: u64,
3369 prefix: ParsedReadAtOpenPrefix,
3370 options: ReaderOptions,
3371) -> Result<ParsedSeekableReadAtVolume, FormatError> {
3372 let ParsedReadAtOpenPrefix {
3373 volume_header,
3374 crypto_header,
3375 crypto_header_bytes,
3376 crypto_end,
3377 subkeys,
3378 } = prefix;
3379
3380 let terminal = locate_v41_terminal_read_at(
3381 reader.as_ref(),
3382 observed_len,
3383 KeyHoldingTerminalContext {
3384 subkeys: &subkeys,
3385 volume_header: &volume_header,
3386 crypto_header: &crypto_header,
3387 crypto_header_bytes: &crypto_header_bytes,
3388 },
3389 options,
3390 )?;
3391 finish_parse_seekable_read_at_volume(
3392 reader,
3393 volume_header,
3394 crypto_header,
3395 crypto_header_bytes,
3396 crypto_end,
3397 subkeys,
3398 terminal,
3399 )
3400}
3401
3402fn parse_seekable_read_at_volume_from_recovered_terminal(
3403 reader: Arc<dyn ArchiveReadAt>,
3404 observed_len: u64,
3405 master_key: &MasterKey,
3406 options: ReaderOptions,
3407) -> Result<ParsedSeekableReadAtVolume, FormatError> {
3408 let authority =
3409 locate_v41_terminal_authority_read_at(reader.as_ref(), observed_len, master_key, options)?;
3410 let crypto_end = checked_u64_add(
3411 authority.volume_header.crypto_header_offset as u64,
3412 authority.volume_header.crypto_header_length as u64,
3413 "CryptoHeader",
3414 )?;
3415 finish_parse_seekable_read_at_volume(
3416 reader,
3417 authority.volume_header,
3418 authority.crypto_header,
3419 authority.crypto_header_bytes,
3420 crypto_end,
3421 authority.subkeys,
3422 authority.terminal,
3423 )
3424}
3425
3426fn parse_read_at_open_prefix(
3427 reader: &dyn ArchiveReadAt,
3428 master_key: &MasterKey,
3429) -> Result<ParsedReadAtOpenPrefix, FormatError> {
3430 let volume_header_bytes = read_at_vec(reader, 0, VOLUME_HEADER_LEN, "archive")?;
3431 let volume_header = VolumeHeader::parse(&volume_header_bytes)?;
3432 let crypto_start = volume_header.crypto_header_offset as u64;
3433 let crypto_len = volume_header.crypto_header_length as u64;
3434 let crypto_end = checked_u64_add(crypto_start, crypto_len, "CryptoHeader")?;
3435 let crypto_bytes = read_at_vec(
3436 reader,
3437 crypto_start,
3438 to_usize(crypto_len, "CryptoHeader")?,
3439 "CryptoHeader",
3440 )?;
3441 let parsed_crypto = CryptoHeader::parse(&crypto_bytes, volume_header.crypto_header_length)?;
3442 let subkeys = subkeys_for_open(
3443 Some(master_key),
3444 parsed_crypto.fixed.aead_algo,
3445 &volume_header.archive_uuid,
3446 &volume_header.session_id,
3447 )?;
3448 verify_integrity_tag(
3449 HmacDomain::CryptoHeader,
3450 parsed_crypto.fixed.aead_algo,
3451 Some(&subkeys.mac_key),
3452 &volume_header.archive_uuid,
3453 &volume_header.session_id,
3454 parsed_crypto.hmac_covered_bytes,
3455 &parsed_crypto.header_hmac,
3456 )?;
3457 parsed_crypto.validate_extension_semantics()?;
3458 validate_seekable_supported_volume(
3459 &volume_header,
3460 &parsed_crypto.fixed,
3461 &parsed_crypto.extensions,
3462 )?;
3463 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
3464 let crypto_header = parsed_crypto.fixed.clone();
3465 drop(parsed_crypto);
3466 Ok(ParsedReadAtOpenPrefix {
3467 volume_header,
3468 crypto_header,
3469 crypto_header_bytes: crypto_bytes,
3470 crypto_end,
3471 subkeys,
3472 })
3473}
3474
3475fn finish_parse_seekable_read_at_volume(
3476 reader: Arc<dyn ArchiveReadAt>,
3477 volume_header: VolumeHeader,
3478 crypto_header: CryptoHeaderFixed,
3479 crypto_header_bytes: Vec<u8>,
3480 crypto_end: u64,
3481 subkeys: Subkeys,
3482 terminal: V41Terminal,
3483) -> Result<ParsedSeekableReadAtVolume, FormatError> {
3484 let volume_trailer = terminal.volume_trailer.clone();
3485 validate_trailer_identity(&volume_header, &volume_trailer)?;
3486
3487 let manifest_offset = volume_trailer.manifest_footer_offset;
3488 let manifest_end = checked_u64_add(
3489 manifest_offset,
3490 MANIFEST_FOOTER_LEN as u64,
3491 "ManifestFooter",
3492 )?;
3493 if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
3494 if volume_trailer.root_auth_footer_offset != manifest_end
3495 || volume_trailer
3496 .root_auth_footer_offset
3497 .checked_add(volume_trailer.root_auth_footer_length as u64)
3498 .ok_or(FormatError::InvalidArchive(
3499 "RootAuthFooter terminal boundary overflow",
3500 ))?
3501 != terminal.image.volume_trailer_offset
3502 {
3503 return Err(FormatError::InvalidArchive(
3504 "RootAuthFooter does not sit before selected trailer",
3505 ));
3506 }
3507 } else if manifest_end != terminal.image.volume_trailer_offset {
3508 return Err(FormatError::InvalidArchive(
3509 "ManifestFooter does not end at selected trailer",
3510 ));
3511 }
3512 validate_seekable_block_region_layout(
3513 crypto_end,
3514 manifest_offset,
3515 crypto_header.block_size as usize,
3516 &volume_trailer,
3517 )?;
3518
3519 let manifest_bytes = &terminal.manifest_footer_bytes;
3520 let (manifest_footer, manifest_footer_error) =
3521 match parse_valid_manifest_footer(&volume_header, &crypto_header, &subkeys, manifest_bytes)
3522 {
3523 Ok(footer) => (Some(footer), None),
3524 Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
3525 Err(err) => return Err(err),
3526 };
3527
3528 Ok(ParsedSeekableReadAtVolume {
3529 reader,
3530 volume_header,
3531 crypto_header,
3532 crypto_header_bytes,
3533 subkeys,
3534 manifest_footer,
3535 manifest_footer_error,
3536 root_auth_footer: terminal.root_auth_footer,
3537 root_auth_footer_bytes: terminal.root_auth_footer_bytes,
3538 volume_trailer,
3539 crypto_end,
3540 })
3541}
3542
3543#[derive(Debug)]
3544struct ParsedPublicNoKeyVolume {
3545 volume_header: VolumeHeader,
3546 crypto_header: CryptoHeaderFixed,
3547 root_auth_footer: RootAuthFooterV1,
3548 root_auth_footer_bytes: Vec<u8>,
3549 blocks: BTreeMap<u64, BlockRecord>,
3550}
3551
3552pub fn public_no_key_verify_volumes_with_options<F>(
3553 volumes: &[&[u8]],
3554 mut verifier: F,
3555 options: ReaderOptions,
3556) -> Result<PublicNoKeyVerification, FormatError>
3557where
3558 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
3559{
3560 validate_reader_options(options)?;
3561 if volumes.is_empty() {
3562 return Err(FormatError::InvalidArchive("no volumes supplied"));
3563 }
3564 let mut parsed = Vec::with_capacity(volumes.len());
3565 for volume in volumes {
3566 parsed.push(parse_public_no_key_volume(volume, options)?);
3567 }
3568 let first = parsed
3569 .first()
3570 .ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
3571 if parsed.len() != first.crypto_header.stripe_width as usize {
3572 return Err(FormatError::ReaderUnsupported(
3573 "public no-key verification requires a complete volume set",
3574 ));
3575 }
3576
3577 let mut seen_volume_indexes = BTreeSet::new();
3578 let mut blocks = BTreeMap::new();
3579 for volume in &parsed {
3580 if volume.volume_header.archive_uuid != first.volume_header.archive_uuid
3581 || volume.volume_header.session_id != first.volume_header.session_id
3582 || !public_crypto_headers_agree(&volume.crypto_header, &first.crypto_header)
3583 {
3584 return Err(FormatError::InvalidArchive(
3585 "public no-key volume global metadata differs",
3586 ));
3587 }
3588 if volume.root_auth_footer_bytes != first.root_auth_footer_bytes {
3589 return Err(FormatError::InvalidArchive(
3590 "public no-key RootAuthFooter copies differ",
3591 ));
3592 }
3593 if !seen_volume_indexes.insert(volume.volume_header.volume_index) {
3594 return Err(FormatError::InvalidArchive(
3595 "duplicate public no-key volume index",
3596 ));
3597 }
3598 for (block_index, record) in &volume.blocks {
3599 if blocks.insert(*block_index, record.clone()).is_some() {
3600 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
3601 }
3602 }
3603 }
3604 validate_complete_global_block_coverage(&blocks, &BTreeSet::new())?;
3605
3606 let footer = &first.root_auth_footer;
3607 let mut data_leaves = blocks
3608 .values()
3609 .filter(|record| record.kind.is_data())
3610 .map(|record| DataBlockMerkleLeaf {
3611 block_index: record.block_index,
3612 kind: record.kind,
3613 flags: record.flags,
3614 payload: record.payload.clone(),
3615 })
3616 .collect::<Vec<_>>();
3617 data_leaves.sort_by_key(|leaf| leaf.block_index);
3618 let total_data_block_count = u64::try_from(data_leaves.len())
3619 .map_err(|_| FormatError::InvalidArchive("public no-key data block count overflow"))?;
3620 let observed_data_root = data_block_merkle_root(&data_leaves);
3621 if total_data_block_count != footer.total_data_block_count
3622 || observed_data_root != footer.data_block_merkle_root
3623 {
3624 return Err(FormatError::InvalidArchive(
3625 "public no-key data-block commitment mismatch",
3626 ));
3627 }
3628 let archive_root = recompute_public_archive_root(footer, &first.crypto_header)?;
3629 if archive_root != footer.archive_root {
3630 return Err(FormatError::InvalidArchive(
3631 "public no-key archive_root mismatch",
3632 ));
3633 }
3634 if !verifier(footer, &archive_root)? {
3635 return Err(FormatError::InvalidArchive(
3636 "public no-key authenticator verification failed",
3637 ));
3638 }
3639 Ok(PublicNoKeyVerification {
3640 archive_root,
3641 authenticator_id: footer.authenticator_id,
3642 signer_identity_type: footer.signer_identity_type,
3643 signer_identity_bytes: footer.signer_identity_bytes.clone(),
3644 total_data_block_count,
3645 diagnostics: vec![
3646 PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
3647 PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
3648 PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
3649 ],
3650 })
3651}
3652
3653fn parse_public_no_key_volume(
3654 bytes: &[u8],
3655 options: ReaderOptions,
3656) -> Result<ParsedPublicNoKeyVolume, FormatError> {
3657 if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
3658 return Err(FormatError::InvalidLength {
3659 structure: "archive",
3660 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
3661 actual: bytes.len(),
3662 });
3663 }
3664 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
3665 let crypto_start = volume_header.crypto_header_offset as usize;
3666 let crypto_len = volume_header.crypto_header_length as usize;
3667 let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
3668 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
3669 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
3670 parsed_crypto.validate_extension_semantics()?;
3671 validate_seekable_supported_volume(
3672 &volume_header,
3673 &parsed_crypto.fixed,
3674 &parsed_crypto.extensions,
3675 )?;
3676 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
3677
3678 let terminal =
3679 locate_v41_public_terminal(bytes, &volume_header, &parsed_crypto.fixed, options)?;
3680 let block_region = parse_public_block_observation(
3681 bytes,
3682 crypto_end,
3683 &terminal.image,
3684 parsed_crypto.fixed.block_size as usize,
3685 &volume_header,
3686 )?;
3687 Ok(ParsedPublicNoKeyVolume {
3688 volume_header,
3689 crypto_header: parsed_crypto.fixed,
3690 root_auth_footer: terminal.root_auth_footer,
3691 root_auth_footer_bytes: terminal.root_auth_footer_bytes,
3692 blocks: block_region,
3693 })
3694}
3695
3696fn public_crypto_headers_agree(left: &CryptoHeaderFixed, right: &CryptoHeaderFixed) -> bool {
3697 left.length == right.length
3698 && left.stripe_width == right.stripe_width
3699 && left.block_size == right.block_size
3700 && left.compression_algo == right.compression_algo
3701 && left.aead_algo == right.aead_algo
3702 && left.fec_algo == right.fec_algo
3703 && left.kdf_algo == right.kdf_algo
3704}
3705
3706fn recompute_public_archive_root(
3707 footer: &RootAuthFooterV1,
3708 crypto_header: &CryptoHeaderFixed,
3709) -> Result<[u8; 32], FormatError> {
3710 let descriptor_digest = root_auth_descriptor_digest(
3711 footer.authenticator_id,
3712 footer.signer_identity_type,
3713 &footer.signer_identity_bytes,
3714 u32::try_from(footer.authenticator_value.len()).map_err(|_| {
3715 FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
3716 })?,
3717 footer.footer_length()?,
3718 )?;
3719 let signer_digest =
3720 signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
3721 if signer_digest != footer.signer_identity_digest {
3722 return Err(FormatError::InvalidArchive(
3723 "public no-key signer identity digest mismatch",
3724 ));
3725 }
3726 Ok(archive_root(ArchiveRootInputs {
3727 archive_uuid: footer.archive_uuid,
3728 session_id: footer.session_id,
3729 format_version: FORMAT_VERSION,
3730 volume_format_rev: VOLUME_FORMAT_REV,
3731 compression_algo: crypto_header.compression_algo,
3732 aead_algo: crypto_header.aead_algo,
3733 fec_algo: crypto_header.fec_algo,
3734 kdf_algo: crypto_header.kdf_algo,
3735 critical_metadata_digest: footer.critical_metadata_digest,
3736 index_digest: footer.index_digest,
3737 fec_layout_digest: footer.fec_layout_digest,
3738 total_data_block_count: footer.total_data_block_count,
3739 data_block_merkle_root: footer.data_block_merkle_root,
3740 root_auth_descriptor_digest: descriptor_digest,
3741 signer_identity_digest: signer_digest,
3742 }))
3743}
3744
3745fn parse_valid_manifest_footer(
3746 volume_header: &VolumeHeader,
3747 crypto_header: &CryptoHeaderFixed,
3748 subkeys: &Subkeys,
3749 manifest_bytes: &[u8],
3750) -> Result<ManifestFooter, FormatError> {
3751 let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
3752 validate_manifest_footer(
3753 volume_header,
3754 crypto_header,
3755 &manifest_footer,
3756 subkeys,
3757 manifest_bytes,
3758 )?;
3759 manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
3760 Ok(manifest_footer)
3761}
3762
3763fn manifest_footer_copy_error_is_recoverable(error: &FormatError) -> bool {
3764 matches!(
3765 error,
3766 FormatError::BadMagic {
3767 structure: "ManifestFooter",
3768 } | FormatError::NonZeroReserved {
3769 structure: "ManifestFooter",
3770 } | FormatError::InvalidAuthoritativeFlag(_)
3771 | FormatError::HmacMismatch {
3772 structure: "ManifestFooter",
3773 }
3774 | FormatError::IntegrityDigestMismatch {
3775 structure: "ManifestFooter",
3776 }
3777 )
3778}
3779
3780fn validate_seekable_supported_volume(
3781 volume_header: &VolumeHeader,
3782 crypto_header: &CryptoHeaderFixed,
3783 extensions: &[ExtensionTlv<'_>],
3784) -> Result<(), FormatError> {
3785 reject_unsupported_raw_stream_profile(extensions)?;
3786 if crypto_header.stripe_width != volume_header.stripe_width {
3787 return Err(FormatError::InvalidArchive(
3788 "VolumeHeader and CryptoHeader stripe_width differ",
3789 ));
3790 }
3791 Ok(())
3792}
3793
3794pub(crate) fn validate_crypto_class_parity_exactness(
3795 crypto_header: &CryptoHeaderFixed,
3796) -> Result<(), FormatError> {
3797 let fec = required_object_parity(crypto_header.fec_data_shards as u64, crypto_header)?;
3798 if crypto_header.fec_parity_shards as u32 != fec {
3799 return Err(FormatError::InvalidArchive(
3800 "fec_parity_shards does not match v41 compute_parity",
3801 ));
3802 }
3803 let index = required_object_parity(crypto_header.index_fec_data_shards as u64, crypto_header)?;
3804 if crypto_header.index_fec_parity_shards as u32 != index {
3805 return Err(FormatError::InvalidArchive(
3806 "index_fec_parity_shards does not match v41 compute_parity",
3807 ));
3808 }
3809 let index_root = required_object_parity(
3810 crypto_header.index_root_fec_data_shards as u64,
3811 crypto_header,
3812 )?;
3813 if crypto_header.index_root_fec_parity_shards as u32 != index_root {
3814 return Err(FormatError::InvalidArchive(
3815 "index_root_fec_parity_shards does not match v41 compute_parity",
3816 ));
3817 }
3818 Ok(())
3819}
3820
3821fn validate_volume_set_member(
3822 first: &ParsedSeekableVolume,
3823 candidate: &ParsedSeekableVolume,
3824) -> Result<(), FormatError> {
3825 validate_volume_set_member_metadata(
3826 &first.volume_header,
3827 &first.crypto_header,
3828 &first.crypto_header_bytes,
3829 &candidate.volume_header,
3830 &candidate.crypto_header,
3831 &candidate.crypto_header_bytes,
3832 )
3833}
3834
3835fn validate_volume_set_member_metadata(
3836 first_volume_header: &VolumeHeader,
3837 first_crypto_header: &CryptoHeaderFixed,
3838 first_crypto_header_bytes: &[u8],
3839 candidate_volume_header: &VolumeHeader,
3840 candidate_crypto_header: &CryptoHeaderFixed,
3841 candidate_crypto_header_bytes: &[u8],
3842) -> Result<(), FormatError> {
3843 if candidate_volume_header.archive_uuid != first_volume_header.archive_uuid
3844 || candidate_volume_header.session_id != first_volume_header.session_id
3845 {
3846 return Err(FormatError::InvalidArchive(
3847 "mixed archive or session IDs in volume set",
3848 ));
3849 }
3850 if candidate_crypto_header_bytes != first_crypto_header_bytes
3851 || candidate_crypto_header != first_crypto_header
3852 {
3853 return Err(FormatError::InvalidArchive("CryptoHeader copies differ"));
3854 }
3855 Ok(())
3856}
3857
3858pub(crate) fn manifest_bootstrap_fields_match(
3859 left: &ManifestFooter,
3860 right: &ManifestFooter,
3861) -> bool {
3862 left.archive_uuid == right.archive_uuid
3863 && left.session_id == right.session_id
3864 && left.is_authoritative == right.is_authoritative
3865 && left.total_volumes == right.total_volumes
3866 && left.index_root_first_block == right.index_root_first_block
3867 && left.index_root_data_block_count == right.index_root_data_block_count
3868 && left.index_root_parity_block_count == right.index_root_parity_block_count
3869 && left.index_root_encrypted_size == right.index_root_encrypted_size
3870 && left.index_root_decompressed_size == right.index_root_decompressed_size
3871}
3872
3873fn validate_complete_global_block_coverage(
3874 blocks: &BTreeMap<u64, BlockRecord>,
3875 erased_block_indices: &BTreeSet<u64>,
3876) -> Result<(), FormatError> {
3877 let mut expected = 0u64;
3878 let mut block_iter = blocks.keys().copied().peekable();
3879 let mut erasure_iter = erased_block_indices.iter().copied().peekable();
3880
3881 loop {
3882 let next_block = block_iter.peek().copied();
3883 let next_erasure = erasure_iter.peek().copied();
3884 let next = match (next_block, next_erasure) {
3885 (Some(block), Some(erasure)) if block == erasure => {
3886 return Err(FormatError::InvalidArchive(
3887 "BlockRecord index is both present and erased",
3888 ));
3889 }
3890 (Some(block), Some(erasure)) => block.min(erasure),
3891 (Some(block), None) => block,
3892 (None, Some(erasure)) => erasure,
3893 (None, None) => return Ok(()),
3894 };
3895
3896 if next != expected {
3897 return Err(FormatError::InvalidArchive(
3898 "complete volume set has missing global blocks",
3899 ));
3900 }
3901 if next_block == Some(next) {
3902 block_iter.next();
3903 }
3904 if next_erasure == Some(next) {
3905 erasure_iter.next();
3906 }
3907 expected = expected
3908 .checked_add(1)
3909 .ok_or(FormatError::InvalidArchive("global block index overflow"))?;
3910 }
3911}
3912
3913#[derive(Debug)]
3914struct V41Terminal {
3915 image: CriticalMetadataImage,
3916 manifest_footer_bytes: Vec<u8>,
3917 root_auth_footer_bytes: Option<Vec<u8>>,
3918 root_auth_footer: Option<RootAuthFooterV1>,
3919 volume_trailer: VolumeTrailer,
3920}
3921
3922pub(crate) struct SequentialTerminalMaterial {
3923 pub(crate) manifest_footer: ManifestFooter,
3924 pub(crate) volume_trailer: VolumeTrailer,
3925 pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
3926}
3927
3928#[derive(Debug)]
3929struct V41PublicTerminal {
3930 image: CriticalMetadataImage,
3931 root_auth_footer_bytes: Vec<u8>,
3932 root_auth_footer: RootAuthFooterV1,
3933}
3934
3935#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3936struct CmraDecoderTuple {
3937 shard_size: u32,
3938 data_shard_count: u16,
3939 parity_shard_count: u16,
3940 image_length: u32,
3941 image_sha256: [u8; 32],
3942}
3943
3944#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3945struct CmraIdentityHints {
3946 archive_uuid: [u8; 16],
3947 session_id: [u8; 16],
3948 volume_index: u32,
3949}
3950
3951impl From<CriticalMetadataRecoveryHeader> for CmraDecoderTuple {
3952 fn from(header: CriticalMetadataRecoveryHeader) -> Self {
3953 Self {
3954 shard_size: header.shard_size,
3955 data_shard_count: header.data_shard_count,
3956 parity_shard_count: header.parity_shard_count,
3957 image_length: header.image_length,
3958 image_sha256: header.image_sha256,
3959 }
3960 }
3961}
3962
3963impl From<CriticalMetadataRecoveryHeader> for CmraIdentityHints {
3964 fn from(header: CriticalMetadataRecoveryHeader) -> Self {
3965 Self {
3966 archive_uuid: header.archive_uuid_hint,
3967 session_id: header.session_id_hint,
3968 volume_index: header.volume_index_hint,
3969 }
3970 }
3971}
3972
3973impl From<CriticalRecoveryLocator> for CmraDecoderTuple {
3974 fn from(locator: CriticalRecoveryLocator) -> Self {
3975 Self {
3976 shard_size: locator.cmra_shard_size,
3977 data_shard_count: locator.cmra_data_shard_count,
3978 parity_shard_count: locator.cmra_parity_shard_count,
3979 image_length: locator.cmra_image_length,
3980 image_sha256: locator.cmra_image_sha256,
3981 }
3982 }
3983}
3984
3985impl From<CriticalRecoveryLocator> for CmraIdentityHints {
3986 fn from(locator: CriticalRecoveryLocator) -> Self {
3987 Self {
3988 archive_uuid: locator.archive_uuid_hint,
3989 session_id: locator.session_id_hint,
3990 volume_index: locator.volume_index_hint,
3991 }
3992 }
3993}
3994
3995#[derive(Debug)]
3996struct RecoveredCmra {
3997 image: CriticalMetadataImage,
3998 tuple: CmraDecoderTuple,
3999 header_hints: Option<CmraIdentityHints>,
4000 cmra_length: u64,
4001}
4002
4003#[derive(Debug)]
4004struct TerminalCandidate {
4005 terminal: V41Terminal,
4006 anchor: usize,
4007 locator_sequence: Option<u32>,
4008 cmra_offset: u64,
4009 cmra_length: u64,
4010}
4011
4012#[derive(Debug)]
4013struct PublicTerminalCandidate {
4014 terminal: V41PublicTerminal,
4015 anchor: usize,
4016 cmra_offset: u64,
4017 cmra_length: u64,
4018}
4019
4020#[derive(Debug)]
4021struct RecoveredTerminalAuthority {
4022 terminal: V41Terminal,
4023 volume_header: VolumeHeader,
4024 crypto_header: CryptoHeaderFixed,
4025 crypto_header_bytes: Vec<u8>,
4026 subkeys: Subkeys,
4027}
4028
4029#[derive(Debug)]
4030struct TerminalAuthorityCandidate {
4031 authority: RecoveredTerminalAuthority,
4032 anchor: usize,
4033 cmra_offset: u64,
4034 cmra_length: u64,
4035}
4036
4037#[derive(Debug, Clone, Copy)]
4038enum CmraRecoveryMode {
4039 KeyHolding,
4040 PublicNoKey,
4041}
4042
4043#[derive(Clone, Copy)]
4044pub(crate) struct KeyHoldingTerminalContext<'a> {
4045 pub(crate) subkeys: &'a Subkeys,
4046 pub(crate) volume_header: &'a VolumeHeader,
4047 pub(crate) crypto_header: &'a CryptoHeaderFixed,
4048 pub(crate) crypto_header_bytes: &'a [u8],
4049}
4050
4051fn locate_v41_terminal(
4052 bytes: &[u8],
4053 context: KeyHoldingTerminalContext<'_>,
4054 options: ReaderOptions,
4055) -> Result<V41Terminal, FormatError> {
4056 locate_v41_terminal_candidate(bytes, context, options).map(|candidate| candidate.terminal)
4057}
4058
4059fn locate_v41_terminal_read_at(
4060 reader: &dyn ArchiveReadAt,
4061 len: u64,
4062 context: KeyHoldingTerminalContext<'_>,
4063 options: ReaderOptions,
4064) -> Result<V41Terminal, FormatError> {
4065 let mut candidates = Vec::new();
4066 if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
4067 let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
4068 collect_v41_locator_candidate_read_at(reader, final_offset, 0, context, &mut candidates);
4069 }
4070 if len >= LOCATOR_PAIR_LEN as u64 {
4071 let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
4072 collect_v41_locator_candidate_read_at(reader, mirror_offset, 1, context, &mut candidates);
4073 }
4074
4075 if candidates.is_empty() {
4076 let scan = max_critical_recovery_scan(options)? as u64;
4077 let scan_start = len.saturating_sub(scan);
4078 let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
4079 let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
4080 let mut offset = tail.len().saturating_sub(4);
4081 while offset < tail.len() {
4082 let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
4083 if tail.get(offset..offset + 4) == Some(b"TZCL") {
4084 collect_v41_locator_candidate_read_at(
4085 reader,
4086 absolute_offset,
4087 2,
4088 context,
4089 &mut candidates,
4090 );
4091 } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
4092 if let Ok(candidate) =
4093 parse_locatorless_cmra_candidate_read_at(reader, absolute_offset, context)
4094 {
4095 candidates.push(candidate);
4096 }
4097 }
4098 if offset == 0 {
4099 break;
4100 }
4101 offset -= 1;
4102 }
4103 }
4104
4105 choose_v41_terminal_candidate(candidates).map(|candidate| candidate.terminal)
4106}
4107
4108fn locate_v41_terminal_authority(
4109 bytes: &[u8],
4110 master_key: &MasterKey,
4111 options: ReaderOptions,
4112) -> Result<RecoveredTerminalAuthority, FormatError> {
4113 let mut candidates = Vec::new();
4114 if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
4115 let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
4116 collect_v41_locator_authority_candidate(
4117 bytes,
4118 final_offset,
4119 0,
4120 master_key,
4121 &mut candidates,
4122 );
4123 }
4124 if bytes.len() >= LOCATOR_PAIR_LEN {
4125 let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
4126 collect_v41_locator_authority_candidate(
4127 bytes,
4128 mirror_offset,
4129 1,
4130 master_key,
4131 &mut candidates,
4132 );
4133 }
4134
4135 if candidates.is_empty() {
4136 let scan = max_critical_recovery_scan(options)?;
4137 let scan_start = bytes.len().saturating_sub(scan);
4138 let mut offset = bytes.len().saturating_sub(4);
4139 while offset >= scan_start {
4140 if bytes.get(offset..offset + 4) == Some(b"TZCL") {
4141 collect_v41_locator_authority_candidate(
4142 bytes,
4143 offset,
4144 2,
4145 master_key,
4146 &mut candidates,
4147 );
4148 } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
4149 if let Ok(candidate) =
4150 parse_locatorless_cmra_authority_candidate(bytes, offset, master_key)
4151 {
4152 candidates.push(candidate);
4153 }
4154 }
4155 if offset == 0 {
4156 break;
4157 }
4158 offset -= 1;
4159 }
4160 }
4161
4162 choose_v41_terminal_authority_candidate(candidates).map(|candidate| candidate.authority)
4163}
4164
4165fn locate_v41_terminal_authority_read_at(
4166 reader: &dyn ArchiveReadAt,
4167 len: u64,
4168 master_key: &MasterKey,
4169 options: ReaderOptions,
4170) -> Result<RecoveredTerminalAuthority, FormatError> {
4171 let mut candidates = Vec::new();
4172 if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
4173 let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
4174 collect_v41_locator_authority_candidate_read_at(
4175 reader,
4176 final_offset,
4177 0,
4178 master_key,
4179 &mut candidates,
4180 );
4181 }
4182 if len >= LOCATOR_PAIR_LEN as u64 {
4183 let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
4184 collect_v41_locator_authority_candidate_read_at(
4185 reader,
4186 mirror_offset,
4187 1,
4188 master_key,
4189 &mut candidates,
4190 );
4191 }
4192
4193 if candidates.is_empty() {
4194 let scan = max_critical_recovery_scan(options)? as u64;
4195 let scan_start = len.saturating_sub(scan);
4196 let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
4197 let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
4198 let mut offset = tail.len().saturating_sub(4);
4199 while offset < tail.len() {
4200 let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
4201 if tail.get(offset..offset + 4) == Some(b"TZCL") {
4202 collect_v41_locator_authority_candidate_read_at(
4203 reader,
4204 absolute_offset,
4205 2,
4206 master_key,
4207 &mut candidates,
4208 );
4209 } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
4210 if let Ok(candidate) = parse_locatorless_cmra_authority_candidate_read_at(
4211 reader,
4212 absolute_offset,
4213 master_key,
4214 ) {
4215 candidates.push(candidate);
4216 }
4217 }
4218 if offset == 0 {
4219 break;
4220 }
4221 offset -= 1;
4222 }
4223 }
4224
4225 choose_v41_terminal_authority_candidate(candidates).map(|candidate| candidate.authority)
4226}
4227
4228fn locate_v41_terminal_candidate(
4229 bytes: &[u8],
4230 context: KeyHoldingTerminalContext<'_>,
4231 options: ReaderOptions,
4232) -> Result<TerminalCandidate, FormatError> {
4233 let mut candidates = Vec::new();
4234 if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
4235 let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
4236 collect_v41_locator_candidate(bytes, final_offset, 0, context, &mut candidates);
4237 }
4238 if bytes.len() >= LOCATOR_PAIR_LEN {
4239 let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
4240 collect_v41_locator_candidate(bytes, mirror_offset, 1, context, &mut candidates);
4241 }
4242
4243 if candidates.is_empty() {
4244 let scan = max_critical_recovery_scan(options)?;
4245 let scan_start = bytes.len().saturating_sub(scan);
4246 let mut offset = bytes.len().saturating_sub(4);
4247 while offset >= scan_start {
4248 if bytes.get(offset..offset + 4) == Some(b"TZCL") {
4249 collect_v41_locator_candidate(bytes, offset, 2, context, &mut candidates);
4250 } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
4251 if let Ok(candidate) = parse_locatorless_cmra_candidate(bytes, offset, context) {
4252 candidates.push(candidate);
4253 }
4254 }
4255 if offset == 0 {
4256 break;
4257 }
4258 offset -= 1;
4259 }
4260 }
4261
4262 choose_v41_terminal_candidate(candidates)
4263}
4264
4265fn locate_v41_public_terminal(
4266 bytes: &[u8],
4267 volume_header: &VolumeHeader,
4268 crypto_header: &CryptoHeaderFixed,
4269 options: ReaderOptions,
4270) -> Result<V41PublicTerminal, FormatError> {
4271 let mut candidates = Vec::new();
4272 if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
4273 let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
4274 collect_v41_public_locator_candidate(
4275 bytes,
4276 final_offset,
4277 0,
4278 volume_header,
4279 crypto_header,
4280 &mut candidates,
4281 );
4282 }
4283 if bytes.len() >= LOCATOR_PAIR_LEN {
4284 let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
4285 collect_v41_public_locator_candidate(
4286 bytes,
4287 mirror_offset,
4288 1,
4289 volume_header,
4290 crypto_header,
4291 &mut candidates,
4292 );
4293 }
4294
4295 if candidates.is_empty() {
4296 let scan = max_critical_recovery_scan(options)?;
4297 let scan_start = bytes.len().saturating_sub(scan);
4298 let mut offset = bytes.len().saturating_sub(4);
4299 while offset >= scan_start {
4300 if bytes.get(offset..offset + 4) == Some(b"TZCL") {
4301 collect_v41_public_locator_candidate(
4302 bytes,
4303 offset,
4304 2,
4305 volume_header,
4306 crypto_header,
4307 &mut candidates,
4308 );
4309 } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
4310 if let Ok(candidate) = parse_public_locatorless_cmra_candidate(
4311 bytes,
4312 offset,
4313 volume_header,
4314 crypto_header,
4315 ) {
4316 candidates.push(candidate);
4317 }
4318 }
4319 if offset == 0 {
4320 break;
4321 }
4322 offset -= 1;
4323 }
4324 }
4325
4326 choose_v41_public_terminal_candidate(candidates).map(|candidate| candidate.terminal)
4327}
4328
4329fn collect_v41_locator_candidate(
4330 bytes: &[u8],
4331 offset: usize,
4332 expected_sequence: u32,
4333 context: KeyHoldingTerminalContext<'_>,
4334 candidates: &mut Vec<TerminalCandidate>,
4335) {
4336 let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
4337 return;
4338 };
4339 let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
4340 return;
4341 };
4342 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4343 return;
4344 }
4345 if let Ok(candidate) = parse_locator_cmra_candidate(bytes, offset, locator, context) {
4346 candidates.push(candidate);
4347 }
4348}
4349
4350fn collect_v41_locator_candidate_read_at(
4351 reader: &dyn ArchiveReadAt,
4352 offset: u64,
4353 expected_sequence: u32,
4354 context: KeyHoldingTerminalContext<'_>,
4355 candidates: &mut Vec<TerminalCandidate>,
4356) {
4357 let Ok(raw) = read_at_vec(
4358 reader,
4359 offset,
4360 CRITICAL_RECOVERY_LOCATOR_LEN,
4361 "CriticalRecoveryLocator",
4362 ) else {
4363 return;
4364 };
4365 let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
4366 return;
4367 };
4368 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4369 return;
4370 }
4371 if let Ok(candidate) = parse_locator_cmra_candidate_read_at(reader, offset, locator, context) {
4372 candidates.push(candidate);
4373 }
4374}
4375
4376fn collect_v41_locator_authority_candidate(
4377 bytes: &[u8],
4378 offset: usize,
4379 expected_sequence: u32,
4380 master_key: &MasterKey,
4381 candidates: &mut Vec<TerminalAuthorityCandidate>,
4382) {
4383 let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
4384 return;
4385 };
4386 let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
4387 return;
4388 };
4389 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4390 return;
4391 }
4392 if let Ok(candidate) =
4393 parse_locator_cmra_authority_candidate(bytes, offset, locator, master_key)
4394 {
4395 candidates.push(candidate);
4396 }
4397}
4398
4399fn collect_v41_locator_authority_candidate_read_at(
4400 reader: &dyn ArchiveReadAt,
4401 offset: u64,
4402 expected_sequence: u32,
4403 master_key: &MasterKey,
4404 candidates: &mut Vec<TerminalAuthorityCandidate>,
4405) {
4406 let Ok(raw) = read_at_vec(
4407 reader,
4408 offset,
4409 CRITICAL_RECOVERY_LOCATOR_LEN,
4410 "CriticalRecoveryLocator",
4411 ) else {
4412 return;
4413 };
4414 let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
4415 return;
4416 };
4417 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4418 return;
4419 }
4420 if let Ok(candidate) =
4421 parse_locator_cmra_authority_candidate_read_at(reader, offset, locator, master_key)
4422 {
4423 candidates.push(candidate);
4424 }
4425}
4426
4427fn collect_v41_public_locator_candidate(
4428 bytes: &[u8],
4429 offset: usize,
4430 expected_sequence: u32,
4431 volume_header: &VolumeHeader,
4432 crypto_header: &CryptoHeaderFixed,
4433 candidates: &mut Vec<PublicTerminalCandidate>,
4434) {
4435 let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
4436 return;
4437 };
4438 let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
4439 return;
4440 };
4441 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4442 return;
4443 }
4444 if let Ok(candidate) =
4445 parse_public_locator_cmra_candidate(bytes, offset, locator, volume_header, crypto_header)
4446 {
4447 candidates.push(candidate);
4448 }
4449}
4450
4451fn choose_v41_terminal_candidate(
4452 mut candidates: Vec<TerminalCandidate>,
4453) -> Result<TerminalCandidate, FormatError> {
4454 candidates.sort_by_key(|candidate| candidate.anchor);
4455 let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
4456 "no valid v41 CMRA candidate found",
4457 ))?;
4458 if let Some(previous) = candidates.last() {
4459 if previous.anchor == winner.anchor
4460 && (previous.cmra_offset != winner.cmra_offset
4461 || previous.cmra_length != winner.cmra_length)
4462 {
4463 return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
4464 }
4465 }
4466 Ok(winner)
4467}
4468
4469fn choose_v41_terminal_authority_candidate(
4470 mut candidates: Vec<TerminalAuthorityCandidate>,
4471) -> Result<TerminalAuthorityCandidate, FormatError> {
4472 candidates.sort_by_key(|candidate| candidate.anchor);
4473 let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
4474 "no valid v41 CMRA candidate found",
4475 ))?;
4476 if let Some(previous) = candidates.last() {
4477 if previous.anchor == winner.anchor
4478 && (previous.cmra_offset != winner.cmra_offset
4479 || previous.cmra_length != winner.cmra_length)
4480 {
4481 return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
4482 }
4483 }
4484 Ok(winner)
4485}
4486
4487fn choose_v41_public_terminal_candidate(
4488 mut candidates: Vec<PublicTerminalCandidate>,
4489) -> Result<PublicTerminalCandidate, FormatError> {
4490 candidates.sort_by_key(|candidate| candidate.anchor);
4491 let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
4492 "no valid v41 public CMRA candidate found",
4493 ))?;
4494 if let Some(previous) = candidates.last() {
4495 if previous.anchor == winner.anchor
4496 && (previous.cmra_offset != winner.cmra_offset
4497 || previous.cmra_length != winner.cmra_length)
4498 {
4499 return Err(FormatError::InvalidArchive(
4500 "ambiguous v41 public CMRA candidates",
4501 ));
4502 }
4503 }
4504 Ok(winner)
4505}
4506
4507fn parse_locator_cmra_candidate(
4508 bytes: &[u8],
4509 locator_offset: usize,
4510 locator: CriticalRecoveryLocator,
4511 context: KeyHoldingTerminalContext<'_>,
4512) -> Result<TerminalCandidate, FormatError> {
4513 let tuple = CmraDecoderTuple::from(locator);
4514 validate_cmra_decoder_tuple(tuple)?;
4515 let expected_cmra_length = cmra_serialized_length(tuple)?;
4516 if locator.cmra_length as u64 != expected_cmra_length {
4517 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4518 }
4519 validate_locator_position(locator_offset, locator)?;
4520 let recovered = recover_cmra(
4521 bytes,
4522 locator.cmra_offset,
4523 Some(tuple),
4524 CmraRecoveryMode::KeyHolding,
4525 )?;
4526 if recovered.tuple != tuple {
4527 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4528 }
4529 if expected_cmra_length != recovered.cmra_length {
4530 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4531 }
4532 validate_locator_image_boundary(locator, &recovered.image)?;
4533 validate_cmra_identity_hints(
4534 recovered.header_hints,
4535 Some(CmraIdentityHints::from(locator)),
4536 &recovered.image,
4537 )?;
4538 let terminal =
4539 validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context, false)?;
4540 Ok(TerminalCandidate {
4541 terminal,
4542 anchor: locator_offset
4543 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
4544 .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
4545 locator_sequence: Some(locator.locator_sequence),
4546 cmra_offset: locator.cmra_offset,
4547 cmra_length: recovered.cmra_length,
4548 })
4549}
4550
4551fn parse_locator_cmra_candidate_read_at(
4552 reader: &dyn ArchiveReadAt,
4553 locator_offset: u64,
4554 locator: CriticalRecoveryLocator,
4555 context: KeyHoldingTerminalContext<'_>,
4556) -> Result<TerminalCandidate, FormatError> {
4557 let tuple = CmraDecoderTuple::from(locator);
4558 validate_cmra_decoder_tuple(tuple)?;
4559 let expected_cmra_length = cmra_serialized_length(tuple)?;
4560 if locator.cmra_length as u64 != expected_cmra_length {
4561 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4562 }
4563 validate_locator_position(
4564 to_usize(locator_offset, "CriticalRecoveryLocator")?,
4565 locator,
4566 )?;
4567 let recovered = recover_cmra_read_at(
4568 reader,
4569 locator.cmra_offset,
4570 Some(tuple),
4571 CmraRecoveryMode::KeyHolding,
4572 )?;
4573 if recovered.tuple != tuple {
4574 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4575 }
4576 if expected_cmra_length != recovered.cmra_length {
4577 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4578 }
4579 validate_locator_image_boundary(locator, &recovered.image)?;
4580 validate_cmra_identity_hints(
4581 recovered.header_hints,
4582 Some(CmraIdentityHints::from(locator)),
4583 &recovered.image,
4584 )?;
4585 let terminal = validate_recovered_terminal_read_at(
4586 recovered.image,
4587 recovered.tuple,
4588 reader,
4589 context,
4590 false,
4591 )?;
4592 Ok(TerminalCandidate {
4593 terminal,
4594 anchor: to_usize(
4595 checked_u64_add(
4596 locator_offset,
4597 CRITICAL_RECOVERY_LOCATOR_LEN as u64,
4598 "locator anchor overflow",
4599 )?,
4600 "locator anchor overflow",
4601 )?,
4602 locator_sequence: Some(locator.locator_sequence),
4603 cmra_offset: locator.cmra_offset,
4604 cmra_length: recovered.cmra_length,
4605 })
4606}
4607
4608fn parse_locator_cmra_authority_candidate(
4609 bytes: &[u8],
4610 locator_offset: usize,
4611 locator: CriticalRecoveryLocator,
4612 master_key: &MasterKey,
4613) -> Result<TerminalAuthorityCandidate, FormatError> {
4614 let tuple = CmraDecoderTuple::from(locator);
4615 validate_cmra_decoder_tuple(tuple)?;
4616 let expected_cmra_length = cmra_serialized_length(tuple)?;
4617 if locator.cmra_length as u64 != expected_cmra_length {
4618 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4619 }
4620 validate_locator_position(locator_offset, locator)?;
4621 let recovered = recover_cmra(
4622 bytes,
4623 locator.cmra_offset,
4624 Some(tuple),
4625 CmraRecoveryMode::KeyHolding,
4626 )?;
4627 if recovered.tuple != tuple {
4628 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4629 }
4630 if expected_cmra_length != recovered.cmra_length {
4631 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4632 }
4633 validate_locator_image_boundary(locator, &recovered.image)?;
4634 validate_cmra_identity_hints(
4635 recovered.header_hints,
4636 Some(CmraIdentityHints::from(locator)),
4637 &recovered.image,
4638 )?;
4639 let cmra_length = recovered.cmra_length;
4640 let authority =
4641 validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, false)?;
4642 Ok(TerminalAuthorityCandidate {
4643 authority,
4644 anchor: locator_offset
4645 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
4646 .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
4647 cmra_offset: locator.cmra_offset,
4648 cmra_length,
4649 })
4650}
4651
4652fn parse_locator_cmra_authority_candidate_read_at(
4653 reader: &dyn ArchiveReadAt,
4654 locator_offset: u64,
4655 locator: CriticalRecoveryLocator,
4656 master_key: &MasterKey,
4657) -> Result<TerminalAuthorityCandidate, FormatError> {
4658 let tuple = CmraDecoderTuple::from(locator);
4659 validate_cmra_decoder_tuple(tuple)?;
4660 let expected_cmra_length = cmra_serialized_length(tuple)?;
4661 if locator.cmra_length as u64 != expected_cmra_length {
4662 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4663 }
4664 validate_locator_position(
4665 to_usize(locator_offset, "CriticalRecoveryLocator")?,
4666 locator,
4667 )?;
4668 let recovered = recover_cmra_read_at(
4669 reader,
4670 locator.cmra_offset,
4671 Some(tuple),
4672 CmraRecoveryMode::KeyHolding,
4673 )?;
4674 if recovered.tuple != tuple {
4675 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4676 }
4677 if expected_cmra_length != recovered.cmra_length {
4678 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4679 }
4680 validate_locator_image_boundary(locator, &recovered.image)?;
4681 validate_cmra_identity_hints(
4682 recovered.header_hints,
4683 Some(CmraIdentityHints::from(locator)),
4684 &recovered.image,
4685 )?;
4686 let cmra_length = recovered.cmra_length;
4687 let authority =
4688 validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, false)?;
4689 Ok(TerminalAuthorityCandidate {
4690 authority,
4691 anchor: to_usize(
4692 checked_u64_add(
4693 locator_offset,
4694 CRITICAL_RECOVERY_LOCATOR_LEN as u64,
4695 "locator anchor overflow",
4696 )?,
4697 "locator anchor overflow",
4698 )?,
4699 cmra_offset: locator.cmra_offset,
4700 cmra_length,
4701 })
4702}
4703
4704fn parse_public_locator_cmra_candidate(
4705 bytes: &[u8],
4706 locator_offset: usize,
4707 locator: CriticalRecoveryLocator,
4708 volume_header: &VolumeHeader,
4709 crypto_header: &CryptoHeaderFixed,
4710) -> Result<PublicTerminalCandidate, FormatError> {
4711 let tuple = CmraDecoderTuple::from(locator);
4712 validate_cmra_decoder_tuple(tuple)?;
4713 let expected_cmra_length = cmra_serialized_length(tuple)?;
4714 if locator.cmra_length as u64 != expected_cmra_length {
4715 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4716 }
4717 validate_locator_position(locator_offset, locator)?;
4718 let recovered = recover_cmra(
4719 bytes,
4720 locator.cmra_offset,
4721 Some(tuple),
4722 CmraRecoveryMode::PublicNoKey,
4723 )?;
4724 if recovered.tuple != tuple {
4725 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4726 }
4727 if expected_cmra_length != recovered.cmra_length {
4728 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4729 }
4730 validate_locator_image_boundary(locator, &recovered.image)?;
4731 validate_cmra_identity_hints(
4732 recovered.header_hints,
4733 Some(CmraIdentityHints::from(locator)),
4734 &recovered.image,
4735 )?;
4736 let terminal = validate_recovered_public_terminal(
4737 recovered.image,
4738 bytes,
4739 volume_header,
4740 crypto_header,
4741 false,
4742 )?;
4743 Ok(PublicTerminalCandidate {
4744 terminal,
4745 anchor: locator_offset
4746 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
4747 .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
4748 cmra_offset: locator.cmra_offset,
4749 cmra_length: recovered.cmra_length,
4750 })
4751}
4752
4753fn parse_locatorless_cmra_candidate(
4754 bytes: &[u8],
4755 cmra_offset: usize,
4756 context: KeyHoldingTerminalContext<'_>,
4757) -> Result<TerminalCandidate, FormatError> {
4758 let recovered = recover_cmra(
4759 bytes,
4760 cmra_offset as u64,
4761 None,
4762 CmraRecoveryMode::KeyHolding,
4763 )?;
4764 if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
4765 return Err(FormatError::InvalidArchive(
4766 "locatorless CMRA boundary mismatch",
4767 ));
4768 }
4769 if recovered
4770 .image
4771 .volume_trailer_offset
4772 .checked_add(VOLUME_TRAILER_LEN as u64)
4773 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
4774 != cmra_offset as u64
4775 {
4776 return Err(FormatError::InvalidArchive(
4777 "locatorless trailer boundary mismatch",
4778 ));
4779 }
4780 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
4781 let terminal =
4782 validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context, true)?;
4783 Ok(TerminalCandidate {
4784 terminal,
4785 anchor: cmra_offset
4786 .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
4787 .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
4788 locator_sequence: None,
4789 cmra_offset: cmra_offset as u64,
4790 cmra_length: recovered.cmra_length,
4791 })
4792}
4793
4794fn parse_locatorless_cmra_candidate_read_at(
4795 reader: &dyn ArchiveReadAt,
4796 cmra_offset: u64,
4797 context: KeyHoldingTerminalContext<'_>,
4798) -> Result<TerminalCandidate, FormatError> {
4799 let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
4800 if recovered.image.body_bytes_before_cmra != cmra_offset {
4801 return Err(FormatError::InvalidArchive(
4802 "locatorless CMRA boundary mismatch",
4803 ));
4804 }
4805 if recovered
4806 .image
4807 .volume_trailer_offset
4808 .checked_add(VOLUME_TRAILER_LEN as u64)
4809 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
4810 != cmra_offset
4811 {
4812 return Err(FormatError::InvalidArchive(
4813 "locatorless trailer boundary mismatch",
4814 ));
4815 }
4816 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
4817 let terminal = validate_recovered_terminal_read_at(
4818 recovered.image,
4819 recovered.tuple,
4820 reader,
4821 context,
4822 true,
4823 )?;
4824 Ok(TerminalCandidate {
4825 terminal,
4826 anchor: to_usize(
4827 checked_u64_add(cmra_offset, recovered.cmra_length, "CMRA anchor overflow")?,
4828 "CMRA anchor overflow",
4829 )?,
4830 locator_sequence: None,
4831 cmra_offset,
4832 cmra_length: recovered.cmra_length,
4833 })
4834}
4835
4836fn parse_locatorless_cmra_authority_candidate(
4837 bytes: &[u8],
4838 cmra_offset: usize,
4839 master_key: &MasterKey,
4840) -> Result<TerminalAuthorityCandidate, FormatError> {
4841 let recovered = recover_cmra(
4842 bytes,
4843 cmra_offset as u64,
4844 None,
4845 CmraRecoveryMode::KeyHolding,
4846 )?;
4847 if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
4848 return Err(FormatError::InvalidArchive(
4849 "locatorless CMRA boundary mismatch",
4850 ));
4851 }
4852 if recovered
4853 .image
4854 .volume_trailer_offset
4855 .checked_add(VOLUME_TRAILER_LEN as u64)
4856 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
4857 != cmra_offset as u64
4858 {
4859 return Err(FormatError::InvalidArchive(
4860 "locatorless trailer boundary mismatch",
4861 ));
4862 }
4863 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
4864 let cmra_length = recovered.cmra_length;
4865 let authority =
4866 validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, true)?;
4867 Ok(TerminalAuthorityCandidate {
4868 authority,
4869 anchor: cmra_offset
4870 .checked_add(to_usize(cmra_length, "CMRA")?)
4871 .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
4872 cmra_offset: cmra_offset as u64,
4873 cmra_length,
4874 })
4875}
4876
4877fn parse_locatorless_cmra_authority_candidate_read_at(
4878 reader: &dyn ArchiveReadAt,
4879 cmra_offset: u64,
4880 master_key: &MasterKey,
4881) -> Result<TerminalAuthorityCandidate, FormatError> {
4882 let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
4883 if recovered.image.body_bytes_before_cmra != cmra_offset {
4884 return Err(FormatError::InvalidArchive(
4885 "locatorless CMRA boundary mismatch",
4886 ));
4887 }
4888 if recovered
4889 .image
4890 .volume_trailer_offset
4891 .checked_add(VOLUME_TRAILER_LEN as u64)
4892 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
4893 != cmra_offset
4894 {
4895 return Err(FormatError::InvalidArchive(
4896 "locatorless trailer boundary mismatch",
4897 ));
4898 }
4899 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
4900 let cmra_length = recovered.cmra_length;
4901 let authority =
4902 validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, true)?;
4903 Ok(TerminalAuthorityCandidate {
4904 authority,
4905 anchor: to_usize(
4906 checked_u64_add(cmra_offset, cmra_length, "CMRA anchor overflow")?,
4907 "CMRA anchor overflow",
4908 )?,
4909 cmra_offset,
4910 cmra_length,
4911 })
4912}
4913
4914fn parse_public_locatorless_cmra_candidate(
4915 bytes: &[u8],
4916 cmra_offset: usize,
4917 volume_header: &VolumeHeader,
4918 crypto_header: &CryptoHeaderFixed,
4919) -> Result<PublicTerminalCandidate, FormatError> {
4920 let recovered = recover_cmra(
4921 bytes,
4922 cmra_offset as u64,
4923 None,
4924 CmraRecoveryMode::PublicNoKey,
4925 )?;
4926 if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
4927 return Err(FormatError::InvalidArchive(
4928 "locatorless CMRA boundary mismatch",
4929 ));
4930 }
4931 if recovered
4932 .image
4933 .volume_trailer_offset
4934 .checked_add(VOLUME_TRAILER_LEN as u64)
4935 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
4936 != cmra_offset as u64
4937 {
4938 return Err(FormatError::InvalidArchive(
4939 "locatorless trailer boundary mismatch",
4940 ));
4941 }
4942 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
4943 let terminal = validate_recovered_public_terminal(
4944 recovered.image,
4945 bytes,
4946 volume_header,
4947 crypto_header,
4948 true,
4949 )?;
4950 Ok(PublicTerminalCandidate {
4951 terminal,
4952 anchor: cmra_offset
4953 .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
4954 .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
4955 cmra_offset: cmra_offset as u64,
4956 cmra_length: recovered.cmra_length,
4957 })
4958}
4959
4960fn validate_locator_position(
4961 locator_offset: usize,
4962 locator: CriticalRecoveryLocator,
4963) -> Result<(), FormatError> {
4964 if locator.cmra_offset != locator.body_bytes_before_cmra {
4965 return Err(FormatError::InvalidArchive(
4966 "locator CMRA boundary mismatch",
4967 ));
4968 }
4969 if locator
4970 .volume_trailer_offset
4971 .checked_add(VOLUME_TRAILER_LEN as u64)
4972 .ok_or(FormatError::InvalidArchive("locator trailer overflow"))?
4973 != locator.cmra_offset
4974 {
4975 return Err(FormatError::InvalidArchive(
4976 "locator trailer boundary mismatch",
4977 ));
4978 }
4979 let expected_offset = match locator.locator_sequence {
4980 1 => locator.cmra_offset.checked_add(locator.cmra_length as u64),
4981 0 => locator
4982 .cmra_offset
4983 .checked_add(locator.cmra_length as u64)
4984 .and_then(|value| value.checked_add(CRITICAL_RECOVERY_LOCATOR_LEN as u64)),
4985 _ => None,
4986 }
4987 .ok_or(FormatError::InvalidArchive("locator position overflow"))?;
4988 if expected_offset != locator_offset as u64 {
4989 return Err(FormatError::InvalidArchive(
4990 "locator position does not match sequence",
4991 ));
4992 }
4993 Ok(())
4994}
4995
4996fn validate_locator_image_boundary(
4997 locator: CriticalRecoveryLocator,
4998 image: &CriticalMetadataImage,
4999) -> Result<(), FormatError> {
5000 if locator.volume_trailer_offset != image.volume_trailer_offset
5001 || locator.body_bytes_before_cmra != image.body_bytes_before_cmra
5002 || image
5003 .volume_trailer_offset
5004 .checked_add(VOLUME_TRAILER_LEN as u64)
5005 .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
5006 != locator.cmra_offset
5007 {
5008 return Err(FormatError::InvalidArchive(
5009 "locator and CMRA image boundaries differ",
5010 ));
5011 }
5012 Ok(())
5013}
5014
5015fn validate_cmra_identity_hints(
5016 header_hints: Option<CmraIdentityHints>,
5017 locator_hints: Option<CmraIdentityHints>,
5018 image: &CriticalMetadataImage,
5019) -> Result<(), FormatError> {
5020 if let (Some(header), Some(locator)) = (header_hints, locator_hints) {
5021 if header != locator {
5022 return Err(FormatError::InvalidArchive(
5023 "CMRA header and locator identity hints differ",
5024 ));
5025 }
5026 }
5027 for hints in [header_hints, locator_hints].into_iter().flatten() {
5028 if hints.archive_uuid != image.archive_uuid
5029 || hints.session_id != image.session_id
5030 || hints.volume_index != image.volume_index
5031 {
5032 return Err(FormatError::InvalidArchive(
5033 "CMRA identity hints do not match recovered image",
5034 ));
5035 }
5036 }
5037 Ok(())
5038}
5039
5040fn recover_cmra(
5041 bytes: &[u8],
5042 cmra_offset: u64,
5043 locator_tuple: Option<CmraDecoderTuple>,
5044 mode: CmraRecoveryMode,
5045) -> Result<RecoveredCmra, FormatError> {
5046 let offset = to_usize(cmra_offset, "CMRA")?;
5047 let header_bytes = slice(
5048 bytes,
5049 offset,
5050 CRITICAL_METADATA_RECOVERY_HEADER_LEN,
5051 "CriticalMetadataRecoveryHeader",
5052 )?;
5053 let (tuple, header_hints) = recover_cmra_header_tuple(header_bytes, locator_tuple)?;
5054 validate_cmra_decoder_tuple(tuple)?;
5055 let cmra_length = cmra_serialized_length(tuple)?;
5056 let cmra_len = to_usize(cmra_length, "CMRA")?;
5057 let cmra_bytes = slice(bytes, offset, cmra_len, "CMRA")?;
5058 recover_cmra_from_bytes(cmra_bytes, tuple, header_hints, cmra_length, mode)
5059}
5060
5061fn recover_cmra_read_at(
5062 reader: &dyn ArchiveReadAt,
5063 cmra_offset: u64,
5064 locator_tuple: Option<CmraDecoderTuple>,
5065 mode: CmraRecoveryMode,
5066) -> Result<RecoveredCmra, FormatError> {
5067 let header_bytes = read_at_vec(
5068 reader,
5069 cmra_offset,
5070 CRITICAL_METADATA_RECOVERY_HEADER_LEN,
5071 "CriticalMetadataRecoveryHeader",
5072 )?;
5073 let (tuple, header_hints) = recover_cmra_header_tuple(&header_bytes, locator_tuple)?;
5074 validate_cmra_decoder_tuple(tuple)?;
5075 let cmra_length = cmra_serialized_length(tuple)?;
5076 let cmra_bytes = read_at_vec(reader, cmra_offset, to_usize(cmra_length, "CMRA")?, "CMRA")?;
5077 recover_cmra_from_bytes(&cmra_bytes, tuple, header_hints, cmra_length, mode)
5078}
5079
5080fn recover_cmra_header_tuple(
5081 header_bytes: &[u8],
5082 locator_tuple: Option<CmraDecoderTuple>,
5083) -> Result<(CmraDecoderTuple, Option<CmraIdentityHints>), FormatError> {
5084 let parsed_header = CriticalMetadataRecoveryHeader::parse(header_bytes);
5085 Ok(match (parsed_header, locator_tuple) {
5086 (Ok(header), Some(locator_tuple)) => {
5087 let header_tuple = CmraDecoderTuple::from(header);
5088 if header_tuple != locator_tuple {
5089 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
5090 }
5091 (locator_tuple, Some(CmraIdentityHints::from(header)))
5092 }
5093 (Ok(header), None) => (
5094 CmraDecoderTuple::from(header),
5095 Some(CmraIdentityHints::from(header)),
5096 ),
5097 (Err(_), Some(tuple)) => (tuple, None),
5098 (Err(err), _) => return Err(err),
5099 })
5100}
5101
5102fn recover_cmra_from_bytes(
5103 cmra_bytes: &[u8],
5104 tuple: CmraDecoderTuple,
5105 header_hints: Option<CmraIdentityHints>,
5106 cmra_length: u64,
5107 mode: CmraRecoveryMode,
5108) -> Result<RecoveredCmra, FormatError> {
5109 let shard_size = tuple.shard_size as usize;
5110 let mut data_shards = vec![None; tuple.data_shard_count as usize];
5111 let mut parity_shards = vec![None; tuple.parity_shard_count as usize];
5112 let mut cursor = CRITICAL_METADATA_RECOVERY_HEADER_LEN;
5113 for idx in 0..(tuple.data_shard_count as usize + tuple.parity_shard_count as usize) {
5114 let raw = slice(
5115 cmra_bytes,
5116 cursor,
5117 CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
5118 "CriticalMetadataRecoveryShard",
5119 )?;
5120 let shard = CriticalMetadataRecoveryShard::parse(raw, shard_size).ok();
5121 if let Some(shard) = shard {
5122 validate_cmra_shard(&shard, idx, tuple)?;
5123 if shard.shard_role == 0 {
5124 let data_slot = data_shards
5125 .get_mut(idx)
5126 .ok_or(FormatError::InvalidArchive("CMRA data shard out of range"))?;
5127 *data_slot = Some(shard.payload);
5128 } else {
5129 let parity_idx = idx - tuple.data_shard_count as usize;
5130 let parity_slot =
5131 parity_shards
5132 .get_mut(parity_idx)
5133 .ok_or(FormatError::InvalidArchive(
5134 "CMRA parity shard out of range",
5135 ))?;
5136 *parity_slot = Some(shard.payload);
5137 }
5138 }
5139 cursor = checked_add(
5140 cursor,
5141 CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
5142 "CriticalMetadataRecoveryShard",
5143 )?;
5144 }
5145 let repaired = repair_data_gf16(&data_shards, &parity_shards, shard_size)?;
5146 let mut image_bytes = Vec::with_capacity(tuple.image_length as usize);
5147 for shard in repaired {
5148 image_bytes.extend_from_slice(&shard);
5149 }
5150 image_bytes.truncate(tuple.image_length as usize);
5151 if sha256_bytes(&image_bytes) != tuple.image_sha256 {
5152 return Err(FormatError::InvalidArchive("CMRA image SHA-256 mismatch"));
5153 }
5154 let image = CriticalMetadataImage::parse(&image_bytes)?;
5155 validate_critical_metadata_image(&image, mode)?;
5156 Ok(RecoveredCmra {
5157 image,
5158 tuple,
5159 header_hints,
5160 cmra_length,
5161 })
5162}
5163
5164fn validate_cmra_decoder_tuple(tuple: CmraDecoderTuple) -> Result<(), FormatError> {
5165 let shard_size = tuple.shard_size as u64;
5166 if !(512..=4096).contains(&shard_size) || shard_size % 2 != 0 {
5167 return Err(FormatError::InvalidArchive("CMRA shard_size is invalid"));
5168 }
5169 let image_length = tuple.image_length as u64;
5170 let min = critical_image_min();
5171 let cap = critical_image_cap()?;
5172 if image_length < min || image_length > cap {
5173 return Err(FormatError::InvalidArchive(
5174 "CMRA image_length is outside bounds",
5175 ));
5176 }
5177 let expected_data_shards = ceil_div_u64(image_length, shard_size)?;
5178 if expected_data_shards == 0 || expected_data_shards != tuple.data_shard_count as u64 {
5179 return Err(FormatError::InvalidArchive(
5180 "CMRA data_shard_count does not match image length",
5181 ));
5182 }
5183 let max_parity = 2u64.max(ceil_div_u64(
5184 checked_u64_mul(
5185 expected_data_shards,
5186 READER_MAX_CMRA_PARITY_PCT as u64,
5187 "CMRA parity overflow",
5188 )?,
5189 100,
5190 )?);
5191 if tuple.parity_shard_count as u64 > max_parity {
5192 return Err(FormatError::ReaderResourceLimitExceeded {
5193 field: "CMRA parity shard count",
5194 cap: max_parity,
5195 actual: tuple.parity_shard_count as u64,
5196 });
5197 }
5198 let total = expected_data_shards
5199 .checked_add(tuple.parity_shard_count as u64)
5200 .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
5201 if total > 65_535 {
5202 return Err(FormatError::FecTooManyShards(total as usize));
5203 }
5204 Ok(())
5205}
5206
5207fn validate_cmra_writer_parity_lower_bound(
5208 tuple: CmraDecoderTuple,
5209 bit_rot_buffer_pct: u8,
5210) -> Result<(), FormatError> {
5211 let min_parity = 2u64.max(ceil_div_u64(
5212 checked_u64_mul(
5213 tuple.data_shard_count as u64,
5214 bit_rot_buffer_pct as u64,
5215 "CMRA parity lower-bound overflow",
5216 )?,
5217 100,
5218 )?);
5219 if (tuple.parity_shard_count as u64) < min_parity {
5220 return Err(FormatError::InvalidArchive(
5221 "CMRA parity shard count is below authenticated bit-rot lower bound",
5222 ));
5223 }
5224 Ok(())
5225}
5226
5227fn validate_cmra_shard(
5228 shard: &CriticalMetadataRecoveryShard,
5229 serialized_idx: usize,
5230 tuple: CmraDecoderTuple,
5231) -> Result<(), FormatError> {
5232 if shard.shard_index as usize != serialized_idx {
5233 return Err(FormatError::InvalidArchive(
5234 "CMRA shards are not in canonical order",
5235 ));
5236 }
5237 let data_count = tuple.data_shard_count as usize;
5238 let shard_size = tuple.shard_size as usize;
5239 if serialized_idx < data_count {
5240 if shard.shard_role != 0 {
5241 return Err(FormatError::InvalidArchive(
5242 "CMRA data shard has wrong role",
5243 ));
5244 }
5245 let expected_len = if serialized_idx + 1 == data_count {
5246 let used = tuple.image_length as usize - serialized_idx * shard_size;
5247 if used == 0 {
5248 shard_size
5249 } else {
5250 used
5251 }
5252 } else {
5253 shard_size
5254 };
5255 if shard.shard_payload_length as usize != expected_len {
5256 return Err(FormatError::InvalidArchive(
5257 "CMRA data shard payload length is non-canonical",
5258 ));
5259 }
5260 if serialized_idx + 1 == data_count
5261 && shard.payload[expected_len..].iter().any(|byte| *byte != 0)
5262 {
5263 return Err(FormatError::InvalidArchive(
5264 "CMRA final data shard padding is non-zero",
5265 ));
5266 }
5267 } else {
5268 if shard.shard_role != 1 {
5269 return Err(FormatError::InvalidArchive(
5270 "CMRA parity shard has wrong role",
5271 ));
5272 }
5273 if shard.shard_payload_length as usize != shard_size {
5274 return Err(FormatError::InvalidArchive(
5275 "CMRA parity shard payload length is non-canonical",
5276 ));
5277 }
5278 }
5279 Ok(())
5280}
5281
5282fn validate_critical_metadata_image(
5283 image: &CriticalMetadataImage,
5284 mode: CmraRecoveryMode,
5285) -> Result<(), FormatError> {
5286 let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
5287 if image.volume_header_offset != 0
5288 || image.volume_header_length != VOLUME_HEADER_LEN as u32
5289 || image.crypto_header_offset != VOLUME_HEADER_LEN as u64
5290 || image.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
5291 || image.volume_trailer_length != VOLUME_TRAILER_LEN as u32
5292 || image.body_bytes_before_cmra
5293 != image
5294 .volume_trailer_offset
5295 .checked_add(VOLUME_TRAILER_LEN as u64)
5296 .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
5297 {
5298 return Err(FormatError::InvalidArchive(
5299 "CriticalMetadataImage fixed layout is invalid",
5300 ));
5301 }
5302 if root_auth_present {
5303 if image.root_auth_footer_offset == 0
5304 || image.root_auth_footer_length == 0
5305 || image.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
5306 {
5307 return Err(FormatError::InvalidArchive(
5308 "CriticalMetadataImage root-auth range is invalid",
5309 ));
5310 }
5311 } else if image.root_auth_footer_offset != 0
5312 || image.root_auth_footer_length != 0
5313 || image.root_auth_footer_sha256 != [0u8; 32]
5314 {
5315 return Err(FormatError::InvalidArchive(
5316 "CriticalMetadataImage root-auth fields must be zero when absent",
5317 ));
5318 }
5319 let block_record_len = image_block_record_len_from_region(image)?;
5320 let block_record_len_u64 = u64::try_from(block_record_len)
5321 .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?;
5322 match mode {
5323 CmraRecoveryMode::KeyHolding => {
5324 let expected_len = image.block_count.checked_mul(block_record_len_u64).ok_or(
5325 FormatError::InvalidArchive("BlockRecord region length overflow"),
5326 )?;
5327 if image.block_records_length != expected_len {
5328 return Err(FormatError::InvalidArchive(
5329 "CriticalMetadataImage terminal equations are invalid",
5330 ));
5331 }
5332 }
5333 CmraRecoveryMode::PublicNoKey => {
5334 if image.block_records_length % block_record_len_u64 != 0 {
5335 return Err(FormatError::InvalidArchive(
5336 "CriticalMetadataImage BlockRecord region is not aligned",
5337 ));
5338 }
5339 }
5340 }
5341 if image.block_records_offset
5342 != image
5343 .crypto_header_offset
5344 .checked_add(image.crypto_header_length as u64)
5345 .ok_or(FormatError::InvalidArchive(
5346 "CryptoHeader boundary overflow",
5347 ))?
5348 || image.manifest_footer_offset
5349 != image
5350 .block_records_offset
5351 .checked_add(image.block_records_length)
5352 .ok_or(FormatError::InvalidArchive(
5353 "ManifestFooter boundary overflow",
5354 ))?
5355 {
5356 return Err(FormatError::InvalidArchive(
5357 "CriticalMetadataImage terminal equations are invalid",
5358 ));
5359 }
5360 let manifest_end = image
5361 .manifest_footer_offset
5362 .checked_add(MANIFEST_FOOTER_LEN as u64)
5363 .ok_or(FormatError::InvalidArchive(
5364 "RootAuthFooter boundary overflow",
5365 ))?;
5366 if root_auth_present {
5367 if image.root_auth_footer_offset != manifest_end
5368 || image
5369 .root_auth_footer_offset
5370 .checked_add(image.root_auth_footer_length as u64)
5371 .ok_or(FormatError::InvalidArchive(
5372 "VolumeTrailer boundary overflow",
5373 ))?
5374 != image.volume_trailer_offset
5375 {
5376 return Err(FormatError::InvalidArchive(
5377 "CriticalMetadataImage root-auth terminal equations are invalid",
5378 ));
5379 }
5380 } else if image.volume_trailer_offset != manifest_end {
5381 return Err(FormatError::InvalidArchive(
5382 "CriticalMetadataImage unsigned terminal equations are invalid",
5383 ));
5384 }
5385 let expected_types: &[u16] = if root_auth_present {
5386 &[1, 2, 3, 4, 5]
5387 } else {
5388 &[1, 2, 3, 5]
5389 };
5390 if image.regions.len() != expected_types.len()
5391 || image
5392 .regions
5393 .iter()
5394 .map(|region| region.region_type)
5395 .ne(expected_types.iter().copied())
5396 {
5397 return Err(FormatError::InvalidArchive(
5398 "CriticalMetadataImage regions are not canonical",
5399 ));
5400 }
5401 validate_image_region(
5402 image,
5403 1,
5404 image.volume_header_offset,
5405 image.volume_header_length,
5406 )?;
5407 validate_image_region(
5408 image,
5409 2,
5410 image.crypto_header_offset,
5411 image.crypto_header_length,
5412 )?;
5413 validate_image_region(
5414 image,
5415 3,
5416 image.manifest_footer_offset,
5417 image.manifest_footer_length,
5418 )?;
5419 if root_auth_present {
5420 validate_image_region(
5421 image,
5422 4,
5423 image.root_auth_footer_offset,
5424 image.root_auth_footer_length,
5425 )?;
5426 }
5427 validate_image_region(
5428 image,
5429 5,
5430 image.volume_trailer_offset,
5431 image.volume_trailer_length,
5432 )?;
5433 if sha256_region(image, 1)? != image.volume_header_sha256
5434 || sha256_region(image, 2)? != image.crypto_header_sha256
5435 || sha256_region(image, 3)? != image.manifest_footer_sha256
5436 || (root_auth_present && sha256_region(image, 4)? != image.root_auth_footer_sha256)
5437 || (!root_auth_present && image.root_auth_footer_sha256 != [0u8; 32])
5438 || sha256_region(image, 5)? != image.volume_trailer_sha256
5439 {
5440 return Err(FormatError::InvalidArchive(
5441 "CriticalMetadataImage region digest mismatch",
5442 ));
5443 }
5444 Ok(())
5445}
5446
5447fn image_block_record_len_from_region(image: &CriticalMetadataImage) -> Result<usize, FormatError> {
5448 let crypto_region = image
5449 .region(2)
5450 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
5451 let crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
5452 crypto.fixed.validate_supported_profile()?;
5453 Ok(crypto.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN)
5454}
5455
5456fn validate_image_region(
5457 image: &CriticalMetadataImage,
5458 region_type: u16,
5459 offset: u64,
5460 length: u32,
5461) -> Result<(), FormatError> {
5462 let region = image
5463 .region(region_type)
5464 .ok_or(FormatError::InvalidArchive(
5465 "missing CriticalMetadataImage region",
5466 ))?;
5467 if region.offset != offset || region.bytes.len() != length as usize {
5468 return Err(FormatError::InvalidArchive(
5469 "CriticalMetadataImage region range mismatch",
5470 ));
5471 }
5472 Ok(())
5473}
5474
5475fn validate_image_identity(
5476 image: &CriticalMetadataImage,
5477 volume_header: &VolumeHeader,
5478 crypto_header: &CryptoHeaderFixed,
5479) -> Result<(), FormatError> {
5480 if image.archive_uuid != volume_header.archive_uuid
5481 || image.session_id != volume_header.session_id
5482 || image.volume_index != volume_header.volume_index
5483 || image.stripe_width != volume_header.stripe_width
5484 || image.stripe_width != crypto_header.stripe_width
5485 {
5486 return Err(FormatError::InvalidArchive(
5487 "CriticalMetadataImage identity does not match selected volume",
5488 ));
5489 }
5490 Ok(())
5491}
5492
5493fn sha256_region(image: &CriticalMetadataImage, region_type: u16) -> Result<[u8; 32], FormatError> {
5494 Ok(sha256_bytes(
5495 &image
5496 .region(region_type)
5497 .ok_or(FormatError::InvalidArchive(
5498 "missing CriticalMetadataImage region",
5499 ))?
5500 .bytes,
5501 ))
5502}
5503
5504fn validate_recovered_terminal_authority(
5505 image: CriticalMetadataImage,
5506 tuple: CmraDecoderTuple,
5507 master_key: &MasterKey,
5508 require_cmra_boundary_magic: bool,
5509) -> Result<RecoveredTerminalAuthority, FormatError> {
5510 let volume_header_region = image
5511 .region(1)
5512 .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
5513 let volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
5514 let crypto_region = image
5515 .region(2)
5516 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
5517 let crypto_header_bytes = crypto_region.bytes.clone();
5518 let parsed_crypto = CryptoHeader::parse(&crypto_header_bytes, image.crypto_header_length)?;
5519 let subkeys = subkeys_for_open(
5520 Some(master_key),
5521 parsed_crypto.fixed.aead_algo,
5522 &volume_header.archive_uuid,
5523 &volume_header.session_id,
5524 )?;
5525 verify_integrity_tag(
5526 HmacDomain::CryptoHeader,
5527 parsed_crypto.fixed.aead_algo,
5528 Some(&subkeys.mac_key),
5529 &volume_header.archive_uuid,
5530 &volume_header.session_id,
5531 parsed_crypto.hmac_covered_bytes,
5532 &parsed_crypto.header_hmac,
5533 )?;
5534 parsed_crypto.validate_extension_semantics()?;
5535 validate_seekable_supported_volume(
5536 &volume_header,
5537 &parsed_crypto.fixed,
5538 &parsed_crypto.extensions,
5539 )?;
5540 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
5541 let crypto_header = parsed_crypto.fixed.clone();
5542 if crypto_header.bit_rot_buffer_pct == 0 {
5543 return Err(FormatError::InvalidArchive(
5544 "CMRA startup recovery requires a nonzero bit-rot budget",
5545 ));
5546 }
5547 drop(parsed_crypto);
5548
5549 let terminal = validate_recovered_terminal_inner(
5550 image,
5551 tuple,
5552 require_cmra_boundary_magic,
5553 true,
5554 KeyHoldingTerminalContext {
5555 subkeys: &subkeys,
5556 volume_header: &volume_header,
5557 crypto_header: &crypto_header,
5558 crypto_header_bytes: &crypto_header_bytes,
5559 },
5560 )?;
5561 Ok(RecoveredTerminalAuthority {
5562 terminal,
5563 volume_header,
5564 crypto_header,
5565 crypto_header_bytes,
5566 subkeys,
5567 })
5568}
5569
5570fn validate_recovered_terminal(
5571 image: CriticalMetadataImage,
5572 tuple: CmraDecoderTuple,
5573 bytes: &[u8],
5574 context: KeyHoldingTerminalContext<'_>,
5575 require_cmra_boundary_magic: bool,
5576) -> Result<V41Terminal, FormatError> {
5577 let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
5578 let cmra_boundary_magic_ok = bytes.get(cmra_offset..cmra_offset + 4) == Some(b"TZCR");
5579 validate_recovered_terminal_inner(
5580 image,
5581 tuple,
5582 require_cmra_boundary_magic,
5583 cmra_boundary_magic_ok,
5584 context,
5585 )
5586}
5587
5588fn validate_recovered_terminal_read_at(
5589 image: CriticalMetadataImage,
5590 tuple: CmraDecoderTuple,
5591 reader: &dyn ArchiveReadAt,
5592 context: KeyHoldingTerminalContext<'_>,
5593 require_cmra_boundary_magic: bool,
5594) -> Result<V41Terminal, FormatError> {
5595 let mut magic = [0u8; 4];
5596 reader.read_exact_at(image.body_bytes_before_cmra, &mut magic)?;
5597 validate_recovered_terminal_inner(
5598 image,
5599 tuple,
5600 require_cmra_boundary_magic,
5601 magic == *b"TZCR",
5602 context,
5603 )
5604}
5605
5606fn validate_recovered_terminal_inner(
5607 image: CriticalMetadataImage,
5608 tuple: CmraDecoderTuple,
5609 require_cmra_boundary_magic: bool,
5610 cmra_boundary_magic_ok: bool,
5611 context: KeyHoldingTerminalContext<'_>,
5612) -> Result<V41Terminal, FormatError> {
5613 let subkeys = context.subkeys;
5614 let volume_header = context.volume_header;
5615 let crypto_header = context.crypto_header;
5616 let volume_header_region = image
5617 .region(1)
5618 .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
5619 let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
5620 if &recovered_volume_header != volume_header {
5621 return Err(FormatError::InvalidArchive(
5622 "CMRA VolumeHeader differs from parsed VolumeHeader",
5623 ));
5624 }
5625 validate_image_identity(&image, volume_header, crypto_header)?;
5626 let crypto_region = image
5627 .region(2)
5628 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
5629 let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
5630 if recovered_crypto.fixed != *crypto_header {
5631 return Err(FormatError::InvalidArchive(
5632 "CMRA CryptoHeader differs from parsed CryptoHeader",
5633 ));
5634 }
5635 let recovered_pre_hmac_len = crypto_region
5636 .bytes
5637 .len()
5638 .checked_sub(CRYPTO_HEADER_HMAC_LEN)
5639 .ok_or(FormatError::InvalidArchive(
5640 "CMRA CryptoHeader is too short",
5641 ))?;
5642 let parsed_pre_hmac_len = context
5643 .crypto_header_bytes
5644 .len()
5645 .checked_sub(CRYPTO_HEADER_HMAC_LEN)
5646 .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
5647 if recovered_pre_hmac_len != parsed_pre_hmac_len
5648 || crypto_region.bytes[..recovered_pre_hmac_len]
5649 != context.crypto_header_bytes[..parsed_pre_hmac_len]
5650 {
5651 return Err(FormatError::InvalidArchive(
5652 "CMRA CryptoHeader differs from parsed CryptoHeader",
5653 ));
5654 }
5655 verify_integrity_tag(
5656 HmacDomain::CryptoHeader,
5657 recovered_crypto.fixed.aead_algo,
5658 Some(&subkeys.mac_key),
5659 &volume_header.archive_uuid,
5660 &volume_header.session_id,
5661 recovered_crypto.hmac_covered_bytes,
5662 &recovered_crypto.header_hmac,
5663 )?;
5664 validate_cmra_writer_parity_lower_bound(tuple, recovered_crypto.fixed.bit_rot_buffer_pct)?;
5665 recovered_crypto.validate_extension_semantics()?;
5666
5667 let manifest_region = image
5668 .region(3)
5669 .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
5670 let manifest_footer = ManifestFooter::parse(&manifest_region.bytes)?;
5671 validate_manifest_footer(
5672 volume_header,
5673 crypto_header,
5674 &manifest_footer,
5675 subkeys,
5676 &manifest_region.bytes,
5677 )?;
5678 manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
5679
5680 let root_auth_footer = if image.layout_flags & 0x0000_0001 != 0 {
5681 let root_auth_region = image
5682 .region(4)
5683 .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
5684 let footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
5685 if footer.archive_uuid != volume_header.archive_uuid
5686 || footer.session_id != volume_header.session_id
5687 || footer.footer_length()? != image.root_auth_footer_length
5688 {
5689 return Err(FormatError::InvalidArchive(
5690 "RootAuthFooter identity or length does not match terminal image",
5691 ));
5692 }
5693 Some(footer)
5694 } else {
5695 None
5696 };
5697
5698 let trailer_region = image
5699 .region(5)
5700 .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
5701 let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
5702 verify_integrity_tag(
5703 HmacDomain::VolumeTrailer,
5704 crypto_header.aead_algo,
5705 Some(&subkeys.mac_key),
5706 &volume_header.archive_uuid,
5707 &volume_header.session_id,
5708 &trailer_region.bytes[..TRAILER_HMAC_COVERED_LEN],
5709 &trailer.trailer_hmac,
5710 )?;
5711 validate_trailer_identity(volume_header, &trailer)?;
5712 validate_v41_trailer_equations(&image, &trailer)?;
5713
5714 if require_cmra_boundary_magic && !cmra_boundary_magic_ok {
5715 return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
5716 }
5717
5718 let manifest_footer_bytes = manifest_region.bytes.clone();
5719 let root_auth_footer_bytes = image.region(4).map(|region| region.bytes.clone());
5720 Ok(V41Terminal {
5721 image,
5722 manifest_footer_bytes,
5723 root_auth_footer_bytes,
5724 root_auth_footer,
5725 volume_trailer: trailer,
5726 })
5727}
5728
5729fn validate_recovered_public_terminal(
5730 image: CriticalMetadataImage,
5731 bytes: &[u8],
5732 volume_header: &VolumeHeader,
5733 crypto_header: &CryptoHeaderFixed,
5734 require_cmra_boundary_magic: bool,
5735) -> Result<V41PublicTerminal, FormatError> {
5736 if image.layout_flags & 0x0000_0001 == 0 {
5737 return Err(FormatError::ReaderUnsupported(
5738 "public no-key verification requires root-auth",
5739 ));
5740 }
5741 let volume_header_region = image
5742 .region(1)
5743 .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
5744 let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
5745 if &recovered_volume_header != volume_header {
5746 return Err(FormatError::InvalidArchive(
5747 "CMRA VolumeHeader differs from parsed VolumeHeader",
5748 ));
5749 }
5750 validate_image_identity(&image, volume_header, crypto_header)?;
5751 let crypto_region = image
5752 .region(2)
5753 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
5754 let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
5755 if recovered_crypto.fixed != *crypto_header {
5756 return Err(FormatError::InvalidArchive(
5757 "CMRA CryptoHeader differs from parsed CryptoHeader",
5758 ));
5759 }
5760 recovered_crypto.validate_extension_semantics()?;
5761
5762 image
5763 .region(3)
5764 .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
5765
5766 let root_auth_region = image
5767 .region(4)
5768 .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
5769 let root_auth_footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
5770 if root_auth_footer.archive_uuid != volume_header.archive_uuid
5771 || root_auth_footer.session_id != volume_header.session_id
5772 || root_auth_footer.footer_length()? != image.root_auth_footer_length
5773 {
5774 return Err(FormatError::InvalidArchive(
5775 "public RootAuthFooter identity or length does not match terminal image",
5776 ));
5777 }
5778
5779 let trailer_region = image
5780 .region(5)
5781 .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
5782 let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
5783 validate_trailer_identity(volume_header, &trailer)?;
5784 validate_v41_public_trailer_profile(&image, &trailer)?;
5785
5786 let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
5787 if require_cmra_boundary_magic && bytes.get(cmra_offset..cmra_offset + 4) != Some(b"TZCR") {
5788 return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
5789 }
5790
5791 let root_auth_footer_bytes = root_auth_region.bytes.clone();
5792 Ok(V41PublicTerminal {
5793 image,
5794 root_auth_footer_bytes,
5795 root_auth_footer,
5796 })
5797}
5798
5799fn validate_v41_trailer_equations(
5800 image: &CriticalMetadataImage,
5801 trailer: &VolumeTrailer,
5802) -> Result<(), FormatError> {
5803 let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
5804 if trailer.bytes_written != image.volume_trailer_offset
5805 || trailer.manifest_footer_offset != image.manifest_footer_offset
5806 || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
5807 || trailer.block_count != image.block_count
5808 {
5809 return Err(FormatError::InvalidArchive(
5810 "VolumeTrailer does not match v41 terminal layout",
5811 ));
5812 }
5813 if root_auth_present {
5814 if trailer.root_auth_flags != 0x0000_0001
5815 || trailer.root_auth_footer_offset != image.root_auth_footer_offset
5816 || trailer.root_auth_footer_length != image.root_auth_footer_length
5817 || image.root_auth_footer_offset
5818 != image
5819 .manifest_footer_offset
5820 .checked_add(MANIFEST_FOOTER_LEN as u64)
5821 .ok_or(FormatError::InvalidArchive(
5822 "RootAuthFooter trailer boundary overflow",
5823 ))?
5824 || image
5825 .root_auth_footer_offset
5826 .checked_add(image.root_auth_footer_length as u64)
5827 .ok_or(FormatError::InvalidArchive(
5828 "RootAuthFooter trailer boundary overflow",
5829 ))?
5830 != image.volume_trailer_offset
5831 {
5832 return Err(FormatError::InvalidArchive(
5833 "VolumeTrailer root-auth fields do not match v41 terminal layout",
5834 ));
5835 }
5836 } else if trailer.root_auth_footer_offset != 0
5837 || trailer.root_auth_footer_length != 0
5838 || trailer.root_auth_flags != 0
5839 {
5840 return Err(FormatError::InvalidArchive(
5841 "VolumeTrailer root-auth fields must be zero when absent",
5842 ));
5843 }
5844 Ok(())
5845}
5846
5847fn validate_v41_public_trailer_profile(
5848 image: &CriticalMetadataImage,
5849 trailer: &VolumeTrailer,
5850) -> Result<(), FormatError> {
5851 if trailer.bytes_written != image.volume_trailer_offset
5852 || trailer.manifest_footer_offset != image.manifest_footer_offset
5853 || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
5854 {
5855 return Err(FormatError::InvalidArchive(
5856 "VolumeTrailer does not match v41 public terminal layout",
5857 ));
5858 }
5859 if trailer.root_auth_flags != 0x0000_0001
5860 || trailer.root_auth_footer_offset == 0
5861 || trailer.root_auth_footer_length == 0
5862 || trailer.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
5863 || trailer.root_auth_footer_offset != image.root_auth_footer_offset
5864 || trailer.root_auth_footer_length != image.root_auth_footer_length
5865 || image.root_auth_footer_offset
5866 != image
5867 .manifest_footer_offset
5868 .checked_add(MANIFEST_FOOTER_LEN as u64)
5869 .ok_or(FormatError::InvalidArchive(
5870 "RootAuthFooter trailer boundary overflow",
5871 ))?
5872 || image
5873 .root_auth_footer_offset
5874 .checked_add(image.root_auth_footer_length as u64)
5875 .ok_or(FormatError::InvalidArchive(
5876 "RootAuthFooter trailer boundary overflow",
5877 ))?
5878 != image.volume_trailer_offset
5879 {
5880 return Err(FormatError::InvalidArchive(
5881 "VolumeTrailer root-auth fields do not match v41 public terminal layout",
5882 ));
5883 }
5884 Ok(())
5885}
5886
5887fn critical_image_min() -> u64 {
5888 const MIN_CRYPTO_HEADER_LEN: u64 = 116;
5889 CRITICAL_METADATA_IMAGE_FIXED_LEN as u64
5890 + 4 * SERIALIZED_REGION_HEADER_LEN as u64
5891 + VOLUME_HEADER_LEN as u64
5892 + MIN_CRYPTO_HEADER_LEN
5893 + MANIFEST_FOOTER_LEN as u64
5894 + VOLUME_TRAILER_LEN as u64
5895 + IMAGE_CRC_LEN as u64
5896}
5897
5898fn critical_image_cap() -> Result<u64, FormatError> {
5899 [
5900 CRITICAL_METADATA_IMAGE_FIXED_LEN as u64,
5901 5 * SERIALIZED_REGION_HEADER_LEN as u64,
5902 VOLUME_HEADER_LEN as u64,
5903 READER_MAX_CRYPTO_HEADER_LEN as u64,
5904 MANIFEST_FOOTER_LEN as u64,
5905 READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
5906 VOLUME_TRAILER_LEN as u64,
5907 IMAGE_CRC_LEN as u64,
5908 ]
5909 .into_iter()
5910 .try_fold(0u64, |total, value| {
5911 total
5912 .checked_add(value)
5913 .ok_or(FormatError::InvalidArchive("critical image cap overflow"))
5914 })
5915}
5916
5917fn cmra_serialized_length(tuple: CmraDecoderTuple) -> Result<u64, FormatError> {
5918 let shard_total = (tuple.data_shard_count as u64)
5919 .checked_add(tuple.parity_shard_count as u64)
5920 .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
5921 let row_len = (CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN as u64)
5922 .checked_add(tuple.shard_size as u64)
5923 .ok_or(FormatError::InvalidArchive("CMRA row length overflow"))?;
5924 checked_u64_mul(shard_total, row_len, "CMRA length overflow")?
5925 .checked_add(CRITICAL_METADATA_RECOVERY_HEADER_LEN as u64)
5926 .ok_or(FormatError::InvalidArchive("CMRA length overflow"))
5927}
5928
5929fn cmra_worst_case_cap() -> Result<u64, FormatError> {
5930 let cap = critical_image_cap()?;
5931 let mut worst = 0u64;
5932 let mut shard_size = 512u64;
5933 while shard_size <= 4096 {
5934 let data = ceil_div_u64(cap, shard_size)?;
5935 let parity = 2u64.max(ceil_div_u64(
5936 checked_u64_mul(data, READER_MAX_CMRA_PARITY_PCT as u64, "CMRA cap overflow")?,
5937 100,
5938 )?);
5939 let tuple = CmraDecoderTuple {
5940 shard_size: shard_size as u32,
5941 data_shard_count: u16::try_from(data)
5942 .map_err(|_| FormatError::InvalidArchive("CMRA cap data shard overflow"))?,
5943 parity_shard_count: u16::try_from(parity)
5944 .map_err(|_| FormatError::InvalidArchive("CMRA cap parity shard overflow"))?,
5945 image_length: u32::try_from(cap)
5946 .map_err(|_| FormatError::InvalidArchive("CMRA cap image overflow"))?,
5947 image_sha256: [0u8; 32],
5948 };
5949 worst = worst.max(cmra_serialized_length(tuple)?);
5950 shard_size += 2;
5951 }
5952 Ok(worst)
5953}
5954
5955pub(crate) fn v41_terminal_tail_cap() -> Result<usize, FormatError> {
5956 let total = [
5957 MANIFEST_FOOTER_LEN as u64,
5958 READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
5959 VOLUME_TRAILER_LEN as u64,
5960 cmra_worst_case_cap()?,
5961 LOCATOR_PAIR_LEN as u64,
5962 ]
5963 .into_iter()
5964 .try_fold(0u64, |sum, value| {
5965 sum.checked_add(value)
5966 .ok_or(FormatError::InvalidArchive("terminal tail cap overflow"))
5967 })?;
5968 usize::try_from(total).map_err(|_| FormatError::InvalidArchive("terminal tail cap overflow"))
5969}
5970
5971fn max_critical_recovery_scan(options: ReaderOptions) -> Result<usize, FormatError> {
5972 let worst = cmra_worst_case_cap()?;
5973 let total = options
5974 .max_trailing_garbage_scan
5975 .try_into()
5976 .map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
5977 .and_then(|scan: u64| {
5978 scan.checked_add(worst)
5979 .and_then(|value| value.checked_add(LOCATOR_PAIR_LEN as u64))
5980 .ok_or(FormatError::InvalidArchive("scan cap overflow"))
5981 })?;
5982 usize::try_from(total).map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
5983}
5984
5985fn validate_bootstrap_single_volume_input(
5986 volume_header: &VolumeHeader,
5987 crypto_header: &CryptoHeaderFixed,
5988) -> Result<(), FormatError> {
5989 if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
5990 return Err(FormatError::ReaderUnsupported(
5991 "bootstrap sidecar reader supports only single-volume archive input",
5992 ));
5993 }
5994 if crypto_header.stripe_width != volume_header.stripe_width {
5995 return Err(FormatError::InvalidArchive(
5996 "VolumeHeader and CryptoHeader stripe_width differ",
5997 ));
5998 }
5999 Ok(())
6000}
6001
6002#[derive(Debug)]
6003struct ParsedBootstrapSidecar {
6004 manifest_footer: Option<ManifestFooter>,
6005 index_root_records_section: Option<(u64, u64)>,
6006 dictionary_records_section: Option<(u64, u64)>,
6007}
6008
6009pub(crate) struct NonSeekableBootstrapMaterial {
6010 pub(crate) manifest_footer: ManifestFooter,
6011 pub(crate) payload_dictionary: Option<Vec<u8>>,
6012}
6013
6014#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6015enum BootstrapSidecarUse {
6016 SeekableAssist,
6017 NonSeekableRandomAccess,
6018}
6019
6020impl ParsedBootstrapSidecar {
6021 fn require_sections_for(
6022 &self,
6023 sidecar_use: BootstrapSidecarUse,
6024 crypto_header: &CryptoHeaderFixed,
6025 ) -> Result<(), FormatError> {
6026 if sidecar_use == BootstrapSidecarUse::NonSeekableRandomAccess {
6027 if self.manifest_footer.is_none() || self.index_root_records_section.is_none() {
6028 return Err(FormatError::ReaderUnsupported(
6029 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
6030 ));
6031 }
6032 if crypto_header.has_dictionary != 0 && self.dictionary_records_section.is_none() {
6033 return Err(FormatError::ReaderUnsupported(
6034 "dictionary bootstrap required",
6035 ));
6036 }
6037 }
6038 Ok(())
6039 }
6040}
6041
6042pub(crate) fn parse_non_seekable_bootstrap_material(
6043 bootstrap_sidecar: &[u8],
6044 volume_header: &VolumeHeader,
6045 crypto_header: &CryptoHeaderFixed,
6046 subkeys: &Subkeys,
6047) -> Result<NonSeekableBootstrapMaterial, FormatError> {
6048 validate_bootstrap_single_volume_input(volume_header, crypto_header)?;
6049 let sidecar =
6050 parse_bootstrap_sidecar(bootstrap_sidecar, volume_header, crypto_header, subkeys)?;
6051 sidecar.require_sections_for(BootstrapSidecarUse::NonSeekableRandomAccess, crypto_header)?;
6052 let manifest_footer = sidecar
6053 .manifest_footer
6054 .clone()
6055 .ok_or(FormatError::ReaderUnsupported(
6056 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
6057 ))?;
6058
6059 let mut blocks = BTreeMap::new();
6060 let (offset, length) =
6061 sidecar
6062 .index_root_records_section
6063 .ok_or(FormatError::ReaderUnsupported(
6064 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
6065 ))?;
6066 let index_root_records = parse_sidecar_block_records(
6067 bootstrap_sidecar,
6068 crypto_header.block_size as usize,
6069 SidecarBlockRecordsSection {
6070 offset,
6071 length,
6072 extent: index_root_extent_from_manifest(&manifest_footer),
6073 data_kind: BlockKind::IndexRootData,
6074 parity_kind: BlockKind::IndexRootParity,
6075 structure: "IndexRoot",
6076 },
6077 )?;
6078 insert_sidecar_records(&mut blocks, index_root_records)?;
6079
6080 let limits = metadata_limits(crypto_header);
6081 let index_root_plaintext = load_metadata_object_from_parts(
6082 &blocks,
6083 ObjectLoadContext::index_root(
6084 volume_header,
6085 crypto_header,
6086 subkeys,
6087 index_root_extent_from_manifest(&manifest_footer),
6088 ),
6089 manifest_footer.index_root_decompressed_size,
6090 )?;
6091 let index_root = IndexRoot::parse(
6092 &index_root_plaintext,
6093 crypto_header.has_dictionary != 0,
6094 limits,
6095 )?;
6096
6097 if crypto_header.has_dictionary != 0 {
6098 let (offset, length) =
6099 sidecar
6100 .dictionary_records_section
6101 .ok_or(FormatError::ReaderUnsupported(
6102 "dictionary bootstrap required",
6103 ))?;
6104 let dictionary_records = parse_sidecar_block_records(
6105 bootstrap_sidecar,
6106 crypto_header.block_size as usize,
6107 SidecarBlockRecordsSection {
6108 offset,
6109 length,
6110 extent: dictionary_extent_from_index_root(&index_root)?,
6111 data_kind: BlockKind::DictionaryData,
6112 parity_kind: BlockKind::DictionaryParity,
6113 structure: "dictionary",
6114 },
6115 )?;
6116 insert_sidecar_records(&mut blocks, dictionary_records)?;
6117 }
6118 let payload_dictionary =
6119 load_archive_dictionary(&blocks, subkeys, volume_header, crypto_header, &index_root)?;
6120
6121 Ok(NonSeekableBootstrapMaterial {
6122 manifest_footer,
6123 payload_dictionary,
6124 })
6125}
6126
6127fn parse_bootstrap_sidecar(
6128 bytes: &[u8],
6129 volume_header: &VolumeHeader,
6130 crypto_header: &CryptoHeaderFixed,
6131 subkeys: &Subkeys,
6132) -> Result<ParsedBootstrapSidecar, FormatError> {
6133 let header_bytes = slice(
6134 bytes,
6135 0,
6136 BOOTSTRAP_SIDECAR_HEADER_LEN,
6137 "BootstrapSidecarHeader",
6138 )?;
6139 let header = BootstrapSidecarHeader::parse(header_bytes)?;
6140 if header.archive_uuid != volume_header.archive_uuid
6141 || header.session_id != volume_header.session_id
6142 {
6143 return Err(FormatError::InvalidArchive(
6144 "bootstrap sidecar identity does not match VolumeHeader",
6145 ));
6146 }
6147 verify_integrity_tag(
6148 HmacDomain::BootstrapSidecar,
6149 crypto_header.aead_algo,
6150 Some(&subkeys.mac_key),
6151 &volume_header.archive_uuid,
6152 &volume_header.session_id,
6153 &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
6154 &header.sidecar_hmac,
6155 )?;
6156 header.validate_packed_layout(bytes.len() as u64)?;
6157 validate_sidecar_size_cap(&header, crypto_header, bytes.len() as u64)?;
6158
6159 if header.has_dictionary_records() && crypto_header.has_dictionary == 0 {
6160 return Err(FormatError::InvalidArchive(
6161 "bootstrap sidecar has dictionary records while has_dictionary is false",
6162 ));
6163 }
6164
6165 let manifest_footer = if header.has_manifest_footer() {
6166 let manifest_offset = to_usize(header.manifest_footer_offset, "BootstrapSidecarHeader")?;
6167 let manifest_bytes = slice(
6168 bytes,
6169 manifest_offset,
6170 MANIFEST_FOOTER_LEN,
6171 "ManifestFooter",
6172 )?;
6173 let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
6174 validate_sidecar_manifest_footer(
6175 volume_header,
6176 crypto_header,
6177 &manifest_footer,
6178 subkeys,
6179 manifest_bytes,
6180 )?;
6181 manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
6182 Some(manifest_footer)
6183 } else {
6184 None
6185 };
6186
6187 Ok(ParsedBootstrapSidecar {
6188 manifest_footer,
6189 index_root_records_section: header.has_index_root_records().then_some((
6190 header.index_root_records_offset,
6191 header.index_root_records_length,
6192 )),
6193 dictionary_records_section: header.has_dictionary_records().then_some((
6194 header.dictionary_records_offset,
6195 header.dictionary_records_length,
6196 )),
6197 })
6198}
6199
6200fn index_root_extent_from_manifest(manifest_footer: &ManifestFooter) -> ObjectExtent {
6201 ObjectExtent {
6202 first_block_index: manifest_footer.index_root_first_block,
6203 data_block_count: manifest_footer.index_root_data_block_count,
6204 parity_block_count: manifest_footer.index_root_parity_block_count,
6205 encrypted_size: manifest_footer.index_root_encrypted_size,
6206 }
6207}
6208
6209fn insert_sidecar_records(
6210 blocks: &mut BTreeMap<u64, BlockRecord>,
6211 records: Vec<BlockRecord>,
6212) -> Result<(), FormatError> {
6213 for record in records {
6214 if let Some(existing) = blocks.insert(record.block_index, record.clone()) {
6215 if existing != record {
6216 return Err(FormatError::InvalidArchive(
6217 "bootstrap sidecar conflicts with volume BlockRecord",
6218 ));
6219 }
6220 }
6221 }
6222 Ok(())
6223}
6224
6225fn validate_sidecar_manifest_footer(
6226 volume_header: &VolumeHeader,
6227 crypto_header: &CryptoHeaderFixed,
6228 footer: &ManifestFooter,
6229 subkeys: &Subkeys,
6230 raw: &[u8],
6231) -> Result<(), FormatError> {
6232 if footer.archive_uuid != volume_header.archive_uuid
6233 || footer.session_id != volume_header.session_id
6234 {
6235 return Err(FormatError::InvalidArchive(
6236 "sidecar ManifestFooter identity does not match VolumeHeader",
6237 ));
6238 }
6239 if footer.volume_index != 0 {
6240 return Err(FormatError::InvalidArchive(
6241 "sidecar ManifestFooter volume_index must be zero",
6242 ));
6243 }
6244 if footer.total_volumes != crypto_header.stripe_width {
6245 return Err(FormatError::InvalidArchive(
6246 "sidecar ManifestFooter total_volumes does not match stripe_width",
6247 ));
6248 }
6249 if footer.is_authoritative != 1 {
6250 return Err(FormatError::InvalidArchive(
6251 "sidecar ManifestFooter is not authoritative",
6252 ));
6253 }
6254 verify_integrity_tag(
6255 HmacDomain::ManifestFooter,
6256 crypto_header.aead_algo,
6257 Some(&subkeys.mac_key),
6258 &volume_header.archive_uuid,
6259 &volume_header.session_id,
6260 &raw[..MANIFEST_HMAC_COVERED_LEN],
6261 &footer.manifest_hmac,
6262 )
6263}
6264
6265fn validate_sidecar_size_cap(
6266 header: &BootstrapSidecarHeader,
6267 crypto_header: &CryptoHeaderFixed,
6268 file_size: u64,
6269) -> Result<(), FormatError> {
6270 let record_len = checked_u64_add(
6271 crypto_header.block_size as u64,
6272 BLOCK_RECORD_FRAMING_LEN as u64,
6273 "bootstrap sidecar cap overflow",
6274 )?;
6275 let max_index_records = crypto_header.index_root_fec_data_shards as u64
6276 + crypto_header.index_root_fec_parity_shards as u64;
6277 let max_record_section_bytes = checked_u64_mul(
6278 max_index_records,
6279 record_len,
6280 "bootstrap sidecar cap overflow",
6281 )?;
6282 if header.index_root_records_length % record_len != 0 {
6283 return Err(FormatError::InvalidArchive(
6284 "bootstrap sidecar IndexRoot records length is not aligned",
6285 ));
6286 }
6287 if header.index_root_records_length / record_len > max_index_records {
6288 return Err(FormatError::InvalidArchive(
6289 "bootstrap sidecar IndexRoot records exceed resource cap",
6290 ));
6291 }
6292 if header.dictionary_records_length % record_len != 0 {
6293 return Err(FormatError::InvalidArchive(
6294 "bootstrap sidecar dictionary records length is not aligned",
6295 ));
6296 }
6297 if header.dictionary_records_length / record_len > max_index_records {
6298 return Err(FormatError::InvalidArchive(
6299 "bootstrap sidecar dictionary records exceed resource cap",
6300 ));
6301 }
6302
6303 let mut cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64;
6304 if header.has_manifest_footer() {
6305 cap = cap
6306 .checked_add(MANIFEST_FOOTER_LEN as u64)
6307 .ok_or(FormatError::InvalidArchive(
6308 "bootstrap sidecar cap overflow",
6309 ))?;
6310 }
6311 if header.has_index_root_records() {
6312 cap = checked_u64_add(
6313 cap,
6314 max_record_section_bytes,
6315 "bootstrap sidecar cap overflow",
6316 )?;
6317 }
6318 if header.has_dictionary_records() {
6319 cap = checked_u64_add(
6320 cap,
6321 max_record_section_bytes,
6322 "bootstrap sidecar cap overflow",
6323 )?;
6324 }
6325 if file_size > cap {
6326 return Err(FormatError::InvalidArchive(
6327 "bootstrap sidecar exceeds resource cap",
6328 ));
6329 }
6330 Ok(())
6331}
6332
6333#[derive(Debug, Clone, Copy)]
6334struct SidecarBlockRecordsSection {
6335 offset: u64,
6336 length: u64,
6337 extent: ObjectExtent,
6338 data_kind: BlockKind,
6339 parity_kind: BlockKind,
6340 structure: &'static str,
6341}
6342
6343fn parse_sidecar_block_records(
6344 sidecar_bytes: &[u8],
6345 block_size: usize,
6346 section: SidecarBlockRecordsSection,
6347) -> Result<Vec<BlockRecord>, FormatError> {
6348 let record_len = block_size
6349 .checked_add(BLOCK_RECORD_FRAMING_LEN)
6350 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6351 if section.length % record_len as u64 != 0 {
6352 return Err(FormatError::InvalidArchive(
6353 "sidecar BlockRecord section is not aligned",
6354 ));
6355 }
6356 let expected_count =
6357 section.extent.data_block_count as usize + section.extent.parity_block_count as usize;
6358 let actual_count = usize::try_from(section.length / record_len as u64)
6359 .map_err(|_| FormatError::InvalidArchive("sidecar BlockRecord count overflow"))?;
6360 if actual_count != expected_count {
6361 return Err(FormatError::InvalidArchive(
6362 "sidecar BlockRecord section does not match declared extent",
6363 ));
6364 }
6365 let start = to_usize(section.offset, "BootstrapSidecarHeader")?;
6366 let raw = slice(
6367 sidecar_bytes,
6368 start,
6369 to_usize(section.length, "BootstrapSidecarHeader")?,
6370 "BootstrapSidecarHeader",
6371 )?;
6372 let mut records = Vec::with_capacity(expected_count);
6373
6374 for idx in 0..expected_count {
6375 let record = BlockRecord::parse(
6376 slice(raw, idx * record_len, record_len, "BlockRecord")?,
6377 block_size,
6378 )?;
6379 let expected_block_index = checked_u64_add(
6380 section.extent.first_block_index,
6381 idx as u64,
6382 section.structure,
6383 )?;
6384 if record.block_index != expected_block_index {
6385 return Err(FormatError::InvalidArchive(
6386 "sidecar BlockRecord section has missing or duplicate blocks",
6387 ));
6388 }
6389 let expected_kind = if idx < section.extent.data_block_count as usize {
6390 section.data_kind
6391 } else {
6392 section.parity_kind
6393 };
6394 if record.kind != expected_kind {
6395 return Err(FormatError::InvalidArchive(
6396 "sidecar BlockRecord section has wrong kind",
6397 ));
6398 }
6399 let should_be_last = idx + 1 == section.extent.data_block_count as usize;
6400 if idx < section.extent.data_block_count as usize && record.is_last_data() != should_be_last
6401 {
6402 return Err(FormatError::InvalidArchive(
6403 "sidecar BlockRecord section has wrong last-data flag",
6404 ));
6405 }
6406 records.push(record);
6407 }
6408
6409 Ok(records)
6410}
6411
6412fn validate_trailer_identity(
6413 volume_header: &VolumeHeader,
6414 trailer: &VolumeTrailer,
6415) -> Result<(), FormatError> {
6416 if trailer.archive_uuid != volume_header.archive_uuid
6417 || trailer.session_id != volume_header.session_id
6418 || trailer.volume_index != volume_header.volume_index
6419 {
6420 return Err(FormatError::InvalidArchive(
6421 "VolumeTrailer identity does not match VolumeHeader",
6422 ));
6423 }
6424 Ok(())
6425}
6426
6427fn validate_manifest_footer(
6428 volume_header: &VolumeHeader,
6429 crypto_header: &CryptoHeaderFixed,
6430 footer: &ManifestFooter,
6431 subkeys: &Subkeys,
6432 raw: &[u8],
6433) -> Result<(), FormatError> {
6434 if footer.archive_uuid != volume_header.archive_uuid
6435 || footer.session_id != volume_header.session_id
6436 || footer.volume_index != volume_header.volume_index
6437 {
6438 return Err(FormatError::InvalidArchive(
6439 "ManifestFooter identity does not match VolumeHeader",
6440 ));
6441 }
6442 if footer.total_volumes != volume_header.stripe_width {
6443 return Err(FormatError::InvalidArchive(
6444 "ManifestFooter total_volumes does not match stripe_width",
6445 ));
6446 }
6447 if footer.is_authoritative != 1 {
6448 return Err(FormatError::InvalidArchive(
6449 "ManifestFooter is not authoritative",
6450 ));
6451 }
6452 verify_integrity_tag(
6453 HmacDomain::ManifestFooter,
6454 crypto_header.aead_algo,
6455 Some(&subkeys.mac_key),
6456 &volume_header.archive_uuid,
6457 &volume_header.session_id,
6458 &raw[..MANIFEST_HMAC_COVERED_LEN],
6459 &footer.manifest_hmac,
6460 )
6461}
6462
6463#[derive(Debug)]
6464struct ParsedBlockRegion {
6465 blocks: BTreeMap<u64, BlockRecord>,
6466 erased_block_indices: BTreeSet<u64>,
6467}
6468
6469fn parse_block_region(
6470 bytes: &[u8],
6471 start: usize,
6472 end: usize,
6473 block_size: usize,
6474 volume_header: &VolumeHeader,
6475 trailer: &VolumeTrailer,
6476) -> Result<ParsedBlockRegion, FormatError> {
6477 if end < start {
6478 return Err(FormatError::InvalidArchive(
6479 "ManifestFooter starts before BlockRecord region",
6480 ));
6481 }
6482 let record_len = block_size
6483 .checked_add(BLOCK_RECORD_FRAMING_LEN)
6484 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6485 let region_len = end - start;
6486 if region_len % record_len != 0 {
6487 return Err(FormatError::InvalidArchive(
6488 "BlockRecord region length is not aligned",
6489 ));
6490 }
6491 let observed_count = region_len / record_len;
6492 if observed_count as u64 != trailer.block_count {
6493 return Err(FormatError::InvalidArchive(
6494 "VolumeTrailer block_count does not match BlockRecord region",
6495 ));
6496 }
6497
6498 let mut blocks = BTreeMap::new();
6499 let mut erased_block_indices = BTreeSet::new();
6500 for idx in 0..observed_count {
6501 let offset = start + idx * record_len;
6502 let expected_block_index = checked_u64_add(
6503 volume_header.volume_index as u64,
6504 checked_u64_mul(
6505 idx as u64,
6506 volume_header.stripe_width as u64,
6507 "BlockRecord index overflow",
6508 )?,
6509 "BlockRecord index overflow",
6510 )?;
6511 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
6512 match BlockRecord::parse(raw, block_size) {
6513 Ok(record) => {
6514 if record.block_index != expected_block_index {
6515 return Err(FormatError::InvalidArchive(
6516 "BlockRecord index does not match volume position",
6517 ));
6518 }
6519 if blocks.insert(record.block_index, record).is_some() {
6520 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
6521 }
6522 }
6523 Err(err) if block_record_error_is_recoverable_erasure(&err) => {
6524 if !erased_block_indices.insert(expected_block_index) {
6525 return Err(FormatError::InvalidArchive(
6526 "duplicate erased BlockRecord index",
6527 ));
6528 }
6529 }
6530 Err(err) => return Err(err),
6531 }
6532 }
6533
6534 Ok(ParsedBlockRegion {
6535 blocks,
6536 erased_block_indices,
6537 })
6538}
6539
6540fn validate_seekable_block_region_layout(
6541 start: u64,
6542 end: u64,
6543 block_size: usize,
6544 trailer: &VolumeTrailer,
6545) -> Result<(), FormatError> {
6546 if end < start {
6547 return Err(FormatError::InvalidArchive(
6548 "ManifestFooter starts before BlockRecord region",
6549 ));
6550 }
6551 let record_len = block_record_len(block_size)?;
6552 let region_len = end - start;
6553 if region_len % record_len != 0 {
6554 return Err(FormatError::InvalidArchive(
6555 "BlockRecord region length is not aligned",
6556 ));
6557 }
6558 let observed_count = region_len / record_len;
6559 if observed_count != trailer.block_count {
6560 return Err(FormatError::InvalidArchive(
6561 "VolumeTrailer block_count does not match BlockRecord region",
6562 ));
6563 }
6564 Ok(())
6565}
6566
6567fn parse_public_block_observation(
6568 bytes: &[u8],
6569 start: usize,
6570 image: &CriticalMetadataImage,
6571 block_size: usize,
6572 volume_header: &VolumeHeader,
6573) -> Result<BTreeMap<u64, BlockRecord>, FormatError> {
6574 let image_start = to_usize(image.block_records_offset, "BlockRecord")?;
6575 if start != image_start {
6576 return Err(FormatError::InvalidArchive(
6577 "public BlockRecord observation start mismatch",
6578 ));
6579 }
6580 let scan_limit_u64 = image
6581 .block_records_offset
6582 .checked_add(image.block_records_length)
6583 .ok_or(FormatError::InvalidArchive(
6584 "public BlockRecord observation limit overflow",
6585 ))?;
6586 if scan_limit_u64 != image.manifest_footer_offset {
6587 return Err(FormatError::InvalidArchive(
6588 "public BlockRecord observation limit mismatch",
6589 ));
6590 }
6591 let scan_limit = to_usize(scan_limit_u64, "BlockRecord")?;
6592 if scan_limit < start {
6593 return Err(FormatError::InvalidArchive(
6594 "public BlockRecord observation limit before start",
6595 ));
6596 }
6597 let record_len = block_size
6598 .checked_add(BLOCK_RECORD_FRAMING_LEN)
6599 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6600 let region_len = scan_limit - start;
6601 if region_len % record_len != 0 {
6602 return Err(FormatError::InvalidArchive(
6603 "public BlockRecord observation window is not aligned",
6604 ));
6605 }
6606
6607 let mut blocks = BTreeMap::new();
6608 let mut offset = start;
6609 let mut observed_slot = 0u64;
6610 while offset < scan_limit {
6611 let magic_end = checked_add(offset, 4, "BlockRecord")?;
6612 if magic_end > scan_limit || bytes.get(offset..magic_end) != Some(b"TZBK") {
6613 break;
6614 }
6615 let record_end = checked_add(offset, record_len, "BlockRecord")?;
6616 if record_end > scan_limit {
6617 return Err(FormatError::InvalidArchive(
6618 "public BlockRecord observation slot is incomplete",
6619 ));
6620 }
6621 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
6622 let record = BlockRecord::parse(raw, block_size)?;
6623 let expected_block_index = checked_u64_add(
6624 volume_header.volume_index as u64,
6625 checked_u64_mul(
6626 observed_slot,
6627 volume_header.stripe_width as u64,
6628 "BlockRecord index overflow",
6629 )?,
6630 "BlockRecord index overflow",
6631 )?;
6632 if record.block_index != expected_block_index {
6633 return Err(FormatError::InvalidArchive(
6634 "public BlockRecord index does not match volume position",
6635 ));
6636 }
6637 if blocks.insert(record.block_index, record).is_some() {
6638 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
6639 }
6640 offset = record_end;
6641 observed_slot = observed_slot
6642 .checked_add(1)
6643 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
6644 }
6645
6646 let mut scan = if offset < scan_limit {
6647 checked_add(offset, record_len, "BlockRecord")?
6648 } else {
6649 scan_limit
6650 };
6651 while scan < scan_limit {
6652 let magic_end = checked_add(scan, 4, "BlockRecord")?;
6653 let record_end = checked_add(scan, record_len, "BlockRecord")?;
6654 if record_end <= scan_limit && bytes.get(scan..magic_end) == Some(b"TZBK") {
6655 let raw = slice(bytes, scan, record_len, "BlockRecord")?;
6656 if BlockRecord::parse(raw, block_size).is_ok() {
6657 return Err(FormatError::InvalidArchive(
6658 "public observation has ambiguous extra BlockRecord",
6659 ));
6660 }
6661 }
6662 scan = record_end;
6663 }
6664
6665 Ok(blocks)
6666}
6667
6668pub(crate) fn block_record_error_is_recoverable_erasure(error: &FormatError) -> bool {
6669 matches!(
6670 error,
6671 FormatError::BadCrc {
6672 structure: "BlockRecord",
6673 } | FormatError::BadMagic {
6674 structure: "BlockRecord",
6675 } | FormatError::NonZeroReserved {
6676 structure: "BlockRecord",
6677 }
6678 )
6679}
6680
6681fn block_record_len(block_size: usize) -> Result<u64, FormatError> {
6682 let len = block_size
6683 .checked_add(BLOCK_RECORD_FRAMING_LEN)
6684 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6685 u64::try_from(len).map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))
6686}
6687
6688fn checked_u64_mul(lhs: u64, rhs: u64, reason: &'static str) -> Result<u64, FormatError> {
6689 lhs.checked_mul(rhs)
6690 .ok_or(FormatError::InvalidArchive(reason))
6691}
6692
6693fn parse_stream_block_prefix(
6694 bytes: &[u8],
6695 start: usize,
6696 block_size: usize,
6697 volume_header: &VolumeHeader,
6698) -> Result<(BTreeMap<u64, BlockRecord>, usize, u64), FormatError> {
6699 let record_len = block_size
6700 .checked_add(BLOCK_RECORD_FRAMING_LEN)
6701 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6702 let mut blocks = BTreeMap::new();
6703 let mut offset = start;
6704 let mut observed_block_count = 0u64;
6705
6706 while bytes.get(offset..offset + 4) == Some(b"TZBK") {
6707 let expected_block_index =
6708 expected_stream_block_index(volume_header, observed_block_count)?;
6709 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
6710 match BlockRecord::parse(raw, block_size) {
6711 Ok(record) => {
6712 if record.block_index != expected_block_index {
6713 return Err(FormatError::InvalidArchive(
6714 "BlockRecord index does not match stream position",
6715 ));
6716 }
6717 if blocks.insert(record.block_index, record).is_some() {
6718 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
6719 }
6720 }
6721 Err(err) if block_record_error_is_recoverable_erasure(&err) => {}
6722 Err(err) => return Err(err),
6723 }
6724 offset = checked_add(offset, record_len, "BlockRecord")?;
6725 observed_block_count = observed_block_count
6726 .checked_add(1)
6727 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
6728 }
6729
6730 Ok((blocks, offset, observed_block_count))
6731}
6732
6733pub(crate) fn expected_stream_block_index(
6734 volume_header: &VolumeHeader,
6735 observed_block_count: u64,
6736) -> Result<u64, FormatError> {
6737 checked_u64_add(
6738 volume_header.volume_index as u64,
6739 checked_u64_mul(
6740 observed_block_count,
6741 volume_header.stripe_width as u64,
6742 "BlockRecord index overflow",
6743 )?,
6744 "BlockRecord index overflow",
6745 )
6746}
6747
6748fn parse_sequential_block_or_erasure(
6749 bytes: &[u8],
6750 offset: usize,
6751 record_len: usize,
6752 block_size: usize,
6753 volume_header: &VolumeHeader,
6754 observed_block_count: u64,
6755) -> Result<Option<BlockRecord>, FormatError> {
6756 let expected_block_index = expected_stream_block_index(volume_header, observed_block_count)?;
6757 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
6758 match BlockRecord::parse(raw, block_size) {
6759 Ok(record) => {
6760 if record.block_index != expected_block_index {
6761 return Err(FormatError::InvalidArchive(
6762 "BlockRecord index does not match stream position",
6763 ));
6764 }
6765 Ok(Some(record))
6766 }
6767 Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
6768 Err(err) => Err(err),
6769 }
6770}
6771
6772fn parse_terminal_material(
6773 bytes: &[u8],
6774 manifest_offset: usize,
6775 observed_block_count: u64,
6776 context: KeyHoldingTerminalContext<'_>,
6777 options: ReaderOptions,
6778) -> Result<(ManifestFooter, VolumeTrailer, Option<RootAuthFooterV1>), FormatError> {
6779 let candidate = locate_v41_terminal_candidate(bytes, context, options)?;
6780 if !terminal_candidate_reaches_eof(&candidate, bytes.len())? {
6781 return Err(FormatError::InvalidArchive(
6782 "sequential terminal does not end at EOF",
6783 ));
6784 }
6785 let terminal = candidate.terminal;
6786 if terminal.image.manifest_footer_offset != manifest_offset as u64 {
6787 return Err(FormatError::InvalidArchive(
6788 "VolumeTrailer ManifestFooter offset does not match observed stream offset",
6789 ));
6790 }
6791 if terminal.volume_trailer.block_count != observed_block_count {
6792 return Err(FormatError::InvalidArchive(
6793 "VolumeTrailer block_count does not match observed stream",
6794 ));
6795 }
6796 let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
6797 Ok((
6798 manifest_footer,
6799 terminal.volume_trailer,
6800 terminal.root_auth_footer,
6801 ))
6802}
6803
6804pub(crate) fn parse_terminal_material_read_at(
6805 reader: &dyn ArchiveReadAt,
6806 input_len: u64,
6807 manifest_offset: u64,
6808 observed_block_count: u64,
6809 context: KeyHoldingTerminalContext<'_>,
6810) -> Result<SequentialTerminalMaterial, FormatError> {
6811 let mut candidates = Vec::new();
6812 if input_len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
6813 collect_v41_locator_candidate_read_at(
6814 reader,
6815 input_len - CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6816 0,
6817 context,
6818 &mut candidates,
6819 );
6820 }
6821 if input_len >= LOCATOR_PAIR_LEN as u64 {
6822 collect_v41_locator_candidate_read_at(
6823 reader,
6824 input_len - LOCATOR_PAIR_LEN as u64,
6825 1,
6826 context,
6827 &mut candidates,
6828 );
6829 }
6830
6831 let candidate = choose_v41_terminal_candidate(candidates)?;
6832 if !terminal_candidate_reaches_eof(&candidate, to_usize(input_len, "terminal EOF")?)? {
6833 return Err(FormatError::InvalidArchive(
6834 "sequential terminal does not end at EOF",
6835 ));
6836 }
6837 let terminal = candidate.terminal;
6838 if terminal.image.manifest_footer_offset != manifest_offset {
6839 return Err(FormatError::InvalidArchive(
6840 "VolumeTrailer ManifestFooter offset does not match observed stream offset",
6841 ));
6842 }
6843 if terminal.volume_trailer.block_count != observed_block_count {
6844 return Err(FormatError::InvalidArchive(
6845 "VolumeTrailer block_count does not match observed stream",
6846 ));
6847 }
6848 let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
6849 Ok(SequentialTerminalMaterial {
6850 manifest_footer,
6851 volume_trailer: terminal.volume_trailer,
6852 root_auth_footer: terminal.root_auth_footer,
6853 })
6854}
6855
6856fn terminal_candidate_reaches_eof(
6857 candidate: &TerminalCandidate,
6858 input_len: usize,
6859) -> Result<bool, FormatError> {
6860 let expected_end =
6861 match candidate.locator_sequence {
6862 Some(0) => candidate.anchor,
6863 Some(1) => candidate
6864 .anchor
6865 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6866 .ok_or(FormatError::InvalidArchive(
6867 "terminal EOF boundary overflow",
6868 ))?,
6869 None => candidate.anchor.checked_add(LOCATOR_PAIR_LEN).ok_or(
6870 FormatError::InvalidArchive("terminal EOF boundary overflow"),
6871 )?,
6872 Some(_) => {
6873 return Err(FormatError::InvalidArchive(
6874 "invalid terminal locator sequence",
6875 ))
6876 }
6877 };
6878 Ok(expected_end == input_len)
6879}
6880
6881#[derive(Debug, Default)]
6882struct PendingSequentialEnvelope {
6883 data_shards: Vec<Option<Vec<u8>>>,
6884 parity_shards: Vec<Option<Vec<u8>>>,
6885 saw_last_data: bool,
6886 awaiting_tentative_parity: bool,
6887}
6888
6889impl PendingSequentialEnvelope {
6890 fn is_empty(&self) -> bool {
6891 self.data_shards.is_empty() && self.parity_shards.is_empty()
6892 }
6893}
6894
6895fn handle_sequential_payload_erasure(
6896 pending: &mut PendingSequentialEnvelope,
6897 crypto_header: &CryptoHeaderFixed,
6898 metadata_seen: bool,
6899) -> Result<(), FormatError> {
6900 if metadata_seen || pending.saw_last_data {
6901 return Err(FormatError::BadCrc {
6902 structure: "BlockRecord",
6903 });
6904 }
6905 if !sequential_payload_parity_is_guaranteed(crypto_header) {
6906 return Err(FormatError::BadCrc {
6907 structure: "BlockRecord",
6908 });
6909 }
6910 pending.data_shards.push(None);
6911 pending.awaiting_tentative_parity = true;
6912 if pending.data_shards.len() > crypto_header.fec_data_shards as usize {
6913 return Err(FormatError::InvalidArchive(
6914 "sequential payload envelope exceeds data-shard cap",
6915 ));
6916 }
6917 Ok(())
6918}
6919
6920fn sequential_payload_parity_is_guaranteed(crypto_header: &CryptoHeaderFixed) -> bool {
6921 crypto_header.fec_parity_shards > 0
6922 && (crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0)
6923}
6924
6925fn sequential_extract_tar_stream_with_options(
6926 bytes: &[u8],
6927 master_key: &MasterKey,
6928 options: ReaderOptions,
6929) -> Result<Vec<u8>, FormatError> {
6930 validate_reader_options(options)?;
6931 if bytes.len() < VOLUME_HEADER_LEN {
6932 return Err(FormatError::InvalidLength {
6933 structure: "archive",
6934 expected: VOLUME_HEADER_LEN,
6935 actual: bytes.len(),
6936 });
6937 }
6938
6939 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
6940 let crypto_start = volume_header.crypto_header_offset as usize;
6941 let crypto_len = volume_header.crypto_header_length as usize;
6942 let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
6943 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
6944 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
6945 let subkeys = subkeys_for_open(
6946 Some(master_key),
6947 parsed_crypto.fixed.aead_algo,
6948 &volume_header.archive_uuid,
6949 &volume_header.session_id,
6950 )?;
6951 verify_integrity_tag(
6952 HmacDomain::CryptoHeader,
6953 parsed_crypto.fixed.aead_algo,
6954 Some(&subkeys.mac_key),
6955 &volume_header.archive_uuid,
6956 &volume_header.session_id,
6957 parsed_crypto.hmac_covered_bytes,
6958 &parsed_crypto.header_hmac,
6959 )?;
6960 parsed_crypto.validate_extension_semantics()?;
6961 validate_sequential_supported_volume(
6962 &volume_header,
6963 &parsed_crypto.fixed,
6964 &parsed_crypto.extensions,
6965 )?;
6966 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
6967
6968 let block_size = parsed_crypto.fixed.block_size as usize;
6969 let record_len = block_size
6970 .checked_add(BLOCK_RECORD_FRAMING_LEN)
6971 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6972 let mut offset = crypto_end;
6973 let mut observed_block_count = 0u64;
6974 let mut metadata_seen = false;
6975 let mut pending = PendingSequentialEnvelope::default();
6976 let mut next_envelope_index = 0u64;
6977 let mut tar_stream = Vec::new();
6978 let max_tar_stream_size = options.max_verify_tar_size;
6979 let observed_archive_bytes = observed_archive_size([bytes.len() as u64])?;
6980 let total_extraction_cap = total_extraction_size_cap(options, observed_archive_bytes);
6981 let mut tar_stream_total_validator = TarStreamTotalExtractionSizeValidator::new(
6982 parsed_crypto.fixed.max_path_length,
6983 total_extraction_cap,
6984 );
6985
6986 while bytes.get(offset..offset + 4) == Some(b"TZBK") {
6987 let record = parse_sequential_block_or_erasure(
6988 bytes,
6989 offset,
6990 record_len,
6991 block_size,
6992 &volume_header,
6993 observed_block_count,
6994 )?;
6995 observed_block_count = observed_block_count
6996 .checked_add(1)
6997 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
6998 let Some(record) = record else {
6999 handle_sequential_payload_erasure(&mut pending, &parsed_crypto.fixed, metadata_seen)?;
7000 offset = checked_add(offset, record_len, "BlockRecord")?;
7001 continue;
7002 };
7003
7004 match record.kind {
7005 BlockKind::PayloadData => {
7006 if metadata_seen {
7007 return Err(FormatError::InvalidArchive(
7008 "payload BlockRecord appears after metadata",
7009 ));
7010 }
7011 if pending.awaiting_tentative_parity {
7012 return Err(FormatError::InvalidArchive(
7013 "sequential payload envelope boundary is ambiguous after CRC erasure",
7014 ));
7015 }
7016 if pending.saw_last_data {
7017 finalize_sequential_envelope(
7018 &mut pending,
7019 SequentialEnvelopeDecodeContext {
7020 crypto_header: &parsed_crypto.fixed,
7021 subkeys: &subkeys,
7022 volume_header: &volume_header,
7023 next_envelope_index: &mut next_envelope_index,
7024 tar_stream: &mut tar_stream,
7025 max_tar_stream_size,
7026 tar_stream_total_validator: &mut tar_stream_total_validator,
7027 },
7028 )?;
7029 }
7030 let is_last_data = record.is_last_data();
7031 pending.data_shards.push(Some(record.payload));
7032 if is_last_data {
7033 pending.saw_last_data = true;
7034 }
7035 if pending.data_shards.len() > parsed_crypto.fixed.fec_data_shards as usize {
7036 return Err(FormatError::InvalidArchive(
7037 "sequential payload envelope exceeds data-shard cap",
7038 ));
7039 }
7040 }
7041 BlockKind::PayloadParity => {
7042 if metadata_seen {
7043 return Err(FormatError::InvalidArchive(
7044 "payload parity BlockRecord appears after metadata",
7045 ));
7046 }
7047 if pending.awaiting_tentative_parity {
7048 pending.awaiting_tentative_parity = false;
7049 pending.saw_last_data = true;
7050 } else if pending.data_shards.is_empty() || !pending.saw_last_data {
7051 return Err(FormatError::InvalidArchive(
7052 "payload parity appears before envelope data is complete",
7053 ));
7054 }
7055 pending.parity_shards.push(Some(record.payload));
7056 if pending.parity_shards.len() > parsed_crypto.fixed.fec_parity_shards as usize {
7057 return Err(FormatError::InvalidArchive(
7058 "sequential payload envelope exceeds parity-shard cap",
7059 ));
7060 }
7061 }
7062 _ => {
7063 if !pending.is_empty() {
7064 finalize_sequential_envelope(
7065 &mut pending,
7066 SequentialEnvelopeDecodeContext {
7067 crypto_header: &parsed_crypto.fixed,
7068 subkeys: &subkeys,
7069 volume_header: &volume_header,
7070 next_envelope_index: &mut next_envelope_index,
7071 tar_stream: &mut tar_stream,
7072 max_tar_stream_size,
7073 tar_stream_total_validator: &mut tar_stream_total_validator,
7074 },
7075 )?;
7076 }
7077 metadata_seen = true;
7078 }
7079 }
7080
7081 offset = checked_add(offset, record_len, "BlockRecord")?;
7082 }
7083
7084 if !pending.is_empty() {
7085 finalize_sequential_envelope(
7086 &mut pending,
7087 SequentialEnvelopeDecodeContext {
7088 crypto_header: &parsed_crypto.fixed,
7089 subkeys: &subkeys,
7090 volume_header: &volume_header,
7091 next_envelope_index: &mut next_envelope_index,
7092 tar_stream: &mut tar_stream,
7093 max_tar_stream_size,
7094 tar_stream_total_validator: &mut tar_stream_total_validator,
7095 },
7096 )?;
7097 }
7098
7099 parse_terminal_material(
7100 bytes,
7101 offset,
7102 observed_block_count,
7103 KeyHoldingTerminalContext {
7104 subkeys: &subkeys,
7105 volume_header: &volume_header,
7106 crypto_header: &parsed_crypto.fixed,
7107 crypto_header_bytes: crypto_bytes,
7108 },
7109 options,
7110 )?;
7111 validate_tar_stream_total_extraction_size(
7114 &tar_stream,
7115 parsed_crypto.fixed.max_path_length,
7116 total_extraction_cap,
7117 )?;
7118 Ok(tar_stream)
7119}
7120
7121fn validate_sequential_supported_volume(
7122 volume_header: &VolumeHeader,
7123 crypto_header: &CryptoHeaderFixed,
7124 extensions: &[ExtensionTlv<'_>],
7125) -> Result<(), FormatError> {
7126 reject_unsupported_raw_stream_profile(extensions)?;
7127 if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
7128 return Err(FormatError::ReaderUnsupported(
7129 "sequential reader supports only single-volume archive input",
7130 ));
7131 }
7132 if crypto_header.stripe_width != volume_header.stripe_width {
7133 return Err(FormatError::InvalidArchive(
7134 "VolumeHeader and CryptoHeader stripe_width differ",
7135 ));
7136 }
7137 if crypto_header.has_dictionary != 0 {
7138 return Err(FormatError::ReaderUnsupported(
7139 "dictionary bootstrap required for non-seekable sequential extraction",
7140 ));
7141 }
7142 Ok(())
7143}
7144
7145struct SequentialEnvelopeDecodeContext<'a> {
7146 crypto_header: &'a CryptoHeaderFixed,
7147 subkeys: &'a Subkeys,
7148 volume_header: &'a VolumeHeader,
7149 next_envelope_index: &'a mut u64,
7150 tar_stream: &'a mut Vec<u8>,
7151 max_tar_stream_size: usize,
7152 tar_stream_total_validator: &'a mut TarStreamTotalExtractionSizeValidator,
7153}
7154
7155fn finalize_sequential_envelope(
7156 pending: &mut PendingSequentialEnvelope,
7157 context: SequentialEnvelopeDecodeContext<'_>,
7158) -> Result<(), FormatError> {
7159 if !pending.saw_last_data {
7160 return Err(FormatError::InvalidArchive(
7161 "sequential payload envelope is missing last-data flag",
7162 ));
7163 }
7164 if pending.data_shards.len() > context.crypto_header.fec_data_shards as usize {
7165 return Err(FormatError::InvalidArchive(
7166 "sequential payload envelope exceeds data-shard cap",
7167 ));
7168 }
7169 if pending.parity_shards.len() > context.crypto_header.fec_parity_shards as usize {
7170 return Err(FormatError::InvalidArchive(
7171 "sequential payload envelope exceeds parity-shard cap",
7172 ));
7173 }
7174 let required_parity =
7175 required_object_parity(pending.data_shards.len() as u64, context.crypto_header)?;
7176 if pending.parity_shards.len() < required_parity as usize {
7177 return Err(FormatError::InvalidArchive(
7178 "sequential payload envelope has insufficient parity for recovery settings",
7179 ));
7180 }
7181
7182 let repaired = repair_data_gf16(
7183 &pending.data_shards,
7184 &pending.parity_shards,
7185 context.crypto_header.block_size as usize,
7186 )?;
7187 let mut encrypted =
7188 Vec::with_capacity(repaired.len() * context.crypto_header.block_size as usize);
7189 for shard in repaired {
7190 encrypted.extend_from_slice(&shard);
7191 }
7192 let plaintext = decrypt_padded_aead_object(
7193 AeadObjectContext {
7194 algo: context.crypto_header.aead_algo,
7195 key: &context.subkeys.enc_key,
7196 nonce_seed: &context.subkeys.nonce_seed,
7197 domain: b"envelope",
7198 archive_uuid: &context.volume_header.archive_uuid,
7199 session_id: &context.volume_header.session_id,
7200 counter: *context.next_envelope_index,
7201 },
7202 &encrypted,
7203 )?;
7204 decode_concatenated_zstd_frames_with_cap(
7205 &plaintext,
7206 None,
7207 context.tar_stream,
7208 context.max_tar_stream_size,
7209 Some(context.tar_stream_total_validator),
7210 )?;
7211 *context.next_envelope_index = (*context.next_envelope_index)
7212 .checked_add(1)
7213 .ok_or(FormatError::InvalidArchive("envelope counter overflow"))?;
7214 *pending = PendingSequentialEnvelope::default();
7215 Ok(())
7216}
7217
7218fn decode_concatenated_zstd_frames_with_cap(
7219 plaintext: &[u8],
7220 dictionary: Option<&[u8]>,
7221 output: &mut Vec<u8>,
7222 max_output_len: usize,
7223 mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
7224) -> Result<(), FormatError> {
7225 let mut cursor = 0usize;
7226 while cursor < plaintext.len() {
7227 let frame_len = zstd_safe::find_frame_compressed_size(&plaintext[cursor..])
7228 .map_err(|_| FormatError::InvalidZstdFrame)?;
7229 if frame_len == 0 {
7230 return Err(FormatError::InvalidZstdFrame);
7231 }
7232 let end = checked_add(cursor, frame_len, "zstd frame")?;
7233 validate_exact_zstd_frame(&plaintext[cursor..end])?;
7234 if let Some(dictionary) = dictionary {
7235 let mut decoder =
7236 zstd::stream::Decoder::with_dictionary(&plaintext[cursor..end], dictionary)
7237 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
7238 read_zstd_frame_to_capped_output(
7239 &mut decoder,
7240 output,
7241 max_output_len,
7242 tar_stream_total_validator.as_deref_mut(),
7243 )?;
7244 } else {
7245 let mut decoder = zstd::stream::Decoder::new(&plaintext[cursor..end])
7246 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
7247 read_zstd_frame_to_capped_output(
7248 &mut decoder,
7249 output,
7250 max_output_len,
7251 tar_stream_total_validator.as_deref_mut(),
7252 )?;
7253 }
7254 cursor = end;
7255 }
7256 Ok(())
7257}
7258
7259fn read_zstd_frame_to_capped_output<R: Read>(
7260 decoder: &mut R,
7261 output: &mut Vec<u8>,
7262 max_output_len: usize,
7263 mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
7264) -> Result<(), FormatError> {
7265 let mut buf = [0u8; 64 * 1024];
7266 loop {
7267 let read = decoder
7268 .read(&mut buf)
7269 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
7270 if read == 0 {
7271 return Ok(());
7272 }
7273 let next_len = output
7274 .len()
7275 .checked_add(read)
7276 .ok_or(FormatError::ReaderUnsupported(
7277 "sequential tar stream exceeds configured verification cap",
7278 ))?;
7279 if next_len > max_output_len {
7280 return Err(FormatError::ReaderUnsupported(
7281 "sequential tar stream exceeds configured verification cap",
7282 ));
7283 }
7284 output.extend_from_slice(&buf[..read]);
7285 if let Some(validator) = tar_stream_total_validator.as_mut() {
7286 validator.observe(output)?;
7287 }
7288 }
7289}
7290
7291fn load_archive_dictionary(
7292 blocks: &impl BlockProvider,
7293 subkeys: &Subkeys,
7294 volume_header: &VolumeHeader,
7295 crypto_header: &CryptoHeaderFixed,
7296 index_root: &IndexRoot,
7297) -> Result<Option<Vec<u8>>, FormatError> {
7298 if crypto_header.has_dictionary == 0 {
7299 return Ok(None);
7300 }
7301 let plaintext = load_metadata_object_from_parts(
7302 blocks,
7303 ObjectLoadContext::dictionary(volume_header, crypto_header, subkeys, index_root)?,
7304 index_root.header.dictionary_decompressed_size,
7305 )?;
7306 Ok(Some(plaintext))
7307}
7308
7309#[derive(Clone, Copy)]
7310struct ObjectLoadContext<'a> {
7311 volume_header: &'a VolumeHeader,
7312 crypto_header: &'a CryptoHeaderFixed,
7313 extent: ObjectExtent,
7314 data_kind: BlockKind,
7315 parity_kind: BlockKind,
7316 key: &'a [u8; 32],
7317 nonce_seed: &'a [u8; 32],
7318 domain: &'a [u8],
7319 counter: u64,
7320 class_data_shard_max: u16,
7321 class_parity_shard_max: u16,
7322}
7323
7324impl<'a> ObjectLoadContext<'a> {
7325 fn index_root(
7326 volume_header: &'a VolumeHeader,
7327 crypto_header: &'a CryptoHeaderFixed,
7328 subkeys: &'a Subkeys,
7329 extent: ObjectExtent,
7330 ) -> Self {
7331 Self {
7332 volume_header,
7333 crypto_header,
7334 extent,
7335 data_kind: BlockKind::IndexRootData,
7336 parity_kind: BlockKind::IndexRootParity,
7337 key: &subkeys.index_root_key,
7338 nonce_seed: &subkeys.index_nonce_seed,
7339 domain: b"idxroot",
7340 counter: 0,
7341 class_data_shard_max: crypto_header.index_root_fec_data_shards,
7342 class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
7343 }
7344 }
7345
7346 fn index_shard(
7347 volume_header: &'a VolumeHeader,
7348 crypto_header: &'a CryptoHeaderFixed,
7349 subkeys: &'a Subkeys,
7350 entry: &ShardEntry,
7351 ) -> Self {
7352 Self {
7353 volume_header,
7354 crypto_header,
7355 extent: ObjectExtent {
7356 first_block_index: entry.first_block_index,
7357 data_block_count: entry.data_block_count,
7358 parity_block_count: entry.parity_block_count,
7359 encrypted_size: entry.encrypted_size,
7360 },
7361 data_kind: BlockKind::IndexShardData,
7362 parity_kind: BlockKind::IndexShardParity,
7363 key: &subkeys.index_shard_key,
7364 nonce_seed: &subkeys.index_nonce_seed,
7365 domain: b"idxshard",
7366 counter: entry.shard_index,
7367 class_data_shard_max: crypto_header.index_fec_data_shards,
7368 class_parity_shard_max: crypto_header.index_fec_parity_shards,
7369 }
7370 }
7371
7372 fn directory_hint(
7373 volume_header: &'a VolumeHeader,
7374 crypto_header: &'a CryptoHeaderFixed,
7375 subkeys: &'a Subkeys,
7376 entry: &DirectoryHintShardEntry,
7377 ) -> Self {
7378 Self {
7379 volume_header,
7380 crypto_header,
7381 extent: ObjectExtent {
7382 first_block_index: entry.first_block_index,
7383 data_block_count: entry.data_block_count,
7384 parity_block_count: entry.parity_block_count,
7385 encrypted_size: entry.encrypted_size,
7386 },
7387 data_kind: BlockKind::DirectoryHintData,
7388 parity_kind: BlockKind::DirectoryHintParity,
7389 key: &subkeys.dir_hint_key,
7390 nonce_seed: &subkeys.index_nonce_seed,
7391 domain: b"dirhint",
7392 counter: entry.hint_shard_index,
7393 class_data_shard_max: crypto_header.index_fec_data_shards,
7394 class_parity_shard_max: crypto_header.index_fec_parity_shards,
7395 }
7396 }
7397
7398 fn dictionary(
7399 volume_header: &'a VolumeHeader,
7400 crypto_header: &'a CryptoHeaderFixed,
7401 subkeys: &'a Subkeys,
7402 index_root: &IndexRoot,
7403 ) -> Result<Self, FormatError> {
7404 Ok(Self {
7405 volume_header,
7406 crypto_header,
7407 extent: dictionary_extent_from_index_root(index_root)?,
7408 data_kind: BlockKind::DictionaryData,
7409 parity_kind: BlockKind::DictionaryParity,
7410 key: &subkeys.dictionary_key,
7411 nonce_seed: &subkeys.index_nonce_seed,
7412 domain: b"dict",
7413 counter: 0,
7414 class_data_shard_max: crypto_header.index_root_fec_data_shards,
7415 class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
7416 })
7417 }
7418
7419 fn payload(
7420 volume_header: &'a VolumeHeader,
7421 crypto_header: &'a CryptoHeaderFixed,
7422 subkeys: &'a Subkeys,
7423 envelope: &EnvelopeEntry,
7424 ) -> Self {
7425 Self {
7426 volume_header,
7427 crypto_header,
7428 extent: ObjectExtent {
7429 first_block_index: envelope.first_block_index,
7430 data_block_count: envelope.data_block_count,
7431 parity_block_count: envelope.parity_block_count,
7432 encrypted_size: envelope.encrypted_size,
7433 },
7434 data_kind: BlockKind::PayloadData,
7435 parity_kind: BlockKind::PayloadParity,
7436 key: &subkeys.enc_key,
7437 nonce_seed: &subkeys.nonce_seed,
7438 domain: b"envelope",
7439 counter: envelope.envelope_index,
7440 class_data_shard_max: crypto_header.fec_data_shards,
7441 class_parity_shard_max: crypto_header.fec_parity_shards,
7442 }
7443 }
7444}
7445
7446fn dictionary_extent_from_index_root(index_root: &IndexRoot) -> Result<ObjectExtent, FormatError> {
7447 if index_root.header.dictionary_data_block_count == 0
7448 || index_root.header.dictionary_encrypted_size == 0
7449 || index_root.header.dictionary_decompressed_size == 0
7450 {
7451 return Err(FormatError::InvalidArchive("dictionary bootstrap required"));
7452 }
7453 Ok(ObjectExtent {
7454 first_block_index: index_root.header.dictionary_first_block,
7455 data_block_count: index_root.header.dictionary_data_block_count,
7456 parity_block_count: index_root.header.dictionary_parity_block_count,
7457 encrypted_size: index_root.header.dictionary_encrypted_size,
7458 })
7459}
7460
7461fn load_metadata_object_from_parts(
7462 blocks: &impl BlockProvider,
7463 context: ObjectLoadContext<'_>,
7464 decompressed_size: u32,
7465) -> Result<Vec<u8>, FormatError> {
7466 let compressed = load_decrypted_object_from_parts(blocks, context)?;
7467 decompress_exact_zstd_frame(&compressed, decompressed_size as usize)
7468}
7469
7470fn load_decrypted_object_from_parts(
7471 blocks: &impl BlockProvider,
7472 context: ObjectLoadContext<'_>,
7473) -> Result<Vec<u8>, FormatError> {
7474 load_decrypted_object_from_parts_with_parity_policy(blocks, context, ParityReadPolicy::Always)
7475}
7476
7477fn load_decrypted_object_from_parts_with_parity_policy(
7478 blocks: &impl BlockProvider,
7479 context: ObjectLoadContext<'_>,
7480 parity_policy: ParityReadPolicy,
7481) -> Result<Vec<u8>, FormatError> {
7482 let repaired = load_repaired_object_data_shards_from_parts_with_parity_policy(
7483 blocks,
7484 context.crypto_header,
7485 context.extent,
7486 context.data_kind,
7487 context.parity_kind,
7488 context.class_data_shard_max,
7489 context.class_parity_shard_max,
7490 parity_policy,
7491 )?;
7492 let mut encrypted = Vec::with_capacity(context.extent.encrypted_size as usize);
7493 for shard in repaired {
7494 encrypted.extend_from_slice(&shard);
7495 }
7496 if encrypted.len() != context.extent.encrypted_size as usize {
7497 return Err(FormatError::InvalidArchive(
7498 "object encrypted size does not match repaired shards",
7499 ));
7500 }
7501
7502 decrypt_padded_aead_object(
7503 AeadObjectContext {
7504 algo: context.crypto_header.aead_algo,
7505 key: context.key,
7506 nonce_seed: context.nonce_seed,
7507 domain: context.domain,
7508 archive_uuid: &context.volume_header.archive_uuid,
7509 session_id: &context.volume_header.session_id,
7510 counter: context.counter,
7511 },
7512 &encrypted,
7513 )
7514}
7515
7516fn load_repaired_object_data_shards_from_parts(
7517 blocks: &impl BlockProvider,
7518 crypto_header: &CryptoHeaderFixed,
7519 extent: ObjectExtent,
7520 data_kind: BlockKind,
7521 parity_kind: BlockKind,
7522 class_data_shard_max: u16,
7523 class_parity_shard_max: u16,
7524) -> Result<Vec<Vec<u8>>, FormatError> {
7525 load_repaired_object_data_shards_from_parts_with_parity_policy(
7526 blocks,
7527 crypto_header,
7528 extent,
7529 data_kind,
7530 parity_kind,
7531 class_data_shard_max,
7532 class_parity_shard_max,
7533 ParityReadPolicy::Always,
7534 )
7535}
7536
7537#[allow(clippy::too_many_arguments)]
7538fn load_repaired_object_data_shards_from_parts_with_parity_policy(
7539 blocks: &impl BlockProvider,
7540 crypto_header: &CryptoHeaderFixed,
7541 extent: ObjectExtent,
7542 data_kind: BlockKind,
7543 parity_kind: BlockKind,
7544 class_data_shard_max: u16,
7545 class_parity_shard_max: u16,
7546 parity_policy: ParityReadPolicy,
7547) -> Result<Vec<Vec<u8>>, FormatError> {
7548 validate_object_extent(
7549 extent,
7550 crypto_header,
7551 class_data_shard_max,
7552 class_parity_shard_max,
7553 )?;
7554 let block_size = crypto_header.block_size as usize;
7555 let data_count = extent.data_block_count as usize;
7556 let parity_count = extent.parity_block_count as usize;
7557 let mut data_shards = Vec::with_capacity(data_count);
7558 let mut parity_shards = Vec::with_capacity(parity_count);
7559
7560 for offset in 0..data_count {
7561 let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
7562 if let Some(record) = blocks.block(block_index)? {
7563 if record.kind != data_kind {
7564 return Err(FormatError::InvalidArchive(
7565 "object data block has unexpected kind",
7566 ));
7567 }
7568 let should_be_last = offset + 1 == data_count;
7569 if record.is_last_data() != should_be_last {
7570 return Err(FormatError::InvalidArchive(
7571 "object last-data flag is not on the final data block",
7572 ));
7573 }
7574 data_shards.push(Some(record.payload.clone()));
7575 } else {
7576 data_shards.push(None);
7577 }
7578 }
7579
7580 if parity_policy == ParityReadPolicy::RepairOnly && data_shards.iter().all(Option::is_some) {
7581 return repair_data_gf16(&data_shards, &[], block_size);
7582 }
7583
7584 for offset in 0..parity_count {
7585 let block_index = checked_u64_add(
7586 extent.first_block_index,
7587 data_count as u64 + offset as u64,
7588 "object",
7589 )?;
7590 if let Some(record) = blocks.block(block_index)? {
7591 if record.kind != parity_kind {
7592 return Err(FormatError::InvalidArchive(
7593 "object parity block has unexpected kind",
7594 ));
7595 }
7596 if record.is_last_data() {
7597 return Err(FormatError::InvalidArchive(
7598 "object parity block has last-data flag",
7599 ));
7600 }
7601 parity_shards.push(Some(record.payload.clone()));
7602 } else {
7603 parity_shards.push(None);
7604 }
7605 }
7606
7607 repair_data_gf16(&data_shards, &parity_shards, block_size)
7608}
7609
7610fn validate_object_extent(
7611 extent: ObjectExtent,
7612 crypto_header: &CryptoHeaderFixed,
7613 class_data_shard_max: u16,
7614 class_parity_shard_max: u16,
7615) -> Result<(), FormatError> {
7616 if extent.data_block_count == 0 || extent.encrypted_size == 0 {
7617 return Err(FormatError::InvalidArchive(
7618 "encrypted object has zero data blocks or size",
7619 ));
7620 }
7621 if extent.data_block_count > class_data_shard_max as u32 {
7622 return Err(FormatError::InvalidArchive(
7623 "encrypted object exceeds its class data-shard maximum",
7624 ));
7625 }
7626 if extent.parity_block_count > class_parity_shard_max as u32 {
7627 return Err(FormatError::InvalidArchive(
7628 "encrypted object exceeds its class parity-shard maximum",
7629 ));
7630 }
7631 let required_parity = required_object_parity(extent.data_block_count as u64, crypto_header)?;
7632 if extent.parity_block_count != required_parity {
7633 return Err(FormatError::InvalidArchive(
7634 "encrypted object parity does not match v41 compute_parity",
7635 ));
7636 }
7637 let total = checked_u64_add(
7638 extent.data_block_count as u64,
7639 extent.parity_block_count as u64,
7640 "encrypted object shard count overflow",
7641 )?;
7642 if total > 65_535 {
7643 return Err(FormatError::FecTooManyShards(total as usize));
7644 }
7645 let expected = checked_u64_mul(
7646 extent.data_block_count as u64,
7647 crypto_header.block_size as u64,
7648 "encrypted object size overflow",
7649 )?;
7650 if expected != extent.encrypted_size as u64 {
7651 return Err(FormatError::InvalidArchive(
7652 "encrypted object size is not data_block_count * block_size",
7653 ));
7654 }
7655 if extent.encrypted_size as usize <= crypto_header.aead_algo.tag_len() {
7656 return Err(FormatError::InvalidArchive(
7657 "encrypted object is too small for AEAD tag",
7658 ));
7659 }
7660 Ok(())
7661}
7662
7663pub(crate) fn required_object_parity(
7664 data_block_count: u64,
7665 crypto_header: &CryptoHeaderFixed,
7666) -> Result<u32, FormatError> {
7667 let min_parity =
7668 if crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0 {
7669 1
7670 } else {
7671 0
7672 };
7673 let mut parity = 0u64;
7674 for _ in 0..100 {
7675 let total = data_block_count
7676 .checked_add(parity)
7677 .ok_or(FormatError::InvalidArchive("parity total overflow"))?;
7678 let by_volume = checked_u64_mul(
7679 crypto_header.volume_loss_tolerance as u64,
7680 ceil_div_u64(total, crypto_header.stripe_width as u64)?,
7681 "volume-loss parity overflow",
7682 )?;
7683 let by_bitrot = ceil_div_u64(
7684 checked_u64_mul(
7685 total,
7686 crypto_header.bit_rot_buffer_pct as u64,
7687 "bit-rot parity overflow",
7688 )?,
7689 100,
7690 )?;
7691 let next = by_volume
7692 .checked_add(by_bitrot)
7693 .ok_or(FormatError::InvalidArchive("parity overflow"))?
7694 .max(min_parity);
7695 if next == parity {
7696 return u32::try_from(next)
7697 .map_err(|_| FormatError::InvalidArchive("parity count overflow"));
7698 }
7699 parity = next;
7700 }
7701 Err(FormatError::InvalidArchive(
7702 "parity calculation did not converge",
7703 ))
7704}
7705
7706fn ceil_div_u64(numerator: u64, denominator: u64) -> Result<u64, FormatError> {
7707 if denominator == 0 {
7708 return Err(FormatError::InvalidArchive("division by zero"));
7709 }
7710 numerator
7711 .checked_add(denominator - 1)
7712 .ok_or(FormatError::InvalidArchive("ceiling division overflow"))
7713 .map(|value| value / denominator)
7714}
7715
7716fn frame_range_for_file<'b>(
7717 shard: &'b IndexShard,
7718 file: &FileEntry,
7719) -> Result<Vec<&'b FrameEntry>, FormatError> {
7720 let mut frames = Vec::with_capacity(file.frame_count as usize);
7721 for offset in 0..file.frame_count as u64 {
7722 let frame_index =
7723 file.first_frame_index
7724 .checked_add(offset)
7725 .ok_or(FormatError::InvalidArchive(
7726 "FileEntry frame range overflow",
7727 ))?;
7728 let frame = shard
7729 .frames
7730 .iter()
7731 .find(|entry| entry.frame_index == frame_index)
7732 .ok_or(FormatError::InvalidArchive(
7733 "FileEntry references missing FrameEntry",
7734 ))?;
7735 frames.push(frame);
7736 }
7737 Ok(frames)
7738}
7739
7740fn metadata_limits(crypto_header: &CryptoHeaderFixed) -> MetadataLimits {
7741 MetadataLimits {
7742 block_size: crypto_header.block_size,
7743 max_path_length: crypto_header.max_path_length,
7744 max_payload_data_shards: crypto_header.fec_data_shards,
7745 max_payload_parity_shards: crypto_header.fec_parity_shards,
7746 max_index_data_shards: crypto_header.index_fec_data_shards,
7747 max_index_parity_shards: crypto_header.index_fec_parity_shards,
7748 max_index_root_data_shards: crypto_header.index_root_fec_data_shards,
7749 max_index_root_parity_shards: crypto_header.index_root_fec_parity_shards,
7750 ..MetadataLimits::default()
7751 }
7752}
7753
7754fn verify_dense_keys<T>(
7755 entries: &BTreeMap<u64, T>,
7756 expected_count: u64,
7757 structure: &'static str,
7758) -> Result<(), FormatError> {
7759 if entries.len() as u64 != expected_count {
7760 return Err(FormatError::InvalidArchive(
7761 "decoded table count does not match IndexRoot",
7762 ));
7763 }
7764 for expected in 0..expected_count {
7765 if !entries.contains_key(&expected) {
7766 return Err(FormatError::InvalidMetadata {
7767 structure,
7768 reason: "global index coverage has a gap",
7769 });
7770 }
7771 }
7772 Ok(())
7773}
7774
7775fn validate_envelope_frame_coverage(
7776 frames: &BTreeMap<u64, FrameEntry>,
7777 envelopes: &BTreeMap<u64, EnvelopeEntry>,
7778) -> Result<(), FormatError> {
7779 let mut accounted_frames = BTreeSet::new();
7780 for envelope in envelopes.values() {
7781 let first = envelope.first_frame_index;
7782 let end =
7783 first
7784 .checked_add(envelope.frame_count as u64)
7785 .ok_or(FormatError::InvalidArchive(
7786 "EnvelopeEntry frame range overflow",
7787 ))?;
7788 let mut ranges = Vec::with_capacity(envelope.frame_count as usize);
7789 for frame_index in first..end {
7790 let frame = frames.get(&frame_index).ok_or(FormatError::InvalidArchive(
7791 "EnvelopeEntry references missing FrameEntry",
7792 ))?;
7793 if frame.envelope_index != envelope.envelope_index {
7794 return Err(FormatError::InvalidArchive(
7795 "FrameEntry envelope_index does not match containing EnvelopeEntry",
7796 ));
7797 }
7798 if !accounted_frames.insert(frame_index) {
7799 return Err(FormatError::InvalidArchive(
7800 "FrameEntry is covered by multiple EnvelopeEntries",
7801 ));
7802 }
7803 let start = frame.offset_in_envelope as usize;
7804 let end = checked_add(start, frame.compressed_size as usize, "FrameEntry")?;
7805 if end > envelope.plaintext_size as usize {
7806 return Err(FormatError::InvalidArchive(
7807 "FrameEntry exceeds EnvelopeEntry plaintext_size",
7808 ));
7809 }
7810 ranges.push((start, end));
7811 }
7812 validate_exact_coverage_ranges(
7813 &mut ranges,
7814 envelope.plaintext_size as usize,
7815 "EnvelopeEntry frame coverage has a gap or overlap",
7816 )?;
7817 }
7818
7819 for frame_index in frames.keys() {
7820 if !accounted_frames.contains(frame_index) {
7821 return Err(FormatError::InvalidArchive(
7822 "FrameEntry is not covered by any EnvelopeEntry",
7823 ));
7824 }
7825 }
7826 Ok(())
7827}
7828
7829fn validate_global_file_table_order(shards: &[IndexShard]) -> Result<(), FormatError> {
7830 let mut previous = None::<([u8; 8], Vec<u8>, u64)>;
7831 for shard in shards {
7832 for (idx, file) in shard.files.iter().enumerate() {
7833 let path = shard
7834 .file_path(idx)
7835 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?
7836 .to_vec();
7837 let start = shard
7838 .tar_member_group_start(idx)
7839 .ok_or(FormatError::InvalidArchive(
7840 "FileEntry tar member start is missing",
7841 ))?;
7842 let key = (file.path_hash, path, start);
7843 validate_global_file_table_key_step(previous.as_ref(), &key)?;
7844 previous = Some(key);
7845 }
7846 }
7847 Ok(())
7848}
7849
7850fn validate_global_file_table_key_step(
7851 previous: Option<&([u8; 8], Vec<u8>, u64)>,
7852 current: &([u8; 8], Vec<u8>, u64),
7853) -> Result<(), FormatError> {
7854 if let Some(previous) = previous {
7855 if previous >= current {
7856 return Err(FormatError::InvalidArchive(
7857 "global FileEntry rows are not sorted and unique",
7858 ));
7859 }
7860 }
7861 Ok(())
7862}
7863
7864fn validate_file_extent_coverage_ranges(
7865 extents: &[(u64, u64)],
7866 tar_len: u64,
7867) -> Result<(), FormatError> {
7868 let mut ranges = Vec::with_capacity(extents.len());
7869 for (start, len) in extents {
7870 let end = checked_u64_add(*start, *len, "FileEntry")?;
7871 if end > tar_len {
7872 return Err(FormatError::InvalidArchive(
7873 "FileEntry extent exceeds IndexRoot tar_total_size",
7874 ));
7875 }
7876 ranges.push((*start, end));
7877 }
7878 validate_exact_coverage_ranges_u64(
7879 &mut ranges,
7880 tar_len,
7881 "FileEntry extents do not cover tar stream exactly",
7882 )
7883}
7884
7885fn add_expected_directory_hint_rows(
7886 map: &mut DirectoryHintMap,
7887 shard_row_index: u32,
7888 path: &[u8],
7889 kind: TarEntryKind,
7890) {
7891 map.entry(Vec::new()).or_default().insert(shard_row_index);
7892 for (idx, byte) in path.iter().enumerate() {
7893 if *byte == b'/' {
7894 map.entry(path[..idx].to_vec())
7895 .or_default()
7896 .insert(shard_row_index);
7897 }
7898 }
7899 if kind == TarEntryKind::Directory {
7900 map.entry(path.to_vec())
7901 .or_default()
7902 .insert(shard_row_index);
7903 }
7904}
7905
7906fn validate_directory_hint_tables_against_expected(
7907 tables: &[DirectoryHintTable],
7908 expected: &DirectoryHintMap,
7909) -> Result<(), FormatError> {
7910 let mut actual = Vec::new();
7911 let mut previous_key: Option<([u8; 8], Vec<u8>)> = None;
7912
7913 for table in tables {
7914 for entry_index in 0..table.entries.len() {
7915 let path = table
7916 .entry_path(entry_index)
7917 .ok_or(FormatError::InvalidArchive(
7918 "DirectoryHintEntry path is missing",
7919 ))?;
7920 let key = (hash_prefix(path), path.to_vec());
7921 if let Some(previous) = &previous_key {
7922 if previous >= &key {
7923 return Err(FormatError::InvalidArchive(
7924 "DirectoryHintEntry rows are not globally sorted",
7925 ));
7926 }
7927 }
7928 previous_key = Some(key);
7929
7930 let rows =
7931 table
7932 .shard_rows_for_entry(entry_index)
7933 .ok_or(FormatError::InvalidArchive(
7934 "DirectoryHintEntry shard rows are missing",
7935 ))?;
7936 actual.push((path.to_vec(), rows.to_vec()));
7937 }
7938 }
7939
7940 if actual != sorted_directory_hint_rows(expected) {
7941 return Err(FormatError::InvalidArchive(
7942 "directory hint map does not match decoded files",
7943 ));
7944 }
7945 Ok(())
7946}
7947
7948fn sorted_directory_hint_rows(map: &DirectoryHintMap) -> Vec<(Vec<u8>, Vec<u32>)> {
7949 let mut rows = map
7950 .iter()
7951 .map(|(path, shard_rows)| {
7952 (
7953 path.clone(),
7954 shard_rows.iter().copied().collect::<Vec<u32>>(),
7955 )
7956 })
7957 .collect::<Vec<_>>();
7958 rows.sort_by(|(left_path, _), (right_path, _)| {
7959 hash_prefix(left_path)
7960 .cmp(&hash_prefix(right_path))
7961 .then_with(|| left_path.cmp(right_path))
7962 });
7963 rows
7964}
7965
7966fn validate_exact_coverage_ranges(
7967 ranges: &mut [(usize, usize)],
7968 expected_end: usize,
7969 reason: &'static str,
7970) -> Result<(), FormatError> {
7971 ranges.sort_unstable();
7972 let mut cursor = 0usize;
7973 for (start, end) in ranges.iter().copied() {
7974 if start != cursor || end < start {
7975 return Err(FormatError::InvalidArchive(reason));
7976 }
7977 cursor = end;
7978 }
7979 if cursor != expected_end {
7980 return Err(FormatError::InvalidArchive(reason));
7981 }
7982 Ok(())
7983}
7984
7985fn validate_exact_coverage_ranges_u64(
7986 ranges: &mut [(u64, u64)],
7987 expected_end: u64,
7988 reason: &'static str,
7989) -> Result<(), FormatError> {
7990 ranges.sort_unstable();
7991 let mut cursor = 0u64;
7992 for (start, end) in ranges.iter().copied() {
7993 if start != cursor || end < start {
7994 return Err(FormatError::InvalidArchive(reason));
7995 }
7996 cursor = end;
7997 }
7998 if cursor != expected_end {
7999 return Err(FormatError::InvalidArchive(reason));
8000 }
8001 Ok(())
8002}
8003
8004fn object_block_range(
8005 first_block_index: u64,
8006 data_block_count: u32,
8007 parity_block_count: u32,
8008 structure: &'static str,
8009) -> Result<(u64, u64), FormatError> {
8010 let total = data_block_count as u64 + parity_block_count as u64;
8011 if total == 0 {
8012 return Err(FormatError::InvalidArchive(structure));
8013 }
8014 let end = checked_u64_add(first_block_index, total, structure)?;
8015 Ok((first_block_index, end))
8016}
8017
8018fn validate_non_overlapping_object_ranges(ranges: &mut [(u64, u64)]) -> Result<(), FormatError> {
8019 ranges.sort_unstable();
8020 for pair in ranges.windows(2) {
8021 if pair[0].1 > pair[1].0 {
8022 return Err(FormatError::InvalidArchive(
8023 "encrypted object block ranges overlap",
8024 ));
8025 }
8026 }
8027 Ok(())
8028}
8029
8030pub(crate) fn observed_archive_size(
8031 sizes: impl IntoIterator<Item = u64>,
8032) -> Result<u64, FormatError> {
8033 sizes.into_iter().try_fold(0u64, |sum, size| {
8034 sum.checked_add(size).ok_or(FormatError::InvalidArchive(
8035 "observed archive size overflow",
8036 ))
8037 })
8038}
8039
8040pub(crate) fn total_extraction_size_cap(
8041 options: ReaderOptions,
8042 observed_archive_bytes: u64,
8043) -> u64 {
8044 options
8045 .max_total_extraction_size
8046 .min(observed_archive_bytes.saturating_mul(10))
8047}
8048
8049fn utf8_path(bytes: &[u8]) -> Result<String, FormatError> {
8050 std::str::from_utf8(bytes)
8051 .map(|path| path.to_owned())
8052 .map_err(|_| FormatError::UnsafeArchivePath)
8053}
8054
8055fn manifest_footer_global_pre_hmac_bytes(manifest_footer: &ManifestFooter) -> [u8; 104] {
8056 let mut bytes = [0u8; 104];
8057 bytes.copy_from_slice(&manifest_footer.to_bytes()[..104]);
8058 bytes[36..40].fill(0);
8059 bytes
8060}
8061
8062fn sha256_bytes(bytes: &[u8]) -> [u8; 32] {
8063 let digest = Sha256::digest(bytes);
8064 let mut out = [0u8; 32];
8065 out.copy_from_slice(&digest);
8066 out
8067}
8068
8069fn slice<'b>(
8070 bytes: &'b [u8],
8071 offset: usize,
8072 len: usize,
8073 structure: &'static str,
8074) -> Result<&'b [u8], FormatError> {
8075 let end = checked_add(offset, len, structure)?;
8076 bytes.get(offset..end).ok_or(FormatError::InvalidLength {
8077 structure,
8078 expected: end,
8079 actual: bytes.len(),
8080 })
8081}
8082
8083fn read_at_vec(
8084 reader: &dyn ArchiveReadAt,
8085 offset: u64,
8086 len: usize,
8087 structure: &'static str,
8088) -> Result<Vec<u8>, FormatError> {
8089 let expected_end = offset
8090 .checked_add(len as u64)
8091 .ok_or(FormatError::InvalidArchive("archive read range overflow"))?;
8092 let observed_len = reader.len()?;
8093 if expected_end > observed_len {
8094 return Err(FormatError::InvalidLength {
8095 structure,
8096 expected: to_usize(expected_end, structure)?,
8097 actual: to_usize(observed_len, structure)?,
8098 });
8099 }
8100 let mut out = vec![0u8; len];
8101 reader.read_exact_at(offset, &mut out)?;
8102 Ok(out)
8103}
8104
8105fn parallel_map_ref<T, U, F>(items: &[T], jobs: usize, f: F) -> Result<Vec<U>, FormatError>
8106where
8107 T: Sync,
8108 U: Send,
8109 F: Fn(&T) -> Result<U, FormatError> + Sync,
8110{
8111 if jobs <= 1 || items.len() <= 1 {
8112 return items.iter().map(f).collect();
8113 }
8114 let worker_count = jobs.min(items.len());
8115 let chunk_size = items.len().div_ceil(worker_count);
8116 let mut out = Vec::with_capacity(items.len());
8117 thread::scope(|scope| {
8118 let handles = items
8119 .chunks(chunk_size)
8120 .map(|chunk| scope.spawn(|| chunk.iter().map(&f).collect::<Result<Vec<_>, _>>()))
8121 .collect::<Vec<_>>();
8122 for handle in handles {
8123 let mut chunk = handle
8124 .join()
8125 .map_err(|_| FormatError::InvalidArchive("reader worker panicked"))??;
8126 out.append(&mut chunk);
8127 }
8128 Ok(out)
8129 })
8130}
8131
8132fn checked_add(lhs: usize, rhs: usize, structure: &'static str) -> Result<usize, FormatError> {
8133 lhs.checked_add(rhs)
8134 .ok_or(FormatError::InvalidArchive(structure))
8135}
8136
8137fn checked_u64_add(lhs: u64, rhs: u64, structure: &'static str) -> Result<u64, FormatError> {
8138 lhs.checked_add(rhs)
8139 .ok_or(FormatError::InvalidArchive(structure))
8140}
8141
8142fn to_usize(value: u64, structure: &'static str) -> Result<usize, FormatError> {
8143 usize::try_from(value).map_err(|_| FormatError::InvalidArchive(structure))
8144}
8145
8146#[cfg(test)]
8147mod tests {
8148 use std::fs;
8149
8150 use super::*;
8151 use crate::compression::compress_zstd_frame;
8152 use crate::crypto::{compute_hmac, encrypt_padded_aead_object};
8153 use crate::fec::encode_parity_gf16;
8154 use crate::format::{
8155 AeadAlgo, CompressionAlgo, FecAlgo, KdfAlgo, CRYPTO_HEADER_FIXED_LEN, FORMAT_VERSION,
8156 VOLUME_FORMAT_REV,
8157 };
8158 use crate::metadata::{
8159 DirectoryHintEntry, DirectoryHintTableHeader, IndexRootHeader, IndexShardHeader,
8160 ENVELOPE_ENTRY_LEN, FILE_ENTRY_LEN, FRAME_ENTRY_LEN, INDEX_SHARD_HEADER_LEN,
8161 };
8162 use crate::non_seekable_reader::{
8163 extract_non_seekable_stream_to_dir, list_non_seekable_stream, verify_non_seekable_stream,
8164 verify_non_seekable_stream_with_bootstrap_sidecar, verify_non_seekable_stream_with_options,
8165 NonSeekableReaderOptions, SequentialRootAuthStatus,
8166 };
8167 use crate::writer::{
8168 write_archive, write_archive_with_dictionary, write_archive_with_kdf,
8169 write_archive_with_root_auth, RegularFile, RootAuthSigningRequest, RootAuthWriterConfig,
8170 WriterOptions,
8171 };
8172
8173 fn master_key() -> MasterKey {
8174 MasterKey::from_raw_key(&[0x42; 32]).unwrap()
8175 }
8176
8177 #[test]
8178 fn reader_defaults_use_available_parallelism_jobs() {
8179 let options = ReaderOptions::default();
8180
8181 assert_eq!(options.jobs, default_jobs());
8182 assert!(options.jobs >= 1);
8183 }
8184
8185 #[test]
8186 fn reader_options_reject_zero_jobs() {
8187 let err = OpenedArchive::open_with_options(
8188 &[],
8189 &master_key(),
8190 ReaderOptions {
8191 jobs: 0,
8192 ..ReaderOptions::default()
8193 },
8194 )
8195 .unwrap_err();
8196
8197 assert_eq!(
8198 err,
8199 FormatError::ReaderUnsupported("jobs must be at least 1")
8200 );
8201 }
8202
8203 const TEST_ROOT_AUTH_ID: u16 = 0xe001;
8204 const TEST_ROOT_AUTH_VALUE_LEN: u32 = 32;
8205
8206 fn test_root_auth_config() -> RootAuthWriterConfig<'static> {
8207 RootAuthWriterConfig {
8208 authenticator_id: TEST_ROOT_AUTH_ID,
8209 signer_identity_type: 0,
8210 signer_identity: &[],
8211 authenticator_value_length: TEST_ROOT_AUTH_VALUE_LEN,
8212 }
8213 }
8214
8215 fn test_root_auth_value(request: &RootAuthSigningRequest) -> Vec<u8> {
8216 request.archive_root.to_vec()
8217 }
8218
8219 fn test_root_auth_verifies(footer: &RootAuthFooterV1, archive_root: &[u8; 32]) -> bool {
8220 footer.authenticator_id == TEST_ROOT_AUTH_ID
8221 && footer.signer_identity_type == 0
8222 && footer.signer_identity_bytes.is_empty()
8223 && footer.authenticator_value.as_slice() == archive_root
8224 }
8225
8226 fn dictionary() -> &'static [u8] {
8227 b"dir/dict.txt common words common words common words dictionary payload"
8228 }
8229
8230 #[derive(Clone)]
8231 struct CountingReadAt {
8232 bytes: std::sync::Arc<Vec<u8>>,
8233 reads: std::sync::Arc<std::sync::Mutex<Vec<(u64, u64)>>>,
8234 denied_ranges: std::sync::Arc<Vec<(u64, u64)>>,
8235 }
8236
8237 impl CountingReadAt {
8238 fn new(bytes: Vec<u8>, denied_ranges: Vec<(u64, u64)>) -> Self {
8239 Self {
8240 bytes: std::sync::Arc::new(bytes),
8241 reads: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
8242 denied_ranges: std::sync::Arc::new(denied_ranges),
8243 }
8244 }
8245
8246 fn reads(&self) -> Vec<(u64, u64)> {
8247 self.reads.lock().unwrap().clone()
8248 }
8249 }
8250
8251 impl ArchiveReadAt for CountingReadAt {
8252 fn len(&self) -> Result<u64, FormatError> {
8253 u64::try_from(self.bytes.as_ref().len())
8254 .map_err(|_| FormatError::InvalidArchive("archive length overflow"))
8255 }
8256
8257 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
8258 let end = checked_u64_add(offset, buf.len() as u64, "archive read range overflow")?;
8259 self.reads.lock().unwrap().push((offset, end));
8260 if self
8261 .denied_ranges
8262 .iter()
8263 .any(|(start, limit)| ranges_overlap(offset, end, *start, *limit))
8264 {
8265 return Err(FormatError::InvalidArchive("denied test read"));
8266 }
8267 let start = to_usize(offset, "archive")?;
8268 let end_usize = checked_add(start, buf.len(), "archive")?;
8269 let source = self
8270 .bytes
8271 .get(start..end_usize)
8272 .ok_or(FormatError::InvalidLength {
8273 structure: "archive",
8274 expected: end_usize,
8275 actual: self.bytes.as_ref().len(),
8276 })?;
8277 buf.copy_from_slice(source);
8278 Ok(())
8279 }
8280 }
8281
8282 fn ranges_overlap(left_start: u64, left_end: u64, right_start: u64, right_end: u64) -> bool {
8283 left_start < right_end && right_start < left_end
8284 }
8285
8286 fn single_stream_options() -> WriterOptions {
8287 WriterOptions {
8288 stripe_width: 1,
8289 volume_loss_tolerance: 0,
8290 ..WriterOptions::default()
8291 }
8292 }
8293
8294 struct ChunkedReader {
8295 bytes: Vec<u8>,
8296 cursor: usize,
8297 max_chunk: usize,
8298 }
8299
8300 impl ChunkedReader {
8301 fn new(bytes: Vec<u8>, max_chunk: usize) -> Self {
8302 Self {
8303 bytes,
8304 cursor: 0,
8305 max_chunk,
8306 }
8307 }
8308 }
8309
8310 impl std::io::Read for ChunkedReader {
8311 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
8312 if self.cursor >= self.bytes.len() {
8313 return Ok(0);
8314 }
8315 let available = self.bytes.len() - self.cursor;
8316 let len = available.min(buf.len()).min(self.max_chunk);
8317 buf[..len].copy_from_slice(&self.bytes[self.cursor..self.cursor + len]);
8318 self.cursor += len;
8319 Ok(len)
8320 }
8321 }
8322
8323 #[test]
8324 fn global_file_table_key_step_rejects_distinct_path_regression() {
8325 let previous = ([1u8; 8], b"b.txt".to_vec(), 0);
8326 let current = ([1u8; 8], b"a.txt".to_vec(), 0);
8327
8328 assert_eq!(
8329 validate_global_file_table_key_step(Some(&previous), ¤t).unwrap_err(),
8330 FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
8331 );
8332 }
8333
8334 #[test]
8335 fn global_file_table_key_step_rejects_duplicate_full_key() {
8336 let previous = ([1u8; 8], b"a.txt".to_vec(), 7);
8337 let current = ([1u8; 8], b"a.txt".to_vec(), 7);
8338
8339 assert_eq!(
8340 validate_global_file_table_key_step(Some(&previous), ¤t).unwrap_err(),
8341 FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
8342 );
8343 }
8344
8345 fn small_block_recovery_options() -> WriterOptions {
8346 WriterOptions {
8347 block_size: 4096,
8348 chunk_size: 32 * 1024,
8349 envelope_target_size: 32 * 1024,
8350 stripe_width: 1,
8351 volume_loss_tolerance: 0,
8352 bit_rot_buffer_pct: 1,
8353 fec_data_shards: 16,
8354 fec_parity_shards: 1,
8355 index_fec_data_shards: 4,
8356 index_fec_parity_shards: 1,
8357 index_root_fec_data_shards: 16,
8358 index_root_fec_parity_shards: 1,
8359 ..WriterOptions::default()
8360 }
8361 }
8362
8363 fn parity_rich_recovery_options() -> WriterOptions {
8364 WriterOptions {
8365 block_size: 4096,
8366 chunk_size: 32 * 1024,
8367 envelope_target_size: 32 * 1024,
8368 stripe_width: 1,
8369 volume_loss_tolerance: 0,
8370 bit_rot_buffer_pct: 40,
8371 fec_data_shards: 16,
8372 fec_parity_shards: 16,
8373 index_fec_data_shards: 4,
8374 index_fec_parity_shards: 4,
8375 index_root_fec_data_shards: 16,
8376 index_root_fec_parity_shards: 16,
8377 ..WriterOptions::default()
8378 }
8379 }
8380
8381 fn pseudo_random_bytes(len: usize) -> Vec<u8> {
8382 let mut state = 0x1234_5678u32;
8383 (0..len)
8384 .map(|_| {
8385 state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
8386 (state >> 24) as u8
8387 })
8388 .collect()
8389 }
8390
8391 #[test]
8392 fn opens_lists_verifies_and_extracts_one_file_archive() {
8393 let archive = write_archive(
8394 &[RegularFile::new("dir/hello.txt", b"hello m7")],
8395 &master_key(),
8396 single_stream_options(),
8397 )
8398 .unwrap();
8399 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8400
8401 assert_eq!(
8402 opened.list_files().unwrap(),
8403 vec![ArchiveEntry {
8404 path: "dir/hello.txt".to_string(),
8405 file_data_size: 8,
8406 kind: TarEntryKind::Regular,
8407 mode: 0o644,
8408 mtime: 0,
8409 diagnostics: Vec::new(),
8410 }]
8411 );
8412 opened.verify().unwrap();
8413 assert_eq!(
8414 opened.extract_file("dir/hello.txt").unwrap(),
8415 Some(b"hello m7".to_vec())
8416 );
8417 assert_eq!(opened.extract_file("missing.txt").unwrap(), None);
8418 }
8419
8420 #[test]
8421 fn root_auth_archive_round_trips_and_verifies_with_callback() {
8422 let archive = write_archive_with_root_auth(
8423 &[RegularFile::new("signed.txt", b"root-auth payload")],
8424 &master_key(),
8425 single_stream_options(),
8426 RootAuthWriterConfig {
8427 authenticator_id: 0x7777,
8428 signer_identity_type: 1,
8429 signer_identity: b"test signer",
8430 authenticator_value_length: 32,
8431 },
8432 |request| Ok(request.archive_root.to_vec()),
8433 )
8434 .unwrap();
8435
8436 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8437 opened.verify().unwrap();
8438 let verified = opened
8439 .verify_root_auth_with(|footer, archive_root| {
8440 Ok(footer.authenticator_value == archive_root.as_slice())
8441 })
8442 .unwrap();
8443
8444 assert_eq!(verified.authenticator_id, 0x7777);
8445 assert_eq!(verified.signer_identity_type, 1);
8446 assert_eq!(verified.signer_identity_bytes, b"test signer");
8447 assert_eq!(
8448 verified.archive_root,
8449 opened.root_auth_footer.as_ref().unwrap().archive_root
8450 );
8451 assert_eq!(
8452 verified.diagnostics,
8453 vec![
8454 RootAuthDiagnostic::RootAuthContentVerified,
8455 RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
8456 RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
8457 RootAuthDiagnostic::RecoveryMarginUnchecked,
8458 ]
8459 );
8460 }
8461
8462 #[test]
8463 fn root_auth_verification_requires_authenticator_success() {
8464 let archive = write_archive_with_root_auth(
8465 &[RegularFile::new("signed.txt", b"root-auth payload")],
8466 &master_key(),
8467 single_stream_options(),
8468 RootAuthWriterConfig {
8469 authenticator_id: 9,
8470 signer_identity_type: 1,
8471 signer_identity: b"test signer",
8472 authenticator_value_length: 32,
8473 },
8474 |request| Ok(request.archive_root.to_vec()),
8475 )
8476 .unwrap();
8477 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8478
8479 assert_eq!(
8480 opened.verify_root_auth_with(|_, _| Ok(false)).unwrap_err(),
8481 FormatError::InvalidArchive("root-auth authenticator verification failed")
8482 );
8483 }
8484
8485 #[test]
8486 fn public_no_key_verifies_encrypted_data_block_commitment_with_callback() {
8487 let archive = write_archive_with_root_auth(
8488 &[RegularFile::new("public.txt", b"public commitment")],
8489 &master_key(),
8490 single_stream_options(),
8491 RootAuthWriterConfig {
8492 authenticator_id: 0x2222,
8493 signer_identity_type: 1,
8494 signer_identity: b"public verifier",
8495 authenticator_value_length: 32,
8496 },
8497 |request| Ok(request.archive_root.to_vec()),
8498 )
8499 .unwrap();
8500
8501 let verified = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
8502 Ok(footer.authenticator_value == archive_root.as_slice())
8503 })
8504 .unwrap();
8505
8506 assert_eq!(verified.authenticator_id, 0x2222);
8507 assert_eq!(verified.signer_identity_bytes, b"public verifier");
8508 assert!(verified.total_data_block_count > 0);
8509 }
8510
8511 #[test]
8512 fn public_no_key_ignores_untrusted_manifest_and_trailer_block_count_fields() {
8513 let archive = write_archive_with_root_auth(
8514 &[RegularFile::new(
8515 "public-fields.txt",
8516 b"public source authority",
8517 )],
8518 &master_key(),
8519 single_stream_options(),
8520 RootAuthWriterConfig {
8521 authenticator_id: 0x2222,
8522 signer_identity_type: 1,
8523 signer_identity: b"public verifier",
8524 authenticator_value_length: 32,
8525 },
8526 |request| Ok(request.archive_root.to_vec()),
8527 )
8528 .unwrap();
8529 let mut bytes = archive.bytes.clone();
8530
8531 rewrite_public_cmra_image(&mut bytes, |image| {
8532 let manifest_region = image
8533 .regions
8534 .iter_mut()
8535 .find(|region| region.region_type == 3)
8536 .unwrap();
8537 manifest_region.bytes[44..48].copy_from_slice(&99u32.to_le_bytes());
8538
8539 let trailer_region = image
8540 .regions
8541 .iter_mut()
8542 .find(|region| region.region_type == 5)
8543 .unwrap();
8544 let mut trailer = VolumeTrailer::parse(&trailer_region.bytes).unwrap();
8545 trailer.block_count += 7;
8546 trailer_region.bytes = trailer.to_bytes().to_vec();
8547 });
8548
8549 public_no_key_verify_archive_with(&bytes, |footer, archive_root| {
8550 Ok(footer.authenticator_value == archive_root.as_slice())
8551 })
8552 .unwrap();
8553 }
8554
8555 #[test]
8556 fn public_no_key_compares_only_public_crypto_profile_across_volumes() {
8557 let archive = write_archive_with_root_auth(
8558 &[RegularFile::new(
8559 "public-crypto.txt",
8560 b"cross-volume public profile",
8561 )],
8562 &master_key(),
8563 WriterOptions {
8564 stripe_width: 2,
8565 volume_loss_tolerance: 0,
8566 ..WriterOptions::default()
8567 },
8568 RootAuthWriterConfig {
8569 authenticator_id: 0x3333,
8570 signer_identity_type: 1,
8571 signer_identity: b"public verifier",
8572 authenticator_value_length: 32,
8573 },
8574 |request| Ok(request.archive_root.to_vec()),
8575 )
8576 .unwrap();
8577 let mut volumes = archive.volumes.clone();
8578 let volume_header = VolumeHeader::parse(&volumes[1][..VOLUME_HEADER_LEN]).unwrap();
8579 let crypto_offset = volume_header.crypto_header_offset as usize;
8580 let expected_volume_size = 123_456_789u64;
8581 volumes[1][crypto_offset + 52..crypto_offset + 60]
8582 .copy_from_slice(&expected_volume_size.to_le_bytes());
8583 rewrite_public_cmra_image(&mut volumes[1], |image| {
8584 let crypto_region = image
8585 .regions
8586 .iter_mut()
8587 .find(|region| region.region_type == 2)
8588 .unwrap();
8589 crypto_region.bytes[52..60].copy_from_slice(&expected_volume_size.to_le_bytes());
8590 });
8591
8592 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
8593 public_no_key_verify_volumes_with(&volume_refs, |footer, archive_root| {
8594 Ok(footer.authenticator_value == archive_root.as_slice())
8595 })
8596 .unwrap();
8597 }
8598
8599 #[test]
8600 fn locator_based_cmra_recovery_treats_header_damage_as_recoverable() {
8601 let archive = write_archive_with_root_auth(
8602 &[RegularFile::new("cmra-header.txt", b"header fallback")],
8603 &master_key(),
8604 single_stream_options(),
8605 RootAuthWriterConfig {
8606 authenticator_id: 0x4444,
8607 signer_identity_type: 1,
8608 signer_identity: b"public verifier",
8609 authenticator_value_length: 32,
8610 },
8611 |request| Ok(request.archive_root.to_vec()),
8612 )
8613 .unwrap();
8614 let final_locator = final_recovery_locator(&archive.bytes);
8615
8616 let mut bad_crc = archive.bytes.clone();
8617 let crc_offset =
8618 final_locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN - 1;
8619 bad_crc[crc_offset] ^= 0x55;
8620 public_no_key_verify_archive_with(&bad_crc, |footer, archive_root| {
8621 Ok(footer.authenticator_value == archive_root.as_slice())
8622 })
8623 .unwrap();
8624
8625 let mut bad_magic = archive.bytes.clone();
8626 bad_magic[final_locator.cmra_offset as usize] ^= 0x55;
8627 public_no_key_verify_archive_with(&bad_magic, |footer, archive_root| {
8628 Ok(footer.authenticator_value == archive_root.as_slice())
8629 })
8630 .unwrap();
8631
8632 let mut bad_hint = archive.bytes.clone();
8633 bad_hint[crc_offset] ^= 0xAA;
8634 for offset in [
8635 bad_hint.len() - LOCATOR_PAIR_LEN,
8636 bad_hint.len() - CRITICAL_RECOVERY_LOCATOR_LEN,
8637 ] {
8638 let mut locator = CriticalRecoveryLocator::parse(
8639 &bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN],
8640 )
8641 .unwrap();
8642 locator.volume_index_hint += 1;
8643 bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN]
8644 .copy_from_slice(&locator.to_bytes());
8645 }
8646 assert_eq!(
8647 public_no_key_verify_archive_with(&bad_hint, |_, _| Ok(true)).unwrap_err(),
8648 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
8649 );
8650 }
8651
8652 #[test]
8653 fn recovers_physical_volume_header_magic_from_cmra_authority() {
8654 let payload = b"front header authority".to_vec();
8655 let archive = write_archive(
8656 &[RegularFile::new("volume-header.txt", &payload)],
8657 &master_key(),
8658 small_block_recovery_options(),
8659 )
8660 .unwrap();
8661
8662 let mut corrupted = archive.bytes;
8663 corrupted[0] ^= 0x55;
8664
8665 let opened = open_archive(&corrupted, &master_key()).unwrap();
8666 assert_eq!(
8667 opened.extract_file("volume-header.txt").unwrap(),
8668 Some(payload)
8669 );
8670 opened.verify().unwrap();
8671 }
8672
8673 #[test]
8674 fn recovers_crc_valid_physical_volume_index_from_cmra_authority() {
8675 let payload = b"crc-valid wrong volume index".to_vec();
8676 let mut options = small_block_recovery_options();
8677 options.stripe_width = 2;
8678 options.volume_loss_tolerance = 0;
8679 let archive = write_archive(
8680 &[RegularFile::new("volume-index.txt", &payload)],
8681 &master_key(),
8682 options,
8683 )
8684 .unwrap();
8685
8686 let mut corrupted = archive.volumes[0].clone();
8687 let mut header = VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN]).unwrap();
8688 assert_eq!(header.volume_index, 0);
8689 header.volume_index = 1;
8690 corrupted[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
8691 assert_eq!(
8692 VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN])
8693 .unwrap()
8694 .volume_index,
8695 1
8696 );
8697
8698 let opened = open_archive_volumes(
8699 &[corrupted.as_slice(), archive.volumes[1].as_slice()],
8700 &master_key(),
8701 )
8702 .unwrap();
8703 assert_eq!(opened.volume_header.volume_index, 0);
8704 assert_eq!(
8705 opened.extract_file("volume-index.txt").unwrap(),
8706 Some(payload)
8707 );
8708 opened.verify().unwrap();
8709 }
8710
8711 #[test]
8712 fn recovers_physical_crypto_header_magic_from_cmra_authority() {
8713 let payload = b"crypto header authority".to_vec();
8714 let archive = write_archive(
8715 &[RegularFile::new("crypto-header.txt", &payload)],
8716 &master_key(),
8717 small_block_recovery_options(),
8718 )
8719 .unwrap();
8720 let crypto_offset = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN])
8721 .unwrap()
8722 .crypto_header_offset;
8723
8724 let mut corrupted = archive.bytes;
8725 corrupted[crypto_offset as usize] ^= 0x55;
8726
8727 let opened = open_archive(&corrupted, &master_key()).unwrap();
8728 assert_eq!(
8729 opened.extract_file("crypto-header.txt").unwrap(),
8730 Some(payload)
8731 );
8732 opened.verify().unwrap();
8733 }
8734
8735 #[test]
8736 fn read_at_api_recovers_physical_header_magic_from_cmra_authority() {
8737 let payload = b"read-at header authority".to_vec();
8738 let archive = write_archive(
8739 &[RegularFile::new("read-at-header.txt", &payload)],
8740 &master_key(),
8741 small_block_recovery_options(),
8742 )
8743 .unwrap();
8744
8745 let mut corrupted = archive.bytes;
8746 corrupted[0] ^= 0x55;
8747
8748 let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
8749 assert_eq!(
8750 opened.extract_file("read-at-header.txt").unwrap(),
8751 Some(payload)
8752 );
8753 opened.verify().unwrap();
8754 }
8755
8756 #[test]
8757 fn read_at_api_recovers_crc_valid_physical_volume_index_from_cmra_authority() {
8758 let payload = b"read-at crc-valid wrong volume index".to_vec();
8759 let mut options = small_block_recovery_options();
8760 options.stripe_width = 2;
8761 options.volume_loss_tolerance = 0;
8762 let archive = write_archive(
8763 &[RegularFile::new("read-at-volume-index.txt", &payload)],
8764 &master_key(),
8765 options,
8766 )
8767 .unwrap();
8768
8769 let mut corrupted = archive.volumes[0].clone();
8770 let mut header = VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN]).unwrap();
8771 assert_eq!(header.volume_index, 0);
8772 header.volume_index = 1;
8773 corrupted[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
8774
8775 let opened = open_seekable_archive_volumes(
8776 vec![corrupted, archive.volumes[1].clone()],
8777 &master_key(),
8778 )
8779 .unwrap();
8780 assert_eq!(opened.volume_header.volume_index, 0);
8781 assert_eq!(
8782 opened.extract_file("read-at-volume-index.txt").unwrap(),
8783 Some(payload)
8784 );
8785 opened.verify().unwrap();
8786 }
8787
8788 #[test]
8789 fn recovers_cmra_header_magic_from_locator_tuple() {
8790 let payload = b"cmra header authority".to_vec();
8791 let archive = write_archive(
8792 &[RegularFile::new("cmra-header-magic.txt", &payload)],
8793 &master_key(),
8794 small_block_recovery_options(),
8795 )
8796 .unwrap();
8797 let locator = final_recovery_locator(&archive.bytes);
8798
8799 let mut corrupted = archive.bytes;
8800 corrupted[locator.cmra_offset as usize] ^= 0x55;
8801
8802 let opened = open_archive(&corrupted, &master_key()).unwrap();
8803 assert_eq!(
8804 opened.extract_file("cmra-header-magic.txt").unwrap(),
8805 Some(payload)
8806 );
8807 opened.verify().unwrap();
8808 }
8809
8810 #[test]
8811 fn recovers_cmra_shard_magic_as_erasure() {
8812 let payload = b"cmra shard authority".to_vec();
8813 let archive = write_archive(
8814 &[RegularFile::new("cmra-shard-magic.txt", &payload)],
8815 &master_key(),
8816 small_block_recovery_options(),
8817 )
8818 .unwrap();
8819 let locator = final_recovery_locator(&archive.bytes);
8820 let first_shard_offset =
8821 locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
8822
8823 let mut corrupted = archive.bytes;
8824 corrupted[first_shard_offset] ^= 0x55;
8825
8826 let opened = open_archive(&corrupted, &master_key()).unwrap();
8827 assert_eq!(
8828 opened.extract_file("cmra-shard-magic.txt").unwrap(),
8829 Some(payload)
8830 );
8831 opened.verify().unwrap();
8832 }
8833
8834 #[test]
8835 fn key_holding_rejects_cmra_below_authenticated_parity_floor() {
8836 let archive = write_archive(
8837 &[RegularFile::new(
8838 "cmra-floor.txt",
8839 b"authenticated CMRA floor",
8840 )],
8841 &master_key(),
8842 single_stream_options(),
8843 )
8844 .unwrap();
8845 let malformed = rewrite_cmra_parity_count(&archive.bytes, 1);
8846 let final_offset = malformed.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
8847 let locator = CriticalRecoveryLocator::parse(
8848 &malformed[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
8849 )
8850 .unwrap();
8851 let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
8852 let crypto_start = volume_header.crypto_header_offset as usize;
8853 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
8854 let crypto_header = CryptoHeader::parse(
8855 &malformed[crypto_start..crypto_end],
8856 volume_header.crypto_header_length,
8857 )
8858 .unwrap();
8859 let subkeys = Subkeys::derive(
8860 &master_key(),
8861 &volume_header.archive_uuid,
8862 &volume_header.session_id,
8863 )
8864 .unwrap();
8865
8866 assert_eq!(
8867 parse_locator_cmra_candidate(
8868 &malformed,
8869 final_offset,
8870 locator,
8871 KeyHoldingTerminalContext {
8872 subkeys: &subkeys,
8873 volume_header: &volume_header,
8874 crypto_header: &crypto_header.fixed,
8875 crypto_header_bytes: &malformed[crypto_start..crypto_end],
8876 },
8877 )
8878 .unwrap_err(),
8879 FormatError::InvalidArchive(
8880 "CMRA parity shard count is below authenticated bit-rot lower bound"
8881 )
8882 );
8883 assert!(open_archive(&malformed, &master_key()).is_err());
8884 }
8885
8886 #[test]
8887 fn locator_tuple_bounds_are_checked_before_locator_position_fields() {
8888 let archive = write_archive(
8889 &[RegularFile::new(
8890 "locator-order.txt",
8891 b"locator tuple first",
8892 )],
8893 &master_key(),
8894 single_stream_options(),
8895 )
8896 .unwrap();
8897 let final_offset = archive.bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
8898 let mut locator = final_recovery_locator(&archive.bytes);
8899 locator.cmra_shard_size = 513;
8900 locator.body_bytes_before_cmra = locator.cmra_offset + 1;
8901 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
8902 let crypto_start = volume_header.crypto_header_offset as usize;
8903 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
8904 let crypto_header = CryptoHeader::parse(
8905 &archive.bytes[crypto_start..crypto_end],
8906 volume_header.crypto_header_length,
8907 )
8908 .unwrap();
8909 let subkeys = Subkeys::derive(
8910 &master_key(),
8911 &volume_header.archive_uuid,
8912 &volume_header.session_id,
8913 )
8914 .unwrap();
8915
8916 assert_eq!(
8917 parse_locator_cmra_candidate(
8918 &archive.bytes,
8919 final_offset,
8920 locator,
8921 KeyHoldingTerminalContext {
8922 subkeys: &subkeys,
8923 volume_header: &volume_header,
8924 crypto_header: &crypto_header.fixed,
8925 crypto_header_bytes: &archive.bytes[crypto_start..crypto_end],
8926 },
8927 )
8928 .unwrap_err(),
8929 FormatError::InvalidArchive("CMRA shard_size is invalid")
8930 );
8931 }
8932
8933 #[test]
8934 fn sequential_extract_rejects_bytes_after_terminal_locator() {
8935 let archive = write_archive(
8936 &[RegularFile::new("seq.txt", b"sequential EOF")],
8937 &master_key(),
8938 single_stream_options(),
8939 )
8940 .unwrap();
8941 let mut appended = archive.bytes.clone();
8942 appended.extend_from_slice(&[0xAA; 32]);
8943
8944 assert_eq!(
8945 sequential_extract_tar_stream(&appended, &master_key()).unwrap_err(),
8946 FormatError::InvalidArchive("sequential terminal does not end at EOF")
8947 );
8948 }
8949
8950 #[test]
8951 fn global_file_table_order_rejects_cross_shard_duplicate_reversal() {
8952 let first = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 2048);
8953 let second = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 1024);
8954
8955 assert_eq!(
8956 validate_global_file_table_key_step(Some(&first), &second).unwrap_err(),
8957 FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
8958 );
8959 }
8960
8961 #[test]
8962 fn root_auth_verifies_key_holding_and_public_no_key_modes() {
8963 let archive = write_archive_with_root_auth(
8964 &[RegularFile::new("signed.txt", b"ed25519 payload")],
8965 &master_key(),
8966 single_stream_options(),
8967 test_root_auth_config(),
8968 |request| Ok(test_root_auth_value(request)),
8969 )
8970 .unwrap();
8971
8972 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8973 let root_auth = opened
8974 .verify_root_auth_with(|footer, archive_root| {
8975 Ok(test_root_auth_verifies(footer, archive_root))
8976 })
8977 .unwrap();
8978 assert_eq!(
8979 root_auth.archive_root,
8980 opened.root_auth_footer.as_ref().unwrap().archive_root
8981 );
8982
8983 let public = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
8984 Ok(test_root_auth_verifies(footer, archive_root))
8985 })
8986 .unwrap();
8987 assert_eq!(public.archive_root, root_auth.archive_root);
8988 assert_eq!(
8989 public.diagnostics,
8990 vec![
8991 PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
8992 PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
8993 PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
8994 ]
8995 );
8996 }
8997
8998 #[test]
8999 fn root_auth_verifies_with_tolerated_missing_volume_after_fec_repair() {
9000 let options = WriterOptions {
9001 block_size: 4096,
9002 chunk_size: 16 * 1024,
9003 envelope_target_size: 16 * 1024,
9004 stripe_width: 2,
9005 volume_loss_tolerance: 1,
9006 bit_rot_buffer_pct: 0,
9007 fec_data_shards: 16,
9008 fec_parity_shards: 1,
9009 index_fec_data_shards: 4,
9010 index_fec_parity_shards: 1,
9011 index_root_fec_data_shards: 16,
9012 index_root_fec_parity_shards: 1,
9013 ..WriterOptions::default()
9014 };
9015 let archive = write_archive_with_root_auth(
9016 &[RegularFile::new("missing-volume.txt", b"recover me")],
9017 &master_key(),
9018 options,
9019 test_root_auth_config(),
9020 |request| Ok(test_root_auth_value(request)),
9021 )
9022 .unwrap();
9023
9024 let opened = open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap();
9025 let root_auth = opened
9026 .verify_root_auth_with(|footer, archive_root| {
9027 Ok(test_root_auth_verifies(footer, archive_root))
9028 })
9029 .unwrap();
9030 assert!(root_auth
9031 .diagnostics
9032 .contains(&RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss));
9033 }
9034
9035 #[test]
9036 fn public_no_key_rejects_unsigned_archives() {
9037 let archive = write_archive(
9038 &[RegularFile::new("plain.txt", b"unsigned")],
9039 &master_key(),
9040 single_stream_options(),
9041 )
9042 .unwrap();
9043
9044 assert_eq!(
9045 public_no_key_verify_archive_with(&archive.bytes, |_, _| Ok(true)).unwrap_err(),
9046 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
9047 );
9048 }
9049
9050 #[test]
9051 fn unsigned_archive_reports_root_auth_absent() {
9052 let archive = write_archive(
9053 &[RegularFile::new("plain.txt", b"unsigned")],
9054 &master_key(),
9055 single_stream_options(),
9056 )
9057 .unwrap();
9058 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9059
9060 assert_eq!(
9061 opened.verify_root_auth_with(|_, _| Ok(true)).unwrap_err(),
9062 FormatError::ReaderUnsupported("root-auth footer is absent")
9063 );
9064 }
9065
9066 #[test]
9067 fn safe_extract_writes_regular_file_under_root() {
9068 let archive = write_archive(
9069 &[RegularFile::new("dir/hello.txt", b"safe m8")],
9070 &master_key(),
9071 single_stream_options(),
9072 )
9073 .unwrap();
9074 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9075 let tmp = tempfile::tempdir().unwrap();
9076
9077 opened
9078 .extract_file_to(
9079 "dir/hello.txt",
9080 tmp.path(),
9081 SafeExtractionOptions::default(),
9082 )
9083 .unwrap()
9084 .unwrap();
9085
9086 assert_eq!(
9087 std::fs::read(tmp.path().join("dir").join("hello.txt")).unwrap(),
9088 b"safe m8"
9089 );
9090 }
9091
9092 #[test]
9093 fn seekable_extract_all_to_streams_unique_archive() {
9094 let archive = write_archive(
9095 &[
9096 RegularFile::new("alpha.txt", b"alpha"),
9097 RegularFile::new("dir/beta.txt", b"beta"),
9098 ],
9099 &master_key(),
9100 single_stream_options(),
9101 )
9102 .unwrap();
9103 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9104 let tmp = tempfile::tempdir().unwrap();
9105
9106 let diagnostics = opened
9107 .extract_all_to(tmp.path(), SafeExtractionOptions::default())
9108 .unwrap();
9109
9110 assert_eq!(diagnostics.len(), 2);
9111 assert_eq!(fs::read(tmp.path().join("alpha.txt")).unwrap(), b"alpha");
9112 assert_eq!(
9113 fs::read(tmp.path().join("dir").join("beta.txt")).unwrap(),
9114 b"beta"
9115 );
9116 }
9117
9118 #[test]
9119 fn seekable_extract_all_to_rejects_duplicate_paths_for_cli_fallback() {
9120 let archive = write_archive(
9121 &[
9122 RegularFile::new("same.txt", b"old"),
9123 RegularFile::new("same.txt", b"new"),
9124 ],
9125 &master_key(),
9126 single_stream_options(),
9127 )
9128 .unwrap();
9129 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9130 let tmp = tempfile::tempdir().unwrap();
9131
9132 assert_eq!(
9133 opened
9134 .extract_all_to(tmp.path(), SafeExtractionOptions::default())
9135 .unwrap_err(),
9136 FormatError::ReaderUnsupported("fast full extract requires unique archive paths")
9137 );
9138 }
9139
9140 #[test]
9141 fn seekable_extract_indexed_files_to_restores_final_duplicate_winner() {
9142 let archive = write_archive(
9143 &[
9144 RegularFile::new("same.txt", b"old"),
9145 RegularFile::new("same.txt", b"new"),
9146 ],
9147 &master_key(),
9148 single_stream_options(),
9149 )
9150 .unwrap();
9151 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9152 let tmp = tempfile::tempdir().unwrap();
9153
9154 let diagnostics = opened
9155 .extract_indexed_files_to(tmp.path(), SafeExtractionOptions::default(), 2)
9156 .unwrap();
9157
9158 assert_eq!(diagnostics.len(), 1);
9159 assert_eq!(diagnostics[0].0, "same.txt");
9160 assert_eq!(fs::read(tmp.path().join("same.txt")).unwrap(), b"new");
9161 }
9162
9163 #[test]
9164 fn safe_extract_rejects_overwriting_existing_file_by_default() {
9165 let archive = write_archive(
9166 &[RegularFile::new("hello.txt", b"new")],
9167 &master_key(),
9168 single_stream_options(),
9169 )
9170 .unwrap();
9171 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9172 let tmp = tempfile::tempdir().unwrap();
9173 std::fs::write(tmp.path().join("hello.txt"), b"old").unwrap();
9174
9175 assert_eq!(
9176 opened
9177 .extract_file_to("hello.txt", tmp.path(), SafeExtractionOptions::default())
9178 .unwrap_err(),
9179 FormatError::UnsafeOverwrite
9180 );
9181 assert_eq!(std::fs::read(tmp.path().join("hello.txt")).unwrap(), b"old");
9182 }
9183
9184 #[test]
9185 fn opens_and_verifies_empty_archive() {
9186 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9187 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9188
9189 assert!(opened.list_files().unwrap().is_empty());
9190 opened.verify().unwrap();
9191 }
9192
9193 #[test]
9194 fn default_reader_options_allow_v36_trailing_garbage_scan() {
9195 let archive = write_archive(
9196 &[RegularFile::new("garbage-tolerant.txt", b"still intact")],
9197 &master_key(),
9198 single_stream_options(),
9199 )
9200 .unwrap();
9201 let mut with_trailing_garbage = archive.bytes.clone();
9202 with_trailing_garbage.extend_from_slice(b"ignored trailing bytes");
9203
9204 let opened = open_archive(&with_trailing_garbage, &master_key()).unwrap();
9205 assert_eq!(
9206 opened.extract_file("garbage-tolerant.txt").unwrap(),
9207 Some(b"still intact".to_vec())
9208 );
9209 }
9210
9211 #[test]
9212 fn seekable_open_rejects_too_small_and_unavailable_header_crypto_bytes() {
9213 assert_eq!(
9214 open_archive(
9215 &[0u8; VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1],
9216 &master_key()
9217 )
9218 .unwrap_err(),
9219 FormatError::InvalidLength {
9220 structure: "archive",
9221 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
9222 actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1,
9223 }
9224 );
9225
9226 let mut header = test_volume_header();
9227 header.crypto_header_length = 512;
9228 let mut unavailable_crypto = header.to_bytes().to_vec();
9229 unavailable_crypto.resize(VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN, 0);
9230
9231 assert_eq!(
9232 open_archive(&unavailable_crypto, &master_key()).unwrap_err(),
9233 FormatError::InvalidLength {
9234 structure: "CryptoHeader",
9235 expected: VOLUME_HEADER_LEN + 512,
9236 actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
9237 }
9238 );
9239 }
9240
9241 #[test]
9242 fn seekable_open_recovers_physical_noncanonical_crypto_header_offset() {
9243 let archive = write_archive(
9244 &[RegularFile::new("offset.txt", b"offset")],
9245 &master_key(),
9246 small_block_recovery_options(),
9247 )
9248 .unwrap();
9249 let mut mutated = archive.bytes;
9250 let mut header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
9251 header.crypto_header_offset = VOLUME_HEADER_LEN as u32 + 1;
9252 mutated[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
9253
9254 let opened = open_archive(&mutated, &master_key()).unwrap();
9255 assert_eq!(
9256 opened.volume_header.crypto_header_offset,
9257 VOLUME_HEADER_LEN as u32
9258 );
9259 assert_eq!(
9260 opened.extract_file("offset.txt").unwrap(),
9261 Some(b"offset".to_vec())
9262 );
9263 opened.verify().unwrap();
9264 }
9265
9266 #[test]
9267 fn rejects_wrong_key_before_metadata_release() {
9268 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9269 let wrong = MasterKey::from_raw_key(&[0x43; 32]).unwrap();
9270
9271 assert_eq!(
9272 open_archive(&archive.bytes, &wrong).unwrap_err(),
9273 FormatError::HmacMismatch {
9274 structure: "CryptoHeader"
9275 }
9276 );
9277 }
9278
9279 #[test]
9280 fn rejects_payload_tamper_even_with_recomputed_block_crc() {
9281 let mut archive = write_archive(
9282 &[RegularFile::new("file.txt", b"authenticated")],
9283 &master_key(),
9284 single_stream_options(),
9285 )
9286 .unwrap()
9287 .bytes;
9288 let volume = VolumeHeader::parse(&archive[..VOLUME_HEADER_LEN]).unwrap();
9289 let crypto_end = VOLUME_HEADER_LEN + usize::try_from(volume.crypto_header_length).unwrap();
9290 let crypto = CryptoHeader::parse(
9291 &archive[VOLUME_HEADER_LEN..crypto_end],
9292 volume.crypto_header_length,
9293 )
9294 .unwrap();
9295 let block_size = crypto.fixed.block_size as usize;
9296 archive[crypto_end + 16] ^= 1;
9297 let crc_offset = crypto_end + 16 + block_size;
9298 let crc = crc32c::crc32c(&archive[crypto_end..crc_offset]);
9299 archive[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
9300
9301 let opened = open_archive(&archive, &master_key()).unwrap();
9302 assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
9303 }
9304
9305 #[test]
9306 fn list_and_extract_use_final_view_for_duplicate_paths() {
9307 let archive = write_archive(
9308 &[
9309 RegularFile::new("same.txt", b"old"),
9310 RegularFile::new("same.txt", b"newer"),
9311 ],
9312 &master_key(),
9313 single_stream_options(),
9314 )
9315 .unwrap();
9316 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9317
9318 assert_eq!(
9319 opened.list_index_entries().unwrap(),
9320 vec![ArchiveIndexEntry {
9321 path: "same.txt".to_string(),
9322 file_data_size: 5,
9323 }]
9324 );
9325 assert_eq!(
9326 opened.lookup_index_entry("same.txt").unwrap(),
9327 Some(ArchiveIndexEntry {
9328 path: "same.txt".to_string(),
9329 file_data_size: 5,
9330 })
9331 );
9332 assert_eq!(opened.lookup_index_entry("missing.txt").unwrap(), None);
9333 assert_eq!(
9334 opened.list_files().unwrap(),
9335 vec![ArchiveEntry {
9336 path: "same.txt".to_string(),
9337 file_data_size: 5,
9338 kind: TarEntryKind::Regular,
9339 mode: 0o644,
9340 mtime: 0,
9341 diagnostics: Vec::new(),
9342 }]
9343 );
9344 assert_eq!(
9345 opened.extract_file("same.txt").unwrap(),
9346 Some(b"newer".to_vec())
9347 );
9348 opened.verify().unwrap();
9349 }
9350
9351 #[test]
9352 fn index_entries_do_not_decrypt_payload_envelopes() {
9353 let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
9354 corrupt_payload_record(&mut opened.blocks, broken_payload_block);
9355
9356 assert_eq!(
9357 opened.list_index_entries().unwrap(),
9358 vec![
9359 ArchiveIndexEntry {
9360 path: "broken.txt".to_string(),
9361 file_data_size: b"broken payload\n".len() as u64,
9362 },
9363 ArchiveIndexEntry {
9364 path: "healthy.txt".to_string(),
9365 file_data_size: b"healthy payload\n".len() as u64,
9366 },
9367 ]
9368 );
9369 assert_eq!(
9370 opened.lookup_index_entry("broken.txt").unwrap(),
9371 Some(ArchiveIndexEntry {
9372 path: "broken.txt".to_string(),
9373 file_data_size: b"broken payload\n".len() as u64,
9374 })
9375 );
9376 assert_eq!(opened.list_files().unwrap_err(), FormatError::AeadFailure);
9377 }
9378
9379 #[test]
9380 fn extract_file_does_not_decrypt_unselected_payload_envelope() {
9381 let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
9384 corrupt_payload_record(&mut opened.blocks, broken_payload_block);
9385
9386 assert_eq!(
9387 opened.extract_file("healthy.txt").unwrap(),
9388 Some(b"healthy payload\n".to_vec())
9389 );
9390 assert_eq!(
9391 opened.extract_file("broken.txt").unwrap_err(),
9392 FormatError::AeadFailure
9393 );
9394 assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
9395 }
9396
9397 #[test]
9398 fn seekable_extract_does_not_read_unselected_payload_envelope() {
9399 let healthy = pseudo_random_bytes(64 * 1024);
9400 let broken = pseudo_random_bytes(64 * 1024);
9401 let options = WriterOptions {
9402 block_size: 4096,
9403 chunk_size: 4096,
9404 envelope_target_size: 8192,
9405 stripe_width: 1,
9406 volume_loss_tolerance: 0,
9407 bit_rot_buffer_pct: 0,
9408 fec_data_shards: 4,
9409 fec_parity_shards: 0,
9410 index_fec_data_shards: 4,
9411 index_fec_parity_shards: 0,
9412 index_root_fec_data_shards: 4,
9413 index_root_fec_parity_shards: 0,
9414 ..WriterOptions::default()
9415 };
9416 let archive = write_archive(
9417 &[
9418 RegularFile::new("healthy.bin", &healthy),
9419 RegularFile::new("broken.bin", &broken),
9420 ],
9421 &master_key(),
9422 options,
9423 )
9424 .unwrap();
9425 let eager = open_archive(&archive.bytes, &master_key()).unwrap();
9426 let healthy_envelopes = envelope_indices_for_path(&eager, "healthy.bin");
9427 let broken_envelopes = envelope_entries_for_path(&eager, "broken.bin");
9428 let denied_block_indices = broken_envelopes
9429 .iter()
9430 .filter(|envelope| !healthy_envelopes.contains(&envelope.envelope_index))
9431 .flat_map(|envelope| {
9432 let block_count =
9433 envelope.data_block_count as u64 + envelope.parity_block_count as u64;
9434 envelope.first_block_index..envelope.first_block_index + block_count
9435 })
9436 .collect::<BTreeSet<_>>();
9437 assert!(
9438 !denied_block_indices.is_empty(),
9439 "fixture must place broken.bin in at least one unshared envelope"
9440 );
9441 let denied_ranges = block_record_slots(&archive.bytes)
9442 .into_iter()
9443 .filter_map(|(offset, len, record)| {
9444 denied_block_indices
9445 .contains(&record.block_index)
9446 .then_some((offset as u64, (offset + len) as u64))
9447 })
9448 .collect::<Vec<_>>();
9449 assert!(!denied_ranges.is_empty());
9450
9451 let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
9452 let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
9453
9454 assert_eq!(opened.extract_file("healthy.bin").unwrap(), Some(healthy));
9455 for (read_start, read_end) in reader.reads() {
9456 assert!(
9457 denied_ranges
9458 .iter()
9459 .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
9460 "targeted extract read an unrelated payload BlockRecord range"
9461 );
9462 }
9463 assert_eq!(
9464 opened.extract_file("broken.bin").unwrap_err(),
9465 FormatError::InvalidArchive("denied test read")
9466 );
9467 }
9468
9469 #[test]
9470 fn extract_file_to_writer_streams_before_reading_later_envelopes() {
9471 struct FailOnFirstWrite;
9472
9473 impl std::io::Write for FailOnFirstWrite {
9474 fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
9475 Err(std::io::Error::other("sink stopped"))
9476 }
9477
9478 fn flush(&mut self) -> std::io::Result<()> {
9479 Ok(())
9480 }
9481 }
9482
9483 let payload = pseudo_random_bytes(128 * 1024);
9484 let options = WriterOptions {
9485 block_size: 4096,
9486 chunk_size: 4096,
9487 envelope_target_size: 8192,
9488 stripe_width: 1,
9489 volume_loss_tolerance: 0,
9490 bit_rot_buffer_pct: 0,
9491 fec_data_shards: 4,
9492 fec_parity_shards: 0,
9493 index_fec_data_shards: 4,
9494 index_fec_parity_shards: 0,
9495 index_root_fec_data_shards: 4,
9496 index_root_fec_parity_shards: 0,
9497 ..WriterOptions::default()
9498 };
9499 let archive = write_archive(
9500 &[RegularFile::new("large.bin", &payload)],
9501 &master_key(),
9502 options,
9503 )
9504 .unwrap();
9505 let eager = open_archive(&archive.bytes, &master_key()).unwrap();
9506 let envelopes = envelope_entries_for_path(&eager, "large.bin");
9507 let first_envelope = envelopes
9508 .first()
9509 .expect("large fixture should have at least one envelope")
9510 .envelope_index;
9511 let later_envelope_blocks = envelopes
9512 .iter()
9513 .filter(|entry| entry.envelope_index != first_envelope)
9514 .flat_map(|entry| {
9515 let block_count = entry.data_block_count as u64 + entry.parity_block_count as u64;
9516 entry.first_block_index..entry.first_block_index + block_count
9517 })
9518 .collect::<BTreeSet<_>>();
9519 assert!(
9520 !later_envelope_blocks.is_empty(),
9521 "fixture must span more than one payload envelope"
9522 );
9523 let denied_ranges = block_record_slots(&archive.bytes)
9524 .into_iter()
9525 .filter_map(|(offset, len, record)| {
9526 later_envelope_blocks
9527 .contains(&record.block_index)
9528 .then_some((offset as u64, (offset + len) as u64))
9529 })
9530 .collect::<Vec<_>>();
9531 assert!(!denied_ranges.is_empty());
9532
9533 let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
9534 let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
9535 let mut writer = FailOnFirstWrite;
9536
9537 let err = opened
9538 .extract_file_to_writer("large.bin", &mut writer)
9539 .unwrap_err();
9540 assert_eq!(err.to_string(), "extraction output write failed");
9541 for (read_start, read_end) in reader.reads() {
9542 assert!(
9543 denied_ranges
9544 .iter()
9545 .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
9546 "streaming writer read a later payload envelope before surfacing writer failure"
9547 );
9548 }
9549 }
9550
9551 #[test]
9552 fn extract_file_to_writer_writes_bounded_chunks() {
9553 struct ChunkRecorder {
9554 total: usize,
9555 max_write: usize,
9556 writes: usize,
9557 }
9558
9559 impl std::io::Write for ChunkRecorder {
9560 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
9561 self.total += buf.len();
9562 self.max_write = self.max_write.max(buf.len());
9563 self.writes += 1;
9564 Ok(buf.len())
9565 }
9566
9567 fn flush(&mut self) -> std::io::Result<()> {
9568 Ok(())
9569 }
9570 }
9571
9572 let payload = pseudo_random_bytes(128 * 1024);
9573 let options = WriterOptions {
9574 block_size: 4096,
9575 chunk_size: 4096,
9576 envelope_target_size: 8192,
9577 stripe_width: 1,
9578 volume_loss_tolerance: 0,
9579 bit_rot_buffer_pct: 0,
9580 fec_data_shards: 4,
9581 fec_parity_shards: 0,
9582 index_fec_data_shards: 4,
9583 index_fec_parity_shards: 0,
9584 index_root_fec_data_shards: 4,
9585 index_root_fec_parity_shards: 0,
9586 ..WriterOptions::default()
9587 };
9588 let archive = write_archive(
9589 &[RegularFile::new("large.bin", &payload)],
9590 &master_key(),
9591 options,
9592 )
9593 .unwrap();
9594 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9595 let mut writer = ChunkRecorder {
9596 total: 0,
9597 max_write: 0,
9598 writes: 0,
9599 };
9600
9601 opened
9602 .extract_file_to_writer("large.bin", &mut writer)
9603 .unwrap()
9604 .unwrap();
9605
9606 assert_eq!(writer.total, payload.len());
9607 assert!(writer.writes > 1);
9608 assert!(
9609 writer.max_write <= options.chunk_size as usize,
9610 "writer saw a {} byte chunk, larger than the {} byte frame target",
9611 writer.max_write,
9612 options.chunk_size
9613 );
9614 }
9615
9616 #[test]
9617 fn streaming_filesystem_extract_does_not_publish_partial_file_on_late_payload_error() {
9618 let payload = pseudo_random_bytes(128 * 1024);
9619 let options = WriterOptions {
9620 block_size: 4096,
9621 chunk_size: 4096,
9622 envelope_target_size: 8192,
9623 stripe_width: 1,
9624 volume_loss_tolerance: 0,
9625 bit_rot_buffer_pct: 0,
9626 fec_data_shards: 4,
9627 fec_parity_shards: 0,
9628 index_fec_data_shards: 4,
9629 index_fec_parity_shards: 0,
9630 index_root_fec_data_shards: 4,
9631 index_root_fec_parity_shards: 0,
9632 ..WriterOptions::default()
9633 };
9634 let archive = write_archive(
9635 &[RegularFile::new("large.bin", &payload)],
9636 &master_key(),
9637 options,
9638 )
9639 .unwrap();
9640 let eager = open_archive(&archive.bytes, &master_key()).unwrap();
9641 let envelopes = envelope_entries_for_path(&eager, "large.bin");
9642 let last_envelope = envelopes
9643 .last()
9644 .expect("large fixture should have at least one envelope");
9645 assert_ne!(
9646 envelopes.first().unwrap().envelope_index,
9647 last_envelope.envelope_index,
9648 "fixture must span more than one payload envelope"
9649 );
9650 let corrupt_slot = block_record_slots(&archive.bytes)
9651 .into_iter()
9652 .enumerate()
9653 .find_map(|(slot, (_, _, record))| {
9654 (record.block_index == last_envelope.first_block_index).then_some(slot)
9655 })
9656 .unwrap();
9657 let mut corrupted = archive.bytes;
9658 corrupt_block_record_payload_at_slot(&mut corrupted, corrupt_slot);
9659 let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
9660 let tmp = tempfile::tempdir().unwrap();
9661
9662 assert!(matches!(
9663 opened
9664 .extract_file_to("large.bin", tmp.path(), SafeExtractionOptions::default())
9665 .unwrap_err(),
9666 FormatError::AeadFailure | FormatError::FecTooFewAvailableShards
9667 ));
9668 assert!(!tmp.path().join("large.bin").exists());
9669 assert_eq!(std::fs::read_dir(tmp.path()).unwrap().count(), 0);
9670 }
9671
9672 #[test]
9673 fn bootstrap_sidecar_opens_lists_verifies_and_extracts() {
9674 let archive = write_archive(
9675 &[RegularFile::new("dir/sidecar.txt", b"hello sidecar")],
9676 &master_key(),
9677 single_stream_options(),
9678 )
9679 .unwrap();
9680 let opened = open_archive_with_bootstrap_sidecar(
9681 &archive.bytes,
9682 &archive.bootstrap_sidecar,
9683 &master_key(),
9684 )
9685 .unwrap();
9686
9687 assert_eq!(
9688 opened.list_files().unwrap(),
9689 vec![ArchiveEntry {
9690 path: "dir/sidecar.txt".to_string(),
9691 file_data_size: 13,
9692 kind: TarEntryKind::Regular,
9693 mode: 0o644,
9694 mtime: 0,
9695 diagnostics: Vec::new(),
9696 }]
9697 );
9698 assert_eq!(
9699 opened.extract_file("dir/sidecar.txt").unwrap(),
9700 Some(b"hello sidecar".to_vec())
9701 );
9702 opened.verify().unwrap();
9703 }
9704
9705 #[test]
9706 fn dictionary_archive_opens_lists_verifies_and_extracts_seekable() {
9707 let archive = write_archive_with_dictionary(
9708 &[RegularFile::new(
9709 "dir/dict.txt",
9710 b"common words common words dictionary payload",
9711 )],
9712 &master_key(),
9713 single_stream_options(),
9714 dictionary(),
9715 )
9716 .unwrap();
9717 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9718
9719 assert_eq!(opened.crypto_header.has_dictionary, 1);
9720 assert!(opened.index_root.header.dictionary_data_block_count > 0);
9721 assert_eq!(
9722 opened.list_files().unwrap(),
9723 vec![ArchiveEntry {
9724 path: "dir/dict.txt".to_string(),
9725 file_data_size: 44,
9726 kind: TarEntryKind::Regular,
9727 mode: 0o644,
9728 mtime: 0,
9729 diagnostics: Vec::new(),
9730 }]
9731 );
9732 assert_eq!(
9733 opened.extract_file("dir/dict.txt").unwrap(),
9734 Some(b"common words common words dictionary payload".to_vec())
9735 );
9736 opened.verify().unwrap();
9737 }
9738
9739 #[test]
9740 fn dictionary_object_tamper_fails_before_payload_decompression() {
9741 let archive = write_archive_with_dictionary(
9742 &[RegularFile::new(
9743 "dir/dict.txt",
9744 b"common words common words dictionary payload",
9745 )],
9746 &master_key(),
9747 single_stream_options(),
9748 dictionary(),
9749 )
9750 .unwrap();
9751 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9752 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
9753 let crypto_end = VOLUME_HEADER_LEN + volume_header.crypto_header_length as usize;
9754 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
9755 let dictionary_offset =
9756 crypto_end + opened.index_root.header.dictionary_first_block as usize * record_len;
9757
9758 let mut tampered = archive.bytes.clone();
9759 tampered[dictionary_offset + 16] ^= 0x01;
9760 let crc_offset = dictionary_offset + 16 + opened.crypto_header.block_size as usize;
9761 let crc = crc32c::crc32c(&tampered[dictionary_offset..crc_offset]);
9762 tampered[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
9763
9764 assert_eq!(
9765 open_archive(&tampered, &master_key()).unwrap_err(),
9766 FormatError::AeadFailure
9767 );
9768 }
9769
9770 #[test]
9771 fn dictionary_archive_bootstraps_from_sidecar_for_non_seekable_open() {
9772 let archive = write_archive_with_dictionary(
9773 &[RegularFile::new(
9774 "dict-sidecar.txt",
9775 b"common words common words sidecar payload",
9776 )],
9777 &master_key(),
9778 single_stream_options(),
9779 dictionary(),
9780 )
9781 .unwrap();
9782 let opened = open_non_seekable_archive(
9783 &archive.bytes,
9784 &master_key(),
9785 Some(&archive.bootstrap_sidecar),
9786 )
9787 .unwrap();
9788
9789 assert_eq!(
9790 opened.extract_file("dict-sidecar.txt").unwrap(),
9791 Some(b"common words common words sidecar payload".to_vec())
9792 );
9793 opened.verify().unwrap();
9794 }
9795
9796 #[test]
9797 fn non_seekable_full_sidecar_bootstraps_when_terminal_trailer_is_corrupt() {
9798 let archive = write_archive(
9799 &[RegularFile::new(
9800 "sidecar-terminal.txt",
9801 b"sidecar authority",
9802 )],
9803 &master_key(),
9804 single_stream_options(),
9805 )
9806 .unwrap();
9807 let mut corrupted = archive.bytes.clone();
9808 corrupt_v41_terminal_recovery(&mut corrupted);
9809 assert!(open_archive(&corrupted, &master_key()).is_err());
9810
9811 let opened =
9812 open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
9813 .unwrap();
9814
9815 assert!(opened.volume_trailer.is_none());
9816 assert_eq!(
9817 opened.extract_file("sidecar-terminal.txt").unwrap(),
9818 Some(b"sidecar authority".to_vec())
9819 );
9820 opened.verify().unwrap();
9821 }
9822
9823 #[test]
9824 fn dictionary_full_sidecar_bootstraps_when_terminal_material_is_absent() {
9825 let archive = write_archive_with_dictionary(
9826 &[RegularFile::new(
9827 "dict-no-terminal.txt",
9828 b"common words common words without terminal",
9829 )],
9830 &master_key(),
9831 single_stream_options(),
9832 dictionary(),
9833 )
9834 .unwrap();
9835 let terminal_offset = terminal_material_offset(&archive.bytes);
9836 let truncated = archive.bytes[..terminal_offset].to_vec();
9837 assert!(open_archive(&truncated, &master_key()).is_err());
9838
9839 let opened =
9840 open_non_seekable_archive(&truncated, &master_key(), Some(&archive.bootstrap_sidecar))
9841 .unwrap();
9842
9843 assert!(opened.volume_trailer.is_none());
9844 assert_eq!(
9845 opened.extract_file("dict-no-terminal.txt").unwrap(),
9846 Some(b"common words common words without terminal".to_vec())
9847 );
9848 opened.verify().unwrap();
9849 }
9850
9851 #[test]
9852 fn bootstrap_sidecar_treats_crc_failed_payload_block_as_erasure() {
9853 let archive = write_archive(
9854 &[RegularFile::new(
9855 "sidecar-erasure.txt",
9856 b"repair through sidecar",
9857 )],
9858 &master_key(),
9859 single_stream_options(),
9860 )
9861 .unwrap();
9862 let mut corrupted = archive.bytes.clone();
9863 corrupt_first_block_record_payload(&mut corrupted);
9864
9865 let opened = open_archive_with_bootstrap_sidecar(
9866 &corrupted,
9867 &archive.bootstrap_sidecar,
9868 &master_key(),
9869 )
9870 .unwrap();
9871 assert_eq!(
9872 opened.extract_file("sidecar-erasure.txt").unwrap(),
9873 Some(b"repair through sidecar".to_vec())
9874 );
9875 }
9876
9877 #[test]
9878 fn extraction_rejects_logical_payload_above_total_size_cap() {
9879 let archive = write_archive(
9880 &[RegularFile::new("cap.txt", b"payload")],
9881 &master_key(),
9882 single_stream_options(),
9883 )
9884 .unwrap();
9885 let options = ReaderOptions {
9886 max_total_extraction_size: 3,
9887 ..ReaderOptions::default()
9888 };
9889 let opened =
9890 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
9891
9892 assert_eq!(
9893 opened.extract_file("cap.txt").unwrap_err(),
9894 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
9895 );
9896 }
9897
9898 #[test]
9899 fn verify_does_not_apply_extraction_payload_cap() {
9900 let archive = write_archive(
9901 &[RegularFile::new("verify-cap.txt", b"payload")],
9902 &master_key(),
9903 single_stream_options(),
9904 )
9905 .unwrap();
9906 let options = ReaderOptions {
9907 max_total_extraction_size: 3,
9908 ..ReaderOptions::default()
9909 };
9910 let opened =
9911 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
9912
9913 opened.verify().unwrap();
9914 assert_eq!(
9915 opened.extract_file("verify-cap.txt").unwrap_err(),
9916 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
9917 );
9918 }
9919
9920 #[test]
9921 fn verify_streams_past_legacy_in_memory_tar_cap() {
9922 let data = vec![0x5a; 4096];
9923 let archive = write_archive(
9924 &[RegularFile::new("verify-large.txt", &data)],
9925 &master_key(),
9926 single_stream_options(),
9927 )
9928 .unwrap();
9929 let options = ReaderOptions {
9930 max_verify_tar_size: 1,
9931 ..ReaderOptions::default()
9932 };
9933 let opened =
9934 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
9935
9936 opened.verify().unwrap();
9937 }
9938
9939 #[test]
9940 fn dictionary_sidecar_requires_dictionary_record_section() {
9941 let archive = write_archive_with_dictionary(
9942 &[RegularFile::new("dict-missing.txt", b"common words")],
9943 &master_key(),
9944 single_stream_options(),
9945 dictionary(),
9946 )
9947 .unwrap();
9948 let header = BootstrapSidecarHeader::parse(
9949 &archive.bootstrap_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN],
9950 )
9951 .unwrap();
9952 let mut missing_dictionary =
9953 archive.bootstrap_sidecar[..header.dictionary_records_offset as usize].to_vec();
9954 rewrite_sidecar_header(&mut missing_dictionary, &master_key(), |header| {
9955 header.flags &= !0x04;
9956 header.dictionary_records_offset = 0;
9957 header.dictionary_records_length = 0;
9958 });
9959
9960 assert_eq!(
9961 open_non_seekable_archive(&archive.bytes, &master_key(), Some(&missing_dictionary))
9962 .unwrap_err(),
9963 FormatError::ReaderUnsupported("dictionary bootstrap required")
9964 );
9965 }
9966
9967 #[test]
9968 fn dictionary_sidecar_records_are_validated_against_dictionary_extent() {
9969 let archive = write_archive_with_dictionary(
9970 &[RegularFile::new("dict-sidecar-kind.txt", b"common words")],
9971 &master_key(),
9972 single_stream_options(),
9973 dictionary(),
9974 )
9975 .unwrap();
9976
9977 let mut wrong_kind = archive.bootstrap_sidecar.clone();
9978 mutate_sidecar_dictionary_record(&mut wrong_kind, 0, |record| {
9979 record.kind = BlockKind::IndexRootData;
9980 });
9981 assert_eq!(
9982 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
9983 .unwrap_err(),
9984 FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
9985 );
9986
9987 let mut wrong_last = archive.bootstrap_sidecar.clone();
9988 mutate_sidecar_dictionary_record(&mut wrong_last, 0, |record| {
9989 record.flags = 0;
9990 });
9991 assert_eq!(
9992 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
9993 .unwrap_err(),
9994 FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
9995 );
9996 }
9997
9998 #[test]
9999 fn non_seekable_random_access_requires_sidecar() {
10000 let archive = write_archive(
10001 &[RegularFile::new("file.txt", b"payload")],
10002 &master_key(),
10003 single_stream_options(),
10004 )
10005 .unwrap();
10006
10007 assert_eq!(
10008 open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
10009 FormatError::ReaderUnsupported(
10010 "non-seekable random access requires a bootstrap sidecar"
10011 )
10012 );
10013 assert!(open_non_seekable_archive(
10014 &archive.bytes,
10015 &master_key(),
10016 Some(&archive.bootstrap_sidecar)
10017 )
10018 .is_ok());
10019 }
10020
10021 #[test]
10022 fn non_seekable_bootstrap_rejects_index_root_only_sidecar() {
10023 let archive = write_archive(
10024 &[RegularFile::new("sparse.txt", b"sparse sidecar")],
10025 &master_key(),
10026 single_stream_options(),
10027 )
10028 .unwrap();
10029 let index_root_only = sparse_bootstrap_sidecar(
10030 &archive.bootstrap_sidecar,
10031 &master_key(),
10032 false,
10033 true,
10034 false,
10035 );
10036
10037 assert_eq!(
10038 open_non_seekable_archive(&archive.bytes, &master_key(), Some(&index_root_only))
10039 .unwrap_err(),
10040 FormatError::ReaderUnsupported(
10041 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
10042 )
10043 );
10044 }
10045
10046 #[test]
10047 fn seekable_sidecar_uses_index_root_records_after_terminal_manifest_authority() {
10048 let archive = write_archive(
10049 &[RegularFile::new("sparse-index.txt", b"recover index root")],
10050 &master_key(),
10051 single_stream_options(),
10052 )
10053 .unwrap();
10054 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10055 let mut corrupted = archive.bytes.clone();
10056 corrupt_object_extent_records(
10057 &mut corrupted,
10058 index_root_extent_from_manifest(&opened.manifest_footer),
10059 );
10060 assert!(open_archive(&corrupted, &master_key()).is_err());
10061
10062 let index_root_only = sparse_bootstrap_sidecar(
10063 &archive.bootstrap_sidecar,
10064 &master_key(),
10065 false,
10066 true,
10067 false,
10068 );
10069 let recovered =
10070 open_archive_with_bootstrap_sidecar(&corrupted, &index_root_only, &master_key())
10071 .unwrap();
10072
10073 assert_eq!(
10074 recovered.extract_file("sparse-index.txt").unwrap(),
10075 Some(b"recover index root".to_vec())
10076 );
10077 recovered.verify().unwrap();
10078 }
10079
10080 #[test]
10081 fn seekable_sidecar_uses_dictionary_records_after_index_root_authority() {
10082 let archive = write_archive_with_dictionary(
10083 &[RegularFile::new(
10084 "sparse-dict.txt",
10085 b"common words common words sparse dictionary",
10086 )],
10087 &master_key(),
10088 single_stream_options(),
10089 dictionary(),
10090 )
10091 .unwrap();
10092 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10093 let mut corrupted = archive.bytes.clone();
10094 corrupt_object_extent_records(
10095 &mut corrupted,
10096 dictionary_extent_from_index_root(&opened.index_root).unwrap(),
10097 );
10098 assert!(open_archive(&corrupted, &master_key()).is_err());
10099
10100 let dictionary_only = sparse_bootstrap_sidecar(
10101 &archive.bootstrap_sidecar,
10102 &master_key(),
10103 false,
10104 false,
10105 true,
10106 );
10107 assert_eq!(
10108 open_non_seekable_archive(&archive.bytes, &master_key(), Some(&dictionary_only))
10109 .unwrap_err(),
10110 FormatError::ReaderUnsupported(
10111 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
10112 )
10113 );
10114
10115 let recovered =
10116 open_archive_with_bootstrap_sidecar(&corrupted, &dictionary_only, &master_key())
10117 .unwrap();
10118 assert_eq!(
10119 recovered.extract_file("sparse-dict.txt").unwrap(),
10120 Some(b"common words common words sparse dictionary".to_vec())
10121 );
10122 recovered.verify().unwrap();
10123 }
10124
10125 #[test]
10126 fn sequential_extracts_dictionary_free_tar_stream() {
10127 let archive = write_archive(
10128 &[RegularFile::new("seq.txt", b"streaming")],
10129 &master_key(),
10130 single_stream_options(),
10131 )
10132 .unwrap();
10133
10134 let tar_stream = sequential_extract_tar_stream(&archive.bytes, &master_key()).unwrap();
10135 let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
10136 assert_eq!(member.path, b"seq.txt");
10137 assert_eq!(member.data, b"streaming");
10138 }
10139
10140 #[test]
10141 fn sequential_rejects_logical_payload_above_total_size_cap() {
10142 let archive = write_archive(
10143 &[RegularFile::new("seq-cap.txt", b"payload")],
10144 &master_key(),
10145 single_stream_options(),
10146 )
10147 .unwrap();
10148 let options = ReaderOptions {
10149 max_total_extraction_size: 3,
10150 ..ReaderOptions::default()
10151 };
10152
10153 assert_eq!(
10154 sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
10155 .unwrap_err(),
10156 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
10157 );
10158 }
10159
10160 #[test]
10161 fn sequential_rejects_tar_stream_above_buffer_cap_during_decode() {
10162 let archive = write_archive(
10163 &[RegularFile::new("seq-buffer-cap.txt", b"payload")],
10164 &master_key(),
10165 single_stream_options(),
10166 )
10167 .unwrap();
10168 let options = ReaderOptions {
10169 max_verify_tar_size: 512,
10170 ..ReaderOptions::default()
10171 };
10172
10173 assert_eq!(
10174 sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
10175 .unwrap_err(),
10176 FormatError::ReaderUnsupported(
10177 "sequential tar stream exceeds configured verification cap"
10178 )
10179 );
10180 }
10181
10182 #[test]
10183 fn sequential_repairs_crc_failed_payload_data_when_parity_is_guaranteed() {
10184 let archive = write_archive(
10185 &[RegularFile::new("seq-erasure.txt", b"stream repair")],
10186 &master_key(),
10187 single_stream_options(),
10188 )
10189 .unwrap();
10190 let mut corrupted = archive.bytes;
10191 corrupt_first_block_record_payload(&mut corrupted);
10192
10193 let tar_stream = sequential_extract_tar_stream(&corrupted, &master_key()).unwrap();
10194 let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
10195 assert_eq!(member.path, b"seq-erasure.txt");
10196 assert_eq!(member.data, b"stream repair");
10197 }
10198
10199 #[test]
10200 fn sequential_rejects_crc_failed_payload_data_without_guaranteed_parity() {
10201 let archive = write_archive(
10202 &[RegularFile::new("seq-no-parity.txt", b"no repair")],
10203 &master_key(),
10204 WriterOptions {
10205 bit_rot_buffer_pct: 0,
10206 fec_parity_shards: 0,
10207 index_fec_parity_shards: 0,
10208 index_root_fec_parity_shards: 0,
10209 ..single_stream_options()
10210 },
10211 )
10212 .unwrap();
10213 let mut corrupted = archive.bytes;
10214 corrupt_first_block_record_payload(&mut corrupted);
10215
10216 assert_eq!(
10217 sequential_extract_tar_stream(&corrupted, &master_key()).unwrap_err(),
10218 FormatError::BadCrc {
10219 structure: "BlockRecord"
10220 }
10221 );
10222 }
10223
10224 #[test]
10225 fn sequential_rejects_when_terminal_authentication_fails_without_returning_bytes() {
10226 let archive = write_archive(
10227 &[RegularFile::new(
10228 "seq.txt",
10229 b"payload must not be returned after terminal auth failure",
10230 )],
10231 &master_key(),
10232 single_stream_options(),
10233 )
10234 .unwrap();
10235 let mut corrupted = archive.bytes;
10236 corrupt_v41_terminal_recovery(&mut corrupted);
10237
10238 match sequential_extract_tar_stream(&corrupted, &master_key()) {
10239 Ok(bytes) => panic!(
10240 "sequential helper returned {} decoded byte(s) despite terminal HMAC failure",
10241 bytes.len()
10242 ),
10243 Err(err) => assert_eq!(
10244 err,
10245 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
10246 ),
10247 }
10248 }
10249
10250 #[test]
10251 fn sequential_rejects_dictionary_archive_without_bootstrap_before_payload_release() {
10252 let archive = write_archive_with_dictionary(
10253 &[RegularFile::new(
10254 "seq-dict.txt",
10255 b"common words common words dictionary payload",
10256 )],
10257 &master_key(),
10258 single_stream_options(),
10259 b"common words dictionary",
10260 )
10261 .unwrap();
10262
10263 match sequential_extract_tar_stream(&archive.bytes, &master_key()) {
10264 Ok(bytes) => panic!(
10265 "sequential helper returned {} decoded byte(s) for dictionary archive without bootstrap",
10266 bytes.len()
10267 ),
10268 Err(err) => assert_eq!(
10269 err,
10270 FormatError::ReaderUnsupported(
10271 "dictionary bootstrap required for non-seekable sequential extraction"
10272 )
10273 ),
10274 }
10275 }
10276
10277 #[test]
10278 fn non_seekable_dictionary_error_keeps_missing_bootstrap_wording() {
10279 let archive = write_archive_with_dictionary(
10280 &[RegularFile::new(
10281 "seq-dict-open.txt",
10282 b"common words common words bootstrap required",
10283 )],
10284 &master_key(),
10285 single_stream_options(),
10286 b"common words bootstrap",
10287 )
10288 .unwrap();
10289
10290 assert_eq!(
10291 open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
10292 FormatError::ReaderUnsupported(
10293 "non-seekable random access requires a bootstrap sidecar"
10294 )
10295 );
10296 }
10297
10298 #[test]
10299 fn sequential_zstd_stream_rejects_skippable_frame_segments() {
10300 let skippable = [0x50, 0x2a, 0x4d, 0x18, 0, 0, 0, 0];
10301 let mut output = Vec::new();
10302
10303 assert_eq!(
10304 decode_concatenated_zstd_frames_with_cap(
10305 &skippable,
10306 None,
10307 &mut output,
10308 usize::MAX,
10309 None,
10310 )
10311 .unwrap_err(),
10312 FormatError::NotStandardZstdFrame
10313 );
10314 assert!(output.is_empty());
10315 }
10316
10317 #[test]
10318 fn live_non_seekable_verify_stream_accepts_single_volume_archive() {
10319 let archive = write_archive(
10320 &[RegularFile::new("live.txt", b"stream verify")],
10321 &master_key(),
10322 single_stream_options(),
10323 )
10324 .unwrap();
10325
10326 let report =
10327 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
10328
10329 assert_eq!(report.file_count, 1);
10330 assert_eq!(report.total_volumes, 1);
10331 assert_eq!(report.root_auth, SequentialRootAuthStatus::Absent);
10332 assert!(report.payload_block_count > 0);
10333 }
10334
10335 #[test]
10336 fn live_non_seekable_verify_stream_accepts_tiny_read_chunks() {
10337 let archive = write_archive(
10338 &[RegularFile::new("tiny-chunks.txt", b"one byte at a time")],
10339 &master_key(),
10340 single_stream_options(),
10341 )
10342 .unwrap();
10343
10344 let report =
10345 verify_non_seekable_stream(ChunkedReader::new(archive.bytes, 1), &master_key())
10346 .unwrap();
10347
10348 assert_eq!(report.file_count, 1);
10349 assert_eq!(report.tar_total_size % 512, 0);
10350 }
10351
10352 #[test]
10353 fn live_non_seekable_verify_stream_accepts_empty_archive() {
10354 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10355
10356 let report =
10357 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
10358
10359 assert_eq!(report.file_count, 0);
10360 assert_eq!(report.payload_block_count, 0);
10361 assert_eq!(report.tar_total_size, 0);
10362 }
10363
10364 #[test]
10365 fn live_non_seekable_verify_rejects_dictionary_archive_without_bootstrap() {
10366 let archive = write_archive_with_dictionary(
10367 &[RegularFile::new(
10368 "live-dict.txt",
10369 b"common words common words dictionary payload",
10370 )],
10371 &master_key(),
10372 single_stream_options(),
10373 b"common words dictionary",
10374 )
10375 .unwrap();
10376
10377 assert_eq!(
10378 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key())
10379 .unwrap_err(),
10380 FormatError::ReaderUnsupported(
10381 "dictionary bootstrap required for non-seekable sequential verification"
10382 )
10383 );
10384 }
10385
10386 #[test]
10387 fn live_non_seekable_verify_accepts_dictionary_archive_with_bootstrap() {
10388 let archive = write_archive_with_dictionary(
10389 &[RegularFile::new(
10390 "live-dict-sidecar.txt",
10391 b"common words common words dictionary payload",
10392 )],
10393 &master_key(),
10394 single_stream_options(),
10395 b"common words dictionary",
10396 )
10397 .unwrap();
10398
10399 let report = verify_non_seekable_stream_with_bootstrap_sidecar(
10400 std::io::Cursor::new(archive.bytes),
10401 &archive.bootstrap_sidecar,
10402 &master_key(),
10403 NonSeekableReaderOptions::default(),
10404 )
10405 .unwrap();
10406
10407 assert_eq!(report.file_count, 1);
10408 assert_eq!(report.total_volumes, 1);
10409 }
10410
10411 #[test]
10412 fn live_non_seekable_verify_rejects_terminal_tail_above_cap() {
10413 let archive = write_archive(
10414 &[RegularFile::new("tail-cap.txt", b"payload")],
10415 &master_key(),
10416 single_stream_options(),
10417 )
10418 .unwrap();
10419 let options = NonSeekableReaderOptions {
10420 max_terminal_tail_size: 8,
10421 ..NonSeekableReaderOptions::default()
10422 };
10423
10424 assert_eq!(
10425 verify_non_seekable_stream_with_options(
10426 std::io::Cursor::new(archive.bytes),
10427 &master_key(),
10428 options
10429 )
10430 .unwrap_err(),
10431 FormatError::ReaderUnsupported("terminal tail exceeds configured cap")
10432 );
10433 }
10434
10435 #[test]
10436 fn live_non_seekable_verify_rejects_metadata_above_retention_cap() {
10437 let archive = write_archive(
10438 &[RegularFile::new("metadata-cap.txt", b"payload")],
10439 &master_key(),
10440 single_stream_options(),
10441 )
10442 .unwrap();
10443 let options = NonSeekableReaderOptions {
10444 max_retained_metadata_bytes: 1,
10445 ..NonSeekableReaderOptions::default()
10446 };
10447
10448 assert_eq!(
10449 verify_non_seekable_stream_with_options(
10450 std::io::Cursor::new(archive.bytes),
10451 &master_key(),
10452 options
10453 )
10454 .unwrap_err(),
10455 FormatError::ReaderUnsupported("retained metadata exceeds configured streaming cap")
10456 );
10457 }
10458
10459 #[test]
10460 fn live_non_seekable_verify_repairs_crc_failed_metadata_block() {
10461 let archive = write_archive(
10462 &[RegularFile::new("metadata-erasure.txt", b"payload")],
10463 &master_key(),
10464 single_stream_options(),
10465 )
10466 .unwrap();
10467 let mut corrupted = archive.bytes;
10468 let slot = first_block_record_slot_with_kind(&corrupted, BlockKind::IndexRootData).unwrap();
10469 corrupt_block_record_payload_at_slot(&mut corrupted, slot);
10470
10471 let report =
10472 verify_non_seekable_stream(std::io::Cursor::new(corrupted), &master_key()).unwrap();
10473
10474 assert_eq!(report.file_count, 1);
10475 }
10476
10477 #[test]
10478 fn live_non_seekable_verify_rejects_member_count_above_cap() {
10479 let archive = write_archive(
10480 &[RegularFile::new("member-cap.txt", b"payload")],
10481 &master_key(),
10482 single_stream_options(),
10483 )
10484 .unwrap();
10485 let options = NonSeekableReaderOptions {
10486 max_streamed_member_count: 0,
10487 ..NonSeekableReaderOptions::default()
10488 };
10489
10490 assert_eq!(
10491 verify_non_seekable_stream_with_options(
10492 std::io::Cursor::new(archive.bytes),
10493 &master_key(),
10494 options
10495 )
10496 .unwrap_err(),
10497 FormatError::ReaderUnsupported("tar member count exceeds configured streaming cap")
10498 );
10499 }
10500
10501 #[test]
10502 fn live_non_seekable_verify_rejects_total_extraction_cap_during_decode() {
10503 let archive = write_archive(
10504 &[RegularFile::new("live-total-cap.txt", b"payload")],
10505 &master_key(),
10506 single_stream_options(),
10507 )
10508 .unwrap();
10509 let mut options = NonSeekableReaderOptions::default();
10510 options.reader.max_total_extraction_size = 3;
10511
10512 assert_eq!(
10513 verify_non_seekable_stream_with_options(
10514 std::io::Cursor::new(archive.bytes),
10515 &master_key(),
10516 options
10517 )
10518 .unwrap_err(),
10519 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
10520 );
10521 }
10522
10523 #[test]
10524 fn live_non_seekable_verify_reports_root_auth_wire_only() {
10525 let archive = write_archive_with_root_auth(
10526 &[RegularFile::new("signed-live.txt", b"root-auth stream")],
10527 &master_key(),
10528 single_stream_options(),
10529 test_root_auth_config(),
10530 |request| Ok(test_root_auth_value(request)),
10531 )
10532 .unwrap();
10533
10534 let report =
10535 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
10536
10537 assert_eq!(report.root_auth, SequentialRootAuthStatus::WireValidOnly);
10538 }
10539
10540 #[test]
10541 fn live_non_seekable_extract_stream_commits_after_terminal_verify() {
10542 let archive = write_archive(
10543 &[
10544 RegularFile::new("alpha.txt", b"alpha"),
10545 RegularFile::new("nested/beta.txt", b"beta"),
10546 ],
10547 &master_key(),
10548 single_stream_options(),
10549 )
10550 .unwrap();
10551 let tmp = tempfile::tempdir().unwrap();
10552 let out = tmp.path().join("out");
10553
10554 let report = extract_non_seekable_stream_to_dir(
10555 std::io::Cursor::new(archive.bytes),
10556 &master_key(),
10557 &out,
10558 NonSeekableReaderOptions::default(),
10559 SafeExtractionOptions::default(),
10560 )
10561 .unwrap();
10562
10563 assert_eq!(report.verification.file_count, 2);
10564 assert_eq!(report.extracted_member_count, 2);
10565 assert_eq!(fs::read(out.join("alpha.txt")).unwrap(), b"alpha");
10566 assert_eq!(fs::read(out.join("nested/beta.txt")).unwrap(), b"beta");
10567 }
10568
10569 #[test]
10570 fn live_non_seekable_extract_stream_accepts_tiny_read_chunks() {
10571 let archive = write_archive(
10572 &[RegularFile::new("tiny-extract.txt", b"chunked extraction")],
10573 &master_key(),
10574 single_stream_options(),
10575 )
10576 .unwrap();
10577 let tmp = tempfile::tempdir().unwrap();
10578 let out = tmp.path().join("out");
10579
10580 extract_non_seekable_stream_to_dir(
10581 ChunkedReader::new(archive.bytes, 1),
10582 &master_key(),
10583 &out,
10584 NonSeekableReaderOptions::default(),
10585 SafeExtractionOptions::default(),
10586 )
10587 .unwrap();
10588
10589 assert_eq!(
10590 fs::read(out.join("tiny-extract.txt")).unwrap(),
10591 b"chunked extraction"
10592 );
10593 }
10594
10595 #[test]
10596 fn live_non_seekable_extract_stream_terminal_failure_leaves_no_final_output() {
10597 let archive = write_archive(
10598 &[RegularFile::new("late-fail.txt", b"must remain staged")],
10599 &master_key(),
10600 single_stream_options(),
10601 )
10602 .unwrap();
10603 let mut corrupted = archive.bytes;
10604 corrupt_v41_terminal_recovery(&mut corrupted);
10605 let tmp = tempfile::tempdir().unwrap();
10606 let out = tmp.path().join("out");
10607
10608 match extract_non_seekable_stream_to_dir(
10609 std::io::Cursor::new(corrupted),
10610 &master_key(),
10611 &out,
10612 NonSeekableReaderOptions::default(),
10613 SafeExtractionOptions::default(),
10614 )
10615 .unwrap_err()
10616 {
10617 ExtractError::Format(err) => assert_eq!(
10618 err,
10619 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
10620 ),
10621 ExtractError::Output(err) => panic!("unexpected output error: {err}"),
10622 }
10623 assert!(!out.exists());
10624 }
10625
10626 #[test]
10627 fn live_non_seekable_extract_stream_existing_destination_obeys_overwrite_policy() {
10628 let archive = write_archive(
10629 &[RegularFile::new("same.txt", b"new")],
10630 &master_key(),
10631 single_stream_options(),
10632 )
10633 .unwrap();
10634 let tmp = tempfile::tempdir().unwrap();
10635 let out = tmp.path().join("out");
10636 fs::create_dir(&out).unwrap();
10637 fs::write(out.join("same.txt"), b"old").unwrap();
10638
10639 match extract_non_seekable_stream_to_dir(
10640 std::io::Cursor::new(archive.bytes.clone()),
10641 &master_key(),
10642 &out,
10643 NonSeekableReaderOptions::default(),
10644 SafeExtractionOptions::default(),
10645 )
10646 .unwrap_err()
10647 {
10648 ExtractError::Format(err) => assert_eq!(err, FormatError::UnsafeOverwrite),
10649 ExtractError::Output(err) => panic!("unexpected output error: {err}"),
10650 }
10651 assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"old");
10652
10653 extract_non_seekable_stream_to_dir(
10654 std::io::Cursor::new(archive.bytes),
10655 &master_key(),
10656 &out,
10657 NonSeekableReaderOptions::default(),
10658 SafeExtractionOptions {
10659 overwrite_existing: true,
10660 },
10661 )
10662 .unwrap();
10663 assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"new");
10664 }
10665
10666 #[test]
10667 fn live_non_seekable_list_stream_matches_seekable_final_view() {
10668 let archive = write_archive(
10669 &[
10670 RegularFile::new("a.txt", b"a"),
10671 RegularFile::new("b.txt", b"bb"),
10672 ],
10673 &master_key(),
10674 single_stream_options(),
10675 )
10676 .unwrap();
10677 let seekable = open_archive(&archive.bytes, &master_key()).unwrap();
10678 let expected = seekable.list_files().unwrap();
10679
10680 let report = list_non_seekable_stream(
10681 std::io::Cursor::new(archive.bytes),
10682 &master_key(),
10683 NonSeekableReaderOptions::default(),
10684 )
10685 .unwrap();
10686
10687 assert_eq!(report.verification.file_count, 2);
10688 assert_eq!(report.entries, expected);
10689 }
10690
10691 #[test]
10692 fn bootstrap_sidecar_rejects_bad_flags_and_trailing_bytes() {
10693 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10694 let mut bad_flags = archive.bootstrap_sidecar.clone();
10695 rewrite_sidecar_header(&mut bad_flags, &master_key(), |header| {
10696 header.flags |= 0x08;
10697 });
10698 assert_eq!(
10699 open_archive_with_bootstrap_sidecar(&archive.bytes, &bad_flags, &master_key())
10700 .unwrap_err(),
10701 FormatError::UnknownBootstrapSidecarFlags(0x0b)
10702 );
10703
10704 let mut trailing = archive.bootstrap_sidecar.clone();
10705 trailing.push(0);
10706 assert_eq!(
10707 open_archive_with_bootstrap_sidecar(&archive.bytes, &trailing, &master_key())
10708 .unwrap_err(),
10709 FormatError::NonCanonicalBootstrapSidecarLayout
10710 );
10711 }
10712
10713 #[test]
10714 fn bootstrap_sidecar_rejects_bad_manifest_footer_semantics() {
10715 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10716 let mut wrong_volume = archive.bootstrap_sidecar.clone();
10717 mutate_sidecar_manifest(&mut wrong_volume, &master_key(), |footer| {
10718 footer.volume_index = 1;
10719 });
10720 assert_eq!(
10721 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_volume, &master_key())
10722 .unwrap_err(),
10723 FormatError::InvalidArchive("sidecar ManifestFooter volume_index must be zero")
10724 );
10725
10726 let mut non_authoritative = archive.bootstrap_sidecar.clone();
10727 mutate_sidecar_manifest(&mut non_authoritative, &master_key(), |footer| {
10728 footer.is_authoritative = 0;
10729 });
10730 assert_eq!(
10731 open_archive_with_bootstrap_sidecar(&archive.bytes, &non_authoritative, &master_key())
10732 .unwrap_err(),
10733 FormatError::InvalidArchive("sidecar ManifestFooter is not authoritative")
10734 );
10735 }
10736
10737 #[test]
10738 fn sidecar_manifest_validation_does_not_compare_opened_volume_index() {
10739 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10740 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
10741 let crypto_start = volume_header.crypto_header_offset as usize;
10742 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
10743 let crypto_header = CryptoHeader::parse(
10744 &archive.bytes[crypto_start..crypto_end],
10745 volume_header.crypto_header_length,
10746 )
10747 .unwrap();
10748 let subkeys = Subkeys::derive(
10749 &master_key(),
10750 &volume_header.archive_uuid,
10751 &volume_header.session_id,
10752 )
10753 .unwrap();
10754 let mut opened_header = volume_header;
10755 opened_header.volume_index = 1;
10756
10757 let parsed = parse_bootstrap_sidecar(
10758 &archive.bootstrap_sidecar,
10759 &opened_header,
10760 &crypto_header.fixed,
10761 &subkeys,
10762 )
10763 .unwrap();
10764
10765 assert_eq!(parsed.manifest_footer.unwrap().volume_index, 0);
10766 }
10767
10768 #[test]
10769 fn bootstrap_sidecar_rejects_conflicting_manifest_bootstrap_fields() {
10770 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10771 let mut conflicting = archive.bootstrap_sidecar.clone();
10772 mutate_sidecar_manifest(&mut conflicting, &master_key(), |footer| {
10773 footer.index_root_first_block += 1;
10774 });
10775
10776 assert_eq!(
10777 open_archive_with_bootstrap_sidecar(&archive.bytes, &conflicting, &master_key())
10778 .unwrap_err(),
10779 FormatError::InvalidArchive("bootstrap sidecar conflicts with terminal ManifestFooter")
10780 );
10781 }
10782
10783 #[test]
10784 fn sidecar_size_cap_counts_only_present_sparse_sections() {
10785 let mut crypto_header = test_crypto_header();
10786 crypto_header.has_dictionary = 1;
10787 crypto_header.index_root_fec_data_shards = 1;
10788 crypto_header.index_root_fec_parity_shards = 0;
10789 let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
10790 let header = BootstrapSidecarHeader {
10791 archive_uuid: [0x31; 16],
10792 session_id: [0x42; 16],
10793 flags: 0x04,
10794 manifest_footer_offset: 0,
10795 manifest_footer_length: 0,
10796 index_root_records_offset: 0,
10797 index_root_records_length: 0,
10798 dictionary_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
10799 dictionary_records_length: record_len,
10800 sidecar_hmac: [0u8; 32],
10801 header_crc32c: 0,
10802 };
10803
10804 validate_sidecar_size_cap(
10805 &header,
10806 &crypto_header,
10807 BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len,
10808 )
10809 .unwrap();
10810 assert_eq!(
10811 validate_sidecar_size_cap(
10812 &header,
10813 &crypto_header,
10814 BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len + 1,
10815 )
10816 .unwrap_err(),
10817 FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
10818 );
10819 }
10820
10821 #[test]
10822 fn sidecar_size_cap_rejects_sparse_section_above_class_max() {
10823 let mut crypto_header = test_crypto_header();
10824 crypto_header.index_root_fec_data_shards = 1;
10825 crypto_header.index_root_fec_parity_shards = 0;
10826 let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
10827 let header = BootstrapSidecarHeader {
10828 archive_uuid: [0x31; 16],
10829 session_id: [0x42; 16],
10830 flags: 0x02,
10831 manifest_footer_offset: 0,
10832 manifest_footer_length: 0,
10833 index_root_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
10834 index_root_records_length: record_len * 2,
10835 dictionary_records_offset: 0,
10836 dictionary_records_length: 0,
10837 sidecar_hmac: [0u8; 32],
10838 header_crc32c: 0,
10839 };
10840
10841 assert_eq!(
10842 validate_sidecar_size_cap(
10843 &header,
10844 &crypto_header,
10845 BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len * 2,
10846 )
10847 .unwrap_err(),
10848 FormatError::InvalidArchive("bootstrap sidecar IndexRoot records exceed resource cap")
10849 );
10850 }
10851
10852 #[test]
10853 fn sidecar_size_cap_uses_wide_arithmetic_for_large_record_classes() {
10854 let mut crypto_header = test_crypto_header();
10855 crypto_header.block_size = u32::MAX;
10856 crypto_header.index_root_fec_data_shards = u16::MAX;
10857 crypto_header.index_root_fec_parity_shards = u16::MAX;
10858 let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
10859 let max_records = crypto_header.index_root_fec_data_shards as u64
10860 + crypto_header.index_root_fec_parity_shards as u64;
10861 let max_section_len = max_records * record_len;
10862 let cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64
10863 + MANIFEST_FOOTER_LEN as u64
10864 + max_section_len
10865 + max_section_len;
10866 let header = BootstrapSidecarHeader {
10867 archive_uuid: [0x31; 16],
10868 session_id: [0x42; 16],
10869 flags: 0x01 | 0x02 | 0x04,
10870 manifest_footer_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
10871 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
10872 index_root_records_offset: 0,
10873 index_root_records_length: max_section_len,
10874 dictionary_records_offset: 0,
10875 dictionary_records_length: max_section_len,
10876 sidecar_hmac: [0u8; 32],
10877 header_crc32c: 0,
10878 };
10879
10880 validate_sidecar_size_cap(&header, &crypto_header, cap).unwrap();
10881 assert_eq!(
10882 validate_sidecar_size_cap(&header, &crypto_header, cap + 1).unwrap_err(),
10883 FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
10884 );
10885 }
10886
10887 #[test]
10888 fn bootstrap_sidecar_rejects_dictionary_section_for_no_dictionary_archive() {
10889 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10890 let mut with_dictionary = archive.bootstrap_sidecar.clone();
10891 let header =
10892 BootstrapSidecarHeader::parse(&with_dictionary[..BOOTSTRAP_SIDECAR_HEADER_LEN])
10893 .unwrap();
10894 let record_len = sidecar_record_len(&with_dictionary);
10895 let first_record = header.index_root_records_offset as usize;
10896 let copied_record = with_dictionary[first_record..first_record + record_len].to_vec();
10897 let dictionary_offset = with_dictionary.len() as u64;
10898 with_dictionary.extend_from_slice(&copied_record);
10899 rewrite_sidecar_header(&mut with_dictionary, &master_key(), |header| {
10900 header.flags |= 0x04;
10901 header.dictionary_records_offset = dictionary_offset;
10902 header.dictionary_records_length = record_len as u64;
10903 });
10904
10905 assert_eq!(
10906 open_archive_with_bootstrap_sidecar(&archive.bytes, &with_dictionary, &master_key())
10907 .unwrap_err(),
10908 FormatError::InvalidArchive(
10909 "bootstrap sidecar has dictionary records while has_dictionary is false"
10910 )
10911 );
10912 }
10913
10914 #[test]
10915 fn bootstrap_sidecar_rejects_missing_duplicate_wrong_kind_and_wrong_last_flag() {
10916 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10917 let mut missing = archive.bootstrap_sidecar.clone();
10918 let record_len = sidecar_record_len(&missing);
10919 let new_len = missing.len() - record_len;
10920 missing.truncate(new_len);
10921 rewrite_sidecar_header(&mut missing, &master_key(), |header| {
10922 header.index_root_records_length -= record_len as u64;
10923 });
10924 assert_eq!(
10925 open_archive_with_bootstrap_sidecar(&archive.bytes, &missing, &master_key())
10926 .unwrap_err(),
10927 FormatError::InvalidArchive(
10928 "sidecar BlockRecord section does not match declared extent"
10929 )
10930 );
10931
10932 let mut duplicate = archive.bootstrap_sidecar.clone();
10933 mutate_sidecar_index_record(&mut duplicate, 1, |record| {
10934 record.block_index -= 1;
10935 });
10936 assert_eq!(
10937 open_archive_with_bootstrap_sidecar(&archive.bytes, &duplicate, &master_key())
10938 .unwrap_err(),
10939 FormatError::InvalidArchive(
10940 "sidecar BlockRecord section has missing or duplicate blocks"
10941 )
10942 );
10943
10944 let mut misordered = archive.bootstrap_sidecar.clone();
10945 swap_sidecar_index_records(&mut misordered, 0, 1);
10946 assert_eq!(
10947 open_archive_with_bootstrap_sidecar(&archive.bytes, &misordered, &master_key())
10948 .unwrap_err(),
10949 FormatError::InvalidArchive(
10950 "sidecar BlockRecord section has missing or duplicate blocks"
10951 )
10952 );
10953
10954 let mut wrong_kind = archive.bootstrap_sidecar.clone();
10955 mutate_sidecar_index_record(&mut wrong_kind, 0, |record| {
10956 record.kind = BlockKind::PayloadData;
10957 });
10958 assert_eq!(
10959 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
10960 .unwrap_err(),
10961 FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
10962 );
10963
10964 let mut wrong_last = archive.bootstrap_sidecar.clone();
10965 mutate_sidecar_index_record(&mut wrong_last, 0, |record| {
10966 record.flags = 0;
10967 });
10968 assert_eq!(
10969 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
10970 .unwrap_err(),
10971 FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
10972 );
10973 }
10974
10975 #[test]
10976 fn verify_helper_rejects_envelope_frame_coverage_gap() {
10977 let frames = BTreeMap::from([(
10978 0,
10979 FrameEntry {
10980 frame_index: 0,
10981 envelope_index: 0,
10982 offset_in_envelope: 0,
10983 compressed_size: 10,
10984 decompressed_size: 512,
10985 flags: 0,
10986 tar_stream_offset: 0,
10987 },
10988 )]);
10989 let envelopes = BTreeMap::from([(
10990 0,
10991 EnvelopeEntry {
10992 envelope_index: 0,
10993 first_block_index: 0,
10994 data_block_count: 1,
10995 parity_block_count: 1,
10996 encrypted_size: 4096,
10997 plaintext_size: 11,
10998 first_frame_index: 0,
10999 frame_count: 1,
11000 },
11001 )]);
11002
11003 assert_eq!(
11004 validate_envelope_frame_coverage(&frames, &envelopes).unwrap_err(),
11005 FormatError::InvalidArchive("EnvelopeEntry frame coverage has a gap or overlap")
11006 );
11007 }
11008
11009 #[test]
11010 fn verify_helper_rejects_file_extent_gaps_and_overlaps() {
11011 assert!(validate_file_extent_coverage_ranges(&[(512, 512), (0, 512)], 1024).is_ok());
11012 assert_eq!(
11013 validate_file_extent_coverage_ranges(&[(0, 512), (1024, 512)], 1536).unwrap_err(),
11014 FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
11015 );
11016 assert_eq!(
11017 validate_file_extent_coverage_ranges(&[(0, 1024), (512, 512)], 1024).unwrap_err(),
11018 FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
11019 );
11020 }
11021
11022 #[test]
11023 fn verify_rejects_authenticated_content_hash_mismatch() {
11024 let options = WriterOptions {
11025 index_root_fec_parity_shards: 0,
11026 ..single_stream_options()
11027 };
11028 let archive = write_archive(
11029 &[RegularFile::new("content-hash.txt", b"hash covered")],
11030 &master_key(),
11031 options,
11032 )
11033 .unwrap();
11034 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11035
11036 let mut root = opened.index_root.clone();
11037 root.header.content_sha256 = [0xa5; 32];
11038 let root_plaintext = root.to_bytes();
11039 IndexRoot::parse(
11040 &root_plaintext,
11041 false,
11042 metadata_limits(&opened.crypto_header),
11043 )
11044 .unwrap();
11045 assert_eq!(
11046 root_plaintext.len() as u32,
11047 opened.manifest_footer.index_root_decompressed_size
11048 );
11049
11050 let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
11051 let mut next_block_index = opened.manifest_footer.index_root_first_block;
11052 let replacement = encrypt_test_object(
11053 &compressed_root,
11054 &opened.subkeys.index_root_key,
11055 &opened.subkeys.index_nonce_seed,
11056 b"idxroot",
11057 0,
11058 BlockKind::IndexRootData,
11059 &mut next_block_index,
11060 &opened.crypto_header,
11061 &opened.volume_header,
11062 );
11063 assert_eq!(
11064 replacement.extent.data_block_count,
11065 opened.manifest_footer.index_root_data_block_count
11066 );
11067 assert_eq!(
11068 replacement.extent.encrypted_size,
11069 opened.manifest_footer.index_root_encrypted_size
11070 );
11071
11072 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11073 let crypto_end = volume_header.crypto_header_offset as usize
11074 + volume_header.crypto_header_length as usize;
11075 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
11076 let mut malformed = archive.bytes.clone();
11077 for record in replacement.records {
11078 let offset = crypto_end + record.block_index as usize * record_len;
11079 malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
11080 }
11081
11082 let reopened = open_archive(&malformed, &master_key()).unwrap();
11083 assert_eq!(
11084 reopened.verify().unwrap_err(),
11085 FormatError::InvalidArchive(
11086 "IndexRoot content_sha256 does not match decoded tar stream"
11087 )
11088 );
11089 }
11090
11091 #[test]
11092 fn verify_rejects_file_entry_tar_path_and_size_mismatches() {
11093 let (mut path_mismatch, _) = multi_envelope_reader_fixture();
11094 rewrite_as_single_healthy_file(&mut path_mismatch, |_file, path| {
11095 path[0] = b'x';
11096 });
11097 assert_eq!(
11098 path_mismatch.verify().unwrap_err(),
11099 FormatError::InvalidArchive("tar member path does not match FileEntry path")
11100 );
11101
11102 let (mut size_mismatch, _) = multi_envelope_reader_fixture();
11103 rewrite_as_single_healthy_file(&mut size_mismatch, |file, _path| {
11104 file.file_data_size += 1;
11105 });
11106 assert_eq!(
11107 size_mismatch.verify().unwrap_err(),
11108 FormatError::InvalidArchive("tar member size does not match FileEntry file_data_size")
11109 );
11110 }
11111
11112 #[test]
11113 fn verify_rejects_inconsistent_duplicate_local_frame_rows_across_shards() {
11114 let (mut opened, _) = multi_envelope_reader_fixture();
11115 let locating = opened.index_root.shards[0].clone();
11116 let mut duplicate = opened.load_index_shard(&locating).unwrap();
11117 duplicate.header.shard_index = 1;
11118 duplicate.frames[0].flags ^= 0x0000_0001;
11119 let duplicate_plaintext = duplicate.to_bytes();
11120 let mut next_block_index = opened
11121 .blocks
11122 .keys()
11123 .last()
11124 .copied()
11125 .map(|index| index + 1)
11126 .unwrap_or(0);
11127 let duplicate_object = encrypt_test_object(
11128 &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
11129 &opened.subkeys.index_shard_key,
11130 &opened.subkeys.index_nonce_seed,
11131 b"idxshard",
11132 1,
11133 BlockKind::IndexShardData,
11134 &mut next_block_index,
11135 &opened.crypto_header,
11136 &opened.volume_header,
11137 );
11138 insert_records(&mut opened.blocks, &duplicate_object.records);
11139 opened.index_root.shards.push(ShardEntry {
11140 shard_index: 1,
11141 first_block_index: duplicate_object.extent.first_block_index,
11142 data_block_count: duplicate_object.extent.data_block_count,
11143 parity_block_count: 0,
11144 encrypted_size: duplicate_object.extent.encrypted_size,
11145 decompressed_size: duplicate_plaintext.len() as u32,
11146 file_count: locating.file_count,
11147 first_path_hash: locating.first_path_hash,
11148 last_path_hash: locating.last_path_hash,
11149 });
11150 opened.index_root.header.file_count += locating.file_count as u64;
11151
11152 assert_eq!(
11153 opened.verify().unwrap_err(),
11154 FormatError::InvalidArchive("duplicate FrameEntry rows do not match")
11155 );
11156 }
11157
11158 #[test]
11159 fn verify_rejects_inconsistent_duplicate_local_envelope_rows_across_shards() {
11160 let (mut opened, _) = multi_envelope_reader_fixture();
11161 let locating = opened.index_root.shards[0].clone();
11162 let mut duplicate = opened.load_index_shard(&locating).unwrap();
11163 duplicate.header.shard_index = 1;
11164 duplicate.envelopes[0].first_block_index += 1;
11165 let duplicate_plaintext = duplicate.to_bytes();
11166 let mut next_block_index = opened
11167 .blocks
11168 .keys()
11169 .last()
11170 .copied()
11171 .map(|index| index + 1)
11172 .unwrap_or(0);
11173 let duplicate_object = encrypt_test_object(
11174 &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
11175 &opened.subkeys.index_shard_key,
11176 &opened.subkeys.index_nonce_seed,
11177 b"idxshard",
11178 1,
11179 BlockKind::IndexShardData,
11180 &mut next_block_index,
11181 &opened.crypto_header,
11182 &opened.volume_header,
11183 );
11184 insert_records(&mut opened.blocks, &duplicate_object.records);
11185 opened.index_root.shards.push(ShardEntry {
11186 shard_index: 1,
11187 first_block_index: duplicate_object.extent.first_block_index,
11188 data_block_count: duplicate_object.extent.data_block_count,
11189 parity_block_count: 0,
11190 encrypted_size: duplicate_object.extent.encrypted_size,
11191 decompressed_size: duplicate_plaintext.len() as u32,
11192 file_count: locating.file_count,
11193 first_path_hash: locating.first_path_hash,
11194 last_path_hash: locating.last_path_hash,
11195 });
11196 opened.index_root.header.file_count += locating.file_count as u64;
11197
11198 assert_eq!(
11199 opened.verify().unwrap_err(),
11200 FormatError::InvalidArchive("duplicate EnvelopeEntry rows do not match")
11201 );
11202 }
11203
11204 #[test]
11205 fn verify_rejects_non_contiguous_global_envelope_indexes() {
11206 let (mut opened, _) = multi_envelope_reader_fixture();
11207 replace_first_index_shard(&mut opened, |shard| {
11208 let frame = shard
11209 .frames
11210 .iter_mut()
11211 .find(|entry| entry.frame_index == 1)
11212 .unwrap();
11213 frame.envelope_index = 2;
11214
11215 let envelope = shard
11216 .envelopes
11217 .iter_mut()
11218 .find(|entry| entry.envelope_index == 1)
11219 .unwrap();
11220 envelope.envelope_index = 2;
11221 });
11222
11223 assert_eq!(
11224 opened.verify().unwrap_err(),
11225 FormatError::InvalidMetadata {
11226 structure: "EnvelopeEntry",
11227 reason: "global index coverage has a gap",
11228 }
11229 );
11230 }
11231
11232 #[test]
11233 fn verify_rejects_payload_object_extent_overlap() {
11234 let (mut opened, _) = multi_envelope_reader_fixture();
11235 replace_first_index_shard(&mut opened, |shard| {
11236 let first_block_index = shard.envelopes[0].first_block_index;
11237 shard.envelopes[1].first_block_index = first_block_index;
11238 });
11239
11240 assert_eq!(
11241 opened.verify().unwrap_err(),
11242 FormatError::InvalidArchive("encrypted object block ranges overlap")
11243 );
11244 }
11245
11246 #[test]
11247 fn verify_accepts_cross_shard_shared_envelope_frame_union() {
11248 let volume_header = test_volume_header();
11249 let crypto_header = test_crypto_header();
11250 let subkeys = Subkeys::derive(
11251 &master_key(),
11252 &volume_header.archive_uuid,
11253 &volume_header.session_id,
11254 )
11255 .unwrap();
11256 let mut next_block_index = 0u64;
11257 let mut blocks = BTreeMap::new();
11258
11259 let alpha = test_member(b"alpha.txt", b"alpha cross shard\n");
11260 let beta = test_member(b"beta.txt", b"beta cross shard\n");
11261 let tar_stream = [alpha.as_slice(), beta.as_slice()].concat();
11262 let frame0_plaintext = compress_zstd_frame(&alpha, 1).unwrap();
11263 let frame1_plaintext = compress_zstd_frame(&beta, 1).unwrap();
11264 let envelope_plaintext =
11265 [frame0_plaintext.as_slice(), frame1_plaintext.as_slice()].concat();
11266 let payload = encrypt_test_object(
11267 &envelope_plaintext,
11268 &subkeys.enc_key,
11269 &subkeys.nonce_seed,
11270 b"envelope",
11271 0,
11272 BlockKind::PayloadData,
11273 &mut next_block_index,
11274 &crypto_header,
11275 &volume_header,
11276 );
11277 insert_records(&mut blocks, &payload.records);
11278
11279 let envelope = EnvelopeEntry {
11280 envelope_index: 0,
11281 first_block_index: payload.extent.first_block_index,
11282 data_block_count: payload.extent.data_block_count,
11283 parity_block_count: 0,
11284 encrypted_size: payload.extent.encrypted_size,
11285 plaintext_size: envelope_plaintext.len() as u32,
11286 first_frame_index: 0,
11287 frame_count: 2,
11288 };
11289 let frame0 = FrameEntry {
11290 frame_index: 0,
11291 envelope_index: 0,
11292 offset_in_envelope: 0,
11293 compressed_size: frame0_plaintext.len() as u32,
11294 decompressed_size: alpha.len() as u32,
11295 flags: 0x0000_0003,
11296 tar_stream_offset: 0,
11297 };
11298 let frame1 = FrameEntry {
11299 frame_index: 1,
11300 envelope_index: 0,
11301 offset_in_envelope: frame0_plaintext.len() as u32,
11302 compressed_size: frame1_plaintext.len() as u32,
11303 decompressed_size: beta.len() as u32,
11304 flags: 0x0000_0003,
11305 tar_stream_offset: alpha.len() as u64,
11306 };
11307
11308 let (shard0_plaintext, first0, last0) = build_test_index_shard(
11309 &[TestFileMeta {
11310 path: b"alpha.txt".to_vec(),
11311 frame_index: 0,
11312 tar_stream_offset: 0,
11313 member_group_size: alpha.len() as u64,
11314 file_data_size: b"alpha cross shard\n".len() as u64,
11315 }],
11316 &[frame0],
11317 std::slice::from_ref(&envelope),
11318 );
11319 let (mut shard1_plaintext, first1, last1) = build_test_index_shard(
11320 &[TestFileMeta {
11321 path: b"beta.txt".to_vec(),
11322 frame_index: 1,
11323 tar_stream_offset: alpha.len() as u64,
11324 member_group_size: beta.len() as u64,
11325 file_data_size: b"beta cross shard\n".len() as u64,
11326 }],
11327 &[frame1],
11328 std::slice::from_ref(&envelope),
11329 );
11330 shard1_plaintext[8..16].copy_from_slice(&1u64.to_le_bytes());
11331
11332 let shard0 = encrypt_test_object(
11333 &compress_zstd_frame(&shard0_plaintext, 1).unwrap(),
11334 &subkeys.index_shard_key,
11335 &subkeys.index_nonce_seed,
11336 b"idxshard",
11337 0,
11338 BlockKind::IndexShardData,
11339 &mut next_block_index,
11340 &crypto_header,
11341 &volume_header,
11342 );
11343 let shard1 = encrypt_test_object(
11344 &compress_zstd_frame(&shard1_plaintext, 1).unwrap(),
11345 &subkeys.index_shard_key,
11346 &subkeys.index_nonce_seed,
11347 b"idxshard",
11348 1,
11349 BlockKind::IndexShardData,
11350 &mut next_block_index,
11351 &crypto_header,
11352 &volume_header,
11353 );
11354 insert_records(&mut blocks, &shard0.records);
11355 insert_records(&mut blocks, &shard1.records);
11356
11357 let index_root = IndexRoot {
11358 header: IndexRootHeader {
11359 frame_count: 2,
11360 envelope_count: 1,
11361 file_count: 2,
11362 payload_block_count: payload.extent.data_block_count as u64,
11363 tar_total_size: tar_stream.len() as u64,
11364 content_sha256: sha256_bytes(&tar_stream),
11365 ..IndexRootHeader::empty()
11366 },
11367 shards: vec![
11368 ShardEntry {
11369 shard_index: 0,
11370 first_block_index: shard0.extent.first_block_index,
11371 data_block_count: shard0.extent.data_block_count,
11372 parity_block_count: 0,
11373 encrypted_size: shard0.extent.encrypted_size,
11374 decompressed_size: shard0_plaintext.len() as u32,
11375 file_count: 1,
11376 first_path_hash: first0,
11377 last_path_hash: last0,
11378 },
11379 ShardEntry {
11380 shard_index: 1,
11381 first_block_index: shard1.extent.first_block_index,
11382 data_block_count: shard1.extent.data_block_count,
11383 parity_block_count: 0,
11384 encrypted_size: shard1.extent.encrypted_size,
11385 decompressed_size: shard1_plaintext.len() as u32,
11386 file_count: 1,
11387 first_path_hash: first1,
11388 last_path_hash: last1,
11389 },
11390 ],
11391 directory_hint_shards: Vec::new(),
11392 };
11393
11394 let index_root_plaintext = index_root.to_bytes();
11395 let index_root_object = encrypt_test_object(
11396 &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
11397 &subkeys.index_root_key,
11398 &subkeys.index_nonce_seed,
11399 b"idxroot",
11400 0,
11401 BlockKind::IndexRootData,
11402 &mut next_block_index,
11403 &crypto_header,
11404 &volume_header,
11405 );
11406 insert_records(&mut blocks, &index_root_object.records);
11407
11408 let archive_uuid = volume_header.archive_uuid;
11409 let session_id = volume_header.session_id;
11410 let opened = OpenedArchive {
11411 options: ReaderOptions::default(),
11412 observed_archive_bytes: 1_000_000,
11413 observed_volume_count: 1,
11414 subkeys,
11415 blocks,
11416 lazy_blocks: None,
11417 crypto_header_bytes: Vec::new(),
11418 volume_header,
11419 crypto_header,
11420 manifest_footer: ManifestFooter {
11421 archive_uuid,
11422 session_id,
11423 volume_index: 0,
11424 is_authoritative: 1,
11425 total_volumes: 1,
11426 index_root_first_block: index_root_object.extent.first_block_index,
11427 index_root_data_block_count: index_root_object.extent.data_block_count,
11428 index_root_parity_block_count: 0,
11429 index_root_encrypted_size: index_root_object.extent.encrypted_size,
11430 index_root_decompressed_size: index_root_plaintext.len() as u32,
11431 manifest_hmac: [0u8; 32],
11432 },
11433 volume_trailer: Some(VolumeTrailer {
11434 archive_uuid,
11435 session_id,
11436 volume_index: 0,
11437 block_count: next_block_index,
11438 bytes_written: 0,
11439 manifest_footer_offset: 0,
11440 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
11441 closed_at_ns: 0,
11442 root_auth_footer_offset: 0,
11443 root_auth_footer_length: 0,
11444 root_auth_flags: 0,
11445 trailer_hmac: [0u8; 32],
11446 }),
11447 root_auth_footer: None,
11448 index_root,
11449 payload_dictionary: None,
11450 };
11451
11452 opened.verify().unwrap();
11453 }
11454
11455 #[test]
11456 fn verify_rejects_authenticated_archive_missing_required_directory_hints() {
11457 let options = WriterOptions {
11458 index_root_fec_parity_shards: 0,
11459 ..single_stream_options()
11460 };
11461 let archive = write_archive(
11462 &[RegularFile::new("only.txt", b"only payload")],
11463 &master_key(),
11464 options,
11465 )
11466 .unwrap();
11467 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11468 assert!(opened.index_root.directory_hint_shards.is_empty());
11469
11470 let mut root = opened.index_root.clone();
11471 root.header.file_count = DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1;
11472 root.shards[0].file_count = (DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1) as u32;
11473 let root_plaintext = root.to_bytes();
11474 IndexRoot::parse(
11475 &root_plaintext,
11476 false,
11477 metadata_limits(&opened.crypto_header),
11478 )
11479 .unwrap();
11480 assert_eq!(
11481 root_plaintext.len() as u32,
11482 opened.manifest_footer.index_root_decompressed_size
11483 );
11484
11485 let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
11486 let mut next_block_index = opened.manifest_footer.index_root_first_block;
11487 let replacement = encrypt_test_object(
11488 &compressed_root,
11489 &opened.subkeys.index_root_key,
11490 &opened.subkeys.index_nonce_seed,
11491 b"idxroot",
11492 0,
11493 BlockKind::IndexRootData,
11494 &mut next_block_index,
11495 &opened.crypto_header,
11496 &opened.volume_header,
11497 );
11498 assert_eq!(
11499 replacement.extent.first_block_index,
11500 opened.manifest_footer.index_root_first_block
11501 );
11502 assert_eq!(
11503 replacement.extent.data_block_count,
11504 opened.manifest_footer.index_root_data_block_count
11505 );
11506 assert_eq!(
11507 replacement.extent.encrypted_size,
11508 opened.manifest_footer.index_root_encrypted_size
11509 );
11510
11511 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11512 let crypto_end = volume_header.crypto_header_offset as usize
11513 + volume_header.crypto_header_length as usize;
11514 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
11515 let mut malformed = archive.bytes.clone();
11516 for record in replacement.records {
11517 let offset = crypto_end + record.block_index as usize * record_len;
11518 malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
11519 }
11520
11521 let reopened = open_archive(&malformed, &master_key()).unwrap();
11522 assert_eq!(
11523 reopened.index_root.header.file_count,
11524 DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1
11525 );
11526 assert!(reopened.index_root.directory_hint_shards.is_empty());
11527
11528 assert_eq!(
11529 reopened.verify().unwrap_err(),
11530 FormatError::InvalidArchive("IndexRoot file_count requires directory hints")
11531 );
11532 }
11533
11534 #[test]
11535 fn expected_directory_hint_rows_include_ancestors_and_directory_entries() {
11536 let mut map = DirectoryHintMap::new();
11537 add_expected_directory_hint_rows(&mut map, 2, b"foo/bar/baz.txt", TarEntryKind::Regular);
11538 add_expected_directory_hint_rows(&mut map, 4, b"foo/bar", TarEntryKind::Directory);
11539
11540 assert_eq!(map.get(&Vec::new()), Some(&BTreeSet::from([2, 4])));
11541 assert_eq!(map.get(b"foo".as_slice()), Some(&BTreeSet::from([2, 4])));
11542 assert_eq!(
11543 map.get(b"foo/bar".as_slice()),
11544 Some(&BTreeSet::from([2, 4]))
11545 );
11546 assert!(!map.contains_key(b"foo/bar/baz.txt".as_slice()));
11547 assert!(!map.contains_key(b"foobar".as_slice()));
11548 }
11549
11550 #[test]
11551 fn directory_hint_validation_requires_exact_global_map() {
11552 let mut expected = DirectoryHintMap::new();
11553 add_expected_directory_hint_rows(&mut expected, 0, b"foo/bar.txt", TarEntryKind::Regular);
11554 add_expected_directory_hint_rows(&mut expected, 1, b"foo", TarEntryKind::Directory);
11555 let rows = sorted_directory_hint_rows(&expected);
11556 let table = directory_hint_table_from_rows(7, &rows, 2);
11557
11558 validate_directory_hint_tables_against_expected(std::slice::from_ref(&table), &expected)
11559 .unwrap();
11560
11561 let mut missing_root = expected.clone();
11562 missing_root.remove(&Vec::new());
11563 let missing_root_rows = sorted_directory_hint_rows(&missing_root);
11564 let missing_root_table = directory_hint_table_from_rows(8, &missing_root_rows, 2);
11565 assert_eq!(
11566 validate_directory_hint_tables_against_expected(&[missing_root_table], &expected)
11567 .unwrap_err(),
11568 FormatError::InvalidArchive("directory hint map does not match decoded files")
11569 );
11570
11571 let mut expected_missing_directory_entry = expected.clone();
11572 expected_missing_directory_entry
11573 .get_mut(b"foo".as_slice())
11574 .unwrap()
11575 .remove(&1);
11576 assert_eq!(
11577 validate_directory_hint_tables_against_expected(
11578 std::slice::from_ref(&table),
11579 &expected_missing_directory_entry,
11580 )
11581 .unwrap_err(),
11582 FormatError::InvalidArchive("directory hint map does not match decoded files")
11583 );
11584
11585 let mut extra = expected.clone();
11586 extra.insert(b"foo/extra".to_vec(), BTreeSet::from([0]));
11587 let extra_rows = sorted_directory_hint_rows(&extra);
11588 let extra_table = directory_hint_table_from_rows(9, &extra_rows, 2);
11589 assert_eq!(
11590 validate_directory_hint_tables_against_expected(&[extra_table], &expected).unwrap_err(),
11591 FormatError::InvalidArchive("directory hint map does not match decoded files")
11592 );
11593 }
11594
11595 #[test]
11596 fn directory_hint_validation_rejects_global_order_mismatch() {
11597 let mut expected = DirectoryHintMap::new();
11598 expected.insert(Vec::new(), BTreeSet::from([0]));
11599 expected.insert(b"alpha".to_vec(), BTreeSet::from([0]));
11600 let rows = sorted_directory_hint_rows(&expected);
11601 let first = directory_hint_table_from_rows(8, &rows[..1], 1);
11602 let second = directory_hint_table_from_rows(9, &rows[1..], 1);
11603
11604 assert_eq!(
11605 validate_directory_hint_tables_against_expected(&[second, first], &expected)
11606 .unwrap_err(),
11607 FormatError::InvalidArchive("DirectoryHintEntry rows are not globally sorted")
11608 );
11609 }
11610
11611 #[test]
11612 fn object_extent_rejects_parity_above_class_cap() {
11613 let crypto_header = CryptoHeaderFixed {
11614 length: 0,
11615 compression_algo: CompressionAlgo::ZstdFramed,
11616 aead_algo: AeadAlgo::AesGcmSiv256,
11617 fec_algo: FecAlgo::ReedSolomonGF16,
11618 kdf_algo: KdfAlgo::Raw,
11619 chunk_size: 1024,
11620 envelope_target_size: 4096,
11621 block_size: 4096,
11622 fec_data_shards: 1,
11623 fec_parity_shards: 1,
11624 index_fec_data_shards: 1,
11625 index_fec_parity_shards: 1,
11626 index_root_fec_data_shards: 1,
11627 index_root_fec_parity_shards: 1,
11628 stripe_width: 1,
11629 volume_loss_tolerance: 0,
11630 bit_rot_buffer_pct: 0,
11631 has_dictionary: 0,
11632 max_path_length: 4096,
11633 expected_volume_size: 0,
11634 };
11635 let extent = ObjectExtent {
11636 first_block_index: 0,
11637 data_block_count: 1,
11638 parity_block_count: 2,
11639 encrypted_size: 4096,
11640 };
11641
11642 assert_eq!(
11643 validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
11644 FormatError::InvalidArchive("encrypted object exceeds its class parity-shard maximum")
11645 );
11646 }
11647
11648 #[test]
11649 fn object_extent_rejects_parity_below_recoverability_requirement() {
11650 let crypto_header = CryptoHeaderFixed {
11651 length: 0,
11652 compression_algo: CompressionAlgo::ZstdFramed,
11653 aead_algo: AeadAlgo::AesGcmSiv256,
11654 fec_algo: FecAlgo::ReedSolomonGF16,
11655 kdf_algo: KdfAlgo::Raw,
11656 chunk_size: 1024,
11657 envelope_target_size: 4096,
11658 block_size: 4096,
11659 fec_data_shards: 1,
11660 fec_parity_shards: 1,
11661 index_fec_data_shards: 1,
11662 index_fec_parity_shards: 1,
11663 index_root_fec_data_shards: 1,
11664 index_root_fec_parity_shards: 1,
11665 stripe_width: 2,
11666 volume_loss_tolerance: 1,
11667 bit_rot_buffer_pct: 0,
11668 has_dictionary: 0,
11669 max_path_length: 4096,
11670 expected_volume_size: 0,
11671 };
11672 let extent = ObjectExtent {
11673 first_block_index: 0,
11674 data_block_count: 1,
11675 parity_block_count: 0,
11676 encrypted_size: 4096,
11677 };
11678
11679 assert_eq!(
11680 validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
11681 FormatError::InvalidArchive(
11682 "encrypted object parity does not match v41 compute_parity"
11683 )
11684 );
11685 }
11686
11687 #[test]
11688 fn encrypted_object_extent_matrix_rejects_overlaps() {
11689 let (opened, _) = multi_envelope_reader_fixture();
11690 let loaded_shard = opened
11691 .load_index_shard(&opened.index_root.shards[0])
11692 .unwrap();
11693 let base_envelopes = loaded_shard
11694 .envelopes
11695 .iter()
11696 .map(|entry| (entry.envelope_index, entry.clone()))
11697 .collect::<BTreeMap<_, _>>();
11698 let payload_start = loaded_shard.envelopes[0].first_block_index;
11699 let overlap = FormatError::InvalidArchive("encrypted object block ranges overlap");
11700
11701 let mut payload_overlap = base_envelopes.clone();
11702 payload_overlap
11703 .get_mut(&loaded_shard.envelopes[1].envelope_index)
11704 .unwrap()
11705 .first_block_index = payload_start;
11706 assert_eq!(
11707 opened
11708 .validate_encrypted_object_block_ranges(&payload_overlap)
11709 .unwrap_err(),
11710 overlap
11711 );
11712
11713 let mut shard_overlap = opened.clone();
11714 let shard = shard_overlap.index_root.shards[0].clone();
11715 shard_overlap.index_root.shards.push(ShardEntry {
11716 shard_index: 1,
11717 ..shard
11718 });
11719 assert_eq!(
11720 shard_overlap
11721 .validate_encrypted_object_block_ranges(&base_envelopes)
11722 .unwrap_err(),
11723 overlap
11724 );
11725
11726 let mut dictionary_overlap = opened.clone();
11727 dictionary_overlap.crypto_header.has_dictionary = 1;
11728 dictionary_overlap.index_root.header.dictionary_first_block = payload_start;
11729 dictionary_overlap
11730 .index_root
11731 .header
11732 .dictionary_data_block_count = 1;
11733 dictionary_overlap
11734 .index_root
11735 .header
11736 .dictionary_parity_block_count = 0;
11737 dictionary_overlap
11738 .index_root
11739 .header
11740 .dictionary_encrypted_size = 4096;
11741 dictionary_overlap
11742 .index_root
11743 .header
11744 .dictionary_decompressed_size = 128;
11745 assert_eq!(
11746 dictionary_overlap
11747 .validate_encrypted_object_block_ranges(&base_envelopes)
11748 .unwrap_err(),
11749 overlap
11750 );
11751
11752 let mut hint_overlap = opened.clone();
11753 hint_overlap
11754 .index_root
11755 .directory_hint_shards
11756 .push(DirectoryHintShardEntry {
11757 hint_shard_index: 0,
11758 first_dir_hash: [0; 8],
11759 last_dir_hash: [0; 8],
11760 first_block_index: payload_start,
11761 data_block_count: 1,
11762 parity_block_count: 0,
11763 encrypted_size: 4096,
11764 decompressed_size: 128,
11765 entry_count: 1,
11766 });
11767 assert_eq!(
11768 hint_overlap
11769 .validate_encrypted_object_block_ranges(&base_envelopes)
11770 .unwrap_err(),
11771 overlap
11772 );
11773 }
11774
11775 #[test]
11776 fn load_metadata_object_rejects_per_object_zstd_frame_exactness_mutations() {
11777 let volume_header = test_volume_header();
11778 let crypto_header = test_crypto_header();
11779 let subkeys = Subkeys::derive(
11780 &master_key(),
11781 &volume_header.archive_uuid,
11782 &volume_header.session_id,
11783 )
11784 .unwrap();
11785 let mut next_block_index = 0u64;
11786
11787 let index_root_payload = b"index root metadata object";
11788 let index_root_compressed = compress_zstd_frame(index_root_payload, 1).unwrap();
11789 assert_metadata_object_from_compressed(
11790 &{
11791 let mut bytes = index_root_compressed.clone();
11792 bytes.push(0);
11793 bytes
11794 },
11795 index_root_payload.len(),
11796 &subkeys,
11797 &volume_header,
11798 &crypto_header,
11799 &subkeys.index_root_key,
11800 &subkeys.index_nonce_seed,
11801 b"idxroot",
11802 0,
11803 BlockKind::IndexRootData,
11804 BlockKind::IndexRootParity,
11805 crypto_header.index_root_fec_data_shards,
11806 crypto_header.index_root_fec_parity_shards,
11807 &mut next_block_index,
11808 FormatError::TrailingBytesAfterZstdFrame,
11809 );
11810 assert_metadata_object_from_compressed(
11811 &index_root_compressed,
11812 index_root_payload.len() + 1,
11813 &subkeys,
11814 &volume_header,
11815 &crypto_header,
11816 &subkeys.index_root_key,
11817 &subkeys.index_nonce_seed,
11818 b"idxroot",
11819 0,
11820 BlockKind::IndexRootData,
11821 BlockKind::IndexRootParity,
11822 crypto_header.index_root_fec_data_shards,
11823 crypto_header.index_root_fec_parity_shards,
11824 &mut next_block_index,
11825 FormatError::ZstdDecompressedSizeMismatch {
11826 expected: index_root_payload.len() + 1,
11827 actual: index_root_payload.len(),
11828 },
11829 );
11830
11831 let index_shard_payload = b"index shard metadata object";
11832 let index_shard_compressed = compress_zstd_frame(index_shard_payload, 1).unwrap();
11833 assert_metadata_object_from_compressed(
11834 &{
11835 let mut bytes = index_shard_compressed.clone();
11836 bytes.push(0);
11837 bytes
11838 },
11839 index_shard_payload.len(),
11840 &subkeys,
11841 &volume_header,
11842 &crypto_header,
11843 &subkeys.index_shard_key,
11844 &subkeys.index_nonce_seed,
11845 b"idxshard",
11846 1,
11847 BlockKind::IndexShardData,
11848 BlockKind::IndexShardParity,
11849 crypto_header.index_fec_data_shards,
11850 crypto_header.index_fec_parity_shards,
11851 &mut next_block_index,
11852 FormatError::TrailingBytesAfterZstdFrame,
11853 );
11854 assert_metadata_object_from_compressed(
11855 &index_shard_compressed,
11856 index_shard_payload.len() + 1,
11857 &subkeys,
11858 &volume_header,
11859 &crypto_header,
11860 &subkeys.index_shard_key,
11861 &subkeys.index_nonce_seed,
11862 b"idxshard",
11863 1,
11864 BlockKind::IndexShardData,
11865 BlockKind::IndexShardParity,
11866 crypto_header.index_fec_data_shards,
11867 crypto_header.index_fec_parity_shards,
11868 &mut next_block_index,
11869 FormatError::ZstdDecompressedSizeMismatch {
11870 expected: index_shard_payload.len() + 1,
11871 actual: index_shard_payload.len(),
11872 },
11873 );
11874
11875 let directory_hint_payload = b"directory hint metadata object";
11876 let directory_hint_compressed = compress_zstd_frame(directory_hint_payload, 1).unwrap();
11877 assert_metadata_object_from_compressed(
11878 &{
11879 let mut bytes = directory_hint_compressed.clone();
11880 bytes.push(0);
11881 bytes
11882 },
11883 directory_hint_payload.len(),
11884 &subkeys,
11885 &volume_header,
11886 &crypto_header,
11887 &subkeys.dir_hint_key,
11888 &subkeys.index_nonce_seed,
11889 b"dirhint",
11890 0,
11891 BlockKind::DirectoryHintData,
11892 BlockKind::DirectoryHintParity,
11893 crypto_header.index_fec_data_shards,
11894 crypto_header.index_fec_parity_shards,
11895 &mut next_block_index,
11896 FormatError::TrailingBytesAfterZstdFrame,
11897 );
11898 assert_metadata_object_from_compressed(
11899 &directory_hint_compressed,
11900 directory_hint_payload.len() + 1,
11901 &subkeys,
11902 &volume_header,
11903 &crypto_header,
11904 &subkeys.dir_hint_key,
11905 &subkeys.index_nonce_seed,
11906 b"dirhint",
11907 0,
11908 BlockKind::DirectoryHintData,
11909 BlockKind::DirectoryHintParity,
11910 crypto_header.index_fec_data_shards,
11911 crypto_header.index_fec_parity_shards,
11912 &mut next_block_index,
11913 FormatError::ZstdDecompressedSizeMismatch {
11914 expected: directory_hint_payload.len() + 1,
11915 actual: directory_hint_payload.len(),
11916 },
11917 );
11918
11919 let dictionary_payload = b"dictionary metadata object";
11920 let dictionary_compressed = compress_zstd_frame(dictionary_payload, 1).unwrap();
11921 assert_metadata_object_from_compressed(
11922 &{
11923 let mut bytes = dictionary_compressed.clone();
11924 bytes.push(0);
11925 bytes
11926 },
11927 dictionary_payload.len(),
11928 &subkeys,
11929 &volume_header,
11930 &crypto_header,
11931 &subkeys.dictionary_key,
11932 &subkeys.index_nonce_seed,
11933 b"dict",
11934 0,
11935 BlockKind::DictionaryData,
11936 BlockKind::DictionaryParity,
11937 crypto_header.index_root_fec_data_shards,
11938 crypto_header.index_root_fec_parity_shards,
11939 &mut next_block_index,
11940 FormatError::TrailingBytesAfterZstdFrame,
11941 );
11942 assert_metadata_object_from_compressed(
11943 &dictionary_compressed,
11944 dictionary_payload.len() + 1,
11945 &subkeys,
11946 &volume_header,
11947 &crypto_header,
11948 &subkeys.dictionary_key,
11949 &subkeys.index_nonce_seed,
11950 b"dict",
11951 0,
11952 BlockKind::DictionaryData,
11953 BlockKind::DictionaryParity,
11954 crypto_header.index_root_fec_data_shards,
11955 crypto_header.index_root_fec_parity_shards,
11956 &mut next_block_index,
11957 FormatError::ZstdDecompressedSizeMismatch {
11958 expected: dictionary_payload.len() + 1,
11959 actual: dictionary_payload.len(),
11960 },
11961 );
11962 }
11963
11964 #[test]
11965 fn load_metadata_object_extent_rejects_encrypted_size_not_data_block_count_times_block_size() {
11966 let volume_header = test_volume_header();
11967 let crypto_header = test_crypto_header();
11968 let subkeys = Subkeys::derive(
11969 &master_key(),
11970 &volume_header.archive_uuid,
11971 &volume_header.session_id,
11972 )
11973 .unwrap();
11974 let mut next_block_index = 0u64;
11975
11976 let index_root_payload = b"index root metadata object";
11977 let (index_root_extent, index_root_records) = build_metadata_object_from_payload(
11978 index_root_payload,
11979 &subkeys,
11980 &volume_header,
11981 &crypto_header,
11982 &subkeys.index_root_key,
11983 &subkeys.index_nonce_seed,
11984 b"idxroot",
11985 0,
11986 BlockKind::IndexRootData,
11987 &mut next_block_index,
11988 );
11989 let mut index_root_extent = index_root_extent;
11990 index_root_extent.encrypted_size = index_root_extent
11991 .encrypted_size
11992 .saturating_add(crypto_header.block_size);
11993 assert_eq!(
11994 load_metadata_object_from_parts(
11995 &index_root_records,
11996 ObjectLoadContext::index_root(
11997 &volume_header,
11998 &crypto_header,
11999 &subkeys,
12000 index_root_extent,
12001 ),
12002 index_root_payload.len() as u32,
12003 )
12004 .unwrap_err(),
12005 FormatError::InvalidArchive(
12006 "encrypted object size is not data_block_count * block_size"
12007 )
12008 );
12009
12010 let index_shard_payload = b"index shard metadata object";
12011 let (index_shard_extent, index_shard_records) = build_metadata_object_from_payload(
12012 index_shard_payload,
12013 &subkeys,
12014 &volume_header,
12015 &crypto_header,
12016 &subkeys.index_shard_key,
12017 &subkeys.index_nonce_seed,
12018 b"idxshard",
12019 1,
12020 BlockKind::IndexShardData,
12021 &mut next_block_index,
12022 );
12023 let mut index_shard_extent = index_shard_extent;
12024 index_shard_extent.encrypted_size = index_shard_extent
12025 .encrypted_size
12026 .saturating_add(crypto_header.block_size);
12027 assert_eq!(
12028 load_metadata_object_from_parts(
12029 &index_shard_records,
12030 ObjectLoadContext {
12031 volume_header: &volume_header,
12032 crypto_header: &crypto_header,
12033 extent: index_shard_extent,
12034 data_kind: BlockKind::IndexShardData,
12035 parity_kind: BlockKind::IndexShardParity,
12036 key: &subkeys.index_shard_key,
12037 nonce_seed: &subkeys.index_nonce_seed,
12038 domain: b"idxshard",
12039 counter: 1,
12040 class_data_shard_max: crypto_header.index_fec_data_shards,
12041 class_parity_shard_max: crypto_header.index_fec_parity_shards,
12042 },
12043 index_shard_payload.len() as u32,
12044 )
12045 .unwrap_err(),
12046 FormatError::InvalidArchive(
12047 "encrypted object size is not data_block_count * block_size"
12048 )
12049 );
12050
12051 let directory_hint_payload = b"directory hint metadata object";
12052 let (directory_hint_extent, directory_hint_records) = build_metadata_object_from_payload(
12053 directory_hint_payload,
12054 &subkeys,
12055 &volume_header,
12056 &crypto_header,
12057 &subkeys.dir_hint_key,
12058 &subkeys.index_nonce_seed,
12059 b"dirhint",
12060 0,
12061 BlockKind::DirectoryHintData,
12062 &mut next_block_index,
12063 );
12064 let mut directory_hint_extent = directory_hint_extent;
12065 directory_hint_extent.encrypted_size = directory_hint_extent
12066 .encrypted_size
12067 .saturating_add(crypto_header.block_size);
12068 assert_eq!(
12069 load_metadata_object_from_parts(
12070 &directory_hint_records,
12071 ObjectLoadContext {
12072 volume_header: &volume_header,
12073 crypto_header: &crypto_header,
12074 extent: directory_hint_extent,
12075 data_kind: BlockKind::DirectoryHintData,
12076 parity_kind: BlockKind::DirectoryHintParity,
12077 key: &subkeys.dir_hint_key,
12078 nonce_seed: &subkeys.index_nonce_seed,
12079 domain: b"dirhint",
12080 counter: 0,
12081 class_data_shard_max: crypto_header.index_fec_data_shards,
12082 class_parity_shard_max: crypto_header.index_fec_parity_shards,
12083 },
12084 directory_hint_payload.len() as u32,
12085 )
12086 .unwrap_err(),
12087 FormatError::InvalidArchive(
12088 "encrypted object size is not data_block_count * block_size"
12089 )
12090 );
12091
12092 let dictionary_payload = b"dictionary metadata object";
12093 let (dictionary_extent, dictionary_records) = build_metadata_object_from_payload(
12094 dictionary_payload,
12095 &subkeys,
12096 &volume_header,
12097 &crypto_header,
12098 &subkeys.dictionary_key,
12099 &subkeys.index_nonce_seed,
12100 b"dict",
12101 0,
12102 BlockKind::DictionaryData,
12103 &mut next_block_index,
12104 );
12105 let mut dictionary_extent = dictionary_extent;
12106 dictionary_extent.encrypted_size = dictionary_extent
12107 .encrypted_size
12108 .saturating_add(crypto_header.block_size);
12109 assert_eq!(
12110 load_metadata_object_from_parts(
12111 &dictionary_records,
12112 ObjectLoadContext {
12113 volume_header: &volume_header,
12114 crypto_header: &crypto_header,
12115 extent: dictionary_extent,
12116 data_kind: BlockKind::DictionaryData,
12117 parity_kind: BlockKind::DictionaryParity,
12118 key: &subkeys.dictionary_key,
12119 nonce_seed: &subkeys.index_nonce_seed,
12120 domain: b"dict",
12121 counter: 0,
12122 class_data_shard_max: crypto_header.index_root_fec_data_shards,
12123 class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
12124 },
12125 dictionary_payload.len() as u32,
12126 )
12127 .unwrap_err(),
12128 FormatError::InvalidArchive(
12129 "encrypted object size is not data_block_count * block_size"
12130 )
12131 );
12132 }
12133
12134 #[test]
12135 fn opens_complete_multi_volume_archive() {
12136 let files = [RegularFile::new("alpha.txt", b"hello from volume stripes")];
12137 let archive = write_archive(
12138 &files,
12139 &master_key(),
12140 WriterOptions {
12141 stripe_width: 2,
12142 volume_loss_tolerance: 1,
12143 ..single_stream_options()
12144 },
12145 )
12146 .unwrap();
12147 assert_eq!(archive.volumes.len(), 2);
12148
12149 let volume_refs = archive
12150 .volumes
12151 .iter()
12152 .map(Vec::as_slice)
12153 .collect::<Vec<_>>();
12154 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
12155
12156 assert_eq!(opened.volume_header.stripe_width, 2);
12157 assert_eq!(opened.list_files().unwrap()[0].path, "alpha.txt");
12158 assert_eq!(
12159 opened.extract_file("alpha.txt").unwrap(),
12160 Some(b"hello from volume stripes".to_vec())
12161 );
12162 opened.verify().unwrap();
12163 }
12164
12165 #[test]
12166 fn recovers_from_one_missing_volume_when_parity_allows() {
12167 let files = [RegularFile::new("alpha.txt", b"recover me")];
12168 let archive = write_archive(
12169 &files,
12170 &master_key(),
12171 WriterOptions {
12172 stripe_width: 2,
12173 volume_loss_tolerance: 1,
12174 ..single_stream_options()
12175 },
12176 )
12177 .unwrap();
12178
12179 let recovered =
12180 open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap();
12181 assert_eq!(
12182 recovered.extract_file("alpha.txt").unwrap(),
12183 Some(b"recover me".to_vec())
12184 );
12185 recovered.verify().unwrap();
12186 }
12187
12188 #[test]
12189 fn recovers_from_crc_corrupted_block_when_parity_allows() {
12190 let files = [RegularFile::new("alpha.txt", b"repair corrupt block")];
12191 let archive = write_archive(
12192 &files,
12193 &master_key(),
12194 WriterOptions {
12195 stripe_width: 2,
12196 volume_loss_tolerance: 1,
12197 ..single_stream_options()
12198 },
12199 )
12200 .unwrap();
12201 let mut volumes = archive.volumes.clone();
12202 corrupt_first_block_record_payload(&mut volumes[0]);
12203
12204 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
12205 let recovered = open_archive_volumes(&volume_refs, &master_key()).unwrap();
12206
12207 assert_eq!(
12208 recovered.extract_file("alpha.txt").unwrap(),
12209 Some(b"repair corrupt block".to_vec())
12210 );
12211 recovered.verify().unwrap();
12212 }
12213
12214 #[test]
12215 fn rejects_multi_volume_count_mismatch_without_tolerance() {
12216 let files = [RegularFile::new("alpha.txt", b"count check")];
12217 let archive = write_archive(
12218 &files,
12219 &master_key(),
12220 WriterOptions {
12221 stripe_width: 3,
12222 volume_loss_tolerance: 0,
12223 ..single_stream_options()
12224 },
12225 )
12226 .unwrap();
12227
12228 assert_eq!(
12229 open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap_err(),
12230 FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
12231 );
12232 }
12233
12234 #[test]
12235 fn rejects_multi_volume_manifest_bootstrap_field_mismatch() {
12236 let files = [RegularFile::new("alpha.txt", b"footer mismatch")];
12237 let archive = write_archive(
12238 &files,
12239 &master_key(),
12240 WriterOptions {
12241 stripe_width: 2,
12242 volume_loss_tolerance: 1,
12243 ..single_stream_options()
12244 },
12245 )
12246 .unwrap();
12247
12248 let mut bad_first = archive.volumes[0].clone();
12249 rewrite_manifest_footer(&mut bad_first, &master_key(), |footer| {
12250 footer.index_root_first_block = footer.index_root_first_block.wrapping_add(1);
12251 });
12252
12253 open_archive_volumes(
12254 &[bad_first.as_slice(), archive.volumes[1].as_slice()],
12255 &master_key(),
12256 )
12257 .unwrap();
12258 }
12259
12260 #[test]
12261 fn repairs_corrupted_index_root_block_in_multi_volume_archive() {
12262 let files = [RegularFile::new("alpha.txt", b"repair meta root")];
12263 let archive = write_archive(
12264 &files,
12265 &master_key(),
12266 WriterOptions {
12267 stripe_width: 2,
12268 volume_loss_tolerance: 1,
12269 ..single_stream_options()
12270 },
12271 )
12272 .unwrap();
12273 let mut volumes = archive.volumes.clone();
12274
12275 let mut corrupted = false;
12276 for volume in &mut volumes {
12277 if let Some(slot) =
12278 block_record_slots_with_kind(volume, BlockKind::IndexRootData).first()
12279 {
12280 corrupt_block_record_payload_at_slot(volume, *slot);
12281 corrupted = true;
12282 break;
12283 }
12284 }
12285 assert!(corrupted, "expected an IndexRootData record");
12286
12287 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
12288 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
12289 assert_eq!(
12290 opened.extract_file("alpha.txt").unwrap(),
12291 Some(b"repair meta root".to_vec())
12292 );
12293 opened.verify().unwrap();
12294 }
12295
12296 #[test]
12297 fn repairs_corrupted_index_shard_block_in_multi_volume_archive() {
12298 let files = [RegularFile::new("alpha.txt", b"repair meta shard")];
12299 let archive = write_archive(
12300 &files,
12301 &master_key(),
12302 WriterOptions {
12303 stripe_width: 2,
12304 volume_loss_tolerance: 1,
12305 ..single_stream_options()
12306 },
12307 )
12308 .unwrap();
12309 let mut volumes = archive.volumes.clone();
12310
12311 let mut corrupted = false;
12312 for volume in &mut volumes {
12313 if let Some(slot) =
12314 block_record_slots_with_kind(volume, BlockKind::IndexShardData).first()
12315 {
12316 corrupt_block_record_payload_at_slot(volume, *slot);
12317 corrupted = true;
12318 break;
12319 }
12320 }
12321 assert!(corrupted, "expected an IndexShardData record");
12322
12323 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
12324 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
12325 assert_eq!(
12326 opened.extract_file("alpha.txt").unwrap(),
12327 Some(b"repair meta shard".to_vec())
12328 );
12329 opened.verify().unwrap();
12330 }
12331
12332 #[test]
12333 fn rejects_missing_volume_when_loss_tolerance_zero_even_with_bitrot_parity() {
12334 let files = [RegularFile::new(
12335 "alpha.txt",
12336 b"bitrot parity is not volume loss",
12337 )];
12338 let archive = write_archive(
12339 &files,
12340 &master_key(),
12341 WriterOptions {
12342 stripe_width: 2,
12343 volume_loss_tolerance: 0,
12344 bit_rot_buffer_pct: 1,
12345 ..single_stream_options()
12346 },
12347 )
12348 .unwrap();
12349
12350 assert_eq!(
12351 open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap_err(),
12352 FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
12353 );
12354 }
12355
12356 #[test]
12357 fn repairs_crc_erasure_only_within_parity_budget() {
12358 let payload = pseudo_random_bytes(12_000);
12359 let archive = write_archive(
12360 &[RegularFile::new("rot.bin", &payload)],
12361 &master_key(),
12362 small_block_recovery_options(),
12363 )
12364 .unwrap();
12365 let payload_slots = first_payload_data_run_slots(&archive.bytes);
12366 assert!(
12367 payload_slots.len() >= 2,
12368 "fixture must contain a multi-block payload object"
12369 );
12370
12371 let mut one_erasure = archive.bytes.clone();
12372 corrupt_block_record_payload_at_slot(&mut one_erasure, payload_slots[0]);
12373 let repaired = open_archive(&one_erasure, &master_key()).unwrap();
12374 assert_eq!(
12375 repaired.extract_file("rot.bin").unwrap(),
12376 Some(payload.clone())
12377 );
12378
12379 let mut two_erasures = archive.bytes.clone();
12380 corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[0]);
12381 corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[1]);
12382 let unrepaired = open_archive(&two_erasures, &master_key()).unwrap();
12383 assert_eq!(
12384 unrepaired.extract_file("rot.bin").unwrap_err(),
12385 FormatError::FecTooFewAvailableShards
12386 );
12387 }
12388
12389 #[test]
12390 fn verify_rejects_missing_required_object_block_extent() {
12391 let (mut opened, missing_block) = multi_envelope_reader_fixture();
12392 assert!(opened.blocks.remove(&missing_block).is_some());
12393
12394 assert_eq!(
12395 opened.verify().unwrap_err(),
12396 FormatError::FecTooFewAvailableShards
12397 );
12398 }
12399
12400 #[test]
12401 fn parity_crc_erasure_does_not_hide_authenticated_data() {
12402 let payload = pseudo_random_bytes(12_000);
12403 let archive = write_archive(
12404 &[RegularFile::new("parity-erasure.bin", &payload)],
12405 &master_key(),
12406 parity_rich_recovery_options(),
12407 )
12408 .unwrap();
12409 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
12410 let parity_slots = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity);
12411 assert!(
12412 parity_slots.len() >= 2,
12413 "fixture must contain redundant parity shards"
12414 );
12415 let mut corrupted = archive.bytes;
12416 corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
12417 corrupt_block_record_payload_at_slot(&mut corrupted, parity_slots[0]);
12418
12419 let opened = open_archive(&corrupted, &master_key()).unwrap();
12420 assert_eq!(
12421 opened.extract_file("parity-erasure.bin").unwrap(),
12422 Some(payload)
12423 );
12424 opened.verify().unwrap();
12425 }
12426
12427 #[test]
12428 fn repair_patches_restore_crc_erased_payload_block() {
12429 let payload = pseudo_random_bytes(12_000);
12430 let archive = write_archive(
12431 &[RegularFile::new("rot.bin", &payload)],
12432 &master_key(),
12433 small_block_recovery_options(),
12434 )
12435 .unwrap();
12436 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
12437 let mut corrupted = archive.bytes.clone();
12438 corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
12439
12440 let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
12441 opened.verify().unwrap();
12442 let patches = opened.repair_patches().unwrap();
12443 assert_eq!(patches.len(), 1);
12444 apply_repair_patches(&mut corrupted, &patches);
12445
12446 let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
12447 repaired.verify().unwrap();
12448 assert!(repaired.repair_patches().unwrap().is_empty());
12449 }
12450
12451 #[test]
12452 fn repair_patches_restore_crc_erased_payload_parity_block() {
12453 let payload = pseudo_random_bytes(12_000);
12454 let archive = write_archive(
12455 &[RegularFile::new("parity-erasure.bin", &payload)],
12456 &master_key(),
12457 parity_rich_recovery_options(),
12458 )
12459 .unwrap();
12460 let parity_slot = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity)[0];
12461 let mut corrupted = archive.bytes.clone();
12462 corrupt_block_record_payload_at_slot(&mut corrupted, parity_slot);
12463
12464 let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
12465 opened.verify().unwrap();
12466 let patches = opened.repair_patches().unwrap();
12467 assert_eq!(patches.len(), 1);
12468 apply_repair_patches(&mut corrupted, &patches);
12469
12470 let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
12471 repaired.verify().unwrap();
12472 assert!(repaired.repair_patches().unwrap().is_empty());
12473 }
12474
12475 #[test]
12476 fn recovers_physical_odd_block_size_from_cmra_authority() {
12477 let archive = write_archive(
12478 &[RegularFile::new("odd-block.txt", b"payload")],
12479 &master_key(),
12480 small_block_recovery_options(),
12481 )
12482 .unwrap();
12483 let mut malformed = archive.bytes;
12484 let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
12485 let block_size_offset = volume_header.crypto_header_offset as usize + 24;
12486 malformed[block_size_offset..block_size_offset + 4].copy_from_slice(&4097u32.to_le_bytes());
12487
12488 let opened = open_archive(&malformed, &master_key()).unwrap();
12489 assert_ne!(opened.crypto_header.block_size, 4097);
12490 assert_eq!(
12491 opened.extract_file("odd-block.txt").unwrap(),
12492 Some(b"payload".to_vec())
12493 );
12494 opened.verify().unwrap();
12495 }
12496
12497 #[test]
12498 fn repairs_structurally_malformed_payload_block_slots() {
12499 let payload = pseudo_random_bytes(12_000);
12500 let archive = write_archive(
12501 &[RegularFile::new("structural-block.bin", &payload)],
12502 &master_key(),
12503 small_block_recovery_options(),
12504 )
12505 .unwrap();
12506 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
12507
12508 let mut bad_magic = archive.bytes.clone();
12509 corrupt_block_record_magic_at_slot(&mut bad_magic, payload_slot);
12510 assert_eq!(
12511 open_archive(&bad_magic, &master_key())
12512 .unwrap()
12513 .extract_file("structural-block.bin")
12514 .unwrap(),
12515 Some(payload.clone())
12516 );
12517
12518 let mut bad_reserved = archive.bytes;
12519 corrupt_block_record_reserved_at_slot(&mut bad_reserved, payload_slot);
12520 assert_eq!(
12521 open_archive(&bad_reserved, &master_key())
12522 .unwrap()
12523 .extract_file("structural-block.bin")
12524 .unwrap(),
12525 Some(payload)
12526 );
12527 }
12528
12529 #[test]
12530 fn repair_patches_restore_structurally_malformed_payload_block_slot() {
12531 let payload = pseudo_random_bytes(12_000);
12532 let archive = write_archive(
12533 &[RegularFile::new("structural-patch.bin", &payload)],
12534 &master_key(),
12535 small_block_recovery_options(),
12536 )
12537 .unwrap();
12538 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
12539 let mut corrupted = archive.bytes.clone();
12540 corrupt_block_record_magic_at_slot(&mut corrupted, payload_slot);
12541
12542 let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
12543 opened.verify().unwrap();
12544 assert_eq!(
12545 opened.extract_file("structural-patch.bin").unwrap(),
12546 Some(payload)
12547 );
12548 let patches = opened.repair_patches().unwrap();
12549 assert_eq!(patches.len(), 1);
12550 apply_repair_patches(&mut corrupted, &patches);
12551
12552 let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
12553 repaired.verify().unwrap();
12554 assert!(repaired.repair_patches().unwrap().is_empty());
12555 }
12556
12557 #[test]
12558 fn repairs_structurally_malformed_index_root_block_slot() {
12559 let archive = write_archive(
12560 &[RegularFile::new(
12561 "structural-index-root.txt",
12562 b"metadata repair",
12563 )],
12564 &master_key(),
12565 small_block_recovery_options(),
12566 )
12567 .unwrap();
12568 let index_root_slot =
12569 first_block_record_slot_with_kind(&archive.bytes, BlockKind::IndexRootData).unwrap();
12570 let mut corrupted = archive.bytes;
12571 corrupt_block_record_magic_at_slot(&mut corrupted, index_root_slot);
12572
12573 let opened = open_archive(&corrupted, &master_key()).unwrap();
12574 assert_eq!(
12575 opened.extract_file("structural-index-root.txt").unwrap(),
12576 Some(b"metadata repair".to_vec())
12577 );
12578 opened.verify().unwrap();
12579 }
12580
12581 #[test]
12582 fn rejects_parity_block_with_last_data_flag() {
12583 let archive = write_archive(
12584 &[RegularFile::new("parity-flag.txt", b"payload")],
12585 &master_key(),
12586 small_block_recovery_options(),
12587 )
12588 .unwrap();
12589 let parity_slot =
12590 first_block_record_slot_with_kind(&archive.bytes, BlockKind::PayloadParity).unwrap();
12591 let mut malformed = archive.bytes;
12592 mutate_block_record_at_slot(&mut malformed, parity_slot, |record| {
12593 record.flags = 0x01;
12594 });
12595
12596 assert_eq!(
12597 open_archive(&malformed, &master_key()).unwrap_err(),
12598 FormatError::ParityBlockHasLastDataFlag
12599 );
12600 }
12601
12602 #[test]
12603 fn rejects_missing_and_duplicate_payload_last_data_flags() {
12604 let payload = pseudo_random_bytes(12_000);
12605 let archive = write_archive(
12606 &[RegularFile::new("flags.bin", &payload)],
12607 &master_key(),
12608 small_block_recovery_options(),
12609 )
12610 .unwrap();
12611 let payload_slots = first_payload_data_run_slots(&archive.bytes);
12612 assert!(
12613 payload_slots.len() >= 2,
12614 "fixture must contain a multi-block payload object"
12615 );
12616
12617 let mut duplicate_last = archive.bytes.clone();
12618 mutate_block_record_at_slot(&mut duplicate_last, payload_slots[0], |record| {
12619 record.flags = 0x01;
12620 });
12621 let opened = open_archive(&duplicate_last, &master_key()).unwrap();
12622 assert_eq!(
12623 opened.extract_file("flags.bin").unwrap_err(),
12624 FormatError::InvalidArchive("object last-data flag is not on the final data block")
12625 );
12626
12627 let mut missing_last = archive.bytes;
12628 mutate_block_record_at_slot(
12629 &mut missing_last,
12630 *payload_slots.last().unwrap(),
12631 |record| {
12632 record.flags = 0;
12633 },
12634 );
12635 let opened = open_archive(&missing_last, &master_key()).unwrap();
12636 assert_eq!(
12637 opened.extract_file("flags.bin").unwrap_err(),
12638 FormatError::InvalidArchive("object last-data flag is not on the final data block")
12639 );
12640 }
12641
12642 #[test]
12643 fn recovers_from_one_corrupt_manifest_footer_copy_when_another_volume_authenticates() {
12644 let files = [RegularFile::new(
12645 "footer-copy.txt",
12646 b"survives one bad footer",
12647 )];
12648 let archive = write_archive(
12649 &files,
12650 &master_key(),
12651 WriterOptions {
12652 stripe_width: 2,
12653 volume_loss_tolerance: 1,
12654 ..single_stream_options()
12655 },
12656 )
12657 .unwrap();
12658 let mut volumes = archive.volumes.clone();
12659 corrupt_manifest_footer_hmac(&mut volumes[0]);
12660
12661 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
12662 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
12663 assert_eq!(opened.manifest_footer.volume_index, 0);
12664 assert_eq!(opened.volume_header.volume_index, 0);
12665 assert_eq!(opened.volume_trailer.as_ref().unwrap().volume_index, 0);
12666 assert_eq!(
12667 opened.extract_file("footer-copy.txt").unwrap(),
12668 Some(b"survives one bad footer".to_vec())
12669 );
12670 opened.verify().unwrap();
12671 }
12672
12673 #[test]
12674 fn manifest_footer_corruption_requires_trusted_sidecar() {
12675 let archive = write_archive(
12676 &[RegularFile::new("footer.txt", b"sidecar authority")],
12677 &master_key(),
12678 single_stream_options(),
12679 )
12680 .unwrap();
12681 let manifest_offset = terminal_material_offset(&archive.bytes);
12682 let mut corrupted = archive.bytes.clone();
12683 corrupted[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
12684 corrupt_v41_terminal_recovery(&mut corrupted);
12685
12686 assert!(open_archive(&corrupted, &master_key()).is_err());
12687
12688 let opened =
12689 open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
12690 .unwrap();
12691 assert!(opened.volume_trailer.is_none());
12692 assert_eq!(
12693 opened.extract_file("footer.txt").unwrap(),
12694 Some(b"sidecar authority".to_vec())
12695 );
12696 opened.verify().unwrap();
12697 }
12698
12699 #[test]
12700 fn authenticated_footer_trailer_and_sidecar_hmac_boundaries_are_enforced() {
12701 let archive = write_archive(
12702 &[RegularFile::new("hmac-boundary.txt", b"boundary bytes")],
12703 &master_key(),
12704 single_stream_options(),
12705 )
12706 .unwrap();
12707 let strict_options = ReaderOptions {
12708 max_trailing_garbage_scan: 0,
12709 ..ReaderOptions::default()
12710 };
12711
12712 let manifest_offset = terminal_material_offset(&archive.bytes);
12713 for offset in [
12714 manifest_offset + 71,
12715 manifest_offset + MANIFEST_HMAC_COVERED_LEN,
12716 ] {
12717 let mut corrupted = archive.bytes.clone();
12718 corrupted[offset] ^= 0x01;
12719 open_archive(&corrupted, &master_key()).unwrap();
12720 }
12721
12722 let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
12723 for offset in [
12724 trailer_offset + 75,
12725 trailer_offset + TRAILER_HMAC_COVERED_LEN,
12726 ] {
12727 let mut corrupted = archive.bytes.clone();
12728 corrupted[offset] ^= 0x01;
12729 OpenedArchive::open_with_options(&corrupted, &master_key(), strict_options).unwrap();
12730 }
12731
12732 let mut covered_sidecar = archive.bootstrap_sidecar.clone();
12733 let mut header =
12734 BootstrapSidecarHeader::parse(&covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
12735 .unwrap();
12736 header.manifest_footer_offset += 1;
12737 covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
12738 assert_eq!(
12739 open_archive_with_bootstrap_sidecar(&archive.bytes, &covered_sidecar, &master_key())
12740 .unwrap_err(),
12741 FormatError::HmacMismatch {
12742 structure: "BootstrapSidecarHeader"
12743 }
12744 );
12745
12746 let mut tag_sidecar = archive.bootstrap_sidecar.clone();
12747 let mut header =
12748 BootstrapSidecarHeader::parse(&tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12749 header.sidecar_hmac[0] ^= 1;
12750 tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
12751 assert_eq!(
12752 open_archive_with_bootstrap_sidecar(&archive.bytes, &tag_sidecar, &master_key())
12753 .unwrap_err(),
12754 FormatError::HmacMismatch {
12755 structure: "BootstrapSidecarHeader"
12756 }
12757 );
12758
12759 let mut non_covered_sidecar = archive.bootstrap_sidecar.clone();
12760 let header =
12761 BootstrapSidecarHeader::parse(&non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
12762 .unwrap();
12763 let mut header_bytes = header.to_bytes();
12764 header_bytes[124] ^= 0x01;
12765 let crc = crc32c::crc32c(&header_bytes[..124]);
12766 header_bytes[124..128].copy_from_slice(&crc.to_le_bytes());
12767 non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
12768 let opened = open_archive_with_bootstrap_sidecar(
12769 &archive.bytes,
12770 &non_covered_sidecar,
12771 &master_key(),
12772 )
12773 .unwrap();
12774 assert_eq!(
12775 opened.extract_file("hmac-boundary.txt").unwrap(),
12776 Some(b"boundary bytes".to_vec())
12777 );
12778 }
12779
12780 #[test]
12781 fn rejects_authenticated_footer_and_trailer_volume_index_mismatches() {
12782 let archive = write_archive(
12783 &[RegularFile::new("volume-index.txt", b"identity")],
12784 &master_key(),
12785 single_stream_options(),
12786 )
12787 .unwrap();
12788
12789 let mut bad_trailer = archive.bytes.clone();
12790 rewrite_volume_trailer(&mut bad_trailer, &master_key(), |trailer| {
12791 trailer.volume_index = 1;
12792 });
12793 open_archive(&bad_trailer, &master_key()).unwrap();
12794
12795 let mut bad_manifest = archive.bytes;
12796 rewrite_manifest_footer(&mut bad_manifest, &master_key(), |footer| {
12797 footer.volume_index = 1;
12798 });
12799 open_archive(&bad_manifest, &master_key()).unwrap();
12800 }
12801
12802 #[test]
12803 fn rejects_same_key_header_terminal_material_splice() {
12804 let first = write_archive(
12805 &[RegularFile::new("splice.txt", b"same shape")],
12806 &master_key(),
12807 single_stream_options(),
12808 )
12809 .unwrap();
12810 let second = write_archive(
12811 &[RegularFile::new("splice.txt", b"same shape")],
12812 &master_key(),
12813 single_stream_options(),
12814 )
12815 .unwrap();
12816 assert_ne!(first.archive_uuid, second.archive_uuid);
12817 assert_eq!(
12818 terminal_material_offset(&first.bytes),
12819 terminal_material_offset(&second.bytes)
12820 );
12821 assert_eq!(first.bytes.len(), second.bytes.len());
12822
12823 let terminal_offset = terminal_material_offset(&first.bytes);
12824 let mut spliced = first.bytes.clone();
12825 spliced[terminal_offset..].copy_from_slice(&second.bytes[terminal_offset..]);
12826
12827 assert_eq!(
12828 open_archive(&spliced, &master_key()).unwrap_err(),
12829 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
12830 );
12831 }
12832
12833 #[test]
12834 fn rejects_cmra_crypto_header_pre_hmac_mismatch() {
12835 let kdf_params = crate::crypto::KdfParams::Argon2id {
12836 t_cost: 1,
12837 m_cost_kib: 8,
12838 parallelism: 1,
12839 salt: b"0123456789abcdef".to_vec(),
12840 };
12841 let archive = write_archive_with_kdf(
12842 &[RegularFile::new("cmra-crypto.txt", b"same fixed header")],
12843 &master_key(),
12844 single_stream_options(),
12845 &kdf_params,
12846 )
12847 .unwrap();
12848 let mut mutated = archive.bytes.clone();
12849 let volume_header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
12850 let subkeys = Subkeys::derive(
12851 &master_key(),
12852 &volume_header.archive_uuid,
12853 &volume_header.session_id,
12854 )
12855 .unwrap();
12856
12857 rewrite_cmra_image(&mut mutated, CmraRecoveryMode::KeyHolding, |image| {
12858 let crypto_region = image
12859 .regions
12860 .iter_mut()
12861 .find(|region| region.region_type == 2)
12862 .unwrap();
12863 let hmac_offset = crypto_region.bytes.len() - CRYPTO_HEADER_HMAC_LEN;
12864 let salt_start = CRYPTO_HEADER_FIXED_LEN + 16;
12865 crypto_region.bytes[salt_start] ^= 0x01;
12866 let hmac = compute_hmac(
12867 HmacDomain::CryptoHeader,
12868 &subkeys.mac_key,
12869 &volume_header.archive_uuid,
12870 &volume_header.session_id,
12871 &crypto_region.bytes[..hmac_offset],
12872 );
12873 crypto_region.bytes[hmac_offset..].copy_from_slice(&hmac);
12874 });
12875
12876 let final_offset = mutated.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
12877 let locator = final_recovery_locator(&mutated);
12878 let crypto_start = volume_header.crypto_header_offset as usize;
12879 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
12880 let parsed_crypto = CryptoHeader::parse(
12881 &mutated[crypto_start..crypto_end],
12882 volume_header.crypto_header_length,
12883 )
12884 .unwrap();
12885 assert_eq!(
12886 parse_locator_cmra_candidate(
12887 &mutated,
12888 final_offset,
12889 locator,
12890 KeyHoldingTerminalContext {
12891 subkeys: &subkeys,
12892 volume_header: &volume_header,
12893 crypto_header: &parsed_crypto.fixed,
12894 crypto_header_bytes: &mutated[crypto_start..crypto_end],
12895 },
12896 )
12897 .unwrap_err(),
12898 FormatError::InvalidArchive("CMRA CryptoHeader differs from parsed CryptoHeader")
12899 );
12900 assert!(open_archive(&mutated, &master_key()).is_err());
12901 }
12902
12903 #[test]
12904 fn recovers_physical_crypto_header_splice_from_cmra_authority() {
12905 let base = WriterOptions {
12906 archive_uuid: Some([0x11; 16]),
12907 session_id: Some([0x22; 16]),
12908 ..small_block_recovery_options()
12909 };
12910 let same_archive = WriterOptions {
12911 archive_uuid: Some([0x11; 16]),
12912 session_id: Some([0x33; 16]),
12913 ..small_block_recovery_options()
12914 };
12915
12916 let first = write_archive(
12917 &[RegularFile::new("splice.txt", b"same shape")],
12918 &master_key(),
12919 base,
12920 )
12921 .unwrap();
12922 let second = write_archive(
12923 &[RegularFile::new("splice.txt", b"same shape")],
12924 &master_key(),
12925 same_archive,
12926 )
12927 .unwrap();
12928
12929 let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
12930 let second_volume_header = VolumeHeader::parse(&second.bytes[..VOLUME_HEADER_LEN]).unwrap();
12931 let crypto_start = volume_header.crypto_header_offset as usize;
12932 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
12933 let second_crypto_end = second_volume_header.crypto_header_offset as usize
12934 + second_volume_header.crypto_header_length as usize;
12935 assert_eq!(crypto_end, second_crypto_end);
12936
12937 let mut spliced = first.bytes.clone();
12938 spliced[crypto_start..crypto_end].copy_from_slice(&second.bytes[crypto_start..crypto_end]);
12939
12940 let opened = open_archive(&spliced, &master_key()).unwrap();
12941 assert_eq!(
12942 opened.crypto_header_bytes,
12943 first.bytes[crypto_start..crypto_end].to_vec()
12944 );
12945 assert_eq!(
12946 opened.extract_file("splice.txt").unwrap(),
12947 Some(b"same shape".to_vec())
12948 );
12949 opened.verify().unwrap();
12950 }
12951
12952 #[test]
12953 fn rejects_same_key_object_splice_with_session_mismatch() {
12954 let first = write_archive(
12955 &[RegularFile::new("splice.txt", b"same shape")],
12956 &master_key(),
12957 WriterOptions {
12958 archive_uuid: Some([0x11; 16]),
12959 session_id: Some([0x22; 16]),
12960 ..single_stream_options()
12961 },
12962 )
12963 .unwrap();
12964 let second = write_archive(
12965 &[RegularFile::new("splice.txt", b"same shape")],
12966 &master_key(),
12967 WriterOptions {
12968 archive_uuid: Some([0x11; 16]),
12969 session_id: Some([0x33; 16]),
12970 ..single_stream_options()
12971 },
12972 )
12973 .unwrap();
12974
12975 let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
12976 let crypto_end = volume_header.crypto_header_offset as usize
12977 + volume_header.crypto_header_length as usize;
12978 let terminal_offset = terminal_material_offset(&first.bytes);
12979 let second_terminal_offset = terminal_material_offset(&second.bytes);
12980 assert_eq!(terminal_offset, second_terminal_offset);
12981
12982 let mut spliced = first.bytes.clone();
12983 spliced[crypto_end..terminal_offset]
12984 .copy_from_slice(&second.bytes[crypto_end..terminal_offset]);
12985
12986 assert_eq!(
12987 open_archive(&spliced, &master_key()).unwrap_err(),
12988 FormatError::AeadFailure
12989 );
12990 }
12991
12992 #[test]
12993 fn rejects_authenticated_trailer_pointer_and_count_mutations() {
12994 let archive = write_archive(
12995 &[RegularFile::new(
12996 "trailer-range.txt",
12997 b"authenticated ranges",
12998 )],
12999 &master_key(),
13000 single_stream_options(),
13001 )
13002 .unwrap();
13003 let strict_options = ReaderOptions {
13004 max_trailing_garbage_scan: 0,
13005 ..ReaderOptions::default()
13006 };
13007 let bytes = archive.bytes;
13008 let manifest_offset = terminal_material_offset(&bytes);
13009 let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
13010
13011 let mut wrong_footer_length = bytes.clone();
13012 rewrite_volume_trailer(&mut wrong_footer_length, &master_key(), |trailer| {
13013 trailer.manifest_footer_length = 42;
13014 });
13015 OpenedArchive::open_with_options(&wrong_footer_length, &master_key(), strict_options)
13016 .unwrap();
13017
13018 for (label, offset) in [
13019 (
13020 "offset before trailer by 1",
13021 manifest_offset.saturating_sub(1),
13022 ),
13023 ("offset after trailer", manifest_offset + 1),
13024 ("offset at stream start", 0),
13025 ("offset at trailer", trailer_offset),
13026 ("offset beyond trailer", trailer_offset + 4),
13027 ] {
13028 let mut wrong_footer_offset = bytes.clone();
13029 rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
13030 trailer.manifest_footer_offset = offset as u64;
13031 });
13032 open_archive(&wrong_footer_offset, &master_key())
13033 .unwrap_or_else(|err| panic!("manifest offset case {label}: {err:?}"));
13034 }
13035
13036 let mut wrong_bytes_written = bytes.clone();
13037 rewrite_volume_trailer(&mut wrong_bytes_written, &master_key(), |trailer| {
13038 trailer.bytes_written += 1;
13039 });
13040 open_archive(&wrong_bytes_written, &master_key()).unwrap();
13041
13042 let mut wrong_block_count = bytes.clone();
13043 rewrite_volume_trailer(&mut wrong_block_count, &master_key(), |trailer| {
13044 trailer.block_count += 1;
13045 });
13046 open_archive(&wrong_block_count, &master_key()).unwrap();
13047
13048 let mut wrong_footer_offset = bytes.clone();
13049 rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
13050 trailer.manifest_footer_offset = bytes.len() as u64 + 1024;
13051 });
13052 open_archive(&wrong_footer_offset, &master_key()).unwrap();
13053 }
13054
13055 #[test]
13056 fn rejects_authenticated_trailer_outside_trailing_scan_cap() {
13057 let archive = write_archive(
13058 &[RegularFile::new(
13059 "trailer-trailing-scan.txt",
13060 b"trailer scan boundaries",
13061 )],
13062 &master_key(),
13063 single_stream_options(),
13064 )
13065 .unwrap();
13066 let options = ReaderOptions {
13067 max_trailing_garbage_scan: 8,
13068 ..ReaderOptions::default()
13069 };
13070
13071 let mut within_scan = archive.bytes.clone();
13072 within_scan.resize(within_scan.len() + options.max_trailing_garbage_scan, 0xAA);
13073 let opened =
13074 OpenedArchive::open_with_options(&within_scan, &master_key(), options).unwrap();
13075 assert_eq!(
13076 opened.extract_file("trailer-trailing-scan.txt").unwrap(),
13077 Some(b"trailer scan boundaries".to_vec())
13078 );
13079
13080 let mut beyond_scan = archive.bytes.clone();
13081 beyond_scan.resize(
13082 beyond_scan.len() + max_critical_recovery_scan(options).unwrap() + 1,
13083 0xAA,
13084 );
13085 assert_eq!(
13086 OpenedArchive::open_with_options(&beyond_scan, &master_key(), options).unwrap_err(),
13087 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
13088 );
13089 }
13090
13091 #[test]
13092 fn rejects_authenticated_index_root_extent_size_mismatch_at_open() {
13093 let archive = write_archive(
13094 &[RegularFile::new("index-root-size.txt", b"extent size")],
13095 &master_key(),
13096 single_stream_options(),
13097 )
13098 .unwrap();
13099 let mut malformed = archive.bytes;
13100 let slot = first_block_record_slot_with_kind(&malformed, BlockKind::IndexRootData)
13101 .expect("archive should contain IndexRootData");
13102 mutate_block_record_at_slot(&mut malformed, slot, |record| {
13103 record.payload[0] ^= 0x55;
13104 });
13105
13106 assert_eq!(
13107 open_archive(&malformed, &master_key()).unwrap_err(),
13108 FormatError::AeadFailure
13109 );
13110 }
13111
13112 #[test]
13113 fn rejects_block_record_at_wrong_stripe_position() {
13114 let files = [RegularFile::new("alpha.txt", b"wrong stripe")];
13115 let archive = write_archive(
13116 &files,
13117 &master_key(),
13118 WriterOptions {
13119 stripe_width: 2,
13120 volume_loss_tolerance: 1,
13121 ..single_stream_options()
13122 },
13123 )
13124 .unwrap();
13125 let mut volumes = archive.volumes.clone();
13126 mutate_first_block_record(&mut volumes[0], |record| {
13127 record.block_index += 2;
13128 });
13129
13130 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
13131 assert_eq!(
13132 open_archive_volumes(&volume_refs, &master_key()).unwrap_err(),
13133 FormatError::InvalidArchive("BlockRecord index does not match volume position")
13134 );
13135 }
13136
13137 #[test]
13138 fn rejects_decreasing_block_record_index_in_required_region() {
13139 let archive = write_archive(
13140 &[RegularFile::new("alpha.txt", b"decreasing block index")],
13141 &master_key(),
13142 single_stream_options(),
13143 )
13144 .unwrap();
13145 assert!(block_record_slots(&archive.bytes).len() >= 2);
13146
13147 let mut malformed = archive.bytes;
13148 mutate_block_record_at_slot(&mut malformed, 1, |record| {
13149 record.block_index = 0;
13150 });
13151
13152 assert_eq!(
13153 open_archive(&malformed, &master_key()).unwrap_err(),
13154 FormatError::InvalidArchive("BlockRecord index does not match volume position")
13155 );
13156 }
13157
13158 #[test]
13159 fn rejects_duplicate_authenticated_volume_indexes() {
13160 let files = [RegularFile::new("alpha.txt", b"duplicates")];
13161 let archive = write_archive(
13162 &files,
13163 &master_key(),
13164 WriterOptions {
13165 stripe_width: 2,
13166 volume_loss_tolerance: 1,
13167 ..single_stream_options()
13168 },
13169 )
13170 .unwrap();
13171
13172 assert_eq!(
13173 open_archive_volumes(
13174 &[archive.volumes[0].as_slice(), archive.volumes[0].as_slice()],
13175 &master_key()
13176 )
13177 .unwrap_err(),
13178 FormatError::InvalidArchive("duplicate authenticated volume index")
13179 );
13180 }
13181
13182 #[test]
13183 fn rejects_conflicting_duplicate_authenticated_volume_indexes_by_default() {
13184 let files = [RegularFile::new("alpha.txt", b"conflicting duplicates")];
13185 let archive = write_archive(
13186 &files,
13187 &master_key(),
13188 WriterOptions {
13189 stripe_width: 2,
13190 volume_loss_tolerance: 1,
13191 ..single_stream_options()
13192 },
13193 )
13194 .unwrap();
13195 let mut conflicting = archive.volumes[0].clone();
13196 corrupt_first_block_record_payload(&mut conflicting);
13197
13198 assert_eq!(
13199 open_archive_volumes(
13200 &[archive.volumes[0].as_slice(), conflicting.as_slice()],
13201 &master_key()
13202 )
13203 .unwrap_err(),
13204 FormatError::InvalidArchive("duplicate authenticated volume index")
13205 );
13206 }
13207
13208 fn directory_hint_table_from_rows(
13209 hint_shard_index: u64,
13210 rows: &[(Vec<u8>, Vec<u32>)],
13211 shard_count: u32,
13212 ) -> DirectoryHintTable {
13213 let mut entries = Vec::new();
13214 let mut shard_row_indexes = Vec::new();
13215 let mut string_pool = Vec::new();
13216
13217 for (path, rows) in rows {
13218 let path_offset = if path.is_empty() {
13219 0
13220 } else {
13221 let offset = string_pool.len() as u64;
13222 string_pool.extend_from_slice(path);
13223 offset
13224 };
13225 let shard_list_start_index = shard_row_indexes.len() as u32;
13226 shard_row_indexes.extend_from_slice(rows);
13227 entries.push(DirectoryHintEntry {
13228 dir_hash: hash_prefix(path),
13229 path_offset,
13230 path_length: path.len() as u32,
13231 shard_list_start_index,
13232 shard_count: rows.len() as u32,
13233 });
13234 }
13235
13236 let table_bytes =
13237 directory_hint_table_bytes(hint_shard_index, entries, shard_row_indexes, string_pool);
13238 let locating = DirectoryHintShardEntry {
13239 hint_shard_index,
13240 first_dir_hash: hash_prefix(&rows.first().unwrap().0),
13241 last_dir_hash: hash_prefix(&rows.last().unwrap().0),
13242 first_block_index: 0,
13243 data_block_count: 1,
13244 parity_block_count: 0,
13245 encrypted_size: 4096,
13246 decompressed_size: table_bytes.len() as u32,
13247 entry_count: rows.len() as u64,
13248 };
13249 DirectoryHintTable::parse(
13250 &table_bytes,
13251 &locating,
13252 shard_count,
13253 MetadataLimits::default(),
13254 )
13255 .unwrap()
13256 }
13257
13258 fn directory_hint_table_bytes(
13259 hint_shard_index: u64,
13260 entries: Vec<DirectoryHintEntry>,
13261 shard_row_indexes: Vec<u32>,
13262 string_pool: Vec<u8>,
13263 ) -> Vec<u8> {
13264 let header_len = DirectoryHintTableHeader {
13265 version: 1,
13266 hint_shard_index,
13267 entry_count: 0,
13268 entry_table_offset: 0,
13269 shard_list_offset: 0,
13270 string_pool_offset: 0,
13271 string_pool_size: 0,
13272 }
13273 .to_bytes()
13274 .len();
13275 let entry_len = entries
13276 .first()
13277 .map(|entry| entry.to_bytes().len())
13278 .unwrap_or(0);
13279 let shard_list_offset = if entries.is_empty() {
13280 0
13281 } else {
13282 header_len + entries.len() * entry_len
13283 };
13284 let string_pool_offset = if string_pool.is_empty() {
13285 0
13286 } else {
13287 shard_list_offset + shard_row_indexes.len() * 4
13288 };
13289
13290 let header = DirectoryHintTableHeader {
13291 version: 1,
13292 hint_shard_index,
13293 entry_count: entries.len() as u64,
13294 entry_table_offset: if entries.is_empty() {
13295 0
13296 } else {
13297 header_len as u64
13298 },
13299 shard_list_offset: shard_list_offset as u64,
13300 string_pool_offset: string_pool_offset as u64,
13301 string_pool_size: string_pool.len() as u64,
13302 };
13303
13304 let mut out = Vec::new();
13305 out.extend_from_slice(&header.to_bytes());
13306 for entry in entries {
13307 out.extend_from_slice(&entry.to_bytes());
13308 }
13309 for row in shard_row_indexes {
13310 out.extend_from_slice(&row.to_le_bytes());
13311 }
13312 out.extend_from_slice(&string_pool);
13313 out
13314 }
13315
13316 fn corrupt_first_block_record_payload(volume: &mut [u8]) {
13317 let (record_offset, _) = first_block_record(volume);
13318 volume[record_offset + 16] ^= 0x55;
13319 }
13320
13321 fn corrupt_block_record_payload_at_slot(volume: &mut [u8], slot: usize) {
13322 let (record_offset, _) = block_record_at_slot(volume, slot);
13323 volume[record_offset + 16] ^= 0x55;
13324 }
13325
13326 fn apply_repair_patches(volume: &mut [u8], patches: &[ArchiveRepairPatch]) {
13327 for patch in patches {
13328 let offset = patch.record_offset as usize;
13329 let end = offset + patch.record_bytes.len();
13330 volume[offset..end].copy_from_slice(&patch.record_bytes);
13331 }
13332 }
13333
13334 fn corrupt_block_record_magic_at_slot(volume: &mut [u8], slot: usize) {
13335 let (record_offset, _) = block_record_at_slot(volume, slot);
13336 volume[record_offset] ^= 0x55;
13337 }
13338
13339 fn corrupt_block_record_reserved_at_slot(volume: &mut [u8], slot: usize) {
13340 let (record_offset, _) = block_record_at_slot(volume, slot);
13341 volume[record_offset + 14] = 0x01;
13342 }
13343
13344 fn corrupt_manifest_footer_hmac(volume: &mut [u8]) {
13345 let manifest_offset = terminal_material_offset(volume);
13346 volume[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
13347 }
13348
13349 fn final_recovery_locator(volume: &[u8]) -> CriticalRecoveryLocator {
13350 let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
13351 CriticalRecoveryLocator::parse(
13352 &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
13353 )
13354 .unwrap()
13355 }
13356
13357 fn rewrite_cmra_parity_count(volume: &[u8], parity_shard_count: u16) -> Vec<u8> {
13358 let locator = final_recovery_locator(volume);
13359 let tuple = CmraDecoderTuple::from(locator);
13360 assert!(parity_shard_count < tuple.parity_shard_count);
13361 let cmra_offset = locator.cmra_offset as usize;
13362 let shard_size = tuple.shard_size as usize;
13363 let row_len = CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size;
13364 let kept_rows = tuple.data_shard_count as usize + parity_shard_count as usize;
13365 let mut header = CriticalMetadataRecoveryHeader::parse(
13366 &volume[cmra_offset..cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN],
13367 )
13368 .unwrap();
13369 header.parity_shard_count = parity_shard_count;
13370
13371 let mut cmra =
13372 Vec::with_capacity(CRITICAL_METADATA_RECOVERY_HEADER_LEN + kept_rows * row_len);
13373 cmra.extend_from_slice(&header.to_bytes());
13374 let rows_start = cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
13375 for row in 0..kept_rows {
13376 let start = rows_start + row * row_len;
13377 cmra.extend_from_slice(&volume[start..start + row_len]);
13378 }
13379
13380 let mut out = Vec::with_capacity(cmra_offset + cmra.len() + LOCATOR_PAIR_LEN);
13381 out.extend_from_slice(&volume[..cmra_offset]);
13382 out.extend_from_slice(&cmra);
13383 let mut mirror = locator;
13384 mirror.locator_sequence = 1;
13385 mirror.cmra_length = cmra.len() as u32;
13386 mirror.cmra_parity_shard_count = parity_shard_count;
13387 out.extend_from_slice(&mirror.to_bytes());
13388 let final_locator = CriticalRecoveryLocator {
13389 locator_sequence: 0,
13390 ..mirror
13391 };
13392 out.extend_from_slice(&final_locator.to_bytes());
13393 out
13394 }
13395
13396 fn rewrite_public_cmra_image(
13397 volume: &mut [u8],
13398 mutate: impl FnOnce(&mut CriticalMetadataImage),
13399 ) {
13400 rewrite_cmra_image(volume, CmraRecoveryMode::PublicNoKey, mutate);
13401 }
13402
13403 fn rewrite_cmra_image(
13404 volume: &mut [u8],
13405 mode: CmraRecoveryMode,
13406 mutate: impl FnOnce(&mut CriticalMetadataImage),
13407 ) {
13408 let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
13409 let locator = final_recovery_locator(volume);
13410 let tuple = CmraDecoderTuple::from(locator);
13411 let recovered = recover_cmra(volume, locator.cmra_offset, Some(tuple), mode).unwrap();
13412 let mut image = recovered.image;
13413 mutate(&mut image);
13414 refresh_critical_image_region_digests(&mut image);
13415 let image_bytes = image.to_bytes().unwrap();
13416 assert_eq!(image_bytes.len(), tuple.image_length as usize);
13417
13418 let shard_size = tuple.shard_size as usize;
13419 let data_shard_count = tuple.data_shard_count as usize;
13420 let parity_shard_count = tuple.parity_shard_count as usize;
13421 assert!(image_bytes.len() <= data_shard_count * shard_size);
13422
13423 let mut data_shards = Vec::with_capacity(data_shard_count);
13424 for idx in 0..data_shard_count {
13425 let start = idx * shard_size;
13426 let end = (start + shard_size).min(image_bytes.len());
13427 let mut payload = vec![0u8; shard_size];
13428 if start < image_bytes.len() {
13429 payload[..end - start].copy_from_slice(&image_bytes[start..end]);
13430 }
13431 data_shards.push(payload);
13432 }
13433 let parity_shards = encode_parity_gf16(&data_shards, parity_shard_count).unwrap();
13434 let image_sha256 = sha256_bytes(&image_bytes);
13435
13436 let header = CriticalMetadataRecoveryHeader {
13437 shard_size: tuple.shard_size,
13438 data_shard_count: tuple.data_shard_count,
13439 parity_shard_count: tuple.parity_shard_count,
13440 image_length: tuple.image_length,
13441 archive_uuid_hint: locator.archive_uuid_hint,
13442 session_id_hint: locator.session_id_hint,
13443 volume_index_hint: locator.volume_index_hint,
13444 image_sha256,
13445 header_crc32c: 0,
13446 };
13447 let mut cmra = Vec::new();
13448 cmra.extend_from_slice(&header.to_bytes());
13449 for (idx, payload) in data_shards.into_iter().enumerate() {
13450 let payload_len = if idx + 1 == data_shard_count {
13451 image_bytes.len() - idx * shard_size
13452 } else {
13453 shard_size
13454 };
13455 cmra.extend_from_slice(
13456 &CriticalMetadataRecoveryShard {
13457 shard_index: idx as u16,
13458 shard_role: 0,
13459 shard_payload_length: payload_len as u32,
13460 payload,
13461 shard_crc32c: 0,
13462 }
13463 .to_bytes(shard_size)
13464 .unwrap(),
13465 );
13466 }
13467 for (idx, payload) in parity_shards.into_iter().enumerate() {
13468 cmra.extend_from_slice(
13469 &CriticalMetadataRecoveryShard {
13470 shard_index: (data_shard_count + idx) as u16,
13471 shard_role: 1,
13472 shard_payload_length: shard_size as u32,
13473 payload,
13474 shard_crc32c: 0,
13475 }
13476 .to_bytes(shard_size)
13477 .unwrap(),
13478 );
13479 }
13480 assert_eq!(cmra.len() as u64, recovered.cmra_length);
13481 let cmra_offset = locator.cmra_offset as usize;
13482 volume[cmra_offset..cmra_offset + cmra.len()].copy_from_slice(&cmra);
13483
13484 rewrite_locator_image_sha(volume, final_offset, image_sha256);
13485 let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
13486 rewrite_locator_image_sha(volume, mirror_offset, image_sha256);
13487 }
13488
13489 fn refresh_critical_image_region_digests(image: &mut CriticalMetadataImage) {
13490 image.volume_header_sha256 = sha256_bytes(
13491 &image
13492 .regions
13493 .iter()
13494 .find(|region| region.region_type == 1)
13495 .unwrap()
13496 .bytes,
13497 );
13498 image.crypto_header_sha256 = sha256_bytes(
13499 &image
13500 .regions
13501 .iter()
13502 .find(|region| region.region_type == 2)
13503 .unwrap()
13504 .bytes,
13505 );
13506 image.manifest_footer_sha256 = sha256_bytes(
13507 &image
13508 .regions
13509 .iter()
13510 .find(|region| region.region_type == 3)
13511 .unwrap()
13512 .bytes,
13513 );
13514 image.root_auth_footer_sha256 = image
13515 .regions
13516 .iter()
13517 .find(|region| region.region_type == 4)
13518 .map(|region| sha256_bytes(®ion.bytes))
13519 .unwrap_or([0u8; 32]);
13520 image.volume_trailer_sha256 = sha256_bytes(
13521 &image
13522 .regions
13523 .iter()
13524 .find(|region| region.region_type == 5)
13525 .unwrap()
13526 .bytes,
13527 );
13528 }
13529
13530 fn rewrite_locator_image_sha(volume: &mut [u8], offset: usize, image_sha256: [u8; 32]) {
13531 let mut locator =
13532 CriticalRecoveryLocator::parse(&volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN])
13533 .unwrap();
13534 locator.cmra_image_sha256 = image_sha256;
13535 volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN].copy_from_slice(&locator.to_bytes());
13536 }
13537
13538 fn corrupt_v41_terminal_recovery(volume: &mut [u8]) {
13539 let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
13540 let final_locator = CriticalRecoveryLocator::parse(
13541 &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
13542 )
13543 .unwrap();
13544 let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
13545 volume[final_locator.cmra_offset as usize] ^= 0x55;
13546 volume[mirror_offset] ^= 0x55;
13547 volume[final_offset] ^= 0x55;
13548 }
13549
13550 fn mutate_first_block_record(volume: &mut [u8], mutate: impl FnOnce(&mut BlockRecord)) {
13551 let (record_offset, record_len) = first_block_record(volume);
13552 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
13553 let mut record = BlockRecord::parse(
13554 &volume[record_offset..record_offset + record_len],
13555 block_size,
13556 )
13557 .unwrap();
13558 mutate(&mut record);
13559 volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
13560 }
13561
13562 fn mutate_block_record_at_slot(
13563 volume: &mut [u8],
13564 slot: usize,
13565 mutate: impl FnOnce(&mut BlockRecord),
13566 ) {
13567 let (record_offset, record_len) = block_record_at_slot(volume, slot);
13568 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
13569 let mut record = BlockRecord::parse(
13570 &volume[record_offset..record_offset + record_len],
13571 block_size,
13572 )
13573 .unwrap();
13574 mutate(&mut record);
13575 volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
13576 }
13577
13578 fn first_block_record(volume: &[u8]) -> (usize, usize) {
13579 block_record_at_slot(volume, 0)
13580 }
13581
13582 fn block_record_at_slot(volume: &[u8], slot: usize) -> (usize, usize) {
13583 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
13584 let crypto_start = volume_header.crypto_header_offset as usize;
13585 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
13586 let crypto_header = CryptoHeader::parse(
13587 &volume[crypto_start..crypto_end],
13588 volume_header.crypto_header_length,
13589 )
13590 .unwrap();
13591 let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
13592 let record_offset = crypto_end + slot * record_len;
13593 assert!(volume.len() >= record_offset + record_len);
13594 (record_offset, record_len)
13595 }
13596
13597 fn first_block_record_slot_with_kind(volume: &[u8], kind: BlockKind) -> Option<usize> {
13598 block_record_slots(volume)
13599 .into_iter()
13600 .enumerate()
13601 .find_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
13602 }
13603
13604 fn block_record_slots_with_kind(volume: &[u8], kind: BlockKind) -> Vec<usize> {
13605 block_record_slots(volume)
13606 .into_iter()
13607 .enumerate()
13608 .filter_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
13609 .collect()
13610 }
13611
13612 fn first_payload_data_run_slots(volume: &[u8]) -> Vec<usize> {
13613 let mut slots = Vec::new();
13614 for (slot, (_, _, record)) in block_record_slots(volume).into_iter().enumerate() {
13615 if record.kind == BlockKind::PayloadData {
13616 slots.push(slot);
13617 } else if !slots.is_empty() {
13618 break;
13619 }
13620 }
13621 slots
13622 }
13623
13624 fn envelope_indices_for_path(opened: &OpenedArchive, path: &str) -> BTreeSet<u64> {
13625 envelope_entries_for_path(opened, path)
13626 .into_iter()
13627 .map(|entry| entry.envelope_index)
13628 .collect()
13629 }
13630
13631 fn envelope_entries_for_path(opened: &OpenedArchive, path: &str) -> Vec<EnvelopeEntry> {
13632 let normalized =
13633 normalize_lookup_file_path(path, opened.crypto_header.max_path_length).unwrap();
13634 let located = opened.locate_index_file(&normalized).unwrap().unwrap();
13635 let file = &located.shard.files[located.file_index];
13636 frame_range_for_file(&located.shard, file)
13637 .unwrap()
13638 .into_iter()
13639 .map(|frame| {
13640 located
13641 .shard
13642 .envelopes
13643 .iter()
13644 .find(|entry| entry.envelope_index == frame.envelope_index)
13645 .unwrap()
13646 .clone()
13647 })
13648 .collect()
13649 }
13650
13651 fn block_record_slots(volume: &[u8]) -> Vec<(usize, usize, BlockRecord)> {
13652 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
13653 let crypto_start = volume_header.crypto_header_offset as usize;
13654 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
13655 let crypto_header = CryptoHeader::parse(
13656 &volume[crypto_start..crypto_end],
13657 volume_header.crypto_header_length,
13658 )
13659 .unwrap();
13660 let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
13661 let manifest_offset = terminal_material_offset(volume);
13662 assert_eq!((manifest_offset - crypto_end) % record_len, 0);
13663 let record_count = (manifest_offset - crypto_end) / record_len;
13664 (0..record_count)
13665 .map(|slot| {
13666 let offset = crypto_end + slot * record_len;
13667 let record = BlockRecord::parse(
13668 &volume[offset..offset + record_len],
13669 record_len - BLOCK_RECORD_FRAMING_LEN,
13670 )
13671 .unwrap();
13672 (offset, record_len, record)
13673 })
13674 .collect()
13675 }
13676
13677 fn rewrite_manifest_footer(
13678 volume: &mut [u8],
13679 master_key: &MasterKey,
13680 mutate: impl FnOnce(&mut ManifestFooter),
13681 ) {
13682 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
13683 let offset = terminal_material_offset(volume);
13684 let mut footer =
13685 ManifestFooter::parse(&volume[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
13686 mutate(&mut footer);
13687 footer.manifest_hmac = [0u8; 32];
13688 let mut footer_bytes = footer.to_bytes();
13689 let subkeys = Subkeys::derive(
13690 master_key,
13691 &volume_header.archive_uuid,
13692 &volume_header.session_id,
13693 )
13694 .unwrap();
13695 footer.manifest_hmac = compute_hmac(
13696 HmacDomain::ManifestFooter,
13697 &subkeys.mac_key,
13698 &volume_header.archive_uuid,
13699 &volume_header.session_id,
13700 &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
13701 );
13702 footer_bytes = footer.to_bytes();
13703 volume[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
13704 }
13705
13706 fn rewrite_volume_trailer(
13707 volume: &mut [u8],
13708 master_key: &MasterKey,
13709 mutate: impl FnOnce(&mut VolumeTrailer),
13710 ) {
13711 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
13712 let offset = terminal_material_offset(volume) + MANIFEST_FOOTER_LEN;
13713 let mut trailer =
13714 VolumeTrailer::parse(&volume[offset..offset + VOLUME_TRAILER_LEN]).unwrap();
13715 mutate(&mut trailer);
13716 trailer.trailer_hmac = [0u8; 32];
13717 let mut trailer_bytes = trailer.to_bytes();
13718 let subkeys = Subkeys::derive(
13719 master_key,
13720 &volume_header.archive_uuid,
13721 &volume_header.session_id,
13722 )
13723 .unwrap();
13724 trailer.trailer_hmac = compute_hmac(
13725 HmacDomain::VolumeTrailer,
13726 &subkeys.mac_key,
13727 &volume_header.archive_uuid,
13728 &volume_header.session_id,
13729 &trailer_bytes[..TRAILER_HMAC_COVERED_LEN],
13730 );
13731 trailer_bytes = trailer.to_bytes();
13732 volume[offset..offset + VOLUME_TRAILER_LEN].copy_from_slice(&trailer_bytes);
13733 }
13734
13735 fn rewrite_sidecar_header(
13736 sidecar: &mut [u8],
13737 master_key: &MasterKey,
13738 mutate: impl FnOnce(&mut BootstrapSidecarHeader),
13739 ) {
13740 let mut header =
13741 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
13742 mutate(&mut header);
13743 write_signed_sidecar_header(sidecar, master_key, &mut header);
13744 }
13745
13746 fn write_signed_sidecar_header(
13747 sidecar: &mut [u8],
13748 master_key: &MasterKey,
13749 header: &mut BootstrapSidecarHeader,
13750 ) {
13751 header.sidecar_hmac = [0u8; 32];
13752 let mut header_bytes = header.to_bytes();
13753 let subkeys =
13754 Subkeys::derive(master_key, &header.archive_uuid, &header.session_id).unwrap();
13755 header.sidecar_hmac = compute_hmac(
13756 HmacDomain::BootstrapSidecar,
13757 &subkeys.mac_key,
13758 &header.archive_uuid,
13759 &header.session_id,
13760 &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
13761 );
13762 header_bytes = header.to_bytes();
13763 sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
13764 }
13765
13766 fn sparse_bootstrap_sidecar(
13767 source: &[u8],
13768 master_key: &MasterKey,
13769 include_manifest: bool,
13770 include_index_root: bool,
13771 include_dictionary: bool,
13772 ) -> Vec<u8> {
13773 let source_header =
13774 BootstrapSidecarHeader::parse(&source[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
13775 let mut sidecar = vec![0u8; BOOTSTRAP_SIDECAR_HEADER_LEN];
13776 let mut header = BootstrapSidecarHeader {
13777 archive_uuid: source_header.archive_uuid,
13778 session_id: source_header.session_id,
13779 flags: 0,
13780 manifest_footer_offset: 0,
13781 manifest_footer_length: 0,
13782 index_root_records_offset: 0,
13783 index_root_records_length: 0,
13784 dictionary_records_offset: 0,
13785 dictionary_records_length: 0,
13786 sidecar_hmac: [0u8; 32],
13787 header_crc32c: 0,
13788 };
13789
13790 if include_manifest {
13791 assert!(source_header.has_manifest_footer());
13792 let (offset, length) = append_sidecar_section(
13793 source,
13794 &mut sidecar,
13795 source_header.manifest_footer_offset,
13796 source_header.manifest_footer_length as u64,
13797 );
13798 header.flags |= 0x01;
13799 header.manifest_footer_offset = offset;
13800 header.manifest_footer_length = length as u32;
13801 }
13802 if include_index_root {
13803 assert!(source_header.has_index_root_records());
13804 let (offset, length) = append_sidecar_section(
13805 source,
13806 &mut sidecar,
13807 source_header.index_root_records_offset,
13808 source_header.index_root_records_length,
13809 );
13810 header.flags |= 0x02;
13811 header.index_root_records_offset = offset;
13812 header.index_root_records_length = length;
13813 }
13814 if include_dictionary {
13815 assert!(source_header.has_dictionary_records());
13816 let (offset, length) = append_sidecar_section(
13817 source,
13818 &mut sidecar,
13819 source_header.dictionary_records_offset,
13820 source_header.dictionary_records_length,
13821 );
13822 header.flags |= 0x04;
13823 header.dictionary_records_offset = offset;
13824 header.dictionary_records_length = length;
13825 }
13826
13827 write_signed_sidecar_header(&mut sidecar, master_key, &mut header);
13828 sidecar
13829 }
13830
13831 fn append_sidecar_section(
13832 source: &[u8],
13833 sidecar: &mut Vec<u8>,
13834 source_offset: u64,
13835 length: u64,
13836 ) -> (u64, u64) {
13837 let source_offset = source_offset as usize;
13838 let length = length as usize;
13839 let offset = sidecar.len() as u64;
13840 sidecar.extend_from_slice(&source[source_offset..source_offset + length]);
13841 (offset, length as u64)
13842 }
13843
13844 fn mutate_sidecar_manifest(
13845 sidecar: &mut [u8],
13846 master_key: &MasterKey,
13847 mutate: impl FnOnce(&mut ManifestFooter),
13848 ) {
13849 let header =
13850 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
13851 let offset = header.manifest_footer_offset as usize;
13852 let mut footer =
13853 ManifestFooter::parse(&sidecar[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
13854 mutate(&mut footer);
13855 footer.manifest_hmac = [0u8; 32];
13856 let mut footer_bytes = footer.to_bytes();
13857 let subkeys =
13858 Subkeys::derive(master_key, &footer.archive_uuid, &footer.session_id).unwrap();
13859 footer.manifest_hmac = compute_hmac(
13860 HmacDomain::ManifestFooter,
13861 &subkeys.mac_key,
13862 &footer.archive_uuid,
13863 &footer.session_id,
13864 &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
13865 );
13866 footer_bytes = footer.to_bytes();
13867 sidecar[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
13868 }
13869
13870 fn mutate_sidecar_index_record(
13871 sidecar: &mut [u8],
13872 record_index: usize,
13873 mutate: impl FnOnce(&mut BlockRecord),
13874 ) {
13875 let header =
13876 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
13877 let record_len = sidecar_record_len(sidecar);
13878 let offset = header.index_root_records_offset as usize + record_index * record_len;
13879 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
13880 let mut record =
13881 BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
13882 mutate(&mut record);
13883 sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
13884 }
13885
13886 fn mutate_sidecar_dictionary_record(
13887 sidecar: &mut [u8],
13888 record_index: usize,
13889 mutate: impl FnOnce(&mut BlockRecord),
13890 ) {
13891 let header =
13892 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
13893 let record_len = sidecar_record_len(sidecar);
13894 let offset = header.dictionary_records_offset as usize + record_index * record_len;
13895 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
13896 let mut record =
13897 BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
13898 mutate(&mut record);
13899 sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
13900 }
13901
13902 fn swap_sidecar_index_records(sidecar: &mut [u8], left: usize, right: usize) {
13903 let header =
13904 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
13905 let record_len = sidecar_record_len(sidecar);
13906 let left_offset = header.index_root_records_offset as usize + left * record_len;
13907 let right_offset = header.index_root_records_offset as usize + right * record_len;
13908 for idx in 0..record_len {
13909 sidecar.swap(left_offset + idx, right_offset + idx);
13910 }
13911 }
13912
13913 fn sidecar_record_len(sidecar: &[u8]) -> usize {
13914 let header =
13915 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
13916 let footer_offset = header.manifest_footer_offset as usize;
13917 let footer =
13918 ManifestFooter::parse(&sidecar[footer_offset..footer_offset + MANIFEST_FOOTER_LEN])
13919 .unwrap();
13920 let index_record_count = footer.index_root_data_block_count as usize
13921 + footer.index_root_parity_block_count as usize;
13922 header.index_root_records_length as usize / index_record_count
13923 }
13924
13925 fn corrupt_object_extent_records(volume: &mut [u8], extent: ObjectExtent) {
13926 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
13927 assert_eq!(volume_header.volume_index, 0);
13928 assert_eq!(volume_header.stripe_width, 1);
13929 let crypto_start = volume_header.crypto_header_offset as usize;
13930 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
13931 let crypto_header = CryptoHeader::parse(
13932 &volume[crypto_start..crypto_end],
13933 volume_header.crypto_header_length,
13934 )
13935 .unwrap();
13936 let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
13937 let record_count = extent.data_block_count as u64 + extent.parity_block_count as u64;
13938 for offset in 0..record_count {
13939 let block_index = extent.first_block_index + offset;
13940 let record_offset = crypto_end + block_index as usize * record_len;
13941 volume[record_offset + 16] ^= 0x55;
13942 }
13943 }
13944
13945 fn terminal_material_offset(volume: &[u8]) -> usize {
13946 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
13947 let crypto_start = volume_header.crypto_header_offset as usize;
13948 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
13949 let crypto_header = CryptoHeader::parse(
13950 &volume[crypto_start..crypto_end],
13951 volume_header.crypto_header_length,
13952 )
13953 .unwrap();
13954 let (_, offset, _) = parse_stream_block_prefix(
13955 volume,
13956 crypto_end,
13957 crypto_header.fixed.block_size as usize,
13958 &volume_header,
13959 )
13960 .unwrap();
13961 offset
13962 }
13963
13964 #[derive(Debug)]
13965 struct TestObject {
13966 extent: ObjectExtent,
13967 records: Vec<BlockRecord>,
13968 }
13969
13970 #[derive(Debug)]
13971 struct TestFileMeta {
13972 path: Vec<u8>,
13973 frame_index: u64,
13974 tar_stream_offset: u64,
13975 member_group_size: u64,
13976 file_data_size: u64,
13977 }
13978
13979 fn multi_envelope_reader_fixture() -> (OpenedArchive, u64) {
13980 let volume_header = test_volume_header();
13981 let crypto_header = test_crypto_header();
13982 let subkeys = Subkeys::derive(
13983 &master_key(),
13984 &volume_header.archive_uuid,
13985 &volume_header.session_id,
13986 )
13987 .unwrap();
13988 let mut next_block_index = 0u64;
13989 let mut blocks = BTreeMap::new();
13990
13991 let healthy = test_member(b"healthy.txt", b"healthy payload\n");
13992 let broken = test_member(b"broken.txt", b"broken payload\n");
13993 let tar_stream = [healthy.as_slice(), broken.as_slice()].concat();
13994
13995 let healthy_frame = compress_zstd_frame(&healthy, 1).unwrap();
13996 let broken_frame = compress_zstd_frame(&broken, 1).unwrap();
13997
13998 let healthy_payload = encrypt_test_object(
13999 &healthy_frame,
14000 &subkeys.enc_key,
14001 &subkeys.nonce_seed,
14002 b"envelope",
14003 0,
14004 BlockKind::PayloadData,
14005 &mut next_block_index,
14006 &crypto_header,
14007 &volume_header,
14008 );
14009 let broken_payload = encrypt_test_object(
14010 &broken_frame,
14011 &subkeys.enc_key,
14012 &subkeys.nonce_seed,
14013 b"envelope",
14014 1,
14015 BlockKind::PayloadData,
14016 &mut next_block_index,
14017 &crypto_header,
14018 &volume_header,
14019 );
14020 let broken_payload_block = broken_payload.extent.first_block_index;
14021 insert_records(&mut blocks, &healthy_payload.records);
14022 insert_records(&mut blocks, &broken_payload.records);
14023
14024 let frames = vec![
14025 FrameEntry {
14026 frame_index: 0,
14027 envelope_index: 0,
14028 offset_in_envelope: 0,
14029 compressed_size: healthy_frame.len() as u32,
14030 decompressed_size: healthy.len() as u32,
14031 flags: 0x0000_0003,
14032 tar_stream_offset: 0,
14033 },
14034 FrameEntry {
14035 frame_index: 1,
14036 envelope_index: 1,
14037 offset_in_envelope: 0,
14038 compressed_size: broken_frame.len() as u32,
14039 decompressed_size: broken.len() as u32,
14040 flags: 0x0000_0003,
14041 tar_stream_offset: healthy.len() as u64,
14042 },
14043 ];
14044 let envelopes = vec![
14045 EnvelopeEntry {
14046 envelope_index: 0,
14047 first_block_index: healthy_payload.extent.first_block_index,
14048 data_block_count: healthy_payload.extent.data_block_count,
14049 parity_block_count: 0,
14050 encrypted_size: healthy_payload.extent.encrypted_size,
14051 plaintext_size: healthy_frame.len() as u32,
14052 first_frame_index: 0,
14053 frame_count: 1,
14054 },
14055 EnvelopeEntry {
14056 envelope_index: 1,
14057 first_block_index: broken_payload.extent.first_block_index,
14058 data_block_count: broken_payload.extent.data_block_count,
14059 parity_block_count: 0,
14060 encrypted_size: broken_payload.extent.encrypted_size,
14061 plaintext_size: broken_frame.len() as u32,
14062 first_frame_index: 1,
14063 frame_count: 1,
14064 },
14065 ];
14066 let files = vec![
14067 TestFileMeta {
14068 path: b"healthy.txt".to_vec(),
14069 frame_index: 0,
14070 tar_stream_offset: 0,
14071 member_group_size: healthy.len() as u64,
14072 file_data_size: b"healthy payload\n".len() as u64,
14073 },
14074 TestFileMeta {
14075 path: b"broken.txt".to_vec(),
14076 frame_index: 1,
14077 tar_stream_offset: healthy.len() as u64,
14078 member_group_size: broken.len() as u64,
14079 file_data_size: b"broken payload\n".len() as u64,
14080 },
14081 ];
14082
14083 let (index_shard_plaintext, first_path_hash, last_path_hash) =
14084 build_test_index_shard(&files, &frames, &envelopes);
14085 let index_shard = encrypt_test_object(
14086 &compress_zstd_frame(&index_shard_plaintext, 1).unwrap(),
14087 &subkeys.index_shard_key,
14088 &subkeys.index_nonce_seed,
14089 b"idxshard",
14090 0,
14091 BlockKind::IndexShardData,
14092 &mut next_block_index,
14093 &crypto_header,
14094 &volume_header,
14095 );
14096 insert_records(&mut blocks, &index_shard.records);
14097
14098 let shard_entry = ShardEntry {
14099 shard_index: 0,
14100 first_block_index: index_shard.extent.first_block_index,
14101 data_block_count: index_shard.extent.data_block_count,
14102 parity_block_count: 0,
14103 encrypted_size: index_shard.extent.encrypted_size,
14104 decompressed_size: index_shard_plaintext.len() as u32,
14105 file_count: files.len() as u32,
14106 first_path_hash,
14107 last_path_hash,
14108 };
14109 let mut root_header = IndexRootHeader::empty();
14110 root_header.frame_count = frames.len() as u64;
14111 root_header.envelope_count = envelopes.len() as u64;
14112 root_header.file_count = files.len() as u64;
14113 root_header.payload_block_count = healthy_payload.extent.data_block_count as u64
14114 + broken_payload.extent.data_block_count as u64;
14115 root_header.tar_total_size = tar_stream.len() as u64;
14116 root_header.content_sha256 = sha256_bytes(&tar_stream);
14117 let index_root = IndexRoot {
14118 header: root_header,
14119 shards: vec![shard_entry],
14120 directory_hint_shards: Vec::new(),
14121 };
14122
14123 let index_root_plaintext = index_root.to_bytes();
14124 let index_root_object = encrypt_test_object(
14125 &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
14126 &subkeys.index_root_key,
14127 &subkeys.index_nonce_seed,
14128 b"idxroot",
14129 0,
14130 BlockKind::IndexRootData,
14131 &mut next_block_index,
14132 &crypto_header,
14133 &volume_header,
14134 );
14135 insert_records(&mut blocks, &index_root_object.records);
14136
14137 let archive_uuid = volume_header.archive_uuid;
14138 let session_id = volume_header.session_id;
14139 let opened = OpenedArchive {
14140 options: ReaderOptions::default(),
14141 observed_archive_bytes: 1_000_000,
14142 observed_volume_count: 1,
14143 subkeys,
14144 blocks,
14145 lazy_blocks: None,
14146 crypto_header_bytes: Vec::new(),
14147 volume_header,
14148 crypto_header,
14149 manifest_footer: ManifestFooter {
14150 archive_uuid,
14151 session_id,
14152 volume_index: 0,
14153 is_authoritative: 1,
14154 total_volumes: 1,
14155 index_root_first_block: index_root_object.extent.first_block_index,
14156 index_root_data_block_count: index_root_object.extent.data_block_count,
14157 index_root_parity_block_count: 0,
14158 index_root_encrypted_size: index_root_object.extent.encrypted_size,
14159 index_root_decompressed_size: index_root_plaintext.len() as u32,
14160 manifest_hmac: [0u8; 32],
14161 },
14162 volume_trailer: Some(VolumeTrailer {
14163 archive_uuid,
14164 session_id,
14165 volume_index: 0,
14166 block_count: next_block_index,
14167 bytes_written: 0,
14168 manifest_footer_offset: 0,
14169 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
14170 closed_at_ns: 0,
14171 root_auth_footer_offset: 0,
14172 root_auth_footer_length: 0,
14173 root_auth_flags: 0,
14174 trailer_hmac: [0u8; 32],
14175 }),
14176 root_auth_footer: None,
14177 index_root,
14178 payload_dictionary: None,
14179 };
14180 (opened, broken_payload_block)
14181 }
14182
14183 fn replace_first_index_shard(opened: &mut OpenedArchive, mutate: impl FnOnce(&mut IndexShard)) {
14184 let locating = opened.index_root.shards[0].clone();
14185 let mut shard = opened.load_index_shard(&locating).unwrap();
14186 mutate(&mut shard);
14187 let plaintext = shard.to_bytes();
14188 let mut next_block_index = opened
14189 .blocks
14190 .keys()
14191 .last()
14192 .copied()
14193 .map(|index| index + 1)
14194 .unwrap_or(0);
14195 let replacement = encrypt_test_object(
14196 &compress_zstd_frame(&plaintext, 1).unwrap(),
14197 &opened.subkeys.index_shard_key,
14198 &opened.subkeys.index_nonce_seed,
14199 b"idxshard",
14200 locating.shard_index,
14201 BlockKind::IndexShardData,
14202 &mut next_block_index,
14203 &opened.crypto_header,
14204 &opened.volume_header,
14205 );
14206 insert_records(&mut opened.blocks, &replacement.records);
14207 opened.index_root.shards[0] = ShardEntry {
14208 shard_index: locating.shard_index,
14209 first_block_index: replacement.extent.first_block_index,
14210 data_block_count: replacement.extent.data_block_count,
14211 parity_block_count: 0,
14212 encrypted_size: replacement.extent.encrypted_size,
14213 decompressed_size: plaintext.len() as u32,
14214 file_count: shard.files.len() as u32,
14215 first_path_hash: shard.files.first().unwrap().path_hash,
14216 last_path_hash: shard.files.last().unwrap().path_hash,
14217 };
14218 }
14219
14220 fn rewrite_as_single_healthy_file(
14221 opened: &mut OpenedArchive,
14222 mutate: impl FnOnce(&mut FileEntry, &mut Vec<u8>),
14223 ) {
14224 let healthy_path = b"healthy.txt";
14225 let healthy_payload = b"healthy payload\n";
14226 let healthy_member = test_member(healthy_path, healthy_payload);
14227 replace_first_index_shard(opened, |shard| {
14228 let file_index = (0..shard.files.len())
14229 .find(|idx| shard.file_path(*idx) == Some(healthy_path.as_slice()))
14230 .unwrap();
14231 let mut file = shard.files[file_index].clone();
14232 let frame = shard
14233 .frames
14234 .iter()
14235 .find(|entry| entry.frame_index == 0)
14236 .unwrap()
14237 .clone();
14238 let envelope = shard
14239 .envelopes
14240 .iter()
14241 .find(|entry| entry.envelope_index == 0)
14242 .unwrap()
14243 .clone();
14244 let mut path = healthy_path.to_vec();
14245
14246 file.path_offset = 0;
14247 file.path_length = path.len() as u32;
14248 file.first_frame_index = 0;
14249 file.frame_count = 1;
14250 file.offset_in_first_frame_plaintext = 0;
14251 file.tar_member_group_size = healthy_member.len() as u64;
14252 file.file_data_size = healthy_payload.len() as u64;
14253 file.flags = 0;
14254 mutate(&mut file, &mut path);
14255 file.path_offset = 0;
14256 file.path_length = path.len() as u32;
14257 file.path_hash = hash_prefix(&path);
14258
14259 shard.files = vec![file];
14260 shard.frames = vec![frame];
14261 shard.envelopes = vec![envelope];
14262 shard.string_pool = path;
14263 });
14264
14265 opened.index_root.header.file_count = 1;
14266 opened.index_root.header.frame_count = 1;
14267 opened.index_root.header.envelope_count = 1;
14268 opened.index_root.header.payload_block_count = 1;
14269 opened.index_root.header.tar_total_size = healthy_member.len() as u64;
14270 opened.index_root.header.content_sha256 = sha256_bytes(&healthy_member);
14271 }
14272
14273 fn test_volume_header() -> VolumeHeader {
14274 VolumeHeader {
14275 format_version: FORMAT_VERSION,
14276 volume_format_rev: VOLUME_FORMAT_REV,
14277 volume_index: 0,
14278 stripe_width: 1,
14279 archive_uuid: [0x31; 16],
14280 session_id: [0x42; 16],
14281 crypto_header_offset: VOLUME_HEADER_LEN as u32,
14282 crypto_header_length: CRYPTO_HEADER_FIXED_LEN as u32,
14283 header_crc32c: 0,
14284 }
14285 }
14286
14287 fn test_crypto_header() -> CryptoHeaderFixed {
14288 CryptoHeaderFixed {
14289 length: CRYPTO_HEADER_FIXED_LEN as u32,
14290 compression_algo: CompressionAlgo::ZstdFramed,
14291 aead_algo: AeadAlgo::AesGcmSiv256,
14292 fec_algo: FecAlgo::ReedSolomonGF16,
14293 kdf_algo: KdfAlgo::Raw,
14294 chunk_size: 4096,
14295 envelope_target_size: 8192,
14296 block_size: 4096,
14297 fec_data_shards: 4,
14298 fec_parity_shards: 0,
14299 index_fec_data_shards: 4,
14300 index_fec_parity_shards: 0,
14301 index_root_fec_data_shards: 4,
14302 index_root_fec_parity_shards: 0,
14303 stripe_width: 1,
14304 volume_loss_tolerance: 0,
14305 bit_rot_buffer_pct: 0,
14306 has_dictionary: 0,
14307 max_path_length: 4096,
14308 expected_volume_size: 0,
14309 }
14310 }
14311
14312 #[allow(clippy::too_many_arguments)]
14313 fn encrypt_test_object(
14314 plaintext: &[u8],
14315 key: &[u8; 32],
14316 nonce_seed: &[u8; 32],
14317 domain: &[u8],
14318 counter: u64,
14319 data_kind: BlockKind,
14320 next_block_index: &mut u64,
14321 crypto_header: &CryptoHeaderFixed,
14322 volume_header: &VolumeHeader,
14323 ) -> TestObject {
14324 let block_size = crypto_header.block_size as usize;
14325 let encrypted = encrypt_padded_aead_object(
14326 AeadObjectContext {
14327 algo: crypto_header.aead_algo,
14328 key,
14329 nonce_seed,
14330 domain,
14331 archive_uuid: &volume_header.archive_uuid,
14332 session_id: &volume_header.session_id,
14333 counter,
14334 },
14335 block_size,
14336 plaintext,
14337 )
14338 .unwrap();
14339 assert_eq!(encrypted.len() % block_size, 0);
14340
14341 let first_block_index = *next_block_index;
14342 let data_block_count = encrypted.len() / block_size;
14343 let records = encrypted
14344 .chunks(block_size)
14345 .enumerate()
14346 .map(|(index, payload)| BlockRecord {
14347 block_index: first_block_index + index as u64,
14348 kind: data_kind,
14349 flags: if index + 1 == data_block_count {
14350 0x01
14351 } else {
14352 0
14353 },
14354 payload: payload.to_vec(),
14355 record_crc32c: 0,
14356 })
14357 .collect::<Vec<_>>();
14358 *next_block_index += data_block_count as u64;
14359
14360 TestObject {
14361 extent: ObjectExtent {
14362 first_block_index,
14363 data_block_count: data_block_count as u32,
14364 parity_block_count: 0,
14365 encrypted_size: encrypted.len() as u32,
14366 },
14367 records,
14368 }
14369 }
14370
14371 fn insert_records(blocks: &mut BTreeMap<u64, BlockRecord>, records: &[BlockRecord]) {
14372 for record in records {
14373 assert!(blocks.insert(record.block_index, record.clone()).is_none());
14374 }
14375 }
14376
14377 #[allow(clippy::too_many_arguments)]
14378 fn build_metadata_object_from_payload(
14379 payload: &[u8],
14380 _subkeys: &Subkeys,
14381 volume_header: &VolumeHeader,
14382 crypto_header: &CryptoHeaderFixed,
14383 key: &[u8; 32],
14384 nonce_seed: &[u8; 32],
14385 domain: &[u8],
14386 counter: u64,
14387 data_kind: BlockKind,
14388 next_block_index: &mut u64,
14389 ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
14390 let compressed = compress_zstd_frame(payload, 1).unwrap();
14391 build_metadata_object_from_compressed(
14392 &compressed,
14393 key,
14394 nonce_seed,
14395 domain,
14396 counter,
14397 data_kind,
14398 next_block_index,
14399 crypto_header,
14400 volume_header,
14401 )
14402 }
14403
14404 #[allow(clippy::too_many_arguments)]
14405 fn build_metadata_object_from_compressed(
14406 compressed: &[u8],
14407 key: &[u8; 32],
14408 nonce_seed: &[u8; 32],
14409 domain: &[u8],
14410 counter: u64,
14411 data_kind: BlockKind,
14412 next_block_index: &mut u64,
14413 crypto_header: &CryptoHeaderFixed,
14414 volume_header: &VolumeHeader,
14415 ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
14416 let object = encrypt_test_object(
14417 compressed,
14418 key,
14419 nonce_seed,
14420 domain,
14421 counter,
14422 data_kind,
14423 next_block_index,
14424 crypto_header,
14425 volume_header,
14426 );
14427
14428 let mut blocks = BTreeMap::new();
14429 for record in object.records {
14430 blocks.insert(record.block_index, record);
14431 }
14432 (object.extent, blocks)
14433 }
14434
14435 #[allow(clippy::too_many_arguments)]
14436 fn assert_metadata_object_from_compressed(
14437 compressed: &[u8],
14438 decompressed_size: usize,
14439 _subkeys: &Subkeys,
14440 volume_header: &VolumeHeader,
14441 crypto_header: &CryptoHeaderFixed,
14442 key: &[u8; 32],
14443 nonce_seed: &[u8; 32],
14444 domain: &[u8],
14445 counter: u64,
14446 data_kind: BlockKind,
14447 parity_kind: BlockKind,
14448 class_data_shards: u16,
14449 class_parity_shards: u16,
14450 next_block_index: &mut u64,
14451 expected: FormatError,
14452 ) {
14453 let (extent, blocks) = build_metadata_object_from_compressed(
14454 compressed,
14455 key,
14456 nonce_seed,
14457 domain,
14458 counter,
14459 data_kind,
14460 next_block_index,
14461 crypto_header,
14462 volume_header,
14463 );
14464 let error = load_metadata_object_from_parts(
14465 &blocks,
14466 ObjectLoadContext {
14467 volume_header,
14468 crypto_header,
14469 extent,
14470 data_kind,
14471 parity_kind,
14472 key,
14473 nonce_seed,
14474 domain,
14475 counter,
14476 class_data_shard_max: class_data_shards,
14477 class_parity_shard_max: class_parity_shards,
14478 },
14479 decompressed_size as u32,
14480 )
14481 .unwrap_err();
14482 assert_eq!(error, expected);
14483 }
14484
14485 fn corrupt_payload_record(blocks: &mut BTreeMap<u64, BlockRecord>, block_index: u64) {
14486 let record = blocks.get_mut(&block_index).unwrap();
14487 assert_eq!(record.kind, BlockKind::PayloadData);
14488 record.payload[0] ^= 0x55;
14489 }
14490
14491 fn build_test_index_shard(
14492 files: &[TestFileMeta],
14493 frames: &[FrameEntry],
14494 envelopes: &[EnvelopeEntry],
14495 ) -> (Vec<u8>, [u8; 8], [u8; 8]) {
14496 let mut sorted = files
14497 .iter()
14498 .map(|file| (hash_prefix(&file.path), file))
14499 .collect::<Vec<_>>();
14500 sorted.sort_by(|left, right| {
14501 (left.0, left.1.path.as_slice(), left.1.tar_stream_offset).cmp(&(
14502 right.0,
14503 right.1.path.as_slice(),
14504 right.1.tar_stream_offset,
14505 ))
14506 });
14507
14508 let mut string_pool = Vec::new();
14509 let mut file_entries = Vec::with_capacity(sorted.len());
14510 for (path_hash, file) in &sorted {
14511 let path_offset = string_pool.len() as u32;
14512 string_pool.extend_from_slice(&file.path);
14513 file_entries.push(FileEntry {
14514 path_hash: *path_hash,
14515 path_offset,
14516 path_length: file.path.len() as u32,
14517 first_frame_index: file.frame_index,
14518 frame_count: 1,
14519 offset_in_first_frame_plaintext: 0,
14520 tar_member_group_size: file.member_group_size,
14521 file_data_size: file.file_data_size,
14522 flags: 0,
14523 });
14524 }
14525
14526 let header = IndexShardHeader {
14527 version: 1,
14528 shard_index: 0,
14529 file_count: file_entries.len() as u32,
14530 frame_count: frames.len() as u32,
14531 envelope_count: envelopes.len() as u32,
14532 file_table_offset: INDEX_SHARD_HEADER_LEN as u32,
14533 frame_table_offset: (INDEX_SHARD_HEADER_LEN + file_entries.len() * FILE_ENTRY_LEN)
14534 as u32,
14535 envelope_table_offset: (INDEX_SHARD_HEADER_LEN
14536 + file_entries.len() * FILE_ENTRY_LEN
14537 + frames.len() * FRAME_ENTRY_LEN) as u32,
14538 string_pool_offset: (INDEX_SHARD_HEADER_LEN
14539 + file_entries.len() * FILE_ENTRY_LEN
14540 + frames.len() * FRAME_ENTRY_LEN
14541 + envelopes.len() * ENVELOPE_ENTRY_LEN) as u32,
14542 string_pool_size: string_pool.len() as u32,
14543 };
14544
14545 let mut bytes = Vec::new();
14546 bytes.extend_from_slice(&header.to_bytes());
14547 for entry in &file_entries {
14548 bytes.extend_from_slice(&entry.to_bytes());
14549 }
14550 for entry in frames {
14551 bytes.extend_from_slice(&entry.to_bytes());
14552 }
14553 for entry in envelopes {
14554 bytes.extend_from_slice(&entry.to_bytes());
14555 }
14556 bytes.extend_from_slice(&string_pool);
14557
14558 (bytes, sorted.first().unwrap().0, sorted.last().unwrap().0)
14559 }
14560
14561 fn test_member(path: &[u8], data: &[u8]) -> Vec<u8> {
14562 let mut out = Vec::new();
14563 out.extend_from_slice(&test_tar_header(path, data.len() as u64));
14564 out.extend_from_slice(data);
14565 out.resize(out.len() + padding_to_512(data.len()), 0);
14566 out
14567 }
14568
14569 fn test_tar_header(path: &[u8], size: u64) -> [u8; 512] {
14570 let mut header = [0u8; 512];
14571 header[..path.len()].copy_from_slice(path);
14572 write_test_tar_octal(&mut header[100..108], 0o644);
14573 write_test_tar_octal(&mut header[108..116], 0);
14574 write_test_tar_octal(&mut header[116..124], 0);
14575 write_test_tar_octal(&mut header[124..136], size);
14576 write_test_tar_octal(&mut header[136..148], 0);
14577 header[148..156].fill(b' ');
14578 header[156] = b'0';
14579 header[257..263].copy_from_slice(b"ustar\0");
14580 header[263..265].copy_from_slice(b"00");
14581 let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
14582 write_test_tar_checksum(&mut header[148..156], checksum);
14583 header
14584 }
14585
14586 fn write_test_tar_octal(field: &mut [u8], value: u64) {
14587 let digits = format!("{value:o}");
14588 field.fill(0);
14589 let start = field.len() - 1 - digits.len();
14590 field[..start].fill(b'0');
14591 field[start..start + digits.len()].copy_from_slice(digits.as_bytes());
14592 }
14593
14594 fn write_test_tar_checksum(field: &mut [u8], value: u64) {
14595 let digits = format!("{value:06o}");
14596 field[0..6].copy_from_slice(digits.as_bytes());
14597 field[6] = 0;
14598 field[7] = b' ';
14599 }
14600
14601 fn padding_to_512(len: usize) -> usize {
14602 let remainder = len % 512;
14603 if remainder == 0 {
14604 0
14605 } else {
14606 512 - remainder
14607 }
14608 }
14609}