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