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