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