use forensicnomicon::olecf as k;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DirEntry {
pub sid: u32,
pub name: String,
pub object_type: u8,
pub color: u8,
pub left: u32,
pub right: u32,
pub child: u32,
pub clsid: [u8; 16],
pub state_bits: u32,
pub create_time: u64,
pub modify_time: u64,
pub start_sector: u32,
pub stream_size: u64,
}
impl DirEntry {
#[must_use]
pub fn is_allocated(&self) -> bool {
matches!(self.object_type, 0x01 | 0x02 | 0x05)
}
#[must_use]
pub fn is_stream(&self) -> bool {
self.object_type == 0x02
}
}
#[derive(Debug, Clone)]
pub struct RawCfb {
pub major_version: u16,
pub sector_shift: u16,
pub mini_sector_shift: u16,
pub mini_stream_cutoff: u32,
pub byte_order: u16,
pub sector_size: usize,
pub first_difat_sector: u32,
pub num_difat_sectors: u32,
pub fat: Vec<u32>,
pub mini_fat: Vec<u32>,
pub dir_entries: Vec<DirEntry>,
pub file_len: u64,
}
const MAX_SECTORS: usize = 16 * 1024 * 1024;
const MAX_DIR_ENTRIES: usize = 4 * 1024 * 1024;
const MAX_CHAIN_STEPS: usize = 32 * 1024 * 1024;
#[inline]
fn le_u16(data: &[u8], off: usize) -> u16 {
let mut b = [0u8; 2];
if let Some(s) = data.get(off..off + 2) {
b.copy_from_slice(s);
}
u16::from_le_bytes(b)
}
#[inline]
fn le_u32(data: &[u8], off: usize) -> u32 {
let mut b = [0u8; 4];
if let Some(s) = data.get(off..off + 4) {
b.copy_from_slice(s);
}
u32::from_le_bytes(b)
}
#[inline]
fn le_u64(data: &[u8], off: usize) -> u64 {
let mut b = [0u8; 8];
if let Some(s) = data.get(off..off + 8) {
b.copy_from_slice(s);
}
u64::from_le_bytes(b)
}
#[must_use]
pub fn sector_offset(sid: u32, sector_shift: u16) -> Option<u64> {
(u64::from(sid))
.checked_add(1)?
.checked_shl(u32::from(sector_shift))
}
#[must_use]
pub fn decode(data: &[u8]) -> Option<RawCfb> {
if data.len() < k::HEADER_SIZE {
return None;
}
if data.get(0..8) != Some(&k::OLECF_SIGNATURE) {
return None;
}
let major_version = le_u16(data, k::MAJOR_VERSION);
let sector_shift = le_u16(data, k::SECTOR_SHIFT);
let mini_sector_shift = le_u16(data, k::MINI_SECTOR_SHIFT);
let mini_stream_cutoff = le_u32(data, k::MINI_STREAM_CUTOFF);
let byte_order = le_u16(data, k::BYTE_ORDER);
let effective_shift = if (9..=20).contains(§or_shift) {
sector_shift
} else {
k::SECTOR_SHIFT_V3
};
let sector_size = 1usize << effective_shift;
let first_dir_sector = le_u32(data, k::FIRST_DIR_SECTOR);
let first_minifat_sector = le_u32(data, k::FIRST_MINIFAT_SECTOR);
let first_difat_sector = le_u32(data, k::FIRST_DIFAT_SECTOR);
let num_difat_sectors = le_u32(data, k::NUM_DIFAT_SECTORS);
let file_len = data.len() as u64;
let fat = read_fat(data, sector_size, first_difat_sector, num_difat_sectors);
let mini_fat = read_chain_table(data, sector_size, &fat, first_minifat_sector);
let dir_entries = read_directory(data, sector_size, &fat, first_dir_sector);
Some(RawCfb {
major_version,
sector_shift,
mini_sector_shift,
mini_stream_cutoff,
byte_order,
sector_size,
first_difat_sector,
num_difat_sectors,
fat,
mini_fat,
dir_entries,
file_len,
})
}
fn sector_slice(data: &[u8], sector_size: usize, sid: u32) -> Option<&[u8]> {
let start = (u64::from(sid) + 1).checked_mul(sector_size as u64)?;
let start = usize::try_from(start).ok()?;
let end = start.checked_add(sector_size)?;
data.get(start..end)
}
fn read_fat(
data: &[u8],
sector_size: usize,
first_difat_sector: u32,
num_difat_sectors: u32,
) -> Vec<u32> {
let entries_per_sector = sector_size / 4;
let mut fat_sector_ids: Vec<u32> = Vec::new();
for i in 0..k::DIFAT_HEADER_COUNT {
let sid = le_u32(data, k::DIFAT_HEADER_OFFSET + i * 4);
if sid <= k::MAXREGSECT {
fat_sector_ids.push(sid);
}
}
let mut difat_sid = first_difat_sector;
let mut steps = 0usize;
let difat_cap = (num_difat_sectors as usize)
.saturating_add(MAX_SECTORS)
.min(MAX_SECTORS);
while difat_sid <= k::MAXREGSECT && steps < difat_cap && steps < MAX_CHAIN_STEPS {
let Some(sector) = sector_slice(data, sector_size, difat_sid) else {
break;
};
if entries_per_sector == 0 {
break; }
for i in 0..(entries_per_sector - 1) {
let sid = le_u32(sector, i * 4);
if sid <= k::MAXREGSECT {
fat_sector_ids.push(sid);
}
}
difat_sid = le_u32(sector, (entries_per_sector - 1) * 4);
steps += 1;
if fat_sector_ids.len() > MAX_SECTORS {
break;
}
}
let mut fat: Vec<u32> = Vec::new();
for sid in fat_sector_ids {
let Some(sector) = sector_slice(data, sector_size, sid) else {
continue;
};
for i in 0..entries_per_sector {
fat.push(le_u32(sector, i * 4));
if fat.len() >= MAX_SECTORS {
return fat;
}
}
}
fat
}
fn read_chain_table(data: &[u8], sector_size: usize, fat: &[u32], first_sid: u32) -> Vec<u32> {
let entries_per_sector = sector_size / 4;
let mut out: Vec<u32> = Vec::new();
let mut sid = first_sid;
let mut seen = 0usize;
let mut visited = vec![false; fat.len()];
while sid <= k::MAXREGSECT && seen < MAX_CHAIN_STEPS {
if let Some(slot) = visited.get_mut(sid as usize) {
if *slot {
break; }
*slot = true;
}
let Some(sector) = sector_slice(data, sector_size, sid) else {
break;
};
for i in 0..entries_per_sector {
out.push(le_u32(sector, i * 4));
if out.len() >= MAX_SECTORS {
return out;
}
}
sid = next_in_fat(fat, sid);
seen += 1;
}
out
}
fn read_directory(
data: &[u8],
sector_size: usize,
fat: &[u32],
first_dir_sector: u32,
) -> Vec<DirEntry> {
let entries_per_sector = sector_size / k::DIR_ENTRY_SIZE;
let mut entries: Vec<DirEntry> = Vec::new();
let mut sid = first_dir_sector;
let mut seen = 0usize;
let mut visited = vec![false; fat.len()];
while sid <= k::MAXREGSECT && seen < MAX_CHAIN_STEPS {
if let Some(slot) = visited.get_mut(sid as usize) {
if *slot {
break; }
*slot = true;
}
let Some(sector) = sector_slice(data, sector_size, sid) else {
break;
};
for i in 0..entries_per_sector {
let base = i * k::DIR_ENTRY_SIZE;
let Some(raw) = sector.get(base..base + k::DIR_ENTRY_SIZE) else {
break; };
let sid_index = entries.len() as u32;
entries.push(parse_dir_entry(raw, sid_index));
if entries.len() >= MAX_DIR_ENTRIES {
return entries;
}
}
sid = next_in_fat(fat, sid);
seen += 1;
}
entries
}
fn next_in_fat(fat: &[u32], sid: u32) -> u32 {
fat.get(sid as usize).copied().unwrap_or(k::ENDOFCHAIN)
}
fn parse_dir_entry(raw: &[u8], sid: u32) -> DirEntry {
let name_len = le_u16(raw, k::NAME_LEN) as usize;
let name = decode_entry_name(raw, name_len);
let mut clsid = [0u8; 16];
if let Some(s) = raw.get(k::CLSID..k::CLSID + 16) {
clsid.copy_from_slice(s);
}
DirEntry {
sid,
name,
object_type: raw.get(k::OBJECT_TYPE).copied().unwrap_or(0),
color: raw.get(k::COLOR).copied().unwrap_or(0),
left: le_u32(raw, k::LEFT_SIBLING),
right: le_u32(raw, k::RIGHT_SIBLING),
child: le_u32(raw, k::CHILD),
clsid,
state_bits: le_u32(raw, k::STATE_BITS),
create_time: le_u64(raw, k::CREATE_TIME),
modify_time: le_u64(raw, k::MODIFY_TIME),
start_sector: le_u32(raw, k::START_SECTOR),
stream_size: le_u64(raw, k::STREAM_SIZE),
}
}
fn decode_entry_name(raw: &[u8], declared_len: usize) -> String {
let byte_len = declared_len.min(k::NAME_LEN);
let chars = (byte_len / 2).saturating_sub(1);
let mut units: Vec<u16> = Vec::with_capacity(chars);
for i in 0..chars {
units.push(le_u16(raw, k::NAME + i * 2));
}
String::from_utf16_lossy(&units)
}
#[must_use]
pub fn reachable_sids(entries: &[DirEntry]) -> Vec<bool> {
let mut reachable = vec![false; entries.len()];
if entries.is_empty() {
return reachable;
}
let mut stack: Vec<u32> = vec![0];
let mut steps = 0usize;
while let Some(sid) = stack.pop() {
steps += 1;
if steps > MAX_DIR_ENTRIES {
break; }
let idx = sid as usize;
let Some(slot) = reachable.get_mut(idx) else {
continue;
};
if *slot {
continue;
}
*slot = true;
let Some(entry) = entries.get(idx) else {
continue; };
for next in [entry.child, entry.left, entry.right] {
if next <= k::MAXREGSECT && (next as usize) < entries.len() {
stack.push(next);
}
}
}
reachable
}