1use forensicnomicon::ntfs::{boot_offsets as off, OEM_ID};
23
24use crate::bytes::{arr, le_u16, le_u64};
25use crate::error::{NtfsError, Result};
26
27const MIN_LEN: usize = 0x50;
29
30const MIN_RECORD_SIZE: u64 = 256;
32const MAX_RECORD_SIZE: u64 = 1 << 20; #[derive(Debug, Clone, PartialEq, Eq)]
36pub struct BootSector {
37 pub bytes_per_sector: u16,
39 pub sectors_per_cluster: u8,
41 pub total_sectors: u64,
43 pub mft_lcn: u64,
45 pub mftmirr_lcn: u64,
47 pub mft_record_size: u64,
49 pub index_record_size: u64,
51 pub volume_serial: u64,
53}
54
55impl BootSector {
56 #[must_use]
58 pub fn cluster_size(&self) -> u64 {
59 u64::from(self.bytes_per_sector) * u64::from(self.sectors_per_cluster)
60 }
61
62 #[must_use]
64 pub fn mft_byte_offset(&self) -> u64 {
65 self.mft_lcn.saturating_mul(self.cluster_size())
66 }
67
68 #[must_use]
70 pub fn mftmirr_byte_offset(&self) -> u64 {
71 self.mftmirr_lcn.saturating_mul(self.cluster_size())
72 }
73
74 pub fn parse(sector: &[u8]) -> Result<BootSector> {
82 if sector.len() < MIN_LEN {
83 return Err(NtfsError::TooShort {
84 what: "boot sector",
85 need: MIN_LEN,
86 got: sector.len(),
87 });
88 }
89
90 let oem: [u8; 8] = arr(sector, off::OEM_ID);
91 if oem != OEM_ID {
92 return Err(NtfsError::BadOemId(oem));
93 }
94
95 let bytes_per_sector = le_u16(sector, off::BYTES_PER_SECTOR);
96 if !(256..=4096).contains(&bytes_per_sector) || !bytes_per_sector.is_power_of_two() {
97 return Err(NtfsError::BadBytesPerSector(bytes_per_sector));
98 }
99
100 let sectors_per_cluster = sector[off::SECTORS_PER_CLUSTER];
101 if sectors_per_cluster == 0 || !sectors_per_cluster.is_power_of_two() {
102 return Err(NtfsError::BadSectorsPerCluster(sectors_per_cluster));
103 }
104
105 let cluster_size = u64::from(bytes_per_sector) * u64::from(sectors_per_cluster);
106 let total_sectors = le_u64(sector, off::TOTAL_SECTORS);
107 let mft_lcn = le_u64(sector, off::MFT_LCN);
108 let mftmirr_lcn = le_u64(sector, off::MFTMIRR_LCN);
109
110 let cpr = sector[off::CLUSTERS_PER_RECORD];
111 let mft_record_size =
112 record_size(cpr, cluster_size).ok_or(NtfsError::BadRecordSize(cpr))?;
113 let cpi = sector[off::CLUSTERS_PER_INDEX];
114 let index_record_size =
115 record_size(cpi, cluster_size).ok_or(NtfsError::BadIndexRecordSize(cpi))?;
116
117 let volume_serial = le_u64(sector, off::VOLUME_SERIAL);
118
119 Ok(BootSector {
120 bytes_per_sector,
121 sectors_per_cluster,
122 total_sectors,
123 mft_lcn,
124 mftmirr_lcn,
125 mft_record_size,
126 index_record_size,
127 volume_serial,
128 })
129 }
130}
131
132fn record_size(raw: u8, cluster_size: u64) -> Option<u64> {
139 let v = raw as i8;
140 let size = if v > 0 {
141 u64::from(v.unsigned_abs()).checked_mul(cluster_size)?
142 } else {
143 1u64.checked_shl(u32::from(v.unsigned_abs()))?
144 };
145 (MIN_RECORD_SIZE..=MAX_RECORD_SIZE)
146 .contains(&size)
147 .then_some(size)
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[allow(clippy::too_many_arguments)]
156 fn make_boot(
157 bytes_per_sector: u16,
158 sectors_per_cluster: u8,
159 total_sectors: u64,
160 mft_lcn: u64,
161 mftmirr_lcn: u64,
162 clusters_per_record: u8,
163 clusters_per_index: u8,
164 volume_serial: u64,
165 ) -> [u8; 512] {
166 let mut b = [0u8; 512];
167 b[0..3].copy_from_slice(&[0xEB, 0x52, 0x90]); b[3..11].copy_from_slice(b"NTFS ");
169 b[0x0B..0x0D].copy_from_slice(&bytes_per_sector.to_le_bytes());
170 b[0x0D] = sectors_per_cluster;
171 b[0x15] = 0xF8; b[0x28..0x30].copy_from_slice(&total_sectors.to_le_bytes());
173 b[0x30..0x38].copy_from_slice(&mft_lcn.to_le_bytes());
174 b[0x38..0x40].copy_from_slice(&mftmirr_lcn.to_le_bytes());
175 b[0x40] = clusters_per_record;
176 b[0x44] = clusters_per_index;
177 b[0x48..0x50].copy_from_slice(&volume_serial.to_le_bytes());
178 b[510] = 0x55;
179 b[511] = 0xAA;
180 b
181 }
182
183 #[test]
184 fn parses_standard_boot_sector() {
185 let b = make_boot(
189 512,
190 8,
191 0x0010_0000,
192 0x0004_0000,
193 0x02,
194 0xF6,
195 0x01,
196 0xDEAD_BEEF_CAFE_F00D,
197 );
198 let bs = BootSector::parse(&b).expect("valid NTFS boot sector");
199 assert_eq!(bs.bytes_per_sector, 512);
200 assert_eq!(bs.sectors_per_cluster, 8);
201 assert_eq!(bs.cluster_size(), 4096);
202 assert_eq!(bs.total_sectors, 0x0010_0000);
203 assert_eq!(bs.mft_lcn, 0x0004_0000);
204 assert_eq!(bs.mftmirr_lcn, 0x02);
205 assert_eq!(bs.mft_record_size, 1024);
206 assert_eq!(bs.index_record_size, 4096);
207 assert_eq!(bs.volume_serial, 0xDEAD_BEEF_CAFE_F00D);
208 assert_eq!(bs.mft_byte_offset(), 0x0004_0000 * 4096);
209 assert_eq!(bs.mftmirr_byte_offset(), 0x02 * 4096);
210 }
211
212 #[test]
213 fn positive_clusters_per_record_multiplies_cluster_size() {
214 let b = make_boot(512, 8, 1000, 100, 2, 0x01, 0x01, 0);
216 let bs = BootSector::parse(&b).unwrap();
217 assert_eq!(bs.mft_record_size, 4096);
218 }
219
220 #[test]
221 fn rejects_non_ntfs_oem_id() {
222 let mut b = make_boot(512, 8, 1000, 100, 2, 0xF6, 0x01, 0);
223 b[3..11].copy_from_slice(b"MSDOS5.0");
224 assert!(matches!(BootSector::parse(&b), Err(NtfsError::BadOemId(_))));
225 }
226
227 #[test]
228 fn too_short_returns_error() {
229 let short = [0u8; 16];
230 assert!(matches!(
231 BootSector::parse(&short),
232 Err(NtfsError::TooShort { .. })
233 ));
234 }
235
236 #[test]
237 fn rejects_bad_bytes_per_sector() {
238 let b = make_boot(513, 8, 1000, 100, 2, 0xF6, 0x01, 0);
240 assert!(matches!(
241 BootSector::parse(&b),
242 Err(NtfsError::BadBytesPerSector(513))
243 ));
244 }
245
246 #[test]
247 fn rejects_zero_sectors_per_cluster() {
248 let b = make_boot(512, 0, 1000, 100, 2, 0xF6, 0x01, 0);
249 assert!(matches!(
250 BootSector::parse(&b),
251 Err(NtfsError::BadSectorsPerCluster(0))
252 ));
253 }
254
255 #[test]
256 fn record_size_encoding_min_i8_does_not_panic() {
257 let b = make_boot(512, 8, 1000, 100, 2, 0x80, 0x01, 0);
260 assert!(matches!(
261 BootSector::parse(&b),
262 Err(NtfsError::BadRecordSize(0x80))
263 ));
264 }
265
266 #[test]
267 fn rejects_bad_index_record_size() {
268 let b = make_boot(512, 8, 1000, 100, 2, 0xF6, 0x80, 0);
269 assert!(matches!(
270 BootSector::parse(&b),
271 Err(NtfsError::BadIndexRecordSize(0x80))
272 ));
273 }
274}