use anyhow::{Result, bail};
use std::io::{Cursor, Read};
use super::descriptor::{SdlManager, SdlType, StateDescriptor, VarDescriptor};
#[allow(dead_code)]
mod sdl_flags {
pub const HAS_UOID: u16 = 0x1;
pub const HAS_NOTIFICATION_INFO: u16 = 0x2;
pub const HAS_TIMESTAMP: u16 = 0x4;
pub const SAME_AS_DEFAULT: u16 = 0x8;
pub const HAS_DIRTY_FLAG: u16 = 0x10;
pub const WANT_TIMESTAMP: u16 = 0x20;
pub const ADDED_VAR_LENGTH_IO: u16 = 0x8000;
}
const IO_VERSION: u8 = 6;
#[derive(Debug, Clone)]
pub struct SdlRecord {
pub descriptor_name: String,
pub descriptor_version: u16,
pub flags: u16,
pub variables: Vec<SdlVarValue>,
pub sd_variables: Vec<SdlNestedRecord>,
}
#[derive(Debug, Clone)]
pub struct SdlVarValue {
pub name: String,
pub var_type: SdlType,
pub is_dirty: bool,
pub has_timestamp: bool,
pub timestamp_secs: u32,
pub timestamp_micros: u32,
pub same_as_default: bool,
pub values: Vec<SdlAtomicValue>,
}
#[derive(Debug, Clone)]
pub struct SdlNestedRecord {
pub name: String,
pub records: Vec<SdlRecord>,
}
#[derive(Debug, Clone)]
pub enum SdlAtomicValue {
Int(i32),
Float(f32),
Bool(bool),
String(String),
Double(f64),
Byte(u8),
Short(i16),
Time { secs: u32, micros: u32 },
Key(Vec<u8>),
Creatable { class_index: u16, data: Vec<u8> },
AgeTimeOfDay,
}
pub fn parse_sdl_record(data: &[u8], sdl_mgr: &SdlManager) -> Result<SdlRecord> {
let mut cursor = Cursor::new(data);
let (name, version) = read_stream_header(&mut cursor)?;
let desc = sdl_mgr.find(&name, version as u32)
.or_else(|| sdl_mgr.find(&name, 0));
read_state_data_record(&mut cursor, &name, version, desc)
}
pub fn read_stream_header(cursor: &mut Cursor<&[u8]>) -> Result<(String, u16)> {
let sav_flags = read_u16(cursor)?;
if sav_flags & sdl_flags::ADDED_VAR_LENGTH_IO == 0 {
bail!("SDL stream header missing kAddedVarLengthIO flag");
}
let name = read_safe_string(cursor)?;
let version = read_u16(cursor)?;
if sav_flags & sdl_flags::HAS_UOID != 0 {
skip_uoid(cursor)?;
}
Ok((name, version))
}
fn read_state_data_record(
cursor: &mut Cursor<&[u8]>,
desc_name: &str,
desc_version: u16,
desc: Option<&StateDescriptor>,
) -> Result<SdlRecord> {
let flags = read_u16(cursor)?;
let io_version = read_u8(cursor)?;
if io_version != IO_VERSION {
bail!("SDL IO version mismatch: expected {}, got {}", IO_VERSION, io_version);
}
let (simple_descs, sd_descs) = if let Some(d) = desc {
let mut simple = Vec::new();
let mut nested = Vec::new();
for v in &d.variables {
if v.var_type == SdlType::StateDescriptor {
nested.push(v);
} else {
simple.push(v);
}
}
(simple, nested)
} else {
(Vec::new(), Vec::new())
};
let total_vars = if let Some(d) = desc { d.variables.len() } else { 256 };
let num_simple = variable_length_read(cursor, total_vars)?;
let all_simple = num_simple == simple_descs.len();
let mut variables = Vec::with_capacity(num_simple);
for i in 0..num_simple {
let idx = if !all_simple {
variable_length_read(cursor, total_vars)?
} else {
i
};
let var_desc = simple_descs.get(idx);
let var = read_simple_var(cursor, var_desc.copied())?;
variables.push(var);
}
let num_sd = variable_length_read(cursor, total_vars)?;
let all_sd = num_sd == sd_descs.len();
let mut sd_variables = Vec::with_capacity(num_sd);
for i in 0..num_sd {
let idx = if !all_sd {
variable_length_read(cursor, total_vars)?
} else {
i
};
let sd_desc = sd_descs.get(idx);
let sd_var = read_sd_var(cursor, sd_desc.copied(), desc_name)?;
sd_variables.push(sd_var);
}
Ok(SdlRecord {
descriptor_name: desc_name.to_string(),
descriptor_version: desc_version,
flags,
variables,
sd_variables,
})
}
fn read_simple_var(cursor: &mut Cursor<&[u8]>, desc: Option<&VarDescriptor>) -> Result<SdlVarValue> {
let name = desc.map(|d| d.name.clone()).unwrap_or_default();
let var_type = desc.map(|d| d.var_type).unwrap_or(SdlType::Int);
let base_save_flags = read_u8(cursor)?;
if base_save_flags as u16 & sdl_flags::HAS_NOTIFICATION_INFO != 0 {
read_notification_info(cursor)?;
}
let save_flags = read_u8(cursor)?;
let is_dirty = save_flags as u16 & sdl_flags::HAS_DIRTY_FLAG != 0;
let same_as_default = save_flags as u16 & sdl_flags::SAME_AS_DEFAULT != 0;
let has_timestamp = save_flags as u16 & sdl_flags::HAS_TIMESTAMP != 0;
let mut timestamp_secs = 0u32;
let mut timestamp_micros = 0u32;
if has_timestamp {
timestamp_secs = read_u32(cursor)?;
timestamp_micros = read_u32(cursor)?;
}
let mut values = Vec::new();
if !same_as_default {
let count = if desc.map(|d| d.count == 0).unwrap_or(false) {
read_u32(cursor)? as usize
} else {
desc.map(|d| d.count).unwrap_or(1)
};
let atomic_count = get_atomic_count(var_type);
for _i in 0..count {
let atom_values = read_atomic_values(cursor, var_type, atomic_count)?;
values.extend(atom_values);
}
}
Ok(SdlVarValue {
name,
var_type,
is_dirty,
has_timestamp,
timestamp_secs,
timestamp_micros,
same_as_default,
values,
})
}
fn read_sd_var(
cursor: &mut Cursor<&[u8]>,
desc: Option<&VarDescriptor>,
_parent_desc_name: &str,
) -> Result<SdlNestedRecord> {
let name = desc.map(|d| d.name.clone()).unwrap_or_default();
let base_save_flags = read_u8(cursor)?;
if base_save_flags as u16 & sdl_flags::HAS_NOTIFICATION_INFO != 0 {
read_notification_info(cursor)?;
}
let _save_flags = read_u8(cursor)?;
let is_variable_length = desc.map(|d| d.count == 0).unwrap_or(false);
let total_count = if is_variable_length {
read_u32(cursor)? as usize
} else {
desc.map(|d| d.count).unwrap_or(1)
};
let size_for_vl = if is_variable_length { 0xFFFF_FFFF_usize } else { total_count };
let cnt = variable_length_read(cursor, size_for_vl)?;
let all = cnt == total_count;
let mut records = Vec::with_capacity(cnt);
for i in 0..cnt {
let _idx = if !all {
variable_length_read(cursor, size_for_vl)?
} else {
i
};
let record = read_state_data_record(cursor, &name, 0, None)?;
records.push(record);
}
Ok(SdlNestedRecord { name, records })
}
fn read_safe_string(cursor: &mut Cursor<&[u8]>) -> Result<String> {
let raw_len = read_u16(cursor)?;
let num_chars = (raw_len & 0x0FFF) as usize;
if num_chars == 0 {
return Ok(String::new());
}
let mut buf = vec![0u8; num_chars];
cursor.read_exact(&mut buf)?;
if buf[0] & 0x80 != 0 {
for b in &mut buf {
*b = !*b;
}
}
Ok(String::from_utf8_lossy(&buf).into_owned())
}
fn read_notification_info(cursor: &mut Cursor<&[u8]>) -> Result<()> {
let _save_flags = read_u8(cursor)?; let _hint = read_safe_string(cursor)?;
Ok(())
}
fn skip_uoid(cursor: &mut Cursor<&[u8]>) -> Result<()> {
let contents = read_u8(cursor)?;
let _seq = read_u32(cursor)?;
let _loc_flags = read_u16(cursor)?;
if contents & 0x02 != 0 {
let _quality = read_u8(cursor)?;
let _cap = read_u8(cursor)?;
}
let _class_type = read_u16(cursor)?;
let _obj_id = read_u32(cursor)?;
let name_len = read_u16(cursor)? as usize;
let mut name_buf = vec![0u8; name_len];
cursor.read_exact(&mut name_buf)?;
if contents & 0x01 != 0 {
let _clone_id = read_u32(cursor)?;
let _clone_player_id = read_u32(cursor)?;
}
Ok(())
}
fn variable_length_read(cursor: &mut Cursor<&[u8]>, size: usize) -> Result<usize> {
if size < 256 {
Ok(read_u8(cursor)? as usize)
} else if size < 65536 {
Ok(read_u16(cursor)? as usize)
} else {
Ok(read_u32(cursor)? as usize)
}
}
fn get_atomic_count(var_type: SdlType) -> usize {
match var_type {
SdlType::Vector3 | SdlType::Point3 | SdlType::Rgb | SdlType::Rgb8 => 3,
SdlType::Rgba | SdlType::Quaternion => 4,
_ => 1,
}
}
fn get_atomic_type(var_type: SdlType) -> SdlType {
match var_type {
SdlType::Vector3 | SdlType::Point3 | SdlType::Rgb | SdlType::Rgba
| SdlType::Quaternion => SdlType::Float,
SdlType::Rgb8 => SdlType::Byte,
_ => var_type,
}
}
fn read_atomic_values(cursor: &mut Cursor<&[u8]>, var_type: SdlType, atomic_count: usize) -> Result<Vec<SdlAtomicValue>> {
let atomic_type = get_atomic_type(var_type);
let mut values = Vec::with_capacity(atomic_count);
match atomic_type {
SdlType::AgeTimeOfDay => {
values.push(SdlAtomicValue::AgeTimeOfDay);
}
SdlType::Int => {
for _ in 0..atomic_count {
values.push(SdlAtomicValue::Int(read_i32(cursor)?));
}
}
SdlType::Short => {
for _ in 0..atomic_count {
values.push(SdlAtomicValue::Short(read_i16(cursor)?));
}
}
SdlType::Byte => {
for _ in 0..atomic_count {
values.push(SdlAtomicValue::Byte(read_u8(cursor)?));
}
}
SdlType::Float => {
for _ in 0..atomic_count {
values.push(SdlAtomicValue::Float(read_f32(cursor)?));
}
}
SdlType::Double => {
for _ in 0..atomic_count {
values.push(SdlAtomicValue::Double(read_f64(cursor)?));
}
}
SdlType::Bool => {
for _ in 0..atomic_count {
values.push(SdlAtomicValue::Bool(read_u8(cursor)? != 0));
}
}
SdlType::Time => {
for _ in 0..atomic_count {
let secs = read_u32(cursor)?;
let micros = read_u32(cursor)?;
values.push(SdlAtomicValue::Time { secs, micros });
}
}
SdlType::Key => {
for _ in 0..atomic_count {
let start = cursor.position() as usize;
skip_uoid(cursor)?;
let end = cursor.position() as usize;
let data = cursor.get_ref()[start..end].to_vec();
values.push(SdlAtomicValue::Key(data));
}
}
SdlType::String32 => {
for _ in 0..atomic_count {
let mut buf = [0u8; 32];
cursor.read_exact(&mut buf)?;
let s = std::str::from_utf8(&buf)
.unwrap_or("")
.trim_end_matches('\0')
.to_string();
values.push(SdlAtomicValue::String(s));
}
}
SdlType::Creatable => {
let class_index = read_u16(cursor)?;
if class_index != 0x8000 {
let len = read_u32(cursor)? as usize;
let mut data = vec![0u8; len];
cursor.read_exact(&mut data)?;
values.push(SdlAtomicValue::Creatable { class_index, data });
}
}
SdlType::Matrix44 => {
for _ in 0..16 {
values.push(SdlAtomicValue::Float(read_f32(cursor)?));
}
}
_ => {
bail!("Unsupported SDL atomic type: {:?}", atomic_type);
}
}
Ok(values)
}
fn read_u8(cursor: &mut Cursor<&[u8]>) -> Result<u8> {
let mut buf = [0u8; 1];
cursor.read_exact(&mut buf)?;
Ok(buf[0])
}
fn read_u16(cursor: &mut Cursor<&[u8]>) -> Result<u16> {
let mut buf = [0u8; 2];
cursor.read_exact(&mut buf)?;
Ok(u16::from_le_bytes(buf))
}
fn read_u32(cursor: &mut Cursor<&[u8]>) -> Result<u32> {
let mut buf = [0u8; 4];
cursor.read_exact(&mut buf)?;
Ok(u32::from_le_bytes(buf))
}
fn read_i32(cursor: &mut Cursor<&[u8]>) -> Result<i32> {
let mut buf = [0u8; 4];
cursor.read_exact(&mut buf)?;
Ok(i32::from_le_bytes(buf))
}
fn read_i16(cursor: &mut Cursor<&[u8]>) -> Result<i16> {
let mut buf = [0u8; 2];
cursor.read_exact(&mut buf)?;
Ok(i16::from_le_bytes(buf))
}
fn read_f32(cursor: &mut Cursor<&[u8]>) -> Result<f32> {
let mut buf = [0u8; 4];
cursor.read_exact(&mut buf)?;
Ok(f32::from_le_bytes(buf))
}
fn read_f64(cursor: &mut Cursor<&[u8]>) -> Result<f64> {
let mut buf = [0u8; 8];
cursor.read_exact(&mut buf)?;
Ok(f64::from_le_bytes(buf))
}
pub fn write_sdl_record(record: &SdlRecord, desc: Option<&StateDescriptor>) -> Vec<u8> {
let mut buf = Vec::with_capacity(256);
let sav_flags: u16 = sdl_flags::ADDED_VAR_LENGTH_IO;
buf.extend_from_slice(&sav_flags.to_le_bytes());
write_safe_string(&mut buf, &record.descriptor_name);
buf.extend_from_slice(&record.descriptor_version.to_le_bytes());
buf.extend_from_slice(&record.flags.to_le_bytes());
buf.push(IO_VERSION);
let total_vars = desc.map(|d| d.variables.len()).unwrap_or(256);
let num_simple = record.variables.len();
variable_length_write(&mut buf, total_vars, num_simple);
let simple_count_in_desc = desc.map(|d| {
d.variables.iter().filter(|v| v.var_type != SdlType::StateDescriptor).count()
}).unwrap_or(0);
let all_simple = num_simple == simple_count_in_desc;
for (i, var) in record.variables.iter().enumerate() {
if !all_simple {
variable_length_write(&mut buf, total_vars, i);
}
write_simple_var(&mut buf, var);
}
let num_sd = record.sd_variables.len();
variable_length_write(&mut buf, total_vars, num_sd);
buf
}
fn write_safe_string(buf: &mut Vec<u8>, s: &str) {
let bytes = s.as_bytes();
let len_with_flag = (bytes.len() as u16) | 0xF000;
buf.extend_from_slice(&len_with_flag.to_le_bytes());
for &b in bytes {
buf.push(!b);
}
}
fn variable_length_write(buf: &mut Vec<u8>, size: usize, val: usize) {
if size < 256 {
buf.push(val as u8);
} else if size < 65536 {
buf.extend_from_slice(&(val as u16).to_le_bytes());
} else {
buf.extend_from_slice(&(val as u32).to_le_bytes());
}
}
fn write_simple_var(buf: &mut Vec<u8>, var: &SdlVarValue) {
buf.push(0);
let mut save_flags: u8 = 0;
if var.is_dirty { save_flags |= sdl_flags::HAS_DIRTY_FLAG as u8; }
if var.same_as_default { save_flags |= sdl_flags::SAME_AS_DEFAULT as u8; }
if var.has_timestamp { save_flags |= sdl_flags::HAS_TIMESTAMP as u8; }
buf.push(save_flags);
if var.has_timestamp {
buf.extend_from_slice(&var.timestamp_secs.to_le_bytes());
buf.extend_from_slice(&var.timestamp_micros.to_le_bytes());
}
if !var.same_as_default {
for val in &var.values {
write_atomic_value(buf, val);
}
}
}
fn write_atomic_value(buf: &mut Vec<u8>, val: &SdlAtomicValue) {
match val {
SdlAtomicValue::Int(v) => buf.extend_from_slice(&v.to_le_bytes()),
SdlAtomicValue::Float(v) => buf.extend_from_slice(&v.to_le_bytes()),
SdlAtomicValue::Bool(v) => buf.push(*v as u8),
SdlAtomicValue::Double(v) => buf.extend_from_slice(&v.to_le_bytes()),
SdlAtomicValue::Byte(v) => buf.push(*v),
SdlAtomicValue::Short(v) => buf.extend_from_slice(&v.to_le_bytes()),
SdlAtomicValue::Time { secs, micros } => {
buf.extend_from_slice(&secs.to_le_bytes());
buf.extend_from_slice(µs.to_le_bytes());
}
SdlAtomicValue::String(s) => {
let mut fixed = [0u8; 32];
let bytes = s.as_bytes();
let len = bytes.len().min(31);
fixed[..len].copy_from_slice(&bytes[..len]);
buf.extend_from_slice(&fixed);
}
SdlAtomicValue::Key(data) => buf.extend_from_slice(data),
SdlAtomicValue::Creatable { class_index, data } => {
buf.extend_from_slice(&class_index.to_le_bytes());
buf.extend_from_slice(&(data.len() as u32).to_le_bytes());
buf.extend_from_slice(data);
}
SdlAtomicValue::AgeTimeOfDay => {} }
}
#[cfg(test)]
mod tests {
use super::*;
fn build_test_sdl_stream(name: &str, version: u16, var_value: bool) -> Vec<u8> {
let mut buf = Vec::new();
let sav_flags: u16 = sdl_flags::ADDED_VAR_LENGTH_IO;
buf.extend_from_slice(&sav_flags.to_le_bytes());
let name_bytes = name.as_bytes();
let len_with_flag = (name_bytes.len() as u16) | 0xF000;
buf.extend_from_slice(&len_with_flag.to_le_bytes());
for &b in name_bytes {
buf.push(!b); }
buf.extend_from_slice(&version.to_le_bytes());
let rec_flags: u16 = 0; buf.extend_from_slice(&rec_flags.to_le_bytes());
buf.push(IO_VERSION);
buf.push(1);
buf.push(0);
buf.push(sdl_flags::HAS_DIRTY_FLAG as u8);
buf.push(var_value as u8);
buf.push(0);
buf
}
#[test]
fn test_read_safe_string() {
let name = "Cleft";
let mut buf = Vec::new();
let len_with_flag = (name.len() as u16) | 0xF000;
buf.extend_from_slice(&len_with_flag.to_le_bytes());
for &b in name.as_bytes() {
buf.push(!b);
}
let mut cursor = Cursor::new(buf.as_slice());
let result = read_safe_string(&mut cursor).unwrap();
assert_eq!(result, "Cleft");
}
#[test]
fn test_variable_length_read() {
let data = [42u8];
let mut cursor = Cursor::new(data.as_slice());
assert_eq!(variable_length_read(&mut cursor, 100).unwrap(), 42);
let data = 300u16.to_le_bytes();
let mut cursor = Cursor::new(data.as_slice());
assert_eq!(variable_length_read(&mut cursor, 500).unwrap(), 300);
let data = 70000u32.to_le_bytes();
let mut cursor = Cursor::new(data.as_slice());
assert_eq!(variable_length_read(&mut cursor, 100_000).unwrap(), 70000);
}
#[test]
fn test_parse_minimal_sdl() {
let mut mgr = SdlManager::new();
let content = "STATEDESC TestSDL\n{\nVERSION 1\nVAR BOOL testVar[1] DEFAULT=0\n}\n";
let descs = super::super::descriptor::parse_sdl_for_test(content).unwrap();
for d in descs {
mgr.add_descriptor(d);
}
let stream = build_test_sdl_stream("TestSDL", 1, true);
let record = parse_sdl_record(&stream, &mgr).unwrap();
assert_eq!(record.descriptor_name, "TestSDL");
assert_eq!(record.descriptor_version, 1);
assert_eq!(record.variables.len(), 1);
assert_eq!(record.variables[0].name, "testVar");
assert!(record.variables[0].is_dirty);
assert!(!record.variables[0].same_as_default);
assert_eq!(record.variables[0].values.len(), 1);
assert!(matches!(record.variables[0].values[0], SdlAtomicValue::Bool(true)));
}
#[test]
fn test_write_read_roundtrip() {
let mut mgr = SdlManager::new();
let content = "STATEDESC RoundTrip\n{\nVERSION 2\nVAR INT myInt[1] DEFAULT=0\nVAR BOOL myBool[1] DEFAULT=0\n}\n";
let descs = super::super::descriptor::parse_sdl_for_test(content).unwrap();
for d in descs {
mgr.add_descriptor(d);
}
let record = SdlRecord {
descriptor_name: "RoundTrip".to_string(),
descriptor_version: 2,
flags: 0,
variables: vec![
SdlVarValue {
name: "myInt".to_string(),
var_type: SdlType::Int,
is_dirty: true,
has_timestamp: false,
timestamp_secs: 0,
timestamp_micros: 0,
same_as_default: false,
values: vec![SdlAtomicValue::Int(42)],
},
SdlVarValue {
name: "myBool".to_string(),
var_type: SdlType::Bool,
is_dirty: true,
has_timestamp: false,
timestamp_secs: 0,
timestamp_micros: 0,
same_as_default: false,
values: vec![SdlAtomicValue::Bool(true)],
},
],
sd_variables: vec![],
};
let desc = mgr.find("RoundTrip", 2).unwrap();
let bytes = write_sdl_record(&record, Some(desc));
let parsed = parse_sdl_record(&bytes, &mgr).unwrap();
assert_eq!(parsed.descriptor_name, "RoundTrip");
assert_eq!(parsed.descriptor_version, 2);
assert_eq!(parsed.variables.len(), 2);
assert!(matches!(parsed.variables[0].values[0], SdlAtomicValue::Int(42)));
assert!(matches!(parsed.variables[1].values[0], SdlAtomicValue::Bool(true)));
}
#[test]
fn test_stream_header_roundtrip() {
let mut buf = Vec::new();
let sav_flags: u16 = sdl_flags::ADDED_VAR_LENGTH_IO;
buf.extend_from_slice(&sav_flags.to_le_bytes());
let name = "Neighborhood";
let len_with_flag = (name.len() as u16) | 0xF000;
buf.extend_from_slice(&len_with_flag.to_le_bytes());
for &b in name.as_bytes() {
buf.push(!b);
}
buf.extend_from_slice(&42u16.to_le_bytes());
let mut cursor = Cursor::new(buf.as_slice());
let (parsed_name, parsed_version) = read_stream_header(&mut cursor).unwrap();
assert_eq!(parsed_name, "Neighborhood");
assert_eq!(parsed_version, 42);
}
}