use crate::{Error, Result, ioutils::ReadInt, utils::jenkins3_hashpath};
use modular_bitfield::{bitfield, prelude::*};
use std::{
collections::{BTreeMap, HashMap},
fmt::Debug,
io::{ErrorKind, Read, Seek},
ops::BitAnd,
};
const TACT_MAGIC: &[u8; 4] = b"TSFM";
const MD5_LENGTH: usize = 16;
pub type Md5 = [u8; MD5_LENGTH];
#[derive(Debug)]
pub struct WowRootHeader {
pub use_old_record_format: bool,
pub version: u32,
pub total_file_count: u32,
pub named_file_count: u32,
pub allow_non_named_files: bool,
}
impl WowRootHeader {
pub fn parse<R: Read + Seek>(f: &mut R) -> Result<Self> {
let mut magic = [0; TACT_MAGIC.len()];
f.read_exact(&mut magic)?;
if &magic != TACT_MAGIC {
f.seek_relative(-(TACT_MAGIC.len() as i64))?;
return Ok(Self {
use_old_record_format: true,
version: 0,
total_file_count: 0,
named_file_count: 0,
allow_non_named_files: true,
});
}
let mut header_size = f.read_u32le()?;
let mut version = 0;
let total_file_count;
if header_size == 0x18 {
version = f.read_u32le()?;
total_file_count = f.read_u32le()?;
} else {
total_file_count = header_size;
header_size = 0;
}
let named_file_count = f.read_u32le()?;
if header_size == 0x18 {
f.seek_relative(4)?;
}
Ok(Self {
use_old_record_format: false,
allow_non_named_files: total_file_count != named_file_count,
version,
total_file_count,
named_file_count,
})
}
}
pub struct CasBlock {
pub flags: LocaleContentFlags,
pub fid_md5: Option<Vec<(u32, Md5)>>,
pub name_hash_fid: Option<Vec<(u64, u32)>>,
}
impl std::fmt::Debug for CasBlock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CasBlock")
.field("context", &self.flags)
.field("fid_md5.len", &self.fid_md5.as_ref().map(|v| v.len()))
.field(
"name_hash_fid.len",
&self.name_hash_fid.as_ref().map(|v| v.len()),
)
.finish()
}
}
impl CasBlock {
pub fn parse<R: Read + Seek>(
f: &mut R,
header: &WowRootHeader,
only_locale: LocaleFlags,
) -> Result<Self> {
let num_records = f.read_u32le()? as usize;
let flags = if header.version == 2 {
let locale = LocaleFlags::from(f.read_u32le()?);
let v1 = f.read_u32le()?;
let v2 = f.read_u32le()?;
let v3 = f.read_u8()?;
LocaleContentFlags {
locale,
content: ContentFlags::from(v1 | v2 | (u32::from(v3) << 17)),
}
} else {
LocaleContentFlags {
content: ContentFlags::from(f.read_u32le()?),
locale: LocaleFlags::from(f.read_u32le()?),
}
};
if num_records == 0 {
return Ok(Self {
flags,
fid_md5: None,
name_hash_fid: None,
});
}
let has_name_hashes = header.use_old_record_format
|| !(header.allow_non_named_files && flags.content.no_name_hash());
if !flags.locale.all() && !(flags.locale & only_locale).any() {
let record_length =
size_of::<u32>() + MD5_LENGTH + if has_name_hashes { size_of::<u64>() } else { 0 };
f.seek_relative((num_records * record_length) as i64)?;
return Ok(Self {
flags,
fid_md5: None,
name_hash_fid: None,
});
}
let mut file_ids: Vec<u32> = Vec::with_capacity(num_records);
let mut file_id = 0u32;
for i in 0..num_records {
let delta = f.read_i32le()?;
file_id = if i == 0 {
u32::try_from(delta).map_err(|_| Error::FileIdDeltaOverflow)?
} else {
(file_id)
.checked_add_signed(1 + delta)
.ok_or(Error::FileIdDeltaOverflow)?
};
file_ids.push(file_id);
}
let mut fid_md5: Vec<(u32, Md5)> = Vec::with_capacity(num_records);
let mut name_hash_fid: Option<Vec<(u64, u32)>> = None;
if header.use_old_record_format {
let mut o = Vec::with_capacity(num_records);
for file_id in file_ids {
let mut md5 = [0; MD5_LENGTH];
f.read_exact(&mut md5)?;
fid_md5.push((file_id, md5));
o.push((f.read_u64le()?, file_id));
}
name_hash_fid = Some(o);
} else {
for &file_id in file_ids.iter() {
let mut md5 = [0; MD5_LENGTH];
f.read_exact(&mut md5)?;
fid_md5.push((file_id, md5));
}
if has_name_hashes {
let mut o = Vec::with_capacity(num_records);
for &file_id in file_ids.iter() {
let hash = f.read_u64le()?;
o.push((hash, file_id));
}
name_hash_fid = Some(o);
}
}
Ok(Self {
flags,
fid_md5: Some(fid_md5),
name_hash_fid,
})
}
}
#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub struct LocaleContentFlags {
pub locale: LocaleFlags,
pub content: ContentFlags,
}
#[bitfield(bytes = 4)]
#[derive(PartialEq, Eq, Debug, Copy, Clone, Hash, PartialOrd, Ord)]
#[repr(u32)]
pub struct LocaleFlags {
#[skip]
__: B1,
pub en_us: bool, #[skip]
__: B1,
pub ko_kr: bool,
pub fr_fr: bool, pub de_de: bool, pub zh_cn: bool, pub es_es: bool,
pub zh_tw: bool, pub en_gb: bool, pub en_cn: bool, pub en_tw: bool,
pub es_mx: bool, pub ru_ru: bool, pub pt_br: bool, pub it_it: bool,
pub pt_pt: bool, #[skip]
__: B15,
}
impl LocaleFlags {
pub fn any_locale() -> Self {
LocaleFlags::from(0xffffffff)
}
pub fn all(&self) -> bool {
self == &Self::any_locale()
}
pub fn any(&self) -> bool {
u32::from(*self) != 0
}
}
impl BitAnd for LocaleFlags {
type Output = LocaleFlags;
fn bitand(self, rhs: Self) -> Self::Output {
Self::from(u32::from(self) & u32::from(rhs))
}
}
#[bitfield(bytes = 4)]
#[derive(PartialEq, Eq, Debug, Copy, Clone, Hash, PartialOrd, Ord)]
#[repr(u32)]
pub struct ContentFlags {
pub high_res_texture: bool, #[skip]
__: B1,
pub install: bool, pub windows: bool,
pub macos: bool, pub x86_32: bool, pub x86_64: bool, pub low_violence: bool,
pub mystery_platform: bool, #[skip]
__: B2,
pub update_plugin: bool,
#[skip]
__: B3,
pub aarch64: bool,
#[skip]
__: B11,
pub encrypted: bool,
pub no_name_hash: bool, pub uncommon_resolution: bool, pub bundle: bool, pub no_compression: bool, }
pub struct WowRoot {
pub fid_md5: BTreeMap<u32, BTreeMap<LocaleContentFlags, Md5>>,
pub name_hash_fid: HashMap<u64, u32>,
}
impl WowRoot {
pub fn parse<R: Read + Seek>(f: &mut R, only_locale: LocaleFlags) -> Result<Self> {
let header = WowRootHeader::parse(f)?;
let mut o = Self {
fid_md5: BTreeMap::new(),
name_hash_fid: HashMap::new(),
};
loop {
match CasBlock::parse(f, &header, only_locale) {
Ok(block) => {
if let Some(fid_md5) = block.fid_md5 {
for (k, v) in fid_md5 {
if let Some(e) = o.fid_md5.get_mut(&k) {
assert!(e.insert(block.flags, v).is_none());
} else {
o.fid_md5.insert(k, BTreeMap::from([(block.flags, v)]));
}
}
}
if let Some(name_hash_fid) = block.name_hash_fid {
for (k, v) in name_hash_fid {
o.name_hash_fid.entry(k).or_insert(v);
}
}
}
Err(Error::IOError(e)) if e.kind() == ErrorKind::UnexpectedEof => {
break;
}
Err(e) => return Err(e),
}
}
Ok(o)
}
pub fn get_fid(&self, path: &str) -> Option<u32> {
let hash = jenkins3_hashpath(path);
self.name_hash_fid.get(&hash).copied()
}
}
impl Debug for WowRoot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WowRoot")
.field("fid_md5.len", &self.fid_md5.len())
.field("name_hash_fid.len", &self.name_hash_fid.len())
.finish()
}
}