use std::io::{Read, Seek, SeekFrom};
pub mod normalize;
pub mod report;
pub use forensicnomicon::partition_schemes::Scheme;
const BOOT_AREA_BYTES: usize = 1024;
const APM_MAX_BYTES: usize = 1 << 20;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("unrecognised partitioning scheme (no MBR, GPT, or APM signature found)")]
UnknownScheme,
#[error("APM analysis failed: {0}")]
Apm(#[from] apm_forensic::Error),
#[error("MBR/GPT analysis failed: {0}")]
Mbr(#[from] mbr_forensic::Error),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum DiskReport {
Apm(apm_forensic::ApmAnalysis),
Mbr(Box<mbr_forensic::MbrAnalysis>),
Gpt(Box<mbr_forensic::MbrAnalysis>),
}
impl DiskReport {
#[must_use]
pub fn scheme(&self) -> Scheme {
match self {
DiskReport::Apm(_) => Scheme::Apm,
DiskReport::Mbr(_) => Scheme::Mbr,
DiskReport::Gpt(_) => Scheme::Gpt,
}
}
#[must_use]
pub fn has_anomalies(&self) -> bool {
match self {
DiskReport::Apm(a) => !a.anomalies.is_empty(),
DiskReport::Mbr(m) | DiskReport::Gpt(m) => !m.anomalies.is_empty(),
}
}
}
pub fn analyse_disk<R: Read + Seek>(
reader: &mut R,
disk_size_bytes: u64,
) -> Result<DiskReport, Error> {
let boot = read_boot_area(reader)?;
match forensicnomicon::partition_schemes::detect_scheme(&boot) {
Some(Scheme::Apm) => Ok(DiskReport::Apm(apm_forensic::analyse_reader(
reader,
APM_MAX_BYTES,
)?)),
Some(Scheme::Gpt | Scheme::Mbr) => {
let mbr = mbr_forensic::analyse(reader, disk_size_bytes)?;
if mbr.gpt.is_some() {
Ok(DiskReport::Gpt(Box::new(mbr)))
} else {
Ok(DiskReport::Mbr(Box::new(mbr)))
}
}
None => Err(Error::UnknownScheme),
}
}
fn read_boot_area<R: Read + Seek>(reader: &mut R) -> Result<Vec<u8>, std::io::Error> {
reader.seek(SeekFrom::Start(0))?;
let mut buf = vec![0u8; BOOT_AREA_BYTES];
let mut filled = 0;
while filled < BOOT_AREA_BYTES {
match reader.read(&mut buf[filled..]) {
Ok(0) => break,
Ok(n) => filled += n,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
Err(e) => return Err(e),
}
}
buf.truncate(filled);
Ok(buf)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Error as IoError, ErrorKind};
#[test]
fn error_display_covers_every_variant() {
assert!(Error::UnknownScheme.to_string().contains("unrecognised"));
let apm: Error = apm_forensic::Error::NotApm.into();
assert!(apm.to_string().contains("APM"));
let mbr: Error = mbr_forensic::Error::TooShort(1).into();
assert!(mbr.to_string().contains("MBR"));
let io: Error = IoError::other("boom").into();
assert!(io.to_string().contains("I/O"));
}
struct FlakyReader {
interrupted_once: bool,
}
impl Read for FlakyReader {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
if self.interrupted_once {
Err(IoError::other("hard read failure"))
} else {
self.interrupted_once = true;
Err(IoError::from(ErrorKind::Interrupted))
}
}
}
impl Seek for FlakyReader {
fn seek(&mut self, _: SeekFrom) -> std::io::Result<u64> {
Ok(0)
}
}
#[test]
fn read_boot_area_retries_interrupted_then_propagates_error() {
let mut r = FlakyReader {
interrupted_once: false,
};
let err = read_boot_area(&mut r).unwrap_err();
assert_eq!(err.to_string(), "hard read failure");
}
#[test]
fn read_boot_area_stops_at_eof_on_short_image() {
let mut r = std::io::Cursor::new(vec![0u8; 16]);
let boot = read_boot_area(&mut r).unwrap();
assert_eq!(boot.len(), 16);
}
}