Skip to main content

ewf_image/
writer.rs

1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::fs::{self, File};
4use std::io::{Read, Seek, SeekFrom, Write};
5use std::ops::Range;
6use std::path::{Component, Path, PathBuf};
7use std::sync::atomic::{AtomicBool, Ordering};
8
9use bzip2::Compression as Bzip2Compression;
10use bzip2::write::BzEncoder;
11use flate2::Compression as ZlibCompression;
12use flate2::write::ZlibEncoder;
13use md5::{Digest, Md5};
14use sha1::Sha1;
15use sha2::Sha256;
16use tempfile::NamedTempFile;
17
18use crate::codepage::encode_header_text;
19use crate::date_time::{
20    format_ewf1_header_date_value, format_ewf1_header2_date_value, format_xheader_date_value,
21};
22use crate::decode::{ChunkEncoding, decode_chunk, validate_encoded_size};
23use crate::format::{ewf1, ewf2};
24use crate::image::Image;
25use crate::types::{
26    AcquisitionError, CompressionLevel, CompressionMethod, CompressionValues, DataChunk,
27    DataChunkEncoding, EncodedDataChunk, EwfMetadata, Format, FormatProfile, HeaderCodepage,
28    HeaderDateFormat, ImageInfo, MediaFlags, MediaType, MemoryExtent, SectorRange,
29    SingleFileAttribute, SingleFileEntry, SingleFileEntryType, SingleFileExtent,
30    SingleFilePermission, SingleFilePermissionGroup, SingleFileSource, SingleFileSubject,
31    SingleFilesAuxTables, SingleFilesInfo, StoredHashes,
32};
33use crate::{EwfError, Result};
34
35mod acquisition;
36mod logical;
37mod sequential;
38pub use acquisition::{
39    AcquisitionCheckpoint, AcquisitionOperationPhase, AcquisitionOperationProgress,
40    AcquisitionOptions, AcquisitionOutcome, AcquisitionProgress, AcquisitionReadOptions,
41    AcquisitionStatus, AcquisitionWriter, UnreadableSectorPolicy,
42};
43pub use logical::{LogicalEntryMetadata, LogicalWriteProgress, LogicalWriter};
44pub use sequential::{SequentialOptions, SequentialWriter};
45
46const VOLUME_DATA_SIZE: usize = 1052;
47const EWF1_LTREE_HEADER_SIZE: usize = 48;
48const EWF2_DEVICE_INFORMATION_SECTION: u32 = 0x01;
49const EWF2_CASE_DATA_SECTION: u32 = 0x02;
50const EWF2_SECTOR_DATA_SECTION: u32 = 0x03;
51const EWF2_SECTOR_TABLE_SECTION: u32 = 0x04;
52const EWF2_ERROR_TABLE_SECTION: u32 = 0x05;
53const EWF2_SESSION_TABLE_SECTION: u32 = 0x06;
54const EWF2_INCREMENT_DATA_SECTION: u32 = 0x07;
55const EWF2_MD5_HASH_SECTION: u32 = 0x08;
56const EWF2_SHA1_HASH_SECTION: u32 = 0x09;
57const EWF2_RESTART_DATA_SECTION: u32 = 0x0a;
58const EWF2_MEMORY_EXTENTS_TABLE_SECTION: u32 = 0x0c;
59const EWF2_NEXT_SECTION: u32 = 0x0d;
60const EWF2_FINAL_INFORMATION_SECTION: u32 = 0x0e;
61const EWF2_DONE_SECTION: u32 = 0x0f;
62const EWF2_ANALYTICAL_DATA_SECTION: u32 = 0x10;
63const EWF2_SINGLE_FILES_DATA_SECTION: u32 = 0x20;
64const EWF2_SINGLE_FILES_TABLE_SECTION: u32 = 0x21;
65const EWF2_SINGLE_FILES_MD5_HASH_TABLE_SECTION: u32 = 0x22;
66const EWF2_SINGLE_FILES_UNKNOWN_TABLE_SECTION: u32 = 0x23;
67const EWF2_TABLE_HEADER_V2_SIZE: usize = 32;
68const EWF2_TABLE_FOOTER_SIZE: usize = 16;
69// EWF1 table entries hold 31-bit offsets relative to the table base offset, so
70// each sectors/table group can address at most 2 GiB of chunk payload. Large
71// non-segmented images are written as multiple groups per segment, matching
72// EnCase 6 and FTK Imager output.
73const EWF1_TABLE_GROUP_MAX_PAYLOAD: u64 = 0x7fff_ffff;
74const EWF1_TABLE_GROUP_MAX_ENTRIES: usize = 16_375;
75const SIGNED_SECTOR_RANGE_MAX: u64 = i64::MAX as u64;
76const EWF2_EXTENDED_ATTRIBUTES_HEADER: &[u8; 37] = &[
77    0x00, 0x00, 0x00, 0x00, 0x01, 0x0b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x41, 0x00, 0x74,
78    0x00, 0x74, 0x00, 0x72, 0x00, 0x69, 0x00, 0x62, 0x00, 0x75, 0x00, 0x74, 0x00, 0x65, 0x00, 0x73,
79    0x00, 0x00, 0x00, 0x00, 0x00,
80];
81
82const SINGLE_FILE_ENTRY_TYPES: &[&str] = &[
83    "id", "p", "n", "ls", "lo", "po", "du", "src", "sub", "pm", "cid", "opr", "be", "ha", "sha",
84    "snh", "cr", "wr", "ac", "mo", "dl", "ea",
85];
86const SINGLE_FILE_SOURCE_TYPES: &[&str] = &[
87    "id", "n", "ev", "loc", "gu", "pgu", "dt", "mfr", "mo", "se", "do", "ip", "ma", "tb", "lo",
88    "po", "aq", "ah", "sh",
89];
90const SINGLE_FILE_SUBJECT_TYPES: &[&str] = &["id", "n"];
91const SINGLE_FILE_PERMISSION_TYPES: &[&str] = &["n", "pr", "s", "nta", "nti"];
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94/// Configuration used to create an [`EwfWriter`].
95pub struct WriteOptions {
96    /// Output EWF format.
97    pub format: WriteFormat,
98    /// Number of sectors represented by each chunk.
99    pub sectors_per_chunk: u32,
100    /// Bytes in each logical sector.
101    pub bytes_per_sector: u32,
102    /// Segment set identifier written to output metadata.
103    pub set_identifier: Option<[u8; 16]>,
104    /// Compression method for newly encoded chunks.
105    pub compression: WriteCompression,
106    /// Compression level and flags for newly encoded chunks.
107    pub compression_values: WriteCompressionValues,
108    /// Stored hash values to write.
109    pub hashes: WriteHashes,
110    /// Maximum output segment size in bytes.
111    pub maximum_segment_size: Option<u64>,
112    /// Permit replacing existing segment files during file-backed finalization.
113    /// Defaults to false. Replacement uses a recoverable publication journal.
114    pub overwrite_existing: bool,
115    /// First path of the mirrored secondary output segment set.
116    pub secondary_segment_filename: Option<PathBuf>,
117    /// Declared logical media size in bytes.
118    pub media_size: Option<u64>,
119    /// Case and acquisition metadata to write.
120    pub metadata: EwfMetadata,
121    /// Codepage used to encode EWF1 header values.
122    pub header_codepage: HeaderCodepage,
123    /// Date format used for EWF1 header date values.
124    pub header_values_date_format: HeaderDateFormat,
125    /// Acquisition error ranges to write.
126    pub acquisition_errors: Vec<AcquisitionError>,
127    /// Checksum error ranges to write.
128    pub checksum_errors: Vec<SectorRange>,
129    /// Session sector ranges to write.
130    pub sessions: Vec<SectorRange>,
131    /// Track sector ranges to write.
132    pub tracks: Vec<SectorRange>,
133    /// Media type and acquisition flags to write.
134    pub media_profile: WriteMediaProfile,
135    /// Memory acquisition extents to write.
136    pub memory_extents: Vec<MemoryExtent>,
137    /// Logical single-file catalog to write.
138    pub single_files: Option<SingleFilesInfo>,
139    /// Preserved EWF2 single-file auxiliary table data.
140    pub ewf2_single_files_tables: SingleFilesAuxTables,
141    /// Raw EWF2 increment data sections to write.
142    pub ewf2_increment_data: Vec<Vec<u8>>,
143    /// Raw EWF2 final information section to write.
144    pub ewf2_final_information: Option<Vec<u8>>,
145    /// EWF2 restart data text to write.
146    pub ewf2_restart_data: Option<String>,
147    /// EWF2 analytical data text to write.
148    pub ewf2_analytical_data: Option<String>,
149}
150
151impl Default for WriteOptions {
152    fn default() -> Self {
153        Self {
154            format: WriteFormat::Ewf1Physical,
155            sectors_per_chunk: 64,
156            bytes_per_sector: 512,
157            set_identifier: None,
158            compression: WriteCompression::None,
159            compression_values: WriteCompressionValues::default(),
160            hashes: WriteHashes::default(),
161            maximum_segment_size: None,
162            overwrite_existing: false,
163            secondary_segment_filename: None,
164            media_size: None,
165            metadata: EwfMetadata::default(),
166            header_codepage: HeaderCodepage::Ascii,
167            header_values_date_format: HeaderDateFormat::Ctime,
168            acquisition_errors: Vec::new(),
169            checksum_errors: Vec::new(),
170            sessions: Vec::new(),
171            tracks: Vec::new(),
172            media_profile: WriteMediaProfile::default(),
173            memory_extents: Vec::new(),
174            single_files: None,
175            ewf2_single_files_tables: SingleFilesAuxTables::default(),
176            ewf2_increment_data: Vec::new(),
177            ewf2_final_information: None,
178            ewf2_restart_data: None,
179            ewf2_analytical_data: None,
180        }
181    }
182}
183
184impl WriteOptions {
185    /// Copies media geometry and flags from an opened image.
186    ///
187    /// # Errors
188    ///
189    /// Returns an error if the source image contains media values that cannot be
190    /// represented by writer options.
191    pub fn copy_media_values_from_image(&mut self, image: &Image) -> Result<()> {
192        self.copy_media_values_from_info(image.info())
193    }
194
195    /// Copies media geometry and flags from parsed image metadata.
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if the source metadata contains media values that cannot
200    /// be represented by writer options.
201    pub fn copy_media_values_from_info(&mut self, info: &ImageInfo) -> Result<()> {
202        let mut candidate = self.clone();
203        candidate.format = write_format_from_image_info(info);
204        candidate.sectors_per_chunk =
205            required_media_u32(info.media.sectors_per_chunk, "sectors per chunk")?;
206        candidate.bytes_per_sector =
207            required_media_u32(info.media.bytes_per_sector, "bytes per sector")?;
208        candidate.set_identifier = info.media.set_identifier;
209        candidate.media_size = Some(info.logical_size);
210        candidate.media_profile = WriteMediaProfile {
211            media_type: info.media.media_type,
212            error_granularity: info.media.error_granularity,
213            fastbloc: info.media.media_flags.fastbloc,
214            tableau: info.media.media_flags.tableau,
215        };
216        let has_explicit_compression_values =
217            info.media.compression_values != CompressionValues::default();
218        candidate.compression_values = if has_explicit_compression_values {
219            write_compression_values_from_media_values(info.media.compression_values)?
220        } else {
221            WriteCompressionValues::default()
222        };
223        if let Some(compression_method) = info.media.compression_method {
224            candidate.compression = write_compression_from_media_method(compression_method)?;
225        } else if has_explicit_compression_values
226            && candidate.compression_values.level != WriteCompressionLevel::None
227        {
228            candidate.compression = WriteCompression::Zlib;
229        } else {
230            candidate.compression = WriteCompression::None;
231        }
232        validate_options(&candidate)?;
233        *self = candidate;
234        Ok(())
235    }
236
237    /// Copies header values from an opened image.
238    pub fn copy_header_values_from_image(&mut self, image: &Image) {
239        self.copy_header_values_from_info(image.info());
240    }
241
242    /// Copies header values from parsed image metadata.
243    pub fn copy_header_values_from_info(&mut self, info: &ImageInfo) {
244        self.copy_header_values_from_metadata(&info.metadata);
245    }
246
247    /// Copies header values from metadata.
248    pub fn copy_header_values_from_metadata(&mut self, metadata: &EwfMetadata) {
249        self.metadata.copy_header_values_from(metadata);
250    }
251
252    /// Copies stored hash values from an opened image.
253    pub fn copy_hash_values_from_image(&mut self, image: &Image) {
254        self.copy_hash_values_from_info(image.info());
255    }
256
257    /// Copies stored hash values from parsed image metadata.
258    pub fn copy_hash_values_from_info(&mut self, info: &ImageInfo) {
259        self.copy_hash_values_from_stored_hashes(&info.stored_hashes);
260    }
261
262    /// Copies stored hash values from a hash collection.
263    pub fn copy_hash_values_from_stored_hashes(&mut self, hashes: &StoredHashes) {
264        self.hashes.copy_hash_values_from_stored_hashes(hashes);
265    }
266}
267
268#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
269/// Output EWF format selected for writing.
270pub enum WriteFormat {
271    /// EWF1 physical `.E01` output.
272    #[default]
273    Ewf1Physical,
274    /// EWF1 logical `.L01` output.
275    Ewf1Logical,
276    /// EWF1 SMART `.S01` output.
277    Ewf1Smart,
278    /// EWF2 physical `.Ex01` output.
279    Ewf2Physical,
280    /// EWF2 logical `.Lx01` output.
281    Ewf2Logical,
282}
283
284#[derive(Debug, Clone, Default, PartialEq, Eq)]
285/// Hash values configured for writer output.
286pub struct WriteHashes {
287    /// MD5 hash bytes.
288    pub md5: Option<[u8; 16]>,
289    /// SHA1 hash bytes.
290    pub sha1: Option<[u8; 20]>,
291    /// Hash strings keyed by hash identifier.
292    pub hash_values: BTreeMap<String, String>,
293}
294
295impl WriteHashes {
296    /// Copies stored hash values from an image hash collection.
297    pub fn copy_hash_values_from_stored_hashes(&mut self, hashes: &StoredHashes) {
298        self.md5 = hashes.md5;
299        self.sha1 = hashes.sha1;
300        self.hash_values = hashes.hash_values.clone();
301    }
302
303    /// Returns a hash string by identifier.
304    pub fn hash_value(&self, identifier: &str) -> Option<&str> {
305        self.hash_values.get(identifier).map(String::as_str)
306    }
307
308    /// Sets a hash string by identifier and returns the previous value.
309    ///
310    /// `MD5` and `SHA1` values are also parsed into typed byte arrays.
311    ///
312    /// # Errors
313    ///
314    /// Returns an error if an `MD5`, `SHA1`, or `SHA256` value is not valid hexadecimal of
315    /// the required length.
316    pub fn set_hash_value(
317        &mut self,
318        identifier: impl Into<String>,
319        value: impl Into<String>,
320    ) -> Result<Option<String>> {
321        let identifier = identifier.into();
322        let value = value.into();
323        if identifier.eq_ignore_ascii_case("SHA256") {
324            crate::hashes::validate_digest(&identifier, &value)?;
325        }
326        if identifier.eq_ignore_ascii_case("MD5") {
327            self.md5 = Some(parse_writer_hash_value("MD5", &value)?);
328        } else if identifier.eq_ignore_ascii_case("SHA1") {
329            self.sha1 = Some(parse_writer_hash_value("SHA1", &value)?);
330        }
331        Ok(self.hash_values.insert(identifier, value))
332    }
333
334    /// Returns the number of configured hash strings.
335    pub fn number_of_hash_values(&self) -> usize {
336        self.hash_values.len()
337    }
338
339    /// Returns a hash identifier by enumeration index.
340    pub fn hash_value_identifier(&self, index: usize) -> Option<&str> {
341        self.hash_values.keys().nth(index).map(String::as_str)
342    }
343}
344
345fn parse_writer_hash_value<const N: usize>(label: &str, value: &str) -> Result<[u8; N]> {
346    if value.len() != N * 2 {
347        return Err(EwfError::Unsupported(format!("invalid {label} hash value")));
348    }
349
350    let mut bytes = [0; N];
351    for (index, pair) in value.as_bytes().as_chunks::<2>().0.iter().enumerate() {
352        let high = writer_hash_nibble(pair[0])
353            .ok_or_else(|| EwfError::Unsupported(format!("invalid {label} hash value")))?;
354        let low = writer_hash_nibble(pair[1])
355            .ok_or_else(|| EwfError::Unsupported(format!("invalid {label} hash value")))?;
356        bytes[index] = (high << 4) | low;
357    }
358    Ok(bytes)
359}
360
361fn writer_hash_nibble(value: u8) -> Option<u8> {
362    match value {
363        b'0'..=b'9' => Some(value - b'0'),
364        b'a'..=b'f' => Some(value - b'a' + 10),
365        b'A'..=b'F' => Some(value - b'A' + 10),
366        _ => None,
367    }
368}
369
370#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
371/// Media type and acquisition flags configured for writer output.
372pub struct WriteMediaProfile {
373    /// Media type to write.
374    pub media_type: Option<MediaType>,
375    /// Error granularity in sectors.
376    pub error_granularity: Option<u64>,
377    /// `FastBloc` acquisition flag.
378    pub fastbloc: bool,
379    /// Tableau acquisition flag.
380    pub tableau: bool,
381}
382
383#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
384/// Compression method for writer output.
385pub enum WriteCompression {
386    /// Store chunks without compression.
387    #[default]
388    None,
389    /// Compress chunks with zlib.
390    Zlib,
391    /// Compress chunks with `BZip2`.
392    Bzip2,
393}
394
395#[derive(Debug, Clone, Copy, PartialEq, Eq)]
396/// Compression settings for writer output.
397pub struct WriteCompressionValues {
398    /// Compression level.
399    pub level: WriteCompressionLevel,
400    /// Whether empty-block compression flags should be written.
401    pub empty_block: bool,
402}
403
404impl Default for WriteCompressionValues {
405    fn default() -> Self {
406        Self {
407            level: WriteCompressionLevel::Default,
408            empty_block: true,
409        }
410    }
411}
412
413#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
414/// Compression level for writer output.
415pub enum WriteCompressionLevel {
416    /// Use the compressor default level.
417    #[default]
418    Default,
419    /// Do not compress.
420    None,
421    /// Prefer faster compression.
422    Fast,
423    /// Prefer stronger compression.
424    Best,
425}
426
427#[derive(Debug, Clone, PartialEq, Eq)]
428/// Result returned after finalizing writer output.
429pub struct WriteResult {
430    /// Output segment paths or labels.
431    pub segment_paths: Vec<PathBuf>,
432    /// Mirrored secondary output segment paths.
433    pub secondary_segment_paths: Vec<PathBuf>,
434    /// Logical media size written in bytes.
435    pub logical_size: u64,
436    /// Logical chunk size written in bytes.
437    pub chunk_size: u64,
438    /// Number of logical chunks written.
439    pub chunk_count: u64,
440    /// SHA256 of the written logical media, including sector padding.
441    /// EWF1 complete output also embeds it unless a reference was supplied.
442    /// EWF2 has no SHA256 section here; retain this value externally.
443    pub computed_sha256: [u8; 32],
444}
445
446/// Incremental EWF writer.
447///
448/// The writer buffers media data until [`EwfWriter::finish`] or another finish
449/// method is called. Geometry and format options must be configured before
450/// media data is written.
451pub struct EwfWriter {
452    path: PathBuf,
453    options: WriteOptions,
454    chunk_size: u64,
455    chunk_capacity: usize,
456    raw: RawSpool,
457    encoded_chunks: BTreeMap<u64, RememberedEncodedChunk>,
458    current_offset: u64,
459    logical_input_size: u64,
460    abort_signaled: AtomicBool,
461}
462
463struct PreparedWrite {
464    path: PathBuf,
465    options: WriteOptions,
466    chunks: Vec<ChunkDescriptor>,
467    spool: ChunkSpool,
468    logical_size: u64,
469    chunk_size: u64,
470    chunk_count: u64,
471    chunk_count_u32: u32,
472    sector_count: u64,
473    computed_sha256: [u8; 32],
474}
475
476#[derive(Debug, Clone, Copy, PartialEq, Eq)]
477enum TerminalSection {
478    Done,
479    Next,
480}
481
482impl TerminalSection {
483    fn ewf1_type(self) -> &'static [u8; 4] {
484        match self {
485            Self::Done => b"done",
486            Self::Next => b"next",
487        }
488    }
489
490    fn ewf2_type(self) -> u32 {
491        match self {
492            Self::Done => EWF2_DONE_SECTION,
493            Self::Next => EWF2_NEXT_SECTION,
494        }
495    }
496}
497
498impl std::fmt::Debug for EwfWriter {
499    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
500        formatter
501            .debug_struct("EwfWriter")
502            .field("path", &self.path)
503            .field("options", &self.options)
504            .field("chunk_size", &self.chunk_size)
505            .field("chunk_capacity", &self.chunk_capacity)
506            .field("raw", &"<temporary>")
507            .field("raw_spooled_bytes", &self.raw.len())
508            .field("encoded_chunks", &self.encoded_chunks.len())
509            .field("current_offset", &self.current_offset)
510            .field("logical_input_size", &self.logical_input_size)
511            .field(
512                "abort_signaled",
513                &self.abort_signaled.load(Ordering::Relaxed),
514            )
515            .finish()
516    }
517}
518
519impl EwfWriter {
520    /// Creates a new writer for a file-backed EWF segment set.
521    ///
522    /// # Errors
523    ///
524    /// Returns an error if options are invalid, the secondary segment filename
525    /// conflicts with the primary filename, or temporary spool creation fails.
526    pub fn create(path: impl AsRef<Path>, mut options: WriteOptions) -> Result<Self> {
527        let path = path.as_ref().to_path_buf();
528        options.maximum_segment_size = normalize_maximum_segment_size(options.maximum_segment_size);
529        options.media_size = normalize_media_size(options.media_size);
530        validate_options(&options)?;
531        validate_secondary_segment_filename(&path, options.secondary_segment_filename.as_deref())?;
532        let (chunk_size, chunk_capacity) =
533            writer_chunk_geometry(options.sectors_per_chunk, options.bytes_per_sector)?;
534
535        Ok(Self {
536            path,
537            options,
538            chunk_size,
539            chunk_capacity,
540            raw: RawSpool::new()?,
541            encoded_chunks: BTreeMap::new(),
542            current_offset: 0,
543            logical_input_size: 0,
544            abort_signaled: AtomicBool::new(false),
545        })
546    }
547
548    /// Creates a writer initialized from an existing image and copies its media.
549    ///
550    /// # Errors
551    ///
552    /// Returns an error if the source image cannot be represented by the writer,
553    /// media copying fails, or writer creation fails.
554    pub fn create_from_image(path: impl AsRef<Path>, image: &Image) -> Result<Self> {
555        let options = rewrite_options_from_image_info(image.info())?;
556        let mut writer = Self::create(path, options)?;
557        copy_image_media_to_writer(image, &mut writer)?;
558        Ok(writer)
559    }
560
561    /// Opens an existing image and prepares a writer that can append or rewrite it.
562    ///
563    /// # Errors
564    ///
565    /// Returns an error if the existing image cannot be opened, copied, or
566    /// represented by writer options.
567    pub fn resume<P: AsRef<Path>>(path: P) -> Result<Self> {
568        let path = path.as_ref().to_path_buf();
569        let image = Image::open(&path)?;
570        let mut writer = Self::create_from_image(&path, &image)?;
571        writer.options.media_size = None;
572        writer.options.overwrite_existing = true;
573        Ok(writer)
574    }
575
576    /// Returns the first output segment filename.
577    pub fn filename(&self) -> &Path {
578        self.path.as_path()
579    }
580
581    /// Returns the first output segment filename.
582    pub fn segment_filename(&self) -> &Path {
583        self.path.as_path()
584    }
585
586    /// Sets the first output segment filename.
587    pub fn set_segment_filename(&mut self, path: impl AsRef<Path>) {
588        self.path = path.as_ref().to_path_buf();
589    }
590
591    /// Returns the first mirrored secondary output segment filename.
592    pub fn secondary_segment_filename(&self) -> Option<&Path> {
593        self.options.secondary_segment_filename.as_deref()
594    }
595
596    /// Sets the first mirrored secondary output segment filename.
597    ///
598    /// # Errors
599    ///
600    /// Returns an error if the secondary filename conflicts with the primary
601    /// output filename.
602    pub fn set_secondary_segment_filename(&mut self, path: impl AsRef<Path>) -> Result<()> {
603        let path = path.as_ref().to_path_buf();
604        validate_secondary_segment_filename(&self.path, Some(&path))?;
605        self.options.secondary_segment_filename = Some(path);
606        Ok(())
607    }
608
609    /// Clears mirrored secondary output.
610    pub fn clear_secondary_segment_filename(&mut self) {
611        self.options.secondary_segment_filename = None;
612    }
613
614    /// Writes all bytes at the current writer position.
615    ///
616    /// # Errors
617    ///
618    /// Returns an error if the writer has been aborted or writing would exceed
619    /// configured limits.
620    pub fn write_all(&mut self, data: &[u8]) -> Result<()> {
621        self.ensure_not_aborted()?;
622        self.write_to_current(data)?;
623        Ok(())
624    }
625
626    /// Writes bytes at the current writer position and returns the byte count.
627    ///
628    /// # Errors
629    ///
630    /// Returns an error if the writer has been aborted or writing would exceed
631    /// configured limits.
632    pub fn write_buffer(&mut self, data: &[u8]) -> Result<usize> {
633        self.ensure_not_aborted()?;
634        self.write_to_current(data)
635    }
636
637    /// Writes bytes at an absolute logical byte offset.
638    ///
639    /// # Errors
640    ///
641    /// Returns an error if the writer has been aborted or writing would exceed
642    /// configured limits.
643    pub fn write_at(&mut self, data: &[u8], offset: u64) -> Result<usize> {
644        self.ensure_not_aborted()?;
645        self.current_offset = offset;
646        self.write_to_current(data)
647    }
648
649    /// Alias for [`EwfWriter::write_at`].
650    ///
651    /// # Errors
652    ///
653    /// Returns the same errors as [`EwfWriter::write_at`].
654    pub fn write_buffer_at_offset(&mut self, data: &[u8], offset: u64) -> Result<usize> {
655        self.write_at(data, offset)
656    }
657
658    /// Writes a decoded data chunk at the current writer position.
659    ///
660    /// # Errors
661    ///
662    /// Returns an error if the chunk metadata is inconsistent, the writer has
663    /// been aborted, or writing would exceed configured limits.
664    pub fn write_data_chunk(&mut self, chunk: &DataChunk) -> Result<usize> {
665        self.ensure_not_aborted()?;
666        validate_write_data_chunk(chunk)?;
667        self.write_to_current(&chunk.data)
668    }
669
670    /// Decodes and writes an encoded data chunk at the current writer position.
671    ///
672    /// # Errors
673    ///
674    /// Returns an error if the encoded chunk cannot be decoded, the writer has
675    /// been aborted, or writing would exceed configured limits.
676    pub fn write_encoded_data_chunk(&mut self, chunk: &EncodedDataChunk) -> Result<usize> {
677        self.ensure_not_aborted()?;
678        let data = decode_write_encoded_data_chunk(chunk)?;
679        let offset = self.current_offset;
680        let written = self.write_to_current(&data)?;
681        self.remember_encoded_data_chunk_at(chunk, offset, data.len());
682        Ok(written)
683    }
684
685    /// Writes a decoded data chunk at an absolute logical byte offset.
686    ///
687    /// # Errors
688    ///
689    /// Returns the same errors as [`EwfWriter::write_data_chunk`] and
690    /// [`EwfWriter::write_at`].
691    pub fn write_data_chunk_at(&mut self, chunk: &DataChunk, offset: u64) -> Result<usize> {
692        self.ensure_not_aborted()?;
693        validate_write_data_chunk(chunk)?;
694        self.write_at(&chunk.data, offset)
695    }
696
697    /// Decodes and writes an encoded data chunk at an absolute logical byte offset.
698    ///
699    /// # Errors
700    ///
701    /// Returns the same errors as [`EwfWriter::write_encoded_data_chunk`] and
702    /// [`EwfWriter::write_at`].
703    pub fn write_encoded_data_chunk_at(
704        &mut self,
705        chunk: &EncodedDataChunk,
706        offset: u64,
707    ) -> Result<usize> {
708        self.ensure_not_aborted()?;
709        let data = decode_write_encoded_data_chunk(chunk)?;
710        self.current_offset = offset;
711        let written = self.write_to_current(&data)?;
712        self.remember_encoded_data_chunk_at(chunk, offset, data.len());
713        Ok(written)
714    }
715
716    /// Returns the configured output format.
717    pub fn format(&self) -> WriteFormat {
718        self.options.format
719    }
720
721    /// Sets the output format before media data is written.
722    ///
723    /// # Errors
724    ///
725    /// Returns an error if media data has already been written or the new option
726    /// combination is unsupported.
727    pub fn set_format(&mut self, format: WriteFormat) -> Result<()> {
728        self.ensure_configuration_mutable("format")?;
729        let mut options = self.options.clone();
730        options.format = format;
731        validate_options(&options)?;
732        self.options = options;
733        Ok(())
734    }
735
736    /// Returns the current logical byte write position.
737    pub fn position(&self) -> u64 {
738        self.current_offset
739    }
740
741    /// Returns the current logical chunk size in bytes.
742    pub fn chunk_size(&self) -> u64 {
743        self.chunk_size
744    }
745
746    /// Returns the configured sectors per chunk.
747    pub fn sectors_per_chunk(&self) -> u32 {
748        self.options.sectors_per_chunk
749    }
750
751    /// Sets sectors per chunk before media data is written.
752    ///
753    /// # Errors
754    ///
755    /// Returns an error if media data has already been written or the resulting
756    /// chunk geometry is invalid.
757    pub fn set_sectors_per_chunk(&mut self, sectors_per_chunk: u32) -> Result<()> {
758        self.ensure_configuration_mutable("sectors per chunk")?;
759        let (chunk_size, chunk_capacity) =
760            writer_chunk_geometry(sectors_per_chunk, self.options.bytes_per_sector)?;
761        let mut options = self.options.clone();
762        options.sectors_per_chunk = sectors_per_chunk;
763        validate_options(&options)?;
764        self.options = options;
765        self.chunk_size = chunk_size;
766        self.chunk_capacity = chunk_capacity;
767        self.encoded_chunks.clear();
768        Ok(())
769    }
770
771    /// Returns the configured bytes per sector.
772    pub fn bytes_per_sector(&self) -> u32 {
773        self.options.bytes_per_sector
774    }
775
776    /// Sets bytes per sector before media data is written.
777    ///
778    /// # Errors
779    ///
780    /// Returns an error if media data has already been written or the resulting
781    /// chunk geometry is invalid.
782    pub fn set_bytes_per_sector(&mut self, bytes_per_sector: u32) -> Result<()> {
783        self.ensure_configuration_mutable("bytes per sector")?;
784        let (chunk_size, chunk_capacity) =
785            writer_chunk_geometry(self.options.sectors_per_chunk, bytes_per_sector)?;
786        let mut options = self.options.clone();
787        options.bytes_per_sector = bytes_per_sector;
788        validate_options(&options)?;
789        self.options = options;
790        self.chunk_size = chunk_size;
791        self.chunk_capacity = chunk_capacity;
792        self.encoded_chunks.clear();
793        Ok(())
794    }
795
796    /// Returns the logical size that would be written after sector padding.
797    ///
798    /// # Errors
799    ///
800    /// Returns an error if configured sizes overflow or are inconsistent.
801    pub fn logical_size(&self) -> Result<u64> {
802        padded_logical_size(
803            target_media_input_size(self.logical_input_size, self.options.media_size)?,
804            self.options.bytes_per_sector,
805        )
806    }
807
808    /// Alias for [`EwfWriter::logical_size`].
809    ///
810    /// # Errors
811    ///
812    /// Returns the same errors as [`EwfWriter::logical_size`].
813    pub fn media_size(&self) -> Result<u64> {
814        self.logical_size()
815    }
816
817    /// Returns the logical sector count that would be written.
818    ///
819    /// # Errors
820    ///
821    /// Returns an error if the logical size cannot be computed.
822    pub fn number_of_sectors(&self) -> Result<u64> {
823        Ok(self.media_size()? / u64::from(self.options.bytes_per_sector))
824    }
825
826    /// Returns the configured maximum segment size in bytes.
827    pub fn maximum_segment_size(&self) -> Option<u64> {
828        self.options.maximum_segment_size
829    }
830
831    /// Sets the maximum segment size before media data is written.
832    ///
833    /// # Errors
834    ///
835    /// Returns an error if media data has already been written or the value is
836    /// invalid for the current output format.
837    pub fn set_maximum_segment_size(&mut self, maximum_segment_size: Option<u64>) -> Result<()> {
838        self.ensure_configuration_mutable("maximum segment size")?;
839        let mut options = self.options.clone();
840        options.maximum_segment_size = normalize_maximum_segment_size(maximum_segment_size);
841        validate_options(&options)?;
842        self.options = options;
843        Ok(())
844    }
845
846    /// Sets the declared logical media size before media data is written.
847    ///
848    /// # Errors
849    ///
850    /// Returns an error if media data has already been written or the requested
851    /// size is smaller than data already written.
852    pub fn set_media_size(&mut self, media_size: u64) -> Result<()> {
853        self.ensure_configuration_mutable("media size")?;
854        let media_size = normalize_media_size(Some(media_size));
855        if media_size.is_some_and(|media_size| self.logical_input_size > media_size) {
856            return Err(EwfError::Unsupported(
857                "writer configured media size is smaller than written data".into(),
858            ));
859        }
860        self.options.media_size = media_size;
861        Ok(())
862    }
863
864    /// Returns the number of logical chunks that would be written.
865    ///
866    /// # Errors
867    ///
868    /// Returns an error if the logical size cannot be computed.
869    pub fn number_of_chunks_written(&self) -> Result<u64> {
870        Ok(self.logical_size()?.div_ceil(self.chunk_size))
871    }
872
873    /// Returns the configured segment set identifier.
874    pub fn segment_file_set_identifier(&self) -> Option<[u8; 16]> {
875        self.options.set_identifier
876    }
877
878    /// Signals future writer operations to abort with [`EwfError::Aborted`].
879    pub fn signal_abort(&self) {
880        self.abort_signaled.store(true, Ordering::Relaxed);
881    }
882
883    /// Sets the segment set identifier before media data is written.
884    ///
885    /// # Errors
886    ///
887    /// Returns an error if media data has already been written.
888    pub fn set_segment_file_set_identifier(&mut self, set_identifier: [u8; 16]) -> Result<()> {
889        self.ensure_configuration_mutable("set identifier")?;
890        self.options.set_identifier = Some(set_identifier);
891        Ok(())
892    }
893
894    /// Returns the configured compression method.
895    pub fn compression_method(&self) -> WriteCompression {
896        self.options.compression
897    }
898
899    /// Sets the compression method before media data is written.
900    ///
901    /// # Errors
902    ///
903    /// Returns an error if media data has already been written or `BZip2` is
904    /// selected for an EWF1 output format.
905    pub fn set_compression_method(&mut self, compression: WriteCompression) -> Result<()> {
906        self.ensure_configuration_mutable("compression method")?;
907        if matches!(compression, WriteCompression::Bzip2) && !is_ewf2_format(self.options.format) {
908            return Err(EwfError::Unsupported(
909                "BZip2 writer compression is only supported for EWF2".into(),
910            ));
911        }
912        self.options.compression = compression;
913        Ok(())
914    }
915
916    /// Returns the configured compression values.
917    pub fn compression_values(&self) -> WriteCompressionValues {
918        self.options.compression_values
919    }
920
921    /// Sets compression values before media data is written.
922    ///
923    /// # Errors
924    ///
925    /// Returns an error if media data has already been written.
926    pub fn set_compression_values(
927        &mut self,
928        compression_values: WriteCompressionValues,
929    ) -> Result<()> {
930        self.ensure_configuration_mutable("compression values")?;
931        self.options.compression_values = compression_values;
932        Ok(())
933    }
934
935    /// Returns the configured media type.
936    pub fn media_type(&self) -> Option<MediaType> {
937        self.options.media_profile.media_type
938    }
939
940    /// Sets the media type before media data is written.
941    ///
942    /// # Errors
943    ///
944    /// Returns an error if media data has already been written.
945    pub fn set_media_type(&mut self, media_type: Option<MediaType>) -> Result<()> {
946        self.ensure_configuration_mutable("media type")?;
947        self.options.media_profile.media_type = media_type;
948        Ok(())
949    }
950
951    /// Returns the configured error granularity in sectors.
952    pub fn error_granularity(&self) -> Option<u64> {
953        self.options.media_profile.error_granularity
954    }
955
956    /// Sets error granularity before media data is written.
957    ///
958    /// # Errors
959    ///
960    /// Returns an error if media data has already been written.
961    pub fn set_error_granularity(&mut self, error_granularity: Option<u64>) -> Result<()> {
962        self.ensure_configuration_mutable("error granularity")?;
963        self.options.media_profile.error_granularity = error_granularity;
964        Ok(())
965    }
966
967    /// Returns media flags derived from format and media profile options.
968    pub fn media_flags(&self) -> MediaFlags {
969        writer_media_flags(self.options.format, self.options.media_profile)
970    }
971
972    /// Sets writable media flags before media data is written.
973    ///
974    /// The physical flag is controlled by [`WriteFormat`] and cannot be changed
975    /// independently.
976    ///
977    /// # Errors
978    ///
979    /// Returns an error if media data has already been written or if the
980    /// physical flag conflicts with the output format.
981    pub fn set_media_flags(&mut self, media_flags: MediaFlags) -> Result<()> {
982        self.ensure_configuration_mutable("media flags")?;
983        if media_flags.physical != write_format_is_physical(self.options.format) {
984            return Err(EwfError::Unsupported(
985                "writer physical media flag is controlled by image format".into(),
986            ));
987        }
988        self.options.media_profile.fastbloc = media_flags.fastbloc;
989        self.options.media_profile.tableau = media_flags.tableau;
990        Ok(())
991    }
992
993    /// Copies media geometry and flags from an opened image.
994    ///
995    /// # Errors
996    ///
997    /// Returns an error if the source values cannot be represented or conflict
998    /// with media data already written.
999    pub fn copy_media_values_from_image(&mut self, image: &Image) -> Result<()> {
1000        self.copy_media_values_from_info(image.info())
1001    }
1002
1003    /// Copies media geometry and flags from parsed image metadata.
1004    ///
1005    /// # Errors
1006    ///
1007    /// Returns an error if the source values cannot be represented or conflict
1008    /// with media data already written.
1009    pub fn copy_media_values_from_info(&mut self, info: &ImageInfo) -> Result<()> {
1010        let mut options = self.options.clone();
1011        options.copy_media_values_from_info(info)?;
1012        if options
1013            .media_size
1014            .is_some_and(|media_size| self.logical_input_size > media_size)
1015        {
1016            return Err(EwfError::Unsupported(
1017                "writer configured media size is smaller than written data".into(),
1018            ));
1019        }
1020        let (chunk_size, chunk_capacity) =
1021            writer_chunk_geometry(options.sectors_per_chunk, options.bytes_per_sector)?;
1022        self.options = options;
1023        self.chunk_size = chunk_size;
1024        self.chunk_capacity = chunk_capacity;
1025        Ok(())
1026    }
1027
1028    /// Returns a configured header value by EWF identifier.
1029    pub fn header_value(&self, identifier: &str) -> Option<std::borrow::Cow<'_, str>> {
1030        self.options
1031            .metadata
1032            .header_value_with_date_format(identifier, self.options.header_values_date_format)
1033    }
1034
1035    /// Returns the configured header codepage.
1036    pub fn header_codepage(&self) -> HeaderCodepage {
1037        self.options.header_codepage
1038    }
1039
1040    /// Sets the header codepage.
1041    pub fn set_header_codepage(&mut self, header_codepage: HeaderCodepage) {
1042        self.options.header_codepage = header_codepage;
1043    }
1044
1045    /// Returns the configured header date format.
1046    pub fn header_values_date_format(&self) -> HeaderDateFormat {
1047        self.options.header_values_date_format
1048    }
1049
1050    /// Sets the header date format.
1051    pub fn set_header_values_date_format(&mut self, date_format: HeaderDateFormat) {
1052        self.options.header_values_date_format = date_format;
1053    }
1054
1055    /// Copies header values from an opened image.
1056    pub fn copy_header_values_from_image(&mut self, image: &Image) {
1057        self.copy_header_values_from_info(image.info());
1058    }
1059
1060    /// Copies header values from parsed image metadata.
1061    pub fn copy_header_values_from_info(&mut self, info: &ImageInfo) {
1062        self.copy_header_values_from_metadata(&info.metadata);
1063    }
1064
1065    /// Copies header values from metadata.
1066    pub fn copy_header_values_from_metadata(&mut self, metadata: &EwfMetadata) {
1067        self.options.copy_header_values_from_metadata(metadata);
1068    }
1069
1070    /// Copies stored hash values from an opened image.
1071    pub fn copy_hash_values_from_image(&mut self, image: &Image) {
1072        self.copy_hash_values_from_info(image.info());
1073    }
1074
1075    /// Copies stored hash values from parsed image metadata.
1076    pub fn copy_hash_values_from_info(&mut self, info: &ImageInfo) {
1077        self.copy_hash_values_from_stored_hashes(&info.stored_hashes);
1078    }
1079
1080    /// Copies stored hash values from a hash collection.
1081    pub fn copy_hash_values_from_stored_hashes(&mut self, hashes: &StoredHashes) {
1082        self.options.copy_hash_values_from_stored_hashes(hashes);
1083    }
1084
1085    /// Sets a header value by EWF identifier and returns the previous value.
1086    pub fn set_header_value(
1087        &mut self,
1088        identifier: &str,
1089        value: impl Into<String>,
1090    ) -> Option<String> {
1091        self.options.metadata.set_header_value(identifier, value)
1092    }
1093
1094    /// Returns the number of configured header values.
1095    pub fn number_of_header_values(&self) -> usize {
1096        self.options.metadata.number_of_header_values()
1097    }
1098
1099    /// Returns a configured header value identifier by enumeration index.
1100    pub fn header_value_identifier(&self, index: usize) -> Option<&str> {
1101        self.options.metadata.header_value_identifier(index)
1102    }
1103
1104    /// Returns a configured hash string by identifier.
1105    pub fn hash_value(&self, identifier: &str) -> Option<&str> {
1106        self.options.hashes.hash_value(identifier)
1107    }
1108
1109    /// Sets a configured hash string by identifier and returns the previous value.
1110    ///
1111    /// # Errors
1112    ///
1113    /// Returns an error if an `MD5`, `SHA1`, or `SHA256` value is not valid hexadecimal of
1114    /// the required length.
1115    pub fn set_hash_value(
1116        &mut self,
1117        identifier: impl Into<String>,
1118        value: impl Into<String>,
1119    ) -> Result<Option<String>> {
1120        self.options.hashes.set_hash_value(identifier, value)
1121    }
1122
1123    /// Returns the number of configured hash strings.
1124    pub fn number_of_hash_values(&self) -> usize {
1125        self.options.hashes.number_of_hash_values()
1126    }
1127
1128    /// Returns a configured hash identifier by enumeration index.
1129    pub fn hash_value_identifier(&self, index: usize) -> Option<&str> {
1130        self.options.hashes.hash_value_identifier(index)
1131    }
1132
1133    /// Returns the configured MD5 hash bytes.
1134    pub fn md5_hash(&self) -> Option<[u8; 16]> {
1135        self.options.hashes.md5
1136    }
1137
1138    /// Sets the configured MD5 hash bytes.
1139    ///
1140    /// # Errors
1141    ///
1142    /// Returns an error if an MD5 hash is already configured.
1143    pub fn set_md5_hash(&mut self, md5: [u8; 16]) -> Result<()> {
1144        if self.options.hashes.md5.is_some() {
1145            return Err(EwfError::Unsupported("MD5 hash cannot be changed".into()));
1146        }
1147        self.options.hashes.md5 = Some(md5);
1148        self.options
1149            .hashes
1150            .hash_values
1151            .entry("MD5".to_string())
1152            .or_insert_with(|| hex_string(&md5));
1153        Ok(())
1154    }
1155
1156    /// Returns the configured SHA1 hash bytes.
1157    pub fn sha1_hash(&self) -> Option<[u8; 20]> {
1158        self.options.hashes.sha1
1159    }
1160
1161    /// Sets the configured SHA1 hash bytes.
1162    ///
1163    /// # Errors
1164    ///
1165    /// Returns an error if a SHA1 hash is already configured.
1166    pub fn set_sha1_hash(&mut self, sha1: [u8; 20]) -> Result<()> {
1167        if self.options.hashes.sha1.is_some() {
1168            return Err(EwfError::Unsupported("SHA1 hash cannot be changed".into()));
1169        }
1170        self.options.hashes.sha1 = Some(sha1);
1171        self.options
1172            .hashes
1173            .hash_values
1174            .entry("SHA1".to_string())
1175            .or_insert_with(|| hex_string(&sha1));
1176        Ok(())
1177    }
1178
1179    /// Returns configured acquisition error ranges.
1180    pub fn acquisition_errors(&self) -> &[AcquisitionError] {
1181        &self.options.acquisition_errors
1182    }
1183
1184    /// Returns the number of configured acquisition error ranges.
1185    pub fn number_of_acquisition_errors(&self) -> usize {
1186        self.options.acquisition_errors.len()
1187    }
1188
1189    /// Returns a configured acquisition error range by index.
1190    pub fn acquisition_error(&self, index: usize) -> Option<&AcquisitionError> {
1191        self.options.acquisition_errors.get(index)
1192    }
1193
1194    /// Appends an acquisition error range.
1195    ///
1196    /// # Errors
1197    ///
1198    /// This method currently validates no additional constraints and returns
1199    /// `Ok(())` after appending.
1200    pub fn append_acquisition_error(&mut self, first_sector: u64, sector_count: u64) -> Result<()> {
1201        self.options.acquisition_errors.push(AcquisitionError {
1202            first_sector,
1203            sector_count,
1204        });
1205        Ok(())
1206    }
1207
1208    /// Returns configured checksum error ranges.
1209    pub fn checksum_errors(&self) -> &[SectorRange] {
1210        &self.options.checksum_errors
1211    }
1212
1213    /// Returns the number of configured checksum error ranges.
1214    pub fn number_of_checksum_errors(&self) -> usize {
1215        self.options.checksum_errors.len()
1216    }
1217
1218    /// Returns a configured checksum error range by index.
1219    pub fn checksum_error(&self, index: usize) -> Option<&SectorRange> {
1220        self.options.checksum_errors.get(index)
1221    }
1222
1223    /// Appends a checksum error range.
1224    ///
1225    /// # Errors
1226    ///
1227    /// This method currently validates no additional constraints and returns
1228    /// `Ok(())` after appending.
1229    pub fn append_checksum_error(&mut self, first_sector: u64, sector_count: u64) -> Result<()> {
1230        self.options.checksum_errors.push(SectorRange {
1231            first_sector,
1232            sector_count,
1233        });
1234        Ok(())
1235    }
1236
1237    /// Returns configured session sector ranges.
1238    pub fn sessions(&self) -> &[SectorRange] {
1239        &self.options.sessions
1240    }
1241
1242    /// Returns the number of configured session sector ranges.
1243    pub fn number_of_sessions(&self) -> usize {
1244        self.options.sessions.len()
1245    }
1246
1247    /// Returns a configured session sector range by index.
1248    pub fn session(&self, index: usize) -> Option<&SectorRange> {
1249        self.options.sessions.get(index)
1250    }
1251
1252    /// Appends a session sector range.
1253    ///
1254    /// # Errors
1255    ///
1256    /// Returns an error if the range values exceed the signed 64-bit range
1257    /// required by EWF metadata.
1258    pub fn append_session(&mut self, first_sector: u64, sector_count: u64) -> Result<()> {
1259        validate_signed_sector_range_value("session", "start sector", first_sector)?;
1260        validate_signed_sector_range_value("session", "sector count", sector_count)?;
1261        self.options.sessions.push(SectorRange {
1262            first_sector,
1263            sector_count,
1264        });
1265        Ok(())
1266    }
1267
1268    /// Returns configured track sector ranges.
1269    pub fn tracks(&self) -> &[SectorRange] {
1270        &self.options.tracks
1271    }
1272
1273    /// Returns the number of configured track sector ranges.
1274    pub fn number_of_tracks(&self) -> usize {
1275        self.options.tracks.len()
1276    }
1277
1278    /// Returns a configured track sector range by index.
1279    pub fn track(&self, index: usize) -> Option<&SectorRange> {
1280        self.options.tracks.get(index)
1281    }
1282
1283    /// Appends a track sector range.
1284    ///
1285    /// # Errors
1286    ///
1287    /// Returns an error if the range values exceed the signed 64-bit range
1288    /// required by EWF metadata.
1289    pub fn append_track(&mut self, first_sector: u64, sector_count: u64) -> Result<()> {
1290        validate_signed_sector_range_value("track", "start sector", first_sector)?;
1291        validate_signed_sector_range_value("track", "sector count", sector_count)?;
1292        self.options.tracks.push(SectorRange {
1293            first_sector,
1294            sector_count,
1295        });
1296        Ok(())
1297    }
1298
1299    /// Returns the current logical byte write position.
1300    pub fn offset(&self) -> u64 {
1301        self.current_offset
1302    }
1303
1304    /// Seeks the writer position and returns the new position.
1305    ///
1306    /// # Errors
1307    ///
1308    /// Returns an error if the seek would move before the start of the media or
1309    /// beyond representable bounds.
1310    pub fn seek_offset(&mut self, position: SeekFrom) -> Result<u64> {
1311        self.seek_position(position)
1312    }
1313
1314    /// Seeks the writer position and returns the new position.
1315    ///
1316    /// # Errors
1317    ///
1318    /// Returns an error if the seek would move before the start of the media or
1319    /// beyond representable bounds.
1320    pub fn seek_position(&mut self, position: SeekFrom) -> Result<u64> {
1321        let end_size = target_media_input_size(self.logical_input_size, self.options.media_size)?;
1322        let next = checked_writer_seek(self.current_offset, end_size, position)?;
1323        self.current_offset = next;
1324        Ok(next)
1325    }
1326
1327    /// Finalizes a complete image and writes a terminal `done` marker.
1328    ///
1329    /// # Errors
1330    ///
1331    /// Returns an error if output cannot be prepared, encoded, written, flushed,
1332    /// or mirrored.
1333    pub fn finish(self) -> Result<WriteResult> {
1334        self.finish_with_terminal_section(TerminalSection::Done)
1335    }
1336
1337    /// Finalizes an incomplete image with a continuation marker.
1338    ///
1339    /// # Errors
1340    ///
1341    /// Returns an error if output cannot be prepared, encoded, written, flushed,
1342    /// or mirrored.
1343    pub fn finish_incomplete(self) -> Result<WriteResult> {
1344        self.finish_with_terminal_section(TerminalSection::Next)
1345    }
1346
1347    /// Recovers an interrupted file-backed publication. Pass the actual first
1348    /// segment filename and the original secondary first filename, if any.
1349    /// Uncommitted output is rolled back; committed output is retained. Returns
1350    /// false if no journal exists. Recovery refuses a currently active writer.
1351    pub fn recover_output(path: impl AsRef<Path>, secondary: Option<&Path>) -> Result<bool> {
1352        crate::publication::Publication::recover(path.as_ref(), secondary)
1353    }
1354
1355    fn finish_with_terminal_section(self, terminal: TerminalSection) -> Result<WriteResult> {
1356        let PreparedWrite {
1357            path,
1358            options,
1359            chunks,
1360            mut spool,
1361            logical_size,
1362            chunk_size,
1363            chunk_count,
1364            chunk_count_u32,
1365            sector_count,
1366            computed_sha256,
1367        } = self.prepare_write()?;
1368        let is_v2 = is_ewf2_format(options.format);
1369        let path_for_segment = if is_v2 {
1370            ewf2_segment_path
1371        } else {
1372            segment_path
1373        };
1374        let groups = segment_groups(
1375            &chunks,
1376            options.maximum_segment_size,
1377            &options,
1378            sector_count,
1379            u64::from(chunk_count_u32),
1380        )?;
1381        let group_count = groups.len();
1382        let segment_paths = (1..=group_count)
1383            .map(|index| path_for_segment(&path, index))
1384            .collect::<Result<Vec<_>>>()?;
1385        let secondary_segment_paths = if let Some(base) = &options.secondary_segment_filename {
1386            let paths = (1..=group_count)
1387                .map(|index| path_for_segment(base, index))
1388                .collect::<Result<Vec<_>>>()?;
1389            for secondary in &paths {
1390                ensure_secondary_segment_path_is_distinct(&segment_paths, secondary)?;
1391            }
1392            paths
1393        } else {
1394            Vec::new()
1395        };
1396        let mut publication = crate::publication::Publication::begin(
1397            &segment_paths[0],
1398            secondary_segment_paths.first().map(PathBuf::as_path),
1399            options.overwrite_existing,
1400        )?;
1401        let mut first_chunk = 0_u64;
1402        for (index, group) in groups.into_iter().enumerate() {
1403            let group_chunks = &chunks[group];
1404            let is_last = index + 1 == group_count;
1405            let staged = publication.stage(0, &segment_paths[index])?;
1406            let mut file = File::create(&staged)?;
1407            if is_v2 {
1408                write_ewf2_segment(
1409                    &mut file,
1410                    &mut spool,
1411                    group_chunks,
1412                    &options,
1413                    Ewf2SegmentWriteContext {
1414                        segment_number: u32::try_from(index + 1).map_err(|_| {
1415                            EwfError::Unsupported("EWF2 writer segment count exceeds u32".into())
1416                        })?,
1417                        first_chunk,
1418                        total_chunk_count: chunk_count_u32,
1419                        sector_count,
1420                        terminal_section_type: if is_last {
1421                            terminal.ewf2_type()
1422                        } else {
1423                            EWF2_NEXT_SECTION
1424                        },
1425                    },
1426                )?;
1427            } else {
1428                write_ewf1_segment(
1429                    &mut file,
1430                    &mut spool,
1431                    group_chunks,
1432                    &options,
1433                    Ewf1SegmentWriteContext {
1434                        segment_number: u16::try_from(index + 1).map_err(|_| {
1435                            EwfError::Unsupported("EWF1 writer segment count exceeds u16".into())
1436                        })?,
1437                        chunk_count: chunk_count_u32,
1438                        sector_count,
1439                        sections: Ewf1SegmentSections::for_segment(
1440                            index == 0,
1441                            is_last && terminal == TerminalSection::Done,
1442                        ),
1443                        terminal_section: if is_last {
1444                            terminal
1445                        } else {
1446                            TerminalSection::Done
1447                        },
1448                    },
1449                )?;
1450            }
1451            file.sync_all()?;
1452            drop(file);
1453            if let Some(secondary) = secondary_segment_paths.get(index) {
1454                let mirror = publication.stage(1, secondary)?;
1455                fs::copy(&staged, &mirror)?;
1456                fs::OpenOptions::new()
1457                    .write(true)
1458                    .open(mirror)?
1459                    .sync_all()?;
1460            }
1461            first_chunk += group_chunks.len() as u64;
1462        }
1463        let mut destinations = vec![publication_segment_paths(&segment_paths, is_v2)?];
1464        if !secondary_segment_paths.is_empty() {
1465            destinations.push(publication_segment_paths(&secondary_segment_paths, is_v2)?);
1466        }
1467        publication.publish(&destinations, group_count)?;
1468        Ok(WriteResult {
1469            segment_paths,
1470            secondary_segment_paths,
1471            logical_size,
1472            chunk_size,
1473            chunk_count,
1474            computed_sha256,
1475        })
1476    }
1477
1478    /// Alias for [`EwfWriter::finish`].
1479    ///
1480    /// # Errors
1481    ///
1482    /// Returns the same errors as [`EwfWriter::finish`].
1483    pub fn write_finalize(self) -> Result<WriteResult> {
1484        self.finish()
1485    }
1486
1487    /// Finalizes the image to one supplied writer.
1488    ///
1489    /// # Errors
1490    ///
1491    /// Returns an error if the image requires multiple output segments, if
1492    /// secondary output is configured, or if output preparation/writing fails.
1493    pub fn finish_to_writer<W: Write>(self, writer: &mut W) -> Result<WriteResult> {
1494        let path = self.path.clone();
1495        self.finish_to_segment_writers([(path, writer)])
1496    }
1497
1498    /// Finalizes the image to explicitly supplied segment writers.
1499    ///
1500    /// The number of supplied writers must match the number of output segments
1501    /// implied by the current writer configuration.
1502    ///
1503    /// # Errors
1504    ///
1505    /// Returns an error if secondary output is configured, if the writer count
1506    /// does not match the computed segment count, or if output
1507    /// preparation/writing fails.
1508    pub fn finish_to_segment_writers<P, W, I>(self, segment_writers: I) -> Result<WriteResult>
1509    where
1510        P: Into<PathBuf>,
1511        W: Write,
1512        I: IntoIterator<Item = (P, W)>,
1513    {
1514        let PreparedWrite {
1515            path: _,
1516            options,
1517            chunks,
1518            mut spool,
1519            logical_size,
1520            chunk_size,
1521            chunk_count,
1522            chunk_count_u32,
1523            sector_count,
1524            computed_sha256,
1525        } = self.prepare_write()?;
1526        if options.secondary_segment_filename.is_some() {
1527            return Err(EwfError::Unsupported(
1528                "secondary segment output requires file-backed finish".into(),
1529            ));
1530        }
1531
1532        let segment_writers: Vec<_> = segment_writers
1533            .into_iter()
1534            .map(|(path, writer)| (path.into(), writer))
1535            .collect();
1536        let groups = segment_groups(
1537            &chunks,
1538            options.maximum_segment_size,
1539            &options,
1540            sector_count,
1541            u64::from(chunk_count_u32),
1542        )?;
1543        if segment_writers.len() != groups.len() {
1544            return Err(EwfError::Unsupported(
1545                "finish_to_segment_writers requires exactly one writer per output segment".into(),
1546            ));
1547        }
1548        let group_count = groups.len();
1549        let mut segment_paths = Vec::with_capacity(group_count);
1550
1551        if is_ewf2_format(options.format) {
1552            let mut first_chunk = 0_u64;
1553            for (index, ((segment_path, mut writer), group)) in
1554                segment_writers.into_iter().zip(groups).enumerate()
1555            {
1556                let group_chunks = &chunks[group];
1557                let segment_number = u32::try_from(index + 1).map_err(|_| {
1558                    EwfError::Unsupported("EWF2 writer segment count exceeds u32".into())
1559                })?;
1560                let terminal_section_type = if index + 1 == group_count {
1561                    EWF2_DONE_SECTION
1562                } else {
1563                    EWF2_NEXT_SECTION
1564                };
1565                write_ewf2_segment(
1566                    &mut writer,
1567                    &mut spool,
1568                    group_chunks,
1569                    &options,
1570                    Ewf2SegmentWriteContext {
1571                        segment_number,
1572                        first_chunk,
1573                        total_chunk_count: chunk_count_u32,
1574                        sector_count,
1575                        terminal_section_type,
1576                    },
1577                )?;
1578                writer.flush()?;
1579                first_chunk = first_chunk
1580                    .checked_add(u64::try_from(group_chunks.len()).expect("usize fits u64"))
1581                    .ok_or_else(|| {
1582                        EwfError::Malformed("writer EWF2 first chunk overflow".into())
1583                    })?;
1584                segment_paths.push(segment_path);
1585            }
1586        } else {
1587            for (index, ((segment_path, mut writer), group)) in
1588                segment_writers.into_iter().zip(groups).enumerate()
1589            {
1590                let group_chunks = &chunks[group];
1591                let segment_number = u16::try_from(index + 1).map_err(|_| {
1592                    EwfError::Unsupported("EWF1 writer segment count exceeds u16".into())
1593                })?;
1594                let sections =
1595                    Ewf1SegmentSections::for_segment(index == 0, index + 1 == group_count);
1596                write_ewf1_segment(
1597                    &mut writer,
1598                    &mut spool,
1599                    group_chunks,
1600                    &options,
1601                    Ewf1SegmentWriteContext {
1602                        segment_number,
1603                        chunk_count: chunk_count_u32,
1604                        sector_count,
1605                        sections,
1606                        terminal_section: TerminalSection::Done,
1607                    },
1608                )?;
1609                writer.flush()?;
1610                segment_paths.push(segment_path);
1611            }
1612        }
1613
1614        Ok(WriteResult {
1615            segment_paths,
1616            secondary_segment_paths: Vec::new(),
1617            logical_size,
1618            chunk_size,
1619            chunk_count,
1620            computed_sha256,
1621        })
1622    }
1623
1624    fn write_to_current(&mut self, data: &[u8]) -> Result<usize> {
1625        self.ensure_not_aborted()?;
1626        let data_len = u64::try_from(data.len())
1627            .map_err(|_| EwfError::Malformed("writer data length does not fit u64".into()))?;
1628        let end_offset = self
1629            .current_offset
1630            .checked_add(data_len)
1631            .ok_or_else(|| EwfError::Malformed("writer logical input size overflow".into()))?;
1632        if self
1633            .options
1634            .media_size
1635            .is_some_and(|media_size| end_offset > media_size)
1636        {
1637            return Err(EwfError::Unsupported(
1638                "writer write exceeds configured media size".into(),
1639            ));
1640        }
1641        self.forget_encoded_chunks_in_range(self.current_offset, end_offset);
1642        self.raw.write_at(self.current_offset, data)?;
1643        self.current_offset = end_offset;
1644        self.logical_input_size = self.logical_input_size.max(end_offset);
1645        Ok(data.len())
1646    }
1647
1648    fn prepare_write(self) -> Result<PreparedWrite> {
1649        self.ensure_not_aborted()?;
1650        let logical_size = self.logical_size()?;
1651        let sector_count = logical_size / u64::from(self.options.bytes_per_sector);
1652
1653        let EwfWriter {
1654            path,
1655            mut options,
1656            chunk_size,
1657            chunk_capacity,
1658            mut raw,
1659            encoded_chunks,
1660            ..
1661        } = self;
1662
1663        let EncodedSpool {
1664            chunks,
1665            spool,
1666            computed_md5,
1667            computed_sha1,
1668            computed_sha256,
1669        } = encode_raw_spool(
1670            &mut raw,
1671            encoded_chunks,
1672            logical_size,
1673            chunk_capacity,
1674            chunk_size,
1675            &options,
1676        )?;
1677
1678        options.hashes = effective_write_hashes(
1679            &options.hashes,
1680            computed_md5,
1681            computed_sha1,
1682            computed_sha256,
1683        )?;
1684        validate_session_ranges("sessions", &options.sessions, sector_count)?;
1685        validate_session_ranges("tracks", &options.tracks, sector_count)?;
1686        let chunk_count = u64::try_from(chunks.len())
1687            .map_err(|_| EwfError::Malformed("writer chunk count does not fit u64".into()))?;
1688        let chunk_count_u32 = u32::try_from(chunk_count)
1689            .map_err(|_| EwfError::Unsupported("writer chunk count exceeds u32".into()))?;
1690
1691        Ok(PreparedWrite {
1692            path,
1693            options,
1694            chunks,
1695            spool,
1696            logical_size,
1697            chunk_size,
1698            chunk_count,
1699            chunk_count_u32,
1700            sector_count,
1701            computed_sha256,
1702        })
1703    }
1704
1705    fn ensure_not_aborted(&self) -> Result<()> {
1706        if self.abort_signaled.load(Ordering::Relaxed) {
1707            return Err(EwfError::Aborted);
1708        }
1709        Ok(())
1710    }
1711
1712    fn ensure_configuration_mutable(&self, label: &str) -> Result<()> {
1713        if self.logical_input_size == 0 {
1714            return Ok(());
1715        }
1716        Err(EwfError::Unsupported(format!(
1717            "writer {label} cannot be changed after media data has been written"
1718        )))
1719    }
1720
1721    fn remember_encoded_data_chunk_at(
1722        &mut self,
1723        chunk: &EncodedDataChunk,
1724        offset: u64,
1725        decoded_len: usize,
1726    ) {
1727        let Some(encoded) = remembered_encoded_data_chunk(
1728            chunk,
1729            &self.options,
1730            self.chunk_size,
1731            offset,
1732            decoded_len,
1733        ) else {
1734            return;
1735        };
1736        self.encoded_chunks
1737            .insert(offset / self.chunk_size, encoded);
1738    }
1739
1740    fn forget_encoded_chunks_in_range(&mut self, start_offset: u64, end_offset: u64) {
1741        if start_offset >= end_offset || self.encoded_chunks.is_empty() {
1742            return;
1743        }
1744
1745        let first_chunk = start_offset / self.chunk_size;
1746        let last_chunk = (end_offset - 1) / self.chunk_size;
1747        let keys: Vec<_> = self
1748            .encoded_chunks
1749            .range(first_chunk..=last_chunk)
1750            .map(|(chunk_index, _)| *chunk_index)
1751            .collect();
1752        for chunk_index in keys {
1753            self.encoded_chunks.remove(&chunk_index);
1754        }
1755    }
1756}
1757
1758fn target_media_input_size(written_size: u64, configured_media_size: Option<u64>) -> Result<u64> {
1759    match configured_media_size {
1760        Some(media_size) if written_size > media_size => Err(EwfError::Unsupported(
1761            "writer configured media size is smaller than written data".into(),
1762        )),
1763        Some(media_size) => Ok(media_size),
1764        None => Ok(written_size),
1765    }
1766}
1767
1768fn padded_logical_size(logical_input_size: u64, bytes_per_sector: u32) -> Result<u64> {
1769    let bytes_per_sector = u64::from(bytes_per_sector);
1770    let sector_count = logical_input_size.div_ceil(bytes_per_sector);
1771    sector_count
1772        .checked_mul(bytes_per_sector)
1773        .ok_or_else(|| EwfError::Malformed("writer logical size overflow".into()))
1774}
1775
1776fn required_media_u32(value: Option<u64>, label: &str) -> Result<u32> {
1777    let value = value.ok_or_else(|| {
1778        EwfError::Unsupported(format!("cannot copy media values without {label}"))
1779    })?;
1780    u32::try_from(value)
1781        .map_err(|_| EwfError::Unsupported(format!("copied {label} does not fit u32")))
1782}
1783
1784fn write_format_from_image_info(info: &ImageInfo) -> WriteFormat {
1785    match info.format {
1786        Format::Ewf2 if image_info_is_physical(info) => WriteFormat::Ewf2Physical,
1787        Format::Ewf2 => WriteFormat::Ewf2Logical,
1788        Format::Ewf1 if info.format_profile == FormatProfile::Smart => WriteFormat::Ewf1Smart,
1789        Format::Ewf1 if image_info_is_physical(info) => WriteFormat::Ewf1Physical,
1790        Format::Ewf1 => WriteFormat::Ewf1Logical,
1791    }
1792}
1793
1794fn image_info_is_physical(info: &ImageInfo) -> bool {
1795    !matches!(info.media.media_type, Some(MediaType::SingleFiles))
1796        && info.media.media_flags.physical
1797}
1798
1799fn write_compression_from_media_method(method: CompressionMethod) -> Result<WriteCompression> {
1800    match method {
1801        CompressionMethod::None => Ok(WriteCompression::None),
1802        CompressionMethod::Zlib => Ok(WriteCompression::Zlib),
1803        CompressionMethod::Zstd => Err(EwfError::Unsupported(
1804            "cannot copy X-Ways Zstandard compression settings".into(),
1805        )),
1806        CompressionMethod::Bzip2 => Ok(WriteCompression::Bzip2),
1807        CompressionMethod::Unknown(value) => Err(EwfError::Unsupported(format!(
1808            "cannot copy unknown compression method {value}"
1809        ))),
1810    }
1811}
1812
1813fn write_compression_values_from_media_values(
1814    values: CompressionValues,
1815) -> Result<WriteCompressionValues> {
1816    if values.flags.pattern_fill {
1817        return Err(EwfError::Unsupported(
1818            "cannot copy pattern fill compression flag".into(),
1819        ));
1820    }
1821    if values.flags.unknown_bits != 0 {
1822        return Err(EwfError::Unsupported(format!(
1823            "cannot copy unknown compression flags 0x{:02x}",
1824            values.flags.unknown_bits
1825        )));
1826    }
1827
1828    Ok(WriteCompressionValues {
1829        level: write_compression_level_from_media_level(values.level)?,
1830        empty_block: values.flags.empty_block,
1831    })
1832}
1833
1834fn write_compression_level_from_media_level(
1835    level: CompressionLevel,
1836) -> Result<WriteCompressionLevel> {
1837    match level {
1838        CompressionLevel::Default => Ok(WriteCompressionLevel::Default),
1839        CompressionLevel::None => Ok(WriteCompressionLevel::None),
1840        CompressionLevel::Fast => Ok(WriteCompressionLevel::Fast),
1841        CompressionLevel::Best => Ok(WriteCompressionLevel::Best),
1842        CompressionLevel::Unknown(value) => Err(EwfError::Unsupported(format!(
1843            "cannot copy unknown compression level {value}"
1844        ))),
1845    }
1846}
1847
1848fn rewrite_options_from_image_info(info: &ImageInfo) -> Result<WriteOptions> {
1849    let mut options = WriteOptions::default();
1850    options.copy_media_values_from_info(info)?;
1851    options.copy_header_values_from_info(info);
1852    options.header_codepage = info.header_codepage;
1853    options.header_values_date_format = info.header_values_date_format;
1854    options
1855        .acquisition_errors
1856        .clone_from(&info.acquisition_errors);
1857    options.sessions.clone_from(&info.sessions);
1858    options.tracks.clone_from(&info.tracks);
1859    options.memory_extents.clone_from(&info.memory_extents);
1860    options.single_files.clone_from(&info.single_files);
1861    options.ewf2_single_files_tables = info.ewf2_single_files_tables.clone();
1862    options
1863        .ewf2_increment_data
1864        .clone_from(&info.ewf2_increment_data);
1865    options
1866        .ewf2_final_information
1867        .clone_from(&info.ewf2_final_information);
1868    options
1869        .ewf2_restart_data
1870        .clone_from(&info.ewf2_restart_data);
1871    options
1872        .ewf2_analytical_data
1873        .clone_from(&info.ewf2_analytical_data);
1874    validate_options(&options)?;
1875    Ok(options)
1876}
1877
1878fn copy_image_media_to_writer(image: &Image, writer: &mut EwfWriter) -> Result<()> {
1879    if let Some(chunk_count) = image.number_of_chunks() {
1880        for chunk_index in 0..chunk_count {
1881            let chunk = image.read_encoded_data_chunk(chunk_index)?;
1882            writer.write_encoded_data_chunk(&chunk)?;
1883        }
1884        return Ok(());
1885    }
1886
1887    copy_image_media_bytes_to_writer(image, writer)
1888}
1889
1890fn copy_image_media_bytes_to_writer(image: &Image, writer: &mut EwfWriter) -> Result<()> {
1891    let media_size = image.media_size();
1892    let buffer_size = writer.chunk_capacity.clamp(1, 1024 * 1024);
1893    let mut buffer = vec![0; buffer_size];
1894    let mut offset = 0_u64;
1895
1896    while offset < media_size {
1897        let remaining = media_size - offset;
1898        let take = usize::try_from(remaining.min(buffer.len() as u64))
1899            .expect("source image copy is bounded by buffer length");
1900        let read = image.read_at(&mut buffer[..take], offset)?;
1901        if read != take {
1902            return Err(EwfError::Malformed(format!(
1903                "source image read returned {read} bytes, expected {take}"
1904            )));
1905        }
1906        writer.write_all(&buffer[..take])?;
1907        offset = offset
1908            .checked_add(u64::try_from(take).expect("usize fits u64"))
1909            .ok_or_else(|| EwfError::Malformed("source image copy offset overflow".into()))?;
1910    }
1911
1912    Ok(())
1913}
1914
1915impl std::io::Write for EwfWriter {
1916    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1917        self.write_all(buf).map_err(std::io::Error::other)?;
1918        Ok(buf.len())
1919    }
1920
1921    fn flush(&mut self) -> std::io::Result<()> {
1922        Ok(())
1923    }
1924}
1925
1926impl std::io::Seek for EwfWriter {
1927    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
1928        self.seek_position(pos).map_err(std::io::Error::other)
1929    }
1930
1931    fn stream_position(&mut self) -> std::io::Result<u64> {
1932        Ok(self.current_offset)
1933    }
1934}
1935
1936#[derive(Clone)]
1937struct WriteHashState {
1938    md5: Md5,
1939    sha1: Sha1,
1940    sha256: Sha256,
1941}
1942
1943impl WriteHashState {
1944    fn new() -> Self {
1945        Self {
1946            md5: Md5::new(),
1947            sha1: Sha1::new(),
1948            sha256: Sha256::new(),
1949        }
1950    }
1951
1952    fn update(&mut self, data: &[u8]) {
1953        self.md5.update(data);
1954        self.sha1.update(data);
1955        self.sha256.update(data);
1956    }
1957
1958    fn finalize(self) -> ([u8; 16], [u8; 20], [u8; 32]) {
1959        (
1960            self.md5.finalize().into(),
1961            self.sha1.finalize().into(),
1962            self.sha256.finalize().into(),
1963        )
1964    }
1965}
1966
1967fn validate_options(options: &WriteOptions) -> Result<()> {
1968    validated_write_hashes(&options.hashes)?;
1969    if options.sectors_per_chunk == 0 {
1970        return Err(EwfError::Malformed(
1971            "writer sectors_per_chunk is zero".into(),
1972        ));
1973    }
1974    if options.bytes_per_sector == 0 {
1975        return Err(EwfError::Malformed(
1976            "writer bytes_per_sector is zero".into(),
1977        ));
1978    }
1979    if options.format == WriteFormat::Ewf2Logical
1980        && u64::from(options.sectors_per_chunk) * u64::from(options.bytes_per_sector) < 8192
1981    {
1982        return Err(EwfError::Unsupported(
1983            "EWF2 logical output requires chunks of at least 8192 bytes for libewf interoperability"
1984                .into(),
1985        ));
1986    }
1987    if matches!(options.compression, WriteCompression::Bzip2) && !is_ewf2_format(options.format) {
1988        return Err(EwfError::Unsupported(
1989            "BZip2 writer compression is only supported for EWF2".into(),
1990        ));
1991    }
1992    if matches!(options.compression, WriteCompression::Bzip2)
1993        && options.compression_values.level == WriteCompressionLevel::None
1994    {
1995        return Err(EwfError::Unsupported(
1996            "BZip2 writer compression does not support none compression level".into(),
1997        ));
1998    }
1999    if options.single_files.is_some()
2000        && !matches!(
2001            options.format,
2002            WriteFormat::Ewf1Logical | WriteFormat::Ewf2Logical
2003        )
2004    {
2005        return Err(EwfError::Unsupported(
2006            "single files catalog writing is only supported for logical images".into(),
2007        ));
2008    }
2009    if !options.ewf2_single_files_tables.is_empty() && options.format != WriteFormat::Ewf2Logical {
2010        return Err(EwfError::Unsupported(
2011            "single files auxiliary table writing is only supported for EWF2 logical images".into(),
2012        ));
2013    }
2014    if !options.memory_extents.is_empty() && !is_ewf2_format(options.format) {
2015        return Err(EwfError::Unsupported(
2016            "memory extents writing is only supported for EWF2 images".into(),
2017        ));
2018    }
2019    if (!options.ewf2_increment_data.is_empty() || options.ewf2_final_information.is_some())
2020        && !is_ewf2_format(options.format)
2021    {
2022        return Err(EwfError::Unsupported(
2023            "opaque EWF2 section writing is only supported for EWF2 images".into(),
2024        ));
2025    }
2026    if (options.ewf2_restart_data.is_some() || options.ewf2_analytical_data.is_some())
2027        && !is_ewf2_format(options.format)
2028    {
2029        return Err(EwfError::Unsupported(
2030            "EWF2 application data writing is only supported for EWF2 images".into(),
2031        ));
2032    }
2033    Ok(())
2034}
2035
2036fn normalize_maximum_segment_size(maximum_segment_size: Option<u64>) -> Option<u64> {
2037    maximum_segment_size.filter(|size| *size != 0)
2038}
2039
2040fn normalize_media_size(media_size: Option<u64>) -> Option<u64> {
2041    media_size.filter(|size| *size != 0)
2042}
2043
2044fn writer_chunk_geometry(sectors_per_chunk: u32, bytes_per_sector: u32) -> Result<(u64, usize)> {
2045    if sectors_per_chunk == 0 {
2046        return Err(EwfError::Malformed(
2047            "writer sectors_per_chunk is zero".into(),
2048        ));
2049    }
2050    if bytes_per_sector == 0 {
2051        return Err(EwfError::Malformed(
2052            "writer bytes_per_sector is zero".into(),
2053        ));
2054    }
2055
2056    let chunk_size = u64::from(sectors_per_chunk)
2057        .checked_mul(u64::from(bytes_per_sector))
2058        .ok_or_else(|| EwfError::Malformed("writer chunk size overflow".into()))?;
2059    let chunk_capacity = usize::try_from(chunk_size)
2060        .map_err(|_| EwfError::Unsupported("writer chunk size does not fit usize".into()))?;
2061    Ok((chunk_size, chunk_capacity))
2062}
2063
2064fn validate_write_data_chunk(chunk: &DataChunk) -> Result<()> {
2065    if chunk.corrupted {
2066        return Err(EwfError::Malformed(
2067            "writer cannot write corrupted data chunk".into(),
2068        ));
2069    }
2070    if chunk.data.len() != chunk.logical_size {
2071        return Err(EwfError::Malformed(
2072            "writer data chunk payload length does not match logical size".into(),
2073        ));
2074    }
2075
2076    Ok(())
2077}
2078
2079fn decode_write_encoded_data_chunk(chunk: &EncodedDataChunk) -> Result<Vec<u8>> {
2080    validate_write_encoded_data_chunk(chunk)?;
2081    decode_chunk(
2082        &chunk.data,
2083        encoded_data_chunk_encoding(chunk.encoding),
2084        chunk.logical_size,
2085    )
2086}
2087
2088fn validate_write_encoded_data_chunk(chunk: &EncodedDataChunk) -> Result<()> {
2089    if chunk.encoding == DataChunkEncoding::Zstd {
2090        return Err(EwfError::Unsupported(
2091            "writer does not support X-Ways Zstandard encoded chunks".into(),
2092        ));
2093    }
2094    let encoded_size = usize::try_from(chunk.encoded_size)
2095        .map_err(|_| EwfError::Malformed("writer encoded chunk size does not fit usize".into()))?;
2096    if chunk.data.len() != encoded_size {
2097        return Err(EwfError::Malformed(
2098            "writer encoded data chunk payload length does not match encoded size".into(),
2099        ));
2100    }
2101
2102    Ok(())
2103}
2104
2105fn encoded_data_chunk_encoding(encoding: DataChunkEncoding) -> ChunkEncoding {
2106    match encoding {
2107        DataChunkEncoding::Raw => ChunkEncoding::Raw,
2108        DataChunkEncoding::Zlib => ChunkEncoding::Zlib,
2109        DataChunkEncoding::Zstd => ChunkEncoding::Zstd,
2110        DataChunkEncoding::Bzip2 => ChunkEncoding::Bzip2,
2111        DataChunkEncoding::PatternFill(pattern) => ChunkEncoding::PatternFill(pattern),
2112    }
2113}
2114
2115fn remembered_encoded_data_chunk(
2116    chunk: &EncodedDataChunk,
2117    options: &WriteOptions,
2118    target_chunk_size: u64,
2119    offset: u64,
2120    decoded_len: usize,
2121) -> Option<RememberedEncodedChunk> {
2122    if !offset.is_multiple_of(target_chunk_size) || decoded_len != chunk.logical_size {
2123        return None;
2124    }
2125    if u64::try_from(decoded_len).ok()? > target_chunk_size {
2126        return None;
2127    }
2128    if !encoded_data_chunk_encoding_compatible(chunk.encoding, options) {
2129        return None;
2130    }
2131    if validate_encoded_size(
2132        chunk.encoded_size,
2133        target_chunk_size,
2134        encoded_data_chunk_encoding(chunk.encoding),
2135    )
2136    .is_err()
2137    {
2138        return None;
2139    }
2140
2141    let encoded = match chunk.encoding {
2142        DataChunkEncoding::Raw => {
2143            if chunk.has_checksum && !raw_encoded_data_chunk_checksum_is_valid(chunk) {
2144                return None;
2145            }
2146            EncodedChunk {
2147                bytes: chunk.data.clone(),
2148                compressed: false,
2149                has_checksum: chunk.has_checksum,
2150                pattern_fill: None,
2151            }
2152        }
2153        DataChunkEncoding::Zlib | DataChunkEncoding::Bzip2 => EncodedChunk {
2154            bytes: chunk.data.clone(),
2155            compressed: true,
2156            has_checksum: false,
2157            pattern_fill: None,
2158        },
2159        DataChunkEncoding::Zstd => return None,
2160        DataChunkEncoding::PatternFill(pattern) => EncodedChunk {
2161            bytes: Vec::new(),
2162            compressed: true,
2163            has_checksum: false,
2164            pattern_fill: Some(pattern),
2165        },
2166    };
2167
2168    Some(RememberedEncodedChunk {
2169        logical_size: decoded_len,
2170        encoding: chunk.encoding,
2171        encoded,
2172    })
2173}
2174
2175fn encoded_data_chunk_encoding_compatible(
2176    encoding: DataChunkEncoding,
2177    options: &WriteOptions,
2178) -> bool {
2179    match encoding {
2180        DataChunkEncoding::Raw => true,
2181        DataChunkEncoding::Zlib => options.compression != WriteCompression::Bzip2,
2182        DataChunkEncoding::Zstd => false,
2183        DataChunkEncoding::Bzip2 => {
2184            is_ewf2_format(options.format) && options.compression == WriteCompression::Bzip2
2185        }
2186        DataChunkEncoding::PatternFill(_) => is_ewf2_format(options.format),
2187    }
2188}
2189
2190fn raw_encoded_data_chunk_checksum_is_valid(chunk: &EncodedDataChunk) -> bool {
2191    let Some(checksum_offset) = chunk.logical_size.checked_add(4) else {
2192        return false;
2193    };
2194    if chunk.data.len() != checksum_offset {
2195        return false;
2196    }
2197    let expected = adler32(&chunk.data[..chunk.logical_size]).to_le_bytes();
2198    chunk.data[chunk.logical_size..] == expected
2199}
2200
2201fn validate_session_ranges(
2202    label: &str,
2203    ranges: &[SectorRange],
2204    media_sector_count: u64,
2205) -> Result<()> {
2206    validate_sector_ranges(label, ranges, media_sector_count)?;
2207    if ranges.is_empty() {
2208        return Ok(());
2209    }
2210
2211    let mut expected_start = 0_u64;
2212    for range in ranges {
2213        if range.first_sector != expected_start {
2214            return Err(EwfError::Unsupported(format!(
2215                "writer {label} must cover the media contiguously from sector 0"
2216            )));
2217        }
2218        expected_start = range
2219            .first_sector
2220            .checked_add(range.sector_count)
2221            .ok_or_else(|| EwfError::Malformed(format!("writer {label} range overflow")))?;
2222    }
2223    if expected_start != media_sector_count {
2224        return Err(EwfError::Unsupported(format!(
2225            "writer {label} must end at the media sector count"
2226        )));
2227    }
2228
2229    Ok(())
2230}
2231
2232fn validate_sector_ranges(
2233    label: &str,
2234    ranges: &[SectorRange],
2235    media_sector_count: u64,
2236) -> Result<()> {
2237    let mut previous_end = 0_u64;
2238    for range in ranges {
2239        if range.sector_count == 0 {
2240            return Err(EwfError::Unsupported(format!(
2241                "writer {label} cannot contain zero-length ranges"
2242            )));
2243        }
2244        if range.first_sector < previous_end {
2245            return Err(EwfError::Unsupported(format!(
2246                "writer {label} ranges must be sorted and non-overlapping"
2247            )));
2248        }
2249        let end = range
2250            .first_sector
2251            .checked_add(range.sector_count)
2252            .ok_or_else(|| EwfError::Malformed(format!("writer {label} range overflow")))?;
2253        if end > media_sector_count {
2254            return Err(EwfError::Unsupported(format!(
2255                "writer {label} range exceeds the media sector count"
2256            )));
2257        }
2258        previous_end = end;
2259    }
2260
2261    Ok(())
2262}
2263
2264fn validate_signed_sector_range_value(label: &str, field: &str, value: u64) -> Result<()> {
2265    if value > SIGNED_SECTOR_RANGE_MAX {
2266        return Err(EwfError::Unsupported(format!(
2267            "writer {label} {field} exceeds signed 64-bit range"
2268        )));
2269    }
2270    Ok(())
2271}
2272
2273fn write_ewf1_segment<W: Write>(
2274    writer: &mut W,
2275    spool: &mut ChunkSpool,
2276    chunks: &[ChunkDescriptor],
2277    options: &WriteOptions,
2278    context: Ewf1SegmentWriteContext,
2279) -> Result<()> {
2280    u32::try_from(chunks.len())
2281        .map_err(|_| EwfError::Unsupported("EWF1 writer segment chunk count exceeds u32".into()))?;
2282    let header = if context.sections.contains(Ewf1SegmentSections::HEADER) {
2283        header_payload(
2284            &options.metadata,
2285            options.header_codepage,
2286            options.compression_values.level,
2287        )?
2288    } else {
2289        None
2290    };
2291    let header2 = if context.sections.contains(Ewf1SegmentSections::HEADER) {
2292        header2_payload(&options.metadata, options.compression_values.level)?
2293    } else {
2294        None
2295    };
2296    let xheader = if context.sections.contains(Ewf1SegmentSections::HEADER) {
2297        xheader_payload(&options.metadata, options.compression_values.level)?
2298    } else {
2299        None
2300    };
2301    let digest = context
2302        .sections
2303        .contains(Ewf1SegmentSections::DIGEST)
2304        .then(|| digest_payload(&options.hashes))
2305        .flatten();
2306    let xhash = if context.sections.contains(Ewf1SegmentSections::DIGEST) {
2307        xhash_payload(&options.hashes, options.compression_values.level)?
2308    } else {
2309        None
2310    };
2311    let error2 = if context.sections.contains(Ewf1SegmentSections::ERRORS) {
2312        ewf1_error2_payload(&options.acquisition_errors)?
2313    } else {
2314        None
2315    };
2316    let session = if context.sections.contains(Ewf1SegmentSections::SESSIONS) {
2317        ewf1_session_payload(&options.sessions, &options.tracks)?
2318    } else {
2319        None
2320    };
2321    let ltree = if context.sections.contains(Ewf1SegmentSections::DIGEST)
2322        && options.format == WriteFormat::Ewf1Logical
2323    {
2324        options
2325            .single_files
2326            .as_ref()
2327            .map(ewf1_ltree_payload)
2328            .transpose()?
2329    } else {
2330        None
2331    };
2332    let header_desc_offset = ewf1::FILE_HEADER_SIZE as u64;
2333    let volume_data_size = volume_data_size(options.format);
2334    let header2_desc_offset = if let Some(header) = &header {
2335        header_desc_offset
2336            .checked_add(ewf1::SECTION_DESCRIPTOR_SIZE as u64)
2337            .and_then(|value| value.checked_add(u64::try_from(header.len()).ok()?))
2338            .ok_or_else(|| EwfError::Malformed("writer volume descriptor offset overflow".into()))?
2339    } else {
2340        header_desc_offset
2341    };
2342    let xheader_desc_offset = if let Some(header2) = &header2 {
2343        header2_desc_offset
2344            .checked_add(ewf1::SECTION_DESCRIPTOR_SIZE as u64)
2345            .and_then(|value| value.checked_add(u64::try_from(header2.len()).ok()?))
2346            .ok_or_else(|| EwfError::Malformed("writer volume descriptor offset overflow".into()))?
2347    } else {
2348        header2_desc_offset
2349    };
2350    let volume_desc_offset = if let Some(xheader) = &xheader {
2351        xheader_desc_offset
2352            .checked_add(ewf1::SECTION_DESCRIPTOR_SIZE as u64)
2353            .and_then(|value| value.checked_add(u64::try_from(xheader.len()).ok()?))
2354            .ok_or_else(|| EwfError::Malformed("writer volume descriptor offset overflow".into()))?
2355    } else {
2356        xheader_desc_offset
2357    };
2358    let volume_data_offset = volume_desc_offset + ewf1::SECTION_DESCRIPTOR_SIZE as u64;
2359    let session_desc_offset = volume_data_offset + volume_data_size;
2360    let sectors_desc_offset = if let Some(session) = &session {
2361        session_desc_offset
2362            .checked_add(ewf1::SECTION_DESCRIPTOR_SIZE as u64)
2363            .and_then(|value| value.checked_add(u64::try_from(session.len()).ok()?))
2364            .ok_or_else(|| {
2365                EwfError::Malformed("writer sectors descriptor offset overflow".into())
2366            })?
2367    } else {
2368        session_desc_offset
2369    };
2370    let table_footer_bytes = if matches!(options.format, WriteFormat::Ewf1Smart) {
2371        0
2372    } else {
2373        4
2374    };
2375    let writes_table2 = matches!(
2376        options.format,
2377        WriteFormat::Ewf1Physical | WriteFormat::Ewf1Logical
2378    );
2379    let group_layouts = ewf1_table_group_layouts(
2380        chunks,
2381        sectors_desc_offset,
2382        table_footer_bytes,
2383        writes_table2,
2384    )?;
2385    let post_table_desc_offset = group_layouts
2386        .last()
2387        .map_or(sectors_desc_offset, |layout| layout.end_offset);
2388    let ltree_desc_offset = post_table_desc_offset;
2389    let error2_desc_offset = if let Some(ltree) = &ltree {
2390        ltree_desc_offset
2391            .checked_add(ewf1::SECTION_DESCRIPTOR_SIZE as u64)
2392            .and_then(|value| value.checked_add(u64::try_from(ltree.len()).ok()?))
2393            .ok_or_else(|| EwfError::Malformed("writer error2 descriptor offset overflow".into()))?
2394    } else {
2395        post_table_desc_offset
2396    };
2397    let digest_desc_offset = if let Some(error2) = &error2 {
2398        error2_desc_offset
2399            .checked_add(ewf1::SECTION_DESCRIPTOR_SIZE as u64)
2400            .and_then(|value| value.checked_add(u64::try_from(error2.len()).ok()?))
2401            .ok_or_else(|| EwfError::Malformed("writer digest descriptor offset overflow".into()))?
2402    } else {
2403        error2_desc_offset
2404    };
2405    let xhash_desc_offset = if let Some(digest) = &digest {
2406        digest_desc_offset
2407            .checked_add(ewf1::SECTION_DESCRIPTOR_SIZE as u64)
2408            .and_then(|value| value.checked_add(u64::try_from(digest.len()).ok()?))
2409            .ok_or_else(|| EwfError::Malformed("writer xhash descriptor offset overflow".into()))?
2410    } else {
2411        digest_desc_offset
2412    };
2413    let done_desc_offset = if let Some(xhash) = &xhash {
2414        xhash_desc_offset
2415            .checked_add(ewf1::SECTION_DESCRIPTOR_SIZE as u64)
2416            .and_then(|value| value.checked_add(u64::try_from(xhash.len()).ok()?))
2417            .ok_or_else(|| EwfError::Malformed("writer done descriptor offset overflow".into()))?
2418    } else {
2419        xhash_desc_offset
2420    };
2421    let after_tables_desc_offset = if ltree.is_some() {
2422        ltree_desc_offset
2423    } else if error2.is_some() {
2424        error2_desc_offset
2425    } else if digest.is_some() {
2426        digest_desc_offset
2427    } else if xhash.is_some() {
2428        xhash_desc_offset
2429    } else {
2430        done_desc_offset
2431    };
2432    let after_ltree_desc_offset = if error2.is_some() {
2433        error2_desc_offset
2434    } else if digest.is_some() {
2435        digest_desc_offset
2436    } else if xhash.is_some() {
2437        xhash_desc_offset
2438    } else {
2439        done_desc_offset
2440    };
2441    let after_error2_desc_offset = if digest.is_some() {
2442        digest_desc_offset
2443    } else if xhash.is_some() {
2444        xhash_desc_offset
2445    } else {
2446        done_desc_offset
2447    };
2448    let after_digest_desc_offset = if xhash.is_some() {
2449        xhash_desc_offset
2450    } else {
2451        done_desc_offset
2452    };
2453
2454    writer.write_all(ewf1_signature(options.format))?;
2455    writer.write_all(&[1])?;
2456    writer.write_all(&context.segment_number.to_le_bytes())?;
2457    writer.write_all(&0_u16.to_le_bytes())?;
2458
2459    if let Some(header) = &header {
2460        writer.write_all(&section_desc(
2461            b"header",
2462            if header2.is_some() {
2463                header2_desc_offset
2464            } else if xheader.is_some() {
2465                xheader_desc_offset
2466            } else {
2467                volume_desc_offset
2468            },
2469            ewf1::SECTION_DESCRIPTOR_SIZE as u64
2470                + u64::try_from(header.len()).expect("usize fits u64"),
2471        ))?;
2472        writer.write_all(header)?;
2473    }
2474
2475    if let Some(header2) = &header2 {
2476        writer.write_all(&section_desc(
2477            b"header2",
2478            if xheader.is_some() {
2479                xheader_desc_offset
2480            } else {
2481                volume_desc_offset
2482            },
2483            ewf1::SECTION_DESCRIPTOR_SIZE as u64
2484                + u64::try_from(header2.len()).expect("usize fits u64"),
2485        ))?;
2486        writer.write_all(header2)?;
2487    }
2488
2489    if let Some(xheader) = &xheader {
2490        writer.write_all(&section_desc(
2491            b"xheader",
2492            volume_desc_offset,
2493            ewf1::SECTION_DESCRIPTOR_SIZE as u64
2494                + u64::try_from(xheader.len()).expect("usize fits u64"),
2495        ))?;
2496        writer.write_all(xheader)?;
2497    }
2498
2499    writer.write_all(&section_desc(
2500        b"volume",
2501        if session.is_some() {
2502            session_desc_offset
2503        } else {
2504            sectors_desc_offset
2505        },
2506        ewf1::SECTION_DESCRIPTOR_SIZE as u64 + volume_data_size,
2507    ))?;
2508    writer.write_all(&volume_data(
2509        options,
2510        context.chunk_count,
2511        context.sector_count,
2512    )?)?;
2513
2514    if let Some(session) = session {
2515        writer.write_all(&section_desc(
2516            b"session",
2517            sectors_desc_offset,
2518            ewf1::SECTION_DESCRIPTOR_SIZE as u64
2519                + u64::try_from(session.len()).expect("usize fits u64"),
2520        ))?;
2521        writer.write_all(&session)?;
2522    }
2523
2524    for (group_index, layout) in group_layouts.iter().enumerate() {
2525        let group_chunks = &chunks[layout.first_chunk..layout.end_chunk];
2526        let group_chunk_count = u32::try_from(group_chunks.len()).map_err(|_| {
2527            EwfError::Unsupported("EWF1 writer table group chunk count exceeds u32".into())
2528        })?;
2529        let next_group_desc_offset = group_layouts
2530            .get(group_index + 1)
2531            .map_or(after_tables_desc_offset, |next| next.sectors_desc_offset);
2532
2533        writer.write_all(&section_desc(
2534            b"sectors",
2535            layout.table_desc_offset,
2536            ewf1::SECTION_DESCRIPTOR_SIZE as u64 + layout.payload_size,
2537        ))?;
2538        for chunk in group_chunks {
2539            spool.copy_chunk_to(chunk, writer)?;
2540        }
2541
2542        writer.write_all(&section_desc(
2543            b"table",
2544            layout.table2_desc_offset.unwrap_or(next_group_desc_offset),
2545            ewf1::SECTION_DESCRIPTOR_SIZE as u64 + layout.table_data_size,
2546        ))?;
2547        let table_capacity = usize::try_from(layout.table_data_size)
2548            .map_err(|_| EwfError::Malformed("writer table data size does not fit usize".into()))?;
2549        let mut table_data = Vec::with_capacity(table_capacity);
2550        append_ewf1_table_data(
2551            &mut table_data,
2552            group_chunk_count,
2553            layout.sectors_data_offset,
2554            group_chunks,
2555            table_footer_bytes != 0,
2556        )?;
2557        writer.write_all(&table_data)?;
2558
2559        if layout.table2_desc_offset.is_some() {
2560            writer.write_all(&section_desc(
2561                b"table2",
2562                next_group_desc_offset,
2563                ewf1::SECTION_DESCRIPTOR_SIZE as u64 + layout.table_data_size,
2564            ))?;
2565            let mut table2_data = Vec::with_capacity(table_capacity);
2566            append_ewf1_table_data(
2567                &mut table2_data,
2568                group_chunk_count,
2569                layout.sectors_data_offset,
2570                group_chunks,
2571                true,
2572            )?;
2573            writer.write_all(&table2_data)?;
2574        }
2575    }
2576
2577    if let Some(ltree) = &ltree {
2578        writer.write_all(&section_desc(
2579            b"ltree",
2580            after_ltree_desc_offset,
2581            ewf1::SECTION_DESCRIPTOR_SIZE as u64
2582                + u64::try_from(ltree.len()).expect("usize fits u64"),
2583        ))?;
2584        writer.write_all(ltree)?;
2585    }
2586
2587    if let Some(error2) = error2 {
2588        writer.write_all(&section_desc(
2589            b"error2",
2590            after_error2_desc_offset,
2591            ewf1::SECTION_DESCRIPTOR_SIZE as u64
2592                + u64::try_from(error2.len()).expect("usize fits u64"),
2593        ))?;
2594        writer.write_all(&error2)?;
2595    }
2596    if let Some(digest) = digest {
2597        writer.write_all(&section_desc(
2598            b"digest",
2599            after_digest_desc_offset,
2600            ewf1::SECTION_DESCRIPTOR_SIZE as u64
2601                + u64::try_from(digest.len()).expect("usize fits u64"),
2602        ))?;
2603        writer.write_all(&digest)?;
2604    }
2605    if let Some(xhash) = xhash {
2606        writer.write_all(&section_desc(
2607            b"xhash",
2608            done_desc_offset,
2609            ewf1::SECTION_DESCRIPTOR_SIZE as u64
2610                + u64::try_from(xhash.len()).expect("usize fits u64"),
2611        ))?;
2612        writer.write_all(&xhash)?;
2613    }
2614    writer.write_all(&section_desc(
2615        context.terminal_section.ewf1_type(),
2616        done_desc_offset,
2617        ewf1::SECTION_DESCRIPTOR_SIZE as u64,
2618    ))?;
2619
2620    Ok(())
2621}
2622
2623#[derive(Debug, Clone, Copy)]
2624struct Ewf1SegmentWriteContext {
2625    segment_number: u16,
2626    chunk_count: u32,
2627    sector_count: u64,
2628    sections: Ewf1SegmentSections,
2629    terminal_section: TerminalSection,
2630}
2631
2632#[derive(Debug, Clone, Copy)]
2633struct Ewf1SegmentSections {
2634    bits: u8,
2635}
2636
2637impl Ewf1SegmentSections {
2638    const HEADER: u8 = 1 << 0;
2639    const DIGEST: u8 = 1 << 1;
2640    const ERRORS: u8 = 1 << 2;
2641    const SESSIONS: u8 = 1 << 3;
2642
2643    fn for_segment(is_first: bool, is_last: bool) -> Self {
2644        let mut bits = 0;
2645        if is_first {
2646            bits |= Self::HEADER | Self::ERRORS | Self::SESSIONS;
2647        }
2648        if is_last {
2649            bits |= Self::DIGEST;
2650        }
2651        Self { bits }
2652    }
2653
2654    fn contains(self, flag: u8) -> bool {
2655        self.bits & flag != 0
2656    }
2657}
2658
2659fn ewf1_signature(format: WriteFormat) -> &'static [u8; 8] {
2660    match format {
2661        WriteFormat::Ewf1Physical | WriteFormat::Ewf1Smart => &ewf1::EVF_SIGNATURE,
2662        WriteFormat::Ewf1Logical => &ewf1::LVF_SIGNATURE,
2663        WriteFormat::Ewf2Physical | WriteFormat::Ewf2Logical => {
2664            unreachable!("EWF2 formats are not EWF1")
2665        }
2666    }
2667}
2668
2669fn is_ewf2_format(format: WriteFormat) -> bool {
2670    matches!(format, WriteFormat::Ewf2Physical | WriteFormat::Ewf2Logical)
2671}
2672
2673fn write_format_is_physical(format: WriteFormat) -> bool {
2674    matches!(
2675        format,
2676        WriteFormat::Ewf1Physical | WriteFormat::Ewf1Smart | WriteFormat::Ewf2Physical
2677    )
2678}
2679
2680fn writer_media_flags(format: WriteFormat, profile: WriteMediaProfile) -> MediaFlags {
2681    MediaFlags {
2682        physical: write_format_is_physical(format),
2683        fastbloc: profile.fastbloc,
2684        tableau: profile.tableau,
2685    }
2686}
2687
2688#[derive(Debug, Clone, Copy)]
2689struct Ewf2SegmentWriteContext {
2690    segment_number: u32,
2691    first_chunk: u64,
2692    total_chunk_count: u32,
2693    sector_count: u64,
2694    terminal_section_type: u32,
2695}
2696
2697#[derive(Debug, Clone, Copy)]
2698struct Ewf2SectionPlan {
2699    data_offset: u64,
2700    previous_desc_offset: u64,
2701}
2702
2703fn plan_ewf2_section(
2704    current_offset: &mut u64,
2705    previous_desc_offset: &mut u64,
2706    data_size: u64,
2707    label: &str,
2708) -> Result<Ewf2SectionPlan> {
2709    let data_offset = *current_offset;
2710    let desc_offset = checked_add(data_offset, data_size, label)?;
2711    let section = Ewf2SectionPlan {
2712        data_offset,
2713        previous_desc_offset: *previous_desc_offset,
2714    };
2715    *current_offset = checked_add(
2716        desc_offset,
2717        ewf2::SECTION_DESCRIPTOR_SIZE as u64,
2718        "EWF2 next section",
2719    )?;
2720    *previous_desc_offset = desc_offset;
2721    Ok(section)
2722}
2723
2724fn write_ewf2_section<W: Write>(
2725    writer: &mut W,
2726    section_type: u32,
2727    payload: &[u8],
2728    section: Ewf2SectionPlan,
2729) -> Result<()> {
2730    writer.write_all(payload)?;
2731    writer.write_all(&ewf2_section_desc(
2732        section_type,
2733        u64::try_from(payload.len()).expect("usize fits u64"),
2734        section.previous_desc_offset,
2735    ))?;
2736    Ok(())
2737}
2738
2739fn write_ewf2_segment<W: Write>(
2740    writer: &mut W,
2741    spool: &mut ChunkSpool,
2742    chunks: &[ChunkDescriptor],
2743    options: &WriteOptions,
2744    context: Ewf2SegmentWriteContext,
2745) -> Result<()> {
2746    let local_chunk_count = u32::try_from(chunks.len())
2747        .map_err(|_| EwfError::Unsupported("EWF2 writer segment chunk count exceeds u32".into()))?;
2748    let device_information_payload = ewf2_device_information_payload(
2749        options,
2750        context.sector_count,
2751        u64::from(context.total_chunk_count),
2752    );
2753    let device_information =
2754        ewf2_device_information_section_payload(&device_information_payload, options)?;
2755    let case_data_payload = ewf2_case_data_payload(options, u64::from(context.total_chunk_count));
2756    let case_data = ewf2_metadata_section_payload(
2757        &case_data_payload,
2758        options.compression,
2759        options.compression_values.level,
2760    )?;
2761    let error_table = if context.segment_number == 1 {
2762        ewf2_error_table_payload(&options.acquisition_errors)?
2763    } else {
2764        None
2765    };
2766    let session_table = if context.segment_number == 1 {
2767        ewf2_session_table_payload(&options.sessions, &options.tracks)?
2768    } else {
2769        None
2770    };
2771    let memory_extents_table = if context.segment_number == 1 {
2772        ewf2_memory_extents_table_payload(&options.memory_extents)
2773    } else {
2774        None
2775    };
2776    let increment_data: &[Vec<u8>] = if context.segment_number == 1 {
2777        &options.ewf2_increment_data
2778    } else {
2779        &[]
2780    };
2781    // libewf completes the logical catalog while opening this segment and
2782    // rejects subsequent segments once that catalog exists. Keep all logical
2783    // catalog sections in the terminal segment, as with EWF1 ltree.
2784    let writes_hash_sections = context.terminal_section_type == EWF2_DONE_SECTION;
2785    let single_files_data = if writes_hash_sections {
2786        options
2787            .single_files
2788            .as_ref()
2789            .map(ewf2_single_files_data_payload)
2790            .transpose()?
2791    } else {
2792        None
2793    };
2794    let single_files_table_0x21 = if writes_hash_sections {
2795        ewf2_single_files_aux_u64_table_payload(
2796            &options.ewf2_single_files_tables.table_0x21_entries,
2797        )
2798    } else {
2799        None
2800    };
2801    let single_files_md5_hash_table = if writes_hash_sections {
2802        ewf2_single_files_md5_hash_table_payload(&options.ewf2_single_files_tables.md5_hashes)
2803    } else {
2804        None
2805    };
2806    let single_files_table_0x23 = if writes_hash_sections {
2807        ewf2_single_files_aux_u64_table_payload(
2808            &options.ewf2_single_files_tables.table_0x23_entries,
2809        )
2810    } else {
2811        None
2812    };
2813    let md5_hash = writes_hash_sections
2814        .then(|| options.hashes.md5.map(ewf2_hash_payload))
2815        .flatten();
2816    let sha1_hash = writes_hash_sections
2817        .then(|| options.hashes.sha1.map(ewf2_hash_payload))
2818        .flatten();
2819    let final_information = if writes_hash_sections {
2820        options.ewf2_final_information.as_deref()
2821    } else {
2822        None
2823    };
2824    let analytical_data = if writes_hash_sections {
2825        options
2826            .ewf2_analytical_data
2827            .as_deref()
2828            .map(|data| {
2829                ewf2_string_section_payload(
2830                    data,
2831                    options.compression,
2832                    options.compression_values.level,
2833                )
2834            })
2835            .transpose()?
2836    } else {
2837        None
2838    };
2839    let restart_data = if writes_hash_sections {
2840        options
2841            .ewf2_restart_data
2842            .as_deref()
2843            .map(|data| {
2844                ewf2_string_section_payload(
2845                    data,
2846                    options.compression,
2847                    options.compression_values.level,
2848                )
2849            })
2850            .transpose()?
2851    } else {
2852        None
2853    };
2854    let table_entry_bytes = u64::from(local_chunk_count)
2855        .checked_mul(ewf2::TABLE_ENTRY_SIZE as u64)
2856        .ok_or_else(|| EwfError::Malformed("writer EWF2 table entry size overflow".into()))?;
2857    let table_data_size = (EWF2_TABLE_HEADER_V2_SIZE as u64)
2858        .checked_add(table_entry_bytes)
2859        .and_then(|value| value.checked_add(EWF2_TABLE_FOOTER_SIZE as u64))
2860        .ok_or_else(|| EwfError::Malformed("writer EWF2 table size overflow".into()))?;
2861    let chunk_payload_size = chunks.iter().try_fold(0_u64, |total, chunk| {
2862        total
2863            .checked_add(chunk.data_size)
2864            .ok_or_else(|| EwfError::Malformed("writer EWF2 chunk payload size overflow".into()))
2865    })?;
2866
2867    let mut current_offset = ewf2::FILE_HEADER_SIZE as u64;
2868    let mut previous_desc_offset = 0_u64;
2869
2870    let device_section = plan_ewf2_section(
2871        &mut current_offset,
2872        &mut previous_desc_offset,
2873        u64::try_from(device_information.len()).expect("usize fits u64"),
2874        "EWF2 device information",
2875    )?;
2876    let case_section = plan_ewf2_section(
2877        &mut current_offset,
2878        &mut previous_desc_offset,
2879        u64::try_from(case_data.len()).expect("usize fits u64"),
2880        "EWF2 case data",
2881    )?;
2882    let error_section = error_table
2883        .as_ref()
2884        .map(|payload| {
2885            plan_ewf2_section(
2886                &mut current_offset,
2887                &mut previous_desc_offset,
2888                u64::try_from(payload.len()).expect("usize fits u64"),
2889                "EWF2 error table",
2890            )
2891        })
2892        .transpose()?;
2893    let session_section = session_table
2894        .as_ref()
2895        .map(|payload| {
2896            plan_ewf2_section(
2897                &mut current_offset,
2898                &mut previous_desc_offset,
2899                u64::try_from(payload.len()).expect("usize fits u64"),
2900                "EWF2 session table",
2901            )
2902        })
2903        .transpose()?;
2904    let memory_extents_section = memory_extents_table
2905        .as_ref()
2906        .map(|payload| {
2907            plan_ewf2_section(
2908                &mut current_offset,
2909                &mut previous_desc_offset,
2910                u64::try_from(payload.len()).expect("usize fits u64"),
2911                "EWF2 memory extents table",
2912            )
2913        })
2914        .transpose()?;
2915
2916    let mut increment_sections = Vec::with_capacity(increment_data.len());
2917    for payload in increment_data {
2918        increment_sections.push(plan_ewf2_section(
2919            &mut current_offset,
2920            &mut previous_desc_offset,
2921            u64::try_from(payload.len()).expect("usize fits u64"),
2922            "EWF2 increment data",
2923        )?);
2924    }
2925
2926    let single_files_section = single_files_data
2927        .as_ref()
2928        .map(|payload| {
2929            plan_ewf2_section(
2930                &mut current_offset,
2931                &mut previous_desc_offset,
2932                u64::try_from(payload.len()).expect("usize fits u64"),
2933                "EWF2 single files data",
2934            )
2935        })
2936        .transpose()?;
2937    let single_files_table_0x21_section = single_files_table_0x21
2938        .as_ref()
2939        .map(|payload| {
2940            plan_ewf2_section(
2941                &mut current_offset,
2942                &mut previous_desc_offset,
2943                u64::try_from(payload.len()).expect("usize fits u64"),
2944                "EWF2 single files 0x21 table",
2945            )
2946        })
2947        .transpose()?;
2948    let single_files_md5_hash_table_section = single_files_md5_hash_table
2949        .as_ref()
2950        .map(|payload| {
2951            plan_ewf2_section(
2952                &mut current_offset,
2953                &mut previous_desc_offset,
2954                u64::try_from(payload.len()).expect("usize fits u64"),
2955                "EWF2 single files MD5 hash table",
2956            )
2957        })
2958        .transpose()?;
2959    let single_files_table_0x23_section = single_files_table_0x23
2960        .as_ref()
2961        .map(|payload| {
2962            plan_ewf2_section(
2963                &mut current_offset,
2964                &mut previous_desc_offset,
2965                u64::try_from(payload.len()).expect("usize fits u64"),
2966                "EWF2 single files 0x23 table",
2967            )
2968        })
2969        .transpose()?;
2970
2971    let sectors_section = plan_ewf2_section(
2972        &mut current_offset,
2973        &mut previous_desc_offset,
2974        chunk_payload_size,
2975        "EWF2 sector data",
2976    )?;
2977    let table_section = plan_ewf2_section(
2978        &mut current_offset,
2979        &mut previous_desc_offset,
2980        table_data_size,
2981        "EWF2 sector table",
2982    )?;
2983    let md5_section = md5_hash
2984        .as_ref()
2985        .map(|payload| {
2986            plan_ewf2_section(
2987                &mut current_offset,
2988                &mut previous_desc_offset,
2989                u64::try_from(payload.len()).expect("usize fits u64"),
2990                "EWF2 MD5 hash",
2991            )
2992        })
2993        .transpose()?;
2994    let sha1_section = sha1_hash
2995        .as_ref()
2996        .map(|payload| {
2997            plan_ewf2_section(
2998                &mut current_offset,
2999                &mut previous_desc_offset,
3000                u64::try_from(payload.len()).expect("usize fits u64"),
3001                "EWF2 SHA1 hash",
3002            )
3003        })
3004        .transpose()?;
3005    let final_information_section = final_information
3006        .map(|payload| {
3007            plan_ewf2_section(
3008                &mut current_offset,
3009                &mut previous_desc_offset,
3010                u64::try_from(payload.len()).expect("usize fits u64"),
3011                "EWF2 final information",
3012            )
3013        })
3014        .transpose()?;
3015    let analytical_section = analytical_data
3016        .as_ref()
3017        .map(|payload| {
3018            plan_ewf2_section(
3019                &mut current_offset,
3020                &mut previous_desc_offset,
3021                u64::try_from(payload.len()).expect("usize fits u64"),
3022                "EWF2 analytical data",
3023            )
3024        })
3025        .transpose()?;
3026    let restart_section = restart_data
3027        .as_ref()
3028        .map(|payload| {
3029            plan_ewf2_section(
3030                &mut current_offset,
3031                &mut previous_desc_offset,
3032                u64::try_from(payload.len()).expect("usize fits u64"),
3033                "EWF2 restart data",
3034            )
3035        })
3036        .transpose()?;
3037    let terminal_section = plan_ewf2_section(
3038        &mut current_offset,
3039        &mut previous_desc_offset,
3040        0,
3041        "EWF2 terminal",
3042    )?;
3043
3044    writer.write_all(ewf2_signature(options.format))?;
3045    writer.write_all(&[2, 1])?;
3046    writer.write_all(&ewf2_compression_method(options.compression).to_le_bytes())?;
3047    writer.write_all(&context.segment_number.to_le_bytes())?;
3048    writer.write_all(&options.set_identifier.unwrap_or([0; 16]))?;
3049
3050    write_ewf2_section(
3051        writer,
3052        EWF2_DEVICE_INFORMATION_SECTION,
3053        &device_information,
3054        device_section,
3055    )?;
3056
3057    write_ewf2_section(writer, EWF2_CASE_DATA_SECTION, &case_data, case_section)?;
3058
3059    if let (Some(error_table), Some(section)) = (&error_table, error_section) {
3060        write_ewf2_section(writer, EWF2_ERROR_TABLE_SECTION, error_table, section)?;
3061    }
3062
3063    if let (Some(session_table), Some(section)) = (&session_table, session_section) {
3064        write_ewf2_section(writer, EWF2_SESSION_TABLE_SECTION, session_table, section)?;
3065    }
3066
3067    if let (Some(memory_extents_table), Some(section)) =
3068        (&memory_extents_table, memory_extents_section)
3069    {
3070        write_ewf2_section(
3071            writer,
3072            EWF2_MEMORY_EXTENTS_TABLE_SECTION,
3073            memory_extents_table,
3074            section,
3075        )?;
3076    }
3077
3078    for (payload, section) in increment_data.iter().zip(&increment_sections) {
3079        write_ewf2_section(writer, EWF2_INCREMENT_DATA_SECTION, payload, *section)?;
3080    }
3081
3082    if let (Some(single_files_data), Some(section)) = (&single_files_data, single_files_section) {
3083        write_ewf2_section(
3084            writer,
3085            EWF2_SINGLE_FILES_DATA_SECTION,
3086            single_files_data,
3087            section,
3088        )?;
3089    }
3090
3091    if let (Some(single_files_table_0x21), Some(section)) =
3092        (&single_files_table_0x21, single_files_table_0x21_section)
3093    {
3094        write_ewf2_section(
3095            writer,
3096            EWF2_SINGLE_FILES_TABLE_SECTION,
3097            single_files_table_0x21,
3098            section,
3099        )?;
3100    }
3101
3102    if let (Some(single_files_md5_hash_table), Some(section)) = (
3103        &single_files_md5_hash_table,
3104        single_files_md5_hash_table_section,
3105    ) {
3106        write_ewf2_section(
3107            writer,
3108            EWF2_SINGLE_FILES_MD5_HASH_TABLE_SECTION,
3109            single_files_md5_hash_table,
3110            section,
3111        )?;
3112    }
3113
3114    if let (Some(single_files_table_0x23), Some(section)) =
3115        (&single_files_table_0x23, single_files_table_0x23_section)
3116    {
3117        write_ewf2_section(
3118            writer,
3119            EWF2_SINGLE_FILES_UNKNOWN_TABLE_SECTION,
3120            single_files_table_0x23,
3121            section,
3122        )?;
3123    }
3124
3125    for chunk in chunks {
3126        spool.copy_chunk_to(chunk, writer)?;
3127    }
3128    writer.write_all(&ewf2_section_desc(
3129        EWF2_SECTOR_DATA_SECTION,
3130        chunk_payload_size,
3131        sectors_section.previous_desc_offset,
3132    ))?;
3133
3134    let table_capacity = usize::try_from(table_data_size)
3135        .map_err(|_| EwfError::Malformed("writer EWF2 table size does not fit usize".into()))?;
3136    let mut table_data = Vec::with_capacity(table_capacity);
3137    table_data.extend_from_slice(&ewf2_table_header(context.first_chunk, local_chunk_count));
3138    let table_entries_start = table_data.len();
3139    let mut chunk_offset = sectors_section.data_offset;
3140    for chunk in chunks {
3141        let chunk_size = u32::try_from(chunk.data_size)
3142            .map_err(|_| EwfError::Unsupported("EWF2 writer chunk data size exceeds u32".into()))?;
3143        table_data.extend_from_slice(&ewf2_table_entry(
3144            chunk.pattern_fill.unwrap_or(chunk_offset),
3145            chunk_size,
3146            chunk.compressed,
3147            chunk.has_checksum,
3148            chunk.pattern_fill.is_some(),
3149        ));
3150        chunk_offset = checked_add(chunk_offset, chunk.data_size, "EWF2 chunk data offset")?;
3151    }
3152    let entries_checksum = adler32(&table_data[table_entries_start..]);
3153    table_data.extend_from_slice(&entries_checksum.to_le_bytes());
3154    table_data.extend_from_slice(&[0; EWF2_TABLE_FOOTER_SIZE - 4]);
3155    write_ewf2_section(
3156        writer,
3157        EWF2_SECTOR_TABLE_SECTION,
3158        &table_data,
3159        table_section,
3160    )?;
3161
3162    if let (Some(md5_hash), Some(section)) = (&md5_hash, md5_section) {
3163        write_ewf2_section(writer, EWF2_MD5_HASH_SECTION, md5_hash, section)?;
3164    }
3165    if let (Some(sha1_hash), Some(section)) = (&sha1_hash, sha1_section) {
3166        write_ewf2_section(writer, EWF2_SHA1_HASH_SECTION, sha1_hash, section)?;
3167    }
3168    if let (Some(final_information), Some(section)) = (final_information, final_information_section)
3169    {
3170        write_ewf2_section(
3171            writer,
3172            EWF2_FINAL_INFORMATION_SECTION,
3173            final_information,
3174            section,
3175        )?;
3176    }
3177    if let (Some(analytical_data), Some(section)) = (&analytical_data, analytical_section) {
3178        write_ewf2_section(
3179            writer,
3180            EWF2_ANALYTICAL_DATA_SECTION,
3181            analytical_data,
3182            section,
3183        )?;
3184    }
3185    if let (Some(restart_data), Some(section)) = (&restart_data, restart_section) {
3186        write_ewf2_section(writer, EWF2_RESTART_DATA_SECTION, restart_data, section)?;
3187    }
3188    writer.write_all(&ewf2_section_desc(
3189        context.terminal_section_type,
3190        0,
3191        terminal_section.previous_desc_offset,
3192    ))?;
3193
3194    Ok(())
3195}
3196
3197fn ewf2_signature(format: WriteFormat) -> &'static [u8; 8] {
3198    match format {
3199        WriteFormat::Ewf2Physical => &ewf2::EX01_SIGNATURE,
3200        WriteFormat::Ewf2Logical => &ewf2::LEF2_SIGNATURE,
3201        WriteFormat::Ewf1Physical | WriteFormat::Ewf1Logical | WriteFormat::Ewf1Smart => {
3202            unreachable!("EWF1 formats are not EWF2")
3203        }
3204    }
3205}
3206
3207fn ewf2_compression_method(compression: WriteCompression) -> u16 {
3208    match compression {
3209        // Common EWF readers reject EWF2 segment headers that use COMPRESSION_NONE.
3210        // Raw chunks are still represented by table flags; the header method must
3211        // name a supported compression family for the segment.
3212        WriteCompression::None | WriteCompression::Zlib => 1,
3213        WriteCompression::Bzip2 => 2,
3214    }
3215}
3216
3217fn zlib_compression(level: WriteCompressionLevel) -> ZlibCompression {
3218    match level {
3219        WriteCompressionLevel::Default => ZlibCompression::default(),
3220        WriteCompressionLevel::None => ZlibCompression::none(),
3221        WriteCompressionLevel::Fast => ZlibCompression::fast(),
3222        WriteCompressionLevel::Best => ZlibCompression::best(),
3223    }
3224}
3225
3226fn empty_block_zlib_compression(level: WriteCompressionLevel) -> ZlibCompression {
3227    match level {
3228        WriteCompressionLevel::None => ZlibCompression::default(),
3229        level => zlib_compression(level),
3230    }
3231}
3232
3233fn bzip2_compression(level: WriteCompressionLevel) -> Result<Bzip2Compression> {
3234    match level {
3235        WriteCompressionLevel::Default => Ok(Bzip2Compression::default()),
3236        WriteCompressionLevel::None => Err(EwfError::Unsupported(
3237            "BZip2 writer compression does not support none compression level".into(),
3238        )),
3239        WriteCompressionLevel::Fast => Ok(Bzip2Compression::fast()),
3240        WriteCompressionLevel::Best => Ok(Bzip2Compression::best()),
3241    }
3242}
3243
3244fn ewf2_metadata_section_payload(
3245    payload: &[u8],
3246    compression: WriteCompression,
3247    compression_level: WriteCompressionLevel,
3248) -> Result<Vec<u8>> {
3249    match compression {
3250        WriteCompression::None | WriteCompression::Zlib => {
3251            let mut encoder = ZlibEncoder::new(Vec::new(), zlib_compression(compression_level));
3252            encoder.write_all(payload)?;
3253            Ok(encoder.finish()?)
3254        }
3255        WriteCompression::Bzip2 => {
3256            let mut encoder = BzEncoder::new(Vec::new(), bzip2_compression(compression_level)?);
3257            encoder.write_all(payload)?;
3258            Ok(encoder.finish()?)
3259        }
3260    }
3261}
3262
3263fn ewf2_device_information_section_payload(
3264    payload: &[u8],
3265    options: &WriteOptions,
3266) -> Result<Vec<u8>> {
3267    let compression = if matches!(options.compression, WriteCompression::Bzip2) {
3268        WriteCompression::Zlib
3269    } else {
3270        options.compression
3271    };
3272    ewf2_metadata_section_payload(payload, compression, options.compression_values.level)
3273}
3274
3275fn ewf2_device_information_payload(
3276    options: &WriteOptions,
3277    sector_count: u64,
3278    chunk_count: u64,
3279) -> Vec<u8> {
3280    let physical = u8::from(options.format == WriteFormat::Ewf2Physical);
3281    let mut names = vec![
3282        "sn", "md", "lb", "ts", "hs", "dc", "dt", "pid", "rs", "ls", "bp", "ph", "sc", "tb",
3283    ];
3284    let media_type = options
3285        .media_profile
3286        .media_type
3287        .or(Some(MediaType::Removable))
3288        .map(ewf2_media_type_value)
3289        .map(|value| value.to_string())
3290        .unwrap_or_default();
3291    let mut values = vec![
3292        ewf2_device_header_value(&options.metadata, "serial_number", &["sn"]),
3293        ewf2_device_header_value(&options.metadata, "model", &["md"]),
3294        ewf2_device_header_value(&options.metadata, "device_label", &["lb", "l"]),
3295        sector_count.to_string(),
3296        String::new(),
3297        String::new(),
3298        media_type,
3299        ewf2_device_header_value(&options.metadata, "process_identifier", &["pid"]),
3300        String::new(),
3301        String::new(),
3302        options.bytes_per_sector.to_string(),
3303        physical.to_string(),
3304        options.sectors_per_chunk.to_string(),
3305        chunk_count.to_string(),
3306    ];
3307    if let Some(error_granularity) = options.media_profile.error_granularity {
3308        names.push("gr");
3309        values.push(error_granularity.to_string());
3310    }
3311    let write_blocker_flags =
3312        u64::from(options.media_profile.fastbloc) | (u64::from(options.media_profile.tableau) << 1);
3313    if write_blocker_flags != 0 {
3314        names.push("wb");
3315        values.push(write_blocker_flags.to_string());
3316    }
3317
3318    let text = format!("1\nmain\n{}\n{}\n\n", names.join("\t"), values.join("\t"));
3319    utf16le_with_bom(&text)
3320}
3321
3322fn ewf2_device_header_value(metadata: &EwfMetadata, identifier: &str, aliases: &[&str]) -> String {
3323    metadata
3324        .header_value(identifier)
3325        .or_else(|| {
3326            aliases
3327                .iter()
3328                .find_map(|alias| metadata.header_values.get(*alias).map(String::as_str))
3329        })
3330        .map(sanitize_header_value)
3331        .unwrap_or_default()
3332}
3333
3334fn ewf2_media_type_value(media_type: MediaType) -> char {
3335    match media_type {
3336        MediaType::Removable => 'r',
3337        MediaType::Fixed => 'f',
3338        MediaType::Optical => 'c',
3339        MediaType::SingleFiles => 'l',
3340        MediaType::Memory => 'm',
3341        MediaType::Unknown(value) => char::from(value),
3342    }
3343}
3344
3345fn ewf2_case_data_payload(options: &WriteOptions, chunk_count: u64) -> Vec<u8> {
3346    let metadata = &options.metadata;
3347    let write_blocker_flags =
3348        u64::from(options.media_profile.fastbloc) | (u64::from(options.media_profile.tableau) << 1);
3349    let compression_method = ewf2_case_data_header_value(metadata, "compression_method", &["cp"]);
3350    let error_granularity = options.media_profile.error_granularity.map_or_else(
3351        || {
3352            ewf2_case_data_header_value(metadata, "error_granularity", &["gr"])
3353                .unwrap_or_else(|| "0".to_string())
3354        },
3355        |value| value.to_string(),
3356    );
3357    let write_blocker = if write_blocker_flags != 0 {
3358        write_blocker_flags.to_string()
3359    } else {
3360        ewf2_case_data_header_value(metadata, "write_blocker", &["wb"]).unwrap_or_default()
3361    };
3362    let fields = [
3363        (
3364            "nm",
3365            ewf2_case_data_header_value(metadata, "description", &["nm", "de"]).unwrap_or_default(),
3366        ),
3367        (
3368            "cn",
3369            ewf2_case_data_header_value(metadata, "case_number", &["cn"]).unwrap_or_default(),
3370        ),
3371        (
3372            "en",
3373            ewf2_case_data_header_value(metadata, "evidence_number", &["en"]).unwrap_or_default(),
3374        ),
3375        (
3376            "ex",
3377            ewf2_case_data_header_value(metadata, "examiner_name", &["ex"]).unwrap_or_default(),
3378        ),
3379        (
3380            "nt",
3381            ewf2_case_data_header_value(metadata, "notes", &["nt"]).unwrap_or_default(),
3382        ),
3383        (
3384            "av",
3385            ewf2_case_data_header_value(metadata, "acquiry_software_version", &["av"])
3386                .unwrap_or_default(),
3387        ),
3388        (
3389            "os",
3390            ewf2_case_data_header_value(metadata, "acquiry_operating_system", &["os", "ov"])
3391                .unwrap_or_default(),
3392        ),
3393        (
3394            "tt",
3395            ewf2_case_data_header_value(metadata, "system_date", &["tt", "sd"]).unwrap_or_default(),
3396        ),
3397        (
3398            "at",
3399            ewf2_case_data_header_value(metadata, "acquiry_date", &["at", "ad"])
3400                .unwrap_or_default(),
3401        ),
3402        ("tb", chunk_count.to_string()),
3403        ("cp", compression_method.unwrap_or_default()),
3404        ("sb", options.sectors_per_chunk.to_string()),
3405        ("gr", error_granularity),
3406        ("wb", write_blocker),
3407    ];
3408    let mut names = fields
3409        .iter()
3410        .map(|(name, _)| (*name).to_string())
3411        .collect::<Vec<_>>();
3412    let mut values = fields
3413        .into_iter()
3414        .map(|(_, value)| value)
3415        .collect::<Vec<_>>();
3416    if let Some(acquisition_software) = metadata.acquisition_software.as_deref() {
3417        names.push("acquiry_software".to_string());
3418        values.push(sanitize_header_value(acquisition_software));
3419    }
3420    if let Some(password) = metadata.password.as_deref() {
3421        names.push("password".to_string());
3422        values.push(sanitize_header_value(password));
3423    }
3424    for (name, value) in &metadata.header_values {
3425        let tag = ewf2_case_data_tag(name);
3426        if names.iter().any(|existing| existing == tag) {
3427            continue;
3428        }
3429        names.push(tag.to_string());
3430        values.push(sanitize_header_value(value));
3431    }
3432
3433    let text = format!("1\nmain\n{}\n{}\n\n", names.join("\t"), values.join("\t"));
3434    utf16le_with_bom(&text)
3435}
3436
3437fn ewf2_case_data_header_value(
3438    metadata: &EwfMetadata,
3439    identifier: &str,
3440    aliases: &[&str],
3441) -> Option<String> {
3442    metadata
3443        .header_value(identifier)
3444        .or_else(|| {
3445            aliases
3446                .iter()
3447                .find_map(|alias| metadata.header_values.get(*alias).map(String::as_str))
3448        })
3449        .map(sanitize_header_value)
3450}
3451
3452fn ewf2_case_data_tag(identifier: &str) -> &str {
3453    match identifier {
3454        "acquiry_date" | "ad" => "at",
3455        "acquiry_operating_system" | "ov" => "os",
3456        "acquiry_software_version" => "av",
3457        "case_number" => "cn",
3458        "compression_method" => "cp",
3459        "description" | "de" => "nm",
3460        "evidence_number" => "en",
3461        "examiner_name" => "ex",
3462        "error_granularity" => "gr",
3463        "notes" => "nt",
3464        "number_of_chunks" => "tb",
3465        "sectors_per_chunk" => "sb",
3466        "system_date" | "sd" => "tt",
3467        "write_blocker" => "wb",
3468        _ => identifier,
3469    }
3470}
3471
3472fn ewf2_memory_extents_table_payload(extents: &[MemoryExtent]) -> Option<Vec<u8>> {
3473    if extents.is_empty() {
3474        return None;
3475    }
3476
3477    let mut payload = Vec::with_capacity(extents.len() * 16);
3478    for extent in extents {
3479        payload.extend_from_slice(&extent.start_page.to_le_bytes());
3480        payload.extend_from_slice(&extent.page_count.to_le_bytes());
3481    }
3482    Some(payload)
3483}
3484
3485fn ewf2_single_files_aux_u64_table_payload(entries: &[u64]) -> Option<Vec<u8>> {
3486    if entries.is_empty() {
3487        return None;
3488    }
3489
3490    let mut entry_data = Vec::with_capacity(entries.len() * 8);
3491    for entry in entries {
3492        entry_data.extend_from_slice(&entry.to_le_bytes());
3493    }
3494    Some(ewf2_single_files_aux_table_payload(
3495        entries.len(),
3496        &entry_data,
3497    ))
3498}
3499
3500fn ewf2_single_files_md5_hash_table_payload(hashes: &[[u8; 16]]) -> Option<Vec<u8>> {
3501    if hashes.is_empty() {
3502        return None;
3503    }
3504
3505    let mut entry_data = Vec::with_capacity(hashes.len() * 16);
3506    for hash in hashes {
3507        entry_data.extend_from_slice(hash);
3508    }
3509    Some(ewf2_single_files_aux_table_payload(
3510        hashes.len(),
3511        &entry_data,
3512    ))
3513}
3514
3515fn ewf2_single_files_aux_table_payload(entry_count: usize, entry_data: &[u8]) -> Vec<u8> {
3516    let mut payload = Vec::with_capacity(32 + entry_data.len() + 16);
3517    let entry_count = u32::try_from(entry_count).expect("entry count fits u32");
3518    payload.extend_from_slice(&entry_count.to_le_bytes());
3519    payload.extend_from_slice(&[0; 12]);
3520    let header_checksum = adler32(&payload);
3521    payload.extend_from_slice(&header_checksum.to_le_bytes());
3522    payload.extend_from_slice(&[0; 12]);
3523    payload.extend_from_slice(entry_data);
3524    let entries_checksum = adler32(entry_data);
3525    payload.extend_from_slice(&entries_checksum.to_le_bytes());
3526    payload.extend_from_slice(&[0; 12]);
3527    payload
3528}
3529
3530fn ewf2_string_section_payload(
3531    text: &str,
3532    compression: WriteCompression,
3533    compression_level: WriteCompressionLevel,
3534) -> Result<Vec<u8>> {
3535    ewf2_metadata_section_payload(&utf16le_with_bom(text), compression, compression_level)
3536}
3537
3538fn ewf1_ltree_payload(info: &SingleFilesInfo) -> Result<Vec<u8>> {
3539    let single_files_data = ewf2_single_files_data_payload(info)?;
3540    let mut payload = Vec::with_capacity(EWF1_LTREE_HEADER_SIZE + single_files_data.len());
3541    let mut hasher = Md5::new();
3542    hasher.update(&single_files_data);
3543    payload.extend_from_slice(&hasher.finalize());
3544    payload.extend_from_slice(
3545        &u64::try_from(single_files_data.len())
3546            .expect("usize fits u64")
3547            .to_le_bytes(),
3548    );
3549    payload.extend_from_slice(&0_u32.to_le_bytes());
3550    payload.extend_from_slice(&[0; 20]);
3551    let checksum = adler32(&payload[..EWF1_LTREE_HEADER_SIZE]);
3552    payload[24..28].copy_from_slice(&checksum.to_le_bytes());
3553    payload.extend_from_slice(&single_files_data);
3554    Ok(payload)
3555}
3556
3557fn ewf2_single_files_data_payload(info: &SingleFilesInfo) -> Result<Vec<u8>> {
3558    let source_ids = SingleFileSourceIds::new(&info.sources)?;
3559    let mut lines = Vec::new();
3560    lines.push("5".to_owned());
3561    append_single_file_record_category(&mut lines, info)?;
3562    append_single_file_permission_category(&mut lines, &info.permission_groups);
3563    append_single_file_source_category(&mut lines, &info.sources)?;
3564    append_single_file_subject_category(&mut lines, &info.subjects);
3565    append_single_file_entry_category(&mut lines, &info.root, &source_ids)?;
3566    // Terminate the final empty category-separator line, as well as the last
3567    // record line. Streaming consumers must not need to read past the payload.
3568    lines.push(String::new());
3569    Ok(utf16le_with_bom(&lines.join("\n")))
3570}
3571
3572struct SingleFileSourceIds {
3573    remapped: BTreeMap<i32, i32>,
3574}
3575
3576impl SingleFileSourceIds {
3577    fn new(sources: &[SingleFileSource]) -> Result<Self> {
3578        let mut remapped = BTreeMap::new();
3579        let children = sources.get(1..).unwrap_or(&[]);
3580        for (index, source) in children.iter().enumerate() {
3581            if let Some(identifier) = source.identifier {
3582                remapped
3583                    .entry(identifier)
3584                    .or_insert(source_child_identifier(index)?);
3585            }
3586        }
3587        Ok(Self { remapped })
3588    }
3589
3590    fn entry_identifier(&self, identifier: Option<i32>) -> Option<i32> {
3591        match identifier {
3592            Some(identifier) if identifier > 0 => {
3593                self.remapped.get(&identifier).copied().or(Some(identifier))
3594            }
3595            _ => identifier,
3596        }
3597    }
3598}
3599
3600fn source_child_identifier(index: usize) -> Result<i32> {
3601    let identifier = index
3602        .checked_add(1)
3603        .ok_or_else(|| EwfError::Malformed("single files source identifier overflow".into()))?;
3604    i32::try_from(identifier).map_err(|_| {
3605        EwfError::Unsupported("single files source count exceeds supported identifier range".into())
3606    })
3607}
3608
3609fn append_single_file_record_category(
3610    lines: &mut Vec<String>,
3611    info: &SingleFilesInfo,
3612) -> Result<()> {
3613    lines.push("rec".to_owned());
3614    lines.push("tb".to_owned());
3615    lines.push(single_files_record_data_size(info)?.to_string());
3616    lines.push(String::new());
3617    Ok(())
3618}
3619
3620fn single_files_record_data_size(info: &SingleFilesInfo) -> Result<u64> {
3621    Ok(info.data_size.max(single_file_entry_data_size(&info.root)?))
3622}
3623
3624fn single_file_entry_data_size(entry: &SingleFileEntry) -> Result<u64> {
3625    let mut data_size = entry.size.unwrap_or(0);
3626    for extent in &entry.extents {
3627        if extent.sparse {
3628            continue;
3629        }
3630        let extent_end = extent
3631            .data_offset
3632            .checked_add(extent.data_size)
3633            .ok_or_else(|| EwfError::Malformed("single files extent end overflow".into()))?;
3634        data_size = data_size.max(extent_end);
3635    }
3636    for child in &entry.children {
3637        data_size = data_size.max(single_file_entry_data_size(child)?);
3638    }
3639    Ok(data_size)
3640}
3641
3642fn append_single_file_entry_category(
3643    lines: &mut Vec<String>,
3644    root: &SingleFileEntry,
3645    source_ids: &SingleFileSourceIds,
3646) -> Result<()> {
3647    let include_guid = single_file_entry_tree_has_guid(root);
3648    let entry_types = single_file_entry_types(include_guid);
3649    lines.push("entry".to_owned());
3650    lines.push("0\t1".to_owned());
3651    lines.push(entry_types.join("\t"));
3652    append_single_file_entry(lines, root, source_ids, include_guid)?;
3653    lines.push(String::new());
3654    Ok(())
3655}
3656
3657fn single_file_entry_types(include_guid: bool) -> Vec<&'static str> {
3658    let mut entry_types = SINGLE_FILE_ENTRY_TYPES.to_vec();
3659    if include_guid {
3660        entry_types.push("mid");
3661    }
3662    entry_types
3663}
3664
3665fn single_file_entry_tree_has_guid(entry: &SingleFileEntry) -> bool {
3666    entry.guid.is_some() || entry.children.iter().any(single_file_entry_tree_has_guid)
3667}
3668
3669fn append_single_file_entry(
3670    lines: &mut Vec<String>,
3671    entry: &SingleFileEntry,
3672    source_ids: &SingleFileSourceIds,
3673    include_guid: bool,
3674) -> Result<()> {
3675    lines.push(format!("26\t{}", entry.children.len()));
3676    lines.push(single_file_entry_row(entry, source_ids, include_guid)?);
3677    for child in &entry.children {
3678        append_single_file_entry(lines, child, source_ids, include_guid)?;
3679    }
3680    Ok(())
3681}
3682
3683fn single_file_entry_row(
3684    entry: &SingleFileEntry,
3685    source_ids: &SingleFileSourceIds,
3686    include_guid: bool,
3687) -> Result<String> {
3688    if entry.name_utf16.is_some() {
3689        return Err(EwfError::Unsupported(
3690            "writing a single-file name with unpaired UTF-16 units is not supported".into(),
3691        ));
3692    }
3693    let mut values = vec![
3694        optional_display(entry.identifier),
3695        single_file_entry_type_value(entry.file_entry_type),
3696        optional_text(entry.name.as_deref()),
3697        optional_display(entry.size),
3698        optional_display(entry.logical_offset),
3699        optional_display(entry.physical_offset),
3700        optional_display(entry.duplicate_data_offset),
3701        optional_display(source_ids.entry_identifier(entry.source_identifier)),
3702        optional_display(entry.subject_identifier),
3703        optional_display(entry.permission_group_index),
3704        optional_display(entry.record_type),
3705        optional_display(entry.flags),
3706        single_file_extents_value(&entry.extents),
3707        optional_text(entry.md5.as_deref()),
3708        optional_text(entry.sha1.as_deref()),
3709        single_file_short_name_value(entry.short_name.as_deref()),
3710        optional_display(entry.creation_time),
3711        optional_display(entry.modification_time),
3712        optional_display(entry.access_time),
3713        optional_display(entry.entry_modification_time),
3714        optional_display(entry.deletion_time),
3715        single_file_attributes_value(&entry.attributes)?,
3716    ];
3717    if include_guid {
3718        values.push(optional_text(entry.guid.as_deref()));
3719    }
3720    Ok(values.join("\t"))
3721}
3722
3723fn append_single_file_source_category(
3724    lines: &mut Vec<String>,
3725    sources: &[SingleFileSource],
3726) -> Result<()> {
3727    let default_root = SingleFileSource {
3728        identifier: Some(0),
3729        ..SingleFileSource::default()
3730    };
3731    let (root, children) = sources.split_first().unwrap_or((&default_root, &[]));
3732    lines.push("srce".to_owned());
3733    lines.push(format!("{}\t1", children.len()));
3734    lines.push(SINGLE_FILE_SOURCE_TYPES.join("\t"));
3735    lines.push(format!("0\t{}", children.len()));
3736    lines.push(single_file_source_row(root, Some(0)));
3737    for (index, source) in children.iter().enumerate() {
3738        lines.push("0\t0".to_owned());
3739        lines.push(single_file_source_row(
3740            source,
3741            Some(source_child_identifier(index)?),
3742        ));
3743    }
3744    lines.push(String::new());
3745    Ok(())
3746}
3747
3748fn single_file_source_row(source: &SingleFileSource, identifier: Option<i32>) -> String {
3749    vec![
3750        optional_display(identifier.or(source.identifier)),
3751        optional_text(source.name.as_deref()),
3752        optional_text(source.evidence_number.as_deref()),
3753        optional_text(source.location.as_deref()),
3754        optional_text(source.device_guid.as_deref()),
3755        optional_text(source.primary_device_guid.as_deref()),
3756        source
3757            .drive_type
3758            .map(|value| value.to_string())
3759            .unwrap_or_default(),
3760        optional_text(source.manufacturer.as_deref()),
3761        optional_text(source.model.as_deref()),
3762        optional_text(source.serial_number.as_deref()),
3763        optional_text(source.domain.as_deref()),
3764        optional_text(source.ip_address.as_deref()),
3765        optional_text(source.mac_address.as_deref()),
3766        optional_display(source.size),
3767        optional_display(source.logical_offset),
3768        optional_display(source.physical_offset),
3769        optional_display(source.acquisition_time),
3770        optional_text(source.md5.as_deref()),
3771        optional_text(source.sha1.as_deref()),
3772    ]
3773    .join("\t")
3774}
3775
3776fn append_single_file_subject_category(lines: &mut Vec<String>, subjects: &[SingleFileSubject]) {
3777    let default_root = SingleFileSubject {
3778        identifier: Some(0),
3779        ..SingleFileSubject::default()
3780    };
3781    let (root, children) = subjects.split_first().unwrap_or((&default_root, &[]));
3782    lines.push("sub".to_owned());
3783    lines.push(format!("{}\t1", children.len()));
3784    lines.push(SINGLE_FILE_SUBJECT_TYPES.join("\t"));
3785    lines.push(format!("0\t{}", children.len()));
3786    lines.push(single_file_subject_row(root));
3787    for subject in children {
3788        lines.push("0\t0".to_owned());
3789        lines.push(single_file_subject_row(subject));
3790    }
3791    lines.push(String::new());
3792}
3793
3794fn single_file_subject_row(subject: &SingleFileSubject) -> String {
3795    [
3796        optional_display(subject.identifier),
3797        optional_text(subject.name.as_deref()),
3798    ]
3799    .join("\t")
3800}
3801
3802fn append_single_file_permission_category(
3803    lines: &mut Vec<String>,
3804    groups: &[SingleFilePermissionGroup],
3805) {
3806    lines.push("perm".to_owned());
3807    lines.push(format!("{}\t1", groups.len()));
3808    lines.push(SINGLE_FILE_PERMISSION_TYPES.join("\t"));
3809    lines.push(format!("0\t{}", groups.len()));
3810    lines.push(single_file_permission_root_row());
3811    for group in groups {
3812        lines.push(format!("0\t{}", group.permissions.len()));
3813        lines.push(single_file_permission_group_row(group));
3814        for permission in &group.permissions {
3815            lines.push("0\t0".to_owned());
3816            lines.push(single_file_permission_row(permission));
3817        }
3818    }
3819    lines.push(String::new());
3820}
3821
3822fn single_file_permission_root_row() -> String {
3823    single_file_permission_row(&SingleFilePermission {
3824        property_type: Some(10),
3825        ..SingleFilePermission::default()
3826    })
3827}
3828
3829fn single_file_permission_group_row(group: &SingleFilePermissionGroup) -> String {
3830    [
3831        optional_text(group.name.as_deref()),
3832        optional_display(group.property_type.or(Some(10))),
3833        optional_text(group.identifier.as_deref()),
3834        optional_display(group.access_mask),
3835        optional_display(group.ace_flags),
3836    ]
3837    .join("\t")
3838}
3839
3840fn single_file_permission_row(permission: &SingleFilePermission) -> String {
3841    [
3842        optional_text(permission.name.as_deref()),
3843        optional_display(permission.property_type),
3844        optional_text(permission.identifier.as_deref()),
3845        optional_display(permission.access_mask),
3846        optional_display(permission.ace_flags),
3847    ]
3848    .join("\t")
3849}
3850
3851fn single_file_entry_type_value(value: Option<SingleFileEntryType>) -> String {
3852    match value {
3853        Some(SingleFileEntryType::File) => "0".to_owned(),
3854        Some(SingleFileEntryType::Directory) => "1".to_owned(),
3855        Some(SingleFileEntryType::Unknown) => "u".to_owned(),
3856        None => String::new(),
3857    }
3858}
3859
3860fn single_file_extents_value(extents: &[SingleFileExtent]) -> String {
3861    if extents.is_empty() {
3862        return String::new();
3863    }
3864
3865    let mut parts = Vec::with_capacity(1 + extents.len() * 3);
3866    parts.push(format!("{:x}", extents.len()));
3867    for extent in extents {
3868        if extent.sparse {
3869            parts.push("S".to_owned());
3870        }
3871        parts.push(format!("{:x}", extent.data_offset));
3872        parts.push(format!("{:x}", extent.data_size));
3873    }
3874    parts.join(" ")
3875}
3876
3877fn single_file_attributes_value(attributes: &[SingleFileAttribute]) -> Result<String> {
3878    if attributes.is_empty() {
3879        return Ok(String::new());
3880    }
3881
3882    let mut data = EWF2_EXTENDED_ATTRIBUTES_HEADER.to_vec();
3883    for attribute in attributes {
3884        let name = optional_utf16le_null_terminated(attribute.name.as_deref());
3885        let value = optional_utf16le_null_terminated(attribute.value.as_deref());
3886        let name_units = u32::try_from(name.len() / 2).map_err(|_| {
3887            EwfError::Unsupported("single file attribute name length exceeds u32".into())
3888        })?;
3889        let value_units = u32::try_from(value.len() / 2).map_err(|_| {
3890            EwfError::Unsupported("single file attribute value length exceeds u32".into())
3891        })?;
3892
3893        data.extend_from_slice(&[0; 4]);
3894        data.push(0);
3895        data.extend_from_slice(&name_units.to_le_bytes());
3896        data.extend_from_slice(&value_units.to_le_bytes());
3897        data.extend_from_slice(&name);
3898        data.extend_from_slice(&value);
3899    }
3900    Ok(hex_bytes(&data))
3901}
3902
3903fn optional_utf16le_null_terminated(value: Option<&str>) -> Vec<u8> {
3904    let Some(value) = value else {
3905        return Vec::new();
3906    };
3907
3908    let mut bytes = utf16le(&optional_text(Some(value)));
3909    bytes.extend_from_slice(&0_u16.to_le_bytes());
3910    bytes
3911}
3912
3913fn optional_text(value: Option<&str>) -> String {
3914    value.map(sanitize_header_value).unwrap_or_default()
3915}
3916
3917fn single_file_short_name_value(value: Option<&str>) -> String {
3918    let value = optional_text(value);
3919    if value.is_empty() {
3920        String::new()
3921    } else {
3922        format!("{} {value}", value.len() + 1)
3923    }
3924}
3925
3926fn optional_display<T: std::fmt::Display>(value: Option<T>) -> String {
3927    value.map(|value| value.to_string()).unwrap_or_default()
3928}
3929
3930fn hex_bytes(bytes: &[u8]) -> String {
3931    const HEX: &[u8; 16] = b"0123456789abcdef";
3932    let mut out = String::with_capacity(bytes.len() * 2);
3933    for byte in bytes {
3934        out.push(char::from(HEX[(byte >> 4) as usize]));
3935        out.push(char::from(HEX[(byte & 0x0f) as usize]));
3936    }
3937    out
3938}
3939
3940fn ewf2_hash_payload<const N: usize>(hash: [u8; N]) -> [u8; 32] {
3941    let mut payload = [0; 32];
3942    payload[..N].copy_from_slice(&hash);
3943    let checksum = adler32(&payload[..N]);
3944    payload[N..N + 4].copy_from_slice(&checksum.to_le_bytes());
3945    payload
3946}
3947
3948fn ewf1_error2_payload(errors: &[AcquisitionError]) -> Result<Option<Vec<u8>>> {
3949    if errors.is_empty() {
3950        return Ok(None);
3951    }
3952
3953    let entry_count = u32::try_from(errors.len()).map_err(|_| {
3954        EwfError::Unsupported("EWF1 writer acquisition error count exceeds u32".into())
3955    })?;
3956    let mut payload = Vec::with_capacity(520 + errors.len() * 8 + 4);
3957    payload.resize(520, 0);
3958    payload[0..4].copy_from_slice(&entry_count.to_le_bytes());
3959    let header_checksum = adler32(&payload[..516]);
3960    payload[516..520].copy_from_slice(&header_checksum.to_le_bytes());
3961    let entries_start = payload.len();
3962    for error in errors {
3963        let first_sector = u32::try_from(error.first_sector).map_err(|_| {
3964            EwfError::Unsupported("EWF1 writer acquisition error first sector exceeds u32".into())
3965        })?;
3966        let sector_count = u32::try_from(error.sector_count).map_err(|_| {
3967            EwfError::Unsupported("EWF1 writer acquisition error sector count exceeds u32".into())
3968        })?;
3969        payload.extend_from_slice(&first_sector.to_le_bytes());
3970        payload.extend_from_slice(&sector_count.to_le_bytes());
3971    }
3972    let entries_checksum = adler32(&payload[entries_start..]);
3973    payload.extend_from_slice(&entries_checksum.to_le_bytes());
3974    Ok(Some(payload))
3975}
3976
3977fn ewf2_error_table_payload(errors: &[AcquisitionError]) -> Result<Option<Vec<u8>>> {
3978    if errors.is_empty() {
3979        return Ok(None);
3980    }
3981
3982    let entry_count = u32::try_from(errors.len()).map_err(|_| {
3983        EwfError::Unsupported("EWF2 writer acquisition error count exceeds u32".into())
3984    })?;
3985    let mut payload = vec![0; 32];
3986    payload[0..4].copy_from_slice(&entry_count.to_le_bytes());
3987    let header_checksum = adler32(&payload[..16]);
3988    payload[16..20].copy_from_slice(&header_checksum.to_le_bytes());
3989    let entries_start = payload.len();
3990    for error in errors {
3991        let sector_count = u32::try_from(error.sector_count).map_err(|_| {
3992            EwfError::Unsupported("EWF2 writer acquisition error sector count exceeds u32".into())
3993        })?;
3994        payload.extend_from_slice(&error.first_sector.to_le_bytes());
3995        payload.extend_from_slice(&sector_count.to_le_bytes());
3996        payload.extend_from_slice(&[0; 4]);
3997    }
3998    let entries_checksum = adler32(&payload[entries_start..]);
3999    payload.extend_from_slice(&entries_checksum.to_le_bytes());
4000    payload.extend_from_slice(&[0; 12]);
4001    Ok(Some(payload))
4002}
4003
4004fn ewf1_session_payload(
4005    sessions: &[SectorRange],
4006    tracks: &[SectorRange],
4007) -> Result<Option<Vec<u8>>> {
4008    session_payload(1, sessions, tracks)
4009}
4010
4011fn ewf2_session_table_payload(
4012    sessions: &[SectorRange],
4013    tracks: &[SectorRange],
4014) -> Result<Option<Vec<u8>>> {
4015    session_payload(2, sessions, tracks)
4016}
4017
4018fn session_payload(
4019    format_version: u8,
4020    sessions: &[SectorRange],
4021    tracks: &[SectorRange],
4022) -> Result<Option<Vec<u8>>> {
4023    const AUDIO_TRACK_FLAG: u32 = 0x01;
4024
4025    if sessions.is_empty() && tracks.is_empty() {
4026        return Ok(None);
4027    }
4028
4029    let mut entries = Vec::with_capacity(sessions.len() + tracks.len());
4030    entries.extend(sessions.iter().map(|range| (range.first_sector, 0)));
4031    entries.extend(
4032        tracks
4033            .iter()
4034            .map(|range| (range.first_sector, AUDIO_TRACK_FLAG)),
4035    );
4036    entries.sort_by_key(|(start_sector, flags)| (*start_sector, *flags & AUDIO_TRACK_FLAG));
4037
4038    let entry_count = u32::try_from(entries.len())
4039        .map_err(|_| EwfError::Unsupported("writer session entry count exceeds u32".into()))?;
4040    let (header_size, checksum_offset, checksum_data_size, footer_size) = match format_version {
4041        1 => (36_usize, 32_usize, 32_usize, 4_usize),
4042        2 => (32_usize, 16_usize, 16_usize, 16_usize),
4043        _ => unreachable!("writer only emits EWF1 or EWF2 sessions"),
4044    };
4045    let mut payload = vec![0; header_size];
4046    payload[0..4].copy_from_slice(&entry_count.to_le_bytes());
4047    let header_checksum = adler32(&payload[..checksum_data_size]);
4048    payload[checksum_offset..checksum_offset + 4].copy_from_slice(&header_checksum.to_le_bytes());
4049
4050    let entries_start = payload.len();
4051    for (start_sector, flags) in entries {
4052        if format_version == 1 {
4053            let start_sector = u32::try_from(start_sector).map_err(|_| {
4054                EwfError::Unsupported("EWF1 writer session start sector exceeds u32".into())
4055            })?;
4056            payload.extend_from_slice(&flags.to_le_bytes());
4057            payload.extend_from_slice(&start_sector.to_le_bytes());
4058            payload.extend_from_slice(&[0; 24]);
4059        } else {
4060            payload.extend_from_slice(&start_sector.to_le_bytes());
4061            payload.extend_from_slice(&flags.to_le_bytes());
4062            payload.extend_from_slice(&[0; 20]);
4063        }
4064    }
4065    let entries_checksum = adler32(&payload[entries_start..]);
4066    payload.extend_from_slice(&entries_checksum.to_le_bytes());
4067    if footer_size > 4 {
4068        payload.extend_from_slice(&vec![0; footer_size - 4]);
4069    }
4070
4071    Ok(Some(payload))
4072}
4073
4074fn adler32(data: &[u8]) -> u32 {
4075    const MOD_ADLER: u32 = 65_521;
4076    let mut a = 1_u32;
4077    let mut b = 0_u32;
4078    for byte in data {
4079        a = (a + u32::from(*byte)) % MOD_ADLER;
4080        b = (b + a) % MOD_ADLER;
4081    }
4082    (b << 16) | a
4083}
4084
4085fn utf16le(text: &str) -> Vec<u8> {
4086    let mut bytes = Vec::with_capacity(text.len() * 2);
4087    for unit in text.encode_utf16() {
4088        bytes.extend_from_slice(&unit.to_le_bytes());
4089    }
4090    bytes
4091}
4092
4093fn utf16le_with_bom(text: &str) -> Vec<u8> {
4094    let mut bytes = Vec::with_capacity(2 + text.len() * 2);
4095    bytes.extend_from_slice(&0xfeff_u16.to_le_bytes());
4096    bytes.extend_from_slice(&utf16le(text));
4097    bytes
4098}
4099
4100fn ewf2_table_header(first_chunk: u64, chunk_count: u32) -> [u8; EWF2_TABLE_HEADER_V2_SIZE] {
4101    let mut table = [0; EWF2_TABLE_HEADER_V2_SIZE];
4102    table[0..8].copy_from_slice(&first_chunk.to_le_bytes());
4103    table[8..12].copy_from_slice(&chunk_count.to_le_bytes());
4104    let checksum = adler32(&table[..16]);
4105    table[16..20].copy_from_slice(&checksum.to_le_bytes());
4106    table
4107}
4108
4109fn ewf2_table_entry(
4110    chunk_offset: u64,
4111    chunk_size: u32,
4112    compressed: bool,
4113    has_checksum: bool,
4114    pattern_fill: bool,
4115) -> [u8; ewf2::TABLE_ENTRY_SIZE] {
4116    let mut entry = [0; ewf2::TABLE_ENTRY_SIZE];
4117    entry[0..8].copy_from_slice(&chunk_offset.to_le_bytes());
4118    entry[8..12].copy_from_slice(&chunk_size.to_le_bytes());
4119    let mut flags = 0_u32;
4120    if compressed {
4121        flags |= ewf2::CHUNK_FLAG_COMPRESSED;
4122    }
4123    if has_checksum {
4124        flags |= ewf2::CHUNK_FLAG_HAS_CHECKSUM;
4125    }
4126    if pattern_fill {
4127        flags |= ewf2::CHUNK_FLAG_PATTERN_FILL;
4128    }
4129    entry[12..16].copy_from_slice(&flags.to_le_bytes());
4130    entry
4131}
4132
4133fn ewf2_section_desc(
4134    section_type: u32,
4135    data_size: u64,
4136    previous_offset: u64,
4137) -> [u8; ewf2::SECTION_DESCRIPTOR_SIZE] {
4138    let mut desc = [0; ewf2::SECTION_DESCRIPTOR_SIZE];
4139    desc[0..4].copy_from_slice(&section_type.to_le_bytes());
4140    desc[8..16].copy_from_slice(&previous_offset.to_le_bytes());
4141    desc[16..24].copy_from_slice(&data_size.to_le_bytes());
4142    desc[24..28].copy_from_slice(&(ewf2::SECTION_DESCRIPTOR_SIZE as u32).to_le_bytes());
4143    let checksum = adler32(&desc[..ewf2::SECTION_DESCRIPTOR_SIZE - 4]);
4144    desc[ewf2::SECTION_DESCRIPTOR_SIZE - 4..].copy_from_slice(&checksum.to_le_bytes());
4145    desc
4146}
4147
4148fn checked_add(left: u64, right: u64, label: &str) -> Result<u64> {
4149    left.checked_add(right)
4150        .ok_or_else(|| EwfError::Malformed(format!("writer {label} offset overflow")))
4151}
4152
4153fn validated_write_hashes(requested: &WriteHashes) -> Result<WriteHashes> {
4154    let mut hashes = StoredHashes {
4155        md5: requested.md5,
4156        sha1: requested.sha1,
4157        ..StoredHashes::default()
4158    };
4159    for (identifier, value) in &requested.hash_values {
4160        crate::hashes::insert_stored_hash(&mut hashes, identifier, value)?;
4161    }
4162    Ok(WriteHashes {
4163        md5: hashes.md5,
4164        sha1: hashes.sha1,
4165        hash_values: hashes.hash_values,
4166    })
4167}
4168
4169fn effective_write_hashes(
4170    requested: &WriteHashes,
4171    computed_md5: [u8; 16],
4172    computed_sha1: [u8; 20],
4173    computed_sha256: [u8; 32],
4174) -> Result<WriteHashes> {
4175    let mut hashes = validated_write_hashes(requested)?;
4176    hashes.md5 = hashes.md5.or(Some(computed_md5));
4177    hashes.sha1 = hashes.sha1.or(Some(computed_sha1));
4178    hashes
4179        .hash_values
4180        .entry("SHA256".into())
4181        .or_insert_with(|| hex_string(&computed_sha256));
4182    Ok(hashes)
4183}
4184
4185fn checked_writer_seek(
4186    current_offset: u64,
4187    logical_input_size: u64,
4188    position: SeekFrom,
4189) -> Result<u64> {
4190    let next = match position {
4191        SeekFrom::Start(offset) => return Ok(offset),
4192        SeekFrom::Current(offset) => i128::from(current_offset) + i128::from(offset),
4193        SeekFrom::End(offset) => i128::from(logical_input_size) + i128::from(offset),
4194    };
4195    if next < 0 {
4196        return Err(EwfError::Malformed(
4197            "writer seek before start of media".into(),
4198        ));
4199    }
4200    u64::try_from(next)
4201        .map_err(|_| EwfError::Malformed("writer seek offset does not fit u64".into()))
4202}
4203
4204fn encode_raw_spool(
4205    raw: &mut RawSpool,
4206    mut encoded_chunks: BTreeMap<u64, RememberedEncodedChunk>,
4207    logical_size: u64,
4208    chunk_capacity: usize,
4209    chunk_size: u64,
4210    options: &WriteOptions,
4211) -> Result<EncodedSpool> {
4212    let mut chunks = Vec::new();
4213    let mut encoded_spool = ChunkSpool::new()?;
4214    let mut hash_state = WriteHashState::new();
4215    let mut offset = 0_u64;
4216    let is_ewf2 = is_ewf2_format(options.format);
4217
4218    while offset < logical_size {
4219        let take = usize::try_from((logical_size - offset).min(chunk_capacity as u64))
4220            .expect("chunk read is bounded by chunk capacity");
4221        let mut chunk = vec![0; take];
4222        raw.read_at_filling_zeroes(offset, &mut chunk)?;
4223        hash_state.update(&chunk);
4224        let chunk_index = offset / chunk_size;
4225        let encoded = if let Some(remembered) = encoded_chunks.remove(&chunk_index) {
4226            if remembered.logical_size == take
4227                && encoded_data_chunk_encoding_compatible(remembered.encoding, options)
4228            {
4229                remembered.encoded
4230            } else {
4231                encode_chunk(
4232                    chunk,
4233                    options.compression,
4234                    options.compression_values,
4235                    chunk_size,
4236                    is_ewf2,
4237                    !is_ewf2 && options.compression_values.empty_block,
4238                )?
4239            }
4240        } else {
4241            encode_chunk(
4242                chunk,
4243                options.compression,
4244                options.compression_values,
4245                chunk_size,
4246                is_ewf2,
4247                !is_ewf2 && options.compression_values.empty_block,
4248            )?
4249        };
4250        let descriptor = encoded_spool.append(encoded)?;
4251        chunks.push(descriptor);
4252        offset = offset
4253            .checked_add(u64::try_from(take).expect("usize fits u64"))
4254            .ok_or_else(|| EwfError::Malformed("writer raw spool encode offset overflow".into()))?;
4255    }
4256
4257    let (computed_md5, computed_sha1, computed_sha256) = hash_state.finalize();
4258    Ok(EncodedSpool {
4259        chunks,
4260        spool: encoded_spool,
4261        computed_md5,
4262        computed_sha1,
4263        computed_sha256,
4264    })
4265}
4266
4267struct EncodedSpool {
4268    chunks: Vec<ChunkDescriptor>,
4269    spool: ChunkSpool,
4270    computed_md5: [u8; 16],
4271    computed_sha1: [u8; 20],
4272    computed_sha256: [u8; 32],
4273}
4274
4275struct RememberedEncodedChunk {
4276    logical_size: usize,
4277    encoding: DataChunkEncoding,
4278    encoded: EncodedChunk,
4279}
4280
4281#[derive(Debug, Clone, Copy)]
4282struct ChunkDescriptor {
4283    data_offset: u64,
4284    data_size: u64,
4285    compressed: bool,
4286    has_checksum: bool,
4287    pattern_fill: Option<u64>,
4288}
4289
4290struct RawSpool {
4291    file: NamedTempFile,
4292    len: u64,
4293}
4294
4295impl RawSpool {
4296    fn new() -> Result<Self> {
4297        Ok(Self {
4298            file: NamedTempFile::new()?,
4299            len: 0,
4300        })
4301    }
4302
4303    fn len(&self) -> u64 {
4304        self.len
4305    }
4306
4307    fn write_at(&mut self, offset: u64, data: &[u8]) -> Result<()> {
4308        if data.is_empty() {
4309            return Ok(());
4310        }
4311
4312        let data_len = u64::try_from(data.len())
4313            .map_err(|_| EwfError::Malformed("writer data length does not fit u64".into()))?;
4314        let end_offset = offset
4315            .checked_add(data_len)
4316            .ok_or_else(|| EwfError::Malformed("writer raw spool size overflow".into()))?;
4317        let file = self.file.as_file_mut();
4318        file.seek(SeekFrom::Start(offset))?;
4319        file.write_all(data)?;
4320        self.len = self.len.max(end_offset);
4321        Ok(())
4322    }
4323
4324    fn read_at_filling_zeroes(&mut self, offset: u64, buffer: &mut [u8]) -> Result<()> {
4325        buffer.fill(0);
4326        if buffer.is_empty() || offset >= self.len {
4327            return Ok(());
4328        }
4329
4330        let available = self.len - offset;
4331        let take = usize::try_from(available.min(buffer.len() as u64))
4332            .expect("raw spool read is bounded by buffer length");
4333        let file = self.file.as_file_mut();
4334        file.seek(SeekFrom::Start(offset))?;
4335        file.read_exact(&mut buffer[..take])?;
4336        Ok(())
4337    }
4338}
4339
4340struct ChunkSpool {
4341    file: NamedTempFile,
4342    len: u64,
4343    position: u64,
4344}
4345
4346impl ChunkSpool {
4347    fn new() -> Result<Self> {
4348        Ok(Self {
4349            file: NamedTempFile::new()?,
4350            len: 0,
4351            position: 0,
4352        })
4353    }
4354
4355    fn append(&mut self, encoded: EncodedChunk) -> Result<ChunkDescriptor> {
4356        let EncodedChunk {
4357            bytes,
4358            compressed,
4359            has_checksum,
4360            pattern_fill,
4361        } = encoded;
4362        let data_offset = self.len;
4363        let data_size = u64::try_from(bytes.len()).map_err(|_| {
4364            EwfError::Malformed("writer encoded chunk size does not fit u64".into())
4365        })?;
4366        let new_len = self.len.checked_add(data_size).ok_or_else(|| {
4367            EwfError::Malformed("writer encoded chunk spool size overflow".into())
4368        })?;
4369        if data_size > 0 {
4370            let file = self.file.as_file_mut();
4371            if self.position != data_offset {
4372                file.seek(SeekFrom::Start(data_offset))?;
4373            }
4374            file.write_all(&bytes)?;
4375            self.position = new_len;
4376        }
4377        self.len = new_len;
4378
4379        Ok(ChunkDescriptor {
4380            data_offset,
4381            data_size,
4382            compressed,
4383            has_checksum,
4384            pattern_fill,
4385        })
4386    }
4387
4388    fn copy_chunk_to<W: Write>(
4389        &mut self,
4390        descriptor: &ChunkDescriptor,
4391        writer: &mut W,
4392    ) -> Result<()> {
4393        if descriptor.data_size == 0 {
4394            return Ok(());
4395        }
4396
4397        let mut remaining = descriptor.data_size;
4398        let mut buffer = vec![0; 64 * 1024];
4399        let file = self.file.as_file_mut();
4400        if self.position != descriptor.data_offset {
4401            file.seek(SeekFrom::Start(descriptor.data_offset))?;
4402            self.position = descriptor.data_offset;
4403        }
4404        while remaining > 0 {
4405            let take = usize::try_from(remaining.min(buffer.len() as u64))
4406                .expect("copy chunk is bounded by buffer length");
4407            file.read_exact(&mut buffer[..take])?;
4408            writer.write_all(&buffer[..take])?;
4409            remaining -= u64::try_from(take).expect("usize fits u64");
4410            self.position += u64::try_from(take).expect("usize fits u64");
4411        }
4412
4413        Ok(())
4414    }
4415
4416    fn reset(&mut self) -> Result<()> {
4417        self.file.as_file_mut().set_len(0)?;
4418        self.file.as_file_mut().seek(SeekFrom::Start(0))?;
4419        self.len = 0;
4420        self.position = 0;
4421        Ok(())
4422    }
4423}
4424
4425struct EncodedChunk {
4426    bytes: Vec<u8>,
4427    compressed: bool,
4428    has_checksum: bool,
4429    pattern_fill: Option<u64>,
4430}
4431
4432fn encode_chunk(
4433    chunk: Vec<u8>,
4434    compression: WriteCompression,
4435    compression_values: WriteCompressionValues,
4436    chunk_size: u64,
4437    allow_pattern_fill: bool,
4438    allow_empty_block_compression: bool,
4439) -> Result<EncodedChunk> {
4440    if allow_pattern_fill && let Some(pattern) = repeated_ewf2_pattern(&chunk) {
4441        return Ok(EncodedChunk {
4442            bytes: Vec::new(),
4443            compressed: true,
4444            has_checksum: false,
4445            pattern_fill: Some(pattern),
4446        });
4447    }
4448
4449    if allow_empty_block_compression && is_full_zero_chunk(&chunk, chunk_size) {
4450        let mut encoder = ZlibEncoder::new(
4451            Vec::new(),
4452            empty_block_zlib_compression(compression_values.level),
4453        );
4454        encoder.write_all(&chunk)?;
4455        return Ok(EncodedChunk {
4456            bytes: encoder.finish()?,
4457            compressed: true,
4458            has_checksum: false,
4459            pattern_fill: None,
4460        });
4461    }
4462
4463    match compression {
4464        WriteCompression::None => {
4465            let checksum = adler32(&chunk);
4466            let mut bytes = chunk;
4467            bytes.extend_from_slice(&checksum.to_le_bytes());
4468            Ok(EncodedChunk {
4469                bytes,
4470                compressed: false,
4471                has_checksum: true,
4472                pattern_fill: None,
4473            })
4474        }
4475        WriteCompression::Zlib => {
4476            let mut encoder =
4477                ZlibEncoder::new(Vec::new(), zlib_compression(compression_values.level));
4478            encoder.write_all(&chunk)?;
4479            let bytes = encoder.finish()?;
4480            // EWF consumers allocate roughly one media chunk plus its checksum.
4481            // Deflate can expand incompressible data, especially with large
4482            // chunks. Store those chunks raw with the ordinary Adler checksum.
4483            if bytes.len() >= chunk.len() {
4484                return encode_chunk(
4485                    chunk,
4486                    WriteCompression::None,
4487                    compression_values,
4488                    chunk_size,
4489                    false,
4490                    false,
4491                );
4492            }
4493            Ok(EncodedChunk {
4494                bytes,
4495                compressed: true,
4496                has_checksum: false,
4497                pattern_fill: None,
4498            })
4499        }
4500        WriteCompression::Bzip2 => {
4501            let mut encoder =
4502                BzEncoder::new(Vec::new(), bzip2_compression(compression_values.level)?);
4503            encoder.write_all(&chunk)?;
4504            Ok(EncodedChunk {
4505                bytes: encoder.finish()?,
4506                compressed: true,
4507                has_checksum: false,
4508                pattern_fill: None,
4509            })
4510        }
4511    }
4512}
4513
4514fn encode_chunks(
4515    chunks: Vec<Vec<u8>>,
4516    compression: WriteCompression,
4517    compression_values: WriteCompressionValues,
4518    chunk_size: u64,
4519    allow_pattern_fill: bool,
4520    allow_empty_block_compression: bool,
4521) -> Result<Vec<EncodedChunk>> {
4522    #[cfg(feature = "parallel")]
4523    if chunks.len() > 1 {
4524        use rayon::prelude::*;
4525        return chunks
4526            .into_par_iter()
4527            .map(|chunk| {
4528                encode_chunk(
4529                    chunk,
4530                    compression,
4531                    compression_values,
4532                    chunk_size,
4533                    allow_pattern_fill,
4534                    allow_empty_block_compression,
4535                )
4536            })
4537            .collect();
4538    }
4539
4540    chunks
4541        .into_iter()
4542        .map(|chunk| {
4543            encode_chunk(
4544                chunk,
4545                compression,
4546                compression_values,
4547                chunk_size,
4548                allow_pattern_fill,
4549                allow_empty_block_compression,
4550            )
4551        })
4552        .collect()
4553}
4554
4555fn automatic_encode_batch_chunks(chunk_size: usize) -> usize {
4556    #[cfg(feature = "parallel")]
4557    {
4558        const MAXIMUM_BATCH_BYTES: usize = 8 * 1024 * 1024;
4559        let memory_limit = MAXIMUM_BATCH_BYTES
4560            .checked_div(chunk_size)
4561            .unwrap_or(0)
4562            .max(1);
4563        rayon::current_num_threads()
4564            .saturating_mul(2)
4565            .min(memory_limit)
4566            .max(1)
4567    }
4568    #[cfg(not(feature = "parallel"))]
4569    {
4570        let _ = chunk_size;
4571        1
4572    }
4573}
4574
4575fn is_full_zero_chunk(chunk: &[u8], chunk_size: u64) -> bool {
4576    u64::try_from(chunk.len()).ok() == Some(chunk_size) && chunk.iter().all(|byte| *byte == 0)
4577}
4578
4579fn repeated_ewf2_pattern(chunk: &[u8]) -> Option<u64> {
4580    if chunk.is_empty() {
4581        return None;
4582    }
4583
4584    let mut pattern = [0; 8];
4585    let seed_size = chunk.len().min(pattern.len());
4586    pattern[..seed_size].copy_from_slice(&chunk[..seed_size]);
4587    chunk
4588        .iter()
4589        .enumerate()
4590        .all(|(index, byte)| *byte == pattern[index % pattern.len()])
4591        .then(|| u64::from_le_bytes(pattern))
4592}
4593
4594fn segment_groups(
4595    chunks: &[ChunkDescriptor],
4596    maximum_segment_size: Option<u64>,
4597    options: &WriteOptions,
4598    sector_count: u64,
4599    total_chunk_count: u64,
4600) -> Result<Vec<Range<usize>>> {
4601    let Some(maximum_segment_size) = maximum_segment_size else {
4602        return Ok(std::iter::once(0..chunks.len()).collect());
4603    };
4604
4605    let mut groups = Vec::new();
4606    let mut current_start = 0_usize;
4607    let mut payload_size = 0_u64;
4608    let is_ewf2 = is_ewf2_format(options.format);
4609    let mut ewf1_table_group_state = Ewf1TableGroupState::default();
4610    for (chunk_index, chunk) in chunks.iter().enumerate() {
4611        let chunk_size = chunk.data_size;
4612        let proposed_payload_size = payload_size
4613            .checked_add(chunk_size)
4614            .ok_or_else(|| EwfError::Malformed("writer segment payload size overflow".into()))?;
4615        let proposed_ewf1_table_group_state = if is_ewf2 {
4616            ewf1_table_group_state
4617        } else {
4618            ewf1_table_group_state.add_chunk(
4619                chunk_size,
4620                EWF1_TABLE_GROUP_MAX_ENTRIES,
4621                EWF1_TABLE_GROUP_MAX_PAYLOAD,
4622            )?
4623        };
4624        let proposed_size = estimated_segment_size(
4625            chunk_index - current_start + 1,
4626            proposed_payload_size,
4627            proposed_ewf1_table_group_state.group_count.max(1),
4628            options,
4629            sector_count,
4630            total_chunk_count,
4631        )?;
4632        if chunk_index > current_start && proposed_size > maximum_segment_size {
4633            groups.push(current_start..chunk_index);
4634            current_start = chunk_index;
4635            payload_size = 0;
4636            ewf1_table_group_state = if is_ewf2 {
4637                Ewf1TableGroupState::default()
4638            } else {
4639                Ewf1TableGroupState::default().add_chunk(
4640                    chunk_size,
4641                    EWF1_TABLE_GROUP_MAX_ENTRIES,
4642                    EWF1_TABLE_GROUP_MAX_PAYLOAD,
4643                )?
4644            };
4645        } else {
4646            ewf1_table_group_state = proposed_ewf1_table_group_state;
4647        }
4648
4649        payload_size = payload_size
4650            .checked_add(chunk_size)
4651            .ok_or_else(|| EwfError::Malformed("writer segment payload size overflow".into()))?;
4652    }
4653
4654    groups.push(current_start..chunks.len());
4655    Ok(groups)
4656}
4657
4658fn estimated_segment_size(
4659    chunk_count: usize,
4660    chunk_payload_size: u64,
4661    ewf1_table_group_count: u64,
4662    options: &WriteOptions,
4663    sector_count: u64,
4664    total_chunk_count: u64,
4665) -> Result<u64> {
4666    if is_ewf2_format(options.format) {
4667        return estimated_ewf2_segment_size(
4668            chunk_count,
4669            chunk_payload_size,
4670            options,
4671            sector_count,
4672            total_chunk_count,
4673        );
4674    }
4675    estimated_ewf1_segment_size(
4676        chunk_count,
4677        chunk_payload_size,
4678        ewf1_table_group_count,
4679        options,
4680    )
4681}
4682
4683fn estimated_ewf1_segment_size(
4684    chunk_count: usize,
4685    chunk_payload_size: u64,
4686    table_group_count: u64,
4687    options: &WriteOptions,
4688) -> Result<u64> {
4689    let table_entry_bytes = u64::try_from(chunk_count)
4690        .map_err(|_| EwfError::Malformed("writer segment chunk count does not fit u64".into()))?
4691        .checked_mul(4)
4692        .ok_or_else(|| EwfError::Malformed("writer table entry size overflow".into()))?;
4693    let table_footer_bytes = if matches!(options.format, WriteFormat::Ewf1Smart) {
4694        0
4695    } else {
4696        4
4697    };
4698    let table_group_overhead = (ewf1::SECTION_DESCRIPTOR_SIZE as u64)
4699        .checked_add(24)
4700        .and_then(|value| value.checked_add(table_footer_bytes))
4701        .ok_or_else(|| EwfError::Malformed("writer table group overhead overflow".into()))?;
4702    let table_sections_size = table_group_count
4703        .checked_mul(table_group_overhead)
4704        .and_then(|value| value.checked_add(table_entry_bytes))
4705        .ok_or_else(|| EwfError::Malformed("writer table sections size overflow".into()))?;
4706    let sectors_sections_size = table_group_count
4707        .checked_mul(ewf1::SECTION_DESCRIPTOR_SIZE as u64)
4708        .and_then(|value| value.checked_add(chunk_payload_size))
4709        .ok_or_else(|| EwfError::Malformed("writer sectors sections size overflow".into()))?;
4710    let writes_table2 = matches!(
4711        options.format,
4712        WriteFormat::Ewf1Physical | WriteFormat::Ewf1Logical
4713    );
4714
4715    let header_size = header_payload(
4716        &options.metadata,
4717        options.header_codepage,
4718        options.compression_values.level,
4719    )?
4720    .map(|payload| estimated_ewf1_section_size(payload.len() as u64))
4721    .transpose()?
4722    .unwrap_or(0);
4723    let header2_size = header2_payload(&options.metadata, options.compression_values.level)?
4724        .map(|payload| estimated_ewf1_section_size(payload.len() as u64))
4725        .transpose()?
4726        .unwrap_or(0);
4727    let xheader_size = xheader_payload(&options.metadata, options.compression_values.level)?
4728        .map(|payload| estimated_ewf1_section_size(payload.len() as u64))
4729        .transpose()?
4730        .unwrap_or(0);
4731    let session_size = ewf1_session_payload(&options.sessions, &options.tracks)?
4732        .map(|payload| estimated_ewf1_section_size(payload.len() as u64))
4733        .transpose()?
4734        .unwrap_or(0);
4735    let ltree_size = if matches!(options.format, WriteFormat::Ewf1Logical) {
4736        options
4737            .single_files
4738            .as_ref()
4739            .map(ewf1_ltree_payload)
4740            .transpose()?
4741            .as_ref()
4742            .map(|payload| estimated_ewf1_section_size(payload.len() as u64))
4743            .transpose()?
4744            .unwrap_or(0)
4745    } else {
4746        0
4747    };
4748    let error2_size = ewf1_error2_payload(&options.acquisition_errors)?
4749        .map(|payload| estimated_ewf1_section_size(payload.len() as u64))
4750        .transpose()?
4751        .unwrap_or(0);
4752    let digest_size = digest_payload(&options.hashes)
4753        .map(|payload| estimated_ewf1_section_size(payload.len() as u64))
4754        .transpose()?
4755        .unwrap_or(0);
4756    let xhash_size = xhash_payload(&options.hashes, options.compression_values.level)?
4757        .map(|payload| estimated_ewf1_section_size(payload.len() as u64))
4758        .transpose()?
4759        .unwrap_or(0);
4760
4761    checked_sum(
4762        [
4763            ewf1::FILE_HEADER_SIZE as u64,
4764            header_size,
4765            header2_size,
4766            xheader_size,
4767            estimated_ewf1_section_size(volume_data_size(options.format))?,
4768            session_size,
4769            table_sections_size,
4770            if writes_table2 {
4771                table_sections_size
4772            } else {
4773                0
4774            },
4775            ltree_size,
4776            sectors_sections_size,
4777            error2_size,
4778            digest_size,
4779            xhash_size,
4780            ewf1::SECTION_DESCRIPTOR_SIZE as u64,
4781        ],
4782        "estimated EWF1 segment size",
4783    )
4784}
4785
4786fn estimated_ewf2_segment_size(
4787    chunk_count: usize,
4788    chunk_payload_size: u64,
4789    options: &WriteOptions,
4790    sector_count: u64,
4791    total_chunk_count: u64,
4792) -> Result<u64> {
4793    let device_information_payload =
4794        ewf2_device_information_payload(options, sector_count, total_chunk_count);
4795    let device_information =
4796        ewf2_device_information_section_payload(&device_information_payload, options)?;
4797    let case_data_payload = ewf2_case_data_payload(options, total_chunk_count);
4798    let case_data = ewf2_metadata_section_payload(
4799        &case_data_payload,
4800        options.compression,
4801        options.compression_values.level,
4802    )?;
4803    let error_table = ewf2_error_table_payload(&options.acquisition_errors)?;
4804    let session_table = ewf2_session_table_payload(&options.sessions, &options.tracks)?;
4805    let memory_extents_table = ewf2_memory_extents_table_payload(&options.memory_extents);
4806    let single_files_table_0x21 = ewf2_single_files_aux_u64_table_payload(
4807        &options.ewf2_single_files_tables.table_0x21_entries,
4808    );
4809    let single_files_md5_hash_table =
4810        ewf2_single_files_md5_hash_table_payload(&options.ewf2_single_files_tables.md5_hashes);
4811    let single_files_table_0x23 = ewf2_single_files_aux_u64_table_payload(
4812        &options.ewf2_single_files_tables.table_0x23_entries,
4813    );
4814    let increment_data_size =
4815        options
4816            .ewf2_increment_data
4817            .iter()
4818            .try_fold(0_u64, |total, payload| -> Result<u64> {
4819                let section_size = estimated_ewf2_section_size(
4820                    u64::try_from(payload.len()).expect("usize fits u64"),
4821                )?;
4822                total.checked_add(section_size).ok_or_else(|| {
4823                    EwfError::Malformed("writer estimated EWF2 increment data size overflow".into())
4824                })
4825            })?;
4826    let md5_hash = options.hashes.md5.map(ewf2_hash_payload);
4827    let sha1_hash = options.hashes.sha1.map(ewf2_hash_payload);
4828    let final_information_size = options
4829        .ewf2_final_information
4830        .as_ref()
4831        .map(|payload| estimated_ewf2_section_size(payload.len() as u64))
4832        .transpose()?
4833        .unwrap_or(0);
4834    let analytical_data = options
4835        .ewf2_analytical_data
4836        .as_deref()
4837        .map(|data| {
4838            ewf2_string_section_payload(data, options.compression, options.compression_values.level)
4839        })
4840        .transpose()?;
4841    let restart_data = options
4842        .ewf2_restart_data
4843        .as_deref()
4844        .map(|data| {
4845            ewf2_string_section_payload(data, options.compression, options.compression_values.level)
4846        })
4847        .transpose()?;
4848    let table_entry_bytes = u64::try_from(chunk_count)
4849        .map_err(|_| EwfError::Malformed("writer segment chunk count does not fit u64".into()))?
4850        .checked_mul(ewf2::TABLE_ENTRY_SIZE as u64)
4851        .ok_or_else(|| EwfError::Malformed("writer EWF2 table entry size overflow".into()))?;
4852    let table_data_size = (EWF2_TABLE_HEADER_V2_SIZE as u64)
4853        .checked_add(table_entry_bytes)
4854        .and_then(|value| value.checked_add(EWF2_TABLE_FOOTER_SIZE as u64))
4855        .ok_or_else(|| EwfError::Malformed("writer EWF2 table size overflow".into()))?;
4856
4857    checked_sum(
4858        [
4859            ewf2::FILE_HEADER_SIZE as u64,
4860            estimated_ewf2_section_size(device_information.len() as u64)?,
4861            estimated_ewf2_section_size(case_data.len() as u64)?,
4862            error_table
4863                .as_ref()
4864                .map(|payload| estimated_ewf2_section_size(payload.len() as u64))
4865                .transpose()?
4866                .unwrap_or(0),
4867            session_table
4868                .as_ref()
4869                .map(|payload| estimated_ewf2_section_size(payload.len() as u64))
4870                .transpose()?
4871                .unwrap_or(0),
4872            memory_extents_table
4873                .as_ref()
4874                .map(|payload| estimated_ewf2_section_size(payload.len() as u64))
4875                .transpose()?
4876                .unwrap_or(0),
4877            increment_data_size,
4878            single_files_table_0x21
4879                .as_ref()
4880                .map(|payload| estimated_ewf2_section_size(payload.len() as u64))
4881                .transpose()?
4882                .unwrap_or(0),
4883            single_files_md5_hash_table
4884                .as_ref()
4885                .map(|payload| estimated_ewf2_section_size(payload.len() as u64))
4886                .transpose()?
4887                .unwrap_or(0),
4888            single_files_table_0x23
4889                .as_ref()
4890                .map(|payload| estimated_ewf2_section_size(payload.len() as u64))
4891                .transpose()?
4892                .unwrap_or(0),
4893            estimated_ewf2_section_size(table_data_size)?,
4894            estimated_ewf2_section_size(chunk_payload_size)?,
4895            md5_hash
4896                .as_ref()
4897                .map(|payload| estimated_ewf2_section_size(payload.len() as u64))
4898                .transpose()?
4899                .unwrap_or(0),
4900            sha1_hash
4901                .as_ref()
4902                .map(|payload| estimated_ewf2_section_size(payload.len() as u64))
4903                .transpose()?
4904                .unwrap_or(0),
4905            final_information_size,
4906            analytical_data
4907                .as_ref()
4908                .map(|payload| estimated_ewf2_section_size(payload.len() as u64))
4909                .transpose()?
4910                .unwrap_or(0),
4911            restart_data
4912                .as_ref()
4913                .map(|payload| estimated_ewf2_section_size(payload.len() as u64))
4914                .transpose()?
4915                .unwrap_or(0),
4916            options
4917                .single_files
4918                .as_ref()
4919                .map(ewf2_single_files_data_payload)
4920                .transpose()?
4921                .as_ref()
4922                .map(|payload| estimated_ewf2_section_size(payload.len() as u64))
4923                .transpose()?
4924                .unwrap_or(0),
4925            ewf2::SECTION_DESCRIPTOR_SIZE as u64,
4926        ],
4927        "estimated EWF2 segment size",
4928    )
4929}
4930
4931fn estimated_ewf1_section_size(data_size: u64) -> Result<u64> {
4932    (ewf1::SECTION_DESCRIPTOR_SIZE as u64)
4933        .checked_add(data_size)
4934        .ok_or_else(|| EwfError::Malformed("writer estimated EWF1 section size overflow".into()))
4935}
4936
4937fn estimated_ewf2_section_size(data_size: u64) -> Result<u64> {
4938    (ewf2::SECTION_DESCRIPTOR_SIZE as u64)
4939        .checked_add(data_size)
4940        .ok_or_else(|| EwfError::Malformed("writer estimated EWF2 section size overflow".into()))
4941}
4942
4943fn checked_sum(parts: impl IntoIterator<Item = u64>, label: &str) -> Result<u64> {
4944    parts.into_iter().try_fold(0_u64, |total, part| {
4945        total
4946            .checked_add(part)
4947            .ok_or_else(|| EwfError::Malformed(format!("writer {label} overflow")))
4948    })
4949}
4950
4951fn volume_data_size(format: WriteFormat) -> u64 {
4952    match format {
4953        WriteFormat::Ewf1Physical | WriteFormat::Ewf1Logical => VOLUME_DATA_SIZE as u64,
4954        WriteFormat::Ewf1Smart => 94,
4955        WriteFormat::Ewf2Physical | WriteFormat::Ewf2Logical => {
4956            unreachable!("EWF2 formats do not use EWF1 volume data")
4957        }
4958    }
4959}
4960
4961fn segment_path(first_path: &Path, segment_number: usize) -> Result<PathBuf> {
4962    if segment_number == 1 {
4963        return Ok(first_path.to_path_buf());
4964    }
4965    Ok(first_path.with_extension(v1_segment_extension(first_path, segment_number)?))
4966}
4967
4968fn ewf2_segment_path(first_path: &Path, segment_number: usize) -> Result<PathBuf> {
4969    if segment_number == 1 {
4970        return Ok(first_path.to_path_buf());
4971    }
4972    Ok(first_path.with_extension(v2_segment_extension(first_path, segment_number)?))
4973}
4974
4975fn publication_segment_paths(written: &[PathBuf], is_v2: bool) -> Result<Vec<PathBuf>> {
4976    let first = &written[0];
4977    let prefix = first
4978        .extension()
4979        .and_then(|value| value.to_str())
4980        .and_then(|value| value.chars().next())
4981        .unwrap_or('E');
4982    let mut paths = written.to_vec();
4983    let existing = written
4984        .iter()
4985        .filter_map(|path| path.canonicalize().ok())
4986        .collect::<Vec<_>>();
4987    let mut stale = Vec::new();
4988    for entry in fs::read_dir(crate::segment::segment_dir(first))? {
4989        let path = entry?.path();
4990        if path.file_stem() != first.file_stem() {
4991            continue;
4992        }
4993        let extension = path
4994            .extension()
4995            .and_then(|value| value.to_str())
4996            .unwrap_or_default();
4997        if !crate::segment::is_segment_extension(extension, prefix, is_v2) {
4998            continue;
4999        }
5000        if written.contains(&path)
5001            || path
5002                .canonicalize()
5003                .is_ok_and(|path| existing.contains(&path))
5004        {
5005            continue;
5006        }
5007        stale.push(path);
5008    }
5009    stale.sort();
5010    paths.extend(stale);
5011    Ok(paths)
5012}
5013
5014fn validate_secondary_segment_filename(
5015    primary_base: &Path,
5016    secondary_base: Option<&Path>,
5017) -> Result<()> {
5018    let Some(secondary_base) = secondary_base else {
5019        return Ok(());
5020    };
5021    if normalized_output_path(primary_base)? == normalized_output_path(secondary_base)? {
5022        return Err(EwfError::Unsupported(
5023            "secondary segment filename overlaps primary output".into(),
5024        ));
5025    }
5026    Ok(())
5027}
5028
5029fn ensure_secondary_segment_path_is_distinct(
5030    primary_segment_paths: &[PathBuf],
5031    secondary_path: &Path,
5032) -> Result<()> {
5033    let secondary_path = normalized_output_path(secondary_path)?;
5034    for primary_path in primary_segment_paths {
5035        if normalized_output_path(primary_path)? == secondary_path {
5036            return Err(EwfError::Unsupported(
5037                "secondary segment filename overlaps primary output".into(),
5038            ));
5039        }
5040    }
5041    Ok(())
5042}
5043
5044fn normalized_output_path(path: &Path) -> Result<PathBuf> {
5045    let absolute = if path.is_absolute() {
5046        path.to_path_buf()
5047    } else {
5048        std::env::current_dir()?.join(path)
5049    };
5050    let mut normalized = PathBuf::new();
5051    for component in absolute.components() {
5052        match component {
5053            Component::CurDir => {}
5054            Component::ParentDir => {
5055                normalized.pop();
5056            }
5057            other => normalized.push(other.as_os_str()),
5058        }
5059    }
5060    Ok(normalized)
5061}
5062
5063fn v1_segment_extension(first_path: &Path, segment_number: usize) -> Result<String> {
5064    let prefix = first_path
5065        .extension()
5066        .and_then(|value| value.to_str())
5067        .and_then(|value| value.chars().next())
5068        .unwrap_or('E')
5069        .to_ascii_uppercase();
5070    if segment_number <= 99 {
5071        return Ok(format!("{prefix}{segment_number:02}"));
5072    }
5073
5074    let alpha_index = segment_number - 100;
5075    let prefix_offset = alpha_index / (26 * 26);
5076    let prefix = char::from_u32(
5077        (prefix as u32)
5078            .checked_add(prefix_offset as u32)
5079            .ok_or_else(|| {
5080                EwfError::Unsupported("EWF1 writer segment extension prefix overflow".into())
5081            })?,
5082    )
5083    .ok_or_else(|| EwfError::Unsupported("EWF1 writer segment extension prefix overflow".into()))?;
5084    if !prefix.is_ascii_uppercase() {
5085        return Err(EwfError::Unsupported(
5086            "EWF1 writer segment count exceeds supported extensions".into(),
5087        ));
5088    }
5089
5090    let suffix = alpha_index % (26 * 26);
5091    let first = char::from(b'A' + u8::try_from(suffix / 26).expect("suffix fits u8"));
5092    let second = char::from(b'A' + u8::try_from(suffix % 26).expect("suffix fits u8"));
5093    Ok(format!("{prefix}{first}{second}"))
5094}
5095
5096fn v2_segment_extension(first_path: &Path, segment_number: usize) -> Result<String> {
5097    let prefix = first_path
5098        .extension()
5099        .and_then(|value| value.to_str())
5100        .and_then(|value| value.chars().next())
5101        .unwrap_or('E')
5102        .to_ascii_uppercase();
5103    if segment_number <= 99 {
5104        return Ok(format!("{prefix}x{segment_number:02}"));
5105    }
5106
5107    let alpha_index = segment_number - 100;
5108    let infix_offset = alpha_index / (26 * 26);
5109    let infix = char::from_u32(u32::from(b'x') + infix_offset as u32)
5110        .ok_or_else(|| EwfError::Unsupported("EWF2 writer segment extension overflow".into()))?;
5111    if !matches!(infix, 'x'..='z') {
5112        return Err(EwfError::Unsupported(
5113            "EWF2 writer segment count exceeds supported extensions".into(),
5114        ));
5115    }
5116
5117    let suffix = alpha_index % (26 * 26);
5118    let first = char::from(b'A' + u8::try_from(suffix / 26).expect("suffix fits u8"));
5119    let second = char::from(b'A' + u8::try_from(suffix % 26).expect("suffix fits u8"));
5120    Ok(format!("{prefix}{infix}{first}{second}"))
5121}
5122
5123fn volume_data(options: &WriteOptions, chunk_count: u32, sector_count: u64) -> Result<Vec<u8>> {
5124    let mut volume = vec![0; usize::try_from(volume_data_size(options.format)).expect("fits")];
5125    if options.format == WriteFormat::Ewf1Smart {
5126        volume[0] = 1;
5127    } else {
5128        volume[0] = ewf1_media_type_value(options.format, options.media_profile.media_type);
5129        volume[36] = ewf1_media_flags_value(options.format, options.media_profile);
5130    }
5131    volume[4..8].copy_from_slice(&chunk_count.to_le_bytes());
5132    volume[8..12].copy_from_slice(&options.sectors_per_chunk.to_le_bytes());
5133    volume[12..16].copy_from_slice(&options.bytes_per_sector.to_le_bytes());
5134    volume[52] = ewf1_compression_level_value(options.compression_values.level);
5135    if options.format == WriteFormat::Ewf1Smart {
5136        let sector_count = u32::try_from(sector_count)
5137            .map_err(|_| EwfError::Unsupported("EWF-S01 writer sector count exceeds u32".into()))?;
5138        volume[16..20].copy_from_slice(&sector_count.to_le_bytes());
5139        volume[85..90].copy_from_slice(b"SMART");
5140    } else {
5141        volume[16..24].copy_from_slice(&sector_count.to_le_bytes());
5142    }
5143    if let Some(identifier) = options
5144        .set_identifier
5145        .filter(|_| options.format != WriteFormat::Ewf1Smart)
5146    {
5147        volume[64..80].copy_from_slice(&identifier);
5148    }
5149    if let Some(error_granularity) = options
5150        .media_profile
5151        .error_granularity
5152        .filter(|_| options.format != WriteFormat::Ewf1Smart)
5153    {
5154        let error_granularity = u32::try_from(error_granularity).map_err(|_| {
5155            EwfError::Unsupported("EWF1 writer error granularity exceeds u32".into())
5156        })?;
5157        volume[56..60].copy_from_slice(&error_granularity.to_le_bytes());
5158    }
5159    let checksum_offset = volume.len() - 4;
5160    let checksum = adler32(&volume[..checksum_offset]);
5161    volume[checksum_offset..].copy_from_slice(&checksum.to_le_bytes());
5162    Ok(volume)
5163}
5164
5165fn ewf1_compression_level_value(level: WriteCompressionLevel) -> u8 {
5166    match level {
5167        WriteCompressionLevel::Default | WriteCompressionLevel::None => 0,
5168        WriteCompressionLevel::Fast => 1,
5169        WriteCompressionLevel::Best => 2,
5170    }
5171}
5172
5173fn ewf1_media_type_value(format: WriteFormat, media_type: Option<MediaType>) -> u8 {
5174    media_type.map_or_else(
5175        || match format {
5176            WriteFormat::Ewf1Logical => 0x0e,
5177            WriteFormat::Ewf1Physical | WriteFormat::Ewf1Smart => 0x00,
5178            WriteFormat::Ewf2Physical | WriteFormat::Ewf2Logical => {
5179                unreachable!("EWF2 formats do not use EWF1 media type values")
5180            }
5181        },
5182        ewf1_media_type_byte,
5183    )
5184}
5185
5186fn ewf1_media_type_byte(media_type: MediaType) -> u8 {
5187    match media_type {
5188        MediaType::Removable => 0x00,
5189        MediaType::Fixed => 0x01,
5190        MediaType::Optical => 0x03,
5191        MediaType::SingleFiles => 0x0e,
5192        MediaType::Memory => 0x10,
5193        MediaType::Unknown(value) => value,
5194    }
5195}
5196
5197fn ewf1_media_flags_value(format: WriteFormat, profile: WriteMediaProfile) -> u8 {
5198    let mut flags = 0x01;
5199    if format == WriteFormat::Ewf1Physical {
5200        flags |= 0x02;
5201    }
5202    if profile.fastbloc {
5203        flags |= 0x04;
5204    }
5205    if profile.tableau {
5206        flags |= 0x08;
5207    }
5208    flags
5209}
5210
5211fn table_header(chunk_count: u32, sectors_data_offset: u64) -> [u8; 24] {
5212    let mut table = [0; 24];
5213    table[0..4].copy_from_slice(&chunk_count.to_le_bytes());
5214    table[8..16].copy_from_slice(&sectors_data_offset.to_le_bytes());
5215    let checksum = adler32(&table[..20]);
5216    table[20..24].copy_from_slice(&checksum.to_le_bytes());
5217    table
5218}
5219
5220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5221struct Ewf1TableGroup {
5222    first_chunk: usize,
5223    end_chunk: usize,
5224    payload_size: u64,
5225}
5226
5227#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
5228struct Ewf1TableGroupState {
5229    group_count: u64,
5230    current_entry_count: usize,
5231    current_payload_size: u64,
5232}
5233
5234impl Ewf1TableGroupState {
5235    fn add_chunk(self, data_size: u64, max_entries: usize, max_payload: u64) -> Result<Self> {
5236        let proposed_payload_size = self
5237            .current_payload_size
5238            .checked_add(data_size)
5239            .ok_or_else(|| EwfError::Malformed("writer table group payload overflow".into()))?;
5240        if self.current_entry_count != 0
5241            && (self.current_entry_count >= max_entries || proposed_payload_size > max_payload)
5242        {
5243            return Ok(Self {
5244                group_count: self.group_count.checked_add(1).ok_or_else(|| {
5245                    EwfError::Malformed("writer table group count overflow".into())
5246                })?,
5247                current_entry_count: 1,
5248                current_payload_size: data_size,
5249            });
5250        }
5251
5252        Ok(Self {
5253            group_count: self.group_count.max(1),
5254            current_entry_count: self.current_entry_count.checked_add(1).ok_or_else(|| {
5255                EwfError::Malformed("writer table group entry count overflow".into())
5256            })?,
5257            current_payload_size: proposed_payload_size,
5258        })
5259    }
5260}
5261
5262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5263struct Ewf1TableGroupLayout {
5264    first_chunk: usize,
5265    end_chunk: usize,
5266    payload_size: u64,
5267    sectors_desc_offset: u64,
5268    sectors_data_offset: u64,
5269    table_desc_offset: u64,
5270    table_data_size: u64,
5271    table2_desc_offset: Option<u64>,
5272    end_offset: u64,
5273}
5274
5275fn ewf1_table_groups(
5276    chunks: &[ChunkDescriptor],
5277    max_entries: usize,
5278    max_payload: u64,
5279) -> Result<Vec<Ewf1TableGroup>> {
5280    let mut groups = Vec::new();
5281    let mut first_chunk = 0_usize;
5282    let mut state = Ewf1TableGroupState::default();
5283    for (index, chunk) in chunks.iter().enumerate() {
5284        let next_state = state.add_chunk(chunk.data_size, max_entries, max_payload)?;
5285        if next_state.group_count > state.group_count && state.current_entry_count != 0 {
5286            groups.push(Ewf1TableGroup {
5287                first_chunk,
5288                end_chunk: index,
5289                payload_size: state.current_payload_size,
5290            });
5291            first_chunk = index;
5292        }
5293        state = next_state;
5294    }
5295    groups.push(Ewf1TableGroup {
5296        first_chunk,
5297        end_chunk: chunks.len(),
5298        payload_size: state.current_payload_size,
5299    });
5300    Ok(groups)
5301}
5302
5303fn ewf1_table_group_layouts(
5304    chunks: &[ChunkDescriptor],
5305    first_sectors_desc_offset: u64,
5306    table_footer_bytes: u64,
5307    writes_table2: bool,
5308) -> Result<Vec<Ewf1TableGroupLayout>> {
5309    let groups = ewf1_table_groups(
5310        chunks,
5311        EWF1_TABLE_GROUP_MAX_ENTRIES,
5312        EWF1_TABLE_GROUP_MAX_PAYLOAD,
5313    )?;
5314    let mut layouts = Vec::with_capacity(groups.len());
5315    let mut cursor = first_sectors_desc_offset;
5316    for group in groups {
5317        let table_entry_bytes = u64::try_from(group.end_chunk - group.first_chunk)
5318            .map_err(|_| EwfError::Malformed("writer table entry count does not fit u64".into()))?
5319            .checked_mul(4)
5320            .ok_or_else(|| EwfError::Malformed("writer table entry size overflow".into()))?;
5321        let table_data_size = 24_u64
5322            .checked_add(table_entry_bytes)
5323            .and_then(|value| value.checked_add(table_footer_bytes))
5324            .ok_or_else(|| EwfError::Malformed("writer table data size overflow".into()))?;
5325        let sectors_desc_offset = cursor;
5326        let sectors_data_offset = sectors_desc_offset
5327            .checked_add(ewf1::SECTION_DESCRIPTOR_SIZE as u64)
5328            .ok_or_else(|| EwfError::Malformed("writer sectors data offset overflow".into()))?;
5329        let table_desc_offset = sectors_data_offset
5330            .checked_add(group.payload_size)
5331            .ok_or_else(|| EwfError::Malformed("writer table descriptor offset overflow".into()))?;
5332        let table_end_offset = table_desc_offset
5333            .checked_add(ewf1::SECTION_DESCRIPTOR_SIZE as u64)
5334            .and_then(|value| value.checked_add(table_data_size))
5335            .ok_or_else(|| EwfError::Malformed("writer table section range overflow".into()))?;
5336        let (table2_desc_offset, end_offset) = if writes_table2 {
5337            let end_offset = table_end_offset
5338                .checked_add(ewf1::SECTION_DESCRIPTOR_SIZE as u64)
5339                .and_then(|value| value.checked_add(table_data_size))
5340                .ok_or_else(|| {
5341                    EwfError::Malformed("writer table2 section range overflow".into())
5342                })?;
5343            (Some(table_end_offset), end_offset)
5344        } else {
5345            (None, table_end_offset)
5346        };
5347        layouts.push(Ewf1TableGroupLayout {
5348            first_chunk: group.first_chunk,
5349            end_chunk: group.end_chunk,
5350            payload_size: group.payload_size,
5351            sectors_desc_offset,
5352            sectors_data_offset,
5353            table_desc_offset,
5354            table_data_size,
5355            table2_desc_offset,
5356            end_offset,
5357        });
5358        cursor = end_offset;
5359    }
5360    Ok(layouts)
5361}
5362
5363fn append_ewf1_table_data(
5364    bytes: &mut Vec<u8>,
5365    chunk_count: u32,
5366    sectors_data_offset: u64,
5367    chunks: &[ChunkDescriptor],
5368    include_footer: bool,
5369) -> Result<()> {
5370    bytes.extend_from_slice(&table_header(chunk_count, sectors_data_offset));
5371    let table_entries_start = bytes.len();
5372    let mut relative_offset = 0_u64;
5373    for chunk in chunks {
5374        let mut entry = u32::try_from(relative_offset).map_err(|_| {
5375            EwfError::Unsupported("EWF1 writer raw chunk offset exceeds u32".into())
5376        })?;
5377        if entry & 0x8000_0000 != 0 {
5378            return Err(EwfError::Unsupported(
5379                "EWF1 writer raw chunk offset exceeds 31 bits".into(),
5380            ));
5381        }
5382        if chunk.compressed {
5383            entry |= 0x8000_0000;
5384        }
5385        bytes.extend_from_slice(&entry.to_le_bytes());
5386        relative_offset = relative_offset
5387            .checked_add(chunk.data_size)
5388            .ok_or_else(|| EwfError::Malformed("writer chunk offset overflow".into()))?;
5389    }
5390    if include_footer {
5391        let entries_checksum = adler32(&bytes[table_entries_start..]);
5392        bytes.extend_from_slice(&entries_checksum.to_le_bytes());
5393    }
5394    Ok(())
5395}
5396
5397fn section_desc(section_type: &[u8], next: u64, size: u64) -> [u8; ewf1::SECTION_DESCRIPTOR_SIZE] {
5398    let mut desc = [0; ewf1::SECTION_DESCRIPTOR_SIZE];
5399    desc[..section_type.len()].copy_from_slice(section_type);
5400    desc[16..24].copy_from_slice(&next.to_le_bytes());
5401    desc[24..32].copy_from_slice(&size.to_le_bytes());
5402    let checksum = adler32(&desc[..ewf1::SECTION_DESCRIPTOR_SIZE - 4]);
5403    desc[ewf1::SECTION_DESCRIPTOR_SIZE - 4..].copy_from_slice(&checksum.to_le_bytes());
5404    desc
5405}
5406
5407fn header_payload(
5408    metadata: &EwfMetadata,
5409    header_codepage: HeaderCodepage,
5410    compression_level: WriteCompressionLevel,
5411) -> Result<Option<Vec<u8>>> {
5412    ewf1_header_text(metadata, Ewf1HeaderDateStyle::Header)
5413        .map(|text| {
5414            zlib_payload(
5415                &encode_header_text(&text, header_codepage),
5416                compression_level,
5417            )
5418        })
5419        .transpose()
5420}
5421
5422fn header2_payload(
5423    metadata: &EwfMetadata,
5424    compression_level: WriteCompressionLevel,
5425) -> Result<Option<Vec<u8>>> {
5426    ewf1_header_text(metadata, Ewf1HeaderDateStyle::Header2)
5427        .map(|text| zlib_payload(&utf16le(&text), compression_level))
5428        .transpose()
5429}
5430
5431fn xheader_payload(
5432    metadata: &EwfMetadata,
5433    compression_level: WriteCompressionLevel,
5434) -> Result<Option<Vec<u8>>> {
5435    let fields = [
5436        ("case_number", metadata.case_number.as_deref()),
5437        ("description", metadata.description.as_deref()),
5438        ("examiner_name", metadata.examiner.as_deref()),
5439        ("evidence_number", metadata.evidence_number.as_deref()),
5440        ("notes", metadata.notes.as_deref()),
5441        ("acquiry_operating_system", metadata.os_version.as_deref()),
5442        ("acquiry_date", metadata.acquisition_date.as_deref()),
5443        ("acquiry_software", metadata.acquisition_software.as_deref()),
5444        (
5445            "acquiry_software_version",
5446            metadata.acquisition_software_version.as_deref(),
5447        ),
5448        ("password", metadata.password.as_deref()),
5449    ];
5450    if fields.iter().all(|(_, value)| value.is_none()) && metadata.header_values.is_empty() {
5451        return Ok(None);
5452    }
5453
5454    let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<xheader>\n");
5455    for (tag, value) in fields {
5456        let Some(value) = value else {
5457            continue;
5458        };
5459        if value.is_empty() {
5460            continue;
5461        }
5462        xml.push('\t');
5463        xml.push('<');
5464        xml.push_str(tag);
5465        xml.push('>');
5466        xml.push_str(&escape_xml_text(&xheader_field_value(tag, value)));
5467        xml.push_str("</");
5468        xml.push_str(tag);
5469        xml.push_str(">\n");
5470    }
5471    for (tag, value) in &metadata.header_values {
5472        if fields.iter().any(|(existing_tag, _)| existing_tag == tag) || value.is_empty() {
5473            continue;
5474        }
5475        xml.push('\t');
5476        xml.push('<');
5477        xml.push_str(tag);
5478        xml.push('>');
5479        xml.push_str(&escape_xml_text(&xheader_field_value(tag, value)));
5480        xml.push_str("</");
5481        xml.push_str(tag);
5482        xml.push_str(">\n");
5483    }
5484    xml.push_str("</xheader>\n\n");
5485
5486    let mut payload = vec![0xef, 0xbb, 0xbf];
5487    payload.extend_from_slice(xml.as_bytes());
5488    Ok(Some(zlib_payload(&payload, compression_level)?))
5489}
5490
5491fn xheader_field_value<'a>(tag: &str, value: &'a str) -> Cow<'a, str> {
5492    if tag == "acquiry_date" {
5493        format_xheader_date_value(value)
5494    } else {
5495        Cow::Borrowed(value)
5496    }
5497}
5498
5499fn zlib_payload(payload: &[u8], compression_level: WriteCompressionLevel) -> Result<Vec<u8>> {
5500    let mut encoder = ZlibEncoder::new(Vec::new(), zlib_compression(compression_level));
5501    encoder.write_all(payload)?;
5502    Ok(encoder.finish()?)
5503}
5504
5505fn escape_xml_text(value: &str) -> String {
5506    let mut escaped = String::with_capacity(value.len());
5507    for ch in value.chars() {
5508        match ch {
5509            '&' => escaped.push_str("&amp;"),
5510            '<' => escaped.push_str("&lt;"),
5511            '>' => escaped.push_str("&gt;"),
5512            '"' => escaped.push_str("&quot;"),
5513            '\'' => escaped.push_str("&apos;"),
5514            _ => escaped.push(ch),
5515        }
5516    }
5517    escaped
5518}
5519
5520#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5521enum Ewf1HeaderDateStyle {
5522    Header,
5523    Header2,
5524}
5525
5526fn ewf1_header_text(metadata: &EwfMetadata, date_style: Ewf1HeaderDateStyle) -> Option<String> {
5527    let fields = [
5528        ("c", metadata.case_number.as_deref()),
5529        ("n", metadata.evidence_number.as_deref()),
5530        ("a", metadata.description.as_deref()),
5531        ("e", metadata.examiner.as_deref()),
5532        ("t", metadata.notes.as_deref()),
5533        ("av", metadata.acquisition_software_version.as_deref()),
5534        ("ov", metadata.os_version.as_deref()),
5535        ("m", metadata.acquisition_date.as_deref()),
5536        ("u", metadata.system_date.as_deref()),
5537        ("p", metadata.password.as_deref()),
5538    ];
5539    if fields.iter().all(|(_, value)| value.is_none()) && metadata.header_values.is_empty() {
5540        return None;
5541    }
5542
5543    let mut names = Vec::new();
5544    let mut values = Vec::new();
5545    for (name, value) in fields {
5546        if let Some(value) = value {
5547            names.push(name.to_string());
5548            values.push(sanitize_header_value(&ewf1_header_field_value(
5549                name, value, date_style,
5550            )));
5551        }
5552    }
5553    for (name, value) in &metadata.header_values {
5554        let tag = ewf1_header_tag(name);
5555        if metadata.password.is_some() && tag == "p" {
5556            continue;
5557        }
5558        if names.iter().any(|existing| existing == tag) {
5559            continue;
5560        }
5561        names.push(tag.to_string());
5562        values.push(sanitize_header_value(&ewf1_header_field_value(
5563            tag, value, date_style,
5564        )));
5565    }
5566
5567    let text = format!("1\nmain\n{}\n{}\n", names.join("\t"), values.join("\t"));
5568    Some(text)
5569}
5570
5571fn ewf1_header_tag(identifier: &str) -> &str {
5572    match identifier {
5573        "acquiry_date" => "m",
5574        "acquiry_operating_system" => "ov",
5575        "acquiry_software_version" => "av",
5576        "case_number" => "c",
5577        "compression_level" => "r",
5578        "description" => "a",
5579        "device_label" => "l",
5580        "evidence_number" => "n",
5581        "examiner_name" => "e",
5582        "extents" => "ext",
5583        "model" => "md",
5584        "notes" => "t",
5585        "password" => "p",
5586        "process_identifier" => "pid",
5587        "serial_number" => "sn",
5588        "system_date" => "u",
5589        "unknown_dc" => "dc",
5590        _ => identifier,
5591    }
5592}
5593
5594fn ewf1_header_field_value<'a>(
5595    name: &str,
5596    value: &'a str,
5597    date_style: Ewf1HeaderDateStyle,
5598) -> Cow<'a, str> {
5599    if !matches!(name, "m" | "u" | "acquiry_date" | "system_date") {
5600        return Cow::Borrowed(value);
5601    }
5602
5603    match date_style {
5604        Ewf1HeaderDateStyle::Header => format_ewf1_header_date_value(value),
5605        Ewf1HeaderDateStyle::Header2 => format_ewf1_header2_date_value(value),
5606    }
5607}
5608
5609fn sanitize_header_value(value: &str) -> String {
5610    value
5611        .chars()
5612        .map(|ch| {
5613            if matches!(ch, '\t' | '\n' | '\r') {
5614                ' '
5615            } else {
5616                ch
5617            }
5618        })
5619        .collect()
5620}
5621
5622fn digest_payload(hashes: &WriteHashes) -> Option<[u8; 80]> {
5623    if hashes.md5.is_none() && hashes.sha1.is_none() {
5624        return None;
5625    }
5626
5627    let mut digest = [0; 80];
5628    if let Some(md5) = hashes.md5 {
5629        digest[..16].copy_from_slice(&md5);
5630    }
5631    if let Some(sha1) = hashes.sha1 {
5632        digest[16..36].copy_from_slice(&sha1);
5633    }
5634    let checksum = adler32(&digest[..76]);
5635    digest[76..80].copy_from_slice(&checksum.to_le_bytes());
5636    Some(digest)
5637}
5638
5639fn xhash_payload(
5640    hashes: &WriteHashes,
5641    compression_level: WriteCompressionLevel,
5642) -> Result<Option<Vec<u8>>> {
5643    if hashes.md5.is_none() && hashes.sha1.is_none() && hashes.hash_values.is_empty() {
5644        return Ok(None);
5645    }
5646
5647    let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<xhash>\n");
5648    if let Some(md5) = hashes.md5 {
5649        xml.push_str("\t<md5>");
5650        xml.push_str(&hex_string(&md5));
5651        xml.push_str("</md5>\n");
5652    }
5653    if let Some(sha1) = hashes.sha1 {
5654        xml.push_str("\t<sha1>");
5655        xml.push_str(&hex_string(&sha1));
5656        xml.push_str("</sha1>\n");
5657    }
5658    for (tag, value) in &hashes.hash_values {
5659        if matches!(tag.as_str(), "MD5" | "md5" | "SHA1" | "sha1") || value.is_empty() {
5660            continue;
5661        }
5662        xml.push('\t');
5663        xml.push('<');
5664        xml.push_str(tag);
5665        xml.push('>');
5666        xml.push_str(&escape_xml_text(value));
5667        xml.push_str("</");
5668        xml.push_str(tag);
5669        xml.push_str(">\n");
5670    }
5671    xml.push_str("</xhash>\n\n");
5672
5673    let mut payload = vec![0xef, 0xbb, 0xbf];
5674    payload.extend_from_slice(xml.as_bytes());
5675    Ok(Some(zlib_payload(&payload, compression_level)?))
5676}
5677
5678fn hex_string(bytes: &[u8]) -> String {
5679    const HEX: &[u8; 16] = b"0123456789abcdef";
5680    let mut out = String::with_capacity(bytes.len() * 2);
5681    for byte in bytes {
5682        out.push(char::from(HEX[(byte >> 4) as usize]));
5683        out.push(char::from(HEX[(byte & 0x0f) as usize]));
5684    }
5685    out
5686}
5687
5688#[cfg(test)]
5689mod tests {
5690    use super::*;
5691
5692    fn chunk(data_size: u64) -> ChunkDescriptor {
5693        ChunkDescriptor {
5694            data_offset: 0,
5695            data_size,
5696            compressed: false,
5697            has_checksum: false,
5698            pattern_fill: None,
5699        }
5700    }
5701
5702    #[test]
5703    fn table_groups_split_at_31_bit_payload_cap() {
5704        let one_gib = 0x4000_0000_u64;
5705        let chunks = vec![chunk(one_gib), chunk(one_gib), chunk(one_gib)];
5706
5707        let groups = ewf1_table_groups(
5708            &chunks,
5709            EWF1_TABLE_GROUP_MAX_ENTRIES,
5710            EWF1_TABLE_GROUP_MAX_PAYLOAD,
5711        )
5712        .unwrap();
5713
5714        assert_eq!(groups.len(), 3);
5715        for group in &groups {
5716            assert!(group.payload_size <= EWF1_TABLE_GROUP_MAX_PAYLOAD);
5717        }
5718    }
5719
5720    #[test]
5721    fn table_groups_split_at_entry_cap() {
5722        let chunks = vec![chunk(512); 5];
5723
5724        let groups = ewf1_table_groups(&chunks, 2, EWF1_TABLE_GROUP_MAX_PAYLOAD).unwrap();
5725
5726        assert_eq!(
5727            groups,
5728            [
5729                Ewf1TableGroup {
5730                    first_chunk: 0,
5731                    end_chunk: 2,
5732                    payload_size: 1024
5733                },
5734                Ewf1TableGroup {
5735                    first_chunk: 2,
5736                    end_chunk: 4,
5737                    payload_size: 1024
5738                },
5739                Ewf1TableGroup {
5740                    first_chunk: 4,
5741                    end_chunk: 5,
5742                    payload_size: 512
5743                },
5744            ]
5745        );
5746    }
5747
5748    #[test]
5749    fn table_group_count_estimate_accounts_for_interacting_limits() {
5750        let large_chunk_size =
5751            EWF1_TABLE_GROUP_MAX_PAYLOAD / EWF1_TABLE_GROUP_MAX_ENTRIES as u64 + 1;
5752        let mut chunks = vec![chunk(1); EWF1_TABLE_GROUP_MAX_ENTRIES];
5753        chunks.extend(std::iter::repeat_n(
5754            chunk(large_chunk_size),
5755            EWF1_TABLE_GROUP_MAX_ENTRIES,
5756        ));
5757        let actual_groups = ewf1_table_groups(
5758            &chunks,
5759            EWF1_TABLE_GROUP_MAX_ENTRIES,
5760            EWF1_TABLE_GROUP_MAX_PAYLOAD,
5761        )
5762        .unwrap();
5763        let estimated_state = chunks
5764            .iter()
5765            .try_fold(Ewf1TableGroupState::default(), |state, chunk| {
5766                state.add_chunk(
5767                    chunk.data_size,
5768                    EWF1_TABLE_GROUP_MAX_ENTRIES,
5769                    EWF1_TABLE_GROUP_MAX_PAYLOAD,
5770                )
5771            })
5772            .unwrap();
5773
5774        assert_eq!(actual_groups.len(), 3);
5775        assert_eq!(estimated_state.group_count, actual_groups.len() as u64);
5776    }
5777
5778    #[test]
5779    fn segment_groups_respect_maximum_size_when_table_limits_interact() {
5780        let large_chunk_size =
5781            EWF1_TABLE_GROUP_MAX_PAYLOAD / EWF1_TABLE_GROUP_MAX_ENTRIES as u64 + 1;
5782        let mut chunks = vec![chunk(1); EWF1_TABLE_GROUP_MAX_ENTRIES];
5783        chunks.extend(std::iter::repeat_n(
5784            chunk(large_chunk_size),
5785            EWF1_TABLE_GROUP_MAX_ENTRIES,
5786        ));
5787        let options = WriteOptions::default();
5788        let payload_size = chunks.iter().map(|chunk| chunk.data_size).sum();
5789        let old_aggregate_estimate =
5790            estimated_ewf1_segment_size(chunks.len(), payload_size, 2, &options).unwrap();
5791
5792        let segments =
5793            segment_groups(&chunks, Some(old_aggregate_estimate), &options, 0, 0).unwrap();
5794
5795        assert!(segments.len() > 1);
5796        for segment_range in segments {
5797            let segment = &chunks[segment_range];
5798            let payload_size = segment.iter().map(|chunk| chunk.data_size).sum();
5799            let table_group_state = segment
5800                .iter()
5801                .try_fold(Ewf1TableGroupState::default(), |state, chunk| {
5802                    state.add_chunk(
5803                        chunk.data_size,
5804                        EWF1_TABLE_GROUP_MAX_ENTRIES,
5805                        EWF1_TABLE_GROUP_MAX_PAYLOAD,
5806                    )
5807                })
5808                .unwrap();
5809            let estimated_size = estimated_ewf1_segment_size(
5810                segment.len(),
5811                payload_size,
5812                table_group_state.group_count,
5813                &options,
5814            )
5815            .unwrap();
5816            assert!(estimated_size <= old_aggregate_estimate);
5817        }
5818    }
5819
5820    #[test]
5821    fn segment_groups_return_ranges_into_descriptor_slice() {
5822        let chunks = vec![chunk(512); 3];
5823        let options = WriteOptions::default();
5824        let one_chunk_segment_size = estimated_ewf1_segment_size(1, 512, 1, &options).unwrap();
5825
5826        let groups: Vec<std::ops::Range<usize>> =
5827            segment_groups(&chunks, Some(one_chunk_segment_size), &options, 0, 0).unwrap();
5828
5829        assert_eq!(groups, [0..1, 1..2, 2..3]);
5830    }
5831
5832    #[test]
5833    fn table_groups_keep_small_payload_in_one_group() {
5834        let chunks = vec![chunk(32_768), chunk(32_768)];
5835
5836        let groups = ewf1_table_groups(
5837            &chunks,
5838            EWF1_TABLE_GROUP_MAX_ENTRIES,
5839            EWF1_TABLE_GROUP_MAX_PAYLOAD,
5840        )
5841        .unwrap();
5842
5843        assert_eq!(groups.len(), 1);
5844        assert_eq!(groups[0].payload_size, 65_536);
5845    }
5846
5847    #[test]
5848    fn table_groups_cover_empty_chunk_list_with_one_empty_group() {
5849        let groups = ewf1_table_groups(
5850            &[],
5851            EWF1_TABLE_GROUP_MAX_ENTRIES,
5852            EWF1_TABLE_GROUP_MAX_PAYLOAD,
5853        )
5854        .unwrap();
5855
5856        assert_eq!(
5857            groups,
5858            [Ewf1TableGroup {
5859                first_chunk: 0,
5860                end_chunk: 0,
5861                payload_size: 0
5862            }]
5863        );
5864    }
5865
5866    #[test]
5867    fn table_group_layouts_chain_sequential_section_offsets() {
5868        let chunks = vec![chunk(0x4000_0000), chunk(0x4000_0000), chunk(0x4000_0000)];
5869
5870        let layouts = ewf1_table_group_layouts(&chunks, 1000, 4, true).unwrap();
5871
5872        assert_eq!(layouts.len(), 3);
5873        let mut expected_offset = 1000;
5874        for (index, layout) in layouts.iter().enumerate() {
5875            assert_eq!(layout.sectors_desc_offset, expected_offset);
5876            assert_eq!(layout.sectors_data_offset, expected_offset + 76);
5877            assert_eq!(
5878                layout.table_desc_offset,
5879                layout.sectors_data_offset + 0x4000_0000
5880            );
5881            assert_eq!(layout.table_data_size, 24 + 4 + 4);
5882            assert_eq!(
5883                layout.table2_desc_offset,
5884                Some(layout.table_desc_offset + 76 + layout.table_data_size)
5885            );
5886            assert_eq!(layout.first_chunk, index);
5887            assert_eq!(layout.end_chunk, index + 1);
5888            expected_offset = layout.end_offset;
5889        }
5890    }
5891}