Skip to main content

ewf_image/writer/
sequential.rs

1//! Append-only EWF1 physical and EWF2 encoding with bounded payload scratch.
2
3use super::{
4    ChunkDescriptor, ChunkSpool, EWF2_DONE_SECTION, EWF2_NEXT_SECTION, Ewf1SegmentSections,
5    Ewf1SegmentWriteContext, Ewf2SegmentWriteContext, WriteFormat, WriteHashState, WriteOptions,
6    WriteResult, automatic_encode_batch_chunks, effective_write_hashes, encode_chunks,
7    estimated_ewf1_segment_size, ewf2_segment_path, is_ewf2_format, normalize_maximum_segment_size,
8    normalize_media_size, publication_segment_paths, segment_path, validate_options,
9    validate_secondary_segment_filename, validate_session_ranges, write_ewf1_segment,
10    write_ewf2_segment, writer_chunk_geometry,
11};
12use crate::publication::Publication;
13use crate::{EwfError, Result};
14use std::collections::BTreeSet;
15use std::fs::{self, File};
16use std::io::Write;
17use std::path::{Path, PathBuf};
18use tempfile::NamedTempFile;
19
20/// Settings for a known-length, append-only EWF1 physical or EWF2 write.
21#[derive(Debug, Clone)]
22pub struct SequentialOptions {
23    /// Exact number of source bytes, excluding final sector padding.
24    pub source_size: u64,
25    /// Chunks per segment, independent of compressed byte size.
26    /// The uncompressed segment capacity must not exceed 512 MiB.
27    pub chunks_per_segment: u32,
28    /// EWF1 physical or EWF2 physical/logical settings. `maximum_segment_size`
29    /// is supported for EWF1 physical output only.
30    pub write: WriteOptions,
31}
32
33impl SequentialOptions {
34    /// Creates physical EWF2 settings with 16,375 chunks per segment.
35    pub fn new(source_size: u64) -> Self {
36        Self {
37            source_size,
38            chunks_per_segment: 16_375,
39            write: WriteOptions {
40                format: WriteFormat::Ewf2Physical,
41                ..WriteOptions::default()
42            },
43        }
44    }
45}
46
47/// Streams EWF1 physical or EWF2 payload into staged native segments without
48/// a full raw spool.
49///
50/// Payload scratch holds one encoded segment, plus an automatically bounded
51/// in-memory chunk-encoding batch when the `parallel` feature is enabled.
52/// Logical catalogs are written in the final segment.
53/// Catalogs and output path lists still grow with entry and segment counts.
54/// Input must match the declared size exactly. Final sector padding is zeroed
55/// and included in image digests. Errors poison the writer. Dropping it removes
56/// uncommitted staging; after process interruption use `EwfWriter::recover_output`.
57/// This writer does not support seeking or checkpoint resume.
58pub struct SequentialWriter {
59    pub(super) options: WriteOptions,
60    path: PathBuf,
61    source_size: u64,
62    position: u64,
63    logical_size: u64,
64    chunk_size: u64,
65    capacity: usize,
66    per_segment: usize,
67    total_chunks: u32,
68    encoded_chunks: u64,
69    pending: Vec<u8>,
70    current: Group,
71    hashes: WriteHashState,
72    paths: Vec<PathBuf>,
73    mirrors: Vec<PathBuf>,
74    poisoned: bool,
75    // Drop spools before the publication journal, including on Windows.
76    publication: Publication,
77}
78
79struct Group {
80    spool: ChunkSpool,
81    chunks: Vec<ChunkDescriptor>,
82}
83
84impl Group {
85    fn new(directory: &Path) -> Result<Self> {
86        Ok(Self {
87            spool: ChunkSpool {
88                file: NamedTempFile::new_in(directory)?,
89                len: 0,
90                position: 0,
91            },
92            chunks: Vec::new(),
93        })
94    }
95}
96
97impl SequentialWriter {
98    /// Starts a publication transaction. Nothing is published until `finish`.
99    pub fn create(path: impl AsRef<Path>, settings: SequentialOptions) -> Result<Self> {
100        let path = path.as_ref().to_path_buf();
101        let options = settings.write;
102        validate_options(&options)?;
103        let is_v2 = is_ewf2_format(options.format);
104        if !(is_v2 || options.format == WriteFormat::Ewf1Physical)
105            || (is_v2 && normalize_maximum_segment_size(options.maximum_segment_size).is_some())
106        {
107            return Err(EwfError::Unsupported(
108                "sequential writing supports EWF1 physical or EWF2 with chunk-count segmentation"
109                    .into(),
110            ));
111        }
112        let (chunk_size, capacity) =
113            writer_chunk_geometry(options.sectors_per_chunk, options.bytes_per_sector)?;
114        if chunk_size > 16 * 1024 * 1024
115            || settings.chunks_per_segment == 0
116            || chunk_size
117                .checked_mul(u64::from(settings.chunks_per_segment))
118                .is_none_or(|size| size > 512 * 1024 * 1024)
119        {
120            return Err(EwfError::Unsupported(
121                "sequential writing requires chunks <=16 MiB and segment capacity <=512 MiB".into(),
122            ));
123        }
124        let sector = u64::from(options.bytes_per_sector);
125        let logical_size = settings
126            .source_size
127            .checked_add(sector - 1)
128            .map(|size| size / sector * sector)
129            .ok_or_else(|| EwfError::Malformed("sequential source size overflow".into()))?;
130        if normalize_media_size(options.media_size).is_some_and(|size| size != logical_size) {
131            return Err(EwfError::Unsupported(
132                "media_size differs from padded source size".into(),
133            ));
134        }
135        let total_chunks = u32::try_from(logical_size.div_ceil(chunk_size))
136            .map_err(|_| EwfError::Unsupported("sequential chunk count exceeds u32".into()))?;
137        validate_session_ranges("sessions", &options.sessions, logical_size / sector)?;
138        validate_session_ranges("tracks", &options.tracks, logical_size / sector)?;
139        let per_segment = segment_chunk_limit(&options, chunk_size, settings.chunks_per_segment)?;
140        let path_for_segment = if is_v2 {
141            ewf2_segment_path
142        } else {
143            segment_path
144        };
145        validate_secondary_segment_filename(&path, options.secondary_segment_filename.as_deref())?;
146        if let Some(secondary) = &options.secondary_segment_filename {
147            validate_namespaces(
148                &path,
149                secondary,
150                (total_chunks as usize).div_ceil(per_segment).max(1),
151                path_for_segment,
152            )?;
153        }
154        let publication = Publication::begin(
155            &path,
156            options.secondary_segment_filename.as_deref(),
157            options.overwrite_existing,
158        )?;
159        let staged = publication.stage(0, &path)?;
160        let current = Group::new(staged.parent().expect("staged parent"))?;
161        Ok(Self {
162            options,
163            path,
164            source_size: settings.source_size,
165            position: 0,
166            logical_size,
167            chunk_size,
168            capacity,
169            per_segment,
170            total_chunks,
171            encoded_chunks: 0,
172            pending: Vec::with_capacity(capacity),
173            current,
174            hashes: WriteHashState::new(),
175            paths: Vec::new(),
176            mirrors: Vec::new(),
177            poisoned: false,
178            publication,
179        })
180    }
181
182    /// Source bytes accepted so far; excludes sector padding.
183    pub fn position(&self) -> u64 {
184        self.position
185    }
186
187    /// Appends bytes. An oversize write poisons the transaction without publishing.
188    pub fn write_all(&mut self, mut bytes: &[u8]) -> Result<()> {
189        self.healthy()?;
190        self.poisoned = true;
191        if bytes.len() as u64 > self.source_size - self.position {
192            return Err(EwfError::Malformed(
193                "sequential input exceeds declared source size".into(),
194            ));
195        }
196        let batch_size = automatic_encode_batch_chunks(self.capacity);
197        while !bytes.is_empty() {
198            let mut chunks = Vec::with_capacity(batch_size);
199            while !bytes.is_empty() && chunks.len() < batch_size {
200                let take = bytes.len().min(self.capacity - self.pending.len());
201                self.pending.extend_from_slice(&bytes[..take]);
202                self.position += take as u64;
203                bytes = &bytes[take..];
204                if self.pending.len() == self.capacity {
205                    chunks.push(std::mem::replace(
206                        &mut self.pending,
207                        Vec::with_capacity(self.capacity),
208                    ));
209                }
210            }
211            self.encode_batch(chunks)?;
212        }
213        self.poisoned = false;
214        Ok(())
215    }
216
217    fn healthy(&self) -> Result<()> {
218        if self.poisoned {
219            return Err(EwfError::Aborted);
220        }
221        Ok(())
222    }
223
224    fn encode_batch(&mut self, chunks: Vec<Vec<u8>>) -> Result<()> {
225        for chunk in &chunks {
226            self.hashes.update(chunk);
227        }
228        let encoded = encode_chunks(
229            chunks,
230            self.options.compression,
231            self.options.compression_values,
232            self.chunk_size,
233            is_ewf2_format(self.options.format),
234            !is_ewf2_format(self.options.format) && self.options.compression_values.empty_block,
235        )?;
236        for chunk in encoded {
237            self.current.chunks.push(self.current.spool.append(chunk)?);
238            self.encoded_chunks += 1;
239            if self.current.chunks.len() == self.per_segment
240                && self.encoded_chunks < u64::from(self.total_chunks)
241            {
242                let staged = self.publication.stage(0, &self.path)?;
243                let next = Group::new(staged.parent().expect("staged parent"))?;
244                let mut complete = std::mem::replace(&mut self.current, next);
245                let number = self.paths.len() + 1;
246                self.stage_group(&mut complete, number, false)?;
247                self.paths.push(self.segment_path(&self.path, number)?);
248            }
249        }
250        Ok(())
251    }
252
253    fn stage_group(&self, group: &mut Group, number: usize, last: bool) -> Result<()> {
254        let path = self.segment_path(&self.path, number)?;
255        let staged = self.publication.stage(0, &path)?;
256        let mut file = File::create(&staged)?;
257        let sector_count = self.logical_size / u64::from(self.options.bytes_per_sector);
258        if is_ewf2_format(self.options.format) {
259            write_ewf2_segment(
260                &mut file,
261                &mut group.spool,
262                &group.chunks,
263                &self.options,
264                Ewf2SegmentWriteContext {
265                    segment_number: u32::try_from(number)
266                        .map_err(|_| EwfError::Unsupported("too many segments".into()))?,
267                    first_chunk: (number as u64 - 1) * self.per_segment as u64,
268                    total_chunk_count: self.total_chunks,
269                    sector_count,
270                    terminal_section_type: if last {
271                        EWF2_DONE_SECTION
272                    } else {
273                        EWF2_NEXT_SECTION
274                    },
275                },
276            )?;
277        } else {
278            write_ewf1_segment(
279                &mut file,
280                &mut group.spool,
281                &group.chunks,
282                &self.options,
283                Ewf1SegmentWriteContext {
284                    segment_number: u16::try_from(number)
285                        .map_err(|_| EwfError::Unsupported("too many EWF1 segments".into()))?,
286                    chunk_count: self.total_chunks,
287                    sector_count,
288                    sections: Ewf1SegmentSections::for_segment(number == 1, last),
289                    terminal_section: super::TerminalSection::Done,
290                },
291            )?;
292            if let Some(limit) = normalize_maximum_segment_size(self.options.maximum_segment_size)
293                && group.chunks.len() > 1
294                && file.metadata()?.len() > limit
295            {
296                return Err(EwfError::Unsupported(
297                    "sequential EWF1 segment exceeds maximum_segment_size".into(),
298                ));
299            }
300        }
301        file.sync_all()?;
302        drop(file);
303        if let Some(base) = &self.options.secondary_segment_filename {
304            let mirror = self.segment_path(base, number)?;
305            let destination = self.publication.stage(1, &mirror)?;
306            fs::copy(staged, &destination)?;
307            fs::OpenOptions::new()
308                .write(true)
309                .open(destination)?
310                .sync_all()?;
311        }
312        Ok(())
313    }
314
315    fn segment_path(&self, base: &Path, number: usize) -> Result<PathBuf> {
316        if is_ewf2_format(self.options.format) {
317            ewf2_segment_path(base, number)
318        } else {
319            segment_path(base, number)
320        }
321    }
322
323    /// Checks the source length, pads the last sector, and publishes all segments.
324    pub fn finish(mut self) -> Result<WriteResult> {
325        self.healthy()?;
326        let is_v2 = is_ewf2_format(self.options.format);
327        if self.position != self.source_size {
328            return Err(EwfError::Malformed(
329                "sequential input is shorter than declared source size".into(),
330            ));
331        }
332        let padding = (self.logical_size - self.source_size) as usize;
333        self.pending.resize(self.pending.len() + padding, 0);
334        if !self.pending.is_empty() {
335            let chunk = std::mem::replace(&mut self.pending, Vec::with_capacity(self.capacity));
336            self.encode_batch(vec![chunk])?;
337        }
338        let (md5, sha1, sha256) = self.hashes.clone().finalize();
339        self.options.hashes = effective_write_hashes(&self.options.hashes, md5, sha1, sha256)?;
340        let staged = self.publication.stage(0, &self.path)?;
341        let empty = Group::new(staged.parent().expect("staged parent"))?;
342        let mut last = std::mem::replace(&mut self.current, empty);
343        let number = self.paths.len() + 1;
344        self.stage_group(&mut last, number, true)?;
345        self.paths.push(self.segment_path(&self.path, number)?);
346        // Remove scratch handles before publication cleanup on Windows.
347        drop(last);
348        self.current.spool.file.close()?;
349        if let Some(base) = &self.options.secondary_segment_filename {
350            for number in 1..=self.paths.len() {
351                self.mirrors.push(if is_v2 {
352                    ewf2_segment_path(base, number)?
353                } else {
354                    segment_path(base, number)?
355                });
356            }
357        }
358        let mut destinations = vec![publication_segment_paths(&self.paths, is_v2)?];
359        if !self.mirrors.is_empty() {
360            destinations.push(publication_segment_paths(&self.mirrors, is_v2)?);
361        }
362        self.publication.publish(&destinations, self.paths.len())?;
363        Ok(WriteResult {
364            segment_paths: self.paths,
365            secondary_segment_paths: self.mirrors,
366            logical_size: self.logical_size,
367            chunk_size: self.chunk_size,
368            chunk_count: u64::from(self.total_chunks),
369            computed_sha256: sha256,
370        })
371    }
372}
373
374fn segment_chunk_limit(options: &WriteOptions, chunk_size: u64, requested: u32) -> Result<usize> {
375    let Some(maximum_size) = normalize_maximum_segment_size(options.maximum_segment_size) else {
376        return Ok(requested as usize);
377    };
378    if is_ewf2_format(options.format) {
379        return Err(EwfError::Unsupported(
380            "maximum_segment_size is not supported for sequential EWF2".into(),
381        ));
382    }
383    // Use the largest encoded EWF1 chunk (raw bytes plus Adler32) and include
384    // final digest sections on every estimate. This fixes segment boundaries
385    // before source data arrives and leaves room for all first/last metadata.
386    let mut sizing = options.clone();
387    sizing.hashes = effective_write_hashes(&options.hashes, [0; 16], [0; 20], [0; 32])?;
388    let encoded_chunk_size = chunk_size
389        .checked_add(4)
390        .ok_or_else(|| EwfError::Malformed("sequential encoded chunk size overflow".into()))?;
391    let mut low = 1_u32;
392    let mut high = requested;
393    while low < high {
394        let candidate = low + (high - low).div_ceil(2);
395        let payload_size = encoded_chunk_size
396            .checked_mul(u64::from(candidate))
397            .ok_or_else(|| EwfError::Malformed("sequential segment size overflow".into()))?;
398        let estimate = estimated_ewf1_segment_size(candidate as usize, payload_size, 1, &sizing)?;
399        if estimate <= maximum_size {
400            low = candidate;
401        } else {
402            high = candidate - 1;
403        }
404    }
405    Ok(low as usize)
406}
407
408// Validate the complete namespace once, before creating a journal or accepting
409// source bytes. Resolve parent aliases and use conservative Windows case folding.
410// O(n log n) comparisons replace the former per-segment O(n^2) filesystem work.
411fn validate_namespaces(
412    primary: &Path,
413    secondary: &Path,
414    count: usize,
415    segment_path: fn(&Path, usize) -> Result<PathBuf>,
416) -> Result<()> {
417    fn key(path: &Path) -> Result<PathBuf> {
418        let path = super::normalized_output_path(path)?;
419        let parent = path
420            .parent()
421            .ok_or_else(|| EwfError::Malformed("missing output parent".into()))?;
422        let key = parent.canonicalize()?.join(
423            path.file_name()
424                .ok_or_else(|| EwfError::Malformed("missing output name".into()))?,
425        );
426        #[cfg(windows)]
427        let key = PathBuf::from(key.to_string_lossy().to_lowercase());
428        Ok(key)
429    }
430    let primary: BTreeSet<_> = (1..=count)
431        .map(|n| key(&segment_path(primary, n)?))
432        .collect::<Result<_>>()?;
433    for number in 1..=count {
434        if primary.contains(&key(&segment_path(secondary, number)?)?) {
435            return Err(EwfError::Unsupported(
436                "secondary segment filename overlaps primary output".into(),
437            ));
438        }
439    }
440    Ok(())
441}
442
443impl Write for SequentialWriter {
444    fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
445        Self::write_all(self, bytes).map_err(std::io::Error::other)?;
446        Ok(bytes.len())
447    }
448    fn flush(&mut self) -> std::io::Result<()> {
449        self.healthy().map_err(std::io::Error::other)
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use super::{SequentialOptions, SequentialWriter, WriteFormat};
456    #[test]
457    fn scratch_and_descriptors_do_not_grow_with_completed_segments() {
458        for format in [
459            WriteFormat::Ewf1Physical,
460            WriteFormat::Ewf2Physical,
461            WriteFormat::Ewf2Logical,
462        ] {
463            let dir = tempfile::tempdir().unwrap();
464            let sectors_per_chunk = if format == WriteFormat::Ewf2Logical {
465                16
466            } else {
467                1
468            };
469            let chunk_size = sectors_per_chunk * 512;
470            let mut options = SequentialOptions::new(u64::from(chunk_size) * 100);
471            options.write.format = format;
472            options.write.sectors_per_chunk = sectors_per_chunk;
473            options.chunks_per_segment = 3;
474            let mut writer = SequentialWriter::create(
475                dir.path().join(match format {
476                    WriteFormat::Ewf1Physical => "case.E01",
477                    WriteFormat::Ewf2Physical => "case.Ex01",
478                    WriteFormat::Ewf2Logical => "case.Lx01",
479                    _ => unreachable!(),
480                }),
481                options,
482            )
483            .unwrap();
484            let chunk: Vec<u8> = (0..chunk_size).map(|n| (n % 251) as u8).collect();
485            for _ in 0..100 {
486                writer.write_all(&chunk).unwrap();
487                assert!(writer.current.chunks.len() <= 3);
488                assert!(writer.current.spool.len <= u64::from(3 * (chunk_size + 4)));
489                assert!(writer.pending.len() < chunk_size as usize);
490            }
491            writer.finish().unwrap();
492        }
493    }
494}