use std::collections::HashMap;
use crate::error::{Error, Result};
use crate::io::BitReader;
use super::class_info::ClassInfo;
use boon_proto::proto::{CDemoStringTables, CsvcMsgCreateStringTable, CsvcMsgUpdateStringTable};
const HISTORY_SIZE: usize = 32;
const HISTORY_BITMASK: usize = HISTORY_SIZE - 1;
const MAX_STRING_BITS: usize = 5;
const MAX_STRING_SIZE: usize = 1 << MAX_STRING_BITS;
const MAX_USERDATA_BITS: usize = 17;
const MAX_USERDATA_SIZE: usize = 1 << MAX_USERDATA_BITS;
pub const INSTANCE_BASELINE_TABLE_NAME: &str = "instancebaseline";
#[derive(Debug, Clone)]
pub struct StringTableEntry {
pub string: Option<String>,
pub user_data: Option<Vec<u8>>,
}
#[derive(Debug)]
pub struct StringTable {
pub name: String,
user_data_fixed_size: bool,
user_data_size: i32,
user_data_size_bits: i32,
flags: i32,
using_varint_bitcounts: bool,
pub entries: Vec<StringTableEntry>,
}
impl StringTable {
fn new(
name: &str,
user_data_fixed_size: bool,
user_data_size: i32,
user_data_size_bits: i32,
flags: i32,
using_varint_bitcounts: bool,
) -> Self {
Self {
name: name.to_string(),
user_data_fixed_size,
user_data_size,
user_data_size_bits,
flags,
using_varint_bitcounts,
entries: Vec::new(),
}
}
pub fn parse_update(&mut self, br: &mut BitReader, num_entries: i32) -> Result<()> {
let mut entry_index: i32 = -1;
let mut history: Vec<[u8; MAX_STRING_SIZE]> = vec![[0u8; MAX_STRING_SIZE]; HISTORY_SIZE];
let mut history_delta_index: usize = 0;
let mut string_buf = vec![0u8; 1024];
let mut user_data_buf = vec![0u8; MAX_USERDATA_SIZE];
let mut user_data_uncompressed_buf = vec![0u8; MAX_USERDATA_SIZE];
for _ in 0..num_entries as usize {
entry_index = if br.read_bool()? {
entry_index + 1
} else {
br.read_uvarint32()? as i32 + 1
};
let has_string = br.read_bool()?;
let string = if has_string {
let mut size: usize = 0;
if br.read_bool()? {
let mut history_delta_zero = 0;
if history_delta_index > HISTORY_SIZE {
history_delta_zero = history_delta_index & HISTORY_BITMASK;
}
let index = (history_delta_zero + br.read_bits(5)? as usize) & HISTORY_BITMASK;
let bytes_to_copy = br.read_bits(MAX_STRING_BITS)? as usize;
size += bytes_to_copy;
string_buf[..bytes_to_copy].copy_from_slice(&history[index][..bytes_to_copy]);
size += br.read_string_into(&mut string_buf[bytes_to_copy..])?;
} else {
size += br.read_string_into(&mut string_buf)?;
}
let mut she = [0u8; MAX_STRING_SIZE];
let copy_len = size.min(MAX_STRING_SIZE);
she[..copy_len].copy_from_slice(&string_buf[..copy_len]);
history[history_delta_index & HISTORY_BITMASK] = she;
history_delta_index += 1;
Some(String::from_utf8_lossy(&string_buf[..size]).into_owned())
} else {
None
};
let has_user_data = br.read_bool()?;
let user_data = if has_user_data {
if self.user_data_fixed_size {
br.read_bits_to_bytes(&mut user_data_buf, self.user_data_size_bits as usize)?;
Some(user_data_buf[..self.user_data_size as usize].to_vec())
} else {
let mut is_compressed = false;
if (self.flags & 0x1) != 0 {
is_compressed = br.read_bool()?;
}
let size = if self.using_varint_bitcounts {
br.read_ubitvar()? as usize
} else {
br.read_bits(MAX_USERDATA_BITS)? as usize
};
br.read_bytes(&mut user_data_buf[..size])?;
if is_compressed {
let decomp_len = snap::raw::decompress_len(&user_data_buf[..size])
.map_err(|e| Error::Decompress(e.to_string()))?;
user_data_uncompressed_buf.resize(decomp_len, 0);
snap::raw::Decoder::new()
.decompress(&user_data_buf[..size], &mut user_data_uncompressed_buf)
.map_err(|e| Error::Decompress(e.to_string()))?;
Some(user_data_uncompressed_buf[..decomp_len].to_vec())
} else {
Some(user_data_buf[..size].to_vec())
}
}
} else {
None
};
let idx = entry_index as usize;
if idx < self.entries.len() {
if let Some(ud) = user_data {
self.entries[idx].user_data = Some(ud);
}
if let Some(s) = string {
self.entries[idx].string = Some(s);
}
} else {
while self.entries.len() < idx {
self.entries.push(StringTableEntry {
string: None,
user_data: None,
});
}
self.entries.push(StringTableEntry { string, user_data });
}
}
Ok(())
}
}
#[derive(Default)]
pub struct StringTableContainer {
tables: Vec<StringTable>,
pub instance_baselines: HashMap<i32, Vec<u8>>,
}
impl StringTableContainer {
pub fn new() -> Self {
Self::default()
}
pub fn handle_create(&mut self, msg: CsvcMsgCreateStringTable) -> Result<bool> {
let name = msg.name.as_deref().unwrap_or("");
let is_baseline = name == INSTANCE_BASELINE_TABLE_NAME;
let mut table = StringTable::new(
name,
msg.user_data_fixed_size.unwrap_or(false),
msg.user_data_size.unwrap_or(0),
msg.user_data_size_bits.unwrap_or(0),
msg.flags.unwrap_or(0),
msg.using_varint_bitcounts.unwrap_or(false),
);
let string_data = if msg.data_compressed.unwrap_or(false) {
let sd = msg.string_data.as_deref().unwrap_or(&[]);
let decomp_len =
snap::raw::decompress_len(sd).map_err(|e| Error::Decompress(e.to_string()))?;
let mut buf = vec![0u8; decomp_len];
snap::raw::Decoder::new()
.decompress(sd, &mut buf)
.map_err(|e| Error::Decompress(e.to_string()))?;
buf
} else {
msg.string_data.unwrap_or_default()
};
let mut br = BitReader::new(&string_data);
table.parse_update(&mut br, msg.num_entries.unwrap_or(0))?;
self.tables.push(table);
Ok(is_baseline)
}
pub fn handle_update(&mut self, msg: CsvcMsgUpdateStringTable) -> Result<bool> {
let table_id = msg.table_id.unwrap_or(0) as usize;
if table_id >= self.tables.len() {
return Err(Error::Parse {
context: format!("string table update for non-existent table {}", table_id),
});
}
let is_baseline = self.tables[table_id].name == INSTANCE_BASELINE_TABLE_NAME;
let string_data = msg.string_data.unwrap_or_default();
let mut br = BitReader::new(&string_data);
self.tables[table_id].parse_update(&mut br, msg.num_changed_entries.unwrap_or(0))?;
Ok(is_baseline)
}
pub fn do_full_update(&mut self, cmd: CDemoStringTables) {
for incoming in &cmd.tables {
let table_name = incoming.table_name.as_deref().unwrap_or("");
if let Some(table) = self.tables.iter_mut().find(|t| t.name == table_name) {
for (i, item) in incoming.items.iter().enumerate() {
let entry = StringTableEntry {
string: item.str.clone(),
user_data: item.data.clone(),
};
if i < table.entries.len() {
if entry.user_data.is_some() {
table.entries[i].user_data = entry.user_data;
}
} else {
while table.entries.len() < i {
table.entries.push(StringTableEntry {
string: None,
user_data: None,
});
}
table.entries.push(entry);
}
}
}
}
}
pub fn update_instance_baselines(&mut self, _class_info: &ClassInfo) {
if let Some(table) = self
.tables
.iter()
.find(|t| t.name == INSTANCE_BASELINE_TABLE_NAME)
{
for entry in &table.entries {
if let (Some(s), Some(data)) = (&entry.string, &entry.user_data)
&& let Ok(class_id) = s.parse::<i32>()
{
if self.instance_baselines.get(&class_id) != Some(data) {
self.instance_baselines.insert(class_id, data.clone());
}
}
}
}
}
pub fn find_table(&self, name: &str) -> Option<&StringTable> {
self.tables.iter().find(|t| t.name == name)
}
pub fn tables(&self) -> &[StringTable] {
&self.tables
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn container_new_is_empty() {
let c = StringTableContainer::new();
assert!(c.tables().is_empty());
assert!(c.instance_baselines.is_empty());
}
#[test]
fn find_table_missing() {
let c = StringTableContainer::new();
assert!(c.find_table("nonexistent").is_none());
}
#[test]
fn update_instance_baselines_on_empty_is_noop() {
let mut c = StringTableContainer::new();
let ci = ClassInfo::empty();
c.update_instance_baselines(&ci);
assert!(c.instance_baselines.is_empty());
}
#[test]
fn handle_update_invalid_table_id() {
let mut c = StringTableContainer::new();
let msg = CsvcMsgUpdateStringTable {
table_id: Some(99),
num_changed_entries: Some(0),
string_data: None,
};
let result = c.handle_update(msg);
assert!(result.is_err());
}
#[test]
fn handle_create_empty_uncompressed() {
let mut c = StringTableContainer::new();
let msg = CsvcMsgCreateStringTable {
name: Some("test".to_string()),
num_entries: Some(0),
user_data_fixed_size: Some(false),
user_data_size: Some(0),
user_data_size_bits: Some(0),
flags: Some(0),
string_data: Some(vec![]),
data_compressed: Some(false),
using_varint_bitcounts: Some(false),
..Default::default()
};
c.handle_create(msg).unwrap();
assert_eq!(c.tables().len(), 1);
assert!(c.find_table("test").is_some());
}
}