ntfs_core/
attribute_list.rs1use forensicnomicon::ntfs::attribute_type_name;
8
9use crate::bytes::{le_u16, le_u32, le_u64};
10use crate::error::{NtfsError, Result};
11use crate::file_name::FileReference;
12
13const ENTRY_MIN: usize = 0x1A;
15const MAX_ENTRIES: usize = 1 << 20;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct AttributeListEntry {
21 pub type_code: u32,
23 pub start_vcn: u64,
25 pub base_reference: FileReference,
27 pub attribute_id: u16,
29 pub name: Option<String>,
31}
32
33impl AttributeListEntry {
34 #[must_use]
36 pub fn type_name(&self) -> Option<&'static str> {
37 attribute_type_name(self.type_code)
38 }
39}
40
41pub fn parse(content: &[u8]) -> Result<Vec<AttributeListEntry>> {
48 let mut entries = Vec::new();
49 let mut pos = 0;
50
51 for _ in 0..MAX_ENTRIES {
52 if pos + ENTRY_MIN > content.len() {
53 break;
54 }
55 let entry_length = le_u16(content, pos + 0x04) as usize;
56 if entry_length < ENTRY_MIN {
57 return Err(NtfsError::BadAttributeList("entry length below minimum"));
58 }
59 let entry_end = pos
60 .checked_add(entry_length)
61 .ok_or(NtfsError::BadAttributeList("entry length overflow"))?;
62 if entry_end > content.len() {
63 return Err(NtfsError::BadAttributeList("entry extends past content"));
64 }
65
66 let type_code = le_u32(content, pos);
67 let name_length = content[pos + 0x06] as usize;
68 let name_offset = content[pos + 0x07] as usize;
69 let start_vcn = le_u64(content, pos + 0x08);
70 let base_reference = FileReference::from_u64(le_u64(content, pos + 0x10));
71 let attribute_id = le_u16(content, pos + 0x18);
72
73 let name = if name_length == 0 {
74 None
75 } else {
76 let n_start = pos
77 .checked_add(name_offset)
78 .ok_or(NtfsError::BadAttributeList("name offset overflow"))?;
79 let n_end = n_start
80 .checked_add(name_length * 2)
81 .ok_or(NtfsError::BadAttributeList("name length overflow"))?;
82 if n_end > entry_end {
83 return Err(NtfsError::BadAttributeList("name extends past entry"));
84 }
85 let units: Vec<u16> = content[n_start..n_end]
86 .chunks_exact(2)
87 .map(|c| u16::from_le_bytes([c[0], c[1]]))
88 .collect();
89 Some(
90 char::decode_utf16(units)
91 .map(|r| r.unwrap_or('\u{FFFD}'))
92 .collect(),
93 )
94 };
95
96 entries.push(AttributeListEntry {
97 type_code,
98 start_vcn,
99 base_reference,
100 attribute_id,
101 name,
102 });
103 pos = entry_end;
104 }
105
106 Ok(entries)
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use forensicnomicon::ntfs::attr_types;
113
114 fn entry(type_code: u32, start_vcn: u64, base_record: u64, name: Option<&str>) -> Vec<u8> {
115 let name_u: Vec<u16> = name.map(|n| n.encode_utf16().collect()).unwrap_or_default();
116 let name_off = ENTRY_MIN;
117 let len = (name_off + name_u.len() * 2 + 7) & !7;
118 let mut e = vec![0u8; len];
119 e[0x00..0x04].copy_from_slice(&type_code.to_le_bytes());
120 e[0x04..0x06].copy_from_slice(&(len as u16).to_le_bytes());
121 e[0x06] = name_u.len() as u8;
122 e[0x07] = name_off as u8;
123 e[0x08..0x10].copy_from_slice(&start_vcn.to_le_bytes());
124 let base_ref = (1u64 << 48) | base_record;
125 e[0x10..0x18].copy_from_slice(&base_ref.to_le_bytes());
126 e[0x18..0x1A].copy_from_slice(&3u16.to_le_bytes());
127 for (i, u) in name_u.iter().enumerate() {
128 e[name_off + i * 2..name_off + i * 2 + 2].copy_from_slice(&u.to_le_bytes());
129 }
130 e
131 }
132
133 #[test]
134 fn parses_entries_pointing_to_extension_records() {
135 let content = [
136 entry(attr_types::STANDARD_INFORMATION, 0, 5, None),
137 entry(attr_types::DATA, 0, 9, None),
138 ]
139 .concat();
140 let es = parse(&content).unwrap();
141 assert_eq!(es.len(), 2);
142 assert_eq!(es[0].type_code, attr_types::STANDARD_INFORMATION);
143 assert_eq!(es[0].base_reference.record_number, 5);
144 assert_eq!(es[1].type_code, attr_types::DATA);
145 assert_eq!(es[1].base_reference.record_number, 9);
146 assert_eq!(es[1].type_name(), Some("$DATA"));
147 }
148
149 #[test]
150 fn decodes_named_entry() {
151 let content = entry(attr_types::DATA, 7, 12, Some("stream"));
152 let es = parse(&content).unwrap();
153 assert_eq!(es[0].name.as_deref(), Some("stream"));
154 assert_eq!(es[0].start_vcn, 7);
155 }
156
157 #[test]
158 fn rejects_undersized_entry() {
159 let mut content = vec![0u8; 0x20];
160 content[0x04..0x06].copy_from_slice(&4u16.to_le_bytes()); assert!(matches!(
162 parse(&content),
163 Err(NtfsError::BadAttributeList(_))
164 ));
165 }
166
167 #[test]
168 fn rejects_name_extending_past_entry() {
169 let mut content = vec![0u8; ENTRY_MIN];
171 content[0x04..0x06].copy_from_slice(&(ENTRY_MIN as u16).to_le_bytes());
172 content[0x06] = 4; content[0x07] = ENTRY_MIN as u8; assert!(matches!(
175 parse(&content),
176 Err(NtfsError::BadAttributeList(d)) if d == "name extends past entry"
177 ));
178 }
179
180 #[test]
181 fn rejects_entry_past_content() {
182 let mut content = vec![0u8; 0x20];
183 content[0x04..0x06].copy_from_slice(&0x100u16.to_le_bytes());
184 assert!(matches!(
185 parse(&content),
186 Err(NtfsError::BadAttributeList(_))
187 ));
188 }
189}