Skip to main content

ntfs_core/
attribute_list.rs

1//! `$ATTRIBUTE_LIST` (type `0x20`) — present when a file's attributes don't fit
2//! in one MFT record. Each entry points at the extension record (a file
3//! reference) holding one of the file's attributes, with its type, starting
4//! VCN, id, and name. Following these references is how a heavily-fragmented
5//! file's attributes are gathered.
6
7use forensicnomicon::ntfs::attribute_type_name;
8
9use crate::error::{NtfsError, Result};
10use crate::file_name::FileReference;
11
12/// Fixed size of an entry header (through the attribute id; name follows).
13const ENTRY_MIN: usize = 0x1A;
14/// Loop cap on entries.
15const MAX_ENTRIES: usize = 1 << 20;
16
17/// One `$ATTRIBUTE_LIST` entry.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct AttributeListEntry {
20    /// Attribute type code.
21    pub type_code: u32,
22    /// Starting VCN of this attribute's portion.
23    pub start_vcn: u64,
24    /// Reference to the MFT record that holds the attribute.
25    pub base_reference: FileReference,
26    /// Attribute id.
27    pub attribute_id: u16,
28    /// Attribute name, or `None` if unnamed.
29    pub name: Option<String>,
30}
31
32impl AttributeListEntry {
33    /// Canonical `$NAME` of this entry's attribute type, if known.
34    #[must_use]
35    pub fn type_name(&self) -> Option<&'static str> {
36        attribute_type_name(self.type_code)
37    }
38}
39
40/// Parse an `$ATTRIBUTE_LIST` value into its entries.
41///
42/// # Errors
43///
44/// [`NtfsError::BadAttributeList`] for an undersized entry, an entry past the
45/// content, or a name out of bounds.
46pub fn parse(content: &[u8]) -> Result<Vec<AttributeListEntry>> {
47    let mut entries = Vec::new();
48    let mut pos = 0;
49
50    for _ in 0..MAX_ENTRIES {
51        if pos + ENTRY_MIN > content.len() {
52            break;
53        }
54        let entry_length =
55            u16::from_le_bytes(content[pos + 0x04..pos + 0x06].try_into().unwrap()) 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 = u32::from_le_bytes(content[pos..pos + 4].try_into().unwrap());
67        let name_length = content[pos + 0x06] as usize;
68        let name_offset = content[pos + 0x07] as usize;
69        let start_vcn = u64::from_le_bytes(content[pos + 0x08..pos + 0x10].try_into().unwrap());
70        let base_reference = FileReference::from_u64(u64::from_le_bytes(
71            content[pos + 0x10..pos + 0x18].try_into().unwrap(),
72        ));
73        let attribute_id = u16::from_le_bytes(content[pos + 0x18..pos + 0x1A].try_into().unwrap());
74
75        let name = if name_length == 0 {
76            None
77        } else {
78            let n_start = pos
79                .checked_add(name_offset)
80                .ok_or(NtfsError::BadAttributeList("name offset overflow"))?;
81            let n_end = n_start
82                .checked_add(name_length * 2)
83                .ok_or(NtfsError::BadAttributeList("name length overflow"))?;
84            if n_end > entry_end {
85                return Err(NtfsError::BadAttributeList("name extends past entry"));
86            }
87            let units: Vec<u16> = content[n_start..n_end]
88                .chunks_exact(2)
89                .map(|c| u16::from_le_bytes([c[0], c[1]]))
90                .collect();
91            Some(
92                char::decode_utf16(units)
93                    .map(|r| r.unwrap_or('\u{FFFD}'))
94                    .collect(),
95            )
96        };
97
98        entries.push(AttributeListEntry {
99            type_code,
100            start_vcn,
101            base_reference,
102            attribute_id,
103            name,
104        });
105        pos = entry_end;
106    }
107
108    Ok(entries)
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use forensicnomicon::ntfs::attr_types;
115
116    fn entry(type_code: u32, start_vcn: u64, base_record: u64, name: Option<&str>) -> Vec<u8> {
117        let name_u: Vec<u16> = name.map(|n| n.encode_utf16().collect()).unwrap_or_default();
118        let name_off = ENTRY_MIN;
119        let len = (name_off + name_u.len() * 2 + 7) & !7;
120        let mut e = vec![0u8; len];
121        e[0x00..0x04].copy_from_slice(&type_code.to_le_bytes());
122        e[0x04..0x06].copy_from_slice(&(len as u16).to_le_bytes());
123        e[0x06] = name_u.len() as u8;
124        e[0x07] = name_off as u8;
125        e[0x08..0x10].copy_from_slice(&start_vcn.to_le_bytes());
126        let base_ref = (1u64 << 48) | base_record;
127        e[0x10..0x18].copy_from_slice(&base_ref.to_le_bytes());
128        e[0x18..0x1A].copy_from_slice(&3u16.to_le_bytes());
129        for (i, u) in name_u.iter().enumerate() {
130            e[name_off + i * 2..name_off + i * 2 + 2].copy_from_slice(&u.to_le_bytes());
131        }
132        e
133    }
134
135    #[test]
136    fn parses_entries_pointing_to_extension_records() {
137        let content = [
138            entry(attr_types::STANDARD_INFORMATION, 0, 5, None),
139            entry(attr_types::DATA, 0, 9, None),
140        ]
141        .concat();
142        let es = parse(&content).unwrap();
143        assert_eq!(es.len(), 2);
144        assert_eq!(es[0].type_code, attr_types::STANDARD_INFORMATION);
145        assert_eq!(es[0].base_reference.record_number, 5);
146        assert_eq!(es[1].type_code, attr_types::DATA);
147        assert_eq!(es[1].base_reference.record_number, 9);
148        assert_eq!(es[1].type_name(), Some("$DATA"));
149    }
150
151    #[test]
152    fn decodes_named_entry() {
153        let content = entry(attr_types::DATA, 7, 12, Some("stream"));
154        let es = parse(&content).unwrap();
155        assert_eq!(es[0].name.as_deref(), Some("stream"));
156        assert_eq!(es[0].start_vcn, 7);
157    }
158
159    #[test]
160    fn rejects_undersized_entry() {
161        let mut content = vec![0u8; 0x20];
162        content[0x04..0x06].copy_from_slice(&4u16.to_le_bytes()); // < ENTRY_MIN
163        assert!(matches!(
164            parse(&content),
165            Err(NtfsError::BadAttributeList(_))
166        ));
167    }
168
169    #[test]
170    fn rejects_name_extending_past_entry() {
171        // entry_length leaves no room for the declared name.
172        let mut content = vec![0u8; ENTRY_MIN];
173        content[0x04..0x06].copy_from_slice(&(ENTRY_MIN as u16).to_le_bytes());
174        content[0x06] = 4; // name_length = 4 chars (8 bytes)
175        content[0x07] = ENTRY_MIN as u8; // name_offset at the very end
176        assert!(matches!(
177            parse(&content),
178            Err(NtfsError::BadAttributeList(d)) if d == "name extends past entry"
179        ));
180    }
181
182    #[test]
183    fn rejects_entry_past_content() {
184        let mut content = vec![0u8; 0x20];
185        content[0x04..0x06].copy_from_slice(&0x100u16.to_le_bytes());
186        assert!(matches!(
187            parse(&content),
188            Err(NtfsError::BadAttributeList(_))
189        ));
190    }
191}