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