Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .cursorignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
!/external
!/external/refs

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ members = [

[dependencies]
log = { version = "0.4", features = ["release_max_level_debug"] }
encoding = "0.2"
byteorder = "1"
bitflags = "2"
serde = { version = "1", features = ["derive"] }
Expand All @@ -46,6 +45,7 @@ simplelog = { version = "0.12", optional = true }
dialoguer = { version = "0.12", optional = true }
indoc = { version = "2.0", optional = true }
sonic-rs = { version = "0.5.6", optional = true }
utf16-simd = "0.1.0"

[features]
default = ["mft_dump"]
Expand Down
199 changes: 127 additions & 72 deletions src/attribute/header.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,42 @@
use crate::Utf16LeStr;
use crate::attribute::{AttributeDataFlags, MftAttributeType};
use crate::err::{Error, Result};
use crate::utils::read_utf16_string;

use byteorder::{LittleEndian, ReadBytesExt};
use byteorder::{ByteOrder, LittleEndian};
use num_traits::FromPrimitive;
use serde::Serialize;
use std::io::{Read, Seek, SeekFrom};
use std::io;

fn get_slice(buf: &[u8], offset: usize, len: usize) -> io::Result<&[u8]> {
let end = offset
.checked_add(len)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "offset overflow"))?;
buf.get(offset..end)
.ok_or_else(|| io::Error::from(io::ErrorKind::UnexpectedEof))
}

fn read_u8(buf: &[u8], offset: usize) -> io::Result<u8> {
buf.get(offset)
.copied()
.ok_or_else(|| io::Error::from(io::ErrorKind::UnexpectedEof))
}

fn read_u16_le(buf: &[u8], offset: usize) -> io::Result<u16> {
Ok(LittleEndian::read_u16(get_slice(buf, offset, 2)?))
}

fn read_u32_le(buf: &[u8], offset: usize) -> io::Result<u32> {
Ok(LittleEndian::read_u32(get_slice(buf, offset, 4)?))
}

fn read_u64_le(buf: &[u8], offset: usize) -> io::Result<u64> {
Ok(LittleEndian::read_u64(get_slice(buf, offset, 8)?))
}

