1use std::collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap};
2use std::fs::File;
3use std::io::{Read, Write};
4use std::sync::Arc;
5use std::thread;
6
7use sha2::{Digest, Sha256};
8
9use crate::compression::{decompress_exact_zstd_frame, validate_exact_zstd_frame};
10use crate::crypto::{
11 decrypt_padded_aead_object, verify_integrity_tag, AeadObjectContext, HmacDomain, KdfParams,
12 MasterKey, Subkeys,
13};
14use crate::fec::{encode_parity_gf16, repair_data_gf16};
15use crate::format::{
16 AeadAlgo, BlockKind, ExtractError, FormatError, KdfAlgo, VolumeFormatRevision,
17 BLOCK_RECORD_FRAMING_LEN, BOOTSTRAP_SIDECAR_HEADER_LEN, CRITICAL_METADATA_IMAGE_FIXED_LEN,
18 CRITICAL_METADATA_IMAGE_FIXED_LEN_V43, CRITICAL_METADATA_RECOVERY_HEADER_LEN,
19 CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN, CRITICAL_RECOVERY_LOCATOR_LEN,
20 CRYPTO_HEADER_HMAC_LEN, IMAGE_CRC_LEN, LOCATOR_PAIR_LEN, MANIFEST_FOOTER_LEN, MASTER_KEY_LEN,
21 READER_MAX_CMRA_PARITY_PCT, READER_MAX_CRYPTO_HEADER_LEN, READER_MAX_KEY_WRAP_TABLE_LEN,
22 READER_MAX_ROOT_AUTH_FOOTER_LEN, SERIALIZED_REGION_HEADER_LEN, VOLUME_FORMAT_REV_44,
23 VOLUME_HEADER_LEN, VOLUME_TRAILER_LEN,
24};
25use crate::metadata::{
26 hash_prefix, normalize_lookup_file_path, DirectoryHintShardEntry, DirectoryHintTable,
27 EnvelopeEntry, FileEntry, FrameEntry, IndexRoot, IndexShard, MetadataLimits, ShardEntry,
28};
29use crate::non_seekable_reader::{
30 StreamedEnvelopeSummary, StreamedFrameSummary, StreamedPayloadSummary,
31};
32use crate::raw_stream_profile::reject_unsupported_raw_stream_profile;
33use crate::root_auth::{
34 archive_root_for_revision, critical_metadata_digest, data_block_merkle_root_for_revision,
35 fec_layout_digest_for_revision, index_digest_for_revision,
36 root_auth_descriptor_digest_for_revision, signer_identity_digest, ArchiveRootInputs,
37 CriticalMetadataDigestInputs, DataBlockMerkleLeaf, FecLayoutObjectRow,
38};
39use crate::tar_model::{
40 parse_tar_member_group, restore_streaming_tar_member_group,
41 stream_regular_tar_member_group_to_writer, validate_tar_stream_total_extraction_size,
42 MetadataDiagnostic, NoopTarStreamObserver, OwnedTarMember, SafeExtractionOptions, TarEntryKind,
43 TarMemberGroupReader, TarStreamFilesystemRestoreObserver, TarStreamObserver,
44 TarStreamSummaryValidator, TarStreamTotalExtractionSizeValidator,
45};
46use crate::wire::{
47 compute_key_wrap_table_digest, BlockRecord, BootstrapSidecarHeader, CriticalMetadataImage,
48 CriticalMetadataRecoveryHeader, CriticalMetadataRecoveryShard, CriticalRecoveryLocator,
49 CryptoHeader, CryptoHeaderFixed, ExtensionTlv, KeyWrapTableV1, ManifestFooter,
50 RootAuthFooterV1, VolumeHeader, VolumeTrailer,
51};
52
53const TRAILER_HMAC_COVERED_LEN: usize = 96;
54const MANIFEST_HMAC_COVERED_LEN: usize = 104;
55const SIDECAR_HMAC_COVERED_LEN: usize = 92;
56const DEFAULT_MAX_VERIFY_TAR_SIZE: usize = 128 * 1024 * 1024;
57const DEFAULT_MAX_TRAILING_GARBAGE_SCAN: usize = 1024 * 1024;
58const DEFAULT_MAX_TOTAL_EXTRACTION_SIZE: u64 = 100 * 1024 * 1024 * 1024;
59const DIRECTORY_HINT_REQUIRED_FILE_COUNT: u64 = 100_000;
60
61fn default_jobs() -> usize {
62 std::thread::available_parallelism()
63 .map(|jobs| jobs.get())
64 .unwrap_or(1)
65}
66
67pub trait ArchiveReadAt: Send + Sync + 'static {
68 fn len(&self) -> Result<u64, FormatError>;
69 fn is_empty(&self) -> Result<bool, FormatError> {
70 Ok(self.len()? == 0)
71 }
72 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError>;
73}
74
75pub type RecipientWrapCandidateMasterKey = [u8; MASTER_KEY_LEN];
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct RecipientWrapArchiveIdentity {
79 pub archive_uuid: [u8; 16],
80 pub session_id: [u8; 16],
81 pub format_version: u16,
82 pub volume_format_rev: u16,
83}
84
85#[derive(Debug, Clone, Copy)]
86pub struct RecipientWrapRecordContext<'a> {
87 pub archive_identity: RecipientWrapArchiveIdentity,
88 pub record: &'a crate::wire::RecipientRecordV1,
89}
90
91impl ArchiveReadAt for File {
92 fn len(&self) -> Result<u64, FormatError> {
93 self.metadata()
94 .map(|metadata| metadata.len())
95 .map_err(|_| FormatError::InvalidArchive("archive read metadata failed"))
96 }
97
98 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
99 file_read_exact_at(self, offset, buf)
100 }
101}
102
103#[cfg(unix)]
104fn file_read_exact_at(file: &File, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
105 use std::os::unix::fs::FileExt;
106
107 file_read_exact_at_with(offset, buf, |chunk, offset| file.read_at(chunk, offset))
108}
109
110#[cfg(windows)]
111fn file_read_exact_at(file: &File, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
112 use std::os::windows::fs::FileExt;
113
114 file_read_exact_at_with(offset, buf, |chunk, offset| file.seek_read(chunk, offset))
115}
116
117#[cfg(any(unix, windows))]
118fn file_read_exact_at_with<F>(
119 mut offset: u64,
120 mut buf: &mut [u8],
121 mut read_at: F,
122) -> Result<(), FormatError>
123where
124 F: FnMut(&mut [u8], u64) -> std::io::Result<usize>,
125{
126 while !buf.is_empty() {
127 let read =
128 read_at(buf, offset).map_err(|_| FormatError::InvalidArchive("archive read failed"))?;
129 if read == 0 {
130 return Err(FormatError::InvalidArchive("archive read failed"));
131 }
132 offset = checked_u64_add(offset, read as u64, "archive read offset overflow")?;
133 let rest = std::mem::take(&mut buf).split_at_mut(read).1;
134 buf = rest;
135 }
136 Ok(())
137}
138
139#[cfg(not(any(unix, windows)))]
140fn file_read_exact_at(file: &File, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
141 let mut file = file
142 .try_clone()
143 .map_err(|_| FormatError::InvalidArchive("archive read clone failed"))?;
144 std::io::Seek::seek(&mut file, std::io::SeekFrom::Start(offset))
145 .map_err(|_| FormatError::InvalidArchive("archive read seek failed"))?;
146 file.read_exact(buf)
147 .map_err(|_| FormatError::InvalidArchive("archive read failed"))
148}
149
150impl ArchiveReadAt for Vec<u8> {
151 fn len(&self) -> Result<u64, FormatError> {
152 u64::try_from(self.len())
153 .map_err(|_| FormatError::InvalidArchive("archive length overflow"))
154 }
155
156 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
157 let offset = to_usize(offset, "archive")?;
158 let end = checked_add(offset, buf.len(), "archive")?;
159 let source = self.get(offset..end).ok_or(FormatError::InvalidLength {
160 structure: "archive",
161 expected: end,
162 actual: self.len(),
163 })?;
164 buf.copy_from_slice(source);
165 Ok(())
166 }
167}
168
169impl<T: ArchiveReadAt + ?Sized> ArchiveReadAt for Arc<T> {
170 fn len(&self) -> Result<u64, FormatError> {
171 (**self).len()
172 }
173
174 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
175 (**self).read_exact_at(offset, buf)
176 }
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub struct ReaderOptions {
181 pub max_trailing_garbage_scan: usize,
182 pub max_verify_tar_size: usize,
183 pub max_total_extraction_size: u64,
184 pub jobs: usize,
185}
186
187impl Default for ReaderOptions {
188 fn default() -> Self {
189 Self {
190 max_trailing_garbage_scan: DEFAULT_MAX_TRAILING_GARBAGE_SCAN,
191 max_verify_tar_size: DEFAULT_MAX_VERIFY_TAR_SIZE,
192 max_total_extraction_size: DEFAULT_MAX_TOTAL_EXTRACTION_SIZE,
193 jobs: default_jobs(),
194 }
195 }
196}
197
198pub(crate) fn validate_reader_options(options: ReaderOptions) -> Result<(), FormatError> {
199 if options.jobs == 0 {
200 return Err(FormatError::ReaderUnsupported("jobs must be at least 1"));
201 }
202 Ok(())
203}
204
205#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct ArchiveEntry {
207 pub path: String,
208 pub file_data_size: u64,
209 pub kind: TarEntryKind,
210 pub mode: u32,
211 pub mtime: u64,
212 pub diagnostics: Vec<MetadataDiagnostic>,
213}
214
215#[derive(Debug, Clone, PartialEq, Eq)]
216pub struct ArchiveIndexEntry {
217 pub path: String,
218 pub file_data_size: u64,
219}
220
221#[derive(Debug, Clone, PartialEq, Eq)]
222pub struct ExtractedArchiveMember {
223 pub path: String,
224 pub kind: TarEntryKind,
225 pub data: Vec<u8>,
226 pub link_target: Option<String>,
227 pub diagnostics: Vec<MetadataDiagnostic>,
228}
229
230pub trait ArchiveExtractProgressSink {
236 fn file_bytes_extracted(&mut self, archive_path: &str, bytes: u64);
238}
239
240impl<F> ArchiveExtractProgressSink for F
241where
242 F: FnMut(&str, u64),
243{
244 fn file_bytes_extracted(&mut self, archive_path: &str, bytes: u64) {
245 self(archive_path, bytes);
246 }
247}
248
249#[derive(Debug, Clone)]
250pub struct OpenedArchive {
251 options: ReaderOptions,
252 observed_archive_bytes: u64,
253 observed_volume_count: u32,
254 subkeys: Subkeys,
255 blocks: BTreeMap<u64, BlockRecord>,
256 lazy_blocks: Option<Arc<SeekableBlockSource>>,
257 crypto_header_bytes: Vec<u8>,
258 pub volume_header: VolumeHeader,
259 pub crypto_header: CryptoHeaderFixed,
260 pub manifest_footer: ManifestFooter,
261 pub volume_trailer: Option<VolumeTrailer>,
262 pub root_auth_footer: Option<RootAuthFooterV1>,
263 pub index_root: IndexRoot,
264 payload_dictionary: Option<Vec<u8>>,
265}
266
267#[derive(Debug)]
268pub struct ArchiveContentVerification<'a> {
269 archive: &'a OpenedArchive,
270 mode: ContentVerificationMode,
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274enum ContentVerificationMode {
275 Full,
276 Fast,
277}
278
279#[derive(Debug, Clone, PartialEq, Eq)]
280pub struct ArchiveRepairPatch {
281 pub volume_index: u32,
282 pub block_index: u64,
283 pub record_offset: u64,
284 pub record_bytes: Vec<u8>,
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288pub enum RootAuthDiagnostic {
289 RootAuthContentVerified,
290 RootAuthDeferredFullArchiveScanRequired,
291 AuthenticatedMetadataNotRootSigned,
292 RecoveryMarginNotRootAuthenticated,
293 ReplicatedGlobalCopyUncheckedDueToVolumeLoss,
294 RecoveryMarginChecked,
295 RecoveryMarginFailed,
296 RecoveryMarginUnchecked,
297}
298
299impl RootAuthDiagnostic {
300 pub const fn label(self) -> &'static str {
301 match self {
302 Self::RootAuthContentVerified => "root_auth_content_verified",
303 Self::RootAuthDeferredFullArchiveScanRequired => {
304 "root_auth_deferred_full_archive_scan_required"
305 }
306 Self::AuthenticatedMetadataNotRootSigned => "authenticated_metadata_not_root_signed",
307 Self::RecoveryMarginNotRootAuthenticated => "recovery_margin_not_root_authenticated",
308 Self::ReplicatedGlobalCopyUncheckedDueToVolumeLoss => {
309 "replicated_global_copy_unchecked_due_to_volume_loss"
310 }
311 Self::RecoveryMarginChecked => "recovery_margin_checked",
312 Self::RecoveryMarginFailed => "recovery_margin_failed",
313 Self::RecoveryMarginUnchecked => "recovery_margin_unchecked",
314 }
315 }
316}
317
318#[derive(Debug, Clone, Copy, PartialEq, Eq)]
319pub enum PublicNoKeyDiagnostic {
320 PublicDataBlockCommitmentVerified,
321 PublicPhysicalCompletenessUnverified,
322 PublicRecoveryMarginUnchecked,
323}
324
325impl PublicNoKeyDiagnostic {
326 pub const fn label(self) -> &'static str {
327 match self {
328 Self::PublicDataBlockCommitmentVerified => "public_data_block_commitment_verified",
329 Self::PublicPhysicalCompletenessUnverified => "public_physical_completeness_unverified",
330 Self::PublicRecoveryMarginUnchecked => "public_recovery_margin_unchecked",
331 }
332 }
333}
334
335#[derive(Debug, Clone, PartialEq, Eq)]
336pub struct RootAuthVerification {
337 pub format_version: u16,
338 pub volume_format_rev: u16,
339 pub archive_root: [u8; 32],
340 pub authenticator_id: u16,
341 pub signer_identity_type: u16,
342 pub signer_identity_bytes: Vec<u8>,
343 pub total_data_block_count: u64,
344 pub diagnostics: Vec<RootAuthDiagnostic>,
345}
346
347#[derive(Debug, Clone, PartialEq, Eq)]
348pub struct PublicNoKeyVerification {
349 pub format_version: u16,
350 pub volume_format_rev: u16,
351 pub archive_root: [u8; 32],
352 pub authenticator_id: u16,
353 pub signer_identity_type: u16,
354 pub signer_identity_bytes: Vec<u8>,
355 pub total_data_block_count: u64,
356 pub diagnostics: Vec<PublicNoKeyDiagnostic>,
357}
358
359#[derive(Debug, Clone, Copy, PartialEq, Eq)]
360struct RootAuthMaterial {
361 critical_metadata_digest: [u8; 32],
362 index_digest: [u8; 32],
363 fec_layout_digest: [u8; 32],
364 data_block_merkle_root: [u8; 32],
365 signer_identity_digest: [u8; 32],
366 archive_root: [u8; 32],
367 total_data_block_count: u64,
368}
369
370#[derive(Debug, Clone, Copy)]
371struct ObjectExtent {
372 first_block_index: u64,
373 data_block_count: u32,
374 parity_block_count: u32,
375 encrypted_size: u32,
376}
377
378#[derive(Debug, Clone, Copy, PartialEq, Eq)]
379enum ParityReadPolicy {
380 Always,
381 RepairOnly,
382}
383
384pub(crate) struct StreamedArchiveOpenParts {
385 pub(crate) options: ReaderOptions,
386 pub(crate) observed_archive_bytes: u64,
387 pub(crate) subkeys: Subkeys,
388 pub(crate) blocks: BTreeMap<u64, BlockRecord>,
389 pub(crate) crypto_header_bytes: Vec<u8>,
390 pub(crate) volume_header: VolumeHeader,
391 pub(crate) crypto_header: CryptoHeaderFixed,
392 pub(crate) manifest_footer: ManifestFooter,
393 pub(crate) volume_trailer: VolumeTrailer,
394 pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
395}
396
397#[derive(Clone, Copy)]
398struct WinningIndexEntry {
399 start: u64,
400 file_data_size: u64,
401 shard_index: usize,
402 file_index: usize,
403}
404
405struct LocatedIndexFile {
406 shard: IndexShard,
407 file_index: usize,
408 start: u64,
409}
410
411struct ExtractProgressWriter<'a, W> {
412 inner: &'a mut W,
413 archive_path: &'a str,
414 file_data_size: u64,
415 reported_bytes: u64,
416 progress: &'a mut dyn ArchiveExtractProgressSink,
417}
418
419impl<'a, W> ExtractProgressWriter<'a, W> {
420 fn new(
421 inner: &'a mut W,
422 archive_path: &'a str,
423 file_data_size: u64,
424 progress: &'a mut dyn ArchiveExtractProgressSink,
425 ) -> Self {
426 Self {
427 inner,
428 archive_path,
429 file_data_size,
430 reported_bytes: 0,
431 progress,
432 }
433 }
434
435 fn report(&mut self, bytes: u64) {
436 if bytes == 0 || self.file_data_size == 0 {
437 return;
438 }
439 let capped_next = self
440 .reported_bytes
441 .saturating_add(bytes)
442 .min(self.file_data_size);
443 let delta = capped_next.saturating_sub(self.reported_bytes);
444 if delta == 0 {
445 return;
446 }
447 self.reported_bytes = capped_next;
448 self.progress.file_bytes_extracted(self.archive_path, delta);
449 }
450}
451
452impl<W: Write> Write for ExtractProgressWriter<'_, W> {
453 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
454 let written = self.inner.write(buf)?;
455 self.report(written as u64);
456 Ok(written)
457 }
458
459 fn flush(&mut self) -> std::io::Result<()> {
460 self.inner.flush()
461 }
462}
463
464struct DecodedTarMemberGroupReader<'a> {
465 archive: &'a OpenedArchive,
466 shard: &'a IndexShard,
467 file: &'a FileEntry,
468 decompressor: zstd::bulk::Decompressor<'static>,
469 next_frame_offset: u64,
470 cached_envelope_index: Option<u64>,
471 cached_envelope_plaintext: Vec<u8>,
472 current_frame: Vec<u8>,
473 current_frame_offset: usize,
474 remaining_group_bytes: u64,
475}
476
477struct SeekableVolumeSource {
478 reader: Arc<dyn ArchiveReadAt>,
479 volume_index: u32,
480 block_records_start: u64,
481 block_count: u64,
482 record_len: u64,
483 block_size: usize,
484}
485
486impl std::fmt::Debug for SeekableVolumeSource {
487 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
488 f.debug_struct("SeekableVolumeSource")
489 .field("volume_index", &self.volume_index)
490 .field("block_records_start", &self.block_records_start)
491 .field("block_count", &self.block_count)
492 .field("record_len", &self.record_len)
493 .field("block_size", &self.block_size)
494 .finish_non_exhaustive()
495 }
496}
497
498#[derive(Debug)]
499struct SeekableBlockSource {
500 stripe_width: u32,
501 volumes: Vec<Option<SeekableVolumeSource>>,
502}
503
504trait BlockProvider {
505 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError>;
506}
507
508struct OpenedBlockProvider<'a> {
509 memory_blocks: &'a BTreeMap<u64, BlockRecord>,
510 lazy_blocks: Option<&'a SeekableBlockSource>,
511}
512
513impl SeekableBlockSource {
514 fn record_location(&self, block_index: u64) -> Result<(u32, u64), FormatError> {
515 if self.stripe_width == 0 {
516 return Err(FormatError::ZeroStripeWidth);
517 }
518 let volume_index = u32::try_from(block_index % self.stripe_width as u64)
519 .map_err(|_| FormatError::InvalidArchive("BlockRecord volume index overflow"))?;
520 let Some(volume) = self
521 .volumes
522 .get(volume_index as usize)
523 .and_then(Option::as_ref)
524 else {
525 return Err(FormatError::InvalidArchive(
526 "repair output requires all archive volumes",
527 ));
528 };
529 let slot = block_index / self.stripe_width as u64;
530 if slot >= volume.block_count {
531 return Err(FormatError::InvalidArchive(
532 "BlockRecord global coverage has a gap",
533 ));
534 }
535 Ok((volume_index, volume.record_offset(slot)?))
536 }
537
538 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
539 if self.stripe_width == 0 {
540 return Err(FormatError::ZeroStripeWidth);
541 }
542 let volume_index = u32::try_from(block_index % self.stripe_width as u64)
543 .map_err(|_| FormatError::InvalidArchive("BlockRecord volume index overflow"))?;
544 let Some(volume) = self
545 .volumes
546 .get(volume_index as usize)
547 .and_then(Option::as_ref)
548 else {
549 return Ok(None);
550 };
551 let slot = block_index / self.stripe_width as u64;
552 if slot >= volume.block_count {
553 return Ok(None);
554 }
555 match volume.read_slot(slot, block_index) {
556 Ok(record) => Ok(Some(record)),
557 Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
558 Err(err) => Err(err),
559 }
560 }
561
562 fn is_complete_volume_set(&self) -> bool {
563 self.volumes.iter().all(Option::is_some)
564 }
565
566 fn total_block_count(&self) -> Result<u64, FormatError> {
567 self.volumes
568 .iter()
569 .map(|volume| {
570 volume
571 .as_ref()
572 .map(|volume| volume.block_count)
573 .ok_or(FormatError::InvalidArchive(
574 "missing volume in complete set",
575 ))
576 })
577 .try_fold(0u64, |sum, count| {
578 checked_u64_add(sum, count?, "BlockRecord count overflow")
579 })
580 }
581}
582
583impl SeekableVolumeSource {
584 fn record_offset(&self, slot: u64) -> Result<u64, FormatError> {
585 self.block_records_start
586 .checked_add(checked_u64_mul(
587 slot,
588 self.record_len,
589 "BlockRecord offset overflow",
590 )?)
591 .ok_or(FormatError::InvalidArchive("BlockRecord offset overflow"))
592 }
593
594 fn read_slot(&self, slot: u64, expected_block_index: u64) -> Result<BlockRecord, FormatError> {
595 let record_offset = self.record_offset(slot)?;
596 let raw = read_at_vec_unchecked(
597 self.reader.as_ref(),
598 record_offset,
599 usize::try_from(self.record_len)
600 .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?,
601 )?;
602 let record = BlockRecord::parse(&raw, self.block_size)?;
603 if record.block_index != expected_block_index {
604 return Err(FormatError::InvalidArchive(
605 "BlockRecord index does not match volume position",
606 ));
607 }
608 Ok(record)
609 }
610}
611
612impl BlockProvider for BTreeMap<u64, BlockRecord> {
613 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
614 Ok(self.get(&block_index).cloned())
615 }
616}
617
618impl BlockProvider for OpenedBlockProvider<'_> {
619 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
620 if let Some(record) = self.memory_blocks.get(&block_index) {
621 return Ok(Some(record.clone()));
622 }
623 match self.lazy_blocks {
624 Some(source) => source.block(block_index),
625 None => Ok(None),
626 }
627 }
628}
629
630fn subkeys_for_open(
631 master_key: Option<&MasterKey>,
632 aead_algo: AeadAlgo,
633 archive_uuid: &[u8; 16],
634 session_id: &[u8; 16],
635) -> Result<Subkeys, FormatError> {
636 if aead_algo.is_encrypted() {
637 Subkeys::derive(
638 master_key.ok_or(FormatError::KeyMaterialMismatch)?,
639 archive_uuid,
640 session_id,
641 )
642 } else {
643 Ok(Subkeys::unencrypted_placeholder())
644 }
645}
646
647type DirectoryHintMap = BTreeMap<Vec<u8>, BTreeSet<u32>>;
648pub type ExtractedRegularFile = (Vec<u8>, Vec<MetadataDiagnostic>);
649const FAST_FULL_EXTRACT_UNIQUE_PATHS_UNSUPPORTED: &str =
650 "fast full extract requires unique archive paths";
651
652fn parse_volume_format_dispatch(
653 volume_header: &VolumeHeader,
654) -> Result<VolumeFormatRevision, FormatError> {
655 let revision = volume_header.parse_volume_format_revision()?;
656 match revision {
657 VolumeFormatRevision::V43 | VolumeFormatRevision::V44 => Ok(revision),
658 }
659}
660
661#[derive(Debug)]
662struct PayloadIndexTables {
663 shards: Vec<IndexShard>,
664 file_count: u64,
665 frames: BTreeMap<u64, FrameEntry>,
666 envelopes: BTreeMap<u64, EnvelopeEntry>,
667}
668
669pub fn open_archive(bytes: &[u8], master_key: &MasterKey) -> Result<OpenedArchive, FormatError> {
670 OpenedArchive::open_with_options(bytes, master_key, ReaderOptions::default())
671}
672
673pub fn open_archive_with_recipient_wrap_resolver<F>(
674 bytes: &[u8],
675 resolver: F,
676) -> Result<OpenedArchive, FormatError>
677where
678 F: FnMut(
679 RecipientWrapRecordContext<'_>,
680 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
681{
682 OpenedArchive::open_with_recipient_wrap_resolver_options(
683 bytes,
684 resolver,
685 ReaderOptions::default(),
686 )
687}
688
689pub fn open_archive_unencrypted(bytes: &[u8]) -> Result<OpenedArchive, FormatError> {
690 require_unencrypted_volume_profile(bytes)?;
691 let placeholder = MasterKey::from_raw_key(&[0; 32])?;
692 OpenedArchive::open_with_options(bytes, &placeholder, ReaderOptions::default())
693}
694
695pub fn open_archive_volumes(
696 volumes: &[&[u8]],
697 master_key: &MasterKey,
698) -> Result<OpenedArchive, FormatError> {
699 OpenedArchive::open_volumes_with_options(volumes, master_key, ReaderOptions::default())
700}
701
702pub fn open_archive_volumes_unencrypted(volumes: &[&[u8]]) -> Result<OpenedArchive, FormatError> {
703 for volume in volumes {
704 require_unencrypted_volume_profile(volume)?;
705 }
706 let placeholder = MasterKey::from_raw_key(&[0; 32])?;
707 OpenedArchive::open_volumes_with_options(volumes, &placeholder, ReaderOptions::default())
708}
709
710pub fn open_archive_with_bootstrap_sidecar(
711 bytes: &[u8],
712 bootstrap_sidecar: &[u8],
713 master_key: &MasterKey,
714) -> Result<OpenedArchive, FormatError> {
715 OpenedArchive::open_with_bootstrap_sidecar_options(
716 bytes,
717 bootstrap_sidecar,
718 master_key,
719 ReaderOptions::default(),
720 )
721}
722
723fn require_unencrypted_volume_profile(bytes: &[u8]) -> Result<(), FormatError> {
724 if bytes.len() < VOLUME_HEADER_LEN {
725 return Err(FormatError::InvalidLength {
726 structure: "archive",
727 expected: VOLUME_HEADER_LEN,
728 actual: bytes.len(),
729 });
730 }
731 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
732 parse_volume_format_dispatch(&volume_header)?;
733 let crypto_start = volume_header.crypto_header_offset as usize;
734 let crypto_len = volume_header.crypto_header_length as usize;
735 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
736 let crypto_header = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
737 if crypto_header.fixed.aead_algo == AeadAlgo::None
738 && crypto_header.fixed.kdf_algo == KdfAlgo::None
739 {
740 Ok(())
741 } else {
742 Err(FormatError::KeyMaterialMismatch)
743 }
744}
745
746pub fn open_seekable_archive<R: ArchiveReadAt>(
747 reader: R,
748 master_key: &MasterKey,
749) -> Result<OpenedArchive, FormatError> {
750 OpenedArchive::open_seekable_volumes_with_options(
751 vec![reader],
752 master_key,
753 ReaderOptions::default(),
754 )
755}
756
757pub fn open_seekable_archive_volumes<R: ArchiveReadAt>(
758 readers: Vec<R>,
759 master_key: &MasterKey,
760) -> Result<OpenedArchive, FormatError> {
761 OpenedArchive::open_seekable_volumes_with_options(readers, master_key, ReaderOptions::default())
762}
763
764pub fn open_seekable_archive_with_bootstrap_sidecar<R: ArchiveReadAt>(
765 reader: R,
766 bootstrap_sidecar: &[u8],
767 master_key: &MasterKey,
768) -> Result<OpenedArchive, FormatError> {
769 open_seekable_archive_with_bootstrap_sidecar_options(
770 reader,
771 bootstrap_sidecar,
772 master_key,
773 ReaderOptions::default(),
774 )
775}
776
777pub fn open_seekable_archive_with_bootstrap_sidecar_options<R: ArchiveReadAt>(
778 reader: R,
779 bootstrap_sidecar: &[u8],
780 master_key: &MasterKey,
781 options: ReaderOptions,
782) -> Result<OpenedArchive, FormatError> {
783 OpenedArchive::open_seekable_volumes_with_options_for_mode(
784 vec![Arc::new(reader) as Arc<dyn ArchiveReadAt>],
785 master_key,
786 options,
787 Some(bootstrap_sidecar),
788 )
789}
790
791pub fn open_seekable_archive_with_recipient_wrap_resolver_options<R, F>(
792 reader: R,
793 resolver: F,
794 options: ReaderOptions,
795) -> Result<OpenedArchive, FormatError>
796where
797 R: ArchiveReadAt,
798 F: FnMut(
799 RecipientWrapRecordContext<'_>,
800 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
801{
802 OpenedArchive::open_seekable_with_recipient_wrap_resolver_options(reader, resolver, options)
803}
804
805pub fn open_seekable_archive_volumes_with_recipient_wrap_resolver_options<R, F>(
806 readers: Vec<R>,
807 resolver: F,
808 options: ReaderOptions,
809) -> Result<OpenedArchive, FormatError>
810where
811 R: ArchiveReadAt,
812 F: FnMut(
813 RecipientWrapRecordContext<'_>,
814 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
815{
816 OpenedArchive::open_seekable_volumes_with_recipient_wrap_resolver_options(
817 readers, resolver, options,
818 )
819}
820
821pub fn open_non_seekable_archive(
822 bytes: &[u8],
823 master_key: &MasterKey,
824 bootstrap_sidecar: Option<&[u8]>,
825) -> Result<OpenedArchive, FormatError> {
826 match bootstrap_sidecar {
827 Some(sidecar) => OpenedArchive::open_with_bootstrap_sidecar_options_for_mode(
828 bytes,
829 sidecar,
830 master_key,
831 ReaderOptions::default(),
832 BootstrapSidecarUse::NonSeekableRandomAccess,
833 ),
834 None => Err(FormatError::ReaderUnsupported(
835 "non-seekable random access requires a bootstrap sidecar",
836 )),
837 }
838}
839
840pub fn public_no_key_verify_archive_with<F>(
841 bytes: &[u8],
842 verifier: F,
843) -> Result<PublicNoKeyVerification, FormatError>
844where
845 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
846{
847 public_no_key_verify_volumes_with_options(&[bytes], verifier, ReaderOptions::default())
848}
849
850pub fn public_no_key_verify_volumes_with<F>(
851 volumes: &[&[u8]],
852 verifier: F,
853) -> Result<PublicNoKeyVerification, FormatError>
854where
855 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
856{
857 public_no_key_verify_volumes_with_options(volumes, verifier, ReaderOptions::default())
858}
859
860pub fn sequential_extract_tar_stream(
866 bytes: &[u8],
867 master_key: &MasterKey,
868) -> Result<Vec<u8>, FormatError> {
869 sequential_extract_tar_stream_with_options(bytes, master_key, ReaderOptions::default())
870}
871
872impl OpenedArchive {
873 fn block_provider(&self) -> OpenedBlockProvider<'_> {
874 OpenedBlockProvider {
875 memory_blocks: &self.blocks,
876 lazy_blocks: self.lazy_blocks.as_deref(),
877 }
878 }
879
880 fn missing_volume_count(&self) -> u32 {
881 self.crypto_header
882 .stripe_width
883 .saturating_sub(self.observed_volume_count)
884 }
885
886 fn root_auth_success_diagnostics(&self) -> Vec<RootAuthDiagnostic> {
887 let mut diagnostics = vec![
888 RootAuthDiagnostic::RootAuthContentVerified,
889 RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
890 RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
891 ];
892 if self.missing_volume_count() > 0 {
893 diagnostics.push(RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss);
894 }
895 diagnostics.push(RootAuthDiagnostic::RecoveryMarginUnchecked);
896 diagnostics
897 }
898
899 pub fn open_with_options(
900 bytes: &[u8],
901 master_key: &MasterKey,
902 options: ReaderOptions,
903 ) -> Result<Self, FormatError> {
904 Self::open_volumes_with_options(&[bytes], master_key, options)
905 }
906
907 pub fn open_with_recipient_wrap_resolver_options<F>(
908 bytes: &[u8],
909 mut resolver: F,
910 options: ReaderOptions,
911 ) -> Result<Self, FormatError>
912 where
913 F: FnMut(
914 RecipientWrapRecordContext<'_>,
915 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
916 {
917 validate_reader_options(options)?;
918 let observed_archive_bytes = observed_archive_size(std::iter::once(bytes.len() as u64))?;
919 let parsed =
920 parse_seekable_volume_with_recipient_wrap_resolver(bytes, &mut resolver, options)?;
921 let ParsedSeekableVolume {
922 volume_header,
923 crypto_header,
924 crypto_header_bytes,
925 key_wrap_table_bytes: _,
926 subkeys,
927 manifest_footer,
928 manifest_footer_error,
929 root_auth_footer,
930 root_auth_footer_bytes: _,
931 volume_trailer,
932 blocks,
933 erased_block_indices,
934 } = parsed;
935 let manifest_footer = match manifest_footer {
936 Some(footer) => footer,
937 None => {
938 return Err(manifest_footer_error.unwrap_or(FormatError::InvalidArchive(
939 "no authenticated ManifestFooter found",
940 )));
941 }
942 };
943 let observed_volume_count = 1;
944 let missing_volume_count = crypto_header
945 .stripe_width
946 .checked_sub(observed_volume_count)
947 .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
948 if missing_volume_count > crypto_header.volume_loss_tolerance as u32 {
949 return Err(FormatError::InvalidArchive(
950 "missing volume count exceeds volume_loss_tolerance",
951 ));
952 }
953 if missing_volume_count == 0 {
954 validate_complete_global_block_coverage(&blocks, &erased_block_indices)?;
955 }
956
957 let limits = metadata_limits(&crypto_header);
958 let index_root_plaintext = load_metadata_object_from_parts(
959 &blocks,
960 ObjectLoadContext::index_root(
961 &volume_header,
962 &crypto_header,
963 &subkeys,
964 ObjectExtent {
965 first_block_index: manifest_footer.index_root_first_block,
966 data_block_count: manifest_footer.index_root_data_block_count,
967 parity_block_count: manifest_footer.index_root_parity_block_count,
968 encrypted_size: manifest_footer.index_root_encrypted_size,
969 },
970 ),
971 manifest_footer.index_root_decompressed_size,
972 )?;
973 let index_root = IndexRoot::parse(
974 &index_root_plaintext,
975 crypto_header.has_dictionary != 0,
976 limits,
977 )?;
978 let payload_dictionary = load_archive_dictionary(
979 &blocks,
980 &subkeys,
981 &volume_header,
982 &crypto_header,
983 &index_root,
984 )?;
985
986 Ok(Self {
987 options,
988 observed_archive_bytes,
989 observed_volume_count,
990 subkeys,
991 blocks,
992 lazy_blocks: None,
993 crypto_header_bytes,
994 volume_header,
995 crypto_header,
996 manifest_footer,
997 volume_trailer: Some(volume_trailer),
998 root_auth_footer,
999 index_root,
1000 payload_dictionary,
1001 })
1002 }
1003
1004 pub fn open_seekable_with_recipient_wrap_resolver_options<R, F>(
1005 reader: R,
1006 mut resolver: F,
1007 options: ReaderOptions,
1008 ) -> Result<Self, FormatError>
1009 where
1010 R: ArchiveReadAt,
1011 F: FnMut(
1012 RecipientWrapRecordContext<'_>,
1013 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
1014 {
1015 validate_reader_options(options)?;
1016 let reader = Arc::new(reader) as Arc<dyn ArchiveReadAt>;
1017 let observed_len = reader.len()?;
1018 let observed_archive_bytes = observed_archive_size([observed_len])?;
1019 let mut parsed = parse_seekable_read_at_volume_with_recipient_wrap_resolver(
1020 reader.clone(),
1021 &mut resolver,
1022 options,
1023 )?;
1024 let manifest_footer = match parsed.manifest_footer.take() {
1025 Some(footer) => footer,
1026 None => {
1027 return Err(parsed.manifest_footer_error.take().unwrap_or(
1028 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1029 ));
1030 }
1031 };
1032 let observed_volume_count = 1;
1033 let missing_volume_count = parsed
1034 .crypto_header
1035 .stripe_width
1036 .checked_sub(observed_volume_count)
1037 .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1038 if missing_volume_count > parsed.crypto_header.volume_loss_tolerance as u32 {
1039 return Err(FormatError::InvalidArchive(
1040 "missing volume count exceeds volume_loss_tolerance",
1041 ));
1042 }
1043
1044 let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
1045 let mut lazy_volume_slots = Vec::new();
1046 lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
1047 let slot = parsed.volume_header.volume_index as usize;
1048 if slot >= lazy_volume_slots.len() {
1049 return Err(FormatError::InvalidArchive(
1050 "authenticated volume index exceeds stripe_width",
1051 ));
1052 }
1053 lazy_volume_slots[slot] = Some(SeekableVolumeSource {
1054 reader: parsed.reader.clone(),
1055 volume_index: parsed.volume_header.volume_index,
1056 block_records_start: parsed.block_records_start,
1057 block_count: parsed.volume_trailer.block_count,
1058 record_len,
1059 block_size: parsed.crypto_header.block_size as usize,
1060 });
1061 let lazy_source = Arc::new(SeekableBlockSource {
1062 stripe_width: parsed.crypto_header.stripe_width,
1063 volumes: lazy_volume_slots,
1064 });
1065 let blocks = BTreeMap::new();
1066 let block_provider = OpenedBlockProvider {
1067 memory_blocks: &blocks,
1068 lazy_blocks: Some(lazy_source.as_ref()),
1069 };
1070 let limits = metadata_limits(&parsed.crypto_header);
1071 let index_root_plaintext = load_metadata_object_from_parts(
1072 &block_provider,
1073 ObjectLoadContext::index_root(
1074 &parsed.volume_header,
1075 &parsed.crypto_header,
1076 &parsed.subkeys,
1077 index_root_extent_from_manifest(&manifest_footer),
1078 ),
1079 manifest_footer.index_root_decompressed_size,
1080 )?;
1081 let index_root = IndexRoot::parse(
1082 &index_root_plaintext,
1083 parsed.crypto_header.has_dictionary != 0,
1084 limits,
1085 )?;
1086 let block_provider = OpenedBlockProvider {
1087 memory_blocks: &blocks,
1088 lazy_blocks: Some(lazy_source.as_ref()),
1089 };
1090 let payload_dictionary = load_archive_dictionary(
1091 &block_provider,
1092 &parsed.subkeys,
1093 &parsed.volume_header,
1094 &parsed.crypto_header,
1095 &index_root,
1096 )?;
1097
1098 Ok(Self {
1099 options,
1100 observed_archive_bytes,
1101 observed_volume_count,
1102 subkeys: parsed.subkeys,
1103 blocks,
1104 lazy_blocks: Some(lazy_source),
1105 crypto_header_bytes: parsed.crypto_header_bytes,
1106 volume_header: parsed.volume_header,
1107 crypto_header: parsed.crypto_header,
1108 manifest_footer,
1109 volume_trailer: Some(parsed.volume_trailer),
1110 root_auth_footer: parsed.root_auth_footer,
1111 index_root,
1112 payload_dictionary,
1113 })
1114 }
1115
1116 pub fn open_seekable_volumes_with_recipient_wrap_resolver_options<R, F>(
1117 readers: Vec<R>,
1118 mut resolver: F,
1119 options: ReaderOptions,
1120 ) -> Result<Self, FormatError>
1121 where
1122 R: ArchiveReadAt,
1123 F: FnMut(
1124 RecipientWrapRecordContext<'_>,
1125 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
1126 {
1127 validate_reader_options(options)?;
1128 if readers.is_empty() {
1129 return Err(FormatError::InvalidArchive("no volumes supplied"));
1130 }
1131 let readers = readers
1132 .into_iter()
1133 .map(|reader| Arc::new(reader) as Arc<dyn ArchiveReadAt>)
1134 .collect::<Vec<_>>();
1135 let observed_archive_bytes = observed_archive_size(
1136 readers
1137 .iter()
1138 .map(|reader| reader.len())
1139 .collect::<Result<Vec<_>, _>>()?,
1140 )?;
1141 let mut first: Option<ParsedSeekableReadAtVolume> = None;
1142 let mut manifest_authority: Option<ManifestFooter> = None;
1143 let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
1144 let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
1145 let mut root_auth_authority: Option<RootAuthFooterV1> = None;
1146 let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
1147 let mut saw_root_auth_absent = false;
1148 let mut first_manifest_footer_error: Option<FormatError> = None;
1149 let mut seen_volume_indexes = BTreeSet::new();
1150 let mut lazy_volume_slots: Vec<Option<SeekableVolumeSource>> = Vec::new();
1151
1152 for reader in readers {
1153 let mut parsed = parse_seekable_read_at_volume_with_recipient_wrap_resolver(
1154 reader,
1155 &mut resolver,
1156 options,
1157 )?;
1158 if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
1159 return Err(FormatError::InvalidArchive(
1160 "duplicate authenticated volume index",
1161 ));
1162 }
1163
1164 if let Some(first) = &first {
1165 validate_volume_set_member_metadata(
1166 &first.volume_header,
1167 &first.crypto_header,
1168 &first.crypto_header_bytes,
1169 &parsed.volume_header,
1170 &parsed.crypto_header,
1171 &parsed.crypto_header_bytes,
1172 )?;
1173 validate_key_wrap_table_bytes_match(
1174 &first.key_wrap_table_bytes,
1175 &parsed.key_wrap_table_bytes,
1176 )?;
1177 } else {
1178 lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
1179 }
1180
1181 if let Some(footer) = &parsed.manifest_footer {
1182 if let Some(authority) = &manifest_authority {
1183 if !manifest_bootstrap_fields_match(authority, footer) {
1184 return Err(FormatError::InvalidArchive(
1185 "ManifestFooter bootstrap fields differ",
1186 ));
1187 }
1188 } else {
1189 manifest_authority = Some(footer.clone());
1190 manifest_authority_volume_header = Some(parsed.volume_header.clone());
1191 manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
1192 }
1193 } else if first_manifest_footer_error.is_none() {
1194 first_manifest_footer_error = parsed.manifest_footer_error.take();
1195 }
1196
1197 match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
1198 (Some(footer), Some(bytes)) => {
1199 if saw_root_auth_absent {
1200 return Err(FormatError::InvalidArchive(
1201 "root-auth footer presence differs across volumes",
1202 ));
1203 }
1204 if let Some(authority_bytes) = &root_auth_authority_bytes {
1205 if authority_bytes != bytes {
1206 return Err(FormatError::InvalidArchive(
1207 "RootAuthFooter copies differ",
1208 ));
1209 }
1210 } else {
1211 root_auth_authority = Some(footer.clone());
1212 root_auth_authority_bytes = Some(bytes.clone());
1213 }
1214 }
1215 (None, None) => {
1216 if root_auth_authority_bytes.is_some() {
1217 return Err(FormatError::InvalidArchive(
1218 "root-auth footer presence differs across volumes",
1219 ));
1220 }
1221 saw_root_auth_absent = true;
1222 }
1223 _ => {
1224 return Err(FormatError::InvalidArchive(
1225 "root-auth footer terminal state is inconsistent",
1226 ));
1227 }
1228 }
1229
1230 let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
1231 let source = SeekableVolumeSource {
1232 reader: parsed.reader.clone(),
1233 volume_index: parsed.volume_header.volume_index,
1234 block_records_start: parsed.block_records_start,
1235 block_count: parsed.volume_trailer.block_count,
1236 record_len,
1237 block_size: parsed.crypto_header.block_size as usize,
1238 };
1239 let slot = parsed.volume_header.volume_index as usize;
1240 if slot >= lazy_volume_slots.len() || lazy_volume_slots[slot].replace(source).is_some()
1241 {
1242 return Err(FormatError::InvalidArchive(
1243 "duplicate authenticated volume index",
1244 ));
1245 }
1246
1247 if first.is_none() {
1248 first = Some(parsed);
1249 }
1250 }
1251
1252 let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
1253 let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
1254 Some(err) => err,
1255 None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1256 })?;
1257 let authority_volume_header = manifest_authority_volume_header.ok_or(
1258 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1259 )?;
1260 let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
1261 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1262 )?;
1263 let observed_volume_count = u32::try_from(seen_volume_indexes.len())
1264 .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
1265 let missing_volume_count = first
1266 .crypto_header
1267 .stripe_width
1268 .checked_sub(observed_volume_count)
1269 .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1270 if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
1271 return Err(FormatError::InvalidArchive(
1272 "missing volume count exceeds volume_loss_tolerance",
1273 ));
1274 }
1275
1276 let blocks = BTreeMap::new();
1277 let lazy_source = Arc::new(SeekableBlockSource {
1278 stripe_width: first.crypto_header.stripe_width,
1279 volumes: lazy_volume_slots,
1280 });
1281 let block_provider = OpenedBlockProvider {
1282 memory_blocks: &blocks,
1283 lazy_blocks: Some(lazy_source.as_ref()),
1284 };
1285 let limits = metadata_limits(&first.crypto_header);
1286 let index_root_plaintext = load_metadata_object_from_parts(
1287 &block_provider,
1288 ObjectLoadContext::index_root(
1289 &first.volume_header,
1290 &first.crypto_header,
1291 &first.subkeys,
1292 index_root_extent_from_manifest(&manifest_footer),
1293 ),
1294 manifest_footer.index_root_decompressed_size,
1295 )?;
1296 let index_root = IndexRoot::parse(
1297 &index_root_plaintext,
1298 first.crypto_header.has_dictionary != 0,
1299 limits,
1300 )?;
1301 let block_provider = OpenedBlockProvider {
1302 memory_blocks: &blocks,
1303 lazy_blocks: Some(lazy_source.as_ref()),
1304 };
1305 let payload_dictionary = load_archive_dictionary(
1306 &block_provider,
1307 &first.subkeys,
1308 &first.volume_header,
1309 &first.crypto_header,
1310 &index_root,
1311 )?;
1312
1313 Ok(Self {
1314 options,
1315 observed_archive_bytes,
1316 observed_volume_count,
1317 subkeys: first.subkeys,
1318 blocks,
1319 lazy_blocks: Some(lazy_source),
1320 crypto_header_bytes: first.crypto_header_bytes,
1321 volume_header: authority_volume_header,
1322 crypto_header: first.crypto_header,
1323 manifest_footer,
1324 volume_trailer: Some(authority_volume_trailer),
1325 root_auth_footer: root_auth_authority,
1326 index_root,
1327 payload_dictionary,
1328 })
1329 }
1330
1331 pub fn open_volumes_with_options(
1332 volumes: &[&[u8]],
1333 master_key: &MasterKey,
1334 options: ReaderOptions,
1335 ) -> Result<Self, FormatError> {
1336 validate_reader_options(options)?;
1337 if volumes.is_empty() {
1338 return Err(FormatError::InvalidArchive("no volumes supplied"));
1339 }
1340
1341 let observed_archive_bytes =
1342 observed_archive_size(volumes.iter().map(|volume| volume.len() as u64))?;
1343 let mut first: Option<ParsedSeekableVolume> = None;
1344 let mut manifest_authority: Option<ManifestFooter> = None;
1345 let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
1346 let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
1347 let mut root_auth_authority: Option<RootAuthFooterV1> = None;
1348 let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
1349 let mut saw_root_auth_absent = false;
1350 let mut first_manifest_footer_error: Option<FormatError> = None;
1351 let mut seen_volume_indexes = BTreeSet::new();
1352 let mut blocks = BTreeMap::new();
1353 let mut erased_block_indices = BTreeSet::new();
1354
1355 for volume_bytes in volumes {
1356 let mut parsed = parse_seekable_volume(volume_bytes, master_key, options)?;
1357 if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
1358 return Err(FormatError::InvalidArchive(
1359 "duplicate authenticated volume index",
1360 ));
1361 }
1362
1363 if let Some(first) = &first {
1364 validate_volume_set_member(first, &parsed)?;
1365 }
1366
1367 if let Some(footer) = &parsed.manifest_footer {
1368 if let Some(authority) = &manifest_authority {
1369 if !manifest_bootstrap_fields_match(authority, footer) {
1370 return Err(FormatError::InvalidArchive(
1371 "ManifestFooter bootstrap fields differ",
1372 ));
1373 }
1374 } else {
1375 manifest_authority = Some(footer.clone());
1376 manifest_authority_volume_header = Some(parsed.volume_header.clone());
1377 manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
1378 }
1379 } else if first_manifest_footer_error.is_none() {
1380 first_manifest_footer_error = parsed.manifest_footer_error.take();
1381 }
1382
1383 match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
1384 (Some(footer), Some(bytes)) => {
1385 if saw_root_auth_absent {
1386 return Err(FormatError::InvalidArchive(
1387 "root-auth footer presence differs across volumes",
1388 ));
1389 }
1390 if let Some(authority_bytes) = &root_auth_authority_bytes {
1391 if authority_bytes != bytes {
1392 return Err(FormatError::InvalidArchive(
1393 "RootAuthFooter copies differ",
1394 ));
1395 }
1396 } else {
1397 root_auth_authority = Some(footer.clone());
1398 root_auth_authority_bytes = Some(bytes.clone());
1399 }
1400 }
1401 (None, None) => {
1402 if root_auth_authority_bytes.is_some() {
1403 return Err(FormatError::InvalidArchive(
1404 "root-auth footer presence differs across volumes",
1405 ));
1406 }
1407 saw_root_auth_absent = true;
1408 }
1409 _ => {
1410 return Err(FormatError::InvalidArchive(
1411 "root-auth footer terminal state is inconsistent",
1412 ));
1413 }
1414 }
1415
1416 for (block_index, record) in &parsed.blocks {
1417 if blocks.insert(*block_index, record.clone()).is_some() {
1418 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
1419 }
1420 }
1421 for block_index in &parsed.erased_block_indices {
1422 erased_block_indices.insert(*block_index);
1423 }
1424
1425 if first.is_none() {
1426 first = Some(parsed);
1427 }
1428 }
1429
1430 let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
1431 let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
1432 Some(err) => err,
1433 None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1434 })?;
1435 let authority_volume_header = manifest_authority_volume_header.ok_or(
1436 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1437 )?;
1438 let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
1439 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1440 )?;
1441 let observed_volume_count = u32::try_from(seen_volume_indexes.len())
1442 .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
1443 let missing_volume_count = first
1444 .crypto_header
1445 .stripe_width
1446 .checked_sub(observed_volume_count)
1447 .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1448 if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
1449 return Err(FormatError::InvalidArchive(
1450 "missing volume count exceeds volume_loss_tolerance",
1451 ));
1452 }
1453 if seen_volume_indexes.len() == first.crypto_header.stripe_width as usize {
1454 validate_complete_global_block_coverage(&blocks, &erased_block_indices)?;
1455 }
1456
1457 let limits = metadata_limits(&first.crypto_header);
1458 let index_root_plaintext = load_metadata_object_from_parts(
1459 &blocks,
1460 ObjectLoadContext::index_root(
1461 &first.volume_header,
1462 &first.crypto_header,
1463 &first.subkeys,
1464 ObjectExtent {
1465 first_block_index: manifest_footer.index_root_first_block,
1466 data_block_count: manifest_footer.index_root_data_block_count,
1467 parity_block_count: manifest_footer.index_root_parity_block_count,
1468 encrypted_size: manifest_footer.index_root_encrypted_size,
1469 },
1470 ),
1471 manifest_footer.index_root_decompressed_size,
1472 )?;
1473 let index_root = IndexRoot::parse(
1474 &index_root_plaintext,
1475 first.crypto_header.has_dictionary != 0,
1476 limits,
1477 )?;
1478 let payload_dictionary = load_archive_dictionary(
1479 &blocks,
1480 &first.subkeys,
1481 &first.volume_header,
1482 &first.crypto_header,
1483 &index_root,
1484 )?;
1485
1486 Ok(Self {
1487 options,
1488 observed_archive_bytes,
1489 observed_volume_count,
1490 subkeys: first.subkeys,
1491 blocks,
1492 lazy_blocks: None,
1493 crypto_header_bytes: first.crypto_header_bytes,
1494 volume_header: authority_volume_header,
1495 crypto_header: first.crypto_header,
1496 manifest_footer,
1497 volume_trailer: Some(authority_volume_trailer),
1498 root_auth_footer: root_auth_authority,
1499 index_root,
1500 payload_dictionary,
1501 })
1502 }
1503
1504 pub fn open_seekable_volumes_with_options<R: ArchiveReadAt>(
1505 readers: Vec<R>,
1506 master_key: &MasterKey,
1507 options: ReaderOptions,
1508 ) -> Result<Self, FormatError> {
1509 let readers = readers
1510 .into_iter()
1511 .map(|reader| Arc::new(reader) as Arc<dyn ArchiveReadAt>)
1512 .collect::<Vec<_>>();
1513 Self::open_seekable_volumes_with_options_for_mode(readers, master_key, options, None)
1514 }
1515
1516 fn open_seekable_volumes_with_options_for_mode(
1517 readers: Vec<Arc<dyn ArchiveReadAt>>,
1518 master_key: &MasterKey,
1519 options: ReaderOptions,
1520 bootstrap_sidecar: Option<&[u8]>,
1521 ) -> Result<Self, FormatError> {
1522 validate_reader_options(options)?;
1523 if readers.is_empty() {
1524 return Err(FormatError::InvalidArchive("no volumes supplied"));
1525 }
1526 if bootstrap_sidecar.is_some() && readers.len() > 1 {
1527 return Err(FormatError::ReaderUnsupported(
1528 "multi-volume inputs with bootstrap sidecar are not supported",
1529 ));
1530 }
1531
1532 let observed_archive_bytes = observed_archive_size(
1533 readers
1534 .iter()
1535 .map(|reader| reader.len())
1536 .collect::<Result<Vec<_>, _>>()?
1537 .into_iter()
1538 .chain(bootstrap_sidecar.map(|sidecar| sidecar.len() as u64)),
1539 )?;
1540 let mut first: Option<ParsedSeekableReadAtVolume> = None;
1541 let mut manifest_authority: Option<ManifestFooter> = None;
1542 let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
1543 let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
1544 let mut root_auth_authority: Option<RootAuthFooterV1> = None;
1545 let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
1546 let mut saw_root_auth_absent = false;
1547 let mut first_manifest_footer_error: Option<FormatError> = None;
1548 let mut seen_volume_indexes = BTreeSet::new();
1549 let mut lazy_volume_slots: Vec<Option<SeekableVolumeSource>> = Vec::new();
1550
1551 for reader in readers {
1552 let mut parsed = parse_seekable_read_at_volume(reader, master_key, options)?;
1553 if bootstrap_sidecar.is_some() {
1554 validate_bootstrap_single_volume_input(
1555 &parsed.volume_header,
1556 &parsed.crypto_header,
1557 )?;
1558 }
1559 if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
1560 return Err(FormatError::InvalidArchive(
1561 "duplicate authenticated volume index",
1562 ));
1563 }
1564
1565 if let Some(first) = &first {
1566 validate_volume_set_member_metadata(
1567 &first.volume_header,
1568 &first.crypto_header,
1569 &first.crypto_header_bytes,
1570 &parsed.volume_header,
1571 &parsed.crypto_header,
1572 &parsed.crypto_header_bytes,
1573 )?;
1574 validate_key_wrap_table_bytes_match(
1575 &first.key_wrap_table_bytes,
1576 &parsed.key_wrap_table_bytes,
1577 )?;
1578 } else {
1579 lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
1580 }
1581
1582 if let Some(footer) = &parsed.manifest_footer {
1583 if let Some(authority) = &manifest_authority {
1584 if !manifest_bootstrap_fields_match(authority, footer) {
1585 return Err(FormatError::InvalidArchive(
1586 "ManifestFooter bootstrap fields differ",
1587 ));
1588 }
1589 } else {
1590 manifest_authority = Some(footer.clone());
1591 manifest_authority_volume_header = Some(parsed.volume_header.clone());
1592 manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
1593 }
1594 } else if first_manifest_footer_error.is_none() {
1595 first_manifest_footer_error = parsed.manifest_footer_error.take();
1596 }
1597
1598 match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
1599 (Some(footer), Some(bytes)) => {
1600 if saw_root_auth_absent {
1601 return Err(FormatError::InvalidArchive(
1602 "root-auth footer presence differs across volumes",
1603 ));
1604 }
1605 if let Some(authority_bytes) = &root_auth_authority_bytes {
1606 if authority_bytes != bytes {
1607 return Err(FormatError::InvalidArchive(
1608 "RootAuthFooter copies differ",
1609 ));
1610 }
1611 } else {
1612 root_auth_authority = Some(footer.clone());
1613 root_auth_authority_bytes = Some(bytes.clone());
1614 }
1615 }
1616 (None, None) => {
1617 if root_auth_authority_bytes.is_some() {
1618 return Err(FormatError::InvalidArchive(
1619 "root-auth footer presence differs across volumes",
1620 ));
1621 }
1622 saw_root_auth_absent = true;
1623 }
1624 _ => {
1625 return Err(FormatError::InvalidArchive(
1626 "root-auth footer terminal state is inconsistent",
1627 ));
1628 }
1629 }
1630
1631 let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
1632 let source = SeekableVolumeSource {
1633 reader: parsed.reader.clone(),
1634 volume_index: parsed.volume_header.volume_index,
1635 block_records_start: parsed.block_records_start,
1636 block_count: parsed.volume_trailer.block_count,
1637 record_len,
1638 block_size: parsed.crypto_header.block_size as usize,
1639 };
1640 let slot = parsed.volume_header.volume_index as usize;
1641 if slot >= lazy_volume_slots.len() || lazy_volume_slots[slot].replace(source).is_some()
1642 {
1643 return Err(FormatError::InvalidArchive(
1644 "duplicate authenticated volume index",
1645 ));
1646 }
1647
1648 if first.is_none() {
1649 first = Some(parsed);
1650 }
1651 }
1652
1653 let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
1654 let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
1655 Some(err) => err,
1656 None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1657 })?;
1658 let authority_volume_header = manifest_authority_volume_header.ok_or(
1659 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1660 )?;
1661 let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
1662 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1663 )?;
1664 let observed_volume_count = u32::try_from(seen_volume_indexes.len())
1665 .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
1666 let missing_volume_count = first
1667 .crypto_header
1668 .stripe_width
1669 .checked_sub(observed_volume_count)
1670 .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1671 if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
1672 return Err(FormatError::InvalidArchive(
1673 "missing volume count exceeds volume_loss_tolerance",
1674 ));
1675 }
1676
1677 let mut blocks = BTreeMap::new();
1678 let sidecar = if let Some(bytes) = bootstrap_sidecar {
1679 let sidecar = parse_bootstrap_sidecar(
1680 bytes,
1681 &first.volume_header,
1682 &first.crypto_header,
1683 &first.subkeys,
1684 )?;
1685 sidecar
1686 .require_sections_for(BootstrapSidecarUse::SeekableAssist, &first.crypto_header)?;
1687 if let Some(sidecar_manifest) = &sidecar.manifest_footer {
1688 if !manifest_bootstrap_fields_match(&manifest_footer, sidecar_manifest) {
1689 return Err(FormatError::InvalidArchive(
1690 "bootstrap sidecar conflicts with terminal ManifestFooter",
1691 ));
1692 }
1693 }
1694 Some((bytes, sidecar))
1695 } else {
1696 None
1697 };
1698
1699 if let Some((sidecar_bytes, sidecar)) = &sidecar {
1700 if let Some((offset, length)) = sidecar.index_root_records_section {
1701 let index_root_records = parse_sidecar_block_records(
1702 sidecar_bytes,
1703 first.crypto_header.block_size as usize,
1704 SidecarBlockRecordsSection {
1705 offset,
1706 length,
1707 extent: index_root_extent_from_manifest(&manifest_footer),
1708 data_kind: BlockKind::IndexRootData,
1709 parity_kind: BlockKind::IndexRootParity,
1710 structure: "IndexRoot",
1711 },
1712 )?;
1713 insert_sidecar_records(&mut blocks, index_root_records)?;
1714 }
1715 }
1716
1717 let lazy_source = Arc::new(SeekableBlockSource {
1718 stripe_width: first.crypto_header.stripe_width,
1719 volumes: lazy_volume_slots,
1720 });
1721 let block_provider = OpenedBlockProvider {
1722 memory_blocks: &blocks,
1723 lazy_blocks: Some(lazy_source.as_ref()),
1724 };
1725 let limits = metadata_limits(&first.crypto_header);
1726 let index_root_plaintext = load_metadata_object_from_parts(
1727 &block_provider,
1728 ObjectLoadContext::index_root(
1729 &first.volume_header,
1730 &first.crypto_header,
1731 &first.subkeys,
1732 index_root_extent_from_manifest(&manifest_footer),
1733 ),
1734 manifest_footer.index_root_decompressed_size,
1735 )?;
1736 let index_root = IndexRoot::parse(
1737 &index_root_plaintext,
1738 first.crypto_header.has_dictionary != 0,
1739 limits,
1740 )?;
1741 if first.crypto_header.has_dictionary != 0 {
1742 if let Some((sidecar_bytes, sidecar)) = &sidecar {
1743 if let Some((offset, length)) = sidecar.dictionary_records_section {
1744 let dictionary_records = parse_sidecar_block_records(
1745 sidecar_bytes,
1746 first.crypto_header.block_size as usize,
1747 SidecarBlockRecordsSection {
1748 offset,
1749 length,
1750 extent: dictionary_extent_from_index_root(&index_root)?,
1751 data_kind: BlockKind::DictionaryData,
1752 parity_kind: BlockKind::DictionaryParity,
1753 structure: "Dictionary",
1754 },
1755 )?;
1756 insert_sidecar_records(&mut blocks, dictionary_records)?;
1757 }
1758 }
1759 }
1760 let block_provider = OpenedBlockProvider {
1761 memory_blocks: &blocks,
1762 lazy_blocks: Some(lazy_source.as_ref()),
1763 };
1764 let payload_dictionary = load_archive_dictionary(
1765 &block_provider,
1766 &first.subkeys,
1767 &first.volume_header,
1768 &first.crypto_header,
1769 &index_root,
1770 )?;
1771
1772 Ok(Self {
1773 options,
1774 observed_archive_bytes,
1775 observed_volume_count,
1776 subkeys: first.subkeys,
1777 blocks,
1778 lazy_blocks: Some(lazy_source),
1779 crypto_header_bytes: first.crypto_header_bytes,
1780 volume_header: authority_volume_header,
1781 crypto_header: first.crypto_header,
1782 manifest_footer,
1783 volume_trailer: Some(authority_volume_trailer),
1784 root_auth_footer: root_auth_authority,
1785 index_root,
1786 payload_dictionary,
1787 })
1788 }
1789
1790 pub fn open_with_bootstrap_sidecar_options(
1791 bytes: &[u8],
1792 bootstrap_sidecar: &[u8],
1793 master_key: &MasterKey,
1794 options: ReaderOptions,
1795 ) -> Result<Self, FormatError> {
1796 Self::open_with_bootstrap_sidecar_options_for_mode(
1797 bytes,
1798 bootstrap_sidecar,
1799 master_key,
1800 options,
1801 BootstrapSidecarUse::SeekableAssist,
1802 )
1803 }
1804
1805 fn open_with_bootstrap_sidecar_options_for_mode(
1806 bytes: &[u8],
1807 bootstrap_sidecar: &[u8],
1808 master_key: &MasterKey,
1809 options: ReaderOptions,
1810 sidecar_use: BootstrapSidecarUse,
1811 ) -> Result<Self, FormatError> {
1812 let observed_archive_bytes =
1813 observed_archive_size([bytes.len() as u64, bootstrap_sidecar.len() as u64])?;
1814 if bytes.len() < VOLUME_HEADER_LEN {
1815 return Err(FormatError::InvalidLength {
1816 structure: "archive",
1817 expected: VOLUME_HEADER_LEN,
1818 actual: bytes.len(),
1819 });
1820 }
1821
1822 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
1823 parse_volume_format_dispatch(&volume_header)?;
1824 let crypto_start = volume_header.crypto_header_offset as usize;
1825 let crypto_len = volume_header.crypto_header_length as usize;
1826 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
1827 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
1828 let subkeys = subkeys_for_open(
1829 Some(master_key),
1830 parsed_crypto.fixed.aead_algo,
1831 &volume_header.archive_uuid,
1832 &volume_header.session_id,
1833 )?;
1834 verify_integrity_tag(
1835 HmacDomain::CryptoHeader,
1836 parsed_crypto.fixed.aead_algo,
1837 volume_header.volume_format_rev,
1838 Some(&subkeys.mac_key),
1839 &volume_header.archive_uuid,
1840 &volume_header.session_id,
1841 parsed_crypto.hmac_covered_bytes,
1842 &parsed_crypto.header_hmac,
1843 )?;
1844 parsed_crypto.validate_extension_semantics()?;
1845 reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
1846 validate_bootstrap_single_volume_input(&volume_header, &parsed_crypto.fixed)?;
1847 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
1848
1849 let sidecar = parse_bootstrap_sidecar(
1850 bootstrap_sidecar,
1851 &volume_header,
1852 &parsed_crypto.fixed,
1853 &subkeys,
1854 )?;
1855 sidecar.require_sections_for(sidecar_use, &parsed_crypto.fixed)?;
1856 let block_records_start = startup_block_records_start(
1857 &volume_header,
1858 &parsed_crypto.kdf_params,
1859 |start, length| {
1860 let start = to_usize(start, "KeyWrapTableV1")?;
1861 Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
1862 },
1863 )?;
1864
1865 let (mut blocks, terminal_offset, observed_block_count) = parse_stream_block_prefix(
1866 bytes,
1867 to_usize(block_records_start, "BlockRecord")?,
1868 parsed_crypto.fixed.block_size as usize,
1869 &volume_header,
1870 )?;
1871 let terminal_material = match sidecar_use {
1872 BootstrapSidecarUse::SeekableAssist => Some(parse_terminal_material(
1873 bytes,
1874 terminal_offset,
1875 observed_block_count,
1876 KeyHoldingTerminalContext {
1877 subkeys: &subkeys,
1878 volume_header: &volume_header,
1879 crypto_header: &parsed_crypto.fixed,
1880 crypto_header_bytes: crypto_bytes,
1881 },
1882 options,
1883 )?),
1884 BootstrapSidecarUse::NonSeekableRandomAccess => parse_terminal_material(
1885 bytes,
1886 terminal_offset,
1887 observed_block_count,
1888 KeyHoldingTerminalContext {
1889 subkeys: &subkeys,
1890 volume_header: &volume_header,
1891 crypto_header: &parsed_crypto.fixed,
1892 crypto_header_bytes: crypto_bytes,
1893 },
1894 options,
1895 )
1896 .ok(),
1897 };
1898 let terminal_manifest = terminal_material.as_ref().map(|(manifest, _, _)| manifest);
1899 let manifest_authority = match sidecar_use {
1900 BootstrapSidecarUse::SeekableAssist => {
1901 let terminal_manifest = terminal_manifest.ok_or(FormatError::InvalidArchive(
1902 "terminal ManifestFooter/VolumeTrailer is required",
1903 ))?;
1904 if let Some(sidecar_manifest) = &sidecar.manifest_footer {
1905 if !manifest_bootstrap_fields_match(terminal_manifest, sidecar_manifest) {
1906 return Err(FormatError::InvalidArchive(
1907 "bootstrap sidecar conflicts with terminal ManifestFooter",
1908 ));
1909 }
1910 }
1911 terminal_manifest.clone()
1912 }
1913 BootstrapSidecarUse::NonSeekableRandomAccess => {
1914 let sidecar_manifest = sidecar
1915 .manifest_footer
1916 .as_ref()
1917 .ok_or(FormatError::ReaderUnsupported(
1918 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
1919 ))?;
1920 if let Some(terminal_manifest) = terminal_manifest {
1921 if !manifest_bootstrap_fields_match(terminal_manifest, sidecar_manifest) {
1922 return Err(FormatError::InvalidArchive(
1923 "bootstrap sidecar conflicts with terminal ManifestFooter",
1924 ));
1925 }
1926 }
1927 sidecar_manifest.clone()
1928 }
1929 };
1930 manifest_authority.validate_index_root_extent(parsed_crypto.fixed.block_size)?;
1931
1932 if let Some((offset, length)) = sidecar.index_root_records_section {
1933 let index_root_records = parse_sidecar_block_records(
1934 bootstrap_sidecar,
1935 parsed_crypto.fixed.block_size as usize,
1936 SidecarBlockRecordsSection {
1937 offset,
1938 length,
1939 extent: index_root_extent_from_manifest(&manifest_authority),
1940 data_kind: BlockKind::IndexRootData,
1941 parity_kind: BlockKind::IndexRootParity,
1942 structure: "IndexRoot",
1943 },
1944 )?;
1945 insert_sidecar_records(&mut blocks, index_root_records)?;
1946 }
1947
1948 let limits = metadata_limits(&parsed_crypto.fixed);
1949 let index_root_plaintext = load_metadata_object_from_parts(
1950 &blocks,
1951 ObjectLoadContext::index_root(
1952 &volume_header,
1953 &parsed_crypto.fixed,
1954 &subkeys,
1955 index_root_extent_from_manifest(&manifest_authority),
1956 ),
1957 manifest_authority.index_root_decompressed_size,
1958 )?;
1959 let index_root = IndexRoot::parse(
1960 &index_root_plaintext,
1961 parsed_crypto.fixed.has_dictionary != 0,
1962 limits,
1963 )?;
1964 if parsed_crypto.fixed.has_dictionary != 0 {
1965 if let Some((offset, length)) = sidecar.dictionary_records_section {
1966 let dictionary_records = parse_sidecar_block_records(
1967 bootstrap_sidecar,
1968 parsed_crypto.fixed.block_size as usize,
1969 SidecarBlockRecordsSection {
1970 offset,
1971 length,
1972 extent: dictionary_extent_from_index_root(&index_root)?,
1973 data_kind: BlockKind::DictionaryData,
1974 parity_kind: BlockKind::DictionaryParity,
1975 structure: "dictionary",
1976 },
1977 )?;
1978 insert_sidecar_records(&mut blocks, dictionary_records)?;
1979 }
1980 }
1981 let payload_dictionary = load_archive_dictionary(
1982 &blocks,
1983 &subkeys,
1984 &volume_header,
1985 &parsed_crypto.fixed,
1986 &index_root,
1987 )?;
1988
1989 Ok(Self {
1990 options,
1991 observed_archive_bytes,
1992 observed_volume_count: 1,
1993 subkeys,
1994 blocks,
1995 lazy_blocks: None,
1996 crypto_header_bytes: crypto_bytes.to_vec(),
1997 volume_header,
1998 crypto_header: parsed_crypto.fixed,
1999 manifest_footer: manifest_authority,
2000 volume_trailer: terminal_material
2001 .as_ref()
2002 .map(|(_, trailer, _)| trailer.clone()),
2003 root_auth_footer: terminal_material.and_then(|(_, _, root_auth)| root_auth),
2004 index_root,
2005 payload_dictionary,
2006 })
2007 }
2008
2009 pub fn list_index_entries(&self) -> Result<Vec<ArchiveIndexEntry>, FormatError> {
2015 let shards = self.load_all_index_shards()?;
2016 final_index_entry_winners(&shards)?
2017 .into_iter()
2018 .map(|(path, winner)| {
2019 Ok(ArchiveIndexEntry {
2020 path,
2021 file_data_size: winner.file_data_size,
2022 })
2023 })
2024 .collect()
2025 }
2026
2027 pub fn lookup_index_entry(&self, path: &str) -> Result<Option<ArchiveIndexEntry>, FormatError> {
2029 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2030 self.locate_index_file(&normalized)?
2031 .map(|located| archive_index_entry_from_loaded_file(&located.shard, located.file_index))
2032 .transpose()
2033 }
2034
2035 pub fn list_files(&self) -> Result<Vec<ArchiveEntry>, FormatError> {
2036 let shards = self.load_all_index_shards()?;
2037 final_index_entry_winners(&shards)?
2038 .into_iter()
2039 .map(|(path, winner)| {
2040 let shard = &shards[winner.shard_index];
2041 let member =
2042 self.decode_loaded_owned_tar_member(shard, winner.file_index, false)?;
2043 Ok(ArchiveEntry {
2044 path,
2045 file_data_size: winner.file_data_size,
2046 kind: member.kind,
2047 mode: member.mode,
2048 mtime: member.mtime,
2049 diagnostics: member.diagnostics,
2050 })
2051 })
2052 .collect()
2053 }
2054
2055 pub fn extract_file(&self, path: &str) -> Result<Option<Vec<u8>>, FormatError> {
2062 self.extract_member(path)?
2063 .map(|member| {
2064 if member.kind != TarEntryKind::Regular {
2065 return Err(FormatError::ReaderUnsupported(
2066 "extract_file returns only regular file payloads",
2067 ));
2068 }
2069 Ok(member.data)
2070 })
2071 .transpose()
2072 }
2073
2074 pub fn extract_file_with_diagnostics(
2077 &self,
2078 path: &str,
2079 ) -> Result<Option<ExtractedRegularFile>, FormatError> {
2080 self.extract_member(path)?
2081 .map(|member| {
2082 if member.kind != TarEntryKind::Regular {
2083 return Err(FormatError::ReaderUnsupported(
2084 "extract_file_with_diagnostics returns only regular file payloads",
2085 ));
2086 }
2087 Ok((member.data, member.diagnostics))
2088 })
2089 .transpose()
2090 }
2091
2092 pub fn extract_file_to_writer<W: Write>(
2098 &self,
2099 path: &str,
2100 writer: &mut W,
2101 ) -> Result<Option<Vec<MetadataDiagnostic>>, ExtractError> {
2102 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2103 self.locate_index_file(&normalized)?
2104 .map(|located| {
2105 self.stream_loaded_file_to_writer(&located.shard, located.file_index, writer)
2106 })
2107 .transpose()
2108 }
2109
2110 pub fn extract_file_to_writer_with_progress<W: Write>(
2113 &self,
2114 path: &str,
2115 writer: &mut W,
2116 progress: &mut dyn ArchiveExtractProgressSink,
2117 ) -> Result<Option<Vec<MetadataDiagnostic>>, ExtractError> {
2118 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2119 self.locate_index_file(&normalized)?
2120 .map(|located| {
2121 self.stream_loaded_file_to_writer_with_progress(
2122 &located.shard,
2123 located.file_index,
2124 writer,
2125 progress,
2126 )
2127 })
2128 .transpose()
2129 }
2130
2131 pub fn extract_member(
2132 &self,
2133 path: &str,
2134 ) -> Result<Option<ExtractedArchiveMember>, FormatError> {
2135 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2136 self.locate_index_file(&normalized)?
2137 .map(|located| self.extract_loaded_member(&located.shard, located.file_index))
2138 .transpose()
2139 }
2140
2141 pub fn extract_file_to(
2142 &self,
2143 path: &str,
2144 root: &std::path::Path,
2145 options: SafeExtractionOptions,
2146 ) -> Result<Option<Vec<MetadataDiagnostic>>, FormatError> {
2147 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2148 self.locate_index_file(&normalized)?
2149 .map(|located| {
2150 self.stream_loaded_file_to_path(&located.shard, located.file_index, root, options)
2151 })
2152 .transpose()
2153 }
2154
2155 pub fn extract_indexed_files_to(
2156 &self,
2157 root: &std::path::Path,
2158 options: SafeExtractionOptions,
2159 jobs: usize,
2160 ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
2161 if jobs == 0 {
2162 return Err(FormatError::ReaderUnsupported("jobs must be at least 1"));
2163 }
2164
2165 let shards = self.load_all_index_shards()?;
2166 let entries = final_index_entry_winners(&shards)?.into_iter().collect();
2167 self.extract_winning_index_entries_to(&shards, entries, root, options, jobs)
2168 }
2169
2170 pub fn verify(&self) -> Result<(), FormatError> {
2171 self.verify_content().map(|_| ())
2172 }
2173
2174 pub fn verify_content(&self) -> Result<ArchiveContentVerification<'_>, FormatError> {
2175 self.verify_content_with_parity_policy(
2176 ParityReadPolicy::Always,
2177 ContentVerificationMode::Full,
2178 )
2179 }
2180
2181 pub fn verify_content_fast(&self) -> Result<ArchiveContentVerification<'_>, FormatError> {
2182 if self.fast_verify_defers_payload_semantics() {
2183 self.verify_payload_record_integrity_only()?;
2184 return Ok(ArchiveContentVerification {
2185 archive: self,
2186 mode: ContentVerificationMode::Fast,
2187 });
2188 }
2189 self.verify_content_with_parity_policy(
2190 ParityReadPolicy::RepairOnly,
2191 ContentVerificationMode::Fast,
2192 )
2193 }
2194
2195 pub fn fast_verify_defers_payload_semantics(&self) -> bool {
2196 self.root_auth_footer.is_none()
2197 && self.crypto_header.has_dictionary == 0
2198 && !self.crypto_header.aead_algo.is_encrypted()
2199 && self.crypto_header.fec_parity_shards == 0
2200 && self.crypto_header.index_fec_parity_shards == 0
2201 && self.crypto_header.index_root_fec_parity_shards == 0
2202 && self.manifest_footer.index_root_parity_block_count == 0
2203 }
2204
2205 fn verify_payload_record_integrity_only(&self) -> Result<(), FormatError> {
2206 let tables = self.load_payload_index_tables()?;
2207 let block_provider = self.block_provider();
2208 let block_size = self.crypto_header.block_size as u64;
2209 for envelope in tables.envelopes.values() {
2210 if envelope.parity_block_count != 0 {
2211 return Err(FormatError::InvalidArchive(
2212 "fast payload record scan requires zero parity",
2213 ));
2214 }
2215 let expected_encrypted_size = checked_u64_mul(
2216 envelope.data_block_count as u64,
2217 block_size,
2218 "payload envelope encrypted size",
2219 )?;
2220 if envelope.encrypted_size as u64 != expected_encrypted_size {
2221 return Err(FormatError::InvalidArchive(
2222 "payload envelope encrypted_size mismatch",
2223 ));
2224 }
2225 for offset in 0..envelope.data_block_count {
2226 let block_index =
2227 checked_u64_add(envelope.first_block_index, offset as u64, "payload")?;
2228 let record = block_provider
2229 .block(block_index)?
2230 .ok_or(FormatError::InvalidArchive("payload data block is missing"))?;
2231 if record.kind != BlockKind::PayloadData {
2232 return Err(FormatError::InvalidArchive(
2233 "payload data block has unexpected kind",
2234 ));
2235 }
2236 let should_be_last = offset + 1 == envelope.data_block_count;
2237 if record.is_last_data() != should_be_last {
2238 return Err(FormatError::InvalidArchive(
2239 "payload last-data flag is not on the final data block",
2240 ));
2241 }
2242 }
2243 }
2244 Ok(())
2245 }
2246
2247 fn verify_content_with_parity_policy(
2248 &self,
2249 parity_policy: ParityReadPolicy,
2250 mode: ContentVerificationMode,
2251 ) -> Result<ArchiveContentVerification<'_>, FormatError> {
2252 let tables = self.load_payload_index_tables()?;
2253 let streamed = self.scan_seekable_payload(
2254 &tables,
2255 u64::MAX,
2256 NoopTarStreamObserver,
2257 true,
2258 parity_policy,
2259 )?;
2260 self.validate_streamed_payload_summary(&tables, &streamed, false, true)?;
2261 Ok(ArchiveContentVerification {
2262 archive: self,
2263 mode,
2264 })
2265 }
2266
2267 pub fn repair_patches(&self) -> Result<Vec<ArchiveRepairPatch>, FormatError> {
2268 let lazy_source = self
2269 .lazy_blocks
2270 .as_ref()
2271 .ok_or(FormatError::ReaderUnsupported(
2272 "repair output requires seekable archive input",
2273 ))?;
2274 if !lazy_source.is_complete_volume_set() {
2275 return Err(FormatError::ReaderUnsupported(
2276 "repair output requires all archive volumes",
2277 ));
2278 }
2279
2280 let shards = self.load_all_index_shards()?;
2281 let rows = self.root_auth_fec_layout_rows(&shards)?;
2282 let block_provider = self.block_provider();
2283 let mut patches = BTreeMap::<u64, ArchiveRepairPatch>::new();
2284 for row in rows.into_iter().filter(|row| row.present) {
2285 self.collect_repair_patches_for_object(
2286 &block_provider,
2287 lazy_source,
2288 row,
2289 &mut patches,
2290 )?;
2291 }
2292 Ok(patches.into_values().collect())
2293 }
2294
2295 pub fn extract_all_to(
2296 &self,
2297 root: &std::path::Path,
2298 options: SafeExtractionOptions,
2299 ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
2300 let tables = self.load_payload_index_tables()?;
2301 if final_index_entry_winners(&tables.shards)?.len() as u64 != tables.file_count {
2302 return Err(FormatError::ReaderUnsupported(
2303 FAST_FULL_EXTRACT_UNIQUE_PATHS_UNSUPPORTED,
2304 ));
2305 }
2306
2307 let observer = TarStreamFilesystemRestoreObserver::new(root, options);
2308 let streamed = self.scan_seekable_payload(
2309 &tables,
2310 total_extraction_size_cap(self.options, self.observed_archive_bytes),
2311 observer,
2312 false,
2313 ParityReadPolicy::RepairOnly,
2314 )?;
2315 self.validate_streamed_payload_summary(&tables, &streamed, true, false)?;
2316 streamed
2317 .tar
2318 .members
2319 .into_iter()
2320 .map(|member| Ok((utf8_path(&member.path)?, member.diagnostics)))
2321 .collect()
2322 }
2323
2324 fn collect_repair_patches_for_object(
2325 &self,
2326 blocks: &impl BlockProvider,
2327 source: &SeekableBlockSource,
2328 row: FecLayoutObjectRow,
2329 patches: &mut BTreeMap<u64, ArchiveRepairPatch>,
2330 ) -> Result<(), FormatError> {
2331 let (data_kind, parity_kind, data_max, parity_max) =
2332 self.fec_object_class_shape(row.object_class)?;
2333 let extent = ObjectExtent {
2334 first_block_index: row.first_block_index,
2335 data_block_count: row.data_block_count,
2336 parity_block_count: row.parity_block_count,
2337 encrypted_size: row.encrypted_size,
2338 };
2339 validate_object_extent(extent, &self.crypto_header, data_max, parity_max)?;
2340
2341 let block_size = self.crypto_header.block_size as usize;
2342 let data_count = extent.data_block_count as usize;
2343 let parity_count = extent.parity_block_count as usize;
2344 let mut data_shards = Vec::with_capacity(data_count);
2345 let mut parity_shards = Vec::with_capacity(parity_count);
2346
2347 for offset in 0..data_count {
2348 let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
2349 match blocks.block(block_index)? {
2350 Some(record) => {
2351 if record.kind != data_kind {
2352 return Err(FormatError::InvalidArchive(
2353 "object data block has unexpected kind",
2354 ));
2355 }
2356 let should_be_last = offset + 1 == data_count;
2357 if record.is_last_data() != should_be_last {
2358 return Err(FormatError::InvalidArchive(
2359 "object last-data flag is not on the final data block",
2360 ));
2361 }
2362 data_shards.push(Some(record.payload.clone()));
2363 }
2364 None => data_shards.push(None),
2365 }
2366 }
2367
2368 for offset in 0..parity_count {
2369 let block_index = checked_u64_add(
2370 extent.first_block_index,
2371 data_count as u64 + offset as u64,
2372 "object",
2373 )?;
2374 match blocks.block(block_index)? {
2375 Some(record) => {
2376 if record.kind != parity_kind {
2377 return Err(FormatError::InvalidArchive(
2378 "object parity block has unexpected kind",
2379 ));
2380 }
2381 if record.is_last_data() {
2382 return Err(FormatError::InvalidArchive(
2383 "object parity block has last-data flag",
2384 ));
2385 }
2386 parity_shards.push(Some(record.payload.clone()));
2387 }
2388 None => parity_shards.push(None),
2389 }
2390 }
2391
2392 let repaired_data = repair_data_gf16(&data_shards, &parity_shards, block_size)?;
2393 for (offset, payload) in repaired_data.iter().enumerate() {
2394 if data_shards[offset].is_none() {
2395 let block_index =
2396 checked_u64_add(extent.first_block_index, offset as u64, "object")?;
2397 let flags = if offset + 1 == data_count { 0x01 } else { 0 };
2398 self.insert_repair_patch(
2399 patches,
2400 source,
2401 block_index,
2402 data_kind,
2403 flags,
2404 payload.clone(),
2405 )?;
2406 }
2407 }
2408
2409 if parity_count > 0 {
2410 let repaired_parity = encode_parity_gf16(&repaired_data, parity_count)?;
2411 for (offset, payload) in repaired_parity.into_iter().enumerate() {
2412 if parity_shards[offset].as_ref() != Some(&payload) {
2413 let block_index = checked_u64_add(
2414 extent.first_block_index,
2415 data_count as u64 + offset as u64,
2416 "object",
2417 )?;
2418 self.insert_repair_patch(
2419 patches,
2420 source,
2421 block_index,
2422 parity_kind,
2423 0,
2424 payload,
2425 )?;
2426 }
2427 }
2428 }
2429
2430 Ok(())
2431 }
2432
2433 fn insert_repair_patch(
2434 &self,
2435 patches: &mut BTreeMap<u64, ArchiveRepairPatch>,
2436 source: &SeekableBlockSource,
2437 block_index: u64,
2438 kind: BlockKind,
2439 flags: u8,
2440 payload: Vec<u8>,
2441 ) -> Result<(), FormatError> {
2442 let (volume_index, record_offset) = source.record_location(block_index)?;
2443 let record = BlockRecord {
2444 block_index,
2445 kind,
2446 flags,
2447 payload,
2448 record_crc32c: 0,
2449 };
2450 let patch = ArchiveRepairPatch {
2451 volume_index,
2452 block_index,
2453 record_offset,
2454 record_bytes: record.to_bytes(),
2455 };
2456 if let Some(existing) = patches.insert(block_index, patch.clone()) {
2457 if existing != patch {
2458 return Err(FormatError::InvalidArchive(
2459 "conflicting repair patch for BlockRecord",
2460 ));
2461 }
2462 }
2463 Ok(())
2464 }
2465
2466 fn load_payload_index_tables(&self) -> Result<PayloadIndexTables, FormatError> {
2467 if self.index_root.header.file_count > DIRECTORY_HINT_REQUIRED_FILE_COUNT
2468 && self.index_root.directory_hint_shards.is_empty()
2469 {
2470 return Err(FormatError::InvalidArchive(
2471 "IndexRoot file_count requires directory hints",
2472 ));
2473 }
2474
2475 let shards = self.load_all_index_shards()?;
2476 let mut file_count = 0u64;
2477 let mut frames = BTreeMap::<u64, FrameEntry>::new();
2478 let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
2479
2480 for shard in &shards {
2481 file_count = file_count
2482 .checked_add(shard.files.len() as u64)
2483 .ok_or(FormatError::InvalidArchive("file count overflow"))?;
2484 for frame in &shard.frames {
2485 if let Some(existing) = frames.insert(frame.frame_index, frame.clone()) {
2486 if existing != *frame {
2487 return Err(FormatError::InvalidArchive(
2488 "duplicate FrameEntry rows do not match",
2489 ));
2490 }
2491 }
2492 }
2493 for envelope in &shard.envelopes {
2494 if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
2495 {
2496 if existing != *envelope {
2497 return Err(FormatError::InvalidArchive(
2498 "duplicate EnvelopeEntry rows do not match",
2499 ));
2500 }
2501 }
2502 }
2503 }
2504 validate_global_file_table_order(&shards)?;
2505
2506 if file_count != self.index_root.header.file_count {
2507 return Err(FormatError::InvalidArchive(
2508 "IndexRoot file_count does not match decoded shards",
2509 ));
2510 }
2511 verify_dense_keys(&frames, self.index_root.header.frame_count, "FrameEntry")?;
2512 verify_dense_keys(
2513 &envelopes,
2514 self.index_root.header.envelope_count,
2515 "EnvelopeEntry",
2516 )?;
2517 validate_envelope_frame_coverage(&frames, &envelopes)?;
2518 self.validate_encrypted_object_block_ranges(&envelopes)?;
2519
2520 let payload_block_count = envelopes.values().try_fold(0u64, |sum, envelope| {
2521 sum.checked_add(envelope.data_block_count as u64)
2522 .ok_or(FormatError::InvalidArchive("payload block count overflow"))
2523 })?;
2524 if payload_block_count != self.index_root.header.payload_block_count {
2525 return Err(FormatError::InvalidArchive(
2526 "IndexRoot payload_block_count does not match envelopes",
2527 ));
2528 }
2529
2530 Ok(PayloadIndexTables {
2531 shards,
2532 file_count,
2533 frames,
2534 envelopes,
2535 })
2536 }
2537
2538 fn scan_seekable_payload<O: TarStreamObserver>(
2539 &self,
2540 tables: &PayloadIndexTables,
2541 extraction_cap: u64,
2542 observer: O,
2543 hash_content: bool,
2544 parity_policy: ParityReadPolicy,
2545 ) -> Result<StreamedPayloadSummary, FormatError> {
2546 let mut tar = TarStreamSummaryValidator::with_observer(
2547 self.crypto_header.max_path_length,
2548 extraction_cap,
2549 usize::MAX,
2550 self.index_root.header.file_count,
2551 observer,
2552 );
2553 let mut content_hasher = hash_content.then(Sha256::new);
2554 let mut streamed_frames = Vec::with_capacity(tables.frames.len());
2555 let streamed_envelopes = tables
2556 .envelopes
2557 .values()
2558 .map(|envelope| StreamedEnvelopeSummary {
2559 envelope_index: envelope.envelope_index,
2560 first_block_index: envelope.first_block_index,
2561 data_block_count: envelope.data_block_count,
2562 parity_block_count: envelope.parity_block_count,
2563 encrypted_size: envelope.encrypted_size,
2564 plaintext_size: envelope.plaintext_size,
2565 first_frame_index: envelope.first_frame_index,
2566 frame_count: envelope.frame_count,
2567 })
2568 .collect::<Vec<_>>();
2569 let mut cached_envelope_index = None;
2570 let mut cached_envelope_plaintext = Vec::new();
2571 let mut decompressor = self.new_payload_decompressor()?;
2572
2573 for frame in tables.frames.values() {
2574 let envelope =
2575 tables
2576 .envelopes
2577 .get(&frame.envelope_index)
2578 .ok_or(FormatError::InvalidArchive(
2579 "FrameEntry references missing EnvelopeEntry",
2580 ))?;
2581 if cached_envelope_index != Some(envelope.envelope_index) {
2582 cached_envelope_plaintext = self.load_payload_envelope(envelope, parity_policy)?;
2583 cached_envelope_index = Some(envelope.envelope_index);
2584 }
2585 let compressed = slice(
2586 &cached_envelope_plaintext,
2587 frame.offset_in_envelope as usize,
2588 frame.compressed_size as usize,
2589 "FrameEntry",
2590 )?;
2591 let tar_stream_offset = tar.tar_total_size();
2592 let decoded = self.decompress_payload_frame_with(
2593 &mut decompressor,
2594 compressed,
2595 frame.decompressed_size,
2596 )?;
2597 if decoded.is_empty() {
2598 return Err(FormatError::InvalidArchive(
2599 "zstd payload frame decompressed to zero bytes",
2600 ));
2601 }
2602 if let Some(hasher) = &mut content_hasher {
2603 hasher.update(&decoded);
2604 }
2605 tar.observe(&decoded)?;
2606 streamed_frames.push(StreamedFrameSummary {
2607 frame_index: frame.frame_index,
2608 envelope_index: frame.envelope_index,
2609 offset_in_envelope: frame.offset_in_envelope,
2610 compressed_size: u32::try_from(compressed.len()).map_err(|_| {
2611 FormatError::InvalidArchive("FrameEntry.compressed_size overflow")
2612 })?,
2613 decompressed_size: u32::try_from(decoded.len()).map_err(|_| {
2614 FormatError::InvalidArchive("FrameEntry.decompressed_size overflow")
2615 })?,
2616 tar_stream_offset,
2617 });
2618 }
2619
2620 let mut content_sha256 = [0u8; 32];
2621 if let Some(hasher) = content_hasher {
2622 let digest = hasher.finalize();
2623 content_sha256.copy_from_slice(&digest);
2624 }
2625 Ok(StreamedPayloadSummary {
2626 tar: tar.finish()?,
2627 content_sha256,
2628 envelopes: streamed_envelopes,
2629 frames: streamed_frames,
2630 })
2631 }
2632
2633 fn validate_streamed_payload_summary(
2634 &self,
2635 tables: &PayloadIndexTables,
2636 streamed: &StreamedPayloadSummary,
2637 enforce_total_extraction_cap: bool,
2638 enforce_content_sha256: bool,
2639 ) -> Result<(), FormatError> {
2640 if enforce_total_extraction_cap
2641 && streamed.tar.total_extraction_size
2642 > total_extraction_size_cap(self.options, self.observed_archive_bytes)
2643 {
2644 return Err(FormatError::ReaderUnsupported(
2645 "total extraction size exceeds configured cap",
2646 ));
2647 }
2648
2649 let streamed_payload_block_count =
2650 streamed.envelopes.iter().try_fold(0u64, |sum, envelope| {
2651 sum.checked_add(envelope.data_block_count as u64)
2652 .ok_or(FormatError::InvalidArchive("payload block count overflow"))
2653 })?;
2654 if streamed_payload_block_count != self.index_root.header.payload_block_count {
2655 return Err(FormatError::InvalidArchive(
2656 "streamed payload block count does not match IndexRoot",
2657 ));
2658 }
2659
2660 if streamed.tar.tar_total_size != self.index_root.header.tar_total_size {
2661 return Err(FormatError::InvalidArchive(
2662 "IndexRoot tar_total_size does not match streamed tar stream",
2663 ));
2664 }
2665 if enforce_content_sha256
2666 && streamed.content_sha256 != self.index_root.header.content_sha256
2667 {
2668 return Err(FormatError::InvalidArchive(
2669 "IndexRoot content_sha256 does not match decoded tar stream",
2670 ));
2671 }
2672
2673 let streamed_envelopes = streamed.envelope_map()?;
2674 for envelope in tables.envelopes.values() {
2675 let actual = streamed_envelopes.get(&envelope.envelope_index).ok_or(
2676 FormatError::InvalidArchive(
2677 "metadata references missing streamed payload envelope",
2678 ),
2679 )?;
2680 if actual.first_block_index != envelope.first_block_index
2681 || actual.data_block_count != envelope.data_block_count
2682 || actual.parity_block_count != envelope.parity_block_count
2683 || actual.encrypted_size != envelope.encrypted_size
2684 || actual.plaintext_size != envelope.plaintext_size
2685 || actual.first_frame_index != envelope.first_frame_index
2686 || actual.frame_count != envelope.frame_count
2687 {
2688 return Err(FormatError::InvalidArchive(
2689 "EnvelopeEntry does not match streamed payload envelope",
2690 ));
2691 }
2692 }
2693
2694 let streamed_frames = streamed.frame_map()?;
2695 for frame in tables.frames.values() {
2696 let actual =
2697 streamed_frames
2698 .get(&frame.frame_index)
2699 .ok_or(FormatError::InvalidArchive(
2700 "metadata references missing streamed payload frame",
2701 ))?;
2702 if actual.envelope_index != frame.envelope_index
2703 || actual.offset_in_envelope != frame.offset_in_envelope
2704 || actual.compressed_size != frame.compressed_size
2705 || actual.decompressed_size != frame.decompressed_size
2706 || actual.tar_stream_offset != frame.tar_stream_offset
2707 || streamed.frame_flags(actual)? != frame.flags
2708 {
2709 return Err(FormatError::InvalidArchive(
2710 "FrameEntry does not match streamed payload frame",
2711 ));
2712 }
2713 }
2714
2715 let streamed_members = streamed.member_start_map()?;
2716 if streamed.tar.members.len() as u64 != tables.file_count {
2717 return Err(FormatError::InvalidArchive(
2718 "streamed tar member count does not match decoded shards",
2719 ));
2720 }
2721 let mut file_extents = Vec::new();
2722 let mut directory_hint_map = DirectoryHintMap::new();
2723 for (shard_row_index, shard) in tables.shards.iter().enumerate() {
2724 let shard_row_index = u32::try_from(shard_row_index)
2725 .map_err(|_| FormatError::InvalidArchive("shard row index overflow"))?;
2726 for idx in 0..shard.files.len() {
2727 let file = &shard.files[idx];
2728 let start =
2729 shard
2730 .tar_member_group_start(idx)
2731 .ok_or(FormatError::InvalidArchive(
2732 "FileEntry tar member start is missing",
2733 ))?;
2734 file_extents.push((start, file.tar_member_group_size));
2735 let path = shard
2736 .file_path(idx)
2737 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2738 let member = streamed_members
2739 .get(&start)
2740 .ok_or(FormatError::InvalidArchive(
2741 "FileEntry tar member start is missing from streamed tar",
2742 ))?;
2743 if member.path != path {
2744 return Err(FormatError::InvalidArchive(
2745 "tar member path does not match FileEntry path",
2746 ));
2747 }
2748 if member.logical_size != file.file_data_size {
2749 return Err(FormatError::InvalidArchive(
2750 "tar member size does not match FileEntry file_data_size",
2751 ));
2752 }
2753 if member.group_size != file.tar_member_group_size {
2754 return Err(FormatError::InvalidArchive(
2755 "FileEntry does not match streamed tar member",
2756 ));
2757 }
2758 add_expected_directory_hint_rows(
2759 &mut directory_hint_map,
2760 shard_row_index,
2761 path,
2762 member.kind,
2763 );
2764 }
2765 }
2766 validate_file_extent_coverage_ranges(&file_extents, self.index_root.header.tar_total_size)?;
2767 if !self.index_root.directory_hint_shards.is_empty() {
2768 let hint_tables = self.load_all_directory_hint_tables()?;
2769 validate_directory_hint_tables_against_expected(&hint_tables, &directory_hint_map)?;
2770 }
2771
2772 Ok(())
2773 }
2774
2775 pub(crate) fn from_streamed_parts(
2776 parts: StreamedArchiveOpenParts,
2777 ) -> Result<Self, FormatError> {
2778 let limits = metadata_limits(&parts.crypto_header);
2779 let index_root_plaintext = load_metadata_object_from_parts(
2780 &parts.blocks,
2781 ObjectLoadContext::index_root(
2782 &parts.volume_header,
2783 &parts.crypto_header,
2784 &parts.subkeys,
2785 ObjectExtent {
2786 first_block_index: parts.manifest_footer.index_root_first_block,
2787 data_block_count: parts.manifest_footer.index_root_data_block_count,
2788 parity_block_count: parts.manifest_footer.index_root_parity_block_count,
2789 encrypted_size: parts.manifest_footer.index_root_encrypted_size,
2790 },
2791 ),
2792 parts.manifest_footer.index_root_decompressed_size,
2793 )?;
2794 let index_root = IndexRoot::parse(
2795 &index_root_plaintext,
2796 parts.crypto_header.has_dictionary != 0,
2797 limits,
2798 )?;
2799 let payload_dictionary = load_archive_dictionary(
2800 &parts.blocks,
2801 &parts.subkeys,
2802 &parts.volume_header,
2803 &parts.crypto_header,
2804 &index_root,
2805 )?;
2806
2807 Ok(Self {
2808 options: parts.options,
2809 observed_archive_bytes: parts.observed_archive_bytes,
2810 observed_volume_count: 1,
2811 subkeys: parts.subkeys,
2812 blocks: parts.blocks,
2813 lazy_blocks: None,
2814 crypto_header_bytes: parts.crypto_header_bytes,
2815 volume_header: parts.volume_header,
2816 crypto_header: parts.crypto_header,
2817 manifest_footer: parts.manifest_footer,
2818 volume_trailer: Some(parts.volume_trailer),
2819 root_auth_footer: parts.root_auth_footer,
2820 index_root,
2821 payload_dictionary,
2822 })
2823 }
2824
2825 pub(crate) fn verify_streamed_payload_summary(
2826 &self,
2827 streamed: &StreamedPayloadSummary,
2828 ) -> Result<(), FormatError> {
2829 let tables = self.load_payload_index_tables()?;
2830 self.validate_streamed_payload_summary(&tables, streamed, true, true)
2831 }
2832
2833 pub fn verify_root_auth_with<F>(&self, verifier: F) -> Result<RootAuthVerification, FormatError>
2834 where
2835 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
2836 {
2837 let content_verification = self.verify_content()?;
2838 self.verify_root_auth_with_verified_content(&content_verification, verifier)
2839 }
2840
2841 pub fn verify_root_auth_with_verified_content<F>(
2842 &self,
2843 content_verification: &ArchiveContentVerification<'_>,
2844 mut verifier: F,
2845 ) -> Result<RootAuthVerification, FormatError>
2846 where
2847 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
2848 {
2849 if !std::ptr::eq(content_verification.archive, self) {
2850 return Err(FormatError::InvalidArchive(
2851 "content verification does not match archive",
2852 ));
2853 }
2854 if content_verification.mode != ContentVerificationMode::Full {
2855 return Err(FormatError::ReaderUnsupported(
2856 "RootAuth verification requires full archive content verification",
2857 ));
2858 }
2859 let footer = self
2860 .root_auth_footer
2861 .as_ref()
2862 .ok_or(FormatError::ReaderUnsupported("root-auth footer is absent"))?;
2863 let material = self.recompute_root_auth_material(footer)?;
2864 if material.critical_metadata_digest != footer.critical_metadata_digest
2865 || material.index_digest != footer.index_digest
2866 || material.fec_layout_digest != footer.fec_layout_digest
2867 || material.data_block_merkle_root != footer.data_block_merkle_root
2868 || material.signer_identity_digest != footer.signer_identity_digest
2869 || material.archive_root != footer.archive_root
2870 || material.total_data_block_count != footer.total_data_block_count
2871 {
2872 return Err(FormatError::InvalidArchive(
2873 "RootAuthFooter commitments do not match recomputed archive root",
2874 ));
2875 }
2876 if !verifier(footer, &material.archive_root)? {
2877 return Err(FormatError::InvalidArchive(
2878 "root-auth authenticator verification failed",
2879 ));
2880 }
2881 Ok(RootAuthVerification {
2882 format_version: footer.format_version,
2883 volume_format_rev: footer.volume_format_rev,
2884 archive_root: material.archive_root,
2885 authenticator_id: footer.authenticator_id,
2886 signer_identity_type: footer.signer_identity_type,
2887 signer_identity_bytes: footer.signer_identity_bytes.clone(),
2888 total_data_block_count: footer.total_data_block_count,
2889 diagnostics: self.root_auth_success_diagnostics(),
2890 })
2891 }
2892
2893 fn load_all_index_shards(&self) -> Result<Vec<IndexShard>, FormatError> {
2894 parallel_map_ref(&self.index_root.shards, self.options.jobs, |entry| {
2895 self.load_index_shard(entry)
2896 })
2897 }
2898
2899 fn load_index_shard(&self, entry: &ShardEntry) -> Result<IndexShard, FormatError> {
2900 let block_provider = self.block_provider();
2901 let plaintext = load_metadata_object_from_parts(
2902 &block_provider,
2903 ObjectLoadContext::index_shard(
2904 &self.volume_header,
2905 &self.crypto_header,
2906 &self.subkeys,
2907 entry,
2908 ),
2909 entry.decompressed_size,
2910 )?;
2911 IndexShard::parse(&plaintext, entry, self.metadata_limits())
2912 }
2913
2914 fn load_all_directory_hint_tables(&self) -> Result<Vec<DirectoryHintTable>, FormatError> {
2915 parallel_map_ref(
2916 &self.index_root.directory_hint_shards,
2917 self.options.jobs,
2918 |entry| self.load_directory_hint_table(entry),
2919 )
2920 }
2921
2922 fn load_directory_hint_table(
2923 &self,
2924 entry: &DirectoryHintShardEntry,
2925 ) -> Result<DirectoryHintTable, FormatError> {
2926 let block_provider = self.block_provider();
2927 let plaintext = load_metadata_object_from_parts(
2928 &block_provider,
2929 ObjectLoadContext::directory_hint(
2930 &self.volume_header,
2931 &self.crypto_header,
2932 &self.subkeys,
2933 entry,
2934 ),
2935 entry.decompressed_size,
2936 )?;
2937 DirectoryHintTable::parse(
2938 &plaintext,
2939 entry,
2940 self.index_root.header.shard_count,
2941 self.metadata_limits(),
2942 )
2943 }
2944
2945 fn load_payload_envelope(
2946 &self,
2947 envelope: &EnvelopeEntry,
2948 parity_policy: ParityReadPolicy,
2949 ) -> Result<Vec<u8>, FormatError> {
2950 let block_provider = self.block_provider();
2951 let plaintext = load_decrypted_object_from_parts_with_parity_policy(
2952 &block_provider,
2953 ObjectLoadContext::payload(
2954 &self.volume_header,
2955 &self.crypto_header,
2956 &self.subkeys,
2957 envelope,
2958 ),
2959 parity_policy,
2960 )?;
2961 if plaintext.len() != envelope.plaintext_size as usize {
2962 return Err(FormatError::InvalidArchive(
2963 "payload envelope plaintext_size mismatch",
2964 ));
2965 }
2966 Ok(plaintext)
2967 }
2968
2969 fn locate_index_file(
2970 &self,
2971 normalized: &[u8],
2972 ) -> Result<Option<LocatedIndexFile>, FormatError> {
2973 let candidate_indexes = self
2974 .index_root
2975 .candidate_shards_for_path(normalized, self.metadata_limits())?;
2976 let mut winner: Option<LocatedIndexFile> = None;
2977
2978 for row_index in candidate_indexes {
2979 let locating =
2980 self.index_root
2981 .shards
2982 .get(row_index)
2983 .ok_or(FormatError::InvalidArchive(
2984 "candidate shard row is out of bounds",
2985 ))?;
2986 let shard = self.load_index_shard(locating)?;
2987 if let Some(file_index) = shard.lookup_file_index(normalized) {
2988 let start =
2989 shard
2990 .tar_member_group_start(file_index)
2991 .ok_or(FormatError::InvalidArchive(
2992 "FileEntry tar member start is missing",
2993 ))?;
2994 if winner
2995 .as_ref()
2996 .map(|existing| start > existing.start)
2997 .unwrap_or(true)
2998 {
2999 winner = Some(LocatedIndexFile {
3000 shard,
3001 file_index,
3002 start,
3003 });
3004 }
3005 }
3006 }
3007
3008 Ok(winner)
3009 }
3010
3011 fn extract_loaded_member(
3012 &self,
3013 shard: &IndexShard,
3014 file_index: usize,
3015 ) -> Result<ExtractedArchiveMember, FormatError> {
3016 let member = self.extract_loaded_owned_tar_member(shard, file_index)?;
3017 Ok(ExtractedArchiveMember {
3018 path: utf8_path(&member.path)?,
3019 kind: member.kind,
3020 data: member.data,
3021 link_target: member
3022 .link_target
3023 .map(|target| utf8_path(&target))
3024 .transpose()?,
3025 diagnostics: member.diagnostics,
3026 })
3027 }
3028
3029 fn extract_loaded_owned_tar_member(
3030 &self,
3031 shard: &IndexShard,
3032 file_index: usize,
3033 ) -> Result<OwnedTarMember, FormatError> {
3034 self.decode_loaded_owned_tar_member(shard, file_index, true)
3035 }
3036
3037 fn stream_loaded_file_to_writer<W: Write>(
3038 &self,
3039 shard: &IndexShard,
3040 file_index: usize,
3041 writer: &mut W,
3042 ) -> Result<Vec<MetadataDiagnostic>, ExtractError> {
3043 let file = shard
3044 .files
3045 .get(file_index)
3046 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3047 self.validate_total_extraction_size(file.file_data_size)?;
3048 let expected_path = shard
3049 .file_path(file_index)
3050 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3051 let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
3052 stream_regular_tar_member_group_to_writer(
3053 &mut reader,
3054 expected_path,
3055 file.file_data_size,
3056 file.tar_member_group_size,
3057 self.crypto_header.max_path_length,
3058 writer,
3059 )
3060 }
3061
3062 fn stream_loaded_file_to_writer_with_progress<W: Write>(
3063 &self,
3064 shard: &IndexShard,
3065 file_index: usize,
3066 writer: &mut W,
3067 progress: &mut dyn ArchiveExtractProgressSink,
3068 ) -> Result<Vec<MetadataDiagnostic>, ExtractError> {
3069 let file = shard
3070 .files
3071 .get(file_index)
3072 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3073 self.validate_total_extraction_size(file.file_data_size)?;
3074 let expected_path = shard
3075 .file_path(file_index)
3076 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3077 let archive_path = utf8_path(expected_path)?;
3078 let mut progress_writer =
3079 ExtractProgressWriter::new(writer, &archive_path, file.file_data_size, progress);
3080 let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
3081 stream_regular_tar_member_group_to_writer(
3082 &mut reader,
3083 expected_path,
3084 file.file_data_size,
3085 file.tar_member_group_size,
3086 self.crypto_header.max_path_length,
3087 &mut progress_writer,
3088 )
3089 }
3090
3091 fn stream_loaded_file_to_path(
3092 &self,
3093 shard: &IndexShard,
3094 file_index: usize,
3095 root: &std::path::Path,
3096 options: SafeExtractionOptions,
3097 ) -> Result<Vec<MetadataDiagnostic>, FormatError> {
3098 let file = shard
3099 .files
3100 .get(file_index)
3101 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3102 self.validate_total_extraction_size(file.file_data_size)?;
3103 let expected_path = shard
3104 .file_path(file_index)
3105 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3106 let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
3107 restore_streaming_tar_member_group(
3108 root,
3109 expected_path,
3110 file.file_data_size,
3111 file.tar_member_group_size,
3112 self.crypto_header.max_path_length,
3113 options,
3114 &mut reader,
3115 )
3116 .map_err(format_error_from_extract_error)
3117 }
3118
3119 fn extract_winning_index_entries_to(
3120 &self,
3121 shards: &[IndexShard],
3122 entries: Vec<(String, WinningIndexEntry)>,
3123 root: &std::path::Path,
3124 options: SafeExtractionOptions,
3125 jobs: usize,
3126 ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
3127 if entries.is_empty() {
3128 return Ok(Vec::new());
3129 }
3130 if jobs <= 1 || entries.len() <= 1 {
3131 return entries
3132 .into_iter()
3133 .map(|(path, entry)| {
3134 let shard =
3135 shards
3136 .get(entry.shard_index)
3137 .ok_or(FormatError::InvalidArchive(
3138 "winning FileEntry shard is out of bounds",
3139 ))?;
3140 let diagnostics =
3141 self.stream_loaded_file_to_path(shard, entry.file_index, root, options)?;
3142 Ok((path, diagnostics))
3143 })
3144 .collect();
3145 }
3146
3147 let worker_count = jobs.min(entries.len());
3148 let chunk_size = entries.len().div_ceil(worker_count);
3149 std::thread::scope(|scope| {
3150 let handles = entries
3151 .chunks(chunk_size)
3152 .map(|chunk| {
3153 scope.spawn(move || {
3154 let mut out = Vec::with_capacity(chunk.len());
3155 for (path, entry) in chunk {
3156 let shard = shards.get(entry.shard_index).ok_or(
3157 FormatError::InvalidArchive(
3158 "winning FileEntry shard is out of bounds",
3159 ),
3160 )?;
3161 let diagnostics = self.stream_loaded_file_to_path(
3162 shard,
3163 entry.file_index,
3164 root,
3165 options,
3166 )?;
3167 out.push((path.clone(), diagnostics));
3168 }
3169 Ok(out)
3170 })
3171 })
3172 .collect::<Vec<_>>();
3173 let mut out = Vec::new();
3174 for handle in handles {
3175 let mut chunk = handle
3176 .join()
3177 .map_err(|_| FormatError::ReaderUnsupported("extract worker panicked"))??;
3178 out.append(&mut chunk);
3179 }
3180 Ok(out)
3181 })
3182 }
3183
3184 fn decode_loaded_owned_tar_member(
3185 &self,
3186 shard: &IndexShard,
3187 file_index: usize,
3188 enforce_extraction_cap: bool,
3189 ) -> Result<OwnedTarMember, FormatError> {
3190 let file = shard
3191 .files
3192 .get(file_index)
3193 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3194 if enforce_extraction_cap {
3195 self.validate_total_extraction_size(file.file_data_size)?;
3196 }
3197 let expected_path = shard
3198 .file_path(file_index)
3199 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3200 let frames = frame_range_for_file(shard, file)?;
3201 let mut envelope_cache = HashMap::<u64, Vec<u8>>::new();
3202 let mut decoded = Vec::new();
3203
3204 for frame in frames {
3205 let envelope = shard
3206 .envelopes
3207 .iter()
3208 .find(|entry| entry.envelope_index == frame.envelope_index)
3209 .ok_or(FormatError::InvalidArchive(
3210 "FrameEntry references missing EnvelopeEntry",
3211 ))?;
3212 if let Entry::Vacant(entry) = envelope_cache.entry(envelope.envelope_index) {
3213 entry.insert(self.load_payload_envelope(envelope, ParityReadPolicy::RepairOnly)?);
3214 }
3215 let envelope_plaintext = envelope_cache
3216 .get(&envelope.envelope_index)
3217 .expect("inserted above");
3218 let compressed = slice(
3219 envelope_plaintext,
3220 frame.offset_in_envelope as usize,
3221 frame.compressed_size as usize,
3222 "FrameEntry",
3223 )?;
3224 decoded.extend_from_slice(
3225 &self.decompress_payload_frame(compressed, frame.decompressed_size)?,
3226 );
3227 }
3228
3229 let offset = file.offset_in_first_frame_plaintext as usize;
3230 let group_len = to_usize(file.tar_member_group_size, "FileEntry")?;
3231 let group = slice(&decoded, offset, group_len, "FileEntry")?;
3232 let member = parse_tar_member_group(group, self.crypto_header.max_path_length)?;
3233 if member.path != expected_path {
3234 return Err(FormatError::InvalidArchive(
3235 "tar member path does not match FileEntry path",
3236 ));
3237 }
3238 if member.logical_size != file.file_data_size {
3239 return Err(FormatError::InvalidArchive(
3240 "tar member size does not match FileEntry file_data_size",
3241 ));
3242 }
3243 Ok(member.to_owned_member())
3244 }
3245
3246 fn metadata_limits(&self) -> MetadataLimits {
3247 metadata_limits(&self.crypto_header)
3248 }
3249
3250 fn recompute_root_auth_material(
3251 &self,
3252 footer: &RootAuthFooterV1,
3253 ) -> Result<RootAuthMaterial, FormatError> {
3254 if footer.format_version != self.volume_header.format_version {
3255 return Err(FormatError::InvalidArchive(
3256 "RootAuthFooter format_version differs from authenticated VolumeHeader",
3257 ));
3258 }
3259 if footer.volume_format_rev != self.volume_header.volume_format_rev {
3260 return Err(FormatError::InvalidArchive(
3261 "RootAuthFooter volume_format_rev differs from authenticated VolumeHeader",
3262 ));
3263 }
3264 let format_version = self.volume_header.format_version;
3265 let volume_format_rev = self.volume_header.volume_format_rev;
3266 let footer_length = footer.footer_length()?;
3267 let root_auth_descriptor_digest = root_auth_descriptor_digest_for_revision(
3268 format_version,
3269 volume_format_rev,
3270 footer.authenticator_id,
3271 footer.signer_identity_type,
3272 &footer.signer_identity_bytes,
3273 u32::try_from(footer.authenticator_value.len()).map_err(|_| {
3274 FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
3275 })?,
3276 footer_length,
3277 )?;
3278 let signer_identity_digest =
3279 signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
3280 let manifest_pre_hmac = manifest_footer_global_pre_hmac_bytes(&self.manifest_footer);
3281 let crypto_pre_hmac_len = self
3282 .crypto_header_bytes
3283 .len()
3284 .checked_sub(CRYPTO_HEADER_HMAC_LEN)
3285 .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
3286 let critical_metadata_digest = critical_metadata_digest(CriticalMetadataDigestInputs {
3287 archive_uuid: self.volume_header.archive_uuid,
3288 session_id: self.volume_header.session_id,
3289 format_version,
3290 volume_format_rev,
3291 stripe_width: self.crypto_header.stripe_width,
3292 total_volumes: self.manifest_footer.total_volumes,
3293 compression_algo: self.crypto_header.compression_algo,
3294 aead_algo: self.crypto_header.aead_algo,
3295 fec_algo: self.crypto_header.fec_algo,
3296 kdf_algo: self.crypto_header.kdf_algo,
3297 crypto_header_pre_hmac_bytes: &self.crypto_header_bytes[..crypto_pre_hmac_len],
3298 chunk_size: self.crypto_header.chunk_size,
3299 envelope_target_size: self.crypto_header.envelope_target_size,
3300 block_size: self.crypto_header.block_size,
3301 fec_data_shards: self.crypto_header.fec_data_shards,
3302 fec_parity_shards: self.crypto_header.fec_parity_shards,
3303 index_fec_data_shards: self.crypto_header.index_fec_data_shards,
3304 index_fec_parity_shards: self.crypto_header.index_fec_parity_shards,
3305 index_root_fec_data_shards: self.crypto_header.index_root_fec_data_shards,
3306 index_root_fec_parity_shards: self.crypto_header.index_root_fec_parity_shards,
3307 volume_loss_tolerance: self.crypto_header.volume_loss_tolerance,
3308 bit_rot_buffer_pct: self.crypto_header.bit_rot_buffer_pct,
3309 has_dictionary: self.crypto_header.has_dictionary,
3310 manifest_footer_global_pre_hmac_bytes: &manifest_pre_hmac,
3311 index_root_first_block: self.manifest_footer.index_root_first_block,
3312 index_root_data_block_count: self.manifest_footer.index_root_data_block_count,
3313 index_root_parity_block_count: self.manifest_footer.index_root_parity_block_count,
3314 index_root_encrypted_size: self.manifest_footer.index_root_encrypted_size,
3315 index_root_decompressed_size: self.manifest_footer.index_root_decompressed_size,
3316 root_auth_descriptor_digest,
3317 })?;
3318 let index_root_plaintext = self.index_root.to_bytes();
3319 let index_digest =
3320 index_digest_for_revision(format_version, volume_format_rev, &index_root_plaintext)?;
3321 let shards = self.load_all_index_shards()?;
3322 let fec_layout_rows = self.root_auth_fec_layout_rows(&shards)?;
3323 let fec_layout_digest =
3324 fec_layout_digest_for_revision(format_version, volume_format_rev, &fec_layout_rows)?;
3325 let data_leaves = self.root_auth_data_block_leaves(&fec_layout_rows)?;
3326 let total_data_block_count = u64::try_from(data_leaves.len())
3327 .map_err(|_| FormatError::InvalidArchive("root-auth data block count overflow"))?;
3328 let data_block_merkle_root =
3329 data_block_merkle_root_for_revision(format_version, volume_format_rev, &data_leaves)?;
3330 let archive_root = archive_root_for_revision(ArchiveRootInputs {
3331 archive_uuid: self.volume_header.archive_uuid,
3332 session_id: self.volume_header.session_id,
3333 format_version,
3334 volume_format_rev,
3335 compression_algo: self.crypto_header.compression_algo,
3336 aead_algo: self.crypto_header.aead_algo,
3337 fec_algo: self.crypto_header.fec_algo,
3338 kdf_algo: self.crypto_header.kdf_algo,
3339 critical_metadata_digest,
3340 index_digest,
3341 fec_layout_digest,
3342 total_data_block_count,
3343 data_block_merkle_root,
3344 root_auth_descriptor_digest,
3345 signer_identity_digest,
3346 })?;
3347 Ok(RootAuthMaterial {
3348 critical_metadata_digest,
3349 index_digest,
3350 fec_layout_digest,
3351 data_block_merkle_root,
3352 signer_identity_digest,
3353 archive_root,
3354 total_data_block_count,
3355 })
3356 }
3357
3358 fn root_auth_fec_layout_rows(
3359 &self,
3360 shards: &[IndexShard],
3361 ) -> Result<Vec<FecLayoutObjectRow>, FormatError> {
3362 let mut rows = Vec::new();
3363 rows.push(FecLayoutObjectRow {
3364 object_class: 1,
3365 present: true,
3366 object_id: 0,
3367 first_block_index: self.manifest_footer.index_root_first_block,
3368 data_block_count: self.manifest_footer.index_root_data_block_count,
3369 parity_block_count: self.manifest_footer.index_root_parity_block_count,
3370 encrypted_size: self.manifest_footer.index_root_encrypted_size,
3371 plain_size: self.manifest_footer.index_root_decompressed_size,
3372 });
3373 if self.crypto_header.has_dictionary != 0 {
3374 rows.push(FecLayoutObjectRow {
3375 object_class: 2,
3376 present: true,
3377 object_id: 0,
3378 first_block_index: self.index_root.header.dictionary_first_block,
3379 data_block_count: self.index_root.header.dictionary_data_block_count,
3380 parity_block_count: self.index_root.header.dictionary_parity_block_count,
3381 encrypted_size: self.index_root.header.dictionary_encrypted_size,
3382 plain_size: self.index_root.header.dictionary_decompressed_size,
3383 });
3384 } else {
3385 rows.push(FecLayoutObjectRow {
3386 object_class: 2,
3387 present: false,
3388 object_id: 0,
3389 first_block_index: 0,
3390 data_block_count: 0,
3391 parity_block_count: 0,
3392 encrypted_size: 0,
3393 plain_size: 0,
3394 });
3395 }
3396 for entry in &self.index_root.shards {
3397 rows.push(FecLayoutObjectRow {
3398 object_class: 3,
3399 present: true,
3400 object_id: entry.shard_index,
3401 first_block_index: entry.first_block_index,
3402 data_block_count: entry.data_block_count,
3403 parity_block_count: entry.parity_block_count,
3404 encrypted_size: entry.encrypted_size,
3405 plain_size: entry.decompressed_size,
3406 });
3407 }
3408 let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
3409 for shard in shards {
3410 for envelope in &shard.envelopes {
3411 if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
3412 {
3413 if existing != *envelope {
3414 return Err(FormatError::InvalidArchive(
3415 "duplicate EnvelopeEntry rows do not match",
3416 ));
3417 }
3418 }
3419 }
3420 }
3421 for envelope in envelopes.values() {
3422 rows.push(FecLayoutObjectRow {
3423 object_class: 4,
3424 present: true,
3425 object_id: envelope.envelope_index,
3426 first_block_index: envelope.first_block_index,
3427 data_block_count: envelope.data_block_count,
3428 parity_block_count: envelope.parity_block_count,
3429 encrypted_size: envelope.encrypted_size,
3430 plain_size: envelope.plaintext_size,
3431 });
3432 }
3433 for entry in &self.index_root.directory_hint_shards {
3434 rows.push(FecLayoutObjectRow {
3435 object_class: 5,
3436 present: true,
3437 object_id: entry.hint_shard_index,
3438 first_block_index: entry.first_block_index,
3439 data_block_count: entry.data_block_count,
3440 parity_block_count: entry.parity_block_count,
3441 encrypted_size: entry.encrypted_size,
3442 plain_size: entry.decompressed_size,
3443 });
3444 }
3445 Ok(rows)
3446 }
3447
3448 fn fec_object_class_shape(
3449 &self,
3450 object_class: u8,
3451 ) -> Result<(BlockKind, BlockKind, u16, u16), FormatError> {
3452 match object_class {
3453 1 => Ok((
3454 BlockKind::IndexRootData,
3455 BlockKind::IndexRootParity,
3456 self.crypto_header.index_root_fec_data_shards,
3457 self.crypto_header.index_root_fec_parity_shards,
3458 )),
3459 2 => Ok((
3460 BlockKind::DictionaryData,
3461 BlockKind::DictionaryParity,
3462 self.crypto_header.index_root_fec_data_shards,
3463 self.crypto_header.index_root_fec_parity_shards,
3464 )),
3465 3 => Ok((
3466 BlockKind::IndexShardData,
3467 BlockKind::IndexShardParity,
3468 self.crypto_header.index_fec_data_shards,
3469 self.crypto_header.index_fec_parity_shards,
3470 )),
3471 4 => Ok((
3472 BlockKind::PayloadData,
3473 BlockKind::PayloadParity,
3474 self.crypto_header.fec_data_shards,
3475 self.crypto_header.fec_parity_shards,
3476 )),
3477 5 => Ok((
3478 BlockKind::DirectoryHintData,
3479 BlockKind::DirectoryHintParity,
3480 self.crypto_header.index_fec_data_shards,
3481 self.crypto_header.index_fec_parity_shards,
3482 )),
3483 _ => Err(FormatError::InvalidArchive(
3484 "unknown root-auth FEC row class",
3485 )),
3486 }
3487 }
3488
3489 fn root_auth_data_block_leaves(
3490 &self,
3491 rows: &[FecLayoutObjectRow],
3492 ) -> Result<Vec<DataBlockMerkleLeaf>, FormatError> {
3493 let block_provider = self.block_provider();
3494 let present_rows = rows.iter().filter(|row| row.present).collect::<Vec<_>>();
3495 let chunks = parallel_map_ref(&present_rows, self.options.jobs, |row| {
3496 let row = **row;
3497 let (data_kind, parity_kind, data_max, parity_max) =
3498 self.fec_object_class_shape(row.object_class)?;
3499 let extent = ObjectExtent {
3500 first_block_index: row.first_block_index,
3501 data_block_count: row.data_block_count,
3502 parity_block_count: row.parity_block_count,
3503 encrypted_size: row.encrypted_size,
3504 };
3505 let repaired = load_repaired_object_data_shards_from_parts(
3506 &block_provider,
3507 &self.crypto_header,
3508 extent,
3509 data_kind,
3510 parity_kind,
3511 data_max,
3512 parity_max,
3513 )?;
3514 let mut leaves = Vec::new();
3515 for (offset, payload) in repaired.into_iter().enumerate() {
3516 leaves.push(DataBlockMerkleLeaf {
3517 block_index: checked_u64_add(
3518 row.first_block_index,
3519 offset as u64,
3520 "root-auth data block",
3521 )?,
3522 kind: data_kind,
3523 flags: if offset + 1 == row.data_block_count as usize {
3524 0x01
3525 } else {
3526 0
3527 },
3528 payload,
3529 });
3530 }
3531 Ok(leaves)
3532 })?;
3533 let mut leaves = Vec::new();
3534 for mut chunk in chunks {
3535 leaves.append(&mut chunk);
3536 }
3537 leaves.sort_by_key(|leaf| leaf.block_index);
3538 Ok(leaves)
3539 }
3540
3541 fn validate_total_extraction_size(&self, logical_size: u64) -> Result<(), FormatError> {
3542 let cap = total_extraction_size_cap(self.options, self.observed_archive_bytes);
3543 if logical_size > cap {
3544 return Err(FormatError::ReaderUnsupported(
3545 "total extraction size exceeds configured cap",
3546 ));
3547 }
3548 Ok(())
3549 }
3550
3551 fn decompress_payload_frame(
3552 &self,
3553 compressed: &[u8],
3554 decompressed_size: u32,
3555 ) -> Result<Vec<u8>, FormatError> {
3556 let mut decompressor = self.new_payload_decompressor()?;
3557 self.decompress_payload_frame_with(&mut decompressor, compressed, decompressed_size)
3558 }
3559
3560 fn new_payload_decompressor(&self) -> Result<zstd::bulk::Decompressor<'static>, FormatError> {
3561 match &self.payload_dictionary {
3562 Some(dictionary) => zstd::bulk::Decompressor::with_dictionary(dictionary),
3563 None => zstd::bulk::Decompressor::new(),
3564 }
3565 .map_err(|_| FormatError::ZstdDecompressionFailure)
3566 }
3567
3568 fn decompress_payload_frame_with(
3569 &self,
3570 decompressor: &mut zstd::bulk::Decompressor<'static>,
3571 compressed: &[u8],
3572 decompressed_size: u32,
3573 ) -> Result<Vec<u8>, FormatError> {
3574 validate_exact_zstd_frame(compressed)?;
3575 let expected = decompressed_size as usize;
3576 let decoded = decompressor
3577 .decompress(compressed, expected)
3578 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
3579 if decoded.len() != expected {
3580 return Err(FormatError::ZstdDecompressedSizeMismatch {
3581 expected,
3582 actual: decoded.len(),
3583 });
3584 }
3585 Ok(decoded)
3586 }
3587
3588 fn validate_encrypted_object_block_ranges(
3589 &self,
3590 envelopes: &BTreeMap<u64, EnvelopeEntry>,
3591 ) -> Result<(), FormatError> {
3592 let mut ranges = Vec::new();
3593 ranges.push(object_block_range(
3594 self.manifest_footer.index_root_first_block,
3595 self.manifest_footer.index_root_data_block_count,
3596 self.manifest_footer.index_root_parity_block_count,
3597 "IndexRoot",
3598 )?);
3599 for shard in &self.index_root.shards {
3600 ranges.push(object_block_range(
3601 shard.first_block_index,
3602 shard.data_block_count,
3603 shard.parity_block_count,
3604 "IndexShard",
3605 )?);
3606 }
3607 for hint in &self.index_root.directory_hint_shards {
3608 ranges.push(object_block_range(
3609 hint.first_block_index,
3610 hint.data_block_count,
3611 hint.parity_block_count,
3612 "DirectoryHintShardEntry",
3613 )?);
3614 }
3615 if self.crypto_header.has_dictionary != 0 {
3616 ranges.push(object_block_range(
3617 self.index_root.header.dictionary_first_block,
3618 self.index_root.header.dictionary_data_block_count,
3619 self.index_root.header.dictionary_parity_block_count,
3620 "dictionary",
3621 )?);
3622 }
3623 for envelope in envelopes.values() {
3624 ranges.push(object_block_range(
3625 envelope.first_block_index,
3626 envelope.data_block_count,
3627 envelope.parity_block_count,
3628 "EnvelopeEntry",
3629 )?);
3630 }
3631 validate_non_overlapping_object_ranges(&mut ranges)?;
3632 if let Some(source) = &self.lazy_blocks {
3633 if source.is_complete_volume_set() {
3634 validate_exact_coverage_ranges_u64(
3635 &mut ranges,
3636 source.total_block_count()?,
3637 "encrypted object block ranges do not cover complete archive exactly",
3638 )?;
3639 }
3640 }
3641 Ok(())
3642 }
3643}
3644
3645impl<'a> DecodedTarMemberGroupReader<'a> {
3646 fn new(
3647 archive: &'a OpenedArchive,
3648 shard: &'a IndexShard,
3649 file: &'a FileEntry,
3650 ) -> Result<Self, FormatError> {
3651 Ok(Self {
3652 archive,
3653 shard,
3654 file,
3655 decompressor: archive.new_payload_decompressor()?,
3656 next_frame_offset: 0,
3657 cached_envelope_index: None,
3658 cached_envelope_plaintext: Vec::new(),
3659 current_frame: Vec::new(),
3660 current_frame_offset: 0,
3661 remaining_group_bytes: file.tar_member_group_size,
3662 })
3663 }
3664
3665 fn ensure_frame_available(&mut self) -> Result<(), ExtractError> {
3666 while self.current_frame_offset >= self.current_frame.len() {
3667 if self.next_frame_offset >= self.file.frame_count as u64 {
3668 return Err(
3669 FormatError::InvalidArchive("tar member group exceeds frame range").into(),
3670 );
3671 }
3672 let frame_index = self
3673 .file
3674 .first_frame_index
3675 .checked_add(self.next_frame_offset)
3676 .ok_or(FormatError::InvalidArchive(
3677 "FileEntry frame range overflow",
3678 ))?;
3679 let frame = frame_by_index(self.shard, frame_index)?;
3680 let envelope = envelope_by_index(self.shard, frame.envelope_index)?;
3681 if self.cached_envelope_index != Some(envelope.envelope_index) {
3682 self.cached_envelope_plaintext = self
3683 .archive
3684 .load_payload_envelope(envelope, ParityReadPolicy::RepairOnly)?;
3685 self.cached_envelope_index = Some(envelope.envelope_index);
3686 }
3687 let compressed = slice(
3688 &self.cached_envelope_plaintext,
3689 frame.offset_in_envelope as usize,
3690 frame.compressed_size as usize,
3691 "FrameEntry",
3692 )?;
3693 let decoded = self.archive.decompress_payload_frame_with(
3694 &mut self.decompressor,
3695 compressed,
3696 frame.decompressed_size,
3697 )?;
3698 let offset = if self.next_frame_offset == 0 {
3699 self.file.offset_in_first_frame_plaintext as usize
3700 } else {
3701 0
3702 };
3703 if offset > decoded.len() {
3704 return Err(FormatError::InvalidArchive(
3705 "offset in first frame is outside the first referenced frame",
3706 )
3707 .into());
3708 }
3709 self.next_frame_offset += 1;
3710 self.current_frame = decoded;
3711 self.current_frame_offset = offset;
3712 }
3713 Ok(())
3714 }
3715}
3716
3717impl TarMemberGroupReader for DecodedTarMemberGroupReader<'_> {
3718 fn read_some_member_bytes(&mut self, buf: &mut [u8]) -> Result<usize, ExtractError> {
3719 if buf.is_empty() {
3720 return Ok(0);
3721 }
3722 if self.remaining_group_bytes == 0 {
3723 return Ok(0);
3724 }
3725 self.ensure_frame_available()?;
3726 let available = self.current_frame.len() - self.current_frame_offset;
3727 let len = available
3728 .min(buf.len())
3729 .min(to_usize(self.remaining_group_bytes, "FileEntry")?);
3730 if len == 0 {
3731 return Err(FormatError::InvalidArchive("tar member group exceeds frame range").into());
3732 }
3733 buf[..len].copy_from_slice(
3734 &self.current_frame[self.current_frame_offset..self.current_frame_offset + len],
3735 );
3736 self.current_frame_offset += len;
3737 self.remaining_group_bytes -= len as u64;
3738 Ok(len)
3739 }
3740}
3741
3742fn frame_by_index(shard: &IndexShard, frame_index: u64) -> Result<&FrameEntry, FormatError> {
3743 shard
3744 .frames
3745 .binary_search_by_key(&frame_index, |entry| entry.frame_index)
3746 .map(|idx| &shard.frames[idx])
3747 .map_err(|_| FormatError::InvalidArchive("FileEntry references missing FrameEntry"))
3748}
3749
3750fn envelope_by_index(
3751 shard: &IndexShard,
3752 envelope_index: u64,
3753) -> Result<&EnvelopeEntry, FormatError> {
3754 shard
3755 .envelopes
3756 .binary_search_by_key(&envelope_index, |entry| entry.envelope_index)
3757 .map(|idx| &shard.envelopes[idx])
3758 .map_err(|_| FormatError::InvalidArchive("FrameEntry references missing EnvelopeEntry"))
3759}
3760
3761fn format_error_from_extract_error(err: ExtractError) -> FormatError {
3762 match err {
3763 ExtractError::Format(err) => err,
3764 ExtractError::Output(_) => {
3765 FormatError::FilesystemExtractionFailed("failed to write regular file")
3766 }
3767 }
3768}
3769
3770fn final_index_entry_winners(
3771 shards: &[IndexShard],
3772) -> Result<BTreeMap<String, WinningIndexEntry>, FormatError> {
3773 let mut final_entries = BTreeMap::<String, WinningIndexEntry>::new();
3774 for (shard_index, shard) in shards.iter().enumerate() {
3775 for (idx, file) in shard.files.iter().enumerate() {
3776 let path = utf8_path(
3777 shard
3778 .file_path(idx)
3779 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
3780 )?;
3781 let start = shard
3782 .tar_member_group_start(idx)
3783 .ok_or(FormatError::InvalidArchive(
3784 "FileEntry tar member start is missing",
3785 ))?;
3786 if let Some(winner) = final_entries.get_mut(&path) {
3787 if start >= winner.start {
3788 winner.start = start;
3789 winner.file_data_size = file.file_data_size;
3790 winner.shard_index = shard_index;
3791 winner.file_index = idx;
3792 }
3793 } else {
3794 final_entries.insert(
3795 path,
3796 WinningIndexEntry {
3797 start,
3798 file_data_size: file.file_data_size,
3799 shard_index,
3800 file_index: idx,
3801 },
3802 );
3803 }
3804 }
3805 }
3806 Ok(final_entries)
3807}
3808
3809fn archive_index_entry_from_loaded_file(
3810 shard: &IndexShard,
3811 file_index: usize,
3812) -> Result<ArchiveIndexEntry, FormatError> {
3813 let file = shard
3814 .files
3815 .get(file_index)
3816 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3817 let path = utf8_path(
3818 shard
3819 .file_path(file_index)
3820 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
3821 )?;
3822 Ok(ArchiveIndexEntry {
3823 path,
3824 file_data_size: file.file_data_size,
3825 })
3826}
3827
3828#[derive(Debug)]
3829struct ParsedSeekableVolume {
3830 volume_header: VolumeHeader,
3831 crypto_header: CryptoHeaderFixed,
3832 crypto_header_bytes: Vec<u8>,
3833 key_wrap_table_bytes: Option<Vec<u8>>,
3834 subkeys: Subkeys,
3835 manifest_footer: Option<ManifestFooter>,
3836 manifest_footer_error: Option<FormatError>,
3837 root_auth_footer: Option<RootAuthFooterV1>,
3838 root_auth_footer_bytes: Option<Vec<u8>>,
3839 volume_trailer: VolumeTrailer,
3840 blocks: BTreeMap<u64, BlockRecord>,
3841 erased_block_indices: BTreeSet<u64>,
3842}
3843
3844struct ParsedSeekableReadAtVolume {
3845 reader: Arc<dyn ArchiveReadAt>,
3846 volume_header: VolumeHeader,
3847 crypto_header: CryptoHeaderFixed,
3848 crypto_header_bytes: Vec<u8>,
3849 key_wrap_table_bytes: Option<Vec<u8>>,
3850 subkeys: Subkeys,
3851 manifest_footer: Option<ManifestFooter>,
3852 manifest_footer_error: Option<FormatError>,
3853 root_auth_footer: Option<RootAuthFooterV1>,
3854 root_auth_footer_bytes: Option<Vec<u8>>,
3855 volume_trailer: VolumeTrailer,
3856 block_records_start: u64,
3857}
3858
3859struct ParsedOpenPrefix {
3860 volume_header: VolumeHeader,
3861 crypto_header: CryptoHeaderFixed,
3862 crypto_header_bytes: Vec<u8>,
3863 key_wrap_table_bytes: Option<Vec<u8>>,
3864 block_records_start: u64,
3865 subkeys: Subkeys,
3866}
3867
3868struct ParsedReadAtOpenPrefix {
3869 volume_header: VolumeHeader,
3870 crypto_header: CryptoHeaderFixed,
3871 crypto_header_bytes: Vec<u8>,
3872 key_wrap_table_bytes: Option<Vec<u8>>,
3873 block_records_start: u64,
3874 subkeys: Subkeys,
3875}
3876
3877pub(crate) struct StartupKeyWrapTable {
3878 pub(crate) table: KeyWrapTableV1,
3879 pub(crate) bytes: Vec<u8>,
3880 pub(crate) block_records_start: u64,
3881}
3882
3883pub(crate) fn startup_block_records_start(
3884 volume_header: &VolumeHeader,
3885 kdf_params: &KdfParams,
3886 read_key_wrap_table: impl FnMut(u64, usize) -> Result<Vec<u8>, FormatError>,
3887) -> Result<u64, FormatError> {
3888 Ok(
3889 startup_key_wrap_table(volume_header, kdf_params, read_key_wrap_table)?
3890 .map(|startup| startup.block_records_start)
3891 .unwrap_or_else(|| {
3892 volume_header.crypto_header_offset as u64
3893 + volume_header.crypto_header_length as u64
3894 }),
3895 )
3896}
3897
3898pub(crate) fn startup_key_wrap_table(
3899 volume_header: &VolumeHeader,
3900 kdf_params: &KdfParams,
3901 mut read_key_wrap_table: impl FnMut(u64, usize) -> Result<Vec<u8>, FormatError>,
3902) -> Result<Option<StartupKeyWrapTable>, FormatError> {
3903 let crypto_end = checked_u64_add(
3904 volume_header.crypto_header_offset as u64,
3905 volume_header.crypto_header_length as u64,
3906 "CryptoHeader",
3907 )?;
3908 if volume_header.volume_format_rev == VOLUME_FORMAT_REV_44 {
3911 let &KdfParams::RecipientWrap {
3912 key_wrap_table_length,
3913 ..
3914 } = kdf_params
3915 else {
3916 return Ok(None);
3917 };
3918 let key_wrap_table_length_usize =
3919 to_usize(u64::from(key_wrap_table_length), "KeyWrapTableV1 length")?;
3920 let key_wrap_table_bytes = read_key_wrap_table(crypto_end, key_wrap_table_length_usize)?;
3921 Ok(Some(parse_startup_key_wrap_table_bytes(
3922 volume_header,
3923 kdf_params,
3924 key_wrap_table_bytes,
3925 )?))
3926 } else if matches!(kdf_params, KdfParams::RecipientWrap { .. }) {
3927 Err(FormatError::InvalidArchive(
3928 "RecipientWrap KdfParams require volume_format_rev 44",
3929 ))
3930 } else {
3931 Ok(None)
3932 }
3933}
3934
3935pub(crate) fn parse_startup_key_wrap_table_bytes(
3936 volume_header: &VolumeHeader,
3937 kdf_params: &KdfParams,
3938 key_wrap_table_bytes: Vec<u8>,
3939) -> Result<StartupKeyWrapTable, FormatError> {
3940 let crypto_end = checked_u64_add(
3941 volume_header.crypto_header_offset as u64,
3942 volume_header.crypto_header_length as u64,
3943 "CryptoHeader",
3944 )?;
3945 let KdfParams::RecipientWrap {
3946 key_wrap_table_length,
3947 key_wrap_table_record_count,
3948 key_wrap_table_digest,
3949 ..
3950 } = kdf_params
3951 else {
3952 return Err(FormatError::KeyMaterialMismatch);
3953 };
3954 let key_wrap_table = KeyWrapTableV1::parse(
3955 &key_wrap_table_bytes,
3956 &volume_header.archive_uuid,
3957 &volume_header.session_id,
3958 *key_wrap_table_length,
3959 *key_wrap_table_record_count,
3960 )?;
3961 if compute_key_wrap_table_digest(*key_wrap_table_length, &key_wrap_table_bytes)
3962 != *key_wrap_table_digest
3963 {
3964 return Err(FormatError::IntegrityDigestMismatch {
3965 structure: "KeyWrapTableV1",
3966 });
3967 }
3968 let block_records_start = checked_u64_add(
3969 crypto_end,
3970 key_wrap_table.table_length as u64,
3971 "KeyWrapTableV1",
3972 )?;
3973 Ok(StartupKeyWrapTable {
3974 table: key_wrap_table,
3975 bytes: key_wrap_table_bytes,
3976 block_records_start,
3977 })
3978}
3979
3980fn parse_seekable_volume(
3981 bytes: &[u8],
3982 master_key: &MasterKey,
3983 options: ReaderOptions,
3984) -> Result<ParsedSeekableVolume, FormatError> {
3985 if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
3986 return Err(FormatError::InvalidLength {
3987 structure: "archive",
3988 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
3989 actual: bytes.len(),
3990 });
3991 }
3992
3993 let prefix = match parse_open_prefix(bytes, master_key) {
3994 Ok(prefix) => prefix,
3995 Err(prefix_err) => {
3996 if matches!(
3997 prefix_err,
3998 FormatError::UnsupportedVolumeFormatRevision { .. }
3999 ) {
4000 return Err(prefix_err);
4001 }
4002 if matches!(prefix_err, FormatError::KeyMaterialMismatch)
4003 && prefix_uses_recipient_wrap(bytes)
4004 {
4005 return Err(prefix_err);
4006 }
4007 return parse_seekable_volume_from_recovered_terminal(bytes, master_key, options)
4008 .or(Err(prefix_err));
4009 }
4010 };
4011 let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4012 match parse_seekable_volume_with_prefix(bytes, prefix, options) {
4013 Ok(parsed) => Ok(parsed),
4014 Err(prefix_err) => {
4015 match parse_seekable_volume_from_recovered_terminal(bytes, master_key, options) {
4016 Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4017 Ok(recovered)
4018 }
4019 Ok(_) | Err(_) => Err(prefix_err),
4020 }
4021 }
4022 }
4023}
4024
4025fn parse_seekable_volume_with_recipient_wrap_resolver<F>(
4026 bytes: &[u8],
4027 resolver: &mut F,
4028 options: ReaderOptions,
4029) -> Result<ParsedSeekableVolume, FormatError>
4030where
4031 F: FnMut(
4032 RecipientWrapRecordContext<'_>,
4033 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4034{
4035 let prefix = match parse_open_prefix_with_recipient_wrap_resolver(bytes, resolver) {
4036 Ok(prefix) => prefix,
4037 Err(prefix_err) => {
4038 if recipient_wrap_prefix_error_precludes_recovery(&prefix_err) {
4039 return Err(prefix_err);
4040 }
4041 return parse_seekable_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4042 bytes, resolver, options,
4043 )
4044 .or(Err(prefix_err));
4045 }
4046 };
4047 let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4048 match parse_seekable_volume_with_prefix(bytes, prefix, options) {
4049 Ok(parsed) => Ok(parsed),
4050 Err(prefix_err) => {
4051 match parse_seekable_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4052 bytes, resolver, options,
4053 ) {
4054 Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4055 Ok(recovered)
4056 }
4057 Ok(_) | Err(_) => Err(prefix_err),
4058 }
4059 }
4060 }
4061}
4062
4063fn parse_seekable_volume_with_prefix(
4064 bytes: &[u8],
4065 prefix: ParsedOpenPrefix,
4066 options: ReaderOptions,
4067) -> Result<ParsedSeekableVolume, FormatError> {
4068 let ParsedOpenPrefix {
4069 volume_header,
4070 crypto_header,
4071 crypto_header_bytes,
4072 key_wrap_table_bytes,
4073 block_records_start,
4074 subkeys,
4075 } = prefix;
4076 let crypto_bytes = crypto_header_bytes.as_slice();
4077
4078 let terminal = locate_v41_terminal(
4079 bytes,
4080 KeyHoldingTerminalContext {
4081 subkeys: &subkeys,
4082 volume_header: &volume_header,
4083 crypto_header: &crypto_header,
4084 crypto_header_bytes: crypto_bytes,
4085 },
4086 options,
4087 )?;
4088 finish_parse_seekable_volume(
4089 bytes,
4090 volume_header,
4091 crypto_header,
4092 crypto_header_bytes,
4093 key_wrap_table_bytes,
4094 block_records_start,
4095 subkeys,
4096 terminal,
4097 )
4098}
4099
4100fn parse_seekable_volume_from_recovered_terminal(
4101 bytes: &[u8],
4102 master_key: &MasterKey,
4103 options: ReaderOptions,
4104) -> Result<ParsedSeekableVolume, FormatError> {
4105 let authority = locate_v41_terminal_authority(bytes, master_key, options)?;
4106 parse_volume_format_dispatch(&authority.volume_header)?;
4107 let startup_key_wrap_table = startup_key_wrap_table(
4108 &authority.volume_header,
4109 &authority.kdf_params,
4110 |start, length| {
4111 let start = to_usize(start, "KeyWrapTableV1")?;
4112 Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
4113 },
4114 )?;
4115 let crypto_end = checked_u64_add(
4116 authority.volume_header.crypto_header_offset as u64,
4117 authority.volume_header.crypto_header_length as u64,
4118 "CryptoHeader",
4119 )?;
4120 let (key_wrap_table_bytes, block_records_start) = startup_key_wrap_table
4121 .map(|startup| (Some(startup.bytes), startup.block_records_start))
4122 .unwrap_or((None, crypto_end));
4123 finish_parse_seekable_volume(
4124 bytes,
4125 authority.volume_header,
4126 authority.crypto_header,
4127 authority.crypto_header_bytes,
4128 key_wrap_table_bytes,
4129 block_records_start,
4130 authority.subkeys,
4131 authority.terminal,
4132 )
4133}
4134
4135fn parse_seekable_volume_with_recipient_wrap_resolver_from_recovered_terminal<F>(
4136 bytes: &[u8],
4137 resolver: &mut F,
4138 options: ReaderOptions,
4139) -> Result<ParsedSeekableVolume, FormatError>
4140where
4141 F: FnMut(
4142 RecipientWrapRecordContext<'_>,
4143 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4144{
4145 let authority = locate_v41_recipient_wrap_terminal_authority(bytes, resolver, options)?;
4146 finish_parse_seekable_volume(
4147 bytes,
4148 authority.volume_header,
4149 authority.crypto_header,
4150 authority.crypto_header_bytes,
4151 Some(authority.key_wrap_table_bytes),
4152 authority.block_records_start,
4153 authority.subkeys,
4154 authority.terminal,
4155 )
4156}
4157
4158fn recipient_wrap_prefix_error_precludes_recovery(error: &FormatError) -> bool {
4159 matches!(
4160 error,
4161 FormatError::UnsupportedVolumeFormatRevision { .. }
4162 | FormatError::ReaderUnsupported(_)
4163 | FormatError::InvalidArchive(
4164 "VolumeHeader and CryptoHeader stripe_width differ"
4165 | "fec_parity_shards does not match v41 compute_parity"
4166 | "index_fec_parity_shards does not match v41 compute_parity"
4167 | "index_root_fec_parity_shards does not match v41 compute_parity"
4168 )
4169 )
4170}
4171
4172fn parse_open_prefix(
4173 bytes: &[u8],
4174 master_key: &MasterKey,
4175) -> Result<ParsedOpenPrefix, FormatError> {
4176 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
4177 parse_volume_format_dispatch(&volume_header)?;
4178 let crypto_start = volume_header.crypto_header_offset as usize;
4179 let crypto_len = volume_header.crypto_header_length as usize;
4180 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
4181 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
4182 if matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. }) {
4183 return Err(FormatError::KeyMaterialMismatch);
4184 }
4185 let subkeys = subkeys_for_open(
4186 Some(master_key),
4187 parsed_crypto.fixed.aead_algo,
4188 &volume_header.archive_uuid,
4189 &volume_header.session_id,
4190 )?;
4191 verify_integrity_tag(
4192 HmacDomain::CryptoHeader,
4193 parsed_crypto.fixed.aead_algo,
4194 volume_header.volume_format_rev,
4195 Some(&subkeys.mac_key),
4196 &volume_header.archive_uuid,
4197 &volume_header.session_id,
4198 parsed_crypto.hmac_covered_bytes,
4199 &parsed_crypto.header_hmac,
4200 )?;
4201 parsed_crypto.validate_extension_semantics()?;
4202 validate_seekable_supported_volume(
4203 &volume_header,
4204 &parsed_crypto.fixed,
4205 &parsed_crypto.extensions,
4206 )?;
4207 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4208 let block_records_start = startup_block_records_start(
4209 &volume_header,
4210 &parsed_crypto.kdf_params,
4211 |start, length| {
4212 let start = to_usize(start, "KeyWrapTableV1")?;
4213 Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
4214 },
4215 )?;
4216 let crypto_header = parsed_crypto.fixed.clone();
4217 Ok(ParsedOpenPrefix {
4218 volume_header,
4219 crypto_header,
4220 crypto_header_bytes: crypto_bytes.to_vec(),
4221 key_wrap_table_bytes: None,
4222 block_records_start,
4223 subkeys,
4224 })
4225}
4226
4227fn prefix_uses_recipient_wrap(bytes: &[u8]) -> bool {
4228 let Ok(volume_header_bytes) = slice(bytes, 0, VOLUME_HEADER_LEN, "archive") else {
4229 return false;
4230 };
4231 let Ok(volume_header) = VolumeHeader::parse(volume_header_bytes) else {
4232 return false;
4233 };
4234 let crypto_start = volume_header.crypto_header_offset as usize;
4235 let crypto_len = volume_header.crypto_header_length as usize;
4236 let Ok(crypto_bytes) = slice(bytes, crypto_start, crypto_len, "CryptoHeader") else {
4237 return false;
4238 };
4239 let Ok(parsed_crypto) = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)
4240 else {
4241 return false;
4242 };
4243 matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
4244}
4245
4246fn parse_open_prefix_with_recipient_wrap_resolver<F>(
4247 bytes: &[u8],
4248 resolver: &mut F,
4249) -> Result<ParsedOpenPrefix, FormatError>
4250where
4251 F: FnMut(
4252 RecipientWrapRecordContext<'_>,
4253 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4254{
4255 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
4256 parse_volume_format_dispatch(&volume_header)?;
4257 let crypto_start = volume_header.crypto_header_offset as usize;
4258 let crypto_len = volume_header.crypto_header_length as usize;
4259 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
4260 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
4261 if !matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
4262 || !parsed_crypto.fixed.aead_algo.is_encrypted()
4263 {
4264 return Err(FormatError::KeyMaterialMismatch);
4265 }
4266
4267 validate_seekable_supported_volume(&volume_header, &parsed_crypto.fixed, &[])?;
4268 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4269
4270 let startup_key_wrap_table = startup_key_wrap_table(
4271 &volume_header,
4272 &parsed_crypto.kdf_params,
4273 |start, length| {
4274 let start = to_usize(start, "KeyWrapTableV1")?;
4275 Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
4276 },
4277 )?
4278 .ok_or(FormatError::KeyMaterialMismatch)?;
4279 let key_wrap_table = startup_key_wrap_table.table;
4280 let key_wrap_table_bytes = Some(startup_key_wrap_table.bytes);
4281 let block_records_start = startup_key_wrap_table.block_records_start;
4282
4283 let subkeys = recipient_wrap_subkeys_from_table(
4284 &volume_header,
4285 &parsed_crypto,
4286 &key_wrap_table,
4287 resolver,
4288 )?;
4289 parsed_crypto.validate_extension_semantics()?;
4290 reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
4291
4292 let crypto_header = parsed_crypto.fixed.clone();
4293 Ok(ParsedOpenPrefix {
4294 volume_header,
4295 crypto_header,
4296 crypto_header_bytes: crypto_bytes.to_vec(),
4297 key_wrap_table_bytes,
4298 block_records_start,
4299 subkeys,
4300 })
4301}
4302
4303pub(crate) fn recipient_wrap_subkeys_from_table<F>(
4304 volume_header: &VolumeHeader,
4305 parsed_crypto: &CryptoHeader<'_>,
4306 key_wrap_table: &KeyWrapTableV1,
4307 resolver: &mut F,
4308) -> Result<Subkeys, FormatError>
4309where
4310 F: FnMut(
4311 RecipientWrapRecordContext<'_>,
4312 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>
4313 + ?Sized,
4314{
4315 let archive_identity = RecipientWrapArchiveIdentity {
4316 archive_uuid: volume_header.archive_uuid,
4317 session_id: volume_header.session_id,
4318 format_version: volume_header.format_version,
4319 volume_format_rev: volume_header.volume_format_rev,
4320 };
4321
4322 for record in &key_wrap_table.recipient_records {
4323 let candidates = resolver(RecipientWrapRecordContext {
4324 archive_identity,
4325 record,
4326 })?;
4327 for candidate in candidates {
4328 let master_key = MasterKey::from_raw_key(&candidate)?;
4329 let subkeys = subkeys_for_open(
4330 Some(&master_key),
4331 parsed_crypto.fixed.aead_algo,
4332 &volume_header.archive_uuid,
4333 &volume_header.session_id,
4334 )?;
4335 if verify_integrity_tag(
4336 HmacDomain::CryptoHeader,
4337 parsed_crypto.fixed.aead_algo,
4338 volume_header.volume_format_rev,
4339 Some(&subkeys.mac_key),
4340 &volume_header.archive_uuid,
4341 &volume_header.session_id,
4342 parsed_crypto.hmac_covered_bytes,
4343 &parsed_crypto.header_hmac,
4344 )
4345 .is_ok()
4346 {
4347 return Ok(subkeys);
4348 }
4349 }
4350 }
4351 Err(FormatError::KeyMaterialMismatch)
4352}
4353
4354#[allow(clippy::too_many_arguments)]
4355fn finish_parse_seekable_volume(
4356 bytes: &[u8],
4357 volume_header: VolumeHeader,
4358 crypto_header: CryptoHeaderFixed,
4359 crypto_header_bytes: Vec<u8>,
4360 key_wrap_table_bytes: Option<Vec<u8>>,
4361 block_records_start: u64,
4362 subkeys: Subkeys,
4363 terminal: V41Terminal,
4364) -> Result<ParsedSeekableVolume, FormatError> {
4365 let trailer_offset = to_usize(terminal.image.volume_trailer_offset, "VolumeTrailer")?;
4366 let volume_trailer = terminal.volume_trailer.clone();
4367 validate_trailer_identity(&volume_header, &volume_trailer)?;
4368
4369 let manifest_offset = to_usize(volume_trailer.manifest_footer_offset, "ManifestFooter")?;
4370 let manifest_end = checked_add(manifest_offset, MANIFEST_FOOTER_LEN, "ManifestFooter")?;
4371 if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
4372 if to_usize(volume_trailer.root_auth_footer_offset, "RootAuthFooter")? != manifest_end
4373 || volume_trailer
4374 .root_auth_footer_offset
4375 .checked_add(volume_trailer.root_auth_footer_length as u64)
4376 .ok_or(FormatError::InvalidArchive(
4377 "RootAuthFooter terminal boundary overflow",
4378 ))?
4379 != trailer_offset as u64
4380 {
4381 return Err(FormatError::InvalidArchive(
4382 "RootAuthFooter does not sit before selected trailer",
4383 ));
4384 }
4385 } else if manifest_end != trailer_offset {
4386 return Err(FormatError::InvalidArchive(
4387 "ManifestFooter does not end at selected trailer",
4388 ));
4389 }
4390 let manifest_bytes = &terminal.manifest_footer_bytes;
4391 let (manifest_footer, manifest_footer_error) =
4392 match parse_valid_manifest_footer(&volume_header, &crypto_header, &subkeys, manifest_bytes)
4393 {
4394 Ok(footer) => (Some(footer), None),
4395 Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
4396 Err(err) => return Err(err),
4397 };
4398
4399 let block_region = parse_block_region(
4400 bytes,
4401 to_usize(block_records_start, "BlockRecord")?,
4402 manifest_offset,
4403 crypto_header.block_size as usize,
4404 &volume_header,
4405 &volume_trailer,
4406 )?;
4407
4408 Ok(ParsedSeekableVolume {
4409 volume_header,
4410 crypto_header,
4411 crypto_header_bytes,
4412 key_wrap_table_bytes,
4413 subkeys,
4414 manifest_footer,
4415 manifest_footer_error,
4416 root_auth_footer: terminal.root_auth_footer,
4417 root_auth_footer_bytes: terminal.root_auth_footer_bytes,
4418 volume_trailer,
4419 blocks: block_region.blocks,
4420 erased_block_indices: block_region.erased_block_indices,
4421 })
4422}
4423
4424fn parse_seekable_read_at_volume(
4425 reader: Arc<dyn ArchiveReadAt>,
4426 master_key: &MasterKey,
4427 options: ReaderOptions,
4428) -> Result<ParsedSeekableReadAtVolume, FormatError> {
4429 let observed_len = reader.len()?;
4430 if observed_len < (VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN) as u64 {
4431 return Err(FormatError::InvalidLength {
4432 structure: "archive",
4433 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
4434 actual: to_usize(observed_len, "archive")?,
4435 });
4436 }
4437
4438 let prefix = match parse_read_at_open_prefix(reader.as_ref(), master_key) {
4439 Ok(prefix) => prefix,
4440 Err(prefix_err) => {
4441 if matches!(
4442 prefix_err,
4443 FormatError::UnsupportedVolumeFormatRevision { .. }
4444 ) {
4445 return Err(prefix_err);
4446 }
4447 return parse_seekable_read_at_volume_from_recovered_terminal(
4448 reader,
4449 observed_len,
4450 master_key,
4451 options,
4452 )
4453 .or(Err(prefix_err));
4454 }
4455 };
4456 let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4457 match parse_seekable_read_at_volume_with_prefix(reader.clone(), observed_len, prefix, options) {
4458 Ok(parsed) => Ok(parsed),
4459 Err(prefix_err) => match parse_seekable_read_at_volume_from_recovered_terminal(
4460 reader,
4461 observed_len,
4462 master_key,
4463 options,
4464 ) {
4465 Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4466 Ok(recovered)
4467 }
4468 Ok(_) | Err(_) => Err(prefix_err),
4469 },
4470 }
4471}
4472
4473fn parse_seekable_read_at_volume_with_recipient_wrap_resolver<F>(
4474 reader: Arc<dyn ArchiveReadAt>,
4475 resolver: &mut F,
4476 options: ReaderOptions,
4477) -> Result<ParsedSeekableReadAtVolume, FormatError>
4478where
4479 F: FnMut(
4480 RecipientWrapRecordContext<'_>,
4481 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4482{
4483 let observed_len = reader.len()?;
4484 if observed_len < (VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN) as u64 {
4485 return Err(FormatError::InvalidLength {
4486 structure: "archive",
4487 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
4488 actual: to_usize(observed_len, "archive")?,
4489 });
4490 }
4491
4492 let prefix = match parse_read_at_open_prefix_with_recipient_wrap_resolver(
4493 reader.as_ref(),
4494 resolver,
4495 ) {
4496 Ok(prefix) => prefix,
4497 Err(prefix_err) => {
4498 if recipient_wrap_prefix_error_precludes_recovery(&prefix_err) {
4499 return Err(prefix_err);
4500 }
4501 return parse_seekable_read_at_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4502 reader,
4503 observed_len,
4504 resolver,
4505 options,
4506 )
4507 .or(Err(prefix_err));
4508 }
4509 };
4510 let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4511 match parse_seekable_read_at_volume_with_prefix(reader.clone(), observed_len, prefix, options) {
4512 Ok(parsed) => Ok(parsed),
4513 Err(prefix_err) => {
4514 match parse_seekable_read_at_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4515 reader,
4516 observed_len,
4517 resolver,
4518 options,
4519 ) {
4520 Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4521 Ok(recovered)
4522 }
4523 Ok(_) | Err(_) => Err(prefix_err),
4524 }
4525 }
4526 }
4527}
4528
4529fn parse_seekable_read_at_volume_with_prefix(
4530 reader: Arc<dyn ArchiveReadAt>,
4531 observed_len: u64,
4532 prefix: ParsedReadAtOpenPrefix,
4533 options: ReaderOptions,
4534) -> Result<ParsedSeekableReadAtVolume, FormatError> {
4535 let ParsedReadAtOpenPrefix {
4536 volume_header,
4537 crypto_header,
4538 crypto_header_bytes,
4539 key_wrap_table_bytes,
4540 block_records_start,
4541 subkeys,
4542 } = prefix;
4543
4544 let terminal = locate_v41_terminal_read_at(
4545 reader.as_ref(),
4546 observed_len,
4547 KeyHoldingTerminalContext {
4548 subkeys: &subkeys,
4549 volume_header: &volume_header,
4550 crypto_header: &crypto_header,
4551 crypto_header_bytes: &crypto_header_bytes,
4552 },
4553 options,
4554 )?;
4555 finish_parse_seekable_read_at_volume(
4556 reader,
4557 volume_header,
4558 crypto_header,
4559 crypto_header_bytes,
4560 key_wrap_table_bytes,
4561 block_records_start,
4562 subkeys,
4563 terminal,
4564 )
4565}
4566
4567fn parse_seekable_read_at_volume_from_recovered_terminal(
4568 reader: Arc<dyn ArchiveReadAt>,
4569 observed_len: u64,
4570 master_key: &MasterKey,
4571 options: ReaderOptions,
4572) -> Result<ParsedSeekableReadAtVolume, FormatError> {
4573 let authority =
4574 locate_v41_terminal_authority_read_at(reader.as_ref(), observed_len, master_key, options)?;
4575 parse_volume_format_dispatch(&authority.volume_header)?;
4576 if matches!(authority.kdf_params, KdfParams::RecipientWrap { .. }) {
4577 return Err(FormatError::KeyMaterialMismatch);
4578 }
4579 let block_records_start = startup_block_records_start(
4580 &authority.volume_header,
4581 &authority.kdf_params,
4582 |start, length| read_at_vec(reader.as_ref(), start, length, "KeyWrapTableV1"),
4583 )?;
4584 finish_parse_seekable_read_at_volume(
4585 reader,
4586 authority.volume_header,
4587 authority.crypto_header,
4588 authority.crypto_header_bytes,
4589 None,
4590 block_records_start,
4591 authority.subkeys,
4592 authority.terminal,
4593 )
4594}
4595
4596fn parse_seekable_read_at_volume_with_recipient_wrap_resolver_from_recovered_terminal<F>(
4597 reader: Arc<dyn ArchiveReadAt>,
4598 observed_len: u64,
4599 resolver: &mut F,
4600 options: ReaderOptions,
4601) -> Result<ParsedSeekableReadAtVolume, FormatError>
4602where
4603 F: FnMut(
4604 RecipientWrapRecordContext<'_>,
4605 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4606{
4607 let authority = locate_v41_recipient_wrap_terminal_authority_read_at(
4608 reader.as_ref(),
4609 observed_len,
4610 resolver,
4611 options,
4612 )?;
4613 finish_parse_seekable_read_at_volume(
4614 reader,
4615 authority.volume_header,
4616 authority.crypto_header,
4617 authority.crypto_header_bytes,
4618 Some(authority.key_wrap_table_bytes),
4619 authority.block_records_start,
4620 authority.subkeys,
4621 authority.terminal,
4622 )
4623}
4624
4625fn parse_read_at_open_prefix(
4626 reader: &dyn ArchiveReadAt,
4627 master_key: &MasterKey,
4628) -> Result<ParsedReadAtOpenPrefix, FormatError> {
4629 let volume_header_bytes = read_at_vec(reader, 0, VOLUME_HEADER_LEN, "archive")?;
4630 let volume_header = VolumeHeader::parse(&volume_header_bytes)?;
4631 parse_volume_format_dispatch(&volume_header)?;
4632 let crypto_start = volume_header.crypto_header_offset as u64;
4633 let crypto_len = volume_header.crypto_header_length as u64;
4634 let crypto_bytes = read_at_vec(
4635 reader,
4636 crypto_start,
4637 to_usize(crypto_len, "CryptoHeader")?,
4638 "CryptoHeader",
4639 )?;
4640 let parsed_crypto = CryptoHeader::parse(&crypto_bytes, volume_header.crypto_header_length)?;
4641 if matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. }) {
4642 return Err(FormatError::KeyMaterialMismatch);
4643 }
4644 let subkeys = subkeys_for_open(
4645 Some(master_key),
4646 parsed_crypto.fixed.aead_algo,
4647 &volume_header.archive_uuid,
4648 &volume_header.session_id,
4649 )?;
4650 verify_integrity_tag(
4651 HmacDomain::CryptoHeader,
4652 parsed_crypto.fixed.aead_algo,
4653 volume_header.volume_format_rev,
4654 Some(&subkeys.mac_key),
4655 &volume_header.archive_uuid,
4656 &volume_header.session_id,
4657 parsed_crypto.hmac_covered_bytes,
4658 &parsed_crypto.header_hmac,
4659 )?;
4660 parsed_crypto.validate_extension_semantics()?;
4661 validate_seekable_supported_volume(
4662 &volume_header,
4663 &parsed_crypto.fixed,
4664 &parsed_crypto.extensions,
4665 )?;
4666 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4667 let block_records_start = startup_block_records_start(
4668 &volume_header,
4669 &parsed_crypto.kdf_params,
4670 |start, length| read_at_vec(reader, start, length, "KeyWrapTableV1"),
4671 )?;
4672 let crypto_header = parsed_crypto.fixed.clone();
4673 drop(parsed_crypto);
4674 Ok(ParsedReadAtOpenPrefix {
4675 volume_header,
4676 crypto_header,
4677 crypto_header_bytes: crypto_bytes,
4678 key_wrap_table_bytes: None,
4679 block_records_start,
4680 subkeys,
4681 })
4682}
4683
4684fn parse_read_at_open_prefix_with_recipient_wrap_resolver<F>(
4685 reader: &dyn ArchiveReadAt,
4686 resolver: &mut F,
4687) -> Result<ParsedReadAtOpenPrefix, FormatError>
4688where
4689 F: FnMut(
4690 RecipientWrapRecordContext<'_>,
4691 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4692{
4693 let volume_header_bytes = read_at_vec(reader, 0, VOLUME_HEADER_LEN, "archive")?;
4694 let volume_header = VolumeHeader::parse(&volume_header_bytes)?;
4695 parse_volume_format_dispatch(&volume_header)?;
4696 let crypto_start = volume_header.crypto_header_offset as u64;
4697 let crypto_len = volume_header.crypto_header_length as u64;
4698 let crypto_bytes = read_at_vec(
4699 reader,
4700 crypto_start,
4701 to_usize(crypto_len, "CryptoHeader")?,
4702 "CryptoHeader",
4703 )?;
4704 let parsed_crypto = CryptoHeader::parse(&crypto_bytes, volume_header.crypto_header_length)?;
4705 if !matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
4706 || !parsed_crypto.fixed.aead_algo.is_encrypted()
4707 {
4708 return Err(FormatError::KeyMaterialMismatch);
4709 }
4710
4711 validate_seekable_supported_volume(&volume_header, &parsed_crypto.fixed, &[])?;
4712 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4713
4714 let startup_key_wrap_table = startup_key_wrap_table(
4715 &volume_header,
4716 &parsed_crypto.kdf_params,
4717 |start, length| read_at_vec(reader, start, length, "KeyWrapTableV1"),
4718 )?
4719 .ok_or(FormatError::KeyMaterialMismatch)?;
4720 let key_wrap_table = startup_key_wrap_table.table;
4721 let key_wrap_table_bytes = Some(startup_key_wrap_table.bytes);
4722 let block_records_start = startup_key_wrap_table.block_records_start;
4723
4724 let subkeys = recipient_wrap_subkeys_from_table(
4725 &volume_header,
4726 &parsed_crypto,
4727 &key_wrap_table,
4728 resolver,
4729 )?;
4730 parsed_crypto.validate_extension_semantics()?;
4731 reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
4732
4733 let crypto_header = parsed_crypto.fixed.clone();
4734 Ok(ParsedReadAtOpenPrefix {
4735 volume_header,
4736 crypto_header,
4737 crypto_header_bytes: crypto_bytes,
4738 key_wrap_table_bytes,
4739 block_records_start,
4740 subkeys,
4741 })
4742}
4743
4744#[allow(clippy::too_many_arguments)]
4745fn finish_parse_seekable_read_at_volume(
4746 reader: Arc<dyn ArchiveReadAt>,
4747 volume_header: VolumeHeader,
4748 crypto_header: CryptoHeaderFixed,
4749 crypto_header_bytes: Vec<u8>,
4750 key_wrap_table_bytes: Option<Vec<u8>>,
4751 block_records_start: u64,
4752 subkeys: Subkeys,
4753 terminal: V41Terminal,
4754) -> Result<ParsedSeekableReadAtVolume, FormatError> {
4755 let volume_trailer = terminal.volume_trailer.clone();
4756 validate_trailer_identity(&volume_header, &volume_trailer)?;
4757
4758 let manifest_offset = volume_trailer.manifest_footer_offset;
4759 let manifest_end = checked_u64_add(
4760 manifest_offset,
4761 MANIFEST_FOOTER_LEN as u64,
4762 "ManifestFooter",
4763 )?;
4764 if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
4765 if volume_trailer.root_auth_footer_offset != manifest_end
4766 || volume_trailer
4767 .root_auth_footer_offset
4768 .checked_add(volume_trailer.root_auth_footer_length as u64)
4769 .ok_or(FormatError::InvalidArchive(
4770 "RootAuthFooter terminal boundary overflow",
4771 ))?
4772 != terminal.image.volume_trailer_offset
4773 {
4774 return Err(FormatError::InvalidArchive(
4775 "RootAuthFooter does not sit before selected trailer",
4776 ));
4777 }
4778 } else if manifest_end != terminal.image.volume_trailer_offset {
4779 return Err(FormatError::InvalidArchive(
4780 "ManifestFooter does not end at selected trailer",
4781 ));
4782 }
4783 validate_seekable_block_region_layout(
4784 block_records_start,
4785 manifest_offset,
4786 crypto_header.block_size as usize,
4787 &volume_trailer,
4788 )?;
4789
4790 let manifest_bytes = &terminal.manifest_footer_bytes;
4791 let (manifest_footer, manifest_footer_error) =
4792 match parse_valid_manifest_footer(&volume_header, &crypto_header, &subkeys, manifest_bytes)
4793 {
4794 Ok(footer) => (Some(footer), None),
4795 Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
4796 Err(err) => return Err(err),
4797 };
4798
4799 Ok(ParsedSeekableReadAtVolume {
4800 reader,
4801 volume_header,
4802 crypto_header,
4803 crypto_header_bytes,
4804 key_wrap_table_bytes,
4805 subkeys,
4806 manifest_footer,
4807 manifest_footer_error,
4808 root_auth_footer: terminal.root_auth_footer,
4809 root_auth_footer_bytes: terminal.root_auth_footer_bytes,
4810 volume_trailer,
4811 block_records_start,
4812 })
4813}
4814
4815#[derive(Debug)]
4816struct ParsedPublicNoKeyVolume {
4817 volume_header: VolumeHeader,
4818 crypto_header: CryptoHeaderFixed,
4819 kdf_params: KdfParams,
4820 root_auth_footer: RootAuthFooterV1,
4821 root_auth_footer_bytes: Vec<u8>,
4822 blocks: BTreeMap<u64, BlockRecord>,
4823}
4824
4825pub fn public_no_key_verify_volumes_with_options<F>(
4826 volumes: &[&[u8]],
4827 mut verifier: F,
4828 options: ReaderOptions,
4829) -> Result<PublicNoKeyVerification, FormatError>
4830where
4831 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
4832{
4833 validate_reader_options(options)?;
4834 if volumes.is_empty() {
4835 return Err(FormatError::InvalidArchive("no volumes supplied"));
4836 }
4837 let mut parsed = Vec::with_capacity(volumes.len());
4838 for volume in volumes {
4839 parsed.push(parse_public_no_key_volume(volume, options)?);
4840 }
4841 let first = parsed
4842 .first()
4843 .ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
4844 if parsed.len() != first.crypto_header.stripe_width as usize {
4845 return Err(FormatError::ReaderUnsupported(
4846 "public no-key verification requires a complete volume set",
4847 ));
4848 }
4849
4850 let mut seen_volume_indexes = BTreeSet::new();
4851 let mut blocks = BTreeMap::new();
4852 for volume in &parsed {
4853 if volume.volume_header.archive_uuid != first.volume_header.archive_uuid
4854 || volume.volume_header.session_id != first.volume_header.session_id
4855 || !public_crypto_headers_agree(&volume.crypto_header, &first.crypto_header)
4856 || !public_kdf_profiles_agree(&volume.kdf_params, &first.kdf_params)
4857 {
4858 return Err(FormatError::InvalidArchive(
4859 "public no-key volume global metadata differs",
4860 ));
4861 }
4862 if volume.root_auth_footer_bytes != first.root_auth_footer_bytes {
4863 return Err(FormatError::InvalidArchive(
4864 "public no-key RootAuthFooter copies differ",
4865 ));
4866 }
4867 if !seen_volume_indexes.insert(volume.volume_header.volume_index) {
4868 return Err(FormatError::InvalidArchive(
4869 "duplicate public no-key volume index",
4870 ));
4871 }
4872 for (block_index, record) in &volume.blocks {
4873 if blocks.insert(*block_index, record.clone()).is_some() {
4874 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
4875 }
4876 }
4877 }
4878 validate_complete_global_block_coverage(&blocks, &BTreeSet::new())?;
4879
4880 let footer = &first.root_auth_footer;
4881 let mut data_leaves = blocks
4882 .values()
4883 .filter(|record| record.kind.is_data())
4884 .map(|record| DataBlockMerkleLeaf {
4885 block_index: record.block_index,
4886 kind: record.kind,
4887 flags: record.flags,
4888 payload: record.payload.clone(),
4889 })
4890 .collect::<Vec<_>>();
4891 data_leaves.sort_by_key(|leaf| leaf.block_index);
4892 let total_data_block_count = u64::try_from(data_leaves.len())
4893 .map_err(|_| FormatError::InvalidArchive("public no-key data block count overflow"))?;
4894 let observed_data_root = data_block_merkle_root_for_revision(
4895 footer.format_version,
4896 footer.volume_format_rev,
4897 &data_leaves,
4898 )?;
4899 if total_data_block_count != footer.total_data_block_count
4900 || observed_data_root != footer.data_block_merkle_root
4901 {
4902 return Err(FormatError::InvalidArchive(
4903 "public no-key data-block commitment mismatch",
4904 ));
4905 }
4906 let archive_root = recompute_public_archive_root(footer, &first.crypto_header)?;
4907 if archive_root != footer.archive_root {
4908 return Err(FormatError::InvalidArchive(
4909 "public no-key archive_root mismatch",
4910 ));
4911 }
4912 if !verifier(footer, &archive_root)? {
4913 return Err(FormatError::InvalidArchive(
4914 "public no-key authenticator verification failed",
4915 ));
4916 }
4917 Ok(PublicNoKeyVerification {
4918 format_version: footer.format_version,
4919 volume_format_rev: footer.volume_format_rev,
4920 archive_root,
4921 authenticator_id: footer.authenticator_id,
4922 signer_identity_type: footer.signer_identity_type,
4923 signer_identity_bytes: footer.signer_identity_bytes.clone(),
4924 total_data_block_count,
4925 diagnostics: vec![
4926 PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
4927 PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
4928 PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
4929 ],
4930 })
4931}
4932
4933fn parse_public_no_key_volume(
4934 bytes: &[u8],
4935 options: ReaderOptions,
4936) -> Result<ParsedPublicNoKeyVolume, FormatError> {
4937 if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
4938 return Err(FormatError::InvalidLength {
4939 structure: "archive",
4940 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
4941 actual: bytes.len(),
4942 });
4943 }
4944 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
4945 parse_volume_format_dispatch(&volume_header)?;
4946 let crypto_start = volume_header.crypto_header_offset as usize;
4947 let crypto_len = volume_header.crypto_header_length as usize;
4948 let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
4949 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
4950 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
4951 parsed_crypto.validate_extension_semantics()?;
4952 validate_seekable_supported_volume(
4953 &volume_header,
4954 &parsed_crypto.fixed,
4955 &parsed_crypto.extensions,
4956 )?;
4957 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4958
4959 let terminal = locate_v41_public_terminal(bytes, &volume_header, &parsed_crypto, options)?;
4960 let block_records_start = match &parsed_crypto.kdf_params {
4961 KdfParams::RecipientWrap {
4962 key_wrap_table_length,
4963 ..
4964 } => checked_add(
4965 crypto_end,
4966 *key_wrap_table_length as usize,
4967 "KeyWrapTableV1",
4968 )?,
4969 _ => crypto_end,
4970 };
4971 let block_region = parse_public_block_observation(
4972 bytes,
4973 block_records_start,
4974 &terminal.image,
4975 parsed_crypto.fixed.block_size as usize,
4976 &volume_header,
4977 )?;
4978 Ok(ParsedPublicNoKeyVolume {
4979 volume_header,
4980 crypto_header: parsed_crypto.fixed,
4981 kdf_params: parsed_crypto.kdf_params,
4982 root_auth_footer: terminal.root_auth_footer,
4983 root_auth_footer_bytes: terminal.root_auth_footer_bytes,
4984 blocks: block_region,
4985 })
4986}
4987
4988fn public_crypto_headers_agree(left: &CryptoHeaderFixed, right: &CryptoHeaderFixed) -> bool {
4989 left.length == right.length
4990 && left.stripe_width == right.stripe_width
4991 && left.block_size == right.block_size
4992 && left.compression_algo == right.compression_algo
4993 && left.aead_algo == right.aead_algo
4994 && left.fec_algo == right.fec_algo
4995 && left.kdf_algo == right.kdf_algo
4996}
4997
4998fn public_kdf_profiles_agree(left: &KdfParams, right: &KdfParams) -> bool {
4999 match (left, right) {
5000 (
5001 KdfParams::RecipientWrap {
5002 key_wrap_table_length: left_length,
5003 key_wrap_table_record_count: left_count,
5004 key_wrap_table_version: left_version,
5005 key_wrap_table_digest: left_digest,
5006 },
5007 KdfParams::RecipientWrap {
5008 key_wrap_table_length: right_length,
5009 key_wrap_table_record_count: right_count,
5010 key_wrap_table_version: right_version,
5011 key_wrap_table_digest: right_digest,
5012 },
5013 ) => {
5014 left_length == right_length
5015 && left_count == right_count
5016 && left_version == right_version
5017 && left_digest == right_digest
5018 }
5019 (KdfParams::RecipientWrap { .. }, _) | (_, KdfParams::RecipientWrap { .. }) => false,
5020 _ => true,
5021 }
5022}
5023
5024fn recompute_public_archive_root(
5025 footer: &RootAuthFooterV1,
5026 crypto_header: &CryptoHeaderFixed,
5027) -> Result<[u8; 32], FormatError> {
5028 let descriptor_digest = root_auth_descriptor_digest_for_revision(
5029 footer.format_version,
5030 footer.volume_format_rev,
5031 footer.authenticator_id,
5032 footer.signer_identity_type,
5033 &footer.signer_identity_bytes,
5034 u32::try_from(footer.authenticator_value.len()).map_err(|_| {
5035 FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
5036 })?,
5037 footer.footer_length()?,
5038 )?;
5039 let signer_digest =
5040 signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
5041 if signer_digest != footer.signer_identity_digest {
5042 return Err(FormatError::InvalidArchive(
5043 "public no-key signer identity digest mismatch",
5044 ));
5045 }
5046 archive_root_for_revision(ArchiveRootInputs {
5047 archive_uuid: footer.archive_uuid,
5048 session_id: footer.session_id,
5049 format_version: footer.format_version,
5050 volume_format_rev: footer.volume_format_rev,
5051 compression_algo: crypto_header.compression_algo,
5052 aead_algo: crypto_header.aead_algo,
5053 fec_algo: crypto_header.fec_algo,
5054 kdf_algo: crypto_header.kdf_algo,
5055 critical_metadata_digest: footer.critical_metadata_digest,
5056 index_digest: footer.index_digest,
5057 fec_layout_digest: footer.fec_layout_digest,
5058 total_data_block_count: footer.total_data_block_count,
5059 data_block_merkle_root: footer.data_block_merkle_root,
5060 root_auth_descriptor_digest: descriptor_digest,
5061 signer_identity_digest: signer_digest,
5062 })
5063}
5064
5065fn parse_valid_manifest_footer(
5066 volume_header: &VolumeHeader,
5067 crypto_header: &CryptoHeaderFixed,
5068 subkeys: &Subkeys,
5069 manifest_bytes: &[u8],
5070) -> Result<ManifestFooter, FormatError> {
5071 let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
5072 validate_manifest_footer(
5073 volume_header,
5074 crypto_header,
5075 &manifest_footer,
5076 subkeys,
5077 volume_header.volume_format_rev,
5078 manifest_bytes,
5079 )?;
5080 manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
5081 Ok(manifest_footer)
5082}
5083
5084fn manifest_footer_copy_error_is_recoverable(error: &FormatError) -> bool {
5085 matches!(
5086 error,
5087 FormatError::BadMagic {
5088 structure: "ManifestFooter",
5089 } | FormatError::NonZeroReserved {
5090 structure: "ManifestFooter",
5091 } | FormatError::InvalidAuthoritativeFlag(_)
5092 | FormatError::HmacMismatch {
5093 structure: "ManifestFooter",
5094 }
5095 | FormatError::IntegrityDigestMismatch {
5096 structure: "ManifestFooter",
5097 }
5098 )
5099}
5100
5101fn validate_seekable_supported_volume(
5102 volume_header: &VolumeHeader,
5103 crypto_header: &CryptoHeaderFixed,
5104 extensions: &[ExtensionTlv<'_>],
5105) -> Result<(), FormatError> {
5106 reject_unsupported_raw_stream_profile(extensions)?;
5107 if crypto_header.stripe_width != volume_header.stripe_width {
5108 return Err(FormatError::InvalidArchive(
5109 "VolumeHeader and CryptoHeader stripe_width differ",
5110 ));
5111 }
5112 Ok(())
5113}
5114
5115pub(crate) fn validate_crypto_class_parity_exactness(
5116 crypto_header: &CryptoHeaderFixed,
5117) -> Result<(), FormatError> {
5118 let fec = required_object_parity(crypto_header.fec_data_shards as u64, crypto_header)?;
5119 if crypto_header.fec_parity_shards as u32 != fec {
5120 return Err(FormatError::InvalidArchive(
5121 "fec_parity_shards does not match v41 compute_parity",
5122 ));
5123 }
5124 let index = required_object_parity(crypto_header.index_fec_data_shards as u64, crypto_header)?;
5125 if crypto_header.index_fec_parity_shards as u32 != index {
5126 return Err(FormatError::InvalidArchive(
5127 "index_fec_parity_shards does not match v41 compute_parity",
5128 ));
5129 }
5130 let index_root = required_object_parity(
5131 crypto_header.index_root_fec_data_shards as u64,
5132 crypto_header,
5133 )?;
5134 if crypto_header.index_root_fec_parity_shards as u32 != index_root {
5135 return Err(FormatError::InvalidArchive(
5136 "index_root_fec_parity_shards does not match v41 compute_parity",
5137 ));
5138 }
5139 Ok(())
5140}
5141
5142fn validate_volume_set_member(
5143 first: &ParsedSeekableVolume,
5144 candidate: &ParsedSeekableVolume,
5145) -> Result<(), FormatError> {
5146 validate_volume_set_member_metadata(
5147 &first.volume_header,
5148 &first.crypto_header,
5149 &first.crypto_header_bytes,
5150 &candidate.volume_header,
5151 &candidate.crypto_header,
5152 &candidate.crypto_header_bytes,
5153 )?;
5154 validate_key_wrap_table_bytes_match(
5155 &first.key_wrap_table_bytes,
5156 &candidate.key_wrap_table_bytes,
5157 )
5158}
5159
5160fn validate_key_wrap_table_bytes_match(
5161 first_key_wrap_table_bytes: &Option<Vec<u8>>,
5162 candidate_key_wrap_table_bytes: &Option<Vec<u8>>,
5163) -> Result<(), FormatError> {
5164 if candidate_key_wrap_table_bytes != first_key_wrap_table_bytes {
5165 return Err(FormatError::InvalidArchive("KeyWrapTableV1 copies differ"));
5166 }
5167 Ok(())
5168}
5169
5170fn validate_volume_set_member_metadata(
5171 first_volume_header: &VolumeHeader,
5172 first_crypto_header: &CryptoHeaderFixed,
5173 first_crypto_header_bytes: &[u8],
5174 candidate_volume_header: &VolumeHeader,
5175 candidate_crypto_header: &CryptoHeaderFixed,
5176 candidate_crypto_header_bytes: &[u8],
5177) -> Result<(), FormatError> {
5178 if candidate_volume_header.archive_uuid != first_volume_header.archive_uuid
5179 || candidate_volume_header.session_id != first_volume_header.session_id
5180 {
5181 return Err(FormatError::InvalidArchive(
5182 "mixed archive or session IDs in volume set",
5183 ));
5184 }
5185 if candidate_crypto_header_bytes != first_crypto_header_bytes
5186 || candidate_crypto_header != first_crypto_header
5187 {
5188 return Err(FormatError::InvalidArchive("CryptoHeader copies differ"));
5189 }
5190 Ok(())
5191}
5192
5193pub(crate) fn manifest_bootstrap_fields_match(
5194 left: &ManifestFooter,
5195 right: &ManifestFooter,
5196) -> bool {
5197 left.archive_uuid == right.archive_uuid
5198 && left.session_id == right.session_id
5199 && left.is_authoritative == right.is_authoritative
5200 && left.total_volumes == right.total_volumes
5201 && left.index_root_first_block == right.index_root_first_block
5202 && left.index_root_data_block_count == right.index_root_data_block_count
5203 && left.index_root_parity_block_count == right.index_root_parity_block_count
5204 && left.index_root_encrypted_size == right.index_root_encrypted_size
5205 && left.index_root_decompressed_size == right.index_root_decompressed_size
5206}
5207
5208fn validate_complete_global_block_coverage(
5209 blocks: &BTreeMap<u64, BlockRecord>,
5210 erased_block_indices: &BTreeSet<u64>,
5211) -> Result<(), FormatError> {
5212 let mut expected = 0u64;
5213 let mut block_iter = blocks.keys().copied().peekable();
5214 let mut erasure_iter = erased_block_indices.iter().copied().peekable();
5215
5216 loop {
5217 let next_block = block_iter.peek().copied();
5218 let next_erasure = erasure_iter.peek().copied();
5219 let next = match (next_block, next_erasure) {
5220 (Some(block), Some(erasure)) if block == erasure => {
5221 return Err(FormatError::InvalidArchive(
5222 "BlockRecord index is both present and erased",
5223 ));
5224 }
5225 (Some(block), Some(erasure)) => block.min(erasure),
5226 (Some(block), None) => block,
5227 (None, Some(erasure)) => erasure,
5228 (None, None) => return Ok(()),
5229 };
5230
5231 if next != expected {
5232 return Err(FormatError::InvalidArchive(
5233 "complete volume set has missing global blocks",
5234 ));
5235 }
5236 if next_block == Some(next) {
5237 block_iter.next();
5238 }
5239 if next_erasure == Some(next) {
5240 erasure_iter.next();
5241 }
5242 expected = expected
5243 .checked_add(1)
5244 .ok_or(FormatError::InvalidArchive("global block index overflow"))?;
5245 }
5246}
5247
5248#[derive(Debug)]
5249struct V41Terminal {
5250 image: CriticalMetadataImage,
5251 manifest_footer_bytes: Vec<u8>,
5252 root_auth_footer_bytes: Option<Vec<u8>>,
5253 root_auth_footer: Option<RootAuthFooterV1>,
5254 volume_trailer: VolumeTrailer,
5255}
5256
5257pub(crate) struct SequentialTerminalMaterial {
5258 pub(crate) manifest_footer: ManifestFooter,
5259 pub(crate) volume_trailer: VolumeTrailer,
5260 pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
5261}
5262
5263#[derive(Debug)]
5264struct V41PublicTerminal {
5265 image: CriticalMetadataImage,
5266 root_auth_footer_bytes: Vec<u8>,
5267 root_auth_footer: RootAuthFooterV1,
5268}
5269
5270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5271struct CmraDecoderTuple {
5272 shard_size: u32,
5273 data_shard_count: u16,
5274 parity_shard_count: u16,
5275 image_length: u32,
5276 image_sha256: [u8; 32],
5277}
5278
5279#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5280struct CmraIdentityHints {
5281 archive_uuid: [u8; 16],
5282 session_id: [u8; 16],
5283 volume_index: u32,
5284}
5285
5286impl From<CriticalMetadataRecoveryHeader> for CmraDecoderTuple {
5287 fn from(header: CriticalMetadataRecoveryHeader) -> Self {
5288 Self {
5289 shard_size: header.shard_size,
5290 data_shard_count: header.data_shard_count,
5291 parity_shard_count: header.parity_shard_count,
5292 image_length: header.image_length,
5293 image_sha256: header.image_sha256,
5294 }
5295 }
5296}
5297
5298impl From<CriticalMetadataRecoveryHeader> for CmraIdentityHints {
5299 fn from(header: CriticalMetadataRecoveryHeader) -> Self {
5300 Self {
5301 archive_uuid: header.archive_uuid_hint,
5302 session_id: header.session_id_hint,
5303 volume_index: header.volume_index_hint,
5304 }
5305 }
5306}
5307
5308impl From<CriticalRecoveryLocator> for CmraDecoderTuple {
5309 fn from(locator: CriticalRecoveryLocator) -> Self {
5310 Self {
5311 shard_size: locator.cmra_shard_size,
5312 data_shard_count: locator.cmra_data_shard_count,
5313 parity_shard_count: locator.cmra_parity_shard_count,
5314 image_length: locator.cmra_image_length,
5315 image_sha256: locator.cmra_image_sha256,
5316 }
5317 }
5318}
5319
5320impl From<CriticalRecoveryLocator> for CmraIdentityHints {
5321 fn from(locator: CriticalRecoveryLocator) -> Self {
5322 Self {
5323 archive_uuid: locator.archive_uuid_hint,
5324 session_id: locator.session_id_hint,
5325 volume_index: locator.volume_index_hint,
5326 }
5327 }
5328}
5329
5330#[derive(Debug)]
5331struct RecoveredCmra {
5332 image: CriticalMetadataImage,
5333 tuple: CmraDecoderTuple,
5334 header_hints: Option<CmraIdentityHints>,
5335 cmra_length: u64,
5336}
5337
5338#[derive(Debug)]
5339struct TerminalCandidate {
5340 terminal: V41Terminal,
5341 anchor: usize,
5342 locator_sequence: Option<u32>,
5343 cmra_offset: u64,
5344 cmra_length: u64,
5345}
5346
5347#[derive(Debug)]
5348struct PublicTerminalCandidate {
5349 terminal: V41PublicTerminal,
5350 anchor: usize,
5351 cmra_offset: u64,
5352 cmra_length: u64,
5353}
5354
5355#[derive(Debug)]
5356struct RecoveredTerminalAuthority {
5357 terminal: V41Terminal,
5358 volume_header: VolumeHeader,
5359 crypto_header: CryptoHeaderFixed,
5360 crypto_header_bytes: Vec<u8>,
5361 subkeys: Subkeys,
5362 kdf_params: KdfParams,
5363}
5364
5365#[derive(Debug)]
5366struct RecoveredRecipientWrapTerminalAuthority {
5367 terminal: V41Terminal,
5368 volume_header: VolumeHeader,
5369 crypto_header: CryptoHeaderFixed,
5370 crypto_header_bytes: Vec<u8>,
5371 key_wrap_table_bytes: Vec<u8>,
5372 block_records_start: u64,
5373 subkeys: Subkeys,
5374}
5375
5376#[derive(Debug)]
5377struct TerminalAuthorityCandidate {
5378 authority: RecoveredTerminalAuthority,
5379 anchor: usize,
5380 cmra_offset: u64,
5381 cmra_length: u64,
5382}
5383
5384#[derive(Debug)]
5385struct RecipientWrapTerminalAuthorityCandidate {
5386 authority: RecoveredRecipientWrapTerminalAuthority,
5387 anchor: usize,
5388 cmra_offset: u64,
5389 cmra_length: u64,
5390}
5391
5392#[derive(Debug, Clone, Copy)]
5393enum CmraRecoveryMode {
5394 KeyHolding,
5395 PublicNoKey,
5396}
5397
5398#[derive(Clone, Copy)]
5399pub(crate) struct KeyHoldingTerminalContext<'a> {
5400 pub(crate) subkeys: &'a Subkeys,
5401 pub(crate) volume_header: &'a VolumeHeader,
5402 pub(crate) crypto_header: &'a CryptoHeaderFixed,
5403 pub(crate) crypto_header_bytes: &'a [u8],
5404}
5405
5406fn locate_v41_terminal(
5407 bytes: &[u8],
5408 context: KeyHoldingTerminalContext<'_>,
5409 options: ReaderOptions,
5410) -> Result<V41Terminal, FormatError> {
5411 locate_v41_terminal_candidate(bytes, context, options).map(|candidate| candidate.terminal)
5412}
5413
5414fn locate_v41_terminal_read_at(
5415 reader: &dyn ArchiveReadAt,
5416 len: u64,
5417 context: KeyHoldingTerminalContext<'_>,
5418 options: ReaderOptions,
5419) -> Result<V41Terminal, FormatError> {
5420 let mut candidates = Vec::new();
5421 if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
5422 let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
5423 collect_v41_locator_candidate_read_at(reader, final_offset, 0, context, &mut candidates);
5424 }
5425 if len >= LOCATOR_PAIR_LEN as u64 {
5426 let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
5427 collect_v41_locator_candidate_read_at(reader, mirror_offset, 1, context, &mut candidates);
5428 }
5429
5430 if candidates.is_empty() {
5431 let scan = max_critical_recovery_scan(options)? as u64;
5432 let scan_start = len.saturating_sub(scan);
5433 let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
5434 let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
5435 let mut offset = tail.len().saturating_sub(4);
5436 while offset < tail.len() {
5437 let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
5438 if tail.get(offset..offset + 4) == Some(b"TZCL") {
5439 collect_v41_locator_candidate_read_at(
5440 reader,
5441 absolute_offset,
5442 2,
5443 context,
5444 &mut candidates,
5445 );
5446 } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
5447 if let Ok(candidate) =
5448 parse_locatorless_cmra_candidate_read_at(reader, absolute_offset, context)
5449 {
5450 candidates.push(candidate);
5451 }
5452 }
5453 if offset == 0 {
5454 break;
5455 }
5456 offset -= 1;
5457 }
5458 }
5459
5460 choose_v41_terminal_candidate(candidates).map(|candidate| candidate.terminal)
5461}
5462
5463fn locate_v41_terminal_authority(
5464 bytes: &[u8],
5465 master_key: &MasterKey,
5466 options: ReaderOptions,
5467) -> Result<RecoveredTerminalAuthority, FormatError> {
5468 let mut candidates = Vec::new();
5469 if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
5470 let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
5471 collect_v41_locator_authority_candidate(
5472 bytes,
5473 final_offset,
5474 0,
5475 master_key,
5476 &mut candidates,
5477 );
5478 }
5479 if bytes.len() >= LOCATOR_PAIR_LEN {
5480 let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
5481 collect_v41_locator_authority_candidate(
5482 bytes,
5483 mirror_offset,
5484 1,
5485 master_key,
5486 &mut candidates,
5487 );
5488 }
5489
5490 if candidates.is_empty() {
5491 let scan = max_critical_recovery_scan(options)?;
5492 let scan_start = bytes.len().saturating_sub(scan);
5493 let mut offset = bytes.len().saturating_sub(4);
5494 while offset >= scan_start {
5495 if bytes.get(offset..offset + 4) == Some(b"TZCL") {
5496 collect_v41_locator_authority_candidate(
5497 bytes,
5498 offset,
5499 2,
5500 master_key,
5501 &mut candidates,
5502 );
5503 } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
5504 if let Ok(candidate) =
5505 parse_locatorless_cmra_authority_candidate(bytes, offset, master_key)
5506 {
5507 candidates.push(candidate);
5508 }
5509 }
5510 if offset == 0 {
5511 break;
5512 }
5513 offset -= 1;
5514 }
5515 }
5516
5517 choose_v41_terminal_authority_candidate(candidates).map(|candidate| candidate.authority)
5518}
5519
5520fn locate_v41_recipient_wrap_terminal_authority<F>(
5521 bytes: &[u8],
5522 resolver: &mut F,
5523 options: ReaderOptions,
5524) -> Result<RecoveredRecipientWrapTerminalAuthority, FormatError>
5525where
5526 F: FnMut(
5527 RecipientWrapRecordContext<'_>,
5528 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
5529{
5530 let mut candidates = Vec::new();
5531 if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
5532 let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
5533 collect_v41_recipient_wrap_locator_authority_candidate(
5534 bytes,
5535 final_offset,
5536 0,
5537 resolver,
5538 &mut candidates,
5539 );
5540 }
5541 if bytes.len() >= LOCATOR_PAIR_LEN {
5542 let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
5543 collect_v41_recipient_wrap_locator_authority_candidate(
5544 bytes,
5545 mirror_offset,
5546 1,
5547 resolver,
5548 &mut candidates,
5549 );
5550 }
5551
5552 if candidates.is_empty() {
5553 let scan = max_critical_recovery_scan(options)?;
5554 let scan_start = bytes.len().saturating_sub(scan);
5555 let mut offset = bytes.len().saturating_sub(4);
5556 while offset >= scan_start {
5557 if bytes.get(offset..offset + 4) == Some(b"TZCL") {
5558 collect_v41_recipient_wrap_locator_authority_candidate(
5559 bytes,
5560 offset,
5561 2,
5562 resolver,
5563 &mut candidates,
5564 );
5565 } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
5566 if let Ok(candidate) = parse_locatorless_cmra_recipient_wrap_authority_candidate(
5567 bytes, offset, resolver,
5568 ) {
5569 candidates.push(candidate);
5570 }
5571 }
5572 if offset == 0 {
5573 break;
5574 }
5575 offset -= 1;
5576 }
5577 }
5578
5579 choose_v41_recipient_wrap_terminal_authority_candidate(candidates)
5580 .map(|candidate| candidate.authority)
5581}
5582
5583fn locate_v41_recipient_wrap_terminal_authority_read_at<F>(
5584 reader: &dyn ArchiveReadAt,
5585 len: u64,
5586 resolver: &mut F,
5587 options: ReaderOptions,
5588) -> Result<RecoveredRecipientWrapTerminalAuthority, FormatError>
5589where
5590 F: FnMut(
5591 RecipientWrapRecordContext<'_>,
5592 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
5593{
5594 let mut candidates = Vec::new();
5595 if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
5596 let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
5597 collect_v41_recipient_wrap_locator_authority_candidate_read_at(
5598 reader,
5599 final_offset,
5600 0,
5601 resolver,
5602 &mut candidates,
5603 );
5604 }
5605 if len >= LOCATOR_PAIR_LEN as u64 {
5606 let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
5607 collect_v41_recipient_wrap_locator_authority_candidate_read_at(
5608 reader,
5609 mirror_offset,
5610 1,
5611 resolver,
5612 &mut candidates,
5613 );
5614 }
5615
5616 if candidates.is_empty() {
5617 let scan = max_critical_recovery_scan(options)? as u64;
5618 let scan_start = len.saturating_sub(scan);
5619 let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
5620 let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
5621 let mut offset = tail.len().saturating_sub(4);
5622 while offset < tail.len() {
5623 let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
5624 if tail.get(offset..offset + 4) == Some(b"TZCL") {
5625 collect_v41_recipient_wrap_locator_authority_candidate_read_at(
5626 reader,
5627 absolute_offset,
5628 2,
5629 resolver,
5630 &mut candidates,
5631 );
5632 } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
5633 if let Ok(candidate) =
5634 parse_locatorless_cmra_recipient_wrap_authority_candidate_read_at(
5635 reader,
5636 absolute_offset,
5637 resolver,
5638 )
5639 {
5640 candidates.push(candidate);
5641 }
5642 }
5643 if offset == 0 {
5644 break;
5645 }
5646 offset -= 1;
5647 }
5648 }
5649
5650 choose_v41_recipient_wrap_terminal_authority_candidate(candidates)
5651 .map(|candidate| candidate.authority)
5652}
5653
5654fn locate_v41_terminal_authority_read_at(
5655 reader: &dyn ArchiveReadAt,
5656 len: u64,
5657 master_key: &MasterKey,
5658 options: ReaderOptions,
5659) -> Result<RecoveredTerminalAuthority, FormatError> {
5660 let mut candidates = Vec::new();
5661 if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
5662 let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
5663 collect_v41_locator_authority_candidate_read_at(
5664 reader,
5665 final_offset,
5666 0,
5667 master_key,
5668 &mut candidates,
5669 );
5670 }
5671 if len >= LOCATOR_PAIR_LEN as u64 {
5672 let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
5673 collect_v41_locator_authority_candidate_read_at(
5674 reader,
5675 mirror_offset,
5676 1,
5677 master_key,
5678 &mut candidates,
5679 );
5680 }
5681
5682 if candidates.is_empty() {
5683 let scan = max_critical_recovery_scan(options)? as u64;
5684 let scan_start = len.saturating_sub(scan);
5685 let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
5686 let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
5687 let mut offset = tail.len().saturating_sub(4);
5688 while offset < tail.len() {
5689 let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
5690 if tail.get(offset..offset + 4) == Some(b"TZCL") {
5691 collect_v41_locator_authority_candidate_read_at(
5692 reader,
5693 absolute_offset,
5694 2,
5695 master_key,
5696 &mut candidates,
5697 );
5698 } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
5699 if let Ok(candidate) = parse_locatorless_cmra_authority_candidate_read_at(
5700 reader,
5701 absolute_offset,
5702 master_key,
5703 ) {
5704 candidates.push(candidate);
5705 }
5706 }
5707 if offset == 0 {
5708 break;
5709 }
5710 offset -= 1;
5711 }
5712 }
5713
5714 choose_v41_terminal_authority_candidate(candidates).map(|candidate| candidate.authority)
5715}
5716
5717fn locate_v41_terminal_candidate(
5718 bytes: &[u8],
5719 context: KeyHoldingTerminalContext<'_>,
5720 options: ReaderOptions,
5721) -> Result<TerminalCandidate, FormatError> {
5722 let mut candidates = Vec::new();
5723 if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
5724 let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
5725 collect_v41_locator_candidate(bytes, final_offset, 0, context, &mut candidates);
5726 }
5727 if bytes.len() >= LOCATOR_PAIR_LEN {
5728 let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
5729 collect_v41_locator_candidate(bytes, mirror_offset, 1, context, &mut candidates);
5730 }
5731
5732 if candidates.is_empty() {
5733 let scan = max_critical_recovery_scan(options)?;
5734 let scan_start = bytes.len().saturating_sub(scan);
5735 let mut offset = bytes.len().saturating_sub(4);
5736 while offset >= scan_start {
5737 if bytes.get(offset..offset + 4) == Some(b"TZCL") {
5738 collect_v41_locator_candidate(bytes, offset, 2, context, &mut candidates);
5739 } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
5740 if let Ok(candidate) = parse_locatorless_cmra_candidate(bytes, offset, context) {
5741 candidates.push(candidate);
5742 }
5743 }
5744 if offset == 0 {
5745 break;
5746 }
5747 offset -= 1;
5748 }
5749 }
5750
5751 choose_v41_terminal_candidate(candidates)
5752}
5753
5754fn locate_v41_public_terminal(
5755 bytes: &[u8],
5756 volume_header: &VolumeHeader,
5757 crypto_header: &CryptoHeader<'_>,
5758 options: ReaderOptions,
5759) -> Result<V41PublicTerminal, FormatError> {
5760 let mut candidates = Vec::new();
5761 if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
5762 let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
5763 collect_v41_public_locator_candidate(
5764 bytes,
5765 final_offset,
5766 0,
5767 volume_header,
5768 crypto_header,
5769 &mut candidates,
5770 );
5771 }
5772 if bytes.len() >= LOCATOR_PAIR_LEN {
5773 let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
5774 collect_v41_public_locator_candidate(
5775 bytes,
5776 mirror_offset,
5777 1,
5778 volume_header,
5779 crypto_header,
5780 &mut candidates,
5781 );
5782 }
5783
5784 if candidates.is_empty() {
5785 let scan = max_critical_recovery_scan(options)?;
5786 let scan_start = bytes.len().saturating_sub(scan);
5787 let mut offset = bytes.len().saturating_sub(4);
5788 while offset >= scan_start {
5789 if bytes.get(offset..offset + 4) == Some(b"TZCL") {
5790 collect_v41_public_locator_candidate(
5791 bytes,
5792 offset,
5793 2,
5794 volume_header,
5795 crypto_header,
5796 &mut candidates,
5797 );
5798 } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
5799 if let Ok(candidate) = parse_public_locatorless_cmra_candidate(
5800 bytes,
5801 offset,
5802 volume_header,
5803 crypto_header,
5804 ) {
5805 candidates.push(candidate);
5806 }
5807 }
5808 if offset == 0 {
5809 break;
5810 }
5811 offset -= 1;
5812 }
5813 }
5814
5815 choose_v41_public_terminal_candidate(candidates).map(|candidate| candidate.terminal)
5816}
5817
5818fn collect_v41_locator_candidate(
5819 bytes: &[u8],
5820 offset: usize,
5821 expected_sequence: u32,
5822 context: KeyHoldingTerminalContext<'_>,
5823 candidates: &mut Vec<TerminalCandidate>,
5824) {
5825 let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
5826 return;
5827 };
5828 let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
5829 return;
5830 };
5831 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
5832 return;
5833 }
5834 if let Ok(candidate) = parse_locator_cmra_candidate(bytes, offset, locator, context) {
5835 candidates.push(candidate);
5836 }
5837}
5838
5839fn collect_v41_locator_candidate_read_at(
5840 reader: &dyn ArchiveReadAt,
5841 offset: u64,
5842 expected_sequence: u32,
5843 context: KeyHoldingTerminalContext<'_>,
5844 candidates: &mut Vec<TerminalCandidate>,
5845) {
5846 let Ok(raw) = read_at_vec(
5847 reader,
5848 offset,
5849 CRITICAL_RECOVERY_LOCATOR_LEN,
5850 "CriticalRecoveryLocator",
5851 ) else {
5852 return;
5853 };
5854 let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
5855 return;
5856 };
5857 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
5858 return;
5859 }
5860 if let Ok(candidate) = parse_locator_cmra_candidate_read_at(reader, offset, locator, context) {
5861 candidates.push(candidate);
5862 }
5863}
5864
5865fn collect_v41_locator_authority_candidate(
5866 bytes: &[u8],
5867 offset: usize,
5868 expected_sequence: u32,
5869 master_key: &MasterKey,
5870 candidates: &mut Vec<TerminalAuthorityCandidate>,
5871) {
5872 let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
5873 return;
5874 };
5875 let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
5876 return;
5877 };
5878 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
5879 return;
5880 }
5881 if let Ok(candidate) =
5882 parse_locator_cmra_authority_candidate(bytes, offset, locator, master_key)
5883 {
5884 candidates.push(candidate);
5885 }
5886}
5887
5888fn collect_v41_recipient_wrap_locator_authority_candidate<F>(
5889 bytes: &[u8],
5890 offset: usize,
5891 expected_sequence: u32,
5892 resolver: &mut F,
5893 candidates: &mut Vec<RecipientWrapTerminalAuthorityCandidate>,
5894) where
5895 F: FnMut(
5896 RecipientWrapRecordContext<'_>,
5897 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
5898{
5899 let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
5900 return;
5901 };
5902 let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
5903 return;
5904 };
5905 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
5906 return;
5907 }
5908 if let Ok(candidate) =
5909 parse_locator_cmra_recipient_wrap_authority_candidate(bytes, offset, locator, resolver)
5910 {
5911 candidates.push(candidate);
5912 }
5913}
5914
5915fn collect_v41_recipient_wrap_locator_authority_candidate_read_at<F>(
5916 reader: &dyn ArchiveReadAt,
5917 offset: u64,
5918 expected_sequence: u32,
5919 resolver: &mut F,
5920 candidates: &mut Vec<RecipientWrapTerminalAuthorityCandidate>,
5921) where
5922 F: FnMut(
5923 RecipientWrapRecordContext<'_>,
5924 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
5925{
5926 let Ok(raw) = read_at_vec(
5927 reader,
5928 offset,
5929 CRITICAL_RECOVERY_LOCATOR_LEN,
5930 "CriticalRecoveryLocator",
5931 ) else {
5932 return;
5933 };
5934 let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
5935 return;
5936 };
5937 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
5938 return;
5939 }
5940 if let Ok(candidate) = parse_locator_cmra_recipient_wrap_authority_candidate_read_at(
5941 reader, offset, locator, resolver,
5942 ) {
5943 candidates.push(candidate);
5944 }
5945}
5946
5947fn collect_v41_locator_authority_candidate_read_at(
5948 reader: &dyn ArchiveReadAt,
5949 offset: u64,
5950 expected_sequence: u32,
5951 master_key: &MasterKey,
5952 candidates: &mut Vec<TerminalAuthorityCandidate>,
5953) {
5954 let Ok(raw) = read_at_vec(
5955 reader,
5956 offset,
5957 CRITICAL_RECOVERY_LOCATOR_LEN,
5958 "CriticalRecoveryLocator",
5959 ) else {
5960 return;
5961 };
5962 let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
5963 return;
5964 };
5965 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
5966 return;
5967 }
5968 if let Ok(candidate) =
5969 parse_locator_cmra_authority_candidate_read_at(reader, offset, locator, master_key)
5970 {
5971 candidates.push(candidate);
5972 }
5973}
5974
5975fn collect_v41_public_locator_candidate(
5976 bytes: &[u8],
5977 offset: usize,
5978 expected_sequence: u32,
5979 volume_header: &VolumeHeader,
5980 crypto_header: &CryptoHeader<'_>,
5981 candidates: &mut Vec<PublicTerminalCandidate>,
5982) {
5983 let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
5984 return;
5985 };
5986 let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
5987 return;
5988 };
5989 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
5990 return;
5991 }
5992 if let Ok(candidate) =
5993 parse_public_locator_cmra_candidate(bytes, offset, locator, volume_header, crypto_header)
5994 {
5995 candidates.push(candidate);
5996 }
5997}
5998
5999fn choose_v41_terminal_candidate(
6000 mut candidates: Vec<TerminalCandidate>,
6001) -> Result<TerminalCandidate, FormatError> {
6002 candidates.sort_by_key(|candidate| candidate.anchor);
6003 let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6004 "no valid v41 CMRA candidate found",
6005 ))?;
6006 if let Some(previous) = candidates.last() {
6007 if previous.anchor == winner.anchor
6008 && (previous.cmra_offset != winner.cmra_offset
6009 || previous.cmra_length != winner.cmra_length)
6010 {
6011 return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
6012 }
6013 }
6014 Ok(winner)
6015}
6016
6017fn choose_v41_terminal_authority_candidate(
6018 mut candidates: Vec<TerminalAuthorityCandidate>,
6019) -> Result<TerminalAuthorityCandidate, FormatError> {
6020 candidates.sort_by_key(|candidate| candidate.anchor);
6021 let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6022 "no valid v41 CMRA candidate found",
6023 ))?;
6024 if let Some(previous) = candidates.last() {
6025 if previous.anchor == winner.anchor
6026 && (previous.cmra_offset != winner.cmra_offset
6027 || previous.cmra_length != winner.cmra_length)
6028 {
6029 return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
6030 }
6031 }
6032 Ok(winner)
6033}
6034
6035fn choose_v41_recipient_wrap_terminal_authority_candidate(
6036 mut candidates: Vec<RecipientWrapTerminalAuthorityCandidate>,
6037) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError> {
6038 candidates.sort_by_key(|candidate| candidate.anchor);
6039 let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6040 "no valid v41 CMRA candidate found",
6041 ))?;
6042 if let Some(previous) = candidates.last() {
6043 if previous.anchor == winner.anchor
6044 && (previous.cmra_offset != winner.cmra_offset
6045 || previous.cmra_length != winner.cmra_length)
6046 {
6047 return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
6048 }
6049 }
6050 Ok(winner)
6051}
6052
6053fn choose_v41_public_terminal_candidate(
6054 mut candidates: Vec<PublicTerminalCandidate>,
6055) -> Result<PublicTerminalCandidate, FormatError> {
6056 candidates.sort_by_key(|candidate| candidate.anchor);
6057 let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6058 "no valid v41 public CMRA candidate found",
6059 ))?;
6060 if let Some(previous) = candidates.last() {
6061 if previous.anchor == winner.anchor
6062 && (previous.cmra_offset != winner.cmra_offset
6063 || previous.cmra_length != winner.cmra_length)
6064 {
6065 return Err(FormatError::InvalidArchive(
6066 "ambiguous v41 public CMRA candidates",
6067 ));
6068 }
6069 }
6070 Ok(winner)
6071}
6072
6073fn parse_locator_cmra_candidate(
6074 bytes: &[u8],
6075 locator_offset: usize,
6076 locator: CriticalRecoveryLocator,
6077 context: KeyHoldingTerminalContext<'_>,
6078) -> Result<TerminalCandidate, FormatError> {
6079 let tuple = CmraDecoderTuple::from(locator);
6080 validate_cmra_decoder_tuple(tuple)?;
6081 let expected_cmra_length = cmra_serialized_length(tuple)?;
6082 if locator.cmra_length as u64 != expected_cmra_length {
6083 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6084 }
6085 validate_locator_position(locator_offset, locator)?;
6086 let recovered = recover_cmra(
6087 bytes,
6088 locator.cmra_offset,
6089 Some(tuple),
6090 CmraRecoveryMode::KeyHolding,
6091 )?;
6092 if recovered.tuple != tuple {
6093 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6094 }
6095 if expected_cmra_length != recovered.cmra_length {
6096 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6097 }
6098 validate_locator_image_boundary(locator, &recovered.image)?;
6099 validate_cmra_identity_hints(
6100 recovered.header_hints,
6101 Some(CmraIdentityHints::from(locator)),
6102 &recovered.image,
6103 )?;
6104 let terminal =
6105 validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context, false)?;
6106 Ok(TerminalCandidate {
6107 terminal,
6108 anchor: locator_offset
6109 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6110 .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6111 locator_sequence: Some(locator.locator_sequence),
6112 cmra_offset: locator.cmra_offset,
6113 cmra_length: recovered.cmra_length,
6114 })
6115}
6116
6117fn parse_locator_cmra_candidate_read_at(
6118 reader: &dyn ArchiveReadAt,
6119 locator_offset: u64,
6120 locator: CriticalRecoveryLocator,
6121 context: KeyHoldingTerminalContext<'_>,
6122) -> Result<TerminalCandidate, FormatError> {
6123 let tuple = CmraDecoderTuple::from(locator);
6124 validate_cmra_decoder_tuple(tuple)?;
6125 let expected_cmra_length = cmra_serialized_length(tuple)?;
6126 if locator.cmra_length as u64 != expected_cmra_length {
6127 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6128 }
6129 validate_locator_position(
6130 to_usize(locator_offset, "CriticalRecoveryLocator")?,
6131 locator,
6132 )?;
6133 let recovered = recover_cmra_read_at(
6134 reader,
6135 locator.cmra_offset,
6136 Some(tuple),
6137 CmraRecoveryMode::KeyHolding,
6138 )?;
6139 if recovered.tuple != tuple {
6140 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6141 }
6142 if expected_cmra_length != recovered.cmra_length {
6143 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6144 }
6145 validate_locator_image_boundary(locator, &recovered.image)?;
6146 validate_cmra_identity_hints(
6147 recovered.header_hints,
6148 Some(CmraIdentityHints::from(locator)),
6149 &recovered.image,
6150 )?;
6151 let terminal = validate_recovered_terminal_read_at(
6152 recovered.image,
6153 recovered.tuple,
6154 reader,
6155 context,
6156 false,
6157 )?;
6158 Ok(TerminalCandidate {
6159 terminal,
6160 anchor: to_usize(
6161 checked_u64_add(
6162 locator_offset,
6163 CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6164 "locator anchor overflow",
6165 )?,
6166 "locator anchor overflow",
6167 )?,
6168 locator_sequence: Some(locator.locator_sequence),
6169 cmra_offset: locator.cmra_offset,
6170 cmra_length: recovered.cmra_length,
6171 })
6172}
6173
6174fn parse_locator_cmra_authority_candidate(
6175 bytes: &[u8],
6176 locator_offset: usize,
6177 locator: CriticalRecoveryLocator,
6178 master_key: &MasterKey,
6179) -> Result<TerminalAuthorityCandidate, FormatError> {
6180 let tuple = CmraDecoderTuple::from(locator);
6181 validate_cmra_decoder_tuple(tuple)?;
6182 let expected_cmra_length = cmra_serialized_length(tuple)?;
6183 if locator.cmra_length as u64 != expected_cmra_length {
6184 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6185 }
6186 validate_locator_position(locator_offset, locator)?;
6187 let recovered = recover_cmra(
6188 bytes,
6189 locator.cmra_offset,
6190 Some(tuple),
6191 CmraRecoveryMode::KeyHolding,
6192 )?;
6193 if recovered.tuple != tuple {
6194 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6195 }
6196 if expected_cmra_length != recovered.cmra_length {
6197 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6198 }
6199 validate_locator_image_boundary(locator, &recovered.image)?;
6200 validate_cmra_identity_hints(
6201 recovered.header_hints,
6202 Some(CmraIdentityHints::from(locator)),
6203 &recovered.image,
6204 )?;
6205 let cmra_length = recovered.cmra_length;
6206 let authority =
6207 validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, false)?;
6208 Ok(TerminalAuthorityCandidate {
6209 authority,
6210 anchor: locator_offset
6211 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6212 .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6213 cmra_offset: locator.cmra_offset,
6214 cmra_length,
6215 })
6216}
6217
6218fn parse_locator_cmra_recipient_wrap_authority_candidate<F>(
6219 bytes: &[u8],
6220 locator_offset: usize,
6221 locator: CriticalRecoveryLocator,
6222 resolver: &mut F,
6223) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6224where
6225 F: FnMut(
6226 RecipientWrapRecordContext<'_>,
6227 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6228{
6229 let tuple = CmraDecoderTuple::from(locator);
6230 validate_cmra_decoder_tuple(tuple)?;
6231 let expected_cmra_length = cmra_serialized_length(tuple)?;
6232 if locator.cmra_length as u64 != expected_cmra_length {
6233 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6234 }
6235 validate_locator_position(locator_offset, locator)?;
6236 let recovered = recover_cmra(
6237 bytes,
6238 locator.cmra_offset,
6239 Some(tuple),
6240 CmraRecoveryMode::KeyHolding,
6241 )?;
6242 if recovered.tuple != tuple {
6243 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6244 }
6245 if expected_cmra_length != recovered.cmra_length {
6246 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6247 }
6248 validate_locator_image_boundary(locator, &recovered.image)?;
6249 validate_cmra_identity_hints(
6250 recovered.header_hints,
6251 Some(CmraIdentityHints::from(locator)),
6252 &recovered.image,
6253 )?;
6254 let cmra_length = recovered.cmra_length;
6255 let authority = validate_recovered_recipient_wrap_terminal_authority(
6256 recovered.image,
6257 recovered.tuple,
6258 resolver,
6259 false,
6260 )?;
6261 Ok(RecipientWrapTerminalAuthorityCandidate {
6262 authority,
6263 anchor: locator_offset
6264 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6265 .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6266 cmra_offset: locator.cmra_offset,
6267 cmra_length,
6268 })
6269}
6270
6271fn parse_locator_cmra_recipient_wrap_authority_candidate_read_at<F>(
6272 reader: &dyn ArchiveReadAt,
6273 locator_offset: u64,
6274 locator: CriticalRecoveryLocator,
6275 resolver: &mut F,
6276) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6277where
6278 F: FnMut(
6279 RecipientWrapRecordContext<'_>,
6280 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6281{
6282 let tuple = CmraDecoderTuple::from(locator);
6283 validate_cmra_decoder_tuple(tuple)?;
6284 let expected_cmra_length = cmra_serialized_length(tuple)?;
6285 if locator.cmra_length as u64 != expected_cmra_length {
6286 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6287 }
6288 validate_locator_position(
6289 to_usize(locator_offset, "CriticalRecoveryLocator")?,
6290 locator,
6291 )?;
6292 let recovered = recover_cmra_read_at(
6293 reader,
6294 locator.cmra_offset,
6295 Some(tuple),
6296 CmraRecoveryMode::KeyHolding,
6297 )?;
6298 if recovered.tuple != tuple {
6299 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6300 }
6301 if expected_cmra_length != recovered.cmra_length {
6302 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6303 }
6304 validate_locator_image_boundary(locator, &recovered.image)?;
6305 validate_cmra_identity_hints(
6306 recovered.header_hints,
6307 Some(CmraIdentityHints::from(locator)),
6308 &recovered.image,
6309 )?;
6310 let cmra_length = recovered.cmra_length;
6311 let authority = validate_recovered_recipient_wrap_terminal_authority(
6312 recovered.image,
6313 recovered.tuple,
6314 resolver,
6315 false,
6316 )?;
6317 Ok(RecipientWrapTerminalAuthorityCandidate {
6318 authority,
6319 anchor: to_usize(
6320 checked_u64_add(
6321 locator_offset,
6322 CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6323 "locator anchor overflow",
6324 )?,
6325 "locator anchor overflow",
6326 )?,
6327 cmra_offset: locator.cmra_offset,
6328 cmra_length,
6329 })
6330}
6331
6332fn parse_locator_cmra_authority_candidate_read_at(
6333 reader: &dyn ArchiveReadAt,
6334 locator_offset: u64,
6335 locator: CriticalRecoveryLocator,
6336 master_key: &MasterKey,
6337) -> Result<TerminalAuthorityCandidate, FormatError> {
6338 let tuple = CmraDecoderTuple::from(locator);
6339 validate_cmra_decoder_tuple(tuple)?;
6340 let expected_cmra_length = cmra_serialized_length(tuple)?;
6341 if locator.cmra_length as u64 != expected_cmra_length {
6342 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6343 }
6344 validate_locator_position(
6345 to_usize(locator_offset, "CriticalRecoveryLocator")?,
6346 locator,
6347 )?;
6348 let recovered = recover_cmra_read_at(
6349 reader,
6350 locator.cmra_offset,
6351 Some(tuple),
6352 CmraRecoveryMode::KeyHolding,
6353 )?;
6354 if recovered.tuple != tuple {
6355 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6356 }
6357 if expected_cmra_length != recovered.cmra_length {
6358 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6359 }
6360 validate_locator_image_boundary(locator, &recovered.image)?;
6361 validate_cmra_identity_hints(
6362 recovered.header_hints,
6363 Some(CmraIdentityHints::from(locator)),
6364 &recovered.image,
6365 )?;
6366 let cmra_length = recovered.cmra_length;
6367 let authority =
6368 validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, false)?;
6369 Ok(TerminalAuthorityCandidate {
6370 authority,
6371 anchor: to_usize(
6372 checked_u64_add(
6373 locator_offset,
6374 CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6375 "locator anchor overflow",
6376 )?,
6377 "locator anchor overflow",
6378 )?,
6379 cmra_offset: locator.cmra_offset,
6380 cmra_length,
6381 })
6382}
6383
6384fn parse_public_locator_cmra_candidate(
6385 bytes: &[u8],
6386 locator_offset: usize,
6387 locator: CriticalRecoveryLocator,
6388 volume_header: &VolumeHeader,
6389 crypto_header: &CryptoHeader<'_>,
6390) -> Result<PublicTerminalCandidate, FormatError> {
6391 let tuple = CmraDecoderTuple::from(locator);
6392 validate_cmra_decoder_tuple(tuple)?;
6393 let expected_cmra_length = cmra_serialized_length(tuple)?;
6394 if locator.cmra_length as u64 != expected_cmra_length {
6395 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6396 }
6397 validate_locator_position(locator_offset, locator)?;
6398 let recovered = recover_cmra(
6399 bytes,
6400 locator.cmra_offset,
6401 Some(tuple),
6402 CmraRecoveryMode::PublicNoKey,
6403 )?;
6404 if recovered.tuple != tuple {
6405 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6406 }
6407 if expected_cmra_length != recovered.cmra_length {
6408 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6409 }
6410 validate_locator_image_boundary(locator, &recovered.image)?;
6411 validate_cmra_identity_hints(
6412 recovered.header_hints,
6413 Some(CmraIdentityHints::from(locator)),
6414 &recovered.image,
6415 )?;
6416 let terminal = validate_recovered_public_terminal(
6417 recovered.image,
6418 bytes,
6419 volume_header,
6420 crypto_header,
6421 false,
6422 )?;
6423 Ok(PublicTerminalCandidate {
6424 terminal,
6425 anchor: locator_offset
6426 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6427 .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6428 cmra_offset: locator.cmra_offset,
6429 cmra_length: recovered.cmra_length,
6430 })
6431}
6432
6433fn parse_locatorless_cmra_candidate(
6434 bytes: &[u8],
6435 cmra_offset: usize,
6436 context: KeyHoldingTerminalContext<'_>,
6437) -> Result<TerminalCandidate, FormatError> {
6438 let recovered = recover_cmra(
6439 bytes,
6440 cmra_offset as u64,
6441 None,
6442 CmraRecoveryMode::KeyHolding,
6443 )?;
6444 if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
6445 return Err(FormatError::InvalidArchive(
6446 "locatorless CMRA boundary mismatch",
6447 ));
6448 }
6449 if recovered
6450 .image
6451 .volume_trailer_offset
6452 .checked_add(VOLUME_TRAILER_LEN as u64)
6453 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6454 != cmra_offset as u64
6455 {
6456 return Err(FormatError::InvalidArchive(
6457 "locatorless trailer boundary mismatch",
6458 ));
6459 }
6460 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6461 let terminal =
6462 validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context, true)?;
6463 Ok(TerminalCandidate {
6464 terminal,
6465 anchor: cmra_offset
6466 .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
6467 .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
6468 locator_sequence: None,
6469 cmra_offset: cmra_offset as u64,
6470 cmra_length: recovered.cmra_length,
6471 })
6472}
6473
6474fn parse_locatorless_cmra_candidate_read_at(
6475 reader: &dyn ArchiveReadAt,
6476 cmra_offset: u64,
6477 context: KeyHoldingTerminalContext<'_>,
6478) -> Result<TerminalCandidate, FormatError> {
6479 let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
6480 if recovered.image.body_bytes_before_cmra != cmra_offset {
6481 return Err(FormatError::InvalidArchive(
6482 "locatorless CMRA boundary mismatch",
6483 ));
6484 }
6485 if recovered
6486 .image
6487 .volume_trailer_offset
6488 .checked_add(VOLUME_TRAILER_LEN as u64)
6489 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6490 != cmra_offset
6491 {
6492 return Err(FormatError::InvalidArchive(
6493 "locatorless trailer boundary mismatch",
6494 ));
6495 }
6496 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6497 let terminal = validate_recovered_terminal_read_at(
6498 recovered.image,
6499 recovered.tuple,
6500 reader,
6501 context,
6502 true,
6503 )?;
6504 Ok(TerminalCandidate {
6505 terminal,
6506 anchor: to_usize(
6507 checked_u64_add(cmra_offset, recovered.cmra_length, "CMRA anchor overflow")?,
6508 "CMRA anchor overflow",
6509 )?,
6510 locator_sequence: None,
6511 cmra_offset,
6512 cmra_length: recovered.cmra_length,
6513 })
6514}
6515
6516fn parse_locatorless_cmra_authority_candidate(
6517 bytes: &[u8],
6518 cmra_offset: usize,
6519 master_key: &MasterKey,
6520) -> Result<TerminalAuthorityCandidate, FormatError> {
6521 let recovered = recover_cmra(
6522 bytes,
6523 cmra_offset as u64,
6524 None,
6525 CmraRecoveryMode::KeyHolding,
6526 )?;
6527 if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
6528 return Err(FormatError::InvalidArchive(
6529 "locatorless CMRA boundary mismatch",
6530 ));
6531 }
6532 if recovered
6533 .image
6534 .volume_trailer_offset
6535 .checked_add(VOLUME_TRAILER_LEN as u64)
6536 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6537 != cmra_offset as u64
6538 {
6539 return Err(FormatError::InvalidArchive(
6540 "locatorless trailer boundary mismatch",
6541 ));
6542 }
6543 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6544 let cmra_length = recovered.cmra_length;
6545 let authority =
6546 validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, true)?;
6547 Ok(TerminalAuthorityCandidate {
6548 authority,
6549 anchor: cmra_offset
6550 .checked_add(to_usize(cmra_length, "CMRA")?)
6551 .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
6552 cmra_offset: cmra_offset as u64,
6553 cmra_length,
6554 })
6555}
6556
6557fn parse_locatorless_cmra_recipient_wrap_authority_candidate<F>(
6558 bytes: &[u8],
6559 cmra_offset: usize,
6560 resolver: &mut F,
6561) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6562where
6563 F: FnMut(
6564 RecipientWrapRecordContext<'_>,
6565 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6566{
6567 let recovered = recover_cmra(
6568 bytes,
6569 cmra_offset as u64,
6570 None,
6571 CmraRecoveryMode::KeyHolding,
6572 )?;
6573 if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
6574 return Err(FormatError::InvalidArchive(
6575 "locatorless CMRA boundary mismatch",
6576 ));
6577 }
6578 if recovered
6579 .image
6580 .volume_trailer_offset
6581 .checked_add(VOLUME_TRAILER_LEN as u64)
6582 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6583 != cmra_offset as u64
6584 {
6585 return Err(FormatError::InvalidArchive(
6586 "locatorless trailer boundary mismatch",
6587 ));
6588 }
6589 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6590 let cmra_length = recovered.cmra_length;
6591 let authority = validate_recovered_recipient_wrap_terminal_authority(
6592 recovered.image,
6593 recovered.tuple,
6594 resolver,
6595 true,
6596 )?;
6597 Ok(RecipientWrapTerminalAuthorityCandidate {
6598 authority,
6599 anchor: cmra_offset
6600 .checked_add(to_usize(cmra_length, "CMRA")?)
6601 .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
6602 cmra_offset: cmra_offset as u64,
6603 cmra_length,
6604 })
6605}
6606
6607fn parse_locatorless_cmra_recipient_wrap_authority_candidate_read_at<F>(
6608 reader: &dyn ArchiveReadAt,
6609 cmra_offset: u64,
6610 resolver: &mut F,
6611) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6612where
6613 F: FnMut(
6614 RecipientWrapRecordContext<'_>,
6615 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6616{
6617 let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
6618 if recovered.image.body_bytes_before_cmra != cmra_offset {
6619 return Err(FormatError::InvalidArchive(
6620 "locatorless CMRA boundary mismatch",
6621 ));
6622 }
6623 if recovered
6624 .image
6625 .volume_trailer_offset
6626 .checked_add(VOLUME_TRAILER_LEN as u64)
6627 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6628 != cmra_offset
6629 {
6630 return Err(FormatError::InvalidArchive(
6631 "locatorless trailer boundary mismatch",
6632 ));
6633 }
6634 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6635 let cmra_length = recovered.cmra_length;
6636 let authority = validate_recovered_recipient_wrap_terminal_authority(
6637 recovered.image,
6638 recovered.tuple,
6639 resolver,
6640 true,
6641 )?;
6642 Ok(RecipientWrapTerminalAuthorityCandidate {
6643 authority,
6644 anchor: to_usize(
6645 checked_u64_add(cmra_offset, cmra_length, "CMRA anchor overflow")?,
6646 "CMRA anchor overflow",
6647 )?,
6648 cmra_offset,
6649 cmra_length,
6650 })
6651}
6652
6653fn parse_locatorless_cmra_authority_candidate_read_at(
6654 reader: &dyn ArchiveReadAt,
6655 cmra_offset: u64,
6656 master_key: &MasterKey,
6657) -> Result<TerminalAuthorityCandidate, FormatError> {
6658 let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
6659 if recovered.image.body_bytes_before_cmra != cmra_offset {
6660 return Err(FormatError::InvalidArchive(
6661 "locatorless CMRA boundary mismatch",
6662 ));
6663 }
6664 if recovered
6665 .image
6666 .volume_trailer_offset
6667 .checked_add(VOLUME_TRAILER_LEN as u64)
6668 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6669 != cmra_offset
6670 {
6671 return Err(FormatError::InvalidArchive(
6672 "locatorless trailer boundary mismatch",
6673 ));
6674 }
6675 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6676 let cmra_length = recovered.cmra_length;
6677 let authority =
6678 validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, true)?;
6679 Ok(TerminalAuthorityCandidate {
6680 authority,
6681 anchor: to_usize(
6682 checked_u64_add(cmra_offset, cmra_length, "CMRA anchor overflow")?,
6683 "CMRA anchor overflow",
6684 )?,
6685 cmra_offset,
6686 cmra_length,
6687 })
6688}
6689
6690fn parse_public_locatorless_cmra_candidate(
6691 bytes: &[u8],
6692 cmra_offset: usize,
6693 volume_header: &VolumeHeader,
6694 crypto_header: &CryptoHeader<'_>,
6695) -> Result<PublicTerminalCandidate, FormatError> {
6696 let recovered = recover_cmra(
6697 bytes,
6698 cmra_offset as u64,
6699 None,
6700 CmraRecoveryMode::PublicNoKey,
6701 )?;
6702 if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
6703 return Err(FormatError::InvalidArchive(
6704 "locatorless CMRA boundary mismatch",
6705 ));
6706 }
6707 if recovered
6708 .image
6709 .volume_trailer_offset
6710 .checked_add(VOLUME_TRAILER_LEN as u64)
6711 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6712 != cmra_offset as u64
6713 {
6714 return Err(FormatError::InvalidArchive(
6715 "locatorless trailer boundary mismatch",
6716 ));
6717 }
6718 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6719 let terminal = validate_recovered_public_terminal(
6720 recovered.image,
6721 bytes,
6722 volume_header,
6723 crypto_header,
6724 true,
6725 )?;
6726 Ok(PublicTerminalCandidate {
6727 terminal,
6728 anchor: cmra_offset
6729 .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
6730 .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
6731 cmra_offset: cmra_offset as u64,
6732 cmra_length: recovered.cmra_length,
6733 })
6734}
6735
6736fn validate_locator_position(
6737 locator_offset: usize,
6738 locator: CriticalRecoveryLocator,
6739) -> Result<(), FormatError> {
6740 if locator.cmra_offset != locator.body_bytes_before_cmra {
6741 return Err(FormatError::InvalidArchive(
6742 "locator CMRA boundary mismatch",
6743 ));
6744 }
6745 if locator
6746 .volume_trailer_offset
6747 .checked_add(VOLUME_TRAILER_LEN as u64)
6748 .ok_or(FormatError::InvalidArchive("locator trailer overflow"))?
6749 != locator.cmra_offset
6750 {
6751 return Err(FormatError::InvalidArchive(
6752 "locator trailer boundary mismatch",
6753 ));
6754 }
6755 let expected_offset = match locator.locator_sequence {
6756 1 => locator.cmra_offset.checked_add(locator.cmra_length as u64),
6757 0 => locator
6758 .cmra_offset
6759 .checked_add(locator.cmra_length as u64)
6760 .and_then(|value| value.checked_add(CRITICAL_RECOVERY_LOCATOR_LEN as u64)),
6761 _ => None,
6762 }
6763 .ok_or(FormatError::InvalidArchive("locator position overflow"))?;
6764 if expected_offset != locator_offset as u64 {
6765 return Err(FormatError::InvalidArchive(
6766 "locator position does not match sequence",
6767 ));
6768 }
6769 Ok(())
6770}
6771
6772fn validate_locator_image_boundary(
6773 locator: CriticalRecoveryLocator,
6774 image: &CriticalMetadataImage,
6775) -> Result<(), FormatError> {
6776 if locator.volume_format_rev != image.volume_format_rev
6777 || locator.volume_trailer_offset != image.volume_trailer_offset
6778 || locator.body_bytes_before_cmra != image.body_bytes_before_cmra
6779 || image
6780 .volume_trailer_offset
6781 .checked_add(VOLUME_TRAILER_LEN as u64)
6782 .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
6783 != locator.cmra_offset
6784 {
6785 return Err(FormatError::InvalidArchive(
6786 "locator and CMRA image boundaries differ",
6787 ));
6788 }
6789 Ok(())
6790}
6791
6792fn validate_cmra_identity_hints(
6793 header_hints: Option<CmraIdentityHints>,
6794 locator_hints: Option<CmraIdentityHints>,
6795 image: &CriticalMetadataImage,
6796) -> Result<(), FormatError> {
6797 if let (Some(header), Some(locator)) = (header_hints, locator_hints) {
6798 if header != locator {
6799 return Err(FormatError::InvalidArchive(
6800 "CMRA header and locator identity hints differ",
6801 ));
6802 }
6803 }
6804 for hints in [header_hints, locator_hints].into_iter().flatten() {
6805 if hints.archive_uuid != image.archive_uuid
6806 || hints.session_id != image.session_id
6807 || hints.volume_index != image.volume_index
6808 {
6809 return Err(FormatError::InvalidArchive(
6810 "CMRA identity hints do not match recovered image",
6811 ));
6812 }
6813 }
6814 Ok(())
6815}
6816
6817fn recover_cmra(
6818 bytes: &[u8],
6819 cmra_offset: u64,
6820 locator_tuple: Option<CmraDecoderTuple>,
6821 mode: CmraRecoveryMode,
6822) -> Result<RecoveredCmra, FormatError> {
6823 let offset = to_usize(cmra_offset, "CMRA")?;
6824 let header_bytes = slice(
6825 bytes,
6826 offset,
6827 CRITICAL_METADATA_RECOVERY_HEADER_LEN,
6828 "CriticalMetadataRecoveryHeader",
6829 )?;
6830 let (tuple, header_hints) = recover_cmra_header_tuple(header_bytes, locator_tuple)?;
6831 validate_cmra_decoder_tuple(tuple)?;
6832 let cmra_length = cmra_serialized_length(tuple)?;
6833 let cmra_len = to_usize(cmra_length, "CMRA")?;
6834 let cmra_bytes = slice(bytes, offset, cmra_len, "CMRA")?;
6835 recover_cmra_from_bytes(cmra_bytes, tuple, header_hints, cmra_length, mode)
6836}
6837
6838fn recover_cmra_read_at(
6839 reader: &dyn ArchiveReadAt,
6840 cmra_offset: u64,
6841 locator_tuple: Option<CmraDecoderTuple>,
6842 mode: CmraRecoveryMode,
6843) -> Result<RecoveredCmra, FormatError> {
6844 let header_bytes = read_at_vec(
6845 reader,
6846 cmra_offset,
6847 CRITICAL_METADATA_RECOVERY_HEADER_LEN,
6848 "CriticalMetadataRecoveryHeader",
6849 )?;
6850 let (tuple, header_hints) = recover_cmra_header_tuple(&header_bytes, locator_tuple)?;
6851 validate_cmra_decoder_tuple(tuple)?;
6852 let cmra_length = cmra_serialized_length(tuple)?;
6853 let cmra_bytes = read_at_vec(reader, cmra_offset, to_usize(cmra_length, "CMRA")?, "CMRA")?;
6854 recover_cmra_from_bytes(&cmra_bytes, tuple, header_hints, cmra_length, mode)
6855}
6856
6857fn recover_cmra_header_tuple(
6858 header_bytes: &[u8],
6859 locator_tuple: Option<CmraDecoderTuple>,
6860) -> Result<(CmraDecoderTuple, Option<CmraIdentityHints>), FormatError> {
6861 let parsed_header = CriticalMetadataRecoveryHeader::parse(header_bytes);
6862 Ok(match (parsed_header, locator_tuple) {
6863 (Ok(header), Some(locator_tuple)) => {
6864 let header_tuple = CmraDecoderTuple::from(header);
6865 if header_tuple != locator_tuple {
6866 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6867 }
6868 (locator_tuple, Some(CmraIdentityHints::from(header)))
6869 }
6870 (Ok(header), None) => (
6871 CmraDecoderTuple::from(header),
6872 Some(CmraIdentityHints::from(header)),
6873 ),
6874 (Err(_), Some(tuple)) => (tuple, None),
6875 (Err(err), _) => return Err(err),
6876 })
6877}
6878
6879fn recover_cmra_from_bytes(
6880 cmra_bytes: &[u8],
6881 tuple: CmraDecoderTuple,
6882 header_hints: Option<CmraIdentityHints>,
6883 cmra_length: u64,
6884 mode: CmraRecoveryMode,
6885) -> Result<RecoveredCmra, FormatError> {
6886 let shard_size = tuple.shard_size as usize;
6887 let mut data_shards = vec![None; tuple.data_shard_count as usize];
6888 let mut parity_shards = vec![None; tuple.parity_shard_count as usize];
6889 let mut cursor = CRITICAL_METADATA_RECOVERY_HEADER_LEN;
6890 for idx in 0..(tuple.data_shard_count as usize + tuple.parity_shard_count as usize) {
6891 let raw = slice(
6892 cmra_bytes,
6893 cursor,
6894 CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
6895 "CriticalMetadataRecoveryShard",
6896 )?;
6897 let shard = CriticalMetadataRecoveryShard::parse(raw, shard_size).ok();
6898 if let Some(shard) = shard {
6899 validate_cmra_shard(&shard, idx, tuple)?;
6900 if shard.shard_role == 0 {
6901 let data_slot = data_shards
6902 .get_mut(idx)
6903 .ok_or(FormatError::InvalidArchive("CMRA data shard out of range"))?;
6904 *data_slot = Some(shard.payload);
6905 } else {
6906 let parity_idx = idx - tuple.data_shard_count as usize;
6907 let parity_slot =
6908 parity_shards
6909 .get_mut(parity_idx)
6910 .ok_or(FormatError::InvalidArchive(
6911 "CMRA parity shard out of range",
6912 ))?;
6913 *parity_slot = Some(shard.payload);
6914 }
6915 }
6916 cursor = checked_add(
6917 cursor,
6918 CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
6919 "CriticalMetadataRecoveryShard",
6920 )?;
6921 }
6922 let repaired = repair_data_gf16(&data_shards, &parity_shards, shard_size)?;
6923 let mut image_bytes = Vec::with_capacity(tuple.image_length as usize);
6924 for shard in repaired {
6925 image_bytes.extend_from_slice(&shard);
6926 }
6927 image_bytes.truncate(tuple.image_length as usize);
6928 if sha256_bytes(&image_bytes) != tuple.image_sha256 {
6929 return Err(FormatError::InvalidArchive("CMRA image SHA-256 mismatch"));
6930 }
6931 let image = CriticalMetadataImage::parse(&image_bytes)?;
6932 validate_critical_metadata_image(&image, mode)?;
6933 Ok(RecoveredCmra {
6934 image,
6935 tuple,
6936 header_hints,
6937 cmra_length,
6938 })
6939}
6940
6941fn validate_cmra_decoder_tuple(tuple: CmraDecoderTuple) -> Result<(), FormatError> {
6942 let shard_size = tuple.shard_size as u64;
6943 if !(512..=4096).contains(&shard_size) || shard_size % 2 != 0 {
6944 return Err(FormatError::InvalidArchive("CMRA shard_size is invalid"));
6945 }
6946 let image_length = tuple.image_length as u64;
6947 let min = critical_image_min();
6948 let cap = critical_image_cap()?;
6949 if image_length < min || image_length > cap {
6950 return Err(FormatError::InvalidArchive(
6951 "CMRA image_length is outside bounds",
6952 ));
6953 }
6954 let expected_data_shards = ceil_div_u64(image_length, shard_size)?;
6955 if expected_data_shards == 0 || expected_data_shards != tuple.data_shard_count as u64 {
6956 return Err(FormatError::InvalidArchive(
6957 "CMRA data_shard_count does not match image length",
6958 ));
6959 }
6960 let max_parity = 2u64.max(ceil_div_u64(
6961 checked_u64_mul(
6962 expected_data_shards,
6963 READER_MAX_CMRA_PARITY_PCT as u64,
6964 "CMRA parity overflow",
6965 )?,
6966 100,
6967 )?);
6968 if tuple.parity_shard_count as u64 > max_parity {
6969 return Err(FormatError::ReaderResourceLimitExceeded {
6970 field: "CMRA parity shard count",
6971 cap: max_parity,
6972 actual: tuple.parity_shard_count as u64,
6973 });
6974 }
6975 let total = expected_data_shards
6976 .checked_add(tuple.parity_shard_count as u64)
6977 .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
6978 if total > 65_535 {
6979 return Err(FormatError::FecTooManyShards(total as usize));
6980 }
6981 Ok(())
6982}
6983
6984fn validate_cmra_writer_parity_lower_bound(
6985 tuple: CmraDecoderTuple,
6986 bit_rot_buffer_pct: u8,
6987) -> Result<(), FormatError> {
6988 let min_parity = 2u64.max(ceil_div_u64(
6989 checked_u64_mul(
6990 tuple.data_shard_count as u64,
6991 bit_rot_buffer_pct as u64,
6992 "CMRA parity lower-bound overflow",
6993 )?,
6994 100,
6995 )?);
6996 if (tuple.parity_shard_count as u64) < min_parity {
6997 return Err(FormatError::InvalidArchive(
6998 "CMRA parity shard count is below authenticated bit-rot lower bound",
6999 ));
7000 }
7001 Ok(())
7002}
7003
7004fn validate_cmra_shard(
7005 shard: &CriticalMetadataRecoveryShard,
7006 serialized_idx: usize,
7007 tuple: CmraDecoderTuple,
7008) -> Result<(), FormatError> {
7009 if shard.shard_index as usize != serialized_idx {
7010 return Err(FormatError::InvalidArchive(
7011 "CMRA shards are not in canonical order",
7012 ));
7013 }
7014 let data_count = tuple.data_shard_count as usize;
7015 let shard_size = tuple.shard_size as usize;
7016 if serialized_idx < data_count {
7017 if shard.shard_role != 0 {
7018 return Err(FormatError::InvalidArchive(
7019 "CMRA data shard has wrong role",
7020 ));
7021 }
7022 let expected_len = if serialized_idx + 1 == data_count {
7023 let used = tuple.image_length as usize - serialized_idx * shard_size;
7024 if used == 0 {
7025 shard_size
7026 } else {
7027 used
7028 }
7029 } else {
7030 shard_size
7031 };
7032 if shard.shard_payload_length as usize != expected_len {
7033 return Err(FormatError::InvalidArchive(
7034 "CMRA data shard payload length is non-canonical",
7035 ));
7036 }
7037 if serialized_idx + 1 == data_count
7038 && shard.payload[expected_len..].iter().any(|byte| *byte != 0)
7039 {
7040 return Err(FormatError::InvalidArchive(
7041 "CMRA final data shard padding is non-zero",
7042 ));
7043 }
7044 } else {
7045 if shard.shard_role != 1 {
7046 return Err(FormatError::InvalidArchive(
7047 "CMRA parity shard has wrong role",
7048 ));
7049 }
7050 if shard.shard_payload_length as usize != shard_size {
7051 return Err(FormatError::InvalidArchive(
7052 "CMRA parity shard payload length is non-canonical",
7053 ));
7054 }
7055 }
7056 Ok(())
7057}
7058
7059fn validate_critical_metadata_image(
7060 image: &CriticalMetadataImage,
7061 mode: CmraRecoveryMode,
7062) -> Result<(), FormatError> {
7063 let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
7064 let key_wrap_layout_present = image.layout_flags & 0x0000_0002 != 0;
7065 let key_wrap_region = image.region(6);
7066 let key_wrap_present = if image.volume_format_rev == VOLUME_FORMAT_REV_44 {
7067 if key_wrap_layout_present != key_wrap_region.is_some() {
7068 return Err(FormatError::InvalidArchive(
7069 "CriticalMetadataImage key-wrap layout flag mismatch",
7070 ));
7071 }
7072 key_wrap_layout_present
7073 } else {
7074 if key_wrap_layout_present
7075 || key_wrap_region.is_some()
7076 || image.key_wrap_table_offset != 0
7077 || image.key_wrap_table_length != 0
7078 || image.key_wrap_table_sha256 != [0u8; 32]
7079 {
7080 return Err(FormatError::InvalidArchive(
7081 "v43 CriticalMetadataImage cannot contain KeyWrapTableV1",
7082 ));
7083 }
7084 false
7085 };
7086 if image.volume_header_offset != 0
7087 || image.volume_header_length != VOLUME_HEADER_LEN as u32
7088 || image.crypto_header_offset != VOLUME_HEADER_LEN as u64
7089 || image.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
7090 || image.volume_trailer_length != VOLUME_TRAILER_LEN as u32
7091 || image.body_bytes_before_cmra
7092 != image
7093 .volume_trailer_offset
7094 .checked_add(VOLUME_TRAILER_LEN as u64)
7095 .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
7096 {
7097 return Err(FormatError::InvalidArchive(
7098 "CriticalMetadataImage fixed layout is invalid",
7099 ));
7100 }
7101 if root_auth_present {
7102 if image.root_auth_footer_offset == 0
7103 || image.root_auth_footer_length == 0
7104 || image.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
7105 {
7106 return Err(FormatError::InvalidArchive(
7107 "CriticalMetadataImage root-auth range is invalid",
7108 ));
7109 }
7110 } else if image.root_auth_footer_offset != 0
7111 || image.root_auth_footer_length != 0
7112 || image.root_auth_footer_sha256 != [0u8; 32]
7113 {
7114 return Err(FormatError::InvalidArchive(
7115 "CriticalMetadataImage root-auth fields must be zero when absent",
7116 ));
7117 }
7118 let block_record_len = image_block_record_len_from_region(image)?;
7119 let block_record_len_u64 = u64::try_from(block_record_len)
7120 .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?;
7121 match mode {
7122 CmraRecoveryMode::KeyHolding => {
7123 let expected_len = image.block_count.checked_mul(block_record_len_u64).ok_or(
7124 FormatError::InvalidArchive("BlockRecord region length overflow"),
7125 )?;
7126 if image.block_records_length != expected_len {
7127 return Err(FormatError::InvalidArchive(
7128 "CriticalMetadataImage terminal equations are invalid",
7129 ));
7130 }
7131 }
7132 CmraRecoveryMode::PublicNoKey => {
7133 if image.block_records_length % block_record_len_u64 != 0 {
7134 return Err(FormatError::InvalidArchive(
7135 "CriticalMetadataImage BlockRecord region is not aligned",
7136 ));
7137 }
7138 }
7139 }
7140 let crypto_header_end = image
7141 .crypto_header_offset
7142 .checked_add(image.crypto_header_length as u64)
7143 .ok_or(FormatError::InvalidArchive(
7144 "CryptoHeader boundary overflow",
7145 ))?;
7146 let expected_block_records_offset = if key_wrap_present {
7147 let key_wrap_region = key_wrap_region.ok_or(FormatError::InvalidArchive(
7148 "missing CriticalMetadataImage key-wrap region",
7149 ))?;
7150 if image.key_wrap_table_offset != crypto_header_end
7151 || image.key_wrap_table_length == 0
7152 || key_wrap_region.offset != image.key_wrap_table_offset
7153 || key_wrap_region.bytes.len() != image.key_wrap_table_length as usize
7154 {
7155 return Err(FormatError::InvalidArchive(
7156 "CriticalMetadataImage key-wrap region is malformed",
7157 ));
7158 }
7159 image
7160 .key_wrap_table_offset
7161 .checked_add(image.key_wrap_table_length as u64)
7162 .ok_or(FormatError::InvalidArchive(
7163 "KeyWrapTableV1 boundary overflow",
7164 ))?
7165 } else {
7166 if image.key_wrap_table_offset != 0
7167 || image.key_wrap_table_length != 0
7168 || image.key_wrap_table_sha256 != [0u8; 32]
7169 {
7170 return Err(FormatError::InvalidArchive(
7171 "CriticalMetadataImage key-wrap fields must be zero when absent",
7172 ));
7173 }
7174 crypto_header_end
7175 };
7176 if image.block_records_offset != expected_block_records_offset
7177 || image.manifest_footer_offset
7178 != image
7179 .block_records_offset
7180 .checked_add(image.block_records_length)
7181 .ok_or(FormatError::InvalidArchive(
7182 "ManifestFooter boundary overflow",
7183 ))?
7184 {
7185 return Err(FormatError::InvalidArchive(
7186 "CriticalMetadataImage terminal equations are invalid",
7187 ));
7188 }
7189 let manifest_end = image
7190 .manifest_footer_offset
7191 .checked_add(MANIFEST_FOOTER_LEN as u64)
7192 .ok_or(FormatError::InvalidArchive(
7193 "RootAuthFooter boundary overflow",
7194 ))?;
7195 if root_auth_present {
7196 if image.root_auth_footer_offset != manifest_end
7197 || image
7198 .root_auth_footer_offset
7199 .checked_add(image.root_auth_footer_length as u64)
7200 .ok_or(FormatError::InvalidArchive(
7201 "VolumeTrailer boundary overflow",
7202 ))?
7203 != image.volume_trailer_offset
7204 {
7205 return Err(FormatError::InvalidArchive(
7206 "CriticalMetadataImage root-auth terminal equations are invalid",
7207 ));
7208 }
7209 } else if image.volume_trailer_offset != manifest_end {
7210 return Err(FormatError::InvalidArchive(
7211 "CriticalMetadataImage unsigned terminal equations are invalid",
7212 ));
7213 }
7214 let expected_types: &[u16] = match (key_wrap_present, root_auth_present) {
7215 (false, false) => &[1, 2, 3, 5],
7216 (false, true) => &[1, 2, 3, 4, 5],
7217 (true, false) => &[1, 2, 6, 3, 5],
7218 (true, true) => &[1, 2, 6, 3, 4, 5],
7219 };
7220 if image.regions.len() != expected_types.len()
7221 || image
7222 .regions
7223 .iter()
7224 .map(|region| region.region_type)
7225 .ne(expected_types.iter().copied())
7226 {
7227 return Err(FormatError::InvalidArchive(
7228 "CriticalMetadataImage regions are not canonical",
7229 ));
7230 }
7231 validate_image_region(
7232 image,
7233 1,
7234 image.volume_header_offset,
7235 image.volume_header_length,
7236 )?;
7237 validate_image_region(
7238 image,
7239 2,
7240 image.crypto_header_offset,
7241 image.crypto_header_length,
7242 )?;
7243 validate_image_region(
7244 image,
7245 3,
7246 image.manifest_footer_offset,
7247 image.manifest_footer_length,
7248 )?;
7249 if key_wrap_present {
7250 validate_image_region(
7251 image,
7252 6,
7253 image.key_wrap_table_offset,
7254 image.key_wrap_table_length,
7255 )?;
7256 }
7257 if root_auth_present {
7258 validate_image_region(
7259 image,
7260 4,
7261 image.root_auth_footer_offset,
7262 image.root_auth_footer_length,
7263 )?;
7264 }
7265 validate_image_region(
7266 image,
7267 5,
7268 image.volume_trailer_offset,
7269 image.volume_trailer_length,
7270 )?;
7271 if sha256_region(image, 1)? != image.volume_header_sha256
7272 || sha256_region(image, 2)? != image.crypto_header_sha256
7273 || (key_wrap_present && sha256_region(image, 6)? != image.key_wrap_table_sha256)
7274 || (!key_wrap_present && image.key_wrap_table_sha256 != [0u8; 32])
7275 || sha256_region(image, 3)? != image.manifest_footer_sha256
7276 || (root_auth_present && sha256_region(image, 4)? != image.root_auth_footer_sha256)
7277 || (!root_auth_present && image.root_auth_footer_sha256 != [0u8; 32])
7278 || sha256_region(image, 5)? != image.volume_trailer_sha256
7279 {
7280 return Err(FormatError::InvalidArchive(
7281 "CriticalMetadataImage region digest mismatch",
7282 ));
7283 }
7284 Ok(())
7285}
7286
7287fn image_block_record_len_from_region(image: &CriticalMetadataImage) -> Result<usize, FormatError> {
7288 let crypto_region = image
7289 .region(2)
7290 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7291 let crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
7292 crypto.fixed.validate_supported_profile()?;
7293 Ok(crypto.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN)
7294}
7295
7296fn validate_image_region(
7297 image: &CriticalMetadataImage,
7298 region_type: u16,
7299 offset: u64,
7300 length: u32,
7301) -> Result<(), FormatError> {
7302 let region = image
7303 .region(region_type)
7304 .ok_or(FormatError::InvalidArchive(
7305 "missing CriticalMetadataImage region",
7306 ))?;
7307 if region.offset != offset || region.bytes.len() != length as usize {
7308 return Err(FormatError::InvalidArchive(
7309 "CriticalMetadataImage region range mismatch",
7310 ));
7311 }
7312 Ok(())
7313}
7314
7315fn validate_image_identity(
7316 image: &CriticalMetadataImage,
7317 volume_header: &VolumeHeader,
7318 crypto_header: &CryptoHeaderFixed,
7319) -> Result<(), FormatError> {
7320 if image.volume_format_rev != volume_header.volume_format_rev
7321 || image.archive_uuid != volume_header.archive_uuid
7322 || image.session_id != volume_header.session_id
7323 || image.volume_index != volume_header.volume_index
7324 || image.stripe_width != volume_header.stripe_width
7325 || image.stripe_width != crypto_header.stripe_width
7326 {
7327 return Err(FormatError::InvalidArchive(
7328 "CriticalMetadataImage identity does not match selected volume",
7329 ));
7330 }
7331 Ok(())
7332}
7333
7334fn validate_image_key_wrap_table(
7335 image: &CriticalMetadataImage,
7336 volume_header: &VolumeHeader,
7337 kdf_params: &KdfParams,
7338) -> Result<(), FormatError> {
7339 match kdf_params {
7340 KdfParams::RecipientWrap {
7341 key_wrap_table_length,
7342 key_wrap_table_record_count,
7343 key_wrap_table_digest,
7344 ..
7345 } => {
7346 if image.volume_format_rev != VOLUME_FORMAT_REV_44
7347 || image.layout_flags & 0x0000_0002 == 0
7348 || image.key_wrap_table_length != *key_wrap_table_length
7349 {
7350 return Err(FormatError::InvalidArchive(
7351 "CriticalMetadataImage key-wrap fields do not match KdfParams",
7352 ));
7353 }
7354 let region = image.region(6).ok_or(FormatError::InvalidArchive(
7355 "missing CriticalMetadataImage key-wrap region",
7356 ))?;
7357 if region.offset != image.key_wrap_table_offset
7358 || region.bytes.len() != *key_wrap_table_length as usize
7359 {
7360 return Err(FormatError::InvalidArchive(
7361 "CriticalMetadataImage key-wrap region is malformed",
7362 ));
7363 }
7364 if compute_key_wrap_table_digest(*key_wrap_table_length, ®ion.bytes)
7365 != *key_wrap_table_digest
7366 {
7367 return Err(FormatError::IntegrityDigestMismatch {
7368 structure: "KeyWrapTableV1",
7369 });
7370 }
7371 KeyWrapTableV1::parse(
7372 ®ion.bytes,
7373 &volume_header.archive_uuid,
7374 &volume_header.session_id,
7375 *key_wrap_table_length,
7376 *key_wrap_table_record_count,
7377 )?;
7378 Ok(())
7379 }
7380 _ => {
7381 if image.layout_flags & 0x0000_0002 != 0
7382 || image.region(6).is_some()
7383 || image.key_wrap_table_offset != 0
7384 || image.key_wrap_table_length != 0
7385 || image.key_wrap_table_sha256 != [0u8; 32]
7386 {
7387 return Err(FormatError::InvalidArchive(
7388 "CriticalMetadataImage key-wrap fields must be zero when absent",
7389 ));
7390 }
7391 Ok(())
7392 }
7393 }
7394}
7395
7396fn sha256_region(image: &CriticalMetadataImage, region_type: u16) -> Result<[u8; 32], FormatError> {
7397 Ok(sha256_bytes(
7398 &image
7399 .region(region_type)
7400 .ok_or(FormatError::InvalidArchive(
7401 "missing CriticalMetadataImage region",
7402 ))?
7403 .bytes,
7404 ))
7405}
7406
7407fn validate_recovered_terminal_authority(
7408 image: CriticalMetadataImage,
7409 tuple: CmraDecoderTuple,
7410 master_key: &MasterKey,
7411 require_cmra_boundary_magic: bool,
7412) -> Result<RecoveredTerminalAuthority, FormatError> {
7413 let volume_header_region = image
7414 .region(1)
7415 .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
7416 let volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
7417 let crypto_region = image
7418 .region(2)
7419 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7420 let crypto_header_bytes = crypto_region.bytes.clone();
7421 let parsed_crypto = CryptoHeader::parse(&crypto_header_bytes, image.crypto_header_length)?;
7422 let kdf_params = parsed_crypto.kdf_params.clone();
7423 let subkeys = subkeys_for_open(
7424 Some(master_key),
7425 parsed_crypto.fixed.aead_algo,
7426 &volume_header.archive_uuid,
7427 &volume_header.session_id,
7428 )?;
7429 verify_integrity_tag(
7430 HmacDomain::CryptoHeader,
7431 parsed_crypto.fixed.aead_algo,
7432 volume_header.volume_format_rev,
7433 Some(&subkeys.mac_key),
7434 &volume_header.archive_uuid,
7435 &volume_header.session_id,
7436 parsed_crypto.hmac_covered_bytes,
7437 &parsed_crypto.header_hmac,
7438 )?;
7439 parsed_crypto.validate_extension_semantics()?;
7440 validate_seekable_supported_volume(
7441 &volume_header,
7442 &parsed_crypto.fixed,
7443 &parsed_crypto.extensions,
7444 )?;
7445 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
7446 let crypto_header = parsed_crypto.fixed.clone();
7447 if crypto_header.bit_rot_buffer_pct == 0 {
7448 return Err(FormatError::InvalidArchive(
7449 "CMRA startup recovery requires a nonzero bit-rot budget",
7450 ));
7451 }
7452 drop(parsed_crypto);
7453
7454 let terminal = validate_recovered_terminal_inner(
7455 image,
7456 tuple,
7457 require_cmra_boundary_magic,
7458 true,
7459 KeyHoldingTerminalContext {
7460 subkeys: &subkeys,
7461 volume_header: &volume_header,
7462 crypto_header: &crypto_header,
7463 crypto_header_bytes: &crypto_header_bytes,
7464 },
7465 )?;
7466 Ok(RecoveredTerminalAuthority {
7467 terminal,
7468 volume_header,
7469 crypto_header,
7470 crypto_header_bytes,
7471 subkeys,
7472 kdf_params,
7473 })
7474}
7475
7476fn validate_recovered_recipient_wrap_terminal_authority<F>(
7477 image: CriticalMetadataImage,
7478 tuple: CmraDecoderTuple,
7479 resolver: &mut F,
7480 require_cmra_boundary_magic: bool,
7481) -> Result<RecoveredRecipientWrapTerminalAuthority, FormatError>
7482where
7483 F: FnMut(
7484 RecipientWrapRecordContext<'_>,
7485 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
7486{
7487 let volume_header_region = image
7488 .region(1)
7489 .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
7490 let volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
7491 let crypto_region = image
7492 .region(2)
7493 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7494 let crypto_header_bytes = crypto_region.bytes.clone();
7495 let parsed_crypto = CryptoHeader::parse(&crypto_header_bytes, image.crypto_header_length)?;
7496 if !matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
7497 || !parsed_crypto.fixed.aead_algo.is_encrypted()
7498 {
7499 return Err(FormatError::KeyMaterialMismatch);
7500 }
7501 validate_seekable_supported_volume(&volume_header, &parsed_crypto.fixed, &[])?;
7502 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
7503 if parsed_crypto.fixed.bit_rot_buffer_pct == 0 {
7504 return Err(FormatError::InvalidArchive(
7505 "CMRA startup recovery requires a nonzero bit-rot budget",
7506 ));
7507 }
7508 validate_cmra_writer_parity_lower_bound(tuple, parsed_crypto.fixed.bit_rot_buffer_pct)?;
7509 validate_image_key_wrap_table(&image, &volume_header, &parsed_crypto.kdf_params)?;
7510 let key_wrap_region = image.region(6).ok_or(FormatError::InvalidArchive(
7511 "missing CriticalMetadataImage key-wrap region",
7512 ))?;
7513 let startup_key_wrap_table = parse_startup_key_wrap_table_bytes(
7514 &volume_header,
7515 &parsed_crypto.kdf_params,
7516 key_wrap_region.bytes.clone(),
7517 )?;
7518 let subkeys = recipient_wrap_subkeys_from_table(
7519 &volume_header,
7520 &parsed_crypto,
7521 &startup_key_wrap_table.table,
7522 resolver,
7523 )?;
7524 parsed_crypto.validate_extension_semantics()?;
7525 reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
7526 let crypto_header = parsed_crypto.fixed.clone();
7527
7528 let terminal = validate_recovered_terminal_inner(
7529 image,
7530 tuple,
7531 require_cmra_boundary_magic,
7532 true,
7533 KeyHoldingTerminalContext {
7534 subkeys: &subkeys,
7535 volume_header: &volume_header,
7536 crypto_header: &crypto_header,
7537 crypto_header_bytes: &crypto_header_bytes,
7538 },
7539 )?;
7540 Ok(RecoveredRecipientWrapTerminalAuthority {
7541 terminal,
7542 volume_header,
7543 crypto_header,
7544 crypto_header_bytes,
7545 key_wrap_table_bytes: startup_key_wrap_table.bytes,
7546 block_records_start: startup_key_wrap_table.block_records_start,
7547 subkeys,
7548 })
7549}
7550
7551fn validate_recovered_terminal(
7552 image: CriticalMetadataImage,
7553 tuple: CmraDecoderTuple,
7554 bytes: &[u8],
7555 context: KeyHoldingTerminalContext<'_>,
7556 require_cmra_boundary_magic: bool,
7557) -> Result<V41Terminal, FormatError> {
7558 let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
7559 let cmra_boundary_magic_ok = bytes.get(cmra_offset..cmra_offset + 4) == Some(b"TZCR");
7560 validate_recovered_terminal_inner(
7561 image,
7562 tuple,
7563 require_cmra_boundary_magic,
7564 cmra_boundary_magic_ok,
7565 context,
7566 )
7567}
7568
7569fn validate_recovered_terminal_read_at(
7570 image: CriticalMetadataImage,
7571 tuple: CmraDecoderTuple,
7572 reader: &dyn ArchiveReadAt,
7573 context: KeyHoldingTerminalContext<'_>,
7574 require_cmra_boundary_magic: bool,
7575) -> Result<V41Terminal, FormatError> {
7576 let mut magic = [0u8; 4];
7577 reader.read_exact_at(image.body_bytes_before_cmra, &mut magic)?;
7578 validate_recovered_terminal_inner(
7579 image,
7580 tuple,
7581 require_cmra_boundary_magic,
7582 magic == *b"TZCR",
7583 context,
7584 )
7585}
7586
7587fn validate_recovered_terminal_inner(
7588 image: CriticalMetadataImage,
7589 tuple: CmraDecoderTuple,
7590 require_cmra_boundary_magic: bool,
7591 cmra_boundary_magic_ok: bool,
7592 context: KeyHoldingTerminalContext<'_>,
7593) -> Result<V41Terminal, FormatError> {
7594 let subkeys = context.subkeys;
7595 let volume_header = context.volume_header;
7596 let crypto_header = context.crypto_header;
7597 let volume_header_region = image
7598 .region(1)
7599 .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
7600 let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
7601 if &recovered_volume_header != volume_header {
7602 return Err(FormatError::InvalidArchive(
7603 "CMRA VolumeHeader differs from parsed VolumeHeader",
7604 ));
7605 }
7606 validate_image_identity(&image, volume_header, crypto_header)?;
7607 let crypto_region = image
7608 .region(2)
7609 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7610 let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
7611 if recovered_crypto.fixed != *crypto_header {
7612 return Err(FormatError::InvalidArchive(
7613 "CMRA CryptoHeader differs from parsed CryptoHeader",
7614 ));
7615 }
7616 let recovered_pre_hmac_len = crypto_region
7617 .bytes
7618 .len()
7619 .checked_sub(CRYPTO_HEADER_HMAC_LEN)
7620 .ok_or(FormatError::InvalidArchive(
7621 "CMRA CryptoHeader is too short",
7622 ))?;
7623 let parsed_pre_hmac_len = context
7624 .crypto_header_bytes
7625 .len()
7626 .checked_sub(CRYPTO_HEADER_HMAC_LEN)
7627 .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
7628 if recovered_pre_hmac_len != parsed_pre_hmac_len
7629 || crypto_region.bytes[..recovered_pre_hmac_len]
7630 != context.crypto_header_bytes[..parsed_pre_hmac_len]
7631 {
7632 return Err(FormatError::InvalidArchive(
7633 "CMRA CryptoHeader differs from parsed CryptoHeader",
7634 ));
7635 }
7636 verify_integrity_tag(
7637 HmacDomain::CryptoHeader,
7638 recovered_crypto.fixed.aead_algo,
7639 volume_header.volume_format_rev,
7640 Some(&subkeys.mac_key),
7641 &volume_header.archive_uuid,
7642 &volume_header.session_id,
7643 recovered_crypto.hmac_covered_bytes,
7644 &recovered_crypto.header_hmac,
7645 )?;
7646 validate_cmra_writer_parity_lower_bound(tuple, recovered_crypto.fixed.bit_rot_buffer_pct)?;
7647 recovered_crypto.validate_extension_semantics()?;
7648 validate_image_key_wrap_table(&image, volume_header, &recovered_crypto.kdf_params)?;
7649
7650 let manifest_region = image
7651 .region(3)
7652 .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
7653 let manifest_footer = ManifestFooter::parse(&manifest_region.bytes)?;
7654 validate_manifest_footer(
7655 volume_header,
7656 crypto_header,
7657 &manifest_footer,
7658 subkeys,
7659 volume_header.volume_format_rev,
7660 &manifest_region.bytes,
7661 )?;
7662 manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
7663
7664 let root_auth_footer = if image.layout_flags & 0x0000_0001 != 0 {
7665 let root_auth_region = image
7666 .region(4)
7667 .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
7668 let footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
7669 if footer.format_version != volume_header.format_version
7670 || footer.volume_format_rev != volume_header.volume_format_rev
7671 {
7672 return Err(FormatError::InvalidArchive(
7673 "RootAuthFooter format/revision does not match VolumeHeader",
7674 ));
7675 }
7676 if footer.archive_uuid != volume_header.archive_uuid
7677 || footer.session_id != volume_header.session_id
7678 || footer.footer_length()? != image.root_auth_footer_length
7679 {
7680 return Err(FormatError::InvalidArchive(
7681 "RootAuthFooter identity or length does not match terminal image",
7682 ));
7683 }
7684 Some(footer)
7685 } else {
7686 None
7687 };
7688
7689 let trailer_region = image
7690 .region(5)
7691 .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
7692 let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
7693 verify_integrity_tag(
7694 HmacDomain::VolumeTrailer,
7695 crypto_header.aead_algo,
7696 volume_header.volume_format_rev,
7697 Some(&subkeys.mac_key),
7698 &volume_header.archive_uuid,
7699 &volume_header.session_id,
7700 &trailer_region.bytes[..TRAILER_HMAC_COVERED_LEN],
7701 &trailer.trailer_hmac,
7702 )?;
7703 validate_trailer_identity(volume_header, &trailer)?;
7704 validate_v41_trailer_equations(&image, &trailer)?;
7705
7706 if require_cmra_boundary_magic && !cmra_boundary_magic_ok {
7707 return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
7708 }
7709
7710 let manifest_footer_bytes = manifest_region.bytes.clone();
7711 let root_auth_footer_bytes = image.region(4).map(|region| region.bytes.clone());
7712 Ok(V41Terminal {
7713 image,
7714 manifest_footer_bytes,
7715 root_auth_footer_bytes,
7716 root_auth_footer,
7717 volume_trailer: trailer,
7718 })
7719}
7720
7721fn validate_recovered_public_terminal(
7722 image: CriticalMetadataImage,
7723 bytes: &[u8],
7724 volume_header: &VolumeHeader,
7725 public_crypto_header: &CryptoHeader<'_>,
7726 require_cmra_boundary_magic: bool,
7727) -> Result<V41PublicTerminal, FormatError> {
7728 if image.layout_flags & 0x0000_0001 == 0 {
7729 return Err(FormatError::ReaderUnsupported(
7730 "public no-key verification requires root-auth",
7731 ));
7732 }
7733 let volume_header_region = image
7734 .region(1)
7735 .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
7736 let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
7737 if &recovered_volume_header != volume_header {
7738 return Err(FormatError::InvalidArchive(
7739 "CMRA VolumeHeader differs from parsed VolumeHeader",
7740 ));
7741 }
7742 validate_image_identity(&image, volume_header, &public_crypto_header.fixed)?;
7743 let crypto_region = image
7744 .region(2)
7745 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7746 let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
7747 if !public_crypto_headers_agree(&recovered_crypto.fixed, &public_crypto_header.fixed)
7748 || !public_kdf_profiles_agree(
7749 &recovered_crypto.kdf_params,
7750 &public_crypto_header.kdf_params,
7751 )
7752 {
7753 return Err(FormatError::InvalidArchive(
7754 "CMRA CryptoHeader differs from parsed CryptoHeader",
7755 ));
7756 }
7757 recovered_crypto.validate_extension_semantics()?;
7758 validate_image_key_wrap_table(&image, volume_header, &recovered_crypto.kdf_params)?;
7759
7760 image
7761 .region(3)
7762 .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
7763
7764 let root_auth_region = image
7765 .region(4)
7766 .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
7767 let root_auth_footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
7768 if root_auth_footer.format_version != volume_header.format_version
7769 || root_auth_footer.volume_format_rev != volume_header.volume_format_rev
7770 {
7771 return Err(FormatError::InvalidArchive(
7772 "public RootAuthFooter format/revision does not match VolumeHeader",
7773 ));
7774 }
7775 if root_auth_footer.archive_uuid != volume_header.archive_uuid
7776 || root_auth_footer.session_id != volume_header.session_id
7777 || root_auth_footer.footer_length()? != image.root_auth_footer_length
7778 {
7779 return Err(FormatError::InvalidArchive(
7780 "public RootAuthFooter identity or length does not match terminal image",
7781 ));
7782 }
7783
7784 let trailer_region = image
7785 .region(5)
7786 .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
7787 let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
7788 validate_trailer_identity(volume_header, &trailer)?;
7789 validate_v41_public_trailer_profile(&image, &trailer)?;
7790
7791 let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
7792 if require_cmra_boundary_magic && bytes.get(cmra_offset..cmra_offset + 4) != Some(b"TZCR") {
7793 return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
7794 }
7795
7796 let root_auth_footer_bytes = root_auth_region.bytes.clone();
7797 Ok(V41PublicTerminal {
7798 image,
7799 root_auth_footer_bytes,
7800 root_auth_footer,
7801 })
7802}
7803
7804fn validate_v41_trailer_equations(
7805 image: &CriticalMetadataImage,
7806 trailer: &VolumeTrailer,
7807) -> Result<(), FormatError> {
7808 let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
7809 if trailer.bytes_written != image.volume_trailer_offset
7810 || trailer.manifest_footer_offset != image.manifest_footer_offset
7811 || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
7812 || trailer.block_count != image.block_count
7813 {
7814 return Err(FormatError::InvalidArchive(
7815 "VolumeTrailer does not match v41 terminal layout",
7816 ));
7817 }
7818 if root_auth_present {
7819 if trailer.root_auth_flags != 0x0000_0001
7820 || trailer.root_auth_footer_offset != image.root_auth_footer_offset
7821 || trailer.root_auth_footer_length != image.root_auth_footer_length
7822 || image.root_auth_footer_offset
7823 != image
7824 .manifest_footer_offset
7825 .checked_add(MANIFEST_FOOTER_LEN as u64)
7826 .ok_or(FormatError::InvalidArchive(
7827 "RootAuthFooter trailer boundary overflow",
7828 ))?
7829 || image
7830 .root_auth_footer_offset
7831 .checked_add(image.root_auth_footer_length as u64)
7832 .ok_or(FormatError::InvalidArchive(
7833 "RootAuthFooter trailer boundary overflow",
7834 ))?
7835 != image.volume_trailer_offset
7836 {
7837 return Err(FormatError::InvalidArchive(
7838 "VolumeTrailer root-auth fields do not match v41 terminal layout",
7839 ));
7840 }
7841 } else if trailer.root_auth_footer_offset != 0
7842 || trailer.root_auth_footer_length != 0
7843 || trailer.root_auth_flags != 0
7844 {
7845 return Err(FormatError::InvalidArchive(
7846 "VolumeTrailer root-auth fields must be zero when absent",
7847 ));
7848 }
7849 Ok(())
7850}
7851
7852fn validate_v41_public_trailer_profile(
7853 image: &CriticalMetadataImage,
7854 trailer: &VolumeTrailer,
7855) -> Result<(), FormatError> {
7856 if trailer.bytes_written != image.volume_trailer_offset
7857 || trailer.manifest_footer_offset != image.manifest_footer_offset
7858 || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
7859 {
7860 return Err(FormatError::InvalidArchive(
7861 "VolumeTrailer does not match v41 public terminal layout",
7862 ));
7863 }
7864 if trailer.root_auth_flags != 0x0000_0001
7865 || trailer.root_auth_footer_offset == 0
7866 || trailer.root_auth_footer_length == 0
7867 || trailer.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
7868 || trailer.root_auth_footer_offset != image.root_auth_footer_offset
7869 || trailer.root_auth_footer_length != image.root_auth_footer_length
7870 || image.root_auth_footer_offset
7871 != image
7872 .manifest_footer_offset
7873 .checked_add(MANIFEST_FOOTER_LEN as u64)
7874 .ok_or(FormatError::InvalidArchive(
7875 "RootAuthFooter trailer boundary overflow",
7876 ))?
7877 || image
7878 .root_auth_footer_offset
7879 .checked_add(image.root_auth_footer_length as u64)
7880 .ok_or(FormatError::InvalidArchive(
7881 "RootAuthFooter trailer boundary overflow",
7882 ))?
7883 != image.volume_trailer_offset
7884 {
7885 return Err(FormatError::InvalidArchive(
7886 "VolumeTrailer root-auth fields do not match v41 public terminal layout",
7887 ));
7888 }
7889 Ok(())
7890}
7891
7892fn critical_image_min() -> u64 {
7893 const MIN_CRYPTO_HEADER_LEN: u64 = 116;
7894 CRITICAL_METADATA_IMAGE_FIXED_LEN_V43 as u64
7895 + 4 * SERIALIZED_REGION_HEADER_LEN as u64
7896 + VOLUME_HEADER_LEN as u64
7897 + MIN_CRYPTO_HEADER_LEN
7898 + MANIFEST_FOOTER_LEN as u64
7899 + VOLUME_TRAILER_LEN as u64
7900 + IMAGE_CRC_LEN as u64
7901}
7902
7903fn critical_image_cap() -> Result<u64, FormatError> {
7904 [
7905 CRITICAL_METADATA_IMAGE_FIXED_LEN as u64,
7906 6 * SERIALIZED_REGION_HEADER_LEN as u64,
7907 VOLUME_HEADER_LEN as u64,
7908 READER_MAX_CRYPTO_HEADER_LEN as u64,
7909 READER_MAX_KEY_WRAP_TABLE_LEN as u64,
7910 MANIFEST_FOOTER_LEN as u64,
7911 READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
7912 VOLUME_TRAILER_LEN as u64,
7913 IMAGE_CRC_LEN as u64,
7914 ]
7915 .into_iter()
7916 .try_fold(0u64, |total, value| {
7917 total
7918 .checked_add(value)
7919 .ok_or(FormatError::InvalidArchive("critical image cap overflow"))
7920 })
7921}
7922
7923fn cmra_serialized_length(tuple: CmraDecoderTuple) -> Result<u64, FormatError> {
7924 let shard_total = (tuple.data_shard_count as u64)
7925 .checked_add(tuple.parity_shard_count as u64)
7926 .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
7927 let row_len = (CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN as u64)
7928 .checked_add(tuple.shard_size as u64)
7929 .ok_or(FormatError::InvalidArchive("CMRA row length overflow"))?;
7930 checked_u64_mul(shard_total, row_len, "CMRA length overflow")?
7931 .checked_add(CRITICAL_METADATA_RECOVERY_HEADER_LEN as u64)
7932 .ok_or(FormatError::InvalidArchive("CMRA length overflow"))
7933}
7934
7935fn cmra_worst_case_cap() -> Result<u64, FormatError> {
7936 let cap = critical_image_cap()?;
7937 let mut worst = 0u64;
7938 let mut shard_size = 512u64;
7939 while shard_size <= 4096 {
7940 let data = ceil_div_u64(cap, shard_size)?;
7941 let parity = 2u64.max(ceil_div_u64(
7942 checked_u64_mul(data, READER_MAX_CMRA_PARITY_PCT as u64, "CMRA cap overflow")?,
7943 100,
7944 )?);
7945 let tuple = CmraDecoderTuple {
7946 shard_size: shard_size as u32,
7947 data_shard_count: u16::try_from(data)
7948 .map_err(|_| FormatError::InvalidArchive("CMRA cap data shard overflow"))?,
7949 parity_shard_count: u16::try_from(parity)
7950 .map_err(|_| FormatError::InvalidArchive("CMRA cap parity shard overflow"))?,
7951 image_length: u32::try_from(cap)
7952 .map_err(|_| FormatError::InvalidArchive("CMRA cap image overflow"))?,
7953 image_sha256: [0u8; 32],
7954 };
7955 worst = worst.max(cmra_serialized_length(tuple)?);
7956 shard_size += 2;
7957 }
7958 Ok(worst)
7959}
7960
7961pub(crate) fn v41_terminal_tail_cap() -> Result<usize, FormatError> {
7962 let total = [
7963 MANIFEST_FOOTER_LEN as u64,
7964 READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
7965 VOLUME_TRAILER_LEN as u64,
7966 cmra_worst_case_cap()?,
7967 LOCATOR_PAIR_LEN as u64,
7968 ]
7969 .into_iter()
7970 .try_fold(0u64, |sum, value| {
7971 sum.checked_add(value)
7972 .ok_or(FormatError::InvalidArchive("terminal tail cap overflow"))
7973 })?;
7974 usize::try_from(total).map_err(|_| FormatError::InvalidArchive("terminal tail cap overflow"))
7975}
7976
7977fn max_critical_recovery_scan(options: ReaderOptions) -> Result<usize, FormatError> {
7978 let worst = cmra_worst_case_cap()?;
7979 let total = options
7980 .max_trailing_garbage_scan
7981 .try_into()
7982 .map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
7983 .and_then(|scan: u64| {
7984 scan.checked_add(worst)
7985 .and_then(|value| value.checked_add(LOCATOR_PAIR_LEN as u64))
7986 .ok_or(FormatError::InvalidArchive("scan cap overflow"))
7987 })?;
7988 usize::try_from(total).map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
7989}
7990
7991fn validate_bootstrap_single_volume_input(
7992 volume_header: &VolumeHeader,
7993 crypto_header: &CryptoHeaderFixed,
7994) -> Result<(), FormatError> {
7995 if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
7996 return Err(FormatError::ReaderUnsupported(
7997 "bootstrap sidecar reader supports only single-volume archive input",
7998 ));
7999 }
8000 if crypto_header.stripe_width != volume_header.stripe_width {
8001 return Err(FormatError::InvalidArchive(
8002 "VolumeHeader and CryptoHeader stripe_width differ",
8003 ));
8004 }
8005 Ok(())
8006}
8007
8008#[derive(Debug)]
8009struct ParsedBootstrapSidecar {
8010 manifest_footer: Option<ManifestFooter>,
8011 index_root_records_section: Option<(u64, u64)>,
8012 dictionary_records_section: Option<(u64, u64)>,
8013}
8014
8015pub(crate) struct NonSeekableBootstrapMaterial {
8016 pub(crate) manifest_footer: ManifestFooter,
8017 pub(crate) payload_dictionary: Option<Vec<u8>>,
8018}
8019
8020#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8021enum BootstrapSidecarUse {
8022 SeekableAssist,
8023 NonSeekableRandomAccess,
8024}
8025
8026impl ParsedBootstrapSidecar {
8027 fn require_sections_for(
8028 &self,
8029 sidecar_use: BootstrapSidecarUse,
8030 crypto_header: &CryptoHeaderFixed,
8031 ) -> Result<(), FormatError> {
8032 if sidecar_use == BootstrapSidecarUse::NonSeekableRandomAccess {
8033 if self.manifest_footer.is_none() || self.index_root_records_section.is_none() {
8034 return Err(FormatError::ReaderUnsupported(
8035 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
8036 ));
8037 }
8038 if crypto_header.has_dictionary != 0 && self.dictionary_records_section.is_none() {
8039 return Err(FormatError::ReaderUnsupported(
8040 "dictionary bootstrap required",
8041 ));
8042 }
8043 }
8044 Ok(())
8045 }
8046}
8047
8048pub(crate) fn parse_non_seekable_bootstrap_material(
8049 bootstrap_sidecar: &[u8],
8050 volume_header: &VolumeHeader,
8051 crypto_header: &CryptoHeaderFixed,
8052 subkeys: &Subkeys,
8053) -> Result<NonSeekableBootstrapMaterial, FormatError> {
8054 validate_bootstrap_single_volume_input(volume_header, crypto_header)?;
8055 let sidecar =
8056 parse_bootstrap_sidecar(bootstrap_sidecar, volume_header, crypto_header, subkeys)?;
8057 sidecar.require_sections_for(BootstrapSidecarUse::NonSeekableRandomAccess, crypto_header)?;
8058 let manifest_footer = sidecar
8059 .manifest_footer
8060 .clone()
8061 .ok_or(FormatError::ReaderUnsupported(
8062 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
8063 ))?;
8064
8065 let mut blocks = BTreeMap::new();
8066 let (offset, length) =
8067 sidecar
8068 .index_root_records_section
8069 .ok_or(FormatError::ReaderUnsupported(
8070 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
8071 ))?;
8072 let index_root_records = parse_sidecar_block_records(
8073 bootstrap_sidecar,
8074 crypto_header.block_size as usize,
8075 SidecarBlockRecordsSection {
8076 offset,
8077 length,
8078 extent: index_root_extent_from_manifest(&manifest_footer),
8079 data_kind: BlockKind::IndexRootData,
8080 parity_kind: BlockKind::IndexRootParity,
8081 structure: "IndexRoot",
8082 },
8083 )?;
8084 insert_sidecar_records(&mut blocks, index_root_records)?;
8085
8086 let limits = metadata_limits(crypto_header);
8087 let index_root_plaintext = load_metadata_object_from_parts(
8088 &blocks,
8089 ObjectLoadContext::index_root(
8090 volume_header,
8091 crypto_header,
8092 subkeys,
8093 index_root_extent_from_manifest(&manifest_footer),
8094 ),
8095 manifest_footer.index_root_decompressed_size,
8096 )?;
8097 let index_root = IndexRoot::parse(
8098 &index_root_plaintext,
8099 crypto_header.has_dictionary != 0,
8100 limits,
8101 )?;
8102
8103 if crypto_header.has_dictionary != 0 {
8104 let (offset, length) =
8105 sidecar
8106 .dictionary_records_section
8107 .ok_or(FormatError::ReaderUnsupported(
8108 "dictionary bootstrap required",
8109 ))?;
8110 let dictionary_records = parse_sidecar_block_records(
8111 bootstrap_sidecar,
8112 crypto_header.block_size as usize,
8113 SidecarBlockRecordsSection {
8114 offset,
8115 length,
8116 extent: dictionary_extent_from_index_root(&index_root)?,
8117 data_kind: BlockKind::DictionaryData,
8118 parity_kind: BlockKind::DictionaryParity,
8119 structure: "dictionary",
8120 },
8121 )?;
8122 insert_sidecar_records(&mut blocks, dictionary_records)?;
8123 }
8124 let payload_dictionary =
8125 load_archive_dictionary(&blocks, subkeys, volume_header, crypto_header, &index_root)?;
8126
8127 Ok(NonSeekableBootstrapMaterial {
8128 manifest_footer,
8129 payload_dictionary,
8130 })
8131}
8132
8133fn parse_bootstrap_sidecar(
8134 bytes: &[u8],
8135 volume_header: &VolumeHeader,
8136 crypto_header: &CryptoHeaderFixed,
8137 subkeys: &Subkeys,
8138) -> Result<ParsedBootstrapSidecar, FormatError> {
8139 let header_bytes = slice(
8140 bytes,
8141 0,
8142 BOOTSTRAP_SIDECAR_HEADER_LEN,
8143 "BootstrapSidecarHeader",
8144 )?;
8145 let header = BootstrapSidecarHeader::parse(header_bytes)?;
8146 if header.archive_uuid != volume_header.archive_uuid
8147 || header.session_id != volume_header.session_id
8148 {
8149 return Err(FormatError::InvalidArchive(
8150 "bootstrap sidecar identity does not match VolumeHeader",
8151 ));
8152 }
8153 verify_integrity_tag(
8154 HmacDomain::BootstrapSidecar,
8155 crypto_header.aead_algo,
8156 volume_header.volume_format_rev,
8157 Some(&subkeys.mac_key),
8158 &volume_header.archive_uuid,
8159 &volume_header.session_id,
8160 &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
8161 &header.sidecar_hmac,
8162 )?;
8163 header.validate_packed_layout(bytes.len() as u64)?;
8164 validate_sidecar_size_cap(&header, crypto_header, bytes.len() as u64)?;
8165
8166 if header.has_dictionary_records() && crypto_header.has_dictionary == 0 {
8167 return Err(FormatError::InvalidArchive(
8168 "bootstrap sidecar has dictionary records while has_dictionary is false",
8169 ));
8170 }
8171
8172 let manifest_footer = if header.has_manifest_footer() {
8173 let manifest_offset = to_usize(header.manifest_footer_offset, "BootstrapSidecarHeader")?;
8174 let manifest_bytes = slice(
8175 bytes,
8176 manifest_offset,
8177 MANIFEST_FOOTER_LEN,
8178 "ManifestFooter",
8179 )?;
8180 let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
8181 validate_sidecar_manifest_footer(
8182 volume_header,
8183 crypto_header,
8184 &manifest_footer,
8185 subkeys,
8186 volume_header.volume_format_rev,
8187 manifest_bytes,
8188 )?;
8189 manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
8190 Some(manifest_footer)
8191 } else {
8192 None
8193 };
8194
8195 Ok(ParsedBootstrapSidecar {
8196 manifest_footer,
8197 index_root_records_section: header.has_index_root_records().then_some((
8198 header.index_root_records_offset,
8199 header.index_root_records_length,
8200 )),
8201 dictionary_records_section: header.has_dictionary_records().then_some((
8202 header.dictionary_records_offset,
8203 header.dictionary_records_length,
8204 )),
8205 })
8206}
8207
8208fn index_root_extent_from_manifest(manifest_footer: &ManifestFooter) -> ObjectExtent {
8209 ObjectExtent {
8210 first_block_index: manifest_footer.index_root_first_block,
8211 data_block_count: manifest_footer.index_root_data_block_count,
8212 parity_block_count: manifest_footer.index_root_parity_block_count,
8213 encrypted_size: manifest_footer.index_root_encrypted_size,
8214 }
8215}
8216
8217fn insert_sidecar_records(
8218 blocks: &mut BTreeMap<u64, BlockRecord>,
8219 records: Vec<BlockRecord>,
8220) -> Result<(), FormatError> {
8221 for record in records {
8222 if let Some(existing) = blocks.insert(record.block_index, record.clone()) {
8223 if existing != record {
8224 return Err(FormatError::InvalidArchive(
8225 "bootstrap sidecar conflicts with volume BlockRecord",
8226 ));
8227 }
8228 }
8229 }
8230 Ok(())
8231}
8232
8233fn validate_sidecar_manifest_footer(
8234 volume_header: &VolumeHeader,
8235 crypto_header: &CryptoHeaderFixed,
8236 footer: &ManifestFooter,
8237 subkeys: &Subkeys,
8238 volume_format_rev: u16,
8239 raw: &[u8],
8240) -> Result<(), FormatError> {
8241 if footer.archive_uuid != volume_header.archive_uuid
8242 || footer.session_id != volume_header.session_id
8243 {
8244 return Err(FormatError::InvalidArchive(
8245 "sidecar ManifestFooter identity does not match VolumeHeader",
8246 ));
8247 }
8248 if footer.volume_index != 0 {
8249 return Err(FormatError::InvalidArchive(
8250 "sidecar ManifestFooter volume_index must be zero",
8251 ));
8252 }
8253 if footer.total_volumes != crypto_header.stripe_width {
8254 return Err(FormatError::InvalidArchive(
8255 "sidecar ManifestFooter total_volumes does not match stripe_width",
8256 ));
8257 }
8258 if footer.is_authoritative != 1 {
8259 return Err(FormatError::InvalidArchive(
8260 "sidecar ManifestFooter is not authoritative",
8261 ));
8262 }
8263 verify_integrity_tag(
8264 HmacDomain::ManifestFooter,
8265 crypto_header.aead_algo,
8266 volume_format_rev,
8267 Some(&subkeys.mac_key),
8268 &volume_header.archive_uuid,
8269 &volume_header.session_id,
8270 &raw[..MANIFEST_HMAC_COVERED_LEN],
8271 &footer.manifest_hmac,
8272 )
8273}
8274
8275fn validate_sidecar_size_cap(
8276 header: &BootstrapSidecarHeader,
8277 crypto_header: &CryptoHeaderFixed,
8278 file_size: u64,
8279) -> Result<(), FormatError> {
8280 let record_len = checked_u64_add(
8281 crypto_header.block_size as u64,
8282 BLOCK_RECORD_FRAMING_LEN as u64,
8283 "bootstrap sidecar cap overflow",
8284 )?;
8285 let max_index_records = crypto_header.index_root_fec_data_shards as u64
8286 + crypto_header.index_root_fec_parity_shards as u64;
8287 let max_record_section_bytes = checked_u64_mul(
8288 max_index_records,
8289 record_len,
8290 "bootstrap sidecar cap overflow",
8291 )?;
8292 if header.index_root_records_length % record_len != 0 {
8293 return Err(FormatError::InvalidArchive(
8294 "bootstrap sidecar IndexRoot records length is not aligned",
8295 ));
8296 }
8297 if header.index_root_records_length / record_len > max_index_records {
8298 return Err(FormatError::InvalidArchive(
8299 "bootstrap sidecar IndexRoot records exceed resource cap",
8300 ));
8301 }
8302 if header.dictionary_records_length % record_len != 0 {
8303 return Err(FormatError::InvalidArchive(
8304 "bootstrap sidecar dictionary records length is not aligned",
8305 ));
8306 }
8307 if header.dictionary_records_length / record_len > max_index_records {
8308 return Err(FormatError::InvalidArchive(
8309 "bootstrap sidecar dictionary records exceed resource cap",
8310 ));
8311 }
8312
8313 let mut cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64;
8314 if header.has_manifest_footer() {
8315 cap = cap
8316 .checked_add(MANIFEST_FOOTER_LEN as u64)
8317 .ok_or(FormatError::InvalidArchive(
8318 "bootstrap sidecar cap overflow",
8319 ))?;
8320 }
8321 if header.has_index_root_records() {
8322 cap = checked_u64_add(
8323 cap,
8324 max_record_section_bytes,
8325 "bootstrap sidecar cap overflow",
8326 )?;
8327 }
8328 if header.has_dictionary_records() {
8329 cap = checked_u64_add(
8330 cap,
8331 max_record_section_bytes,
8332 "bootstrap sidecar cap overflow",
8333 )?;
8334 }
8335 if file_size > cap {
8336 return Err(FormatError::InvalidArchive(
8337 "bootstrap sidecar exceeds resource cap",
8338 ));
8339 }
8340 Ok(())
8341}
8342
8343#[derive(Debug, Clone, Copy)]
8344struct SidecarBlockRecordsSection {
8345 offset: u64,
8346 length: u64,
8347 extent: ObjectExtent,
8348 data_kind: BlockKind,
8349 parity_kind: BlockKind,
8350 structure: &'static str,
8351}
8352
8353fn parse_sidecar_block_records(
8354 sidecar_bytes: &[u8],
8355 block_size: usize,
8356 section: SidecarBlockRecordsSection,
8357) -> Result<Vec<BlockRecord>, FormatError> {
8358 let record_len = block_size
8359 .checked_add(BLOCK_RECORD_FRAMING_LEN)
8360 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8361 if section.length % record_len as u64 != 0 {
8362 return Err(FormatError::InvalidArchive(
8363 "sidecar BlockRecord section is not aligned",
8364 ));
8365 }
8366 let expected_count =
8367 section.extent.data_block_count as usize + section.extent.parity_block_count as usize;
8368 let actual_count = usize::try_from(section.length / record_len as u64)
8369 .map_err(|_| FormatError::InvalidArchive("sidecar BlockRecord count overflow"))?;
8370 if actual_count != expected_count {
8371 return Err(FormatError::InvalidArchive(
8372 "sidecar BlockRecord section does not match declared extent",
8373 ));
8374 }
8375 let start = to_usize(section.offset, "BootstrapSidecarHeader")?;
8376 let raw = slice(
8377 sidecar_bytes,
8378 start,
8379 to_usize(section.length, "BootstrapSidecarHeader")?,
8380 "BootstrapSidecarHeader",
8381 )?;
8382 let mut records = Vec::with_capacity(expected_count);
8383
8384 for idx in 0..expected_count {
8385 let record = BlockRecord::parse(
8386 slice(raw, idx * record_len, record_len, "BlockRecord")?,
8387 block_size,
8388 )?;
8389 let expected_block_index = checked_u64_add(
8390 section.extent.first_block_index,
8391 idx as u64,
8392 section.structure,
8393 )?;
8394 if record.block_index != expected_block_index {
8395 return Err(FormatError::InvalidArchive(
8396 "sidecar BlockRecord section has missing or duplicate blocks",
8397 ));
8398 }
8399 let expected_kind = if idx < section.extent.data_block_count as usize {
8400 section.data_kind
8401 } else {
8402 section.parity_kind
8403 };
8404 if record.kind != expected_kind {
8405 return Err(FormatError::InvalidArchive(
8406 "sidecar BlockRecord section has wrong kind",
8407 ));
8408 }
8409 let should_be_last = idx + 1 == section.extent.data_block_count as usize;
8410 if idx < section.extent.data_block_count as usize && record.is_last_data() != should_be_last
8411 {
8412 return Err(FormatError::InvalidArchive(
8413 "sidecar BlockRecord section has wrong last-data flag",
8414 ));
8415 }
8416 records.push(record);
8417 }
8418
8419 Ok(records)
8420}
8421
8422fn validate_trailer_identity(
8423 volume_header: &VolumeHeader,
8424 trailer: &VolumeTrailer,
8425) -> Result<(), FormatError> {
8426 if trailer.archive_uuid != volume_header.archive_uuid
8427 || trailer.session_id != volume_header.session_id
8428 || trailer.volume_index != volume_header.volume_index
8429 {
8430 return Err(FormatError::InvalidArchive(
8431 "VolumeTrailer identity does not match VolumeHeader",
8432 ));
8433 }
8434 Ok(())
8435}
8436
8437fn validate_manifest_footer(
8438 volume_header: &VolumeHeader,
8439 crypto_header: &CryptoHeaderFixed,
8440 footer: &ManifestFooter,
8441 subkeys: &Subkeys,
8442 volume_format_rev: u16,
8443 raw: &[u8],
8444) -> Result<(), FormatError> {
8445 if footer.archive_uuid != volume_header.archive_uuid
8446 || footer.session_id != volume_header.session_id
8447 || footer.volume_index != volume_header.volume_index
8448 {
8449 return Err(FormatError::InvalidArchive(
8450 "ManifestFooter identity does not match VolumeHeader",
8451 ));
8452 }
8453 if footer.total_volumes != volume_header.stripe_width {
8454 return Err(FormatError::InvalidArchive(
8455 "ManifestFooter total_volumes does not match stripe_width",
8456 ));
8457 }
8458 if footer.is_authoritative != 1 {
8459 return Err(FormatError::InvalidArchive(
8460 "ManifestFooter is not authoritative",
8461 ));
8462 }
8463 verify_integrity_tag(
8464 HmacDomain::ManifestFooter,
8465 crypto_header.aead_algo,
8466 volume_format_rev,
8467 Some(&subkeys.mac_key),
8468 &volume_header.archive_uuid,
8469 &volume_header.session_id,
8470 &raw[..MANIFEST_HMAC_COVERED_LEN],
8471 &footer.manifest_hmac,
8472 )
8473}
8474
8475#[derive(Debug)]
8476struct ParsedBlockRegion {
8477 blocks: BTreeMap<u64, BlockRecord>,
8478 erased_block_indices: BTreeSet<u64>,
8479}
8480
8481fn parse_block_region(
8482 bytes: &[u8],
8483 start: usize,
8484 end: usize,
8485 block_size: usize,
8486 volume_header: &VolumeHeader,
8487 trailer: &VolumeTrailer,
8488) -> Result<ParsedBlockRegion, FormatError> {
8489 if end < start {
8490 return Err(FormatError::InvalidArchive(
8491 "ManifestFooter starts before BlockRecord region",
8492 ));
8493 }
8494 let record_len = block_size
8495 .checked_add(BLOCK_RECORD_FRAMING_LEN)
8496 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8497 let region_len = end - start;
8498 if region_len % record_len != 0 {
8499 return Err(FormatError::InvalidArchive(
8500 "BlockRecord region length is not aligned",
8501 ));
8502 }
8503 let observed_count = region_len / record_len;
8504 if observed_count as u64 != trailer.block_count {
8505 return Err(FormatError::InvalidArchive(
8506 "VolumeTrailer block_count does not match BlockRecord region",
8507 ));
8508 }
8509
8510 let mut blocks = BTreeMap::new();
8511 let mut erased_block_indices = BTreeSet::new();
8512 for idx in 0..observed_count {
8513 let offset = start + idx * record_len;
8514 let expected_block_index = checked_u64_add(
8515 volume_header.volume_index as u64,
8516 checked_u64_mul(
8517 idx as u64,
8518 volume_header.stripe_width as u64,
8519 "BlockRecord index overflow",
8520 )?,
8521 "BlockRecord index overflow",
8522 )?;
8523 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
8524 match BlockRecord::parse(raw, block_size) {
8525 Ok(record) => {
8526 if record.block_index != expected_block_index {
8527 return Err(FormatError::InvalidArchive(
8528 "BlockRecord index does not match volume position",
8529 ));
8530 }
8531 if blocks.insert(record.block_index, record).is_some() {
8532 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
8533 }
8534 }
8535 Err(err) if block_record_error_is_recoverable_erasure(&err) => {
8536 if !erased_block_indices.insert(expected_block_index) {
8537 return Err(FormatError::InvalidArchive(
8538 "duplicate erased BlockRecord index",
8539 ));
8540 }
8541 }
8542 Err(err) => return Err(err),
8543 }
8544 }
8545
8546 Ok(ParsedBlockRegion {
8547 blocks,
8548 erased_block_indices,
8549 })
8550}
8551
8552fn validate_seekable_block_region_layout(
8553 start: u64,
8554 end: u64,
8555 block_size: usize,
8556 trailer: &VolumeTrailer,
8557) -> Result<(), FormatError> {
8558 if end < start {
8559 return Err(FormatError::InvalidArchive(
8560 "ManifestFooter starts before BlockRecord region",
8561 ));
8562 }
8563 let record_len = block_record_len(block_size)?;
8564 let region_len = end - start;
8565 if region_len % record_len != 0 {
8566 return Err(FormatError::InvalidArchive(
8567 "BlockRecord region length is not aligned",
8568 ));
8569 }
8570 let observed_count = region_len / record_len;
8571 if observed_count != trailer.block_count {
8572 return Err(FormatError::InvalidArchive(
8573 "VolumeTrailer block_count does not match BlockRecord region",
8574 ));
8575 }
8576 Ok(())
8577}
8578
8579fn parse_public_block_observation(
8580 bytes: &[u8],
8581 start: usize,
8582 image: &CriticalMetadataImage,
8583 block_size: usize,
8584 volume_header: &VolumeHeader,
8585) -> Result<BTreeMap<u64, BlockRecord>, FormatError> {
8586 let image_start = to_usize(image.block_records_offset, "BlockRecord")?;
8587 if start != image_start {
8588 return Err(FormatError::InvalidArchive(
8589 "public BlockRecord observation start mismatch",
8590 ));
8591 }
8592 let scan_limit_u64 = image
8593 .block_records_offset
8594 .checked_add(image.block_records_length)
8595 .ok_or(FormatError::InvalidArchive(
8596 "public BlockRecord observation limit overflow",
8597 ))?;
8598 if scan_limit_u64 != image.manifest_footer_offset {
8599 return Err(FormatError::InvalidArchive(
8600 "public BlockRecord observation limit mismatch",
8601 ));
8602 }
8603 let scan_limit = to_usize(scan_limit_u64, "BlockRecord")?;
8604 if scan_limit < start {
8605 return Err(FormatError::InvalidArchive(
8606 "public BlockRecord observation limit before start",
8607 ));
8608 }
8609 let record_len = block_size
8610 .checked_add(BLOCK_RECORD_FRAMING_LEN)
8611 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8612 let region_len = scan_limit - start;
8613 if region_len % record_len != 0 {
8614 return Err(FormatError::InvalidArchive(
8615 "public BlockRecord observation window is not aligned",
8616 ));
8617 }
8618
8619 let mut blocks = BTreeMap::new();
8620 let mut offset = start;
8621 let mut observed_slot = 0u64;
8622 while offset < scan_limit {
8623 let magic_end = checked_add(offset, 4, "BlockRecord")?;
8624 if magic_end > scan_limit || bytes.get(offset..magic_end) != Some(b"TZBK") {
8625 break;
8626 }
8627 let record_end = checked_add(offset, record_len, "BlockRecord")?;
8628 if record_end > scan_limit {
8629 return Err(FormatError::InvalidArchive(
8630 "public BlockRecord observation slot is incomplete",
8631 ));
8632 }
8633 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
8634 let record = BlockRecord::parse(raw, block_size)?;
8635 let expected_block_index = checked_u64_add(
8636 volume_header.volume_index as u64,
8637 checked_u64_mul(
8638 observed_slot,
8639 volume_header.stripe_width as u64,
8640 "BlockRecord index overflow",
8641 )?,
8642 "BlockRecord index overflow",
8643 )?;
8644 if record.block_index != expected_block_index {
8645 return Err(FormatError::InvalidArchive(
8646 "public BlockRecord index does not match volume position",
8647 ));
8648 }
8649 if blocks.insert(record.block_index, record).is_some() {
8650 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
8651 }
8652 offset = record_end;
8653 observed_slot = observed_slot
8654 .checked_add(1)
8655 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
8656 }
8657
8658 let mut scan = if offset < scan_limit {
8659 checked_add(offset, record_len, "BlockRecord")?
8660 } else {
8661 scan_limit
8662 };
8663 while scan < scan_limit {
8664 let magic_end = checked_add(scan, 4, "BlockRecord")?;
8665 let record_end = checked_add(scan, record_len, "BlockRecord")?;
8666 if record_end <= scan_limit && bytes.get(scan..magic_end) == Some(b"TZBK") {
8667 let raw = slice(bytes, scan, record_len, "BlockRecord")?;
8668 if BlockRecord::parse(raw, block_size).is_ok() {
8669 return Err(FormatError::InvalidArchive(
8670 "public observation has ambiguous extra BlockRecord",
8671 ));
8672 }
8673 }
8674 scan = record_end;
8675 }
8676
8677 Ok(blocks)
8678}
8679
8680pub(crate) fn block_record_error_is_recoverable_erasure(error: &FormatError) -> bool {
8681 matches!(
8682 error,
8683 FormatError::BadCrc {
8684 structure: "BlockRecord",
8685 } | FormatError::BadMagic {
8686 structure: "BlockRecord",
8687 } | FormatError::NonZeroReserved {
8688 structure: "BlockRecord",
8689 }
8690 )
8691}
8692
8693fn block_record_len(block_size: usize) -> Result<u64, FormatError> {
8694 let len = block_size
8695 .checked_add(BLOCK_RECORD_FRAMING_LEN)
8696 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8697 u64::try_from(len).map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))
8698}
8699
8700fn checked_u64_mul(lhs: u64, rhs: u64, reason: &'static str) -> Result<u64, FormatError> {
8701 lhs.checked_mul(rhs)
8702 .ok_or(FormatError::InvalidArchive(reason))
8703}
8704
8705fn parse_stream_block_prefix(
8706 bytes: &[u8],
8707 start: usize,
8708 block_size: usize,
8709 volume_header: &VolumeHeader,
8710) -> Result<(BTreeMap<u64, BlockRecord>, usize, u64), FormatError> {
8711 let record_len = block_size
8712 .checked_add(BLOCK_RECORD_FRAMING_LEN)
8713 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8714 let mut blocks = BTreeMap::new();
8715 let mut offset = start;
8716 let mut observed_block_count = 0u64;
8717
8718 while bytes.get(offset..offset + 4) == Some(b"TZBK") {
8719 let expected_block_index =
8720 expected_stream_block_index(volume_header, observed_block_count)?;
8721 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
8722 match BlockRecord::parse(raw, block_size) {
8723 Ok(record) => {
8724 if record.block_index != expected_block_index {
8725 return Err(FormatError::InvalidArchive(
8726 "BlockRecord index does not match stream position",
8727 ));
8728 }
8729 if blocks.insert(record.block_index, record).is_some() {
8730 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
8731 }
8732 }
8733 Err(err) if block_record_error_is_recoverable_erasure(&err) => {}
8734 Err(err) => return Err(err),
8735 }
8736 offset = checked_add(offset, record_len, "BlockRecord")?;
8737 observed_block_count = observed_block_count
8738 .checked_add(1)
8739 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
8740 }
8741
8742 Ok((blocks, offset, observed_block_count))
8743}
8744
8745pub(crate) fn expected_stream_block_index(
8746 volume_header: &VolumeHeader,
8747 observed_block_count: u64,
8748) -> Result<u64, FormatError> {
8749 checked_u64_add(
8750 volume_header.volume_index as u64,
8751 checked_u64_mul(
8752 observed_block_count,
8753 volume_header.stripe_width as u64,
8754 "BlockRecord index overflow",
8755 )?,
8756 "BlockRecord index overflow",
8757 )
8758}
8759
8760fn parse_sequential_block_or_erasure(
8761 bytes: &[u8],
8762 offset: usize,
8763 record_len: usize,
8764 block_size: usize,
8765 volume_header: &VolumeHeader,
8766 observed_block_count: u64,
8767) -> Result<Option<BlockRecord>, FormatError> {
8768 let expected_block_index = expected_stream_block_index(volume_header, observed_block_count)?;
8769 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
8770 match BlockRecord::parse(raw, block_size) {
8771 Ok(record) => {
8772 if record.block_index != expected_block_index {
8773 return Err(FormatError::InvalidArchive(
8774 "BlockRecord index does not match stream position",
8775 ));
8776 }
8777 Ok(Some(record))
8778 }
8779 Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
8780 Err(err) => Err(err),
8781 }
8782}
8783
8784fn parse_terminal_material(
8785 bytes: &[u8],
8786 manifest_offset: usize,
8787 observed_block_count: u64,
8788 context: KeyHoldingTerminalContext<'_>,
8789 options: ReaderOptions,
8790) -> Result<(ManifestFooter, VolumeTrailer, Option<RootAuthFooterV1>), FormatError> {
8791 let candidate = locate_v41_terminal_candidate(bytes, context, options)?;
8792 if !terminal_candidate_reaches_eof(&candidate, bytes.len())? {
8793 return Err(FormatError::InvalidArchive(
8794 "sequential terminal does not end at EOF",
8795 ));
8796 }
8797 let terminal = candidate.terminal;
8798 if terminal.image.manifest_footer_offset != manifest_offset as u64 {
8799 return Err(FormatError::InvalidArchive(
8800 "VolumeTrailer ManifestFooter offset does not match observed stream offset",
8801 ));
8802 }
8803 if terminal.volume_trailer.block_count != observed_block_count {
8804 return Err(FormatError::InvalidArchive(
8805 "VolumeTrailer block_count does not match observed stream",
8806 ));
8807 }
8808 let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
8809 Ok((
8810 manifest_footer,
8811 terminal.volume_trailer,
8812 terminal.root_auth_footer,
8813 ))
8814}
8815
8816pub(crate) fn parse_terminal_material_read_at(
8817 reader: &dyn ArchiveReadAt,
8818 input_len: u64,
8819 manifest_offset: u64,
8820 observed_block_count: u64,
8821 context: KeyHoldingTerminalContext<'_>,
8822) -> Result<SequentialTerminalMaterial, FormatError> {
8823 let mut candidates = Vec::new();
8824 if input_len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
8825 collect_v41_locator_candidate_read_at(
8826 reader,
8827 input_len - CRITICAL_RECOVERY_LOCATOR_LEN as u64,
8828 0,
8829 context,
8830 &mut candidates,
8831 );
8832 }
8833 if input_len >= LOCATOR_PAIR_LEN as u64 {
8834 collect_v41_locator_candidate_read_at(
8835 reader,
8836 input_len - LOCATOR_PAIR_LEN as u64,
8837 1,
8838 context,
8839 &mut candidates,
8840 );
8841 }
8842
8843 let candidate = choose_v41_terminal_candidate(candidates)?;
8844 if !terminal_candidate_reaches_eof(&candidate, to_usize(input_len, "terminal EOF")?)? {
8845 return Err(FormatError::InvalidArchive(
8846 "sequential terminal does not end at EOF",
8847 ));
8848 }
8849 let terminal = candidate.terminal;
8850 if terminal.image.manifest_footer_offset != manifest_offset {
8851 return Err(FormatError::InvalidArchive(
8852 "VolumeTrailer ManifestFooter offset does not match observed stream offset",
8853 ));
8854 }
8855 if terminal.volume_trailer.block_count != observed_block_count {
8856 return Err(FormatError::InvalidArchive(
8857 "VolumeTrailer block_count does not match observed stream",
8858 ));
8859 }
8860 let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
8861 Ok(SequentialTerminalMaterial {
8862 manifest_footer,
8863 volume_trailer: terminal.volume_trailer,
8864 root_auth_footer: terminal.root_auth_footer,
8865 })
8866}
8867
8868fn terminal_candidate_reaches_eof(
8869 candidate: &TerminalCandidate,
8870 input_len: usize,
8871) -> Result<bool, FormatError> {
8872 let expected_end =
8873 match candidate.locator_sequence {
8874 Some(0) => candidate.anchor,
8875 Some(1) => candidate
8876 .anchor
8877 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
8878 .ok_or(FormatError::InvalidArchive(
8879 "terminal EOF boundary overflow",
8880 ))?,
8881 None => candidate.anchor.checked_add(LOCATOR_PAIR_LEN).ok_or(
8882 FormatError::InvalidArchive("terminal EOF boundary overflow"),
8883 )?,
8884 Some(_) => {
8885 return Err(FormatError::InvalidArchive(
8886 "invalid terminal locator sequence",
8887 ))
8888 }
8889 };
8890 Ok(expected_end == input_len)
8891}
8892
8893#[derive(Debug, Default)]
8894struct PendingSequentialEnvelope {
8895 data_shards: Vec<Option<Vec<u8>>>,
8896 parity_shards: Vec<Option<Vec<u8>>>,
8897 saw_last_data: bool,
8898 awaiting_tentative_parity: bool,
8899}
8900
8901impl PendingSequentialEnvelope {
8902 fn is_empty(&self) -> bool {
8903 self.data_shards.is_empty() && self.parity_shards.is_empty()
8904 }
8905}
8906
8907fn handle_sequential_payload_erasure(
8908 pending: &mut PendingSequentialEnvelope,
8909 crypto_header: &CryptoHeaderFixed,
8910 metadata_seen: bool,
8911) -> Result<(), FormatError> {
8912 if metadata_seen || pending.saw_last_data {
8913 return Err(FormatError::BadCrc {
8914 structure: "BlockRecord",
8915 });
8916 }
8917 if !sequential_payload_parity_is_guaranteed(crypto_header) {
8918 return Err(FormatError::BadCrc {
8919 structure: "BlockRecord",
8920 });
8921 }
8922 pending.data_shards.push(None);
8923 pending.awaiting_tentative_parity = true;
8924 if pending.data_shards.len() > crypto_header.fec_data_shards as usize {
8925 return Err(FormatError::InvalidArchive(
8926 "sequential payload envelope exceeds data-shard cap",
8927 ));
8928 }
8929 Ok(())
8930}
8931
8932fn sequential_payload_parity_is_guaranteed(crypto_header: &CryptoHeaderFixed) -> bool {
8933 crypto_header.fec_parity_shards > 0
8934 && (crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0)
8935}
8936
8937fn sequential_extract_tar_stream_with_options(
8938 bytes: &[u8],
8939 master_key: &MasterKey,
8940 options: ReaderOptions,
8941) -> Result<Vec<u8>, FormatError> {
8942 validate_reader_options(options)?;
8943 if bytes.len() < VOLUME_HEADER_LEN {
8944 return Err(FormatError::InvalidLength {
8945 structure: "archive",
8946 expected: VOLUME_HEADER_LEN,
8947 actual: bytes.len(),
8948 });
8949 }
8950
8951 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
8952 let crypto_start = volume_header.crypto_header_offset as usize;
8953 let crypto_len = volume_header.crypto_header_length as usize;
8954 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
8955 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
8956 let subkeys = subkeys_for_open(
8957 Some(master_key),
8958 parsed_crypto.fixed.aead_algo,
8959 &volume_header.archive_uuid,
8960 &volume_header.session_id,
8961 )?;
8962 verify_integrity_tag(
8963 HmacDomain::CryptoHeader,
8964 parsed_crypto.fixed.aead_algo,
8965 volume_header.volume_format_rev,
8966 Some(&subkeys.mac_key),
8967 &volume_header.archive_uuid,
8968 &volume_header.session_id,
8969 parsed_crypto.hmac_covered_bytes,
8970 &parsed_crypto.header_hmac,
8971 )?;
8972 parsed_crypto.validate_extension_semantics()?;
8973 validate_sequential_supported_volume(
8974 &volume_header,
8975 &parsed_crypto.fixed,
8976 &parsed_crypto.extensions,
8977 )?;
8978 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
8979 let block_records_start = startup_block_records_start(
8980 &volume_header,
8981 &parsed_crypto.kdf_params,
8982 |start, length| {
8983 let start = to_usize(start, "KeyWrapTableV1")?;
8984 Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
8985 },
8986 )?;
8987
8988 let block_size = parsed_crypto.fixed.block_size as usize;
8989 let record_len = block_size
8990 .checked_add(BLOCK_RECORD_FRAMING_LEN)
8991 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8992 let mut offset = to_usize(block_records_start, "BlockRecord")?;
8993 let mut observed_block_count = 0u64;
8994 let mut metadata_seen = false;
8995 let mut pending = PendingSequentialEnvelope::default();
8996 let mut next_envelope_index = 0u64;
8997 let mut tar_stream = Vec::new();
8998 let max_tar_stream_size = options.max_verify_tar_size;
8999 let observed_archive_bytes = observed_archive_size([bytes.len() as u64])?;
9000 let total_extraction_cap = total_extraction_size_cap(options, observed_archive_bytes);
9001 let mut tar_stream_total_validator = TarStreamTotalExtractionSizeValidator::new(
9002 parsed_crypto.fixed.max_path_length,
9003 total_extraction_cap,
9004 );
9005
9006 while bytes.get(offset..offset + 4) == Some(b"TZBK") {
9007 let record = parse_sequential_block_or_erasure(
9008 bytes,
9009 offset,
9010 record_len,
9011 block_size,
9012 &volume_header,
9013 observed_block_count,
9014 )?;
9015 observed_block_count = observed_block_count
9016 .checked_add(1)
9017 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
9018 let Some(record) = record else {
9019 handle_sequential_payload_erasure(&mut pending, &parsed_crypto.fixed, metadata_seen)?;
9020 offset = checked_add(offset, record_len, "BlockRecord")?;
9021 continue;
9022 };
9023
9024 match record.kind {
9025 BlockKind::PayloadData => {
9026 if metadata_seen {
9027 return Err(FormatError::InvalidArchive(
9028 "payload BlockRecord appears after metadata",
9029 ));
9030 }
9031 if pending.awaiting_tentative_parity {
9032 return Err(FormatError::InvalidArchive(
9033 "sequential payload envelope boundary is ambiguous after CRC erasure",
9034 ));
9035 }
9036 if pending.saw_last_data {
9037 finalize_sequential_envelope(
9038 &mut pending,
9039 SequentialEnvelopeDecodeContext {
9040 crypto_header: &parsed_crypto.fixed,
9041 subkeys: &subkeys,
9042 volume_header: &volume_header,
9043 next_envelope_index: &mut next_envelope_index,
9044 tar_stream: &mut tar_stream,
9045 max_tar_stream_size,
9046 tar_stream_total_validator: &mut tar_stream_total_validator,
9047 },
9048 )?;
9049 }
9050 let is_last_data = record.is_last_data();
9051 pending.data_shards.push(Some(record.payload));
9052 if is_last_data {
9053 pending.saw_last_data = true;
9054 }
9055 if pending.data_shards.len() > parsed_crypto.fixed.fec_data_shards as usize {
9056 return Err(FormatError::InvalidArchive(
9057 "sequential payload envelope exceeds data-shard cap",
9058 ));
9059 }
9060 }
9061 BlockKind::PayloadParity => {
9062 if metadata_seen {
9063 return Err(FormatError::InvalidArchive(
9064 "payload parity BlockRecord appears after metadata",
9065 ));
9066 }
9067 if pending.awaiting_tentative_parity {
9068 pending.awaiting_tentative_parity = false;
9069 pending.saw_last_data = true;
9070 } else if pending.data_shards.is_empty() || !pending.saw_last_data {
9071 return Err(FormatError::InvalidArchive(
9072 "payload parity appears before envelope data is complete",
9073 ));
9074 }
9075 pending.parity_shards.push(Some(record.payload));
9076 if pending.parity_shards.len() > parsed_crypto.fixed.fec_parity_shards as usize {
9077 return Err(FormatError::InvalidArchive(
9078 "sequential payload envelope exceeds parity-shard cap",
9079 ));
9080 }
9081 }
9082 _ => {
9083 if !pending.is_empty() {
9084 finalize_sequential_envelope(
9085 &mut pending,
9086 SequentialEnvelopeDecodeContext {
9087 crypto_header: &parsed_crypto.fixed,
9088 subkeys: &subkeys,
9089 volume_header: &volume_header,
9090 next_envelope_index: &mut next_envelope_index,
9091 tar_stream: &mut tar_stream,
9092 max_tar_stream_size,
9093 tar_stream_total_validator: &mut tar_stream_total_validator,
9094 },
9095 )?;
9096 }
9097 metadata_seen = true;
9098 }
9099 }
9100
9101 offset = checked_add(offset, record_len, "BlockRecord")?;
9102 }
9103
9104 if !pending.is_empty() {
9105 finalize_sequential_envelope(
9106 &mut pending,
9107 SequentialEnvelopeDecodeContext {
9108 crypto_header: &parsed_crypto.fixed,
9109 subkeys: &subkeys,
9110 volume_header: &volume_header,
9111 next_envelope_index: &mut next_envelope_index,
9112 tar_stream: &mut tar_stream,
9113 max_tar_stream_size,
9114 tar_stream_total_validator: &mut tar_stream_total_validator,
9115 },
9116 )?;
9117 }
9118
9119 parse_terminal_material(
9120 bytes,
9121 offset,
9122 observed_block_count,
9123 KeyHoldingTerminalContext {
9124 subkeys: &subkeys,
9125 volume_header: &volume_header,
9126 crypto_header: &parsed_crypto.fixed,
9127 crypto_header_bytes: crypto_bytes,
9128 },
9129 options,
9130 )?;
9131 validate_tar_stream_total_extraction_size(
9134 &tar_stream,
9135 parsed_crypto.fixed.max_path_length,
9136 total_extraction_cap,
9137 )?;
9138 Ok(tar_stream)
9139}
9140
9141fn validate_sequential_supported_volume(
9142 volume_header: &VolumeHeader,
9143 crypto_header: &CryptoHeaderFixed,
9144 extensions: &[ExtensionTlv<'_>],
9145) -> Result<(), FormatError> {
9146 reject_unsupported_raw_stream_profile(extensions)?;
9147 if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
9148 return Err(FormatError::ReaderUnsupported(
9149 "sequential reader supports only single-volume archive input",
9150 ));
9151 }
9152 if crypto_header.stripe_width != volume_header.stripe_width {
9153 return Err(FormatError::InvalidArchive(
9154 "VolumeHeader and CryptoHeader stripe_width differ",
9155 ));
9156 }
9157 if crypto_header.has_dictionary != 0 {
9158 return Err(FormatError::ReaderUnsupported(
9159 "dictionary bootstrap required for non-seekable sequential extraction",
9160 ));
9161 }
9162 Ok(())
9163}
9164
9165struct SequentialEnvelopeDecodeContext<'a> {
9166 crypto_header: &'a CryptoHeaderFixed,
9167 subkeys: &'a Subkeys,
9168 volume_header: &'a VolumeHeader,
9169 next_envelope_index: &'a mut u64,
9170 tar_stream: &'a mut Vec<u8>,
9171 max_tar_stream_size: usize,
9172 tar_stream_total_validator: &'a mut TarStreamTotalExtractionSizeValidator,
9173}
9174
9175fn finalize_sequential_envelope(
9176 pending: &mut PendingSequentialEnvelope,
9177 context: SequentialEnvelopeDecodeContext<'_>,
9178) -> Result<(), FormatError> {
9179 if !pending.saw_last_data {
9180 return Err(FormatError::InvalidArchive(
9181 "sequential payload envelope is missing last-data flag",
9182 ));
9183 }
9184 if pending.data_shards.len() > context.crypto_header.fec_data_shards as usize {
9185 return Err(FormatError::InvalidArchive(
9186 "sequential payload envelope exceeds data-shard cap",
9187 ));
9188 }
9189 if pending.parity_shards.len() > context.crypto_header.fec_parity_shards as usize {
9190 return Err(FormatError::InvalidArchive(
9191 "sequential payload envelope exceeds parity-shard cap",
9192 ));
9193 }
9194 let required_parity =
9195 required_object_parity(pending.data_shards.len() as u64, context.crypto_header)?;
9196 if pending.parity_shards.len() < required_parity as usize {
9197 return Err(FormatError::InvalidArchive(
9198 "sequential payload envelope has insufficient parity for recovery settings",
9199 ));
9200 }
9201
9202 let repaired = repair_data_gf16(
9203 &pending.data_shards,
9204 &pending.parity_shards,
9205 context.crypto_header.block_size as usize,
9206 )?;
9207 let mut encrypted =
9208 Vec::with_capacity(repaired.len() * context.crypto_header.block_size as usize);
9209 for shard in repaired {
9210 encrypted.extend_from_slice(&shard);
9211 }
9212 let plaintext = decrypt_padded_aead_object(
9213 AeadObjectContext {
9214 algo: context.crypto_header.aead_algo,
9215 key: &context.subkeys.enc_key,
9216 nonce_seed: &context.subkeys.nonce_seed,
9217 domain: b"envelope",
9218 archive_uuid: &context.volume_header.archive_uuid,
9219 session_id: &context.volume_header.session_id,
9220 counter: *context.next_envelope_index,
9221 },
9222 &encrypted,
9223 )?;
9224 decode_concatenated_zstd_frames_with_cap(
9225 &plaintext,
9226 None,
9227 context.tar_stream,
9228 context.max_tar_stream_size,
9229 Some(context.tar_stream_total_validator),
9230 )?;
9231 *context.next_envelope_index = (*context.next_envelope_index)
9232 .checked_add(1)
9233 .ok_or(FormatError::InvalidArchive("envelope counter overflow"))?;
9234 *pending = PendingSequentialEnvelope::default();
9235 Ok(())
9236}
9237
9238fn decode_concatenated_zstd_frames_with_cap(
9239 plaintext: &[u8],
9240 dictionary: Option<&[u8]>,
9241 output: &mut Vec<u8>,
9242 max_output_len: usize,
9243 mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
9244) -> Result<(), FormatError> {
9245 let mut cursor = 0usize;
9246 while cursor < plaintext.len() {
9247 let frame_len = zstd_safe::find_frame_compressed_size(&plaintext[cursor..])
9248 .map_err(|_| FormatError::InvalidZstdFrame)?;
9249 if frame_len == 0 {
9250 return Err(FormatError::InvalidZstdFrame);
9251 }
9252 let end = checked_add(cursor, frame_len, "zstd frame")?;
9253 validate_exact_zstd_frame(&plaintext[cursor..end])?;
9254 if let Some(dictionary) = dictionary {
9255 let mut decoder =
9256 zstd::stream::Decoder::with_dictionary(&plaintext[cursor..end], dictionary)
9257 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
9258 read_zstd_frame_to_capped_output(
9259 &mut decoder,
9260 output,
9261 max_output_len,
9262 tar_stream_total_validator.as_deref_mut(),
9263 )?;
9264 } else {
9265 let mut decoder = zstd::stream::Decoder::new(&plaintext[cursor..end])
9266 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
9267 read_zstd_frame_to_capped_output(
9268 &mut decoder,
9269 output,
9270 max_output_len,
9271 tar_stream_total_validator.as_deref_mut(),
9272 )?;
9273 }
9274 cursor = end;
9275 }
9276 Ok(())
9277}
9278
9279fn read_zstd_frame_to_capped_output<R: Read>(
9280 decoder: &mut R,
9281 output: &mut Vec<u8>,
9282 max_output_len: usize,
9283 mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
9284) -> Result<(), FormatError> {
9285 let mut buf = [0u8; 64 * 1024];
9286 loop {
9287 let read = decoder
9288 .read(&mut buf)
9289 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
9290 if read == 0 {
9291 return Ok(());
9292 }
9293 let next_len = output
9294 .len()
9295 .checked_add(read)
9296 .ok_or(FormatError::ReaderUnsupported(
9297 "sequential tar stream exceeds configured verification cap",
9298 ))?;
9299 if next_len > max_output_len {
9300 return Err(FormatError::ReaderUnsupported(
9301 "sequential tar stream exceeds configured verification cap",
9302 ));
9303 }
9304 output.extend_from_slice(&buf[..read]);
9305 if let Some(validator) = tar_stream_total_validator.as_mut() {
9306 validator.observe(output)?;
9307 }
9308 }
9309}
9310
9311fn load_archive_dictionary(
9312 blocks: &impl BlockProvider,
9313 subkeys: &Subkeys,
9314 volume_header: &VolumeHeader,
9315 crypto_header: &CryptoHeaderFixed,
9316 index_root: &IndexRoot,
9317) -> Result<Option<Vec<u8>>, FormatError> {
9318 if crypto_header.has_dictionary == 0 {
9319 return Ok(None);
9320 }
9321 let plaintext = load_metadata_object_from_parts(
9322 blocks,
9323 ObjectLoadContext::dictionary(volume_header, crypto_header, subkeys, index_root)?,
9324 index_root.header.dictionary_decompressed_size,
9325 )?;
9326 Ok(Some(plaintext))
9327}
9328
9329#[derive(Clone, Copy)]
9330struct ObjectLoadContext<'a> {
9331 volume_header: &'a VolumeHeader,
9332 crypto_header: &'a CryptoHeaderFixed,
9333 extent: ObjectExtent,
9334 data_kind: BlockKind,
9335 parity_kind: BlockKind,
9336 key: &'a [u8; 32],
9337 nonce_seed: &'a [u8; 32],
9338 domain: &'a [u8],
9339 counter: u64,
9340 class_data_shard_max: u16,
9341 class_parity_shard_max: u16,
9342}
9343
9344impl<'a> ObjectLoadContext<'a> {
9345 fn index_root(
9346 volume_header: &'a VolumeHeader,
9347 crypto_header: &'a CryptoHeaderFixed,
9348 subkeys: &'a Subkeys,
9349 extent: ObjectExtent,
9350 ) -> Self {
9351 Self {
9352 volume_header,
9353 crypto_header,
9354 extent,
9355 data_kind: BlockKind::IndexRootData,
9356 parity_kind: BlockKind::IndexRootParity,
9357 key: &subkeys.index_root_key,
9358 nonce_seed: &subkeys.index_nonce_seed,
9359 domain: b"idxroot",
9360 counter: 0,
9361 class_data_shard_max: crypto_header.index_root_fec_data_shards,
9362 class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
9363 }
9364 }
9365
9366 fn index_shard(
9367 volume_header: &'a VolumeHeader,
9368 crypto_header: &'a CryptoHeaderFixed,
9369 subkeys: &'a Subkeys,
9370 entry: &ShardEntry,
9371 ) -> Self {
9372 Self {
9373 volume_header,
9374 crypto_header,
9375 extent: ObjectExtent {
9376 first_block_index: entry.first_block_index,
9377 data_block_count: entry.data_block_count,
9378 parity_block_count: entry.parity_block_count,
9379 encrypted_size: entry.encrypted_size,
9380 },
9381 data_kind: BlockKind::IndexShardData,
9382 parity_kind: BlockKind::IndexShardParity,
9383 key: &subkeys.index_shard_key,
9384 nonce_seed: &subkeys.index_nonce_seed,
9385 domain: b"idxshard",
9386 counter: entry.shard_index,
9387 class_data_shard_max: crypto_header.index_fec_data_shards,
9388 class_parity_shard_max: crypto_header.index_fec_parity_shards,
9389 }
9390 }
9391
9392 fn directory_hint(
9393 volume_header: &'a VolumeHeader,
9394 crypto_header: &'a CryptoHeaderFixed,
9395 subkeys: &'a Subkeys,
9396 entry: &DirectoryHintShardEntry,
9397 ) -> Self {
9398 Self {
9399 volume_header,
9400 crypto_header,
9401 extent: ObjectExtent {
9402 first_block_index: entry.first_block_index,
9403 data_block_count: entry.data_block_count,
9404 parity_block_count: entry.parity_block_count,
9405 encrypted_size: entry.encrypted_size,
9406 },
9407 data_kind: BlockKind::DirectoryHintData,
9408 parity_kind: BlockKind::DirectoryHintParity,
9409 key: &subkeys.dir_hint_key,
9410 nonce_seed: &subkeys.index_nonce_seed,
9411 domain: b"dirhint",
9412 counter: entry.hint_shard_index,
9413 class_data_shard_max: crypto_header.index_fec_data_shards,
9414 class_parity_shard_max: crypto_header.index_fec_parity_shards,
9415 }
9416 }
9417
9418 fn dictionary(
9419 volume_header: &'a VolumeHeader,
9420 crypto_header: &'a CryptoHeaderFixed,
9421 subkeys: &'a Subkeys,
9422 index_root: &IndexRoot,
9423 ) -> Result<Self, FormatError> {
9424 Ok(Self {
9425 volume_header,
9426 crypto_header,
9427 extent: dictionary_extent_from_index_root(index_root)?,
9428 data_kind: BlockKind::DictionaryData,
9429 parity_kind: BlockKind::DictionaryParity,
9430 key: &subkeys.dictionary_key,
9431 nonce_seed: &subkeys.index_nonce_seed,
9432 domain: b"dict",
9433 counter: 0,
9434 class_data_shard_max: crypto_header.index_root_fec_data_shards,
9435 class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
9436 })
9437 }
9438
9439 fn payload(
9440 volume_header: &'a VolumeHeader,
9441 crypto_header: &'a CryptoHeaderFixed,
9442 subkeys: &'a Subkeys,
9443 envelope: &EnvelopeEntry,
9444 ) -> Self {
9445 Self {
9446 volume_header,
9447 crypto_header,
9448 extent: ObjectExtent {
9449 first_block_index: envelope.first_block_index,
9450 data_block_count: envelope.data_block_count,
9451 parity_block_count: envelope.parity_block_count,
9452 encrypted_size: envelope.encrypted_size,
9453 },
9454 data_kind: BlockKind::PayloadData,
9455 parity_kind: BlockKind::PayloadParity,
9456 key: &subkeys.enc_key,
9457 nonce_seed: &subkeys.nonce_seed,
9458 domain: b"envelope",
9459 counter: envelope.envelope_index,
9460 class_data_shard_max: crypto_header.fec_data_shards,
9461 class_parity_shard_max: crypto_header.fec_parity_shards,
9462 }
9463 }
9464}
9465
9466fn dictionary_extent_from_index_root(index_root: &IndexRoot) -> Result<ObjectExtent, FormatError> {
9467 if index_root.header.dictionary_data_block_count == 0
9468 || index_root.header.dictionary_encrypted_size == 0
9469 || index_root.header.dictionary_decompressed_size == 0
9470 {
9471 return Err(FormatError::InvalidArchive("dictionary bootstrap required"));
9472 }
9473 Ok(ObjectExtent {
9474 first_block_index: index_root.header.dictionary_first_block,
9475 data_block_count: index_root.header.dictionary_data_block_count,
9476 parity_block_count: index_root.header.dictionary_parity_block_count,
9477 encrypted_size: index_root.header.dictionary_encrypted_size,
9478 })
9479}
9480
9481fn load_metadata_object_from_parts(
9482 blocks: &impl BlockProvider,
9483 context: ObjectLoadContext<'_>,
9484 decompressed_size: u32,
9485) -> Result<Vec<u8>, FormatError> {
9486 let compressed = load_decrypted_object_from_parts(blocks, context)?;
9487 decompress_exact_zstd_frame(&compressed, decompressed_size as usize)
9488}
9489
9490fn load_decrypted_object_from_parts(
9491 blocks: &impl BlockProvider,
9492 context: ObjectLoadContext<'_>,
9493) -> Result<Vec<u8>, FormatError> {
9494 load_decrypted_object_from_parts_with_parity_policy(blocks, context, ParityReadPolicy::Always)
9495}
9496
9497fn load_decrypted_object_from_parts_with_parity_policy(
9498 blocks: &impl BlockProvider,
9499 context: ObjectLoadContext<'_>,
9500 parity_policy: ParityReadPolicy,
9501) -> Result<Vec<u8>, FormatError> {
9502 let repaired = load_repaired_object_data_shards_from_parts_with_parity_policy(
9503 blocks,
9504 context.crypto_header,
9505 context.extent,
9506 context.data_kind,
9507 context.parity_kind,
9508 context.class_data_shard_max,
9509 context.class_parity_shard_max,
9510 parity_policy,
9511 )?;
9512 let mut encrypted = Vec::with_capacity(context.extent.encrypted_size as usize);
9513 for shard in repaired {
9514 encrypted.extend_from_slice(&shard);
9515 }
9516 if encrypted.len() != context.extent.encrypted_size as usize {
9517 return Err(FormatError::InvalidArchive(
9518 "object encrypted size does not match repaired shards",
9519 ));
9520 }
9521
9522 decrypt_padded_aead_object(
9523 AeadObjectContext {
9524 algo: context.crypto_header.aead_algo,
9525 key: context.key,
9526 nonce_seed: context.nonce_seed,
9527 domain: context.domain,
9528 archive_uuid: &context.volume_header.archive_uuid,
9529 session_id: &context.volume_header.session_id,
9530 counter: context.counter,
9531 },
9532 &encrypted,
9533 )
9534}
9535
9536fn load_repaired_object_data_shards_from_parts(
9537 blocks: &impl BlockProvider,
9538 crypto_header: &CryptoHeaderFixed,
9539 extent: ObjectExtent,
9540 data_kind: BlockKind,
9541 parity_kind: BlockKind,
9542 class_data_shard_max: u16,
9543 class_parity_shard_max: u16,
9544) -> Result<Vec<Vec<u8>>, FormatError> {
9545 load_repaired_object_data_shards_from_parts_with_parity_policy(
9546 blocks,
9547 crypto_header,
9548 extent,
9549 data_kind,
9550 parity_kind,
9551 class_data_shard_max,
9552 class_parity_shard_max,
9553 ParityReadPolicy::Always,
9554 )
9555}
9556
9557#[allow(clippy::too_many_arguments)]
9558fn load_repaired_object_data_shards_from_parts_with_parity_policy(
9559 blocks: &impl BlockProvider,
9560 crypto_header: &CryptoHeaderFixed,
9561 extent: ObjectExtent,
9562 data_kind: BlockKind,
9563 parity_kind: BlockKind,
9564 class_data_shard_max: u16,
9565 class_parity_shard_max: u16,
9566 parity_policy: ParityReadPolicy,
9567) -> Result<Vec<Vec<u8>>, FormatError> {
9568 validate_object_extent(
9569 extent,
9570 crypto_header,
9571 class_data_shard_max,
9572 class_parity_shard_max,
9573 )?;
9574 let block_size = crypto_header.block_size as usize;
9575 let data_count = extent.data_block_count as usize;
9576 let parity_count = extent.parity_block_count as usize;
9577 let mut data_shards = Vec::with_capacity(data_count);
9578 let mut parity_shards = Vec::with_capacity(parity_count);
9579
9580 for offset in 0..data_count {
9581 let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
9582 if let Some(record) = blocks.block(block_index)? {
9583 if record.kind != data_kind {
9584 return Err(FormatError::InvalidArchive(
9585 "object data block has unexpected kind",
9586 ));
9587 }
9588 let should_be_last = offset + 1 == data_count;
9589 if record.is_last_data() != should_be_last {
9590 return Err(FormatError::InvalidArchive(
9591 "object last-data flag is not on the final data block",
9592 ));
9593 }
9594 data_shards.push(Some(record.payload.clone()));
9595 } else {
9596 data_shards.push(None);
9597 }
9598 }
9599
9600 if parity_policy == ParityReadPolicy::RepairOnly && data_shards.iter().all(Option::is_some) {
9601 return repair_data_gf16(&data_shards, &[], block_size);
9602 }
9603
9604 for offset in 0..parity_count {
9605 let block_index = checked_u64_add(
9606 extent.first_block_index,
9607 data_count as u64 + offset as u64,
9608 "object",
9609 )?;
9610 if let Some(record) = blocks.block(block_index)? {
9611 if record.kind != parity_kind {
9612 return Err(FormatError::InvalidArchive(
9613 "object parity block has unexpected kind",
9614 ));
9615 }
9616 if record.is_last_data() {
9617 return Err(FormatError::InvalidArchive(
9618 "object parity block has last-data flag",
9619 ));
9620 }
9621 parity_shards.push(Some(record.payload.clone()));
9622 } else {
9623 parity_shards.push(None);
9624 }
9625 }
9626
9627 repair_data_gf16(&data_shards, &parity_shards, block_size)
9628}
9629
9630fn validate_object_extent(
9631 extent: ObjectExtent,
9632 crypto_header: &CryptoHeaderFixed,
9633 class_data_shard_max: u16,
9634 class_parity_shard_max: u16,
9635) -> Result<(), FormatError> {
9636 if extent.data_block_count == 0 || extent.encrypted_size == 0 {
9637 return Err(FormatError::InvalidArchive(
9638 "encrypted object has zero data blocks or size",
9639 ));
9640 }
9641 if extent.data_block_count > class_data_shard_max as u32 {
9642 return Err(FormatError::InvalidArchive(
9643 "encrypted object exceeds its class data-shard maximum",
9644 ));
9645 }
9646 if extent.parity_block_count > class_parity_shard_max as u32 {
9647 return Err(FormatError::InvalidArchive(
9648 "encrypted object exceeds its class parity-shard maximum",
9649 ));
9650 }
9651 let required_parity = required_object_parity(extent.data_block_count as u64, crypto_header)?;
9652 if extent.parity_block_count != required_parity {
9653 return Err(FormatError::InvalidArchive(
9654 "encrypted object parity does not match v41 compute_parity",
9655 ));
9656 }
9657 let total = checked_u64_add(
9658 extent.data_block_count as u64,
9659 extent.parity_block_count as u64,
9660 "encrypted object shard count overflow",
9661 )?;
9662 if total > 65_535 {
9663 return Err(FormatError::FecTooManyShards(total as usize));
9664 }
9665 let expected = checked_u64_mul(
9666 extent.data_block_count as u64,
9667 crypto_header.block_size as u64,
9668 "encrypted object size overflow",
9669 )?;
9670 if expected != extent.encrypted_size as u64 {
9671 return Err(FormatError::InvalidArchive(
9672 "encrypted object size is not data_block_count * block_size",
9673 ));
9674 }
9675 if extent.encrypted_size as usize <= crypto_header.aead_algo.tag_len() {
9676 return Err(FormatError::InvalidArchive(
9677 "encrypted object is too small for AEAD tag",
9678 ));
9679 }
9680 Ok(())
9681}
9682
9683pub(crate) fn required_object_parity(
9684 data_block_count: u64,
9685 crypto_header: &CryptoHeaderFixed,
9686) -> Result<u32, FormatError> {
9687 let min_parity =
9688 if crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0 {
9689 1
9690 } else {
9691 0
9692 };
9693 let mut parity = 0u64;
9694 for _ in 0..100 {
9695 let total = data_block_count
9696 .checked_add(parity)
9697 .ok_or(FormatError::InvalidArchive("parity total overflow"))?;
9698 let by_volume = checked_u64_mul(
9699 crypto_header.volume_loss_tolerance as u64,
9700 ceil_div_u64(total, crypto_header.stripe_width as u64)?,
9701 "volume-loss parity overflow",
9702 )?;
9703 let by_bitrot = ceil_div_u64(
9704 checked_u64_mul(
9705 total,
9706 crypto_header.bit_rot_buffer_pct as u64,
9707 "bit-rot parity overflow",
9708 )?,
9709 100,
9710 )?;
9711 let next = by_volume
9712 .checked_add(by_bitrot)
9713 .ok_or(FormatError::InvalidArchive("parity overflow"))?
9714 .max(min_parity);
9715 if next == parity {
9716 return u32::try_from(next)
9717 .map_err(|_| FormatError::InvalidArchive("parity count overflow"));
9718 }
9719 parity = next;
9720 }
9721 Err(FormatError::InvalidArchive(
9722 "parity calculation did not converge",
9723 ))
9724}
9725
9726fn ceil_div_u64(numerator: u64, denominator: u64) -> Result<u64, FormatError> {
9727 if denominator == 0 {
9728 return Err(FormatError::InvalidArchive("division by zero"));
9729 }
9730 numerator
9731 .checked_add(denominator - 1)
9732 .ok_or(FormatError::InvalidArchive("ceiling division overflow"))
9733 .map(|value| value / denominator)
9734}
9735
9736fn frame_range_for_file<'b>(
9737 shard: &'b IndexShard,
9738 file: &FileEntry,
9739) -> Result<Vec<&'b FrameEntry>, FormatError> {
9740 let mut frames = Vec::with_capacity(file.frame_count as usize);
9741 for offset in 0..file.frame_count as u64 {
9742 let frame_index =
9743 file.first_frame_index
9744 .checked_add(offset)
9745 .ok_or(FormatError::InvalidArchive(
9746 "FileEntry frame range overflow",
9747 ))?;
9748 let frame = shard
9749 .frames
9750 .iter()
9751 .find(|entry| entry.frame_index == frame_index)
9752 .ok_or(FormatError::InvalidArchive(
9753 "FileEntry references missing FrameEntry",
9754 ))?;
9755 frames.push(frame);
9756 }
9757 Ok(frames)
9758}
9759
9760fn metadata_limits(crypto_header: &CryptoHeaderFixed) -> MetadataLimits {
9761 MetadataLimits {
9762 block_size: crypto_header.block_size,
9763 max_path_length: crypto_header.max_path_length,
9764 max_payload_data_shards: crypto_header.fec_data_shards,
9765 max_payload_parity_shards: crypto_header.fec_parity_shards,
9766 max_index_data_shards: crypto_header.index_fec_data_shards,
9767 max_index_parity_shards: crypto_header.index_fec_parity_shards,
9768 max_index_root_data_shards: crypto_header.index_root_fec_data_shards,
9769 max_index_root_parity_shards: crypto_header.index_root_fec_parity_shards,
9770 ..MetadataLimits::default()
9771 }
9772}
9773
9774fn verify_dense_keys<T>(
9775 entries: &BTreeMap<u64, T>,
9776 expected_count: u64,
9777 structure: &'static str,
9778) -> Result<(), FormatError> {
9779 if entries.len() as u64 != expected_count {
9780 return Err(FormatError::InvalidArchive(
9781 "decoded table count does not match IndexRoot",
9782 ));
9783 }
9784 for expected in 0..expected_count {
9785 if !entries.contains_key(&expected) {
9786 return Err(FormatError::InvalidMetadata {
9787 structure,
9788 reason: "global index coverage has a gap",
9789 });
9790 }
9791 }
9792 Ok(())
9793}
9794
9795fn validate_envelope_frame_coverage(
9796 frames: &BTreeMap<u64, FrameEntry>,
9797 envelopes: &BTreeMap<u64, EnvelopeEntry>,
9798) -> Result<(), FormatError> {
9799 let mut accounted_frames = BTreeSet::new();
9800 for envelope in envelopes.values() {
9801 let first = envelope.first_frame_index;
9802 let end =
9803 first
9804 .checked_add(envelope.frame_count as u64)
9805 .ok_or(FormatError::InvalidArchive(
9806 "EnvelopeEntry frame range overflow",
9807 ))?;
9808 let mut ranges = Vec::with_capacity(envelope.frame_count as usize);
9809 for frame_index in first..end {
9810 let frame = frames.get(&frame_index).ok_or(FormatError::InvalidArchive(
9811 "EnvelopeEntry references missing FrameEntry",
9812 ))?;
9813 if frame.envelope_index != envelope.envelope_index {
9814 return Err(FormatError::InvalidArchive(
9815 "FrameEntry envelope_index does not match containing EnvelopeEntry",
9816 ));
9817 }
9818 if !accounted_frames.insert(frame_index) {
9819 return Err(FormatError::InvalidArchive(
9820 "FrameEntry is covered by multiple EnvelopeEntries",
9821 ));
9822 }
9823 let start = frame.offset_in_envelope as usize;
9824 let end = checked_add(start, frame.compressed_size as usize, "FrameEntry")?;
9825 if end > envelope.plaintext_size as usize {
9826 return Err(FormatError::InvalidArchive(
9827 "FrameEntry exceeds EnvelopeEntry plaintext_size",
9828 ));
9829 }
9830 ranges.push((start, end));
9831 }
9832 validate_exact_coverage_ranges(
9833 &mut ranges,
9834 envelope.plaintext_size as usize,
9835 "EnvelopeEntry frame coverage has a gap or overlap",
9836 )?;
9837 }
9838
9839 for frame_index in frames.keys() {
9840 if !accounted_frames.contains(frame_index) {
9841 return Err(FormatError::InvalidArchive(
9842 "FrameEntry is not covered by any EnvelopeEntry",
9843 ));
9844 }
9845 }
9846 Ok(())
9847}
9848
9849fn validate_global_file_table_order(shards: &[IndexShard]) -> Result<(), FormatError> {
9850 let mut previous = None::<([u8; 8], Vec<u8>, u64)>;
9851 for shard in shards {
9852 for (idx, file) in shard.files.iter().enumerate() {
9853 let path = shard
9854 .file_path(idx)
9855 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?
9856 .to_vec();
9857 let start = shard
9858 .tar_member_group_start(idx)
9859 .ok_or(FormatError::InvalidArchive(
9860 "FileEntry tar member start is missing",
9861 ))?;
9862 let key = (file.path_hash, path, start);
9863 validate_global_file_table_key_step(previous.as_ref(), &key)?;
9864 previous = Some(key);
9865 }
9866 }
9867 Ok(())
9868}
9869
9870fn validate_global_file_table_key_step(
9871 previous: Option<&([u8; 8], Vec<u8>, u64)>,
9872 current: &([u8; 8], Vec<u8>, u64),
9873) -> Result<(), FormatError> {
9874 if let Some(previous) = previous {
9875 if previous >= current {
9876 return Err(FormatError::InvalidArchive(
9877 "global FileEntry rows are not sorted and unique",
9878 ));
9879 }
9880 }
9881 Ok(())
9882}
9883
9884fn validate_file_extent_coverage_ranges(
9885 extents: &[(u64, u64)],
9886 tar_len: u64,
9887) -> Result<(), FormatError> {
9888 let mut ranges = Vec::with_capacity(extents.len());
9889 for (start, len) in extents {
9890 let end = checked_u64_add(*start, *len, "FileEntry")?;
9891 if end > tar_len {
9892 return Err(FormatError::InvalidArchive(
9893 "FileEntry extent exceeds IndexRoot tar_total_size",
9894 ));
9895 }
9896 ranges.push((*start, end));
9897 }
9898 validate_exact_coverage_ranges_u64(
9899 &mut ranges,
9900 tar_len,
9901 "FileEntry extents do not cover tar stream exactly",
9902 )
9903}
9904
9905fn add_expected_directory_hint_rows(
9906 map: &mut DirectoryHintMap,
9907 shard_row_index: u32,
9908 path: &[u8],
9909 kind: TarEntryKind,
9910) {
9911 map.entry(Vec::new()).or_default().insert(shard_row_index);
9912 for (idx, byte) in path.iter().enumerate() {
9913 if *byte == b'/' {
9914 map.entry(path[..idx].to_vec())
9915 .or_default()
9916 .insert(shard_row_index);
9917 }
9918 }
9919 if kind == TarEntryKind::Directory {
9920 map.entry(path.to_vec())
9921 .or_default()
9922 .insert(shard_row_index);
9923 }
9924}
9925
9926fn validate_directory_hint_tables_against_expected(
9927 tables: &[DirectoryHintTable],
9928 expected: &DirectoryHintMap,
9929) -> Result<(), FormatError> {
9930 let mut actual = Vec::new();
9931 let mut previous_key: Option<([u8; 8], Vec<u8>)> = None;
9932
9933 for table in tables {
9934 for entry_index in 0..table.entries.len() {
9935 let path = table
9936 .entry_path(entry_index)
9937 .ok_or(FormatError::InvalidArchive(
9938 "DirectoryHintEntry path is missing",
9939 ))?;
9940 let key = (hash_prefix(path), path.to_vec());
9941 if let Some(previous) = &previous_key {
9942 if previous >= &key {
9943 return Err(FormatError::InvalidArchive(
9944 "DirectoryHintEntry rows are not globally sorted",
9945 ));
9946 }
9947 }
9948 previous_key = Some(key);
9949
9950 let rows =
9951 table
9952 .shard_rows_for_entry(entry_index)
9953 .ok_or(FormatError::InvalidArchive(
9954 "DirectoryHintEntry shard rows are missing",
9955 ))?;
9956 actual.push((path.to_vec(), rows.to_vec()));
9957 }
9958 }
9959
9960 if actual != sorted_directory_hint_rows(expected) {
9961 return Err(FormatError::InvalidArchive(
9962 "directory hint map does not match decoded files",
9963 ));
9964 }
9965 Ok(())
9966}
9967
9968fn sorted_directory_hint_rows(map: &DirectoryHintMap) -> Vec<(Vec<u8>, Vec<u32>)> {
9969 let mut rows = map
9970 .iter()
9971 .map(|(path, shard_rows)| {
9972 (
9973 path.clone(),
9974 shard_rows.iter().copied().collect::<Vec<u32>>(),
9975 )
9976 })
9977 .collect::<Vec<_>>();
9978 rows.sort_by(|(left_path, _), (right_path, _)| {
9979 hash_prefix(left_path)
9980 .cmp(&hash_prefix(right_path))
9981 .then_with(|| left_path.cmp(right_path))
9982 });
9983 rows
9984}
9985
9986fn validate_exact_coverage_ranges(
9987 ranges: &mut [(usize, usize)],
9988 expected_end: usize,
9989 reason: &'static str,
9990) -> Result<(), FormatError> {
9991 ranges.sort_unstable();
9992 let mut cursor = 0usize;
9993 for (start, end) in ranges.iter().copied() {
9994 if start != cursor || end < start {
9995 return Err(FormatError::InvalidArchive(reason));
9996 }
9997 cursor = end;
9998 }
9999 if cursor != expected_end {
10000 return Err(FormatError::InvalidArchive(reason));
10001 }
10002 Ok(())
10003}
10004
10005fn validate_exact_coverage_ranges_u64(
10006 ranges: &mut [(u64, u64)],
10007 expected_end: u64,
10008 reason: &'static str,
10009) -> Result<(), FormatError> {
10010 ranges.sort_unstable();
10011 let mut cursor = 0u64;
10012 for (start, end) in ranges.iter().copied() {
10013 if start != cursor || end < start {
10014 return Err(FormatError::InvalidArchive(reason));
10015 }
10016 cursor = end;
10017 }
10018 if cursor != expected_end {
10019 return Err(FormatError::InvalidArchive(reason));
10020 }
10021 Ok(())
10022}
10023
10024fn object_block_range(
10025 first_block_index: u64,
10026 data_block_count: u32,
10027 parity_block_count: u32,
10028 structure: &'static str,
10029) -> Result<(u64, u64), FormatError> {
10030 let total = data_block_count as u64 + parity_block_count as u64;
10031 if total == 0 {
10032 return Err(FormatError::InvalidArchive(structure));
10033 }
10034 let end = checked_u64_add(first_block_index, total, structure)?;
10035 Ok((first_block_index, end))
10036}
10037
10038fn validate_non_overlapping_object_ranges(ranges: &mut [(u64, u64)]) -> Result<(), FormatError> {
10039 ranges.sort_unstable();
10040 for pair in ranges.windows(2) {
10041 if pair[0].1 > pair[1].0 {
10042 return Err(FormatError::InvalidArchive(
10043 "encrypted object block ranges overlap",
10044 ));
10045 }
10046 }
10047 Ok(())
10048}
10049
10050pub(crate) fn observed_archive_size(
10051 sizes: impl IntoIterator<Item = u64>,
10052) -> Result<u64, FormatError> {
10053 sizes.into_iter().try_fold(0u64, |sum, size| {
10054 sum.checked_add(size).ok_or(FormatError::InvalidArchive(
10055 "observed archive size overflow",
10056 ))
10057 })
10058}
10059
10060pub(crate) fn total_extraction_size_cap(
10061 options: ReaderOptions,
10062 observed_archive_bytes: u64,
10063) -> u64 {
10064 options
10065 .max_total_extraction_size
10066 .min(observed_archive_bytes.saturating_mul(10))
10067}
10068
10069fn utf8_path(bytes: &[u8]) -> Result<String, FormatError> {
10070 std::str::from_utf8(bytes)
10071 .map(|path| path.to_owned())
10072 .map_err(|_| FormatError::UnsafeArchivePath)
10073}
10074
10075fn manifest_footer_global_pre_hmac_bytes(manifest_footer: &ManifestFooter) -> [u8; 104] {
10076 let mut bytes = [0u8; 104];
10077 bytes.copy_from_slice(&manifest_footer.to_bytes()[..104]);
10078 bytes[36..40].fill(0);
10079 bytes
10080}
10081
10082fn sha256_bytes(bytes: &[u8]) -> [u8; 32] {
10083 let digest = Sha256::digest(bytes);
10084 let mut out = [0u8; 32];
10085 out.copy_from_slice(&digest);
10086 out
10087}
10088
10089fn slice<'b>(
10090 bytes: &'b [u8],
10091 offset: usize,
10092 len: usize,
10093 structure: &'static str,
10094) -> Result<&'b [u8], FormatError> {
10095 let end = checked_add(offset, len, structure)?;
10096 bytes.get(offset..end).ok_or(FormatError::InvalidLength {
10097 structure,
10098 expected: end,
10099 actual: bytes.len(),
10100 })
10101}
10102
10103fn read_at_vec(
10104 reader: &dyn ArchiveReadAt,
10105 offset: u64,
10106 len: usize,
10107 structure: &'static str,
10108) -> Result<Vec<u8>, FormatError> {
10109 let expected_end = offset
10110 .checked_add(len as u64)
10111 .ok_or(FormatError::InvalidArchive("archive read range overflow"))?;
10112 let observed_len = reader.len()?;
10113 if expected_end > observed_len {
10114 return Err(FormatError::InvalidLength {
10115 structure,
10116 expected: to_usize(expected_end, structure)?,
10117 actual: to_usize(observed_len, structure)?,
10118 });
10119 }
10120 let mut out = vec![0u8; len];
10121 reader.read_exact_at(offset, &mut out)?;
10122 Ok(out)
10123}
10124
10125fn read_at_vec_unchecked(
10126 reader: &dyn ArchiveReadAt,
10127 offset: u64,
10128 len: usize,
10129) -> Result<Vec<u8>, FormatError> {
10130 let mut out = vec![0u8; len];
10131 reader.read_exact_at(offset, &mut out)?;
10132 Ok(out)
10133}
10134
10135fn parallel_map_ref<T, U, F>(items: &[T], jobs: usize, f: F) -> Result<Vec<U>, FormatError>
10136where
10137 T: Sync,
10138 U: Send,
10139 F: Fn(&T) -> Result<U, FormatError> + Sync,
10140{
10141 if jobs <= 1 || items.len() <= 1 {
10142 return items.iter().map(f).collect();
10143 }
10144 let worker_count = jobs.min(items.len());
10145 let chunk_size = items.len().div_ceil(worker_count);
10146 let mut out = Vec::with_capacity(items.len());
10147 thread::scope(|scope| {
10148 let handles = items
10149 .chunks(chunk_size)
10150 .map(|chunk| scope.spawn(|| chunk.iter().map(&f).collect::<Result<Vec<_>, _>>()))
10151 .collect::<Vec<_>>();
10152 for handle in handles {
10153 let mut chunk = handle
10154 .join()
10155 .map_err(|_| FormatError::InvalidArchive("reader worker panicked"))??;
10156 out.append(&mut chunk);
10157 }
10158 Ok(out)
10159 })
10160}
10161
10162fn checked_add(lhs: usize, rhs: usize, structure: &'static str) -> Result<usize, FormatError> {
10163 lhs.checked_add(rhs)
10164 .ok_or(FormatError::InvalidArchive(structure))
10165}
10166
10167fn checked_u64_add(lhs: u64, rhs: u64, structure: &'static str) -> Result<u64, FormatError> {
10168 lhs.checked_add(rhs)
10169 .ok_or(FormatError::InvalidArchive(structure))
10170}
10171
10172fn to_usize(value: u64, structure: &'static str) -> Result<usize, FormatError> {
10173 usize::try_from(value).map_err(|_| FormatError::InvalidArchive(structure))
10174}
10175
10176#[cfg(test)]
10177mod tests {
10178 use std::fs;
10179
10180 use super::*;
10181 use crate::compression::compress_zstd_frame;
10182 use crate::crypto::{compute_hmac, encrypt_padded_aead_object};
10183 use crate::fec::encode_parity_gf16;
10184 use crate::format::{
10185 AeadAlgo, CompressionAlgo, FecAlgo, KdfAlgo, CRYPTO_EXTENSION_HEADER_LEN,
10186 CRYPTO_HEADER_FIXED_LEN, FORMAT_VERSION, READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
10187 VOLUME_FORMAT_REV, VOLUME_FORMAT_REV_43, VOLUME_FORMAT_REV_44,
10188 };
10189 use crate::metadata::{
10190 DirectoryHintEntry, DirectoryHintTableHeader, IndexRootHeader, IndexShardHeader,
10191 ENVELOPE_ENTRY_LEN, FILE_ENTRY_LEN, FRAME_ENTRY_LEN, INDEX_SHARD_HEADER_LEN,
10192 };
10193 use crate::non_seekable_reader::{
10194 extract_non_seekable_stream_to_dir, list_non_seekable_stream, verify_non_seekable_stream,
10195 verify_non_seekable_stream_with_bootstrap_sidecar, verify_non_seekable_stream_with_options,
10196 verify_non_seekable_stream_with_recipient_wrap_resolver_options, NonSeekableReaderOptions,
10197 SequentialRootAuthStatus,
10198 };
10199 use crate::raw_stream_profile::{
10200 serialize_raw_stream_content_model_extension, RAW_STREAM_UNSUPPORTED_MESSAGE,
10201 };
10202 use crate::wire::RecipientRecordV1;
10203 use crate::writer::{
10204 write_archive, write_archive_unencrypted, write_archive_with_dictionary,
10205 write_archive_with_kdf, write_archive_with_recipient_wrap_records,
10206 write_archive_with_root_auth, write_archive_with_root_auth_and_recipient_wrap_records,
10207 RegularFile, RootAuthSigningRequest, RootAuthWriterConfig, WriterOptions,
10208 };
10209
10210 fn master_key() -> MasterKey {
10211 MasterKey::from_raw_key(&[0x42; 32]).unwrap()
10212 }
10213
10214 fn recipient_wrap_test_record() -> RecipientRecordV1 {
10215 RecipientRecordV1 {
10216 record_length: 0,
10217 profile_id: 1,
10218 recipient_identity_type: 2,
10219 flags: 0,
10220 recipient_identity_length: 0,
10221 profile_payload_length: 0,
10222 recipient_identity_digest: [0u8; 32],
10223 recipient_identity_bytes: b"recipient-a".to_vec(),
10224 profile_payload_bytes: b"profile-payload".to_vec(),
10225 }
10226 }
10227
10228 fn recipient_wrap_layout(volume: &[u8]) -> (usize, usize, usize, usize, u32) {
10229 let header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
10230 let crypto_start = header.crypto_header_offset as usize;
10231 let crypto_len = header.crypto_header_length as usize;
10232 let crypto_end = crypto_start + crypto_len;
10233 let crypto = CryptoHeader::parse(
10234 &volume[crypto_start..crypto_end],
10235 header.crypto_header_length,
10236 )
10237 .unwrap();
10238 let KdfParams::RecipientWrap {
10239 key_wrap_table_length,
10240 ..
10241 } = crypto.kdf_params
10242 else {
10243 panic!("expected RecipientWrap KdfParams");
10244 };
10245 (
10246 crypto_start,
10247 crypto_len,
10248 crypto_end,
10249 key_wrap_table_length as usize,
10250 key_wrap_table_length,
10251 )
10252 }
10253
10254 fn rewrite_recipient_wrap_kdf_digest(crypto_bytes: &mut [u8], digest: [u8; 32]) {
10255 let digest_start = CRYPTO_HEADER_FIXED_LEN + 14;
10256 crypto_bytes[digest_start..digest_start + 32].copy_from_slice(&digest);
10257 }
10258
10259 fn mutate_top_level_recipient_wrap_public_profile(volume: &mut [u8]) {
10260 let (crypto_start, crypto_len, table_start, table_len, table_len_u32) =
10261 recipient_wrap_layout(volume);
10262 let table_end = table_start + table_len;
10263 volume[table_end - 1] ^= 0x5a;
10264 let digest = compute_key_wrap_table_digest(table_len_u32, &volume[table_start..table_end]);
10265 rewrite_recipient_wrap_kdf_digest(
10266 &mut volume[crypto_start..crypto_start + crypto_len],
10267 digest,
10268 );
10269 }
10270
10271 fn mutate_cmra_recipient_wrap_public_profile(volume: &mut [u8]) {
10272 rewrite_public_cmra_image(volume, |image| {
10273 let table_region = image
10274 .regions
10275 .iter_mut()
10276 .find(|region| region.region_type == 6)
10277 .unwrap();
10278 *table_region.bytes.last_mut().unwrap() ^= 0x5a;
10279 let digest =
10280 compute_key_wrap_table_digest(table_region.bytes.len() as u32, &table_region.bytes);
10281 let crypto_region = image
10282 .regions
10283 .iter_mut()
10284 .find(|region| region.region_type == 2)
10285 .unwrap();
10286 rewrite_recipient_wrap_kdf_digest(&mut crypto_region.bytes, digest);
10287 });
10288 }
10289
10290 fn add_raw_stream_profile_to_physical_crypto_header(volume: &mut Vec<u8>) {
10291 let mut header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
10292 let crypto_start = header.crypto_header_offset as usize;
10293 let crypto_len = header.crypto_header_length as usize;
10294 let hmac_start = crypto_start + crypto_len - CRYPTO_HEADER_HMAC_LEN;
10295 let terminator_start = hmac_start - CRYPTO_EXTENSION_HEADER_LEN;
10296 let extension = serialize_raw_stream_content_model_extension();
10297 let new_crypto_len = header.crypto_header_length + extension.len() as u32;
10298
10299 volume.splice(terminator_start..terminator_start, extension);
10300 header.crypto_header_length = new_crypto_len;
10301 volume[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
10302 volume[crypto_start + 4..crypto_start + 8].copy_from_slice(&new_crypto_len.to_le_bytes());
10303 }
10304
10305 fn recompute_physical_crypto_header_hmac(volume: &mut [u8], master_key: &MasterKey) {
10306 let header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
10307 let crypto_start = header.crypto_header_offset as usize;
10308 let crypto_end = crypto_start + header.crypto_header_length as usize;
10309 let hmac_start = crypto_end - CRYPTO_HEADER_HMAC_LEN;
10310 let subkeys =
10311 Subkeys::derive(master_key, &header.archive_uuid, &header.session_id).unwrap();
10312 let hmac = compute_hmac(
10313 HmacDomain::CryptoHeader,
10314 &subkeys.mac_key,
10315 &header.archive_uuid,
10316 &header.session_id,
10317 &volume[crypto_start..hmac_start],
10318 );
10319 volume[hmac_start..crypto_end].copy_from_slice(&hmac);
10320 }
10321
10322 #[test]
10323 fn reader_defaults_use_available_parallelism_jobs() {
10324 let options = ReaderOptions::default();
10325
10326 assert_eq!(options.jobs, default_jobs());
10327 assert!(options.jobs >= 1);
10328 }
10329
10330 #[test]
10331 fn reader_options_reject_zero_jobs() {
10332 let err = OpenedArchive::open_with_options(
10333 &[],
10334 &master_key(),
10335 ReaderOptions {
10336 jobs: 0,
10337 ..ReaderOptions::default()
10338 },
10339 )
10340 .unwrap_err();
10341
10342 assert_eq!(
10343 err,
10344 FormatError::ReaderUnsupported("jobs must be at least 1")
10345 );
10346 }
10347
10348 const TEST_ROOT_AUTH_ID: u16 = 0xe001;
10349 const TEST_ROOT_AUTH_VALUE_LEN: u32 = 32;
10350
10351 fn test_root_auth_config() -> RootAuthWriterConfig<'static> {
10352 RootAuthWriterConfig {
10353 authenticator_id: TEST_ROOT_AUTH_ID,
10354 signer_identity_type: 0,
10355 signer_identity: &[],
10356 authenticator_value_length: TEST_ROOT_AUTH_VALUE_LEN,
10357 }
10358 }
10359
10360 fn test_root_auth_value(request: &RootAuthSigningRequest) -> Vec<u8> {
10361 request.archive_root.to_vec()
10362 }
10363
10364 fn test_root_auth_verifies(footer: &RootAuthFooterV1, archive_root: &[u8; 32]) -> bool {
10365 footer.authenticator_id == TEST_ROOT_AUTH_ID
10366 && footer.signer_identity_type == 0
10367 && footer.signer_identity_bytes.is_empty()
10368 && footer.authenticator_value.as_slice() == archive_root
10369 }
10370
10371 fn dictionary() -> &'static [u8] {
10372 b"dir/dict.txt common words common words common words dictionary payload"
10373 }
10374
10375 #[derive(Clone)]
10376 struct CountingReadAt {
10377 bytes: std::sync::Arc<Vec<u8>>,
10378 reads: std::sync::Arc<std::sync::Mutex<Vec<(u64, u64)>>>,
10379 denied_ranges: std::sync::Arc<Vec<(u64, u64)>>,
10380 }
10381
10382 impl CountingReadAt {
10383 fn new(bytes: Vec<u8>, denied_ranges: Vec<(u64, u64)>) -> Self {
10384 Self {
10385 bytes: std::sync::Arc::new(bytes),
10386 reads: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
10387 denied_ranges: std::sync::Arc::new(denied_ranges),
10388 }
10389 }
10390
10391 fn reads(&self) -> Vec<(u64, u64)> {
10392 self.reads.lock().unwrap().clone()
10393 }
10394 }
10395
10396 impl ArchiveReadAt for CountingReadAt {
10397 fn len(&self) -> Result<u64, FormatError> {
10398 u64::try_from(self.bytes.as_ref().len())
10399 .map_err(|_| FormatError::InvalidArchive("archive length overflow"))
10400 }
10401
10402 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
10403 let end = checked_u64_add(offset, buf.len() as u64, "archive read range overflow")?;
10404 self.reads.lock().unwrap().push((offset, end));
10405 if self
10406 .denied_ranges
10407 .iter()
10408 .any(|(start, limit)| ranges_overlap(offset, end, *start, *limit))
10409 {
10410 return Err(FormatError::InvalidArchive("denied test read"));
10411 }
10412 let start = to_usize(offset, "archive")?;
10413 let end_usize = checked_add(start, buf.len(), "archive")?;
10414 let source = self
10415 .bytes
10416 .get(start..end_usize)
10417 .ok_or(FormatError::InvalidLength {
10418 structure: "archive",
10419 expected: end_usize,
10420 actual: self.bytes.as_ref().len(),
10421 })?;
10422 buf.copy_from_slice(source);
10423 Ok(())
10424 }
10425 }
10426
10427 fn ranges_overlap(left_start: u64, left_end: u64, right_start: u64, right_end: u64) -> bool {
10428 left_start < right_end && right_start < left_end
10429 }
10430
10431 fn single_stream_options() -> WriterOptions {
10432 WriterOptions {
10433 stripe_width: 1,
10434 volume_loss_tolerance: 0,
10435 ..WriterOptions::default()
10436 }
10437 }
10438
10439 struct ChunkedReader {
10440 bytes: Vec<u8>,
10441 cursor: usize,
10442 max_chunk: usize,
10443 }
10444
10445 impl ChunkedReader {
10446 fn new(bytes: Vec<u8>, max_chunk: usize) -> Self {
10447 Self {
10448 bytes,
10449 cursor: 0,
10450 max_chunk,
10451 }
10452 }
10453 }
10454
10455 impl std::io::Read for ChunkedReader {
10456 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
10457 if self.cursor >= self.bytes.len() {
10458 return Ok(0);
10459 }
10460 let available = self.bytes.len() - self.cursor;
10461 let len = available.min(buf.len()).min(self.max_chunk);
10462 buf[..len].copy_from_slice(&self.bytes[self.cursor..self.cursor + len]);
10463 self.cursor += len;
10464 Ok(len)
10465 }
10466 }
10467
10468 #[test]
10469 fn global_file_table_key_step_rejects_distinct_path_regression() {
10470 let previous = ([1u8; 8], b"b.txt".to_vec(), 0);
10471 let current = ([1u8; 8], b"a.txt".to_vec(), 0);
10472
10473 assert_eq!(
10474 validate_global_file_table_key_step(Some(&previous), ¤t).unwrap_err(),
10475 FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
10476 );
10477 }
10478
10479 #[test]
10480 fn global_file_table_key_step_rejects_duplicate_full_key() {
10481 let previous = ([1u8; 8], b"a.txt".to_vec(), 7);
10482 let current = ([1u8; 8], b"a.txt".to_vec(), 7);
10483
10484 assert_eq!(
10485 validate_global_file_table_key_step(Some(&previous), ¤t).unwrap_err(),
10486 FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
10487 );
10488 }
10489
10490 fn small_block_recovery_options() -> WriterOptions {
10491 WriterOptions {
10492 block_size: 4096,
10493 chunk_size: 32 * 1024,
10494 envelope_target_size: 32 * 1024,
10495 stripe_width: 1,
10496 volume_loss_tolerance: 0,
10497 bit_rot_buffer_pct: 1,
10498 fec_data_shards: 16,
10499 fec_parity_shards: 1,
10500 index_fec_data_shards: 4,
10501 index_fec_parity_shards: 1,
10502 index_root_fec_data_shards: 16,
10503 index_root_fec_parity_shards: 1,
10504 ..WriterOptions::default()
10505 }
10506 }
10507
10508 fn parity_rich_recovery_options() -> WriterOptions {
10509 WriterOptions {
10510 block_size: 4096,
10511 chunk_size: 32 * 1024,
10512 envelope_target_size: 32 * 1024,
10513 stripe_width: 1,
10514 volume_loss_tolerance: 0,
10515 bit_rot_buffer_pct: 40,
10516 fec_data_shards: 16,
10517 fec_parity_shards: 16,
10518 index_fec_data_shards: 4,
10519 index_fec_parity_shards: 4,
10520 index_root_fec_data_shards: 16,
10521 index_root_fec_parity_shards: 16,
10522 ..WriterOptions::default()
10523 }
10524 }
10525
10526 fn pseudo_random_bytes(len: usize) -> Vec<u8> {
10527 let mut state = 0x1234_5678u32;
10528 (0..len)
10529 .map(|_| {
10530 state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
10531 (state >> 24) as u8
10532 })
10533 .collect()
10534 }
10535
10536 #[test]
10537 fn opens_lists_verifies_and_extracts_one_file_archive() {
10538 let archive = write_archive(
10539 &[RegularFile::new("dir/hello.txt", b"hello m7")],
10540 &master_key(),
10541 single_stream_options(),
10542 )
10543 .unwrap();
10544 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10545
10546 assert_eq!(
10547 opened.list_files().unwrap(),
10548 vec![ArchiveEntry {
10549 path: "dir/hello.txt".to_string(),
10550 file_data_size: 8,
10551 kind: TarEntryKind::Regular,
10552 mode: 0o644,
10553 mtime: 0,
10554 diagnostics: Vec::new(),
10555 }]
10556 );
10557 opened.verify().unwrap();
10558 assert_eq!(
10559 opened.extract_file("dir/hello.txt").unwrap(),
10560 Some(b"hello m7".to_vec())
10561 );
10562 assert_eq!(opened.extract_file("missing.txt").unwrap(), None);
10563 }
10564
10565 #[test]
10566 fn root_auth_archive_round_trips_and_verifies_with_callback() {
10567 let archive = write_archive_with_root_auth(
10568 &[RegularFile::new("signed.txt", b"root-auth payload")],
10569 &master_key(),
10570 single_stream_options(),
10571 RootAuthWriterConfig {
10572 authenticator_id: 0x7777,
10573 signer_identity_type: 1,
10574 signer_identity: b"test signer",
10575 authenticator_value_length: 32,
10576 },
10577 |request| Ok(request.archive_root.to_vec()),
10578 )
10579 .unwrap();
10580
10581 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10582 opened.verify().unwrap();
10583 let verified = opened
10584 .verify_root_auth_with(|footer, archive_root| {
10585 Ok(footer.authenticator_value == archive_root.as_slice())
10586 })
10587 .unwrap();
10588
10589 assert_eq!(verified.authenticator_id, 0x7777);
10590 assert_eq!(verified.signer_identity_type, 1);
10591 assert_eq!(verified.signer_identity_bytes, b"test signer");
10592 assert_eq!(
10593 verified.archive_root,
10594 opened.root_auth_footer.as_ref().unwrap().archive_root
10595 );
10596 assert_eq!(
10597 verified.diagnostics,
10598 vec![
10599 RootAuthDiagnostic::RootAuthContentVerified,
10600 RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
10601 RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
10602 RootAuthDiagnostic::RecoveryMarginUnchecked,
10603 ]
10604 );
10605 }
10606
10607 #[test]
10608 fn root_auth_rejects_fast_content_verification_token() {
10609 let archive = write_archive_with_root_auth(
10610 &[RegularFile::new("signed.txt", b"root-auth payload")],
10611 &master_key(),
10612 single_stream_options(),
10613 RootAuthWriterConfig {
10614 authenticator_id: 0x7777,
10615 signer_identity_type: 1,
10616 signer_identity: b"test signer",
10617 authenticator_value_length: 32,
10618 },
10619 |request| Ok(request.archive_root.to_vec()),
10620 )
10621 .unwrap();
10622
10623 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10624 let content_verification = opened.verify_content_fast().unwrap();
10625 assert_eq!(
10626 opened
10627 .verify_root_auth_with_verified_content(&content_verification, |_, _| Ok(true))
10628 .unwrap_err(),
10629 FormatError::ReaderUnsupported(
10630 "RootAuth verification requires full archive content verification"
10631 )
10632 );
10633 }
10634
10635 #[test]
10636 fn root_auth_verification_requires_authenticator_success() {
10637 let archive = write_archive_with_root_auth(
10638 &[RegularFile::new("signed.txt", b"root-auth payload")],
10639 &master_key(),
10640 single_stream_options(),
10641 RootAuthWriterConfig {
10642 authenticator_id: 9,
10643 signer_identity_type: 1,
10644 signer_identity: b"test signer",
10645 authenticator_value_length: 32,
10646 },
10647 |request| Ok(request.archive_root.to_vec()),
10648 )
10649 .unwrap();
10650 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10651
10652 assert_eq!(
10653 opened.verify_root_auth_with(|_, _| Ok(false)).unwrap_err(),
10654 FormatError::InvalidArchive("root-auth authenticator verification failed")
10655 );
10656 }
10657
10658 #[test]
10659 fn public_no_key_verifies_encrypted_data_block_commitment_with_callback() {
10660 let archive = write_archive_with_root_auth(
10661 &[RegularFile::new("public.txt", b"public commitment")],
10662 &master_key(),
10663 single_stream_options(),
10664 RootAuthWriterConfig {
10665 authenticator_id: 0x2222,
10666 signer_identity_type: 1,
10667 signer_identity: b"public verifier",
10668 authenticator_value_length: 32,
10669 },
10670 |request| Ok(request.archive_root.to_vec()),
10671 )
10672 .unwrap();
10673
10674 let verified = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
10675 Ok(footer.authenticator_value == archive_root.as_slice())
10676 })
10677 .unwrap();
10678
10679 assert_eq!(verified.authenticator_id, 0x2222);
10680 assert_eq!(verified.signer_identity_bytes, b"public verifier");
10681 assert!(verified.total_data_block_count > 0);
10682 }
10683
10684 #[test]
10685 fn public_no_key_verifier_not_invoked_for_future_revision() {
10686 let archive = write_archive_with_root_auth(
10687 &[RegularFile::new("public.txt", b"public callback")],
10688 &master_key(),
10689 single_stream_options(),
10690 RootAuthWriterConfig {
10691 authenticator_id: 0x2222,
10692 signer_identity_type: 1,
10693 signer_identity: b"public verifier",
10694 authenticator_value_length: 32,
10695 },
10696 |request| Ok(request.archive_root.to_vec()),
10697 )
10698 .unwrap();
10699 let mut bytes = archive.bytes;
10700 let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
10701 header.volume_format_rev = 45;
10702 bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
10703
10704 let mut called = false;
10705 let err = public_no_key_verify_archive_with(&bytes, |_, _| {
10706 called = true;
10707 Ok(true)
10708 })
10709 .unwrap_err();
10710
10711 assert!(!called);
10712 assert_eq!(
10713 err,
10714 FormatError::UnsupportedVolumeFormatRevision {
10715 format_version: FORMAT_VERSION,
10716 volume_format_rev: 45,
10717 reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
10718 }
10719 );
10720 }
10721
10722 #[test]
10723 fn public_no_key_rejects_public_header_revision_mismatch() {
10724 let archive = write_archive_with_root_auth(
10725 &[RegularFile::new("public.txt", b"public v44 only")],
10726 &master_key(),
10727 single_stream_options(),
10728 RootAuthWriterConfig {
10729 authenticator_id: 0x2222,
10730 signer_identity_type: 1,
10731 signer_identity: b"public verifier",
10732 authenticator_value_length: 32,
10733 },
10734 |request| Ok(request.archive_root.to_vec()),
10735 )
10736 .unwrap();
10737 let mut bytes = archive.bytes;
10738 let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
10739 header.volume_format_rev = VOLUME_FORMAT_REV_43;
10740 bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
10741
10742 let mut called = false;
10743 let err = public_no_key_verify_archive_with(&bytes, |_, _| {
10744 called = true;
10745 Ok(true)
10746 })
10747 .unwrap_err();
10748
10749 assert!(!called);
10750 assert_eq!(
10751 err,
10752 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
10753 );
10754 }
10755
10756 #[test]
10757 fn public_no_key_rejects_recovered_footer_revision_mismatch() {
10758 let archive = write_archive_with_root_auth(
10759 &[RegularFile::new("public.txt", b"public footer mismatch")],
10760 &master_key(),
10761 single_stream_options(),
10762 RootAuthWriterConfig {
10763 authenticator_id: 0x2222,
10764 signer_identity_type: 1,
10765 signer_identity: b"public verifier",
10766 authenticator_value_length: 32,
10767 },
10768 |request| Ok(request.archive_root.to_vec()),
10769 )
10770 .unwrap();
10771 let mut bytes = archive.bytes;
10772 rewrite_public_cmra_image(&mut bytes, |image| {
10773 let root_auth_region = image
10774 .regions
10775 .iter_mut()
10776 .find(|region| region.region_type == 4)
10777 .unwrap();
10778 let mut footer = RootAuthFooterV1::parse(&root_auth_region.bytes).unwrap();
10779 footer.volume_format_rev = VOLUME_FORMAT_REV_43;
10780 root_auth_region.bytes = footer.to_bytes().unwrap();
10781 });
10782
10783 let mut called = false;
10784 let err = public_no_key_verify_archive_with(&bytes, |_, _| {
10785 called = true;
10786 Ok(true)
10787 })
10788 .unwrap_err();
10789
10790 assert!(!called);
10791 assert_eq!(
10792 err,
10793 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
10794 );
10795 }
10796
10797 #[test]
10798 fn public_no_key_rejects_recovered_image_revision_mismatch() {
10799 let archive = write_archive_with_root_auth(
10800 &[RegularFile::new("public.txt", b"public image mismatch")],
10801 &master_key(),
10802 single_stream_options(),
10803 RootAuthWriterConfig {
10804 authenticator_id: 0x2222,
10805 signer_identity_type: 1,
10806 signer_identity: b"public verifier",
10807 authenticator_value_length: 32,
10808 },
10809 |request| Ok(request.archive_root.to_vec()),
10810 )
10811 .unwrap();
10812 let bytes = rewrite_cmra_image_variable_len(
10813 &archive.bytes,
10814 CmraRecoveryMode::PublicNoKey,
10815 |image| {
10816 image.volume_format_rev = VOLUME_FORMAT_REV_43;
10817 },
10818 );
10819
10820 let mut called = false;
10821 let err = public_no_key_verify_archive_with(&bytes, |_, _| {
10822 called = true;
10823 Ok(true)
10824 })
10825 .unwrap_err();
10826
10827 assert!(!called);
10828 assert_eq!(
10829 err,
10830 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
10831 );
10832 }
10833
10834 #[test]
10835 fn public_no_key_ignores_untrusted_manifest_and_trailer_block_count_fields() {
10836 let archive = write_archive_with_root_auth(
10837 &[RegularFile::new(
10838 "public-fields.txt",
10839 b"public source authority",
10840 )],
10841 &master_key(),
10842 single_stream_options(),
10843 RootAuthWriterConfig {
10844 authenticator_id: 0x2222,
10845 signer_identity_type: 1,
10846 signer_identity: b"public verifier",
10847 authenticator_value_length: 32,
10848 },
10849 |request| Ok(request.archive_root.to_vec()),
10850 )
10851 .unwrap();
10852 let mut bytes = archive.bytes.clone();
10853
10854 rewrite_public_cmra_image(&mut bytes, |image| {
10855 let manifest_region = image
10856 .regions
10857 .iter_mut()
10858 .find(|region| region.region_type == 3)
10859 .unwrap();
10860 manifest_region.bytes[44..48].copy_from_slice(&99u32.to_le_bytes());
10861
10862 let trailer_region = image
10863 .regions
10864 .iter_mut()
10865 .find(|region| region.region_type == 5)
10866 .unwrap();
10867 let mut trailer = VolumeTrailer::parse(&trailer_region.bytes).unwrap();
10868 trailer.block_count += 7;
10869 trailer_region.bytes = trailer.to_bytes().to_vec();
10870 });
10871
10872 public_no_key_verify_archive_with(&bytes, |footer, archive_root| {
10873 Ok(footer.authenticator_value == archive_root.as_slice())
10874 })
10875 .unwrap();
10876 }
10877
10878 #[test]
10879 fn public_no_key_compares_only_public_crypto_profile_across_volumes() {
10880 let archive = write_archive_with_root_auth(
10881 &[RegularFile::new(
10882 "public-crypto.txt",
10883 b"cross-volume public profile",
10884 )],
10885 &master_key(),
10886 WriterOptions {
10887 stripe_width: 2,
10888 volume_loss_tolerance: 0,
10889 ..WriterOptions::default()
10890 },
10891 RootAuthWriterConfig {
10892 authenticator_id: 0x3333,
10893 signer_identity_type: 1,
10894 signer_identity: b"public verifier",
10895 authenticator_value_length: 32,
10896 },
10897 |request| Ok(request.archive_root.to_vec()),
10898 )
10899 .unwrap();
10900 let mut volumes = archive.volumes.clone();
10901 let volume_header = VolumeHeader::parse(&volumes[1][..VOLUME_HEADER_LEN]).unwrap();
10902 let crypto_offset = volume_header.crypto_header_offset as usize;
10903 let expected_volume_size = 123_456_789u64;
10904 volumes[1][crypto_offset + 52..crypto_offset + 60]
10905 .copy_from_slice(&expected_volume_size.to_le_bytes());
10906 rewrite_public_cmra_image(&mut volumes[1], |image| {
10907 let crypto_region = image
10908 .regions
10909 .iter_mut()
10910 .find(|region| region.region_type == 2)
10911 .unwrap();
10912 crypto_region.bytes[52..60].copy_from_slice(&expected_volume_size.to_le_bytes());
10913 });
10914
10915 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
10916 public_no_key_verify_volumes_with(&volume_refs, |footer, archive_root| {
10917 Ok(footer.authenticator_value == archive_root.as_slice())
10918 })
10919 .unwrap();
10920 }
10921
10922 #[test]
10923 fn locator_based_cmra_recovery_treats_header_damage_as_recoverable() {
10924 let archive = write_archive_with_root_auth(
10925 &[RegularFile::new("cmra-header.txt", b"header fallback")],
10926 &master_key(),
10927 single_stream_options(),
10928 RootAuthWriterConfig {
10929 authenticator_id: 0x4444,
10930 signer_identity_type: 1,
10931 signer_identity: b"public verifier",
10932 authenticator_value_length: 32,
10933 },
10934 |request| Ok(request.archive_root.to_vec()),
10935 )
10936 .unwrap();
10937 let final_locator = final_recovery_locator(&archive.bytes);
10938
10939 let mut bad_crc = archive.bytes.clone();
10940 let crc_offset =
10941 final_locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN - 1;
10942 bad_crc[crc_offset] ^= 0x55;
10943 public_no_key_verify_archive_with(&bad_crc, |footer, archive_root| {
10944 Ok(footer.authenticator_value == archive_root.as_slice())
10945 })
10946 .unwrap();
10947
10948 let mut bad_magic = archive.bytes.clone();
10949 bad_magic[final_locator.cmra_offset as usize] ^= 0x55;
10950 public_no_key_verify_archive_with(&bad_magic, |footer, archive_root| {
10951 Ok(footer.authenticator_value == archive_root.as_slice())
10952 })
10953 .unwrap();
10954
10955 let mut bad_hint = archive.bytes.clone();
10956 bad_hint[crc_offset] ^= 0xAA;
10957 for offset in [
10958 bad_hint.len() - LOCATOR_PAIR_LEN,
10959 bad_hint.len() - CRITICAL_RECOVERY_LOCATOR_LEN,
10960 ] {
10961 let mut locator = CriticalRecoveryLocator::parse(
10962 &bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN],
10963 )
10964 .unwrap();
10965 locator.volume_index_hint += 1;
10966 bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN]
10967 .copy_from_slice(&locator.to_bytes());
10968 }
10969 assert_eq!(
10970 public_no_key_verify_archive_with(&bad_hint, |_, _| Ok(true)).unwrap_err(),
10971 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
10972 );
10973 }
10974
10975 #[test]
10976 fn recovers_physical_volume_header_magic_from_cmra_authority() {
10977 let payload = b"front header authority".to_vec();
10978 let archive = write_archive(
10979 &[RegularFile::new("volume-header.txt", &payload)],
10980 &master_key(),
10981 small_block_recovery_options(),
10982 )
10983 .unwrap();
10984
10985 let mut corrupted = archive.bytes;
10986 corrupted[0] ^= 0x55;
10987
10988 let opened = open_archive(&corrupted, &master_key()).unwrap();
10989 assert_eq!(
10990 opened.extract_file("volume-header.txt").unwrap(),
10991 Some(payload)
10992 );
10993 opened.verify().unwrap();
10994 }
10995
10996 #[test]
10997 fn recovers_crc_valid_physical_volume_index_from_cmra_authority() {
10998 let payload = b"crc-valid wrong volume index".to_vec();
10999 let mut options = small_block_recovery_options();
11000 options.stripe_width = 2;
11001 options.volume_loss_tolerance = 0;
11002 let archive = write_archive(
11003 &[RegularFile::new("volume-index.txt", &payload)],
11004 &master_key(),
11005 options,
11006 )
11007 .unwrap();
11008
11009 let mut corrupted = archive.volumes[0].clone();
11010 let mut header = VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN]).unwrap();
11011 assert_eq!(header.volume_index, 0);
11012 header.volume_index = 1;
11013 corrupted[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
11014 assert_eq!(
11015 VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN])
11016 .unwrap()
11017 .volume_index,
11018 1
11019 );
11020
11021 let opened = open_archive_volumes(
11022 &[corrupted.as_slice(), archive.volumes[1].as_slice()],
11023 &master_key(),
11024 )
11025 .unwrap();
11026 assert_eq!(opened.volume_header.volume_index, 0);
11027 assert_eq!(
11028 opened.extract_file("volume-index.txt").unwrap(),
11029 Some(payload)
11030 );
11031 opened.verify().unwrap();
11032 }
11033
11034 #[test]
11035 fn recovers_physical_crypto_header_magic_from_cmra_authority() {
11036 let payload = b"crypto header authority".to_vec();
11037 let archive = write_archive(
11038 &[RegularFile::new("crypto-header.txt", &payload)],
11039 &master_key(),
11040 small_block_recovery_options(),
11041 )
11042 .unwrap();
11043 let crypto_offset = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN])
11044 .unwrap()
11045 .crypto_header_offset;
11046
11047 let mut corrupted = archive.bytes;
11048 corrupted[crypto_offset as usize] ^= 0x55;
11049
11050 let opened = open_archive(&corrupted, &master_key()).unwrap();
11051 assert_eq!(
11052 opened.extract_file("crypto-header.txt").unwrap(),
11053 Some(payload)
11054 );
11055 opened.verify().unwrap();
11056 }
11057
11058 #[test]
11059 fn read_at_api_recovers_physical_header_magic_from_cmra_authority() {
11060 let payload = b"read-at header authority".to_vec();
11061 let archive = write_archive(
11062 &[RegularFile::new("read-at-header.txt", &payload)],
11063 &master_key(),
11064 small_block_recovery_options(),
11065 )
11066 .unwrap();
11067
11068 let mut corrupted = archive.bytes;
11069 corrupted[0] ^= 0x55;
11070
11071 let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
11072 assert_eq!(
11073 opened.extract_file("read-at-header.txt").unwrap(),
11074 Some(payload)
11075 );
11076 opened.verify().unwrap();
11077 }
11078
11079 #[test]
11080 fn read_at_api_recovers_crc_valid_physical_volume_index_from_cmra_authority() {
11081 let payload = b"read-at crc-valid wrong volume index".to_vec();
11082 let mut options = small_block_recovery_options();
11083 options.stripe_width = 2;
11084 options.volume_loss_tolerance = 0;
11085 let archive = write_archive(
11086 &[RegularFile::new("read-at-volume-index.txt", &payload)],
11087 &master_key(),
11088 options,
11089 )
11090 .unwrap();
11091
11092 let mut corrupted = archive.volumes[0].clone();
11093 let mut header = VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN]).unwrap();
11094 assert_eq!(header.volume_index, 0);
11095 header.volume_index = 1;
11096 corrupted[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
11097
11098 let opened = open_seekable_archive_volumes(
11099 vec![corrupted, archive.volumes[1].clone()],
11100 &master_key(),
11101 )
11102 .unwrap();
11103 assert_eq!(opened.volume_header.volume_index, 0);
11104 assert_eq!(
11105 opened.extract_file("read-at-volume-index.txt").unwrap(),
11106 Some(payload)
11107 );
11108 opened.verify().unwrap();
11109 }
11110
11111 #[test]
11112 fn recovers_cmra_header_magic_from_locator_tuple() {
11113 let payload = b"cmra header authority".to_vec();
11114 let archive = write_archive(
11115 &[RegularFile::new("cmra-header-magic.txt", &payload)],
11116 &master_key(),
11117 small_block_recovery_options(),
11118 )
11119 .unwrap();
11120 let locator = final_recovery_locator(&archive.bytes);
11121
11122 let mut corrupted = archive.bytes;
11123 corrupted[locator.cmra_offset as usize] ^= 0x55;
11124
11125 let opened = open_archive(&corrupted, &master_key()).unwrap();
11126 assert_eq!(
11127 opened.extract_file("cmra-header-magic.txt").unwrap(),
11128 Some(payload)
11129 );
11130 opened.verify().unwrap();
11131 }
11132
11133 #[test]
11134 fn recovers_cmra_shard_magic_as_erasure() {
11135 let payload = b"cmra shard authority".to_vec();
11136 let archive = write_archive(
11137 &[RegularFile::new("cmra-shard-magic.txt", &payload)],
11138 &master_key(),
11139 small_block_recovery_options(),
11140 )
11141 .unwrap();
11142 let locator = final_recovery_locator(&archive.bytes);
11143 let first_shard_offset =
11144 locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
11145
11146 let mut corrupted = archive.bytes;
11147 corrupted[first_shard_offset] ^= 0x55;
11148
11149 let opened = open_archive(&corrupted, &master_key()).unwrap();
11150 assert_eq!(
11151 opened.extract_file("cmra-shard-magic.txt").unwrap(),
11152 Some(payload)
11153 );
11154 opened.verify().unwrap();
11155 }
11156
11157 #[test]
11158 fn key_holding_rejects_recovered_image_revision_mismatch() {
11159 let archive = write_archive(
11160 &[RegularFile::new("cmra-image-revision.txt", b"payload")],
11161 &master_key(),
11162 small_block_recovery_options(),
11163 )
11164 .unwrap();
11165 let mut mutated = rewrite_cmra_image_variable_len(
11166 &archive.bytes,
11167 CmraRecoveryMode::KeyHolding,
11168 |image| {
11169 image.volume_format_rev = VOLUME_FORMAT_REV_43;
11170 },
11171 );
11172 mutated[0] ^= 0x55;
11173
11174 assert!(open_archive(&mutated, &master_key()).is_err());
11175 }
11176
11177 #[test]
11178 fn key_holding_rejects_recovered_footer_revision_mismatch() {
11179 let archive = write_archive_with_root_auth(
11180 &[RegularFile::new("cmra-footer-revision.txt", b"payload")],
11181 &master_key(),
11182 single_stream_options(),
11183 test_root_auth_config(),
11184 |request| Ok(test_root_auth_value(request)),
11185 )
11186 .unwrap();
11187 let mut mutated = archive.bytes;
11188 rewrite_cmra_image(&mut mutated, CmraRecoveryMode::KeyHolding, |image| {
11189 let root_auth_region = image
11190 .regions
11191 .iter_mut()
11192 .find(|region| region.region_type == 4)
11193 .unwrap();
11194 let mut footer = RootAuthFooterV1::parse(&root_auth_region.bytes).unwrap();
11195 footer.volume_format_rev = VOLUME_FORMAT_REV_43;
11196 root_auth_region.bytes = footer.to_bytes().unwrap();
11197 });
11198 mutated[0] ^= 0x55;
11199
11200 assert!(open_archive(&mutated, &master_key()).is_err());
11201 }
11202
11203 #[test]
11204 fn key_holding_rejects_locator_image_revision_mismatch() {
11205 let archive = write_archive(
11206 &[RegularFile::new("cmra-locator-revision.txt", b"payload")],
11207 &master_key(),
11208 small_block_recovery_options(),
11209 )
11210 .unwrap();
11211 let mut mutated = archive.bytes;
11212 let locator = final_recovery_locator(&mutated);
11213 let mirror_offset = mutated.len() - LOCATOR_PAIR_LEN;
11214 let final_offset = mutated.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11215 for offset in [mirror_offset, final_offset] {
11216 rewrite_recovery_locator(&mut mutated, offset, |locator| {
11217 locator.volume_format_rev = VOLUME_FORMAT_REV_43;
11218 });
11219 }
11220 mutated[0] ^= 0x55;
11221 mutated[locator.cmra_offset as usize] ^= 0x55;
11222
11223 assert!(open_archive(&mutated, &master_key()).is_err());
11224 }
11225
11226 #[test]
11227 fn image_identity_allows_matching_v43_revision() {
11228 let archive = write_archive(
11229 &[RegularFile::new("matching-v43.txt", b"payload")],
11230 &master_key(),
11231 single_stream_options(),
11232 )
11233 .unwrap();
11234 let locator = final_recovery_locator(&archive.bytes);
11235 let recovered = recover_cmra(
11236 &archive.bytes,
11237 locator.cmra_offset,
11238 Some(CmraDecoderTuple::from(locator)),
11239 CmraRecoveryMode::KeyHolding,
11240 )
11241 .unwrap();
11242 let mut image = recovered.image;
11243 let mut header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11244 let crypto_start = header.crypto_header_offset as usize;
11245 let crypto_end = crypto_start + header.crypto_header_length as usize;
11246 let crypto = CryptoHeader::parse(
11247 &archive.bytes[crypto_start..crypto_end],
11248 header.crypto_header_length,
11249 )
11250 .unwrap();
11251
11252 image.volume_format_rev = VOLUME_FORMAT_REV_43;
11253 header.volume_format_rev = VOLUME_FORMAT_REV_43;
11254
11255 validate_image_identity(&image, &header, &crypto.fixed).unwrap();
11256 }
11257
11258 #[test]
11259 fn key_holding_rejects_cmra_below_authenticated_parity_floor() {
11260 let archive = write_archive(
11261 &[RegularFile::new(
11262 "cmra-floor.txt",
11263 b"authenticated CMRA floor",
11264 )],
11265 &master_key(),
11266 single_stream_options(),
11267 )
11268 .unwrap();
11269 let malformed = rewrite_cmra_parity_count(&archive.bytes, 1);
11270 let final_offset = malformed.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11271 let locator = CriticalRecoveryLocator::parse(
11272 &malformed[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
11273 )
11274 .unwrap();
11275 let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
11276 let crypto_start = volume_header.crypto_header_offset as usize;
11277 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
11278 let crypto_header = CryptoHeader::parse(
11279 &malformed[crypto_start..crypto_end],
11280 volume_header.crypto_header_length,
11281 )
11282 .unwrap();
11283 let subkeys = Subkeys::derive(
11284 &master_key(),
11285 &volume_header.archive_uuid,
11286 &volume_header.session_id,
11287 )
11288 .unwrap();
11289
11290 assert_eq!(
11291 parse_locator_cmra_candidate(
11292 &malformed,
11293 final_offset,
11294 locator,
11295 KeyHoldingTerminalContext {
11296 subkeys: &subkeys,
11297 volume_header: &volume_header,
11298 crypto_header: &crypto_header.fixed,
11299 crypto_header_bytes: &malformed[crypto_start..crypto_end],
11300 },
11301 )
11302 .unwrap_err(),
11303 FormatError::InvalidArchive(
11304 "CMRA parity shard count is below authenticated bit-rot lower bound"
11305 )
11306 );
11307 assert!(open_archive(&malformed, &master_key()).is_err());
11308 }
11309
11310 #[test]
11311 fn locator_tuple_bounds_are_checked_before_locator_position_fields() {
11312 let archive = write_archive(
11313 &[RegularFile::new(
11314 "locator-order.txt",
11315 b"locator tuple first",
11316 )],
11317 &master_key(),
11318 single_stream_options(),
11319 )
11320 .unwrap();
11321 let final_offset = archive.bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11322 let mut locator = final_recovery_locator(&archive.bytes);
11323 locator.cmra_shard_size = 513;
11324 locator.body_bytes_before_cmra = locator.cmra_offset + 1;
11325 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11326 let crypto_start = volume_header.crypto_header_offset as usize;
11327 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
11328 let crypto_header = CryptoHeader::parse(
11329 &archive.bytes[crypto_start..crypto_end],
11330 volume_header.crypto_header_length,
11331 )
11332 .unwrap();
11333 let subkeys = Subkeys::derive(
11334 &master_key(),
11335 &volume_header.archive_uuid,
11336 &volume_header.session_id,
11337 )
11338 .unwrap();
11339
11340 assert_eq!(
11341 parse_locator_cmra_candidate(
11342 &archive.bytes,
11343 final_offset,
11344 locator,
11345 KeyHoldingTerminalContext {
11346 subkeys: &subkeys,
11347 volume_header: &volume_header,
11348 crypto_header: &crypto_header.fixed,
11349 crypto_header_bytes: &archive.bytes[crypto_start..crypto_end],
11350 },
11351 )
11352 .unwrap_err(),
11353 FormatError::InvalidArchive("CMRA shard_size is invalid")
11354 );
11355 }
11356
11357 #[test]
11358 fn sequential_extract_rejects_bytes_after_terminal_locator() {
11359 let archive = write_archive(
11360 &[RegularFile::new("seq.txt", b"sequential EOF")],
11361 &master_key(),
11362 single_stream_options(),
11363 )
11364 .unwrap();
11365 let mut appended = archive.bytes.clone();
11366 appended.extend_from_slice(&[0xAA; 32]);
11367
11368 assert_eq!(
11369 sequential_extract_tar_stream(&appended, &master_key()).unwrap_err(),
11370 FormatError::InvalidArchive("sequential terminal does not end at EOF")
11371 );
11372 }
11373
11374 #[test]
11375 fn global_file_table_order_rejects_cross_shard_duplicate_reversal() {
11376 let first = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 2048);
11377 let second = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 1024);
11378
11379 assert_eq!(
11380 validate_global_file_table_key_step(Some(&first), &second).unwrap_err(),
11381 FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
11382 );
11383 }
11384
11385 #[test]
11386 fn root_auth_verifies_key_holding_and_public_no_key_modes() {
11387 let archive = write_archive_with_root_auth(
11388 &[RegularFile::new("signed.txt", b"ed25519 payload")],
11389 &master_key(),
11390 single_stream_options(),
11391 test_root_auth_config(),
11392 |request| Ok(test_root_auth_value(request)),
11393 )
11394 .unwrap();
11395
11396 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11397 let root_auth = opened
11398 .verify_root_auth_with(|footer, archive_root| {
11399 Ok(test_root_auth_verifies(footer, archive_root))
11400 })
11401 .unwrap();
11402 assert_eq!(
11403 root_auth.archive_root,
11404 opened.root_auth_footer.as_ref().unwrap().archive_root
11405 );
11406
11407 let public = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
11408 Ok(test_root_auth_verifies(footer, archive_root))
11409 })
11410 .unwrap();
11411 assert_eq!(public.archive_root, root_auth.archive_root);
11412 assert_eq!(
11413 public.diagnostics,
11414 vec![
11415 PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
11416 PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
11417 PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
11418 ]
11419 );
11420 }
11421
11422 #[test]
11423 fn root_auth_verifies_with_tolerated_missing_volume_after_fec_repair() {
11424 let options = WriterOptions {
11425 block_size: 4096,
11426 chunk_size: 16 * 1024,
11427 envelope_target_size: 16 * 1024,
11428 stripe_width: 2,
11429 volume_loss_tolerance: 1,
11430 bit_rot_buffer_pct: 0,
11431 fec_data_shards: 16,
11432 fec_parity_shards: 1,
11433 index_fec_data_shards: 4,
11434 index_fec_parity_shards: 1,
11435 index_root_fec_data_shards: 16,
11436 index_root_fec_parity_shards: 1,
11437 ..WriterOptions::default()
11438 };
11439 let archive = write_archive_with_root_auth(
11440 &[RegularFile::new("missing-volume.txt", b"recover me")],
11441 &master_key(),
11442 options,
11443 test_root_auth_config(),
11444 |request| Ok(test_root_auth_value(request)),
11445 )
11446 .unwrap();
11447
11448 let opened = open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap();
11449 let root_auth = opened
11450 .verify_root_auth_with(|footer, archive_root| {
11451 Ok(test_root_auth_verifies(footer, archive_root))
11452 })
11453 .unwrap();
11454 assert!(root_auth
11455 .diagnostics
11456 .contains(&RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss));
11457 }
11458
11459 #[test]
11460 fn public_no_key_rejects_unsigned_archives() {
11461 let archive = write_archive(
11462 &[RegularFile::new("plain.txt", b"unsigned")],
11463 &master_key(),
11464 single_stream_options(),
11465 )
11466 .unwrap();
11467
11468 assert_eq!(
11469 public_no_key_verify_archive_with(&archive.bytes, |_, _| Ok(true)).unwrap_err(),
11470 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
11471 );
11472 }
11473
11474 #[test]
11475 fn unsigned_archive_reports_root_auth_absent() {
11476 let archive = write_archive(
11477 &[RegularFile::new("plain.txt", b"unsigned")],
11478 &master_key(),
11479 single_stream_options(),
11480 )
11481 .unwrap();
11482 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11483
11484 assert_eq!(
11485 opened.verify_root_auth_with(|_, _| Ok(true)).unwrap_err(),
11486 FormatError::ReaderUnsupported("root-auth footer is absent")
11487 );
11488 }
11489
11490 #[test]
11491 fn safe_extract_writes_regular_file_under_root() {
11492 let archive = write_archive(
11493 &[RegularFile::new("dir/hello.txt", b"safe m8")],
11494 &master_key(),
11495 single_stream_options(),
11496 )
11497 .unwrap();
11498 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11499 let tmp = tempfile::tempdir().unwrap();
11500
11501 opened
11502 .extract_file_to(
11503 "dir/hello.txt",
11504 tmp.path(),
11505 SafeExtractionOptions::default(),
11506 )
11507 .unwrap()
11508 .unwrap();
11509
11510 assert_eq!(
11511 std::fs::read(tmp.path().join("dir").join("hello.txt")).unwrap(),
11512 b"safe m8"
11513 );
11514 }
11515
11516 #[test]
11517 fn seekable_extract_all_to_streams_unique_archive() {
11518 let archive = write_archive(
11519 &[
11520 RegularFile::new("alpha.txt", b"alpha"),
11521 RegularFile::new("dir/beta.txt", b"beta"),
11522 ],
11523 &master_key(),
11524 single_stream_options(),
11525 )
11526 .unwrap();
11527 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11528 let tmp = tempfile::tempdir().unwrap();
11529
11530 let diagnostics = opened
11531 .extract_all_to(tmp.path(), SafeExtractionOptions::default())
11532 .unwrap();
11533
11534 assert_eq!(diagnostics.len(), 2);
11535 assert_eq!(fs::read(tmp.path().join("alpha.txt")).unwrap(), b"alpha");
11536 assert_eq!(
11537 fs::read(tmp.path().join("dir").join("beta.txt")).unwrap(),
11538 b"beta"
11539 );
11540 }
11541
11542 #[test]
11543 fn seekable_extract_all_to_rejects_duplicate_paths_for_cli_fallback() {
11544 let archive = write_archive(
11545 &[
11546 RegularFile::new("same.txt", b"old"),
11547 RegularFile::new("same.txt", b"new"),
11548 ],
11549 &master_key(),
11550 single_stream_options(),
11551 )
11552 .unwrap();
11553 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11554 let tmp = tempfile::tempdir().unwrap();
11555
11556 assert_eq!(
11557 opened
11558 .extract_all_to(tmp.path(), SafeExtractionOptions::default())
11559 .unwrap_err(),
11560 FormatError::ReaderUnsupported("fast full extract requires unique archive paths")
11561 );
11562 }
11563
11564 #[test]
11565 fn seekable_extract_indexed_files_to_restores_final_duplicate_winner() {
11566 let archive = write_archive(
11567 &[
11568 RegularFile::new("same.txt", b"old"),
11569 RegularFile::new("same.txt", b"new"),
11570 ],
11571 &master_key(),
11572 single_stream_options(),
11573 )
11574 .unwrap();
11575 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11576 let tmp = tempfile::tempdir().unwrap();
11577
11578 let diagnostics = opened
11579 .extract_indexed_files_to(tmp.path(), SafeExtractionOptions::default(), 2)
11580 .unwrap();
11581
11582 assert_eq!(diagnostics.len(), 1);
11583 assert_eq!(diagnostics[0].0, "same.txt");
11584 assert_eq!(fs::read(tmp.path().join("same.txt")).unwrap(), b"new");
11585 }
11586
11587 #[test]
11588 fn safe_extract_rejects_overwriting_existing_file_by_default() {
11589 let archive = write_archive(
11590 &[RegularFile::new("hello.txt", b"new")],
11591 &master_key(),
11592 single_stream_options(),
11593 )
11594 .unwrap();
11595 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11596 let tmp = tempfile::tempdir().unwrap();
11597 std::fs::write(tmp.path().join("hello.txt"), b"old").unwrap();
11598
11599 assert_eq!(
11600 opened
11601 .extract_file_to("hello.txt", tmp.path(), SafeExtractionOptions::default())
11602 .unwrap_err(),
11603 FormatError::UnsafeOverwrite
11604 );
11605 assert_eq!(std::fs::read(tmp.path().join("hello.txt")).unwrap(), b"old");
11606 }
11607
11608 #[test]
11609 fn opens_and_verifies_empty_archive() {
11610 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
11611 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11612
11613 assert!(opened.list_files().unwrap().is_empty());
11614 opened.verify().unwrap();
11615 }
11616
11617 #[test]
11618 fn default_reader_options_allow_v36_trailing_garbage_scan() {
11619 let archive = write_archive(
11620 &[RegularFile::new("garbage-tolerant.txt", b"still intact")],
11621 &master_key(),
11622 single_stream_options(),
11623 )
11624 .unwrap();
11625 let mut with_trailing_garbage = archive.bytes.clone();
11626 with_trailing_garbage.extend_from_slice(b"ignored trailing bytes");
11627
11628 let opened = open_archive(&with_trailing_garbage, &master_key()).unwrap();
11629 assert_eq!(
11630 opened.extract_file("garbage-tolerant.txt").unwrap(),
11631 Some(b"still intact".to_vec())
11632 );
11633 }
11634
11635 #[test]
11636 fn seekable_open_rejects_too_small_and_unavailable_header_crypto_bytes() {
11637 assert_eq!(
11638 open_archive(
11639 &[0u8; VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1],
11640 &master_key()
11641 )
11642 .unwrap_err(),
11643 FormatError::InvalidLength {
11644 structure: "archive",
11645 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
11646 actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1,
11647 }
11648 );
11649
11650 let mut header = test_volume_header();
11651 header.crypto_header_length = 512;
11652 let mut unavailable_crypto = header.to_bytes().to_vec();
11653 unavailable_crypto.resize(VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN, 0);
11654
11655 assert_eq!(
11656 open_archive(&unavailable_crypto, &master_key()).unwrap_err(),
11657 FormatError::InvalidLength {
11658 structure: "CryptoHeader",
11659 expected: VOLUME_HEADER_LEN + 512,
11660 actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
11661 }
11662 );
11663 }
11664
11665 #[test]
11666 fn seekable_open_recovers_physical_noncanonical_crypto_header_offset() {
11667 let archive = write_archive(
11668 &[RegularFile::new("offset.txt", b"offset")],
11669 &master_key(),
11670 small_block_recovery_options(),
11671 )
11672 .unwrap();
11673 let mut mutated = archive.bytes;
11674 let mut header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
11675 header.crypto_header_offset = VOLUME_HEADER_LEN as u32 + 1;
11676 mutated[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
11677
11678 let opened = open_archive(&mutated, &master_key()).unwrap();
11679 assert_eq!(
11680 opened.volume_header.crypto_header_offset,
11681 VOLUME_HEADER_LEN as u32
11682 );
11683 assert_eq!(
11684 opened.extract_file("offset.txt").unwrap(),
11685 Some(b"offset".to_vec())
11686 );
11687 opened.verify().unwrap();
11688 }
11689
11690 #[test]
11691 fn rejects_wrong_key_before_metadata_release() {
11692 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
11693 let wrong = MasterKey::from_raw_key(&[0x43; 32]).unwrap();
11694
11695 assert_eq!(
11696 open_archive(&archive.bytes, &wrong).unwrap_err(),
11697 FormatError::HmacMismatch {
11698 structure: "CryptoHeader"
11699 }
11700 );
11701 }
11702
11703 #[test]
11704 fn ordinary_encrypted_writers_emit_v44_archives() {
11705 let raw_key_archive = write_archive(
11706 &[RegularFile::new("raw.txt", b"raw key payload")],
11707 &master_key(),
11708 single_stream_options(),
11709 )
11710 .unwrap();
11711 let raw_header = VolumeHeader::parse(&raw_key_archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11712 assert_eq!(raw_header.volume_format_rev, VOLUME_FORMAT_REV_44);
11713 let raw_opened = open_archive(&raw_key_archive.bytes, &master_key()).unwrap();
11714 assert_eq!(
11715 raw_opened.volume_header.volume_format_rev,
11716 VOLUME_FORMAT_REV_44
11717 );
11718 assert_eq!(
11719 raw_opened.extract_file("raw.txt").unwrap(),
11720 Some(b"raw key payload".to_vec())
11721 );
11722
11723 let passphrase_kdf = KdfParams::Argon2id {
11724 t_cost: 1,
11725 m_cost_kib: 8,
11726 parallelism: 1,
11727 salt: b"0123456789abcdef".to_vec(),
11728 };
11729 let passphrase_archive = write_archive_with_kdf(
11730 &[RegularFile::new("pass.txt", b"passphrase payload")],
11731 &master_key(),
11732 single_stream_options(),
11733 &passphrase_kdf,
11734 )
11735 .unwrap();
11736 let passphrase_header =
11737 VolumeHeader::parse(&passphrase_archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11738 assert_eq!(passphrase_header.volume_format_rev, VOLUME_FORMAT_REV_44);
11739 let passphrase_opened = open_archive(&passphrase_archive.bytes, &master_key()).unwrap();
11740 assert_eq!(
11741 passphrase_opened.volume_header.volume_format_rev,
11742 VOLUME_FORMAT_REV_44
11743 );
11744 assert_eq!(
11745 passphrase_opened.extract_file("pass.txt").unwrap(),
11746 Some(b"passphrase payload".to_vec())
11747 );
11748 }
11749
11750 #[test]
11751 fn rejects_future_volume_format_revision_before_key_mismatch() {
11752 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
11753 let mut bytes = archive.bytes;
11754 let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
11755 header.volume_format_rev = 45;
11756 bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
11757 let wrong = MasterKey::from_raw_key(&[0x43; 32]).unwrap();
11758
11759 assert_eq!(
11760 open_archive(&bytes, &wrong).unwrap_err(),
11761 FormatError::UnsupportedVolumeFormatRevision {
11762 format_version: FORMAT_VERSION,
11763 volume_format_rev: 45,
11764 reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
11765 }
11766 );
11767 }
11768
11769 #[test]
11770 fn open_archive_unencrypted_accepts_v44_profile() {
11771 let archive = write_archive_unencrypted(
11772 &[RegularFile::new("payload.txt", b"smoke-v44-unencrypted")],
11773 WriterOptions {
11774 aead_algo: AeadAlgo::None,
11775 ..single_stream_options()
11776 },
11777 )
11778 .unwrap();
11779
11780 let opened = open_archive_unencrypted(&archive.bytes).unwrap();
11781 let header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11782
11783 assert_eq!(header.volume_format_rev, VOLUME_FORMAT_REV_44);
11784 assert_eq!(opened.volume_header.volume_format_rev, VOLUME_FORMAT_REV_44);
11785 assert_eq!(
11786 opened.extract_file("payload.txt").unwrap(),
11787 Some(b"smoke-v44-unencrypted".to_vec())
11788 );
11789 opened.verify().unwrap();
11790 }
11791
11792 #[test]
11793 fn root_auth_unencrypted_v44_round_trips_with_recomputed_archive_root() {
11794 let archive = write_archive_with_root_auth(
11795 &[RegularFile::new(
11796 "signed-v44.txt",
11797 b"root-auth v44 plaintext",
11798 )],
11799 &master_key(),
11800 WriterOptions {
11801 aead_algo: AeadAlgo::None,
11802 ..single_stream_options()
11803 },
11804 test_root_auth_config(),
11805 |request| Ok(test_root_auth_value(request)),
11806 )
11807 .unwrap();
11808 let opened = open_archive_unencrypted(&archive.bytes).unwrap();
11809 let header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11810
11811 assert_eq!(header.volume_format_rev, VOLUME_FORMAT_REV_44);
11812 assert_eq!(opened.volume_header.volume_format_rev, VOLUME_FORMAT_REV_44);
11813 assert_eq!(
11814 opened.extract_file("signed-v44.txt").unwrap(),
11815 Some(b"root-auth v44 plaintext".to_vec())
11816 );
11817
11818 let verified = opened
11819 .verify_root_auth_with(|footer, archive_root| {
11820 Ok(test_root_auth_verifies(footer, archive_root))
11821 })
11822 .unwrap();
11823
11824 assert_eq!(verified.format_version, FORMAT_VERSION);
11825 assert_eq!(verified.volume_format_rev, VOLUME_FORMAT_REV_44);
11826 assert_eq!(
11827 verified.archive_root,
11828 opened.root_auth_footer.as_ref().unwrap().archive_root
11829 );
11830 }
11831
11832 #[test]
11833 fn recipientwrap_open_accepts_candidate_after_header_hmac() {
11834 let master = master_key();
11835 let archive = write_archive_with_recipient_wrap_records(
11836 &[RegularFile::new("wrapped.txt", b"recipient payload")],
11837 &master,
11838 single_stream_options(),
11839 vec![recipient_wrap_test_record()],
11840 )
11841 .unwrap();
11842
11843 let opened = open_archive_with_recipient_wrap_resolver(&archive.bytes, |context| {
11844 assert_eq!(
11845 context.archive_identity.volume_format_rev,
11846 VOLUME_FORMAT_REV_44
11847 );
11848 assert_eq!(context.record.profile_id, 1);
11849 Ok(vec![master.0])
11850 })
11851 .unwrap();
11852
11853 assert_eq!(
11854 opened.extract_file("wrapped.txt").unwrap(),
11855 Some(b"recipient payload".to_vec())
11856 );
11857 opened.verify().unwrap();
11858 }
11859
11860 #[test]
11861 fn recipientwrap_seekable_open_uses_lazy_block_source() {
11862 let master = master_key();
11863 let archive = write_archive_with_recipient_wrap_records(
11864 &[RegularFile::new("wrapped.txt", b"recipient payload")],
11865 &master,
11866 single_stream_options(),
11867 vec![recipient_wrap_test_record()],
11868 )
11869 .unwrap();
11870
11871 let opened = open_seekable_archive_with_recipient_wrap_resolver_options(
11872 CountingReadAt::new(archive.bytes, vec![]),
11873 |context| {
11874 assert_eq!(
11875 context.archive_identity.volume_format_rev,
11876 VOLUME_FORMAT_REV_44
11877 );
11878 assert_eq!(context.record.profile_id, 1);
11879 Ok(vec![master.0])
11880 },
11881 ReaderOptions::default(),
11882 )
11883 .unwrap();
11884
11885 assert!(opened.blocks.is_empty());
11886 assert!(opened.lazy_blocks.is_some());
11887 assert_eq!(
11888 opened.extract_file("wrapped.txt").unwrap(),
11889 Some(b"recipient payload".to_vec())
11890 );
11891 opened.verify().unwrap();
11892 }
11893
11894 #[test]
11895 fn recipientwrap_seekable_volume_set_opens_with_resolver() {
11896 let master = master_key();
11897 let archive = write_archive_with_recipient_wrap_records(
11898 &[RegularFile::new("wrapped.txt", b"recipient payload")],
11899 &master,
11900 WriterOptions {
11901 stripe_width: 2,
11902 volume_loss_tolerance: 0,
11903 ..WriterOptions::default()
11904 },
11905 vec![recipient_wrap_test_record()],
11906 )
11907 .unwrap();
11908 assert_eq!(archive.volumes.len(), 2);
11909
11910 let opened = open_seekable_archive_volumes_with_recipient_wrap_resolver_options(
11911 archive.volumes,
11912 |context| {
11913 assert_eq!(
11914 context.archive_identity.volume_format_rev,
11915 VOLUME_FORMAT_REV_44
11916 );
11917 assert_eq!(context.record.profile_id, 1);
11918 Ok(vec![master.0])
11919 },
11920 ReaderOptions::default(),
11921 )
11922 .unwrap();
11923
11924 assert_eq!(opened.observed_volume_count, 2);
11925 assert!(opened.blocks.is_empty());
11926 assert!(opened.lazy_blocks.is_some());
11927 assert_eq!(
11928 opened.extract_file("wrapped.txt").unwrap(),
11929 Some(b"recipient payload".to_vec())
11930 );
11931 opened.verify().unwrap();
11932 }
11933
11934 #[test]
11935 fn recipientwrap_open_tries_subsequent_records_after_failed_candidate() {
11936 let master = master_key();
11937 let mut first_record = recipient_wrap_test_record();
11938 first_record.recipient_identity_bytes = b"first-candidate".to_vec();
11939 let mut second_record = recipient_wrap_test_record();
11940 second_record.recipient_identity_bytes = b"second-candidate".to_vec();
11941
11942 let archive = write_archive_with_recipient_wrap_records(
11943 &[RegularFile::new("wrapped.txt", b"recipient payload")],
11944 &master,
11945 single_stream_options(),
11946 vec![first_record, second_record],
11947 )
11948 .unwrap();
11949
11950 let mut attempts = Vec::new();
11951 let opened = open_archive_with_recipient_wrap_resolver(&archive.bytes, |context| {
11952 attempts.push(context.record.recipient_identity_bytes.clone());
11953 if context.record.recipient_identity_bytes.as_slice() == b"second-candidate" {
11954 Ok(vec![master.0])
11955 } else {
11956 Ok(vec![[0x99u8; 32]])
11957 }
11958 })
11959 .unwrap();
11960
11961 assert_eq!(
11962 attempts,
11963 vec![b"first-candidate".to_vec(), b"second-candidate".to_vec(),]
11964 );
11965 assert_eq!(
11966 opened.extract_file("wrapped.txt").unwrap(),
11967 Some(b"recipient payload".to_vec())
11968 );
11969 opened.verify().unwrap();
11970 }
11971
11972 #[test]
11973 fn recipientwrap_startup_rejects_malformed_record_length() {
11974 let master = master_key();
11975 let options = WriterOptions {
11976 bit_rot_buffer_pct: 0,
11977 ..single_stream_options()
11978 };
11979 let archive = write_archive_with_recipient_wrap_records(
11980 &[RegularFile::new("wrapped.txt", b"recipient payload")],
11981 &master,
11982 options,
11983 vec![recipient_wrap_test_record()],
11984 )
11985 .unwrap();
11986 let mut bytes = archive.bytes;
11987 let (_, _, table_start, _table_len, _) = recipient_wrap_layout(&bytes);
11988 bytes[table_start + 96..table_start + 100].copy_from_slice(&1u32.to_le_bytes());
11989
11990 assert_eq!(
11991 open_archive_with_recipient_wrap_resolver(&bytes, |_| { Ok(vec![master.0]) })
11992 .unwrap_err(),
11993 FormatError::InvalidArchive("RecipientRecordV1 record_length is too small")
11994 );
11995 }
11996
11997 #[test]
11998 fn recipientwrap_future_revision_rejects_before_resolver_callback() {
11999 let archive = write_archive_with_recipient_wrap_records(
12000 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12001 &master_key(),
12002 single_stream_options(),
12003 vec![recipient_wrap_test_record()],
12004 )
12005 .unwrap();
12006 let mut bytes = archive.bytes;
12007 let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12008 header.volume_format_rev = VOLUME_FORMAT_REV_44 + 1;
12009 bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
12010
12011 let mut called = false;
12012 let err = open_archive_with_recipient_wrap_resolver(&bytes, |_| {
12013 called = true;
12014 Ok(vec![master_key().0])
12015 })
12016 .unwrap_err();
12017
12018 assert!(!called);
12019 assert_eq!(
12020 err,
12021 FormatError::UnsupportedVolumeFormatRevision {
12022 format_version: FORMAT_VERSION,
12023 volume_format_rev: VOLUME_FORMAT_REV_44 + 1,
12024 reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
12025 }
12026 );
12027 }
12028
12029 #[test]
12030 fn recipientwrap_stripe_width_mismatch_rejects_before_resolver_callback() {
12031 let archive = write_archive_with_recipient_wrap_records(
12032 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12033 &master_key(),
12034 single_stream_options(),
12035 vec![recipient_wrap_test_record()],
12036 )
12037 .unwrap();
12038 let mut bytes = archive.bytes;
12039 let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12040 header.stripe_width += 1;
12041 bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
12042
12043 let mut called = false;
12044 let err = open_archive_with_recipient_wrap_resolver(&bytes, |_| {
12045 called = true;
12046 Ok(vec![master_key().0])
12047 })
12048 .unwrap_err();
12049
12050 assert!(!called);
12051 assert_eq!(
12052 err,
12053 FormatError::InvalidArchive("VolumeHeader and CryptoHeader stripe_width differ")
12054 );
12055 }
12056
12057 #[test]
12058 fn recipientwrap_defers_raw_stream_profile_rejection_until_after_resolver_callback() {
12059 let master = master_key();
12060 let archive = write_archive_with_recipient_wrap_records(
12061 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12062 &master,
12063 single_stream_options(),
12064 vec![recipient_wrap_test_record()],
12065 )
12066 .unwrap();
12067 let mut bytes = archive.bytes;
12068 add_raw_stream_profile_to_physical_crypto_header(&mut bytes);
12069 recompute_physical_crypto_header_hmac(&mut bytes, &master);
12070
12071 let mut called = false;
12072 let err = open_archive_with_recipient_wrap_resolver(&bytes, |_| {
12073 called = true;
12074 Ok(vec![master.0])
12075 })
12076 .unwrap_err();
12077
12078 assert!(called);
12079 assert_eq!(
12080 err,
12081 FormatError::ReaderUnsupported(RAW_STREAM_UNSUPPORTED_MESSAGE)
12082 );
12083 }
12084
12085 #[test]
12086 fn recipientwrap_recovers_physical_key_wrap_table_from_cmra_authority() {
12087 let master = master_key();
12088 let archive = write_archive_with_recipient_wrap_records(
12089 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12090 &master,
12091 single_stream_options(),
12092 vec![recipient_wrap_test_record()],
12093 )
12094 .unwrap();
12095 let mut bytes = archive.bytes;
12096 let header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12097 let crypto_start = header.crypto_header_offset as usize;
12098 let crypto_end = crypto_start + header.crypto_header_length as usize;
12099 let crypto_header = CryptoHeader::parse(
12100 &bytes[crypto_start..crypto_end],
12101 header.crypto_header_length,
12102 )
12103 .unwrap();
12104 let KdfParams::RecipientWrap {
12105 key_wrap_table_length,
12106 ..
12107 } = crypto_header.kdf_params
12108 else {
12109 panic!("expected RecipientWrap KdfParams");
12110 };
12111 let table_start = crypto_end;
12112 let table_end = table_start + key_wrap_table_length as usize;
12113 bytes[table_end - 1] ^= 0x01;
12114
12115 let mut called = false;
12116 let opened = open_archive_with_recipient_wrap_resolver(&bytes, |context| {
12117 called = true;
12118 assert_eq!(
12119 context.archive_identity.volume_format_rev,
12120 VOLUME_FORMAT_REV_44
12121 );
12122 assert_eq!(context.record.profile_id, 1);
12123 Ok(vec![master.0])
12124 })
12125 .unwrap();
12126
12127 assert!(called);
12128 assert_eq!(
12129 opened.extract_file("wrapped.txt").unwrap(),
12130 Some(b"recipient payload".to_vec())
12131 );
12132 opened.verify().unwrap();
12133 }
12134
12135 #[test]
12136 fn recipientwrap_open_rejects_wrong_candidate_header_hmac() {
12137 let archive = write_archive_with_recipient_wrap_records(
12138 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12139 &master_key(),
12140 single_stream_options(),
12141 vec![recipient_wrap_test_record()],
12142 )
12143 .unwrap();
12144 let wrong_candidate = [0x99u8; 32];
12145
12146 assert_eq!(
12147 open_archive_with_recipient_wrap_resolver(&archive.bytes, |_| {
12148 Ok(vec![wrong_candidate])
12149 })
12150 .unwrap_err(),
12151 FormatError::KeyMaterialMismatch
12152 );
12153 }
12154
12155 #[test]
12156 fn recipientwrap_open_recovers_tampered_physical_crypto_header_hmac_from_cmra() {
12157 let master = master_key();
12158 let archive = write_archive_with_recipient_wrap_records(
12159 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12160 &master,
12161 single_stream_options(),
12162 vec![recipient_wrap_test_record()],
12163 )
12164 .unwrap();
12165 let mut bytes = archive.bytes.clone();
12166 let header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12167 let hmac_end = header.crypto_header_offset as usize + header.crypto_header_length as usize;
12168 bytes[hmac_end - 1] ^= 0x55;
12169
12170 let opened =
12171 open_archive_with_recipient_wrap_resolver(&bytes, |_| Ok(vec![master.0])).unwrap();
12172
12173 assert_eq!(
12174 opened.extract_file("wrapped.txt").unwrap(),
12175 Some(b"recipient payload".to_vec())
12176 );
12177 assert_eq!(
12178 opened.crypto_header_bytes,
12179 archive.bytes[header.crypto_header_offset as usize..hmac_end]
12180 );
12181 opened.verify().unwrap();
12182 }
12183
12184 #[test]
12185 fn recipientwrap_archive_does_not_fall_back_to_raw_master_key_open() {
12186 let master = master_key();
12187 let archive = write_archive_with_recipient_wrap_records(
12188 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12189 &master,
12190 single_stream_options(),
12191 vec![recipient_wrap_test_record()],
12192 )
12193 .unwrap();
12194
12195 assert_eq!(
12196 open_archive(&archive.bytes, &master).unwrap_err(),
12197 FormatError::KeyMaterialMismatch
12198 );
12199 }
12200
12201 #[test]
12202 fn recipientwrap_seekable_archive_does_not_fall_back_to_raw_master_key_open() {
12203 let master = master_key();
12204 let archive = write_archive_with_recipient_wrap_records(
12205 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12206 &master,
12207 single_stream_options(),
12208 vec![recipient_wrap_test_record()],
12209 )
12210 .unwrap();
12211
12212 assert_eq!(
12213 open_seekable_archive(archive.bytes, &master).unwrap_err(),
12214 FormatError::KeyMaterialMismatch
12215 );
12216 }
12217
12218 #[test]
12219 fn public_no_key_verifies_signed_recipientwrap_block_commitment() {
12220 let archive = write_archive_with_root_auth_and_recipient_wrap_records(
12221 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12222 &master_key(),
12223 single_stream_options(),
12224 vec![recipient_wrap_test_record()],
12225 test_root_auth_config(),
12226 |request| Ok(test_root_auth_value(request)),
12227 )
12228 .unwrap();
12229
12230 let verified = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
12231 Ok(test_root_auth_verifies(footer, archive_root))
12232 })
12233 .unwrap();
12234
12235 assert_eq!(verified.volume_format_rev, VOLUME_FORMAT_REV_44);
12236 assert_eq!(verified.total_data_block_count, 3);
12237 }
12238
12239 #[test]
12240 fn public_no_key_rejects_recipientwrap_startup_and_cmra_kdf_mismatch() {
12241 let archive = write_archive_with_root_auth_and_recipient_wrap_records(
12242 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12243 &master_key(),
12244 single_stream_options(),
12245 vec![recipient_wrap_test_record()],
12246 test_root_auth_config(),
12247 |request| Ok(test_root_auth_value(request)),
12248 )
12249 .unwrap();
12250 let mut bytes = archive.bytes;
12251 mutate_top_level_recipient_wrap_public_profile(&mut bytes);
12252
12253 let err = public_no_key_verify_archive_with(&bytes, |footer, archive_root| {
12254 Ok(test_root_auth_verifies(footer, archive_root))
12255 })
12256 .unwrap_err();
12257
12258 assert_eq!(
12259 err,
12260 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
12261 );
12262 }
12263
12264 #[test]
12265 fn public_no_key_rejects_recipientwrap_kdf_profile_mismatch_across_volumes() {
12266 let archive = write_archive_with_root_auth_and_recipient_wrap_records(
12267 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12268 &master_key(),
12269 WriterOptions {
12270 stripe_width: 2,
12271 volume_loss_tolerance: 0,
12272 ..WriterOptions::default()
12273 },
12274 vec![recipient_wrap_test_record()],
12275 test_root_auth_config(),
12276 |request| Ok(test_root_auth_value(request)),
12277 )
12278 .unwrap();
12279 let mut volumes = archive.volumes;
12280 mutate_top_level_recipient_wrap_public_profile(&mut volumes[1]);
12281 mutate_cmra_recipient_wrap_public_profile(&mut volumes[1]);
12282
12283 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
12284 let err = public_no_key_verify_volumes_with(&volume_refs, |footer, archive_root| {
12285 Ok(test_root_auth_verifies(footer, archive_root))
12286 })
12287 .unwrap_err();
12288
12289 assert_eq!(
12290 err,
12291 FormatError::InvalidArchive("public no-key volume global metadata differs")
12292 );
12293 }
12294
12295 #[test]
12296 fn write_archive_defaults_to_current_revision() {
12297 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
12298 let header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
12299
12300 assert_eq!(header.format_version, FORMAT_VERSION);
12301 assert_eq!(header.volume_format_rev, VOLUME_FORMAT_REV);
12302 }
12303
12304 #[test]
12305 fn non_seekable_stream_rejects_future_volume_format_revision() {
12306 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
12307 let mut bytes = archive.bytes;
12308 let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12309 header.volume_format_rev = 45;
12310 bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
12311
12312 assert_eq!(
12313 verify_non_seekable_stream(std::io::Cursor::new(bytes), &master_key()).unwrap_err(),
12314 FormatError::UnsupportedVolumeFormatRevision {
12315 format_version: FORMAT_VERSION,
12316 volume_format_rev: 45,
12317 reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
12318 }
12319 );
12320 }
12321
12322 #[test]
12323 fn open_seekable_archive_rejects_future_volume_format_revision() {
12324 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
12325 let mut bytes = archive.bytes;
12326 let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12327 header.volume_format_rev = 45;
12328 bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
12329
12330 assert_eq!(
12331 open_seekable_archive(CountingReadAt::new(bytes, vec![]), &master_key()).unwrap_err(),
12332 FormatError::UnsupportedVolumeFormatRevision {
12333 format_version: FORMAT_VERSION,
12334 volume_format_rev: 45,
12335 reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
12336 }
12337 );
12338 }
12339
12340 #[test]
12341 fn rejects_payload_tamper_even_with_recomputed_block_crc() {
12342 let mut archive = write_archive(
12343 &[RegularFile::new("file.txt", b"authenticated")],
12344 &master_key(),
12345 single_stream_options(),
12346 )
12347 .unwrap()
12348 .bytes;
12349 let volume = VolumeHeader::parse(&archive[..VOLUME_HEADER_LEN]).unwrap();
12350 let crypto_end = VOLUME_HEADER_LEN + usize::try_from(volume.crypto_header_length).unwrap();
12351 let crypto = CryptoHeader::parse(
12352 &archive[VOLUME_HEADER_LEN..crypto_end],
12353 volume.crypto_header_length,
12354 )
12355 .unwrap();
12356 let block_size = crypto.fixed.block_size as usize;
12357 archive[crypto_end + 16] ^= 1;
12358 let crc_offset = crypto_end + 16 + block_size;
12359 let crc = crc32c::crc32c(&archive[crypto_end..crc_offset]);
12360 archive[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
12361
12362 let opened = open_archive(&archive, &master_key()).unwrap();
12363 assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
12364 }
12365
12366 #[test]
12367 fn list_and_extract_use_final_view_for_duplicate_paths() {
12368 let archive = write_archive(
12369 &[
12370 RegularFile::new("same.txt", b"old"),
12371 RegularFile::new("same.txt", b"newer"),
12372 ],
12373 &master_key(),
12374 single_stream_options(),
12375 )
12376 .unwrap();
12377 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12378
12379 assert_eq!(
12380 opened.list_index_entries().unwrap(),
12381 vec![ArchiveIndexEntry {
12382 path: "same.txt".to_string(),
12383 file_data_size: 5,
12384 }]
12385 );
12386 assert_eq!(
12387 opened.lookup_index_entry("same.txt").unwrap(),
12388 Some(ArchiveIndexEntry {
12389 path: "same.txt".to_string(),
12390 file_data_size: 5,
12391 })
12392 );
12393 assert_eq!(opened.lookup_index_entry("missing.txt").unwrap(), None);
12394 assert_eq!(
12395 opened.list_files().unwrap(),
12396 vec![ArchiveEntry {
12397 path: "same.txt".to_string(),
12398 file_data_size: 5,
12399 kind: TarEntryKind::Regular,
12400 mode: 0o644,
12401 mtime: 0,
12402 diagnostics: Vec::new(),
12403 }]
12404 );
12405 assert_eq!(
12406 opened.extract_file("same.txt").unwrap(),
12407 Some(b"newer".to_vec())
12408 );
12409 opened.verify().unwrap();
12410 }
12411
12412 #[test]
12413 fn index_entries_do_not_decrypt_payload_envelopes() {
12414 let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
12415 corrupt_payload_record(&mut opened.blocks, broken_payload_block);
12416
12417 assert_eq!(
12418 opened.list_index_entries().unwrap(),
12419 vec![
12420 ArchiveIndexEntry {
12421 path: "broken.txt".to_string(),
12422 file_data_size: b"broken payload\n".len() as u64,
12423 },
12424 ArchiveIndexEntry {
12425 path: "healthy.txt".to_string(),
12426 file_data_size: b"healthy payload\n".len() as u64,
12427 },
12428 ]
12429 );
12430 assert_eq!(
12431 opened.lookup_index_entry("broken.txt").unwrap(),
12432 Some(ArchiveIndexEntry {
12433 path: "broken.txt".to_string(),
12434 file_data_size: b"broken payload\n".len() as u64,
12435 })
12436 );
12437 assert_eq!(opened.list_files().unwrap_err(), FormatError::AeadFailure);
12438 }
12439
12440 #[test]
12441 fn extract_file_does_not_decrypt_unselected_payload_envelope() {
12442 let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
12445 corrupt_payload_record(&mut opened.blocks, broken_payload_block);
12446
12447 assert_eq!(
12448 opened.extract_file("healthy.txt").unwrap(),
12449 Some(b"healthy payload\n".to_vec())
12450 );
12451 assert_eq!(
12452 opened.extract_file("broken.txt").unwrap_err(),
12453 FormatError::AeadFailure
12454 );
12455 assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
12456 }
12457
12458 #[test]
12459 fn seekable_extract_does_not_read_unselected_payload_envelope() {
12460 let healthy = pseudo_random_bytes(64 * 1024);
12461 let broken = pseudo_random_bytes(64 * 1024);
12462 let options = WriterOptions {
12463 block_size: 4096,
12464 chunk_size: 4096,
12465 envelope_target_size: 8192,
12466 stripe_width: 1,
12467 volume_loss_tolerance: 0,
12468 bit_rot_buffer_pct: 0,
12469 fec_data_shards: 4,
12470 fec_parity_shards: 0,
12471 index_fec_data_shards: 4,
12472 index_fec_parity_shards: 0,
12473 index_root_fec_data_shards: 4,
12474 index_root_fec_parity_shards: 0,
12475 ..WriterOptions::default()
12476 };
12477 let archive = write_archive(
12478 &[
12479 RegularFile::new("healthy.bin", &healthy),
12480 RegularFile::new("broken.bin", &broken),
12481 ],
12482 &master_key(),
12483 options,
12484 )
12485 .unwrap();
12486 let eager = open_archive(&archive.bytes, &master_key()).unwrap();
12487 let healthy_envelopes = envelope_indices_for_path(&eager, "healthy.bin");
12488 let broken_envelopes = envelope_entries_for_path(&eager, "broken.bin");
12489 let denied_block_indices = broken_envelopes
12490 .iter()
12491 .filter(|envelope| !healthy_envelopes.contains(&envelope.envelope_index))
12492 .flat_map(|envelope| {
12493 let block_count =
12494 envelope.data_block_count as u64 + envelope.parity_block_count as u64;
12495 envelope.first_block_index..envelope.first_block_index + block_count
12496 })
12497 .collect::<BTreeSet<_>>();
12498 assert!(
12499 !denied_block_indices.is_empty(),
12500 "fixture must place broken.bin in at least one unshared envelope"
12501 );
12502 let denied_ranges = block_record_slots(&archive.bytes)
12503 .into_iter()
12504 .filter_map(|(offset, len, record)| {
12505 denied_block_indices
12506 .contains(&record.block_index)
12507 .then_some((offset as u64, (offset + len) as u64))
12508 })
12509 .collect::<Vec<_>>();
12510 assert!(!denied_ranges.is_empty());
12511
12512 let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
12513 let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
12514
12515 assert_eq!(opened.extract_file("healthy.bin").unwrap(), Some(healthy));
12516 for (read_start, read_end) in reader.reads() {
12517 assert!(
12518 denied_ranges
12519 .iter()
12520 .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
12521 "targeted extract read an unrelated payload BlockRecord range"
12522 );
12523 }
12524 assert_eq!(
12525 opened.extract_file("broken.bin").unwrap_err(),
12526 FormatError::InvalidArchive("denied test read")
12527 );
12528 }
12529
12530 #[test]
12531 fn extract_file_to_writer_streams_before_reading_later_envelopes() {
12532 struct FailOnFirstWrite;
12533
12534 impl std::io::Write for FailOnFirstWrite {
12535 fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
12536 Err(std::io::Error::other("sink stopped"))
12537 }
12538
12539 fn flush(&mut self) -> std::io::Result<()> {
12540 Ok(())
12541 }
12542 }
12543
12544 let payload = pseudo_random_bytes(128 * 1024);
12545 let options = WriterOptions {
12546 block_size: 4096,
12547 chunk_size: 4096,
12548 envelope_target_size: 8192,
12549 stripe_width: 1,
12550 volume_loss_tolerance: 0,
12551 bit_rot_buffer_pct: 0,
12552 fec_data_shards: 4,
12553 fec_parity_shards: 0,
12554 index_fec_data_shards: 4,
12555 index_fec_parity_shards: 0,
12556 index_root_fec_data_shards: 4,
12557 index_root_fec_parity_shards: 0,
12558 ..WriterOptions::default()
12559 };
12560 let archive = write_archive(
12561 &[RegularFile::new("large.bin", &payload)],
12562 &master_key(),
12563 options,
12564 )
12565 .unwrap();
12566 let eager = open_archive(&archive.bytes, &master_key()).unwrap();
12567 let envelopes = envelope_entries_for_path(&eager, "large.bin");
12568 let first_envelope = envelopes
12569 .first()
12570 .expect("large fixture should have at least one envelope")
12571 .envelope_index;
12572 let later_envelope_blocks = envelopes
12573 .iter()
12574 .filter(|entry| entry.envelope_index != first_envelope)
12575 .flat_map(|entry| {
12576 let block_count = entry.data_block_count as u64 + entry.parity_block_count as u64;
12577 entry.first_block_index..entry.first_block_index + block_count
12578 })
12579 .collect::<BTreeSet<_>>();
12580 assert!(
12581 !later_envelope_blocks.is_empty(),
12582 "fixture must span more than one payload envelope"
12583 );
12584 let denied_ranges = block_record_slots(&archive.bytes)
12585 .into_iter()
12586 .filter_map(|(offset, len, record)| {
12587 later_envelope_blocks
12588 .contains(&record.block_index)
12589 .then_some((offset as u64, (offset + len) as u64))
12590 })
12591 .collect::<Vec<_>>();
12592 assert!(!denied_ranges.is_empty());
12593
12594 let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
12595 let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
12596 let mut writer = FailOnFirstWrite;
12597
12598 let err = opened
12599 .extract_file_to_writer("large.bin", &mut writer)
12600 .unwrap_err();
12601 assert_eq!(err.to_string(), "extraction output write failed");
12602 for (read_start, read_end) in reader.reads() {
12603 assert!(
12604 denied_ranges
12605 .iter()
12606 .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
12607 "streaming writer read a later payload envelope before surfacing writer failure"
12608 );
12609 }
12610 }
12611
12612 #[test]
12613 fn extract_file_to_writer_writes_bounded_chunks() {
12614 struct ChunkRecorder {
12615 total: usize,
12616 max_write: usize,
12617 writes: usize,
12618 }
12619
12620 impl std::io::Write for ChunkRecorder {
12621 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
12622 self.total += buf.len();
12623 self.max_write = self.max_write.max(buf.len());
12624 self.writes += 1;
12625 Ok(buf.len())
12626 }
12627
12628 fn flush(&mut self) -> std::io::Result<()> {
12629 Ok(())
12630 }
12631 }
12632
12633 let payload = pseudo_random_bytes(128 * 1024);
12634 let options = WriterOptions {
12635 block_size: 4096,
12636 chunk_size: 4096,
12637 envelope_target_size: 8192,
12638 stripe_width: 1,
12639 volume_loss_tolerance: 0,
12640 bit_rot_buffer_pct: 0,
12641 fec_data_shards: 4,
12642 fec_parity_shards: 0,
12643 index_fec_data_shards: 4,
12644 index_fec_parity_shards: 0,
12645 index_root_fec_data_shards: 4,
12646 index_root_fec_parity_shards: 0,
12647 ..WriterOptions::default()
12648 };
12649 let archive = write_archive(
12650 &[RegularFile::new("large.bin", &payload)],
12651 &master_key(),
12652 options,
12653 )
12654 .unwrap();
12655 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12656 let mut writer = ChunkRecorder {
12657 total: 0,
12658 max_write: 0,
12659 writes: 0,
12660 };
12661
12662 opened
12663 .extract_file_to_writer("large.bin", &mut writer)
12664 .unwrap()
12665 .unwrap();
12666
12667 assert_eq!(writer.total, payload.len());
12668 assert!(writer.writes > 1);
12669 assert!(
12670 writer.max_write <= options.chunk_size as usize,
12671 "writer saw a {} byte chunk, larger than the {} byte frame target",
12672 writer.max_write,
12673 options.chunk_size
12674 );
12675 }
12676
12677 #[test]
12678 fn extract_file_to_writer_with_progress_reports_payload_bytes() {
12679 struct ChunkRecorder {
12680 total: usize,
12681 }
12682
12683 impl std::io::Write for ChunkRecorder {
12684 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
12685 self.total += buf.len();
12686 Ok(buf.len())
12687 }
12688
12689 fn flush(&mut self) -> std::io::Result<()> {
12690 Ok(())
12691 }
12692 }
12693
12694 let payload = pseudo_random_bytes(128 * 1024);
12695 let options = WriterOptions {
12696 block_size: 4096,
12697 chunk_size: 4096,
12698 envelope_target_size: 8192,
12699 stripe_width: 1,
12700 volume_loss_tolerance: 0,
12701 bit_rot_buffer_pct: 0,
12702 fec_data_shards: 4,
12703 fec_parity_shards: 0,
12704 index_fec_data_shards: 4,
12705 index_fec_parity_shards: 0,
12706 index_root_fec_data_shards: 4,
12707 index_root_fec_parity_shards: 0,
12708 ..WriterOptions::default()
12709 };
12710 let archive = write_archive(
12711 &[RegularFile::new("large.bin", &payload)],
12712 &master_key(),
12713 options,
12714 )
12715 .unwrap();
12716 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12717 let mut writer = ChunkRecorder { total: 0 };
12718 let mut progress_events = Vec::new();
12719 let mut progress = |archive_path: &str, bytes: u64| {
12720 progress_events.push((archive_path.to_owned(), bytes));
12721 };
12722
12723 opened
12724 .extract_file_to_writer_with_progress("large.bin", &mut writer, &mut progress)
12725 .unwrap()
12726 .unwrap();
12727
12728 let reported_bytes = progress_events.iter().map(|(_, bytes)| *bytes).sum::<u64>();
12729 assert_eq!(writer.total, payload.len());
12730 assert_eq!(reported_bytes, payload.len() as u64);
12731 assert!(progress_events.len() > 1);
12732 assert!(progress_events.iter().all(|(path, _)| path == "large.bin"));
12733 }
12734
12735 #[test]
12736 fn streaming_filesystem_extract_does_not_publish_partial_file_on_late_payload_error() {
12737 let payload = pseudo_random_bytes(128 * 1024);
12738 let options = WriterOptions {
12739 block_size: 4096,
12740 chunk_size: 4096,
12741 envelope_target_size: 8192,
12742 stripe_width: 1,
12743 volume_loss_tolerance: 0,
12744 bit_rot_buffer_pct: 0,
12745 fec_data_shards: 4,
12746 fec_parity_shards: 0,
12747 index_fec_data_shards: 4,
12748 index_fec_parity_shards: 0,
12749 index_root_fec_data_shards: 4,
12750 index_root_fec_parity_shards: 0,
12751 ..WriterOptions::default()
12752 };
12753 let archive = write_archive(
12754 &[RegularFile::new("large.bin", &payload)],
12755 &master_key(),
12756 options,
12757 )
12758 .unwrap();
12759 let eager = open_archive(&archive.bytes, &master_key()).unwrap();
12760 let envelopes = envelope_entries_for_path(&eager, "large.bin");
12761 let last_envelope = envelopes
12762 .last()
12763 .expect("large fixture should have at least one envelope");
12764 assert_ne!(
12765 envelopes.first().unwrap().envelope_index,
12766 last_envelope.envelope_index,
12767 "fixture must span more than one payload envelope"
12768 );
12769 let corrupt_slot = block_record_slots(&archive.bytes)
12770 .into_iter()
12771 .enumerate()
12772 .find_map(|(slot, (_, _, record))| {
12773 (record.block_index == last_envelope.first_block_index).then_some(slot)
12774 })
12775 .unwrap();
12776 let mut corrupted = archive.bytes;
12777 corrupt_block_record_payload_at_slot(&mut corrupted, corrupt_slot);
12778 let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
12779 let tmp = tempfile::tempdir().unwrap();
12780
12781 assert!(matches!(
12782 opened
12783 .extract_file_to("large.bin", tmp.path(), SafeExtractionOptions::default())
12784 .unwrap_err(),
12785 FormatError::AeadFailure | FormatError::FecTooFewAvailableShards
12786 ));
12787 assert!(!tmp.path().join("large.bin").exists());
12788 assert_eq!(std::fs::read_dir(tmp.path()).unwrap().count(), 0);
12789 }
12790
12791 #[test]
12792 fn bootstrap_sidecar_opens_lists_verifies_and_extracts() {
12793 let archive = write_archive(
12794 &[RegularFile::new("dir/sidecar.txt", b"hello sidecar")],
12795 &master_key(),
12796 single_stream_options(),
12797 )
12798 .unwrap();
12799 let opened = open_archive_with_bootstrap_sidecar(
12800 &archive.bytes,
12801 &archive.bootstrap_sidecar,
12802 &master_key(),
12803 )
12804 .unwrap();
12805
12806 assert_eq!(
12807 opened.list_files().unwrap(),
12808 vec![ArchiveEntry {
12809 path: "dir/sidecar.txt".to_string(),
12810 file_data_size: 13,
12811 kind: TarEntryKind::Regular,
12812 mode: 0o644,
12813 mtime: 0,
12814 diagnostics: Vec::new(),
12815 }]
12816 );
12817 assert_eq!(
12818 opened.extract_file("dir/sidecar.txt").unwrap(),
12819 Some(b"hello sidecar".to_vec())
12820 );
12821 opened.verify().unwrap();
12822 }
12823
12824 #[test]
12825 fn fast_verify_plaintext_zero_recovery_defers_payload_semantics() {
12826 let options = WriterOptions {
12827 aead_algo: AeadAlgo::None,
12828 volume_loss_tolerance: 0,
12829 bit_rot_buffer_pct: 0,
12830 ..single_stream_options()
12831 };
12832 let archive = write_archive_unencrypted(
12833 &[RegularFile::new(
12834 "payload.txt",
12835 b"payload bytes large enough to produce a zstd frame",
12836 )],
12837 options,
12838 )
12839 .unwrap();
12840 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12841 let tables = opened.load_payload_index_tables().unwrap();
12842 let first_envelope = tables.envelopes.values().next().unwrap();
12843 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
12844 let crypto_end = VOLUME_HEADER_LEN + volume_header.crypto_header_length as usize;
12845 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
12846 let payload_offset = crypto_end + first_envelope.first_block_index as usize * record_len;
12847
12848 let mut tampered = archive.bytes.clone();
12849 tampered[payload_offset + 16] ^= 0x01;
12850 let crc_offset = payload_offset + 16 + opened.crypto_header.block_size as usize;
12851 let crc = crc32c::crc32c(&tampered[payload_offset..crc_offset]);
12852 tampered[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
12853
12854 let tampered_opened = open_archive(&tampered, &master_key()).unwrap();
12855 assert!(tampered_opened.fast_verify_defers_payload_semantics());
12856 tampered_opened.verify_content_fast().unwrap();
12857 assert!(tampered_opened.verify_content().is_err());
12858 }
12859
12860 #[test]
12861 fn fast_verify_root_auth_archive_requires_full_root_auth_scan() {
12862 let archive = write_archive_with_root_auth(
12863 &[RegularFile::new("signed.txt", b"root-auth payload")],
12864 &master_key(),
12865 single_stream_options(),
12866 RootAuthWriterConfig {
12867 authenticator_id: 0x7777,
12868 signer_identity_type: 1,
12869 signer_identity: b"test signer",
12870 authenticator_value_length: 32,
12871 },
12872 |request| Ok(request.archive_root.to_vec()),
12873 )
12874 .unwrap();
12875
12876 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12877 assert!(!opened.fast_verify_defers_payload_semantics());
12878 assert!(matches!(
12879 opened.verify_content_fast().unwrap().mode,
12880 ContentVerificationMode::Fast
12881 ));
12882 }
12883
12884 #[test]
12885 fn fast_verify_dictionary_archive_does_not_defer_payload_semantics() {
12886 let archive = write_archive_with_dictionary(
12887 &[RegularFile::new("dict.txt", b"dictionary payload")],
12888 &master_key(),
12889 single_stream_options(),
12890 dictionary(),
12891 )
12892 .unwrap();
12893
12894 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12895 assert!(!opened.fast_verify_defers_payload_semantics());
12896 }
12897
12898 #[test]
12899 fn fast_verify_encrypted_archive_does_not_defer_payload_semantics() {
12900 let options = WriterOptions {
12901 aead_algo: AeadAlgo::AesGcmSiv256,
12902 volume_loss_tolerance: 0,
12903 bit_rot_buffer_pct: 0,
12904 ..single_stream_options()
12905 };
12906 let archive = write_archive(
12907 &[RegularFile::new("payload.txt", b"encrypted payload")],
12908 &master_key(),
12909 options,
12910 )
12911 .unwrap();
12912
12913 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12914 assert!(!opened.fast_verify_defers_payload_semantics());
12915 }
12916
12917 #[test]
12918 fn fast_verify_repair_archive_does_not_defer_payload_semantics() {
12919 let options = WriterOptions {
12920 fec_parity_shards: 2,
12921 volume_loss_tolerance: 0,
12922 bit_rot_buffer_pct: 0,
12923 ..single_stream_options()
12924 };
12925 let archive = write_archive(
12926 &[RegularFile::new("payload.txt", b"payload for repair")],
12927 &master_key(),
12928 options,
12929 )
12930 .unwrap();
12931
12932 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12933 assert!(!opened.fast_verify_defers_payload_semantics());
12934 }
12935
12936 #[test]
12937 fn dictionary_archive_opens_lists_verifies_and_extracts_seekable() {
12938 let archive = write_archive_with_dictionary(
12939 &[RegularFile::new(
12940 "dir/dict.txt",
12941 b"common words common words dictionary payload",
12942 )],
12943 &master_key(),
12944 single_stream_options(),
12945 dictionary(),
12946 )
12947 .unwrap();
12948 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12949
12950 assert_eq!(opened.crypto_header.has_dictionary, 1);
12951 assert!(opened.index_root.header.dictionary_data_block_count > 0);
12952 assert_eq!(
12953 opened.list_files().unwrap(),
12954 vec![ArchiveEntry {
12955 path: "dir/dict.txt".to_string(),
12956 file_data_size: 44,
12957 kind: TarEntryKind::Regular,
12958 mode: 0o644,
12959 mtime: 0,
12960 diagnostics: Vec::new(),
12961 }]
12962 );
12963 assert_eq!(
12964 opened.extract_file("dir/dict.txt").unwrap(),
12965 Some(b"common words common words dictionary payload".to_vec())
12966 );
12967 opened.verify().unwrap();
12968 }
12969
12970 #[test]
12971 fn dictionary_object_tamper_fails_before_payload_decompression() {
12972 let archive = write_archive_with_dictionary(
12973 &[RegularFile::new(
12974 "dir/dict.txt",
12975 b"common words common words dictionary payload",
12976 )],
12977 &master_key(),
12978 single_stream_options(),
12979 dictionary(),
12980 )
12981 .unwrap();
12982 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12983 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
12984 let crypto_end = VOLUME_HEADER_LEN + volume_header.crypto_header_length as usize;
12985 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
12986 let dictionary_offset =
12987 crypto_end + opened.index_root.header.dictionary_first_block as usize * record_len;
12988
12989 let mut tampered = archive.bytes.clone();
12990 tampered[dictionary_offset + 16] ^= 0x01;
12991 let crc_offset = dictionary_offset + 16 + opened.crypto_header.block_size as usize;
12992 let crc = crc32c::crc32c(&tampered[dictionary_offset..crc_offset]);
12993 tampered[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
12994
12995 assert_eq!(
12996 open_archive(&tampered, &master_key()).unwrap_err(),
12997 FormatError::AeadFailure
12998 );
12999 }
13000
13001 #[test]
13002 fn dictionary_archive_bootstraps_from_sidecar_for_non_seekable_open() {
13003 let archive = write_archive_with_dictionary(
13004 &[RegularFile::new(
13005 "dict-sidecar.txt",
13006 b"common words common words sidecar payload",
13007 )],
13008 &master_key(),
13009 single_stream_options(),
13010 dictionary(),
13011 )
13012 .unwrap();
13013 let opened = open_non_seekable_archive(
13014 &archive.bytes,
13015 &master_key(),
13016 Some(&archive.bootstrap_sidecar),
13017 )
13018 .unwrap();
13019
13020 assert_eq!(
13021 opened.extract_file("dict-sidecar.txt").unwrap(),
13022 Some(b"common words common words sidecar payload".to_vec())
13023 );
13024 opened.verify().unwrap();
13025 }
13026
13027 #[test]
13028 fn non_seekable_full_sidecar_bootstraps_when_terminal_trailer_is_corrupt() {
13029 let archive = write_archive(
13030 &[RegularFile::new(
13031 "sidecar-terminal.txt",
13032 b"sidecar authority",
13033 )],
13034 &master_key(),
13035 single_stream_options(),
13036 )
13037 .unwrap();
13038 let mut corrupted = archive.bytes.clone();
13039 corrupt_v41_terminal_recovery(&mut corrupted);
13040 assert!(open_archive(&corrupted, &master_key()).is_err());
13041
13042 let opened =
13043 open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
13044 .unwrap();
13045
13046 assert!(opened.volume_trailer.is_none());
13047 assert_eq!(
13048 opened.extract_file("sidecar-terminal.txt").unwrap(),
13049 Some(b"sidecar authority".to_vec())
13050 );
13051 opened.verify().unwrap();
13052 }
13053
13054 #[test]
13055 fn dictionary_full_sidecar_bootstraps_when_terminal_material_is_absent() {
13056 let archive = write_archive_with_dictionary(
13057 &[RegularFile::new(
13058 "dict-no-terminal.txt",
13059 b"common words common words without terminal",
13060 )],
13061 &master_key(),
13062 single_stream_options(),
13063 dictionary(),
13064 )
13065 .unwrap();
13066 let terminal_offset = terminal_material_offset(&archive.bytes);
13067 let truncated = archive.bytes[..terminal_offset].to_vec();
13068 assert!(open_archive(&truncated, &master_key()).is_err());
13069
13070 let opened =
13071 open_non_seekable_archive(&truncated, &master_key(), Some(&archive.bootstrap_sidecar))
13072 .unwrap();
13073
13074 assert!(opened.volume_trailer.is_none());
13075 assert_eq!(
13076 opened.extract_file("dict-no-terminal.txt").unwrap(),
13077 Some(b"common words common words without terminal".to_vec())
13078 );
13079 opened.verify().unwrap();
13080 }
13081
13082 #[test]
13083 fn bootstrap_sidecar_treats_crc_failed_payload_block_as_erasure() {
13084 let archive = write_archive(
13085 &[RegularFile::new(
13086 "sidecar-erasure.txt",
13087 b"repair through sidecar",
13088 )],
13089 &master_key(),
13090 single_stream_options(),
13091 )
13092 .unwrap();
13093 let mut corrupted = archive.bytes.clone();
13094 corrupt_first_block_record_payload(&mut corrupted);
13095
13096 let opened = open_archive_with_bootstrap_sidecar(
13097 &corrupted,
13098 &archive.bootstrap_sidecar,
13099 &master_key(),
13100 )
13101 .unwrap();
13102 assert_eq!(
13103 opened.extract_file("sidecar-erasure.txt").unwrap(),
13104 Some(b"repair through sidecar".to_vec())
13105 );
13106 }
13107
13108 #[test]
13109 fn extraction_rejects_logical_payload_above_total_size_cap() {
13110 let archive = write_archive(
13111 &[RegularFile::new("cap.txt", b"payload")],
13112 &master_key(),
13113 single_stream_options(),
13114 )
13115 .unwrap();
13116 let options = ReaderOptions {
13117 max_total_extraction_size: 3,
13118 ..ReaderOptions::default()
13119 };
13120 let opened =
13121 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
13122
13123 assert_eq!(
13124 opened.extract_file("cap.txt").unwrap_err(),
13125 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
13126 );
13127 }
13128
13129 #[test]
13130 fn verify_does_not_apply_extraction_payload_cap() {
13131 let archive = write_archive(
13132 &[RegularFile::new("verify-cap.txt", b"payload")],
13133 &master_key(),
13134 single_stream_options(),
13135 )
13136 .unwrap();
13137 let options = ReaderOptions {
13138 max_total_extraction_size: 3,
13139 ..ReaderOptions::default()
13140 };
13141 let opened =
13142 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
13143
13144 opened.verify().unwrap();
13145 assert_eq!(
13146 opened.extract_file("verify-cap.txt").unwrap_err(),
13147 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
13148 );
13149 }
13150
13151 #[test]
13152 fn verify_streams_past_legacy_in_memory_tar_cap() {
13153 let data = vec![0x5a; 4096];
13154 let archive = write_archive(
13155 &[RegularFile::new("verify-large.txt", &data)],
13156 &master_key(),
13157 single_stream_options(),
13158 )
13159 .unwrap();
13160 let options = ReaderOptions {
13161 max_verify_tar_size: 1,
13162 ..ReaderOptions::default()
13163 };
13164 let opened =
13165 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
13166
13167 opened.verify().unwrap();
13168 }
13169
13170 #[test]
13171 fn dictionary_sidecar_requires_dictionary_record_section() {
13172 let archive = write_archive_with_dictionary(
13173 &[RegularFile::new("dict-missing.txt", b"common words")],
13174 &master_key(),
13175 single_stream_options(),
13176 dictionary(),
13177 )
13178 .unwrap();
13179 let header = BootstrapSidecarHeader::parse(
13180 &archive.bootstrap_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN],
13181 )
13182 .unwrap();
13183 let mut missing_dictionary =
13184 archive.bootstrap_sidecar[..header.dictionary_records_offset as usize].to_vec();
13185 rewrite_sidecar_header(&mut missing_dictionary, &master_key(), |header| {
13186 header.flags &= !0x04;
13187 header.dictionary_records_offset = 0;
13188 header.dictionary_records_length = 0;
13189 });
13190
13191 assert_eq!(
13192 open_non_seekable_archive(&archive.bytes, &master_key(), Some(&missing_dictionary))
13193 .unwrap_err(),
13194 FormatError::ReaderUnsupported("dictionary bootstrap required")
13195 );
13196 }
13197
13198 #[test]
13199 fn dictionary_sidecar_records_are_validated_against_dictionary_extent() {
13200 let archive = write_archive_with_dictionary(
13201 &[RegularFile::new("dict-sidecar-kind.txt", b"common words")],
13202 &master_key(),
13203 single_stream_options(),
13204 dictionary(),
13205 )
13206 .unwrap();
13207
13208 let mut wrong_kind = archive.bootstrap_sidecar.clone();
13209 mutate_sidecar_dictionary_record(&mut wrong_kind, 0, |record| {
13210 record.kind = BlockKind::IndexRootData;
13211 });
13212 assert_eq!(
13213 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
13214 .unwrap_err(),
13215 FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
13216 );
13217
13218 let mut wrong_last = archive.bootstrap_sidecar.clone();
13219 mutate_sidecar_dictionary_record(&mut wrong_last, 0, |record| {
13220 record.flags = 0;
13221 });
13222 assert_eq!(
13223 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
13224 .unwrap_err(),
13225 FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
13226 );
13227 }
13228
13229 #[test]
13230 fn non_seekable_random_access_requires_sidecar() {
13231 let archive = write_archive(
13232 &[RegularFile::new("file.txt", b"payload")],
13233 &master_key(),
13234 single_stream_options(),
13235 )
13236 .unwrap();
13237
13238 assert_eq!(
13239 open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
13240 FormatError::ReaderUnsupported(
13241 "non-seekable random access requires a bootstrap sidecar"
13242 )
13243 );
13244 assert!(open_non_seekable_archive(
13245 &archive.bytes,
13246 &master_key(),
13247 Some(&archive.bootstrap_sidecar)
13248 )
13249 .is_ok());
13250 }
13251
13252 #[test]
13253 fn non_seekable_bootstrap_rejects_index_root_only_sidecar() {
13254 let archive = write_archive(
13255 &[RegularFile::new("sparse.txt", b"sparse sidecar")],
13256 &master_key(),
13257 single_stream_options(),
13258 )
13259 .unwrap();
13260 let index_root_only = sparse_bootstrap_sidecar(
13261 &archive.bootstrap_sidecar,
13262 &master_key(),
13263 false,
13264 true,
13265 false,
13266 );
13267
13268 assert_eq!(
13269 open_non_seekable_archive(&archive.bytes, &master_key(), Some(&index_root_only))
13270 .unwrap_err(),
13271 FormatError::ReaderUnsupported(
13272 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
13273 )
13274 );
13275 }
13276
13277 #[test]
13278 fn seekable_sidecar_uses_index_root_records_after_terminal_manifest_authority() {
13279 let archive = write_archive(
13280 &[RegularFile::new("sparse-index.txt", b"recover index root")],
13281 &master_key(),
13282 single_stream_options(),
13283 )
13284 .unwrap();
13285 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13286 let mut corrupted = archive.bytes.clone();
13287 corrupt_object_extent_records(
13288 &mut corrupted,
13289 index_root_extent_from_manifest(&opened.manifest_footer),
13290 );
13291 assert!(open_archive(&corrupted, &master_key()).is_err());
13292
13293 let index_root_only = sparse_bootstrap_sidecar(
13294 &archive.bootstrap_sidecar,
13295 &master_key(),
13296 false,
13297 true,
13298 false,
13299 );
13300 let recovered =
13301 open_archive_with_bootstrap_sidecar(&corrupted, &index_root_only, &master_key())
13302 .unwrap();
13303
13304 assert_eq!(
13305 recovered.extract_file("sparse-index.txt").unwrap(),
13306 Some(b"recover index root".to_vec())
13307 );
13308 recovered.verify().unwrap();
13309 }
13310
13311 #[test]
13312 fn seekable_sidecar_uses_dictionary_records_after_index_root_authority() {
13313 let archive = write_archive_with_dictionary(
13314 &[RegularFile::new(
13315 "sparse-dict.txt",
13316 b"common words common words sparse dictionary",
13317 )],
13318 &master_key(),
13319 single_stream_options(),
13320 dictionary(),
13321 )
13322 .unwrap();
13323 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13324 let mut corrupted = archive.bytes.clone();
13325 corrupt_object_extent_records(
13326 &mut corrupted,
13327 dictionary_extent_from_index_root(&opened.index_root).unwrap(),
13328 );
13329 assert!(open_archive(&corrupted, &master_key()).is_err());
13330
13331 let dictionary_only = sparse_bootstrap_sidecar(
13332 &archive.bootstrap_sidecar,
13333 &master_key(),
13334 false,
13335 false,
13336 true,
13337 );
13338 assert_eq!(
13339 open_non_seekable_archive(&archive.bytes, &master_key(), Some(&dictionary_only))
13340 .unwrap_err(),
13341 FormatError::ReaderUnsupported(
13342 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
13343 )
13344 );
13345
13346 let recovered =
13347 open_archive_with_bootstrap_sidecar(&corrupted, &dictionary_only, &master_key())
13348 .unwrap();
13349 assert_eq!(
13350 recovered.extract_file("sparse-dict.txt").unwrap(),
13351 Some(b"common words common words sparse dictionary".to_vec())
13352 );
13353 recovered.verify().unwrap();
13354 }
13355
13356 #[test]
13357 fn sequential_extracts_dictionary_free_tar_stream() {
13358 let archive = write_archive(
13359 &[RegularFile::new("seq.txt", b"streaming")],
13360 &master_key(),
13361 single_stream_options(),
13362 )
13363 .unwrap();
13364
13365 let tar_stream = sequential_extract_tar_stream(&archive.bytes, &master_key()).unwrap();
13366 let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
13367 assert_eq!(member.path, b"seq.txt");
13368 assert_eq!(member.data, b"streaming");
13369 }
13370
13371 #[test]
13372 fn sequential_rejects_logical_payload_above_total_size_cap() {
13373 let archive = write_archive(
13374 &[RegularFile::new("seq-cap.txt", b"payload")],
13375 &master_key(),
13376 single_stream_options(),
13377 )
13378 .unwrap();
13379 let options = ReaderOptions {
13380 max_total_extraction_size: 3,
13381 ..ReaderOptions::default()
13382 };
13383
13384 assert_eq!(
13385 sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
13386 .unwrap_err(),
13387 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
13388 );
13389 }
13390
13391 #[test]
13392 fn sequential_rejects_tar_stream_above_buffer_cap_during_decode() {
13393 let archive = write_archive(
13394 &[RegularFile::new("seq-buffer-cap.txt", b"payload")],
13395 &master_key(),
13396 single_stream_options(),
13397 )
13398 .unwrap();
13399 let options = ReaderOptions {
13400 max_verify_tar_size: 512,
13401 ..ReaderOptions::default()
13402 };
13403
13404 assert_eq!(
13405 sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
13406 .unwrap_err(),
13407 FormatError::ReaderUnsupported(
13408 "sequential tar stream exceeds configured verification cap"
13409 )
13410 );
13411 }
13412
13413 #[test]
13414 fn sequential_repairs_crc_failed_payload_data_when_parity_is_guaranteed() {
13415 let archive = write_archive(
13416 &[RegularFile::new("seq-erasure.txt", b"stream repair")],
13417 &master_key(),
13418 single_stream_options(),
13419 )
13420 .unwrap();
13421 let mut corrupted = archive.bytes;
13422 corrupt_first_block_record_payload(&mut corrupted);
13423
13424 let tar_stream = sequential_extract_tar_stream(&corrupted, &master_key()).unwrap();
13425 let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
13426 assert_eq!(member.path, b"seq-erasure.txt");
13427 assert_eq!(member.data, b"stream repair");
13428 }
13429
13430 #[test]
13431 fn sequential_rejects_crc_failed_payload_data_without_guaranteed_parity() {
13432 let archive = write_archive(
13433 &[RegularFile::new("seq-no-parity.txt", b"no repair")],
13434 &master_key(),
13435 WriterOptions {
13436 bit_rot_buffer_pct: 0,
13437 fec_parity_shards: 0,
13438 index_fec_parity_shards: 0,
13439 index_root_fec_parity_shards: 0,
13440 ..single_stream_options()
13441 },
13442 )
13443 .unwrap();
13444 let mut corrupted = archive.bytes;
13445 corrupt_first_block_record_payload(&mut corrupted);
13446
13447 assert_eq!(
13448 sequential_extract_tar_stream(&corrupted, &master_key()).unwrap_err(),
13449 FormatError::BadCrc {
13450 structure: "BlockRecord"
13451 }
13452 );
13453 }
13454
13455 #[test]
13456 fn sequential_rejects_when_terminal_authentication_fails_without_returning_bytes() {
13457 let archive = write_archive(
13458 &[RegularFile::new(
13459 "seq.txt",
13460 b"payload must not be returned after terminal auth failure",
13461 )],
13462 &master_key(),
13463 single_stream_options(),
13464 )
13465 .unwrap();
13466 let mut corrupted = archive.bytes;
13467 corrupt_v41_terminal_recovery(&mut corrupted);
13468
13469 match sequential_extract_tar_stream(&corrupted, &master_key()) {
13470 Ok(bytes) => panic!(
13471 "sequential helper returned {} decoded byte(s) despite terminal HMAC failure",
13472 bytes.len()
13473 ),
13474 Err(err) => assert_eq!(
13475 err,
13476 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
13477 ),
13478 }
13479 }
13480
13481 #[test]
13482 fn sequential_rejects_dictionary_archive_without_bootstrap_before_payload_release() {
13483 let archive = write_archive_with_dictionary(
13484 &[RegularFile::new(
13485 "seq-dict.txt",
13486 b"common words common words dictionary payload",
13487 )],
13488 &master_key(),
13489 single_stream_options(),
13490 b"common words dictionary",
13491 )
13492 .unwrap();
13493
13494 match sequential_extract_tar_stream(&archive.bytes, &master_key()) {
13495 Ok(bytes) => panic!(
13496 "sequential helper returned {} decoded byte(s) for dictionary archive without bootstrap",
13497 bytes.len()
13498 ),
13499 Err(err) => assert_eq!(
13500 err,
13501 FormatError::ReaderUnsupported(
13502 "dictionary bootstrap required for non-seekable sequential extraction"
13503 )
13504 ),
13505 }
13506 }
13507
13508 #[test]
13509 fn non_seekable_dictionary_error_keeps_missing_bootstrap_wording() {
13510 let archive = write_archive_with_dictionary(
13511 &[RegularFile::new(
13512 "seq-dict-open.txt",
13513 b"common words common words bootstrap required",
13514 )],
13515 &master_key(),
13516 single_stream_options(),
13517 b"common words bootstrap",
13518 )
13519 .unwrap();
13520
13521 assert_eq!(
13522 open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
13523 FormatError::ReaderUnsupported(
13524 "non-seekable random access requires a bootstrap sidecar"
13525 )
13526 );
13527 }
13528
13529 #[test]
13530 fn sequential_zstd_stream_rejects_skippable_frame_segments() {
13531 let skippable = [0x50, 0x2a, 0x4d, 0x18, 0, 0, 0, 0];
13532 let mut output = Vec::new();
13533
13534 assert_eq!(
13535 decode_concatenated_zstd_frames_with_cap(
13536 &skippable,
13537 None,
13538 &mut output,
13539 usize::MAX,
13540 None,
13541 )
13542 .unwrap_err(),
13543 FormatError::NotStandardZstdFrame
13544 );
13545 assert!(output.is_empty());
13546 }
13547
13548 #[test]
13549 fn live_non_seekable_verify_stream_accepts_single_volume_archive() {
13550 let archive = write_archive(
13551 &[RegularFile::new("live.txt", b"stream verify")],
13552 &master_key(),
13553 single_stream_options(),
13554 )
13555 .unwrap();
13556
13557 let report =
13558 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
13559
13560 assert_eq!(report.file_count, 1);
13561 assert_eq!(report.total_volumes, 1);
13562 assert_eq!(report.root_auth, SequentialRootAuthStatus::Absent);
13563 assert!(report.payload_block_count > 0);
13564 }
13565
13566 #[test]
13567 fn live_non_seekable_verify_stream_accepts_recipientwrap_archive() {
13568 let master = master_key();
13569 let archive = write_archive_with_recipient_wrap_records(
13570 &[RegularFile::new(
13571 "wrapped-live.txt",
13572 b"stream recipient verify",
13573 )],
13574 &master,
13575 single_stream_options(),
13576 vec![recipient_wrap_test_record()],
13577 )
13578 .unwrap();
13579
13580 let mut called = false;
13581 let report = verify_non_seekable_stream_with_recipient_wrap_resolver_options(
13582 std::io::Cursor::new(archive.bytes),
13583 |context| {
13584 called = true;
13585 assert_eq!(
13586 context.archive_identity.volume_format_rev,
13587 VOLUME_FORMAT_REV_44
13588 );
13589 assert_eq!(context.record.profile_id, 1);
13590 Ok(vec![master.0])
13591 },
13592 NonSeekableReaderOptions::default(),
13593 )
13594 .unwrap();
13595
13596 assert!(called);
13597 assert_eq!(report.file_count, 1);
13598 assert_eq!(report.total_volumes, 1);
13599 assert_eq!(report.root_auth, SequentialRootAuthStatus::Absent);
13600 }
13601
13602 #[test]
13603 fn live_non_seekable_recipientwrap_resolver_rejects_unencrypted_archive() {
13604 let archive = write_archive_unencrypted(
13605 &[RegularFile::new("plain-live.txt", b"plaintext payload")],
13606 single_stream_options(),
13607 )
13608 .unwrap();
13609
13610 let mut called = false;
13611 let err = verify_non_seekable_stream_with_recipient_wrap_resolver_options(
13612 std::io::Cursor::new(archive.bytes),
13613 |_| {
13614 called = true;
13615 Ok(vec![master_key().0])
13616 },
13617 NonSeekableReaderOptions::default(),
13618 )
13619 .unwrap_err();
13620
13621 assert!(!called);
13622 assert_eq!(err, FormatError::KeyMaterialMismatch);
13623 }
13624
13625 #[test]
13626 fn live_non_seekable_verify_stream_accepts_tiny_read_chunks() {
13627 let archive = write_archive(
13628 &[RegularFile::new("tiny-chunks.txt", b"one byte at a time")],
13629 &master_key(),
13630 single_stream_options(),
13631 )
13632 .unwrap();
13633
13634 let report =
13635 verify_non_seekable_stream(ChunkedReader::new(archive.bytes, 1), &master_key())
13636 .unwrap();
13637
13638 assert_eq!(report.file_count, 1);
13639 assert_eq!(report.tar_total_size % 512, 0);
13640 }
13641
13642 #[test]
13643 fn live_non_seekable_verify_stream_accepts_empty_archive() {
13644 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
13645
13646 let report =
13647 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
13648
13649 assert_eq!(report.file_count, 0);
13650 assert_eq!(report.payload_block_count, 0);
13651 assert_eq!(report.tar_total_size, 0);
13652 }
13653
13654 #[test]
13655 fn live_non_seekable_verify_rejects_dictionary_archive_without_bootstrap() {
13656 let archive = write_archive_with_dictionary(
13657 &[RegularFile::new(
13658 "live-dict.txt",
13659 b"common words common words dictionary payload",
13660 )],
13661 &master_key(),
13662 single_stream_options(),
13663 b"common words dictionary",
13664 )
13665 .unwrap();
13666
13667 assert_eq!(
13668 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key())
13669 .unwrap_err(),
13670 FormatError::ReaderUnsupported(
13671 "dictionary bootstrap required for non-seekable sequential verification"
13672 )
13673 );
13674 }
13675
13676 #[test]
13677 fn live_non_seekable_verify_accepts_dictionary_archive_with_bootstrap() {
13678 let archive = write_archive_with_dictionary(
13679 &[RegularFile::new(
13680 "live-dict-sidecar.txt",
13681 b"common words common words dictionary payload",
13682 )],
13683 &master_key(),
13684 single_stream_options(),
13685 b"common words dictionary",
13686 )
13687 .unwrap();
13688
13689 let report = verify_non_seekable_stream_with_bootstrap_sidecar(
13690 std::io::Cursor::new(archive.bytes),
13691 &archive.bootstrap_sidecar,
13692 &master_key(),
13693 NonSeekableReaderOptions::default(),
13694 )
13695 .unwrap();
13696
13697 assert_eq!(report.file_count, 1);
13698 assert_eq!(report.total_volumes, 1);
13699 }
13700
13701 #[test]
13702 fn live_non_seekable_verify_rejects_terminal_tail_above_cap() {
13703 let archive = write_archive(
13704 &[RegularFile::new("tail-cap.txt", b"payload")],
13705 &master_key(),
13706 single_stream_options(),
13707 )
13708 .unwrap();
13709 let options = NonSeekableReaderOptions {
13710 max_terminal_tail_size: 8,
13711 ..NonSeekableReaderOptions::default()
13712 };
13713
13714 assert_eq!(
13715 verify_non_seekable_stream_with_options(
13716 std::io::Cursor::new(archive.bytes),
13717 &master_key(),
13718 options
13719 )
13720 .unwrap_err(),
13721 FormatError::ReaderUnsupported("terminal tail exceeds configured cap")
13722 );
13723 }
13724
13725 #[test]
13726 fn live_non_seekable_verify_rejects_metadata_above_retention_cap() {
13727 let archive = write_archive(
13728 &[RegularFile::new("metadata-cap.txt", b"payload")],
13729 &master_key(),
13730 single_stream_options(),
13731 )
13732 .unwrap();
13733 let options = NonSeekableReaderOptions {
13734 max_retained_metadata_bytes: 1,
13735 ..NonSeekableReaderOptions::default()
13736 };
13737
13738 assert_eq!(
13739 verify_non_seekable_stream_with_options(
13740 std::io::Cursor::new(archive.bytes),
13741 &master_key(),
13742 options
13743 )
13744 .unwrap_err(),
13745 FormatError::ReaderUnsupported("retained metadata exceeds configured streaming cap")
13746 );
13747 }
13748
13749 #[test]
13750 fn live_non_seekable_verify_repairs_crc_failed_metadata_block() {
13751 let archive = write_archive(
13752 &[RegularFile::new("metadata-erasure.txt", b"payload")],
13753 &master_key(),
13754 single_stream_options(),
13755 )
13756 .unwrap();
13757 let mut corrupted = archive.bytes;
13758 let slot = first_block_record_slot_with_kind(&corrupted, BlockKind::IndexRootData).unwrap();
13759 corrupt_block_record_payload_at_slot(&mut corrupted, slot);
13760
13761 let report =
13762 verify_non_seekable_stream(std::io::Cursor::new(corrupted), &master_key()).unwrap();
13763
13764 assert_eq!(report.file_count, 1);
13765 }
13766
13767 #[test]
13768 fn live_non_seekable_verify_rejects_member_count_above_cap() {
13769 let archive = write_archive(
13770 &[RegularFile::new("member-cap.txt", b"payload")],
13771 &master_key(),
13772 single_stream_options(),
13773 )
13774 .unwrap();
13775 let options = NonSeekableReaderOptions {
13776 max_streamed_member_count: 0,
13777 ..NonSeekableReaderOptions::default()
13778 };
13779
13780 assert_eq!(
13781 verify_non_seekable_stream_with_options(
13782 std::io::Cursor::new(archive.bytes),
13783 &master_key(),
13784 options
13785 )
13786 .unwrap_err(),
13787 FormatError::ReaderUnsupported("tar member count exceeds configured streaming cap")
13788 );
13789 }
13790
13791 #[test]
13792 fn live_non_seekable_verify_rejects_total_extraction_cap_during_decode() {
13793 let archive = write_archive(
13794 &[RegularFile::new("live-total-cap.txt", b"payload")],
13795 &master_key(),
13796 single_stream_options(),
13797 )
13798 .unwrap();
13799 let mut options = NonSeekableReaderOptions::default();
13800 options.reader.max_total_extraction_size = 3;
13801
13802 assert_eq!(
13803 verify_non_seekable_stream_with_options(
13804 std::io::Cursor::new(archive.bytes),
13805 &master_key(),
13806 options
13807 )
13808 .unwrap_err(),
13809 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
13810 );
13811 }
13812
13813 #[test]
13814 fn live_non_seekable_verify_reports_root_auth_wire_only() {
13815 let archive = write_archive_with_root_auth(
13816 &[RegularFile::new("signed-live.txt", b"root-auth stream")],
13817 &master_key(),
13818 single_stream_options(),
13819 test_root_auth_config(),
13820 |request| Ok(test_root_auth_value(request)),
13821 )
13822 .unwrap();
13823
13824 let report =
13825 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
13826
13827 assert_eq!(report.root_auth, SequentialRootAuthStatus::WireValidOnly);
13828 }
13829
13830 #[test]
13831 fn live_non_seekable_extract_stream_commits_after_terminal_verify() {
13832 let archive = write_archive(
13833 &[
13834 RegularFile::new("alpha.txt", b"alpha"),
13835 RegularFile::new("nested/beta.txt", b"beta"),
13836 ],
13837 &master_key(),
13838 single_stream_options(),
13839 )
13840 .unwrap();
13841 let tmp = tempfile::tempdir().unwrap();
13842 let out = tmp.path().join("out");
13843
13844 let report = extract_non_seekable_stream_to_dir(
13845 std::io::Cursor::new(archive.bytes),
13846 &master_key(),
13847 &out,
13848 NonSeekableReaderOptions::default(),
13849 SafeExtractionOptions::default(),
13850 )
13851 .unwrap();
13852
13853 assert_eq!(report.verification.file_count, 2);
13854 assert_eq!(report.extracted_member_count, 2);
13855 assert_eq!(fs::read(out.join("alpha.txt")).unwrap(), b"alpha");
13856 assert_eq!(fs::read(out.join("nested/beta.txt")).unwrap(), b"beta");
13857 }
13858
13859 #[test]
13860 fn live_non_seekable_extract_stream_accepts_tiny_read_chunks() {
13861 let archive = write_archive(
13862 &[RegularFile::new("tiny-extract.txt", b"chunked extraction")],
13863 &master_key(),
13864 single_stream_options(),
13865 )
13866 .unwrap();
13867 let tmp = tempfile::tempdir().unwrap();
13868 let out = tmp.path().join("out");
13869
13870 extract_non_seekable_stream_to_dir(
13871 ChunkedReader::new(archive.bytes, 1),
13872 &master_key(),
13873 &out,
13874 NonSeekableReaderOptions::default(),
13875 SafeExtractionOptions::default(),
13876 )
13877 .unwrap();
13878
13879 assert_eq!(
13880 fs::read(out.join("tiny-extract.txt")).unwrap(),
13881 b"chunked extraction"
13882 );
13883 }
13884
13885 #[test]
13886 fn live_non_seekable_extract_stream_terminal_failure_leaves_no_final_output() {
13887 let archive = write_archive(
13888 &[RegularFile::new("late-fail.txt", b"must remain staged")],
13889 &master_key(),
13890 single_stream_options(),
13891 )
13892 .unwrap();
13893 let mut corrupted = archive.bytes;
13894 corrupt_v41_terminal_recovery(&mut corrupted);
13895 let tmp = tempfile::tempdir().unwrap();
13896 let out = tmp.path().join("out");
13897
13898 match extract_non_seekable_stream_to_dir(
13899 std::io::Cursor::new(corrupted),
13900 &master_key(),
13901 &out,
13902 NonSeekableReaderOptions::default(),
13903 SafeExtractionOptions::default(),
13904 )
13905 .unwrap_err()
13906 {
13907 ExtractError::Format(err) => assert_eq!(
13908 err,
13909 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
13910 ),
13911 ExtractError::Output(err) => panic!("unexpected output error: {err}"),
13912 }
13913 assert!(!out.exists());
13914 }
13915
13916 #[test]
13917 fn live_non_seekable_extract_stream_existing_destination_obeys_overwrite_policy() {
13918 let archive = write_archive(
13919 &[RegularFile::new("same.txt", b"new")],
13920 &master_key(),
13921 single_stream_options(),
13922 )
13923 .unwrap();
13924 let tmp = tempfile::tempdir().unwrap();
13925 let out = tmp.path().join("out");
13926 fs::create_dir(&out).unwrap();
13927 fs::write(out.join("same.txt"), b"old").unwrap();
13928
13929 match extract_non_seekable_stream_to_dir(
13930 std::io::Cursor::new(archive.bytes.clone()),
13931 &master_key(),
13932 &out,
13933 NonSeekableReaderOptions::default(),
13934 SafeExtractionOptions::default(),
13935 )
13936 .unwrap_err()
13937 {
13938 ExtractError::Format(err) => assert_eq!(err, FormatError::UnsafeOverwrite),
13939 ExtractError::Output(err) => panic!("unexpected output error: {err}"),
13940 }
13941 assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"old");
13942
13943 extract_non_seekable_stream_to_dir(
13944 std::io::Cursor::new(archive.bytes),
13945 &master_key(),
13946 &out,
13947 NonSeekableReaderOptions::default(),
13948 SafeExtractionOptions {
13949 overwrite_existing: true,
13950 },
13951 )
13952 .unwrap();
13953 assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"new");
13954 }
13955
13956 #[test]
13957 fn live_non_seekable_list_stream_matches_seekable_final_view() {
13958 let archive = write_archive(
13959 &[
13960 RegularFile::new("a.txt", b"a"),
13961 RegularFile::new("b.txt", b"bb"),
13962 ],
13963 &master_key(),
13964 single_stream_options(),
13965 )
13966 .unwrap();
13967 let seekable = open_archive(&archive.bytes, &master_key()).unwrap();
13968 let expected = seekable.list_files().unwrap();
13969
13970 let report = list_non_seekable_stream(
13971 std::io::Cursor::new(archive.bytes),
13972 &master_key(),
13973 NonSeekableReaderOptions::default(),
13974 )
13975 .unwrap();
13976
13977 assert_eq!(report.verification.file_count, 2);
13978 assert_eq!(report.entries, expected);
13979 }
13980
13981 #[test]
13982 fn bootstrap_sidecar_rejects_bad_flags_and_trailing_bytes() {
13983 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
13984 let mut bad_flags = archive.bootstrap_sidecar.clone();
13985 rewrite_sidecar_header(&mut bad_flags, &master_key(), |header| {
13986 header.flags |= 0x08;
13987 });
13988 assert_eq!(
13989 open_archive_with_bootstrap_sidecar(&archive.bytes, &bad_flags, &master_key())
13990 .unwrap_err(),
13991 FormatError::UnknownBootstrapSidecarFlags(0x0b)
13992 );
13993
13994 let mut trailing = archive.bootstrap_sidecar.clone();
13995 trailing.push(0);
13996 assert_eq!(
13997 open_archive_with_bootstrap_sidecar(&archive.bytes, &trailing, &master_key())
13998 .unwrap_err(),
13999 FormatError::NonCanonicalBootstrapSidecarLayout
14000 );
14001 }
14002
14003 #[test]
14004 fn bootstrap_sidecar_rejects_bad_manifest_footer_semantics() {
14005 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14006 let mut wrong_volume = archive.bootstrap_sidecar.clone();
14007 mutate_sidecar_manifest(&mut wrong_volume, &master_key(), |footer| {
14008 footer.volume_index = 1;
14009 });
14010 assert_eq!(
14011 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_volume, &master_key())
14012 .unwrap_err(),
14013 FormatError::InvalidArchive("sidecar ManifestFooter volume_index must be zero")
14014 );
14015
14016 let mut non_authoritative = archive.bootstrap_sidecar.clone();
14017 mutate_sidecar_manifest(&mut non_authoritative, &master_key(), |footer| {
14018 footer.is_authoritative = 0;
14019 });
14020 assert_eq!(
14021 open_archive_with_bootstrap_sidecar(&archive.bytes, &non_authoritative, &master_key())
14022 .unwrap_err(),
14023 FormatError::InvalidArchive("sidecar ManifestFooter is not authoritative")
14024 );
14025 }
14026
14027 #[test]
14028 fn sidecar_manifest_validation_does_not_compare_opened_volume_index() {
14029 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14030 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
14031 let crypto_start = volume_header.crypto_header_offset as usize;
14032 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
14033 let crypto_header = CryptoHeader::parse(
14034 &archive.bytes[crypto_start..crypto_end],
14035 volume_header.crypto_header_length,
14036 )
14037 .unwrap();
14038 let subkeys = Subkeys::derive(
14039 &master_key(),
14040 &volume_header.archive_uuid,
14041 &volume_header.session_id,
14042 )
14043 .unwrap();
14044 let mut opened_header = volume_header;
14045 opened_header.volume_index = 1;
14046
14047 let parsed = parse_bootstrap_sidecar(
14048 &archive.bootstrap_sidecar,
14049 &opened_header,
14050 &crypto_header.fixed,
14051 &subkeys,
14052 )
14053 .unwrap();
14054
14055 assert_eq!(parsed.manifest_footer.unwrap().volume_index, 0);
14056 }
14057
14058 #[test]
14059 fn bootstrap_sidecar_rejects_conflicting_manifest_bootstrap_fields() {
14060 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14061 let mut conflicting = archive.bootstrap_sidecar.clone();
14062 mutate_sidecar_manifest(&mut conflicting, &master_key(), |footer| {
14063 footer.index_root_first_block += 1;
14064 });
14065
14066 assert_eq!(
14067 open_archive_with_bootstrap_sidecar(&archive.bytes, &conflicting, &master_key())
14068 .unwrap_err(),
14069 FormatError::InvalidArchive("bootstrap sidecar conflicts with terminal ManifestFooter")
14070 );
14071 }
14072
14073 #[test]
14074 fn sidecar_size_cap_counts_only_present_sparse_sections() {
14075 let mut crypto_header = test_crypto_header();
14076 crypto_header.has_dictionary = 1;
14077 crypto_header.index_root_fec_data_shards = 1;
14078 crypto_header.index_root_fec_parity_shards = 0;
14079 let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
14080 let header = BootstrapSidecarHeader {
14081 archive_uuid: [0x31; 16],
14082 session_id: [0x42; 16],
14083 flags: 0x04,
14084 manifest_footer_offset: 0,
14085 manifest_footer_length: 0,
14086 index_root_records_offset: 0,
14087 index_root_records_length: 0,
14088 dictionary_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
14089 dictionary_records_length: record_len,
14090 sidecar_hmac: [0u8; 32],
14091 header_crc32c: 0,
14092 };
14093
14094 validate_sidecar_size_cap(
14095 &header,
14096 &crypto_header,
14097 BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len,
14098 )
14099 .unwrap();
14100 assert_eq!(
14101 validate_sidecar_size_cap(
14102 &header,
14103 &crypto_header,
14104 BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len + 1,
14105 )
14106 .unwrap_err(),
14107 FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
14108 );
14109 }
14110
14111 #[test]
14112 fn sidecar_size_cap_rejects_sparse_section_above_class_max() {
14113 let mut crypto_header = test_crypto_header();
14114 crypto_header.index_root_fec_data_shards = 1;
14115 crypto_header.index_root_fec_parity_shards = 0;
14116 let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
14117 let header = BootstrapSidecarHeader {
14118 archive_uuid: [0x31; 16],
14119 session_id: [0x42; 16],
14120 flags: 0x02,
14121 manifest_footer_offset: 0,
14122 manifest_footer_length: 0,
14123 index_root_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
14124 index_root_records_length: record_len * 2,
14125 dictionary_records_offset: 0,
14126 dictionary_records_length: 0,
14127 sidecar_hmac: [0u8; 32],
14128 header_crc32c: 0,
14129 };
14130
14131 assert_eq!(
14132 validate_sidecar_size_cap(
14133 &header,
14134 &crypto_header,
14135 BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len * 2,
14136 )
14137 .unwrap_err(),
14138 FormatError::InvalidArchive("bootstrap sidecar IndexRoot records exceed resource cap")
14139 );
14140 }
14141
14142 #[test]
14143 fn sidecar_size_cap_uses_wide_arithmetic_for_large_record_classes() {
14144 let mut crypto_header = test_crypto_header();
14145 crypto_header.block_size = u32::MAX;
14146 crypto_header.index_root_fec_data_shards = u16::MAX;
14147 crypto_header.index_root_fec_parity_shards = u16::MAX;
14148 let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
14149 let max_records = crypto_header.index_root_fec_data_shards as u64
14150 + crypto_header.index_root_fec_parity_shards as u64;
14151 let max_section_len = max_records * record_len;
14152 let cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64
14153 + MANIFEST_FOOTER_LEN as u64
14154 + max_section_len
14155 + max_section_len;
14156 let header = BootstrapSidecarHeader {
14157 archive_uuid: [0x31; 16],
14158 session_id: [0x42; 16],
14159 flags: 0x01 | 0x02 | 0x04,
14160 manifest_footer_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
14161 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
14162 index_root_records_offset: 0,
14163 index_root_records_length: max_section_len,
14164 dictionary_records_offset: 0,
14165 dictionary_records_length: max_section_len,
14166 sidecar_hmac: [0u8; 32],
14167 header_crc32c: 0,
14168 };
14169
14170 validate_sidecar_size_cap(&header, &crypto_header, cap).unwrap();
14171 assert_eq!(
14172 validate_sidecar_size_cap(&header, &crypto_header, cap + 1).unwrap_err(),
14173 FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
14174 );
14175 }
14176
14177 #[test]
14178 fn bootstrap_sidecar_rejects_dictionary_section_for_no_dictionary_archive() {
14179 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14180 let mut with_dictionary = archive.bootstrap_sidecar.clone();
14181 let header =
14182 BootstrapSidecarHeader::parse(&with_dictionary[..BOOTSTRAP_SIDECAR_HEADER_LEN])
14183 .unwrap();
14184 let record_len = sidecar_record_len(&with_dictionary);
14185 let first_record = header.index_root_records_offset as usize;
14186 let copied_record = with_dictionary[first_record..first_record + record_len].to_vec();
14187 let dictionary_offset = with_dictionary.len() as u64;
14188 with_dictionary.extend_from_slice(&copied_record);
14189 rewrite_sidecar_header(&mut with_dictionary, &master_key(), |header| {
14190 header.flags |= 0x04;
14191 header.dictionary_records_offset = dictionary_offset;
14192 header.dictionary_records_length = record_len as u64;
14193 });
14194
14195 assert_eq!(
14196 open_archive_with_bootstrap_sidecar(&archive.bytes, &with_dictionary, &master_key())
14197 .unwrap_err(),
14198 FormatError::InvalidArchive(
14199 "bootstrap sidecar has dictionary records while has_dictionary is false"
14200 )
14201 );
14202 }
14203
14204 #[test]
14205 fn bootstrap_sidecar_rejects_missing_duplicate_wrong_kind_and_wrong_last_flag() {
14206 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14207 let mut missing = archive.bootstrap_sidecar.clone();
14208 let record_len = sidecar_record_len(&missing);
14209 let new_len = missing.len() - record_len;
14210 missing.truncate(new_len);
14211 rewrite_sidecar_header(&mut missing, &master_key(), |header| {
14212 header.index_root_records_length -= record_len as u64;
14213 });
14214 assert_eq!(
14215 open_archive_with_bootstrap_sidecar(&archive.bytes, &missing, &master_key())
14216 .unwrap_err(),
14217 FormatError::InvalidArchive(
14218 "sidecar BlockRecord section does not match declared extent"
14219 )
14220 );
14221
14222 let mut duplicate = archive.bootstrap_sidecar.clone();
14223 mutate_sidecar_index_record(&mut duplicate, 1, |record| {
14224 record.block_index -= 1;
14225 });
14226 assert_eq!(
14227 open_archive_with_bootstrap_sidecar(&archive.bytes, &duplicate, &master_key())
14228 .unwrap_err(),
14229 FormatError::InvalidArchive(
14230 "sidecar BlockRecord section has missing or duplicate blocks"
14231 )
14232 );
14233
14234 let mut misordered = archive.bootstrap_sidecar.clone();
14235 swap_sidecar_index_records(&mut misordered, 0, 1);
14236 assert_eq!(
14237 open_archive_with_bootstrap_sidecar(&archive.bytes, &misordered, &master_key())
14238 .unwrap_err(),
14239 FormatError::InvalidArchive(
14240 "sidecar BlockRecord section has missing or duplicate blocks"
14241 )
14242 );
14243
14244 let mut wrong_kind = archive.bootstrap_sidecar.clone();
14245 mutate_sidecar_index_record(&mut wrong_kind, 0, |record| {
14246 record.kind = BlockKind::PayloadData;
14247 });
14248 assert_eq!(
14249 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
14250 .unwrap_err(),
14251 FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
14252 );
14253
14254 let mut wrong_last = archive.bootstrap_sidecar.clone();
14255 mutate_sidecar_index_record(&mut wrong_last, 0, |record| {
14256 record.flags = 0;
14257 });
14258 assert_eq!(
14259 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
14260 .unwrap_err(),
14261 FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
14262 );
14263 }
14264
14265 #[test]
14266 fn verify_helper_rejects_envelope_frame_coverage_gap() {
14267 let frames = BTreeMap::from([(
14268 0,
14269 FrameEntry {
14270 frame_index: 0,
14271 envelope_index: 0,
14272 offset_in_envelope: 0,
14273 compressed_size: 10,
14274 decompressed_size: 512,
14275 flags: 0,
14276 tar_stream_offset: 0,
14277 },
14278 )]);
14279 let envelopes = BTreeMap::from([(
14280 0,
14281 EnvelopeEntry {
14282 envelope_index: 0,
14283 first_block_index: 0,
14284 data_block_count: 1,
14285 parity_block_count: 1,
14286 encrypted_size: 4096,
14287 plaintext_size: 11,
14288 first_frame_index: 0,
14289 frame_count: 1,
14290 },
14291 )]);
14292
14293 assert_eq!(
14294 validate_envelope_frame_coverage(&frames, &envelopes).unwrap_err(),
14295 FormatError::InvalidArchive("EnvelopeEntry frame coverage has a gap or overlap")
14296 );
14297 }
14298
14299 #[test]
14300 fn verify_helper_rejects_file_extent_gaps_and_overlaps() {
14301 assert!(validate_file_extent_coverage_ranges(&[(512, 512), (0, 512)], 1024).is_ok());
14302 assert_eq!(
14303 validate_file_extent_coverage_ranges(&[(0, 512), (1024, 512)], 1536).unwrap_err(),
14304 FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
14305 );
14306 assert_eq!(
14307 validate_file_extent_coverage_ranges(&[(0, 1024), (512, 512)], 1024).unwrap_err(),
14308 FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
14309 );
14310 }
14311
14312 #[test]
14313 fn verify_rejects_authenticated_content_hash_mismatch() {
14314 let options = WriterOptions {
14315 index_root_fec_parity_shards: 0,
14316 ..single_stream_options()
14317 };
14318 let archive = write_archive(
14319 &[RegularFile::new("content-hash.txt", b"hash covered")],
14320 &master_key(),
14321 options,
14322 )
14323 .unwrap();
14324 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14325
14326 let mut root = opened.index_root.clone();
14327 root.header.content_sha256 = [0xa5; 32];
14328 let root_plaintext = root.to_bytes();
14329 IndexRoot::parse(
14330 &root_plaintext,
14331 false,
14332 metadata_limits(&opened.crypto_header),
14333 )
14334 .unwrap();
14335 assert_eq!(
14336 root_plaintext.len() as u32,
14337 opened.manifest_footer.index_root_decompressed_size
14338 );
14339
14340 let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
14341 let mut next_block_index = opened.manifest_footer.index_root_first_block;
14342 let replacement = encrypt_test_object(
14343 &compressed_root,
14344 &opened.subkeys.index_root_key,
14345 &opened.subkeys.index_nonce_seed,
14346 b"idxroot",
14347 0,
14348 BlockKind::IndexRootData,
14349 &mut next_block_index,
14350 &opened.crypto_header,
14351 &opened.volume_header,
14352 );
14353 assert_eq!(
14354 replacement.extent.data_block_count,
14355 opened.manifest_footer.index_root_data_block_count
14356 );
14357 assert_eq!(
14358 replacement.extent.encrypted_size,
14359 opened.manifest_footer.index_root_encrypted_size
14360 );
14361
14362 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
14363 let crypto_end = volume_header.crypto_header_offset as usize
14364 + volume_header.crypto_header_length as usize;
14365 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
14366 let mut malformed = archive.bytes.clone();
14367 for record in replacement.records {
14368 let offset = crypto_end + record.block_index as usize * record_len;
14369 malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
14370 }
14371
14372 let reopened = open_archive(&malformed, &master_key()).unwrap();
14373 assert_eq!(
14374 reopened.verify().unwrap_err(),
14375 FormatError::InvalidArchive(
14376 "IndexRoot content_sha256 does not match decoded tar stream"
14377 )
14378 );
14379 }
14380
14381 #[test]
14382 fn verify_rejects_file_entry_tar_path_and_size_mismatches() {
14383 let (mut path_mismatch, _) = multi_envelope_reader_fixture();
14384 rewrite_as_single_healthy_file(&mut path_mismatch, |_file, path| {
14385 path[0] = b'x';
14386 });
14387 assert_eq!(
14388 path_mismatch.verify().unwrap_err(),
14389 FormatError::InvalidArchive("tar member path does not match FileEntry path")
14390 );
14391
14392 let (mut size_mismatch, _) = multi_envelope_reader_fixture();
14393 rewrite_as_single_healthy_file(&mut size_mismatch, |file, _path| {
14394 file.file_data_size += 1;
14395 });
14396 assert_eq!(
14397 size_mismatch.verify().unwrap_err(),
14398 FormatError::InvalidArchive("tar member size does not match FileEntry file_data_size")
14399 );
14400 }
14401
14402 #[test]
14403 fn verify_rejects_inconsistent_duplicate_local_frame_rows_across_shards() {
14404 let (mut opened, _) = multi_envelope_reader_fixture();
14405 let locating = opened.index_root.shards[0].clone();
14406 let mut duplicate = opened.load_index_shard(&locating).unwrap();
14407 duplicate.header.shard_index = 1;
14408 duplicate.frames[0].flags ^= 0x0000_0001;
14409 let duplicate_plaintext = duplicate.to_bytes();
14410 let mut next_block_index = opened
14411 .blocks
14412 .keys()
14413 .last()
14414 .copied()
14415 .map(|index| index + 1)
14416 .unwrap_or(0);
14417 let duplicate_object = encrypt_test_object(
14418 &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
14419 &opened.subkeys.index_shard_key,
14420 &opened.subkeys.index_nonce_seed,
14421 b"idxshard",
14422 1,
14423 BlockKind::IndexShardData,
14424 &mut next_block_index,
14425 &opened.crypto_header,
14426 &opened.volume_header,
14427 );
14428 insert_records(&mut opened.blocks, &duplicate_object.records);
14429 opened.index_root.shards.push(ShardEntry {
14430 shard_index: 1,
14431 first_block_index: duplicate_object.extent.first_block_index,
14432 data_block_count: duplicate_object.extent.data_block_count,
14433 parity_block_count: 0,
14434 encrypted_size: duplicate_object.extent.encrypted_size,
14435 decompressed_size: duplicate_plaintext.len() as u32,
14436 file_count: locating.file_count,
14437 first_path_hash: locating.first_path_hash,
14438 last_path_hash: locating.last_path_hash,
14439 });
14440 opened.index_root.header.file_count += locating.file_count as u64;
14441
14442 assert_eq!(
14443 opened.verify().unwrap_err(),
14444 FormatError::InvalidArchive("duplicate FrameEntry rows do not match")
14445 );
14446 }
14447
14448 #[test]
14449 fn verify_rejects_inconsistent_duplicate_local_envelope_rows_across_shards() {
14450 let (mut opened, _) = multi_envelope_reader_fixture();
14451 let locating = opened.index_root.shards[0].clone();
14452 let mut duplicate = opened.load_index_shard(&locating).unwrap();
14453 duplicate.header.shard_index = 1;
14454 duplicate.envelopes[0].first_block_index += 1;
14455 let duplicate_plaintext = duplicate.to_bytes();
14456 let mut next_block_index = opened
14457 .blocks
14458 .keys()
14459 .last()
14460 .copied()
14461 .map(|index| index + 1)
14462 .unwrap_or(0);
14463 let duplicate_object = encrypt_test_object(
14464 &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
14465 &opened.subkeys.index_shard_key,
14466 &opened.subkeys.index_nonce_seed,
14467 b"idxshard",
14468 1,
14469 BlockKind::IndexShardData,
14470 &mut next_block_index,
14471 &opened.crypto_header,
14472 &opened.volume_header,
14473 );
14474 insert_records(&mut opened.blocks, &duplicate_object.records);
14475 opened.index_root.shards.push(ShardEntry {
14476 shard_index: 1,
14477 first_block_index: duplicate_object.extent.first_block_index,
14478 data_block_count: duplicate_object.extent.data_block_count,
14479 parity_block_count: 0,
14480 encrypted_size: duplicate_object.extent.encrypted_size,
14481 decompressed_size: duplicate_plaintext.len() as u32,
14482 file_count: locating.file_count,
14483 first_path_hash: locating.first_path_hash,
14484 last_path_hash: locating.last_path_hash,
14485 });
14486 opened.index_root.header.file_count += locating.file_count as u64;
14487
14488 assert_eq!(
14489 opened.verify().unwrap_err(),
14490 FormatError::InvalidArchive("duplicate EnvelopeEntry rows do not match")
14491 );
14492 }
14493
14494 #[test]
14495 fn verify_rejects_non_contiguous_global_envelope_indexes() {
14496 let (mut opened, _) = multi_envelope_reader_fixture();
14497 replace_first_index_shard(&mut opened, |shard| {
14498 let frame = shard
14499 .frames
14500 .iter_mut()
14501 .find(|entry| entry.frame_index == 1)
14502 .unwrap();
14503 frame.envelope_index = 2;
14504
14505 let envelope = shard
14506 .envelopes
14507 .iter_mut()
14508 .find(|entry| entry.envelope_index == 1)
14509 .unwrap();
14510 envelope.envelope_index = 2;
14511 });
14512
14513 assert_eq!(
14514 opened.verify().unwrap_err(),
14515 FormatError::InvalidMetadata {
14516 structure: "EnvelopeEntry",
14517 reason: "global index coverage has a gap",
14518 }
14519 );
14520 }
14521
14522 #[test]
14523 fn verify_rejects_payload_object_extent_overlap() {
14524 let (mut opened, _) = multi_envelope_reader_fixture();
14525 replace_first_index_shard(&mut opened, |shard| {
14526 let first_block_index = shard.envelopes[0].first_block_index;
14527 shard.envelopes[1].first_block_index = first_block_index;
14528 });
14529
14530 assert_eq!(
14531 opened.verify().unwrap_err(),
14532 FormatError::InvalidArchive("encrypted object block ranges overlap")
14533 );
14534 }
14535
14536 #[test]
14537 fn verify_accepts_cross_shard_shared_envelope_frame_union() {
14538 let volume_header = test_volume_header();
14539 let crypto_header = test_crypto_header();
14540 let subkeys = Subkeys::derive(
14541 &master_key(),
14542 &volume_header.archive_uuid,
14543 &volume_header.session_id,
14544 )
14545 .unwrap();
14546 let mut next_block_index = 0u64;
14547 let mut blocks = BTreeMap::new();
14548
14549 let alpha = test_member(b"alpha.txt", b"alpha cross shard\n");
14550 let beta = test_member(b"beta.txt", b"beta cross shard\n");
14551 let tar_stream = [alpha.as_slice(), beta.as_slice()].concat();
14552 let frame0_plaintext = compress_zstd_frame(&alpha, 1).unwrap();
14553 let frame1_plaintext = compress_zstd_frame(&beta, 1).unwrap();
14554 let envelope_plaintext =
14555 [frame0_plaintext.as_slice(), frame1_plaintext.as_slice()].concat();
14556 let payload = encrypt_test_object(
14557 &envelope_plaintext,
14558 &subkeys.enc_key,
14559 &subkeys.nonce_seed,
14560 b"envelope",
14561 0,
14562 BlockKind::PayloadData,
14563 &mut next_block_index,
14564 &crypto_header,
14565 &volume_header,
14566 );
14567 insert_records(&mut blocks, &payload.records);
14568
14569 let envelope = EnvelopeEntry {
14570 envelope_index: 0,
14571 first_block_index: payload.extent.first_block_index,
14572 data_block_count: payload.extent.data_block_count,
14573 parity_block_count: 0,
14574 encrypted_size: payload.extent.encrypted_size,
14575 plaintext_size: envelope_plaintext.len() as u32,
14576 first_frame_index: 0,
14577 frame_count: 2,
14578 };
14579 let frame0 = FrameEntry {
14580 frame_index: 0,
14581 envelope_index: 0,
14582 offset_in_envelope: 0,
14583 compressed_size: frame0_plaintext.len() as u32,
14584 decompressed_size: alpha.len() as u32,
14585 flags: 0x0000_0003,
14586 tar_stream_offset: 0,
14587 };
14588 let frame1 = FrameEntry {
14589 frame_index: 1,
14590 envelope_index: 0,
14591 offset_in_envelope: frame0_plaintext.len() as u32,
14592 compressed_size: frame1_plaintext.len() as u32,
14593 decompressed_size: beta.len() as u32,
14594 flags: 0x0000_0003,
14595 tar_stream_offset: alpha.len() as u64,
14596 };
14597
14598 let (shard0_plaintext, first0, last0) = build_test_index_shard(
14599 &[TestFileMeta {
14600 path: b"alpha.txt".to_vec(),
14601 frame_index: 0,
14602 tar_stream_offset: 0,
14603 member_group_size: alpha.len() as u64,
14604 file_data_size: b"alpha cross shard\n".len() as u64,
14605 }],
14606 &[frame0],
14607 std::slice::from_ref(&envelope),
14608 );
14609 let (mut shard1_plaintext, first1, last1) = build_test_index_shard(
14610 &[TestFileMeta {
14611 path: b"beta.txt".to_vec(),
14612 frame_index: 1,
14613 tar_stream_offset: alpha.len() as u64,
14614 member_group_size: beta.len() as u64,
14615 file_data_size: b"beta cross shard\n".len() as u64,
14616 }],
14617 &[frame1],
14618 std::slice::from_ref(&envelope),
14619 );
14620 shard1_plaintext[8..16].copy_from_slice(&1u64.to_le_bytes());
14621
14622 let shard0 = encrypt_test_object(
14623 &compress_zstd_frame(&shard0_plaintext, 1).unwrap(),
14624 &subkeys.index_shard_key,
14625 &subkeys.index_nonce_seed,
14626 b"idxshard",
14627 0,
14628 BlockKind::IndexShardData,
14629 &mut next_block_index,
14630 &crypto_header,
14631 &volume_header,
14632 );
14633 let shard1 = encrypt_test_object(
14634 &compress_zstd_frame(&shard1_plaintext, 1).unwrap(),
14635 &subkeys.index_shard_key,
14636 &subkeys.index_nonce_seed,
14637 b"idxshard",
14638 1,
14639 BlockKind::IndexShardData,
14640 &mut next_block_index,
14641 &crypto_header,
14642 &volume_header,
14643 );
14644 insert_records(&mut blocks, &shard0.records);
14645 insert_records(&mut blocks, &shard1.records);
14646
14647 let index_root = IndexRoot {
14648 header: IndexRootHeader {
14649 frame_count: 2,
14650 envelope_count: 1,
14651 file_count: 2,
14652 payload_block_count: payload.extent.data_block_count as u64,
14653 tar_total_size: tar_stream.len() as u64,
14654 content_sha256: sha256_bytes(&tar_stream),
14655 ..IndexRootHeader::empty()
14656 },
14657 shards: vec![
14658 ShardEntry {
14659 shard_index: 0,
14660 first_block_index: shard0.extent.first_block_index,
14661 data_block_count: shard0.extent.data_block_count,
14662 parity_block_count: 0,
14663 encrypted_size: shard0.extent.encrypted_size,
14664 decompressed_size: shard0_plaintext.len() as u32,
14665 file_count: 1,
14666 first_path_hash: first0,
14667 last_path_hash: last0,
14668 },
14669 ShardEntry {
14670 shard_index: 1,
14671 first_block_index: shard1.extent.first_block_index,
14672 data_block_count: shard1.extent.data_block_count,
14673 parity_block_count: 0,
14674 encrypted_size: shard1.extent.encrypted_size,
14675 decompressed_size: shard1_plaintext.len() as u32,
14676 file_count: 1,
14677 first_path_hash: first1,
14678 last_path_hash: last1,
14679 },
14680 ],
14681 directory_hint_shards: Vec::new(),
14682 };
14683
14684 let index_root_plaintext = index_root.to_bytes();
14685 let index_root_object = encrypt_test_object(
14686 &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
14687 &subkeys.index_root_key,
14688 &subkeys.index_nonce_seed,
14689 b"idxroot",
14690 0,
14691 BlockKind::IndexRootData,
14692 &mut next_block_index,
14693 &crypto_header,
14694 &volume_header,
14695 );
14696 insert_records(&mut blocks, &index_root_object.records);
14697
14698 let archive_uuid = volume_header.archive_uuid;
14699 let session_id = volume_header.session_id;
14700 let opened = OpenedArchive {
14701 options: ReaderOptions::default(),
14702 observed_archive_bytes: 1_000_000,
14703 observed_volume_count: 1,
14704 subkeys,
14705 blocks,
14706 lazy_blocks: None,
14707 crypto_header_bytes: Vec::new(),
14708 volume_header,
14709 crypto_header,
14710 manifest_footer: ManifestFooter {
14711 archive_uuid,
14712 session_id,
14713 volume_index: 0,
14714 is_authoritative: 1,
14715 total_volumes: 1,
14716 index_root_first_block: index_root_object.extent.first_block_index,
14717 index_root_data_block_count: index_root_object.extent.data_block_count,
14718 index_root_parity_block_count: 0,
14719 index_root_encrypted_size: index_root_object.extent.encrypted_size,
14720 index_root_decompressed_size: index_root_plaintext.len() as u32,
14721 manifest_hmac: [0u8; 32],
14722 },
14723 volume_trailer: Some(VolumeTrailer {
14724 archive_uuid,
14725 session_id,
14726 volume_index: 0,
14727 block_count: next_block_index,
14728 bytes_written: 0,
14729 manifest_footer_offset: 0,
14730 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
14731 closed_at_ns: 0,
14732 root_auth_footer_offset: 0,
14733 root_auth_footer_length: 0,
14734 root_auth_flags: 0,
14735 trailer_hmac: [0u8; 32],
14736 }),
14737 root_auth_footer: None,
14738 index_root,
14739 payload_dictionary: None,
14740 };
14741
14742 opened.verify().unwrap();
14743 }
14744
14745 #[test]
14746 fn verify_rejects_authenticated_archive_missing_required_directory_hints() {
14747 let options = WriterOptions {
14748 index_root_fec_parity_shards: 0,
14749 ..single_stream_options()
14750 };
14751 let archive = write_archive(
14752 &[RegularFile::new("only.txt", b"only payload")],
14753 &master_key(),
14754 options,
14755 )
14756 .unwrap();
14757 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14758 assert!(opened.index_root.directory_hint_shards.is_empty());
14759
14760 let mut root = opened.index_root.clone();
14761 root.header.file_count = DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1;
14762 root.shards[0].file_count = (DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1) as u32;
14763 let root_plaintext = root.to_bytes();
14764 IndexRoot::parse(
14765 &root_plaintext,
14766 false,
14767 metadata_limits(&opened.crypto_header),
14768 )
14769 .unwrap();
14770 assert_eq!(
14771 root_plaintext.len() as u32,
14772 opened.manifest_footer.index_root_decompressed_size
14773 );
14774
14775 let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
14776 let mut next_block_index = opened.manifest_footer.index_root_first_block;
14777 let replacement = encrypt_test_object(
14778 &compressed_root,
14779 &opened.subkeys.index_root_key,
14780 &opened.subkeys.index_nonce_seed,
14781 b"idxroot",
14782 0,
14783 BlockKind::IndexRootData,
14784 &mut next_block_index,
14785 &opened.crypto_header,
14786 &opened.volume_header,
14787 );
14788 assert_eq!(
14789 replacement.extent.first_block_index,
14790 opened.manifest_footer.index_root_first_block
14791 );
14792 assert_eq!(
14793 replacement.extent.data_block_count,
14794 opened.manifest_footer.index_root_data_block_count
14795 );
14796 assert_eq!(
14797 replacement.extent.encrypted_size,
14798 opened.manifest_footer.index_root_encrypted_size
14799 );
14800
14801 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
14802 let crypto_end = volume_header.crypto_header_offset as usize
14803 + volume_header.crypto_header_length as usize;
14804 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
14805 let mut malformed = archive.bytes.clone();
14806 for record in replacement.records {
14807 let offset = crypto_end + record.block_index as usize * record_len;
14808 malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
14809 }
14810
14811 let reopened = open_archive(&malformed, &master_key()).unwrap();
14812 assert_eq!(
14813 reopened.index_root.header.file_count,
14814 DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1
14815 );
14816 assert!(reopened.index_root.directory_hint_shards.is_empty());
14817
14818 assert_eq!(
14819 reopened.verify().unwrap_err(),
14820 FormatError::InvalidArchive("IndexRoot file_count requires directory hints")
14821 );
14822 }
14823
14824 #[test]
14825 fn expected_directory_hint_rows_include_ancestors_and_directory_entries() {
14826 let mut map = DirectoryHintMap::new();
14827 add_expected_directory_hint_rows(&mut map, 2, b"foo/bar/baz.txt", TarEntryKind::Regular);
14828 add_expected_directory_hint_rows(&mut map, 4, b"foo/bar", TarEntryKind::Directory);
14829
14830 assert_eq!(map.get(&Vec::new()), Some(&BTreeSet::from([2, 4])));
14831 assert_eq!(map.get(b"foo".as_slice()), Some(&BTreeSet::from([2, 4])));
14832 assert_eq!(
14833 map.get(b"foo/bar".as_slice()),
14834 Some(&BTreeSet::from([2, 4]))
14835 );
14836 assert!(!map.contains_key(b"foo/bar/baz.txt".as_slice()));
14837 assert!(!map.contains_key(b"foobar".as_slice()));
14838 }
14839
14840 #[test]
14841 fn directory_hint_validation_requires_exact_global_map() {
14842 let mut expected = DirectoryHintMap::new();
14843 add_expected_directory_hint_rows(&mut expected, 0, b"foo/bar.txt", TarEntryKind::Regular);
14844 add_expected_directory_hint_rows(&mut expected, 1, b"foo", TarEntryKind::Directory);
14845 let rows = sorted_directory_hint_rows(&expected);
14846 let table = directory_hint_table_from_rows(7, &rows, 2);
14847
14848 validate_directory_hint_tables_against_expected(std::slice::from_ref(&table), &expected)
14849 .unwrap();
14850
14851 let mut missing_root = expected.clone();
14852 missing_root.remove(&Vec::new());
14853 let missing_root_rows = sorted_directory_hint_rows(&missing_root);
14854 let missing_root_table = directory_hint_table_from_rows(8, &missing_root_rows, 2);
14855 assert_eq!(
14856 validate_directory_hint_tables_against_expected(&[missing_root_table], &expected)
14857 .unwrap_err(),
14858 FormatError::InvalidArchive("directory hint map does not match decoded files")
14859 );
14860
14861 let mut expected_missing_directory_entry = expected.clone();
14862 expected_missing_directory_entry
14863 .get_mut(b"foo".as_slice())
14864 .unwrap()
14865 .remove(&1);
14866 assert_eq!(
14867 validate_directory_hint_tables_against_expected(
14868 std::slice::from_ref(&table),
14869 &expected_missing_directory_entry,
14870 )
14871 .unwrap_err(),
14872 FormatError::InvalidArchive("directory hint map does not match decoded files")
14873 );
14874
14875 let mut extra = expected.clone();
14876 extra.insert(b"foo/extra".to_vec(), BTreeSet::from([0]));
14877 let extra_rows = sorted_directory_hint_rows(&extra);
14878 let extra_table = directory_hint_table_from_rows(9, &extra_rows, 2);
14879 assert_eq!(
14880 validate_directory_hint_tables_against_expected(&[extra_table], &expected).unwrap_err(),
14881 FormatError::InvalidArchive("directory hint map does not match decoded files")
14882 );
14883 }
14884
14885 #[test]
14886 fn directory_hint_validation_rejects_global_order_mismatch() {
14887 let mut expected = DirectoryHintMap::new();
14888 expected.insert(Vec::new(), BTreeSet::from([0]));
14889 expected.insert(b"alpha".to_vec(), BTreeSet::from([0]));
14890 let rows = sorted_directory_hint_rows(&expected);
14891 let first = directory_hint_table_from_rows(8, &rows[..1], 1);
14892 let second = directory_hint_table_from_rows(9, &rows[1..], 1);
14893
14894 assert_eq!(
14895 validate_directory_hint_tables_against_expected(&[second, first], &expected)
14896 .unwrap_err(),
14897 FormatError::InvalidArchive("DirectoryHintEntry rows are not globally sorted")
14898 );
14899 }
14900
14901 #[test]
14902 fn object_extent_rejects_parity_above_class_cap() {
14903 let crypto_header = CryptoHeaderFixed {
14904 length: 0,
14905 compression_algo: CompressionAlgo::ZstdFramed,
14906 aead_algo: AeadAlgo::AesGcmSiv256,
14907 fec_algo: FecAlgo::ReedSolomonGF16,
14908 kdf_algo: KdfAlgo::Raw,
14909 chunk_size: 1024,
14910 envelope_target_size: 4096,
14911 block_size: 4096,
14912 fec_data_shards: 1,
14913 fec_parity_shards: 1,
14914 index_fec_data_shards: 1,
14915 index_fec_parity_shards: 1,
14916 index_root_fec_data_shards: 1,
14917 index_root_fec_parity_shards: 1,
14918 stripe_width: 1,
14919 volume_loss_tolerance: 0,
14920 bit_rot_buffer_pct: 0,
14921 has_dictionary: 0,
14922 max_path_length: 4096,
14923 expected_volume_size: 0,
14924 };
14925 let extent = ObjectExtent {
14926 first_block_index: 0,
14927 data_block_count: 1,
14928 parity_block_count: 2,
14929 encrypted_size: 4096,
14930 };
14931
14932 assert_eq!(
14933 validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
14934 FormatError::InvalidArchive("encrypted object exceeds its class parity-shard maximum")
14935 );
14936 }
14937
14938 #[test]
14939 fn object_extent_rejects_parity_below_recoverability_requirement() {
14940 let crypto_header = CryptoHeaderFixed {
14941 length: 0,
14942 compression_algo: CompressionAlgo::ZstdFramed,
14943 aead_algo: AeadAlgo::AesGcmSiv256,
14944 fec_algo: FecAlgo::ReedSolomonGF16,
14945 kdf_algo: KdfAlgo::Raw,
14946 chunk_size: 1024,
14947 envelope_target_size: 4096,
14948 block_size: 4096,
14949 fec_data_shards: 1,
14950 fec_parity_shards: 1,
14951 index_fec_data_shards: 1,
14952 index_fec_parity_shards: 1,
14953 index_root_fec_data_shards: 1,
14954 index_root_fec_parity_shards: 1,
14955 stripe_width: 2,
14956 volume_loss_tolerance: 1,
14957 bit_rot_buffer_pct: 0,
14958 has_dictionary: 0,
14959 max_path_length: 4096,
14960 expected_volume_size: 0,
14961 };
14962 let extent = ObjectExtent {
14963 first_block_index: 0,
14964 data_block_count: 1,
14965 parity_block_count: 0,
14966 encrypted_size: 4096,
14967 };
14968
14969 assert_eq!(
14970 validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
14971 FormatError::InvalidArchive(
14972 "encrypted object parity does not match v41 compute_parity"
14973 )
14974 );
14975 }
14976
14977 #[test]
14978 fn encrypted_object_extent_matrix_rejects_overlaps() {
14979 let (opened, _) = multi_envelope_reader_fixture();
14980 let loaded_shard = opened
14981 .load_index_shard(&opened.index_root.shards[0])
14982 .unwrap();
14983 let base_envelopes = loaded_shard
14984 .envelopes
14985 .iter()
14986 .map(|entry| (entry.envelope_index, entry.clone()))
14987 .collect::<BTreeMap<_, _>>();
14988 let payload_start = loaded_shard.envelopes[0].first_block_index;
14989 let overlap = FormatError::InvalidArchive("encrypted object block ranges overlap");
14990
14991 let mut payload_overlap = base_envelopes.clone();
14992 payload_overlap
14993 .get_mut(&loaded_shard.envelopes[1].envelope_index)
14994 .unwrap()
14995 .first_block_index = payload_start;
14996 assert_eq!(
14997 opened
14998 .validate_encrypted_object_block_ranges(&payload_overlap)
14999 .unwrap_err(),
15000 overlap
15001 );
15002
15003 let mut shard_overlap = opened.clone();
15004 let shard = shard_overlap.index_root.shards[0].clone();
15005 shard_overlap.index_root.shards.push(ShardEntry {
15006 shard_index: 1,
15007 ..shard
15008 });
15009 assert_eq!(
15010 shard_overlap
15011 .validate_encrypted_object_block_ranges(&base_envelopes)
15012 .unwrap_err(),
15013 overlap
15014 );
15015
15016 let mut dictionary_overlap = opened.clone();
15017 dictionary_overlap.crypto_header.has_dictionary = 1;
15018 dictionary_overlap.index_root.header.dictionary_first_block = payload_start;
15019 dictionary_overlap
15020 .index_root
15021 .header
15022 .dictionary_data_block_count = 1;
15023 dictionary_overlap
15024 .index_root
15025 .header
15026 .dictionary_parity_block_count = 0;
15027 dictionary_overlap
15028 .index_root
15029 .header
15030 .dictionary_encrypted_size = 4096;
15031 dictionary_overlap
15032 .index_root
15033 .header
15034 .dictionary_decompressed_size = 128;
15035 assert_eq!(
15036 dictionary_overlap
15037 .validate_encrypted_object_block_ranges(&base_envelopes)
15038 .unwrap_err(),
15039 overlap
15040 );
15041
15042 let mut hint_overlap = opened.clone();
15043 hint_overlap
15044 .index_root
15045 .directory_hint_shards
15046 .push(DirectoryHintShardEntry {
15047 hint_shard_index: 0,
15048 first_dir_hash: [0; 8],
15049 last_dir_hash: [0; 8],
15050 first_block_index: payload_start,
15051 data_block_count: 1,
15052 parity_block_count: 0,
15053 encrypted_size: 4096,
15054 decompressed_size: 128,
15055 entry_count: 1,
15056 });
15057 assert_eq!(
15058 hint_overlap
15059 .validate_encrypted_object_block_ranges(&base_envelopes)
15060 .unwrap_err(),
15061 overlap
15062 );
15063 }
15064
15065 #[test]
15066 fn load_metadata_object_rejects_per_object_zstd_frame_exactness_mutations() {
15067 let volume_header = test_volume_header();
15068 let crypto_header = test_crypto_header();
15069 let subkeys = Subkeys::derive(
15070 &master_key(),
15071 &volume_header.archive_uuid,
15072 &volume_header.session_id,
15073 )
15074 .unwrap();
15075 let mut next_block_index = 0u64;
15076
15077 let index_root_payload = b"index root metadata object";
15078 let index_root_compressed = compress_zstd_frame(index_root_payload, 1).unwrap();
15079 assert_metadata_object_from_compressed(
15080 &{
15081 let mut bytes = index_root_compressed.clone();
15082 bytes.push(0);
15083 bytes
15084 },
15085 index_root_payload.len(),
15086 &subkeys,
15087 &volume_header,
15088 &crypto_header,
15089 &subkeys.index_root_key,
15090 &subkeys.index_nonce_seed,
15091 b"idxroot",
15092 0,
15093 BlockKind::IndexRootData,
15094 BlockKind::IndexRootParity,
15095 crypto_header.index_root_fec_data_shards,
15096 crypto_header.index_root_fec_parity_shards,
15097 &mut next_block_index,
15098 FormatError::TrailingBytesAfterZstdFrame,
15099 );
15100 assert_metadata_object_from_compressed(
15101 &index_root_compressed,
15102 index_root_payload.len() + 1,
15103 &subkeys,
15104 &volume_header,
15105 &crypto_header,
15106 &subkeys.index_root_key,
15107 &subkeys.index_nonce_seed,
15108 b"idxroot",
15109 0,
15110 BlockKind::IndexRootData,
15111 BlockKind::IndexRootParity,
15112 crypto_header.index_root_fec_data_shards,
15113 crypto_header.index_root_fec_parity_shards,
15114 &mut next_block_index,
15115 FormatError::ZstdDecompressedSizeMismatch {
15116 expected: index_root_payload.len() + 1,
15117 actual: index_root_payload.len(),
15118 },
15119 );
15120
15121 let index_shard_payload = b"index shard metadata object";
15122 let index_shard_compressed = compress_zstd_frame(index_shard_payload, 1).unwrap();
15123 assert_metadata_object_from_compressed(
15124 &{
15125 let mut bytes = index_shard_compressed.clone();
15126 bytes.push(0);
15127 bytes
15128 },
15129 index_shard_payload.len(),
15130 &subkeys,
15131 &volume_header,
15132 &crypto_header,
15133 &subkeys.index_shard_key,
15134 &subkeys.index_nonce_seed,
15135 b"idxshard",
15136 1,
15137 BlockKind::IndexShardData,
15138 BlockKind::IndexShardParity,
15139 crypto_header.index_fec_data_shards,
15140 crypto_header.index_fec_parity_shards,
15141 &mut next_block_index,
15142 FormatError::TrailingBytesAfterZstdFrame,
15143 );
15144 assert_metadata_object_from_compressed(
15145 &index_shard_compressed,
15146 index_shard_payload.len() + 1,
15147 &subkeys,
15148 &volume_header,
15149 &crypto_header,
15150 &subkeys.index_shard_key,
15151 &subkeys.index_nonce_seed,
15152 b"idxshard",
15153 1,
15154 BlockKind::IndexShardData,
15155 BlockKind::IndexShardParity,
15156 crypto_header.index_fec_data_shards,
15157 crypto_header.index_fec_parity_shards,
15158 &mut next_block_index,
15159 FormatError::ZstdDecompressedSizeMismatch {
15160 expected: index_shard_payload.len() + 1,
15161 actual: index_shard_payload.len(),
15162 },
15163 );
15164
15165 let directory_hint_payload = b"directory hint metadata object";
15166 let directory_hint_compressed = compress_zstd_frame(directory_hint_payload, 1).unwrap();
15167 assert_metadata_object_from_compressed(
15168 &{
15169 let mut bytes = directory_hint_compressed.clone();
15170 bytes.push(0);
15171 bytes
15172 },
15173 directory_hint_payload.len(),
15174 &subkeys,
15175 &volume_header,
15176 &crypto_header,
15177 &subkeys.dir_hint_key,
15178 &subkeys.index_nonce_seed,
15179 b"dirhint",
15180 0,
15181 BlockKind::DirectoryHintData,
15182 BlockKind::DirectoryHintParity,
15183 crypto_header.index_fec_data_shards,
15184 crypto_header.index_fec_parity_shards,
15185 &mut next_block_index,
15186 FormatError::TrailingBytesAfterZstdFrame,
15187 );
15188 assert_metadata_object_from_compressed(
15189 &directory_hint_compressed,
15190 directory_hint_payload.len() + 1,
15191 &subkeys,
15192 &volume_header,
15193 &crypto_header,
15194 &subkeys.dir_hint_key,
15195 &subkeys.index_nonce_seed,
15196 b"dirhint",
15197 0,
15198 BlockKind::DirectoryHintData,
15199 BlockKind::DirectoryHintParity,
15200 crypto_header.index_fec_data_shards,
15201 crypto_header.index_fec_parity_shards,
15202 &mut next_block_index,
15203 FormatError::ZstdDecompressedSizeMismatch {
15204 expected: directory_hint_payload.len() + 1,
15205 actual: directory_hint_payload.len(),
15206 },
15207 );
15208
15209 let dictionary_payload = b"dictionary metadata object";
15210 let dictionary_compressed = compress_zstd_frame(dictionary_payload, 1).unwrap();
15211 assert_metadata_object_from_compressed(
15212 &{
15213 let mut bytes = dictionary_compressed.clone();
15214 bytes.push(0);
15215 bytes
15216 },
15217 dictionary_payload.len(),
15218 &subkeys,
15219 &volume_header,
15220 &crypto_header,
15221 &subkeys.dictionary_key,
15222 &subkeys.index_nonce_seed,
15223 b"dict",
15224 0,
15225 BlockKind::DictionaryData,
15226 BlockKind::DictionaryParity,
15227 crypto_header.index_root_fec_data_shards,
15228 crypto_header.index_root_fec_parity_shards,
15229 &mut next_block_index,
15230 FormatError::TrailingBytesAfterZstdFrame,
15231 );
15232 assert_metadata_object_from_compressed(
15233 &dictionary_compressed,
15234 dictionary_payload.len() + 1,
15235 &subkeys,
15236 &volume_header,
15237 &crypto_header,
15238 &subkeys.dictionary_key,
15239 &subkeys.index_nonce_seed,
15240 b"dict",
15241 0,
15242 BlockKind::DictionaryData,
15243 BlockKind::DictionaryParity,
15244 crypto_header.index_root_fec_data_shards,
15245 crypto_header.index_root_fec_parity_shards,
15246 &mut next_block_index,
15247 FormatError::ZstdDecompressedSizeMismatch {
15248 expected: dictionary_payload.len() + 1,
15249 actual: dictionary_payload.len(),
15250 },
15251 );
15252 }
15253
15254 #[test]
15255 fn load_metadata_object_extent_rejects_encrypted_size_not_data_block_count_times_block_size() {
15256 let volume_header = test_volume_header();
15257 let crypto_header = test_crypto_header();
15258 let subkeys = Subkeys::derive(
15259 &master_key(),
15260 &volume_header.archive_uuid,
15261 &volume_header.session_id,
15262 )
15263 .unwrap();
15264 let mut next_block_index = 0u64;
15265
15266 let index_root_payload = b"index root metadata object";
15267 let (index_root_extent, index_root_records) = build_metadata_object_from_payload(
15268 index_root_payload,
15269 &subkeys,
15270 &volume_header,
15271 &crypto_header,
15272 &subkeys.index_root_key,
15273 &subkeys.index_nonce_seed,
15274 b"idxroot",
15275 0,
15276 BlockKind::IndexRootData,
15277 &mut next_block_index,
15278 );
15279 let mut index_root_extent = index_root_extent;
15280 index_root_extent.encrypted_size = index_root_extent
15281 .encrypted_size
15282 .saturating_add(crypto_header.block_size);
15283 assert_eq!(
15284 load_metadata_object_from_parts(
15285 &index_root_records,
15286 ObjectLoadContext::index_root(
15287 &volume_header,
15288 &crypto_header,
15289 &subkeys,
15290 index_root_extent,
15291 ),
15292 index_root_payload.len() as u32,
15293 )
15294 .unwrap_err(),
15295 FormatError::InvalidArchive(
15296 "encrypted object size is not data_block_count * block_size"
15297 )
15298 );
15299
15300 let index_shard_payload = b"index shard metadata object";
15301 let (index_shard_extent, index_shard_records) = build_metadata_object_from_payload(
15302 index_shard_payload,
15303 &subkeys,
15304 &volume_header,
15305 &crypto_header,
15306 &subkeys.index_shard_key,
15307 &subkeys.index_nonce_seed,
15308 b"idxshard",
15309 1,
15310 BlockKind::IndexShardData,
15311 &mut next_block_index,
15312 );
15313 let mut index_shard_extent = index_shard_extent;
15314 index_shard_extent.encrypted_size = index_shard_extent
15315 .encrypted_size
15316 .saturating_add(crypto_header.block_size);
15317 assert_eq!(
15318 load_metadata_object_from_parts(
15319 &index_shard_records,
15320 ObjectLoadContext {
15321 volume_header: &volume_header,
15322 crypto_header: &crypto_header,
15323 extent: index_shard_extent,
15324 data_kind: BlockKind::IndexShardData,
15325 parity_kind: BlockKind::IndexShardParity,
15326 key: &subkeys.index_shard_key,
15327 nonce_seed: &subkeys.index_nonce_seed,
15328 domain: b"idxshard",
15329 counter: 1,
15330 class_data_shard_max: crypto_header.index_fec_data_shards,
15331 class_parity_shard_max: crypto_header.index_fec_parity_shards,
15332 },
15333 index_shard_payload.len() as u32,
15334 )
15335 .unwrap_err(),
15336 FormatError::InvalidArchive(
15337 "encrypted object size is not data_block_count * block_size"
15338 )
15339 );
15340
15341 let directory_hint_payload = b"directory hint metadata object";
15342 let (directory_hint_extent, directory_hint_records) = build_metadata_object_from_payload(
15343 directory_hint_payload,
15344 &subkeys,
15345 &volume_header,
15346 &crypto_header,
15347 &subkeys.dir_hint_key,
15348 &subkeys.index_nonce_seed,
15349 b"dirhint",
15350 0,
15351 BlockKind::DirectoryHintData,
15352 &mut next_block_index,
15353 );
15354 let mut directory_hint_extent = directory_hint_extent;
15355 directory_hint_extent.encrypted_size = directory_hint_extent
15356 .encrypted_size
15357 .saturating_add(crypto_header.block_size);
15358 assert_eq!(
15359 load_metadata_object_from_parts(
15360 &directory_hint_records,
15361 ObjectLoadContext {
15362 volume_header: &volume_header,
15363 crypto_header: &crypto_header,
15364 extent: directory_hint_extent,
15365 data_kind: BlockKind::DirectoryHintData,
15366 parity_kind: BlockKind::DirectoryHintParity,
15367 key: &subkeys.dir_hint_key,
15368 nonce_seed: &subkeys.index_nonce_seed,
15369 domain: b"dirhint",
15370 counter: 0,
15371 class_data_shard_max: crypto_header.index_fec_data_shards,
15372 class_parity_shard_max: crypto_header.index_fec_parity_shards,
15373 },
15374 directory_hint_payload.len() as u32,
15375 )
15376 .unwrap_err(),
15377 FormatError::InvalidArchive(
15378 "encrypted object size is not data_block_count * block_size"
15379 )
15380 );
15381
15382 let dictionary_payload = b"dictionary metadata object";
15383 let (dictionary_extent, dictionary_records) = build_metadata_object_from_payload(
15384 dictionary_payload,
15385 &subkeys,
15386 &volume_header,
15387 &crypto_header,
15388 &subkeys.dictionary_key,
15389 &subkeys.index_nonce_seed,
15390 b"dict",
15391 0,
15392 BlockKind::DictionaryData,
15393 &mut next_block_index,
15394 );
15395 let mut dictionary_extent = dictionary_extent;
15396 dictionary_extent.encrypted_size = dictionary_extent
15397 .encrypted_size
15398 .saturating_add(crypto_header.block_size);
15399 assert_eq!(
15400 load_metadata_object_from_parts(
15401 &dictionary_records,
15402 ObjectLoadContext {
15403 volume_header: &volume_header,
15404 crypto_header: &crypto_header,
15405 extent: dictionary_extent,
15406 data_kind: BlockKind::DictionaryData,
15407 parity_kind: BlockKind::DictionaryParity,
15408 key: &subkeys.dictionary_key,
15409 nonce_seed: &subkeys.index_nonce_seed,
15410 domain: b"dict",
15411 counter: 0,
15412 class_data_shard_max: crypto_header.index_root_fec_data_shards,
15413 class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
15414 },
15415 dictionary_payload.len() as u32,
15416 )
15417 .unwrap_err(),
15418 FormatError::InvalidArchive(
15419 "encrypted object size is not data_block_count * block_size"
15420 )
15421 );
15422 }
15423
15424 #[test]
15425 fn opens_complete_multi_volume_archive() {
15426 let files = [RegularFile::new("alpha.txt", b"hello from volume stripes")];
15427 let archive = write_archive(
15428 &files,
15429 &master_key(),
15430 WriterOptions {
15431 stripe_width: 2,
15432 volume_loss_tolerance: 1,
15433 ..single_stream_options()
15434 },
15435 )
15436 .unwrap();
15437 assert_eq!(archive.volumes.len(), 2);
15438
15439 let volume_refs = archive
15440 .volumes
15441 .iter()
15442 .map(Vec::as_slice)
15443 .collect::<Vec<_>>();
15444 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
15445
15446 assert_eq!(opened.volume_header.stripe_width, 2);
15447 assert_eq!(opened.list_files().unwrap()[0].path, "alpha.txt");
15448 assert_eq!(
15449 opened.extract_file("alpha.txt").unwrap(),
15450 Some(b"hello from volume stripes".to_vec())
15451 );
15452 opened.verify().unwrap();
15453 }
15454
15455 #[test]
15456 fn recovers_from_one_missing_volume_when_parity_allows() {
15457 let files = [RegularFile::new("alpha.txt", b"recover me")];
15458 let archive = write_archive(
15459 &files,
15460 &master_key(),
15461 WriterOptions {
15462 stripe_width: 2,
15463 volume_loss_tolerance: 1,
15464 ..single_stream_options()
15465 },
15466 )
15467 .unwrap();
15468
15469 let recovered =
15470 open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap();
15471 assert_eq!(
15472 recovered.extract_file("alpha.txt").unwrap(),
15473 Some(b"recover me".to_vec())
15474 );
15475 recovered.verify().unwrap();
15476 }
15477
15478 #[test]
15479 fn recovers_from_crc_corrupted_block_when_parity_allows() {
15480 let files = [RegularFile::new("alpha.txt", b"repair corrupt block")];
15481 let archive = write_archive(
15482 &files,
15483 &master_key(),
15484 WriterOptions {
15485 stripe_width: 2,
15486 volume_loss_tolerance: 1,
15487 ..single_stream_options()
15488 },
15489 )
15490 .unwrap();
15491 let mut volumes = archive.volumes.clone();
15492 corrupt_first_block_record_payload(&mut volumes[0]);
15493
15494 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
15495 let recovered = open_archive_volumes(&volume_refs, &master_key()).unwrap();
15496
15497 assert_eq!(
15498 recovered.extract_file("alpha.txt").unwrap(),
15499 Some(b"repair corrupt block".to_vec())
15500 );
15501 recovered.verify().unwrap();
15502 }
15503
15504 #[test]
15505 fn rejects_multi_volume_count_mismatch_without_tolerance() {
15506 let files = [RegularFile::new("alpha.txt", b"count check")];
15507 let archive = write_archive(
15508 &files,
15509 &master_key(),
15510 WriterOptions {
15511 stripe_width: 3,
15512 volume_loss_tolerance: 0,
15513 ..single_stream_options()
15514 },
15515 )
15516 .unwrap();
15517
15518 assert_eq!(
15519 open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap_err(),
15520 FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
15521 );
15522 }
15523
15524 #[test]
15525 fn rejects_multi_volume_manifest_bootstrap_field_mismatch() {
15526 let files = [RegularFile::new("alpha.txt", b"footer mismatch")];
15527 let archive = write_archive(
15528 &files,
15529 &master_key(),
15530 WriterOptions {
15531 stripe_width: 2,
15532 volume_loss_tolerance: 1,
15533 ..single_stream_options()
15534 },
15535 )
15536 .unwrap();
15537
15538 let mut bad_first = archive.volumes[0].clone();
15539 rewrite_manifest_footer(&mut bad_first, &master_key(), |footer| {
15540 footer.index_root_first_block = footer.index_root_first_block.wrapping_add(1);
15541 });
15542
15543 open_archive_volumes(
15544 &[bad_first.as_slice(), archive.volumes[1].as_slice()],
15545 &master_key(),
15546 )
15547 .unwrap();
15548 }
15549
15550 #[test]
15551 fn repairs_corrupted_index_root_block_in_multi_volume_archive() {
15552 let files = [RegularFile::new("alpha.txt", b"repair meta root")];
15553 let archive = write_archive(
15554 &files,
15555 &master_key(),
15556 WriterOptions {
15557 stripe_width: 2,
15558 volume_loss_tolerance: 1,
15559 ..single_stream_options()
15560 },
15561 )
15562 .unwrap();
15563 let mut volumes = archive.volumes.clone();
15564
15565 let mut corrupted = false;
15566 for volume in &mut volumes {
15567 if let Some(slot) =
15568 block_record_slots_with_kind(volume, BlockKind::IndexRootData).first()
15569 {
15570 corrupt_block_record_payload_at_slot(volume, *slot);
15571 corrupted = true;
15572 break;
15573 }
15574 }
15575 assert!(corrupted, "expected an IndexRootData record");
15576
15577 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
15578 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
15579 assert_eq!(
15580 opened.extract_file("alpha.txt").unwrap(),
15581 Some(b"repair meta root".to_vec())
15582 );
15583 opened.verify().unwrap();
15584 }
15585
15586 #[test]
15587 fn repairs_corrupted_index_shard_block_in_multi_volume_archive() {
15588 let files = [RegularFile::new("alpha.txt", b"repair meta shard")];
15589 let archive = write_archive(
15590 &files,
15591 &master_key(),
15592 WriterOptions {
15593 stripe_width: 2,
15594 volume_loss_tolerance: 1,
15595 ..single_stream_options()
15596 },
15597 )
15598 .unwrap();
15599 let mut volumes = archive.volumes.clone();
15600
15601 let mut corrupted = false;
15602 for volume in &mut volumes {
15603 if let Some(slot) =
15604 block_record_slots_with_kind(volume, BlockKind::IndexShardData).first()
15605 {
15606 corrupt_block_record_payload_at_slot(volume, *slot);
15607 corrupted = true;
15608 break;
15609 }
15610 }
15611 assert!(corrupted, "expected an IndexShardData record");
15612
15613 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
15614 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
15615 assert_eq!(
15616 opened.extract_file("alpha.txt").unwrap(),
15617 Some(b"repair meta shard".to_vec())
15618 );
15619 opened.verify().unwrap();
15620 }
15621
15622 #[test]
15623 fn rejects_missing_volume_when_loss_tolerance_zero_even_with_bitrot_parity() {
15624 let files = [RegularFile::new(
15625 "alpha.txt",
15626 b"bitrot parity is not volume loss",
15627 )];
15628 let archive = write_archive(
15629 &files,
15630 &master_key(),
15631 WriterOptions {
15632 stripe_width: 2,
15633 volume_loss_tolerance: 0,
15634 bit_rot_buffer_pct: 1,
15635 ..single_stream_options()
15636 },
15637 )
15638 .unwrap();
15639
15640 assert_eq!(
15641 open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap_err(),
15642 FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
15643 );
15644 }
15645
15646 #[test]
15647 fn repairs_crc_erasure_only_within_parity_budget() {
15648 let payload = pseudo_random_bytes(12_000);
15649 let archive = write_archive(
15650 &[RegularFile::new("rot.bin", &payload)],
15651 &master_key(),
15652 small_block_recovery_options(),
15653 )
15654 .unwrap();
15655 let payload_slots = first_payload_data_run_slots(&archive.bytes);
15656 assert!(
15657 payload_slots.len() >= 2,
15658 "fixture must contain a multi-block payload object"
15659 );
15660
15661 let mut one_erasure = archive.bytes.clone();
15662 corrupt_block_record_payload_at_slot(&mut one_erasure, payload_slots[0]);
15663 let repaired = open_archive(&one_erasure, &master_key()).unwrap();
15664 assert_eq!(
15665 repaired.extract_file("rot.bin").unwrap(),
15666 Some(payload.clone())
15667 );
15668
15669 let mut two_erasures = archive.bytes.clone();
15670 corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[0]);
15671 corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[1]);
15672 let unrepaired = open_archive(&two_erasures, &master_key()).unwrap();
15673 assert_eq!(
15674 unrepaired.extract_file("rot.bin").unwrap_err(),
15675 FormatError::FecTooFewAvailableShards
15676 );
15677 }
15678
15679 #[test]
15680 fn verify_rejects_missing_required_object_block_extent() {
15681 let (mut opened, missing_block) = multi_envelope_reader_fixture();
15682 assert!(opened.blocks.remove(&missing_block).is_some());
15683
15684 assert_eq!(
15685 opened.verify().unwrap_err(),
15686 FormatError::FecTooFewAvailableShards
15687 );
15688 }
15689
15690 #[test]
15691 fn parity_crc_erasure_does_not_hide_authenticated_data() {
15692 let payload = pseudo_random_bytes(12_000);
15693 let archive = write_archive(
15694 &[RegularFile::new("parity-erasure.bin", &payload)],
15695 &master_key(),
15696 parity_rich_recovery_options(),
15697 )
15698 .unwrap();
15699 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
15700 let parity_slots = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity);
15701 assert!(
15702 parity_slots.len() >= 2,
15703 "fixture must contain redundant parity shards"
15704 );
15705 let mut corrupted = archive.bytes;
15706 corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
15707 corrupt_block_record_payload_at_slot(&mut corrupted, parity_slots[0]);
15708
15709 let opened = open_archive(&corrupted, &master_key()).unwrap();
15710 assert_eq!(
15711 opened.extract_file("parity-erasure.bin").unwrap(),
15712 Some(payload)
15713 );
15714 opened.verify().unwrap();
15715 }
15716
15717 #[test]
15718 fn repair_patches_restore_crc_erased_payload_block() {
15719 let payload = pseudo_random_bytes(12_000);
15720 let archive = write_archive(
15721 &[RegularFile::new("rot.bin", &payload)],
15722 &master_key(),
15723 small_block_recovery_options(),
15724 )
15725 .unwrap();
15726 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
15727 let mut corrupted = archive.bytes.clone();
15728 corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
15729
15730 let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
15731 opened.verify().unwrap();
15732 let patches = opened.repair_patches().unwrap();
15733 assert_eq!(patches.len(), 1);
15734 apply_repair_patches(&mut corrupted, &patches);
15735
15736 let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
15737 repaired.verify().unwrap();
15738 assert!(repaired.repair_patches().unwrap().is_empty());
15739 }
15740
15741 #[test]
15742 fn repair_patches_restore_crc_erased_payload_parity_block() {
15743 let payload = pseudo_random_bytes(12_000);
15744 let archive = write_archive(
15745 &[RegularFile::new("parity-erasure.bin", &payload)],
15746 &master_key(),
15747 parity_rich_recovery_options(),
15748 )
15749 .unwrap();
15750 let parity_slot = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity)[0];
15751 let mut corrupted = archive.bytes.clone();
15752 corrupt_block_record_payload_at_slot(&mut corrupted, parity_slot);
15753
15754 let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
15755 opened.verify().unwrap();
15756 let patches = opened.repair_patches().unwrap();
15757 assert_eq!(patches.len(), 1);
15758 apply_repair_patches(&mut corrupted, &patches);
15759
15760 let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
15761 repaired.verify().unwrap();
15762 assert!(repaired.repair_patches().unwrap().is_empty());
15763 }
15764
15765 #[test]
15766 fn recovers_physical_odd_block_size_from_cmra_authority() {
15767 let archive = write_archive(
15768 &[RegularFile::new("odd-block.txt", b"payload")],
15769 &master_key(),
15770 small_block_recovery_options(),
15771 )
15772 .unwrap();
15773 let mut malformed = archive.bytes;
15774 let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
15775 let block_size_offset = volume_header.crypto_header_offset as usize + 24;
15776 malformed[block_size_offset..block_size_offset + 4].copy_from_slice(&4097u32.to_le_bytes());
15777
15778 let opened = open_archive(&malformed, &master_key()).unwrap();
15779 assert_ne!(opened.crypto_header.block_size, 4097);
15780 assert_eq!(
15781 opened.extract_file("odd-block.txt").unwrap(),
15782 Some(b"payload".to_vec())
15783 );
15784 opened.verify().unwrap();
15785 }
15786
15787 #[test]
15788 fn repairs_structurally_malformed_payload_block_slots() {
15789 let payload = pseudo_random_bytes(12_000);
15790 let archive = write_archive(
15791 &[RegularFile::new("structural-block.bin", &payload)],
15792 &master_key(),
15793 small_block_recovery_options(),
15794 )
15795 .unwrap();
15796 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
15797
15798 let mut bad_magic = archive.bytes.clone();
15799 corrupt_block_record_magic_at_slot(&mut bad_magic, payload_slot);
15800 assert_eq!(
15801 open_archive(&bad_magic, &master_key())
15802 .unwrap()
15803 .extract_file("structural-block.bin")
15804 .unwrap(),
15805 Some(payload.clone())
15806 );
15807
15808 let mut bad_reserved = archive.bytes;
15809 corrupt_block_record_reserved_at_slot(&mut bad_reserved, payload_slot);
15810 assert_eq!(
15811 open_archive(&bad_reserved, &master_key())
15812 .unwrap()
15813 .extract_file("structural-block.bin")
15814 .unwrap(),
15815 Some(payload)
15816 );
15817 }
15818
15819 #[test]
15820 fn repair_patches_restore_structurally_malformed_payload_block_slot() {
15821 let payload = pseudo_random_bytes(12_000);
15822 let archive = write_archive(
15823 &[RegularFile::new("structural-patch.bin", &payload)],
15824 &master_key(),
15825 small_block_recovery_options(),
15826 )
15827 .unwrap();
15828 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
15829 let mut corrupted = archive.bytes.clone();
15830 corrupt_block_record_magic_at_slot(&mut corrupted, payload_slot);
15831
15832 let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
15833 opened.verify().unwrap();
15834 assert_eq!(
15835 opened.extract_file("structural-patch.bin").unwrap(),
15836 Some(payload)
15837 );
15838 let patches = opened.repair_patches().unwrap();
15839 assert_eq!(patches.len(), 1);
15840 apply_repair_patches(&mut corrupted, &patches);
15841
15842 let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
15843 repaired.verify().unwrap();
15844 assert!(repaired.repair_patches().unwrap().is_empty());
15845 }
15846
15847 #[test]
15848 fn repairs_structurally_malformed_index_root_block_slot() {
15849 let archive = write_archive(
15850 &[RegularFile::new(
15851 "structural-index-root.txt",
15852 b"metadata repair",
15853 )],
15854 &master_key(),
15855 small_block_recovery_options(),
15856 )
15857 .unwrap();
15858 let index_root_slot =
15859 first_block_record_slot_with_kind(&archive.bytes, BlockKind::IndexRootData).unwrap();
15860 let mut corrupted = archive.bytes;
15861 corrupt_block_record_magic_at_slot(&mut corrupted, index_root_slot);
15862
15863 let opened = open_archive(&corrupted, &master_key()).unwrap();
15864 assert_eq!(
15865 opened.extract_file("structural-index-root.txt").unwrap(),
15866 Some(b"metadata repair".to_vec())
15867 );
15868 opened.verify().unwrap();
15869 }
15870
15871 #[test]
15872 fn rejects_parity_block_with_last_data_flag() {
15873 let archive = write_archive(
15874 &[RegularFile::new("parity-flag.txt", b"payload")],
15875 &master_key(),
15876 small_block_recovery_options(),
15877 )
15878 .unwrap();
15879 let parity_slot =
15880 first_block_record_slot_with_kind(&archive.bytes, BlockKind::PayloadParity).unwrap();
15881 let mut malformed = archive.bytes;
15882 mutate_block_record_at_slot(&mut malformed, parity_slot, |record| {
15883 record.flags = 0x01;
15884 });
15885
15886 assert_eq!(
15887 open_archive(&malformed, &master_key()).unwrap_err(),
15888 FormatError::ParityBlockHasLastDataFlag
15889 );
15890 }
15891
15892 #[test]
15893 fn rejects_missing_and_duplicate_payload_last_data_flags() {
15894 let payload = pseudo_random_bytes(12_000);
15895 let archive = write_archive(
15896 &[RegularFile::new("flags.bin", &payload)],
15897 &master_key(),
15898 small_block_recovery_options(),
15899 )
15900 .unwrap();
15901 let payload_slots = first_payload_data_run_slots(&archive.bytes);
15902 assert!(
15903 payload_slots.len() >= 2,
15904 "fixture must contain a multi-block payload object"
15905 );
15906
15907 let mut duplicate_last = archive.bytes.clone();
15908 mutate_block_record_at_slot(&mut duplicate_last, payload_slots[0], |record| {
15909 record.flags = 0x01;
15910 });
15911 let opened = open_archive(&duplicate_last, &master_key()).unwrap();
15912 assert_eq!(
15913 opened.extract_file("flags.bin").unwrap_err(),
15914 FormatError::InvalidArchive("object last-data flag is not on the final data block")
15915 );
15916
15917 let mut missing_last = archive.bytes;
15918 mutate_block_record_at_slot(
15919 &mut missing_last,
15920 *payload_slots.last().unwrap(),
15921 |record| {
15922 record.flags = 0;
15923 },
15924 );
15925 let opened = open_archive(&missing_last, &master_key()).unwrap();
15926 assert_eq!(
15927 opened.extract_file("flags.bin").unwrap_err(),
15928 FormatError::InvalidArchive("object last-data flag is not on the final data block")
15929 );
15930 }
15931
15932 #[test]
15933 fn recovers_from_one_corrupt_manifest_footer_copy_when_another_volume_authenticates() {
15934 let files = [RegularFile::new(
15935 "footer-copy.txt",
15936 b"survives one bad footer",
15937 )];
15938 let archive = write_archive(
15939 &files,
15940 &master_key(),
15941 WriterOptions {
15942 stripe_width: 2,
15943 volume_loss_tolerance: 1,
15944 ..single_stream_options()
15945 },
15946 )
15947 .unwrap();
15948 let mut volumes = archive.volumes.clone();
15949 corrupt_manifest_footer_hmac(&mut volumes[0]);
15950
15951 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
15952 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
15953 assert_eq!(opened.manifest_footer.volume_index, 0);
15954 assert_eq!(opened.volume_header.volume_index, 0);
15955 assert_eq!(opened.volume_trailer.as_ref().unwrap().volume_index, 0);
15956 assert_eq!(
15957 opened.extract_file("footer-copy.txt").unwrap(),
15958 Some(b"survives one bad footer".to_vec())
15959 );
15960 opened.verify().unwrap();
15961 }
15962
15963 #[test]
15964 fn manifest_footer_corruption_requires_trusted_sidecar() {
15965 let archive = write_archive(
15966 &[RegularFile::new("footer.txt", b"sidecar authority")],
15967 &master_key(),
15968 single_stream_options(),
15969 )
15970 .unwrap();
15971 let manifest_offset = terminal_material_offset(&archive.bytes);
15972 let mut corrupted = archive.bytes.clone();
15973 corrupted[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
15974 corrupt_v41_terminal_recovery(&mut corrupted);
15975
15976 assert!(open_archive(&corrupted, &master_key()).is_err());
15977
15978 let opened =
15979 open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
15980 .unwrap();
15981 assert!(opened.volume_trailer.is_none());
15982 assert_eq!(
15983 opened.extract_file("footer.txt").unwrap(),
15984 Some(b"sidecar authority".to_vec())
15985 );
15986 opened.verify().unwrap();
15987 }
15988
15989 #[test]
15990 fn authenticated_footer_trailer_and_sidecar_hmac_boundaries_are_enforced() {
15991 let archive = write_archive(
15992 &[RegularFile::new("hmac-boundary.txt", b"boundary bytes")],
15993 &master_key(),
15994 single_stream_options(),
15995 )
15996 .unwrap();
15997 let strict_options = ReaderOptions {
15998 max_trailing_garbage_scan: 0,
15999 ..ReaderOptions::default()
16000 };
16001
16002 let manifest_offset = terminal_material_offset(&archive.bytes);
16003 for offset in [
16004 manifest_offset + 71,
16005 manifest_offset + MANIFEST_HMAC_COVERED_LEN,
16006 ] {
16007 let mut corrupted = archive.bytes.clone();
16008 corrupted[offset] ^= 0x01;
16009 open_archive(&corrupted, &master_key()).unwrap();
16010 }
16011
16012 let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
16013 for offset in [
16014 trailer_offset + 75,
16015 trailer_offset + TRAILER_HMAC_COVERED_LEN,
16016 ] {
16017 let mut corrupted = archive.bytes.clone();
16018 corrupted[offset] ^= 0x01;
16019 OpenedArchive::open_with_options(&corrupted, &master_key(), strict_options).unwrap();
16020 }
16021
16022 let mut covered_sidecar = archive.bootstrap_sidecar.clone();
16023 let mut header =
16024 BootstrapSidecarHeader::parse(&covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
16025 .unwrap();
16026 header.manifest_footer_offset += 1;
16027 covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
16028 assert_eq!(
16029 open_archive_with_bootstrap_sidecar(&archive.bytes, &covered_sidecar, &master_key())
16030 .unwrap_err(),
16031 FormatError::HmacMismatch {
16032 structure: "BootstrapSidecarHeader"
16033 }
16034 );
16035
16036 let mut tag_sidecar = archive.bootstrap_sidecar.clone();
16037 let mut header =
16038 BootstrapSidecarHeader::parse(&tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
16039 header.sidecar_hmac[0] ^= 1;
16040 tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
16041 assert_eq!(
16042 open_archive_with_bootstrap_sidecar(&archive.bytes, &tag_sidecar, &master_key())
16043 .unwrap_err(),
16044 FormatError::HmacMismatch {
16045 structure: "BootstrapSidecarHeader"
16046 }
16047 );
16048
16049 let mut non_covered_sidecar = archive.bootstrap_sidecar.clone();
16050 let header =
16051 BootstrapSidecarHeader::parse(&non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
16052 .unwrap();
16053 let mut header_bytes = header.to_bytes();
16054 header_bytes[124] ^= 0x01;
16055 let crc = crc32c::crc32c(&header_bytes[..124]);
16056 header_bytes[124..128].copy_from_slice(&crc.to_le_bytes());
16057 non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
16058 let opened = open_archive_with_bootstrap_sidecar(
16059 &archive.bytes,
16060 &non_covered_sidecar,
16061 &master_key(),
16062 )
16063 .unwrap();
16064 assert_eq!(
16065 opened.extract_file("hmac-boundary.txt").unwrap(),
16066 Some(b"boundary bytes".to_vec())
16067 );
16068 }
16069
16070 #[test]
16071 fn rejects_authenticated_footer_and_trailer_volume_index_mismatches() {
16072 let archive = write_archive(
16073 &[RegularFile::new("volume-index.txt", b"identity")],
16074 &master_key(),
16075 single_stream_options(),
16076 )
16077 .unwrap();
16078
16079 let mut bad_trailer = archive.bytes.clone();
16080 rewrite_volume_trailer(&mut bad_trailer, &master_key(), |trailer| {
16081 trailer.volume_index = 1;
16082 });
16083 open_archive(&bad_trailer, &master_key()).unwrap();
16084
16085 let mut bad_manifest = archive.bytes;
16086 rewrite_manifest_footer(&mut bad_manifest, &master_key(), |footer| {
16087 footer.volume_index = 1;
16088 });
16089 open_archive(&bad_manifest, &master_key()).unwrap();
16090 }
16091
16092 #[test]
16093 fn rejects_same_key_header_terminal_material_splice() {
16094 let first = write_archive(
16095 &[RegularFile::new("splice.txt", b"same shape")],
16096 &master_key(),
16097 single_stream_options(),
16098 )
16099 .unwrap();
16100 let second = write_archive(
16101 &[RegularFile::new("splice.txt", b"same shape")],
16102 &master_key(),
16103 single_stream_options(),
16104 )
16105 .unwrap();
16106 assert_ne!(first.archive_uuid, second.archive_uuid);
16107 assert_eq!(
16108 terminal_material_offset(&first.bytes),
16109 terminal_material_offset(&second.bytes)
16110 );
16111 assert_eq!(first.bytes.len(), second.bytes.len());
16112
16113 let terminal_offset = terminal_material_offset(&first.bytes);
16114 let mut spliced = first.bytes.clone();
16115 spliced[terminal_offset..].copy_from_slice(&second.bytes[terminal_offset..]);
16116
16117 assert_eq!(
16118 open_archive(&spliced, &master_key()).unwrap_err(),
16119 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
16120 );
16121 }
16122
16123 #[test]
16124 fn rejects_cmra_crypto_header_pre_hmac_mismatch() {
16125 let kdf_params = crate::crypto::KdfParams::Argon2id {
16126 t_cost: 1,
16127 m_cost_kib: 8,
16128 parallelism: 1,
16129 salt: b"0123456789abcdef".to_vec(),
16130 };
16131 let archive = write_archive_with_kdf(
16132 &[RegularFile::new("cmra-crypto.txt", b"same fixed header")],
16133 &master_key(),
16134 single_stream_options(),
16135 &kdf_params,
16136 )
16137 .unwrap();
16138 let mut mutated = archive.bytes.clone();
16139 let volume_header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
16140 let subkeys = Subkeys::derive(
16141 &master_key(),
16142 &volume_header.archive_uuid,
16143 &volume_header.session_id,
16144 )
16145 .unwrap();
16146
16147 rewrite_cmra_image(&mut mutated, CmraRecoveryMode::KeyHolding, |image| {
16148 let crypto_region = image
16149 .regions
16150 .iter_mut()
16151 .find(|region| region.region_type == 2)
16152 .unwrap();
16153 let hmac_offset = crypto_region.bytes.len() - CRYPTO_HEADER_HMAC_LEN;
16154 let salt_start = CRYPTO_HEADER_FIXED_LEN + 16;
16155 crypto_region.bytes[salt_start] ^= 0x01;
16156 let hmac = compute_hmac(
16157 HmacDomain::CryptoHeader,
16158 &subkeys.mac_key,
16159 &volume_header.archive_uuid,
16160 &volume_header.session_id,
16161 &crypto_region.bytes[..hmac_offset],
16162 );
16163 crypto_region.bytes[hmac_offset..].copy_from_slice(&hmac);
16164 });
16165
16166 let final_offset = mutated.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
16167 let locator = final_recovery_locator(&mutated);
16168 let crypto_start = volume_header.crypto_header_offset as usize;
16169 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
16170 let parsed_crypto = CryptoHeader::parse(
16171 &mutated[crypto_start..crypto_end],
16172 volume_header.crypto_header_length,
16173 )
16174 .unwrap();
16175 assert_eq!(
16176 parse_locator_cmra_candidate(
16177 &mutated,
16178 final_offset,
16179 locator,
16180 KeyHoldingTerminalContext {
16181 subkeys: &subkeys,
16182 volume_header: &volume_header,
16183 crypto_header: &parsed_crypto.fixed,
16184 crypto_header_bytes: &mutated[crypto_start..crypto_end],
16185 },
16186 )
16187 .unwrap_err(),
16188 FormatError::InvalidArchive("CMRA CryptoHeader differs from parsed CryptoHeader")
16189 );
16190 assert!(open_archive(&mutated, &master_key()).is_err());
16191 }
16192
16193 #[test]
16194 fn recovers_physical_crypto_header_splice_from_cmra_authority() {
16195 let base = WriterOptions {
16196 archive_uuid: Some([0x11; 16]),
16197 session_id: Some([0x22; 16]),
16198 ..small_block_recovery_options()
16199 };
16200 let same_archive = WriterOptions {
16201 archive_uuid: Some([0x11; 16]),
16202 session_id: Some([0x33; 16]),
16203 ..small_block_recovery_options()
16204 };
16205
16206 let first = write_archive(
16207 &[RegularFile::new("splice.txt", b"same shape")],
16208 &master_key(),
16209 base,
16210 )
16211 .unwrap();
16212 let second = write_archive(
16213 &[RegularFile::new("splice.txt", b"same shape")],
16214 &master_key(),
16215 same_archive,
16216 )
16217 .unwrap();
16218
16219 let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
16220 let second_volume_header = VolumeHeader::parse(&second.bytes[..VOLUME_HEADER_LEN]).unwrap();
16221 let crypto_start = volume_header.crypto_header_offset as usize;
16222 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
16223 let second_crypto_end = second_volume_header.crypto_header_offset as usize
16224 + second_volume_header.crypto_header_length as usize;
16225 assert_eq!(crypto_end, second_crypto_end);
16226
16227 let mut spliced = first.bytes.clone();
16228 spliced[crypto_start..crypto_end].copy_from_slice(&second.bytes[crypto_start..crypto_end]);
16229
16230 let opened = open_archive(&spliced, &master_key()).unwrap();
16231 assert_eq!(
16232 opened.crypto_header_bytes,
16233 first.bytes[crypto_start..crypto_end].to_vec()
16234 );
16235 assert_eq!(
16236 opened.extract_file("splice.txt").unwrap(),
16237 Some(b"same shape".to_vec())
16238 );
16239 opened.verify().unwrap();
16240 }
16241
16242 #[test]
16243 fn rejects_same_key_object_splice_with_session_mismatch() {
16244 let first = write_archive(
16245 &[RegularFile::new("splice.txt", b"same shape")],
16246 &master_key(),
16247 WriterOptions {
16248 archive_uuid: Some([0x11; 16]),
16249 session_id: Some([0x22; 16]),
16250 ..single_stream_options()
16251 },
16252 )
16253 .unwrap();
16254 let second = write_archive(
16255 &[RegularFile::new("splice.txt", b"same shape")],
16256 &master_key(),
16257 WriterOptions {
16258 archive_uuid: Some([0x11; 16]),
16259 session_id: Some([0x33; 16]),
16260 ..single_stream_options()
16261 },
16262 )
16263 .unwrap();
16264
16265 let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
16266 let crypto_end = volume_header.crypto_header_offset as usize
16267 + volume_header.crypto_header_length as usize;
16268 let terminal_offset = terminal_material_offset(&first.bytes);
16269 let second_terminal_offset = terminal_material_offset(&second.bytes);
16270 assert_eq!(terminal_offset, second_terminal_offset);
16271
16272 let mut spliced = first.bytes.clone();
16273 spliced[crypto_end..terminal_offset]
16274 .copy_from_slice(&second.bytes[crypto_end..terminal_offset]);
16275
16276 assert_eq!(
16277 open_archive(&spliced, &master_key()).unwrap_err(),
16278 FormatError::AeadFailure
16279 );
16280 }
16281
16282 #[test]
16283 fn rejects_authenticated_trailer_pointer_and_count_mutations() {
16284 let archive = write_archive(
16285 &[RegularFile::new(
16286 "trailer-range.txt",
16287 b"authenticated ranges",
16288 )],
16289 &master_key(),
16290 single_stream_options(),
16291 )
16292 .unwrap();
16293 let strict_options = ReaderOptions {
16294 max_trailing_garbage_scan: 0,
16295 ..ReaderOptions::default()
16296 };
16297 let bytes = archive.bytes;
16298 let manifest_offset = terminal_material_offset(&bytes);
16299 let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
16300
16301 let mut wrong_footer_length = bytes.clone();
16302 rewrite_volume_trailer(&mut wrong_footer_length, &master_key(), |trailer| {
16303 trailer.manifest_footer_length = 42;
16304 });
16305 OpenedArchive::open_with_options(&wrong_footer_length, &master_key(), strict_options)
16306 .unwrap();
16307
16308 for (label, offset) in [
16309 (
16310 "offset before trailer by 1",
16311 manifest_offset.saturating_sub(1),
16312 ),
16313 ("offset after trailer", manifest_offset + 1),
16314 ("offset at stream start", 0),
16315 ("offset at trailer", trailer_offset),
16316 ("offset beyond trailer", trailer_offset + 4),
16317 ] {
16318 let mut wrong_footer_offset = bytes.clone();
16319 rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
16320 trailer.manifest_footer_offset = offset as u64;
16321 });
16322 open_archive(&wrong_footer_offset, &master_key())
16323 .unwrap_or_else(|err| panic!("manifest offset case {label}: {err:?}"));
16324 }
16325
16326 let mut wrong_bytes_written = bytes.clone();
16327 rewrite_volume_trailer(&mut wrong_bytes_written, &master_key(), |trailer| {
16328 trailer.bytes_written += 1;
16329 });
16330 open_archive(&wrong_bytes_written, &master_key()).unwrap();
16331
16332 let mut wrong_block_count = bytes.clone();
16333 rewrite_volume_trailer(&mut wrong_block_count, &master_key(), |trailer| {
16334 trailer.block_count += 1;
16335 });
16336 open_archive(&wrong_block_count, &master_key()).unwrap();
16337
16338 let mut wrong_footer_offset = bytes.clone();
16339 rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
16340 trailer.manifest_footer_offset = bytes.len() as u64 + 1024;
16341 });
16342 open_archive(&wrong_footer_offset, &master_key()).unwrap();
16343 }
16344
16345 #[test]
16346 fn rejects_authenticated_trailer_outside_trailing_scan_cap() {
16347 let archive = write_archive(
16348 &[RegularFile::new(
16349 "trailer-trailing-scan.txt",
16350 b"trailer scan boundaries",
16351 )],
16352 &master_key(),
16353 single_stream_options(),
16354 )
16355 .unwrap();
16356 let options = ReaderOptions {
16357 max_trailing_garbage_scan: 8,
16358 ..ReaderOptions::default()
16359 };
16360
16361 let mut within_scan = archive.bytes.clone();
16362 within_scan.resize(within_scan.len() + options.max_trailing_garbage_scan, 0xAA);
16363 let opened =
16364 OpenedArchive::open_with_options(&within_scan, &master_key(), options).unwrap();
16365 assert_eq!(
16366 opened.extract_file("trailer-trailing-scan.txt").unwrap(),
16367 Some(b"trailer scan boundaries".to_vec())
16368 );
16369
16370 let mut beyond_scan = archive.bytes.clone();
16371 beyond_scan.resize(
16372 beyond_scan.len() + max_critical_recovery_scan(options).unwrap() + 1,
16373 0xAA,
16374 );
16375 assert_eq!(
16376 OpenedArchive::open_with_options(&beyond_scan, &master_key(), options).unwrap_err(),
16377 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
16378 );
16379 }
16380
16381 #[test]
16382 fn rejects_authenticated_index_root_extent_size_mismatch_at_open() {
16383 let archive = write_archive(
16384 &[RegularFile::new("index-root-size.txt", b"extent size")],
16385 &master_key(),
16386 single_stream_options(),
16387 )
16388 .unwrap();
16389 let mut malformed = archive.bytes;
16390 let slot = first_block_record_slot_with_kind(&malformed, BlockKind::IndexRootData)
16391 .expect("archive should contain IndexRootData");
16392 mutate_block_record_at_slot(&mut malformed, slot, |record| {
16393 record.payload[0] ^= 0x55;
16394 });
16395
16396 assert_eq!(
16397 open_archive(&malformed, &master_key()).unwrap_err(),
16398 FormatError::AeadFailure
16399 );
16400 }
16401
16402 #[test]
16403 fn rejects_block_record_at_wrong_stripe_position() {
16404 let files = [RegularFile::new("alpha.txt", b"wrong stripe")];
16405 let archive = write_archive(
16406 &files,
16407 &master_key(),
16408 WriterOptions {
16409 stripe_width: 2,
16410 volume_loss_tolerance: 1,
16411 ..single_stream_options()
16412 },
16413 )
16414 .unwrap();
16415 let mut volumes = archive.volumes.clone();
16416 mutate_first_block_record(&mut volumes[0], |record| {
16417 record.block_index += 2;
16418 });
16419
16420 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
16421 assert_eq!(
16422 open_archive_volumes(&volume_refs, &master_key()).unwrap_err(),
16423 FormatError::InvalidArchive("BlockRecord index does not match volume position")
16424 );
16425 }
16426
16427 #[test]
16428 fn rejects_decreasing_block_record_index_in_required_region() {
16429 let archive = write_archive(
16430 &[RegularFile::new("alpha.txt", b"decreasing block index")],
16431 &master_key(),
16432 single_stream_options(),
16433 )
16434 .unwrap();
16435 assert!(block_record_slots(&archive.bytes).len() >= 2);
16436
16437 let mut malformed = archive.bytes;
16438 mutate_block_record_at_slot(&mut malformed, 1, |record| {
16439 record.block_index = 0;
16440 });
16441
16442 assert_eq!(
16443 open_archive(&malformed, &master_key()).unwrap_err(),
16444 FormatError::InvalidArchive("BlockRecord index does not match volume position")
16445 );
16446 }
16447
16448 #[test]
16449 fn rejects_duplicate_authenticated_volume_indexes() {
16450 let files = [RegularFile::new("alpha.txt", b"duplicates")];
16451 let archive = write_archive(
16452 &files,
16453 &master_key(),
16454 WriterOptions {
16455 stripe_width: 2,
16456 volume_loss_tolerance: 1,
16457 ..single_stream_options()
16458 },
16459 )
16460 .unwrap();
16461
16462 assert_eq!(
16463 open_archive_volumes(
16464 &[archive.volumes[0].as_slice(), archive.volumes[0].as_slice()],
16465 &master_key()
16466 )
16467 .unwrap_err(),
16468 FormatError::InvalidArchive("duplicate authenticated volume index")
16469 );
16470 }
16471
16472 #[test]
16473 fn rejects_conflicting_duplicate_authenticated_volume_indexes_by_default() {
16474 let files = [RegularFile::new("alpha.txt", b"conflicting duplicates")];
16475 let archive = write_archive(
16476 &files,
16477 &master_key(),
16478 WriterOptions {
16479 stripe_width: 2,
16480 volume_loss_tolerance: 1,
16481 ..single_stream_options()
16482 },
16483 )
16484 .unwrap();
16485 let mut conflicting = archive.volumes[0].clone();
16486 corrupt_first_block_record_payload(&mut conflicting);
16487
16488 assert_eq!(
16489 open_archive_volumes(
16490 &[archive.volumes[0].as_slice(), conflicting.as_slice()],
16491 &master_key()
16492 )
16493 .unwrap_err(),
16494 FormatError::InvalidArchive("duplicate authenticated volume index")
16495 );
16496 }
16497
16498 fn directory_hint_table_from_rows(
16499 hint_shard_index: u64,
16500 rows: &[(Vec<u8>, Vec<u32>)],
16501 shard_count: u32,
16502 ) -> DirectoryHintTable {
16503 let mut entries = Vec::new();
16504 let mut shard_row_indexes = Vec::new();
16505 let mut string_pool = Vec::new();
16506
16507 for (path, rows) in rows {
16508 let path_offset = if path.is_empty() {
16509 0
16510 } else {
16511 let offset = string_pool.len() as u64;
16512 string_pool.extend_from_slice(path);
16513 offset
16514 };
16515 let shard_list_start_index = shard_row_indexes.len() as u32;
16516 shard_row_indexes.extend_from_slice(rows);
16517 entries.push(DirectoryHintEntry {
16518 dir_hash: hash_prefix(path),
16519 path_offset,
16520 path_length: path.len() as u32,
16521 shard_list_start_index,
16522 shard_count: rows.len() as u32,
16523 });
16524 }
16525
16526 let table_bytes =
16527 directory_hint_table_bytes(hint_shard_index, entries, shard_row_indexes, string_pool);
16528 let locating = DirectoryHintShardEntry {
16529 hint_shard_index,
16530 first_dir_hash: hash_prefix(&rows.first().unwrap().0),
16531 last_dir_hash: hash_prefix(&rows.last().unwrap().0),
16532 first_block_index: 0,
16533 data_block_count: 1,
16534 parity_block_count: 0,
16535 encrypted_size: 4096,
16536 decompressed_size: table_bytes.len() as u32,
16537 entry_count: rows.len() as u64,
16538 };
16539 DirectoryHintTable::parse(
16540 &table_bytes,
16541 &locating,
16542 shard_count,
16543 MetadataLimits::default(),
16544 )
16545 .unwrap()
16546 }
16547
16548 fn directory_hint_table_bytes(
16549 hint_shard_index: u64,
16550 entries: Vec<DirectoryHintEntry>,
16551 shard_row_indexes: Vec<u32>,
16552 string_pool: Vec<u8>,
16553 ) -> Vec<u8> {
16554 let header_len = DirectoryHintTableHeader {
16555 version: 1,
16556 hint_shard_index,
16557 entry_count: 0,
16558 entry_table_offset: 0,
16559 shard_list_offset: 0,
16560 string_pool_offset: 0,
16561 string_pool_size: 0,
16562 }
16563 .to_bytes()
16564 .len();
16565 let entry_len = entries
16566 .first()
16567 .map(|entry| entry.to_bytes().len())
16568 .unwrap_or(0);
16569 let shard_list_offset = if entries.is_empty() {
16570 0
16571 } else {
16572 header_len + entries.len() * entry_len
16573 };
16574 let string_pool_offset = if string_pool.is_empty() {
16575 0
16576 } else {
16577 shard_list_offset + shard_row_indexes.len() * 4
16578 };
16579
16580 let header = DirectoryHintTableHeader {
16581 version: 1,
16582 hint_shard_index,
16583 entry_count: entries.len() as u64,
16584 entry_table_offset: if entries.is_empty() {
16585 0
16586 } else {
16587 header_len as u64
16588 },
16589 shard_list_offset: shard_list_offset as u64,
16590 string_pool_offset: string_pool_offset as u64,
16591 string_pool_size: string_pool.len() as u64,
16592 };
16593
16594 let mut out = Vec::new();
16595 out.extend_from_slice(&header.to_bytes());
16596 for entry in entries {
16597 out.extend_from_slice(&entry.to_bytes());
16598 }
16599 for row in shard_row_indexes {
16600 out.extend_from_slice(&row.to_le_bytes());
16601 }
16602 out.extend_from_slice(&string_pool);
16603 out
16604 }
16605
16606 fn corrupt_first_block_record_payload(volume: &mut [u8]) {
16607 let (record_offset, _) = first_block_record(volume);
16608 volume[record_offset + 16] ^= 0x55;
16609 }
16610
16611 fn corrupt_block_record_payload_at_slot(volume: &mut [u8], slot: usize) {
16612 let (record_offset, _) = block_record_at_slot(volume, slot);
16613 volume[record_offset + 16] ^= 0x55;
16614 }
16615
16616 fn apply_repair_patches(volume: &mut [u8], patches: &[ArchiveRepairPatch]) {
16617 for patch in patches {
16618 let offset = patch.record_offset as usize;
16619 let end = offset + patch.record_bytes.len();
16620 volume[offset..end].copy_from_slice(&patch.record_bytes);
16621 }
16622 }
16623
16624 fn corrupt_block_record_magic_at_slot(volume: &mut [u8], slot: usize) {
16625 let (record_offset, _) = block_record_at_slot(volume, slot);
16626 volume[record_offset] ^= 0x55;
16627 }
16628
16629 fn corrupt_block_record_reserved_at_slot(volume: &mut [u8], slot: usize) {
16630 let (record_offset, _) = block_record_at_slot(volume, slot);
16631 volume[record_offset + 14] = 0x01;
16632 }
16633
16634 fn corrupt_manifest_footer_hmac(volume: &mut [u8]) {
16635 let manifest_offset = terminal_material_offset(volume);
16636 volume[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
16637 }
16638
16639 fn final_recovery_locator(volume: &[u8]) -> CriticalRecoveryLocator {
16640 let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
16641 CriticalRecoveryLocator::parse(
16642 &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
16643 )
16644 .unwrap()
16645 }
16646
16647 fn rewrite_cmra_parity_count(volume: &[u8], parity_shard_count: u16) -> Vec<u8> {
16648 let locator = final_recovery_locator(volume);
16649 let tuple = CmraDecoderTuple::from(locator);
16650 assert!(parity_shard_count < tuple.parity_shard_count);
16651 let cmra_offset = locator.cmra_offset as usize;
16652 let shard_size = tuple.shard_size as usize;
16653 let row_len = CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size;
16654 let kept_rows = tuple.data_shard_count as usize + parity_shard_count as usize;
16655 let mut header = CriticalMetadataRecoveryHeader::parse(
16656 &volume[cmra_offset..cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN],
16657 )
16658 .unwrap();
16659 header.parity_shard_count = parity_shard_count;
16660
16661 let mut cmra =
16662 Vec::with_capacity(CRITICAL_METADATA_RECOVERY_HEADER_LEN + kept_rows * row_len);
16663 cmra.extend_from_slice(&header.to_bytes());
16664 let rows_start = cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
16665 for row in 0..kept_rows {
16666 let start = rows_start + row * row_len;
16667 cmra.extend_from_slice(&volume[start..start + row_len]);
16668 }
16669
16670 let mut out = Vec::with_capacity(cmra_offset + cmra.len() + LOCATOR_PAIR_LEN);
16671 out.extend_from_slice(&volume[..cmra_offset]);
16672 out.extend_from_slice(&cmra);
16673 let mut mirror = locator;
16674 mirror.locator_sequence = 1;
16675 mirror.cmra_length = cmra.len() as u32;
16676 mirror.cmra_parity_shard_count = parity_shard_count;
16677 out.extend_from_slice(&mirror.to_bytes());
16678 let final_locator = CriticalRecoveryLocator {
16679 volume_format_rev: locator.volume_format_rev,
16680 locator_sequence: 0,
16681 ..mirror
16682 };
16683 out.extend_from_slice(&final_locator.to_bytes());
16684 out
16685 }
16686
16687 fn rewrite_public_cmra_image(
16688 volume: &mut [u8],
16689 mutate: impl FnOnce(&mut CriticalMetadataImage),
16690 ) {
16691 rewrite_cmra_image(volume, CmraRecoveryMode::PublicNoKey, mutate);
16692 }
16693
16694 fn rewrite_cmra_image(
16695 volume: &mut [u8],
16696 mode: CmraRecoveryMode,
16697 mutate: impl FnOnce(&mut CriticalMetadataImage),
16698 ) {
16699 let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
16700 let locator = final_recovery_locator(volume);
16701 let tuple = CmraDecoderTuple::from(locator);
16702 let recovered = recover_cmra(volume, locator.cmra_offset, Some(tuple), mode).unwrap();
16703 let mut image = recovered.image;
16704 mutate(&mut image);
16705 refresh_critical_image_region_digests(&mut image);
16706 let image_bytes = image.to_bytes().unwrap();
16707 assert_eq!(image_bytes.len(), tuple.image_length as usize);
16708
16709 let shard_size = tuple.shard_size as usize;
16710 let data_shard_count = tuple.data_shard_count as usize;
16711 let parity_shard_count = tuple.parity_shard_count as usize;
16712 assert!(image_bytes.len() <= data_shard_count * shard_size);
16713
16714 let mut data_shards = Vec::with_capacity(data_shard_count);
16715 for idx in 0..data_shard_count {
16716 let start = idx * shard_size;
16717 let end = (start + shard_size).min(image_bytes.len());
16718 let mut payload = vec![0u8; shard_size];
16719 if start < image_bytes.len() {
16720 payload[..end - start].copy_from_slice(&image_bytes[start..end]);
16721 }
16722 data_shards.push(payload);
16723 }
16724 let parity_shards = encode_parity_gf16(&data_shards, parity_shard_count).unwrap();
16725 let image_sha256 = sha256_bytes(&image_bytes);
16726
16727 let header = CriticalMetadataRecoveryHeader {
16728 shard_size: tuple.shard_size,
16729 data_shard_count: tuple.data_shard_count,
16730 parity_shard_count: tuple.parity_shard_count,
16731 image_length: tuple.image_length,
16732 archive_uuid_hint: locator.archive_uuid_hint,
16733 session_id_hint: locator.session_id_hint,
16734 volume_index_hint: locator.volume_index_hint,
16735 image_sha256,
16736 header_crc32c: 0,
16737 };
16738 let mut cmra = Vec::new();
16739 cmra.extend_from_slice(&header.to_bytes());
16740 for (idx, payload) in data_shards.into_iter().enumerate() {
16741 let payload_len = if idx + 1 == data_shard_count {
16742 image_bytes.len() - idx * shard_size
16743 } else {
16744 shard_size
16745 };
16746 cmra.extend_from_slice(
16747 &CriticalMetadataRecoveryShard {
16748 shard_index: idx as u16,
16749 shard_role: 0,
16750 shard_payload_length: payload_len as u32,
16751 payload,
16752 shard_crc32c: 0,
16753 }
16754 .to_bytes(shard_size)
16755 .unwrap(),
16756 );
16757 }
16758 for (idx, payload) in parity_shards.into_iter().enumerate() {
16759 cmra.extend_from_slice(
16760 &CriticalMetadataRecoveryShard {
16761 shard_index: (data_shard_count + idx) as u16,
16762 shard_role: 1,
16763 shard_payload_length: shard_size as u32,
16764 payload,
16765 shard_crc32c: 0,
16766 }
16767 .to_bytes(shard_size)
16768 .unwrap(),
16769 );
16770 }
16771 assert_eq!(cmra.len() as u64, recovered.cmra_length);
16772 let cmra_offset = locator.cmra_offset as usize;
16773 volume[cmra_offset..cmra_offset + cmra.len()].copy_from_slice(&cmra);
16774
16775 rewrite_locator_image_sha(volume, final_offset, image_sha256);
16776 let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
16777 rewrite_locator_image_sha(volume, mirror_offset, image_sha256);
16778 }
16779
16780 fn rewrite_cmra_image_variable_len(
16781 volume: &[u8],
16782 mode: CmraRecoveryMode,
16783 mutate: impl FnOnce(&mut CriticalMetadataImage),
16784 ) -> Vec<u8> {
16785 let locator = final_recovery_locator(volume);
16786 let tuple = CmraDecoderTuple::from(locator);
16787 let recovered = recover_cmra(volume, locator.cmra_offset, Some(tuple), mode).unwrap();
16788 let mut image = recovered.image;
16789 mutate(&mut image);
16790 refresh_critical_image_region_digests(&mut image);
16791 let image_bytes = image.to_bytes().unwrap();
16792
16793 let shard_size = tuple.shard_size as usize;
16794 let data_shard_count = image_bytes.len().div_ceil(shard_size);
16795 let parity_shard_count = tuple.parity_shard_count as usize;
16796 assert!(data_shard_count > 0);
16797 assert!(image_bytes.len() <= data_shard_count * shard_size);
16798
16799 let mut data_shards = Vec::with_capacity(data_shard_count);
16800 for idx in 0..data_shard_count {
16801 let start = idx * shard_size;
16802 let end = (start + shard_size).min(image_bytes.len());
16803 let mut payload = vec![0u8; shard_size];
16804 if start < image_bytes.len() {
16805 payload[..end - start].copy_from_slice(&image_bytes[start..end]);
16806 }
16807 data_shards.push(payload);
16808 }
16809 let parity_shards = encode_parity_gf16(&data_shards, parity_shard_count).unwrap();
16810 let image_sha256 = sha256_bytes(&image_bytes);
16811 let data_shard_count_u16 = u16::try_from(data_shard_count).unwrap();
16812 let image_length_u32 = u32::try_from(image_bytes.len()).unwrap();
16813
16814 let header = CriticalMetadataRecoveryHeader {
16815 shard_size: tuple.shard_size,
16816 data_shard_count: data_shard_count_u16,
16817 parity_shard_count: tuple.parity_shard_count,
16818 image_length: image_length_u32,
16819 archive_uuid_hint: locator.archive_uuid_hint,
16820 session_id_hint: locator.session_id_hint,
16821 volume_index_hint: locator.volume_index_hint,
16822 image_sha256,
16823 header_crc32c: 0,
16824 };
16825 let mut cmra = Vec::new();
16826 cmra.extend_from_slice(&header.to_bytes());
16827 for (idx, payload) in data_shards.into_iter().enumerate() {
16828 let payload_len = if idx + 1 == data_shard_count {
16829 image_bytes.len() - idx * shard_size
16830 } else {
16831 shard_size
16832 };
16833 cmra.extend_from_slice(
16834 &CriticalMetadataRecoveryShard {
16835 shard_index: idx as u16,
16836 shard_role: 0,
16837 shard_payload_length: payload_len as u32,
16838 payload,
16839 shard_crc32c: 0,
16840 }
16841 .to_bytes(shard_size)
16842 .unwrap(),
16843 );
16844 }
16845 for (idx, payload) in parity_shards.into_iter().enumerate() {
16846 cmra.extend_from_slice(
16847 &CriticalMetadataRecoveryShard {
16848 shard_index: (data_shard_count + idx) as u16,
16849 shard_role: 1,
16850 shard_payload_length: shard_size as u32,
16851 payload,
16852 shard_crc32c: 0,
16853 }
16854 .to_bytes(shard_size)
16855 .unwrap(),
16856 );
16857 }
16858
16859 let locator_base = CriticalRecoveryLocator {
16860 volume_format_rev: image.volume_format_rev,
16861 cmra_offset: locator.cmra_offset,
16862 cmra_length: cmra.len() as u32,
16863 volume_trailer_offset: locator.volume_trailer_offset,
16864 body_bytes_before_cmra: locator.body_bytes_before_cmra,
16865 archive_uuid_hint: locator.archive_uuid_hint,
16866 session_id_hint: locator.session_id_hint,
16867 volume_index_hint: locator.volume_index_hint,
16868 locator_sequence: 1,
16869 cmra_shard_size: tuple.shard_size,
16870 cmra_data_shard_count: data_shard_count_u16,
16871 cmra_parity_shard_count: tuple.parity_shard_count,
16872 cmra_image_length: image_length_u32,
16873 cmra_image_sha256: image_sha256,
16874 locator_crc32c: 0,
16875 };
16876
16877 let cmra_offset = locator.cmra_offset as usize;
16878 let mut out = Vec::new();
16879 out.extend_from_slice(&volume[..cmra_offset]);
16880 out.extend_from_slice(&cmra);
16881 out.extend_from_slice(&locator_base.to_bytes());
16882 out.extend_from_slice(
16883 &CriticalRecoveryLocator {
16884 locator_sequence: 0,
16885 ..locator_base
16886 }
16887 .to_bytes(),
16888 );
16889 out
16890 }
16891
16892 fn rewrite_recovery_locator(
16893 volume: &mut [u8],
16894 offset: usize,
16895 mutate: impl FnOnce(&mut CriticalRecoveryLocator),
16896 ) {
16897 let mut locator =
16898 CriticalRecoveryLocator::parse(&volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN])
16899 .unwrap();
16900 mutate(&mut locator);
16901 volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN].copy_from_slice(&locator.to_bytes());
16902 }
16903
16904 fn refresh_critical_image_region_digests(image: &mut CriticalMetadataImage) {
16905 image.volume_header_sha256 = sha256_bytes(
16906 &image
16907 .regions
16908 .iter()
16909 .find(|region| region.region_type == 1)
16910 .unwrap()
16911 .bytes,
16912 );
16913 image.crypto_header_sha256 = sha256_bytes(
16914 &image
16915 .regions
16916 .iter()
16917 .find(|region| region.region_type == 2)
16918 .unwrap()
16919 .bytes,
16920 );
16921 image.key_wrap_table_sha256 = image
16922 .regions
16923 .iter()
16924 .find(|region| region.region_type == 6)
16925 .map(|region| sha256_bytes(®ion.bytes))
16926 .unwrap_or([0u8; 32]);
16927 image.manifest_footer_sha256 = sha256_bytes(
16928 &image
16929 .regions
16930 .iter()
16931 .find(|region| region.region_type == 3)
16932 .unwrap()
16933 .bytes,
16934 );
16935 image.root_auth_footer_sha256 = image
16936 .regions
16937 .iter()
16938 .find(|region| region.region_type == 4)
16939 .map(|region| sha256_bytes(®ion.bytes))
16940 .unwrap_or([0u8; 32]);
16941 image.volume_trailer_sha256 = sha256_bytes(
16942 &image
16943 .regions
16944 .iter()
16945 .find(|region| region.region_type == 5)
16946 .unwrap()
16947 .bytes,
16948 );
16949 }
16950
16951 fn rewrite_locator_image_sha(volume: &mut [u8], offset: usize, image_sha256: [u8; 32]) {
16952 let mut locator =
16953 CriticalRecoveryLocator::parse(&volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN])
16954 .unwrap();
16955 locator.cmra_image_sha256 = image_sha256;
16956 volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN].copy_from_slice(&locator.to_bytes());
16957 }
16958
16959 fn corrupt_v41_terminal_recovery(volume: &mut [u8]) {
16960 let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
16961 let final_locator = CriticalRecoveryLocator::parse(
16962 &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
16963 )
16964 .unwrap();
16965 let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
16966 volume[final_locator.cmra_offset as usize] ^= 0x55;
16967 volume[mirror_offset] ^= 0x55;
16968 volume[final_offset] ^= 0x55;
16969 }
16970
16971 fn mutate_first_block_record(volume: &mut [u8], mutate: impl FnOnce(&mut BlockRecord)) {
16972 let (record_offset, record_len) = first_block_record(volume);
16973 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
16974 let mut record = BlockRecord::parse(
16975 &volume[record_offset..record_offset + record_len],
16976 block_size,
16977 )
16978 .unwrap();
16979 mutate(&mut record);
16980 volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
16981 }
16982
16983 fn mutate_block_record_at_slot(
16984 volume: &mut [u8],
16985 slot: usize,
16986 mutate: impl FnOnce(&mut BlockRecord),
16987 ) {
16988 let (record_offset, record_len) = block_record_at_slot(volume, slot);
16989 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
16990 let mut record = BlockRecord::parse(
16991 &volume[record_offset..record_offset + record_len],
16992 block_size,
16993 )
16994 .unwrap();
16995 mutate(&mut record);
16996 volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
16997 }
16998
16999 fn first_block_record(volume: &[u8]) -> (usize, usize) {
17000 block_record_at_slot(volume, 0)
17001 }
17002
17003 fn block_record_at_slot(volume: &[u8], slot: usize) -> (usize, usize) {
17004 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17005 let crypto_start = volume_header.crypto_header_offset as usize;
17006 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
17007 let crypto_header = CryptoHeader::parse(
17008 &volume[crypto_start..crypto_end],
17009 volume_header.crypto_header_length,
17010 )
17011 .unwrap();
17012 let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
17013 let record_offset = crypto_end + slot * record_len;
17014 assert!(volume.len() >= record_offset + record_len);
17015 (record_offset, record_len)
17016 }
17017
17018 fn first_block_record_slot_with_kind(volume: &[u8], kind: BlockKind) -> Option<usize> {
17019 block_record_slots(volume)
17020 .into_iter()
17021 .enumerate()
17022 .find_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
17023 }
17024
17025 fn block_record_slots_with_kind(volume: &[u8], kind: BlockKind) -> Vec<usize> {
17026 block_record_slots(volume)
17027 .into_iter()
17028 .enumerate()
17029 .filter_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
17030 .collect()
17031 }
17032
17033 fn first_payload_data_run_slots(volume: &[u8]) -> Vec<usize> {
17034 let mut slots = Vec::new();
17035 for (slot, (_, _, record)) in block_record_slots(volume).into_iter().enumerate() {
17036 if record.kind == BlockKind::PayloadData {
17037 slots.push(slot);
17038 } else if !slots.is_empty() {
17039 break;
17040 }
17041 }
17042 slots
17043 }
17044
17045 fn envelope_indices_for_path(opened: &OpenedArchive, path: &str) -> BTreeSet<u64> {
17046 envelope_entries_for_path(opened, path)
17047 .into_iter()
17048 .map(|entry| entry.envelope_index)
17049 .collect()
17050 }
17051
17052 fn envelope_entries_for_path(opened: &OpenedArchive, path: &str) -> Vec<EnvelopeEntry> {
17053 let normalized =
17054 normalize_lookup_file_path(path, opened.crypto_header.max_path_length).unwrap();
17055 let located = opened.locate_index_file(&normalized).unwrap().unwrap();
17056 let file = &located.shard.files[located.file_index];
17057 frame_range_for_file(&located.shard, file)
17058 .unwrap()
17059 .into_iter()
17060 .map(|frame| {
17061 located
17062 .shard
17063 .envelopes
17064 .iter()
17065 .find(|entry| entry.envelope_index == frame.envelope_index)
17066 .unwrap()
17067 .clone()
17068 })
17069 .collect()
17070 }
17071
17072 fn block_record_slots(volume: &[u8]) -> Vec<(usize, usize, BlockRecord)> {
17073 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17074 let crypto_start = volume_header.crypto_header_offset as usize;
17075 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
17076 let crypto_header = CryptoHeader::parse(
17077 &volume[crypto_start..crypto_end],
17078 volume_header.crypto_header_length,
17079 )
17080 .unwrap();
17081 let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
17082 let manifest_offset = terminal_material_offset(volume);
17083 assert_eq!((manifest_offset - crypto_end) % record_len, 0);
17084 let record_count = (manifest_offset - crypto_end) / record_len;
17085 (0..record_count)
17086 .map(|slot| {
17087 let offset = crypto_end + slot * record_len;
17088 let record = BlockRecord::parse(
17089 &volume[offset..offset + record_len],
17090 record_len - BLOCK_RECORD_FRAMING_LEN,
17091 )
17092 .unwrap();
17093 (offset, record_len, record)
17094 })
17095 .collect()
17096 }
17097
17098 fn rewrite_manifest_footer(
17099 volume: &mut [u8],
17100 master_key: &MasterKey,
17101 mutate: impl FnOnce(&mut ManifestFooter),
17102 ) {
17103 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17104 let offset = terminal_material_offset(volume);
17105 let mut footer =
17106 ManifestFooter::parse(&volume[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
17107 mutate(&mut footer);
17108 footer.manifest_hmac = [0u8; 32];
17109 let mut footer_bytes = footer.to_bytes();
17110 let subkeys = Subkeys::derive(
17111 master_key,
17112 &volume_header.archive_uuid,
17113 &volume_header.session_id,
17114 )
17115 .unwrap();
17116 footer.manifest_hmac = compute_hmac(
17117 HmacDomain::ManifestFooter,
17118 &subkeys.mac_key,
17119 &volume_header.archive_uuid,
17120 &volume_header.session_id,
17121 &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
17122 );
17123 footer_bytes = footer.to_bytes();
17124 volume[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
17125 }
17126
17127 fn rewrite_volume_trailer(
17128 volume: &mut [u8],
17129 master_key: &MasterKey,
17130 mutate: impl FnOnce(&mut VolumeTrailer),
17131 ) {
17132 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17133 let offset = terminal_material_offset(volume) + MANIFEST_FOOTER_LEN;
17134 let mut trailer =
17135 VolumeTrailer::parse(&volume[offset..offset + VOLUME_TRAILER_LEN]).unwrap();
17136 mutate(&mut trailer);
17137 trailer.trailer_hmac = [0u8; 32];
17138 let mut trailer_bytes = trailer.to_bytes();
17139 let subkeys = Subkeys::derive(
17140 master_key,
17141 &volume_header.archive_uuid,
17142 &volume_header.session_id,
17143 )
17144 .unwrap();
17145 trailer.trailer_hmac = compute_hmac(
17146 HmacDomain::VolumeTrailer,
17147 &subkeys.mac_key,
17148 &volume_header.archive_uuid,
17149 &volume_header.session_id,
17150 &trailer_bytes[..TRAILER_HMAC_COVERED_LEN],
17151 );
17152 trailer_bytes = trailer.to_bytes();
17153 volume[offset..offset + VOLUME_TRAILER_LEN].copy_from_slice(&trailer_bytes);
17154 }
17155
17156 fn rewrite_sidecar_header(
17157 sidecar: &mut [u8],
17158 master_key: &MasterKey,
17159 mutate: impl FnOnce(&mut BootstrapSidecarHeader),
17160 ) {
17161 let mut header =
17162 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17163 mutate(&mut header);
17164 write_signed_sidecar_header(sidecar, master_key, &mut header);
17165 }
17166
17167 fn write_signed_sidecar_header(
17168 sidecar: &mut [u8],
17169 master_key: &MasterKey,
17170 header: &mut BootstrapSidecarHeader,
17171 ) {
17172 header.sidecar_hmac = [0u8; 32];
17173 let mut header_bytes = header.to_bytes();
17174 let subkeys =
17175 Subkeys::derive(master_key, &header.archive_uuid, &header.session_id).unwrap();
17176 header.sidecar_hmac = compute_hmac(
17177 HmacDomain::BootstrapSidecar,
17178 &subkeys.mac_key,
17179 &header.archive_uuid,
17180 &header.session_id,
17181 &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
17182 );
17183 header_bytes = header.to_bytes();
17184 sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
17185 }
17186
17187 fn sparse_bootstrap_sidecar(
17188 source: &[u8],
17189 master_key: &MasterKey,
17190 include_manifest: bool,
17191 include_index_root: bool,
17192 include_dictionary: bool,
17193 ) -> Vec<u8> {
17194 let source_header =
17195 BootstrapSidecarHeader::parse(&source[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17196 let mut sidecar = vec![0u8; BOOTSTRAP_SIDECAR_HEADER_LEN];
17197 let mut header = BootstrapSidecarHeader {
17198 archive_uuid: source_header.archive_uuid,
17199 session_id: source_header.session_id,
17200 flags: 0,
17201 manifest_footer_offset: 0,
17202 manifest_footer_length: 0,
17203 index_root_records_offset: 0,
17204 index_root_records_length: 0,
17205 dictionary_records_offset: 0,
17206 dictionary_records_length: 0,
17207 sidecar_hmac: [0u8; 32],
17208 header_crc32c: 0,
17209 };
17210
17211 if include_manifest {
17212 assert!(source_header.has_manifest_footer());
17213 let (offset, length) = append_sidecar_section(
17214 source,
17215 &mut sidecar,
17216 source_header.manifest_footer_offset,
17217 source_header.manifest_footer_length as u64,
17218 );
17219 header.flags |= 0x01;
17220 header.manifest_footer_offset = offset;
17221 header.manifest_footer_length = length as u32;
17222 }
17223 if include_index_root {
17224 assert!(source_header.has_index_root_records());
17225 let (offset, length) = append_sidecar_section(
17226 source,
17227 &mut sidecar,
17228 source_header.index_root_records_offset,
17229 source_header.index_root_records_length,
17230 );
17231 header.flags |= 0x02;
17232 header.index_root_records_offset = offset;
17233 header.index_root_records_length = length;
17234 }
17235 if include_dictionary {
17236 assert!(source_header.has_dictionary_records());
17237 let (offset, length) = append_sidecar_section(
17238 source,
17239 &mut sidecar,
17240 source_header.dictionary_records_offset,
17241 source_header.dictionary_records_length,
17242 );
17243 header.flags |= 0x04;
17244 header.dictionary_records_offset = offset;
17245 header.dictionary_records_length = length;
17246 }
17247
17248 write_signed_sidecar_header(&mut sidecar, master_key, &mut header);
17249 sidecar
17250 }
17251
17252 fn append_sidecar_section(
17253 source: &[u8],
17254 sidecar: &mut Vec<u8>,
17255 source_offset: u64,
17256 length: u64,
17257 ) -> (u64, u64) {
17258 let source_offset = source_offset as usize;
17259 let length = length as usize;
17260 let offset = sidecar.len() as u64;
17261 sidecar.extend_from_slice(&source[source_offset..source_offset + length]);
17262 (offset, length as u64)
17263 }
17264
17265 fn mutate_sidecar_manifest(
17266 sidecar: &mut [u8],
17267 master_key: &MasterKey,
17268 mutate: impl FnOnce(&mut ManifestFooter),
17269 ) {
17270 let header =
17271 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17272 let offset = header.manifest_footer_offset as usize;
17273 let mut footer =
17274 ManifestFooter::parse(&sidecar[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
17275 mutate(&mut footer);
17276 footer.manifest_hmac = [0u8; 32];
17277 let mut footer_bytes = footer.to_bytes();
17278 let subkeys =
17279 Subkeys::derive(master_key, &footer.archive_uuid, &footer.session_id).unwrap();
17280 footer.manifest_hmac = compute_hmac(
17281 HmacDomain::ManifestFooter,
17282 &subkeys.mac_key,
17283 &footer.archive_uuid,
17284 &footer.session_id,
17285 &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
17286 );
17287 footer_bytes = footer.to_bytes();
17288 sidecar[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
17289 }
17290
17291 fn mutate_sidecar_index_record(
17292 sidecar: &mut [u8],
17293 record_index: usize,
17294 mutate: impl FnOnce(&mut BlockRecord),
17295 ) {
17296 let header =
17297 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17298 let record_len = sidecar_record_len(sidecar);
17299 let offset = header.index_root_records_offset as usize + record_index * record_len;
17300 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
17301 let mut record =
17302 BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
17303 mutate(&mut record);
17304 sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
17305 }
17306
17307 fn mutate_sidecar_dictionary_record(
17308 sidecar: &mut [u8],
17309 record_index: usize,
17310 mutate: impl FnOnce(&mut BlockRecord),
17311 ) {
17312 let header =
17313 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17314 let record_len = sidecar_record_len(sidecar);
17315 let offset = header.dictionary_records_offset as usize + record_index * record_len;
17316 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
17317 let mut record =
17318 BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
17319 mutate(&mut record);
17320 sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
17321 }
17322
17323 fn swap_sidecar_index_records(sidecar: &mut [u8], left: usize, right: usize) {
17324 let header =
17325 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17326 let record_len = sidecar_record_len(sidecar);
17327 let left_offset = header.index_root_records_offset as usize + left * record_len;
17328 let right_offset = header.index_root_records_offset as usize + right * record_len;
17329 for idx in 0..record_len {
17330 sidecar.swap(left_offset + idx, right_offset + idx);
17331 }
17332 }
17333
17334 fn sidecar_record_len(sidecar: &[u8]) -> usize {
17335 let header =
17336 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17337 let footer_offset = header.manifest_footer_offset as usize;
17338 let footer =
17339 ManifestFooter::parse(&sidecar[footer_offset..footer_offset + MANIFEST_FOOTER_LEN])
17340 .unwrap();
17341 let index_record_count = footer.index_root_data_block_count as usize
17342 + footer.index_root_parity_block_count as usize;
17343 header.index_root_records_length as usize / index_record_count
17344 }
17345
17346 fn corrupt_object_extent_records(volume: &mut [u8], extent: ObjectExtent) {
17347 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17348 assert_eq!(volume_header.volume_index, 0);
17349 assert_eq!(volume_header.stripe_width, 1);
17350 let crypto_start = volume_header.crypto_header_offset as usize;
17351 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
17352 let crypto_header = CryptoHeader::parse(
17353 &volume[crypto_start..crypto_end],
17354 volume_header.crypto_header_length,
17355 )
17356 .unwrap();
17357 let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
17358 let record_count = extent.data_block_count as u64 + extent.parity_block_count as u64;
17359 for offset in 0..record_count {
17360 let block_index = extent.first_block_index + offset;
17361 let record_offset = crypto_end + block_index as usize * record_len;
17362 volume[record_offset + 16] ^= 0x55;
17363 }
17364 }
17365
17366 fn terminal_material_offset(volume: &[u8]) -> usize {
17367 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17368 let crypto_start = volume_header.crypto_header_offset as usize;
17369 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
17370 let crypto_header = CryptoHeader::parse(
17371 &volume[crypto_start..crypto_end],
17372 volume_header.crypto_header_length,
17373 )
17374 .unwrap();
17375 let (_, offset, _) = parse_stream_block_prefix(
17376 volume,
17377 crypto_end,
17378 crypto_header.fixed.block_size as usize,
17379 &volume_header,
17380 )
17381 .unwrap();
17382 offset
17383 }
17384
17385 #[derive(Debug)]
17386 struct TestObject {
17387 extent: ObjectExtent,
17388 records: Vec<BlockRecord>,
17389 }
17390
17391 #[derive(Debug)]
17392 struct TestFileMeta {
17393 path: Vec<u8>,
17394 frame_index: u64,
17395 tar_stream_offset: u64,
17396 member_group_size: u64,
17397 file_data_size: u64,
17398 }
17399
17400 fn multi_envelope_reader_fixture() -> (OpenedArchive, u64) {
17401 let volume_header = test_volume_header();
17402 let crypto_header = test_crypto_header();
17403 let subkeys = Subkeys::derive(
17404 &master_key(),
17405 &volume_header.archive_uuid,
17406 &volume_header.session_id,
17407 )
17408 .unwrap();
17409 let mut next_block_index = 0u64;
17410 let mut blocks = BTreeMap::new();
17411
17412 let healthy = test_member(b"healthy.txt", b"healthy payload\n");
17413 let broken = test_member(b"broken.txt", b"broken payload\n");
17414 let tar_stream = [healthy.as_slice(), broken.as_slice()].concat();
17415
17416 let healthy_frame = compress_zstd_frame(&healthy, 1).unwrap();
17417 let broken_frame = compress_zstd_frame(&broken, 1).unwrap();
17418
17419 let healthy_payload = encrypt_test_object(
17420 &healthy_frame,
17421 &subkeys.enc_key,
17422 &subkeys.nonce_seed,
17423 b"envelope",
17424 0,
17425 BlockKind::PayloadData,
17426 &mut next_block_index,
17427 &crypto_header,
17428 &volume_header,
17429 );
17430 let broken_payload = encrypt_test_object(
17431 &broken_frame,
17432 &subkeys.enc_key,
17433 &subkeys.nonce_seed,
17434 b"envelope",
17435 1,
17436 BlockKind::PayloadData,
17437 &mut next_block_index,
17438 &crypto_header,
17439 &volume_header,
17440 );
17441 let broken_payload_block = broken_payload.extent.first_block_index;
17442 insert_records(&mut blocks, &healthy_payload.records);
17443 insert_records(&mut blocks, &broken_payload.records);
17444
17445 let frames = vec![
17446 FrameEntry {
17447 frame_index: 0,
17448 envelope_index: 0,
17449 offset_in_envelope: 0,
17450 compressed_size: healthy_frame.len() as u32,
17451 decompressed_size: healthy.len() as u32,
17452 flags: 0x0000_0003,
17453 tar_stream_offset: 0,
17454 },
17455 FrameEntry {
17456 frame_index: 1,
17457 envelope_index: 1,
17458 offset_in_envelope: 0,
17459 compressed_size: broken_frame.len() as u32,
17460 decompressed_size: broken.len() as u32,
17461 flags: 0x0000_0003,
17462 tar_stream_offset: healthy.len() as u64,
17463 },
17464 ];
17465 let envelopes = vec![
17466 EnvelopeEntry {
17467 envelope_index: 0,
17468 first_block_index: healthy_payload.extent.first_block_index,
17469 data_block_count: healthy_payload.extent.data_block_count,
17470 parity_block_count: 0,
17471 encrypted_size: healthy_payload.extent.encrypted_size,
17472 plaintext_size: healthy_frame.len() as u32,
17473 first_frame_index: 0,
17474 frame_count: 1,
17475 },
17476 EnvelopeEntry {
17477 envelope_index: 1,
17478 first_block_index: broken_payload.extent.first_block_index,
17479 data_block_count: broken_payload.extent.data_block_count,
17480 parity_block_count: 0,
17481 encrypted_size: broken_payload.extent.encrypted_size,
17482 plaintext_size: broken_frame.len() as u32,
17483 first_frame_index: 1,
17484 frame_count: 1,
17485 },
17486 ];
17487 let files = vec![
17488 TestFileMeta {
17489 path: b"healthy.txt".to_vec(),
17490 frame_index: 0,
17491 tar_stream_offset: 0,
17492 member_group_size: healthy.len() as u64,
17493 file_data_size: b"healthy payload\n".len() as u64,
17494 },
17495 TestFileMeta {
17496 path: b"broken.txt".to_vec(),
17497 frame_index: 1,
17498 tar_stream_offset: healthy.len() as u64,
17499 member_group_size: broken.len() as u64,
17500 file_data_size: b"broken payload\n".len() as u64,
17501 },
17502 ];
17503
17504 let (index_shard_plaintext, first_path_hash, last_path_hash) =
17505 build_test_index_shard(&files, &frames, &envelopes);
17506 let index_shard = encrypt_test_object(
17507 &compress_zstd_frame(&index_shard_plaintext, 1).unwrap(),
17508 &subkeys.index_shard_key,
17509 &subkeys.index_nonce_seed,
17510 b"idxshard",
17511 0,
17512 BlockKind::IndexShardData,
17513 &mut next_block_index,
17514 &crypto_header,
17515 &volume_header,
17516 );
17517 insert_records(&mut blocks, &index_shard.records);
17518
17519 let shard_entry = ShardEntry {
17520 shard_index: 0,
17521 first_block_index: index_shard.extent.first_block_index,
17522 data_block_count: index_shard.extent.data_block_count,
17523 parity_block_count: 0,
17524 encrypted_size: index_shard.extent.encrypted_size,
17525 decompressed_size: index_shard_plaintext.len() as u32,
17526 file_count: files.len() as u32,
17527 first_path_hash,
17528 last_path_hash,
17529 };
17530 let mut root_header = IndexRootHeader::empty();
17531 root_header.frame_count = frames.len() as u64;
17532 root_header.envelope_count = envelopes.len() as u64;
17533 root_header.file_count = files.len() as u64;
17534 root_header.payload_block_count = healthy_payload.extent.data_block_count as u64
17535 + broken_payload.extent.data_block_count as u64;
17536 root_header.tar_total_size = tar_stream.len() as u64;
17537 root_header.content_sha256 = sha256_bytes(&tar_stream);
17538 let index_root = IndexRoot {
17539 header: root_header,
17540 shards: vec![shard_entry],
17541 directory_hint_shards: Vec::new(),
17542 };
17543
17544 let index_root_plaintext = index_root.to_bytes();
17545 let index_root_object = encrypt_test_object(
17546 &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
17547 &subkeys.index_root_key,
17548 &subkeys.index_nonce_seed,
17549 b"idxroot",
17550 0,
17551 BlockKind::IndexRootData,
17552 &mut next_block_index,
17553 &crypto_header,
17554 &volume_header,
17555 );
17556 insert_records(&mut blocks, &index_root_object.records);
17557
17558 let archive_uuid = volume_header.archive_uuid;
17559 let session_id = volume_header.session_id;
17560 let opened = OpenedArchive {
17561 options: ReaderOptions::default(),
17562 observed_archive_bytes: 1_000_000,
17563 observed_volume_count: 1,
17564 subkeys,
17565 blocks,
17566 lazy_blocks: None,
17567 crypto_header_bytes: Vec::new(),
17568 volume_header,
17569 crypto_header,
17570 manifest_footer: ManifestFooter {
17571 archive_uuid,
17572 session_id,
17573 volume_index: 0,
17574 is_authoritative: 1,
17575 total_volumes: 1,
17576 index_root_first_block: index_root_object.extent.first_block_index,
17577 index_root_data_block_count: index_root_object.extent.data_block_count,
17578 index_root_parity_block_count: 0,
17579 index_root_encrypted_size: index_root_object.extent.encrypted_size,
17580 index_root_decompressed_size: index_root_plaintext.len() as u32,
17581 manifest_hmac: [0u8; 32],
17582 },
17583 volume_trailer: Some(VolumeTrailer {
17584 archive_uuid,
17585 session_id,
17586 volume_index: 0,
17587 block_count: next_block_index,
17588 bytes_written: 0,
17589 manifest_footer_offset: 0,
17590 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
17591 closed_at_ns: 0,
17592 root_auth_footer_offset: 0,
17593 root_auth_footer_length: 0,
17594 root_auth_flags: 0,
17595 trailer_hmac: [0u8; 32],
17596 }),
17597 root_auth_footer: None,
17598 index_root,
17599 payload_dictionary: None,
17600 };
17601 (opened, broken_payload_block)
17602 }
17603
17604 fn replace_first_index_shard(opened: &mut OpenedArchive, mutate: impl FnOnce(&mut IndexShard)) {
17605 let locating = opened.index_root.shards[0].clone();
17606 let mut shard = opened.load_index_shard(&locating).unwrap();
17607 mutate(&mut shard);
17608 let plaintext = shard.to_bytes();
17609 let mut next_block_index = opened
17610 .blocks
17611 .keys()
17612 .last()
17613 .copied()
17614 .map(|index| index + 1)
17615 .unwrap_or(0);
17616 let replacement = encrypt_test_object(
17617 &compress_zstd_frame(&plaintext, 1).unwrap(),
17618 &opened.subkeys.index_shard_key,
17619 &opened.subkeys.index_nonce_seed,
17620 b"idxshard",
17621 locating.shard_index,
17622 BlockKind::IndexShardData,
17623 &mut next_block_index,
17624 &opened.crypto_header,
17625 &opened.volume_header,
17626 );
17627 insert_records(&mut opened.blocks, &replacement.records);
17628 opened.index_root.shards[0] = ShardEntry {
17629 shard_index: locating.shard_index,
17630 first_block_index: replacement.extent.first_block_index,
17631 data_block_count: replacement.extent.data_block_count,
17632 parity_block_count: 0,
17633 encrypted_size: replacement.extent.encrypted_size,
17634 decompressed_size: plaintext.len() as u32,
17635 file_count: shard.files.len() as u32,
17636 first_path_hash: shard.files.first().unwrap().path_hash,
17637 last_path_hash: shard.files.last().unwrap().path_hash,
17638 };
17639 }
17640
17641 fn rewrite_as_single_healthy_file(
17642 opened: &mut OpenedArchive,
17643 mutate: impl FnOnce(&mut FileEntry, &mut Vec<u8>),
17644 ) {
17645 let healthy_path = b"healthy.txt";
17646 let healthy_payload = b"healthy payload\n";
17647 let healthy_member = test_member(healthy_path, healthy_payload);
17648 replace_first_index_shard(opened, |shard| {
17649 let file_index = (0..shard.files.len())
17650 .find(|idx| shard.file_path(*idx) == Some(healthy_path.as_slice()))
17651 .unwrap();
17652 let mut file = shard.files[file_index].clone();
17653 let frame = shard
17654 .frames
17655 .iter()
17656 .find(|entry| entry.frame_index == 0)
17657 .unwrap()
17658 .clone();
17659 let envelope = shard
17660 .envelopes
17661 .iter()
17662 .find(|entry| entry.envelope_index == 0)
17663 .unwrap()
17664 .clone();
17665 let mut path = healthy_path.to_vec();
17666
17667 file.path_offset = 0;
17668 file.path_length = path.len() as u32;
17669 file.first_frame_index = 0;
17670 file.frame_count = 1;
17671 file.offset_in_first_frame_plaintext = 0;
17672 file.tar_member_group_size = healthy_member.len() as u64;
17673 file.file_data_size = healthy_payload.len() as u64;
17674 file.flags = 0;
17675 mutate(&mut file, &mut path);
17676 file.path_offset = 0;
17677 file.path_length = path.len() as u32;
17678 file.path_hash = hash_prefix(&path);
17679
17680 shard.files = vec![file];
17681 shard.frames = vec![frame];
17682 shard.envelopes = vec![envelope];
17683 shard.string_pool = path;
17684 });
17685
17686 opened.index_root.header.file_count = 1;
17687 opened.index_root.header.frame_count = 1;
17688 opened.index_root.header.envelope_count = 1;
17689 opened.index_root.header.payload_block_count = 1;
17690 opened.index_root.header.tar_total_size = healthy_member.len() as u64;
17691 opened.index_root.header.content_sha256 = sha256_bytes(&healthy_member);
17692 }
17693
17694 fn test_volume_header() -> VolumeHeader {
17695 VolumeHeader {
17696 format_version: FORMAT_VERSION,
17697 volume_format_rev: VOLUME_FORMAT_REV,
17698 volume_index: 0,
17699 stripe_width: 1,
17700 archive_uuid: [0x31; 16],
17701 session_id: [0x42; 16],
17702 crypto_header_offset: VOLUME_HEADER_LEN as u32,
17703 crypto_header_length: CRYPTO_HEADER_FIXED_LEN as u32,
17704 header_crc32c: 0,
17705 }
17706 }
17707
17708 fn test_crypto_header() -> CryptoHeaderFixed {
17709 CryptoHeaderFixed {
17710 length: CRYPTO_HEADER_FIXED_LEN as u32,
17711 compression_algo: CompressionAlgo::ZstdFramed,
17712 aead_algo: AeadAlgo::AesGcmSiv256,
17713 fec_algo: FecAlgo::ReedSolomonGF16,
17714 kdf_algo: KdfAlgo::Raw,
17715 chunk_size: 4096,
17716 envelope_target_size: 8192,
17717 block_size: 4096,
17718 fec_data_shards: 4,
17719 fec_parity_shards: 0,
17720 index_fec_data_shards: 4,
17721 index_fec_parity_shards: 0,
17722 index_root_fec_data_shards: 4,
17723 index_root_fec_parity_shards: 0,
17724 stripe_width: 1,
17725 volume_loss_tolerance: 0,
17726 bit_rot_buffer_pct: 0,
17727 has_dictionary: 0,
17728 max_path_length: 4096,
17729 expected_volume_size: 0,
17730 }
17731 }
17732
17733 #[allow(clippy::too_many_arguments)]
17734 fn encrypt_test_object(
17735 plaintext: &[u8],
17736 key: &[u8; 32],
17737 nonce_seed: &[u8; 32],
17738 domain: &[u8],
17739 counter: u64,
17740 data_kind: BlockKind,
17741 next_block_index: &mut u64,
17742 crypto_header: &CryptoHeaderFixed,
17743 volume_header: &VolumeHeader,
17744 ) -> TestObject {
17745 let block_size = crypto_header.block_size as usize;
17746 let encrypted = encrypt_padded_aead_object(
17747 AeadObjectContext {
17748 algo: crypto_header.aead_algo,
17749 key,
17750 nonce_seed,
17751 domain,
17752 archive_uuid: &volume_header.archive_uuid,
17753 session_id: &volume_header.session_id,
17754 counter,
17755 },
17756 block_size,
17757 plaintext,
17758 )
17759 .unwrap();
17760 assert_eq!(encrypted.len() % block_size, 0);
17761
17762 let first_block_index = *next_block_index;
17763 let data_block_count = encrypted.len() / block_size;
17764 let records = encrypted
17765 .chunks(block_size)
17766 .enumerate()
17767 .map(|(index, payload)| BlockRecord {
17768 block_index: first_block_index + index as u64,
17769 kind: data_kind,
17770 flags: if index + 1 == data_block_count {
17771 0x01
17772 } else {
17773 0
17774 },
17775 payload: payload.to_vec(),
17776 record_crc32c: 0,
17777 })
17778 .collect::<Vec<_>>();
17779 *next_block_index += data_block_count as u64;
17780
17781 TestObject {
17782 extent: ObjectExtent {
17783 first_block_index,
17784 data_block_count: data_block_count as u32,
17785 parity_block_count: 0,
17786 encrypted_size: encrypted.len() as u32,
17787 },
17788 records,
17789 }
17790 }
17791
17792 fn insert_records(blocks: &mut BTreeMap<u64, BlockRecord>, records: &[BlockRecord]) {
17793 for record in records {
17794 assert!(blocks.insert(record.block_index, record.clone()).is_none());
17795 }
17796 }
17797
17798 #[allow(clippy::too_many_arguments)]
17799 fn build_metadata_object_from_payload(
17800 payload: &[u8],
17801 _subkeys: &Subkeys,
17802 volume_header: &VolumeHeader,
17803 crypto_header: &CryptoHeaderFixed,
17804 key: &[u8; 32],
17805 nonce_seed: &[u8; 32],
17806 domain: &[u8],
17807 counter: u64,
17808 data_kind: BlockKind,
17809 next_block_index: &mut u64,
17810 ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
17811 let compressed = compress_zstd_frame(payload, 1).unwrap();
17812 build_metadata_object_from_compressed(
17813 &compressed,
17814 key,
17815 nonce_seed,
17816 domain,
17817 counter,
17818 data_kind,
17819 next_block_index,
17820 crypto_header,
17821 volume_header,
17822 )
17823 }
17824
17825 #[allow(clippy::too_many_arguments)]
17826 fn build_metadata_object_from_compressed(
17827 compressed: &[u8],
17828 key: &[u8; 32],
17829 nonce_seed: &[u8; 32],
17830 domain: &[u8],
17831 counter: u64,
17832 data_kind: BlockKind,
17833 next_block_index: &mut u64,
17834 crypto_header: &CryptoHeaderFixed,
17835 volume_header: &VolumeHeader,
17836 ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
17837 let object = encrypt_test_object(
17838 compressed,
17839 key,
17840 nonce_seed,
17841 domain,
17842 counter,
17843 data_kind,
17844 next_block_index,
17845 crypto_header,
17846 volume_header,
17847 );
17848
17849 let mut blocks = BTreeMap::new();
17850 for record in object.records {
17851 blocks.insert(record.block_index, record);
17852 }
17853 (object.extent, blocks)
17854 }
17855
17856 #[allow(clippy::too_many_arguments)]
17857 fn assert_metadata_object_from_compressed(
17858 compressed: &[u8],
17859 decompressed_size: usize,
17860 _subkeys: &Subkeys,
17861 volume_header: &VolumeHeader,
17862 crypto_header: &CryptoHeaderFixed,
17863 key: &[u8; 32],
17864 nonce_seed: &[u8; 32],
17865 domain: &[u8],
17866 counter: u64,
17867 data_kind: BlockKind,
17868 parity_kind: BlockKind,
17869 class_data_shards: u16,
17870 class_parity_shards: u16,
17871 next_block_index: &mut u64,
17872 expected: FormatError,
17873 ) {
17874 let (extent, blocks) = build_metadata_object_from_compressed(
17875 compressed,
17876 key,
17877 nonce_seed,
17878 domain,
17879 counter,
17880 data_kind,
17881 next_block_index,
17882 crypto_header,
17883 volume_header,
17884 );
17885 let error = load_metadata_object_from_parts(
17886 &blocks,
17887 ObjectLoadContext {
17888 volume_header,
17889 crypto_header,
17890 extent,
17891 data_kind,
17892 parity_kind,
17893 key,
17894 nonce_seed,
17895 domain,
17896 counter,
17897 class_data_shard_max: class_data_shards,
17898 class_parity_shard_max: class_parity_shards,
17899 },
17900 decompressed_size as u32,
17901 )
17902 .unwrap_err();
17903 assert_eq!(error, expected);
17904 }
17905
17906 fn corrupt_payload_record(blocks: &mut BTreeMap<u64, BlockRecord>, block_index: u64) {
17907 let record = blocks.get_mut(&block_index).unwrap();
17908 assert_eq!(record.kind, BlockKind::PayloadData);
17909 record.payload[0] ^= 0x55;
17910 }
17911
17912 fn build_test_index_shard(
17913 files: &[TestFileMeta],
17914 frames: &[FrameEntry],
17915 envelopes: &[EnvelopeEntry],
17916 ) -> (Vec<u8>, [u8; 8], [u8; 8]) {
17917 let mut sorted = files
17918 .iter()
17919 .map(|file| (hash_prefix(&file.path), file))
17920 .collect::<Vec<_>>();
17921 sorted.sort_by(|left, right| {
17922 (left.0, left.1.path.as_slice(), left.1.tar_stream_offset).cmp(&(
17923 right.0,
17924 right.1.path.as_slice(),
17925 right.1.tar_stream_offset,
17926 ))
17927 });
17928
17929 let mut string_pool = Vec::new();
17930 let mut file_entries = Vec::with_capacity(sorted.len());
17931 for (path_hash, file) in &sorted {
17932 let path_offset = string_pool.len() as u32;
17933 string_pool.extend_from_slice(&file.path);
17934 file_entries.push(FileEntry {
17935 path_hash: *path_hash,
17936 path_offset,
17937 path_length: file.path.len() as u32,
17938 first_frame_index: file.frame_index,
17939 frame_count: 1,
17940 offset_in_first_frame_plaintext: 0,
17941 tar_member_group_size: file.member_group_size,
17942 file_data_size: file.file_data_size,
17943 flags: 0,
17944 });
17945 }
17946
17947 let header = IndexShardHeader {
17948 version: 1,
17949 shard_index: 0,
17950 file_count: file_entries.len() as u32,
17951 frame_count: frames.len() as u32,
17952 envelope_count: envelopes.len() as u32,
17953 file_table_offset: INDEX_SHARD_HEADER_LEN as u32,
17954 frame_table_offset: (INDEX_SHARD_HEADER_LEN + file_entries.len() * FILE_ENTRY_LEN)
17955 as u32,
17956 envelope_table_offset: (INDEX_SHARD_HEADER_LEN
17957 + file_entries.len() * FILE_ENTRY_LEN
17958 + frames.len() * FRAME_ENTRY_LEN) as u32,
17959 string_pool_offset: (INDEX_SHARD_HEADER_LEN
17960 + file_entries.len() * FILE_ENTRY_LEN
17961 + frames.len() * FRAME_ENTRY_LEN
17962 + envelopes.len() * ENVELOPE_ENTRY_LEN) as u32,
17963 string_pool_size: string_pool.len() as u32,
17964 };
17965
17966 let mut bytes = Vec::new();
17967 bytes.extend_from_slice(&header.to_bytes());
17968 for entry in &file_entries {
17969 bytes.extend_from_slice(&entry.to_bytes());
17970 }
17971 for entry in frames {
17972 bytes.extend_from_slice(&entry.to_bytes());
17973 }
17974 for entry in envelopes {
17975 bytes.extend_from_slice(&entry.to_bytes());
17976 }
17977 bytes.extend_from_slice(&string_pool);
17978
17979 (bytes, sorted.first().unwrap().0, sorted.last().unwrap().0)
17980 }
17981
17982 fn test_member(path: &[u8], data: &[u8]) -> Vec<u8> {
17983 let mut out = Vec::new();
17984 out.extend_from_slice(&test_tar_header(path, data.len() as u64));
17985 out.extend_from_slice(data);
17986 out.resize(out.len() + padding_to_512(data.len()), 0);
17987 out
17988 }
17989
17990 fn test_tar_header(path: &[u8], size: u64) -> [u8; 512] {
17991 let mut header = [0u8; 512];
17992 header[..path.len()].copy_from_slice(path);
17993 write_test_tar_octal(&mut header[100..108], 0o644);
17994 write_test_tar_octal(&mut header[108..116], 0);
17995 write_test_tar_octal(&mut header[116..124], 0);
17996 write_test_tar_octal(&mut header[124..136], size);
17997 write_test_tar_octal(&mut header[136..148], 0);
17998 header[148..156].fill(b' ');
17999 header[156] = b'0';
18000 header[257..263].copy_from_slice(b"ustar\0");
18001 header[263..265].copy_from_slice(b"00");
18002 let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
18003 write_test_tar_checksum(&mut header[148..156], checksum);
18004 header
18005 }
18006
18007 fn write_test_tar_octal(field: &mut [u8], value: u64) {
18008 let digits = format!("{value:o}");
18009 field.fill(0);
18010 let start = field.len() - 1 - digits.len();
18011 field[..start].fill(b'0');
18012 field[start..start + digits.len()].copy_from_slice(digits.as_bytes());
18013 }
18014
18015 fn write_test_tar_checksum(field: &mut [u8], value: u64) {
18016 let digits = format!("{value:06o}");
18017 field[0..6].copy_from_slice(digits.as_bytes());
18018 field[6] = 0;
18019 field[7] = b' ';
18020 }
18021
18022 fn padding_to_512(len: usize) -> usize {
18023 let remainder = len % 512;
18024 if remainder == 0 {
18025 0
18026 } else {
18027 512 - remainder
18028 }
18029 }
18030}