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