use std::io::Read;
use crate::Result;
use crate::block::BlockDevice;
use crate::fs::dir_batch::{DEFAULT_CAPACITY, DirBatch};
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_with_security, emit_record,
encode_single_run, pack_mft_ref, rewrite_resident_attr, security_id_for, unix_to_filetime,
};
use super::mft;
use super::secure::SecurityClass;
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 dir_batch: DirBatch<u64, Vec<u8>>,
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,
dir_batch: DirBatch::new(DEFAULT_CAPACITY),
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 cluster_size = self.cluster_size;
let rec_size = self.layout.mft_record_size as u64;
let cur_data: u64 = self.layout.mft_extents.iter().map(|(_, l)| *l).sum();
let new_clusters = cur_data.max(8);
let new_lcn = self.layout.bitmap.allocate(new_clusters)?;
let new_records = new_clusters * 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);
}
let needed_clusters = (new_bm_size as u64).div_ceil(cluster_size);
if needed_clusters > self.layout.mft_bitmap_clusters {
let grow_to = needed_clusters
.max(self.layout.mft_bitmap_clusters * 2)
.max(1);
let new_bm_lcn = self.layout.bitmap.allocate(grow_to)?;
self.layout.mft_bitmap_lcn = new_bm_lcn;
self.layout.mft_bitmap_clusters = grow_to;
}
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));
ntfs.index_system_files_in_root(dev)?;
Ok(ntfs)
}
fn index_system_files_in_root(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
const SYSTEM_RECS: &[u64] = &[
4, 8, 6, 7, 11, 2, 0, 1, 12, 13, 14, 15, 9, 10, 3, ];
let (rec_size, sector_size) = {
let w = self.writer.as_ref().expect("writer present");
(
w.layout.mft_record_size as usize,
w.layout.bytes_per_sector as usize,
)
};
for &rec_no in SYSTEM_RECS {
let off = self
.writer
.as_ref()
.expect("writer present")
.mft_offset(rec_no)?;
let mut rec = vec![0u8; rec_size];
dev.read_at(off, &mut rec)?;
mft::apply_fixup(&mut rec, sector_size)?;
let Some(fn_value) = extract_resident_attr_value(&rec, TYPE_FILE_NAME, "") else {
continue;
};
let is_dir = mft::RecordHeader::parse(&rec)
.map(|h| h.is_directory())
.unwrap_or(false);
self.add_entry_to_dir(dev, REC_ROOT, &fn_value, rec_no, is_dir)?;
}
Ok(())
}
pub fn create_file(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
src: FileSource,
meta: FileMeta,
) -> Result<()> {
self.ensure_writer(dev)?;
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 parent_ref = pack_mft_ref(parent_rec, 1);
let si = build_resident_attr(
TYPE_STANDARD_INFORMATION,
&[],
&build_si_value_with_security(
filetime,
dos_attrs_from_mode(meta.mode, false),
security_id_for(SecurityClass::User),
),
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);
const MFT_HEADER_OVERHEAD: usize = 64;
const RESIDENT_DATA_HDR: usize = 24;
const TERM_SLACK: usize = 16;
let resident_budget = rec_size.saturating_sub(
MFT_HEADER_OVERHEAD + si.len() + fn_attr.len() + RESIDENT_DATA_HDR + TERM_SLACK,
);
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 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<()> {
self.ensure_writer(dev)?;
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_with_security(
filetime,
dos_attrs_from_mode(meta.mode, true),
security_id_for(SecurityClass::User),
),
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<()> {
self.ensure_writer(dev)?;
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_with_security(
filetime,
0x400, security_id_for(SecurityClass::User),
),
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<()> {
let magic: &[u8; 8] = match kind {
DeviceKind::Char => b"IntxCHR\0",
DeviceKind::Block => b"IntxBLK\0",
DeviceKind::Fifo | DeviceKind::Socket => {
return Err(crate::Error::Unsupported(
"ntfs: FIFO / socket nodes are not representable in ntfs-3g's Intx \
vocabulary (only char / block / symlink); use ext or HFS+ instead"
.into(),
));
}
};
let mut payload: Vec<u8> = Vec::with_capacity(24);
payload.extend_from_slice(magic);
payload.extend_from_slice(&(major as u64).to_le_bytes());
payload.extend_from_slice(&(minor as u64).to_le_bytes());
debug_assert_eq!(payload.len(), 24);
let len = payload.len() as u64;
let src = FileSource::Reader {
reader: Box::new(std::io::Cursor::new(payload)),
len,
};
self.create_file(dev, path, src, meta)
}
pub fn remove(&mut self, dev: &mut dyn BlockDevice, path: &str) -> Result<()> {
self.ensure_writer(dev)?;
let norm = normalize_path(path);
if norm == "/" {
return Err(crate::Error::InvalidArgument(
"ntfs: refusing to remove the root directory".into(),
));
}
let target_rec = self.lookup_path(dev, &norm)?;
if target_rec < FIRST_USER_RECORD {
return Err(crate::Error::InvalidArgument(format!(
"ntfs: refusing to remove system record {target_rec}"
)));
}
let (parent_path, _base_name) = split_parent(&norm)?;
let parent_rec = self.resolve_dir(dev, &parent_path)?;
let records = self.load_record_set(dev, target_rec)?;
if records.len() > 1 {
return Err(crate::Error::Unsupported(
"ntfs: remove of records with $ATTRIBUTE_LIST spill is not supported".into(),
));
}
let target_rec_bytes = records[0].1.clone();
let target_hdr = mft::RecordHeader::parse(&target_rec_bytes)?;
if !target_hdr.is_in_use() {
return Err(crate::Error::InvalidImage(format!(
"ntfs: record {target_rec} is not in use"
)));
}
if target_hdr.is_directory() {
let kids = self.read_directory(dev, target_rec)?;
if !kids.is_empty() {
return Err(crate::Error::InvalidArgument(format!(
"ntfs: cannot remove non-empty directory ({} entries)",
kids.len()
)));
}
}
let link_count = u16::from_le_bytes(target_rec_bytes[0x12..0x14].try_into().unwrap());
let is_hardlink = link_count > 1;
for attr_res in super::attribute::AttributeIter::new(
&target_rec_bytes,
target_hdr.first_attribute_offset as usize,
) {
let attr = attr_res?;
if attr.type_code == super::attribute::TYPE_DATA
&& (attr.flags
& (super::attribute::ATTR_FLAG_COMPRESSED
| super::attribute::ATTR_FLAG_ENCRYPTED
| super::attribute::ATTR_FLAG_SPARSE))
!= 0
{
return Err(crate::Error::Unsupported(
"ntfs: remove of compressed / encrypted / sparse $DATA is not supported".into(),
));
}
}
self.remove_entries_from_parent(dev, parent_rec, target_rec)?;
if is_hardlink {
let mut rec_buf = target_rec_bytes.clone();
let new_lc = link_count - 1;
rec_buf[0x12..0x14].copy_from_slice(&new_lc.to_le_bytes());
let off = self
.writer
.as_ref()
.expect("writer present")
.mft_offset(target_rec)?;
let sector_size = self
.writer
.as_ref()
.expect("writer present")
.layout
.bytes_per_sector as usize;
mft::install_fixup(&mut rec_buf, sector_size, 1);
dev.write_at(off, &rec_buf)?;
} else {
self.free_runlist_clusters(&target_rec_bytes)?;
self.mark_record_free(dev, target_rec, target_rec_bytes)?;
}
if let Some(w) = self.writer.as_mut() {
w.dir_cache.remove(&norm);
}
Ok(())
}
fn remove_entries_from_parent(
&mut self,
dev: &mut dyn BlockDevice,
parent_rec: u64,
target_rec: u64,
) -> Result<()> {
let (rec_size, sector_size) = {
let w = self.writer.as_ref().expect("writer present");
(
w.layout.mft_record_size as usize,
w.layout.bytes_per_sector as usize,
)
};
let off = self
.writer
.as_ref()
.expect("writer present")
.mft_offset(parent_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: parent directory missing $INDEX_ROOT".into())
})?;
let is_large_index = current.len() >= 29 && current[28] & 0x01 != 0;
if is_large_index {
self.remove_entry_from_allocation_block(dev, parent_rec, target_rec)
} else {
let new_value = format::remove_entry_from_index_root(¤t, target_rec)?;
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(())
}
}
fn remove_entry_from_allocation_block(
&mut self,
dev: &mut dyn BlockDevice,
dir_rec: u64,
target_rec: u64,
) -> 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;
const FILE_REF_MASK: u64 = 0x0000_FFFF_FFFF_FFFF;
let mut kept_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;
}
let file_ref =
u64::from_le_bytes(block[cursor..cursor + 8].try_into().unwrap()) & FILE_REF_MASK;
if file_ref != target_rec {
kept_entries.push(block[cursor..cursor + entry_len].to_vec());
}
cursor += entry_len;
}
let mut new_block = build_indx_block(block_size, sector_size, 0, &kept_entries)?;
mft::install_fixup(&mut new_block, sector_size, 1);
dev.write_at(block_off, &new_block)?;
let _ = rec_size; Ok(())
}
fn free_runlist_clusters(&mut self, rec_bytes: &[u8]) -> Result<()> {
let writer = self.writer.as_mut().expect("writer present");
for (_tc, _name, runs) in for_each_non_resident_attr(rec_bytes) {
for (lcn, length) in runs {
for c in lcn..lcn.saturating_add(length) {
writer.layout.bitmap.clear(c);
}
}
}
writer.dirty = true;
Ok(())
}
fn mark_record_free(
&mut self,
dev: &mut dyn BlockDevice,
target_rec: u64,
mut rec_bytes: Vec<u8>,
) -> Result<()> {
let writer = self.writer.as_mut().expect("writer present");
let sector_size = writer.layout.bytes_per_sector as usize;
let off = writer.mft_offset(target_rec)?;
let mut flags = u16::from_le_bytes(rec_bytes[0x16..0x18].try_into().unwrap());
flags &= !mft::RecordHeader::FLAG_IN_USE;
rec_bytes[0x16..0x18].copy_from_slice(&flags.to_le_bytes());
let seq = u16::from_le_bytes(rec_bytes[0x10..0x12].try_into().unwrap());
let mut new_seq = seq.wrapping_add(1);
if new_seq == 0 {
new_seq = 1;
}
rec_bytes[0x10..0x12].copy_from_slice(&new_seq.to_le_bytes());
mft::install_fixup(&mut rec_bytes, sector_size, 1);
dev.write_at(off, &rec_bytes)?;
let i = (target_rec / 8) as usize;
let m = 1u8 << ((target_rec % 8) as u8);
if i < writer.mft_bitmap.len() {
writer.mft_bitmap[i] &= !m;
}
if target_rec < writer.next_user_record {
writer.next_user_record = target_rec;
}
writer.dirty = true;
Ok(())
}
fn reconstruct_writer_state(&mut self, dev: &mut dyn BlockDevice) -> Result<WriterState> {
let cluster_size = self.boot.cluster_size() as u64;
let mft_record_size = self.boot.mft_record_size();
let bytes_per_sector = self.boot.bytes_per_sector;
let sectors_per_cluster = self.boot.sectors_per_cluster;
let index_record_size = self.boot.index_record_size();
let total_clusters = dev.total_size() / cluster_size;
let mft_set = self.load_record_set(dev, 0)?;
let mft_rec = &mft_set[0].1;
let mft_extents = extract_non_resident_runs(mft_rec, TYPE_DATA, "").ok_or_else(|| {
crate::Error::InvalidImage("ntfs: $MFT (rec 0) has no non-resident $DATA".into())
})?;
let mft_clusters_total: u64 = mft_extents.iter().map(|&(_, len)| len).sum();
let mft_records = mft_clusters_total * cluster_size / mft_record_size as u64;
let mft_bitmap_runs = extract_non_resident_runs(mft_rec, super::attribute::TYPE_BITMAP, "")
.ok_or_else(|| {
crate::Error::Unsupported(
"ntfs: reopen-mutate requires a non-resident $MFT $BITMAP".into(),
)
})?;
let (mft_bitmap_lcn, mft_bitmap_clusters) = mft_bitmap_runs[0];
let mft_bitmap = read_runs_prefix(
dev,
&mft_bitmap_runs,
cluster_size,
mft_records.div_ceil(8) as usize,
)?;
let bm_set = self.load_record_set(dev, 6)?;
let bm_rec = &bm_set[0].1;
let bitmap_runs = extract_non_resident_runs(bm_rec, TYPE_DATA, "").ok_or_else(|| {
crate::Error::InvalidImage("ntfs: $Bitmap (rec 6) has no non-resident $DATA".into())
})?;
let bitmap_lcn = bitmap_runs[0].0;
let bitmap_clusters: u64 = bitmap_runs.iter().map(|&(_, len)| len).sum();
let bitmap_bytes = read_runs_prefix(
dev,
&bitmap_runs,
cluster_size,
total_clusters.div_ceil(8) as usize,
)?;
let bitmap = format::BitmapAlloc {
bytes: bitmap_bytes,
total: total_clusters,
next_hint: 0,
};
let (logfile_lcn, logfile_clusters) = self.first_data_run(dev, 2);
let (attrdef_lcn, attrdef_clusters) = self.first_data_run(dev, 4);
let (upcase_lcn, upcase_clusters) = self.first_data_run(dev, 10);
let layout = LayoutResult {
cluster_size: cluster_size as u32,
bytes_per_sector,
sectors_per_cluster,
total_clusters,
mft_record_size,
index_record_size,
mft_extents,
mft_records,
mftmirr_lcn: self.boot.mft_mirr_lcn,
bitmap_lcn,
bitmap_clusters,
bitmap,
mft_bitmap_lcn,
mft_bitmap_clusters,
volume_serial: self.boot.volume_serial,
upcase_lcn,
upcase_clusters,
logfile_lcn,
logfile_clusters,
attrdef_lcn,
attrdef_clusters,
};
let next_user_record = (FIRST_USER_RECORD..mft_records)
.find(|&r| {
let i = (r / 8) as usize;
let m = 1u8 << ((r % 8) as u8);
i >= mft_bitmap.len() || mft_bitmap[i] & m == 0
})
.unwrap_or(mft_records);
let mut state = WriterState::new(layout);
state.mft_bitmap = mft_bitmap;
state.next_user_record = next_user_record;
state.dirty = false;
Ok(state)
}
pub(super) fn ensure_writer(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
if self.writer.is_none() {
let state = self.reconstruct_writer_state(dev)?;
self.writer = Some(state);
}
Ok(())
}
fn first_data_run(&mut self, dev: &mut dyn BlockDevice, rec_no: u64) -> (u64, u64) {
self.load_record_set(dev, rec_no)
.ok()
.and_then(|s| {
extract_non_resident_runs(&s[0].1, TYPE_DATA, "").and_then(|r| r.first().copied())
})
.unwrap_or((0, 0))
}
pub fn flush(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
if self.writer.is_none() {
return Ok(());
}
let pending = self
.writer
.as_mut()
.expect("writer present")
.dir_batch
.drain_all();
for (dir_rec, entries) in pending {
self.serialize_dir(dev, dir_rec, &entries)?;
}
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 region = w.layout.mft_bitmap_clusters * cluster_size;
if (w.mft_bitmap.len() as u64) < region {
let pad = vec![0u8; (region - w.mft_bitmap.len() as u64) as usize];
dev.write_at(off + w.mft_bitmap.len() as u64, &pad)?;
}
}
{
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 mirr_off = w.layout.mftmirr_lcn * cluster_size;
let mft_start = w.layout.mft_extents[0].0 * cluster_size;
let mut buf = vec![0u8; 4 * rec_size];
dev.read_at(mft_start, &mut buf)?;
dev.write_at(mirr_off, &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 file_ref = pack_mft_ref(file_rec, 1);
let entry = build_index_entry(file_ref, file_name_value, 0, None);
let writer = self.writer.as_mut().expect("writer present");
writer.dirty = true;
let victim = writer.dir_batch.stage(dir_rec, entry);
if let Some((victim_rec, entries)) = victim {
self.serialize_dir(dev, victim_rec, &entries)?;
}
Ok(())
}
pub(super) fn serialize_dir(
&mut self,
dev: &mut dyn BlockDevice,
dir_rec: u64,
entries: &[Vec<u8>],
) -> Result<()> {
if entries.is_empty() {
return Ok(());
}
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 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".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, entries);
}
match format::insert_entries_into_index_root(¤t, entries, 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, entries)
}
Err(e) => Err(e),
}
}
fn insert_into_allocation_block(
&mut self,
dev: &mut dyn BlockDevice,
dir_rec: u64,
new_entries: &[Vec<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.extend(new_entries.iter().cloned());
existing_entries.sort_by_key(|e| format::entry_sort_key(e));
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_entries: &[Vec<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.extend(new_entries.iter().cloned());
existing_entries.sort_by_key(|e| format::entry_sort_key(e));
let (root_vcn, blocks) =
build_index_btree(&existing_entries, index_block_size as usize, sector_size)?;
let nblocks = blocks.len() as u64;
let total_clusters = nblocks * clusters_per_block;
let alloc_lcn = writer.alloc_clusters(total_clusters)?;
let block_span = (clusters_per_block * cluster_size) as usize;
for (vcn, block) in &blocks {
let off = (alloc_lcn + vcn * clusters_per_block) * cluster_size;
dev.write_at(off, block)?;
if block.len() < block_span {
let pad = vec![0u8; block_span - block.len()];
dev.write_at(off + block.len() as u64, &pad)?;
}
}
let new_root = build_large_index_root(root_vcn);
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, total_clusters);
let alloc_size = total_clusters * cluster_size;
let alloc_attr = build_non_resident_attr(
TYPE_INDEX_ALLOCATION,
&i30_name,
&runs,
0,
total_clusters - 1,
alloc_size,
alloc_size,
alloc_size,
0,
0,
);
let bm_value = build_index_bitmap(nblocks);
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 read_runs_prefix(
dev: &mut dyn BlockDevice,
runs: &[(u64, u64)],
cluster_size: u64,
want_bytes: usize,
) -> Result<Vec<u8>> {
let mut out: Vec<u8> = Vec::with_capacity(want_bytes);
for &(lcn, clusters) in runs {
if out.len() >= want_bytes {
break;
}
let span = (clusters * cluster_size) as usize;
let mut buf = vec![0u8; span];
dev.read_at(lcn * cluster_size, &mut buf)?;
out.extend_from_slice(&buf);
}
out.resize(want_bytes, 0);
Ok(out)
}
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
}
type NonResidentAttrInfo = (u32, String, Vec<(u64, u64)>);
fn for_each_non_resident_attr(rec: &[u8]) -> Vec<NonResidentAttrInfo> {
let mut out: Vec<NonResidentAttrInfo> = Vec::new();
let Ok(hdr) = mft::RecordHeader::parse(rec) else {
return out;
};
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 Ok(tc_b) = rec[cursor..cursor + 4].try_into() else {
break;
};
let tc = u32::from_le_bytes(tc_b);
if tc == 0xFFFF_FFFF {
break;
}
let Ok(len_b) = rec[cursor + 4..cursor + 8].try_into() else {
break;
};
let len = u32::from_le_bytes(len_b) as usize;
if len == 0 || cursor + len > bytes_in_use {
break;
}
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().unwrap_or([0; 2])) 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 non_resident {
let runs_off = u16::from_le_bytes(
rec[cursor + 0x20..cursor + 0x22]
.try_into()
.unwrap_or([0; 2]),
) as usize;
let runs_bytes = &rec[cursor + runs_off..cursor + len];
if let Ok(extents) = super::run_list::decode(runs_bytes) {
let runs: Vec<(u64, u64)> = extents
.into_iter()
.filter_map(|e| e.lcn.map(|lcn| (lcn, e.length)))
.collect();
if !runs.is_empty() {
out.push((tc, attr_name, runs));
}
}
}
cursor += len;
}
out
}
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_indx_block_full(
block_size: usize,
sector_size: usize,
vcn: u64,
entries: &[Vec<u8>],
terminator_child: Option<u64>,
) -> 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: Vec<u8> = match terminator_child {
Some(child) => {
let mut e = vec![0u8; 24];
e[8..10].copy_from_slice(&24u16.to_le_bytes());
e[12..16].copy_from_slice(&0x03u32.to_le_bytes()); e[16..24].copy_from_slice(&child.to_le_bytes());
e
}
None => {
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: INDX node entries overflow the block".into(),
));
}
let bytes_in_use = (entries_start - 0x18) as u32 + entries_total as u32;
let bytes_allocated = (block_size - 0x18) as u32;
buf[0x24] = if terminator_child.is_some() { 0x01 } else { 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());
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);
mft::install_fixup(&mut buf, sector_size, 1);
Ok(buf)
}
#[derive(Clone)]
struct BtreeItem {
raw: Vec<u8>,
left_child: Option<u64>,
}
fn btree_item_entry(it: &BtreeItem) -> Vec<u8> {
match it.left_child {
None => it.raw.clone(),
Some(child) => {
let file_ref = u64::from_le_bytes(it.raw[0..8].try_into().unwrap());
let key_len = u16::from_le_bytes(it.raw[10..12].try_into().unwrap()) as usize;
build_index_entry(file_ref, &it.raw[16..16 + key_len], 0, Some(child))
}
}
}
type IndxBlock = (u64, Vec<u8>);
fn build_index_btree(
entries: &[Vec<u8>],
block_size: usize,
sector_size: usize,
) -> Result<(u64, Vec<IndxBlock>)> {
let sectors = block_size / sector_size;
let usa_end = 0x28 + 2 * (sectors + 1);
let entries_start = (usa_end + 7) & !7;
let mut out: Vec<(u64, Vec<u8>)> = Vec::new();
let mut next_vcn = 0u64;
let mut items: Vec<BtreeItem> = entries
.iter()
.map(|e| BtreeItem {
raw: e.clone(),
left_child: None,
})
.collect();
let mut rightmost: Option<u64> = None;
loop {
let internal = items.first().is_some_and(|i| i.left_child.is_some());
let term_len = if internal { 24 } else { 16 };
let usable = block_size - entries_start - term_len;
let build_node = |vcn: u64, group: &[BtreeItem], term_child: Option<u64>| {
let encoded: Vec<Vec<u8>> = group.iter().map(btree_item_entry).collect();
build_indx_block_full(block_size, sector_size, vcn, &encoded, term_child)
};
let mut parent: Vec<BtreeItem> = Vec::new();
let mut cur: Vec<BtreeItem> = Vec::new();
let mut cur_bytes = 0usize;
let mut i = 0;
while i < items.len() {
let elen = btree_item_entry(&items[i]).len();
if elen > usable {
return Err(crate::Error::Unsupported(
"ntfs: a single directory entry is too large for an INDX block".into(),
));
}
if !cur.is_empty() && cur_bytes + elen > usable {
let vcn = next_vcn;
next_vcn += 1;
out.push((vcn, build_node(vcn, &cur, items[i].left_child)?));
parent.push(BtreeItem {
raw: items[i].raw.clone(),
left_child: Some(vcn),
});
cur.clear();
cur_bytes = 0;
i += 1; } else {
cur_bytes += elen;
cur.push(items[i].clone());
i += 1;
}
}
let vcn = next_vcn;
next_vcn += 1;
out.push((vcn, build_node(vcn, &cur, rightmost)?));
if parent.is_empty() {
return Ok((vcn, out));
}
items = parent;
rightmost = Some(vcn);
}
}
fn build_index_bitmap(n: u64) -> Vec<u8> {
let nbytes = ((n as usize).div_ceil(8)).max(1);
let nbytes = (nbytes + 7) & !7;
let mut v = vec![0u8; nbytes];
for i in 0..n as usize {
v[i / 8] |= 1u8 << (i % 8);
}
v
}
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()
}