use std::io::{Read, Seek, SeekFrom, Write};
use crate::Result;
use crate::block::BlockDevice;
use crate::fs::FileHandle;
use super::attribute::{AttributeIter, AttributeKind, FileName, TYPE_DATA, TYPE_FILE_NAME};
use super::format::{
self, build_file_name_value, build_non_resident_attr, build_resident_attr, encode_run_list,
};
use super::index;
use super::mft;
use super::run_list::Extent;
pub struct NtfsFileHandle<'a> {
fs: &'a mut super::Ntfs,
dev: &'a mut dyn BlockDevice,
rec_no: u64,
rec_size: usize,
sector_size: usize,
cluster_size: u64,
pos: u64,
len: u64,
resident: Option<Vec<u8>>,
runs: Vec<Extent>,
file_name_value: Vec<u8>,
parent_ref: u64,
si_value: Vec<u8>,
dirty: bool,
}
impl<'a> NtfsFileHandle<'a> {
fn vcn_to_disk(&self, vcn: u64) -> Option<u64> {
let cs = self.cluster_size;
let mut walked: u64 = 0;
for ext in &self.runs {
if vcn < walked + ext.length {
let local = vcn - walked;
return ext.lcn.map(|lcn| (lcn + local) * cs);
}
walked += ext.length;
}
None
}
fn allocated_bytes(&self) -> u64 {
self.runs.iter().map(|r| r.length).sum::<u64>() * self.cluster_size
}
fn read_internal(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
if self.pos >= self.len {
return Ok(0);
}
let avail = self.len - self.pos;
let want = (out.len() as u64).min(avail) as usize;
if want == 0 {
return Ok(0);
}
if let Some(ref bytes) = self.resident {
let s = self.pos as usize;
let e = s + want;
out[..want].copy_from_slice(&bytes[s..e]);
self.pos += want as u64;
return Ok(want);
}
let cs = self.cluster_size;
let vcn = self.pos / cs;
let off_in_cluster = (self.pos % cs) as usize;
let in_cluster = (cs as usize - off_in_cluster).min(want);
let disk = match self.vcn_to_disk(vcn) {
Some(p) => p,
None => {
out[..in_cluster].fill(0);
self.pos += in_cluster as u64;
return Ok(in_cluster);
}
};
let phys = disk + off_in_cluster as u64;
self.dev
.read_at(phys, &mut out[..in_cluster])
.map_err(std::io::Error::other)?;
self.pos += in_cluster as u64;
Ok(in_cluster)
}
fn grow_runs(&mut self, extra_clusters: u64) -> Result<()> {
if extra_clusters == 0 {
return Ok(());
}
let w = self.fs.writer.as_mut().ok_or_else(|| {
crate::Error::Unsupported(
"ntfs: open_file_rw requires the writer (format the volume first)".into(),
)
})?;
let start = w.alloc_clusters(extra_clusters)?;
let zero = vec![0u8; (extra_clusters * self.cluster_size) as usize];
self.dev.write_at(start * self.cluster_size, &zero)?;
if let Some(last) = self.runs.last_mut()
&& let Some(last_lcn) = last.lcn
&& last_lcn + last.length == start
{
last.length += extra_clusters;
self.dirty = true;
return Ok(());
}
self.runs.push(Extent {
lcn: Some(start),
length: extra_clusters,
});
self.dirty = true;
Ok(())
}
fn shrink_runs(&mut self, keep_clusters: u64) -> Result<()> {
let total: u64 = self.runs.iter().map(|r| r.length).sum();
if keep_clusters >= total {
return Ok(());
}
let w = self.fs.writer.as_mut().ok_or_else(|| {
crate::Error::Unsupported(
"ntfs: open_file_rw requires the writer (format the volume first)".into(),
)
})?;
let mut remaining = total - keep_clusters;
while remaining > 0 {
let last = self.runs.last_mut().expect("non-empty runs");
let take = remaining.min(last.length);
if let Some(lcn) = last.lcn {
let free_start = lcn + (last.length - take);
for c in free_start..lcn + last.length {
w.layout.bitmap.clear(c);
if w.layout.bitmap.next_hint > c {
w.layout.bitmap.next_hint = c;
}
}
}
last.length -= take;
remaining -= take;
if last.length == 0 {
self.runs.pop();
}
}
w.dirty = true;
self.dirty = true;
Ok(())
}
fn promote_to_non_resident(&mut self) -> Result<()> {
let bytes = match self.resident.take() {
Some(b) => b,
None => return Ok(()),
};
let cs = self.cluster_size;
let need = bytes.len() as u64;
let clusters = need.div_ceil(cs);
if clusters == 0 {
return Ok(());
}
let w = self
.fs
.writer
.as_mut()
.ok_or_else(|| crate::Error::Unsupported("ntfs: writer not initialised".into()))?;
let lcn = w.alloc_clusters(clusters)?;
let mut padded = bytes;
let pad_len = (clusters * cs) as usize - padded.len();
padded.extend(std::iter::repeat_n(0u8, pad_len));
self.dev.write_at(lcn * cs, &padded)?;
self.runs.push(Extent {
lcn: Some(lcn),
length: clusters,
});
self.dirty = true;
Ok(())
}
fn ensure_capacity(&mut self, new_len: u64) -> Result<()> {
let resident_budget = self.rec_size.saturating_sub(232) as u64;
if let Some(b) = self.resident.as_mut() {
if new_len <= resident_budget {
if (b.len() as u64) < new_len {
b.resize(new_len as usize, 0);
self.dirty = true;
}
return Ok(());
}
self.promote_to_non_resident()?;
}
let cs = self.cluster_size;
let have = self.allocated_bytes();
if new_len <= have {
return Ok(());
}
let need_total_clusters = new_len.div_ceil(cs);
let cur_clusters: u64 = self.runs.iter().map(|r| r.length).sum();
let extra = need_total_clusters - cur_clusters;
self.grow_runs(extra)
}
fn write_internal(&mut self, buf: &[u8]) -> Result<usize> {
if buf.is_empty() {
return Ok(0);
}
if self.pos > self.len {
self.ensure_capacity(self.pos)?;
self.zero_range(self.len, self.pos)?;
self.len = self.pos;
self.dirty = true;
}
let new_end = self.pos + buf.len() as u64;
self.ensure_capacity(new_end)?;
if let Some(ref mut bytes) = self.resident {
let s = self.pos as usize;
let e = s + buf.len();
if bytes.len() < e {
bytes.resize(e, 0);
}
bytes[s..e].copy_from_slice(buf);
} else {
let cs = self.cluster_size;
let mut written = 0usize;
while written < buf.len() {
let p = self.pos + written as u64;
let vcn = p / cs;
let off = (p % cs) as usize;
let in_cluster = ((cs as usize) - off).min(buf.len() - written);
let disk = self.vcn_to_disk(vcn).ok_or_else(|| {
crate::Error::InvalidImage(format!("ntfs: write past run list at VCN {vcn}"))
})?;
self.dev
.write_at(disk + off as u64, &buf[written..written + in_cluster])?;
written += in_cluster;
}
}
self.pos += buf.len() as u64;
if self.pos > self.len {
self.len = self.pos;
}
self.dirty = true;
Ok(buf.len())
}
fn zero_range(&mut self, from: u64, to: u64) -> Result<()> {
if from >= to {
return Ok(());
}
if let Some(ref mut bytes) = self.resident {
let s = from as usize;
let e = to as usize;
if bytes.len() < e {
bytes.resize(e, 0);
}
for b in &mut bytes[s..e] {
*b = 0;
}
return Ok(());
}
let cs = self.cluster_size;
let mut p = from;
let zero = vec![0u8; cs as usize];
while p < to {
let vcn = p / cs;
let off = (p % cs) as usize;
let in_cluster = ((cs - off as u64).min(to - p)) as usize;
let disk = self.vcn_to_disk(vcn).ok_or_else(|| {
crate::Error::InvalidImage(format!("ntfs: zero_range past run list at VCN {vcn}"))
})?;
self.dev.write_at(disk + off as u64, &zero[..in_cluster])?;
p += in_cluster as u64;
}
Ok(())
}
fn flush_record(&mut self) -> Result<()> {
if !self.dirty {
return Ok(());
}
let (filetime, flags, namespace, name) = decode_file_name_meta(&self.file_name_value)?;
let allocated = if self.resident.is_some() {
(self.len + self.cluster_size - 1) & !(self.cluster_size - 1)
} else {
self.runs.iter().map(|r| r.length).sum::<u64>() * self.cluster_size
};
let fn_value = build_file_name_value(
self.parent_ref,
&name,
flags,
self.len,
allocated,
filetime,
namespace,
);
self.file_name_value = fn_value.clone();
let data_attr = if let Some(ref bytes) = self.resident {
let mut payload = bytes.clone();
payload.truncate(self.len as usize);
build_resident_attr(TYPE_DATA, &[], &payload, 0, 0)
} else {
let extents: Vec<(u64, u64)> = self
.runs
.iter()
.filter_map(|r| r.lcn.map(|l| (l, r.length)))
.collect();
let total_clusters: u64 = self.runs.iter().map(|r| r.length).sum();
let alloc_bytes = total_clusters * self.cluster_size;
let (runs_bytes, last_vcn) = if extents.is_empty() {
(vec![0u8], 0u64)
} else {
(encode_run_list(&extents), total_clusters - 1)
};
build_non_resident_attr(
TYPE_DATA,
&[],
&runs_bytes,
0,
last_vcn,
alloc_bytes,
self.len,
self.len,
0,
0,
)
};
let si = build_resident_attr(
super::attribute::TYPE_STANDARD_INFORMATION,
&[],
&self.si_value,
0,
0,
);
let fn_attr = build_resident_attr(TYPE_FILE_NAME, &[], &fn_value, 0, 1);
let mut rec_buf = vec![0u8; self.rec_size];
let existing_flags = self.read_existing_flags()?;
format::emit_record(
&mut rec_buf,
self.rec_size,
self.rec_no,
existing_flags,
&[si, fn_attr, data_attr],
self.sector_size,
1,
)?;
let mft_off = {
let w =
self.fs.writer.as_ref().ok_or_else(|| {
crate::Error::Unsupported("ntfs: writer not initialised".into())
})?;
w.mft_offset(self.rec_no)?
};
let mut old_rec = vec![0u8; self.rec_size];
self.dev.read_at(mft_off, &mut old_rec)?;
let mut txn: Vec<super::logfile::RedoEntry> = Vec::new();
txn.push(super::logfile::RedoEntry {
target_offset: mft_off,
redo_bytes: rec_buf.clone(),
undo_bytes: old_rec,
});
txn.extend(self.build_index_entry_redo()?);
self.commit_txn(&txn)?;
self.dirty = false;
Ok(())
}
fn build_index_entry_redo(&mut self) -> Result<Vec<super::logfile::RedoEntry>> {
let parent_rec_no = self.parent_ref & 0x0000_FFFF_FFFF_FFFF;
let off = self
.fs
.writer
.as_ref()
.ok_or_else(|| crate::Error::Unsupported("ntfs: writer not initialised".into()))?
.mft_offset(parent_rec_no)?;
let mut rec = vec![0u8; self.rec_size];
self.dev.read_at(off, &mut rec)?;
let old_rec = rec.clone();
if mft::apply_fixup(&mut rec, self.sector_size).is_err() {
return Ok(Vec::new());
}
let hdr = mft::RecordHeader::parse(&rec)?;
let mut root_range: Option<(usize, usize)> = None;
let mut alloc_runs: Option<Vec<Extent>> = None;
for attr in
AttributeIter::new(&rec, hdr.first_attribute_offset as usize).map_while(Result::ok)
{
if attr.name != "$I30" {
continue;
}
match (attr.type_code, attr.kind) {
(super::attribute::TYPE_INDEX_ROOT, AttributeKind::Resident { value, .. }) => {
let value_off = u16::from_le_bytes(
rec[attr.offset + 0x14..attr.offset + 0x16]
.try_into()
.unwrap(),
) as usize;
let start = attr.offset + value_off;
root_range = Some((start, start + value.len()));
}
(
super::attribute::TYPE_INDEX_ALLOCATION,
AttributeKind::NonResident { runs, .. },
) => {
alloc_runs = Some(runs);
}
_ => {}
}
}
let Some((root_v_start, root_v_end)) = root_range else {
return Ok(Vec::new());
};
let mut out = Vec::new();
let Ok(root_hdr) = index::IndexRootHeader::parse(&rec[root_v_start..root_v_end]) else {
return Ok(Vec::new());
};
let entries_start = root_hdr.header_offset + root_hdr.first_entry_offset as usize;
let entries_len =
(root_hdr.bytes_in_use as usize).saturating_sub(root_hdr.first_entry_offset as usize);
let children = index::walk_index_node(
&rec[root_v_start..root_v_end],
entries_start,
entries_len,
|_| {},
)
.unwrap_or_default();
let large_index = root_hdr.has_index_allocation();
let block_size = match root_hdr.index_block_size {
0 => self
.fs
.writer
.as_ref()
.map(|w| w.layout.index_record_size as usize)
.unwrap_or(4096),
n => n as usize,
};
if patch_entries_for_record(
&mut rec[root_v_start..root_v_end],
entries_start,
self.rec_no,
self.len,
self.cluster_size,
) {
mft::install_fixup(&mut rec, self.sector_size, 1);
if rec != old_rec {
out.push(super::logfile::RedoEntry {
target_offset: off,
redo_bytes: rec,
undo_bytes: old_rec,
});
}
}
if !large_index {
return Ok(out);
}
let Some(runs) = alloc_runs else {
return Ok(out);
};
let vcn_unit = index::vcn_unit_bytes(self.cluster_size, block_size as u64);
let mut block = vec![0u8; block_size];
let mut stack = children;
let mut visited = std::collections::HashSet::<u64>::new();
while let Some(vcn) = stack.pop() {
if !visited.insert(vcn) {
break;
}
let byte_off = vcn * vcn_unit;
if super::writer::read_alloc_bytes(
self.dev,
&runs,
self.cluster_size,
byte_off,
&mut block,
)
.is_err()
{
break;
}
let old_block = block.clone();
if mft::apply_fixup(&mut block, self.sector_size).is_err() {
break;
}
let Ok(blk_hdr) = index::IndexBlockHeader::parse(&block) else {
break;
};
let entries_start = blk_hdr.entries_start();
let entries_len = blk_hdr.entries_byte_len();
if let Ok(kids) = index::walk_index_node(&block, entries_start, entries_len, |_| {}) {
stack.extend(kids);
}
if patch_entries_for_record(
&mut block,
entries_start,
self.rec_no,
self.len,
self.cluster_size,
) {
mft::install_fixup(&mut block, self.sector_size, 1);
if block != old_block {
match super::writer::alloc_byte_to_disk(&runs, self.cluster_size, byte_off) {
Some((disk, left)) if left >= block_size as u64 => {
out.push(super::logfile::RedoEntry {
target_offset: disk,
redo_bytes: block.clone(),
undo_bytes: old_block,
});
}
Some(_) => super::writer::write_alloc_bytes(
self.dev,
&runs,
self.cluster_size,
byte_off,
&block,
)?,
None => break,
}
}
}
}
Ok(out)
}
fn commit_txn(&mut self, entries: &[super::logfile::RedoEntry]) -> Result<()> {
commit_txn(self.fs, self.dev, entries)
}
}
pub(super) fn commit_txn(
fs: &mut super::Ntfs,
dev: &mut dyn BlockDevice,
entries: &[super::logfile::RedoEntry],
) -> Result<()> {
{
if entries.is_empty() {
return Ok(());
}
let (logfile_offset, log_size) = locate_logfile(fs, dev)?;
if log_size < 4 * super::logfile::LOG_PAGE_SIZE as u64 {
for e in entries {
dev.write_at(e.target_offset, &e.redo_bytes)?;
}
return Ok(());
}
let prev_view = super::logfile::read_current_restart(dev, logfile_offset)?;
let prev_lsn = prev_view.map(|(v, _)| v.current_lsn).unwrap_or(0);
let mut next_lsn = prev_lsn + 1;
let rcrd_region_start = logfile_offset + 2 * super::logfile::LOG_PAGE_SIZE as u64;
let rcrd_region_size = log_size - 2 * super::logfile::LOG_PAGE_SIZE as u64;
let mut page_idx: u64 = 0;
let mut chunk_start = 0usize;
let mut last_lsn = next_lsn;
while chunk_start < entries.len() {
let mut chunk_end = chunk_start + 1;
while chunk_end <= entries.len() {
match super::logfile::build_record_page(&entries[chunk_start..chunk_end], next_lsn)
{
Ok(_) => chunk_end += 1,
Err(_) => break,
}
}
if chunk_end == chunk_start + 1 {
for e in entries {
dev.write_at(e.target_offset, &e.redo_bytes)?;
}
return Ok(());
}
chunk_end -= 1;
let (page, ll) =
super::logfile::build_record_page(&entries[chunk_start..chunk_end], next_lsn)?;
let off = rcrd_region_start
+ (page_idx * super::logfile::LOG_PAGE_SIZE as u64) % rcrd_region_size;
dev.write_at(off, &page)?;
last_lsn = ll;
next_lsn = ll + 1;
page_idx += 1;
chunk_start = chunk_end;
}
let page_size = super::logfile::LOG_PAGE_SIZE as u64;
let tail_delta = page_idx * page_size;
if tail_delta < rcrd_region_size {
dev.zero_range(rcrd_region_start + tail_delta, page_size)?;
}
let dirty_page = super::logfile::build_restart_page(last_lsn, log_size, false);
dev.write_at(logfile_offset, &dirty_page)?;
dev.write_at(
logfile_offset + super::logfile::LOG_PAGE_SIZE as u64,
&dirty_page,
)?;
for e in entries {
dev.write_at(e.target_offset, &e.redo_bytes)?;
}
let clean_page = super::logfile::build_restart_page(last_lsn, log_size, true);
dev.write_at(logfile_offset, &clean_page)?;
dev.write_at(
logfile_offset + super::logfile::LOG_PAGE_SIZE as u64,
&clean_page,
)?;
Ok(())
}
}
impl<'a> NtfsFileHandle<'a> {
fn read_existing_flags(&mut self) -> Result<u16> {
let off = self
.fs
.writer
.as_ref()
.ok_or_else(|| crate::Error::Unsupported("ntfs: writer not initialised".into()))?
.mft_offset(self.rec_no)?;
let mut rec = vec![0u8; self.rec_size];
self.dev.read_at(off, &mut rec)?;
if mft::apply_fixup(&mut rec, self.sector_size).is_err() {
return Ok(mft::RecordHeader::FLAG_IN_USE);
}
let hdr = mft::RecordHeader::parse(&rec)?;
Ok(hdr.flags)
}
}
fn locate_logfile(fs: &mut super::Ntfs, dev: &mut dyn BlockDevice) -> Result<(u64, u64)> {
let (rec_size, sector_size, cluster_size) = {
let w = fs.writer.as_ref().expect("writer present");
(
w.layout.mft_record_size as usize,
mft::NTFS_BLOCK_SIZE,
w.cluster_size,
)
};
let mft_off = fs
.writer
.as_ref()
.expect("writer present")
.mft_offset(super::format::REC_LOGFILE)?;
let mut rec = vec![0u8; rec_size];
dev.read_at(mft_off, &mut rec)?;
mft::apply_fixup(&mut rec, sector_size)?;
let hdr = mft::RecordHeader::parse(&rec)?;
for attr_res in AttributeIter::new(&rec, hdr.first_attribute_offset as usize) {
let attr = attr_res?;
if attr.type_code == TYPE_DATA
&& attr.name.is_empty()
&& let AttributeKind::NonResident {
runs, real_size, ..
} = attr.kind
&& let Some(first) = runs.first().and_then(|r| r.lcn)
{
return Ok((first * cluster_size, real_size));
}
}
Ok((0, 0))
}
fn replay_and_clean(
dev: &mut dyn BlockDevice,
logfile_offset: u64,
log_size: u64,
entries: &[super::logfile::RedoEntry],
_last_seq: u64,
) -> Result<()> {
for e in entries {
dev.write_at(e.target_offset, &e.redo_bytes)?;
}
let rstr_region = 2 * super::logfile::LOG_PAGE_SIZE as u64;
if log_size > rstr_region {
dev.zero_range(logfile_offset + rstr_region, log_size - rstr_region)?;
}
let page = super::logfile::build_restart_page(0, log_size, true);
dev.write_at(logfile_offset, &page)?;
dev.write_at(logfile_offset + super::logfile::LOG_PAGE_SIZE as u64, &page)?;
Ok(())
}
fn patch_entries_for_record(
buf: &mut [u8],
start_off: usize,
rec_no: u64,
new_len: u64,
cluster_size: u64,
) -> bool {
let mut cursor = start_off;
let mut changed = false;
while cursor + 16 <= buf.len() {
let entry_len =
u16::from_le_bytes(buf[cursor + 8..cursor + 10].try_into().unwrap()) as usize;
let flags = u32::from_le_bytes(buf[cursor + 12..cursor + 16].try_into().unwrap());
if entry_len < 16 || cursor + entry_len > buf.len() {
break;
}
let is_last = flags & 0x02 != 0;
let key_len =
u16::from_le_bytes(buf[cursor + 10..cursor + 12].try_into().unwrap()) as usize;
let key_off = cursor + 16;
if !is_last && key_len >= 66 && entry_len >= 16 + key_len && key_off + 56 <= buf.len() {
let file_ref = u64::from_le_bytes(buf[cursor..cursor + 8].try_into().unwrap());
if (file_ref & 0x0000_FFFF_FFFF_FFFF) == rec_no {
let allocated = new_len.div_ceil(cluster_size.max(1)) * cluster_size.max(1);
buf[key_off + 40..key_off + 48].copy_from_slice(&allocated.to_le_bytes());
buf[key_off + 48..key_off + 56].copy_from_slice(&new_len.to_le_bytes());
changed = true;
}
}
if is_last {
break;
}
cursor += entry_len;
}
changed
}
fn decode_file_name_meta(v: &[u8]) -> Result<(u64, u32, u8, String)> {
let fname = FileName::parse(v)?;
Ok((
fname.modified_time,
fname.flags,
fname.namespace,
fname.name,
))
}
impl<'a> Read for NtfsFileHandle<'a> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.read_internal(buf)
}
}
impl<'a> Write for NtfsFileHandle<'a> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.write_internal(buf).map_err(std::io::Error::other)
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<'a> Seek for NtfsFileHandle<'a> {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
let new = match pos {
SeekFrom::Start(n) => n as i128,
SeekFrom::Current(d) => self.pos as i128 + d as i128,
SeekFrom::End(d) => self.len as i128 + d as i128,
};
if new < 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"ntfs: seek to negative offset",
));
}
self.pos = new as u64;
Ok(self.pos)
}
}
impl<'a> FileHandle for NtfsFileHandle<'a> {
fn len(&self) -> u64 {
self.len
}
fn set_len(&mut self, new_len: u64) -> Result<()> {
if new_len == self.len {
return Ok(());
}
if new_len > self.len {
self.ensure_capacity(new_len)?;
let old_len = self.len;
self.zero_range(old_len, new_len)?;
self.len = new_len;
} else {
if let Some(ref mut bytes) = self.resident {
bytes.truncate(new_len as usize);
} else {
let cs = self.cluster_size;
let keep_clusters = new_len.div_ceil(cs);
self.shrink_runs(keep_clusters)?;
}
self.len = new_len;
if self.pos > self.len {
self.pos = self.len;
}
}
self.dirty = true;
Ok(())
}
fn sync(&mut self) -> Result<()> {
self.flush_record()?;
self.fs.flush(self.dev)?;
Ok(())
}
}
impl<'a> Drop for NtfsFileHandle<'a> {
fn drop(&mut self) {
let _ = self.flush_record();
}
}
impl super::Ntfs {
pub(super) fn open_rw<'a>(
&'a mut self,
dev: &'a mut dyn BlockDevice,
path: &str,
flags: crate::fs::OpenFlags,
meta: Option<crate::fs::FileMeta>,
) -> Result<Box<dyn FileHandle + 'a>> {
self.ensure_writer(dev)?;
Self::ensure_clean_log(self, dev)?;
let rec_no = self.lookup_path(dev, path).ok();
let rec_no = match rec_no {
Some(r) => r,
None => {
if !flags.create {
return Err(crate::Error::InvalidArgument(format!(
"ntfs: no such file: {path:?}"
)));
}
let m = meta.ok_or_else(|| {
crate::Error::InvalidArgument(
"ntfs: open_file_rw create=true requires meta".into(),
)
})?;
self.create_file(dev, path, crate::fs::FileSource::Zero(0), m)?;
self.lookup_path(dev, path)?
}
};
let (rec_size, sector_size, cluster_size) = {
let w = self.writer.as_ref().expect("writer present");
(
w.layout.mft_record_size as usize,
mft::NTFS_BLOCK_SIZE,
w.cluster_size,
)
};
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 hdr = mft::RecordHeader::parse(&rec)?;
if hdr.is_directory() {
return Err(crate::Error::InvalidArgument(format!(
"ntfs: {path:?} is a directory"
)));
}
if !hdr.is_in_use() {
return Err(crate::Error::InvalidImage(format!(
"ntfs: record {rec_no} is not in use"
)));
}
let mut resident: Option<Vec<u8>> = None;
let mut runs: Vec<Extent> = Vec::new();
let mut real_size: u64 = 0;
let mut file_name_value: Option<Vec<u8>> = None;
let mut si_value: Option<Vec<u8>> = None;
let mut parent_ref: u64 = 0;
for attr_res in AttributeIter::new(&rec, hdr.first_attribute_offset as usize) {
let attr = attr_res?;
match attr.type_code {
super::attribute::TYPE_STANDARD_INFORMATION => {
if let AttributeKind::Resident { value, .. } = attr.kind {
si_value = Some(value.to_vec());
}
}
TYPE_FILE_NAME => {
if let AttributeKind::Resident { value, .. } = attr.kind {
let fname = FileName::parse(value)?;
let take = match fname.namespace {
FileName::NAMESPACE_DOS => file_name_value.is_none(),
_ => true,
};
if take {
file_name_value = Some(value.to_vec());
parent_ref = fname.parent_mft_ref;
}
}
}
TYPE_DATA if attr.name.is_empty() => match attr.kind {
AttributeKind::Resident { value, .. } => {
resident = Some(value.to_vec());
real_size = value.len() as u64;
}
AttributeKind::NonResident {
runs: rs,
real_size: r,
..
} => {
runs = rs;
real_size = r;
}
},
_ => {}
}
}
let file_name_value = file_name_value.ok_or_else(|| {
crate::Error::InvalidImage(format!("ntfs: record {rec_no} missing $FILE_NAME"))
})?;
let si_value = si_value.unwrap_or_default();
let mut handle = NtfsFileHandle {
fs: self,
dev,
rec_no,
rec_size,
sector_size,
cluster_size,
pos: 0,
len: real_size,
resident,
runs,
file_name_value,
parent_ref,
si_value,
dirty: false,
};
if flags.truncate && handle.len != 0 {
handle.set_len(0)?;
}
if flags.append {
handle.pos = handle.len;
}
Ok(Box::new(handle))
}
pub(super) fn ensure_clean_log(fs: &mut super::Ntfs, dev: &mut dyn BlockDevice) -> Result<()> {
let (logfile_offset, log_size) = locate_logfile(fs, dev)?;
if log_size == 0 {
return Ok(());
}
let mut head = [0u8; 8];
dev.read_at(logfile_offset, &mut head)?;
if head.iter().all(|&b| b == 0) {
return Ok(());
}
match super::logfile::read_current_restart(dev, logfile_offset)? {
Some((view, _which)) if view.is_clean() => Ok(()),
Some((view, _which)) => {
let entries = super::logfile::walk_records(dev, logfile_offset, log_size)?;
let seq = super::logfile::lsn_seq(view.current_lsn).max(1);
replay_and_clean(dev, logfile_offset, log_size, &entries, seq)
}
None => Err(crate::Error::Unsupported(
"ntfs: open_file_rw refuses to mutate a volume with a non-LFS-shaped $LogFile"
.into(),
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::MemoryBackend;
use crate::fs::ntfs::Ntfs;
use crate::fs::ntfs::format::FormatOpts;
use crate::fs::{FileMeta, FileSource, Filesystem, OpenFlags};
use std::path::Path;
fn fresh(size: u64) -> (MemoryBackend, Ntfs) {
let mut dev = MemoryBackend::new(size);
let opts = FormatOpts {
volume_label: "rw-test".into(),
..Default::default()
};
let ntfs = Ntfs::format(&mut dev, &opts).unwrap();
(dev, ntfs)
}
fn read_all(fs: &mut Ntfs, dev: &mut MemoryBackend, path: &str) -> Vec<u8> {
let mut r = fs.open_file_reader(dev, path).unwrap();
let mut buf = Vec::new();
r.read_to_end(&mut buf).unwrap();
buf
}
#[test]
fn open_file_rw_partial_write_round_trip() {
let (mut dev, mut fs) = fresh(8 * 1024 * 1024);
let payload = b"AAAAAAAAAAAAAAAAAAAA";
fs.create_file(
&mut dev,
"/x.bin",
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(payload.to_vec())),
len: payload.len() as u64,
},
FileMeta::default(),
)
.unwrap();
fs.flush(&mut dev).unwrap();
{
let mut h = Filesystem::open_file_rw(
&mut fs,
&mut dev,
Path::new("/x.bin"),
OpenFlags::default(),
None,
)
.unwrap();
assert_eq!(h.len(), 20);
h.seek(SeekFrom::Start(5)).unwrap();
h.write_all(b"ZZZZZ").unwrap();
h.sync().unwrap();
}
let bytes = read_all(&mut fs, &mut dev, "/x.bin");
let mut expected = payload.to_vec();
expected[5..10].copy_from_slice(b"ZZZZZ");
assert_eq!(bytes, expected);
}
#[test]
fn open_file_rw_extends_file() {
let (mut dev, mut fs) = fresh(8 * 1024 * 1024);
fs.create_file(
&mut dev,
"/g.txt",
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(b"hello".to_vec())),
len: 5,
},
FileMeta::default(),
)
.unwrap();
fs.flush(&mut dev).unwrap();
{
let mut h = Filesystem::open_file_rw(
&mut fs,
&mut dev,
Path::new("/g.txt"),
OpenFlags::default(),
None,
)
.unwrap();
h.seek(SeekFrom::End(0)).unwrap();
h.write_all(b", world!").unwrap();
h.sync().unwrap();
assert_eq!(h.len(), 13);
}
let bytes = read_all(&mut fs, &mut dev, "/g.txt");
assert_eq!(bytes, b"hello, world!");
}
#[test]
fn open_file_rw_set_len_grow_and_shrink() {
let (mut dev, mut fs) = fresh(16 * 1024 * 1024);
fs.create_file(
&mut dev,
"/s.bin",
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(b"ABCDEFGH".to_vec())),
len: 8,
},
FileMeta::default(),
)
.unwrap();
fs.flush(&mut dev).unwrap();
{
let mut h = Filesystem::open_file_rw(
&mut fs,
&mut dev,
Path::new("/s.bin"),
OpenFlags::default(),
None,
)
.unwrap();
h.set_len(20 * 1024).unwrap();
assert_eq!(h.len(), 20 * 1024);
h.sync().unwrap();
}
{
let bytes = read_all(&mut fs, &mut dev, "/s.bin");
assert_eq!(bytes.len(), 20 * 1024);
assert_eq!(&bytes[..8], b"ABCDEFGH");
assert!(bytes[8..].iter().all(|&b| b == 0));
}
{
let mut h = Filesystem::open_file_rw(
&mut fs,
&mut dev,
Path::new("/s.bin"),
OpenFlags::default(),
None,
)
.unwrap();
h.set_len(4).unwrap();
assert_eq!(h.len(), 4);
h.sync().unwrap();
}
let bytes = read_all(&mut fs, &mut dev, "/s.bin");
assert_eq!(bytes, b"ABCD");
}
#[test]
fn open_file_rw_append() {
let (mut dev, mut fs) = fresh(8 * 1024 * 1024);
fs.create_file(
&mut dev,
"/a.txt",
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(b"head ".to_vec())),
len: 5,
},
FileMeta::default(),
)
.unwrap();
fs.flush(&mut dev).unwrap();
{
let mut h = Filesystem::open_file_rw(
&mut fs,
&mut dev,
Path::new("/a.txt"),
OpenFlags {
create: false,
truncate: false,
append: true,
},
None,
)
.unwrap();
h.write_all(b"tail").unwrap();
h.sync().unwrap();
}
let bytes = read_all(&mut fs, &mut dev, "/a.txt");
assert_eq!(bytes, b"head tail");
}
#[test]
fn open_file_rw_create_new() {
let (mut dev, mut fs) = fresh(8 * 1024 * 1024);
{
let mut h = Filesystem::open_file_rw(
&mut fs,
&mut dev,
Path::new("/n.txt"),
OpenFlags {
create: true,
truncate: false,
append: false,
},
Some(FileMeta::default()),
)
.unwrap();
assert_eq!(h.len(), 0);
h.write_all(b"freshly created").unwrap();
h.sync().unwrap();
}
let bytes = read_all(&mut fs, &mut dev, "/n.txt");
assert_eq!(bytes, b"freshly created");
}
#[test]
fn open_file_rw_refused_when_log_unparseable() {
let (mut dev, mut fs) = fresh(8 * 1024 * 1024);
let (lcn, cs) = {
let w = fs.writer.as_ref().unwrap();
(w.layout.logfile_lcn, w.cluster_size)
};
let phys = lcn * cs;
dev.write_at(phys, b"JUNKJUNK\x01\x02\x03\x04").unwrap();
dev.write_at(phys + 4096, b"JUNKJUNK\x01\x02\x03\x04")
.unwrap();
let res = Filesystem::open_file_rw(
&mut fs,
&mut dev,
Path::new("/somefile"),
OpenFlags::default(),
None,
);
match res {
Err(crate::Error::Unsupported(msg)) => {
assert!(msg.contains("LogFile") || msg.contains("log"));
}
Err(other) => panic!("expected Unsupported on dirty log, got {other:?}"),
Ok(_) => panic!("expected Unsupported on dirty log, got Ok"),
}
}
#[test]
fn open_file_rw_replay_restores_metadata() {
let (mut dev, mut fs) = fresh(16 * 1024 * 1024);
let payload = b"abcdefghij".to_vec();
fs.create_file(
&mut dev,
"/jnl.bin",
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(payload.clone())),
len: payload.len() as u64,
},
FileMeta::default(),
)
.unwrap();
fs.flush(&mut dev).unwrap();
let rec_no = fs.lookup_path(&mut dev, "/jnl.bin").unwrap();
let mft_off = fs.writer.as_ref().unwrap().mft_offset(rec_no).unwrap();
let rec_size = fs.writer.as_ref().unwrap().layout.mft_record_size as usize;
{
let mut h = Filesystem::open_file_rw(
&mut fs,
&mut dev,
Path::new("/jnl.bin"),
OpenFlags::default(),
None,
)
.unwrap();
h.write_all(b"REPLAY MATERIAL XX").unwrap();
h.sync().unwrap();
}
let zero_rec = vec![0u8; rec_size];
dev.write_at(mft_off, &zero_rec).unwrap();
let (lcn, cs) = {
let w = fs.writer.as_ref().unwrap();
(w.layout.logfile_lcn, w.cluster_size)
};
let logfile_offset = lcn * cs;
let (view, _) = super::super::logfile::read_current_restart(&mut dev, logfile_offset)
.unwrap()
.expect("restart parses");
let dirty =
super::super::logfile::build_restart_page(view.current_lsn, view.file_size, false);
dev.write_at(logfile_offset, &dirty).unwrap();
dev.write_at(
logfile_offset + super::super::logfile::LOG_PAGE_SIZE as u64,
&dirty,
)
.unwrap();
{
let _h = Filesystem::open_file_rw(
&mut fs,
&mut dev,
Path::new("/jnl.bin"),
OpenFlags::default(),
None,
)
.unwrap();
}
let bytes = read_all(&mut fs, &mut dev, "/jnl.bin");
assert_eq!(bytes, b"REPLAY MATERIAL XX");
{
let mut h = Filesystem::open_file_rw(
&mut fs,
&mut dev,
Path::new("/jnl.bin"),
OpenFlags::default(),
None,
)
.unwrap();
h.write_all(b"!").unwrap();
h.sync().unwrap();
}
let bytes = read_all(&mut fs, &mut dev, "/jnl.bin");
assert_eq!(&bytes[..1], b"!");
assert_eq!(bytes.len(), 18);
assert_eq!(&bytes[1..], b"EPLAY MATERIAL XX");
}
#[test]
fn open_file_rw_journal_idempotent_clean_close() {
let (mut dev, mut fs) = fresh(16 * 1024 * 1024);
fs.create_file(
&mut dev,
"/k.bin",
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(b"A".to_vec())),
len: 1,
},
FileMeta::default(),
)
.unwrap();
fs.flush(&mut dev).unwrap();
for _ in 0..3 {
let mut h = Filesystem::open_file_rw(
&mut fs,
&mut dev,
Path::new("/k.bin"),
OpenFlags::default(),
None,
)
.unwrap();
h.write_all(b"Z").unwrap();
h.sync().unwrap();
}
let (lcn, cs) = {
let w = fs.writer.as_ref().unwrap();
(w.layout.logfile_lcn, w.cluster_size)
};
let logfile_offset = lcn * cs;
let (view, _) = super::super::logfile::read_current_restart(&mut dev, logfile_offset)
.unwrap()
.expect("restart parses");
assert!(view.is_clean(), "restart area must be clean after sync");
}
#[test]
fn open_file_rw_round_trip_ntfsfix_clean() {
if std::process::Command::new("ntfsfix")
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_err()
{
eprintln!("ntfsfix not found — skipping");
return;
}
let (mut dev, mut fs) = fresh(16 * 1024 * 1024);
fs.create_file(
&mut dev,
"/round.bin",
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(b"original".to_vec())),
len: 8,
},
FileMeta::default(),
)
.unwrap();
fs.flush(&mut dev).unwrap();
{
let mut h = Filesystem::open_file_rw(
&mut fs,
&mut dev,
Path::new("/round.bin"),
OpenFlags::default(),
None,
)
.unwrap();
h.write_all(b"REWRITTEN").unwrap();
h.sync().unwrap();
}
let mut tmp = std::env::temp_dir();
tmp.push(format!("fstool-ntfs-rw-{}.img", std::process::id()));
let buf = dev.as_slice().to_vec();
std::fs::write(&tmp, &buf).unwrap();
let out = std::process::Command::new("ntfsfix")
.arg("--no-action")
.arg(&tmp)
.output()
.expect("ntfsfix invocation");
let _ = std::fs::remove_file(&tmp);
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
let combined = format!("{stdout}\n{stderr}");
assert!(
combined.contains("Processing of $MFT and $MFTMirr completed successfully."),
"ntfsfix MFT processing failed: stdout={stdout}, stderr={stderr}"
);
}
}