use std::io::Read;
use crate::Result;
use crate::block::BlockDevice;
use crate::fs::{DeviceKind, FileMeta, FileSource};
use super::attribute::{
FileName, TYPE_DATA, TYPE_FILE_NAME, TYPE_INDEX_ALLOCATION, TYPE_INDEX_ROOT,
TYPE_REPARSE_POINT, TYPE_STANDARD_INFORMATION,
};
use super::format::{
self, FIRST_USER_RECORD, FormatOpts, LayoutResult, REC_ROOT, build_file_name_value,
build_non_resident_attr, build_resident_attr, build_si_value, emit_record, encode_single_run,
insert_into_index_root, pack_mft_ref, rewrite_resident_attr, unix_to_filetime,
};
use super::mft;
const MAX_INDEX_ROOT_BYTES: usize = 512;
const WRITE_SCRATCH: usize = 64 * 1024;
#[derive(Debug)]
pub struct WriterState {
pub layout: LayoutResult,
pub mft_bitmap: Vec<u8>,
pub next_user_record: u64,
pub dir_cache: std::collections::HashMap<String, u64>,
pub cluster_size: u64,
pub dirty: bool,
}
impl WriterState {
pub fn new(layout: LayoutResult) -> Self {
let mft_records = layout.mft_records;
let mut mft_bitmap = vec![0u8; mft_records.div_ceil(8) as usize];
for r in 0..16u64 {
mft_bitmap[(r / 8) as usize] |= 1u8 << ((r % 8) as u8);
}
let mut dir_cache = std::collections::HashMap::new();
dir_cache.insert("/".to_string(), REC_ROOT);
let cluster_size = layout.cluster_size as u64;
Self {
layout,
mft_bitmap,
next_user_record: FIRST_USER_RECORD,
dir_cache,
cluster_size,
dirty: true,
}
}
fn allocate_mft_record(&mut self, dev: &mut dyn BlockDevice) -> Result<u64> {
for r in self.next_user_record..self.layout.mft_records {
let i = (r / 8) as usize;
let m = 1u8 << ((r % 8) as u8);
if self.mft_bitmap[i] & m == 0 {
self.mft_bitmap[i] |= m;
self.next_user_record = r + 1;
return Ok(r);
}
}
self.extend_mft(dev)?;
for r in self.next_user_record..self.layout.mft_records {
let i = (r / 8) as usize;
let m = 1u8 << ((r % 8) as u8);
if self.mft_bitmap[i] & m == 0 {
self.mft_bitmap[i] |= m;
self.next_user_record = r + 1;
return Ok(r);
}
}
Err(crate::Error::Unsupported(
"ntfs: writer cannot extend $MFT further".into(),
))
}
fn extend_mft(&mut self, _dev: &mut dyn BlockDevice) -> Result<()> {
let new_clusters = 8u64;
let new_lcn = self.layout.bitmap.allocate(new_clusters)?;
let rec_size = self.layout.mft_record_size as u64;
let new_records = new_clusters * self.cluster_size / rec_size;
self.layout.mft_extents.push((new_lcn, new_clusters));
self.layout.mft_records += new_records;
let new_bm_size = self.layout.mft_records.div_ceil(8) as usize;
while self.mft_bitmap.len() < new_bm_size {
self.mft_bitmap.push(0);
}
self.dirty = true;
Ok(())
}
pub fn alloc_clusters(&mut self, count: u64) -> Result<u64> {
self.dirty = true;
self.layout.bitmap.allocate(count)
}
pub fn mft_offset(&self, rec_no: u64) -> Result<u64> {
let rec_size = self.layout.mft_record_size as u64;
let target = rec_no * rec_size;
let mut walked: u64 = 0;
for &(lcn, length) in &self.layout.mft_extents {
let span = length * self.cluster_size;
if target < walked + span {
let local = target - walked;
return Ok(lcn * self.cluster_size + local);
}
walked += span;
}
Err(crate::Error::InvalidImage(format!(
"ntfs: record {rec_no} past end of $MFT"
)))
}
}
impl super::Ntfs {
pub fn format(dev: &mut dyn BlockDevice, opts: &FormatOpts) -> Result<Self> {
let layout = format::format_volume(dev, opts)?;
let mut ntfs = Self::open(dev)?;
ntfs.writer = Some(WriterState::new(layout));
Ok(ntfs)
}
pub fn create_file(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
src: FileSource,
meta: FileMeta,
) -> Result<()> {
if self.writer.is_none() {
return Err(crate::Error::Unsupported(
"ntfs: create_file requires a writer (call Ntfs::format first)".into(),
));
}
let (parent_path, base_name) = split_parent(path)?;
let parent_rec = self.resolve_dir(dev, &parent_path)?;
let file_size = src.len().map_err(crate::Error::from)?;
let (mut reader, _len) = src.open().map_err(crate::Error::from)?;
let writer = self.writer.as_mut().expect("writer present");
let rec_no = writer.allocate_mft_record(dev)?;
let filetime = unix_to_filetime(meta.mtime);
let rec_size = writer.layout.mft_record_size as usize;
let cluster_size = writer.cluster_size;
let resident_budget = rec_size.saturating_sub(232);
let (data_attr, alloc_clusters) = if (file_size as usize) <= resident_budget {
let mut buf = Vec::with_capacity(file_size as usize);
let mut remaining = file_size;
let mut tmp = [0u8; WRITE_SCRATCH];
while remaining > 0 {
let want = (remaining as usize).min(tmp.len());
let n = reader.read(&mut tmp[..want]).map_err(crate::Error::from)?;
if n == 0 {
break;
}
buf.extend_from_slice(&tmp[..n]);
remaining -= n as u64;
}
(build_resident_attr(TYPE_DATA, &[], &buf, 0, 0), 0u64)
} else {
let need_clusters = file_size.div_ceil(cluster_size);
let data_lcn = writer.alloc_clusters(need_clusters)?;
let mut scratch = vec![0u8; WRITE_SCRATCH];
let mut written: u64 = 0;
while written < file_size {
let chunk = ((file_size - written) as usize).min(scratch.len());
let mut filled = 0;
while filled < chunk {
let n = reader
.read(&mut scratch[filled..chunk])
.map_err(crate::Error::from)?;
if n == 0 {
break;
}
filled += n;
}
if filled < chunk {
for b in &mut scratch[filled..chunk] {
*b = 0;
}
}
let phys = data_lcn * cluster_size + written;
dev.write_at(phys, &scratch[..chunk])?;
written += chunk as u64;
}
let last_cluster_end = need_clusters * cluster_size;
if written < last_cluster_end {
let pad_off = data_lcn * cluster_size + written;
let pad_len = (last_cluster_end - written) as usize;
let pad = vec![0u8; pad_len];
dev.write_at(pad_off, &pad)?;
}
let runs = encode_single_run(data_lcn, need_clusters);
let attr = build_non_resident_attr(
TYPE_DATA,
&[],
&runs,
0,
need_clusters - 1,
need_clusters * cluster_size,
file_size,
file_size,
0,
0,
);
(attr, need_clusters)
};
let _ = alloc_clusters;
let parent_ref = pack_mft_ref(parent_rec, 1);
let si = build_resident_attr(
TYPE_STANDARD_INFORMATION,
&[],
&build_si_value(filetime, dos_attrs_from_mode(meta.mode, false)),
0,
0,
);
let fn_value = build_file_name_value(
parent_ref,
base_name,
dos_flags_from_mode(meta.mode, false),
file_size,
(file_size + cluster_size - 1) & !(cluster_size - 1),
filetime,
FileName::NAMESPACE_WIN32,
);
let fn_attr = build_resident_attr(TYPE_FILE_NAME, &[], &fn_value, 0, 1);
let mut rec_buf = vec![0u8; rec_size];
emit_record(
&mut rec_buf,
rec_size,
rec_no,
mft::RecordHeader::FLAG_IN_USE,
&[si, fn_attr, data_attr],
writer.layout.bytes_per_sector as usize,
1,
);
let off = writer.mft_offset(rec_no)?;
dev.write_at(off, &rec_buf)?;
self.add_entry_to_dir(dev, parent_rec, &fn_value, rec_no, false)?;
Ok(())
}
pub fn create_dir(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
meta: FileMeta,
) -> Result<()> {
if self.writer.is_none() {
return Err(crate::Error::Unsupported(
"ntfs: create_dir requires a writer (call Ntfs::format first)".into(),
));
}
let (parent_path, base_name) = split_parent(path)?;
let parent_rec = self.resolve_dir(dev, &parent_path)?;
let writer = self.writer.as_mut().expect("writer present");
let rec_no = writer.allocate_mft_record(dev)?;
let rec_size = writer.layout.mft_record_size as usize;
let filetime = unix_to_filetime(meta.mtime);
let parent_ref = pack_mft_ref(parent_rec, 1);
let si = build_resident_attr(
TYPE_STANDARD_INFORMATION,
&[],
&build_si_value(filetime, dos_attrs_from_mode(meta.mode, true)),
0,
0,
);
let fn_value = build_file_name_value(
parent_ref,
base_name,
dos_flags_from_mode(meta.mode, true),
0,
0,
filetime,
FileName::NAMESPACE_WIN32,
);
let fn_attr = build_resident_attr(TYPE_FILE_NAME, &[], &fn_value, 0, 1);
let i30_name: Vec<u8> = "$I30"
.encode_utf16()
.flat_map(|u| u.to_le_bytes())
.collect();
let idx_root = build_resident_attr(
TYPE_INDEX_ROOT,
&i30_name,
&format::build_empty_index_root(),
0,
0,
);
let mut rec_buf = vec![0u8; rec_size];
emit_record(
&mut rec_buf,
rec_size,
rec_no,
mft::RecordHeader::FLAG_IN_USE | mft::RecordHeader::FLAG_DIRECTORY,
&[si, fn_attr, idx_root],
writer.layout.bytes_per_sector as usize,
1,
);
let off = writer.mft_offset(rec_no)?;
dev.write_at(off, &rec_buf)?;
self.add_entry_to_dir(dev, parent_rec, &fn_value, rec_no, true)?;
let dir_path = normalize_path(path);
if let Some(w) = self.writer.as_mut() {
w.dir_cache.insert(dir_path, rec_no);
}
Ok(())
}
pub fn create_symlink(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
target: &str,
meta: FileMeta,
) -> Result<()> {
if self.writer.is_none() {
return Err(crate::Error::Unsupported(
"ntfs: create_symlink requires a writer".into(),
));
}
let (parent_path, base_name) = split_parent(path)?;
let parent_rec = self.resolve_dir(dev, &parent_path)?;
let writer = self.writer.as_mut().expect("writer present");
let rec_no = writer.allocate_mft_record(dev)?;
let rec_size = writer.layout.mft_record_size as usize;
let filetime = unix_to_filetime(meta.mtime);
let parent_ref = pack_mft_ref(parent_rec, 1);
let si = build_resident_attr(
TYPE_STANDARD_INFORMATION,
&[],
&build_si_value(filetime, 0x400), 0,
0,
);
let fn_value = build_file_name_value(
parent_ref,
base_name,
0x400, 0,
0,
filetime,
FileName::NAMESPACE_WIN32,
);
let fn_attr = build_resident_attr(TYPE_FILE_NAME, &[], &fn_value, 0, 1);
let empty_data = build_resident_attr(TYPE_DATA, &[], &[], 0, 0);
let target_utf16: Vec<u16> = target.encode_utf16().collect();
let target_bytes: Vec<u8> = target_utf16.iter().flat_map(|u| u.to_le_bytes()).collect();
let substitute_off = 0u16;
let substitute_len = target_bytes.len() as u16;
let print_off = substitute_len;
let print_len = substitute_len;
let flags: u32 = if target.starts_with('/') || target.starts_with('\\') {
0
} else {
1
};
let mut reparse_data = Vec::new();
reparse_data.extend_from_slice(&substitute_off.to_le_bytes());
reparse_data.extend_from_slice(&substitute_len.to_le_bytes());
reparse_data.extend_from_slice(&print_off.to_le_bytes());
reparse_data.extend_from_slice(&print_len.to_le_bytes());
reparse_data.extend_from_slice(&flags.to_le_bytes());
reparse_data.extend_from_slice(&target_bytes);
reparse_data.extend_from_slice(&target_bytes);
let reparse_tag: u32 = 0xA000_000C;
let reparse_len = reparse_data.len() as u16;
let mut reparse_payload = Vec::new();
reparse_payload.extend_from_slice(&reparse_tag.to_le_bytes());
reparse_payload.extend_from_slice(&reparse_len.to_le_bytes());
reparse_payload.extend_from_slice(&0u16.to_le_bytes()); reparse_payload.extend_from_slice(&reparse_data);
let reparse_attr = build_resident_attr(TYPE_REPARSE_POINT, &[], &reparse_payload, 0, 0);
let mut rec_buf = vec![0u8; rec_size];
emit_record(
&mut rec_buf,
rec_size,
rec_no,
mft::RecordHeader::FLAG_IN_USE,
&[si, fn_attr, empty_data, reparse_attr],
writer.layout.bytes_per_sector as usize,
1,
);
let off = writer.mft_offset(rec_no)?;
dev.write_at(off, &rec_buf)?;
self.add_entry_to_dir(dev, parent_rec, &fn_value, rec_no, false)?;
Ok(())
}
pub fn create_device(
&mut self,
_dev: &mut dyn BlockDevice,
_path: &str,
_kind: DeviceKind,
_major: u32,
_minor: u32,
_meta: FileMeta,
) -> Result<()> {
Err(crate::Error::Unsupported(
"ntfs: special files (char/block/fifo/socket) are not representable".into(),
))
}
pub fn flush(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
let Some(w) = self.writer.as_mut() else {
return Ok(());
};
if !w.dirty {
return Ok(());
}
let cluster_size = w.cluster_size;
let rec_size = w.layout.mft_record_size as usize;
let bps = w.layout.bytes_per_sector as usize;
{
let off = w.layout.bitmap_lcn * cluster_size;
dev.write_at(off, &w.layout.bitmap.bytes)?;
let padded = w.layout.bitmap_clusters * cluster_size;
if (w.layout.bitmap.bytes.len() as u64) < padded {
let pad = vec![0u8; (padded - w.layout.bitmap.bytes.len() as u64) as usize];
dev.write_at(off + w.layout.bitmap.bytes.len() as u64, &pad)?;
}
}
{
let off = w.layout.mft_bitmap_lcn * cluster_size;
dev.write_at(off, &w.mft_bitmap)?;
}
{
let mut rec_buf = vec![0u8; rec_size];
let filetime = unix_to_filetime(0);
let parent_root_ref = pack_mft_ref(REC_ROOT, 1);
format::build_mft_record(
&mut rec_buf,
rec_size,
parent_root_ref,
&w.layout.mft_extents,
w.layout.mft_records,
w.layout.mft_bitmap_lcn,
w.layout.mft_bitmap_clusters,
filetime,
cluster_size,
bps,
);
let off = w.layout.mft_extents[0].0 * cluster_size;
dev.write_at(off, &rec_buf)?;
}
{
let mut boot_buf = vec![0u8; bps];
dev.read_at(0, &mut boot_buf)?;
let last_lba_offset =
(w.layout.total_clusters * (cluster_size / bps as u64) - 1) * bps as u64;
dev.write_at(last_lba_offset, &boot_buf)?;
}
w.dirty = false;
dev.sync()?;
Ok(())
}
fn resolve_dir(&mut self, dev: &mut dyn BlockDevice, path: &str) -> Result<u64> {
let norm = normalize_path(path);
if let Some(w) = self.writer.as_ref() {
if let Some(&rec) = w.dir_cache.get(&norm) {
return Ok(rec);
}
}
let rec = self.lookup_path(dev, &norm)?;
if let Some(w) = self.writer.as_mut() {
w.dir_cache.insert(norm, rec);
}
Ok(rec)
}
fn add_entry_to_dir(
&mut self,
dev: &mut dyn BlockDevice,
dir_rec: u64,
file_name_value: &[u8],
file_rec: u64,
_is_directory: bool,
) -> Result<()> {
let writer = self.writer.as_mut().expect("writer present");
let rec_size = writer.layout.mft_record_size as usize;
let sector_size = writer.layout.bytes_per_sector as usize;
let cluster_size = writer.cluster_size;
let _ = cluster_size;
let off = writer.mft_offset(dir_rec)?;
let mut rec = vec![0u8; rec_size];
dev.read_at(off, &mut rec)?;
mft::apply_fixup(&mut rec, sector_size)?;
let file_ref = pack_mft_ref(file_rec, 1);
let entry = build_index_entry(file_ref, file_name_value, 0, None);
let current =
extract_resident_attr_value(&rec, TYPE_INDEX_ROOT, "$I30").ok_or_else(|| {
crate::Error::InvalidImage("ntfs: directory missing $INDEX_ROOT".into())
})?;
let is_large_index = current.len() >= 29 && current[28] & 0x01 != 0;
if is_large_index {
return self.insert_into_allocation_block(dev, dir_rec, &entry);
}
match insert_into_index_root(¤t, &entry, MAX_INDEX_ROOT_BYTES) {
Ok(new_value) => {
rewrite_resident_attr(&mut rec, rec_size, TYPE_INDEX_ROOT, "$I30", &new_value)?;
mft::install_fixup(&mut rec, sector_size, 1);
dev.write_at(off, &rec)?;
Ok(())
}
Err(crate::Error::Unsupported(_)) => {
self.promote_index_to_allocation(dev, dir_rec, &entry)
}
Err(e) => Err(e),
}
}
fn insert_into_allocation_block(
&mut self,
dev: &mut dyn BlockDevice,
dir_rec: u64,
new_entry: &[u8],
) -> Result<()> {
let writer = self.writer.as_mut().expect("writer present");
let rec_size = writer.layout.mft_record_size as usize;
let sector_size = writer.layout.bytes_per_sector as usize;
let cluster_size = writer.cluster_size;
let block_size = writer.layout.index_record_size as usize;
let off = writer.mft_offset(dir_rec)?;
let mut rec = vec![0u8; rec_size];
dev.read_at(off, &mut rec)?;
mft::apply_fixup(&mut rec, sector_size)?;
let alloc_runs = extract_non_resident_runs(&rec, TYPE_INDEX_ALLOCATION, "$I30")
.ok_or_else(|| {
crate::Error::InvalidImage(
"ntfs: promoted directory missing $INDEX_ALLOCATION".into(),
)
})?;
let (alloc_lcn, _alloc_clusters) = alloc_runs[0];
let block_off = alloc_lcn * cluster_size;
let mut block = vec![0u8; block_size];
dev.read_at(block_off, &mut block)?;
mft::apply_fixup(&mut block, sector_size)?;
let first_entry_off = u32::from_le_bytes(block[0x18..0x1C].try_into().unwrap()) as usize;
let bytes_in_use = u32::from_le_bytes(block[0x1C..0x20].try_into().unwrap()) as usize;
let entries_start = 0x18 + first_entry_off;
let entries_end = 0x18 + bytes_in_use;
let mut existing_entries: Vec<Vec<u8>> = Vec::new();
let mut cursor = entries_start;
while cursor + 16 <= entries_end {
let entry_len =
u16::from_le_bytes(block[cursor + 8..cursor + 10].try_into().unwrap()) as usize;
if entry_len < 16 || cursor + entry_len > entries_end {
break;
}
let flags = u32::from_le_bytes(block[cursor + 12..cursor + 16].try_into().unwrap());
let is_last = flags & 0x02 != 0;
if is_last {
break;
}
existing_entries.push(block[cursor..cursor + entry_len].to_vec());
cursor += entry_len;
}
existing_entries.push(new_entry.to_vec());
let mut new_block = build_indx_block(block_size, sector_size, 0, &existing_entries)?;
mft::install_fixup(&mut new_block, sector_size, 1);
dev.write_at(block_off, &new_block)?;
let _ = rec_size;
Ok(())
}
fn promote_index_to_allocation(
&mut self,
dev: &mut dyn BlockDevice,
dir_rec: u64,
new_entry: &[u8],
) -> Result<()> {
let writer = self.writer.as_mut().expect("writer present");
let rec_size = writer.layout.mft_record_size as usize;
let sector_size = writer.layout.bytes_per_sector as usize;
let cluster_size = writer.cluster_size;
let index_block_size = writer.layout.index_record_size as u64;
let blocks_per_cluster = cluster_size / index_block_size;
let clusters_per_block = if blocks_per_cluster == 0 {
index_block_size.div_ceil(cluster_size)
} else {
1
};
let off = writer.mft_offset(dir_rec)?;
let mut rec = vec![0u8; rec_size];
dev.read_at(off, &mut rec)?;
mft::apply_fixup(&mut rec, sector_size)?;
let current =
extract_resident_attr_value(&rec, TYPE_INDEX_ROOT, "$I30").ok_or_else(|| {
crate::Error::InvalidImage(
"ntfs: directory missing $INDEX_ROOT for promotion".into(),
)
})?;
let mut existing_entries: Vec<Vec<u8>> = Vec::new();
let mut cursor = 16 + 16;
let bytes_in_use = u32::from_le_bytes(current[20..24].try_into().unwrap()) as usize;
let entries_end = 16 + bytes_in_use;
while cursor + 16 <= entries_end {
let entry_len =
u16::from_le_bytes(current[cursor + 8..cursor + 10].try_into().unwrap()) as usize;
let flags = u32::from_le_bytes(current[cursor + 12..cursor + 16].try_into().unwrap());
if entry_len < 16 || cursor + entry_len > entries_end {
break;
}
let is_last = flags & 0x02 != 0;
if !is_last {
existing_entries.push(current[cursor..cursor + entry_len].to_vec());
} else {
break;
}
cursor += entry_len;
}
existing_entries.push(new_entry.to_vec());
let mut block_buf = build_indx_block(
index_block_size as usize,
sector_size,
0, &existing_entries,
)?;
mft::install_fixup(&mut block_buf, sector_size, 1);
let alloc_lcn = writer.alloc_clusters(clusters_per_block)?;
let alloc_off = alloc_lcn * cluster_size;
dev.write_at(alloc_off, &block_buf)?;
if block_buf.len() < (clusters_per_block * cluster_size) as usize {
let pad = vec![0u8; (clusters_per_block * cluster_size) as usize - block_buf.len()];
dev.write_at(alloc_off + block_buf.len() as u64, &pad)?;
}
let new_root = build_large_index_root(0);
rewrite_resident_attr(&mut rec, rec_size, TYPE_INDEX_ROOT, "$I30", &new_root)?;
let i30_name: Vec<u8> = "$I30"
.encode_utf16()
.flat_map(|u| u.to_le_bytes())
.collect();
let runs = encode_single_run(alloc_lcn, clusters_per_block);
let alloc_attr = build_non_resident_attr(
TYPE_INDEX_ALLOCATION,
&i30_name,
&runs,
0,
clusters_per_block - 1,
clusters_per_block * cluster_size,
clusters_per_block * cluster_size,
clusters_per_block * cluster_size,
0,
0,
);
let bm_value = vec![0x01u8, 0, 0, 0, 0, 0, 0, 0]; let bm_attr =
build_resident_attr(super::attribute::TYPE_BITMAP, &i30_name, &bm_value, 0, 0);
append_attrs(&mut rec, rec_size, &[alloc_attr, bm_attr])?;
mft::install_fixup(&mut rec, sector_size, 1);
dev.write_at(off, &rec)?;
Ok(())
}
}
fn dos_attrs_from_mode(mode: u16, isdir: bool) -> u32 {
let mut a = 0u32;
if mode & 0o222 == 0 {
a |= 0x01; }
if isdir {
} else {
a |= 0x20; }
a
}
fn dos_flags_from_mode(mode: u16, isdir: bool) -> u32 {
let mut a = dos_attrs_from_mode(mode, isdir);
if isdir {
a |= 0x1000_0000; }
a
}
fn extract_non_resident_runs(rec: &[u8], type_code: u32, name: &str) -> Option<Vec<(u64, u64)>> {
let hdr = mft::RecordHeader::parse(rec).ok()?;
let bytes_in_use = hdr.bytes_in_use as usize;
let first = hdr.first_attribute_offset as usize;
let mut cursor = first;
while cursor + 4 <= bytes_in_use {
let tc = u32::from_le_bytes(rec[cursor..cursor + 4].try_into().ok()?);
if tc == 0xFFFF_FFFF {
return None;
}
let len = u32::from_le_bytes(rec[cursor + 4..cursor + 8].try_into().ok()?) as usize;
let non_resident = rec[cursor + 8] != 0;
let name_len = rec[cursor + 9] as usize;
let name_off = u16::from_le_bytes(rec[cursor + 10..cursor + 12].try_into().ok()?) as usize;
let attr_name = if name_len == 0 {
String::new()
} else {
super::attribute::decode_utf16le(
&rec[cursor + name_off..cursor + name_off + name_len * 2],
)
};
if tc == type_code && attr_name == name && non_resident {
let runs_off =
u16::from_le_bytes(rec[cursor + 0x20..cursor + 0x22].try_into().ok()?) as usize;
let runs_bytes = &rec[cursor + runs_off..cursor + len];
let extents = super::run_list::decode(runs_bytes).ok()?;
let mut out = Vec::new();
for e in extents {
if let Some(lcn) = e.lcn {
out.push((lcn, e.length));
}
}
return Some(out);
}
cursor += len;
}
None
}
fn extract_resident_attr_value(rec: &[u8], type_code: u32, name: &str) -> Option<Vec<u8>> {
let hdr = mft::RecordHeader::parse(rec).ok()?;
let bytes_in_use = hdr.bytes_in_use as usize;
let first = hdr.first_attribute_offset as usize;
let mut cursor = first;
while cursor + 4 <= bytes_in_use {
let tc = u32::from_le_bytes(rec[cursor..cursor + 4].try_into().ok()?);
if tc == 0xFFFF_FFFF {
return None;
}
let len = u32::from_le_bytes(rec[cursor + 4..cursor + 8].try_into().ok()?) as usize;
let non_resident = rec[cursor + 8] != 0;
let name_len = rec[cursor + 9] as usize;
let name_off = u16::from_le_bytes(rec[cursor + 10..cursor + 12].try_into().ok()?) as usize;
let attr_name = if name_len == 0 {
String::new()
} else {
super::attribute::decode_utf16le(
&rec[cursor + name_off..cursor + name_off + name_len * 2],
)
};
if tc == type_code && attr_name == name && !non_resident {
let value_len =
u32::from_le_bytes(rec[cursor + 0x10..cursor + 0x14].try_into().ok()?) as usize;
let value_off =
u16::from_le_bytes(rec[cursor + 0x14..cursor + 0x16].try_into().ok()?) as usize;
return Some(rec[cursor + value_off..cursor + value_off + value_len].to_vec());
}
cursor += len;
}
None
}
fn append_attrs(rec: &mut [u8], rec_size: usize, attrs: &[Vec<u8>]) -> Result<()> {
let hdr = mft::RecordHeader::parse(rec)?;
let bytes_in_use = hdr.bytes_in_use as usize;
let term_pos = bytes_in_use - 4;
let total_new: usize = attrs.iter().map(|a| a.len()).sum();
if term_pos + total_new + 4 > rec_size {
return Err(crate::Error::Unsupported(
"ntfs: not enough room in MFT record for new attributes".into(),
));
}
let mut next_attr_id = u16::from_le_bytes(rec[0x28..0x2A].try_into().unwrap());
let mut cursor = term_pos;
for a in attrs {
rec[cursor..cursor + a.len()].copy_from_slice(a);
rec[cursor + 14..cursor + 16].copy_from_slice(&next_attr_id.to_le_bytes());
next_attr_id = next_attr_id.wrapping_add(1);
cursor += a.len();
}
rec[cursor..cursor + 4].copy_from_slice(&[0xFFu8, 0xFF, 0xFF, 0xFF]);
cursor += 4;
let new_bytes_in_use = cursor as u32;
rec[0x18..0x1C].copy_from_slice(&new_bytes_in_use.to_le_bytes());
rec[0x28..0x2A].copy_from_slice(&next_attr_id.to_le_bytes());
for b in &mut rec[cursor..rec_size] {
*b = 0;
}
Ok(())
}
fn build_indx_block(
block_size: usize,
sector_size: usize,
vcn: u64,
entries: &[Vec<u8>],
) -> Result<Vec<u8>> {
let mut buf = vec![0u8; block_size];
buf[0..4].copy_from_slice(b"INDX");
buf[4..6].copy_from_slice(&0x28u16.to_le_bytes());
let sectors = block_size / sector_size;
let usa_size = sectors + 1;
buf[6..8].copy_from_slice(&(usa_size as u16).to_le_bytes());
buf[16..24].copy_from_slice(&vcn.to_le_bytes());
let usa_end = 0x28 + 2 * usa_size;
let entries_start = (usa_end + 7) & !7;
let first_entry_offset = (entries_start - 0x18) as u32;
let term_entry = {
let mut e = vec![0u8; 16];
e[8..10].copy_from_slice(&16u16.to_le_bytes());
e[12..16].copy_from_slice(&0x02u32.to_le_bytes());
e
};
let entries_total: usize = entries.iter().map(|e| e.len()).sum::<usize>() + term_entry.len();
if entries_start + entries_total > block_size {
return Err(crate::Error::Unsupported(
"ntfs: directory entry overflow in single INDX block".into(),
));
}
let bytes_in_use = (entries_start - 0x18) as u32 + entries_total as u32;
let bytes_allocated = (block_size - 0x18) as u32;
let flags: u8 = 0;
buf[0x18..0x1C].copy_from_slice(&first_entry_offset.to_le_bytes());
buf[0x1C..0x20].copy_from_slice(&bytes_in_use.to_le_bytes());
buf[0x20..0x24].copy_from_slice(&bytes_allocated.to_le_bytes());
buf[0x24] = flags;
let mut cursor = entries_start;
for e in entries {
buf[cursor..cursor + e.len()].copy_from_slice(e);
cursor += e.len();
}
buf[cursor..cursor + term_entry.len()].copy_from_slice(&term_entry);
Ok(buf)
}
fn build_large_index_root(child_vcn: u64) -> Vec<u8> {
let index_block_size = format::DEFAULT_INDEX_RECORD_SIZE;
let cpib: i8 = 1;
let mut v = Vec::new();
v.extend_from_slice(&TYPE_FILE_NAME.to_le_bytes());
v.extend_from_slice(&1u32.to_le_bytes());
v.extend_from_slice(&index_block_size.to_le_bytes());
v.push(cpib as u8);
v.extend_from_slice(&[0u8; 3]);
let first_entry_offset = 16u32;
let term_entry_len = 24u32;
let bytes_in_use = 16u32 + term_entry_len;
v.extend_from_slice(&first_entry_offset.to_le_bytes());
v.extend_from_slice(&bytes_in_use.to_le_bytes());
v.extend_from_slice(&bytes_in_use.to_le_bytes());
v.push(0x01); v.extend_from_slice(&[0u8; 3]);
let mut term = vec![0u8; 24];
term[8..10].copy_from_slice(&24u16.to_le_bytes());
term[10..12].copy_from_slice(&0u16.to_le_bytes());
term[12..16].copy_from_slice(&0x03u32.to_le_bytes()); term[16..24].copy_from_slice(&child_vcn.to_le_bytes());
v.extend_from_slice(&term);
v
}
fn build_index_entry(
file_ref: u64,
file_name_value: &[u8],
flags: u32,
child_vcn: Option<u64>,
) -> Vec<u8> {
let key_len = file_name_value.len();
let mut payload_len = 16 + key_len;
payload_len = (payload_len + 7) & !7;
let entry_len = if child_vcn.is_some() {
payload_len + 8
} else {
payload_len
};
let mut e = vec![0u8; entry_len];
e[0..8].copy_from_slice(&file_ref.to_le_bytes());
e[8..10].copy_from_slice(&(entry_len as u16).to_le_bytes());
e[10..12].copy_from_slice(&(key_len as u16).to_le_bytes());
let final_flags = flags | if child_vcn.is_some() { 0x01 } else { 0 };
e[12..16].copy_from_slice(&final_flags.to_le_bytes());
e[16..16 + key_len].copy_from_slice(file_name_value);
if let Some(vcn) = child_vcn {
let off = entry_len - 8;
e[off..off + 8].copy_from_slice(&vcn.to_le_bytes());
}
e
}
fn split_parent(path: &str) -> Result<(String, &str)> {
if !path.starts_with('/') {
return Err(crate::Error::InvalidArgument(format!(
"ntfs: path must be absolute, got {path:?}"
)));
}
let trimmed = path.trim_end_matches('/');
let last_slash = trimmed
.rfind('/')
.ok_or_else(|| crate::Error::InvalidArgument("ntfs: path has no name component".into()))?;
let (parent, rest) = trimmed.split_at(last_slash);
let parent = if parent.is_empty() {
"/".to_string()
} else {
parent.to_string()
};
let base = &rest[1..]; if base.is_empty() {
return Err(crate::Error::InvalidArgument(
"ntfs: missing file name".into(),
));
}
Ok((parent, base))
}
fn normalize_path(path: &str) -> String {
if path == "/" {
return "/".to_string();
}
path.trim_end_matches('/').to_string()
}