/// Represents the union defined in
/// <https://docs.microsoft.com/en-us/windows/desktop/devnotes/attribute-record-header>
#[derive(Serialize, Clone, Debug)]
pub struct MftAttributeHeader {
pub struct MftAttributeHeader<'a> {
pub type_code: MftAttributeType,
/// The size of the attribute record, in bytes.
/// This value reflects the required size for the record variant and is always rounded to the nearest quadword boundary.
Expand All @@ -28,7 +54,7 @@ pub struct MftAttributeHeader {
pub data_flags: AttributeDataFlags,
/// The unique instance for this attribute in the file record.
pub instance: u16,
pub name: String,
pub name: Utf16LeStr<'a>,
/// start of the attribute; used for calculating relative offsets
pub start_offset: u64,
}
Expand All @@ -40,72 +66,104 @@ pub enum ResidentialHeader {
NonResident(NonResidentHeader),
}

impl MftAttributeHeader {
/// Tries to read an AttributeHeader from the stream.
/// Will return `None` if the type code is $END.
pub fn from_stream<S: Read + Seek>(stream: &mut S) -> Result<Option<MftAttributeHeader>> {
let attribute_header_start_offset = stream.stream_position()?;

let type_code_value = stream.read_u32::<LittleEndian>()?;

impl<'a> MftAttributeHeader<'a> {
/// Parse an attribute header from an attribute record slice.
///
/// Returns `Ok(None)` if the type code is `$END` (`0xFFFF_FFFF`).
pub fn from_slice(
record: &'a [u8],
attribute_start_offset: u64,
) -> Result<Option<MftAttributeHeader<'a>>> {
let type_code_value = read_u32_le(record, 0)?;
if type_code_value == 0xFFFF_FFFF {
return Ok(None);
}

let type_code = match MftAttributeType::from_u32(type_code_value) {
Some(attribute_type) => attribute_type,
None => {
return Err(Error::UnknownAttributeType {
attribute_type: type_code_value,
});
}
};
let type_code =
MftAttributeType::from_u32(type_code_value).ok_or(Error::UnknownAttributeType {
attribute_type: type_code_value,
})?;

let attribute_size = stream.read_u32::<LittleEndian>()?;
let resident_flag = stream.read_u8()?;
let name_size = stream.read_u8()?;
let name_offset = {
// We always read the two bytes to advance the stream.
let value = stream.read_u16::<LittleEndian>()?;
if name_size > 0 { Some(value) } else { None }
};
let record_length = read_u32_le(record, 4)?;
let form_code = read_u8(record, 8)?;
let name_size = read_u8(record, 9)?;
let name_offset_raw = read_u16_le(record, 10)?;
let name_offset = (name_size > 0).then_some(name_offset_raw);

let data_flags = AttributeDataFlags::from_bits_truncate(read_u16_le(record, 12)?);
let instance = read_u16_le(record, 14)?;

let residential_header = match form_code {
0 => {
let data_size = read_u32_le(record, 16)?;
let data_offset = read_u16_le(record, 20)?;
let index_flag = read_u8(record, 22)?;
let padding = read_u8(record, 23)?;
ResidentialHeader::Resident(ResidentHeader {
data_size,
data_offset,
index_flag,
padding,
})
}
1 => {
let vnc_first = read_u64_le(record, 16)?;
let vnc_last = read_u64_le(record, 24)?;
let datarun_offset = read_u16_le(record, 32)?;
let unit_compression_size = read_u16_le(record, 34)?;
let padding = read_u32_le(record, 36)?;
let allocated_length = read_u64_le(record, 40)?;
let file_size = read_u64_le(record, 48)?;
let valid_data_length = read_u64_le(record, 56)?;

let data_flags = AttributeDataFlags::from_bits_truncate(stream.read_u16::<LittleEndian>()?);
let id = stream.read_u16::<LittleEndian>()?;
let total_allocated = if unit_compression_size > 0 {
Some(read_u64_le(record, 64)?)
} else {
None
};

let residential_header = match resident_flag {
0 => ResidentialHeader::Resident(ResidentHeader::from_stream(stream)?),
1 => ResidentialHeader::NonResident(NonResidentHeader::from_stream(stream)?),
ResidentialHeader::NonResident(NonResidentHeader {
vnc_first,
vnc_last,
datarun_offset,
unit_compression_size,
padding,
allocated_length,
file_size,
valid_data_length,
total_allocated,
})
}
_ => {
return Err(Error::UnhandledResidentFlag {
flag: resident_flag,
offset: stream.stream_position()?,
flag: form_code,
offset: attribute_start_offset,
});
}
};

// Name is optional, and will not be present if size == 0.
let name = if name_size > 0 {
stream.seek(SeekFrom::Start(
attribute_header_start_offset
+ u64::from(name_offset.expect("name_size > 0 is invariant")),
))?;
read_utf16_string(stream, Some(name_size as usize))?
let off = name_offset_raw as usize;
let len_bytes = name_size as usize * 2;
let name_bytes = record
.get(off..off + len_bytes)
.ok_or(Error::InvalidFilename)?;
Utf16LeStr::from_utf16le_bytes_until_nul(name_bytes)
} else {
String::new()
Utf16LeStr::empty()
};

Ok(Some(MftAttributeHeader {
type_code,
record_length: attribute_size,
form_code: resident_flag,
record_length,
form_code,
residential_header,
name_size,
name_offset,
data_flags,
instance: id,
instance,
name,
residential_header,
start_offset: attribute_header_start_offset,
start_offset: attribute_start_offset,
}))
}
}
Expand All @@ -123,10 +181,11 @@ pub struct ResidentHeader {
}

impl ResidentHeader {
pub fn from_stream<R: Read>(reader: &mut R) -> Result<ResidentHeader> {
pub fn from_stream<R: std::io::Read>(reader: &mut R) -> Result<ResidentHeader> {
use byteorder::ReadBytesExt;
Ok(ResidentHeader {
data_size: reader.read_u32::<LittleEndian>()?,
data_offset: reader.read_u16::<LittleEndian>()?,
data_size: reader.read_u32::<byteorder::LittleEndian>()?,
data_offset: reader.read_u16::<byteorder::LittleEndian>()?,
index_flag: reader.read_u8()?,
padding: reader.read_u8()?,
})
Expand Down Expand Up @@ -159,18 +218,19 @@ pub struct NonResidentHeader {
}

impl NonResidentHeader {
pub fn from_stream<R: Read>(reader: &mut R) -> Result<NonResidentHeader> {
let vnc_first = reader.read_u64::<LittleEndian>()?;
let vnc_last = reader.read_u64::<LittleEndian>()?;
let datarun_offset = reader.read_u16::<LittleEndian>()?;
let unit_compression_size = reader.read_u16::<LittleEndian>()?;
let padding = reader.read_u32::<LittleEndian>()?;
let allocated_length = reader.read_u64::<LittleEndian>()?;
let file_size = reader.read_u64::<LittleEndian>()?;
let valid_data_length = reader.read_u64::<LittleEndian>()?;
pub fn from_stream<R: std::io::Read>(reader: &mut R) -> Result<NonResidentHeader> {
use byteorder::ReadBytesExt;
let vnc_first = reader.read_u64::<byteorder::LittleEndian>()?;
let vnc_last = reader.read_u64::<byteorder::LittleEndian>()?;
let datarun_offset = reader.read_u16::<byteorder::LittleEndian>()?;
let unit_compression_size = reader.read_u16::<byteorder::LittleEndian>()?;
let padding = reader.read_u32::<byteorder::LittleEndian>()?;
let allocated_length = reader.read_u64::<byteorder::LittleEndian>()?;
let file_size = reader.read_u64::<byteorder::LittleEndian>()?;
let valid_data_length = reader.read_u64::<byteorder::LittleEndian>()?;

let total_allocated = if unit_compression_size > 0 {
Some(reader.read_u64::<LittleEndian>()?)
Some(reader.read_u64::<byteorder::LittleEndian>()?)
} else {
None
};
Expand All @@ -193,7 +253,6 @@ impl NonResidentHeader {
mod tests {
use super::MftAttributeHeader;
use crate::attribute::MftAttributeType;
use std::io::Cursor;

#[test]
fn attribute_test_01_resident() {
Expand All @@ -202,11 +261,9 @@ mod tests {
0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00,
];

let mut cursor = Cursor::new(raw);

let attribute_header = MftAttributeHeader::from_stream(&mut cursor)
.expect("Should not be $End")
.expect("Shold parse correctly");
let attribute_header = MftAttributeHeader::from_slice(raw, 0)
.expect("Shold parse correctly")
.expect("Should not be $End");

assert_eq!(
attribute_header.type_code,
Expand All @@ -229,11 +286,9 @@ mod tests {
0x0C, 0x32, 0xA0, 0x56, 0xE3, 0xE6, 0x24, 0x00, 0xFF, 0xFF,
];

let mut cursor = Cursor::new(raw);

let attribute_header = MftAttributeHeader::from_stream(&mut cursor)
.expect("Should not be $End")
.expect("Shold parse correctly");
let attribute_header = MftAttributeHeader::from_slice(raw, 0)
.expect("Shold parse correctly")
.expect("Should not be $End");

assert_eq!(attribute_header.type_code, MftAttributeType::DATA);
assert_eq!(attribute_header.record_length, 80);
Expand Down
Loading