1use std::cell::RefCell;
2use std::collections::{BTreeMap, BTreeSet};
3use std::io::{Cursor, Read};
4use std::rc::Rc;
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7
8use sha2::{Digest, Sha256};
9use unicode_normalization::UnicodeNormalization;
10use uuid::Uuid;
11
12use crate::compression::{
13 compress_zstd_frame_with_dictionary_and_jobs, compress_zstd_frame_with_jobs,
14};
15use crate::crypto::{
16 aead_encrypt, build_aad, compute_integrity_tag, derive_nonce, HmacDomain, KdfParams, MasterKey,
17 Subkeys,
18};
19use crate::entry_metadata::{
20 canonical_base64_encode, encode_canonical_pax, is_source_os,
21 parse_auxiliary_declaration_for_writer, parse_auxiliary_record, parse_primary_metadata,
22 portable_primary_pax, valid_filesystem_token, validate_group_metadata, ArchiveTimestamp,
23 RestoreClass, SparseExtent, CAPTURE_PARTIAL, CAPTURE_REPORT_KIND, EXTENDED_METADATA_V1,
24 HAS_AUXILIARY_STREAMS, HAS_NATIVE_METADATA, HAS_SPARSE_EXTENTS, MAX_SPARSE_EXTENTS,
25 PORTABLE_PROFILE, REQUIRES_SYSTEM_RESTORE,
26};
27use crate::fec::encode_parity_gf16;
28use crate::format::{
29 root_auth_spec_id_for_revision, AeadAlgo, ArchiveWriteError, BlockKind, CompressionAlgo,
30 FecAlgo, FormatError, KdfAlgo, BLOCK_RECORD_FRAMING_LEN, BOOTSTRAP_SIDECAR_HEADER_LEN,
31 CRITICAL_RECOVERY_LOCATOR_LEN, CRYPTO_EXTENSION_HEADER_LEN, CRYPTO_HEADER_FIXED_LEN,
32 CRYPTO_HEADER_HMAC_LEN, FORMAT_VERSION, MANIFEST_FOOTER_LEN, READER_MAX_CMRA_PARITY_PCT,
33 READER_MAX_INDEX_ROOT_FEC_CLASS_SHARDS, READER_MAX_ROOT_AUTH_AUTHENTICATOR_VALUE_LEN,
34 READER_MAX_ROOT_AUTH_FOOTER_LEN, READER_MAX_ROOT_AUTH_SIGNER_IDENTITY_LEN,
35 VOLUME_FORMAT_REV_45, VOLUME_HEADER_LEN, VOLUME_TRAILER_LEN,
36};
37use crate::metadata::{
38 hash_prefix, normalize_lookup_file_path, validate_file_path_bytes, DirectoryHintEntry,
39 DirectoryHintShardEntry, DirectoryHintTableHeader, EnvelopeEntry, FileEntry, FrameEntry,
40 IndexRoot, IndexRootHeader, IndexShardHeader, ShardEntry, DIRECTORY_HINT_ENTRY_LEN,
41 DIRECTORY_HINT_TABLE_LEN, ENVELOPE_ENTRY_LEN, FILE_ENTRY_LEN, FRAME_ENTRY_LEN,
42 INDEX_SHARD_HEADER_LEN,
43};
44use crate::padding::suffix_pad_for_aead;
45use crate::root_auth::{
46 archive_root_for_revision, critical_metadata_digest, data_block_merkle_leaf_hash_for_revision,
47 data_block_merkle_root_from_leaf_hashes_for_revision, fec_layout_digest_for_revision,
48 index_digest_for_revision, root_auth_descriptor_digest_for_revision, signer_identity_digest,
49 ArchiveRootInputs, CriticalMetadataDigestInputs, FecLayoutObjectRow,
50};
51use crate::wire::{
52 compute_key_wrap_table_digest, BlockRecord, BootstrapSidecarHeader, CriticalMetadataImage,
53 CriticalMetadataRecoveryHeader, CriticalMetadataRecoveryShard, CriticalRecoveryLocator,
54 CryptoHeader, CryptoHeaderFixed, KeyWrapTableV1, ManifestFooter, RecipientRecordV1,
55 RootAuthFooterV1, SerializedRegion, VolumeHeader, VolumeTrailer,
56};
57
58const TAR_BLOCK_LEN: usize = 512;
59const MAX_REED_SOLOMON_GF16_SHARDS: u64 = 65_535;
60const MIN_BLOCK_SIZE: u32 = 4096;
61const DEFAULT_BLOCK_SIZE: u32 = 64 * 1024;
62const DEFAULT_CHUNK_SIZE: u32 = 256 * 1024;
63const DEFAULT_ENVELOPE_TARGET_SIZE: u32 = 1024 * 1024;
64const DEFAULT_FEC_DATA_SHARDS: u16 = 224;
65const DEFAULT_FEC_PARITY_SHARDS: u16 = 1;
66const DEFAULT_INDEX_FEC_DATA_SHARDS: u16 = 16;
67const DEFAULT_INDEX_FEC_PARITY_SHARDS: u16 = 1;
68const MIN_INDEX_ROOT_FEC_DATA_SHARDS: u16 = 16;
69const DEFAULT_INDEX_ROOT_FEC_DATA_SHARDS: u16 = MIN_INDEX_ROOT_FEC_DATA_SHARDS;
70const DEFAULT_INDEX_ROOT_FEC_PARITY_SHARDS: u16 = 1;
71const DEFAULT_STRIPE_WIDTH: u32 = 8;
72const DEFAULT_VOLUME_LOSS_TOLERANCE: u8 = 1;
73const DEFAULT_BIT_ROT_BUFFER_PCT: u8 = 5;
74const DEFAULT_FILES_PER_INDEX_SHARD: usize = 10_000;
75const DIRECTORY_HINT_REQUIRED_FILE_COUNT: usize = 100_000;
76const MAX_FILES_PER_INDEX_SHARD: usize = 1_000_000;
77const MAX_HASH_PREFIX_RUN_FILES: usize = 50_000;
78const DEFAULT_DIRECTORY_HINT_ENTRIES_PER_SHARD: usize = 10_000;
79const CMRA_SHARD_SIZE: usize = 512;
80
81#[derive(Clone, Default)]
82pub enum KeyWrapRecordSource {
83 #[default]
84 None,
85 Fixed(Vec<RecipientRecordV1>),
86 Callback(Arc<dyn Fn() -> Result<Vec<RecipientRecordV1>, FormatError> + Send + Sync>),
87}
88
89impl KeyWrapRecordSource {
90 pub fn fixed(records: Vec<RecipientRecordV1>) -> Self {
91 Self::Fixed(records)
92 }
93
94 pub fn callback<F>(callback: F) -> Self
95 where
96 F: Fn() -> Result<Vec<RecipientRecordV1>, FormatError> + Send + Sync + 'static,
97 {
98 Self::Callback(Arc::new(callback))
99 }
100
101 fn resolve(&self) -> Result<Option<Vec<RecipientRecordV1>>, FormatError> {
102 match self {
103 Self::None => Ok(None),
104 Self::Fixed(records) => Ok(Some(records.clone())),
105 Self::Callback(callback) => Ok(Some(callback()?)),
106 }
107 }
108}
109
110fn default_jobs() -> usize {
111 std::thread::available_parallelism()
112 .map(|jobs| jobs.get())
113 .unwrap_or(1)
114}
115
116fn volume_format_revision_for_options(_options: &WriterOptions, _kdf_params: &KdfParams) -> u16 {
117 VOLUME_FORMAT_REV_45
119}
120
121fn resolve_key_wrap_artifacts(
122 kdf_params: &KdfParams,
123 archive_uuid: &[u8; 16],
124 session_id: &[u8; 16],
125 key_wrap_records: Option<&KeyWrapRecordSource>,
126) -> Result<(KdfParams, Option<Vec<u8>>), FormatError> {
127 match kdf_params {
128 KdfParams::RecipientWrap {
129 key_wrap_table_length,
130 key_wrap_table_record_count,
131 key_wrap_table_version,
132 ..
133 } => {
134 if *key_wrap_table_version != 1 {
135 return Err(FormatError::InvalidKdfParams(
136 "recipient-wrap table version must be 1",
137 ));
138 }
139 let Some(records) = key_wrap_records
140 .ok_or(FormatError::WriterUnsupported(
141 "RecipientWrap requires key-wrap records",
142 ))?
143 .resolve()?
144 else {
145 return Err(FormatError::WriterUnsupported(
146 "RecipientWrap requires key-wrap records",
147 ));
148 };
149 if records.is_empty() {
150 return Err(FormatError::WriterUnsupported(
151 "RecipientWrap requires at least one recipient record",
152 ));
153 }
154 let mut recipient_records = Vec::with_capacity(records.len());
155 for record in records {
156 let mut prepared = record.clone();
157 let record_length = u32_len(prepared.to_bytes()?.len(), "RecipientWrap record")?;
158 prepared = prepared.with_record_length(record_length);
159 recipient_records.push(prepared);
160 }
161 let declared_record_count = u32_len(
162 recipient_records.len(),
163 "KeyWrapTableV1 recipient_record_count",
164 )?;
165 if *key_wrap_table_record_count != 0
166 && *key_wrap_table_record_count != declared_record_count
167 {
168 return Err(FormatError::InvalidKdfParams(
169 "recipient-wrap key_wrap_table_record_count mismatch",
170 ));
171 }
172
173 let key_wrap_table = KeyWrapTableV1 {
174 version: *key_wrap_table_version,
175 volume_format_rev: VOLUME_FORMAT_REV_45,
176 table_length: 0,
177 flags: 0,
178 archive_uuid: *archive_uuid,
179 session_id: *session_id,
180 recipient_record_count: declared_record_count,
181 records_offset: 96,
182 records_length: 0,
183 recipient_records,
184 }
185 .to_bytes()?;
186 let computed_key_wrap_table_length =
187 u32_len(key_wrap_table.len(), "KeyWrapTableV1 table_length")?;
188 if *key_wrap_table_length != 0
189 && computed_key_wrap_table_length != *key_wrap_table_length
190 {
191 return Err(FormatError::InvalidKdfParams(
192 "recipient-wrap key_wrap_table_length mismatch",
193 ));
194 }
195 let key_wrap_table_digest =
196 compute_key_wrap_table_digest(computed_key_wrap_table_length, &key_wrap_table);
197 Ok((
198 KdfParams::RecipientWrap {
199 key_wrap_table_length: computed_key_wrap_table_length,
200 key_wrap_table_record_count: declared_record_count,
201 key_wrap_table_version: *key_wrap_table_version,
202 key_wrap_table_digest,
203 },
204 Some(key_wrap_table),
205 ))
206 }
207 _ => Ok((kdf_params.clone(), None)),
208 }
209}
210
211fn recipient_wrap_kdf_params_for_record_count(
212 record_count: usize,
213) -> Result<KdfParams, FormatError> {
214 Ok(KdfParams::RecipientWrap {
215 key_wrap_table_length: 0,
216 key_wrap_table_record_count: u32_len(
217 record_count,
218 "KeyWrapTableV1 recipient_record_count",
219 )?,
220 key_wrap_table_version: 1,
221 key_wrap_table_digest: [0u8; 32],
222 })
223}
224
225fn stabilized_key_wrap_record_source(
226 kdf_params: &KdfParams,
227 key_wrap_records: Option<&KeyWrapRecordSource>,
228) -> Result<Option<KeyWrapRecordSource>, FormatError> {
229 if !matches!(kdf_params, KdfParams::RecipientWrap { .. }) {
230 return Ok(None);
231 }
232 let Some(records) = key_wrap_records
233 .ok_or(FormatError::WriterUnsupported(
234 "RecipientWrap requires key-wrap records",
235 ))?
236 .resolve()?
237 else {
238 return Err(FormatError::WriterUnsupported(
239 "RecipientWrap requires key-wrap records",
240 ));
241 };
242 Ok(Some(KeyWrapRecordSource::fixed(records)))
243}
244
245fn should_emit_directory_hints(file_count: usize) -> bool {
246 file_count > DIRECTORY_HINT_REQUIRED_FILE_COUNT
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq)]
250pub struct WriterOptions {
251 pub block_size: u32,
252 pub chunk_size: u32,
253 pub envelope_target_size: u32,
254 pub stripe_width: u32,
255 pub volume_loss_tolerance: u8,
256 pub bit_rot_buffer_pct: u8,
257 pub zstd_level: i32,
258 pub jobs: usize,
259 pub aead_algo: AeadAlgo,
260 pub fec_data_shards: u16,
261 pub fec_parity_shards: u16,
262 pub index_fec_data_shards: u16,
263 pub index_fec_parity_shards: u16,
264 pub index_root_fec_data_shards: u16,
265 pub index_root_fec_parity_shards: u16,
266 pub max_path_length: u32,
267 pub target_volume_size: Option<u64>,
268 pub archive_uuid: Option<[u8; 16]>,
269 pub session_id: Option<[u8; 16]>,
270 pub closed_at_ns: i64,
271}
272
273impl Default for WriterOptions {
274 fn default() -> Self {
275 Self {
276 block_size: DEFAULT_BLOCK_SIZE,
277 chunk_size: DEFAULT_CHUNK_SIZE,
278 envelope_target_size: DEFAULT_ENVELOPE_TARGET_SIZE,
279 stripe_width: DEFAULT_STRIPE_WIDTH,
280 volume_loss_tolerance: DEFAULT_VOLUME_LOSS_TOLERANCE,
281 bit_rot_buffer_pct: DEFAULT_BIT_ROT_BUFFER_PCT,
282 zstd_level: 3,
283 jobs: default_jobs(),
284 aead_algo: AeadAlgo::AesGcmSiv256,
285 fec_data_shards: DEFAULT_FEC_DATA_SHARDS,
286 fec_parity_shards: DEFAULT_FEC_PARITY_SHARDS,
287 index_fec_data_shards: DEFAULT_INDEX_FEC_DATA_SHARDS,
288 index_fec_parity_shards: DEFAULT_INDEX_FEC_PARITY_SHARDS,
289 index_root_fec_data_shards: DEFAULT_INDEX_ROOT_FEC_DATA_SHARDS,
290 index_root_fec_parity_shards: DEFAULT_INDEX_ROOT_FEC_PARITY_SHARDS,
291 max_path_length: 4096,
292 target_volume_size: None,
293 archive_uuid: None,
294 session_id: None,
295 closed_at_ns: 0,
296 }
297 }
298}
299
300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301pub struct RootAuthWriterConfig<'a> {
302 pub authenticator_id: u16,
303 pub signer_identity_type: u16,
304 pub signer_identity: &'a [u8],
305 pub authenticator_value_length: u32,
306}
307
308#[derive(Debug, Clone, Copy, PartialEq, Eq)]
309pub struct RootAuthSigningRequest {
310 pub root_auth_spec_id: [u8; 24],
311 pub archive_uuid: [u8; 16],
312 pub session_id: [u8; 16],
313 pub archive_root: [u8; 32],
314}
315
316pub type RootAuthAuthenticator<'a> =
317 dyn FnMut(&RootAuthSigningRequest) -> Result<Vec<u8>, FormatError> + 'a;
318
319#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
320pub enum PortableModeOrigin {
321 Native,
322 #[default]
323 Projected,
324}
325
326#[derive(Debug, Clone, PartialEq, Eq)]
327pub struct PortablePosixOwner {
328 pub uid: u64,
329 pub gid: u64,
330 pub uname: Option<String>,
331 pub gname: Option<String>,
332}
333
334#[derive(Debug, Clone, Copy, PartialEq, Eq)]
340pub enum NativeAuxiliaryNameEncoding {
341 None,
342 Utf8,
343 Utf16Le,
344 Bytes,
345}
346
347#[derive(Debug, Clone, PartialEq, Eq)]
348pub struct NativeAuxiliaryMetadata {
349 pub kind: String,
350 pub profile: String,
351 pub restore_class: RestoreClass,
352 pub native: bool,
353 pub name_encoding: NativeAuxiliaryNameEncoding,
354 pub name: Vec<u8>,
356 pub flags: u64,
357 pub logical_size: u64,
358 pub payload: Vec<u8>,
359 pub meta: BTreeMap<String, Vec<u8>>,
360 streamed_payload: Option<StreamedAuxiliaryPayload>,
361}
362
363#[derive(Debug, Clone, PartialEq, Eq)]
364struct StreamedAuxiliaryPayload {
365 stored_size: u64,
366 sha256: [u8; 32],
367 sparse_extents: Option<Vec<SparseExtent>>,
368}
369
370impl NativeAuxiliaryMetadata {
371 pub fn new(
372 kind: impl Into<String>,
373 profile: impl Into<String>,
374 restore_class: RestoreClass,
375 payload: Vec<u8>,
376 ) -> Self {
377 let logical_size = payload.len() as u64;
378 Self {
379 kind: kind.into(),
380 profile: profile.into(),
381 restore_class,
382 native: true,
383 name_encoding: NativeAuxiliaryNameEncoding::None,
384 name: Vec::new(),
385 flags: 0,
386 logical_size,
387 payload,
388 meta: BTreeMap::new(),
389 streamed_payload: None,
390 }
391 }
392
393 pub fn new_streamed(
396 kind: impl Into<String>,
397 profile: impl Into<String>,
398 restore_class: RestoreClass,
399 stored_size: u64,
400 sha256: [u8; 32],
401 ) -> Self {
402 Self {
403 kind: kind.into(),
404 profile: profile.into(),
405 restore_class,
406 native: true,
407 name_encoding: NativeAuxiliaryNameEncoding::None,
408 name: Vec::new(),
409 flags: 0,
410 logical_size: stored_size,
411 payload: Vec::new(),
412 meta: BTreeMap::new(),
413 streamed_payload: Some(StreamedAuxiliaryPayload {
414 stored_size,
415 sha256,
416 sparse_extents: None,
417 }),
418 }
419 }
420
421 pub fn new_streamed_sparse(
422 kind: impl Into<String>,
423 profile: impl Into<String>,
424 restore_class: RestoreClass,
425 logical_size: u64,
426 sparse_extents: Vec<SparseExtent>,
427 sha256: [u8; 32],
428 ) -> Result<Self, FormatError> {
429 let map = encode_v45_sparse_map(&sparse_extents, logical_size)?;
430 let extent_bytes = sparse_extent_bytes(&sparse_extents, logical_size)?;
431 let stored_size = checked_u64_add(map.len() as u64, extent_bytes, "sparse auxiliary")?;
432 Ok(Self {
433 kind: kind.into(),
434 profile: profile.into(),
435 restore_class,
436 native: true,
437 name_encoding: NativeAuxiliaryNameEncoding::None,
438 name: Vec::new(),
439 flags: 1,
440 logical_size,
441 payload: Vec::new(),
442 meta: BTreeMap::new(),
443 streamed_payload: Some(StreamedAuxiliaryPayload {
444 stored_size,
445 sha256,
446 sparse_extents: Some(sparse_extents),
447 }),
448 })
449 }
450
451 pub fn is_streamed(&self) -> bool {
452 self.streamed_payload.is_some()
453 }
454
455 pub fn streamed_sparse_extents(&self) -> Option<&[SparseExtent]> {
456 self.streamed_payload
457 .as_ref()
458 .and_then(|payload| payload.sparse_extents.as_deref())
459 }
460
461 pub fn stored_payload_size(&self) -> u64 {
462 self.streamed_payload
463 .as_ref()
464 .map_or(self.payload.len() as u64, |payload| payload.stored_size)
465 }
466
467 fn sha256(&self) -> [u8; 32] {
468 self.streamed_payload.as_ref().map_or_else(
469 || Sha256::digest(&self.payload).into(),
470 |payload| payload.sha256,
471 )
472 }
473}
474
475#[derive(Debug, Clone, PartialEq, Eq, Default)]
476pub struct NativeFileMetadata {
477 pub required_profiles: Vec<String>,
478 pub optional_profiles: Vec<String>,
479 pub primary_pax_records: BTreeMap<String, Vec<u8>>,
480 pub auxiliary_records: Vec<NativeAuxiliaryMetadata>,
481}
482
483#[derive(Debug, Clone, PartialEq, Eq)]
484pub struct PortableFileMetadata {
485 pub source_os: String,
486 pub source_filesystem: String,
487 pub mode_origin: PortableModeOrigin,
488 pub posix_owner: Option<PortablePosixOwner>,
489 pub attributes: Option<u32>,
490 pub native: NativeFileMetadata,
491}
492
493#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
499pub enum SourceEntryKind {
500 #[default]
501 Regular,
502 Directory,
503 Symlink,
504 Hardlink,
505 CharacterDevice,
506 BlockDevice,
507 Fifo,
508 ReparseDirectory,
511 ReparseRegular,
514}
515
516impl Default for PortableFileMetadata {
517 fn default() -> Self {
518 Self {
519 source_os: "other".into(),
520 source_filesystem: "unknown".into(),
521 mode_origin: PortableModeOrigin::Projected,
522 posix_owner: None,
523 attributes: None,
524 native: NativeFileMetadata::default(),
525 }
526 }
527}
528
529#[derive(Debug, Clone, PartialEq, Eq)]
530pub struct RegularFile<'a> {
531 pub path: &'a str,
532 pub contents: &'a [u8],
533 pub mode: u32,
534 pub mtime: ArchiveTimestamp,
535 pub portable_metadata: PortableFileMetadata,
536}
537
538impl<'a> RegularFile<'a> {
539 pub fn new(path: &'a str, contents: &'a [u8]) -> Self {
540 Self {
541 path,
542 contents,
543 mode: 0o644,
544 mtime: ArchiveTimestamp::UNIX_EPOCH,
545 portable_metadata: PortableFileMetadata::default(),
546 }
547 }
548}
549
550pub trait RegularFileSource {
556 fn archive_path(&self) -> &str;
557 fn entry_kind(&self) -> SourceEntryKind {
558 SourceEntryKind::Regular
559 }
560 fn link_target(&self) -> Option<&[u8]> {
562 None
563 }
564 fn file_data_size(&self) -> u64;
565 fn sparse_extents(&self) -> Option<&[SparseExtent]> {
571 None
572 }
573 fn mode(&self) -> u32;
574 fn mtime(&self) -> ArchiveTimestamp;
575 fn portable_metadata(&self) -> PortableFileMetadata {
576 PortableFileMetadata::default()
577 }
578 fn open(&self) -> Result<Box<dyn Read + '_>, ArchiveWriteError>;
579 fn open_auxiliary(&self, ordinal: usize) -> Result<Box<dyn Read + '_>, ArchiveWriteError> {
585 let metadata = self.portable_metadata();
586 let record =
587 metadata
588 .native
589 .auxiliary_records
590 .get(ordinal)
591 .ok_or(FormatError::WriterInvariant(
592 "auxiliary source ordinal is missing",
593 ))?;
594 if record.is_streamed() {
595 return Err(FormatError::WriterUnsupported(
596 "streamed auxiliary source did not implement open_auxiliary",
597 )
598 .into());
599 }
600 Ok(Box::new(Cursor::new(record.payload.clone())))
601 }
602}
603
604impl RegularFileSource for RegularFile<'_> {
605 fn archive_path(&self) -> &str {
606 self.path
607 }
608
609 fn file_data_size(&self) -> u64 {
610 self.contents.len() as u64
611 }
612
613 fn mode(&self) -> u32 {
614 self.mode
615 }
616
617 fn mtime(&self) -> ArchiveTimestamp {
618 self.mtime
619 }
620
621 fn portable_metadata(&self) -> PortableFileMetadata {
622 self.portable_metadata.clone()
623 }
624
625 fn open(&self) -> Result<Box<dyn Read + '_>, ArchiveWriteError> {
626 Ok(Box::new(Cursor::new(self.contents)))
627 }
628}
629
630#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
632pub enum ArchiveWritePhase {
633 PlanningPayload,
635 PlanningMetadata,
637 EmittingPayload,
639 EmittingMetadata,
641}
642
643pub trait ArchiveWriteProgressSink {
652 fn phase_started(&mut self, phase: ArchiveWritePhase);
654
655 fn source_bytes_read(&mut self, phase: ArchiveWritePhase, archive_path: &str, bytes: u64);
657}
658
659pub(crate) struct SourceProgressState<'a> {
660 sink: &'a mut dyn ArchiveWriteProgressSink,
661 active_phase: Option<ArchiveWritePhase>,
662 phase_reported_by_path: BTreeMap<String, u64>,
663}
664
665impl<'a> SourceProgressState<'a> {
666 fn new(sink: &'a mut dyn ArchiveWriteProgressSink) -> Self {
667 Self {
668 sink,
669 active_phase: None,
670 phase_reported_by_path: BTreeMap::new(),
671 }
672 }
673
674 fn start_phase(&mut self, phase: ArchiveWritePhase) {
675 self.active_phase = Some(phase);
676 self.phase_reported_by_path.clear();
677 self.sink.phase_started(phase);
678 }
679
680 fn record(&mut self, archive_path: &str, bytes: u64, file_data_size: u64) {
681 if bytes == 0 || file_data_size == 0 {
682 return;
683 }
684 if let Some(phase) = self.active_phase {
685 let phase_reported = self
686 .phase_reported_by_path
687 .entry(archive_path.to_owned())
688 .or_default();
689 let capped_next = phase_reported.saturating_add(bytes).min(file_data_size);
690 let delta = capped_next.saturating_sub(*phase_reported);
691 if delta > 0 {
692 *phase_reported = capped_next;
693 self.sink.source_bytes_read(phase, archive_path, delta);
694 }
695 }
696 }
697}
698
699struct ProgressRegularFileSource<'a, S> {
700 inner: &'a S,
701 source_bytes: u64,
702 state: Rc<RefCell<SourceProgressState<'a>>>,
703}
704
705impl<S: RegularFileSource> RegularFileSource for ProgressRegularFileSource<'_, S> {
706 fn archive_path(&self) -> &str {
707 self.inner.archive_path()
708 }
709
710 fn entry_kind(&self) -> SourceEntryKind {
711 self.inner.entry_kind()
712 }
713
714 fn link_target(&self) -> Option<&[u8]> {
715 self.inner.link_target()
716 }
717
718 fn file_data_size(&self) -> u64 {
719 self.inner.file_data_size()
720 }
721
722 fn sparse_extents(&self) -> Option<&[SparseExtent]> {
723 self.inner.sparse_extents()
724 }
725
726 fn mode(&self) -> u32 {
727 self.inner.mode()
728 }
729
730 fn mtime(&self) -> ArchiveTimestamp {
731 self.inner.mtime()
732 }
733
734 fn portable_metadata(&self) -> PortableFileMetadata {
735 self.inner.portable_metadata()
736 }
737
738 fn open(&self) -> Result<Box<dyn Read + '_>, ArchiveWriteError> {
739 Ok(Box::new(SourceProgressReader {
740 inner: self.inner.open()?,
741 archive_path: self.inner.archive_path().to_owned(),
742 source_bytes: self.source_bytes,
743 state: Rc::clone(&self.state),
744 }))
745 }
746
747 fn open_auxiliary(&self, ordinal: usize) -> Result<Box<dyn Read + '_>, ArchiveWriteError> {
748 Ok(Box::new(SourceProgressReader {
749 inner: self.inner.open_auxiliary(ordinal)?,
750 archive_path: self.inner.archive_path().to_owned(),
751 source_bytes: self.source_bytes,
752 state: Rc::clone(&self.state),
753 }))
754 }
755}
756
757struct SourceProgressReader<'a> {
758 inner: Box<dyn Read + 'a>,
759 archive_path: String,
760 source_bytes: u64,
761 state: Rc<RefCell<SourceProgressState<'a>>>,
762}
763
764impl Read for SourceProgressReader<'_> {
765 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
766 let bytes = self.inner.read(buf)?;
767 self.state
768 .borrow_mut()
769 .record(&self.archive_path, bytes as u64, self.source_bytes);
770 Ok(bytes)
771 }
772}
773
774pub(crate) type SourceProgressHandle<'a> = Rc<RefCell<SourceProgressState<'a>>>;
775
776fn progress_sources<'a, S: RegularFileSource>(
777 files: &'a [S],
778 sink: &'a mut dyn ArchiveWriteProgressSink,
779) -> (
780 Vec<ProgressRegularFileSource<'a, S>>,
781 SourceProgressHandle<'a>,
782) {
783 let state = Rc::new(RefCell::new(SourceProgressState::new(sink)));
784 let sources = files
785 .iter()
786 .map(|inner| {
787 let source_bytes = inner
788 .portable_metadata()
789 .native
790 .auxiliary_records
791 .iter()
792 .filter(|record| record.is_streamed())
793 .fold(inner.file_data_size(), |total, record| {
794 total.saturating_add(record.stored_payload_size())
795 });
796 ProgressRegularFileSource {
797 inner,
798 source_bytes,
799 state: Rc::clone(&state),
800 }
801 })
802 .collect();
803 (sources, state)
804}
805
806pub trait ArchiveWriteSink {
811 fn begin_archive(&mut self, volume_count: usize) -> Result<(), ArchiveWriteError>;
812 fn write_volume(&mut self, volume_index: usize, bytes: &[u8]) -> Result<(), ArchiveWriteError>;
813 fn write_bootstrap_sidecar(&mut self, bytes: &[u8]) -> Result<(), ArchiveWriteError>;
814}
815
816#[derive(Debug, Clone, PartialEq, Eq)]
817pub struct WrittenArchiveSummary {
818 pub volume_count: usize,
819 pub archive_bytes: u64,
820 pub bootstrap_sidecar_bytes: u64,
821 pub archive_uuid: [u8; 16],
822 pub session_id: [u8; 16],
823 pub timings: WriterTimings,
824}
825
826#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
827pub struct WriterTimings {
828 pub total: Duration,
829 pub plan_payload: Duration,
830 pub plan_metadata: Duration,
831 pub emit_payload: Duration,
832 pub emit_metadata: Duration,
833}
834
835impl WriterTimings {
836 fn add_assign(&mut self, other: Self) {
837 self.total += other.total;
838 self.plan_payload += other.plan_payload;
839 self.plan_metadata += other.plan_metadata;
840 self.emit_payload += other.emit_payload;
841 self.emit_metadata += other.emit_metadata;
842 }
843}
844
845#[derive(Debug, Clone, Default, PartialEq, Eq)]
847pub struct MemoryArchiveSink {
848 pub volumes: Vec<Vec<u8>>,
849 pub bootstrap_sidecar: Vec<u8>,
850}
851
852impl ArchiveWriteSink for MemoryArchiveSink {
853 fn begin_archive(&mut self, volume_count: usize) -> Result<(), ArchiveWriteError> {
854 self.volumes = vec![Vec::new(); volume_count];
855 self.bootstrap_sidecar.clear();
856 Ok(())
857 }
858
859 fn write_volume(&mut self, volume_index: usize, bytes: &[u8]) -> Result<(), ArchiveWriteError> {
860 let volume = self
861 .volumes
862 .get_mut(volume_index)
863 .ok_or(FormatError::WriterInvariant(
864 "volume sink index is out of bounds",
865 ))?;
866 volume.extend_from_slice(bytes);
867 Ok(())
868 }
869
870 fn write_bootstrap_sidecar(&mut self, bytes: &[u8]) -> Result<(), ArchiveWriteError> {
871 self.bootstrap_sidecar.extend_from_slice(bytes);
872 Ok(())
873 }
874}
875
876#[derive(Debug, Clone, PartialEq, Eq)]
881pub struct WrittenArchive {
882 pub bytes: Vec<u8>,
883 pub volumes: Vec<Vec<u8>>,
884 pub bootstrap_sidecar: Vec<u8>,
885 pub archive_uuid: [u8; 16],
886 pub session_id: [u8; 16],
887 pub timings: WriterTimings,
888}
889
890#[derive(Debug, Clone)]
891struct TarMember {
892 path: Vec<u8>,
893 entry_kind: SourceEntryKind,
894 link_target: Option<Vec<u8>>,
895 tar_member_group_start: u64,
896 tar_member_group_size: u64,
897 file_data_size: u64,
898 sparse_extents: Option<Vec<SparseExtent>>,
899 mode: u32,
900 mtime: ArchiveTimestamp,
901 portable_metadata: PortableFileMetadata,
902}
903
904#[derive(Debug, Clone)]
905struct PayloadFrame {
906 frame_index: u64,
907 envelope_index: u64,
908 member_index: usize,
909 offset_in_envelope: u32,
910 compressed_size: u32,
911 decompressed_size: u32,
912 flags: u32,
913 tar_stream_offset: u64,
914}
915
916#[derive(Debug, Clone)]
917struct FileRow {
918 path_hash: [u8; 8],
919 path: Vec<u8>,
920 member_index: usize,
921 member: TarMember,
922}
923
924#[derive(Debug, Clone)]
925struct PlannedIndexShard {
926 shard_index: u64,
927 plaintext: Vec<u8>,
928 file_count: u32,
929 first_path_hash: [u8; 8],
930 last_path_hash: [u8; 8],
931}
932
933#[derive(Debug, Clone)]
934struct PlannedDirectoryHintShard {
935 hint_shard_index: u64,
936 plaintext: Vec<u8>,
937 entry_count: u64,
938 first_dir_hash: [u8; 8],
939 last_dir_hash: [u8; 8],
940}
941
942#[derive(Debug, Clone)]
943#[cfg(test)]
944struct PayloadEnvelope {
945 envelope_index: u64,
946 plaintext: Vec<u8>,
947}
948
949#[derive(Debug, Clone)]
950struct PayloadObject {
951 envelope_index: u64,
952 plaintext_size: u32,
953 object: ObjectExtent,
954}
955
956#[derive(Debug, Clone)]
957struct EncryptedObject {
958 first_block_index: u64,
959 data_block_count: u32,
960 parity_block_count: u32,
961 encrypted_size: u32,
962 records: Vec<BlockRecord>,
963}
964
965#[derive(Debug, Clone, Copy, PartialEq, Eq)]
966struct ObjectExtent {
967 first_block_index: u64,
968 data_block_count: u32,
969 parity_block_count: u32,
970 encrypted_size: u32,
971}
972
973#[derive(Debug, Clone, Copy, PartialEq, Eq)]
974struct PlannedEncryptedObject {
975 data_block_count: u32,
976 parity_block_count: u32,
977 encrypted_size: u32,
978}
979
980#[derive(Debug, Clone, Copy, PartialEq, Eq)]
981enum MetadataObjectKind {
982 IndexRoot,
983 Dictionary,
984}
985
986impl MetadataObjectKind {
987 fn too_large_error(self) -> FormatError {
988 match self {
989 Self::IndexRoot => FormatError::WriterUnsupported("IndexRoot too large"),
990 Self::Dictionary => FormatError::WriterUnsupported("dictionary object too large"),
991 }
992 }
993}
994
995impl ObjectExtent {
996 fn new(first_block_index: u64, plan: PlannedEncryptedObject) -> Result<Self, FormatError> {
997 Ok(Self {
998 first_block_index,
999 data_block_count: plan.data_block_count,
1000 parity_block_count: plan.parity_block_count,
1001 encrypted_size: plan.encrypted_size,
1002 })
1003 }
1004
1005 fn next_block_index(self) -> Result<u64, FormatError> {
1006 checked_u64_add(
1007 self.first_block_index,
1008 self.data_block_count as u64 + self.parity_block_count as u64,
1009 "next_block_index",
1010 )
1011 }
1012}
1013
1014impl From<&EncryptedObject> for ObjectExtent {
1015 fn from(object: &EncryptedObject) -> Self {
1016 Self {
1017 first_block_index: object.first_block_index,
1018 data_block_count: object.data_block_count,
1019 parity_block_count: object.parity_block_count,
1020 encrypted_size: object.encrypted_size,
1021 }
1022 }
1023}
1024
1025#[derive(Debug, Clone)]
1026struct PlannedDirectoryHintObject {
1027 hint_shard_index: u64,
1028 compressed: Vec<u8>,
1029 extent: ObjectExtent,
1030}
1031
1032struct WriterPlan {
1033 options: WriterOptions,
1034 archive_uuid: [u8; 16],
1035 session_id: [u8; 16],
1036 crypto_header: Vec<u8>,
1037 tar_members: Vec<TarMember>,
1038 frames: Vec<PayloadFrame>,
1039 payload_objects: Vec<PayloadObject>,
1040 index_root_plaintext: Vec<u8>,
1041 compressed_index_root: Vec<u8>,
1042 index_root_extent: ObjectExtent,
1043 index_shard_objects: Vec<PlannedIndexShardObject>,
1044 shard_entries: Vec<ShardEntry>,
1045 volume_format_rev: u16,
1046 compressed_dictionary: Option<Vec<u8>>,
1047 dictionary_extent: Option<(ObjectExtent, u32)>,
1048 directory_hint_objects: Vec<PlannedDirectoryHintObject>,
1049 directory_hint_entries: Vec<DirectoryHintShardEntry>,
1050 root_auth_footer_length: Option<u32>,
1051 key_wrap_table: Option<Vec<u8>>,
1052 block_records_offset: u64,
1053 total_block_count: u64,
1054}
1055
1056struct PlannedIndexShardObject {
1057 shard_index: u64,
1058 compressed: Vec<u8>,
1059 extent: ObjectExtent,
1060}
1061
1062struct PayloadPlanning {
1063 tar_members: Vec<TarMember>,
1064 frames: Vec<PayloadFrame>,
1065 payload_objects: Vec<PayloadObject>,
1066 payload_block_count: u64,
1067 tar_total_size: u64,
1068 content_sha256: [u8; 32],
1069}
1070
1071struct PayloadEnvelopeBuilder {
1072 envelope_index: u64,
1073 plaintext: Vec<u8>,
1074}
1075
1076#[derive(Debug, Clone, Copy)]
1077struct PayloadFrameMetadataInput {
1078 frame_index: u64,
1079 envelope_index: u64,
1080 member_index: usize,
1081 offset_in_envelope: u32,
1082 compressed_size: usize,
1083 decompressed_size: usize,
1084 member_start: u64,
1085 member_offset: u64,
1086 member_group_size: u64,
1087}
1088
1089fn payload_envelope_needs_flush(
1090 envelope: &PayloadEnvelopeBuilder,
1091 frame_len: usize,
1092 options: WriterOptions,
1093) -> Result<bool, FormatError> {
1094 let next_len = checked_usize_add(envelope.plaintext.len(), frame_len, "payload")?;
1095 Ok(!envelope.plaintext.is_empty()
1096 && (next_len > options.envelope_target_size as usize
1097 || !payload_object_can_fit(next_len, options)?))
1098}
1099
1100fn payload_frame_metadata(input: PayloadFrameMetadataInput) -> Result<PayloadFrame, FormatError> {
1101 let mut flags = 0u32;
1102 if input.member_offset == 0 {
1103 flags |= 0x0000_0001;
1104 }
1105 if checked_u64_add(
1106 input.member_offset,
1107 input.decompressed_size as u64,
1108 "payload chunk",
1109 )? == input.member_group_size
1110 {
1111 flags |= 0x0000_0002;
1112 }
1113 Ok(PayloadFrame {
1114 frame_index: input.frame_index,
1115 envelope_index: input.envelope_index,
1116 member_index: input.member_index,
1117 offset_in_envelope: input.offset_in_envelope,
1118 compressed_size: u32_len(input.compressed_size, "FrameEntry.compressed_size")?,
1119 decompressed_size: u32_len(input.decompressed_size, "FrameEntry.decompressed_size")?,
1120 flags,
1121 tar_stream_offset: checked_u64_add(
1122 input.member_start,
1123 input.member_offset,
1124 "PayloadFrame.tar_stream_offset",
1125 )?,
1126 })
1127}
1128
1129#[derive(Debug, Clone, PartialEq, Eq)]
1130pub(crate) struct StreamingRegularMember {
1131 pub archive_path: Vec<u8>,
1132 pub entry_kind: SourceEntryKind,
1133 pub link_target: Option<Vec<u8>>,
1134 pub file_data_size: u64,
1135 pub sparse_extents: Option<Vec<SparseExtent>>,
1136 pub mode: u32,
1137 pub mtime: ArchiveTimestamp,
1138 pub portable_metadata: PortableFileMetadata,
1139}
1140
1141struct WriterEmissionState {
1142 volume_headers: Vec<[u8; VOLUME_HEADER_LEN]>,
1143 bytes_written: Vec<u64>,
1144 record_counts: Vec<u64>,
1145 volume_format_rev: u16,
1146 data_leaf_hashes: Option<Vec<(u64, [u8; 32])>>,
1147 next_block_index: u64,
1148}
1149
1150pub(crate) struct StreamingArchiveWriter<'a, O: ArchiveWriteSink> {
1151 sink: &'a mut O,
1152 options: WriterOptions,
1153 archive_uuid: [u8; 16],
1154 session_id: [u8; 16],
1155 crypto_header: Vec<u8>,
1156 subkeys: Subkeys,
1157 tar_members: Vec<TarMember>,
1158 frames: Vec<PayloadFrame>,
1159 payload_objects: Vec<PayloadObject>,
1160 payload_block_count: u64,
1161 tar_total_size: u64,
1162 hasher: Sha256,
1163 next_frame_index: u64,
1164 envelope: PayloadEnvelopeBuilder,
1165 emission_state: WriterEmissionState,
1166}
1167
1168pub fn write_archive(
1169 files: &[RegularFile<'_>],
1170 master_key: &MasterKey,
1171 options: WriterOptions,
1172) -> Result<WrittenArchive, FormatError> {
1173 write_archive_inner(
1174 files,
1175 master_key,
1176 options,
1177 None,
1178 &KdfParams::Raw,
1179 None,
1180 None,
1181 None,
1182 )
1183}
1184
1185pub fn write_archive_unencrypted(
1186 files: &[RegularFile<'_>],
1187 mut options: WriterOptions,
1188) -> Result<WrittenArchive, FormatError> {
1189 options.aead_algo = AeadAlgo::None;
1190 let placeholder = MasterKey::from_raw_key(&[0; 32])?;
1191 write_archive_inner(
1192 files,
1193 &placeholder,
1194 options,
1195 None,
1196 &KdfParams::None,
1197 None,
1198 None,
1199 None,
1200 )
1201}
1202
1203pub fn write_archive_with_kdf(
1204 files: &[RegularFile<'_>],
1205 master_key: &MasterKey,
1206 options: WriterOptions,
1207 kdf_params: &KdfParams,
1208) -> Result<WrittenArchive, FormatError> {
1209 write_archive_inner(
1210 files, master_key, options, None, kdf_params, None, None, None,
1211 )
1212}
1213
1214pub fn write_archive_with_recipient_wrap_records(
1215 files: &[RegularFile<'_>],
1216 master_key: &MasterKey,
1217 options: WriterOptions,
1218 records: Vec<RecipientRecordV1>,
1219) -> Result<WrittenArchive, FormatError> {
1220 let kdf_params = recipient_wrap_kdf_params_for_record_count(records.len())?;
1221 let key_wrap_records = KeyWrapRecordSource::fixed(records);
1222 write_archive_inner(
1223 files,
1224 master_key,
1225 options,
1226 None,
1227 &kdf_params,
1228 None,
1229 None,
1230 Some(&key_wrap_records),
1231 )
1232}
1233
1234pub fn write_archive_with_root_auth_and_recipient_wrap_records<F>(
1235 files: &[RegularFile<'_>],
1236 master_key: &MasterKey,
1237 options: WriterOptions,
1238 records: Vec<RecipientRecordV1>,
1239 root_auth: RootAuthWriterConfig<'_>,
1240 mut authenticator: F,
1241) -> Result<WrittenArchive, FormatError>
1242where
1243 F: FnMut(&RootAuthSigningRequest) -> Result<Vec<u8>, FormatError>,
1244{
1245 let kdf_params = recipient_wrap_kdf_params_for_record_count(records.len())?;
1246 let key_wrap_records = KeyWrapRecordSource::fixed(records);
1247 write_archive_inner(
1248 files,
1249 master_key,
1250 options,
1251 None,
1252 &kdf_params,
1253 Some(root_auth),
1254 Some(&mut authenticator),
1255 Some(&key_wrap_records),
1256 )
1257}
1258
1259pub fn write_archive_with_root_auth<F>(
1260 files: &[RegularFile<'_>],
1261 master_key: &MasterKey,
1262 options: WriterOptions,
1263 root_auth: RootAuthWriterConfig<'_>,
1264 mut authenticator: F,
1265) -> Result<WrittenArchive, FormatError>
1266where
1267 F: FnMut(&RootAuthSigningRequest) -> Result<Vec<u8>, FormatError>,
1268{
1269 let kdf_params = if options.aead_algo.is_encrypted() {
1270 KdfParams::Raw
1271 } else {
1272 KdfParams::None
1273 };
1274 write_archive_inner(
1275 files,
1276 master_key,
1277 options,
1278 None,
1279 &kdf_params,
1280 Some(root_auth),
1281 Some(&mut authenticator),
1282 None,
1283 )
1284}
1285
1286pub fn write_archive_with_root_auth_and_kdf<F>(
1287 files: &[RegularFile<'_>],
1288 master_key: &MasterKey,
1289 options: WriterOptions,
1290 kdf_params: &KdfParams,
1291 root_auth: RootAuthWriterConfig<'_>,
1292 mut authenticator: F,
1293) -> Result<WrittenArchive, FormatError>
1294where
1295 F: FnMut(&RootAuthSigningRequest) -> Result<Vec<u8>, FormatError>,
1296{
1297 write_archive_inner(
1298 files,
1299 master_key,
1300 options,
1301 None,
1302 kdf_params,
1303 Some(root_auth),
1304 Some(&mut authenticator),
1305 None,
1306 )
1307}
1308
1309pub fn write_archive_with_dictionary_and_root_auth<F>(
1310 files: &[RegularFile<'_>],
1311 master_key: &MasterKey,
1312 options: WriterOptions,
1313 dictionary: &[u8],
1314 root_auth: RootAuthWriterConfig<'_>,
1315 mut authenticator: F,
1316) -> Result<WrittenArchive, FormatError>
1317where
1318 F: FnMut(&RootAuthSigningRequest) -> Result<Vec<u8>, FormatError>,
1319{
1320 write_archive_inner(
1321 files,
1322 master_key,
1323 options,
1324 Some(dictionary),
1325 &KdfParams::Raw,
1326 Some(root_auth),
1327 Some(&mut authenticator),
1328 None,
1329 )
1330}
1331
1332pub fn write_archive_with_dictionary_kdf_and_root_auth<F>(
1333 files: &[RegularFile<'_>],
1334 master_key: &MasterKey,
1335 options: WriterOptions,
1336 dictionary: &[u8],
1337 kdf_params: &KdfParams,
1338 root_auth: RootAuthWriterConfig<'_>,
1339 mut authenticator: F,
1340) -> Result<WrittenArchive, FormatError>
1341where
1342 F: FnMut(&RootAuthSigningRequest) -> Result<Vec<u8>, FormatError>,
1343{
1344 write_archive_inner(
1345 files,
1346 master_key,
1347 options,
1348 Some(dictionary),
1349 kdf_params,
1350 Some(root_auth),
1351 Some(&mut authenticator),
1352 None,
1353 )
1354}
1355
1356pub fn write_archive_with_dictionary(
1357 files: &[RegularFile<'_>],
1358 master_key: &MasterKey,
1359 options: WriterOptions,
1360 dictionary: &[u8],
1361) -> Result<WrittenArchive, FormatError> {
1362 if dictionary.is_empty() {
1363 return Err(FormatError::WriterUnsupported(
1364 "dictionary archives require a non-empty dictionary",
1365 ));
1366 }
1367 if files.is_empty() {
1368 return Err(FormatError::WriterUnsupported(
1369 "dictionary archives require at least one file",
1370 ));
1371 }
1372 if dictionary.len() > u32::MAX as usize {
1373 return Err(FormatError::WriterUnsupported(
1374 "dictionary decompressed size exceeds u32",
1375 ));
1376 }
1377 write_archive_inner(
1378 files,
1379 master_key,
1380 options,
1381 Some(dictionary),
1382 &KdfParams::Raw,
1383 None,
1384 None,
1385 None,
1386 )
1387}
1388
1389pub fn write_archive_with_dictionary_and_kdf(
1390 files: &[RegularFile<'_>],
1391 master_key: &MasterKey,
1392 options: WriterOptions,
1393 dictionary: &[u8],
1394 kdf_params: &KdfParams,
1395) -> Result<WrittenArchive, FormatError> {
1396 if dictionary.is_empty() {
1397 return Err(FormatError::WriterUnsupported(
1398 "dictionary archives require a non-empty dictionary",
1399 ));
1400 }
1401 if files.is_empty() {
1402 return Err(FormatError::WriterUnsupported(
1403 "dictionary archives require at least one file",
1404 ));
1405 }
1406 if dictionary.len() > u32::MAX as usize {
1407 return Err(FormatError::WriterUnsupported(
1408 "dictionary decompressed size exceeds u32",
1409 ));
1410 }
1411 write_archive_inner(
1412 files,
1413 master_key,
1414 options,
1415 Some(dictionary),
1416 kdf_params,
1417 None,
1418 None,
1419 None,
1420 )
1421}
1422
1423#[allow(clippy::too_many_arguments)]
1424pub fn write_archive_sources_to_sink<S, O>(
1425 files: &[S],
1426 master_key: &MasterKey,
1427 options: WriterOptions,
1428 dictionary: Option<&[u8]>,
1429 kdf_params: &KdfParams,
1430 root_auth: Option<RootAuthWriterConfig<'_>>,
1431 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
1432 sink: &mut O,
1433) -> Result<WrittenArchiveSummary, ArchiveWriteError>
1434where
1435 S: RegularFileSource,
1436 O: ArchiveWriteSink,
1437{
1438 write_archive_stream_inner(
1439 files,
1440 master_key,
1441 options,
1442 dictionary,
1443 kdf_params,
1444 root_auth,
1445 authenticator,
1446 None,
1447 sink,
1448 None,
1449 )
1450}
1451
1452#[allow(clippy::too_many_arguments)]
1453pub fn write_archive_sources_to_sink_with_progress<S, O>(
1454 files: &[S],
1455 master_key: &MasterKey,
1456 options: WriterOptions,
1457 dictionary: Option<&[u8]>,
1458 kdf_params: &KdfParams,
1459 root_auth: Option<RootAuthWriterConfig<'_>>,
1460 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
1461 sink: &mut O,
1462 progress: &mut dyn ArchiveWriteProgressSink,
1463) -> Result<WrittenArchiveSummary, ArchiveWriteError>
1464where
1465 S: RegularFileSource,
1466 O: ArchiveWriteSink,
1467{
1468 let (progress_files, progress_state) = progress_sources(files, progress);
1469 write_archive_stream_inner(
1470 &progress_files,
1471 master_key,
1472 options,
1473 dictionary,
1474 kdf_params,
1475 root_auth,
1476 authenticator,
1477 None,
1478 sink,
1479 Some(&progress_state),
1480 )
1481}
1482
1483#[allow(clippy::too_many_arguments)]
1484pub fn write_archive_sources_to_sink_single_pass<S, O>(
1485 files: &[S],
1486 master_key: &MasterKey,
1487 options: WriterOptions,
1488 kdf_params: &KdfParams,
1489 root_auth: Option<RootAuthWriterConfig<'_>>,
1490 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
1491 sink: &mut O,
1492) -> Result<WrittenArchiveSummary, ArchiveWriteError>
1493where
1494 S: RegularFileSource,
1495 O: ArchiveWriteSink,
1496{
1497 write_single_pass_archive_to_sink(
1498 master_key,
1499 options,
1500 kdf_params,
1501 root_auth,
1502 authenticator,
1503 None,
1504 sink,
1505 None,
1506 |writer| {
1507 for file in files {
1508 writer.write_regular_member_from_source(file)?;
1509 }
1510 Ok(())
1511 },
1512 )
1513}
1514
1515#[allow(clippy::too_many_arguments)]
1516pub fn write_archive_sources_to_sink_single_pass_with_recipient_wrap_records<S, O>(
1517 files: &[S],
1518 master_key: &MasterKey,
1519 options: WriterOptions,
1520 records: Vec<RecipientRecordV1>,
1521 root_auth: Option<RootAuthWriterConfig<'_>>,
1522 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
1523 sink: &mut O,
1524) -> Result<WrittenArchiveSummary, ArchiveWriteError>
1525where
1526 S: RegularFileSource,
1527 O: ArchiveWriteSink,
1528{
1529 let kdf_params = recipient_wrap_kdf_params_for_record_count(records.len())?;
1530 let key_wrap_records = KeyWrapRecordSource::fixed(records);
1531 write_single_pass_archive_to_sink(
1532 master_key,
1533 options,
1534 &kdf_params,
1535 root_auth,
1536 authenticator,
1537 Some(&key_wrap_records),
1538 sink,
1539 None,
1540 |writer| {
1541 for file in files {
1542 writer.write_regular_member_from_source(file)?;
1543 }
1544 Ok(())
1545 },
1546 )
1547}
1548
1549#[allow(clippy::too_many_arguments)]
1550pub fn write_archive_sources_to_sink_single_pass_with_progress<S, O>(
1551 files: &[S],
1552 master_key: &MasterKey,
1553 options: WriterOptions,
1554 kdf_params: &KdfParams,
1555 root_auth: Option<RootAuthWriterConfig<'_>>,
1556 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
1557 sink: &mut O,
1558 progress: &mut dyn ArchiveWriteProgressSink,
1559) -> Result<WrittenArchiveSummary, ArchiveWriteError>
1560where
1561 S: RegularFileSource,
1562 O: ArchiveWriteSink,
1563{
1564 let (progress_files, progress_state) = progress_sources(files, progress);
1565 write_single_pass_archive_to_sink(
1566 master_key,
1567 options,
1568 kdf_params,
1569 root_auth,
1570 authenticator,
1571 None,
1572 sink,
1573 Some(&progress_state),
1574 |writer| {
1575 for file in &progress_files {
1576 writer.write_regular_member_from_source(file)?;
1577 }
1578 Ok(())
1579 },
1580 )
1581}
1582
1583#[allow(clippy::too_many_arguments)]
1584pub fn write_archive_sources_to_sink_ordered_parallel<S, O>(
1585 files: &[S],
1586 master_key: &MasterKey,
1587 options: WriterOptions,
1588 kdf_params: &KdfParams,
1589 root_auth: Option<RootAuthWriterConfig<'_>>,
1590 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
1591 sink: &mut O,
1592) -> Result<WrittenArchiveSummary, ArchiveWriteError>
1593where
1594 S: RegularFileSource,
1595 O: ArchiveWriteSink,
1596{
1597 write_ordered_parallel_archive_to_sink(
1598 files,
1599 master_key,
1600 options,
1601 kdf_params,
1602 root_auth,
1603 authenticator,
1604 None,
1605 sink,
1606 None,
1607 )
1608}
1609
1610#[allow(clippy::too_many_arguments)]
1611pub fn write_archive_sources_to_sink_ordered_parallel_with_recipient_wrap_records<S, O>(
1612 files: &[S],
1613 master_key: &MasterKey,
1614 options: WriterOptions,
1615 records: Vec<RecipientRecordV1>,
1616 root_auth: Option<RootAuthWriterConfig<'_>>,
1617 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
1618 sink: &mut O,
1619) -> Result<WrittenArchiveSummary, ArchiveWriteError>
1620where
1621 S: RegularFileSource,
1622 O: ArchiveWriteSink,
1623{
1624 let kdf_params = recipient_wrap_kdf_params_for_record_count(records.len())?;
1625 let key_wrap_records = KeyWrapRecordSource::fixed(records);
1626 write_ordered_parallel_archive_to_sink(
1627 files,
1628 master_key,
1629 options,
1630 &kdf_params,
1631 root_auth,
1632 authenticator,
1633 Some(&key_wrap_records),
1634 sink,
1635 None,
1636 )
1637}
1638
1639#[allow(clippy::too_many_arguments)]
1640pub fn write_archive_sources_to_sink_ordered_parallel_with_recipient_wrap_records_and_progress<
1641 S,
1642 O,
1643>(
1644 files: &[S],
1645 master_key: &MasterKey,
1646 options: WriterOptions,
1647 records: Vec<RecipientRecordV1>,
1648 root_auth: Option<RootAuthWriterConfig<'_>>,
1649 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
1650 sink: &mut O,
1651 progress: &mut dyn ArchiveWriteProgressSink,
1652) -> Result<WrittenArchiveSummary, ArchiveWriteError>
1653where
1654 S: RegularFileSource,
1655 O: ArchiveWriteSink,
1656{
1657 let kdf_params = recipient_wrap_kdf_params_for_record_count(records.len())?;
1658 let key_wrap_records = KeyWrapRecordSource::fixed(records);
1659 let (progress_files, progress_state) = progress_sources(files, progress);
1660 write_ordered_parallel_archive_to_sink(
1661 &progress_files,
1662 master_key,
1663 options,
1664 &kdf_params,
1665 root_auth,
1666 authenticator,
1667 Some(&key_wrap_records),
1668 sink,
1669 Some(&progress_state),
1670 )
1671}
1672
1673#[allow(clippy::too_many_arguments)]
1674pub fn write_archive_sources_to_sink_ordered_parallel_with_progress<S, O>(
1675 files: &[S],
1676 master_key: &MasterKey,
1677 options: WriterOptions,
1678 kdf_params: &KdfParams,
1679 root_auth: Option<RootAuthWriterConfig<'_>>,
1680 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
1681 sink: &mut O,
1682 progress: &mut dyn ArchiveWriteProgressSink,
1683) -> Result<WrittenArchiveSummary, ArchiveWriteError>
1684where
1685 S: RegularFileSource,
1686 O: ArchiveWriteSink,
1687{
1688 let (progress_files, progress_state) = progress_sources(files, progress);
1689 write_ordered_parallel_archive_to_sink(
1690 &progress_files,
1691 master_key,
1692 options,
1693 kdf_params,
1694 root_auth,
1695 authenticator,
1696 None,
1697 sink,
1698 Some(&progress_state),
1699 )
1700}
1701
1702#[allow(clippy::too_many_arguments)]
1703fn write_archive_inner(
1704 files: &[RegularFile<'_>],
1705 master_key: &MasterKey,
1706 options: WriterOptions,
1707 dictionary: Option<&[u8]>,
1708 kdf_params: &KdfParams,
1709 root_auth: Option<RootAuthWriterConfig<'_>>,
1710 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
1711 key_wrap_records: Option<&KeyWrapRecordSource>,
1712) -> Result<WrittenArchive, FormatError> {
1713 let mut sink = MemoryArchiveSink::default();
1714 let summary = write_archive_stream_inner(
1715 files,
1716 master_key,
1717 options,
1718 dictionary,
1719 kdf_params,
1720 root_auth,
1721 authenticator,
1722 key_wrap_records,
1723 &mut sink,
1724 None,
1725 )
1726 .map_err(format_error_from_archive_write_error)?;
1727 Ok(WrittenArchive {
1728 bytes: sink
1729 .volumes
1730 .first()
1731 .cloned()
1732 .ok_or(FormatError::WriterInvariant("no volumes emitted"))?,
1733 volumes: sink.volumes,
1734 bootstrap_sidecar: sink.bootstrap_sidecar,
1735 archive_uuid: summary.archive_uuid,
1736 session_id: summary.session_id,
1737 timings: summary.timings,
1738 })
1739}
1740
1741fn format_error_from_archive_write_error(error: ArchiveWriteError) -> FormatError {
1742 match error {
1743 ArchiveWriteError::Format(error) => error,
1744 ArchiveWriteError::Io(_) => {
1745 FormatError::WriterInvariant("in-memory archive writer returned I/O")
1746 }
1747 }
1748}
1749
1750fn writer_subkeys(
1751 master_key: &MasterKey,
1752 aead_algo: AeadAlgo,
1753 archive_uuid: &[u8; 16],
1754 session_id: &[u8; 16],
1755) -> Result<Subkeys, FormatError> {
1756 if aead_algo.is_encrypted() {
1757 Subkeys::derive(master_key, archive_uuid, session_id)
1758 } else {
1759 Ok(Subkeys::unencrypted_placeholder())
1760 }
1761}
1762
1763#[allow(clippy::too_many_arguments)]
1764fn write_archive_stream_inner<S, O>(
1765 files: &[S],
1766 master_key: &MasterKey,
1767 options: WriterOptions,
1768 dictionary: Option<&[u8]>,
1769 kdf_params: &KdfParams,
1770 root_auth: Option<RootAuthWriterConfig<'_>>,
1771 mut authenticator: Option<&mut RootAuthAuthenticator<'_>>,
1772 key_wrap_records: Option<&KeyWrapRecordSource>,
1773 sink: &mut O,
1774 progress: Option<&SourceProgressHandle<'_>>,
1775) -> Result<WrittenArchiveSummary, ArchiveWriteError>
1776where
1777 S: RegularFileSource,
1778 O: ArchiveWriteSink,
1779{
1780 let total_started = Instant::now();
1781 validate_dictionary_inputs(files.is_empty(), dictionary)?;
1782 if let Some(root_auth) = root_auth {
1783 validate_root_auth_writer_config(root_auth)?;
1784 }
1785 let mut requested_options = options;
1786 if requested_options.target_volume_size.is_some() {
1787 requested_options.stripe_width = requested_options
1788 .stripe_width
1789 .max(requested_options.volume_loss_tolerance as u32 + 1);
1790 }
1791 let archive_uuid = requested_options
1792 .archive_uuid
1793 .unwrap_or_else(|| *Uuid::new_v4().as_bytes());
1794 let session_id = requested_options
1795 .session_id
1796 .unwrap_or_else(|| *Uuid::new_v4().as_bytes());
1797 let mut accumulated_timings = WriterTimings::default();
1798
1799 loop {
1800 let planned_options = plan_writer_options(requested_options)?;
1801 start_write_phase(progress, ArchiveWritePhase::PlanningPayload);
1802 let timed_plan = build_writer_plan(
1803 files,
1804 master_key,
1805 planned_options,
1806 dictionary,
1807 kdf_params,
1808 key_wrap_records,
1809 archive_uuid,
1810 session_id,
1811 root_auth,
1812 progress,
1813 )?;
1814 accumulated_timings.add_assign(timed_plan.timings);
1815 let plan = timed_plan.plan;
1816 if let Some(target_volume_size) = planned_options.target_volume_size {
1817 let required = required_stripe_width_for_plan(&plan, master_key, target_volume_size)?;
1818 if required > planned_options.stripe_width {
1819 requested_options.stripe_width = required;
1820 continue;
1821 }
1822 }
1823 start_write_phase(progress, ArchiveWritePhase::EmittingPayload);
1824 let mut summary = emit_writer_plan(
1825 files,
1826 master_key,
1827 dictionary,
1828 root_auth,
1829 authenticator.take(),
1830 plan,
1831 sink,
1832 progress,
1833 )?;
1834 summary.timings.add_assign(accumulated_timings);
1835 summary.timings.total = total_started.elapsed();
1836 return Ok(summary);
1837 }
1838}
1839
1840fn validate_dictionary_inputs(
1841 files_are_empty: bool,
1842 dictionary: Option<&[u8]>,
1843) -> Result<(), FormatError> {
1844 if let Some(dictionary) = dictionary {
1845 if dictionary.is_empty() {
1846 return Err(FormatError::WriterUnsupported(
1847 "dictionary archives require a non-empty dictionary",
1848 ));
1849 }
1850 if files_are_empty {
1851 return Err(FormatError::WriterUnsupported(
1852 "dictionary archives require at least one file",
1853 ));
1854 }
1855 if dictionary.len() > u32::MAX as usize {
1856 return Err(FormatError::WriterUnsupported(
1857 "dictionary decompressed size exceeds u32",
1858 ));
1859 }
1860 }
1861 Ok(())
1862}
1863
1864#[allow(clippy::too_many_arguments)]
1865struct TimedWriterPlan {
1866 plan: WriterPlan,
1867 timings: WriterTimings,
1868}
1869
1870#[allow(clippy::too_many_arguments)]
1871fn build_writer_plan<S: RegularFileSource>(
1872 files: &[S],
1873 master_key: &MasterKey,
1874 options: WriterOptions,
1875 dictionary: Option<&[u8]>,
1876 kdf_params: &KdfParams,
1877 key_wrap_records: Option<&KeyWrapRecordSource>,
1878 archive_uuid: [u8; 16],
1879 session_id: [u8; 16],
1880 root_auth: Option<RootAuthWriterConfig<'_>>,
1881 progress: Option<&SourceProgressHandle<'_>>,
1882) -> Result<TimedWriterPlan, ArchiveWriteError> {
1883 let mut next_block_index = 0u64;
1884 let payload_started = Instant::now();
1885 let payload = plan_payload_stream(files, options, dictionary, &mut next_block_index)?;
1886 let plan_payload = payload_started.elapsed();
1887 start_write_phase(progress, ArchiveWritePhase::PlanningMetadata);
1888 let metadata_started = Instant::now();
1889 let plan = build_writer_plan_from_payload(
1890 payload,
1891 next_block_index,
1892 master_key,
1893 options,
1894 dictionary,
1895 kdf_params,
1896 key_wrap_records,
1897 archive_uuid,
1898 session_id,
1899 root_auth,
1900 )?;
1901 Ok(TimedWriterPlan {
1902 plan,
1903 timings: WriterTimings {
1904 plan_payload,
1905 plan_metadata: metadata_started.elapsed(),
1906 ..WriterTimings::default()
1907 },
1908 })
1909}
1910
1911#[allow(clippy::too_many_arguments)]
1912fn build_writer_plan_from_payload(
1913 payload: PayloadPlanning,
1914 mut next_block_index: u64,
1915 master_key: &MasterKey,
1916 mut options: WriterOptions,
1917 dictionary: Option<&[u8]>,
1918 kdf_params: &KdfParams,
1919 key_wrap_records: Option<&KeyWrapRecordSource>,
1920 archive_uuid: [u8; 16],
1921 session_id: [u8; 16],
1922 root_auth: Option<RootAuthWriterConfig<'_>>,
1923) -> Result<WriterPlan, ArchiveWriteError> {
1924 let subkeys = writer_subkeys(master_key, options.aead_algo, &archive_uuid, &session_id)?;
1925 let (resolved_kdf_params, key_wrap_table) =
1926 resolve_key_wrap_artifacts(kdf_params, &archive_uuid, &session_id, key_wrap_records)?;
1927 let volume_format_rev = volume_format_revision_for_options(&options, &resolved_kdf_params);
1928 let (shard_file_rows, planned_index_shards) = if payload.tar_members.is_empty() {
1929 (Vec::new(), Vec::new())
1930 } else {
1931 let rows = sorted_file_rows(&payload.tar_members);
1932 let shard_file_rows = partition_file_rows(rows)?;
1933 let planned_index_shards = build_index_shard_plaintexts(
1934 &shard_file_rows,
1935 &payload.frames,
1936 &payload.payload_objects,
1937 options,
1938 )?;
1939 (shard_file_rows, planned_index_shards)
1940 };
1941
1942 let mut shard_entries = Vec::with_capacity(planned_index_shards.len());
1943 let mut index_shard_objects = Vec::with_capacity(planned_index_shards.len());
1944 for planned in planned_index_shards {
1945 let compressed =
1946 compress_zstd_frame_with_jobs(&planned.plaintext, options.zstd_level, options.jobs)?;
1947 let object_plan = plan_encrypted_object(
1948 compressed.len(),
1949 options.index_fec_data_shards,
1950 options.index_fec_parity_shards,
1951 options,
1952 )?;
1953 let extent = ObjectExtent::new(next_block_index, object_plan)?;
1954 next_block_index = extent.next_block_index()?;
1955 shard_entries.push(ShardEntry {
1956 shard_index: planned.shard_index,
1957 first_block_index: extent.first_block_index,
1958 data_block_count: extent.data_block_count,
1959 parity_block_count: extent.parity_block_count,
1960 encrypted_size: extent.encrypted_size,
1961 decompressed_size: u32_len(planned.plaintext.len(), "IndexShard")?,
1962 file_count: planned.file_count,
1963 first_path_hash: planned.first_path_hash,
1964 last_path_hash: planned.last_path_hash,
1965 });
1966 index_shard_objects.push(PlannedIndexShardObject {
1967 shard_index: planned.shard_index,
1968 compressed,
1969 extent,
1970 });
1971 }
1972
1973 let compressed_dictionary = dictionary
1974 .map(|dictionary| {
1975 compress_zstd_frame_with_jobs(dictionary, options.zstd_level, options.jobs)
1976 })
1977 .transpose()?;
1978 let dictionary_decompressed_size = dictionary
1979 .map(|dictionary| u32_len(dictionary.len(), "dictionary"))
1980 .transpose()?;
1981 let dictionary_plan = compressed_dictionary
1982 .as_ref()
1983 .map(|compressed| {
1984 plan_metadata_object_without_class(
1985 compressed.len(),
1986 options,
1987 MetadataObjectKind::Dictionary,
1988 )
1989 })
1990 .transpose()?;
1991 let dictionary_extent = dictionary_plan
1992 .map(|plan| ObjectExtent::new(next_block_index, plan))
1993 .transpose()?;
1994 let next_after_dictionary = if let Some(extent) = dictionary_extent {
1995 extent.next_block_index()?
1996 } else {
1997 next_block_index
1998 };
1999
2000 let planned_directory_hint_shards = if should_emit_directory_hints(payload.tar_members.len()) {
2001 build_directory_hint_plaintexts(&shard_file_rows, options)?
2002 } else {
2003 Vec::new()
2004 };
2005 let mut directory_hint_entries = Vec::with_capacity(planned_directory_hint_shards.len());
2006 let mut directory_hint_objects = Vec::with_capacity(planned_directory_hint_shards.len());
2007 let mut planned_next_block_index = next_after_dictionary;
2008 for planned in planned_directory_hint_shards {
2009 let compressed =
2010 compress_zstd_frame_with_jobs(&planned.plaintext, options.zstd_level, options.jobs)?;
2011 let object_plan = plan_encrypted_object(
2012 compressed.len(),
2013 options.index_fec_data_shards,
2014 options.index_fec_parity_shards,
2015 options,
2016 )?;
2017 let extent = ObjectExtent::new(planned_next_block_index, object_plan)?;
2018 planned_next_block_index = extent.next_block_index()?;
2019 directory_hint_entries.push(DirectoryHintShardEntry {
2020 hint_shard_index: planned.hint_shard_index,
2021 first_dir_hash: planned.first_dir_hash,
2022 last_dir_hash: planned.last_dir_hash,
2023 first_block_index: extent.first_block_index,
2024 data_block_count: extent.data_block_count,
2025 parity_block_count: extent.parity_block_count,
2026 encrypted_size: extent.encrypted_size,
2027 decompressed_size: u32_len(planned.plaintext.len(), "DirectoryHintTable")?,
2028 entry_count: planned.entry_count,
2029 });
2030 directory_hint_objects.push(PlannedDirectoryHintObject {
2031 hint_shard_index: planned.hint_shard_index,
2032 compressed,
2033 extent,
2034 });
2035 }
2036
2037 let dictionary_extent = dictionary_extent.zip(dictionary_decompressed_size);
2038 let index_root_plaintext = build_index_root_plaintext(IndexRootPlaintextInput {
2039 shard_entries: &shard_entries,
2040 frame_count: payload.frames.len() as u64,
2041 envelope_count: payload.payload_objects.len() as u64,
2042 file_count: payload.tar_members.len() as u64,
2043 payload_block_count: payload.payload_block_count,
2044 tar_total_size: payload.tar_total_size,
2045 content_sha256: payload.content_sha256,
2046 directory_hint_entries: &directory_hint_entries,
2047 dictionary_extent,
2048 });
2049 let compressed_index_root =
2050 compress_zstd_frame_with_jobs(&index_root_plaintext, options.zstd_level, options.jobs)?;
2051 let metadata_class = plan_index_root_metadata_class(
2052 options,
2053 compressed_index_root.len(),
2054 compressed_dictionary.as_ref().map(Vec::len),
2055 )?;
2056 options = metadata_class.options;
2057 let crypto_header = build_crypto_header(
2058 options,
2059 volume_format_rev,
2060 dictionary.is_some(),
2061 &subkeys,
2062 &archive_uuid,
2063 &session_id,
2064 &resolved_kdf_params,
2065 )?;
2066 let index_root_extent = ObjectExtent::new(planned_next_block_index, metadata_class.index_root)?;
2067 let total_block_count = index_root_extent.next_block_index()?;
2068 let root_auth_footer_length = root_auth
2069 .map(|config| {
2070 root_auth_footer_wire_length(
2071 config.signer_identity.len(),
2072 config.authenticator_value_length as usize,
2073 )
2074 })
2075 .transpose()?;
2076 let block_records_offset = checked_u64_add(
2077 checked_u64_add(
2078 VOLUME_HEADER_LEN as u64,
2079 crypto_header.len() as u64,
2080 "CryptoHeader",
2081 )?,
2082 key_wrap_table.as_ref().map(Vec::len).unwrap_or(0) as u64,
2083 "KeyWrapTableV1",
2084 )?;
2085
2086 Ok(WriterPlan {
2087 options,
2088 archive_uuid,
2089 session_id,
2090 crypto_header,
2091 tar_members: payload.tar_members,
2092 frames: payload.frames,
2093 payload_objects: payload.payload_objects,
2094 index_root_plaintext,
2095 compressed_index_root,
2096 index_root_extent,
2097 index_shard_objects,
2098 shard_entries,
2099 compressed_dictionary,
2100 dictionary_extent,
2101 volume_format_rev,
2102 directory_hint_objects,
2103 directory_hint_entries,
2104 root_auth_footer_length,
2105 key_wrap_table,
2106 block_records_offset,
2107 total_block_count,
2108 })
2109}
2110
2111fn plan_payload_stream<S: RegularFileSource>(
2112 files: &[S],
2113 options: WriterOptions,
2114 dictionary: Option<&[u8]>,
2115 next_block_index: &mut u64,
2116) -> Result<PayloadPlanning, ArchiveWriteError> {
2117 let mut tar_members = Vec::with_capacity(files.len());
2118 let mut frames = Vec::new();
2119 let mut payload_objects = Vec::new();
2120 let mut tar_total_size = 0u64;
2121 let mut hasher = Sha256::new();
2122 let mut payload_block_count = 0u64;
2123 let mut next_frame_index = 0u64;
2124 let mut envelope = PayloadEnvelopeBuilder {
2125 envelope_index: 0,
2126 plaintext: Vec::new(),
2127 };
2128
2129 for (member_index, file) in files.iter().enumerate() {
2130 let path = normalize_lookup_file_path(file.archive_path(), options.max_path_length)?;
2131 let entry_kind = file.entry_kind();
2132 if entry_kind != SourceEntryKind::Regular && file.file_data_size() != 0 {
2133 return Err(FormatError::WriterInvariant(
2134 "non-regular source has non-zero file data size",
2135 )
2136 .into());
2137 }
2138 let link_target = file.link_target().map(<[u8]>::to_vec);
2139 let sparse_extents = file.sparse_extents().map(<[SparseExtent]>::to_vec);
2140 let source_payload_size = sparse_extents
2141 .as_deref()
2142 .map(|extents| sparse_extent_bytes(extents, file.file_data_size()))
2143 .transpose()?
2144 .unwrap_or(file.file_data_size());
2145 let portable_metadata = file.portable_metadata();
2146 let layout = build_primary_member_layout(
2147 &path,
2148 entry_kind,
2149 link_target.as_deref(),
2150 file.file_data_size(),
2151 sparse_extents.as_deref(),
2152 file.mode(),
2153 file.mtime(),
2154 &portable_metadata,
2155 )?;
2156 let member_start = tar_total_size;
2157 let member_group_size = primary_member_layout_size(&layout, source_payload_size)?;
2158 let mut reader = StreamingMemberReader::from_source(
2159 file,
2160 &portable_metadata,
2161 layout,
2162 source_payload_size,
2163 )?;
2164 tar_members.push(TarMember {
2165 path,
2166 entry_kind,
2167 link_target,
2168 tar_member_group_start: member_start,
2169 tar_member_group_size: member_group_size,
2170 file_data_size: file.file_data_size(),
2171 sparse_extents,
2172 mode: file.mode(),
2173 mtime: file.mtime(),
2174 portable_metadata,
2175 });
2176 let mut member_offset = 0u64;
2177 while member_offset < member_group_size {
2178 let remaining = member_group_size - member_offset;
2179 let max_chunk = remaining.min(options.chunk_size as u64);
2180 let mut chunk = vec![0u8; to_usize_writer(max_chunk, "payload chunk")?];
2181 reader
2182 .read_exact(&mut chunk)
2183 .map_err(ArchiveWriteError::Io)?;
2184 let mut chunk_len = chunk.len();
2185 let frame = loop {
2186 let candidate = &chunk[..chunk_len];
2187 let frame = if let Some(dictionary) = dictionary {
2188 compress_zstd_frame_with_dictionary_and_jobs(
2189 candidate,
2190 options.zstd_level,
2191 dictionary,
2192 options.jobs,
2193 )?
2194 } else {
2195 compress_zstd_frame_with_jobs(candidate, options.zstd_level, options.jobs)?
2196 };
2197 if payload_object_can_fit(frame.len(), options)? {
2198 break frame;
2199 }
2200 if chunk_len == 1 {
2201 return Err(FormatError::WriterUnsupported(
2202 "single-byte payload frame exceeds envelope object limits",
2203 )
2204 .into());
2205 }
2206 chunk_len = (chunk_len / 2).max(1);
2207 };
2208 if chunk_len < chunk.len() {
2209 reader.push_back(chunk[chunk_len..].to_vec());
2210 }
2211 let chunk = &chunk[..chunk_len];
2212 hasher.update(chunk);
2213 append_payload_frame_to_plan(
2214 PayloadFramePlanState {
2215 envelope: &mut envelope,
2216 payload_objects: &mut payload_objects,
2217 payload_block_count: &mut payload_block_count,
2218 next_block_index,
2219 frames: &mut frames,
2220 next_frame_index: &mut next_frame_index,
2221 options,
2222 },
2223 PayloadFramePlanInput {
2224 frame: &frame,
2225 decompressed_size: chunk_len,
2226 member_index,
2227 member_start,
2228 member_offset,
2229 member_group_size,
2230 },
2231 )?;
2232 member_offset = checked_u64_add(member_offset, chunk_len as u64, "payload chunk")?;
2233 tar_total_size = checked_u64_add(tar_total_size, chunk_len as u64, "tar stream")?;
2234 }
2235 }
2236
2237 if !envelope.plaintext.is_empty() {
2238 flush_payload_envelope_plan(
2239 &mut envelope,
2240 &mut payload_objects,
2241 &mut payload_block_count,
2242 next_block_index,
2243 options,
2244 )?;
2245 }
2246 let digest = hasher.finalize();
2247 let mut content_sha256 = [0u8; 32];
2248 content_sha256.copy_from_slice(&digest);
2249 Ok(PayloadPlanning {
2250 tar_members,
2251 frames,
2252 payload_objects,
2253 payload_block_count,
2254 tar_total_size,
2255 content_sha256,
2256 })
2257}
2258
2259#[allow(clippy::too_many_arguments)]
2260pub(crate) fn write_single_pass_archive_to_sink<O, F>(
2261 master_key: &MasterKey,
2262 options: WriterOptions,
2263 kdf_params: &KdfParams,
2264 root_auth: Option<RootAuthWriterConfig<'_>>,
2265 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
2266 key_wrap_records: Option<&KeyWrapRecordSource>,
2267 sink: &mut O,
2268 progress: Option<&SourceProgressHandle<'_>>,
2269 drive_members: F,
2270) -> Result<WrittenArchiveSummary, ArchiveWriteError>
2271where
2272 O: ArchiveWriteSink,
2273 F: FnOnce(&mut StreamingArchiveWriter<'_, O>) -> Result<(), ArchiveWriteError>,
2274{
2275 let total_started = Instant::now();
2276 validate_single_pass_writer_options(options)?;
2277 if let Some(root_auth) = root_auth {
2278 validate_root_auth_writer_config(root_auth)?;
2279 }
2280 let options = plan_single_pass_writer_options(options)?;
2281 let archive_uuid = options
2282 .archive_uuid
2283 .unwrap_or_else(|| *Uuid::new_v4().as_bytes());
2284 let session_id = options
2285 .session_id
2286 .unwrap_or_else(|| *Uuid::new_v4().as_bytes());
2287 let stabilized_key_wrap_records =
2288 stabilized_key_wrap_record_source(kdf_params, key_wrap_records)?;
2289 let key_wrap_records = stabilized_key_wrap_records.as_ref().or(key_wrap_records);
2290 let (resolved_kdf_params, key_wrap_table) =
2291 resolve_key_wrap_artifacts(kdf_params, &archive_uuid, &session_id, key_wrap_records)?;
2292 let volume_format_rev = volume_format_revision_for_options(&options, &resolved_kdf_params);
2293 let subkeys = writer_subkeys(master_key, options.aead_algo, &archive_uuid, &session_id)?;
2294 let crypto_header = build_crypto_header(
2295 options,
2296 volume_format_rev,
2297 false,
2298 &subkeys,
2299 &archive_uuid,
2300 &session_id,
2301 &resolved_kdf_params,
2302 )?;
2303 let emission_state = begin_writer_emission_state(
2304 sink,
2305 options,
2306 &crypto_header,
2307 key_wrap_table.as_deref(),
2308 archive_uuid,
2309 session_id,
2310 volume_format_rev,
2311 root_auth.is_some(),
2312 )?;
2313
2314 let mut writer = StreamingArchiveWriter {
2315 sink,
2316 options,
2317 archive_uuid,
2318 session_id,
2319 crypto_header,
2320 subkeys,
2321 tar_members: Vec::new(),
2322 frames: Vec::new(),
2323 payload_objects: Vec::new(),
2324 payload_block_count: 0,
2325 tar_total_size: 0,
2326 hasher: Sha256::new(),
2327 next_frame_index: 0,
2328 envelope: PayloadEnvelopeBuilder {
2329 envelope_index: 0,
2330 plaintext: Vec::new(),
2331 },
2332 emission_state,
2333 };
2334 start_write_phase(progress, ArchiveWritePhase::EmittingPayload);
2335 let emit_payload_started = Instant::now();
2336 drive_members(&mut writer)?;
2337 let emit_payload = emit_payload_started.elapsed();
2338 let mut summary = writer.finish(
2339 master_key,
2340 &resolved_kdf_params,
2341 key_wrap_records,
2342 root_auth,
2343 authenticator,
2344 progress,
2345 )?;
2346 summary.timings.emit_payload += emit_payload;
2347 summary.timings.total = total_started.elapsed();
2348 Ok(summary)
2349}
2350
2351struct OrderedFrameJob {
2352 frame_index: u64,
2353 member_index: usize,
2354 member_start: u64,
2355 member_offset: u64,
2356 member_group_size: u64,
2357 plaintext: Vec<u8>,
2358}
2359
2360struct OrderedFrameResult {
2361 frame_index: u64,
2362 member_index: usize,
2363 member_start: u64,
2364 member_offset: u64,
2365 member_group_size: u64,
2366 decompressed_size: usize,
2367 frame: Vec<u8>,
2368}
2369
2370struct OrderedEnvelopeJob {
2371 envelope_index: u64,
2372 plaintext: Vec<u8>,
2373 extent: ObjectExtent,
2374 collect_data_leaf_hashes: bool,
2375}
2376
2377struct OrderedEnvelopeResult {
2378 envelope_index: u64,
2379 records: OrderedEnvelopeRecords,
2380}
2381
2382enum OrderedEnvelopeRecords {
2383 Materialized(Vec<BlockRecord>),
2384 Serialized(Vec<SerializedBlockRecord>),
2385}
2386
2387struct SerializedBlockRecord {
2388 block_index: u64,
2389 bytes: Vec<u8>,
2390}
2391
2392struct OrderedParallelState {
2393 tar_members: Vec<TarMember>,
2394 frames: Vec<PayloadFrame>,
2395 payload_objects: Vec<PayloadObject>,
2396 payload_block_count: u64,
2397 tar_total_size: u64,
2398 hasher: Sha256,
2399 next_frame_job_index: u64,
2400 next_frame_result_index: u64,
2401 next_frame_metadata_index: u64,
2402 frame_buffer: std::collections::BTreeMap<u64, OrderedFrameResult>,
2403 envelope: PayloadEnvelopeBuilder,
2404 next_payload_block_index: u64,
2405 next_envelope_result_index: u64,
2406 envelope_buffer: std::collections::BTreeMap<u64, OrderedEnvelopeResult>,
2407}
2408
2409impl OrderedParallelState {
2410 fn new(file_count: usize) -> Self {
2411 Self {
2412 tar_members: Vec::with_capacity(file_count),
2413 frames: Vec::new(),
2414 payload_objects: Vec::new(),
2415 payload_block_count: 0,
2416 tar_total_size: 0,
2417 hasher: Sha256::new(),
2418 next_frame_job_index: 0,
2419 next_frame_result_index: 0,
2420 next_frame_metadata_index: 0,
2421 frame_buffer: std::collections::BTreeMap::new(),
2422 envelope: PayloadEnvelopeBuilder {
2423 envelope_index: 0,
2424 plaintext: Vec::new(),
2425 },
2426 next_payload_block_index: 0,
2427 next_envelope_result_index: 0,
2428 envelope_buffer: std::collections::BTreeMap::new(),
2429 }
2430 }
2431}
2432
2433#[allow(clippy::too_many_arguments)]
2434fn write_ordered_parallel_archive_to_sink<S, O>(
2435 files: &[S],
2436 master_key: &MasterKey,
2437 options: WriterOptions,
2438 kdf_params: &KdfParams,
2439 root_auth: Option<RootAuthWriterConfig<'_>>,
2440 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
2441 key_wrap_records: Option<&KeyWrapRecordSource>,
2442 sink: &mut O,
2443 progress: Option<&SourceProgressHandle<'_>>,
2444) -> Result<WrittenArchiveSummary, ArchiveWriteError>
2445where
2446 S: RegularFileSource,
2447 O: ArchiveWriteSink,
2448{
2449 write_ordered_parallel_stream_archive_to_sink(
2450 master_key,
2451 options,
2452 kdf_params,
2453 root_auth,
2454 authenticator,
2455 key_wrap_records,
2456 sink,
2457 progress,
2458 |writer| {
2459 writer.reserve_member_capacity(files.len());
2460 for file in files {
2461 writer.write_regular_member_from_source(file)?;
2462 }
2463 Ok(())
2464 },
2465 )
2466}
2467
2468#[allow(clippy::too_many_arguments)]
2469pub(crate) fn write_ordered_parallel_stream_archive_to_sink<O, F>(
2470 master_key: &MasterKey,
2471 options: WriterOptions,
2472 kdf_params: &KdfParams,
2473 root_auth: Option<RootAuthWriterConfig<'_>>,
2474 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
2475 key_wrap_records: Option<&KeyWrapRecordSource>,
2476 sink: &mut O,
2477 progress: Option<&SourceProgressHandle<'_>>,
2478 drive_members: F,
2479) -> Result<WrittenArchiveSummary, ArchiveWriteError>
2480where
2481 O: ArchiveWriteSink,
2482 F: FnOnce(&mut OrderedParallelArchiveWriter<'_, O>) -> Result<(), ArchiveWriteError>,
2483{
2484 let total_started = Instant::now();
2485 validate_single_pass_writer_options(options)?;
2486 if let Some(root_auth) = root_auth {
2487 validate_root_auth_writer_config(root_auth)?;
2488 }
2489 let options = plan_single_pass_writer_options(options)?;
2490 let archive_uuid = options
2491 .archive_uuid
2492 .unwrap_or_else(|| *Uuid::new_v4().as_bytes());
2493 let session_id = options
2494 .session_id
2495 .unwrap_or_else(|| *Uuid::new_v4().as_bytes());
2496 let stabilized_key_wrap_records =
2497 stabilized_key_wrap_record_source(kdf_params, key_wrap_records)?;
2498 let key_wrap_records = stabilized_key_wrap_records.as_ref().or(key_wrap_records);
2499 let (resolved_kdf_params, key_wrap_table) =
2500 resolve_key_wrap_artifacts(kdf_params, &archive_uuid, &session_id, key_wrap_records)?;
2501 let volume_format_rev = volume_format_revision_for_options(&options, &resolved_kdf_params);
2502 let subkeys = writer_subkeys(master_key, options.aead_algo, &archive_uuid, &session_id)?;
2503 let crypto_header = build_crypto_header(
2504 options,
2505 volume_format_rev,
2506 false,
2507 &subkeys,
2508 &archive_uuid,
2509 &session_id,
2510 &resolved_kdf_params,
2511 )?;
2512 let mut emission_state = begin_writer_emission_state(
2513 sink,
2514 options,
2515 &crypto_header,
2516 key_wrap_table.as_deref(),
2517 archive_uuid,
2518 session_id,
2519 volume_format_rev,
2520 root_auth.is_some(),
2521 )?;
2522
2523 start_write_phase(progress, ArchiveWritePhase::EmittingPayload);
2524 let emit_payload_started = Instant::now();
2525 let mut ordered = OrderedParallelState::new(0);
2526 let worker_count = options.jobs.max(1);
2527 let frame_job_buffer = worker_count.saturating_mul(4).max(1);
2528 let envelope_job_buffer = worker_count.saturating_mul(2).max(1);
2529 let subkeys_for_workers = std::sync::Arc::new(subkeys.clone());
2530
2531 std::thread::scope(|scope| -> Result<(), ArchiveWriteError> {
2532 let (frame_job_tx, frame_job_rx) =
2533 std::sync::mpsc::sync_channel::<OrderedFrameJob>(frame_job_buffer);
2534 let (frame_result_tx, frame_result_rx) =
2535 std::sync::mpsc::channel::<Result<OrderedFrameResult, ArchiveWriteError>>();
2536 let frame_job_rx = std::sync::Arc::new(std::sync::Mutex::new(frame_job_rx));
2537
2538 let (envelope_job_tx, envelope_job_rx) =
2539 std::sync::mpsc::sync_channel::<OrderedEnvelopeJob>(envelope_job_buffer);
2540 let (envelope_result_tx, envelope_result_rx) =
2541 std::sync::mpsc::channel::<Result<OrderedEnvelopeResult, ArchiveWriteError>>();
2542 let envelope_job_rx = std::sync::Arc::new(std::sync::Mutex::new(envelope_job_rx));
2543
2544 let frame_handles = (0..worker_count)
2545 .map(|_| {
2546 let frame_job_rx = std::sync::Arc::clone(&frame_job_rx);
2547 let frame_result_tx = frame_result_tx.clone();
2548 scope.spawn(move || loop {
2549 let job = {
2550 let receiver = frame_job_rx
2551 .lock()
2552 .expect("ordered frame receiver poisoned");
2553 receiver.recv()
2554 };
2555 let Ok(job) = job else {
2556 break;
2557 };
2558 let is_error = match build_ordered_frame_result(job, options) {
2559 Ok(result) => frame_result_tx.send(Ok(result)).is_err(),
2560 Err(error) => {
2561 let _ = frame_result_tx.send(Err(error));
2562 true
2563 }
2564 };
2565 if is_error {
2566 break;
2567 }
2568 })
2569 })
2570 .collect::<Vec<_>>();
2571 drop(frame_result_tx);
2572
2573 let envelope_handles = (0..worker_count)
2574 .map(|_| {
2575 let envelope_job_rx = std::sync::Arc::clone(&envelope_job_rx);
2576 let envelope_result_tx = envelope_result_tx.clone();
2577 let subkeys = std::sync::Arc::clone(&subkeys_for_workers);
2578 scope.spawn(move || loop {
2579 let job = {
2580 let receiver = envelope_job_rx
2581 .lock()
2582 .expect("ordered envelope receiver poisoned");
2583 receiver.recv()
2584 };
2585 let Ok(job) = job else {
2586 break;
2587 };
2588 let is_error = match build_ordered_envelope_result(
2589 job,
2590 &subkeys,
2591 options,
2592 archive_uuid,
2593 session_id,
2594 ) {
2595 Ok(result) => envelope_result_tx.send(Ok(result)).is_err(),
2596 Err(error) => {
2597 let _ = envelope_result_tx.send(Err(error));
2598 true
2599 }
2600 };
2601 if is_error {
2602 break;
2603 }
2604 })
2605 })
2606 .collect::<Vec<_>>();
2607 drop(envelope_result_tx);
2608
2609 {
2610 let mut writer = OrderedParallelArchiveWriter {
2611 frame_job_tx: &frame_job_tx,
2612 frame_result_rx: &frame_result_rx,
2613 envelope_job_tx: &envelope_job_tx,
2614 envelope_result_rx: &envelope_result_rx,
2615 ordered: &mut ordered,
2616 sink,
2617 options,
2618 emission_state: &mut emission_state,
2619 };
2620 drive_members(&mut writer)?;
2621 }
2622
2623 drop(frame_job_tx);
2624 while ordered.next_frame_result_index < ordered.next_frame_job_index {
2625 receive_ordered_frame_result(
2626 &frame_result_rx,
2627 &envelope_job_tx,
2628 &envelope_result_rx,
2629 &mut ordered,
2630 sink,
2631 options,
2632 &mut emission_state,
2633 true,
2634 )?;
2635 }
2636 flush_ordered_parallel_envelope(
2637 &envelope_job_tx,
2638 &envelope_result_rx,
2639 &mut ordered,
2640 sink,
2641 options,
2642 &mut emission_state,
2643 )?;
2644 drop(envelope_job_tx);
2645 while ordered.next_envelope_result_index < ordered.envelope.envelope_index {
2646 receive_ordered_envelope_result(
2647 &envelope_result_rx,
2648 &mut ordered,
2649 sink,
2650 options,
2651 &mut emission_state,
2652 true,
2653 )?;
2654 }
2655
2656 for handle in frame_handles {
2657 handle
2658 .join()
2659 .map_err(|_| FormatError::WriterInvariant("ordered frame worker panicked"))?;
2660 }
2661 for handle in envelope_handles {
2662 handle
2663 .join()
2664 .map_err(|_| FormatError::WriterInvariant("ordered envelope worker panicked"))?;
2665 }
2666 Ok(())
2667 })?;
2668 let emit_payload = emit_payload_started.elapsed();
2669
2670 emission_state.next_block_index = ordered.next_payload_block_index;
2671 let digest = ordered.hasher.finalize();
2672 let mut content_sha256 = [0u8; 32];
2673 content_sha256.copy_from_slice(&digest);
2674 let payload = PayloadPlanning {
2675 tar_members: ordered.tar_members,
2676 frames: ordered.frames,
2677 payload_objects: ordered.payload_objects,
2678 payload_block_count: ordered.payload_block_count,
2679 tar_total_size: ordered.tar_total_size,
2680 content_sha256,
2681 };
2682 start_write_phase(progress, ArchiveWritePhase::EmittingMetadata);
2683 let plan = build_writer_plan_from_payload(
2684 payload,
2685 emission_state.next_block_index,
2686 master_key,
2687 options,
2688 None,
2689 &resolved_kdf_params,
2690 key_wrap_records,
2691 archive_uuid,
2692 session_id,
2693 root_auth,
2694 )?;
2695 if plan.options != options || plan.crypto_header != crypto_header {
2696 return Err(FormatError::WriterUnsupported(
2697 "ordered parallel metadata exceeded the predeclared header class",
2698 )
2699 .into());
2700 }
2701 let mut summary = emit_writer_plan_suffix(
2702 &subkeys,
2703 root_auth,
2704 authenticator,
2705 plan,
2706 sink,
2707 emission_state,
2708 )?;
2709 summary.timings.emit_payload += emit_payload;
2710 summary.timings.total = total_started.elapsed();
2711 Ok(summary)
2712}
2713
2714pub(crate) struct OrderedParallelArchiveWriter<'a, O: ArchiveWriteSink> {
2715 frame_job_tx: &'a std::sync::mpsc::SyncSender<OrderedFrameJob>,
2716 frame_result_rx: &'a std::sync::mpsc::Receiver<Result<OrderedFrameResult, ArchiveWriteError>>,
2717 envelope_job_tx: &'a std::sync::mpsc::SyncSender<OrderedEnvelopeJob>,
2718 envelope_result_rx:
2719 &'a std::sync::mpsc::Receiver<Result<OrderedEnvelopeResult, ArchiveWriteError>>,
2720 ordered: &'a mut OrderedParallelState,
2721 sink: &'a mut O,
2722 options: WriterOptions,
2723 emission_state: &'a mut WriterEmissionState,
2724}
2725
2726impl<O: ArchiveWriteSink> OrderedParallelArchiveWriter<'_, O> {
2727 fn reserve_member_capacity(&mut self, additional: usize) {
2728 self.ordered.tar_members.reserve(additional);
2729 }
2730
2731 pub(crate) fn write_regular_member_from_reader(
2732 &mut self,
2733 member: StreamingRegularMember,
2734 payload: &mut dyn Read,
2735 ) -> Result<(), ArchiveWriteError> {
2736 let path = &member.archive_path;
2737 validate_file_path_bytes(path, self.options.max_path_length)?;
2738 if member.entry_kind != SourceEntryKind::Regular && member.file_data_size != 0 {
2739 return Err(FormatError::WriterInvariant(
2740 "non-regular source has non-zero file data size",
2741 )
2742 .into());
2743 }
2744 let source_payload_size = member
2745 .sparse_extents
2746 .as_deref()
2747 .map(|extents| sparse_extent_bytes(extents, member.file_data_size))
2748 .transpose()?
2749 .unwrap_or(member.file_data_size);
2750 let prefix = build_primary_member_prefix(
2751 path,
2752 member.entry_kind,
2753 member.link_target.as_deref(),
2754 member.file_data_size,
2755 member.sparse_extents.as_deref(),
2756 member.mode,
2757 member.mtime,
2758 &member.portable_metadata,
2759 )?;
2760 let member_group_size = checked_u64_add(
2761 prefix.len() as u64,
2762 checked_u64_add(
2763 source_payload_size,
2764 padding_to_512_u64(source_payload_size),
2765 "tar member",
2766 )?,
2767 "tar member",
2768 )?;
2769 let mut reader = StreamingMemberReader::new(Box::new(payload), prefix, source_payload_size);
2770 self.write_prebuilt_member(member, &mut reader, member_group_size)
2771 }
2772
2773 fn write_regular_member_from_source<S: RegularFileSource + ?Sized>(
2774 &mut self,
2775 source: &S,
2776 ) -> Result<(), ArchiveWriteError> {
2777 let member = StreamingRegularMember {
2778 archive_path: normalize_lookup_file_path(
2779 source.archive_path(),
2780 self.options.max_path_length,
2781 )?,
2782 entry_kind: source.entry_kind(),
2783 link_target: source.link_target().map(<[u8]>::to_vec),
2784 file_data_size: source.file_data_size(),
2785 sparse_extents: source.sparse_extents().map(<[SparseExtent]>::to_vec),
2786 mode: source.mode(),
2787 mtime: source.mtime(),
2788 portable_metadata: source.portable_metadata(),
2789 };
2790 if member.entry_kind != SourceEntryKind::Regular && member.file_data_size != 0 {
2791 return Err(FormatError::WriterInvariant(
2792 "non-regular source has non-zero file data size",
2793 )
2794 .into());
2795 }
2796 let source_payload_size = member
2797 .sparse_extents
2798 .as_deref()
2799 .map(|extents| sparse_extent_bytes(extents, member.file_data_size))
2800 .transpose()?
2801 .unwrap_or(member.file_data_size);
2802 let layout = build_primary_member_layout(
2803 &member.archive_path,
2804 member.entry_kind,
2805 member.link_target.as_deref(),
2806 member.file_data_size,
2807 member.sparse_extents.as_deref(),
2808 member.mode,
2809 member.mtime,
2810 &member.portable_metadata,
2811 )?;
2812 let member_group_size = primary_member_layout_size(&layout, source_payload_size)?;
2813 let mut reader = StreamingMemberReader::from_source(
2814 source,
2815 &member.portable_metadata,
2816 layout,
2817 source_payload_size,
2818 )?;
2819 self.write_prebuilt_member(member, &mut reader, member_group_size)
2820 }
2821
2822 fn write_prebuilt_member(
2823 &mut self,
2824 member: StreamingRegularMember,
2825 reader: &mut StreamingMemberReader<'_>,
2826 member_group_size: u64,
2827 ) -> Result<(), ArchiveWriteError> {
2828 let member_start = self.ordered.tar_total_size;
2829 let member_index = self.ordered.tar_members.len();
2830 self.ordered.tar_members.push(TarMember {
2831 path: member.archive_path,
2832 entry_kind: member.entry_kind,
2833 link_target: member.link_target,
2834 tar_member_group_start: member_start,
2835 tar_member_group_size: member_group_size,
2836 file_data_size: member.file_data_size,
2837 sparse_extents: member.sparse_extents,
2838 mode: member.mode,
2839 mtime: member.mtime,
2840 portable_metadata: member.portable_metadata,
2841 });
2842 let mut member_offset = 0u64;
2843 while member_offset < member_group_size {
2844 let remaining = member_group_size - member_offset;
2845 let read_len = remaining.min(self.options.chunk_size as u64);
2846 let mut plaintext = vec![0u8; to_usize_writer(read_len, "payload chunk")?];
2847 reader
2848 .read_exact(&mut plaintext)
2849 .map_err(ArchiveWriteError::Io)?;
2850 self.ordered.hasher.update(&plaintext);
2851 let frame_index = self.ordered.next_frame_job_index;
2852 self.ordered.next_frame_job_index = checked_u64_add(
2853 self.ordered.next_frame_job_index,
2854 1,
2855 "PayloadFrame.frame_index",
2856 )?;
2857 send_ordered_frame_job(
2858 OrderedFrameJob {
2859 frame_index,
2860 member_index,
2861 member_start,
2862 member_offset,
2863 member_group_size,
2864 plaintext,
2865 },
2866 self.frame_job_tx,
2867 self.frame_result_rx,
2868 self.envelope_job_tx,
2869 self.envelope_result_rx,
2870 self.ordered,
2871 self.sink,
2872 self.options,
2873 self.emission_state,
2874 )?;
2875 member_offset = checked_u64_add(member_offset, read_len, "payload chunk")?;
2876 self.ordered.tar_total_size =
2877 checked_u64_add(self.ordered.tar_total_size, read_len, "tar stream")?;
2878 }
2879 Ok(())
2880 }
2881}
2882
2883#[allow(clippy::too_many_arguments)]
2884fn send_ordered_frame_job<O: ArchiveWriteSink>(
2885 mut job: OrderedFrameJob,
2886 frame_job_tx: &std::sync::mpsc::SyncSender<OrderedFrameJob>,
2887 frame_result_rx: &std::sync::mpsc::Receiver<Result<OrderedFrameResult, ArchiveWriteError>>,
2888 envelope_job_tx: &std::sync::mpsc::SyncSender<OrderedEnvelopeJob>,
2889 envelope_result_rx: &std::sync::mpsc::Receiver<
2890 Result<OrderedEnvelopeResult, ArchiveWriteError>,
2891 >,
2892 ordered: &mut OrderedParallelState,
2893 sink: &mut O,
2894 options: WriterOptions,
2895 emission_state: &mut WriterEmissionState,
2896) -> Result<(), ArchiveWriteError> {
2897 loop {
2898 match frame_job_tx.try_send(job) {
2899 Ok(()) => {
2900 drain_ordered_frame_results(
2901 frame_result_rx,
2902 envelope_job_tx,
2903 envelope_result_rx,
2904 ordered,
2905 sink,
2906 options,
2907 emission_state,
2908 )?;
2909 return Ok(());
2910 }
2911 Err(std::sync::mpsc::TrySendError::Full(returned)) => {
2912 job = returned;
2913 receive_ordered_frame_result(
2914 frame_result_rx,
2915 envelope_job_tx,
2916 envelope_result_rx,
2917 ordered,
2918 sink,
2919 options,
2920 emission_state,
2921 true,
2922 )?;
2923 }
2924 Err(std::sync::mpsc::TrySendError::Disconnected(_)) => {
2925 return Err(FormatError::WriterInvariant("ordered frame worker stopped").into());
2926 }
2927 }
2928 }
2929}
2930
2931#[allow(clippy::too_many_arguments)]
2932fn drain_ordered_frame_results<O: ArchiveWriteSink>(
2933 frame_result_rx: &std::sync::mpsc::Receiver<Result<OrderedFrameResult, ArchiveWriteError>>,
2934 envelope_job_tx: &std::sync::mpsc::SyncSender<OrderedEnvelopeJob>,
2935 envelope_result_rx: &std::sync::mpsc::Receiver<
2936 Result<OrderedEnvelopeResult, ArchiveWriteError>,
2937 >,
2938 ordered: &mut OrderedParallelState,
2939 sink: &mut O,
2940 options: WriterOptions,
2941 emission_state: &mut WriterEmissionState,
2942) -> Result<(), ArchiveWriteError> {
2943 while receive_ordered_frame_result(
2944 frame_result_rx,
2945 envelope_job_tx,
2946 envelope_result_rx,
2947 ordered,
2948 sink,
2949 options,
2950 emission_state,
2951 false,
2952 )? {}
2953 Ok(())
2954}
2955
2956#[allow(clippy::too_many_arguments)]
2957fn receive_ordered_frame_result<O: ArchiveWriteSink>(
2958 frame_result_rx: &std::sync::mpsc::Receiver<Result<OrderedFrameResult, ArchiveWriteError>>,
2959 envelope_job_tx: &std::sync::mpsc::SyncSender<OrderedEnvelopeJob>,
2960 envelope_result_rx: &std::sync::mpsc::Receiver<
2961 Result<OrderedEnvelopeResult, ArchiveWriteError>,
2962 >,
2963 ordered: &mut OrderedParallelState,
2964 sink: &mut O,
2965 options: WriterOptions,
2966 emission_state: &mut WriterEmissionState,
2967 wait: bool,
2968) -> Result<bool, ArchiveWriteError> {
2969 let result = if wait {
2970 match frame_result_rx.recv() {
2971 Ok(result) => result?,
2972 Err(_) => {
2973 return Err(FormatError::WriterInvariant("ordered frame worker stopped").into())
2974 }
2975 }
2976 } else {
2977 match frame_result_rx.try_recv() {
2978 Ok(result) => result?,
2979 Err(std::sync::mpsc::TryRecvError::Empty) => return Ok(false),
2980 Err(std::sync::mpsc::TryRecvError::Disconnected) => return Ok(false),
2981 }
2982 };
2983 ordered.frame_buffer.insert(result.frame_index, result);
2984 while let Some(result) = ordered
2985 .frame_buffer
2986 .remove(&ordered.next_frame_result_index)
2987 {
2988 append_ordered_frame_result(
2989 result,
2990 envelope_job_tx,
2991 envelope_result_rx,
2992 ordered,
2993 sink,
2994 options,
2995 emission_state,
2996 )?;
2997 ordered.next_frame_result_index = checked_u64_add(
2998 ordered.next_frame_result_index,
2999 1,
3000 "PayloadFrame.frame_index",
3001 )?;
3002 }
3003 Ok(true)
3004}
3005
3006#[allow(clippy::too_many_arguments)]
3007fn append_ordered_frame_result<O: ArchiveWriteSink>(
3008 result: OrderedFrameResult,
3009 envelope_job_tx: &std::sync::mpsc::SyncSender<OrderedEnvelopeJob>,
3010 envelope_result_rx: &std::sync::mpsc::Receiver<
3011 Result<OrderedEnvelopeResult, ArchiveWriteError>,
3012 >,
3013 ordered: &mut OrderedParallelState,
3014 sink: &mut O,
3015 options: WriterOptions,
3016 emission_state: &mut WriterEmissionState,
3017) -> Result<(), ArchiveWriteError> {
3018 if payload_envelope_needs_flush(&ordered.envelope, result.frame.len(), options)? {
3019 flush_ordered_parallel_envelope(
3020 envelope_job_tx,
3021 envelope_result_rx,
3022 ordered,
3023 sink,
3024 options,
3025 emission_state,
3026 )?;
3027 }
3028 if ordered.envelope.plaintext.is_empty()
3029 && !payload_object_can_fit(result.frame.len(), options)?
3030 {
3031 return Err(
3032 FormatError::WriterUnsupported("payload frame exceeds envelope object limits").into(),
3033 );
3034 }
3035 let offset = u32_len(
3036 ordered.envelope.plaintext.len(),
3037 "FrameEntry.offset_in_envelope",
3038 )?;
3039 ordered.envelope.plaintext.extend_from_slice(&result.frame);
3040 ordered
3041 .frames
3042 .push(payload_frame_metadata(PayloadFrameMetadataInput {
3043 frame_index: ordered.next_frame_metadata_index,
3044 envelope_index: ordered.envelope.envelope_index,
3045 member_index: result.member_index,
3046 offset_in_envelope: offset,
3047 compressed_size: result.frame.len(),
3048 decompressed_size: result.decompressed_size,
3049 member_start: result.member_start,
3050 member_offset: result.member_offset,
3051 member_group_size: result.member_group_size,
3052 })?);
3053 ordered.next_frame_metadata_index = checked_u64_add(
3054 ordered.next_frame_metadata_index,
3055 1,
3056 "PayloadFrame.frame_index",
3057 )?;
3058 Ok(())
3059}
3060
3061#[allow(clippy::too_many_arguments)]
3062fn flush_ordered_parallel_envelope<O: ArchiveWriteSink>(
3063 envelope_job_tx: &std::sync::mpsc::SyncSender<OrderedEnvelopeJob>,
3064 envelope_result_rx: &std::sync::mpsc::Receiver<
3065 Result<OrderedEnvelopeResult, ArchiveWriteError>,
3066 >,
3067 ordered: &mut OrderedParallelState,
3068 sink: &mut O,
3069 options: WriterOptions,
3070 emission_state: &mut WriterEmissionState,
3071) -> Result<(), ArchiveWriteError> {
3072 if ordered.envelope.plaintext.is_empty() {
3073 return Ok(());
3074 }
3075 let plaintext_size = u32_len(
3076 ordered.envelope.plaintext.len(),
3077 "EnvelopeEntry.plaintext_size",
3078 )?;
3079 let object_plan = plan_encrypted_object(
3080 ordered.envelope.plaintext.len(),
3081 options.fec_data_shards,
3082 options.fec_parity_shards,
3083 options,
3084 )?;
3085 let extent = ObjectExtent::new(ordered.next_payload_block_index, object_plan)?;
3086 ordered.next_payload_block_index = extent.next_block_index()?;
3087 ordered.payload_block_count = checked_u64_add(
3088 ordered.payload_block_count,
3089 extent.data_block_count as u64,
3090 "payload",
3091 )?;
3092 ordered.payload_objects.push(PayloadObject {
3093 envelope_index: ordered.envelope.envelope_index,
3094 plaintext_size,
3095 object: extent,
3096 });
3097 let mut job = OrderedEnvelopeJob {
3098 envelope_index: ordered.envelope.envelope_index,
3099 plaintext: std::mem::take(&mut ordered.envelope.plaintext),
3100 extent,
3101 collect_data_leaf_hashes: emission_state.data_leaf_hashes.is_some(),
3102 };
3103 ordered.envelope.envelope_index =
3104 checked_u64_add(ordered.envelope.envelope_index, 1, "EnvelopeEntry")?;
3105 loop {
3106 match envelope_job_tx.try_send(job) {
3107 Ok(()) => {
3108 drain_ordered_envelope_results(
3109 envelope_result_rx,
3110 ordered,
3111 sink,
3112 options,
3113 emission_state,
3114 )?;
3115 return Ok(());
3116 }
3117 Err(std::sync::mpsc::TrySendError::Full(returned)) => {
3118 job = returned;
3119 receive_ordered_envelope_result(
3120 envelope_result_rx,
3121 ordered,
3122 sink,
3123 options,
3124 emission_state,
3125 true,
3126 )?;
3127 }
3128 Err(std::sync::mpsc::TrySendError::Disconnected(_)) => {
3129 return Err(FormatError::WriterInvariant("ordered envelope worker stopped").into());
3130 }
3131 }
3132 }
3133}
3134
3135fn drain_ordered_envelope_results<O: ArchiveWriteSink>(
3136 envelope_result_rx: &std::sync::mpsc::Receiver<
3137 Result<OrderedEnvelopeResult, ArchiveWriteError>,
3138 >,
3139 ordered: &mut OrderedParallelState,
3140 sink: &mut O,
3141 options: WriterOptions,
3142 emission_state: &mut WriterEmissionState,
3143) -> Result<(), ArchiveWriteError> {
3144 while receive_ordered_envelope_result(
3145 envelope_result_rx,
3146 ordered,
3147 sink,
3148 options,
3149 emission_state,
3150 false,
3151 )? {}
3152 Ok(())
3153}
3154
3155fn receive_ordered_envelope_result<O: ArchiveWriteSink>(
3156 envelope_result_rx: &std::sync::mpsc::Receiver<
3157 Result<OrderedEnvelopeResult, ArchiveWriteError>,
3158 >,
3159 ordered: &mut OrderedParallelState,
3160 sink: &mut O,
3161 options: WriterOptions,
3162 emission_state: &mut WriterEmissionState,
3163 wait: bool,
3164) -> Result<bool, ArchiveWriteError> {
3165 let result = if wait {
3166 match envelope_result_rx.recv() {
3167 Ok(result) => result?,
3168 Err(_) => {
3169 return Err(FormatError::WriterInvariant("ordered envelope worker stopped").into());
3170 }
3171 }
3172 } else {
3173 match envelope_result_rx.try_recv() {
3174 Ok(result) => result?,
3175 Err(std::sync::mpsc::TryRecvError::Empty) => return Ok(false),
3176 Err(std::sync::mpsc::TryRecvError::Disconnected) => return Ok(false),
3177 }
3178 };
3179 ordered
3180 .envelope_buffer
3181 .insert(result.envelope_index, result);
3182 while let Some(result) = ordered
3183 .envelope_buffer
3184 .remove(&ordered.next_envelope_result_index)
3185 {
3186 emit_ordered_envelope_result(result, sink, options, emission_state)?;
3187 ordered.next_envelope_result_index =
3188 checked_u64_add(ordered.next_envelope_result_index, 1, "EnvelopeEntry")?;
3189 }
3190 Ok(true)
3191}
3192
3193fn build_ordered_frame_result(
3194 job: OrderedFrameJob,
3195 options: WriterOptions,
3196) -> Result<OrderedFrameResult, ArchiveWriteError> {
3197 let frame = compress_zstd_frame_with_jobs(&job.plaintext, options.zstd_level, 1)?;
3198 Ok(OrderedFrameResult {
3199 frame_index: job.frame_index,
3200 member_index: job.member_index,
3201 member_start: job.member_start,
3202 member_offset: job.member_offset,
3203 member_group_size: job.member_group_size,
3204 decompressed_size: job.plaintext.len(),
3205 frame,
3206 })
3207}
3208
3209fn build_ordered_envelope_result(
3210 job: OrderedEnvelopeJob,
3211 subkeys: &Subkeys,
3212 options: WriterOptions,
3213 archive_uuid: [u8; 16],
3214 session_id: [u8; 16],
3215) -> Result<OrderedEnvelopeResult, ArchiveWriteError> {
3216 let context = ObjectEncryptionContext {
3217 key: &subkeys.enc_key,
3218 nonce_seed: &subkeys.nonce_seed,
3219 domain: b"envelope",
3220 counter: job.envelope_index,
3221 data_kind: BlockKind::PayloadData,
3222 parity_kind: BlockKind::PayloadParity,
3223 data_shard_max: options.fec_data_shards,
3224 class_parity_shard_max: options.fec_parity_shards,
3225 archive_uuid: &archive_uuid,
3226 session_id: &session_id,
3227 };
3228 if job.extent.parity_block_count == 0 && !job.collect_data_leaf_hashes {
3229 return Ok(OrderedEnvelopeResult {
3230 envelope_index: job.envelope_index,
3231 records: OrderedEnvelopeRecords::Serialized(serialize_zero_parity_encrypted_object(
3232 &job.plaintext,
3233 context,
3234 job.extent,
3235 options,
3236 )?),
3237 });
3238 }
3239
3240 let mut local_next_block_index = job.extent.first_block_index;
3241 let object = encrypt_object(
3242 &job.plaintext,
3243 context,
3244 &mut local_next_block_index,
3245 options,
3246 )?;
3247 validate_planned_extent(&object, job.extent)?;
3248 Ok(OrderedEnvelopeResult {
3249 envelope_index: job.envelope_index,
3250 records: OrderedEnvelopeRecords::Materialized(object.records),
3251 })
3252}
3253
3254fn emit_ordered_envelope_result<O: ArchiveWriteSink>(
3255 result: OrderedEnvelopeResult,
3256 sink: &mut O,
3257 options: WriterOptions,
3258 emission_state: &mut WriterEmissionState,
3259) -> Result<(), ArchiveWriteError> {
3260 match result.records {
3261 OrderedEnvelopeRecords::Materialized(records) => {
3262 for record in &records {
3263 emit_block_record(
3264 sink,
3265 options,
3266 &mut emission_state.bytes_written,
3267 &mut emission_state.record_counts,
3268 emission_state.volume_format_rev,
3269 &mut emission_state.data_leaf_hashes,
3270 record,
3271 )?;
3272 }
3273 }
3274 OrderedEnvelopeRecords::Serialized(records) => {
3275 for record in &records {
3276 emit_serialized_block_record(
3277 sink,
3278 options,
3279 &mut emission_state.bytes_written,
3280 &mut emission_state.record_counts,
3281 record,
3282 )?;
3283 }
3284 }
3285 }
3286 Ok(())
3287}
3288
3289fn validate_single_pass_writer_options(options: WriterOptions) -> Result<(), FormatError> {
3290 if options.volume_loss_tolerance != 0 {
3291 return Err(FormatError::WriterUnsupported(
3292 "streaming create cannot tolerate volume loss",
3293 ));
3294 }
3295 if options.target_volume_size.is_some() {
3296 return Err(FormatError::WriterUnsupported(
3297 "streaming create does not support target volume sizing",
3298 ));
3299 }
3300 Ok(())
3301}
3302
3303#[allow(clippy::too_many_arguments)]
3304fn begin_writer_emission_state<O: ArchiveWriteSink>(
3305 sink: &mut O,
3306 options: WriterOptions,
3307 crypto_header: &[u8],
3308 key_wrap_table: Option<&[u8]>,
3309 archive_uuid: [u8; 16],
3310 session_id: [u8; 16],
3311 volume_format_rev: u16,
3312 collect_data_leaf_hashes: bool,
3313) -> Result<WriterEmissionState, ArchiveWriteError> {
3314 let volume_count = usize::try_from(options.stripe_width)
3315 .map_err(|_| FormatError::WriterUnsupported("stripe_width"))?;
3316 sink.begin_archive(volume_count)?;
3317
3318 let mut state = WriterEmissionState {
3319 volume_headers: Vec::with_capacity(volume_count),
3320 bytes_written: vec![0u64; volume_count],
3321 record_counts: vec![0u64; volume_count],
3322 volume_format_rev,
3323 data_leaf_hashes: collect_data_leaf_hashes.then(Vec::new),
3324 next_block_index: 0,
3325 };
3326
3327 for volume_index in 0..volume_count {
3328 let volume_index_u32 = u32::try_from(volume_index)
3329 .map_err(|_| FormatError::WriterUnsupported("volume_index"))?;
3330 let volume_header = VolumeHeader {
3331 format_version: FORMAT_VERSION,
3332 volume_format_rev,
3333 volume_index: volume_index_u32,
3334 stripe_width: options.stripe_width,
3335 archive_uuid,
3336 session_id,
3337 crypto_header_offset: VOLUME_HEADER_LEN as u32,
3338 crypto_header_length: u32_len(crypto_header.len(), "CryptoHeader")?,
3339 header_crc32c: 0,
3340 };
3341 let volume_header_bytes = volume_header.to_bytes();
3342 sink.write_volume(volume_index, &volume_header_bytes)?;
3343 sink.write_volume(volume_index, crypto_header)?;
3344 let mut bytes_written = checked_u64_add(
3345 VOLUME_HEADER_LEN as u64,
3346 crypto_header.len() as u64,
3347 "volume header",
3348 )?;
3349 if let Some(key_wrap_table) = key_wrap_table {
3350 sink.write_volume(volume_index, key_wrap_table)?;
3351 bytes_written =
3352 checked_u64_add(bytes_written, key_wrap_table.len() as u64, "KeyWrapTableV1")?;
3353 }
3354 state.bytes_written[volume_index] = bytes_written;
3355 state.volume_headers.push(volume_header_bytes);
3356 }
3357
3358 Ok(state)
3359}
3360
3361fn plan_single_pass_writer_options(options: WriterOptions) -> Result<WriterOptions, FormatError> {
3362 let mut options = plan_writer_options(options)?;
3363 options.index_root_fec_data_shards = max_single_pass_index_root_data_shards(options)?;
3364 plan_writer_options(options)
3365}
3366
3367fn max_single_pass_index_root_data_shards(options: WriterOptions) -> Result<u16, FormatError> {
3368 let block_size_limit = (u32::MAX as u64 / options.block_size as u64).min(u16::MAX as u64);
3369 let mut low = MIN_INDEX_ROOT_FEC_DATA_SHARDS as u64;
3370 let mut high = block_size_limit;
3371 let mut best = low;
3372 while low <= high {
3373 let mid = low + (high - low) / 2;
3374 match compute_parity(mid, options) {
3375 Ok(parity)
3376 if mid + u64::from(parity) <= READER_MAX_INDEX_ROOT_FEC_CLASS_SHARDS as u64 =>
3377 {
3378 best = mid;
3379 low = mid + 1;
3380 }
3381 _ => {
3382 if mid == 0 {
3383 break;
3384 }
3385 high = mid - 1;
3386 }
3387 }
3388 }
3389 u16::try_from(best).map_err(|_| FormatError::WriterUnsupported("index_root_fec_data_shards"))
3390}
3391
3392impl<O: ArchiveWriteSink> StreamingArchiveWriter<'_, O> {
3393 fn write_regular_member_from_source<S: RegularFileSource + ?Sized>(
3394 &mut self,
3395 source: &S,
3396 ) -> Result<(), ArchiveWriteError> {
3397 let member = StreamingRegularMember {
3398 archive_path: normalize_lookup_file_path(
3399 source.archive_path(),
3400 self.options.max_path_length,
3401 )?,
3402 entry_kind: source.entry_kind(),
3403 link_target: source.link_target().map(<[u8]>::to_vec),
3404 file_data_size: source.file_data_size(),
3405 sparse_extents: source.sparse_extents().map(<[SparseExtent]>::to_vec),
3406 mode: source.mode(),
3407 mtime: source.mtime(),
3408 portable_metadata: source.portable_metadata(),
3409 };
3410 if member.entry_kind != SourceEntryKind::Regular && member.file_data_size != 0 {
3411 return Err(FormatError::WriterInvariant(
3412 "non-regular source has non-zero file data size",
3413 )
3414 .into());
3415 }
3416 let source_payload_size = member
3417 .sparse_extents
3418 .as_deref()
3419 .map(|extents| sparse_extent_bytes(extents, member.file_data_size))
3420 .transpose()?
3421 .unwrap_or(member.file_data_size);
3422 let layout = build_primary_member_layout(
3423 &member.archive_path,
3424 member.entry_kind,
3425 member.link_target.as_deref(),
3426 member.file_data_size,
3427 member.sparse_extents.as_deref(),
3428 member.mode,
3429 member.mtime,
3430 &member.portable_metadata,
3431 )?;
3432 let member_group_size = primary_member_layout_size(&layout, source_payload_size)?;
3433 let mut reader = StreamingMemberReader::from_source(
3434 source,
3435 &member.portable_metadata,
3436 layout,
3437 source_payload_size,
3438 )?;
3439 self.write_prebuilt_member(member, &mut reader, member_group_size)
3440 }
3441
3442 fn write_prebuilt_member(
3443 &mut self,
3444 member: StreamingRegularMember,
3445 reader: &mut StreamingMemberReader<'_>,
3446 member_group_size: u64,
3447 ) -> Result<(), ArchiveWriteError> {
3448 let member_start = self.tar_total_size;
3449 let member_index = self.tar_members.len();
3450 self.tar_members.push(TarMember {
3451 path: member.archive_path,
3452 entry_kind: member.entry_kind,
3453 link_target: member.link_target,
3454 tar_member_group_start: member_start,
3455 tar_member_group_size: member_group_size,
3456 file_data_size: member.file_data_size,
3457 sparse_extents: member.sparse_extents,
3458 mode: member.mode,
3459 mtime: member.mtime,
3460 portable_metadata: member.portable_metadata,
3461 });
3462 let mut member_offset = 0u64;
3463 while member_offset < member_group_size {
3464 let remaining = member_group_size - member_offset;
3465 let max_chunk = remaining.min(self.options.chunk_size as u64);
3466 let mut chunk = vec![0u8; to_usize_writer(max_chunk, "payload chunk")?];
3467 reader
3468 .read_exact(&mut chunk)
3469 .map_err(ArchiveWriteError::Io)?;
3470 let mut chunk_len = chunk.len();
3471 let frame = loop {
3472 let candidate = &chunk[..chunk_len];
3473 let frame = compress_zstd_frame_with_jobs(
3474 candidate,
3475 self.options.zstd_level,
3476 self.options.jobs,
3477 )?;
3478 if payload_object_can_fit(frame.len(), self.options)? {
3479 break frame;
3480 }
3481 if chunk_len == 1 {
3482 return Err(FormatError::WriterUnsupported(
3483 "single-byte payload frame exceeds envelope object limits",
3484 )
3485 .into());
3486 }
3487 chunk_len = (chunk_len / 2).max(1);
3488 };
3489 if chunk_len < chunk.len() {
3490 reader.push_back(chunk[chunk_len..].to_vec());
3491 }
3492 let chunk = &chunk[..chunk_len];
3493 self.hasher.update(chunk);
3494 self.append_payload_frame(
3495 &frame,
3496 chunk_len,
3497 member_index,
3498 member_start,
3499 member_offset,
3500 member_group_size,
3501 )?;
3502 member_offset = checked_u64_add(member_offset, chunk_len as u64, "payload chunk")?;
3503 self.tar_total_size =
3504 checked_u64_add(self.tar_total_size, chunk_len as u64, "tar stream")?;
3505 }
3506 Ok(())
3507 }
3508
3509 fn append_payload_frame(
3510 &mut self,
3511 frame: &[u8],
3512 decompressed_size: usize,
3513 member_index: usize,
3514 member_start: u64,
3515 member_offset: u64,
3516 member_group_size: u64,
3517 ) -> Result<(), ArchiveWriteError> {
3518 if payload_envelope_needs_flush(&self.envelope, frame.len(), self.options)? {
3519 self.flush_payload_envelope()?;
3520 }
3521 if self.envelope.plaintext.is_empty() && !payload_object_can_fit(frame.len(), self.options)?
3522 {
3523 return Err(FormatError::WriterUnsupported(
3524 "payload frame exceeds envelope object limits",
3525 )
3526 .into());
3527 }
3528 let offset = u32_len(
3529 self.envelope.plaintext.len(),
3530 "FrameEntry.offset_in_envelope",
3531 )?;
3532 self.envelope.plaintext.extend_from_slice(frame);
3533 self.frames
3534 .push(payload_frame_metadata(PayloadFrameMetadataInput {
3535 frame_index: self.next_frame_index,
3536 envelope_index: self.envelope.envelope_index,
3537 member_index,
3538 offset_in_envelope: offset,
3539 compressed_size: frame.len(),
3540 decompressed_size,
3541 member_start,
3542 member_offset,
3543 member_group_size,
3544 })?);
3545 self.next_frame_index =
3546 checked_u64_add(self.next_frame_index, 1, "PayloadFrame.frame_index")?;
3547 Ok(())
3548 }
3549
3550 fn flush_payload_envelope(&mut self) -> Result<(), ArchiveWriteError> {
3551 if self.envelope.plaintext.is_empty() {
3552 return Ok(());
3553 }
3554 let plaintext_size = u32_len(
3555 self.envelope.plaintext.len(),
3556 "EnvelopeEntry.plaintext_size",
3557 )?;
3558 let object_plan = plan_encrypted_object(
3559 self.envelope.plaintext.len(),
3560 self.options.fec_data_shards,
3561 self.options.fec_parity_shards,
3562 self.options,
3563 )?;
3564 let extent = ObjectExtent::new(self.emission_state.next_block_index, object_plan)?;
3565 let object = encrypt_object(
3566 &self.envelope.plaintext,
3567 ObjectEncryptionContext {
3568 key: &self.subkeys.enc_key,
3569 nonce_seed: &self.subkeys.nonce_seed,
3570 domain: b"envelope",
3571 counter: self.envelope.envelope_index,
3572 data_kind: BlockKind::PayloadData,
3573 parity_kind: BlockKind::PayloadParity,
3574 data_shard_max: self.options.fec_data_shards,
3575 class_parity_shard_max: self.options.fec_parity_shards,
3576 archive_uuid: &self.archive_uuid,
3577 session_id: &self.session_id,
3578 },
3579 &mut self.emission_state.next_block_index,
3580 self.options,
3581 )?;
3582 validate_planned_extent(&object, extent)?;
3583 for record in &object.records {
3584 emit_block_record(
3585 self.sink,
3586 self.options,
3587 &mut self.emission_state.bytes_written,
3588 &mut self.emission_state.record_counts,
3589 self.emission_state.volume_format_rev,
3590 &mut self.emission_state.data_leaf_hashes,
3591 record,
3592 )?;
3593 }
3594 self.payload_block_count = checked_u64_add(
3595 self.payload_block_count,
3596 extent.data_block_count as u64,
3597 "payload",
3598 )?;
3599 self.payload_objects.push(PayloadObject {
3600 envelope_index: self.envelope.envelope_index,
3601 plaintext_size,
3602 object: extent,
3603 });
3604 self.envelope.envelope_index =
3605 checked_u64_add(self.envelope.envelope_index, 1, "EnvelopeEntry")?;
3606 self.envelope.plaintext.clear();
3607 Ok(())
3608 }
3609
3610 fn finish(
3611 mut self,
3612 master_key: &MasterKey,
3613 kdf_params: &KdfParams,
3614 key_wrap_records: Option<&KeyWrapRecordSource>,
3615 root_auth: Option<RootAuthWriterConfig<'_>>,
3616 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
3617 progress: Option<&SourceProgressHandle<'_>>,
3618 ) -> Result<WrittenArchiveSummary, ArchiveWriteError> {
3619 self.flush_payload_envelope()?;
3620 let digest = self.hasher.finalize();
3621 let mut content_sha256 = [0u8; 32];
3622 content_sha256.copy_from_slice(&digest);
3623 let payload = PayloadPlanning {
3624 tar_members: self.tar_members,
3625 frames: self.frames,
3626 payload_objects: self.payload_objects,
3627 payload_block_count: self.payload_block_count,
3628 tar_total_size: self.tar_total_size,
3629 content_sha256,
3630 };
3631 start_write_phase(progress, ArchiveWritePhase::EmittingMetadata);
3632 let plan = build_writer_plan_from_payload(
3633 payload,
3634 self.emission_state.next_block_index,
3635 master_key,
3636 self.options,
3637 None,
3638 kdf_params,
3639 key_wrap_records,
3640 self.archive_uuid,
3641 self.session_id,
3642 root_auth,
3643 )?;
3644 if plan.options != self.options || plan.crypto_header != self.crypto_header {
3645 return Err(FormatError::WriterUnsupported(
3646 "streaming tar stdin metadata exceeded the predeclared header class",
3647 )
3648 .into());
3649 }
3650 emit_writer_plan_suffix(
3651 &self.subkeys,
3652 root_auth,
3653 authenticator,
3654 plan,
3655 self.sink,
3656 self.emission_state,
3657 )
3658 }
3659}
3660
3661struct PayloadFramePlanState<'a> {
3662 envelope: &'a mut PayloadEnvelopeBuilder,
3663 payload_objects: &'a mut Vec<PayloadObject>,
3664 payload_block_count: &'a mut u64,
3665 next_block_index: &'a mut u64,
3666 frames: &'a mut Vec<PayloadFrame>,
3667 next_frame_index: &'a mut u64,
3668 options: WriterOptions,
3669}
3670
3671struct PayloadFramePlanInput<'a> {
3672 frame: &'a [u8],
3673 decompressed_size: usize,
3674 member_index: usize,
3675 member_start: u64,
3676 member_offset: u64,
3677 member_group_size: u64,
3678}
3679
3680fn append_payload_frame_to_plan(
3681 state: PayloadFramePlanState<'_>,
3682 input: PayloadFramePlanInput<'_>,
3683) -> Result<(), FormatError> {
3684 if payload_envelope_needs_flush(state.envelope, input.frame.len(), state.options)? {
3685 flush_payload_envelope_plan(
3686 state.envelope,
3687 state.payload_objects,
3688 state.payload_block_count,
3689 state.next_block_index,
3690 state.options,
3691 )?;
3692 }
3693 if state.envelope.plaintext.is_empty()
3694 && !payload_object_can_fit(input.frame.len(), state.options)?
3695 {
3696 return Err(FormatError::WriterUnsupported(
3697 "payload frame exceeds envelope object limits",
3698 ));
3699 }
3700 let offset = u32_len(
3701 state.envelope.plaintext.len(),
3702 "FrameEntry.offset_in_envelope",
3703 )?;
3704 state.envelope.plaintext.extend_from_slice(input.frame);
3705 state
3706 .frames
3707 .push(payload_frame_metadata(PayloadFrameMetadataInput {
3708 frame_index: *state.next_frame_index,
3709 envelope_index: state.envelope.envelope_index,
3710 member_index: input.member_index,
3711 offset_in_envelope: offset,
3712 compressed_size: input.frame.len(),
3713 decompressed_size: input.decompressed_size,
3714 member_start: input.member_start,
3715 member_offset: input.member_offset,
3716 member_group_size: input.member_group_size,
3717 })?);
3718 *state.next_frame_index =
3719 checked_u64_add(*state.next_frame_index, 1, "PayloadFrame.frame_index")?;
3720 Ok(())
3721}
3722
3723fn flush_payload_envelope_plan(
3724 envelope: &mut PayloadEnvelopeBuilder,
3725 payload_objects: &mut Vec<PayloadObject>,
3726 payload_block_count: &mut u64,
3727 next_block_index: &mut u64,
3728 options: WriterOptions,
3729) -> Result<(), FormatError> {
3730 let plaintext_size = u32_len(envelope.plaintext.len(), "EnvelopeEntry.plaintext_size")?;
3731 let object_plan = plan_encrypted_object(
3732 envelope.plaintext.len(),
3733 options.fec_data_shards,
3734 options.fec_parity_shards,
3735 options,
3736 )?;
3737 let extent = ObjectExtent::new(*next_block_index, object_plan)?;
3738 *next_block_index = extent.next_block_index()?;
3739 *payload_block_count = checked_u64_add(
3740 *payload_block_count,
3741 extent.data_block_count as u64,
3742 "payload",
3743 )?;
3744 payload_objects.push(PayloadObject {
3745 envelope_index: envelope.envelope_index,
3746 plaintext_size,
3747 object: extent,
3748 });
3749 envelope.envelope_index = checked_u64_add(envelope.envelope_index, 1, "EnvelopeEntry")?;
3750 envelope.plaintext.clear();
3751 Ok(())
3752}
3753
3754fn required_stripe_width_for_plan(
3755 plan: &WriterPlan,
3756 master_key: &MasterKey,
3757 target_volume_size: u64,
3758) -> Result<u32, FormatError> {
3759 let subkeys = writer_subkeys(
3760 master_key,
3761 plan.options.aead_algo,
3762 &plan.archive_uuid,
3763 &plan.session_id,
3764 )?;
3765 let mut max_volume_size = 0u64;
3766 let mut max_overhead = 0u64;
3767 let block_record_len = plan.options.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
3768 for volume_index in 0..plan.options.stripe_width {
3769 let block_count = striped_block_count(
3770 plan.total_block_count,
3771 plan.options.stripe_width,
3772 volume_index,
3773 );
3774 let volume_size = planned_v41_volume_size(plan, &subkeys, volume_index, block_count)?;
3775 max_volume_size = max_volume_size.max(volume_size);
3776 let record_bytes = checked_u64_mul(block_count, block_record_len, "volume records")?;
3777 let overhead =
3778 volume_size
3779 .checked_sub(record_bytes)
3780 .ok_or(FormatError::WriterInvariant(
3781 "planned volume record overflow",
3782 ))?;
3783 max_overhead = max_overhead.max(overhead);
3784 }
3785 if max_volume_size <= target_volume_size {
3786 return Ok(plan.options.stripe_width);
3787 }
3788 if target_volume_size <= max_overhead {
3789 return Err(FormatError::WriterUnsupported(
3790 "volume-size is too small for per-volume metadata",
3791 ));
3792 }
3793
3794 let records_per_volume = (target_volume_size - max_overhead) / block_record_len;
3795 if records_per_volume == 0 {
3796 return Err(FormatError::WriterUnsupported(
3797 "volume-size is too small for the configured block-size",
3798 ));
3799 }
3800
3801 let required = ceil_div(plan.total_block_count, records_per_volume)?
3802 .max(plan.options.volume_loss_tolerance as u64 + 1)
3803 .max(1);
3804 u32::try_from(required).map_err(|_| FormatError::WriterUnsupported("volume count"))
3805}
3806
3807fn planned_v41_volume_size(
3808 plan: &WriterPlan,
3809 subkeys: &Subkeys,
3810 volume_index: u32,
3811 block_count: u64,
3812) -> Result<u64, FormatError> {
3813 let volume_header = VolumeHeader {
3814 format_version: FORMAT_VERSION,
3815 volume_format_rev: plan.volume_format_rev,
3816 volume_index,
3817 stripe_width: plan.options.stripe_width,
3818 archive_uuid: plan.archive_uuid,
3819 session_id: plan.session_id,
3820 crypto_header_offset: VOLUME_HEADER_LEN as u32,
3821 crypto_header_length: u32_len(plan.crypto_header.len(), "CryptoHeader")?,
3822 header_crc32c: 0,
3823 };
3824 let volume_header_bytes = volume_header.to_bytes();
3825 let block_record_len = plan.options.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
3826 let block_record_bytes = checked_u64_mul(block_count, block_record_len, "volume records")?;
3827 let manifest_footer_offset = checked_u64_add(
3828 plan.block_records_offset,
3829 block_record_bytes,
3830 "volume records",
3831 )?;
3832 let manifest_footer = build_manifest_footer(
3833 subkeys,
3834 plan.options.aead_algo,
3835 plan.volume_format_rev,
3836 plan.archive_uuid,
3837 plan.session_id,
3838 volume_index,
3839 plan.options.stripe_width,
3840 &plan.index_root_extent,
3841 plan.index_root_plaintext.len(),
3842 )?;
3843 let root_auth_footer = plan
3844 .root_auth_footer_length
3845 .map(|length| vec![0u8; length as usize]);
3846 let root_auth_footer_offset = root_auth_footer
3847 .as_ref()
3848 .map(|_| {
3849 checked_u64_add(
3850 manifest_footer_offset,
3851 MANIFEST_FOOTER_LEN as u64,
3852 "RootAuthFooterV1",
3853 )
3854 })
3855 .transpose()?;
3856 let trailer_offset = checked_u64_add(
3857 manifest_footer_offset,
3858 MANIFEST_FOOTER_LEN as u64 + u64::from(plan.root_auth_footer_length.unwrap_or(0)),
3859 "VolumeTrailer",
3860 )?;
3861 let trailer = build_volume_trailer(VolumeTrailerBuildInput {
3862 subkeys,
3863 aead_algo: plan.options.aead_algo,
3864 volume_format_rev: plan.volume_format_rev,
3865 archive_uuid: plan.archive_uuid,
3866 session_id: plan.session_id,
3867 volume_index,
3868 block_count,
3869 bytes_written: trailer_offset,
3870 manifest_footer_offset,
3871 closed_at_ns: plan.options.closed_at_ns,
3872 root_auth_footer: root_auth_footer_offset.zip(plan.root_auth_footer_length),
3873 })?;
3874 let cmra_offset = checked_u64_add(trailer_offset, VOLUME_TRAILER_LEN as u64, "CMRA")?;
3875 let cmra = build_v41_cmra(CmraBuildInput {
3876 volume_format_rev: plan.volume_format_rev,
3877 volume_header_bytes: &volume_header_bytes,
3878 crypto_header: &plan.crypto_header,
3879 block_count,
3880 block_records_offset: plan.block_records_offset,
3881 manifest_footer_offset,
3882 manifest_footer: &manifest_footer,
3883 root_auth_footer_offset,
3884 root_auth_footer: root_auth_footer.as_deref(),
3885 key_wrap_table: plan.key_wrap_table.as_deref(),
3886 trailer_offset,
3887 trailer: &trailer,
3888 cmra_offset,
3889 options: plan.options,
3890 archive_uuid: plan.archive_uuid,
3891 session_id: plan.session_id,
3892 volume_index,
3893 })?;
3894 checked_u64_add(
3895 checked_u64_add(cmra_offset, cmra.bytes.len() as u64, "CMRA")?,
3896 (CRITICAL_RECOVERY_LOCATOR_LEN * 2) as u64,
3897 "critical recovery locators",
3898 )
3899}
3900
3901fn striped_block_count(total_block_count: u64, stripe_width: u32, volume_index: u32) -> u64 {
3902 let volume_index = volume_index as u64;
3903 let stripe_width = stripe_width as u64;
3904 if total_block_count <= volume_index {
3905 0
3906 } else {
3907 (total_block_count - 1 - volume_index) / stripe_width + 1
3908 }
3909}
3910
3911#[allow(clippy::too_many_arguments)]
3912fn emit_writer_plan<S, O>(
3913 files: &[S],
3914 master_key: &MasterKey,
3915 dictionary: Option<&[u8]>,
3916 root_auth: Option<RootAuthWriterConfig<'_>>,
3917 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
3918 plan: WriterPlan,
3919 sink: &mut O,
3920 progress: Option<&SourceProgressHandle<'_>>,
3921) -> Result<WrittenArchiveSummary, ArchiveWriteError>
3922where
3923 S: RegularFileSource,
3924 O: ArchiveWriteSink,
3925{
3926 let subkeys = writer_subkeys(
3927 master_key,
3928 plan.options.aead_algo,
3929 &plan.archive_uuid,
3930 &plan.session_id,
3931 )?;
3932 let mut state = begin_writer_emission_state(
3933 sink,
3934 plan.options,
3935 &plan.crypto_header,
3936 plan.key_wrap_table.as_deref(),
3937 plan.archive_uuid,
3938 plan.session_id,
3939 plan.volume_format_rev,
3940 root_auth.is_some(),
3941 )?;
3942
3943 let emit_payload_started = Instant::now();
3944 emit_payload_stream(
3945 files,
3946 dictionary,
3947 &subkeys,
3948 &plan,
3949 &mut state.next_block_index,
3950 sink,
3951 &mut state.bytes_written,
3952 &mut state.record_counts,
3953 &mut state.data_leaf_hashes,
3954 )?;
3955 let emit_payload = emit_payload_started.elapsed();
3956
3957 start_write_phase(progress, ArchiveWritePhase::EmittingMetadata);
3958 let mut summary =
3959 emit_writer_plan_suffix(&subkeys, root_auth, authenticator, plan, sink, state)?;
3960 summary.timings.emit_payload += emit_payload;
3961 Ok(summary)
3962}
3963
3964fn start_write_phase(progress: Option<&SourceProgressHandle<'_>>, phase: ArchiveWritePhase) {
3965 if let Some(progress) = progress {
3966 progress.borrow_mut().start_phase(phase);
3967 }
3968}
3969
3970fn emit_writer_plan_suffix<O: ArchiveWriteSink>(
3971 subkeys: &Subkeys,
3972 root_auth: Option<RootAuthWriterConfig<'_>>,
3973 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
3974 plan: WriterPlan,
3975 sink: &mut O,
3976 mut state: WriterEmissionState,
3977) -> Result<WrittenArchiveSummary, ArchiveWriteError> {
3978 let emit_metadata_started = Instant::now();
3979 let volume_count = plan.options.stripe_width as usize;
3980
3981 for planned in &plan.index_shard_objects {
3982 emit_encrypted_object(
3983 &planned.compressed,
3984 &subkeys.index_shard_key,
3985 &subkeys.index_nonce_seed,
3986 b"idxshard",
3987 planned.shard_index,
3988 BlockKind::IndexShardData,
3989 BlockKind::IndexShardParity,
3990 plan.options.index_fec_data_shards,
3991 plan.options.index_fec_parity_shards,
3992 &mut state.next_block_index,
3993 plan.options,
3994 &plan.archive_uuid,
3995 &plan.session_id,
3996 planned.extent,
3997 None,
3998 plan.volume_format_rev,
3999 sink,
4000 &mut state.bytes_written,
4001 &mut state.record_counts,
4002 &mut state.data_leaf_hashes,
4003 )?;
4004 }
4005
4006 let dictionary_records = if let (Some(compressed), Some((extent, _))) =
4007 (plan.compressed_dictionary.as_ref(), plan.dictionary_extent)
4008 {
4009 let object = emit_encrypted_object(
4010 compressed,
4011 &subkeys.dictionary_key,
4012 &subkeys.index_nonce_seed,
4013 b"dict",
4014 0,
4015 BlockKind::DictionaryData,
4016 BlockKind::DictionaryParity,
4017 plan.options.index_root_fec_data_shards,
4018 plan.options.index_root_fec_parity_shards,
4019 &mut state.next_block_index,
4020 plan.options,
4021 &plan.archive_uuid,
4022 &plan.session_id,
4023 extent,
4024 Some(MetadataObjectKind::Dictionary),
4025 plan.volume_format_rev,
4026 sink,
4027 &mut state.bytes_written,
4028 &mut state.record_counts,
4029 &mut state.data_leaf_hashes,
4030 )?;
4031 Some(object.records)
4032 } else {
4033 None
4034 };
4035
4036 for planned in &plan.directory_hint_objects {
4037 emit_encrypted_object(
4038 &planned.compressed,
4039 &subkeys.dir_hint_key,
4040 &subkeys.index_nonce_seed,
4041 b"dirhint",
4042 planned.hint_shard_index,
4043 BlockKind::DirectoryHintData,
4044 BlockKind::DirectoryHintParity,
4045 plan.options.index_fec_data_shards,
4046 plan.options.index_fec_parity_shards,
4047 &mut state.next_block_index,
4048 plan.options,
4049 &plan.archive_uuid,
4050 &plan.session_id,
4051 planned.extent,
4052 None,
4053 plan.volume_format_rev,
4054 sink,
4055 &mut state.bytes_written,
4056 &mut state.record_counts,
4057 &mut state.data_leaf_hashes,
4058 )?;
4059 }
4060
4061 let index_root_object = emit_encrypted_object(
4062 &plan.compressed_index_root,
4063 &subkeys.index_root_key,
4064 &subkeys.index_nonce_seed,
4065 b"idxroot",
4066 0,
4067 BlockKind::IndexRootData,
4068 BlockKind::IndexRootParity,
4069 plan.options.index_root_fec_data_shards,
4070 plan.options.index_root_fec_parity_shards,
4071 &mut state.next_block_index,
4072 plan.options,
4073 &plan.archive_uuid,
4074 &plan.session_id,
4075 plan.index_root_extent,
4076 Some(MetadataObjectKind::IndexRoot),
4077 plan.volume_format_rev,
4078 sink,
4079 &mut state.bytes_written,
4080 &mut state.record_counts,
4081 &mut state.data_leaf_hashes,
4082 )?;
4083 if state.next_block_index != plan.total_block_count {
4084 return Err(FormatError::WriterInvariant("streaming writer block plan mismatch").into());
4085 }
4086
4087 let volume_zero_manifest = build_manifest_footer(
4088 subkeys,
4089 plan.options.aead_algo,
4090 plan.volume_format_rev,
4091 plan.archive_uuid,
4092 plan.session_id,
4093 0,
4094 plan.options.stripe_width,
4095 &plan.index_root_extent,
4096 plan.index_root_plaintext.len(),
4097 )?;
4098 let root_auth_footer = match root_auth {
4099 Some(config) => {
4100 let signer = authenticator.ok_or(FormatError::WriterInvariant(
4101 "missing root-auth authenticator",
4102 ))?;
4103 Some(build_root_auth_footer_from_leaf_hashes(
4104 config,
4105 signer,
4106 RootAuthFooterBuildInput {
4107 archive_uuid: plan.archive_uuid,
4108 session_id: plan.session_id,
4109 volume_format_rev: plan.volume_format_rev,
4110 options: plan.options,
4111 crypto_header: &plan.crypto_header,
4112 volume_zero_manifest: &volume_zero_manifest,
4113 index_root_plaintext: &plan.index_root_plaintext,
4114 index_root_extent: plan.index_root_extent,
4115 dictionary_extent: plan.dictionary_extent,
4116 shard_entries: &plan.shard_entries,
4117 payload_objects: &plan.payload_objects,
4118 directory_hint_entries: &plan.directory_hint_entries,
4119 data_leaf_hashes: state.data_leaf_hashes.as_deref().ok_or(
4120 FormatError::WriterInvariant("missing root-auth data leaf hashes"),
4121 )?,
4122 },
4123 )?)
4124 }
4125 None => None,
4126 };
4127 let root_auth_footer_length = root_auth_footer
4128 .as_ref()
4129 .map(|footer| u32_len(footer.len(), "RootAuthFooterV1"))
4130 .transpose()?;
4131
4132 for volume_index in 0..volume_count {
4133 let volume_index_u32 = u32::try_from(volume_index)
4134 .map_err(|_| FormatError::WriterUnsupported("volume_index"))?;
4135 let manifest_footer_offset = state.bytes_written[volume_index];
4136 let manifest_footer = build_manifest_footer(
4137 subkeys,
4138 plan.options.aead_algo,
4139 plan.volume_format_rev,
4140 plan.archive_uuid,
4141 plan.session_id,
4142 volume_index_u32,
4143 plan.options.stripe_width,
4144 &plan.index_root_extent,
4145 plan.index_root_plaintext.len(),
4146 )?;
4147 sink.write_volume(volume_index, &manifest_footer)?;
4148 state.bytes_written[volume_index] = checked_u64_add(
4149 state.bytes_written[volume_index],
4150 MANIFEST_FOOTER_LEN as u64,
4151 "ManifestFooter",
4152 )?;
4153
4154 let root_auth_footer_offset = if let Some(root_auth_footer) = root_auth_footer.as_ref() {
4155 let offset = state.bytes_written[volume_index];
4156 sink.write_volume(volume_index, root_auth_footer)?;
4157 state.bytes_written[volume_index] = checked_u64_add(
4158 state.bytes_written[volume_index],
4159 root_auth_footer.len() as u64,
4160 "RootAuthFooterV1",
4161 )?;
4162 Some(offset)
4163 } else {
4164 None
4165 };
4166
4167 let trailer_offset = state.bytes_written[volume_index];
4168 let trailer = build_volume_trailer(VolumeTrailerBuildInput {
4169 subkeys,
4170 aead_algo: plan.options.aead_algo,
4171 volume_format_rev: plan.volume_format_rev,
4172 archive_uuid: plan.archive_uuid,
4173 session_id: plan.session_id,
4174 volume_index: volume_index_u32,
4175 block_count: state.record_counts[volume_index],
4176 bytes_written: trailer_offset,
4177 manifest_footer_offset,
4178 closed_at_ns: plan.options.closed_at_ns,
4179 root_auth_footer: root_auth_footer_offset.zip(root_auth_footer_length),
4180 })?;
4181 sink.write_volume(volume_index, &trailer)?;
4182 state.bytes_written[volume_index] = checked_u64_add(
4183 state.bytes_written[volume_index],
4184 VOLUME_TRAILER_LEN as u64,
4185 "VolumeTrailer",
4186 )?;
4187
4188 let cmra_offset = state.bytes_written[volume_index];
4189 let cmra = build_v41_cmra(CmraBuildInput {
4190 volume_format_rev: plan.volume_format_rev,
4191 volume_header_bytes: &state.volume_headers[volume_index],
4192 crypto_header: &plan.crypto_header,
4193 block_count: state.record_counts[volume_index],
4194 block_records_offset: plan.block_records_offset,
4195 manifest_footer_offset,
4196 manifest_footer: &manifest_footer,
4197 root_auth_footer_offset,
4198 root_auth_footer: root_auth_footer.as_deref(),
4199 key_wrap_table: plan.key_wrap_table.as_deref(),
4200 trailer_offset,
4201 trailer: &trailer,
4202 cmra_offset,
4203 options: plan.options,
4204 archive_uuid: plan.archive_uuid,
4205 session_id: plan.session_id,
4206 volume_index: volume_index_u32,
4207 })?;
4208 sink.write_volume(volume_index, &cmra.bytes)?;
4209 state.bytes_written[volume_index] = checked_u64_add(
4210 state.bytes_written[volume_index],
4211 cmra.bytes.len() as u64,
4212 "CMRA",
4213 )?;
4214 let locator_base = CriticalRecoveryLocator {
4215 volume_format_rev: plan.volume_format_rev,
4216 cmra_offset,
4217 cmra_length: u32_len(cmra.bytes.len(), "CMRA")?,
4218 volume_trailer_offset: trailer_offset,
4219 body_bytes_before_cmra: cmra_offset,
4220 archive_uuid_hint: plan.archive_uuid,
4221 session_id_hint: plan.session_id,
4222 volume_index_hint: volume_index_u32,
4223 locator_sequence: 1,
4224 cmra_shard_size: cmra.shard_size,
4225 cmra_data_shard_count: cmra.data_shard_count,
4226 cmra_parity_shard_count: cmra.parity_shard_count,
4227 cmra_image_length: cmra.image_length,
4228 cmra_image_sha256: cmra.image_sha256,
4229 locator_crc32c: 0,
4230 };
4231 let mirror = locator_base.to_bytes();
4232 sink.write_volume(volume_index, &mirror)?;
4233 let final_locator = CriticalRecoveryLocator {
4234 locator_sequence: 0,
4235 ..locator_base
4236 }
4237 .to_bytes();
4238 sink.write_volume(volume_index, &final_locator)?;
4239 state.bytes_written[volume_index] = checked_u64_add(
4240 state.bytes_written[volume_index],
4241 (CRITICAL_RECOVERY_LOCATOR_LEN * 2) as u64,
4242 "critical recovery locators",
4243 )?;
4244
4245 if volume_index == 0 {
4246 debug_assert_eq!(volume_zero_manifest, manifest_footer);
4247 }
4248 }
4249
4250 let bootstrap_sidecar_bytes = if plan.options.stripe_width == 1 {
4251 let sidecar = build_bootstrap_sidecar(
4252 subkeys,
4253 plan.options.aead_algo,
4254 plan.volume_format_rev,
4255 plan.archive_uuid,
4256 plan.session_id,
4257 &volume_zero_manifest,
4258 &index_root_object.records,
4259 dictionary_records.as_deref(),
4260 )?;
4261 let sidecar_len = sidecar.len() as u64;
4262 sink.write_bootstrap_sidecar(&sidecar)?;
4263 sidecar_len
4264 } else {
4265 0
4266 };
4267
4268 Ok(WrittenArchiveSummary {
4269 volume_count,
4270 archive_bytes: state.bytes_written.iter().sum(),
4271 bootstrap_sidecar_bytes,
4272 archive_uuid: plan.archive_uuid,
4273 session_id: plan.session_id,
4274 timings: WriterTimings {
4275 emit_metadata: emit_metadata_started.elapsed(),
4276 ..WriterTimings::default()
4277 },
4278 })
4279}
4280
4281#[allow(clippy::too_many_arguments)]
4282fn emit_encrypted_object<O: ArchiveWriteSink>(
4283 payload: &[u8],
4284 key: &[u8; 32],
4285 nonce_seed: &[u8; 32],
4286 domain: &[u8],
4287 counter: u64,
4288 data_kind: BlockKind,
4289 parity_kind: BlockKind,
4290 data_shard_max: u16,
4291 class_parity_shard_max: u16,
4292 next_block_index: &mut u64,
4293 options: WriterOptions,
4294 archive_uuid: &[u8; 16],
4295 session_id: &[u8; 16],
4296 expected_extent: ObjectExtent,
4297 metadata_kind: Option<MetadataObjectKind>,
4298 volume_format_rev: u16,
4299 sink: &mut O,
4300 bytes_written: &mut [u64],
4301 record_counts: &mut [u64],
4302 data_leaf_hashes: &mut Option<Vec<(u64, [u8; 32])>>,
4303) -> Result<EncryptedObject, ArchiveWriteError> {
4304 let object = encrypt_object(
4305 payload,
4306 ObjectEncryptionContext {
4307 key,
4308 nonce_seed,
4309 domain,
4310 counter,
4311 data_kind,
4312 parity_kind,
4313 data_shard_max,
4314 class_parity_shard_max,
4315 archive_uuid,
4316 session_id,
4317 },
4318 next_block_index,
4319 options,
4320 )
4321 .map_err(|error| match metadata_kind {
4322 Some(kind) => map_metadata_encrypt_error(error, kind),
4323 None => error,
4324 })?;
4325 validate_planned_extent(&object, expected_extent)?;
4326 for record in &object.records {
4327 emit_block_record(
4328 sink,
4329 options,
4330 bytes_written,
4331 record_counts,
4332 volume_format_rev,
4333 data_leaf_hashes,
4334 record,
4335 )?;
4336 }
4337 Ok(object)
4338}
4339
4340fn emit_block_record<O: ArchiveWriteSink>(
4341 sink: &mut O,
4342 options: WriterOptions,
4343 bytes_written: &mut [u64],
4344 record_counts: &mut [u64],
4345 volume_format_rev: u16,
4346 data_leaf_hashes: &mut Option<Vec<(u64, [u8; 32])>>,
4347 record: &BlockRecord,
4348) -> Result<(), ArchiveWriteError> {
4349 let volume_index = (record.block_index % options.stripe_width as u64) as usize;
4350 let record_bytes = record.to_bytes();
4351 sink.write_volume(volume_index, &record_bytes)?;
4352 bytes_written[volume_index] = checked_u64_add(
4353 bytes_written[volume_index],
4354 record_bytes.len() as u64,
4355 "BlockRecord",
4356 )?;
4357 record_counts[volume_index] =
4358 checked_u64_add(record_counts[volume_index], 1, "BlockRecord count")?;
4359 if let Some(data_leaf_hashes) = data_leaf_hashes.as_mut() {
4360 if record.kind.is_data() {
4361 data_leaf_hashes.push((
4362 record.block_index,
4363 data_block_merkle_leaf_hash_for_revision(
4364 FORMAT_VERSION,
4365 volume_format_rev,
4366 record.block_index,
4367 record.kind,
4368 record.flags,
4369 &record.payload,
4370 )?,
4371 ));
4372 }
4373 }
4374 Ok(())
4375}
4376
4377fn emit_serialized_block_record<O: ArchiveWriteSink>(
4378 sink: &mut O,
4379 options: WriterOptions,
4380 bytes_written: &mut [u64],
4381 record_counts: &mut [u64],
4382 record: &SerializedBlockRecord,
4383) -> Result<(), ArchiveWriteError> {
4384 let volume_index = (record.block_index % options.stripe_width as u64) as usize;
4385 sink.write_volume(volume_index, &record.bytes)?;
4386 bytes_written[volume_index] = checked_u64_add(
4387 bytes_written[volume_index],
4388 record.bytes.len() as u64,
4389 "BlockRecord",
4390 )?;
4391 record_counts[volume_index] =
4392 checked_u64_add(record_counts[volume_index], 1, "BlockRecord count")?;
4393 Ok(())
4394}
4395
4396#[allow(clippy::too_many_arguments)]
4397fn emit_payload_stream<S, O>(
4398 files: &[S],
4399 dictionary: Option<&[u8]>,
4400 subkeys: &Subkeys,
4401 plan: &WriterPlan,
4402 next_block_index: &mut u64,
4403 sink: &mut O,
4404 bytes_written: &mut [u64],
4405 record_counts: &mut [u64],
4406 data_leaf_hashes: &mut Option<Vec<(u64, [u8; 32])>>,
4407) -> Result<(), ArchiveWriteError>
4408where
4409 S: RegularFileSource,
4410 O: ArchiveWriteSink,
4411{
4412 let mut envelope = PayloadEnvelopeBuilder {
4413 envelope_index: 0,
4414 plaintext: Vec::new(),
4415 };
4416 let mut next_frame_index = 0u64;
4417
4418 for (member_index, file) in files.iter().enumerate() {
4419 let member = plan
4420 .tar_members
4421 .get(member_index)
4422 .ok_or(FormatError::WriterInvariant(
4423 "planned tar member is missing",
4424 ))?;
4425 let current_path =
4426 normalize_lookup_file_path(file.archive_path(), plan.options.max_path_length)?;
4427 if current_path != member.path
4428 || file.entry_kind() != member.entry_kind
4429 || file.link_target() != member.link_target.as_deref()
4430 || file.file_data_size() != member.file_data_size
4431 || file.sparse_extents() != member.sparse_extents.as_deref()
4432 || file.mode() != member.mode
4433 || file.mtime() != member.mtime
4434 || file.portable_metadata() != member.portable_metadata
4435 {
4436 return Err(FormatError::WriterInvariant(
4437 "file source changed between planning and emission",
4438 )
4439 .into());
4440 }
4441 let layout = build_primary_member_layout(
4442 &member.path,
4443 member.entry_kind,
4444 member.link_target.as_deref(),
4445 member.file_data_size,
4446 member.sparse_extents.as_deref(),
4447 member.mode,
4448 member.mtime,
4449 &member.portable_metadata,
4450 )?;
4451 let source_payload_size = member
4452 .sparse_extents
4453 .as_deref()
4454 .map(|extents| sparse_extent_bytes(extents, member.file_data_size))
4455 .transpose()?
4456 .unwrap_or(member.file_data_size);
4457 let actual_member_group_size = primary_member_layout_size(&layout, source_payload_size)?;
4458 if actual_member_group_size != member.tar_member_group_size {
4459 return Err(FormatError::WriterInvariant(
4460 "streamed auxiliary layout changed between planning and emission",
4461 )
4462 .into());
4463 }
4464 let mut reader = StreamingMemberReader::from_source(
4465 file,
4466 &member.portable_metadata,
4467 layout,
4468 source_payload_size,
4469 )?;
4470 let mut member_offset = 0u64;
4471 while member_offset < member.tar_member_group_size {
4472 let remaining = member.tar_member_group_size - member_offset;
4473 let max_chunk = remaining.min(plan.options.chunk_size as u64);
4474 let mut chunk = vec![0u8; to_usize_writer(max_chunk, "payload chunk")?];
4475 reader
4476 .read_exact(&mut chunk)
4477 .map_err(ArchiveWriteError::Io)?;
4478 let mut chunk_len = chunk.len();
4479 let frame = loop {
4480 let candidate = &chunk[..chunk_len];
4481 let frame = if let Some(dictionary) = dictionary {
4482 compress_zstd_frame_with_dictionary_and_jobs(
4483 candidate,
4484 plan.options.zstd_level,
4485 dictionary,
4486 plan.options.jobs,
4487 )?
4488 } else {
4489 compress_zstd_frame_with_jobs(
4490 candidate,
4491 plan.options.zstd_level,
4492 plan.options.jobs,
4493 )?
4494 };
4495 if payload_object_can_fit(frame.len(), plan.options)? {
4496 break frame;
4497 }
4498 if chunk_len == 1 {
4499 return Err(FormatError::WriterUnsupported(
4500 "single-byte payload frame exceeds envelope object limits",
4501 )
4502 .into());
4503 }
4504 chunk_len = (chunk_len / 2).max(1);
4505 };
4506 if chunk_len < chunk.len() {
4507 reader.push_back(chunk[chunk_len..].to_vec());
4508 }
4509 append_payload_frame_to_emit(
4510 &mut envelope,
4511 &frame,
4512 chunk_len,
4513 member_index,
4514 member.tar_member_group_start,
4515 member_offset,
4516 member.tar_member_group_size,
4517 &mut next_frame_index,
4518 subkeys,
4519 plan,
4520 next_block_index,
4521 sink,
4522 bytes_written,
4523 record_counts,
4524 data_leaf_hashes,
4525 )?;
4526 member_offset = checked_u64_add(member_offset, chunk_len as u64, "payload chunk")?;
4527 }
4528 }
4529
4530 if !envelope.plaintext.is_empty() {
4531 flush_payload_envelope_emit(
4532 &mut envelope,
4533 subkeys,
4534 plan,
4535 next_block_index,
4536 sink,
4537 bytes_written,
4538 record_counts,
4539 data_leaf_hashes,
4540 )?;
4541 }
4542 if next_frame_index != plan.frames.len() as u64
4543 || envelope.envelope_index != plan.payload_objects.len() as u64
4544 {
4545 return Err(FormatError::WriterInvariant("streaming payload plan mismatch").into());
4546 }
4547 Ok(())
4548}
4549
4550#[allow(clippy::too_many_arguments)]
4551fn append_payload_frame_to_emit<O: ArchiveWriteSink>(
4552 envelope: &mut PayloadEnvelopeBuilder,
4553 frame: &[u8],
4554 decompressed_size: usize,
4555 member_index: usize,
4556 member_start: u64,
4557 member_offset: u64,
4558 member_group_size: u64,
4559 next_frame_index: &mut u64,
4560 subkeys: &Subkeys,
4561 plan: &WriterPlan,
4562 next_block_index: &mut u64,
4563 sink: &mut O,
4564 bytes_written: &mut [u64],
4565 record_counts: &mut [u64],
4566 data_leaf_hashes: &mut Option<Vec<(u64, [u8; 32])>>,
4567) -> Result<(), ArchiveWriteError> {
4568 if payload_envelope_needs_flush(envelope, frame.len(), plan.options)? {
4569 flush_payload_envelope_emit(
4570 envelope,
4571 subkeys,
4572 plan,
4573 next_block_index,
4574 sink,
4575 bytes_written,
4576 record_counts,
4577 data_leaf_hashes,
4578 )?;
4579 }
4580 if envelope.plaintext.is_empty() && !payload_object_can_fit(frame.len(), plan.options)? {
4581 return Err(
4582 FormatError::WriterUnsupported("payload frame exceeds envelope object limits").into(),
4583 );
4584 }
4585 let offset = u32_len(envelope.plaintext.len(), "FrameEntry.offset_in_envelope")?;
4586 let actual = payload_frame_metadata(PayloadFrameMetadataInput {
4587 frame_index: *next_frame_index,
4588 envelope_index: envelope.envelope_index,
4589 member_index,
4590 offset_in_envelope: offset,
4591 compressed_size: frame.len(),
4592 decompressed_size,
4593 member_start,
4594 member_offset,
4595 member_group_size,
4596 })?;
4597 let expected =
4598 plan.frames
4599 .get(*next_frame_index as usize)
4600 .ok_or(FormatError::WriterInvariant(
4601 "planned payload frame is missing",
4602 ))?;
4603 if expected.envelope_index != actual.envelope_index
4604 || expected.member_index != actual.member_index
4605 || expected.offset_in_envelope != actual.offset_in_envelope
4606 || expected.compressed_size != actual.compressed_size
4607 || expected.decompressed_size != actual.decompressed_size
4608 || expected.flags != actual.flags
4609 || expected.tar_stream_offset != actual.tar_stream_offset
4610 {
4611 return Err(
4612 FormatError::WriterInvariant("emitted payload frame does not match plan").into(),
4613 );
4614 }
4615 envelope.plaintext.extend_from_slice(frame);
4616 *next_frame_index = checked_u64_add(*next_frame_index, 1, "PayloadFrame.frame_index")?;
4617 Ok(())
4618}
4619
4620#[allow(clippy::too_many_arguments)]
4621fn flush_payload_envelope_emit<O: ArchiveWriteSink>(
4622 envelope: &mut PayloadEnvelopeBuilder,
4623 subkeys: &Subkeys,
4624 plan: &WriterPlan,
4625 next_block_index: &mut u64,
4626 sink: &mut O,
4627 bytes_written: &mut [u64],
4628 record_counts: &mut [u64],
4629 data_leaf_hashes: &mut Option<Vec<(u64, [u8; 32])>>,
4630) -> Result<(), ArchiveWriteError> {
4631 let expected = plan
4632 .payload_objects
4633 .get(envelope.envelope_index as usize)
4634 .ok_or(FormatError::WriterInvariant(
4635 "planned payload envelope is missing",
4636 ))?;
4637 if expected.envelope_index != envelope.envelope_index
4638 || expected.plaintext_size
4639 != u32_len(envelope.plaintext.len(), "EnvelopeEntry.plaintext_size")?
4640 {
4641 return Err(
4642 FormatError::WriterInvariant("emitted payload envelope does not match plan").into(),
4643 );
4644 }
4645 emit_encrypted_object(
4646 &envelope.plaintext,
4647 &subkeys.enc_key,
4648 &subkeys.nonce_seed,
4649 b"envelope",
4650 envelope.envelope_index,
4651 BlockKind::PayloadData,
4652 BlockKind::PayloadParity,
4653 plan.options.fec_data_shards,
4654 plan.options.fec_parity_shards,
4655 next_block_index,
4656 plan.options,
4657 &plan.archive_uuid,
4658 &plan.session_id,
4659 expected.object,
4660 None,
4661 plan.volume_format_rev,
4662 sink,
4663 bytes_written,
4664 record_counts,
4665 data_leaf_hashes,
4666 )?;
4667 envelope.envelope_index = checked_u64_add(envelope.envelope_index, 1, "EnvelopeEntry")?;
4668 envelope.plaintext.clear();
4669 Ok(())
4670}
4671
4672pub fn write_empty_archive(master_key: &MasterKey) -> Result<WrittenArchive, FormatError> {
4673 write_archive(&[], master_key, WriterOptions::default())
4674}
4675
4676fn plan_writer_options(mut options: WriterOptions) -> Result<WriterOptions, FormatError> {
4677 if options.jobs == 0 {
4678 return Err(FormatError::WriterUnsupported("jobs must be at least 1"));
4679 }
4680 if options.block_size < MIN_BLOCK_SIZE || options.block_size % 2 != 0 {
4681 return Err(FormatError::WriterUnsupported(
4682 "writer requires an even block size of at least 4096",
4683 ));
4684 }
4685 if options.stripe_width == 0 {
4686 return Err(FormatError::WriterUnsupported(
4687 "stripe_width must be non-zero",
4688 ));
4689 }
4690 if options.volume_loss_tolerance as u32 >= options.stripe_width {
4691 return Err(FormatError::WriterUnsupported(
4692 "volume_loss_tolerance must be less than stripe_width",
4693 ));
4694 }
4695 if options.stripe_width == 1 && options.volume_loss_tolerance != 0 {
4696 return Err(FormatError::WriterUnsupported(
4697 "single-volume archives cannot tolerate volume loss",
4698 ));
4699 }
4700 if matches!(options.target_volume_size, Some(0)) {
4701 return Err(FormatError::WriterUnsupported(
4702 "target_volume_size must be non-zero",
4703 ));
4704 }
4705 if options.bit_rot_buffer_pct > 100 {
4706 return Err(FormatError::WriterUnsupported(
4707 "bit_rot_buffer_pct must be at most 100",
4708 ));
4709 }
4710 if options.chunk_size == 0 || options.chunk_size > options.envelope_target_size {
4711 return Err(FormatError::WriterUnsupported(
4712 "chunk_size must be non-zero and no larger than envelope_target_size",
4713 ));
4714 }
4715 if options.fec_data_shards == 0
4716 || options.index_fec_data_shards == 0
4717 || options.index_root_fec_data_shards == 0
4718 {
4719 return Err(FormatError::WriterUnsupported(
4720 "FEC data shard class maxima must be non-zero",
4721 ));
4722 }
4723 options.index_root_fec_data_shards = options
4724 .index_root_fec_data_shards
4725 .max(MIN_INDEX_ROOT_FEC_DATA_SHARDS);
4726 options.fec_parity_shards =
4727 compute_parity_u16(options.fec_data_shards as u64, options, "fec_parity_shards")?;
4728 options.index_fec_parity_shards = compute_parity_u16(
4729 options.index_fec_data_shards as u64,
4730 options,
4731 "index_fec_parity_shards",
4732 )?;
4733 options.index_root_fec_parity_shards = compute_parity_u16(
4734 options.index_root_fec_data_shards as u64,
4735 options,
4736 "index_root_fec_parity_shards",
4737 )?;
4738 validate_writer_options_match_reader_caps(options)?;
4739 Ok(options)
4740}
4741
4742fn validate_writer_options_match_reader_caps(options: WriterOptions) -> Result<(), FormatError> {
4743 CryptoHeaderFixed {
4744 length: CRYPTO_HEADER_FIXED_LEN as u32,
4745 compression_algo: CompressionAlgo::ZstdFramed,
4746 aead_algo: options.aead_algo,
4747 fec_algo: FecAlgo::ReedSolomonGF16,
4748 kdf_algo: if options.aead_algo.is_encrypted() {
4749 KdfAlgo::Raw
4750 } else {
4751 KdfAlgo::None
4752 },
4753 chunk_size: options.chunk_size,
4754 envelope_target_size: options.envelope_target_size,
4755 block_size: options.block_size,
4756 fec_data_shards: options.fec_data_shards,
4757 fec_parity_shards: options.fec_parity_shards,
4758 index_fec_data_shards: options.index_fec_data_shards,
4759 index_fec_parity_shards: options.index_fec_parity_shards,
4760 index_root_fec_data_shards: options.index_root_fec_data_shards,
4761 index_root_fec_parity_shards: options.index_root_fec_parity_shards,
4762 stripe_width: options.stripe_width,
4763 volume_loss_tolerance: options.volume_loss_tolerance,
4764 bit_rot_buffer_pct: options.bit_rot_buffer_pct,
4765 has_dictionary: 0,
4766 max_path_length: options.max_path_length,
4767 expected_volume_size: options.target_volume_size.unwrap_or(0),
4768 }
4769 .validate_supported_profile()
4770}
4771
4772fn build_crypto_header(
4773 options: WriterOptions,
4774 volume_format_rev: u16,
4775 has_dictionary: bool,
4776 subkeys: &Subkeys,
4777 archive_uuid: &[u8; 16],
4778 session_id: &[u8; 16],
4779 kdf_params: &KdfParams,
4780) -> Result<Vec<u8>, FormatError> {
4781 let kdf_payload = serialize_kdf_params(kdf_params)?;
4782 let length = CRYPTO_HEADER_FIXED_LEN
4783 .checked_add(kdf_payload.len())
4784 .and_then(|value| value.checked_add(CRYPTO_EXTENSION_HEADER_LEN))
4785 .and_then(|value| value.checked_add(CRYPTO_HEADER_HMAC_LEN))
4786 .ok_or(FormatError::WriterUnsupported(
4787 "CryptoHeader length overflow",
4788 ))?;
4789 let kdf_algo = match kdf_params {
4790 KdfParams::None => KdfAlgo::None,
4791 KdfParams::Raw => KdfAlgo::Raw,
4792 KdfParams::Argon2id { .. } => KdfAlgo::Argon2id,
4793 KdfParams::RecipientWrap { .. } => KdfAlgo::RecipientWrap,
4794 };
4795 match (options.aead_algo, kdf_algo) {
4796 (AeadAlgo::None, KdfAlgo::None) => {}
4797 (aead_algo, KdfAlgo::Raw | KdfAlgo::Argon2id | KdfAlgo::RecipientWrap)
4798 if aead_algo.is_encrypted() => {}
4799 _ => {
4800 return Err(FormatError::InvalidProtectionMode {
4801 aead_algo: options.aead_algo,
4802 kdf_algo,
4803 });
4804 }
4805 }
4806 let fixed = CryptoHeaderFixed {
4807 length: length as u32,
4808 compression_algo: CompressionAlgo::ZstdFramed,
4809 aead_algo: options.aead_algo,
4810 fec_algo: FecAlgo::ReedSolomonGF16,
4811 kdf_algo,
4812 chunk_size: options.chunk_size,
4813 envelope_target_size: options.envelope_target_size,
4814 block_size: options.block_size,
4815 fec_data_shards: options.fec_data_shards,
4816 fec_parity_shards: options.fec_parity_shards,
4817 index_fec_data_shards: options.index_fec_data_shards,
4818 index_fec_parity_shards: options.index_fec_parity_shards,
4819 index_root_fec_data_shards: options.index_root_fec_data_shards,
4820 index_root_fec_parity_shards: options.index_root_fec_parity_shards,
4821 stripe_width: options.stripe_width,
4822 volume_loss_tolerance: options.volume_loss_tolerance,
4823 bit_rot_buffer_pct: options.bit_rot_buffer_pct,
4824 has_dictionary: if has_dictionary { 1 } else { 0 },
4825 max_path_length: options.max_path_length,
4826 expected_volume_size: options.target_volume_size.unwrap_or(0),
4827 };
4828
4829 let mut bytes = fixed.to_bytes().to_vec();
4830 bytes.extend_from_slice(&kdf_payload);
4831 bytes.extend_from_slice(&0u16.to_le_bytes());
4832 bytes.extend_from_slice(&0u32.to_le_bytes());
4833 let hmac = compute_integrity_tag(
4834 HmacDomain::CryptoHeader,
4835 options.aead_algo,
4836 volume_format_rev,
4837 Some(&subkeys.mac_key),
4838 archive_uuid,
4839 session_id,
4840 &bytes,
4841 )?;
4842 bytes.extend_from_slice(&hmac);
4843 Ok(bytes)
4844}
4845
4846fn serialize_kdf_params(params: &KdfParams) -> Result<Vec<u8>, FormatError> {
4847 let mut bytes = Vec::new();
4848 match params {
4849 KdfParams::None => {
4850 bytes.extend_from_slice(&(KdfAlgo::None as u16).to_le_bytes());
4851 }
4852 KdfParams::Raw => {
4853 bytes.extend_from_slice(&(KdfAlgo::Raw as u16).to_le_bytes());
4854 }
4855 KdfParams::Argon2id {
4856 t_cost,
4857 m_cost_kib,
4858 parallelism,
4859 salt,
4860 } => {
4861 if *t_cost == 0 {
4862 return Err(FormatError::InvalidKdfParams("t_cost must be non-zero"));
4863 }
4864 if *parallelism == 0 {
4865 return Err(FormatError::InvalidKdfParams(
4866 "parallelism must be non-zero",
4867 ));
4868 }
4869 let min_memory = parallelism
4870 .checked_mul(8)
4871 .ok_or(FormatError::InvalidKdfParams(
4872 "m_cost_kib requirement overflow",
4873 ))?;
4874 if *m_cost_kib < min_memory {
4875 return Err(FormatError::InvalidKdfParams(
4876 "m_cost_kib must be at least 8 * parallelism",
4877 ));
4878 }
4879 if !(8..=64).contains(&salt.len()) {
4880 return Err(FormatError::InvalidKdfParams(
4881 "argon2id salt length must be 8..64",
4882 ));
4883 }
4884 let salt_len = u16::try_from(salt.len())
4885 .map_err(|_| FormatError::InvalidKdfParams("argon2id salt too long"))?;
4886 bytes.extend_from_slice(&(KdfAlgo::Argon2id as u16).to_le_bytes());
4887 bytes.extend_from_slice(&t_cost.to_le_bytes());
4888 bytes.extend_from_slice(&m_cost_kib.to_le_bytes());
4889 bytes.extend_from_slice(¶llelism.to_le_bytes());
4890 bytes.extend_from_slice(&salt_len.to_le_bytes());
4891 bytes.extend_from_slice(salt);
4892 }
4893 KdfParams::RecipientWrap {
4894 key_wrap_table_length,
4895 key_wrap_table_record_count,
4896 key_wrap_table_version,
4897 key_wrap_table_digest,
4898 } => {
4899 bytes.extend_from_slice(&(KdfAlgo::RecipientWrap as u16).to_le_bytes());
4900 bytes.extend_from_slice(&key_wrap_table_length.to_le_bytes());
4901 bytes.extend_from_slice(&key_wrap_table_record_count.to_le_bytes());
4902 bytes.extend_from_slice(&key_wrap_table_version.to_le_bytes());
4903 bytes.extend_from_slice(&0u16.to_le_bytes());
4904 bytes.extend_from_slice(key_wrap_table_digest);
4905 }
4906 }
4907 Ok(bytes)
4908}
4909
4910#[cfg(test)]
4911fn build_tar_stream<S: RegularFileSource>(
4912 files: &[S],
4913 max_path_length: u32,
4914) -> Result<(Vec<u8>, Vec<TarMember>), FormatError> {
4915 let mut stream = Vec::new();
4916 let mut members = Vec::with_capacity(files.len());
4917 for file in files {
4918 let path = normalize_lookup_file_path(file.archive_path(), max_path_length)?;
4919 let start = stream.len() as u64;
4920 let sparse_extents = file.sparse_extents().map(<[SparseExtent]>::to_vec);
4921 let source_payload_size = sparse_extents
4922 .as_deref()
4923 .map(|extents| sparse_extent_bytes(extents, file.file_data_size()))
4924 .transpose()?
4925 .unwrap_or(file.file_data_size());
4926 let mut member_group = build_primary_member_prefix(
4927 &path,
4928 file.entry_kind(),
4929 file.link_target(),
4930 file.file_data_size(),
4931 sparse_extents.as_deref(),
4932 file.mode(),
4933 file.mtime(),
4934 &file.portable_metadata(),
4935 )?;
4936 let mut reader = file
4937 .open()
4938 .map_err(|_| FormatError::WriterInvariant("test source failed to open"))?;
4939 let mut payload = Vec::new();
4940 reader
4941 .read_to_end(&mut payload)
4942 .map_err(|_| FormatError::WriterInvariant("test source failed to read"))?;
4943 if payload.len() as u64 != source_payload_size {
4944 return Err(FormatError::WriterInvariant(
4945 "test source payload size mismatch",
4946 ));
4947 }
4948 member_group.extend_from_slice(&payload);
4949 member_group.resize(
4950 member_group.len() + padding_to_512(source_payload_size as usize),
4951 0,
4952 );
4953 stream.extend_from_slice(&member_group);
4954 members.push(TarMember {
4955 path,
4956 entry_kind: file.entry_kind(),
4957 link_target: file.link_target().map(<[u8]>::to_vec),
4958 tar_member_group_start: start,
4959 tar_member_group_size: member_group.len() as u64,
4960 file_data_size: file.file_data_size(),
4961 sparse_extents,
4962 mode: file.mode(),
4963 mtime: file.mtime(),
4964 portable_metadata: file.portable_metadata(),
4965 });
4966 }
4967 Ok((stream, members))
4968}
4969
4970#[cfg(test)]
4971fn build_payload_envelopes(
4972 tar_stream: &[u8],
4973 members: &[TarMember],
4974 options: WriterOptions,
4975 dictionary: Option<&[u8]>,
4976) -> Result<(Vec<PayloadEnvelope>, Vec<PayloadFrame>), FormatError> {
4977 let chunk_size = options.chunk_size as usize;
4978 if chunk_size == 0 {
4979 return Err(FormatError::WriterUnsupported(
4980 "chunk_size must be non-zero and no larger than envelope_target_size",
4981 ));
4982 }
4983 let envelope_target_size = options.envelope_target_size as usize;
4984 let mut envelopes = Vec::new();
4985 let mut current = PayloadEnvelope {
4986 envelope_index: 0,
4987 plaintext: Vec::new(),
4988 };
4989 let mut frames = Vec::new();
4990 let mut next_frame_index = 0u64;
4991
4992 for (member_index, member) in members.iter().enumerate() {
4993 let start = member.tar_member_group_start as usize;
4994 let end = checked_usize_add(start, member.tar_member_group_size as usize, "tar member")?;
4995 let member_bytes = tar_stream
4996 .get(start..end)
4997 .ok_or(FormatError::WriterInvariant(
4998 "tar member range is out of bounds",
4999 ))?;
5000 let mut member_offset = 0usize;
5001 while member_offset < member_bytes.len() {
5002 let mut chunk_len = (member_bytes.len() - member_offset).min(chunk_size);
5003 let frame = loop {
5004 let end = checked_usize_add(member_offset, chunk_len, "payload chunk")?;
5005 let chunk = &member_bytes[member_offset..end];
5006 let frame = if let Some(dictionary) = dictionary {
5007 compress_zstd_frame_with_dictionary_and_jobs(
5008 chunk,
5009 options.zstd_level,
5010 dictionary,
5011 options.jobs,
5012 )?
5013 } else {
5014 compress_zstd_frame_with_jobs(chunk, options.zstd_level, options.jobs)?
5015 };
5016 if payload_object_can_fit(frame.len(), options)? {
5017 break frame;
5018 }
5019 if chunk_len == 1 {
5020 return Err(FormatError::WriterUnsupported(
5021 "single-byte payload frame exceeds envelope object limits",
5022 ));
5023 }
5024 chunk_len = (chunk_len / 2).max(1);
5025 };
5026 let next_len = checked_usize_add(current.plaintext.len(), frame.len(), "payload")?;
5027 if !current.plaintext.is_empty()
5028 && (next_len > envelope_target_size || !payload_object_can_fit(next_len, options)?)
5029 {
5030 envelopes.push(current);
5031 current = PayloadEnvelope {
5032 envelope_index: envelopes.len() as u64,
5033 plaintext: Vec::new(),
5034 };
5035 }
5036
5037 if current.plaintext.is_empty() && !payload_object_can_fit(frame.len(), options)? {
5038 return Err(FormatError::WriterUnsupported(
5039 "payload frame exceeds envelope object limits",
5040 ));
5041 }
5042 let offset = u32_len(current.plaintext.len(), "FrameEntry.offset_in_envelope")?;
5043 current.plaintext.extend_from_slice(&frame);
5044 let is_first_member_frame = member_offset == 0;
5045 let is_last_member_frame =
5046 checked_usize_add(member_offset, chunk_len, "payload chunk")? == member_bytes.len();
5047 let mut flags = 0u32;
5048 if is_first_member_frame {
5049 flags |= 0x0000_0001;
5050 }
5051 if is_last_member_frame {
5052 flags |= 0x0000_0002;
5053 }
5054 frames.push(PayloadFrame {
5055 frame_index: next_frame_index,
5056 envelope_index: current.envelope_index,
5057 member_index,
5058 offset_in_envelope: offset,
5059 compressed_size: u32_len(frame.len(), "FrameEntry.compressed_size")?,
5060 decompressed_size: u32_len(chunk_len, "FrameEntry.decompressed_size")?,
5061 flags,
5062 tar_stream_offset: checked_u64_add(
5063 member.tar_member_group_start,
5064 u64::try_from(member_offset)
5065 .map_err(|_| FormatError::WriterUnsupported("chunk offset"))?,
5066 "PayloadFrame.tar_stream_offset",
5067 )?,
5068 });
5069 next_frame_index = checked_u64_add(next_frame_index, 1, "PayloadFrame.frame_index")?;
5070 member_offset = checked_usize_add(member_offset, chunk_len, "payload chunk")?;
5071 }
5072 }
5073 if !current.plaintext.is_empty() {
5074 envelopes.push(current);
5075 }
5076 Ok((envelopes, frames))
5077}
5078
5079fn sorted_file_rows(members: &[TarMember]) -> Vec<FileRow> {
5080 let mut rows = members
5081 .iter()
5082 .enumerate()
5083 .map(|(member_index, member)| FileRow {
5084 path_hash: hash_prefix(&member.path),
5085 path: member.path.clone(),
5086 member_index,
5087 member: member.clone(),
5088 })
5089 .collect::<Vec<_>>();
5090 rows.sort_by(|left, right| {
5091 (
5092 left.path_hash,
5093 left.path.as_slice(),
5094 left.member.tar_member_group_start,
5095 )
5096 .cmp(&(
5097 right.path_hash,
5098 right.path.as_slice(),
5099 right.member.tar_member_group_start,
5100 ))
5101 });
5102 rows
5103}
5104
5105fn partition_file_rows(rows: Vec<FileRow>) -> Result<Vec<Vec<FileRow>>, FormatError> {
5106 let mut shards = Vec::new();
5107 let mut start = 0usize;
5108 while start < rows.len() {
5109 let mut end = (start + DEFAULT_FILES_PER_INDEX_SHARD).min(rows.len());
5110 if end < rows.len() && rows[end - 1].path_hash == rows[end].path_hash {
5111 let boundary_hash = rows[end].path_hash;
5112 let mut run_start_in_shard = end - 1;
5113 while run_start_in_shard > start
5114 && rows[run_start_in_shard - 1].path_hash == boundary_hash
5115 {
5116 run_start_in_shard -= 1;
5117 }
5118 let mut full_run_start = run_start_in_shard;
5119 while full_run_start > 0 && rows[full_run_start - 1].path_hash == boundary_hash {
5120 full_run_start -= 1;
5121 }
5122 let mut full_run_end = end + 1;
5123 while full_run_end < rows.len() && rows[full_run_end].path_hash == boundary_hash {
5124 full_run_end += 1;
5125 }
5126 let full_run_len = full_run_end - full_run_start;
5127 end = if full_run_len <= MAX_HASH_PREFIX_RUN_FILES {
5128 full_run_end
5129 } else {
5130 (run_start_in_shard + MAX_HASH_PREFIX_RUN_FILES).min(full_run_end)
5131 };
5132 }
5133 if end - start > MAX_FILES_PER_INDEX_SHARD {
5134 return Err(FormatError::WriterUnsupported(
5135 "hash-prefix collision run exceeds max_files_per_index_shard",
5136 ));
5137 }
5138 shards.push(rows[start..end].to_vec());
5139 start = end;
5140 }
5141 Ok(shards)
5142}
5143
5144fn build_index_shard_plaintexts(
5145 shard_rows: &[Vec<FileRow>],
5146 frames: &[PayloadFrame],
5147 payloads: &[PayloadObject],
5148 options: WriterOptions,
5149) -> Result<Vec<PlannedIndexShard>, FormatError> {
5150 let mut planned = Vec::new();
5151 for rows in shard_rows {
5152 append_index_shards_for_rows(&mut planned, rows, frames, payloads, options)?;
5153 }
5154 Ok(planned)
5155}
5156
5157fn append_index_shards_for_rows(
5158 planned: &mut Vec<PlannedIndexShard>,
5159 rows: &[FileRow],
5160 frames: &[PayloadFrame],
5161 payloads: &[PayloadObject],
5162 options: WriterOptions,
5163) -> Result<(), FormatError> {
5164 let shard_index =
5165 u64::try_from(planned.len()).map_err(|_| FormatError::WriterUnsupported("shard_index"))?;
5166 let candidate = build_index_shard_plaintext(shard_index, rows, frames, payloads, options)?;
5167 let compressed =
5168 compress_zstd_frame_with_jobs(&candidate.plaintext, options.zstd_level, options.jobs)?;
5169 if index_object_can_fit(compressed.len(), options)? {
5170 planned.push(candidate);
5171 return Ok(());
5172 }
5173 if rows.len() == 1 {
5174 return Err(FormatError::WriterUnsupported(
5175 "single-file IndexShard exceeds index object limits",
5176 ));
5177 }
5178 let split_at = split_sorted_file_rows_for_object_limit(rows);
5179 append_index_shards_for_rows(planned, &rows[..split_at], frames, payloads, options)?;
5180 append_index_shards_for_rows(planned, &rows[split_at..], frames, payloads, options)
5181}
5182
5183fn split_sorted_file_rows_for_object_limit(rows: &[FileRow]) -> usize {
5184 let midpoint = rows.len() / 2;
5185 if rows[midpoint - 1].path_hash != rows[midpoint].path_hash {
5186 return midpoint;
5187 }
5188
5189 let boundary_hash = rows[midpoint].path_hash;
5190 let mut left = midpoint;
5191 while left > 0 && rows[left - 1].path_hash == boundary_hash {
5192 left -= 1;
5193 }
5194 let mut right = midpoint;
5195 while right < rows.len() && rows[right].path_hash == boundary_hash {
5196 right += 1;
5197 }
5198
5199 match (left > 0, right < rows.len()) {
5200 (true, true) if midpoint - left <= right - midpoint => left,
5201 (true, true) => right,
5202 (true, false) => left,
5203 (false, true) => right,
5204 (false, false) => midpoint,
5205 }
5206}
5207
5208fn build_index_shard_plaintext(
5209 shard_index: u64,
5210 file_rows: &[FileRow],
5211 frames: &[PayloadFrame],
5212 payloads: &[PayloadObject],
5213 options: WriterOptions,
5214) -> Result<PlannedIndexShard, FormatError> {
5215 let mut string_pool = Vec::new();
5216 let mut file_entries = Vec::with_capacity(file_rows.len());
5217 let mut required_frame_indexes = BTreeSet::new();
5218 for row in file_rows {
5219 let path_offset = u32_len(string_pool.len(), "FileEntry.path_offset")?;
5220 string_pool.extend_from_slice(&row.path);
5221 let (first_frame_index, frame_count) = member_frame_range(row.member_index, frames)?;
5222 for offset in 0..frame_count as u64 {
5223 required_frame_indexes.insert(checked_u64_add(
5224 first_frame_index,
5225 offset,
5226 "FileEntry.frame_count",
5227 )?);
5228 }
5229 file_entries.push(FileEntry {
5230 path_hash: row.path_hash,
5231 path_offset,
5232 path_length: u32_len(row.path.len(), "FileEntry.path_length")?,
5233 first_frame_index,
5234 frame_count,
5235 offset_in_first_frame_plaintext: 0,
5236 tar_member_group_size: row.member.tar_member_group_size,
5237 file_data_size: row.member.file_data_size,
5238 flags: v45_portable_file_entry_flags(
5239 row.member.mode,
5240 row.member.sparse_extents.is_some(),
5241 &row.member.portable_metadata,
5242 ),
5243 });
5244 }
5245
5246 let frame_entries = frames
5247 .iter()
5248 .filter(|frame| required_frame_indexes.contains(&frame.frame_index))
5249 .map(|frame| FrameEntry {
5250 frame_index: frame.frame_index,
5251 envelope_index: frame.envelope_index,
5252 offset_in_envelope: frame.offset_in_envelope,
5253 compressed_size: frame.compressed_size,
5254 decompressed_size: frame.decompressed_size,
5255 flags: frame.flags,
5256 tar_stream_offset: frame.tar_stream_offset,
5257 })
5258 .collect::<Vec<_>>();
5259 let required_envelope_indexes = frame_entries
5260 .iter()
5261 .map(|frame| frame.envelope_index)
5262 .collect::<BTreeSet<_>>();
5263 let envelope_entries = payloads
5264 .iter()
5265 .filter(|payload| required_envelope_indexes.contains(&payload.envelope_index))
5266 .map(|payload| {
5267 let (first_frame_index, frame_count) =
5268 envelope_frame_range(payload.envelope_index, frames)?;
5269 Ok(EnvelopeEntry {
5270 envelope_index: payload.envelope_index,
5271 first_block_index: payload.object.first_block_index,
5272 data_block_count: payload.object.data_block_count,
5273 parity_block_count: payload.object.parity_block_count,
5274 encrypted_size: payload.object.encrypted_size,
5275 plaintext_size: payload.plaintext_size,
5276 first_frame_index,
5277 frame_count,
5278 })
5279 })
5280 .collect::<Result<Vec<_>, FormatError>>()?;
5281
5282 let plaintext = serialize_index_shard(
5283 shard_index,
5284 &file_entries,
5285 &frame_entries,
5286 &envelope_entries,
5287 &string_pool,
5288 options,
5289 )?;
5290 let first_path_hash = file_rows
5291 .first()
5292 .ok_or(FormatError::WriterInvariant("empty planned IndexShard"))?
5293 .path_hash;
5294 let last_path_hash = file_rows
5295 .last()
5296 .ok_or(FormatError::WriterInvariant("empty planned IndexShard"))?
5297 .path_hash;
5298 Ok(PlannedIndexShard {
5299 shard_index,
5300 plaintext,
5301 file_count: u32_len(file_rows.len(), "IndexShard.file_count")?,
5302 first_path_hash,
5303 last_path_hash,
5304 })
5305}
5306
5307fn serialize_index_shard(
5308 shard_index: u64,
5309 files: &[FileEntry],
5310 frames: &[FrameEntry],
5311 envelopes: &[EnvelopeEntry],
5312 string_pool: &[u8],
5313 _options: WriterOptions,
5314) -> Result<Vec<u8>, FormatError> {
5315 let mut cursor = INDEX_SHARD_HEADER_LEN;
5316 let file_table_offset = table_offset(files.len(), cursor)?;
5317 cursor = checked_usize_add(cursor, files.len() * FILE_ENTRY_LEN, "IndexShard")?;
5318 let frame_table_offset = table_offset(frames.len(), cursor)?;
5319 cursor = checked_usize_add(cursor, frames.len() * FRAME_ENTRY_LEN, "IndexShard")?;
5320 let envelope_table_offset = table_offset(envelopes.len(), cursor)?;
5321 cursor = checked_usize_add(cursor, envelopes.len() * ENVELOPE_ENTRY_LEN, "IndexShard")?;
5322 let string_pool_offset = table_offset(string_pool.len(), cursor)?;
5323
5324 let header = IndexShardHeader {
5325 version: 1,
5326 shard_index,
5327 file_count: u32_len(files.len(), "IndexShard.file_count")?,
5328 frame_count: u32_len(frames.len(), "IndexShard.frame_count")?,
5329 envelope_count: u32_len(envelopes.len(), "IndexShard.envelope_count")?,
5330 file_table_offset,
5331 frame_table_offset,
5332 envelope_table_offset,
5333 string_pool_offset,
5334 string_pool_size: u32_len(string_pool.len(), "IndexShard.string_pool_size")?,
5335 };
5336
5337 let mut bytes = Vec::with_capacity(cursor + string_pool.len());
5338 bytes.extend_from_slice(&header.to_bytes());
5339 for entry in files {
5340 bytes.extend_from_slice(&entry.to_bytes());
5341 }
5342 for entry in frames {
5343 bytes.extend_from_slice(&entry.to_bytes());
5344 }
5345 for entry in envelopes {
5346 bytes.extend_from_slice(&entry.to_bytes());
5347 }
5348 bytes.extend_from_slice(string_pool);
5349 Ok(bytes)
5350}
5351
5352fn build_directory_hint_plaintexts(
5353 shard_rows: &[Vec<FileRow>],
5354 options: WriterOptions,
5355) -> Result<Vec<PlannedDirectoryHintShard>, FormatError> {
5356 let mut map = BTreeMap::<Vec<u8>, BTreeSet<u32>>::new();
5357 for (shard_row_index, rows) in shard_rows.iter().enumerate() {
5358 let shard_row_index = u32::try_from(shard_row_index)
5359 .map_err(|_| FormatError::WriterUnsupported("directory hint shard row index"))?;
5360 for row in rows {
5361 add_directory_hint_rows(&mut map, shard_row_index, &row.path);
5362 }
5363 }
5364
5365 let rows = map
5366 .into_iter()
5367 .map(|(path, shard_rows)| (hash_prefix(&path), path, shard_rows))
5368 .collect::<Vec<_>>();
5369 let mut rows = rows;
5370 rows.sort_by(|left, right| (left.0, left.1.as_slice()).cmp(&(right.0, right.1.as_slice())));
5371
5372 let mut planned = Vec::new();
5373 for chunk in rows.chunks(DEFAULT_DIRECTORY_HINT_ENTRIES_PER_SHARD) {
5374 append_directory_hint_shards_for_rows(&mut planned, chunk, options)?;
5375 }
5376 Ok(planned)
5377}
5378
5379fn append_directory_hint_shards_for_rows(
5380 planned: &mut Vec<PlannedDirectoryHintShard>,
5381 rows: &[([u8; 8], Vec<u8>, BTreeSet<u32>)],
5382 options: WriterOptions,
5383) -> Result<(), FormatError> {
5384 let hint_shard_index = u64::try_from(planned.len())
5385 .map_err(|_| FormatError::WriterUnsupported("hint_shard_index"))?;
5386 let candidate = build_directory_hint_plaintext(hint_shard_index, rows)?;
5387 let compressed =
5388 compress_zstd_frame_with_jobs(&candidate.plaintext, options.zstd_level, options.jobs)?;
5389 if index_object_can_fit(compressed.len(), options)? {
5390 planned.push(candidate);
5391 return Ok(());
5392 }
5393 if rows.len() == 1 {
5394 return Err(FormatError::WriterUnsupported(
5395 "single DirectoryHintEntry exceeds index object limits",
5396 ));
5397 }
5398 let split_at = rows.len() / 2;
5399 append_directory_hint_shards_for_rows(planned, &rows[..split_at], options)?;
5400 append_directory_hint_shards_for_rows(planned, &rows[split_at..], options)
5401}
5402
5403fn add_directory_hint_rows(
5404 map: &mut BTreeMap<Vec<u8>, BTreeSet<u32>>,
5405 shard_row_index: u32,
5406 path: &[u8],
5407) {
5408 map.entry(Vec::new()).or_default().insert(shard_row_index);
5409 let mut cursor = 0usize;
5410 while let Some(position) = path[cursor..].iter().position(|byte| *byte == b'/') {
5411 let slash = cursor + position;
5412 if slash > 0 {
5413 map.entry(path[..slash].to_vec())
5414 .or_default()
5415 .insert(shard_row_index);
5416 }
5417 cursor = slash + 1;
5418 }
5419}
5420
5421fn build_directory_hint_plaintext(
5422 hint_shard_index: u64,
5423 rows: &[([u8; 8], Vec<u8>, BTreeSet<u32>)],
5424) -> Result<PlannedDirectoryHintShard, FormatError> {
5425 let mut entries = Vec::with_capacity(rows.len());
5426 let mut shard_row_indexes = Vec::new();
5427 let mut string_pool = Vec::new();
5428
5429 for (dir_hash, path, shard_rows) in rows {
5430 let path_offset = if path.is_empty() {
5431 0
5432 } else {
5433 u64::try_from(string_pool.len())
5434 .map_err(|_| FormatError::WriterUnsupported("DirectoryHintEntry.path_offset"))?
5435 };
5436 if !path.is_empty() {
5437 string_pool.extend_from_slice(path);
5438 }
5439 let shard_list_start_index = u32_len(
5440 shard_row_indexes.len(),
5441 "DirectoryHintEntry.shard_list_start_index",
5442 )?;
5443 shard_row_indexes.extend(shard_rows.iter().copied());
5444 entries.push(DirectoryHintEntry {
5445 dir_hash: *dir_hash,
5446 path_offset,
5447 path_length: u32_len(path.len(), "DirectoryHintEntry.path_length")?,
5448 shard_list_start_index,
5449 shard_count: u32_len(shard_rows.len(), "DirectoryHintEntry.shard_count")?,
5450 });
5451 }
5452
5453 let plaintext = serialize_directory_hint_table(
5454 hint_shard_index,
5455 &entries,
5456 &shard_row_indexes,
5457 &string_pool,
5458 )?;
5459 let first_dir_hash = rows
5460 .first()
5461 .ok_or(FormatError::WriterInvariant("empty directory hint shard"))?
5462 .0;
5463 let last_dir_hash = rows
5464 .last()
5465 .ok_or(FormatError::WriterInvariant("empty directory hint shard"))?
5466 .0;
5467 Ok(PlannedDirectoryHintShard {
5468 hint_shard_index,
5469 plaintext,
5470 entry_count: rows.len() as u64,
5471 first_dir_hash,
5472 last_dir_hash,
5473 })
5474}
5475
5476fn serialize_directory_hint_table(
5477 hint_shard_index: u64,
5478 entries: &[DirectoryHintEntry],
5479 shard_row_indexes: &[u32],
5480 string_pool: &[u8],
5481) -> Result<Vec<u8>, FormatError> {
5482 let entry_table_offset = table_offset(entries.len(), DIRECTORY_HINT_TABLE_LEN)?;
5483 let shard_list_cursor = checked_usize_add(
5484 DIRECTORY_HINT_TABLE_LEN,
5485 entries.len() * DIRECTORY_HINT_ENTRY_LEN,
5486 "DirectoryHintTable",
5487 )?;
5488 let shard_list_offset = table_offset(shard_row_indexes.len(), shard_list_cursor)?;
5489 let string_pool_cursor = checked_usize_add(
5490 shard_list_cursor,
5491 shard_row_indexes.len() * 4,
5492 "DirectoryHintTable",
5493 )?;
5494 let string_pool_offset = if string_pool.is_empty() {
5495 0
5496 } else {
5497 u64::try_from(string_pool_cursor)
5498 .map_err(|_| FormatError::WriterUnsupported("DirectoryHintTable.string_pool_offset"))?
5499 };
5500 let header = DirectoryHintTableHeader {
5501 version: 1,
5502 hint_shard_index,
5503 entry_count: entries.len() as u64,
5504 entry_table_offset: entry_table_offset as u64,
5505 shard_list_offset: shard_list_offset as u64,
5506 string_pool_offset,
5507 string_pool_size: string_pool.len() as u64,
5508 };
5509
5510 let mut bytes = Vec::with_capacity(string_pool_cursor + string_pool.len());
5511 bytes.extend_from_slice(&header.to_bytes());
5512 for entry in entries {
5513 bytes.extend_from_slice(&entry.to_bytes());
5514 }
5515 for row in shard_row_indexes {
5516 bytes.extend_from_slice(&row.to_le_bytes());
5517 }
5518 bytes.extend_from_slice(string_pool);
5519 Ok(bytes)
5520}
5521
5522#[derive(Debug, Clone, Copy)]
5523struct IndexRootPlaintextInput<'a> {
5524 shard_entries: &'a [ShardEntry],
5525 frame_count: u64,
5526 envelope_count: u64,
5527 file_count: u64,
5528 payload_block_count: u64,
5529 tar_total_size: u64,
5530 content_sha256: [u8; 32],
5531 directory_hint_entries: &'a [DirectoryHintShardEntry],
5532 dictionary_extent: Option<(ObjectExtent, u32)>,
5533}
5534
5535fn build_index_root_plaintext(input: IndexRootPlaintextInput<'_>) -> Vec<u8> {
5536 let mut header = IndexRootHeader::empty();
5537 header.frame_count = input.frame_count;
5538 header.envelope_count = input.envelope_count;
5539 header.file_count = input.file_count;
5540 header.payload_block_count = input.payload_block_count;
5541 header.tar_total_size = input.tar_total_size;
5542 header.content_sha256 = input.content_sha256;
5543 if let Some((dictionary, decompressed_size)) = input.dictionary_extent {
5544 header.dictionary_first_block = dictionary.first_block_index;
5545 header.dictionary_data_block_count = dictionary.data_block_count;
5546 header.dictionary_parity_block_count = dictionary.parity_block_count;
5547 header.dictionary_encrypted_size = dictionary.encrypted_size;
5548 header.dictionary_decompressed_size = decompressed_size;
5549 }
5550 let root = IndexRoot {
5551 header,
5552 shards: input.shard_entries.to_vec(),
5553 directory_hint_shards: input.directory_hint_entries.to_vec(),
5554 };
5555 root.to_bytes()
5556}
5557
5558#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5559struct MetadataClassPlan {
5560 options: WriterOptions,
5561 index_root: PlannedEncryptedObject,
5562 dictionary: Option<PlannedEncryptedObject>,
5563}
5564
5565fn plan_index_root_metadata_class(
5566 mut options: WriterOptions,
5567 compressed_index_root_len: usize,
5568 compressed_dictionary_len: Option<usize>,
5569) -> Result<MetadataClassPlan, FormatError> {
5570 let index_root = plan_metadata_object_without_class(
5571 compressed_index_root_len,
5572 options,
5573 MetadataObjectKind::IndexRoot,
5574 )?;
5575 let dictionary = compressed_dictionary_len
5576 .map(|len| plan_metadata_object_without_class(len, options, MetadataObjectKind::Dictionary))
5577 .transpose()?;
5578 let required_data_shards = u32::from(options.index_root_fec_data_shards)
5579 .max(MIN_INDEX_ROOT_FEC_DATA_SHARDS as u32)
5580 .max(index_root.data_block_count)
5581 .max(dictionary.map(|plan| plan.data_block_count).unwrap_or(0));
5582 let required_data_shards = u16::try_from(required_data_shards)
5583 .map_err(|_| MetadataObjectKind::IndexRoot.too_large_error())?;
5584 options.index_root_fec_data_shards = required_data_shards;
5585 let required_parity_shards = compute_parity_u16(
5586 options.index_root_fec_data_shards as u64,
5587 options,
5588 "index_root_fec_parity_shards",
5589 )?;
5590 options.index_root_fec_parity_shards = options
5591 .index_root_fec_parity_shards
5592 .max(required_parity_shards);
5593 ensure_metadata_object_fits_class(index_root, options, MetadataObjectKind::IndexRoot)?;
5594 if let Some(dictionary) = dictionary {
5595 ensure_metadata_object_fits_class(dictionary, options, MetadataObjectKind::Dictionary)?;
5596 }
5597 Ok(MetadataClassPlan {
5598 options,
5599 index_root,
5600 dictionary,
5601 })
5602}
5603
5604fn plan_metadata_object_without_class(
5605 payload_len: usize,
5606 options: WriterOptions,
5607 kind: MetadataObjectKind,
5608) -> Result<PlannedEncryptedObject, FormatError> {
5609 let plan = plan_encrypted_object_without_class(payload_len, options)
5610 .map_err(|_| kind.too_large_error())?;
5611 if plan.data_block_count > u16::MAX as u32 || plan.parity_block_count > u16::MAX as u32 {
5612 return Err(kind.too_large_error());
5613 }
5614 validate_object_shard_total(plan.data_block_count, plan.parity_block_count)
5615 .map_err(|_| kind.too_large_error())?;
5616 Ok(plan)
5617}
5618
5619fn ensure_metadata_object_fits_class(
5620 plan: PlannedEncryptedObject,
5621 options: WriterOptions,
5622 kind: MetadataObjectKind,
5623) -> Result<(), FormatError> {
5624 if plan.data_block_count > options.index_root_fec_data_shards as u32 {
5625 return Err(kind.too_large_error());
5626 }
5627 if plan.parity_block_count > options.index_root_fec_parity_shards as u32 {
5628 return Err(kind.too_large_error());
5629 }
5630 validate_object_shard_total(plan.data_block_count, plan.parity_block_count)
5631 .map_err(|_| kind.too_large_error())
5632}
5633
5634fn payload_object_can_fit(payload_len: usize, options: WriterOptions) -> Result<bool, FormatError> {
5635 encrypted_object_can_fit(
5636 payload_len,
5637 options.fec_data_shards,
5638 options.fec_parity_shards,
5639 options,
5640 )
5641}
5642
5643fn index_object_can_fit(payload_len: usize, options: WriterOptions) -> Result<bool, FormatError> {
5644 encrypted_object_can_fit(
5645 payload_len,
5646 options.index_fec_data_shards,
5647 options.index_fec_parity_shards,
5648 options,
5649 )
5650}
5651
5652fn encrypted_object_can_fit(
5653 payload_len: usize,
5654 data_shard_max: u16,
5655 parity_shard_max: u16,
5656 options: WriterOptions,
5657) -> Result<bool, FormatError> {
5658 match plan_encrypted_object(payload_len, data_shard_max, parity_shard_max, options) {
5659 Ok(_) => Ok(true),
5660 Err(FormatError::WriterUnsupported("encrypted object exceeds u32 size limit"))
5661 | Err(FormatError::WriterUnsupported(
5662 "encrypted object exceeds its data shard class maximum",
5663 ))
5664 | Err(FormatError::WriterUnsupported(
5665 "encrypted object exceeds its parity shard class maximum",
5666 ))
5667 | Err(FormatError::WriterUnsupported(
5668 "encrypted object exceeds ReedSolomonGF16 shard limit",
5669 )) => Ok(false),
5670 Err(err) => Err(err),
5671 }
5672}
5673
5674fn plan_encrypted_object(
5675 payload_len: usize,
5676 data_shard_max: u16,
5677 parity_shard_max: u16,
5678 options: WriterOptions,
5679) -> Result<PlannedEncryptedObject, FormatError> {
5680 let plan = plan_encrypted_object_without_class(payload_len, options)?;
5681 if plan.data_block_count > data_shard_max as u32 {
5682 return Err(FormatError::WriterUnsupported(
5683 "encrypted object exceeds its data shard class maximum",
5684 ));
5685 }
5686 if plan.parity_block_count > parity_shard_max as u32 {
5687 return Err(FormatError::WriterUnsupported(
5688 "encrypted object exceeds its parity shard class maximum",
5689 ));
5690 }
5691 validate_object_shard_total(plan.data_block_count, plan.parity_block_count)?;
5692 Ok(plan)
5693}
5694
5695fn plan_encrypted_object_without_class(
5696 payload_len: usize,
5697 options: WriterOptions,
5698) -> Result<PlannedEncryptedObject, FormatError> {
5699 let (data_block_count, encrypted_size) = encrypted_object_data_extent(payload_len, options)?;
5700 let parity_block_count = compute_parity(data_block_count as u64, options)?;
5701 Ok(PlannedEncryptedObject {
5702 data_block_count,
5703 parity_block_count,
5704 encrypted_size,
5705 })
5706}
5707
5708fn encrypted_object_data_extent(
5709 payload_len: usize,
5710 options: WriterOptions,
5711) -> Result<(u32, u32), FormatError> {
5712 let block_size = options.block_size as usize;
5713 let tag_len = options.aead_algo.tag_len();
5714 let total_before_padding =
5715 payload_len
5716 .checked_add(tag_len)
5717 .ok_or(FormatError::WriterUnsupported(
5718 "encrypted object size overflow",
5719 ))?;
5720 let remainder = total_before_padding % block_size;
5721 let encrypted_size = if remainder == 0 {
5722 total_before_padding
5723 .checked_add(block_size)
5724 .ok_or(FormatError::WriterUnsupported(
5725 "encrypted object size overflow",
5726 ))?
5727 } else {
5728 total_before_padding
5729 .checked_add(block_size - remainder)
5730 .ok_or(FormatError::WriterUnsupported(
5731 "encrypted object size overflow",
5732 ))?
5733 };
5734 let encrypted_size = u32_len(encrypted_size, "encrypted_size")
5735 .map_err(|_| FormatError::WriterUnsupported("encrypted object exceeds u32 size limit"))?;
5736 Ok((encrypted_size / options.block_size, encrypted_size))
5737}
5738
5739#[derive(Debug, Clone, Copy)]
5740struct ObjectEncryptionContext<'a> {
5741 key: &'a [u8; 32],
5742 nonce_seed: &'a [u8; 32],
5743 domain: &'a [u8],
5744 counter: u64,
5745 data_kind: BlockKind,
5746 parity_kind: BlockKind,
5747 data_shard_max: u16,
5748 class_parity_shard_max: u16,
5749 archive_uuid: &'a [u8; 16],
5750 session_id: &'a [u8; 16],
5751}
5752
5753fn encrypt_object(
5754 payload: &[u8],
5755 context: ObjectEncryptionContext<'_>,
5756 next_block_index: &mut u64,
5757 options: WriterOptions,
5758) -> Result<EncryptedObject, FormatError> {
5759 let block_size = options.block_size as usize;
5760 let encrypted = encrypt_object_payload(payload, context, options)?;
5761 if encrypted.len() % block_size != 0 {
5762 return Err(FormatError::WriterInvariant(
5763 "encrypted object is not block aligned",
5764 ));
5765 }
5766 let encrypted_size = u32_len(encrypted.len(), "encrypted_size")?;
5767 let data_shards = encrypted
5768 .chunks(block_size)
5769 .map(|chunk| chunk.to_vec())
5770 .collect::<Vec<_>>();
5771 let data_block_count = u32_len(data_shards.len(), "data_block_count")?;
5772 if data_block_count == 0 {
5773 return Err(FormatError::WriterInvariant(
5774 "encrypted object has no data blocks",
5775 ));
5776 }
5777 if data_block_count > context.data_shard_max as u32 {
5778 return Err(FormatError::WriterUnsupported(
5779 "encrypted object exceeds its data shard class maximum",
5780 ));
5781 }
5782 let required_parity = compute_object_parity(
5783 data_block_count as u64,
5784 options,
5785 context.class_parity_shard_max as u32,
5786 )?;
5787 if required_parity > context.class_parity_shard_max as u32 {
5788 return Err(FormatError::WriterUnsupported(
5789 "encrypted object exceeds its parity shard class maximum",
5790 ));
5791 }
5792 validate_object_shard_total(data_block_count, required_parity)?;
5793 let parity_count = required_parity as u16;
5794 let parity_shards = if parity_count == 0 {
5795 Vec::new()
5796 } else {
5797 encode_parity_gf16(&data_shards, parity_count as usize)?
5798 };
5799
5800 let first_block_index = *next_block_index;
5801 let mut records = Vec::with_capacity(data_shards.len() + parity_shards.len());
5802 for (index, payload) in data_shards.into_iter().enumerate() {
5803 records.push(BlockRecord {
5804 block_index: checked_u64_add(first_block_index, index as u64, "BlockRecord")?,
5805 kind: context.data_kind,
5806 flags: if index + 1 == data_block_count as usize {
5807 0x01
5808 } else {
5809 0
5810 },
5811 payload,
5812 record_crc32c: 0,
5813 });
5814 }
5815 let parity_first_block = checked_u64_add(first_block_index, data_block_count as u64, "FEC")?;
5816 for (index, payload) in parity_shards.into_iter().enumerate() {
5817 records.push(BlockRecord {
5818 block_index: checked_u64_add(parity_first_block, index as u64, "BlockRecord")?,
5819 kind: context.parity_kind,
5820 flags: 0,
5821 payload,
5822 record_crc32c: 0,
5823 });
5824 }
5825
5826 *next_block_index = checked_u64_add(
5827 first_block_index,
5828 data_block_count as u64 + parity_count as u64,
5829 "next_block_index",
5830 )?;
5831
5832 Ok(EncryptedObject {
5833 first_block_index,
5834 data_block_count,
5835 parity_block_count: parity_count as u32,
5836 encrypted_size,
5837 records,
5838 })
5839}
5840
5841fn serialize_zero_parity_encrypted_object(
5842 payload: &[u8],
5843 context: ObjectEncryptionContext<'_>,
5844 expected_extent: ObjectExtent,
5845 options: WriterOptions,
5846) -> Result<Vec<SerializedBlockRecord>, FormatError> {
5847 let planned = plan_encrypted_object(
5848 payload.len(),
5849 context.data_shard_max,
5850 context.class_parity_shard_max,
5851 options,
5852 )?;
5853 if planned.parity_block_count != 0 || expected_extent.parity_block_count != 0 {
5854 return Err(FormatError::WriterInvariant(
5855 "zero-parity serialization received a parity object",
5856 ));
5857 }
5858 if planned.data_block_count != expected_extent.data_block_count
5859 || planned.encrypted_size != expected_extent.encrypted_size
5860 {
5861 return Err(FormatError::WriterInvariant(
5862 "encrypted object did not match planned sizing",
5863 ));
5864 }
5865
5866 let block_size = options.block_size as usize;
5867 let encrypted = encrypt_object_payload(payload, context, options)?;
5868 if encrypted.len() != expected_extent.encrypted_size as usize
5869 || encrypted.len() % block_size != 0
5870 {
5871 return Err(FormatError::WriterInvariant(
5872 "encrypted object did not match planned sizing",
5873 ));
5874 }
5875 let data_block_count = encrypted.len() / block_size;
5876 if data_block_count == 0 || data_block_count != expected_extent.data_block_count as usize {
5877 return Err(FormatError::WriterInvariant(
5878 "encrypted object did not match planned sizing",
5879 ));
5880 }
5881
5882 let mut records = Vec::with_capacity(data_block_count);
5883 for (index, chunk) in encrypted.chunks(block_size).enumerate() {
5884 let block_index = checked_u64_add(
5885 expected_extent.first_block_index,
5886 index as u64,
5887 "BlockRecord",
5888 )?;
5889 let flags = if index + 1 == data_block_count {
5890 0x01
5891 } else {
5892 0
5893 };
5894 records.push(SerializedBlockRecord {
5895 block_index,
5896 bytes: BlockRecord::to_bytes_from_parts(block_index, context.data_kind, flags, chunk),
5897 });
5898 }
5899 Ok(records)
5900}
5901
5902fn encrypt_object_payload(
5903 payload: &[u8],
5904 context: ObjectEncryptionContext<'_>,
5905 options: WriterOptions,
5906) -> Result<Vec<u8>, FormatError> {
5907 let block_size = options.block_size as usize;
5908 let padded = suffix_pad_for_aead(payload, options.aead_algo.tag_len(), block_size)?;
5909 if matches!(options.aead_algo, AeadAlgo::None) {
5910 return Ok(padded);
5911 }
5912 let nonce = derive_nonce(
5913 context.nonce_seed,
5914 context.domain,
5915 context.archive_uuid,
5916 context.session_id,
5917 context.counter,
5918 options.aead_algo.nonce_len(),
5919 )?;
5920 let aad = build_aad(
5921 context.domain,
5922 context.archive_uuid,
5923 context.session_id,
5924 context.counter,
5925 )?;
5926 aead_encrypt(options.aead_algo, context.key, &nonce, &aad, &padded)
5927}
5928
5929fn validate_planned_object(
5930 object: &EncryptedObject,
5931 expected: PlannedEncryptedObject,
5932) -> Result<(), FormatError> {
5933 if object.data_block_count != expected.data_block_count
5934 || object.parity_block_count != expected.parity_block_count
5935 || object.encrypted_size != expected.encrypted_size
5936 {
5937 return Err(FormatError::WriterInvariant(
5938 "encrypted object did not match planned sizing",
5939 ));
5940 }
5941 Ok(())
5942}
5943
5944fn validate_planned_extent(
5945 object: &EncryptedObject,
5946 expected: ObjectExtent,
5947) -> Result<(), FormatError> {
5948 validate_planned_object(
5949 object,
5950 PlannedEncryptedObject {
5951 data_block_count: expected.data_block_count,
5952 parity_block_count: expected.parity_block_count,
5953 encrypted_size: expected.encrypted_size,
5954 },
5955 )?;
5956 if object.first_block_index != expected.first_block_index {
5957 return Err(FormatError::WriterInvariant(
5958 "encrypted object did not match planned extent",
5959 ));
5960 }
5961 Ok(())
5962}
5963
5964fn map_metadata_encrypt_error(error: FormatError, kind: MetadataObjectKind) -> FormatError {
5965 match error {
5966 FormatError::WriterUnsupported("encrypted object exceeds u32 size limit")
5967 | FormatError::WriterUnsupported("encrypted object exceeds its data shard class maximum")
5968 | FormatError::WriterUnsupported(
5969 "encrypted object exceeds its parity shard class maximum",
5970 )
5971 | FormatError::WriterUnsupported("encrypted object exceeds ReedSolomonGF16 shard limit") => {
5972 kind.too_large_error()
5973 }
5974 other => other,
5975 }
5976}
5977
5978#[derive(Debug, Clone, Copy)]
5979struct RootAuthFooterBuildInput<'a> {
5980 archive_uuid: [u8; 16],
5981 session_id: [u8; 16],
5982 volume_format_rev: u16,
5983 options: WriterOptions,
5984 crypto_header: &'a [u8],
5985 volume_zero_manifest: &'a [u8; MANIFEST_FOOTER_LEN],
5986 index_root_plaintext: &'a [u8],
5987 index_root_extent: ObjectExtent,
5988 dictionary_extent: Option<(ObjectExtent, u32)>,
5989 shard_entries: &'a [ShardEntry],
5990 payload_objects: &'a [PayloadObject],
5991 directory_hint_entries: &'a [DirectoryHintShardEntry],
5992 data_leaf_hashes: &'a [(u64, [u8; 32])],
5993}
5994
5995fn build_root_auth_footer_from_leaf_hashes(
5996 config: RootAuthWriterConfig<'_>,
5997 authenticator: &mut RootAuthAuthenticator<'_>,
5998 input: RootAuthFooterBuildInput<'_>,
5999) -> Result<Vec<u8>, FormatError> {
6000 let mut sorted_leaf_hashes = input.data_leaf_hashes.to_vec();
6001 sorted_leaf_hashes.sort_by_key(|(block_index, _)| *block_index);
6002 let leaf_hashes = sorted_leaf_hashes
6003 .iter()
6004 .map(|(_, leaf_hash)| *leaf_hash)
6005 .collect::<Vec<_>>();
6006 let total_data_block_count = u64::try_from(leaf_hashes.len())
6007 .map_err(|_| FormatError::WriterUnsupported("root-auth data block count"))?;
6008 let data_block_merkle_root = data_block_merkle_root_from_leaf_hashes_for_revision(
6009 FORMAT_VERSION,
6010 input.volume_format_rev,
6011 &leaf_hashes,
6012 )?;
6013
6014 let parsed_crypto = CryptoHeader::parse(
6015 input.crypto_header,
6016 u32_len(input.crypto_header.len(), "CryptoHeader")?,
6017 )?;
6018 let footer_length = root_auth_footer_wire_length(
6019 config.signer_identity.len(),
6020 config.authenticator_value_length as usize,
6021 )?;
6022 let root_auth_descriptor_digest = root_auth_descriptor_digest_for_revision(
6023 FORMAT_VERSION,
6024 input.volume_format_rev,
6025 config.authenticator_id,
6026 config.signer_identity_type,
6027 config.signer_identity,
6028 config.authenticator_value_length,
6029 footer_length,
6030 )?;
6031 let signer_identity_digest =
6032 signer_identity_digest(config.signer_identity_type, config.signer_identity)?;
6033 let manifest_pre_hmac = manifest_footer_global_pre_hmac_bytes(input.volume_zero_manifest);
6034 let critical_metadata_digest = critical_metadata_digest(CriticalMetadataDigestInputs {
6035 archive_uuid: input.archive_uuid,
6036 session_id: input.session_id,
6037 format_version: FORMAT_VERSION,
6038 volume_format_rev: input.volume_format_rev,
6039 stripe_width: input.options.stripe_width,
6040 total_volumes: input.options.stripe_width,
6041 compression_algo: parsed_crypto.fixed.compression_algo,
6042 aead_algo: parsed_crypto.fixed.aead_algo,
6043 fec_algo: parsed_crypto.fixed.fec_algo,
6044 kdf_algo: parsed_crypto.fixed.kdf_algo,
6045 crypto_header_pre_hmac_bytes: parsed_crypto.hmac_covered_bytes,
6046 chunk_size: parsed_crypto.fixed.chunk_size,
6047 envelope_target_size: parsed_crypto.fixed.envelope_target_size,
6048 block_size: parsed_crypto.fixed.block_size,
6049 fec_data_shards: parsed_crypto.fixed.fec_data_shards,
6050 fec_parity_shards: parsed_crypto.fixed.fec_parity_shards,
6051 index_fec_data_shards: parsed_crypto.fixed.index_fec_data_shards,
6052 index_fec_parity_shards: parsed_crypto.fixed.index_fec_parity_shards,
6053 index_root_fec_data_shards: parsed_crypto.fixed.index_root_fec_data_shards,
6054 index_root_fec_parity_shards: parsed_crypto.fixed.index_root_fec_parity_shards,
6055 volume_loss_tolerance: parsed_crypto.fixed.volume_loss_tolerance,
6056 bit_rot_buffer_pct: parsed_crypto.fixed.bit_rot_buffer_pct,
6057 has_dictionary: parsed_crypto.fixed.has_dictionary,
6058 manifest_footer_global_pre_hmac_bytes: &manifest_pre_hmac,
6059 index_root_first_block: input.index_root_extent.first_block_index,
6060 index_root_data_block_count: input.index_root_extent.data_block_count,
6061 index_root_parity_block_count: input.index_root_extent.parity_block_count,
6062 index_root_encrypted_size: input.index_root_extent.encrypted_size,
6063 index_root_decompressed_size: u32_len(input.index_root_plaintext.len(), "IndexRoot")?,
6064 root_auth_descriptor_digest,
6065 })?;
6066 let index_digest = index_digest_for_revision(
6067 FORMAT_VERSION,
6068 input.volume_format_rev,
6069 input.index_root_plaintext,
6070 )?;
6071 let fec_layout_rows = writer_fec_layout_rows_from_extents(
6072 input.index_root_extent,
6073 u32_len(input.index_root_plaintext.len(), "IndexRoot")?,
6074 input.dictionary_extent,
6075 input.shard_entries,
6076 input.payload_objects,
6077 input.directory_hint_entries,
6078 );
6079 let expected_data_block_count = fec_layout_rows.iter().try_fold(0u64, |total, row| {
6080 if row.present {
6081 checked_u64_add(
6082 total,
6083 row.data_block_count as u64,
6084 "root-auth data block count",
6085 )
6086 } else {
6087 Ok(total)
6088 }
6089 })?;
6090 if expected_data_block_count != total_data_block_count {
6091 return Err(FormatError::WriterInvariant(
6092 "root-auth data block count does not match FEC layout",
6093 ));
6094 }
6095 let fec_layout_digest =
6096 fec_layout_digest_for_revision(FORMAT_VERSION, input.volume_format_rev, &fec_layout_rows)?;
6097 let archive_root = archive_root_for_revision(ArchiveRootInputs {
6098 archive_uuid: input.archive_uuid,
6099 session_id: input.session_id,
6100 format_version: FORMAT_VERSION,
6101 volume_format_rev: input.volume_format_rev,
6102 compression_algo: parsed_crypto.fixed.compression_algo,
6103 aead_algo: parsed_crypto.fixed.aead_algo,
6104 fec_algo: parsed_crypto.fixed.fec_algo,
6105 kdf_algo: parsed_crypto.fixed.kdf_algo,
6106 critical_metadata_digest,
6107 index_digest,
6108 fec_layout_digest,
6109 total_data_block_count,
6110 data_block_merkle_root,
6111 root_auth_descriptor_digest,
6112 signer_identity_digest,
6113 })?;
6114 let authenticator_value = authenticator(&RootAuthSigningRequest {
6115 root_auth_spec_id: root_auth_spec_id_for_revision(FORMAT_VERSION, input.volume_format_rev)?,
6116 archive_uuid: input.archive_uuid,
6117 session_id: input.session_id,
6118 archive_root,
6119 })?;
6120 if authenticator_value.len() != config.authenticator_value_length as usize {
6121 return Err(FormatError::WriterUnsupported(
6122 "root-auth authenticator length mismatch",
6123 ));
6124 }
6125
6126 RootAuthFooterV1 {
6127 archive_uuid: input.archive_uuid,
6128 session_id: input.session_id,
6129 format_version: FORMAT_VERSION,
6130 volume_format_rev: input.volume_format_rev,
6131 authenticator_id: config.authenticator_id,
6132 signer_identity_type: config.signer_identity_type,
6133 signer_identity_bytes: config.signer_identity.to_vec(),
6134 authenticator_value,
6135 total_data_block_count,
6136 critical_metadata_digest,
6137 index_digest,
6138 fec_layout_digest,
6139 data_block_merkle_root,
6140 signer_identity_digest,
6141 archive_root,
6142 footer_crc32c: 0,
6143 }
6144 .to_bytes()
6145}
6146
6147fn writer_fec_layout_rows_from_extents(
6148 index_root_extent: ObjectExtent,
6149 index_root_plain_size: u32,
6150 dictionary_extent: Option<(ObjectExtent, u32)>,
6151 shard_entries: &[ShardEntry],
6152 payload_objects: &[PayloadObject],
6153 directory_hint_entries: &[DirectoryHintShardEntry],
6154) -> Vec<FecLayoutObjectRow> {
6155 let mut rows = Vec::new();
6156 rows.push(FecLayoutObjectRow {
6157 object_class: 1,
6158 present: true,
6159 object_id: 0,
6160 first_block_index: index_root_extent.first_block_index,
6161 data_block_count: index_root_extent.data_block_count,
6162 parity_block_count: index_root_extent.parity_block_count,
6163 encrypted_size: index_root_extent.encrypted_size,
6164 plain_size: index_root_plain_size,
6165 });
6166 if let Some((dictionary, decompressed_size)) = dictionary_extent {
6167 rows.push(FecLayoutObjectRow {
6168 object_class: 2,
6169 present: true,
6170 object_id: 0,
6171 first_block_index: dictionary.first_block_index,
6172 data_block_count: dictionary.data_block_count,
6173 parity_block_count: dictionary.parity_block_count,
6174 encrypted_size: dictionary.encrypted_size,
6175 plain_size: decompressed_size,
6176 });
6177 } else {
6178 rows.push(FecLayoutObjectRow {
6179 object_class: 2,
6180 present: false,
6181 object_id: 0,
6182 first_block_index: 0,
6183 data_block_count: 0,
6184 parity_block_count: 0,
6185 encrypted_size: 0,
6186 plain_size: 0,
6187 });
6188 }
6189 for entry in shard_entries {
6190 rows.push(FecLayoutObjectRow {
6191 object_class: 3,
6192 present: true,
6193 object_id: entry.shard_index,
6194 first_block_index: entry.first_block_index,
6195 data_block_count: entry.data_block_count,
6196 parity_block_count: entry.parity_block_count,
6197 encrypted_size: entry.encrypted_size,
6198 plain_size: entry.decompressed_size,
6199 });
6200 }
6201 for payload in payload_objects {
6202 rows.push(FecLayoutObjectRow {
6203 object_class: 4,
6204 present: true,
6205 object_id: payload.envelope_index,
6206 first_block_index: payload.object.first_block_index,
6207 data_block_count: payload.object.data_block_count,
6208 parity_block_count: payload.object.parity_block_count,
6209 encrypted_size: payload.object.encrypted_size,
6210 plain_size: payload.plaintext_size,
6211 });
6212 }
6213 for entry in directory_hint_entries {
6214 rows.push(FecLayoutObjectRow {
6215 object_class: 5,
6216 present: true,
6217 object_id: entry.hint_shard_index,
6218 first_block_index: entry.first_block_index,
6219 data_block_count: entry.data_block_count,
6220 parity_block_count: entry.parity_block_count,
6221 encrypted_size: entry.encrypted_size,
6222 plain_size: entry.decompressed_size,
6223 });
6224 }
6225 rows
6226}
6227
6228fn manifest_footer_global_pre_hmac_bytes(manifest_footer: &[u8; MANIFEST_FOOTER_LEN]) -> [u8; 104] {
6229 let mut bytes = [0u8; 104];
6230 bytes.copy_from_slice(&manifest_footer[..104]);
6231 bytes[36..40].fill(0);
6232 bytes
6233}
6234
6235fn root_auth_footer_wire_length(
6236 signer_identity_len: usize,
6237 authenticator_value_len: usize,
6238) -> Result<u32, FormatError> {
6239 validate_root_auth_variable_lengths_for_writer(signer_identity_len, authenticator_value_len)?;
6240 let len = crate::format::ROOT_AUTH_FOOTER_FIXED_LEN
6241 .checked_add(signer_identity_len)
6242 .and_then(|value| value.checked_add(authenticator_value_len))
6243 .and_then(|value| value.checked_add(4))
6244 .ok_or(FormatError::WriterUnsupported(
6245 "RootAuthFooterV1 length overflow",
6246 ))?;
6247 if len > READER_MAX_ROOT_AUTH_FOOTER_LEN as usize {
6248 return Err(FormatError::ReaderResourceLimitExceeded {
6249 field: "RootAuthFooterV1 length",
6250 cap: READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
6251 actual: len as u64,
6252 });
6253 }
6254 u32::try_from(len).map_err(|_| FormatError::WriterUnsupported("RootAuthFooterV1 length"))
6255}
6256
6257fn validate_root_auth_writer_config(config: RootAuthWriterConfig<'_>) -> Result<(), FormatError> {
6258 root_auth_footer_wire_length(
6259 config.signer_identity.len(),
6260 config.authenticator_value_length as usize,
6261 )?;
6262 Ok(())
6263}
6264
6265fn validate_root_auth_variable_lengths_for_writer(
6266 signer_identity_len: usize,
6267 authenticator_value_len: usize,
6268) -> Result<(), FormatError> {
6269 if signer_identity_len > READER_MAX_ROOT_AUTH_SIGNER_IDENTITY_LEN as usize {
6270 return Err(FormatError::ReaderResourceLimitExceeded {
6271 field: "RootAuthFooterV1 signer identity length",
6272 cap: READER_MAX_ROOT_AUTH_SIGNER_IDENTITY_LEN as u64,
6273 actual: signer_identity_len as u64,
6274 });
6275 }
6276 if authenticator_value_len > READER_MAX_ROOT_AUTH_AUTHENTICATOR_VALUE_LEN as usize {
6277 return Err(FormatError::ReaderResourceLimitExceeded {
6278 field: "RootAuthFooterV1 authenticator value length",
6279 cap: READER_MAX_ROOT_AUTH_AUTHENTICATOR_VALUE_LEN as u64,
6280 actual: authenticator_value_len as u64,
6281 });
6282 }
6283 Ok(())
6284}
6285
6286#[allow(clippy::too_many_arguments)]
6287fn build_manifest_footer(
6288 subkeys: &Subkeys,
6289 aead_algo: AeadAlgo,
6290 volume_format_rev: u16,
6291 archive_uuid: [u8; 16],
6292 session_id: [u8; 16],
6293 volume_index: u32,
6294 total_volumes: u32,
6295 index_root_extent: &ObjectExtent,
6296 index_root_decompressed_size: usize,
6297) -> Result<[u8; MANIFEST_FOOTER_LEN], FormatError> {
6298 let mut footer = ManifestFooter {
6299 archive_uuid,
6300 session_id,
6301 volume_index,
6302 is_authoritative: 1,
6303 total_volumes,
6304 index_root_first_block: index_root_extent.first_block_index,
6305 index_root_data_block_count: index_root_extent.data_block_count,
6306 index_root_parity_block_count: index_root_extent.parity_block_count,
6307 index_root_encrypted_size: index_root_extent.encrypted_size,
6308 index_root_decompressed_size: u32_len(index_root_decompressed_size, "IndexRoot")?,
6309 manifest_hmac: [0u8; 32],
6310 };
6311 let mut bytes = footer.to_bytes();
6312 footer.manifest_hmac = compute_integrity_tag(
6313 HmacDomain::ManifestFooter,
6314 aead_algo,
6315 volume_format_rev,
6316 Some(&subkeys.mac_key),
6317 &archive_uuid,
6318 &session_id,
6319 &bytes[..104],
6320 )?;
6321 bytes = footer.to_bytes();
6322 Ok(bytes)
6323}
6324
6325#[derive(Debug, Clone, Copy)]
6326struct VolumeTrailerBuildInput<'a> {
6327 subkeys: &'a Subkeys,
6328 aead_algo: AeadAlgo,
6329 volume_format_rev: u16,
6330 archive_uuid: [u8; 16],
6331 session_id: [u8; 16],
6332 volume_index: u32,
6333 block_count: u64,
6334 bytes_written: u64,
6335 manifest_footer_offset: u64,
6336 closed_at_ns: i64,
6337 root_auth_footer: Option<(u64, u32)>,
6338}
6339
6340fn build_volume_trailer(
6341 input: VolumeTrailerBuildInput<'_>,
6342) -> Result<[u8; VOLUME_TRAILER_LEN], FormatError> {
6343 let (root_auth_footer_offset, root_auth_footer_length, root_auth_flags) =
6344 match input.root_auth_footer {
6345 Some((offset, length)) => (offset, length, 0x0000_0001),
6346 None => (0, 0, 0),
6347 };
6348 let mut trailer = VolumeTrailer {
6349 archive_uuid: input.archive_uuid,
6350 session_id: input.session_id,
6351 volume_index: input.volume_index,
6352 block_count: input.block_count,
6353 bytes_written: input.bytes_written,
6354 manifest_footer_offset: input.manifest_footer_offset,
6355 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
6356 closed_at_ns: input.closed_at_ns,
6357 root_auth_footer_offset,
6358 root_auth_footer_length,
6359 root_auth_flags,
6360 trailer_hmac: [0u8; 32],
6361 };
6362 let mut bytes = trailer.to_bytes();
6363 trailer.trailer_hmac = compute_integrity_tag(
6364 HmacDomain::VolumeTrailer,
6365 input.aead_algo,
6366 input.volume_format_rev,
6367 Some(&input.subkeys.mac_key),
6368 &input.archive_uuid,
6369 &input.session_id,
6370 &bytes[..96],
6371 )?;
6372 bytes = trailer.to_bytes();
6373 Ok(bytes)
6374}
6375
6376struct BuiltCmra {
6377 bytes: Vec<u8>,
6378 shard_size: u32,
6379 data_shard_count: u16,
6380 parity_shard_count: u16,
6381 image_length: u32,
6382 image_sha256: [u8; 32],
6383}
6384
6385#[derive(Debug, Clone, Copy)]
6386struct CmraBuildInput<'a> {
6387 volume_format_rev: u16,
6388 volume_header_bytes: &'a [u8; VOLUME_HEADER_LEN],
6389 crypto_header: &'a [u8],
6390 block_count: u64,
6391 block_records_offset: u64,
6392 manifest_footer_offset: u64,
6393 manifest_footer: &'a [u8; MANIFEST_FOOTER_LEN],
6394 root_auth_footer_offset: Option<u64>,
6395 root_auth_footer: Option<&'a [u8]>,
6396 key_wrap_table: Option<&'a [u8]>,
6397 trailer_offset: u64,
6398 trailer: &'a [u8; VOLUME_TRAILER_LEN],
6399 cmra_offset: u64,
6400 options: WriterOptions,
6401 archive_uuid: [u8; 16],
6402 session_id: [u8; 16],
6403 volume_index: u32,
6404}
6405
6406fn build_v41_cmra(input: CmraBuildInput<'_>) -> Result<BuiltCmra, FormatError> {
6407 let block_record_len = input.options.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
6408 let crypto_end = VOLUME_HEADER_LEN as u64 + input.crypto_header.len() as u64;
6409 let block_records_offset = input.block_records_offset;
6410 let key_wrap_table = input
6411 .key_wrap_table
6412 .map(|table| {
6413 if table.is_empty() {
6414 return Err(FormatError::WriterInvariant(
6415 "KeyWrapTableV1 must include at least one recipient record",
6416 ));
6417 }
6418 let key_wrap_table_length = u32_len(table.len(), "KeyWrapTableV1 table_length")?;
6419 Ok((table, key_wrap_table_length))
6420 })
6421 .transpose()?;
6422 if input.volume_format_rev != VOLUME_FORMAT_REV_45 && key_wrap_table.is_some() {
6423 return Err(FormatError::WriterInvariant(
6424 "KeyWrapTableV1 requires volume_format_rev 45",
6425 ));
6426 }
6427 let key_wrap_table_length = key_wrap_table.map(|(_, length)| u64::from(length));
6428 let expected_block_records_offset = match key_wrap_table_length {
6429 Some(length) => checked_u64_add(crypto_end, length, "KeyWrapTableV1")?,
6430 None => crypto_end,
6431 };
6432 if block_records_offset != expected_block_records_offset {
6433 return Err(FormatError::WriterInvariant(
6434 "CMRA block records offset does not match key-wrap table layout",
6435 ));
6436 }
6437 let block_records_length = checked_u64_mul(
6438 input.block_count,
6439 block_record_len,
6440 "CMRA BlockRecord length overflow",
6441 )?;
6442 let manifest_end = input
6443 .manifest_footer_offset
6444 .checked_add(MANIFEST_FOOTER_LEN as u64)
6445 .ok_or(FormatError::WriterUnsupported("CMRA terminal overflow"))?;
6446 let root_auth_footer_length = input
6447 .root_auth_footer
6448 .map(|footer| u32_len(footer.len(), "RootAuthFooterV1"))
6449 .transpose()?;
6450 match (input.root_auth_footer_offset, root_auth_footer_length) {
6451 (Some(offset), Some(length)) => {
6452 if manifest_end != offset
6453 || offset
6454 .checked_add(length as u64)
6455 .ok_or(FormatError::WriterUnsupported("CMRA terminal overflow"))?
6456 != input.trailer_offset
6457 {
6458 return Err(FormatError::WriterInvariant(
6459 "RootAuthFooter does not sit between ManifestFooter and VolumeTrailer",
6460 ));
6461 }
6462 }
6463 (None, None) => {
6464 if manifest_end != input.trailer_offset {
6465 return Err(FormatError::WriterInvariant(
6466 "ManifestFooter does not end at VolumeTrailer",
6467 ));
6468 }
6469 }
6470 _ => {
6471 return Err(FormatError::WriterInvariant(
6472 "RootAuthFooter offset/bytes mismatch",
6473 ));
6474 }
6475 }
6476 let body_bytes_before_cmra = input
6477 .trailer_offset
6478 .checked_add(VOLUME_TRAILER_LEN as u64)
6479 .ok_or(FormatError::WriterUnsupported("CMRA terminal overflow"))?;
6480 if body_bytes_before_cmra != input.cmra_offset {
6481 return Err(FormatError::WriterInvariant(
6482 "CMRA does not start after VolumeTrailer",
6483 ));
6484 }
6485
6486 let mut regions = vec![
6487 SerializedRegion {
6488 region_type: 1,
6489 offset: 0,
6490 bytes: input.volume_header_bytes.to_vec(),
6491 },
6492 SerializedRegion {
6493 region_type: 2,
6494 offset: VOLUME_HEADER_LEN as u64,
6495 bytes: input.crypto_header.to_vec(),
6496 },
6497 ];
6498 if let Some((table, _)) = key_wrap_table {
6499 regions.push(SerializedRegion {
6500 region_type: 6,
6501 offset: crypto_end,
6502 bytes: table.to_vec(),
6503 });
6504 }
6505 regions.push(SerializedRegion {
6506 region_type: 3,
6507 offset: input.manifest_footer_offset,
6508 bytes: input.manifest_footer.to_vec(),
6509 });
6510 if let (Some(offset), Some(footer)) = (input.root_auth_footer_offset, input.root_auth_footer) {
6511 regions.push(SerializedRegion {
6512 region_type: 4,
6513 offset,
6514 bytes: footer.to_vec(),
6515 });
6516 }
6517 regions.push(SerializedRegion {
6518 region_type: 5,
6519 offset: input.trailer_offset,
6520 bytes: input.trailer.to_vec(),
6521 });
6522 let root_auth_flag = if input.root_auth_footer.is_some() {
6523 0x0000_0001
6524 } else {
6525 0
6526 };
6527 let key_wrap_flag = if key_wrap_table.is_some() {
6528 0x0000_0002
6529 } else {
6530 0
6531 };
6532 let image = CriticalMetadataImage {
6533 volume_format_rev: input.volume_format_rev,
6534 archive_uuid: input.archive_uuid,
6535 session_id: input.session_id,
6536 volume_index: input.volume_index,
6537 stripe_width: input.options.stripe_width,
6538 layout_flags: root_auth_flag | key_wrap_flag,
6539 volume_header_offset: 0,
6540 volume_header_length: VOLUME_HEADER_LEN as u32,
6541 crypto_header_offset: VOLUME_HEADER_LEN as u64,
6542 crypto_header_length: u32_len(input.crypto_header.len(), "CryptoHeader")?,
6543 key_wrap_table_offset: key_wrap_table.map(|_| crypto_end).unwrap_or(0),
6544 key_wrap_table_length: key_wrap_table.map(|(_, length)| length).unwrap_or(0),
6545 block_records_offset,
6546 block_records_length,
6547 block_count: input.block_count,
6548 manifest_footer_offset: input.manifest_footer_offset,
6549 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
6550 root_auth_footer_offset: input.root_auth_footer_offset.unwrap_or(0),
6551 root_auth_footer_length: root_auth_footer_length.unwrap_or(0),
6552 volume_trailer_offset: input.trailer_offset,
6553 volume_trailer_length: VOLUME_TRAILER_LEN as u32,
6554 body_bytes_before_cmra,
6555 volume_header_sha256: sha256_bytes(input.volume_header_bytes),
6556 crypto_header_sha256: sha256_bytes(input.crypto_header),
6557 key_wrap_table_sha256: key_wrap_table
6558 .map(|(table, _)| sha256_bytes(table))
6559 .unwrap_or([0u8; 32]),
6560 manifest_footer_sha256: sha256_bytes(input.manifest_footer),
6561 root_auth_footer_sha256: input
6562 .root_auth_footer
6563 .map(sha256_bytes)
6564 .unwrap_or([0u8; 32]),
6565 volume_trailer_sha256: sha256_bytes(input.trailer),
6566 regions,
6567 };
6568 let image_bytes = image.to_bytes()?;
6569 let image_sha256 = sha256_bytes(&image_bytes);
6570 let data_shard_count = ceil_div(image_bytes.len() as u64, CMRA_SHARD_SIZE as u64)?;
6571 let data_shard_count_u16 = u16::try_from(data_shard_count)
6572 .map_err(|_| FormatError::WriterUnsupported("CMRA data shard count"))?;
6573 let parity_lower = cmra_min_parity_shards(data_shard_count, input.options.bit_rot_buffer_pct)?;
6574 let parity_upper = cmra_min_parity_shards(data_shard_count, READER_MAX_CMRA_PARITY_PCT as u8)?;
6575 if parity_lower > parity_upper {
6576 return Err(FormatError::WriterUnsupported("CMRA parity bounds"));
6577 }
6578 let parity_shard_count_u16 = u16::try_from(parity_lower)
6579 .map_err(|_| FormatError::WriterUnsupported("CMRA parity shard count"))?;
6580
6581 let mut data_shards = Vec::with_capacity(data_shard_count as usize);
6582 for idx in 0..data_shard_count as usize {
6583 let start = idx * CMRA_SHARD_SIZE;
6584 let end = (start + CMRA_SHARD_SIZE).min(image_bytes.len());
6585 let mut shard = vec![0u8; CMRA_SHARD_SIZE];
6586 if start < image_bytes.len() {
6587 shard[..end - start].copy_from_slice(&image_bytes[start..end]);
6588 }
6589 data_shards.push(shard);
6590 }
6591 let parity_shards = encode_parity_gf16(&data_shards, parity_shard_count_u16 as usize)?;
6592
6593 let header = CriticalMetadataRecoveryHeader {
6594 shard_size: CMRA_SHARD_SIZE as u32,
6595 data_shard_count: data_shard_count_u16,
6596 parity_shard_count: parity_shard_count_u16,
6597 image_length: u32_len(image_bytes.len(), "CriticalMetadataImageV1")?,
6598 archive_uuid_hint: input.archive_uuid,
6599 session_id_hint: input.session_id,
6600 volume_index_hint: input.volume_index,
6601 image_sha256,
6602 header_crc32c: 0,
6603 };
6604 let mut cmra = Vec::new();
6605 cmra.extend_from_slice(&header.to_bytes());
6606 for (idx, payload) in data_shards.into_iter().enumerate() {
6607 let payload_len = if idx + 1 == data_shard_count as usize {
6608 let final_len = image_bytes.len() - idx * CMRA_SHARD_SIZE;
6609 if final_len == 0 {
6610 CMRA_SHARD_SIZE
6611 } else {
6612 final_len
6613 }
6614 } else {
6615 CMRA_SHARD_SIZE
6616 };
6617 cmra.extend_from_slice(
6618 &CriticalMetadataRecoveryShard {
6619 shard_index: u16::try_from(idx)
6620 .map_err(|_| FormatError::WriterUnsupported("CMRA shard index"))?,
6621 shard_role: 0,
6622 shard_payload_length: u32_len(payload_len, "CMRA shard payload")?,
6623 payload,
6624 shard_crc32c: 0,
6625 }
6626 .to_bytes(CMRA_SHARD_SIZE)?,
6627 );
6628 }
6629 for (idx, payload) in parity_shards.into_iter().enumerate() {
6630 let shard_index = data_shard_count
6631 .checked_add(idx as u64)
6632 .ok_or(FormatError::WriterUnsupported("CMRA shard index overflow"))?;
6633 cmra.extend_from_slice(
6634 &CriticalMetadataRecoveryShard {
6635 shard_index: u16::try_from(shard_index)
6636 .map_err(|_| FormatError::WriterUnsupported("CMRA shard index"))?,
6637 shard_role: 1,
6638 shard_payload_length: CMRA_SHARD_SIZE as u32,
6639 payload,
6640 shard_crc32c: 0,
6641 }
6642 .to_bytes(CMRA_SHARD_SIZE)?,
6643 );
6644 }
6645
6646 Ok(BuiltCmra {
6647 bytes: cmra,
6648 shard_size: CMRA_SHARD_SIZE as u32,
6649 data_shard_count: data_shard_count_u16,
6650 parity_shard_count: parity_shard_count_u16,
6651 image_length: u32_len(image_bytes.len(), "CriticalMetadataImageV1")?,
6652 image_sha256,
6653 })
6654}
6655
6656fn cmra_min_parity_shards(data_shard_count: u64, pct: u8) -> Result<u64, FormatError> {
6657 let by_pct = ceil_div(
6658 checked_u64_mul(data_shard_count, pct as u64, "CMRA parity overflow")?,
6659 100,
6660 )?;
6661 Ok(2u64.max(by_pct))
6662}
6663
6664fn compute_object_parity(
6665 data_block_count: u64,
6666 options: WriterOptions,
6667 class_parity_shard_max: u32,
6668) -> Result<u32, FormatError> {
6669 let computed = compute_parity(data_block_count, options)?;
6670 if computed > class_parity_shard_max {
6671 return Err(FormatError::WriterUnsupported(
6672 "encrypted object exceeds its parity shard class maximum",
6673 ));
6674 }
6675 Ok(computed)
6676}
6677
6678fn validate_object_shard_total(
6679 data_block_count: u32,
6680 parity_block_count: u32,
6681) -> Result<(), FormatError> {
6682 let total = checked_u64_add(
6683 data_block_count as u64,
6684 parity_block_count as u64,
6685 "encrypted object shard total overflow",
6686 )?;
6687 if total > MAX_REED_SOLOMON_GF16_SHARDS {
6688 return Err(FormatError::WriterUnsupported(
6689 "encrypted object exceeds ReedSolomonGF16 shard limit",
6690 ));
6691 }
6692 Ok(())
6693}
6694
6695fn compute_parity_u16(
6696 data_block_count: u64,
6697 options: WriterOptions,
6698 field: &'static str,
6699) -> Result<u16, FormatError> {
6700 let parity = compute_parity(data_block_count, options)?;
6701 u16::try_from(parity).map_err(|_| FormatError::WriterUnsupported(field))
6702}
6703
6704fn compute_parity(data_block_count: u64, options: WriterOptions) -> Result<u32, FormatError> {
6705 let min_parity = if options.volume_loss_tolerance > 0 || options.bit_rot_buffer_pct > 0 {
6706 1u64
6707 } else {
6708 0u64
6709 };
6710 let mut parity = 0u64;
6711 for _ in 0..100 {
6712 let total = data_block_count
6713 .checked_add(parity)
6714 .ok_or(FormatError::WriterUnsupported("parity total overflow"))?;
6715 let by_volume = checked_u64_mul(
6716 options.volume_loss_tolerance as u64,
6717 ceil_div(total, options.stripe_width as u64)?,
6718 "volume-loss parity overflow",
6719 )?;
6720 let by_bitrot = ceil_div(
6721 checked_u64_mul(
6722 total,
6723 options.bit_rot_buffer_pct as u64,
6724 "bit-rot parity overflow",
6725 )?,
6726 100,
6727 )?;
6728 let next = by_volume
6729 .checked_add(by_bitrot)
6730 .ok_or(FormatError::WriterUnsupported("parity overflow"))?
6731 .max(min_parity);
6732 if next == parity {
6733 return u32::try_from(next).map_err(|_| FormatError::WriterUnsupported("parity count"));
6734 }
6735 parity = next;
6736 }
6737 Err(FormatError::WriterUnsupported(
6738 "parity calculation did not converge",
6739 ))
6740}
6741
6742fn ceil_div(numerator: u64, denominator: u64) -> Result<u64, FormatError> {
6743 if denominator == 0 {
6744 return Err(FormatError::WriterUnsupported("division by zero"));
6745 }
6746 numerator
6747 .checked_add(denominator - 1)
6748 .ok_or(FormatError::WriterUnsupported("ceiling division overflow"))
6749 .map(|value| value / denominator)
6750}
6751
6752fn checked_u64_mul(lhs: u64, rhs: u64, field: &'static str) -> Result<u64, FormatError> {
6753 lhs.checked_mul(rhs)
6754 .ok_or(FormatError::WriterUnsupported(field))
6755}
6756
6757#[allow(clippy::too_many_arguments)]
6758fn build_bootstrap_sidecar(
6759 subkeys: &Subkeys,
6760 aead_algo: AeadAlgo,
6761 volume_format_rev: u16,
6762 archive_uuid: [u8; 16],
6763 session_id: [u8; 16],
6764 manifest_footer: &[u8; MANIFEST_FOOTER_LEN],
6765 index_root_records: &[BlockRecord],
6766 dictionary_records: Option<&[BlockRecord]>,
6767) -> Result<Vec<u8>, FormatError> {
6768 let index_records_len = index_root_records.iter().try_fold(0usize, |sum, record| {
6769 checked_usize_add(sum, record.to_bytes().len(), "bootstrap sidecar")
6770 })?;
6771 let dictionary_records_len = dictionary_records
6772 .unwrap_or(&[])
6773 .iter()
6774 .try_fold(0usize, |sum, record| {
6775 checked_usize_add(sum, record.to_bytes().len(), "bootstrap sidecar")
6776 })?;
6777 let manifest_offset = BOOTSTRAP_SIDECAR_HEADER_LEN as u64;
6778 let index_root_offset = manifest_offset + MANIFEST_FOOTER_LEN as u64;
6779 let dictionary_offset = if dictionary_records.is_some() {
6780 index_root_offset + index_records_len as u64
6781 } else {
6782 0
6783 };
6784 let mut header = BootstrapSidecarHeader {
6785 archive_uuid,
6786 session_id,
6787 flags: if dictionary_records.is_some() {
6788 0x07
6789 } else {
6790 0x03
6791 },
6792 manifest_footer_offset: manifest_offset,
6793 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
6794 index_root_records_offset: index_root_offset,
6795 index_root_records_length: index_records_len as u64,
6796 dictionary_records_offset: dictionary_offset,
6797 dictionary_records_length: dictionary_records_len as u64,
6798 sidecar_hmac: [0u8; 32],
6799 header_crc32c: 0,
6800 };
6801 let mut header_bytes = header.to_bytes();
6802 header.sidecar_hmac = compute_integrity_tag(
6803 HmacDomain::BootstrapSidecar,
6804 aead_algo,
6805 volume_format_rev,
6806 Some(&subkeys.mac_key),
6807 &archive_uuid,
6808 &session_id,
6809 &header_bytes[..92],
6810 )?;
6811 header_bytes = header.to_bytes();
6812
6813 let mut sidecar = Vec::with_capacity(
6814 BOOTSTRAP_SIDECAR_HEADER_LEN
6815 + MANIFEST_FOOTER_LEN
6816 + index_records_len
6817 + dictionary_records_len,
6818 );
6819 sidecar.extend_from_slice(&header_bytes);
6820 sidecar.extend_from_slice(manifest_footer);
6821 for record in index_root_records {
6822 sidecar.extend_from_slice(&record.to_bytes());
6823 }
6824 if let Some(dictionary_records) = dictionary_records {
6825 for record in dictionary_records {
6826 sidecar.extend_from_slice(&record.to_bytes());
6827 }
6828 }
6829 Ok(sidecar)
6830}
6831
6832struct StreamingMemberReader<'a> {
6833 sections: Vec<StreamingMemberSection<'a>>,
6834 section_index: usize,
6835 pushback: Vec<u8>,
6836}
6837
6838struct StreamingMemberSection<'a> {
6839 reader: Option<Box<dyn Read + 'a>>,
6840 opener: Option<SectionOpener<'a>>,
6841 remaining: u64,
6842 remaining_padding: u64,
6843 expected_sha256: Option<[u8; 32]>,
6844 hasher: Sha256,
6845 source_eof_checked: bool,
6846}
6847
6848type SectionOpener<'a> = Box<dyn FnOnce() -> Result<Box<dyn Read + 'a>, ArchiveWriteError> + 'a>;
6849
6850impl<'a> StreamingMemberSection<'a> {
6851 fn bytes(bytes: Vec<u8>) -> Self {
6852 let size = bytes.len() as u64;
6853 Self {
6854 reader: Some(Box::new(Cursor::new(bytes))),
6855 opener: None,
6856 remaining: size,
6857 remaining_padding: 0,
6858 expected_sha256: None,
6859 hasher: Sha256::new(),
6860 source_eof_checked: false,
6861 }
6862 }
6863
6864 fn payload(reader: Box<dyn Read + 'a>, size: u64, expected_sha256: Option<[u8; 32]>) -> Self {
6865 Self {
6866 reader: Some(reader),
6867 opener: None,
6868 remaining: size,
6869 remaining_padding: padding_to_512_u64(size),
6870 expected_sha256,
6871 hasher: Sha256::new(),
6872 source_eof_checked: false,
6873 }
6874 }
6875
6876 fn deferred_payload(
6877 opener: SectionOpener<'a>,
6878 size: u64,
6879 expected_sha256: Option<[u8; 32]>,
6880 ) -> Self {
6881 Self {
6882 reader: None,
6883 opener: Some(opener),
6884 remaining: size,
6885 remaining_padding: padding_to_512_u64(size),
6886 expected_sha256,
6887 hasher: Sha256::new(),
6888 source_eof_checked: false,
6889 }
6890 }
6891
6892 fn reader(&mut self) -> std::io::Result<&mut Box<dyn Read + 'a>> {
6893 if self.reader.is_none() {
6894 let opener = self
6895 .opener
6896 .take()
6897 .ok_or_else(|| std::io::Error::other("streaming member section has no source"))?;
6898 self.reader = Some(opener().map_err(|error| std::io::Error::other(error.to_string()))?);
6899 }
6900 Ok(self.reader.as_mut().expect("reader was initialized"))
6901 }
6902}
6903
6904impl Read for StreamingMemberSection<'_> {
6905 fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
6906 if out.is_empty() {
6907 return Ok(0);
6908 }
6909 if self.remaining > 0 {
6910 let limit = out
6911 .len()
6912 .min(usize::try_from(self.remaining).unwrap_or(usize::MAX));
6913 let count = self.reader()?.read(&mut out[..limit])?;
6914 if count == 0 {
6915 return Err(std::io::Error::new(
6916 std::io::ErrorKind::UnexpectedEof,
6917 "member source ended before its declared size",
6918 ));
6919 }
6920 self.remaining -= count as u64;
6921 if self.expected_sha256.is_some() {
6922 self.hasher.update(&out[..count]);
6923 }
6924 return Ok(count);
6925 }
6926 if !self.source_eof_checked {
6927 let mut extra = [0u8; 1];
6928 if self.reader()?.read(&mut extra)? != 0 {
6929 return Err(std::io::Error::new(
6930 std::io::ErrorKind::InvalidData,
6931 "member source exceeded its declared size",
6932 ));
6933 }
6934 if let Some(expected) = self.expected_sha256 {
6935 let actual: [u8; 32] = self.hasher.clone().finalize().into();
6936 if actual != expected {
6937 return Err(std::io::Error::new(
6938 std::io::ErrorKind::InvalidData,
6939 "auxiliary source changed after metadata capture",
6940 ));
6941 }
6942 }
6943 self.source_eof_checked = true;
6944 self.reader = None;
6945 }
6946 if self.remaining_padding > 0 {
6947 let count = out
6948 .len()
6949 .min(usize::try_from(self.remaining_padding).unwrap_or(usize::MAX));
6950 out[..count].fill(0);
6951 self.remaining_padding -= count as u64;
6952 return Ok(count);
6953 }
6954 Ok(0)
6955 }
6956}
6957
6958impl<'a> StreamingMemberReader<'a> {
6959 fn new(file: Box<dyn Read + 'a>, prefix: Vec<u8>, file_size: u64) -> Self {
6960 Self {
6961 sections: vec![
6962 StreamingMemberSection::bytes(prefix),
6963 StreamingMemberSection::payload(file, file_size, None),
6964 ],
6965 section_index: 0,
6966 pushback: Vec::new(),
6967 }
6968 }
6969
6970 fn from_source<S: RegularFileSource + ?Sized>(
6971 source: &'a S,
6972 metadata: &PortableFileMetadata,
6973 layout: PrimaryMemberLayout,
6974 primary_size: u64,
6975 ) -> Result<Self, ArchiveWriteError> {
6976 let mut sections = Vec::with_capacity(layout.auxiliary.len() * 2 + 2);
6977 for (ordinal, auxiliary) in layout.auxiliary.into_iter().enumerate() {
6978 sections.push(StreamingMemberSection::bytes(auxiliary.bytes));
6979 let record = metadata.native.auxiliary_records.get(ordinal).ok_or(
6980 FormatError::WriterInvariant("planned auxiliary source ordinal is missing"),
6981 )?;
6982 if record.is_streamed() {
6983 sections.push(StreamingMemberSection::deferred_payload(
6984 Box::new(move || source.open_auxiliary(ordinal)),
6985 auxiliary.stored_size,
6986 Some(auxiliary.sha256),
6987 ));
6988 } else {
6989 sections.push(StreamingMemberSection::payload(
6990 Box::new(Cursor::new(record.payload.clone())),
6991 auxiliary.stored_size,
6992 Some(auxiliary.sha256),
6993 ));
6994 }
6995 if record.stored_payload_size() != auxiliary.stored_size {
6996 return Err(FormatError::WriterInvariant(
6997 "auxiliary source declaration changed while opening",
6998 )
6999 .into());
7000 }
7001 }
7002 sections.push(StreamingMemberSection::bytes(layout.primary));
7003 sections.push(StreamingMemberSection::deferred_payload(
7004 Box::new(move || source.open()),
7005 primary_size,
7006 None,
7007 ));
7008 Ok(Self {
7009 sections,
7010 section_index: 0,
7011 pushback: Vec::new(),
7012 })
7013 }
7014
7015 fn push_back(&mut self, bytes: Vec<u8>) {
7016 if bytes.is_empty() {
7017 return;
7018 }
7019 if self.pushback.is_empty() {
7020 self.pushback = bytes;
7021 } else {
7022 let mut merged = bytes;
7023 merged.extend_from_slice(&self.pushback);
7024 self.pushback = merged;
7025 }
7026 }
7027}
7028
7029impl Read for StreamingMemberReader<'_> {
7030 fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
7031 if out.is_empty() {
7032 return Ok(0);
7033 }
7034
7035 let mut written = 0usize;
7036 if !self.pushback.is_empty() {
7037 let count = out.len().min(self.pushback.len());
7038 out[..count].copy_from_slice(&self.pushback[..count]);
7039 self.pushback.drain(..count);
7040 written += count;
7041 if written == out.len() {
7042 return Ok(written);
7043 }
7044 }
7045
7046 while written < out.len() && self.section_index < self.sections.len() {
7047 let count = self.sections[self.section_index].read(&mut out[written..])?;
7048 if count == 0 {
7049 self.section_index += 1;
7050 } else {
7051 written += count;
7052 }
7053 }
7054
7055 Ok(written)
7056 }
7057}
7058
7059#[cfg(test)]
7060fn build_regular_file_member_prefix(
7061 path: &[u8],
7062 file_size: u64,
7063 mode: u32,
7064 mtime: ArchiveTimestamp,
7065 portable_metadata: &PortableFileMetadata,
7066) -> Result<Vec<u8>, FormatError> {
7067 build_primary_member_prefix(
7068 path,
7069 SourceEntryKind::Regular,
7070 None,
7071 file_size,
7072 None,
7073 mode,
7074 mtime,
7075 portable_metadata,
7076 )
7077}
7078
7079#[allow(clippy::too_many_arguments)]
7080fn build_primary_member_prefix(
7081 path: &[u8],
7082 entry_kind: SourceEntryKind,
7083 link_target: Option<&[u8]>,
7084 file_size: u64,
7085 sparse_extents: Option<&[SparseExtent]>,
7086 mode: u32,
7087 mtime: ArchiveTimestamp,
7088 portable_metadata: &PortableFileMetadata,
7089) -> Result<Vec<u8>, FormatError> {
7090 let layout = build_primary_member_layout(
7091 path,
7092 entry_kind,
7093 link_target,
7094 file_size,
7095 sparse_extents,
7096 mode,
7097 mtime,
7098 portable_metadata,
7099 )?;
7100 let mut out = Vec::new();
7101 for (ordinal, auxiliary) in layout.auxiliary.iter().enumerate() {
7102 let record = &portable_metadata.native.auxiliary_records[ordinal];
7103 if record.is_streamed() {
7104 return Err(FormatError::WriterUnsupported(
7105 "this writer path does not accept streamed auxiliary payloads",
7106 ));
7107 }
7108 out.extend_from_slice(&auxiliary.bytes);
7109 out.extend_from_slice(&record.payload);
7110 out.resize(
7111 out.len() + padding_to_512_u64(auxiliary.stored_size) as usize,
7112 0,
7113 );
7114 }
7115 out.extend_from_slice(&layout.primary);
7116 Ok(out)
7117}
7118
7119struct PrimaryMemberLayout {
7120 auxiliary: Vec<NativeAuxiliaryMemberPrefix>,
7121 primary: Vec<u8>,
7122}
7123
7124fn primary_member_layout_size(
7125 layout: &PrimaryMemberLayout,
7126 primary_payload_size: u64,
7127) -> Result<u64, FormatError> {
7128 let mut size = 0u64;
7129 for auxiliary in &layout.auxiliary {
7130 size = checked_u64_add(size, auxiliary.bytes.len() as u64, "auxiliary member")?;
7131 size = checked_u64_add(size, auxiliary.stored_size, "auxiliary member")?;
7132 size = checked_u64_add(
7133 size,
7134 padding_to_512_u64(auxiliary.stored_size),
7135 "auxiliary member",
7136 )?;
7137 }
7138 size = checked_u64_add(size, layout.primary.len() as u64, "primary member")?;
7139 size = checked_u64_add(size, primary_payload_size, "primary member")?;
7140 checked_u64_add(
7141 size,
7142 padding_to_512_u64(primary_payload_size),
7143 "primary member",
7144 )
7145}
7146
7147#[allow(clippy::too_many_arguments)]
7148fn build_primary_member_layout(
7149 path: &[u8],
7150 entry_kind: SourceEntryKind,
7151 link_target: Option<&[u8]>,
7152 file_size: u64,
7153 sparse_extents: Option<&[SparseExtent]>,
7154 mode: u32,
7155 mtime: ArchiveTimestamp,
7156 portable_metadata: &PortableFileMetadata,
7157) -> Result<PrimaryMemberLayout, FormatError> {
7158 if entry_kind != SourceEntryKind::Regular && (file_size != 0 || sparse_extents.is_some()) {
7159 return Err(FormatError::WriterInvariant(
7160 "non-regular member has non-zero file data size",
7161 ));
7162 }
7163 match (entry_kind, link_target) {
7164 (SourceEntryKind::Symlink, Some(target)) => {
7165 crate::tar_model::validate_symlink_target(path, target)?;
7166 }
7167 (SourceEntryKind::Hardlink, Some(target)) => {
7168 validate_file_path_bytes(target, u32::MAX)?;
7169 if target == path {
7170 return Err(FormatError::WriterInvariant("hardlink aliases itself"));
7171 }
7172 if portable_metadata.native != NativeFileMetadata::default() {
7173 return Err(FormatError::WriterInvariant(
7174 "hardlink alias carries native file-object metadata",
7175 ));
7176 }
7177 }
7178 (SourceEntryKind::Symlink, None) => {
7179 return Err(FormatError::WriterInvariant(
7180 "symlink member has no link target",
7181 ));
7182 }
7183 (_, Some(_)) => {
7184 return Err(FormatError::WriterInvariant(
7185 "non-link member has a link target",
7186 ));
7187 }
7188 (_, None) => {}
7189 }
7190 let sparse_map = sparse_extents
7191 .map(|extents| encode_v45_sparse_map(extents, file_size))
7192 .transpose()?;
7193 let stored_extent_bytes = sparse_extents
7194 .map(|extents| sparse_extent_bytes(extents, file_size))
7195 .transpose()?
7196 .unwrap_or(file_size);
7197 let stored_size = checked_u64_add(
7198 sparse_map.as_ref().map_or(0, |map| map.len() as u64),
7199 stored_extent_bytes,
7200 "sparse primary stored size",
7201 )?;
7202 let use_path_override = sparse_map.is_none() && path_requires_pax(path);
7203 validate_portable_file_metadata(portable_metadata)?;
7204 let reparse_placeholder = portable_metadata
7205 .native
7206 .primary_pax_records
7207 .contains_key("TZAP.windows.reparse-placeholder");
7208 if matches!(
7209 entry_kind,
7210 SourceEntryKind::ReparseDirectory | SourceEntryKind::ReparseRegular
7211 ) != reparse_placeholder
7212 {
7213 return Err(FormatError::WriterInvariant(
7214 "Windows reparse source kind and placeholder metadata disagree",
7215 ));
7216 }
7217 let mut pax_records =
7218 portable_primary_pax(path, mode, &portable_metadata.source_os, use_path_override)?;
7219 pax_records.insert(
7220 "TZAP.metadata.source-filesystem".into(),
7221 portable_metadata.source_filesystem.as_bytes().to_vec(),
7222 );
7223 pax_records.insert(
7224 "TZAP.portable.mode-origin".into(),
7225 match portable_metadata.mode_origin {
7226 PortableModeOrigin::Native => b"native".to_vec(),
7227 PortableModeOrigin::Projected => b"projected".to_vec(),
7228 },
7229 );
7230 if let Some(attributes) = portable_metadata.attributes {
7231 pax_records.insert(
7232 "TZAP.portable.attributes".into(),
7233 hex::encode(attributes.to_be_bytes()).into_bytes(),
7234 );
7235 }
7236 if sparse_map.is_some() {
7237 pax_records.insert("GNU.sparse.major".into(), b"1".to_vec());
7238 pax_records.insert("GNU.sparse.minor".into(), b"0".to_vec());
7239 pax_records.insert("GNU.sparse.name".into(), path.to_vec());
7240 pax_records.insert(
7241 "GNU.sparse.realsize".into(),
7242 file_size.to_string().into_bytes(),
7243 );
7244 }
7245 merge_native_primary_metadata(&mut pax_records, &portable_metadata.native)?;
7246 if portable_metadata
7247 .native
7248 .auxiliary_records
7249 .iter()
7250 .any(|record| record.kind == CAPTURE_REPORT_KIND)
7251 {
7252 pax_records.insert("TZAP.metadata.capture-status".into(), b"partial".to_vec());
7253 }
7254 let use_linkpath_override = link_target.is_some_and(|target| target.len() > 100);
7255 if use_linkpath_override {
7256 pax_records.insert(
7257 "linkpath".into(),
7258 link_target.expect("link target was checked").to_vec(),
7259 );
7260 }
7261 let primary_identity = prepare_primary_tar_identity(&mut pax_records, portable_metadata)?;
7262 let header_mtime = if mtime.nanoseconds == 0 && mtime.seconds >= 0 {
7263 let seconds = mtime.seconds as u64;
7264 if tar_octal_fits(12, seconds) {
7265 seconds
7266 } else {
7267 pax_records.insert("mtime".into(), mtime.canonical_pax_value()?);
7268 0
7269 }
7270 } else {
7271 pax_records.insert("mtime".into(), mtime.canonical_pax_value()?);
7272 0
7273 };
7274 let header_size = if tar_octal_fits(12, stored_size) {
7275 stored_size
7276 } else {
7277 pax_records.insert("size".into(), stored_size.to_string().into_bytes());
7278 0
7279 };
7280 let primary_metadata = parse_primary_metadata(&pax_records).map_err(|_| {
7281 FormatError::WriterUnsupported("native primary metadata is not a valid v45 declaration")
7282 })?;
7283 let mut parsed_auxiliary = Vec::with_capacity(portable_metadata.native.auxiliary_records.len());
7284 let mut auxiliary = Vec::with_capacity(portable_metadata.native.auxiliary_records.len());
7285 for (ordinal, record) in portable_metadata
7286 .native
7287 .auxiliary_records
7288 .iter()
7289 .enumerate()
7290 {
7291 let ordinal = u32::try_from(ordinal).map_err(|_| {
7292 FormatError::WriterUnsupported("auxiliary record count exceeds revision-45 range")
7293 })?;
7294 let member = build_native_auxiliary_member_prefix(ordinal, record)?;
7295 parsed_auxiliary.push(member.parsed.clone());
7296 auxiliary.push(member);
7297 }
7298 validate_group_metadata(&primary_metadata, &parsed_auxiliary).map_err(|_| {
7299 FormatError::WriterUnsupported("native auxiliary metadata is not a valid v45 declaration")
7300 })?;
7301 let pax_payload = encode_canonical_pax(&pax_records)?;
7302 let pax_header = build_ustar_header(b"TZAP-PAX/PRIMARY", pax_payload.len() as u64, 0, 0, b'x')?;
7303 let mut primary = Vec::new();
7304 primary.extend_from_slice(&pax_header);
7305 primary.extend_from_slice(&pax_payload);
7306 primary.resize(primary.len() + padding_to_512(pax_payload.len()), 0);
7307
7308 let header_path = if sparse_map.is_some() {
7309 b"GNUSparseFile.0/TZAP".to_vec()
7310 } else if use_path_override {
7311 b"TZAP-PRIMARY".to_vec()
7312 } else {
7313 path.to_vec()
7314 };
7315
7316 let typeflag = match entry_kind {
7317 SourceEntryKind::Regular | SourceEntryKind::ReparseRegular => b'0',
7318 SourceEntryKind::Directory | SourceEntryKind::ReparseDirectory => b'5',
7319 SourceEntryKind::Symlink => b'2',
7320 SourceEntryKind::Hardlink => b'1',
7321 SourceEntryKind::CharacterDevice => b'3',
7322 SourceEntryKind::BlockDevice => b'4',
7323 SourceEntryKind::Fifo => b'6',
7324 };
7325 let mut header = build_ustar_header(&header_path, header_size, mode, header_mtime, typeflag)?;
7326 if let Some(target) = link_target.filter(|_| !use_linkpath_override) {
7327 header[157..157 + target.len()].copy_from_slice(target);
7328 finalize_tar_checksum(&mut header)?;
7329 }
7330 apply_primary_tar_identity(&mut header, &primary_identity)?;
7331 if matches!(
7332 entry_kind,
7333 SourceEntryKind::CharacterDevice | SourceEntryKind::BlockDevice
7334 ) {
7335 let parse_device = |key: &str| -> Result<u64, FormatError> {
7336 let value = pax_records.get(key).ok_or(FormatError::WriterInvariant(
7337 "device member lacks device-number metadata",
7338 ))?;
7339 let text = std::str::from_utf8(value).map_err(|_| {
7340 FormatError::WriterUnsupported("device number is not canonical decimal")
7341 })?;
7342 text.parse::<u64>().map_err(|_| {
7343 FormatError::WriterUnsupported("device number is not canonical decimal")
7344 })
7345 };
7346 let major = parse_device("TZAP.posix.device-major")?;
7347 let minor = parse_device("TZAP.posix.device-minor")?;
7348 if tar_octal_fits(8, major) {
7349 write_tar_octal(&mut header[329..337], major)?;
7350 }
7351 if tar_octal_fits(8, minor) {
7352 write_tar_octal(&mut header[337..345], minor)?;
7353 }
7354 finalize_tar_checksum(&mut header)?;
7355 }
7356 primary.extend_from_slice(&header);
7357 if let Some(sparse_map) = sparse_map {
7358 primary.extend_from_slice(&sparse_map);
7359 }
7360 Ok(PrimaryMemberLayout { auxiliary, primary })
7361}
7362
7363fn sparse_extent_bytes(extents: &[SparseExtent], logical_size: u64) -> Result<u64, FormatError> {
7364 if extents.len() > MAX_SPARSE_EXTENTS {
7365 return Err(FormatError::WriterUnsupported(
7366 "sparse extent count exceeds revision-45 limit",
7367 ));
7368 }
7369 let mut previous_end = 0u64;
7370 let mut stored_size = 0u64;
7371 for (index, extent) in extents.iter().enumerate() {
7372 if extent.length == 0 || extent.offset < previous_end {
7373 return Err(FormatError::WriterUnsupported(
7374 "sparse extents overlap or have zero length",
7375 ));
7376 }
7377 if index != 0 && extent.offset == previous_end {
7378 return Err(FormatError::WriterUnsupported(
7379 "adjacent sparse extents must be merged",
7380 ));
7381 }
7382 let end = extent
7383 .offset
7384 .checked_add(extent.length)
7385 .ok_or(FormatError::WriterUnsupported("sparse extent overflow"))?;
7386 if end > logical_size {
7387 return Err(FormatError::WriterUnsupported(
7388 "sparse extent exceeds logical size",
7389 ));
7390 }
7391 stored_size =
7392 stored_size
7393 .checked_add(extent.length)
7394 .ok_or(FormatError::WriterUnsupported(
7395 "sparse stored size overflow",
7396 ))?;
7397 previous_end = end;
7398 }
7399 Ok(stored_size)
7400}
7401
7402pub fn encode_v45_sparse_map(
7403 extents: &[SparseExtent],
7404 logical_size: u64,
7405) -> Result<Vec<u8>, FormatError> {
7406 sparse_extent_bytes(extents, logical_size)?;
7407 let mut map = Vec::new();
7408 map.extend_from_slice(extents.len().to_string().as_bytes());
7409 map.push(b'\n');
7410 for extent in extents {
7411 map.extend_from_slice(extent.offset.to_string().as_bytes());
7412 map.push(b'\n');
7413 map.extend_from_slice(extent.length.to_string().as_bytes());
7414 map.push(b'\n');
7415 }
7416 let padding = padding_to_512(map.len());
7417 map.resize(
7418 map.len()
7419 .checked_add(padding)
7420 .ok_or(FormatError::WriterUnsupported("sparse map size overflow"))?,
7421 0,
7422 );
7423 Ok(map)
7424}
7425
7426struct NativeAuxiliaryMemberPrefix {
7427 bytes: Vec<u8>,
7428 stored_size: u64,
7429 sha256: [u8; 32],
7430 parsed: crate::entry_metadata::AuxiliaryRecord,
7431}
7432
7433fn build_native_auxiliary_member_prefix(
7434 ordinal: u32,
7435 record: &NativeAuxiliaryMetadata,
7436) -> Result<NativeAuxiliaryMemberPrefix, FormatError> {
7437 if record.is_streamed()
7438 && !matches!(
7439 record.kind.as_str(),
7440 "windows.alternate-data"
7441 | "windows.property-data"
7442 | "windows.efs-raw"
7443 | "macos.resource-fork"
7444 | "generic.xattr"
7445 )
7446 && !record.kind.starts_with("x.")
7447 {
7448 return Err(FormatError::WriterUnsupported(
7449 "this auxiliary kind requires inline structural payload validation",
7450 ));
7451 }
7452 let mut pax_records = BTreeMap::new();
7453 pax_records.insert("TZAP.aux.version".into(), b"1".to_vec());
7454 pax_records.insert("TZAP.aux.kind".into(), record.kind.as_bytes().to_vec());
7455 pax_records.insert(
7456 "TZAP.aux.profile".into(),
7457 record.profile.as_bytes().to_vec(),
7458 );
7459 pax_records.insert(
7460 "TZAP.aux.restore-class".into(),
7461 match record.restore_class {
7462 RestoreClass::None => b"none".to_vec(),
7463 RestoreClass::Portable => b"portable".to_vec(),
7464 RestoreClass::SameOs => b"same-os".to_vec(),
7465 RestoreClass::System => b"system".to_vec(),
7466 },
7467 );
7468 pax_records.insert(
7469 "TZAP.aux.native".into(),
7470 if record.native { b"1" } else { b"0" }.to_vec(),
7471 );
7472 let (name_encoding, encoded_name) = match record.name_encoding {
7473 NativeAuxiliaryNameEncoding::None => ("none", record.name.clone()),
7474 NativeAuxiliaryNameEncoding::Utf8 => ("utf8", record.name.clone()),
7475 NativeAuxiliaryNameEncoding::Utf16Le => {
7476 ("utf16le-base64", canonical_base64_encode(&record.name))
7477 }
7478 NativeAuxiliaryNameEncoding::Bytes => {
7479 ("bytes-base64", canonical_base64_encode(&record.name))
7480 }
7481 };
7482 pax_records.insert(
7483 "TZAP.aux.name-encoding".into(),
7484 name_encoding.as_bytes().to_vec(),
7485 );
7486 pax_records.insert("TZAP.aux.name".into(), encoded_name);
7487 pax_records.insert(
7488 "TZAP.aux.flags".into(),
7489 hex::encode(record.flags.to_be_bytes()).into_bytes(),
7490 );
7491 pax_records.insert(
7492 "TZAP.aux.logical-size".into(),
7493 record.logical_size.to_string().into_bytes(),
7494 );
7495 let digest = record.sha256();
7496 pax_records.insert("TZAP.aux.sha256".into(), hex::encode(digest).into_bytes());
7497 for (key, value) in &record.meta {
7498 if pax_records.insert(key.clone(), value.clone()).is_some() {
7499 return Err(FormatError::WriterUnsupported(
7500 "auxiliary metadata collides with a writer-owned key",
7501 ));
7502 }
7503 }
7504
7505 let stored_size = record.stored_payload_size();
7506 let header_size = if tar_octal_fits(12, stored_size) {
7507 stored_size
7508 } else {
7509 pax_records.insert("size".into(), stored_size.to_string().into_bytes());
7510 0
7511 };
7512 let parsed = if record.is_streamed() {
7513 parse_auxiliary_declaration_for_writer(&pax_records, ordinal, stored_size)
7514 } else {
7515 parse_auxiliary_record(&pax_records, ordinal, stored_size, &record.payload)
7516 }?;
7517 let pax_payload = encode_canonical_pax(&pax_records)?;
7518 let pax_label = format!("TZAP-PAX/AUX/{ordinal:08x}");
7519 let pax_header =
7520 build_ustar_header(pax_label.as_bytes(), pax_payload.len() as u64, 0, 0, b'x')?;
7521 let auxiliary_label = format!("TZAP-AUX/{ordinal:08x}");
7522 let auxiliary_header = build_ustar_header(auxiliary_label.as_bytes(), header_size, 0, 0, b'Z')?;
7523 let mut bytes = Vec::new();
7524 bytes.extend_from_slice(&pax_header);
7525 bytes.extend_from_slice(&pax_payload);
7526 bytes.resize(bytes.len() + padding_to_512(pax_payload.len()), 0);
7527 bytes.extend_from_slice(&auxiliary_header);
7528 Ok(NativeAuxiliaryMemberPrefix {
7529 bytes,
7530 stored_size,
7531 sha256: digest,
7532 parsed,
7533 })
7534}
7535
7536fn merge_native_primary_metadata(
7537 pax_records: &mut crate::entry_metadata::PaxRecords,
7538 native: &NativeFileMetadata,
7539) -> Result<(), FormatError> {
7540 if native.required_profiles.is_empty()
7541 && native.optional_profiles.is_empty()
7542 && native.primary_pax_records.is_empty()
7543 && native.auxiliary_records.is_empty()
7544 {
7545 return Ok(());
7546 }
7547 let mut required = native.required_profiles.clone();
7548 required.push("portable-v1".into());
7549 required.sort();
7550 required.dedup();
7551 let mut optional = native.optional_profiles.clone();
7552 optional.sort();
7553 optional.dedup();
7554 if required
7555 .iter()
7556 .any(|profile| optional.binary_search(profile).is_ok())
7557 {
7558 return Err(FormatError::WriterUnsupported(
7559 "metadata profile is both required and optional",
7560 ));
7561 }
7562 pax_records.insert(
7563 "TZAP.metadata.required-profiles".into(),
7564 required.join(",").into_bytes(),
7565 );
7566 pax_records.insert(
7567 "TZAP.metadata.optional-profiles".into(),
7568 optional.join(",").into_bytes(),
7569 );
7570 for (key, value) in &native.primary_pax_records {
7571 if pax_records.contains_key(key)
7572 || matches!(
7573 key.as_str(),
7574 "path" | "linkpath" | "size" | "uid" | "gid" | "uname" | "gname" | "mtime"
7575 )
7576 || key.starts_with("TZAP.metadata.")
7577 || key.starts_with("TZAP.portable.")
7578 {
7579 return Err(FormatError::WriterUnsupported(
7580 "native metadata collides with a writer-owned primary key",
7581 ));
7582 }
7583 pax_records.insert(key.clone(), value.clone());
7584 }
7585 Ok(())
7586}
7587
7588#[derive(Debug, Default)]
7589struct PrimaryTarIdentity {
7590 uid: u64,
7591 gid: u64,
7592 uname: Vec<u8>,
7593 gname: Vec<u8>,
7594}
7595
7596fn validate_portable_file_metadata(metadata: &PortableFileMetadata) -> Result<(), FormatError> {
7597 if !is_source_os(&metadata.source_os) {
7598 return Err(FormatError::WriterUnsupported("invalid metadata source OS"));
7599 }
7600 if !valid_filesystem_token(&metadata.source_filesystem) {
7601 return Err(FormatError::WriterUnsupported(
7602 "invalid metadata source filesystem",
7603 ));
7604 }
7605 if metadata
7606 .attributes
7607 .is_some_and(|attributes| attributes & !0x0f != 0)
7608 {
7609 return Err(FormatError::WriterUnsupported(
7610 "portable attributes contain reserved bits",
7611 ));
7612 }
7613 Ok(())
7614}
7615
7616fn prepare_primary_tar_identity(
7617 pax_records: &mut crate::entry_metadata::PaxRecords,
7618 metadata: &PortableFileMetadata,
7619) -> Result<PrimaryTarIdentity, FormatError> {
7620 let Some(owner) = &metadata.posix_owner else {
7621 return Ok(PrimaryTarIdentity::default());
7622 };
7623 pax_records.insert("TZAP.portable.owner-kind".into(), b"posix".to_vec());
7624 let uid = if tar_octal_fits(8, owner.uid) {
7625 owner.uid
7626 } else {
7627 pax_records.insert("uid".into(), owner.uid.to_string().into_bytes());
7628 0
7629 };
7630 let gid = if tar_octal_fits(8, owner.gid) {
7631 owner.gid
7632 } else {
7633 pax_records.insert("gid".into(), owner.gid.to_string().into_bytes());
7634 0
7635 };
7636 let uname = prepare_primary_name(pax_records, "uname", owner.uname.as_deref())?;
7637 let gname = prepare_primary_name(pax_records, "gname", owner.gname.as_deref())?;
7638 Ok(PrimaryTarIdentity {
7639 uid,
7640 gid,
7641 uname,
7642 gname,
7643 })
7644}
7645
7646fn prepare_primary_name(
7647 pax_records: &mut crate::entry_metadata::PaxRecords,
7648 key: &'static str,
7649 name: Option<&str>,
7650) -> Result<Vec<u8>, FormatError> {
7651 let Some(name) = name else {
7652 return Ok(Vec::new());
7653 };
7654 if name.is_empty() || name.contains('\0') || name.nfc().collect::<String>() != name {
7655 return Err(FormatError::WriterUnsupported(
7656 "portable owner name is not canonical NFC UTF-8",
7657 ));
7658 }
7659 if name.len() <= 32 {
7660 Ok(name.as_bytes().to_vec())
7661 } else {
7662 pax_records.insert(key.into(), name.as_bytes().to_vec());
7663 Ok(Vec::new())
7664 }
7665}
7666
7667fn apply_primary_tar_identity(
7668 header: &mut [u8; TAR_BLOCK_LEN],
7669 identity: &PrimaryTarIdentity,
7670) -> Result<(), FormatError> {
7671 write_tar_octal(&mut header[108..116], identity.uid)?;
7672 write_tar_octal(&mut header[116..124], identity.gid)?;
7673 header[265..297].fill(0);
7674 header[265..265 + identity.uname.len()].copy_from_slice(&identity.uname);
7675 header[297..329].fill(0);
7676 header[297..297 + identity.gname.len()].copy_from_slice(&identity.gname);
7677 finalize_tar_checksum(header)
7678}
7679
7680#[cfg(test)]
7681fn build_regular_file_member_group(
7682 path: &[u8],
7683 contents: &[u8],
7684 mode: u32,
7685 mtime: ArchiveTimestamp,
7686 portable_metadata: &PortableFileMetadata,
7687) -> Result<Vec<u8>, FormatError> {
7688 let mut out = build_regular_file_member_prefix(
7689 path,
7690 contents.len() as u64,
7691 mode,
7692 mtime,
7693 portable_metadata,
7694 )?;
7695 out.extend_from_slice(contents);
7696 out.resize(out.len() + padding_to_512(contents.len()), 0);
7697 Ok(out)
7698}
7699
7700fn path_requires_pax(path: &[u8]) -> bool {
7701 path.len() > 100
7702}
7703
7704fn v45_portable_file_entry_flags(
7705 mode: u32,
7706 primary_sparse: bool,
7707 metadata: &PortableFileMetadata,
7708) -> u32 {
7709 EXTENDED_METADATA_V1
7710 | if metadata
7711 .native
7712 .auxiliary_records
7713 .iter()
7714 .any(|record| record.kind == CAPTURE_REPORT_KIND)
7715 {
7716 CAPTURE_PARTIAL
7717 } else {
7718 0
7719 }
7720 | if metadata.native.auxiliary_records.is_empty() {
7721 0
7722 } else {
7723 HAS_AUXILIARY_STREAMS
7724 }
7725 | if metadata
7726 .native
7727 .required_profiles
7728 .iter()
7729 .chain(&metadata.native.optional_profiles)
7730 .any(|profile| profile != PORTABLE_PROFILE)
7731 || !metadata.native.primary_pax_records.is_empty()
7732 || metadata
7733 .native
7734 .auxiliary_records
7735 .iter()
7736 .any(|record| record.native)
7737 {
7738 HAS_NATIVE_METADATA
7739 } else {
7740 0
7741 }
7742 | if primary_sparse
7743 || metadata
7744 .native
7745 .auxiliary_records
7746 .iter()
7747 .any(|record| record.flags & 1 != 0)
7748 {
7749 HAS_SPARSE_EXTENTS
7750 } else {
7751 0
7752 }
7753 | if mode & 0o6000 != 0
7754 || metadata.posix_owner.is_some()
7755 || native_metadata_requires_system_restore(&metadata.native, &metadata.source_os)
7756 || metadata
7757 .native
7758 .auxiliary_records
7759 .iter()
7760 .any(|record| record.restore_class == RestoreClass::System)
7761 {
7762 REQUIRES_SYSTEM_RESTORE
7763 } else {
7764 0
7765 }
7766}
7767
7768fn native_metadata_requires_system_restore(native: &NativeFileMetadata, source_os: &str) -> bool {
7769 native.primary_pax_records.iter().any(|(key, value)| {
7770 key.starts_with("TZAP.posix.device-")
7771 || key == "TZAP.linux.whiteout"
7772 || key == "TZAP.linux.project-id"
7773 || key == "TZAP.windows.reparse-placeholder"
7774 || key == "TZAP.windows.directory-case-sensitive"
7775 || key.starts_with("LIBARCHIVE.xattr.security")
7776 || key.starts_with("LIBARCHIVE.xattr.trusted")
7777 || key.starts_with("LIBARCHIVE.xattr.system")
7778 || (source_os == "linux"
7779 && key.starts_with("LIBARCHIVE.xattr.")
7780 && !key.starts_with("LIBARCHIVE.xattr.user.")
7781 && !key.starts_with("LIBARCHIVE.xattr.com.apple."))
7782 || (key == "TZAP.linux.fsflags"
7783 && std::str::from_utf8(value)
7784 .ok()
7785 .and_then(|value| u64::from_str_radix(value, 16).ok())
7786 .is_some_and(|flags| flags & 0x30 != 0))
7787 || (key == "TZAP.bsd.st-flags"
7788 && std::str::from_utf8(value)
7789 .ok()
7790 .and_then(|value| u64::from_str_radix(value, 16).ok())
7791 .is_some_and(|flags| flags & 0x0006_0006 != 0))
7792 || (key == "TZAP.macos.st-flags"
7793 && std::str::from_utf8(value)
7794 .ok()
7795 .and_then(|value| u64::from_str_radix(value, 16).ok())
7796 .is_some_and(|flags| flags & 0x009f_0086 != 0))
7797 || (key == "TZAP.windows.data-stream-attributes"
7798 && std::str::from_utf8(value)
7799 .ok()
7800 .and_then(|value| u32::from_str_radix(value, 16).ok())
7801 .is_some_and(|flags| flags & 0x0000_0002 != 0))
7802 || (key == "SCHILY.fflags"
7803 && std::str::from_utf8(value).ok().is_some_and(|value| {
7804 value.split(',').any(|token| {
7805 matches!(
7806 token,
7807 "append" | "immutable" | "sappnd" | "schg" | "uappnd" | "uchg"
7808 )
7809 })
7810 }))
7811 })
7812}
7813
7814fn build_ustar_header(
7815 path: &[u8],
7816 size: u64,
7817 mode: u32,
7818 mtime: u64,
7819 typeflag: u8,
7820) -> Result<[u8; TAR_BLOCK_LEN], FormatError> {
7821 if path.len() > 100 {
7822 return Err(FormatError::WriterUnsupported(
7823 "ustar path exceeds name field",
7824 ));
7825 }
7826 let mut header = [0u8; TAR_BLOCK_LEN];
7827 header[0..path.len()].copy_from_slice(path);
7828 write_tar_octal(&mut header[100..108], mode as u64)?;
7829 write_tar_octal(&mut header[108..116], 0)?;
7830 write_tar_octal(&mut header[116..124], 0)?;
7831 write_tar_octal(&mut header[124..136], size)?;
7832 write_tar_octal(&mut header[136..148], mtime)?;
7833 header[148..156].fill(b' ');
7834 header[156] = typeflag;
7835 header[257..263].copy_from_slice(b"ustar\0");
7836 header[263..265].copy_from_slice(b"00");
7837 finalize_tar_checksum(&mut header)?;
7838 Ok(header)
7839}
7840
7841fn finalize_tar_checksum(header: &mut [u8; TAR_BLOCK_LEN]) -> Result<(), FormatError> {
7842 header[148..156].fill(b' ');
7843 let checksum = header.iter().map(|byte| *byte as u32).sum::<u32>() as u64;
7844 write_tar_checksum(&mut header[148..156], checksum)
7845}
7846
7847fn write_tar_octal(field: &mut [u8], mut value: u64) -> Result<(), FormatError> {
7848 if field.is_empty() {
7849 return Err(FormatError::WriterUnsupported("tar octal field overflow"));
7850 }
7851 let mut i = field.len() - 1;
7852 field[i] = 0;
7853
7854 if value == 0 {
7855 if i == 0 {
7856 return Err(FormatError::WriterUnsupported("tar octal field overflow"));
7857 }
7858 i -= 1;
7859 field[i] = b'0';
7860 } else {
7861 while value > 0 {
7862 if i == 0 {
7863 return Err(FormatError::WriterUnsupported("tar octal field overflow"));
7864 }
7865 i -= 1;
7866 field[i] = b'0' + (value & 7) as u8;
7867 value >>= 3;
7868 }
7869 }
7870 while i > 0 {
7871 i -= 1;
7872 field[i] = b'0';
7873 }
7874 Ok(())
7875}
7876
7877fn tar_octal_fits(field_len: usize, mut value: u64) -> bool {
7878 if field_len == 0 {
7879 return false;
7880 }
7881 let max_digits = field_len - 1;
7882 let mut digits = 0;
7883 if value == 0 {
7884 digits = 1;
7885 }
7886 while value > 0 {
7887 digits += 1;
7888 value >>= 3;
7889 }
7890 digits <= max_digits
7891}
7892
7893fn write_tar_checksum(field: &mut [u8], mut value: u64) -> Result<(), FormatError> {
7894 if field.len() < 8 {
7895 return Err(FormatError::WriterUnsupported(
7896 "tar checksum field overflow",
7897 ));
7898 }
7899 let mut i = 6;
7900 while i > 0 {
7901 i -= 1;
7902 field[i] = b'0' + (value & 7) as u8;
7903 value >>= 3;
7904 }
7905 if value > 0 {
7906 return Err(FormatError::WriterUnsupported(
7907 "tar checksum field overflow",
7908 ));
7909 }
7910 field[6] = 0;
7911 field[7] = b' ';
7912 Ok(())
7913}
7914
7915fn member_frame_range(
7916 member_index: usize,
7917 frames: &[PayloadFrame],
7918) -> Result<(u64, u32), FormatError> {
7919 let first = frames
7920 .iter()
7921 .find(|frame| frame.member_index == member_index)
7922 .map(|frame| frame.frame_index)
7923 .ok_or(FormatError::WriterInvariant("member frame is missing"))?;
7924 let count = frames
7925 .iter()
7926 .filter(|frame| frame.member_index == member_index)
7927 .count();
7928 Ok((first, u32_len(count, "FileEntry.frame_count")?))
7929}
7930
7931fn envelope_frame_range(
7932 envelope_index: u64,
7933 frames: &[PayloadFrame],
7934) -> Result<(u64, u32), FormatError> {
7935 let first = frames
7936 .iter()
7937 .find(|frame| frame.envelope_index == envelope_index)
7938 .map(|frame| frame.frame_index)
7939 .ok_or(FormatError::WriterInvariant("envelope frame is missing"))?;
7940 let count = frames
7941 .iter()
7942 .filter(|frame| frame.envelope_index == envelope_index)
7943 .count();
7944 Ok((first, u32_len(count, "EnvelopeEntry.frame_count")?))
7945}
7946
7947fn sha256_bytes(bytes: &[u8]) -> [u8; 32] {
7948 let digest = Sha256::digest(bytes);
7949 let mut out = [0u8; 32];
7950 out.copy_from_slice(&digest);
7951 out
7952}
7953
7954fn padding_to_512(len: usize) -> usize {
7955 let remainder = len % TAR_BLOCK_LEN;
7956 if remainder == 0 {
7957 0
7958 } else {
7959 TAR_BLOCK_LEN - remainder
7960 }
7961}
7962
7963fn padding_to_512_u64(len: u64) -> u64 {
7964 let remainder = len % TAR_BLOCK_LEN as u64;
7965 if remainder == 0 {
7966 0
7967 } else {
7968 TAR_BLOCK_LEN as u64 - remainder
7969 }
7970}
7971
7972fn table_offset(len: usize, cursor: usize) -> Result<u32, FormatError> {
7973 if len == 0 {
7974 Ok(0)
7975 } else {
7976 u32_len(cursor, "table offset")
7977 }
7978}
7979
7980fn u32_len(value: usize, field: &'static str) -> Result<u32, FormatError> {
7981 u32::try_from(value).map_err(|_| FormatError::WriterUnsupported(field))
7982}
7983
7984fn to_usize_writer(value: u64, field: &'static str) -> Result<usize, FormatError> {
7985 usize::try_from(value).map_err(|_| FormatError::WriterUnsupported(field))
7986}
7987
7988fn checked_usize_add(lhs: usize, rhs: usize, field: &'static str) -> Result<usize, FormatError> {
7989 lhs.checked_add(rhs)
7990 .ok_or(FormatError::WriterUnsupported(field))
7991}
7992
7993fn checked_u64_add(lhs: u64, rhs: u64, field: &'static str) -> Result<u64, FormatError> {
7994 lhs.checked_add(rhs)
7995 .ok_or(FormatError::WriterUnsupported(field))
7996}
7997
7998#[cfg(test)]
7999mod tests {
8000 use super::*;
8001 use crate::crypto::{verify_hmac, Subkeys};
8002 use crate::metadata::{DirectoryHintTable, IndexShard, MetadataLimits};
8003 use crate::reader::{open_archive, open_archive_with_recipient_wrap_resolver};
8004 use crate::tar_model::parse_tar_member_group;
8005 use crate::wire::{CriticalRecoveryLocator, CryptoHeader};
8006 use std::cell::RefCell;
8007 use std::io::{self, Read};
8008 use std::rc::Rc;
8009
8010 #[test]
8011 fn writer_defaults_use_v41_sizing_and_parallel_mode() {
8012 let options = WriterOptions::default();
8013
8014 assert_eq!(options.chunk_size, 256 * 1024);
8015 assert_eq!(options.envelope_target_size, 1024 * 1024);
8016 assert_eq!(options.block_size, 64 * 1024);
8017 assert_eq!(options.stripe_width, 8);
8018 assert_eq!(options.volume_loss_tolerance, 1);
8019 assert_eq!(options.fec_data_shards, 224);
8020 assert_eq!(options.index_fec_data_shards, 16);
8021 assert_eq!(
8022 options.index_root_fec_data_shards,
8023 MIN_INDEX_ROOT_FEC_DATA_SHARDS
8024 );
8025 assert_eq!(options.bit_rot_buffer_pct, 5);
8026 assert_eq!(options.jobs, default_jobs());
8027 assert!(options.jobs >= 1);
8028 }
8029
8030 #[test]
8031 fn emission_state_collects_data_leaf_hashes_only_for_root_auth() {
8032 let options = single_volume_metadata_test_options();
8033 let archive_uuid = [1u8; 16];
8034 let session_id = [2u8; 16];
8035 let crypto_header = b"test crypto header";
8036
8037 let mut unsigned_sink = MemoryArchiveSink::default();
8038 let volume_format_rev = volume_format_revision_for_options(&options, &KdfParams::None);
8039 let unsigned = begin_writer_emission_state(
8040 &mut unsigned_sink,
8041 options,
8042 crypto_header,
8043 None,
8044 archive_uuid,
8045 session_id,
8046 volume_format_rev,
8047 false,
8048 )
8049 .unwrap();
8050 assert!(unsigned.data_leaf_hashes.is_none());
8051
8052 let mut signed_sink = MemoryArchiveSink::default();
8053 let volume_format_rev = volume_format_revision_for_options(&options, &KdfParams::None);
8054 let signed = begin_writer_emission_state(
8055 &mut signed_sink,
8056 options,
8057 crypto_header,
8058 None,
8059 archive_uuid,
8060 session_id,
8061 volume_format_rev,
8062 true,
8063 )
8064 .unwrap();
8065 assert_eq!(signed.data_leaf_hashes.as_deref(), Some([].as_slice()));
8066 }
8067
8068 #[test]
8069 fn ordered_envelope_serializes_zero_parity_without_root_auth_leaf_collection() {
8070 let options = plan_writer_options(WriterOptions {
8071 stripe_width: 1,
8072 volume_loss_tolerance: 0,
8073 bit_rot_buffer_pct: 0,
8074 aead_algo: AeadAlgo::None,
8075 ..WriterOptions::default()
8076 })
8077 .unwrap();
8078 let payload = deterministic_bytes(options.block_size as usize * 2 + 17);
8079 let extent = ObjectExtent::new(
8080 11,
8081 plan_encrypted_object(
8082 payload.len(),
8083 options.fec_data_shards,
8084 options.fec_parity_shards,
8085 options,
8086 )
8087 .unwrap(),
8088 )
8089 .unwrap();
8090 assert_eq!(extent.parity_block_count, 0);
8091 assert_eq!(extent.data_block_count, 3);
8092 let subkeys = Subkeys::unencrypted_placeholder();
8093
8094 let result = build_ordered_envelope_result(
8095 OrderedEnvelopeJob {
8096 envelope_index: 3,
8097 plaintext: payload.clone(),
8098 extent,
8099 collect_data_leaf_hashes: false,
8100 },
8101 &subkeys,
8102 options,
8103 [1u8; 16],
8104 [2u8; 16],
8105 )
8106 .unwrap();
8107
8108 match result.records {
8109 OrderedEnvelopeRecords::Serialized(records) => {
8110 assert_eq!(records.len(), extent.data_block_count as usize);
8111 let parsed =
8112 BlockRecord::parse(&records[0].bytes, options.block_size as usize).unwrap();
8113 assert_eq!(parsed.block_index, extent.first_block_index);
8114 assert_eq!(parsed.kind, BlockKind::PayloadData);
8115 assert!(!parsed.is_last_data());
8116 let last =
8117 BlockRecord::parse(&records[2].bytes, options.block_size as usize).unwrap();
8118 assert_eq!(last.block_index, extent.first_block_index + 2);
8119 assert_eq!(last.kind, BlockKind::PayloadData);
8120 assert!(last.is_last_data());
8121 }
8122 OrderedEnvelopeRecords::Materialized(_) => {
8123 panic!("zero-parity unsigned envelope should use serialized records")
8124 }
8125 }
8126
8127 let result = build_ordered_envelope_result(
8128 OrderedEnvelopeJob {
8129 envelope_index: 3,
8130 plaintext: payload,
8131 extent,
8132 collect_data_leaf_hashes: true,
8133 },
8134 &subkeys,
8135 options,
8136 [1u8; 16],
8137 [2u8; 16],
8138 )
8139 .unwrap();
8140
8141 match result.records {
8142 OrderedEnvelopeRecords::Materialized(records) => {
8143 assert_eq!(records.len(), extent.data_block_count as usize);
8144 }
8145 OrderedEnvelopeRecords::Serialized(_) => {
8146 panic!("root-auth leaf collection requires materialized records")
8147 }
8148 }
8149 }
8150
8151 #[test]
8152 fn ordered_envelope_serializes_encrypted_zero_parity_like_materialized_path() {
8153 let options = plan_writer_options(WriterOptions {
8154 stripe_width: 1,
8155 volume_loss_tolerance: 0,
8156 bit_rot_buffer_pct: 0,
8157 aead_algo: AeadAlgo::AesGcmSiv256,
8158 ..WriterOptions::default()
8159 })
8160 .unwrap();
8161 let payload = deterministic_bytes(options.block_size as usize * 2 + 17);
8162 let extent = ObjectExtent::new(
8163 29,
8164 plan_encrypted_object(
8165 payload.len(),
8166 options.fec_data_shards,
8167 options.fec_parity_shards,
8168 options,
8169 )
8170 .unwrap(),
8171 )
8172 .unwrap();
8173 assert_eq!(extent.parity_block_count, 0);
8174 assert_eq!(extent.data_block_count, 3);
8175 let archive_uuid = [3u8; 16];
8176 let session_id = [4u8; 16];
8177 let master_key = MasterKey::from_raw_key(&[0x5a; 32]).unwrap();
8178 let subkeys = Subkeys::derive(&master_key, &archive_uuid, &session_id).unwrap();
8179
8180 let serialized = build_ordered_envelope_result(
8181 OrderedEnvelopeJob {
8182 envelope_index: 7,
8183 plaintext: payload.clone(),
8184 extent,
8185 collect_data_leaf_hashes: false,
8186 },
8187 &subkeys,
8188 options,
8189 archive_uuid,
8190 session_id,
8191 )
8192 .unwrap();
8193 let serialized_bytes = match serialized.records {
8194 OrderedEnvelopeRecords::Serialized(records) => records
8195 .into_iter()
8196 .map(|record| record.bytes)
8197 .collect::<Vec<_>>(),
8198 OrderedEnvelopeRecords::Materialized(_) => {
8199 panic!("zero-parity encrypted envelope should use serialized records")
8200 }
8201 };
8202
8203 let materialized = build_ordered_envelope_result(
8204 OrderedEnvelopeJob {
8205 envelope_index: 7,
8206 plaintext: payload,
8207 extent,
8208 collect_data_leaf_hashes: true,
8209 },
8210 &subkeys,
8211 options,
8212 archive_uuid,
8213 session_id,
8214 )
8215 .unwrap();
8216 let materialized_bytes = match materialized.records {
8217 OrderedEnvelopeRecords::Materialized(records) => records
8218 .iter()
8219 .map(BlockRecord::to_bytes)
8220 .collect::<Vec<_>>(),
8221 OrderedEnvelopeRecords::Serialized(_) => {
8222 panic!("root-auth leaf collection requires materialized records")
8223 }
8224 };
8225
8226 assert_eq!(serialized_bytes, materialized_bytes);
8227 }
8228
8229 #[test]
8230 fn writer_options_reject_zero_jobs() {
8231 let err = plan_writer_options(WriterOptions {
8232 jobs: 0,
8233 ..WriterOptions::default()
8234 })
8235 .unwrap_err();
8236
8237 assert_eq!(
8238 err,
8239 FormatError::WriterUnsupported("jobs must be at least 1")
8240 );
8241 }
8242
8243 #[test]
8244 fn production_writer_defaults_generate_distinct_v4_identities() {
8245 let master_key = MasterKey::from_raw_key(&[9u8; 32]).unwrap();
8246 let first = write_archive(&[], &master_key, WriterOptions::default()).unwrap();
8247 let second = write_archive(&[], &master_key, WriterOptions::default()).unwrap();
8248
8249 assert_ne!(first.archive_uuid, [0u8; 16]);
8250 assert_ne!(first.session_id, [0u8; 16]);
8251 assert_ne!(second.archive_uuid, [0u8; 16]);
8252 assert_ne!(second.session_id, [0u8; 16]);
8253 assert_ne!(first.archive_uuid, first.session_id);
8254 assert_ne!(first.archive_uuid, second.archive_uuid);
8255 assert_ne!(first.session_id, second.session_id);
8256
8257 for raw in [
8258 first.archive_uuid,
8259 first.session_id,
8260 second.archive_uuid,
8261 second.session_id,
8262 ] {
8263 let id = Uuid::from_bytes(raw);
8264 assert_eq!(id.get_version_num(), 4);
8265 }
8266
8267 let deterministic = WriterOptions {
8268 archive_uuid: Some([0x44; 16]),
8269 session_id: Some([0x55; 16]),
8270 ..WriterOptions::default()
8271 };
8272 let fixture = write_archive(&[], &master_key, deterministic).unwrap();
8273 assert_eq!(fixture.archive_uuid, [0x44; 16]);
8274 assert_eq!(fixture.session_id, [0x55; 16]);
8275 }
8276
8277 #[test]
8278 fn writer_partitions_multiple_default_sized_index_shards() {
8279 let members = (0..=DEFAULT_FILES_PER_INDEX_SHARD)
8280 .map(|idx| TarMember {
8281 path: format!("file-{idx:05}.txt").into_bytes(),
8282 entry_kind: SourceEntryKind::Regular,
8283 link_target: None,
8284 tar_member_group_start: idx as u64 * 512,
8285 tar_member_group_size: 512,
8286 file_data_size: 0,
8287 sparse_extents: None,
8288 mode: 0o644,
8289 mtime: ArchiveTimestamp::UNIX_EPOCH,
8290 portable_metadata: PortableFileMetadata::default(),
8291 })
8292 .collect::<Vec<_>>();
8293
8294 let shards = partition_file_rows(sorted_file_rows(&members)).unwrap();
8295
8296 assert_eq!(shards.len(), 2);
8297 assert_eq!(shards[0].len(), DEFAULT_FILES_PER_INDEX_SHARD);
8298 assert_eq!(shards[1].len(), 1);
8299 }
8300
8301 #[test]
8302 fn writer_extends_shard_for_bounded_hash_prefix_run() {
8303 let mut rows = Vec::new();
8304 rows.extend((0..9_000).map(|idx| test_file_row(idx, [0u8; 8])));
8305 rows.extend((9_000..54_000).map(|idx| test_file_row(idx, [1u8; 8])));
8306 rows.push(test_file_row(54_000, [2u8; 8]));
8307
8308 let shards = partition_file_rows(rows).unwrap();
8309
8310 assert_eq!(shards.len(), 2);
8311 assert_eq!(shards[0].len(), 54_000);
8312 assert!(shards[0]
8313 .iter()
8314 .skip(9_000)
8315 .all(|row| row.path_hash == [1u8; 8]));
8316 assert_eq!(shards[1][0].path_hash, [2u8; 8]);
8317 }
8318
8319 #[test]
8320 fn writer_splits_oversized_hash_prefix_run_at_writer_ceiling() {
8321 let rows = (0..MAX_HASH_PREFIX_RUN_FILES + 1)
8322 .map(|idx| test_file_row(idx, [7u8; 8]))
8323 .collect::<Vec<_>>();
8324
8325 let shards = partition_file_rows(rows).unwrap();
8326
8327 assert_eq!(shards.len(), 2);
8328 assert_eq!(shards[0].len(), MAX_HASH_PREFIX_RUN_FILES);
8329 assert_eq!(shards[1].len(), 1);
8330 }
8331
8332 #[test]
8333 fn writer_builds_directory_hint_rows_for_ancestor_directories() {
8334 let shard_rows = vec![
8335 vec![FileRow {
8336 path_hash: hash_prefix(b"a/b/one.txt"),
8337 path: b"a/b/one.txt".to_vec(),
8338 member_index: 0,
8339 member: TarMember {
8340 path: b"a/b/one.txt".to_vec(),
8341 entry_kind: SourceEntryKind::Regular,
8342 link_target: None,
8343 tar_member_group_start: 0,
8344 tar_member_group_size: 512,
8345 file_data_size: 0,
8346 sparse_extents: None,
8347 mode: 0o644,
8348 mtime: ArchiveTimestamp::UNIX_EPOCH,
8349 portable_metadata: PortableFileMetadata::default(),
8350 },
8351 }],
8352 vec![FileRow {
8353 path_hash: hash_prefix(b"a/c/two.txt"),
8354 path: b"a/c/two.txt".to_vec(),
8355 member_index: 1,
8356 member: TarMember {
8357 path: b"a/c/two.txt".to_vec(),
8358 entry_kind: SourceEntryKind::Regular,
8359 link_target: None,
8360 tar_member_group_start: 512,
8361 tar_member_group_size: 512,
8362 file_data_size: 0,
8363 sparse_extents: None,
8364 mode: 0o644,
8365 mtime: ArchiveTimestamp::UNIX_EPOCH,
8366 portable_metadata: PortableFileMetadata::default(),
8367 },
8368 }],
8369 ];
8370
8371 let options = plan_writer_options(WriterOptions::default()).unwrap();
8372 let planned = build_directory_hint_plaintexts(&shard_rows, options).unwrap();
8373 assert_eq!(planned.len(), 1);
8374 let locating = DirectoryHintShardEntry {
8375 hint_shard_index: planned[0].hint_shard_index,
8376 first_dir_hash: planned[0].first_dir_hash,
8377 last_dir_hash: planned[0].last_dir_hash,
8378 first_block_index: 0,
8379 data_block_count: 1,
8380 parity_block_count: 0,
8381 encrypted_size: 4096,
8382 decompressed_size: planned[0].plaintext.len() as u32,
8383 entry_count: planned[0].entry_count,
8384 };
8385 let table = DirectoryHintTable::parse(
8386 &planned[0].plaintext,
8387 &locating,
8388 2,
8389 MetadataLimits::default(),
8390 )
8391 .unwrap();
8392
8393 let root = table.lookup_directory_index(b"").unwrap();
8394 assert_eq!(table.shard_rows_for_entry(root).unwrap(), &[0, 1]);
8395 let a = table.lookup_directory_index(b"a").unwrap();
8396 assert_eq!(table.shard_rows_for_entry(a).unwrap(), &[0, 1]);
8397 let ab = table.lookup_directory_index(b"a/b").unwrap();
8398 assert_eq!(table.shard_rows_for_entry(ab).unwrap(), &[0]);
8399 }
8400
8401 #[test]
8402 fn directory_hints_are_required_only_above_v41_threshold() {
8403 assert!(!should_emit_directory_hints(0));
8404 assert!(!should_emit_directory_hints(
8405 DIRECTORY_HINT_REQUIRED_FILE_COUNT
8406 ));
8407 assert!(should_emit_directory_hints(
8408 DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1
8409 ));
8410 }
8411
8412 #[test]
8413 fn regular_file_writer_uses_local_pax_path_for_long_and_non_ascii_paths() {
8414 let long_path = format!("dir/{}.txt", "a".repeat(120));
8415 let unicode_path = "unicode/e\u{301}.txt";
8416 let files = [
8417 RegularFile::new(&long_path, b"long path"),
8418 RegularFile::new(unicode_path, b"unicode path"),
8419 ];
8420
8421 let (tar_stream, members) = build_tar_stream(&files, 4096).unwrap();
8422
8423 for (member, expected_path, expected_data) in [
8424 (&members[0], long_path.as_bytes(), b"long path".as_slice()),
8425 (
8426 &members[1],
8427 "unicode/\u{e9}.txt".as_bytes(),
8428 b"unicode path".as_slice(),
8429 ),
8430 ] {
8431 let start = member.tar_member_group_start as usize;
8432 let end = start + member.tar_member_group_size as usize;
8433 let group = &tar_stream[start..end];
8434 assert_eq!(group[156], b'x');
8435 let parsed = parse_tar_member_group(group, 4096).unwrap();
8436 assert_eq!(parsed.path, expected_path);
8437 assert_eq!(parsed.data, expected_data);
8438 }
8439 }
8440
8441 #[test]
8442 fn regular_file_writer_emits_no_global_metadata_or_tar_eof() {
8443 let long_path = format!("dir/{}.txt", "a".repeat(120));
8444 let files = [
8445 RegularFile::new("plain.txt", b"plain contents"),
8446 RegularFile::new(&long_path, b"long path contents"),
8447 ];
8448
8449 let (tar_stream, members) = build_tar_stream(&files, 4096).unwrap();
8450
8451 let member_bytes = members
8452 .iter()
8453 .map(|member| member.tar_member_group_size)
8454 .sum::<u64>();
8455 assert_eq!(tar_stream.len() as u64, member_bytes);
8456 assert!(!tar_stream[tar_stream.len() - TAR_BLOCK_LEN * 2..]
8457 .chunks(TAR_BLOCK_LEN)
8458 .all(|block| block.iter().all(|byte| *byte == 0)));
8459
8460 for member in members {
8461 let start = member.tar_member_group_start as usize;
8462 let end = start + member.tar_member_group_size as usize;
8463 assert_path_specific_member_group(&tar_stream[start..end]);
8464 }
8465 }
8466
8467 struct SparseTestSource {
8468 logical_size: u64,
8469 extents: Vec<SparseExtent>,
8470 extent_bytes: Vec<u8>,
8471 }
8472
8473 impl RegularFileSource for SparseTestSource {
8474 fn archive_path(&self) -> &str {
8475 "sparse.bin"
8476 }
8477
8478 fn file_data_size(&self) -> u64 {
8479 self.logical_size
8480 }
8481
8482 fn sparse_extents(&self) -> Option<&[SparseExtent]> {
8483 Some(&self.extents)
8484 }
8485
8486 fn mode(&self) -> u32 {
8487 0o644
8488 }
8489
8490 fn mtime(&self) -> ArchiveTimestamp {
8491 ArchiveTimestamp::UNIX_EPOCH
8492 }
8493
8494 fn open(&self) -> Result<Box<dyn Read + '_>, ArchiveWriteError> {
8495 Ok(Box::new(Cursor::new(self.extent_bytes.as_slice())))
8496 }
8497 }
8498
8499 #[test]
8500 fn sparse_writer_emits_canonical_gnu_sparse_primary_and_all_hole_file() {
8501 for source in [
8502 SparseTestSource {
8503 logical_size: 32,
8504 extents: vec![
8505 SparseExtent {
8506 offset: 4,
8507 length: 3,
8508 },
8509 SparseExtent {
8510 offset: 16,
8511 length: 2,
8512 },
8513 SparseExtent {
8514 offset: 30,
8515 length: 2,
8516 },
8517 ],
8518 extent_bytes: b"abcdeyz".to_vec(),
8519 },
8520 SparseTestSource {
8521 logical_size: 1 << 20,
8522 extents: Vec::new(),
8523 extent_bytes: Vec::new(),
8524 },
8525 ] {
8526 let expected_map_prefix = format!("{}\n", source.extents.len());
8527 let (tar_stream, members) = build_tar_stream(&[source], 4096).unwrap();
8528 let parsed = parse_tar_member_group(&tar_stream, 4096).unwrap();
8529 assert_eq!(parsed.path, b"sparse.bin");
8530 assert_eq!(parsed.logical_size, members[0].file_data_size);
8531 let layout = parsed.v45_metadata.sparse_layout.unwrap();
8532 assert_eq!(layout.logical_size, members[0].file_data_size);
8533 assert_eq!(layout.extents, members[0].sparse_extents.clone().unwrap());
8534 assert!(parsed.data.starts_with(expected_map_prefix.as_bytes()));
8535 }
8536 }
8537
8538 #[test]
8539 fn sparse_writer_rejects_noncanonical_extent_maps() {
8540 for extents in [
8541 vec![SparseExtent {
8542 offset: 0,
8543 length: 0,
8544 }],
8545 vec![
8546 SparseExtent {
8547 offset: 0,
8548 length: 2,
8549 },
8550 SparseExtent {
8551 offset: 2,
8552 length: 2,
8553 },
8554 ],
8555 vec![SparseExtent {
8556 offset: 9,
8557 length: 2,
8558 }],
8559 ] {
8560 let source = SparseTestSource {
8561 logical_size: 10,
8562 extents,
8563 extent_bytes: Vec::new(),
8564 };
8565 assert!(build_tar_stream(&[source], 4096).is_err());
8566 }
8567 }
8568
8569 #[test]
8570 fn sparse_writer_indexes_logical_size_and_streams_expanded_content() {
8571 let source = SparseTestSource {
8572 logical_size: 12,
8573 extents: vec![
8574 SparseExtent {
8575 offset: 2,
8576 length: 3,
8577 },
8578 SparseExtent {
8579 offset: 9,
8580 length: 2,
8581 },
8582 ],
8583 extent_bytes: b"abcxy".to_vec(),
8584 };
8585 let master_key = MasterKey::from_raw_key(&[7u8; 32]).unwrap();
8586 let mut sink = MemoryArchiveSink::default();
8587 write_archive_sources_to_sink(
8588 &[source],
8589 &master_key,
8590 single_volume_metadata_test_options(),
8591 None,
8592 &KdfParams::Raw,
8593 None,
8594 None,
8595 &mut sink,
8596 )
8597 .unwrap();
8598
8599 let opened = open_archive(&sink.volumes[0], &master_key).unwrap();
8600 opened.verify().unwrap();
8601 let entry = opened.lookup_index_entry("sparse.bin").unwrap().unwrap();
8602 assert_eq!(entry.file_data_size, 12);
8603 assert_ne!(entry.flags & HAS_SPARSE_EXTENTS, 0);
8604 assert_eq!(
8605 opened.extract_file("sparse.bin").unwrap().unwrap(),
8606 b"\0\0abc\0\0\0\0xy\0"
8607 );
8608 }
8609
8610 #[test]
8611 fn regular_file_writer_round_trips_mode_and_mtime() {
8612 let group = build_regular_file_member_group(
8613 b"script.sh",
8614 b"#!/bin/sh\n",
8615 0o755,
8616 ArchiveTimestamp::from_seconds(1_700_000_000),
8617 &PortableFileMetadata::default(),
8618 )
8619 .unwrap();
8620
8621 let parsed = parse_tar_member_group(&group, 4096).unwrap();
8622
8623 assert_eq!(parsed.mode, 0o755);
8624 assert_eq!(parsed.mtime, ArchiveTimestamp::from_seconds(1_700_000_000));
8625 }
8626
8627 #[test]
8628 fn regular_file_writer_round_trips_nanosecond_and_pre_epoch_mtimes() {
8629 for expected in [
8630 ArchiveTimestamp::new(1_700_000_000, 123_456_789),
8631 ArchiveTimestamp::new(-1, 500_000_000),
8632 ] {
8633 let group = build_regular_file_member_group(
8634 b"dated.txt",
8635 b"dated",
8636 0o644,
8637 expected,
8638 &PortableFileMetadata::default(),
8639 )
8640 .unwrap();
8641 let parsed = parse_tar_member_group(&group, 4096).unwrap();
8642 assert_eq!(parsed.mtime, expected);
8643 assert_eq!(
8644 parsed.v45_metadata.portable_mirror.mtime,
8645 (expected.seconds, expected.nanoseconds)
8646 );
8647 }
8648 }
8649
8650 #[test]
8651 fn regular_file_writer_rejects_invalid_timestamp_nanoseconds() {
8652 assert!(matches!(
8653 build_regular_file_member_group(
8654 b"dated.txt",
8655 b"dated",
8656 0o644,
8657 ArchiveTimestamp::new(0, 1_000_000_000),
8658 &PortableFileMetadata::default(),
8659 ),
8660 Err(FormatError::WriterUnsupported(
8661 "timestamp nanoseconds must be less than one billion"
8662 ))
8663 ));
8664 }
8665
8666 #[test]
8667 fn macos_entitlement_and_superuser_flags_require_system_restore() {
8668 for flags in [0x0000_0080u64, 0x0008_0000, 0x0010_0000, 0x0080_0000] {
8669 let mut native = NativeFileMetadata::default();
8670 native.primary_pax_records.insert(
8671 "TZAP.macos.st-flags".into(),
8672 format!("{flags:016x}").into_bytes(),
8673 );
8674 assert!(native_metadata_requires_system_restore(&native, "macos"));
8675 }
8676 }
8677
8678 #[test]
8679 fn linux_bsd_macos_immutable_flags_require_system_restore() {
8680 let mut native = NativeFileMetadata::default();
8681 native.primary_pax_records.insert(
8682 "TZAP.linux.fsflags".into(),
8683 "0000000000000030".into(), );
8685 assert!(native_metadata_requires_system_restore(&native, "linux"));
8686
8687 let mut native = NativeFileMetadata::default();
8688 native.primary_pax_records.insert(
8689 "TZAP.bsd.st-flags".into(),
8690 "0000000000060006".into(), );
8692 assert!(native_metadata_requires_system_restore(&native, "freebsd"));
8693
8694 let mut native = NativeFileMetadata::default();
8695 native.primary_pax_records.insert(
8696 "TZAP.macos.st-flags".into(),
8697 "0000000000040000".into(), );
8699 assert!(native_metadata_requires_system_restore(&native, "macos"));
8700
8701 let mut native = NativeFileMetadata::default();
8702 native
8703 .primary_pax_records
8704 .insert("SCHILY.fflags".into(), "uchg,uappnd".into());
8705 assert!(native_metadata_requires_system_restore(&native, "linux"));
8706 }
8707
8708 #[test]
8709 fn windows_reparse_and_attributes_require_system_restore() {
8710 let mut native = NativeFileMetadata::default();
8711 native
8712 .primary_pax_records
8713 .insert("TZAP.windows.reparse-placeholder".into(), "1".into());
8714 assert!(native_metadata_requires_system_restore(&native, "windows"));
8715
8716 let mut native = NativeFileMetadata::default();
8717 native.primary_pax_records.insert(
8718 "TZAP.windows.data-stream-attributes".into(),
8719 "00000002".into(), );
8721 assert!(native_metadata_requires_system_restore(&native, "windows"));
8722 }
8723
8724 #[test]
8725 fn regular_file_writer_round_trips_portable_owner_origin_and_attributes() {
8726 let portable_metadata = PortableFileMetadata {
8727 source_os: "other-unix".into(),
8728 source_filesystem: "ext4".into(),
8729 mode_origin: PortableModeOrigin::Native,
8730 posix_owner: Some(PortablePosixOwner {
8731 uid: 9_000_000,
8732 gid: 42,
8733 uname: Some("tést-user".into()),
8734 gname: Some("archive".into()),
8735 }),
8736 attributes: Some(1),
8737 native: NativeFileMetadata::default(),
8738 };
8739 let group = build_regular_file_member_group(
8740 b"owned.txt",
8741 b"owned",
8742 0o640,
8743 ArchiveTimestamp::UNIX_EPOCH,
8744 &portable_metadata,
8745 )
8746 .unwrap();
8747 let parsed = parse_tar_member_group(&group, 4096).unwrap();
8748
8749 assert!(parsed.v45_metadata.declaration.owner_kind_posix);
8750 assert!(parsed.v45_metadata.declaration.mode_origin_native);
8751 assert_eq!(parsed.v45_metadata.declaration.source_os, "other-unix");
8752 assert_eq!(parsed.v45_metadata.declaration.source_filesystem, "ext4");
8753 assert_eq!(parsed.v45_metadata.portable_mirror.uid, Some(9_000_000));
8754 assert_eq!(parsed.v45_metadata.portable_mirror.gid, Some(42));
8755 assert_eq!(
8756 parsed.v45_metadata.portable_mirror.uname.as_deref(),
8757 Some("tést-user".as_bytes())
8758 );
8759 assert_eq!(parsed.v45_metadata.portable_mirror.attributes, Some(1));
8760 assert_ne!(
8761 parsed.v45_metadata.file_entry_flags & REQUIRES_SYSTEM_RESTORE,
8762 0
8763 );
8764 }
8765
8766 #[test]
8767 fn directory_writer_emits_type_and_portable_ownership_metadata() {
8768 let portable_metadata = PortableFileMetadata {
8769 source_os: "other-unix".into(),
8770 source_filesystem: "unknown".into(),
8771 mode_origin: PortableModeOrigin::Native,
8772 posix_owner: Some(PortablePosixOwner {
8773 uid: 9_000_000,
8774 gid: 42,
8775 uname: Some("directory-owner".into()),
8776 gname: Some("archive".into()),
8777 }),
8778 attributes: None,
8779 native: NativeFileMetadata::default(),
8780 };
8781 let bytes = build_primary_member_prefix(
8782 b"empty-dir",
8783 SourceEntryKind::Directory,
8784 None,
8785 0,
8786 None,
8787 0o2750,
8788 ArchiveTimestamp::new(1_700_000_000, 123_456_789),
8789 &portable_metadata,
8790 )
8791 .unwrap();
8792 let parsed = parse_tar_member_group(&bytes, 4096).unwrap();
8793
8794 assert_eq!(parsed.kind, crate::tar_model::TarEntryKind::Directory);
8795 assert_eq!(parsed.logical_size, 0);
8796 assert_eq!(parsed.v45_metadata.portable_mirror.mode, 0o2750);
8797 assert_eq!(parsed.v45_metadata.portable_mirror.uid, Some(9_000_000));
8798 assert_eq!(parsed.v45_metadata.portable_mirror.gid, Some(42));
8799 assert_eq!(
8800 parsed.v45_metadata.portable_mirror.mtime,
8801 (1_700_000_000, 123_456_789)
8802 );
8803 }
8804
8805 #[test]
8806 fn posix_special_writer_emits_fifo_and_device_primaries() {
8807 let mut portable = PortableFileMetadata {
8808 source_os: "linux".into(),
8809 mode_origin: PortableModeOrigin::Native,
8810 native: NativeFileMetadata {
8811 required_profiles: vec!["posix-backup-v1".into(), "linux-backup-v1".into()],
8812 ..NativeFileMetadata::default()
8813 },
8814 ..PortableFileMetadata::default()
8815 };
8816 let fifo = build_primary_member_prefix(
8817 b"pipe",
8818 SourceEntryKind::Fifo,
8819 None,
8820 0,
8821 None,
8822 0o640,
8823 ArchiveTimestamp::UNIX_EPOCH,
8824 &portable,
8825 )
8826 .unwrap();
8827 assert_eq!(
8828 parse_tar_member_group(&fifo, 4096).unwrap().kind,
8829 crate::tar_model::TarEntryKind::Fifo
8830 );
8831
8832 portable
8833 .native
8834 .primary_pax_records
8835 .insert("TZAP.posix.device-major".into(), b"1".to_vec());
8836 portable
8837 .native
8838 .primary_pax_records
8839 .insert("TZAP.posix.device-minor".into(), b"3".to_vec());
8840 let device = build_primary_member_prefix(
8841 b"null",
8842 SourceEntryKind::CharacterDevice,
8843 None,
8844 0,
8845 None,
8846 0o666,
8847 ArchiveTimestamp::UNIX_EPOCH,
8848 &portable,
8849 )
8850 .unwrap();
8851 assert_eq!(
8852 parse_tar_member_group(&device, 4096).unwrap().kind,
8853 crate::tar_model::TarEntryKind::CharacterDevice
8854 );
8855 }
8856
8857 #[test]
8858 fn symlink_writer_emits_target_and_fractional_mtime() {
8859 let mtime = ArchiveTimestamp::new(1_700_000_321, 654_321_000);
8860 let bytes = build_primary_member_prefix(
8861 b"links/current",
8862 SourceEntryKind::Symlink,
8863 Some(b"../target.txt"),
8864 0,
8865 None,
8866 0o777,
8867 mtime,
8868 &PortableFileMetadata::default(),
8869 )
8870 .unwrap();
8871 let parsed = parse_tar_member_group(&bytes, 4096).unwrap();
8872
8873 assert_eq!(parsed.kind, crate::tar_model::TarEntryKind::Symlink);
8874 assert_eq!(
8875 parsed.link_target.as_deref(),
8876 Some(b"../target.txt".as_slice())
8877 );
8878 assert_eq!(parsed.mtime, mtime);
8879 assert_eq!(
8880 parsed.v45_metadata.portable_mirror.mtime,
8881 (mtime.seconds, mtime.nanoseconds)
8882 );
8883 }
8884
8885 #[test]
8886 fn hardlink_writer_emits_zero_data_alias_with_portable_mirror_only() {
8887 let portable = PortableFileMetadata {
8888 source_os: "other-unix".into(),
8889 source_filesystem: "ext4".into(),
8890 mode_origin: PortableModeOrigin::Native,
8891 posix_owner: Some(PortablePosixOwner {
8892 uid: 1000,
8893 gid: 100,
8894 uname: Some("owner".into()),
8895 gname: Some("group".into()),
8896 }),
8897 attributes: Some(1),
8898 native: NativeFileMetadata::default(),
8899 };
8900 let bytes = build_primary_member_prefix(
8901 b"aliases/beta",
8902 SourceEntryKind::Hardlink,
8903 Some(b"aliases/alpha"),
8904 0,
8905 None,
8906 0o640,
8907 ArchiveTimestamp::new(1_700_000_000, 123_456_700),
8908 &portable,
8909 )
8910 .unwrap();
8911 let parsed = parse_tar_member_group(&bytes, 4096).unwrap();
8912 assert_eq!(parsed.kind, crate::tar_model::TarEntryKind::Hardlink);
8913 assert_eq!(parsed.logical_size, 0);
8914 assert_eq!(
8915 parsed.link_target.as_deref(),
8916 Some(b"aliases/alpha".as_slice())
8917 );
8918 assert_eq!(
8919 parsed.v45_metadata.declaration.required_profiles,
8920 ["portable-v1"]
8921 );
8922 assert!(parsed.v45_metadata.auxiliary.is_empty());
8923 assert_eq!(parsed.v45_metadata.portable_mirror.mode, 0o640);
8924 assert_eq!(parsed.v45_metadata.portable_mirror.uid, Some(1000));
8925 assert_eq!(parsed.v45_metadata.portable_mirror.attributes, Some(1));
8926 }
8927
8928 #[test]
8929 fn hardlink_writer_rejects_native_file_object_metadata() {
8930 let mut portable = PortableFileMetadata::default();
8931 portable
8932 .native
8933 .primary_pax_records
8934 .insert("TZAP.unix.ctime-observed".into(), b"1".to_vec());
8935 assert!(matches!(
8936 build_primary_member_prefix(
8937 b"beta",
8938 SourceEntryKind::Hardlink,
8939 Some(b"alpha"),
8940 0,
8941 None,
8942 0o644,
8943 ArchiveTimestamp::UNIX_EPOCH,
8944 &portable,
8945 ),
8946 Err(FormatError::WriterInvariant(
8947 "hardlink alias carries native file-object metadata"
8948 ))
8949 ));
8950 }
8951
8952 #[test]
8953 fn regular_file_writer_rejects_reserved_portable_attribute_bits() {
8954 let portable_metadata = PortableFileMetadata {
8955 attributes: Some(1 << 4),
8956 ..PortableFileMetadata::default()
8957 };
8958 assert_eq!(
8959 build_regular_file_member_group(
8960 b"attributes.txt",
8961 b"data",
8962 0o644,
8963 ArchiveTimestamp::UNIX_EPOCH,
8964 &portable_metadata,
8965 )
8966 .unwrap_err(),
8967 FormatError::WriterUnsupported("portable attributes contain reserved bits")
8968 );
8969 }
8970
8971 #[test]
8972 fn regular_file_writer_emits_and_flags_valid_native_primary_metadata() {
8973 let mut native = NativeFileMetadata {
8974 required_profiles: vec!["posix-backup-v1".into()],
8975 ..NativeFileMetadata::default()
8976 };
8977 native.primary_pax_records.insert(
8978 "LIBARCHIVE.xattr.user.comment".into(),
8979 crate::entry_metadata::canonical_base64_encode(b"preserved"),
8980 );
8981 let metadata = PortableFileMetadata {
8982 source_os: "linux".into(),
8983 source_filesystem: "ext4".into(),
8984 native,
8985 ..PortableFileMetadata::default()
8986 };
8987 let group = build_regular_file_member_group(
8988 b"native.txt",
8989 b"data",
8990 0o640,
8991 ArchiveTimestamp::UNIX_EPOCH,
8992 &metadata,
8993 )
8994 .unwrap();
8995 let parsed = parse_tar_member_group(&group, 4096).unwrap();
8996
8997 assert_eq!(
8998 parsed.v45_metadata.declaration.required_profiles,
8999 vec!["portable-v1", "posix-backup-v1"]
9000 );
9001 assert_eq!(
9002 parsed
9003 .v45_metadata
9004 .primary_records
9005 .get("LIBARCHIVE.xattr.user.comment")
9006 .map(Vec::as_slice),
9007 Some(b"cHJlc2VydmVk".as_slice())
9008 );
9009 assert_ne!(
9010 parsed.v45_metadata.file_entry_flags & HAS_NATIVE_METADATA,
9011 0
9012 );
9013 }
9014
9015 #[test]
9016 fn regular_file_writer_emits_valid_native_auxiliary_metadata() {
9017 let mut native = NativeFileMetadata {
9018 required_profiles: vec!["posix-backup-v1".into()],
9019 ..NativeFileMetadata::default()
9020 };
9021 let mut auxiliary = NativeAuxiliaryMetadata::new(
9022 "generic.xattr",
9023 "posix-backup-v1",
9024 RestoreClass::SameOs,
9025 b"large xattr value".to_vec(),
9026 );
9027 auxiliary.name_encoding = NativeAuxiliaryNameEncoding::Bytes;
9028 auxiliary.name = b"user.large".to_vec();
9029 native.auxiliary_records.push(auxiliary);
9030 let metadata = PortableFileMetadata {
9031 source_os: "linux".into(),
9032 native,
9033 ..PortableFileMetadata::default()
9034 };
9035
9036 let group = build_regular_file_member_group(
9037 b"native-aux.txt",
9038 b"contents",
9039 0o640,
9040 ArchiveTimestamp::from_seconds(12),
9041 &metadata,
9042 )
9043 .unwrap();
9044 let parsed = parse_tar_member_group(&group, 4096).unwrap();
9045
9046 assert_eq!(parsed.v45_metadata.auxiliary.len(), 1);
9047 assert_eq!(parsed.v45_metadata.auxiliary[0].kind, "generic.xattr");
9048 assert_eq!(parsed.v45_metadata.auxiliary[0].decoded_name, b"user.large");
9049 assert_ne!(
9050 parsed.v45_metadata.file_entry_flags & HAS_AUXILIARY_STREAMS,
9051 0
9052 );
9053 }
9054
9055 #[test]
9056 fn regular_file_writer_emits_capture_partial_flag_when_capture_report_present() {
9057 let mut native = NativeFileMetadata::default();
9058 let mut auxiliary = NativeAuxiliaryMetadata::new(
9059 "tzap.capture-report",
9060 "tzap-core-v1",
9061 RestoreClass::None,
9062 b"tzap-capture-report-v1\nportable-v1\ttzap-core-v1\texcluded-policy\tdetail\n"
9063 .to_vec(),
9064 );
9065 auxiliary.native = false;
9066 auxiliary.name_encoding = NativeAuxiliaryNameEncoding::None;
9067 auxiliary.name = vec![];
9068 native.auxiliary_records.push(auxiliary);
9069 let metadata = PortableFileMetadata {
9070 source_os: "linux".into(),
9071 native,
9072 ..PortableFileMetadata::default()
9073 };
9074
9075 let group = build_regular_file_member_group(
9076 b"capture-report.txt",
9077 b"contents",
9078 0o640,
9079 ArchiveTimestamp::from_seconds(12),
9080 &metadata,
9081 )
9082 .unwrap();
9083 let parsed = parse_tar_member_group(&group, 4096).unwrap();
9084
9085 assert_eq!(parsed.v45_metadata.auxiliary.len(), 1);
9086 assert_eq!(parsed.v45_metadata.auxiliary[0].kind, "tzap.capture-report");
9087 assert_ne!(parsed.v45_metadata.file_entry_flags & CAPTURE_PARTIAL, 0);
9088 }
9089
9090 #[test]
9091 fn streamed_auxiliary_sources_work_across_writer_modes_and_verify_digest() {
9092 struct StreamedSource {
9093 metadata: PortableFileMetadata,
9094 payload: Vec<u8>,
9095 }
9096
9097 impl RegularFileSource for StreamedSource {
9098 fn archive_path(&self) -> &str {
9099 "streamed-aux.txt"
9100 }
9101
9102 fn file_data_size(&self) -> u64 {
9103 8
9104 }
9105
9106 fn mode(&self) -> u32 {
9107 0o640
9108 }
9109
9110 fn mtime(&self) -> ArchiveTimestamp {
9111 ArchiveTimestamp::from_seconds(12)
9112 }
9113
9114 fn portable_metadata(&self) -> PortableFileMetadata {
9115 self.metadata.clone()
9116 }
9117
9118 fn open(&self) -> Result<Box<dyn Read + '_>, ArchiveWriteError> {
9119 Ok(Box::new(Cursor::new(b"contents".as_slice())))
9120 }
9121
9122 fn open_auxiliary(
9123 &self,
9124 ordinal: usize,
9125 ) -> Result<Box<dyn Read + '_>, ArchiveWriteError> {
9126 assert_eq!(ordinal, 0);
9127 Ok(Box::new(Cursor::new(self.payload.as_slice())))
9128 }
9129 }
9130
9131 let payload = deterministic_bytes(1024 * 1024 + 17);
9132 let digest: [u8; 32] = Sha256::digest(&payload).into();
9133 let mut auxiliary = NativeAuxiliaryMetadata::new_streamed(
9134 "generic.xattr",
9135 "posix-backup-v1",
9136 RestoreClass::SameOs,
9137 payload.len() as u64,
9138 digest,
9139 );
9140 auxiliary.name_encoding = NativeAuxiliaryNameEncoding::Bytes;
9141 auxiliary.name = b"user.large".to_vec();
9142 let mut source = StreamedSource {
9143 metadata: PortableFileMetadata {
9144 source_os: "linux".into(),
9145 native: NativeFileMetadata {
9146 required_profiles: vec!["posix-backup-v1".into()],
9147 auxiliary_records: vec![auxiliary],
9148 ..NativeFileMetadata::default()
9149 },
9150 ..PortableFileMetadata::default()
9151 },
9152 payload,
9153 };
9154 let key = MasterKey::from_raw_key(&[17u8; 32]).unwrap();
9155 let options = WriterOptions {
9156 stripe_width: 1,
9157 volume_loss_tolerance: 0,
9158 bit_rot_buffer_pct: 0,
9159 ..WriterOptions::default()
9160 };
9161
9162 let mut two_pass = MemoryArchiveSink::default();
9163 write_archive_sources_to_sink(
9164 std::slice::from_ref(&source),
9165 &key,
9166 options,
9167 None,
9168 &KdfParams::Raw,
9169 None,
9170 None,
9171 &mut two_pass,
9172 )
9173 .unwrap();
9174 open_archive(&two_pass.volumes[0], &key)
9175 .unwrap()
9176 .verify_content()
9177 .unwrap();
9178
9179 let mut single_pass = MemoryArchiveSink::default();
9180 write_archive_sources_to_sink_single_pass(
9181 std::slice::from_ref(&source),
9182 &key,
9183 options,
9184 &KdfParams::Raw,
9185 None,
9186 None,
9187 &mut single_pass,
9188 )
9189 .unwrap();
9190 open_archive(&single_pass.volumes[0], &key)
9191 .unwrap()
9192 .verify_content()
9193 .unwrap();
9194
9195 let mut ordered = MemoryArchiveSink::default();
9196 write_archive_sources_to_sink_ordered_parallel(
9197 std::slice::from_ref(&source),
9198 &key,
9199 options,
9200 &KdfParams::Raw,
9201 None,
9202 None,
9203 &mut ordered,
9204 )
9205 .unwrap();
9206 open_archive(&ordered.volumes[0], &key)
9207 .unwrap()
9208 .verify_content()
9209 .unwrap();
9210
9211 let mut progress_sink = MemoryArchiveSink::default();
9212 let mut progress = RecordingWriteProgress::default();
9213 write_archive_sources_to_sink_single_pass_with_progress(
9214 std::slice::from_ref(&source),
9215 &key,
9216 options,
9217 &KdfParams::Raw,
9218 None,
9219 None,
9220 &mut progress_sink,
9221 &mut progress,
9222 )
9223 .unwrap();
9224 assert_eq!(
9225 progress.bytes_for(ArchiveWritePhase::EmittingPayload),
9226 8 + source.payload.len() as u64
9227 );
9228
9229 source.metadata.native.auxiliary_records[0]
9230 .streamed_payload
9231 .as_mut()
9232 .unwrap()
9233 .sha256[0] ^= 1;
9234 let mut rejected = MemoryArchiveSink::default();
9235 assert!(write_archive_sources_to_sink(
9236 std::slice::from_ref(&source),
9237 &key,
9238 options,
9239 None,
9240 &KdfParams::Raw,
9241 None,
9242 None,
9243 &mut rejected,
9244 )
9245 .is_err());
9246 }
9247
9248 #[test]
9249 fn writer_splits_large_payload_across_seekable_envelopes() {
9250 let master_key = MasterKey::from_raw_key(&[8u8; 32]).unwrap();
9251 let data = deterministic_bytes(2 * 1024 * 1024);
9252 let archive = write_archive(
9253 &[RegularFile::new("large.bin", &data)],
9254 &master_key,
9255 WriterOptions {
9256 stripe_width: 1,
9257 volume_loss_tolerance: 0,
9258 bit_rot_buffer_pct: 0,
9259 ..WriterOptions::default()
9260 },
9261 )
9262 .unwrap();
9263 let opened = open_archive(&archive.bytes, &master_key).unwrap();
9264
9265 assert_eq!(opened.list_files().unwrap()[0].path, "large.bin");
9266 assert_eq!(opened.extract_file("large.bin").unwrap(), Some(data));
9267 opened.verify().unwrap();
9268 assert!(opened.index_root.header.envelope_count > 1);
9269 }
9270
9271 #[test]
9272 fn split_member_frames_carry_exact_boundary_flags() {
9273 let data = deterministic_bytes(12 * 1024);
9274 let files = [RegularFile::new("large.bin", &data)];
9275 let options = WriterOptions {
9276 chunk_size: 1024,
9277 envelope_target_size: 64 * 1024,
9278 stripe_width: 1,
9279 volume_loss_tolerance: 0,
9280 bit_rot_buffer_pct: 0,
9281 ..WriterOptions::default()
9282 };
9283 let (tar_stream, members) = build_tar_stream(&files, options.max_path_length).unwrap();
9284 let (_, frames) = build_payload_envelopes(&tar_stream, &members, options, None).unwrap();
9285
9286 assert!(frames.len() > 2);
9287 assert_eq!(frames.first().unwrap().flags, 0x0000_0001);
9288 assert_eq!(frames.last().unwrap().flags, 0x0000_0002);
9289 assert!(frames[1..frames.len() - 1]
9290 .iter()
9291 .all(|frame| frame.flags == 0));
9292 }
9293
9294 #[test]
9295 fn spanning_payload_derives_exact_multi_frame_multi_envelope_stats() {
9296 let data = deterministic_bytes(13 * 1024);
9297 let files = [RegularFile::new("spanning.bin", &data)];
9298 let options = plan_writer_options(WriterOptions {
9299 block_size: MIN_BLOCK_SIZE,
9300 chunk_size: 1024,
9301 envelope_target_size: 2500,
9302 stripe_width: 1,
9303 volume_loss_tolerance: 0,
9304 bit_rot_buffer_pct: 0,
9305 fec_parity_shards: 0,
9306 index_fec_parity_shards: 0,
9307 index_root_fec_parity_shards: 0,
9308 ..WriterOptions::default()
9309 })
9310 .unwrap();
9311 let mut next_block_index = 0u64;
9312
9313 let payload = plan_payload_stream(&files, options, None, &mut next_block_index).unwrap();
9314 let expected_tar_total_size =
9315 3 * TAR_BLOCK_LEN as u64 + data.len() as u64 + padding_to_512(data.len()) as u64;
9316 let (tar_stream, _) = build_tar_stream(&files, options.max_path_length).unwrap();
9317
9318 assert_eq!(payload.tar_members.len(), 1);
9319 assert_eq!(payload.tar_members[0].tar_member_group_start, 0);
9320 assert_eq!(
9321 payload.tar_members[0].tar_member_group_size,
9322 expected_tar_total_size
9323 );
9324 assert_eq!(payload.tar_total_size, expected_tar_total_size);
9325 assert_eq!(payload.content_sha256, sha256_bytes(&tar_stream));
9326 assert_eq!(payload.frames.len(), 15);
9327 assert_eq!(payload.payload_objects.len(), 7);
9328 assert_eq!(payload.payload_block_count, 7);
9329 assert_eq!(next_block_index, 7);
9330
9331 for (idx, frame) in payload.frames.iter().enumerate() {
9332 let expected_decompressed_size = if idx == 14 { 512 } else { 1024 };
9333 let expected_flags = match idx {
9334 0 => 0x0000_0001,
9335 14 => 0x0000_0002,
9336 _ => 0,
9337 };
9338 let expected_offset = payload.frames[..idx]
9339 .iter()
9340 .filter(|prior| prior.envelope_index == frame.envelope_index)
9341 .map(|prior| prior.compressed_size)
9342 .sum::<u32>();
9343
9344 assert_eq!(frame.frame_index, idx as u64);
9345 assert_eq!(frame.member_index, 0);
9346 assert!((frame.envelope_index as usize) < payload.payload_objects.len());
9347 assert_eq!(frame.offset_in_envelope, expected_offset);
9348 assert_eq!(frame.decompressed_size, expected_decompressed_size);
9349 assert_eq!(frame.flags, expected_flags);
9350 assert_eq!(frame.tar_stream_offset, idx as u64 * 1024);
9351 }
9352
9353 for (idx, object) in payload.payload_objects.iter().enumerate() {
9354 let expected_plaintext_size = payload
9355 .frames
9356 .iter()
9357 .filter(|frame| frame.envelope_index == idx as u64)
9358 .map(|frame| frame.compressed_size)
9359 .sum::<u32>();
9360
9361 assert_eq!(object.envelope_index, idx as u64);
9362 assert_eq!(object.plaintext_size, expected_plaintext_size);
9363 assert_eq!(object.object.first_block_index, idx as u64);
9364 assert_eq!(object.object.data_block_count, 1);
9365 assert_eq!(object.object.parity_block_count, 0);
9366 assert_eq!(object.object.encrypted_size, options.block_size);
9367 }
9368
9369 let shard_rows = partition_file_rows(sorted_file_rows(&payload.tar_members)).unwrap();
9370 let planned_shards = build_index_shard_plaintexts(
9371 &shard_rows,
9372 &payload.frames,
9373 &payload.payload_objects,
9374 options,
9375 )
9376 .unwrap();
9377 assert_eq!(planned_shards.len(), 1);
9378 let locating_shard = ShardEntry {
9379 shard_index: planned_shards[0].shard_index,
9380 first_block_index: 0,
9381 data_block_count: 1,
9382 parity_block_count: 0,
9383 encrypted_size: options.block_size,
9384 decompressed_size: planned_shards[0].plaintext.len() as u32,
9385 file_count: planned_shards[0].file_count,
9386 first_path_hash: planned_shards[0].first_path_hash,
9387 last_path_hash: planned_shards[0].last_path_hash,
9388 };
9389 let shard = IndexShard::parse(
9390 &planned_shards[0].plaintext,
9391 &locating_shard,
9392 MetadataLimits::default(),
9393 )
9394 .unwrap();
9395 assert_eq!(shard.header.file_count, 1);
9396 assert_eq!(shard.header.frame_count, 15);
9397 assert_eq!(shard.header.envelope_count, 7);
9398 assert_eq!(shard.files[0].first_frame_index, 0);
9399 assert_eq!(shard.files[0].frame_count, 15);
9400 assert_eq!(
9401 shard.files[0].tar_member_group_size,
9402 expected_tar_total_size
9403 );
9404 assert_eq!(shard.files[0].file_data_size, data.len() as u64);
9405
9406 for (idx, frame) in shard.frames.iter().enumerate() {
9407 assert_eq!(frame.frame_index, idx as u64);
9408 assert_eq!(frame.envelope_index, payload.frames[idx].envelope_index);
9409 assert_eq!(
9410 frame.offset_in_envelope,
9411 payload.frames[idx].offset_in_envelope
9412 );
9413 assert_eq!(frame.compressed_size, payload.frames[idx].compressed_size);
9414 assert_eq!(
9415 frame.decompressed_size,
9416 payload.frames[idx].decompressed_size
9417 );
9418 assert_eq!(frame.flags, payload.frames[idx].flags);
9419 assert_eq!(
9420 frame.tar_stream_offset,
9421 payload.frames[idx].tar_stream_offset
9422 );
9423 }
9424
9425 for (idx, envelope) in shard.envelopes.iter().enumerate() {
9426 assert_eq!(envelope.envelope_index, idx as u64);
9427 assert_eq!(envelope.first_block_index, idx as u64);
9428 assert_eq!(envelope.data_block_count, 1);
9429 assert_eq!(envelope.parity_block_count, 0);
9430 assert_eq!(envelope.encrypted_size, options.block_size);
9431 assert_eq!(
9432 envelope.plaintext_size,
9433 payload.payload_objects[idx].plaintext_size
9434 );
9435 let envelope_frames: Vec<_> = payload
9436 .frames
9437 .iter()
9438 .filter(|frame| frame.envelope_index == idx as u64)
9439 .collect();
9440 assert_eq!(
9441 envelope.first_frame_index,
9442 envelope_frames.first().unwrap().frame_index
9443 );
9444 assert_eq!(envelope.frame_count, envelope_frames.len() as u32);
9445 }
9446
9447 let master_key = MasterKey::from_raw_key(&[6u8; 32]).unwrap();
9448 let plan = build_writer_plan_from_payload(
9449 payload,
9450 next_block_index,
9451 &master_key,
9452 options,
9453 None,
9454 &KdfParams::Raw,
9455 None,
9456 [0x44; 16],
9457 [0x55; 16],
9458 None,
9459 )
9460 .unwrap();
9461 let index_root =
9462 IndexRoot::parse(&plan.index_root_plaintext, false, MetadataLimits::default()).unwrap();
9463
9464 assert_eq!(index_root.header.frame_count, 15);
9465 assert_eq!(index_root.header.envelope_count, 7);
9466 assert_eq!(index_root.header.file_count, 1);
9467 assert_eq!(index_root.header.payload_block_count, 7);
9468 assert_eq!(index_root.header.tar_total_size, expected_tar_total_size);
9469 assert_eq!(index_root.header.content_sha256, sha256_bytes(&tar_stream));
9470 }
9471
9472 #[test]
9473 fn writes_empty_archive_with_authentic_bootstrap_structures() {
9474 let master_key = MasterKey::from_raw_key(&[7u8; 32]).unwrap();
9475 let archive = write_empty_archive(&master_key).unwrap();
9476 let bytes = archive.bytes;
9477
9478 let volume_header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
9479 assert_eq!(volume_header.archive_uuid, archive.archive_uuid);
9480 assert_eq!(volume_header.session_id, archive.session_id);
9481
9482 let crypto_start = VOLUME_HEADER_LEN;
9483 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
9484 let crypto_header = CryptoHeader::parse(
9485 &bytes[crypto_start..crypto_end],
9486 volume_header.crypto_header_length,
9487 )
9488 .unwrap();
9489 let subkeys =
9490 Subkeys::derive(&master_key, &archive.archive_uuid, &archive.session_id).unwrap();
9491 verify_hmac(
9492 HmacDomain::CryptoHeader,
9493 &subkeys.mac_key,
9494 &archive.archive_uuid,
9495 &archive.session_id,
9496 crypto_header.hmac_covered_bytes,
9497 &crypto_header.header_hmac,
9498 )
9499 .unwrap();
9500
9501 let locator =
9502 CriticalRecoveryLocator::parse(&bytes[bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN..])
9503 .unwrap();
9504 let trailer_offset = locator.volume_trailer_offset as usize;
9505 let trailer =
9506 VolumeTrailer::parse(&bytes[trailer_offset..trailer_offset + VOLUME_TRAILER_LEN])
9507 .unwrap();
9508 assert_eq!(trailer.bytes_written, trailer_offset as u64);
9509 verify_hmac(
9510 HmacDomain::VolumeTrailer,
9511 &subkeys.mac_key,
9512 &archive.archive_uuid,
9513 &archive.session_id,
9514 &bytes[trailer_offset..trailer_offset + 96],
9515 &trailer.trailer_hmac,
9516 )
9517 .unwrap();
9518
9519 let manifest_offset = trailer.manifest_footer_offset as usize;
9520 let manifest_end = manifest_offset + MANIFEST_FOOTER_LEN;
9521 let manifest = ManifestFooter::parse(&bytes[manifest_offset..manifest_end]).unwrap();
9522 assert_eq!(manifest.is_authoritative, 1);
9523 assert_eq!(manifest.total_volumes, DEFAULT_STRIPE_WIDTH);
9524 verify_hmac(
9525 HmacDomain::ManifestFooter,
9526 &subkeys.mac_key,
9527 &archive.archive_uuid,
9528 &archive.session_id,
9529 &bytes[manifest_offset..manifest_offset + 104],
9530 &manifest.manifest_hmac,
9531 )
9532 .unwrap();
9533 }
9534
9535 #[test]
9536 fn parity_auto_scaling_matches_v41_examples() {
9537 let options = WriterOptions {
9538 fec_data_shards: 224,
9539 stripe_width: 8,
9540 volume_loss_tolerance: 1,
9541 bit_rot_buffer_pct: 5,
9542 ..WriterOptions::default()
9543 };
9544
9545 assert_eq!(compute_parity(224, options).unwrap(), 48);
9546 assert_eq!(compute_parity(17, options).unwrap(), 5);
9547 }
9548
9549 #[test]
9550 fn parity_auto_scaling_rejects_non_convergent_budget() {
9551 let err = compute_parity(
9552 1,
9553 WriterOptions {
9554 stripe_width: 2,
9555 volume_loss_tolerance: 1,
9556 bit_rot_buffer_pct: 50,
9557 ..WriterOptions::default()
9558 },
9559 )
9560 .unwrap_err();
9561
9562 assert_eq!(
9563 err,
9564 FormatError::WriterUnsupported("parity calculation did not converge")
9565 );
9566 }
9567
9568 #[test]
9569 fn zero_parity_is_allowed_when_no_recovery_margin_is_requested() {
9570 let planned = plan_writer_options(WriterOptions {
9571 bit_rot_buffer_pct: 0,
9572 stripe_width: 1,
9573 volume_loss_tolerance: 0,
9574 fec_parity_shards: 0,
9575 index_fec_parity_shards: 0,
9576 index_root_fec_parity_shards: 0,
9577 ..WriterOptions::default()
9578 })
9579 .unwrap();
9580
9581 assert_eq!(planned.fec_parity_shards, 0);
9582 assert_eq!(planned.index_fec_parity_shards, 0);
9583 assert_eq!(planned.index_root_fec_parity_shards, 0);
9584 assert_eq!(compute_parity(1, planned).unwrap(), 0);
9585 }
9586
9587 #[test]
9588 fn index_root_data_shard_maximum_obeys_v41_minimum() {
9589 let planned = plan_writer_options(WriterOptions {
9590 index_root_fec_data_shards: 1,
9591 ..WriterOptions::default()
9592 })
9593 .unwrap();
9594
9595 assert_eq!(
9596 planned.index_root_fec_data_shards,
9597 MIN_INDEX_ROOT_FEC_DATA_SHARDS
9598 );
9599 }
9600
9601 #[test]
9602 fn metadata_class_planning_raises_index_root_class_above_default() {
9603 let options = plan_writer_options(WriterOptions {
9604 block_size: MIN_BLOCK_SIZE,
9605 index_root_fec_parity_shards: 0,
9606 bit_rot_buffer_pct: 0,
9607 ..WriterOptions::default()
9608 })
9609 .unwrap();
9610 let index_root_payload_len = payload_len_for_encrypted_data_blocks(64, options);
9611
9612 let planned =
9613 plan_index_root_metadata_class(options, index_root_payload_len, None).unwrap();
9614
9615 assert_eq!(planned.index_root.data_block_count, 64);
9616 assert_eq!(planned.options.index_root_fec_data_shards, 64);
9617 assert_eq!(
9618 planned.options.index_root_fec_parity_shards,
9619 compute_parity_u16(
9620 planned.options.index_root_fec_data_shards as u64,
9621 planned.options,
9622 "index_root_fec_parity_shards",
9623 )
9624 .unwrap()
9625 );
9626 }
9627
9628 #[test]
9629 fn single_pass_writer_predeclares_metadata_class_before_payload_streaming() {
9630 let planned = plan_single_pass_writer_options(WriterOptions {
9631 block_size: MIN_BLOCK_SIZE,
9632 stripe_width: 1,
9633 volume_loss_tolerance: 0,
9634 bit_rot_buffer_pct: 0,
9635 index_root_fec_parity_shards: 0,
9636 ..WriterOptions::default()
9637 })
9638 .unwrap();
9639
9640 assert!(planned.index_root_fec_data_shards > DEFAULT_INDEX_ROOT_FEC_DATA_SHARDS);
9641 let index_root_payload_len = payload_len_for_encrypted_data_blocks(
9642 u32::from(planned.index_root_fec_data_shards - 1),
9643 planned,
9644 );
9645 let metadata_class =
9646 plan_index_root_metadata_class(planned, index_root_payload_len, None).unwrap();
9647
9648 assert_eq!(metadata_class.options, planned);
9649 }
9650
9651 #[test]
9652 fn metadata_class_planning_rejects_oversized_index_root() {
9653 let options = single_volume_metadata_test_options();
9654 let index_root_payload_len =
9655 payload_len_for_encrypted_data_blocks(u16::MAX as u32 + 1, options);
9656
9657 let err =
9658 plan_index_root_metadata_class(options, index_root_payload_len, None).unwrap_err();
9659
9660 assert_eq!(err, FormatError::WriterUnsupported("IndexRoot too large"));
9661 }
9662
9663 #[test]
9664 fn metadata_class_planning_rejects_index_root_u32_encrypted_size_overflow() {
9665 let options = single_volume_metadata_test_options();
9666 let index_root_payload_len = u32::MAX as usize - options.aead_algo.tag_len() + 1;
9667
9668 let err =
9669 plan_index_root_metadata_class(options, index_root_payload_len, None).unwrap_err();
9670
9671 assert_eq!(err, FormatError::WriterUnsupported("IndexRoot too large"));
9672 }
9673
9674 #[test]
9675 fn metadata_class_planning_rejects_oversized_dictionary() {
9676 let options = single_volume_metadata_test_options();
9677 let dictionary_payload_len =
9678 payload_len_for_encrypted_data_blocks(u16::MAX as u32 + 1, options);
9679
9680 let err =
9681 plan_index_root_metadata_class(options, 1, Some(dictionary_payload_len)).unwrap_err();
9682
9683 assert_eq!(
9684 err,
9685 FormatError::WriterUnsupported("dictionary object too large")
9686 );
9687 }
9688
9689 #[test]
9690 fn metadata_class_planning_rejects_gf16_total_overflow_for_dictionary() {
9691 let options = plan_writer_options(WriterOptions {
9692 block_size: MIN_BLOCK_SIZE,
9693 stripe_width: 8,
9694 volume_loss_tolerance: 1,
9695 bit_rot_buffer_pct: 5,
9696 ..WriterOptions::default()
9697 })
9698 .unwrap();
9699 let dictionary_payload_len = payload_len_for_encrypted_data_blocks(60_000, options);
9700
9701 let err =
9702 plan_index_root_metadata_class(options, 1, Some(dictionary_payload_len)).unwrap_err();
9703
9704 assert_eq!(
9705 err,
9706 FormatError::WriterUnsupported("dictionary object too large")
9707 );
9708 }
9709
9710 #[test]
9711 fn written_archive_authenticates_final_index_root_fec_class() {
9712 let master_key = MasterKey::from_raw_key(&[9u8; 32]).unwrap();
9713 let dictionary = deterministic_bytes(80 * 1024);
9714 let file = RegularFile::new("uses-dictionary.txt", b"payload");
9715 let archive = write_archive_with_dictionary(
9716 &[file],
9717 &master_key,
9718 WriterOptions {
9719 block_size: MIN_BLOCK_SIZE,
9720 stripe_width: 1,
9721 volume_loss_tolerance: 0,
9722 bit_rot_buffer_pct: 0,
9723 index_root_fec_parity_shards: 0,
9724 ..WriterOptions::default()
9725 },
9726 &dictionary,
9727 )
9728 .unwrap();
9729
9730 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
9731 let crypto_start = VOLUME_HEADER_LEN;
9732 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
9733 let crypto_header = CryptoHeader::parse(
9734 &archive.bytes[crypto_start..crypto_end],
9735 volume_header.crypto_header_length,
9736 )
9737 .unwrap();
9738 let subkeys =
9739 Subkeys::derive(&master_key, &archive.archive_uuid, &archive.session_id).unwrap();
9740 verify_hmac(
9741 HmacDomain::CryptoHeader,
9742 &subkeys.mac_key,
9743 &archive.archive_uuid,
9744 &archive.session_id,
9745 crypto_header.hmac_covered_bytes,
9746 &crypto_header.header_hmac,
9747 )
9748 .unwrap();
9749
9750 assert!(crypto_header.fixed.index_root_fec_data_shards > MIN_INDEX_ROOT_FEC_DATA_SHARDS);
9751 assert_eq!(crypto_header.fixed.index_root_fec_parity_shards, 0);
9752 let opened = open_archive(&archive.bytes, &master_key).unwrap();
9753 assert_eq!(
9754 opened.extract_file("uses-dictionary.txt").unwrap(),
9755 Some(b"payload".to_vec())
9756 );
9757 opened.verify().unwrap();
9758 }
9759
9760 #[test]
9761 fn object_parity_uses_per_object_recurrence_even_with_larger_class_max() {
9762 let options = WriterOptions {
9763 bit_rot_buffer_pct: 0,
9764 stripe_width: 1,
9765 volume_loss_tolerance: 0,
9766 fec_parity_shards: 1,
9767 ..WriterOptions::default()
9768 };
9769
9770 assert_eq!(compute_object_parity(1, options, 1).unwrap(), 0);
9771 }
9772
9773 #[test]
9774 fn object_total_shards_obeys_reed_solomon_limit() {
9775 assert!(validate_object_shard_total(65_535, 0).is_ok());
9776 assert_eq!(
9777 validate_object_shard_total(65_535, 1).unwrap_err(),
9778 FormatError::WriterUnsupported("encrypted object exceeds ReedSolomonGF16 shard limit")
9779 );
9780 }
9781
9782 #[test]
9783 fn argon2id_kdf_serialization_rejects_memory_requirement_overflow() {
9784 assert_eq!(
9785 serialize_kdf_params(&KdfParams::Argon2id {
9786 t_cost: 1,
9787 m_cost_kib: u32::MAX,
9788 parallelism: u32::MAX,
9789 salt: b"12345678".to_vec(),
9790 })
9791 .unwrap_err(),
9792 FormatError::InvalidKdfParams("m_cost_kib requirement overflow")
9793 );
9794 }
9795
9796 #[test]
9797 fn recipient_wrap_kdf_serialization() {
9798 let params = KdfParams::RecipientWrap {
9799 key_wrap_table_length: 16,
9800 key_wrap_table_record_count: 4,
9801 key_wrap_table_version: 1,
9802 key_wrap_table_digest: [0xaau8; 32],
9803 };
9804 let serialized = serialize_kdf_params(¶ms).unwrap();
9805 let mut expected = Vec::new();
9806 expected.extend_from_slice(&(KdfAlgo::RecipientWrap as u16).to_le_bytes());
9807 expected.extend_from_slice(&16u32.to_le_bytes());
9808 expected.extend_from_slice(&4u32.to_le_bytes());
9809 expected.extend_from_slice(&1u16.to_le_bytes());
9810 expected.extend_from_slice(&0u16.to_le_bytes());
9811 expected.extend_from_slice(&[0xaau8; 32]);
9812 assert_eq!(serialized, expected);
9813 }
9814
9815 fn recipient_wrap_test_record() -> RecipientRecordV1 {
9816 RecipientRecordV1 {
9817 record_length: 0,
9818 profile_id: 1,
9819 recipient_identity_type: 2,
9820 flags: 0,
9821 recipient_identity_length: 0,
9822 profile_payload_length: 0,
9823 recipient_identity_digest: [0u8; 32],
9824 recipient_identity_bytes: b"recipient-a".to_vec(),
9825 profile_payload_bytes: b"profile-payload".to_vec(),
9826 }
9827 }
9828
9829 #[test]
9830 fn writer_options_reject_reader_resource_cap_excesses() {
9831 assert_eq!(
9832 plan_writer_options(WriterOptions {
9833 stripe_width: crate::format::READER_MAX_STRIPE_WIDTH + 1,
9834 volume_loss_tolerance: 0,
9835 ..WriterOptions::default()
9836 })
9837 .unwrap_err(),
9838 FormatError::ReaderResourceLimitExceeded {
9839 field: "stripe_width",
9840 cap: crate::format::READER_MAX_STRIPE_WIDTH as u64,
9841 actual: crate::format::READER_MAX_STRIPE_WIDTH as u64 + 1,
9842 }
9843 );
9844 assert_eq!(
9845 plan_writer_options(WriterOptions {
9846 block_size: crate::format::READER_MAX_BLOCK_SIZE + 2,
9847 ..WriterOptions::default()
9848 })
9849 .unwrap_err(),
9850 FormatError::ReaderResourceLimitExceeded {
9851 field: "block_size",
9852 cap: crate::format::READER_MAX_BLOCK_SIZE as u64,
9853 actual: crate::format::READER_MAX_BLOCK_SIZE as u64 + 2,
9854 }
9855 );
9856 assert_eq!(
9857 plan_writer_options(WriterOptions {
9858 chunk_size: crate::format::READER_MAX_CHUNK_SIZE + 1,
9859 envelope_target_size: crate::format::READER_MAX_CHUNK_SIZE + 1,
9860 ..WriterOptions::default()
9861 })
9862 .unwrap_err(),
9863 FormatError::ReaderResourceLimitExceeded {
9864 field: "chunk_size",
9865 cap: crate::format::READER_MAX_CHUNK_SIZE as u64,
9866 actual: crate::format::READER_MAX_CHUNK_SIZE as u64 + 1,
9867 }
9868 );
9869 assert_eq!(
9870 plan_writer_options(WriterOptions {
9871 max_path_length: crate::format::READER_MAX_PATH_LENGTH + 1,
9872 ..WriterOptions::default()
9873 })
9874 .unwrap_err(),
9875 FormatError::ReaderResourceLimitExceeded {
9876 field: "max_path_length",
9877 cap: crate::format::READER_MAX_PATH_LENGTH as u64,
9878 actual: crate::format::READER_MAX_PATH_LENGTH as u64 + 1,
9879 }
9880 );
9881 assert_eq!(
9882 plan_writer_options(WriterOptions {
9883 bit_rot_buffer_pct: 0,
9884 stripe_width: 1,
9885 volume_loss_tolerance: 0,
9886 fec_data_shards: crate::format::READER_MAX_FEC_CLASS_SHARDS as u16 + 1,
9887 ..WriterOptions::default()
9888 })
9889 .unwrap_err(),
9890 FormatError::ReaderResourceLimitExceeded {
9891 field: "fec_data_shards + fec_parity_shards",
9892 cap: crate::format::READER_MAX_FEC_CLASS_SHARDS as u64,
9893 actual: crate::format::READER_MAX_FEC_CLASS_SHARDS as u64 + 1,
9894 }
9895 );
9896 }
9897
9898 #[test]
9899 fn root_auth_writer_config_rejects_reader_cap_excess_before_authenticator() {
9900 let master_key = MasterKey::from_raw_key(&[7u8; 32]).unwrap();
9901 let mut authenticator_called = false;
9902 let err = write_archive_with_root_auth(
9903 &[RegularFile::new("signed.txt", b"payload")],
9904 &master_key,
9905 single_volume_metadata_test_options(),
9906 RootAuthWriterConfig {
9907 authenticator_id: 1,
9908 signer_identity_type: 1,
9909 signer_identity: b"signer",
9910 authenticator_value_length: READER_MAX_ROOT_AUTH_AUTHENTICATOR_VALUE_LEN + 1,
9911 },
9912 |_| {
9913 authenticator_called = true;
9914 Ok(Vec::new())
9915 },
9916 )
9917 .unwrap_err();
9918
9919 assert!(!authenticator_called);
9920 assert_eq!(
9921 err,
9922 FormatError::ReaderResourceLimitExceeded {
9923 field: "RootAuthFooterV1 authenticator value length",
9924 cap: READER_MAX_ROOT_AUTH_AUTHENTICATOR_VALUE_LEN as u64,
9925 actual: READER_MAX_ROOT_AUTH_AUTHENTICATOR_VALUE_LEN as u64 + 1,
9926 }
9927 );
9928 }
9929
9930 #[test]
9931 fn root_auth_writer_accepts_128_kib_authenticator_value() {
9932 let master_key = MasterKey::from_raw_key(&[8u8; 32]).unwrap();
9933 let authenticator_value = vec![0x5a; READER_MAX_ROOT_AUTH_AUTHENTICATOR_VALUE_LEN as usize];
9934 let expected_value = authenticator_value.clone();
9935 let archive = write_archive_with_root_auth(
9936 &[RegularFile::new("signed.txt", b"payload")],
9937 &master_key,
9938 single_volume_metadata_test_options(),
9939 RootAuthWriterConfig {
9940 authenticator_id: 0xcafe,
9941 signer_identity_type: 1,
9942 signer_identity: b"certificate-profile-signer",
9943 authenticator_value_length: READER_MAX_ROOT_AUTH_AUTHENTICATOR_VALUE_LEN,
9944 },
9945 |_| Ok(authenticator_value.clone()),
9946 )
9947 .unwrap();
9948
9949 let opened = open_archive(&archive.bytes, &master_key).unwrap();
9950 let footer = opened.root_auth_footer.as_ref().unwrap();
9951 assert_eq!(footer.authenticator_id, 0xcafe);
9952 assert_eq!(
9953 footer.authenticator_value.as_slice(),
9954 expected_value.as_slice()
9955 );
9956
9957 let verification = opened
9958 .verify_root_auth_with(|footer, _| {
9959 Ok(footer.authenticator_id == 0xcafe
9960 && footer.authenticator_value.as_slice() == expected_value.as_slice())
9961 })
9962 .unwrap();
9963 assert_eq!(verification.authenticator_id, 0xcafe);
9964 }
9965
9966 #[test]
9967 fn streaming_writer_sink_round_trips_archive() {
9968 let files = [
9969 RegularFile::new("alpha.txt", b"alpha"),
9970 RegularFile::new("nested/beta.txt", b"beta payload"),
9971 ];
9972 let master_key = MasterKey::from_raw_key(&[7u8; 32]).unwrap();
9973 let mut sink = MemoryArchiveSink::default();
9974
9975 let summary = write_archive_sources_to_sink(
9976 &files,
9977 &master_key,
9978 single_volume_metadata_test_options(),
9979 None,
9980 &KdfParams::Raw,
9981 None,
9982 None,
9983 &mut sink,
9984 )
9985 .unwrap();
9986
9987 assert_eq!(summary.volume_count, 1);
9988 let opened = crate::reader::open_archive(&sink.volumes[0], &master_key).unwrap();
9989 assert_eq!(
9990 opened.extract_file("nested/beta.txt").unwrap(),
9991 Some(b"beta payload".to_vec())
9992 );
9993 }
9994
9995 #[test]
9996 fn ordered_sink_writer_round_trips_recipientwrap_records() {
9997 let files = [RegularFile::new("wrapped.txt", b"recipient sink payload")];
9998 let master_key = MasterKey::from_raw_key(&[9u8; 32]).unwrap();
9999 let mut sink = MemoryArchiveSink::default();
10000
10001 let summary = write_archive_sources_to_sink_ordered_parallel_with_recipient_wrap_records(
10002 &files,
10003 &master_key,
10004 single_volume_metadata_test_options(),
10005 vec![recipient_wrap_test_record()],
10006 None,
10007 None,
10008 &mut sink,
10009 )
10010 .unwrap();
10011
10012 assert_eq!(summary.volume_count, 1);
10013 let opened =
10014 open_archive_with_recipient_wrap_resolver(&sink.volumes[0], |_| Ok(vec![master_key.0]))
10015 .unwrap();
10016 assert_eq!(
10017 opened.extract_file("wrapped.txt").unwrap(),
10018 Some(b"recipient sink payload".to_vec())
10019 );
10020 }
10021
10022 #[test]
10023 fn ordered_sink_writer_indexes_v45_metadata_flags_from_member_semantics() {
10024 let mut sparse_payload = vec![0u8; TAR_BLOCK_LEN];
10025 sparse_payload[..2].copy_from_slice(b"0\n");
10026 let mut sparse_fork = NativeAuxiliaryMetadata::new(
10027 "macos.resource-fork",
10028 "macos-backup-v1",
10029 RestoreClass::SameOs,
10030 sparse_payload,
10031 );
10032 sparse_fork.flags = 1;
10033 sparse_fork.logical_size = 4096;
10034
10035 let sparse_metadata = PortableFileMetadata {
10036 source_os: "macos".into(),
10037 native: NativeFileMetadata {
10038 required_profiles: vec!["macos-backup-v1".into(), "posix-backup-v1".into()],
10039 auxiliary_records: vec![sparse_fork],
10040 ..NativeFileMetadata::default()
10041 },
10042 ..PortableFileMetadata::default()
10043 };
10044 let portable_only_metadata = PortableFileMetadata {
10045 native: NativeFileMetadata {
10046 required_profiles: vec![PORTABLE_PROFILE.into()],
10047 ..NativeFileMetadata::default()
10048 },
10049 ..PortableFileMetadata::default()
10050 };
10051 let files = [
10052 RegularFile {
10053 path: "sparse-fork.bin",
10054 contents: b"primary",
10055 mode: 0o644,
10056 mtime: ArchiveTimestamp::UNIX_EPOCH,
10057 portable_metadata: sparse_metadata,
10058 },
10059 RegularFile {
10060 path: "portable-only.bin",
10061 contents: b"portable",
10062 mode: 0o644,
10063 mtime: ArchiveTimestamp::UNIX_EPOCH,
10064 portable_metadata: portable_only_metadata,
10065 },
10066 ];
10067 let master_key = MasterKey::from_raw_key(&[0x45; 32]).unwrap();
10068 let mut sink = MemoryArchiveSink::default();
10069
10070 write_archive_sources_to_sink_ordered_parallel(
10071 &files,
10072 &master_key,
10073 single_volume_metadata_test_options(),
10074 &KdfParams::Raw,
10075 None,
10076 None,
10077 &mut sink,
10078 )
10079 .unwrap();
10080
10081 let opened = open_archive(&sink.volumes[0], &master_key).unwrap();
10082 opened.verify().unwrap();
10083 let entries = opened.list_index_entries().unwrap();
10084 let sparse = entries
10085 .iter()
10086 .find(|entry| entry.path == "sparse-fork.bin")
10087 .unwrap();
10088 assert_ne!(sparse.flags & HAS_SPARSE_EXTENTS, 0);
10089 let portable_only = entries
10090 .iter()
10091 .find(|entry| entry.path == "portable-only.bin")
10092 .unwrap();
10093 assert_eq!(portable_only.flags & HAS_NATIVE_METADATA, 0);
10094 }
10095
10096 #[test]
10097 fn single_pass_sink_writer_round_trips_recipientwrap_records() {
10098 let files = [RegularFile::new(
10099 "wrapped.txt",
10100 b"recipient single-pass sink payload",
10101 )];
10102 let master_key = MasterKey::from_raw_key(&[9u8; 32]).unwrap();
10103 let mut sink = MemoryArchiveSink::default();
10104
10105 let summary = write_archive_sources_to_sink_single_pass_with_recipient_wrap_records(
10106 &files,
10107 &master_key,
10108 single_volume_metadata_test_options(),
10109 vec![recipient_wrap_test_record()],
10110 None,
10111 None,
10112 &mut sink,
10113 )
10114 .unwrap();
10115
10116 assert_eq!(summary.volume_count, 1);
10117 let opened =
10118 open_archive_with_recipient_wrap_resolver(&sink.volumes[0], |_| Ok(vec![master_key.0]))
10119 .unwrap();
10120 assert_eq!(
10121 opened.extract_file("wrapped.txt").unwrap(),
10122 Some(b"recipient single-pass sink payload".to_vec())
10123 );
10124 opened.verify().unwrap();
10125 }
10126
10127 #[test]
10128 fn single_pass_sink_writer_round_trips_recipientwrap_root_auth() {
10129 let files = [RegularFile::new(
10130 "wrapped.txt",
10131 b"recipient single-pass signed payload",
10132 )];
10133 let master_key = MasterKey::from_raw_key(&[9u8; 32]).unwrap();
10134 let mut sink = MemoryArchiveSink::default();
10135
10136 let summary = write_archive_sources_to_sink_single_pass_with_recipient_wrap_records(
10137 &files,
10138 &master_key,
10139 single_volume_metadata_test_options(),
10140 vec![recipient_wrap_test_record()],
10141 Some(RootAuthWriterConfig {
10142 authenticator_id: 0x44,
10143 signer_identity_type: 1,
10144 signer_identity: b"recipient-wrap-single-pass",
10145 authenticator_value_length: 32,
10146 }),
10147 Some(&mut |request| Ok(request.archive_root.to_vec())),
10148 &mut sink,
10149 )
10150 .unwrap();
10151
10152 assert_eq!(summary.volume_count, 1);
10153 let opened =
10154 open_archive_with_recipient_wrap_resolver(&sink.volumes[0], |_| Ok(vec![master_key.0]))
10155 .unwrap();
10156 let verification = opened
10157 .verify_root_auth_with(|footer, archive_root| {
10158 Ok(footer.authenticator_id == 0x44
10159 && footer.authenticator_value.as_slice() == archive_root)
10160 })
10161 .unwrap();
10162 assert_eq!(verification.volume_format_rev, VOLUME_FORMAT_REV_45);
10163 }
10164
10165 #[test]
10166 fn single_pass_writer_rejects_recipientwrap_kdf_without_records_before_writing() {
10167 let files = [RegularFile::new("wrapped.txt", b"recipient sink payload")];
10168 let master_key = MasterKey::from_raw_key(&[9u8; 32]).unwrap();
10169 let mut sink = MemoryArchiveSink::default();
10170 let kdf_params = KdfParams::RecipientWrap {
10171 key_wrap_table_length: 0,
10172 key_wrap_table_record_count: 1,
10173 key_wrap_table_version: 1,
10174 key_wrap_table_digest: [0u8; 32],
10175 };
10176
10177 let err = write_archive_sources_to_sink_single_pass(
10178 &files,
10179 &master_key,
10180 single_volume_metadata_test_options(),
10181 &kdf_params,
10182 None,
10183 None,
10184 &mut sink,
10185 )
10186 .unwrap_err();
10187
10188 match err {
10189 ArchiveWriteError::Format(FormatError::WriterUnsupported(message)) => {
10190 assert_eq!(message, "RecipientWrap requires key-wrap records");
10191 }
10192 other => panic!("unexpected error: {other:?}"),
10193 }
10194 assert!(sink.volumes.is_empty());
10195 }
10196
10197 #[test]
10198 fn ordered_sink_writer_rejects_recipientwrap_kdf_without_records_before_writing() {
10199 let files = [RegularFile::new("wrapped.txt", b"recipient sink payload")];
10200 let master_key = MasterKey::from_raw_key(&[9u8; 32]).unwrap();
10201 let mut sink = MemoryArchiveSink::default();
10202 let kdf_params = KdfParams::RecipientWrap {
10203 key_wrap_table_length: 0,
10204 key_wrap_table_record_count: 1,
10205 key_wrap_table_version: 1,
10206 key_wrap_table_digest: [0u8; 32],
10207 };
10208
10209 let err = write_archive_sources_to_sink_ordered_parallel(
10210 &files,
10211 &master_key,
10212 single_volume_metadata_test_options(),
10213 &kdf_params,
10214 None,
10215 None,
10216 &mut sink,
10217 )
10218 .unwrap_err();
10219
10220 match err {
10221 ArchiveWriteError::Format(FormatError::WriterUnsupported(message)) => {
10222 assert_eq!(message, "RecipientWrap requires key-wrap records");
10223 }
10224 other => panic!("unexpected error: {other:?}"),
10225 }
10226 assert!(sink.volumes.is_empty());
10227 }
10228
10229 #[test]
10230 fn streaming_writer_bounds_source_reads_and_sink_writes_for_large_file() {
10231 let file_size = 3 * 1024 * 1024;
10232 let stats = Rc::new(RefCell::new(GeneratedSourceStats::default()));
10233 let file = GeneratedFileSource {
10234 path: "large/generated.bin",
10235 len: file_size,
10236 stats: Rc::clone(&stats),
10237 };
10238 let master_key = MasterKey::from_raw_key(&[3u8; 32]).unwrap();
10239 let options = plan_writer_options(WriterOptions {
10240 block_size: MIN_BLOCK_SIZE,
10241 chunk_size: 16 * 1024,
10242 envelope_target_size: 64 * 1024,
10243 stripe_width: 1,
10244 volume_loss_tolerance: 0,
10245 bit_rot_buffer_pct: 0,
10246 fec_parity_shards: 0,
10247 index_fec_parity_shards: 0,
10248 index_root_fec_parity_shards: 0,
10249 ..WriterOptions::default()
10250 })
10251 .unwrap();
10252 let mut sink = TrackingArchiveSink::default();
10253
10254 let summary = write_archive_sources_to_sink_single_pass(
10255 &[file],
10256 &master_key,
10257 options,
10258 &KdfParams::Raw,
10259 None,
10260 None,
10261 &mut sink,
10262 )
10263 .unwrap();
10264
10265 let stats = stats.borrow();
10266 assert_eq!(stats.open_count, 1);
10267 assert_eq!(stats.total_read, file_size as u64);
10268 assert!(stats.max_read_request <= options.chunk_size as usize);
10269 assert_eq!(summary.volume_count, 1);
10270 assert_eq!(summary.archive_bytes, sink.volume_bytes.iter().sum());
10271 assert_eq!(
10272 summary.bootstrap_sidecar_bytes,
10273 sink.bootstrap_sidecar_bytes
10274 );
10275 assert!(sink.max_write_len <= 128 * 1024);
10276 }
10277
10278 #[test]
10279 fn sink_writer_progress_reports_source_bytes_for_each_multi_pass_phase() {
10280 let file_size = 512 * 1024;
10281 let stats = Rc::new(RefCell::new(GeneratedSourceStats::default()));
10282 let file = GeneratedFileSource {
10283 path: "large/generated.bin",
10284 len: file_size,
10285 stats: Rc::clone(&stats),
10286 };
10287 let master_key = MasterKey::from_raw_key(&[4u8; 32]).unwrap();
10288 let options = WriterOptions {
10289 block_size: MIN_BLOCK_SIZE,
10290 chunk_size: 16 * 1024,
10291 envelope_target_size: 64 * 1024,
10292 stripe_width: 1,
10293 volume_loss_tolerance: 0,
10294 bit_rot_buffer_pct: 0,
10295 fec_parity_shards: 0,
10296 index_fec_parity_shards: 0,
10297 index_root_fec_parity_shards: 0,
10298 ..WriterOptions::default()
10299 };
10300 let mut sink = MemoryArchiveSink::default();
10301 let mut progress = RecordingWriteProgress::default();
10302
10303 let summary = write_archive_sources_to_sink_with_progress(
10304 &[file],
10305 &master_key,
10306 options,
10307 None,
10308 &KdfParams::Raw,
10309 None,
10310 None,
10311 &mut sink,
10312 &mut progress,
10313 )
10314 .unwrap();
10315
10316 let stats = stats.borrow();
10317 assert!(stats.open_count > 1);
10318 assert_eq!(
10319 progress.bytes_for(ArchiveWritePhase::PlanningPayload),
10320 file_size as u64
10321 );
10322 assert_eq!(
10323 progress.bytes_for(ArchiveWritePhase::EmittingPayload),
10324 file_size as u64
10325 );
10326 assert_eq!(summary.volume_count, 1);
10327 assert!(!sink.volumes.is_empty());
10328 }
10329
10330 #[test]
10331 fn sink_writer_progress_reports_multi_pass_phases_and_phase_bytes() {
10332 let file_size = 512 * 1024;
10333 let stats = Rc::new(RefCell::new(GeneratedSourceStats::default()));
10334 let file = GeneratedFileSource {
10335 path: "large/generated.bin",
10336 len: file_size,
10337 stats: Rc::clone(&stats),
10338 };
10339 let master_key = MasterKey::from_raw_key(&[4u8; 32]).unwrap();
10340 let options = WriterOptions {
10341 block_size: MIN_BLOCK_SIZE,
10342 chunk_size: 16 * 1024,
10343 envelope_target_size: 64 * 1024,
10344 stripe_width: 1,
10345 volume_loss_tolerance: 0,
10346 bit_rot_buffer_pct: 0,
10347 fec_parity_shards: 0,
10348 index_fec_parity_shards: 0,
10349 index_root_fec_parity_shards: 0,
10350 ..WriterOptions::default()
10351 };
10352 let mut sink = MemoryArchiveSink::default();
10353 let mut progress = RecordingWriteProgress::default();
10354
10355 write_archive_sources_to_sink_with_progress(
10356 &[file],
10357 &master_key,
10358 options,
10359 None,
10360 &KdfParams::Raw,
10361 None,
10362 None,
10363 &mut sink,
10364 &mut progress,
10365 )
10366 .unwrap();
10367
10368 assert_eq!(
10369 progress.phases,
10370 vec![
10371 ArchiveWritePhase::PlanningPayload,
10372 ArchiveWritePhase::PlanningMetadata,
10373 ArchiveWritePhase::EmittingPayload,
10374 ArchiveWritePhase::EmittingMetadata,
10375 ]
10376 );
10377 assert_eq!(
10378 progress.bytes_for(ArchiveWritePhase::PlanningPayload),
10379 file_size as u64
10380 );
10381 assert_eq!(
10382 progress.bytes_for(ArchiveWritePhase::EmittingPayload),
10383 file_size as u64
10384 );
10385 }
10386
10387 #[test]
10388 fn sink_writer_progress_reports_each_volume_size_replanning_attempt() {
10389 let file_size = 64 * 1024;
10390 let mut contents = Vec::with_capacity(file_size);
10391 let mut random = 0x1234_5678u32;
10392 for _ in 0..file_size {
10393 random = random.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
10394 contents.push((random >> 24) as u8);
10395 }
10396 let file = RegularFile::new("sized.bin", &contents);
10397 let master_key = MasterKey::from_raw_key(&[9u8; 32]).unwrap();
10398 let options = WriterOptions {
10399 block_size: 4 * 1024,
10400 chunk_size: 4 * 1024,
10401 envelope_target_size: 128 * 1024,
10402 stripe_width: 1,
10403 volume_loss_tolerance: 1,
10404 target_volume_size: Some(8 * 1024),
10405 ..WriterOptions::default()
10406 };
10407 let mut sink = MemoryArchiveSink::default();
10408 let mut progress = RecordingWriteProgress::default();
10409
10410 write_archive_sources_to_sink_with_progress(
10411 &[file],
10412 &master_key,
10413 options,
10414 None,
10415 &KdfParams::Raw,
10416 None,
10417 None,
10418 &mut sink,
10419 &mut progress,
10420 )
10421 .unwrap();
10422
10423 let planning_attempts = progress
10424 .phases
10425 .iter()
10426 .filter(|phase| **phase == ArchiveWritePhase::PlanningPayload)
10427 .count();
10428 assert!(planning_attempts > 1);
10429 assert_eq!(
10430 progress.phases.len(),
10431 planning_attempts * 2 + 2,
10432 "each planning attempt has payload and metadata phases before emission",
10433 );
10434 for phases in progress.phases[..planning_attempts * 2].chunks_exact(2) {
10435 assert_eq!(
10436 phases,
10437 [
10438 ArchiveWritePhase::PlanningPayload,
10439 ArchiveWritePhase::PlanningMetadata,
10440 ]
10441 );
10442 }
10443 assert_eq!(
10444 &progress.phases[planning_attempts * 2..],
10445 [
10446 ArchiveWritePhase::EmittingPayload,
10447 ArchiveWritePhase::EmittingMetadata,
10448 ]
10449 );
10450 assert_eq!(
10451 progress.bytes_for(ArchiveWritePhase::PlanningPayload),
10452 file_size as u64 * planning_attempts as u64,
10453 );
10454 assert_eq!(
10455 progress.bytes_for(ArchiveWritePhase::EmittingPayload),
10456 file_size as u64,
10457 );
10458 }
10459
10460 #[test]
10461 fn single_pass_progress_reports_emission_phases() {
10462 let file_size = 128 * 1024;
10463 let stats = Rc::new(RefCell::new(GeneratedSourceStats::default()));
10464 let file = GeneratedFileSource {
10465 path: "single/generated.bin",
10466 len: file_size,
10467 stats,
10468 };
10469 let master_key = MasterKey::from_raw_key(&[5u8; 32]).unwrap();
10470 let options = progress_test_writer_options();
10471 let mut sink = MemoryArchiveSink::default();
10472 let mut progress = RecordingWriteProgress::default();
10473
10474 write_archive_sources_to_sink_single_pass_with_progress(
10475 &[file],
10476 &master_key,
10477 options,
10478 &KdfParams::Raw,
10479 None,
10480 None,
10481 &mut sink,
10482 &mut progress,
10483 )
10484 .unwrap();
10485
10486 assert_eq!(
10487 progress.phases,
10488 vec![
10489 ArchiveWritePhase::EmittingPayload,
10490 ArchiveWritePhase::EmittingMetadata,
10491 ]
10492 );
10493 assert_eq!(
10494 progress.bytes_for(ArchiveWritePhase::EmittingPayload),
10495 file_size as u64
10496 );
10497 }
10498
10499 #[test]
10500 fn ordered_parallel_progress_reports_emission_phases() {
10501 let file_size = 128 * 1024;
10502 let stats = Rc::new(RefCell::new(GeneratedSourceStats::default()));
10503 let file = GeneratedFileSource {
10504 path: "parallel/generated.bin",
10505 len: file_size,
10506 stats,
10507 };
10508 let master_key = MasterKey::from_raw_key(&[6u8; 32]).unwrap();
10509 let options = progress_test_writer_options();
10510 let mut sink = MemoryArchiveSink::default();
10511 let mut progress = RecordingWriteProgress::default();
10512
10513 write_archive_sources_to_sink_ordered_parallel_with_progress(
10514 &[file],
10515 &master_key,
10516 options,
10517 &KdfParams::Raw,
10518 None,
10519 None,
10520 &mut sink,
10521 &mut progress,
10522 )
10523 .unwrap();
10524
10525 assert_eq!(
10526 progress.phases,
10527 vec![
10528 ArchiveWritePhase::EmittingPayload,
10529 ArchiveWritePhase::EmittingMetadata,
10530 ]
10531 );
10532 assert_eq!(
10533 progress.bytes_for(ArchiveWritePhase::EmittingPayload),
10534 file_size as u64
10535 );
10536 }
10537
10538 fn progress_test_writer_options() -> WriterOptions {
10539 WriterOptions {
10540 block_size: MIN_BLOCK_SIZE,
10541 chunk_size: 16 * 1024,
10542 envelope_target_size: 64 * 1024,
10543 stripe_width: 1,
10544 volume_loss_tolerance: 0,
10545 bit_rot_buffer_pct: 0,
10546 fec_parity_shards: 0,
10547 index_fec_parity_shards: 0,
10548 index_root_fec_parity_shards: 0,
10549 ..WriterOptions::default()
10550 }
10551 }
10552
10553 #[derive(Default)]
10554 struct RecordingWriteProgress {
10555 phases: Vec<ArchiveWritePhase>,
10556 phase_bytes: BTreeMap<ArchiveWritePhase, u64>,
10557 }
10558
10559 impl RecordingWriteProgress {
10560 fn bytes_for(&self, phase: ArchiveWritePhase) -> u64 {
10561 self.phase_bytes.get(&phase).copied().unwrap_or_default()
10562 }
10563 }
10564
10565 impl ArchiveWriteProgressSink for RecordingWriteProgress {
10566 fn phase_started(&mut self, phase: ArchiveWritePhase) {
10567 self.phases.push(phase);
10568 }
10569
10570 fn source_bytes_read(&mut self, phase: ArchiveWritePhase, _archive_path: &str, bytes: u64) {
10571 let total = self.phase_bytes.entry(phase).or_default();
10572 *total = total.saturating_add(bytes);
10573 }
10574 }
10575
10576 #[derive(Default)]
10577 struct GeneratedSourceStats {
10578 open_count: usize,
10579 total_read: u64,
10580 max_read_request: usize,
10581 }
10582
10583 struct GeneratedFileSource {
10584 path: &'static str,
10585 len: usize,
10586 stats: Rc<RefCell<GeneratedSourceStats>>,
10587 }
10588
10589 impl RegularFileSource for GeneratedFileSource {
10590 fn archive_path(&self) -> &str {
10591 self.path
10592 }
10593
10594 fn file_data_size(&self) -> u64 {
10595 self.len as u64
10596 }
10597
10598 fn mode(&self) -> u32 {
10599 0o644
10600 }
10601
10602 fn mtime(&self) -> ArchiveTimestamp {
10603 ArchiveTimestamp::UNIX_EPOCH
10604 }
10605
10606 fn open(&self) -> Result<Box<dyn Read + '_>, ArchiveWriteError> {
10607 self.stats.borrow_mut().open_count += 1;
10608 Ok(Box::new(GeneratedReader {
10609 remaining: self.len,
10610 position: 0,
10611 stats: Rc::clone(&self.stats),
10612 }))
10613 }
10614 }
10615
10616 struct GeneratedReader {
10617 remaining: usize,
10618 position: usize,
10619 stats: Rc<RefCell<GeneratedSourceStats>>,
10620 }
10621
10622 impl Read for GeneratedReader {
10623 fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
10624 if self.remaining == 0 {
10625 return Ok(0);
10626 }
10627 let count = out.len().min(self.remaining);
10628 for (offset, byte) in out[..count].iter_mut().enumerate() {
10629 let position = self.position + offset;
10630 *byte = position.wrapping_mul(31).wrapping_add(17) as u8;
10631 }
10632 self.position += count;
10633 self.remaining -= count;
10634 let mut stats = self.stats.borrow_mut();
10635 stats.total_read += count as u64;
10636 stats.max_read_request = stats.max_read_request.max(out.len());
10637 Ok(count)
10638 }
10639 }
10640
10641 #[derive(Default)]
10642 struct TrackingArchiveSink {
10643 volume_bytes: Vec<u64>,
10644 bootstrap_sidecar_bytes: u64,
10645 max_write_len: usize,
10646 }
10647
10648 impl ArchiveWriteSink for TrackingArchiveSink {
10649 fn begin_archive(&mut self, volume_count: usize) -> Result<(), ArchiveWriteError> {
10650 self.volume_bytes = vec![0; volume_count];
10651 self.bootstrap_sidecar_bytes = 0;
10652 self.max_write_len = 0;
10653 Ok(())
10654 }
10655
10656 fn write_volume(
10657 &mut self,
10658 volume_index: usize,
10659 bytes: &[u8],
10660 ) -> Result<(), ArchiveWriteError> {
10661 let volume =
10662 self.volume_bytes
10663 .get_mut(volume_index)
10664 .ok_or(FormatError::WriterInvariant(
10665 "tracking sink volume index is out of bounds",
10666 ))?;
10667 *volume += bytes.len() as u64;
10668 self.max_write_len = self.max_write_len.max(bytes.len());
10669 Ok(())
10670 }
10671
10672 fn write_bootstrap_sidecar(&mut self, bytes: &[u8]) -> Result<(), ArchiveWriteError> {
10673 self.bootstrap_sidecar_bytes += bytes.len() as u64;
10674 self.max_write_len = self.max_write_len.max(bytes.len());
10675 Ok(())
10676 }
10677 }
10678
10679 fn deterministic_bytes(len: usize) -> Vec<u8> {
10680 let mut state = 0x4d41_4d45u32;
10681 let mut out = Vec::with_capacity(len);
10682 for _ in 0..len {
10683 state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
10684 out.push((state >> 24) as u8);
10685 }
10686 out
10687 }
10688
10689 fn single_volume_metadata_test_options() -> WriterOptions {
10690 plan_writer_options(WriterOptions {
10691 block_size: MIN_BLOCK_SIZE,
10692 stripe_width: 1,
10693 volume_loss_tolerance: 0,
10694 bit_rot_buffer_pct: 0,
10695 index_root_fec_parity_shards: 0,
10696 ..WriterOptions::default()
10697 })
10698 .unwrap()
10699 }
10700
10701 fn payload_len_for_encrypted_data_blocks(
10702 data_block_count: u32,
10703 options: WriterOptions,
10704 ) -> usize {
10705 assert!(data_block_count > 0);
10706 if data_block_count == 1 {
10707 return 1;
10708 }
10709 let block_size = options.block_size as usize;
10710 (data_block_count as usize - 1) * block_size - options.aead_algo.tag_len() + 1
10711 }
10712
10713 fn assert_path_specific_member_group(group: &[u8]) {
10714 let mut cursor = 0usize;
10715 let mut saw_main = false;
10716 while cursor < group.len() {
10717 let header = &group[cursor..cursor + TAR_BLOCK_LEN];
10718 assert!(
10719 header.iter().any(|byte| *byte != 0),
10720 "writer emitted tar zero block inside member group"
10721 );
10722 let typeflag = header[156];
10723 assert_ne!(typeflag, b'g', "writer emitted global PAX metadata");
10724 assert!(
10725 !matches!(typeflag, b'V' | b'M' | b'N'),
10726 "writer emitted global GNU metadata"
10727 );
10728 assert!(
10729 matches!(typeflag, b'x' | b'0'),
10730 "writer emitted unexpected tar record type {typeflag:?}"
10731 );
10732 if typeflag == b'0' {
10733 saw_main = true;
10734 }
10735 let size = read_test_tar_octal(&header[124..136]);
10736 cursor += TAR_BLOCK_LEN + size + padding_to_512(size);
10737 }
10738 assert_eq!(cursor, group.len());
10739 assert!(saw_main);
10740 }
10741
10742 fn read_test_tar_octal(field: &[u8]) -> usize {
10743 let mut value = 0usize;
10744 for byte in field {
10745 match *byte {
10746 0 | b' ' => break,
10747 b'0'..=b'7' => {
10748 value = value * 8 + usize::from(*byte - b'0');
10749 }
10750 other => panic!("malformed test tar octal byte {other:?}"),
10751 }
10752 }
10753 value
10754 }
10755
10756 fn test_file_row(idx: usize, path_hash: [u8; 8]) -> FileRow {
10757 let path = format!("file-{idx:05}.txt").into_bytes();
10758 FileRow {
10759 path_hash,
10760 path: path.clone(),
10761 member_index: idx,
10762 member: TarMember {
10763 path,
10764 entry_kind: SourceEntryKind::Regular,
10765 link_target: None,
10766 tar_member_group_start: idx as u64 * 512,
10767 tar_member_group_size: 512,
10768 file_data_size: 0,
10769 sparse_extents: None,
10770 mode: 0o644,
10771 mtime: ArchiveTimestamp::UNIX_EPOCH,
10772 portable_metadata: PortableFileMetadata::default(),
10773 },
10774 }
10775 }
10776}