ewf-image 0.6.0

Pure-Rust library for reading and writing EWF/E01 forensic disk images.
Documentation
use super::*;
use crate::Image;

const ID: [u8; 32] = [0x21; 32];
const SIZE: usize = 9 * 1024 + 512;

fn options() -> AcquisitionOptions {
    AcquisitionOptions {
        sectors_per_chunk: 2,
        chunks_per_segment: 3,
        ..AcquisitionOptions::new(SIZE as u64)
    }
}

fn input() -> Vec<u8> {
    (0..SIZE)
        .map(|index| (index * 71 + index / 11) as u8)
        .collect()
}

pub(super) fn crash_at(point: &str, index: usize) {
    if std::env::var("EWF_ACQUISITION_CRASH_POINT")
        .is_ok_and(|value| value == format!("{point}:{index}"))
    {
        std::process::exit(77);
    }
}

#[test]
fn acquisition_crash_worker() {
    let Some(path) = std::env::var_os("EWF_ACQUISITION_CRASH_OUTPUT") else {
        return;
    };
    let mut writer = AcquisitionWriter::create(Path::new(&path), &options(), ID).unwrap();
    writer.write_all(&input()).unwrap();
    writer.finish().unwrap();
    panic!("crash point was not reached");
}

#[test]
fn process_exit_at_each_checkpoint_and_publication_boundary() {
    let points = [
        ("segment-synced:1", 0),
        ("segment-installed:1", 0),
        ("record-installed:1", 3072),
        ("checkpoint-synced:1", 3072),
        ("segment-synced:2", 3072),
        ("segment-installed:2", 3072),
        ("record-installed:2", 6144),
        ("checkpoint-synced:2", 6144),
        ("publishing:0", SIZE as u64),
        ("linked:1", SIZE as u64),
        ("linked:2", SIZE as u64),
        ("linked:3", SIZE as u64),
        ("linked:4", SIZE as u64),
        ("published:0", SIZE as u64),
        ("retired:0", SIZE as u64),
    ];
    for (point, offset) in points {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("case.E01");
        let output = std::process::Command::new(std::env::current_exe().unwrap())
            .args([
                "--exact",
                "writer::acquisition::tests::acquisition_crash_worker",
                "--nocapture",
            ])
            .env("EWF_ACQUISITION_CRASH_OUTPUT", &path)
            .env("EWF_ACQUISITION_CRASH_POINT", point)
            .output()
            .unwrap();
        assert_eq!(
            output.status.code(),
            Some(77),
            "{point}: {}",
            String::from_utf8_lossy(&output.stderr)
        );
        if point != "retired:0" {
            assert!(
                Image::open(&path).is_err(),
                "pending output opened at {point}"
            );
            let mut resumed = AcquisitionWriter::resume(&path, &options(), ID)
                .unwrap_or_else(|error| panic!("{point}: {error}"));
            assert_eq!(resumed.position(), offset, "{point}");
            resumed.write_all(&input()[offset as usize..]).unwrap();
            resumed.finish().unwrap();
        }
        let image = Image::open(&path).unwrap();
        let mut bytes = Vec::new();
        image.cursor().read_to_end(&mut bytes).unwrap();
        assert_eq!(bytes, input(), "{point}");
        #[cfg(feature = "verify")]
        {
            let result = image.verify().unwrap();
            assert_eq!(result.md5_match, Some(true), "{point}");
            assert_eq!(result.sha1_match, Some(true), "{point}");
            assert_eq!(result.sha256_match, Some(true), "{point}");
        }
    }
}

#[test]
fn failed_seal_poisoning_and_resume_keep_prior_checkpoint() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("case.E01");
    let mut writer = AcquisitionWriter::create(&path, &options(), ID).unwrap();
    let bytes = input();
    writer.write_all(&bytes[..3072]).unwrap();
    writer.fail_seal = true;
    assert!(writer.write_all(&bytes[3072..6144]).is_err());
    assert_eq!(writer.checkpoint_offset(), 3072);
    assert!(writer.write_all(&[0]).is_err());
    assert!(writer.checkpoint().is_err());
    assert!(writer.finish().is_err());
    let mut writer = AcquisitionWriter::resume(&path, &options(), ID).unwrap();
    assert_eq!(writer.position(), 3072);
    writer.write_all(&bytes[3072..]).unwrap();
    writer.finish().unwrap();
    let mut actual = Vec::new();
    Image::open(&path)
        .unwrap()
        .cursor()
        .read_to_end(&mut actual)
        .unwrap();
    assert_eq!(actual, bytes);
}

#[test]
fn chunk_buffers_and_scratch_stay_within_one_segment() {
    for compression in [WriteCompression::None, WriteCompression::Zlib] {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("case.E01");
        let mut options = options();
        options.source_size = 256 * 1024;
        options.compression = compression;
        let mut writer = AcquisitionWriter::create(&path, &options, ID).unwrap();
        let bytes = [0x42; 512];
        for _ in 0..512 {
            writer.write_all(&bytes).unwrap();
            assert!(writer.pending.capacity() <= 1024);
            assert!(writer.pending.len() < 1024);
            assert_eq!(writer.chunks.capacity(), 3);
            assert!(writer.chunks.len() < 3);
            let spool = writer.spool.as_ref().unwrap();
            assert!(spool.len < 3 * (1024 + 128));
            assert_eq!(spool.file.as_file().metadata().unwrap().len(), spool.len);
        }
        assert_eq!(writer.sealed_segments(), 86);
        writer.finish().unwrap();
    }
}

#[test]
fn interrupted_publication_preserves_conflicting_output() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("case.E01");
    let mut writer = AcquisitionWriter::create(&path, &options(), ID).unwrap();
    writer.write_all(&input()).unwrap();
    let state = writer.state.clone();
    // Simulate interruption after the publication decision but before all links.
    atomic_file(&state.join("scratch"), &state.join("publishing"), &[]).unwrap();
    fs::hard_link(state.join("case.E01"), &path).unwrap();
    let collision = path.with_extension("E02");
    fs::write(&collision, b"unrelated output").unwrap();
    assert!(writer.finish().is_err());
    assert_eq!(fs::read(&collision).unwrap(), b"unrelated output");
    assert!(Image::open(&path).is_err());
    fs::remove_file(collision).unwrap();
    AcquisitionWriter::resume(&path, &options(), ID)
        .unwrap()
        .finish()
        .unwrap();
    let mut actual = Vec::new();
    Image::open(&path)
        .unwrap()
        .cursor()
        .read_to_end(&mut actual)
        .unwrap();
    assert_eq!(actual, input());
}