#![allow(clippy::cast_possible_wrap, clippy::cast_lossless)]
use thiserror::Error;
use crate::load::AttachmentLoaderError;
use crate::math::Color;
#[derive(Debug, Error)]
pub enum BinaryError {
#[error("unexpected end of input at byte {at} (wanted {wanted} more byte(s))")]
UnexpectedEof { at: usize, wanted: usize },
#[error("byte {at}: varint overflowed 32 bits")]
VarintOverflow { at: usize },
#[error("byte {at}: string is not valid UTF-8: {source}")]
InvalidUtf8 {
at: usize,
#[source]
source: std::str::Utf8Error,
},
#[error("byte {at}: string-table index {index} out of range ({len} entries)")]
StringRefOutOfRange { at: usize, index: usize, len: usize },
#[error("skeleton version mismatch: file reports {found:?}, runtime targets {expected:?}")]
UnsupportedVersion { found: String, expected: String },
#[error("byte {at}: {entity} index {index} out of range ({len} entries)")]
IndexOutOfRange {
at: usize,
entity: &'static str,
index: usize,
len: usize,
},
#[error("byte {at}: unknown {entity} discriminant {value}")]
UnknownDiscriminant {
at: usize,
entity: &'static str,
value: u32,
},
#[error("byte {at}: linked-mesh parent {parent:?} not found on skin {skin:?} slot {slot}")]
LinkedMeshParentMissing {
at: usize,
skin: String,
slot: usize,
parent: String,
},
#[error("attachment loader failed: {0}")]
AttachmentLoader(#[from] AttachmentLoaderError),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
}
pub(crate) struct BinaryReader<'a> {
buf: &'a [u8],
pos: usize,
}
impl<'a> BinaryReader<'a> {
pub fn new(buf: &'a [u8]) -> Self {
Self { buf, pos: 0 }
}
pub fn position(&self) -> usize {
self.pos
}
fn need(&self, n: usize) -> Result<(), BinaryError> {
if self.pos + n > self.buf.len() {
Err(BinaryError::UnexpectedEof {
at: self.pos,
wanted: n,
})
} else {
Ok(())
}
}
pub fn read_byte(&mut self) -> Result<u8, BinaryError> {
self.need(1)?;
let b = self.buf[self.pos];
self.pos += 1;
Ok(b)
}
pub fn read_sbyte(&mut self) -> Result<i8, BinaryError> {
self.read_byte().map(|b| b as i8)
}
pub fn read_bool(&mut self) -> Result<bool, BinaryError> {
self.read_byte().map(|b| b != 0)
}
pub fn read_int(&mut self) -> Result<i32, BinaryError> {
self.need(4)?;
let b0 = self.buf[self.pos];
let b1 = self.buf[self.pos + 1];
let b2 = self.buf[self.pos + 2];
let b3 = self.buf[self.pos + 3];
self.pos += 4;
Ok(i32::from_be_bytes([b0, b1, b2, b3]))
}
pub fn read_float(&mut self) -> Result<f32, BinaryError> {
self.read_int().map(|bits| f32::from_bits(bits as u32))
}
pub fn read_varint(&mut self, optimize_positive: bool) -> Result<i32, BinaryError> {
let start = self.pos;
let mut b = self.read_byte()?;
let mut value = u32::from(b & 0x7F);
if b & 0x80 != 0 {
b = self.read_byte()?;
value |= u32::from(b & 0x7F) << 7;
if b & 0x80 != 0 {
b = self.read_byte()?;
value |= u32::from(b & 0x7F) << 14;
if b & 0x80 != 0 {
b = self.read_byte()?;
value |= u32::from(b & 0x7F) << 21;
if b & 0x80 != 0 {
b = self.read_byte()?;
value |= u32::from(b & 0x7F) << 28;
}
}
}
}
if self.pos == start + 5 && (b & 0x80) != 0 {
return Err(BinaryError::VarintOverflow { at: start });
}
Ok(if optimize_positive {
value as i32
} else {
let sign_mask = (value & 1).wrapping_neg();
((value >> 1) ^ sign_mask) as i32
})
}
pub fn read_uvarint(&mut self) -> Result<usize, BinaryError> {
let v = self.read_varint(true)?;
Ok(v as u32 as usize)
}
pub fn read_string(&mut self) -> Result<Option<String>, BinaryError> {
let len = self.read_uvarint()?;
if len == 0 {
return Ok(None);
}
let payload_len = len - 1;
self.need(payload_len)?;
let start = self.pos;
let slice = &self.buf[start..start + payload_len];
self.pos += payload_len;
std::str::from_utf8(slice)
.map(|s| Some(s.to_string()))
.map_err(|source| BinaryError::InvalidUtf8 { at: start, source })
}
pub fn read_string_ref(&mut self, strings: &[String]) -> Result<Option<String>, BinaryError> {
let at = self.pos;
let index = self.read_uvarint()?;
if index == 0 {
return Ok(None);
}
let idx = index - 1;
strings
.get(idx)
.cloned()
.map(Some)
.ok_or(BinaryError::StringRefOutOfRange {
at,
index: idx,
len: strings.len(),
})
}
pub fn read_color(&mut self) -> Result<Color, BinaryError> {
let r = f32::from(self.read_byte()?) / 255.0;
let g = f32::from(self.read_byte()?) / 255.0;
let b = f32::from(self.read_byte()?) / 255.0;
let a = f32::from(self.read_byte()?) / 255.0;
Ok(Color::new(r, g, b, a))
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
#[test]
fn read_byte_sbyte_bool() {
let mut r = BinaryReader::new(&[0x00, 0xFF, 0x7F, 0x80]);
assert_eq!(r.read_byte().unwrap(), 0x00);
assert!(r.read_bool().unwrap());
assert_eq!(r.read_sbyte().unwrap(), 0x7F);
assert_eq!(r.read_sbyte().unwrap(), -128);
}
#[test]
fn read_int_is_big_endian() {
let mut r = BinaryReader::new(&[0x00, 0x00, 0x00, 0x2A]);
assert_eq!(r.read_int().unwrap(), 42);
let mut r = BinaryReader::new(&[0xFF, 0xFF, 0xFF, 0xFF]);
assert_eq!(r.read_int().unwrap(), -1);
let mut r = BinaryReader::new(&[0x12, 0x34, 0x56, 0x78]);
assert_eq!(r.read_int().unwrap(), 0x1234_5678);
}
#[test]
fn read_float_reinterprets_int_bits() {
let mut r = BinaryReader::new(&[0x3F, 0x80, 0x00, 0x00]);
assert_eq!(r.read_float().unwrap(), 1.0);
}
#[test]
fn read_varint_positive_single_byte() {
let mut r = BinaryReader::new(&[0x2A]);
assert_eq!(r.read_varint(true).unwrap(), 42);
}
#[test]
fn read_varint_positive_multi_byte() {
let mut r = BinaryReader::new(&[0xAC, 0x02]);
assert_eq!(r.read_varint(true).unwrap(), 300);
}
#[test]
fn read_varint_zigzag_negative() {
let mut r = BinaryReader::new(&[0x01, 0x02, 0x03]);
assert_eq!(r.read_varint(false).unwrap(), -1);
assert_eq!(r.read_varint(false).unwrap(), 1);
assert_eq!(r.read_varint(false).unwrap(), -2);
}
#[test]
fn read_string_none_on_zero_length() {
let mut r = BinaryReader::new(&[0x00]);
assert_eq!(r.read_string().unwrap(), None);
}
#[test]
fn read_string_payload_is_length_minus_one_bytes() {
let mut r = BinaryReader::new(&[0x03, b'h', b'i']);
assert_eq!(r.read_string().unwrap().as_deref(), Some("hi"));
}
#[test]
fn read_string_ref_resolves_to_table() {
let strings = vec!["alpha".to_string(), "beta".to_string()];
let mut r = BinaryReader::new(&[0x00, 0x01, 0x02]);
assert_eq!(r.read_string_ref(&strings).unwrap(), None);
assert_eq!(
r.read_string_ref(&strings).unwrap().as_deref(),
Some("alpha")
);
assert_eq!(
r.read_string_ref(&strings).unwrap().as_deref(),
Some("beta")
);
}
#[test]
fn read_string_ref_out_of_range_is_error() {
let strings = vec!["alpha".to_string()];
let mut r = BinaryReader::new(&[0x03]);
match r.read_string_ref(&strings) {
Err(BinaryError::StringRefOutOfRange { index, len, .. }) => {
assert_eq!(index, 2);
assert_eq!(len, 1);
}
other => panic!("expected StringRefOutOfRange, got {other:?}"),
}
}
#[test]
fn read_color_rgba_normalized() {
let mut r = BinaryReader::new(&[0xFF, 0x00, 0x80, 0x40]);
let c = r.read_color().unwrap();
assert_eq!(c.r, 1.0);
assert_eq!(c.g, 0.0);
assert!((c.b - 0x80 as f32 / 255.0).abs() < 1e-6);
assert!((c.a - 0x40 as f32 / 255.0).abs() < 1e-6);
}
#[test]
fn eof_surfaces_as_error() {
let mut r = BinaryReader::new(&[0x00]);
assert!(matches!(
r.read_int(),
Err(BinaryError::UnexpectedEof { .. })
));
}
#[test]
fn position_tracks_cursor() {
let mut r = BinaryReader::new(&[0x01, 0x02, 0x03, 0x04, 0x05]);
assert_eq!(r.position(), 0);
r.read_byte().unwrap();
assert_eq!(r.position(), 1);
r.read_int().unwrap();
assert_eq!(r.position(), 5);
}
}