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