use std::collections::HashMap;
use crate::error::{CascError, Result};
use crate::util::io::{read_le_i32, read_le_u32, read_le_u64};
use super::flags::{ContentFlags, LocaleFlags};
const MFST_MAGIC_BE: u32 = 0x5453464D; const MFST_MAGIC_LE: u32 = 0x4D465354;
#[derive(Debug, Clone)]
pub struct RootEntry {
pub ckey: [u8; 16],
pub content_flags: ContentFlags,
pub locale_flags: LocaleFlags,
pub name_hash: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RootFormat {
Legacy,
MfstV1,
MfstV2,
}
pub struct RootFile {
format: RootFormat,
entries: HashMap<u32, Vec<RootEntry>>,
total_entries: usize,
}
impl RootFile {
pub fn parse(data: &[u8]) -> Result<Self> {
let (format, block_start) = detect_format(data)?;
let mut entries: HashMap<u32, Vec<RootEntry>> = HashMap::new();
let mut total_entries: usize = 0;
let mut pos = block_start;
while pos < data.len() {
let (block_entries, new_pos) = parse_block(data, pos, format)?;
total_entries += block_entries.len();
for (fdid, entry) in block_entries {
entries.entry(fdid).or_default().push(entry);
}
pos = new_pos;
}
Ok(Self {
format,
entries,
total_entries,
})
}
pub fn find_by_fdid(&self, fdid: u32, locale: LocaleFlags) -> Option<&RootEntry> {
self.entries
.get(&fdid)?
.iter()
.find(|e| e.locale_flags.matches(locale))
}
pub fn iter_all(&self) -> impl Iterator<Item = (u32, &RootEntry)> {
self.entries
.iter()
.flat_map(|(fdid, entries)| entries.iter().map(move |entry| (*fdid, entry)))
}
pub fn format(&self) -> RootFormat {
self.format
}
pub fn len(&self) -> usize {
self.total_entries
}
pub fn is_empty(&self) -> bool {
self.total_entries == 0
}
pub fn fdid_count(&self) -> usize {
self.entries.len()
}
}
fn detect_format(data: &[u8]) -> Result<(RootFormat, usize)> {
if data.len() < 4 {
if data.is_empty() {
return Err(CascError::InvalidFormat("root file is empty".to_string()));
}
return Ok((RootFormat::Legacy, 0));
}
let magic = read_le_u32(&data[0..4]);
if magic != MFST_MAGIC_LE && magic != MFST_MAGIC_BE {
return Ok((RootFormat::Legacy, 0));
}
if data.len() < 12 {
return Err(CascError::InvalidFormat(
"MFST header too short".to_string(),
));
}
let field_at_4 = read_le_u32(&data[4..8]);
if field_at_4 == 24 && data.len() >= 24 {
let version = read_le_u32(&data[8..12]);
let format = match version {
1 => RootFormat::MfstV1,
2 => RootFormat::MfstV2,
_ => {
return Err(CascError::UnsupportedVersion(version));
}
};
Ok((format, 24))
} else {
Ok((RootFormat::MfstV1, 12))
}
}
fn parse_block(
data: &[u8],
pos: usize,
format: RootFormat,
) -> Result<(Vec<(u32, RootEntry)>, usize)> {
let (num_records, content_flags, locale_flags, mut pos) =
parse_block_header(data, pos, format)?;
if num_records == 0 {
return Ok((Vec::new(), pos));
}
let num = num_records as usize;
let deltas_size = num * 4;
if pos + deltas_size > data.len() {
return Err(CascError::InvalidFormat(
"root block: not enough data for FileDataID deltas".to_string(),
));
}
let mut fdids = Vec::with_capacity(num);
let mut current_fdid: i64 = 0;
for i in 0..num {
let delta = read_le_i32(&data[pos + i * 4..]) as i64;
if i == 0 {
current_fdid = delta;
} else {
current_fdid = current_fdid + 1 + delta;
}
fdids.push(current_fdid as u32);
}
pos += deltas_size;
let ckeys_size = num * 16;
if pos + ckeys_size > data.len() {
return Err(CascError::InvalidFormat(
"root block: not enough data for content keys".to_string(),
));
}
let mut ckeys = Vec::with_capacity(num);
for i in 0..num {
let mut ckey = [0u8; 16];
ckey.copy_from_slice(&data[pos + i * 16..pos + i * 16 + 16]);
ckeys.push(ckey);
}
pos += ckeys_size;
let has_name_hashes = !content_flags.has_no_name_hash();
let mut name_hashes: Vec<Option<u64>> = Vec::with_capacity(num);
if has_name_hashes {
let hashes_size = num * 8;
if pos + hashes_size > data.len() {
return Err(CascError::InvalidFormat(
"root block: not enough data for name hashes".to_string(),
));
}
for i in 0..num {
name_hashes.push(Some(read_le_u64(&data[pos + i * 8..])));
}
pos += hashes_size;
} else {
name_hashes.resize(num, None);
}
let mut result = Vec::with_capacity(num);
for i in 0..num {
result.push((
fdids[i],
RootEntry {
ckey: ckeys[i],
content_flags,
locale_flags,
name_hash: name_hashes[i],
},
));
}
Ok((result, pos))
}
fn parse_block_header(
data: &[u8],
pos: usize,
format: RootFormat,
) -> Result<(u32, ContentFlags, LocaleFlags, usize)> {
match format {
RootFormat::Legacy | RootFormat::MfstV1 => {
if pos + 12 > data.len() {
return Err(CascError::InvalidFormat(
"root block header v1: not enough data".to_string(),
));
}
let num_records = read_le_u32(&data[pos..]);
let content_flags = ContentFlags(read_le_u32(&data[pos + 4..]));
let locale_flags = LocaleFlags(read_le_u32(&data[pos + 8..]));
Ok((num_records, content_flags, locale_flags, pos + 12))
}
RootFormat::MfstV2 => {
if pos + 17 > data.len() {
return Err(CascError::InvalidFormat(
"root block header v2: not enough data".to_string(),
));
}
let num_records = read_le_u32(&data[pos..]);
let locale_flags = LocaleFlags(read_le_u32(&data[pos + 4..]));
let unk1 = read_le_u32(&data[pos + 8..]);
let unk2 = read_le_u32(&data[pos + 12..]);
let unk3 = data[pos + 16];
let content_flags = ContentFlags(unk1 | unk2 | ((unk3 as u32) << 17));
Ok((num_records, content_flags, locale_flags, pos + 17))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::root::flags::{ContentFlags, LocaleFlags};
type RootBlockEntry = (i32, [u8; 16], Option<u64>);
fn build_root_v1(blocks: &[(u32, u32, Vec<RootBlockEntry>)]) -> Vec<u8> {
let total_count: u32 = blocks.iter().map(|(_, _, e)| e.len() as u32).sum();
let named_count: u32 = blocks
.iter()
.filter(|(cf, _, _)| (cf & 0x10000000) == 0)
.map(|(_, _, e)| e.len() as u32)
.sum();
let mut data = Vec::new();
data.extend_from_slice(&MFST_MAGIC_BE.to_le_bytes()); data.extend_from_slice(&24u32.to_le_bytes()); data.extend_from_slice(&1u32.to_le_bytes()); data.extend_from_slice(&total_count.to_le_bytes()); data.extend_from_slice(&named_count.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes()); assert_eq!(data.len(), 24);
for (content_flags, locale_flags, entries) in blocks {
let num_records = entries.len() as u32;
data.extend_from_slice(&num_records.to_le_bytes());
data.extend_from_slice(&content_flags.to_le_bytes());
data.extend_from_slice(&locale_flags.to_le_bytes());
for (delta, _, _) in entries {
data.extend_from_slice(&delta.to_le_bytes());
}
for (_, ckey, _) in entries {
data.extend_from_slice(ckey);
}
if (content_flags & 0x10000000) == 0 {
for (_, _, name_hash) in entries {
let hash = name_hash.unwrap_or(0);
data.extend_from_slice(&hash.to_le_bytes());
}
}
}
data
}
#[test]
fn detect_mfst_format() {
let data = build_root_v1(&[]);
let root = RootFile::parse(&data).unwrap();
assert_eq!(root.format(), RootFormat::MfstV1);
}
#[test]
fn parse_single_block_single_entry() {
let ckey = [0xAA; 16];
let blocks = vec![(0x8u32, 0x2u32, vec![(100i32, ckey, Some(0xDEADBEEF_u64))])]; let data = build_root_v1(&blocks);
let root = RootFile::parse(&data).unwrap();
assert_eq!(root.len(), 1);
let entry = root.find_by_fdid(100, LocaleFlags::EN_US).unwrap();
assert_eq!(entry.ckey, ckey);
assert_eq!(entry.name_hash, Some(0xDEADBEEF));
}
#[test]
fn parse_fdid_deltas_sequential() {
let blocks = vec![(
0x10000008u32,
0x2u32,
vec![
(100i32, [0x01; 16], None), (0i32, [0x02; 16], None), (0i32, [0x03; 16], None), (2i32, [0x04; 16], None), ],
)];
let data = build_root_v1(&blocks);
let root = RootFile::parse(&data).unwrap();
assert_eq!(root.len(), 4);
assert!(root.find_by_fdid(100, LocaleFlags::ALL).is_some());
assert!(root.find_by_fdid(101, LocaleFlags::ALL).is_some());
assert!(root.find_by_fdid(102, LocaleFlags::ALL).is_some());
assert!(root.find_by_fdid(103, LocaleFlags::ALL).is_none()); assert!(root.find_by_fdid(104, LocaleFlags::ALL).is_none()); assert!(root.find_by_fdid(105, LocaleFlags::ALL).is_some());
}
#[test]
fn parse_block_with_name_hashes() {
let blocks = vec![(
0x8u32,
0x2u32,
vec![(50i32, [0xBB; 16], Some(0x1234567890ABCDEF_u64))],
)]; let data = build_root_v1(&blocks);
let root = RootFile::parse(&data).unwrap();
let entry = root.find_by_fdid(50, LocaleFlags::ALL).unwrap();
assert_eq!(entry.name_hash, Some(0x1234567890ABCDEF));
}
#[test]
fn parse_block_without_name_hashes() {
let blocks = vec![(0x10000008u32, 0x2u32, vec![(50i32, [0xCC; 16], None)])]; let data = build_root_v1(&blocks);
let root = RootFile::parse(&data).unwrap();
let entry = root.find_by_fdid(50, LocaleFlags::ALL).unwrap();
assert_eq!(entry.name_hash, None);
}
#[test]
fn parse_multiple_blocks_different_locales() {
let blocks = vec![
(0x8u32, 0x2u32, vec![(100i32, [0x01; 16], Some(0))]), (0x8u32, 0x20u32, vec![(100i32, [0x02; 16], Some(0))]), ];
let data = build_root_v1(&blocks);
let root = RootFile::parse(&data).unwrap();
let en = root.find_by_fdid(100, LocaleFlags::EN_US).unwrap();
assert_eq!(en.ckey, [0x01; 16]);
let de = root.find_by_fdid(100, LocaleFlags::DE_DE).unwrap();
assert_eq!(de.ckey, [0x02; 16]);
}
#[test]
fn parse_locale_filter() {
let blocks = vec![(0x8u32, 0x20u32, vec![(200i32, [0xFF; 16], Some(0))])]; let data = build_root_v1(&blocks);
let root = RootFile::parse(&data).unwrap();
assert!(root.find_by_fdid(200, LocaleFlags::EN_US).is_none()); assert!(root.find_by_fdid(200, LocaleFlags::DE_DE).is_some()); assert!(root.find_by_fdid(200, LocaleFlags::ALL).is_some()); }
#[test]
fn iter_all_entries() {
let blocks = vec![(
0x10000008u32,
0x2u32,
vec![(10i32, [0x01; 16], None), (0i32, [0x02; 16], None)],
)];
let data = build_root_v1(&blocks);
let root = RootFile::parse(&data).unwrap();
let all: Vec<_> = root.iter_all().collect();
assert_eq!(all.len(), 2);
}
#[test]
fn parse_empty_root() {
let data = build_root_v1(&[]);
let root = RootFile::parse(&data).unwrap();
assert!(root.is_empty());
assert_eq!(root.fdid_count(), 0);
}
#[test]
fn detect_legacy_format() {
let mut data = Vec::new();
data.extend_from_slice(&1u32.to_le_bytes());
data.extend_from_slice(&0x10000008u32.to_le_bytes());
data.extend_from_slice(&0x2u32.to_le_bytes());
data.extend_from_slice(&42i32.to_le_bytes());
data.extend_from_slice(&[0xDD; 16]);
let root = RootFile::parse(&data).unwrap();
assert_eq!(root.format(), RootFormat::Legacy);
assert_eq!(root.len(), 1);
assert!(root.find_by_fdid(42, LocaleFlags::ALL).is_some());
}
#[test]
fn detect_pre_1017_mfst() {
let mut data = Vec::new();
data.extend_from_slice(&MFST_MAGIC_BE.to_le_bytes()); data.extend_from_slice(&500000u32.to_le_bytes()); data.extend_from_slice(&400000u32.to_le_bytes());
data.extend_from_slice(&1u32.to_le_bytes()); data.extend_from_slice(&0x10000008u32.to_le_bytes()); data.extend_from_slice(&0x2u32.to_le_bytes()); data.extend_from_slice(&7i32.to_le_bytes()); data.extend_from_slice(&[0xEE; 16]);
let root = RootFile::parse(&data).unwrap();
assert_eq!(root.format(), RootFormat::MfstV1);
assert_eq!(root.len(), 1);
assert!(root.find_by_fdid(7, LocaleFlags::ALL).is_some());
}
#[test]
fn mfst_v2_block_header() {
let mut data = Vec::new();
data.extend_from_slice(&MFST_MAGIC_BE.to_le_bytes());
data.extend_from_slice(&24u32.to_le_bytes()); data.extend_from_slice(&2u32.to_le_bytes()); data.extend_from_slice(&1u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&1u32.to_le_bytes()); data.extend_from_slice(&0x2u32.to_le_bytes()); data.extend_from_slice(&0x8u32.to_le_bytes()); data.extend_from_slice(&0x10000000u32.to_le_bytes()); data.push(0);
data.extend_from_slice(&99i32.to_le_bytes());
data.extend_from_slice(&[0xAB; 16]);
let root = RootFile::parse(&data).unwrap();
assert_eq!(root.format(), RootFormat::MfstV2);
assert_eq!(root.len(), 1);
let entry = root.find_by_fdid(99, LocaleFlags::EN_US).unwrap();
assert_eq!(entry.ckey, [0xAB; 16]);
assert!(entry.content_flags.has(ContentFlags::LOAD_ON_WINDOWS));
assert!(entry.content_flags.has_no_name_hash());
assert_eq!(entry.name_hash, None);
}
#[test]
fn parse_error_on_empty_data() {
let result = RootFile::parse(&[]);
assert!(result.is_err());
}
#[test]
fn parse_error_on_truncated_block() {
let mut data = Vec::new();
data.extend_from_slice(&MFST_MAGIC_BE.to_le_bytes());
data.extend_from_slice(&24u32.to_le_bytes());
data.extend_from_slice(&1u32.to_le_bytes());
data.extend_from_slice(&1u32.to_le_bytes());
data.extend_from_slice(&1u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&1000u32.to_le_bytes());
data.extend_from_slice(&0x8u32.to_le_bytes());
data.extend_from_slice(&0x2u32.to_le_bytes());
let result = RootFile::parse(&data);
assert!(result.is_err());
}
#[test]
fn fdid_count_vs_len() {
let blocks = vec![
(0x8u32, 0x2u32, vec![(50i32, [0x01; 16], Some(0))]),
(0x8u32, 0x20u32, vec![(50i32, [0x02; 16], Some(0))]),
];
let data = build_root_v1(&blocks);
let root = RootFile::parse(&data).unwrap();
assert_eq!(root.len(), 2);
assert_eq!(root.fdid_count(), 1);
}
}