Skip to main content

ewf_image/writer/
acquisition.rs

1//! Append-only EWF1 acquisition with immutable sealed-segment checkpoints.
2
3use std::fs::{self, File};
4use std::io::{Read, Write};
5use std::path::{Path, PathBuf};
6use std::time::{Duration, Instant};
7
8use sha2::{Digest, Sha256};
9use tempfile::NamedTempFile;
10
11use super::{
12    ChunkDescriptor, ChunkSpool, EWF1_TABLE_GROUP_MAX_ENTRIES, Ewf1SegmentSections,
13    Ewf1SegmentWriteContext, TerminalSection, WriteCompression, WriteHashState, WriteOptions,
14    WriteResult, effective_write_hashes, encode_chunk, header_payload, header2_payload,
15    publication_segment_paths, segment_path, validate_options, write_ewf1_segment,
16    writer_chunk_geometry, xheader_payload,
17};
18use crate::publication::{OutputLock, acquisition_path, sync_dir};
19use crate::{AcquisitionError, EwfError, EwfMetadata, Result};
20
21mod checkpoint;
22pub use checkpoint::{
23    AcquisitionCheckpoint, AcquisitionOperationPhase, AcquisitionOperationProgress,
24};
25use checkpoint::{load_checkpoint, operation_progress};
26use std::ops::ControlFlow;
27mod source;
28pub use source::{
29    AcquisitionOutcome, AcquisitionProgress, AcquisitionReadOptions, AcquisitionStatus,
30    UnreadableSectorPolicy,
31};
32
33#[cfg(test)]
34mod tests;
35
36/// Configuration for a new, single-destination physical E01 acquisition.
37/// The source size must be known, nonzero, and a multiple of the sector size.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct AcquisitionOptions {
40    /// Exact source size. Acquisition never pads or silently truncates input.
41    pub source_size: u64,
42    /// Bytes per sector; defaults to 512.
43    pub bytes_per_sector: u32,
44    /// Sectors per chunk; defaults to 64. Chunks may not exceed 16 MiB.
45    pub sectors_per_chunk: u32,
46    /// Maximum chunks per sealed segment, from 1 through 16,375.
47    /// Defaults to 16,375 (about 512 MiB of raw data at default geometry).
48    pub chunks_per_segment: u32,
49    /// Raw or zlib compression. `BZip2` is not supported by this EWF1 API.
50    pub compression: WriteCompression,
51    /// Acquisition metadata, fixed at creation and checked on resume.
52    pub metadata: EwfMetadata,
53}
54
55impl AcquisitionOptions {
56    /// Uses default E01 geometry and zlib compression for the given source size.
57    pub fn new(source_size: u64) -> Self {
58        Self {
59            source_size,
60            bytes_per_sector: 512,
61            sectors_per_chunk: 64,
62            chunks_per_segment: EWF1_TABLE_GROUP_MAX_ENTRIES as u32,
63            compression: WriteCompression::Zlib,
64            metadata: EwfMetadata::default(),
65        }
66    }
67
68    fn writer_options(&self) -> Result<WriteOptions> {
69        let (chunk_size, _) = writer_chunk_geometry(self.sectors_per_chunk, self.bytes_per_sector)?;
70        if self.source_size == 0
71            || !self
72                .source_size
73                .is_multiple_of(u64::from(self.bytes_per_sector))
74            || chunk_size > 16 * 1024 * 1024
75            || self.chunks_per_segment == 0
76            || self.chunks_per_segment as usize > EWF1_TABLE_GROUP_MAX_ENTRIES
77            || self.source_size.div_ceil(chunk_size) > u64::from(u32::MAX)
78        {
79            return Err(EwfError::Unsupported(
80                "unsupported acquisition geometry".into(),
81            ));
82        }
83        let options = WriteOptions {
84            sectors_per_chunk: self.sectors_per_chunk,
85            bytes_per_sector: self.bytes_per_sector,
86            compression: self.compression,
87            media_size: Some(self.source_size),
88            metadata: self.metadata.clone(),
89            ..WriteOptions::default()
90        };
91        validate_options(&options)?;
92        Ok(options)
93    }
94}
95
96/// Append-only physical E01 writer with resumable sealed segments.
97///
98/// RAM holds a fixed number of chunk buffers and at most `chunks_per_segment` descriptors. Scratch disk
99/// holds at most one encoded segment, plus its copy while sealing. Completed
100/// native segments accumulate in an adjacent `.ewf-acquisition` directory.
101/// [`Self::finish`] publishes them without copying payloads, using hard links;
102/// the destination filesystem must support hard links and file locking.
103///
104/// Drop preserves sealed checkpoints. Resume discards unsealed input, verifies
105/// every sealed segment, and rehashes its decoded media to restore digest state.
106/// Supply input again starting at [`Self::checkpoint_offset`]. No sealed segment
107/// is rewritten. The source identity is a caller-supplied assertion (for example
108/// a SHA256 of stable device identifiers); this API cannot detect source changes
109/// if the caller reuses an identity. Use a stable source snapshot when needed.
110///
111/// Flush seals complete chunks. A partial chunk remains uncheckpointed until
112/// filled or until the declared source size is reached. Any I/O failure poisons
113/// the writer: drop it and resume to discover the last successful checkpoint.
114/// Files are synchronized before checkpoint acknowledgement; directory entries
115/// are synchronized on Unix. Windows process-crash recovery is supported, but
116/// power-loss durability is not certified.
117pub struct AcquisitionWriter {
118    first: PathBuf,
119    state: PathBuf,
120    options: WriteOptions,
121    source_size: u64,
122    chunk_size: usize,
123    chunks_per_segment: usize,
124    offset: u64,
125    pending: Vec<u8>,
126    chunks: Vec<ChunkDescriptor>,
127    spool: Option<ChunkSpool>,
128    hashes: WriteHashState,
129    sealed: Vec<Seal>,
130    errors: Vec<AcquisitionError>,
131    substituted_sectors: u64,
132    processing_duration: Duration,
133    scratch_write_duration: Duration,
134    segment_seal_duration: Duration,
135    failed: bool,
136    #[cfg(test)]
137    fail_seal: bool,
138    _lock: OutputLock,
139}
140
141#[derive(Clone)]
142struct Seal {
143    end: u64,
144    size: u64,
145    digest: [u8; 32],
146}
147
148impl AcquisitionWriter {
149    /// Creates a new acquisition. Existing E01-family output is never replaced.
150    /// The identity and all options must be identical when resuming.
151    pub fn create(
152        first: impl AsRef<Path>,
153        options: &AcquisitionOptions,
154        source_identity: [u8; 32],
155    ) -> Result<Self> {
156        let write_options = options.writer_options()?;
157        let first = normalize_first(first.as_ref())?;
158        validate_segment_budget(&first, options)?;
159        let lock = OutputLock::acquire(&first)?;
160        crate::publication::ensure_no_pending(&first)?;
161        ensure_output_absent(&first)?;
162        let state = acquisition_path(&first)?;
163        let init = tempfile::Builder::new()
164            .prefix(".ewf-acquisition-init-")
165            .tempdir_in(crate::segment::segment_dir(&first))?;
166        fs::create_dir(init.path().join("scratch"))?;
167        atomic_file(
168            init.path(),
169            &init.path().join("config"),
170            &fingerprint(&first, options, &write_options, source_identity)?,
171        )?;
172        // Fail before accepting source data on filesystems that cannot perform
173        // the eventual copy-free, exclusive publication.
174        let probe = init.path().join("hard-link-probe");
175        fs::hard_link(init.path().join("config"), &probe)?;
176        fs::remove_file(probe)?;
177        sync_dir(init.path())?;
178        fs::rename(init.path(), &state)?;
179        sync_dir(crate::segment::segment_dir(&first))?;
180        Self::new(first, state, write_options, options, lock)
181    }
182
183    /// Reopens an interrupted acquisition and validates its sealed prefix.
184    /// Returns an error for a different identity/configuration, corrupt or missing
185    /// sealed data, or a live writer. Work is proportional to checkpointed bytes.
186    /// A fully acquired but unpublished set can be resumed and finished directly.
187    pub fn resume(
188        first: impl AsRef<Path>,
189        options: &AcquisitionOptions,
190        source_identity: [u8; 32],
191    ) -> Result<Self> {
192        Self::resume_with_progress(first, options, source_identity, |_| {
193            ControlFlow::Continue(())
194        })
195    }
196
197    /// Resumes with cancellable segment validation and media rehash progress.
198    /// Cancellation returns `EwfError::Aborted` and leaves checkpoint files intact.
199    pub fn resume_with_progress(
200        first: impl AsRef<Path>,
201        options: &AcquisitionOptions,
202        source_identity: [u8; 32],
203        mut on_progress: impl FnMut(AcquisitionOperationProgress) -> ControlFlow<()>,
204    ) -> Result<Self> {
205        let write_options = options.writer_options()?;
206        let first = normalize_first(first.as_ref())?;
207        validate_segment_budget(&first, options)?;
208        let lock = OutputLock::acquire(&first)?;
209        let checkpoint = load_checkpoint(
210            &first,
211            options,
212            &write_options,
213            source_identity,
214            true,
215            &mut on_progress,
216        )?;
217        let state = checkpoint.state;
218        let sealed = checkpoint.sealed;
219        let previous = checkpoint.report.checkpoint_bytes;
220        let count = sealed.len();
221        let errors = checkpoint.report.acquisition_errors;
222        let mut hashes = WriteHashState::new();
223        if let Some(image) = checkpoint.image {
224            let mut progress =
225                operation_progress(AcquisitionOperationPhase::RehashingMedia, previous, count);
226            checkpoint::notify(progress, &mut on_progress)?;
227            let mut cursor = image.cursor();
228            let mut buffer = vec![0; image.chunk_size() as usize];
229            loop {
230                let size = cursor.read(&mut buffer)?;
231                if size == 0 {
232                    break;
233                }
234                hashes.update(&buffer[..size]);
235                progress.bytes_processed += size as u64;
236                progress.segments_processed =
237                    sealed.partition_point(|seal| seal.end <= progress.bytes_processed);
238                checkpoint::notify(progress, &mut on_progress)?;
239            }
240        }
241        // Validate first; only uncommitted scratch and the possible next segment
242        // may be discarded. Sealed segments and their records stay immutable.
243        if previous < options.source_size {
244            let unsealed = staged_path(&first, &state, count + 1)?;
245            match fs::symlink_metadata(&unsealed) {
246                Ok(metadata) => {
247                    require_file_type(&metadata)?;
248                    fs::remove_file(unsealed)?;
249                }
250                Err(error) if error.kind() == std::io::ErrorKind::NotFound => (),
251                Err(error) => return Err(error.into()),
252            }
253        }
254        for entry in fs::read_dir(state.join("scratch"))? {
255            let entry = entry?;
256            require_file_type(&fs::symlink_metadata(entry.path())?)?;
257            fs::remove_file(entry.path())?;
258        }
259        let mut writer = Self::new(first, state, write_options, options, lock)?;
260        writer.offset = previous;
261        writer.hashes = hashes;
262        writer.sealed = sealed;
263        writer.substituted_sectors = errors.iter().map(|range| range.sector_count).sum();
264        writer.errors = errors;
265        Ok(writer)
266    }
267
268    fn new(
269        first: PathBuf,
270        state: PathBuf,
271        options: WriteOptions,
272        acquisition: &AcquisitionOptions,
273        lock: OutputLock,
274    ) -> Result<Self> {
275        let chunk_size =
276            (u64::from(options.bytes_per_sector) * u64::from(options.sectors_per_chunk)) as usize;
277        let spool = ChunkSpool {
278            file: NamedTempFile::new_in(state.join("scratch"))?,
279            len: 0,
280            position: 0,
281        };
282        Ok(Self {
283            first,
284            state,
285            options,
286            source_size: acquisition.source_size,
287            chunk_size,
288            chunks_per_segment: acquisition.chunks_per_segment as usize,
289            offset: 0,
290            pending: Vec::with_capacity(chunk_size),
291            chunks: Vec::with_capacity(acquisition.chunks_per_segment as usize),
292            spool: Some(spool),
293            hashes: WriteHashState::new(),
294            sealed: Vec::new(),
295            errors: Vec::new(),
296            substituted_sectors: 0,
297            processing_duration: Duration::ZERO,
298            scratch_write_duration: Duration::ZERO,
299            segment_seal_duration: Duration::ZERO,
300            failed: false,
301            #[cfg(test)]
302            fail_seal: false,
303            _lock: lock,
304        })
305    }
306
307    /// Number of bytes accepted in this acquisition, including unsealed bytes.
308    pub fn position(&self) -> u64 {
309        self.offset
310    }
311
312    /// Source offset durable at the last acknowledged sealed-segment checkpoint.
313    pub fn checkpoint_offset(&self) -> u64 {
314        self.sealed.last().map_or(0, |seal| seal.end)
315    }
316
317    /// Number of immutable native EWF segments checkpointed so far.
318    pub fn sealed_segments(&self) -> usize {
319        self.sealed.len()
320    }
321
322    /// Time spent hashing and encoding new chunks in this writer instance.
323    pub fn processing_duration(&self) -> Duration {
324        self.processing_duration
325    }
326
327    /// Time spent appending encoded chunks to scratch storage in this instance.
328    pub fn scratch_write_duration(&self) -> Duration {
329        self.scratch_write_duration
330    }
331
332    /// Time spent building, synchronizing, hashing, and checkpointing segments.
333    pub fn segment_seal_duration(&self) -> Duration {
334        self.segment_seal_duration
335    }
336
337    /// Unreadable source sectors replaced with zeroes, including unsealed input.
338    /// Checkpointed ranges are recovered from native EWF error sections on resume.
339    pub fn acquisition_errors(&self) -> &[AcquisitionError] {
340        &self.errors
341    }
342
343    /// Seals complete buffered chunks and returns the resumable source offset.
344    /// Any partial chunk remains in memory and is not included in this offset.
345    pub fn checkpoint(&mut self) -> Result<u64> {
346        self.ensure_healthy()?;
347        if !self.chunks.is_empty() {
348            let result = self.seal(false);
349            self.failed = result.is_err();
350            result?;
351        }
352        Ok(self.checkpoint_offset())
353    }
354
355    /// Completes and publishes the exact declared source, including MD5, SHA1,
356    /// and SHA256 references. Too little input is an error, never an implicit pad.
357    /// Before journal retirement, a publication error preserves checkpoints for
358    /// `resume(...).finish()`. A cleanup failure after retirement can leave an
359    /// already completed image; open the image to check that outcome.
360    pub fn finish(self) -> Result<WriteResult> {
361        self.finish_with_progress(|_| ControlFlow::Continue(()))
362    }
363
364    /// Validates and publishes with cooperative cancellation. Returning `Break`
365    /// leaves the acquisition resumable, including after some output links have
366    /// been installed. Cancellation is checked before journal retirement; cleanup
367    /// after that commit point is not cancellable.
368    pub fn finish_with_progress(
369        mut self,
370        mut on_progress: impl FnMut(AcquisitionOperationProgress) -> ControlFlow<()>,
371    ) -> Result<WriteResult> {
372        self.ensure_healthy()?;
373        if self.checkpoint_offset() != self.source_size {
374            return Err(EwfError::Malformed(
375                "acquisition source is incomplete".into(),
376            ));
377        }
378        let paths = (1..=self.sealed.len())
379            .map(|index| segment_path(&self.first, index))
380            .collect::<Result<Vec<_>>>()?;
381        let stored_bytes = self
382            .sealed
383            .iter()
384            .try_fold(0_u64, |total, seal| total.checked_add(seal.size))
385            .ok_or_else(|| EwfError::Malformed("acquisition stored-size overflow".into()))?;
386        let mut progress = operation_progress(
387            AcquisitionOperationPhase::ValidatingSegments,
388            stored_bytes,
389            self.sealed.len(),
390        );
391        checkpoint::notify(progress, &mut on_progress)?;
392        for (index, seal) in self.sealed.iter().enumerate() {
393            seal.validate_with_progress(
394                &staged_path(&self.first, &self.state, index + 1)?,
395                &mut progress,
396                &mut on_progress,
397            )?;
398        }
399        let mut progress = operation_progress(
400            AcquisitionOperationPhase::Publishing,
401            stored_bytes,
402            self.sealed.len(),
403        );
404        checkpoint::notify(progress, &mut on_progress)?;
405        let publishing = self.state.join("publishing");
406        if !publishing.try_exists()? {
407            ensure_output_absent(&self.first)?;
408            atomic_file(&self.state.join("scratch"), &publishing, &[])?;
409            sync_dir(&self.state)?;
410        }
411        #[cfg(test)]
412        tests::crash_at("publishing", 0);
413        if publication_segment_paths(&paths, false)?.len() != paths.len() {
414            return Err(EwfError::Malformed(
415                "unexpected acquisition output segments".into(),
416            ));
417        }
418        for (index, (path, seal)) in paths.iter().zip(&self.sealed).enumerate() {
419            let staged = staged_path(&self.first, &self.state, index + 1)?;
420            match fs::hard_link(staged, path) {
421                Ok(()) => (),
422                Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
423                    // Existing links can result from interrupted publication.
424                    // Revalidate their contents without counting the bytes twice.
425                    let mut validation = operation_progress(
426                        AcquisitionOperationPhase::ValidatingSegments,
427                        seal.size,
428                        1,
429                    );
430                    seal.validate_with_progress(path, &mut validation, &mut on_progress)?;
431                }
432                Err(error) => return Err(error.into()),
433            }
434            progress.bytes_processed += seal.size;
435            progress.segments_processed += 1;
436            checkpoint::notify(progress, &mut on_progress)?;
437            #[cfg(test)]
438            tests::crash_at("linked", index + 1);
439        }
440        sync_dir(crate::segment::segment_dir(&self.first))?;
441        #[cfg(test)]
442        tests::crash_at("published", 0);
443        // Retire the entire journal atomically. Interruption during deletion
444        // leaves only inert cleanup files, never a half-deleted live checkpoint.
445        drop(self.spool.take());
446        let cleanup = tempfile::Builder::new()
447            .prefix(".ewf-acquisition-cleanup-")
448            .tempdir_in(crate::segment::segment_dir(&self.first))?;
449        fs::rename(&self.state, cleanup.path().join("retired"))?;
450        sync_dir(crate::segment::segment_dir(&self.first))?;
451        #[cfg(test)]
452        tests::crash_at("retired", 0);
453        cleanup.close()?;
454        let (_, _, computed_sha256) = self.hashes.finalize();
455        Ok(WriteResult {
456            segment_paths: paths,
457            secondary_segment_paths: Vec::new(),
458            logical_size: self.source_size,
459            chunk_size: self.chunk_size as u64,
460            chunk_count: self.source_size.div_ceil(self.chunk_size as u64),
461            computed_sha256,
462        })
463    }
464
465    fn ensure_healthy(&self) -> Result<()> {
466        if self.failed {
467            return Err(EwfError::Unsupported(
468                "acquisition failed; drop and resume it".into(),
469            ));
470        }
471        Ok(())
472    }
473
474    fn append(&mut self, bytes: &[u8]) -> Result<usize> {
475        self.ensure_healthy()?;
476        if bytes.is_empty() {
477            return Ok(0);
478        }
479        if self.offset == self.source_size {
480            return Err(EwfError::Malformed(
481                "input exceeds declared acquisition size".into(),
482            ));
483        }
484        let take = bytes
485            .len()
486            .min(self.chunk_size - self.pending.len())
487            .min((self.source_size - self.offset).min(usize::MAX as u64) as usize);
488        self.pending.extend_from_slice(&bytes[..take]);
489        self.offset += take as u64;
490        if self.pending.len() == self.chunk_size || self.offset == self.source_size {
491            let processing_started = Instant::now();
492            self.hashes.update(&self.pending);
493            let data = std::mem::replace(&mut self.pending, Vec::with_capacity(self.chunk_size));
494            let encoded = encode_chunk(
495                data,
496                self.options.compression,
497                self.options.compression_values,
498                self.chunk_size as u64,
499                false,
500                false,
501            )?;
502            self.processing_duration += processing_started.elapsed();
503            let scratch_started = Instant::now();
504            self.chunks
505                .push(self.spool.as_mut().expect("active spool").append(encoded)?);
506            self.scratch_write_duration += scratch_started.elapsed();
507            if self.chunks.len() == self.chunks_per_segment || self.offset == self.source_size {
508                self.seal(self.offset == self.source_size)?;
509            }
510        }
511        Ok(take)
512    }
513
514    fn seal(&mut self, final_segment: bool) -> Result<()> {
515        let seal_started = Instant::now();
516        let index = self.sealed.len() + 1;
517        let target = staged_path(&self.first, &self.state, index)?;
518        let mut options = self.options.clone();
519        let sector_size = u64::from(options.bytes_per_sector);
520        let end_sector = (self.offset - self.pending.len() as u64) / sector_size;
521        options.acquisition_errors = self
522            .errors
523            .iter()
524            .filter_map(|range| {
525                // libewf replaces its error table when a later segment carries
526                // error2. Keep each sealed prefix independently resumable and
527                // make the last table describe the entire acquired prefix.
528                let start = range.first_sector;
529                let end = (range.first_sector + range.sector_count).min(end_sector);
530                (start < end).then(|| AcquisitionError {
531                    first_sector: start,
532                    sector_count: end - start,
533                })
534            })
535            .collect();
536        let mut sections = Ewf1SegmentSections::for_segment(index == 1, final_segment);
537        sections.bits |= Ewf1SegmentSections::ERRORS;
538        if final_segment {
539            let (md5, sha1, sha256) = self.hashes.clone().finalize();
540            options.hashes = effective_write_hashes(&options.hashes, md5, sha1, sha256)?;
541        }
542        let mut segment = NamedTempFile::new_in(self.state.join("scratch"))?;
543        let spool = self.spool.as_mut().expect("active spool");
544        write_ewf1_segment(
545            segment.as_file_mut(),
546            spool,
547            &self.chunks,
548            &options,
549            Ewf1SegmentWriteContext {
550                segment_number: u16::try_from(index)
551                    .map_err(|_| EwfError::Unsupported("too many acquisition segments".into()))?,
552                chunk_count: self.source_size.div_ceil(self.chunk_size as u64) as u32,
553                sector_count: self.source_size / u64::from(options.bytes_per_sector),
554                sections,
555                terminal_section: if final_segment {
556                    TerminalSection::Done
557                } else {
558                    TerminalSection::Next
559                },
560            },
561        )?;
562        segment.as_file().sync_all()?;
563        #[cfg(test)]
564        {
565            tests::crash_at("segment-synced", index);
566            if self.fail_seal {
567                return Err(std::io::Error::new(
568                    std::io::ErrorKind::StorageFull,
569                    "injected acquisition storage failure",
570                )
571                .into());
572            }
573        }
574        let size = segment.as_file().metadata()?.len();
575        let digest = file_digest(segment.path())?;
576        segment
577            .persist_noclobber(&target)
578            .map_err(|error| error.error)?;
579        sync_dir(&self.state)?;
580        #[cfg(test)]
581        tests::crash_at("segment-installed", index);
582        let seal = Seal {
583            end: self.offset - self.pending.len() as u64,
584            size,
585            digest,
586        };
587        atomic_file(
588            &self.state.join("scratch"),
589            &checkpoint_path(&self.state, index),
590            &seal.bytes(),
591        )?;
592        #[cfg(test)]
593        tests::crash_at("record-installed", index);
594        sync_dir(&self.state)?;
595        #[cfg(test)]
596        tests::crash_at("checkpoint-synced", index);
597        self.sealed.push(seal);
598        self.chunks.clear();
599        spool.reset()?;
600        self.segment_seal_duration += seal_started.elapsed();
601        Ok(())
602    }
603}
604
605fn normalize_errors(
606    errors: &[AcquisitionError],
607    bytes: u64,
608    sector_size: u32,
609) -> Result<Vec<AcquisitionError>> {
610    let mut errors = errors.to_vec();
611    errors.sort_unstable_by_key(|range| range.first_sector);
612    let mut normalized: Vec<AcquisitionError> = Vec::new();
613    for range in errors {
614        let end = range
615            .first_sector
616            .checked_add(range.sector_count)
617            .filter(|end| *end <= bytes / u64::from(sector_size))
618            .ok_or_else(|| {
619                EwfError::Malformed("acquisition error range exceeds checkpoint".into())
620            })?;
621        if range.sector_count == 0 {
622            return Err(EwfError::Malformed("empty acquisition error range".into()));
623        }
624        if let Some(previous) = normalized.last_mut()
625            && range.first_sector <= previous.first_sector + previous.sector_count
626        {
627            previous.sector_count =
628                end.max(previous.first_sector + previous.sector_count) - previous.first_sector;
629        } else {
630            normalized.push(range);
631        }
632    }
633    Ok(normalized)
634}
635
636impl Write for AcquisitionWriter {
637    fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
638        let result = self.append(bytes);
639        self.failed |= result.is_err();
640        result.map_err(std::io::Error::other)
641    }
642
643    fn flush(&mut self) -> std::io::Result<()> {
644        self.checkpoint().map(|_| ()).map_err(std::io::Error::other)
645    }
646}
647
648impl Seal {
649    fn bytes(&self) -> Vec<u8> {
650        let mut bytes = Vec::with_capacity(80);
651        bytes.extend_from_slice(&self.end.to_le_bytes());
652        bytes.extend_from_slice(&self.size.to_le_bytes());
653        bytes.extend_from_slice(&self.digest);
654        bytes.extend_from_slice(&Sha256::digest(&bytes));
655        bytes
656    }
657
658    fn read(path: &Path) -> Result<Self> {
659        let bytes = read_fixed::<80>(path)?;
660        if Sha256::digest(&bytes[..48])[..] != bytes[48..] {
661            return Err(EwfError::Malformed("damaged acquisition checkpoint".into()));
662        }
663        Ok(Self {
664            end: u64::from_le_bytes(bytes[..8].try_into().expect("fixed length")),
665            size: u64::from_le_bytes(bytes[8..16].try_into().expect("fixed length")),
666            digest: bytes[16..48].try_into().expect("fixed length"),
667        })
668    }
669
670    fn validate_with_progress(
671        &self,
672        path: &Path,
673        progress: &mut AcquisitionOperationProgress,
674        callback: &mut impl FnMut(AcquisitionOperationProgress) -> ControlFlow<()>,
675    ) -> Result<()> {
676        let metadata = fs::symlink_metadata(path)?;
677        require_file_type(&metadata)?;
678        if metadata.len() != self.size {
679            return Err(EwfError::Malformed(
680                "sealed acquisition segment changed".into(),
681            ));
682        }
683        let mut file = File::open(path)?;
684        let mut hash = Sha256::new();
685        let mut buffer = [0; 16 * 1024];
686        let mut remaining = self.size;
687        while remaining > 0 {
688            let take = remaining.min(buffer.len() as u64) as usize;
689            file.read_exact(&mut buffer[..take])?;
690            hash.update(&buffer[..take]);
691            remaining -= take as u64;
692            progress.bytes_processed += take as u64;
693            checkpoint::notify(*progress, callback)?;
694        }
695        if <[u8; 32]>::from(hash.finalize()) != self.digest {
696            return Err(EwfError::Malformed(
697                "sealed acquisition segment changed".into(),
698            ));
699        }
700        progress.segments_processed += 1;
701        checkpoint::notify(*progress, callback)
702    }
703}
704
705fn normalize_first(first: &Path) -> Result<PathBuf> {
706    if first.extension().and_then(|name| name.to_str()) != Some("E01") {
707        return Err(EwfError::Unsupported(
708            "acquisition requires an .E01 first segment".into(),
709        ));
710    }
711    Ok(crate::segment::segment_dir(first)
712        .canonicalize()?
713        .join(first.file_name().expect("E01 filename")))
714}
715
716fn validate_segment_budget(first: &Path, options: &AcquisitionOptions) -> Result<()> {
717    let chunk_size = u64::from(options.bytes_per_sector) * u64::from(options.sectors_per_chunk);
718    let segments = options
719        .source_size
720        .div_ceil(chunk_size)
721        .div_ceil(u64::from(options.chunks_per_segment));
722    segment_path(
723        first,
724        usize::try_from(segments)
725            .map_err(|_| EwfError::Unsupported("too many acquisition segments".into()))?,
726    )?;
727    Ok(())
728}
729
730fn fingerprint(
731    first: &Path,
732    acquisition: &AcquisitionOptions,
733    options: &WriteOptions,
734    source: [u8; 32],
735) -> Result<[u8; 32]> {
736    let mut hash = Sha256::new();
737    hash.update(b"ewf-image-acquisition-v1");
738    hash.update(source);
739    hash.update(acquisition.source_size.to_le_bytes());
740    hash.update(acquisition.bytes_per_sector.to_le_bytes());
741    hash.update(acquisition.sectors_per_chunk.to_le_bytes());
742    hash.update(acquisition.chunks_per_segment.to_le_bytes());
743    hash.update([u8::from(acquisition.compression == WriteCompression::Zlib)]);
744    let path = first.as_os_str().as_encoded_bytes();
745    hash.update((path.len() as u64).to_le_bytes());
746    hash.update(path);
747    for payload in [
748        header_payload(
749            &options.metadata,
750            options.header_codepage,
751            options.compression_values.level,
752        )?,
753        header2_payload(&options.metadata, options.compression_values.level)?,
754        xheader_payload(&options.metadata, options.compression_values.level)?,
755    ] {
756        let payload = payload.unwrap_or_default();
757        hash.update((payload.len() as u64).to_le_bytes());
758        hash.update(payload);
759    }
760    Ok(hash.finalize().into())
761}
762
763fn staged_path(first: &Path, state: &Path, index: usize) -> Result<PathBuf> {
764    Ok(state.join(
765        segment_path(first, index)?
766            .file_name()
767            .expect("segment filename"),
768    ))
769}
770
771fn checkpoint_path(state: &Path, index: usize) -> PathBuf {
772    state.join(format!("checkpoint-{index:05}"))
773}
774
775fn checkpoint_count(state: &Path) -> Result<usize> {
776    let mut indices = Vec::new();
777    for entry in fs::read_dir(state)? {
778        let entry = entry?;
779        let name = entry.file_name();
780        if let Some(index) = name
781            .to_str()
782            .and_then(|name| name.strip_prefix("checkpoint-"))
783        {
784            let index = index
785                .parse::<u16>()
786                .map_err(|_| EwfError::Malformed("invalid acquisition checkpoint name".into()))?;
787            if entry.path() != checkpoint_path(state, usize::from(index)) {
788                return Err(EwfError::Malformed(
789                    "invalid acquisition checkpoint name".into(),
790                ));
791            }
792            indices.push(index);
793        }
794    }
795    indices.sort_unstable();
796    if indices
797        .iter()
798        .enumerate()
799        .any(|(index, value)| index + 1 != usize::from(*value))
800    {
801        return Err(EwfError::Malformed("missing acquisition checkpoint".into()));
802    }
803    Ok(indices.len())
804}
805
806fn atomic_file(scratch: &Path, target: &Path, bytes: &[u8]) -> Result<()> {
807    let mut file = NamedTempFile::new_in(scratch)?;
808    file.write_all(bytes)?;
809    file.as_file().sync_all()?;
810    file.persist_noclobber(target)
811        .map_err(|error| error.error)?;
812    Ok(())
813}
814
815fn read_fixed<const N: usize>(path: &Path) -> Result<[u8; N]> {
816    let metadata = fs::symlink_metadata(path)?;
817    require_file_type(&metadata)?;
818    if metadata.len() != N as u64 {
819        return Err(EwfError::Malformed(
820            "invalid acquisition record length".into(),
821        ));
822    }
823    let mut bytes = [0; N];
824    File::open(path)?.read_exact(&mut bytes)?;
825    Ok(bytes)
826}
827
828fn require_file_type(metadata: &fs::Metadata) -> Result<()> {
829    if !metadata.file_type().is_file() {
830        return Err(EwfError::Malformed(
831            "acquisition path is not a regular file".into(),
832        ));
833    }
834    Ok(())
835}
836
837fn require_directory(path: &Path) -> Result<()> {
838    if !fs::symlink_metadata(path)?.file_type().is_dir() {
839        return Err(EwfError::Malformed(
840            "acquisition path is not a directory".into(),
841        ));
842    }
843    Ok(())
844}
845
846fn file_digest(path: &Path) -> Result<[u8; 32]> {
847    let mut file = File::open(path)?;
848    let mut hash = Sha256::new();
849    let mut buffer = [0; 16 * 1024];
850    loop {
851        let size = file.read(&mut buffer)?;
852        if size == 0 {
853            break;
854        }
855        hash.update(&buffer[..size]);
856    }
857    Ok(hash.finalize().into())
858}
859
860fn ensure_output_absent(first: &Path) -> Result<()> {
861    for path in publication_segment_paths(&[first.to_path_buf()], false)? {
862        match fs::symlink_metadata(path) {
863            Err(error) if error.kind() == std::io::ErrorKind::NotFound => (),
864            Err(error) => return Err(error.into()),
865            Ok(_) => {
866                return Err(std::io::Error::new(
867                    std::io::ErrorKind::AlreadyExists,
868                    "acquisition output already exists",
869                )
870                .into());
871            }
872        }
873    }
874    Ok(())
875}