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