use crate::error::{CascError, Result};
use crate::types::EKey;
use parking_lot::RwLock;
use std::collections::HashMap;
use std::io::{Cursor, Read};
use std::path::Path;
use std::sync::Arc;
use tact_parser::{
encoding::EncodingFile,
wow_root::{ContentFlags, LocaleFlags, WowRoot},
};
use tracing::{debug, info};
#[derive(Debug, Clone)]
pub struct ManifestConfig {
pub locale: LocaleFlags,
pub content_flags: Option<ContentFlags>,
pub cache_manifests: bool,
pub lazy_loading: bool,
pub lazy_cache_limit: usize,
}
impl Default for ManifestConfig {
fn default() -> Self {
Self {
locale: LocaleFlags::any_locale(),
content_flags: None,
cache_manifests: true,
lazy_loading: true,
lazy_cache_limit: 10_000,
}
}
}
#[derive(Debug, Clone)]
pub struct FileMapping {
pub file_data_id: u32,
pub content_key: [u8; 16],
pub encoding_key: Option<EKey>,
pub flags: Option<ContentFlags>,
}
#[allow(dead_code)] struct LazyRootManifest {
data: Vec<u8>,
fdid_cache: HashMap<
u32,
std::collections::BTreeMap<tact_parser::wow_root::LocaleContentFlags, [u8; 16]>,
>,
hash_cache: HashMap<u64, u32>,
config: ManifestConfig,
approx_file_count: u32,
}
#[allow(dead_code)] struct LazyEncodingManifest {
data: Vec<u8>,
ckey_cache: HashMap<Vec<u8>, tact_parser::encoding::EncodingEntry>,
ekey_cache: HashMap<Vec<u8>, Vec<u8>>,
cache_limit: usize,
}
pub struct TactManifests {
config: ManifestConfig,
root: Arc<RwLock<Option<WowRoot>>>,
lazy_root: Arc<RwLock<Option<LazyRootManifest>>>,
encoding: Arc<RwLock<Option<EncodingFile>>>,
lazy_encoding: Arc<RwLock<Option<LazyEncodingManifest>>>,
fdid_cache: Arc<RwLock<HashMap<u32, FileMapping>>>,
filename_cache: Arc<RwLock<HashMap<String, u32>>>,
}
impl TactManifests {
pub fn new(config: ManifestConfig) -> Self {
Self {
config,
root: Arc::new(RwLock::new(None)),
lazy_root: Arc::new(RwLock::new(None)),
encoding: Arc::new(RwLock::new(None)),
lazy_encoding: Arc::new(RwLock::new(None)),
fdid_cache: Arc::new(RwLock::new(HashMap::new())),
filename_cache: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn load_root_from_data(&self, data: Vec<u8>) -> Result<()> {
info!("Loading root manifest from data ({} bytes)", data.len());
if self.config.lazy_loading {
return self.load_root_lazy(data);
}
let decompressed = if data.starts_with(b"BLTE") {
debug!("Root manifest is BLTE compressed, decompressing with streaming");
use std::io::{Cursor, Read};
let cursor = Cursor::new(data);
let mut stream = blte::create_streaming_reader(cursor, None)
.map_err(|e| CascError::DecompressionError(e.to_string()))?;
let mut result = Vec::new();
stream
.read_to_end(&mut result)
.map_err(|e| CascError::DecompressionError(e.to_string()))?;
result
} else {
data
};
let mut cursor = Cursor::new(decompressed);
let root = WowRoot::parse(&mut cursor, self.config.locale)
.map_err(|e| CascError::InvalidFormat(format!("Failed to parse root: {e}")))?;
info!(
"Loaded root manifest: {} FileDataIDs, {} name hashes",
root.fid_md5.len(),
root.name_hash_fid.len()
);
*self.root.write() = Some(root);
self.fdid_cache.write().clear();
Ok(())
}
fn load_root_lazy(&self, data: Vec<u8>) -> Result<()> {
info!(
"Loading root manifest with lazy loading ({} bytes)",
data.len()
);
let decompressed = if data.starts_with(b"BLTE") {
debug!("Root manifest is BLTE compressed, decompressing");
use std::io::{Cursor, Read};
let cursor = Cursor::new(&data);
let mut stream = blte::create_streaming_reader(cursor, None)
.map_err(|e| CascError::DecompressionError(e.to_string()))?;
let mut result = Vec::new();
stream
.read_to_end(&mut result)
.map_err(|e| CascError::DecompressionError(e.to_string()))?;
result
} else {
data
};
let mut cursor = Cursor::new(&decompressed);
let header = tact_parser::wow_root::WowRootHeader::parse(&mut cursor)
.map_err(|e| CascError::InvalidFormat(format!("Failed to parse root header: {e}")))?;
info!(
"Parsed root header for lazy loading: {} total files, {} named files",
header.total_file_count, header.named_file_count
);
let lazy_manifest = LazyRootManifest {
data: decompressed,
fdid_cache: HashMap::new(),
hash_cache: HashMap::new(),
config: self.config.clone(),
approx_file_count: header.total_file_count,
};
*self.lazy_root.write() = Some(lazy_manifest);
self.fdid_cache.write().clear();
*self.root.write() = None;
Ok(())
}
pub fn load_encoding_from_data(&self, data: Vec<u8>) -> Result<()> {
info!("Loading encoding manifest from data ({} bytes)", data.len());
if self.config.lazy_loading {
return self.load_encoding_lazy(data);
}
let decompressed = if data.starts_with(b"BLTE") {
debug!("Encoding manifest is BLTE compressed, decompressing with streaming");
use std::io::{Cursor, Read};
let cursor = Cursor::new(data);
let mut stream = blte::create_streaming_reader(cursor, None)
.map_err(|e| CascError::DecompressionError(e.to_string()))?;
let mut result = Vec::new();
stream
.read_to_end(&mut result)
.map_err(|e| CascError::DecompressionError(e.to_string()))?;
result
} else {
data
};
let encoding = EncodingFile::parse(&decompressed)
.map_err(|e| CascError::InvalidFormat(format!("Failed to parse encoding: {e}")))?;
info!(
"Loaded encoding manifest: {} CKey entries",
encoding.ckey_count()
);
*self.encoding.write() = Some(encoding);
self.fdid_cache.write().clear();
Ok(())
}
fn load_encoding_lazy(&self, data: Vec<u8>) -> Result<()> {
info!(
"Loading encoding manifest with lazy loading ({} bytes)",
data.len()
);
let decompressed = if data.starts_with(b"BLTE") {
debug!("Encoding manifest is BLTE compressed, decompressing");
use std::io::{Cursor, Read};
let cursor = Cursor::new(&data);
let mut stream = blte::create_streaming_reader(cursor, None)
.map_err(|e| CascError::DecompressionError(e.to_string()))?;
let mut result = Vec::new();
stream
.read_to_end(&mut result)
.map_err(|e| CascError::DecompressionError(e.to_string()))?;
result
} else {
data
};
info!(
"Stored encoding manifest data for lazy loading ({} bytes)",
decompressed.len()
);
let lazy_manifest = LazyEncodingManifest {
data: decompressed,
ckey_cache: HashMap::new(),
ekey_cache: HashMap::new(),
cache_limit: self.config.lazy_cache_limit,
};
*self.lazy_encoding.write() = Some(lazy_manifest);
self.fdid_cache.write().clear();
*self.encoding.write() = None;
Ok(())
}
pub fn load_root_from_reader<R: std::io::Read + std::io::Seek>(
&self,
mut reader: R,
) -> Result<()> {
info!("Loading root manifest from streaming reader");
let mut magic = [0u8; 4];
reader.read_exact(&mut magic)?;
let root = if &magic == b"BLTE" {
debug!("Root manifest is BLTE compressed, decompressing with streaming");
reader.seek(std::io::SeekFrom::Start(0))?;
let mut blte_stream = blte::create_streaming_reader(reader, None)
.map_err(|e| CascError::DecompressionError(e.to_string()))?;
let mut decompressed = Vec::new();
blte_stream
.read_to_end(&mut decompressed)
.map_err(|e| CascError::DecompressionError(e.to_string()))?;
let mut cursor = Cursor::new(decompressed);
WowRoot::parse(&mut cursor, self.config.locale)
.map_err(|e| CascError::InvalidFormat(format!("Failed to parse root: {e}")))?
} else {
debug!("Root manifest is uncompressed, parsing directly");
reader.seek(std::io::SeekFrom::Start(0))?;
WowRoot::parse(&mut reader, self.config.locale)
.map_err(|e| CascError::InvalidFormat(format!("Failed to parse root: {e}")))?
};
info!(
"Loaded root manifest: {} FileDataIDs, {} name hashes",
root.fid_md5.len(),
root.name_hash_fid.len()
);
*self.root.write() = Some(root);
self.fdid_cache.write().clear();
Ok(())
}
pub fn load_encoding_from_reader<R: std::io::Read + std::io::Seek>(
&self,
mut reader: R,
) -> Result<()> {
info!("Loading encoding manifest from streaming reader");
let mut magic = [0u8; 4];
reader.read_exact(&mut magic)?;
let encoding = if &magic == b"BLTE" {
debug!("Encoding manifest is BLTE compressed, decompressing with streaming");
reader.seek(std::io::SeekFrom::Start(0))?;
let mut blte_stream = blte::create_streaming_reader(reader, None)
.map_err(|e| CascError::DecompressionError(e.to_string()))?;
let mut decompressed = Vec::new();
blte_stream
.read_to_end(&mut decompressed)
.map_err(|e| CascError::DecompressionError(e.to_string()))?;
EncodingFile::parse(&decompressed)
.map_err(|e| CascError::InvalidFormat(format!("Failed to parse encoding: {e}")))?
} else {
debug!("Encoding manifest is uncompressed");
reader.seek(std::io::SeekFrom::Start(0))?;
let mut data = Vec::new();
reader.read_to_end(&mut data)?;
EncodingFile::parse(&data)
.map_err(|e| CascError::InvalidFormat(format!("Failed to parse encoding: {e}")))?
};
info!(
"Loaded encoding manifest: {} CKey entries",
encoding.ckey_count()
);
*self.encoding.write() = Some(encoding);
self.fdid_cache.write().clear();
Ok(())
}
pub fn load_root_from_file(&self, path: &Path) -> Result<()> {
info!("Loading root manifest from file: {:?}", path);
let file = std::fs::File::open(path)?;
self.load_root_from_reader(file)
}
pub fn load_encoding_from_file(&self, path: &Path) -> Result<()> {
info!("Loading encoding manifest from file: {:?}", path);
let file = std::fs::File::open(path)?;
self.load_encoding_from_reader(file)
}
pub fn load_listfile(&self, path: &Path) -> Result<usize> {
info!("Loading listfile from: {:?}", path);
let file = std::fs::File::open(path)?;
self.load_listfile_from_reader(file)
}
pub fn load_listfile_from_reader<R: std::io::Read>(&self, reader: R) -> Result<usize> {
info!("Loading listfile from streaming reader");
let mut cache = self.filename_cache.write();
cache.clear();
use std::io::{BufRead, BufReader};
let buf_reader = BufReader::new(reader);
let mut count = 0;
for line_result in buf_reader.lines() {
let line = line_result?;
if let Some(sep_pos) = line.find(';') {
if let Ok(fdid) = line[..sep_pos].parse::<u32>() {
let filename = line[sep_pos + 1..].to_string();
cache.insert(filename, fdid);
count += 1;
}
}
}
info!("Loaded {} filename mappings from listfile", count);
Ok(count)
}
pub fn lookup_by_fdid(&self, fdid: u32) -> Result<FileMapping> {
{
let cache = self.fdid_cache.read();
if let Some(mapping) = cache.get(&fdid) {
return Ok(mapping.clone());
}
}
if self.config.lazy_loading {
if let Some(result) = self.lookup_fdid_lazy(fdid)? {
return Ok(result);
}
}
let root = self.root.read();
let encoding = self.encoding.read();
let root = root
.as_ref()
.ok_or_else(|| CascError::ManifestNotLoaded("root".to_string()))?;
let encoding = encoding
.as_ref()
.ok_or_else(|| CascError::ManifestNotLoaded("encoding".to_string()))?;
let content_entries = root
.fid_md5
.get(&fdid)
.ok_or_else(|| CascError::EntryNotFound(format!("FileDataID {fdid}")))?;
let (flags, content_key) = self.select_best_content(content_entries)?;
let encoding_entry = encoding.lookup_by_ckey(content_key).ok_or_else(|| {
CascError::EntryNotFound(format!("CKey {} in encoding", hex::encode(content_key)))
})?;
let ekey = encoding_entry
.encoding_keys
.first()
.ok_or_else(|| CascError::EntryNotFound("EKey in encoding entry".to_string()))?;
let mapping = FileMapping {
file_data_id: fdid,
content_key: *content_key,
encoding_key: Some(EKey::from_slice(ekey).unwrap()),
flags: Some(*flags),
};
if self.config.cache_manifests {
self.fdid_cache.write().insert(fdid, mapping.clone());
}
Ok(mapping)
}
pub fn lookup_by_filename(&self, filename: &str) -> Result<FileMapping> {
let fdid = {
let cache = self.filename_cache.read();
cache.get(filename).copied()
};
if let Some(fdid) = fdid {
return self.lookup_by_fdid(fdid);
}
let root = self.root.read();
let root = root
.as_ref()
.ok_or_else(|| CascError::ManifestNotLoaded("root".to_string()))?;
let fdid = root
.get_fid(filename)
.ok_or_else(|| CascError::EntryNotFound(format!("Filename: {filename}")))?;
self.lookup_by_fdid(fdid)
}
pub fn get_all_fdids(&self) -> Result<Vec<u32>> {
let root = self.root.read();
let root = root
.as_ref()
.ok_or_else(|| CascError::ManifestNotLoaded("root".to_string()))?;
Ok(root.fid_md5.keys().copied().collect())
}
pub fn get_fdid_for_filename(&self, filename: &str) -> Option<u32> {
{
let cache = self.filename_cache.read();
if let Some(&fdid) = cache.get(filename) {
return Some(fdid);
}
}
let root = self.root.read();
root.as_ref()?.get_fid(filename)
}
pub fn get_ekey_for_fdid(&self, fdid: u32) -> Result<EKey> {
let mapping = self.lookup_by_fdid(fdid)?;
mapping
.encoding_key
.ok_or_else(|| CascError::EntryNotFound(format!("EKey for FDID {fdid}")))
}
pub fn is_loaded(&self) -> bool {
let has_root = self.root.read().is_some() || self.lazy_root.read().is_some();
let has_encoding = self.encoding.read().is_some() || self.lazy_encoding.read().is_some();
has_root && has_encoding
}
pub fn clear_cache(&self) {
self.fdid_cache.write().clear();
if let Some(lazy_root) = self.lazy_root.write().as_mut() {
lazy_root.fdid_cache.clear();
lazy_root.hash_cache.clear();
}
if let Some(lazy_encoding) = self.lazy_encoding.write().as_mut() {
lazy_encoding.ckey_cache.clear();
lazy_encoding.ekey_cache.clear();
}
debug!("Cleared FileDataID and lazy manifest caches");
}
fn lookup_fdid_lazy(&self, _fdid: u32) -> Result<Option<FileMapping>> {
let lazy_root = self.lazy_root.read();
let lazy_encoding = self.lazy_encoding.read();
if lazy_root.is_none() || lazy_encoding.is_none() {
return Ok(None); }
debug!("Lazy lookup infrastructure ready, falling back to full loading for now");
Ok(None)
}
fn select_best_content<'a>(
&self,
entries: &'a std::collections::BTreeMap<
tact_parser::wow_root::LocaleContentFlags,
[u8; 16],
>,
) -> Result<(&'a ContentFlags, &'a [u8; 16])> {
if entries.len() == 1 {
let (flags, key) = entries.iter().next().unwrap();
return Ok((&flags.content, key));
}
let locale_matches: Vec<_> = entries
.iter()
.filter(|(flags, _)| (flags.locale & self.config.locale).any() || flags.locale.all())
.collect();
if locale_matches.is_empty() {
let (flags, key) = entries.iter().next().unwrap();
return Ok((&flags.content, key));
}
if let Some(required_flags) = self.config.content_flags {
for (flags, key) in &locale_matches {
if self.content_flags_match(&flags.content, &required_flags) {
return Ok((&flags.content, key));
}
}
}
let (flags, key) = locale_matches[0];
Ok((&flags.content, key))
}
fn content_flags_match(&self, flags: &ContentFlags, required: &ContentFlags) -> bool {
if required.windows() && !flags.windows() {
return false;
}
if required.macos() && !flags.macos() {
return false;
}
if required.x86_64() && !flags.x86_64() {
return false;
}
if required.x86_32() && !flags.x86_32() {
return false;
}
if required.aarch64() && !flags.aarch64() {
return false;
}
true
}
}