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