#![forbid(unsafe_code)]
use std::collections::HashMap;
use std::sync::Mutex;
use dokan::{
init, shutdown, CreateFileInfo, DiskSpaceInfo, FileInfo, FileSystemHandler, FileSystemMounter,
FillDataError, FillDataResult, FindData, FindStreamData, MountFlags,
MountOptions as DokanMountOptions, OperationInfo, OperationResult, VolumeInfo,
IO_SECURITY_CONTEXT,
};
use widestring::{U16CStr, U16CString};
use crate::marking::{self, Mark, MarkStream};
use crate::session::Session;
use crate::win_map::{path_components, split_path_stream, to_system_time, windows_attributes};
use crate::{ForensicFs, FsFileType, FsMetadata, MountOptions};
const STATUS_OBJECT_NAME_NOT_FOUND: i32 = 0xC000_0034u32 as i32;
const STATUS_INVALID_DEVICE_REQUEST: i32 = 0xC000_0010u32 as i32;
const STATUS_BUFFER_OVERFLOW: i32 = 0x8000_0005u32 as i32;
const FILE_CASE_SENSITIVE_SEARCH: u32 = 0x0000_0001;
const FILE_CASE_PRESERVED_NAMES: u32 = 0x0000_0002;
const FILE_UNICODE_ON_DISK: u32 = 0x0000_0004;
fn lock_failed() -> i32 {
STATUS_INVALID_DEVICE_REQUEST
}
#[derive(Debug, Clone, Copy)]
pub struct FileCtx {
ino: u64,
stream: Option<MarkStream>,
}
pub struct DokanForensicFs {
fs: Mutex<Box<dyn ForensicFs + Send>>,
root_ino: u64,
label: U16CString,
marks: HashMap<u64, Mark>,
}
impl DokanForensicFs {
fn new(mut fs: Box<dyn ForensicFs + Send>, label: &str) -> Self {
let root_ino = fs.root_ino();
let marks = fs
.deleted_nodes()
.map(|nodes| nodes.iter().map(|n| (n.ino, Mark::from_node(n))).collect())
.unwrap_or_default();
Self {
fs: Mutex::new(fs),
root_ino,
label: U16CString::from_str(label).unwrap_or_default(),
marks,
}
}
fn resolve(&self, path: &str) -> Option<u64> {
let mut ino = self.root_ino;
let mut fs = self.fs.lock().ok()?;
for comp in path_components(path) {
ino = fs.lookup(ino, comp.as_bytes()).ok().flatten()?;
}
Some(ino)
}
fn metadata(&self, ino: u64) -> OperationResult<FsMetadata> {
let mut fs = self.fs.lock().map_err(|_| lock_failed())?;
fs.metadata(ino).map_err(|_| STATUS_OBJECT_NAME_NOT_FOUND)
}
fn mark(&self, ino: u64) -> Option<Mark> {
self.marks.get(&ino).copied()
}
}
fn file_info(meta: &FsMetadata, ino: u64) -> FileInfo {
FileInfo {
attributes: windows_attributes(meta.file_type),
creation_time: to_system_time(meta.crtime),
last_access_time: to_system_time(meta.atime),
last_write_time: to_system_time(meta.mtime),
file_size: meta.size,
number_of_links: u32::from(meta.links_count).max(1),
file_index: ino,
}
}
fn fill_stream(
fill: &mut impl FnMut(&FindStreamData) -> FillDataResult,
name: &str,
size: i64,
) -> OperationResult<()> {
let Ok(name) = U16CString::from_str(name) else {
return Ok(());
};
match fill(&FindStreamData { size, name }) {
Ok(()) | Err(FillDataError::NameTooLong) => Ok(()),
Err(FillDataError::BufferFull) => Err(STATUS_BUFFER_OVERFLOW),
}
}
impl<'c, 'h: 'c> FileSystemHandler<'c, 'h> for DokanForensicFs {
type Context = FileCtx;
#[allow(clippy::too_many_arguments)]
fn create_file(
&'h self,
file_name: &U16CStr,
_security_context: &IO_SECURITY_CONTEXT,
_desired_access: u32,
_file_attributes: u32,
_share_access: u32,
_create_disposition: u32,
_create_options: u32,
_info: &mut OperationInfo<'c, 'h, Self>,
) -> OperationResult<CreateFileInfo<Self::Context>> {
let path = file_name.to_string_lossy();
let (file_path, stream_name) = split_path_stream(&path);
let ino = self
.resolve(file_path)
.ok_or(STATUS_OBJECT_NAME_NOT_FOUND)?;
let meta = self.metadata(ino)?;
let stream = match stream_name {
None => None,
Some(name) => Some(
MarkStream::from_base(name)
.filter(|_| self.mark(ino).is_some())
.ok_or(STATUS_OBJECT_NAME_NOT_FOUND)?,
),
};
Ok(CreateFileInfo {
context: FileCtx { ino, stream },
is_dir: stream.is_none() && matches!(meta.file_type, FsFileType::Directory),
new_file_created: false,
})
}
fn close_file(
&'h self,
_file_name: &U16CStr,
_info: &OperationInfo<'c, 'h, Self>,
_context: &'c Self::Context,
) {
}
fn read_file(
&'h self,
_file_name: &U16CStr,
offset: i64,
buffer: &mut [u8],
_info: &OperationInfo<'c, 'h, Self>,
context: &'c Self::Context,
) -> OperationResult<u32> {
if let Some(stream) = context.stream {
let mark = self.mark(context.ino).ok_or(STATUS_OBJECT_NAME_NOT_FOUND)?;
let bytes = marking::ads_stream_value(&mark, stream);
let start = (offset.max(0) as usize).min(bytes.len());
let n = (bytes.len() - start).min(buffer.len());
buffer[..n].copy_from_slice(&bytes[start..start + n]);
return Ok(n as u32);
}
let data = {
let mut fs = self.fs.lock().map_err(|_| lock_failed())?;
fs.read_file_range(context.ino, offset.max(0) as u64, buffer.len() as u64)
.map_err(|_| STATUS_INVALID_DEVICE_REQUEST)?
};
let n = data.len().min(buffer.len());
buffer[..n].copy_from_slice(&data[..n]);
Ok(n as u32)
}
fn get_file_information(
&'h self,
_file_name: &U16CStr,
_info: &OperationInfo<'c, 'h, Self>,
context: &'c Self::Context,
) -> OperationResult<FileInfo> {
let meta = self.metadata(context.ino)?;
Ok(file_info(&meta, context.ino))
}
fn find_files(
&'h self,
_file_name: &U16CStr,
mut fill_find_data: impl FnMut(&FindData) -> FillDataResult,
_info: &OperationInfo<'c, 'h, Self>,
context: &'c Self::Context,
) -> OperationResult<()> {
let mut entries = {
let mut fs = self.fs.lock().map_err(|_| lock_failed())?;
fs.read_dir(context.ino)
.map_err(|_| STATUS_INVALID_DEVICE_REQUEST)?
};
entries.sort_by(|a, b| a.name.cmp(&b.name));
for e in entries {
if e.name == b"." || e.name == b".." {
continue;
}
let Ok(meta) = self.metadata(e.inode) else {
continue;
};
let Ok(name) = U16CString::from_str(String::from_utf8_lossy(&e.name)) else {
continue;
};
let find = FindData {
attributes: windows_attributes(meta.file_type),
creation_time: to_system_time(meta.crtime),
last_access_time: to_system_time(meta.atime),
last_write_time: to_system_time(meta.mtime),
file_size: meta.size,
file_name: name,
};
match fill_find_data(&find) {
Ok(()) => {}
Err(FillDataError::NameTooLong) => continue,
Err(FillDataError::BufferFull) => return Err(STATUS_BUFFER_OVERFLOW),
}
}
Ok(())
}
fn find_streams(
&'h self,
_file_name: &U16CStr,
mut fill_find_stream_data: impl FnMut(&FindStreamData) -> FillDataResult,
_info: &OperationInfo<'c, 'h, Self>,
context: &'c Self::Context,
) -> OperationResult<()> {
let meta = self.metadata(context.ino)?;
if matches!(meta.file_type, FsFileType::Directory) {
return Ok(());
}
fill_stream(&mut fill_find_stream_data, "::$DATA", meta.size as i64)?;
if let Some(mark) = self.mark(context.ino) {
for stream in marking::ADS_STREAMS {
let bytes = marking::ads_stream_value(&mark, stream);
fill_stream(
&mut fill_find_stream_data,
&stream.ads_full_name(),
bytes.len() as i64,
)?;
}
}
Ok(())
}
fn get_disk_free_space(
&'h self,
_info: &OperationInfo<'c, 'h, Self>,
) -> OperationResult<DiskSpaceInfo> {
Ok(DiskSpaceInfo {
byte_count: 0,
free_byte_count: 0,
available_byte_count: 0,
})
}
fn get_volume_information(
&'h self,
_info: &OperationInfo<'c, 'h, Self>,
) -> OperationResult<VolumeInfo> {
Ok(VolumeInfo {
name: self.label.clone(),
serial_number: 0,
max_component_length: 255,
fs_flags: FILE_CASE_SENSITIVE_SEARCH | FILE_CASE_PRESERVED_NAMES | FILE_UNICODE_ON_DISK,
fs_name: U16CString::from_str("NTFS").unwrap_or_default(),
})
}
}
pub fn mount_windows(
fs: Box<dyn ForensicFs + Send>,
mountpoint: &std::path::Path,
_session: Option<Session>,
options: &MountOptions,
) -> std::io::Result<()> {
let mount_point = U16CString::from_os_str(mountpoint.as_os_str())
.map_err(|e| std::io::Error::other(format!("invalid mount point: {e}")))?;
let handler = DokanForensicFs::new(fs, &options.fs_name);
let dokan_options = DokanMountOptions {
flags: MountFlags::WRITE_PROTECT,
..Default::default()
};
init();
let mut mounter = FileSystemMounter::new(&handler, &mount_point, &dokan_options);
let file_system = mounter
.mount()
.map_err(|e| std::io::Error::other(format!("Dokan mount: {e}")))?;
eprintln!("4n6mount: mounted at {} (Dokan)", mountpoint.display());
drop(file_system);
shutdown();
Ok(())
}