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 uuid::Uuid;
10
11use crate::compression::{
12 compress_zstd_frame_with_dictionary_and_jobs, compress_zstd_frame_with_jobs,
13};
14use crate::crypto::{
15 aead_encrypt, build_aad, compute_integrity_tag, derive_nonce, HmacDomain, KdfParams, MasterKey,
16 Subkeys,
17};
18use crate::fec::encode_parity_gf16;
19use crate::format::{
20 root_auth_spec_id_for_revision, AeadAlgo, ArchiveWriteError, BlockKind, CompressionAlgo,
21 FecAlgo, FormatError, KdfAlgo, BLOCK_RECORD_FRAMING_LEN, BOOTSTRAP_SIDECAR_HEADER_LEN,
22 CRITICAL_RECOVERY_LOCATOR_LEN, CRYPTO_EXTENSION_HEADER_LEN, CRYPTO_HEADER_FIXED_LEN,
23 CRYPTO_HEADER_HMAC_LEN, FORMAT_VERSION, MANIFEST_FOOTER_LEN, READER_MAX_CMRA_PARITY_PCT,
24 READER_MAX_INDEX_ROOT_FEC_CLASS_SHARDS, READER_MAX_ROOT_AUTH_AUTHENTICATOR_VALUE_LEN,
25 READER_MAX_ROOT_AUTH_FOOTER_LEN, READER_MAX_ROOT_AUTH_SIGNER_IDENTITY_LEN,
26 VOLUME_FORMAT_REV_44, VOLUME_HEADER_LEN, VOLUME_TRAILER_LEN,
27};
28use crate::metadata::{
29 hash_prefix, normalize_lookup_file_path, validate_file_path_bytes, DirectoryHintEntry,
30 DirectoryHintShardEntry, DirectoryHintTableHeader, EnvelopeEntry, FileEntry, FrameEntry,
31 IndexRoot, IndexRootHeader, IndexShardHeader, ShardEntry, DIRECTORY_HINT_ENTRY_LEN,
32 DIRECTORY_HINT_TABLE_LEN, ENVELOPE_ENTRY_LEN, FILE_ENTRY_LEN, FRAME_ENTRY_LEN,
33 INDEX_SHARD_HEADER_LEN,
34};
35use crate::padding::suffix_pad_for_aead;
36use crate::root_auth::{
37 archive_root_for_revision, critical_metadata_digest, data_block_merkle_leaf_hash_for_revision,
38 data_block_merkle_root_from_leaf_hashes_for_revision, fec_layout_digest_for_revision,
39 index_digest_for_revision, root_auth_descriptor_digest_for_revision, signer_identity_digest,
40 ArchiveRootInputs, CriticalMetadataDigestInputs, FecLayoutObjectRow,
41};
42use crate::tar_model::TarEntryKind;
43use crate::wire::{
44 compute_key_wrap_table_digest, BlockRecord, BootstrapSidecarHeader, CriticalMetadataImage,
45 CriticalMetadataRecoveryHeader, CriticalMetadataRecoveryShard, CriticalRecoveryLocator,
46 CryptoHeader, CryptoHeaderFixed, KeyWrapTableV1, ManifestFooter, RecipientRecordV1,
47 RootAuthFooterV1, SerializedRegion, VolumeHeader, VolumeTrailer,
48};
49
50const TAR_BLOCK_LEN: usize = 512;
51const MAX_REED_SOLOMON_GF16_SHARDS: u64 = 65_535;
52const MIN_BLOCK_SIZE: u32 = 4096;
53const DEFAULT_BLOCK_SIZE: u32 = 64 * 1024;
54const DEFAULT_CHUNK_SIZE: u32 = 256 * 1024;
55const DEFAULT_ENVELOPE_TARGET_SIZE: u32 = 1024 * 1024;
56const DEFAULT_FEC_DATA_SHARDS: u16 = 224;
57const DEFAULT_FEC_PARITY_SHARDS: u16 = 1;
58const DEFAULT_INDEX_FEC_DATA_SHARDS: u16 = 16;
59const DEFAULT_INDEX_FEC_PARITY_SHARDS: u16 = 1;
60const MIN_INDEX_ROOT_FEC_DATA_SHARDS: u16 = 16;
61const DEFAULT_INDEX_ROOT_FEC_DATA_SHARDS: u16 = MIN_INDEX_ROOT_FEC_DATA_SHARDS;
62const DEFAULT_INDEX_ROOT_FEC_PARITY_SHARDS: u16 = 1;
63const DEFAULT_STRIPE_WIDTH: u32 = 8;
64const DEFAULT_VOLUME_LOSS_TOLERANCE: u8 = 1;
65const DEFAULT_BIT_ROT_BUFFER_PCT: u8 = 5;
66const DEFAULT_FILES_PER_INDEX_SHARD: usize = 10_000;
67const DIRECTORY_HINT_REQUIRED_FILE_COUNT: usize = 100_000;
68const MAX_FILES_PER_INDEX_SHARD: usize = 1_000_000;
69const MAX_HASH_PREFIX_RUN_FILES: usize = 50_000;
70const DEFAULT_DIRECTORY_HINT_ENTRIES_PER_SHARD: usize = 10_000;
71const CMRA_SHARD_SIZE: usize = 512;
72
73#[derive(Clone, Default)]
74pub enum KeyWrapRecordSource {
75 #[default]
76 None,
77 Fixed(Vec<RecipientRecordV1>),
78 Callback(Arc<dyn Fn() -> Result<Vec<RecipientRecordV1>, FormatError> + Send + Sync>),
79}
80
81impl KeyWrapRecordSource {
82 pub fn fixed(records: Vec<RecipientRecordV1>) -> Self {
83 Self::Fixed(records)
84 }
85
86 pub fn callback<F>(callback: F) -> Self
87 where
88 F: Fn() -> Result<Vec<RecipientRecordV1>, FormatError> + Send + Sync + 'static,
89 {
90 Self::Callback(Arc::new(callback))
91 }
92
93 fn resolve(&self) -> Result<Option<Vec<RecipientRecordV1>>, FormatError> {
94 match self {
95 Self::None => Ok(None),
96 Self::Fixed(records) => Ok(Some(records.clone())),
97 Self::Callback(callback) => Ok(Some(callback()?)),
98 }
99 }
100}
101
102fn default_jobs() -> usize {
103 std::thread::available_parallelism()
104 .map(|jobs| jobs.get())
105 .unwrap_or(1)
106}
107
108fn volume_format_revision_for_options(_options: &WriterOptions, _kdf_params: &KdfParams) -> u16 {
109 VOLUME_FORMAT_REV_44
111}
112
113fn resolve_key_wrap_artifacts(
114 kdf_params: &KdfParams,
115 archive_uuid: &[u8; 16],
116 session_id: &[u8; 16],
117 key_wrap_records: Option<&KeyWrapRecordSource>,
118) -> Result<(KdfParams, Option<Vec<u8>>), FormatError> {
119 match kdf_params {
120 KdfParams::RecipientWrap {
121 key_wrap_table_length,
122 key_wrap_table_record_count,
123 key_wrap_table_version,
124 ..
125 } => {
126 if *key_wrap_table_version != 1 {
127 return Err(FormatError::InvalidKdfParams(
128 "recipient-wrap table version must be 1",
129 ));
130 }
131 let Some(records) = key_wrap_records
132 .ok_or(FormatError::WriterUnsupported(
133 "RecipientWrap requires key-wrap records",
134 ))?
135 .resolve()?
136 else {
137 return Err(FormatError::WriterUnsupported(
138 "RecipientWrap requires key-wrap records",
139 ));
140 };
141 if records.is_empty() {
142 return Err(FormatError::WriterUnsupported(
143 "RecipientWrap requires at least one recipient record",
144 ));
145 }
146 let mut recipient_records = Vec::with_capacity(records.len());
147 for record in records {
148 let mut prepared = record.clone();
149 let record_length = u32_len(prepared.to_bytes()?.len(), "RecipientWrap record")?;
150 prepared = prepared.with_record_length(record_length);
151 recipient_records.push(prepared);
152 }
153 let declared_record_count = u32_len(
154 recipient_records.len(),
155 "KeyWrapTableV1 recipient_record_count",
156 )?;
157 if *key_wrap_table_record_count != 0
158 && *key_wrap_table_record_count != declared_record_count
159 {
160 return Err(FormatError::InvalidKdfParams(
161 "recipient-wrap key_wrap_table_record_count mismatch",
162 ));
163 }
164
165 let key_wrap_table = KeyWrapTableV1 {
166 version: *key_wrap_table_version,
167 volume_format_rev: VOLUME_FORMAT_REV_44,
168 table_length: 0,
169 flags: 0,
170 archive_uuid: *archive_uuid,
171 session_id: *session_id,
172 recipient_record_count: declared_record_count,
173 records_offset: 96,
174 records_length: 0,
175 recipient_records,
176 }
177 .to_bytes()?;
178 let computed_key_wrap_table_length =
179 u32_len(key_wrap_table.len(), "KeyWrapTableV1 table_length")?;
180 if *key_wrap_table_length != 0
181 && computed_key_wrap_table_length != *key_wrap_table_length
182 {
183 return Err(FormatError::InvalidKdfParams(
184 "recipient-wrap key_wrap_table_length mismatch",
185 ));
186 }
187 let key_wrap_table_digest =
188 compute_key_wrap_table_digest(computed_key_wrap_table_length, &key_wrap_table);
189 Ok((
190 KdfParams::RecipientWrap {
191 key_wrap_table_length: computed_key_wrap_table_length,
192 key_wrap_table_record_count: declared_record_count,
193 key_wrap_table_version: *key_wrap_table_version,
194 key_wrap_table_digest,
195 },
196 Some(key_wrap_table),
197 ))
198 }
199 _ => Ok((kdf_params.clone(), None)),
200 }
201}
202
203fn recipient_wrap_kdf_params_for_record_count(
204 record_count: usize,
205) -> Result<KdfParams, FormatError> {
206 Ok(KdfParams::RecipientWrap {
207 key_wrap_table_length: 0,
208 key_wrap_table_record_count: u32_len(
209 record_count,
210 "KeyWrapTableV1 recipient_record_count",
211 )?,
212 key_wrap_table_version: 1,
213 key_wrap_table_digest: [0u8; 32],
214 })
215}
216
217fn stabilized_key_wrap_record_source(
218 kdf_params: &KdfParams,
219 key_wrap_records: Option<&KeyWrapRecordSource>,
220) -> Result<Option<KeyWrapRecordSource>, FormatError> {
221 if !matches!(kdf_params, KdfParams::RecipientWrap { .. }) {
222 return Ok(None);
223 }
224 let Some(records) = key_wrap_records
225 .ok_or(FormatError::WriterUnsupported(
226 "RecipientWrap requires key-wrap records",
227 ))?
228 .resolve()?
229 else {
230 return Err(FormatError::WriterUnsupported(
231 "RecipientWrap requires key-wrap records",
232 ));
233 };
234 Ok(Some(KeyWrapRecordSource::fixed(records)))
235}
236
237fn should_emit_directory_hints(file_count: usize) -> bool {
238 file_count > DIRECTORY_HINT_REQUIRED_FILE_COUNT
239}
240
241#[derive(Debug, Clone, Copy, PartialEq, Eq)]
242pub struct WriterOptions {
243 pub block_size: u32,
244 pub chunk_size: u32,
245 pub envelope_target_size: u32,
246 pub stripe_width: u32,
247 pub volume_loss_tolerance: u8,
248 pub bit_rot_buffer_pct: u8,
249 pub zstd_level: i32,
250 pub jobs: usize,
251 pub aead_algo: AeadAlgo,
252 pub fec_data_shards: u16,
253 pub fec_parity_shards: u16,
254 pub index_fec_data_shards: u16,
255 pub index_fec_parity_shards: u16,
256 pub index_root_fec_data_shards: u16,
257 pub index_root_fec_parity_shards: u16,
258 pub max_path_length: u32,
259 pub target_volume_size: Option<u64>,
260 pub archive_uuid: Option<[u8; 16]>,
261 pub session_id: Option<[u8; 16]>,
262 pub closed_at_ns: i64,
263}
264
265impl Default for WriterOptions {
266 fn default() -> Self {
267 Self {
268 block_size: DEFAULT_BLOCK_SIZE,
269 chunk_size: DEFAULT_CHUNK_SIZE,
270 envelope_target_size: DEFAULT_ENVELOPE_TARGET_SIZE,
271 stripe_width: DEFAULT_STRIPE_WIDTH,
272 volume_loss_tolerance: DEFAULT_VOLUME_LOSS_TOLERANCE,
273 bit_rot_buffer_pct: DEFAULT_BIT_ROT_BUFFER_PCT,
274 zstd_level: 3,
275 jobs: default_jobs(),
276 aead_algo: AeadAlgo::AesGcmSiv256,
277 fec_data_shards: DEFAULT_FEC_DATA_SHARDS,
278 fec_parity_shards: DEFAULT_FEC_PARITY_SHARDS,
279 index_fec_data_shards: DEFAULT_INDEX_FEC_DATA_SHARDS,
280 index_fec_parity_shards: DEFAULT_INDEX_FEC_PARITY_SHARDS,
281 index_root_fec_data_shards: DEFAULT_INDEX_ROOT_FEC_DATA_SHARDS,
282 index_root_fec_parity_shards: DEFAULT_INDEX_ROOT_FEC_PARITY_SHARDS,
283 max_path_length: 4096,
284 target_volume_size: None,
285 archive_uuid: None,
286 session_id: None,
287 closed_at_ns: 0,
288 }
289 }
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Eq)]
293pub struct RootAuthWriterConfig<'a> {
294 pub authenticator_id: u16,
295 pub signer_identity_type: u16,
296 pub signer_identity: &'a [u8],
297 pub authenticator_value_length: u32,
298}
299
300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301pub struct RootAuthSigningRequest {
302 pub root_auth_spec_id: [u8; 24],
303 pub archive_uuid: [u8; 16],
304 pub session_id: [u8; 16],
305 pub archive_root: [u8; 32],
306}
307
308pub type RootAuthAuthenticator<'a> =
309 dyn FnMut(&RootAuthSigningRequest) -> Result<Vec<u8>, FormatError> + 'a;
310
311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
312pub struct RegularFile<'a> {
313 pub path: &'a str,
314 pub contents: &'a [u8],
315 pub mode: u32,
316 pub mtime: u64,
317}
318
319impl<'a> RegularFile<'a> {
320 pub fn new(path: &'a str, contents: &'a [u8]) -> Self {
321 Self {
322 path,
323 contents,
324 mode: 0o644,
325 mtime: 0,
326 }
327 }
328}
329
330pub trait RegularFileSource {
335 fn archive_path(&self) -> &str;
336 fn file_data_size(&self) -> u64;
337 fn mode(&self) -> u32;
338 fn mtime(&self) -> u64;
339 fn open(&self) -> Result<Box<dyn Read + '_>, ArchiveWriteError>;
340}
341
342impl RegularFileSource for RegularFile<'_> {
343 fn archive_path(&self) -> &str {
344 self.path
345 }
346
347 fn file_data_size(&self) -> u64 {
348 self.contents.len() as u64
349 }
350
351 fn mode(&self) -> u32 {
352 self.mode
353 }
354
355 fn mtime(&self) -> u64 {
356 self.mtime
357 }
358
359 fn open(&self) -> Result<Box<dyn Read + '_>, ArchiveWriteError> {
360 Ok(Box::new(Cursor::new(self.contents)))
361 }
362}
363
364pub trait ArchiveWriteProgressSink {
370 fn source_bytes_read(&mut self, archive_path: &str, bytes: u64);
372}
373
374impl<F> ArchiveWriteProgressSink for F
375where
376 F: FnMut(&str, u64),
377{
378 fn source_bytes_read(&mut self, archive_path: &str, bytes: u64) {
379 self(archive_path, bytes);
380 }
381}
382
383struct SourceProgressState<'a> {
384 sink: &'a mut dyn ArchiveWriteProgressSink,
385 reported_by_path: BTreeMap<String, u64>,
386}
387
388impl<'a> SourceProgressState<'a> {
389 fn new(sink: &'a mut dyn ArchiveWriteProgressSink) -> Self {
390 Self {
391 sink,
392 reported_by_path: BTreeMap::new(),
393 }
394 }
395
396 fn record(&mut self, archive_path: &str, bytes: u64, file_data_size: u64) {
397 if bytes == 0 || file_data_size == 0 {
398 return;
399 }
400 let reported = self
401 .reported_by_path
402 .entry(archive_path.to_owned())
403 .or_default();
404 let capped_next = reported.saturating_add(bytes).min(file_data_size);
405 let delta = capped_next.saturating_sub(*reported);
406 if delta == 0 {
407 return;
408 }
409 *reported = capped_next;
410 self.sink.source_bytes_read(archive_path, delta);
411 }
412}
413
414struct ProgressRegularFileSource<'a, S> {
415 inner: &'a S,
416 state: Rc<RefCell<SourceProgressState<'a>>>,
417}
418
419impl<S: RegularFileSource> RegularFileSource for ProgressRegularFileSource<'_, S> {
420 fn archive_path(&self) -> &str {
421 self.inner.archive_path()
422 }
423
424 fn file_data_size(&self) -> u64 {
425 self.inner.file_data_size()
426 }
427
428 fn mode(&self) -> u32 {
429 self.inner.mode()
430 }
431
432 fn mtime(&self) -> u64 {
433 self.inner.mtime()
434 }
435
436 fn open(&self) -> Result<Box<dyn Read + '_>, ArchiveWriteError> {
437 Ok(Box::new(SourceProgressReader {
438 inner: self.inner.open()?,
439 archive_path: self.inner.archive_path().to_owned(),
440 file_data_size: self.inner.file_data_size(),
441 state: Rc::clone(&self.state),
442 }))
443 }
444}
445
446struct SourceProgressReader<'a> {
447 inner: Box<dyn Read + 'a>,
448 archive_path: String,
449 file_data_size: u64,
450 state: Rc<RefCell<SourceProgressState<'a>>>,
451}
452
453impl Read for SourceProgressReader<'_> {
454 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
455 let bytes = self.inner.read(buf)?;
456 self.state
457 .borrow_mut()
458 .record(&self.archive_path, bytes as u64, self.file_data_size);
459 Ok(bytes)
460 }
461}
462
463fn progress_sources<'a, S: RegularFileSource>(
464 files: &'a [S],
465 sink: &'a mut dyn ArchiveWriteProgressSink,
466) -> Vec<ProgressRegularFileSource<'a, S>> {
467 let state = Rc::new(RefCell::new(SourceProgressState::new(sink)));
468 files
469 .iter()
470 .map(|inner| ProgressRegularFileSource {
471 inner,
472 state: Rc::clone(&state),
473 })
474 .collect()
475}
476
477pub trait ArchiveWriteSink {
482 fn begin_archive(&mut self, volume_count: usize) -> Result<(), ArchiveWriteError>;
483 fn write_volume(&mut self, volume_index: usize, bytes: &[u8]) -> Result<(), ArchiveWriteError>;
484 fn write_bootstrap_sidecar(&mut self, bytes: &[u8]) -> Result<(), ArchiveWriteError>;
485}
486
487#[derive(Debug, Clone, PartialEq, Eq)]
488pub struct WrittenArchiveSummary {
489 pub volume_count: usize,
490 pub archive_bytes: u64,
491 pub bootstrap_sidecar_bytes: u64,
492 pub archive_uuid: [u8; 16],
493 pub session_id: [u8; 16],
494 pub timings: WriterTimings,
495}
496
497#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
498pub struct WriterTimings {
499 pub total: Duration,
500 pub plan_payload: Duration,
501 pub plan_metadata: Duration,
502 pub emit_payload: Duration,
503 pub emit_metadata: Duration,
504}
505
506impl WriterTimings {
507 fn add_assign(&mut self, other: Self) {
508 self.total += other.total;
509 self.plan_payload += other.plan_payload;
510 self.plan_metadata += other.plan_metadata;
511 self.emit_payload += other.emit_payload;
512 self.emit_metadata += other.emit_metadata;
513 }
514}
515
516#[derive(Debug, Clone, Default, PartialEq, Eq)]
518pub struct MemoryArchiveSink {
519 pub volumes: Vec<Vec<u8>>,
520 pub bootstrap_sidecar: Vec<u8>,
521}
522
523impl ArchiveWriteSink for MemoryArchiveSink {
524 fn begin_archive(&mut self, volume_count: usize) -> Result<(), ArchiveWriteError> {
525 self.volumes = vec![Vec::new(); volume_count];
526 self.bootstrap_sidecar.clear();
527 Ok(())
528 }
529
530 fn write_volume(&mut self, volume_index: usize, bytes: &[u8]) -> Result<(), ArchiveWriteError> {
531 let volume = self
532 .volumes
533 .get_mut(volume_index)
534 .ok_or(FormatError::WriterInvariant(
535 "volume sink index is out of bounds",
536 ))?;
537 volume.extend_from_slice(bytes);
538 Ok(())
539 }
540
541 fn write_bootstrap_sidecar(&mut self, bytes: &[u8]) -> Result<(), ArchiveWriteError> {
542 self.bootstrap_sidecar.extend_from_slice(bytes);
543 Ok(())
544 }
545}
546
547#[derive(Debug, Clone, PartialEq, Eq)]
552pub struct WrittenArchive {
553 pub bytes: Vec<u8>,
554 pub volumes: Vec<Vec<u8>>,
555 pub bootstrap_sidecar: Vec<u8>,
556 pub archive_uuid: [u8; 16],
557 pub session_id: [u8; 16],
558 pub timings: WriterTimings,
559}
560
561#[derive(Debug, Clone)]
562struct TarMember {
563 path: Vec<u8>,
564 tar_member_group_start: u64,
565 tar_member_group_size: u64,
566 file_data_size: u64,
567 kind: TarEntryKind,
568 mode: u32,
569 mtime: u64,
570}
571
572#[derive(Debug, Clone)]
573struct PayloadFrame {
574 frame_index: u64,
575 envelope_index: u64,
576 member_index: usize,
577 offset_in_envelope: u32,
578 compressed_size: u32,
579 decompressed_size: u32,
580 flags: u32,
581 tar_stream_offset: u64,
582}
583
584#[derive(Debug, Clone)]
585struct FileRow {
586 path_hash: [u8; 8],
587 path: Vec<u8>,
588 member_index: usize,
589 member: TarMember,
590}
591
592#[derive(Debug, Clone)]
593struct PlannedIndexShard {
594 shard_index: u64,
595 plaintext: Vec<u8>,
596 file_count: u32,
597 first_path_hash: [u8; 8],
598 last_path_hash: [u8; 8],
599}
600
601#[derive(Debug, Clone)]
602struct PlannedDirectoryHintShard {
603 hint_shard_index: u64,
604 plaintext: Vec<u8>,
605 entry_count: u64,
606 first_dir_hash: [u8; 8],
607 last_dir_hash: [u8; 8],
608}
609
610#[derive(Debug, Clone)]
611#[cfg(test)]
612struct PayloadEnvelope {
613 envelope_index: u64,
614 plaintext: Vec<u8>,
615}
616
617#[derive(Debug, Clone)]
618struct PayloadObject {
619 envelope_index: u64,
620 plaintext_size: u32,
621 object: ObjectExtent,
622}
623
624#[derive(Debug, Clone)]
625struct EncryptedObject {
626 first_block_index: u64,
627 data_block_count: u32,
628 parity_block_count: u32,
629 encrypted_size: u32,
630 records: Vec<BlockRecord>,
631}
632
633#[derive(Debug, Clone, Copy, PartialEq, Eq)]
634struct ObjectExtent {
635 first_block_index: u64,
636 data_block_count: u32,
637 parity_block_count: u32,
638 encrypted_size: u32,
639}
640
641#[derive(Debug, Clone, Copy, PartialEq, Eq)]
642struct PlannedEncryptedObject {
643 data_block_count: u32,
644 parity_block_count: u32,
645 encrypted_size: u32,
646}
647
648#[derive(Debug, Clone, Copy, PartialEq, Eq)]
649enum MetadataObjectKind {
650 IndexRoot,
651 Dictionary,
652}
653
654impl MetadataObjectKind {
655 fn too_large_error(self) -> FormatError {
656 match self {
657 Self::IndexRoot => FormatError::WriterUnsupported("IndexRoot too large"),
658 Self::Dictionary => FormatError::WriterUnsupported("dictionary object too large"),
659 }
660 }
661}
662
663impl ObjectExtent {
664 fn new(first_block_index: u64, plan: PlannedEncryptedObject) -> Result<Self, FormatError> {
665 Ok(Self {
666 first_block_index,
667 data_block_count: plan.data_block_count,
668 parity_block_count: plan.parity_block_count,
669 encrypted_size: plan.encrypted_size,
670 })
671 }
672
673 fn next_block_index(self) -> Result<u64, FormatError> {
674 checked_u64_add(
675 self.first_block_index,
676 self.data_block_count as u64 + self.parity_block_count as u64,
677 "next_block_index",
678 )
679 }
680}
681
682impl From<&EncryptedObject> for ObjectExtent {
683 fn from(object: &EncryptedObject) -> Self {
684 Self {
685 first_block_index: object.first_block_index,
686 data_block_count: object.data_block_count,
687 parity_block_count: object.parity_block_count,
688 encrypted_size: object.encrypted_size,
689 }
690 }
691}
692
693#[derive(Debug, Clone)]
694struct PlannedDirectoryHintObject {
695 hint_shard_index: u64,
696 compressed: Vec<u8>,
697 extent: ObjectExtent,
698}
699
700struct WriterPlan {
701 options: WriterOptions,
702 archive_uuid: [u8; 16],
703 session_id: [u8; 16],
704 crypto_header: Vec<u8>,
705 tar_members: Vec<TarMember>,
706 frames: Vec<PayloadFrame>,
707 payload_objects: Vec<PayloadObject>,
708 index_root_plaintext: Vec<u8>,
709 compressed_index_root: Vec<u8>,
710 index_root_extent: ObjectExtent,
711 index_shard_objects: Vec<PlannedIndexShardObject>,
712 shard_entries: Vec<ShardEntry>,
713 volume_format_rev: u16,
714 compressed_dictionary: Option<Vec<u8>>,
715 dictionary_extent: Option<(ObjectExtent, u32)>,
716 directory_hint_objects: Vec<PlannedDirectoryHintObject>,
717 directory_hint_entries: Vec<DirectoryHintShardEntry>,
718 root_auth_footer_length: Option<u32>,
719 key_wrap_table: Option<Vec<u8>>,
720 block_records_offset: u64,
721 total_block_count: u64,
722}
723
724struct PlannedIndexShardObject {
725 shard_index: u64,
726 compressed: Vec<u8>,
727 extent: ObjectExtent,
728}
729
730struct PayloadPlanning {
731 tar_members: Vec<TarMember>,
732 frames: Vec<PayloadFrame>,
733 payload_objects: Vec<PayloadObject>,
734 payload_block_count: u64,
735 tar_total_size: u64,
736 content_sha256: [u8; 32],
737}
738
739struct PayloadEnvelopeBuilder {
740 envelope_index: u64,
741 plaintext: Vec<u8>,
742}
743
744#[derive(Debug, Clone, Copy)]
745struct PayloadFrameMetadataInput {
746 frame_index: u64,
747 envelope_index: u64,
748 member_index: usize,
749 offset_in_envelope: u32,
750 compressed_size: usize,
751 decompressed_size: usize,
752 member_start: u64,
753 member_offset: u64,
754 member_group_size: u64,
755}
756
757fn payload_envelope_needs_flush(
758 envelope: &PayloadEnvelopeBuilder,
759 frame_len: usize,
760 options: WriterOptions,
761) -> Result<bool, FormatError> {
762 let next_len = checked_usize_add(envelope.plaintext.len(), frame_len, "payload")?;
763 Ok(!envelope.plaintext.is_empty()
764 && (next_len > options.envelope_target_size as usize
765 || !payload_object_can_fit(next_len, options)?))
766}
767
768fn payload_frame_metadata(input: PayloadFrameMetadataInput) -> Result<PayloadFrame, FormatError> {
769 let mut flags = 0u32;
770 if input.member_offset == 0 {
771 flags |= 0x0000_0001;
772 }
773 if checked_u64_add(
774 input.member_offset,
775 input.decompressed_size as u64,
776 "payload chunk",
777 )? == input.member_group_size
778 {
779 flags |= 0x0000_0002;
780 }
781 Ok(PayloadFrame {
782 frame_index: input.frame_index,
783 envelope_index: input.envelope_index,
784 member_index: input.member_index,
785 offset_in_envelope: input.offset_in_envelope,
786 compressed_size: u32_len(input.compressed_size, "FrameEntry.compressed_size")?,
787 decompressed_size: u32_len(input.decompressed_size, "FrameEntry.decompressed_size")?,
788 flags,
789 tar_stream_offset: checked_u64_add(
790 input.member_start,
791 input.member_offset,
792 "PayloadFrame.tar_stream_offset",
793 )?,
794 })
795}
796
797#[derive(Debug, Clone, PartialEq, Eq)]
798pub(crate) struct StreamingRegularMember {
799 pub archive_path: Vec<u8>,
800 pub file_data_size: u64,
801 pub mode: u32,
802 pub mtime: u64,
803}
804
805struct WriterEmissionState {
806 volume_headers: Vec<[u8; VOLUME_HEADER_LEN]>,
807 bytes_written: Vec<u64>,
808 record_counts: Vec<u64>,
809 volume_format_rev: u16,
810 data_leaf_hashes: Option<Vec<(u64, [u8; 32])>>,
811 next_block_index: u64,
812}
813
814pub(crate) struct StreamingArchiveWriter<'a, O: ArchiveWriteSink> {
815 sink: &'a mut O,
816 options: WriterOptions,
817 archive_uuid: [u8; 16],
818 session_id: [u8; 16],
819 crypto_header: Vec<u8>,
820 subkeys: Subkeys,
821 tar_members: Vec<TarMember>,
822 frames: Vec<PayloadFrame>,
823 payload_objects: Vec<PayloadObject>,
824 payload_block_count: u64,
825 tar_total_size: u64,
826 hasher: Sha256,
827 next_frame_index: u64,
828 envelope: PayloadEnvelopeBuilder,
829 emission_state: WriterEmissionState,
830}
831
832pub fn write_archive(
833 files: &[RegularFile<'_>],
834 master_key: &MasterKey,
835 options: WriterOptions,
836) -> Result<WrittenArchive, FormatError> {
837 write_archive_inner(
838 files,
839 master_key,
840 options,
841 None,
842 &KdfParams::Raw,
843 None,
844 None,
845 None,
846 )
847}
848
849pub fn write_archive_unencrypted(
850 files: &[RegularFile<'_>],
851 mut options: WriterOptions,
852) -> Result<WrittenArchive, FormatError> {
853 options.aead_algo = AeadAlgo::None;
854 let placeholder = MasterKey::from_raw_key(&[0; 32])?;
855 write_archive_inner(
856 files,
857 &placeholder,
858 options,
859 None,
860 &KdfParams::None,
861 None,
862 None,
863 None,
864 )
865}
866
867pub fn write_archive_with_kdf(
868 files: &[RegularFile<'_>],
869 master_key: &MasterKey,
870 options: WriterOptions,
871 kdf_params: &KdfParams,
872) -> Result<WrittenArchive, FormatError> {
873 write_archive_inner(
874 files, master_key, options, None, kdf_params, None, None, None,
875 )
876}
877
878pub fn write_archive_with_recipient_wrap_records(
879 files: &[RegularFile<'_>],
880 master_key: &MasterKey,
881 options: WriterOptions,
882 records: Vec<RecipientRecordV1>,
883) -> Result<WrittenArchive, FormatError> {
884 let kdf_params = recipient_wrap_kdf_params_for_record_count(records.len())?;
885 let key_wrap_records = KeyWrapRecordSource::fixed(records);
886 write_archive_inner(
887 files,
888 master_key,
889 options,
890 None,
891 &kdf_params,
892 None,
893 None,
894 Some(&key_wrap_records),
895 )
896}
897
898pub fn write_archive_with_root_auth_and_recipient_wrap_records<F>(
899 files: &[RegularFile<'_>],
900 master_key: &MasterKey,
901 options: WriterOptions,
902 records: Vec<RecipientRecordV1>,
903 root_auth: RootAuthWriterConfig<'_>,
904 mut authenticator: F,
905) -> Result<WrittenArchive, FormatError>
906where
907 F: FnMut(&RootAuthSigningRequest) -> Result<Vec<u8>, FormatError>,
908{
909 let kdf_params = recipient_wrap_kdf_params_for_record_count(records.len())?;
910 let key_wrap_records = KeyWrapRecordSource::fixed(records);
911 write_archive_inner(
912 files,
913 master_key,
914 options,
915 None,
916 &kdf_params,
917 Some(root_auth),
918 Some(&mut authenticator),
919 Some(&key_wrap_records),
920 )
921}
922
923pub fn write_archive_with_root_auth<F>(
924 files: &[RegularFile<'_>],
925 master_key: &MasterKey,
926 options: WriterOptions,
927 root_auth: RootAuthWriterConfig<'_>,
928 mut authenticator: F,
929) -> Result<WrittenArchive, FormatError>
930where
931 F: FnMut(&RootAuthSigningRequest) -> Result<Vec<u8>, FormatError>,
932{
933 let kdf_params = if options.aead_algo.is_encrypted() {
934 KdfParams::Raw
935 } else {
936 KdfParams::None
937 };
938 write_archive_inner(
939 files,
940 master_key,
941 options,
942 None,
943 &kdf_params,
944 Some(root_auth),
945 Some(&mut authenticator),
946 None,
947 )
948}
949
950pub fn write_archive_with_root_auth_and_kdf<F>(
951 files: &[RegularFile<'_>],
952 master_key: &MasterKey,
953 options: WriterOptions,
954 kdf_params: &KdfParams,
955 root_auth: RootAuthWriterConfig<'_>,
956 mut authenticator: F,
957) -> Result<WrittenArchive, FormatError>
958where
959 F: FnMut(&RootAuthSigningRequest) -> Result<Vec<u8>, FormatError>,
960{
961 write_archive_inner(
962 files,
963 master_key,
964 options,
965 None,
966 kdf_params,
967 Some(root_auth),
968 Some(&mut authenticator),
969 None,
970 )
971}
972
973pub fn write_archive_with_dictionary_and_root_auth<F>(
974 files: &[RegularFile<'_>],
975 master_key: &MasterKey,
976 options: WriterOptions,
977 dictionary: &[u8],
978 root_auth: RootAuthWriterConfig<'_>,
979 mut authenticator: F,
980) -> Result<WrittenArchive, FormatError>
981where
982 F: FnMut(&RootAuthSigningRequest) -> Result<Vec<u8>, FormatError>,
983{
984 write_archive_inner(
985 files,
986 master_key,
987 options,
988 Some(dictionary),
989 &KdfParams::Raw,
990 Some(root_auth),
991 Some(&mut authenticator),
992 None,
993 )
994}
995
996pub fn write_archive_with_dictionary_kdf_and_root_auth<F>(
997 files: &[RegularFile<'_>],
998 master_key: &MasterKey,
999 options: WriterOptions,
1000 dictionary: &[u8],
1001 kdf_params: &KdfParams,
1002 root_auth: RootAuthWriterConfig<'_>,
1003 mut authenticator: F,
1004) -> Result<WrittenArchive, FormatError>
1005where
1006 F: FnMut(&RootAuthSigningRequest) -> Result<Vec<u8>, FormatError>,
1007{
1008 write_archive_inner(
1009 files,
1010 master_key,
1011 options,
1012 Some(dictionary),
1013 kdf_params,
1014 Some(root_auth),
1015 Some(&mut authenticator),
1016 None,
1017 )
1018}
1019
1020pub fn write_archive_with_dictionary(
1021 files: &[RegularFile<'_>],
1022 master_key: &MasterKey,
1023 options: WriterOptions,
1024 dictionary: &[u8],
1025) -> Result<WrittenArchive, FormatError> {
1026 if dictionary.is_empty() {
1027 return Err(FormatError::WriterUnsupported(
1028 "dictionary archives require a non-empty dictionary",
1029 ));
1030 }
1031 if files.is_empty() {
1032 return Err(FormatError::WriterUnsupported(
1033 "dictionary archives require at least one file",
1034 ));
1035 }
1036 if dictionary.len() > u32::MAX as usize {
1037 return Err(FormatError::WriterUnsupported(
1038 "dictionary decompressed size exceeds u32",
1039 ));
1040 }
1041 write_archive_inner(
1042 files,
1043 master_key,
1044 options,
1045 Some(dictionary),
1046 &KdfParams::Raw,
1047 None,
1048 None,
1049 None,
1050 )
1051}
1052
1053pub fn write_archive_with_dictionary_and_kdf(
1054 files: &[RegularFile<'_>],
1055 master_key: &MasterKey,
1056 options: WriterOptions,
1057 dictionary: &[u8],
1058 kdf_params: &KdfParams,
1059) -> Result<WrittenArchive, FormatError> {
1060 if dictionary.is_empty() {
1061 return Err(FormatError::WriterUnsupported(
1062 "dictionary archives require a non-empty dictionary",
1063 ));
1064 }
1065 if files.is_empty() {
1066 return Err(FormatError::WriterUnsupported(
1067 "dictionary archives require at least one file",
1068 ));
1069 }
1070 if dictionary.len() > u32::MAX as usize {
1071 return Err(FormatError::WriterUnsupported(
1072 "dictionary decompressed size exceeds u32",
1073 ));
1074 }
1075 write_archive_inner(
1076 files,
1077 master_key,
1078 options,
1079 Some(dictionary),
1080 kdf_params,
1081 None,
1082 None,
1083 None,
1084 )
1085}
1086
1087#[allow(clippy::too_many_arguments)]
1088pub fn write_archive_sources_to_sink<S, O>(
1089 files: &[S],
1090 master_key: &MasterKey,
1091 options: WriterOptions,
1092 dictionary: Option<&[u8]>,
1093 kdf_params: &KdfParams,
1094 root_auth: Option<RootAuthWriterConfig<'_>>,
1095 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
1096 sink: &mut O,
1097) -> Result<WrittenArchiveSummary, ArchiveWriteError>
1098where
1099 S: RegularFileSource,
1100 O: ArchiveWriteSink,
1101{
1102 write_archive_stream_inner(
1103 files,
1104 master_key,
1105 options,
1106 dictionary,
1107 kdf_params,
1108 root_auth,
1109 authenticator,
1110 None,
1111 sink,
1112 )
1113}
1114
1115#[allow(clippy::too_many_arguments)]
1116pub fn write_archive_sources_to_sink_with_progress<S, O>(
1117 files: &[S],
1118 master_key: &MasterKey,
1119 options: WriterOptions,
1120 dictionary: Option<&[u8]>,
1121 kdf_params: &KdfParams,
1122 root_auth: Option<RootAuthWriterConfig<'_>>,
1123 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
1124 sink: &mut O,
1125 progress: &mut dyn ArchiveWriteProgressSink,
1126) -> Result<WrittenArchiveSummary, ArchiveWriteError>
1127where
1128 S: RegularFileSource,
1129 O: ArchiveWriteSink,
1130{
1131 let progress_files = progress_sources(files, progress);
1132 write_archive_stream_inner(
1133 &progress_files,
1134 master_key,
1135 options,
1136 dictionary,
1137 kdf_params,
1138 root_auth,
1139 authenticator,
1140 None,
1141 sink,
1142 )
1143}
1144
1145#[allow(clippy::too_many_arguments)]
1146pub fn write_archive_sources_to_sink_single_pass<S, O>(
1147 files: &[S],
1148 master_key: &MasterKey,
1149 options: WriterOptions,
1150 kdf_params: &KdfParams,
1151 root_auth: Option<RootAuthWriterConfig<'_>>,
1152 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
1153 sink: &mut O,
1154) -> Result<WrittenArchiveSummary, ArchiveWriteError>
1155where
1156 S: RegularFileSource,
1157 O: ArchiveWriteSink,
1158{
1159 write_single_pass_archive_to_sink(
1160 master_key,
1161 options,
1162 kdf_params,
1163 root_auth,
1164 authenticator,
1165 None,
1166 sink,
1167 |writer| {
1168 for file in files {
1169 let archive_path =
1170 normalize_lookup_file_path(file.archive_path(), options.max_path_length)?;
1171 let mut reader = file.open()?;
1172 writer.write_regular_member_from_reader(
1173 StreamingRegularMember {
1174 archive_path,
1175 file_data_size: file.file_data_size(),
1176 mode: file.mode(),
1177 mtime: file.mtime(),
1178 },
1179 reader.as_mut(),
1180 )?;
1181 }
1182 Ok(())
1183 },
1184 )
1185}
1186
1187#[allow(clippy::too_many_arguments)]
1188pub fn write_archive_sources_to_sink_single_pass_with_recipient_wrap_records<S, O>(
1189 files: &[S],
1190 master_key: &MasterKey,
1191 options: WriterOptions,
1192 records: Vec<RecipientRecordV1>,
1193 root_auth: Option<RootAuthWriterConfig<'_>>,
1194 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
1195 sink: &mut O,
1196) -> Result<WrittenArchiveSummary, ArchiveWriteError>
1197where
1198 S: RegularFileSource,
1199 O: ArchiveWriteSink,
1200{
1201 let kdf_params = recipient_wrap_kdf_params_for_record_count(records.len())?;
1202 let key_wrap_records = KeyWrapRecordSource::fixed(records);
1203 write_single_pass_archive_to_sink(
1204 master_key,
1205 options,
1206 &kdf_params,
1207 root_auth,
1208 authenticator,
1209 Some(&key_wrap_records),
1210 sink,
1211 |writer| {
1212 for file in files {
1213 let archive_path =
1214 normalize_lookup_file_path(file.archive_path(), options.max_path_length)?;
1215 let mut reader = file.open()?;
1216 writer.write_regular_member_from_reader(
1217 StreamingRegularMember {
1218 archive_path,
1219 file_data_size: file.file_data_size(),
1220 mode: file.mode(),
1221 mtime: file.mtime(),
1222 },
1223 reader.as_mut(),
1224 )?;
1225 }
1226 Ok(())
1227 },
1228 )
1229}
1230
1231#[allow(clippy::too_many_arguments)]
1232pub fn write_archive_sources_to_sink_single_pass_with_progress<S, O>(
1233 files: &[S],
1234 master_key: &MasterKey,
1235 options: WriterOptions,
1236 kdf_params: &KdfParams,
1237 root_auth: Option<RootAuthWriterConfig<'_>>,
1238 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
1239 sink: &mut O,
1240 progress: &mut dyn ArchiveWriteProgressSink,
1241) -> Result<WrittenArchiveSummary, ArchiveWriteError>
1242where
1243 S: RegularFileSource,
1244 O: ArchiveWriteSink,
1245{
1246 let progress_files = progress_sources(files, progress);
1247 write_archive_sources_to_sink_single_pass(
1248 &progress_files,
1249 master_key,
1250 options,
1251 kdf_params,
1252 root_auth,
1253 authenticator,
1254 sink,
1255 )
1256}
1257
1258#[allow(clippy::too_many_arguments)]
1259pub fn write_archive_sources_to_sink_ordered_parallel<S, O>(
1260 files: &[S],
1261 master_key: &MasterKey,
1262 options: WriterOptions,
1263 kdf_params: &KdfParams,
1264 root_auth: Option<RootAuthWriterConfig<'_>>,
1265 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
1266 sink: &mut O,
1267) -> Result<WrittenArchiveSummary, ArchiveWriteError>
1268where
1269 S: RegularFileSource,
1270 O: ArchiveWriteSink,
1271{
1272 write_ordered_parallel_archive_to_sink(
1273 files,
1274 master_key,
1275 options,
1276 kdf_params,
1277 root_auth,
1278 authenticator,
1279 None,
1280 sink,
1281 )
1282}
1283
1284#[allow(clippy::too_many_arguments)]
1285pub fn write_archive_sources_to_sink_ordered_parallel_with_recipient_wrap_records<S, O>(
1286 files: &[S],
1287 master_key: &MasterKey,
1288 options: WriterOptions,
1289 records: Vec<RecipientRecordV1>,
1290 root_auth: Option<RootAuthWriterConfig<'_>>,
1291 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
1292 sink: &mut O,
1293) -> Result<WrittenArchiveSummary, ArchiveWriteError>
1294where
1295 S: RegularFileSource,
1296 O: ArchiveWriteSink,
1297{
1298 let kdf_params = recipient_wrap_kdf_params_for_record_count(records.len())?;
1299 let key_wrap_records = KeyWrapRecordSource::fixed(records);
1300 write_ordered_parallel_archive_to_sink(
1301 files,
1302 master_key,
1303 options,
1304 &kdf_params,
1305 root_auth,
1306 authenticator,
1307 Some(&key_wrap_records),
1308 sink,
1309 )
1310}
1311
1312#[allow(clippy::too_many_arguments)]
1313pub fn write_archive_sources_to_sink_ordered_parallel_with_progress<S, O>(
1314 files: &[S],
1315 master_key: &MasterKey,
1316 options: WriterOptions,
1317 kdf_params: &KdfParams,
1318 root_auth: Option<RootAuthWriterConfig<'_>>,
1319 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
1320 sink: &mut O,
1321 progress: &mut dyn ArchiveWriteProgressSink,
1322) -> Result<WrittenArchiveSummary, ArchiveWriteError>
1323where
1324 S: RegularFileSource,
1325 O: ArchiveWriteSink,
1326{
1327 let progress_files = progress_sources(files, progress);
1328 write_ordered_parallel_archive_to_sink(
1329 &progress_files,
1330 master_key,
1331 options,
1332 kdf_params,
1333 root_auth,
1334 authenticator,
1335 None,
1336 sink,
1337 )
1338}
1339
1340#[allow(clippy::too_many_arguments)]
1341fn write_archive_inner(
1342 files: &[RegularFile<'_>],
1343 master_key: &MasterKey,
1344 options: WriterOptions,
1345 dictionary: Option<&[u8]>,
1346 kdf_params: &KdfParams,
1347 root_auth: Option<RootAuthWriterConfig<'_>>,
1348 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
1349 key_wrap_records: Option<&KeyWrapRecordSource>,
1350) -> Result<WrittenArchive, FormatError> {
1351 let mut sink = MemoryArchiveSink::default();
1352 let summary = write_archive_stream_inner(
1353 files,
1354 master_key,
1355 options,
1356 dictionary,
1357 kdf_params,
1358 root_auth,
1359 authenticator,
1360 key_wrap_records,
1361 &mut sink,
1362 )
1363 .map_err(format_error_from_archive_write_error)?;
1364 Ok(WrittenArchive {
1365 bytes: sink
1366 .volumes
1367 .first()
1368 .cloned()
1369 .ok_or(FormatError::WriterInvariant("no volumes emitted"))?,
1370 volumes: sink.volumes,
1371 bootstrap_sidecar: sink.bootstrap_sidecar,
1372 archive_uuid: summary.archive_uuid,
1373 session_id: summary.session_id,
1374 timings: summary.timings,
1375 })
1376}
1377
1378fn format_error_from_archive_write_error(error: ArchiveWriteError) -> FormatError {
1379 match error {
1380 ArchiveWriteError::Format(error) => error,
1381 ArchiveWriteError::Io(_) => {
1382 FormatError::WriterInvariant("in-memory archive writer returned I/O")
1383 }
1384 }
1385}
1386
1387fn writer_subkeys(
1388 master_key: &MasterKey,
1389 aead_algo: AeadAlgo,
1390 archive_uuid: &[u8; 16],
1391 session_id: &[u8; 16],
1392) -> Result<Subkeys, FormatError> {
1393 if aead_algo.is_encrypted() {
1394 Subkeys::derive(master_key, archive_uuid, session_id)
1395 } else {
1396 Ok(Subkeys::unencrypted_placeholder())
1397 }
1398}
1399
1400#[allow(clippy::too_many_arguments)]
1401fn write_archive_stream_inner<S, O>(
1402 files: &[S],
1403 master_key: &MasterKey,
1404 options: WriterOptions,
1405 dictionary: Option<&[u8]>,
1406 kdf_params: &KdfParams,
1407 root_auth: Option<RootAuthWriterConfig<'_>>,
1408 mut authenticator: Option<&mut RootAuthAuthenticator<'_>>,
1409 key_wrap_records: Option<&KeyWrapRecordSource>,
1410 sink: &mut O,
1411) -> Result<WrittenArchiveSummary, ArchiveWriteError>
1412where
1413 S: RegularFileSource,
1414 O: ArchiveWriteSink,
1415{
1416 let total_started = Instant::now();
1417 validate_dictionary_inputs(files.is_empty(), dictionary)?;
1418 if let Some(root_auth) = root_auth {
1419 validate_root_auth_writer_config(root_auth)?;
1420 }
1421 let mut requested_options = options;
1422 if requested_options.target_volume_size.is_some() {
1423 requested_options.stripe_width = requested_options
1424 .stripe_width
1425 .max(requested_options.volume_loss_tolerance as u32 + 1);
1426 }
1427 let archive_uuid = requested_options
1428 .archive_uuid
1429 .unwrap_or_else(|| *Uuid::new_v4().as_bytes());
1430 let session_id = requested_options
1431 .session_id
1432 .unwrap_or_else(|| *Uuid::new_v4().as_bytes());
1433 let mut accumulated_timings = WriterTimings::default();
1434
1435 loop {
1436 let planned_options = plan_writer_options(requested_options)?;
1437 let timed_plan = build_writer_plan(
1438 files,
1439 master_key,
1440 planned_options,
1441 dictionary,
1442 kdf_params,
1443 key_wrap_records,
1444 archive_uuid,
1445 session_id,
1446 root_auth,
1447 )?;
1448 accumulated_timings.add_assign(timed_plan.timings);
1449 let plan = timed_plan.plan;
1450 if let Some(target_volume_size) = planned_options.target_volume_size {
1451 let required = required_stripe_width_for_plan(&plan, master_key, target_volume_size)?;
1452 if required > planned_options.stripe_width {
1453 requested_options.stripe_width = required;
1454 continue;
1455 }
1456 }
1457 let mut summary = emit_writer_plan(
1458 files,
1459 master_key,
1460 dictionary,
1461 root_auth,
1462 authenticator.take(),
1463 plan,
1464 sink,
1465 )?;
1466 summary.timings.add_assign(accumulated_timings);
1467 summary.timings.total = total_started.elapsed();
1468 return Ok(summary);
1469 }
1470}
1471
1472fn validate_dictionary_inputs(
1473 files_are_empty: bool,
1474 dictionary: Option<&[u8]>,
1475) -> Result<(), FormatError> {
1476 if let Some(dictionary) = dictionary {
1477 if dictionary.is_empty() {
1478 return Err(FormatError::WriterUnsupported(
1479 "dictionary archives require a non-empty dictionary",
1480 ));
1481 }
1482 if files_are_empty {
1483 return Err(FormatError::WriterUnsupported(
1484 "dictionary archives require at least one file",
1485 ));
1486 }
1487 if dictionary.len() > u32::MAX as usize {
1488 return Err(FormatError::WriterUnsupported(
1489 "dictionary decompressed size exceeds u32",
1490 ));
1491 }
1492 }
1493 Ok(())
1494}
1495
1496#[allow(clippy::too_many_arguments)]
1497struct TimedWriterPlan {
1498 plan: WriterPlan,
1499 timings: WriterTimings,
1500}
1501
1502#[allow(clippy::too_many_arguments)]
1503fn build_writer_plan<S: RegularFileSource>(
1504 files: &[S],
1505 master_key: &MasterKey,
1506 options: WriterOptions,
1507 dictionary: Option<&[u8]>,
1508 kdf_params: &KdfParams,
1509 key_wrap_records: Option<&KeyWrapRecordSource>,
1510 archive_uuid: [u8; 16],
1511 session_id: [u8; 16],
1512 root_auth: Option<RootAuthWriterConfig<'_>>,
1513) -> Result<TimedWriterPlan, ArchiveWriteError> {
1514 let mut next_block_index = 0u64;
1515 let payload_started = Instant::now();
1516 let payload = plan_payload_stream(files, options, dictionary, &mut next_block_index)?;
1517 let plan_payload = payload_started.elapsed();
1518 let metadata_started = Instant::now();
1519 let plan = build_writer_plan_from_payload(
1520 payload,
1521 next_block_index,
1522 master_key,
1523 options,
1524 dictionary,
1525 kdf_params,
1526 key_wrap_records,
1527 archive_uuid,
1528 session_id,
1529 root_auth,
1530 )?;
1531 Ok(TimedWriterPlan {
1532 plan,
1533 timings: WriterTimings {
1534 plan_payload,
1535 plan_metadata: metadata_started.elapsed(),
1536 ..WriterTimings::default()
1537 },
1538 })
1539}
1540
1541#[allow(clippy::too_many_arguments)]
1542fn build_writer_plan_from_payload(
1543 payload: PayloadPlanning,
1544 mut next_block_index: u64,
1545 master_key: &MasterKey,
1546 mut options: WriterOptions,
1547 dictionary: Option<&[u8]>,
1548 kdf_params: &KdfParams,
1549 key_wrap_records: Option<&KeyWrapRecordSource>,
1550 archive_uuid: [u8; 16],
1551 session_id: [u8; 16],
1552 root_auth: Option<RootAuthWriterConfig<'_>>,
1553) -> Result<WriterPlan, ArchiveWriteError> {
1554 let subkeys = writer_subkeys(master_key, options.aead_algo, &archive_uuid, &session_id)?;
1555 let (resolved_kdf_params, key_wrap_table) =
1556 resolve_key_wrap_artifacts(kdf_params, &archive_uuid, &session_id, key_wrap_records)?;
1557 let volume_format_rev = volume_format_revision_for_options(&options, &resolved_kdf_params);
1558 let (shard_file_rows, planned_index_shards) = if payload.tar_members.is_empty() {
1559 (Vec::new(), Vec::new())
1560 } else {
1561 let rows = sorted_file_rows(&payload.tar_members);
1562 let shard_file_rows = partition_file_rows(rows)?;
1563 let planned_index_shards = build_index_shard_plaintexts(
1564 &shard_file_rows,
1565 &payload.frames,
1566 &payload.payload_objects,
1567 options,
1568 )?;
1569 (shard_file_rows, planned_index_shards)
1570 };
1571
1572 let mut shard_entries = Vec::with_capacity(planned_index_shards.len());
1573 let mut index_shard_objects = Vec::with_capacity(planned_index_shards.len());
1574 for planned in planned_index_shards {
1575 let compressed =
1576 compress_zstd_frame_with_jobs(&planned.plaintext, options.zstd_level, options.jobs)?;
1577 let object_plan = plan_encrypted_object(
1578 compressed.len(),
1579 options.index_fec_data_shards,
1580 options.index_fec_parity_shards,
1581 options,
1582 )?;
1583 let extent = ObjectExtent::new(next_block_index, object_plan)?;
1584 next_block_index = extent.next_block_index()?;
1585 shard_entries.push(ShardEntry {
1586 shard_index: planned.shard_index,
1587 first_block_index: extent.first_block_index,
1588 data_block_count: extent.data_block_count,
1589 parity_block_count: extent.parity_block_count,
1590 encrypted_size: extent.encrypted_size,
1591 decompressed_size: u32_len(planned.plaintext.len(), "IndexShard")?,
1592 file_count: planned.file_count,
1593 first_path_hash: planned.first_path_hash,
1594 last_path_hash: planned.last_path_hash,
1595 });
1596 index_shard_objects.push(PlannedIndexShardObject {
1597 shard_index: planned.shard_index,
1598 compressed,
1599 extent,
1600 });
1601 }
1602
1603 let compressed_dictionary = dictionary
1604 .map(|dictionary| {
1605 compress_zstd_frame_with_jobs(dictionary, options.zstd_level, options.jobs)
1606 })
1607 .transpose()?;
1608 let dictionary_decompressed_size = dictionary
1609 .map(|dictionary| u32_len(dictionary.len(), "dictionary"))
1610 .transpose()?;
1611 let dictionary_plan = compressed_dictionary
1612 .as_ref()
1613 .map(|compressed| {
1614 plan_metadata_object_without_class(
1615 compressed.len(),
1616 options,
1617 MetadataObjectKind::Dictionary,
1618 )
1619 })
1620 .transpose()?;
1621 let dictionary_extent = dictionary_plan
1622 .map(|plan| ObjectExtent::new(next_block_index, plan))
1623 .transpose()?;
1624 let next_after_dictionary = if let Some(extent) = dictionary_extent {
1625 extent.next_block_index()?
1626 } else {
1627 next_block_index
1628 };
1629
1630 let planned_directory_hint_shards = if should_emit_directory_hints(payload.tar_members.len()) {
1631 build_directory_hint_plaintexts(&shard_file_rows, options)?
1632 } else {
1633 Vec::new()
1634 };
1635 let mut directory_hint_entries = Vec::with_capacity(planned_directory_hint_shards.len());
1636 let mut directory_hint_objects = Vec::with_capacity(planned_directory_hint_shards.len());
1637 let mut planned_next_block_index = next_after_dictionary;
1638 for planned in planned_directory_hint_shards {
1639 let compressed =
1640 compress_zstd_frame_with_jobs(&planned.plaintext, options.zstd_level, options.jobs)?;
1641 let object_plan = plan_encrypted_object(
1642 compressed.len(),
1643 options.index_fec_data_shards,
1644 options.index_fec_parity_shards,
1645 options,
1646 )?;
1647 let extent = ObjectExtent::new(planned_next_block_index, object_plan)?;
1648 planned_next_block_index = extent.next_block_index()?;
1649 directory_hint_entries.push(DirectoryHintShardEntry {
1650 hint_shard_index: planned.hint_shard_index,
1651 first_dir_hash: planned.first_dir_hash,
1652 last_dir_hash: planned.last_dir_hash,
1653 first_block_index: extent.first_block_index,
1654 data_block_count: extent.data_block_count,
1655 parity_block_count: extent.parity_block_count,
1656 encrypted_size: extent.encrypted_size,
1657 decompressed_size: u32_len(planned.plaintext.len(), "DirectoryHintTable")?,
1658 entry_count: planned.entry_count,
1659 });
1660 directory_hint_objects.push(PlannedDirectoryHintObject {
1661 hint_shard_index: planned.hint_shard_index,
1662 compressed,
1663 extent,
1664 });
1665 }
1666
1667 let dictionary_extent = dictionary_extent.zip(dictionary_decompressed_size);
1668 let index_root_plaintext = build_index_root_plaintext(IndexRootPlaintextInput {
1669 shard_entries: &shard_entries,
1670 frame_count: payload.frames.len() as u64,
1671 envelope_count: payload.payload_objects.len() as u64,
1672 file_count: payload.tar_members.len() as u64,
1673 payload_block_count: payload.payload_block_count,
1674 tar_total_size: payload.tar_total_size,
1675 content_sha256: payload.content_sha256,
1676 directory_hint_entries: &directory_hint_entries,
1677 dictionary_extent,
1678 });
1679 let compressed_index_root =
1680 compress_zstd_frame_with_jobs(&index_root_plaintext, options.zstd_level, options.jobs)?;
1681 let metadata_class = plan_index_root_metadata_class(
1682 options,
1683 compressed_index_root.len(),
1684 compressed_dictionary.as_ref().map(Vec::len),
1685 )?;
1686 options = metadata_class.options;
1687 let crypto_header = build_crypto_header(
1688 options,
1689 volume_format_rev,
1690 dictionary.is_some(),
1691 &subkeys,
1692 &archive_uuid,
1693 &session_id,
1694 &resolved_kdf_params,
1695 )?;
1696 let index_root_extent = ObjectExtent::new(planned_next_block_index, metadata_class.index_root)?;
1697 let total_block_count = index_root_extent.next_block_index()?;
1698 let root_auth_footer_length = root_auth
1699 .map(|config| {
1700 root_auth_footer_wire_length(
1701 config.signer_identity.len(),
1702 config.authenticator_value_length as usize,
1703 )
1704 })
1705 .transpose()?;
1706 let block_records_offset = checked_u64_add(
1707 checked_u64_add(
1708 VOLUME_HEADER_LEN as u64,
1709 crypto_header.len() as u64,
1710 "CryptoHeader",
1711 )?,
1712 key_wrap_table.as_ref().map(Vec::len).unwrap_or(0) as u64,
1713 "KeyWrapTableV1",
1714 )?;
1715
1716 Ok(WriterPlan {
1717 options,
1718 archive_uuid,
1719 session_id,
1720 crypto_header,
1721 tar_members: payload.tar_members,
1722 frames: payload.frames,
1723 payload_objects: payload.payload_objects,
1724 index_root_plaintext,
1725 compressed_index_root,
1726 index_root_extent,
1727 index_shard_objects,
1728 shard_entries,
1729 compressed_dictionary,
1730 dictionary_extent,
1731 volume_format_rev,
1732 directory_hint_objects,
1733 directory_hint_entries,
1734 root_auth_footer_length,
1735 key_wrap_table,
1736 block_records_offset,
1737 total_block_count,
1738 })
1739}
1740
1741fn plan_payload_stream<S: RegularFileSource>(
1742 files: &[S],
1743 options: WriterOptions,
1744 dictionary: Option<&[u8]>,
1745 next_block_index: &mut u64,
1746) -> Result<PayloadPlanning, ArchiveWriteError> {
1747 let mut tar_members = Vec::with_capacity(files.len());
1748 let mut frames = Vec::new();
1749 let mut payload_objects = Vec::new();
1750 let mut tar_total_size = 0u64;
1751 let mut hasher = Sha256::new();
1752 let mut payload_block_count = 0u64;
1753 let mut next_frame_index = 0u64;
1754 let mut envelope = PayloadEnvelopeBuilder {
1755 envelope_index: 0,
1756 plaintext: Vec::new(),
1757 };
1758
1759 for (member_index, file) in files.iter().enumerate() {
1760 let path = normalize_lookup_file_path(file.archive_path(), options.max_path_length)?;
1761 let prefix = build_regular_file_member_prefix(
1762 &path,
1763 file.file_data_size(),
1764 file.mode(),
1765 file.mtime(),
1766 )?;
1767 let member_start = tar_total_size;
1768 let member_group_size = checked_u64_add(
1769 prefix.len() as u64,
1770 checked_u64_add(
1771 file.file_data_size(),
1772 padding_to_512_u64(file.file_data_size()),
1773 "tar member",
1774 )?,
1775 "tar member",
1776 )?;
1777 tar_members.push(TarMember {
1778 path,
1779 tar_member_group_start: member_start,
1780 tar_member_group_size: member_group_size,
1781 file_data_size: file.file_data_size(),
1782 kind: TarEntryKind::Regular,
1783 mode: file.mode(),
1784 mtime: file.mtime(),
1785 });
1786 let mut reader = StreamingMemberReader::new(file.open()?, prefix, file.file_data_size());
1787 let mut member_offset = 0u64;
1788 while member_offset < member_group_size {
1789 let remaining = member_group_size - member_offset;
1790 let max_chunk = remaining.min(options.chunk_size as u64);
1791 let mut chunk = vec![0u8; to_usize_writer(max_chunk, "payload chunk")?];
1792 reader
1793 .read_exact(&mut chunk)
1794 .map_err(ArchiveWriteError::Io)?;
1795 let mut chunk_len = chunk.len();
1796 let frame = loop {
1797 let candidate = &chunk[..chunk_len];
1798 let frame = if let Some(dictionary) = dictionary {
1799 compress_zstd_frame_with_dictionary_and_jobs(
1800 candidate,
1801 options.zstd_level,
1802 dictionary,
1803 options.jobs,
1804 )?
1805 } else {
1806 compress_zstd_frame_with_jobs(candidate, options.zstd_level, options.jobs)?
1807 };
1808 if payload_object_can_fit(frame.len(), options)? {
1809 break frame;
1810 }
1811 if chunk_len == 1 {
1812 return Err(FormatError::WriterUnsupported(
1813 "single-byte payload frame exceeds envelope object limits",
1814 )
1815 .into());
1816 }
1817 chunk_len = (chunk_len / 2).max(1);
1818 };
1819 if chunk_len < chunk.len() {
1820 reader.push_back(chunk[chunk_len..].to_vec());
1821 }
1822 let chunk = &chunk[..chunk_len];
1823 hasher.update(chunk);
1824 append_payload_frame_to_plan(
1825 PayloadFramePlanState {
1826 envelope: &mut envelope,
1827 payload_objects: &mut payload_objects,
1828 payload_block_count: &mut payload_block_count,
1829 next_block_index,
1830 frames: &mut frames,
1831 next_frame_index: &mut next_frame_index,
1832 options,
1833 },
1834 PayloadFramePlanInput {
1835 frame: &frame,
1836 decompressed_size: chunk_len,
1837 member_index,
1838 member_start,
1839 member_offset,
1840 member_group_size,
1841 },
1842 )?;
1843 member_offset = checked_u64_add(member_offset, chunk_len as u64, "payload chunk")?;
1844 tar_total_size = checked_u64_add(tar_total_size, chunk_len as u64, "tar stream")?;
1845 }
1846 }
1847
1848 if !envelope.plaintext.is_empty() {
1849 flush_payload_envelope_plan(
1850 &mut envelope,
1851 &mut payload_objects,
1852 &mut payload_block_count,
1853 next_block_index,
1854 options,
1855 )?;
1856 }
1857 let digest = hasher.finalize();
1858 let mut content_sha256 = [0u8; 32];
1859 content_sha256.copy_from_slice(&digest);
1860 Ok(PayloadPlanning {
1861 tar_members,
1862 frames,
1863 payload_objects,
1864 payload_block_count,
1865 tar_total_size,
1866 content_sha256,
1867 })
1868}
1869
1870#[allow(clippy::too_many_arguments)]
1871pub(crate) fn write_single_pass_archive_to_sink<O, F>(
1872 master_key: &MasterKey,
1873 options: WriterOptions,
1874 kdf_params: &KdfParams,
1875 root_auth: Option<RootAuthWriterConfig<'_>>,
1876 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
1877 key_wrap_records: Option<&KeyWrapRecordSource>,
1878 sink: &mut O,
1879 drive_members: F,
1880) -> Result<WrittenArchiveSummary, ArchiveWriteError>
1881where
1882 O: ArchiveWriteSink,
1883 F: FnOnce(&mut StreamingArchiveWriter<'_, O>) -> Result<(), ArchiveWriteError>,
1884{
1885 let total_started = Instant::now();
1886 validate_single_pass_writer_options(options)?;
1887 if let Some(root_auth) = root_auth {
1888 validate_root_auth_writer_config(root_auth)?;
1889 }
1890 let options = plan_single_pass_writer_options(options)?;
1891 let archive_uuid = options
1892 .archive_uuid
1893 .unwrap_or_else(|| *Uuid::new_v4().as_bytes());
1894 let session_id = options
1895 .session_id
1896 .unwrap_or_else(|| *Uuid::new_v4().as_bytes());
1897 let stabilized_key_wrap_records =
1898 stabilized_key_wrap_record_source(kdf_params, key_wrap_records)?;
1899 let key_wrap_records = stabilized_key_wrap_records.as_ref().or(key_wrap_records);
1900 let (resolved_kdf_params, key_wrap_table) =
1901 resolve_key_wrap_artifacts(kdf_params, &archive_uuid, &session_id, key_wrap_records)?;
1902 let volume_format_rev = volume_format_revision_for_options(&options, &resolved_kdf_params);
1903 let subkeys = writer_subkeys(master_key, options.aead_algo, &archive_uuid, &session_id)?;
1904 let crypto_header = build_crypto_header(
1905 options,
1906 volume_format_rev,
1907 false,
1908 &subkeys,
1909 &archive_uuid,
1910 &session_id,
1911 &resolved_kdf_params,
1912 )?;
1913 let emission_state = begin_writer_emission_state(
1914 sink,
1915 options,
1916 &crypto_header,
1917 key_wrap_table.as_deref(),
1918 archive_uuid,
1919 session_id,
1920 volume_format_rev,
1921 root_auth.is_some(),
1922 )?;
1923
1924 let mut writer = StreamingArchiveWriter {
1925 sink,
1926 options,
1927 archive_uuid,
1928 session_id,
1929 crypto_header,
1930 subkeys,
1931 tar_members: Vec::new(),
1932 frames: Vec::new(),
1933 payload_objects: Vec::new(),
1934 payload_block_count: 0,
1935 tar_total_size: 0,
1936 hasher: Sha256::new(),
1937 next_frame_index: 0,
1938 envelope: PayloadEnvelopeBuilder {
1939 envelope_index: 0,
1940 plaintext: Vec::new(),
1941 },
1942 emission_state,
1943 };
1944 let emit_payload_started = Instant::now();
1945 drive_members(&mut writer)?;
1946 let emit_payload = emit_payload_started.elapsed();
1947 let mut summary = writer.finish(
1948 master_key,
1949 &resolved_kdf_params,
1950 key_wrap_records,
1951 root_auth,
1952 authenticator,
1953 )?;
1954 summary.timings.emit_payload += emit_payload;
1955 summary.timings.total = total_started.elapsed();
1956 Ok(summary)
1957}
1958
1959struct OrderedFrameJob {
1960 frame_index: u64,
1961 member_index: usize,
1962 member_start: u64,
1963 member_offset: u64,
1964 member_group_size: u64,
1965 plaintext: Vec<u8>,
1966}
1967
1968struct OrderedFrameResult {
1969 frame_index: u64,
1970 member_index: usize,
1971 member_start: u64,
1972 member_offset: u64,
1973 member_group_size: u64,
1974 decompressed_size: usize,
1975 frame: Vec<u8>,
1976}
1977
1978struct OrderedEnvelopeJob {
1979 envelope_index: u64,
1980 plaintext: Vec<u8>,
1981 extent: ObjectExtent,
1982 collect_data_leaf_hashes: bool,
1983}
1984
1985struct OrderedEnvelopeResult {
1986 envelope_index: u64,
1987 records: OrderedEnvelopeRecords,
1988}
1989
1990enum OrderedEnvelopeRecords {
1991 Materialized(Vec<BlockRecord>),
1992 Serialized(Vec<SerializedBlockRecord>),
1993}
1994
1995struct SerializedBlockRecord {
1996 block_index: u64,
1997 bytes: Vec<u8>,
1998}
1999
2000struct OrderedParallelState {
2001 tar_members: Vec<TarMember>,
2002 frames: Vec<PayloadFrame>,
2003 payload_objects: Vec<PayloadObject>,
2004 payload_block_count: u64,
2005 tar_total_size: u64,
2006 hasher: Sha256,
2007 next_frame_job_index: u64,
2008 next_frame_result_index: u64,
2009 next_frame_metadata_index: u64,
2010 frame_buffer: std::collections::BTreeMap<u64, OrderedFrameResult>,
2011 envelope: PayloadEnvelopeBuilder,
2012 next_payload_block_index: u64,
2013 next_envelope_result_index: u64,
2014 envelope_buffer: std::collections::BTreeMap<u64, OrderedEnvelopeResult>,
2015}
2016
2017impl OrderedParallelState {
2018 fn new(file_count: usize) -> Self {
2019 Self {
2020 tar_members: Vec::with_capacity(file_count),
2021 frames: Vec::new(),
2022 payload_objects: Vec::new(),
2023 payload_block_count: 0,
2024 tar_total_size: 0,
2025 hasher: Sha256::new(),
2026 next_frame_job_index: 0,
2027 next_frame_result_index: 0,
2028 next_frame_metadata_index: 0,
2029 frame_buffer: std::collections::BTreeMap::new(),
2030 envelope: PayloadEnvelopeBuilder {
2031 envelope_index: 0,
2032 plaintext: Vec::new(),
2033 },
2034 next_payload_block_index: 0,
2035 next_envelope_result_index: 0,
2036 envelope_buffer: std::collections::BTreeMap::new(),
2037 }
2038 }
2039}
2040
2041#[allow(clippy::too_many_arguments)]
2042fn write_ordered_parallel_archive_to_sink<S, O>(
2043 files: &[S],
2044 master_key: &MasterKey,
2045 options: WriterOptions,
2046 kdf_params: &KdfParams,
2047 root_auth: Option<RootAuthWriterConfig<'_>>,
2048 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
2049 key_wrap_records: Option<&KeyWrapRecordSource>,
2050 sink: &mut O,
2051) -> Result<WrittenArchiveSummary, ArchiveWriteError>
2052where
2053 S: RegularFileSource,
2054 O: ArchiveWriteSink,
2055{
2056 write_ordered_parallel_stream_archive_to_sink(
2057 master_key,
2058 options,
2059 kdf_params,
2060 root_auth,
2061 authenticator,
2062 key_wrap_records,
2063 sink,
2064 |writer| {
2065 writer.reserve_member_capacity(files.len());
2066 for file in files {
2067 let archive_path =
2068 normalize_lookup_file_path(file.archive_path(), options.max_path_length)?;
2069 let mut reader = file.open()?;
2070 writer.write_regular_member_from_reader(
2071 StreamingRegularMember {
2072 archive_path,
2073 file_data_size: file.file_data_size(),
2074 mode: file.mode(),
2075 mtime: file.mtime(),
2076 },
2077 reader.as_mut(),
2078 )?;
2079 }
2080 Ok(())
2081 },
2082 )
2083}
2084
2085#[allow(clippy::too_many_arguments)]
2086pub(crate) fn write_ordered_parallel_stream_archive_to_sink<O, F>(
2087 master_key: &MasterKey,
2088 options: WriterOptions,
2089 kdf_params: &KdfParams,
2090 root_auth: Option<RootAuthWriterConfig<'_>>,
2091 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
2092 key_wrap_records: Option<&KeyWrapRecordSource>,
2093 sink: &mut O,
2094 drive_members: F,
2095) -> Result<WrittenArchiveSummary, ArchiveWriteError>
2096where
2097 O: ArchiveWriteSink,
2098 F: FnOnce(&mut OrderedParallelArchiveWriter<'_, O>) -> Result<(), ArchiveWriteError>,
2099{
2100 let total_started = Instant::now();
2101 validate_single_pass_writer_options(options)?;
2102 if let Some(root_auth) = root_auth {
2103 validate_root_auth_writer_config(root_auth)?;
2104 }
2105 let options = plan_single_pass_writer_options(options)?;
2106 let archive_uuid = options
2107 .archive_uuid
2108 .unwrap_or_else(|| *Uuid::new_v4().as_bytes());
2109 let session_id = options
2110 .session_id
2111 .unwrap_or_else(|| *Uuid::new_v4().as_bytes());
2112 let stabilized_key_wrap_records =
2113 stabilized_key_wrap_record_source(kdf_params, key_wrap_records)?;
2114 let key_wrap_records = stabilized_key_wrap_records.as_ref().or(key_wrap_records);
2115 let (resolved_kdf_params, key_wrap_table) =
2116 resolve_key_wrap_artifacts(kdf_params, &archive_uuid, &session_id, key_wrap_records)?;
2117 let volume_format_rev = volume_format_revision_for_options(&options, &resolved_kdf_params);
2118 let subkeys = writer_subkeys(master_key, options.aead_algo, &archive_uuid, &session_id)?;
2119 let crypto_header = build_crypto_header(
2120 options,
2121 volume_format_rev,
2122 false,
2123 &subkeys,
2124 &archive_uuid,
2125 &session_id,
2126 &resolved_kdf_params,
2127 )?;
2128 let mut emission_state = begin_writer_emission_state(
2129 sink,
2130 options,
2131 &crypto_header,
2132 key_wrap_table.as_deref(),
2133 archive_uuid,
2134 session_id,
2135 volume_format_rev,
2136 root_auth.is_some(),
2137 )?;
2138
2139 let emit_payload_started = Instant::now();
2140 let mut ordered = OrderedParallelState::new(0);
2141 let worker_count = options.jobs.max(1);
2142 let frame_job_buffer = worker_count.saturating_mul(4).max(1);
2143 let envelope_job_buffer = worker_count.saturating_mul(2).max(1);
2144 let subkeys_for_workers = std::sync::Arc::new(subkeys.clone());
2145
2146 std::thread::scope(|scope| -> Result<(), ArchiveWriteError> {
2147 let (frame_job_tx, frame_job_rx) =
2148 std::sync::mpsc::sync_channel::<OrderedFrameJob>(frame_job_buffer);
2149 let (frame_result_tx, frame_result_rx) =
2150 std::sync::mpsc::channel::<Result<OrderedFrameResult, ArchiveWriteError>>();
2151 let frame_job_rx = std::sync::Arc::new(std::sync::Mutex::new(frame_job_rx));
2152
2153 let (envelope_job_tx, envelope_job_rx) =
2154 std::sync::mpsc::sync_channel::<OrderedEnvelopeJob>(envelope_job_buffer);
2155 let (envelope_result_tx, envelope_result_rx) =
2156 std::sync::mpsc::channel::<Result<OrderedEnvelopeResult, ArchiveWriteError>>();
2157 let envelope_job_rx = std::sync::Arc::new(std::sync::Mutex::new(envelope_job_rx));
2158
2159 let frame_handles = (0..worker_count)
2160 .map(|_| {
2161 let frame_job_rx = std::sync::Arc::clone(&frame_job_rx);
2162 let frame_result_tx = frame_result_tx.clone();
2163 scope.spawn(move || loop {
2164 let job = {
2165 let receiver = frame_job_rx
2166 .lock()
2167 .expect("ordered frame receiver poisoned");
2168 receiver.recv()
2169 };
2170 let Ok(job) = job else {
2171 break;
2172 };
2173 let is_error = match build_ordered_frame_result(job, options) {
2174 Ok(result) => frame_result_tx.send(Ok(result)).is_err(),
2175 Err(error) => {
2176 let _ = frame_result_tx.send(Err(error));
2177 true
2178 }
2179 };
2180 if is_error {
2181 break;
2182 }
2183 })
2184 })
2185 .collect::<Vec<_>>();
2186 drop(frame_result_tx);
2187
2188 let envelope_handles = (0..worker_count)
2189 .map(|_| {
2190 let envelope_job_rx = std::sync::Arc::clone(&envelope_job_rx);
2191 let envelope_result_tx = envelope_result_tx.clone();
2192 let subkeys = std::sync::Arc::clone(&subkeys_for_workers);
2193 scope.spawn(move || loop {
2194 let job = {
2195 let receiver = envelope_job_rx
2196 .lock()
2197 .expect("ordered envelope receiver poisoned");
2198 receiver.recv()
2199 };
2200 let Ok(job) = job else {
2201 break;
2202 };
2203 let is_error = match build_ordered_envelope_result(
2204 job,
2205 &subkeys,
2206 options,
2207 archive_uuid,
2208 session_id,
2209 ) {
2210 Ok(result) => envelope_result_tx.send(Ok(result)).is_err(),
2211 Err(error) => {
2212 let _ = envelope_result_tx.send(Err(error));
2213 true
2214 }
2215 };
2216 if is_error {
2217 break;
2218 }
2219 })
2220 })
2221 .collect::<Vec<_>>();
2222 drop(envelope_result_tx);
2223
2224 {
2225 let mut writer = OrderedParallelArchiveWriter {
2226 frame_job_tx: &frame_job_tx,
2227 frame_result_rx: &frame_result_rx,
2228 envelope_job_tx: &envelope_job_tx,
2229 envelope_result_rx: &envelope_result_rx,
2230 ordered: &mut ordered,
2231 sink,
2232 options,
2233 emission_state: &mut emission_state,
2234 };
2235 drive_members(&mut writer)?;
2236 }
2237
2238 drop(frame_job_tx);
2239 while ordered.next_frame_result_index < ordered.next_frame_job_index {
2240 receive_ordered_frame_result(
2241 &frame_result_rx,
2242 &envelope_job_tx,
2243 &envelope_result_rx,
2244 &mut ordered,
2245 sink,
2246 options,
2247 &mut emission_state,
2248 true,
2249 )?;
2250 }
2251 flush_ordered_parallel_envelope(
2252 &envelope_job_tx,
2253 &envelope_result_rx,
2254 &mut ordered,
2255 sink,
2256 options,
2257 &mut emission_state,
2258 )?;
2259 drop(envelope_job_tx);
2260 while ordered.next_envelope_result_index < ordered.envelope.envelope_index {
2261 receive_ordered_envelope_result(
2262 &envelope_result_rx,
2263 &mut ordered,
2264 sink,
2265 options,
2266 &mut emission_state,
2267 true,
2268 )?;
2269 }
2270
2271 for handle in frame_handles {
2272 handle
2273 .join()
2274 .map_err(|_| FormatError::WriterInvariant("ordered frame worker panicked"))?;
2275 }
2276 for handle in envelope_handles {
2277 handle
2278 .join()
2279 .map_err(|_| FormatError::WriterInvariant("ordered envelope worker panicked"))?;
2280 }
2281 Ok(())
2282 })?;
2283 let emit_payload = emit_payload_started.elapsed();
2284
2285 emission_state.next_block_index = ordered.next_payload_block_index;
2286 let digest = ordered.hasher.finalize();
2287 let mut content_sha256 = [0u8; 32];
2288 content_sha256.copy_from_slice(&digest);
2289 let payload = PayloadPlanning {
2290 tar_members: ordered.tar_members,
2291 frames: ordered.frames,
2292 payload_objects: ordered.payload_objects,
2293 payload_block_count: ordered.payload_block_count,
2294 tar_total_size: ordered.tar_total_size,
2295 content_sha256,
2296 };
2297 let plan = build_writer_plan_from_payload(
2298 payload,
2299 emission_state.next_block_index,
2300 master_key,
2301 options,
2302 None,
2303 &resolved_kdf_params,
2304 key_wrap_records,
2305 archive_uuid,
2306 session_id,
2307 root_auth,
2308 )?;
2309 if plan.options != options || plan.crypto_header != crypto_header {
2310 return Err(FormatError::WriterUnsupported(
2311 "ordered parallel metadata exceeded the predeclared header class",
2312 )
2313 .into());
2314 }
2315 let mut summary = emit_writer_plan_suffix(
2316 &subkeys,
2317 root_auth,
2318 authenticator,
2319 plan,
2320 sink,
2321 emission_state,
2322 )?;
2323 summary.timings.emit_payload += emit_payload;
2324 summary.timings.total = total_started.elapsed();
2325 Ok(summary)
2326}
2327
2328pub(crate) struct OrderedParallelArchiveWriter<'a, O: ArchiveWriteSink> {
2329 frame_job_tx: &'a std::sync::mpsc::SyncSender<OrderedFrameJob>,
2330 frame_result_rx: &'a std::sync::mpsc::Receiver<Result<OrderedFrameResult, ArchiveWriteError>>,
2331 envelope_job_tx: &'a std::sync::mpsc::SyncSender<OrderedEnvelopeJob>,
2332 envelope_result_rx:
2333 &'a std::sync::mpsc::Receiver<Result<OrderedEnvelopeResult, ArchiveWriteError>>,
2334 ordered: &'a mut OrderedParallelState,
2335 sink: &'a mut O,
2336 options: WriterOptions,
2337 emission_state: &'a mut WriterEmissionState,
2338}
2339
2340impl<O: ArchiveWriteSink> OrderedParallelArchiveWriter<'_, O> {
2341 fn reserve_member_capacity(&mut self, additional: usize) {
2342 self.ordered.tar_members.reserve(additional);
2343 }
2344
2345 pub(crate) fn write_regular_member_from_reader(
2346 &mut self,
2347 member: StreamingRegularMember,
2348 payload: &mut dyn Read,
2349 ) -> Result<(), ArchiveWriteError> {
2350 let path = member.archive_path;
2351 validate_file_path_bytes(&path, self.options.max_path_length)?;
2352 let prefix = build_regular_file_member_prefix(
2353 &path,
2354 member.file_data_size,
2355 member.mode,
2356 member.mtime,
2357 )?;
2358 let member_start = self.ordered.tar_total_size;
2359 let member_group_size = checked_u64_add(
2360 prefix.len() as u64,
2361 checked_u64_add(
2362 member.file_data_size,
2363 padding_to_512_u64(member.file_data_size),
2364 "tar member",
2365 )?,
2366 "tar member",
2367 )?;
2368 let member_index = self.ordered.tar_members.len();
2369 self.ordered.tar_members.push(TarMember {
2370 path,
2371 tar_member_group_start: member_start,
2372 tar_member_group_size: member_group_size,
2373 file_data_size: member.file_data_size,
2374 kind: TarEntryKind::Regular,
2375 mode: member.mode,
2376 mtime: member.mtime,
2377 });
2378
2379 let mut reader =
2380 StreamingMemberReader::new(Box::new(payload), prefix, member.file_data_size);
2381 let mut member_offset = 0u64;
2382 while member_offset < member_group_size {
2383 let remaining = member_group_size - member_offset;
2384 let read_len = remaining.min(self.options.chunk_size as u64);
2385 let mut plaintext = vec![0u8; to_usize_writer(read_len, "payload chunk")?];
2386 reader
2387 .read_exact(&mut plaintext)
2388 .map_err(ArchiveWriteError::Io)?;
2389 self.ordered.hasher.update(&plaintext);
2390 let frame_index = self.ordered.next_frame_job_index;
2391 self.ordered.next_frame_job_index = checked_u64_add(
2392 self.ordered.next_frame_job_index,
2393 1,
2394 "PayloadFrame.frame_index",
2395 )?;
2396 send_ordered_frame_job(
2397 OrderedFrameJob {
2398 frame_index,
2399 member_index,
2400 member_start,
2401 member_offset,
2402 member_group_size,
2403 plaintext,
2404 },
2405 self.frame_job_tx,
2406 self.frame_result_rx,
2407 self.envelope_job_tx,
2408 self.envelope_result_rx,
2409 self.ordered,
2410 self.sink,
2411 self.options,
2412 self.emission_state,
2413 )?;
2414 member_offset = checked_u64_add(member_offset, read_len, "payload chunk")?;
2415 self.ordered.tar_total_size =
2416 checked_u64_add(self.ordered.tar_total_size, read_len, "tar stream")?;
2417 }
2418 Ok(())
2419 }
2420}
2421
2422#[allow(clippy::too_many_arguments)]
2423fn send_ordered_frame_job<O: ArchiveWriteSink>(
2424 mut job: OrderedFrameJob,
2425 frame_job_tx: &std::sync::mpsc::SyncSender<OrderedFrameJob>,
2426 frame_result_rx: &std::sync::mpsc::Receiver<Result<OrderedFrameResult, ArchiveWriteError>>,
2427 envelope_job_tx: &std::sync::mpsc::SyncSender<OrderedEnvelopeJob>,
2428 envelope_result_rx: &std::sync::mpsc::Receiver<
2429 Result<OrderedEnvelopeResult, ArchiveWriteError>,
2430 >,
2431 ordered: &mut OrderedParallelState,
2432 sink: &mut O,
2433 options: WriterOptions,
2434 emission_state: &mut WriterEmissionState,
2435) -> Result<(), ArchiveWriteError> {
2436 loop {
2437 match frame_job_tx.try_send(job) {
2438 Ok(()) => {
2439 drain_ordered_frame_results(
2440 frame_result_rx,
2441 envelope_job_tx,
2442 envelope_result_rx,
2443 ordered,
2444 sink,
2445 options,
2446 emission_state,
2447 )?;
2448 return Ok(());
2449 }
2450 Err(std::sync::mpsc::TrySendError::Full(returned)) => {
2451 job = returned;
2452 receive_ordered_frame_result(
2453 frame_result_rx,
2454 envelope_job_tx,
2455 envelope_result_rx,
2456 ordered,
2457 sink,
2458 options,
2459 emission_state,
2460 true,
2461 )?;
2462 }
2463 Err(std::sync::mpsc::TrySendError::Disconnected(_)) => {
2464 return Err(FormatError::WriterInvariant("ordered frame worker stopped").into());
2465 }
2466 }
2467 }
2468}
2469
2470#[allow(clippy::too_many_arguments)]
2471fn drain_ordered_frame_results<O: ArchiveWriteSink>(
2472 frame_result_rx: &std::sync::mpsc::Receiver<Result<OrderedFrameResult, ArchiveWriteError>>,
2473 envelope_job_tx: &std::sync::mpsc::SyncSender<OrderedEnvelopeJob>,
2474 envelope_result_rx: &std::sync::mpsc::Receiver<
2475 Result<OrderedEnvelopeResult, ArchiveWriteError>,
2476 >,
2477 ordered: &mut OrderedParallelState,
2478 sink: &mut O,
2479 options: WriterOptions,
2480 emission_state: &mut WriterEmissionState,
2481) -> Result<(), ArchiveWriteError> {
2482 while receive_ordered_frame_result(
2483 frame_result_rx,
2484 envelope_job_tx,
2485 envelope_result_rx,
2486 ordered,
2487 sink,
2488 options,
2489 emission_state,
2490 false,
2491 )? {}
2492 Ok(())
2493}
2494
2495#[allow(clippy::too_many_arguments)]
2496fn receive_ordered_frame_result<O: ArchiveWriteSink>(
2497 frame_result_rx: &std::sync::mpsc::Receiver<Result<OrderedFrameResult, ArchiveWriteError>>,
2498 envelope_job_tx: &std::sync::mpsc::SyncSender<OrderedEnvelopeJob>,
2499 envelope_result_rx: &std::sync::mpsc::Receiver<
2500 Result<OrderedEnvelopeResult, ArchiveWriteError>,
2501 >,
2502 ordered: &mut OrderedParallelState,
2503 sink: &mut O,
2504 options: WriterOptions,
2505 emission_state: &mut WriterEmissionState,
2506 wait: bool,
2507) -> Result<bool, ArchiveWriteError> {
2508 let result = if wait {
2509 match frame_result_rx.recv() {
2510 Ok(result) => result?,
2511 Err(_) => {
2512 return Err(FormatError::WriterInvariant("ordered frame worker stopped").into())
2513 }
2514 }
2515 } else {
2516 match frame_result_rx.try_recv() {
2517 Ok(result) => result?,
2518 Err(std::sync::mpsc::TryRecvError::Empty) => return Ok(false),
2519 Err(std::sync::mpsc::TryRecvError::Disconnected) => return Ok(false),
2520 }
2521 };
2522 ordered.frame_buffer.insert(result.frame_index, result);
2523 while let Some(result) = ordered
2524 .frame_buffer
2525 .remove(&ordered.next_frame_result_index)
2526 {
2527 append_ordered_frame_result(
2528 result,
2529 envelope_job_tx,
2530 envelope_result_rx,
2531 ordered,
2532 sink,
2533 options,
2534 emission_state,
2535 )?;
2536 ordered.next_frame_result_index = checked_u64_add(
2537 ordered.next_frame_result_index,
2538 1,
2539 "PayloadFrame.frame_index",
2540 )?;
2541 }
2542 Ok(true)
2543}
2544
2545#[allow(clippy::too_many_arguments)]
2546fn append_ordered_frame_result<O: ArchiveWriteSink>(
2547 result: OrderedFrameResult,
2548 envelope_job_tx: &std::sync::mpsc::SyncSender<OrderedEnvelopeJob>,
2549 envelope_result_rx: &std::sync::mpsc::Receiver<
2550 Result<OrderedEnvelopeResult, ArchiveWriteError>,
2551 >,
2552 ordered: &mut OrderedParallelState,
2553 sink: &mut O,
2554 options: WriterOptions,
2555 emission_state: &mut WriterEmissionState,
2556) -> Result<(), ArchiveWriteError> {
2557 if payload_envelope_needs_flush(&ordered.envelope, result.frame.len(), options)? {
2558 flush_ordered_parallel_envelope(
2559 envelope_job_tx,
2560 envelope_result_rx,
2561 ordered,
2562 sink,
2563 options,
2564 emission_state,
2565 )?;
2566 }
2567 if ordered.envelope.plaintext.is_empty()
2568 && !payload_object_can_fit(result.frame.len(), options)?
2569 {
2570 return Err(
2571 FormatError::WriterUnsupported("payload frame exceeds envelope object limits").into(),
2572 );
2573 }
2574 let offset = u32_len(
2575 ordered.envelope.plaintext.len(),
2576 "FrameEntry.offset_in_envelope",
2577 )?;
2578 ordered.envelope.plaintext.extend_from_slice(&result.frame);
2579 ordered
2580 .frames
2581 .push(payload_frame_metadata(PayloadFrameMetadataInput {
2582 frame_index: ordered.next_frame_metadata_index,
2583 envelope_index: ordered.envelope.envelope_index,
2584 member_index: result.member_index,
2585 offset_in_envelope: offset,
2586 compressed_size: result.frame.len(),
2587 decompressed_size: result.decompressed_size,
2588 member_start: result.member_start,
2589 member_offset: result.member_offset,
2590 member_group_size: result.member_group_size,
2591 })?);
2592 ordered.next_frame_metadata_index = checked_u64_add(
2593 ordered.next_frame_metadata_index,
2594 1,
2595 "PayloadFrame.frame_index",
2596 )?;
2597 Ok(())
2598}
2599
2600#[allow(clippy::too_many_arguments)]
2601fn flush_ordered_parallel_envelope<O: ArchiveWriteSink>(
2602 envelope_job_tx: &std::sync::mpsc::SyncSender<OrderedEnvelopeJob>,
2603 envelope_result_rx: &std::sync::mpsc::Receiver<
2604 Result<OrderedEnvelopeResult, ArchiveWriteError>,
2605 >,
2606 ordered: &mut OrderedParallelState,
2607 sink: &mut O,
2608 options: WriterOptions,
2609 emission_state: &mut WriterEmissionState,
2610) -> Result<(), ArchiveWriteError> {
2611 if ordered.envelope.plaintext.is_empty() {
2612 return Ok(());
2613 }
2614 let plaintext_size = u32_len(
2615 ordered.envelope.plaintext.len(),
2616 "EnvelopeEntry.plaintext_size",
2617 )?;
2618 let object_plan = plan_encrypted_object(
2619 ordered.envelope.plaintext.len(),
2620 options.fec_data_shards,
2621 options.fec_parity_shards,
2622 options,
2623 )?;
2624 let extent = ObjectExtent::new(ordered.next_payload_block_index, object_plan)?;
2625 ordered.next_payload_block_index = extent.next_block_index()?;
2626 ordered.payload_block_count = checked_u64_add(
2627 ordered.payload_block_count,
2628 extent.data_block_count as u64,
2629 "payload",
2630 )?;
2631 ordered.payload_objects.push(PayloadObject {
2632 envelope_index: ordered.envelope.envelope_index,
2633 plaintext_size,
2634 object: extent,
2635 });
2636 let mut job = OrderedEnvelopeJob {
2637 envelope_index: ordered.envelope.envelope_index,
2638 plaintext: std::mem::take(&mut ordered.envelope.plaintext),
2639 extent,
2640 collect_data_leaf_hashes: emission_state.data_leaf_hashes.is_some(),
2641 };
2642 ordered.envelope.envelope_index =
2643 checked_u64_add(ordered.envelope.envelope_index, 1, "EnvelopeEntry")?;
2644 loop {
2645 match envelope_job_tx.try_send(job) {
2646 Ok(()) => {
2647 drain_ordered_envelope_results(
2648 envelope_result_rx,
2649 ordered,
2650 sink,
2651 options,
2652 emission_state,
2653 )?;
2654 return Ok(());
2655 }
2656 Err(std::sync::mpsc::TrySendError::Full(returned)) => {
2657 job = returned;
2658 receive_ordered_envelope_result(
2659 envelope_result_rx,
2660 ordered,
2661 sink,
2662 options,
2663 emission_state,
2664 true,
2665 )?;
2666 }
2667 Err(std::sync::mpsc::TrySendError::Disconnected(_)) => {
2668 return Err(FormatError::WriterInvariant("ordered envelope worker stopped").into());
2669 }
2670 }
2671 }
2672}
2673
2674fn drain_ordered_envelope_results<O: ArchiveWriteSink>(
2675 envelope_result_rx: &std::sync::mpsc::Receiver<
2676 Result<OrderedEnvelopeResult, ArchiveWriteError>,
2677 >,
2678 ordered: &mut OrderedParallelState,
2679 sink: &mut O,
2680 options: WriterOptions,
2681 emission_state: &mut WriterEmissionState,
2682) -> Result<(), ArchiveWriteError> {
2683 while receive_ordered_envelope_result(
2684 envelope_result_rx,
2685 ordered,
2686 sink,
2687 options,
2688 emission_state,
2689 false,
2690 )? {}
2691 Ok(())
2692}
2693
2694fn receive_ordered_envelope_result<O: ArchiveWriteSink>(
2695 envelope_result_rx: &std::sync::mpsc::Receiver<
2696 Result<OrderedEnvelopeResult, ArchiveWriteError>,
2697 >,
2698 ordered: &mut OrderedParallelState,
2699 sink: &mut O,
2700 options: WriterOptions,
2701 emission_state: &mut WriterEmissionState,
2702 wait: bool,
2703) -> Result<bool, ArchiveWriteError> {
2704 let result = if wait {
2705 match envelope_result_rx.recv() {
2706 Ok(result) => result?,
2707 Err(_) => {
2708 return Err(FormatError::WriterInvariant("ordered envelope worker stopped").into());
2709 }
2710 }
2711 } else {
2712 match envelope_result_rx.try_recv() {
2713 Ok(result) => result?,
2714 Err(std::sync::mpsc::TryRecvError::Empty) => return Ok(false),
2715 Err(std::sync::mpsc::TryRecvError::Disconnected) => return Ok(false),
2716 }
2717 };
2718 ordered
2719 .envelope_buffer
2720 .insert(result.envelope_index, result);
2721 while let Some(result) = ordered
2722 .envelope_buffer
2723 .remove(&ordered.next_envelope_result_index)
2724 {
2725 emit_ordered_envelope_result(result, sink, options, emission_state)?;
2726 ordered.next_envelope_result_index =
2727 checked_u64_add(ordered.next_envelope_result_index, 1, "EnvelopeEntry")?;
2728 }
2729 Ok(true)
2730}
2731
2732fn build_ordered_frame_result(
2733 job: OrderedFrameJob,
2734 options: WriterOptions,
2735) -> Result<OrderedFrameResult, ArchiveWriteError> {
2736 let frame = compress_zstd_frame_with_jobs(&job.plaintext, options.zstd_level, 1)?;
2737 Ok(OrderedFrameResult {
2738 frame_index: job.frame_index,
2739 member_index: job.member_index,
2740 member_start: job.member_start,
2741 member_offset: job.member_offset,
2742 member_group_size: job.member_group_size,
2743 decompressed_size: job.plaintext.len(),
2744 frame,
2745 })
2746}
2747
2748fn build_ordered_envelope_result(
2749 job: OrderedEnvelopeJob,
2750 subkeys: &Subkeys,
2751 options: WriterOptions,
2752 archive_uuid: [u8; 16],
2753 session_id: [u8; 16],
2754) -> Result<OrderedEnvelopeResult, ArchiveWriteError> {
2755 let context = ObjectEncryptionContext {
2756 key: &subkeys.enc_key,
2757 nonce_seed: &subkeys.nonce_seed,
2758 domain: b"envelope",
2759 counter: job.envelope_index,
2760 data_kind: BlockKind::PayloadData,
2761 parity_kind: BlockKind::PayloadParity,
2762 data_shard_max: options.fec_data_shards,
2763 class_parity_shard_max: options.fec_parity_shards,
2764 archive_uuid: &archive_uuid,
2765 session_id: &session_id,
2766 };
2767 if job.extent.parity_block_count == 0 && !job.collect_data_leaf_hashes {
2768 return Ok(OrderedEnvelopeResult {
2769 envelope_index: job.envelope_index,
2770 records: OrderedEnvelopeRecords::Serialized(serialize_zero_parity_encrypted_object(
2771 &job.plaintext,
2772 context,
2773 job.extent,
2774 options,
2775 )?),
2776 });
2777 }
2778
2779 let mut local_next_block_index = job.extent.first_block_index;
2780 let object = encrypt_object(
2781 &job.plaintext,
2782 context,
2783 &mut local_next_block_index,
2784 options,
2785 )?;
2786 validate_planned_extent(&object, job.extent)?;
2787 Ok(OrderedEnvelopeResult {
2788 envelope_index: job.envelope_index,
2789 records: OrderedEnvelopeRecords::Materialized(object.records),
2790 })
2791}
2792
2793fn emit_ordered_envelope_result<O: ArchiveWriteSink>(
2794 result: OrderedEnvelopeResult,
2795 sink: &mut O,
2796 options: WriterOptions,
2797 emission_state: &mut WriterEmissionState,
2798) -> Result<(), ArchiveWriteError> {
2799 match result.records {
2800 OrderedEnvelopeRecords::Materialized(records) => {
2801 for record in &records {
2802 emit_block_record(
2803 sink,
2804 options,
2805 &mut emission_state.bytes_written,
2806 &mut emission_state.record_counts,
2807 emission_state.volume_format_rev,
2808 &mut emission_state.data_leaf_hashes,
2809 record,
2810 )?;
2811 }
2812 }
2813 OrderedEnvelopeRecords::Serialized(records) => {
2814 for record in &records {
2815 emit_serialized_block_record(
2816 sink,
2817 options,
2818 &mut emission_state.bytes_written,
2819 &mut emission_state.record_counts,
2820 record,
2821 )?;
2822 }
2823 }
2824 }
2825 Ok(())
2826}
2827
2828fn validate_single_pass_writer_options(options: WriterOptions) -> Result<(), FormatError> {
2829 if options.volume_loss_tolerance != 0 {
2830 return Err(FormatError::WriterUnsupported(
2831 "streaming create cannot tolerate volume loss",
2832 ));
2833 }
2834 if options.target_volume_size.is_some() {
2835 return Err(FormatError::WriterUnsupported(
2836 "streaming create does not support target volume sizing",
2837 ));
2838 }
2839 Ok(())
2840}
2841
2842#[allow(clippy::too_many_arguments)]
2843fn begin_writer_emission_state<O: ArchiveWriteSink>(
2844 sink: &mut O,
2845 options: WriterOptions,
2846 crypto_header: &[u8],
2847 key_wrap_table: Option<&[u8]>,
2848 archive_uuid: [u8; 16],
2849 session_id: [u8; 16],
2850 volume_format_rev: u16,
2851 collect_data_leaf_hashes: bool,
2852) -> Result<WriterEmissionState, ArchiveWriteError> {
2853 let volume_count = usize::try_from(options.stripe_width)
2854 .map_err(|_| FormatError::WriterUnsupported("stripe_width"))?;
2855 sink.begin_archive(volume_count)?;
2856
2857 let mut state = WriterEmissionState {
2858 volume_headers: Vec::with_capacity(volume_count),
2859 bytes_written: vec![0u64; volume_count],
2860 record_counts: vec![0u64; volume_count],
2861 volume_format_rev,
2862 data_leaf_hashes: collect_data_leaf_hashes.then(Vec::new),
2863 next_block_index: 0,
2864 };
2865
2866 for volume_index in 0..volume_count {
2867 let volume_index_u32 = u32::try_from(volume_index)
2868 .map_err(|_| FormatError::WriterUnsupported("volume_index"))?;
2869 let volume_header = VolumeHeader {
2870 format_version: FORMAT_VERSION,
2871 volume_format_rev,
2872 volume_index: volume_index_u32,
2873 stripe_width: options.stripe_width,
2874 archive_uuid,
2875 session_id,
2876 crypto_header_offset: VOLUME_HEADER_LEN as u32,
2877 crypto_header_length: u32_len(crypto_header.len(), "CryptoHeader")?,
2878 header_crc32c: 0,
2879 };
2880 let volume_header_bytes = volume_header.to_bytes();
2881 sink.write_volume(volume_index, &volume_header_bytes)?;
2882 sink.write_volume(volume_index, crypto_header)?;
2883 let mut bytes_written = checked_u64_add(
2884 VOLUME_HEADER_LEN as u64,
2885 crypto_header.len() as u64,
2886 "volume header",
2887 )?;
2888 if let Some(key_wrap_table) = key_wrap_table {
2889 sink.write_volume(volume_index, key_wrap_table)?;
2890 bytes_written =
2891 checked_u64_add(bytes_written, key_wrap_table.len() as u64, "KeyWrapTableV1")?;
2892 }
2893 state.bytes_written[volume_index] = bytes_written;
2894 state.volume_headers.push(volume_header_bytes);
2895 }
2896
2897 Ok(state)
2898}
2899
2900fn plan_single_pass_writer_options(options: WriterOptions) -> Result<WriterOptions, FormatError> {
2901 let mut options = plan_writer_options(options)?;
2902 options.index_root_fec_data_shards = max_single_pass_index_root_data_shards(options)?;
2903 plan_writer_options(options)
2904}
2905
2906fn max_single_pass_index_root_data_shards(options: WriterOptions) -> Result<u16, FormatError> {
2907 let block_size_limit = (u32::MAX as u64 / options.block_size as u64).min(u16::MAX as u64);
2908 let mut low = MIN_INDEX_ROOT_FEC_DATA_SHARDS as u64;
2909 let mut high = block_size_limit;
2910 let mut best = low;
2911 while low <= high {
2912 let mid = low + (high - low) / 2;
2913 match compute_parity(mid, options) {
2914 Ok(parity)
2915 if mid + u64::from(parity) <= READER_MAX_INDEX_ROOT_FEC_CLASS_SHARDS as u64 =>
2916 {
2917 best = mid;
2918 low = mid + 1;
2919 }
2920 _ => {
2921 if mid == 0 {
2922 break;
2923 }
2924 high = mid - 1;
2925 }
2926 }
2927 }
2928 u16::try_from(best).map_err(|_| FormatError::WriterUnsupported("index_root_fec_data_shards"))
2929}
2930
2931impl<O: ArchiveWriteSink> StreamingArchiveWriter<'_, O> {
2932 pub(crate) fn write_regular_member_from_reader(
2933 &mut self,
2934 member: StreamingRegularMember,
2935 payload: &mut dyn Read,
2936 ) -> Result<(), ArchiveWriteError> {
2937 let path = member.archive_path;
2938 validate_file_path_bytes(&path, self.options.max_path_length)?;
2939 let prefix = build_regular_file_member_prefix(
2940 &path,
2941 member.file_data_size,
2942 member.mode,
2943 member.mtime,
2944 )?;
2945 let member_start = self.tar_total_size;
2946 let member_group_size = checked_u64_add(
2947 prefix.len() as u64,
2948 checked_u64_add(
2949 member.file_data_size,
2950 padding_to_512_u64(member.file_data_size),
2951 "tar member",
2952 )?,
2953 "tar member",
2954 )?;
2955 let member_index = self.tar_members.len();
2956 self.tar_members.push(TarMember {
2957 path,
2958 tar_member_group_start: member_start,
2959 tar_member_group_size: member_group_size,
2960 file_data_size: member.file_data_size,
2961 kind: TarEntryKind::Regular,
2962 mode: member.mode,
2963 mtime: member.mtime,
2964 });
2965
2966 let mut reader =
2967 StreamingMemberReader::new(Box::new(payload), prefix, member.file_data_size);
2968 let mut member_offset = 0u64;
2969 while member_offset < member_group_size {
2970 let remaining = member_group_size - member_offset;
2971 let max_chunk = remaining.min(self.options.chunk_size as u64);
2972 let mut chunk = vec![0u8; to_usize_writer(max_chunk, "payload chunk")?];
2973 reader
2974 .read_exact(&mut chunk)
2975 .map_err(ArchiveWriteError::Io)?;
2976 let mut chunk_len = chunk.len();
2977 let frame = loop {
2978 let candidate = &chunk[..chunk_len];
2979 let frame = compress_zstd_frame_with_jobs(
2980 candidate,
2981 self.options.zstd_level,
2982 self.options.jobs,
2983 )?;
2984 if payload_object_can_fit(frame.len(), self.options)? {
2985 break frame;
2986 }
2987 if chunk_len == 1 {
2988 return Err(FormatError::WriterUnsupported(
2989 "single-byte payload frame exceeds envelope object limits",
2990 )
2991 .into());
2992 }
2993 chunk_len = (chunk_len / 2).max(1);
2994 };
2995 if chunk_len < chunk.len() {
2996 reader.push_back(chunk[chunk_len..].to_vec());
2997 }
2998 let chunk = &chunk[..chunk_len];
2999 self.hasher.update(chunk);
3000 self.append_payload_frame(
3001 &frame,
3002 chunk_len,
3003 member_index,
3004 member_start,
3005 member_offset,
3006 member_group_size,
3007 )?;
3008 member_offset = checked_u64_add(member_offset, chunk_len as u64, "payload chunk")?;
3009 self.tar_total_size =
3010 checked_u64_add(self.tar_total_size, chunk_len as u64, "tar stream")?;
3011 }
3012 Ok(())
3013 }
3014
3015 fn append_payload_frame(
3016 &mut self,
3017 frame: &[u8],
3018 decompressed_size: usize,
3019 member_index: usize,
3020 member_start: u64,
3021 member_offset: u64,
3022 member_group_size: u64,
3023 ) -> Result<(), ArchiveWriteError> {
3024 if payload_envelope_needs_flush(&self.envelope, frame.len(), self.options)? {
3025 self.flush_payload_envelope()?;
3026 }
3027 if self.envelope.plaintext.is_empty() && !payload_object_can_fit(frame.len(), self.options)?
3028 {
3029 return Err(FormatError::WriterUnsupported(
3030 "payload frame exceeds envelope object limits",
3031 )
3032 .into());
3033 }
3034 let offset = u32_len(
3035 self.envelope.plaintext.len(),
3036 "FrameEntry.offset_in_envelope",
3037 )?;
3038 self.envelope.plaintext.extend_from_slice(frame);
3039 self.frames
3040 .push(payload_frame_metadata(PayloadFrameMetadataInput {
3041 frame_index: self.next_frame_index,
3042 envelope_index: self.envelope.envelope_index,
3043 member_index,
3044 offset_in_envelope: offset,
3045 compressed_size: frame.len(),
3046 decompressed_size,
3047 member_start,
3048 member_offset,
3049 member_group_size,
3050 })?);
3051 self.next_frame_index =
3052 checked_u64_add(self.next_frame_index, 1, "PayloadFrame.frame_index")?;
3053 Ok(())
3054 }
3055
3056 fn flush_payload_envelope(&mut self) -> Result<(), ArchiveWriteError> {
3057 if self.envelope.plaintext.is_empty() {
3058 return Ok(());
3059 }
3060 let plaintext_size = u32_len(
3061 self.envelope.plaintext.len(),
3062 "EnvelopeEntry.plaintext_size",
3063 )?;
3064 let object_plan = plan_encrypted_object(
3065 self.envelope.plaintext.len(),
3066 self.options.fec_data_shards,
3067 self.options.fec_parity_shards,
3068 self.options,
3069 )?;
3070 let extent = ObjectExtent::new(self.emission_state.next_block_index, object_plan)?;
3071 let object = encrypt_object(
3072 &self.envelope.plaintext,
3073 ObjectEncryptionContext {
3074 key: &self.subkeys.enc_key,
3075 nonce_seed: &self.subkeys.nonce_seed,
3076 domain: b"envelope",
3077 counter: self.envelope.envelope_index,
3078 data_kind: BlockKind::PayloadData,
3079 parity_kind: BlockKind::PayloadParity,
3080 data_shard_max: self.options.fec_data_shards,
3081 class_parity_shard_max: self.options.fec_parity_shards,
3082 archive_uuid: &self.archive_uuid,
3083 session_id: &self.session_id,
3084 },
3085 &mut self.emission_state.next_block_index,
3086 self.options,
3087 )?;
3088 validate_planned_extent(&object, extent)?;
3089 for record in &object.records {
3090 emit_block_record(
3091 self.sink,
3092 self.options,
3093 &mut self.emission_state.bytes_written,
3094 &mut self.emission_state.record_counts,
3095 self.emission_state.volume_format_rev,
3096 &mut self.emission_state.data_leaf_hashes,
3097 record,
3098 )?;
3099 }
3100 self.payload_block_count = checked_u64_add(
3101 self.payload_block_count,
3102 extent.data_block_count as u64,
3103 "payload",
3104 )?;
3105 self.payload_objects.push(PayloadObject {
3106 envelope_index: self.envelope.envelope_index,
3107 plaintext_size,
3108 object: extent,
3109 });
3110 self.envelope.envelope_index =
3111 checked_u64_add(self.envelope.envelope_index, 1, "EnvelopeEntry")?;
3112 self.envelope.plaintext.clear();
3113 Ok(())
3114 }
3115
3116 fn finish(
3117 mut self,
3118 master_key: &MasterKey,
3119 kdf_params: &KdfParams,
3120 key_wrap_records: Option<&KeyWrapRecordSource>,
3121 root_auth: Option<RootAuthWriterConfig<'_>>,
3122 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
3123 ) -> Result<WrittenArchiveSummary, ArchiveWriteError> {
3124 self.flush_payload_envelope()?;
3125 let digest = self.hasher.finalize();
3126 let mut content_sha256 = [0u8; 32];
3127 content_sha256.copy_from_slice(&digest);
3128 let payload = PayloadPlanning {
3129 tar_members: self.tar_members,
3130 frames: self.frames,
3131 payload_objects: self.payload_objects,
3132 payload_block_count: self.payload_block_count,
3133 tar_total_size: self.tar_total_size,
3134 content_sha256,
3135 };
3136 let plan = build_writer_plan_from_payload(
3137 payload,
3138 self.emission_state.next_block_index,
3139 master_key,
3140 self.options,
3141 None,
3142 kdf_params,
3143 key_wrap_records,
3144 self.archive_uuid,
3145 self.session_id,
3146 root_auth,
3147 )?;
3148 if plan.options != self.options || plan.crypto_header != self.crypto_header {
3149 return Err(FormatError::WriterUnsupported(
3150 "streaming tar stdin metadata exceeded the predeclared header class",
3151 )
3152 .into());
3153 }
3154 emit_writer_plan_suffix(
3155 &self.subkeys,
3156 root_auth,
3157 authenticator,
3158 plan,
3159 self.sink,
3160 self.emission_state,
3161 )
3162 }
3163}
3164
3165struct PayloadFramePlanState<'a> {
3166 envelope: &'a mut PayloadEnvelopeBuilder,
3167 payload_objects: &'a mut Vec<PayloadObject>,
3168 payload_block_count: &'a mut u64,
3169 next_block_index: &'a mut u64,
3170 frames: &'a mut Vec<PayloadFrame>,
3171 next_frame_index: &'a mut u64,
3172 options: WriterOptions,
3173}
3174
3175struct PayloadFramePlanInput<'a> {
3176 frame: &'a [u8],
3177 decompressed_size: usize,
3178 member_index: usize,
3179 member_start: u64,
3180 member_offset: u64,
3181 member_group_size: u64,
3182}
3183
3184fn append_payload_frame_to_plan(
3185 state: PayloadFramePlanState<'_>,
3186 input: PayloadFramePlanInput<'_>,
3187) -> Result<(), FormatError> {
3188 if payload_envelope_needs_flush(state.envelope, input.frame.len(), state.options)? {
3189 flush_payload_envelope_plan(
3190 state.envelope,
3191 state.payload_objects,
3192 state.payload_block_count,
3193 state.next_block_index,
3194 state.options,
3195 )?;
3196 }
3197 if state.envelope.plaintext.is_empty()
3198 && !payload_object_can_fit(input.frame.len(), state.options)?
3199 {
3200 return Err(FormatError::WriterUnsupported(
3201 "payload frame exceeds envelope object limits",
3202 ));
3203 }
3204 let offset = u32_len(
3205 state.envelope.plaintext.len(),
3206 "FrameEntry.offset_in_envelope",
3207 )?;
3208 state.envelope.plaintext.extend_from_slice(input.frame);
3209 state
3210 .frames
3211 .push(payload_frame_metadata(PayloadFrameMetadataInput {
3212 frame_index: *state.next_frame_index,
3213 envelope_index: state.envelope.envelope_index,
3214 member_index: input.member_index,
3215 offset_in_envelope: offset,
3216 compressed_size: input.frame.len(),
3217 decompressed_size: input.decompressed_size,
3218 member_start: input.member_start,
3219 member_offset: input.member_offset,
3220 member_group_size: input.member_group_size,
3221 })?);
3222 *state.next_frame_index =
3223 checked_u64_add(*state.next_frame_index, 1, "PayloadFrame.frame_index")?;
3224 Ok(())
3225}
3226
3227fn flush_payload_envelope_plan(
3228 envelope: &mut PayloadEnvelopeBuilder,
3229 payload_objects: &mut Vec<PayloadObject>,
3230 payload_block_count: &mut u64,
3231 next_block_index: &mut u64,
3232 options: WriterOptions,
3233) -> Result<(), FormatError> {
3234 let plaintext_size = u32_len(envelope.plaintext.len(), "EnvelopeEntry.plaintext_size")?;
3235 let object_plan = plan_encrypted_object(
3236 envelope.plaintext.len(),
3237 options.fec_data_shards,
3238 options.fec_parity_shards,
3239 options,
3240 )?;
3241 let extent = ObjectExtent::new(*next_block_index, object_plan)?;
3242 *next_block_index = extent.next_block_index()?;
3243 *payload_block_count = checked_u64_add(
3244 *payload_block_count,
3245 extent.data_block_count as u64,
3246 "payload",
3247 )?;
3248 payload_objects.push(PayloadObject {
3249 envelope_index: envelope.envelope_index,
3250 plaintext_size,
3251 object: extent,
3252 });
3253 envelope.envelope_index = checked_u64_add(envelope.envelope_index, 1, "EnvelopeEntry")?;
3254 envelope.plaintext.clear();
3255 Ok(())
3256}
3257
3258fn required_stripe_width_for_plan(
3259 plan: &WriterPlan,
3260 master_key: &MasterKey,
3261 target_volume_size: u64,
3262) -> Result<u32, FormatError> {
3263 let subkeys = writer_subkeys(
3264 master_key,
3265 plan.options.aead_algo,
3266 &plan.archive_uuid,
3267 &plan.session_id,
3268 )?;
3269 let mut max_volume_size = 0u64;
3270 let mut max_overhead = 0u64;
3271 let block_record_len = plan.options.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
3272 for volume_index in 0..plan.options.stripe_width {
3273 let block_count = striped_block_count(
3274 plan.total_block_count,
3275 plan.options.stripe_width,
3276 volume_index,
3277 );
3278 let volume_size = planned_v41_volume_size(plan, &subkeys, volume_index, block_count)?;
3279 max_volume_size = max_volume_size.max(volume_size);
3280 let record_bytes = checked_u64_mul(block_count, block_record_len, "volume records")?;
3281 let overhead =
3282 volume_size
3283 .checked_sub(record_bytes)
3284 .ok_or(FormatError::WriterInvariant(
3285 "planned volume record overflow",
3286 ))?;
3287 max_overhead = max_overhead.max(overhead);
3288 }
3289 if max_volume_size <= target_volume_size {
3290 return Ok(plan.options.stripe_width);
3291 }
3292 if target_volume_size <= max_overhead {
3293 return Err(FormatError::WriterUnsupported(
3294 "volume-size is too small for per-volume metadata",
3295 ));
3296 }
3297
3298 let records_per_volume = (target_volume_size - max_overhead) / block_record_len;
3299 if records_per_volume == 0 {
3300 return Err(FormatError::WriterUnsupported(
3301 "volume-size is too small for the configured block-size",
3302 ));
3303 }
3304
3305 let required = ceil_div(plan.total_block_count, records_per_volume)?
3306 .max(plan.options.volume_loss_tolerance as u64 + 1)
3307 .max(1);
3308 u32::try_from(required).map_err(|_| FormatError::WriterUnsupported("volume count"))
3309}
3310
3311fn planned_v41_volume_size(
3312 plan: &WriterPlan,
3313 subkeys: &Subkeys,
3314 volume_index: u32,
3315 block_count: u64,
3316) -> Result<u64, FormatError> {
3317 let volume_header = VolumeHeader {
3318 format_version: FORMAT_VERSION,
3319 volume_format_rev: plan.volume_format_rev,
3320 volume_index,
3321 stripe_width: plan.options.stripe_width,
3322 archive_uuid: plan.archive_uuid,
3323 session_id: plan.session_id,
3324 crypto_header_offset: VOLUME_HEADER_LEN as u32,
3325 crypto_header_length: u32_len(plan.crypto_header.len(), "CryptoHeader")?,
3326 header_crc32c: 0,
3327 };
3328 let volume_header_bytes = volume_header.to_bytes();
3329 let block_record_len = plan.options.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
3330 let block_record_bytes = checked_u64_mul(block_count, block_record_len, "volume records")?;
3331 let manifest_footer_offset = checked_u64_add(
3332 plan.block_records_offset,
3333 block_record_bytes,
3334 "volume records",
3335 )?;
3336 let manifest_footer = build_manifest_footer(
3337 subkeys,
3338 plan.options.aead_algo,
3339 plan.volume_format_rev,
3340 plan.archive_uuid,
3341 plan.session_id,
3342 volume_index,
3343 plan.options.stripe_width,
3344 &plan.index_root_extent,
3345 plan.index_root_plaintext.len(),
3346 )?;
3347 let root_auth_footer = plan
3348 .root_auth_footer_length
3349 .map(|length| vec![0u8; length as usize]);
3350 let root_auth_footer_offset = root_auth_footer
3351 .as_ref()
3352 .map(|_| {
3353 checked_u64_add(
3354 manifest_footer_offset,
3355 MANIFEST_FOOTER_LEN as u64,
3356 "RootAuthFooterV1",
3357 )
3358 })
3359 .transpose()?;
3360 let trailer_offset = checked_u64_add(
3361 manifest_footer_offset,
3362 MANIFEST_FOOTER_LEN as u64 + u64::from(plan.root_auth_footer_length.unwrap_or(0)),
3363 "VolumeTrailer",
3364 )?;
3365 let trailer = build_volume_trailer(VolumeTrailerBuildInput {
3366 subkeys,
3367 aead_algo: plan.options.aead_algo,
3368 volume_format_rev: plan.volume_format_rev,
3369 archive_uuid: plan.archive_uuid,
3370 session_id: plan.session_id,
3371 volume_index,
3372 block_count,
3373 bytes_written: trailer_offset,
3374 manifest_footer_offset,
3375 closed_at_ns: plan.options.closed_at_ns,
3376 root_auth_footer: root_auth_footer_offset.zip(plan.root_auth_footer_length),
3377 })?;
3378 let cmra_offset = checked_u64_add(trailer_offset, VOLUME_TRAILER_LEN as u64, "CMRA")?;
3379 let cmra = build_v41_cmra(CmraBuildInput {
3380 volume_format_rev: plan.volume_format_rev,
3381 volume_header_bytes: &volume_header_bytes,
3382 crypto_header: &plan.crypto_header,
3383 block_count,
3384 block_records_offset: plan.block_records_offset,
3385 manifest_footer_offset,
3386 manifest_footer: &manifest_footer,
3387 root_auth_footer_offset,
3388 root_auth_footer: root_auth_footer.as_deref(),
3389 key_wrap_table: plan.key_wrap_table.as_deref(),
3390 trailer_offset,
3391 trailer: &trailer,
3392 cmra_offset,
3393 options: plan.options,
3394 archive_uuid: plan.archive_uuid,
3395 session_id: plan.session_id,
3396 volume_index,
3397 })?;
3398 checked_u64_add(
3399 checked_u64_add(cmra_offset, cmra.bytes.len() as u64, "CMRA")?,
3400 (CRITICAL_RECOVERY_LOCATOR_LEN * 2) as u64,
3401 "critical recovery locators",
3402 )
3403}
3404
3405fn striped_block_count(total_block_count: u64, stripe_width: u32, volume_index: u32) -> u64 {
3406 let volume_index = volume_index as u64;
3407 let stripe_width = stripe_width as u64;
3408 if total_block_count <= volume_index {
3409 0
3410 } else {
3411 (total_block_count - 1 - volume_index) / stripe_width + 1
3412 }
3413}
3414
3415fn emit_writer_plan<S, O>(
3416 files: &[S],
3417 master_key: &MasterKey,
3418 dictionary: Option<&[u8]>,
3419 root_auth: Option<RootAuthWriterConfig<'_>>,
3420 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
3421 plan: WriterPlan,
3422 sink: &mut O,
3423) -> Result<WrittenArchiveSummary, ArchiveWriteError>
3424where
3425 S: RegularFileSource,
3426 O: ArchiveWriteSink,
3427{
3428 let subkeys = writer_subkeys(
3429 master_key,
3430 plan.options.aead_algo,
3431 &plan.archive_uuid,
3432 &plan.session_id,
3433 )?;
3434 let mut state = begin_writer_emission_state(
3435 sink,
3436 plan.options,
3437 &plan.crypto_header,
3438 plan.key_wrap_table.as_deref(),
3439 plan.archive_uuid,
3440 plan.session_id,
3441 plan.volume_format_rev,
3442 root_auth.is_some(),
3443 )?;
3444
3445 let emit_payload_started = Instant::now();
3446 emit_payload_stream(
3447 files,
3448 dictionary,
3449 &subkeys,
3450 &plan,
3451 &mut state.next_block_index,
3452 sink,
3453 &mut state.bytes_written,
3454 &mut state.record_counts,
3455 &mut state.data_leaf_hashes,
3456 )?;
3457 let emit_payload = emit_payload_started.elapsed();
3458
3459 let mut summary =
3460 emit_writer_plan_suffix(&subkeys, root_auth, authenticator, plan, sink, state)?;
3461 summary.timings.emit_payload += emit_payload;
3462 Ok(summary)
3463}
3464
3465fn emit_writer_plan_suffix<O: ArchiveWriteSink>(
3466 subkeys: &Subkeys,
3467 root_auth: Option<RootAuthWriterConfig<'_>>,
3468 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
3469 plan: WriterPlan,
3470 sink: &mut O,
3471 mut state: WriterEmissionState,
3472) -> Result<WrittenArchiveSummary, ArchiveWriteError> {
3473 let emit_metadata_started = Instant::now();
3474 let volume_count = plan.options.stripe_width as usize;
3475
3476 for planned in &plan.index_shard_objects {
3477 emit_encrypted_object(
3478 &planned.compressed,
3479 &subkeys.index_shard_key,
3480 &subkeys.index_nonce_seed,
3481 b"idxshard",
3482 planned.shard_index,
3483 BlockKind::IndexShardData,
3484 BlockKind::IndexShardParity,
3485 plan.options.index_fec_data_shards,
3486 plan.options.index_fec_parity_shards,
3487 &mut state.next_block_index,
3488 plan.options,
3489 &plan.archive_uuid,
3490 &plan.session_id,
3491 planned.extent,
3492 None,
3493 plan.volume_format_rev,
3494 sink,
3495 &mut state.bytes_written,
3496 &mut state.record_counts,
3497 &mut state.data_leaf_hashes,
3498 )?;
3499 }
3500
3501 let dictionary_records = if let (Some(compressed), Some((extent, _))) =
3502 (plan.compressed_dictionary.as_ref(), plan.dictionary_extent)
3503 {
3504 let object = emit_encrypted_object(
3505 compressed,
3506 &subkeys.dictionary_key,
3507 &subkeys.index_nonce_seed,
3508 b"dict",
3509 0,
3510 BlockKind::DictionaryData,
3511 BlockKind::DictionaryParity,
3512 plan.options.index_root_fec_data_shards,
3513 plan.options.index_root_fec_parity_shards,
3514 &mut state.next_block_index,
3515 plan.options,
3516 &plan.archive_uuid,
3517 &plan.session_id,
3518 extent,
3519 Some(MetadataObjectKind::Dictionary),
3520 plan.volume_format_rev,
3521 sink,
3522 &mut state.bytes_written,
3523 &mut state.record_counts,
3524 &mut state.data_leaf_hashes,
3525 )?;
3526 Some(object.records)
3527 } else {
3528 None
3529 };
3530
3531 for planned in &plan.directory_hint_objects {
3532 emit_encrypted_object(
3533 &planned.compressed,
3534 &subkeys.dir_hint_key,
3535 &subkeys.index_nonce_seed,
3536 b"dirhint",
3537 planned.hint_shard_index,
3538 BlockKind::DirectoryHintData,
3539 BlockKind::DirectoryHintParity,
3540 plan.options.index_fec_data_shards,
3541 plan.options.index_fec_parity_shards,
3542 &mut state.next_block_index,
3543 plan.options,
3544 &plan.archive_uuid,
3545 &plan.session_id,
3546 planned.extent,
3547 None,
3548 plan.volume_format_rev,
3549 sink,
3550 &mut state.bytes_written,
3551 &mut state.record_counts,
3552 &mut state.data_leaf_hashes,
3553 )?;
3554 }
3555
3556 let index_root_object = emit_encrypted_object(
3557 &plan.compressed_index_root,
3558 &subkeys.index_root_key,
3559 &subkeys.index_nonce_seed,
3560 b"idxroot",
3561 0,
3562 BlockKind::IndexRootData,
3563 BlockKind::IndexRootParity,
3564 plan.options.index_root_fec_data_shards,
3565 plan.options.index_root_fec_parity_shards,
3566 &mut state.next_block_index,
3567 plan.options,
3568 &plan.archive_uuid,
3569 &plan.session_id,
3570 plan.index_root_extent,
3571 Some(MetadataObjectKind::IndexRoot),
3572 plan.volume_format_rev,
3573 sink,
3574 &mut state.bytes_written,
3575 &mut state.record_counts,
3576 &mut state.data_leaf_hashes,
3577 )?;
3578 if state.next_block_index != plan.total_block_count {
3579 return Err(FormatError::WriterInvariant("streaming writer block plan mismatch").into());
3580 }
3581
3582 let volume_zero_manifest = build_manifest_footer(
3583 subkeys,
3584 plan.options.aead_algo,
3585 plan.volume_format_rev,
3586 plan.archive_uuid,
3587 plan.session_id,
3588 0,
3589 plan.options.stripe_width,
3590 &plan.index_root_extent,
3591 plan.index_root_plaintext.len(),
3592 )?;
3593 let root_auth_footer = match root_auth {
3594 Some(config) => {
3595 let signer = authenticator.ok_or(FormatError::WriterInvariant(
3596 "missing root-auth authenticator",
3597 ))?;
3598 Some(build_root_auth_footer_from_leaf_hashes(
3599 config,
3600 signer,
3601 RootAuthFooterBuildInput {
3602 archive_uuid: plan.archive_uuid,
3603 session_id: plan.session_id,
3604 volume_format_rev: plan.volume_format_rev,
3605 options: plan.options,
3606 crypto_header: &plan.crypto_header,
3607 volume_zero_manifest: &volume_zero_manifest,
3608 index_root_plaintext: &plan.index_root_plaintext,
3609 index_root_extent: plan.index_root_extent,
3610 dictionary_extent: plan.dictionary_extent,
3611 shard_entries: &plan.shard_entries,
3612 payload_objects: &plan.payload_objects,
3613 directory_hint_entries: &plan.directory_hint_entries,
3614 data_leaf_hashes: state.data_leaf_hashes.as_deref().ok_or(
3615 FormatError::WriterInvariant("missing root-auth data leaf hashes"),
3616 )?,
3617 },
3618 )?)
3619 }
3620 None => None,
3621 };
3622 let root_auth_footer_length = root_auth_footer
3623 .as_ref()
3624 .map(|footer| u32_len(footer.len(), "RootAuthFooterV1"))
3625 .transpose()?;
3626
3627 for volume_index in 0..volume_count {
3628 let volume_index_u32 = u32::try_from(volume_index)
3629 .map_err(|_| FormatError::WriterUnsupported("volume_index"))?;
3630 let manifest_footer_offset = state.bytes_written[volume_index];
3631 let manifest_footer = build_manifest_footer(
3632 subkeys,
3633 plan.options.aead_algo,
3634 plan.volume_format_rev,
3635 plan.archive_uuid,
3636 plan.session_id,
3637 volume_index_u32,
3638 plan.options.stripe_width,
3639 &plan.index_root_extent,
3640 plan.index_root_plaintext.len(),
3641 )?;
3642 sink.write_volume(volume_index, &manifest_footer)?;
3643 state.bytes_written[volume_index] = checked_u64_add(
3644 state.bytes_written[volume_index],
3645 MANIFEST_FOOTER_LEN as u64,
3646 "ManifestFooter",
3647 )?;
3648
3649 let root_auth_footer_offset = if let Some(root_auth_footer) = root_auth_footer.as_ref() {
3650 let offset = state.bytes_written[volume_index];
3651 sink.write_volume(volume_index, root_auth_footer)?;
3652 state.bytes_written[volume_index] = checked_u64_add(
3653 state.bytes_written[volume_index],
3654 root_auth_footer.len() as u64,
3655 "RootAuthFooterV1",
3656 )?;
3657 Some(offset)
3658 } else {
3659 None
3660 };
3661
3662 let trailer_offset = state.bytes_written[volume_index];
3663 let trailer = build_volume_trailer(VolumeTrailerBuildInput {
3664 subkeys,
3665 aead_algo: plan.options.aead_algo,
3666 volume_format_rev: plan.volume_format_rev,
3667 archive_uuid: plan.archive_uuid,
3668 session_id: plan.session_id,
3669 volume_index: volume_index_u32,
3670 block_count: state.record_counts[volume_index],
3671 bytes_written: trailer_offset,
3672 manifest_footer_offset,
3673 closed_at_ns: plan.options.closed_at_ns,
3674 root_auth_footer: root_auth_footer_offset.zip(root_auth_footer_length),
3675 })?;
3676 sink.write_volume(volume_index, &trailer)?;
3677 state.bytes_written[volume_index] = checked_u64_add(
3678 state.bytes_written[volume_index],
3679 VOLUME_TRAILER_LEN as u64,
3680 "VolumeTrailer",
3681 )?;
3682
3683 let cmra_offset = state.bytes_written[volume_index];
3684 let cmra = build_v41_cmra(CmraBuildInput {
3685 volume_format_rev: plan.volume_format_rev,
3686 volume_header_bytes: &state.volume_headers[volume_index],
3687 crypto_header: &plan.crypto_header,
3688 block_count: state.record_counts[volume_index],
3689 block_records_offset: plan.block_records_offset,
3690 manifest_footer_offset,
3691 manifest_footer: &manifest_footer,
3692 root_auth_footer_offset,
3693 root_auth_footer: root_auth_footer.as_deref(),
3694 key_wrap_table: plan.key_wrap_table.as_deref(),
3695 trailer_offset,
3696 trailer: &trailer,
3697 cmra_offset,
3698 options: plan.options,
3699 archive_uuid: plan.archive_uuid,
3700 session_id: plan.session_id,
3701 volume_index: volume_index_u32,
3702 })?;
3703 sink.write_volume(volume_index, &cmra.bytes)?;
3704 state.bytes_written[volume_index] = checked_u64_add(
3705 state.bytes_written[volume_index],
3706 cmra.bytes.len() as u64,
3707 "CMRA",
3708 )?;
3709 let locator_base = CriticalRecoveryLocator {
3710 volume_format_rev: plan.volume_format_rev,
3711 cmra_offset,
3712 cmra_length: u32_len(cmra.bytes.len(), "CMRA")?,
3713 volume_trailer_offset: trailer_offset,
3714 body_bytes_before_cmra: cmra_offset,
3715 archive_uuid_hint: plan.archive_uuid,
3716 session_id_hint: plan.session_id,
3717 volume_index_hint: volume_index_u32,
3718 locator_sequence: 1,
3719 cmra_shard_size: cmra.shard_size,
3720 cmra_data_shard_count: cmra.data_shard_count,
3721 cmra_parity_shard_count: cmra.parity_shard_count,
3722 cmra_image_length: cmra.image_length,
3723 cmra_image_sha256: cmra.image_sha256,
3724 locator_crc32c: 0,
3725 };
3726 let mirror = locator_base.to_bytes();
3727 sink.write_volume(volume_index, &mirror)?;
3728 let final_locator = CriticalRecoveryLocator {
3729 locator_sequence: 0,
3730 ..locator_base
3731 }
3732 .to_bytes();
3733 sink.write_volume(volume_index, &final_locator)?;
3734 state.bytes_written[volume_index] = checked_u64_add(
3735 state.bytes_written[volume_index],
3736 (CRITICAL_RECOVERY_LOCATOR_LEN * 2) as u64,
3737 "critical recovery locators",
3738 )?;
3739
3740 if volume_index == 0 {
3741 debug_assert_eq!(volume_zero_manifest, manifest_footer);
3742 }
3743 }
3744
3745 let bootstrap_sidecar_bytes = if plan.options.stripe_width == 1 {
3746 let sidecar = build_bootstrap_sidecar(
3747 subkeys,
3748 plan.options.aead_algo,
3749 plan.volume_format_rev,
3750 plan.archive_uuid,
3751 plan.session_id,
3752 &volume_zero_manifest,
3753 &index_root_object.records,
3754 dictionary_records.as_deref(),
3755 )?;
3756 let sidecar_len = sidecar.len() as u64;
3757 sink.write_bootstrap_sidecar(&sidecar)?;
3758 sidecar_len
3759 } else {
3760 0
3761 };
3762
3763 Ok(WrittenArchiveSummary {
3764 volume_count,
3765 archive_bytes: state.bytes_written.iter().sum(),
3766 bootstrap_sidecar_bytes,
3767 archive_uuid: plan.archive_uuid,
3768 session_id: plan.session_id,
3769 timings: WriterTimings {
3770 emit_metadata: emit_metadata_started.elapsed(),
3771 ..WriterTimings::default()
3772 },
3773 })
3774}
3775
3776#[allow(clippy::too_many_arguments)]
3777fn emit_encrypted_object<O: ArchiveWriteSink>(
3778 payload: &[u8],
3779 key: &[u8; 32],
3780 nonce_seed: &[u8; 32],
3781 domain: &[u8],
3782 counter: u64,
3783 data_kind: BlockKind,
3784 parity_kind: BlockKind,
3785 data_shard_max: u16,
3786 class_parity_shard_max: u16,
3787 next_block_index: &mut u64,
3788 options: WriterOptions,
3789 archive_uuid: &[u8; 16],
3790 session_id: &[u8; 16],
3791 expected_extent: ObjectExtent,
3792 metadata_kind: Option<MetadataObjectKind>,
3793 volume_format_rev: u16,
3794 sink: &mut O,
3795 bytes_written: &mut [u64],
3796 record_counts: &mut [u64],
3797 data_leaf_hashes: &mut Option<Vec<(u64, [u8; 32])>>,
3798) -> Result<EncryptedObject, ArchiveWriteError> {
3799 let object = encrypt_object(
3800 payload,
3801 ObjectEncryptionContext {
3802 key,
3803 nonce_seed,
3804 domain,
3805 counter,
3806 data_kind,
3807 parity_kind,
3808 data_shard_max,
3809 class_parity_shard_max,
3810 archive_uuid,
3811 session_id,
3812 },
3813 next_block_index,
3814 options,
3815 )
3816 .map_err(|error| match metadata_kind {
3817 Some(kind) => map_metadata_encrypt_error(error, kind),
3818 None => error,
3819 })?;
3820 validate_planned_extent(&object, expected_extent)?;
3821 for record in &object.records {
3822 emit_block_record(
3823 sink,
3824 options,
3825 bytes_written,
3826 record_counts,
3827 volume_format_rev,
3828 data_leaf_hashes,
3829 record,
3830 )?;
3831 }
3832 Ok(object)
3833}
3834
3835fn emit_block_record<O: ArchiveWriteSink>(
3836 sink: &mut O,
3837 options: WriterOptions,
3838 bytes_written: &mut [u64],
3839 record_counts: &mut [u64],
3840 volume_format_rev: u16,
3841 data_leaf_hashes: &mut Option<Vec<(u64, [u8; 32])>>,
3842 record: &BlockRecord,
3843) -> Result<(), ArchiveWriteError> {
3844 let volume_index = (record.block_index % options.stripe_width as u64) as usize;
3845 let record_bytes = record.to_bytes();
3846 sink.write_volume(volume_index, &record_bytes)?;
3847 bytes_written[volume_index] = checked_u64_add(
3848 bytes_written[volume_index],
3849 record_bytes.len() as u64,
3850 "BlockRecord",
3851 )?;
3852 record_counts[volume_index] =
3853 checked_u64_add(record_counts[volume_index], 1, "BlockRecord count")?;
3854 if let Some(data_leaf_hashes) = data_leaf_hashes.as_mut() {
3855 if record.kind.is_data() {
3856 data_leaf_hashes.push((
3857 record.block_index,
3858 data_block_merkle_leaf_hash_for_revision(
3859 FORMAT_VERSION,
3860 volume_format_rev,
3861 record.block_index,
3862 record.kind,
3863 record.flags,
3864 &record.payload,
3865 )?,
3866 ));
3867 }
3868 }
3869 Ok(())
3870}
3871
3872fn emit_serialized_block_record<O: ArchiveWriteSink>(
3873 sink: &mut O,
3874 options: WriterOptions,
3875 bytes_written: &mut [u64],
3876 record_counts: &mut [u64],
3877 record: &SerializedBlockRecord,
3878) -> Result<(), ArchiveWriteError> {
3879 let volume_index = (record.block_index % options.stripe_width as u64) as usize;
3880 sink.write_volume(volume_index, &record.bytes)?;
3881 bytes_written[volume_index] = checked_u64_add(
3882 bytes_written[volume_index],
3883 record.bytes.len() as u64,
3884 "BlockRecord",
3885 )?;
3886 record_counts[volume_index] =
3887 checked_u64_add(record_counts[volume_index], 1, "BlockRecord count")?;
3888 Ok(())
3889}
3890
3891#[allow(clippy::too_many_arguments)]
3892fn emit_payload_stream<S, O>(
3893 files: &[S],
3894 dictionary: Option<&[u8]>,
3895 subkeys: &Subkeys,
3896 plan: &WriterPlan,
3897 next_block_index: &mut u64,
3898 sink: &mut O,
3899 bytes_written: &mut [u64],
3900 record_counts: &mut [u64],
3901 data_leaf_hashes: &mut Option<Vec<(u64, [u8; 32])>>,
3902) -> Result<(), ArchiveWriteError>
3903where
3904 S: RegularFileSource,
3905 O: ArchiveWriteSink,
3906{
3907 let mut envelope = PayloadEnvelopeBuilder {
3908 envelope_index: 0,
3909 plaintext: Vec::new(),
3910 };
3911 let mut next_frame_index = 0u64;
3912
3913 for (member_index, file) in files.iter().enumerate() {
3914 let member = plan
3915 .tar_members
3916 .get(member_index)
3917 .ok_or(FormatError::WriterInvariant(
3918 "planned tar member is missing",
3919 ))?;
3920 let current_path =
3921 normalize_lookup_file_path(file.archive_path(), plan.options.max_path_length)?;
3922 if current_path != member.path
3923 || file.file_data_size() != member.file_data_size
3924 || file.mode() != member.mode
3925 || file.mtime() != member.mtime
3926 {
3927 return Err(FormatError::WriterInvariant(
3928 "file source changed between planning and emission",
3929 )
3930 .into());
3931 }
3932 let prefix = build_regular_file_member_prefix(
3933 &member.path,
3934 member.file_data_size,
3935 member.mode,
3936 member.mtime,
3937 )?;
3938 let mut reader = StreamingMemberReader::new(file.open()?, prefix, member.file_data_size);
3939 let mut member_offset = 0u64;
3940 while member_offset < member.tar_member_group_size {
3941 let remaining = member.tar_member_group_size - member_offset;
3942 let max_chunk = remaining.min(plan.options.chunk_size as u64);
3943 let mut chunk = vec![0u8; to_usize_writer(max_chunk, "payload chunk")?];
3944 reader
3945 .read_exact(&mut chunk)
3946 .map_err(ArchiveWriteError::Io)?;
3947 let mut chunk_len = chunk.len();
3948 let frame = loop {
3949 let candidate = &chunk[..chunk_len];
3950 let frame = if let Some(dictionary) = dictionary {
3951 compress_zstd_frame_with_dictionary_and_jobs(
3952 candidate,
3953 plan.options.zstd_level,
3954 dictionary,
3955 plan.options.jobs,
3956 )?
3957 } else {
3958 compress_zstd_frame_with_jobs(
3959 candidate,
3960 plan.options.zstd_level,
3961 plan.options.jobs,
3962 )?
3963 };
3964 if payload_object_can_fit(frame.len(), plan.options)? {
3965 break frame;
3966 }
3967 if chunk_len == 1 {
3968 return Err(FormatError::WriterUnsupported(
3969 "single-byte payload frame exceeds envelope object limits",
3970 )
3971 .into());
3972 }
3973 chunk_len = (chunk_len / 2).max(1);
3974 };
3975 if chunk_len < chunk.len() {
3976 reader.push_back(chunk[chunk_len..].to_vec());
3977 }
3978 append_payload_frame_to_emit(
3979 &mut envelope,
3980 &frame,
3981 chunk_len,
3982 member_index,
3983 member.tar_member_group_start,
3984 member_offset,
3985 member.tar_member_group_size,
3986 &mut next_frame_index,
3987 subkeys,
3988 plan,
3989 next_block_index,
3990 sink,
3991 bytes_written,
3992 record_counts,
3993 data_leaf_hashes,
3994 )?;
3995 member_offset = checked_u64_add(member_offset, chunk_len as u64, "payload chunk")?;
3996 }
3997 }
3998
3999 if !envelope.plaintext.is_empty() {
4000 flush_payload_envelope_emit(
4001 &mut envelope,
4002 subkeys,
4003 plan,
4004 next_block_index,
4005 sink,
4006 bytes_written,
4007 record_counts,
4008 data_leaf_hashes,
4009 )?;
4010 }
4011 if next_frame_index != plan.frames.len() as u64
4012 || envelope.envelope_index != plan.payload_objects.len() as u64
4013 {
4014 return Err(FormatError::WriterInvariant("streaming payload plan mismatch").into());
4015 }
4016 Ok(())
4017}
4018
4019#[allow(clippy::too_many_arguments)]
4020fn append_payload_frame_to_emit<O: ArchiveWriteSink>(
4021 envelope: &mut PayloadEnvelopeBuilder,
4022 frame: &[u8],
4023 decompressed_size: usize,
4024 member_index: usize,
4025 member_start: u64,
4026 member_offset: u64,
4027 member_group_size: u64,
4028 next_frame_index: &mut u64,
4029 subkeys: &Subkeys,
4030 plan: &WriterPlan,
4031 next_block_index: &mut u64,
4032 sink: &mut O,
4033 bytes_written: &mut [u64],
4034 record_counts: &mut [u64],
4035 data_leaf_hashes: &mut Option<Vec<(u64, [u8; 32])>>,
4036) -> Result<(), ArchiveWriteError> {
4037 if payload_envelope_needs_flush(envelope, frame.len(), plan.options)? {
4038 flush_payload_envelope_emit(
4039 envelope,
4040 subkeys,
4041 plan,
4042 next_block_index,
4043 sink,
4044 bytes_written,
4045 record_counts,
4046 data_leaf_hashes,
4047 )?;
4048 }
4049 if envelope.plaintext.is_empty() && !payload_object_can_fit(frame.len(), plan.options)? {
4050 return Err(
4051 FormatError::WriterUnsupported("payload frame exceeds envelope object limits").into(),
4052 );
4053 }
4054 let offset = u32_len(envelope.plaintext.len(), "FrameEntry.offset_in_envelope")?;
4055 let actual = payload_frame_metadata(PayloadFrameMetadataInput {
4056 frame_index: *next_frame_index,
4057 envelope_index: envelope.envelope_index,
4058 member_index,
4059 offset_in_envelope: offset,
4060 compressed_size: frame.len(),
4061 decompressed_size,
4062 member_start,
4063 member_offset,
4064 member_group_size,
4065 })?;
4066 let expected =
4067 plan.frames
4068 .get(*next_frame_index as usize)
4069 .ok_or(FormatError::WriterInvariant(
4070 "planned payload frame is missing",
4071 ))?;
4072 if expected.envelope_index != actual.envelope_index
4073 || expected.member_index != actual.member_index
4074 || expected.offset_in_envelope != actual.offset_in_envelope
4075 || expected.compressed_size != actual.compressed_size
4076 || expected.decompressed_size != actual.decompressed_size
4077 || expected.flags != actual.flags
4078 || expected.tar_stream_offset != actual.tar_stream_offset
4079 {
4080 return Err(
4081 FormatError::WriterInvariant("emitted payload frame does not match plan").into(),
4082 );
4083 }
4084 envelope.plaintext.extend_from_slice(frame);
4085 *next_frame_index = checked_u64_add(*next_frame_index, 1, "PayloadFrame.frame_index")?;
4086 Ok(())
4087}
4088
4089#[allow(clippy::too_many_arguments)]
4090fn flush_payload_envelope_emit<O: ArchiveWriteSink>(
4091 envelope: &mut PayloadEnvelopeBuilder,
4092 subkeys: &Subkeys,
4093 plan: &WriterPlan,
4094 next_block_index: &mut u64,
4095 sink: &mut O,
4096 bytes_written: &mut [u64],
4097 record_counts: &mut [u64],
4098 data_leaf_hashes: &mut Option<Vec<(u64, [u8; 32])>>,
4099) -> Result<(), ArchiveWriteError> {
4100 let expected = plan
4101 .payload_objects
4102 .get(envelope.envelope_index as usize)
4103 .ok_or(FormatError::WriterInvariant(
4104 "planned payload envelope is missing",
4105 ))?;
4106 if expected.envelope_index != envelope.envelope_index
4107 || expected.plaintext_size
4108 != u32_len(envelope.plaintext.len(), "EnvelopeEntry.plaintext_size")?
4109 {
4110 return Err(
4111 FormatError::WriterInvariant("emitted payload envelope does not match plan").into(),
4112 );
4113 }
4114 emit_encrypted_object(
4115 &envelope.plaintext,
4116 &subkeys.enc_key,
4117 &subkeys.nonce_seed,
4118 b"envelope",
4119 envelope.envelope_index,
4120 BlockKind::PayloadData,
4121 BlockKind::PayloadParity,
4122 plan.options.fec_data_shards,
4123 plan.options.fec_parity_shards,
4124 next_block_index,
4125 plan.options,
4126 &plan.archive_uuid,
4127 &plan.session_id,
4128 expected.object,
4129 None,
4130 plan.volume_format_rev,
4131 sink,
4132 bytes_written,
4133 record_counts,
4134 data_leaf_hashes,
4135 )?;
4136 envelope.envelope_index = checked_u64_add(envelope.envelope_index, 1, "EnvelopeEntry")?;
4137 envelope.plaintext.clear();
4138 Ok(())
4139}
4140
4141pub fn write_empty_archive(master_key: &MasterKey) -> Result<WrittenArchive, FormatError> {
4142 write_archive(&[], master_key, WriterOptions::default())
4143}
4144
4145fn plan_writer_options(mut options: WriterOptions) -> Result<WriterOptions, FormatError> {
4146 if options.jobs == 0 {
4147 return Err(FormatError::WriterUnsupported("jobs must be at least 1"));
4148 }
4149 if options.block_size < MIN_BLOCK_SIZE || options.block_size % 2 != 0 {
4150 return Err(FormatError::WriterUnsupported(
4151 "writer requires an even block size of at least 4096",
4152 ));
4153 }
4154 if options.stripe_width == 0 {
4155 return Err(FormatError::WriterUnsupported(
4156 "stripe_width must be non-zero",
4157 ));
4158 }
4159 if options.volume_loss_tolerance as u32 >= options.stripe_width {
4160 return Err(FormatError::WriterUnsupported(
4161 "volume_loss_tolerance must be less than stripe_width",
4162 ));
4163 }
4164 if options.stripe_width == 1 && options.volume_loss_tolerance != 0 {
4165 return Err(FormatError::WriterUnsupported(
4166 "single-volume archives cannot tolerate volume loss",
4167 ));
4168 }
4169 if matches!(options.target_volume_size, Some(0)) {
4170 return Err(FormatError::WriterUnsupported(
4171 "target_volume_size must be non-zero",
4172 ));
4173 }
4174 if options.bit_rot_buffer_pct > 100 {
4175 return Err(FormatError::WriterUnsupported(
4176 "bit_rot_buffer_pct must be at most 100",
4177 ));
4178 }
4179 if options.chunk_size == 0 || options.chunk_size > options.envelope_target_size {
4180 return Err(FormatError::WriterUnsupported(
4181 "chunk_size must be non-zero and no larger than envelope_target_size",
4182 ));
4183 }
4184 if options.fec_data_shards == 0
4185 || options.index_fec_data_shards == 0
4186 || options.index_root_fec_data_shards == 0
4187 {
4188 return Err(FormatError::WriterUnsupported(
4189 "FEC data shard class maxima must be non-zero",
4190 ));
4191 }
4192 options.index_root_fec_data_shards = options
4193 .index_root_fec_data_shards
4194 .max(MIN_INDEX_ROOT_FEC_DATA_SHARDS);
4195 options.fec_parity_shards =
4196 compute_parity_u16(options.fec_data_shards as u64, options, "fec_parity_shards")?;
4197 options.index_fec_parity_shards = compute_parity_u16(
4198 options.index_fec_data_shards as u64,
4199 options,
4200 "index_fec_parity_shards",
4201 )?;
4202 options.index_root_fec_parity_shards = compute_parity_u16(
4203 options.index_root_fec_data_shards as u64,
4204 options,
4205 "index_root_fec_parity_shards",
4206 )?;
4207 validate_writer_options_match_reader_caps(options)?;
4208 Ok(options)
4209}
4210
4211fn validate_writer_options_match_reader_caps(options: WriterOptions) -> Result<(), FormatError> {
4212 CryptoHeaderFixed {
4213 length: CRYPTO_HEADER_FIXED_LEN as u32,
4214 compression_algo: CompressionAlgo::ZstdFramed,
4215 aead_algo: options.aead_algo,
4216 fec_algo: FecAlgo::ReedSolomonGF16,
4217 kdf_algo: if options.aead_algo.is_encrypted() {
4218 KdfAlgo::Raw
4219 } else {
4220 KdfAlgo::None
4221 },
4222 chunk_size: options.chunk_size,
4223 envelope_target_size: options.envelope_target_size,
4224 block_size: options.block_size,
4225 fec_data_shards: options.fec_data_shards,
4226 fec_parity_shards: options.fec_parity_shards,
4227 index_fec_data_shards: options.index_fec_data_shards,
4228 index_fec_parity_shards: options.index_fec_parity_shards,
4229 index_root_fec_data_shards: options.index_root_fec_data_shards,
4230 index_root_fec_parity_shards: options.index_root_fec_parity_shards,
4231 stripe_width: options.stripe_width,
4232 volume_loss_tolerance: options.volume_loss_tolerance,
4233 bit_rot_buffer_pct: options.bit_rot_buffer_pct,
4234 has_dictionary: 0,
4235 max_path_length: options.max_path_length,
4236 expected_volume_size: options.target_volume_size.unwrap_or(0),
4237 }
4238 .validate_supported_profile()
4239}
4240
4241fn build_crypto_header(
4242 options: WriterOptions,
4243 volume_format_rev: u16,
4244 has_dictionary: bool,
4245 subkeys: &Subkeys,
4246 archive_uuid: &[u8; 16],
4247 session_id: &[u8; 16],
4248 kdf_params: &KdfParams,
4249) -> Result<Vec<u8>, FormatError> {
4250 let kdf_payload = serialize_kdf_params(kdf_params)?;
4251 let length = CRYPTO_HEADER_FIXED_LEN
4252 .checked_add(kdf_payload.len())
4253 .and_then(|value| value.checked_add(CRYPTO_EXTENSION_HEADER_LEN))
4254 .and_then(|value| value.checked_add(CRYPTO_HEADER_HMAC_LEN))
4255 .ok_or(FormatError::WriterUnsupported(
4256 "CryptoHeader length overflow",
4257 ))?;
4258 let kdf_algo = match kdf_params {
4259 KdfParams::None => KdfAlgo::None,
4260 KdfParams::Raw => KdfAlgo::Raw,
4261 KdfParams::Argon2id { .. } => KdfAlgo::Argon2id,
4262 KdfParams::RecipientWrap { .. } => KdfAlgo::RecipientWrap,
4263 };
4264 match (options.aead_algo, kdf_algo) {
4265 (AeadAlgo::None, KdfAlgo::None) => {}
4266 (aead_algo, KdfAlgo::Raw | KdfAlgo::Argon2id | KdfAlgo::RecipientWrap)
4267 if aead_algo.is_encrypted() => {}
4268 _ => {
4269 return Err(FormatError::InvalidProtectionMode {
4270 aead_algo: options.aead_algo,
4271 kdf_algo,
4272 });
4273 }
4274 }
4275 let fixed = CryptoHeaderFixed {
4276 length: length as u32,
4277 compression_algo: CompressionAlgo::ZstdFramed,
4278 aead_algo: options.aead_algo,
4279 fec_algo: FecAlgo::ReedSolomonGF16,
4280 kdf_algo,
4281 chunk_size: options.chunk_size,
4282 envelope_target_size: options.envelope_target_size,
4283 block_size: options.block_size,
4284 fec_data_shards: options.fec_data_shards,
4285 fec_parity_shards: options.fec_parity_shards,
4286 index_fec_data_shards: options.index_fec_data_shards,
4287 index_fec_parity_shards: options.index_fec_parity_shards,
4288 index_root_fec_data_shards: options.index_root_fec_data_shards,
4289 index_root_fec_parity_shards: options.index_root_fec_parity_shards,
4290 stripe_width: options.stripe_width,
4291 volume_loss_tolerance: options.volume_loss_tolerance,
4292 bit_rot_buffer_pct: options.bit_rot_buffer_pct,
4293 has_dictionary: if has_dictionary { 1 } else { 0 },
4294 max_path_length: options.max_path_length,
4295 expected_volume_size: options.target_volume_size.unwrap_or(0),
4296 };
4297
4298 let mut bytes = fixed.to_bytes().to_vec();
4299 bytes.extend_from_slice(&kdf_payload);
4300 bytes.extend_from_slice(&0u16.to_le_bytes());
4301 bytes.extend_from_slice(&0u32.to_le_bytes());
4302 let hmac = compute_integrity_tag(
4303 HmacDomain::CryptoHeader,
4304 options.aead_algo,
4305 volume_format_rev,
4306 Some(&subkeys.mac_key),
4307 archive_uuid,
4308 session_id,
4309 &bytes,
4310 )?;
4311 bytes.extend_from_slice(&hmac);
4312 Ok(bytes)
4313}
4314
4315fn serialize_kdf_params(params: &KdfParams) -> Result<Vec<u8>, FormatError> {
4316 let mut bytes = Vec::new();
4317 match params {
4318 KdfParams::None => {
4319 bytes.extend_from_slice(&(KdfAlgo::None as u16).to_le_bytes());
4320 }
4321 KdfParams::Raw => {
4322 bytes.extend_from_slice(&(KdfAlgo::Raw as u16).to_le_bytes());
4323 }
4324 KdfParams::Argon2id {
4325 t_cost,
4326 m_cost_kib,
4327 parallelism,
4328 salt,
4329 } => {
4330 if *t_cost == 0 {
4331 return Err(FormatError::InvalidKdfParams("t_cost must be non-zero"));
4332 }
4333 if *parallelism == 0 {
4334 return Err(FormatError::InvalidKdfParams(
4335 "parallelism must be non-zero",
4336 ));
4337 }
4338 let min_memory = parallelism
4339 .checked_mul(8)
4340 .ok_or(FormatError::InvalidKdfParams(
4341 "m_cost_kib requirement overflow",
4342 ))?;
4343 if *m_cost_kib < min_memory {
4344 return Err(FormatError::InvalidKdfParams(
4345 "m_cost_kib must be at least 8 * parallelism",
4346 ));
4347 }
4348 if !(8..=64).contains(&salt.len()) {
4349 return Err(FormatError::InvalidKdfParams(
4350 "argon2id salt length must be 8..64",
4351 ));
4352 }
4353 let salt_len = u16::try_from(salt.len())
4354 .map_err(|_| FormatError::InvalidKdfParams("argon2id salt too long"))?;
4355 bytes.extend_from_slice(&(KdfAlgo::Argon2id as u16).to_le_bytes());
4356 bytes.extend_from_slice(&t_cost.to_le_bytes());
4357 bytes.extend_from_slice(&m_cost_kib.to_le_bytes());
4358 bytes.extend_from_slice(¶llelism.to_le_bytes());
4359 bytes.extend_from_slice(&salt_len.to_le_bytes());
4360 bytes.extend_from_slice(salt);
4361 }
4362 KdfParams::RecipientWrap {
4363 key_wrap_table_length,
4364 key_wrap_table_record_count,
4365 key_wrap_table_version,
4366 key_wrap_table_digest,
4367 } => {
4368 bytes.extend_from_slice(&(KdfAlgo::RecipientWrap as u16).to_le_bytes());
4369 bytes.extend_from_slice(&key_wrap_table_length.to_le_bytes());
4370 bytes.extend_from_slice(&key_wrap_table_record_count.to_le_bytes());
4371 bytes.extend_from_slice(&key_wrap_table_version.to_le_bytes());
4372 bytes.extend_from_slice(&0u16.to_le_bytes());
4373 bytes.extend_from_slice(key_wrap_table_digest);
4374 }
4375 }
4376 Ok(bytes)
4377}
4378
4379#[cfg(test)]
4380fn build_tar_stream(
4381 files: &[RegularFile<'_>],
4382 max_path_length: u32,
4383) -> Result<(Vec<u8>, Vec<TarMember>), FormatError> {
4384 let mut stream = Vec::new();
4385 let mut members = Vec::with_capacity(files.len());
4386 for file in files {
4387 let path = normalize_lookup_file_path(file.path, max_path_length)?;
4388 let start = stream.len() as u64;
4389 let member_group =
4390 build_regular_file_member_group(&path, file.contents, file.mode, file.mtime)?;
4391 stream.extend_from_slice(&member_group);
4392 members.push(TarMember {
4393 path,
4394 tar_member_group_start: start,
4395 tar_member_group_size: member_group.len() as u64,
4396 file_data_size: file.contents.len() as u64,
4397 kind: TarEntryKind::Regular,
4398 mode: file.mode,
4399 mtime: file.mtime,
4400 });
4401 }
4402 Ok((stream, members))
4403}
4404
4405#[cfg(test)]
4406fn build_payload_envelopes(
4407 tar_stream: &[u8],
4408 members: &[TarMember],
4409 options: WriterOptions,
4410 dictionary: Option<&[u8]>,
4411) -> Result<(Vec<PayloadEnvelope>, Vec<PayloadFrame>), FormatError> {
4412 let chunk_size = options.chunk_size as usize;
4413 if chunk_size == 0 {
4414 return Err(FormatError::WriterUnsupported(
4415 "chunk_size must be non-zero and no larger than envelope_target_size",
4416 ));
4417 }
4418 let envelope_target_size = options.envelope_target_size as usize;
4419 let mut envelopes = Vec::new();
4420 let mut current = PayloadEnvelope {
4421 envelope_index: 0,
4422 plaintext: Vec::new(),
4423 };
4424 let mut frames = Vec::new();
4425 let mut next_frame_index = 0u64;
4426
4427 for (member_index, member) in members.iter().enumerate() {
4428 let start = member.tar_member_group_start as usize;
4429 let end = checked_usize_add(start, member.tar_member_group_size as usize, "tar member")?;
4430 let member_bytes = tar_stream
4431 .get(start..end)
4432 .ok_or(FormatError::WriterInvariant(
4433 "tar member range is out of bounds",
4434 ))?;
4435 let mut member_offset = 0usize;
4436 while member_offset < member_bytes.len() {
4437 let mut chunk_len = (member_bytes.len() - member_offset).min(chunk_size);
4438 let frame = loop {
4439 let end = checked_usize_add(member_offset, chunk_len, "payload chunk")?;
4440 let chunk = &member_bytes[member_offset..end];
4441 let frame = if let Some(dictionary) = dictionary {
4442 compress_zstd_frame_with_dictionary_and_jobs(
4443 chunk,
4444 options.zstd_level,
4445 dictionary,
4446 options.jobs,
4447 )?
4448 } else {
4449 compress_zstd_frame_with_jobs(chunk, options.zstd_level, options.jobs)?
4450 };
4451 if payload_object_can_fit(frame.len(), options)? {
4452 break frame;
4453 }
4454 if chunk_len == 1 {
4455 return Err(FormatError::WriterUnsupported(
4456 "single-byte payload frame exceeds envelope object limits",
4457 ));
4458 }
4459 chunk_len = (chunk_len / 2).max(1);
4460 };
4461 let next_len = checked_usize_add(current.plaintext.len(), frame.len(), "payload")?;
4462 if !current.plaintext.is_empty()
4463 && (next_len > envelope_target_size || !payload_object_can_fit(next_len, options)?)
4464 {
4465 envelopes.push(current);
4466 current = PayloadEnvelope {
4467 envelope_index: envelopes.len() as u64,
4468 plaintext: Vec::new(),
4469 };
4470 }
4471
4472 if current.plaintext.is_empty() && !payload_object_can_fit(frame.len(), options)? {
4473 return Err(FormatError::WriterUnsupported(
4474 "payload frame exceeds envelope object limits",
4475 ));
4476 }
4477 let offset = u32_len(current.plaintext.len(), "FrameEntry.offset_in_envelope")?;
4478 current.plaintext.extend_from_slice(&frame);
4479 let is_first_member_frame = member_offset == 0;
4480 let is_last_member_frame =
4481 checked_usize_add(member_offset, chunk_len, "payload chunk")? == member_bytes.len();
4482 let mut flags = 0u32;
4483 if is_first_member_frame {
4484 flags |= 0x0000_0001;
4485 }
4486 if is_last_member_frame {
4487 flags |= 0x0000_0002;
4488 }
4489 frames.push(PayloadFrame {
4490 frame_index: next_frame_index,
4491 envelope_index: current.envelope_index,
4492 member_index,
4493 offset_in_envelope: offset,
4494 compressed_size: u32_len(frame.len(), "FrameEntry.compressed_size")?,
4495 decompressed_size: u32_len(chunk_len, "FrameEntry.decompressed_size")?,
4496 flags,
4497 tar_stream_offset: checked_u64_add(
4498 member.tar_member_group_start,
4499 u64::try_from(member_offset)
4500 .map_err(|_| FormatError::WriterUnsupported("chunk offset"))?,
4501 "PayloadFrame.tar_stream_offset",
4502 )?,
4503 });
4504 next_frame_index = checked_u64_add(next_frame_index, 1, "PayloadFrame.frame_index")?;
4505 member_offset = checked_usize_add(member_offset, chunk_len, "payload chunk")?;
4506 }
4507 }
4508 if !current.plaintext.is_empty() {
4509 envelopes.push(current);
4510 }
4511 Ok((envelopes, frames))
4512}
4513
4514fn sorted_file_rows(members: &[TarMember]) -> Vec<FileRow> {
4515 let mut rows = members
4516 .iter()
4517 .enumerate()
4518 .map(|(member_index, member)| FileRow {
4519 path_hash: hash_prefix(&member.path),
4520 path: member.path.clone(),
4521 member_index,
4522 member: member.clone(),
4523 })
4524 .collect::<Vec<_>>();
4525 rows.sort_by(|left, right| {
4526 (
4527 left.path_hash,
4528 left.path.as_slice(),
4529 left.member.tar_member_group_start,
4530 )
4531 .cmp(&(
4532 right.path_hash,
4533 right.path.as_slice(),
4534 right.member.tar_member_group_start,
4535 ))
4536 });
4537 rows
4538}
4539
4540fn partition_file_rows(rows: Vec<FileRow>) -> Result<Vec<Vec<FileRow>>, FormatError> {
4541 let mut shards = Vec::new();
4542 let mut start = 0usize;
4543 while start < rows.len() {
4544 let mut end = (start + DEFAULT_FILES_PER_INDEX_SHARD).min(rows.len());
4545 if end < rows.len() && rows[end - 1].path_hash == rows[end].path_hash {
4546 let boundary_hash = rows[end].path_hash;
4547 let mut run_start_in_shard = end - 1;
4548 while run_start_in_shard > start
4549 && rows[run_start_in_shard - 1].path_hash == boundary_hash
4550 {
4551 run_start_in_shard -= 1;
4552 }
4553 let mut full_run_start = run_start_in_shard;
4554 while full_run_start > 0 && rows[full_run_start - 1].path_hash == boundary_hash {
4555 full_run_start -= 1;
4556 }
4557 let mut full_run_end = end + 1;
4558 while full_run_end < rows.len() && rows[full_run_end].path_hash == boundary_hash {
4559 full_run_end += 1;
4560 }
4561 let full_run_len = full_run_end - full_run_start;
4562 end = if full_run_len <= MAX_HASH_PREFIX_RUN_FILES {
4563 full_run_end
4564 } else {
4565 (run_start_in_shard + MAX_HASH_PREFIX_RUN_FILES).min(full_run_end)
4566 };
4567 }
4568 if end - start > MAX_FILES_PER_INDEX_SHARD {
4569 return Err(FormatError::WriterUnsupported(
4570 "hash-prefix collision run exceeds max_files_per_index_shard",
4571 ));
4572 }
4573 shards.push(rows[start..end].to_vec());
4574 start = end;
4575 }
4576 Ok(shards)
4577}
4578
4579fn build_index_shard_plaintexts(
4580 shard_rows: &[Vec<FileRow>],
4581 frames: &[PayloadFrame],
4582 payloads: &[PayloadObject],
4583 options: WriterOptions,
4584) -> Result<Vec<PlannedIndexShard>, FormatError> {
4585 let mut planned = Vec::new();
4586 for rows in shard_rows {
4587 append_index_shards_for_rows(&mut planned, rows, frames, payloads, options)?;
4588 }
4589 Ok(planned)
4590}
4591
4592fn append_index_shards_for_rows(
4593 planned: &mut Vec<PlannedIndexShard>,
4594 rows: &[FileRow],
4595 frames: &[PayloadFrame],
4596 payloads: &[PayloadObject],
4597 options: WriterOptions,
4598) -> Result<(), FormatError> {
4599 let shard_index =
4600 u64::try_from(planned.len()).map_err(|_| FormatError::WriterUnsupported("shard_index"))?;
4601 let candidate = build_index_shard_plaintext(shard_index, rows, frames, payloads, options)?;
4602 let compressed =
4603 compress_zstd_frame_with_jobs(&candidate.plaintext, options.zstd_level, options.jobs)?;
4604 if index_object_can_fit(compressed.len(), options)? {
4605 planned.push(candidate);
4606 return Ok(());
4607 }
4608 if rows.len() == 1 {
4609 return Err(FormatError::WriterUnsupported(
4610 "single-file IndexShard exceeds index object limits",
4611 ));
4612 }
4613 let split_at = split_sorted_file_rows_for_object_limit(rows);
4614 append_index_shards_for_rows(planned, &rows[..split_at], frames, payloads, options)?;
4615 append_index_shards_for_rows(planned, &rows[split_at..], frames, payloads, options)
4616}
4617
4618fn split_sorted_file_rows_for_object_limit(rows: &[FileRow]) -> usize {
4619 let midpoint = rows.len() / 2;
4620 if rows[midpoint - 1].path_hash != rows[midpoint].path_hash {
4621 return midpoint;
4622 }
4623
4624 let boundary_hash = rows[midpoint].path_hash;
4625 let mut left = midpoint;
4626 while left > 0 && rows[left - 1].path_hash == boundary_hash {
4627 left -= 1;
4628 }
4629 let mut right = midpoint;
4630 while right < rows.len() && rows[right].path_hash == boundary_hash {
4631 right += 1;
4632 }
4633
4634 match (left > 0, right < rows.len()) {
4635 (true, true) if midpoint - left <= right - midpoint => left,
4636 (true, true) => right,
4637 (true, false) => left,
4638 (false, true) => right,
4639 (false, false) => midpoint,
4640 }
4641}
4642
4643fn build_index_shard_plaintext(
4644 shard_index: u64,
4645 file_rows: &[FileRow],
4646 frames: &[PayloadFrame],
4647 payloads: &[PayloadObject],
4648 options: WriterOptions,
4649) -> Result<PlannedIndexShard, FormatError> {
4650 let mut string_pool = Vec::new();
4651 let mut file_entries = Vec::with_capacity(file_rows.len());
4652 let mut required_frame_indexes = BTreeSet::new();
4653 for row in file_rows {
4654 let path_offset = u32_len(string_pool.len(), "FileEntry.path_offset")?;
4655 string_pool.extend_from_slice(&row.path);
4656 let (first_frame_index, frame_count) = member_frame_range(row.member_index, frames)?;
4657 for offset in 0..frame_count as u64 {
4658 required_frame_indexes.insert(checked_u64_add(
4659 first_frame_index,
4660 offset,
4661 "FileEntry.frame_count",
4662 )?);
4663 }
4664 file_entries.push(FileEntry {
4665 path_hash: row.path_hash,
4666 path_offset,
4667 path_length: u32_len(row.path.len(), "FileEntry.path_length")?,
4668 first_frame_index,
4669 frame_count,
4670 offset_in_first_frame_plaintext: 0,
4671 tar_member_group_size: row.member.tar_member_group_size,
4672 file_data_size: row.member.file_data_size,
4673 kind: row.member.kind,
4674 mode: row.member.mode,
4675 mtime: row.member.mtime,
4676 flags: 0,
4677 });
4678 }
4679
4680 let frame_entries = frames
4681 .iter()
4682 .filter(|frame| required_frame_indexes.contains(&frame.frame_index))
4683 .map(|frame| FrameEntry {
4684 frame_index: frame.frame_index,
4685 envelope_index: frame.envelope_index,
4686 offset_in_envelope: frame.offset_in_envelope,
4687 compressed_size: frame.compressed_size,
4688 decompressed_size: frame.decompressed_size,
4689 flags: frame.flags,
4690 tar_stream_offset: frame.tar_stream_offset,
4691 })
4692 .collect::<Vec<_>>();
4693 let required_envelope_indexes = frame_entries
4694 .iter()
4695 .map(|frame| frame.envelope_index)
4696 .collect::<BTreeSet<_>>();
4697 let envelope_entries = payloads
4698 .iter()
4699 .filter(|payload| required_envelope_indexes.contains(&payload.envelope_index))
4700 .map(|payload| {
4701 let (first_frame_index, frame_count) =
4702 envelope_frame_range(payload.envelope_index, frames)?;
4703 Ok(EnvelopeEntry {
4704 envelope_index: payload.envelope_index,
4705 first_block_index: payload.object.first_block_index,
4706 data_block_count: payload.object.data_block_count,
4707 parity_block_count: payload.object.parity_block_count,
4708 encrypted_size: payload.object.encrypted_size,
4709 plaintext_size: payload.plaintext_size,
4710 first_frame_index,
4711 frame_count,
4712 })
4713 })
4714 .collect::<Result<Vec<_>, FormatError>>()?;
4715
4716 let plaintext = serialize_index_shard(
4717 shard_index,
4718 &file_entries,
4719 &frame_entries,
4720 &envelope_entries,
4721 &string_pool,
4722 options,
4723 )?;
4724 let first_path_hash = file_rows
4725 .first()
4726 .ok_or(FormatError::WriterInvariant("empty planned IndexShard"))?
4727 .path_hash;
4728 let last_path_hash = file_rows
4729 .last()
4730 .ok_or(FormatError::WriterInvariant("empty planned IndexShard"))?
4731 .path_hash;
4732 Ok(PlannedIndexShard {
4733 shard_index,
4734 plaintext,
4735 file_count: u32_len(file_rows.len(), "IndexShard.file_count")?,
4736 first_path_hash,
4737 last_path_hash,
4738 })
4739}
4740
4741fn serialize_index_shard(
4742 shard_index: u64,
4743 files: &[FileEntry],
4744 frames: &[FrameEntry],
4745 envelopes: &[EnvelopeEntry],
4746 string_pool: &[u8],
4747 _options: WriterOptions,
4748) -> Result<Vec<u8>, FormatError> {
4749 let mut cursor = INDEX_SHARD_HEADER_LEN;
4750 let file_table_offset = table_offset(files.len(), cursor)?;
4751 cursor = checked_usize_add(cursor, files.len() * FILE_ENTRY_LEN, "IndexShard")?;
4752 let frame_table_offset = table_offset(frames.len(), cursor)?;
4753 cursor = checked_usize_add(cursor, frames.len() * FRAME_ENTRY_LEN, "IndexShard")?;
4754 let envelope_table_offset = table_offset(envelopes.len(), cursor)?;
4755 cursor = checked_usize_add(cursor, envelopes.len() * ENVELOPE_ENTRY_LEN, "IndexShard")?;
4756 let string_pool_offset = table_offset(string_pool.len(), cursor)?;
4757
4758 let header = IndexShardHeader {
4759 version: 1,
4760 shard_index,
4761 file_count: u32_len(files.len(), "IndexShard.file_count")?,
4762 frame_count: u32_len(frames.len(), "IndexShard.frame_count")?,
4763 envelope_count: u32_len(envelopes.len(), "IndexShard.envelope_count")?,
4764 file_table_offset,
4765 frame_table_offset,
4766 envelope_table_offset,
4767 string_pool_offset,
4768 string_pool_size: u32_len(string_pool.len(), "IndexShard.string_pool_size")?,
4769 };
4770
4771 let mut bytes = Vec::with_capacity(cursor + string_pool.len());
4772 bytes.extend_from_slice(&header.to_bytes());
4773 for entry in files {
4774 bytes.extend_from_slice(&entry.to_bytes());
4775 }
4776 for entry in frames {
4777 bytes.extend_from_slice(&entry.to_bytes());
4778 }
4779 for entry in envelopes {
4780 bytes.extend_from_slice(&entry.to_bytes());
4781 }
4782 bytes.extend_from_slice(string_pool);
4783 Ok(bytes)
4784}
4785
4786fn build_directory_hint_plaintexts(
4787 shard_rows: &[Vec<FileRow>],
4788 options: WriterOptions,
4789) -> Result<Vec<PlannedDirectoryHintShard>, FormatError> {
4790 let mut map = BTreeMap::<Vec<u8>, BTreeSet<u32>>::new();
4791 for (shard_row_index, rows) in shard_rows.iter().enumerate() {
4792 let shard_row_index = u32::try_from(shard_row_index)
4793 .map_err(|_| FormatError::WriterUnsupported("directory hint shard row index"))?;
4794 for row in rows {
4795 add_directory_hint_rows(&mut map, shard_row_index, &row.path);
4796 }
4797 }
4798
4799 let rows = map
4800 .into_iter()
4801 .map(|(path, shard_rows)| (hash_prefix(&path), path, shard_rows))
4802 .collect::<Vec<_>>();
4803 let mut rows = rows;
4804 rows.sort_by(|left, right| (left.0, left.1.as_slice()).cmp(&(right.0, right.1.as_slice())));
4805
4806 let mut planned = Vec::new();
4807 for chunk in rows.chunks(DEFAULT_DIRECTORY_HINT_ENTRIES_PER_SHARD) {
4808 append_directory_hint_shards_for_rows(&mut planned, chunk, options)?;
4809 }
4810 Ok(planned)
4811}
4812
4813fn append_directory_hint_shards_for_rows(
4814 planned: &mut Vec<PlannedDirectoryHintShard>,
4815 rows: &[([u8; 8], Vec<u8>, BTreeSet<u32>)],
4816 options: WriterOptions,
4817) -> Result<(), FormatError> {
4818 let hint_shard_index = u64::try_from(planned.len())
4819 .map_err(|_| FormatError::WriterUnsupported("hint_shard_index"))?;
4820 let candidate = build_directory_hint_plaintext(hint_shard_index, rows)?;
4821 let compressed =
4822 compress_zstd_frame_with_jobs(&candidate.plaintext, options.zstd_level, options.jobs)?;
4823 if index_object_can_fit(compressed.len(), options)? {
4824 planned.push(candidate);
4825 return Ok(());
4826 }
4827 if rows.len() == 1 {
4828 return Err(FormatError::WriterUnsupported(
4829 "single DirectoryHintEntry exceeds index object limits",
4830 ));
4831 }
4832 let split_at = rows.len() / 2;
4833 append_directory_hint_shards_for_rows(planned, &rows[..split_at], options)?;
4834 append_directory_hint_shards_for_rows(planned, &rows[split_at..], options)
4835}
4836
4837fn add_directory_hint_rows(
4838 map: &mut BTreeMap<Vec<u8>, BTreeSet<u32>>,
4839 shard_row_index: u32,
4840 path: &[u8],
4841) {
4842 map.entry(Vec::new()).or_default().insert(shard_row_index);
4843 let mut cursor = 0usize;
4844 while let Some(position) = path[cursor..].iter().position(|byte| *byte == b'/') {
4845 let slash = cursor + position;
4846 if slash > 0 {
4847 map.entry(path[..slash].to_vec())
4848 .or_default()
4849 .insert(shard_row_index);
4850 }
4851 cursor = slash + 1;
4852 }
4853}
4854
4855fn build_directory_hint_plaintext(
4856 hint_shard_index: u64,
4857 rows: &[([u8; 8], Vec<u8>, BTreeSet<u32>)],
4858) -> Result<PlannedDirectoryHintShard, FormatError> {
4859 let mut entries = Vec::with_capacity(rows.len());
4860 let mut shard_row_indexes = Vec::new();
4861 let mut string_pool = Vec::new();
4862
4863 for (dir_hash, path, shard_rows) in rows {
4864 let path_offset = if path.is_empty() {
4865 0
4866 } else {
4867 u64::try_from(string_pool.len())
4868 .map_err(|_| FormatError::WriterUnsupported("DirectoryHintEntry.path_offset"))?
4869 };
4870 if !path.is_empty() {
4871 string_pool.extend_from_slice(path);
4872 }
4873 let shard_list_start_index = u32_len(
4874 shard_row_indexes.len(),
4875 "DirectoryHintEntry.shard_list_start_index",
4876 )?;
4877 shard_row_indexes.extend(shard_rows.iter().copied());
4878 entries.push(DirectoryHintEntry {
4879 dir_hash: *dir_hash,
4880 path_offset,
4881 path_length: u32_len(path.len(), "DirectoryHintEntry.path_length")?,
4882 shard_list_start_index,
4883 shard_count: u32_len(shard_rows.len(), "DirectoryHintEntry.shard_count")?,
4884 });
4885 }
4886
4887 let plaintext = serialize_directory_hint_table(
4888 hint_shard_index,
4889 &entries,
4890 &shard_row_indexes,
4891 &string_pool,
4892 )?;
4893 let first_dir_hash = rows
4894 .first()
4895 .ok_or(FormatError::WriterInvariant("empty directory hint shard"))?
4896 .0;
4897 let last_dir_hash = rows
4898 .last()
4899 .ok_or(FormatError::WriterInvariant("empty directory hint shard"))?
4900 .0;
4901 Ok(PlannedDirectoryHintShard {
4902 hint_shard_index,
4903 plaintext,
4904 entry_count: rows.len() as u64,
4905 first_dir_hash,
4906 last_dir_hash,
4907 })
4908}
4909
4910fn serialize_directory_hint_table(
4911 hint_shard_index: u64,
4912 entries: &[DirectoryHintEntry],
4913 shard_row_indexes: &[u32],
4914 string_pool: &[u8],
4915) -> Result<Vec<u8>, FormatError> {
4916 let entry_table_offset = table_offset(entries.len(), DIRECTORY_HINT_TABLE_LEN)?;
4917 let shard_list_cursor = checked_usize_add(
4918 DIRECTORY_HINT_TABLE_LEN,
4919 entries.len() * DIRECTORY_HINT_ENTRY_LEN,
4920 "DirectoryHintTable",
4921 )?;
4922 let shard_list_offset = table_offset(shard_row_indexes.len(), shard_list_cursor)?;
4923 let string_pool_cursor = checked_usize_add(
4924 shard_list_cursor,
4925 shard_row_indexes.len() * 4,
4926 "DirectoryHintTable",
4927 )?;
4928 let string_pool_offset = if string_pool.is_empty() {
4929 0
4930 } else {
4931 u64::try_from(string_pool_cursor)
4932 .map_err(|_| FormatError::WriterUnsupported("DirectoryHintTable.string_pool_offset"))?
4933 };
4934 let header = DirectoryHintTableHeader {
4935 version: 1,
4936 hint_shard_index,
4937 entry_count: entries.len() as u64,
4938 entry_table_offset: entry_table_offset as u64,
4939 shard_list_offset: shard_list_offset as u64,
4940 string_pool_offset,
4941 string_pool_size: string_pool.len() as u64,
4942 };
4943
4944 let mut bytes = Vec::with_capacity(string_pool_cursor + string_pool.len());
4945 bytes.extend_from_slice(&header.to_bytes());
4946 for entry in entries {
4947 bytes.extend_from_slice(&entry.to_bytes());
4948 }
4949 for row in shard_row_indexes {
4950 bytes.extend_from_slice(&row.to_le_bytes());
4951 }
4952 bytes.extend_from_slice(string_pool);
4953 Ok(bytes)
4954}
4955
4956#[derive(Debug, Clone, Copy)]
4957struct IndexRootPlaintextInput<'a> {
4958 shard_entries: &'a [ShardEntry],
4959 frame_count: u64,
4960 envelope_count: u64,
4961 file_count: u64,
4962 payload_block_count: u64,
4963 tar_total_size: u64,
4964 content_sha256: [u8; 32],
4965 directory_hint_entries: &'a [DirectoryHintShardEntry],
4966 dictionary_extent: Option<(ObjectExtent, u32)>,
4967}
4968
4969fn build_index_root_plaintext(input: IndexRootPlaintextInput<'_>) -> Vec<u8> {
4970 let mut header = IndexRootHeader::empty();
4971 header.frame_count = input.frame_count;
4972 header.envelope_count = input.envelope_count;
4973 header.file_count = input.file_count;
4974 header.payload_block_count = input.payload_block_count;
4975 header.tar_total_size = input.tar_total_size;
4976 header.content_sha256 = input.content_sha256;
4977 if let Some((dictionary, decompressed_size)) = input.dictionary_extent {
4978 header.dictionary_first_block = dictionary.first_block_index;
4979 header.dictionary_data_block_count = dictionary.data_block_count;
4980 header.dictionary_parity_block_count = dictionary.parity_block_count;
4981 header.dictionary_encrypted_size = dictionary.encrypted_size;
4982 header.dictionary_decompressed_size = decompressed_size;
4983 }
4984 let root = IndexRoot {
4985 header,
4986 shards: input.shard_entries.to_vec(),
4987 directory_hint_shards: input.directory_hint_entries.to_vec(),
4988 };
4989 root.to_bytes()
4990}
4991
4992#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4993struct MetadataClassPlan {
4994 options: WriterOptions,
4995 index_root: PlannedEncryptedObject,
4996 dictionary: Option<PlannedEncryptedObject>,
4997}
4998
4999fn plan_index_root_metadata_class(
5000 mut options: WriterOptions,
5001 compressed_index_root_len: usize,
5002 compressed_dictionary_len: Option<usize>,
5003) -> Result<MetadataClassPlan, FormatError> {
5004 let index_root = plan_metadata_object_without_class(
5005 compressed_index_root_len,
5006 options,
5007 MetadataObjectKind::IndexRoot,
5008 )?;
5009 let dictionary = compressed_dictionary_len
5010 .map(|len| plan_metadata_object_without_class(len, options, MetadataObjectKind::Dictionary))
5011 .transpose()?;
5012 let required_data_shards = u32::from(options.index_root_fec_data_shards)
5013 .max(MIN_INDEX_ROOT_FEC_DATA_SHARDS as u32)
5014 .max(index_root.data_block_count)
5015 .max(dictionary.map(|plan| plan.data_block_count).unwrap_or(0));
5016 let required_data_shards = u16::try_from(required_data_shards)
5017 .map_err(|_| MetadataObjectKind::IndexRoot.too_large_error())?;
5018 options.index_root_fec_data_shards = required_data_shards;
5019 let required_parity_shards = compute_parity_u16(
5020 options.index_root_fec_data_shards as u64,
5021 options,
5022 "index_root_fec_parity_shards",
5023 )?;
5024 options.index_root_fec_parity_shards = options
5025 .index_root_fec_parity_shards
5026 .max(required_parity_shards);
5027 ensure_metadata_object_fits_class(index_root, options, MetadataObjectKind::IndexRoot)?;
5028 if let Some(dictionary) = dictionary {
5029 ensure_metadata_object_fits_class(dictionary, options, MetadataObjectKind::Dictionary)?;
5030 }
5031 Ok(MetadataClassPlan {
5032 options,
5033 index_root,
5034 dictionary,
5035 })
5036}
5037
5038fn plan_metadata_object_without_class(
5039 payload_len: usize,
5040 options: WriterOptions,
5041 kind: MetadataObjectKind,
5042) -> Result<PlannedEncryptedObject, FormatError> {
5043 let plan = plan_encrypted_object_without_class(payload_len, options)
5044 .map_err(|_| kind.too_large_error())?;
5045 if plan.data_block_count > u16::MAX as u32 || plan.parity_block_count > u16::MAX as u32 {
5046 return Err(kind.too_large_error());
5047 }
5048 validate_object_shard_total(plan.data_block_count, plan.parity_block_count)
5049 .map_err(|_| kind.too_large_error())?;
5050 Ok(plan)
5051}
5052
5053fn ensure_metadata_object_fits_class(
5054 plan: PlannedEncryptedObject,
5055 options: WriterOptions,
5056 kind: MetadataObjectKind,
5057) -> Result<(), FormatError> {
5058 if plan.data_block_count > options.index_root_fec_data_shards as u32 {
5059 return Err(kind.too_large_error());
5060 }
5061 if plan.parity_block_count > options.index_root_fec_parity_shards as u32 {
5062 return Err(kind.too_large_error());
5063 }
5064 validate_object_shard_total(plan.data_block_count, plan.parity_block_count)
5065 .map_err(|_| kind.too_large_error())
5066}
5067
5068fn payload_object_can_fit(payload_len: usize, options: WriterOptions) -> Result<bool, FormatError> {
5069 encrypted_object_can_fit(
5070 payload_len,
5071 options.fec_data_shards,
5072 options.fec_parity_shards,
5073 options,
5074 )
5075}
5076
5077fn index_object_can_fit(payload_len: usize, options: WriterOptions) -> Result<bool, FormatError> {
5078 encrypted_object_can_fit(
5079 payload_len,
5080 options.index_fec_data_shards,
5081 options.index_fec_parity_shards,
5082 options,
5083 )
5084}
5085
5086fn encrypted_object_can_fit(
5087 payload_len: usize,
5088 data_shard_max: u16,
5089 parity_shard_max: u16,
5090 options: WriterOptions,
5091) -> Result<bool, FormatError> {
5092 match plan_encrypted_object(payload_len, data_shard_max, parity_shard_max, options) {
5093 Ok(_) => Ok(true),
5094 Err(FormatError::WriterUnsupported("encrypted object exceeds u32 size limit"))
5095 | Err(FormatError::WriterUnsupported(
5096 "encrypted object exceeds its data shard class maximum",
5097 ))
5098 | Err(FormatError::WriterUnsupported(
5099 "encrypted object exceeds its parity shard class maximum",
5100 ))
5101 | Err(FormatError::WriterUnsupported(
5102 "encrypted object exceeds ReedSolomonGF16 shard limit",
5103 )) => Ok(false),
5104 Err(err) => Err(err),
5105 }
5106}
5107
5108fn plan_encrypted_object(
5109 payload_len: usize,
5110 data_shard_max: u16,
5111 parity_shard_max: u16,
5112 options: WriterOptions,
5113) -> Result<PlannedEncryptedObject, FormatError> {
5114 let plan = plan_encrypted_object_without_class(payload_len, options)?;
5115 if plan.data_block_count > data_shard_max as u32 {
5116 return Err(FormatError::WriterUnsupported(
5117 "encrypted object exceeds its data shard class maximum",
5118 ));
5119 }
5120 if plan.parity_block_count > parity_shard_max as u32 {
5121 return Err(FormatError::WriterUnsupported(
5122 "encrypted object exceeds its parity shard class maximum",
5123 ));
5124 }
5125 validate_object_shard_total(plan.data_block_count, plan.parity_block_count)?;
5126 Ok(plan)
5127}
5128
5129fn plan_encrypted_object_without_class(
5130 payload_len: usize,
5131 options: WriterOptions,
5132) -> Result<PlannedEncryptedObject, FormatError> {
5133 let (data_block_count, encrypted_size) = encrypted_object_data_extent(payload_len, options)?;
5134 let parity_block_count = compute_parity(data_block_count as u64, options)?;
5135 Ok(PlannedEncryptedObject {
5136 data_block_count,
5137 parity_block_count,
5138 encrypted_size,
5139 })
5140}
5141
5142fn encrypted_object_data_extent(
5143 payload_len: usize,
5144 options: WriterOptions,
5145) -> Result<(u32, u32), FormatError> {
5146 let block_size = options.block_size as usize;
5147 let tag_len = options.aead_algo.tag_len();
5148 let total_before_padding =
5149 payload_len
5150 .checked_add(tag_len)
5151 .ok_or(FormatError::WriterUnsupported(
5152 "encrypted object size overflow",
5153 ))?;
5154 let remainder = total_before_padding % block_size;
5155 let encrypted_size = if remainder == 0 {
5156 total_before_padding
5157 .checked_add(block_size)
5158 .ok_or(FormatError::WriterUnsupported(
5159 "encrypted object size overflow",
5160 ))?
5161 } else {
5162 total_before_padding
5163 .checked_add(block_size - remainder)
5164 .ok_or(FormatError::WriterUnsupported(
5165 "encrypted object size overflow",
5166 ))?
5167 };
5168 let encrypted_size = u32_len(encrypted_size, "encrypted_size")
5169 .map_err(|_| FormatError::WriterUnsupported("encrypted object exceeds u32 size limit"))?;
5170 Ok((encrypted_size / options.block_size, encrypted_size))
5171}
5172
5173#[derive(Debug, Clone, Copy)]
5174struct ObjectEncryptionContext<'a> {
5175 key: &'a [u8; 32],
5176 nonce_seed: &'a [u8; 32],
5177 domain: &'a [u8],
5178 counter: u64,
5179 data_kind: BlockKind,
5180 parity_kind: BlockKind,
5181 data_shard_max: u16,
5182 class_parity_shard_max: u16,
5183 archive_uuid: &'a [u8; 16],
5184 session_id: &'a [u8; 16],
5185}
5186
5187fn encrypt_object(
5188 payload: &[u8],
5189 context: ObjectEncryptionContext<'_>,
5190 next_block_index: &mut u64,
5191 options: WriterOptions,
5192) -> Result<EncryptedObject, FormatError> {
5193 let block_size = options.block_size as usize;
5194 let encrypted = encrypt_object_payload(payload, context, options)?;
5195 if encrypted.len() % block_size != 0 {
5196 return Err(FormatError::WriterInvariant(
5197 "encrypted object is not block aligned",
5198 ));
5199 }
5200 let encrypted_size = u32_len(encrypted.len(), "encrypted_size")?;
5201 let data_shards = encrypted
5202 .chunks(block_size)
5203 .map(|chunk| chunk.to_vec())
5204 .collect::<Vec<_>>();
5205 let data_block_count = u32_len(data_shards.len(), "data_block_count")?;
5206 if data_block_count == 0 {
5207 return Err(FormatError::WriterInvariant(
5208 "encrypted object has no data blocks",
5209 ));
5210 }
5211 if data_block_count > context.data_shard_max as u32 {
5212 return Err(FormatError::WriterUnsupported(
5213 "encrypted object exceeds its data shard class maximum",
5214 ));
5215 }
5216 let required_parity = compute_object_parity(
5217 data_block_count as u64,
5218 options,
5219 context.class_parity_shard_max as u32,
5220 )?;
5221 if required_parity > context.class_parity_shard_max as u32 {
5222 return Err(FormatError::WriterUnsupported(
5223 "encrypted object exceeds its parity shard class maximum",
5224 ));
5225 }
5226 validate_object_shard_total(data_block_count, required_parity)?;
5227 let parity_count = required_parity as u16;
5228 let parity_shards = if parity_count == 0 {
5229 Vec::new()
5230 } else {
5231 encode_parity_gf16(&data_shards, parity_count as usize)?
5232 };
5233
5234 let first_block_index = *next_block_index;
5235 let mut records = Vec::with_capacity(data_shards.len() + parity_shards.len());
5236 for (index, payload) in data_shards.into_iter().enumerate() {
5237 records.push(BlockRecord {
5238 block_index: checked_u64_add(first_block_index, index as u64, "BlockRecord")?,
5239 kind: context.data_kind,
5240 flags: if index + 1 == data_block_count as usize {
5241 0x01
5242 } else {
5243 0
5244 },
5245 payload,
5246 record_crc32c: 0,
5247 });
5248 }
5249 let parity_first_block = checked_u64_add(first_block_index, data_block_count as u64, "FEC")?;
5250 for (index, payload) in parity_shards.into_iter().enumerate() {
5251 records.push(BlockRecord {
5252 block_index: checked_u64_add(parity_first_block, index as u64, "BlockRecord")?,
5253 kind: context.parity_kind,
5254 flags: 0,
5255 payload,
5256 record_crc32c: 0,
5257 });
5258 }
5259
5260 *next_block_index = checked_u64_add(
5261 first_block_index,
5262 data_block_count as u64 + parity_count as u64,
5263 "next_block_index",
5264 )?;
5265
5266 Ok(EncryptedObject {
5267 first_block_index,
5268 data_block_count,
5269 parity_block_count: parity_count as u32,
5270 encrypted_size,
5271 records,
5272 })
5273}
5274
5275fn serialize_zero_parity_encrypted_object(
5276 payload: &[u8],
5277 context: ObjectEncryptionContext<'_>,
5278 expected_extent: ObjectExtent,
5279 options: WriterOptions,
5280) -> Result<Vec<SerializedBlockRecord>, FormatError> {
5281 let planned = plan_encrypted_object(
5282 payload.len(),
5283 context.data_shard_max,
5284 context.class_parity_shard_max,
5285 options,
5286 )?;
5287 if planned.parity_block_count != 0 || expected_extent.parity_block_count != 0 {
5288 return Err(FormatError::WriterInvariant(
5289 "zero-parity serialization received a parity object",
5290 ));
5291 }
5292 if planned.data_block_count != expected_extent.data_block_count
5293 || planned.encrypted_size != expected_extent.encrypted_size
5294 {
5295 return Err(FormatError::WriterInvariant(
5296 "encrypted object did not match planned sizing",
5297 ));
5298 }
5299
5300 let block_size = options.block_size as usize;
5301 let encrypted = encrypt_object_payload(payload, context, options)?;
5302 if encrypted.len() != expected_extent.encrypted_size as usize
5303 || encrypted.len() % block_size != 0
5304 {
5305 return Err(FormatError::WriterInvariant(
5306 "encrypted object did not match planned sizing",
5307 ));
5308 }
5309 let data_block_count = encrypted.len() / block_size;
5310 if data_block_count == 0 || data_block_count != expected_extent.data_block_count as usize {
5311 return Err(FormatError::WriterInvariant(
5312 "encrypted object did not match planned sizing",
5313 ));
5314 }
5315
5316 let mut records = Vec::with_capacity(data_block_count);
5317 for (index, chunk) in encrypted.chunks(block_size).enumerate() {
5318 let block_index = checked_u64_add(
5319 expected_extent.first_block_index,
5320 index as u64,
5321 "BlockRecord",
5322 )?;
5323 let flags = if index + 1 == data_block_count {
5324 0x01
5325 } else {
5326 0
5327 };
5328 records.push(SerializedBlockRecord {
5329 block_index,
5330 bytes: BlockRecord::to_bytes_from_parts(block_index, context.data_kind, flags, chunk),
5331 });
5332 }
5333 Ok(records)
5334}
5335
5336fn encrypt_object_payload(
5337 payload: &[u8],
5338 context: ObjectEncryptionContext<'_>,
5339 options: WriterOptions,
5340) -> Result<Vec<u8>, FormatError> {
5341 let block_size = options.block_size as usize;
5342 let padded = suffix_pad_for_aead(payload, options.aead_algo.tag_len(), block_size)?;
5343 if matches!(options.aead_algo, AeadAlgo::None) {
5344 return Ok(padded);
5345 }
5346 let nonce = derive_nonce(
5347 context.nonce_seed,
5348 context.domain,
5349 context.archive_uuid,
5350 context.session_id,
5351 context.counter,
5352 options.aead_algo.nonce_len(),
5353 )?;
5354 let aad = build_aad(
5355 context.domain,
5356 context.archive_uuid,
5357 context.session_id,
5358 context.counter,
5359 )?;
5360 aead_encrypt(options.aead_algo, context.key, &nonce, &aad, &padded)
5361}
5362
5363fn validate_planned_object(
5364 object: &EncryptedObject,
5365 expected: PlannedEncryptedObject,
5366) -> Result<(), FormatError> {
5367 if object.data_block_count != expected.data_block_count
5368 || object.parity_block_count != expected.parity_block_count
5369 || object.encrypted_size != expected.encrypted_size
5370 {
5371 return Err(FormatError::WriterInvariant(
5372 "encrypted object did not match planned sizing",
5373 ));
5374 }
5375 Ok(())
5376}
5377
5378fn validate_planned_extent(
5379 object: &EncryptedObject,
5380 expected: ObjectExtent,
5381) -> Result<(), FormatError> {
5382 validate_planned_object(
5383 object,
5384 PlannedEncryptedObject {
5385 data_block_count: expected.data_block_count,
5386 parity_block_count: expected.parity_block_count,
5387 encrypted_size: expected.encrypted_size,
5388 },
5389 )?;
5390 if object.first_block_index != expected.first_block_index {
5391 return Err(FormatError::WriterInvariant(
5392 "encrypted object did not match planned extent",
5393 ));
5394 }
5395 Ok(())
5396}
5397
5398fn map_metadata_encrypt_error(error: FormatError, kind: MetadataObjectKind) -> FormatError {
5399 match error {
5400 FormatError::WriterUnsupported("encrypted object exceeds u32 size limit")
5401 | FormatError::WriterUnsupported("encrypted object exceeds its data shard class maximum")
5402 | FormatError::WriterUnsupported(
5403 "encrypted object exceeds its parity shard class maximum",
5404 )
5405 | FormatError::WriterUnsupported("encrypted object exceeds ReedSolomonGF16 shard limit") => {
5406 kind.too_large_error()
5407 }
5408 other => other,
5409 }
5410}
5411
5412#[derive(Debug, Clone, Copy)]
5413struct RootAuthFooterBuildInput<'a> {
5414 archive_uuid: [u8; 16],
5415 session_id: [u8; 16],
5416 volume_format_rev: u16,
5417 options: WriterOptions,
5418 crypto_header: &'a [u8],
5419 volume_zero_manifest: &'a [u8; MANIFEST_FOOTER_LEN],
5420 index_root_plaintext: &'a [u8],
5421 index_root_extent: ObjectExtent,
5422 dictionary_extent: Option<(ObjectExtent, u32)>,
5423 shard_entries: &'a [ShardEntry],
5424 payload_objects: &'a [PayloadObject],
5425 directory_hint_entries: &'a [DirectoryHintShardEntry],
5426 data_leaf_hashes: &'a [(u64, [u8; 32])],
5427}
5428
5429fn build_root_auth_footer_from_leaf_hashes(
5430 config: RootAuthWriterConfig<'_>,
5431 authenticator: &mut RootAuthAuthenticator<'_>,
5432 input: RootAuthFooterBuildInput<'_>,
5433) -> Result<Vec<u8>, FormatError> {
5434 let mut sorted_leaf_hashes = input.data_leaf_hashes.to_vec();
5435 sorted_leaf_hashes.sort_by_key(|(block_index, _)| *block_index);
5436 let leaf_hashes = sorted_leaf_hashes
5437 .iter()
5438 .map(|(_, leaf_hash)| *leaf_hash)
5439 .collect::<Vec<_>>();
5440 let total_data_block_count = u64::try_from(leaf_hashes.len())
5441 .map_err(|_| FormatError::WriterUnsupported("root-auth data block count"))?;
5442 let data_block_merkle_root = data_block_merkle_root_from_leaf_hashes_for_revision(
5443 FORMAT_VERSION,
5444 input.volume_format_rev,
5445 &leaf_hashes,
5446 )?;
5447
5448 let parsed_crypto = CryptoHeader::parse(
5449 input.crypto_header,
5450 u32_len(input.crypto_header.len(), "CryptoHeader")?,
5451 )?;
5452 let footer_length = root_auth_footer_wire_length(
5453 config.signer_identity.len(),
5454 config.authenticator_value_length as usize,
5455 )?;
5456 let root_auth_descriptor_digest = root_auth_descriptor_digest_for_revision(
5457 FORMAT_VERSION,
5458 input.volume_format_rev,
5459 config.authenticator_id,
5460 config.signer_identity_type,
5461 config.signer_identity,
5462 config.authenticator_value_length,
5463 footer_length,
5464 )?;
5465 let signer_identity_digest =
5466 signer_identity_digest(config.signer_identity_type, config.signer_identity)?;
5467 let manifest_pre_hmac = manifest_footer_global_pre_hmac_bytes(input.volume_zero_manifest);
5468 let critical_metadata_digest = critical_metadata_digest(CriticalMetadataDigestInputs {
5469 archive_uuid: input.archive_uuid,
5470 session_id: input.session_id,
5471 format_version: FORMAT_VERSION,
5472 volume_format_rev: input.volume_format_rev,
5473 stripe_width: input.options.stripe_width,
5474 total_volumes: input.options.stripe_width,
5475 compression_algo: parsed_crypto.fixed.compression_algo,
5476 aead_algo: parsed_crypto.fixed.aead_algo,
5477 fec_algo: parsed_crypto.fixed.fec_algo,
5478 kdf_algo: parsed_crypto.fixed.kdf_algo,
5479 crypto_header_pre_hmac_bytes: parsed_crypto.hmac_covered_bytes,
5480 chunk_size: parsed_crypto.fixed.chunk_size,
5481 envelope_target_size: parsed_crypto.fixed.envelope_target_size,
5482 block_size: parsed_crypto.fixed.block_size,
5483 fec_data_shards: parsed_crypto.fixed.fec_data_shards,
5484 fec_parity_shards: parsed_crypto.fixed.fec_parity_shards,
5485 index_fec_data_shards: parsed_crypto.fixed.index_fec_data_shards,
5486 index_fec_parity_shards: parsed_crypto.fixed.index_fec_parity_shards,
5487 index_root_fec_data_shards: parsed_crypto.fixed.index_root_fec_data_shards,
5488 index_root_fec_parity_shards: parsed_crypto.fixed.index_root_fec_parity_shards,
5489 volume_loss_tolerance: parsed_crypto.fixed.volume_loss_tolerance,
5490 bit_rot_buffer_pct: parsed_crypto.fixed.bit_rot_buffer_pct,
5491 has_dictionary: parsed_crypto.fixed.has_dictionary,
5492 manifest_footer_global_pre_hmac_bytes: &manifest_pre_hmac,
5493 index_root_first_block: input.index_root_extent.first_block_index,
5494 index_root_data_block_count: input.index_root_extent.data_block_count,
5495 index_root_parity_block_count: input.index_root_extent.parity_block_count,
5496 index_root_encrypted_size: input.index_root_extent.encrypted_size,
5497 index_root_decompressed_size: u32_len(input.index_root_plaintext.len(), "IndexRoot")?,
5498 root_auth_descriptor_digest,
5499 })?;
5500 let index_digest = index_digest_for_revision(
5501 FORMAT_VERSION,
5502 input.volume_format_rev,
5503 input.index_root_plaintext,
5504 )?;
5505 let fec_layout_rows = writer_fec_layout_rows_from_extents(
5506 input.index_root_extent,
5507 u32_len(input.index_root_plaintext.len(), "IndexRoot")?,
5508 input.dictionary_extent,
5509 input.shard_entries,
5510 input.payload_objects,
5511 input.directory_hint_entries,
5512 );
5513 let expected_data_block_count = fec_layout_rows.iter().try_fold(0u64, |total, row| {
5514 if row.present {
5515 checked_u64_add(
5516 total,
5517 row.data_block_count as u64,
5518 "root-auth data block count",
5519 )
5520 } else {
5521 Ok(total)
5522 }
5523 })?;
5524 if expected_data_block_count != total_data_block_count {
5525 return Err(FormatError::WriterInvariant(
5526 "root-auth data block count does not match FEC layout",
5527 ));
5528 }
5529 let fec_layout_digest =
5530 fec_layout_digest_for_revision(FORMAT_VERSION, input.volume_format_rev, &fec_layout_rows)?;
5531 let archive_root = archive_root_for_revision(ArchiveRootInputs {
5532 archive_uuid: input.archive_uuid,
5533 session_id: input.session_id,
5534 format_version: FORMAT_VERSION,
5535 volume_format_rev: input.volume_format_rev,
5536 compression_algo: parsed_crypto.fixed.compression_algo,
5537 aead_algo: parsed_crypto.fixed.aead_algo,
5538 fec_algo: parsed_crypto.fixed.fec_algo,
5539 kdf_algo: parsed_crypto.fixed.kdf_algo,
5540 critical_metadata_digest,
5541 index_digest,
5542 fec_layout_digest,
5543 total_data_block_count,
5544 data_block_merkle_root,
5545 root_auth_descriptor_digest,
5546 signer_identity_digest,
5547 })?;
5548 let authenticator_value = authenticator(&RootAuthSigningRequest {
5549 root_auth_spec_id: root_auth_spec_id_for_revision(FORMAT_VERSION, input.volume_format_rev)?,
5550 archive_uuid: input.archive_uuid,
5551 session_id: input.session_id,
5552 archive_root,
5553 })?;
5554 if authenticator_value.len() != config.authenticator_value_length as usize {
5555 return Err(FormatError::WriterUnsupported(
5556 "root-auth authenticator length mismatch",
5557 ));
5558 }
5559
5560 RootAuthFooterV1 {
5561 archive_uuid: input.archive_uuid,
5562 session_id: input.session_id,
5563 format_version: FORMAT_VERSION,
5564 volume_format_rev: input.volume_format_rev,
5565 authenticator_id: config.authenticator_id,
5566 signer_identity_type: config.signer_identity_type,
5567 signer_identity_bytes: config.signer_identity.to_vec(),
5568 authenticator_value,
5569 total_data_block_count,
5570 critical_metadata_digest,
5571 index_digest,
5572 fec_layout_digest,
5573 data_block_merkle_root,
5574 signer_identity_digest,
5575 archive_root,
5576 footer_crc32c: 0,
5577 }
5578 .to_bytes()
5579}
5580
5581fn writer_fec_layout_rows_from_extents(
5582 index_root_extent: ObjectExtent,
5583 index_root_plain_size: u32,
5584 dictionary_extent: Option<(ObjectExtent, u32)>,
5585 shard_entries: &[ShardEntry],
5586 payload_objects: &[PayloadObject],
5587 directory_hint_entries: &[DirectoryHintShardEntry],
5588) -> Vec<FecLayoutObjectRow> {
5589 let mut rows = Vec::new();
5590 rows.push(FecLayoutObjectRow {
5591 object_class: 1,
5592 present: true,
5593 object_id: 0,
5594 first_block_index: index_root_extent.first_block_index,
5595 data_block_count: index_root_extent.data_block_count,
5596 parity_block_count: index_root_extent.parity_block_count,
5597 encrypted_size: index_root_extent.encrypted_size,
5598 plain_size: index_root_plain_size,
5599 });
5600 if let Some((dictionary, decompressed_size)) = dictionary_extent {
5601 rows.push(FecLayoutObjectRow {
5602 object_class: 2,
5603 present: true,
5604 object_id: 0,
5605 first_block_index: dictionary.first_block_index,
5606 data_block_count: dictionary.data_block_count,
5607 parity_block_count: dictionary.parity_block_count,
5608 encrypted_size: dictionary.encrypted_size,
5609 plain_size: decompressed_size,
5610 });
5611 } else {
5612 rows.push(FecLayoutObjectRow {
5613 object_class: 2,
5614 present: false,
5615 object_id: 0,
5616 first_block_index: 0,
5617 data_block_count: 0,
5618 parity_block_count: 0,
5619 encrypted_size: 0,
5620 plain_size: 0,
5621 });
5622 }
5623 for entry in shard_entries {
5624 rows.push(FecLayoutObjectRow {
5625 object_class: 3,
5626 present: true,
5627 object_id: entry.shard_index,
5628 first_block_index: entry.first_block_index,
5629 data_block_count: entry.data_block_count,
5630 parity_block_count: entry.parity_block_count,
5631 encrypted_size: entry.encrypted_size,
5632 plain_size: entry.decompressed_size,
5633 });
5634 }
5635 for payload in payload_objects {
5636 rows.push(FecLayoutObjectRow {
5637 object_class: 4,
5638 present: true,
5639 object_id: payload.envelope_index,
5640 first_block_index: payload.object.first_block_index,
5641 data_block_count: payload.object.data_block_count,
5642 parity_block_count: payload.object.parity_block_count,
5643 encrypted_size: payload.object.encrypted_size,
5644 plain_size: payload.plaintext_size,
5645 });
5646 }
5647 for entry in directory_hint_entries {
5648 rows.push(FecLayoutObjectRow {
5649 object_class: 5,
5650 present: true,
5651 object_id: entry.hint_shard_index,
5652 first_block_index: entry.first_block_index,
5653 data_block_count: entry.data_block_count,
5654 parity_block_count: entry.parity_block_count,
5655 encrypted_size: entry.encrypted_size,
5656 plain_size: entry.decompressed_size,
5657 });
5658 }
5659 rows
5660}
5661
5662fn manifest_footer_global_pre_hmac_bytes(manifest_footer: &[u8; MANIFEST_FOOTER_LEN]) -> [u8; 104] {
5663 let mut bytes = [0u8; 104];
5664 bytes.copy_from_slice(&manifest_footer[..104]);
5665 bytes[36..40].fill(0);
5666 bytes
5667}
5668
5669fn root_auth_footer_wire_length(
5670 signer_identity_len: usize,
5671 authenticator_value_len: usize,
5672) -> Result<u32, FormatError> {
5673 validate_root_auth_variable_lengths_for_writer(signer_identity_len, authenticator_value_len)?;
5674 let len = crate::format::ROOT_AUTH_FOOTER_FIXED_LEN
5675 .checked_add(signer_identity_len)
5676 .and_then(|value| value.checked_add(authenticator_value_len))
5677 .and_then(|value| value.checked_add(4))
5678 .ok_or(FormatError::WriterUnsupported(
5679 "RootAuthFooterV1 length overflow",
5680 ))?;
5681 if len > READER_MAX_ROOT_AUTH_FOOTER_LEN as usize {
5682 return Err(FormatError::ReaderResourceLimitExceeded {
5683 field: "RootAuthFooterV1 length",
5684 cap: READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
5685 actual: len as u64,
5686 });
5687 }
5688 u32::try_from(len).map_err(|_| FormatError::WriterUnsupported("RootAuthFooterV1 length"))
5689}
5690
5691fn validate_root_auth_writer_config(config: RootAuthWriterConfig<'_>) -> Result<(), FormatError> {
5692 root_auth_footer_wire_length(
5693 config.signer_identity.len(),
5694 config.authenticator_value_length as usize,
5695 )?;
5696 Ok(())
5697}
5698
5699fn validate_root_auth_variable_lengths_for_writer(
5700 signer_identity_len: usize,
5701 authenticator_value_len: usize,
5702) -> Result<(), FormatError> {
5703 if signer_identity_len > READER_MAX_ROOT_AUTH_SIGNER_IDENTITY_LEN as usize {
5704 return Err(FormatError::ReaderResourceLimitExceeded {
5705 field: "RootAuthFooterV1 signer identity length",
5706 cap: READER_MAX_ROOT_AUTH_SIGNER_IDENTITY_LEN as u64,
5707 actual: signer_identity_len as u64,
5708 });
5709 }
5710 if authenticator_value_len > READER_MAX_ROOT_AUTH_AUTHENTICATOR_VALUE_LEN as usize {
5711 return Err(FormatError::ReaderResourceLimitExceeded {
5712 field: "RootAuthFooterV1 authenticator value length",
5713 cap: READER_MAX_ROOT_AUTH_AUTHENTICATOR_VALUE_LEN as u64,
5714 actual: authenticator_value_len as u64,
5715 });
5716 }
5717 Ok(())
5718}
5719
5720#[allow(clippy::too_many_arguments)]
5721fn build_manifest_footer(
5722 subkeys: &Subkeys,
5723 aead_algo: AeadAlgo,
5724 volume_format_rev: u16,
5725 archive_uuid: [u8; 16],
5726 session_id: [u8; 16],
5727 volume_index: u32,
5728 total_volumes: u32,
5729 index_root_extent: &ObjectExtent,
5730 index_root_decompressed_size: usize,
5731) -> Result<[u8; MANIFEST_FOOTER_LEN], FormatError> {
5732 let mut footer = ManifestFooter {
5733 archive_uuid,
5734 session_id,
5735 volume_index,
5736 is_authoritative: 1,
5737 total_volumes,
5738 index_root_first_block: index_root_extent.first_block_index,
5739 index_root_data_block_count: index_root_extent.data_block_count,
5740 index_root_parity_block_count: index_root_extent.parity_block_count,
5741 index_root_encrypted_size: index_root_extent.encrypted_size,
5742 index_root_decompressed_size: u32_len(index_root_decompressed_size, "IndexRoot")?,
5743 manifest_hmac: [0u8; 32],
5744 };
5745 let mut bytes = footer.to_bytes();
5746 footer.manifest_hmac = compute_integrity_tag(
5747 HmacDomain::ManifestFooter,
5748 aead_algo,
5749 volume_format_rev,
5750 Some(&subkeys.mac_key),
5751 &archive_uuid,
5752 &session_id,
5753 &bytes[..104],
5754 )?;
5755 bytes = footer.to_bytes();
5756 Ok(bytes)
5757}
5758
5759#[derive(Debug, Clone, Copy)]
5760struct VolumeTrailerBuildInput<'a> {
5761 subkeys: &'a Subkeys,
5762 aead_algo: AeadAlgo,
5763 volume_format_rev: u16,
5764 archive_uuid: [u8; 16],
5765 session_id: [u8; 16],
5766 volume_index: u32,
5767 block_count: u64,
5768 bytes_written: u64,
5769 manifest_footer_offset: u64,
5770 closed_at_ns: i64,
5771 root_auth_footer: Option<(u64, u32)>,
5772}
5773
5774fn build_volume_trailer(
5775 input: VolumeTrailerBuildInput<'_>,
5776) -> Result<[u8; VOLUME_TRAILER_LEN], FormatError> {
5777 let (root_auth_footer_offset, root_auth_footer_length, root_auth_flags) =
5778 match input.root_auth_footer {
5779 Some((offset, length)) => (offset, length, 0x0000_0001),
5780 None => (0, 0, 0),
5781 };
5782 let mut trailer = VolumeTrailer {
5783 archive_uuid: input.archive_uuid,
5784 session_id: input.session_id,
5785 volume_index: input.volume_index,
5786 block_count: input.block_count,
5787 bytes_written: input.bytes_written,
5788 manifest_footer_offset: input.manifest_footer_offset,
5789 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
5790 closed_at_ns: input.closed_at_ns,
5791 root_auth_footer_offset,
5792 root_auth_footer_length,
5793 root_auth_flags,
5794 trailer_hmac: [0u8; 32],
5795 };
5796 let mut bytes = trailer.to_bytes();
5797 trailer.trailer_hmac = compute_integrity_tag(
5798 HmacDomain::VolumeTrailer,
5799 input.aead_algo,
5800 input.volume_format_rev,
5801 Some(&input.subkeys.mac_key),
5802 &input.archive_uuid,
5803 &input.session_id,
5804 &bytes[..96],
5805 )?;
5806 bytes = trailer.to_bytes();
5807 Ok(bytes)
5808}
5809
5810struct BuiltCmra {
5811 bytes: Vec<u8>,
5812 shard_size: u32,
5813 data_shard_count: u16,
5814 parity_shard_count: u16,
5815 image_length: u32,
5816 image_sha256: [u8; 32],
5817}
5818
5819#[derive(Debug, Clone, Copy)]
5820struct CmraBuildInput<'a> {
5821 volume_format_rev: u16,
5822 volume_header_bytes: &'a [u8; VOLUME_HEADER_LEN],
5823 crypto_header: &'a [u8],
5824 block_count: u64,
5825 block_records_offset: u64,
5826 manifest_footer_offset: u64,
5827 manifest_footer: &'a [u8; MANIFEST_FOOTER_LEN],
5828 root_auth_footer_offset: Option<u64>,
5829 root_auth_footer: Option<&'a [u8]>,
5830 key_wrap_table: Option<&'a [u8]>,
5831 trailer_offset: u64,
5832 trailer: &'a [u8; VOLUME_TRAILER_LEN],
5833 cmra_offset: u64,
5834 options: WriterOptions,
5835 archive_uuid: [u8; 16],
5836 session_id: [u8; 16],
5837 volume_index: u32,
5838}
5839
5840fn build_v41_cmra(input: CmraBuildInput<'_>) -> Result<BuiltCmra, FormatError> {
5841 let block_record_len = input.options.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
5842 let crypto_end = VOLUME_HEADER_LEN as u64 + input.crypto_header.len() as u64;
5843 let block_records_offset = input.block_records_offset;
5844 let key_wrap_table = input
5845 .key_wrap_table
5846 .map(|table| {
5847 if table.is_empty() {
5848 return Err(FormatError::WriterInvariant(
5849 "KeyWrapTableV1 must include at least one recipient record",
5850 ));
5851 }
5852 let key_wrap_table_length = u32_len(table.len(), "KeyWrapTableV1 table_length")?;
5853 Ok((table, key_wrap_table_length))
5854 })
5855 .transpose()?;
5856 if input.volume_format_rev != VOLUME_FORMAT_REV_44 && key_wrap_table.is_some() {
5857 return Err(FormatError::WriterInvariant(
5858 "KeyWrapTableV1 requires volume_format_rev 44",
5859 ));
5860 }
5861 let key_wrap_table_length = key_wrap_table.map(|(_, length)| u64::from(length));
5862 let expected_block_records_offset = match key_wrap_table_length {
5863 Some(length) => checked_u64_add(crypto_end, length, "KeyWrapTableV1")?,
5864 None => crypto_end,
5865 };
5866 if block_records_offset != expected_block_records_offset {
5867 return Err(FormatError::WriterInvariant(
5868 "CMRA block records offset does not match key-wrap table layout",
5869 ));
5870 }
5871 let block_records_length = checked_u64_mul(
5872 input.block_count,
5873 block_record_len,
5874 "CMRA BlockRecord length overflow",
5875 )?;
5876 let manifest_end = input
5877 .manifest_footer_offset
5878 .checked_add(MANIFEST_FOOTER_LEN as u64)
5879 .ok_or(FormatError::WriterUnsupported("CMRA terminal overflow"))?;
5880 let root_auth_footer_length = input
5881 .root_auth_footer
5882 .map(|footer| u32_len(footer.len(), "RootAuthFooterV1"))
5883 .transpose()?;
5884 match (input.root_auth_footer_offset, root_auth_footer_length) {
5885 (Some(offset), Some(length)) => {
5886 if manifest_end != offset
5887 || offset
5888 .checked_add(length as u64)
5889 .ok_or(FormatError::WriterUnsupported("CMRA terminal overflow"))?
5890 != input.trailer_offset
5891 {
5892 return Err(FormatError::WriterInvariant(
5893 "RootAuthFooter does not sit between ManifestFooter and VolumeTrailer",
5894 ));
5895 }
5896 }
5897 (None, None) => {
5898 if manifest_end != input.trailer_offset {
5899 return Err(FormatError::WriterInvariant(
5900 "ManifestFooter does not end at VolumeTrailer",
5901 ));
5902 }
5903 }
5904 _ => {
5905 return Err(FormatError::WriterInvariant(
5906 "RootAuthFooter offset/bytes mismatch",
5907 ));
5908 }
5909 }
5910 let body_bytes_before_cmra = input
5911 .trailer_offset
5912 .checked_add(VOLUME_TRAILER_LEN as u64)
5913 .ok_or(FormatError::WriterUnsupported("CMRA terminal overflow"))?;
5914 if body_bytes_before_cmra != input.cmra_offset {
5915 return Err(FormatError::WriterInvariant(
5916 "CMRA does not start after VolumeTrailer",
5917 ));
5918 }
5919
5920 let mut regions = vec![
5921 SerializedRegion {
5922 region_type: 1,
5923 offset: 0,
5924 bytes: input.volume_header_bytes.to_vec(),
5925 },
5926 SerializedRegion {
5927 region_type: 2,
5928 offset: VOLUME_HEADER_LEN as u64,
5929 bytes: input.crypto_header.to_vec(),
5930 },
5931 ];
5932 if let Some((table, _)) = key_wrap_table {
5933 regions.push(SerializedRegion {
5934 region_type: 6,
5935 offset: crypto_end,
5936 bytes: table.to_vec(),
5937 });
5938 }
5939 regions.push(SerializedRegion {
5940 region_type: 3,
5941 offset: input.manifest_footer_offset,
5942 bytes: input.manifest_footer.to_vec(),
5943 });
5944 if let (Some(offset), Some(footer)) = (input.root_auth_footer_offset, input.root_auth_footer) {
5945 regions.push(SerializedRegion {
5946 region_type: 4,
5947 offset,
5948 bytes: footer.to_vec(),
5949 });
5950 }
5951 regions.push(SerializedRegion {
5952 region_type: 5,
5953 offset: input.trailer_offset,
5954 bytes: input.trailer.to_vec(),
5955 });
5956 let root_auth_flag = if input.root_auth_footer.is_some() {
5957 0x0000_0001
5958 } else {
5959 0
5960 };
5961 let key_wrap_flag = if key_wrap_table.is_some() {
5962 0x0000_0002
5963 } else {
5964 0
5965 };
5966 let image = CriticalMetadataImage {
5967 volume_format_rev: input.volume_format_rev,
5968 archive_uuid: input.archive_uuid,
5969 session_id: input.session_id,
5970 volume_index: input.volume_index,
5971 stripe_width: input.options.stripe_width,
5972 layout_flags: root_auth_flag | key_wrap_flag,
5973 volume_header_offset: 0,
5974 volume_header_length: VOLUME_HEADER_LEN as u32,
5975 crypto_header_offset: VOLUME_HEADER_LEN as u64,
5976 crypto_header_length: u32_len(input.crypto_header.len(), "CryptoHeader")?,
5977 key_wrap_table_offset: key_wrap_table.map(|_| crypto_end).unwrap_or(0),
5978 key_wrap_table_length: key_wrap_table.map(|(_, length)| length).unwrap_or(0),
5979 block_records_offset,
5980 block_records_length,
5981 block_count: input.block_count,
5982 manifest_footer_offset: input.manifest_footer_offset,
5983 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
5984 root_auth_footer_offset: input.root_auth_footer_offset.unwrap_or(0),
5985 root_auth_footer_length: root_auth_footer_length.unwrap_or(0),
5986 volume_trailer_offset: input.trailer_offset,
5987 volume_trailer_length: VOLUME_TRAILER_LEN as u32,
5988 body_bytes_before_cmra,
5989 volume_header_sha256: sha256_bytes(input.volume_header_bytes),
5990 crypto_header_sha256: sha256_bytes(input.crypto_header),
5991 key_wrap_table_sha256: key_wrap_table
5992 .map(|(table, _)| sha256_bytes(table))
5993 .unwrap_or([0u8; 32]),
5994 manifest_footer_sha256: sha256_bytes(input.manifest_footer),
5995 root_auth_footer_sha256: input
5996 .root_auth_footer
5997 .map(sha256_bytes)
5998 .unwrap_or([0u8; 32]),
5999 volume_trailer_sha256: sha256_bytes(input.trailer),
6000 regions,
6001 };
6002 let image_bytes = image.to_bytes()?;
6003 let image_sha256 = sha256_bytes(&image_bytes);
6004 let data_shard_count = ceil_div(image_bytes.len() as u64, CMRA_SHARD_SIZE as u64)?;
6005 let data_shard_count_u16 = u16::try_from(data_shard_count)
6006 .map_err(|_| FormatError::WriterUnsupported("CMRA data shard count"))?;
6007 let parity_lower = cmra_min_parity_shards(data_shard_count, input.options.bit_rot_buffer_pct)?;
6008 let parity_upper = cmra_min_parity_shards(data_shard_count, READER_MAX_CMRA_PARITY_PCT as u8)?;
6009 if parity_lower > parity_upper {
6010 return Err(FormatError::WriterUnsupported("CMRA parity bounds"));
6011 }
6012 let parity_shard_count_u16 = u16::try_from(parity_lower)
6013 .map_err(|_| FormatError::WriterUnsupported("CMRA parity shard count"))?;
6014
6015 let mut data_shards = Vec::with_capacity(data_shard_count as usize);
6016 for idx in 0..data_shard_count as usize {
6017 let start = idx * CMRA_SHARD_SIZE;
6018 let end = (start + CMRA_SHARD_SIZE).min(image_bytes.len());
6019 let mut shard = vec![0u8; CMRA_SHARD_SIZE];
6020 if start < image_bytes.len() {
6021 shard[..end - start].copy_from_slice(&image_bytes[start..end]);
6022 }
6023 data_shards.push(shard);
6024 }
6025 let parity_shards = encode_parity_gf16(&data_shards, parity_shard_count_u16 as usize)?;
6026
6027 let header = CriticalMetadataRecoveryHeader {
6028 shard_size: CMRA_SHARD_SIZE as u32,
6029 data_shard_count: data_shard_count_u16,
6030 parity_shard_count: parity_shard_count_u16,
6031 image_length: u32_len(image_bytes.len(), "CriticalMetadataImageV1")?,
6032 archive_uuid_hint: input.archive_uuid,
6033 session_id_hint: input.session_id,
6034 volume_index_hint: input.volume_index,
6035 image_sha256,
6036 header_crc32c: 0,
6037 };
6038 let mut cmra = Vec::new();
6039 cmra.extend_from_slice(&header.to_bytes());
6040 for (idx, payload) in data_shards.into_iter().enumerate() {
6041 let payload_len = if idx + 1 == data_shard_count as usize {
6042 let final_len = image_bytes.len() - idx * CMRA_SHARD_SIZE;
6043 if final_len == 0 {
6044 CMRA_SHARD_SIZE
6045 } else {
6046 final_len
6047 }
6048 } else {
6049 CMRA_SHARD_SIZE
6050 };
6051 cmra.extend_from_slice(
6052 &CriticalMetadataRecoveryShard {
6053 shard_index: u16::try_from(idx)
6054 .map_err(|_| FormatError::WriterUnsupported("CMRA shard index"))?,
6055 shard_role: 0,
6056 shard_payload_length: u32_len(payload_len, "CMRA shard payload")?,
6057 payload,
6058 shard_crc32c: 0,
6059 }
6060 .to_bytes(CMRA_SHARD_SIZE)?,
6061 );
6062 }
6063 for (idx, payload) in parity_shards.into_iter().enumerate() {
6064 let shard_index = data_shard_count
6065 .checked_add(idx as u64)
6066 .ok_or(FormatError::WriterUnsupported("CMRA shard index overflow"))?;
6067 cmra.extend_from_slice(
6068 &CriticalMetadataRecoveryShard {
6069 shard_index: u16::try_from(shard_index)
6070 .map_err(|_| FormatError::WriterUnsupported("CMRA shard index"))?,
6071 shard_role: 1,
6072 shard_payload_length: CMRA_SHARD_SIZE as u32,
6073 payload,
6074 shard_crc32c: 0,
6075 }
6076 .to_bytes(CMRA_SHARD_SIZE)?,
6077 );
6078 }
6079
6080 Ok(BuiltCmra {
6081 bytes: cmra,
6082 shard_size: CMRA_SHARD_SIZE as u32,
6083 data_shard_count: data_shard_count_u16,
6084 parity_shard_count: parity_shard_count_u16,
6085 image_length: u32_len(image_bytes.len(), "CriticalMetadataImageV1")?,
6086 image_sha256,
6087 })
6088}
6089
6090fn cmra_min_parity_shards(data_shard_count: u64, pct: u8) -> Result<u64, FormatError> {
6091 let by_pct = ceil_div(
6092 checked_u64_mul(data_shard_count, pct as u64, "CMRA parity overflow")?,
6093 100,
6094 )?;
6095 Ok(2u64.max(by_pct))
6096}
6097
6098fn compute_object_parity(
6099 data_block_count: u64,
6100 options: WriterOptions,
6101 class_parity_shard_max: u32,
6102) -> Result<u32, FormatError> {
6103 let computed = compute_parity(data_block_count, options)?;
6104 if computed > class_parity_shard_max {
6105 return Err(FormatError::WriterUnsupported(
6106 "encrypted object exceeds its parity shard class maximum",
6107 ));
6108 }
6109 Ok(computed)
6110}
6111
6112fn validate_object_shard_total(
6113 data_block_count: u32,
6114 parity_block_count: u32,
6115) -> Result<(), FormatError> {
6116 let total = checked_u64_add(
6117 data_block_count as u64,
6118 parity_block_count as u64,
6119 "encrypted object shard total overflow",
6120 )?;
6121 if total > MAX_REED_SOLOMON_GF16_SHARDS {
6122 return Err(FormatError::WriterUnsupported(
6123 "encrypted object exceeds ReedSolomonGF16 shard limit",
6124 ));
6125 }
6126 Ok(())
6127}
6128
6129fn compute_parity_u16(
6130 data_block_count: u64,
6131 options: WriterOptions,
6132 field: &'static str,
6133) -> Result<u16, FormatError> {
6134 let parity = compute_parity(data_block_count, options)?;
6135 u16::try_from(parity).map_err(|_| FormatError::WriterUnsupported(field))
6136}
6137
6138fn compute_parity(data_block_count: u64, options: WriterOptions) -> Result<u32, FormatError> {
6139 let min_parity = if options.volume_loss_tolerance > 0 || options.bit_rot_buffer_pct > 0 {
6140 1u64
6141 } else {
6142 0u64
6143 };
6144 let mut parity = 0u64;
6145 for _ in 0..100 {
6146 let total = data_block_count
6147 .checked_add(parity)
6148 .ok_or(FormatError::WriterUnsupported("parity total overflow"))?;
6149 let by_volume = checked_u64_mul(
6150 options.volume_loss_tolerance as u64,
6151 ceil_div(total, options.stripe_width as u64)?,
6152 "volume-loss parity overflow",
6153 )?;
6154 let by_bitrot = ceil_div(
6155 checked_u64_mul(
6156 total,
6157 options.bit_rot_buffer_pct as u64,
6158 "bit-rot parity overflow",
6159 )?,
6160 100,
6161 )?;
6162 let next = by_volume
6163 .checked_add(by_bitrot)
6164 .ok_or(FormatError::WriterUnsupported("parity overflow"))?
6165 .max(min_parity);
6166 if next == parity {
6167 return u32::try_from(next).map_err(|_| FormatError::WriterUnsupported("parity count"));
6168 }
6169 parity = next;
6170 }
6171 Err(FormatError::WriterUnsupported(
6172 "parity calculation did not converge",
6173 ))
6174}
6175
6176fn ceil_div(numerator: u64, denominator: u64) -> Result<u64, FormatError> {
6177 if denominator == 0 {
6178 return Err(FormatError::WriterUnsupported("division by zero"));
6179 }
6180 numerator
6181 .checked_add(denominator - 1)
6182 .ok_or(FormatError::WriterUnsupported("ceiling division overflow"))
6183 .map(|value| value / denominator)
6184}
6185
6186fn checked_u64_mul(lhs: u64, rhs: u64, field: &'static str) -> Result<u64, FormatError> {
6187 lhs.checked_mul(rhs)
6188 .ok_or(FormatError::WriterUnsupported(field))
6189}
6190
6191#[allow(clippy::too_many_arguments)]
6192fn build_bootstrap_sidecar(
6193 subkeys: &Subkeys,
6194 aead_algo: AeadAlgo,
6195 volume_format_rev: u16,
6196 archive_uuid: [u8; 16],
6197 session_id: [u8; 16],
6198 manifest_footer: &[u8; MANIFEST_FOOTER_LEN],
6199 index_root_records: &[BlockRecord],
6200 dictionary_records: Option<&[BlockRecord]>,
6201) -> Result<Vec<u8>, FormatError> {
6202 let index_records_len = index_root_records.iter().try_fold(0usize, |sum, record| {
6203 checked_usize_add(sum, record.to_bytes().len(), "bootstrap sidecar")
6204 })?;
6205 let dictionary_records_len = dictionary_records
6206 .unwrap_or(&[])
6207 .iter()
6208 .try_fold(0usize, |sum, record| {
6209 checked_usize_add(sum, record.to_bytes().len(), "bootstrap sidecar")
6210 })?;
6211 let manifest_offset = BOOTSTRAP_SIDECAR_HEADER_LEN as u64;
6212 let index_root_offset = manifest_offset + MANIFEST_FOOTER_LEN as u64;
6213 let dictionary_offset = if dictionary_records.is_some() {
6214 index_root_offset + index_records_len as u64
6215 } else {
6216 0
6217 };
6218 let mut header = BootstrapSidecarHeader {
6219 archive_uuid,
6220 session_id,
6221 flags: if dictionary_records.is_some() {
6222 0x07
6223 } else {
6224 0x03
6225 },
6226 manifest_footer_offset: manifest_offset,
6227 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
6228 index_root_records_offset: index_root_offset,
6229 index_root_records_length: index_records_len as u64,
6230 dictionary_records_offset: dictionary_offset,
6231 dictionary_records_length: dictionary_records_len as u64,
6232 sidecar_hmac: [0u8; 32],
6233 header_crc32c: 0,
6234 };
6235 let mut header_bytes = header.to_bytes();
6236 header.sidecar_hmac = compute_integrity_tag(
6237 HmacDomain::BootstrapSidecar,
6238 aead_algo,
6239 volume_format_rev,
6240 Some(&subkeys.mac_key),
6241 &archive_uuid,
6242 &session_id,
6243 &header_bytes[..92],
6244 )?;
6245 header_bytes = header.to_bytes();
6246
6247 let mut sidecar = Vec::with_capacity(
6248 BOOTSTRAP_SIDECAR_HEADER_LEN
6249 + MANIFEST_FOOTER_LEN
6250 + index_records_len
6251 + dictionary_records_len,
6252 );
6253 sidecar.extend_from_slice(&header_bytes);
6254 sidecar.extend_from_slice(manifest_footer);
6255 for record in index_root_records {
6256 sidecar.extend_from_slice(&record.to_bytes());
6257 }
6258 if let Some(dictionary_records) = dictionary_records {
6259 for record in dictionary_records {
6260 sidecar.extend_from_slice(&record.to_bytes());
6261 }
6262 }
6263 Ok(sidecar)
6264}
6265
6266struct StreamingMemberReader<'a> {
6267 prefix: Cursor<Vec<u8>>,
6268 file: Box<dyn Read + 'a>,
6269 remaining_file_bytes: u64,
6270 remaining_padding_bytes: usize,
6271 pushback: Vec<u8>,
6272}
6273
6274impl<'a> StreamingMemberReader<'a> {
6275 fn new(file: Box<dyn Read + 'a>, prefix: Vec<u8>, file_size: u64) -> Self {
6276 Self {
6277 prefix: Cursor::new(prefix),
6278 file,
6279 remaining_file_bytes: file_size,
6280 remaining_padding_bytes: padding_to_512_u64(file_size) as usize,
6281 pushback: Vec::new(),
6282 }
6283 }
6284
6285 fn push_back(&mut self, bytes: Vec<u8>) {
6286 if bytes.is_empty() {
6287 return;
6288 }
6289 if self.pushback.is_empty() {
6290 self.pushback = bytes;
6291 } else {
6292 let mut merged = bytes;
6293 merged.extend_from_slice(&self.pushback);
6294 self.pushback = merged;
6295 }
6296 }
6297}
6298
6299impl Read for StreamingMemberReader<'_> {
6300 fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
6301 if out.is_empty() {
6302 return Ok(0);
6303 }
6304
6305 let mut written = 0usize;
6306 if !self.pushback.is_empty() {
6307 let count = out.len().min(self.pushback.len());
6308 out[..count].copy_from_slice(&self.pushback[..count]);
6309 self.pushback.drain(..count);
6310 written += count;
6311 if written == out.len() {
6312 return Ok(written);
6313 }
6314 }
6315
6316 let prefix_count = self.prefix.read(&mut out[written..])?;
6317 written += prefix_count;
6318 if written == out.len() {
6319 return Ok(written);
6320 }
6321
6322 if self.remaining_file_bytes > 0 {
6323 let max_file_read = (out.len() - written)
6324 .min(usize::try_from(self.remaining_file_bytes).unwrap_or(usize::MAX));
6325 let count = self.file.read(&mut out[written..written + max_file_read])?;
6326 if count == 0 {
6327 return Err(std::io::Error::new(
6328 std::io::ErrorKind::UnexpectedEof,
6329 "file source ended before declared size",
6330 ));
6331 }
6332 self.remaining_file_bytes -= count as u64;
6333 written += count;
6334 if written == out.len() {
6335 return Ok(written);
6336 }
6337 if self.remaining_file_bytes > 0 {
6338 return Ok(written);
6339 }
6340 }
6341
6342 if self.remaining_padding_bytes > 0 {
6343 let count = (out.len() - written).min(self.remaining_padding_bytes);
6344 out[written..written + count].fill(0);
6345 self.remaining_padding_bytes -= count;
6346 written += count;
6347 }
6348
6349 Ok(written)
6350 }
6351}
6352
6353fn build_regular_file_member_prefix(
6354 path: &[u8],
6355 file_size: u64,
6356 mode: u32,
6357 mtime: u64,
6358) -> Result<Vec<u8>, FormatError> {
6359 let mut out = Vec::new();
6360 let header_path = if path_requires_pax(path) {
6361 let pax_payload = build_pax_record("path", path)?;
6362 let pax_header = build_ustar_header(
6363 b"PaxHeaders/path",
6364 pax_payload.len() as u64,
6365 0o644,
6366 mtime,
6367 b'x',
6368 )?;
6369 out.extend_from_slice(&pax_header);
6370 out.extend_from_slice(&pax_payload);
6371 out.resize(out.len() + padding_to_512(pax_payload.len()), 0);
6372 pax_ustar_fallback_path(path)
6373 } else {
6374 path.to_vec()
6375 };
6376
6377 let header = build_ustar_header(&header_path, file_size, mode, mtime, b'0')?;
6378 out.extend_from_slice(&header);
6379 Ok(out)
6380}
6381
6382#[cfg(test)]
6383fn build_regular_file_member_group(
6384 path: &[u8],
6385 contents: &[u8],
6386 mode: u32,
6387 mtime: u64,
6388) -> Result<Vec<u8>, FormatError> {
6389 let mut out = build_regular_file_member_prefix(path, contents.len() as u64, mode, mtime)?;
6390 out.extend_from_slice(contents);
6391 out.resize(out.len() + padding_to_512(contents.len()), 0);
6392 Ok(out)
6393}
6394
6395fn path_requires_pax(path: &[u8]) -> bool {
6396 path.len() > 100 || !path.is_ascii()
6397}
6398
6399fn pax_ustar_fallback_path(path: &[u8]) -> Vec<u8> {
6400 path.rsplit(|byte| *byte == b'/')
6401 .next()
6402 .filter(|component| !component.is_empty() && component.len() <= 100 && component.is_ascii())
6403 .map(|component| component.to_vec())
6404 .unwrap_or_else(|| b"pax-file".to_vec())
6405}
6406
6407fn build_pax_record(key: &str, value: &[u8]) -> Result<Vec<u8>, FormatError> {
6408 let body_len = checked_usize_add(key.len(), 1, "PAX record")?;
6409 let body_len = checked_usize_add(body_len, value.len(), "PAX record")?;
6410 let body_len = checked_usize_add(body_len, 1, "PAX record")?;
6411 let mut digits = 1usize;
6412 loop {
6413 let len = checked_usize_add(digits, 1, "PAX record")?;
6414 let len = checked_usize_add(len, body_len, "PAX record")?;
6415 let next_digits = len.to_string().len();
6416 if next_digits == digits {
6417 let mut out = Vec::with_capacity(len);
6418 out.extend_from_slice(len.to_string().as_bytes());
6419 out.push(b' ');
6420 out.extend_from_slice(key.as_bytes());
6421 out.push(b'=');
6422 out.extend_from_slice(value);
6423 out.push(b'\n');
6424 return Ok(out);
6425 }
6426 digits = next_digits;
6427 }
6428}
6429
6430fn build_ustar_header(
6431 path: &[u8],
6432 size: u64,
6433 mode: u32,
6434 mtime: u64,
6435 typeflag: u8,
6436) -> Result<[u8; TAR_BLOCK_LEN], FormatError> {
6437 if path.len() > 100 {
6438 return Err(FormatError::WriterUnsupported(
6439 "ustar path exceeds name field",
6440 ));
6441 }
6442 let mut header = [0u8; TAR_BLOCK_LEN];
6443 header[0..path.len()].copy_from_slice(path);
6444 write_tar_octal(&mut header[100..108], mode as u64)?;
6445 write_tar_octal(&mut header[108..116], 0)?;
6446 write_tar_octal(&mut header[116..124], 0)?;
6447 write_tar_octal(&mut header[124..136], size)?;
6448 write_tar_octal(&mut header[136..148], mtime)?;
6449 header[148..156].fill(b' ');
6450 header[156] = typeflag;
6451 header[257..263].copy_from_slice(b"ustar\0");
6452 header[263..265].copy_from_slice(b"00");
6453 let checksum = header.iter().map(|byte| *byte as u32).sum::<u32>() as u64;
6454 write_tar_checksum(&mut header[148..156], checksum)?;
6455 Ok(header)
6456}
6457
6458fn write_tar_octal(field: &mut [u8], value: u64) -> Result<(), FormatError> {
6459 let digits = format!("{value:o}");
6460 if digits.len() + 1 > field.len() {
6461 return Err(FormatError::WriterUnsupported("tar octal field overflow"));
6462 }
6463 field.fill(0);
6464 let padding = field.len() - 1 - digits.len();
6465 for byte in &mut field[..padding] {
6466 *byte = b'0';
6467 }
6468 field[padding..padding + digits.len()].copy_from_slice(digits.as_bytes());
6469 Ok(())
6470}
6471
6472fn write_tar_checksum(field: &mut [u8], value: u64) -> Result<(), FormatError> {
6473 let digits = format!("{value:06o}");
6474 if digits.len() != 6 {
6475 return Err(FormatError::WriterUnsupported(
6476 "tar checksum field overflow",
6477 ));
6478 }
6479 field[0..6].copy_from_slice(digits.as_bytes());
6480 field[6] = 0;
6481 field[7] = b' ';
6482 Ok(())
6483}
6484
6485fn member_frame_range(
6486 member_index: usize,
6487 frames: &[PayloadFrame],
6488) -> Result<(u64, u32), FormatError> {
6489 let first = frames
6490 .iter()
6491 .find(|frame| frame.member_index == member_index)
6492 .map(|frame| frame.frame_index)
6493 .ok_or(FormatError::WriterInvariant("member frame is missing"))?;
6494 let count = frames
6495 .iter()
6496 .filter(|frame| frame.member_index == member_index)
6497 .count();
6498 Ok((first, u32_len(count, "FileEntry.frame_count")?))
6499}
6500
6501fn envelope_frame_range(
6502 envelope_index: u64,
6503 frames: &[PayloadFrame],
6504) -> Result<(u64, u32), FormatError> {
6505 let first = frames
6506 .iter()
6507 .find(|frame| frame.envelope_index == envelope_index)
6508 .map(|frame| frame.frame_index)
6509 .ok_or(FormatError::WriterInvariant("envelope frame is missing"))?;
6510 let count = frames
6511 .iter()
6512 .filter(|frame| frame.envelope_index == envelope_index)
6513 .count();
6514 Ok((first, u32_len(count, "EnvelopeEntry.frame_count")?))
6515}
6516
6517fn sha256_bytes(bytes: &[u8]) -> [u8; 32] {
6518 let digest = Sha256::digest(bytes);
6519 let mut out = [0u8; 32];
6520 out.copy_from_slice(&digest);
6521 out
6522}
6523
6524fn padding_to_512(len: usize) -> usize {
6525 let remainder = len % TAR_BLOCK_LEN;
6526 if remainder == 0 {
6527 0
6528 } else {
6529 TAR_BLOCK_LEN - remainder
6530 }
6531}
6532
6533fn padding_to_512_u64(len: u64) -> u64 {
6534 let remainder = len % TAR_BLOCK_LEN as u64;
6535 if remainder == 0 {
6536 0
6537 } else {
6538 TAR_BLOCK_LEN as u64 - remainder
6539 }
6540}
6541
6542fn table_offset(len: usize, cursor: usize) -> Result<u32, FormatError> {
6543 if len == 0 {
6544 Ok(0)
6545 } else {
6546 u32_len(cursor, "table offset")
6547 }
6548}
6549
6550fn u32_len(value: usize, field: &'static str) -> Result<u32, FormatError> {
6551 u32::try_from(value).map_err(|_| FormatError::WriterUnsupported(field))
6552}
6553
6554fn to_usize_writer(value: u64, field: &'static str) -> Result<usize, FormatError> {
6555 usize::try_from(value).map_err(|_| FormatError::WriterUnsupported(field))
6556}
6557
6558fn checked_usize_add(lhs: usize, rhs: usize, field: &'static str) -> Result<usize, FormatError> {
6559 lhs.checked_add(rhs)
6560 .ok_or(FormatError::WriterUnsupported(field))
6561}
6562
6563fn checked_u64_add(lhs: u64, rhs: u64, field: &'static str) -> Result<u64, FormatError> {
6564 lhs.checked_add(rhs)
6565 .ok_or(FormatError::WriterUnsupported(field))
6566}
6567
6568#[cfg(test)]
6569mod tests {
6570 use super::*;
6571 use crate::crypto::{verify_hmac, Subkeys};
6572 use crate::metadata::{DirectoryHintTable, IndexShard, MetadataLimits};
6573 use crate::reader::{open_archive, open_archive_with_recipient_wrap_resolver};
6574 use crate::tar_model::parse_tar_member_group;
6575 use crate::wire::{CriticalRecoveryLocator, CryptoHeader};
6576 use std::cell::RefCell;
6577 use std::io::{self, Read};
6578 use std::rc::Rc;
6579
6580 #[test]
6581 fn writer_defaults_use_v41_sizing_and_parallel_mode() {
6582 let options = WriterOptions::default();
6583
6584 assert_eq!(options.chunk_size, 256 * 1024);
6585 assert_eq!(options.envelope_target_size, 1024 * 1024);
6586 assert_eq!(options.block_size, 64 * 1024);
6587 assert_eq!(options.stripe_width, 8);
6588 assert_eq!(options.volume_loss_tolerance, 1);
6589 assert_eq!(options.fec_data_shards, 224);
6590 assert_eq!(options.index_fec_data_shards, 16);
6591 assert_eq!(
6592 options.index_root_fec_data_shards,
6593 MIN_INDEX_ROOT_FEC_DATA_SHARDS
6594 );
6595 assert_eq!(options.bit_rot_buffer_pct, 5);
6596 assert_eq!(options.jobs, default_jobs());
6597 assert!(options.jobs >= 1);
6598 }
6599
6600 #[test]
6601 fn emission_state_collects_data_leaf_hashes_only_for_root_auth() {
6602 let options = single_volume_metadata_test_options();
6603 let archive_uuid = [1u8; 16];
6604 let session_id = [2u8; 16];
6605 let crypto_header = b"test crypto header";
6606
6607 let mut unsigned_sink = MemoryArchiveSink::default();
6608 let volume_format_rev = volume_format_revision_for_options(&options, &KdfParams::None);
6609 let unsigned = begin_writer_emission_state(
6610 &mut unsigned_sink,
6611 options,
6612 crypto_header,
6613 None,
6614 archive_uuid,
6615 session_id,
6616 volume_format_rev,
6617 false,
6618 )
6619 .unwrap();
6620 assert!(unsigned.data_leaf_hashes.is_none());
6621
6622 let mut signed_sink = MemoryArchiveSink::default();
6623 let volume_format_rev = volume_format_revision_for_options(&options, &KdfParams::None);
6624 let signed = begin_writer_emission_state(
6625 &mut signed_sink,
6626 options,
6627 crypto_header,
6628 None,
6629 archive_uuid,
6630 session_id,
6631 volume_format_rev,
6632 true,
6633 )
6634 .unwrap();
6635 assert_eq!(signed.data_leaf_hashes.as_deref(), Some([].as_slice()));
6636 }
6637
6638 #[test]
6639 fn ordered_envelope_serializes_zero_parity_without_root_auth_leaf_collection() {
6640 let options = plan_writer_options(WriterOptions {
6641 stripe_width: 1,
6642 volume_loss_tolerance: 0,
6643 bit_rot_buffer_pct: 0,
6644 aead_algo: AeadAlgo::None,
6645 ..WriterOptions::default()
6646 })
6647 .unwrap();
6648 let payload = deterministic_bytes(options.block_size as usize * 2 + 17);
6649 let extent = ObjectExtent::new(
6650 11,
6651 plan_encrypted_object(
6652 payload.len(),
6653 options.fec_data_shards,
6654 options.fec_parity_shards,
6655 options,
6656 )
6657 .unwrap(),
6658 )
6659 .unwrap();
6660 assert_eq!(extent.parity_block_count, 0);
6661 assert_eq!(extent.data_block_count, 3);
6662 let subkeys = Subkeys::unencrypted_placeholder();
6663
6664 let result = build_ordered_envelope_result(
6665 OrderedEnvelopeJob {
6666 envelope_index: 3,
6667 plaintext: payload.clone(),
6668 extent,
6669 collect_data_leaf_hashes: false,
6670 },
6671 &subkeys,
6672 options,
6673 [1u8; 16],
6674 [2u8; 16],
6675 )
6676 .unwrap();
6677
6678 match result.records {
6679 OrderedEnvelopeRecords::Serialized(records) => {
6680 assert_eq!(records.len(), extent.data_block_count as usize);
6681 let parsed =
6682 BlockRecord::parse(&records[0].bytes, options.block_size as usize).unwrap();
6683 assert_eq!(parsed.block_index, extent.first_block_index);
6684 assert_eq!(parsed.kind, BlockKind::PayloadData);
6685 assert!(!parsed.is_last_data());
6686 let last =
6687 BlockRecord::parse(&records[2].bytes, options.block_size as usize).unwrap();
6688 assert_eq!(last.block_index, extent.first_block_index + 2);
6689 assert_eq!(last.kind, BlockKind::PayloadData);
6690 assert!(last.is_last_data());
6691 }
6692 OrderedEnvelopeRecords::Materialized(_) => {
6693 panic!("zero-parity unsigned envelope should use serialized records")
6694 }
6695 }
6696
6697 let result = build_ordered_envelope_result(
6698 OrderedEnvelopeJob {
6699 envelope_index: 3,
6700 plaintext: payload,
6701 extent,
6702 collect_data_leaf_hashes: true,
6703 },
6704 &subkeys,
6705 options,
6706 [1u8; 16],
6707 [2u8; 16],
6708 )
6709 .unwrap();
6710
6711 match result.records {
6712 OrderedEnvelopeRecords::Materialized(records) => {
6713 assert_eq!(records.len(), extent.data_block_count as usize);
6714 }
6715 OrderedEnvelopeRecords::Serialized(_) => {
6716 panic!("root-auth leaf collection requires materialized records")
6717 }
6718 }
6719 }
6720
6721 #[test]
6722 fn ordered_envelope_serializes_encrypted_zero_parity_like_materialized_path() {
6723 let options = plan_writer_options(WriterOptions {
6724 stripe_width: 1,
6725 volume_loss_tolerance: 0,
6726 bit_rot_buffer_pct: 0,
6727 aead_algo: AeadAlgo::AesGcmSiv256,
6728 ..WriterOptions::default()
6729 })
6730 .unwrap();
6731 let payload = deterministic_bytes(options.block_size as usize * 2 + 17);
6732 let extent = ObjectExtent::new(
6733 29,
6734 plan_encrypted_object(
6735 payload.len(),
6736 options.fec_data_shards,
6737 options.fec_parity_shards,
6738 options,
6739 )
6740 .unwrap(),
6741 )
6742 .unwrap();
6743 assert_eq!(extent.parity_block_count, 0);
6744 assert_eq!(extent.data_block_count, 3);
6745 let archive_uuid = [3u8; 16];
6746 let session_id = [4u8; 16];
6747 let master_key = MasterKey::from_raw_key(&[0x5a; 32]).unwrap();
6748 let subkeys = Subkeys::derive(&master_key, &archive_uuid, &session_id).unwrap();
6749
6750 let serialized = build_ordered_envelope_result(
6751 OrderedEnvelopeJob {
6752 envelope_index: 7,
6753 plaintext: payload.clone(),
6754 extent,
6755 collect_data_leaf_hashes: false,
6756 },
6757 &subkeys,
6758 options,
6759 archive_uuid,
6760 session_id,
6761 )
6762 .unwrap();
6763 let serialized_bytes = match serialized.records {
6764 OrderedEnvelopeRecords::Serialized(records) => records
6765 .into_iter()
6766 .map(|record| record.bytes)
6767 .collect::<Vec<_>>(),
6768 OrderedEnvelopeRecords::Materialized(_) => {
6769 panic!("zero-parity encrypted envelope should use serialized records")
6770 }
6771 };
6772
6773 let materialized = build_ordered_envelope_result(
6774 OrderedEnvelopeJob {
6775 envelope_index: 7,
6776 plaintext: payload,
6777 extent,
6778 collect_data_leaf_hashes: true,
6779 },
6780 &subkeys,
6781 options,
6782 archive_uuid,
6783 session_id,
6784 )
6785 .unwrap();
6786 let materialized_bytes = match materialized.records {
6787 OrderedEnvelopeRecords::Materialized(records) => records
6788 .iter()
6789 .map(BlockRecord::to_bytes)
6790 .collect::<Vec<_>>(),
6791 OrderedEnvelopeRecords::Serialized(_) => {
6792 panic!("root-auth leaf collection requires materialized records")
6793 }
6794 };
6795
6796 assert_eq!(serialized_bytes, materialized_bytes);
6797 }
6798
6799 #[test]
6800 fn writer_options_reject_zero_jobs() {
6801 let err = plan_writer_options(WriterOptions {
6802 jobs: 0,
6803 ..WriterOptions::default()
6804 })
6805 .unwrap_err();
6806
6807 assert_eq!(
6808 err,
6809 FormatError::WriterUnsupported("jobs must be at least 1")
6810 );
6811 }
6812
6813 #[test]
6814 fn production_writer_defaults_generate_distinct_v4_identities() {
6815 let master_key = MasterKey::from_raw_key(&[9u8; 32]).unwrap();
6816 let first = write_archive(&[], &master_key, WriterOptions::default()).unwrap();
6817 let second = write_archive(&[], &master_key, WriterOptions::default()).unwrap();
6818
6819 assert_ne!(first.archive_uuid, [0u8; 16]);
6820 assert_ne!(first.session_id, [0u8; 16]);
6821 assert_ne!(second.archive_uuid, [0u8; 16]);
6822 assert_ne!(second.session_id, [0u8; 16]);
6823 assert_ne!(first.archive_uuid, first.session_id);
6824 assert_ne!(first.archive_uuid, second.archive_uuid);
6825 assert_ne!(first.session_id, second.session_id);
6826
6827 for raw in [
6828 first.archive_uuid,
6829 first.session_id,
6830 second.archive_uuid,
6831 second.session_id,
6832 ] {
6833 let id = Uuid::from_bytes(raw);
6834 assert_eq!(id.get_version_num(), 4);
6835 }
6836
6837 let deterministic = WriterOptions {
6838 archive_uuid: Some([0x44; 16]),
6839 session_id: Some([0x55; 16]),
6840 ..WriterOptions::default()
6841 };
6842 let fixture = write_archive(&[], &master_key, deterministic).unwrap();
6843 assert_eq!(fixture.archive_uuid, [0x44; 16]);
6844 assert_eq!(fixture.session_id, [0x55; 16]);
6845 }
6846
6847 #[test]
6848 fn writer_partitions_multiple_default_sized_index_shards() {
6849 let members = (0..=DEFAULT_FILES_PER_INDEX_SHARD)
6850 .map(|idx| TarMember {
6851 path: format!("file-{idx:05}.txt").into_bytes(),
6852 tar_member_group_start: idx as u64 * 512,
6853 tar_member_group_size: 512,
6854 file_data_size: 0,
6855 kind: TarEntryKind::Regular,
6856 mode: 0o644,
6857 mtime: 0,
6858 })
6859 .collect::<Vec<_>>();
6860
6861 let shards = partition_file_rows(sorted_file_rows(&members)).unwrap();
6862
6863 assert_eq!(shards.len(), 2);
6864 assert_eq!(shards[0].len(), DEFAULT_FILES_PER_INDEX_SHARD);
6865 assert_eq!(shards[1].len(), 1);
6866 }
6867
6868 #[test]
6869 fn writer_extends_shard_for_bounded_hash_prefix_run() {
6870 let mut rows = Vec::new();
6871 rows.extend((0..9_000).map(|idx| test_file_row(idx, [0u8; 8])));
6872 rows.extend((9_000..54_000).map(|idx| test_file_row(idx, [1u8; 8])));
6873 rows.push(test_file_row(54_000, [2u8; 8]));
6874
6875 let shards = partition_file_rows(rows).unwrap();
6876
6877 assert_eq!(shards.len(), 2);
6878 assert_eq!(shards[0].len(), 54_000);
6879 assert!(shards[0]
6880 .iter()
6881 .skip(9_000)
6882 .all(|row| row.path_hash == [1u8; 8]));
6883 assert_eq!(shards[1][0].path_hash, [2u8; 8]);
6884 }
6885
6886 #[test]
6887 fn writer_splits_oversized_hash_prefix_run_at_writer_ceiling() {
6888 let rows = (0..MAX_HASH_PREFIX_RUN_FILES + 1)
6889 .map(|idx| test_file_row(idx, [7u8; 8]))
6890 .collect::<Vec<_>>();
6891
6892 let shards = partition_file_rows(rows).unwrap();
6893
6894 assert_eq!(shards.len(), 2);
6895 assert_eq!(shards[0].len(), MAX_HASH_PREFIX_RUN_FILES);
6896 assert_eq!(shards[1].len(), 1);
6897 }
6898
6899 #[test]
6900 fn writer_builds_directory_hint_rows_for_ancestor_directories() {
6901 let shard_rows = vec![
6902 vec![FileRow {
6903 path_hash: hash_prefix(b"a/b/one.txt"),
6904 path: b"a/b/one.txt".to_vec(),
6905 member_index: 0,
6906 member: TarMember {
6907 path: b"a/b/one.txt".to_vec(),
6908 tar_member_group_start: 0,
6909 tar_member_group_size: 512,
6910 file_data_size: 0,
6911 kind: TarEntryKind::Regular,
6912 mode: 0o644,
6913 mtime: 0,
6914 },
6915 }],
6916 vec![FileRow {
6917 path_hash: hash_prefix(b"a/c/two.txt"),
6918 path: b"a/c/two.txt".to_vec(),
6919 member_index: 1,
6920 member: TarMember {
6921 path: b"a/c/two.txt".to_vec(),
6922 tar_member_group_start: 512,
6923 tar_member_group_size: 512,
6924 file_data_size: 0,
6925 kind: TarEntryKind::Regular,
6926 mode: 0o644,
6927 mtime: 0,
6928 },
6929 }],
6930 ];
6931
6932 let options = plan_writer_options(WriterOptions::default()).unwrap();
6933 let planned = build_directory_hint_plaintexts(&shard_rows, options).unwrap();
6934 assert_eq!(planned.len(), 1);
6935 let locating = DirectoryHintShardEntry {
6936 hint_shard_index: planned[0].hint_shard_index,
6937 first_dir_hash: planned[0].first_dir_hash,
6938 last_dir_hash: planned[0].last_dir_hash,
6939 first_block_index: 0,
6940 data_block_count: 1,
6941 parity_block_count: 0,
6942 encrypted_size: 4096,
6943 decompressed_size: planned[0].plaintext.len() as u32,
6944 entry_count: planned[0].entry_count,
6945 };
6946 let table = DirectoryHintTable::parse(
6947 &planned[0].plaintext,
6948 &locating,
6949 2,
6950 MetadataLimits::default(),
6951 )
6952 .unwrap();
6953
6954 let root = table.lookup_directory_index(b"").unwrap();
6955 assert_eq!(table.shard_rows_for_entry(root).unwrap(), &[0, 1]);
6956 let a = table.lookup_directory_index(b"a").unwrap();
6957 assert_eq!(table.shard_rows_for_entry(a).unwrap(), &[0, 1]);
6958 let ab = table.lookup_directory_index(b"a/b").unwrap();
6959 assert_eq!(table.shard_rows_for_entry(ab).unwrap(), &[0]);
6960 }
6961
6962 #[test]
6963 fn directory_hints_are_required_only_above_v41_threshold() {
6964 assert!(!should_emit_directory_hints(0));
6965 assert!(!should_emit_directory_hints(
6966 DIRECTORY_HINT_REQUIRED_FILE_COUNT
6967 ));
6968 assert!(should_emit_directory_hints(
6969 DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1
6970 ));
6971 }
6972
6973 #[test]
6974 fn regular_file_writer_uses_local_pax_path_for_long_and_non_ascii_paths() {
6975 let long_path = format!("dir/{}.txt", "a".repeat(120));
6976 let unicode_path = "unicode/e\u{301}.txt";
6977 let files = [
6978 RegularFile::new(&long_path, b"long path"),
6979 RegularFile::new(unicode_path, b"unicode path"),
6980 ];
6981
6982 let (tar_stream, members) = build_tar_stream(&files, 4096).unwrap();
6983
6984 for (member, expected_path, expected_data) in [
6985 (&members[0], long_path.as_bytes(), b"long path".as_slice()),
6986 (
6987 &members[1],
6988 "unicode/\u{e9}.txt".as_bytes(),
6989 b"unicode path".as_slice(),
6990 ),
6991 ] {
6992 let start = member.tar_member_group_start as usize;
6993 let end = start + member.tar_member_group_size as usize;
6994 let group = &tar_stream[start..end];
6995 assert_eq!(group[156], b'x');
6996 let parsed = parse_tar_member_group(group, 4096).unwrap();
6997 assert_eq!(parsed.path, expected_path);
6998 assert_eq!(parsed.data, expected_data);
6999 }
7000 }
7001
7002 #[test]
7003 fn regular_file_writer_emits_no_global_metadata_or_tar_eof() {
7004 let long_path = format!("dir/{}.txt", "a".repeat(120));
7005 let files = [
7006 RegularFile::new("plain.txt", b"plain contents"),
7007 RegularFile::new(&long_path, b"long path contents"),
7008 ];
7009
7010 let (tar_stream, members) = build_tar_stream(&files, 4096).unwrap();
7011
7012 let member_bytes = members
7013 .iter()
7014 .map(|member| member.tar_member_group_size)
7015 .sum::<u64>();
7016 assert_eq!(tar_stream.len() as u64, member_bytes);
7017 assert!(!tar_stream[tar_stream.len() - TAR_BLOCK_LEN * 2..]
7018 .chunks(TAR_BLOCK_LEN)
7019 .all(|block| block.iter().all(|byte| *byte == 0)));
7020
7021 for member in members {
7022 let start = member.tar_member_group_start as usize;
7023 let end = start + member.tar_member_group_size as usize;
7024 assert_path_specific_member_group(&tar_stream[start..end]);
7025 }
7026 }
7027
7028 #[test]
7029 fn regular_file_writer_round_trips_mode_and_mtime() {
7030 let group =
7031 build_regular_file_member_group(b"script.sh", b"#!/bin/sh\n", 0o755, 1_700_000_000)
7032 .unwrap();
7033
7034 let parsed = parse_tar_member_group(&group, 4096).unwrap();
7035
7036 assert_eq!(parsed.mode, 0o755);
7037 assert_eq!(parsed.mtime, 1_700_000_000);
7038 }
7039
7040 #[test]
7041 fn writer_splits_large_payload_across_seekable_envelopes() {
7042 let master_key = MasterKey::from_raw_key(&[8u8; 32]).unwrap();
7043 let data = deterministic_bytes(2 * 1024 * 1024);
7044 let archive = write_archive(
7045 &[RegularFile::new("large.bin", &data)],
7046 &master_key,
7047 WriterOptions {
7048 stripe_width: 1,
7049 volume_loss_tolerance: 0,
7050 bit_rot_buffer_pct: 0,
7051 ..WriterOptions::default()
7052 },
7053 )
7054 .unwrap();
7055 let opened = open_archive(&archive.bytes, &master_key).unwrap();
7056
7057 assert_eq!(opened.list_files().unwrap()[0].path, "large.bin");
7058 assert_eq!(opened.extract_file("large.bin").unwrap(), Some(data));
7059 opened.verify().unwrap();
7060 assert!(opened.index_root.header.envelope_count > 1);
7061 }
7062
7063 #[test]
7064 fn split_member_frames_carry_exact_boundary_flags() {
7065 let data = deterministic_bytes(12 * 1024);
7066 let files = [RegularFile::new("large.bin", &data)];
7067 let options = WriterOptions {
7068 chunk_size: 1024,
7069 envelope_target_size: 64 * 1024,
7070 stripe_width: 1,
7071 volume_loss_tolerance: 0,
7072 bit_rot_buffer_pct: 0,
7073 ..WriterOptions::default()
7074 };
7075 let (tar_stream, members) = build_tar_stream(&files, options.max_path_length).unwrap();
7076 let (_, frames) = build_payload_envelopes(&tar_stream, &members, options, None).unwrap();
7077
7078 assert!(frames.len() > 2);
7079 assert_eq!(frames.first().unwrap().flags, 0x0000_0001);
7080 assert_eq!(frames.last().unwrap().flags, 0x0000_0002);
7081 assert!(frames[1..frames.len() - 1]
7082 .iter()
7083 .all(|frame| frame.flags == 0));
7084 }
7085
7086 #[test]
7087 fn spanning_payload_derives_exact_multi_frame_multi_envelope_stats() {
7088 let data = deterministic_bytes(13 * 1024);
7089 let files = [RegularFile::new("spanning.bin", &data)];
7090 let options = plan_writer_options(WriterOptions {
7091 block_size: MIN_BLOCK_SIZE,
7092 chunk_size: 1024,
7093 envelope_target_size: 2500,
7094 stripe_width: 1,
7095 volume_loss_tolerance: 0,
7096 bit_rot_buffer_pct: 0,
7097 fec_parity_shards: 0,
7098 index_fec_parity_shards: 0,
7099 index_root_fec_parity_shards: 0,
7100 ..WriterOptions::default()
7101 })
7102 .unwrap();
7103 let mut next_block_index = 0u64;
7104
7105 let payload = plan_payload_stream(&files, options, None, &mut next_block_index).unwrap();
7106 let expected_tar_total_size =
7107 TAR_BLOCK_LEN as u64 + data.len() as u64 + padding_to_512(data.len()) as u64;
7108 let (tar_stream, _) = build_tar_stream(&files, options.max_path_length).unwrap();
7109
7110 assert_eq!(payload.tar_members.len(), 1);
7111 assert_eq!(payload.tar_members[0].tar_member_group_start, 0);
7112 assert_eq!(
7113 payload.tar_members[0].tar_member_group_size,
7114 expected_tar_total_size
7115 );
7116 assert_eq!(payload.tar_total_size, expected_tar_total_size);
7117 assert_eq!(payload.content_sha256, sha256_bytes(&tar_stream));
7118 assert_eq!(payload.frames.len(), 14);
7119 assert_eq!(payload.payload_objects.len(), 7);
7120 assert_eq!(payload.payload_block_count, 7);
7121 assert_eq!(next_block_index, 7);
7122
7123 for (idx, frame) in payload.frames.iter().enumerate() {
7124 let expected_envelope_index = (idx / 2) as u64;
7125 let expected_decompressed_size = if idx == 13 { 512 } else { 1024 };
7126 let expected_flags = match idx {
7127 0 => 0x0000_0001,
7128 13 => 0x0000_0002,
7129 _ => 0,
7130 };
7131 let expected_offset = if idx % 2 == 0 {
7132 0
7133 } else {
7134 payload.frames[idx - 1].compressed_size
7135 };
7136
7137 assert_eq!(frame.frame_index, idx as u64);
7138 assert_eq!(frame.member_index, 0);
7139 assert_eq!(frame.envelope_index, expected_envelope_index);
7140 assert_eq!(frame.offset_in_envelope, expected_offset);
7141 assert_eq!(frame.decompressed_size, expected_decompressed_size);
7142 assert_eq!(frame.flags, expected_flags);
7143 assert_eq!(frame.tar_stream_offset, idx as u64 * 1024);
7144 }
7145
7146 for (idx, object) in payload.payload_objects.iter().enumerate() {
7147 let frame_start = idx * 2;
7148 let expected_plaintext_size = payload.frames[frame_start..frame_start + 2]
7149 .iter()
7150 .map(|frame| frame.compressed_size)
7151 .sum::<u32>();
7152
7153 assert_eq!(object.envelope_index, idx as u64);
7154 assert_eq!(object.plaintext_size, expected_plaintext_size);
7155 assert_eq!(object.object.first_block_index, idx as u64);
7156 assert_eq!(object.object.data_block_count, 1);
7157 assert_eq!(object.object.parity_block_count, 0);
7158 assert_eq!(object.object.encrypted_size, options.block_size);
7159 }
7160
7161 let shard_rows = partition_file_rows(sorted_file_rows(&payload.tar_members)).unwrap();
7162 let planned_shards = build_index_shard_plaintexts(
7163 &shard_rows,
7164 &payload.frames,
7165 &payload.payload_objects,
7166 options,
7167 )
7168 .unwrap();
7169 assert_eq!(planned_shards.len(), 1);
7170 let locating_shard = ShardEntry {
7171 shard_index: planned_shards[0].shard_index,
7172 first_block_index: 0,
7173 data_block_count: 1,
7174 parity_block_count: 0,
7175 encrypted_size: options.block_size,
7176 decompressed_size: planned_shards[0].plaintext.len() as u32,
7177 file_count: planned_shards[0].file_count,
7178 first_path_hash: planned_shards[0].first_path_hash,
7179 last_path_hash: planned_shards[0].last_path_hash,
7180 };
7181 let shard = IndexShard::parse(
7182 &planned_shards[0].plaintext,
7183 &locating_shard,
7184 MetadataLimits::default(),
7185 )
7186 .unwrap();
7187 assert_eq!(shard.header.file_count, 1);
7188 assert_eq!(shard.header.frame_count, 14);
7189 assert_eq!(shard.header.envelope_count, 7);
7190 assert_eq!(shard.files[0].first_frame_index, 0);
7191 assert_eq!(shard.files[0].frame_count, 14);
7192 assert_eq!(
7193 shard.files[0].tar_member_group_size,
7194 expected_tar_total_size
7195 );
7196 assert_eq!(shard.files[0].file_data_size, data.len() as u64);
7197
7198 for (idx, frame) in shard.frames.iter().enumerate() {
7199 assert_eq!(frame.frame_index, idx as u64);
7200 assert_eq!(frame.envelope_index, (idx / 2) as u64);
7201 assert_eq!(
7202 frame.offset_in_envelope,
7203 payload.frames[idx].offset_in_envelope
7204 );
7205 assert_eq!(frame.compressed_size, payload.frames[idx].compressed_size);
7206 assert_eq!(
7207 frame.decompressed_size,
7208 payload.frames[idx].decompressed_size
7209 );
7210 assert_eq!(frame.flags, payload.frames[idx].flags);
7211 assert_eq!(
7212 frame.tar_stream_offset,
7213 payload.frames[idx].tar_stream_offset
7214 );
7215 }
7216
7217 for (idx, envelope) in shard.envelopes.iter().enumerate() {
7218 assert_eq!(envelope.envelope_index, idx as u64);
7219 assert_eq!(envelope.first_block_index, idx as u64);
7220 assert_eq!(envelope.data_block_count, 1);
7221 assert_eq!(envelope.parity_block_count, 0);
7222 assert_eq!(envelope.encrypted_size, options.block_size);
7223 assert_eq!(
7224 envelope.plaintext_size,
7225 payload.payload_objects[idx].plaintext_size
7226 );
7227 assert_eq!(envelope.first_frame_index, (idx * 2) as u64);
7228 assert_eq!(envelope.frame_count, 2);
7229 }
7230
7231 let master_key = MasterKey::from_raw_key(&[6u8; 32]).unwrap();
7232 let plan = build_writer_plan_from_payload(
7233 payload,
7234 next_block_index,
7235 &master_key,
7236 options,
7237 None,
7238 &KdfParams::Raw,
7239 None,
7240 [0x44; 16],
7241 [0x55; 16],
7242 None,
7243 )
7244 .unwrap();
7245 let index_root =
7246 IndexRoot::parse(&plan.index_root_plaintext, false, MetadataLimits::default()).unwrap();
7247
7248 assert_eq!(index_root.header.frame_count, 14);
7249 assert_eq!(index_root.header.envelope_count, 7);
7250 assert_eq!(index_root.header.file_count, 1);
7251 assert_eq!(index_root.header.payload_block_count, 7);
7252 assert_eq!(index_root.header.tar_total_size, expected_tar_total_size);
7253 assert_eq!(index_root.header.content_sha256, sha256_bytes(&tar_stream));
7254 }
7255
7256 #[test]
7257 fn writes_empty_archive_with_authentic_bootstrap_structures() {
7258 let master_key = MasterKey::from_raw_key(&[7u8; 32]).unwrap();
7259 let archive = write_empty_archive(&master_key).unwrap();
7260 let bytes = archive.bytes;
7261
7262 let volume_header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
7263 assert_eq!(volume_header.archive_uuid, archive.archive_uuid);
7264 assert_eq!(volume_header.session_id, archive.session_id);
7265
7266 let crypto_start = VOLUME_HEADER_LEN;
7267 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
7268 let crypto_header = CryptoHeader::parse(
7269 &bytes[crypto_start..crypto_end],
7270 volume_header.crypto_header_length,
7271 )
7272 .unwrap();
7273 let subkeys =
7274 Subkeys::derive(&master_key, &archive.archive_uuid, &archive.session_id).unwrap();
7275 verify_hmac(
7276 HmacDomain::CryptoHeader,
7277 &subkeys.mac_key,
7278 &archive.archive_uuid,
7279 &archive.session_id,
7280 crypto_header.hmac_covered_bytes,
7281 &crypto_header.header_hmac,
7282 )
7283 .unwrap();
7284
7285 let locator =
7286 CriticalRecoveryLocator::parse(&bytes[bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN..])
7287 .unwrap();
7288 let trailer_offset = locator.volume_trailer_offset as usize;
7289 let trailer =
7290 VolumeTrailer::parse(&bytes[trailer_offset..trailer_offset + VOLUME_TRAILER_LEN])
7291 .unwrap();
7292 assert_eq!(trailer.bytes_written, trailer_offset as u64);
7293 verify_hmac(
7294 HmacDomain::VolumeTrailer,
7295 &subkeys.mac_key,
7296 &archive.archive_uuid,
7297 &archive.session_id,
7298 &bytes[trailer_offset..trailer_offset + 96],
7299 &trailer.trailer_hmac,
7300 )
7301 .unwrap();
7302
7303 let manifest_offset = trailer.manifest_footer_offset as usize;
7304 let manifest_end = manifest_offset + MANIFEST_FOOTER_LEN;
7305 let manifest = ManifestFooter::parse(&bytes[manifest_offset..manifest_end]).unwrap();
7306 assert_eq!(manifest.is_authoritative, 1);
7307 assert_eq!(manifest.total_volumes, DEFAULT_STRIPE_WIDTH);
7308 verify_hmac(
7309 HmacDomain::ManifestFooter,
7310 &subkeys.mac_key,
7311 &archive.archive_uuid,
7312 &archive.session_id,
7313 &bytes[manifest_offset..manifest_offset + 104],
7314 &manifest.manifest_hmac,
7315 )
7316 .unwrap();
7317 }
7318
7319 #[test]
7320 fn parity_auto_scaling_matches_v41_examples() {
7321 let options = WriterOptions {
7322 fec_data_shards: 224,
7323 stripe_width: 8,
7324 volume_loss_tolerance: 1,
7325 bit_rot_buffer_pct: 5,
7326 ..WriterOptions::default()
7327 };
7328
7329 assert_eq!(compute_parity(224, options).unwrap(), 48);
7330 assert_eq!(compute_parity(17, options).unwrap(), 5);
7331 }
7332
7333 #[test]
7334 fn parity_auto_scaling_rejects_non_convergent_budget() {
7335 let err = compute_parity(
7336 1,
7337 WriterOptions {
7338 stripe_width: 2,
7339 volume_loss_tolerance: 1,
7340 bit_rot_buffer_pct: 50,
7341 ..WriterOptions::default()
7342 },
7343 )
7344 .unwrap_err();
7345
7346 assert_eq!(
7347 err,
7348 FormatError::WriterUnsupported("parity calculation did not converge")
7349 );
7350 }
7351
7352 #[test]
7353 fn zero_parity_is_allowed_when_no_recovery_margin_is_requested() {
7354 let planned = plan_writer_options(WriterOptions {
7355 bit_rot_buffer_pct: 0,
7356 stripe_width: 1,
7357 volume_loss_tolerance: 0,
7358 fec_parity_shards: 0,
7359 index_fec_parity_shards: 0,
7360 index_root_fec_parity_shards: 0,
7361 ..WriterOptions::default()
7362 })
7363 .unwrap();
7364
7365 assert_eq!(planned.fec_parity_shards, 0);
7366 assert_eq!(planned.index_fec_parity_shards, 0);
7367 assert_eq!(planned.index_root_fec_parity_shards, 0);
7368 assert_eq!(compute_parity(1, planned).unwrap(), 0);
7369 }
7370
7371 #[test]
7372 fn index_root_data_shard_maximum_obeys_v41_minimum() {
7373 let planned = plan_writer_options(WriterOptions {
7374 index_root_fec_data_shards: 1,
7375 ..WriterOptions::default()
7376 })
7377 .unwrap();
7378
7379 assert_eq!(
7380 planned.index_root_fec_data_shards,
7381 MIN_INDEX_ROOT_FEC_DATA_SHARDS
7382 );
7383 }
7384
7385 #[test]
7386 fn metadata_class_planning_raises_index_root_class_above_default() {
7387 let options = plan_writer_options(WriterOptions {
7388 block_size: MIN_BLOCK_SIZE,
7389 index_root_fec_parity_shards: 0,
7390 bit_rot_buffer_pct: 0,
7391 ..WriterOptions::default()
7392 })
7393 .unwrap();
7394 let index_root_payload_len = payload_len_for_encrypted_data_blocks(64, options);
7395
7396 let planned =
7397 plan_index_root_metadata_class(options, index_root_payload_len, None).unwrap();
7398
7399 assert_eq!(planned.index_root.data_block_count, 64);
7400 assert_eq!(planned.options.index_root_fec_data_shards, 64);
7401 assert_eq!(
7402 planned.options.index_root_fec_parity_shards,
7403 compute_parity_u16(
7404 planned.options.index_root_fec_data_shards as u64,
7405 planned.options,
7406 "index_root_fec_parity_shards",
7407 )
7408 .unwrap()
7409 );
7410 }
7411
7412 #[test]
7413 fn single_pass_writer_predeclares_metadata_class_before_payload_streaming() {
7414 let planned = plan_single_pass_writer_options(WriterOptions {
7415 block_size: MIN_BLOCK_SIZE,
7416 stripe_width: 1,
7417 volume_loss_tolerance: 0,
7418 bit_rot_buffer_pct: 0,
7419 index_root_fec_parity_shards: 0,
7420 ..WriterOptions::default()
7421 })
7422 .unwrap();
7423
7424 assert!(planned.index_root_fec_data_shards > DEFAULT_INDEX_ROOT_FEC_DATA_SHARDS);
7425 let index_root_payload_len = payload_len_for_encrypted_data_blocks(
7426 u32::from(planned.index_root_fec_data_shards - 1),
7427 planned,
7428 );
7429 let metadata_class =
7430 plan_index_root_metadata_class(planned, index_root_payload_len, None).unwrap();
7431
7432 assert_eq!(metadata_class.options, planned);
7433 }
7434
7435 #[test]
7436 fn metadata_class_planning_rejects_oversized_index_root() {
7437 let options = single_volume_metadata_test_options();
7438 let index_root_payload_len =
7439 payload_len_for_encrypted_data_blocks(u16::MAX as u32 + 1, options);
7440
7441 let err =
7442 plan_index_root_metadata_class(options, index_root_payload_len, None).unwrap_err();
7443
7444 assert_eq!(err, FormatError::WriterUnsupported("IndexRoot too large"));
7445 }
7446
7447 #[test]
7448 fn metadata_class_planning_rejects_index_root_u32_encrypted_size_overflow() {
7449 let options = single_volume_metadata_test_options();
7450 let index_root_payload_len = u32::MAX as usize - options.aead_algo.tag_len() + 1;
7451
7452 let err =
7453 plan_index_root_metadata_class(options, index_root_payload_len, None).unwrap_err();
7454
7455 assert_eq!(err, FormatError::WriterUnsupported("IndexRoot too large"));
7456 }
7457
7458 #[test]
7459 fn metadata_class_planning_rejects_oversized_dictionary() {
7460 let options = single_volume_metadata_test_options();
7461 let dictionary_payload_len =
7462 payload_len_for_encrypted_data_blocks(u16::MAX as u32 + 1, options);
7463
7464 let err =
7465 plan_index_root_metadata_class(options, 1, Some(dictionary_payload_len)).unwrap_err();
7466
7467 assert_eq!(
7468 err,
7469 FormatError::WriterUnsupported("dictionary object too large")
7470 );
7471 }
7472
7473 #[test]
7474 fn metadata_class_planning_rejects_gf16_total_overflow_for_dictionary() {
7475 let options = plan_writer_options(WriterOptions {
7476 block_size: MIN_BLOCK_SIZE,
7477 stripe_width: 8,
7478 volume_loss_tolerance: 1,
7479 bit_rot_buffer_pct: 5,
7480 ..WriterOptions::default()
7481 })
7482 .unwrap();
7483 let dictionary_payload_len = payload_len_for_encrypted_data_blocks(60_000, options);
7484
7485 let err =
7486 plan_index_root_metadata_class(options, 1, Some(dictionary_payload_len)).unwrap_err();
7487
7488 assert_eq!(
7489 err,
7490 FormatError::WriterUnsupported("dictionary object too large")
7491 );
7492 }
7493
7494 #[test]
7495 fn written_archive_authenticates_final_index_root_fec_class() {
7496 let master_key = MasterKey::from_raw_key(&[9u8; 32]).unwrap();
7497 let dictionary = deterministic_bytes(80 * 1024);
7498 let file = RegularFile::new("uses-dictionary.txt", b"payload");
7499 let archive = write_archive_with_dictionary(
7500 &[file],
7501 &master_key,
7502 WriterOptions {
7503 block_size: MIN_BLOCK_SIZE,
7504 stripe_width: 1,
7505 volume_loss_tolerance: 0,
7506 bit_rot_buffer_pct: 0,
7507 index_root_fec_parity_shards: 0,
7508 ..WriterOptions::default()
7509 },
7510 &dictionary,
7511 )
7512 .unwrap();
7513
7514 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
7515 let crypto_start = VOLUME_HEADER_LEN;
7516 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
7517 let crypto_header = CryptoHeader::parse(
7518 &archive.bytes[crypto_start..crypto_end],
7519 volume_header.crypto_header_length,
7520 )
7521 .unwrap();
7522 let subkeys =
7523 Subkeys::derive(&master_key, &archive.archive_uuid, &archive.session_id).unwrap();
7524 verify_hmac(
7525 HmacDomain::CryptoHeader,
7526 &subkeys.mac_key,
7527 &archive.archive_uuid,
7528 &archive.session_id,
7529 crypto_header.hmac_covered_bytes,
7530 &crypto_header.header_hmac,
7531 )
7532 .unwrap();
7533
7534 assert!(crypto_header.fixed.index_root_fec_data_shards > MIN_INDEX_ROOT_FEC_DATA_SHARDS);
7535 assert_eq!(crypto_header.fixed.index_root_fec_parity_shards, 0);
7536 let opened = open_archive(&archive.bytes, &master_key).unwrap();
7537 assert_eq!(
7538 opened.extract_file("uses-dictionary.txt").unwrap(),
7539 Some(b"payload".to_vec())
7540 );
7541 opened.verify().unwrap();
7542 }
7543
7544 #[test]
7545 fn object_parity_uses_per_object_recurrence_even_with_larger_class_max() {
7546 let options = WriterOptions {
7547 bit_rot_buffer_pct: 0,
7548 stripe_width: 1,
7549 volume_loss_tolerance: 0,
7550 fec_parity_shards: 1,
7551 ..WriterOptions::default()
7552 };
7553
7554 assert_eq!(compute_object_parity(1, options, 1).unwrap(), 0);
7555 }
7556
7557 #[test]
7558 fn object_total_shards_obeys_reed_solomon_limit() {
7559 assert!(validate_object_shard_total(65_535, 0).is_ok());
7560 assert_eq!(
7561 validate_object_shard_total(65_535, 1).unwrap_err(),
7562 FormatError::WriterUnsupported("encrypted object exceeds ReedSolomonGF16 shard limit")
7563 );
7564 }
7565
7566 #[test]
7567 fn argon2id_kdf_serialization_rejects_memory_requirement_overflow() {
7568 assert_eq!(
7569 serialize_kdf_params(&KdfParams::Argon2id {
7570 t_cost: 1,
7571 m_cost_kib: u32::MAX,
7572 parallelism: u32::MAX,
7573 salt: b"12345678".to_vec(),
7574 })
7575 .unwrap_err(),
7576 FormatError::InvalidKdfParams("m_cost_kib requirement overflow")
7577 );
7578 }
7579
7580 #[test]
7581 fn recipient_wrap_kdf_serialization() {
7582 let params = KdfParams::RecipientWrap {
7583 key_wrap_table_length: 16,
7584 key_wrap_table_record_count: 4,
7585 key_wrap_table_version: 1,
7586 key_wrap_table_digest: [0xaau8; 32],
7587 };
7588 let serialized = serialize_kdf_params(¶ms).unwrap();
7589 let mut expected = Vec::new();
7590 expected.extend_from_slice(&(KdfAlgo::RecipientWrap as u16).to_le_bytes());
7591 expected.extend_from_slice(&16u32.to_le_bytes());
7592 expected.extend_from_slice(&4u32.to_le_bytes());
7593 expected.extend_from_slice(&1u16.to_le_bytes());
7594 expected.extend_from_slice(&0u16.to_le_bytes());
7595 expected.extend_from_slice(&[0xaau8; 32]);
7596 assert_eq!(serialized, expected);
7597 }
7598
7599 fn recipient_wrap_test_record() -> RecipientRecordV1 {
7600 RecipientRecordV1 {
7601 record_length: 0,
7602 profile_id: 1,
7603 recipient_identity_type: 2,
7604 flags: 0,
7605 recipient_identity_length: 0,
7606 profile_payload_length: 0,
7607 recipient_identity_digest: [0u8; 32],
7608 recipient_identity_bytes: b"recipient-a".to_vec(),
7609 profile_payload_bytes: b"profile-payload".to_vec(),
7610 }
7611 }
7612
7613 #[test]
7614 fn writer_options_reject_reader_resource_cap_excesses() {
7615 assert_eq!(
7616 plan_writer_options(WriterOptions {
7617 stripe_width: crate::format::READER_MAX_STRIPE_WIDTH + 1,
7618 volume_loss_tolerance: 0,
7619 ..WriterOptions::default()
7620 })
7621 .unwrap_err(),
7622 FormatError::ReaderResourceLimitExceeded {
7623 field: "stripe_width",
7624 cap: crate::format::READER_MAX_STRIPE_WIDTH as u64,
7625 actual: crate::format::READER_MAX_STRIPE_WIDTH as u64 + 1,
7626 }
7627 );
7628 assert_eq!(
7629 plan_writer_options(WriterOptions {
7630 block_size: crate::format::READER_MAX_BLOCK_SIZE + 2,
7631 ..WriterOptions::default()
7632 })
7633 .unwrap_err(),
7634 FormatError::ReaderResourceLimitExceeded {
7635 field: "block_size",
7636 cap: crate::format::READER_MAX_BLOCK_SIZE as u64,
7637 actual: crate::format::READER_MAX_BLOCK_SIZE as u64 + 2,
7638 }
7639 );
7640 assert_eq!(
7641 plan_writer_options(WriterOptions {
7642 chunk_size: crate::format::READER_MAX_CHUNK_SIZE + 1,
7643 envelope_target_size: crate::format::READER_MAX_CHUNK_SIZE + 1,
7644 ..WriterOptions::default()
7645 })
7646 .unwrap_err(),
7647 FormatError::ReaderResourceLimitExceeded {
7648 field: "chunk_size",
7649 cap: crate::format::READER_MAX_CHUNK_SIZE as u64,
7650 actual: crate::format::READER_MAX_CHUNK_SIZE as u64 + 1,
7651 }
7652 );
7653 assert_eq!(
7654 plan_writer_options(WriterOptions {
7655 max_path_length: crate::format::READER_MAX_PATH_LENGTH + 1,
7656 ..WriterOptions::default()
7657 })
7658 .unwrap_err(),
7659 FormatError::ReaderResourceLimitExceeded {
7660 field: "max_path_length",
7661 cap: crate::format::READER_MAX_PATH_LENGTH as u64,
7662 actual: crate::format::READER_MAX_PATH_LENGTH as u64 + 1,
7663 }
7664 );
7665 assert_eq!(
7666 plan_writer_options(WriterOptions {
7667 bit_rot_buffer_pct: 0,
7668 stripe_width: 1,
7669 volume_loss_tolerance: 0,
7670 fec_data_shards: crate::format::READER_MAX_FEC_CLASS_SHARDS as u16 + 1,
7671 ..WriterOptions::default()
7672 })
7673 .unwrap_err(),
7674 FormatError::ReaderResourceLimitExceeded {
7675 field: "fec_data_shards + fec_parity_shards",
7676 cap: crate::format::READER_MAX_FEC_CLASS_SHARDS as u64,
7677 actual: crate::format::READER_MAX_FEC_CLASS_SHARDS as u64 + 1,
7678 }
7679 );
7680 }
7681
7682 #[test]
7683 fn root_auth_writer_config_rejects_reader_cap_excess_before_authenticator() {
7684 let master_key = MasterKey::from_raw_key(&[7u8; 32]).unwrap();
7685 let mut authenticator_called = false;
7686 let err = write_archive_with_root_auth(
7687 &[RegularFile::new("signed.txt", b"payload")],
7688 &master_key,
7689 single_volume_metadata_test_options(),
7690 RootAuthWriterConfig {
7691 authenticator_id: 1,
7692 signer_identity_type: 1,
7693 signer_identity: b"signer",
7694 authenticator_value_length: READER_MAX_ROOT_AUTH_AUTHENTICATOR_VALUE_LEN + 1,
7695 },
7696 |_| {
7697 authenticator_called = true;
7698 Ok(Vec::new())
7699 },
7700 )
7701 .unwrap_err();
7702
7703 assert!(!authenticator_called);
7704 assert_eq!(
7705 err,
7706 FormatError::ReaderResourceLimitExceeded {
7707 field: "RootAuthFooterV1 authenticator value length",
7708 cap: READER_MAX_ROOT_AUTH_AUTHENTICATOR_VALUE_LEN as u64,
7709 actual: READER_MAX_ROOT_AUTH_AUTHENTICATOR_VALUE_LEN as u64 + 1,
7710 }
7711 );
7712 }
7713
7714 #[test]
7715 fn root_auth_writer_accepts_128_kib_authenticator_value() {
7716 let master_key = MasterKey::from_raw_key(&[8u8; 32]).unwrap();
7717 let authenticator_value = vec![0x5a; READER_MAX_ROOT_AUTH_AUTHENTICATOR_VALUE_LEN as usize];
7718 let expected_value = authenticator_value.clone();
7719 let archive = write_archive_with_root_auth(
7720 &[RegularFile::new("signed.txt", b"payload")],
7721 &master_key,
7722 single_volume_metadata_test_options(),
7723 RootAuthWriterConfig {
7724 authenticator_id: 0xcafe,
7725 signer_identity_type: 1,
7726 signer_identity: b"certificate-profile-signer",
7727 authenticator_value_length: READER_MAX_ROOT_AUTH_AUTHENTICATOR_VALUE_LEN,
7728 },
7729 |_| Ok(authenticator_value.clone()),
7730 )
7731 .unwrap();
7732
7733 let opened = open_archive(&archive.bytes, &master_key).unwrap();
7734 let footer = opened.root_auth_footer.as_ref().unwrap();
7735 assert_eq!(footer.authenticator_id, 0xcafe);
7736 assert_eq!(
7737 footer.authenticator_value.as_slice(),
7738 expected_value.as_slice()
7739 );
7740
7741 let verification = opened
7742 .verify_root_auth_with(|footer, _| {
7743 Ok(footer.authenticator_id == 0xcafe
7744 && footer.authenticator_value.as_slice() == expected_value.as_slice())
7745 })
7746 .unwrap();
7747 assert_eq!(verification.authenticator_id, 0xcafe);
7748 }
7749
7750 #[test]
7751 fn streaming_writer_sink_round_trips_archive() {
7752 let files = [
7753 RegularFile::new("alpha.txt", b"alpha"),
7754 RegularFile::new("nested/beta.txt", b"beta payload"),
7755 ];
7756 let master_key = MasterKey::from_raw_key(&[7u8; 32]).unwrap();
7757 let mut sink = MemoryArchiveSink::default();
7758
7759 let summary = write_archive_sources_to_sink(
7760 &files,
7761 &master_key,
7762 single_volume_metadata_test_options(),
7763 None,
7764 &KdfParams::Raw,
7765 None,
7766 None,
7767 &mut sink,
7768 )
7769 .unwrap();
7770
7771 assert_eq!(summary.volume_count, 1);
7772 let opened = crate::reader::open_archive(&sink.volumes[0], &master_key).unwrap();
7773 assert_eq!(
7774 opened.extract_file("nested/beta.txt").unwrap(),
7775 Some(b"beta payload".to_vec())
7776 );
7777 }
7778
7779 #[test]
7780 fn ordered_sink_writer_round_trips_recipientwrap_records() {
7781 let files = [RegularFile::new("wrapped.txt", b"recipient sink payload")];
7782 let master_key = MasterKey::from_raw_key(&[9u8; 32]).unwrap();
7783 let mut sink = MemoryArchiveSink::default();
7784
7785 let summary = write_archive_sources_to_sink_ordered_parallel_with_recipient_wrap_records(
7786 &files,
7787 &master_key,
7788 single_volume_metadata_test_options(),
7789 vec![recipient_wrap_test_record()],
7790 None,
7791 None,
7792 &mut sink,
7793 )
7794 .unwrap();
7795
7796 assert_eq!(summary.volume_count, 1);
7797 let opened =
7798 open_archive_with_recipient_wrap_resolver(&sink.volumes[0], |_| Ok(vec![master_key.0]))
7799 .unwrap();
7800 assert_eq!(
7801 opened.extract_file("wrapped.txt").unwrap(),
7802 Some(b"recipient sink payload".to_vec())
7803 );
7804 }
7805
7806 #[test]
7807 fn single_pass_sink_writer_round_trips_recipientwrap_records() {
7808 let files = [RegularFile::new(
7809 "wrapped.txt",
7810 b"recipient single-pass sink payload",
7811 )];
7812 let master_key = MasterKey::from_raw_key(&[9u8; 32]).unwrap();
7813 let mut sink = MemoryArchiveSink::default();
7814
7815 let summary = write_archive_sources_to_sink_single_pass_with_recipient_wrap_records(
7816 &files,
7817 &master_key,
7818 single_volume_metadata_test_options(),
7819 vec![recipient_wrap_test_record()],
7820 None,
7821 None,
7822 &mut sink,
7823 )
7824 .unwrap();
7825
7826 assert_eq!(summary.volume_count, 1);
7827 let opened =
7828 open_archive_with_recipient_wrap_resolver(&sink.volumes[0], |_| Ok(vec![master_key.0]))
7829 .unwrap();
7830 assert_eq!(
7831 opened.extract_file("wrapped.txt").unwrap(),
7832 Some(b"recipient single-pass sink payload".to_vec())
7833 );
7834 opened.verify().unwrap();
7835 }
7836
7837 #[test]
7838 fn single_pass_sink_writer_round_trips_recipientwrap_root_auth() {
7839 let files = [RegularFile::new(
7840 "wrapped.txt",
7841 b"recipient single-pass signed payload",
7842 )];
7843 let master_key = MasterKey::from_raw_key(&[9u8; 32]).unwrap();
7844 let mut sink = MemoryArchiveSink::default();
7845
7846 let summary = write_archive_sources_to_sink_single_pass_with_recipient_wrap_records(
7847 &files,
7848 &master_key,
7849 single_volume_metadata_test_options(),
7850 vec![recipient_wrap_test_record()],
7851 Some(RootAuthWriterConfig {
7852 authenticator_id: 0x44,
7853 signer_identity_type: 1,
7854 signer_identity: b"recipient-wrap-single-pass",
7855 authenticator_value_length: 32,
7856 }),
7857 Some(&mut |request| Ok(request.archive_root.to_vec())),
7858 &mut sink,
7859 )
7860 .unwrap();
7861
7862 assert_eq!(summary.volume_count, 1);
7863 let opened =
7864 open_archive_with_recipient_wrap_resolver(&sink.volumes[0], |_| Ok(vec![master_key.0]))
7865 .unwrap();
7866 let verification = opened
7867 .verify_root_auth_with(|footer, archive_root| {
7868 Ok(footer.authenticator_id == 0x44
7869 && footer.authenticator_value.as_slice() == archive_root)
7870 })
7871 .unwrap();
7872 assert_eq!(verification.volume_format_rev, VOLUME_FORMAT_REV_44);
7873 }
7874
7875 #[test]
7876 fn single_pass_writer_rejects_recipientwrap_kdf_without_records_before_writing() {
7877 let files = [RegularFile::new("wrapped.txt", b"recipient sink payload")];
7878 let master_key = MasterKey::from_raw_key(&[9u8; 32]).unwrap();
7879 let mut sink = MemoryArchiveSink::default();
7880 let kdf_params = KdfParams::RecipientWrap {
7881 key_wrap_table_length: 0,
7882 key_wrap_table_record_count: 1,
7883 key_wrap_table_version: 1,
7884 key_wrap_table_digest: [0u8; 32],
7885 };
7886
7887 let err = write_archive_sources_to_sink_single_pass(
7888 &files,
7889 &master_key,
7890 single_volume_metadata_test_options(),
7891 &kdf_params,
7892 None,
7893 None,
7894 &mut sink,
7895 )
7896 .unwrap_err();
7897
7898 match err {
7899 ArchiveWriteError::Format(FormatError::WriterUnsupported(message)) => {
7900 assert_eq!(message, "RecipientWrap requires key-wrap records");
7901 }
7902 other => panic!("unexpected error: {other:?}"),
7903 }
7904 assert!(sink.volumes.is_empty());
7905 }
7906
7907 #[test]
7908 fn ordered_sink_writer_rejects_recipientwrap_kdf_without_records_before_writing() {
7909 let files = [RegularFile::new("wrapped.txt", b"recipient sink payload")];
7910 let master_key = MasterKey::from_raw_key(&[9u8; 32]).unwrap();
7911 let mut sink = MemoryArchiveSink::default();
7912 let kdf_params = KdfParams::RecipientWrap {
7913 key_wrap_table_length: 0,
7914 key_wrap_table_record_count: 1,
7915 key_wrap_table_version: 1,
7916 key_wrap_table_digest: [0u8; 32],
7917 };
7918
7919 let err = write_archive_sources_to_sink_ordered_parallel(
7920 &files,
7921 &master_key,
7922 single_volume_metadata_test_options(),
7923 &kdf_params,
7924 None,
7925 None,
7926 &mut sink,
7927 )
7928 .unwrap_err();
7929
7930 match err {
7931 ArchiveWriteError::Format(FormatError::WriterUnsupported(message)) => {
7932 assert_eq!(message, "RecipientWrap requires key-wrap records");
7933 }
7934 other => panic!("unexpected error: {other:?}"),
7935 }
7936 assert!(sink.volumes.is_empty());
7937 }
7938
7939 #[test]
7940 fn streaming_writer_bounds_source_reads_and_sink_writes_for_large_file() {
7941 let file_size = 3 * 1024 * 1024;
7942 let stats = Rc::new(RefCell::new(GeneratedSourceStats::default()));
7943 let file = GeneratedFileSource {
7944 path: "large/generated.bin",
7945 len: file_size,
7946 stats: Rc::clone(&stats),
7947 };
7948 let master_key = MasterKey::from_raw_key(&[3u8; 32]).unwrap();
7949 let options = plan_writer_options(WriterOptions {
7950 block_size: MIN_BLOCK_SIZE,
7951 chunk_size: 16 * 1024,
7952 envelope_target_size: 64 * 1024,
7953 stripe_width: 1,
7954 volume_loss_tolerance: 0,
7955 bit_rot_buffer_pct: 0,
7956 fec_parity_shards: 0,
7957 index_fec_parity_shards: 0,
7958 index_root_fec_parity_shards: 0,
7959 ..WriterOptions::default()
7960 })
7961 .unwrap();
7962 let mut sink = TrackingArchiveSink::default();
7963
7964 let summary = write_archive_sources_to_sink_single_pass(
7965 &[file],
7966 &master_key,
7967 options,
7968 &KdfParams::Raw,
7969 None,
7970 None,
7971 &mut sink,
7972 )
7973 .unwrap();
7974
7975 let stats = stats.borrow();
7976 assert_eq!(stats.open_count, 1);
7977 assert_eq!(stats.total_read, file_size as u64);
7978 assert!(stats.max_read_request <= options.chunk_size as usize);
7979 assert_eq!(summary.volume_count, 1);
7980 assert_eq!(summary.archive_bytes, sink.volume_bytes.iter().sum());
7981 assert_eq!(
7982 summary.bootstrap_sidecar_bytes,
7983 sink.bootstrap_sidecar_bytes
7984 );
7985 assert!(sink.max_write_len <= 128 * 1024);
7986 }
7987
7988 #[test]
7989 fn sink_writer_progress_reports_source_bytes_during_multi_pass_write() {
7990 let file_size = 512 * 1024;
7991 let stats = Rc::new(RefCell::new(GeneratedSourceStats::default()));
7992 let file = GeneratedFileSource {
7993 path: "large/generated.bin",
7994 len: file_size,
7995 stats: Rc::clone(&stats),
7996 };
7997 let master_key = MasterKey::from_raw_key(&[4u8; 32]).unwrap();
7998 let options = WriterOptions {
7999 block_size: MIN_BLOCK_SIZE,
8000 chunk_size: 16 * 1024,
8001 envelope_target_size: 64 * 1024,
8002 stripe_width: 1,
8003 volume_loss_tolerance: 0,
8004 bit_rot_buffer_pct: 0,
8005 fec_parity_shards: 0,
8006 index_fec_parity_shards: 0,
8007 index_root_fec_parity_shards: 0,
8008 ..WriterOptions::default()
8009 };
8010 let mut sink = MemoryArchiveSink::default();
8011 let mut progress_events = Vec::new();
8012 let mut progress = |archive_path: &str, bytes: u64| {
8013 progress_events.push((archive_path.to_owned(), bytes));
8014 };
8015
8016 let summary = write_archive_sources_to_sink_with_progress(
8017 &[file],
8018 &master_key,
8019 options,
8020 None,
8021 &KdfParams::Raw,
8022 None,
8023 None,
8024 &mut sink,
8025 &mut progress,
8026 )
8027 .unwrap();
8028
8029 let reported_bytes = progress_events.iter().map(|(_, bytes)| *bytes).sum::<u64>();
8030 let stats = stats.borrow();
8031 assert!(stats.open_count > 1);
8032 assert_eq!(reported_bytes, file_size as u64);
8033 assert!(progress_events.len() > 1);
8034 assert!(progress_events
8035 .iter()
8036 .all(|(path, _)| path == "large/generated.bin"));
8037 assert_eq!(summary.volume_count, 1);
8038 assert!(!sink.volumes.is_empty());
8039 }
8040
8041 #[derive(Default)]
8042 struct GeneratedSourceStats {
8043 open_count: usize,
8044 total_read: u64,
8045 max_read_request: usize,
8046 }
8047
8048 struct GeneratedFileSource {
8049 path: &'static str,
8050 len: usize,
8051 stats: Rc<RefCell<GeneratedSourceStats>>,
8052 }
8053
8054 impl RegularFileSource for GeneratedFileSource {
8055 fn archive_path(&self) -> &str {
8056 self.path
8057 }
8058
8059 fn file_data_size(&self) -> u64 {
8060 self.len as u64
8061 }
8062
8063 fn mode(&self) -> u32 {
8064 0o644
8065 }
8066
8067 fn mtime(&self) -> u64 {
8068 0
8069 }
8070
8071 fn open(&self) -> Result<Box<dyn Read + '_>, ArchiveWriteError> {
8072 self.stats.borrow_mut().open_count += 1;
8073 Ok(Box::new(GeneratedReader {
8074 remaining: self.len,
8075 position: 0,
8076 stats: Rc::clone(&self.stats),
8077 }))
8078 }
8079 }
8080
8081 struct GeneratedReader {
8082 remaining: usize,
8083 position: usize,
8084 stats: Rc<RefCell<GeneratedSourceStats>>,
8085 }
8086
8087 impl Read for GeneratedReader {
8088 fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
8089 if self.remaining == 0 {
8090 return Ok(0);
8091 }
8092 let count = out.len().min(self.remaining);
8093 for (offset, byte) in out[..count].iter_mut().enumerate() {
8094 let position = self.position + offset;
8095 *byte = position.wrapping_mul(31).wrapping_add(17) as u8;
8096 }
8097 self.position += count;
8098 self.remaining -= count;
8099 let mut stats = self.stats.borrow_mut();
8100 stats.total_read += count as u64;
8101 stats.max_read_request = stats.max_read_request.max(out.len());
8102 Ok(count)
8103 }
8104 }
8105
8106 #[derive(Default)]
8107 struct TrackingArchiveSink {
8108 volume_bytes: Vec<u64>,
8109 bootstrap_sidecar_bytes: u64,
8110 max_write_len: usize,
8111 }
8112
8113 impl ArchiveWriteSink for TrackingArchiveSink {
8114 fn begin_archive(&mut self, volume_count: usize) -> Result<(), ArchiveWriteError> {
8115 self.volume_bytes = vec![0; volume_count];
8116 self.bootstrap_sidecar_bytes = 0;
8117 self.max_write_len = 0;
8118 Ok(())
8119 }
8120
8121 fn write_volume(
8122 &mut self,
8123 volume_index: usize,
8124 bytes: &[u8],
8125 ) -> Result<(), ArchiveWriteError> {
8126 let volume =
8127 self.volume_bytes
8128 .get_mut(volume_index)
8129 .ok_or(FormatError::WriterInvariant(
8130 "tracking sink volume index is out of bounds",
8131 ))?;
8132 *volume += bytes.len() as u64;
8133 self.max_write_len = self.max_write_len.max(bytes.len());
8134 Ok(())
8135 }
8136
8137 fn write_bootstrap_sidecar(&mut self, bytes: &[u8]) -> Result<(), ArchiveWriteError> {
8138 self.bootstrap_sidecar_bytes += bytes.len() as u64;
8139 self.max_write_len = self.max_write_len.max(bytes.len());
8140 Ok(())
8141 }
8142 }
8143
8144 fn deterministic_bytes(len: usize) -> Vec<u8> {
8145 let mut state = 0x4d41_4d45u32;
8146 let mut out = Vec::with_capacity(len);
8147 for _ in 0..len {
8148 state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
8149 out.push((state >> 24) as u8);
8150 }
8151 out
8152 }
8153
8154 fn single_volume_metadata_test_options() -> WriterOptions {
8155 plan_writer_options(WriterOptions {
8156 block_size: MIN_BLOCK_SIZE,
8157 stripe_width: 1,
8158 volume_loss_tolerance: 0,
8159 bit_rot_buffer_pct: 0,
8160 index_root_fec_parity_shards: 0,
8161 ..WriterOptions::default()
8162 })
8163 .unwrap()
8164 }
8165
8166 fn payload_len_for_encrypted_data_blocks(
8167 data_block_count: u32,
8168 options: WriterOptions,
8169 ) -> usize {
8170 assert!(data_block_count > 0);
8171 if data_block_count == 1 {
8172 return 1;
8173 }
8174 let block_size = options.block_size as usize;
8175 (data_block_count as usize - 1) * block_size - options.aead_algo.tag_len() + 1
8176 }
8177
8178 fn assert_path_specific_member_group(group: &[u8]) {
8179 let mut cursor = 0usize;
8180 let mut saw_main = false;
8181 while cursor < group.len() {
8182 let header = &group[cursor..cursor + TAR_BLOCK_LEN];
8183 assert!(
8184 header.iter().any(|byte| *byte != 0),
8185 "writer emitted tar zero block inside member group"
8186 );
8187 let typeflag = header[156];
8188 assert_ne!(typeflag, b'g', "writer emitted global PAX metadata");
8189 assert!(
8190 !matches!(typeflag, b'V' | b'M' | b'N'),
8191 "writer emitted global GNU metadata"
8192 );
8193 assert!(
8194 matches!(typeflag, b'x' | b'0'),
8195 "writer emitted unexpected tar record type {typeflag:?}"
8196 );
8197 if typeflag == b'0' {
8198 saw_main = true;
8199 }
8200 let size = read_test_tar_octal(&header[124..136]);
8201 cursor += TAR_BLOCK_LEN + size + padding_to_512(size);
8202 }
8203 assert_eq!(cursor, group.len());
8204 assert!(saw_main);
8205 }
8206
8207 fn read_test_tar_octal(field: &[u8]) -> usize {
8208 let mut value = 0usize;
8209 for byte in field {
8210 match *byte {
8211 0 | b' ' => break,
8212 b'0'..=b'7' => {
8213 value = value * 8 + usize::from(*byte - b'0');
8214 }
8215 other => panic!("malformed test tar octal byte {other:?}"),
8216 }
8217 }
8218 value
8219 }
8220
8221 fn test_file_row(idx: usize, path_hash: [u8; 8]) -> FileRow {
8222 let path = format!("file-{idx:05}.txt").into_bytes();
8223 FileRow {
8224 path_hash,
8225 path: path.clone(),
8226 member_index: idx,
8227 member: TarMember {
8228 path,
8229 tar_member_group_start: idx as u64 * 512,
8230 tar_member_group_size: 512,
8231 file_data_size: 0,
8232 kind: TarEntryKind::Regular,
8233 mode: 0o644,
8234 mtime: 0,
8235 },
8236 }
8237 }
8238}