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