use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
use forensicnomicon::{aff4, dmg, ewf, qcow2, vhd, vhdx, vmdk};
pub trait ReadSeek: Read + Seek {}
impl<T: Read + Seek> ReadSeek for T {}
pub struct OpenedImage {
pub format: ContainerFormat,
pub size: u64,
pub reader: Box<dyn ReadSeek>,
}
impl core::fmt::Debug for OpenedImage {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("OpenedImage")
.field("format", &self.format)
.field("size", &self.size)
.finish_non_exhaustive()
}
}
#[derive(Debug, thiserror::Error)]
pub enum OpenError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("EWF decode error: {0}")]
Ewf(String),
#[error(
"ISO 9660 optical image — a filesystem, not a partitioned disk; analyse it with \
iso9660-forensic (disk4n6 filesystem dispatch is not yet wired)"
)]
Optical,
#[error("{0:?} container decoding is not yet supported — decode it to a raw image first")]
Unsupported(ContainerFormat),
}
pub fn open(path: &Path) -> Result<OpenedImage, OpenError> {
let mut file = File::open(path)?;
let format = sniff(&mut file)?;
match format {
ContainerFormat::Raw => {
let size = file.metadata()?.len();
Ok(OpenedImage {
format,
size,
reader: Box::new(file),
})
}
ContainerFormat::Ewf => {
let reader = ::ewf::EwfReader::open(path).map_err(|e| OpenError::Ewf(e.to_string()))?;
let size = reader.total_size();
Ok(OpenedImage {
format,
size,
reader: Box::new(reader),
})
}
ContainerFormat::Iso => Err(OpenError::Optical),
other => Err(OpenError::Unsupported(other)),
}
}
const HEADER_SNIFF_BYTES: usize = 34816;
const FOOTER_SNIFF_BYTES: u64 = 512;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum ContainerFormat {
Raw,
Ewf,
Vhd,
Vhdx,
Vmdk,
Qcow2,
Aff4,
Dmg,
Iso,
}
#[must_use]
pub fn detect(header: &[u8], footer: &[u8]) -> ContainerFormat {
if header.starts_with(&ewf::EVF1_SIGNATURE)
|| header.starts_with(&ewf::EVF2_SIGNATURE)
|| header.starts_with(&ewf::LEF2_SIGNATURE)
{
return ContainerFormat::Ewf;
}
if header.starts_with(vhdx::FILE_IDENTIFIER) {
return ContainerFormat::Vhdx;
}
if header.starts_with(vhd::FOOTER_COOKIE) {
return ContainerFormat::Vhd;
}
if header.starts_with(&vmdk::VMDK4_MAGIC.to_le_bytes()) {
return ContainerFormat::Vmdk;
}
if header.starts_with(&qcow2::MAGIC.to_be_bytes()) {
return ContainerFormat::Qcow2;
}
if header.starts_with(&aff4::ZIP_LOCAL_FILE_HEADER_MAGIC) {
return ContainerFormat::Aff4;
}
const ISO_PVD_OFFSET: usize = 32769;
if header.len() >= ISO_PVD_OFFSET + 5 && &header[ISO_PVD_OFFSET..ISO_PVD_OFFSET + 5] == b"CD001"
{
return ContainerFormat::Iso;
}
if footer.starts_with(vhd::FOOTER_COOKIE) {
return ContainerFormat::Vhd;
}
if footer.starts_with(&dmg::KOLY_MAGIC.to_be_bytes()) {
return ContainerFormat::Dmg;
}
ContainerFormat::Raw
}
pub fn sniff<R: Read + Seek>(reader: &mut R) -> std::io::Result<ContainerFormat> {
let len = reader.seek(SeekFrom::End(0))?;
reader.seek(SeekFrom::Start(0))?;
let header_len = (len as usize).min(HEADER_SNIFF_BYTES);
let mut header = vec![0u8; header_len];
reader.read_exact(&mut header)?;
let footer = if len >= FOOTER_SNIFF_BYTES {
reader.seek(SeekFrom::End(-(FOOTER_SNIFF_BYTES as i64)))?;
let mut f = vec![0u8; FOOTER_SNIFF_BYTES as usize];
reader.read_exact(&mut f)?;
f
} else {
Vec::new()
};
reader.seek(SeekFrom::Start(0))?;
Ok(detect(&header, &footer))
}