use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use super::{
DESCRIPTOR,
boot_sector::NtfsBootSector,
index::read_directory_index_entries,
record::{NtfsDataAttribute, NtfsFileRecord, parse_file_record},
reparse::NtfsReparsePointInfo,
stream::{
build_default_data_source, build_stream_data_source, classify_node, decode_node_id,
grouped_stream_attributes, i30_attributes, load_attribute_list_entries, read_file_record,
resident_index_root_data, resolve_attribute_list_record, resolve_bootstrap_mft_record,
stream_size,
},
};
use crate::{
ByteSourceHandle, Error, NamespaceDirectoryEntry, NamespaceNodeId, NamespaceNodeKind,
NamespaceNodeRecord, Result, SourceHints, filesystems::FileSystem,
};
const ROOT_FILE_RECORD_NUMBER: u64 = 5;
pub struct NtfsFileSystem {
source: ByteSourceHandle,
boot_sector: NtfsBootSector,
mft_stream: ByteSourceHandle,
file_record_size: u64,
record_count: u64,
nodes: Mutex<HashMap<u64, Arc<NtfsNode>>>,
children: Mutex<HashMap<u64, Arc<[NamespaceDirectoryEntry]>>>,
}
struct NtfsNode {
name: String,
parent_id: Option<u64>,
record: NamespaceNodeRecord,
data_attributes: Arc<[NtfsDataAttribute]>,
index_root_attributes: Arc<[NtfsDataAttribute]>,
index_allocation_attributes: Arc<[NtfsDataAttribute]>,
reparse_point: Option<NtfsReparsePointInfo>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NtfsDataStreamInfo {
pub name: Option<String>,
pub size: u64,
}
impl NtfsFileSystem {
pub fn open(source: ByteSourceHandle) -> Result<Self> {
Self::open_with_hints(source, SourceHints::new())
}
pub fn open_with_hints(source: ByteSourceHandle, _hints: SourceHints<'_>) -> Result<Self> {
let boot_sector = NtfsBootSector::read(source.as_ref())?;
let file_record_size = boot_sector.file_record_size()?;
let mft_offset = boot_sector.mft_offset()?;
let first_record = read_file_record(source.as_ref(), mft_offset, file_record_size)?;
let mft_record = parse_file_record(&first_record, 0)?
.ok_or_else(|| Error::invalid_format("ntfs master file table record is not in use"))?;
let bootstrap_mft_stream =
build_stream_data_source(source.clone(), &boot_sector, &mft_record.data_attributes)?;
let resolved_mft_record = resolve_bootstrap_mft_record(
source.clone(),
&boot_sector,
bootstrap_mft_stream.clone(),
file_record_size,
&mft_record,
)?;
let mft_stream = if resolved_mft_record.attribute_list_entries.is_empty()
&& resolved_mft_record.attribute_list_attributes.is_empty()
{
bootstrap_mft_stream
} else {
build_stream_data_source(
source.clone(),
&boot_sector,
&resolved_mft_record.data_attributes,
)?
};
let mft_stream_size = mft_stream.size()?;
if mft_stream_size < file_record_size {
return Err(Error::invalid_format(
"ntfs master file table is smaller than one file record".to_string(),
));
}
let filesystem = Self {
source,
boot_sector,
mft_stream,
file_record_size,
record_count: mft_stream_size / file_record_size,
nodes: Mutex::new(HashMap::new()),
children: Mutex::new(HashMap::new()),
};
let root_id = NamespaceNodeId::from_u64(ROOT_FILE_RECORD_NUMBER);
if filesystem.lookup_node(&root_id).is_err() {
return Err(Error::invalid_format(
"ntfs root directory record is missing".to_string(),
));
}
Ok(filesystem)
}
pub fn data_streams(&self, node_id: &NamespaceNodeId) -> Result<Vec<NtfsDataStreamInfo>> {
let node = self.lookup_node(node_id)?;
let mut streams = Vec::new();
for (name, attributes) in grouped_stream_attributes(&node.data_attributes) {
streams.push(NtfsDataStreamInfo {
size: stream_size(&attributes)?,
name,
});
}
Ok(streams)
}
pub fn open_data_stream(
&self, node_id: &NamespaceNodeId, name: Option<&str>,
) -> Result<ByteSourceHandle> {
let node = self.lookup_node(node_id)?;
let attributes = node
.data_attributes
.iter()
.filter(|attribute| attribute.name.as_deref() == name)
.cloned()
.collect::<Vec<_>>();
if attributes.is_empty() {
let stream_name = name.unwrap_or("$DATA");
return Err(Error::not_found(format!(
"ntfs data stream {stream_name} was not found on node {}",
decode_node_id(node_id)?
)));
}
build_stream_data_source(self.source.clone(), &self.boot_sector, &attributes)
}
pub fn reparse_point(&self, node_id: &NamespaceNodeId) -> Result<Option<NtfsReparsePointInfo>> {
Ok(self.lookup_node(node_id)?.reparse_point.clone())
}
pub fn symlink_target(&self, node_id: &NamespaceNodeId) -> Result<Option<String>> {
Ok(
self
.lookup_node(node_id)?
.reparse_point
.as_ref()
.and_then(NtfsReparsePointInfo::preferred_target),
)
}
fn lookup_node(&self, node_id: &NamespaceNodeId) -> Result<Arc<NtfsNode>> {
let record_number = decode_node_id(node_id)?;
self
.load_node(record_number)?
.ok_or_else(|| Error::not_found(format!("ntfs node {record_number} was not found")))
}
fn load_node(&self, record_number: u64) -> Result<Option<Arc<NtfsNode>>> {
if let Some(node) = self
.nodes
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.get(&record_number)
.cloned()
{
return Ok(Some(node));
}
let Some(record) = self.load_resolved_record(record_number)? else {
return Ok(None);
};
let Some(name) = record.preferred_name().cloned() else {
return Ok(None);
};
let parent_id = if record_number == ROOT_FILE_RECORD_NUMBER {
None
} else {
Some(name.parent_record_number)
};
let (kind, size) = classify_node(&record)?;
let node = Arc::new(NtfsNode {
name: if record_number == ROOT_FILE_RECORD_NUMBER {
String::new()
} else {
name.name
},
parent_id,
record: NamespaceNodeRecord::new(NamespaceNodeId::from_u64(record_number), kind, size),
data_attributes: Arc::from(record.data_attributes),
index_root_attributes: Arc::from(record.index_root_attributes),
index_allocation_attributes: Arc::from(record.index_allocation_attributes),
reparse_point: record.reparse_point,
});
let mut nodes = self
.nodes
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if let Some(cached) = nodes.get(&record_number).cloned() {
return Ok(Some(cached));
}
nodes.insert(record_number, node.clone());
Ok(Some(node))
}
fn load_resolved_record(&self, record_number: u64) -> Result<Option<NtfsFileRecord>> {
let Some(record) = self.read_parsed_record(record_number)? else {
return Ok(None);
};
if record.base_record_number.is_some() {
return Ok(None);
}
let attribute_list_entries =
load_attribute_list_entries(self.source.clone(), &self.boot_sector, &record)?;
resolve_attribute_list_record(
record_number,
&record,
&attribute_list_entries,
&mut |referenced_record| self.read_parsed_record(referenced_record),
)
.map(Some)
}
fn read_parsed_record(&self, record_number: u64) -> Result<Option<NtfsFileRecord>> {
if record_number >= self.record_count {
return Ok(None);
}
let record_bytes = read_file_record(
self.mft_stream.as_ref(),
record_number
.checked_mul(self.file_record_size)
.ok_or_else(|| Error::invalid_range("ntfs MFT record offset overflow"))?,
self.file_record_size,
)?;
parse_file_record(&record_bytes, record_number)
}
fn directory_entries(
&self, record_number: u64, node: &NtfsNode,
) -> Result<Arc<[NamespaceDirectoryEntry]>> {
if let Some(entries) = self
.children
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.get(&record_number)
.cloned()
{
return Ok(entries);
}
let mut entries =
if let Some(root_data) = resident_index_root_data(&node.index_root_attributes)? {
let allocation_source = if node.index_allocation_attributes.is_empty() {
None
} else {
let allocation_attributes = i30_attributes(&node.index_allocation_attributes)
.into_iter()
.cloned()
.collect::<Vec<_>>();
Some(build_stream_data_source(
self.source.clone(),
&self.boot_sector,
&allocation_attributes,
)?)
};
let cluster_size = self.boot_sector.cluster_size()?;
read_directory_index_entries(
&root_data,
allocation_source,
cluster_size,
&mut |child_record_number| {
self
.load_node(child_record_number)?
.map(|child| child.record.kind)
.ok_or_else(|| {
Error::not_found(format!(
"ntfs index entry points to missing record {child_record_number}"
))
})
},
)?
} else {
self.scan_directory_entries(record_number)?
};
entries.sort_by(|left, right| left.name.cmp(&right.name));
let entries: Arc<[NamespaceDirectoryEntry]> = Arc::from(entries.into_boxed_slice());
let mut cache = self
.children
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if let Some(existing) = cache.get(&record_number).cloned() {
return Ok(existing);
}
cache.insert(record_number, entries.clone());
Ok(entries)
}
fn scan_directory_entries(&self, record_number: u64) -> Result<Vec<NamespaceDirectoryEntry>> {
let mut entries = Vec::new();
for child_record_number in 0..self.record_count {
let Some(node) = self.load_node(child_record_number)? else {
continue;
};
if node.parent_id != Some(record_number) || child_record_number == ROOT_FILE_RECORD_NUMBER {
continue;
}
entries.push(NamespaceDirectoryEntry::new(
node.name.clone(),
node.record.id.clone(),
node.record.kind,
));
}
Ok(entries)
}
}
impl FileSystem for NtfsFileSystem {
fn descriptor(&self) -> crate::FormatDescriptor {
DESCRIPTOR
}
fn root_node_id(&self) -> NamespaceNodeId {
NamespaceNodeId::from_u64(ROOT_FILE_RECORD_NUMBER)
}
fn node(&self, node_id: &NamespaceNodeId) -> Result<NamespaceNodeRecord> {
self.lookup_node(node_id).map(|node| node.record.clone())
}
fn read_dir(&self, directory_id: &NamespaceNodeId) -> Result<Vec<NamespaceDirectoryEntry>> {
let record_number = decode_node_id(directory_id)?;
let node = self.lookup_node(directory_id)?;
if node.record.kind != NamespaceNodeKind::Directory {
return Err(Error::not_found(format!(
"ntfs node {record_number} is not a directory"
)));
}
Ok(self.directory_entries(record_number, &node)?.to_vec())
}
fn open_file(&self, file_id: &NamespaceNodeId) -> Result<ByteSourceHandle> {
let record_number = decode_node_id(file_id)?;
let node = self.lookup_node(file_id)?;
if node.record.kind == NamespaceNodeKind::Directory {
return Err(Error::not_found(format!(
"ntfs node {record_number} is not a readable file"
)));
}
build_default_data_source(
self.source.clone(),
&self.boot_sector,
&node.data_attributes,
)
}
}
#[cfg(test)]
mod tests {
use std::{collections::HashMap, sync::Arc};
use super::*;
use crate::{
ByteSourceHandle, BytesDataSource,
filesystems::ntfs::{
NtfsAttributeListEntry,
record::{NtfsDataAttributeValue, NtfsFileNameAttribute, NtfsNonResidentAttribute},
reparse::NtfsReparsePointKind,
stream::{
build_stream_data_source, classify_node, load_attribute_list_entries,
resolve_attribute_list_record, resolve_bootstrap_mft_record, stream_size,
},
},
};
#[derive(Clone, Copy, Default)]
struct NonResidentOptions {
data_flags: u16,
compression_unit: u16,
}
fn resident_data_attribute(data: &[u8], attribute_id: u16) -> NtfsDataAttribute {
NtfsDataAttribute {
attribute_id,
name: None,
data_flags: 0,
value: NtfsDataAttributeValue::Resident(Arc::from(data)),
}
}
fn non_resident_data_attribute(
first_vcn: u64, last_vcn: u64, data_size: u64, valid_data_size: u64, runlist: &[u8],
attribute_id: u16,
) -> NtfsDataAttribute {
non_resident_data_attribute_with_flags(
first_vcn,
last_vcn,
data_size,
valid_data_size,
runlist,
attribute_id,
NonResidentOptions::default(),
)
}
fn non_resident_data_attribute_with_flags(
first_vcn: u64, last_vcn: u64, data_size: u64, valid_data_size: u64, runlist: &[u8],
attribute_id: u16, options: NonResidentOptions,
) -> NtfsDataAttribute {
NtfsDataAttribute {
attribute_id,
name: None,
data_flags: options.data_flags,
value: NtfsDataAttributeValue::NonResident(NtfsNonResidentAttribute {
first_vcn,
last_vcn,
compression_unit: options.compression_unit,
data_size,
valid_data_size,
runlist: Arc::from(runlist),
}),
}
}
fn sample_boot_sector() -> NtfsBootSector {
NtfsBootSector {
bytes_per_sector: 512,
sectors_per_cluster: 8,
total_sectors: 4096,
mft_cluster: 4,
mft_mirror_cluster: 8,
clusters_per_file_record: 0xF6,
clusters_per_index_buffer: 1,
volume_serial_number: 0,
}
}
fn sample_record_with_reparse(
flags: u16, reparse_kind: NtfsReparsePointKind, data_attributes: Vec<NtfsDataAttribute>,
) -> NtfsFileRecord {
NtfsFileRecord {
flags,
base_record_number: None,
file_names: vec![],
data_attributes,
attribute_list_entries: vec![],
attribute_list_attributes: vec![],
index_root_attributes: vec![],
index_allocation_attributes: vec![],
reparse_point: Some(NtfsReparsePointInfo {
kind: reparse_kind,
tag: 0,
substitute_name: None,
print_name: None,
flags: None,
compression_method: None,
}),
has_reparse_point: true,
}
}
fn synthetic_non_resident_attribute_bytes(
first_vcn: u64, last_vcn: u64, data_size: u64, valid_data_size: u64, runlist: &[u8],
attribute_id: u16,
) -> Vec<u8> {
let attribute_size = (64 + runlist.len()).next_multiple_of(8);
let mut attribute = vec![0u8; attribute_size];
attribute[0..4].copy_from_slice(&0x80u32.to_le_bytes());
attribute[4..8].copy_from_slice(&(u32::try_from(attribute_size).unwrap()).to_le_bytes());
attribute[8] = 1;
attribute[14..16].copy_from_slice(&attribute_id.to_le_bytes());
attribute[16..24].copy_from_slice(&first_vcn.to_le_bytes());
attribute[24..32].copy_from_slice(&last_vcn.to_le_bytes());
attribute[32..34].copy_from_slice(&64u16.to_le_bytes());
attribute[40..48].copy_from_slice(&data_size.to_le_bytes());
attribute[48..56].copy_from_slice(&data_size.to_le_bytes());
attribute[56..64].copy_from_slice(&valid_data_size.to_le_bytes());
attribute[64..64 + runlist.len()].copy_from_slice(runlist);
attribute
}
fn synthetic_attribute_list_entry_bytes(
attribute_type: u32, starting_vcn: u64, base_file_record: u64, attribute_id: u16,
) -> Vec<u8> {
let mut bytes = vec![0u8; 32];
bytes[0..4].copy_from_slice(&attribute_type.to_le_bytes());
bytes[4..6].copy_from_slice(&32u16.to_le_bytes());
bytes[8..16].copy_from_slice(&starting_vcn.to_le_bytes());
bytes[16..22].copy_from_slice(&base_file_record.to_le_bytes()[..6]);
bytes[24..26].copy_from_slice(&attribute_id.to_le_bytes());
bytes
}
fn synthetic_file_record(attribute: &[u8]) -> Vec<u8> {
let mut record = vec![0u8; 512];
record[0..4].copy_from_slice(b"FILE");
record[4..6].copy_from_slice(&48u16.to_le_bytes());
record[6..8].copy_from_slice(&2u16.to_le_bytes());
record[16..18].copy_from_slice(&1u16.to_le_bytes());
record[20..22].copy_from_slice(&56u16.to_le_bytes());
record[22..24].copy_from_slice(&0x0001u16.to_le_bytes());
let used_size = 56 + attribute.len() + 4;
let record_size = u32::try_from(record.len()).unwrap();
record[24..28].copy_from_slice(&(u32::try_from(used_size).unwrap()).to_le_bytes());
record[28..32].copy_from_slice(&record_size.to_le_bytes());
record[40..42].copy_from_slice(&2u16.to_le_bytes());
record[48..50].copy_from_slice(&[0xAA, 0xBB]);
record[50..52].copy_from_slice(&[0x11, 0x22]);
record[56..56 + attribute.len()].copy_from_slice(attribute);
record[56 + attribute.len()..60 + attribute.len()]
.copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
record[510..512].copy_from_slice(&[0xAA, 0xBB]);
record
}
#[test]
fn resolves_attribute_list_extension_records() {
let base_record = NtfsFileRecord {
flags: 0x0001,
base_record_number: None,
file_names: vec![],
data_attributes: vec![resident_data_attribute(b"base", 1)],
attribute_list_entries: vec![NtfsAttributeListEntry {
attribute_type: 0x80,
entry_length: 40,
starting_vcn: 0,
base_file_record: 42,
base_file_sequence: 1,
attribute_id: 7,
name: None,
}],
attribute_list_attributes: vec![],
index_root_attributes: vec![],
index_allocation_attributes: vec![],
reparse_point: None,
has_reparse_point: false,
};
let extension_record = NtfsFileRecord {
flags: 0x0001,
base_record_number: Some(5),
file_names: vec![NtfsFileNameAttribute {
attribute_id: 2,
parent_record_number: 5,
name: "merged.txt".to_string(),
namespace: 1,
}],
data_attributes: vec![resident_data_attribute(b"extension", 7)],
attribute_list_entries: vec![],
attribute_list_attributes: vec![],
index_root_attributes: vec![],
index_allocation_attributes: vec![],
reparse_point: None,
has_reparse_point: false,
};
let records = HashMap::from([(42u64, extension_record)]);
let mut load_record = |record_number| Ok(records.get(&record_number).cloned());
let resolved = resolve_attribute_list_record(
5,
&base_record,
&base_record.attribute_list_entries,
&mut load_record,
)
.unwrap();
assert_eq!(resolved.file_names[0].name, "merged.txt");
assert_eq!(resolved.data_attributes.len(), 2);
}
#[test]
fn classifies_wof_reparse_records_as_regular_files() {
let record = sample_record_with_reparse(
0x0001,
NtfsReparsePointKind::WofCompressed,
vec![resident_data_attribute(b"wof", 1)],
);
let (kind, size) = classify_node(&record).unwrap();
assert_eq!(kind, NamespaceNodeKind::File);
assert_eq!(size, 3);
}
#[test]
fn classifies_mount_point_directories_as_symlinks_only() {
let mount_point =
sample_record_with_reparse(0x0001 | 0x0002, NtfsReparsePointKind::MountPoint, vec![]);
let unknown = sample_record_with_reparse(0x0001 | 0x0002, NtfsReparsePointKind::Other, vec![]);
assert_eq!(
classify_node(&mount_point).unwrap().0,
NamespaceNodeKind::Symlink
);
assert_eq!(
classify_node(&unknown).unwrap().0,
NamespaceNodeKind::Directory
);
}
#[test]
fn loads_nonresident_attribute_list_entries() {
let entry_bytes = synthetic_attribute_list_entry_bytes(0x80, 0, 42, 7);
let mut source_bytes = vec![0u8; 2 * 4096];
source_bytes[4096..4096 + entry_bytes.len()].copy_from_slice(&entry_bytes);
let source = Arc::new(BytesDataSource::new(source_bytes)) as ByteSourceHandle;
let record = NtfsFileRecord {
flags: 0x0001,
base_record_number: None,
file_names: vec![],
data_attributes: vec![],
attribute_list_entries: vec![],
attribute_list_attributes: vec![non_resident_data_attribute(
0,
0,
32,
32,
&[0x11, 0x01, 0x01, 0x00],
1,
)],
index_root_attributes: vec![],
index_allocation_attributes: vec![],
reparse_point: None,
has_reparse_point: false,
};
let entries = load_attribute_list_entries(source, &sample_boot_sector(), &record).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].base_file_record, 42);
assert_eq!(entries[0].attribute_id, 7);
}
#[test]
fn resolves_bootstrap_mft_attribute_list_records_from_partial_stream() {
let base_record = NtfsFileRecord {
flags: 0x0001,
base_record_number: None,
file_names: vec![],
data_attributes: vec![non_resident_data_attribute(
0,
0,
1024,
1024,
&[0x11, 0x01, 0x01, 0x00],
1,
)],
attribute_list_entries: vec![NtfsAttributeListEntry {
attribute_type: 0x80,
entry_length: 40,
starting_vcn: 1,
base_file_record: 1,
base_file_sequence: 1,
attribute_id: 7,
name: None,
}],
attribute_list_attributes: vec![],
index_root_attributes: vec![],
index_allocation_attributes: vec![],
reparse_point: None,
has_reparse_point: false,
};
let extension_record = synthetic_file_record(&synthetic_non_resident_attribute_bytes(
1,
1,
0,
0,
&[0x11, 0x01, 0x02, 0x00],
7,
));
let mut bootstrap_bytes = vec![0u8; 1024];
bootstrap_bytes[512..1024].copy_from_slice(&extension_record);
let bootstrap_stream = Arc::new(BytesDataSource::new(bootstrap_bytes)) as ByteSourceHandle;
let resolved = resolve_bootstrap_mft_record(
bootstrap_stream.clone(),
&sample_boot_sector(),
bootstrap_stream,
512,
&base_record,
)
.unwrap();
assert_eq!(resolved.data_attributes.len(), 2);
assert!(resolved.data_attributes.iter().any(|attribute| {
matches!(
&attribute.value,
NtfsDataAttributeValue::NonResident(non_resident) if non_resident.first_vcn == 1
)
}));
}
#[test]
fn stream_size_accepts_empty_nonresident_attributes() {
let attributes = [non_resident_data_attribute(0, u64::MAX, 0, 0, &[0], 1)];
assert_eq!(stream_size(&attributes).unwrap(), 0);
}
#[test]
fn build_stream_data_source_accepts_empty_nonresident_attributes() {
let source = Arc::new(BytesDataSource::new(Vec::<u8>::new())) as ByteSourceHandle;
let attributes = [non_resident_data_attribute(0, u64::MAX, 0, 0, &[0], 1)];
let stream = build_stream_data_source(source, &sample_boot_sector(), &attributes).unwrap();
assert_eq!(stream.size().unwrap(), 0);
assert_eq!(stream.read_all().unwrap(), Vec::<u8>::new());
}
#[test]
fn build_stream_data_source_uses_primary_extent_sizes() {
let mut bytes = vec![0u8; 3 * 4096];
bytes[4096..8192].fill(b'A');
bytes[8192..12288].fill(b'B');
let source = Arc::new(BytesDataSource::new(bytes)) as ByteSourceHandle;
let attributes = [
non_resident_data_attribute(0, 0, 8192, 8192, &[0x11, 0x01, 0x01, 0x00], 1),
non_resident_data_attribute(1, 1, 16384, 12288, &[0x11, 0x01, 0x02, 0x00], 2),
];
let stream = build_stream_data_source(source, &sample_boot_sector(), &attributes).unwrap();
let data = stream.read_all().unwrap();
assert_eq!(stream.size().unwrap(), 8192);
assert_eq!(data.len(), 8192);
assert!(data[..4096].iter().all(|byte| *byte == b'A'));
assert!(data[4096..].iter().all(|byte| *byte == b'B'));
}
#[test]
fn build_stream_data_source_decompresses_lznt1_units() {
let mut backing = vec![0u8; 2 * 4096];
backing[4096..4104].copy_from_slice(&[0x05, 0xB0, 0x08, b'A', b'B', b'C', 0x06, 0x20]);
let source = Arc::new(BytesDataSource::new(backing)) as ByteSourceHandle;
let attributes = [non_resident_data_attribute_with_flags(
0,
15,
12,
12,
&[0x11, 0x01, 0x01, 0x01, 0x0F, 0x00],
1,
NonResidentOptions {
data_flags: 0x0001,
compression_unit: 0,
},
)];
let stream = build_stream_data_source(source, &sample_boot_sector(), &attributes).unwrap();
assert_eq!(stream.read_all().unwrap(), b"ABCABCABCABC");
}
}
crate::filesystems::driver::impl_file_system_data_source!(NtfsFileSystem);