Skip to main content

ewf_image/writer/acquisition/
checkpoint.rs

1use std::fs;
2use std::ops::ControlFlow;
3use std::path::{Path, PathBuf};
4
5use super::{
6    AcquisitionOptions, AcquisitionWriter, Seal, WriteOptions, checkpoint_count, checkpoint_path,
7    ensure_output_absent, fingerprint, normalize_errors, normalize_first, read_fixed,
8    require_directory, require_file_type, staged_path, validate_segment_budget,
9};
10use crate::publication::{OutputLock, acquisition_path};
11use crate::{AcquisitionError, EwfError, Image, Result};
12
13/// Long-running checkpoint or publication operation currently in progress.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum AcquisitionOperationPhase {
17    /// Comparing sealed container bytes with checkpointed SHA256 values.
18    ValidatingSegments,
19    /// Decoding checkpointed media to restore acquisition digest state.
20    RehashingMedia,
21    /// Installing exclusive output links while retaining the checkpoint journal.
22    Publishing,
23}
24
25/// Progress for one validation, rehash, or publication phase.
26///
27/// Counts are local to an operation phase. Revalidation of a previously published
28/// link is a separate validation phase with its own byte and segment totals.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30#[non_exhaustive]
31pub struct AcquisitionOperationProgress {
32    /// Operation phase.
33    pub phase: AcquisitionOperationPhase,
34    /// Bytes processed in this phase: encoded bytes for validation/publication,
35    /// decoded logical bytes for media rehashing.
36    pub bytes_processed: u64,
37    /// Total bytes in this phase, using the same units as `bytes_processed`.
38    pub bytes_total: u64,
39    /// Completed segments in this phase.
40    pub segments_processed: usize,
41    /// Total segments in this phase.
42    pub segments_total: usize,
43}
44
45/// Snapshot of a stopped acquisition.
46///
47/// Inspection never cleans scratch files,
48/// rewrites records, rehashes media, or publishes output. It takes the same output
49/// lock as a writer; close an active writer first or use its live getters.
50#[derive(Debug, Clone, PartialEq, Eq)]
51#[non_exhaustive]
52pub struct AcquisitionCheckpoint {
53    /// Declared full source size.
54    pub source_size: u64,
55    /// Offset at which source input must continue after resume.
56    pub checkpoint_bytes: u64,
57    /// Logical bytes per chunk.
58    pub chunk_size: u64,
59    /// Number of sealed native segments.
60    pub sealed_segments: usize,
61    /// Combined encoded size of sealed segments, excluding scratch and records.
62    pub stored_bytes: u64,
63    /// All declared source bytes are sealed and the final EWF marker is present.
64    pub ready_to_finish: bool,
65    /// Publication has started; some output links may already exist.
66    pub publication_started: bool,
67    /// Every sealed file was checked against its recorded SHA256 in this call.
68    /// False for metadata-only inspection; true for `validate_checkpoint`.
69    pub segment_hashes_validated: bool,
70    /// Normalized substituted-sector ranges wholly inside the sealed prefix.
71    pub acquisition_errors: Vec<AcquisitionError>,
72    /// Number of substituted sectors in the sealed prefix.
73    pub substituted_sectors: u64,
74}
75
76pub(super) struct CheckpointData {
77    pub(super) state: PathBuf,
78    pub(super) sealed: Vec<Seal>,
79    pub(super) report: AcquisitionCheckpoint,
80    pub(super) image: Option<Image>,
81}
82
83impl AcquisitionWriter {
84    /// Inspects checkpoint records, segment lengths, and native metadata without
85    /// scanning media payloads. Requires the original configuration and identity.
86    /// This does not certify segment contents; use `validate_checkpoint` for that.
87    pub fn inspect_checkpoint(
88        first: impl AsRef<Path>,
89        options: &AcquisitionOptions,
90        source_identity: [u8; 32],
91    ) -> Result<AcquisitionCheckpoint> {
92        inspect(first.as_ref(), options, source_identity, false, &mut |_| {
93            ControlFlow::Continue(())
94        })
95    }
96
97    /// Inspects and validates sealed container hashes with cooperative cancellation.
98    /// Returning `Break(())` returns `EwfError::Aborted` without changing checkpoint
99    /// content. Media decoding/hashing is performed later by `resume`.
100    pub fn validate_checkpoint(
101        first: impl AsRef<Path>,
102        options: &AcquisitionOptions,
103        source_identity: [u8; 32],
104        mut on_progress: impl FnMut(AcquisitionOperationProgress) -> ControlFlow<()>,
105    ) -> Result<AcquisitionCheckpoint> {
106        inspect(
107            first.as_ref(),
108            options,
109            source_identity,
110            true,
111            &mut on_progress,
112        )
113    }
114}
115
116fn inspect(
117    first: &Path,
118    options: &AcquisitionOptions,
119    source_identity: [u8; 32],
120    validate: bool,
121    callback: &mut impl FnMut(AcquisitionOperationProgress) -> ControlFlow<()>,
122) -> Result<AcquisitionCheckpoint> {
123    let write_options = options.writer_options()?;
124    let first = normalize_first(first)?;
125    validate_segment_budget(&first, options)?;
126    let _lock = OutputLock::acquire(&first)?;
127    Ok(load_checkpoint(
128        &first,
129        options,
130        &write_options,
131        source_identity,
132        validate,
133        callback,
134    )?
135    .report)
136}
137
138pub(super) fn load_checkpoint(
139    first: &Path,
140    options: &AcquisitionOptions,
141    write_options: &WriteOptions,
142    source_identity: [u8; 32],
143    validate: bool,
144    callback: &mut impl FnMut(AcquisitionOperationProgress) -> ControlFlow<()>,
145) -> Result<CheckpointData> {
146    crate::publication::ensure_no_publication(first)?;
147    let state = acquisition_path(first)?;
148    require_directory(&state)?;
149    require_directory(&state.join("scratch"))?;
150    if read_fixed::<32>(&state.join("config"))?
151        != fingerprint(first, options, write_options, source_identity)?
152    {
153        return Err(EwfError::Malformed(
154            "acquisition identity or options changed".into(),
155        ));
156    }
157    let count = checkpoint_count(&state)?;
158    let mut sealed = Vec::with_capacity(count);
159    let mut paths = Vec::with_capacity(count);
160    let mut previous = 0;
161    let mut stored_bytes = 0_u64;
162    let chunk_size = u64::from(options.bytes_per_sector) * u64::from(options.sectors_per_chunk);
163    for index in 1..=count {
164        let seal = Seal::read(&checkpoint_path(&state, index))?;
165        if seal.end <= previous
166            || seal.end > options.source_size
167            || (seal.end < options.source_size && !seal.end.is_multiple_of(chunk_size))
168            || (seal.end - previous).div_ceil(chunk_size) > u64::from(options.chunks_per_segment)
169        {
170            return Err(EwfError::Malformed(
171                "invalid acquisition checkpoint geometry".into(),
172            ));
173        }
174        let path = staged_path(first, &state, index)?;
175        let metadata = fs::symlink_metadata(&path)?;
176        require_file_type(&metadata)?;
177        if metadata.len() != seal.size {
178            return Err(EwfError::Malformed(
179                "sealed acquisition segment changed".into(),
180            ));
181        }
182        stored_bytes = stored_bytes
183            .checked_add(seal.size)
184            .ok_or_else(|| EwfError::Malformed("acquisition stored-size overflow".into()))?;
185        previous = seal.end;
186        paths.push(path);
187        sealed.push(seal);
188    }
189    let publishing = state.join("publishing").try_exists()?;
190    if publishing {
191        read_fixed::<0>(&state.join("publishing"))?;
192        if previous != options.source_size {
193            return Err(EwfError::Malformed(
194                "incomplete acquisition publication".into(),
195            ));
196        }
197    } else {
198        ensure_output_absent(first)?;
199    }
200    if validate {
201        let mut progress = operation_progress(
202            AcquisitionOperationPhase::ValidatingSegments,
203            stored_bytes,
204            count,
205        );
206        notify(progress, callback)?;
207        for (path, seal) in paths.iter().zip(&sealed) {
208            seal.validate_with_progress(path, &mut progress, callback)?;
209        }
210    }
211    let (image, errors) = if paths.is_empty() {
212        (None, Vec::new())
213    } else {
214        let image = Image::open_acquisition_prefix(paths, options.source_size, previous)?;
215        if image.chunk_size() != chunk_size
216            || image.info().acquisition_complete != (previous == options.source_size)
217        {
218            return Err(EwfError::Malformed(
219                "acquisition segment geometry changed".into(),
220            ));
221        }
222        let errors = normalize_errors(
223            image.acquisition_errors(),
224            previous,
225            options.bytes_per_sector,
226        )?;
227        (Some(image), errors)
228    };
229    let report = AcquisitionCheckpoint {
230        source_size: options.source_size,
231        checkpoint_bytes: previous,
232        chunk_size,
233        sealed_segments: count,
234        stored_bytes,
235        ready_to_finish: previous == options.source_size,
236        publication_started: publishing,
237        segment_hashes_validated: validate,
238        substituted_sectors: errors.iter().map(|range| range.sector_count).sum(),
239        acquisition_errors: errors,
240    };
241    Ok(CheckpointData {
242        state,
243        sealed,
244        report,
245        image,
246    })
247}
248
249pub(super) fn operation_progress(
250    phase: AcquisitionOperationPhase,
251    total: u64,
252    segments: usize,
253) -> AcquisitionOperationProgress {
254    AcquisitionOperationProgress {
255        phase,
256        bytes_processed: 0,
257        bytes_total: total,
258        segments_processed: 0,
259        segments_total: segments,
260    }
261}
262
263pub(super) fn notify(
264    progress: AcquisitionOperationProgress,
265    callback: &mut impl FnMut(AcquisitionOperationProgress) -> ControlFlow<()>,
266) -> Result<()> {
267    if callback(progress).is_break() {
268        Err(EwfError::Aborted)
269    } else {
270        Ok(())
271    }
272}