1use std::io::{Read, Seek, SeekFrom};
27
28pub mod container;
29pub mod normalize;
30pub mod report;
31
32pub use forensicnomicon::partition_schemes::Scheme;
33
34const BOOT_AREA_BYTES: usize = 1024;
36const APM_MAX_BYTES: usize = 1 << 20;
38
39#[derive(Debug, thiserror::Error)]
41pub enum Error {
42 #[error("unrecognised partitioning scheme (no MBR, GPT, or APM signature found)")]
45 UnknownScheme,
46 #[error("APM analysis failed: {0}")]
48 Apm(#[from] apm_forensic::Error),
49 #[error("MBR/GPT analysis failed: {0}")]
51 Mbr(#[from] mbr_forensic::Error),
52 #[error("I/O error: {0}")]
54 Io(#[from] std::io::Error),
55}
56
57#[derive(Debug)]
62#[cfg_attr(feature = "serde", derive(serde::Serialize))]
63pub enum DiskReport {
64 Apm(apm_forensic::ApmAnalysis),
66 Mbr(Box<mbr_forensic::MbrAnalysis>),
68 Gpt(Box<mbr_forensic::MbrAnalysis>),
70}
71
72impl DiskReport {
73 #[must_use]
75 pub fn scheme(&self) -> Scheme {
76 match self {
77 DiskReport::Apm(_) => Scheme::Apm,
78 DiskReport::Mbr(_) => Scheme::Mbr,
79 DiskReport::Gpt(_) => Scheme::Gpt,
80 }
81 }
82
83 #[must_use]
86 pub fn has_anomalies(&self) -> bool {
87 match self {
88 DiskReport::Apm(a) => !a.anomalies.is_empty(),
89 DiskReport::Mbr(m) | DiskReport::Gpt(m) => !m.anomalies.is_empty(),
90 }
91 }
92}
93
94pub fn analyse_disk<R: Read + Seek>(
104 reader: &mut R,
105 disk_size_bytes: u64,
106) -> Result<DiskReport, Error> {
107 let boot = read_boot_area(reader)?;
108 match forensicnomicon::partition_schemes::detect_scheme(&boot) {
109 Some(Scheme::Apm) => Ok(DiskReport::Apm(apm_forensic::analyse_reader(
110 reader,
111 APM_MAX_BYTES,
112 )?)),
113 Some(Scheme::Gpt | Scheme::Mbr) => {
114 let mbr = mbr_forensic::analyse(reader, disk_size_bytes)?;
115 if mbr.gpt.is_some() {
118 Ok(DiskReport::Gpt(Box::new(mbr)))
119 } else {
120 Ok(DiskReport::Mbr(Box::new(mbr)))
121 }
122 }
123 None => Err(Error::UnknownScheme),
124 }
125}
126
127fn read_boot_area<R: Read + Seek>(reader: &mut R) -> Result<Vec<u8>, std::io::Error> {
129 reader.seek(SeekFrom::Start(0))?;
130 let mut buf = vec![0u8; BOOT_AREA_BYTES];
131 let mut filled = 0;
132 while filled < BOOT_AREA_BYTES {
133 match reader.read(&mut buf[filled..]) {
134 Ok(0) => break,
135 Ok(n) => filled += n,
136 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
137 Err(e) => return Err(e),
138 }
139 }
140 buf.truncate(filled);
141 Ok(buf)
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use std::io::{Error as IoError, ErrorKind};
148
149 #[test]
150 fn error_display_covers_every_variant() {
151 assert!(Error::UnknownScheme.to_string().contains("unrecognised"));
152 let apm: Error = apm_forensic::Error::NotApm.into();
153 assert!(apm.to_string().contains("APM"));
154 let mbr: Error = mbr_forensic::Error::TooShort(1).into();
155 assert!(mbr.to_string().contains("MBR"));
156 let io: Error = IoError::other("boom").into();
157 assert!(io.to_string().contains("I/O"));
158 }
159
160 struct FlakyReader {
162 interrupted_once: bool,
163 }
164 impl Read for FlakyReader {
165 fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
166 if self.interrupted_once {
167 Err(IoError::other("hard read failure"))
168 } else {
169 self.interrupted_once = true;
170 Err(IoError::from(ErrorKind::Interrupted))
171 }
172 }
173 }
174 impl Seek for FlakyReader {
175 fn seek(&mut self, _: SeekFrom) -> std::io::Result<u64> {
176 Ok(0)
177 }
178 }
179
180 #[test]
181 fn read_boot_area_retries_interrupted_then_propagates_error() {
182 let mut r = FlakyReader {
183 interrupted_once: false,
184 };
185 let err = read_boot_area(&mut r).unwrap_err();
186 assert_eq!(err.to_string(), "hard read failure");
187 }
188
189 #[test]
190 fn read_boot_area_stops_at_eof_on_short_image() {
191 let mut r = std::io::Cursor::new(vec![0u8; 16]);
193 let boot = read_boot_area(&mut r).unwrap();
194 assert_eq!(boot.len(), 16);
195 }
196}