mod common;
use fast_ntfs::indexes::NtfsFileNameIndex;
use fast_ntfs::structured_values::{NtfsFileName, NtfsFileNamespace, NtfsStandardInformation};
use fast_ntfs::{
KnownNtfsFileRecordNumber, Ntfs, NtfsAttributeType, NtfsError, NtfsGuid, NtfsReadSeek,
};
use zerocopy::FromBytes;
const NT_TIMESTAMP_2021_01_01: u64 = 132_539_328_000_000_000;
fn testfs_with_root_record_split_across_mft_runs() -> std::io::Cursor<Vec<u8>> {
const CLUSTER_SIZE: usize = 512;
const MFT_LCN: usize = 32;
const MFT_DATA_RUNS_OFFSET: usize = MFT_LCN * CLUSTER_SIZE + 0x140;
const ROOT_SECOND_CLUSTER_LCN: usize = MFT_LCN + 11;
const MOVED_CLUSTER_LCN: usize = 2000;
const REMAINING_MFT_CLUSTERS: u16 = 1163;
let mut fs = common::testfs1();
let data = fs.get_mut();
let source = ROOT_SECOND_CLUSTER_LCN * CLUSTER_SIZE;
let destination = MOVED_CLUSTER_LCN * CLUSTER_SIZE;
let cluster = data[source..source + CLUSTER_SIZE].to_vec();
data[destination..destination + CLUSTER_SIZE].copy_from_slice(&cluster);
data[source..source + CLUSTER_SIZE].fill(0);
let mut data_runs = [0u8; 24];
data_runs[..9].copy_from_slice(&[
0x11,
11,
MFT_LCN as u8,
0x22,
REMAINING_MFT_CLUSTERS.to_le_bytes()[0],
REMAINING_MFT_CLUSTERS.to_le_bytes()[1],
0xb0,
0x07,
0,
]);
data[MFT_DATA_RUNS_OFFSET..MFT_DATA_RUNS_OFFSET + data_runs.len()].copy_from_slice(&data_runs);
fs
}
#[test]
fn reads_volume_metadata() {
let mut fs = common::testfs1();
let ntfs = Ntfs::new(&mut fs).unwrap();
assert_eq!(ntfs.cluster_size(), 512);
assert_eq!(ntfs.sector_size(), 512);
assert_eq!(ntfs.size(), 2_096_640);
let volume_info = ntfs.volume_info(&mut fs).unwrap();
assert_eq!(
(volume_info.major_version(), volume_info.minor_version()),
(3, 1)
);
let volume_name = ntfs.volume_name(&mut fs).unwrap().unwrap();
assert_eq!(volume_name.name_length(), 14);
assert_eq!(volume_name.name(), "mylabel");
}
#[test]
fn cached_mft_runs_resolve_file_records() {
let mut fs = common::testfs1();
let ntfs = Ntfs::new(&mut fs).unwrap();
let uncached_position = ntfs.root_directory(&mut fs).unwrap().position();
ntfs.read_mft_runs(&mut fs).unwrap();
ntfs.read_mft_runs(&mut fs).unwrap();
let cached_root = ntfs.root_directory(&mut fs).unwrap();
assert_eq!(cached_root.position(), uncached_position);
assert_eq!(
cached_root.file_record_number(),
KnownNtfsFileRecordNumber::RootDirectory as u64
);
}
#[test]
fn uncached_mft_reads_stitch_file_records_across_data_runs() {
let mut fs = testfs_with_root_record_split_across_mft_runs();
let ntfs = Ntfs::new(&mut fs).unwrap();
let root = ntfs.root_directory(&mut fs).unwrap();
assert_eq!(
root.file_record_number(),
KnownNtfsFileRecordNumber::RootDirectory as u64
);
assert!(root.is_directory());
}
#[test]
fn cached_mft_reads_stitch_file_records_across_data_runs() {
let mut fs = testfs_with_root_record_split_across_mft_runs();
let ntfs = Ntfs::new(&mut fs).unwrap();
ntfs.read_mft_runs(&mut fs).unwrap();
let root = ntfs.root_directory(&mut fs).unwrap();
assert_eq!(
root.file_record_number(),
KnownNtfsFileRecordNumber::RootDirectory as u64
);
assert!(root.is_directory());
}
#[test]
fn reuses_file_record_buffer() {
let mut fs = common::testfs1();
let ntfs = Ntfs::new(&mut fs).unwrap();
ntfs.read_mft_runs(&mut fs).unwrap();
let buffer = Vec::with_capacity(ntfs.file_record_size() as usize);
let original_pointer = buffer.as_ptr();
let root = ntfs
.file_with_buffer(
&mut fs,
KnownNtfsFileRecordNumber::RootDirectory as u64,
buffer,
)
.unwrap();
let buffer = root.into_buffer();
assert_eq!(buffer.as_ptr(), original_pointer);
let volume = ntfs
.file_with_buffer(&mut fs, KnownNtfsFileRecordNumber::Volume as u64, buffer)
.unwrap();
let buffer = volume.into_buffer();
assert_eq!(buffer.as_ptr(), original_pointer);
}
#[test]
fn file_resolver_prepares_caches_and_reuses_its_buffer() {
let mut fs = common::testfs1();
let ntfs = Ntfs::new(&mut fs).unwrap();
let mut resolver = ntfs.file_resolver(&mut fs).unwrap();
assert_eq!(
ntfs.root_directory(&mut fs).unwrap().file_record_number(),
KnownNtfsFileRecordNumber::RootDirectory as u64
);
for file_record_number in [
KnownNtfsFileRecordNumber::RootDirectory as u64,
KnownNtfsFileRecordNumber::Volume as u64,
KnownNtfsFileRecordNumber::RootDirectory as u64,
] {
let resolved_number = resolver
.with_file(&mut fs, file_record_number, |file, _fs| {
file.file_record_number()
})
.unwrap();
assert_eq!(resolved_number, file_record_number);
}
}
#[test]
fn file_resolver_resolves_nested_paths_and_reuses_its_buffer() {
let mut fs = common::testfs1();
let ntfs = Ntfs::new(&mut fs).unwrap();
let mut resolver = ntfs.file_resolver(&mut fs).unwrap();
let file = resolver
.resolve_path(&mut fs, "\\many_subdirs//./256\\")
.unwrap();
assert_eq!(
file.name(&mut fs, None, None).unwrap().unwrap().name(),
"256"
);
let root_record_number = resolver
.with_path(&mut fs, "/", |root, _fs| root.file_record_number())
.unwrap();
assert_eq!(
root_record_number,
KnownNtfsFileRecordNumber::RootDirectory as u64
);
let nested_record_number = resolver
.with_path(&mut fs, "many_subdirs/512", |file, _fs| {
file.file_record_number()
})
.unwrap();
assert_ne!(nested_record_number, root_record_number);
}
#[test]
fn file_resolver_reports_invalid_path_components() {
let mut fs = common::testfs1();
let ntfs = Ntfs::new(&mut fs).unwrap();
let mut resolver = ntfs.file_resolver(&mut fs).unwrap();
let missing = resolver
.resolve_path(&mut fs, "many_subdirs/missing")
.unwrap_err();
assert!(matches!(missing, NtfsError::PathComponentNotFound { .. }));
let not_a_directory = resolver
.resolve_path(&mut fs, "empty-file/child")
.unwrap_err();
assert!(matches!(not_a_directory, NtfsError::NotADirectory { .. }));
}
#[test]
fn parses_mft_file_name_and_standard_information() {
let mut fs = common::testfs1();
let ntfs = Ntfs::new(&mut fs).unwrap();
let mft = ntfs
.file(&mut fs, KnownNtfsFileRecordNumber::MFT as u64)
.unwrap();
let mut attributes = mft.attributes_raw();
let standard_attribute = attributes.next().unwrap().unwrap();
assert_eq!(
standard_attribute.ty().unwrap(),
NtfsAttributeType::StandardInformation
);
assert_eq!(standard_attribute.attribute_length(), 96);
assert_eq!(standard_attribute.value_length(), 72);
standard_attribute
.resident_structured_value::<NtfsStandardInformation>()
.unwrap();
let file_name_attribute = attributes.next().unwrap().unwrap();
assert_eq!(
file_name_attribute.ty().unwrap(),
NtfsAttributeType::FileName
);
assert_eq!(file_name_attribute.attribute_length(), 104);
assert_eq!(file_name_attribute.value_length(), 74);
let file_name = file_name_attribute
.structured_value::<_, NtfsFileName>(&mut fs)
.unwrap();
let creation_time = file_name.creation_time();
assert!(creation_time.nt_timestamp() > NT_TIMESTAMP_2021_01_01);
assert_eq!(creation_time, file_name.modification_time());
assert_eq!(creation_time, file_name.mft_record_modification_time());
assert_eq!(creation_time, file_name.access_time());
assert_eq!(file_name.allocated_size(), file_name.data_size());
assert_eq!(file_name.name_length(), 8);
assert_eq!(file_name.name(), "$MFT");
}
#[test]
fn upcase_table_supports_case_insensitive_lookup_and_empty_data() {
let mut fs = common::testfs1();
let ntfs = Ntfs::new(&mut fs).unwrap();
ntfs.read_upcase_table(&mut fs).unwrap();
let root = ntfs.root_directory(&mut fs).unwrap();
let index = root.directory_index(&mut fs).unwrap();
let mut finder = index.finder();
let entry = NtfsFileNameIndex::find(&mut finder, &ntfs, &mut fs, "EMPTY-FILE")
.unwrap()
.unwrap();
let file = entry.to_file(&ntfs, &mut fs).unwrap();
let item = file.data(&mut fs, "").unwrap().unwrap();
let attribute = item.to_attribute().unwrap();
let mut value = attribute.value(&mut fs).unwrap();
assert_eq!(attribute.value_length(), 0);
assert!(value.is_empty());
assert_eq!(value.read(&mut fs, &mut [0u8; 5]).unwrap(), 0);
}
#[test]
fn converts_public_value_types() {
let known_attribute_types = [
NtfsAttributeType::StandardInformation,
NtfsAttributeType::AttributeList,
NtfsAttributeType::FileName,
NtfsAttributeType::ObjectId,
NtfsAttributeType::SecurityDescriptor,
NtfsAttributeType::VolumeName,
NtfsAttributeType::VolumeInformation,
NtfsAttributeType::Data,
NtfsAttributeType::IndexRoot,
NtfsAttributeType::IndexAllocation,
NtfsAttributeType::Bitmap,
NtfsAttributeType::ReparsePoint,
NtfsAttributeType::EAInformation,
NtfsAttributeType::EA,
NtfsAttributeType::PropertySet,
NtfsAttributeType::LoggedUtilityStream,
NtfsAttributeType::End,
];
for ty in known_attribute_types {
assert_eq!(NtfsAttributeType::try_from(ty as u32), Ok(ty));
}
assert!(NtfsAttributeType::try_from(0).is_err());
assert_eq!(NtfsAttributeType::Data.to_string(), "Data");
for (raw, expected) in [
(0, NtfsFileNamespace::Posix),
(1, NtfsFileNamespace::Win32),
(2, NtfsFileNamespace::Dos),
(3, NtfsFileNamespace::Win32AndDos),
] {
assert_eq!(NtfsFileNamespace::try_from(raw), Ok(expected));
}
assert!(NtfsFileNamespace::try_from(4).is_err());
let guid = NtfsGuid::read_from_bytes(&[
0x0b, 0x77, 0xc8, 0x67, 0xf1, 0x44, 0x0a, 0x41, 0xab, 0x9a, 0xf9, 0xb5, 0x44, 0x6f, 0x13,
0xee,
])
.unwrap();
assert_eq!(guid.to_string(), "67C8770B-44F1-410A-AB9A-F9B5446F13EE");
let mut fs = common::testfs1();
let position = Ntfs::new(&mut fs).unwrap().mft_position();
assert_eq!((position + 0u64).value(), position.value());
assert!((position + u64::MAX).value().is_none());
}
#[test]
fn manual_parameters_report_actionable_errors() {
let mut fs = common::testfs1();
let (cluster_size, sector_size, size, mft_position, file_record_size, serial_number) = {
let ntfs = Ntfs::new(&mut fs).unwrap();
(
ntfs.cluster_size(),
ntfs.sector_size(),
ntfs.size(),
ntfs.mft_position(),
ntfs.file_record_size(),
ntfs.serial_number(),
)
};
assert!(matches!(
Ntfs::new_manual(
&mut fs,
cluster_size,
sector_size,
0,
None,
file_record_size,
Some(serial_number),
),
Err(NtfsError::InvalidFilesystemSize { size: 0 })
));
let mft_offset = mft_position.value().unwrap().get();
assert!(matches!(
Ntfs::new_manual(
&mut fs,
cluster_size,
sector_size,
mft_offset,
Some(mft_position),
file_record_size,
Some(serial_number),
),
Err(NtfsError::MftPositionOutOfBounds { .. })
));
let ntfs = Ntfs::new_manual(
&mut fs,
cluster_size,
sector_size,
size,
None,
file_record_size,
Some(serial_number),
)
.unwrap();
assert!(matches!(
ntfs.file(&mut fs, 0),
Err(NtfsError::MissingMftPosition)
));
let ntfs = Ntfs::new_manual(
&mut fs,
cluster_size,
sector_size,
size,
Some(mft_position),
file_record_size,
Some(serial_number),
)
.unwrap();
assert_eq!(
ntfs.root_directory(&mut fs).unwrap().file_record_number(),
KnownNtfsFileRecordNumber::RootDirectory as u64
);
}