use std::fs::File;
use std::path::Path;
use crate::container::{sniff, ContainerFormat};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct LogicalEntry {
pub path: String,
pub is_dir: bool,
pub size: u64,
}
#[derive(Debug, thiserror::Error)]
pub enum LogicalError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("{0:?} is not a logical file container: {1}")]
NotLogical(ContainerFormat, String),
#[error("AD1 error: {0}")]
Ad1(#[from] ad1::Ad1Error),
#[error("AFF4 error: {0}")]
Aff4(#[from] aff4::Aff4Error),
#[error("DAR error: {0}")]
Dar(#[from] dar::DarError),
#[error("no entry at index {0}")]
NoSuchEntry(usize),
#[error("entry '{0}' is a directory, not a file")]
IsDirectory(String),
}
enum Backend {
Ad1(ad1::Ad1Reader),
Aff4(aff4::LogicalContainer),
Dar(dar::DarReader<std::fs::File>),
}
pub struct LogicalImage {
format: ContainerFormat,
entries: Vec<LogicalEntry>,
backend: Backend,
}
impl core::fmt::Debug for LogicalImage {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("LogicalImage")
.field("format", &self.format)
.field("entries", &self.entries.len())
.finish()
}
}
impl LogicalImage {
#[must_use]
pub fn format(&self) -> ContainerFormat {
self.format
}
#[must_use]
pub fn entries(&self) -> &[LogicalEntry] {
&self.entries
}
pub fn read_file(&mut self, index: usize) -> Result<Vec<u8>, LogicalError> {
let meta = self
.entries
.get(index)
.ok_or(LogicalError::NoSuchEntry(index))?;
if meta.is_dir {
return Err(LogicalError::IsDirectory(meta.path.clone()));
}
match &mut self.backend {
Backend::Ad1(reader) => {
let entry = reader
.entries()
.get(index)
.ok_or(LogicalError::NoSuchEntry(index))?;
let size = entry.size;
let mut buf = vec![0u8; usize::try_from(size).unwrap_or(usize::MAX)];
let mut filled: u64 = 0;
while filled < size {
let n = reader.read_at(entry, filled, &mut buf[filled as usize..])?;
if n == 0 {
break;
}
filled += n as u64;
}
buf.truncate(filled as usize);
Ok(buf)
}
Backend::Aff4(container) => {
let entry = container
.files()
.get(index)
.ok_or(LogicalError::NoSuchEntry(index))?
.clone();
Ok(container.read_file(&entry)?)
}
Backend::Dar(reader) => {
let entry = reader
.entries()
.into_iter()
.nth(index)
.ok_or(LogicalError::NoSuchEntry(index))?;
Ok(reader.extract(&entry.path)?)
}
}
}
}
pub fn open(path: &Path) -> Result<LogicalImage, LogicalError> {
let format = {
let mut file = File::open(path)?;
sniff(&mut file)?
};
match format {
ContainerFormat::Ad1 => open_ad1(path),
ContainerFormat::Aff4 => open_aff4_logical(path),
ContainerFormat::Dar => open_dar(path),
other => Err(LogicalError::NotLogical(
other,
"not a logical file container — try disk_forensic::container::open".into(),
)),
}
}
fn open_ad1(path: &Path) -> Result<LogicalImage, LogicalError> {
let reader = ad1::Ad1Reader::open(path)?;
let entries = reader
.entries()
.iter()
.map(|e| LogicalEntry {
path: e.path.clone(),
is_dir: e.is_dir,
size: e.size,
})
.collect();
Ok(LogicalImage {
format: ContainerFormat::Ad1,
entries,
backend: Backend::Ad1(reader),
})
}
fn open_aff4_logical(path: &Path) -> Result<LogicalImage, LogicalError> {
match aff4::container_kind(path)? {
aff4::ContainerKind::Logical => {}
aff4::ContainerKind::Disk => {
return Err(LogicalError::NotLogical(
ContainerFormat::Aff4,
"this AFF4 is a physical disk image — open it with disk_forensic::container::open"
.into(),
));
}
aff4::ContainerKind::Encrypted => {
return Err(LogicalError::NotLogical(
ContainerFormat::Aff4,
"this AFF4 is encrypted (aff4:EncryptedStream) — needs a password".into(),
));
}
}
let container = aff4::LogicalContainer::open(path)?;
let entries = container
.files()
.iter()
.map(|e| LogicalEntry {
path: e.original_file_name.clone(),
is_dir: false,
size: e.size,
})
.collect();
Ok(LogicalImage {
format: ContainerFormat::Aff4,
entries,
backend: Backend::Aff4(container),
})
}
fn open_dar(path: &Path) -> Result<LogicalImage, LogicalError> {
let reader = dar::DarReader::open(File::open(path)?)?;
let entries = reader
.entries()
.iter()
.map(|e| LogicalEntry {
path: String::from_utf8_lossy(&e.path).into_owned(),
is_dir: matches!(e.kind, dar::EntryKind::Directory),
size: e.size,
})
.collect();
Ok(LogicalImage {
format: ContainerFormat::Dar,
entries,
backend: Backend::Dar(reader),
})
}