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