#![cfg(target_os = "windows")]
use std::collections::HashMap;
use std::io::Read;
use std::os::windows::ffi::OsStringExt;
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use windows_sys::Win32::Foundation::{CloseHandle, GENERIC_READ, HANDLE, INVALID_HANDLE_VALUE};
use windows_sys::Win32::Security::{
GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY,
};
use windows_sys::Win32::Storage::FileSystem::{
CreateFileW, FILE_FLAG_SEQUENTIAL_SCAN, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE,
OPEN_EXISTING,
};
use windows_sys::Win32::System::Threading::{
GetCurrentProcess, OpenProcessToken, WaitForSingleObject, INFINITE,
};
use windows_sys::Win32::UI::Shell::{ShellExecuteExW, SEE_MASK_NOCLOSEPROCESS, SHELLEXECUTEINFOW};
#[derive(Debug)]
pub struct MftEntry {
pub path: PathBuf,
pub size: u64,
}
pub fn volume_path_opt(path: &Path) -> Option<String> {
let s = path.to_string_lossy();
if s.starts_with("\\\\") || s.starts_with("//") {
return None;
}
let mut chars = s.chars();
let letter = chars.next()?;
if chars.next()? != ':' {
return None;
}
Some(format!("\\\\.\\{}:", letter.to_ascii_uppercase()))
}
pub fn volume_path_for(path: &Path) -> String {
volume_path_opt(path).unwrap_or_else(|| "\\\\.\\C:".to_string())
}
fn drive_root_for(path: &Path) -> Option<PathBuf> {
let s = path.to_string_lossy();
if s.starts_with("\\\\") || s.starts_with("//") {
return None;
}
let mut chars = s.chars();
let letter = chars.next()?;
if chars.next()? != ':' {
return None;
}
Some(PathBuf::from(format!("{}:\\", letter.to_ascii_uppercase())))
}
pub fn is_elevated() -> bool {
unsafe {
let mut token: HANDLE = std::ptr::null_mut();
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) == 0 {
return false;
}
let mut elev = TOKEN_ELEVATION { TokenIsElevated: 0 };
let mut ret_len: u32 = 0;
let ok = GetTokenInformation(
token,
TokenElevation,
std::ptr::addr_of_mut!(elev).cast(),
std::mem::size_of::<TOKEN_ELEVATION>() as u32,
&mut ret_len,
);
CloseHandle(token);
ok != 0 && elev.TokenIsElevated != 0
}
}
const MFT_RECORD_SIZE: usize = 1024;
const MFT_RECORD_MAGIC: &[u8; 4] = b"FILE";
const MFT_ATTR_FILE_NAME: u32 = 0x30;
const MFT_ATTR_DATA: u32 = 0x80;
const MFT_ATTR_END: u32 = 0xFFFF_FFFF;
const MFT_FLAG_IN_USE: u16 = 0x01;
const MFT_FLAG_IS_DIR: u16 = 0x02;
const NS_POSIX: u8 = 0;
const NS_WIN32: u8 = 1;
const NS_DOS: u8 = 2;
const NS_WIN32DOS: u8 = 3;
fn u16le(buf: &[u8], off: usize) -> u16 {
u16::from_le_bytes([buf[off], buf[off + 1]])
}
fn u32le(buf: &[u8], off: usize) -> u32 {
u32::from_le_bytes(buf[off..off + 4].try_into().unwrap())
}
fn u64le(buf: &[u8], off: usize) -> u64 {
u64::from_le_bytes(buf[off..off + 8].try_into().unwrap())
}
fn apply_fixup(buf: &mut [u8]) -> bool {
if buf.len() < MFT_RECORD_SIZE {
return false;
}
let usa_off = u16le(buf, 4) as usize;
let usa_cnt = u16le(buf, 6) as usize; if usa_cnt < 2 || usa_off + usa_cnt * 2 > buf.len() {
return false;
}
let sig = u16le(buf, usa_off);
for i in 1..usa_cnt {
let sector_end = i * 512 - 2;
if sector_end + 1 >= buf.len() {
break;
}
if u16le(buf, sector_end) != sig {
return false; }
let orig = u16le(buf, usa_off + i * 2);
buf[sector_end] = orig as u8;
buf[sector_end + 1] = (orig >> 8) as u8;
}
true
}
struct FileNameInfo {
parent_ref: u64, name: std::ffi::OsString,
namespace: u8,
}
fn parse_filename_attr(buf: &[u8], attr_off: usize, attr_len: usize) -> Option<FileNameInfo> {
if buf[attr_off + 8] != 0 {
return None;
}
let val_off = attr_off + u16le(buf, attr_off + 20) as usize;
if val_off + 66 > buf.len() || val_off + 66 > attr_off + attr_len {
return None;
}
let parent_ref = u64le(buf, val_off) & 0x0000_FFFF_FFFF_FFFF;
let name_len = buf[val_off + 64] as usize; let namespace = buf[val_off + 65];
let name_start = val_off + 66;
let name_end = name_start + name_len * 2;
if name_end > buf.len() || name_end > attr_off + attr_len {
return None;
}
let utf16: Vec<u16> = (0..name_len)
.map(|i| u16le(buf, name_start + i * 2))
.collect();
Some(FileNameInfo {
parent_ref,
name: std::ffi::OsString::from_wide(&utf16),
namespace,
})
}
fn parse_data_size(buf: &[u8], attr_off: usize) -> Option<u64> {
let non_resident = buf[attr_off + 8];
Some(if non_resident == 0 {
u32le(buf, attr_off + 16) as u64
} else {
if attr_off + 56 > buf.len() {
return None;
}
u64le(buf, attr_off + 48)
})
}
struct RawRecord {
parent_ref: u64,
name: std::ffi::OsString,
size: u64,
is_dir: bool,
}
fn parse_record(buf: &[u8]) -> Option<RawRecord> {
if buf.len() < MFT_RECORD_SIZE || &buf[0..4] != MFT_RECORD_MAGIC {
return None;
}
let flags = u16le(buf, 22);
if flags & MFT_FLAG_IN_USE == 0 {
return None; }
if u64le(buf, 32) != 0 {
return None;
}
let is_dir = flags & MFT_FLAG_IS_DIR != 0;
let first_attr = u16le(buf, 20) as usize;
if first_attr >= buf.len() {
return None;
}
let mut best_fn: Option<FileNameInfo> = None;
let mut data_size: Option<u64> = None;
let mut off = first_attr;
loop {
if off + 8 > buf.len() {
break;
}
let attr_type = u32le(buf, off);
if attr_type == MFT_ATTR_END {
break;
}
let attr_len = u32le(buf, off + 4) as usize;
if attr_len < 8 || off + attr_len > buf.len() {
break;
}
match attr_type {
MFT_ATTR_FILE_NAME => {
if let Some(fi) = parse_filename_attr(buf, off, attr_len) {
let better = match &best_fn {
None => true,
Some(prev) => {
namespace_priority(fi.namespace) > namespace_priority(prev.namespace)
}
};
if better {
best_fn = Some(fi);
}
}
}
MFT_ATTR_DATA => {
if data_size.is_none() && buf[off + 9] == 0 {
data_size = parse_data_size(buf, off);
}
}
_ => {}
}
off += attr_len;
}
let fi = best_fn?;
Some(RawRecord {
parent_ref: fi.parent_ref,
name: fi.name,
size: data_size.unwrap_or(0),
is_dir,
})
}
fn namespace_priority(ns: u8) -> u8 {
match ns {
NS_WIN32 | NS_WIN32DOS => 3,
NS_DOS => 1,
NS_POSIX => 2,
_ => 0,
}
}
fn resolve_path(
records: &HashMap<u64, RawRecord>,
start: u64,
drive_root: &Path,
) -> Option<PathBuf> {
const NTFS_ROOT: u64 = 5;
let mut components: Vec<std::ffi::OsString> = Vec::new();
let mut current = start;
let mut seen = std::collections::HashSet::new();
loop {
if current == NTFS_ROOT {
break;
}
if !seen.insert(current) {
return None; }
let rec = records.get(¤t)?;
components.push(rec.name.clone());
current = rec.parent_ref;
}
components.reverse();
let mut path = drive_root.to_path_buf();
for c in components {
path.push(c);
}
Some(path)
}
pub fn enumerate_mft_sizes(root: &Path, recursive: bool) -> Result<Vec<MftEntry>> {
let drive_root =
drive_root_for(root).context("path must be on a local NTFS drive (not UNC)")?;
let mft_path = format!("{}$MFT", drive_root.to_string_lossy());
let mft_path_w: Vec<u16> = mft_path.encode_utf16().chain(std::iter::once(0)).collect();
let handle = unsafe {
CreateFileW(
mft_path_w.as_ptr(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
std::ptr::null(),
OPEN_EXISTING,
FILE_FLAG_SEQUENTIAL_SCAN,
std::ptr::null_mut(),
)
};
if handle == INVALID_HANDLE_VALUE {
bail!(
"Cannot open {} — run as Administrator for MFT direct access",
mft_path
);
}
use std::os::windows::io::FromRawHandle;
let mut file = unsafe { std::fs::File::from_raw_handle(handle as *mut _) };
let mut records: HashMap<u64, RawRecord> = HashMap::with_capacity(500_000);
let mut buf = vec![0u8; MFT_RECORD_SIZE];
let mut record_num = 0u64;
loop {
match file.read_exact(&mut buf) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(e.into()),
}
let mut fbuf = buf.clone();
if apply_fixup(&mut fbuf) {
if let Some(rec) = parse_record(&fbuf) {
records.insert(record_num, rec);
}
}
record_num += 1;
}
let root_canonical = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
let mut results = Vec::new();
for (&rec_num, rec) in &records {
if rec.is_dir {
continue; }
let Some(path) = resolve_path(&records, rec_num, &drive_root) else {
continue;
};
let in_scope = if recursive {
path.starts_with(&root_canonical)
} else {
path.parent()
.is_some_and(|p| p == root_canonical || p == root)
};
if in_scope {
results.push(MftEntry {
path,
size: rec.size,
});
}
}
Ok(results)
}
pub fn spawn_elevated_mft_worker(root: &Path, recursive: bool, output_file: &Path) -> Result<()> {
use std::os::windows::ffi::OsStrExt;
let exe = std::env::current_exe().context("cannot determine current executable path")?;
let mut args = format!(
"--_mft-worker \"{}\" \"{}\"",
output_file.to_string_lossy().replace('"', "\"\""),
root.to_string_lossy().replace('"', "\"\""),
);
if recursive {
args.push_str(" --recursive");
}
let verb_w: Vec<u16> = "runas\0".encode_utf16().collect();
let exe_w: Vec<u16> = exe
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
let args_w: Vec<u16> = args.encode_utf16().chain(std::iter::once(0)).collect();
let mut sei: SHELLEXECUTEINFOW = unsafe { std::mem::zeroed() };
sei.cbSize = std::mem::size_of::<SHELLEXECUTEINFOW>() as u32;
sei.fMask = SEE_MASK_NOCLOSEPROCESS;
sei.lpVerb = verb_w.as_ptr();
sei.lpFile = exe_w.as_ptr();
sei.lpParameters = args_w.as_ptr();
sei.nShow = 2;
let ok = unsafe { ShellExecuteExW(&mut sei) };
if ok == 0 || sei.hProcess.is_null() {
bail!("UAC elevation was cancelled or failed");
}
eprintln!("[*] Elevated MFT worker running — waiting for results...");
unsafe {
WaitForSingleObject(sei.hProcess, INFINITE);
CloseHandle(sei.hProcess);
}
Ok(())
}
pub fn run_mft_worker(root: &Path, recursive: bool, output_file: &Path) -> Result<()> {
let entries = enumerate_mft_sizes(root, recursive)?;
let mut out = std::fs::File::create(output_file)
.with_context(|| format!("cannot create worker output: {}", output_file.display()))?;
use std::io::Write;
for e in &entries {
writeln!(out, "{}\t{}", e.size, e.path.display())?;
}
Ok(())
}
pub fn read_mft_results(output_file: &Path) -> Result<Vec<MftEntry>> {
let content = std::fs::read_to_string(output_file).with_context(|| {
format!(
"failed to read MFT worker output from {}",
output_file.display()
)
})?;
let mut entries = Vec::new();
for line in content.lines() {
let mut parts = line.splitn(2, '\t');
let Some(size_str) = parts.next() else {
continue;
};
let Some(path_str) = parts.next() else {
continue;
};
let size: u64 = size_str.parse().unwrap_or(0);
entries.push(MftEntry {
path: PathBuf::from(path_str),
size,
});
}
Ok(entries)
}