1use forensicnomicon::ntfs::SIGNATURE_INDX as INDX_SIGNATURE;
14
15use crate::error::{NtfsError, Result};
16use crate::file_name::{FileName, FileReference};
17use crate::record::apply_fixup;
18
19mod ih {
21 pub const FIRST_ENTRY: usize = 0x00;
22 pub const TOTAL_SIZE: usize = 0x04;
23 pub const FLAGS: usize = 0x0C;
24}
25const IH_FLAG_LARGE: u32 = 0x01;
27const INDEX_HEADER_LEN: usize = 0x10;
29
30mod ie {
32 pub const FILE_REFERENCE: usize = 0x00;
33 pub const ENTRY_LENGTH: usize = 0x08;
34 pub const STREAM_LENGTH: usize = 0x0A;
35 pub const FLAGS: usize = 0x0C;
36 pub const STREAM: usize = 0x10;
37}
38const IE_FLAG_SUBNODE: u8 = 0x01;
40const IE_FLAG_LAST: u8 = 0x02;
42const ENTRY_MIN: usize = 0x10;
44const ROOT_HEADER_LEN: usize = 0x10;
46const INDX_HEADER_LEN: usize = 0x18;
48const MAX_ENTRIES: usize = 1 << 20;
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct IndexEntry {
54 pub file_reference: FileReference,
56 pub file_name: Option<FileName>,
58 pub child_vcn: Option<u64>,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct IndexRoot {
65 pub indexed_type: u32,
67 pub is_large: bool,
69 pub entries: Vec<IndexEntry>,
71}
72
73impl IndexRoot {
74 pub fn parse(content: &[u8]) -> Result<IndexRoot> {
80 if content.len() < ROOT_HEADER_LEN + INDEX_HEADER_LEN {
81 return Err(NtfsError::TooShort {
82 what: "$INDEX_ROOT",
83 need: ROOT_HEADER_LEN + INDEX_HEADER_LEN,
84 got: content.len(),
85 });
86 }
87 let indexed_type = u32::from_le_bytes(content[0x00..0x04].try_into().unwrap());
88 let (entries, is_large) = parse_index_header(content, ROOT_HEADER_LEN)?;
89 Ok(IndexRoot {
90 indexed_type,
91 is_large,
92 entries,
93 })
94 }
95}
96
97fn parse_index_header(node: &[u8], base: usize) -> Result<(Vec<IndexEntry>, bool)> {
100 let header_end = base
101 .checked_add(INDEX_HEADER_LEN)
102 .ok_or(NtfsError::BadIndex("index header overflow"))?;
103 if header_end > node.len() {
104 return Err(NtfsError::BadIndex("index header past buffer"));
105 }
106 let u32at = |o: usize| u32::from_le_bytes(node[base + o..base + o + 4].try_into().unwrap());
107 let first_entry = u32at(ih::FIRST_ENTRY) as usize;
108 let total_size = u32at(ih::TOTAL_SIZE) as usize;
109 let is_large = u32at(ih::FLAGS) & IH_FLAG_LARGE != 0;
110
111 let start = base
112 .checked_add(first_entry)
113 .ok_or(NtfsError::BadIndex("first-entry offset overflow"))?;
114 let end = base
115 .checked_add(total_size)
116 .ok_or(NtfsError::BadIndex("total-size overflow"))?;
117 if start < header_end || end > node.len() || start > end {
118 return Err(NtfsError::BadIndex("index entry region out of bounds"));
119 }
120 let entries = parse_entries(node, start, end)?;
121 Ok((entries, is_large))
122}
123
124pub fn parse_entries(node: &[u8], start: usize, end: usize) -> Result<Vec<IndexEntry>> {
131 if end > node.len() || start > end {
132 return Err(NtfsError::BadIndex("entry region out of bounds"));
133 }
134 let mut entries = Vec::new();
135 let mut pos = start;
136
137 for _ in 0..MAX_ENTRIES {
138 if pos + ENTRY_MIN > end {
139 break; }
141 let entry_length = u16::from_le_bytes(
142 node[pos + ie::ENTRY_LENGTH..pos + ie::ENTRY_LENGTH + 2]
143 .try_into()
144 .unwrap(),
145 ) as usize;
146 if entry_length < ENTRY_MIN {
147 return Err(NtfsError::BadIndex("entry length below minimum"));
148 }
149 let entry_end = pos
150 .checked_add(entry_length)
151 .ok_or(NtfsError::BadIndex("entry length overflow"))?;
152 if entry_end > end {
153 return Err(NtfsError::BadIndex("entry extends past node"));
154 }
155
156 let flags = node[pos + ie::FLAGS];
157 let is_last = flags & IE_FLAG_LAST != 0;
158 let file_reference = FileReference::from_u64(u64::from_le_bytes(
159 node[pos + ie::FILE_REFERENCE..pos + ie::FILE_REFERENCE + 8]
160 .try_into()
161 .unwrap(),
162 ));
163
164 let child_vcn = if flags & IE_FLAG_SUBNODE != 0 {
165 if entry_end < pos + ENTRY_MIN + 8 {
166 return Err(NtfsError::BadIndex("sub-node VCN does not fit in entry"));
167 }
168 let vcn_pos = entry_end - 8;
169 Some(u64::from_le_bytes(
170 node[vcn_pos..vcn_pos + 8].try_into().unwrap(),
171 ))
172 } else {
173 None
174 };
175
176 let file_name = if is_last {
177 None
178 } else {
179 let stream_length = u16::from_le_bytes(
180 node[pos + ie::STREAM_LENGTH..pos + ie::STREAM_LENGTH + 2]
181 .try_into()
182 .unwrap(),
183 ) as usize;
184 let s_start = pos + ie::STREAM;
185 let s_end = s_start
186 .checked_add(stream_length)
187 .ok_or(NtfsError::BadIndex("stream length overflow"))?;
188 if s_end > entry_end {
189 return Err(NtfsError::BadIndex("stream extends past entry"));
190 }
191 Some(FileName::parse(&node[s_start..s_end])?)
192 };
193
194 entries.push(IndexEntry {
195 file_reference,
196 file_name,
197 child_vcn,
198 });
199
200 if is_last {
201 break;
202 }
203 pos = entry_end;
204 }
205
206 Ok(entries)
207}
208
209pub fn parse_index_buffer(
217 buffer: &mut [u8],
218 index_record_size: usize,
219 sector_size: usize,
220) -> Result<Vec<IndexEntry>> {
221 if buffer.len() < index_record_size || index_record_size < INDX_HEADER_LEN + INDEX_HEADER_LEN {
222 return Err(NtfsError::TooShort {
223 what: "INDX buffer",
224 need: index_record_size.max(INDX_HEADER_LEN + INDEX_HEADER_LEN),
225 got: buffer.len(),
226 });
227 }
228 let buf = &mut buffer[..index_record_size];
229 if buf[0..4] != INDX_SIGNATURE {
230 return Err(NtfsError::BadIndex("INDX signature missing"));
231 }
232 apply_fixup(buf, sector_size)?;
233 let (entries, _is_large) = parse_index_header(buf, INDX_HEADER_LEN)?;
234 Ok(entries)
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use forensicnomicon::ntfs::filename_namespace;
241
242 fn fname(parent: u64, name: &str) -> Vec<u8> {
243 let units: Vec<u16> = name.encode_utf16().collect();
244 let mut c = vec![0u8; 0x42 + units.len() * 2];
245 c[0..8].copy_from_slice(&parent.to_le_bytes());
246 c[0x40] = units.len() as u8;
247 c[0x41] = filename_namespace::WIN32;
248 for (i, u) in units.iter().enumerate() {
249 c[0x42 + i * 2..0x42 + i * 2 + 2].copy_from_slice(&u.to_le_bytes());
250 }
251 c
252 }
253
254 fn entry(file_ref: u64, name: &str) -> Vec<u8> {
255 let fnc = fname(5, name);
256 let len = (ie::STREAM + fnc.len() + 7) & !7;
257 let mut e = vec![0u8; len];
258 e[ie::FILE_REFERENCE..ie::FILE_REFERENCE + 8].copy_from_slice(&file_ref.to_le_bytes());
259 e[ie::ENTRY_LENGTH..ie::ENTRY_LENGTH + 2].copy_from_slice(&(len as u16).to_le_bytes());
260 e[ie::STREAM_LENGTH..ie::STREAM_LENGTH + 2]
261 .copy_from_slice(&(fnc.len() as u16).to_le_bytes());
262 e[ie::FLAGS] = 0;
263 e[ie::STREAM..ie::STREAM + fnc.len()].copy_from_slice(&fnc);
264 e
265 }
266
267 fn end_entry() -> Vec<u8> {
268 let mut e = vec![0u8; ENTRY_MIN];
269 e[ie::ENTRY_LENGTH..ie::ENTRY_LENGTH + 2]
270 .copy_from_slice(&(ENTRY_MIN as u16).to_le_bytes());
271 e[ie::FLAGS] = IE_FLAG_LAST;
272 e
273 }
274
275 fn make_root(is_large: bool, entries: &[Vec<u8>]) -> Vec<u8> {
276 let blob: Vec<u8> = entries.concat();
277 let total = (INDEX_HEADER_LEN + blob.len()) as u32;
278 let mut c = vec![0u8; ROOT_HEADER_LEN + INDEX_HEADER_LEN + blob.len()];
279 c[0x00..0x04].copy_from_slice(&0x30u32.to_le_bytes()); let base = ROOT_HEADER_LEN;
281 c[base + ih::FIRST_ENTRY..base + ih::FIRST_ENTRY + 4]
282 .copy_from_slice(&(INDEX_HEADER_LEN as u32).to_le_bytes());
283 c[base + ih::TOTAL_SIZE..base + ih::TOTAL_SIZE + 4].copy_from_slice(&total.to_le_bytes());
284 c[base + ih::FLAGS..base + ih::FLAGS + 4]
285 .copy_from_slice(&(if is_large { IH_FLAG_LARGE } else { 0 }).to_le_bytes());
286 c[base + INDEX_HEADER_LEN..].copy_from_slice(&blob);
287 c
288 }
289
290 #[test]
291 fn parses_entries_until_last() {
292 let node = [entry(11, "alpha.txt"), entry(12, "beta.txt"), end_entry()].concat();
293 let es = parse_entries(&node, 0, node.len()).unwrap();
294 assert_eq!(es.len(), 3);
295 assert_eq!(es[0].file_reference.record_number, 11);
296 assert_eq!(es[0].file_name.as_ref().unwrap().name, "alpha.txt");
297 assert_eq!(es[1].file_name.as_ref().unwrap().name, "beta.txt");
298 assert!(es[2].file_name.is_none()); }
300
301 #[test]
302 fn parses_small_index_root() {
303 let root = make_root(false, &[entry(20, "report.docx"), end_entry()]);
304 let ir = IndexRoot::parse(&root).unwrap();
305 assert_eq!(ir.indexed_type, 0x30);
306 assert!(!ir.is_large);
307 assert_eq!(ir.entries.len(), 2);
308 assert_eq!(
309 ir.entries[0].file_name.as_ref().unwrap().name,
310 "report.docx"
311 );
312 }
313
314 #[test]
315 fn large_index_root_flag_detected() {
316 let root = make_root(true, &[end_entry()]);
317 assert!(IndexRoot::parse(&root).unwrap().is_large);
318 }
319
320 #[test]
321 fn subnode_vcn_is_read() {
322 let mut e = entry(30, "dir");
325 let new_len = e.len() + 8;
326 e.resize(new_len, 0);
327 e[ie::ENTRY_LENGTH..ie::ENTRY_LENGTH + 2].copy_from_slice(&(new_len as u16).to_le_bytes());
328 e[ie::FLAGS] = IE_FLAG_SUBNODE;
329 e[new_len - 8..new_len].copy_from_slice(&7u64.to_le_bytes());
330 let node = [e, end_entry()].concat();
331 let es = parse_entries(&node, 0, node.len()).unwrap();
332 assert_eq!(es[0].child_vcn, Some(7));
333 assert_eq!(es[0].file_name.as_ref().unwrap().name, "dir");
334 }
335
336 #[test]
337 fn parses_indx_buffer_with_fixup() {
338 let record_size = 512usize;
340 let mut b = vec![0u8; record_size];
341 b[0..4].copy_from_slice(b"INDX");
342 let usa_offset = 0x28u16;
343 let usa_count = 2u16; b[0x04..0x06].copy_from_slice(&usa_offset.to_le_bytes());
345 b[0x06..0x08].copy_from_slice(&usa_count.to_le_bytes());
346 let base = INDX_HEADER_LEN;
348 let first_entry = 0x40 - base; let blob = [entry(40, "child.bin"), end_entry()].concat();
350 let total = (first_entry + blob.len()) as u32;
351 b[base + ih::FIRST_ENTRY..base + ih::FIRST_ENTRY + 4]
352 .copy_from_slice(&(first_entry as u32).to_le_bytes());
353 b[base + ih::TOTAL_SIZE..base + ih::TOTAL_SIZE + 4].copy_from_slice(&total.to_le_bytes());
354 b[0x40..0x40 + blob.len()].copy_from_slice(&blob);
355 let usn = 0x0001u16;
357 b[usa_offset as usize..usa_offset as usize + 2].copy_from_slice(&usn.to_le_bytes());
358 b[510..512].copy_from_slice(&usn.to_le_bytes());
359
360 let es = parse_index_buffer(&mut b, record_size, 512).unwrap();
361 assert_eq!(es[0].file_name.as_ref().unwrap().name, "child.bin");
362 }
363
364 #[test]
367 fn rejects_undersized_entry() {
368 let mut node = vec![0u8; 0x20];
369 node[ie::ENTRY_LENGTH..ie::ENTRY_LENGTH + 2].copy_from_slice(&4u16.to_le_bytes()); assert!(matches!(
371 parse_entries(&node, 0, node.len()),
372 Err(NtfsError::BadIndex(_))
373 ));
374 }
375
376 #[test]
377 fn rejects_entry_past_node_end() {
378 let mut node = vec![0u8; 0x20];
379 node[ie::ENTRY_LENGTH..ie::ENTRY_LENGTH + 2].copy_from_slice(&0x100u16.to_le_bytes());
380 assert!(matches!(
381 parse_entries(&node, 0, node.len()),
382 Err(NtfsError::BadIndex(_))
383 ));
384 }
385
386 #[test]
387 fn rejects_indx_bad_signature() {
388 let mut b = vec![0u8; 512];
389 b[0..4].copy_from_slice(b"BADX");
390 assert!(matches!(
391 parse_index_buffer(&mut b, 512, 512),
392 Err(NtfsError::BadIndex(_))
393 ));
394 }
395
396 #[test]
397 fn index_root_rejects_too_short() {
398 assert!(matches!(
399 IndexRoot::parse(&[0u8; 4]),
400 Err(NtfsError::TooShort { .. })
401 ));
402 }
403
404 #[test]
405 fn index_header_rejects_past_buffer() {
406 assert!(matches!(
408 parse_index_header(&[0u8; 4], 0),
409 Err(NtfsError::BadIndex(_))
410 ));
411 }
412
413 #[test]
414 fn index_header_rejects_entry_region_out_of_bounds() {
415 let mut node = vec![0u8; INDEX_HEADER_LEN];
416 node[ih::TOTAL_SIZE..ih::TOTAL_SIZE + 4].copy_from_slice(&0xFFFFu32.to_le_bytes());
417 assert!(matches!(
418 parse_index_header(&node, 0),
419 Err(NtfsError::BadIndex(_))
420 ));
421 }
422
423 #[test]
424 fn parse_entries_rejects_region_out_of_bounds() {
425 let node = vec![0u8; 0x20];
426 assert!(matches!(
427 parse_entries(&node, 0, node.len() + 1),
428 Err(NtfsError::BadIndex(_))
429 ));
430 }
431
432 #[test]
433 fn parse_entries_stops_when_no_header_room() {
434 assert!(parse_entries(&[], 0, 0).unwrap().is_empty());
436 }
437
438 #[test]
439 fn rejects_subnode_vcn_not_fitting() {
440 let mut e = end_entry();
442 e[ie::FLAGS] = IE_FLAG_SUBNODE;
443 assert!(matches!(
444 parse_entries(&e, 0, e.len()),
445 Err(NtfsError::BadIndex(_))
446 ));
447 }
448
449 #[test]
450 fn rejects_stream_extending_past_entry() {
451 let mut e = entry(11, "x.txt");
452 let big = e.len() as u16 + 0x100;
453 e[ie::STREAM_LENGTH..ie::STREAM_LENGTH + 2].copy_from_slice(&big.to_le_bytes());
454 assert!(matches!(
455 parse_entries(&e, 0, e.len()),
456 Err(NtfsError::BadIndex(_))
457 ));
458 }
459
460 #[test]
461 fn index_buffer_rejects_too_short() {
462 let mut b = vec![0u8; 16];
463 assert!(matches!(
464 parse_index_buffer(&mut b, 512, 512),
465 Err(NtfsError::TooShort { .. })
466 ));
467 }
468}