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::entry_metadata::ArchiveTimestamp;
15use crate::fec::{encode_parity_gf16, repair_data_gf16};
16use crate::format::{
17 AeadAlgo, BlockKind, ExtractError, FormatError, KdfAlgo, VolumeFormatRevision,
18 BLOCK_RECORD_FRAMING_LEN, BOOTSTRAP_SIDECAR_HEADER_LEN, CRITICAL_METADATA_IMAGE_FIXED_LEN,
19 CRITICAL_METADATA_RECOVERY_HEADER_LEN, CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN,
20 CRITICAL_RECOVERY_LOCATOR_LEN, CRYPTO_HEADER_HMAC_LEN, IMAGE_CRC_LEN, LOCATOR_PAIR_LEN,
21 MANIFEST_FOOTER_LEN, MASTER_KEY_LEN, READER_MAX_CMRA_PARITY_PCT, READER_MAX_CRYPTO_HEADER_LEN,
22 READER_MAX_KEY_WRAP_TABLE_LEN, READER_MAX_ROOT_AUTH_FOOTER_LEN, SERIALIZED_REGION_HEADER_LEN,
23 VOLUME_FORMAT_REV_45, 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 metadata_verification_report, parse_tar_member_group, plan_owned_member_restore, restore_phase,
41 restore_regular_file_metadata_to_open_file, restore_streaming_tar_member_group,
42 stream_regular_tar_member_group_to_writer, validate_owned_restore_plan,
43 validate_tar_stream_total_extraction_size, MetadataDiagnostic, MetadataVerificationReport,
44 NoopTarStreamObserver, OwnedTarMember, SafeExtractionOptions, StreamingMemberExpectation,
45 TarEntryKind, TarMemberGroupReader, TarStreamFilesystemRestoreObserver, TarStreamObserver,
46 TarStreamSummaryValidator, TarStreamTotalExtractionSizeValidator,
47};
48use crate::wire::{
49 compute_key_wrap_table_digest, BlockRecord, BootstrapSidecarHeader, CriticalMetadataImage,
50 CriticalMetadataRecoveryHeader, CriticalMetadataRecoveryShard, CriticalRecoveryLocator,
51 CryptoHeader, CryptoHeaderFixed, ExtensionTlv, KeyWrapTableV1, ManifestFooter,
52 RootAuthFooterV1, VolumeHeader, VolumeTrailer,
53};
54
55const TRAILER_HMAC_COVERED_LEN: usize = 96;
56const MANIFEST_HMAC_COVERED_LEN: usize = 104;
57const SIDECAR_HMAC_COVERED_LEN: usize = 92;
58const DEFAULT_MAX_VERIFY_TAR_SIZE: usize = 128 * 1024 * 1024;
59const DEFAULT_MAX_TRAILING_GARBAGE_SCAN: usize = 1024 * 1024;
60const DEFAULT_MAX_TOTAL_EXTRACTION_SIZE: u64 = 100 * 1024 * 1024 * 1024;
61const DIRECTORY_HINT_REQUIRED_FILE_COUNT: u64 = 100_000;
62
63fn default_jobs() -> usize {
64 std::thread::available_parallelism()
65 .map(|jobs| jobs.get())
66 .unwrap_or(1)
67}
68
69pub trait ArchiveReadAt: Send + Sync + 'static {
70 fn len(&self) -> Result<u64, FormatError>;
71 fn is_empty(&self) -> Result<bool, FormatError> {
72 Ok(self.len()? == 0)
73 }
74 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError>;
75}
76
77pub type RecipientWrapCandidateMasterKey = [u8; MASTER_KEY_LEN];
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub struct RecipientWrapArchiveIdentity {
81 pub archive_uuid: [u8; 16],
82 pub session_id: [u8; 16],
83 pub format_version: u16,
84 pub volume_format_rev: u16,
85}
86
87#[derive(Debug, Clone, Copy)]
88pub struct RecipientWrapRecordContext<'a> {
89 pub archive_identity: RecipientWrapArchiveIdentity,
90 pub record: &'a crate::wire::RecipientRecordV1,
91}
92
93impl ArchiveReadAt for File {
94 fn len(&self) -> Result<u64, FormatError> {
95 self.metadata()
96 .map(|metadata| metadata.len())
97 .map_err(|_| FormatError::InvalidArchive("archive read metadata failed"))
98 }
99
100 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
101 file_read_exact_at(self, offset, buf)
102 }
103}
104
105#[cfg(unix)]
106fn file_read_exact_at(file: &File, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
107 use std::os::unix::fs::FileExt;
108
109 file_read_exact_at_with(offset, buf, |chunk, offset| file.read_at(chunk, offset))
110}
111
112#[cfg(windows)]
113fn file_read_exact_at(file: &File, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
114 use std::os::windows::fs::FileExt;
115
116 file_read_exact_at_with(offset, buf, |chunk, offset| file.seek_read(chunk, offset))
117}
118
119#[cfg(any(unix, windows))]
120fn file_read_exact_at_with<F>(
121 mut offset: u64,
122 mut buf: &mut [u8],
123 mut read_at: F,
124) -> Result<(), FormatError>
125where
126 F: FnMut(&mut [u8], u64) -> std::io::Result<usize>,
127{
128 while !buf.is_empty() {
129 let read =
130 read_at(buf, offset).map_err(|_| FormatError::InvalidArchive("archive read failed"))?;
131 if read == 0 {
132 return Err(FormatError::InvalidArchive("archive read failed"));
133 }
134 offset = checked_u64_add(offset, read as u64, "archive read offset overflow")?;
135 let rest = std::mem::take(&mut buf).split_at_mut(read).1;
136 buf = rest;
137 }
138 Ok(())
139}
140
141#[cfg(not(any(unix, windows)))]
142fn file_read_exact_at(file: &File, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
143 let mut file = file
144 .try_clone()
145 .map_err(|_| FormatError::InvalidArchive("archive read clone failed"))?;
146 std::io::Seek::seek(&mut file, std::io::SeekFrom::Start(offset))
147 .map_err(|_| FormatError::InvalidArchive("archive read seek failed"))?;
148 file.read_exact(buf)
149 .map_err(|_| FormatError::InvalidArchive("archive read failed"))
150}
151
152impl ArchiveReadAt for Vec<u8> {
153 fn len(&self) -> Result<u64, FormatError> {
154 u64::try_from(self.len())
155 .map_err(|_| FormatError::InvalidArchive("archive length overflow"))
156 }
157
158 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
159 let offset = to_usize(offset, "archive")?;
160 let end = checked_add(offset, buf.len(), "archive")?;
161 let source = self.get(offset..end).ok_or(FormatError::InvalidLength {
162 structure: "archive",
163 expected: end,
164 actual: self.len(),
165 })?;
166 buf.copy_from_slice(source);
167 Ok(())
168 }
169}
170
171impl<T: ArchiveReadAt + ?Sized> ArchiveReadAt for Arc<T> {
172 fn len(&self) -> Result<u64, FormatError> {
173 (**self).len()
174 }
175
176 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
177 (**self).read_exact_at(offset, buf)
178 }
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub struct ReaderOptions {
183 pub max_trailing_garbage_scan: usize,
184 pub max_verify_tar_size: usize,
185 pub max_total_extraction_size: u64,
186 pub jobs: usize,
187}
188
189impl Default for ReaderOptions {
190 fn default() -> Self {
191 Self {
192 max_trailing_garbage_scan: DEFAULT_MAX_TRAILING_GARBAGE_SCAN,
193 max_verify_tar_size: DEFAULT_MAX_VERIFY_TAR_SIZE,
194 max_total_extraction_size: DEFAULT_MAX_TOTAL_EXTRACTION_SIZE,
195 jobs: default_jobs(),
196 }
197 }
198}
199
200pub(crate) fn validate_reader_options(options: ReaderOptions) -> Result<(), FormatError> {
201 if options.jobs == 0 {
202 return Err(FormatError::ReaderUnsupported("jobs must be at least 1"));
203 }
204 Ok(())
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub struct ArchiveEntry {
209 pub path: String,
210 pub file_data_size: u64,
211 pub kind: TarEntryKind,
212 pub mode: u32,
213 pub mtime: ArchiveTimestamp,
214 pub diagnostics: Vec<MetadataDiagnostic>,
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
218pub struct ArchiveIndexEntry {
219 pub path: String,
220 pub name: String,
221 pub file_data_size: u64,
222 pub flags: u32,
223 pub path_hash: [u8; 8],
224 pub tar_member_group_size: u64,
225 pub first_frame_index: u64,
226 pub frame_count: u32,
227 pub offset_in_first_frame_plaintext: u32,
228 pub layout: ArchiveIndexEntryLayout,
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
232pub struct ArchiveIndexEntryLayout {
233 pub compressed_size: u64,
234 pub decompressed_frame_size: u64,
235 pub envelope_count: u32,
236 pub first_envelope_index: Option<u64>,
237 pub last_envelope_index: Option<u64>,
238 pub first_payload_block_index: Option<u64>,
239 pub payload_data_block_count: u64,
240 pub payload_parity_block_count: u64,
241 pub payload_encrypted_size: u64,
242}
243
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct ExtractedArchiveMember {
246 pub path: String,
247 pub kind: TarEntryKind,
248 pub data: Vec<u8>,
249 pub link_target: Option<String>,
250 pub reparse_placeholder: bool,
251 pub diagnostics: Vec<MetadataDiagnostic>,
252}
253
254pub trait ArchiveExtractProgressSink {
260 fn file_bytes_extracted(&mut self, archive_path: &str, bytes: u64);
262}
263
264impl<F> ArchiveExtractProgressSink for F
265where
266 F: FnMut(&str, u64),
267{
268 fn file_bytes_extracted(&mut self, archive_path: &str, bytes: u64) {
269 self(archive_path, bytes);
270 }
271}
272
273#[derive(Debug, Clone)]
274pub struct OpenedArchive {
275 options: ReaderOptions,
276 observed_archive_bytes: u64,
277 observed_volume_count: u32,
278 subkeys: Subkeys,
279 blocks: BTreeMap<u64, BlockRecord>,
280 lazy_blocks: Option<Arc<SeekableBlockSource>>,
281 crypto_header_bytes: Vec<u8>,
282 pub volume_header: VolumeHeader,
283 pub crypto_header: CryptoHeaderFixed,
284 pub manifest_footer: ManifestFooter,
285 pub volume_trailer: Option<VolumeTrailer>,
286 pub root_auth_footer: Option<RootAuthFooterV1>,
287 pub index_root: IndexRoot,
288 payload_dictionary: Option<Vec<u8>>,
289}
290
291#[derive(Debug)]
292pub struct ArchiveContentVerification<'a> {
293 archive: &'a OpenedArchive,
294 mode: ContentVerificationMode,
295 metadata_report: Option<MetadataVerificationReport>,
296}
297
298impl ArchiveContentVerification<'_> {
299 pub fn metadata_report(&self) -> Option<&MetadataVerificationReport> {
303 self.metadata_report.as_ref()
304 }
305}
306
307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
308enum ContentVerificationMode {
309 Full,
310 Fast,
311}
312
313#[derive(Debug, Clone, PartialEq, Eq)]
314pub struct ArchiveRepairPatch {
315 pub volume_index: u32,
316 pub block_index: u64,
317 pub record_offset: u64,
318 pub record_bytes: Vec<u8>,
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
322pub enum RootAuthDiagnostic {
323 RootAuthContentVerified,
324 RootAuthDeferredFullArchiveScanRequired,
325 AuthenticatedMetadataNotRootSigned,
326 RecoveryMarginNotRootAuthenticated,
327 ReplicatedGlobalCopyUncheckedDueToVolumeLoss,
328 RecoveryMarginChecked,
329 RecoveryMarginFailed,
330 RecoveryMarginUnchecked,
331}
332
333impl RootAuthDiagnostic {
334 pub const fn label(self) -> &'static str {
335 match self {
336 Self::RootAuthContentVerified => "root_auth_content_verified",
337 Self::RootAuthDeferredFullArchiveScanRequired => {
338 "root_auth_deferred_full_archive_scan_required"
339 }
340 Self::AuthenticatedMetadataNotRootSigned => "authenticated_metadata_not_root_signed",
341 Self::RecoveryMarginNotRootAuthenticated => "recovery_margin_not_root_authenticated",
342 Self::ReplicatedGlobalCopyUncheckedDueToVolumeLoss => {
343 "replicated_global_copy_unchecked_due_to_volume_loss"
344 }
345 Self::RecoveryMarginChecked => "recovery_margin_checked",
346 Self::RecoveryMarginFailed => "recovery_margin_failed",
347 Self::RecoveryMarginUnchecked => "recovery_margin_unchecked",
348 }
349 }
350}
351
352#[derive(Debug, Clone, Copy, PartialEq, Eq)]
353pub enum PublicNoKeyDiagnostic {
354 PublicDataBlockCommitmentVerified,
355 PublicPhysicalCompletenessUnverified,
356 PublicRecoveryMarginUnchecked,
357}
358
359impl PublicNoKeyDiagnostic {
360 pub const fn label(self) -> &'static str {
361 match self {
362 Self::PublicDataBlockCommitmentVerified => "public_data_block_commitment_verified",
363 Self::PublicPhysicalCompletenessUnverified => "public_physical_completeness_unverified",
364 Self::PublicRecoveryMarginUnchecked => "public_recovery_margin_unchecked",
365 }
366 }
367}
368
369#[derive(Debug, Clone, PartialEq, Eq)]
370pub struct RootAuthVerification {
371 pub format_version: u16,
372 pub volume_format_rev: u16,
373 pub archive_root: [u8; 32],
374 pub authenticator_id: u16,
375 pub signer_identity_type: u16,
376 pub signer_identity_bytes: Vec<u8>,
377 pub total_data_block_count: u64,
378 pub diagnostics: Vec<RootAuthDiagnostic>,
379}
380
381#[derive(Debug, Clone, PartialEq, Eq)]
382pub struct PublicNoKeyVerification {
383 pub format_version: u16,
384 pub volume_format_rev: u16,
385 pub archive_root: [u8; 32],
386 pub authenticator_id: u16,
387 pub signer_identity_type: u16,
388 pub signer_identity_bytes: Vec<u8>,
389 pub total_data_block_count: u64,
390 pub diagnostics: Vec<PublicNoKeyDiagnostic>,
391}
392
393#[derive(Debug, Clone, Copy, PartialEq, Eq)]
394struct RootAuthMaterial {
395 critical_metadata_digest: [u8; 32],
396 index_digest: [u8; 32],
397 fec_layout_digest: [u8; 32],
398 data_block_merkle_root: [u8; 32],
399 signer_identity_digest: [u8; 32],
400 archive_root: [u8; 32],
401 total_data_block_count: u64,
402}
403
404#[derive(Debug, Clone, Copy)]
405struct ObjectExtent {
406 first_block_index: u64,
407 data_block_count: u32,
408 parity_block_count: u32,
409 encrypted_size: u32,
410}
411
412#[derive(Debug, Clone, Copy, PartialEq, Eq)]
413enum ParityReadPolicy {
414 Always,
415 RepairOnly,
416}
417
418pub(crate) struct StreamedArchiveOpenParts {
419 pub(crate) options: ReaderOptions,
420 pub(crate) observed_archive_bytes: u64,
421 pub(crate) subkeys: Subkeys,
422 pub(crate) blocks: BTreeMap<u64, BlockRecord>,
423 pub(crate) crypto_header_bytes: Vec<u8>,
424 pub(crate) volume_header: VolumeHeader,
425 pub(crate) crypto_header: CryptoHeaderFixed,
426 pub(crate) manifest_footer: ManifestFooter,
427 pub(crate) volume_trailer: VolumeTrailer,
428 pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
429}
430
431#[derive(Clone, Copy)]
432struct WinningIndexEntry {
433 start: u64,
434 file_data_size: u64,
435 shard_index: usize,
436 file_index: usize,
437}
438
439struct LocatedIndexFile {
440 shard: IndexShard,
441 file_index: usize,
442 start: u64,
443}
444
445struct ExtractProgressWriter<'a, W> {
446 inner: &'a mut W,
447 archive_path: &'a str,
448 file_data_size: u64,
449 reported_bytes: u64,
450 progress: &'a mut dyn ArchiveExtractProgressSink,
451}
452
453impl<'a, W> ExtractProgressWriter<'a, W> {
454 fn new(
455 inner: &'a mut W,
456 archive_path: &'a str,
457 file_data_size: u64,
458 progress: &'a mut dyn ArchiveExtractProgressSink,
459 ) -> Self {
460 Self {
461 inner,
462 archive_path,
463 file_data_size,
464 reported_bytes: 0,
465 progress,
466 }
467 }
468
469 fn report(&mut self, bytes: u64) {
470 if bytes == 0 || self.file_data_size == 0 {
471 return;
472 }
473 let capped_next = self
474 .reported_bytes
475 .saturating_add(bytes)
476 .min(self.file_data_size);
477 let delta = capped_next.saturating_sub(self.reported_bytes);
478 if delta == 0 {
479 return;
480 }
481 self.reported_bytes = capped_next;
482 self.progress.file_bytes_extracted(self.archive_path, delta);
483 }
484}
485
486impl<W: Write> Write for ExtractProgressWriter<'_, W> {
487 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
488 let written = self.inner.write(buf)?;
489 self.report(written as u64);
490 Ok(written)
491 }
492
493 fn flush(&mut self) -> std::io::Result<()> {
494 self.inner.flush()
495 }
496}
497
498struct DecodedTarMemberGroupReader<'a> {
499 archive: &'a OpenedArchive,
500 shard: &'a IndexShard,
501 file: &'a FileEntry,
502 decompressor: zstd::bulk::Decompressor<'static>,
503 next_frame_offset: u64,
504 cached_envelope_index: Option<u64>,
505 cached_envelope_plaintext: Vec<u8>,
506 current_frame: Vec<u8>,
507 current_frame_offset: usize,
508 remaining_group_bytes: u64,
509}
510
511struct SeekableVolumeSource {
512 reader: Arc<dyn ArchiveReadAt>,
513 volume_index: u32,
514 block_records_start: u64,
515 block_count: u64,
516 record_len: u64,
517 block_size: usize,
518}
519
520impl std::fmt::Debug for SeekableVolumeSource {
521 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
522 f.debug_struct("SeekableVolumeSource")
523 .field("volume_index", &self.volume_index)
524 .field("block_records_start", &self.block_records_start)
525 .field("block_count", &self.block_count)
526 .field("record_len", &self.record_len)
527 .field("block_size", &self.block_size)
528 .finish_non_exhaustive()
529 }
530}
531
532#[derive(Debug)]
533struct SeekableBlockSource {
534 stripe_width: u32,
535 volumes: Vec<Option<SeekableVolumeSource>>,
536}
537
538trait BlockProvider {
539 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError>;
540}
541
542struct OpenedBlockProvider<'a> {
543 memory_blocks: &'a BTreeMap<u64, BlockRecord>,
544 lazy_blocks: Option<&'a SeekableBlockSource>,
545}
546
547impl SeekableBlockSource {
548 fn record_location(&self, block_index: u64) -> Result<(u32, u64), FormatError> {
549 if self.stripe_width == 0 {
550 return Err(FormatError::ZeroStripeWidth);
551 }
552 let volume_index = u32::try_from(block_index % self.stripe_width as u64)
553 .map_err(|_| FormatError::InvalidArchive("BlockRecord volume index overflow"))?;
554 let Some(volume) = self
555 .volumes
556 .get(volume_index as usize)
557 .and_then(Option::as_ref)
558 else {
559 return Err(FormatError::InvalidArchive(
560 "repair output requires all archive volumes",
561 ));
562 };
563 let slot = block_index / self.stripe_width as u64;
564 if slot >= volume.block_count {
565 return Err(FormatError::InvalidArchive(
566 "BlockRecord global coverage has a gap",
567 ));
568 }
569 Ok((volume_index, volume.record_offset(slot)?))
570 }
571
572 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
573 if self.stripe_width == 0 {
574 return Err(FormatError::ZeroStripeWidth);
575 }
576 let volume_index = u32::try_from(block_index % self.stripe_width as u64)
577 .map_err(|_| FormatError::InvalidArchive("BlockRecord volume index overflow"))?;
578 let Some(volume) = self
579 .volumes
580 .get(volume_index as usize)
581 .and_then(Option::as_ref)
582 else {
583 return Ok(None);
584 };
585 let slot = block_index / self.stripe_width as u64;
586 if slot >= volume.block_count {
587 return Ok(None);
588 }
589 match volume.read_slot(slot, block_index) {
590 Ok(record) => Ok(Some(record)),
591 Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
592 Err(err) => Err(err),
593 }
594 }
595
596 fn is_complete_volume_set(&self) -> bool {
597 self.volumes.iter().all(Option::is_some)
598 }
599
600 fn total_block_count(&self) -> Result<u64, FormatError> {
601 self.volumes
602 .iter()
603 .map(|volume| {
604 volume
605 .as_ref()
606 .map(|volume| volume.block_count)
607 .ok_or(FormatError::InvalidArchive(
608 "missing volume in complete set",
609 ))
610 })
611 .try_fold(0u64, |sum, count| {
612 checked_u64_add(sum, count?, "BlockRecord count overflow")
613 })
614 }
615}
616
617impl SeekableVolumeSource {
618 fn record_offset(&self, slot: u64) -> Result<u64, FormatError> {
619 self.block_records_start
620 .checked_add(checked_u64_mul(
621 slot,
622 self.record_len,
623 "BlockRecord offset overflow",
624 )?)
625 .ok_or(FormatError::InvalidArchive("BlockRecord offset overflow"))
626 }
627
628 fn read_slot(&self, slot: u64, expected_block_index: u64) -> Result<BlockRecord, FormatError> {
629 let record_offset = self.record_offset(slot)?;
630 let raw = read_at_vec_unchecked(
631 self.reader.as_ref(),
632 record_offset,
633 usize::try_from(self.record_len)
634 .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?,
635 )?;
636 let record = BlockRecord::parse(&raw, self.block_size)?;
637 if record.block_index != expected_block_index {
638 return Err(FormatError::InvalidArchive(
639 "BlockRecord index does not match volume position",
640 ));
641 }
642 Ok(record)
643 }
644}
645
646impl BlockProvider for BTreeMap<u64, BlockRecord> {
647 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
648 Ok(self.get(&block_index).cloned())
649 }
650}
651
652impl BlockProvider for OpenedBlockProvider<'_> {
653 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
654 if let Some(record) = self.memory_blocks.get(&block_index) {
655 return Ok(Some(record.clone()));
656 }
657 match self.lazy_blocks {
658 Some(source) => source.block(block_index),
659 None => Ok(None),
660 }
661 }
662}
663
664fn subkeys_for_open(
665 master_key: Option<&MasterKey>,
666 aead_algo: AeadAlgo,
667 archive_uuid: &[u8; 16],
668 session_id: &[u8; 16],
669) -> Result<Subkeys, FormatError> {
670 if aead_algo.is_encrypted() {
671 Subkeys::derive(
672 master_key.ok_or(FormatError::KeyMaterialMismatch)?,
673 archive_uuid,
674 session_id,
675 )
676 } else {
677 Ok(Subkeys::unencrypted_placeholder())
678 }
679}
680
681type DirectoryHintMap = BTreeMap<Vec<u8>, BTreeSet<u32>>;
682pub type ExtractedRegularFile = (Vec<u8>, Vec<MetadataDiagnostic>);
683const FAST_FULL_EXTRACT_UNIQUE_PATHS_UNSUPPORTED: &str =
684 "fast full extract requires unique archive paths";
685
686fn parse_volume_format_dispatch(
687 volume_header: &VolumeHeader,
688) -> Result<VolumeFormatRevision, FormatError> {
689 let revision = volume_header.parse_volume_format_revision()?;
690 match revision {
691 VolumeFormatRevision::V45 => Ok(revision),
692 }
693}
694
695#[derive(Debug)]
696struct PayloadIndexTables {
697 shards: Vec<IndexShard>,
698 file_count: u64,
699 frames: BTreeMap<u64, FrameEntry>,
700 envelopes: BTreeMap<u64, EnvelopeEntry>,
701}
702
703pub fn open_archive(bytes: &[u8], master_key: &MasterKey) -> Result<OpenedArchive, FormatError> {
704 OpenedArchive::open_with_options(bytes, master_key, ReaderOptions::default())
705}
706
707pub fn open_archive_with_recipient_wrap_resolver<F>(
708 bytes: &[u8],
709 resolver: F,
710) -> Result<OpenedArchive, FormatError>
711where
712 F: FnMut(
713 RecipientWrapRecordContext<'_>,
714 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
715{
716 OpenedArchive::open_with_recipient_wrap_resolver_options(
717 bytes,
718 resolver,
719 ReaderOptions::default(),
720 )
721}
722
723pub fn open_archive_unencrypted(bytes: &[u8]) -> Result<OpenedArchive, FormatError> {
724 require_unencrypted_volume_profile(bytes)?;
725 let placeholder = MasterKey::from_raw_key(&[0; 32])?;
726 OpenedArchive::open_with_options(bytes, &placeholder, ReaderOptions::default())
727}
728
729pub fn open_archive_volumes(
730 volumes: &[&[u8]],
731 master_key: &MasterKey,
732) -> Result<OpenedArchive, FormatError> {
733 OpenedArchive::open_volumes_with_options(volumes, master_key, ReaderOptions::default())
734}
735
736pub fn open_archive_volumes_unencrypted(volumes: &[&[u8]]) -> Result<OpenedArchive, FormatError> {
737 for volume in volumes {
738 require_unencrypted_volume_profile(volume)?;
739 }
740 let placeholder = MasterKey::from_raw_key(&[0; 32])?;
741 OpenedArchive::open_volumes_with_options(volumes, &placeholder, ReaderOptions::default())
742}
743
744pub fn open_archive_with_bootstrap_sidecar(
745 bytes: &[u8],
746 bootstrap_sidecar: &[u8],
747 master_key: &MasterKey,
748) -> Result<OpenedArchive, FormatError> {
749 OpenedArchive::open_with_bootstrap_sidecar_options(
750 bytes,
751 bootstrap_sidecar,
752 master_key,
753 ReaderOptions::default(),
754 )
755}
756
757fn require_unencrypted_volume_profile(bytes: &[u8]) -> Result<(), FormatError> {
758 if bytes.len() < VOLUME_HEADER_LEN {
759 return Err(FormatError::InvalidLength {
760 structure: "archive",
761 expected: VOLUME_HEADER_LEN,
762 actual: bytes.len(),
763 });
764 }
765 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
766 parse_volume_format_dispatch(&volume_header)?;
767 let crypto_start = volume_header.crypto_header_offset as usize;
768 let crypto_len = volume_header.crypto_header_length as usize;
769 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
770 let crypto_header = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
771 if crypto_header.fixed.aead_algo == AeadAlgo::None
772 && crypto_header.fixed.kdf_algo == KdfAlgo::None
773 {
774 Ok(())
775 } else {
776 Err(FormatError::KeyMaterialMismatch)
777 }
778}
779
780pub fn open_seekable_archive<R: ArchiveReadAt>(
781 reader: R,
782 master_key: &MasterKey,
783) -> Result<OpenedArchive, FormatError> {
784 OpenedArchive::open_seekable_volumes_with_options(
785 vec![reader],
786 master_key,
787 ReaderOptions::default(),
788 )
789}
790
791pub fn open_seekable_archive_volumes<R: ArchiveReadAt>(
792 readers: Vec<R>,
793 master_key: &MasterKey,
794) -> Result<OpenedArchive, FormatError> {
795 OpenedArchive::open_seekable_volumes_with_options(readers, master_key, ReaderOptions::default())
796}
797
798pub fn open_seekable_archive_with_bootstrap_sidecar<R: ArchiveReadAt>(
799 reader: R,
800 bootstrap_sidecar: &[u8],
801 master_key: &MasterKey,
802) -> Result<OpenedArchive, FormatError> {
803 open_seekable_archive_with_bootstrap_sidecar_options(
804 reader,
805 bootstrap_sidecar,
806 master_key,
807 ReaderOptions::default(),
808 )
809}
810
811pub fn open_seekable_archive_with_bootstrap_sidecar_options<R: ArchiveReadAt>(
812 reader: R,
813 bootstrap_sidecar: &[u8],
814 master_key: &MasterKey,
815 options: ReaderOptions,
816) -> Result<OpenedArchive, FormatError> {
817 OpenedArchive::open_seekable_volumes_with_options_for_mode(
818 vec![Arc::new(reader) as Arc<dyn ArchiveReadAt>],
819 master_key,
820 options,
821 Some(bootstrap_sidecar),
822 )
823}
824
825pub fn open_seekable_archive_with_recipient_wrap_resolver_options<R, F>(
826 reader: R,
827 resolver: F,
828 options: ReaderOptions,
829) -> Result<OpenedArchive, FormatError>
830where
831 R: ArchiveReadAt,
832 F: FnMut(
833 RecipientWrapRecordContext<'_>,
834 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
835{
836 OpenedArchive::open_seekable_with_recipient_wrap_resolver_options(reader, resolver, options)
837}
838
839pub fn open_seekable_archive_volumes_with_recipient_wrap_resolver_options<R, F>(
840 readers: Vec<R>,
841 resolver: F,
842 options: ReaderOptions,
843) -> Result<OpenedArchive, FormatError>
844where
845 R: ArchiveReadAt,
846 F: FnMut(
847 RecipientWrapRecordContext<'_>,
848 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
849{
850 OpenedArchive::open_seekable_volumes_with_recipient_wrap_resolver_options(
851 readers, resolver, options,
852 )
853}
854
855pub fn open_non_seekable_archive(
856 bytes: &[u8],
857 master_key: &MasterKey,
858 bootstrap_sidecar: Option<&[u8]>,
859) -> Result<OpenedArchive, FormatError> {
860 match bootstrap_sidecar {
861 Some(sidecar) => OpenedArchive::open_with_bootstrap_sidecar_options_for_mode(
862 bytes,
863 sidecar,
864 master_key,
865 ReaderOptions::default(),
866 BootstrapSidecarUse::NonSeekableRandomAccess,
867 ),
868 None => Err(FormatError::ReaderUnsupported(
869 "non-seekable random access requires a bootstrap sidecar",
870 )),
871 }
872}
873
874pub fn public_no_key_verify_archive_with<F>(
875 bytes: &[u8],
876 verifier: F,
877) -> Result<PublicNoKeyVerification, FormatError>
878where
879 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
880{
881 public_no_key_verify_volumes_with_options(&[bytes], verifier, ReaderOptions::default())
882}
883
884pub fn public_no_key_verify_volumes_with<F>(
885 volumes: &[&[u8]],
886 verifier: F,
887) -> Result<PublicNoKeyVerification, FormatError>
888where
889 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
890{
891 public_no_key_verify_volumes_with_options(volumes, verifier, ReaderOptions::default())
892}
893
894pub fn sequential_extract_tar_stream(
900 bytes: &[u8],
901 master_key: &MasterKey,
902) -> Result<Vec<u8>, FormatError> {
903 sequential_extract_tar_stream_with_options(bytes, master_key, ReaderOptions::default())
904}
905
906impl OpenedArchive {
907 fn block_provider(&self) -> OpenedBlockProvider<'_> {
908 OpenedBlockProvider {
909 memory_blocks: &self.blocks,
910 lazy_blocks: self.lazy_blocks.as_deref(),
911 }
912 }
913
914 fn missing_volume_count(&self) -> u32 {
915 self.crypto_header
916 .stripe_width
917 .saturating_sub(self.observed_volume_count)
918 }
919
920 fn root_auth_success_diagnostics(&self) -> Vec<RootAuthDiagnostic> {
921 let mut diagnostics = vec![
922 RootAuthDiagnostic::RootAuthContentVerified,
923 RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
924 RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
925 ];
926 if self.missing_volume_count() > 0 {
927 diagnostics.push(RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss);
928 }
929 diagnostics.push(RootAuthDiagnostic::RecoveryMarginUnchecked);
930 diagnostics
931 }
932
933 pub fn open_with_options(
934 bytes: &[u8],
935 master_key: &MasterKey,
936 options: ReaderOptions,
937 ) -> Result<Self, FormatError> {
938 Self::open_volumes_with_options(&[bytes], master_key, options)
939 }
940
941 pub fn open_with_recipient_wrap_resolver_options<F>(
942 bytes: &[u8],
943 mut resolver: F,
944 options: ReaderOptions,
945 ) -> Result<Self, FormatError>
946 where
947 F: FnMut(
948 RecipientWrapRecordContext<'_>,
949 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
950 {
951 validate_reader_options(options)?;
952 let observed_archive_bytes = observed_archive_size(std::iter::once(bytes.len() as u64))?;
953 let parsed =
954 parse_seekable_volume_with_recipient_wrap_resolver(bytes, &mut resolver, options)?;
955 let ParsedSeekableVolume {
956 volume_header,
957 crypto_header,
958 crypto_header_bytes,
959 key_wrap_table_bytes: _,
960 subkeys,
961 manifest_footer,
962 manifest_footer_error,
963 root_auth_footer,
964 root_auth_footer_bytes: _,
965 volume_trailer,
966 blocks,
967 erased_block_indices,
968 } = parsed;
969 let manifest_footer = match manifest_footer {
970 Some(footer) => footer,
971 None => {
972 return Err(manifest_footer_error.unwrap_or(FormatError::InvalidArchive(
973 "no authenticated ManifestFooter found",
974 )));
975 }
976 };
977 let observed_volume_count = 1;
978 let missing_volume_count = crypto_header
979 .stripe_width
980 .checked_sub(observed_volume_count)
981 .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
982 if missing_volume_count > crypto_header.volume_loss_tolerance as u32 {
983 return Err(FormatError::InvalidArchive(
984 "missing volume count exceeds volume_loss_tolerance",
985 ));
986 }
987 if missing_volume_count == 0 {
988 validate_complete_global_block_coverage(&blocks, &erased_block_indices)?;
989 }
990
991 let limits = metadata_limits(&crypto_header);
992 let index_root_plaintext = load_metadata_object_from_parts(
993 &blocks,
994 ObjectLoadContext::index_root(
995 &volume_header,
996 &crypto_header,
997 &subkeys,
998 ObjectExtent {
999 first_block_index: manifest_footer.index_root_first_block,
1000 data_block_count: manifest_footer.index_root_data_block_count,
1001 parity_block_count: manifest_footer.index_root_parity_block_count,
1002 encrypted_size: manifest_footer.index_root_encrypted_size,
1003 },
1004 ),
1005 manifest_footer.index_root_decompressed_size,
1006 )?;
1007 let index_root = IndexRoot::parse(
1008 &index_root_plaintext,
1009 crypto_header.has_dictionary != 0,
1010 limits,
1011 )?;
1012 let payload_dictionary = load_archive_dictionary(
1013 &blocks,
1014 &subkeys,
1015 &volume_header,
1016 &crypto_header,
1017 &index_root,
1018 )?;
1019
1020 Ok(Self {
1021 options,
1022 observed_archive_bytes,
1023 observed_volume_count,
1024 subkeys,
1025 blocks,
1026 lazy_blocks: None,
1027 crypto_header_bytes,
1028 volume_header,
1029 crypto_header,
1030 manifest_footer,
1031 volume_trailer: Some(volume_trailer),
1032 root_auth_footer,
1033 index_root,
1034 payload_dictionary,
1035 })
1036 }
1037
1038 pub fn open_seekable_with_recipient_wrap_resolver_options<R, F>(
1039 reader: R,
1040 mut resolver: F,
1041 options: ReaderOptions,
1042 ) -> Result<Self, FormatError>
1043 where
1044 R: ArchiveReadAt,
1045 F: FnMut(
1046 RecipientWrapRecordContext<'_>,
1047 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
1048 {
1049 validate_reader_options(options)?;
1050 let reader = Arc::new(reader) as Arc<dyn ArchiveReadAt>;
1051 let observed_len = reader.len()?;
1052 let observed_archive_bytes = observed_archive_size([observed_len])?;
1053 let mut parsed = parse_seekable_read_at_volume_with_recipient_wrap_resolver(
1054 reader.clone(),
1055 &mut resolver,
1056 options,
1057 )?;
1058 let manifest_footer = match parsed.manifest_footer.take() {
1059 Some(footer) => footer,
1060 None => {
1061 return Err(parsed.manifest_footer_error.take().unwrap_or(
1062 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1063 ));
1064 }
1065 };
1066 let observed_volume_count = 1;
1067 let missing_volume_count = parsed
1068 .crypto_header
1069 .stripe_width
1070 .checked_sub(observed_volume_count)
1071 .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1072 if missing_volume_count > parsed.crypto_header.volume_loss_tolerance as u32 {
1073 return Err(FormatError::InvalidArchive(
1074 "missing volume count exceeds volume_loss_tolerance",
1075 ));
1076 }
1077
1078 let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
1079 let mut lazy_volume_slots = Vec::new();
1080 lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
1081 let slot = parsed.volume_header.volume_index as usize;
1082 if slot >= lazy_volume_slots.len() {
1083 return Err(FormatError::InvalidArchive(
1084 "authenticated volume index exceeds stripe_width",
1085 ));
1086 }
1087 lazy_volume_slots[slot] = Some(SeekableVolumeSource {
1088 reader: parsed.reader.clone(),
1089 volume_index: parsed.volume_header.volume_index,
1090 block_records_start: parsed.block_records_start,
1091 block_count: parsed.volume_trailer.block_count,
1092 record_len,
1093 block_size: parsed.crypto_header.block_size as usize,
1094 });
1095 let lazy_source = Arc::new(SeekableBlockSource {
1096 stripe_width: parsed.crypto_header.stripe_width,
1097 volumes: lazy_volume_slots,
1098 });
1099 let blocks = BTreeMap::new();
1100 let block_provider = OpenedBlockProvider {
1101 memory_blocks: &blocks,
1102 lazy_blocks: Some(lazy_source.as_ref()),
1103 };
1104 let limits = metadata_limits(&parsed.crypto_header);
1105 let index_root_plaintext = load_metadata_object_from_parts(
1106 &block_provider,
1107 ObjectLoadContext::index_root(
1108 &parsed.volume_header,
1109 &parsed.crypto_header,
1110 &parsed.subkeys,
1111 index_root_extent_from_manifest(&manifest_footer),
1112 ),
1113 manifest_footer.index_root_decompressed_size,
1114 )?;
1115 let index_root = IndexRoot::parse(
1116 &index_root_plaintext,
1117 parsed.crypto_header.has_dictionary != 0,
1118 limits,
1119 )?;
1120 let block_provider = OpenedBlockProvider {
1121 memory_blocks: &blocks,
1122 lazy_blocks: Some(lazy_source.as_ref()),
1123 };
1124 let payload_dictionary = load_archive_dictionary(
1125 &block_provider,
1126 &parsed.subkeys,
1127 &parsed.volume_header,
1128 &parsed.crypto_header,
1129 &index_root,
1130 )?;
1131
1132 Ok(Self {
1133 options,
1134 observed_archive_bytes,
1135 observed_volume_count,
1136 subkeys: parsed.subkeys,
1137 blocks,
1138 lazy_blocks: Some(lazy_source),
1139 crypto_header_bytes: parsed.crypto_header_bytes,
1140 volume_header: parsed.volume_header,
1141 crypto_header: parsed.crypto_header,
1142 manifest_footer,
1143 volume_trailer: Some(parsed.volume_trailer),
1144 root_auth_footer: parsed.root_auth_footer,
1145 index_root,
1146 payload_dictionary,
1147 })
1148 }
1149
1150 pub fn open_seekable_volumes_with_recipient_wrap_resolver_options<R, F>(
1151 readers: Vec<R>,
1152 mut resolver: F,
1153 options: ReaderOptions,
1154 ) -> Result<Self, FormatError>
1155 where
1156 R: ArchiveReadAt,
1157 F: FnMut(
1158 RecipientWrapRecordContext<'_>,
1159 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
1160 {
1161 validate_reader_options(options)?;
1162 if readers.is_empty() {
1163 return Err(FormatError::InvalidArchive("no volumes supplied"));
1164 }
1165 let readers = readers
1166 .into_iter()
1167 .map(|reader| Arc::new(reader) as Arc<dyn ArchiveReadAt>)
1168 .collect::<Vec<_>>();
1169 let observed_archive_bytes = observed_archive_size(
1170 readers
1171 .iter()
1172 .map(|reader| reader.len())
1173 .collect::<Result<Vec<_>, _>>()?,
1174 )?;
1175 let mut first: Option<ParsedSeekableReadAtVolume> = None;
1176 let mut manifest_authority: Option<ManifestFooter> = None;
1177 let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
1178 let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
1179 let mut root_auth_authority: Option<RootAuthFooterV1> = None;
1180 let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
1181 let mut saw_root_auth_absent = false;
1182 let mut first_manifest_footer_error: Option<FormatError> = None;
1183 let mut seen_volume_indexes = BTreeSet::new();
1184 let mut lazy_volume_slots: Vec<Option<SeekableVolumeSource>> = Vec::new();
1185
1186 for reader in readers {
1187 let mut parsed = parse_seekable_read_at_volume_with_recipient_wrap_resolver(
1188 reader,
1189 &mut resolver,
1190 options,
1191 )?;
1192 if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
1193 return Err(FormatError::InvalidArchive(
1194 "duplicate authenticated volume index",
1195 ));
1196 }
1197
1198 if let Some(first) = &first {
1199 validate_volume_set_member_metadata(
1200 &first.volume_header,
1201 &first.crypto_header,
1202 &first.crypto_header_bytes,
1203 &parsed.volume_header,
1204 &parsed.crypto_header,
1205 &parsed.crypto_header_bytes,
1206 )?;
1207 validate_key_wrap_table_bytes_match(
1208 &first.key_wrap_table_bytes,
1209 &parsed.key_wrap_table_bytes,
1210 )?;
1211 } else {
1212 lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
1213 }
1214
1215 if let Some(footer) = &parsed.manifest_footer {
1216 if let Some(authority) = &manifest_authority {
1217 if !manifest_bootstrap_fields_match(authority, footer) {
1218 return Err(FormatError::InvalidArchive(
1219 "ManifestFooter bootstrap fields differ",
1220 ));
1221 }
1222 } else {
1223 manifest_authority = Some(footer.clone());
1224 manifest_authority_volume_header = Some(parsed.volume_header.clone());
1225 manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
1226 }
1227 } else if first_manifest_footer_error.is_none() {
1228 first_manifest_footer_error = parsed.manifest_footer_error.take();
1229 }
1230
1231 match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
1232 (Some(footer), Some(bytes)) => {
1233 if saw_root_auth_absent {
1234 return Err(FormatError::InvalidArchive(
1235 "root-auth footer presence differs across volumes",
1236 ));
1237 }
1238 if let Some(authority_bytes) = &root_auth_authority_bytes {
1239 if authority_bytes != bytes {
1240 return Err(FormatError::InvalidArchive(
1241 "RootAuthFooter copies differ",
1242 ));
1243 }
1244 } else {
1245 root_auth_authority = Some(footer.clone());
1246 root_auth_authority_bytes = Some(bytes.clone());
1247 }
1248 }
1249 (None, None) => {
1250 if root_auth_authority_bytes.is_some() {
1251 return Err(FormatError::InvalidArchive(
1252 "root-auth footer presence differs across volumes",
1253 ));
1254 }
1255 saw_root_auth_absent = true;
1256 }
1257 _ => {
1258 return Err(FormatError::InvalidArchive(
1259 "root-auth footer terminal state is inconsistent",
1260 ));
1261 }
1262 }
1263
1264 let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
1265 let source = SeekableVolumeSource {
1266 reader: parsed.reader.clone(),
1267 volume_index: parsed.volume_header.volume_index,
1268 block_records_start: parsed.block_records_start,
1269 block_count: parsed.volume_trailer.block_count,
1270 record_len,
1271 block_size: parsed.crypto_header.block_size as usize,
1272 };
1273 let slot = parsed.volume_header.volume_index as usize;
1274 if slot >= lazy_volume_slots.len() || lazy_volume_slots[slot].replace(source).is_some()
1275 {
1276 return Err(FormatError::InvalidArchive(
1277 "duplicate authenticated volume index",
1278 ));
1279 }
1280
1281 if first.is_none() {
1282 first = Some(parsed);
1283 }
1284 }
1285
1286 let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
1287 let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
1288 Some(err) => err,
1289 None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1290 })?;
1291 let authority_volume_header = manifest_authority_volume_header.ok_or(
1292 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1293 )?;
1294 let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
1295 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1296 )?;
1297 let observed_volume_count = u32::try_from(seen_volume_indexes.len())
1298 .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
1299 let missing_volume_count = first
1300 .crypto_header
1301 .stripe_width
1302 .checked_sub(observed_volume_count)
1303 .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1304 if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
1305 return Err(FormatError::InvalidArchive(
1306 "missing volume count exceeds volume_loss_tolerance",
1307 ));
1308 }
1309
1310 let blocks = BTreeMap::new();
1311 let lazy_source = Arc::new(SeekableBlockSource {
1312 stripe_width: first.crypto_header.stripe_width,
1313 volumes: lazy_volume_slots,
1314 });
1315 let block_provider = OpenedBlockProvider {
1316 memory_blocks: &blocks,
1317 lazy_blocks: Some(lazy_source.as_ref()),
1318 };
1319 let limits = metadata_limits(&first.crypto_header);
1320 let index_root_plaintext = load_metadata_object_from_parts(
1321 &block_provider,
1322 ObjectLoadContext::index_root(
1323 &first.volume_header,
1324 &first.crypto_header,
1325 &first.subkeys,
1326 index_root_extent_from_manifest(&manifest_footer),
1327 ),
1328 manifest_footer.index_root_decompressed_size,
1329 )?;
1330 let index_root = IndexRoot::parse(
1331 &index_root_plaintext,
1332 first.crypto_header.has_dictionary != 0,
1333 limits,
1334 )?;
1335 let block_provider = OpenedBlockProvider {
1336 memory_blocks: &blocks,
1337 lazy_blocks: Some(lazy_source.as_ref()),
1338 };
1339 let payload_dictionary = load_archive_dictionary(
1340 &block_provider,
1341 &first.subkeys,
1342 &first.volume_header,
1343 &first.crypto_header,
1344 &index_root,
1345 )?;
1346
1347 Ok(Self {
1348 options,
1349 observed_archive_bytes,
1350 observed_volume_count,
1351 subkeys: first.subkeys,
1352 blocks,
1353 lazy_blocks: Some(lazy_source),
1354 crypto_header_bytes: first.crypto_header_bytes,
1355 volume_header: authority_volume_header,
1356 crypto_header: first.crypto_header,
1357 manifest_footer,
1358 volume_trailer: Some(authority_volume_trailer),
1359 root_auth_footer: root_auth_authority,
1360 index_root,
1361 payload_dictionary,
1362 })
1363 }
1364
1365 pub fn open_volumes_with_options(
1366 volumes: &[&[u8]],
1367 master_key: &MasterKey,
1368 options: ReaderOptions,
1369 ) -> Result<Self, FormatError> {
1370 validate_reader_options(options)?;
1371 if volumes.is_empty() {
1372 return Err(FormatError::InvalidArchive("no volumes supplied"));
1373 }
1374
1375 let observed_archive_bytes =
1376 observed_archive_size(volumes.iter().map(|volume| volume.len() as u64))?;
1377 let mut first: Option<ParsedSeekableVolume> = None;
1378 let mut manifest_authority: Option<ManifestFooter> = None;
1379 let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
1380 let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
1381 let mut root_auth_authority: Option<RootAuthFooterV1> = None;
1382 let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
1383 let mut saw_root_auth_absent = false;
1384 let mut first_manifest_footer_error: Option<FormatError> = None;
1385 let mut seen_volume_indexes = BTreeSet::new();
1386 let mut blocks = BTreeMap::new();
1387 let mut erased_block_indices = BTreeSet::new();
1388
1389 for volume_bytes in volumes {
1390 let mut parsed = parse_seekable_volume(volume_bytes, master_key, options)?;
1391 if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
1392 return Err(FormatError::InvalidArchive(
1393 "duplicate authenticated volume index",
1394 ));
1395 }
1396
1397 if let Some(first) = &first {
1398 validate_volume_set_member(first, &parsed)?;
1399 }
1400
1401 if let Some(footer) = &parsed.manifest_footer {
1402 if let Some(authority) = &manifest_authority {
1403 if !manifest_bootstrap_fields_match(authority, footer) {
1404 return Err(FormatError::InvalidArchive(
1405 "ManifestFooter bootstrap fields differ",
1406 ));
1407 }
1408 } else {
1409 manifest_authority = Some(footer.clone());
1410 manifest_authority_volume_header = Some(parsed.volume_header.clone());
1411 manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
1412 }
1413 } else if first_manifest_footer_error.is_none() {
1414 first_manifest_footer_error = parsed.manifest_footer_error.take();
1415 }
1416
1417 match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
1418 (Some(footer), Some(bytes)) => {
1419 if saw_root_auth_absent {
1420 return Err(FormatError::InvalidArchive(
1421 "root-auth footer presence differs across volumes",
1422 ));
1423 }
1424 if let Some(authority_bytes) = &root_auth_authority_bytes {
1425 if authority_bytes != bytes {
1426 return Err(FormatError::InvalidArchive(
1427 "RootAuthFooter copies differ",
1428 ));
1429 }
1430 } else {
1431 root_auth_authority = Some(footer.clone());
1432 root_auth_authority_bytes = Some(bytes.clone());
1433 }
1434 }
1435 (None, None) => {
1436 if root_auth_authority_bytes.is_some() {
1437 return Err(FormatError::InvalidArchive(
1438 "root-auth footer presence differs across volumes",
1439 ));
1440 }
1441 saw_root_auth_absent = true;
1442 }
1443 _ => {
1444 return Err(FormatError::InvalidArchive(
1445 "root-auth footer terminal state is inconsistent",
1446 ));
1447 }
1448 }
1449
1450 for (block_index, record) in &parsed.blocks {
1451 if blocks.insert(*block_index, record.clone()).is_some() {
1452 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
1453 }
1454 }
1455 for block_index in &parsed.erased_block_indices {
1456 erased_block_indices.insert(*block_index);
1457 }
1458
1459 if first.is_none() {
1460 first = Some(parsed);
1461 }
1462 }
1463
1464 let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
1465 let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
1466 Some(err) => err,
1467 None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1468 })?;
1469 let authority_volume_header = manifest_authority_volume_header.ok_or(
1470 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1471 )?;
1472 let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
1473 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1474 )?;
1475 let observed_volume_count = u32::try_from(seen_volume_indexes.len())
1476 .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
1477 let missing_volume_count = first
1478 .crypto_header
1479 .stripe_width
1480 .checked_sub(observed_volume_count)
1481 .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1482 if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
1483 return Err(FormatError::InvalidArchive(
1484 "missing volume count exceeds volume_loss_tolerance",
1485 ));
1486 }
1487 if seen_volume_indexes.len() == first.crypto_header.stripe_width as usize {
1488 validate_complete_global_block_coverage(&blocks, &erased_block_indices)?;
1489 }
1490
1491 let limits = metadata_limits(&first.crypto_header);
1492 let index_root_plaintext = load_metadata_object_from_parts(
1493 &blocks,
1494 ObjectLoadContext::index_root(
1495 &first.volume_header,
1496 &first.crypto_header,
1497 &first.subkeys,
1498 ObjectExtent {
1499 first_block_index: manifest_footer.index_root_first_block,
1500 data_block_count: manifest_footer.index_root_data_block_count,
1501 parity_block_count: manifest_footer.index_root_parity_block_count,
1502 encrypted_size: manifest_footer.index_root_encrypted_size,
1503 },
1504 ),
1505 manifest_footer.index_root_decompressed_size,
1506 )?;
1507 let index_root = IndexRoot::parse(
1508 &index_root_plaintext,
1509 first.crypto_header.has_dictionary != 0,
1510 limits,
1511 )?;
1512 let payload_dictionary = load_archive_dictionary(
1513 &blocks,
1514 &first.subkeys,
1515 &first.volume_header,
1516 &first.crypto_header,
1517 &index_root,
1518 )?;
1519
1520 Ok(Self {
1521 options,
1522 observed_archive_bytes,
1523 observed_volume_count,
1524 subkeys: first.subkeys,
1525 blocks,
1526 lazy_blocks: None,
1527 crypto_header_bytes: first.crypto_header_bytes,
1528 volume_header: authority_volume_header,
1529 crypto_header: first.crypto_header,
1530 manifest_footer,
1531 volume_trailer: Some(authority_volume_trailer),
1532 root_auth_footer: root_auth_authority,
1533 index_root,
1534 payload_dictionary,
1535 })
1536 }
1537
1538 pub fn open_seekable_volumes_with_options<R: ArchiveReadAt>(
1539 readers: Vec<R>,
1540 master_key: &MasterKey,
1541 options: ReaderOptions,
1542 ) -> Result<Self, FormatError> {
1543 let readers = readers
1544 .into_iter()
1545 .map(|reader| Arc::new(reader) as Arc<dyn ArchiveReadAt>)
1546 .collect::<Vec<_>>();
1547 Self::open_seekable_volumes_with_options_for_mode(readers, master_key, options, None)
1548 }
1549
1550 fn open_seekable_volumes_with_options_for_mode(
1551 readers: Vec<Arc<dyn ArchiveReadAt>>,
1552 master_key: &MasterKey,
1553 options: ReaderOptions,
1554 bootstrap_sidecar: Option<&[u8]>,
1555 ) -> Result<Self, FormatError> {
1556 validate_reader_options(options)?;
1557 if readers.is_empty() {
1558 return Err(FormatError::InvalidArchive("no volumes supplied"));
1559 }
1560 if bootstrap_sidecar.is_some() && readers.len() > 1 {
1561 return Err(FormatError::ReaderUnsupported(
1562 "multi-volume inputs with bootstrap sidecar are not supported",
1563 ));
1564 }
1565
1566 let observed_archive_bytes = observed_archive_size(
1567 readers
1568 .iter()
1569 .map(|reader| reader.len())
1570 .collect::<Result<Vec<_>, _>>()?
1571 .into_iter()
1572 .chain(bootstrap_sidecar.map(|sidecar| sidecar.len() as u64)),
1573 )?;
1574 let mut first: Option<ParsedSeekableReadAtVolume> = None;
1575 let mut manifest_authority: Option<ManifestFooter> = None;
1576 let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
1577 let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
1578 let mut root_auth_authority: Option<RootAuthFooterV1> = None;
1579 let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
1580 let mut saw_root_auth_absent = false;
1581 let mut first_manifest_footer_error: Option<FormatError> = None;
1582 let mut seen_volume_indexes = BTreeSet::new();
1583 let mut lazy_volume_slots: Vec<Option<SeekableVolumeSource>> = Vec::new();
1584
1585 for reader in readers {
1586 let mut parsed = parse_seekable_read_at_volume(reader, master_key, options)?;
1587 if bootstrap_sidecar.is_some() {
1588 validate_bootstrap_single_volume_input(
1589 &parsed.volume_header,
1590 &parsed.crypto_header,
1591 )?;
1592 }
1593 if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
1594 return Err(FormatError::InvalidArchive(
1595 "duplicate authenticated volume index",
1596 ));
1597 }
1598
1599 if let Some(first) = &first {
1600 validate_volume_set_member_metadata(
1601 &first.volume_header,
1602 &first.crypto_header,
1603 &first.crypto_header_bytes,
1604 &parsed.volume_header,
1605 &parsed.crypto_header,
1606 &parsed.crypto_header_bytes,
1607 )?;
1608 validate_key_wrap_table_bytes_match(
1609 &first.key_wrap_table_bytes,
1610 &parsed.key_wrap_table_bytes,
1611 )?;
1612 } else {
1613 lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
1614 }
1615
1616 if let Some(footer) = &parsed.manifest_footer {
1617 if let Some(authority) = &manifest_authority {
1618 if !manifest_bootstrap_fields_match(authority, footer) {
1619 return Err(FormatError::InvalidArchive(
1620 "ManifestFooter bootstrap fields differ",
1621 ));
1622 }
1623 } else {
1624 manifest_authority = Some(footer.clone());
1625 manifest_authority_volume_header = Some(parsed.volume_header.clone());
1626 manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
1627 }
1628 } else if first_manifest_footer_error.is_none() {
1629 first_manifest_footer_error = parsed.manifest_footer_error.take();
1630 }
1631
1632 match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
1633 (Some(footer), Some(bytes)) => {
1634 if saw_root_auth_absent {
1635 return Err(FormatError::InvalidArchive(
1636 "root-auth footer presence differs across volumes",
1637 ));
1638 }
1639 if let Some(authority_bytes) = &root_auth_authority_bytes {
1640 if authority_bytes != bytes {
1641 return Err(FormatError::InvalidArchive(
1642 "RootAuthFooter copies differ",
1643 ));
1644 }
1645 } else {
1646 root_auth_authority = Some(footer.clone());
1647 root_auth_authority_bytes = Some(bytes.clone());
1648 }
1649 }
1650 (None, None) => {
1651 if root_auth_authority_bytes.is_some() {
1652 return Err(FormatError::InvalidArchive(
1653 "root-auth footer presence differs across volumes",
1654 ));
1655 }
1656 saw_root_auth_absent = true;
1657 }
1658 _ => {
1659 return Err(FormatError::InvalidArchive(
1660 "root-auth footer terminal state is inconsistent",
1661 ));
1662 }
1663 }
1664
1665 let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
1666 let source = SeekableVolumeSource {
1667 reader: parsed.reader.clone(),
1668 volume_index: parsed.volume_header.volume_index,
1669 block_records_start: parsed.block_records_start,
1670 block_count: parsed.volume_trailer.block_count,
1671 record_len,
1672 block_size: parsed.crypto_header.block_size as usize,
1673 };
1674 let slot = parsed.volume_header.volume_index as usize;
1675 if slot >= lazy_volume_slots.len() || lazy_volume_slots[slot].replace(source).is_some()
1676 {
1677 return Err(FormatError::InvalidArchive(
1678 "duplicate authenticated volume index",
1679 ));
1680 }
1681
1682 if first.is_none() {
1683 first = Some(parsed);
1684 }
1685 }
1686
1687 let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
1688 let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
1689 Some(err) => err,
1690 None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1691 })?;
1692 let authority_volume_header = manifest_authority_volume_header.ok_or(
1693 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1694 )?;
1695 let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
1696 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1697 )?;
1698 let observed_volume_count = u32::try_from(seen_volume_indexes.len())
1699 .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
1700 let missing_volume_count = first
1701 .crypto_header
1702 .stripe_width
1703 .checked_sub(observed_volume_count)
1704 .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1705 if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
1706 return Err(FormatError::InvalidArchive(
1707 "missing volume count exceeds volume_loss_tolerance",
1708 ));
1709 }
1710
1711 let mut blocks = BTreeMap::new();
1712 let sidecar = if let Some(bytes) = bootstrap_sidecar {
1713 let sidecar = parse_bootstrap_sidecar(
1714 bytes,
1715 &first.volume_header,
1716 &first.crypto_header,
1717 &first.subkeys,
1718 )?;
1719 sidecar
1720 .require_sections_for(BootstrapSidecarUse::SeekableAssist, &first.crypto_header)?;
1721 if let Some(sidecar_manifest) = &sidecar.manifest_footer {
1722 if !manifest_bootstrap_fields_match(&manifest_footer, sidecar_manifest) {
1723 return Err(FormatError::InvalidArchive(
1724 "bootstrap sidecar conflicts with terminal ManifestFooter",
1725 ));
1726 }
1727 }
1728 Some((bytes, sidecar))
1729 } else {
1730 None
1731 };
1732
1733 if let Some((sidecar_bytes, sidecar)) = &sidecar {
1734 if let Some((offset, length)) = sidecar.index_root_records_section {
1735 let index_root_records = parse_sidecar_block_records(
1736 sidecar_bytes,
1737 first.crypto_header.block_size as usize,
1738 SidecarBlockRecordsSection {
1739 offset,
1740 length,
1741 extent: index_root_extent_from_manifest(&manifest_footer),
1742 data_kind: BlockKind::IndexRootData,
1743 parity_kind: BlockKind::IndexRootParity,
1744 structure: "IndexRoot",
1745 },
1746 )?;
1747 insert_sidecar_records(&mut blocks, index_root_records)?;
1748 }
1749 }
1750
1751 let lazy_source = Arc::new(SeekableBlockSource {
1752 stripe_width: first.crypto_header.stripe_width,
1753 volumes: lazy_volume_slots,
1754 });
1755 let block_provider = OpenedBlockProvider {
1756 memory_blocks: &blocks,
1757 lazy_blocks: Some(lazy_source.as_ref()),
1758 };
1759 let limits = metadata_limits(&first.crypto_header);
1760 let index_root_plaintext = load_metadata_object_from_parts(
1761 &block_provider,
1762 ObjectLoadContext::index_root(
1763 &first.volume_header,
1764 &first.crypto_header,
1765 &first.subkeys,
1766 index_root_extent_from_manifest(&manifest_footer),
1767 ),
1768 manifest_footer.index_root_decompressed_size,
1769 )?;
1770 let index_root = IndexRoot::parse(
1771 &index_root_plaintext,
1772 first.crypto_header.has_dictionary != 0,
1773 limits,
1774 )?;
1775 if first.crypto_header.has_dictionary != 0 {
1776 if let Some((sidecar_bytes, sidecar)) = &sidecar {
1777 if let Some((offset, length)) = sidecar.dictionary_records_section {
1778 let dictionary_records = parse_sidecar_block_records(
1779 sidecar_bytes,
1780 first.crypto_header.block_size as usize,
1781 SidecarBlockRecordsSection {
1782 offset,
1783 length,
1784 extent: dictionary_extent_from_index_root(&index_root)?,
1785 data_kind: BlockKind::DictionaryData,
1786 parity_kind: BlockKind::DictionaryParity,
1787 structure: "Dictionary",
1788 },
1789 )?;
1790 insert_sidecar_records(&mut blocks, dictionary_records)?;
1791 }
1792 }
1793 }
1794 let block_provider = OpenedBlockProvider {
1795 memory_blocks: &blocks,
1796 lazy_blocks: Some(lazy_source.as_ref()),
1797 };
1798 let payload_dictionary = load_archive_dictionary(
1799 &block_provider,
1800 &first.subkeys,
1801 &first.volume_header,
1802 &first.crypto_header,
1803 &index_root,
1804 )?;
1805
1806 Ok(Self {
1807 options,
1808 observed_archive_bytes,
1809 observed_volume_count,
1810 subkeys: first.subkeys,
1811 blocks,
1812 lazy_blocks: Some(lazy_source),
1813 crypto_header_bytes: first.crypto_header_bytes,
1814 volume_header: authority_volume_header,
1815 crypto_header: first.crypto_header,
1816 manifest_footer,
1817 volume_trailer: Some(authority_volume_trailer),
1818 root_auth_footer: root_auth_authority,
1819 index_root,
1820 payload_dictionary,
1821 })
1822 }
1823
1824 pub fn open_with_bootstrap_sidecar_options(
1825 bytes: &[u8],
1826 bootstrap_sidecar: &[u8],
1827 master_key: &MasterKey,
1828 options: ReaderOptions,
1829 ) -> Result<Self, FormatError> {
1830 Self::open_with_bootstrap_sidecar_options_for_mode(
1831 bytes,
1832 bootstrap_sidecar,
1833 master_key,
1834 options,
1835 BootstrapSidecarUse::SeekableAssist,
1836 )
1837 }
1838
1839 fn open_with_bootstrap_sidecar_options_for_mode(
1840 bytes: &[u8],
1841 bootstrap_sidecar: &[u8],
1842 master_key: &MasterKey,
1843 options: ReaderOptions,
1844 sidecar_use: BootstrapSidecarUse,
1845 ) -> Result<Self, FormatError> {
1846 let observed_archive_bytes =
1847 observed_archive_size([bytes.len() as u64, bootstrap_sidecar.len() as u64])?;
1848 if bytes.len() < VOLUME_HEADER_LEN {
1849 return Err(FormatError::InvalidLength {
1850 structure: "archive",
1851 expected: VOLUME_HEADER_LEN,
1852 actual: bytes.len(),
1853 });
1854 }
1855
1856 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
1857 parse_volume_format_dispatch(&volume_header)?;
1858 let crypto_start = volume_header.crypto_header_offset as usize;
1859 let crypto_len = volume_header.crypto_header_length as usize;
1860 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
1861 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
1862 let subkeys = subkeys_for_open(
1863 Some(master_key),
1864 parsed_crypto.fixed.aead_algo,
1865 &volume_header.archive_uuid,
1866 &volume_header.session_id,
1867 )?;
1868 verify_integrity_tag(
1869 HmacDomain::CryptoHeader,
1870 parsed_crypto.fixed.aead_algo,
1871 volume_header.volume_format_rev,
1872 Some(&subkeys.mac_key),
1873 &volume_header.archive_uuid,
1874 &volume_header.session_id,
1875 parsed_crypto.hmac_covered_bytes,
1876 &parsed_crypto.header_hmac,
1877 )?;
1878 parsed_crypto.validate_extension_semantics()?;
1879 reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
1880 validate_bootstrap_single_volume_input(&volume_header, &parsed_crypto.fixed)?;
1881 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
1882
1883 let sidecar = parse_bootstrap_sidecar(
1884 bootstrap_sidecar,
1885 &volume_header,
1886 &parsed_crypto.fixed,
1887 &subkeys,
1888 )?;
1889 sidecar.require_sections_for(sidecar_use, &parsed_crypto.fixed)?;
1890 let block_records_start = startup_block_records_start(
1891 &volume_header,
1892 &parsed_crypto.kdf_params,
1893 |start, length| {
1894 let start = to_usize(start, "KeyWrapTableV1")?;
1895 Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
1896 },
1897 )?;
1898
1899 let (mut blocks, terminal_offset, observed_block_count) = parse_stream_block_prefix(
1900 bytes,
1901 to_usize(block_records_start, "BlockRecord")?,
1902 parsed_crypto.fixed.block_size as usize,
1903 &volume_header,
1904 )?;
1905 let terminal_material = match sidecar_use {
1906 BootstrapSidecarUse::SeekableAssist => Some(parse_terminal_material(
1907 bytes,
1908 terminal_offset,
1909 observed_block_count,
1910 KeyHoldingTerminalContext {
1911 subkeys: &subkeys,
1912 volume_header: &volume_header,
1913 crypto_header: &parsed_crypto.fixed,
1914 crypto_header_bytes: crypto_bytes,
1915 },
1916 options,
1917 )?),
1918 BootstrapSidecarUse::NonSeekableRandomAccess => parse_terminal_material(
1919 bytes,
1920 terminal_offset,
1921 observed_block_count,
1922 KeyHoldingTerminalContext {
1923 subkeys: &subkeys,
1924 volume_header: &volume_header,
1925 crypto_header: &parsed_crypto.fixed,
1926 crypto_header_bytes: crypto_bytes,
1927 },
1928 options,
1929 )
1930 .ok(),
1931 };
1932 let terminal_manifest = terminal_material.as_ref().map(|(manifest, _, _)| manifest);
1933 let manifest_authority = match sidecar_use {
1934 BootstrapSidecarUse::SeekableAssist => {
1935 let terminal_manifest = terminal_manifest.ok_or(FormatError::InvalidArchive(
1936 "terminal ManifestFooter/VolumeTrailer is required",
1937 ))?;
1938 if let Some(sidecar_manifest) = &sidecar.manifest_footer {
1939 if !manifest_bootstrap_fields_match(terminal_manifest, sidecar_manifest) {
1940 return Err(FormatError::InvalidArchive(
1941 "bootstrap sidecar conflicts with terminal ManifestFooter",
1942 ));
1943 }
1944 }
1945 terminal_manifest.clone()
1946 }
1947 BootstrapSidecarUse::NonSeekableRandomAccess => {
1948 let sidecar_manifest = sidecar
1949 .manifest_footer
1950 .as_ref()
1951 .ok_or(FormatError::ReaderUnsupported(
1952 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
1953 ))?;
1954 if let Some(terminal_manifest) = terminal_manifest {
1955 if !manifest_bootstrap_fields_match(terminal_manifest, sidecar_manifest) {
1956 return Err(FormatError::InvalidArchive(
1957 "bootstrap sidecar conflicts with terminal ManifestFooter",
1958 ));
1959 }
1960 }
1961 sidecar_manifest.clone()
1962 }
1963 };
1964 manifest_authority.validate_index_root_extent(parsed_crypto.fixed.block_size)?;
1965
1966 if let Some((offset, length)) = sidecar.index_root_records_section {
1967 let index_root_records = parse_sidecar_block_records(
1968 bootstrap_sidecar,
1969 parsed_crypto.fixed.block_size as usize,
1970 SidecarBlockRecordsSection {
1971 offset,
1972 length,
1973 extent: index_root_extent_from_manifest(&manifest_authority),
1974 data_kind: BlockKind::IndexRootData,
1975 parity_kind: BlockKind::IndexRootParity,
1976 structure: "IndexRoot",
1977 },
1978 )?;
1979 insert_sidecar_records(&mut blocks, index_root_records)?;
1980 }
1981
1982 let limits = metadata_limits(&parsed_crypto.fixed);
1983 let index_root_plaintext = load_metadata_object_from_parts(
1984 &blocks,
1985 ObjectLoadContext::index_root(
1986 &volume_header,
1987 &parsed_crypto.fixed,
1988 &subkeys,
1989 index_root_extent_from_manifest(&manifest_authority),
1990 ),
1991 manifest_authority.index_root_decompressed_size,
1992 )?;
1993 let index_root = IndexRoot::parse(
1994 &index_root_plaintext,
1995 parsed_crypto.fixed.has_dictionary != 0,
1996 limits,
1997 )?;
1998 if parsed_crypto.fixed.has_dictionary != 0 {
1999 if let Some((offset, length)) = sidecar.dictionary_records_section {
2000 let dictionary_records = parse_sidecar_block_records(
2001 bootstrap_sidecar,
2002 parsed_crypto.fixed.block_size as usize,
2003 SidecarBlockRecordsSection {
2004 offset,
2005 length,
2006 extent: dictionary_extent_from_index_root(&index_root)?,
2007 data_kind: BlockKind::DictionaryData,
2008 parity_kind: BlockKind::DictionaryParity,
2009 structure: "dictionary",
2010 },
2011 )?;
2012 insert_sidecar_records(&mut blocks, dictionary_records)?;
2013 }
2014 }
2015 let payload_dictionary = load_archive_dictionary(
2016 &blocks,
2017 &subkeys,
2018 &volume_header,
2019 &parsed_crypto.fixed,
2020 &index_root,
2021 )?;
2022
2023 Ok(Self {
2024 options,
2025 observed_archive_bytes,
2026 observed_volume_count: 1,
2027 subkeys,
2028 blocks,
2029 lazy_blocks: None,
2030 crypto_header_bytes: crypto_bytes.to_vec(),
2031 volume_header,
2032 crypto_header: parsed_crypto.fixed,
2033 manifest_footer: manifest_authority,
2034 volume_trailer: terminal_material
2035 .as_ref()
2036 .map(|(_, trailer, _)| trailer.clone()),
2037 root_auth_footer: terminal_material.and_then(|(_, _, root_auth)| root_auth),
2038 index_root,
2039 payload_dictionary,
2040 })
2041 }
2042
2043 pub fn list_index_entries(&self) -> Result<Vec<ArchiveIndexEntry>, FormatError> {
2049 let shards = self.load_all_index_shards()?;
2050 final_index_entry_winners(&shards)?
2051 .into_iter()
2052 .map(|(path, winner)| {
2053 archive_index_entry_from_loaded_file_with_path(
2054 path,
2055 &shards[winner.shard_index],
2056 winner.file_index,
2057 )
2058 })
2059 .collect()
2060 }
2061
2062 pub fn lookup_index_entry(&self, path: &str) -> Result<Option<ArchiveIndexEntry>, FormatError> {
2064 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2065 self.locate_index_file(&normalized)?
2066 .map(|located| archive_index_entry_from_loaded_file(&located.shard, located.file_index))
2067 .transpose()
2068 }
2069
2070 pub fn list_files(&self) -> Result<Vec<ArchiveEntry>, FormatError> {
2071 let shards = self.load_all_index_shards()?;
2072 final_index_entry_winners(&shards)?
2073 .into_iter()
2074 .map(|(path, winner)| {
2075 let shard = &shards[winner.shard_index];
2076 let member =
2077 self.decode_loaded_owned_tar_member(shard, winner.file_index, false)?;
2078 let mtime = member
2079 .v45_metadata
2080 .as_ref()
2081 .ok_or(FormatError::InvalidArchive(
2082 "revision-45 member metadata is missing",
2083 ))?
2084 .portable_mirror
2085 .mtime;
2086 Ok(ArchiveEntry {
2087 path,
2088 file_data_size: winner.file_data_size,
2089 kind: member.kind,
2090 mode: member.mode,
2091 mtime: ArchiveTimestamp::new(mtime.0, mtime.1),
2092 diagnostics: member.diagnostics,
2093 })
2094 })
2095 .collect()
2096 }
2097
2098 pub fn plan_metadata_restore(
2101 &self,
2102 options: SafeExtractionOptions,
2103 ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
2104 let shards = self.load_all_index_shards()?;
2105 let mut planned = Vec::new();
2106 for (path, winner) in final_index_entry_winners(&shards)? {
2107 let member = self.decode_loaded_owned_tar_member(
2108 &shards[winner.shard_index],
2109 winner.file_index,
2110 false,
2111 )?;
2112 planned.push((path, member));
2113 }
2114 let members: Vec<_> = planned.iter().map(|(_, member)| member).collect();
2115 validate_owned_restore_plan(&members, options)?;
2116 planned
2117 .into_iter()
2118 .map(|(path, member)| Ok((path, plan_owned_member_restore(&member, options)?)))
2119 .collect()
2120 }
2121
2122 pub fn extract_file(&self, path: &str) -> Result<Option<Vec<u8>>, FormatError> {
2129 self.extract_member(path)?
2130 .map(|member| {
2131 if member.kind != TarEntryKind::Regular || member.reparse_placeholder {
2132 return Err(FormatError::ReaderUnsupported(
2133 "extract_file returns only regular file payloads",
2134 ));
2135 }
2136 Ok(member.data)
2137 })
2138 .transpose()
2139 }
2140
2141 pub fn extract_file_with_diagnostics(
2144 &self,
2145 path: &str,
2146 ) -> Result<Option<ExtractedRegularFile>, FormatError> {
2147 self.extract_member(path)?
2148 .map(|member| {
2149 if member.kind != TarEntryKind::Regular || member.reparse_placeholder {
2150 return Err(FormatError::ReaderUnsupported(
2151 "extract_file_with_diagnostics returns only regular file payloads",
2152 ));
2153 }
2154 Ok((member.data, member.diagnostics))
2155 })
2156 .transpose()
2157 }
2158
2159 pub fn extract_file_to_writer<W: Write>(
2165 &self,
2166 path: &str,
2167 writer: &mut W,
2168 ) -> Result<Option<Vec<MetadataDiagnostic>>, ExtractError> {
2169 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2170 self.locate_index_file(&normalized)?
2171 .map(|located| {
2172 self.stream_loaded_file_to_writer(&located.shard, located.file_index, writer)
2173 })
2174 .transpose()
2175 }
2176
2177 pub fn extract_file_to_writer_with_progress<W: Write>(
2180 &self,
2181 path: &str,
2182 writer: &mut W,
2183 progress: &mut dyn ArchiveExtractProgressSink,
2184 ) -> Result<Option<Vec<MetadataDiagnostic>>, ExtractError> {
2185 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2186 self.locate_index_file(&normalized)?
2187 .map(|located| {
2188 self.stream_loaded_file_to_writer_with_progress(
2189 &located.shard,
2190 located.file_index,
2191 writer,
2192 progress,
2193 )
2194 })
2195 .transpose()
2196 }
2197
2198 pub fn restore_file_metadata_to_open_file(
2201 &self,
2202 path: &str,
2203 file: &File,
2204 options: SafeExtractionOptions,
2205 ) -> Result<Option<Vec<MetadataDiagnostic>>, FormatError> {
2206 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2207 let normalized_path = std::str::from_utf8(&normalized)
2208 .map_err(|_| FormatError::UnsafeArchivePath)?
2209 .to_owned();
2210 let shards = self.load_all_index_shards()?;
2211 let winners = final_index_entry_winners(&shards)?;
2212 let Some(requested) = winners.get(&normalized_path).copied() else {
2213 return Ok(None);
2214 };
2215 let member = self.decode_loaded_owned_tar_member(
2216 &shards[requested.shard_index],
2217 requested.file_index,
2218 false,
2219 )?;
2220 restore_regular_file_metadata_to_open_file(file, &member, options).map(Some)
2221 }
2222
2223 pub fn extract_member(
2224 &self,
2225 path: &str,
2226 ) -> Result<Option<ExtractedArchiveMember>, FormatError> {
2227 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2228 self.locate_index_file(&normalized)?
2229 .map(|located| self.extract_loaded_member(&located.shard, located.file_index))
2230 .transpose()
2231 }
2232
2233 pub fn extract_file_to(
2234 &self,
2235 path: &str,
2236 root: &std::path::Path,
2237 options: SafeExtractionOptions,
2238 ) -> Result<Option<Vec<MetadataDiagnostic>>, FormatError> {
2239 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2240 let normalized_path = std::str::from_utf8(&normalized)
2241 .map_err(|_| FormatError::UnsafeArchivePath)?
2242 .to_owned();
2243 let shards = self.load_all_index_shards()?;
2244 let winners = final_index_entry_winners(&shards)?;
2245 let Some(requested) = winners.get(&normalized_path).copied() else {
2246 return Ok(None);
2247 };
2248 let requested_member = self.decode_loaded_owned_tar_member(
2249 &shards[requested.shard_index],
2250 requested.file_index,
2251 false,
2252 )?;
2253 let mut entries = Vec::with_capacity(2);
2254 if requested_member.kind == TarEntryKind::Hardlink {
2255 let target = requested_member
2256 .link_target
2257 .as_deref()
2258 .ok_or(FormatError::InvalidArchive("hardlink target is missing"))?;
2259 let target = std::str::from_utf8(target)
2260 .map_err(|_| FormatError::UnsafeArchivePath)?
2261 .to_owned();
2262 let target_entry = winners
2263 .get(&target)
2264 .copied()
2265 .ok_or(FormatError::InvalidArchive(
2266 "hardlink target is absent from the final index",
2267 ))?;
2268 entries.push((target, target_entry));
2269 }
2270 entries.push((normalized_path.clone(), requested));
2271 let restored = self.extract_winning_index_entries_to(&shards, entries, root, options, 1)?;
2272 restored
2273 .into_iter()
2274 .find_map(|(entry_path, diagnostics)| {
2275 (entry_path == normalized_path).then_some(diagnostics)
2276 })
2277 .map(Some)
2278 .ok_or(FormatError::InvalidArchive(
2279 "selected restore result is missing",
2280 ))
2281 }
2282
2283 pub fn extract_indexed_files_to(
2284 &self,
2285 root: &std::path::Path,
2286 options: SafeExtractionOptions,
2287 jobs: usize,
2288 ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
2289 if jobs == 0 {
2290 return Err(FormatError::ReaderUnsupported("jobs must be at least 1"));
2291 }
2292
2293 let shards = self.load_all_index_shards()?;
2294 let mut entries = final_index_entry_winners(&shards)?
2295 .into_iter()
2296 .collect::<Vec<_>>();
2297 entries.sort_by_key(|(_, entry)| entry.start);
2298 self.extract_winning_index_entries_to(&shards, entries, root, options, jobs)
2299 }
2300
2301 pub fn extract_selected_files_to(
2307 &self,
2308 paths: &[String],
2309 root: &std::path::Path,
2310 options: SafeExtractionOptions,
2311 jobs: usize,
2312 ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
2313 if jobs == 0 {
2314 return Err(FormatError::ReaderUnsupported("jobs must be at least 1"));
2315 }
2316
2317 let shards = self.load_all_index_shards()?;
2318 let winners = final_index_entry_winners(&shards)?;
2319 let mut selected = BTreeMap::new();
2320 for path in paths {
2321 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2322 let normalized = std::str::from_utf8(&normalized)
2323 .map_err(|_| FormatError::UnsafeArchivePath)?
2324 .to_owned();
2325 let entry = winners
2326 .get(&normalized)
2327 .copied()
2328 .ok_or(FormatError::ReaderUnsupported(
2329 "selected archive path is absent from the final index",
2330 ))?;
2331 selected.insert(normalized, entry);
2332 }
2333
2334 let requested = selected
2335 .iter()
2336 .map(|(path, entry)| (path.clone(), *entry))
2337 .collect::<Vec<_>>();
2338 for (_, entry) in requested {
2339 let member = self.decode_loaded_owned_tar_member(
2340 &shards[entry.shard_index],
2341 entry.file_index,
2342 false,
2343 )?;
2344 if member.kind != TarEntryKind::Hardlink {
2345 continue;
2346 }
2347 let target = member
2348 .link_target
2349 .as_deref()
2350 .ok_or(FormatError::InvalidArchive("hardlink target is missing"))?;
2351 let target = std::str::from_utf8(target)
2352 .map_err(|_| FormatError::UnsafeArchivePath)?
2353 .to_owned();
2354 let target_entry = winners
2355 .get(&target)
2356 .copied()
2357 .ok_or(FormatError::InvalidArchive(
2358 "hardlink target is absent from the final index",
2359 ))?;
2360 selected.entry(target).or_insert(target_entry);
2361 }
2362
2363 let mut entries = selected.into_iter().collect::<Vec<_>>();
2364 entries.sort_by_key(|(_, entry)| entry.start);
2365 self.extract_winning_index_entries_to(&shards, entries, root, options, jobs)
2366 }
2367
2368 pub fn verify(&self) -> Result<(), FormatError> {
2369 self.verify_content().map(|_| ())
2370 }
2371
2372 pub fn verify_content(&self) -> Result<ArchiveContentVerification<'_>, FormatError> {
2373 self.verify_content_with_parity_policy(
2374 ParityReadPolicy::Always,
2375 ContentVerificationMode::Full,
2376 )
2377 }
2378
2379 pub fn verify_content_fast(&self) -> Result<ArchiveContentVerification<'_>, FormatError> {
2380 if self.fast_verify_defers_payload_semantics() {
2381 self.verify_payload_record_integrity_only()?;
2382 return Ok(ArchiveContentVerification {
2383 archive: self,
2384 mode: ContentVerificationMode::Fast,
2385 metadata_report: None,
2386 });
2387 }
2388 self.verify_content_with_parity_policy(
2389 ParityReadPolicy::RepairOnly,
2390 ContentVerificationMode::Fast,
2391 )
2392 }
2393
2394 pub fn fast_verify_defers_payload_semantics(&self) -> bool {
2395 self.root_auth_footer.is_none()
2396 && self.crypto_header.has_dictionary == 0
2397 && !self.crypto_header.aead_algo.is_encrypted()
2398 && self.crypto_header.fec_parity_shards == 0
2399 && self.crypto_header.index_fec_parity_shards == 0
2400 && self.crypto_header.index_root_fec_parity_shards == 0
2401 && self.manifest_footer.index_root_parity_block_count == 0
2402 }
2403
2404 fn verify_payload_record_integrity_only(&self) -> Result<(), FormatError> {
2405 let tables = self.load_payload_index_tables()?;
2406 let block_provider = self.block_provider();
2407 let block_size = self.crypto_header.block_size as u64;
2408 for envelope in tables.envelopes.values() {
2409 if envelope.parity_block_count != 0 {
2410 return Err(FormatError::InvalidArchive(
2411 "fast payload record scan requires zero parity",
2412 ));
2413 }
2414 let expected_encrypted_size = checked_u64_mul(
2415 envelope.data_block_count as u64,
2416 block_size,
2417 "payload envelope encrypted size",
2418 )?;
2419 if envelope.encrypted_size as u64 != expected_encrypted_size {
2420 return Err(FormatError::InvalidArchive(
2421 "payload envelope encrypted_size mismatch",
2422 ));
2423 }
2424 for offset in 0..envelope.data_block_count {
2425 let block_index =
2426 checked_u64_add(envelope.first_block_index, offset as u64, "payload")?;
2427 let record = block_provider
2428 .block(block_index)?
2429 .ok_or(FormatError::InvalidArchive("payload data block is missing"))?;
2430 if record.kind != BlockKind::PayloadData {
2431 return Err(FormatError::InvalidArchive(
2432 "payload data block has unexpected kind",
2433 ));
2434 }
2435 let should_be_last = offset + 1 == envelope.data_block_count;
2436 if record.is_last_data() != should_be_last {
2437 return Err(FormatError::InvalidArchive(
2438 "payload last-data flag is not on the final data block",
2439 ));
2440 }
2441 }
2442 }
2443 Ok(())
2444 }
2445
2446 fn verify_content_with_parity_policy(
2447 &self,
2448 parity_policy: ParityReadPolicy,
2449 mode: ContentVerificationMode,
2450 ) -> Result<ArchiveContentVerification<'_>, FormatError> {
2451 let tables = self.load_payload_index_tables()?;
2452 let streamed = self.scan_seekable_payload(
2453 &tables,
2454 u64::MAX,
2455 NoopTarStreamObserver,
2456 true,
2457 parity_policy,
2458 )?;
2459 self.validate_streamed_payload_summary(&tables, &streamed, false, true)?;
2460 let metadata_report = metadata_verification_report(&streamed.tar.members)?;
2461 Ok(ArchiveContentVerification {
2462 archive: self,
2463 mode,
2464 metadata_report: Some(metadata_report),
2465 })
2466 }
2467
2468 pub fn repair_patches(&self) -> Result<Vec<ArchiveRepairPatch>, FormatError> {
2469 let lazy_source = self
2470 .lazy_blocks
2471 .as_ref()
2472 .ok_or(FormatError::ReaderUnsupported(
2473 "repair output requires seekable archive input",
2474 ))?;
2475 if !lazy_source.is_complete_volume_set() {
2476 return Err(FormatError::ReaderUnsupported(
2477 "repair output requires all archive volumes",
2478 ));
2479 }
2480
2481 let shards = self.load_all_index_shards()?;
2482 let rows = self.root_auth_fec_layout_rows(&shards)?;
2483 let block_provider = self.block_provider();
2484 let mut patches = BTreeMap::<u64, ArchiveRepairPatch>::new();
2485 for row in rows.into_iter().filter(|row| row.present) {
2486 self.collect_repair_patches_for_object(
2487 &block_provider,
2488 lazy_source,
2489 row,
2490 &mut patches,
2491 )?;
2492 }
2493 Ok(patches.into_values().collect())
2494 }
2495
2496 pub fn extract_all_to(
2497 &self,
2498 root: &std::path::Path,
2499 options: SafeExtractionOptions,
2500 ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
2501 let tables = self.load_payload_index_tables()?;
2502 if final_index_entry_winners(&tables.shards)?.len() as u64 != tables.file_count {
2503 return Err(FormatError::ReaderUnsupported(
2504 FAST_FULL_EXTRACT_UNIQUE_PATHS_UNSUPPORTED,
2505 ));
2506 }
2507
2508 let dry_run = self.scan_seekable_payload(
2509 &tables,
2510 total_extraction_size_cap(self.options, self.observed_archive_bytes),
2511 NoopTarStreamObserver,
2512 false,
2513 ParityReadPolicy::RepairOnly,
2514 )?;
2515 self.validate_streamed_payload_summary(&tables, &dry_run, true, false)?;
2516
2517 let observer = TarStreamFilesystemRestoreObserver::new(root, options);
2518 let streamed = self.scan_seekable_payload(
2519 &tables,
2520 total_extraction_size_cap(self.options, self.observed_archive_bytes),
2521 observer,
2522 false,
2523 ParityReadPolicy::RepairOnly,
2524 )?;
2525 streamed
2526 .tar
2527 .members
2528 .into_iter()
2529 .map(|member| Ok((utf8_path(&member.path)?, member.diagnostics)))
2530 .collect()
2531 }
2532
2533 fn collect_repair_patches_for_object(
2534 &self,
2535 blocks: &impl BlockProvider,
2536 source: &SeekableBlockSource,
2537 row: FecLayoutObjectRow,
2538 patches: &mut BTreeMap<u64, ArchiveRepairPatch>,
2539 ) -> Result<(), FormatError> {
2540 let (data_kind, parity_kind, data_max, parity_max) =
2541 self.fec_object_class_shape(row.object_class)?;
2542 let extent = ObjectExtent {
2543 first_block_index: row.first_block_index,
2544 data_block_count: row.data_block_count,
2545 parity_block_count: row.parity_block_count,
2546 encrypted_size: row.encrypted_size,
2547 };
2548 validate_object_extent(extent, &self.crypto_header, data_max, parity_max)?;
2549
2550 let block_size = self.crypto_header.block_size as usize;
2551 let data_count = extent.data_block_count as usize;
2552 let parity_count = extent.parity_block_count as usize;
2553 let mut data_shards = Vec::with_capacity(data_count);
2554 let mut parity_shards = Vec::with_capacity(parity_count);
2555
2556 for offset in 0..data_count {
2557 let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
2558 match blocks.block(block_index)? {
2559 Some(record) => {
2560 if record.kind != data_kind {
2561 return Err(FormatError::InvalidArchive(
2562 "object data block has unexpected kind",
2563 ));
2564 }
2565 let should_be_last = offset + 1 == data_count;
2566 if record.is_last_data() != should_be_last {
2567 return Err(FormatError::InvalidArchive(
2568 "object last-data flag is not on the final data block",
2569 ));
2570 }
2571 data_shards.push(Some(record.payload.clone()));
2572 }
2573 None => data_shards.push(None),
2574 }
2575 }
2576
2577 for offset in 0..parity_count {
2578 let block_index = checked_u64_add(
2579 extent.first_block_index,
2580 data_count as u64 + offset as u64,
2581 "object",
2582 )?;
2583 match blocks.block(block_index)? {
2584 Some(record) => {
2585 if record.kind != parity_kind {
2586 return Err(FormatError::InvalidArchive(
2587 "object parity block has unexpected kind",
2588 ));
2589 }
2590 if record.is_last_data() {
2591 return Err(FormatError::InvalidArchive(
2592 "object parity block has last-data flag",
2593 ));
2594 }
2595 parity_shards.push(Some(record.payload.clone()));
2596 }
2597 None => parity_shards.push(None),
2598 }
2599 }
2600
2601 let repaired_data = repair_data_gf16(&data_shards, &parity_shards, block_size)?;
2602 for (offset, payload) in repaired_data.iter().enumerate() {
2603 if data_shards[offset].is_none() {
2604 let block_index =
2605 checked_u64_add(extent.first_block_index, offset as u64, "object")?;
2606 let flags = if offset + 1 == data_count { 0x01 } else { 0 };
2607 self.insert_repair_patch(
2608 patches,
2609 source,
2610 block_index,
2611 data_kind,
2612 flags,
2613 payload.clone(),
2614 )?;
2615 }
2616 }
2617
2618 if parity_count > 0 {
2619 let repaired_parity = encode_parity_gf16(&repaired_data, parity_count)?;
2620 for (offset, payload) in repaired_parity.into_iter().enumerate() {
2621 if parity_shards[offset].as_ref() != Some(&payload) {
2622 let block_index = checked_u64_add(
2623 extent.first_block_index,
2624 data_count as u64 + offset as u64,
2625 "object",
2626 )?;
2627 self.insert_repair_patch(
2628 patches,
2629 source,
2630 block_index,
2631 parity_kind,
2632 0,
2633 payload,
2634 )?;
2635 }
2636 }
2637 }
2638
2639 Ok(())
2640 }
2641
2642 fn insert_repair_patch(
2643 &self,
2644 patches: &mut BTreeMap<u64, ArchiveRepairPatch>,
2645 source: &SeekableBlockSource,
2646 block_index: u64,
2647 kind: BlockKind,
2648 flags: u8,
2649 payload: Vec<u8>,
2650 ) -> Result<(), FormatError> {
2651 let (volume_index, record_offset) = source.record_location(block_index)?;
2652 let record = BlockRecord {
2653 block_index,
2654 kind,
2655 flags,
2656 payload,
2657 record_crc32c: 0,
2658 };
2659 let patch = ArchiveRepairPatch {
2660 volume_index,
2661 block_index,
2662 record_offset,
2663 record_bytes: record.to_bytes(),
2664 };
2665 if let Some(existing) = patches.insert(block_index, patch.clone()) {
2666 if existing != patch {
2667 return Err(FormatError::InvalidArchive(
2668 "conflicting repair patch for BlockRecord",
2669 ));
2670 }
2671 }
2672 Ok(())
2673 }
2674
2675 fn load_payload_index_tables(&self) -> Result<PayloadIndexTables, FormatError> {
2676 if self.index_root.header.file_count > DIRECTORY_HINT_REQUIRED_FILE_COUNT
2677 && self.index_root.directory_hint_shards.is_empty()
2678 {
2679 return Err(FormatError::InvalidArchive(
2680 "IndexRoot file_count requires directory hints",
2681 ));
2682 }
2683
2684 let shards = self.load_all_index_shards()?;
2685 let mut file_count = 0u64;
2686 let mut frames = BTreeMap::<u64, FrameEntry>::new();
2687 let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
2688
2689 for shard in &shards {
2690 file_count = file_count
2691 .checked_add(shard.files.len() as u64)
2692 .ok_or(FormatError::InvalidArchive("file count overflow"))?;
2693 for frame in &shard.frames {
2694 if let Some(existing) = frames.insert(frame.frame_index, frame.clone()) {
2695 if existing != *frame {
2696 return Err(FormatError::InvalidArchive(
2697 "duplicate FrameEntry rows do not match",
2698 ));
2699 }
2700 }
2701 }
2702 for envelope in &shard.envelopes {
2703 if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
2704 {
2705 if existing != *envelope {
2706 return Err(FormatError::InvalidArchive(
2707 "duplicate EnvelopeEntry rows do not match",
2708 ));
2709 }
2710 }
2711 }
2712 }
2713 validate_global_file_table_order(&shards)?;
2714
2715 if file_count != self.index_root.header.file_count {
2716 return Err(FormatError::InvalidArchive(
2717 "IndexRoot file_count does not match decoded shards",
2718 ));
2719 }
2720 verify_dense_keys(&frames, self.index_root.header.frame_count, "FrameEntry")?;
2721 verify_dense_keys(
2722 &envelopes,
2723 self.index_root.header.envelope_count,
2724 "EnvelopeEntry",
2725 )?;
2726 validate_envelope_frame_coverage(&frames, &envelopes)?;
2727 self.validate_encrypted_object_block_ranges(&envelopes)?;
2728
2729 let payload_block_count = envelopes.values().try_fold(0u64, |sum, envelope| {
2730 sum.checked_add(envelope.data_block_count as u64)
2731 .ok_or(FormatError::InvalidArchive("payload block count overflow"))
2732 })?;
2733 if payload_block_count != self.index_root.header.payload_block_count {
2734 return Err(FormatError::InvalidArchive(
2735 "IndexRoot payload_block_count does not match envelopes",
2736 ));
2737 }
2738
2739 Ok(PayloadIndexTables {
2740 shards,
2741 file_count,
2742 frames,
2743 envelopes,
2744 })
2745 }
2746
2747 fn scan_seekable_payload<O: TarStreamObserver>(
2748 &self,
2749 tables: &PayloadIndexTables,
2750 extraction_cap: u64,
2751 observer: O,
2752 hash_content: bool,
2753 parity_policy: ParityReadPolicy,
2754 ) -> Result<StreamedPayloadSummary, FormatError> {
2755 let mut tar = TarStreamSummaryValidator::with_observer(
2756 self.crypto_header.max_path_length,
2757 extraction_cap,
2758 usize::MAX,
2759 self.index_root.header.file_count,
2760 observer,
2761 );
2762 let mut content_hasher = hash_content.then(Sha256::new);
2763 let mut streamed_frames = Vec::with_capacity(tables.frames.len());
2764 let streamed_envelopes = tables
2765 .envelopes
2766 .values()
2767 .map(|envelope| StreamedEnvelopeSummary {
2768 envelope_index: envelope.envelope_index,
2769 first_block_index: envelope.first_block_index,
2770 data_block_count: envelope.data_block_count,
2771 parity_block_count: envelope.parity_block_count,
2772 encrypted_size: envelope.encrypted_size,
2773 plaintext_size: envelope.plaintext_size,
2774 first_frame_index: envelope.first_frame_index,
2775 frame_count: envelope.frame_count,
2776 })
2777 .collect::<Vec<_>>();
2778 let mut cached_envelope_index = None;
2779 let mut cached_envelope_plaintext = Vec::new();
2780 let mut decompressor = self.new_payload_decompressor()?;
2781
2782 for frame in tables.frames.values() {
2783 let envelope =
2784 tables
2785 .envelopes
2786 .get(&frame.envelope_index)
2787 .ok_or(FormatError::InvalidArchive(
2788 "FrameEntry references missing EnvelopeEntry",
2789 ))?;
2790 if cached_envelope_index != Some(envelope.envelope_index) {
2791 cached_envelope_plaintext = self.load_payload_envelope(envelope, parity_policy)?;
2792 cached_envelope_index = Some(envelope.envelope_index);
2793 }
2794 let compressed = slice(
2795 &cached_envelope_plaintext,
2796 frame.offset_in_envelope as usize,
2797 frame.compressed_size as usize,
2798 "FrameEntry",
2799 )?;
2800 let tar_stream_offset = tar.tar_total_size();
2801 let decoded = self.decompress_payload_frame_with(
2802 &mut decompressor,
2803 compressed,
2804 frame.decompressed_size,
2805 )?;
2806 if decoded.is_empty() {
2807 return Err(FormatError::InvalidArchive(
2808 "zstd payload frame decompressed to zero bytes",
2809 ));
2810 }
2811 if let Some(hasher) = &mut content_hasher {
2812 hasher.update(&decoded);
2813 }
2814 tar.observe(&decoded)?;
2815 streamed_frames.push(StreamedFrameSummary {
2816 frame_index: frame.frame_index,
2817 envelope_index: frame.envelope_index,
2818 offset_in_envelope: frame.offset_in_envelope,
2819 compressed_size: u32::try_from(compressed.len()).map_err(|_| {
2820 FormatError::InvalidArchive("FrameEntry.compressed_size overflow")
2821 })?,
2822 decompressed_size: u32::try_from(decoded.len()).map_err(|_| {
2823 FormatError::InvalidArchive("FrameEntry.decompressed_size overflow")
2824 })?,
2825 tar_stream_offset,
2826 });
2827 }
2828
2829 let mut content_sha256 = [0u8; 32];
2830 if let Some(hasher) = content_hasher {
2831 let digest = hasher.finalize();
2832 content_sha256.copy_from_slice(&digest);
2833 }
2834 Ok(StreamedPayloadSummary {
2835 tar: tar.finish()?,
2836 content_sha256,
2837 envelopes: streamed_envelopes,
2838 frames: streamed_frames,
2839 })
2840 }
2841
2842 fn validate_streamed_payload_summary(
2843 &self,
2844 tables: &PayloadIndexTables,
2845 streamed: &StreamedPayloadSummary,
2846 enforce_total_extraction_cap: bool,
2847 enforce_content_sha256: bool,
2848 ) -> Result<(), FormatError> {
2849 if enforce_total_extraction_cap
2850 && streamed.tar.total_extraction_size
2851 > total_extraction_size_cap(self.options, self.observed_archive_bytes)
2852 {
2853 return Err(FormatError::ReaderUnsupported(
2854 "total extraction size exceeds configured cap",
2855 ));
2856 }
2857
2858 let streamed_payload_block_count =
2859 streamed.envelopes.iter().try_fold(0u64, |sum, envelope| {
2860 sum.checked_add(envelope.data_block_count as u64)
2861 .ok_or(FormatError::InvalidArchive("payload block count overflow"))
2862 })?;
2863 if streamed_payload_block_count != self.index_root.header.payload_block_count {
2864 return Err(FormatError::InvalidArchive(
2865 "streamed payload block count does not match IndexRoot",
2866 ));
2867 }
2868
2869 if streamed.tar.tar_total_size != self.index_root.header.tar_total_size {
2870 return Err(FormatError::InvalidArchive(
2871 "IndexRoot tar_total_size does not match streamed tar stream",
2872 ));
2873 }
2874 if enforce_content_sha256
2875 && streamed.content_sha256 != self.index_root.header.content_sha256
2876 {
2877 return Err(FormatError::InvalidArchive(
2878 "IndexRoot content_sha256 does not match decoded tar stream",
2879 ));
2880 }
2881
2882 let streamed_envelopes = streamed.envelope_map()?;
2883 for envelope in tables.envelopes.values() {
2884 let actual = streamed_envelopes.get(&envelope.envelope_index).ok_or(
2885 FormatError::InvalidArchive(
2886 "metadata references missing streamed payload envelope",
2887 ),
2888 )?;
2889 if actual.first_block_index != envelope.first_block_index
2890 || actual.data_block_count != envelope.data_block_count
2891 || actual.parity_block_count != envelope.parity_block_count
2892 || actual.encrypted_size != envelope.encrypted_size
2893 || actual.plaintext_size != envelope.plaintext_size
2894 || actual.first_frame_index != envelope.first_frame_index
2895 || actual.frame_count != envelope.frame_count
2896 {
2897 return Err(FormatError::InvalidArchive(
2898 "EnvelopeEntry does not match streamed payload envelope",
2899 ));
2900 }
2901 }
2902
2903 let streamed_frames = streamed.frame_map()?;
2904 for frame in tables.frames.values() {
2905 let actual =
2906 streamed_frames
2907 .get(&frame.frame_index)
2908 .ok_or(FormatError::InvalidArchive(
2909 "metadata references missing streamed payload frame",
2910 ))?;
2911 if actual.envelope_index != frame.envelope_index
2912 || actual.offset_in_envelope != frame.offset_in_envelope
2913 || actual.compressed_size != frame.compressed_size
2914 || actual.decompressed_size != frame.decompressed_size
2915 || actual.tar_stream_offset != frame.tar_stream_offset
2916 || streamed.frame_flags(actual)? != frame.flags
2917 {
2918 return Err(FormatError::InvalidArchive(
2919 "FrameEntry does not match streamed payload frame",
2920 ));
2921 }
2922 }
2923
2924 let streamed_members = streamed.member_start_map()?;
2925 if streamed.tar.members.len() as u64 != tables.file_count {
2926 return Err(FormatError::InvalidArchive(
2927 "streamed tar member count does not match decoded shards",
2928 ));
2929 }
2930 let mut file_extents = Vec::new();
2931 let mut directory_hint_map = DirectoryHintMap::new();
2932 for (shard_row_index, shard) in tables.shards.iter().enumerate() {
2933 let shard_row_index = u32::try_from(shard_row_index)
2934 .map_err(|_| FormatError::InvalidArchive("shard row index overflow"))?;
2935 for idx in 0..shard.files.len() {
2936 let file = &shard.files[idx];
2937 let start =
2938 shard
2939 .tar_member_group_start(idx)
2940 .ok_or(FormatError::InvalidArchive(
2941 "FileEntry tar member start is missing",
2942 ))?;
2943 file_extents.push((start, file.tar_member_group_size));
2944 let path = shard
2945 .file_path(idx)
2946 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2947 let member = streamed_members
2948 .get(&start)
2949 .ok_or(FormatError::InvalidArchive(
2950 "FileEntry tar member start is missing from streamed tar",
2951 ))?;
2952 if member.path != path {
2953 return Err(FormatError::InvalidArchive(
2954 "tar member path does not match FileEntry path",
2955 ));
2956 }
2957 if member.logical_size != file.file_data_size {
2958 return Err(FormatError::InvalidArchive(
2959 "tar member size does not match FileEntry file_data_size",
2960 ));
2961 }
2962 if member.file_entry_flags != file.flags {
2963 return Err(FormatError::InvalidArchive(
2964 "streamed tar member metadata flags do not match FileEntry flags",
2965 ));
2966 }
2967 if member.group_size != file.tar_member_group_size {
2968 return Err(FormatError::InvalidArchive(
2969 "FileEntry does not match streamed tar member",
2970 ));
2971 }
2972 add_expected_directory_hint_rows(
2973 &mut directory_hint_map,
2974 shard_row_index,
2975 path,
2976 member.kind,
2977 );
2978 }
2979 }
2980 validate_file_extent_coverage_ranges(&file_extents, self.index_root.header.tar_total_size)?;
2981 if !self.index_root.directory_hint_shards.is_empty() {
2982 let hint_tables = self.load_all_directory_hint_tables()?;
2983 validate_directory_hint_tables_against_expected(&hint_tables, &directory_hint_map)?;
2984 }
2985
2986 Ok(())
2987 }
2988
2989 pub(crate) fn from_streamed_parts(
2990 parts: StreamedArchiveOpenParts,
2991 ) -> Result<Self, FormatError> {
2992 let limits = metadata_limits(&parts.crypto_header);
2993 let index_root_plaintext = load_metadata_object_from_parts(
2994 &parts.blocks,
2995 ObjectLoadContext::index_root(
2996 &parts.volume_header,
2997 &parts.crypto_header,
2998 &parts.subkeys,
2999 ObjectExtent {
3000 first_block_index: parts.manifest_footer.index_root_first_block,
3001 data_block_count: parts.manifest_footer.index_root_data_block_count,
3002 parity_block_count: parts.manifest_footer.index_root_parity_block_count,
3003 encrypted_size: parts.manifest_footer.index_root_encrypted_size,
3004 },
3005 ),
3006 parts.manifest_footer.index_root_decompressed_size,
3007 )?;
3008 let index_root = IndexRoot::parse(
3009 &index_root_plaintext,
3010 parts.crypto_header.has_dictionary != 0,
3011 limits,
3012 )?;
3013 let payload_dictionary = load_archive_dictionary(
3014 &parts.blocks,
3015 &parts.subkeys,
3016 &parts.volume_header,
3017 &parts.crypto_header,
3018 &index_root,
3019 )?;
3020
3021 Ok(Self {
3022 options: parts.options,
3023 observed_archive_bytes: parts.observed_archive_bytes,
3024 observed_volume_count: 1,
3025 subkeys: parts.subkeys,
3026 blocks: parts.blocks,
3027 lazy_blocks: None,
3028 crypto_header_bytes: parts.crypto_header_bytes,
3029 volume_header: parts.volume_header,
3030 crypto_header: parts.crypto_header,
3031 manifest_footer: parts.manifest_footer,
3032 volume_trailer: Some(parts.volume_trailer),
3033 root_auth_footer: parts.root_auth_footer,
3034 index_root,
3035 payload_dictionary,
3036 })
3037 }
3038
3039 pub(crate) fn verify_streamed_payload_summary(
3040 &self,
3041 streamed: &StreamedPayloadSummary,
3042 ) -> Result<(), FormatError> {
3043 let tables = self.load_payload_index_tables()?;
3044 self.validate_streamed_payload_summary(&tables, streamed, true, true)
3045 }
3046
3047 pub fn verify_root_auth_with<F>(&self, verifier: F) -> Result<RootAuthVerification, FormatError>
3048 where
3049 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
3050 {
3051 let content_verification = self.verify_content()?;
3052 self.verify_root_auth_with_verified_content(&content_verification, verifier)
3053 }
3054
3055 pub fn verify_root_auth_with_verified_content<F>(
3056 &self,
3057 content_verification: &ArchiveContentVerification<'_>,
3058 mut verifier: F,
3059 ) -> Result<RootAuthVerification, FormatError>
3060 where
3061 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
3062 {
3063 if !std::ptr::eq(content_verification.archive, self) {
3064 return Err(FormatError::InvalidArchive(
3065 "content verification does not match archive",
3066 ));
3067 }
3068 if content_verification.mode != ContentVerificationMode::Full {
3069 return Err(FormatError::ReaderUnsupported(
3070 "RootAuth verification requires full archive content verification",
3071 ));
3072 }
3073 let footer = self
3074 .root_auth_footer
3075 .as_ref()
3076 .ok_or(FormatError::ReaderUnsupported("root-auth footer is absent"))?;
3077 let material = self.recompute_root_auth_material(footer)?;
3078 if material.critical_metadata_digest != footer.critical_metadata_digest
3079 || material.index_digest != footer.index_digest
3080 || material.fec_layout_digest != footer.fec_layout_digest
3081 || material.data_block_merkle_root != footer.data_block_merkle_root
3082 || material.signer_identity_digest != footer.signer_identity_digest
3083 || material.archive_root != footer.archive_root
3084 || material.total_data_block_count != footer.total_data_block_count
3085 {
3086 return Err(FormatError::InvalidArchive(
3087 "RootAuthFooter commitments do not match recomputed archive root",
3088 ));
3089 }
3090 if !verifier(footer, &material.archive_root)? {
3091 return Err(FormatError::InvalidArchive(
3092 "root-auth authenticator verification failed",
3093 ));
3094 }
3095 Ok(RootAuthVerification {
3096 format_version: footer.format_version,
3097 volume_format_rev: footer.volume_format_rev,
3098 archive_root: material.archive_root,
3099 authenticator_id: footer.authenticator_id,
3100 signer_identity_type: footer.signer_identity_type,
3101 signer_identity_bytes: footer.signer_identity_bytes.clone(),
3102 total_data_block_count: footer.total_data_block_count,
3103 diagnostics: self.root_auth_success_diagnostics(),
3104 })
3105 }
3106
3107 fn load_all_index_shards(&self) -> Result<Vec<IndexShard>, FormatError> {
3108 parallel_map_ref(&self.index_root.shards, self.options.jobs, |entry| {
3109 self.load_index_shard(entry)
3110 })
3111 }
3112
3113 fn load_index_shard(&self, entry: &ShardEntry) -> Result<IndexShard, FormatError> {
3114 let block_provider = self.block_provider();
3115 let plaintext = load_metadata_object_from_parts(
3116 &block_provider,
3117 ObjectLoadContext::index_shard(
3118 &self.volume_header,
3119 &self.crypto_header,
3120 &self.subkeys,
3121 entry,
3122 ),
3123 entry.decompressed_size,
3124 )?;
3125 IndexShard::parse(&plaintext, entry, self.metadata_limits())
3126 }
3127
3128 fn load_all_directory_hint_tables(&self) -> Result<Vec<DirectoryHintTable>, FormatError> {
3129 parallel_map_ref(
3130 &self.index_root.directory_hint_shards,
3131 self.options.jobs,
3132 |entry| self.load_directory_hint_table(entry),
3133 )
3134 }
3135
3136 fn load_directory_hint_table(
3137 &self,
3138 entry: &DirectoryHintShardEntry,
3139 ) -> Result<DirectoryHintTable, FormatError> {
3140 let block_provider = self.block_provider();
3141 let plaintext = load_metadata_object_from_parts(
3142 &block_provider,
3143 ObjectLoadContext::directory_hint(
3144 &self.volume_header,
3145 &self.crypto_header,
3146 &self.subkeys,
3147 entry,
3148 ),
3149 entry.decompressed_size,
3150 )?;
3151 DirectoryHintTable::parse(
3152 &plaintext,
3153 entry,
3154 self.index_root.header.shard_count,
3155 self.metadata_limits(),
3156 )
3157 }
3158
3159 fn load_payload_envelope(
3160 &self,
3161 envelope: &EnvelopeEntry,
3162 parity_policy: ParityReadPolicy,
3163 ) -> Result<Vec<u8>, FormatError> {
3164 let block_provider = self.block_provider();
3165 let plaintext = load_decrypted_object_from_parts_with_parity_policy(
3166 &block_provider,
3167 ObjectLoadContext::payload(
3168 &self.volume_header,
3169 &self.crypto_header,
3170 &self.subkeys,
3171 envelope,
3172 ),
3173 parity_policy,
3174 )?;
3175 if plaintext.len() != envelope.plaintext_size as usize {
3176 return Err(FormatError::InvalidArchive(
3177 "payload envelope plaintext_size mismatch",
3178 ));
3179 }
3180 Ok(plaintext)
3181 }
3182
3183 fn locate_index_file(
3184 &self,
3185 normalized: &[u8],
3186 ) -> Result<Option<LocatedIndexFile>, FormatError> {
3187 let candidate_indexes = self
3188 .index_root
3189 .candidate_shards_for_path(normalized, self.metadata_limits())?;
3190 let mut winner: Option<LocatedIndexFile> = None;
3191
3192 for row_index in candidate_indexes {
3193 let locating =
3194 self.index_root
3195 .shards
3196 .get(row_index)
3197 .ok_or(FormatError::InvalidArchive(
3198 "candidate shard row is out of bounds",
3199 ))?;
3200 let shard = self.load_index_shard(locating)?;
3201 if let Some(file_index) = shard.lookup_file_index(normalized) {
3202 let start =
3203 shard
3204 .tar_member_group_start(file_index)
3205 .ok_or(FormatError::InvalidArchive(
3206 "FileEntry tar member start is missing",
3207 ))?;
3208 if winner
3209 .as_ref()
3210 .map(|existing| start > existing.start)
3211 .unwrap_or(true)
3212 {
3213 winner = Some(LocatedIndexFile {
3214 shard,
3215 file_index,
3216 start,
3217 });
3218 }
3219 }
3220 }
3221
3222 Ok(winner)
3223 }
3224
3225 fn extract_loaded_member(
3226 &self,
3227 shard: &IndexShard,
3228 file_index: usize,
3229 ) -> Result<ExtractedArchiveMember, FormatError> {
3230 let member = self.extract_loaded_owned_tar_member(shard, file_index)?;
3231 Ok(ExtractedArchiveMember {
3232 path: utf8_path(&member.path)?,
3233 kind: member.kind,
3234 data: member.data,
3235 link_target: member
3236 .link_target
3237 .map(|target| utf8_path(&target))
3238 .transpose()?,
3239 reparse_placeholder: member.reparse_placeholder,
3240 diagnostics: member.diagnostics,
3241 })
3242 }
3243
3244 fn extract_loaded_owned_tar_member(
3245 &self,
3246 shard: &IndexShard,
3247 file_index: usize,
3248 ) -> Result<OwnedTarMember, FormatError> {
3249 self.decode_loaded_owned_tar_member(shard, file_index, true)
3250 }
3251
3252 fn stream_loaded_file_to_writer<W: Write>(
3253 &self,
3254 shard: &IndexShard,
3255 file_index: usize,
3256 writer: &mut W,
3257 ) -> Result<Vec<MetadataDiagnostic>, ExtractError> {
3258 let file = shard
3259 .files
3260 .get(file_index)
3261 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3262 self.validate_total_extraction_size(file.file_data_size)?;
3263 let expected_path = shard
3264 .file_path(file_index)
3265 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3266 let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
3267 stream_regular_tar_member_group_to_writer(
3268 &mut reader,
3269 expected_path,
3270 file.file_data_size,
3271 file.flags,
3272 file.tar_member_group_size,
3273 self.crypto_header.max_path_length,
3274 writer,
3275 )
3276 }
3277
3278 fn stream_loaded_file_to_writer_with_progress<W: Write>(
3279 &self,
3280 shard: &IndexShard,
3281 file_index: usize,
3282 writer: &mut W,
3283 progress: &mut dyn ArchiveExtractProgressSink,
3284 ) -> Result<Vec<MetadataDiagnostic>, ExtractError> {
3285 let file = shard
3286 .files
3287 .get(file_index)
3288 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3289 self.validate_total_extraction_size(file.file_data_size)?;
3290 let expected_path = shard
3291 .file_path(file_index)
3292 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3293 let archive_path = utf8_path(expected_path)?;
3294 let mut progress_writer =
3295 ExtractProgressWriter::new(writer, &archive_path, file.file_data_size, progress);
3296 let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
3297 stream_regular_tar_member_group_to_writer(
3298 &mut reader,
3299 expected_path,
3300 file.file_data_size,
3301 file.flags,
3302 file.tar_member_group_size,
3303 self.crypto_header.max_path_length,
3304 &mut progress_writer,
3305 )
3306 }
3307
3308 fn stream_loaded_file_to_path(
3309 &self,
3310 shard: &IndexShard,
3311 file_index: usize,
3312 root: &std::path::Path,
3313 options: SafeExtractionOptions,
3314 ) -> Result<Vec<MetadataDiagnostic>, FormatError> {
3315 let file = shard
3316 .files
3317 .get(file_index)
3318 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3319 self.validate_total_extraction_size(file.file_data_size)?;
3320 let expected_path = shard
3321 .file_path(file_index)
3322 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3323 let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
3324 restore_streaming_tar_member_group(
3325 root,
3326 StreamingMemberExpectation {
3327 path: expected_path,
3328 file_data_size: file.file_data_size,
3329 file_flags: file.flags,
3330 group_len: file.tar_member_group_size,
3331 max_path_length: self.crypto_header.max_path_length,
3332 },
3333 options,
3334 &mut reader,
3335 )
3336 .map_err(format_error_from_extract_error)
3337 }
3338
3339 fn extract_winning_index_entries_to(
3340 &self,
3341 shards: &[IndexShard],
3342 entries: Vec<(String, WinningIndexEntry)>,
3343 root: &std::path::Path,
3344 options: SafeExtractionOptions,
3345 jobs: usize,
3346 ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
3347 if entries.is_empty() {
3348 return Ok(Vec::new());
3349 }
3350 let metadata = parallel_map_ref(&entries, jobs, |(_, entry)| {
3351 let shard = shards
3352 .get(entry.shard_index)
3353 .ok_or(FormatError::InvalidArchive(
3354 "winning FileEntry shard is out of bounds",
3355 ))?;
3356 self.decode_loaded_owned_tar_member(shard, entry.file_index, false)
3357 })?;
3358 validate_owned_restore_plan(&metadata.iter().collect::<Vec<_>>(), options)?;
3359 let mut planned = entries
3360 .into_iter()
3361 .zip(metadata)
3362 .map(|((path, entry), member)| (path, entry, member))
3363 .collect::<Vec<_>>();
3364 planned.sort_by(|left, right| {
3365 restore_phase(&left.2)
3366 .cmp(&restore_phase(&right.2))
3367 .then_with(|| left.2.path.cmp(&right.2.path))
3368 });
3369 planned
3370 .into_iter()
3371 .map(|(path, entry, _)| {
3372 let shard = &shards[entry.shard_index];
3373 let diagnostics =
3374 self.stream_loaded_file_to_path(shard, entry.file_index, root, options)?;
3375 Ok((path, diagnostics))
3376 })
3377 .collect()
3378 }
3379
3380 fn decode_loaded_owned_tar_member(
3381 &self,
3382 shard: &IndexShard,
3383 file_index: usize,
3384 enforce_extraction_cap: bool,
3385 ) -> Result<OwnedTarMember, FormatError> {
3386 let file = shard
3387 .files
3388 .get(file_index)
3389 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3390 if enforce_extraction_cap {
3391 self.validate_total_extraction_size(file.file_data_size)?;
3392 }
3393 let expected_path = shard
3394 .file_path(file_index)
3395 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3396 let frames = frame_range_for_file(shard, file)?;
3397 let mut envelope_cache = HashMap::<u64, Vec<u8>>::new();
3398 let mut decoded = Vec::new();
3399
3400 for frame in frames {
3401 let envelope = envelope_by_index(shard, frame.envelope_index)?;
3402 if let Entry::Vacant(entry) = envelope_cache.entry(envelope.envelope_index) {
3403 entry.insert(self.load_payload_envelope(envelope, ParityReadPolicy::RepairOnly)?);
3404 }
3405 let envelope_plaintext = envelope_cache
3406 .get(&envelope.envelope_index)
3407 .expect("inserted above");
3408 let compressed = slice(
3409 envelope_plaintext,
3410 frame.offset_in_envelope as usize,
3411 frame.compressed_size as usize,
3412 "FrameEntry",
3413 )?;
3414 decoded.extend_from_slice(
3415 &self.decompress_payload_frame(compressed, frame.decompressed_size)?,
3416 );
3417 }
3418
3419 let offset = file.offset_in_first_frame_plaintext as usize;
3420 let group_len = to_usize(file.tar_member_group_size, "FileEntry")?;
3421 let group = slice(&decoded, offset, group_len, "FileEntry")?;
3422 let member = parse_tar_member_group(group, self.crypto_header.max_path_length)?;
3423 if member.path != expected_path {
3424 return Err(FormatError::InvalidArchive(
3425 "tar member path does not match FileEntry path",
3426 ));
3427 }
3428 if member.logical_size != file.file_data_size {
3429 return Err(FormatError::InvalidArchive(
3430 "tar member size does not match FileEntry file_data_size",
3431 ));
3432 }
3433 if member.v45_metadata.file_entry_flags != file.flags {
3434 return Err(FormatError::InvalidArchive(
3435 "FileEntry flags do not match decoded member-group metadata",
3436 ));
3437 }
3438 if enforce_extraction_cap {
3439 member.to_owned_member()
3440 } else {
3441 Ok(member.to_owned_metadata())
3442 }
3443 }
3444
3445 fn metadata_limits(&self) -> MetadataLimits {
3446 metadata_limits(&self.crypto_header)
3447 }
3448
3449 fn recompute_root_auth_material(
3450 &self,
3451 footer: &RootAuthFooterV1,
3452 ) -> Result<RootAuthMaterial, FormatError> {
3453 if footer.format_version != self.volume_header.format_version {
3454 return Err(FormatError::InvalidArchive(
3455 "RootAuthFooter format_version differs from authenticated VolumeHeader",
3456 ));
3457 }
3458 if footer.volume_format_rev != self.volume_header.volume_format_rev {
3459 return Err(FormatError::InvalidArchive(
3460 "RootAuthFooter volume_format_rev differs from authenticated VolumeHeader",
3461 ));
3462 }
3463 let format_version = self.volume_header.format_version;
3464 let volume_format_rev = self.volume_header.volume_format_rev;
3465 let footer_length = footer.footer_length()?;
3466 let root_auth_descriptor_digest = root_auth_descriptor_digest_for_revision(
3467 format_version,
3468 volume_format_rev,
3469 footer.authenticator_id,
3470 footer.signer_identity_type,
3471 &footer.signer_identity_bytes,
3472 u32::try_from(footer.authenticator_value.len()).map_err(|_| {
3473 FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
3474 })?,
3475 footer_length,
3476 )?;
3477 let signer_identity_digest =
3478 signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
3479 let manifest_pre_hmac = manifest_footer_global_pre_hmac_bytes(&self.manifest_footer);
3480 let crypto_pre_hmac_len = self
3481 .crypto_header_bytes
3482 .len()
3483 .checked_sub(CRYPTO_HEADER_HMAC_LEN)
3484 .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
3485 let critical_metadata_digest = critical_metadata_digest(CriticalMetadataDigestInputs {
3486 archive_uuid: self.volume_header.archive_uuid,
3487 session_id: self.volume_header.session_id,
3488 format_version,
3489 volume_format_rev,
3490 stripe_width: self.crypto_header.stripe_width,
3491 total_volumes: self.manifest_footer.total_volumes,
3492 compression_algo: self.crypto_header.compression_algo,
3493 aead_algo: self.crypto_header.aead_algo,
3494 fec_algo: self.crypto_header.fec_algo,
3495 kdf_algo: self.crypto_header.kdf_algo,
3496 crypto_header_pre_hmac_bytes: &self.crypto_header_bytes[..crypto_pre_hmac_len],
3497 chunk_size: self.crypto_header.chunk_size,
3498 envelope_target_size: self.crypto_header.envelope_target_size,
3499 block_size: self.crypto_header.block_size,
3500 fec_data_shards: self.crypto_header.fec_data_shards,
3501 fec_parity_shards: self.crypto_header.fec_parity_shards,
3502 index_fec_data_shards: self.crypto_header.index_fec_data_shards,
3503 index_fec_parity_shards: self.crypto_header.index_fec_parity_shards,
3504 index_root_fec_data_shards: self.crypto_header.index_root_fec_data_shards,
3505 index_root_fec_parity_shards: self.crypto_header.index_root_fec_parity_shards,
3506 volume_loss_tolerance: self.crypto_header.volume_loss_tolerance,
3507 bit_rot_buffer_pct: self.crypto_header.bit_rot_buffer_pct,
3508 has_dictionary: self.crypto_header.has_dictionary,
3509 manifest_footer_global_pre_hmac_bytes: &manifest_pre_hmac,
3510 index_root_first_block: self.manifest_footer.index_root_first_block,
3511 index_root_data_block_count: self.manifest_footer.index_root_data_block_count,
3512 index_root_parity_block_count: self.manifest_footer.index_root_parity_block_count,
3513 index_root_encrypted_size: self.manifest_footer.index_root_encrypted_size,
3514 index_root_decompressed_size: self.manifest_footer.index_root_decompressed_size,
3515 root_auth_descriptor_digest,
3516 })?;
3517 let index_root_plaintext = self.index_root.to_bytes();
3518 let index_digest =
3519 index_digest_for_revision(format_version, volume_format_rev, &index_root_plaintext)?;
3520 let shards = self.load_all_index_shards()?;
3521 let fec_layout_rows = self.root_auth_fec_layout_rows(&shards)?;
3522 let fec_layout_digest =
3523 fec_layout_digest_for_revision(format_version, volume_format_rev, &fec_layout_rows)?;
3524 let data_leaves = self.root_auth_data_block_leaves(&fec_layout_rows)?;
3525 let total_data_block_count = u64::try_from(data_leaves.len())
3526 .map_err(|_| FormatError::InvalidArchive("root-auth data block count overflow"))?;
3527 let data_block_merkle_root =
3528 data_block_merkle_root_for_revision(format_version, volume_format_rev, &data_leaves)?;
3529 let archive_root = archive_root_for_revision(ArchiveRootInputs {
3530 archive_uuid: self.volume_header.archive_uuid,
3531 session_id: self.volume_header.session_id,
3532 format_version,
3533 volume_format_rev,
3534 compression_algo: self.crypto_header.compression_algo,
3535 aead_algo: self.crypto_header.aead_algo,
3536 fec_algo: self.crypto_header.fec_algo,
3537 kdf_algo: self.crypto_header.kdf_algo,
3538 critical_metadata_digest,
3539 index_digest,
3540 fec_layout_digest,
3541 total_data_block_count,
3542 data_block_merkle_root,
3543 root_auth_descriptor_digest,
3544 signer_identity_digest,
3545 })?;
3546 Ok(RootAuthMaterial {
3547 critical_metadata_digest,
3548 index_digest,
3549 fec_layout_digest,
3550 data_block_merkle_root,
3551 signer_identity_digest,
3552 archive_root,
3553 total_data_block_count,
3554 })
3555 }
3556
3557 fn root_auth_fec_layout_rows(
3558 &self,
3559 shards: &[IndexShard],
3560 ) -> Result<Vec<FecLayoutObjectRow>, FormatError> {
3561 let mut rows = Vec::new();
3562 rows.push(FecLayoutObjectRow {
3563 object_class: 1,
3564 present: true,
3565 object_id: 0,
3566 first_block_index: self.manifest_footer.index_root_first_block,
3567 data_block_count: self.manifest_footer.index_root_data_block_count,
3568 parity_block_count: self.manifest_footer.index_root_parity_block_count,
3569 encrypted_size: self.manifest_footer.index_root_encrypted_size,
3570 plain_size: self.manifest_footer.index_root_decompressed_size,
3571 });
3572 if self.crypto_header.has_dictionary != 0 {
3573 rows.push(FecLayoutObjectRow {
3574 object_class: 2,
3575 present: true,
3576 object_id: 0,
3577 first_block_index: self.index_root.header.dictionary_first_block,
3578 data_block_count: self.index_root.header.dictionary_data_block_count,
3579 parity_block_count: self.index_root.header.dictionary_parity_block_count,
3580 encrypted_size: self.index_root.header.dictionary_encrypted_size,
3581 plain_size: self.index_root.header.dictionary_decompressed_size,
3582 });
3583 } else {
3584 rows.push(FecLayoutObjectRow {
3585 object_class: 2,
3586 present: false,
3587 object_id: 0,
3588 first_block_index: 0,
3589 data_block_count: 0,
3590 parity_block_count: 0,
3591 encrypted_size: 0,
3592 plain_size: 0,
3593 });
3594 }
3595 for entry in &self.index_root.shards {
3596 rows.push(FecLayoutObjectRow {
3597 object_class: 3,
3598 present: true,
3599 object_id: entry.shard_index,
3600 first_block_index: entry.first_block_index,
3601 data_block_count: entry.data_block_count,
3602 parity_block_count: entry.parity_block_count,
3603 encrypted_size: entry.encrypted_size,
3604 plain_size: entry.decompressed_size,
3605 });
3606 }
3607 let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
3608 for shard in shards {
3609 for envelope in &shard.envelopes {
3610 if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
3611 {
3612 if existing != *envelope {
3613 return Err(FormatError::InvalidArchive(
3614 "duplicate EnvelopeEntry rows do not match",
3615 ));
3616 }
3617 }
3618 }
3619 }
3620 for envelope in envelopes.values() {
3621 rows.push(FecLayoutObjectRow {
3622 object_class: 4,
3623 present: true,
3624 object_id: envelope.envelope_index,
3625 first_block_index: envelope.first_block_index,
3626 data_block_count: envelope.data_block_count,
3627 parity_block_count: envelope.parity_block_count,
3628 encrypted_size: envelope.encrypted_size,
3629 plain_size: envelope.plaintext_size,
3630 });
3631 }
3632 for entry in &self.index_root.directory_hint_shards {
3633 rows.push(FecLayoutObjectRow {
3634 object_class: 5,
3635 present: true,
3636 object_id: entry.hint_shard_index,
3637 first_block_index: entry.first_block_index,
3638 data_block_count: entry.data_block_count,
3639 parity_block_count: entry.parity_block_count,
3640 encrypted_size: entry.encrypted_size,
3641 plain_size: entry.decompressed_size,
3642 });
3643 }
3644 Ok(rows)
3645 }
3646
3647 fn fec_object_class_shape(
3648 &self,
3649 object_class: u8,
3650 ) -> Result<(BlockKind, BlockKind, u16, u16), FormatError> {
3651 match object_class {
3652 1 => Ok((
3653 BlockKind::IndexRootData,
3654 BlockKind::IndexRootParity,
3655 self.crypto_header.index_root_fec_data_shards,
3656 self.crypto_header.index_root_fec_parity_shards,
3657 )),
3658 2 => Ok((
3659 BlockKind::DictionaryData,
3660 BlockKind::DictionaryParity,
3661 self.crypto_header.index_root_fec_data_shards,
3662 self.crypto_header.index_root_fec_parity_shards,
3663 )),
3664 3 => Ok((
3665 BlockKind::IndexShardData,
3666 BlockKind::IndexShardParity,
3667 self.crypto_header.index_fec_data_shards,
3668 self.crypto_header.index_fec_parity_shards,
3669 )),
3670 4 => Ok((
3671 BlockKind::PayloadData,
3672 BlockKind::PayloadParity,
3673 self.crypto_header.fec_data_shards,
3674 self.crypto_header.fec_parity_shards,
3675 )),
3676 5 => Ok((
3677 BlockKind::DirectoryHintData,
3678 BlockKind::DirectoryHintParity,
3679 self.crypto_header.index_fec_data_shards,
3680 self.crypto_header.index_fec_parity_shards,
3681 )),
3682 _ => Err(FormatError::InvalidArchive(
3683 "unknown root-auth FEC row class",
3684 )),
3685 }
3686 }
3687
3688 fn root_auth_data_block_leaves(
3689 &self,
3690 rows: &[FecLayoutObjectRow],
3691 ) -> Result<Vec<DataBlockMerkleLeaf>, FormatError> {
3692 let block_provider = self.block_provider();
3693 let present_rows = rows.iter().filter(|row| row.present).collect::<Vec<_>>();
3694 let chunks = parallel_map_ref(&present_rows, self.options.jobs, |row| {
3695 let row = **row;
3696 let (data_kind, parity_kind, data_max, parity_max) =
3697 self.fec_object_class_shape(row.object_class)?;
3698 let extent = ObjectExtent {
3699 first_block_index: row.first_block_index,
3700 data_block_count: row.data_block_count,
3701 parity_block_count: row.parity_block_count,
3702 encrypted_size: row.encrypted_size,
3703 };
3704 let repaired = load_repaired_object_data_shards_from_parts(
3705 &block_provider,
3706 &self.crypto_header,
3707 extent,
3708 data_kind,
3709 parity_kind,
3710 data_max,
3711 parity_max,
3712 )?;
3713 let mut leaves = Vec::new();
3714 for (offset, payload) in repaired.into_iter().enumerate() {
3715 leaves.push(DataBlockMerkleLeaf {
3716 block_index: checked_u64_add(
3717 row.first_block_index,
3718 offset as u64,
3719 "root-auth data block",
3720 )?,
3721 kind: data_kind,
3722 flags: if offset + 1 == row.data_block_count as usize {
3723 0x01
3724 } else {
3725 0
3726 },
3727 payload,
3728 });
3729 }
3730 Ok(leaves)
3731 })?;
3732 let mut leaves = Vec::new();
3733 for mut chunk in chunks {
3734 leaves.append(&mut chunk);
3735 }
3736 leaves.sort_by_key(|leaf| leaf.block_index);
3737 Ok(leaves)
3738 }
3739
3740 fn validate_total_extraction_size(&self, logical_size: u64) -> Result<(), FormatError> {
3741 let cap = total_extraction_size_cap(self.options, self.observed_archive_bytes);
3742 if logical_size > cap {
3743 return Err(FormatError::ReaderUnsupported(
3744 "total extraction size exceeds configured cap",
3745 ));
3746 }
3747 Ok(())
3748 }
3749
3750 fn decompress_payload_frame(
3751 &self,
3752 compressed: &[u8],
3753 decompressed_size: u32,
3754 ) -> Result<Vec<u8>, FormatError> {
3755 let mut decompressor = self.new_payload_decompressor()?;
3756 self.decompress_payload_frame_with(&mut decompressor, compressed, decompressed_size)
3757 }
3758
3759 fn new_payload_decompressor(&self) -> Result<zstd::bulk::Decompressor<'static>, FormatError> {
3760 match &self.payload_dictionary {
3761 Some(dictionary) => zstd::bulk::Decompressor::with_dictionary(dictionary),
3762 None => zstd::bulk::Decompressor::new(),
3763 }
3764 .map_err(|_| FormatError::ZstdDecompressionFailure)
3765 }
3766
3767 fn decompress_payload_frame_with(
3768 &self,
3769 decompressor: &mut zstd::bulk::Decompressor<'static>,
3770 compressed: &[u8],
3771 decompressed_size: u32,
3772 ) -> Result<Vec<u8>, FormatError> {
3773 validate_exact_zstd_frame(compressed)?;
3774 let expected = decompressed_size as usize;
3775 let decoded = decompressor
3776 .decompress(compressed, expected)
3777 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
3778 if decoded.len() != expected {
3779 return Err(FormatError::ZstdDecompressedSizeMismatch {
3780 expected,
3781 actual: decoded.len(),
3782 });
3783 }
3784 Ok(decoded)
3785 }
3786
3787 fn validate_encrypted_object_block_ranges(
3788 &self,
3789 envelopes: &BTreeMap<u64, EnvelopeEntry>,
3790 ) -> Result<(), FormatError> {
3791 let mut ranges = Vec::new();
3792 ranges.push(object_block_range(
3793 self.manifest_footer.index_root_first_block,
3794 self.manifest_footer.index_root_data_block_count,
3795 self.manifest_footer.index_root_parity_block_count,
3796 "IndexRoot",
3797 )?);
3798 for shard in &self.index_root.shards {
3799 ranges.push(object_block_range(
3800 shard.first_block_index,
3801 shard.data_block_count,
3802 shard.parity_block_count,
3803 "IndexShard",
3804 )?);
3805 }
3806 for hint in &self.index_root.directory_hint_shards {
3807 ranges.push(object_block_range(
3808 hint.first_block_index,
3809 hint.data_block_count,
3810 hint.parity_block_count,
3811 "DirectoryHintShardEntry",
3812 )?);
3813 }
3814 if self.crypto_header.has_dictionary != 0 {
3815 ranges.push(object_block_range(
3816 self.index_root.header.dictionary_first_block,
3817 self.index_root.header.dictionary_data_block_count,
3818 self.index_root.header.dictionary_parity_block_count,
3819 "dictionary",
3820 )?);
3821 }
3822 for envelope in envelopes.values() {
3823 ranges.push(object_block_range(
3824 envelope.first_block_index,
3825 envelope.data_block_count,
3826 envelope.parity_block_count,
3827 "EnvelopeEntry",
3828 )?);
3829 }
3830 validate_non_overlapping_object_ranges(&mut ranges)?;
3831 if let Some(source) = &self.lazy_blocks {
3832 if source.is_complete_volume_set() {
3833 validate_exact_coverage_ranges_u64(
3834 &mut ranges,
3835 source.total_block_count()?,
3836 "encrypted object block ranges do not cover complete archive exactly",
3837 )?;
3838 }
3839 }
3840 Ok(())
3841 }
3842}
3843
3844impl<'a> DecodedTarMemberGroupReader<'a> {
3845 fn new(
3846 archive: &'a OpenedArchive,
3847 shard: &'a IndexShard,
3848 file: &'a FileEntry,
3849 ) -> Result<Self, FormatError> {
3850 Ok(Self {
3851 archive,
3852 shard,
3853 file,
3854 decompressor: archive.new_payload_decompressor()?,
3855 next_frame_offset: 0,
3856 cached_envelope_index: None,
3857 cached_envelope_plaintext: Vec::new(),
3858 current_frame: Vec::new(),
3859 current_frame_offset: 0,
3860 remaining_group_bytes: file.tar_member_group_size,
3861 })
3862 }
3863
3864 fn ensure_frame_available(&mut self) -> Result<(), ExtractError> {
3865 while self.current_frame_offset >= self.current_frame.len() {
3866 if self.next_frame_offset >= self.file.frame_count as u64 {
3867 return Err(
3868 FormatError::InvalidArchive("tar member group exceeds frame range").into(),
3869 );
3870 }
3871 let frame_index = self
3872 .file
3873 .first_frame_index
3874 .checked_add(self.next_frame_offset)
3875 .ok_or(FormatError::InvalidArchive(
3876 "FileEntry frame range overflow",
3877 ))?;
3878 let frame = frame_by_index(self.shard, frame_index)?;
3879 let envelope = envelope_by_index(self.shard, frame.envelope_index)?;
3880 if self.cached_envelope_index != Some(envelope.envelope_index) {
3881 self.cached_envelope_plaintext = self
3882 .archive
3883 .load_payload_envelope(envelope, ParityReadPolicy::RepairOnly)?;
3884 self.cached_envelope_index = Some(envelope.envelope_index);
3885 }
3886 let compressed = slice(
3887 &self.cached_envelope_plaintext,
3888 frame.offset_in_envelope as usize,
3889 frame.compressed_size as usize,
3890 "FrameEntry",
3891 )?;
3892 let decoded = self.archive.decompress_payload_frame_with(
3893 &mut self.decompressor,
3894 compressed,
3895 frame.decompressed_size,
3896 )?;
3897 let offset = if self.next_frame_offset == 0 {
3898 self.file.offset_in_first_frame_plaintext as usize
3899 } else {
3900 0
3901 };
3902 if offset > decoded.len() {
3903 return Err(FormatError::InvalidArchive(
3904 "offset in first frame is outside the first referenced frame",
3905 )
3906 .into());
3907 }
3908 self.next_frame_offset += 1;
3909 self.current_frame = decoded;
3910 self.current_frame_offset = offset;
3911 }
3912 Ok(())
3913 }
3914}
3915
3916impl TarMemberGroupReader for DecodedTarMemberGroupReader<'_> {
3917 fn read_some_member_bytes(&mut self, buf: &mut [u8]) -> Result<usize, ExtractError> {
3918 if buf.is_empty() {
3919 return Ok(0);
3920 }
3921 if self.remaining_group_bytes == 0 {
3922 return Ok(0);
3923 }
3924 self.ensure_frame_available()?;
3925 let available = self.current_frame.len() - self.current_frame_offset;
3926 let len = available
3927 .min(buf.len())
3928 .min(to_usize(self.remaining_group_bytes, "FileEntry")?);
3929 if len == 0 {
3930 return Err(FormatError::InvalidArchive("tar member group exceeds frame range").into());
3931 }
3932 buf[..len].copy_from_slice(
3933 &self.current_frame[self.current_frame_offset..self.current_frame_offset + len],
3934 );
3935 self.current_frame_offset += len;
3936 self.remaining_group_bytes -= len as u64;
3937 Ok(len)
3938 }
3939}
3940
3941fn frame_by_index(shard: &IndexShard, frame_index: u64) -> Result<&FrameEntry, FormatError> {
3942 shard
3943 .frames
3944 .binary_search_by_key(&frame_index, |entry| entry.frame_index)
3945 .map(|idx| &shard.frames[idx])
3946 .map_err(|_| FormatError::InvalidArchive("FileEntry references missing FrameEntry"))
3947}
3948
3949fn envelope_by_index(
3950 shard: &IndexShard,
3951 envelope_index: u64,
3952) -> Result<&EnvelopeEntry, FormatError> {
3953 shard
3954 .envelopes
3955 .binary_search_by_key(&envelope_index, |entry| entry.envelope_index)
3956 .map(|idx| &shard.envelopes[idx])
3957 .map_err(|_| FormatError::InvalidArchive("FrameEntry references missing EnvelopeEntry"))
3958}
3959
3960fn format_error_from_extract_error(err: ExtractError) -> FormatError {
3961 match err {
3962 ExtractError::Format(err) => err,
3963 ExtractError::Output(_) => {
3964 FormatError::FilesystemExtractionFailed("failed to write regular file")
3965 }
3966 }
3967}
3968
3969fn final_index_entry_winners(
3970 shards: &[IndexShard],
3971) -> Result<BTreeMap<String, WinningIndexEntry>, FormatError> {
3972 let mut final_entries = BTreeMap::<String, WinningIndexEntry>::new();
3973 for (shard_index, shard) in shards.iter().enumerate() {
3974 for (idx, file) in shard.files.iter().enumerate() {
3975 let path = utf8_path(
3976 shard
3977 .file_path(idx)
3978 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
3979 )?;
3980 let start = shard
3981 .tar_member_group_start(idx)
3982 .ok_or(FormatError::InvalidArchive(
3983 "FileEntry tar member start is missing",
3984 ))?;
3985 if let Some(winner) = final_entries.get_mut(&path) {
3986 if start >= winner.start {
3987 winner.start = start;
3988 winner.file_data_size = file.file_data_size;
3989 winner.shard_index = shard_index;
3990 winner.file_index = idx;
3991 }
3992 } else {
3993 final_entries.insert(
3994 path,
3995 WinningIndexEntry {
3996 start,
3997 file_data_size: file.file_data_size,
3998 shard_index,
3999 file_index: idx,
4000 },
4001 );
4002 }
4003 }
4004 }
4005 Ok(final_entries)
4006}
4007
4008fn archive_index_entry_from_loaded_file(
4009 shard: &IndexShard,
4010 file_index: usize,
4011) -> Result<ArchiveIndexEntry, FormatError> {
4012 let path = utf8_path(
4013 shard
4014 .file_path(file_index)
4015 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
4016 )?;
4017 archive_index_entry_from_loaded_file_with_path(path, shard, file_index)
4018}
4019
4020fn archive_index_entry_from_loaded_file_with_path(
4021 path: String,
4022 shard: &IndexShard,
4023 file_index: usize,
4024) -> Result<ArchiveIndexEntry, FormatError> {
4025 let file = shard
4026 .files
4027 .get(file_index)
4028 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
4029 let layout = archive_index_entry_layout(shard, file)?;
4030 Ok(ArchiveIndexEntry {
4031 name: archive_entry_name(&path),
4032 path,
4033 file_data_size: file.file_data_size,
4034 flags: file.flags,
4035 path_hash: file.path_hash,
4036 tar_member_group_size: file.tar_member_group_size,
4037 first_frame_index: file.first_frame_index,
4038 frame_count: file.frame_count,
4039 offset_in_first_frame_plaintext: file.offset_in_first_frame_plaintext,
4040 layout,
4041 })
4042}
4043
4044fn archive_index_entry_layout(
4045 shard: &IndexShard,
4046 file: &FileEntry,
4047) -> Result<ArchiveIndexEntryLayout, FormatError> {
4048 let frames = frame_range_for_file(shard, file)?;
4049 if let [frame] = frames {
4050 let envelope = envelope_by_index(shard, frame.envelope_index)?;
4051 return Ok(ArchiveIndexEntryLayout {
4052 compressed_size: frame.compressed_size as u64,
4053 decompressed_frame_size: frame.decompressed_size as u64,
4054 envelope_count: 1,
4055 first_envelope_index: Some(envelope.envelope_index),
4056 last_envelope_index: Some(envelope.envelope_index),
4057 first_payload_block_index: Some(envelope.first_block_index),
4058 payload_data_block_count: envelope.data_block_count as u64,
4059 payload_parity_block_count: envelope.parity_block_count as u64,
4060 payload_encrypted_size: envelope.encrypted_size as u64,
4061 });
4062 }
4063
4064 let mut compressed_size = 0u64;
4065 let mut decompressed_frame_size = 0u64;
4066 let mut envelope_indexes = BTreeSet::new();
4067
4068 for frame in frames {
4069 compressed_size = checked_u64_add(
4070 compressed_size,
4071 frame.compressed_size as u64,
4072 "ArchiveIndexEntry.compressed_size",
4073 )?;
4074 decompressed_frame_size = checked_u64_add(
4075 decompressed_frame_size,
4076 frame.decompressed_size as u64,
4077 "ArchiveIndexEntry.decompressed_frame_size",
4078 )?;
4079 envelope_indexes.insert(frame.envelope_index);
4080 }
4081
4082 let mut first_payload_block_index = None::<u64>;
4083 let mut payload_data_block_count = 0u64;
4084 let mut payload_parity_block_count = 0u64;
4085 let mut payload_encrypted_size = 0u64;
4086
4087 for envelope_index in &envelope_indexes {
4088 let envelope = envelope_by_index(shard, *envelope_index)?;
4089 first_payload_block_index = Some(
4090 first_payload_block_index
4091 .map(|existing| existing.min(envelope.first_block_index))
4092 .unwrap_or(envelope.first_block_index),
4093 );
4094 payload_data_block_count = checked_u64_add(
4095 payload_data_block_count,
4096 envelope.data_block_count as u64,
4097 "ArchiveIndexEntry.payload_data_block_count",
4098 )?;
4099 payload_parity_block_count = checked_u64_add(
4100 payload_parity_block_count,
4101 envelope.parity_block_count as u64,
4102 "ArchiveIndexEntry.payload_parity_block_count",
4103 )?;
4104 payload_encrypted_size = checked_u64_add(
4105 payload_encrypted_size,
4106 envelope.encrypted_size as u64,
4107 "ArchiveIndexEntry.payload_encrypted_size",
4108 )?;
4109 }
4110
4111 Ok(ArchiveIndexEntryLayout {
4112 compressed_size,
4113 decompressed_frame_size,
4114 envelope_count: u32::try_from(envelope_indexes.len()).map_err(|_| {
4115 FormatError::InvalidArchive("ArchiveIndexEntry envelope count overflow")
4116 })?,
4117 first_envelope_index: envelope_indexes.iter().next().copied(),
4118 last_envelope_index: envelope_indexes.iter().next_back().copied(),
4119 first_payload_block_index,
4120 payload_data_block_count,
4121 payload_parity_block_count,
4122 payload_encrypted_size,
4123 })
4124}
4125
4126fn archive_entry_name(path: &str) -> String {
4127 path.rsplit('/').next().unwrap_or(path).to_owned()
4128}
4129
4130#[derive(Debug)]
4131struct ParsedSeekableVolume {
4132 volume_header: VolumeHeader,
4133 crypto_header: CryptoHeaderFixed,
4134 crypto_header_bytes: Vec<u8>,
4135 key_wrap_table_bytes: Option<Vec<u8>>,
4136 subkeys: Subkeys,
4137 manifest_footer: Option<ManifestFooter>,
4138 manifest_footer_error: Option<FormatError>,
4139 root_auth_footer: Option<RootAuthFooterV1>,
4140 root_auth_footer_bytes: Option<Vec<u8>>,
4141 volume_trailer: VolumeTrailer,
4142 blocks: BTreeMap<u64, BlockRecord>,
4143 erased_block_indices: BTreeSet<u64>,
4144}
4145
4146struct ParsedSeekableReadAtVolume {
4147 reader: Arc<dyn ArchiveReadAt>,
4148 volume_header: VolumeHeader,
4149 crypto_header: CryptoHeaderFixed,
4150 crypto_header_bytes: Vec<u8>,
4151 key_wrap_table_bytes: Option<Vec<u8>>,
4152 subkeys: Subkeys,
4153 manifest_footer: Option<ManifestFooter>,
4154 manifest_footer_error: Option<FormatError>,
4155 root_auth_footer: Option<RootAuthFooterV1>,
4156 root_auth_footer_bytes: Option<Vec<u8>>,
4157 volume_trailer: VolumeTrailer,
4158 block_records_start: u64,
4159}
4160
4161struct ParsedOpenPrefix {
4162 volume_header: VolumeHeader,
4163 crypto_header: CryptoHeaderFixed,
4164 crypto_header_bytes: Vec<u8>,
4165 key_wrap_table_bytes: Option<Vec<u8>>,
4166 block_records_start: u64,
4167 subkeys: Subkeys,
4168}
4169
4170struct ParsedReadAtOpenPrefix {
4171 volume_header: VolumeHeader,
4172 crypto_header: CryptoHeaderFixed,
4173 crypto_header_bytes: Vec<u8>,
4174 key_wrap_table_bytes: Option<Vec<u8>>,
4175 block_records_start: u64,
4176 subkeys: Subkeys,
4177}
4178
4179pub(crate) struct StartupKeyWrapTable {
4180 pub(crate) table: KeyWrapTableV1,
4181 pub(crate) bytes: Vec<u8>,
4182 pub(crate) block_records_start: u64,
4183}
4184
4185pub(crate) fn startup_block_records_start(
4186 volume_header: &VolumeHeader,
4187 kdf_params: &KdfParams,
4188 read_key_wrap_table: impl FnMut(u64, usize) -> Result<Vec<u8>, FormatError>,
4189) -> Result<u64, FormatError> {
4190 Ok(
4191 startup_key_wrap_table(volume_header, kdf_params, read_key_wrap_table)?
4192 .map(|startup| startup.block_records_start)
4193 .unwrap_or_else(|| {
4194 volume_header.crypto_header_offset as u64
4195 + volume_header.crypto_header_length as u64
4196 }),
4197 )
4198}
4199
4200pub(crate) fn startup_key_wrap_table(
4201 volume_header: &VolumeHeader,
4202 kdf_params: &KdfParams,
4203 mut read_key_wrap_table: impl FnMut(u64, usize) -> Result<Vec<u8>, FormatError>,
4204) -> Result<Option<StartupKeyWrapTable>, FormatError> {
4205 let crypto_end = checked_u64_add(
4206 volume_header.crypto_header_offset as u64,
4207 volume_header.crypto_header_length as u64,
4208 "CryptoHeader",
4209 )?;
4210 let &KdfParams::RecipientWrap {
4211 key_wrap_table_length,
4212 ..
4213 } = kdf_params
4214 else {
4215 return Ok(None);
4216 };
4217 if volume_header.volume_format_rev != VOLUME_FORMAT_REV_45 {
4218 return Err(FormatError::InvalidArchive(
4219 "RecipientWrap KdfParams require volume_format_rev 45",
4220 ));
4221 }
4222 let key_wrap_table_length_usize =
4223 to_usize(u64::from(key_wrap_table_length), "KeyWrapTableV1 length")?;
4224 let key_wrap_table_bytes = read_key_wrap_table(crypto_end, key_wrap_table_length_usize)?;
4225 Ok(Some(parse_startup_key_wrap_table_bytes(
4226 volume_header,
4227 kdf_params,
4228 key_wrap_table_bytes,
4229 )?))
4230}
4231
4232pub(crate) fn parse_startup_key_wrap_table_bytes(
4233 volume_header: &VolumeHeader,
4234 kdf_params: &KdfParams,
4235 key_wrap_table_bytes: Vec<u8>,
4236) -> Result<StartupKeyWrapTable, FormatError> {
4237 let crypto_end = checked_u64_add(
4238 volume_header.crypto_header_offset as u64,
4239 volume_header.crypto_header_length as u64,
4240 "CryptoHeader",
4241 )?;
4242 let KdfParams::RecipientWrap {
4243 key_wrap_table_length,
4244 key_wrap_table_record_count,
4245 key_wrap_table_digest,
4246 ..
4247 } = kdf_params
4248 else {
4249 return Err(FormatError::KeyMaterialMismatch);
4250 };
4251 let key_wrap_table = KeyWrapTableV1::parse(
4252 &key_wrap_table_bytes,
4253 &volume_header.archive_uuid,
4254 &volume_header.session_id,
4255 *key_wrap_table_length,
4256 *key_wrap_table_record_count,
4257 )?;
4258 if compute_key_wrap_table_digest(*key_wrap_table_length, &key_wrap_table_bytes)
4259 != *key_wrap_table_digest
4260 {
4261 return Err(FormatError::IntegrityDigestMismatch {
4262 structure: "KeyWrapTableV1",
4263 });
4264 }
4265 let block_records_start = checked_u64_add(
4266 crypto_end,
4267 key_wrap_table.table_length as u64,
4268 "KeyWrapTableV1",
4269 )?;
4270 Ok(StartupKeyWrapTable {
4271 table: key_wrap_table,
4272 bytes: key_wrap_table_bytes,
4273 block_records_start,
4274 })
4275}
4276
4277fn parse_seekable_volume(
4278 bytes: &[u8],
4279 master_key: &MasterKey,
4280 options: ReaderOptions,
4281) -> Result<ParsedSeekableVolume, FormatError> {
4282 if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
4283 return Err(FormatError::InvalidLength {
4284 structure: "archive",
4285 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
4286 actual: bytes.len(),
4287 });
4288 }
4289
4290 let prefix = match parse_open_prefix(bytes, master_key) {
4291 Ok(prefix) => prefix,
4292 Err(prefix_err) => {
4293 if matches!(
4294 prefix_err,
4295 FormatError::UnsupportedVolumeFormatRevision { .. }
4296 ) {
4297 return Err(prefix_err);
4298 }
4299 if matches!(prefix_err, FormatError::KeyMaterialMismatch)
4300 && prefix_uses_recipient_wrap(bytes)
4301 {
4302 return Err(prefix_err);
4303 }
4304 return parse_seekable_volume_from_recovered_terminal(bytes, master_key, options)
4305 .or(Err(prefix_err));
4306 }
4307 };
4308 let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4309 match parse_seekable_volume_with_prefix(bytes, prefix, options) {
4310 Ok(parsed) => Ok(parsed),
4311 Err(prefix_err) => {
4312 match parse_seekable_volume_from_recovered_terminal(bytes, master_key, options) {
4313 Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4314 Ok(recovered)
4315 }
4316 Ok(_) | Err(_) => Err(prefix_err),
4317 }
4318 }
4319 }
4320}
4321
4322fn parse_seekable_volume_with_recipient_wrap_resolver<F>(
4323 bytes: &[u8],
4324 resolver: &mut F,
4325 options: ReaderOptions,
4326) -> Result<ParsedSeekableVolume, FormatError>
4327where
4328 F: FnMut(
4329 RecipientWrapRecordContext<'_>,
4330 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4331{
4332 let prefix = match parse_open_prefix_with_recipient_wrap_resolver(bytes, resolver) {
4333 Ok(prefix) => prefix,
4334 Err(prefix_err) => {
4335 if recipient_wrap_prefix_error_precludes_recovery(&prefix_err) {
4336 return Err(prefix_err);
4337 }
4338 return parse_seekable_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4339 bytes, resolver, options,
4340 )
4341 .or(Err(prefix_err));
4342 }
4343 };
4344 let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4345 match parse_seekable_volume_with_prefix(bytes, prefix, options) {
4346 Ok(parsed) => Ok(parsed),
4347 Err(prefix_err) => {
4348 match parse_seekable_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4349 bytes, resolver, options,
4350 ) {
4351 Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4352 Ok(recovered)
4353 }
4354 Ok(_) | Err(_) => Err(prefix_err),
4355 }
4356 }
4357 }
4358}
4359
4360fn parse_seekable_volume_with_prefix(
4361 bytes: &[u8],
4362 prefix: ParsedOpenPrefix,
4363 options: ReaderOptions,
4364) -> Result<ParsedSeekableVolume, FormatError> {
4365 let ParsedOpenPrefix {
4366 volume_header,
4367 crypto_header,
4368 crypto_header_bytes,
4369 key_wrap_table_bytes,
4370 block_records_start,
4371 subkeys,
4372 } = prefix;
4373 let crypto_bytes = crypto_header_bytes.as_slice();
4374
4375 let terminal = locate_v41_terminal(
4376 bytes,
4377 KeyHoldingTerminalContext {
4378 subkeys: &subkeys,
4379 volume_header: &volume_header,
4380 crypto_header: &crypto_header,
4381 crypto_header_bytes: crypto_bytes,
4382 },
4383 options,
4384 )?;
4385 finish_parse_seekable_volume(
4386 bytes,
4387 volume_header,
4388 crypto_header,
4389 crypto_header_bytes,
4390 key_wrap_table_bytes,
4391 block_records_start,
4392 subkeys,
4393 terminal,
4394 )
4395}
4396
4397fn parse_seekable_volume_from_recovered_terminal(
4398 bytes: &[u8],
4399 master_key: &MasterKey,
4400 options: ReaderOptions,
4401) -> Result<ParsedSeekableVolume, FormatError> {
4402 let authority = locate_v41_terminal_authority(bytes, master_key, options)?;
4403 parse_volume_format_dispatch(&authority.volume_header)?;
4404 let startup_key_wrap_table = startup_key_wrap_table(
4405 &authority.volume_header,
4406 &authority.kdf_params,
4407 |start, length| {
4408 let start = to_usize(start, "KeyWrapTableV1")?;
4409 Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
4410 },
4411 )?;
4412 let crypto_end = checked_u64_add(
4413 authority.volume_header.crypto_header_offset as u64,
4414 authority.volume_header.crypto_header_length as u64,
4415 "CryptoHeader",
4416 )?;
4417 let (key_wrap_table_bytes, block_records_start) = startup_key_wrap_table
4418 .map(|startup| (Some(startup.bytes), startup.block_records_start))
4419 .unwrap_or((None, crypto_end));
4420 finish_parse_seekable_volume(
4421 bytes,
4422 authority.volume_header,
4423 authority.crypto_header,
4424 authority.crypto_header_bytes,
4425 key_wrap_table_bytes,
4426 block_records_start,
4427 authority.subkeys,
4428 authority.terminal,
4429 )
4430}
4431
4432fn parse_seekable_volume_with_recipient_wrap_resolver_from_recovered_terminal<F>(
4433 bytes: &[u8],
4434 resolver: &mut F,
4435 options: ReaderOptions,
4436) -> Result<ParsedSeekableVolume, FormatError>
4437where
4438 F: FnMut(
4439 RecipientWrapRecordContext<'_>,
4440 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4441{
4442 let authority = locate_v41_recipient_wrap_terminal_authority(bytes, resolver, options)?;
4443 finish_parse_seekable_volume(
4444 bytes,
4445 authority.volume_header,
4446 authority.crypto_header,
4447 authority.crypto_header_bytes,
4448 Some(authority.key_wrap_table_bytes),
4449 authority.block_records_start,
4450 authority.subkeys,
4451 authority.terminal,
4452 )
4453}
4454
4455fn recipient_wrap_prefix_error_precludes_recovery(error: &FormatError) -> bool {
4456 matches!(
4457 error,
4458 FormatError::UnsupportedVolumeFormatRevision { .. }
4459 | FormatError::ReaderUnsupported(_)
4460 | FormatError::InvalidArchive(
4461 "VolumeHeader and CryptoHeader stripe_width differ"
4462 | "fec_parity_shards does not match v41 compute_parity"
4463 | "index_fec_parity_shards does not match v41 compute_parity"
4464 | "index_root_fec_parity_shards does not match v41 compute_parity"
4465 )
4466 )
4467}
4468
4469fn parse_open_prefix(
4470 bytes: &[u8],
4471 master_key: &MasterKey,
4472) -> Result<ParsedOpenPrefix, FormatError> {
4473 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
4474 parse_volume_format_dispatch(&volume_header)?;
4475 let crypto_start = volume_header.crypto_header_offset as usize;
4476 let crypto_len = volume_header.crypto_header_length as usize;
4477 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
4478 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
4479 if matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. }) {
4480 return Err(FormatError::KeyMaterialMismatch);
4481 }
4482 let subkeys = subkeys_for_open(
4483 Some(master_key),
4484 parsed_crypto.fixed.aead_algo,
4485 &volume_header.archive_uuid,
4486 &volume_header.session_id,
4487 )?;
4488 verify_integrity_tag(
4489 HmacDomain::CryptoHeader,
4490 parsed_crypto.fixed.aead_algo,
4491 volume_header.volume_format_rev,
4492 Some(&subkeys.mac_key),
4493 &volume_header.archive_uuid,
4494 &volume_header.session_id,
4495 parsed_crypto.hmac_covered_bytes,
4496 &parsed_crypto.header_hmac,
4497 )?;
4498 parsed_crypto.validate_extension_semantics()?;
4499 validate_seekable_supported_volume(
4500 &volume_header,
4501 &parsed_crypto.fixed,
4502 &parsed_crypto.extensions,
4503 )?;
4504 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4505 let block_records_start = startup_block_records_start(
4506 &volume_header,
4507 &parsed_crypto.kdf_params,
4508 |start, length| {
4509 let start = to_usize(start, "KeyWrapTableV1")?;
4510 Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
4511 },
4512 )?;
4513 let crypto_header = parsed_crypto.fixed.clone();
4514 Ok(ParsedOpenPrefix {
4515 volume_header,
4516 crypto_header,
4517 crypto_header_bytes: crypto_bytes.to_vec(),
4518 key_wrap_table_bytes: None,
4519 block_records_start,
4520 subkeys,
4521 })
4522}
4523
4524fn prefix_uses_recipient_wrap(bytes: &[u8]) -> bool {
4525 let Ok(volume_header_bytes) = slice(bytes, 0, VOLUME_HEADER_LEN, "archive") else {
4526 return false;
4527 };
4528 let Ok(volume_header) = VolumeHeader::parse(volume_header_bytes) else {
4529 return false;
4530 };
4531 let crypto_start = volume_header.crypto_header_offset as usize;
4532 let crypto_len = volume_header.crypto_header_length as usize;
4533 let Ok(crypto_bytes) = slice(bytes, crypto_start, crypto_len, "CryptoHeader") else {
4534 return false;
4535 };
4536 let Ok(parsed_crypto) = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)
4537 else {
4538 return false;
4539 };
4540 matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
4541}
4542
4543fn parse_open_prefix_with_recipient_wrap_resolver<F>(
4544 bytes: &[u8],
4545 resolver: &mut F,
4546) -> Result<ParsedOpenPrefix, FormatError>
4547where
4548 F: FnMut(
4549 RecipientWrapRecordContext<'_>,
4550 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4551{
4552 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
4553 parse_volume_format_dispatch(&volume_header)?;
4554 let crypto_start = volume_header.crypto_header_offset as usize;
4555 let crypto_len = volume_header.crypto_header_length as usize;
4556 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
4557 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
4558 if !matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
4559 || !parsed_crypto.fixed.aead_algo.is_encrypted()
4560 {
4561 return Err(FormatError::KeyMaterialMismatch);
4562 }
4563
4564 validate_seekable_supported_volume(&volume_header, &parsed_crypto.fixed, &[])?;
4565 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4566
4567 let startup_key_wrap_table = startup_key_wrap_table(
4568 &volume_header,
4569 &parsed_crypto.kdf_params,
4570 |start, length| {
4571 let start = to_usize(start, "KeyWrapTableV1")?;
4572 Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
4573 },
4574 )?
4575 .ok_or(FormatError::KeyMaterialMismatch)?;
4576 let key_wrap_table = startup_key_wrap_table.table;
4577 let key_wrap_table_bytes = Some(startup_key_wrap_table.bytes);
4578 let block_records_start = startup_key_wrap_table.block_records_start;
4579
4580 let subkeys = recipient_wrap_subkeys_from_table(
4581 &volume_header,
4582 &parsed_crypto,
4583 &key_wrap_table,
4584 resolver,
4585 )?;
4586 parsed_crypto.validate_extension_semantics()?;
4587 reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
4588
4589 let crypto_header = parsed_crypto.fixed.clone();
4590 Ok(ParsedOpenPrefix {
4591 volume_header,
4592 crypto_header,
4593 crypto_header_bytes: crypto_bytes.to_vec(),
4594 key_wrap_table_bytes,
4595 block_records_start,
4596 subkeys,
4597 })
4598}
4599
4600pub(crate) fn recipient_wrap_subkeys_from_table<F>(
4601 volume_header: &VolumeHeader,
4602 parsed_crypto: &CryptoHeader<'_>,
4603 key_wrap_table: &KeyWrapTableV1,
4604 resolver: &mut F,
4605) -> Result<Subkeys, FormatError>
4606where
4607 F: FnMut(
4608 RecipientWrapRecordContext<'_>,
4609 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>
4610 + ?Sized,
4611{
4612 let archive_identity = RecipientWrapArchiveIdentity {
4613 archive_uuid: volume_header.archive_uuid,
4614 session_id: volume_header.session_id,
4615 format_version: volume_header.format_version,
4616 volume_format_rev: volume_header.volume_format_rev,
4617 };
4618
4619 for record in &key_wrap_table.recipient_records {
4620 let candidates = resolver(RecipientWrapRecordContext {
4621 archive_identity,
4622 record,
4623 })?;
4624 for candidate in candidates {
4625 let master_key = MasterKey::from_raw_key(&candidate)?;
4626 let subkeys = subkeys_for_open(
4627 Some(&master_key),
4628 parsed_crypto.fixed.aead_algo,
4629 &volume_header.archive_uuid,
4630 &volume_header.session_id,
4631 )?;
4632 if verify_integrity_tag(
4633 HmacDomain::CryptoHeader,
4634 parsed_crypto.fixed.aead_algo,
4635 volume_header.volume_format_rev,
4636 Some(&subkeys.mac_key),
4637 &volume_header.archive_uuid,
4638 &volume_header.session_id,
4639 parsed_crypto.hmac_covered_bytes,
4640 &parsed_crypto.header_hmac,
4641 )
4642 .is_ok()
4643 {
4644 return Ok(subkeys);
4645 }
4646 }
4647 }
4648 Err(FormatError::KeyMaterialMismatch)
4649}
4650
4651#[allow(clippy::too_many_arguments)]
4652fn finish_parse_seekable_volume(
4653 bytes: &[u8],
4654 volume_header: VolumeHeader,
4655 crypto_header: CryptoHeaderFixed,
4656 crypto_header_bytes: Vec<u8>,
4657 key_wrap_table_bytes: Option<Vec<u8>>,
4658 block_records_start: u64,
4659 subkeys: Subkeys,
4660 terminal: V41Terminal,
4661) -> Result<ParsedSeekableVolume, FormatError> {
4662 let trailer_offset = to_usize(terminal.image.volume_trailer_offset, "VolumeTrailer")?;
4663 let volume_trailer = terminal.volume_trailer.clone();
4664 validate_trailer_identity(&volume_header, &volume_trailer)?;
4665
4666 let manifest_offset = to_usize(volume_trailer.manifest_footer_offset, "ManifestFooter")?;
4667 let manifest_end = checked_add(manifest_offset, MANIFEST_FOOTER_LEN, "ManifestFooter")?;
4668 if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
4669 if to_usize(volume_trailer.root_auth_footer_offset, "RootAuthFooter")? != manifest_end
4670 || volume_trailer
4671 .root_auth_footer_offset
4672 .checked_add(volume_trailer.root_auth_footer_length as u64)
4673 .ok_or(FormatError::InvalidArchive(
4674 "RootAuthFooter terminal boundary overflow",
4675 ))?
4676 != trailer_offset as u64
4677 {
4678 return Err(FormatError::InvalidArchive(
4679 "RootAuthFooter does not sit before selected trailer",
4680 ));
4681 }
4682 } else if manifest_end != trailer_offset {
4683 return Err(FormatError::InvalidArchive(
4684 "ManifestFooter does not end at selected trailer",
4685 ));
4686 }
4687 let manifest_bytes = &terminal.manifest_footer_bytes;
4688 let (manifest_footer, manifest_footer_error) =
4689 match parse_valid_manifest_footer(&volume_header, &crypto_header, &subkeys, manifest_bytes)
4690 {
4691 Ok(footer) => (Some(footer), None),
4692 Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
4693 Err(err) => return Err(err),
4694 };
4695
4696 let block_region = parse_block_region(
4697 bytes,
4698 to_usize(block_records_start, "BlockRecord")?,
4699 manifest_offset,
4700 crypto_header.block_size as usize,
4701 &volume_header,
4702 &volume_trailer,
4703 )?;
4704
4705 Ok(ParsedSeekableVolume {
4706 volume_header,
4707 crypto_header,
4708 crypto_header_bytes,
4709 key_wrap_table_bytes,
4710 subkeys,
4711 manifest_footer,
4712 manifest_footer_error,
4713 root_auth_footer: terminal.root_auth_footer,
4714 root_auth_footer_bytes: terminal.root_auth_footer_bytes,
4715 volume_trailer,
4716 blocks: block_region.blocks,
4717 erased_block_indices: block_region.erased_block_indices,
4718 })
4719}
4720
4721fn parse_seekable_read_at_volume(
4722 reader: Arc<dyn ArchiveReadAt>,
4723 master_key: &MasterKey,
4724 options: ReaderOptions,
4725) -> Result<ParsedSeekableReadAtVolume, FormatError> {
4726 let observed_len = reader.len()?;
4727 if observed_len < (VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN) as u64 {
4728 return Err(FormatError::InvalidLength {
4729 structure: "archive",
4730 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
4731 actual: to_usize(observed_len, "archive")?,
4732 });
4733 }
4734
4735 let prefix = match parse_read_at_open_prefix(reader.as_ref(), master_key) {
4736 Ok(prefix) => prefix,
4737 Err(prefix_err) => {
4738 if matches!(
4739 prefix_err,
4740 FormatError::UnsupportedVolumeFormatRevision { .. }
4741 ) {
4742 return Err(prefix_err);
4743 }
4744 return parse_seekable_read_at_volume_from_recovered_terminal(
4745 reader,
4746 observed_len,
4747 master_key,
4748 options,
4749 )
4750 .or(Err(prefix_err));
4751 }
4752 };
4753 let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4754 match parse_seekable_read_at_volume_with_prefix(reader.clone(), observed_len, prefix, options) {
4755 Ok(parsed) => Ok(parsed),
4756 Err(prefix_err) => match parse_seekable_read_at_volume_from_recovered_terminal(
4757 reader,
4758 observed_len,
4759 master_key,
4760 options,
4761 ) {
4762 Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4763 Ok(recovered)
4764 }
4765 Ok(_) | Err(_) => Err(prefix_err),
4766 },
4767 }
4768}
4769
4770fn parse_seekable_read_at_volume_with_recipient_wrap_resolver<F>(
4771 reader: Arc<dyn ArchiveReadAt>,
4772 resolver: &mut F,
4773 options: ReaderOptions,
4774) -> Result<ParsedSeekableReadAtVolume, FormatError>
4775where
4776 F: FnMut(
4777 RecipientWrapRecordContext<'_>,
4778 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4779{
4780 let observed_len = reader.len()?;
4781 if observed_len < (VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN) as u64 {
4782 return Err(FormatError::InvalidLength {
4783 structure: "archive",
4784 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
4785 actual: to_usize(observed_len, "archive")?,
4786 });
4787 }
4788
4789 let prefix = match parse_read_at_open_prefix_with_recipient_wrap_resolver(
4790 reader.as_ref(),
4791 resolver,
4792 ) {
4793 Ok(prefix) => prefix,
4794 Err(prefix_err) => {
4795 if recipient_wrap_prefix_error_precludes_recovery(&prefix_err) {
4796 return Err(prefix_err);
4797 }
4798 return parse_seekable_read_at_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4799 reader,
4800 observed_len,
4801 resolver,
4802 options,
4803 )
4804 .or(Err(prefix_err));
4805 }
4806 };
4807 let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4808 match parse_seekable_read_at_volume_with_prefix(reader.clone(), observed_len, prefix, options) {
4809 Ok(parsed) => Ok(parsed),
4810 Err(prefix_err) => {
4811 match parse_seekable_read_at_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4812 reader,
4813 observed_len,
4814 resolver,
4815 options,
4816 ) {
4817 Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4818 Ok(recovered)
4819 }
4820 Ok(_) | Err(_) => Err(prefix_err),
4821 }
4822 }
4823 }
4824}
4825
4826fn parse_seekable_read_at_volume_with_prefix(
4827 reader: Arc<dyn ArchiveReadAt>,
4828 observed_len: u64,
4829 prefix: ParsedReadAtOpenPrefix,
4830 options: ReaderOptions,
4831) -> Result<ParsedSeekableReadAtVolume, FormatError> {
4832 let ParsedReadAtOpenPrefix {
4833 volume_header,
4834 crypto_header,
4835 crypto_header_bytes,
4836 key_wrap_table_bytes,
4837 block_records_start,
4838 subkeys,
4839 } = prefix;
4840
4841 let terminal = locate_v41_terminal_read_at(
4842 reader.as_ref(),
4843 observed_len,
4844 KeyHoldingTerminalContext {
4845 subkeys: &subkeys,
4846 volume_header: &volume_header,
4847 crypto_header: &crypto_header,
4848 crypto_header_bytes: &crypto_header_bytes,
4849 },
4850 options,
4851 )?;
4852 finish_parse_seekable_read_at_volume(
4853 reader,
4854 volume_header,
4855 crypto_header,
4856 crypto_header_bytes,
4857 key_wrap_table_bytes,
4858 block_records_start,
4859 subkeys,
4860 terminal,
4861 )
4862}
4863
4864fn parse_seekable_read_at_volume_from_recovered_terminal(
4865 reader: Arc<dyn ArchiveReadAt>,
4866 observed_len: u64,
4867 master_key: &MasterKey,
4868 options: ReaderOptions,
4869) -> Result<ParsedSeekableReadAtVolume, FormatError> {
4870 let authority =
4871 locate_v41_terminal_authority_read_at(reader.as_ref(), observed_len, master_key, options)?;
4872 parse_volume_format_dispatch(&authority.volume_header)?;
4873 if matches!(authority.kdf_params, KdfParams::RecipientWrap { .. }) {
4874 return Err(FormatError::KeyMaterialMismatch);
4875 }
4876 let block_records_start = startup_block_records_start(
4877 &authority.volume_header,
4878 &authority.kdf_params,
4879 |start, length| read_at_vec(reader.as_ref(), start, length, "KeyWrapTableV1"),
4880 )?;
4881 finish_parse_seekable_read_at_volume(
4882 reader,
4883 authority.volume_header,
4884 authority.crypto_header,
4885 authority.crypto_header_bytes,
4886 None,
4887 block_records_start,
4888 authority.subkeys,
4889 authority.terminal,
4890 )
4891}
4892
4893fn parse_seekable_read_at_volume_with_recipient_wrap_resolver_from_recovered_terminal<F>(
4894 reader: Arc<dyn ArchiveReadAt>,
4895 observed_len: u64,
4896 resolver: &mut F,
4897 options: ReaderOptions,
4898) -> Result<ParsedSeekableReadAtVolume, FormatError>
4899where
4900 F: FnMut(
4901 RecipientWrapRecordContext<'_>,
4902 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4903{
4904 let authority = locate_v41_recipient_wrap_terminal_authority_read_at(
4905 reader.as_ref(),
4906 observed_len,
4907 resolver,
4908 options,
4909 )?;
4910 finish_parse_seekable_read_at_volume(
4911 reader,
4912 authority.volume_header,
4913 authority.crypto_header,
4914 authority.crypto_header_bytes,
4915 Some(authority.key_wrap_table_bytes),
4916 authority.block_records_start,
4917 authority.subkeys,
4918 authority.terminal,
4919 )
4920}
4921
4922fn parse_read_at_open_prefix(
4923 reader: &dyn ArchiveReadAt,
4924 master_key: &MasterKey,
4925) -> Result<ParsedReadAtOpenPrefix, FormatError> {
4926 let volume_header_bytes = read_at_vec(reader, 0, VOLUME_HEADER_LEN, "archive")?;
4927 let volume_header = VolumeHeader::parse(&volume_header_bytes)?;
4928 parse_volume_format_dispatch(&volume_header)?;
4929 let crypto_start = volume_header.crypto_header_offset as u64;
4930 let crypto_len = volume_header.crypto_header_length as u64;
4931 let crypto_bytes = read_at_vec(
4932 reader,
4933 crypto_start,
4934 to_usize(crypto_len, "CryptoHeader")?,
4935 "CryptoHeader",
4936 )?;
4937 let parsed_crypto = CryptoHeader::parse(&crypto_bytes, volume_header.crypto_header_length)?;
4938 if matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. }) {
4939 return Err(FormatError::KeyMaterialMismatch);
4940 }
4941 let subkeys = subkeys_for_open(
4942 Some(master_key),
4943 parsed_crypto.fixed.aead_algo,
4944 &volume_header.archive_uuid,
4945 &volume_header.session_id,
4946 )?;
4947 verify_integrity_tag(
4948 HmacDomain::CryptoHeader,
4949 parsed_crypto.fixed.aead_algo,
4950 volume_header.volume_format_rev,
4951 Some(&subkeys.mac_key),
4952 &volume_header.archive_uuid,
4953 &volume_header.session_id,
4954 parsed_crypto.hmac_covered_bytes,
4955 &parsed_crypto.header_hmac,
4956 )?;
4957 parsed_crypto.validate_extension_semantics()?;
4958 validate_seekable_supported_volume(
4959 &volume_header,
4960 &parsed_crypto.fixed,
4961 &parsed_crypto.extensions,
4962 )?;
4963 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4964 let block_records_start = startup_block_records_start(
4965 &volume_header,
4966 &parsed_crypto.kdf_params,
4967 |start, length| read_at_vec(reader, start, length, "KeyWrapTableV1"),
4968 )?;
4969 let crypto_header = parsed_crypto.fixed.clone();
4970 drop(parsed_crypto);
4971 Ok(ParsedReadAtOpenPrefix {
4972 volume_header,
4973 crypto_header,
4974 crypto_header_bytes: crypto_bytes,
4975 key_wrap_table_bytes: None,
4976 block_records_start,
4977 subkeys,
4978 })
4979}
4980
4981fn parse_read_at_open_prefix_with_recipient_wrap_resolver<F>(
4982 reader: &dyn ArchiveReadAt,
4983 resolver: &mut F,
4984) -> Result<ParsedReadAtOpenPrefix, FormatError>
4985where
4986 F: FnMut(
4987 RecipientWrapRecordContext<'_>,
4988 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4989{
4990 let volume_header_bytes = read_at_vec(reader, 0, VOLUME_HEADER_LEN, "archive")?;
4991 let volume_header = VolumeHeader::parse(&volume_header_bytes)?;
4992 parse_volume_format_dispatch(&volume_header)?;
4993 let crypto_start = volume_header.crypto_header_offset as u64;
4994 let crypto_len = volume_header.crypto_header_length as u64;
4995 let crypto_bytes = read_at_vec(
4996 reader,
4997 crypto_start,
4998 to_usize(crypto_len, "CryptoHeader")?,
4999 "CryptoHeader",
5000 )?;
5001 let parsed_crypto = CryptoHeader::parse(&crypto_bytes, volume_header.crypto_header_length)?;
5002 if !matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
5003 || !parsed_crypto.fixed.aead_algo.is_encrypted()
5004 {
5005 return Err(FormatError::KeyMaterialMismatch);
5006 }
5007
5008 validate_seekable_supported_volume(&volume_header, &parsed_crypto.fixed, &[])?;
5009 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
5010
5011 let startup_key_wrap_table = startup_key_wrap_table(
5012 &volume_header,
5013 &parsed_crypto.kdf_params,
5014 |start, length| read_at_vec(reader, start, length, "KeyWrapTableV1"),
5015 )?
5016 .ok_or(FormatError::KeyMaterialMismatch)?;
5017 let key_wrap_table = startup_key_wrap_table.table;
5018 let key_wrap_table_bytes = Some(startup_key_wrap_table.bytes);
5019 let block_records_start = startup_key_wrap_table.block_records_start;
5020
5021 let subkeys = recipient_wrap_subkeys_from_table(
5022 &volume_header,
5023 &parsed_crypto,
5024 &key_wrap_table,
5025 resolver,
5026 )?;
5027 parsed_crypto.validate_extension_semantics()?;
5028 reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
5029
5030 let crypto_header = parsed_crypto.fixed.clone();
5031 Ok(ParsedReadAtOpenPrefix {
5032 volume_header,
5033 crypto_header,
5034 crypto_header_bytes: crypto_bytes,
5035 key_wrap_table_bytes,
5036 block_records_start,
5037 subkeys,
5038 })
5039}
5040
5041#[allow(clippy::too_many_arguments)]
5042fn finish_parse_seekable_read_at_volume(
5043 reader: Arc<dyn ArchiveReadAt>,
5044 volume_header: VolumeHeader,
5045 crypto_header: CryptoHeaderFixed,
5046 crypto_header_bytes: Vec<u8>,
5047 key_wrap_table_bytes: Option<Vec<u8>>,
5048 block_records_start: u64,
5049 subkeys: Subkeys,
5050 terminal: V41Terminal,
5051) -> Result<ParsedSeekableReadAtVolume, FormatError> {
5052 let volume_trailer = terminal.volume_trailer.clone();
5053 validate_trailer_identity(&volume_header, &volume_trailer)?;
5054
5055 let manifest_offset = volume_trailer.manifest_footer_offset;
5056 let manifest_end = checked_u64_add(
5057 manifest_offset,
5058 MANIFEST_FOOTER_LEN as u64,
5059 "ManifestFooter",
5060 )?;
5061 if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
5062 if volume_trailer.root_auth_footer_offset != manifest_end
5063 || volume_trailer
5064 .root_auth_footer_offset
5065 .checked_add(volume_trailer.root_auth_footer_length as u64)
5066 .ok_or(FormatError::InvalidArchive(
5067 "RootAuthFooter terminal boundary overflow",
5068 ))?
5069 != terminal.image.volume_trailer_offset
5070 {
5071 return Err(FormatError::InvalidArchive(
5072 "RootAuthFooter does not sit before selected trailer",
5073 ));
5074 }
5075 } else if manifest_end != terminal.image.volume_trailer_offset {
5076 return Err(FormatError::InvalidArchive(
5077 "ManifestFooter does not end at selected trailer",
5078 ));
5079 }
5080 validate_seekable_block_region_layout(
5081 block_records_start,
5082 manifest_offset,
5083 crypto_header.block_size as usize,
5084 &volume_trailer,
5085 )?;
5086
5087 let manifest_bytes = &terminal.manifest_footer_bytes;
5088 let (manifest_footer, manifest_footer_error) =
5089 match parse_valid_manifest_footer(&volume_header, &crypto_header, &subkeys, manifest_bytes)
5090 {
5091 Ok(footer) => (Some(footer), None),
5092 Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
5093 Err(err) => return Err(err),
5094 };
5095
5096 Ok(ParsedSeekableReadAtVolume {
5097 reader,
5098 volume_header,
5099 crypto_header,
5100 crypto_header_bytes,
5101 key_wrap_table_bytes,
5102 subkeys,
5103 manifest_footer,
5104 manifest_footer_error,
5105 root_auth_footer: terminal.root_auth_footer,
5106 root_auth_footer_bytes: terminal.root_auth_footer_bytes,
5107 volume_trailer,
5108 block_records_start,
5109 })
5110}
5111
5112#[derive(Debug)]
5113struct ParsedPublicNoKeyVolume {
5114 volume_header: VolumeHeader,
5115 crypto_header: CryptoHeaderFixed,
5116 kdf_params: KdfParams,
5117 root_auth_footer: RootAuthFooterV1,
5118 root_auth_footer_bytes: Vec<u8>,
5119 blocks: BTreeMap<u64, BlockRecord>,
5120}
5121
5122pub fn public_no_key_verify_volumes_with_options<F>(
5123 volumes: &[&[u8]],
5124 mut verifier: F,
5125 options: ReaderOptions,
5126) -> Result<PublicNoKeyVerification, FormatError>
5127where
5128 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
5129{
5130 validate_reader_options(options)?;
5131 if volumes.is_empty() {
5132 return Err(FormatError::InvalidArchive("no volumes supplied"));
5133 }
5134 let mut parsed = Vec::with_capacity(volumes.len());
5135 for volume in volumes {
5136 parsed.push(parse_public_no_key_volume(volume, options)?);
5137 }
5138 let first = parsed
5139 .first()
5140 .ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
5141 if parsed.len() != first.crypto_header.stripe_width as usize {
5142 return Err(FormatError::ReaderUnsupported(
5143 "public no-key verification requires a complete volume set",
5144 ));
5145 }
5146
5147 let mut seen_volume_indexes = BTreeSet::new();
5148 let mut blocks = BTreeMap::new();
5149 for volume in &parsed {
5150 if volume.volume_header.archive_uuid != first.volume_header.archive_uuid
5151 || volume.volume_header.session_id != first.volume_header.session_id
5152 || !public_crypto_headers_agree(&volume.crypto_header, &first.crypto_header)
5153 || !public_kdf_profiles_agree(&volume.kdf_params, &first.kdf_params)
5154 {
5155 return Err(FormatError::InvalidArchive(
5156 "public no-key volume global metadata differs",
5157 ));
5158 }
5159 if volume.root_auth_footer_bytes != first.root_auth_footer_bytes {
5160 return Err(FormatError::InvalidArchive(
5161 "public no-key RootAuthFooter copies differ",
5162 ));
5163 }
5164 if !seen_volume_indexes.insert(volume.volume_header.volume_index) {
5165 return Err(FormatError::InvalidArchive(
5166 "duplicate public no-key volume index",
5167 ));
5168 }
5169 for (block_index, record) in &volume.blocks {
5170 if blocks.insert(*block_index, record.clone()).is_some() {
5171 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
5172 }
5173 }
5174 }
5175 validate_complete_global_block_coverage(&blocks, &BTreeSet::new())?;
5176
5177 let footer = &first.root_auth_footer;
5178 let mut data_leaves = blocks
5179 .values()
5180 .filter(|record| record.kind.is_data())
5181 .map(|record| DataBlockMerkleLeaf {
5182 block_index: record.block_index,
5183 kind: record.kind,
5184 flags: record.flags,
5185 payload: record.payload.clone(),
5186 })
5187 .collect::<Vec<_>>();
5188 data_leaves.sort_by_key(|leaf| leaf.block_index);
5189 let total_data_block_count = u64::try_from(data_leaves.len())
5190 .map_err(|_| FormatError::InvalidArchive("public no-key data block count overflow"))?;
5191 let observed_data_root = data_block_merkle_root_for_revision(
5192 footer.format_version,
5193 footer.volume_format_rev,
5194 &data_leaves,
5195 )?;
5196 if total_data_block_count != footer.total_data_block_count
5197 || observed_data_root != footer.data_block_merkle_root
5198 {
5199 return Err(FormatError::InvalidArchive(
5200 "public no-key data-block commitment mismatch",
5201 ));
5202 }
5203 let archive_root = recompute_public_archive_root(footer, &first.crypto_header)?;
5204 if archive_root != footer.archive_root {
5205 return Err(FormatError::InvalidArchive(
5206 "public no-key archive_root mismatch",
5207 ));
5208 }
5209 if !verifier(footer, &archive_root)? {
5210 return Err(FormatError::InvalidArchive(
5211 "public no-key authenticator verification failed",
5212 ));
5213 }
5214 Ok(PublicNoKeyVerification {
5215 format_version: footer.format_version,
5216 volume_format_rev: footer.volume_format_rev,
5217 archive_root,
5218 authenticator_id: footer.authenticator_id,
5219 signer_identity_type: footer.signer_identity_type,
5220 signer_identity_bytes: footer.signer_identity_bytes.clone(),
5221 total_data_block_count,
5222 diagnostics: vec![
5223 PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
5224 PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
5225 PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
5226 ],
5227 })
5228}
5229
5230fn parse_public_no_key_volume(
5231 bytes: &[u8],
5232 options: ReaderOptions,
5233) -> Result<ParsedPublicNoKeyVolume, FormatError> {
5234 if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
5235 return Err(FormatError::InvalidLength {
5236 structure: "archive",
5237 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
5238 actual: bytes.len(),
5239 });
5240 }
5241 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
5242 parse_volume_format_dispatch(&volume_header)?;
5243 let crypto_start = volume_header.crypto_header_offset as usize;
5244 let crypto_len = volume_header.crypto_header_length as usize;
5245 let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
5246 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
5247 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
5248 parsed_crypto.validate_extension_semantics()?;
5249 validate_seekable_supported_volume(
5250 &volume_header,
5251 &parsed_crypto.fixed,
5252 &parsed_crypto.extensions,
5253 )?;
5254 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
5255
5256 let terminal = locate_v41_public_terminal(bytes, &volume_header, &parsed_crypto, options)?;
5257 let block_records_start = match &parsed_crypto.kdf_params {
5258 KdfParams::RecipientWrap {
5259 key_wrap_table_length,
5260 ..
5261 } => checked_add(
5262 crypto_end,
5263 *key_wrap_table_length as usize,
5264 "KeyWrapTableV1",
5265 )?,
5266 _ => crypto_end,
5267 };
5268 let block_region = parse_public_block_observation(
5269 bytes,
5270 block_records_start,
5271 &terminal.image,
5272 parsed_crypto.fixed.block_size as usize,
5273 &volume_header,
5274 )?;
5275 Ok(ParsedPublicNoKeyVolume {
5276 volume_header,
5277 crypto_header: parsed_crypto.fixed,
5278 kdf_params: parsed_crypto.kdf_params,
5279 root_auth_footer: terminal.root_auth_footer,
5280 root_auth_footer_bytes: terminal.root_auth_footer_bytes,
5281 blocks: block_region,
5282 })
5283}
5284
5285fn public_crypto_headers_agree(left: &CryptoHeaderFixed, right: &CryptoHeaderFixed) -> bool {
5286 left.length == right.length
5287 && left.stripe_width == right.stripe_width
5288 && left.block_size == right.block_size
5289 && left.compression_algo == right.compression_algo
5290 && left.aead_algo == right.aead_algo
5291 && left.fec_algo == right.fec_algo
5292 && left.kdf_algo == right.kdf_algo
5293}
5294
5295fn public_kdf_profiles_agree(left: &KdfParams, right: &KdfParams) -> bool {
5296 match (left, right) {
5297 (
5298 KdfParams::RecipientWrap {
5299 key_wrap_table_length: left_length,
5300 key_wrap_table_record_count: left_count,
5301 key_wrap_table_version: left_version,
5302 key_wrap_table_digest: left_digest,
5303 },
5304 KdfParams::RecipientWrap {
5305 key_wrap_table_length: right_length,
5306 key_wrap_table_record_count: right_count,
5307 key_wrap_table_version: right_version,
5308 key_wrap_table_digest: right_digest,
5309 },
5310 ) => {
5311 left_length == right_length
5312 && left_count == right_count
5313 && left_version == right_version
5314 && left_digest == right_digest
5315 }
5316 (KdfParams::RecipientWrap { .. }, _) | (_, KdfParams::RecipientWrap { .. }) => false,
5317 _ => true,
5318 }
5319}
5320
5321fn recompute_public_archive_root(
5322 footer: &RootAuthFooterV1,
5323 crypto_header: &CryptoHeaderFixed,
5324) -> Result<[u8; 32], FormatError> {
5325 let descriptor_digest = root_auth_descriptor_digest_for_revision(
5326 footer.format_version,
5327 footer.volume_format_rev,
5328 footer.authenticator_id,
5329 footer.signer_identity_type,
5330 &footer.signer_identity_bytes,
5331 u32::try_from(footer.authenticator_value.len()).map_err(|_| {
5332 FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
5333 })?,
5334 footer.footer_length()?,
5335 )?;
5336 let signer_digest =
5337 signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
5338 if signer_digest != footer.signer_identity_digest {
5339 return Err(FormatError::InvalidArchive(
5340 "public no-key signer identity digest mismatch",
5341 ));
5342 }
5343 archive_root_for_revision(ArchiveRootInputs {
5344 archive_uuid: footer.archive_uuid,
5345 session_id: footer.session_id,
5346 format_version: footer.format_version,
5347 volume_format_rev: footer.volume_format_rev,
5348 compression_algo: crypto_header.compression_algo,
5349 aead_algo: crypto_header.aead_algo,
5350 fec_algo: crypto_header.fec_algo,
5351 kdf_algo: crypto_header.kdf_algo,
5352 critical_metadata_digest: footer.critical_metadata_digest,
5353 index_digest: footer.index_digest,
5354 fec_layout_digest: footer.fec_layout_digest,
5355 total_data_block_count: footer.total_data_block_count,
5356 data_block_merkle_root: footer.data_block_merkle_root,
5357 root_auth_descriptor_digest: descriptor_digest,
5358 signer_identity_digest: signer_digest,
5359 })
5360}
5361
5362fn parse_valid_manifest_footer(
5363 volume_header: &VolumeHeader,
5364 crypto_header: &CryptoHeaderFixed,
5365 subkeys: &Subkeys,
5366 manifest_bytes: &[u8],
5367) -> Result<ManifestFooter, FormatError> {
5368 let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
5369 validate_manifest_footer(
5370 volume_header,
5371 crypto_header,
5372 &manifest_footer,
5373 subkeys,
5374 volume_header.volume_format_rev,
5375 manifest_bytes,
5376 )?;
5377 manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
5378 Ok(manifest_footer)
5379}
5380
5381fn manifest_footer_copy_error_is_recoverable(error: &FormatError) -> bool {
5382 matches!(
5383 error,
5384 FormatError::BadMagic {
5385 structure: "ManifestFooter",
5386 } | FormatError::NonZeroReserved {
5387 structure: "ManifestFooter",
5388 } | FormatError::InvalidAuthoritativeFlag(_)
5389 | FormatError::HmacMismatch {
5390 structure: "ManifestFooter",
5391 }
5392 | FormatError::IntegrityDigestMismatch {
5393 structure: "ManifestFooter",
5394 }
5395 )
5396}
5397
5398fn validate_seekable_supported_volume(
5399 volume_header: &VolumeHeader,
5400 crypto_header: &CryptoHeaderFixed,
5401 extensions: &[ExtensionTlv<'_>],
5402) -> Result<(), FormatError> {
5403 reject_unsupported_raw_stream_profile(extensions)?;
5404 if crypto_header.stripe_width != volume_header.stripe_width {
5405 return Err(FormatError::InvalidArchive(
5406 "VolumeHeader and CryptoHeader stripe_width differ",
5407 ));
5408 }
5409 Ok(())
5410}
5411
5412pub(crate) fn validate_crypto_class_parity_exactness(
5413 crypto_header: &CryptoHeaderFixed,
5414) -> Result<(), FormatError> {
5415 let fec = required_object_parity(crypto_header.fec_data_shards as u64, crypto_header)?;
5416 if crypto_header.fec_parity_shards as u32 != fec {
5417 return Err(FormatError::InvalidArchive(
5418 "fec_parity_shards does not match v41 compute_parity",
5419 ));
5420 }
5421 let index = required_object_parity(crypto_header.index_fec_data_shards as u64, crypto_header)?;
5422 if crypto_header.index_fec_parity_shards as u32 != index {
5423 return Err(FormatError::InvalidArchive(
5424 "index_fec_parity_shards does not match v41 compute_parity",
5425 ));
5426 }
5427 let index_root = required_object_parity(
5428 crypto_header.index_root_fec_data_shards as u64,
5429 crypto_header,
5430 )?;
5431 if crypto_header.index_root_fec_parity_shards as u32 != index_root {
5432 return Err(FormatError::InvalidArchive(
5433 "index_root_fec_parity_shards does not match v41 compute_parity",
5434 ));
5435 }
5436 Ok(())
5437}
5438
5439fn validate_volume_set_member(
5440 first: &ParsedSeekableVolume,
5441 candidate: &ParsedSeekableVolume,
5442) -> Result<(), FormatError> {
5443 validate_volume_set_member_metadata(
5444 &first.volume_header,
5445 &first.crypto_header,
5446 &first.crypto_header_bytes,
5447 &candidate.volume_header,
5448 &candidate.crypto_header,
5449 &candidate.crypto_header_bytes,
5450 )?;
5451 validate_key_wrap_table_bytes_match(
5452 &first.key_wrap_table_bytes,
5453 &candidate.key_wrap_table_bytes,
5454 )
5455}
5456
5457fn validate_key_wrap_table_bytes_match(
5458 first_key_wrap_table_bytes: &Option<Vec<u8>>,
5459 candidate_key_wrap_table_bytes: &Option<Vec<u8>>,
5460) -> Result<(), FormatError> {
5461 if candidate_key_wrap_table_bytes != first_key_wrap_table_bytes {
5462 return Err(FormatError::InvalidArchive("KeyWrapTableV1 copies differ"));
5463 }
5464 Ok(())
5465}
5466
5467fn validate_volume_set_member_metadata(
5468 first_volume_header: &VolumeHeader,
5469 first_crypto_header: &CryptoHeaderFixed,
5470 first_crypto_header_bytes: &[u8],
5471 candidate_volume_header: &VolumeHeader,
5472 candidate_crypto_header: &CryptoHeaderFixed,
5473 candidate_crypto_header_bytes: &[u8],
5474) -> Result<(), FormatError> {
5475 if candidate_volume_header.archive_uuid != first_volume_header.archive_uuid
5476 || candidate_volume_header.session_id != first_volume_header.session_id
5477 {
5478 return Err(FormatError::InvalidArchive(
5479 "mixed archive or session IDs in volume set",
5480 ));
5481 }
5482 if candidate_crypto_header_bytes != first_crypto_header_bytes
5483 || candidate_crypto_header != first_crypto_header
5484 {
5485 return Err(FormatError::InvalidArchive("CryptoHeader copies differ"));
5486 }
5487 Ok(())
5488}
5489
5490pub(crate) fn manifest_bootstrap_fields_match(
5491 left: &ManifestFooter,
5492 right: &ManifestFooter,
5493) -> bool {
5494 left.archive_uuid == right.archive_uuid
5495 && left.session_id == right.session_id
5496 && left.is_authoritative == right.is_authoritative
5497 && left.total_volumes == right.total_volumes
5498 && left.index_root_first_block == right.index_root_first_block
5499 && left.index_root_data_block_count == right.index_root_data_block_count
5500 && left.index_root_parity_block_count == right.index_root_parity_block_count
5501 && left.index_root_encrypted_size == right.index_root_encrypted_size
5502 && left.index_root_decompressed_size == right.index_root_decompressed_size
5503}
5504
5505fn validate_complete_global_block_coverage(
5506 blocks: &BTreeMap<u64, BlockRecord>,
5507 erased_block_indices: &BTreeSet<u64>,
5508) -> Result<(), FormatError> {
5509 let mut expected = 0u64;
5510 let mut block_iter = blocks.keys().copied().peekable();
5511 let mut erasure_iter = erased_block_indices.iter().copied().peekable();
5512
5513 loop {
5514 let next_block = block_iter.peek().copied();
5515 let next_erasure = erasure_iter.peek().copied();
5516 let next = match (next_block, next_erasure) {
5517 (Some(block), Some(erasure)) if block == erasure => {
5518 return Err(FormatError::InvalidArchive(
5519 "BlockRecord index is both present and erased",
5520 ));
5521 }
5522 (Some(block), Some(erasure)) => block.min(erasure),
5523 (Some(block), None) => block,
5524 (None, Some(erasure)) => erasure,
5525 (None, None) => return Ok(()),
5526 };
5527
5528 if next != expected {
5529 return Err(FormatError::InvalidArchive(
5530 "complete volume set has missing global blocks",
5531 ));
5532 }
5533 if next_block == Some(next) {
5534 block_iter.next();
5535 }
5536 if next_erasure == Some(next) {
5537 erasure_iter.next();
5538 }
5539 expected = expected
5540 .checked_add(1)
5541 .ok_or(FormatError::InvalidArchive("global block index overflow"))?;
5542 }
5543}
5544
5545#[derive(Debug)]
5546struct V41Terminal {
5547 image: CriticalMetadataImage,
5548 manifest_footer_bytes: Vec<u8>,
5549 root_auth_footer_bytes: Option<Vec<u8>>,
5550 root_auth_footer: Option<RootAuthFooterV1>,
5551 volume_trailer: VolumeTrailer,
5552}
5553
5554pub(crate) struct SequentialTerminalMaterial {
5555 pub(crate) manifest_footer: ManifestFooter,
5556 pub(crate) volume_trailer: VolumeTrailer,
5557 pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
5558}
5559
5560#[derive(Debug)]
5561struct V41PublicTerminal {
5562 image: CriticalMetadataImage,
5563 root_auth_footer_bytes: Vec<u8>,
5564 root_auth_footer: RootAuthFooterV1,
5565}
5566
5567#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5568struct CmraDecoderTuple {
5569 shard_size: u32,
5570 data_shard_count: u16,
5571 parity_shard_count: u16,
5572 image_length: u32,
5573 image_sha256: [u8; 32],
5574}
5575
5576#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5577struct CmraIdentityHints {
5578 archive_uuid: [u8; 16],
5579 session_id: [u8; 16],
5580 volume_index: u32,
5581}
5582
5583impl From<CriticalMetadataRecoveryHeader> for CmraDecoderTuple {
5584 fn from(header: CriticalMetadataRecoveryHeader) -> Self {
5585 Self {
5586 shard_size: header.shard_size,
5587 data_shard_count: header.data_shard_count,
5588 parity_shard_count: header.parity_shard_count,
5589 image_length: header.image_length,
5590 image_sha256: header.image_sha256,
5591 }
5592 }
5593}
5594
5595impl From<CriticalMetadataRecoveryHeader> for CmraIdentityHints {
5596 fn from(header: CriticalMetadataRecoveryHeader) -> Self {
5597 Self {
5598 archive_uuid: header.archive_uuid_hint,
5599 session_id: header.session_id_hint,
5600 volume_index: header.volume_index_hint,
5601 }
5602 }
5603}
5604
5605impl From<CriticalRecoveryLocator> for CmraDecoderTuple {
5606 fn from(locator: CriticalRecoveryLocator) -> Self {
5607 Self {
5608 shard_size: locator.cmra_shard_size,
5609 data_shard_count: locator.cmra_data_shard_count,
5610 parity_shard_count: locator.cmra_parity_shard_count,
5611 image_length: locator.cmra_image_length,
5612 image_sha256: locator.cmra_image_sha256,
5613 }
5614 }
5615}
5616
5617impl From<CriticalRecoveryLocator> for CmraIdentityHints {
5618 fn from(locator: CriticalRecoveryLocator) -> Self {
5619 Self {
5620 archive_uuid: locator.archive_uuid_hint,
5621 session_id: locator.session_id_hint,
5622 volume_index: locator.volume_index_hint,
5623 }
5624 }
5625}
5626
5627#[derive(Debug)]
5628struct RecoveredCmra {
5629 image: CriticalMetadataImage,
5630 tuple: CmraDecoderTuple,
5631 header_hints: Option<CmraIdentityHints>,
5632 cmra_length: u64,
5633}
5634
5635#[derive(Debug)]
5636struct TerminalCandidate {
5637 terminal: V41Terminal,
5638 anchor: usize,
5639 locator_sequence: Option<u32>,
5640 cmra_offset: u64,
5641 cmra_length: u64,
5642}
5643
5644#[derive(Debug)]
5645struct PublicTerminalCandidate {
5646 terminal: V41PublicTerminal,
5647 anchor: usize,
5648 cmra_offset: u64,
5649 cmra_length: u64,
5650}
5651
5652#[derive(Debug)]
5653struct RecoveredTerminalAuthority {
5654 terminal: V41Terminal,
5655 volume_header: VolumeHeader,
5656 crypto_header: CryptoHeaderFixed,
5657 crypto_header_bytes: Vec<u8>,
5658 subkeys: Subkeys,
5659 kdf_params: KdfParams,
5660}
5661
5662#[derive(Debug)]
5663struct RecoveredRecipientWrapTerminalAuthority {
5664 terminal: V41Terminal,
5665 volume_header: VolumeHeader,
5666 crypto_header: CryptoHeaderFixed,
5667 crypto_header_bytes: Vec<u8>,
5668 key_wrap_table_bytes: Vec<u8>,
5669 block_records_start: u64,
5670 subkeys: Subkeys,
5671}
5672
5673#[derive(Debug)]
5674struct TerminalAuthorityCandidate {
5675 authority: RecoveredTerminalAuthority,
5676 anchor: usize,
5677 cmra_offset: u64,
5678 cmra_length: u64,
5679}
5680
5681#[derive(Debug)]
5682struct RecipientWrapTerminalAuthorityCandidate {
5683 authority: RecoveredRecipientWrapTerminalAuthority,
5684 anchor: usize,
5685 cmra_offset: u64,
5686 cmra_length: u64,
5687}
5688
5689#[derive(Debug, Clone, Copy)]
5690enum CmraRecoveryMode {
5691 KeyHolding,
5692 PublicNoKey,
5693}
5694
5695#[derive(Clone, Copy)]
5696pub(crate) struct KeyHoldingTerminalContext<'a> {
5697 pub(crate) subkeys: &'a Subkeys,
5698 pub(crate) volume_header: &'a VolumeHeader,
5699 pub(crate) crypto_header: &'a CryptoHeaderFixed,
5700 pub(crate) crypto_header_bytes: &'a [u8],
5701}
5702
5703fn locate_v41_terminal(
5704 bytes: &[u8],
5705 context: KeyHoldingTerminalContext<'_>,
5706 options: ReaderOptions,
5707) -> Result<V41Terminal, FormatError> {
5708 locate_v41_terminal_candidate(bytes, context, options).map(|candidate| candidate.terminal)
5709}
5710
5711fn locate_v41_terminal_read_at(
5712 reader: &dyn ArchiveReadAt,
5713 len: u64,
5714 context: KeyHoldingTerminalContext<'_>,
5715 options: ReaderOptions,
5716) -> Result<V41Terminal, FormatError> {
5717 let mut candidates = Vec::new();
5718 if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
5719 let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
5720 collect_v41_locator_candidate_read_at(reader, final_offset, 0, context, &mut candidates);
5721 }
5722 if len >= LOCATOR_PAIR_LEN as u64 {
5723 let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
5724 collect_v41_locator_candidate_read_at(reader, mirror_offset, 1, context, &mut candidates);
5725 }
5726
5727 if candidates.is_empty() {
5728 let scan = max_critical_recovery_scan(options)? as u64;
5729 let scan_start = len.saturating_sub(scan);
5730 let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
5731 let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
5732 let mut offset = tail.len().saturating_sub(4);
5733 while offset < tail.len() {
5734 let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
5735 if tail.get(offset..offset + 4) == Some(b"TZCL") {
5736 collect_v41_locator_candidate_read_at(
5737 reader,
5738 absolute_offset,
5739 2,
5740 context,
5741 &mut candidates,
5742 );
5743 } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
5744 if let Ok(candidate) =
5745 parse_locatorless_cmra_candidate_read_at(reader, absolute_offset, context)
5746 {
5747 candidates.push(candidate);
5748 }
5749 }
5750 if offset == 0 {
5751 break;
5752 }
5753 offset -= 1;
5754 }
5755 }
5756
5757 choose_v41_terminal_candidate(candidates).map(|candidate| candidate.terminal)
5758}
5759
5760fn locate_v41_terminal_authority(
5761 bytes: &[u8],
5762 master_key: &MasterKey,
5763 options: ReaderOptions,
5764) -> Result<RecoveredTerminalAuthority, FormatError> {
5765 let mut candidates = Vec::new();
5766 if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
5767 let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
5768 collect_v41_locator_authority_candidate(
5769 bytes,
5770 final_offset,
5771 0,
5772 master_key,
5773 &mut candidates,
5774 );
5775 }
5776 if bytes.len() >= LOCATOR_PAIR_LEN {
5777 let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
5778 collect_v41_locator_authority_candidate(
5779 bytes,
5780 mirror_offset,
5781 1,
5782 master_key,
5783 &mut candidates,
5784 );
5785 }
5786
5787 if candidates.is_empty() {
5788 let scan = max_critical_recovery_scan(options)?;
5789 let scan_start = bytes.len().saturating_sub(scan);
5790 let mut offset = bytes.len().saturating_sub(4);
5791 while offset >= scan_start {
5792 if bytes.get(offset..offset + 4) == Some(b"TZCL") {
5793 collect_v41_locator_authority_candidate(
5794 bytes,
5795 offset,
5796 2,
5797 master_key,
5798 &mut candidates,
5799 );
5800 } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
5801 if let Ok(candidate) =
5802 parse_locatorless_cmra_authority_candidate(bytes, offset, master_key)
5803 {
5804 candidates.push(candidate);
5805 }
5806 }
5807 if offset == 0 {
5808 break;
5809 }
5810 offset -= 1;
5811 }
5812 }
5813
5814 choose_v41_terminal_authority_candidate(candidates).map(|candidate| candidate.authority)
5815}
5816
5817fn locate_v41_recipient_wrap_terminal_authority<F>(
5818 bytes: &[u8],
5819 resolver: &mut F,
5820 options: ReaderOptions,
5821) -> Result<RecoveredRecipientWrapTerminalAuthority, FormatError>
5822where
5823 F: FnMut(
5824 RecipientWrapRecordContext<'_>,
5825 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
5826{
5827 let mut candidates = Vec::new();
5828 if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
5829 let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
5830 collect_v41_recipient_wrap_locator_authority_candidate(
5831 bytes,
5832 final_offset,
5833 0,
5834 resolver,
5835 &mut candidates,
5836 );
5837 }
5838 if bytes.len() >= LOCATOR_PAIR_LEN {
5839 let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
5840 collect_v41_recipient_wrap_locator_authority_candidate(
5841 bytes,
5842 mirror_offset,
5843 1,
5844 resolver,
5845 &mut candidates,
5846 );
5847 }
5848
5849 if candidates.is_empty() {
5850 let scan = max_critical_recovery_scan(options)?;
5851 let scan_start = bytes.len().saturating_sub(scan);
5852 let mut offset = bytes.len().saturating_sub(4);
5853 while offset >= scan_start {
5854 if bytes.get(offset..offset + 4) == Some(b"TZCL") {
5855 collect_v41_recipient_wrap_locator_authority_candidate(
5856 bytes,
5857 offset,
5858 2,
5859 resolver,
5860 &mut candidates,
5861 );
5862 } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
5863 if let Ok(candidate) = parse_locatorless_cmra_recipient_wrap_authority_candidate(
5864 bytes, offset, resolver,
5865 ) {
5866 candidates.push(candidate);
5867 }
5868 }
5869 if offset == 0 {
5870 break;
5871 }
5872 offset -= 1;
5873 }
5874 }
5875
5876 choose_v41_recipient_wrap_terminal_authority_candidate(candidates)
5877 .map(|candidate| candidate.authority)
5878}
5879
5880fn locate_v41_recipient_wrap_terminal_authority_read_at<F>(
5881 reader: &dyn ArchiveReadAt,
5882 len: u64,
5883 resolver: &mut F,
5884 options: ReaderOptions,
5885) -> Result<RecoveredRecipientWrapTerminalAuthority, FormatError>
5886where
5887 F: FnMut(
5888 RecipientWrapRecordContext<'_>,
5889 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
5890{
5891 let mut candidates = Vec::new();
5892 if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
5893 let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
5894 collect_v41_recipient_wrap_locator_authority_candidate_read_at(
5895 reader,
5896 final_offset,
5897 0,
5898 resolver,
5899 &mut candidates,
5900 );
5901 }
5902 if len >= LOCATOR_PAIR_LEN as u64 {
5903 let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
5904 collect_v41_recipient_wrap_locator_authority_candidate_read_at(
5905 reader,
5906 mirror_offset,
5907 1,
5908 resolver,
5909 &mut candidates,
5910 );
5911 }
5912
5913 if candidates.is_empty() {
5914 let scan = max_critical_recovery_scan(options)? as u64;
5915 let scan_start = len.saturating_sub(scan);
5916 let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
5917 let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
5918 let mut offset = tail.len().saturating_sub(4);
5919 while offset < tail.len() {
5920 let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
5921 if tail.get(offset..offset + 4) == Some(b"TZCL") {
5922 collect_v41_recipient_wrap_locator_authority_candidate_read_at(
5923 reader,
5924 absolute_offset,
5925 2,
5926 resolver,
5927 &mut candidates,
5928 );
5929 } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
5930 if let Ok(candidate) =
5931 parse_locatorless_cmra_recipient_wrap_authority_candidate_read_at(
5932 reader,
5933 absolute_offset,
5934 resolver,
5935 )
5936 {
5937 candidates.push(candidate);
5938 }
5939 }
5940 if offset == 0 {
5941 break;
5942 }
5943 offset -= 1;
5944 }
5945 }
5946
5947 choose_v41_recipient_wrap_terminal_authority_candidate(candidates)
5948 .map(|candidate| candidate.authority)
5949}
5950
5951fn locate_v41_terminal_authority_read_at(
5952 reader: &dyn ArchiveReadAt,
5953 len: u64,
5954 master_key: &MasterKey,
5955 options: ReaderOptions,
5956) -> Result<RecoveredTerminalAuthority, FormatError> {
5957 let mut candidates = Vec::new();
5958 if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
5959 let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
5960 collect_v41_locator_authority_candidate_read_at(
5961 reader,
5962 final_offset,
5963 0,
5964 master_key,
5965 &mut candidates,
5966 );
5967 }
5968 if len >= LOCATOR_PAIR_LEN as u64 {
5969 let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
5970 collect_v41_locator_authority_candidate_read_at(
5971 reader,
5972 mirror_offset,
5973 1,
5974 master_key,
5975 &mut candidates,
5976 );
5977 }
5978
5979 if candidates.is_empty() {
5980 let scan = max_critical_recovery_scan(options)? as u64;
5981 let scan_start = len.saturating_sub(scan);
5982 let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
5983 let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
5984 let mut offset = tail.len().saturating_sub(4);
5985 while offset < tail.len() {
5986 let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
5987 if tail.get(offset..offset + 4) == Some(b"TZCL") {
5988 collect_v41_locator_authority_candidate_read_at(
5989 reader,
5990 absolute_offset,
5991 2,
5992 master_key,
5993 &mut candidates,
5994 );
5995 } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
5996 if let Ok(candidate) = parse_locatorless_cmra_authority_candidate_read_at(
5997 reader,
5998 absolute_offset,
5999 master_key,
6000 ) {
6001 candidates.push(candidate);
6002 }
6003 }
6004 if offset == 0 {
6005 break;
6006 }
6007 offset -= 1;
6008 }
6009 }
6010
6011 choose_v41_terminal_authority_candidate(candidates).map(|candidate| candidate.authority)
6012}
6013
6014fn locate_v41_terminal_candidate(
6015 bytes: &[u8],
6016 context: KeyHoldingTerminalContext<'_>,
6017 options: ReaderOptions,
6018) -> Result<TerminalCandidate, FormatError> {
6019 let mut candidates = Vec::new();
6020 if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
6021 let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
6022 collect_v41_locator_candidate(bytes, final_offset, 0, context, &mut candidates);
6023 }
6024 if bytes.len() >= LOCATOR_PAIR_LEN {
6025 let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
6026 collect_v41_locator_candidate(bytes, mirror_offset, 1, context, &mut candidates);
6027 }
6028
6029 if candidates.is_empty() {
6030 let scan = max_critical_recovery_scan(options)?;
6031 let scan_start = bytes.len().saturating_sub(scan);
6032 let mut offset = bytes.len().saturating_sub(4);
6033 while offset >= scan_start {
6034 if bytes.get(offset..offset + 4) == Some(b"TZCL") {
6035 collect_v41_locator_candidate(bytes, offset, 2, context, &mut candidates);
6036 } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
6037 if let Ok(candidate) = parse_locatorless_cmra_candidate(bytes, offset, context) {
6038 candidates.push(candidate);
6039 }
6040 }
6041 if offset == 0 {
6042 break;
6043 }
6044 offset -= 1;
6045 }
6046 }
6047
6048 choose_v41_terminal_candidate(candidates)
6049}
6050
6051fn locate_v41_public_terminal(
6052 bytes: &[u8],
6053 volume_header: &VolumeHeader,
6054 crypto_header: &CryptoHeader<'_>,
6055 options: ReaderOptions,
6056) -> Result<V41PublicTerminal, FormatError> {
6057 let mut candidates = Vec::new();
6058 if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
6059 let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
6060 collect_v41_public_locator_candidate(
6061 bytes,
6062 final_offset,
6063 0,
6064 volume_header,
6065 crypto_header,
6066 &mut candidates,
6067 );
6068 }
6069 if bytes.len() >= LOCATOR_PAIR_LEN {
6070 let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
6071 collect_v41_public_locator_candidate(
6072 bytes,
6073 mirror_offset,
6074 1,
6075 volume_header,
6076 crypto_header,
6077 &mut candidates,
6078 );
6079 }
6080
6081 if candidates.is_empty() {
6082 let scan = max_critical_recovery_scan(options)?;
6083 let scan_start = bytes.len().saturating_sub(scan);
6084 let mut offset = bytes.len().saturating_sub(4);
6085 while offset >= scan_start {
6086 if bytes.get(offset..offset + 4) == Some(b"TZCL") {
6087 collect_v41_public_locator_candidate(
6088 bytes,
6089 offset,
6090 2,
6091 volume_header,
6092 crypto_header,
6093 &mut candidates,
6094 );
6095 } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
6096 if let Ok(candidate) = parse_public_locatorless_cmra_candidate(
6097 bytes,
6098 offset,
6099 volume_header,
6100 crypto_header,
6101 ) {
6102 candidates.push(candidate);
6103 }
6104 }
6105 if offset == 0 {
6106 break;
6107 }
6108 offset -= 1;
6109 }
6110 }
6111
6112 choose_v41_public_terminal_candidate(candidates).map(|candidate| candidate.terminal)
6113}
6114
6115fn collect_v41_locator_candidate(
6116 bytes: &[u8],
6117 offset: usize,
6118 expected_sequence: u32,
6119 context: KeyHoldingTerminalContext<'_>,
6120 candidates: &mut Vec<TerminalCandidate>,
6121) {
6122 let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
6123 return;
6124 };
6125 let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
6126 return;
6127 };
6128 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
6129 return;
6130 }
6131 if let Ok(candidate) = parse_locator_cmra_candidate(bytes, offset, locator, context) {
6132 candidates.push(candidate);
6133 }
6134}
6135
6136fn collect_v41_locator_candidate_read_at(
6137 reader: &dyn ArchiveReadAt,
6138 offset: u64,
6139 expected_sequence: u32,
6140 context: KeyHoldingTerminalContext<'_>,
6141 candidates: &mut Vec<TerminalCandidate>,
6142) {
6143 let Ok(raw) = read_at_vec(
6144 reader,
6145 offset,
6146 CRITICAL_RECOVERY_LOCATOR_LEN,
6147 "CriticalRecoveryLocator",
6148 ) else {
6149 return;
6150 };
6151 let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
6152 return;
6153 };
6154 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
6155 return;
6156 }
6157 if let Ok(candidate) = parse_locator_cmra_candidate_read_at(reader, offset, locator, context) {
6158 candidates.push(candidate);
6159 }
6160}
6161
6162fn collect_v41_locator_authority_candidate(
6163 bytes: &[u8],
6164 offset: usize,
6165 expected_sequence: u32,
6166 master_key: &MasterKey,
6167 candidates: &mut Vec<TerminalAuthorityCandidate>,
6168) {
6169 let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
6170 return;
6171 };
6172 let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
6173 return;
6174 };
6175 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
6176 return;
6177 }
6178 if let Ok(candidate) =
6179 parse_locator_cmra_authority_candidate(bytes, offset, locator, master_key)
6180 {
6181 candidates.push(candidate);
6182 }
6183}
6184
6185fn collect_v41_recipient_wrap_locator_authority_candidate<F>(
6186 bytes: &[u8],
6187 offset: usize,
6188 expected_sequence: u32,
6189 resolver: &mut F,
6190 candidates: &mut Vec<RecipientWrapTerminalAuthorityCandidate>,
6191) where
6192 F: FnMut(
6193 RecipientWrapRecordContext<'_>,
6194 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6195{
6196 let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
6197 return;
6198 };
6199 let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
6200 return;
6201 };
6202 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
6203 return;
6204 }
6205 if let Ok(candidate) =
6206 parse_locator_cmra_recipient_wrap_authority_candidate(bytes, offset, locator, resolver)
6207 {
6208 candidates.push(candidate);
6209 }
6210}
6211
6212fn collect_v41_recipient_wrap_locator_authority_candidate_read_at<F>(
6213 reader: &dyn ArchiveReadAt,
6214 offset: u64,
6215 expected_sequence: u32,
6216 resolver: &mut F,
6217 candidates: &mut Vec<RecipientWrapTerminalAuthorityCandidate>,
6218) where
6219 F: FnMut(
6220 RecipientWrapRecordContext<'_>,
6221 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6222{
6223 let Ok(raw) = read_at_vec(
6224 reader,
6225 offset,
6226 CRITICAL_RECOVERY_LOCATOR_LEN,
6227 "CriticalRecoveryLocator",
6228 ) else {
6229 return;
6230 };
6231 let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
6232 return;
6233 };
6234 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
6235 return;
6236 }
6237 if let Ok(candidate) = parse_locator_cmra_recipient_wrap_authority_candidate_read_at(
6238 reader, offset, locator, resolver,
6239 ) {
6240 candidates.push(candidate);
6241 }
6242}
6243
6244fn collect_v41_locator_authority_candidate_read_at(
6245 reader: &dyn ArchiveReadAt,
6246 offset: u64,
6247 expected_sequence: u32,
6248 master_key: &MasterKey,
6249 candidates: &mut Vec<TerminalAuthorityCandidate>,
6250) {
6251 let Ok(raw) = read_at_vec(
6252 reader,
6253 offset,
6254 CRITICAL_RECOVERY_LOCATOR_LEN,
6255 "CriticalRecoveryLocator",
6256 ) else {
6257 return;
6258 };
6259 let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
6260 return;
6261 };
6262 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
6263 return;
6264 }
6265 if let Ok(candidate) =
6266 parse_locator_cmra_authority_candidate_read_at(reader, offset, locator, master_key)
6267 {
6268 candidates.push(candidate);
6269 }
6270}
6271
6272fn collect_v41_public_locator_candidate(
6273 bytes: &[u8],
6274 offset: usize,
6275 expected_sequence: u32,
6276 volume_header: &VolumeHeader,
6277 crypto_header: &CryptoHeader<'_>,
6278 candidates: &mut Vec<PublicTerminalCandidate>,
6279) {
6280 let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
6281 return;
6282 };
6283 let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
6284 return;
6285 };
6286 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
6287 return;
6288 }
6289 if let Ok(candidate) =
6290 parse_public_locator_cmra_candidate(bytes, offset, locator, volume_header, crypto_header)
6291 {
6292 candidates.push(candidate);
6293 }
6294}
6295
6296fn choose_v41_terminal_candidate(
6297 mut candidates: Vec<TerminalCandidate>,
6298) -> Result<TerminalCandidate, FormatError> {
6299 candidates.sort_by_key(|candidate| candidate.anchor);
6300 let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6301 "no valid v41 CMRA candidate found",
6302 ))?;
6303 if let Some(previous) = candidates.last() {
6304 if previous.anchor == winner.anchor
6305 && (previous.cmra_offset != winner.cmra_offset
6306 || previous.cmra_length != winner.cmra_length)
6307 {
6308 return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
6309 }
6310 }
6311 Ok(winner)
6312}
6313
6314fn choose_v41_terminal_authority_candidate(
6315 mut candidates: Vec<TerminalAuthorityCandidate>,
6316) -> Result<TerminalAuthorityCandidate, FormatError> {
6317 candidates.sort_by_key(|candidate| candidate.anchor);
6318 let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6319 "no valid v41 CMRA candidate found",
6320 ))?;
6321 if let Some(previous) = candidates.last() {
6322 if previous.anchor == winner.anchor
6323 && (previous.cmra_offset != winner.cmra_offset
6324 || previous.cmra_length != winner.cmra_length)
6325 {
6326 return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
6327 }
6328 }
6329 Ok(winner)
6330}
6331
6332fn choose_v41_recipient_wrap_terminal_authority_candidate(
6333 mut candidates: Vec<RecipientWrapTerminalAuthorityCandidate>,
6334) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError> {
6335 candidates.sort_by_key(|candidate| candidate.anchor);
6336 let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6337 "no valid v41 CMRA candidate found",
6338 ))?;
6339 if let Some(previous) = candidates.last() {
6340 if previous.anchor == winner.anchor
6341 && (previous.cmra_offset != winner.cmra_offset
6342 || previous.cmra_length != winner.cmra_length)
6343 {
6344 return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
6345 }
6346 }
6347 Ok(winner)
6348}
6349
6350fn choose_v41_public_terminal_candidate(
6351 mut candidates: Vec<PublicTerminalCandidate>,
6352) -> Result<PublicTerminalCandidate, FormatError> {
6353 candidates.sort_by_key(|candidate| candidate.anchor);
6354 let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6355 "no valid v41 public CMRA candidate found",
6356 ))?;
6357 if let Some(previous) = candidates.last() {
6358 if previous.anchor == winner.anchor
6359 && (previous.cmra_offset != winner.cmra_offset
6360 || previous.cmra_length != winner.cmra_length)
6361 {
6362 return Err(FormatError::InvalidArchive(
6363 "ambiguous v41 public CMRA candidates",
6364 ));
6365 }
6366 }
6367 Ok(winner)
6368}
6369
6370fn parse_locator_cmra_candidate(
6371 bytes: &[u8],
6372 locator_offset: usize,
6373 locator: CriticalRecoveryLocator,
6374 context: KeyHoldingTerminalContext<'_>,
6375) -> Result<TerminalCandidate, FormatError> {
6376 let tuple = CmraDecoderTuple::from(locator);
6377 validate_cmra_decoder_tuple(tuple)?;
6378 let expected_cmra_length = cmra_serialized_length(tuple)?;
6379 if locator.cmra_length as u64 != expected_cmra_length {
6380 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6381 }
6382 validate_locator_position(locator_offset, locator)?;
6383 let recovered = recover_cmra(
6384 bytes,
6385 locator.cmra_offset,
6386 Some(tuple),
6387 CmraRecoveryMode::KeyHolding,
6388 )?;
6389 if recovered.tuple != tuple {
6390 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6391 }
6392 if expected_cmra_length != recovered.cmra_length {
6393 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6394 }
6395 validate_locator_image_boundary(locator, &recovered.image)?;
6396 validate_cmra_identity_hints(
6397 recovered.header_hints,
6398 Some(CmraIdentityHints::from(locator)),
6399 &recovered.image,
6400 )?;
6401 let terminal =
6402 validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context, false)?;
6403 Ok(TerminalCandidate {
6404 terminal,
6405 anchor: locator_offset
6406 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6407 .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6408 locator_sequence: Some(locator.locator_sequence),
6409 cmra_offset: locator.cmra_offset,
6410 cmra_length: recovered.cmra_length,
6411 })
6412}
6413
6414fn parse_locator_cmra_candidate_read_at(
6415 reader: &dyn ArchiveReadAt,
6416 locator_offset: u64,
6417 locator: CriticalRecoveryLocator,
6418 context: KeyHoldingTerminalContext<'_>,
6419) -> Result<TerminalCandidate, FormatError> {
6420 let tuple = CmraDecoderTuple::from(locator);
6421 validate_cmra_decoder_tuple(tuple)?;
6422 let expected_cmra_length = cmra_serialized_length(tuple)?;
6423 if locator.cmra_length as u64 != expected_cmra_length {
6424 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6425 }
6426 validate_locator_position(
6427 to_usize(locator_offset, "CriticalRecoveryLocator")?,
6428 locator,
6429 )?;
6430 let recovered = recover_cmra_read_at(
6431 reader,
6432 locator.cmra_offset,
6433 Some(tuple),
6434 CmraRecoveryMode::KeyHolding,
6435 )?;
6436 if recovered.tuple != tuple {
6437 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6438 }
6439 if expected_cmra_length != recovered.cmra_length {
6440 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6441 }
6442 validate_locator_image_boundary(locator, &recovered.image)?;
6443 validate_cmra_identity_hints(
6444 recovered.header_hints,
6445 Some(CmraIdentityHints::from(locator)),
6446 &recovered.image,
6447 )?;
6448 let terminal = validate_recovered_terminal_read_at(
6449 recovered.image,
6450 recovered.tuple,
6451 reader,
6452 context,
6453 false,
6454 )?;
6455 Ok(TerminalCandidate {
6456 terminal,
6457 anchor: to_usize(
6458 checked_u64_add(
6459 locator_offset,
6460 CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6461 "locator anchor overflow",
6462 )?,
6463 "locator anchor overflow",
6464 )?,
6465 locator_sequence: Some(locator.locator_sequence),
6466 cmra_offset: locator.cmra_offset,
6467 cmra_length: recovered.cmra_length,
6468 })
6469}
6470
6471fn parse_locator_cmra_authority_candidate(
6472 bytes: &[u8],
6473 locator_offset: usize,
6474 locator: CriticalRecoveryLocator,
6475 master_key: &MasterKey,
6476) -> Result<TerminalAuthorityCandidate, FormatError> {
6477 let tuple = CmraDecoderTuple::from(locator);
6478 validate_cmra_decoder_tuple(tuple)?;
6479 let expected_cmra_length = cmra_serialized_length(tuple)?;
6480 if locator.cmra_length as u64 != expected_cmra_length {
6481 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6482 }
6483 validate_locator_position(locator_offset, locator)?;
6484 let recovered = recover_cmra(
6485 bytes,
6486 locator.cmra_offset,
6487 Some(tuple),
6488 CmraRecoveryMode::KeyHolding,
6489 )?;
6490 if recovered.tuple != tuple {
6491 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6492 }
6493 if expected_cmra_length != recovered.cmra_length {
6494 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6495 }
6496 validate_locator_image_boundary(locator, &recovered.image)?;
6497 validate_cmra_identity_hints(
6498 recovered.header_hints,
6499 Some(CmraIdentityHints::from(locator)),
6500 &recovered.image,
6501 )?;
6502 let cmra_length = recovered.cmra_length;
6503 let authority =
6504 validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, false)?;
6505 Ok(TerminalAuthorityCandidate {
6506 authority,
6507 anchor: locator_offset
6508 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6509 .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6510 cmra_offset: locator.cmra_offset,
6511 cmra_length,
6512 })
6513}
6514
6515fn parse_locator_cmra_recipient_wrap_authority_candidate<F>(
6516 bytes: &[u8],
6517 locator_offset: usize,
6518 locator: CriticalRecoveryLocator,
6519 resolver: &mut F,
6520) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6521where
6522 F: FnMut(
6523 RecipientWrapRecordContext<'_>,
6524 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6525{
6526 let tuple = CmraDecoderTuple::from(locator);
6527 validate_cmra_decoder_tuple(tuple)?;
6528 let expected_cmra_length = cmra_serialized_length(tuple)?;
6529 if locator.cmra_length as u64 != expected_cmra_length {
6530 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6531 }
6532 validate_locator_position(locator_offset, locator)?;
6533 let recovered = recover_cmra(
6534 bytes,
6535 locator.cmra_offset,
6536 Some(tuple),
6537 CmraRecoveryMode::KeyHolding,
6538 )?;
6539 if recovered.tuple != tuple {
6540 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6541 }
6542 if expected_cmra_length != recovered.cmra_length {
6543 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6544 }
6545 validate_locator_image_boundary(locator, &recovered.image)?;
6546 validate_cmra_identity_hints(
6547 recovered.header_hints,
6548 Some(CmraIdentityHints::from(locator)),
6549 &recovered.image,
6550 )?;
6551 let cmra_length = recovered.cmra_length;
6552 let authority = validate_recovered_recipient_wrap_terminal_authority(
6553 recovered.image,
6554 recovered.tuple,
6555 resolver,
6556 false,
6557 )?;
6558 Ok(RecipientWrapTerminalAuthorityCandidate {
6559 authority,
6560 anchor: locator_offset
6561 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6562 .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6563 cmra_offset: locator.cmra_offset,
6564 cmra_length,
6565 })
6566}
6567
6568fn parse_locator_cmra_recipient_wrap_authority_candidate_read_at<F>(
6569 reader: &dyn ArchiveReadAt,
6570 locator_offset: u64,
6571 locator: CriticalRecoveryLocator,
6572 resolver: &mut F,
6573) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6574where
6575 F: FnMut(
6576 RecipientWrapRecordContext<'_>,
6577 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6578{
6579 let tuple = CmraDecoderTuple::from(locator);
6580 validate_cmra_decoder_tuple(tuple)?;
6581 let expected_cmra_length = cmra_serialized_length(tuple)?;
6582 if locator.cmra_length as u64 != expected_cmra_length {
6583 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6584 }
6585 validate_locator_position(
6586 to_usize(locator_offset, "CriticalRecoveryLocator")?,
6587 locator,
6588 )?;
6589 let recovered = recover_cmra_read_at(
6590 reader,
6591 locator.cmra_offset,
6592 Some(tuple),
6593 CmraRecoveryMode::KeyHolding,
6594 )?;
6595 if recovered.tuple != tuple {
6596 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6597 }
6598 if expected_cmra_length != recovered.cmra_length {
6599 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6600 }
6601 validate_locator_image_boundary(locator, &recovered.image)?;
6602 validate_cmra_identity_hints(
6603 recovered.header_hints,
6604 Some(CmraIdentityHints::from(locator)),
6605 &recovered.image,
6606 )?;
6607 let cmra_length = recovered.cmra_length;
6608 let authority = validate_recovered_recipient_wrap_terminal_authority(
6609 recovered.image,
6610 recovered.tuple,
6611 resolver,
6612 false,
6613 )?;
6614 Ok(RecipientWrapTerminalAuthorityCandidate {
6615 authority,
6616 anchor: to_usize(
6617 checked_u64_add(
6618 locator_offset,
6619 CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6620 "locator anchor overflow",
6621 )?,
6622 "locator anchor overflow",
6623 )?,
6624 cmra_offset: locator.cmra_offset,
6625 cmra_length,
6626 })
6627}
6628
6629fn parse_locator_cmra_authority_candidate_read_at(
6630 reader: &dyn ArchiveReadAt,
6631 locator_offset: u64,
6632 locator: CriticalRecoveryLocator,
6633 master_key: &MasterKey,
6634) -> Result<TerminalAuthorityCandidate, FormatError> {
6635 let tuple = CmraDecoderTuple::from(locator);
6636 validate_cmra_decoder_tuple(tuple)?;
6637 let expected_cmra_length = cmra_serialized_length(tuple)?;
6638 if locator.cmra_length as u64 != expected_cmra_length {
6639 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6640 }
6641 validate_locator_position(
6642 to_usize(locator_offset, "CriticalRecoveryLocator")?,
6643 locator,
6644 )?;
6645 let recovered = recover_cmra_read_at(
6646 reader,
6647 locator.cmra_offset,
6648 Some(tuple),
6649 CmraRecoveryMode::KeyHolding,
6650 )?;
6651 if recovered.tuple != tuple {
6652 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6653 }
6654 if expected_cmra_length != recovered.cmra_length {
6655 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6656 }
6657 validate_locator_image_boundary(locator, &recovered.image)?;
6658 validate_cmra_identity_hints(
6659 recovered.header_hints,
6660 Some(CmraIdentityHints::from(locator)),
6661 &recovered.image,
6662 )?;
6663 let cmra_length = recovered.cmra_length;
6664 let authority =
6665 validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, false)?;
6666 Ok(TerminalAuthorityCandidate {
6667 authority,
6668 anchor: to_usize(
6669 checked_u64_add(
6670 locator_offset,
6671 CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6672 "locator anchor overflow",
6673 )?,
6674 "locator anchor overflow",
6675 )?,
6676 cmra_offset: locator.cmra_offset,
6677 cmra_length,
6678 })
6679}
6680
6681fn parse_public_locator_cmra_candidate(
6682 bytes: &[u8],
6683 locator_offset: usize,
6684 locator: CriticalRecoveryLocator,
6685 volume_header: &VolumeHeader,
6686 crypto_header: &CryptoHeader<'_>,
6687) -> Result<PublicTerminalCandidate, FormatError> {
6688 let tuple = CmraDecoderTuple::from(locator);
6689 validate_cmra_decoder_tuple(tuple)?;
6690 let expected_cmra_length = cmra_serialized_length(tuple)?;
6691 if locator.cmra_length as u64 != expected_cmra_length {
6692 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6693 }
6694 validate_locator_position(locator_offset, locator)?;
6695 let recovered = recover_cmra(
6696 bytes,
6697 locator.cmra_offset,
6698 Some(tuple),
6699 CmraRecoveryMode::PublicNoKey,
6700 )?;
6701 if recovered.tuple != tuple {
6702 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6703 }
6704 if expected_cmra_length != recovered.cmra_length {
6705 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6706 }
6707 validate_locator_image_boundary(locator, &recovered.image)?;
6708 validate_cmra_identity_hints(
6709 recovered.header_hints,
6710 Some(CmraIdentityHints::from(locator)),
6711 &recovered.image,
6712 )?;
6713 let terminal = validate_recovered_public_terminal(
6714 recovered.image,
6715 bytes,
6716 volume_header,
6717 crypto_header,
6718 false,
6719 )?;
6720 Ok(PublicTerminalCandidate {
6721 terminal,
6722 anchor: locator_offset
6723 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6724 .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6725 cmra_offset: locator.cmra_offset,
6726 cmra_length: recovered.cmra_length,
6727 })
6728}
6729
6730fn parse_locatorless_cmra_candidate(
6731 bytes: &[u8],
6732 cmra_offset: usize,
6733 context: KeyHoldingTerminalContext<'_>,
6734) -> Result<TerminalCandidate, FormatError> {
6735 let recovered = recover_cmra(
6736 bytes,
6737 cmra_offset as u64,
6738 None,
6739 CmraRecoveryMode::KeyHolding,
6740 )?;
6741 if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
6742 return Err(FormatError::InvalidArchive(
6743 "locatorless CMRA boundary mismatch",
6744 ));
6745 }
6746 if recovered
6747 .image
6748 .volume_trailer_offset
6749 .checked_add(VOLUME_TRAILER_LEN as u64)
6750 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6751 != cmra_offset as u64
6752 {
6753 return Err(FormatError::InvalidArchive(
6754 "locatorless trailer boundary mismatch",
6755 ));
6756 }
6757 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6758 let terminal =
6759 validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context, true)?;
6760 Ok(TerminalCandidate {
6761 terminal,
6762 anchor: cmra_offset
6763 .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
6764 .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
6765 locator_sequence: None,
6766 cmra_offset: cmra_offset as u64,
6767 cmra_length: recovered.cmra_length,
6768 })
6769}
6770
6771fn parse_locatorless_cmra_candidate_read_at(
6772 reader: &dyn ArchiveReadAt,
6773 cmra_offset: u64,
6774 context: KeyHoldingTerminalContext<'_>,
6775) -> Result<TerminalCandidate, FormatError> {
6776 let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
6777 if recovered.image.body_bytes_before_cmra != cmra_offset {
6778 return Err(FormatError::InvalidArchive(
6779 "locatorless CMRA boundary mismatch",
6780 ));
6781 }
6782 if recovered
6783 .image
6784 .volume_trailer_offset
6785 .checked_add(VOLUME_TRAILER_LEN as u64)
6786 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6787 != cmra_offset
6788 {
6789 return Err(FormatError::InvalidArchive(
6790 "locatorless trailer boundary mismatch",
6791 ));
6792 }
6793 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6794 let terminal = validate_recovered_terminal_read_at(
6795 recovered.image,
6796 recovered.tuple,
6797 reader,
6798 context,
6799 true,
6800 )?;
6801 Ok(TerminalCandidate {
6802 terminal,
6803 anchor: to_usize(
6804 checked_u64_add(cmra_offset, recovered.cmra_length, "CMRA anchor overflow")?,
6805 "CMRA anchor overflow",
6806 )?,
6807 locator_sequence: None,
6808 cmra_offset,
6809 cmra_length: recovered.cmra_length,
6810 })
6811}
6812
6813fn parse_locatorless_cmra_authority_candidate(
6814 bytes: &[u8],
6815 cmra_offset: usize,
6816 master_key: &MasterKey,
6817) -> Result<TerminalAuthorityCandidate, FormatError> {
6818 let recovered = recover_cmra(
6819 bytes,
6820 cmra_offset as u64,
6821 None,
6822 CmraRecoveryMode::KeyHolding,
6823 )?;
6824 if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
6825 return Err(FormatError::InvalidArchive(
6826 "locatorless CMRA boundary mismatch",
6827 ));
6828 }
6829 if recovered
6830 .image
6831 .volume_trailer_offset
6832 .checked_add(VOLUME_TRAILER_LEN as u64)
6833 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6834 != cmra_offset as u64
6835 {
6836 return Err(FormatError::InvalidArchive(
6837 "locatorless trailer boundary mismatch",
6838 ));
6839 }
6840 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6841 let cmra_length = recovered.cmra_length;
6842 let authority =
6843 validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, true)?;
6844 Ok(TerminalAuthorityCandidate {
6845 authority,
6846 anchor: cmra_offset
6847 .checked_add(to_usize(cmra_length, "CMRA")?)
6848 .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
6849 cmra_offset: cmra_offset as u64,
6850 cmra_length,
6851 })
6852}
6853
6854fn parse_locatorless_cmra_recipient_wrap_authority_candidate<F>(
6855 bytes: &[u8],
6856 cmra_offset: usize,
6857 resolver: &mut F,
6858) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6859where
6860 F: FnMut(
6861 RecipientWrapRecordContext<'_>,
6862 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6863{
6864 let recovered = recover_cmra(
6865 bytes,
6866 cmra_offset as u64,
6867 None,
6868 CmraRecoveryMode::KeyHolding,
6869 )?;
6870 if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
6871 return Err(FormatError::InvalidArchive(
6872 "locatorless CMRA boundary mismatch",
6873 ));
6874 }
6875 if recovered
6876 .image
6877 .volume_trailer_offset
6878 .checked_add(VOLUME_TRAILER_LEN as u64)
6879 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6880 != cmra_offset as u64
6881 {
6882 return Err(FormatError::InvalidArchive(
6883 "locatorless trailer boundary mismatch",
6884 ));
6885 }
6886 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6887 let cmra_length = recovered.cmra_length;
6888 let authority = validate_recovered_recipient_wrap_terminal_authority(
6889 recovered.image,
6890 recovered.tuple,
6891 resolver,
6892 true,
6893 )?;
6894 Ok(RecipientWrapTerminalAuthorityCandidate {
6895 authority,
6896 anchor: cmra_offset
6897 .checked_add(to_usize(cmra_length, "CMRA")?)
6898 .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
6899 cmra_offset: cmra_offset as u64,
6900 cmra_length,
6901 })
6902}
6903
6904fn parse_locatorless_cmra_recipient_wrap_authority_candidate_read_at<F>(
6905 reader: &dyn ArchiveReadAt,
6906 cmra_offset: u64,
6907 resolver: &mut F,
6908) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6909where
6910 F: FnMut(
6911 RecipientWrapRecordContext<'_>,
6912 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6913{
6914 let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
6915 if recovered.image.body_bytes_before_cmra != cmra_offset {
6916 return Err(FormatError::InvalidArchive(
6917 "locatorless CMRA boundary mismatch",
6918 ));
6919 }
6920 if recovered
6921 .image
6922 .volume_trailer_offset
6923 .checked_add(VOLUME_TRAILER_LEN as u64)
6924 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6925 != cmra_offset
6926 {
6927 return Err(FormatError::InvalidArchive(
6928 "locatorless trailer boundary mismatch",
6929 ));
6930 }
6931 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6932 let cmra_length = recovered.cmra_length;
6933 let authority = validate_recovered_recipient_wrap_terminal_authority(
6934 recovered.image,
6935 recovered.tuple,
6936 resolver,
6937 true,
6938 )?;
6939 Ok(RecipientWrapTerminalAuthorityCandidate {
6940 authority,
6941 anchor: to_usize(
6942 checked_u64_add(cmra_offset, cmra_length, "CMRA anchor overflow")?,
6943 "CMRA anchor overflow",
6944 )?,
6945 cmra_offset,
6946 cmra_length,
6947 })
6948}
6949
6950fn parse_locatorless_cmra_authority_candidate_read_at(
6951 reader: &dyn ArchiveReadAt,
6952 cmra_offset: u64,
6953 master_key: &MasterKey,
6954) -> Result<TerminalAuthorityCandidate, FormatError> {
6955 let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
6956 if recovered.image.body_bytes_before_cmra != cmra_offset {
6957 return Err(FormatError::InvalidArchive(
6958 "locatorless CMRA boundary mismatch",
6959 ));
6960 }
6961 if recovered
6962 .image
6963 .volume_trailer_offset
6964 .checked_add(VOLUME_TRAILER_LEN as u64)
6965 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6966 != cmra_offset
6967 {
6968 return Err(FormatError::InvalidArchive(
6969 "locatorless trailer boundary mismatch",
6970 ));
6971 }
6972 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6973 let cmra_length = recovered.cmra_length;
6974 let authority =
6975 validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, true)?;
6976 Ok(TerminalAuthorityCandidate {
6977 authority,
6978 anchor: to_usize(
6979 checked_u64_add(cmra_offset, cmra_length, "CMRA anchor overflow")?,
6980 "CMRA anchor overflow",
6981 )?,
6982 cmra_offset,
6983 cmra_length,
6984 })
6985}
6986
6987fn parse_public_locatorless_cmra_candidate(
6988 bytes: &[u8],
6989 cmra_offset: usize,
6990 volume_header: &VolumeHeader,
6991 crypto_header: &CryptoHeader<'_>,
6992) -> Result<PublicTerminalCandidate, FormatError> {
6993 let recovered = recover_cmra(
6994 bytes,
6995 cmra_offset as u64,
6996 None,
6997 CmraRecoveryMode::PublicNoKey,
6998 )?;
6999 if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
7000 return Err(FormatError::InvalidArchive(
7001 "locatorless CMRA boundary mismatch",
7002 ));
7003 }
7004 if recovered
7005 .image
7006 .volume_trailer_offset
7007 .checked_add(VOLUME_TRAILER_LEN as u64)
7008 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
7009 != cmra_offset as u64
7010 {
7011 return Err(FormatError::InvalidArchive(
7012 "locatorless trailer boundary mismatch",
7013 ));
7014 }
7015 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
7016 let terminal = validate_recovered_public_terminal(
7017 recovered.image,
7018 bytes,
7019 volume_header,
7020 crypto_header,
7021 true,
7022 )?;
7023 Ok(PublicTerminalCandidate {
7024 terminal,
7025 anchor: cmra_offset
7026 .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
7027 .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
7028 cmra_offset: cmra_offset as u64,
7029 cmra_length: recovered.cmra_length,
7030 })
7031}
7032
7033fn validate_locator_position(
7034 locator_offset: usize,
7035 locator: CriticalRecoveryLocator,
7036) -> Result<(), FormatError> {
7037 if locator.cmra_offset != locator.body_bytes_before_cmra {
7038 return Err(FormatError::InvalidArchive(
7039 "locator CMRA boundary mismatch",
7040 ));
7041 }
7042 if locator
7043 .volume_trailer_offset
7044 .checked_add(VOLUME_TRAILER_LEN as u64)
7045 .ok_or(FormatError::InvalidArchive("locator trailer overflow"))?
7046 != locator.cmra_offset
7047 {
7048 return Err(FormatError::InvalidArchive(
7049 "locator trailer boundary mismatch",
7050 ));
7051 }
7052 let expected_offset = match locator.locator_sequence {
7053 1 => locator.cmra_offset.checked_add(locator.cmra_length as u64),
7054 0 => locator
7055 .cmra_offset
7056 .checked_add(locator.cmra_length as u64)
7057 .and_then(|value| value.checked_add(CRITICAL_RECOVERY_LOCATOR_LEN as u64)),
7058 _ => None,
7059 }
7060 .ok_or(FormatError::InvalidArchive("locator position overflow"))?;
7061 if expected_offset != locator_offset as u64 {
7062 return Err(FormatError::InvalidArchive(
7063 "locator position does not match sequence",
7064 ));
7065 }
7066 Ok(())
7067}
7068
7069fn validate_locator_image_boundary(
7070 locator: CriticalRecoveryLocator,
7071 image: &CriticalMetadataImage,
7072) -> Result<(), FormatError> {
7073 if locator.volume_format_rev != image.volume_format_rev
7074 || locator.volume_trailer_offset != image.volume_trailer_offset
7075 || locator.body_bytes_before_cmra != image.body_bytes_before_cmra
7076 || image
7077 .volume_trailer_offset
7078 .checked_add(VOLUME_TRAILER_LEN as u64)
7079 .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
7080 != locator.cmra_offset
7081 {
7082 return Err(FormatError::InvalidArchive(
7083 "locator and CMRA image boundaries differ",
7084 ));
7085 }
7086 Ok(())
7087}
7088
7089fn validate_cmra_identity_hints(
7090 header_hints: Option<CmraIdentityHints>,
7091 locator_hints: Option<CmraIdentityHints>,
7092 image: &CriticalMetadataImage,
7093) -> Result<(), FormatError> {
7094 if let (Some(header), Some(locator)) = (header_hints, locator_hints) {
7095 if header != locator {
7096 return Err(FormatError::InvalidArchive(
7097 "CMRA header and locator identity hints differ",
7098 ));
7099 }
7100 }
7101 for hints in [header_hints, locator_hints].into_iter().flatten() {
7102 if hints.archive_uuid != image.archive_uuid
7103 || hints.session_id != image.session_id
7104 || hints.volume_index != image.volume_index
7105 {
7106 return Err(FormatError::InvalidArchive(
7107 "CMRA identity hints do not match recovered image",
7108 ));
7109 }
7110 }
7111 Ok(())
7112}
7113
7114fn recover_cmra(
7115 bytes: &[u8],
7116 cmra_offset: u64,
7117 locator_tuple: Option<CmraDecoderTuple>,
7118 mode: CmraRecoveryMode,
7119) -> Result<RecoveredCmra, FormatError> {
7120 let offset = to_usize(cmra_offset, "CMRA")?;
7121 let header_bytes = slice(
7122 bytes,
7123 offset,
7124 CRITICAL_METADATA_RECOVERY_HEADER_LEN,
7125 "CriticalMetadataRecoveryHeader",
7126 )?;
7127 let (tuple, header_hints) = recover_cmra_header_tuple(header_bytes, locator_tuple)?;
7128 validate_cmra_decoder_tuple(tuple)?;
7129 let cmra_length = cmra_serialized_length(tuple)?;
7130 let cmra_len = to_usize(cmra_length, "CMRA")?;
7131 let cmra_bytes = slice(bytes, offset, cmra_len, "CMRA")?;
7132 recover_cmra_from_bytes(cmra_bytes, tuple, header_hints, cmra_length, mode)
7133}
7134
7135fn recover_cmra_read_at(
7136 reader: &dyn ArchiveReadAt,
7137 cmra_offset: u64,
7138 locator_tuple: Option<CmraDecoderTuple>,
7139 mode: CmraRecoveryMode,
7140) -> Result<RecoveredCmra, FormatError> {
7141 let header_bytes = read_at_vec(
7142 reader,
7143 cmra_offset,
7144 CRITICAL_METADATA_RECOVERY_HEADER_LEN,
7145 "CriticalMetadataRecoveryHeader",
7146 )?;
7147 let (tuple, header_hints) = recover_cmra_header_tuple(&header_bytes, locator_tuple)?;
7148 validate_cmra_decoder_tuple(tuple)?;
7149 let cmra_length = cmra_serialized_length(tuple)?;
7150 let cmra_bytes = read_at_vec(reader, cmra_offset, to_usize(cmra_length, "CMRA")?, "CMRA")?;
7151 recover_cmra_from_bytes(&cmra_bytes, tuple, header_hints, cmra_length, mode)
7152}
7153
7154fn recover_cmra_header_tuple(
7155 header_bytes: &[u8],
7156 locator_tuple: Option<CmraDecoderTuple>,
7157) -> Result<(CmraDecoderTuple, Option<CmraIdentityHints>), FormatError> {
7158 let parsed_header = CriticalMetadataRecoveryHeader::parse(header_bytes);
7159 Ok(match (parsed_header, locator_tuple) {
7160 (Ok(header), Some(locator_tuple)) => {
7161 let header_tuple = CmraDecoderTuple::from(header);
7162 if header_tuple != locator_tuple {
7163 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
7164 }
7165 (locator_tuple, Some(CmraIdentityHints::from(header)))
7166 }
7167 (Ok(header), None) => (
7168 CmraDecoderTuple::from(header),
7169 Some(CmraIdentityHints::from(header)),
7170 ),
7171 (Err(_), Some(tuple)) => (tuple, None),
7172 (Err(err), _) => return Err(err),
7173 })
7174}
7175
7176fn recover_cmra_from_bytes(
7177 cmra_bytes: &[u8],
7178 tuple: CmraDecoderTuple,
7179 header_hints: Option<CmraIdentityHints>,
7180 cmra_length: u64,
7181 mode: CmraRecoveryMode,
7182) -> Result<RecoveredCmra, FormatError> {
7183 let shard_size = tuple.shard_size as usize;
7184 let mut data_shards = vec![None; tuple.data_shard_count as usize];
7185 let mut parity_shards = vec![None; tuple.parity_shard_count as usize];
7186 let mut cursor = CRITICAL_METADATA_RECOVERY_HEADER_LEN;
7187 for idx in 0..(tuple.data_shard_count as usize + tuple.parity_shard_count as usize) {
7188 let raw = slice(
7189 cmra_bytes,
7190 cursor,
7191 CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
7192 "CriticalMetadataRecoveryShard",
7193 )?;
7194 let shard = CriticalMetadataRecoveryShard::parse(raw, shard_size).ok();
7195 if let Some(shard) = shard {
7196 validate_cmra_shard(&shard, idx, tuple)?;
7197 if shard.shard_role == 0 {
7198 let data_slot = data_shards
7199 .get_mut(idx)
7200 .ok_or(FormatError::InvalidArchive("CMRA data shard out of range"))?;
7201 *data_slot = Some(shard.payload);
7202 } else {
7203 let parity_idx = idx - tuple.data_shard_count as usize;
7204 let parity_slot =
7205 parity_shards
7206 .get_mut(parity_idx)
7207 .ok_or(FormatError::InvalidArchive(
7208 "CMRA parity shard out of range",
7209 ))?;
7210 *parity_slot = Some(shard.payload);
7211 }
7212 }
7213 cursor = checked_add(
7214 cursor,
7215 CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
7216 "CriticalMetadataRecoveryShard",
7217 )?;
7218 }
7219 let repaired = repair_data_gf16(&data_shards, &parity_shards, shard_size)?;
7220 let mut image_bytes = Vec::with_capacity(tuple.image_length as usize);
7221 for shard in repaired {
7222 image_bytes.extend_from_slice(&shard);
7223 }
7224 image_bytes.truncate(tuple.image_length as usize);
7225 if sha256_bytes(&image_bytes) != tuple.image_sha256 {
7226 return Err(FormatError::InvalidArchive("CMRA image SHA-256 mismatch"));
7227 }
7228 let image = CriticalMetadataImage::parse(&image_bytes)?;
7229 validate_critical_metadata_image(&image, mode)?;
7230 Ok(RecoveredCmra {
7231 image,
7232 tuple,
7233 header_hints,
7234 cmra_length,
7235 })
7236}
7237
7238fn validate_cmra_decoder_tuple(tuple: CmraDecoderTuple) -> Result<(), FormatError> {
7239 let shard_size = tuple.shard_size as u64;
7240 if !(512..=4096).contains(&shard_size) || shard_size % 2 != 0 {
7241 return Err(FormatError::InvalidArchive("CMRA shard_size is invalid"));
7242 }
7243 let image_length = tuple.image_length as u64;
7244 let min = critical_image_min();
7245 let cap = critical_image_cap()?;
7246 if image_length < min || image_length > cap {
7247 return Err(FormatError::InvalidArchive(
7248 "CMRA image_length is outside bounds",
7249 ));
7250 }
7251 let expected_data_shards = ceil_div_u64(image_length, shard_size)?;
7252 if expected_data_shards == 0 || expected_data_shards != tuple.data_shard_count as u64 {
7253 return Err(FormatError::InvalidArchive(
7254 "CMRA data_shard_count does not match image length",
7255 ));
7256 }
7257 let max_parity = 2u64.max(ceil_div_u64(
7258 checked_u64_mul(
7259 expected_data_shards,
7260 READER_MAX_CMRA_PARITY_PCT as u64,
7261 "CMRA parity overflow",
7262 )?,
7263 100,
7264 )?);
7265 if tuple.parity_shard_count as u64 > max_parity {
7266 return Err(FormatError::ReaderResourceLimitExceeded {
7267 field: "CMRA parity shard count",
7268 cap: max_parity,
7269 actual: tuple.parity_shard_count as u64,
7270 });
7271 }
7272 let total = expected_data_shards
7273 .checked_add(tuple.parity_shard_count as u64)
7274 .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
7275 if total > 65_535 {
7276 return Err(FormatError::FecTooManyShards(total as usize));
7277 }
7278 Ok(())
7279}
7280
7281fn validate_cmra_writer_parity_lower_bound(
7282 tuple: CmraDecoderTuple,
7283 bit_rot_buffer_pct: u8,
7284) -> Result<(), FormatError> {
7285 let min_parity = 2u64.max(ceil_div_u64(
7286 checked_u64_mul(
7287 tuple.data_shard_count as u64,
7288 bit_rot_buffer_pct as u64,
7289 "CMRA parity lower-bound overflow",
7290 )?,
7291 100,
7292 )?);
7293 if (tuple.parity_shard_count as u64) < min_parity {
7294 return Err(FormatError::InvalidArchive(
7295 "CMRA parity shard count is below authenticated bit-rot lower bound",
7296 ));
7297 }
7298 Ok(())
7299}
7300
7301fn validate_cmra_shard(
7302 shard: &CriticalMetadataRecoveryShard,
7303 serialized_idx: usize,
7304 tuple: CmraDecoderTuple,
7305) -> Result<(), FormatError> {
7306 if shard.shard_index as usize != serialized_idx {
7307 return Err(FormatError::InvalidArchive(
7308 "CMRA shards are not in canonical order",
7309 ));
7310 }
7311 let data_count = tuple.data_shard_count as usize;
7312 let shard_size = tuple.shard_size as usize;
7313 if serialized_idx < data_count {
7314 if shard.shard_role != 0 {
7315 return Err(FormatError::InvalidArchive(
7316 "CMRA data shard has wrong role",
7317 ));
7318 }
7319 let expected_len = if serialized_idx + 1 == data_count {
7320 let used = tuple.image_length as usize - serialized_idx * shard_size;
7321 if used == 0 {
7322 shard_size
7323 } else {
7324 used
7325 }
7326 } else {
7327 shard_size
7328 };
7329 if shard.shard_payload_length as usize != expected_len {
7330 return Err(FormatError::InvalidArchive(
7331 "CMRA data shard payload length is non-canonical",
7332 ));
7333 }
7334 if serialized_idx + 1 == data_count
7335 && shard.payload[expected_len..].iter().any(|byte| *byte != 0)
7336 {
7337 return Err(FormatError::InvalidArchive(
7338 "CMRA final data shard padding is non-zero",
7339 ));
7340 }
7341 } else {
7342 if shard.shard_role != 1 {
7343 return Err(FormatError::InvalidArchive(
7344 "CMRA parity shard has wrong role",
7345 ));
7346 }
7347 if shard.shard_payload_length as usize != shard_size {
7348 return Err(FormatError::InvalidArchive(
7349 "CMRA parity shard payload length is non-canonical",
7350 ));
7351 }
7352 }
7353 Ok(())
7354}
7355
7356fn validate_critical_metadata_image(
7357 image: &CriticalMetadataImage,
7358 mode: CmraRecoveryMode,
7359) -> Result<(), FormatError> {
7360 let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
7361 let key_wrap_layout_present = image.layout_flags & 0x0000_0002 != 0;
7362 let key_wrap_region = image.region(6);
7363 if key_wrap_layout_present != key_wrap_region.is_some() {
7364 return Err(FormatError::InvalidArchive(
7365 "CriticalMetadataImage key-wrap layout flag mismatch",
7366 ));
7367 }
7368 let key_wrap_present = key_wrap_layout_present;
7369 if image.volume_header_offset != 0
7370 || image.volume_header_length != VOLUME_HEADER_LEN as u32
7371 || image.crypto_header_offset != VOLUME_HEADER_LEN as u64
7372 || image.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
7373 || image.volume_trailer_length != VOLUME_TRAILER_LEN as u32
7374 || image.body_bytes_before_cmra
7375 != image
7376 .volume_trailer_offset
7377 .checked_add(VOLUME_TRAILER_LEN as u64)
7378 .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
7379 {
7380 return Err(FormatError::InvalidArchive(
7381 "CriticalMetadataImage fixed layout is invalid",
7382 ));
7383 }
7384 if root_auth_present {
7385 if image.root_auth_footer_offset == 0
7386 || image.root_auth_footer_length == 0
7387 || image.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
7388 {
7389 return Err(FormatError::InvalidArchive(
7390 "CriticalMetadataImage root-auth range is invalid",
7391 ));
7392 }
7393 } else if image.root_auth_footer_offset != 0
7394 || image.root_auth_footer_length != 0
7395 || image.root_auth_footer_sha256 != [0u8; 32]
7396 {
7397 return Err(FormatError::InvalidArchive(
7398 "CriticalMetadataImage root-auth fields must be zero when absent",
7399 ));
7400 }
7401 let block_record_len = image_block_record_len_from_region(image)?;
7402 let block_record_len_u64 = u64::try_from(block_record_len)
7403 .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?;
7404 match mode {
7405 CmraRecoveryMode::KeyHolding => {
7406 let expected_len = image.block_count.checked_mul(block_record_len_u64).ok_or(
7407 FormatError::InvalidArchive("BlockRecord region length overflow"),
7408 )?;
7409 if image.block_records_length != expected_len {
7410 return Err(FormatError::InvalidArchive(
7411 "CriticalMetadataImage terminal equations are invalid",
7412 ));
7413 }
7414 }
7415 CmraRecoveryMode::PublicNoKey => {
7416 if image.block_records_length % block_record_len_u64 != 0 {
7417 return Err(FormatError::InvalidArchive(
7418 "CriticalMetadataImage BlockRecord region is not aligned",
7419 ));
7420 }
7421 }
7422 }
7423 let crypto_header_end = image
7424 .crypto_header_offset
7425 .checked_add(image.crypto_header_length as u64)
7426 .ok_or(FormatError::InvalidArchive(
7427 "CryptoHeader boundary overflow",
7428 ))?;
7429 let expected_block_records_offset = if key_wrap_present {
7430 let key_wrap_region = key_wrap_region.ok_or(FormatError::InvalidArchive(
7431 "missing CriticalMetadataImage key-wrap region",
7432 ))?;
7433 if image.key_wrap_table_offset != crypto_header_end
7434 || image.key_wrap_table_length == 0
7435 || key_wrap_region.offset != image.key_wrap_table_offset
7436 || key_wrap_region.bytes.len() != image.key_wrap_table_length as usize
7437 {
7438 return Err(FormatError::InvalidArchive(
7439 "CriticalMetadataImage key-wrap region is malformed",
7440 ));
7441 }
7442 image
7443 .key_wrap_table_offset
7444 .checked_add(image.key_wrap_table_length as u64)
7445 .ok_or(FormatError::InvalidArchive(
7446 "KeyWrapTableV1 boundary overflow",
7447 ))?
7448 } else {
7449 if image.key_wrap_table_offset != 0
7450 || image.key_wrap_table_length != 0
7451 || image.key_wrap_table_sha256 != [0u8; 32]
7452 {
7453 return Err(FormatError::InvalidArchive(
7454 "CriticalMetadataImage key-wrap fields must be zero when absent",
7455 ));
7456 }
7457 crypto_header_end
7458 };
7459 if image.block_records_offset != expected_block_records_offset
7460 || image.manifest_footer_offset
7461 != image
7462 .block_records_offset
7463 .checked_add(image.block_records_length)
7464 .ok_or(FormatError::InvalidArchive(
7465 "ManifestFooter boundary overflow",
7466 ))?
7467 {
7468 return Err(FormatError::InvalidArchive(
7469 "CriticalMetadataImage terminal equations are invalid",
7470 ));
7471 }
7472 let manifest_end = image
7473 .manifest_footer_offset
7474 .checked_add(MANIFEST_FOOTER_LEN as u64)
7475 .ok_or(FormatError::InvalidArchive(
7476 "RootAuthFooter boundary overflow",
7477 ))?;
7478 if root_auth_present {
7479 if image.root_auth_footer_offset != manifest_end
7480 || image
7481 .root_auth_footer_offset
7482 .checked_add(image.root_auth_footer_length as u64)
7483 .ok_or(FormatError::InvalidArchive(
7484 "VolumeTrailer boundary overflow",
7485 ))?
7486 != image.volume_trailer_offset
7487 {
7488 return Err(FormatError::InvalidArchive(
7489 "CriticalMetadataImage root-auth terminal equations are invalid",
7490 ));
7491 }
7492 } else if image.volume_trailer_offset != manifest_end {
7493 return Err(FormatError::InvalidArchive(
7494 "CriticalMetadataImage unsigned terminal equations are invalid",
7495 ));
7496 }
7497 let expected_types: &[u16] = match (key_wrap_present, root_auth_present) {
7498 (false, false) => &[1, 2, 3, 5],
7499 (false, true) => &[1, 2, 3, 4, 5],
7500 (true, false) => &[1, 2, 6, 3, 5],
7501 (true, true) => &[1, 2, 6, 3, 4, 5],
7502 };
7503 if image.regions.len() != expected_types.len()
7504 || image
7505 .regions
7506 .iter()
7507 .map(|region| region.region_type)
7508 .ne(expected_types.iter().copied())
7509 {
7510 return Err(FormatError::InvalidArchive(
7511 "CriticalMetadataImage regions are not canonical",
7512 ));
7513 }
7514 validate_image_region(
7515 image,
7516 1,
7517 image.volume_header_offset,
7518 image.volume_header_length,
7519 )?;
7520 validate_image_region(
7521 image,
7522 2,
7523 image.crypto_header_offset,
7524 image.crypto_header_length,
7525 )?;
7526 validate_image_region(
7527 image,
7528 3,
7529 image.manifest_footer_offset,
7530 image.manifest_footer_length,
7531 )?;
7532 if key_wrap_present {
7533 validate_image_region(
7534 image,
7535 6,
7536 image.key_wrap_table_offset,
7537 image.key_wrap_table_length,
7538 )?;
7539 }
7540 if root_auth_present {
7541 validate_image_region(
7542 image,
7543 4,
7544 image.root_auth_footer_offset,
7545 image.root_auth_footer_length,
7546 )?;
7547 }
7548 validate_image_region(
7549 image,
7550 5,
7551 image.volume_trailer_offset,
7552 image.volume_trailer_length,
7553 )?;
7554 if sha256_region(image, 1)? != image.volume_header_sha256
7555 || sha256_region(image, 2)? != image.crypto_header_sha256
7556 || (key_wrap_present && sha256_region(image, 6)? != image.key_wrap_table_sha256)
7557 || (!key_wrap_present && image.key_wrap_table_sha256 != [0u8; 32])
7558 || sha256_region(image, 3)? != image.manifest_footer_sha256
7559 || (root_auth_present && sha256_region(image, 4)? != image.root_auth_footer_sha256)
7560 || (!root_auth_present && image.root_auth_footer_sha256 != [0u8; 32])
7561 || sha256_region(image, 5)? != image.volume_trailer_sha256
7562 {
7563 return Err(FormatError::InvalidArchive(
7564 "CriticalMetadataImage region digest mismatch",
7565 ));
7566 }
7567 Ok(())
7568}
7569
7570fn image_block_record_len_from_region(image: &CriticalMetadataImage) -> Result<usize, FormatError> {
7571 let crypto_region = image
7572 .region(2)
7573 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7574 let crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
7575 crypto.fixed.validate_supported_profile()?;
7576 Ok(crypto.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN)
7577}
7578
7579fn validate_image_region(
7580 image: &CriticalMetadataImage,
7581 region_type: u16,
7582 offset: u64,
7583 length: u32,
7584) -> Result<(), FormatError> {
7585 let region = image
7586 .region(region_type)
7587 .ok_or(FormatError::InvalidArchive(
7588 "missing CriticalMetadataImage region",
7589 ))?;
7590 if region.offset != offset || region.bytes.len() != length as usize {
7591 return Err(FormatError::InvalidArchive(
7592 "CriticalMetadataImage region range mismatch",
7593 ));
7594 }
7595 Ok(())
7596}
7597
7598fn validate_image_identity(
7599 image: &CriticalMetadataImage,
7600 volume_header: &VolumeHeader,
7601 crypto_header: &CryptoHeaderFixed,
7602) -> Result<(), FormatError> {
7603 if image.volume_format_rev != volume_header.volume_format_rev
7604 || image.archive_uuid != volume_header.archive_uuid
7605 || image.session_id != volume_header.session_id
7606 || image.volume_index != volume_header.volume_index
7607 || image.stripe_width != volume_header.stripe_width
7608 || image.stripe_width != crypto_header.stripe_width
7609 {
7610 return Err(FormatError::InvalidArchive(
7611 "CriticalMetadataImage identity does not match selected volume",
7612 ));
7613 }
7614 Ok(())
7615}
7616
7617fn validate_image_key_wrap_table(
7618 image: &CriticalMetadataImage,
7619 volume_header: &VolumeHeader,
7620 kdf_params: &KdfParams,
7621) -> Result<(), FormatError> {
7622 match kdf_params {
7623 KdfParams::RecipientWrap {
7624 key_wrap_table_length,
7625 key_wrap_table_record_count,
7626 key_wrap_table_digest,
7627 ..
7628 } => {
7629 if image.layout_flags & 0x0000_0002 == 0
7630 || image.key_wrap_table_length != *key_wrap_table_length
7631 {
7632 return Err(FormatError::InvalidArchive(
7633 "CriticalMetadataImage key-wrap fields do not match KdfParams",
7634 ));
7635 }
7636 let region = image.region(6).ok_or(FormatError::InvalidArchive(
7637 "missing CriticalMetadataImage key-wrap region",
7638 ))?;
7639 if region.offset != image.key_wrap_table_offset
7640 || region.bytes.len() != *key_wrap_table_length as usize
7641 {
7642 return Err(FormatError::InvalidArchive(
7643 "CriticalMetadataImage key-wrap region is malformed",
7644 ));
7645 }
7646 if compute_key_wrap_table_digest(*key_wrap_table_length, ®ion.bytes)
7647 != *key_wrap_table_digest
7648 {
7649 return Err(FormatError::IntegrityDigestMismatch {
7650 structure: "KeyWrapTableV1",
7651 });
7652 }
7653 KeyWrapTableV1::parse(
7654 ®ion.bytes,
7655 &volume_header.archive_uuid,
7656 &volume_header.session_id,
7657 *key_wrap_table_length,
7658 *key_wrap_table_record_count,
7659 )?;
7660 Ok(())
7661 }
7662 _ => {
7663 if image.layout_flags & 0x0000_0002 != 0
7664 || image.region(6).is_some()
7665 || image.key_wrap_table_offset != 0
7666 || image.key_wrap_table_length != 0
7667 || image.key_wrap_table_sha256 != [0u8; 32]
7668 {
7669 return Err(FormatError::InvalidArchive(
7670 "CriticalMetadataImage key-wrap fields must be zero when absent",
7671 ));
7672 }
7673 Ok(())
7674 }
7675 }
7676}
7677
7678fn sha256_region(image: &CriticalMetadataImage, region_type: u16) -> Result<[u8; 32], FormatError> {
7679 Ok(sha256_bytes(
7680 &image
7681 .region(region_type)
7682 .ok_or(FormatError::InvalidArchive(
7683 "missing CriticalMetadataImage region",
7684 ))?
7685 .bytes,
7686 ))
7687}
7688
7689fn validate_recovered_terminal_authority(
7690 image: CriticalMetadataImage,
7691 tuple: CmraDecoderTuple,
7692 master_key: &MasterKey,
7693 require_cmra_boundary_magic: bool,
7694) -> Result<RecoveredTerminalAuthority, FormatError> {
7695 let volume_header_region = image
7696 .region(1)
7697 .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
7698 let volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
7699 let crypto_region = image
7700 .region(2)
7701 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7702 let crypto_header_bytes = crypto_region.bytes.clone();
7703 let parsed_crypto = CryptoHeader::parse(&crypto_header_bytes, image.crypto_header_length)?;
7704 let kdf_params = parsed_crypto.kdf_params.clone();
7705 let subkeys = subkeys_for_open(
7706 Some(master_key),
7707 parsed_crypto.fixed.aead_algo,
7708 &volume_header.archive_uuid,
7709 &volume_header.session_id,
7710 )?;
7711 verify_integrity_tag(
7712 HmacDomain::CryptoHeader,
7713 parsed_crypto.fixed.aead_algo,
7714 volume_header.volume_format_rev,
7715 Some(&subkeys.mac_key),
7716 &volume_header.archive_uuid,
7717 &volume_header.session_id,
7718 parsed_crypto.hmac_covered_bytes,
7719 &parsed_crypto.header_hmac,
7720 )?;
7721 parsed_crypto.validate_extension_semantics()?;
7722 validate_seekable_supported_volume(
7723 &volume_header,
7724 &parsed_crypto.fixed,
7725 &parsed_crypto.extensions,
7726 )?;
7727 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
7728 let crypto_header = parsed_crypto.fixed.clone();
7729 if crypto_header.bit_rot_buffer_pct == 0 {
7730 return Err(FormatError::InvalidArchive(
7731 "CMRA startup recovery requires a nonzero bit-rot budget",
7732 ));
7733 }
7734 drop(parsed_crypto);
7735
7736 let terminal = validate_recovered_terminal_inner(
7737 image,
7738 tuple,
7739 require_cmra_boundary_magic,
7740 true,
7741 KeyHoldingTerminalContext {
7742 subkeys: &subkeys,
7743 volume_header: &volume_header,
7744 crypto_header: &crypto_header,
7745 crypto_header_bytes: &crypto_header_bytes,
7746 },
7747 )?;
7748 Ok(RecoveredTerminalAuthority {
7749 terminal,
7750 volume_header,
7751 crypto_header,
7752 crypto_header_bytes,
7753 subkeys,
7754 kdf_params,
7755 })
7756}
7757
7758fn validate_recovered_recipient_wrap_terminal_authority<F>(
7759 image: CriticalMetadataImage,
7760 tuple: CmraDecoderTuple,
7761 resolver: &mut F,
7762 require_cmra_boundary_magic: bool,
7763) -> Result<RecoveredRecipientWrapTerminalAuthority, FormatError>
7764where
7765 F: FnMut(
7766 RecipientWrapRecordContext<'_>,
7767 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
7768{
7769 let volume_header_region = image
7770 .region(1)
7771 .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
7772 let volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
7773 let crypto_region = image
7774 .region(2)
7775 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7776 let crypto_header_bytes = crypto_region.bytes.clone();
7777 let parsed_crypto = CryptoHeader::parse(&crypto_header_bytes, image.crypto_header_length)?;
7778 if !matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
7779 || !parsed_crypto.fixed.aead_algo.is_encrypted()
7780 {
7781 return Err(FormatError::KeyMaterialMismatch);
7782 }
7783 validate_seekable_supported_volume(&volume_header, &parsed_crypto.fixed, &[])?;
7784 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
7785 if parsed_crypto.fixed.bit_rot_buffer_pct == 0 {
7786 return Err(FormatError::InvalidArchive(
7787 "CMRA startup recovery requires a nonzero bit-rot budget",
7788 ));
7789 }
7790 validate_cmra_writer_parity_lower_bound(tuple, parsed_crypto.fixed.bit_rot_buffer_pct)?;
7791 validate_image_key_wrap_table(&image, &volume_header, &parsed_crypto.kdf_params)?;
7792 let key_wrap_region = image.region(6).ok_or(FormatError::InvalidArchive(
7793 "missing CriticalMetadataImage key-wrap region",
7794 ))?;
7795 let startup_key_wrap_table = parse_startup_key_wrap_table_bytes(
7796 &volume_header,
7797 &parsed_crypto.kdf_params,
7798 key_wrap_region.bytes.clone(),
7799 )?;
7800 let subkeys = recipient_wrap_subkeys_from_table(
7801 &volume_header,
7802 &parsed_crypto,
7803 &startup_key_wrap_table.table,
7804 resolver,
7805 )?;
7806 parsed_crypto.validate_extension_semantics()?;
7807 reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
7808 let crypto_header = parsed_crypto.fixed.clone();
7809
7810 let terminal = validate_recovered_terminal_inner(
7811 image,
7812 tuple,
7813 require_cmra_boundary_magic,
7814 true,
7815 KeyHoldingTerminalContext {
7816 subkeys: &subkeys,
7817 volume_header: &volume_header,
7818 crypto_header: &crypto_header,
7819 crypto_header_bytes: &crypto_header_bytes,
7820 },
7821 )?;
7822 Ok(RecoveredRecipientWrapTerminalAuthority {
7823 terminal,
7824 volume_header,
7825 crypto_header,
7826 crypto_header_bytes,
7827 key_wrap_table_bytes: startup_key_wrap_table.bytes,
7828 block_records_start: startup_key_wrap_table.block_records_start,
7829 subkeys,
7830 })
7831}
7832
7833fn validate_recovered_terminal(
7834 image: CriticalMetadataImage,
7835 tuple: CmraDecoderTuple,
7836 bytes: &[u8],
7837 context: KeyHoldingTerminalContext<'_>,
7838 require_cmra_boundary_magic: bool,
7839) -> Result<V41Terminal, FormatError> {
7840 let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
7841 let cmra_boundary_magic_ok = bytes.get(cmra_offset..cmra_offset + 4) == Some(b"TZCR");
7842 validate_recovered_terminal_inner(
7843 image,
7844 tuple,
7845 require_cmra_boundary_magic,
7846 cmra_boundary_magic_ok,
7847 context,
7848 )
7849}
7850
7851fn validate_recovered_terminal_read_at(
7852 image: CriticalMetadataImage,
7853 tuple: CmraDecoderTuple,
7854 reader: &dyn ArchiveReadAt,
7855 context: KeyHoldingTerminalContext<'_>,
7856 require_cmra_boundary_magic: bool,
7857) -> Result<V41Terminal, FormatError> {
7858 let mut magic = [0u8; 4];
7859 reader.read_exact_at(image.body_bytes_before_cmra, &mut magic)?;
7860 validate_recovered_terminal_inner(
7861 image,
7862 tuple,
7863 require_cmra_boundary_magic,
7864 magic == *b"TZCR",
7865 context,
7866 )
7867}
7868
7869fn validate_recovered_terminal_inner(
7870 image: CriticalMetadataImage,
7871 tuple: CmraDecoderTuple,
7872 require_cmra_boundary_magic: bool,
7873 cmra_boundary_magic_ok: bool,
7874 context: KeyHoldingTerminalContext<'_>,
7875) -> Result<V41Terminal, FormatError> {
7876 let subkeys = context.subkeys;
7877 let volume_header = context.volume_header;
7878 let crypto_header = context.crypto_header;
7879 let volume_header_region = image
7880 .region(1)
7881 .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
7882 let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
7883 if &recovered_volume_header != volume_header {
7884 return Err(FormatError::InvalidArchive(
7885 "CMRA VolumeHeader differs from parsed VolumeHeader",
7886 ));
7887 }
7888 validate_image_identity(&image, volume_header, crypto_header)?;
7889 let crypto_region = image
7890 .region(2)
7891 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7892 let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
7893 if recovered_crypto.fixed != *crypto_header {
7894 return Err(FormatError::InvalidArchive(
7895 "CMRA CryptoHeader differs from parsed CryptoHeader",
7896 ));
7897 }
7898 let recovered_pre_hmac_len = crypto_region
7899 .bytes
7900 .len()
7901 .checked_sub(CRYPTO_HEADER_HMAC_LEN)
7902 .ok_or(FormatError::InvalidArchive(
7903 "CMRA CryptoHeader is too short",
7904 ))?;
7905 let parsed_pre_hmac_len = context
7906 .crypto_header_bytes
7907 .len()
7908 .checked_sub(CRYPTO_HEADER_HMAC_LEN)
7909 .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
7910 if recovered_pre_hmac_len != parsed_pre_hmac_len
7911 || crypto_region.bytes[..recovered_pre_hmac_len]
7912 != context.crypto_header_bytes[..parsed_pre_hmac_len]
7913 {
7914 return Err(FormatError::InvalidArchive(
7915 "CMRA CryptoHeader differs from parsed CryptoHeader",
7916 ));
7917 }
7918 verify_integrity_tag(
7919 HmacDomain::CryptoHeader,
7920 recovered_crypto.fixed.aead_algo,
7921 volume_header.volume_format_rev,
7922 Some(&subkeys.mac_key),
7923 &volume_header.archive_uuid,
7924 &volume_header.session_id,
7925 recovered_crypto.hmac_covered_bytes,
7926 &recovered_crypto.header_hmac,
7927 )?;
7928 validate_cmra_writer_parity_lower_bound(tuple, recovered_crypto.fixed.bit_rot_buffer_pct)?;
7929 recovered_crypto.validate_extension_semantics()?;
7930 validate_image_key_wrap_table(&image, volume_header, &recovered_crypto.kdf_params)?;
7931
7932 let manifest_region = image
7933 .region(3)
7934 .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
7935 let manifest_footer = ManifestFooter::parse(&manifest_region.bytes)?;
7936 validate_manifest_footer(
7937 volume_header,
7938 crypto_header,
7939 &manifest_footer,
7940 subkeys,
7941 volume_header.volume_format_rev,
7942 &manifest_region.bytes,
7943 )?;
7944 manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
7945
7946 let root_auth_footer = if image.layout_flags & 0x0000_0001 != 0 {
7947 let root_auth_region = image
7948 .region(4)
7949 .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
7950 let footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
7951 if footer.format_version != volume_header.format_version
7952 || footer.volume_format_rev != volume_header.volume_format_rev
7953 {
7954 return Err(FormatError::InvalidArchive(
7955 "RootAuthFooter format/revision does not match VolumeHeader",
7956 ));
7957 }
7958 if footer.archive_uuid != volume_header.archive_uuid
7959 || footer.session_id != volume_header.session_id
7960 || footer.footer_length()? != image.root_auth_footer_length
7961 {
7962 return Err(FormatError::InvalidArchive(
7963 "RootAuthFooter identity or length does not match terminal image",
7964 ));
7965 }
7966 Some(footer)
7967 } else {
7968 None
7969 };
7970
7971 let trailer_region = image
7972 .region(5)
7973 .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
7974 let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
7975 verify_integrity_tag(
7976 HmacDomain::VolumeTrailer,
7977 crypto_header.aead_algo,
7978 volume_header.volume_format_rev,
7979 Some(&subkeys.mac_key),
7980 &volume_header.archive_uuid,
7981 &volume_header.session_id,
7982 &trailer_region.bytes[..TRAILER_HMAC_COVERED_LEN],
7983 &trailer.trailer_hmac,
7984 )?;
7985 validate_trailer_identity(volume_header, &trailer)?;
7986 validate_v41_trailer_equations(&image, &trailer)?;
7987
7988 if require_cmra_boundary_magic && !cmra_boundary_magic_ok {
7989 return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
7990 }
7991
7992 let manifest_footer_bytes = manifest_region.bytes.clone();
7993 let root_auth_footer_bytes = image.region(4).map(|region| region.bytes.clone());
7994 Ok(V41Terminal {
7995 image,
7996 manifest_footer_bytes,
7997 root_auth_footer_bytes,
7998 root_auth_footer,
7999 volume_trailer: trailer,
8000 })
8001}
8002
8003fn validate_recovered_public_terminal(
8004 image: CriticalMetadataImage,
8005 bytes: &[u8],
8006 volume_header: &VolumeHeader,
8007 public_crypto_header: &CryptoHeader<'_>,
8008 require_cmra_boundary_magic: bool,
8009) -> Result<V41PublicTerminal, FormatError> {
8010 if image.layout_flags & 0x0000_0001 == 0 {
8011 return Err(FormatError::ReaderUnsupported(
8012 "public no-key verification requires root-auth",
8013 ));
8014 }
8015 let volume_header_region = image
8016 .region(1)
8017 .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
8018 let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
8019 if &recovered_volume_header != volume_header {
8020 return Err(FormatError::InvalidArchive(
8021 "CMRA VolumeHeader differs from parsed VolumeHeader",
8022 ));
8023 }
8024 validate_image_identity(&image, volume_header, &public_crypto_header.fixed)?;
8025 let crypto_region = image
8026 .region(2)
8027 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
8028 let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
8029 if !public_crypto_headers_agree(&recovered_crypto.fixed, &public_crypto_header.fixed)
8030 || !public_kdf_profiles_agree(
8031 &recovered_crypto.kdf_params,
8032 &public_crypto_header.kdf_params,
8033 )
8034 {
8035 return Err(FormatError::InvalidArchive(
8036 "CMRA CryptoHeader differs from parsed CryptoHeader",
8037 ));
8038 }
8039 recovered_crypto.validate_extension_semantics()?;
8040 validate_image_key_wrap_table(&image, volume_header, &recovered_crypto.kdf_params)?;
8041
8042 image
8043 .region(3)
8044 .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
8045
8046 let root_auth_region = image
8047 .region(4)
8048 .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
8049 let root_auth_footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
8050 if root_auth_footer.format_version != volume_header.format_version
8051 || root_auth_footer.volume_format_rev != volume_header.volume_format_rev
8052 {
8053 return Err(FormatError::InvalidArchive(
8054 "public RootAuthFooter format/revision does not match VolumeHeader",
8055 ));
8056 }
8057 if root_auth_footer.archive_uuid != volume_header.archive_uuid
8058 || root_auth_footer.session_id != volume_header.session_id
8059 || root_auth_footer.footer_length()? != image.root_auth_footer_length
8060 {
8061 return Err(FormatError::InvalidArchive(
8062 "public RootAuthFooter identity or length does not match terminal image",
8063 ));
8064 }
8065
8066 let trailer_region = image
8067 .region(5)
8068 .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
8069 let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
8070 validate_trailer_identity(volume_header, &trailer)?;
8071 validate_v41_public_trailer_profile(&image, &trailer)?;
8072
8073 let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
8074 if require_cmra_boundary_magic && bytes.get(cmra_offset..cmra_offset + 4) != Some(b"TZCR") {
8075 return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
8076 }
8077
8078 let root_auth_footer_bytes = root_auth_region.bytes.clone();
8079 Ok(V41PublicTerminal {
8080 image,
8081 root_auth_footer_bytes,
8082 root_auth_footer,
8083 })
8084}
8085
8086fn validate_v41_trailer_equations(
8087 image: &CriticalMetadataImage,
8088 trailer: &VolumeTrailer,
8089) -> Result<(), FormatError> {
8090 let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
8091 if trailer.bytes_written != image.volume_trailer_offset
8092 || trailer.manifest_footer_offset != image.manifest_footer_offset
8093 || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
8094 || trailer.block_count != image.block_count
8095 {
8096 return Err(FormatError::InvalidArchive(
8097 "VolumeTrailer does not match v41 terminal layout",
8098 ));
8099 }
8100 if root_auth_present {
8101 if trailer.root_auth_flags != 0x0000_0001
8102 || trailer.root_auth_footer_offset != image.root_auth_footer_offset
8103 || trailer.root_auth_footer_length != image.root_auth_footer_length
8104 || image.root_auth_footer_offset
8105 != image
8106 .manifest_footer_offset
8107 .checked_add(MANIFEST_FOOTER_LEN as u64)
8108 .ok_or(FormatError::InvalidArchive(
8109 "RootAuthFooter trailer boundary overflow",
8110 ))?
8111 || image
8112 .root_auth_footer_offset
8113 .checked_add(image.root_auth_footer_length as u64)
8114 .ok_or(FormatError::InvalidArchive(
8115 "RootAuthFooter trailer boundary overflow",
8116 ))?
8117 != image.volume_trailer_offset
8118 {
8119 return Err(FormatError::InvalidArchive(
8120 "VolumeTrailer root-auth fields do not match v41 terminal layout",
8121 ));
8122 }
8123 } else if trailer.root_auth_footer_offset != 0
8124 || trailer.root_auth_footer_length != 0
8125 || trailer.root_auth_flags != 0
8126 {
8127 return Err(FormatError::InvalidArchive(
8128 "VolumeTrailer root-auth fields must be zero when absent",
8129 ));
8130 }
8131 Ok(())
8132}
8133
8134fn validate_v41_public_trailer_profile(
8135 image: &CriticalMetadataImage,
8136 trailer: &VolumeTrailer,
8137) -> Result<(), FormatError> {
8138 if trailer.bytes_written != image.volume_trailer_offset
8139 || trailer.manifest_footer_offset != image.manifest_footer_offset
8140 || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
8141 {
8142 return Err(FormatError::InvalidArchive(
8143 "VolumeTrailer does not match v41 public terminal layout",
8144 ));
8145 }
8146 if trailer.root_auth_flags != 0x0000_0001
8147 || trailer.root_auth_footer_offset == 0
8148 || trailer.root_auth_footer_length == 0
8149 || trailer.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
8150 || trailer.root_auth_footer_offset != image.root_auth_footer_offset
8151 || trailer.root_auth_footer_length != image.root_auth_footer_length
8152 || image.root_auth_footer_offset
8153 != image
8154 .manifest_footer_offset
8155 .checked_add(MANIFEST_FOOTER_LEN as u64)
8156 .ok_or(FormatError::InvalidArchive(
8157 "RootAuthFooter trailer boundary overflow",
8158 ))?
8159 || image
8160 .root_auth_footer_offset
8161 .checked_add(image.root_auth_footer_length as u64)
8162 .ok_or(FormatError::InvalidArchive(
8163 "RootAuthFooter trailer boundary overflow",
8164 ))?
8165 != image.volume_trailer_offset
8166 {
8167 return Err(FormatError::InvalidArchive(
8168 "VolumeTrailer root-auth fields do not match v41 public terminal layout",
8169 ));
8170 }
8171 Ok(())
8172}
8173
8174fn critical_image_min() -> u64 {
8175 const MIN_CRYPTO_HEADER_LEN: u64 = 116;
8176 CRITICAL_METADATA_IMAGE_FIXED_LEN as u64
8177 + 4 * SERIALIZED_REGION_HEADER_LEN as u64
8178 + VOLUME_HEADER_LEN as u64
8179 + MIN_CRYPTO_HEADER_LEN
8180 + MANIFEST_FOOTER_LEN as u64
8181 + VOLUME_TRAILER_LEN as u64
8182 + IMAGE_CRC_LEN as u64
8183}
8184
8185fn critical_image_cap() -> Result<u64, FormatError> {
8186 [
8187 CRITICAL_METADATA_IMAGE_FIXED_LEN as u64,
8188 6 * SERIALIZED_REGION_HEADER_LEN as u64,
8189 VOLUME_HEADER_LEN as u64,
8190 READER_MAX_CRYPTO_HEADER_LEN as u64,
8191 READER_MAX_KEY_WRAP_TABLE_LEN as u64,
8192 MANIFEST_FOOTER_LEN as u64,
8193 READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
8194 VOLUME_TRAILER_LEN as u64,
8195 IMAGE_CRC_LEN as u64,
8196 ]
8197 .into_iter()
8198 .try_fold(0u64, |total, value| {
8199 total
8200 .checked_add(value)
8201 .ok_or(FormatError::InvalidArchive("critical image cap overflow"))
8202 })
8203}
8204
8205fn cmra_serialized_length(tuple: CmraDecoderTuple) -> Result<u64, FormatError> {
8206 let shard_total = (tuple.data_shard_count as u64)
8207 .checked_add(tuple.parity_shard_count as u64)
8208 .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
8209 let row_len = (CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN as u64)
8210 .checked_add(tuple.shard_size as u64)
8211 .ok_or(FormatError::InvalidArchive("CMRA row length overflow"))?;
8212 checked_u64_mul(shard_total, row_len, "CMRA length overflow")?
8213 .checked_add(CRITICAL_METADATA_RECOVERY_HEADER_LEN as u64)
8214 .ok_or(FormatError::InvalidArchive("CMRA length overflow"))
8215}
8216
8217fn cmra_worst_case_cap() -> Result<u64, FormatError> {
8218 let cap = critical_image_cap()?;
8219 let mut worst = 0u64;
8220 let mut shard_size = 512u64;
8221 while shard_size <= 4096 {
8222 let data = ceil_div_u64(cap, shard_size)?;
8223 let parity = 2u64.max(ceil_div_u64(
8224 checked_u64_mul(data, READER_MAX_CMRA_PARITY_PCT as u64, "CMRA cap overflow")?,
8225 100,
8226 )?);
8227 let tuple = CmraDecoderTuple {
8228 shard_size: shard_size as u32,
8229 data_shard_count: u16::try_from(data)
8230 .map_err(|_| FormatError::InvalidArchive("CMRA cap data shard overflow"))?,
8231 parity_shard_count: u16::try_from(parity)
8232 .map_err(|_| FormatError::InvalidArchive("CMRA cap parity shard overflow"))?,
8233 image_length: u32::try_from(cap)
8234 .map_err(|_| FormatError::InvalidArchive("CMRA cap image overflow"))?,
8235 image_sha256: [0u8; 32],
8236 };
8237 worst = worst.max(cmra_serialized_length(tuple)?);
8238 shard_size += 2;
8239 }
8240 Ok(worst)
8241}
8242
8243pub(crate) fn v41_terminal_tail_cap() -> Result<usize, FormatError> {
8244 let total = [
8245 MANIFEST_FOOTER_LEN as u64,
8246 READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
8247 VOLUME_TRAILER_LEN as u64,
8248 cmra_worst_case_cap()?,
8249 LOCATOR_PAIR_LEN as u64,
8250 ]
8251 .into_iter()
8252 .try_fold(0u64, |sum, value| {
8253 sum.checked_add(value)
8254 .ok_or(FormatError::InvalidArchive("terminal tail cap overflow"))
8255 })?;
8256 usize::try_from(total).map_err(|_| FormatError::InvalidArchive("terminal tail cap overflow"))
8257}
8258
8259fn max_critical_recovery_scan(options: ReaderOptions) -> Result<usize, FormatError> {
8260 let worst = cmra_worst_case_cap()?;
8261 let total = options
8262 .max_trailing_garbage_scan
8263 .try_into()
8264 .map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
8265 .and_then(|scan: u64| {
8266 scan.checked_add(worst)
8267 .and_then(|value| value.checked_add(LOCATOR_PAIR_LEN as u64))
8268 .ok_or(FormatError::InvalidArchive("scan cap overflow"))
8269 })?;
8270 usize::try_from(total).map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
8271}
8272
8273fn validate_bootstrap_single_volume_input(
8274 volume_header: &VolumeHeader,
8275 crypto_header: &CryptoHeaderFixed,
8276) -> Result<(), FormatError> {
8277 if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
8278 return Err(FormatError::ReaderUnsupported(
8279 "bootstrap sidecar reader supports only single-volume archive input",
8280 ));
8281 }
8282 if crypto_header.stripe_width != volume_header.stripe_width {
8283 return Err(FormatError::InvalidArchive(
8284 "VolumeHeader and CryptoHeader stripe_width differ",
8285 ));
8286 }
8287 Ok(())
8288}
8289
8290#[derive(Debug)]
8291struct ParsedBootstrapSidecar {
8292 manifest_footer: Option<ManifestFooter>,
8293 index_root_records_section: Option<(u64, u64)>,
8294 dictionary_records_section: Option<(u64, u64)>,
8295}
8296
8297pub(crate) struct NonSeekableBootstrapMaterial {
8298 pub(crate) manifest_footer: ManifestFooter,
8299 pub(crate) payload_dictionary: Option<Vec<u8>>,
8300}
8301
8302#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8303enum BootstrapSidecarUse {
8304 SeekableAssist,
8305 NonSeekableRandomAccess,
8306}
8307
8308impl ParsedBootstrapSidecar {
8309 fn require_sections_for(
8310 &self,
8311 sidecar_use: BootstrapSidecarUse,
8312 crypto_header: &CryptoHeaderFixed,
8313 ) -> Result<(), FormatError> {
8314 if sidecar_use == BootstrapSidecarUse::NonSeekableRandomAccess {
8315 if self.manifest_footer.is_none() || self.index_root_records_section.is_none() {
8316 return Err(FormatError::ReaderUnsupported(
8317 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
8318 ));
8319 }
8320 if crypto_header.has_dictionary != 0 && self.dictionary_records_section.is_none() {
8321 return Err(FormatError::ReaderUnsupported(
8322 "dictionary bootstrap required",
8323 ));
8324 }
8325 }
8326 Ok(())
8327 }
8328}
8329
8330pub(crate) fn parse_non_seekable_bootstrap_material(
8331 bootstrap_sidecar: &[u8],
8332 volume_header: &VolumeHeader,
8333 crypto_header: &CryptoHeaderFixed,
8334 subkeys: &Subkeys,
8335) -> Result<NonSeekableBootstrapMaterial, FormatError> {
8336 validate_bootstrap_single_volume_input(volume_header, crypto_header)?;
8337 let sidecar =
8338 parse_bootstrap_sidecar(bootstrap_sidecar, volume_header, crypto_header, subkeys)?;
8339 sidecar.require_sections_for(BootstrapSidecarUse::NonSeekableRandomAccess, crypto_header)?;
8340 let manifest_footer = sidecar
8341 .manifest_footer
8342 .clone()
8343 .ok_or(FormatError::ReaderUnsupported(
8344 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
8345 ))?;
8346
8347 let mut blocks = BTreeMap::new();
8348 let (offset, length) =
8349 sidecar
8350 .index_root_records_section
8351 .ok_or(FormatError::ReaderUnsupported(
8352 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
8353 ))?;
8354 let index_root_records = parse_sidecar_block_records(
8355 bootstrap_sidecar,
8356 crypto_header.block_size as usize,
8357 SidecarBlockRecordsSection {
8358 offset,
8359 length,
8360 extent: index_root_extent_from_manifest(&manifest_footer),
8361 data_kind: BlockKind::IndexRootData,
8362 parity_kind: BlockKind::IndexRootParity,
8363 structure: "IndexRoot",
8364 },
8365 )?;
8366 insert_sidecar_records(&mut blocks, index_root_records)?;
8367
8368 let limits = metadata_limits(crypto_header);
8369 let index_root_plaintext = load_metadata_object_from_parts(
8370 &blocks,
8371 ObjectLoadContext::index_root(
8372 volume_header,
8373 crypto_header,
8374 subkeys,
8375 index_root_extent_from_manifest(&manifest_footer),
8376 ),
8377 manifest_footer.index_root_decompressed_size,
8378 )?;
8379 let index_root = IndexRoot::parse(
8380 &index_root_plaintext,
8381 crypto_header.has_dictionary != 0,
8382 limits,
8383 )?;
8384
8385 if crypto_header.has_dictionary != 0 {
8386 let (offset, length) =
8387 sidecar
8388 .dictionary_records_section
8389 .ok_or(FormatError::ReaderUnsupported(
8390 "dictionary bootstrap required",
8391 ))?;
8392 let dictionary_records = parse_sidecar_block_records(
8393 bootstrap_sidecar,
8394 crypto_header.block_size as usize,
8395 SidecarBlockRecordsSection {
8396 offset,
8397 length,
8398 extent: dictionary_extent_from_index_root(&index_root)?,
8399 data_kind: BlockKind::DictionaryData,
8400 parity_kind: BlockKind::DictionaryParity,
8401 structure: "dictionary",
8402 },
8403 )?;
8404 insert_sidecar_records(&mut blocks, dictionary_records)?;
8405 }
8406 let payload_dictionary =
8407 load_archive_dictionary(&blocks, subkeys, volume_header, crypto_header, &index_root)?;
8408
8409 Ok(NonSeekableBootstrapMaterial {
8410 manifest_footer,
8411 payload_dictionary,
8412 })
8413}
8414
8415fn parse_bootstrap_sidecar(
8416 bytes: &[u8],
8417 volume_header: &VolumeHeader,
8418 crypto_header: &CryptoHeaderFixed,
8419 subkeys: &Subkeys,
8420) -> Result<ParsedBootstrapSidecar, FormatError> {
8421 let header_bytes = slice(
8422 bytes,
8423 0,
8424 BOOTSTRAP_SIDECAR_HEADER_LEN,
8425 "BootstrapSidecarHeader",
8426 )?;
8427 let header = BootstrapSidecarHeader::parse(header_bytes)?;
8428 if header.archive_uuid != volume_header.archive_uuid
8429 || header.session_id != volume_header.session_id
8430 {
8431 return Err(FormatError::InvalidArchive(
8432 "bootstrap sidecar identity does not match VolumeHeader",
8433 ));
8434 }
8435 verify_integrity_tag(
8436 HmacDomain::BootstrapSidecar,
8437 crypto_header.aead_algo,
8438 volume_header.volume_format_rev,
8439 Some(&subkeys.mac_key),
8440 &volume_header.archive_uuid,
8441 &volume_header.session_id,
8442 &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
8443 &header.sidecar_hmac,
8444 )?;
8445 header.validate_packed_layout(bytes.len() as u64)?;
8446 validate_sidecar_size_cap(&header, crypto_header, bytes.len() as u64)?;
8447
8448 if header.has_dictionary_records() && crypto_header.has_dictionary == 0 {
8449 return Err(FormatError::InvalidArchive(
8450 "bootstrap sidecar has dictionary records while has_dictionary is false",
8451 ));
8452 }
8453
8454 let manifest_footer = if header.has_manifest_footer() {
8455 let manifest_offset = to_usize(header.manifest_footer_offset, "BootstrapSidecarHeader")?;
8456 let manifest_bytes = slice(
8457 bytes,
8458 manifest_offset,
8459 MANIFEST_FOOTER_LEN,
8460 "ManifestFooter",
8461 )?;
8462 let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
8463 validate_sidecar_manifest_footer(
8464 volume_header,
8465 crypto_header,
8466 &manifest_footer,
8467 subkeys,
8468 volume_header.volume_format_rev,
8469 manifest_bytes,
8470 )?;
8471 manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
8472 Some(manifest_footer)
8473 } else {
8474 None
8475 };
8476
8477 Ok(ParsedBootstrapSidecar {
8478 manifest_footer,
8479 index_root_records_section: header.has_index_root_records().then_some((
8480 header.index_root_records_offset,
8481 header.index_root_records_length,
8482 )),
8483 dictionary_records_section: header.has_dictionary_records().then_some((
8484 header.dictionary_records_offset,
8485 header.dictionary_records_length,
8486 )),
8487 })
8488}
8489
8490fn index_root_extent_from_manifest(manifest_footer: &ManifestFooter) -> ObjectExtent {
8491 ObjectExtent {
8492 first_block_index: manifest_footer.index_root_first_block,
8493 data_block_count: manifest_footer.index_root_data_block_count,
8494 parity_block_count: manifest_footer.index_root_parity_block_count,
8495 encrypted_size: manifest_footer.index_root_encrypted_size,
8496 }
8497}
8498
8499fn insert_sidecar_records(
8500 blocks: &mut BTreeMap<u64, BlockRecord>,
8501 records: Vec<BlockRecord>,
8502) -> Result<(), FormatError> {
8503 for record in records {
8504 if let Some(existing) = blocks.insert(record.block_index, record.clone()) {
8505 if existing != record {
8506 return Err(FormatError::InvalidArchive(
8507 "bootstrap sidecar conflicts with volume BlockRecord",
8508 ));
8509 }
8510 }
8511 }
8512 Ok(())
8513}
8514
8515fn validate_sidecar_manifest_footer(
8516 volume_header: &VolumeHeader,
8517 crypto_header: &CryptoHeaderFixed,
8518 footer: &ManifestFooter,
8519 subkeys: &Subkeys,
8520 volume_format_rev: u16,
8521 raw: &[u8],
8522) -> Result<(), FormatError> {
8523 if footer.archive_uuid != volume_header.archive_uuid
8524 || footer.session_id != volume_header.session_id
8525 {
8526 return Err(FormatError::InvalidArchive(
8527 "sidecar ManifestFooter identity does not match VolumeHeader",
8528 ));
8529 }
8530 if footer.volume_index != 0 {
8531 return Err(FormatError::InvalidArchive(
8532 "sidecar ManifestFooter volume_index must be zero",
8533 ));
8534 }
8535 if footer.total_volumes != crypto_header.stripe_width {
8536 return Err(FormatError::InvalidArchive(
8537 "sidecar ManifestFooter total_volumes does not match stripe_width",
8538 ));
8539 }
8540 if footer.is_authoritative != 1 {
8541 return Err(FormatError::InvalidArchive(
8542 "sidecar ManifestFooter is not authoritative",
8543 ));
8544 }
8545 verify_integrity_tag(
8546 HmacDomain::ManifestFooter,
8547 crypto_header.aead_algo,
8548 volume_format_rev,
8549 Some(&subkeys.mac_key),
8550 &volume_header.archive_uuid,
8551 &volume_header.session_id,
8552 &raw[..MANIFEST_HMAC_COVERED_LEN],
8553 &footer.manifest_hmac,
8554 )
8555}
8556
8557fn validate_sidecar_size_cap(
8558 header: &BootstrapSidecarHeader,
8559 crypto_header: &CryptoHeaderFixed,
8560 file_size: u64,
8561) -> Result<(), FormatError> {
8562 let record_len = checked_u64_add(
8563 crypto_header.block_size as u64,
8564 BLOCK_RECORD_FRAMING_LEN as u64,
8565 "bootstrap sidecar cap overflow",
8566 )?;
8567 let max_index_records = crypto_header.index_root_fec_data_shards as u64
8568 + crypto_header.index_root_fec_parity_shards as u64;
8569 let max_record_section_bytes = checked_u64_mul(
8570 max_index_records,
8571 record_len,
8572 "bootstrap sidecar cap overflow",
8573 )?;
8574 if header.index_root_records_length % record_len != 0 {
8575 return Err(FormatError::InvalidArchive(
8576 "bootstrap sidecar IndexRoot records length is not aligned",
8577 ));
8578 }
8579 if header.index_root_records_length / record_len > max_index_records {
8580 return Err(FormatError::InvalidArchive(
8581 "bootstrap sidecar IndexRoot records exceed resource cap",
8582 ));
8583 }
8584 if header.dictionary_records_length % record_len != 0 {
8585 return Err(FormatError::InvalidArchive(
8586 "bootstrap sidecar dictionary records length is not aligned",
8587 ));
8588 }
8589 if header.dictionary_records_length / record_len > max_index_records {
8590 return Err(FormatError::InvalidArchive(
8591 "bootstrap sidecar dictionary records exceed resource cap",
8592 ));
8593 }
8594
8595 let mut cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64;
8596 if header.has_manifest_footer() {
8597 cap = cap
8598 .checked_add(MANIFEST_FOOTER_LEN as u64)
8599 .ok_or(FormatError::InvalidArchive(
8600 "bootstrap sidecar cap overflow",
8601 ))?;
8602 }
8603 if header.has_index_root_records() {
8604 cap = checked_u64_add(
8605 cap,
8606 max_record_section_bytes,
8607 "bootstrap sidecar cap overflow",
8608 )?;
8609 }
8610 if header.has_dictionary_records() {
8611 cap = checked_u64_add(
8612 cap,
8613 max_record_section_bytes,
8614 "bootstrap sidecar cap overflow",
8615 )?;
8616 }
8617 if file_size > cap {
8618 return Err(FormatError::InvalidArchive(
8619 "bootstrap sidecar exceeds resource cap",
8620 ));
8621 }
8622 Ok(())
8623}
8624
8625#[derive(Debug, Clone, Copy)]
8626struct SidecarBlockRecordsSection {
8627 offset: u64,
8628 length: u64,
8629 extent: ObjectExtent,
8630 data_kind: BlockKind,
8631 parity_kind: BlockKind,
8632 structure: &'static str,
8633}
8634
8635fn parse_sidecar_block_records(
8636 sidecar_bytes: &[u8],
8637 block_size: usize,
8638 section: SidecarBlockRecordsSection,
8639) -> Result<Vec<BlockRecord>, FormatError> {
8640 let record_len = block_size
8641 .checked_add(BLOCK_RECORD_FRAMING_LEN)
8642 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8643 if section.length % record_len as u64 != 0 {
8644 return Err(FormatError::InvalidArchive(
8645 "sidecar BlockRecord section is not aligned",
8646 ));
8647 }
8648 let expected_count =
8649 section.extent.data_block_count as usize + section.extent.parity_block_count as usize;
8650 let actual_count = usize::try_from(section.length / record_len as u64)
8651 .map_err(|_| FormatError::InvalidArchive("sidecar BlockRecord count overflow"))?;
8652 if actual_count != expected_count {
8653 return Err(FormatError::InvalidArchive(
8654 "sidecar BlockRecord section does not match declared extent",
8655 ));
8656 }
8657 let start = to_usize(section.offset, "BootstrapSidecarHeader")?;
8658 let raw = slice(
8659 sidecar_bytes,
8660 start,
8661 to_usize(section.length, "BootstrapSidecarHeader")?,
8662 "BootstrapSidecarHeader",
8663 )?;
8664 let mut records = Vec::with_capacity(expected_count);
8665
8666 for idx in 0..expected_count {
8667 let record = BlockRecord::parse(
8668 slice(raw, idx * record_len, record_len, "BlockRecord")?,
8669 block_size,
8670 )?;
8671 let expected_block_index = checked_u64_add(
8672 section.extent.first_block_index,
8673 idx as u64,
8674 section.structure,
8675 )?;
8676 if record.block_index != expected_block_index {
8677 return Err(FormatError::InvalidArchive(
8678 "sidecar BlockRecord section has missing or duplicate blocks",
8679 ));
8680 }
8681 let expected_kind = if idx < section.extent.data_block_count as usize {
8682 section.data_kind
8683 } else {
8684 section.parity_kind
8685 };
8686 if record.kind != expected_kind {
8687 return Err(FormatError::InvalidArchive(
8688 "sidecar BlockRecord section has wrong kind",
8689 ));
8690 }
8691 let should_be_last = idx + 1 == section.extent.data_block_count as usize;
8692 if idx < section.extent.data_block_count as usize && record.is_last_data() != should_be_last
8693 {
8694 return Err(FormatError::InvalidArchive(
8695 "sidecar BlockRecord section has wrong last-data flag",
8696 ));
8697 }
8698 records.push(record);
8699 }
8700
8701 Ok(records)
8702}
8703
8704fn validate_trailer_identity(
8705 volume_header: &VolumeHeader,
8706 trailer: &VolumeTrailer,
8707) -> Result<(), FormatError> {
8708 if trailer.archive_uuid != volume_header.archive_uuid
8709 || trailer.session_id != volume_header.session_id
8710 || trailer.volume_index != volume_header.volume_index
8711 {
8712 return Err(FormatError::InvalidArchive(
8713 "VolumeTrailer identity does not match VolumeHeader",
8714 ));
8715 }
8716 Ok(())
8717}
8718
8719fn validate_manifest_footer(
8720 volume_header: &VolumeHeader,
8721 crypto_header: &CryptoHeaderFixed,
8722 footer: &ManifestFooter,
8723 subkeys: &Subkeys,
8724 volume_format_rev: u16,
8725 raw: &[u8],
8726) -> Result<(), FormatError> {
8727 if footer.archive_uuid != volume_header.archive_uuid
8728 || footer.session_id != volume_header.session_id
8729 || footer.volume_index != volume_header.volume_index
8730 {
8731 return Err(FormatError::InvalidArchive(
8732 "ManifestFooter identity does not match VolumeHeader",
8733 ));
8734 }
8735 if footer.total_volumes != volume_header.stripe_width {
8736 return Err(FormatError::InvalidArchive(
8737 "ManifestFooter total_volumes does not match stripe_width",
8738 ));
8739 }
8740 if footer.is_authoritative != 1 {
8741 return Err(FormatError::InvalidArchive(
8742 "ManifestFooter is not authoritative",
8743 ));
8744 }
8745 verify_integrity_tag(
8746 HmacDomain::ManifestFooter,
8747 crypto_header.aead_algo,
8748 volume_format_rev,
8749 Some(&subkeys.mac_key),
8750 &volume_header.archive_uuid,
8751 &volume_header.session_id,
8752 &raw[..MANIFEST_HMAC_COVERED_LEN],
8753 &footer.manifest_hmac,
8754 )
8755}
8756
8757#[derive(Debug)]
8758struct ParsedBlockRegion {
8759 blocks: BTreeMap<u64, BlockRecord>,
8760 erased_block_indices: BTreeSet<u64>,
8761}
8762
8763fn parse_block_region(
8764 bytes: &[u8],
8765 start: usize,
8766 end: usize,
8767 block_size: usize,
8768 volume_header: &VolumeHeader,
8769 trailer: &VolumeTrailer,
8770) -> Result<ParsedBlockRegion, FormatError> {
8771 if end < start {
8772 return Err(FormatError::InvalidArchive(
8773 "ManifestFooter starts before BlockRecord region",
8774 ));
8775 }
8776 let record_len = block_size
8777 .checked_add(BLOCK_RECORD_FRAMING_LEN)
8778 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8779 let region_len = end - start;
8780 if region_len % record_len != 0 {
8781 return Err(FormatError::InvalidArchive(
8782 "BlockRecord region length is not aligned",
8783 ));
8784 }
8785 let observed_count = region_len / record_len;
8786 if observed_count as u64 != trailer.block_count {
8787 return Err(FormatError::InvalidArchive(
8788 "VolumeTrailer block_count does not match BlockRecord region",
8789 ));
8790 }
8791
8792 let mut blocks = BTreeMap::new();
8793 let mut erased_block_indices = BTreeSet::new();
8794 for idx in 0..observed_count {
8795 let offset = start + idx * record_len;
8796 let expected_block_index = checked_u64_add(
8797 volume_header.volume_index as u64,
8798 checked_u64_mul(
8799 idx as u64,
8800 volume_header.stripe_width as u64,
8801 "BlockRecord index overflow",
8802 )?,
8803 "BlockRecord index overflow",
8804 )?;
8805 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
8806 match BlockRecord::parse(raw, block_size) {
8807 Ok(record) => {
8808 if record.block_index != expected_block_index {
8809 return Err(FormatError::InvalidArchive(
8810 "BlockRecord index does not match volume position",
8811 ));
8812 }
8813 if blocks.insert(record.block_index, record).is_some() {
8814 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
8815 }
8816 }
8817 Err(err) if block_record_error_is_recoverable_erasure(&err) => {
8818 if !erased_block_indices.insert(expected_block_index) {
8819 return Err(FormatError::InvalidArchive(
8820 "duplicate erased BlockRecord index",
8821 ));
8822 }
8823 }
8824 Err(err) => return Err(err),
8825 }
8826 }
8827
8828 Ok(ParsedBlockRegion {
8829 blocks,
8830 erased_block_indices,
8831 })
8832}
8833
8834fn validate_seekable_block_region_layout(
8835 start: u64,
8836 end: u64,
8837 block_size: usize,
8838 trailer: &VolumeTrailer,
8839) -> Result<(), FormatError> {
8840 if end < start {
8841 return Err(FormatError::InvalidArchive(
8842 "ManifestFooter starts before BlockRecord region",
8843 ));
8844 }
8845 let record_len = block_record_len(block_size)?;
8846 let region_len = end - start;
8847 if region_len % record_len != 0 {
8848 return Err(FormatError::InvalidArchive(
8849 "BlockRecord region length is not aligned",
8850 ));
8851 }
8852 let observed_count = region_len / record_len;
8853 if observed_count != trailer.block_count {
8854 return Err(FormatError::InvalidArchive(
8855 "VolumeTrailer block_count does not match BlockRecord region",
8856 ));
8857 }
8858 Ok(())
8859}
8860
8861fn parse_public_block_observation(
8862 bytes: &[u8],
8863 start: usize,
8864 image: &CriticalMetadataImage,
8865 block_size: usize,
8866 volume_header: &VolumeHeader,
8867) -> Result<BTreeMap<u64, BlockRecord>, FormatError> {
8868 let image_start = to_usize(image.block_records_offset, "BlockRecord")?;
8869 if start != image_start {
8870 return Err(FormatError::InvalidArchive(
8871 "public BlockRecord observation start mismatch",
8872 ));
8873 }
8874 let scan_limit_u64 = image
8875 .block_records_offset
8876 .checked_add(image.block_records_length)
8877 .ok_or(FormatError::InvalidArchive(
8878 "public BlockRecord observation limit overflow",
8879 ))?;
8880 if scan_limit_u64 != image.manifest_footer_offset {
8881 return Err(FormatError::InvalidArchive(
8882 "public BlockRecord observation limit mismatch",
8883 ));
8884 }
8885 let scan_limit = to_usize(scan_limit_u64, "BlockRecord")?;
8886 if scan_limit < start {
8887 return Err(FormatError::InvalidArchive(
8888 "public BlockRecord observation limit before start",
8889 ));
8890 }
8891 let record_len = block_size
8892 .checked_add(BLOCK_RECORD_FRAMING_LEN)
8893 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8894 let region_len = scan_limit - start;
8895 if region_len % record_len != 0 {
8896 return Err(FormatError::InvalidArchive(
8897 "public BlockRecord observation window is not aligned",
8898 ));
8899 }
8900
8901 let mut blocks = BTreeMap::new();
8902 let mut offset = start;
8903 let mut observed_slot = 0u64;
8904 while offset < scan_limit {
8905 let magic_end = checked_add(offset, 4, "BlockRecord")?;
8906 if magic_end > scan_limit || bytes.get(offset..magic_end) != Some(b"TZBK") {
8907 break;
8908 }
8909 let record_end = checked_add(offset, record_len, "BlockRecord")?;
8910 if record_end > scan_limit {
8911 return Err(FormatError::InvalidArchive(
8912 "public BlockRecord observation slot is incomplete",
8913 ));
8914 }
8915 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
8916 let record = BlockRecord::parse(raw, block_size)?;
8917 let expected_block_index = checked_u64_add(
8918 volume_header.volume_index as u64,
8919 checked_u64_mul(
8920 observed_slot,
8921 volume_header.stripe_width as u64,
8922 "BlockRecord index overflow",
8923 )?,
8924 "BlockRecord index overflow",
8925 )?;
8926 if record.block_index != expected_block_index {
8927 return Err(FormatError::InvalidArchive(
8928 "public BlockRecord index does not match volume position",
8929 ));
8930 }
8931 if blocks.insert(record.block_index, record).is_some() {
8932 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
8933 }
8934 offset = record_end;
8935 observed_slot = observed_slot
8936 .checked_add(1)
8937 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
8938 }
8939
8940 let mut scan = if offset < scan_limit {
8941 checked_add(offset, record_len, "BlockRecord")?
8942 } else {
8943 scan_limit
8944 };
8945 while scan < scan_limit {
8946 let magic_end = checked_add(scan, 4, "BlockRecord")?;
8947 let record_end = checked_add(scan, record_len, "BlockRecord")?;
8948 if record_end <= scan_limit && bytes.get(scan..magic_end) == Some(b"TZBK") {
8949 let raw = slice(bytes, scan, record_len, "BlockRecord")?;
8950 if BlockRecord::parse(raw, block_size).is_ok() {
8951 return Err(FormatError::InvalidArchive(
8952 "public observation has ambiguous extra BlockRecord",
8953 ));
8954 }
8955 }
8956 scan = record_end;
8957 }
8958
8959 Ok(blocks)
8960}
8961
8962pub(crate) fn block_record_error_is_recoverable_erasure(error: &FormatError) -> bool {
8963 matches!(
8964 error,
8965 FormatError::BadCrc {
8966 structure: "BlockRecord",
8967 } | FormatError::BadMagic {
8968 structure: "BlockRecord",
8969 } | FormatError::NonZeroReserved {
8970 structure: "BlockRecord",
8971 }
8972 )
8973}
8974
8975fn block_record_len(block_size: usize) -> Result<u64, FormatError> {
8976 let len = block_size
8977 .checked_add(BLOCK_RECORD_FRAMING_LEN)
8978 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8979 u64::try_from(len).map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))
8980}
8981
8982fn checked_u64_mul(lhs: u64, rhs: u64, reason: &'static str) -> Result<u64, FormatError> {
8983 lhs.checked_mul(rhs)
8984 .ok_or(FormatError::InvalidArchive(reason))
8985}
8986
8987fn parse_stream_block_prefix(
8988 bytes: &[u8],
8989 start: usize,
8990 block_size: usize,
8991 volume_header: &VolumeHeader,
8992) -> Result<(BTreeMap<u64, BlockRecord>, usize, u64), FormatError> {
8993 let record_len = block_size
8994 .checked_add(BLOCK_RECORD_FRAMING_LEN)
8995 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8996 let mut blocks = BTreeMap::new();
8997 let mut offset = start;
8998 let mut observed_block_count = 0u64;
8999
9000 while bytes.get(offset..offset + 4) == Some(b"TZBK") {
9001 let expected_block_index =
9002 expected_stream_block_index(volume_header, observed_block_count)?;
9003 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
9004 match BlockRecord::parse(raw, block_size) {
9005 Ok(record) => {
9006 if record.block_index != expected_block_index {
9007 return Err(FormatError::InvalidArchive(
9008 "BlockRecord index does not match stream position",
9009 ));
9010 }
9011 if blocks.insert(record.block_index, record).is_some() {
9012 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
9013 }
9014 }
9015 Err(err) if block_record_error_is_recoverable_erasure(&err) => {}
9016 Err(err) => return Err(err),
9017 }
9018 offset = checked_add(offset, record_len, "BlockRecord")?;
9019 observed_block_count = observed_block_count
9020 .checked_add(1)
9021 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
9022 }
9023
9024 Ok((blocks, offset, observed_block_count))
9025}
9026
9027pub(crate) fn expected_stream_block_index(
9028 volume_header: &VolumeHeader,
9029 observed_block_count: u64,
9030) -> Result<u64, FormatError> {
9031 checked_u64_add(
9032 volume_header.volume_index as u64,
9033 checked_u64_mul(
9034 observed_block_count,
9035 volume_header.stripe_width as u64,
9036 "BlockRecord index overflow",
9037 )?,
9038 "BlockRecord index overflow",
9039 )
9040}
9041
9042fn parse_sequential_block_or_erasure(
9043 bytes: &[u8],
9044 offset: usize,
9045 record_len: usize,
9046 block_size: usize,
9047 volume_header: &VolumeHeader,
9048 observed_block_count: u64,
9049) -> Result<Option<BlockRecord>, FormatError> {
9050 let expected_block_index = expected_stream_block_index(volume_header, observed_block_count)?;
9051 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
9052 match BlockRecord::parse(raw, block_size) {
9053 Ok(record) => {
9054 if record.block_index != expected_block_index {
9055 return Err(FormatError::InvalidArchive(
9056 "BlockRecord index does not match stream position",
9057 ));
9058 }
9059 Ok(Some(record))
9060 }
9061 Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
9062 Err(err) => Err(err),
9063 }
9064}
9065
9066fn parse_terminal_material(
9067 bytes: &[u8],
9068 manifest_offset: usize,
9069 observed_block_count: u64,
9070 context: KeyHoldingTerminalContext<'_>,
9071 options: ReaderOptions,
9072) -> Result<(ManifestFooter, VolumeTrailer, Option<RootAuthFooterV1>), FormatError> {
9073 let candidate = locate_v41_terminal_candidate(bytes, context, options)?;
9074 if !terminal_candidate_reaches_eof(&candidate, bytes.len())? {
9075 return Err(FormatError::InvalidArchive(
9076 "sequential terminal does not end at EOF",
9077 ));
9078 }
9079 let terminal = candidate.terminal;
9080 if terminal.image.manifest_footer_offset != manifest_offset as u64 {
9081 return Err(FormatError::InvalidArchive(
9082 "VolumeTrailer ManifestFooter offset does not match observed stream offset",
9083 ));
9084 }
9085 if terminal.volume_trailer.block_count != observed_block_count {
9086 return Err(FormatError::InvalidArchive(
9087 "VolumeTrailer block_count does not match observed stream",
9088 ));
9089 }
9090 let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
9091 Ok((
9092 manifest_footer,
9093 terminal.volume_trailer,
9094 terminal.root_auth_footer,
9095 ))
9096}
9097
9098pub(crate) fn parse_terminal_material_read_at(
9099 reader: &dyn ArchiveReadAt,
9100 input_len: u64,
9101 manifest_offset: u64,
9102 observed_block_count: u64,
9103 context: KeyHoldingTerminalContext<'_>,
9104) -> Result<SequentialTerminalMaterial, FormatError> {
9105 let mut candidates = Vec::new();
9106 if input_len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
9107 collect_v41_locator_candidate_read_at(
9108 reader,
9109 input_len - CRITICAL_RECOVERY_LOCATOR_LEN as u64,
9110 0,
9111 context,
9112 &mut candidates,
9113 );
9114 }
9115 if input_len >= LOCATOR_PAIR_LEN as u64 {
9116 collect_v41_locator_candidate_read_at(
9117 reader,
9118 input_len - LOCATOR_PAIR_LEN as u64,
9119 1,
9120 context,
9121 &mut candidates,
9122 );
9123 }
9124
9125 let candidate = choose_v41_terminal_candidate(candidates)?;
9126 if !terminal_candidate_reaches_eof(&candidate, to_usize(input_len, "terminal EOF")?)? {
9127 return Err(FormatError::InvalidArchive(
9128 "sequential terminal does not end at EOF",
9129 ));
9130 }
9131 let terminal = candidate.terminal;
9132 if terminal.image.manifest_footer_offset != manifest_offset {
9133 return Err(FormatError::InvalidArchive(
9134 "VolumeTrailer ManifestFooter offset does not match observed stream offset",
9135 ));
9136 }
9137 if terminal.volume_trailer.block_count != observed_block_count {
9138 return Err(FormatError::InvalidArchive(
9139 "VolumeTrailer block_count does not match observed stream",
9140 ));
9141 }
9142 let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
9143 Ok(SequentialTerminalMaterial {
9144 manifest_footer,
9145 volume_trailer: terminal.volume_trailer,
9146 root_auth_footer: terminal.root_auth_footer,
9147 })
9148}
9149
9150fn terminal_candidate_reaches_eof(
9151 candidate: &TerminalCandidate,
9152 input_len: usize,
9153) -> Result<bool, FormatError> {
9154 let expected_end =
9155 match candidate.locator_sequence {
9156 Some(0) => candidate.anchor,
9157 Some(1) => candidate
9158 .anchor
9159 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
9160 .ok_or(FormatError::InvalidArchive(
9161 "terminal EOF boundary overflow",
9162 ))?,
9163 None => candidate.anchor.checked_add(LOCATOR_PAIR_LEN).ok_or(
9164 FormatError::InvalidArchive("terminal EOF boundary overflow"),
9165 )?,
9166 Some(_) => {
9167 return Err(FormatError::InvalidArchive(
9168 "invalid terminal locator sequence",
9169 ))
9170 }
9171 };
9172 Ok(expected_end == input_len)
9173}
9174
9175#[derive(Debug, Default)]
9176struct PendingSequentialEnvelope {
9177 data_shards: Vec<Option<Vec<u8>>>,
9178 parity_shards: Vec<Option<Vec<u8>>>,
9179 saw_last_data: bool,
9180 awaiting_tentative_parity: bool,
9181}
9182
9183impl PendingSequentialEnvelope {
9184 fn is_empty(&self) -> bool {
9185 self.data_shards.is_empty() && self.parity_shards.is_empty()
9186 }
9187}
9188
9189fn handle_sequential_payload_erasure(
9190 pending: &mut PendingSequentialEnvelope,
9191 crypto_header: &CryptoHeaderFixed,
9192 metadata_seen: bool,
9193) -> Result<(), FormatError> {
9194 if metadata_seen || pending.saw_last_data {
9195 return Err(FormatError::BadCrc {
9196 structure: "BlockRecord",
9197 });
9198 }
9199 if !sequential_payload_parity_is_guaranteed(crypto_header) {
9200 return Err(FormatError::BadCrc {
9201 structure: "BlockRecord",
9202 });
9203 }
9204 pending.data_shards.push(None);
9205 pending.awaiting_tentative_parity = true;
9206 if pending.data_shards.len() > crypto_header.fec_data_shards as usize {
9207 return Err(FormatError::InvalidArchive(
9208 "sequential payload envelope exceeds data-shard cap",
9209 ));
9210 }
9211 Ok(())
9212}
9213
9214fn sequential_payload_parity_is_guaranteed(crypto_header: &CryptoHeaderFixed) -> bool {
9215 crypto_header.fec_parity_shards > 0
9216 && (crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0)
9217}
9218
9219fn sequential_extract_tar_stream_with_options(
9220 bytes: &[u8],
9221 master_key: &MasterKey,
9222 options: ReaderOptions,
9223) -> Result<Vec<u8>, FormatError> {
9224 validate_reader_options(options)?;
9225 if bytes.len() < VOLUME_HEADER_LEN {
9226 return Err(FormatError::InvalidLength {
9227 structure: "archive",
9228 expected: VOLUME_HEADER_LEN,
9229 actual: bytes.len(),
9230 });
9231 }
9232
9233 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
9234 let crypto_start = volume_header.crypto_header_offset as usize;
9235 let crypto_len = volume_header.crypto_header_length as usize;
9236 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
9237 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
9238 let subkeys = subkeys_for_open(
9239 Some(master_key),
9240 parsed_crypto.fixed.aead_algo,
9241 &volume_header.archive_uuid,
9242 &volume_header.session_id,
9243 )?;
9244 verify_integrity_tag(
9245 HmacDomain::CryptoHeader,
9246 parsed_crypto.fixed.aead_algo,
9247 volume_header.volume_format_rev,
9248 Some(&subkeys.mac_key),
9249 &volume_header.archive_uuid,
9250 &volume_header.session_id,
9251 parsed_crypto.hmac_covered_bytes,
9252 &parsed_crypto.header_hmac,
9253 )?;
9254 parsed_crypto.validate_extension_semantics()?;
9255 validate_sequential_supported_volume(
9256 &volume_header,
9257 &parsed_crypto.fixed,
9258 &parsed_crypto.extensions,
9259 )?;
9260 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
9261 let block_records_start = startup_block_records_start(
9262 &volume_header,
9263 &parsed_crypto.kdf_params,
9264 |start, length| {
9265 let start = to_usize(start, "KeyWrapTableV1")?;
9266 Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
9267 },
9268 )?;
9269
9270 let block_size = parsed_crypto.fixed.block_size as usize;
9271 let record_len = block_size
9272 .checked_add(BLOCK_RECORD_FRAMING_LEN)
9273 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
9274 let mut offset = to_usize(block_records_start, "BlockRecord")?;
9275 let mut observed_block_count = 0u64;
9276 let mut metadata_seen = false;
9277 let mut pending = PendingSequentialEnvelope::default();
9278 let mut next_envelope_index = 0u64;
9279 let mut tar_stream = Vec::new();
9280 let max_tar_stream_size = options.max_verify_tar_size;
9281 let observed_archive_bytes = observed_archive_size([bytes.len() as u64])?;
9282 let total_extraction_cap = total_extraction_size_cap(options, observed_archive_bytes);
9283 let mut tar_stream_total_validator = TarStreamTotalExtractionSizeValidator::new(
9284 parsed_crypto.fixed.max_path_length,
9285 total_extraction_cap,
9286 );
9287
9288 while bytes.get(offset..offset + 4) == Some(b"TZBK") {
9289 let record = parse_sequential_block_or_erasure(
9290 bytes,
9291 offset,
9292 record_len,
9293 block_size,
9294 &volume_header,
9295 observed_block_count,
9296 )?;
9297 observed_block_count = observed_block_count
9298 .checked_add(1)
9299 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
9300 let Some(record) = record else {
9301 handle_sequential_payload_erasure(&mut pending, &parsed_crypto.fixed, metadata_seen)?;
9302 offset = checked_add(offset, record_len, "BlockRecord")?;
9303 continue;
9304 };
9305
9306 match record.kind {
9307 BlockKind::PayloadData => {
9308 if metadata_seen {
9309 return Err(FormatError::InvalidArchive(
9310 "payload BlockRecord appears after metadata",
9311 ));
9312 }
9313 if pending.awaiting_tentative_parity {
9314 return Err(FormatError::InvalidArchive(
9315 "sequential payload envelope boundary is ambiguous after CRC erasure",
9316 ));
9317 }
9318 if pending.saw_last_data {
9319 finalize_sequential_envelope(
9320 &mut pending,
9321 SequentialEnvelopeDecodeContext {
9322 crypto_header: &parsed_crypto.fixed,
9323 subkeys: &subkeys,
9324 volume_header: &volume_header,
9325 next_envelope_index: &mut next_envelope_index,
9326 tar_stream: &mut tar_stream,
9327 max_tar_stream_size,
9328 tar_stream_total_validator: &mut tar_stream_total_validator,
9329 },
9330 )?;
9331 }
9332 let is_last_data = record.is_last_data();
9333 pending.data_shards.push(Some(record.payload));
9334 if is_last_data {
9335 pending.saw_last_data = true;
9336 }
9337 if pending.data_shards.len() > parsed_crypto.fixed.fec_data_shards as usize {
9338 return Err(FormatError::InvalidArchive(
9339 "sequential payload envelope exceeds data-shard cap",
9340 ));
9341 }
9342 }
9343 BlockKind::PayloadParity => {
9344 if metadata_seen {
9345 return Err(FormatError::InvalidArchive(
9346 "payload parity BlockRecord appears after metadata",
9347 ));
9348 }
9349 if pending.awaiting_tentative_parity {
9350 pending.awaiting_tentative_parity = false;
9351 pending.saw_last_data = true;
9352 } else if pending.data_shards.is_empty() || !pending.saw_last_data {
9353 return Err(FormatError::InvalidArchive(
9354 "payload parity appears before envelope data is complete",
9355 ));
9356 }
9357 pending.parity_shards.push(Some(record.payload));
9358 if pending.parity_shards.len() > parsed_crypto.fixed.fec_parity_shards as usize {
9359 return Err(FormatError::InvalidArchive(
9360 "sequential payload envelope exceeds parity-shard cap",
9361 ));
9362 }
9363 }
9364 _ => {
9365 if !pending.is_empty() {
9366 finalize_sequential_envelope(
9367 &mut pending,
9368 SequentialEnvelopeDecodeContext {
9369 crypto_header: &parsed_crypto.fixed,
9370 subkeys: &subkeys,
9371 volume_header: &volume_header,
9372 next_envelope_index: &mut next_envelope_index,
9373 tar_stream: &mut tar_stream,
9374 max_tar_stream_size,
9375 tar_stream_total_validator: &mut tar_stream_total_validator,
9376 },
9377 )?;
9378 }
9379 metadata_seen = true;
9380 }
9381 }
9382
9383 offset = checked_add(offset, record_len, "BlockRecord")?;
9384 }
9385
9386 if !pending.is_empty() {
9387 finalize_sequential_envelope(
9388 &mut pending,
9389 SequentialEnvelopeDecodeContext {
9390 crypto_header: &parsed_crypto.fixed,
9391 subkeys: &subkeys,
9392 volume_header: &volume_header,
9393 next_envelope_index: &mut next_envelope_index,
9394 tar_stream: &mut tar_stream,
9395 max_tar_stream_size,
9396 tar_stream_total_validator: &mut tar_stream_total_validator,
9397 },
9398 )?;
9399 }
9400
9401 parse_terminal_material(
9402 bytes,
9403 offset,
9404 observed_block_count,
9405 KeyHoldingTerminalContext {
9406 subkeys: &subkeys,
9407 volume_header: &volume_header,
9408 crypto_header: &parsed_crypto.fixed,
9409 crypto_header_bytes: crypto_bytes,
9410 },
9411 options,
9412 )?;
9413 validate_tar_stream_total_extraction_size(
9416 &tar_stream,
9417 parsed_crypto.fixed.max_path_length,
9418 total_extraction_cap,
9419 )?;
9420 Ok(tar_stream)
9421}
9422
9423fn validate_sequential_supported_volume(
9424 volume_header: &VolumeHeader,
9425 crypto_header: &CryptoHeaderFixed,
9426 extensions: &[ExtensionTlv<'_>],
9427) -> Result<(), FormatError> {
9428 reject_unsupported_raw_stream_profile(extensions)?;
9429 if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
9430 return Err(FormatError::ReaderUnsupported(
9431 "sequential reader supports only single-volume archive input",
9432 ));
9433 }
9434 if crypto_header.stripe_width != volume_header.stripe_width {
9435 return Err(FormatError::InvalidArchive(
9436 "VolumeHeader and CryptoHeader stripe_width differ",
9437 ));
9438 }
9439 if crypto_header.has_dictionary != 0 {
9440 return Err(FormatError::ReaderUnsupported(
9441 "dictionary bootstrap required for non-seekable sequential extraction",
9442 ));
9443 }
9444 Ok(())
9445}
9446
9447struct SequentialEnvelopeDecodeContext<'a> {
9448 crypto_header: &'a CryptoHeaderFixed,
9449 subkeys: &'a Subkeys,
9450 volume_header: &'a VolumeHeader,
9451 next_envelope_index: &'a mut u64,
9452 tar_stream: &'a mut Vec<u8>,
9453 max_tar_stream_size: usize,
9454 tar_stream_total_validator: &'a mut TarStreamTotalExtractionSizeValidator,
9455}
9456
9457fn finalize_sequential_envelope(
9458 pending: &mut PendingSequentialEnvelope,
9459 context: SequentialEnvelopeDecodeContext<'_>,
9460) -> Result<(), FormatError> {
9461 if !pending.saw_last_data {
9462 return Err(FormatError::InvalidArchive(
9463 "sequential payload envelope is missing last-data flag",
9464 ));
9465 }
9466 if pending.data_shards.len() > context.crypto_header.fec_data_shards as usize {
9467 return Err(FormatError::InvalidArchive(
9468 "sequential payload envelope exceeds data-shard cap",
9469 ));
9470 }
9471 if pending.parity_shards.len() > context.crypto_header.fec_parity_shards as usize {
9472 return Err(FormatError::InvalidArchive(
9473 "sequential payload envelope exceeds parity-shard cap",
9474 ));
9475 }
9476 let required_parity =
9477 required_object_parity(pending.data_shards.len() as u64, context.crypto_header)?;
9478 if pending.parity_shards.len() < required_parity as usize {
9479 return Err(FormatError::InvalidArchive(
9480 "sequential payload envelope has insufficient parity for recovery settings",
9481 ));
9482 }
9483
9484 let repaired = repair_data_gf16(
9485 &pending.data_shards,
9486 &pending.parity_shards,
9487 context.crypto_header.block_size as usize,
9488 )?;
9489 let mut encrypted =
9490 Vec::with_capacity(repaired.len() * context.crypto_header.block_size as usize);
9491 for shard in repaired {
9492 encrypted.extend_from_slice(&shard);
9493 }
9494 let plaintext = decrypt_padded_aead_object(
9495 AeadObjectContext {
9496 algo: context.crypto_header.aead_algo,
9497 key: &context.subkeys.enc_key,
9498 nonce_seed: &context.subkeys.nonce_seed,
9499 domain: b"envelope",
9500 archive_uuid: &context.volume_header.archive_uuid,
9501 session_id: &context.volume_header.session_id,
9502 counter: *context.next_envelope_index,
9503 },
9504 &encrypted,
9505 )?;
9506 decode_concatenated_zstd_frames_with_cap(
9507 &plaintext,
9508 None,
9509 context.tar_stream,
9510 context.max_tar_stream_size,
9511 Some(context.tar_stream_total_validator),
9512 )?;
9513 *context.next_envelope_index = (*context.next_envelope_index)
9514 .checked_add(1)
9515 .ok_or(FormatError::InvalidArchive("envelope counter overflow"))?;
9516 *pending = PendingSequentialEnvelope::default();
9517 Ok(())
9518}
9519
9520fn decode_concatenated_zstd_frames_with_cap(
9521 plaintext: &[u8],
9522 dictionary: Option<&[u8]>,
9523 output: &mut Vec<u8>,
9524 max_output_len: usize,
9525 mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
9526) -> Result<(), FormatError> {
9527 let mut cursor = 0usize;
9528 while cursor < plaintext.len() {
9529 let frame_len = zstd_safe::find_frame_compressed_size(&plaintext[cursor..])
9530 .map_err(|_| FormatError::InvalidZstdFrame)?;
9531 if frame_len == 0 {
9532 return Err(FormatError::InvalidZstdFrame);
9533 }
9534 let end = checked_add(cursor, frame_len, "zstd frame")?;
9535 validate_exact_zstd_frame(&plaintext[cursor..end])?;
9536 if let Some(dictionary) = dictionary {
9537 let mut decoder =
9538 zstd::stream::Decoder::with_dictionary(&plaintext[cursor..end], dictionary)
9539 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
9540 read_zstd_frame_to_capped_output(
9541 &mut decoder,
9542 output,
9543 max_output_len,
9544 tar_stream_total_validator.as_deref_mut(),
9545 )?;
9546 } else {
9547 let mut decoder = zstd::stream::Decoder::new(&plaintext[cursor..end])
9548 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
9549 read_zstd_frame_to_capped_output(
9550 &mut decoder,
9551 output,
9552 max_output_len,
9553 tar_stream_total_validator.as_deref_mut(),
9554 )?;
9555 }
9556 cursor = end;
9557 }
9558 Ok(())
9559}
9560
9561fn read_zstd_frame_to_capped_output<R: Read>(
9562 decoder: &mut R,
9563 output: &mut Vec<u8>,
9564 max_output_len: usize,
9565 mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
9566) -> Result<(), FormatError> {
9567 let mut buf = [0u8; 64 * 1024];
9568 loop {
9569 let read = decoder
9570 .read(&mut buf)
9571 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
9572 if read == 0 {
9573 return Ok(());
9574 }
9575 let next_len = output
9576 .len()
9577 .checked_add(read)
9578 .ok_or(FormatError::ReaderUnsupported(
9579 "sequential tar stream exceeds configured verification cap",
9580 ))?;
9581 if next_len > max_output_len {
9582 return Err(FormatError::ReaderUnsupported(
9583 "sequential tar stream exceeds configured verification cap",
9584 ));
9585 }
9586 output.extend_from_slice(&buf[..read]);
9587 if let Some(validator) = tar_stream_total_validator.as_mut() {
9588 validator.observe(output)?;
9589 }
9590 }
9591}
9592
9593fn load_archive_dictionary(
9594 blocks: &impl BlockProvider,
9595 subkeys: &Subkeys,
9596 volume_header: &VolumeHeader,
9597 crypto_header: &CryptoHeaderFixed,
9598 index_root: &IndexRoot,
9599) -> Result<Option<Vec<u8>>, FormatError> {
9600 if crypto_header.has_dictionary == 0 {
9601 return Ok(None);
9602 }
9603 let plaintext = load_metadata_object_from_parts(
9604 blocks,
9605 ObjectLoadContext::dictionary(volume_header, crypto_header, subkeys, index_root)?,
9606 index_root.header.dictionary_decompressed_size,
9607 )?;
9608 Ok(Some(plaintext))
9609}
9610
9611#[derive(Clone, Copy)]
9612struct ObjectLoadContext<'a> {
9613 volume_header: &'a VolumeHeader,
9614 crypto_header: &'a CryptoHeaderFixed,
9615 extent: ObjectExtent,
9616 data_kind: BlockKind,
9617 parity_kind: BlockKind,
9618 key: &'a [u8; 32],
9619 nonce_seed: &'a [u8; 32],
9620 domain: &'a [u8],
9621 counter: u64,
9622 class_data_shard_max: u16,
9623 class_parity_shard_max: u16,
9624}
9625
9626impl<'a> ObjectLoadContext<'a> {
9627 fn index_root(
9628 volume_header: &'a VolumeHeader,
9629 crypto_header: &'a CryptoHeaderFixed,
9630 subkeys: &'a Subkeys,
9631 extent: ObjectExtent,
9632 ) -> Self {
9633 Self {
9634 volume_header,
9635 crypto_header,
9636 extent,
9637 data_kind: BlockKind::IndexRootData,
9638 parity_kind: BlockKind::IndexRootParity,
9639 key: &subkeys.index_root_key,
9640 nonce_seed: &subkeys.index_nonce_seed,
9641 domain: b"idxroot",
9642 counter: 0,
9643 class_data_shard_max: crypto_header.index_root_fec_data_shards,
9644 class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
9645 }
9646 }
9647
9648 fn index_shard(
9649 volume_header: &'a VolumeHeader,
9650 crypto_header: &'a CryptoHeaderFixed,
9651 subkeys: &'a Subkeys,
9652 entry: &ShardEntry,
9653 ) -> Self {
9654 Self {
9655 volume_header,
9656 crypto_header,
9657 extent: ObjectExtent {
9658 first_block_index: entry.first_block_index,
9659 data_block_count: entry.data_block_count,
9660 parity_block_count: entry.parity_block_count,
9661 encrypted_size: entry.encrypted_size,
9662 },
9663 data_kind: BlockKind::IndexShardData,
9664 parity_kind: BlockKind::IndexShardParity,
9665 key: &subkeys.index_shard_key,
9666 nonce_seed: &subkeys.index_nonce_seed,
9667 domain: b"idxshard",
9668 counter: entry.shard_index,
9669 class_data_shard_max: crypto_header.index_fec_data_shards,
9670 class_parity_shard_max: crypto_header.index_fec_parity_shards,
9671 }
9672 }
9673
9674 fn directory_hint(
9675 volume_header: &'a VolumeHeader,
9676 crypto_header: &'a CryptoHeaderFixed,
9677 subkeys: &'a Subkeys,
9678 entry: &DirectoryHintShardEntry,
9679 ) -> Self {
9680 Self {
9681 volume_header,
9682 crypto_header,
9683 extent: ObjectExtent {
9684 first_block_index: entry.first_block_index,
9685 data_block_count: entry.data_block_count,
9686 parity_block_count: entry.parity_block_count,
9687 encrypted_size: entry.encrypted_size,
9688 },
9689 data_kind: BlockKind::DirectoryHintData,
9690 parity_kind: BlockKind::DirectoryHintParity,
9691 key: &subkeys.dir_hint_key,
9692 nonce_seed: &subkeys.index_nonce_seed,
9693 domain: b"dirhint",
9694 counter: entry.hint_shard_index,
9695 class_data_shard_max: crypto_header.index_fec_data_shards,
9696 class_parity_shard_max: crypto_header.index_fec_parity_shards,
9697 }
9698 }
9699
9700 fn dictionary(
9701 volume_header: &'a VolumeHeader,
9702 crypto_header: &'a CryptoHeaderFixed,
9703 subkeys: &'a Subkeys,
9704 index_root: &IndexRoot,
9705 ) -> Result<Self, FormatError> {
9706 Ok(Self {
9707 volume_header,
9708 crypto_header,
9709 extent: dictionary_extent_from_index_root(index_root)?,
9710 data_kind: BlockKind::DictionaryData,
9711 parity_kind: BlockKind::DictionaryParity,
9712 key: &subkeys.dictionary_key,
9713 nonce_seed: &subkeys.index_nonce_seed,
9714 domain: b"dict",
9715 counter: 0,
9716 class_data_shard_max: crypto_header.index_root_fec_data_shards,
9717 class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
9718 })
9719 }
9720
9721 fn payload(
9722 volume_header: &'a VolumeHeader,
9723 crypto_header: &'a CryptoHeaderFixed,
9724 subkeys: &'a Subkeys,
9725 envelope: &EnvelopeEntry,
9726 ) -> Self {
9727 Self {
9728 volume_header,
9729 crypto_header,
9730 extent: ObjectExtent {
9731 first_block_index: envelope.first_block_index,
9732 data_block_count: envelope.data_block_count,
9733 parity_block_count: envelope.parity_block_count,
9734 encrypted_size: envelope.encrypted_size,
9735 },
9736 data_kind: BlockKind::PayloadData,
9737 parity_kind: BlockKind::PayloadParity,
9738 key: &subkeys.enc_key,
9739 nonce_seed: &subkeys.nonce_seed,
9740 domain: b"envelope",
9741 counter: envelope.envelope_index,
9742 class_data_shard_max: crypto_header.fec_data_shards,
9743 class_parity_shard_max: crypto_header.fec_parity_shards,
9744 }
9745 }
9746}
9747
9748fn dictionary_extent_from_index_root(index_root: &IndexRoot) -> Result<ObjectExtent, FormatError> {
9749 if index_root.header.dictionary_data_block_count == 0
9750 || index_root.header.dictionary_encrypted_size == 0
9751 || index_root.header.dictionary_decompressed_size == 0
9752 {
9753 return Err(FormatError::InvalidArchive("dictionary bootstrap required"));
9754 }
9755 Ok(ObjectExtent {
9756 first_block_index: index_root.header.dictionary_first_block,
9757 data_block_count: index_root.header.dictionary_data_block_count,
9758 parity_block_count: index_root.header.dictionary_parity_block_count,
9759 encrypted_size: index_root.header.dictionary_encrypted_size,
9760 })
9761}
9762
9763fn load_metadata_object_from_parts(
9764 blocks: &impl BlockProvider,
9765 context: ObjectLoadContext<'_>,
9766 decompressed_size: u32,
9767) -> Result<Vec<u8>, FormatError> {
9768 let compressed = load_decrypted_object_from_parts(blocks, context)?;
9769 decompress_exact_zstd_frame(&compressed, decompressed_size as usize)
9770}
9771
9772fn load_decrypted_object_from_parts(
9773 blocks: &impl BlockProvider,
9774 context: ObjectLoadContext<'_>,
9775) -> Result<Vec<u8>, FormatError> {
9776 load_decrypted_object_from_parts_with_parity_policy(blocks, context, ParityReadPolicy::Always)
9777}
9778
9779fn load_decrypted_object_from_parts_with_parity_policy(
9780 blocks: &impl BlockProvider,
9781 context: ObjectLoadContext<'_>,
9782 parity_policy: ParityReadPolicy,
9783) -> Result<Vec<u8>, FormatError> {
9784 let repaired = load_repaired_object_data_shards_from_parts_with_parity_policy(
9785 blocks,
9786 context.crypto_header,
9787 context.extent,
9788 context.data_kind,
9789 context.parity_kind,
9790 context.class_data_shard_max,
9791 context.class_parity_shard_max,
9792 parity_policy,
9793 )?;
9794 let mut encrypted = Vec::with_capacity(context.extent.encrypted_size as usize);
9795 for shard in repaired {
9796 encrypted.extend_from_slice(&shard);
9797 }
9798 if encrypted.len() != context.extent.encrypted_size as usize {
9799 return Err(FormatError::InvalidArchive(
9800 "object encrypted size does not match repaired shards",
9801 ));
9802 }
9803
9804 decrypt_padded_aead_object(
9805 AeadObjectContext {
9806 algo: context.crypto_header.aead_algo,
9807 key: context.key,
9808 nonce_seed: context.nonce_seed,
9809 domain: context.domain,
9810 archive_uuid: &context.volume_header.archive_uuid,
9811 session_id: &context.volume_header.session_id,
9812 counter: context.counter,
9813 },
9814 &encrypted,
9815 )
9816}
9817
9818fn load_repaired_object_data_shards_from_parts(
9819 blocks: &impl BlockProvider,
9820 crypto_header: &CryptoHeaderFixed,
9821 extent: ObjectExtent,
9822 data_kind: BlockKind,
9823 parity_kind: BlockKind,
9824 class_data_shard_max: u16,
9825 class_parity_shard_max: u16,
9826) -> Result<Vec<Vec<u8>>, FormatError> {
9827 load_repaired_object_data_shards_from_parts_with_parity_policy(
9828 blocks,
9829 crypto_header,
9830 extent,
9831 data_kind,
9832 parity_kind,
9833 class_data_shard_max,
9834 class_parity_shard_max,
9835 ParityReadPolicy::Always,
9836 )
9837}
9838
9839#[allow(clippy::too_many_arguments)]
9840fn load_repaired_object_data_shards_from_parts_with_parity_policy(
9841 blocks: &impl BlockProvider,
9842 crypto_header: &CryptoHeaderFixed,
9843 extent: ObjectExtent,
9844 data_kind: BlockKind,
9845 parity_kind: BlockKind,
9846 class_data_shard_max: u16,
9847 class_parity_shard_max: u16,
9848 parity_policy: ParityReadPolicy,
9849) -> Result<Vec<Vec<u8>>, FormatError> {
9850 validate_object_extent(
9851 extent,
9852 crypto_header,
9853 class_data_shard_max,
9854 class_parity_shard_max,
9855 )?;
9856 let block_size = crypto_header.block_size as usize;
9857 let data_count = extent.data_block_count as usize;
9858 let parity_count = extent.parity_block_count as usize;
9859 let mut data_shards = Vec::with_capacity(data_count);
9860 let mut parity_shards = Vec::with_capacity(parity_count);
9861
9862 for offset in 0..data_count {
9863 let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
9864 if let Some(record) = blocks.block(block_index)? {
9865 if record.kind != data_kind {
9866 return Err(FormatError::InvalidArchive(
9867 "object data block has unexpected kind",
9868 ));
9869 }
9870 let should_be_last = offset + 1 == data_count;
9871 if record.is_last_data() != should_be_last {
9872 return Err(FormatError::InvalidArchive(
9873 "object last-data flag is not on the final data block",
9874 ));
9875 }
9876 data_shards.push(Some(record.payload.clone()));
9877 } else {
9878 data_shards.push(None);
9879 }
9880 }
9881
9882 if parity_policy == ParityReadPolicy::RepairOnly && data_shards.iter().all(Option::is_some) {
9883 return repair_data_gf16(&data_shards, &[], block_size);
9884 }
9885
9886 for offset in 0..parity_count {
9887 let block_index = checked_u64_add(
9888 extent.first_block_index,
9889 data_count as u64 + offset as u64,
9890 "object",
9891 )?;
9892 if let Some(record) = blocks.block(block_index)? {
9893 if record.kind != parity_kind {
9894 return Err(FormatError::InvalidArchive(
9895 "object parity block has unexpected kind",
9896 ));
9897 }
9898 if record.is_last_data() {
9899 return Err(FormatError::InvalidArchive(
9900 "object parity block has last-data flag",
9901 ));
9902 }
9903 parity_shards.push(Some(record.payload.clone()));
9904 } else {
9905 parity_shards.push(None);
9906 }
9907 }
9908
9909 repair_data_gf16(&data_shards, &parity_shards, block_size)
9910}
9911
9912fn validate_object_extent(
9913 extent: ObjectExtent,
9914 crypto_header: &CryptoHeaderFixed,
9915 class_data_shard_max: u16,
9916 class_parity_shard_max: u16,
9917) -> Result<(), FormatError> {
9918 if extent.data_block_count == 0 || extent.encrypted_size == 0 {
9919 return Err(FormatError::InvalidArchive(
9920 "encrypted object has zero data blocks or size",
9921 ));
9922 }
9923 if extent.data_block_count > class_data_shard_max as u32 {
9924 return Err(FormatError::InvalidArchive(
9925 "encrypted object exceeds its class data-shard maximum",
9926 ));
9927 }
9928 if extent.parity_block_count > class_parity_shard_max as u32 {
9929 return Err(FormatError::InvalidArchive(
9930 "encrypted object exceeds its class parity-shard maximum",
9931 ));
9932 }
9933 let required_parity = required_object_parity(extent.data_block_count as u64, crypto_header)?;
9934 if extent.parity_block_count != required_parity {
9935 return Err(FormatError::InvalidArchive(
9936 "encrypted object parity does not match v41 compute_parity",
9937 ));
9938 }
9939 let total = checked_u64_add(
9940 extent.data_block_count as u64,
9941 extent.parity_block_count as u64,
9942 "encrypted object shard count overflow",
9943 )?;
9944 if total > 65_535 {
9945 return Err(FormatError::FecTooManyShards(total as usize));
9946 }
9947 let expected = checked_u64_mul(
9948 extent.data_block_count as u64,
9949 crypto_header.block_size as u64,
9950 "encrypted object size overflow",
9951 )?;
9952 if expected != extent.encrypted_size as u64 {
9953 return Err(FormatError::InvalidArchive(
9954 "encrypted object size is not data_block_count * block_size",
9955 ));
9956 }
9957 if extent.encrypted_size as usize <= crypto_header.aead_algo.tag_len() {
9958 return Err(FormatError::InvalidArchive(
9959 "encrypted object is too small for AEAD tag",
9960 ));
9961 }
9962 Ok(())
9963}
9964
9965pub(crate) fn required_object_parity(
9966 data_block_count: u64,
9967 crypto_header: &CryptoHeaderFixed,
9968) -> Result<u32, FormatError> {
9969 let min_parity =
9970 if crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0 {
9971 1
9972 } else {
9973 0
9974 };
9975 let mut parity = 0u64;
9976 for _ in 0..100 {
9977 let total = data_block_count
9978 .checked_add(parity)
9979 .ok_or(FormatError::InvalidArchive("parity total overflow"))?;
9980 let by_volume = checked_u64_mul(
9981 crypto_header.volume_loss_tolerance as u64,
9982 ceil_div_u64(total, crypto_header.stripe_width as u64)?,
9983 "volume-loss parity overflow",
9984 )?;
9985 let by_bitrot = ceil_div_u64(
9986 checked_u64_mul(
9987 total,
9988 crypto_header.bit_rot_buffer_pct as u64,
9989 "bit-rot parity overflow",
9990 )?,
9991 100,
9992 )?;
9993 let next = by_volume
9994 .checked_add(by_bitrot)
9995 .ok_or(FormatError::InvalidArchive("parity overflow"))?
9996 .max(min_parity);
9997 if next == parity {
9998 return u32::try_from(next)
9999 .map_err(|_| FormatError::InvalidArchive("parity count overflow"));
10000 }
10001 parity = next;
10002 }
10003 Err(FormatError::InvalidArchive(
10004 "parity calculation did not converge",
10005 ))
10006}
10007
10008fn ceil_div_u64(numerator: u64, denominator: u64) -> Result<u64, FormatError> {
10009 if denominator == 0 {
10010 return Err(FormatError::InvalidArchive("division by zero"));
10011 }
10012 numerator
10013 .checked_add(denominator - 1)
10014 .ok_or(FormatError::InvalidArchive("ceiling division overflow"))
10015 .map(|value| value / denominator)
10016}
10017
10018fn frame_range_for_file<'b>(
10019 shard: &'b IndexShard,
10020 file: &FileEntry,
10021) -> Result<&'b [FrameEntry], FormatError> {
10022 let start = shard
10023 .frames
10024 .binary_search_by_key(&file.first_frame_index, |entry| entry.frame_index)
10025 .map_err(|_| FormatError::InvalidArchive("FileEntry references missing FrameEntry"))?;
10026 let count = usize::try_from(file.frame_count)
10027 .map_err(|_| FormatError::InvalidArchive("FileEntry frame count overflow"))?;
10028 let end = start.checked_add(count).ok_or(FormatError::InvalidArchive(
10029 "FileEntry frame range overflow",
10030 ))?;
10031 let frames = shard
10032 .frames
10033 .get(start..end)
10034 .ok_or(FormatError::InvalidArchive(
10035 "FileEntry references missing FrameEntry",
10036 ))?;
10037 for (offset, frame) in frames.iter().enumerate() {
10038 let expected = file.first_frame_index.checked_add(offset as u64).ok_or(
10039 FormatError::InvalidArchive("FileEntry frame range overflow"),
10040 )?;
10041 if frame.frame_index != expected {
10042 return Err(FormatError::InvalidArchive(
10043 "FileEntry references missing FrameEntry",
10044 ));
10045 }
10046 }
10047 Ok(frames)
10048}
10049
10050fn metadata_limits(crypto_header: &CryptoHeaderFixed) -> MetadataLimits {
10051 MetadataLimits {
10052 block_size: crypto_header.block_size,
10053 max_path_length: crypto_header.max_path_length,
10054 max_payload_data_shards: crypto_header.fec_data_shards,
10055 max_payload_parity_shards: crypto_header.fec_parity_shards,
10056 max_index_data_shards: crypto_header.index_fec_data_shards,
10057 max_index_parity_shards: crypto_header.index_fec_parity_shards,
10058 max_index_root_data_shards: crypto_header.index_root_fec_data_shards,
10059 max_index_root_parity_shards: crypto_header.index_root_fec_parity_shards,
10060 ..MetadataLimits::default()
10061 }
10062}
10063
10064fn verify_dense_keys<T>(
10065 entries: &BTreeMap<u64, T>,
10066 expected_count: u64,
10067 structure: &'static str,
10068) -> Result<(), FormatError> {
10069 if entries.len() as u64 != expected_count {
10070 return Err(FormatError::InvalidArchive(
10071 "decoded table count does not match IndexRoot",
10072 ));
10073 }
10074 for expected in 0..expected_count {
10075 if !entries.contains_key(&expected) {
10076 return Err(FormatError::InvalidMetadata {
10077 structure,
10078 reason: "global index coverage has a gap",
10079 });
10080 }
10081 }
10082 Ok(())
10083}
10084
10085fn validate_envelope_frame_coverage(
10086 frames: &BTreeMap<u64, FrameEntry>,
10087 envelopes: &BTreeMap<u64, EnvelopeEntry>,
10088) -> Result<(), FormatError> {
10089 let mut accounted_frames = BTreeSet::new();
10090 for envelope in envelopes.values() {
10091 let first = envelope.first_frame_index;
10092 let end =
10093 first
10094 .checked_add(envelope.frame_count as u64)
10095 .ok_or(FormatError::InvalidArchive(
10096 "EnvelopeEntry frame range overflow",
10097 ))?;
10098 let mut ranges = Vec::with_capacity(envelope.frame_count as usize);
10099 for frame_index in first..end {
10100 let frame = frames.get(&frame_index).ok_or(FormatError::InvalidArchive(
10101 "EnvelopeEntry references missing FrameEntry",
10102 ))?;
10103 if frame.envelope_index != envelope.envelope_index {
10104 return Err(FormatError::InvalidArchive(
10105 "FrameEntry envelope_index does not match containing EnvelopeEntry",
10106 ));
10107 }
10108 if !accounted_frames.insert(frame_index) {
10109 return Err(FormatError::InvalidArchive(
10110 "FrameEntry is covered by multiple EnvelopeEntries",
10111 ));
10112 }
10113 let start = frame.offset_in_envelope as usize;
10114 let end = checked_add(start, frame.compressed_size as usize, "FrameEntry")?;
10115 if end > envelope.plaintext_size as usize {
10116 return Err(FormatError::InvalidArchive(
10117 "FrameEntry exceeds EnvelopeEntry plaintext_size",
10118 ));
10119 }
10120 ranges.push((start, end));
10121 }
10122 validate_exact_coverage_ranges(
10123 &mut ranges,
10124 envelope.plaintext_size as usize,
10125 "EnvelopeEntry frame coverage has a gap or overlap",
10126 )?;
10127 }
10128
10129 for frame_index in frames.keys() {
10130 if !accounted_frames.contains(frame_index) {
10131 return Err(FormatError::InvalidArchive(
10132 "FrameEntry is not covered by any EnvelopeEntry",
10133 ));
10134 }
10135 }
10136 Ok(())
10137}
10138
10139fn validate_global_file_table_order(shards: &[IndexShard]) -> Result<(), FormatError> {
10140 let mut previous = None::<([u8; 8], Vec<u8>, u64)>;
10141 for shard in shards {
10142 for (idx, file) in shard.files.iter().enumerate() {
10143 let path = shard
10144 .file_path(idx)
10145 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?
10146 .to_vec();
10147 let start = shard
10148 .tar_member_group_start(idx)
10149 .ok_or(FormatError::InvalidArchive(
10150 "FileEntry tar member start is missing",
10151 ))?;
10152 let key = (file.path_hash, path, start);
10153 validate_global_file_table_key_step(previous.as_ref(), &key)?;
10154 previous = Some(key);
10155 }
10156 }
10157 Ok(())
10158}
10159
10160fn validate_global_file_table_key_step(
10161 previous: Option<&([u8; 8], Vec<u8>, u64)>,
10162 current: &([u8; 8], Vec<u8>, u64),
10163) -> Result<(), FormatError> {
10164 if let Some(previous) = previous {
10165 if previous >= current {
10166 return Err(FormatError::InvalidArchive(
10167 "global FileEntry rows are not sorted and unique",
10168 ));
10169 }
10170 }
10171 Ok(())
10172}
10173
10174fn validate_file_extent_coverage_ranges(
10175 extents: &[(u64, u64)],
10176 tar_len: u64,
10177) -> Result<(), FormatError> {
10178 let mut ranges = Vec::with_capacity(extents.len());
10179 for (start, len) in extents {
10180 let end = checked_u64_add(*start, *len, "FileEntry")?;
10181 if end > tar_len {
10182 return Err(FormatError::InvalidArchive(
10183 "FileEntry extent exceeds IndexRoot tar_total_size",
10184 ));
10185 }
10186 ranges.push((*start, end));
10187 }
10188 validate_exact_coverage_ranges_u64(
10189 &mut ranges,
10190 tar_len,
10191 "FileEntry extents do not cover tar stream exactly",
10192 )
10193}
10194
10195fn add_expected_directory_hint_rows(
10196 map: &mut DirectoryHintMap,
10197 shard_row_index: u32,
10198 path: &[u8],
10199 kind: TarEntryKind,
10200) {
10201 map.entry(Vec::new()).or_default().insert(shard_row_index);
10202 for (idx, byte) in path.iter().enumerate() {
10203 if *byte == b'/' {
10204 map.entry(path[..idx].to_vec())
10205 .or_default()
10206 .insert(shard_row_index);
10207 }
10208 }
10209 if kind == TarEntryKind::Directory {
10210 map.entry(path.to_vec())
10211 .or_default()
10212 .insert(shard_row_index);
10213 }
10214}
10215
10216fn validate_directory_hint_tables_against_expected(
10217 tables: &[DirectoryHintTable],
10218 expected: &DirectoryHintMap,
10219) -> Result<(), FormatError> {
10220 let mut actual = Vec::new();
10221 let mut previous_key: Option<([u8; 8], Vec<u8>)> = None;
10222
10223 for table in tables {
10224 for entry_index in 0..table.entries.len() {
10225 let path = table
10226 .entry_path(entry_index)
10227 .ok_or(FormatError::InvalidArchive(
10228 "DirectoryHintEntry path is missing",
10229 ))?;
10230 let key = (hash_prefix(path), path.to_vec());
10231 if let Some(previous) = &previous_key {
10232 if previous >= &key {
10233 return Err(FormatError::InvalidArchive(
10234 "DirectoryHintEntry rows are not globally sorted",
10235 ));
10236 }
10237 }
10238 previous_key = Some(key);
10239
10240 let rows =
10241 table
10242 .shard_rows_for_entry(entry_index)
10243 .ok_or(FormatError::InvalidArchive(
10244 "DirectoryHintEntry shard rows are missing",
10245 ))?;
10246 actual.push((path.to_vec(), rows.to_vec()));
10247 }
10248 }
10249
10250 if actual != sorted_directory_hint_rows(expected) {
10251 return Err(FormatError::InvalidArchive(
10252 "directory hint map does not match decoded files",
10253 ));
10254 }
10255 Ok(())
10256}
10257
10258fn sorted_directory_hint_rows(map: &DirectoryHintMap) -> Vec<(Vec<u8>, Vec<u32>)> {
10259 let mut rows = map
10260 .iter()
10261 .map(|(path, shard_rows)| {
10262 (
10263 path.clone(),
10264 shard_rows.iter().copied().collect::<Vec<u32>>(),
10265 )
10266 })
10267 .collect::<Vec<_>>();
10268 rows.sort_by(|(left_path, _), (right_path, _)| {
10269 hash_prefix(left_path)
10270 .cmp(&hash_prefix(right_path))
10271 .then_with(|| left_path.cmp(right_path))
10272 });
10273 rows
10274}
10275
10276fn validate_exact_coverage_ranges(
10277 ranges: &mut [(usize, usize)],
10278 expected_end: usize,
10279 reason: &'static str,
10280) -> Result<(), FormatError> {
10281 ranges.sort_unstable();
10282 let mut cursor = 0usize;
10283 for (start, end) in ranges.iter().copied() {
10284 if start != cursor || end < start {
10285 return Err(FormatError::InvalidArchive(reason));
10286 }
10287 cursor = end;
10288 }
10289 if cursor != expected_end {
10290 return Err(FormatError::InvalidArchive(reason));
10291 }
10292 Ok(())
10293}
10294
10295fn validate_exact_coverage_ranges_u64(
10296 ranges: &mut [(u64, u64)],
10297 expected_end: u64,
10298 reason: &'static str,
10299) -> Result<(), FormatError> {
10300 ranges.sort_unstable();
10301 let mut cursor = 0u64;
10302 for (start, end) in ranges.iter().copied() {
10303 if start != cursor || end < start {
10304 return Err(FormatError::InvalidArchive(reason));
10305 }
10306 cursor = end;
10307 }
10308 if cursor != expected_end {
10309 return Err(FormatError::InvalidArchive(reason));
10310 }
10311 Ok(())
10312}
10313
10314fn object_block_range(
10315 first_block_index: u64,
10316 data_block_count: u32,
10317 parity_block_count: u32,
10318 structure: &'static str,
10319) -> Result<(u64, u64), FormatError> {
10320 let total = data_block_count as u64 + parity_block_count as u64;
10321 if total == 0 {
10322 return Err(FormatError::InvalidArchive(structure));
10323 }
10324 let end = checked_u64_add(first_block_index, total, structure)?;
10325 Ok((first_block_index, end))
10326}
10327
10328fn validate_non_overlapping_object_ranges(ranges: &mut [(u64, u64)]) -> Result<(), FormatError> {
10329 ranges.sort_unstable();
10330 for pair in ranges.windows(2) {
10331 if pair[0].1 > pair[1].0 {
10332 return Err(FormatError::InvalidArchive(
10333 "encrypted object block ranges overlap",
10334 ));
10335 }
10336 }
10337 Ok(())
10338}
10339
10340pub(crate) fn observed_archive_size(
10341 sizes: impl IntoIterator<Item = u64>,
10342) -> Result<u64, FormatError> {
10343 sizes.into_iter().try_fold(0u64, |sum, size| {
10344 sum.checked_add(size).ok_or(FormatError::InvalidArchive(
10345 "observed archive size overflow",
10346 ))
10347 })
10348}
10349
10350pub(crate) fn total_extraction_size_cap(
10351 options: ReaderOptions,
10352 observed_archive_bytes: u64,
10353) -> u64 {
10354 options
10355 .max_total_extraction_size
10356 .min(observed_archive_bytes.saturating_mul(10))
10357}
10358
10359fn utf8_path(bytes: &[u8]) -> Result<String, FormatError> {
10360 std::str::from_utf8(bytes)
10361 .map(|path| path.to_owned())
10362 .map_err(|_| FormatError::UnsafeArchivePath)
10363}
10364
10365fn manifest_footer_global_pre_hmac_bytes(manifest_footer: &ManifestFooter) -> [u8; 104] {
10366 let mut bytes = [0u8; 104];
10367 bytes.copy_from_slice(&manifest_footer.to_bytes()[..104]);
10368 bytes[36..40].fill(0);
10369 bytes
10370}
10371
10372fn sha256_bytes(bytes: &[u8]) -> [u8; 32] {
10373 let digest = Sha256::digest(bytes);
10374 let mut out = [0u8; 32];
10375 out.copy_from_slice(&digest);
10376 out
10377}
10378
10379fn slice<'b>(
10380 bytes: &'b [u8],
10381 offset: usize,
10382 len: usize,
10383 structure: &'static str,
10384) -> Result<&'b [u8], FormatError> {
10385 let end = checked_add(offset, len, structure)?;
10386 bytes.get(offset..end).ok_or(FormatError::InvalidLength {
10387 structure,
10388 expected: end,
10389 actual: bytes.len(),
10390 })
10391}
10392
10393fn read_at_vec(
10394 reader: &dyn ArchiveReadAt,
10395 offset: u64,
10396 len: usize,
10397 structure: &'static str,
10398) -> Result<Vec<u8>, FormatError> {
10399 let expected_end = offset
10400 .checked_add(len as u64)
10401 .ok_or(FormatError::InvalidArchive("archive read range overflow"))?;
10402 let observed_len = reader.len()?;
10403 if expected_end > observed_len {
10404 return Err(FormatError::InvalidLength {
10405 structure,
10406 expected: to_usize(expected_end, structure)?,
10407 actual: to_usize(observed_len, structure)?,
10408 });
10409 }
10410 let mut out = vec![0u8; len];
10411 reader.read_exact_at(offset, &mut out)?;
10412 Ok(out)
10413}
10414
10415fn read_at_vec_unchecked(
10416 reader: &dyn ArchiveReadAt,
10417 offset: u64,
10418 len: usize,
10419) -> Result<Vec<u8>, FormatError> {
10420 let mut out = vec![0u8; len];
10421 reader.read_exact_at(offset, &mut out)?;
10422 Ok(out)
10423}
10424
10425fn parallel_map_ref<T, U, F>(items: &[T], jobs: usize, f: F) -> Result<Vec<U>, FormatError>
10426where
10427 T: Sync,
10428 U: Send,
10429 F: Fn(&T) -> Result<U, FormatError> + Sync,
10430{
10431 if jobs <= 1 || items.len() <= 1 {
10432 return items.iter().map(f).collect();
10433 }
10434 let worker_count = jobs.min(items.len());
10435 let chunk_size = items.len().div_ceil(worker_count);
10436 let mut out = Vec::with_capacity(items.len());
10437 thread::scope(|scope| {
10438 let handles = items
10439 .chunks(chunk_size)
10440 .map(|chunk| scope.spawn(|| chunk.iter().map(&f).collect::<Result<Vec<_>, _>>()))
10441 .collect::<Vec<_>>();
10442 for handle in handles {
10443 let mut chunk = handle
10444 .join()
10445 .map_err(|_| FormatError::InvalidArchive("reader worker panicked"))??;
10446 out.append(&mut chunk);
10447 }
10448 Ok(out)
10449 })
10450}
10451
10452fn checked_add(lhs: usize, rhs: usize, structure: &'static str) -> Result<usize, FormatError> {
10453 lhs.checked_add(rhs)
10454 .ok_or(FormatError::InvalidArchive(structure))
10455}
10456
10457fn checked_u64_add(lhs: u64, rhs: u64, structure: &'static str) -> Result<u64, FormatError> {
10458 lhs.checked_add(rhs)
10459 .ok_or(FormatError::InvalidArchive(structure))
10460}
10461
10462fn to_usize(value: u64, structure: &'static str) -> Result<usize, FormatError> {
10463 usize::try_from(value).map_err(|_| FormatError::InvalidArchive(structure))
10464}
10465
10466#[cfg(test)]
10467mod tests {
10468 use std::fs;
10469
10470 use super::*;
10471 use crate::compression::compress_zstd_frame;
10472 use crate::crypto::{compute_hmac, encrypt_padded_aead_object};
10473 use crate::entry_metadata::RestorePolicy;
10474 use crate::fec::encode_parity_gf16;
10475 use crate::format::{
10476 AeadAlgo, CompressionAlgo, FecAlgo, KdfAlgo, CRYPTO_EXTENSION_HEADER_LEN,
10477 CRYPTO_HEADER_FIXED_LEN, FORMAT_VERSION, READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
10478 VOLUME_FORMAT_REV, VOLUME_FORMAT_REV_45,
10479 };
10480 use crate::metadata::{
10481 DirectoryHintEntry, DirectoryHintTableHeader, IndexRootHeader, IndexShardHeader,
10482 ENVELOPE_ENTRY_LEN, FILE_ENTRY_LEN, FRAME_ENTRY_LEN, INDEX_SHARD_HEADER_LEN,
10483 };
10484 use crate::non_seekable_reader::{
10485 extract_non_seekable_stream_to_dir, list_non_seekable_stream, verify_non_seekable_stream,
10486 verify_non_seekable_stream_with_bootstrap_sidecar, verify_non_seekable_stream_with_options,
10487 verify_non_seekable_stream_with_recipient_wrap_resolver_options, NonSeekableReaderOptions,
10488 SequentialRootAuthStatus,
10489 };
10490 use crate::raw_stream_profile::{
10491 serialize_raw_stream_content_model_extension, RAW_STREAM_UNSUPPORTED_MESSAGE,
10492 };
10493 use crate::wire::RecipientRecordV1;
10494 use crate::writer::NativeFileMetadata;
10495 use crate::writer::{
10496 write_archive, write_archive_unencrypted, write_archive_with_dictionary,
10497 write_archive_with_kdf, write_archive_with_recipient_wrap_records,
10498 write_archive_with_root_auth, write_archive_with_root_auth_and_recipient_wrap_records,
10499 PortableFileMetadata, PortableModeOrigin, PortablePosixOwner, RegularFile,
10500 RootAuthSigningRequest, RootAuthWriterConfig, WriterOptions,
10501 };
10502 #[cfg(target_os = "linux")]
10503 use crate::writer::{NativeAuxiliaryMetadata, NativeAuxiliaryNameEncoding};
10504
10505 fn master_key() -> MasterKey {
10506 MasterKey::from_raw_key(&[0x42; 32]).unwrap()
10507 }
10508
10509 fn recipient_wrap_test_record() -> RecipientRecordV1 {
10510 RecipientRecordV1 {
10511 record_length: 0,
10512 profile_id: 1,
10513 recipient_identity_type: 2,
10514 flags: 0,
10515 recipient_identity_length: 0,
10516 profile_payload_length: 0,
10517 recipient_identity_digest: [0u8; 32],
10518 recipient_identity_bytes: b"recipient-a".to_vec(),
10519 profile_payload_bytes: b"profile-payload".to_vec(),
10520 }
10521 }
10522
10523 fn recipient_wrap_layout(volume: &[u8]) -> (usize, usize, usize, usize, u32) {
10524 let header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
10525 let crypto_start = header.crypto_header_offset as usize;
10526 let crypto_len = header.crypto_header_length as usize;
10527 let crypto_end = crypto_start + crypto_len;
10528 let crypto = CryptoHeader::parse(
10529 &volume[crypto_start..crypto_end],
10530 header.crypto_header_length,
10531 )
10532 .unwrap();
10533 let KdfParams::RecipientWrap {
10534 key_wrap_table_length,
10535 ..
10536 } = crypto.kdf_params
10537 else {
10538 panic!("expected RecipientWrap KdfParams");
10539 };
10540 (
10541 crypto_start,
10542 crypto_len,
10543 crypto_end,
10544 key_wrap_table_length as usize,
10545 key_wrap_table_length,
10546 )
10547 }
10548
10549 fn rewrite_recipient_wrap_kdf_digest(crypto_bytes: &mut [u8], digest: [u8; 32]) {
10550 let digest_start = CRYPTO_HEADER_FIXED_LEN + 14;
10551 crypto_bytes[digest_start..digest_start + 32].copy_from_slice(&digest);
10552 }
10553
10554 fn mutate_top_level_recipient_wrap_public_profile(volume: &mut [u8]) {
10555 let (crypto_start, crypto_len, table_start, table_len, table_len_u32) =
10556 recipient_wrap_layout(volume);
10557 let table_end = table_start + table_len;
10558 volume[table_end - 1] ^= 0x5a;
10559 let digest = compute_key_wrap_table_digest(table_len_u32, &volume[table_start..table_end]);
10560 rewrite_recipient_wrap_kdf_digest(
10561 &mut volume[crypto_start..crypto_start + crypto_len],
10562 digest,
10563 );
10564 }
10565
10566 fn mutate_cmra_recipient_wrap_public_profile(volume: &mut [u8]) {
10567 rewrite_public_cmra_image(volume, |image| {
10568 let table_region = image
10569 .regions
10570 .iter_mut()
10571 .find(|region| region.region_type == 6)
10572 .unwrap();
10573 *table_region.bytes.last_mut().unwrap() ^= 0x5a;
10574 let digest =
10575 compute_key_wrap_table_digest(table_region.bytes.len() as u32, &table_region.bytes);
10576 let crypto_region = image
10577 .regions
10578 .iter_mut()
10579 .find(|region| region.region_type == 2)
10580 .unwrap();
10581 rewrite_recipient_wrap_kdf_digest(&mut crypto_region.bytes, digest);
10582 });
10583 }
10584
10585 fn add_raw_stream_profile_to_physical_crypto_header(volume: &mut Vec<u8>) {
10586 let mut header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
10587 let crypto_start = header.crypto_header_offset as usize;
10588 let crypto_len = header.crypto_header_length as usize;
10589 let hmac_start = crypto_start + crypto_len - CRYPTO_HEADER_HMAC_LEN;
10590 let terminator_start = hmac_start - CRYPTO_EXTENSION_HEADER_LEN;
10591 let extension = serialize_raw_stream_content_model_extension();
10592 let new_crypto_len = header.crypto_header_length + extension.len() as u32;
10593
10594 volume.splice(terminator_start..terminator_start, extension);
10595 header.crypto_header_length = new_crypto_len;
10596 volume[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
10597 volume[crypto_start + 4..crypto_start + 8].copy_from_slice(&new_crypto_len.to_le_bytes());
10598 }
10599
10600 fn recompute_physical_crypto_header_hmac(volume: &mut [u8], master_key: &MasterKey) {
10601 let header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
10602 let crypto_start = header.crypto_header_offset as usize;
10603 let crypto_end = crypto_start + header.crypto_header_length as usize;
10604 let hmac_start = crypto_end - CRYPTO_HEADER_HMAC_LEN;
10605 let subkeys =
10606 Subkeys::derive(master_key, &header.archive_uuid, &header.session_id).unwrap();
10607 let hmac = compute_hmac(
10608 HmacDomain::CryptoHeader,
10609 &subkeys.mac_key,
10610 &header.archive_uuid,
10611 &header.session_id,
10612 &volume[crypto_start..hmac_start],
10613 );
10614 volume[hmac_start..crypto_end].copy_from_slice(&hmac);
10615 }
10616
10617 #[test]
10618 fn reader_defaults_use_available_parallelism_jobs() {
10619 let options = ReaderOptions::default();
10620
10621 assert_eq!(options.jobs, default_jobs());
10622 assert!(options.jobs >= 1);
10623 }
10624
10625 #[test]
10626 fn reader_options_reject_zero_jobs() {
10627 let err = OpenedArchive::open_with_options(
10628 &[],
10629 &master_key(),
10630 ReaderOptions {
10631 jobs: 0,
10632 ..ReaderOptions::default()
10633 },
10634 )
10635 .unwrap_err();
10636
10637 assert_eq!(
10638 err,
10639 FormatError::ReaderUnsupported("jobs must be at least 1")
10640 );
10641 }
10642
10643 const TEST_ROOT_AUTH_ID: u16 = 0xe001;
10644 const TEST_ROOT_AUTH_VALUE_LEN: u32 = 32;
10645
10646 fn test_root_auth_config() -> RootAuthWriterConfig<'static> {
10647 RootAuthWriterConfig {
10648 authenticator_id: TEST_ROOT_AUTH_ID,
10649 signer_identity_type: 0,
10650 signer_identity: &[],
10651 authenticator_value_length: TEST_ROOT_AUTH_VALUE_LEN,
10652 }
10653 }
10654
10655 fn test_root_auth_value(request: &RootAuthSigningRequest) -> Vec<u8> {
10656 request.archive_root.to_vec()
10657 }
10658
10659 fn test_root_auth_verifies(footer: &RootAuthFooterV1, archive_root: &[u8; 32]) -> bool {
10660 footer.authenticator_id == TEST_ROOT_AUTH_ID
10661 && footer.signer_identity_type == 0
10662 && footer.signer_identity_bytes.is_empty()
10663 && footer.authenticator_value.as_slice() == archive_root
10664 }
10665
10666 fn dictionary() -> &'static [u8] {
10667 b"dir/dict.txt common words common words common words dictionary payload"
10668 }
10669
10670 #[derive(Clone)]
10671 struct CountingReadAt {
10672 bytes: std::sync::Arc<Vec<u8>>,
10673 reads: std::sync::Arc<std::sync::Mutex<Vec<(u64, u64)>>>,
10674 denied_ranges: std::sync::Arc<Vec<(u64, u64)>>,
10675 }
10676
10677 impl CountingReadAt {
10678 fn new(bytes: Vec<u8>, denied_ranges: Vec<(u64, u64)>) -> Self {
10679 Self {
10680 bytes: std::sync::Arc::new(bytes),
10681 reads: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
10682 denied_ranges: std::sync::Arc::new(denied_ranges),
10683 }
10684 }
10685
10686 fn reads(&self) -> Vec<(u64, u64)> {
10687 self.reads.lock().unwrap().clone()
10688 }
10689 }
10690
10691 impl ArchiveReadAt for CountingReadAt {
10692 fn len(&self) -> Result<u64, FormatError> {
10693 u64::try_from(self.bytes.as_ref().len())
10694 .map_err(|_| FormatError::InvalidArchive("archive length overflow"))
10695 }
10696
10697 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
10698 let end = checked_u64_add(offset, buf.len() as u64, "archive read range overflow")?;
10699 self.reads.lock().unwrap().push((offset, end));
10700 if self
10701 .denied_ranges
10702 .iter()
10703 .any(|(start, limit)| ranges_overlap(offset, end, *start, *limit))
10704 {
10705 return Err(FormatError::InvalidArchive("denied test read"));
10706 }
10707 let start = to_usize(offset, "archive")?;
10708 let end_usize = checked_add(start, buf.len(), "archive")?;
10709 let source = self
10710 .bytes
10711 .get(start..end_usize)
10712 .ok_or(FormatError::InvalidLength {
10713 structure: "archive",
10714 expected: end_usize,
10715 actual: self.bytes.as_ref().len(),
10716 })?;
10717 buf.copy_from_slice(source);
10718 Ok(())
10719 }
10720 }
10721
10722 fn ranges_overlap(left_start: u64, left_end: u64, right_start: u64, right_end: u64) -> bool {
10723 left_start < right_end && right_start < left_end
10724 }
10725
10726 fn single_stream_options() -> WriterOptions {
10727 WriterOptions {
10728 stripe_width: 1,
10729 volume_loss_tolerance: 0,
10730 ..WriterOptions::default()
10731 }
10732 }
10733
10734 struct ChunkedReader {
10735 bytes: Vec<u8>,
10736 cursor: usize,
10737 max_chunk: usize,
10738 }
10739
10740 impl ChunkedReader {
10741 fn new(bytes: Vec<u8>, max_chunk: usize) -> Self {
10742 Self {
10743 bytes,
10744 cursor: 0,
10745 max_chunk,
10746 }
10747 }
10748 }
10749
10750 impl std::io::Read for ChunkedReader {
10751 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
10752 if self.cursor >= self.bytes.len() {
10753 return Ok(0);
10754 }
10755 let available = self.bytes.len() - self.cursor;
10756 let len = available.min(buf.len()).min(self.max_chunk);
10757 buf[..len].copy_from_slice(&self.bytes[self.cursor..self.cursor + len]);
10758 self.cursor += len;
10759 Ok(len)
10760 }
10761 }
10762
10763 #[test]
10764 fn global_file_table_key_step_rejects_distinct_path_regression() {
10765 let previous = ([1u8; 8], b"b.txt".to_vec(), 0);
10766 let current = ([1u8; 8], b"a.txt".to_vec(), 0);
10767
10768 assert_eq!(
10769 validate_global_file_table_key_step(Some(&previous), ¤t).unwrap_err(),
10770 FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
10771 );
10772 }
10773
10774 #[test]
10775 fn global_file_table_key_step_rejects_duplicate_full_key() {
10776 let previous = ([1u8; 8], b"a.txt".to_vec(), 7);
10777 let current = ([1u8; 8], b"a.txt".to_vec(), 7);
10778
10779 assert_eq!(
10780 validate_global_file_table_key_step(Some(&previous), ¤t).unwrap_err(),
10781 FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
10782 );
10783 }
10784
10785 fn small_block_recovery_options() -> WriterOptions {
10786 WriterOptions {
10787 block_size: 4096,
10788 chunk_size: 32 * 1024,
10789 envelope_target_size: 32 * 1024,
10790 stripe_width: 1,
10791 volume_loss_tolerance: 0,
10792 bit_rot_buffer_pct: 1,
10793 fec_data_shards: 16,
10794 fec_parity_shards: 1,
10795 index_fec_data_shards: 4,
10796 index_fec_parity_shards: 1,
10797 index_root_fec_data_shards: 16,
10798 index_root_fec_parity_shards: 1,
10799 ..WriterOptions::default()
10800 }
10801 }
10802
10803 fn parity_rich_recovery_options() -> WriterOptions {
10804 WriterOptions {
10805 block_size: 4096,
10806 chunk_size: 32 * 1024,
10807 envelope_target_size: 32 * 1024,
10808 stripe_width: 1,
10809 volume_loss_tolerance: 0,
10810 bit_rot_buffer_pct: 40,
10811 fec_data_shards: 16,
10812 fec_parity_shards: 16,
10813 index_fec_data_shards: 4,
10814 index_fec_parity_shards: 4,
10815 index_root_fec_data_shards: 16,
10816 index_root_fec_parity_shards: 16,
10817 ..WriterOptions::default()
10818 }
10819 }
10820
10821 fn pseudo_random_bytes(len: usize) -> Vec<u8> {
10822 let mut state = 0x1234_5678u32;
10823 (0..len)
10824 .map(|_| {
10825 state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
10826 (state >> 24) as u8
10827 })
10828 .collect()
10829 }
10830
10831 #[test]
10832 fn opens_lists_verifies_and_extracts_one_file_archive() {
10833 let archive = write_archive(
10834 &[RegularFile::new("dir/hello.txt", b"hello m7")],
10835 &master_key(),
10836 single_stream_options(),
10837 )
10838 .unwrap();
10839 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10840
10841 assert_eq!(
10842 opened.list_files().unwrap(),
10843 vec![ArchiveEntry {
10844 path: "dir/hello.txt".to_string(),
10845 file_data_size: 8,
10846 kind: TarEntryKind::Regular,
10847 mode: 0o644,
10848 mtime: ArchiveTimestamp::UNIX_EPOCH,
10849 diagnostics: Vec::new(),
10850 }]
10851 );
10852 opened.verify().unwrap();
10853 assert_eq!(
10854 opened.extract_file("dir/hello.txt").unwrap(),
10855 Some(b"hello m7".to_vec())
10856 );
10857 assert_eq!(opened.extract_file("missing.txt").unwrap(), None);
10858 }
10859
10860 #[test]
10861 fn root_auth_archive_round_trips_and_verifies_with_callback() {
10862 let archive = write_archive_with_root_auth(
10863 &[RegularFile::new("signed.txt", b"root-auth payload")],
10864 &master_key(),
10865 single_stream_options(),
10866 RootAuthWriterConfig {
10867 authenticator_id: 0x7777,
10868 signer_identity_type: 1,
10869 signer_identity: b"test signer",
10870 authenticator_value_length: 32,
10871 },
10872 |request| Ok(request.archive_root.to_vec()),
10873 )
10874 .unwrap();
10875
10876 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10877 opened.verify().unwrap();
10878 let verified = opened
10879 .verify_root_auth_with(|footer, archive_root| {
10880 Ok(footer.authenticator_value == archive_root.as_slice())
10881 })
10882 .unwrap();
10883
10884 assert_eq!(verified.authenticator_id, 0x7777);
10885 assert_eq!(verified.signer_identity_type, 1);
10886 assert_eq!(verified.signer_identity_bytes, b"test signer");
10887 assert_eq!(
10888 verified.archive_root,
10889 opened.root_auth_footer.as_ref().unwrap().archive_root
10890 );
10891 assert_eq!(
10892 verified.diagnostics,
10893 vec![
10894 RootAuthDiagnostic::RootAuthContentVerified,
10895 RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
10896 RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
10897 RootAuthDiagnostic::RecoveryMarginUnchecked,
10898 ]
10899 );
10900 }
10901
10902 #[test]
10903 fn root_auth_rejects_fast_content_verification_token() {
10904 let archive = write_archive_with_root_auth(
10905 &[RegularFile::new("signed.txt", b"root-auth payload")],
10906 &master_key(),
10907 single_stream_options(),
10908 RootAuthWriterConfig {
10909 authenticator_id: 0x7777,
10910 signer_identity_type: 1,
10911 signer_identity: b"test signer",
10912 authenticator_value_length: 32,
10913 },
10914 |request| Ok(request.archive_root.to_vec()),
10915 )
10916 .unwrap();
10917
10918 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10919 let content_verification = opened.verify_content_fast().unwrap();
10920 assert_eq!(
10921 opened
10922 .verify_root_auth_with_verified_content(&content_verification, |_, _| Ok(true))
10923 .unwrap_err(),
10924 FormatError::ReaderUnsupported(
10925 "RootAuth verification requires full archive content verification"
10926 )
10927 );
10928 }
10929
10930 #[test]
10931 fn root_auth_verification_requires_authenticator_success() {
10932 let archive = write_archive_with_root_auth(
10933 &[RegularFile::new("signed.txt", b"root-auth payload")],
10934 &master_key(),
10935 single_stream_options(),
10936 RootAuthWriterConfig {
10937 authenticator_id: 9,
10938 signer_identity_type: 1,
10939 signer_identity: b"test signer",
10940 authenticator_value_length: 32,
10941 },
10942 |request| Ok(request.archive_root.to_vec()),
10943 )
10944 .unwrap();
10945 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10946
10947 assert_eq!(
10948 opened.verify_root_auth_with(|_, _| Ok(false)).unwrap_err(),
10949 FormatError::InvalidArchive("root-auth authenticator verification failed")
10950 );
10951 }
10952
10953 #[test]
10954 fn public_no_key_verifies_encrypted_data_block_commitment_with_callback() {
10955 let archive = write_archive_with_root_auth(
10956 &[RegularFile::new("public.txt", b"public commitment")],
10957 &master_key(),
10958 single_stream_options(),
10959 RootAuthWriterConfig {
10960 authenticator_id: 0x2222,
10961 signer_identity_type: 1,
10962 signer_identity: b"public verifier",
10963 authenticator_value_length: 32,
10964 },
10965 |request| Ok(request.archive_root.to_vec()),
10966 )
10967 .unwrap();
10968
10969 let verified = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
10970 Ok(footer.authenticator_value == archive_root.as_slice())
10971 })
10972 .unwrap();
10973
10974 assert_eq!(verified.authenticator_id, 0x2222);
10975 assert_eq!(verified.signer_identity_bytes, b"public verifier");
10976 assert!(verified.total_data_block_count > 0);
10977 }
10978
10979 #[test]
10980 fn public_no_key_verifier_not_invoked_for_future_revision() {
10981 let archive = write_archive_with_root_auth(
10982 &[RegularFile::new("public.txt", b"public callback")],
10983 &master_key(),
10984 single_stream_options(),
10985 RootAuthWriterConfig {
10986 authenticator_id: 0x2222,
10987 signer_identity_type: 1,
10988 signer_identity: b"public verifier",
10989 authenticator_value_length: 32,
10990 },
10991 |request| Ok(request.archive_root.to_vec()),
10992 )
10993 .unwrap();
10994 let mut bytes = archive.bytes;
10995 let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
10996 header.volume_format_rev = VOLUME_FORMAT_REV_45 + 1;
10997 bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
10998
10999 let mut called = false;
11000 let err = public_no_key_verify_archive_with(&bytes, |_, _| {
11001 called = true;
11002 Ok(true)
11003 })
11004 .unwrap_err();
11005
11006 assert!(!called);
11007 assert_eq!(
11008 err,
11009 FormatError::UnsupportedVolumeFormatRevision {
11010 format_version: FORMAT_VERSION,
11011 volume_format_rev: VOLUME_FORMAT_REV_45 + 1,
11012 reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
11013 }
11014 );
11015 }
11016
11017 #[test]
11018 fn public_no_key_rejects_public_header_revision_mismatch() {
11019 let archive = write_archive_with_root_auth(
11020 &[RegularFile::new("public.txt", b"public v45 only")],
11021 &master_key(),
11022 single_stream_options(),
11023 RootAuthWriterConfig {
11024 authenticator_id: 0x2222,
11025 signer_identity_type: 1,
11026 signer_identity: b"public verifier",
11027 authenticator_value_length: 32,
11028 },
11029 |request| Ok(request.archive_root.to_vec()),
11030 )
11031 .unwrap();
11032 let mut bytes = archive.bytes;
11033 let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
11034 header.volume_format_rev = 43;
11035 bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
11036
11037 let mut called = false;
11038 let err = public_no_key_verify_archive_with(&bytes, |_, _| {
11039 called = true;
11040 Ok(true)
11041 })
11042 .unwrap_err();
11043
11044 assert!(!called);
11045 assert_eq!(
11046 err,
11047 FormatError::UnsupportedVolumeFormatRevision {
11048 format_version: FORMAT_VERSION,
11049 volume_format_rev: 43,
11050 reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
11051 }
11052 );
11053 }
11054
11055 #[test]
11056 fn public_no_key_rejects_recovered_footer_revision_mismatch() {
11057 let archive = write_archive_with_root_auth(
11058 &[RegularFile::new("public.txt", b"public footer mismatch")],
11059 &master_key(),
11060 single_stream_options(),
11061 RootAuthWriterConfig {
11062 authenticator_id: 0x2222,
11063 signer_identity_type: 1,
11064 signer_identity: b"public verifier",
11065 authenticator_value_length: 32,
11066 },
11067 |request| Ok(request.archive_root.to_vec()),
11068 )
11069 .unwrap();
11070 let mut bytes = archive.bytes;
11071 rewrite_public_cmra_image(&mut bytes, |image| {
11072 let root_auth_region = image
11073 .regions
11074 .iter_mut()
11075 .find(|region| region.region_type == 4)
11076 .unwrap();
11077 rewrite_root_auth_footer_revision_bytes(&mut root_auth_region.bytes, 43);
11078 });
11079
11080 let mut called = false;
11081 let err = public_no_key_verify_archive_with(&bytes, |_, _| {
11082 called = true;
11083 Ok(true)
11084 })
11085 .unwrap_err();
11086
11087 assert!(!called);
11088 assert_eq!(
11089 err,
11090 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
11091 );
11092 }
11093
11094 #[test]
11095 fn public_no_key_rejects_recovered_image_with_unknown_layout_flags() {
11096 let archive = write_archive_with_root_auth(
11097 &[RegularFile::new("public.txt", b"public image mismatch")],
11098 &master_key(),
11099 single_stream_options(),
11100 RootAuthWriterConfig {
11101 authenticator_id: 0x2222,
11102 signer_identity_type: 1,
11103 signer_identity: b"public verifier",
11104 authenticator_value_length: 32,
11105 },
11106 |request| Ok(request.archive_root.to_vec()),
11107 )
11108 .unwrap();
11109 let bytes = rewrite_cmra_image_variable_len(
11110 &archive.bytes,
11111 CmraRecoveryMode::PublicNoKey,
11112 |image| {
11113 image.layout_flags |= 0x8000_0000;
11114 },
11115 );
11116
11117 let mut called = false;
11118 let err = public_no_key_verify_archive_with(&bytes, |_, _| {
11119 called = true;
11120 Ok(true)
11121 })
11122 .unwrap_err();
11123
11124 assert!(!called);
11125 assert_eq!(
11126 err,
11127 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
11128 );
11129 }
11130
11131 #[test]
11132 fn public_no_key_ignores_untrusted_manifest_and_trailer_block_count_fields() {
11133 let archive = write_archive_with_root_auth(
11134 &[RegularFile::new(
11135 "public-fields.txt",
11136 b"public source authority",
11137 )],
11138 &master_key(),
11139 single_stream_options(),
11140 RootAuthWriterConfig {
11141 authenticator_id: 0x2222,
11142 signer_identity_type: 1,
11143 signer_identity: b"public verifier",
11144 authenticator_value_length: 32,
11145 },
11146 |request| Ok(request.archive_root.to_vec()),
11147 )
11148 .unwrap();
11149 let mut bytes = archive.bytes.clone();
11150
11151 rewrite_public_cmra_image(&mut bytes, |image| {
11152 let manifest_region = image
11153 .regions
11154 .iter_mut()
11155 .find(|region| region.region_type == 3)
11156 .unwrap();
11157 manifest_region.bytes[44..48].copy_from_slice(&99u32.to_le_bytes());
11158
11159 let trailer_region = image
11160 .regions
11161 .iter_mut()
11162 .find(|region| region.region_type == 5)
11163 .unwrap();
11164 let mut trailer = VolumeTrailer::parse(&trailer_region.bytes).unwrap();
11165 trailer.block_count += 7;
11166 trailer_region.bytes = trailer.to_bytes().to_vec();
11167 });
11168
11169 public_no_key_verify_archive_with(&bytes, |footer, archive_root| {
11170 Ok(footer.authenticator_value == archive_root.as_slice())
11171 })
11172 .unwrap();
11173 }
11174
11175 #[test]
11176 fn public_no_key_compares_only_public_crypto_profile_across_volumes() {
11177 let archive = write_archive_with_root_auth(
11178 &[RegularFile::new(
11179 "public-crypto.txt",
11180 b"cross-volume public profile",
11181 )],
11182 &master_key(),
11183 WriterOptions {
11184 stripe_width: 2,
11185 volume_loss_tolerance: 0,
11186 ..WriterOptions::default()
11187 },
11188 RootAuthWriterConfig {
11189 authenticator_id: 0x3333,
11190 signer_identity_type: 1,
11191 signer_identity: b"public verifier",
11192 authenticator_value_length: 32,
11193 },
11194 |request| Ok(request.archive_root.to_vec()),
11195 )
11196 .unwrap();
11197 let mut volumes = archive.volumes.clone();
11198 let volume_header = VolumeHeader::parse(&volumes[1][..VOLUME_HEADER_LEN]).unwrap();
11199 let crypto_offset = volume_header.crypto_header_offset as usize;
11200 let expected_volume_size = 123_456_789u64;
11201 volumes[1][crypto_offset + 52..crypto_offset + 60]
11202 .copy_from_slice(&expected_volume_size.to_le_bytes());
11203 rewrite_public_cmra_image(&mut volumes[1], |image| {
11204 let crypto_region = image
11205 .regions
11206 .iter_mut()
11207 .find(|region| region.region_type == 2)
11208 .unwrap();
11209 crypto_region.bytes[52..60].copy_from_slice(&expected_volume_size.to_le_bytes());
11210 });
11211
11212 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
11213 public_no_key_verify_volumes_with(&volume_refs, |footer, archive_root| {
11214 Ok(footer.authenticator_value == archive_root.as_slice())
11215 })
11216 .unwrap();
11217 }
11218
11219 #[test]
11220 fn locator_based_cmra_recovery_treats_header_damage_as_recoverable() {
11221 let archive = write_archive_with_root_auth(
11222 &[RegularFile::new("cmra-header.txt", b"header fallback")],
11223 &master_key(),
11224 single_stream_options(),
11225 RootAuthWriterConfig {
11226 authenticator_id: 0x4444,
11227 signer_identity_type: 1,
11228 signer_identity: b"public verifier",
11229 authenticator_value_length: 32,
11230 },
11231 |request| Ok(request.archive_root.to_vec()),
11232 )
11233 .unwrap();
11234 let final_locator = final_recovery_locator(&archive.bytes);
11235
11236 let mut bad_crc = archive.bytes.clone();
11237 let crc_offset =
11238 final_locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN - 1;
11239 bad_crc[crc_offset] ^= 0x55;
11240 public_no_key_verify_archive_with(&bad_crc, |footer, archive_root| {
11241 Ok(footer.authenticator_value == archive_root.as_slice())
11242 })
11243 .unwrap();
11244
11245 let mut bad_magic = archive.bytes.clone();
11246 bad_magic[final_locator.cmra_offset as usize] ^= 0x55;
11247 public_no_key_verify_archive_with(&bad_magic, |footer, archive_root| {
11248 Ok(footer.authenticator_value == archive_root.as_slice())
11249 })
11250 .unwrap();
11251
11252 let mut bad_hint = archive.bytes.clone();
11253 bad_hint[crc_offset] ^= 0xAA;
11254 for offset in [
11255 bad_hint.len() - LOCATOR_PAIR_LEN,
11256 bad_hint.len() - CRITICAL_RECOVERY_LOCATOR_LEN,
11257 ] {
11258 let mut locator = CriticalRecoveryLocator::parse(
11259 &bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN],
11260 )
11261 .unwrap();
11262 locator.volume_index_hint += 1;
11263 bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN]
11264 .copy_from_slice(&locator.to_bytes());
11265 }
11266 assert_eq!(
11267 public_no_key_verify_archive_with(&bad_hint, |_, _| Ok(true)).unwrap_err(),
11268 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
11269 );
11270 }
11271
11272 #[test]
11273 fn recovers_physical_volume_header_magic_from_cmra_authority() {
11274 let payload = b"front header authority".to_vec();
11275 let archive = write_archive(
11276 &[RegularFile::new("volume-header.txt", &payload)],
11277 &master_key(),
11278 small_block_recovery_options(),
11279 )
11280 .unwrap();
11281
11282 let mut corrupted = archive.bytes;
11283 corrupted[0] ^= 0x55;
11284
11285 let opened = open_archive(&corrupted, &master_key()).unwrap();
11286 assert_eq!(
11287 opened.extract_file("volume-header.txt").unwrap(),
11288 Some(payload)
11289 );
11290 opened.verify().unwrap();
11291 }
11292
11293 #[test]
11294 fn recovers_crc_valid_physical_volume_index_from_cmra_authority() {
11295 let payload = b"crc-valid wrong volume index".to_vec();
11296 let mut options = small_block_recovery_options();
11297 options.stripe_width = 2;
11298 options.volume_loss_tolerance = 0;
11299 let archive = write_archive(
11300 &[RegularFile::new("volume-index.txt", &payload)],
11301 &master_key(),
11302 options,
11303 )
11304 .unwrap();
11305
11306 let mut corrupted = archive.volumes[0].clone();
11307 let mut header = VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN]).unwrap();
11308 assert_eq!(header.volume_index, 0);
11309 header.volume_index = 1;
11310 corrupted[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
11311 assert_eq!(
11312 VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN])
11313 .unwrap()
11314 .volume_index,
11315 1
11316 );
11317
11318 let opened = open_archive_volumes(
11319 &[corrupted.as_slice(), archive.volumes[1].as_slice()],
11320 &master_key(),
11321 )
11322 .unwrap();
11323 assert_eq!(opened.volume_header.volume_index, 0);
11324 assert_eq!(
11325 opened.extract_file("volume-index.txt").unwrap(),
11326 Some(payload)
11327 );
11328 opened.verify().unwrap();
11329 }
11330
11331 #[test]
11332 fn recovers_physical_crypto_header_magic_from_cmra_authority() {
11333 let payload = b"crypto header authority".to_vec();
11334 let archive = write_archive(
11335 &[RegularFile::new("crypto-header.txt", &payload)],
11336 &master_key(),
11337 small_block_recovery_options(),
11338 )
11339 .unwrap();
11340 let crypto_offset = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN])
11341 .unwrap()
11342 .crypto_header_offset;
11343
11344 let mut corrupted = archive.bytes;
11345 corrupted[crypto_offset as usize] ^= 0x55;
11346
11347 let opened = open_archive(&corrupted, &master_key()).unwrap();
11348 assert_eq!(
11349 opened.extract_file("crypto-header.txt").unwrap(),
11350 Some(payload)
11351 );
11352 opened.verify().unwrap();
11353 }
11354
11355 #[test]
11356 fn read_at_api_recovers_physical_header_magic_from_cmra_authority() {
11357 let payload = b"read-at header authority".to_vec();
11358 let archive = write_archive(
11359 &[RegularFile::new("read-at-header.txt", &payload)],
11360 &master_key(),
11361 small_block_recovery_options(),
11362 )
11363 .unwrap();
11364
11365 let mut corrupted = archive.bytes;
11366 corrupted[0] ^= 0x55;
11367
11368 let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
11369 assert_eq!(
11370 opened.extract_file("read-at-header.txt").unwrap(),
11371 Some(payload)
11372 );
11373 opened.verify().unwrap();
11374 }
11375
11376 #[test]
11377 fn read_at_api_recovers_crc_valid_physical_volume_index_from_cmra_authority() {
11378 let payload = b"read-at crc-valid wrong volume index".to_vec();
11379 let mut options = small_block_recovery_options();
11380 options.stripe_width = 2;
11381 options.volume_loss_tolerance = 0;
11382 let archive = write_archive(
11383 &[RegularFile::new("read-at-volume-index.txt", &payload)],
11384 &master_key(),
11385 options,
11386 )
11387 .unwrap();
11388
11389 let mut corrupted = archive.volumes[0].clone();
11390 let mut header = VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN]).unwrap();
11391 assert_eq!(header.volume_index, 0);
11392 header.volume_index = 1;
11393 corrupted[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
11394
11395 let opened = open_seekable_archive_volumes(
11396 vec![corrupted, archive.volumes[1].clone()],
11397 &master_key(),
11398 )
11399 .unwrap();
11400 assert_eq!(opened.volume_header.volume_index, 0);
11401 assert_eq!(
11402 opened.extract_file("read-at-volume-index.txt").unwrap(),
11403 Some(payload)
11404 );
11405 opened.verify().unwrap();
11406 }
11407
11408 #[test]
11409 fn recovers_cmra_header_magic_from_locator_tuple() {
11410 let payload = b"cmra header authority".to_vec();
11411 let archive = write_archive(
11412 &[RegularFile::new("cmra-header-magic.txt", &payload)],
11413 &master_key(),
11414 small_block_recovery_options(),
11415 )
11416 .unwrap();
11417 let locator = final_recovery_locator(&archive.bytes);
11418
11419 let mut corrupted = archive.bytes;
11420 corrupted[locator.cmra_offset as usize] ^= 0x55;
11421
11422 let opened = open_archive(&corrupted, &master_key()).unwrap();
11423 assert_eq!(
11424 opened.extract_file("cmra-header-magic.txt").unwrap(),
11425 Some(payload)
11426 );
11427 opened.verify().unwrap();
11428 }
11429
11430 #[test]
11431 fn recovers_cmra_shard_magic_as_erasure() {
11432 let payload = b"cmra shard authority".to_vec();
11433 let archive = write_archive(
11434 &[RegularFile::new("cmra-shard-magic.txt", &payload)],
11435 &master_key(),
11436 small_block_recovery_options(),
11437 )
11438 .unwrap();
11439 let locator = final_recovery_locator(&archive.bytes);
11440 let first_shard_offset =
11441 locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
11442
11443 let mut corrupted = archive.bytes;
11444 corrupted[first_shard_offset] ^= 0x55;
11445
11446 let opened = open_archive(&corrupted, &master_key()).unwrap();
11447 assert_eq!(
11448 opened.extract_file("cmra-shard-magic.txt").unwrap(),
11449 Some(payload)
11450 );
11451 opened.verify().unwrap();
11452 }
11453
11454 #[test]
11455 fn key_holding_rejects_recovered_image_with_unknown_layout_flags() {
11456 let archive = write_archive(
11457 &[RegularFile::new("cmra-image-revision.txt", b"payload")],
11458 &master_key(),
11459 small_block_recovery_options(),
11460 )
11461 .unwrap();
11462 let mut mutated = rewrite_cmra_image_variable_len(
11463 &archive.bytes,
11464 CmraRecoveryMode::KeyHolding,
11465 |image| {
11466 image.layout_flags |= 0x8000_0000;
11467 },
11468 );
11469 mutated[0] ^= 0x55;
11470
11471 assert!(open_archive(&mutated, &master_key()).is_err());
11472 }
11473
11474 #[test]
11475 fn key_holding_rejects_recovered_footer_revision_mismatch() {
11476 let archive = write_archive_with_root_auth(
11477 &[RegularFile::new("cmra-footer-revision.txt", b"payload")],
11478 &master_key(),
11479 single_stream_options(),
11480 test_root_auth_config(),
11481 |request| Ok(test_root_auth_value(request)),
11482 )
11483 .unwrap();
11484 let mut mutated = archive.bytes;
11485 rewrite_cmra_image(&mut mutated, CmraRecoveryMode::KeyHolding, |image| {
11486 let root_auth_region = image
11487 .regions
11488 .iter_mut()
11489 .find(|region| region.region_type == 4)
11490 .unwrap();
11491 rewrite_root_auth_footer_revision_bytes(&mut root_auth_region.bytes, 43);
11492 });
11493 mutated[0] ^= 0x55;
11494
11495 assert!(open_archive(&mutated, &master_key()).is_err());
11496 }
11497
11498 #[test]
11499 fn key_holding_rejects_locator_image_revision_mismatch() {
11500 let archive = write_archive(
11501 &[RegularFile::new("cmra-locator-revision.txt", b"payload")],
11502 &master_key(),
11503 small_block_recovery_options(),
11504 )
11505 .unwrap();
11506 let mut mutated = archive.bytes;
11507 let locator = final_recovery_locator(&mutated);
11508 let mirror_offset = mutated.len() - LOCATOR_PAIR_LEN;
11509 let final_offset = mutated.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11510 for offset in [mirror_offset, final_offset] {
11511 rewrite_recovery_locator(&mut mutated, offset, |locator| {
11512 locator.volume_format_rev = 43;
11513 });
11514 }
11515 mutated[0] ^= 0x55;
11516 mutated[locator.cmra_offset as usize] ^= 0x55;
11517
11518 assert!(open_archive(&mutated, &master_key()).is_err());
11519 }
11520
11521 #[test]
11522 fn image_identity_allows_matching_current_revision() {
11523 let archive = write_archive(
11524 &[RegularFile::new("matching-v45.txt", b"payload")],
11525 &master_key(),
11526 single_stream_options(),
11527 )
11528 .unwrap();
11529 let locator = final_recovery_locator(&archive.bytes);
11530 let recovered = recover_cmra(
11531 &archive.bytes,
11532 locator.cmra_offset,
11533 Some(CmraDecoderTuple::from(locator)),
11534 CmraRecoveryMode::KeyHolding,
11535 )
11536 .unwrap();
11537 let image = recovered.image;
11538 let header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11539 let crypto_start = header.crypto_header_offset as usize;
11540 let crypto_end = crypto_start + header.crypto_header_length as usize;
11541 let crypto = CryptoHeader::parse(
11542 &archive.bytes[crypto_start..crypto_end],
11543 header.crypto_header_length,
11544 )
11545 .unwrap();
11546
11547 validate_image_identity(&image, &header, &crypto.fixed).unwrap();
11548 }
11549
11550 #[test]
11551 fn key_holding_rejects_cmra_below_authenticated_parity_floor() {
11552 let archive = write_archive(
11553 &[RegularFile::new(
11554 "cmra-floor.txt",
11555 b"authenticated CMRA floor",
11556 )],
11557 &master_key(),
11558 single_stream_options(),
11559 )
11560 .unwrap();
11561 let malformed = rewrite_cmra_parity_count(&archive.bytes, 1);
11562 let final_offset = malformed.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11563 let locator = CriticalRecoveryLocator::parse(
11564 &malformed[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
11565 )
11566 .unwrap();
11567 let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
11568 let crypto_start = volume_header.crypto_header_offset as usize;
11569 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
11570 let crypto_header = CryptoHeader::parse(
11571 &malformed[crypto_start..crypto_end],
11572 volume_header.crypto_header_length,
11573 )
11574 .unwrap();
11575 let subkeys = Subkeys::derive(
11576 &master_key(),
11577 &volume_header.archive_uuid,
11578 &volume_header.session_id,
11579 )
11580 .unwrap();
11581
11582 assert_eq!(
11583 parse_locator_cmra_candidate(
11584 &malformed,
11585 final_offset,
11586 locator,
11587 KeyHoldingTerminalContext {
11588 subkeys: &subkeys,
11589 volume_header: &volume_header,
11590 crypto_header: &crypto_header.fixed,
11591 crypto_header_bytes: &malformed[crypto_start..crypto_end],
11592 },
11593 )
11594 .unwrap_err(),
11595 FormatError::InvalidArchive(
11596 "CMRA parity shard count is below authenticated bit-rot lower bound"
11597 )
11598 );
11599 assert!(open_archive(&malformed, &master_key()).is_err());
11600 }
11601
11602 #[test]
11603 fn locator_tuple_bounds_are_checked_before_locator_position_fields() {
11604 let archive = write_archive(
11605 &[RegularFile::new(
11606 "locator-order.txt",
11607 b"locator tuple first",
11608 )],
11609 &master_key(),
11610 single_stream_options(),
11611 )
11612 .unwrap();
11613 let final_offset = archive.bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11614 let mut locator = final_recovery_locator(&archive.bytes);
11615 locator.cmra_shard_size = 513;
11616 locator.body_bytes_before_cmra = locator.cmra_offset + 1;
11617 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11618 let crypto_start = volume_header.crypto_header_offset as usize;
11619 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
11620 let crypto_header = CryptoHeader::parse(
11621 &archive.bytes[crypto_start..crypto_end],
11622 volume_header.crypto_header_length,
11623 )
11624 .unwrap();
11625 let subkeys = Subkeys::derive(
11626 &master_key(),
11627 &volume_header.archive_uuid,
11628 &volume_header.session_id,
11629 )
11630 .unwrap();
11631
11632 assert_eq!(
11633 parse_locator_cmra_candidate(
11634 &archive.bytes,
11635 final_offset,
11636 locator,
11637 KeyHoldingTerminalContext {
11638 subkeys: &subkeys,
11639 volume_header: &volume_header,
11640 crypto_header: &crypto_header.fixed,
11641 crypto_header_bytes: &archive.bytes[crypto_start..crypto_end],
11642 },
11643 )
11644 .unwrap_err(),
11645 FormatError::InvalidArchive("CMRA shard_size is invalid")
11646 );
11647 }
11648
11649 #[test]
11650 fn sequential_extract_rejects_bytes_after_terminal_locator() {
11651 let archive = write_archive(
11652 &[RegularFile::new("seq.txt", b"sequential EOF")],
11653 &master_key(),
11654 single_stream_options(),
11655 )
11656 .unwrap();
11657 let mut appended = archive.bytes.clone();
11658 appended.extend_from_slice(&[0xAA; 32]);
11659
11660 assert_eq!(
11661 sequential_extract_tar_stream(&appended, &master_key()).unwrap_err(),
11662 FormatError::InvalidArchive("sequential terminal does not end at EOF")
11663 );
11664 }
11665
11666 #[test]
11667 fn global_file_table_order_rejects_cross_shard_duplicate_reversal() {
11668 let first = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 2048);
11669 let second = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 1024);
11670
11671 assert_eq!(
11672 validate_global_file_table_key_step(Some(&first), &second).unwrap_err(),
11673 FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
11674 );
11675 }
11676
11677 #[test]
11678 fn root_auth_verifies_key_holding_and_public_no_key_modes() {
11679 let archive = write_archive_with_root_auth(
11680 &[RegularFile::new("signed.txt", b"ed25519 payload")],
11681 &master_key(),
11682 single_stream_options(),
11683 test_root_auth_config(),
11684 |request| Ok(test_root_auth_value(request)),
11685 )
11686 .unwrap();
11687
11688 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11689 let root_auth = opened
11690 .verify_root_auth_with(|footer, archive_root| {
11691 Ok(test_root_auth_verifies(footer, archive_root))
11692 })
11693 .unwrap();
11694 assert_eq!(
11695 root_auth.archive_root,
11696 opened.root_auth_footer.as_ref().unwrap().archive_root
11697 );
11698
11699 let public = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
11700 Ok(test_root_auth_verifies(footer, archive_root))
11701 })
11702 .unwrap();
11703 assert_eq!(public.archive_root, root_auth.archive_root);
11704 assert_eq!(
11705 public.diagnostics,
11706 vec![
11707 PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
11708 PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
11709 PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
11710 ]
11711 );
11712 }
11713
11714 #[test]
11715 fn root_auth_verifies_with_tolerated_missing_volume_after_fec_repair() {
11716 let options = WriterOptions {
11717 block_size: 4096,
11718 chunk_size: 16 * 1024,
11719 envelope_target_size: 16 * 1024,
11720 stripe_width: 2,
11721 volume_loss_tolerance: 1,
11722 bit_rot_buffer_pct: 0,
11723 fec_data_shards: 16,
11724 fec_parity_shards: 1,
11725 index_fec_data_shards: 4,
11726 index_fec_parity_shards: 1,
11727 index_root_fec_data_shards: 16,
11728 index_root_fec_parity_shards: 1,
11729 ..WriterOptions::default()
11730 };
11731 let archive = write_archive_with_root_auth(
11732 &[RegularFile::new("missing-volume.txt", b"recover me")],
11733 &master_key(),
11734 options,
11735 test_root_auth_config(),
11736 |request| Ok(test_root_auth_value(request)),
11737 )
11738 .unwrap();
11739
11740 let opened = open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap();
11741 let root_auth = opened
11742 .verify_root_auth_with(|footer, archive_root| {
11743 Ok(test_root_auth_verifies(footer, archive_root))
11744 })
11745 .unwrap();
11746 assert!(root_auth
11747 .diagnostics
11748 .contains(&RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss));
11749 }
11750
11751 #[test]
11752 fn public_no_key_rejects_unsigned_archives() {
11753 let archive = write_archive(
11754 &[RegularFile::new("plain.txt", b"unsigned")],
11755 &master_key(),
11756 single_stream_options(),
11757 )
11758 .unwrap();
11759
11760 assert_eq!(
11761 public_no_key_verify_archive_with(&archive.bytes, |_, _| Ok(true)).unwrap_err(),
11762 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
11763 );
11764 }
11765
11766 #[test]
11767 fn unsigned_archive_reports_root_auth_absent() {
11768 let archive = write_archive(
11769 &[RegularFile::new("plain.txt", b"unsigned")],
11770 &master_key(),
11771 single_stream_options(),
11772 )
11773 .unwrap();
11774 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11775
11776 assert_eq!(
11777 opened.verify_root_auth_with(|_, _| Ok(true)).unwrap_err(),
11778 FormatError::ReaderUnsupported("root-auth footer is absent")
11779 );
11780 }
11781
11782 #[test]
11783 fn safe_extract_writes_regular_file_under_root() {
11784 let archive = write_archive(
11785 &[RegularFile::new("dir/hello.txt", b"safe m8")],
11786 &master_key(),
11787 single_stream_options(),
11788 )
11789 .unwrap();
11790 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11791 let tmp = tempfile::tempdir().unwrap();
11792
11793 opened
11794 .extract_file_to(
11795 "dir/hello.txt",
11796 tmp.path(),
11797 SafeExtractionOptions::default(),
11798 )
11799 .unwrap()
11800 .unwrap();
11801
11802 assert_eq!(
11803 std::fs::read(tmp.path().join("dir").join("hello.txt")).unwrap(),
11804 b"safe m8"
11805 );
11806 }
11807
11808 fn decoded_primary_metadata_storage(
11809 opened: &OpenedArchive,
11810 path: &str,
11811 ) -> (crate::entry_metadata::PaxRecords, [u8; 512]) {
11812 let located = opened.locate_index_file(path.as_bytes()).unwrap().unwrap();
11813 let file = &located.shard.files[located.file_index];
11814 let mut reader = DecodedTarMemberGroupReader::new(opened, &located.shard, file).unwrap();
11815 let mut group = vec![0u8; usize::try_from(file.tar_member_group_size).unwrap()];
11816 let mut offset = 0usize;
11817 while offset < group.len() {
11818 let read = reader.read_some_member_bytes(&mut group[offset..]).unwrap();
11819 assert_ne!(read, 0, "decoded member group ended early");
11820 offset += read;
11821 }
11822
11823 assert_eq!(group[156], b'x');
11824 let pax_size = usize::try_from(parse_test_tar_octal(&group[124..136])).unwrap();
11825 let pax_end = 512 + pax_size;
11826 let records = crate::entry_metadata::parse_canonical_pax(&group[512..pax_end]).unwrap();
11827 let primary_offset = pax_end + padding_to_512(pax_size);
11828 let primary = group[primary_offset..primary_offset + 512]
11829 .try_into()
11830 .unwrap();
11831 (records, primary)
11832 }
11833
11834 fn parse_test_tar_octal(field: &[u8]) -> u64 {
11835 let field = nul_trimmed_test_field(field);
11836 let text = std::str::from_utf8(field).unwrap().trim();
11837 u64::from_str_radix(text, 8).unwrap()
11838 }
11839
11840 fn nul_trimmed_test_field(field: &[u8]) -> &[u8] {
11841 let end = field
11842 .iter()
11843 .position(|byte| *byte == 0)
11844 .unwrap_or(field.len());
11845 &field[..end]
11846 }
11847
11848 #[test]
11849 fn compressed_archive_round_trips_header_and_pax_portable_metadata_stores() {
11850 let files = [
11851 RegularFile {
11852 mode: 0o640,
11853 mtime: ArchiveTimestamp::from_seconds(1_700_000_000),
11854 portable_metadata: PortableFileMetadata {
11855 source_os: "other-unix".into(),
11856 source_filesystem: "ext4".into(),
11857 mode_origin: PortableModeOrigin::Native,
11858 posix_owner: Some(PortablePosixOwner {
11859 uid: 42,
11860 gid: 84,
11861 uname: Some("alice".into()),
11862 gname: Some("staff".into()),
11863 }),
11864 attributes: Some(1),
11865 native: Default::default(),
11866 },
11867 ..RegularFile::new("header-fields.txt", b"header metadata")
11868 },
11869 RegularFile {
11870 mode: 0o604,
11871 mtime: ArchiveTimestamp::new(-1, 500_000_000),
11872 portable_metadata: PortableFileMetadata {
11873 source_os: "other-unix".into(),
11874 source_filesystem: "zfs".into(),
11875 mode_origin: PortableModeOrigin::Native,
11876 posix_owner: Some(PortablePosixOwner {
11877 uid: 9_000_000,
11878 gid: 8_000_000,
11879 uname: Some("u".repeat(40)),
11880 gname: Some("g".repeat(40)),
11881 }),
11882 attributes: Some(2),
11883 native: Default::default(),
11884 },
11885 ..RegularFile::new("pax-overrides.txt", b"PAX metadata")
11886 },
11887 ];
11888 let archive = write_archive(&files, &master_key(), single_stream_options()).unwrap();
11889 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11890
11891 assert_eq!(
11892 opened.crypto_header.compression_algo,
11893 CompressionAlgo::ZstdFramed
11894 );
11895
11896 let (header_records, header_primary) =
11897 decoded_primary_metadata_storage(&opened, "header-fields.txt");
11898 for key in ["uid", "gid", "uname", "gname", "mtime"] {
11899 assert!(!header_records.contains_key(key), "{key} should use ustar");
11900 }
11901 assert_eq!(parse_test_tar_octal(&header_primary[108..116]), 42);
11902 assert_eq!(parse_test_tar_octal(&header_primary[116..124]), 84);
11903 assert_eq!(nul_trimmed_test_field(&header_primary[265..297]), b"alice");
11904 assert_eq!(nul_trimmed_test_field(&header_primary[297..329]), b"staff");
11905 assert_eq!(
11906 parse_test_tar_octal(&header_primary[136..148]),
11907 1_700_000_000
11908 );
11909
11910 let (pax_records, pax_primary) =
11911 decoded_primary_metadata_storage(&opened, "pax-overrides.txt");
11912 let long_uname = "u".repeat(40);
11913 let long_gname = "g".repeat(40);
11914 for (key, expected) in [
11915 ("uid", b"9000000".as_slice()),
11916 ("gid", b"8000000".as_slice()),
11917 ("uname", long_uname.as_bytes()),
11918 ("gname", long_gname.as_bytes()),
11919 ("mtime", b"-1.5".as_slice()),
11920 ] {
11921 assert_eq!(pax_records.get(key).map(Vec::as_slice), Some(expected));
11922 }
11923 assert_eq!(parse_test_tar_octal(&pax_primary[108..116]), 0);
11924 assert_eq!(parse_test_tar_octal(&pax_primary[116..124]), 0);
11925 assert!(nul_trimmed_test_field(&pax_primary[265..297]).is_empty());
11926 assert!(nul_trimmed_test_field(&pax_primary[297..329]).is_empty());
11927 assert_eq!(parse_test_tar_octal(&pax_primary[136..148]), 0);
11928
11929 for expected in &files {
11930 let index = opened.lookup_index_entry(expected.path).unwrap().unwrap();
11931 assert!(index.layout.compressed_size > 0);
11932 assert_eq!(
11933 index.flags,
11934 crate::entry_metadata::EXTENDED_METADATA_V1
11935 | crate::entry_metadata::REQUIRES_SYSTEM_RESTORE
11936 );
11937
11938 let located = opened
11939 .locate_index_file(expected.path.as_bytes())
11940 .unwrap()
11941 .unwrap();
11942 let member = opened
11943 .decode_loaded_owned_tar_member(&located.shard, located.file_index, false)
11944 .unwrap();
11945 let metadata = member.v45_metadata.unwrap();
11946 let owner = expected.portable_metadata.posix_owner.as_ref().unwrap();
11947
11948 assert_eq!(member.mode, expected.mode);
11949 assert_eq!(member.mtime, expected.mtime);
11950 assert_eq!(
11951 metadata.file_entry_flags,
11952 crate::entry_metadata::EXTENDED_METADATA_V1
11953 | crate::entry_metadata::REQUIRES_SYSTEM_RESTORE
11954 );
11955 assert_eq!(
11956 metadata.declaration.source_os,
11957 expected.portable_metadata.source_os
11958 );
11959 assert_eq!(
11960 metadata.declaration.source_filesystem,
11961 expected.portable_metadata.source_filesystem
11962 );
11963 assert!(metadata.declaration.mode_origin_native);
11964 assert_eq!(metadata.portable_mirror.mode, expected.mode);
11965 assert_eq!(
11966 metadata.portable_mirror.mtime,
11967 (expected.mtime.seconds, expected.mtime.nanoseconds)
11968 );
11969 assert_eq!(
11970 metadata.portable_mirror.attributes,
11971 expected.portable_metadata.attributes
11972 );
11973 assert_eq!(metadata.portable_mirror.uid, Some(owner.uid));
11974 assert_eq!(metadata.portable_mirror.gid, Some(owner.gid));
11975 assert_eq!(
11976 metadata.portable_mirror.uname.as_deref(),
11977 owner.uname.as_deref().map(str::as_bytes)
11978 );
11979 assert_eq!(
11980 metadata.portable_mirror.gname.as_deref(),
11981 owner.gname.as_deref().map(str::as_bytes)
11982 );
11983 }
11984 }
11985
11986 #[test]
11987 fn compressed_archive_extraction_applies_portable_mode_and_pax_mtime() {
11988 let archived_mtime = ArchiveTimestamp::new(946_684_800, 123_456_700);
11989 let archive = write_archive(
11990 &[RegularFile {
11991 mode: 0o604,
11992 mtime: archived_mtime,
11993 ..RegularFile::new("dated.txt", b"dated")
11994 }],
11995 &master_key(),
11996 single_stream_options(),
11997 )
11998 .unwrap();
11999 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12000 assert_eq!(
12001 opened
12002 .lookup_index_entry("dated.txt")
12003 .unwrap()
12004 .unwrap()
12005 .flags,
12006 crate::entry_metadata::EXTENDED_METADATA_V1
12007 );
12008 let content_root = tempfile::tempdir().unwrap();
12009
12010 let content_diagnostics = opened
12011 .extract_file_to(
12012 "dated.txt",
12013 content_root.path(),
12014 SafeExtractionOptions {
12015 restore_policy: crate::entry_metadata::RestorePolicy::Content,
12016 ..SafeExtractionOptions::default()
12017 },
12018 )
12019 .unwrap()
12020 .unwrap();
12021 for metadata_class in ["mode", "mtime"] {
12022 assert!(content_diagnostics.iter().any(|diagnostic| {
12023 diagnostic.metadata_class == metadata_class
12024 && diagnostic.status == crate::tar_model::MetadataDiagnosticStatus::Skipped
12025 }));
12026 }
12027 let content_mtime = fs::metadata(content_root.path().join("dated.txt"))
12028 .unwrap()
12029 .modified()
12030 .unwrap();
12031 assert_ne!(
12032 content_mtime,
12033 std::time::UNIX_EPOCH
12034 + std::time::Duration::new(
12035 archived_mtime.seconds as u64,
12036 archived_mtime.nanoseconds,
12037 )
12038 );
12039
12040 let portable_root = tempfile::tempdir().unwrap();
12041 opened
12042 .extract_file_to(
12043 "dated.txt",
12044 portable_root.path(),
12045 SafeExtractionOptions::default(),
12046 )
12047 .unwrap()
12048 .unwrap();
12049 let portable_mtime = fs::metadata(portable_root.path().join("dated.txt"))
12050 .unwrap()
12051 .modified()
12052 .unwrap();
12053 assert_eq!(
12054 portable_mtime,
12055 std::time::UNIX_EPOCH
12056 + std::time::Duration::new(
12057 archived_mtime.seconds as u64,
12058 archived_mtime.nanoseconds,
12059 )
12060 );
12061 #[cfg(unix)]
12062 {
12063 use std::os::unix::fs::PermissionsExt;
12064
12065 assert_eq!(
12066 fs::metadata(portable_root.path().join("dated.txt"))
12067 .unwrap()
12068 .permissions()
12069 .mode()
12070 & 0o7777,
12071 0o604
12072 );
12073 }
12074 }
12075
12076 #[test]
12077 fn same_os_restore_rejects_required_native_profile_from_another_os() {
12078 let (source_os, required_profiles) = if cfg!(target_os = "macos") {
12079 (
12080 "linux",
12081 vec!["posix-backup-v1".into(), "linux-backup-v1".into()],
12082 )
12083 } else {
12084 (
12085 "macos",
12086 vec!["posix-backup-v1".into(), "macos-backup-v1".into()],
12087 )
12088 };
12089 let archive = write_archive(
12090 &[RegularFile {
12091 portable_metadata: PortableFileMetadata {
12092 source_os: source_os.into(),
12093 native: NativeFileMetadata {
12094 required_profiles,
12095 ..NativeFileMetadata::default()
12096 },
12097 ..PortableFileMetadata::default()
12098 },
12099 ..RegularFile::new("foreign-native.txt", b"payload")
12100 }],
12101 &master_key(),
12102 single_stream_options(),
12103 )
12104 .unwrap();
12105 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12106
12107 assert_eq!(
12108 opened
12109 .plan_metadata_restore(SafeExtractionOptions {
12110 restore_policy: RestorePolicy::SameOs,
12111 ..SafeExtractionOptions::default()
12112 })
12113 .unwrap_err(),
12114 FormatError::ReaderUnsupported(
12115 "requested native metadata is not supported by this conformance class"
12116 )
12117 );
12118 }
12119
12120 #[cfg(unix)]
12121 #[test]
12122 fn compressed_archive_extraction_restores_setid_bits_only_for_authorized_system_policy() {
12123 use std::os::unix::fs::{MetadataExt, PermissionsExt};
12124
12125 let uid = unsafe { libc::geteuid() } as u64;
12127 let gid = unsafe { libc::getegid() } as u64;
12128
12129 let archive = write_archive(
12130 &[RegularFile {
12131 mode: 0o7751,
12132 mtime: ArchiveTimestamp::from_seconds(1_700_000_000),
12133 portable_metadata: PortableFileMetadata {
12134 source_os: "other-unix".into(),
12135 source_filesystem: "unknown".into(),
12136 mode_origin: PortableModeOrigin::Native,
12137 posix_owner: Some(PortablePosixOwner {
12138 uid,
12139 gid,
12140 uname: None,
12141 gname: None,
12142 }),
12143 attributes: None,
12144 native: Default::default(),
12145 },
12146 ..RegularFile::new("privileged.sh", b"#!/bin/sh\n")
12147 }],
12148 &master_key(),
12149 single_stream_options(),
12150 )
12151 .unwrap();
12152 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12153 assert_eq!(
12154 opened
12155 .lookup_index_entry("privileged.sh")
12156 .unwrap()
12157 .unwrap()
12158 .flags,
12159 crate::entry_metadata::EXTENDED_METADATA_V1
12160 | crate::entry_metadata::REQUIRES_SYSTEM_RESTORE
12161 );
12162
12163 let portable_root = tempfile::tempdir().unwrap();
12164 let portable_diagnostics = opened
12165 .extract_file_to(
12166 "privileged.sh",
12167 portable_root.path(),
12168 SafeExtractionOptions::default(),
12169 )
12170 .unwrap()
12171 .unwrap();
12172 assert!(portable_diagnostics.iter().any(|diagnostic| {
12173 diagnostic.metadata_class == "setid-mode"
12174 && diagnostic.status == crate::tar_model::MetadataDiagnosticStatus::Skipped
12175 }));
12176 assert!(portable_diagnostics.iter().any(|diagnostic| {
12177 diagnostic.metadata_class == "numeric-ownership"
12178 && diagnostic.status == crate::tar_model::MetadataDiagnosticStatus::Skipped
12179 }));
12180 assert_eq!(
12181 fs::metadata(portable_root.path().join("privileged.sh"))
12182 .unwrap()
12183 .permissions()
12184 .mode()
12185 & 0o7777,
12186 0o1751
12187 );
12188
12189 let same_os_root = tempfile::tempdir().unwrap();
12190 opened
12191 .extract_file_to(
12192 "privileged.sh",
12193 same_os_root.path(),
12194 SafeExtractionOptions {
12195 restore_policy: crate::entry_metadata::RestorePolicy::SameOs,
12196 ..SafeExtractionOptions::default()
12197 },
12198 )
12199 .unwrap()
12200 .unwrap();
12201 assert_eq!(
12202 fs::metadata(same_os_root.path().join("privileged.sh"))
12203 .unwrap()
12204 .permissions()
12205 .mode()
12206 & 0o7777,
12207 0o1751
12208 );
12209
12210 let unauthorized_root = tempfile::tempdir().unwrap();
12211 assert_eq!(
12212 opened
12213 .extract_file_to(
12214 "privileged.sh",
12215 unauthorized_root.path(),
12216 SafeExtractionOptions {
12217 restore_policy: crate::entry_metadata::RestorePolicy::System,
12218 ..SafeExtractionOptions::default()
12219 },
12220 )
12221 .unwrap_err(),
12222 FormatError::ReaderUnsupported(
12223 "system restore policy requires explicit caller authorization"
12224 )
12225 );
12226 assert!(!unauthorized_root.path().join("privileged.sh").exists());
12227
12228 let system_root = tempfile::tempdir().unwrap();
12229 opened
12230 .extract_file_to(
12231 "privileged.sh",
12232 system_root.path(),
12233 SafeExtractionOptions {
12234 restore_policy: crate::entry_metadata::RestorePolicy::System,
12235 system_authorized: true,
12236 ..SafeExtractionOptions::default()
12237 },
12238 )
12239 .unwrap()
12240 .unwrap();
12241 assert_eq!(
12242 fs::metadata(system_root.path().join("privileged.sh"))
12243 .unwrap()
12244 .permissions()
12245 .mode()
12246 & 0o7777,
12247 0o7751
12248 );
12249 let restored = fs::metadata(system_root.path().join("privileged.sh")).unwrap();
12250 assert_eq!(restored.uid() as u64, uid);
12251 assert_eq!(restored.gid() as u64, gid);
12252 }
12253
12254 #[cfg(unix)]
12255 #[test]
12256 fn compressed_archive_extraction_restores_native_xattrs_only_under_native_policy() {
12257 let mut native = NativeFileMetadata {
12258 required_profiles: vec!["posix-backup-v1".into()],
12259 ..NativeFileMetadata::default()
12260 };
12261 native.primary_pax_records.insert(
12262 "LIBARCHIVE.xattr.user.tzap-test".into(),
12263 crate::entry_metadata::canonical_base64_encode(b"native value"),
12264 );
12265 let archive = write_archive(
12266 &[RegularFile {
12267 portable_metadata: PortableFileMetadata {
12268 source_os: source_os_for_test().into(),
12269 native,
12270 ..PortableFileMetadata::default()
12271 },
12272 ..RegularFile::new("xattr.txt", b"payload")
12273 }],
12274 &master_key(),
12275 single_stream_options(),
12276 )
12277 .unwrap();
12278 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12279
12280 let portable_root = tempfile::tempdir().unwrap();
12281 opened
12282 .extract_file_to(
12283 "xattr.txt",
12284 portable_root.path(),
12285 SafeExtractionOptions {
12286 restore_policy: RestorePolicy::Portable,
12287 ..SafeExtractionOptions::default()
12288 },
12289 )
12290 .unwrap();
12291 assert_eq!(
12292 xattr::get(portable_root.path().join("xattr.txt"), "user.tzap-test").unwrap(),
12293 None
12294 );
12295
12296 let native_root = tempfile::tempdir().unwrap();
12297 opened
12298 .extract_file_to(
12299 "xattr.txt",
12300 native_root.path(),
12301 SafeExtractionOptions {
12302 restore_policy: RestorePolicy::SameOs,
12303 ..SafeExtractionOptions::default()
12304 },
12305 )
12306 .unwrap();
12307 assert_eq!(
12308 xattr::get(native_root.path().join("xattr.txt"), "user.tzap-test").unwrap(),
12309 Some(b"native value".to_vec())
12310 );
12311 }
12312
12313 #[cfg(target_os = "linux")]
12314 #[test]
12315 fn compressed_archive_extraction_restores_auxiliary_linux_xattr() {
12316 let mut record = NativeAuxiliaryMetadata::new(
12317 "generic.xattr",
12318 "posix-backup-v1",
12319 crate::entry_metadata::RestoreClass::SameOs,
12320 b"auxiliary xattr value".to_vec(),
12321 );
12322 record.name_encoding = NativeAuxiliaryNameEncoding::Bytes;
12323 record.name = b"user.tzap-aux".to_vec();
12324 let native = NativeFileMetadata {
12325 required_profiles: vec!["posix-backup-v1".into(), "linux-backup-v1".into()],
12326 auxiliary_records: vec![record],
12327 ..NativeFileMetadata::default()
12328 };
12329 let archive = write_archive(
12330 &[RegularFile {
12331 portable_metadata: PortableFileMetadata {
12332 source_os: "linux".into(),
12333 native,
12334 ..PortableFileMetadata::default()
12335 },
12336 ..RegularFile::new("aux-xattr.txt", b"payload")
12337 }],
12338 &master_key(),
12339 single_stream_options(),
12340 )
12341 .unwrap();
12342 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12343 let root = tempfile::tempdir().unwrap();
12344 opened
12345 .extract_file_to(
12346 "aux-xattr.txt",
12347 root.path(),
12348 SafeExtractionOptions {
12349 restore_policy: RestorePolicy::SameOs,
12350 ..SafeExtractionOptions::default()
12351 },
12352 )
12353 .unwrap();
12354 assert_eq!(
12355 xattr::get(root.path().join("aux-xattr.txt"), "user.tzap-aux").unwrap(),
12356 Some(b"auxiliary xattr value".to_vec())
12357 );
12358 }
12359
12360 #[cfg(target_os = "linux")]
12361 #[test]
12362 fn same_os_restore_reports_system_xattr_as_policy_skipped() {
12363 let mut native = NativeFileMetadata {
12364 required_profiles: vec!["posix-backup-v1".into(), "linux-backup-v1".into()],
12365 ..NativeFileMetadata::default()
12366 };
12367 native.primary_pax_records.insert(
12368 "LIBARCHIVE.xattr.security.selinux".into(),
12369 crate::entry_metadata::canonical_base64_encode(b"label"),
12370 );
12371 let archive = write_archive(
12372 &[RegularFile {
12373 portable_metadata: PortableFileMetadata {
12374 source_os: "linux".into(),
12375 native,
12376 ..PortableFileMetadata::default()
12377 },
12378 ..RegularFile::new("label.txt", b"payload")
12379 }],
12380 &master_key(),
12381 single_stream_options(),
12382 )
12383 .unwrap();
12384 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12385 let root = tempfile::tempdir().unwrap();
12386 let diagnostics = opened
12387 .extract_file_to(
12388 "label.txt",
12389 root.path(),
12390 SafeExtractionOptions {
12391 restore_policy: RestorePolicy::SameOs,
12392 ..SafeExtractionOptions::default()
12393 },
12394 )
12395 .unwrap()
12396 .unwrap();
12397 assert!(diagnostics.iter().any(|diagnostic| {
12398 diagnostic.metadata_class == "system-extended-attribute"
12399 && diagnostic.status == crate::tar_model::MetadataDiagnosticStatus::Skipped
12400 }));
12401 assert_eq!(
12402 xattr::get(root.path().join("label.txt"), "security.selinux").unwrap(),
12403 None
12404 );
12405 }
12406
12407 #[cfg(target_os = "linux")]
12408 fn source_os_for_test() -> &'static str {
12409 "linux"
12410 }
12411
12412 #[cfg(target_os = "macos")]
12413 fn source_os_for_test() -> &'static str {
12414 "macos"
12415 }
12416
12417 #[cfg(all(unix, not(target_os = "linux"), not(target_os = "macos")))]
12418 fn source_os_for_test() -> &'static str {
12419 "other-unix"
12420 }
12421
12422 #[cfg(target_os = "linux")]
12423 #[test]
12424 fn compressed_archive_extraction_restores_linux_inode_flags() {
12425 use std::os::fd::AsRawFd;
12426
12427 let root = tempfile::tempdir().unwrap();
12428 let probe_path = root.path().join("flags-probe.txt");
12429 fs::write(&probe_path, b"probe").unwrap();
12430 let probe = fs::File::open(&probe_path).unwrap();
12431 let mut intrinsic_flags: libc::c_long = 0;
12432 assert_eq!(
12433 unsafe {
12434 libc::ioctl(
12435 probe.as_raw_fd(),
12436 libc::FS_IOC_GETFLAGS,
12437 &mut intrinsic_flags,
12438 )
12439 },
12440 0
12441 );
12442 drop(probe);
12443 fs::remove_file(probe_path).unwrap();
12444 let expected_flags =
12445 intrinsic_flags as u64 | u64::from(linux_raw_sys::general::FS_NODUMP_FL);
12446 let mut native = NativeFileMetadata {
12447 required_profiles: vec!["posix-backup-v1".into(), "linux-backup-v1".into()],
12448 ..NativeFileMetadata::default()
12449 };
12450 native.primary_pax_records.insert(
12451 "TZAP.linux.fsflags".into(),
12452 format!("{expected_flags:016x}").into_bytes(),
12453 );
12454 let archive = write_archive(
12455 &[RegularFile {
12456 portable_metadata: PortableFileMetadata {
12457 source_os: "linux".into(),
12458 native,
12459 ..PortableFileMetadata::default()
12460 },
12461 ..RegularFile::new("flags.txt", b"payload")
12462 }],
12463 &master_key(),
12464 single_stream_options(),
12465 )
12466 .unwrap();
12467 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12468 opened
12469 .extract_file_to(
12470 "flags.txt",
12471 root.path(),
12472 SafeExtractionOptions {
12473 restore_policy: RestorePolicy::SameOs,
12474 ..SafeExtractionOptions::default()
12475 },
12476 )
12477 .unwrap();
12478 let file = fs::File::open(root.path().join("flags.txt")).unwrap();
12479 let mut flags: libc::c_long = 0;
12480 assert_eq!(
12482 unsafe { libc::ioctl(file.as_raw_fd(), libc::FS_IOC_GETFLAGS, &mut flags,) },
12483 0
12484 );
12485 assert_eq!(flags as u64, expected_flags);
12486 }
12487
12488 #[cfg(target_os = "linux")]
12489 #[test]
12490 fn compressed_archive_extraction_restores_canonical_posix_acl() {
12491 let acl = b"user::rw-,group::r--,other::---,user:123:r--,mask::r--";
12492 let mut native = NativeFileMetadata {
12493 required_profiles: vec!["posix-backup-v1".into()],
12494 ..NativeFileMetadata::default()
12495 };
12496 native
12497 .primary_pax_records
12498 .insert("SCHILY.acl.access".into(), acl.to_vec());
12499 native
12500 .primary_pax_records
12501 .insert("TZAP.acl.projection".into(), b"exact".to_vec());
12502 native.primary_pax_records.insert(
12503 "TZAP.acl.syntax".into(),
12504 b"schily-posix1e-extra-id-v1".to_vec(),
12505 );
12506 let archive = write_archive(
12507 &[RegularFile {
12508 mode: 0o640,
12509 portable_metadata: PortableFileMetadata {
12510 source_os: "linux".into(),
12511 native,
12512 ..PortableFileMetadata::default()
12513 },
12514 ..RegularFile::new("acl.txt", b"payload")
12515 }],
12516 &master_key(),
12517 single_stream_options(),
12518 )
12519 .unwrap();
12520 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12521 let root = tempfile::tempdir().unwrap();
12522 opened
12523 .extract_file_to(
12524 "acl.txt",
12525 root.path(),
12526 SafeExtractionOptions {
12527 restore_policy: RestorePolicy::SameOs,
12528 ..SafeExtractionOptions::default()
12529 },
12530 )
12531 .unwrap();
12532 let binary = xattr::get(root.path().join("acl.txt"), "system.posix_acl_access")
12533 .unwrap()
12534 .unwrap();
12535 assert_eq!(
12536 crate::entry_metadata::linux_posix_acl_xattr_to_schily(&binary).unwrap(),
12537 acl
12538 );
12539 }
12540
12541 #[cfg(windows)]
12542 #[test]
12543 #[allow(clippy::permissions_set_readonly_false)]
12544 fn compressed_archive_extraction_restores_portable_readonly_attribute() {
12545 let archive = write_archive(
12546 &[RegularFile {
12547 portable_metadata: PortableFileMetadata {
12548 source_os: "windows".into(),
12549 source_filesystem: "ntfs".into(),
12550 mode_origin: PortableModeOrigin::Projected,
12551 posix_owner: None,
12552 attributes: Some(1),
12553 native: Default::default(),
12554 },
12555 ..RegularFile::new("readonly.txt", b"readonly")
12556 }],
12557 &master_key(),
12558 single_stream_options(),
12559 )
12560 .unwrap();
12561 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12562 let root = tempfile::tempdir().unwrap();
12563
12564 opened
12565 .extract_file_to(
12566 "readonly.txt",
12567 root.path(),
12568 SafeExtractionOptions::default(),
12569 )
12570 .unwrap()
12571 .unwrap();
12572 let path = root.path().join("readonly.txt");
12573 assert!(fs::metadata(&path).unwrap().permissions().readonly());
12574
12575 let mut permissions = fs::metadata(&path).unwrap().permissions();
12577 permissions.set_readonly(false);
12578 fs::set_permissions(path, permissions).unwrap();
12579 }
12580
12581 #[test]
12582 fn seekable_extract_all_to_streams_unique_archive() {
12583 let archive = write_archive(
12584 &[
12585 RegularFile::new("alpha.txt", b"alpha"),
12586 RegularFile::new("dir/beta.txt", b"beta"),
12587 ],
12588 &master_key(),
12589 single_stream_options(),
12590 )
12591 .unwrap();
12592 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12593 let tmp = tempfile::tempdir().unwrap();
12594
12595 let diagnostics = opened
12596 .extract_all_to(tmp.path(), SafeExtractionOptions::default())
12597 .unwrap();
12598
12599 assert_eq!(diagnostics.len(), 2);
12600 assert_eq!(fs::read(tmp.path().join("alpha.txt")).unwrap(), b"alpha");
12601 assert_eq!(
12602 fs::read(tmp.path().join("dir").join("beta.txt")).unwrap(),
12603 b"beta"
12604 );
12605 }
12606
12607 #[test]
12608 fn seekable_extract_all_to_rejects_duplicate_paths_for_cli_fallback() {
12609 let archive = write_archive(
12610 &[
12611 RegularFile::new("same.txt", b"old"),
12612 RegularFile::new("same.txt", b"new"),
12613 ],
12614 &master_key(),
12615 single_stream_options(),
12616 )
12617 .unwrap();
12618 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12619 let tmp = tempfile::tempdir().unwrap();
12620
12621 assert_eq!(
12622 opened
12623 .extract_all_to(tmp.path(), SafeExtractionOptions::default())
12624 .unwrap_err(),
12625 FormatError::ReaderUnsupported("fast full extract requires unique archive paths")
12626 );
12627 }
12628
12629 #[test]
12630 fn seekable_extract_indexed_files_to_restores_final_duplicate_winner() {
12631 let archive = write_archive(
12632 &[
12633 RegularFile::new("same.txt", b"old"),
12634 RegularFile::new("same.txt", b"new"),
12635 ],
12636 &master_key(),
12637 single_stream_options(),
12638 )
12639 .unwrap();
12640 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12641 let tmp = tempfile::tempdir().unwrap();
12642
12643 let diagnostics = opened
12644 .extract_indexed_files_to(tmp.path(), SafeExtractionOptions::default(), 2)
12645 .unwrap();
12646
12647 assert_eq!(diagnostics.len(), 1);
12648 assert_eq!(diagnostics[0].0, "same.txt");
12649 assert_eq!(fs::read(tmp.path().join("same.txt")).unwrap(), b"new");
12650 }
12651
12652 #[test]
12653 fn selected_restore_preflights_every_path_before_writing() {
12654 let archive = write_archive(
12655 &[RegularFile::new("alpha.txt", b"alpha")],
12656 &master_key(),
12657 single_stream_options(),
12658 )
12659 .unwrap();
12660 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12661 let tmp = tempfile::tempdir().unwrap();
12662
12663 let error = opened
12664 .extract_selected_files_to(
12665 &["alpha.txt".into(), "missing.txt".into()],
12666 tmp.path(),
12667 SafeExtractionOptions::default(),
12668 2,
12669 )
12670 .unwrap_err();
12671
12672 assert_eq!(
12673 error,
12674 FormatError::ReaderUnsupported("selected archive path is absent from the final index")
12675 );
12676 assert!(!tmp.path().join("alpha.txt").exists());
12677 }
12678
12679 #[test]
12680 fn safe_extract_rejects_overwriting_existing_file_by_default() {
12681 let archive = write_archive(
12682 &[RegularFile::new("hello.txt", b"new")],
12683 &master_key(),
12684 single_stream_options(),
12685 )
12686 .unwrap();
12687 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12688 let tmp = tempfile::tempdir().unwrap();
12689 std::fs::write(tmp.path().join("hello.txt"), b"old").unwrap();
12690
12691 assert_eq!(
12692 opened
12693 .extract_file_to("hello.txt", tmp.path(), SafeExtractionOptions::default())
12694 .unwrap_err(),
12695 FormatError::UnsafeOverwrite
12696 );
12697 assert_eq!(std::fs::read(tmp.path().join("hello.txt")).unwrap(), b"old");
12698 }
12699
12700 #[test]
12701 fn opens_and_verifies_empty_archive() {
12702 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
12703 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12704
12705 assert!(opened.list_files().unwrap().is_empty());
12706 opened.verify().unwrap();
12707 }
12708
12709 #[test]
12710 fn default_reader_options_allow_v36_trailing_garbage_scan() {
12711 let archive = write_archive(
12712 &[RegularFile::new("garbage-tolerant.txt", b"still intact")],
12713 &master_key(),
12714 single_stream_options(),
12715 )
12716 .unwrap();
12717 let mut with_trailing_garbage = archive.bytes.clone();
12718 with_trailing_garbage.extend_from_slice(b"ignored trailing bytes");
12719
12720 let opened = open_archive(&with_trailing_garbage, &master_key()).unwrap();
12721 assert_eq!(
12722 opened.extract_file("garbage-tolerant.txt").unwrap(),
12723 Some(b"still intact".to_vec())
12724 );
12725 }
12726
12727 #[test]
12728 fn seekable_open_rejects_too_small_and_unavailable_header_crypto_bytes() {
12729 assert_eq!(
12730 open_archive(
12731 &[0u8; VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1],
12732 &master_key()
12733 )
12734 .unwrap_err(),
12735 FormatError::InvalidLength {
12736 structure: "archive",
12737 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
12738 actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1,
12739 }
12740 );
12741
12742 let mut header = test_volume_header();
12743 header.crypto_header_length = 512;
12744 let mut unavailable_crypto = header.to_bytes().to_vec();
12745 unavailable_crypto.resize(VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN, 0);
12746
12747 assert_eq!(
12748 open_archive(&unavailable_crypto, &master_key()).unwrap_err(),
12749 FormatError::InvalidLength {
12750 structure: "CryptoHeader",
12751 expected: VOLUME_HEADER_LEN + 512,
12752 actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
12753 }
12754 );
12755 }
12756
12757 #[test]
12758 fn seekable_open_recovers_physical_noncanonical_crypto_header_offset() {
12759 let archive = write_archive(
12760 &[RegularFile::new("offset.txt", b"offset")],
12761 &master_key(),
12762 small_block_recovery_options(),
12763 )
12764 .unwrap();
12765 let mut mutated = archive.bytes;
12766 let mut header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
12767 header.crypto_header_offset = VOLUME_HEADER_LEN as u32 + 1;
12768 mutated[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
12769
12770 let opened = open_archive(&mutated, &master_key()).unwrap();
12771 assert_eq!(
12772 opened.volume_header.crypto_header_offset,
12773 VOLUME_HEADER_LEN as u32
12774 );
12775 assert_eq!(
12776 opened.extract_file("offset.txt").unwrap(),
12777 Some(b"offset".to_vec())
12778 );
12779 opened.verify().unwrap();
12780 }
12781
12782 #[test]
12783 fn rejects_wrong_key_before_metadata_release() {
12784 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
12785 let wrong = MasterKey::from_raw_key(&[0x43; 32]).unwrap();
12786
12787 assert_eq!(
12788 open_archive(&archive.bytes, &wrong).unwrap_err(),
12789 FormatError::HmacMismatch {
12790 structure: "CryptoHeader"
12791 }
12792 );
12793 }
12794
12795 #[test]
12796 fn ordinary_encrypted_writers_emit_v45_archives() {
12797 let raw_key_archive = write_archive(
12798 &[RegularFile::new("raw.txt", b"raw key payload")],
12799 &master_key(),
12800 single_stream_options(),
12801 )
12802 .unwrap();
12803 let raw_header = VolumeHeader::parse(&raw_key_archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
12804 assert_eq!(raw_header.volume_format_rev, VOLUME_FORMAT_REV_45);
12805 let raw_opened = open_archive(&raw_key_archive.bytes, &master_key()).unwrap();
12806 assert_eq!(
12807 raw_opened.volume_header.volume_format_rev,
12808 VOLUME_FORMAT_REV_45
12809 );
12810 assert_eq!(
12811 raw_opened.extract_file("raw.txt").unwrap(),
12812 Some(b"raw key payload".to_vec())
12813 );
12814
12815 let passphrase_kdf = KdfParams::Argon2id {
12816 t_cost: 1,
12817 m_cost_kib: 8,
12818 parallelism: 1,
12819 salt: b"0123456789abcdef".to_vec(),
12820 };
12821 let passphrase_archive = write_archive_with_kdf(
12822 &[RegularFile::new("pass.txt", b"passphrase payload")],
12823 &master_key(),
12824 single_stream_options(),
12825 &passphrase_kdf,
12826 )
12827 .unwrap();
12828 let passphrase_header =
12829 VolumeHeader::parse(&passphrase_archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
12830 assert_eq!(passphrase_header.volume_format_rev, VOLUME_FORMAT_REV_45);
12831 let passphrase_opened = open_archive(&passphrase_archive.bytes, &master_key()).unwrap();
12832 assert_eq!(
12833 passphrase_opened.volume_header.volume_format_rev,
12834 VOLUME_FORMAT_REV_45
12835 );
12836 assert_eq!(
12837 passphrase_opened.extract_file("pass.txt").unwrap(),
12838 Some(b"passphrase payload".to_vec())
12839 );
12840 }
12841
12842 #[test]
12843 fn rejects_future_volume_format_revision_before_key_mismatch() {
12844 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
12845 let mut bytes = archive.bytes;
12846 let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12847 header.volume_format_rev = VOLUME_FORMAT_REV_45 + 1;
12848 bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
12849 let wrong = MasterKey::from_raw_key(&[0x43; 32]).unwrap();
12850
12851 assert_eq!(
12852 open_archive(&bytes, &wrong).unwrap_err(),
12853 FormatError::UnsupportedVolumeFormatRevision {
12854 format_version: FORMAT_VERSION,
12855 volume_format_rev: VOLUME_FORMAT_REV_45 + 1,
12856 reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
12857 }
12858 );
12859 }
12860
12861 #[test]
12862 fn open_archive_unencrypted_accepts_v45_profile() {
12863 let archive = write_archive_unencrypted(
12864 &[RegularFile::new("payload.txt", b"smoke-v45-unencrypted")],
12865 WriterOptions {
12866 aead_algo: AeadAlgo::None,
12867 ..single_stream_options()
12868 },
12869 )
12870 .unwrap();
12871
12872 let opened = open_archive_unencrypted(&archive.bytes).unwrap();
12873 let header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
12874
12875 assert_eq!(header.volume_format_rev, VOLUME_FORMAT_REV_45);
12876 assert_eq!(opened.volume_header.volume_format_rev, VOLUME_FORMAT_REV_45);
12877 assert_eq!(
12878 opened.extract_file("payload.txt").unwrap(),
12879 Some(b"smoke-v45-unencrypted".to_vec())
12880 );
12881 opened.verify().unwrap();
12882 }
12883
12884 #[test]
12885 fn full_verification_reports_v45_metadata_capabilities_separately() {
12886 let archive = write_archive(
12887 &[RegularFile::new("report.txt", b"report")],
12888 &master_key(),
12889 single_stream_options(),
12890 )
12891 .unwrap();
12892 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12893 let verification = opened.verify_content().unwrap();
12894 let report = verification.metadata_report().unwrap();
12895
12896 assert!(report.all_capture_complete);
12897 assert!(report.full_fidelity_possible);
12898 assert_eq!(report.profiles_present, ["portable-v1"]);
12899 assert!(report.auxiliary_kinds_present.is_empty());
12900 assert_eq!(report.entries.len(), 1);
12901 assert_eq!(report.entries[0].path, b"report.txt");
12902 assert!(report.entries[0]
12903 .policy_capabilities
12904 .iter()
12905 .all(|capability| capability.policy_complete));
12906 assert!(report.entries[0].diagnostics.is_empty());
12907 }
12908
12909 #[test]
12910 fn root_auth_unencrypted_v45_round_trips_with_recomputed_archive_root() {
12911 let archive = write_archive_with_root_auth(
12912 &[RegularFile::new(
12913 "signed-v45.txt",
12914 b"root-auth v45 plaintext",
12915 )],
12916 &master_key(),
12917 WriterOptions {
12918 aead_algo: AeadAlgo::None,
12919 ..single_stream_options()
12920 },
12921 test_root_auth_config(),
12922 |request| Ok(test_root_auth_value(request)),
12923 )
12924 .unwrap();
12925 let opened = open_archive_unencrypted(&archive.bytes).unwrap();
12926 let header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
12927
12928 assert_eq!(header.volume_format_rev, VOLUME_FORMAT_REV_45);
12929 assert_eq!(opened.volume_header.volume_format_rev, VOLUME_FORMAT_REV_45);
12930 assert_eq!(
12931 opened.extract_file("signed-v45.txt").unwrap(),
12932 Some(b"root-auth v45 plaintext".to_vec())
12933 );
12934
12935 let verified = opened
12936 .verify_root_auth_with(|footer, archive_root| {
12937 Ok(test_root_auth_verifies(footer, archive_root))
12938 })
12939 .unwrap();
12940
12941 assert_eq!(verified.format_version, FORMAT_VERSION);
12942 assert_eq!(verified.volume_format_rev, VOLUME_FORMAT_REV_45);
12943 assert_eq!(
12944 verified.archive_root,
12945 opened.root_auth_footer.as_ref().unwrap().archive_root
12946 );
12947 }
12948
12949 #[test]
12950 fn recipientwrap_open_accepts_candidate_after_header_hmac() {
12951 let master = master_key();
12952 let archive = write_archive_with_recipient_wrap_records(
12953 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12954 &master,
12955 single_stream_options(),
12956 vec![recipient_wrap_test_record()],
12957 )
12958 .unwrap();
12959
12960 let opened = open_archive_with_recipient_wrap_resolver(&archive.bytes, |context| {
12961 assert_eq!(
12962 context.archive_identity.volume_format_rev,
12963 VOLUME_FORMAT_REV_45
12964 );
12965 assert_eq!(context.record.profile_id, 1);
12966 Ok(vec![master.0])
12967 })
12968 .unwrap();
12969
12970 assert_eq!(
12971 opened.extract_file("wrapped.txt").unwrap(),
12972 Some(b"recipient payload".to_vec())
12973 );
12974 opened.verify().unwrap();
12975 }
12976
12977 #[test]
12978 fn recipientwrap_seekable_open_uses_lazy_block_source() {
12979 let master = master_key();
12980 let archive = write_archive_with_recipient_wrap_records(
12981 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12982 &master,
12983 single_stream_options(),
12984 vec![recipient_wrap_test_record()],
12985 )
12986 .unwrap();
12987
12988 let opened = open_seekable_archive_with_recipient_wrap_resolver_options(
12989 CountingReadAt::new(archive.bytes, vec![]),
12990 |context| {
12991 assert_eq!(
12992 context.archive_identity.volume_format_rev,
12993 VOLUME_FORMAT_REV_45
12994 );
12995 assert_eq!(context.record.profile_id, 1);
12996 Ok(vec![master.0])
12997 },
12998 ReaderOptions::default(),
12999 )
13000 .unwrap();
13001
13002 assert!(opened.blocks.is_empty());
13003 assert!(opened.lazy_blocks.is_some());
13004 assert_eq!(
13005 opened.extract_file("wrapped.txt").unwrap(),
13006 Some(b"recipient payload".to_vec())
13007 );
13008 opened.verify().unwrap();
13009 }
13010
13011 #[test]
13012 fn recipientwrap_seekable_volume_set_opens_with_resolver() {
13013 let master = master_key();
13014 let archive = write_archive_with_recipient_wrap_records(
13015 &[RegularFile::new("wrapped.txt", b"recipient payload")],
13016 &master,
13017 WriterOptions {
13018 stripe_width: 2,
13019 volume_loss_tolerance: 0,
13020 ..WriterOptions::default()
13021 },
13022 vec![recipient_wrap_test_record()],
13023 )
13024 .unwrap();
13025 assert_eq!(archive.volumes.len(), 2);
13026
13027 let opened = open_seekable_archive_volumes_with_recipient_wrap_resolver_options(
13028 archive.volumes,
13029 |context| {
13030 assert_eq!(
13031 context.archive_identity.volume_format_rev,
13032 VOLUME_FORMAT_REV_45
13033 );
13034 assert_eq!(context.record.profile_id, 1);
13035 Ok(vec![master.0])
13036 },
13037 ReaderOptions::default(),
13038 )
13039 .unwrap();
13040
13041 assert_eq!(opened.observed_volume_count, 2);
13042 assert!(opened.blocks.is_empty());
13043 assert!(opened.lazy_blocks.is_some());
13044 assert_eq!(
13045 opened.extract_file("wrapped.txt").unwrap(),
13046 Some(b"recipient payload".to_vec())
13047 );
13048 opened.verify().unwrap();
13049 }
13050
13051 #[test]
13052 fn recipientwrap_open_tries_subsequent_records_after_failed_candidate() {
13053 let master = master_key();
13054 let mut first_record = recipient_wrap_test_record();
13055 first_record.recipient_identity_bytes = b"first-candidate".to_vec();
13056 let mut second_record = recipient_wrap_test_record();
13057 second_record.recipient_identity_bytes = b"second-candidate".to_vec();
13058
13059 let archive = write_archive_with_recipient_wrap_records(
13060 &[RegularFile::new("wrapped.txt", b"recipient payload")],
13061 &master,
13062 single_stream_options(),
13063 vec![first_record, second_record],
13064 )
13065 .unwrap();
13066
13067 let mut attempts = Vec::new();
13068 let opened = open_archive_with_recipient_wrap_resolver(&archive.bytes, |context| {
13069 attempts.push(context.record.recipient_identity_bytes.clone());
13070 if context.record.recipient_identity_bytes.as_slice() == b"second-candidate" {
13071 Ok(vec![master.0])
13072 } else {
13073 Ok(vec![[0x99u8; 32]])
13074 }
13075 })
13076 .unwrap();
13077
13078 assert_eq!(
13079 attempts,
13080 vec![b"first-candidate".to_vec(), b"second-candidate".to_vec(),]
13081 );
13082 assert_eq!(
13083 opened.extract_file("wrapped.txt").unwrap(),
13084 Some(b"recipient payload".to_vec())
13085 );
13086 opened.verify().unwrap();
13087 }
13088
13089 #[test]
13090 fn recipientwrap_startup_rejects_malformed_record_length() {
13091 let master = master_key();
13092 let options = WriterOptions {
13093 bit_rot_buffer_pct: 0,
13094 ..single_stream_options()
13095 };
13096 let archive = write_archive_with_recipient_wrap_records(
13097 &[RegularFile::new("wrapped.txt", b"recipient payload")],
13098 &master,
13099 options,
13100 vec![recipient_wrap_test_record()],
13101 )
13102 .unwrap();
13103 let mut bytes = archive.bytes;
13104 let (_, _, table_start, _table_len, _) = recipient_wrap_layout(&bytes);
13105 bytes[table_start + 96..table_start + 100].copy_from_slice(&1u32.to_le_bytes());
13106
13107 assert_eq!(
13108 open_archive_with_recipient_wrap_resolver(&bytes, |_| { Ok(vec![master.0]) })
13109 .unwrap_err(),
13110 FormatError::InvalidArchive("RecipientRecordV1 record_length is too small")
13111 );
13112 }
13113
13114 #[test]
13115 fn recipientwrap_future_revision_rejects_before_resolver_callback() {
13116 let archive = write_archive_with_recipient_wrap_records(
13117 &[RegularFile::new("wrapped.txt", b"recipient payload")],
13118 &master_key(),
13119 single_stream_options(),
13120 vec![recipient_wrap_test_record()],
13121 )
13122 .unwrap();
13123 let mut bytes = archive.bytes;
13124 let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
13125 header.volume_format_rev = VOLUME_FORMAT_REV_45 + 1;
13126 bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
13127
13128 let mut called = false;
13129 let err = open_archive_with_recipient_wrap_resolver(&bytes, |_| {
13130 called = true;
13131 Ok(vec![master_key().0])
13132 })
13133 .unwrap_err();
13134
13135 assert!(!called);
13136 assert_eq!(
13137 err,
13138 FormatError::UnsupportedVolumeFormatRevision {
13139 format_version: FORMAT_VERSION,
13140 volume_format_rev: VOLUME_FORMAT_REV_45 + 1,
13141 reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
13142 }
13143 );
13144 }
13145
13146 #[test]
13147 fn recipientwrap_stripe_width_mismatch_rejects_before_resolver_callback() {
13148 let archive = write_archive_with_recipient_wrap_records(
13149 &[RegularFile::new("wrapped.txt", b"recipient payload")],
13150 &master_key(),
13151 single_stream_options(),
13152 vec![recipient_wrap_test_record()],
13153 )
13154 .unwrap();
13155 let mut bytes = archive.bytes;
13156 let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
13157 header.stripe_width += 1;
13158 bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
13159
13160 let mut called = false;
13161 let err = open_archive_with_recipient_wrap_resolver(&bytes, |_| {
13162 called = true;
13163 Ok(vec![master_key().0])
13164 })
13165 .unwrap_err();
13166
13167 assert!(!called);
13168 assert_eq!(
13169 err,
13170 FormatError::InvalidArchive("VolumeHeader and CryptoHeader stripe_width differ")
13171 );
13172 }
13173
13174 #[test]
13175 fn recipientwrap_defers_raw_stream_profile_rejection_until_after_resolver_callback() {
13176 let master = master_key();
13177 let archive = write_archive_with_recipient_wrap_records(
13178 &[RegularFile::new("wrapped.txt", b"recipient payload")],
13179 &master,
13180 single_stream_options(),
13181 vec![recipient_wrap_test_record()],
13182 )
13183 .unwrap();
13184 let mut bytes = archive.bytes;
13185 add_raw_stream_profile_to_physical_crypto_header(&mut bytes);
13186 recompute_physical_crypto_header_hmac(&mut bytes, &master);
13187
13188 let mut called = false;
13189 let err = open_archive_with_recipient_wrap_resolver(&bytes, |_| {
13190 called = true;
13191 Ok(vec![master.0])
13192 })
13193 .unwrap_err();
13194
13195 assert!(called);
13196 assert_eq!(
13197 err,
13198 FormatError::ReaderUnsupported(RAW_STREAM_UNSUPPORTED_MESSAGE)
13199 );
13200 }
13201
13202 #[test]
13203 fn recipientwrap_recovers_physical_key_wrap_table_from_cmra_authority() {
13204 let master = master_key();
13205 let archive = write_archive_with_recipient_wrap_records(
13206 &[RegularFile::new("wrapped.txt", b"recipient payload")],
13207 &master,
13208 single_stream_options(),
13209 vec![recipient_wrap_test_record()],
13210 )
13211 .unwrap();
13212 let mut bytes = archive.bytes;
13213 let header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
13214 let crypto_start = header.crypto_header_offset as usize;
13215 let crypto_end = crypto_start + header.crypto_header_length as usize;
13216 let crypto_header = CryptoHeader::parse(
13217 &bytes[crypto_start..crypto_end],
13218 header.crypto_header_length,
13219 )
13220 .unwrap();
13221 let KdfParams::RecipientWrap {
13222 key_wrap_table_length,
13223 ..
13224 } = crypto_header.kdf_params
13225 else {
13226 panic!("expected RecipientWrap KdfParams");
13227 };
13228 let table_start = crypto_end;
13229 let table_end = table_start + key_wrap_table_length as usize;
13230 bytes[table_end - 1] ^= 0x01;
13231
13232 let mut called = false;
13233 let opened = open_archive_with_recipient_wrap_resolver(&bytes, |context| {
13234 called = true;
13235 assert_eq!(
13236 context.archive_identity.volume_format_rev,
13237 VOLUME_FORMAT_REV_45
13238 );
13239 assert_eq!(context.record.profile_id, 1);
13240 Ok(vec![master.0])
13241 })
13242 .unwrap();
13243
13244 assert!(called);
13245 assert_eq!(
13246 opened.extract_file("wrapped.txt").unwrap(),
13247 Some(b"recipient payload".to_vec())
13248 );
13249 opened.verify().unwrap();
13250 }
13251
13252 #[test]
13253 fn recipientwrap_open_rejects_wrong_candidate_header_hmac() {
13254 let archive = write_archive_with_recipient_wrap_records(
13255 &[RegularFile::new("wrapped.txt", b"recipient payload")],
13256 &master_key(),
13257 single_stream_options(),
13258 vec![recipient_wrap_test_record()],
13259 )
13260 .unwrap();
13261 let wrong_candidate = [0x99u8; 32];
13262
13263 assert_eq!(
13264 open_archive_with_recipient_wrap_resolver(&archive.bytes, |_| {
13265 Ok(vec![wrong_candidate])
13266 })
13267 .unwrap_err(),
13268 FormatError::KeyMaterialMismatch
13269 );
13270 }
13271
13272 #[test]
13273 fn recipientwrap_open_recovers_tampered_physical_crypto_header_hmac_from_cmra() {
13274 let master = master_key();
13275 let archive = write_archive_with_recipient_wrap_records(
13276 &[RegularFile::new("wrapped.txt", b"recipient payload")],
13277 &master,
13278 single_stream_options(),
13279 vec![recipient_wrap_test_record()],
13280 )
13281 .unwrap();
13282 let mut bytes = archive.bytes.clone();
13283 let header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
13284 let hmac_end = header.crypto_header_offset as usize + header.crypto_header_length as usize;
13285 bytes[hmac_end - 1] ^= 0x55;
13286
13287 let opened =
13288 open_archive_with_recipient_wrap_resolver(&bytes, |_| Ok(vec![master.0])).unwrap();
13289
13290 assert_eq!(
13291 opened.extract_file("wrapped.txt").unwrap(),
13292 Some(b"recipient payload".to_vec())
13293 );
13294 assert_eq!(
13295 opened.crypto_header_bytes,
13296 archive.bytes[header.crypto_header_offset as usize..hmac_end]
13297 );
13298 opened.verify().unwrap();
13299 }
13300
13301 #[test]
13302 fn recipientwrap_archive_does_not_fall_back_to_raw_master_key_open() {
13303 let master = master_key();
13304 let archive = write_archive_with_recipient_wrap_records(
13305 &[RegularFile::new("wrapped.txt", b"recipient payload")],
13306 &master,
13307 single_stream_options(),
13308 vec![recipient_wrap_test_record()],
13309 )
13310 .unwrap();
13311
13312 assert_eq!(
13313 open_archive(&archive.bytes, &master).unwrap_err(),
13314 FormatError::KeyMaterialMismatch
13315 );
13316 }
13317
13318 #[test]
13319 fn recipientwrap_seekable_archive_does_not_fall_back_to_raw_master_key_open() {
13320 let master = master_key();
13321 let archive = write_archive_with_recipient_wrap_records(
13322 &[RegularFile::new("wrapped.txt", b"recipient payload")],
13323 &master,
13324 single_stream_options(),
13325 vec![recipient_wrap_test_record()],
13326 )
13327 .unwrap();
13328
13329 assert_eq!(
13330 open_seekable_archive(archive.bytes, &master).unwrap_err(),
13331 FormatError::KeyMaterialMismatch
13332 );
13333 }
13334
13335 #[test]
13336 fn public_no_key_verifies_signed_recipientwrap_block_commitment() {
13337 let archive = write_archive_with_root_auth_and_recipient_wrap_records(
13338 &[RegularFile::new("wrapped.txt", b"recipient payload")],
13339 &master_key(),
13340 single_stream_options(),
13341 vec![recipient_wrap_test_record()],
13342 test_root_auth_config(),
13343 |request| Ok(test_root_auth_value(request)),
13344 )
13345 .unwrap();
13346
13347 let verified = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
13348 Ok(test_root_auth_verifies(footer, archive_root))
13349 })
13350 .unwrap();
13351
13352 assert_eq!(verified.volume_format_rev, VOLUME_FORMAT_REV_45);
13353 assert_eq!(verified.total_data_block_count, 3);
13354 }
13355
13356 #[test]
13357 fn public_no_key_rejects_recipientwrap_startup_and_cmra_kdf_mismatch() {
13358 let archive = write_archive_with_root_auth_and_recipient_wrap_records(
13359 &[RegularFile::new("wrapped.txt", b"recipient payload")],
13360 &master_key(),
13361 single_stream_options(),
13362 vec![recipient_wrap_test_record()],
13363 test_root_auth_config(),
13364 |request| Ok(test_root_auth_value(request)),
13365 )
13366 .unwrap();
13367 let mut bytes = archive.bytes;
13368 mutate_top_level_recipient_wrap_public_profile(&mut bytes);
13369
13370 let err = public_no_key_verify_archive_with(&bytes, |footer, archive_root| {
13371 Ok(test_root_auth_verifies(footer, archive_root))
13372 })
13373 .unwrap_err();
13374
13375 assert_eq!(
13376 err,
13377 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
13378 );
13379 }
13380
13381 #[test]
13382 fn public_no_key_rejects_recipientwrap_kdf_profile_mismatch_across_volumes() {
13383 let archive = write_archive_with_root_auth_and_recipient_wrap_records(
13384 &[RegularFile::new("wrapped.txt", b"recipient payload")],
13385 &master_key(),
13386 WriterOptions {
13387 stripe_width: 2,
13388 volume_loss_tolerance: 0,
13389 ..WriterOptions::default()
13390 },
13391 vec![recipient_wrap_test_record()],
13392 test_root_auth_config(),
13393 |request| Ok(test_root_auth_value(request)),
13394 )
13395 .unwrap();
13396 let mut volumes = archive.volumes;
13397 mutate_top_level_recipient_wrap_public_profile(&mut volumes[1]);
13398 mutate_cmra_recipient_wrap_public_profile(&mut volumes[1]);
13399
13400 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
13401 let err = public_no_key_verify_volumes_with(&volume_refs, |footer, archive_root| {
13402 Ok(test_root_auth_verifies(footer, archive_root))
13403 })
13404 .unwrap_err();
13405
13406 assert_eq!(
13407 err,
13408 FormatError::InvalidArchive("public no-key volume global metadata differs")
13409 );
13410 }
13411
13412 #[test]
13413 fn write_archive_defaults_to_current_revision() {
13414 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
13415 let header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
13416
13417 assert_eq!(header.format_version, FORMAT_VERSION);
13418 assert_eq!(header.volume_format_rev, VOLUME_FORMAT_REV);
13419 }
13420
13421 #[test]
13422 fn non_seekable_stream_rejects_future_volume_format_revision() {
13423 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
13424 let mut bytes = archive.bytes;
13425 let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
13426 header.volume_format_rev = VOLUME_FORMAT_REV_45 + 1;
13427 bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
13428
13429 assert_eq!(
13430 verify_non_seekable_stream(std::io::Cursor::new(bytes), &master_key()).unwrap_err(),
13431 FormatError::UnsupportedVolumeFormatRevision {
13432 format_version: FORMAT_VERSION,
13433 volume_format_rev: VOLUME_FORMAT_REV_45 + 1,
13434 reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
13435 }
13436 );
13437 }
13438
13439 #[test]
13440 fn open_seekable_archive_rejects_future_volume_format_revision() {
13441 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
13442 let mut bytes = archive.bytes;
13443 let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
13444 header.volume_format_rev = VOLUME_FORMAT_REV_45 + 1;
13445 bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
13446
13447 assert_eq!(
13448 open_seekable_archive(CountingReadAt::new(bytes, vec![]), &master_key()).unwrap_err(),
13449 FormatError::UnsupportedVolumeFormatRevision {
13450 format_version: FORMAT_VERSION,
13451 volume_format_rev: VOLUME_FORMAT_REV_45 + 1,
13452 reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
13453 }
13454 );
13455 }
13456
13457 #[test]
13458 fn rejects_payload_tamper_even_with_recomputed_block_crc() {
13459 let mut archive = write_archive(
13460 &[RegularFile::new("file.txt", b"authenticated")],
13461 &master_key(),
13462 single_stream_options(),
13463 )
13464 .unwrap()
13465 .bytes;
13466 let volume = VolumeHeader::parse(&archive[..VOLUME_HEADER_LEN]).unwrap();
13467 let crypto_end = VOLUME_HEADER_LEN + usize::try_from(volume.crypto_header_length).unwrap();
13468 let crypto = CryptoHeader::parse(
13469 &archive[VOLUME_HEADER_LEN..crypto_end],
13470 volume.crypto_header_length,
13471 )
13472 .unwrap();
13473 let block_size = crypto.fixed.block_size as usize;
13474 archive[crypto_end + 16] ^= 1;
13475 let crc_offset = crypto_end + 16 + block_size;
13476 let crc = crc32c::crc32c(&archive[crypto_end..crc_offset]);
13477 archive[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
13478
13479 let opened = open_archive(&archive, &master_key()).unwrap();
13480 assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
13481 }
13482
13483 #[test]
13484 fn list_and_extract_use_final_view_for_duplicate_paths() {
13485 let archive = write_archive(
13486 &[
13487 RegularFile {
13488 mtime: crate::ArchiveTimestamp::from_seconds(1_700_000_000),
13489 ..RegularFile::new("same.txt", b"old")
13490 },
13491 RegularFile {
13492 mtime: crate::ArchiveTimestamp::from_seconds(1_700_000_100),
13493 ..RegularFile::new("same.txt", b"newer")
13494 },
13495 ],
13496 &master_key(),
13497 single_stream_options(),
13498 )
13499 .unwrap();
13500 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13501
13502 let listed = opened.list_index_entries().unwrap();
13503 assert_eq!(listed.len(), 1);
13504 assert_eq!(listed[0].path, "same.txt");
13505 assert_eq!(listed[0].name, "same.txt");
13506 assert_eq!(listed[0].file_data_size, 5);
13507 assert_eq!(listed[0].flags, crate::entry_metadata::EXTENDED_METADATA_V1);
13508 assert_eq!(listed[0].frame_count, 1);
13509 assert!(listed[0].layout.compressed_size > 0);
13510 let looked_up = opened.lookup_index_entry("same.txt").unwrap().unwrap();
13511 assert_eq!(looked_up.path, "same.txt");
13512 assert_eq!(looked_up.file_data_size, 5);
13513 assert_eq!(looked_up.flags, crate::entry_metadata::EXTENDED_METADATA_V1);
13514 assert_eq!(opened.lookup_index_entry("missing.txt").unwrap(), None);
13515 assert_eq!(
13516 opened.list_files().unwrap(),
13517 vec![ArchiveEntry {
13518 path: "same.txt".to_string(),
13519 file_data_size: 5,
13520 kind: TarEntryKind::Regular,
13521 mode: 0o644,
13522 mtime: ArchiveTimestamp::from_seconds(1_700_000_100),
13523 diagnostics: Vec::new(),
13524 }]
13525 );
13526 assert_eq!(
13527 opened.extract_file("same.txt").unwrap(),
13528 Some(b"newer".to_vec())
13529 );
13530 opened.verify().unwrap();
13531 }
13532
13533 #[test]
13534 fn index_entries_do_not_decrypt_payload_envelopes() {
13535 let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
13536 corrupt_payload_record(&mut opened.blocks, broken_payload_block);
13537
13538 let listed = opened.list_index_entries().unwrap();
13539 assert_eq!(listed.len(), 2);
13540 assert_eq!(listed[0].path, "broken.txt");
13541 assert_eq!(listed[0].file_data_size, b"broken payload\n".len() as u64);
13542 assert_eq!(listed[0].flags, crate::entry_metadata::EXTENDED_METADATA_V1);
13543 assert_eq!(listed[1].path, "healthy.txt");
13544 assert_eq!(listed[1].file_data_size, b"healthy payload\n".len() as u64);
13545 assert_eq!(listed[1].flags, crate::entry_metadata::EXTENDED_METADATA_V1);
13546 let looked_up = opened.lookup_index_entry("broken.txt").unwrap().unwrap();
13547 assert_eq!(looked_up.path, "broken.txt");
13548 assert_eq!(looked_up.file_data_size, b"broken payload\n".len() as u64);
13549 assert_eq!(looked_up.flags, crate::entry_metadata::EXTENDED_METADATA_V1);
13550 assert_eq!(opened.list_files().unwrap_err(), FormatError::AeadFailure);
13551 }
13552
13553 #[test]
13554 fn index_entry_layout_stats_match_frame_and_envelope_tables() {
13555 let payload = pseudo_random_bytes(12 * 1024);
13556 let archive = write_archive(
13557 &[RegularFile::new("chunked.bin", &payload)],
13558 &master_key(),
13559 WriterOptions {
13560 block_size: 4096,
13561 chunk_size: 1024,
13562 envelope_target_size: 2048,
13563 stripe_width: 1,
13564 volume_loss_tolerance: 0,
13565 bit_rot_buffer_pct: 0,
13566 fec_data_shards: 16,
13567 fec_parity_shards: 0,
13568 ..WriterOptions::default()
13569 },
13570 )
13571 .unwrap();
13572 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13573 let entry = opened.list_index_entries().unwrap().remove(0);
13574 let located = opened
13575 .locate_index_file(b"chunked.bin")
13576 .unwrap()
13577 .expect("indexed file exists");
13578 let file = &located.shard.files[located.file_index];
13579 let frames = frame_range_for_file(&located.shard, file).unwrap();
13580 assert!(frames.len() > 1);
13581
13582 let expected_compressed_size = frames
13583 .iter()
13584 .map(|frame| frame.compressed_size as u64)
13585 .sum::<u64>();
13586 let expected_decompressed_frame_size = frames
13587 .iter()
13588 .map(|frame| frame.decompressed_size as u64)
13589 .sum::<u64>();
13590 let envelope_indexes = frames
13591 .iter()
13592 .map(|frame| frame.envelope_index)
13593 .collect::<BTreeSet<_>>();
13594 assert!(envelope_indexes.len() > 1);
13595
13596 let envelopes = envelope_indexes
13597 .iter()
13598 .map(|index| envelope_by_index(&located.shard, *index).unwrap())
13599 .collect::<Vec<_>>();
13600 let expected_first_payload_block_index = envelopes
13601 .iter()
13602 .map(|envelope| envelope.first_block_index)
13603 .min();
13604 let expected_payload_data_block_count = envelopes
13605 .iter()
13606 .map(|envelope| envelope.data_block_count as u64)
13607 .sum::<u64>();
13608 let expected_payload_parity_block_count = envelopes
13609 .iter()
13610 .map(|envelope| envelope.parity_block_count as u64)
13611 .sum::<u64>();
13612 let expected_payload_encrypted_size = envelopes
13613 .iter()
13614 .map(|envelope| envelope.encrypted_size as u64)
13615 .sum::<u64>();
13616
13617 assert_eq!(entry.path, "chunked.bin");
13618 assert_eq!(entry.name, "chunked.bin");
13619 assert_eq!(entry.file_data_size, payload.len() as u64);
13620 assert_eq!(entry.flags, file.flags);
13621 assert_eq!(entry.tar_member_group_size, file.tar_member_group_size);
13622 assert_eq!(entry.first_frame_index, file.first_frame_index);
13623 assert_eq!(entry.frame_count, file.frame_count);
13624 assert_eq!(
13625 entry.offset_in_first_frame_plaintext,
13626 file.offset_in_first_frame_plaintext
13627 );
13628 assert_eq!(entry.layout.compressed_size, expected_compressed_size);
13629 assert_eq!(
13630 entry.layout.decompressed_frame_size,
13631 expected_decompressed_frame_size
13632 );
13633 assert_eq!(entry.layout.envelope_count as usize, envelope_indexes.len());
13634 assert_eq!(
13635 entry.layout.first_envelope_index,
13636 envelope_indexes.iter().next().copied()
13637 );
13638 assert_eq!(
13639 entry.layout.last_envelope_index,
13640 envelope_indexes.iter().next_back().copied()
13641 );
13642 assert_eq!(
13643 entry.layout.first_payload_block_index,
13644 expected_first_payload_block_index
13645 );
13646 assert_eq!(
13647 entry.layout.payload_data_block_count,
13648 expected_payload_data_block_count
13649 );
13650 assert_eq!(
13651 entry.layout.payload_parity_block_count,
13652 expected_payload_parity_block_count
13653 );
13654 assert_eq!(
13655 entry.layout.payload_encrypted_size,
13656 expected_payload_encrypted_size
13657 );
13658 }
13659
13660 #[test]
13661 fn extract_file_does_not_decrypt_unselected_payload_envelope() {
13662 let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
13665 corrupt_payload_record(&mut opened.blocks, broken_payload_block);
13666
13667 assert_eq!(
13668 opened.extract_file("healthy.txt").unwrap(),
13669 Some(b"healthy payload\n".to_vec())
13670 );
13671 assert_eq!(
13672 opened.extract_file("broken.txt").unwrap_err(),
13673 FormatError::AeadFailure
13674 );
13675 assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
13676 }
13677
13678 #[test]
13679 fn seekable_extract_does_not_read_unselected_payload_envelope() {
13680 let healthy = pseudo_random_bytes(64 * 1024);
13681 let broken = pseudo_random_bytes(64 * 1024);
13682 let options = WriterOptions {
13683 block_size: 4096,
13684 chunk_size: 4096,
13685 envelope_target_size: 8192,
13686 stripe_width: 1,
13687 volume_loss_tolerance: 0,
13688 bit_rot_buffer_pct: 0,
13689 fec_data_shards: 4,
13690 fec_parity_shards: 0,
13691 index_fec_data_shards: 4,
13692 index_fec_parity_shards: 0,
13693 index_root_fec_data_shards: 4,
13694 index_root_fec_parity_shards: 0,
13695 ..WriterOptions::default()
13696 };
13697 let archive = write_archive(
13698 &[
13699 RegularFile::new("healthy.bin", &healthy),
13700 RegularFile::new("broken.bin", &broken),
13701 ],
13702 &master_key(),
13703 options,
13704 )
13705 .unwrap();
13706 let eager = open_archive(&archive.bytes, &master_key()).unwrap();
13707 let healthy_envelopes = envelope_indices_for_path(&eager, "healthy.bin");
13708 let broken_envelopes = envelope_entries_for_path(&eager, "broken.bin");
13709 let denied_block_indices = broken_envelopes
13710 .iter()
13711 .filter(|envelope| !healthy_envelopes.contains(&envelope.envelope_index))
13712 .flat_map(|envelope| {
13713 let block_count =
13714 envelope.data_block_count as u64 + envelope.parity_block_count as u64;
13715 envelope.first_block_index..envelope.first_block_index + block_count
13716 })
13717 .collect::<BTreeSet<_>>();
13718 assert!(
13719 !denied_block_indices.is_empty(),
13720 "fixture must place broken.bin in at least one unshared envelope"
13721 );
13722 let denied_ranges = block_record_slots(&archive.bytes)
13723 .into_iter()
13724 .filter_map(|(offset, len, record)| {
13725 denied_block_indices
13726 .contains(&record.block_index)
13727 .then_some((offset as u64, (offset + len) as u64))
13728 })
13729 .collect::<Vec<_>>();
13730 assert!(!denied_ranges.is_empty());
13731
13732 let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
13733 let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
13734
13735 assert_eq!(opened.extract_file("healthy.bin").unwrap(), Some(healthy));
13736 for (read_start, read_end) in reader.reads() {
13737 assert!(
13738 denied_ranges
13739 .iter()
13740 .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
13741 "targeted extract read an unrelated payload BlockRecord range"
13742 );
13743 }
13744 assert_eq!(
13745 opened.extract_file("broken.bin").unwrap_err(),
13746 FormatError::InvalidArchive("denied test read")
13747 );
13748 }
13749
13750 #[test]
13751 fn extract_file_to_writer_streams_before_reading_later_envelopes() {
13752 struct FailOnFirstWrite;
13753
13754 impl std::io::Write for FailOnFirstWrite {
13755 fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
13756 Err(std::io::Error::other("sink stopped"))
13757 }
13758
13759 fn flush(&mut self) -> std::io::Result<()> {
13760 Ok(())
13761 }
13762 }
13763
13764 let payload = pseudo_random_bytes(128 * 1024);
13765 let options = WriterOptions {
13766 block_size: 4096,
13767 chunk_size: 4096,
13768 envelope_target_size: 8192,
13769 stripe_width: 1,
13770 volume_loss_tolerance: 0,
13771 bit_rot_buffer_pct: 0,
13772 fec_data_shards: 4,
13773 fec_parity_shards: 0,
13774 index_fec_data_shards: 4,
13775 index_fec_parity_shards: 0,
13776 index_root_fec_data_shards: 4,
13777 index_root_fec_parity_shards: 0,
13778 ..WriterOptions::default()
13779 };
13780 let archive = write_archive(
13781 &[RegularFile::new("large.bin", &payload)],
13782 &master_key(),
13783 options,
13784 )
13785 .unwrap();
13786 let eager = open_archive(&archive.bytes, &master_key()).unwrap();
13787 let envelopes = envelope_entries_for_path(&eager, "large.bin");
13788 let first_envelope = envelopes
13789 .first()
13790 .expect("large fixture should have at least one envelope")
13791 .envelope_index;
13792 let later_envelope_blocks = envelopes
13793 .iter()
13794 .filter(|entry| entry.envelope_index != first_envelope)
13795 .flat_map(|entry| {
13796 let block_count = entry.data_block_count as u64 + entry.parity_block_count as u64;
13797 entry.first_block_index..entry.first_block_index + block_count
13798 })
13799 .collect::<BTreeSet<_>>();
13800 assert!(
13801 !later_envelope_blocks.is_empty(),
13802 "fixture must span more than one payload envelope"
13803 );
13804 let denied_ranges = block_record_slots(&archive.bytes)
13805 .into_iter()
13806 .filter_map(|(offset, len, record)| {
13807 later_envelope_blocks
13808 .contains(&record.block_index)
13809 .then_some((offset as u64, (offset + len) as u64))
13810 })
13811 .collect::<Vec<_>>();
13812 assert!(!denied_ranges.is_empty());
13813
13814 let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
13815 let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
13816 let mut writer = FailOnFirstWrite;
13817
13818 let err = opened
13819 .extract_file_to_writer("large.bin", &mut writer)
13820 .unwrap_err();
13821 assert_eq!(err.to_string(), "extraction output write failed");
13822 for (read_start, read_end) in reader.reads() {
13823 assert!(
13824 denied_ranges
13825 .iter()
13826 .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
13827 "streaming writer read a later payload envelope before surfacing writer failure"
13828 );
13829 }
13830 }
13831
13832 #[test]
13833 fn extract_file_to_writer_writes_bounded_chunks() {
13834 struct ChunkRecorder {
13835 total: usize,
13836 max_write: usize,
13837 writes: usize,
13838 }
13839
13840 impl std::io::Write for ChunkRecorder {
13841 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
13842 self.total += buf.len();
13843 self.max_write = self.max_write.max(buf.len());
13844 self.writes += 1;
13845 Ok(buf.len())
13846 }
13847
13848 fn flush(&mut self) -> std::io::Result<()> {
13849 Ok(())
13850 }
13851 }
13852
13853 let payload = pseudo_random_bytes(128 * 1024);
13854 let options = WriterOptions {
13855 block_size: 4096,
13856 chunk_size: 4096,
13857 envelope_target_size: 8192,
13858 stripe_width: 1,
13859 volume_loss_tolerance: 0,
13860 bit_rot_buffer_pct: 0,
13861 fec_data_shards: 4,
13862 fec_parity_shards: 0,
13863 index_fec_data_shards: 4,
13864 index_fec_parity_shards: 0,
13865 index_root_fec_data_shards: 4,
13866 index_root_fec_parity_shards: 0,
13867 ..WriterOptions::default()
13868 };
13869 let archive = write_archive(
13870 &[RegularFile::new("large.bin", &payload)],
13871 &master_key(),
13872 options,
13873 )
13874 .unwrap();
13875 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13876 let mut writer = ChunkRecorder {
13877 total: 0,
13878 max_write: 0,
13879 writes: 0,
13880 };
13881
13882 opened
13883 .extract_file_to_writer("large.bin", &mut writer)
13884 .unwrap()
13885 .unwrap();
13886
13887 assert_eq!(writer.total, payload.len());
13888 assert!(writer.writes > 1);
13889 assert!(
13890 writer.max_write <= options.chunk_size as usize,
13891 "writer saw a {} byte chunk, larger than the {} byte frame target",
13892 writer.max_write,
13893 options.chunk_size
13894 );
13895 }
13896
13897 #[test]
13898 fn extract_file_to_writer_with_progress_reports_payload_bytes() {
13899 struct ChunkRecorder {
13900 total: usize,
13901 }
13902
13903 impl std::io::Write for ChunkRecorder {
13904 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
13905 self.total += buf.len();
13906 Ok(buf.len())
13907 }
13908
13909 fn flush(&mut self) -> std::io::Result<()> {
13910 Ok(())
13911 }
13912 }
13913
13914 let payload = pseudo_random_bytes(128 * 1024);
13915 let options = WriterOptions {
13916 block_size: 4096,
13917 chunk_size: 4096,
13918 envelope_target_size: 8192,
13919 stripe_width: 1,
13920 volume_loss_tolerance: 0,
13921 bit_rot_buffer_pct: 0,
13922 fec_data_shards: 4,
13923 fec_parity_shards: 0,
13924 index_fec_data_shards: 4,
13925 index_fec_parity_shards: 0,
13926 index_root_fec_data_shards: 4,
13927 index_root_fec_parity_shards: 0,
13928 ..WriterOptions::default()
13929 };
13930 let archive = write_archive(
13931 &[RegularFile::new("large.bin", &payload)],
13932 &master_key(),
13933 options,
13934 )
13935 .unwrap();
13936 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13937 let mut writer = ChunkRecorder { total: 0 };
13938 let mut progress_events = Vec::new();
13939 let mut progress = |archive_path: &str, bytes: u64| {
13940 progress_events.push((archive_path.to_owned(), bytes));
13941 };
13942
13943 opened
13944 .extract_file_to_writer_with_progress("large.bin", &mut writer, &mut progress)
13945 .unwrap()
13946 .unwrap();
13947
13948 let reported_bytes = progress_events.iter().map(|(_, bytes)| *bytes).sum::<u64>();
13949 assert_eq!(writer.total, payload.len());
13950 assert_eq!(reported_bytes, payload.len() as u64);
13951 assert!(progress_events.len() > 1);
13952 assert!(progress_events.iter().all(|(path, _)| path == "large.bin"));
13953 }
13954
13955 #[test]
13956 fn streaming_filesystem_extract_does_not_publish_partial_file_on_late_payload_error() {
13957 let payload = pseudo_random_bytes(128 * 1024);
13958 let options = WriterOptions {
13959 block_size: 4096,
13960 chunk_size: 4096,
13961 envelope_target_size: 8192,
13962 stripe_width: 1,
13963 volume_loss_tolerance: 0,
13964 bit_rot_buffer_pct: 0,
13965 fec_data_shards: 4,
13966 fec_parity_shards: 0,
13967 index_fec_data_shards: 4,
13968 index_fec_parity_shards: 0,
13969 index_root_fec_data_shards: 4,
13970 index_root_fec_parity_shards: 0,
13971 ..WriterOptions::default()
13972 };
13973 let archive = write_archive(
13974 &[RegularFile::new("large.bin", &payload)],
13975 &master_key(),
13976 options,
13977 )
13978 .unwrap();
13979 let eager = open_archive(&archive.bytes, &master_key()).unwrap();
13980 let envelopes = envelope_entries_for_path(&eager, "large.bin");
13981 let last_envelope = envelopes
13982 .last()
13983 .expect("large fixture should have at least one envelope");
13984 assert_ne!(
13985 envelopes.first().unwrap().envelope_index,
13986 last_envelope.envelope_index,
13987 "fixture must span more than one payload envelope"
13988 );
13989 let corrupt_slot = block_record_slots(&archive.bytes)
13990 .into_iter()
13991 .enumerate()
13992 .find_map(|(slot, (_, _, record))| {
13993 (record.block_index == last_envelope.first_block_index).then_some(slot)
13994 })
13995 .unwrap();
13996 let mut corrupted = archive.bytes;
13997 corrupt_block_record_payload_at_slot(&mut corrupted, corrupt_slot);
13998 let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
13999 let tmp = tempfile::tempdir().unwrap();
14000
14001 assert!(matches!(
14002 opened
14003 .extract_file_to("large.bin", tmp.path(), SafeExtractionOptions::default())
14004 .unwrap_err(),
14005 FormatError::AeadFailure | FormatError::FecTooFewAvailableShards
14006 ));
14007 assert!(!tmp.path().join("large.bin").exists());
14008 assert_eq!(std::fs::read_dir(tmp.path()).unwrap().count(), 0);
14009 }
14010
14011 #[test]
14012 fn bootstrap_sidecar_opens_lists_verifies_and_extracts() {
14013 let archive = write_archive(
14014 &[RegularFile::new("dir/sidecar.txt", b"hello sidecar")],
14015 &master_key(),
14016 single_stream_options(),
14017 )
14018 .unwrap();
14019 let opened = open_archive_with_bootstrap_sidecar(
14020 &archive.bytes,
14021 &archive.bootstrap_sidecar,
14022 &master_key(),
14023 )
14024 .unwrap();
14025
14026 assert_eq!(
14027 opened.list_files().unwrap(),
14028 vec![ArchiveEntry {
14029 path: "dir/sidecar.txt".to_string(),
14030 file_data_size: 13,
14031 kind: TarEntryKind::Regular,
14032 mode: 0o644,
14033 mtime: ArchiveTimestamp::UNIX_EPOCH,
14034 diagnostics: Vec::new(),
14035 }]
14036 );
14037 assert_eq!(
14038 opened.extract_file("dir/sidecar.txt").unwrap(),
14039 Some(b"hello sidecar".to_vec())
14040 );
14041 opened.verify().unwrap();
14042 }
14043
14044 #[test]
14045 fn fast_verify_plaintext_zero_recovery_defers_payload_semantics() {
14046 let options = WriterOptions {
14047 aead_algo: AeadAlgo::None,
14048 volume_loss_tolerance: 0,
14049 bit_rot_buffer_pct: 0,
14050 ..single_stream_options()
14051 };
14052 let archive = write_archive_unencrypted(
14053 &[RegularFile::new(
14054 "payload.txt",
14055 b"payload bytes large enough to produce a zstd frame",
14056 )],
14057 options,
14058 )
14059 .unwrap();
14060 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14061 let tables = opened.load_payload_index_tables().unwrap();
14062 let first_envelope = tables.envelopes.values().next().unwrap();
14063 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
14064 let crypto_end = VOLUME_HEADER_LEN + volume_header.crypto_header_length as usize;
14065 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
14066 let payload_offset = crypto_end + first_envelope.first_block_index as usize * record_len;
14067
14068 let mut tampered = archive.bytes.clone();
14069 tampered[payload_offset + 16] ^= 0x01;
14070 let crc_offset = payload_offset + 16 + opened.crypto_header.block_size as usize;
14071 let crc = crc32c::crc32c(&tampered[payload_offset..crc_offset]);
14072 tampered[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
14073
14074 let tampered_opened = open_archive(&tampered, &master_key()).unwrap();
14075 assert!(tampered_opened.fast_verify_defers_payload_semantics());
14076 tampered_opened.verify_content_fast().unwrap();
14077 assert!(tampered_opened.verify_content().is_err());
14078 }
14079
14080 #[test]
14081 fn fast_verify_root_auth_archive_requires_full_root_auth_scan() {
14082 let archive = write_archive_with_root_auth(
14083 &[RegularFile::new("signed.txt", b"root-auth payload")],
14084 &master_key(),
14085 single_stream_options(),
14086 RootAuthWriterConfig {
14087 authenticator_id: 0x7777,
14088 signer_identity_type: 1,
14089 signer_identity: b"test signer",
14090 authenticator_value_length: 32,
14091 },
14092 |request| Ok(request.archive_root.to_vec()),
14093 )
14094 .unwrap();
14095
14096 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14097 assert!(!opened.fast_verify_defers_payload_semantics());
14098 assert!(matches!(
14099 opened.verify_content_fast().unwrap().mode,
14100 ContentVerificationMode::Fast
14101 ));
14102 }
14103
14104 #[test]
14105 fn fast_verify_dictionary_archive_does_not_defer_payload_semantics() {
14106 let archive = write_archive_with_dictionary(
14107 &[RegularFile::new("dict.txt", b"dictionary payload")],
14108 &master_key(),
14109 single_stream_options(),
14110 dictionary(),
14111 )
14112 .unwrap();
14113
14114 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14115 assert!(!opened.fast_verify_defers_payload_semantics());
14116 }
14117
14118 #[test]
14119 fn fast_verify_encrypted_archive_does_not_defer_payload_semantics() {
14120 let options = WriterOptions {
14121 aead_algo: AeadAlgo::AesGcmSiv256,
14122 volume_loss_tolerance: 0,
14123 bit_rot_buffer_pct: 0,
14124 ..single_stream_options()
14125 };
14126 let archive = write_archive(
14127 &[RegularFile::new("payload.txt", b"encrypted payload")],
14128 &master_key(),
14129 options,
14130 )
14131 .unwrap();
14132
14133 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14134 assert!(!opened.fast_verify_defers_payload_semantics());
14135 }
14136
14137 #[test]
14138 fn fast_verify_repair_archive_does_not_defer_payload_semantics() {
14139 let options = WriterOptions {
14140 fec_parity_shards: 2,
14141 volume_loss_tolerance: 0,
14142 bit_rot_buffer_pct: 0,
14143 ..single_stream_options()
14144 };
14145 let archive = write_archive(
14146 &[RegularFile::new("payload.txt", b"payload for repair")],
14147 &master_key(),
14148 options,
14149 )
14150 .unwrap();
14151
14152 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14153 assert!(!opened.fast_verify_defers_payload_semantics());
14154 }
14155
14156 #[test]
14157 fn dictionary_archive_opens_lists_verifies_and_extracts_seekable() {
14158 let archive = write_archive_with_dictionary(
14159 &[RegularFile::new(
14160 "dir/dict.txt",
14161 b"common words common words dictionary payload",
14162 )],
14163 &master_key(),
14164 single_stream_options(),
14165 dictionary(),
14166 )
14167 .unwrap();
14168 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14169
14170 assert_eq!(opened.crypto_header.has_dictionary, 1);
14171 assert!(opened.index_root.header.dictionary_data_block_count > 0);
14172 assert_eq!(
14173 opened.list_files().unwrap(),
14174 vec![ArchiveEntry {
14175 path: "dir/dict.txt".to_string(),
14176 file_data_size: 44,
14177 kind: TarEntryKind::Regular,
14178 mode: 0o644,
14179 mtime: ArchiveTimestamp::UNIX_EPOCH,
14180 diagnostics: Vec::new(),
14181 }]
14182 );
14183 assert_eq!(
14184 opened.extract_file("dir/dict.txt").unwrap(),
14185 Some(b"common words common words dictionary payload".to_vec())
14186 );
14187 opened.verify().unwrap();
14188 }
14189
14190 #[test]
14191 fn dictionary_object_tamper_fails_before_payload_decompression() {
14192 let archive = write_archive_with_dictionary(
14193 &[RegularFile::new(
14194 "dir/dict.txt",
14195 b"common words common words dictionary payload",
14196 )],
14197 &master_key(),
14198 single_stream_options(),
14199 dictionary(),
14200 )
14201 .unwrap();
14202 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14203 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
14204 let crypto_end = VOLUME_HEADER_LEN + volume_header.crypto_header_length as usize;
14205 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
14206 let dictionary_offset =
14207 crypto_end + opened.index_root.header.dictionary_first_block as usize * record_len;
14208
14209 let mut tampered = archive.bytes.clone();
14210 tampered[dictionary_offset + 16] ^= 0x01;
14211 let crc_offset = dictionary_offset + 16 + opened.crypto_header.block_size as usize;
14212 let crc = crc32c::crc32c(&tampered[dictionary_offset..crc_offset]);
14213 tampered[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
14214
14215 assert_eq!(
14216 open_archive(&tampered, &master_key()).unwrap_err(),
14217 FormatError::AeadFailure
14218 );
14219 }
14220
14221 #[test]
14222 fn dictionary_archive_bootstraps_from_sidecar_for_non_seekable_open() {
14223 let archive = write_archive_with_dictionary(
14224 &[RegularFile::new(
14225 "dict-sidecar.txt",
14226 b"common words common words sidecar payload",
14227 )],
14228 &master_key(),
14229 single_stream_options(),
14230 dictionary(),
14231 )
14232 .unwrap();
14233 let opened = open_non_seekable_archive(
14234 &archive.bytes,
14235 &master_key(),
14236 Some(&archive.bootstrap_sidecar),
14237 )
14238 .unwrap();
14239
14240 assert_eq!(
14241 opened.extract_file("dict-sidecar.txt").unwrap(),
14242 Some(b"common words common words sidecar payload".to_vec())
14243 );
14244 opened.verify().unwrap();
14245 }
14246
14247 #[test]
14248 fn non_seekable_full_sidecar_bootstraps_when_terminal_trailer_is_corrupt() {
14249 let archive = write_archive(
14250 &[RegularFile::new(
14251 "sidecar-terminal.txt",
14252 b"sidecar authority",
14253 )],
14254 &master_key(),
14255 single_stream_options(),
14256 )
14257 .unwrap();
14258 let mut corrupted = archive.bytes.clone();
14259 corrupt_v41_terminal_recovery(&mut corrupted);
14260 assert!(open_archive(&corrupted, &master_key()).is_err());
14261
14262 let opened =
14263 open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
14264 .unwrap();
14265
14266 assert!(opened.volume_trailer.is_none());
14267 assert_eq!(
14268 opened.extract_file("sidecar-terminal.txt").unwrap(),
14269 Some(b"sidecar authority".to_vec())
14270 );
14271 opened.verify().unwrap();
14272 }
14273
14274 #[test]
14275 fn dictionary_full_sidecar_bootstraps_when_terminal_material_is_absent() {
14276 let archive = write_archive_with_dictionary(
14277 &[RegularFile::new(
14278 "dict-no-terminal.txt",
14279 b"common words common words without terminal",
14280 )],
14281 &master_key(),
14282 single_stream_options(),
14283 dictionary(),
14284 )
14285 .unwrap();
14286 let terminal_offset = terminal_material_offset(&archive.bytes);
14287 let truncated = archive.bytes[..terminal_offset].to_vec();
14288 assert!(open_archive(&truncated, &master_key()).is_err());
14289
14290 let opened =
14291 open_non_seekable_archive(&truncated, &master_key(), Some(&archive.bootstrap_sidecar))
14292 .unwrap();
14293
14294 assert!(opened.volume_trailer.is_none());
14295 assert_eq!(
14296 opened.extract_file("dict-no-terminal.txt").unwrap(),
14297 Some(b"common words common words without terminal".to_vec())
14298 );
14299 opened.verify().unwrap();
14300 }
14301
14302 #[test]
14303 fn bootstrap_sidecar_treats_crc_failed_payload_block_as_erasure() {
14304 let archive = write_archive(
14305 &[RegularFile::new(
14306 "sidecar-erasure.txt",
14307 b"repair through sidecar",
14308 )],
14309 &master_key(),
14310 single_stream_options(),
14311 )
14312 .unwrap();
14313 let mut corrupted = archive.bytes.clone();
14314 corrupt_first_block_record_payload(&mut corrupted);
14315
14316 let opened = open_archive_with_bootstrap_sidecar(
14317 &corrupted,
14318 &archive.bootstrap_sidecar,
14319 &master_key(),
14320 )
14321 .unwrap();
14322 assert_eq!(
14323 opened.extract_file("sidecar-erasure.txt").unwrap(),
14324 Some(b"repair through sidecar".to_vec())
14325 );
14326 }
14327
14328 #[test]
14329 fn extraction_rejects_logical_payload_above_total_size_cap() {
14330 let archive = write_archive(
14331 &[RegularFile::new("cap.txt", b"payload")],
14332 &master_key(),
14333 single_stream_options(),
14334 )
14335 .unwrap();
14336 let options = ReaderOptions {
14337 max_total_extraction_size: 3,
14338 ..ReaderOptions::default()
14339 };
14340 let opened =
14341 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
14342
14343 assert_eq!(
14344 opened.extract_file("cap.txt").unwrap_err(),
14345 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
14346 );
14347 }
14348
14349 #[test]
14350 fn verify_does_not_apply_extraction_payload_cap() {
14351 let archive = write_archive(
14352 &[RegularFile::new("verify-cap.txt", b"payload")],
14353 &master_key(),
14354 single_stream_options(),
14355 )
14356 .unwrap();
14357 let options = ReaderOptions {
14358 max_total_extraction_size: 3,
14359 ..ReaderOptions::default()
14360 };
14361 let opened =
14362 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
14363
14364 opened.verify().unwrap();
14365 assert_eq!(
14366 opened.extract_file("verify-cap.txt").unwrap_err(),
14367 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
14368 );
14369 }
14370
14371 #[test]
14372 fn verify_streams_past_legacy_in_memory_tar_cap() {
14373 let data = vec![0x5a; 4096];
14374 let archive = write_archive(
14375 &[RegularFile::new("verify-large.txt", &data)],
14376 &master_key(),
14377 single_stream_options(),
14378 )
14379 .unwrap();
14380 let options = ReaderOptions {
14381 max_verify_tar_size: 1,
14382 ..ReaderOptions::default()
14383 };
14384 let opened =
14385 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
14386
14387 opened.verify().unwrap();
14388 }
14389
14390 #[test]
14391 fn dictionary_sidecar_requires_dictionary_record_section() {
14392 let archive = write_archive_with_dictionary(
14393 &[RegularFile::new("dict-missing.txt", b"common words")],
14394 &master_key(),
14395 single_stream_options(),
14396 dictionary(),
14397 )
14398 .unwrap();
14399 let header = BootstrapSidecarHeader::parse(
14400 &archive.bootstrap_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN],
14401 )
14402 .unwrap();
14403 let mut missing_dictionary =
14404 archive.bootstrap_sidecar[..header.dictionary_records_offset as usize].to_vec();
14405 rewrite_sidecar_header(&mut missing_dictionary, &master_key(), |header| {
14406 header.flags &= !0x04;
14407 header.dictionary_records_offset = 0;
14408 header.dictionary_records_length = 0;
14409 });
14410
14411 assert_eq!(
14412 open_non_seekable_archive(&archive.bytes, &master_key(), Some(&missing_dictionary))
14413 .unwrap_err(),
14414 FormatError::ReaderUnsupported("dictionary bootstrap required")
14415 );
14416 }
14417
14418 #[test]
14419 fn dictionary_sidecar_records_are_validated_against_dictionary_extent() {
14420 let archive = write_archive_with_dictionary(
14421 &[RegularFile::new("dict-sidecar-kind.txt", b"common words")],
14422 &master_key(),
14423 single_stream_options(),
14424 dictionary(),
14425 )
14426 .unwrap();
14427
14428 let mut wrong_kind = archive.bootstrap_sidecar.clone();
14429 mutate_sidecar_dictionary_record(&mut wrong_kind, 0, |record| {
14430 record.kind = BlockKind::IndexRootData;
14431 });
14432 assert_eq!(
14433 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
14434 .unwrap_err(),
14435 FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
14436 );
14437
14438 let mut wrong_last = archive.bootstrap_sidecar.clone();
14439 mutate_sidecar_dictionary_record(&mut wrong_last, 0, |record| {
14440 record.flags = 0;
14441 });
14442 assert_eq!(
14443 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
14444 .unwrap_err(),
14445 FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
14446 );
14447 }
14448
14449 #[test]
14450 fn non_seekable_random_access_requires_sidecar() {
14451 let archive = write_archive(
14452 &[RegularFile::new("file.txt", b"payload")],
14453 &master_key(),
14454 single_stream_options(),
14455 )
14456 .unwrap();
14457
14458 assert_eq!(
14459 open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
14460 FormatError::ReaderUnsupported(
14461 "non-seekable random access requires a bootstrap sidecar"
14462 )
14463 );
14464 assert!(open_non_seekable_archive(
14465 &archive.bytes,
14466 &master_key(),
14467 Some(&archive.bootstrap_sidecar)
14468 )
14469 .is_ok());
14470 }
14471
14472 #[test]
14473 fn non_seekable_bootstrap_rejects_index_root_only_sidecar() {
14474 let archive = write_archive(
14475 &[RegularFile::new("sparse.txt", b"sparse sidecar")],
14476 &master_key(),
14477 single_stream_options(),
14478 )
14479 .unwrap();
14480 let index_root_only = sparse_bootstrap_sidecar(
14481 &archive.bootstrap_sidecar,
14482 &master_key(),
14483 false,
14484 true,
14485 false,
14486 );
14487
14488 assert_eq!(
14489 open_non_seekable_archive(&archive.bytes, &master_key(), Some(&index_root_only))
14490 .unwrap_err(),
14491 FormatError::ReaderUnsupported(
14492 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
14493 )
14494 );
14495 }
14496
14497 #[test]
14498 fn seekable_sidecar_uses_index_root_records_after_terminal_manifest_authority() {
14499 let archive = write_archive(
14500 &[RegularFile::new("sparse-index.txt", b"recover index root")],
14501 &master_key(),
14502 single_stream_options(),
14503 )
14504 .unwrap();
14505 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14506 let mut corrupted = archive.bytes.clone();
14507 corrupt_object_extent_records(
14508 &mut corrupted,
14509 index_root_extent_from_manifest(&opened.manifest_footer),
14510 );
14511 assert!(open_archive(&corrupted, &master_key()).is_err());
14512
14513 let index_root_only = sparse_bootstrap_sidecar(
14514 &archive.bootstrap_sidecar,
14515 &master_key(),
14516 false,
14517 true,
14518 false,
14519 );
14520 let recovered =
14521 open_archive_with_bootstrap_sidecar(&corrupted, &index_root_only, &master_key())
14522 .unwrap();
14523
14524 assert_eq!(
14525 recovered.extract_file("sparse-index.txt").unwrap(),
14526 Some(b"recover index root".to_vec())
14527 );
14528 recovered.verify().unwrap();
14529 }
14530
14531 #[test]
14532 fn seekable_sidecar_uses_dictionary_records_after_index_root_authority() {
14533 let archive = write_archive_with_dictionary(
14534 &[RegularFile::new(
14535 "sparse-dict.txt",
14536 b"common words common words sparse dictionary",
14537 )],
14538 &master_key(),
14539 single_stream_options(),
14540 dictionary(),
14541 )
14542 .unwrap();
14543 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14544 let mut corrupted = archive.bytes.clone();
14545 corrupt_object_extent_records(
14546 &mut corrupted,
14547 dictionary_extent_from_index_root(&opened.index_root).unwrap(),
14548 );
14549 assert!(open_archive(&corrupted, &master_key()).is_err());
14550
14551 let dictionary_only = sparse_bootstrap_sidecar(
14552 &archive.bootstrap_sidecar,
14553 &master_key(),
14554 false,
14555 false,
14556 true,
14557 );
14558 assert_eq!(
14559 open_non_seekable_archive(&archive.bytes, &master_key(), Some(&dictionary_only))
14560 .unwrap_err(),
14561 FormatError::ReaderUnsupported(
14562 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
14563 )
14564 );
14565
14566 let recovered =
14567 open_archive_with_bootstrap_sidecar(&corrupted, &dictionary_only, &master_key())
14568 .unwrap();
14569 assert_eq!(
14570 recovered.extract_file("sparse-dict.txt").unwrap(),
14571 Some(b"common words common words sparse dictionary".to_vec())
14572 );
14573 recovered.verify().unwrap();
14574 }
14575
14576 #[test]
14577 fn sequential_extracts_dictionary_free_tar_stream() {
14578 let archive = write_archive(
14579 &[RegularFile::new("seq.txt", b"streaming")],
14580 &master_key(),
14581 single_stream_options(),
14582 )
14583 .unwrap();
14584
14585 let tar_stream = sequential_extract_tar_stream(&archive.bytes, &master_key()).unwrap();
14586 let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
14587 assert_eq!(member.path, b"seq.txt");
14588 assert_eq!(member.data, b"streaming");
14589 }
14590
14591 #[test]
14592 fn sequential_rejects_logical_payload_above_total_size_cap() {
14593 let archive = write_archive(
14594 &[RegularFile::new("seq-cap.txt", b"payload")],
14595 &master_key(),
14596 single_stream_options(),
14597 )
14598 .unwrap();
14599 let options = ReaderOptions {
14600 max_total_extraction_size: 3,
14601 ..ReaderOptions::default()
14602 };
14603
14604 assert_eq!(
14605 sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
14606 .unwrap_err(),
14607 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
14608 );
14609 }
14610
14611 #[test]
14612 fn sequential_rejects_tar_stream_above_buffer_cap_during_decode() {
14613 let archive = write_archive(
14614 &[RegularFile::new("seq-buffer-cap.txt", b"payload")],
14615 &master_key(),
14616 single_stream_options(),
14617 )
14618 .unwrap();
14619 let options = ReaderOptions {
14620 max_verify_tar_size: 512,
14621 ..ReaderOptions::default()
14622 };
14623
14624 assert_eq!(
14625 sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
14626 .unwrap_err(),
14627 FormatError::ReaderUnsupported(
14628 "sequential tar stream exceeds configured verification cap"
14629 )
14630 );
14631 }
14632
14633 #[test]
14634 fn sequential_repairs_crc_failed_payload_data_when_parity_is_guaranteed() {
14635 let archive = write_archive(
14636 &[RegularFile::new("seq-erasure.txt", b"stream repair")],
14637 &master_key(),
14638 single_stream_options(),
14639 )
14640 .unwrap();
14641 let mut corrupted = archive.bytes;
14642 corrupt_first_block_record_payload(&mut corrupted);
14643
14644 let tar_stream = sequential_extract_tar_stream(&corrupted, &master_key()).unwrap();
14645 let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
14646 assert_eq!(member.path, b"seq-erasure.txt");
14647 assert_eq!(member.data, b"stream repair");
14648 }
14649
14650 #[test]
14651 fn sequential_rejects_crc_failed_payload_data_without_guaranteed_parity() {
14652 let archive = write_archive(
14653 &[RegularFile::new("seq-no-parity.txt", b"no repair")],
14654 &master_key(),
14655 WriterOptions {
14656 bit_rot_buffer_pct: 0,
14657 fec_parity_shards: 0,
14658 index_fec_parity_shards: 0,
14659 index_root_fec_parity_shards: 0,
14660 ..single_stream_options()
14661 },
14662 )
14663 .unwrap();
14664 let mut corrupted = archive.bytes;
14665 corrupt_first_block_record_payload(&mut corrupted);
14666
14667 assert_eq!(
14668 sequential_extract_tar_stream(&corrupted, &master_key()).unwrap_err(),
14669 FormatError::BadCrc {
14670 structure: "BlockRecord"
14671 }
14672 );
14673 }
14674
14675 #[test]
14676 fn sequential_rejects_when_terminal_authentication_fails_without_returning_bytes() {
14677 let archive = write_archive(
14678 &[RegularFile::new(
14679 "seq.txt",
14680 b"payload must not be returned after terminal auth failure",
14681 )],
14682 &master_key(),
14683 single_stream_options(),
14684 )
14685 .unwrap();
14686 let mut corrupted = archive.bytes;
14687 corrupt_v41_terminal_recovery(&mut corrupted);
14688
14689 match sequential_extract_tar_stream(&corrupted, &master_key()) {
14690 Ok(bytes) => panic!(
14691 "sequential helper returned {} decoded byte(s) despite terminal HMAC failure",
14692 bytes.len()
14693 ),
14694 Err(err) => assert_eq!(
14695 err,
14696 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
14697 ),
14698 }
14699 }
14700
14701 #[test]
14702 fn sequential_rejects_dictionary_archive_without_bootstrap_before_payload_release() {
14703 let archive = write_archive_with_dictionary(
14704 &[RegularFile::new(
14705 "seq-dict.txt",
14706 b"common words common words dictionary payload",
14707 )],
14708 &master_key(),
14709 single_stream_options(),
14710 b"common words dictionary",
14711 )
14712 .unwrap();
14713
14714 match sequential_extract_tar_stream(&archive.bytes, &master_key()) {
14715 Ok(bytes) => panic!(
14716 "sequential helper returned {} decoded byte(s) for dictionary archive without bootstrap",
14717 bytes.len()
14718 ),
14719 Err(err) => assert_eq!(
14720 err,
14721 FormatError::ReaderUnsupported(
14722 "dictionary bootstrap required for non-seekable sequential extraction"
14723 )
14724 ),
14725 }
14726 }
14727
14728 #[test]
14729 fn non_seekable_dictionary_error_keeps_missing_bootstrap_wording() {
14730 let archive = write_archive_with_dictionary(
14731 &[RegularFile::new(
14732 "seq-dict-open.txt",
14733 b"common words common words bootstrap required",
14734 )],
14735 &master_key(),
14736 single_stream_options(),
14737 b"common words bootstrap",
14738 )
14739 .unwrap();
14740
14741 assert_eq!(
14742 open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
14743 FormatError::ReaderUnsupported(
14744 "non-seekable random access requires a bootstrap sidecar"
14745 )
14746 );
14747 }
14748
14749 #[test]
14750 fn sequential_zstd_stream_rejects_skippable_frame_segments() {
14751 let skippable = [0x50, 0x2a, 0x4d, 0x18, 0, 0, 0, 0];
14752 let mut output = Vec::new();
14753
14754 assert_eq!(
14755 decode_concatenated_zstd_frames_with_cap(
14756 &skippable,
14757 None,
14758 &mut output,
14759 usize::MAX,
14760 None,
14761 )
14762 .unwrap_err(),
14763 FormatError::NotStandardZstdFrame
14764 );
14765 assert!(output.is_empty());
14766 }
14767
14768 #[test]
14769 fn live_non_seekable_verify_stream_accepts_single_volume_archive() {
14770 let archive = write_archive(
14771 &[RegularFile::new("live.txt", b"stream verify")],
14772 &master_key(),
14773 single_stream_options(),
14774 )
14775 .unwrap();
14776
14777 let report =
14778 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
14779
14780 assert_eq!(report.file_count, 1);
14781 assert_eq!(report.total_volumes, 1);
14782 assert_eq!(report.root_auth, SequentialRootAuthStatus::Absent);
14783 assert!(report.payload_block_count > 0);
14784 }
14785
14786 #[test]
14787 fn live_non_seekable_verify_stream_accepts_recipientwrap_archive() {
14788 let master = master_key();
14789 let archive = write_archive_with_recipient_wrap_records(
14790 &[RegularFile::new(
14791 "wrapped-live.txt",
14792 b"stream recipient verify",
14793 )],
14794 &master,
14795 single_stream_options(),
14796 vec![recipient_wrap_test_record()],
14797 )
14798 .unwrap();
14799
14800 let mut called = false;
14801 let report = verify_non_seekable_stream_with_recipient_wrap_resolver_options(
14802 std::io::Cursor::new(archive.bytes),
14803 |context| {
14804 called = true;
14805 assert_eq!(
14806 context.archive_identity.volume_format_rev,
14807 VOLUME_FORMAT_REV_45
14808 );
14809 assert_eq!(context.record.profile_id, 1);
14810 Ok(vec![master.0])
14811 },
14812 NonSeekableReaderOptions::default(),
14813 )
14814 .unwrap();
14815
14816 assert!(called);
14817 assert_eq!(report.file_count, 1);
14818 assert_eq!(report.total_volumes, 1);
14819 assert_eq!(report.root_auth, SequentialRootAuthStatus::Absent);
14820 }
14821
14822 #[test]
14823 fn live_non_seekable_recipientwrap_resolver_rejects_unencrypted_archive() {
14824 let archive = write_archive_unencrypted(
14825 &[RegularFile::new("plain-live.txt", b"plaintext payload")],
14826 single_stream_options(),
14827 )
14828 .unwrap();
14829
14830 let mut called = false;
14831 let err = verify_non_seekable_stream_with_recipient_wrap_resolver_options(
14832 std::io::Cursor::new(archive.bytes),
14833 |_| {
14834 called = true;
14835 Ok(vec![master_key().0])
14836 },
14837 NonSeekableReaderOptions::default(),
14838 )
14839 .unwrap_err();
14840
14841 assert!(!called);
14842 assert_eq!(err, FormatError::KeyMaterialMismatch);
14843 }
14844
14845 #[test]
14846 fn live_non_seekable_verify_stream_accepts_tiny_read_chunks() {
14847 let archive = write_archive(
14848 &[RegularFile::new("tiny-chunks.txt", b"one byte at a time")],
14849 &master_key(),
14850 single_stream_options(),
14851 )
14852 .unwrap();
14853
14854 let report =
14855 verify_non_seekable_stream(ChunkedReader::new(archive.bytes, 1), &master_key())
14856 .unwrap();
14857
14858 assert_eq!(report.file_count, 1);
14859 assert_eq!(report.tar_total_size % 512, 0);
14860 }
14861
14862 #[test]
14863 fn live_non_seekable_verify_stream_accepts_empty_archive() {
14864 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14865
14866 let report =
14867 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
14868
14869 assert_eq!(report.file_count, 0);
14870 assert_eq!(report.payload_block_count, 0);
14871 assert_eq!(report.tar_total_size, 0);
14872 }
14873
14874 #[test]
14875 fn live_non_seekable_verify_rejects_dictionary_archive_without_bootstrap() {
14876 let archive = write_archive_with_dictionary(
14877 &[RegularFile::new(
14878 "live-dict.txt",
14879 b"common words common words dictionary payload",
14880 )],
14881 &master_key(),
14882 single_stream_options(),
14883 b"common words dictionary",
14884 )
14885 .unwrap();
14886
14887 assert_eq!(
14888 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key())
14889 .unwrap_err(),
14890 FormatError::ReaderUnsupported(
14891 "dictionary bootstrap required for non-seekable sequential verification"
14892 )
14893 );
14894 }
14895
14896 #[test]
14897 fn live_non_seekable_verify_accepts_dictionary_archive_with_bootstrap() {
14898 let archive = write_archive_with_dictionary(
14899 &[RegularFile::new(
14900 "live-dict-sidecar.txt",
14901 b"common words common words dictionary payload",
14902 )],
14903 &master_key(),
14904 single_stream_options(),
14905 b"common words dictionary",
14906 )
14907 .unwrap();
14908
14909 let report = verify_non_seekable_stream_with_bootstrap_sidecar(
14910 std::io::Cursor::new(archive.bytes),
14911 &archive.bootstrap_sidecar,
14912 &master_key(),
14913 NonSeekableReaderOptions::default(),
14914 )
14915 .unwrap();
14916
14917 assert_eq!(report.file_count, 1);
14918 assert_eq!(report.total_volumes, 1);
14919 }
14920
14921 #[test]
14922 fn live_non_seekable_verify_rejects_terminal_tail_above_cap() {
14923 let archive = write_archive(
14924 &[RegularFile::new("tail-cap.txt", b"payload")],
14925 &master_key(),
14926 single_stream_options(),
14927 )
14928 .unwrap();
14929 let options = NonSeekableReaderOptions {
14930 max_terminal_tail_size: 8,
14931 ..NonSeekableReaderOptions::default()
14932 };
14933
14934 assert_eq!(
14935 verify_non_seekable_stream_with_options(
14936 std::io::Cursor::new(archive.bytes),
14937 &master_key(),
14938 options
14939 )
14940 .unwrap_err(),
14941 FormatError::ReaderUnsupported("terminal tail exceeds configured cap")
14942 );
14943 }
14944
14945 #[test]
14946 fn live_non_seekable_verify_rejects_metadata_above_retention_cap() {
14947 let archive = write_archive(
14948 &[RegularFile::new("metadata-cap.txt", b"payload")],
14949 &master_key(),
14950 single_stream_options(),
14951 )
14952 .unwrap();
14953 let options = NonSeekableReaderOptions {
14954 max_retained_metadata_bytes: 1,
14955 ..NonSeekableReaderOptions::default()
14956 };
14957
14958 assert_eq!(
14959 verify_non_seekable_stream_with_options(
14960 std::io::Cursor::new(archive.bytes),
14961 &master_key(),
14962 options
14963 )
14964 .unwrap_err(),
14965 FormatError::ReaderUnsupported("retained metadata exceeds configured streaming cap")
14966 );
14967 }
14968
14969 #[test]
14970 fn live_non_seekable_verify_repairs_crc_failed_metadata_block() {
14971 let archive = write_archive(
14972 &[RegularFile::new("metadata-erasure.txt", b"payload")],
14973 &master_key(),
14974 single_stream_options(),
14975 )
14976 .unwrap();
14977 let mut corrupted = archive.bytes;
14978 let slot = first_block_record_slot_with_kind(&corrupted, BlockKind::IndexRootData).unwrap();
14979 corrupt_block_record_payload_at_slot(&mut corrupted, slot);
14980
14981 let report =
14982 verify_non_seekable_stream(std::io::Cursor::new(corrupted), &master_key()).unwrap();
14983
14984 assert_eq!(report.file_count, 1);
14985 }
14986
14987 #[test]
14988 fn live_non_seekable_verify_rejects_member_count_above_cap() {
14989 let archive = write_archive(
14990 &[RegularFile::new("member-cap.txt", b"payload")],
14991 &master_key(),
14992 single_stream_options(),
14993 )
14994 .unwrap();
14995 let options = NonSeekableReaderOptions {
14996 max_streamed_member_count: 0,
14997 ..NonSeekableReaderOptions::default()
14998 };
14999
15000 assert_eq!(
15001 verify_non_seekable_stream_with_options(
15002 std::io::Cursor::new(archive.bytes),
15003 &master_key(),
15004 options
15005 )
15006 .unwrap_err(),
15007 FormatError::ReaderUnsupported("tar member count exceeds configured streaming cap")
15008 );
15009 }
15010
15011 #[test]
15012 fn live_non_seekable_verify_rejects_total_extraction_cap_during_decode() {
15013 let archive = write_archive(
15014 &[RegularFile::new("live-total-cap.txt", b"payload")],
15015 &master_key(),
15016 single_stream_options(),
15017 )
15018 .unwrap();
15019 let mut options = NonSeekableReaderOptions::default();
15020 options.reader.max_total_extraction_size = 3;
15021
15022 assert_eq!(
15023 verify_non_seekable_stream_with_options(
15024 std::io::Cursor::new(archive.bytes),
15025 &master_key(),
15026 options
15027 )
15028 .unwrap_err(),
15029 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
15030 );
15031 }
15032
15033 #[test]
15034 fn live_non_seekable_verify_reports_root_auth_wire_only() {
15035 let archive = write_archive_with_root_auth(
15036 &[RegularFile::new("signed-live.txt", b"root-auth stream")],
15037 &master_key(),
15038 single_stream_options(),
15039 test_root_auth_config(),
15040 |request| Ok(test_root_auth_value(request)),
15041 )
15042 .unwrap();
15043
15044 let report =
15045 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
15046
15047 assert_eq!(report.root_auth, SequentialRootAuthStatus::WireValidOnly);
15048 }
15049
15050 #[test]
15051 fn live_non_seekable_extract_stream_commits_after_terminal_verify() {
15052 let archive = write_archive(
15053 &[
15054 RegularFile::new("alpha.txt", b"alpha"),
15055 RegularFile::new("nested/beta.txt", b"beta"),
15056 ],
15057 &master_key(),
15058 single_stream_options(),
15059 )
15060 .unwrap();
15061 let tmp = tempfile::tempdir().unwrap();
15062 let out = tmp.path().join("out");
15063
15064 let report = extract_non_seekable_stream_to_dir(
15065 std::io::Cursor::new(archive.bytes),
15066 &master_key(),
15067 &out,
15068 NonSeekableReaderOptions::default(),
15069 SafeExtractionOptions::default(),
15070 )
15071 .unwrap();
15072
15073 assert_eq!(report.verification.file_count, 2);
15074 assert_eq!(report.extracted_member_count, 2);
15075 assert_eq!(fs::read(out.join("alpha.txt")).unwrap(), b"alpha");
15076 assert_eq!(fs::read(out.join("nested/beta.txt")).unwrap(), b"beta");
15077 }
15078
15079 #[test]
15080 fn live_non_seekable_extract_stream_accepts_tiny_read_chunks() {
15081 let archive = write_archive(
15082 &[RegularFile::new("tiny-extract.txt", b"chunked extraction")],
15083 &master_key(),
15084 single_stream_options(),
15085 )
15086 .unwrap();
15087 let tmp = tempfile::tempdir().unwrap();
15088 let out = tmp.path().join("out");
15089
15090 extract_non_seekable_stream_to_dir(
15091 ChunkedReader::new(archive.bytes, 1),
15092 &master_key(),
15093 &out,
15094 NonSeekableReaderOptions::default(),
15095 SafeExtractionOptions::default(),
15096 )
15097 .unwrap();
15098
15099 assert_eq!(
15100 fs::read(out.join("tiny-extract.txt")).unwrap(),
15101 b"chunked extraction"
15102 );
15103 }
15104
15105 #[test]
15106 fn live_non_seekable_extract_stream_terminal_failure_leaves_no_final_output() {
15107 let archive = write_archive(
15108 &[RegularFile::new("late-fail.txt", b"must remain staged")],
15109 &master_key(),
15110 single_stream_options(),
15111 )
15112 .unwrap();
15113 let mut corrupted = archive.bytes;
15114 corrupt_v41_terminal_recovery(&mut corrupted);
15115 let tmp = tempfile::tempdir().unwrap();
15116 let out = tmp.path().join("out");
15117
15118 match extract_non_seekable_stream_to_dir(
15119 std::io::Cursor::new(corrupted),
15120 &master_key(),
15121 &out,
15122 NonSeekableReaderOptions::default(),
15123 SafeExtractionOptions::default(),
15124 )
15125 .unwrap_err()
15126 {
15127 ExtractError::Format(err) => assert_eq!(
15128 err,
15129 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
15130 ),
15131 ExtractError::Output(err) => panic!("unexpected output error: {err}"),
15132 }
15133 assert!(!out.exists());
15134 }
15135
15136 #[test]
15137 fn live_non_seekable_extract_stream_existing_destination_obeys_overwrite_policy() {
15138 let archive = write_archive(
15139 &[RegularFile::new("same.txt", b"new")],
15140 &master_key(),
15141 single_stream_options(),
15142 )
15143 .unwrap();
15144 let tmp = tempfile::tempdir().unwrap();
15145 let out = tmp.path().join("out");
15146 fs::create_dir(&out).unwrap();
15147 fs::write(out.join("same.txt"), b"old").unwrap();
15148
15149 match extract_non_seekable_stream_to_dir(
15150 std::io::Cursor::new(archive.bytes.clone()),
15151 &master_key(),
15152 &out,
15153 NonSeekableReaderOptions::default(),
15154 SafeExtractionOptions::default(),
15155 )
15156 .unwrap_err()
15157 {
15158 ExtractError::Format(err) => assert_eq!(err, FormatError::UnsafeOverwrite),
15159 ExtractError::Output(err) => panic!("unexpected output error: {err}"),
15160 }
15161 assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"old");
15162
15163 extract_non_seekable_stream_to_dir(
15164 std::io::Cursor::new(archive.bytes),
15165 &master_key(),
15166 &out,
15167 NonSeekableReaderOptions::default(),
15168 SafeExtractionOptions {
15169 overwrite_existing: true,
15170 ..SafeExtractionOptions::default()
15171 },
15172 )
15173 .unwrap();
15174 assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"new");
15175 }
15176
15177 #[test]
15178 fn live_non_seekable_list_stream_matches_seekable_final_view() {
15179 let archive = write_archive(
15180 &[
15181 RegularFile::new("a.txt", b"a"),
15182 RegularFile::new("b.txt", b"bb"),
15183 ],
15184 &master_key(),
15185 single_stream_options(),
15186 )
15187 .unwrap();
15188 let seekable = open_archive(&archive.bytes, &master_key()).unwrap();
15189 let expected = seekable.list_files().unwrap();
15190
15191 let report = list_non_seekable_stream(
15192 std::io::Cursor::new(archive.bytes),
15193 &master_key(),
15194 NonSeekableReaderOptions::default(),
15195 )
15196 .unwrap();
15197
15198 assert_eq!(report.verification.file_count, 2);
15199 assert_eq!(report.entries, expected);
15200 }
15201
15202 #[test]
15203 fn bootstrap_sidecar_rejects_bad_flags_and_trailing_bytes() {
15204 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
15205 let mut bad_flags = archive.bootstrap_sidecar.clone();
15206 rewrite_sidecar_header(&mut bad_flags, &master_key(), |header| {
15207 header.flags |= 0x08;
15208 });
15209 assert_eq!(
15210 open_archive_with_bootstrap_sidecar(&archive.bytes, &bad_flags, &master_key())
15211 .unwrap_err(),
15212 FormatError::UnknownBootstrapSidecarFlags(0x0b)
15213 );
15214
15215 let mut trailing = archive.bootstrap_sidecar.clone();
15216 trailing.push(0);
15217 assert_eq!(
15218 open_archive_with_bootstrap_sidecar(&archive.bytes, &trailing, &master_key())
15219 .unwrap_err(),
15220 FormatError::NonCanonicalBootstrapSidecarLayout
15221 );
15222 }
15223
15224 #[test]
15225 fn bootstrap_sidecar_rejects_bad_manifest_footer_semantics() {
15226 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
15227 let mut wrong_volume = archive.bootstrap_sidecar.clone();
15228 mutate_sidecar_manifest(&mut wrong_volume, &master_key(), |footer| {
15229 footer.volume_index = 1;
15230 });
15231 assert_eq!(
15232 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_volume, &master_key())
15233 .unwrap_err(),
15234 FormatError::InvalidArchive("sidecar ManifestFooter volume_index must be zero")
15235 );
15236
15237 let mut non_authoritative = archive.bootstrap_sidecar.clone();
15238 mutate_sidecar_manifest(&mut non_authoritative, &master_key(), |footer| {
15239 footer.is_authoritative = 0;
15240 });
15241 assert_eq!(
15242 open_archive_with_bootstrap_sidecar(&archive.bytes, &non_authoritative, &master_key())
15243 .unwrap_err(),
15244 FormatError::InvalidArchive("sidecar ManifestFooter is not authoritative")
15245 );
15246 }
15247
15248 #[test]
15249 fn sidecar_manifest_validation_does_not_compare_opened_volume_index() {
15250 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
15251 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
15252 let crypto_start = volume_header.crypto_header_offset as usize;
15253 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
15254 let crypto_header = CryptoHeader::parse(
15255 &archive.bytes[crypto_start..crypto_end],
15256 volume_header.crypto_header_length,
15257 )
15258 .unwrap();
15259 let subkeys = Subkeys::derive(
15260 &master_key(),
15261 &volume_header.archive_uuid,
15262 &volume_header.session_id,
15263 )
15264 .unwrap();
15265 let mut opened_header = volume_header;
15266 opened_header.volume_index = 1;
15267
15268 let parsed = parse_bootstrap_sidecar(
15269 &archive.bootstrap_sidecar,
15270 &opened_header,
15271 &crypto_header.fixed,
15272 &subkeys,
15273 )
15274 .unwrap();
15275
15276 assert_eq!(parsed.manifest_footer.unwrap().volume_index, 0);
15277 }
15278
15279 #[test]
15280 fn bootstrap_sidecar_rejects_conflicting_manifest_bootstrap_fields() {
15281 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
15282 let mut conflicting = archive.bootstrap_sidecar.clone();
15283 mutate_sidecar_manifest(&mut conflicting, &master_key(), |footer| {
15284 footer.index_root_first_block += 1;
15285 });
15286
15287 assert_eq!(
15288 open_archive_with_bootstrap_sidecar(&archive.bytes, &conflicting, &master_key())
15289 .unwrap_err(),
15290 FormatError::InvalidArchive("bootstrap sidecar conflicts with terminal ManifestFooter")
15291 );
15292 }
15293
15294 #[test]
15295 fn sidecar_size_cap_counts_only_present_sparse_sections() {
15296 let mut crypto_header = test_crypto_header();
15297 crypto_header.has_dictionary = 1;
15298 crypto_header.index_root_fec_data_shards = 1;
15299 crypto_header.index_root_fec_parity_shards = 0;
15300 let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
15301 let header = BootstrapSidecarHeader {
15302 archive_uuid: [0x31; 16],
15303 session_id: [0x42; 16],
15304 flags: 0x04,
15305 manifest_footer_offset: 0,
15306 manifest_footer_length: 0,
15307 index_root_records_offset: 0,
15308 index_root_records_length: 0,
15309 dictionary_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
15310 dictionary_records_length: record_len,
15311 sidecar_hmac: [0u8; 32],
15312 header_crc32c: 0,
15313 };
15314
15315 validate_sidecar_size_cap(
15316 &header,
15317 &crypto_header,
15318 BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len,
15319 )
15320 .unwrap();
15321 assert_eq!(
15322 validate_sidecar_size_cap(
15323 &header,
15324 &crypto_header,
15325 BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len + 1,
15326 )
15327 .unwrap_err(),
15328 FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
15329 );
15330 }
15331
15332 #[test]
15333 fn sidecar_size_cap_rejects_sparse_section_above_class_max() {
15334 let mut crypto_header = test_crypto_header();
15335 crypto_header.index_root_fec_data_shards = 1;
15336 crypto_header.index_root_fec_parity_shards = 0;
15337 let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
15338 let header = BootstrapSidecarHeader {
15339 archive_uuid: [0x31; 16],
15340 session_id: [0x42; 16],
15341 flags: 0x02,
15342 manifest_footer_offset: 0,
15343 manifest_footer_length: 0,
15344 index_root_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
15345 index_root_records_length: record_len * 2,
15346 dictionary_records_offset: 0,
15347 dictionary_records_length: 0,
15348 sidecar_hmac: [0u8; 32],
15349 header_crc32c: 0,
15350 };
15351
15352 assert_eq!(
15353 validate_sidecar_size_cap(
15354 &header,
15355 &crypto_header,
15356 BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len * 2,
15357 )
15358 .unwrap_err(),
15359 FormatError::InvalidArchive("bootstrap sidecar IndexRoot records exceed resource cap")
15360 );
15361 }
15362
15363 #[test]
15364 fn sidecar_size_cap_uses_wide_arithmetic_for_large_record_classes() {
15365 let mut crypto_header = test_crypto_header();
15366 crypto_header.block_size = u32::MAX;
15367 crypto_header.index_root_fec_data_shards = u16::MAX;
15368 crypto_header.index_root_fec_parity_shards = u16::MAX;
15369 let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
15370 let max_records = crypto_header.index_root_fec_data_shards as u64
15371 + crypto_header.index_root_fec_parity_shards as u64;
15372 let max_section_len = max_records * record_len;
15373 let cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64
15374 + MANIFEST_FOOTER_LEN as u64
15375 + max_section_len
15376 + max_section_len;
15377 let header = BootstrapSidecarHeader {
15378 archive_uuid: [0x31; 16],
15379 session_id: [0x42; 16],
15380 flags: 0x01 | 0x02 | 0x04,
15381 manifest_footer_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
15382 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
15383 index_root_records_offset: 0,
15384 index_root_records_length: max_section_len,
15385 dictionary_records_offset: 0,
15386 dictionary_records_length: max_section_len,
15387 sidecar_hmac: [0u8; 32],
15388 header_crc32c: 0,
15389 };
15390
15391 validate_sidecar_size_cap(&header, &crypto_header, cap).unwrap();
15392 assert_eq!(
15393 validate_sidecar_size_cap(&header, &crypto_header, cap + 1).unwrap_err(),
15394 FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
15395 );
15396 }
15397
15398 #[test]
15399 fn bootstrap_sidecar_rejects_dictionary_section_for_no_dictionary_archive() {
15400 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
15401 let mut with_dictionary = archive.bootstrap_sidecar.clone();
15402 let header =
15403 BootstrapSidecarHeader::parse(&with_dictionary[..BOOTSTRAP_SIDECAR_HEADER_LEN])
15404 .unwrap();
15405 let record_len = sidecar_record_len(&with_dictionary);
15406 let first_record = header.index_root_records_offset as usize;
15407 let copied_record = with_dictionary[first_record..first_record + record_len].to_vec();
15408 let dictionary_offset = with_dictionary.len() as u64;
15409 with_dictionary.extend_from_slice(&copied_record);
15410 rewrite_sidecar_header(&mut with_dictionary, &master_key(), |header| {
15411 header.flags |= 0x04;
15412 header.dictionary_records_offset = dictionary_offset;
15413 header.dictionary_records_length = record_len as u64;
15414 });
15415
15416 assert_eq!(
15417 open_archive_with_bootstrap_sidecar(&archive.bytes, &with_dictionary, &master_key())
15418 .unwrap_err(),
15419 FormatError::InvalidArchive(
15420 "bootstrap sidecar has dictionary records while has_dictionary is false"
15421 )
15422 );
15423 }
15424
15425 #[test]
15426 fn bootstrap_sidecar_rejects_missing_duplicate_wrong_kind_and_wrong_last_flag() {
15427 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
15428 let mut missing = archive.bootstrap_sidecar.clone();
15429 let record_len = sidecar_record_len(&missing);
15430 let new_len = missing.len() - record_len;
15431 missing.truncate(new_len);
15432 rewrite_sidecar_header(&mut missing, &master_key(), |header| {
15433 header.index_root_records_length -= record_len as u64;
15434 });
15435 assert_eq!(
15436 open_archive_with_bootstrap_sidecar(&archive.bytes, &missing, &master_key())
15437 .unwrap_err(),
15438 FormatError::InvalidArchive(
15439 "sidecar BlockRecord section does not match declared extent"
15440 )
15441 );
15442
15443 let mut duplicate = archive.bootstrap_sidecar.clone();
15444 mutate_sidecar_index_record(&mut duplicate, 1, |record| {
15445 record.block_index -= 1;
15446 });
15447 assert_eq!(
15448 open_archive_with_bootstrap_sidecar(&archive.bytes, &duplicate, &master_key())
15449 .unwrap_err(),
15450 FormatError::InvalidArchive(
15451 "sidecar BlockRecord section has missing or duplicate blocks"
15452 )
15453 );
15454
15455 let mut misordered = archive.bootstrap_sidecar.clone();
15456 swap_sidecar_index_records(&mut misordered, 0, 1);
15457 assert_eq!(
15458 open_archive_with_bootstrap_sidecar(&archive.bytes, &misordered, &master_key())
15459 .unwrap_err(),
15460 FormatError::InvalidArchive(
15461 "sidecar BlockRecord section has missing or duplicate blocks"
15462 )
15463 );
15464
15465 let mut wrong_kind = archive.bootstrap_sidecar.clone();
15466 mutate_sidecar_index_record(&mut wrong_kind, 0, |record| {
15467 record.kind = BlockKind::PayloadData;
15468 });
15469 assert_eq!(
15470 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
15471 .unwrap_err(),
15472 FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
15473 );
15474
15475 let mut wrong_last = archive.bootstrap_sidecar.clone();
15476 mutate_sidecar_index_record(&mut wrong_last, 0, |record| {
15477 record.flags = 0;
15478 });
15479 assert_eq!(
15480 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
15481 .unwrap_err(),
15482 FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
15483 );
15484 }
15485
15486 #[test]
15487 fn verify_helper_rejects_envelope_frame_coverage_gap() {
15488 let frames = BTreeMap::from([(
15489 0,
15490 FrameEntry {
15491 frame_index: 0,
15492 envelope_index: 0,
15493 offset_in_envelope: 0,
15494 compressed_size: 10,
15495 decompressed_size: 512,
15496 flags: 0,
15497 tar_stream_offset: 0,
15498 },
15499 )]);
15500 let envelopes = BTreeMap::from([(
15501 0,
15502 EnvelopeEntry {
15503 envelope_index: 0,
15504 first_block_index: 0,
15505 data_block_count: 1,
15506 parity_block_count: 1,
15507 encrypted_size: 4096,
15508 plaintext_size: 11,
15509 first_frame_index: 0,
15510 frame_count: 1,
15511 },
15512 )]);
15513
15514 assert_eq!(
15515 validate_envelope_frame_coverage(&frames, &envelopes).unwrap_err(),
15516 FormatError::InvalidArchive("EnvelopeEntry frame coverage has a gap or overlap")
15517 );
15518 }
15519
15520 #[test]
15521 fn verify_helper_rejects_file_extent_gaps_and_overlaps() {
15522 assert!(validate_file_extent_coverage_ranges(&[(512, 512), (0, 512)], 1024).is_ok());
15523 assert_eq!(
15524 validate_file_extent_coverage_ranges(&[(0, 512), (1024, 512)], 1536).unwrap_err(),
15525 FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
15526 );
15527 assert_eq!(
15528 validate_file_extent_coverage_ranges(&[(0, 1024), (512, 512)], 1024).unwrap_err(),
15529 FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
15530 );
15531 }
15532
15533 #[test]
15534 fn verify_rejects_authenticated_content_hash_mismatch() {
15535 let options = WriterOptions {
15536 index_root_fec_parity_shards: 0,
15537 ..single_stream_options()
15538 };
15539 let archive = write_archive(
15540 &[RegularFile::new("content-hash.txt", b"hash covered")],
15541 &master_key(),
15542 options,
15543 )
15544 .unwrap();
15545 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
15546
15547 let mut root = opened.index_root.clone();
15548 root.header.content_sha256 = [0xa5; 32];
15549 let root_plaintext = root.to_bytes();
15550 IndexRoot::parse(
15551 &root_plaintext,
15552 false,
15553 metadata_limits(&opened.crypto_header),
15554 )
15555 .unwrap();
15556 assert_eq!(
15557 root_plaintext.len() as u32,
15558 opened.manifest_footer.index_root_decompressed_size
15559 );
15560
15561 let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
15562 let mut next_block_index = opened.manifest_footer.index_root_first_block;
15563 let replacement = encrypt_test_object(
15564 &compressed_root,
15565 &opened.subkeys.index_root_key,
15566 &opened.subkeys.index_nonce_seed,
15567 b"idxroot",
15568 0,
15569 BlockKind::IndexRootData,
15570 &mut next_block_index,
15571 &opened.crypto_header,
15572 &opened.volume_header,
15573 );
15574 assert_eq!(
15575 replacement.extent.data_block_count,
15576 opened.manifest_footer.index_root_data_block_count
15577 );
15578 assert_eq!(
15579 replacement.extent.encrypted_size,
15580 opened.manifest_footer.index_root_encrypted_size
15581 );
15582
15583 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
15584 let crypto_end = volume_header.crypto_header_offset as usize
15585 + volume_header.crypto_header_length as usize;
15586 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
15587 let mut malformed = archive.bytes.clone();
15588 for record in replacement.records {
15589 let offset = crypto_end + record.block_index as usize * record_len;
15590 malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
15591 }
15592
15593 let reopened = open_archive(&malformed, &master_key()).unwrap();
15594 assert_eq!(
15595 reopened.verify().unwrap_err(),
15596 FormatError::InvalidArchive(
15597 "IndexRoot content_sha256 does not match decoded tar stream"
15598 )
15599 );
15600 }
15601
15602 #[test]
15603 fn verify_rejects_file_entry_tar_path_and_size_mismatches() {
15604 let (mut path_mismatch, _) = multi_envelope_reader_fixture();
15605 rewrite_as_single_healthy_file(&mut path_mismatch, |_file, path| {
15606 path[0] = b'x';
15607 });
15608 assert_eq!(
15609 path_mismatch.verify().unwrap_err(),
15610 FormatError::InvalidArchive("tar member path does not match FileEntry path")
15611 );
15612
15613 let (mut size_mismatch, _) = multi_envelope_reader_fixture();
15614 rewrite_as_single_healthy_file(&mut size_mismatch, |file, _path| {
15615 file.file_data_size += 1;
15616 });
15617 assert_eq!(
15618 size_mismatch.verify().unwrap_err(),
15619 FormatError::InvalidArchive("tar member size does not match FileEntry file_data_size")
15620 );
15621 }
15622
15623 #[test]
15624 fn verify_rejects_inconsistent_duplicate_local_frame_rows_across_shards() {
15625 let (mut opened, _) = multi_envelope_reader_fixture();
15626 let locating = opened.index_root.shards[0].clone();
15627 let mut duplicate = opened.load_index_shard(&locating).unwrap();
15628 duplicate.header.shard_index = 1;
15629 duplicate.frames[0].flags ^= 0x0000_0001;
15630 let duplicate_plaintext = duplicate.to_bytes();
15631 let mut next_block_index = opened
15632 .blocks
15633 .keys()
15634 .last()
15635 .copied()
15636 .map(|index| index + 1)
15637 .unwrap_or(0);
15638 let duplicate_object = encrypt_test_object(
15639 &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
15640 &opened.subkeys.index_shard_key,
15641 &opened.subkeys.index_nonce_seed,
15642 b"idxshard",
15643 1,
15644 BlockKind::IndexShardData,
15645 &mut next_block_index,
15646 &opened.crypto_header,
15647 &opened.volume_header,
15648 );
15649 insert_records(&mut opened.blocks, &duplicate_object.records);
15650 opened.index_root.shards.push(ShardEntry {
15651 shard_index: 1,
15652 first_block_index: duplicate_object.extent.first_block_index,
15653 data_block_count: duplicate_object.extent.data_block_count,
15654 parity_block_count: 0,
15655 encrypted_size: duplicate_object.extent.encrypted_size,
15656 decompressed_size: duplicate_plaintext.len() as u32,
15657 file_count: locating.file_count,
15658 first_path_hash: locating.first_path_hash,
15659 last_path_hash: locating.last_path_hash,
15660 });
15661 opened.index_root.header.file_count += locating.file_count as u64;
15662
15663 assert_eq!(
15664 opened.verify().unwrap_err(),
15665 FormatError::InvalidArchive("duplicate FrameEntry rows do not match")
15666 );
15667 }
15668
15669 #[test]
15670 fn verify_rejects_inconsistent_duplicate_local_envelope_rows_across_shards() {
15671 let (mut opened, _) = multi_envelope_reader_fixture();
15672 let locating = opened.index_root.shards[0].clone();
15673 let mut duplicate = opened.load_index_shard(&locating).unwrap();
15674 duplicate.header.shard_index = 1;
15675 duplicate.envelopes[0].first_block_index += 1;
15676 let duplicate_plaintext = duplicate.to_bytes();
15677 let mut next_block_index = opened
15678 .blocks
15679 .keys()
15680 .last()
15681 .copied()
15682 .map(|index| index + 1)
15683 .unwrap_or(0);
15684 let duplicate_object = encrypt_test_object(
15685 &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
15686 &opened.subkeys.index_shard_key,
15687 &opened.subkeys.index_nonce_seed,
15688 b"idxshard",
15689 1,
15690 BlockKind::IndexShardData,
15691 &mut next_block_index,
15692 &opened.crypto_header,
15693 &opened.volume_header,
15694 );
15695 insert_records(&mut opened.blocks, &duplicate_object.records);
15696 opened.index_root.shards.push(ShardEntry {
15697 shard_index: 1,
15698 first_block_index: duplicate_object.extent.first_block_index,
15699 data_block_count: duplicate_object.extent.data_block_count,
15700 parity_block_count: 0,
15701 encrypted_size: duplicate_object.extent.encrypted_size,
15702 decompressed_size: duplicate_plaintext.len() as u32,
15703 file_count: locating.file_count,
15704 first_path_hash: locating.first_path_hash,
15705 last_path_hash: locating.last_path_hash,
15706 });
15707 opened.index_root.header.file_count += locating.file_count as u64;
15708
15709 assert_eq!(
15710 opened.verify().unwrap_err(),
15711 FormatError::InvalidArchive("duplicate EnvelopeEntry rows do not match")
15712 );
15713 }
15714
15715 #[test]
15716 fn verify_rejects_non_contiguous_global_envelope_indexes() {
15717 let (mut opened, _) = multi_envelope_reader_fixture();
15718 replace_first_index_shard(&mut opened, |shard| {
15719 let frame = shard
15720 .frames
15721 .iter_mut()
15722 .find(|entry| entry.frame_index == 1)
15723 .unwrap();
15724 frame.envelope_index = 2;
15725
15726 let envelope = shard
15727 .envelopes
15728 .iter_mut()
15729 .find(|entry| entry.envelope_index == 1)
15730 .unwrap();
15731 envelope.envelope_index = 2;
15732 });
15733
15734 assert_eq!(
15735 opened.verify().unwrap_err(),
15736 FormatError::InvalidMetadata {
15737 structure: "EnvelopeEntry",
15738 reason: "global index coverage has a gap",
15739 }
15740 );
15741 }
15742
15743 #[test]
15744 fn verify_rejects_payload_object_extent_overlap() {
15745 let (mut opened, _) = multi_envelope_reader_fixture();
15746 replace_first_index_shard(&mut opened, |shard| {
15747 let first_block_index = shard.envelopes[0].first_block_index;
15748 shard.envelopes[1].first_block_index = first_block_index;
15749 });
15750
15751 assert_eq!(
15752 opened.verify().unwrap_err(),
15753 FormatError::InvalidArchive("encrypted object block ranges overlap")
15754 );
15755 }
15756
15757 #[test]
15758 fn verify_accepts_cross_shard_shared_envelope_frame_union() {
15759 let volume_header = test_volume_header();
15760 let crypto_header = test_crypto_header();
15761 let subkeys = Subkeys::derive(
15762 &master_key(),
15763 &volume_header.archive_uuid,
15764 &volume_header.session_id,
15765 )
15766 .unwrap();
15767 let mut next_block_index = 0u64;
15768 let mut blocks = BTreeMap::new();
15769
15770 let alpha = test_member(b"alpha.txt", b"alpha cross shard\n");
15771 let beta = test_member(b"beta.txt", b"beta cross shard\n");
15772 let tar_stream = [alpha.as_slice(), beta.as_slice()].concat();
15773 let frame0_plaintext = compress_zstd_frame(&alpha, 1).unwrap();
15774 let frame1_plaintext = compress_zstd_frame(&beta, 1).unwrap();
15775 let envelope_plaintext =
15776 [frame0_plaintext.as_slice(), frame1_plaintext.as_slice()].concat();
15777 let payload = encrypt_test_object(
15778 &envelope_plaintext,
15779 &subkeys.enc_key,
15780 &subkeys.nonce_seed,
15781 b"envelope",
15782 0,
15783 BlockKind::PayloadData,
15784 &mut next_block_index,
15785 &crypto_header,
15786 &volume_header,
15787 );
15788 insert_records(&mut blocks, &payload.records);
15789
15790 let envelope = EnvelopeEntry {
15791 envelope_index: 0,
15792 first_block_index: payload.extent.first_block_index,
15793 data_block_count: payload.extent.data_block_count,
15794 parity_block_count: 0,
15795 encrypted_size: payload.extent.encrypted_size,
15796 plaintext_size: envelope_plaintext.len() as u32,
15797 first_frame_index: 0,
15798 frame_count: 2,
15799 };
15800 let frame0 = FrameEntry {
15801 frame_index: 0,
15802 envelope_index: 0,
15803 offset_in_envelope: 0,
15804 compressed_size: frame0_plaintext.len() as u32,
15805 decompressed_size: alpha.len() as u32,
15806 flags: 0x0000_0003,
15807 tar_stream_offset: 0,
15808 };
15809 let frame1 = FrameEntry {
15810 frame_index: 1,
15811 envelope_index: 0,
15812 offset_in_envelope: frame0_plaintext.len() as u32,
15813 compressed_size: frame1_plaintext.len() as u32,
15814 decompressed_size: beta.len() as u32,
15815 flags: 0x0000_0003,
15816 tar_stream_offset: alpha.len() as u64,
15817 };
15818
15819 let (shard0_plaintext, first0, last0) = build_test_index_shard(
15820 &[TestFileMeta {
15821 path: b"alpha.txt".to_vec(),
15822 frame_index: 0,
15823 tar_stream_offset: 0,
15824 member_group_size: alpha.len() as u64,
15825 file_data_size: b"alpha cross shard\n".len() as u64,
15826 }],
15827 &[frame0],
15828 std::slice::from_ref(&envelope),
15829 );
15830 let (mut shard1_plaintext, first1, last1) = build_test_index_shard(
15831 &[TestFileMeta {
15832 path: b"beta.txt".to_vec(),
15833 frame_index: 1,
15834 tar_stream_offset: alpha.len() as u64,
15835 member_group_size: beta.len() as u64,
15836 file_data_size: b"beta cross shard\n".len() as u64,
15837 }],
15838 &[frame1],
15839 std::slice::from_ref(&envelope),
15840 );
15841 shard1_plaintext[8..16].copy_from_slice(&1u64.to_le_bytes());
15842
15843 let shard0 = encrypt_test_object(
15844 &compress_zstd_frame(&shard0_plaintext, 1).unwrap(),
15845 &subkeys.index_shard_key,
15846 &subkeys.index_nonce_seed,
15847 b"idxshard",
15848 0,
15849 BlockKind::IndexShardData,
15850 &mut next_block_index,
15851 &crypto_header,
15852 &volume_header,
15853 );
15854 let shard1 = encrypt_test_object(
15855 &compress_zstd_frame(&shard1_plaintext, 1).unwrap(),
15856 &subkeys.index_shard_key,
15857 &subkeys.index_nonce_seed,
15858 b"idxshard",
15859 1,
15860 BlockKind::IndexShardData,
15861 &mut next_block_index,
15862 &crypto_header,
15863 &volume_header,
15864 );
15865 insert_records(&mut blocks, &shard0.records);
15866 insert_records(&mut blocks, &shard1.records);
15867
15868 let index_root = IndexRoot {
15869 header: IndexRootHeader {
15870 frame_count: 2,
15871 envelope_count: 1,
15872 file_count: 2,
15873 payload_block_count: payload.extent.data_block_count as u64,
15874 tar_total_size: tar_stream.len() as u64,
15875 content_sha256: sha256_bytes(&tar_stream),
15876 ..IndexRootHeader::empty()
15877 },
15878 shards: vec![
15879 ShardEntry {
15880 shard_index: 0,
15881 first_block_index: shard0.extent.first_block_index,
15882 data_block_count: shard0.extent.data_block_count,
15883 parity_block_count: 0,
15884 encrypted_size: shard0.extent.encrypted_size,
15885 decompressed_size: shard0_plaintext.len() as u32,
15886 file_count: 1,
15887 first_path_hash: first0,
15888 last_path_hash: last0,
15889 },
15890 ShardEntry {
15891 shard_index: 1,
15892 first_block_index: shard1.extent.first_block_index,
15893 data_block_count: shard1.extent.data_block_count,
15894 parity_block_count: 0,
15895 encrypted_size: shard1.extent.encrypted_size,
15896 decompressed_size: shard1_plaintext.len() as u32,
15897 file_count: 1,
15898 first_path_hash: first1,
15899 last_path_hash: last1,
15900 },
15901 ],
15902 directory_hint_shards: Vec::new(),
15903 };
15904
15905 let index_root_plaintext = index_root.to_bytes();
15906 let index_root_object = encrypt_test_object(
15907 &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
15908 &subkeys.index_root_key,
15909 &subkeys.index_nonce_seed,
15910 b"idxroot",
15911 0,
15912 BlockKind::IndexRootData,
15913 &mut next_block_index,
15914 &crypto_header,
15915 &volume_header,
15916 );
15917 insert_records(&mut blocks, &index_root_object.records);
15918
15919 let archive_uuid = volume_header.archive_uuid;
15920 let session_id = volume_header.session_id;
15921 let opened = OpenedArchive {
15922 options: ReaderOptions::default(),
15923 observed_archive_bytes: 1_000_000,
15924 observed_volume_count: 1,
15925 subkeys,
15926 blocks,
15927 lazy_blocks: None,
15928 crypto_header_bytes: Vec::new(),
15929 volume_header,
15930 crypto_header,
15931 manifest_footer: ManifestFooter {
15932 archive_uuid,
15933 session_id,
15934 volume_index: 0,
15935 is_authoritative: 1,
15936 total_volumes: 1,
15937 index_root_first_block: index_root_object.extent.first_block_index,
15938 index_root_data_block_count: index_root_object.extent.data_block_count,
15939 index_root_parity_block_count: 0,
15940 index_root_encrypted_size: index_root_object.extent.encrypted_size,
15941 index_root_decompressed_size: index_root_plaintext.len() as u32,
15942 manifest_hmac: [0u8; 32],
15943 },
15944 volume_trailer: Some(VolumeTrailer {
15945 archive_uuid,
15946 session_id,
15947 volume_index: 0,
15948 block_count: next_block_index,
15949 bytes_written: 0,
15950 manifest_footer_offset: 0,
15951 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
15952 closed_at_ns: 0,
15953 root_auth_footer_offset: 0,
15954 root_auth_footer_length: 0,
15955 root_auth_flags: 0,
15956 trailer_hmac: [0u8; 32],
15957 }),
15958 root_auth_footer: None,
15959 index_root,
15960 payload_dictionary: None,
15961 };
15962
15963 opened.verify().unwrap();
15964 }
15965
15966 #[test]
15967 fn verify_rejects_authenticated_archive_missing_required_directory_hints() {
15968 let options = WriterOptions {
15969 index_root_fec_parity_shards: 0,
15970 ..single_stream_options()
15971 };
15972 let archive = write_archive(
15973 &[RegularFile::new("only.txt", b"only payload")],
15974 &master_key(),
15975 options,
15976 )
15977 .unwrap();
15978 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
15979 assert!(opened.index_root.directory_hint_shards.is_empty());
15980
15981 let mut root = opened.index_root.clone();
15982 root.header.file_count = DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1;
15983 root.shards[0].file_count = (DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1) as u32;
15984 let root_plaintext = root.to_bytes();
15985 IndexRoot::parse(
15986 &root_plaintext,
15987 false,
15988 metadata_limits(&opened.crypto_header),
15989 )
15990 .unwrap();
15991 assert_eq!(
15992 root_plaintext.len() as u32,
15993 opened.manifest_footer.index_root_decompressed_size
15994 );
15995
15996 let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
15997 let mut next_block_index = opened.manifest_footer.index_root_first_block;
15998 let replacement = encrypt_test_object(
15999 &compressed_root,
16000 &opened.subkeys.index_root_key,
16001 &opened.subkeys.index_nonce_seed,
16002 b"idxroot",
16003 0,
16004 BlockKind::IndexRootData,
16005 &mut next_block_index,
16006 &opened.crypto_header,
16007 &opened.volume_header,
16008 );
16009 assert_eq!(
16010 replacement.extent.first_block_index,
16011 opened.manifest_footer.index_root_first_block
16012 );
16013 assert_eq!(
16014 replacement.extent.data_block_count,
16015 opened.manifest_footer.index_root_data_block_count
16016 );
16017 assert_eq!(
16018 replacement.extent.encrypted_size,
16019 opened.manifest_footer.index_root_encrypted_size
16020 );
16021
16022 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
16023 let crypto_end = volume_header.crypto_header_offset as usize
16024 + volume_header.crypto_header_length as usize;
16025 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
16026 let mut malformed = archive.bytes.clone();
16027 for record in replacement.records {
16028 let offset = crypto_end + record.block_index as usize * record_len;
16029 malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
16030 }
16031
16032 let reopened = open_archive(&malformed, &master_key()).unwrap();
16033 assert_eq!(
16034 reopened.index_root.header.file_count,
16035 DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1
16036 );
16037 assert!(reopened.index_root.directory_hint_shards.is_empty());
16038
16039 assert_eq!(
16040 reopened.verify().unwrap_err(),
16041 FormatError::InvalidArchive("IndexRoot file_count requires directory hints")
16042 );
16043 }
16044
16045 #[test]
16046 fn expected_directory_hint_rows_include_ancestors_and_directory_entries() {
16047 let mut map = DirectoryHintMap::new();
16048 add_expected_directory_hint_rows(&mut map, 2, b"foo/bar/baz.txt", TarEntryKind::Regular);
16049 add_expected_directory_hint_rows(&mut map, 4, b"foo/bar", TarEntryKind::Directory);
16050
16051 assert_eq!(map.get(&Vec::new()), Some(&BTreeSet::from([2, 4])));
16052 assert_eq!(map.get(b"foo".as_slice()), Some(&BTreeSet::from([2, 4])));
16053 assert_eq!(
16054 map.get(b"foo/bar".as_slice()),
16055 Some(&BTreeSet::from([2, 4]))
16056 );
16057 assert!(!map.contains_key(b"foo/bar/baz.txt".as_slice()));
16058 assert!(!map.contains_key(b"foobar".as_slice()));
16059 }
16060
16061 #[test]
16062 fn directory_hint_validation_requires_exact_global_map() {
16063 let mut expected = DirectoryHintMap::new();
16064 add_expected_directory_hint_rows(&mut expected, 0, b"foo/bar.txt", TarEntryKind::Regular);
16065 add_expected_directory_hint_rows(&mut expected, 1, b"foo", TarEntryKind::Directory);
16066 let rows = sorted_directory_hint_rows(&expected);
16067 let table = directory_hint_table_from_rows(7, &rows, 2);
16068
16069 validate_directory_hint_tables_against_expected(std::slice::from_ref(&table), &expected)
16070 .unwrap();
16071
16072 let mut missing_root = expected.clone();
16073 missing_root.remove(&Vec::new());
16074 let missing_root_rows = sorted_directory_hint_rows(&missing_root);
16075 let missing_root_table = directory_hint_table_from_rows(8, &missing_root_rows, 2);
16076 assert_eq!(
16077 validate_directory_hint_tables_against_expected(&[missing_root_table], &expected)
16078 .unwrap_err(),
16079 FormatError::InvalidArchive("directory hint map does not match decoded files")
16080 );
16081
16082 let mut expected_missing_directory_entry = expected.clone();
16083 expected_missing_directory_entry
16084 .get_mut(b"foo".as_slice())
16085 .unwrap()
16086 .remove(&1);
16087 assert_eq!(
16088 validate_directory_hint_tables_against_expected(
16089 std::slice::from_ref(&table),
16090 &expected_missing_directory_entry,
16091 )
16092 .unwrap_err(),
16093 FormatError::InvalidArchive("directory hint map does not match decoded files")
16094 );
16095
16096 let mut extra = expected.clone();
16097 extra.insert(b"foo/extra".to_vec(), BTreeSet::from([0]));
16098 let extra_rows = sorted_directory_hint_rows(&extra);
16099 let extra_table = directory_hint_table_from_rows(9, &extra_rows, 2);
16100 assert_eq!(
16101 validate_directory_hint_tables_against_expected(&[extra_table], &expected).unwrap_err(),
16102 FormatError::InvalidArchive("directory hint map does not match decoded files")
16103 );
16104 }
16105
16106 #[test]
16107 fn directory_hint_validation_rejects_global_order_mismatch() {
16108 let mut expected = DirectoryHintMap::new();
16109 expected.insert(Vec::new(), BTreeSet::from([0]));
16110 expected.insert(b"alpha".to_vec(), BTreeSet::from([0]));
16111 let rows = sorted_directory_hint_rows(&expected);
16112 let first = directory_hint_table_from_rows(8, &rows[..1], 1);
16113 let second = directory_hint_table_from_rows(9, &rows[1..], 1);
16114
16115 assert_eq!(
16116 validate_directory_hint_tables_against_expected(&[second, first], &expected)
16117 .unwrap_err(),
16118 FormatError::InvalidArchive("DirectoryHintEntry rows are not globally sorted")
16119 );
16120 }
16121
16122 #[test]
16123 fn object_extent_rejects_parity_above_class_cap() {
16124 let crypto_header = CryptoHeaderFixed {
16125 length: 0,
16126 compression_algo: CompressionAlgo::ZstdFramed,
16127 aead_algo: AeadAlgo::AesGcmSiv256,
16128 fec_algo: FecAlgo::ReedSolomonGF16,
16129 kdf_algo: KdfAlgo::Raw,
16130 chunk_size: 1024,
16131 envelope_target_size: 4096,
16132 block_size: 4096,
16133 fec_data_shards: 1,
16134 fec_parity_shards: 1,
16135 index_fec_data_shards: 1,
16136 index_fec_parity_shards: 1,
16137 index_root_fec_data_shards: 1,
16138 index_root_fec_parity_shards: 1,
16139 stripe_width: 1,
16140 volume_loss_tolerance: 0,
16141 bit_rot_buffer_pct: 0,
16142 has_dictionary: 0,
16143 max_path_length: 4096,
16144 expected_volume_size: 0,
16145 };
16146 let extent = ObjectExtent {
16147 first_block_index: 0,
16148 data_block_count: 1,
16149 parity_block_count: 2,
16150 encrypted_size: 4096,
16151 };
16152
16153 assert_eq!(
16154 validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
16155 FormatError::InvalidArchive("encrypted object exceeds its class parity-shard maximum")
16156 );
16157 }
16158
16159 #[test]
16160 fn object_extent_rejects_parity_below_recoverability_requirement() {
16161 let crypto_header = CryptoHeaderFixed {
16162 length: 0,
16163 compression_algo: CompressionAlgo::ZstdFramed,
16164 aead_algo: AeadAlgo::AesGcmSiv256,
16165 fec_algo: FecAlgo::ReedSolomonGF16,
16166 kdf_algo: KdfAlgo::Raw,
16167 chunk_size: 1024,
16168 envelope_target_size: 4096,
16169 block_size: 4096,
16170 fec_data_shards: 1,
16171 fec_parity_shards: 1,
16172 index_fec_data_shards: 1,
16173 index_fec_parity_shards: 1,
16174 index_root_fec_data_shards: 1,
16175 index_root_fec_parity_shards: 1,
16176 stripe_width: 2,
16177 volume_loss_tolerance: 1,
16178 bit_rot_buffer_pct: 0,
16179 has_dictionary: 0,
16180 max_path_length: 4096,
16181 expected_volume_size: 0,
16182 };
16183 let extent = ObjectExtent {
16184 first_block_index: 0,
16185 data_block_count: 1,
16186 parity_block_count: 0,
16187 encrypted_size: 4096,
16188 };
16189
16190 assert_eq!(
16191 validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
16192 FormatError::InvalidArchive(
16193 "encrypted object parity does not match v41 compute_parity"
16194 )
16195 );
16196 }
16197
16198 #[test]
16199 fn encrypted_object_extent_matrix_rejects_overlaps() {
16200 let (opened, _) = multi_envelope_reader_fixture();
16201 let loaded_shard = opened
16202 .load_index_shard(&opened.index_root.shards[0])
16203 .unwrap();
16204 let base_envelopes = loaded_shard
16205 .envelopes
16206 .iter()
16207 .map(|entry| (entry.envelope_index, entry.clone()))
16208 .collect::<BTreeMap<_, _>>();
16209 let payload_start = loaded_shard.envelopes[0].first_block_index;
16210 let overlap = FormatError::InvalidArchive("encrypted object block ranges overlap");
16211
16212 let mut payload_overlap = base_envelopes.clone();
16213 payload_overlap
16214 .get_mut(&loaded_shard.envelopes[1].envelope_index)
16215 .unwrap()
16216 .first_block_index = payload_start;
16217 assert_eq!(
16218 opened
16219 .validate_encrypted_object_block_ranges(&payload_overlap)
16220 .unwrap_err(),
16221 overlap
16222 );
16223
16224 let mut shard_overlap = opened.clone();
16225 let shard = shard_overlap.index_root.shards[0].clone();
16226 shard_overlap.index_root.shards.push(ShardEntry {
16227 shard_index: 1,
16228 ..shard
16229 });
16230 assert_eq!(
16231 shard_overlap
16232 .validate_encrypted_object_block_ranges(&base_envelopes)
16233 .unwrap_err(),
16234 overlap
16235 );
16236
16237 let mut dictionary_overlap = opened.clone();
16238 dictionary_overlap.crypto_header.has_dictionary = 1;
16239 dictionary_overlap.index_root.header.dictionary_first_block = payload_start;
16240 dictionary_overlap
16241 .index_root
16242 .header
16243 .dictionary_data_block_count = 1;
16244 dictionary_overlap
16245 .index_root
16246 .header
16247 .dictionary_parity_block_count = 0;
16248 dictionary_overlap
16249 .index_root
16250 .header
16251 .dictionary_encrypted_size = 4096;
16252 dictionary_overlap
16253 .index_root
16254 .header
16255 .dictionary_decompressed_size = 128;
16256 assert_eq!(
16257 dictionary_overlap
16258 .validate_encrypted_object_block_ranges(&base_envelopes)
16259 .unwrap_err(),
16260 overlap
16261 );
16262
16263 let mut hint_overlap = opened.clone();
16264 hint_overlap
16265 .index_root
16266 .directory_hint_shards
16267 .push(DirectoryHintShardEntry {
16268 hint_shard_index: 0,
16269 first_dir_hash: [0; 8],
16270 last_dir_hash: [0; 8],
16271 first_block_index: payload_start,
16272 data_block_count: 1,
16273 parity_block_count: 0,
16274 encrypted_size: 4096,
16275 decompressed_size: 128,
16276 entry_count: 1,
16277 });
16278 assert_eq!(
16279 hint_overlap
16280 .validate_encrypted_object_block_ranges(&base_envelopes)
16281 .unwrap_err(),
16282 overlap
16283 );
16284 }
16285
16286 #[test]
16287 fn load_metadata_object_rejects_per_object_zstd_frame_exactness_mutations() {
16288 let volume_header = test_volume_header();
16289 let crypto_header = test_crypto_header();
16290 let subkeys = Subkeys::derive(
16291 &master_key(),
16292 &volume_header.archive_uuid,
16293 &volume_header.session_id,
16294 )
16295 .unwrap();
16296 let mut next_block_index = 0u64;
16297
16298 let index_root_payload = b"index root metadata object";
16299 let index_root_compressed = compress_zstd_frame(index_root_payload, 1).unwrap();
16300 assert_metadata_object_from_compressed(
16301 &{
16302 let mut bytes = index_root_compressed.clone();
16303 bytes.push(0);
16304 bytes
16305 },
16306 index_root_payload.len(),
16307 &subkeys,
16308 &volume_header,
16309 &crypto_header,
16310 &subkeys.index_root_key,
16311 &subkeys.index_nonce_seed,
16312 b"idxroot",
16313 0,
16314 BlockKind::IndexRootData,
16315 BlockKind::IndexRootParity,
16316 crypto_header.index_root_fec_data_shards,
16317 crypto_header.index_root_fec_parity_shards,
16318 &mut next_block_index,
16319 FormatError::TrailingBytesAfterZstdFrame,
16320 );
16321 assert_metadata_object_from_compressed(
16322 &index_root_compressed,
16323 index_root_payload.len() + 1,
16324 &subkeys,
16325 &volume_header,
16326 &crypto_header,
16327 &subkeys.index_root_key,
16328 &subkeys.index_nonce_seed,
16329 b"idxroot",
16330 0,
16331 BlockKind::IndexRootData,
16332 BlockKind::IndexRootParity,
16333 crypto_header.index_root_fec_data_shards,
16334 crypto_header.index_root_fec_parity_shards,
16335 &mut next_block_index,
16336 FormatError::ZstdDecompressedSizeMismatch {
16337 expected: index_root_payload.len() + 1,
16338 actual: index_root_payload.len(),
16339 },
16340 );
16341
16342 let index_shard_payload = b"index shard metadata object";
16343 let index_shard_compressed = compress_zstd_frame(index_shard_payload, 1).unwrap();
16344 assert_metadata_object_from_compressed(
16345 &{
16346 let mut bytes = index_shard_compressed.clone();
16347 bytes.push(0);
16348 bytes
16349 },
16350 index_shard_payload.len(),
16351 &subkeys,
16352 &volume_header,
16353 &crypto_header,
16354 &subkeys.index_shard_key,
16355 &subkeys.index_nonce_seed,
16356 b"idxshard",
16357 1,
16358 BlockKind::IndexShardData,
16359 BlockKind::IndexShardParity,
16360 crypto_header.index_fec_data_shards,
16361 crypto_header.index_fec_parity_shards,
16362 &mut next_block_index,
16363 FormatError::TrailingBytesAfterZstdFrame,
16364 );
16365 assert_metadata_object_from_compressed(
16366 &index_shard_compressed,
16367 index_shard_payload.len() + 1,
16368 &subkeys,
16369 &volume_header,
16370 &crypto_header,
16371 &subkeys.index_shard_key,
16372 &subkeys.index_nonce_seed,
16373 b"idxshard",
16374 1,
16375 BlockKind::IndexShardData,
16376 BlockKind::IndexShardParity,
16377 crypto_header.index_fec_data_shards,
16378 crypto_header.index_fec_parity_shards,
16379 &mut next_block_index,
16380 FormatError::ZstdDecompressedSizeMismatch {
16381 expected: index_shard_payload.len() + 1,
16382 actual: index_shard_payload.len(),
16383 },
16384 );
16385
16386 let directory_hint_payload = b"directory hint metadata object";
16387 let directory_hint_compressed = compress_zstd_frame(directory_hint_payload, 1).unwrap();
16388 assert_metadata_object_from_compressed(
16389 &{
16390 let mut bytes = directory_hint_compressed.clone();
16391 bytes.push(0);
16392 bytes
16393 },
16394 directory_hint_payload.len(),
16395 &subkeys,
16396 &volume_header,
16397 &crypto_header,
16398 &subkeys.dir_hint_key,
16399 &subkeys.index_nonce_seed,
16400 b"dirhint",
16401 0,
16402 BlockKind::DirectoryHintData,
16403 BlockKind::DirectoryHintParity,
16404 crypto_header.index_fec_data_shards,
16405 crypto_header.index_fec_parity_shards,
16406 &mut next_block_index,
16407 FormatError::TrailingBytesAfterZstdFrame,
16408 );
16409 assert_metadata_object_from_compressed(
16410 &directory_hint_compressed,
16411 directory_hint_payload.len() + 1,
16412 &subkeys,
16413 &volume_header,
16414 &crypto_header,
16415 &subkeys.dir_hint_key,
16416 &subkeys.index_nonce_seed,
16417 b"dirhint",
16418 0,
16419 BlockKind::DirectoryHintData,
16420 BlockKind::DirectoryHintParity,
16421 crypto_header.index_fec_data_shards,
16422 crypto_header.index_fec_parity_shards,
16423 &mut next_block_index,
16424 FormatError::ZstdDecompressedSizeMismatch {
16425 expected: directory_hint_payload.len() + 1,
16426 actual: directory_hint_payload.len(),
16427 },
16428 );
16429
16430 let dictionary_payload = b"dictionary metadata object";
16431 let dictionary_compressed = compress_zstd_frame(dictionary_payload, 1).unwrap();
16432 assert_metadata_object_from_compressed(
16433 &{
16434 let mut bytes = dictionary_compressed.clone();
16435 bytes.push(0);
16436 bytes
16437 },
16438 dictionary_payload.len(),
16439 &subkeys,
16440 &volume_header,
16441 &crypto_header,
16442 &subkeys.dictionary_key,
16443 &subkeys.index_nonce_seed,
16444 b"dict",
16445 0,
16446 BlockKind::DictionaryData,
16447 BlockKind::DictionaryParity,
16448 crypto_header.index_root_fec_data_shards,
16449 crypto_header.index_root_fec_parity_shards,
16450 &mut next_block_index,
16451 FormatError::TrailingBytesAfterZstdFrame,
16452 );
16453 assert_metadata_object_from_compressed(
16454 &dictionary_compressed,
16455 dictionary_payload.len() + 1,
16456 &subkeys,
16457 &volume_header,
16458 &crypto_header,
16459 &subkeys.dictionary_key,
16460 &subkeys.index_nonce_seed,
16461 b"dict",
16462 0,
16463 BlockKind::DictionaryData,
16464 BlockKind::DictionaryParity,
16465 crypto_header.index_root_fec_data_shards,
16466 crypto_header.index_root_fec_parity_shards,
16467 &mut next_block_index,
16468 FormatError::ZstdDecompressedSizeMismatch {
16469 expected: dictionary_payload.len() + 1,
16470 actual: dictionary_payload.len(),
16471 },
16472 );
16473 }
16474
16475 #[test]
16476 fn load_metadata_object_extent_rejects_encrypted_size_not_data_block_count_times_block_size() {
16477 let volume_header = test_volume_header();
16478 let crypto_header = test_crypto_header();
16479 let subkeys = Subkeys::derive(
16480 &master_key(),
16481 &volume_header.archive_uuid,
16482 &volume_header.session_id,
16483 )
16484 .unwrap();
16485 let mut next_block_index = 0u64;
16486
16487 let index_root_payload = b"index root metadata object";
16488 let (index_root_extent, index_root_records) = build_metadata_object_from_payload(
16489 index_root_payload,
16490 &subkeys,
16491 &volume_header,
16492 &crypto_header,
16493 &subkeys.index_root_key,
16494 &subkeys.index_nonce_seed,
16495 b"idxroot",
16496 0,
16497 BlockKind::IndexRootData,
16498 &mut next_block_index,
16499 );
16500 let mut index_root_extent = index_root_extent;
16501 index_root_extent.encrypted_size = index_root_extent
16502 .encrypted_size
16503 .saturating_add(crypto_header.block_size);
16504 assert_eq!(
16505 load_metadata_object_from_parts(
16506 &index_root_records,
16507 ObjectLoadContext::index_root(
16508 &volume_header,
16509 &crypto_header,
16510 &subkeys,
16511 index_root_extent,
16512 ),
16513 index_root_payload.len() as u32,
16514 )
16515 .unwrap_err(),
16516 FormatError::InvalidArchive(
16517 "encrypted object size is not data_block_count * block_size"
16518 )
16519 );
16520
16521 let index_shard_payload = b"index shard metadata object";
16522 let (index_shard_extent, index_shard_records) = build_metadata_object_from_payload(
16523 index_shard_payload,
16524 &subkeys,
16525 &volume_header,
16526 &crypto_header,
16527 &subkeys.index_shard_key,
16528 &subkeys.index_nonce_seed,
16529 b"idxshard",
16530 1,
16531 BlockKind::IndexShardData,
16532 &mut next_block_index,
16533 );
16534 let mut index_shard_extent = index_shard_extent;
16535 index_shard_extent.encrypted_size = index_shard_extent
16536 .encrypted_size
16537 .saturating_add(crypto_header.block_size);
16538 assert_eq!(
16539 load_metadata_object_from_parts(
16540 &index_shard_records,
16541 ObjectLoadContext {
16542 volume_header: &volume_header,
16543 crypto_header: &crypto_header,
16544 extent: index_shard_extent,
16545 data_kind: BlockKind::IndexShardData,
16546 parity_kind: BlockKind::IndexShardParity,
16547 key: &subkeys.index_shard_key,
16548 nonce_seed: &subkeys.index_nonce_seed,
16549 domain: b"idxshard",
16550 counter: 1,
16551 class_data_shard_max: crypto_header.index_fec_data_shards,
16552 class_parity_shard_max: crypto_header.index_fec_parity_shards,
16553 },
16554 index_shard_payload.len() as u32,
16555 )
16556 .unwrap_err(),
16557 FormatError::InvalidArchive(
16558 "encrypted object size is not data_block_count * block_size"
16559 )
16560 );
16561
16562 let directory_hint_payload = b"directory hint metadata object";
16563 let (directory_hint_extent, directory_hint_records) = build_metadata_object_from_payload(
16564 directory_hint_payload,
16565 &subkeys,
16566 &volume_header,
16567 &crypto_header,
16568 &subkeys.dir_hint_key,
16569 &subkeys.index_nonce_seed,
16570 b"dirhint",
16571 0,
16572 BlockKind::DirectoryHintData,
16573 &mut next_block_index,
16574 );
16575 let mut directory_hint_extent = directory_hint_extent;
16576 directory_hint_extent.encrypted_size = directory_hint_extent
16577 .encrypted_size
16578 .saturating_add(crypto_header.block_size);
16579 assert_eq!(
16580 load_metadata_object_from_parts(
16581 &directory_hint_records,
16582 ObjectLoadContext {
16583 volume_header: &volume_header,
16584 crypto_header: &crypto_header,
16585 extent: directory_hint_extent,
16586 data_kind: BlockKind::DirectoryHintData,
16587 parity_kind: BlockKind::DirectoryHintParity,
16588 key: &subkeys.dir_hint_key,
16589 nonce_seed: &subkeys.index_nonce_seed,
16590 domain: b"dirhint",
16591 counter: 0,
16592 class_data_shard_max: crypto_header.index_fec_data_shards,
16593 class_parity_shard_max: crypto_header.index_fec_parity_shards,
16594 },
16595 directory_hint_payload.len() as u32,
16596 )
16597 .unwrap_err(),
16598 FormatError::InvalidArchive(
16599 "encrypted object size is not data_block_count * block_size"
16600 )
16601 );
16602
16603 let dictionary_payload = b"dictionary metadata object";
16604 let (dictionary_extent, dictionary_records) = build_metadata_object_from_payload(
16605 dictionary_payload,
16606 &subkeys,
16607 &volume_header,
16608 &crypto_header,
16609 &subkeys.dictionary_key,
16610 &subkeys.index_nonce_seed,
16611 b"dict",
16612 0,
16613 BlockKind::DictionaryData,
16614 &mut next_block_index,
16615 );
16616 let mut dictionary_extent = dictionary_extent;
16617 dictionary_extent.encrypted_size = dictionary_extent
16618 .encrypted_size
16619 .saturating_add(crypto_header.block_size);
16620 assert_eq!(
16621 load_metadata_object_from_parts(
16622 &dictionary_records,
16623 ObjectLoadContext {
16624 volume_header: &volume_header,
16625 crypto_header: &crypto_header,
16626 extent: dictionary_extent,
16627 data_kind: BlockKind::DictionaryData,
16628 parity_kind: BlockKind::DictionaryParity,
16629 key: &subkeys.dictionary_key,
16630 nonce_seed: &subkeys.index_nonce_seed,
16631 domain: b"dict",
16632 counter: 0,
16633 class_data_shard_max: crypto_header.index_root_fec_data_shards,
16634 class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
16635 },
16636 dictionary_payload.len() as u32,
16637 )
16638 .unwrap_err(),
16639 FormatError::InvalidArchive(
16640 "encrypted object size is not data_block_count * block_size"
16641 )
16642 );
16643 }
16644
16645 #[test]
16646 fn opens_complete_multi_volume_archive() {
16647 let files = [RegularFile::new("alpha.txt", b"hello from volume stripes")];
16648 let archive = write_archive(
16649 &files,
16650 &master_key(),
16651 WriterOptions {
16652 stripe_width: 2,
16653 volume_loss_tolerance: 1,
16654 ..single_stream_options()
16655 },
16656 )
16657 .unwrap();
16658 assert_eq!(archive.volumes.len(), 2);
16659
16660 let volume_refs = archive
16661 .volumes
16662 .iter()
16663 .map(Vec::as_slice)
16664 .collect::<Vec<_>>();
16665 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
16666
16667 assert_eq!(opened.volume_header.stripe_width, 2);
16668 assert_eq!(opened.list_files().unwrap()[0].path, "alpha.txt");
16669 assert_eq!(
16670 opened.extract_file("alpha.txt").unwrap(),
16671 Some(b"hello from volume stripes".to_vec())
16672 );
16673 opened.verify().unwrap();
16674 }
16675
16676 #[test]
16677 fn recovers_from_one_missing_volume_when_parity_allows() {
16678 let files = [RegularFile::new("alpha.txt", b"recover me")];
16679 let archive = write_archive(
16680 &files,
16681 &master_key(),
16682 WriterOptions {
16683 stripe_width: 2,
16684 volume_loss_tolerance: 1,
16685 ..single_stream_options()
16686 },
16687 )
16688 .unwrap();
16689
16690 let recovered =
16691 open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap();
16692 assert_eq!(
16693 recovered.extract_file("alpha.txt").unwrap(),
16694 Some(b"recover me".to_vec())
16695 );
16696 recovered.verify().unwrap();
16697 }
16698
16699 #[test]
16700 fn recovers_from_crc_corrupted_block_when_parity_allows() {
16701 let files = [RegularFile::new("alpha.txt", b"repair corrupt block")];
16702 let archive = write_archive(
16703 &files,
16704 &master_key(),
16705 WriterOptions {
16706 stripe_width: 2,
16707 volume_loss_tolerance: 1,
16708 ..single_stream_options()
16709 },
16710 )
16711 .unwrap();
16712 let mut volumes = archive.volumes.clone();
16713 corrupt_first_block_record_payload(&mut volumes[0]);
16714
16715 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
16716 let recovered = open_archive_volumes(&volume_refs, &master_key()).unwrap();
16717
16718 assert_eq!(
16719 recovered.extract_file("alpha.txt").unwrap(),
16720 Some(b"repair corrupt block".to_vec())
16721 );
16722 recovered.verify().unwrap();
16723 }
16724
16725 #[test]
16726 fn rejects_multi_volume_count_mismatch_without_tolerance() {
16727 let files = [RegularFile::new("alpha.txt", b"count check")];
16728 let archive = write_archive(
16729 &files,
16730 &master_key(),
16731 WriterOptions {
16732 stripe_width: 3,
16733 volume_loss_tolerance: 0,
16734 ..single_stream_options()
16735 },
16736 )
16737 .unwrap();
16738
16739 assert_eq!(
16740 open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap_err(),
16741 FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
16742 );
16743 }
16744
16745 #[test]
16746 fn rejects_multi_volume_manifest_bootstrap_field_mismatch() {
16747 let files = [RegularFile::new("alpha.txt", b"footer mismatch")];
16748 let archive = write_archive(
16749 &files,
16750 &master_key(),
16751 WriterOptions {
16752 stripe_width: 2,
16753 volume_loss_tolerance: 1,
16754 ..single_stream_options()
16755 },
16756 )
16757 .unwrap();
16758
16759 let mut bad_first = archive.volumes[0].clone();
16760 rewrite_manifest_footer(&mut bad_first, &master_key(), |footer| {
16761 footer.index_root_first_block = footer.index_root_first_block.wrapping_add(1);
16762 });
16763
16764 open_archive_volumes(
16765 &[bad_first.as_slice(), archive.volumes[1].as_slice()],
16766 &master_key(),
16767 )
16768 .unwrap();
16769 }
16770
16771 #[test]
16772 fn repairs_corrupted_index_root_block_in_multi_volume_archive() {
16773 let files = [RegularFile::new("alpha.txt", b"repair meta root")];
16774 let archive = write_archive(
16775 &files,
16776 &master_key(),
16777 WriterOptions {
16778 stripe_width: 2,
16779 volume_loss_tolerance: 1,
16780 ..single_stream_options()
16781 },
16782 )
16783 .unwrap();
16784 let mut volumes = archive.volumes.clone();
16785
16786 let mut corrupted = false;
16787 for volume in &mut volumes {
16788 if let Some(slot) =
16789 block_record_slots_with_kind(volume, BlockKind::IndexRootData).first()
16790 {
16791 corrupt_block_record_payload_at_slot(volume, *slot);
16792 corrupted = true;
16793 break;
16794 }
16795 }
16796 assert!(corrupted, "expected an IndexRootData record");
16797
16798 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
16799 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
16800 assert_eq!(
16801 opened.extract_file("alpha.txt").unwrap(),
16802 Some(b"repair meta root".to_vec())
16803 );
16804 opened.verify().unwrap();
16805 }
16806
16807 #[test]
16808 fn repairs_corrupted_index_shard_block_in_multi_volume_archive() {
16809 let files = [RegularFile::new("alpha.txt", b"repair meta shard")];
16810 let archive = write_archive(
16811 &files,
16812 &master_key(),
16813 WriterOptions {
16814 stripe_width: 2,
16815 volume_loss_tolerance: 1,
16816 ..single_stream_options()
16817 },
16818 )
16819 .unwrap();
16820 let mut volumes = archive.volumes.clone();
16821
16822 let mut corrupted = false;
16823 for volume in &mut volumes {
16824 if let Some(slot) =
16825 block_record_slots_with_kind(volume, BlockKind::IndexShardData).first()
16826 {
16827 corrupt_block_record_payload_at_slot(volume, *slot);
16828 corrupted = true;
16829 break;
16830 }
16831 }
16832 assert!(corrupted, "expected an IndexShardData record");
16833
16834 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
16835 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
16836 assert_eq!(
16837 opened.extract_file("alpha.txt").unwrap(),
16838 Some(b"repair meta shard".to_vec())
16839 );
16840 opened.verify().unwrap();
16841 }
16842
16843 #[test]
16844 fn rejects_missing_volume_when_loss_tolerance_zero_even_with_bitrot_parity() {
16845 let files = [RegularFile::new(
16846 "alpha.txt",
16847 b"bitrot parity is not volume loss",
16848 )];
16849 let archive = write_archive(
16850 &files,
16851 &master_key(),
16852 WriterOptions {
16853 stripe_width: 2,
16854 volume_loss_tolerance: 0,
16855 bit_rot_buffer_pct: 1,
16856 ..single_stream_options()
16857 },
16858 )
16859 .unwrap();
16860
16861 assert_eq!(
16862 open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap_err(),
16863 FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
16864 );
16865 }
16866
16867 #[test]
16868 fn repairs_crc_erasure_only_within_parity_budget() {
16869 let payload = pseudo_random_bytes(12_000);
16870 let archive = write_archive(
16871 &[RegularFile::new("rot.bin", &payload)],
16872 &master_key(),
16873 small_block_recovery_options(),
16874 )
16875 .unwrap();
16876 let payload_slots = first_payload_data_run_slots(&archive.bytes);
16877 assert!(
16878 payload_slots.len() >= 2,
16879 "fixture must contain a multi-block payload object"
16880 );
16881
16882 let mut one_erasure = archive.bytes.clone();
16883 corrupt_block_record_payload_at_slot(&mut one_erasure, payload_slots[0]);
16884 let repaired = open_archive(&one_erasure, &master_key()).unwrap();
16885 assert_eq!(
16886 repaired.extract_file("rot.bin").unwrap(),
16887 Some(payload.clone())
16888 );
16889
16890 let mut two_erasures = archive.bytes.clone();
16891 corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[0]);
16892 corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[1]);
16893 let unrepaired = open_archive(&two_erasures, &master_key()).unwrap();
16894 assert_eq!(
16895 unrepaired.extract_file("rot.bin").unwrap_err(),
16896 FormatError::FecTooFewAvailableShards
16897 );
16898 }
16899
16900 #[test]
16901 fn verify_rejects_missing_required_object_block_extent() {
16902 let (mut opened, missing_block) = multi_envelope_reader_fixture();
16903 assert!(opened.blocks.remove(&missing_block).is_some());
16904
16905 assert_eq!(
16906 opened.verify().unwrap_err(),
16907 FormatError::FecTooFewAvailableShards
16908 );
16909 }
16910
16911 #[test]
16912 fn parity_crc_erasure_does_not_hide_authenticated_data() {
16913 let payload = pseudo_random_bytes(12_000);
16914 let archive = write_archive(
16915 &[RegularFile::new("parity-erasure.bin", &payload)],
16916 &master_key(),
16917 parity_rich_recovery_options(),
16918 )
16919 .unwrap();
16920 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
16921 let parity_slots = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity);
16922 assert!(
16923 parity_slots.len() >= 2,
16924 "fixture must contain redundant parity shards"
16925 );
16926 let mut corrupted = archive.bytes;
16927 corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
16928 corrupt_block_record_payload_at_slot(&mut corrupted, parity_slots[0]);
16929
16930 let opened = open_archive(&corrupted, &master_key()).unwrap();
16931 assert_eq!(
16932 opened.extract_file("parity-erasure.bin").unwrap(),
16933 Some(payload)
16934 );
16935 opened.verify().unwrap();
16936 }
16937
16938 #[test]
16939 fn repair_patches_restore_crc_erased_payload_block() {
16940 let payload = pseudo_random_bytes(12_000);
16941 let archive = write_archive(
16942 &[RegularFile::new("rot.bin", &payload)],
16943 &master_key(),
16944 small_block_recovery_options(),
16945 )
16946 .unwrap();
16947 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
16948 let mut corrupted = archive.bytes.clone();
16949 corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
16950
16951 let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
16952 opened.verify().unwrap();
16953 let patches = opened.repair_patches().unwrap();
16954 assert_eq!(patches.len(), 1);
16955 apply_repair_patches(&mut corrupted, &patches);
16956
16957 let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
16958 repaired.verify().unwrap();
16959 assert!(repaired.repair_patches().unwrap().is_empty());
16960 }
16961
16962 #[test]
16963 fn repair_patches_restore_crc_erased_payload_parity_block() {
16964 let payload = pseudo_random_bytes(12_000);
16965 let archive = write_archive(
16966 &[RegularFile::new("parity-erasure.bin", &payload)],
16967 &master_key(),
16968 parity_rich_recovery_options(),
16969 )
16970 .unwrap();
16971 let parity_slot = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity)[0];
16972 let mut corrupted = archive.bytes.clone();
16973 corrupt_block_record_payload_at_slot(&mut corrupted, parity_slot);
16974
16975 let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
16976 opened.verify().unwrap();
16977 let patches = opened.repair_patches().unwrap();
16978 assert_eq!(patches.len(), 1);
16979 apply_repair_patches(&mut corrupted, &patches);
16980
16981 let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
16982 repaired.verify().unwrap();
16983 assert!(repaired.repair_patches().unwrap().is_empty());
16984 }
16985
16986 #[test]
16987 fn recovers_physical_odd_block_size_from_cmra_authority() {
16988 let archive = write_archive(
16989 &[RegularFile::new("odd-block.txt", b"payload")],
16990 &master_key(),
16991 small_block_recovery_options(),
16992 )
16993 .unwrap();
16994 let mut malformed = archive.bytes;
16995 let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
16996 let block_size_offset = volume_header.crypto_header_offset as usize + 24;
16997 malformed[block_size_offset..block_size_offset + 4].copy_from_slice(&4097u32.to_le_bytes());
16998
16999 let opened = open_archive(&malformed, &master_key()).unwrap();
17000 assert_ne!(opened.crypto_header.block_size, 4097);
17001 assert_eq!(
17002 opened.extract_file("odd-block.txt").unwrap(),
17003 Some(b"payload".to_vec())
17004 );
17005 opened.verify().unwrap();
17006 }
17007
17008 #[test]
17009 fn repairs_structurally_malformed_payload_block_slots() {
17010 let payload = pseudo_random_bytes(12_000);
17011 let archive = write_archive(
17012 &[RegularFile::new("structural-block.bin", &payload)],
17013 &master_key(),
17014 small_block_recovery_options(),
17015 )
17016 .unwrap();
17017 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
17018
17019 let mut bad_magic = archive.bytes.clone();
17020 corrupt_block_record_magic_at_slot(&mut bad_magic, payload_slot);
17021 assert_eq!(
17022 open_archive(&bad_magic, &master_key())
17023 .unwrap()
17024 .extract_file("structural-block.bin")
17025 .unwrap(),
17026 Some(payload.clone())
17027 );
17028
17029 let mut bad_reserved = archive.bytes;
17030 corrupt_block_record_reserved_at_slot(&mut bad_reserved, payload_slot);
17031 assert_eq!(
17032 open_archive(&bad_reserved, &master_key())
17033 .unwrap()
17034 .extract_file("structural-block.bin")
17035 .unwrap(),
17036 Some(payload)
17037 );
17038 }
17039
17040 #[test]
17041 fn repair_patches_restore_structurally_malformed_payload_block_slot() {
17042 let payload = pseudo_random_bytes(12_000);
17043 let archive = write_archive(
17044 &[RegularFile::new("structural-patch.bin", &payload)],
17045 &master_key(),
17046 small_block_recovery_options(),
17047 )
17048 .unwrap();
17049 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
17050 let mut corrupted = archive.bytes.clone();
17051 corrupt_block_record_magic_at_slot(&mut corrupted, payload_slot);
17052
17053 let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
17054 opened.verify().unwrap();
17055 assert_eq!(
17056 opened.extract_file("structural-patch.bin").unwrap(),
17057 Some(payload)
17058 );
17059 let patches = opened.repair_patches().unwrap();
17060 assert_eq!(patches.len(), 1);
17061 apply_repair_patches(&mut corrupted, &patches);
17062
17063 let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
17064 repaired.verify().unwrap();
17065 assert!(repaired.repair_patches().unwrap().is_empty());
17066 }
17067
17068 #[test]
17069 fn repairs_structurally_malformed_index_root_block_slot() {
17070 let archive = write_archive(
17071 &[RegularFile::new(
17072 "structural-index-root.txt",
17073 b"metadata repair",
17074 )],
17075 &master_key(),
17076 small_block_recovery_options(),
17077 )
17078 .unwrap();
17079 let index_root_slot =
17080 first_block_record_slot_with_kind(&archive.bytes, BlockKind::IndexRootData).unwrap();
17081 let mut corrupted = archive.bytes;
17082 corrupt_block_record_magic_at_slot(&mut corrupted, index_root_slot);
17083
17084 let opened = open_archive(&corrupted, &master_key()).unwrap();
17085 assert_eq!(
17086 opened.extract_file("structural-index-root.txt").unwrap(),
17087 Some(b"metadata repair".to_vec())
17088 );
17089 opened.verify().unwrap();
17090 }
17091
17092 #[test]
17093 fn rejects_parity_block_with_last_data_flag() {
17094 let archive = write_archive(
17095 &[RegularFile::new("parity-flag.txt", b"payload")],
17096 &master_key(),
17097 small_block_recovery_options(),
17098 )
17099 .unwrap();
17100 let parity_slot =
17101 first_block_record_slot_with_kind(&archive.bytes, BlockKind::PayloadParity).unwrap();
17102 let mut malformed = archive.bytes;
17103 mutate_block_record_at_slot(&mut malformed, parity_slot, |record| {
17104 record.flags = 0x01;
17105 });
17106
17107 assert_eq!(
17108 open_archive(&malformed, &master_key()).unwrap_err(),
17109 FormatError::ParityBlockHasLastDataFlag
17110 );
17111 }
17112
17113 #[test]
17114 fn rejects_missing_and_duplicate_payload_last_data_flags() {
17115 let payload = pseudo_random_bytes(12_000);
17116 let archive = write_archive(
17117 &[RegularFile::new("flags.bin", &payload)],
17118 &master_key(),
17119 small_block_recovery_options(),
17120 )
17121 .unwrap();
17122 let payload_slots = first_payload_data_run_slots(&archive.bytes);
17123 assert!(
17124 payload_slots.len() >= 2,
17125 "fixture must contain a multi-block payload object"
17126 );
17127
17128 let mut duplicate_last = archive.bytes.clone();
17129 mutate_block_record_at_slot(&mut duplicate_last, payload_slots[0], |record| {
17130 record.flags = 0x01;
17131 });
17132 let opened = open_archive(&duplicate_last, &master_key()).unwrap();
17133 assert_eq!(
17134 opened.extract_file("flags.bin").unwrap_err(),
17135 FormatError::InvalidArchive("object last-data flag is not on the final data block")
17136 );
17137
17138 let mut missing_last = archive.bytes;
17139 mutate_block_record_at_slot(
17140 &mut missing_last,
17141 *payload_slots.last().unwrap(),
17142 |record| {
17143 record.flags = 0;
17144 },
17145 );
17146 let opened = open_archive(&missing_last, &master_key()).unwrap();
17147 assert_eq!(
17148 opened.extract_file("flags.bin").unwrap_err(),
17149 FormatError::InvalidArchive("object last-data flag is not on the final data block")
17150 );
17151 }
17152
17153 #[test]
17154 fn recovers_from_one_corrupt_manifest_footer_copy_when_another_volume_authenticates() {
17155 let files = [RegularFile::new(
17156 "footer-copy.txt",
17157 b"survives one bad footer",
17158 )];
17159 let archive = write_archive(
17160 &files,
17161 &master_key(),
17162 WriterOptions {
17163 stripe_width: 2,
17164 volume_loss_tolerance: 1,
17165 ..single_stream_options()
17166 },
17167 )
17168 .unwrap();
17169 let mut volumes = archive.volumes.clone();
17170 corrupt_manifest_footer_hmac(&mut volumes[0]);
17171
17172 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
17173 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
17174 assert_eq!(opened.manifest_footer.volume_index, 0);
17175 assert_eq!(opened.volume_header.volume_index, 0);
17176 assert_eq!(opened.volume_trailer.as_ref().unwrap().volume_index, 0);
17177 assert_eq!(
17178 opened.extract_file("footer-copy.txt").unwrap(),
17179 Some(b"survives one bad footer".to_vec())
17180 );
17181 opened.verify().unwrap();
17182 }
17183
17184 #[test]
17185 fn manifest_footer_corruption_requires_trusted_sidecar() {
17186 let archive = write_archive(
17187 &[RegularFile::new("footer.txt", b"sidecar authority")],
17188 &master_key(),
17189 single_stream_options(),
17190 )
17191 .unwrap();
17192 let manifest_offset = terminal_material_offset(&archive.bytes);
17193 let mut corrupted = archive.bytes.clone();
17194 corrupted[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
17195 corrupt_v41_terminal_recovery(&mut corrupted);
17196
17197 assert!(open_archive(&corrupted, &master_key()).is_err());
17198
17199 let opened =
17200 open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
17201 .unwrap();
17202 assert!(opened.volume_trailer.is_none());
17203 assert_eq!(
17204 opened.extract_file("footer.txt").unwrap(),
17205 Some(b"sidecar authority".to_vec())
17206 );
17207 opened.verify().unwrap();
17208 }
17209
17210 #[test]
17211 fn authenticated_footer_trailer_and_sidecar_hmac_boundaries_are_enforced() {
17212 let archive = write_archive(
17213 &[RegularFile::new("hmac-boundary.txt", b"boundary bytes")],
17214 &master_key(),
17215 single_stream_options(),
17216 )
17217 .unwrap();
17218 let strict_options = ReaderOptions {
17219 max_trailing_garbage_scan: 0,
17220 ..ReaderOptions::default()
17221 };
17222
17223 let manifest_offset = terminal_material_offset(&archive.bytes);
17224 for offset in [
17225 manifest_offset + 71,
17226 manifest_offset + MANIFEST_HMAC_COVERED_LEN,
17227 ] {
17228 let mut corrupted = archive.bytes.clone();
17229 corrupted[offset] ^= 0x01;
17230 open_archive(&corrupted, &master_key()).unwrap();
17231 }
17232
17233 let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
17234 for offset in [
17235 trailer_offset + 75,
17236 trailer_offset + TRAILER_HMAC_COVERED_LEN,
17237 ] {
17238 let mut corrupted = archive.bytes.clone();
17239 corrupted[offset] ^= 0x01;
17240 OpenedArchive::open_with_options(&corrupted, &master_key(), strict_options).unwrap();
17241 }
17242
17243 let mut covered_sidecar = archive.bootstrap_sidecar.clone();
17244 let mut header =
17245 BootstrapSidecarHeader::parse(&covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
17246 .unwrap();
17247 header.manifest_footer_offset += 1;
17248 covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
17249 assert_eq!(
17250 open_archive_with_bootstrap_sidecar(&archive.bytes, &covered_sidecar, &master_key())
17251 .unwrap_err(),
17252 FormatError::HmacMismatch {
17253 structure: "BootstrapSidecarHeader"
17254 }
17255 );
17256
17257 let mut tag_sidecar = archive.bootstrap_sidecar.clone();
17258 let mut header =
17259 BootstrapSidecarHeader::parse(&tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17260 header.sidecar_hmac[0] ^= 1;
17261 tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
17262 assert_eq!(
17263 open_archive_with_bootstrap_sidecar(&archive.bytes, &tag_sidecar, &master_key())
17264 .unwrap_err(),
17265 FormatError::HmacMismatch {
17266 structure: "BootstrapSidecarHeader"
17267 }
17268 );
17269
17270 let mut non_covered_sidecar = archive.bootstrap_sidecar.clone();
17271 let header =
17272 BootstrapSidecarHeader::parse(&non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
17273 .unwrap();
17274 let mut header_bytes = header.to_bytes();
17275 header_bytes[124] ^= 0x01;
17276 let crc = crc32c::crc32c(&header_bytes[..124]);
17277 header_bytes[124..128].copy_from_slice(&crc.to_le_bytes());
17278 non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
17279 let opened = open_archive_with_bootstrap_sidecar(
17280 &archive.bytes,
17281 &non_covered_sidecar,
17282 &master_key(),
17283 )
17284 .unwrap();
17285 assert_eq!(
17286 opened.extract_file("hmac-boundary.txt").unwrap(),
17287 Some(b"boundary bytes".to_vec())
17288 );
17289 }
17290
17291 #[test]
17292 fn rejects_authenticated_footer_and_trailer_volume_index_mismatches() {
17293 let archive = write_archive(
17294 &[RegularFile::new("volume-index.txt", b"identity")],
17295 &master_key(),
17296 single_stream_options(),
17297 )
17298 .unwrap();
17299
17300 let mut bad_trailer = archive.bytes.clone();
17301 rewrite_volume_trailer(&mut bad_trailer, &master_key(), |trailer| {
17302 trailer.volume_index = 1;
17303 });
17304 open_archive(&bad_trailer, &master_key()).unwrap();
17305
17306 let mut bad_manifest = archive.bytes;
17307 rewrite_manifest_footer(&mut bad_manifest, &master_key(), |footer| {
17308 footer.volume_index = 1;
17309 });
17310 open_archive(&bad_manifest, &master_key()).unwrap();
17311 }
17312
17313 #[test]
17314 fn rejects_same_key_header_terminal_material_splice() {
17315 let first = write_archive(
17316 &[RegularFile::new("splice.txt", b"same shape")],
17317 &master_key(),
17318 single_stream_options(),
17319 )
17320 .unwrap();
17321 let second = write_archive(
17322 &[RegularFile::new("splice.txt", b"same shape")],
17323 &master_key(),
17324 single_stream_options(),
17325 )
17326 .unwrap();
17327 assert_ne!(first.archive_uuid, second.archive_uuid);
17328 assert_eq!(
17329 terminal_material_offset(&first.bytes),
17330 terminal_material_offset(&second.bytes)
17331 );
17332 assert_eq!(first.bytes.len(), second.bytes.len());
17333
17334 let terminal_offset = terminal_material_offset(&first.bytes);
17335 let mut spliced = first.bytes.clone();
17336 spliced[terminal_offset..].copy_from_slice(&second.bytes[terminal_offset..]);
17337
17338 assert_eq!(
17339 open_archive(&spliced, &master_key()).unwrap_err(),
17340 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
17341 );
17342 }
17343
17344 #[test]
17345 fn rejects_cmra_crypto_header_pre_hmac_mismatch() {
17346 let kdf_params = crate::crypto::KdfParams::Argon2id {
17347 t_cost: 1,
17348 m_cost_kib: 8,
17349 parallelism: 1,
17350 salt: b"0123456789abcdef".to_vec(),
17351 };
17352 let archive = write_archive_with_kdf(
17353 &[RegularFile::new("cmra-crypto.txt", b"same fixed header")],
17354 &master_key(),
17355 single_stream_options(),
17356 &kdf_params,
17357 )
17358 .unwrap();
17359 let mut mutated = archive.bytes.clone();
17360 let volume_header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
17361 let subkeys = Subkeys::derive(
17362 &master_key(),
17363 &volume_header.archive_uuid,
17364 &volume_header.session_id,
17365 )
17366 .unwrap();
17367
17368 rewrite_cmra_image(&mut mutated, CmraRecoveryMode::KeyHolding, |image| {
17369 let crypto_region = image
17370 .regions
17371 .iter_mut()
17372 .find(|region| region.region_type == 2)
17373 .unwrap();
17374 let hmac_offset = crypto_region.bytes.len() - CRYPTO_HEADER_HMAC_LEN;
17375 let salt_start = CRYPTO_HEADER_FIXED_LEN + 16;
17376 crypto_region.bytes[salt_start] ^= 0x01;
17377 let hmac = compute_hmac(
17378 HmacDomain::CryptoHeader,
17379 &subkeys.mac_key,
17380 &volume_header.archive_uuid,
17381 &volume_header.session_id,
17382 &crypto_region.bytes[..hmac_offset],
17383 );
17384 crypto_region.bytes[hmac_offset..].copy_from_slice(&hmac);
17385 });
17386
17387 let final_offset = mutated.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
17388 let locator = final_recovery_locator(&mutated);
17389 let crypto_start = volume_header.crypto_header_offset as usize;
17390 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
17391 let parsed_crypto = CryptoHeader::parse(
17392 &mutated[crypto_start..crypto_end],
17393 volume_header.crypto_header_length,
17394 )
17395 .unwrap();
17396 assert_eq!(
17397 parse_locator_cmra_candidate(
17398 &mutated,
17399 final_offset,
17400 locator,
17401 KeyHoldingTerminalContext {
17402 subkeys: &subkeys,
17403 volume_header: &volume_header,
17404 crypto_header: &parsed_crypto.fixed,
17405 crypto_header_bytes: &mutated[crypto_start..crypto_end],
17406 },
17407 )
17408 .unwrap_err(),
17409 FormatError::InvalidArchive("CMRA CryptoHeader differs from parsed CryptoHeader")
17410 );
17411 assert!(open_archive(&mutated, &master_key()).is_err());
17412 }
17413
17414 #[test]
17415 fn recovers_physical_crypto_header_splice_from_cmra_authority() {
17416 let base = WriterOptions {
17417 archive_uuid: Some([0x11; 16]),
17418 session_id: Some([0x22; 16]),
17419 ..small_block_recovery_options()
17420 };
17421 let same_archive = WriterOptions {
17422 archive_uuid: Some([0x11; 16]),
17423 session_id: Some([0x33; 16]),
17424 ..small_block_recovery_options()
17425 };
17426
17427 let first = write_archive(
17428 &[RegularFile::new("splice.txt", b"same shape")],
17429 &master_key(),
17430 base,
17431 )
17432 .unwrap();
17433 let second = write_archive(
17434 &[RegularFile::new("splice.txt", b"same shape")],
17435 &master_key(),
17436 same_archive,
17437 )
17438 .unwrap();
17439
17440 let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
17441 let second_volume_header = VolumeHeader::parse(&second.bytes[..VOLUME_HEADER_LEN]).unwrap();
17442 let crypto_start = volume_header.crypto_header_offset as usize;
17443 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
17444 let second_crypto_end = second_volume_header.crypto_header_offset as usize
17445 + second_volume_header.crypto_header_length as usize;
17446 assert_eq!(crypto_end, second_crypto_end);
17447
17448 let mut spliced = first.bytes.clone();
17449 spliced[crypto_start..crypto_end].copy_from_slice(&second.bytes[crypto_start..crypto_end]);
17450
17451 let opened = open_archive(&spliced, &master_key()).unwrap();
17452 assert_eq!(
17453 opened.crypto_header_bytes,
17454 first.bytes[crypto_start..crypto_end].to_vec()
17455 );
17456 assert_eq!(
17457 opened.extract_file("splice.txt").unwrap(),
17458 Some(b"same shape".to_vec())
17459 );
17460 opened.verify().unwrap();
17461 }
17462
17463 #[test]
17464 fn rejects_same_key_object_splice_with_session_mismatch() {
17465 let first = write_archive(
17466 &[RegularFile::new("splice.txt", b"same shape")],
17467 &master_key(),
17468 WriterOptions {
17469 archive_uuid: Some([0x11; 16]),
17470 session_id: Some([0x22; 16]),
17471 ..single_stream_options()
17472 },
17473 )
17474 .unwrap();
17475 let second = write_archive(
17476 &[RegularFile::new("splice.txt", b"same shape")],
17477 &master_key(),
17478 WriterOptions {
17479 archive_uuid: Some([0x11; 16]),
17480 session_id: Some([0x33; 16]),
17481 ..single_stream_options()
17482 },
17483 )
17484 .unwrap();
17485
17486 let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
17487 let crypto_end = volume_header.crypto_header_offset as usize
17488 + volume_header.crypto_header_length as usize;
17489 let terminal_offset = terminal_material_offset(&first.bytes);
17490 let second_terminal_offset = terminal_material_offset(&second.bytes);
17491 assert_eq!(terminal_offset, second_terminal_offset);
17492
17493 let mut spliced = first.bytes.clone();
17494 spliced[crypto_end..terminal_offset]
17495 .copy_from_slice(&second.bytes[crypto_end..terminal_offset]);
17496
17497 assert_eq!(
17498 open_archive(&spliced, &master_key()).unwrap_err(),
17499 FormatError::AeadFailure
17500 );
17501 }
17502
17503 #[test]
17504 fn rejects_authenticated_trailer_pointer_and_count_mutations() {
17505 let archive = write_archive(
17506 &[RegularFile::new(
17507 "trailer-range.txt",
17508 b"authenticated ranges",
17509 )],
17510 &master_key(),
17511 single_stream_options(),
17512 )
17513 .unwrap();
17514 let strict_options = ReaderOptions {
17515 max_trailing_garbage_scan: 0,
17516 ..ReaderOptions::default()
17517 };
17518 let bytes = archive.bytes;
17519 let manifest_offset = terminal_material_offset(&bytes);
17520 let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
17521
17522 let mut wrong_footer_length = bytes.clone();
17523 rewrite_volume_trailer(&mut wrong_footer_length, &master_key(), |trailer| {
17524 trailer.manifest_footer_length = 42;
17525 });
17526 OpenedArchive::open_with_options(&wrong_footer_length, &master_key(), strict_options)
17527 .unwrap();
17528
17529 for (label, offset) in [
17530 (
17531 "offset before trailer by 1",
17532 manifest_offset.saturating_sub(1),
17533 ),
17534 ("offset after trailer", manifest_offset + 1),
17535 ("offset at stream start", 0),
17536 ("offset at trailer", trailer_offset),
17537 ("offset beyond trailer", trailer_offset + 4),
17538 ] {
17539 let mut wrong_footer_offset = bytes.clone();
17540 rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
17541 trailer.manifest_footer_offset = offset as u64;
17542 });
17543 open_archive(&wrong_footer_offset, &master_key())
17544 .unwrap_or_else(|err| panic!("manifest offset case {label}: {err:?}"));
17545 }
17546
17547 let mut wrong_bytes_written = bytes.clone();
17548 rewrite_volume_trailer(&mut wrong_bytes_written, &master_key(), |trailer| {
17549 trailer.bytes_written += 1;
17550 });
17551 open_archive(&wrong_bytes_written, &master_key()).unwrap();
17552
17553 let mut wrong_block_count = bytes.clone();
17554 rewrite_volume_trailer(&mut wrong_block_count, &master_key(), |trailer| {
17555 trailer.block_count += 1;
17556 });
17557 open_archive(&wrong_block_count, &master_key()).unwrap();
17558
17559 let mut wrong_footer_offset = bytes.clone();
17560 rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
17561 trailer.manifest_footer_offset = bytes.len() as u64 + 1024;
17562 });
17563 open_archive(&wrong_footer_offset, &master_key()).unwrap();
17564 }
17565
17566 #[test]
17567 fn rejects_authenticated_trailer_outside_trailing_scan_cap() {
17568 let archive = write_archive(
17569 &[RegularFile::new(
17570 "trailer-trailing-scan.txt",
17571 b"trailer scan boundaries",
17572 )],
17573 &master_key(),
17574 single_stream_options(),
17575 )
17576 .unwrap();
17577 let options = ReaderOptions {
17578 max_trailing_garbage_scan: 8,
17579 ..ReaderOptions::default()
17580 };
17581
17582 let mut within_scan = archive.bytes.clone();
17583 within_scan.resize(within_scan.len() + options.max_trailing_garbage_scan, 0xAA);
17584 let opened =
17585 OpenedArchive::open_with_options(&within_scan, &master_key(), options).unwrap();
17586 assert_eq!(
17587 opened.extract_file("trailer-trailing-scan.txt").unwrap(),
17588 Some(b"trailer scan boundaries".to_vec())
17589 );
17590
17591 let mut beyond_scan = archive.bytes.clone();
17592 beyond_scan.resize(
17593 beyond_scan.len() + max_critical_recovery_scan(options).unwrap() + 1,
17594 0xAA,
17595 );
17596 assert_eq!(
17597 OpenedArchive::open_with_options(&beyond_scan, &master_key(), options).unwrap_err(),
17598 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
17599 );
17600 }
17601
17602 #[test]
17603 fn rejects_authenticated_index_root_extent_size_mismatch_at_open() {
17604 let archive = write_archive(
17605 &[RegularFile::new("index-root-size.txt", b"extent size")],
17606 &master_key(),
17607 single_stream_options(),
17608 )
17609 .unwrap();
17610 let mut malformed = archive.bytes;
17611 let slot = first_block_record_slot_with_kind(&malformed, BlockKind::IndexRootData)
17612 .expect("archive should contain IndexRootData");
17613 mutate_block_record_at_slot(&mut malformed, slot, |record| {
17614 record.payload[0] ^= 0x55;
17615 });
17616
17617 assert_eq!(
17618 open_archive(&malformed, &master_key()).unwrap_err(),
17619 FormatError::AeadFailure
17620 );
17621 }
17622
17623 #[test]
17624 fn rejects_block_record_at_wrong_stripe_position() {
17625 let files = [RegularFile::new("alpha.txt", b"wrong stripe")];
17626 let archive = write_archive(
17627 &files,
17628 &master_key(),
17629 WriterOptions {
17630 stripe_width: 2,
17631 volume_loss_tolerance: 1,
17632 ..single_stream_options()
17633 },
17634 )
17635 .unwrap();
17636 let mut volumes = archive.volumes.clone();
17637 mutate_first_block_record(&mut volumes[0], |record| {
17638 record.block_index += 2;
17639 });
17640
17641 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
17642 assert_eq!(
17643 open_archive_volumes(&volume_refs, &master_key()).unwrap_err(),
17644 FormatError::InvalidArchive("BlockRecord index does not match volume position")
17645 );
17646 }
17647
17648 #[test]
17649 fn rejects_decreasing_block_record_index_in_required_region() {
17650 let archive = write_archive(
17651 &[RegularFile::new("alpha.txt", b"decreasing block index")],
17652 &master_key(),
17653 single_stream_options(),
17654 )
17655 .unwrap();
17656 assert!(block_record_slots(&archive.bytes).len() >= 2);
17657
17658 let mut malformed = archive.bytes;
17659 mutate_block_record_at_slot(&mut malformed, 1, |record| {
17660 record.block_index = 0;
17661 });
17662
17663 assert_eq!(
17664 open_archive(&malformed, &master_key()).unwrap_err(),
17665 FormatError::InvalidArchive("BlockRecord index does not match volume position")
17666 );
17667 }
17668
17669 #[test]
17670 fn rejects_duplicate_authenticated_volume_indexes() {
17671 let files = [RegularFile::new("alpha.txt", b"duplicates")];
17672 let archive = write_archive(
17673 &files,
17674 &master_key(),
17675 WriterOptions {
17676 stripe_width: 2,
17677 volume_loss_tolerance: 1,
17678 ..single_stream_options()
17679 },
17680 )
17681 .unwrap();
17682
17683 assert_eq!(
17684 open_archive_volumes(
17685 &[archive.volumes[0].as_slice(), archive.volumes[0].as_slice()],
17686 &master_key()
17687 )
17688 .unwrap_err(),
17689 FormatError::InvalidArchive("duplicate authenticated volume index")
17690 );
17691 }
17692
17693 #[test]
17694 fn rejects_conflicting_duplicate_authenticated_volume_indexes_by_default() {
17695 let files = [RegularFile::new("alpha.txt", b"conflicting duplicates")];
17696 let archive = write_archive(
17697 &files,
17698 &master_key(),
17699 WriterOptions {
17700 stripe_width: 2,
17701 volume_loss_tolerance: 1,
17702 ..single_stream_options()
17703 },
17704 )
17705 .unwrap();
17706 let mut conflicting = archive.volumes[0].clone();
17707 corrupt_first_block_record_payload(&mut conflicting);
17708
17709 assert_eq!(
17710 open_archive_volumes(
17711 &[archive.volumes[0].as_slice(), conflicting.as_slice()],
17712 &master_key()
17713 )
17714 .unwrap_err(),
17715 FormatError::InvalidArchive("duplicate authenticated volume index")
17716 );
17717 }
17718
17719 fn directory_hint_table_from_rows(
17720 hint_shard_index: u64,
17721 rows: &[(Vec<u8>, Vec<u32>)],
17722 shard_count: u32,
17723 ) -> DirectoryHintTable {
17724 let mut entries = Vec::new();
17725 let mut shard_row_indexes = Vec::new();
17726 let mut string_pool = Vec::new();
17727
17728 for (path, rows) in rows {
17729 let path_offset = if path.is_empty() {
17730 0
17731 } else {
17732 let offset = string_pool.len() as u64;
17733 string_pool.extend_from_slice(path);
17734 offset
17735 };
17736 let shard_list_start_index = shard_row_indexes.len() as u32;
17737 shard_row_indexes.extend_from_slice(rows);
17738 entries.push(DirectoryHintEntry {
17739 dir_hash: hash_prefix(path),
17740 path_offset,
17741 path_length: path.len() as u32,
17742 shard_list_start_index,
17743 shard_count: rows.len() as u32,
17744 });
17745 }
17746
17747 let table_bytes =
17748 directory_hint_table_bytes(hint_shard_index, entries, shard_row_indexes, string_pool);
17749 let locating = DirectoryHintShardEntry {
17750 hint_shard_index,
17751 first_dir_hash: hash_prefix(&rows.first().unwrap().0),
17752 last_dir_hash: hash_prefix(&rows.last().unwrap().0),
17753 first_block_index: 0,
17754 data_block_count: 1,
17755 parity_block_count: 0,
17756 encrypted_size: 4096,
17757 decompressed_size: table_bytes.len() as u32,
17758 entry_count: rows.len() as u64,
17759 };
17760 DirectoryHintTable::parse(
17761 &table_bytes,
17762 &locating,
17763 shard_count,
17764 MetadataLimits::default(),
17765 )
17766 .unwrap()
17767 }
17768
17769 fn directory_hint_table_bytes(
17770 hint_shard_index: u64,
17771 entries: Vec<DirectoryHintEntry>,
17772 shard_row_indexes: Vec<u32>,
17773 string_pool: Vec<u8>,
17774 ) -> Vec<u8> {
17775 let header_len = DirectoryHintTableHeader {
17776 version: 1,
17777 hint_shard_index,
17778 entry_count: 0,
17779 entry_table_offset: 0,
17780 shard_list_offset: 0,
17781 string_pool_offset: 0,
17782 string_pool_size: 0,
17783 }
17784 .to_bytes()
17785 .len();
17786 let entry_len = entries
17787 .first()
17788 .map(|entry| entry.to_bytes().len())
17789 .unwrap_or(0);
17790 let shard_list_offset = if entries.is_empty() {
17791 0
17792 } else {
17793 header_len + entries.len() * entry_len
17794 };
17795 let string_pool_offset = if string_pool.is_empty() {
17796 0
17797 } else {
17798 shard_list_offset + shard_row_indexes.len() * 4
17799 };
17800
17801 let header = DirectoryHintTableHeader {
17802 version: 1,
17803 hint_shard_index,
17804 entry_count: entries.len() as u64,
17805 entry_table_offset: if entries.is_empty() {
17806 0
17807 } else {
17808 header_len as u64
17809 },
17810 shard_list_offset: shard_list_offset as u64,
17811 string_pool_offset: string_pool_offset as u64,
17812 string_pool_size: string_pool.len() as u64,
17813 };
17814
17815 let mut out = Vec::new();
17816 out.extend_from_slice(&header.to_bytes());
17817 for entry in entries {
17818 out.extend_from_slice(&entry.to_bytes());
17819 }
17820 for row in shard_row_indexes {
17821 out.extend_from_slice(&row.to_le_bytes());
17822 }
17823 out.extend_from_slice(&string_pool);
17824 out
17825 }
17826
17827 fn corrupt_first_block_record_payload(volume: &mut [u8]) {
17828 let (record_offset, _) = first_block_record(volume);
17829 volume[record_offset + 16] ^= 0x55;
17830 }
17831
17832 fn corrupt_block_record_payload_at_slot(volume: &mut [u8], slot: usize) {
17833 let (record_offset, _) = block_record_at_slot(volume, slot);
17834 volume[record_offset + 16] ^= 0x55;
17835 }
17836
17837 fn apply_repair_patches(volume: &mut [u8], patches: &[ArchiveRepairPatch]) {
17838 for patch in patches {
17839 let offset = patch.record_offset as usize;
17840 let end = offset + patch.record_bytes.len();
17841 volume[offset..end].copy_from_slice(&patch.record_bytes);
17842 }
17843 }
17844
17845 fn corrupt_block_record_magic_at_slot(volume: &mut [u8], slot: usize) {
17846 let (record_offset, _) = block_record_at_slot(volume, slot);
17847 volume[record_offset] ^= 0x55;
17848 }
17849
17850 fn corrupt_block_record_reserved_at_slot(volume: &mut [u8], slot: usize) {
17851 let (record_offset, _) = block_record_at_slot(volume, slot);
17852 volume[record_offset + 14] = 0x01;
17853 }
17854
17855 fn corrupt_manifest_footer_hmac(volume: &mut [u8]) {
17856 let manifest_offset = terminal_material_offset(volume);
17857 volume[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
17858 }
17859
17860 fn final_recovery_locator(volume: &[u8]) -> CriticalRecoveryLocator {
17861 let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
17862 CriticalRecoveryLocator::parse(
17863 &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
17864 )
17865 .unwrap()
17866 }
17867
17868 fn rewrite_cmra_parity_count(volume: &[u8], parity_shard_count: u16) -> Vec<u8> {
17869 let locator = final_recovery_locator(volume);
17870 let tuple = CmraDecoderTuple::from(locator);
17871 assert!(parity_shard_count < tuple.parity_shard_count);
17872 let cmra_offset = locator.cmra_offset as usize;
17873 let shard_size = tuple.shard_size as usize;
17874 let row_len = CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size;
17875 let kept_rows = tuple.data_shard_count as usize + parity_shard_count as usize;
17876 let mut header = CriticalMetadataRecoveryHeader::parse(
17877 &volume[cmra_offset..cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN],
17878 )
17879 .unwrap();
17880 header.parity_shard_count = parity_shard_count;
17881
17882 let mut cmra =
17883 Vec::with_capacity(CRITICAL_METADATA_RECOVERY_HEADER_LEN + kept_rows * row_len);
17884 cmra.extend_from_slice(&header.to_bytes());
17885 let rows_start = cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
17886 for row in 0..kept_rows {
17887 let start = rows_start + row * row_len;
17888 cmra.extend_from_slice(&volume[start..start + row_len]);
17889 }
17890
17891 let mut out = Vec::with_capacity(cmra_offset + cmra.len() + LOCATOR_PAIR_LEN);
17892 out.extend_from_slice(&volume[..cmra_offset]);
17893 out.extend_from_slice(&cmra);
17894 let mut mirror = locator;
17895 mirror.locator_sequence = 1;
17896 mirror.cmra_length = cmra.len() as u32;
17897 mirror.cmra_parity_shard_count = parity_shard_count;
17898 out.extend_from_slice(&mirror.to_bytes());
17899 let final_locator = CriticalRecoveryLocator {
17900 volume_format_rev: locator.volume_format_rev,
17901 locator_sequence: 0,
17902 ..mirror
17903 };
17904 out.extend_from_slice(&final_locator.to_bytes());
17905 out
17906 }
17907
17908 fn rewrite_public_cmra_image(
17909 volume: &mut [u8],
17910 mutate: impl FnOnce(&mut CriticalMetadataImage),
17911 ) {
17912 rewrite_cmra_image(volume, CmraRecoveryMode::PublicNoKey, mutate);
17913 }
17914
17915 fn rewrite_root_auth_footer_revision_bytes(bytes: &mut [u8], revision: u16) {
17916 bytes[72..74].copy_from_slice(&revision.to_le_bytes());
17917 let crc_offset = bytes.len() - 4;
17918 let crc = crc32c::crc32c(&bytes[..crc_offset]);
17919 bytes[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
17920 }
17921
17922 fn rewrite_cmra_image(
17923 volume: &mut [u8],
17924 mode: CmraRecoveryMode,
17925 mutate: impl FnOnce(&mut CriticalMetadataImage),
17926 ) {
17927 let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
17928 let locator = final_recovery_locator(volume);
17929 let tuple = CmraDecoderTuple::from(locator);
17930 let recovered = recover_cmra(volume, locator.cmra_offset, Some(tuple), mode).unwrap();
17931 let mut image = recovered.image;
17932 mutate(&mut image);
17933 refresh_critical_image_region_digests(&mut image);
17934 let image_bytes = image.to_bytes().unwrap();
17935 assert_eq!(image_bytes.len(), tuple.image_length as usize);
17936
17937 let shard_size = tuple.shard_size as usize;
17938 let data_shard_count = tuple.data_shard_count as usize;
17939 let parity_shard_count = tuple.parity_shard_count as usize;
17940 assert!(image_bytes.len() <= data_shard_count * shard_size);
17941
17942 let mut data_shards = Vec::with_capacity(data_shard_count);
17943 for idx in 0..data_shard_count {
17944 let start = idx * shard_size;
17945 let end = (start + shard_size).min(image_bytes.len());
17946 let mut payload = vec![0u8; shard_size];
17947 if start < image_bytes.len() {
17948 payload[..end - start].copy_from_slice(&image_bytes[start..end]);
17949 }
17950 data_shards.push(payload);
17951 }
17952 let parity_shards = encode_parity_gf16(&data_shards, parity_shard_count).unwrap();
17953 let image_sha256 = sha256_bytes(&image_bytes);
17954
17955 let header = CriticalMetadataRecoveryHeader {
17956 shard_size: tuple.shard_size,
17957 data_shard_count: tuple.data_shard_count,
17958 parity_shard_count: tuple.parity_shard_count,
17959 image_length: tuple.image_length,
17960 archive_uuid_hint: locator.archive_uuid_hint,
17961 session_id_hint: locator.session_id_hint,
17962 volume_index_hint: locator.volume_index_hint,
17963 image_sha256,
17964 header_crc32c: 0,
17965 };
17966 let mut cmra = Vec::new();
17967 cmra.extend_from_slice(&header.to_bytes());
17968 for (idx, payload) in data_shards.into_iter().enumerate() {
17969 let payload_len = if idx + 1 == data_shard_count {
17970 image_bytes.len() - idx * shard_size
17971 } else {
17972 shard_size
17973 };
17974 cmra.extend_from_slice(
17975 &CriticalMetadataRecoveryShard {
17976 shard_index: idx as u16,
17977 shard_role: 0,
17978 shard_payload_length: payload_len as u32,
17979 payload,
17980 shard_crc32c: 0,
17981 }
17982 .to_bytes(shard_size)
17983 .unwrap(),
17984 );
17985 }
17986 for (idx, payload) in parity_shards.into_iter().enumerate() {
17987 cmra.extend_from_slice(
17988 &CriticalMetadataRecoveryShard {
17989 shard_index: (data_shard_count + idx) as u16,
17990 shard_role: 1,
17991 shard_payload_length: shard_size as u32,
17992 payload,
17993 shard_crc32c: 0,
17994 }
17995 .to_bytes(shard_size)
17996 .unwrap(),
17997 );
17998 }
17999 assert_eq!(cmra.len() as u64, recovered.cmra_length);
18000 let cmra_offset = locator.cmra_offset as usize;
18001 volume[cmra_offset..cmra_offset + cmra.len()].copy_from_slice(&cmra);
18002
18003 rewrite_locator_image_sha(volume, final_offset, image_sha256);
18004 let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
18005 rewrite_locator_image_sha(volume, mirror_offset, image_sha256);
18006 }
18007
18008 fn rewrite_cmra_image_variable_len(
18009 volume: &[u8],
18010 mode: CmraRecoveryMode,
18011 mutate: impl FnOnce(&mut CriticalMetadataImage),
18012 ) -> Vec<u8> {
18013 let locator = final_recovery_locator(volume);
18014 let tuple = CmraDecoderTuple::from(locator);
18015 let recovered = recover_cmra(volume, locator.cmra_offset, Some(tuple), mode).unwrap();
18016 let mut image = recovered.image;
18017 mutate(&mut image);
18018 refresh_critical_image_region_digests(&mut image);
18019 let image_bytes = image.to_bytes().unwrap();
18020
18021 let shard_size = tuple.shard_size as usize;
18022 let data_shard_count = image_bytes.len().div_ceil(shard_size);
18023 let parity_shard_count = tuple.parity_shard_count as usize;
18024 assert!(data_shard_count > 0);
18025 assert!(image_bytes.len() <= data_shard_count * shard_size);
18026
18027 let mut data_shards = Vec::with_capacity(data_shard_count);
18028 for idx in 0..data_shard_count {
18029 let start = idx * shard_size;
18030 let end = (start + shard_size).min(image_bytes.len());
18031 let mut payload = vec![0u8; shard_size];
18032 if start < image_bytes.len() {
18033 payload[..end - start].copy_from_slice(&image_bytes[start..end]);
18034 }
18035 data_shards.push(payload);
18036 }
18037 let parity_shards = encode_parity_gf16(&data_shards, parity_shard_count).unwrap();
18038 let image_sha256 = sha256_bytes(&image_bytes);
18039 let data_shard_count_u16 = u16::try_from(data_shard_count).unwrap();
18040 let image_length_u32 = u32::try_from(image_bytes.len()).unwrap();
18041
18042 let header = CriticalMetadataRecoveryHeader {
18043 shard_size: tuple.shard_size,
18044 data_shard_count: data_shard_count_u16,
18045 parity_shard_count: tuple.parity_shard_count,
18046 image_length: image_length_u32,
18047 archive_uuid_hint: locator.archive_uuid_hint,
18048 session_id_hint: locator.session_id_hint,
18049 volume_index_hint: locator.volume_index_hint,
18050 image_sha256,
18051 header_crc32c: 0,
18052 };
18053 let mut cmra = Vec::new();
18054 cmra.extend_from_slice(&header.to_bytes());
18055 for (idx, payload) in data_shards.into_iter().enumerate() {
18056 let payload_len = if idx + 1 == data_shard_count {
18057 image_bytes.len() - idx * shard_size
18058 } else {
18059 shard_size
18060 };
18061 cmra.extend_from_slice(
18062 &CriticalMetadataRecoveryShard {
18063 shard_index: idx as u16,
18064 shard_role: 0,
18065 shard_payload_length: payload_len as u32,
18066 payload,
18067 shard_crc32c: 0,
18068 }
18069 .to_bytes(shard_size)
18070 .unwrap(),
18071 );
18072 }
18073 for (idx, payload) in parity_shards.into_iter().enumerate() {
18074 cmra.extend_from_slice(
18075 &CriticalMetadataRecoveryShard {
18076 shard_index: (data_shard_count + idx) as u16,
18077 shard_role: 1,
18078 shard_payload_length: shard_size as u32,
18079 payload,
18080 shard_crc32c: 0,
18081 }
18082 .to_bytes(shard_size)
18083 .unwrap(),
18084 );
18085 }
18086
18087 let locator_base = CriticalRecoveryLocator {
18088 volume_format_rev: image.volume_format_rev,
18089 cmra_offset: locator.cmra_offset,
18090 cmra_length: cmra.len() as u32,
18091 volume_trailer_offset: locator.volume_trailer_offset,
18092 body_bytes_before_cmra: locator.body_bytes_before_cmra,
18093 archive_uuid_hint: locator.archive_uuid_hint,
18094 session_id_hint: locator.session_id_hint,
18095 volume_index_hint: locator.volume_index_hint,
18096 locator_sequence: 1,
18097 cmra_shard_size: tuple.shard_size,
18098 cmra_data_shard_count: data_shard_count_u16,
18099 cmra_parity_shard_count: tuple.parity_shard_count,
18100 cmra_image_length: image_length_u32,
18101 cmra_image_sha256: image_sha256,
18102 locator_crc32c: 0,
18103 };
18104
18105 let cmra_offset = locator.cmra_offset as usize;
18106 let mut out = Vec::new();
18107 out.extend_from_slice(&volume[..cmra_offset]);
18108 out.extend_from_slice(&cmra);
18109 out.extend_from_slice(&locator_base.to_bytes());
18110 out.extend_from_slice(
18111 &CriticalRecoveryLocator {
18112 locator_sequence: 0,
18113 ..locator_base
18114 }
18115 .to_bytes(),
18116 );
18117 out
18118 }
18119
18120 fn rewrite_recovery_locator(
18121 volume: &mut [u8],
18122 offset: usize,
18123 mutate: impl FnOnce(&mut CriticalRecoveryLocator),
18124 ) {
18125 let mut locator =
18126 CriticalRecoveryLocator::parse(&volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN])
18127 .unwrap();
18128 mutate(&mut locator);
18129 volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN].copy_from_slice(&locator.to_bytes());
18130 }
18131
18132 fn refresh_critical_image_region_digests(image: &mut CriticalMetadataImage) {
18133 image.volume_header_sha256 = sha256_bytes(
18134 &image
18135 .regions
18136 .iter()
18137 .find(|region| region.region_type == 1)
18138 .unwrap()
18139 .bytes,
18140 );
18141 image.crypto_header_sha256 = sha256_bytes(
18142 &image
18143 .regions
18144 .iter()
18145 .find(|region| region.region_type == 2)
18146 .unwrap()
18147 .bytes,
18148 );
18149 image.key_wrap_table_sha256 = image
18150 .regions
18151 .iter()
18152 .find(|region| region.region_type == 6)
18153 .map(|region| sha256_bytes(®ion.bytes))
18154 .unwrap_or([0u8; 32]);
18155 image.manifest_footer_sha256 = sha256_bytes(
18156 &image
18157 .regions
18158 .iter()
18159 .find(|region| region.region_type == 3)
18160 .unwrap()
18161 .bytes,
18162 );
18163 image.root_auth_footer_sha256 = image
18164 .regions
18165 .iter()
18166 .find(|region| region.region_type == 4)
18167 .map(|region| sha256_bytes(®ion.bytes))
18168 .unwrap_or([0u8; 32]);
18169 image.volume_trailer_sha256 = sha256_bytes(
18170 &image
18171 .regions
18172 .iter()
18173 .find(|region| region.region_type == 5)
18174 .unwrap()
18175 .bytes,
18176 );
18177 }
18178
18179 fn rewrite_locator_image_sha(volume: &mut [u8], offset: usize, image_sha256: [u8; 32]) {
18180 let mut locator =
18181 CriticalRecoveryLocator::parse(&volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN])
18182 .unwrap();
18183 locator.cmra_image_sha256 = image_sha256;
18184 volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN].copy_from_slice(&locator.to_bytes());
18185 }
18186
18187 fn corrupt_v41_terminal_recovery(volume: &mut [u8]) {
18188 let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
18189 let final_locator = CriticalRecoveryLocator::parse(
18190 &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
18191 )
18192 .unwrap();
18193 let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
18194 volume[final_locator.cmra_offset as usize] ^= 0x55;
18195 volume[mirror_offset] ^= 0x55;
18196 volume[final_offset] ^= 0x55;
18197 }
18198
18199 fn mutate_first_block_record(volume: &mut [u8], mutate: impl FnOnce(&mut BlockRecord)) {
18200 let (record_offset, record_len) = first_block_record(volume);
18201 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
18202 let mut record = BlockRecord::parse(
18203 &volume[record_offset..record_offset + record_len],
18204 block_size,
18205 )
18206 .unwrap();
18207 mutate(&mut record);
18208 volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
18209 }
18210
18211 fn mutate_block_record_at_slot(
18212 volume: &mut [u8],
18213 slot: usize,
18214 mutate: impl FnOnce(&mut BlockRecord),
18215 ) {
18216 let (record_offset, record_len) = block_record_at_slot(volume, slot);
18217 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
18218 let mut record = BlockRecord::parse(
18219 &volume[record_offset..record_offset + record_len],
18220 block_size,
18221 )
18222 .unwrap();
18223 mutate(&mut record);
18224 volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
18225 }
18226
18227 fn first_block_record(volume: &[u8]) -> (usize, usize) {
18228 block_record_at_slot(volume, 0)
18229 }
18230
18231 fn block_record_at_slot(volume: &[u8], slot: usize) -> (usize, usize) {
18232 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
18233 let crypto_start = volume_header.crypto_header_offset as usize;
18234 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
18235 let crypto_header = CryptoHeader::parse(
18236 &volume[crypto_start..crypto_end],
18237 volume_header.crypto_header_length,
18238 )
18239 .unwrap();
18240 let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
18241 let record_offset = crypto_end + slot * record_len;
18242 assert!(volume.len() >= record_offset + record_len);
18243 (record_offset, record_len)
18244 }
18245
18246 fn first_block_record_slot_with_kind(volume: &[u8], kind: BlockKind) -> Option<usize> {
18247 block_record_slots(volume)
18248 .into_iter()
18249 .enumerate()
18250 .find_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
18251 }
18252
18253 fn block_record_slots_with_kind(volume: &[u8], kind: BlockKind) -> Vec<usize> {
18254 block_record_slots(volume)
18255 .into_iter()
18256 .enumerate()
18257 .filter_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
18258 .collect()
18259 }
18260
18261 fn first_payload_data_run_slots(volume: &[u8]) -> Vec<usize> {
18262 let mut slots = Vec::new();
18263 for (slot, (_, _, record)) in block_record_slots(volume).into_iter().enumerate() {
18264 if record.kind == BlockKind::PayloadData {
18265 slots.push(slot);
18266 } else if !slots.is_empty() {
18267 break;
18268 }
18269 }
18270 slots
18271 }
18272
18273 fn envelope_indices_for_path(opened: &OpenedArchive, path: &str) -> BTreeSet<u64> {
18274 envelope_entries_for_path(opened, path)
18275 .into_iter()
18276 .map(|entry| entry.envelope_index)
18277 .collect()
18278 }
18279
18280 fn envelope_entries_for_path(opened: &OpenedArchive, path: &str) -> Vec<EnvelopeEntry> {
18281 let normalized =
18282 normalize_lookup_file_path(path, opened.crypto_header.max_path_length).unwrap();
18283 let located = opened.locate_index_file(&normalized).unwrap().unwrap();
18284 let file = &located.shard.files[located.file_index];
18285 frame_range_for_file(&located.shard, file)
18286 .unwrap()
18287 .iter()
18288 .map(|frame| {
18289 located
18290 .shard
18291 .envelopes
18292 .iter()
18293 .find(|entry| entry.envelope_index == frame.envelope_index)
18294 .unwrap()
18295 .clone()
18296 })
18297 .collect()
18298 }
18299
18300 fn block_record_slots(volume: &[u8]) -> Vec<(usize, usize, BlockRecord)> {
18301 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
18302 let crypto_start = volume_header.crypto_header_offset as usize;
18303 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
18304 let crypto_header = CryptoHeader::parse(
18305 &volume[crypto_start..crypto_end],
18306 volume_header.crypto_header_length,
18307 )
18308 .unwrap();
18309 let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
18310 let manifest_offset = terminal_material_offset(volume);
18311 assert_eq!((manifest_offset - crypto_end) % record_len, 0);
18312 let record_count = (manifest_offset - crypto_end) / record_len;
18313 (0..record_count)
18314 .map(|slot| {
18315 let offset = crypto_end + slot * record_len;
18316 let record = BlockRecord::parse(
18317 &volume[offset..offset + record_len],
18318 record_len - BLOCK_RECORD_FRAMING_LEN,
18319 )
18320 .unwrap();
18321 (offset, record_len, record)
18322 })
18323 .collect()
18324 }
18325
18326 fn rewrite_manifest_footer(
18327 volume: &mut [u8],
18328 master_key: &MasterKey,
18329 mutate: impl FnOnce(&mut ManifestFooter),
18330 ) {
18331 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
18332 let offset = terminal_material_offset(volume);
18333 let mut footer =
18334 ManifestFooter::parse(&volume[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
18335 mutate(&mut footer);
18336 footer.manifest_hmac = [0u8; 32];
18337 let mut footer_bytes = footer.to_bytes();
18338 let subkeys = Subkeys::derive(
18339 master_key,
18340 &volume_header.archive_uuid,
18341 &volume_header.session_id,
18342 )
18343 .unwrap();
18344 footer.manifest_hmac = compute_hmac(
18345 HmacDomain::ManifestFooter,
18346 &subkeys.mac_key,
18347 &volume_header.archive_uuid,
18348 &volume_header.session_id,
18349 &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
18350 );
18351 footer_bytes = footer.to_bytes();
18352 volume[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
18353 }
18354
18355 fn rewrite_volume_trailer(
18356 volume: &mut [u8],
18357 master_key: &MasterKey,
18358 mutate: impl FnOnce(&mut VolumeTrailer),
18359 ) {
18360 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
18361 let offset = terminal_material_offset(volume) + MANIFEST_FOOTER_LEN;
18362 let mut trailer =
18363 VolumeTrailer::parse(&volume[offset..offset + VOLUME_TRAILER_LEN]).unwrap();
18364 mutate(&mut trailer);
18365 trailer.trailer_hmac = [0u8; 32];
18366 let mut trailer_bytes = trailer.to_bytes();
18367 let subkeys = Subkeys::derive(
18368 master_key,
18369 &volume_header.archive_uuid,
18370 &volume_header.session_id,
18371 )
18372 .unwrap();
18373 trailer.trailer_hmac = compute_hmac(
18374 HmacDomain::VolumeTrailer,
18375 &subkeys.mac_key,
18376 &volume_header.archive_uuid,
18377 &volume_header.session_id,
18378 &trailer_bytes[..TRAILER_HMAC_COVERED_LEN],
18379 );
18380 trailer_bytes = trailer.to_bytes();
18381 volume[offset..offset + VOLUME_TRAILER_LEN].copy_from_slice(&trailer_bytes);
18382 }
18383
18384 fn rewrite_sidecar_header(
18385 sidecar: &mut [u8],
18386 master_key: &MasterKey,
18387 mutate: impl FnOnce(&mut BootstrapSidecarHeader),
18388 ) {
18389 let mut header =
18390 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
18391 mutate(&mut header);
18392 write_signed_sidecar_header(sidecar, master_key, &mut header);
18393 }
18394
18395 fn write_signed_sidecar_header(
18396 sidecar: &mut [u8],
18397 master_key: &MasterKey,
18398 header: &mut BootstrapSidecarHeader,
18399 ) {
18400 header.sidecar_hmac = [0u8; 32];
18401 let mut header_bytes = header.to_bytes();
18402 let subkeys =
18403 Subkeys::derive(master_key, &header.archive_uuid, &header.session_id).unwrap();
18404 header.sidecar_hmac = compute_hmac(
18405 HmacDomain::BootstrapSidecar,
18406 &subkeys.mac_key,
18407 &header.archive_uuid,
18408 &header.session_id,
18409 &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
18410 );
18411 header_bytes = header.to_bytes();
18412 sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
18413 }
18414
18415 fn sparse_bootstrap_sidecar(
18416 source: &[u8],
18417 master_key: &MasterKey,
18418 include_manifest: bool,
18419 include_index_root: bool,
18420 include_dictionary: bool,
18421 ) -> Vec<u8> {
18422 let source_header =
18423 BootstrapSidecarHeader::parse(&source[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
18424 let mut sidecar = vec![0u8; BOOTSTRAP_SIDECAR_HEADER_LEN];
18425 let mut header = BootstrapSidecarHeader {
18426 archive_uuid: source_header.archive_uuid,
18427 session_id: source_header.session_id,
18428 flags: 0,
18429 manifest_footer_offset: 0,
18430 manifest_footer_length: 0,
18431 index_root_records_offset: 0,
18432 index_root_records_length: 0,
18433 dictionary_records_offset: 0,
18434 dictionary_records_length: 0,
18435 sidecar_hmac: [0u8; 32],
18436 header_crc32c: 0,
18437 };
18438
18439 if include_manifest {
18440 assert!(source_header.has_manifest_footer());
18441 let (offset, length) = append_sidecar_section(
18442 source,
18443 &mut sidecar,
18444 source_header.manifest_footer_offset,
18445 source_header.manifest_footer_length as u64,
18446 );
18447 header.flags |= 0x01;
18448 header.manifest_footer_offset = offset;
18449 header.manifest_footer_length = length as u32;
18450 }
18451 if include_index_root {
18452 assert!(source_header.has_index_root_records());
18453 let (offset, length) = append_sidecar_section(
18454 source,
18455 &mut sidecar,
18456 source_header.index_root_records_offset,
18457 source_header.index_root_records_length,
18458 );
18459 header.flags |= 0x02;
18460 header.index_root_records_offset = offset;
18461 header.index_root_records_length = length;
18462 }
18463 if include_dictionary {
18464 assert!(source_header.has_dictionary_records());
18465 let (offset, length) = append_sidecar_section(
18466 source,
18467 &mut sidecar,
18468 source_header.dictionary_records_offset,
18469 source_header.dictionary_records_length,
18470 );
18471 header.flags |= 0x04;
18472 header.dictionary_records_offset = offset;
18473 header.dictionary_records_length = length;
18474 }
18475
18476 write_signed_sidecar_header(&mut sidecar, master_key, &mut header);
18477 sidecar
18478 }
18479
18480 fn append_sidecar_section(
18481 source: &[u8],
18482 sidecar: &mut Vec<u8>,
18483 source_offset: u64,
18484 length: u64,
18485 ) -> (u64, u64) {
18486 let source_offset = source_offset as usize;
18487 let length = length as usize;
18488 let offset = sidecar.len() as u64;
18489 sidecar.extend_from_slice(&source[source_offset..source_offset + length]);
18490 (offset, length as u64)
18491 }
18492
18493 fn mutate_sidecar_manifest(
18494 sidecar: &mut [u8],
18495 master_key: &MasterKey,
18496 mutate: impl FnOnce(&mut ManifestFooter),
18497 ) {
18498 let header =
18499 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
18500 let offset = header.manifest_footer_offset as usize;
18501 let mut footer =
18502 ManifestFooter::parse(&sidecar[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
18503 mutate(&mut footer);
18504 footer.manifest_hmac = [0u8; 32];
18505 let mut footer_bytes = footer.to_bytes();
18506 let subkeys =
18507 Subkeys::derive(master_key, &footer.archive_uuid, &footer.session_id).unwrap();
18508 footer.manifest_hmac = compute_hmac(
18509 HmacDomain::ManifestFooter,
18510 &subkeys.mac_key,
18511 &footer.archive_uuid,
18512 &footer.session_id,
18513 &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
18514 );
18515 footer_bytes = footer.to_bytes();
18516 sidecar[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
18517 }
18518
18519 fn mutate_sidecar_index_record(
18520 sidecar: &mut [u8],
18521 record_index: usize,
18522 mutate: impl FnOnce(&mut BlockRecord),
18523 ) {
18524 let header =
18525 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
18526 let record_len = sidecar_record_len(sidecar);
18527 let offset = header.index_root_records_offset as usize + record_index * record_len;
18528 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
18529 let mut record =
18530 BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
18531 mutate(&mut record);
18532 sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
18533 }
18534
18535 fn mutate_sidecar_dictionary_record(
18536 sidecar: &mut [u8],
18537 record_index: usize,
18538 mutate: impl FnOnce(&mut BlockRecord),
18539 ) {
18540 let header =
18541 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
18542 let record_len = sidecar_record_len(sidecar);
18543 let offset = header.dictionary_records_offset as usize + record_index * record_len;
18544 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
18545 let mut record =
18546 BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
18547 mutate(&mut record);
18548 sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
18549 }
18550
18551 fn swap_sidecar_index_records(sidecar: &mut [u8], left: usize, right: usize) {
18552 let header =
18553 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
18554 let record_len = sidecar_record_len(sidecar);
18555 let left_offset = header.index_root_records_offset as usize + left * record_len;
18556 let right_offset = header.index_root_records_offset as usize + right * record_len;
18557 for idx in 0..record_len {
18558 sidecar.swap(left_offset + idx, right_offset + idx);
18559 }
18560 }
18561
18562 fn sidecar_record_len(sidecar: &[u8]) -> usize {
18563 let header =
18564 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
18565 let footer_offset = header.manifest_footer_offset as usize;
18566 let footer =
18567 ManifestFooter::parse(&sidecar[footer_offset..footer_offset + MANIFEST_FOOTER_LEN])
18568 .unwrap();
18569 let index_record_count = footer.index_root_data_block_count as usize
18570 + footer.index_root_parity_block_count as usize;
18571 header.index_root_records_length as usize / index_record_count
18572 }
18573
18574 fn corrupt_object_extent_records(volume: &mut [u8], extent: ObjectExtent) {
18575 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
18576 assert_eq!(volume_header.volume_index, 0);
18577 assert_eq!(volume_header.stripe_width, 1);
18578 let crypto_start = volume_header.crypto_header_offset as usize;
18579 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
18580 let crypto_header = CryptoHeader::parse(
18581 &volume[crypto_start..crypto_end],
18582 volume_header.crypto_header_length,
18583 )
18584 .unwrap();
18585 let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
18586 let record_count = extent.data_block_count as u64 + extent.parity_block_count as u64;
18587 for offset in 0..record_count {
18588 let block_index = extent.first_block_index + offset;
18589 let record_offset = crypto_end + block_index as usize * record_len;
18590 volume[record_offset + 16] ^= 0x55;
18591 }
18592 }
18593
18594 fn terminal_material_offset(volume: &[u8]) -> usize {
18595 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
18596 let crypto_start = volume_header.crypto_header_offset as usize;
18597 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
18598 let crypto_header = CryptoHeader::parse(
18599 &volume[crypto_start..crypto_end],
18600 volume_header.crypto_header_length,
18601 )
18602 .unwrap();
18603 let (_, offset, _) = parse_stream_block_prefix(
18604 volume,
18605 crypto_end,
18606 crypto_header.fixed.block_size as usize,
18607 &volume_header,
18608 )
18609 .unwrap();
18610 offset
18611 }
18612
18613 #[derive(Debug)]
18614 struct TestObject {
18615 extent: ObjectExtent,
18616 records: Vec<BlockRecord>,
18617 }
18618
18619 #[derive(Debug)]
18620 struct TestFileMeta {
18621 path: Vec<u8>,
18622 frame_index: u64,
18623 tar_stream_offset: u64,
18624 member_group_size: u64,
18625 file_data_size: u64,
18626 }
18627
18628 fn multi_envelope_reader_fixture() -> (OpenedArchive, u64) {
18629 let volume_header = test_volume_header();
18630 let crypto_header = test_crypto_header();
18631 let subkeys = Subkeys::derive(
18632 &master_key(),
18633 &volume_header.archive_uuid,
18634 &volume_header.session_id,
18635 )
18636 .unwrap();
18637 let mut next_block_index = 0u64;
18638 let mut blocks = BTreeMap::new();
18639
18640 let healthy = test_member(b"healthy.txt", b"healthy payload\n");
18641 let broken = test_member(b"broken.txt", b"broken payload\n");
18642 let tar_stream = [healthy.as_slice(), broken.as_slice()].concat();
18643
18644 let healthy_frame = compress_zstd_frame(&healthy, 1).unwrap();
18645 let broken_frame = compress_zstd_frame(&broken, 1).unwrap();
18646
18647 let healthy_payload = encrypt_test_object(
18648 &healthy_frame,
18649 &subkeys.enc_key,
18650 &subkeys.nonce_seed,
18651 b"envelope",
18652 0,
18653 BlockKind::PayloadData,
18654 &mut next_block_index,
18655 &crypto_header,
18656 &volume_header,
18657 );
18658 let broken_payload = encrypt_test_object(
18659 &broken_frame,
18660 &subkeys.enc_key,
18661 &subkeys.nonce_seed,
18662 b"envelope",
18663 1,
18664 BlockKind::PayloadData,
18665 &mut next_block_index,
18666 &crypto_header,
18667 &volume_header,
18668 );
18669 let broken_payload_block = broken_payload.extent.first_block_index;
18670 insert_records(&mut blocks, &healthy_payload.records);
18671 insert_records(&mut blocks, &broken_payload.records);
18672
18673 let frames = vec![
18674 FrameEntry {
18675 frame_index: 0,
18676 envelope_index: 0,
18677 offset_in_envelope: 0,
18678 compressed_size: healthy_frame.len() as u32,
18679 decompressed_size: healthy.len() as u32,
18680 flags: 0x0000_0003,
18681 tar_stream_offset: 0,
18682 },
18683 FrameEntry {
18684 frame_index: 1,
18685 envelope_index: 1,
18686 offset_in_envelope: 0,
18687 compressed_size: broken_frame.len() as u32,
18688 decompressed_size: broken.len() as u32,
18689 flags: 0x0000_0003,
18690 tar_stream_offset: healthy.len() as u64,
18691 },
18692 ];
18693 let envelopes = vec![
18694 EnvelopeEntry {
18695 envelope_index: 0,
18696 first_block_index: healthy_payload.extent.first_block_index,
18697 data_block_count: healthy_payload.extent.data_block_count,
18698 parity_block_count: 0,
18699 encrypted_size: healthy_payload.extent.encrypted_size,
18700 plaintext_size: healthy_frame.len() as u32,
18701 first_frame_index: 0,
18702 frame_count: 1,
18703 },
18704 EnvelopeEntry {
18705 envelope_index: 1,
18706 first_block_index: broken_payload.extent.first_block_index,
18707 data_block_count: broken_payload.extent.data_block_count,
18708 parity_block_count: 0,
18709 encrypted_size: broken_payload.extent.encrypted_size,
18710 plaintext_size: broken_frame.len() as u32,
18711 first_frame_index: 1,
18712 frame_count: 1,
18713 },
18714 ];
18715 let files = vec![
18716 TestFileMeta {
18717 path: b"healthy.txt".to_vec(),
18718 frame_index: 0,
18719 tar_stream_offset: 0,
18720 member_group_size: healthy.len() as u64,
18721 file_data_size: b"healthy payload\n".len() as u64,
18722 },
18723 TestFileMeta {
18724 path: b"broken.txt".to_vec(),
18725 frame_index: 1,
18726 tar_stream_offset: healthy.len() as u64,
18727 member_group_size: broken.len() as u64,
18728 file_data_size: b"broken payload\n".len() as u64,
18729 },
18730 ];
18731
18732 let (index_shard_plaintext, first_path_hash, last_path_hash) =
18733 build_test_index_shard(&files, &frames, &envelopes);
18734 let index_shard = encrypt_test_object(
18735 &compress_zstd_frame(&index_shard_plaintext, 1).unwrap(),
18736 &subkeys.index_shard_key,
18737 &subkeys.index_nonce_seed,
18738 b"idxshard",
18739 0,
18740 BlockKind::IndexShardData,
18741 &mut next_block_index,
18742 &crypto_header,
18743 &volume_header,
18744 );
18745 insert_records(&mut blocks, &index_shard.records);
18746
18747 let shard_entry = ShardEntry {
18748 shard_index: 0,
18749 first_block_index: index_shard.extent.first_block_index,
18750 data_block_count: index_shard.extent.data_block_count,
18751 parity_block_count: 0,
18752 encrypted_size: index_shard.extent.encrypted_size,
18753 decompressed_size: index_shard_plaintext.len() as u32,
18754 file_count: files.len() as u32,
18755 first_path_hash,
18756 last_path_hash,
18757 };
18758 let mut root_header = IndexRootHeader::empty();
18759 root_header.frame_count = frames.len() as u64;
18760 root_header.envelope_count = envelopes.len() as u64;
18761 root_header.file_count = files.len() as u64;
18762 root_header.payload_block_count = healthy_payload.extent.data_block_count as u64
18763 + broken_payload.extent.data_block_count as u64;
18764 root_header.tar_total_size = tar_stream.len() as u64;
18765 root_header.content_sha256 = sha256_bytes(&tar_stream);
18766 let index_root = IndexRoot {
18767 header: root_header,
18768 shards: vec![shard_entry],
18769 directory_hint_shards: Vec::new(),
18770 };
18771
18772 let index_root_plaintext = index_root.to_bytes();
18773 let index_root_object = encrypt_test_object(
18774 &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
18775 &subkeys.index_root_key,
18776 &subkeys.index_nonce_seed,
18777 b"idxroot",
18778 0,
18779 BlockKind::IndexRootData,
18780 &mut next_block_index,
18781 &crypto_header,
18782 &volume_header,
18783 );
18784 insert_records(&mut blocks, &index_root_object.records);
18785
18786 let archive_uuid = volume_header.archive_uuid;
18787 let session_id = volume_header.session_id;
18788 let opened = OpenedArchive {
18789 options: ReaderOptions::default(),
18790 observed_archive_bytes: 1_000_000,
18791 observed_volume_count: 1,
18792 subkeys,
18793 blocks,
18794 lazy_blocks: None,
18795 crypto_header_bytes: Vec::new(),
18796 volume_header,
18797 crypto_header,
18798 manifest_footer: ManifestFooter {
18799 archive_uuid,
18800 session_id,
18801 volume_index: 0,
18802 is_authoritative: 1,
18803 total_volumes: 1,
18804 index_root_first_block: index_root_object.extent.first_block_index,
18805 index_root_data_block_count: index_root_object.extent.data_block_count,
18806 index_root_parity_block_count: 0,
18807 index_root_encrypted_size: index_root_object.extent.encrypted_size,
18808 index_root_decompressed_size: index_root_plaintext.len() as u32,
18809 manifest_hmac: [0u8; 32],
18810 },
18811 volume_trailer: Some(VolumeTrailer {
18812 archive_uuid,
18813 session_id,
18814 volume_index: 0,
18815 block_count: next_block_index,
18816 bytes_written: 0,
18817 manifest_footer_offset: 0,
18818 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
18819 closed_at_ns: 0,
18820 root_auth_footer_offset: 0,
18821 root_auth_footer_length: 0,
18822 root_auth_flags: 0,
18823 trailer_hmac: [0u8; 32],
18824 }),
18825 root_auth_footer: None,
18826 index_root,
18827 payload_dictionary: None,
18828 };
18829 (opened, broken_payload_block)
18830 }
18831
18832 fn replace_first_index_shard(opened: &mut OpenedArchive, mutate: impl FnOnce(&mut IndexShard)) {
18833 let locating = opened.index_root.shards[0].clone();
18834 let mut shard = opened.load_index_shard(&locating).unwrap();
18835 mutate(&mut shard);
18836 let plaintext = shard.to_bytes();
18837 let mut next_block_index = opened
18838 .blocks
18839 .keys()
18840 .last()
18841 .copied()
18842 .map(|index| index + 1)
18843 .unwrap_or(0);
18844 let replacement = encrypt_test_object(
18845 &compress_zstd_frame(&plaintext, 1).unwrap(),
18846 &opened.subkeys.index_shard_key,
18847 &opened.subkeys.index_nonce_seed,
18848 b"idxshard",
18849 locating.shard_index,
18850 BlockKind::IndexShardData,
18851 &mut next_block_index,
18852 &opened.crypto_header,
18853 &opened.volume_header,
18854 );
18855 insert_records(&mut opened.blocks, &replacement.records);
18856 opened.index_root.shards[0] = ShardEntry {
18857 shard_index: locating.shard_index,
18858 first_block_index: replacement.extent.first_block_index,
18859 data_block_count: replacement.extent.data_block_count,
18860 parity_block_count: 0,
18861 encrypted_size: replacement.extent.encrypted_size,
18862 decompressed_size: plaintext.len() as u32,
18863 file_count: shard.files.len() as u32,
18864 first_path_hash: shard.files.first().unwrap().path_hash,
18865 last_path_hash: shard.files.last().unwrap().path_hash,
18866 };
18867 }
18868
18869 fn rewrite_as_single_healthy_file(
18870 opened: &mut OpenedArchive,
18871 mutate: impl FnOnce(&mut FileEntry, &mut Vec<u8>),
18872 ) {
18873 let healthy_path = b"healthy.txt";
18874 let healthy_payload = b"healthy payload\n";
18875 let healthy_member = test_member(healthy_path, healthy_payload);
18876 replace_first_index_shard(opened, |shard| {
18877 let file_index = (0..shard.files.len())
18878 .find(|idx| shard.file_path(*idx) == Some(healthy_path.as_slice()))
18879 .unwrap();
18880 let mut file = shard.files[file_index].clone();
18881 let frame = shard
18882 .frames
18883 .iter()
18884 .find(|entry| entry.frame_index == 0)
18885 .unwrap()
18886 .clone();
18887 let envelope = shard
18888 .envelopes
18889 .iter()
18890 .find(|entry| entry.envelope_index == 0)
18891 .unwrap()
18892 .clone();
18893 let mut path = healthy_path.to_vec();
18894
18895 file.path_offset = 0;
18896 file.path_length = path.len() as u32;
18897 file.first_frame_index = 0;
18898 file.frame_count = 1;
18899 file.offset_in_first_frame_plaintext = 0;
18900 file.tar_member_group_size = healthy_member.len() as u64;
18901 file.file_data_size = healthy_payload.len() as u64;
18902 file.flags = crate::entry_metadata::EXTENDED_METADATA_V1;
18903 mutate(&mut file, &mut path);
18904 file.path_offset = 0;
18905 file.path_length = path.len() as u32;
18906 file.path_hash = hash_prefix(&path);
18907
18908 shard.files = vec![file];
18909 shard.frames = vec![frame];
18910 shard.envelopes = vec![envelope];
18911 shard.string_pool = path;
18912 });
18913
18914 opened.index_root.header.file_count = 1;
18915 opened.index_root.header.frame_count = 1;
18916 opened.index_root.header.envelope_count = 1;
18917 opened.index_root.header.payload_block_count = 1;
18918 opened.index_root.header.tar_total_size = healthy_member.len() as u64;
18919 opened.index_root.header.content_sha256 = sha256_bytes(&healthy_member);
18920 }
18921
18922 fn test_volume_header() -> VolumeHeader {
18923 VolumeHeader {
18924 format_version: FORMAT_VERSION,
18925 volume_format_rev: VOLUME_FORMAT_REV,
18926 volume_index: 0,
18927 stripe_width: 1,
18928 archive_uuid: [0x31; 16],
18929 session_id: [0x42; 16],
18930 crypto_header_offset: VOLUME_HEADER_LEN as u32,
18931 crypto_header_length: CRYPTO_HEADER_FIXED_LEN as u32,
18932 header_crc32c: 0,
18933 }
18934 }
18935
18936 fn test_crypto_header() -> CryptoHeaderFixed {
18937 CryptoHeaderFixed {
18938 length: CRYPTO_HEADER_FIXED_LEN as u32,
18939 compression_algo: CompressionAlgo::ZstdFramed,
18940 aead_algo: AeadAlgo::AesGcmSiv256,
18941 fec_algo: FecAlgo::ReedSolomonGF16,
18942 kdf_algo: KdfAlgo::Raw,
18943 chunk_size: 4096,
18944 envelope_target_size: 8192,
18945 block_size: 4096,
18946 fec_data_shards: 4,
18947 fec_parity_shards: 0,
18948 index_fec_data_shards: 4,
18949 index_fec_parity_shards: 0,
18950 index_root_fec_data_shards: 4,
18951 index_root_fec_parity_shards: 0,
18952 stripe_width: 1,
18953 volume_loss_tolerance: 0,
18954 bit_rot_buffer_pct: 0,
18955 has_dictionary: 0,
18956 max_path_length: 4096,
18957 expected_volume_size: 0,
18958 }
18959 }
18960
18961 #[allow(clippy::too_many_arguments)]
18962 fn encrypt_test_object(
18963 plaintext: &[u8],
18964 key: &[u8; 32],
18965 nonce_seed: &[u8; 32],
18966 domain: &[u8],
18967 counter: u64,
18968 data_kind: BlockKind,
18969 next_block_index: &mut u64,
18970 crypto_header: &CryptoHeaderFixed,
18971 volume_header: &VolumeHeader,
18972 ) -> TestObject {
18973 let block_size = crypto_header.block_size as usize;
18974 let encrypted = encrypt_padded_aead_object(
18975 AeadObjectContext {
18976 algo: crypto_header.aead_algo,
18977 key,
18978 nonce_seed,
18979 domain,
18980 archive_uuid: &volume_header.archive_uuid,
18981 session_id: &volume_header.session_id,
18982 counter,
18983 },
18984 block_size,
18985 plaintext,
18986 )
18987 .unwrap();
18988 assert_eq!(encrypted.len() % block_size, 0);
18989
18990 let first_block_index = *next_block_index;
18991 let data_block_count = encrypted.len() / block_size;
18992 let records = encrypted
18993 .chunks(block_size)
18994 .enumerate()
18995 .map(|(index, payload)| BlockRecord {
18996 block_index: first_block_index + index as u64,
18997 kind: data_kind,
18998 flags: if index + 1 == data_block_count {
18999 0x01
19000 } else {
19001 0
19002 },
19003 payload: payload.to_vec(),
19004 record_crc32c: 0,
19005 })
19006 .collect::<Vec<_>>();
19007 *next_block_index += data_block_count as u64;
19008
19009 TestObject {
19010 extent: ObjectExtent {
19011 first_block_index,
19012 data_block_count: data_block_count as u32,
19013 parity_block_count: 0,
19014 encrypted_size: encrypted.len() as u32,
19015 },
19016 records,
19017 }
19018 }
19019
19020 fn insert_records(blocks: &mut BTreeMap<u64, BlockRecord>, records: &[BlockRecord]) {
19021 for record in records {
19022 assert!(blocks.insert(record.block_index, record.clone()).is_none());
19023 }
19024 }
19025
19026 #[allow(clippy::too_many_arguments)]
19027 fn build_metadata_object_from_payload(
19028 payload: &[u8],
19029 _subkeys: &Subkeys,
19030 volume_header: &VolumeHeader,
19031 crypto_header: &CryptoHeaderFixed,
19032 key: &[u8; 32],
19033 nonce_seed: &[u8; 32],
19034 domain: &[u8],
19035 counter: u64,
19036 data_kind: BlockKind,
19037 next_block_index: &mut u64,
19038 ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
19039 let compressed = compress_zstd_frame(payload, 1).unwrap();
19040 build_metadata_object_from_compressed(
19041 &compressed,
19042 key,
19043 nonce_seed,
19044 domain,
19045 counter,
19046 data_kind,
19047 next_block_index,
19048 crypto_header,
19049 volume_header,
19050 )
19051 }
19052
19053 #[allow(clippy::too_many_arguments)]
19054 fn build_metadata_object_from_compressed(
19055 compressed: &[u8],
19056 key: &[u8; 32],
19057 nonce_seed: &[u8; 32],
19058 domain: &[u8],
19059 counter: u64,
19060 data_kind: BlockKind,
19061 next_block_index: &mut u64,
19062 crypto_header: &CryptoHeaderFixed,
19063 volume_header: &VolumeHeader,
19064 ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
19065 let object = encrypt_test_object(
19066 compressed,
19067 key,
19068 nonce_seed,
19069 domain,
19070 counter,
19071 data_kind,
19072 next_block_index,
19073 crypto_header,
19074 volume_header,
19075 );
19076
19077 let mut blocks = BTreeMap::new();
19078 for record in object.records {
19079 blocks.insert(record.block_index, record);
19080 }
19081 (object.extent, blocks)
19082 }
19083
19084 #[allow(clippy::too_many_arguments)]
19085 fn assert_metadata_object_from_compressed(
19086 compressed: &[u8],
19087 decompressed_size: usize,
19088 _subkeys: &Subkeys,
19089 volume_header: &VolumeHeader,
19090 crypto_header: &CryptoHeaderFixed,
19091 key: &[u8; 32],
19092 nonce_seed: &[u8; 32],
19093 domain: &[u8],
19094 counter: u64,
19095 data_kind: BlockKind,
19096 parity_kind: BlockKind,
19097 class_data_shards: u16,
19098 class_parity_shards: u16,
19099 next_block_index: &mut u64,
19100 expected: FormatError,
19101 ) {
19102 let (extent, blocks) = build_metadata_object_from_compressed(
19103 compressed,
19104 key,
19105 nonce_seed,
19106 domain,
19107 counter,
19108 data_kind,
19109 next_block_index,
19110 crypto_header,
19111 volume_header,
19112 );
19113 let error = load_metadata_object_from_parts(
19114 &blocks,
19115 ObjectLoadContext {
19116 volume_header,
19117 crypto_header,
19118 extent,
19119 data_kind,
19120 parity_kind,
19121 key,
19122 nonce_seed,
19123 domain,
19124 counter,
19125 class_data_shard_max: class_data_shards,
19126 class_parity_shard_max: class_parity_shards,
19127 },
19128 decompressed_size as u32,
19129 )
19130 .unwrap_err();
19131 assert_eq!(error, expected);
19132 }
19133
19134 fn corrupt_payload_record(blocks: &mut BTreeMap<u64, BlockRecord>, block_index: u64) {
19135 let record = blocks.get_mut(&block_index).unwrap();
19136 assert_eq!(record.kind, BlockKind::PayloadData);
19137 record.payload[0] ^= 0x55;
19138 }
19139
19140 fn build_test_index_shard(
19141 files: &[TestFileMeta],
19142 frames: &[FrameEntry],
19143 envelopes: &[EnvelopeEntry],
19144 ) -> (Vec<u8>, [u8; 8], [u8; 8]) {
19145 let mut sorted = files
19146 .iter()
19147 .map(|file| (hash_prefix(&file.path), file))
19148 .collect::<Vec<_>>();
19149 sorted.sort_by(|left, right| {
19150 (left.0, left.1.path.as_slice(), left.1.tar_stream_offset).cmp(&(
19151 right.0,
19152 right.1.path.as_slice(),
19153 right.1.tar_stream_offset,
19154 ))
19155 });
19156
19157 let mut string_pool = Vec::new();
19158 let mut file_entries = Vec::with_capacity(sorted.len());
19159 for (path_hash, file) in &sorted {
19160 let path_offset = string_pool.len() as u32;
19161 string_pool.extend_from_slice(&file.path);
19162 file_entries.push(FileEntry {
19163 path_hash: *path_hash,
19164 path_offset,
19165 path_length: file.path.len() as u32,
19166 first_frame_index: file.frame_index,
19167 frame_count: 1,
19168 offset_in_first_frame_plaintext: 0,
19169 tar_member_group_size: file.member_group_size,
19170 file_data_size: file.file_data_size,
19171 flags: crate::entry_metadata::EXTENDED_METADATA_V1,
19172 });
19173 }
19174
19175 let header = IndexShardHeader {
19176 version: 1,
19177 shard_index: 0,
19178 file_count: file_entries.len() as u32,
19179 frame_count: frames.len() as u32,
19180 envelope_count: envelopes.len() as u32,
19181 file_table_offset: INDEX_SHARD_HEADER_LEN as u32,
19182 frame_table_offset: (INDEX_SHARD_HEADER_LEN + file_entries.len() * FILE_ENTRY_LEN)
19183 as u32,
19184 envelope_table_offset: (INDEX_SHARD_HEADER_LEN
19185 + file_entries.len() * FILE_ENTRY_LEN
19186 + frames.len() * FRAME_ENTRY_LEN) as u32,
19187 string_pool_offset: (INDEX_SHARD_HEADER_LEN
19188 + file_entries.len() * FILE_ENTRY_LEN
19189 + frames.len() * FRAME_ENTRY_LEN
19190 + envelopes.len() * ENVELOPE_ENTRY_LEN) as u32,
19191 string_pool_size: string_pool.len() as u32,
19192 };
19193
19194 let mut bytes = Vec::new();
19195 bytes.extend_from_slice(&header.to_bytes());
19196 for entry in &file_entries {
19197 bytes.extend_from_slice(&entry.to_bytes());
19198 }
19199 for entry in frames {
19200 bytes.extend_from_slice(&entry.to_bytes());
19201 }
19202 for entry in envelopes {
19203 bytes.extend_from_slice(&entry.to_bytes());
19204 }
19205 bytes.extend_from_slice(&string_pool);
19206
19207 (bytes, sorted.first().unwrap().0, sorted.last().unwrap().0)
19208 }
19209
19210 fn test_member(path: &[u8], data: &[u8]) -> Vec<u8> {
19211 let records =
19212 crate::entry_metadata::portable_primary_pax(path, 0o644, "other", false).unwrap();
19213 let pax = crate::entry_metadata::encode_canonical_pax(&records).unwrap();
19214 let mut out = Vec::new();
19215 out.extend_from_slice(&test_tar_header(
19216 b"TZAP-PAX/PRIMARY",
19217 pax.len() as u64,
19218 0,
19219 b'x',
19220 ));
19221 out.extend_from_slice(&pax);
19222 out.resize(out.len() + padding_to_512(pax.len()), 0);
19223 out.extend_from_slice(&test_tar_header(path, data.len() as u64, 0o644, b'0'));
19224 out.extend_from_slice(data);
19225 out.resize(out.len() + padding_to_512(data.len()), 0);
19226 out
19227 }
19228
19229 fn test_tar_header(path: &[u8], size: u64, mode: u64, typeflag: u8) -> [u8; 512] {
19230 let mut header = [0u8; 512];
19231 header[..path.len()].copy_from_slice(path);
19232 write_test_tar_octal(&mut header[100..108], mode);
19233 write_test_tar_octal(&mut header[108..116], 0);
19234 write_test_tar_octal(&mut header[116..124], 0);
19235 write_test_tar_octal(&mut header[124..136], size);
19236 write_test_tar_octal(&mut header[136..148], 0);
19237 header[148..156].fill(b' ');
19238 header[156] = typeflag;
19239 header[257..263].copy_from_slice(b"ustar\0");
19240 header[263..265].copy_from_slice(b"00");
19241 let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
19242 write_test_tar_checksum(&mut header[148..156], checksum);
19243 header
19244 }
19245
19246 fn write_test_tar_octal(field: &mut [u8], value: u64) {
19247 let digits = format!("{value:o}");
19248 field.fill(0);
19249 let start = field.len() - 1 - digits.len();
19250 field[..start].fill(b'0');
19251 field[start..start + digits.len()].copy_from_slice(digits.as_bytes());
19252 }
19253
19254 fn write_test_tar_checksum(field: &mut [u8], value: u64) {
19255 let digits = format!("{value:06o}");
19256 field[0..6].copy_from_slice(digits.as_bytes());
19257 field[6] = 0;
19258 field[7] = b' ';
19259 }
19260
19261 fn padding_to_512(len: usize) -> usize {
19262 let remainder = len % 512;
19263 if remainder == 0 {
19264 0
19265 } else {
19266 512 - remainder
19267 }
19268 }
19269}