Skip to main content

ewf_image/writer/acquisition/
source.rs

1use std::io::{self, Read, Seek, SeekFrom, Write};
2use std::ops::ControlFlow;
3
4use super::super::{encode_chunks, segment_path};
5use super::{AcquisitionError, AcquisitionWriter, EwfError, Result};
6
7const AUTOMATIC_BULK_READ_BYTES: usize = 256 * 1024;
8
9/// Treatment of a sector that remains unreadable after its retries.
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
11pub enum UnreadableSectorPolicy {
12    /// Stop without substituting data. Successfully accepted input is retained.
13    #[default]
14    Stop,
15    /// Write a zero sector and record an acquisition-error range in the EWF.
16    /// EOF, seek, permission, configuration, timeout, and interrupted-operation errors
17    /// always stop; they are never converted to apparently acquired data.
18    ZeroFill,
19}
20
21/// Source-reading policy; it can be changed between calls or after resume.
22/// Previously substituted sectors and their provenance are never rewritten.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct AcquisitionReadOptions {
25    /// Additional attempts per failed sector, from 0 through 100. Default: 2.
26    /// A failed bulk read first falls back to individual sectors; bulk attempts
27    /// do not consume a sector's retry allowance.
28    pub retries: u32,
29    /// Behavior after a sector's attempts are exhausted. Defaults to stopping.
30    pub unreadable_sector_policy: UnreadableSectorPolicy,
31    /// Maximum disjoint substituted-sector ranges retained in memory.
32    /// Defaults to 65,536. Reaching the limit stops before substituting another
33    /// disjoint range. Adjacent failed sectors share a range.
34    pub maximum_error_ranges: usize,
35    /// Optional logical-byte interval for early checkpoints. Must be a positive
36    /// multiple of the chunk size. Segment boundaries also checkpoint normally.
37    pub checkpoint_interval: Option<u64>,
38}
39
40impl Default for AcquisitionReadOptions {
41    fn default() -> Self {
42        Self {
43            retries: 2,
44            unreadable_sector_policy: UnreadableSectorPolicy::Stop,
45            maximum_error_ranges: 65_536,
46            checkpoint_interval: None,
47        }
48    }
49}
50
51/// Source progress delivered synchronously on the acquiring thread.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53#[non_exhaustive]
54pub struct AcquisitionProgress {
55    /// Declared logical source size.
56    pub source_size: u64,
57    /// Logical bytes accepted, including substitutions and unsealed input.
58    pub bytes_written: u64,
59    /// Logical bytes covered by the last acknowledged checkpoint.
60    pub checkpoint_bytes: u64,
61    /// Total sealed segment count.
62    pub sealed_segments: usize,
63    /// Total substituted sectors, including those restored by resume.
64    pub substituted_sectors: u64,
65    /// Bulk and sector attempts started during this call, including retries.
66    pub read_attempts: u64,
67    /// Additional sector attempts started during this call.
68    pub retry_attempts: u64,
69    /// Offset of the current or most recent source-read attempt.
70    pub read_offset: u64,
71    /// Most recent read attempt's error, cleared before a new attempt starts.
72    pub read_error: Option<io::ErrorKind>,
73}
74
75/// Why a source acquisition call returned successfully.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum AcquisitionStatus {
78    /// The declared media range is filled, possibly with recorded substitutions.
79    /// Call `finish` to publish it; completion does not imply all sectors were read.
80    Complete,
81    /// The progress callback requested cancellation. Full chunks were checkpointed.
82    Cancelled,
83}
84
85/// Source-acquisition outcome. Publication remains an explicit `finish` step.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87#[non_exhaustive]
88pub struct AcquisitionOutcome {
89    /// Completed or cancelled.
90    pub status: AcquisitionStatus,
91    /// Final progress, including the durable and accepted byte offsets.
92    pub progress: AcquisitionProgress,
93}
94
95impl AcquisitionWriter {
96    /// Acquires from a seekable file or caller-opened device using the given policy.
97    /// Source position is set explicitly for every attempt, including after resume.
98    pub fn acquire_from<R: Read + Seek>(
99        &mut self,
100        source: &mut R,
101        options: &AcquisitionReadOptions,
102    ) -> Result<AcquisitionOutcome> {
103        self.acquire_with_progress(source, options, |_| ControlFlow::Continue(()))
104    }
105
106    /// Acquires with progress and cooperative cancellation. Returning `Break(())`
107    /// checkpoints full chunks and returns `Cancelled`; a partial chunk stays in
108    /// this writer's memory. Continue with this writer or drop it and resume from
109    /// its checkpoint offset. Callbacks run before source I/O and between reads,
110    /// retries, and writes. An in-flight OS operation or segment seal cannot be
111    /// interrupted. A callback must return promptly and must not panic.
112    ///
113    /// Healthy reads automatically group complete image chunks into a bounded
114    /// buffer. A failed bulk attempt is discarded and retried sector by sector.
115    /// Partial failed reads never enter the image.
116    /// Source failures checkpoint accepted full chunks and leave the writer usable;
117    /// destination failures poison it and require dropping and resuming.
118    pub fn acquire_with_progress<R: Read + Seek>(
119        &mut self,
120        source: &mut R,
121        options: &AcquisitionReadOptions,
122        mut on_progress: impl FnMut(AcquisitionProgress) -> ControlFlow<()>,
123    ) -> Result<AcquisitionOutcome> {
124        self.ensure_healthy()?;
125        let sector_size = self.options.bytes_per_sector as usize;
126        if !self.offset.is_multiple_of(sector_size as u64)
127            || options.retries > 100
128            || options.maximum_error_ranges == 0
129            || self.errors.len() > options.maximum_error_ranges
130            || options.checkpoint_interval.is_some_and(|interval| {
131                interval == 0 || !interval.is_multiple_of(self.chunk_size as u64)
132            })
133        {
134            return Err(EwfError::Malformed(
135                "invalid acquisition read options or unaligned source position".into(),
136            ));
137        }
138        // Early checkpoints consume native segment names too. Reject an
139        // impossible interval before starting I/O, rather than after filling
140        // the supported E01 segment namespace with a partial acquisition.
141        let maximum_segment_bytes = self.chunk_size as u64 * self.chunks_per_segment as u64;
142        let interval = options
143            .checkpoint_interval
144            .unwrap_or(maximum_segment_bytes)
145            .min(maximum_segment_bytes);
146        let remaining_segments = (self.source_size - self.checkpoint_offset()).div_ceil(interval);
147        let final_segment =
148            u64::try_from(self.sealed.len()).expect("segment count fits u64") + remaining_segments;
149        segment_path(
150            &self.first,
151            usize::try_from(final_segment)
152                .map_err(|_| EwfError::Unsupported("too many acquisition segments".into()))?,
153        )?;
154        let mut progress = self.source_progress();
155        let result = self.acquire_loop(source, options, &mut progress, &mut on_progress);
156        let status = match result {
157            Ok(()) => AcquisitionStatus::Complete,
158            Err(EwfError::Aborted) => AcquisitionStatus::Cancelled,
159            Err(error) => {
160                if !self.failed {
161                    self.checkpoint()?;
162                }
163                return Err(error);
164            }
165        };
166        self.checkpoint()?;
167        self.refresh_source_progress(&mut progress);
168        Ok(AcquisitionOutcome { status, progress })
169    }
170
171    fn source_progress(&self) -> AcquisitionProgress {
172        let mut progress = AcquisitionProgress {
173            source_size: self.source_size,
174            bytes_written: 0,
175            checkpoint_bytes: 0,
176            sealed_segments: 0,
177            substituted_sectors: 0,
178            read_attempts: 0,
179            retry_attempts: 0,
180            read_offset: self.offset,
181            read_error: None,
182        };
183        self.refresh_source_progress(&mut progress);
184        progress
185    }
186
187    fn refresh_source_progress(&self, progress: &mut AcquisitionProgress) {
188        progress.bytes_written = self.offset;
189        progress.checkpoint_bytes = self.checkpoint_offset();
190        progress.sealed_segments = self.sealed.len();
191        progress.substituted_sectors = self.substituted_sectors;
192    }
193
194    fn acquire_loop<R: Read + Seek>(
195        &mut self,
196        source: &mut R,
197        options: &AcquisitionReadOptions,
198        progress: &mut AcquisitionProgress,
199        callback: &mut impl FnMut(AcquisitionProgress) -> ControlFlow<()>,
200    ) -> Result<()> {
201        notify(*progress, callback)?;
202        let mut buffer = vec![0; automatic_bulk_read_bytes(self.chunk_size)];
203        let sector_size = self.options.bytes_per_sector as usize;
204        while self.offset < self.source_size {
205            let size = (if self.pending.is_empty() {
206                buffer.len()
207            } else {
208                self.chunk_size - self.pending.len()
209            })
210            .min(buffer.len())
211            .min((self.source_size - self.offset).min(usize::MAX as u64) as usize);
212            let read = if size > sector_size {
213                match read_attempt(source, &mut buffer[..size], self.offset, progress, callback) {
214                    Ok(()) => true,
215                    Err(AttemptError::Read(error)) if recoverable(&error) => false,
216                    Err(error) => return Err(error.into_ewf()),
217                }
218            } else {
219                false
220            };
221            if read {
222                self.append_healthy_batch(&buffer[..size], options, progress, callback)?;
223                continue;
224            }
225            // Re-read the entire failed bulk range by sector; even a successful
226            // prefix of the failed attempt may have come from an unstable source.
227            for sector in buffer[..size].chunks_mut(sector_size) {
228                let mut last_error = None;
229                for attempt in 0..=options.retries {
230                    if attempt > 0 {
231                        progress.retry_attempts += 1;
232                    }
233                    match read_attempt(source, sector, self.offset, progress, callback) {
234                        Ok(()) => {
235                            last_error = None;
236                            break;
237                        }
238                        Err(AttemptError::Read(error)) if recoverable(&error) => {
239                            last_error = Some(error);
240                        }
241                        Err(error) => return Err(error.into_ewf()),
242                    }
243                }
244                if let Some(error) = last_error {
245                    if options.unreadable_sector_policy == UnreadableSectorPolicy::Stop {
246                        return Err(error.into());
247                    }
248                    self.record_substitution(options.maximum_error_ranges)?;
249                    sector.fill(0);
250                }
251                self.write_all(sector)?;
252                self.after_source_write(options, progress, callback)?;
253            }
254        }
255        Ok(())
256    }
257
258    fn append_healthy_batch(
259        &mut self,
260        bytes: &[u8],
261        options: &AcquisitionReadOptions,
262        progress: &mut AcquisitionProgress,
263        callback: &mut impl FnMut(AcquisitionProgress) -> ControlFlow<()>,
264    ) -> Result<()> {
265        self.ensure_healthy()?;
266        if !self.pending.is_empty()
267            || bytes.is_empty()
268            || bytes.len() as u64 > self.source_size - self.offset
269        {
270            return Err(EwfError::Malformed(
271                "invalid healthy acquisition batch".into(),
272            ));
273        }
274
275        let processing_started = std::time::Instant::now();
276        let mut hash_states = Vec::with_capacity(bytes.len().div_ceil(self.chunk_size));
277        let mut hashes = self.hashes.clone();
278        let mut lengths = Vec::with_capacity(hash_states.capacity());
279        let chunks = bytes
280            .chunks(self.chunk_size)
281            .map(|chunk| {
282                hashes.update(chunk);
283                hash_states.push(hashes.clone());
284                lengths.push(chunk.len());
285                chunk.to_vec()
286            })
287            .collect();
288        let encoded = match encode_chunks(
289            chunks,
290            self.options.compression,
291            self.options.compression_values,
292            self.chunk_size as u64,
293            false,
294            false,
295        ) {
296            Ok(encoded) => encoded,
297            Err(error) => {
298                self.failed = true;
299                return Err(error);
300            }
301        };
302        self.processing_duration += processing_started.elapsed();
303
304        for ((chunk, length), hashes) in encoded.into_iter().zip(lengths).zip(hash_states) {
305            self.offset += length as u64;
306            self.hashes = hashes;
307            let scratch_started = std::time::Instant::now();
308            let descriptor = match self.spool.as_mut().expect("active spool").append(chunk) {
309                Ok(descriptor) => descriptor,
310                Err(error) => {
311                    self.failed = true;
312                    return Err(error);
313                }
314            };
315            self.chunks.push(descriptor);
316            self.scratch_write_duration += scratch_started.elapsed();
317            if (self.chunks.len() == self.chunks_per_segment || self.offset == self.source_size)
318                && let Err(error) = self.seal(self.offset == self.source_size)
319            {
320                self.failed = true;
321                return Err(error);
322            }
323            self.after_source_write(options, progress, callback)?;
324        }
325        Ok(())
326    }
327
328    fn after_source_write(
329        &mut self,
330        options: &AcquisitionReadOptions,
331        progress: &mut AcquisitionProgress,
332        callback: &mut impl FnMut(AcquisitionProgress) -> ControlFlow<()>,
333    ) -> Result<()> {
334        if options
335            .checkpoint_interval
336            .is_some_and(|interval| self.offset - self.checkpoint_offset() >= interval)
337        {
338            self.checkpoint()?;
339        }
340        self.refresh_source_progress(progress);
341        notify(*progress, callback)
342    }
343
344    fn record_substitution(&mut self, maximum_ranges: usize) -> Result<()> {
345        let first_sector = self.offset / u64::from(self.options.bytes_per_sector);
346        // The EWF1 error2 representation has 32-bit sector addresses. Never
347        // substitute data whose provenance cannot be represented on disk.
348        if first_sector > u64::from(u32::MAX) {
349            return Err(EwfError::Unsupported(
350                "unreadable sector exceeds EWF1 error-table addressing".into(),
351            ));
352        }
353        if let Some(last) = self.errors.last_mut()
354            && last.first_sector + last.sector_count == first_sector
355            && last.sector_count < u64::from(u32::MAX)
356        {
357            last.sector_count += 1;
358        } else {
359            if self.errors.len() >= maximum_ranges {
360                return Err(EwfError::Unsupported(
361                    "acquisition error-range limit reached".into(),
362                ));
363            }
364            self.errors.push(AcquisitionError {
365                first_sector,
366                sector_count: 1,
367            });
368        }
369        self.substituted_sectors += 1;
370        Ok(())
371    }
372}
373
374fn automatic_bulk_read_bytes(chunk_size: usize) -> usize {
375    (AUTOMATIC_BULK_READ_BYTES / chunk_size).max(1) * chunk_size
376}
377
378enum AttemptError {
379    Read(io::Error),
380    Fatal(EwfError),
381}
382
383impl AttemptError {
384    fn into_ewf(self) -> EwfError {
385        match self {
386            Self::Read(error) => error.into(),
387            Self::Fatal(error) => error,
388        }
389    }
390}
391
392fn read_attempt<R: Read + Seek>(
393    source: &mut R,
394    buffer: &mut [u8],
395    offset: u64,
396    progress: &mut AcquisitionProgress,
397    callback: &mut impl FnMut(AcquisitionProgress) -> ControlFlow<()>,
398) -> std::result::Result<(), AttemptError> {
399    progress.read_offset = offset;
400    progress.read_error = None;
401    notify(*progress, callback).map_err(AttemptError::Fatal)?;
402    let positioned = source
403        .seek(SeekFrom::Start(offset))
404        .map_err(|error| AttemptError::Fatal(error.into()))?;
405    if positioned != offset {
406        return Err(AttemptError::Fatal(EwfError::Malformed(
407            "source seek returned a different offset".into(),
408        )));
409    }
410    progress.read_attempts += 1;
411    let mut filled = 0;
412    while filled < buffer.len() {
413        let result = source.read(&mut buffer[filled..]);
414        let error = match result {
415            Ok(0) => Some(io::Error::new(
416                io::ErrorKind::UnexpectedEof,
417                "source ended before declared acquisition size",
418            )),
419            Ok(size) if size <= buffer.len() - filled => {
420                filled += size;
421                None
422            }
423            Ok(_) => Some(io::Error::new(
424                io::ErrorKind::InvalidInput,
425                "source returned an invalid read length",
426            )),
427            Err(error) => Some(error),
428        };
429        if let Some(error) = error {
430            progress.read_error = Some(error.kind());
431            notify(*progress, callback).map_err(AttemptError::Fatal)?;
432            return Err(AttemptError::Read(error));
433        }
434        notify(*progress, callback).map_err(AttemptError::Fatal)?;
435    }
436    Ok(())
437}
438
439fn recoverable(error: &io::Error) -> bool {
440    !matches!(
441        error.kind(),
442        io::ErrorKind::UnexpectedEof
443            | io::ErrorKind::PermissionDenied
444            | io::ErrorKind::InvalidInput
445            | io::ErrorKind::Unsupported
446            | io::ErrorKind::NotFound
447            | io::ErrorKind::Interrupted
448            | io::ErrorKind::TimedOut
449            | io::ErrorKind::WouldBlock
450            | io::ErrorKind::NotConnected
451            | io::ErrorKind::ConnectionAborted
452            | io::ErrorKind::ConnectionReset
453            | io::ErrorKind::BrokenPipe
454    )
455}
456
457fn notify(
458    progress: AcquisitionProgress,
459    callback: &mut impl FnMut(AcquisitionProgress) -> ControlFlow<()>,
460) -> Result<()> {
461    if callback(progress).is_break() {
462        Err(EwfError::Aborted)
463    } else {
464        Ok(())
465    }
466}