use std::io::{Read, Seek};
use forensic_vfs::{
Allocation, ByteRun, DeletedNode, DeletedStream, DirEntry, DirStream, ExtentStream, FileId,
FileSystem, FsKind, FsMeta, MacbTimes, NodeKind, NodeStream, ResidencyKind, RunAlloc, RunFlags,
RunInfo, SectorSizes, SmallHex, StreamId, TimeResolution, TimeSource, TimeStamp,
TimeZonePolicy, VfsError, VfsResult,
};
use forensicnomicon::ntfs::{attr_types, filename_namespace, mft_records};
use crate::attribute::AttributeBody;
use crate::error::NtfsError;
use crate::file_name::FileName;
use crate::fs::NtfsFs;
use crate::parse_attributes;
use crate::record::MftRecordHeader;
use crate::standard_information::StandardInformation;
use crate::time::Filetime;
const FN_FLAG_DIRECTORY: u32 = 0x1000_0000;
fn entry_of(id: FileId) -> VfsResult<u64> {
match id {
FileId::NtfsRef { entry, .. } => Ok(entry),
other => Err(VfsError::Unsupported {
layer: "ntfs file-id",
scheme: format!("{other:?}"),
}),
}
}
fn stream_name(stream: StreamId) -> VfsResult<Option<&'static str>> {
match stream {
StreamId::Default => Ok(None),
other => Err(VfsError::Unsupported {
layer: "ntfs stream",
scheme: format!("{other:?}"),
}),
}
}
fn map_err(e: NtfsError) -> VfsError {
match e {
NtfsError::Io(source) => VfsError::Io {
op: "ntfs read",
source,
},
other => VfsError::Decode {
layer: "ntfs",
offset: 0,
detail: other.to_string(),
bytes: SmallHex::new(&[]),
},
}
}
fn build_meta(entry: u64, rec: &[u8]) -> VfsResult<FsMeta> {
let header = MftRecordHeader::parse(rec).map_err(map_err)?;
let attrs = parse_attributes(rec, header.first_attribute_offset as usize).map_err(map_err)?;
let mut times = MacbTimes::default();
if let Some(content) = attrs
.iter()
.find(|a| a.type_code == attr_types::STANDARD_INFORMATION)
.and_then(|a| a.resident_content(rec))
{
if let Ok(si) = StandardInformation::parse(content) {
let ts = |ft: Filetime| TimeStamp {
unix_nanos: ft.to_unix_nanos(),
source: TimeSource::Si,
resolution: TimeResolution::WinFileTime,
};
times = MacbTimes {
born: Some(ts(si.created)),
modified: Some(ts(si.modified)),
changed: Some(ts(si.mft_modified)),
accessed: Some(ts(si.accessed)),
};
}
}
let data = attrs
.iter()
.find(|a| a.type_code == attr_types::DATA && a.name.is_none());
let (size, residency) = match data.map(|a| &a.body) {
Some(AttributeBody::Resident { content_length, .. }) => (
u64::from(*content_length),
ResidencyKind::Resident {
inline_len: *content_length,
},
),
Some(AttributeBody::NonResident { real_size, .. }) => {
(*real_size, ResidencyKind::NonResident)
}
None => (0, ResidencyKind::Resident { inline_len: 0 }),
};
Ok(FsMeta {
ino: entry,
kind: if header.is_directory() {
NodeKind::Dir
} else {
NodeKind::File
},
allocated: if header.is_in_use() {
Allocation::Allocated
} else {
Allocation::Deleted
},
size,
nlink: u32::from(header.hard_link_count),
uid: None,
gid: None,
mode: None,
times,
streams: Vec::new(),
residency,
link_target: None,
})
}
fn namespace_rank(ns: u8) -> u8 {
match ns {
filename_namespace::WIN32_AND_DOS => 3,
filename_namespace::WIN32 => 2,
filename_namespace::POSIX => 1,
_ => 0, }
}
fn best_file_name(rec: &[u8]) -> Option<FileName> {
let header = MftRecordHeader::parse(rec).ok()?;
let attrs = parse_attributes(rec, header.first_attribute_offset as usize).ok()?;
attrs
.iter()
.filter(|a| a.type_code == attr_types::FILE_NAME)
.filter_map(|a| a.resident_content(rec))
.filter_map(|c| FileName::parse(c).ok())
.max_by_key(|fnm| namespace_rank(fnm.namespace))
}
fn mft_record_count<R: Read + Seek + Send>(fs: &NtfsFs<R>) -> VfsResult<u64> {
let rec0 = fs.read_record(mft_records::MFT).map_err(map_err)?;
let meta = build_meta(mft_records::MFT, &rec0)?;
let rec_size = fs.boot().mft_record_size;
if rec_size == 0 {
return Ok(0); }
Ok(meta.size / rec_size)
}
impl<R: Read + Seek + Send> FileSystem for NtfsFs<R> {
fn kind(&self) -> FsKind {
FsKind::NTFS
}
fn root(&self) -> FileId {
let seq = self
.read_record(mft_records::ROOT)
.ok()
.and_then(|rec| MftRecordHeader::parse(&rec).ok())
.map_or(0, |h| h.sequence_number);
FileId::NtfsRef {
entry: mft_records::ROOT,
seq,
}
}
fn sector_sizes(&self) -> SectorSizes {
let boot = self.boot();
SectorSizes {
logical: u32::from(boot.bytes_per_sector),
physical: u32::from(boot.bytes_per_sector),
cluster_or_block: boot.cluster_size() as u32,
}
}
fn timestamp_zone(&self) -> TimeZonePolicy {
TimeZonePolicy::Utc
}
fn read_dir(&self, ino: FileId) -> VfsResult<DirStream> {
let entry = entry_of(ino)?;
let rec = self.read_record(entry).map_err(map_err)?;
let entries = self.directory_entries(&rec).map_err(map_err)?;
let out: Vec<VfsResult<DirEntry>> = entries
.into_iter()
.filter_map(|e| {
let file_ref = e.file_reference;
e.file_name.map(|fnm| {
let kind = if fnm.flags & FN_FLAG_DIRECTORY != 0 {
NodeKind::Dir
} else {
NodeKind::File
};
Ok(DirEntry {
name: fnm.name.into_bytes(),
id: FileId::NtfsRef {
entry: file_ref.record_number,
seq: file_ref.sequence,
},
kind,
})
})
})
.collect();
Ok(DirStream::new(out.into_iter()))
}
fn extents(&self, ino: FileId, stream: StreamId) -> VfsResult<ExtentStream> {
let entry = entry_of(ino)?;
let name = stream_name(stream)?;
let runs = self.runs_by_record(entry, name).map_err(map_err)?;
let cluster = self.boot().cluster_size();
let out: Vec<VfsResult<RunInfo>> = runs
.into_iter()
.map(|r| {
let image_offset = r.lcn.unwrap_or(0).saturating_mul(cluster);
let len = r.length.saturating_mul(cluster);
Ok(RunInfo {
run: ByteRun {
image_offset,
len,
flags: RunFlags {
sparse: r.lcn.is_none(),
..RunFlags::default()
},
},
alloc: RunAlloc::Allocated,
})
})
.collect();
Ok(ExtentStream::new(out.into_iter()))
}
fn lookup(&self, parent: FileId, name: &[u8]) -> VfsResult<Option<FileId>> {
let entry = entry_of(parent)?;
let rec = self.read_record(entry).map_err(map_err)?;
for e in self.directory_entries(&rec).map_err(map_err)? {
if let Some(fnm) = &e.file_name {
if fnm.name.as_bytes() == name {
return Ok(Some(FileId::NtfsRef {
entry: e.file_reference.record_number,
seq: e.file_reference.sequence,
}));
}
}
}
Ok(None)
}
fn meta(&self, ino: FileId) -> VfsResult<FsMeta> {
let entry = entry_of(ino)?;
let rec = self.read_record(entry).map_err(map_err)?;
build_meta(entry, &rec)
}
fn read_at(&self, ino: FileId, stream: StreamId, off: u64, buf: &mut [u8]) -> VfsResult<usize> {
let entry = entry_of(ino)?;
let name = stream_name(stream)?;
let cap = off.saturating_add(buf.len() as u64);
let data = self
.read_data_by_record(entry, name, cap)
.map_err(map_err)?;
let start = usize::try_from(off).unwrap_or(usize::MAX);
if start >= data.len() {
return Ok(0);
}
let n = buf.len().min(data.len() - start);
buf[..n].copy_from_slice(&data[start..start + n]);
Ok(n)
}
fn read_link(&self, _ino: FileId, _cap: usize) -> VfsResult<Vec<u8>> {
Ok(Vec::new())
}
fn deleted(&self) -> VfsResult<NodeStream> {
Ok(NodeStream::empty())
}
fn deleted_nodes(&self) -> VfsResult<DeletedStream> {
let count = mft_record_count(self)?;
let mut out: Vec<VfsResult<DeletedNode>> = Vec::new();
for entry in 0..count {
let Ok(rec) = self.read_record(entry) else {
continue;
};
let Ok(header) = MftRecordHeader::parse(&rec) else {
continue;
};
if header.is_in_use() {
continue;
}
let Some(fnm) = best_file_name(&rec) else {
continue; };
let Ok(meta) = build_meta(entry, &rec) else {
continue;
};
let parent = if fnm.parent.record_number == 0 {
None
} else {
Some(FileId::NtfsRef {
entry: fnm.parent.record_number,
seq: fnm.parent.sequence,
})
};
out.push(Ok(DeletedNode {
id: FileId::NtfsRef {
entry,
seq: header.sequence_number,
},
name: fnm.name.into_bytes(),
parent,
meta,
}));
}
Ok(DeletedStream::new(out.into_iter()))
}
fn unallocated(&self) -> VfsResult<ExtentStream> {
Ok(ExtentStream::empty())
}
}