libmft2bodyfile/intern/
complete_mft_entry.rs

1use crate::intern::PreprocessedMft;
2use crate::{FilenameInfo, TimestampTuple};
3use anyhow::Result;
4use bodyfile::Bodyfile3Line;
5use likely_stable::unlikely;
6use mft::attribute::{MftAttributeContent, MftAttributeType};
7use mft::MftEntry;
8use num::ToPrimitive;
9use std::cell::RefCell;
10use std::cmp;
11use usnjrnl::{CommonUsnRecord, UsnRecordData};
12use winstructs::ntfs::mft_reference::MftReference;
13
14///
15/// Represents the set of all $MFT entries that make up a files metadata.
16/// The idea is to store only the minimum required data to generate
17/// a bodyfile line, which would be
18///
19///  - the base reference (needed to print the `inode` number)
20///  - the `$FILE_NAME` attribute. One file can have more than one `$FILE_NAME`
21///    attribbutes, but we store only one of them. We choose the right attribute
22///    using the following priority:
23///    
24///    1. `Win32AndDos`
25///    2. `Win32`
26///    3. `POSIX`
27///    4. ´DOS`
28///    If a file doesn't have a `$FILE_NAME` attribute, which may happen with already deleted files,
29///    then a filename is being generated, but *not* stored in `file_name_attribute`
30///
31///    This attribute is required to display the filename, but also contains four timestamps,
32///    which are being displayed as well.
33///
34///  - the `$STANDARD_INFORMATION` attribute. This attribute contains four timestamps.
35pub struct CompleteMftEntry {
36    base_entry: MftReference,
37    file_name_attribute: Option<FilenameInfo>,
38    standard_info_timestamps: Option<TimestampTuple>,
39    full_path: RefCell<String>,
40    is_allocated: bool,
41    deletion_status: RefCell<&'static str>,
42    usnjrnl_records: Vec<CommonUsnRecord>,
43    streams: Vec<StreamAttribute>,
44    is_directory: bool,
45}
46
47pub struct StreamAttribute {
48    attribute_type: MftAttributeType,
49    name: Option<String>,
50    instance: u16,
51}
52
53impl CompleteMftEntry {
54    pub fn from_base_entry(entry_reference: MftReference, entry: MftEntry) -> Self {
55        let mut c = Self {
56            base_entry: entry_reference,
57            file_name_attribute: None,
58            standard_info_timestamps: None,
59            full_path: RefCell::new(String::new()),
60            is_allocated: entry.is_allocated(),
61            usnjrnl_records: Vec::new(),
62            deletion_status: RefCell::new(if entry.is_allocated() {
63                ""
64            } else {
65                " (deleted)"
66            }),
67            streams: Vec::new(),
68            is_directory: entry.is_dir(),
69        };
70        c.update_attributes(&entry);
71        c
72    }
73
74    pub fn from_nonbase_entry(_entry_ref: MftReference, entry: MftEntry) -> Self {
75        let mut c = Self {
76            base_entry: entry.header.base_reference,
77            file_name_attribute: None,
78            standard_info_timestamps: None,
79            full_path: RefCell::new(String::new()),
80            is_allocated: false,
81            usnjrnl_records: Vec::new(),
82            deletion_status: RefCell::new(" (deleted)"),
83            streams: Vec::new(),
84            is_directory: false,
85        };
86        c.add_nonbase_entry(entry);
87        c
88    }
89
90    pub fn from_usnjrnl_records(_entry_ref: MftReference, records: Vec<CommonUsnRecord>) -> Self {
91        let mut records = records;
92        records.sort_by(|a, b| a.data.timestamp().partial_cmp(b.data.timestamp()).unwrap());
93
94        Self {
95            base_entry: _entry_ref,
96            file_name_attribute: None,
97            standard_info_timestamps: None,
98            full_path: RefCell::new(String::new()),
99            is_allocated: false,
100            usnjrnl_records: records,
101            deletion_status: RefCell::new(" (deleted)"),
102            streams: Vec::new(),
103            is_directory: false,
104        }
105    }
106
107    pub fn base_entry(&self) -> &MftReference {
108        &self.base_entry
109    }
110
111    pub fn is_allocated(&self) -> bool {
112        self.is_allocated
113    }
114
115    pub fn set_base_entry(&mut self, entry_ref: MftReference, entry: MftEntry) {
116        assert_eq!(self.base_entry, entry_ref);
117
118        self.update_attributes(&entry);
119        self.is_allocated = entry.is_allocated();
120        self.is_directory = entry.is_dir();
121    }
122
123    pub fn add_nonbase_entry(&mut self, e: MftEntry) {
124        self.update_attributes(&e);
125    }
126
127    pub fn add_usnjrnl_records(&mut self, records: Vec<CommonUsnRecord>) {
128        let mut records = records;
129        records.sort_by(|a, b| a.data.timestamp().partial_cmp(b.data.timestamp()).unwrap());
130
131        if self.usnjrnl_records.is_empty() {
132            self.usnjrnl_records = records;
133        } else {
134            self.usnjrnl_records.extend(records);
135        }
136    }
137
138    fn update_attributes(&mut self, entry: &MftEntry) {
139        for attr_result in entry
140            .iter_attributes_matching(Some(vec![
141                MftAttributeType::StandardInformation,
142                MftAttributeType::FileName,
143                MftAttributeType::DATA,
144                MftAttributeType::IndexRoot,
145            ]))
146            .filter_map(Result::ok)
147        {
148            //eprintln!("TYPE_CODE: {}", attr_result.header.type_code.to_u32().unwrap());
149
150            if attr_result.header.type_code == MftAttributeType::IndexRoot
151                || attr_result.header.type_code == MftAttributeType::DATA
152            {
153                self.streams.push(StreamAttribute {
154                    attribute_type: attr_result.header.type_code,
155                    name: attr_result
156                        .header
157                        .name_offset
158                        .and(Some(attr_result.header.name)),
159                    instance: attr_result.header.instance,
160                });
161                continue;
162            }
163
164            match attr_result.data {
165                MftAttributeContent::AttrX10(standard_info_attribute) => {
166                    if self.standard_info_timestamps.is_none() {
167                        self.standard_info_timestamps =
168                            Some(TimestampTuple::from(&standard_info_attribute));
169                    } else {
170                        panic!("multiple standard information attributes found")
171                    }
172                }
173
174                MftAttributeContent::AttrX30(file_name_attribute) => {
175                    match self.file_name_attribute {
176                        None => {
177                            self.file_name_attribute = Some(FilenameInfo::from(
178                                &file_name_attribute,
179                                &attr_result.header,
180                            ))
181                        }
182                        Some(ref mut name_attr) => {
183                            name_attr.update(&file_name_attribute, &attr_result.header)
184                        }
185                    }
186                }
187                _ => panic!("filter for iter_attributes_matching() isn't working"),
188            }
189        }
190    }
191
192    pub fn parent(&self) -> Option<&MftReference> {
193        match self.file_name_attribute {
194            None => None,
195            Some(ref fn_attr) => Some(fn_attr.parent()),
196        }
197    }
198
199    fn set_folder_name(&self, mft: &PreprocessedMft, parent: &MftReference, my_name: &str) {
200        assert_ne!(parent, &self.base_entry);
201        let mut fp = self.full_path.borrow_mut();
202
203        let parent_info = mft.get_full_path(parent);
204
205        *fp = parent_info.full_path;
206        if !&fp.ends_with('/') {
207            fp.push('/');
208        }
209        fp.push_str(my_name);
210    }
211
212    pub fn get_full_path(&self, mft: &PreprocessedMft) -> String {
213        if unlikely(self.full_path.borrow().is_empty()) {
214            if self.base_entry.entry == 5
215            /* matchs the root entry */
216            {
217                *self.full_path.borrow_mut() = String::from("/");
218                return self.full_path.borrow().clone();
219            }
220
221            match self.filename_info() {
222                Some(name) => match self.parent() {
223                    None => *self.full_path.borrow_mut() = name.filename().clone(),
224                    Some(p) => self.set_folder_name(mft, p, name.filename()),
225                },
226                None => {
227                    let my_name = match self.filename_from_usnjrnl() {
228                        Some(name) => name.to_owned(),
229                        None => format!(
230                            "unnamed_{}_{}",
231                            self.base_entry.entry, self.base_entry.sequence
232                        ),
233                    };
234
235                    match self.parent_from_usnjrnl() {
236                        None => *self.full_path.borrow_mut() = my_name,
237                        Some(p) => self.set_folder_name(mft, &p, &my_name),
238                    };
239                }
240            }
241        }
242        self.full_path.borrow().to_string()
243    }
244
245    fn filename_from_usnjrnl(&self) -> Option<&str> {
246        self.usnjrnl_records.last().map(|r| r.data.filename())
247    }
248
249    fn parent_from_usnjrnl(&self) -> Option<MftReference> {
250        self.usnjrnl_records.last().and_then(|r| match &r.data {
251            UsnRecordData::V2(data) => Some(data.ParentFileReferenceNumber),
252            #[allow(unreachable_patterns)]
253            _ => None,
254        })
255    }
256
257    pub fn filesize(&self) -> u64 {
258        match self.file_name_attribute {
259            Some(ref fn_attr) => fn_attr.logical_size(),
260            None => 0,
261        }
262    }
263
264    fn format(
265        &self,
266        display_name: String,
267        timestamps: &TimestampTuple,
268        attribute_id: u32,
269        instance_id: u16,
270    ) -> String {
271        Bodyfile3Line::new()
272            .with_owned_name(format!("{}{}", display_name, self.deletion_status.borrow()))
273            .with_owned_inode(format!(
274                "{}-{}-{}",
275                self.base_entry().entry,
276                attribute_id,
277                instance_id
278            ))
279            .with_size(self.filesize())
280            .with_atime(timestamps.accessed())
281            .with_mtime(timestamps.mft_modified())
282            .with_ctime(timestamps.modified())
283            .with_crtime(timestamps.created())
284            .to_string()
285    }
286
287    fn format_fn(&self, mft: &PreprocessedMft) -> Option<String> {
288        self.file_name_attribute.as_ref().map(|fn_attr| {
289            self.format(
290                format!("{} ($FILE_NAME)", self.get_full_path(mft)),
291                fn_attr.timestamps(),
292                MftAttributeType::FileName.to_u32().unwrap(),
293                fn_attr.instance_id(),
294            )
295        })
296    }
297
298    fn format_si(
299        &self,
300        mft: &PreprocessedMft,
301        stream_name: Option<&String>,
302        attribute_id: u32,
303        instance_id: u16,
304    ) -> Option<String> {
305        self.standard_info_timestamps.as_ref().map(|si| {
306            let name = match stream_name {
307                None => self.get_full_path(mft),
308                Some(n) => format!("{}:{}", self.get_full_path(mft), n),
309            };
310            self.format(name, si, attribute_id, instance_id)
311        })
312    }
313
314    /// returns the filename stored in the `$MFT`, if any, or None
315    fn mft_filename(&self) -> Option<&String> {
316        match &self.file_name_attribute {
317            Some(fni) => Some(fni.filename()),
318            None => None,
319        }
320    }
321
322    fn format_usnjrnl(
323        &self,
324        mft: &PreprocessedMft,
325        record: &CommonUsnRecord,
326        usnjrnl_longflags: bool,
327    ) -> String {
328        match &record.data {
329            UsnRecordData::V2(data) => {
330                let filename_info = match self.mft_filename() {
331                    None => format!(" filename={}", data.FileName),
332                    Some(f) => {
333                        if f != &data.FileName {
334                            format!(" filename={}", data.FileName)
335                        } else {
336                            "".to_owned()
337                        }
338                    }
339                };
340
341                let reason_info = if usnjrnl_longflags {
342                    format!(" reason={:+}", data.Reason)
343                } else {
344                    format!(" reason={}", data.Reason)
345                };
346
347                let parent_info = mft.get_full_path(&data.ParentFileReferenceNumber);
348                let parent_info = match &parent_info.reference {
349                    Some(parent_ref) => {
350                        if parent_ref == &data.ParentFileReferenceNumber
351                            || !parent_info.is_allocated
352                                && parent_ref
353                                    == &MftReference::new(
354                                        data.ParentFileReferenceNumber.entry,
355                                        data.ParentFileReferenceNumber.sequence + 1,
356                                    )
357                        {
358                            "".to_owned()
359                        } else {
360                            format!(
361                                " parent={}-{}/{}-{}/'{}'",
362                                parent_ref.entry,
363                                parent_ref.sequence,
364                                data.ParentFileReferenceNumber.entry,
365                                data.ParentFileReferenceNumber.sequence,
366                                parent_info.full_path
367                            )
368                        }
369                    }
370                    None => format!(" parent='{}'", parent_info.full_path),
371                };
372
373                let display_name = format!(
374                    "{} ($UsnJrnl{}{}{})",
375                    self.get_full_path(mft),
376                    filename_info,
377                    parent_info,
378                    reason_info
379                );
380                let timestamp = data.TimeStamp.timestamp();
381                Bodyfile3Line::new()
382                    .with_owned_name(display_name)
383                    .with_atime(timestamp)
384                    .with_owned_inode(format!(
385                        "{mft_entry}-{attr_type}-{usn_number}",
386                        mft_entry = data.FileReferenceNumber.entry,
387                        attr_type = "???",
388                        usn_number = data.FileReferenceNumber.sequence
389                    ))
390                    .to_string()
391            }
392        }
393    }
394 
395    pub fn filename_info(&self) -> &Option<FilenameInfo> {
396        if self.file_name_attribute.is_none() && self.is_allocated {
397            #[cfg(debug_assertions)]
398            panic!(
399                "no $FILE_NAME attribute found for $MFT entry {}-{}",
400                self.base_entry().entry,
401                self.base_entry().sequence
402            );
403
404            #[cfg(not(debug_assertions))]
405            log::error!(
406            "no $FILE_NAME attribute found for $MFT entry {}-{}. This is fatal because this is not a deleted file",
407            self.base_entry().entry,
408            self.base_entry().sequence
409            );
410        } /*else {
411              log::warn!(
412              "no $FILE_NAME attribute found for $MFT entry {}-{}, but this is a deleted file",
413              self.base_entry().entry,
414              self.base_entry().sequence
415          );
416          }*/
417        &self.file_name_attribute
418    }
419
420    pub fn bodyfile_lines(&self, mft: &PreprocessedMft, usnjrnl_longflags: bool) -> BodyfileLines {
421        let mut lines: Vec<String> = Vec::new();
422        for d in self.streams.iter() {
423            // hide default directory index name
424            let name = if d.attribute_type == MftAttributeType::IndexRoot
425                && d.name == Some("$I30".to_owned())
426            {
427                None
428            } else {
429                d.name.as_ref()
430            };
431
432            if let Some(line) =
433                self.format_si(mft, name, d.attribute_type.to_u32().unwrap(), d.instance)
434            {
435                lines.push(line);
436            }
437        }
438
439        if lines.is_empty() {
440            if let Some(line) = self.format_si(mft, None, 0, 0) {
441                lines.push(line);
442            }
443        }
444
445        BodyfileLines {
446            standard_info: lines,
447            filename_info: self.format_fn(mft),
448            usnjrnl_records: self
449                .usnjrnl_records
450                .iter()
451                .map(|r| self.format_usnjrnl(mft, r, usnjrnl_longflags))
452                .collect(),
453        }
454    }
455 
456    pub fn bodyfile_lines_count(&self) -> usize {
457        (match &self.standard_info_timestamps {
458            Some(_) => cmp::min(self.streams.len(), 1),
459            None => 0,
460        } + match &self.file_name_attribute {
461            Some(_) => 1,
462            None => 0,
463        } + self.usnjrnl_records.len())
464    }
465}
466
467pub struct BodyfileLines {
468    standard_info: Vec<String>,
469    filename_info: Option<String>,
470    usnjrnl_records: Vec<String>,
471}
472
473impl Iterator for BodyfileLines {
474    type Item = String;
475    fn next(&mut self) -> Option<Self::Item> {
476        if !self.standard_info.is_empty() {
477            return self.standard_info.pop();
478        }
479        if self.filename_info.is_some() {
480            return self.filename_info.take();
481        }
482        self.usnjrnl_records.pop()
483    }
484}