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