Skip to main content

tzap_core/
writer.rs

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