ewf-image 0.5.0

Pure-Rust library for reading and writing EWF/E01 forensic disk images.
Documentation

ewf-image

Read, verify, and write Expert Witness Format (EWF) forensic images in pure Rust.

Crates.io Documentation CI License

ewf-image project banner

ewf-image opens EnCase-style .E01, .L01, .S01, .Ex01, and .Lx01 images and exposes their decoded media as ordinary Rust readers. The library forbids unsafe code and runs without libewf or any other external tool.

  • Read physical, logical, and SMART images, including split segment sets.
  • Verify decoded media against stored and independently recorded MD5, SHA1, and SHA256 digests.
  • Browse logical file catalogs, then verify and extract individual files.
  • Write EWF1 and EWF2 images, with resumable E01 acquisition and bounded streaming writers.
  • Analyze damaged images and recover readable data with a provenance map.

The examples below target version 0.5.0. See the changelog for its changes and the migration guide for upgrades from 0.4.

This workspace also includes the experimental aff4-image crate for reading, verifying, and writing supported AFF4 containers, plus ewf-cli for EWF, AFF4, and raw images.

Installation

[dependencies]

ewf-image = "0.5"

The crate requires Rust 1.96 or later. Its runtime features are:

Feature Effect
verify Media verification, per-file verification, and integrity analysis. Enabled by default.
parallel Verification across multiple worker threads. Enables verify.
serde Serialization of reports and metadata types.

Stored-hash parsing, section integrity checks, and writer hashing remain available with default-features = false.

Read an image

Open the first segment. The remaining segments are found automatically.

use std::io::Read;

fn main() -> ewf_image::Result<()> {
    let image = ewf_image::Image::open("case.E01")?;
    let info = image.info();
    println!("{:?}, {} bytes in {} segments", info.format, info.logical_size, info.segment_count);

    // Read sequentially through a Read + Seek cursor.
    let mut first_sector = [0u8; 512];
    image.cursor().read_exact(&mut first_sector)?;

    // Or read at an absolute offset without a cursor.
    let mut buffer = [0u8; 4096];
    image.read_at(&mut buffer, 1024 * 1024)?;
    Ok(())
}

Image is a cheap, shareable handle. Clones and cursors share bounded caches, so one image can serve many readers. OpenOptions adjusts cache sizes, handle limits, and strictness. Segment files must remain unchanged while an image is open.

Verify an image

verify decodes the complete media and compares it with the digests stored in the image. Verification bypasses caches, so corrupt data cannot pass as valid.

use ewf_image::{Image, VerifyOptions};

fn main() -> ewf_image::Result<()> {
    let image = Image::open("case.E01")?;

    let result = image.verify()?;
    println!("MD5 match: {:?}", result.md5_match);
    println!("SHA256 match: {:?}", result.sha256_match);

    // Compare against a digest recorded outside the image.
    let acquisition_sha256 = [0u8; 32]; // Replace with the recorded value.
    let options = VerifyOptions::default().with_expected_sha256(acquisition_sha256);
    let report = image.verify_with_options(&options)?;
    println!("references match: {:?}", report.references_match());
    Ok(())
}

A match value of None means the image stores no digest of that type. A match shows that the decoded media equals what was hashed at acquisition. It cannot show whether unreadable source sectors were replaced with zeros at that time. See verification, analysis, and recovery.

Work with logical files

Logical images (.L01 and .Lx01) contain a catalog of files and folders.

use ewf_image::{Image, SingleFileEntryType};
use std::{fs::File, io};

fn main() -> ewf_image::Result<()> {
    let image = Image::open("files.L01")?;
    let Some(root) = image.root_file_entry() else {
        println!("not a logical image");
        return Ok(());
    };

    for entry in &root.children {
        println!("{} ({} bytes)", entry.name().unwrap_or("?"), entry.size().unwrap_or(0));
    }

    let first_file = root
        .children
        .iter()
        .find(|entry| entry.entry_type() == Some(SingleFileEntryType::File));
    if let Some(entry) = first_file {
        let check = image.verify_single_file(entry)?;
        println!("stored file hashes match: {:?}", check.references_match());

        let mut output = File::create_new("extracted.bin")?;
        io::copy(&mut image.single_file_cursor(entry), &mut output)?;
    }
    Ok(())
}

Extraction copies file content only. Timestamps and other recorded metadata remain available on each catalog entry but are not applied to extracted files. The ewf-cli extract --restore-times option applies recorded file access and modification times to a selected new output file.

Write an image

EwfWriter creates EWF1 or EWF2 images from any readable source.

use ewf_image::{EwfWriter, WriteCompression, WriteFormat, WriteOptions};
use std::{fs::File, io};

fn main() -> ewf_image::Result<()> {
    let mut options = WriteOptions {
        format: WriteFormat::Ewf2Physical,
        compression: WriteCompression::Zlib,
        ..WriteOptions::default()
    };
    options.metadata.set_header_value("case_number", "CASE-001");

    let mut writer = EwfWriter::create("case.Ex01", options)?;
    io::copy(&mut File::open("disk.raw")?, &mut writer)?;
    let result = writer.finish()?;
    println!("wrote {} segments", result.segment_paths.len());
    Ok(())
}

EwfWriter supports every output format and positioned writes, but it spools the complete source to temporary storage. Specialized writers cover large or long-running jobs:

  • AcquisitionWriter acquires physical E01 images with checkpoints and can resume after an interruption.
  • SequentialWriter streams known-length E01, Ex01, and Lx01 output and stages one segment at a time. The CLI uses it for E01 conversion.
  • LogicalWriter builds L01 and Lx01 file catalogs from files you supply.

Writers return computed MD5, SHA1, and SHA256 digests but do not reread their output. Acquisition and writing explains resume, publication, and recovery for each writer.

Supported formats

Format Read Write
EWF1 physical .E01 Raw and zlib; X-Ways Zstandard; X-Ways AES-128/AES-256 encryption Raw and zlib
EWF1 logical .L01 Media and file catalog Media and file catalog
EWF1 SMART .S01 Media Media
EWF2 physical .Ex01 Raw, zlib, BZip2, and pattern-fill Raw, zlib, BZip2, and pattern-fill
EWF2 logical .Lx01 Media and file catalog Media and file catalog

Encrypted X-Ways images open with Image::open_with_password. Encrypted EWF2 images, encrypted output, and delta (overlay) images are not supported. Compatibility describes tested producers and consumers, and limitations lists unsupported workflows.

For supported AFF4 profiles and limitations, see aff4-image. ewf-image has no AFF4 dependencies.

Command-line tool

The workspace includes ewf-cli, an unpublished command-line tool for EWF, AFF4, and raw images. Build it from this repository:

cargo build -p ewf-cli --release --locked

ewf-cli info case.E01
ewf-cli verify case.E01
ewf-cli acquire /dev/sdb case.E01
ewf-cli convert case.E01 case.aff4
ewf-cli collect evidence-folder files.Lx01
ewf-cli extract files.Lx01 2 recovered.bin

The output file extension selects the format. Commands never overwrite existing files. Acquisition requires a stable source, and directory collection does not create a filesystem snapshot. See the CLI guide for device acquisition, conversion, JSON output, and exit codes, and the EWF command guide for advanced acquisition and recovery options.

Documentation

Contributing and security

Read Contributing before opening a pull request. Report vulnerabilities privately as described in the security policy.

License

Licensed under the Apache License 2.0.