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