use std::collections::HashMap;
use std::io::Read;
use crate::Result;
use crate::block::BlockDevice;
pub mod attribute;
pub mod attribute_list;
pub mod boot;
pub mod compression;
pub mod format;
pub mod index;
pub mod mft;
pub mod run_list;
pub mod secure;
pub mod upcase_gen;
pub mod writer;
use attribute::{
ATTR_FLAG_COMPRESSED, ATTR_FLAG_ENCRYPTED, AttributeIter, AttributeKind, FileName,
StandardInformation, TYPE_ATTRIBUTE_LIST, TYPE_DATA, TYPE_FILE_NAME, TYPE_INDEX_ALLOCATION,
TYPE_INDEX_ROOT, TYPE_OBJECT_ID, TYPE_REPARSE_POINT, TYPE_SECURITY_DESCRIPTOR,
TYPE_STANDARD_INFORMATION,
};
use boot::BootSector;
use index::IndexEntry;
use run_list::Extent;
use secure::UpcaseTable;
pub const MFT_RECORD_MFT: u64 = 0;
pub const MFT_RECORD_ROOT: u64 = 5;
pub const MFT_RECORD_SECURE: u64 = 9;
pub const MFT_RECORD_UPCASE: u64 = 10;
const MAX_SECURITY_DESCRIPTOR_BYTES: u64 = 64 * 1024;
pub fn probe(dev: &mut dyn BlockDevice) -> Result<bool> {
if dev.total_size() < 11 {
return Ok(false);
}
let mut head = [0u8; 11];
dev.read_at(0, &mut head)?;
Ok(&head[3..11] == boot::NTFS_OEM)
}
pub use boot::BootSector as ExportedBootSector;
pub struct Ntfs {
boot: BootSector,
mft_runs: Vec<Extent>,
upcase: Option<UpcaseTable>,
sii_cache: Option<HashMap<u32, (u64, u32)>>,
writer: Option<writer::WriterState>,
}
impl Ntfs {
pub fn open(dev: &mut dyn BlockDevice) -> Result<Self> {
if dev.total_size() < 512 {
return Err(crate::Error::InvalidImage(
"ntfs: device too small to hold a boot sector".into(),
));
}
let mut buf = [0u8; 512];
dev.read_at(0, &mut buf)?;
let boot = BootSector::decode(&buf).ok_or_else(|| {
crate::Error::InvalidImage("ntfs: boot sector OEM ID is not 'NTFS '".into())
})?;
Ok(Self {
boot,
mft_runs: Vec::new(),
upcase: None,
sii_cache: None,
writer: None,
})
}
pub fn total_bytes(&self) -> u64 {
self.boot.total_sectors * u64::from(self.boot.bytes_per_sector)
}
pub fn cluster_size(&self) -> u32 {
self.boot.cluster_size()
}
pub fn bytes_per_sector(&self) -> u16 {
self.boot.bytes_per_sector
}
pub fn sectors_per_cluster(&self) -> u8 {
self.boot.sectors_per_cluster
}
pub fn mft_record_size(&self) -> u32 {
self.boot.mft_record_size()
}
pub fn volume_serial(&self) -> u64 {
self.boot.volume_serial
}
pub fn boot_sector(&self) -> &BootSector {
&self.boot
}
pub fn read_mft_record(
&mut self,
dev: &mut dyn BlockDevice,
rec: u64,
out: &mut [u8],
) -> Result<()> {
let rec_size = self.boot.mft_record_size() as usize;
if out.len() < rec_size {
return Err(crate::Error::InvalidArgument(
"ntfs: MFT scratch buffer too small".into(),
));
}
let out = &mut out[..rec_size];
if self.mft_runs.is_empty() {
let base = self.boot.mft_lcn * u64::from(self.boot.cluster_size());
dev.read_at(base, out)?;
mft::apply_fixup(out, self.boot.bytes_per_sector as usize)?;
let header = mft::RecordHeader::parse(out)?;
for attr_res in AttributeIter::new(out, header.first_attribute_offset as usize) {
let attr = attr_res?;
if attr.type_code == TYPE_DATA && attr.name.is_empty() {
match attr.kind {
AttributeKind::NonResident { runs, .. } => {
self.mft_runs = runs;
}
AttributeKind::Resident { .. } => {
return Err(crate::Error::InvalidImage(
"ntfs: $MFT $DATA is resident — impossible".into(),
));
}
}
break;
}
}
if self.mft_runs.is_empty() {
return Err(crate::Error::InvalidImage(
"ntfs: could not locate $MFT $DATA run list in record 0".into(),
));
}
if rec == 0 {
return Ok(()); }
}
let mft_byte_offset = rec
.checked_mul(rec_size as u64)
.ok_or_else(|| crate::Error::InvalidImage("ntfs: MFT offset overflow".into()))?;
let cluster_size = u64::from(self.boot.cluster_size());
let mut vcn_bytes: u64 = 0;
let mut found = false;
for ext in &self.mft_runs {
let ext_bytes = ext.length * cluster_size;
if mft_byte_offset < vcn_bytes + ext_bytes {
let local = mft_byte_offset - vcn_bytes;
match ext.lcn {
Some(lcn) => {
let phys = lcn * cluster_size + local;
dev.read_at(phys, out)?;
}
None => {
return Err(crate::Error::InvalidImage(
"ntfs: requested MFT record sits in a sparse run".into(),
));
}
}
found = true;
break;
}
vcn_bytes += ext_bytes;
}
if !found {
return Err(crate::Error::InvalidImage(format!(
"ntfs: MFT record {rec} is past the end of $MFT"
)));
}
mft::apply_fixup(out, self.boot.bytes_per_sector as usize)?;
Ok(())
}
fn load_record_set(
&mut self,
dev: &mut dyn BlockDevice,
rec_no: u64,
) -> Result<Vec<(u64, Vec<u8>)>> {
let rec_size = self.boot.mft_record_size() as usize;
let mut base = vec![0u8; rec_size];
self.read_mft_record(dev, rec_no, &mut base)?;
let mut records: Vec<(u64, Vec<u8>)> = vec![(rec_no, base)];
let base_bytes = records[0].1.clone();
let hdr = mft::RecordHeader::parse(&base_bytes)?;
let mut alist_bytes: Option<Vec<u8>> = None;
for attr_res in AttributeIter::new(&base_bytes, hdr.first_attribute_offset as usize) {
let attr = attr_res?;
if attr.type_code != TYPE_ATTRIBUTE_LIST {
continue;
}
match attr.kind {
AttributeKind::Resident { value, .. } => {
alist_bytes = Some(value.to_vec());
}
AttributeKind::NonResident {
real_size, runs, ..
} => {
let mut reader = NonResidentReader {
dev: &mut *dev,
cluster_size: self.boot.cluster_size() as u64,
runs,
real_size,
initialized_size: real_size,
pos: 0,
cluster_buf: vec![0u8; self.boot.cluster_size() as usize],
cached_vcn: u64::MAX,
cached_cluster_filled: false,
};
let mut buf = Vec::with_capacity(real_size as usize);
reader.read_to_end(&mut buf).map_err(crate::Error::from)?;
alist_bytes = Some(buf);
}
}
break;
}
let Some(alist_bytes) = alist_bytes else {
return Ok(records);
};
let entries = attribute_list::decode(&alist_bytes)?;
let mut seen = std::collections::HashSet::new();
seen.insert(rec_no);
for entry in entries {
let extension_rec = entry.record_number();
if extension_rec == rec_no {
continue;
}
if !seen.insert(extension_rec) {
continue;
}
let mut buf = vec![0u8; rec_size];
self.read_mft_record(dev, extension_rec, &mut buf)?;
records.push((extension_rec, buf));
}
Ok(records)
}
pub fn read_directory(
&mut self,
dev: &mut dyn BlockDevice,
dir_rec: u64,
) -> Result<Vec<IndexEntry>> {
let records = self.load_record_set(dev, dir_rec)?;
let hdr = mft::RecordHeader::parse(&records[0].1)?;
if !hdr.is_in_use() {
return Err(crate::Error::InvalidImage(format!(
"ntfs: directory record {dir_rec} is not in use"
)));
}
let mut root_value: Option<Vec<u8>> = None;
let mut alloc_runs: Option<Vec<Extent>> = None;
for (_rec, rec_buf) in &records {
let h = mft::RecordHeader::parse(rec_buf)?;
for attr_res in AttributeIter::new(rec_buf, h.first_attribute_offset as usize) {
let attr = attr_res?;
if attr.name != "$I30" {
continue;
}
match (attr.type_code, attr.kind) {
(TYPE_INDEX_ROOT, AttributeKind::Resident { value, .. }) => {
root_value = Some(value.to_vec());
}
(TYPE_INDEX_ALLOCATION, AttributeKind::NonResident { runs, .. }) => {
match alloc_runs.as_mut() {
Some(existing) => existing.extend(runs),
None => alloc_runs = Some(runs),
}
}
_ => {}
}
}
}
let root_value = root_value.ok_or_else(|| {
crate::Error::InvalidImage(format!("ntfs: record {dir_rec} has no $INDEX_ROOT $I30"))
})?;
let root_hdr = index::IndexRootHeader::parse(&root_value)?;
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(16);
let mut out = Vec::new();
let mut visited_blocks = std::collections::HashSet::<u64>::new();
let root_children = index::walk_index_node(&root_value, entries_start, entries_len, |e| {
out.push(e.clone());
})?;
if let Some(runs) = alloc_runs {
let block_size = root_hdr.index_block_size as usize;
for vcn in root_children {
self.descend_index(dev, &runs, block_size, vcn, &mut out, &mut visited_blocks)?;
}
}
let mut seen_refs = std::collections::HashMap::<u64, usize>::new();
let mut filtered: Vec<IndexEntry> = Vec::new();
for entry in out.into_iter() {
let key = entry.file_ref;
let is_dos = entry
.file_name
.as_ref()
.map(|fn_| fn_.namespace == FileName::NAMESPACE_DOS)
.unwrap_or(false);
if is_dos && seen_refs.contains_key(&key) {
continue;
}
if let Some(&idx) = seen_refs.get(&key) {
let prior_is_dos = filtered[idx]
.file_name
.as_ref()
.map(|fn_| fn_.namespace == FileName::NAMESPACE_DOS)
.unwrap_or(false);
if prior_is_dos && !is_dos {
filtered[idx] = entry;
}
continue;
}
seen_refs.insert(key, filtered.len());
filtered.push(entry);
}
Ok(filtered)
}
fn descend_index(
&mut self,
dev: &mut dyn BlockDevice,
alloc_runs: &[Extent],
block_size: usize,
vcn: u64,
out: &mut Vec<IndexEntry>,
visited: &mut std::collections::HashSet<u64>,
) -> Result<()> {
if !visited.insert(vcn) {
return Err(crate::Error::InvalidImage(
"ntfs: cycle in $INDEX_ALLOCATION tree".into(),
));
}
let cluster_size = u64::from(self.boot.cluster_size());
let target_bytes = vcn * cluster_size;
let mut walked: u64 = 0;
let mut block_buf = vec![0u8; block_size];
let mut found_offset: Option<u64> = None;
for ext in alloc_runs {
let span = ext.length * cluster_size;
if target_bytes < walked + span {
let local = target_bytes - walked;
match ext.lcn {
Some(lcn) => {
found_offset = Some(lcn * cluster_size + local);
}
None => {
return Err(crate::Error::InvalidImage(
"ntfs: $INDEX_ALLOCATION points to a sparse VCN".into(),
));
}
}
break;
}
walked += span;
}
let phys = found_offset.ok_or_else(|| {
crate::Error::InvalidImage(format!("ntfs: index VCN {vcn} not in run list"))
})?;
dev.read_at(phys, &mut block_buf)?;
mft::apply_fixup(&mut block_buf, self.boot.bytes_per_sector as usize)?;
let blk_hdr = index::IndexBlockHeader::parse(&block_buf)?;
let entries_start = blk_hdr.entries_start();
let entries_len = blk_hdr.entries_byte_len();
let children = index::walk_index_node(&block_buf, entries_start, entries_len, |e| {
out.push(e.clone());
})?;
for child in children {
self.descend_index(dev, alloc_runs, block_size, child, out, visited)?;
}
Ok(())
}
pub fn lookup_path(&mut self, dev: &mut dyn BlockDevice, path: &str) -> Result<u64> {
if !path.starts_with('/') {
return Err(crate::Error::InvalidArgument(format!(
"ntfs: path must be absolute, got {path:?}"
)));
}
self.ensure_upcase(dev)?;
let mut current = MFT_RECORD_ROOT;
for component in path.split('/').filter(|s| !s.is_empty()) {
let entries = self.read_directory(dev, current)?;
let mut next: Option<u64> = None;
for entry in entries {
if let Some(fname) = entry.file_name {
if fname.namespace == FileName::NAMESPACE_DOS {
continue;
}
let matches = match self.upcase.as_ref() {
Some(t) => t.equals_ignore_case(&fname.name, component),
None => fname.name == component,
};
if matches {
next = Some(entry.file_ref & 0x0000_FFFF_FFFF_FFFF);
break;
}
}
}
current = next.ok_or_else(|| {
crate::Error::InvalidImage(format!("ntfs: path component {component:?} not found"))
})?;
}
Ok(current)
}
pub fn list_path(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
) -> Result<Vec<crate::fs::DirEntry>> {
let rec = self.lookup_path(dev, path)?;
let entries = self.read_directory(dev, rec)?;
let mut out = Vec::with_capacity(entries.len());
for entry in entries {
if let Some(fname) = entry.file_name {
let kind = if fname.is_directory() {
crate::fs::EntryKind::Dir
} else {
crate::fs::EntryKind::Regular
};
let rec_no = (entry.file_ref & 0x0000_FFFF_FFFF_FFFF) as u32;
out.push(crate::fs::DirEntry {
name: fname.name,
inode: rec_no,
kind,
});
}
}
Ok(out)
}
pub fn open_file_reader<'a>(
&'a mut self,
dev: &'a mut dyn BlockDevice,
path: &str,
) -> Result<Box<dyn Read + 'a>> {
let rec_no = self.lookup_path(dev, path)?;
self.open_stream_by_record(dev, rec_no, "")
}
pub fn open_stream_by_record<'a>(
&'a mut self,
dev: &'a mut dyn BlockDevice,
rec_no: u64,
stream_name: &str,
) -> Result<Box<dyn Read + 'a>> {
let records = self.load_record_set(dev, rec_no)?;
let hdr = mft::RecordHeader::parse(&records[0].1)?;
if !hdr.is_in_use() {
return Err(crate::Error::InvalidImage(format!(
"ntfs: record {rec_no} is not in use"
)));
}
let mut resident_bytes: Option<Vec<u8>> = None;
type Segment = (u64, u64, u64, u64, u64, u8, Vec<Extent>);
let mut segments: Vec<Segment> = Vec::new();
let mut is_encrypted = false;
let mut is_compressed = false;
for (_rec, rec_buf) in &records {
let h = mft::RecordHeader::parse(rec_buf)?;
for attr_res in AttributeIter::new(rec_buf, h.first_attribute_offset as usize) {
let attr = attr_res?;
if attr.type_code != TYPE_DATA {
continue;
}
if attr.name != stream_name {
continue;
}
if attr.flags & ATTR_FLAG_ENCRYPTED != 0 {
is_encrypted = true;
}
if attr.flags & ATTR_FLAG_COMPRESSED != 0 {
is_compressed = true;
}
match attr.kind {
AttributeKind::Resident { value, .. } => {
resident_bytes = Some(value.to_vec());
}
AttributeKind::NonResident {
starting_vcn,
last_vcn,
allocated_size,
real_size,
initialized_size,
compression_unit,
runs,
} => {
segments.push((
starting_vcn,
last_vcn,
allocated_size,
real_size,
initialized_size,
compression_unit,
runs,
));
}
}
}
}
if is_encrypted {
return Err(crate::Error::Unsupported(
"ntfs: encrypted $DATA (EFS) is not supported".into(),
));
}
if let Some(bytes) = resident_bytes {
return Ok(Box::new(ResidentReader { bytes, pos: 0 }));
}
if segments.is_empty() {
return Err(crate::Error::InvalidImage(format!(
"ntfs: stream {stream_name:?} not found on record {rec_no}"
)));
}
segments.sort_by_key(|s| s.0);
let real_size = segments[0].3;
let initialized_size = segments[0].4;
let compression_unit = segments[0].5;
let mut runs: Vec<Extent> = Vec::new();
for seg in &segments {
runs.extend(seg.6.iter().copied());
}
let cluster_size = self.boot.cluster_size() as u64;
if is_compressed && compression_unit > 0 {
let cu_clusters = 1u64 << compression_unit;
return Ok(Box::new(CompressedReader::new(
dev,
cluster_size,
cu_clusters,
runs,
real_size,
initialized_size,
)));
}
Ok(Box::new(NonResidentReader {
dev,
cluster_size,
runs,
real_size,
initialized_size,
pos: 0,
cluster_buf: vec![0u8; cluster_size as usize],
cached_vcn: u64::MAX,
cached_cluster_filled: false,
}))
}
pub fn read_xattrs(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
) -> Result<HashMap<String, Vec<u8>>> {
let rec_no = self.lookup_path(dev, path)?;
let records = self.load_record_set(dev, rec_no)?;
let mut out: HashMap<String, Vec<u8>> = HashMap::new();
let mut ads_names: Vec<String> = Vec::new();
let mut win32_short_name: Option<Vec<u8>> = None;
let mut security_id: Option<u32> = None;
let mut have_inline_security = false;
for (_rec, rec_buf) in &records {
let h = mft::RecordHeader::parse(rec_buf)?;
for attr_res in AttributeIter::new(rec_buf, h.first_attribute_offset as usize) {
let attr = attr_res?;
match attr.type_code {
TYPE_STANDARD_INFORMATION => {
if let AttributeKind::Resident { value, .. } = attr.kind {
let si = StandardInformation::parse(value)?;
out.insert(
xattr_keys::DOS_ATTRS.into(),
si.file_attributes.to_le_bytes().to_vec(),
);
out.insert(xattr_keys::TIMES_RAW.into(), si.times_raw().to_vec());
if value.len() >= 0x58 {
let id = u32::from_le_bytes(value[0x54..0x58].try_into().unwrap());
if id != 0 {
security_id = Some(id);
}
}
}
}
TYPE_FILE_NAME => {
if let AttributeKind::Resident { value, .. } = attr.kind {
let fname = FileName::parse(value)?;
if fname.namespace == FileName::NAMESPACE_DOS
|| fname.namespace == FileName::NAMESPACE_WIN32_DOS
{
let raw_utf16: Vec<u8> = fname
.name
.encode_utf16()
.flat_map(|u| u.to_le_bytes())
.collect();
win32_short_name = Some(raw_utf16);
}
}
}
TYPE_OBJECT_ID => {
if let AttributeKind::Resident { value, .. } = attr.kind {
let take = value.len().min(16);
out.insert(xattr_keys::OBJECT_ID.into(), value[..take].to_vec());
}
}
TYPE_SECURITY_DESCRIPTOR => {
if let AttributeKind::Resident { value, .. } = attr.kind {
out.insert(xattr_keys::SECURITY.into(), value.to_vec());
have_inline_security = true;
}
}
TYPE_REPARSE_POINT => {
if let AttributeKind::Resident { value, .. } = attr.kind {
out.insert(xattr_keys::REPARSE.into(), value.to_vec());
}
}
TYPE_DATA if !attr.name.is_empty() && !ads_names.contains(&attr.name) => {
ads_names.push(attr.name.clone());
}
_ => {}
}
}
}
if let Some(name) = win32_short_name {
out.insert(xattr_keys::SHORT_NAME.into(), name);
}
if !have_inline_security {
if let Some(id) = security_id {
if let Some(sd) = self.resolve_security_descriptor(dev, id)? {
out.insert(xattr_keys::SECURITY.into(), sd);
}
}
}
for name in ads_names {
let key = format!("{}{}", xattr_keys::ADS_PREFIX, name);
let mut reader = self.open_stream_by_record(dev, rec_no, &name)?;
let mut buf = Vec::new();
let mut chunk = [0u8; 8192];
loop {
let n = reader.read(&mut chunk).map_err(crate::Error::from)?;
if n == 0 {
break;
}
if buf.len() + n > 1024 * 1024 {
return Err(crate::Error::Unsupported(format!(
"ntfs: ADS {name:?} exceeds 1 MiB cap for xattr passthrough"
)));
}
buf.extend_from_slice(&chunk[..n]);
}
out.insert(key, buf);
}
Ok(out)
}
fn ensure_upcase(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
if self.upcase.is_some() {
return Ok(());
}
match self.read_metadata_stream(dev, MFT_RECORD_UPCASE, "", 256 * 1024) {
Ok(bytes) => {
self.upcase = Some(UpcaseTable::from_bytes(&bytes));
}
Err(_) => {
self.upcase = Some(UpcaseTable::identity());
}
}
Ok(())
}
fn read_metadata_stream(
&mut self,
dev: &mut dyn BlockDevice,
rec_no: u64,
stream_name: &str,
cap: usize,
) -> Result<Vec<u8>> {
let mut reader = self.open_stream_by_record(dev, rec_no, stream_name)?;
let mut out = Vec::new();
let mut tmp = [0u8; 8192];
loop {
let n = reader.read(&mut tmp).map_err(crate::Error::from)?;
if n == 0 {
break;
}
if out.len() + n > cap {
let take = cap - out.len();
out.extend_from_slice(&tmp[..take]);
break;
}
out.extend_from_slice(&tmp[..n]);
}
Ok(out)
}
fn resolve_security_descriptor(
&mut self,
dev: &mut dyn BlockDevice,
security_id: u32,
) -> Result<Option<Vec<u8>>> {
if self.sii_cache.is_none() {
let cache = self.build_sii_cache(dev).unwrap_or_default();
self.sii_cache = Some(cache);
}
let cache = self.sii_cache.as_ref().expect("sii_cache populated");
let Some(&(offset, size)) = cache.get(&security_id) else {
return Ok(None);
};
if size as u64 > MAX_SECURITY_DESCRIPTOR_BYTES {
return Ok(None);
}
let mut reader = self.open_stream_by_record(dev, MFT_RECORD_SECURE, "$SDS")?;
let mut skipped: u64 = 0;
let mut sink = [0u8; 8192];
while skipped < offset {
let want = (offset - skipped).min(sink.len() as u64) as usize;
let n = reader.read(&mut sink[..want]).map_err(crate::Error::from)?;
if n == 0 {
return Ok(None);
}
skipped += n as u64;
}
let mut blob = vec![0u8; size as usize];
let mut filled = 0;
while filled < blob.len() {
let n = reader
.read(&mut blob[filled..])
.map_err(crate::Error::from)?;
if n == 0 {
blob.truncate(filled);
break;
}
filled += n;
}
if blob.len() < 0x14 {
return Ok(None);
}
let entry_size = u32::from_le_bytes(blob[16..20].try_into().unwrap()) as usize;
if entry_size <= 0x14 || entry_size > blob.len() {
return Ok(None);
}
let sd = blob[0x14..entry_size].to_vec();
Ok(Some(sd))
}
fn build_sii_cache(&mut self, dev: &mut dyn BlockDevice) -> Result<HashMap<u32, (u64, u32)>> {
let records = self.load_record_set(dev, MFT_RECORD_SECURE)?;
let mut root_value: Option<Vec<u8>> = None;
let mut alloc_runs: Option<Vec<Extent>> = None;
let mut index_block_size: u32 = 0;
for (_rec, rec_buf) in &records {
let h = mft::RecordHeader::parse(rec_buf)?;
for attr_res in AttributeIter::new(rec_buf, h.first_attribute_offset as usize) {
let attr = attr_res?;
if attr.name != "$SII" {
continue;
}
match (attr.type_code, attr.kind) {
(TYPE_INDEX_ROOT, AttributeKind::Resident { value, .. }) => {
let hdr = index::IndexRootHeader::parse(value)?;
index_block_size = hdr.index_block_size;
root_value = Some(value.to_vec());
}
(TYPE_INDEX_ALLOCATION, AttributeKind::NonResident { runs, .. }) => {
match alloc_runs.as_mut() {
Some(existing) => existing.extend(runs),
None => alloc_runs = Some(runs),
}
}
_ => {}
}
}
}
let Some(root_value) = root_value else {
return Ok(HashMap::new());
};
let root_hdr = index::IndexRootHeader::parse(&root_value)?;
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(16);
let mut cache = HashMap::new();
let entry_buf = &root_value
[entries_start..entries_start + entries_len.min(root_value.len() - entries_start)];
for e in secure::walk_sii_node(entry_buf)? {
cache.insert(e.security_id, (e.sds_offset, e.sds_size));
}
if let Some(runs) = alloc_runs {
if index_block_size > 0 {
let cluster_size = u64::from(self.boot.cluster_size());
let block_size = index_block_size as usize;
let mut visited = std::collections::HashSet::<u64>::new();
let mut walked: u64 = 0;
for ext in &runs {
let span = ext.length * cluster_size;
if let Some(lcn) = ext.lcn {
let mut local: u64 = 0;
while local < span {
let phys = lcn * cluster_size + local;
if visited.insert(phys) {
let mut blk = vec![0u8; block_size];
if dev.read_at(phys, &mut blk).is_ok()
&& mft::apply_fixup(
&mut blk,
self.boot.bytes_per_sector as usize,
)
.is_ok()
{
if let Ok(blk_hdr) = index::IndexBlockHeader::parse(&blk) {
let s = blk_hdr.entries_start();
let l = blk_hdr.entries_byte_len();
if s + l <= blk.len() {
let entries = &blk[s..s + l];
if let Ok(rows) = secure::walk_sii_node(entries) {
for r in rows {
cache.insert(
r.security_id,
(r.sds_offset, r.sds_size),
);
}
}
}
}
}
}
local += block_size as u64;
}
}
walked += span;
}
let _ = walked;
}
}
Ok(cache)
}
}
struct ResidentReader {
bytes: Vec<u8>,
pos: usize,
}
impl Read for ResidentReader {
fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
let n = (self.bytes.len() - self.pos).min(out.len());
out[..n].copy_from_slice(&self.bytes[self.pos..self.pos + n]);
self.pos += n;
Ok(n)
}
}
struct NonResidentReader<'a> {
dev: &'a mut dyn BlockDevice,
cluster_size: u64,
runs: Vec<Extent>,
real_size: u64,
initialized_size: u64,
pos: u64,
cluster_buf: Vec<u8>,
cached_vcn: u64,
cached_cluster_filled: bool,
}
impl<'a> NonResidentReader<'a> {
fn map_vcn(&self, vcn: u64) -> std::io::Result<Option<u64>> {
let mut walked: u64 = 0;
for ext in &self.runs {
if vcn < walked + ext.length {
let local = vcn - walked;
return Ok(ext.lcn.map(|lcn| (lcn + local) * self.cluster_size));
}
walked += ext.length;
}
Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
format!("ntfs: VCN {vcn} past end of run list"),
))
}
}
impl<'a> Read for NonResidentReader<'a> {
fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
if self.pos >= self.real_size {
return Ok(0);
}
let cs = self.cluster_size;
let vcn = self.pos / cs;
let off = (self.pos % cs) as usize;
if !self.cached_cluster_filled || self.cached_vcn != vcn {
let phys = self.map_vcn(vcn)?;
match phys {
Some(p) => {
self.dev
.read_at(p, &mut self.cluster_buf)
.map_err(std::io::Error::other)?;
}
None => {
self.cluster_buf.fill(0);
}
}
self.cached_vcn = vcn;
self.cached_cluster_filled = true;
}
let remaining_file = self.real_size - self.pos;
let n = ((cs - off as u64) as usize)
.min(out.len())
.min(remaining_file as usize);
if self.pos + n as u64 <= self.initialized_size {
out[..n].copy_from_slice(&self.cluster_buf[off..off + n]);
} else if self.pos >= self.initialized_size {
out[..n].fill(0);
} else {
let copy_n = (self.initialized_size - self.pos) as usize;
out[..copy_n].copy_from_slice(&self.cluster_buf[off..off + copy_n]);
out[copy_n..n].fill(0);
}
self.pos += n as u64;
Ok(n)
}
}
struct CompressedReader<'a> {
dev: &'a mut dyn BlockDevice,
cluster_size: u64,
cu_clusters: u64,
cu_size: u64,
runs: Vec<Extent>,
real_size: u64,
initialized_size: u64,
pos: u64,
src_buf: Vec<u8>,
out_buf: Vec<u8>,
cached_cu_index: u64,
}
impl<'a> CompressedReader<'a> {
fn new(
dev: &'a mut dyn BlockDevice,
cluster_size: u64,
cu_clusters: u64,
runs: Vec<Extent>,
real_size: u64,
initialized_size: u64,
) -> Self {
let cu_size = cluster_size * cu_clusters;
Self {
dev,
cluster_size,
cu_clusters,
cu_size,
runs,
real_size,
initialized_size,
pos: 0,
src_buf: vec![0u8; cu_size as usize],
out_buf: vec![0u8; cu_size as usize],
cached_cu_index: u64::MAX,
}
}
fn map_vcn(&self, vcn: u64) -> std::io::Result<Option<u64>> {
let mut walked: u64 = 0;
for ext in &self.runs {
if vcn < walked + ext.length {
let local = vcn - walked;
return Ok(ext.lcn.map(|lcn| (lcn + local) * self.cluster_size));
}
walked += ext.length;
}
Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
format!("ntfs: VCN {vcn} past end of run list"),
))
}
fn count_real_clusters_in_cu(&self, base_vcn: u64) -> std::io::Result<u64> {
let mut count = 0u64;
for k in 0..self.cu_clusters {
let phys = self.map_vcn(base_vcn + k)?;
if phys.is_some() {
count += 1;
}
}
Ok(count)
}
fn load_cu(&mut self, cu_index: u64) -> std::io::Result<()> {
if self.cached_cu_index == cu_index {
return Ok(());
}
let base_vcn = cu_index * self.cu_clusters;
let real_clusters = self.count_real_clusters_in_cu(base_vcn)?;
if real_clusters == 0 {
for b in &mut self.out_buf {
*b = 0;
}
} else if real_clusters == self.cu_clusters {
for k in 0..self.cu_clusters {
let phys = self
.map_vcn(base_vcn + k)?
.ok_or_else(|| std::io::Error::other("ntfs: stored-CU sparse cluster"))?;
let lo = (k * self.cluster_size) as usize;
let hi = lo + self.cluster_size as usize;
self.dev
.read_at(phys, &mut self.out_buf[lo..hi])
.map_err(std::io::Error::other)?;
}
} else {
let src_len = (real_clusters * self.cluster_size) as usize;
self.src_buf.resize(src_len, 0);
for k in 0..real_clusters {
let phys = self.map_vcn(base_vcn + k)?.ok_or_else(|| {
std::io::Error::other("ntfs: compressed-CU sparse mid-cluster")
})?;
let lo = (k * self.cluster_size) as usize;
let hi = lo + self.cluster_size as usize;
self.dev
.read_at(phys, &mut self.src_buf[lo..hi])
.map_err(std::io::Error::other)?;
}
compression::decompress_unit(&self.src_buf, &mut self.out_buf)
.map_err(std::io::Error::other)?;
}
self.cached_cu_index = cu_index;
Ok(())
}
}
impl<'a> Read for CompressedReader<'a> {
fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
if self.pos >= self.real_size {
return Ok(0);
}
let cu_index = self.pos / self.cu_size;
let off = (self.pos % self.cu_size) as usize;
self.load_cu(cu_index)?;
let remaining_file = self.real_size - self.pos;
let n = ((self.cu_size - off as u64) as usize)
.min(out.len())
.min(remaining_file as usize);
if self.pos + n as u64 <= self.initialized_size {
out[..n].copy_from_slice(&self.out_buf[off..off + n]);
} else if self.pos >= self.initialized_size {
out[..n].fill(0);
} else {
let copy_n = (self.initialized_size - self.pos) as usize;
out[..copy_n].copy_from_slice(&self.out_buf[off..off + copy_n]);
out[copy_n..n].fill(0);
}
self.pos += n as u64;
Ok(n)
}
}
pub mod xattr_keys {
pub const DOS_ATTRS: &str = "user.ntfs.dos_attrs";
pub const OBJECT_ID: &str = "user.ntfs.object_id";
pub const REPARSE: &str = "user.ntfs.reparse";
pub const ADS_PREFIX: &str = "user.ntfs.ads.";
pub const SECURITY: &str = "system.ntfs_security";
pub const SHORT_NAME: &str = "user.ntfs.short_name";
pub const TIMES_RAW: &str = "user.ntfs.times.raw";
}
#[cfg(test)]
mod tests;