1use forensicnomicon::ntfs::filename_namespace;
11
12use crate::bytes::{arr, le_u32, le_u64};
13use crate::error::{NtfsError, Result};
14use crate::time::Filetime;
15
16const FN_MIN: usize = 0x42;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct FileReference {
23 pub record_number: u64,
25 pub sequence: u16,
27}
28
29impl FileReference {
30 #[must_use]
32 pub fn from_u64(raw: u64) -> Self {
33 FileReference {
34 record_number: raw & 0x0000_FFFF_FFFF_FFFF,
35 sequence: (raw >> 48) as u16,
36 }
37 }
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct FileName {
43 pub parent: FileReference,
45 pub created: Filetime,
47 pub modified: Filetime,
49 pub mft_modified: Filetime,
51 pub accessed: Filetime,
53 pub allocated_size: u64,
55 pub real_size: u64,
57 pub flags: u32,
59 pub namespace: u8,
61 pub name: String,
63}
64
65impl FileName {
66 #[must_use]
68 pub fn namespace_name(&self) -> Option<&'static str> {
69 filename_namespace::name(self.namespace)
70 }
71
72 #[must_use]
75 pub fn is_dos_namespace(&self) -> bool {
76 self.namespace == filename_namespace::DOS
77 }
78
79 pub fn parse(content: &[u8]) -> Result<FileName> {
86 if content.len() < FN_MIN {
87 return Err(NtfsError::TooShort {
88 what: "$FILE_NAME",
89 need: FN_MIN,
90 got: content.len(),
91 });
92 }
93
94 let parent = FileReference::from_u64(le_u64(content, 0x00));
95 let ft = |o: usize| Filetime::from_le(&arr(content, o));
96 let allocated_size = le_u64(content, 0x28);
97 let real_size = le_u64(content, 0x30);
98 let flags = le_u32(content, 0x38);
99
100 let name_length = content[0x40] as usize;
101 let namespace = content[0x41];
102 let name_bytes = name_length.checked_mul(2).ok_or(NtfsError::BadAttribute {
103 offset: 0,
104 detail: "$FILE_NAME name length overflow",
105 })?;
106 let name_end = FN_MIN
107 .checked_add(name_bytes)
108 .ok_or(NtfsError::BadAttribute {
109 offset: 0,
110 detail: "$FILE_NAME name overflow",
111 })?;
112 if name_end > content.len() {
113 return Err(NtfsError::BadAttribute {
114 offset: 0,
115 detail: "$FILE_NAME name extends past content",
116 });
117 }
118
119 let units: Vec<u16> = content[FN_MIN..name_end]
120 .chunks_exact(2)
121 .map(|c| u16::from_le_bytes([c[0], c[1]]))
122 .collect();
123 let name = char::decode_utf16(units)
124 .map(|r| r.unwrap_or('\u{FFFD}'))
125 .collect();
126
127 Ok(FileName {
128 parent,
129 created: ft(0x08),
130 modified: ft(0x10),
131 mft_modified: ft(0x18),
132 accessed: ft(0x20),
133 allocated_size,
134 real_size,
135 flags,
136 namespace,
137 name,
138 })
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[allow(clippy::too_many_arguments)]
147 fn make_fn(
148 parent: u64,
149 created: u64,
150 modified: u64,
151 mft_modified: u64,
152 accessed: u64,
153 allocated: u64,
154 real: u64,
155 flags: u32,
156 namespace: u8,
157 name: &str,
158 ) -> Vec<u8> {
159 let name_units: Vec<u16> = name.encode_utf16().collect();
160 let mut c = vec![0u8; FN_MIN + name_units.len() * 2];
161 c[0x00..0x08].copy_from_slice(&parent.to_le_bytes());
162 c[0x08..0x10].copy_from_slice(&created.to_le_bytes());
163 c[0x10..0x18].copy_from_slice(&modified.to_le_bytes());
164 c[0x18..0x20].copy_from_slice(&mft_modified.to_le_bytes());
165 c[0x20..0x28].copy_from_slice(&accessed.to_le_bytes());
166 c[0x28..0x30].copy_from_slice(&allocated.to_le_bytes());
167 c[0x30..0x38].copy_from_slice(&real.to_le_bytes());
168 c[0x38..0x3C].copy_from_slice(&flags.to_le_bytes());
169 c[0x40] = name_units.len() as u8;
170 c[0x41] = namespace;
171 for (i, u) in name_units.iter().enumerate() {
172 let p = 0x42 + i * 2;
173 c[p..p + 2].copy_from_slice(&u.to_le_bytes());
174 }
175 c
176 }
177
178 #[test]
179 fn file_reference_splits_record_and_sequence() {
180 let raw = (5u64 << 48) | 0x1234;
182 let r = FileReference::from_u64(raw);
183 assert_eq!(r.record_number, 0x1234);
184 assert_eq!(r.sequence, 5);
185 }
186
187 #[test]
188 fn parses_file_name() {
189 let c = make_fn(
190 (3u64 << 48) | 5, 0x10,
192 0x20,
193 0x30,
194 0x40,
195 0x1000,
196 0x0ABC,
197 super::super::standard_information::file_attr::ARCHIVE,
198 filename_namespace::WIN32,
199 "report.docx",
200 );
201 let f = FileName::parse(&c).unwrap();
202 assert_eq!(f.parent.record_number, 5);
203 assert_eq!(f.parent.sequence, 3);
204 assert_eq!(f.created, Filetime(0x10));
205 assert_eq!(f.accessed, Filetime(0x40));
206 assert_eq!(f.allocated_size, 0x1000);
207 assert_eq!(f.real_size, 0x0ABC);
208 assert_eq!(f.namespace, filename_namespace::WIN32);
209 assert_eq!(f.namespace_name(), Some("Win32"));
210 assert!(!f.is_dos_namespace());
211 assert_eq!(f.name, "report.docx");
212 }
213
214 #[test]
215 fn detects_dos_namespace() {
216 let c = make_fn(
217 0,
218 0,
219 0,
220 0,
221 0,
222 0,
223 0,
224 0,
225 filename_namespace::DOS,
226 "REPORT~1.DOC",
227 );
228 let f = FileName::parse(&c).unwrap();
229 assert!(f.is_dos_namespace());
230 }
231
232 #[test]
233 fn rejects_name_past_content() {
234 let mut c = make_fn(0, 0, 0, 0, 0, 0, 0, 0, filename_namespace::WIN32, "ab");
235 c[0x40] = 200; assert!(matches!(
237 FileName::parse(&c),
238 Err(NtfsError::BadAttribute { .. })
239 ));
240 }
241
242 #[test]
243 fn rejects_too_short() {
244 let c = vec![0u8; 0x20];
245 assert!(matches!(
246 FileName::parse(&c),
247 Err(NtfsError::TooShort { .. })
248 ));
249 }
250}