use crate::apfs_impl::ApfsFs;
use crate::filesystem::{DirectoryCommon, File, FileCommon, Filesystem};
use crate::folder_impl::FolderFS;
use exhume_apfs::APFS;
use exhume_body::{Body, BodySlice};
use exhume_exfat::ExFatFS;
use exhume_extfs::ExtFS;
use exhume_ntfs::NTFS;
use exhume_ntfs::bitlocker::BitLockerStream;
use log::info;
use serde_json::Value;
use std::error::Error;
use std::io::{self, Read, Seek, SeekFrom};
#[derive(Debug, Clone, Default)]
pub struct KeyMaterial {
pub bitlocker_fvek: Option<Vec<u8>>,
}
pub enum ImageStream {
Raw(BodySlice),
BitLocker(BitLockerStream<BodySlice>),
}
impl Read for ImageStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
ImageStream::Raw(slice) => slice.read(buf),
ImageStream::BitLocker(bl) => bl.read(buf),
}
}
}
impl Seek for ImageStream {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
match self {
ImageStream::Raw(slice) => slice.seek(pos),
ImageStream::BitLocker(bl) => bl.seek(pos),
}
}
}
#[allow(clippy::large_enum_variant)]
pub enum DetectedFs<T: Read + Seek> {
Ext(ExtFS<T>),
Ntfs(NTFS<T>),
Exfat(ExFatFS<T>),
Apfs(ApfsFs<T>),
Folder(FolderFS),
}
pub enum DetectedFile {
Ext(exhume_extfs::inode::Inode),
Ntfs(exhume_ntfs::mft::MFTRecord),
Exfat(exhume_exfat::exinode::ExInode),
Apfs(crate::apfs_impl::ApfsFileRecord),
Folder(crate::folder_impl::FolderFile),
}
pub enum DetectedDir {
Ext(exhume_extfs::direntry::DirEntry),
Ntfs(exhume_ntfs::mft::DirectoryEntry),
Exfat(exhume_exfat::compat::CompatDirEntry),
Apfs(crate::apfs_impl::ApfsDirectoryEntry),
Folder(crate::folder_impl::FolderDirectory),
}
impl FileCommon for DetectedFile {
fn id(&self) -> u64 {
match self {
DetectedFile::Ext(inode) => inode.id(),
DetectedFile::Ntfs(record) => record.id(),
DetectedFile::Exfat(inode) => inode.id(),
DetectedFile::Apfs(inode) => inode.id(),
DetectedFile::Folder(file) => file.id(),
}
}
fn size(&self) -> u64 {
match self {
DetectedFile::Ext(inode) => inode.size(),
DetectedFile::Ntfs(record) => record.size(),
DetectedFile::Exfat(inode) => inode.size(),
DetectedFile::Apfs(inode) => inode.size(),
DetectedFile::Folder(file) => file.size(),
}
}
fn is_dir(&self) -> bool {
match self {
DetectedFile::Ext(inode) => inode.is_dir(),
DetectedFile::Ntfs(record) => record.is_dir(),
DetectedFile::Exfat(inode) => inode.is_dir(),
DetectedFile::Apfs(inode) => inode.is_dir(),
DetectedFile::Folder(file) => file.is_dir(),
}
}
fn to_string(&self) -> String {
match self {
DetectedFile::Ext(inode) => FileCommon::to_string(inode),
DetectedFile::Ntfs(record) => FileCommon::to_string(record),
DetectedFile::Exfat(inode) => FileCommon::to_string(inode),
DetectedFile::Apfs(inode) => FileCommon::to_string(inode),
DetectedFile::Folder(file) => FileCommon::to_string(file),
}
}
fn to_json(&self) -> Value {
match self {
DetectedFile::Ext(inode) => inode.to_json(),
DetectedFile::Ntfs(record) => record.to_json(),
DetectedFile::Exfat(inode) => inode.to_json(),
DetectedFile::Apfs(inode) => inode.to_json(),
DetectedFile::Folder(file) => file.to_json(),
}
}
}
impl DirectoryCommon for DetectedDir {
fn file_id(&self) -> u64 {
match self {
DetectedDir::Ext(d) => d.file_id(),
DetectedDir::Ntfs(d) => d.file_id(),
DetectedDir::Exfat(d) => d.file_id(),
DetectedDir::Apfs(d) => d.file_id(),
DetectedDir::Folder(d) => d.file_id(),
}
}
fn name(&self) -> &str {
match self {
DetectedDir::Ext(d) => d.name(),
DetectedDir::Ntfs(d) => d.name(),
DetectedDir::Exfat(d) => d.name(),
DetectedDir::Apfs(d) => d.name(),
DetectedDir::Folder(d) => d.name(),
}
}
fn to_string(&self) -> String {
match self {
DetectedDir::Ext(d) => DirectoryCommon::to_string(d),
DetectedDir::Ntfs(d) => DirectoryCommon::to_string(d),
DetectedDir::Exfat(d) => DirectoryCommon::to_string(d),
DetectedDir::Apfs(d) => DirectoryCommon::to_string(d),
DetectedDir::Folder(d) => DirectoryCommon::to_string(d),
}
}
fn to_json(&self) -> Value {
match self {
DetectedDir::Ext(d) => d.to_json(),
DetectedDir::Ntfs(d) => d.to_json(),
DetectedDir::Exfat(d) => d.to_json(),
DetectedDir::Apfs(d) => d.to_json(),
DetectedDir::Folder(d) => d.to_json(),
}
}
}
impl<T: Read + Seek> Filesystem for DetectedFs<T> {
type FileType = DetectedFile;
type DirectoryType = DetectedDir;
fn filesystem_type(&self) -> String {
match self {
DetectedFs::Ext(fs) => fs.filesystem_type(),
DetectedFs::Ntfs(fs) => fs.filesystem_type(),
DetectedFs::Exfat(fs) => fs.filesystem_type(),
DetectedFs::Apfs(fs) => fs.filesystem_type(),
DetectedFs::Folder(fs) => fs.filesystem_type(),
}
}
fn path_separator(&self) -> String {
match self {
DetectedFs::Ext(fs) => fs.path_separator(),
DetectedFs::Ntfs(fs) => fs.path_separator(),
DetectedFs::Exfat(fs) => fs.path_separator(),
DetectedFs::Apfs(fs) => fs.path_separator(),
DetectedFs::Folder(fs) => fs.path_separator(),
}
}
fn record_count(&mut self) -> u64 {
match self {
DetectedFs::Ext(fs) => fs.record_count(),
DetectedFs::Ntfs(fs) => fs.record_count(),
DetectedFs::Exfat(fs) => fs.record_count(),
DetectedFs::Apfs(fs) => fs.record_count(),
DetectedFs::Folder(fs) => fs.record_count(),
}
}
fn block_size(&self) -> u64 {
match self {
DetectedFs::Ext(fs) => fs.block_size(),
DetectedFs::Ntfs(fs) => fs.block_size(),
DetectedFs::Exfat(fs) => fs.block_size(),
DetectedFs::Apfs(fs) => fs.block_size(),
DetectedFs::Folder(fs) => fs.block_size(),
}
}
fn get_metadata(&self) -> Result<Value, Box<dyn Error>> {
match self {
DetectedFs::Ext(fs) => fs.get_metadata(),
DetectedFs::Ntfs(fs) => fs.get_metadata(),
DetectedFs::Exfat(fs) => fs.get_metadata(),
DetectedFs::Apfs(fs) => fs.get_metadata(),
DetectedFs::Folder(fs) => fs.get_metadata(),
}
}
fn get_metadata_pretty(&self) -> Result<String, Box<dyn Error>> {
match self {
DetectedFs::Ext(fs) => fs.get_metadata_pretty(),
DetectedFs::Ntfs(fs) => fs.get_metadata_pretty(),
DetectedFs::Exfat(fs) => fs.get_metadata_pretty(),
DetectedFs::Apfs(fs) => fs.get_metadata_pretty(),
DetectedFs::Folder(fs) => fs.get_metadata_pretty(),
}
}
fn get_file(&mut self, file_id: u64) -> Result<Self::FileType, Box<dyn Error>> {
match self {
DetectedFs::Ext(fs) => fs.get_file(file_id).map(DetectedFile::Ext),
DetectedFs::Ntfs(fs) => fs.get_file(file_id).map(DetectedFile::Ntfs),
DetectedFs::Exfat(fs) => fs.get_file(file_id).map(DetectedFile::Exfat),
DetectedFs::Apfs(fs) => fs.get_file(file_id).map(DetectedFile::Apfs),
DetectedFs::Folder(fs) => fs.get_file(file_id).map(DetectedFile::Folder),
}
}
fn get_file_by_path(
&mut self,
path: &str,
file_id: u64,
) -> Result<Self::FileType, Box<dyn Error>> {
match self {
DetectedFs::Ext(fs) => fs.get_file_by_path(path, file_id).map(DetectedFile::Ext),
DetectedFs::Ntfs(fs) => fs.get_file_by_path(path, file_id).map(DetectedFile::Ntfs),
DetectedFs::Exfat(fs) => fs.get_file_by_path(path, file_id).map(DetectedFile::Exfat),
DetectedFs::Apfs(fs) => fs.get_file_by_path(path, file_id).map(DetectedFile::Apfs),
DetectedFs::Folder(fs) => fs.get_file_by_path(path, file_id).map(DetectedFile::Folder),
}
}
fn read_file_content(&mut self, record: &Self::FileType) -> Result<Vec<u8>, Box<dyn Error>> {
match (self, record) {
(DetectedFs::Ext(fs), DetectedFile::Ext(inode)) => fs.read_file_content(inode),
(DetectedFs::Ntfs(fs), DetectedFile::Ntfs(rec)) => fs.read_file_content(rec),
(DetectedFs::Exfat(fs), DetectedFile::Exfat(inode)) => fs.read_file_content(inode),
(DetectedFs::Apfs(fs), DetectedFile::Apfs(inode)) => fs.read_file_content(inode),
(DetectedFs::Folder(fs), DetectedFile::Folder(file)) => fs.read_file_content(file),
_ => Err("filesystem / record variant mismatch".into()),
}
}
fn read_file_prefix(
&mut self,
record: &Self::FileType,
length: usize,
) -> Result<Vec<u8>, Box<dyn Error>> {
match (self, record) {
(DetectedFs::Ext(fs), DetectedFile::Ext(inode)) => fs.read_file_prefix(inode, length),
(DetectedFs::Ntfs(fs), DetectedFile::Ntfs(rec)) => fs.read_file_prefix(rec, length),
(DetectedFs::Exfat(fs), DetectedFile::Exfat(inode)) => {
fs.read_file_prefix(inode, length)
}
(DetectedFs::Apfs(fs), DetectedFile::Apfs(inode)) => fs.read_file_prefix(inode, length),
(DetectedFs::Folder(fs), DetectedFile::Folder(file)) => {
fs.read_file_prefix(file, length)
}
_ => Err("filesystem / record variant mismatch".into()),
}
}
fn read_file_slice(
&mut self,
record: &Self::FileType,
offset: u64,
length: usize,
) -> Result<Vec<u8>, Box<dyn Error>> {
match (self, record) {
(DetectedFs::Ext(fs), DetectedFile::Ext(inode)) => {
fs.read_file_slice(inode, offset, length)
}
(DetectedFs::Ntfs(fs), DetectedFile::Ntfs(rec)) => {
fs.read_file_slice(rec, offset, length)
}
(DetectedFs::Exfat(fs), DetectedFile::Exfat(inode)) => {
fs.read_file_slice(inode, offset, length)
}
(DetectedFs::Apfs(fs), DetectedFile::Apfs(inode)) => {
fs.read_file_slice(inode, offset, length)
}
(DetectedFs::Folder(fs), DetectedFile::Folder(file)) => {
fs.read_file_slice(file, offset, length)
}
_ => Err("filesystem / record variant mismatch".into()),
}
}
fn list_dir(
&mut self,
file: &Self::FileType,
) -> Result<Vec<Self::DirectoryType>, Box<dyn Error>> {
match (self, file) {
(DetectedFs::Ext(fs), DetectedFile::Ext(inode)) => fs
.list_dir(inode)
.map(|v| v.into_iter().map(DetectedDir::Ext).collect()),
(DetectedFs::Ntfs(fs), DetectedFile::Ntfs(rec)) => fs
.list_dir(rec.id())
.map(|v| v.into_iter().map(DetectedDir::Ntfs).collect()),
(DetectedFs::Exfat(fs), DetectedFile::Exfat(inode)) => Filesystem::list_dir(fs, inode)
.map(|v| v.into_iter().map(DetectedDir::Exfat).collect()),
(DetectedFs::Apfs(fs), DetectedFile::Apfs(inode)) => Filesystem::list_dir(fs, inode)
.map(|v| v.into_iter().map(DetectedDir::Apfs).collect()),
(DetectedFs::Folder(fs), DetectedFile::Folder(file)) => Filesystem::list_dir(fs, file)
.map(|v| v.into_iter().map(DetectedDir::Folder).collect()),
_ => Err("filesystem / record variant mismatch".into()),
}
}
fn get_root_file_id(&self) -> u64 {
match self {
DetectedFs::Ext(fs) => fs.get_root_file_id(),
DetectedFs::Ntfs(fs) => fs.get_root_file_id(),
DetectedFs::Exfat(fs) => fs.get_root_file_id(),
DetectedFs::Apfs(fs) => fs.get_root_file_id(),
DetectedFs::Folder(fs) => fs.get_root_file_id(),
}
}
fn walk_fs(
&mut self,
callback: &mut dyn FnMut(crate::filesystem::WalkEvent),
) -> Result<(), Box<dyn Error>> {
match self {
DetectedFs::Ext(fs) => fs.walk_fs(callback),
DetectedFs::Ntfs(fs) => fs.walk_fs(callback),
DetectedFs::Exfat(fs) => fs.walk_fs(callback),
DetectedFs::Apfs(fs) => fs.walk_fs(callback),
DetectedFs::Folder(fs) => fs.walk_fs(callback),
}
}
fn record_to_file(&self, record: &Self::FileType, inode_num: u64, absolute_path: &str) -> File {
match (self, record) {
(DetectedFs::Ext(fs), DetectedFile::Ext(inode)) => {
fs.record_to_file(inode, inode_num, absolute_path)
}
(DetectedFs::Ntfs(fs), DetectedFile::Ntfs(rec)) => {
fs.record_to_file(rec, inode_num, absolute_path)
}
(DetectedFs::Exfat(fs), DetectedFile::Exfat(inode)) => {
fs.record_to_file(inode, inode_num, absolute_path)
}
(DetectedFs::Apfs(fs), DetectedFile::Apfs(inode)) => {
fs.record_to_file(inode, inode_num, absolute_path)
}
(DetectedFs::Folder(fs), DetectedFile::Folder(file)) => {
fs.record_to_file(file, inode_num, absolute_path)
}
_ => unreachable!("filesystem / record variant mismatch"),
}
}
}
pub fn detect_filesystem(
body: &Body,
offset: u64,
partition_size: u64,
keys: Option<KeyMaterial>,
) -> Result<DetectedFs<ImageStream>, Box<dyn std::error::Error>> {
let partition = BodySlice::new(body, offset, partition_size)
.map_err(|e| format!("Could not create BodySlice: {e}"))?;
if let Ok(ext_fs) = ExtFS::new(ImageStream::Raw(partition)) {
info!("Detected an Extended filesystem.");
return Ok(DetectedFs::Ext(ext_fs));
}
let partition = BodySlice::new(body, offset, partition_size)
.map_err(|e| format!("Could not create BodySlice: {e}"))?;
if let Ok(apfs) = APFS::new(ImageStream::Raw(partition))
&& let Ok(apfs_fs) = ApfsFs::new(apfs)
{
info!("Detected an APFS filesystem/container.");
return Ok(DetectedFs::Apfs(apfs_fs));
}
let partition = BodySlice::new(body, offset, partition_size)
.map_err(|e| format!("Could not create BodySlice: {e}"))?;
if let Ok(exfat) = ExFatFS::new(ImageStream::Raw(partition)) {
info!("Detected an exFAT filesystem.");
return Ok(DetectedFs::Exfat(exfat));
}
let partition = BodySlice::new(body, offset, partition_size)
.map_err(|e| format!("Could not create BodySlice: {e}"))?;
match NTFS::new(ImageStream::Raw(partition)) {
Ok(ntfs) => {
info!("Detected an NT filesystem.");
return Ok(DetectedFs::Ntfs(ntfs));
}
Err(e) if e.to_string().contains("-FVE-FS-") => {
if let Some(mut km) = keys {
if let Some(fvek) = km.bitlocker_fvek.take() {
info!("BitLocker detected. Attempting to decrypt with provided FVEK...");
let partition_for_bl = BodySlice::new(body, offset, partition_size)
.map_err(|e| format!("Could not create BodySlice for BL: {e}"))?;
match BitLockerStream::new(partition_for_bl, &fvek, 512) {
Ok(bl_stream) => match NTFS::new(ImageStream::BitLocker(bl_stream)) {
Ok(ntfs) => {
info!("Successfully detected BitLocker-decrypted NT filesystem.");
return Ok(DetectedFs::Ntfs(ntfs));
}
Err(err) => {
return Err(format!(
"Failed to parse NTFS over BitLocker: {}",
err
)
.into());
}
},
Err(err) => {
return Err(
format!("Failed to initialize BitLocker stream: {}", err).into()
);
}
}
} else {
return Err(
"Partition is BitLocker-encrypted (-FVE-FS-) but no FVEK was provided."
.into(),
);
}
} else {
return Err(
"Partition is BitLocker-encrypted (-FVE-FS-) but no keys were provided.".into(),
);
}
}
Err(_) => {}
}
Err(format!("No supported filesystem detected at offset {offset}").into())
}
pub fn detect_filesystem_from_path(
path: &str,
) -> Result<DetectedFs<ImageStream>, Box<dyn std::error::Error>> {
let folder_fs = FolderFS::new(std::path::PathBuf::from(path));
Ok(DetectedFs::Folder(folder_fs))
}