use crate::intern::PreprocessedMft;
use crate::{FilenameInfo, TimestampTuple};
use anyhow::Result;
use bodyfile::Bodyfile3Line;
use likely_stable::unlikely;
use mft::attribute::{MftAttributeContent, MftAttributeType};
use mft::MftEntry;
use num::ToPrimitive;
use std::cell::RefCell;
use std::cmp;
use usnjrnl::{CommonUsnRecord, UsnRecordData};
use winstructs::ntfs::mft_reference::MftReference;
pub struct CompleteMftEntry {
base_entry: MftReference,
file_name_attribute: Option<FilenameInfo>,
standard_info_timestamps: Option<TimestampTuple>,
full_path: RefCell<String>,
is_allocated: bool,
deletion_status: RefCell<&'static str>,
usnjrnl_records: Vec<CommonUsnRecord>,
streams: Vec<StreamAttribute>,
is_directory: bool,
}
pub struct StreamAttribute {
attribute_type: MftAttributeType,
name: Option<String>,
instance: u16,
}
impl CompleteMftEntry {
pub fn from_base_entry(entry_reference: MftReference, entry: MftEntry) -> Self {
let mut c = Self {
base_entry: entry_reference,
file_name_attribute: None,
standard_info_timestamps: None,
full_path: RefCell::new(String::new()),
is_allocated: entry.is_allocated(),
usnjrnl_records: Vec::new(),
deletion_status: RefCell::new(if entry.is_allocated() {
""
} else {
" (deleted)"
}),
streams: Vec::new(),
is_directory: entry.is_dir(),
};
c.update_attributes(&entry);
c
}
pub fn from_nonbase_entry(_entry_ref: MftReference, entry: MftEntry) -> Self {
let mut c = Self {
base_entry: entry.header.base_reference,
file_name_attribute: None,
standard_info_timestamps: None,
full_path: RefCell::new(String::new()),
is_allocated: false,
usnjrnl_records: Vec::new(),
deletion_status: RefCell::new(" (deleted)"),
streams: Vec::new(),
is_directory: false,
};
c.add_nonbase_entry(entry);
c
}
pub fn from_usnjrnl_records(_entry_ref: MftReference, records: Vec<CommonUsnRecord>) -> Self {
let mut records = records;
records.sort_by(|a, b| a.data.timestamp().partial_cmp(b.data.timestamp()).unwrap());
Self {
base_entry: _entry_ref,
file_name_attribute: None,
standard_info_timestamps: None,
full_path: RefCell::new(String::new()),
is_allocated: false,
usnjrnl_records: records,
deletion_status: RefCell::new(" (deleted)"),
streams: Vec::new(),
is_directory: false,
}
}
pub fn base_entry(&self) -> &MftReference {
&self.base_entry
}
pub fn is_allocated(&self) -> bool {
self.is_allocated
}
pub fn set_base_entry(&mut self, entry_ref: MftReference, entry: MftEntry) {
assert_eq!(self.base_entry, entry_ref);
self.update_attributes(&entry);
self.is_allocated = entry.is_allocated();
self.is_directory = entry.is_dir();
}
pub fn add_nonbase_entry(&mut self, e: MftEntry) {
self.update_attributes(&e);
}
pub fn add_usnjrnl_records(&mut self, records: Vec<CommonUsnRecord>) {
let mut records = records;
records.sort_by(|a, b| a.data.timestamp().partial_cmp(b.data.timestamp()).unwrap());
if self.usnjrnl_records.is_empty() {
self.usnjrnl_records = records;
} else {
self.usnjrnl_records.extend(records);
}
}
fn update_attributes(&mut self, entry: &MftEntry) {
for attr_result in entry
.iter_attributes_matching(Some(vec![
MftAttributeType::StandardInformation,
MftAttributeType::FileName,
MftAttributeType::DATA,
MftAttributeType::IndexRoot,
]))
.filter_map(Result::ok)
{
if attr_result.header.type_code == MftAttributeType::IndexRoot
|| attr_result.header.type_code == MftAttributeType::DATA
{
self.streams.push(StreamAttribute {
attribute_type: attr_result.header.type_code,
name: attr_result
.header
.name_offset
.and(Some(attr_result.header.name)),
instance: attr_result.header.instance,
});
continue;
}
match attr_result.data {
MftAttributeContent::AttrX10(standard_info_attribute) => {
if self.standard_info_timestamps.is_none() {
self.standard_info_timestamps =
Some(TimestampTuple::from(&standard_info_attribute));
} else {
panic!("multiple standard information attributes found")
}
}
MftAttributeContent::AttrX30(file_name_attribute) => {
match self.file_name_attribute {
None => {
self.file_name_attribute = Some(FilenameInfo::from(
&file_name_attribute,
&attr_result.header,
))
}
Some(ref mut name_attr) => {
name_attr.update(&file_name_attribute, &attr_result.header)
}
}
}
_ => panic!("filter for iter_attributes_matching() isn't working"),
}
}
}
pub fn parent(&self) -> Option<&MftReference> {
match self.file_name_attribute {
None => None,
Some(ref fn_attr) => Some(fn_attr.parent()),
}
}
fn set_folder_name(&self, mft: &PreprocessedMft, parent: &MftReference, my_name: &str) {
assert_ne!(parent, &self.base_entry);
let mut fp = self.full_path.borrow_mut();
let parent_info = mft.get_full_path(parent);
*fp = parent_info.full_path;
if !&fp.ends_with('/') {
fp.push('/');
}
fp.push_str(my_name);
}
pub fn get_full_path(&self, mft: &PreprocessedMft) -> String {
if unlikely(self.full_path.borrow().is_empty()) {
if self.base_entry.entry == 5
{
*self.full_path.borrow_mut() = String::from("/");
return self.full_path.borrow().clone();
}
match self.filename_info() {
Some(name) => match self.parent() {
None => *self.full_path.borrow_mut() = name.filename().clone(),
Some(p) => self.set_folder_name(mft, p, name.filename()),
},
None => {
let my_name = match self.filename_from_usnjrnl() {
Some(name) => name.to_owned(),
None => format!(
"unnamed_{}_{}",
self.base_entry.entry, self.base_entry.sequence
),
};
match self.parent_from_usnjrnl() {
None => *self.full_path.borrow_mut() = my_name,
Some(p) => self.set_folder_name(mft, &p, &my_name),
};
}
}
}
self.full_path.borrow().to_string()
}
fn filename_from_usnjrnl(&self) -> Option<&str> {
self.usnjrnl_records.last().map(|r| r.data.filename())
}
fn parent_from_usnjrnl(&self) -> Option<MftReference> {
self.usnjrnl_records.last().and_then(|r| match &r.data {
UsnRecordData::V2(data) => Some(data.ParentFileReferenceNumber),
#[allow(unreachable_patterns)]
_ => None,
})
}
pub fn filesize(&self) -> u64 {
match self.file_name_attribute {
Some(ref fn_attr) => fn_attr.logical_size(),
None => 0,
}
}
fn format(
&self,
display_name: String,
timestamps: &TimestampTuple,
attribute_id: u32,
instance_id: u16,
) -> String {
Bodyfile3Line::new()
.with_owned_name(format!("{}{}", display_name, self.deletion_status.borrow()))
.with_owned_inode(format!(
"{}-{}-{}",
self.base_entry().entry,
attribute_id,
instance_id
))
.with_size(self.filesize())
.with_atime(timestamps.accessed())
.with_mtime(timestamps.mft_modified())
.with_ctime(timestamps.modified())
.with_crtime(timestamps.created())
.to_string()
}
fn format_fn(&self, mft: &PreprocessedMft) -> Option<String> {
self.file_name_attribute.as_ref().map(|fn_attr| {
self.format(
format!("{} ($FILE_NAME)", self.get_full_path(mft)),
fn_attr.timestamps(),
MftAttributeType::FileName.to_u32().unwrap(),
fn_attr.instance_id(),
)
})
}
fn format_si(
&self,
mft: &PreprocessedMft,
stream_name: Option<&String>,
attribute_id: u32,
instance_id: u16,
) -> Option<String> {
self.standard_info_timestamps.as_ref().map(|si| {
let name = match stream_name {
None => self.get_full_path(mft),
Some(n) => format!("{}:{}", self.get_full_path(mft), n),
};
self.format(name, si, attribute_id, instance_id)
})
}
fn mft_filename(&self) -> Option<&String> {
match &self.file_name_attribute {
Some(fni) => Some(fni.filename()),
None => None,
}
}
fn format_usnjrnl(
&self,
mft: &PreprocessedMft,
record: &CommonUsnRecord,
usnjrnl_longflags: bool,
) -> String {
match &record.data {
UsnRecordData::V2(data) => {
let filename_info = match self.mft_filename() {
None => format!(" filename={}", data.FileName),
Some(f) => {
if f != &data.FileName {
format!(" filename={}", data.FileName)
} else {
"".to_owned()
}
}
};
let reason_info = if usnjrnl_longflags {
format!(" reason={:+}", data.Reason)
} else {
format!(" reason={}", data.Reason)
};
let parent_info = mft.get_full_path(&data.ParentFileReferenceNumber);
let parent_info = match &parent_info.reference {
Some(parent_ref) => {
if parent_ref == &data.ParentFileReferenceNumber
|| !parent_info.is_allocated
&& parent_ref
== &MftReference::new(
data.ParentFileReferenceNumber.entry,
data.ParentFileReferenceNumber.sequence + 1,
)
{
"".to_owned()
} else {
format!(
" parent={}-{}/{}-{}/'{}'",
parent_ref.entry,
parent_ref.sequence,
data.ParentFileReferenceNumber.entry,
data.ParentFileReferenceNumber.sequence,
parent_info.full_path
)
}
}
None => format!(" parent='{}'", parent_info.full_path),
};
let display_name = format!(
"{} ($UsnJrnl{}{}{})",
self.get_full_path(mft),
filename_info,
parent_info,
reason_info
);
let timestamp = data.TimeStamp.timestamp();
Bodyfile3Line::new()
.with_owned_name(display_name)
.with_atime(timestamp)
.with_owned_inode(format!(
"{mft_entry}-{attr_type}-{usn_number}",
mft_entry = data.FileReferenceNumber.entry,
attr_type = "???",
usn_number = data.FileReferenceNumber.sequence
))
.to_string()
}
}
}
pub fn filename_info(&self) -> &Option<FilenameInfo> {
if self.file_name_attribute.is_none() && self.is_allocated {
#[cfg(debug_assertions)]
panic!(
"no $FILE_NAME attribute found for $MFT entry {}-{}",
self.base_entry().entry,
self.base_entry().sequence
);
#[cfg(not(debug_assertions))]
log::error!(
"no $FILE_NAME attribute found for $MFT entry {}-{}. This is fatal because this is not a deleted file",
self.base_entry().entry,
self.base_entry().sequence
);
}
&self.file_name_attribute
}
pub fn bodyfile_lines(&self, mft: &PreprocessedMft, usnjrnl_longflags: bool) -> BodyfileLines {
let mut lines: Vec<String> = Vec::new();
for d in self.streams.iter() {
let name = if d.attribute_type == MftAttributeType::IndexRoot
&& d.name == Some("$I30".to_owned())
{
None
} else {
d.name.as_ref()
};
if let Some(line) =
self.format_si(mft, name, d.attribute_type.to_u32().unwrap(), d.instance)
{
lines.push(line);
}
}
if lines.is_empty() {
if let Some(line) = self.format_si(mft, None, 0, 0) {
lines.push(line);
}
}
BodyfileLines {
standard_info: lines,
filename_info: self.format_fn(mft),
usnjrnl_records: self
.usnjrnl_records
.iter()
.map(|r| self.format_usnjrnl(mft, r, usnjrnl_longflags))
.collect(),
}
}
pub fn bodyfile_lines_count(&self) -> usize {
(match &self.standard_info_timestamps {
Some(_) => cmp::min(self.streams.len(), 1),
None => 0,
} + match &self.file_name_attribute {
Some(_) => 1,
None => 0,
} + self.usnjrnl_records.len())
}
}
pub struct BodyfileLines {
standard_info: Vec<String>,
filename_info: Option<String>,
usnjrnl_records: Vec<String>,
}
impl Iterator for BodyfileLines {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
if !self.standard_info.is_empty() {
return self.standard_info.pop();
}
if self.filename_info.is_some() {
return self.filename_info.take();
}
self.usnjrnl_records.pop()
}
}