1use forensicnomicon::ntfs::{mft_flags, mft_offsets as off, SIGNATURE_BAAD, SIGNATURE_FILE};
15
16use crate::error::{NtfsError, Result};
17
18const HEADER_LEN: usize = 0x30;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct MftRecordHeader {
24 pub signature: [u8; 4],
26 pub usa_offset: u16,
28 pub usa_count: u16,
30 pub lsn: u64,
32 pub sequence_number: u16,
34 pub hard_link_count: u16,
36 pub first_attribute_offset: u16,
38 pub flags: u16,
40 pub used_size: u32,
42 pub allocated_size: u32,
44 pub base_record: u64,
46 pub next_attr_id: u16,
48 pub record_number: u32,
50}
51
52impl MftRecordHeader {
53 #[must_use]
55 pub fn is_in_use(&self) -> bool {
56 self.flags & mft_flags::IN_USE != 0
57 }
58
59 #[must_use]
61 pub fn is_directory(&self) -> bool {
62 self.flags & mft_flags::DIRECTORY != 0
63 }
64
65 #[must_use]
67 pub fn is_base_record(&self) -> bool {
68 self.base_record == 0
69 }
70
71 #[must_use]
73 pub fn is_corrupt(&self) -> bool {
74 self.signature == SIGNATURE_BAAD
75 }
76
77 pub fn parse(buf: &[u8]) -> Result<MftRecordHeader> {
87 if buf.len() < HEADER_LEN {
88 return Err(NtfsError::TooShort {
89 what: "MFT record header",
90 need: HEADER_LEN,
91 got: buf.len(),
92 });
93 }
94
95 let signature: [u8; 4] = buf[off::SIGNATURE..off::SIGNATURE + 4].try_into().unwrap();
96 if signature != SIGNATURE_FILE && signature != SIGNATURE_BAAD {
97 return Err(NtfsError::BadRecordSignature(signature));
98 }
99
100 let u16at = |o: usize| u16::from_le_bytes(buf[o..o + 2].try_into().unwrap());
101 let u32at = |o: usize| u32::from_le_bytes(buf[o..o + 4].try_into().unwrap());
102 let u64at = |o: usize| u64::from_le_bytes(buf[o..o + 8].try_into().unwrap());
103
104 Ok(MftRecordHeader {
105 signature,
106 usa_offset: u16at(off::USA_OFFSET),
107 usa_count: u16at(off::USA_COUNT),
108 lsn: u64at(off::LSN),
109 sequence_number: u16at(off::SEQUENCE_NUMBER),
110 hard_link_count: u16at(off::HARD_LINK_COUNT),
111 first_attribute_offset: u16at(off::FIRST_ATTRIBUTE),
112 flags: u16at(off::FLAGS),
113 used_size: u32at(off::USED_SIZE),
114 allocated_size: u32at(off::ALLOCATED_SIZE),
115 base_record: u64at(off::BASE_RECORD),
116 next_attr_id: u16at(off::NEXT_ATTR_ID),
117 record_number: u32at(off::RECORD_NUMBER),
118 })
119 }
120}
121
122pub fn apply_fixup(buf: &mut [u8], sector_size: usize) -> Result<()> {
133 if buf.len() < HEADER_LEN {
134 return Err(NtfsError::TooShort {
135 what: "MFT record",
136 need: HEADER_LEN,
137 got: buf.len(),
138 });
139 }
140 if sector_size < 2 {
141 return Err(NtfsError::BadUpdateSequence(
142 "sector size smaller than 2 bytes",
143 ));
144 }
145
146 let usa_offset = u16::from_le_bytes(
147 buf[off::USA_OFFSET..off::USA_OFFSET + 2]
148 .try_into()
149 .unwrap(),
150 ) as usize;
151 let usa_count =
152 u16::from_le_bytes(buf[off::USA_COUNT..off::USA_COUNT + 2].try_into().unwrap()) as usize;
153 if usa_count == 0 {
154 return Err(NtfsError::BadUpdateSequence("usa_count is zero"));
155 }
156
157 let usa_end = usa_offset
159 .checked_add(usa_count * 2)
160 .ok_or(NtfsError::BadUpdateSequence("usa offset/count overflow"))?;
161 if usa_end > buf.len() {
162 return Err(NtfsError::BadUpdateSequence("usa extends past record"));
163 }
164
165 let fixup_sectors = usa_count - 1;
166 let span = fixup_sectors
167 .checked_mul(sector_size)
168 .ok_or(NtfsError::BadUpdateSequence("sector span overflow"))?;
169 if span > buf.len() {
170 return Err(NtfsError::BadUpdateSequence(
171 "fixup sectors exceed record size",
172 ));
173 }
174
175 let usn = u16::from_le_bytes(buf[usa_offset..usa_offset + 2].try_into().unwrap());
176
177 for i in 0..fixup_sectors {
178 let tail = (i + 1) * sector_size - 2;
179 let found = u16::from_le_bytes([buf[tail], buf[tail + 1]]);
180 if found != usn {
181 return Err(NtfsError::FixupMismatch {
182 sector: i,
183 expected: usn,
184 found,
185 });
186 }
187 let original = usa_offset + 2 + i * 2;
188 buf[tail] = buf[original];
189 buf[tail + 1] = buf[original + 1];
190 }
191
192 Ok(())
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 fn make_record(size: usize, sector_size: usize, usn: u16, originals: &[u16]) -> Vec<u8> {
202 assert_eq!(size / sector_size, originals.len());
203 let mut b = vec![0u8; size];
204 b[0..4].copy_from_slice(b"FILE");
205 let usa_offset: u16 = 0x30;
206 let usa_count = (originals.len() + 1) as u16;
207 b[0x04..0x06].copy_from_slice(&usa_offset.to_le_bytes());
208 b[0x06..0x08].copy_from_slice(&usa_count.to_le_bytes());
209 let first_attr = usa_offset + usa_count * 2;
211 b[0x14..0x16].copy_from_slice(&first_attr.to_le_bytes());
212 b[0x16..0x18].copy_from_slice(&mft_flags::IN_USE.to_le_bytes());
213
214 let uo = usa_offset as usize;
215 b[uo..uo + 2].copy_from_slice(&usn.to_le_bytes());
216 for (i, orig) in originals.iter().enumerate() {
217 let p = uo + 2 + i * 2;
218 b[p..p + 2].copy_from_slice(&orig.to_le_bytes());
219 let tail = (i + 1) * sector_size - 2;
221 b[tail..tail + 2].copy_from_slice(&usn.to_le_bytes());
222 }
223 b
224 }
225
226 #[test]
229 fn parses_file_record_header() {
230 let mut b = make_record(1024, 512, 0xABCD, &[0x1111, 0x2222]);
231 b[0x08..0x10].copy_from_slice(&0x0000_0000_DEAD_BEEFu64.to_le_bytes()); b[0x10..0x12].copy_from_slice(&7u16.to_le_bytes()); b[0x12..0x14].copy_from_slice(&1u16.to_le_bytes()); b[0x18..0x1C].copy_from_slice(&0x0000_0188u32.to_le_bytes()); b[0x1C..0x20].copy_from_slice(&1024u32.to_le_bytes()); b[0x20..0x28].copy_from_slice(&0u64.to_le_bytes()); b[0x28..0x2A].copy_from_slice(&3u16.to_le_bytes()); b[0x2C..0x30].copy_from_slice(&42u32.to_le_bytes()); let h = MftRecordHeader::parse(&b).expect("valid FILE record");
242 assert_eq!(&h.signature, b"FILE");
243 assert_eq!(h.usa_offset, 0x30);
244 assert_eq!(h.usa_count, 3);
245 assert_eq!(h.lsn, 0xDEAD_BEEF);
246 assert_eq!(h.sequence_number, 7);
247 assert_eq!(h.hard_link_count, 1);
248 assert_eq!(h.first_attribute_offset, 0x30 + 3 * 2);
249 assert_eq!(h.used_size, 0x188);
250 assert_eq!(h.allocated_size, 1024);
251 assert_eq!(h.next_attr_id, 3);
252 assert_eq!(h.record_number, 42);
253 assert!(h.is_in_use());
254 assert!(!h.is_directory());
255 assert!(h.is_base_record());
256 assert!(!h.is_corrupt());
257 }
258
259 #[test]
260 fn directory_and_extension_flags_decode() {
261 let mut b = make_record(1024, 512, 1, &[0, 0]);
262 b[0x16..0x18].copy_from_slice(&(mft_flags::IN_USE | mft_flags::DIRECTORY).to_le_bytes());
263 b[0x20..0x28].copy_from_slice(&0x0001_0000_0000_0005u64.to_le_bytes()); let h = MftRecordHeader::parse(&b).unwrap();
265 assert!(h.is_in_use());
266 assert!(h.is_directory());
267 assert!(!h.is_base_record());
268 }
269
270 #[test]
271 fn baad_signature_parses_as_corrupt() {
272 let mut b = make_record(1024, 512, 1, &[0, 0]);
273 b[0..4].copy_from_slice(b"BAAD");
274 let h = MftRecordHeader::parse(&b).expect("BAAD is a valid (corrupt) record");
275 assert!(h.is_corrupt());
276 }
277
278 #[test]
279 fn rejects_unknown_signature() {
280 let mut b = make_record(1024, 512, 1, &[0, 0]);
281 b[0..4].copy_from_slice(b"XXXX");
282 assert!(matches!(
283 MftRecordHeader::parse(&b),
284 Err(NtfsError::BadRecordSignature(s)) if &s == b"XXXX"
285 ));
286 }
287
288 #[test]
289 fn header_too_short_returns_error() {
290 let b = vec![b'F', b'I', b'L', b'E', 0, 0];
291 assert!(matches!(
292 MftRecordHeader::parse(&b),
293 Err(NtfsError::TooShort { .. })
294 ));
295 }
296
297 #[test]
300 fn fixup_restores_sector_tails() {
301 let mut b = make_record(1024, 512, 0xABCD, &[0x1111, 0x2222]);
302 assert_eq!(&b[510..512], &0xABCDu16.to_le_bytes());
304 assert_eq!(&b[1022..1024], &0xABCDu16.to_le_bytes());
305
306 apply_fixup(&mut b, 512).expect("valid fixup");
307
308 assert_eq!(u16::from_le_bytes([b[510], b[511]]), 0x1111);
310 assert_eq!(u16::from_le_bytes([b[1022], b[1023]]), 0x2222);
311 }
312
313 #[test]
314 fn fixup_detects_torn_write() {
315 let mut b = make_record(1024, 512, 0xABCD, &[0x1111, 0x2222]);
316 b[1022..1024].copy_from_slice(&0xDEADu16.to_le_bytes());
318 assert!(matches!(
319 apply_fixup(&mut b, 512),
320 Err(NtfsError::FixupMismatch {
321 sector: 1,
322 expected: 0xABCD,
323 found: 0xDEAD
324 })
325 ));
326 }
327
328 #[test]
329 fn fixup_rejects_zero_usa_count() {
330 let mut b = make_record(1024, 512, 1, &[0, 0]);
331 b[0x06..0x08].copy_from_slice(&0u16.to_le_bytes()); assert!(matches!(
333 apply_fixup(&mut b, 512),
334 Err(NtfsError::BadUpdateSequence(_))
335 ));
336 }
337
338 #[test]
339 fn fixup_rejects_usa_out_of_bounds() {
340 let mut b = make_record(1024, 512, 1, &[0, 0]);
341 b[0x04..0x06].copy_from_slice(&0x0FFEu16.to_le_bytes()); b[0x06..0x08].copy_from_slice(&8u16.to_le_bytes()); assert!(matches!(
344 apply_fixup(&mut b, 512),
345 Err(NtfsError::BadUpdateSequence(_))
346 ));
347 }
348
349 #[test]
350 fn fixup_rejects_buffer_shorter_than_header() {
351 let mut b = vec![0u8; HEADER_LEN - 1];
352 assert!(matches!(
353 apply_fixup(&mut b, 512),
354 Err(NtfsError::TooShort { .. })
355 ));
356 }
357
358 #[test]
359 fn fixup_rejects_sector_size_below_two() {
360 let mut b = make_record(1024, 512, 1, &[0, 0]);
361 assert!(matches!(
362 apply_fixup(&mut b, 1),
363 Err(NtfsError::BadUpdateSequence(_))
364 ));
365 }
366
367 #[test]
368 fn fixup_rejects_sectors_exceeding_record() {
369 let mut b = make_record(1024, 512, 1, &[0, 0]);
371 b[0x06..0x08].copy_from_slice(&10u16.to_le_bytes()); assert!(matches!(
373 apply_fixup(&mut b, 512),
374 Err(NtfsError::BadUpdateSequence(detail)) if detail.contains("exceed")
375 ));
376 }
377}