use wtf_string::Wtf16String;
use crate::entry::{EntryFields, FileIdentity};
use crate::error::MalformedRecord;
use crate::request::RECORD_ALIGNMENT;
use crate::timestamp::WindowsFileTimestamp;
mod field {
pub(super) const NEXT_ENTRY_OFFSET: usize = 0;
pub(super) const CREATION_TIME: usize = 8;
pub(super) const LAST_ACCESS_TIME: usize = 16;
pub(super) const LAST_WRITE_TIME: usize = 24;
pub(super) const CHANGE_TIME: usize = 32;
pub(super) const END_OF_FILE: usize = 40;
pub(super) const ALLOCATION_SIZE: usize = 48;
pub(super) const FILE_ATTRIBUTES: usize = 56;
pub(super) const FILE_NAME_LENGTH: usize = 60;
pub(super) const EA_SIZE: usize = 64;
pub(super) const REPARSE_POINT_TAG: usize = 68;
pub(super) const FILE_ID: usize = 72;
}
const FIXED_FIELDS_LEN: usize = 88;
const DOT: u16 = 0x002E;
#[derive(Debug)]
pub(crate) struct ParsedRecord {
name: Wtf16String,
attributes: u32,
logical_size: u64,
allocation_size: u64,
extended_attribute_size: u32,
creation_time: WindowsFileTimestamp,
last_access_time: WindowsFileTimestamp,
last_write_time: WindowsFileTimestamp,
change_time: WindowsFileTimestamp,
reparse_tag: u32,
file_id: [u8; 16],
}
impl ParsedRecord {
#[must_use]
pub(crate) fn is_dot_or_dotdot(&self) -> bool {
matches!(self.name.as_units(), [DOT] | [DOT, DOT])
}
#[must_use]
pub(crate) fn into_fields(self, volume_serial: Option<u64>) -> EntryFields {
EntryFields {
name: self.name,
attributes: self.attributes,
logical_size: self.logical_size,
allocation_size: self.allocation_size,
extended_attribute_size: self.extended_attribute_size,
creation_time: self.creation_time,
last_access_time: self.last_access_time,
last_write_time: self.last_write_time,
change_time: self.change_time,
reparse_tag: self.reparse_tag,
identity: FileIdentity::new(self.file_id, volume_serial),
}
}
}
fn read_u32(bytes: &[u8], at: usize) -> u32 {
u32::from_ne_bytes(
bytes[at..at + size_of::<u32>()]
.try_into()
.expect("caller validated the fixed-field extent"),
)
}
fn read_i64(bytes: &[u8], at: usize) -> i64 {
i64::from_ne_bytes(
bytes[at..at + size_of::<i64>()]
.try_into()
.expect("caller validated the fixed-field extent"),
)
}
fn decode_name(bytes: &[u8]) -> Wtf16String {
let units: Vec<u16> = bytes
.as_chunks::<2>()
.0
.iter()
.map(|pair| u16::from_ne_bytes(*pair))
.collect();
Wtf16String::from_units(&units)
}
pub(crate) fn parse_record(
bytes: &[u8],
record_start: usize,
) -> Result<(ParsedRecord, Option<usize>), MalformedRecord> {
if !record_start.is_multiple_of(RECORD_ALIGNMENT) {
return Err(MalformedRecord::Alignment);
}
let remaining = bytes.len() - record_start;
if remaining < FIXED_FIELDS_LEN {
return Err(MalformedRecord::TruncatedFixedFields);
}
let next_entry_offset = read_u32(bytes, record_start + field::NEXT_ENTRY_OFFSET);
let name_length = read_u32(bytes, record_start + field::FILE_NAME_LENGTH);
if !name_length.is_multiple_of(2) {
return Err(MalformedRecord::OddNameLength);
}
let name_start = record_start + FIXED_FIELDS_LEN;
let name_end = name_start
.checked_add(name_length as usize)
.filter(|&end| end <= bytes.len())
.ok_or(MalformedRecord::NameOutOfBounds)?;
let next_record_start = if next_entry_offset == 0 {
None
} else {
let candidate = record_start
.checked_add(next_entry_offset as usize)
.filter(|&candidate| candidate >= name_end && candidate <= bytes.len())
.ok_or(MalformedRecord::NextEntryOffset)?;
Some(candidate)
};
let end_of_file = read_i64(bytes, record_start + field::END_OF_FILE);
let allocation_size = read_i64(bytes, record_start + field::ALLOCATION_SIZE);
let logical_size = u64::try_from(end_of_file).map_err(|_| MalformedRecord::NegativeSize)?;
let allocation_size =
u64::try_from(allocation_size).map_err(|_| MalformedRecord::NegativeSize)?;
let creation_time = read_i64(bytes, record_start + field::CREATION_TIME);
let last_access_time = read_i64(bytes, record_start + field::LAST_ACCESS_TIME);
let last_write_time = read_i64(bytes, record_start + field::LAST_WRITE_TIME);
let change_time = read_i64(bytes, record_start + field::CHANGE_TIME);
let attributes = read_u32(bytes, record_start + field::FILE_ATTRIBUTES);
let extended_attribute_size = read_u32(bytes, record_start + field::EA_SIZE);
let reparse_tag = read_u32(bytes, record_start + field::REPARSE_POINT_TAG);
let mut file_id = [0u8; 16];
file_id
.copy_from_slice(&bytes[record_start + field::FILE_ID..record_start + field::FILE_ID + 16]);
let name = decode_name(&bytes[name_start..name_end]);
let record = ParsedRecord {
name,
attributes,
logical_size,
allocation_size,
extended_attribute_size,
creation_time: WindowsFileTimestamp::from_ticks(creation_time),
last_access_time: WindowsFileTimestamp::from_ticks(last_access_time),
last_write_time: WindowsFileTimestamp::from_ticks(last_write_time),
change_time: WindowsFileTimestamp::from_ticks(change_time),
reparse_tag,
file_id,
};
Ok((record, next_record_start))
}
#[cfg(test)]
mod tests;