dtmt/lib/sdk/src/binary.rs
Lucas Schwiderski 08219f05ba
sdk: Fix reading strings
Fatshark has a few weird string fields, where they provide a length
field, but then sometimes write a shorter, NUL-terminated string into
that same field and adding padding up to the "advertised" length.
To properly read those strings, we can't rely on just the length field
anymore, but need to check for a NUL, too.
2024-07-19 09:48:21 +02:00

253 lines
7.4 KiB
Rust

use std::io::{Cursor, Read, Seek, Write};
use color_eyre::Result;
use self::sync::{ReadExt, WriteExt};
pub trait FromBinary: Sized {
fn from_binary<R: Read + Seek>(r: &mut R) -> Result<Self>;
}
pub trait ToBinary {
fn to_binary(&self) -> Result<Vec<u8>>;
}
impl<T: ToBinary> ToBinary for Vec<T> {
fn to_binary(&self) -> Result<Vec<u8>> {
// TODO: Allocations for the vector could be optimized by first
// serializing one value, then calculating the size from that.
let mut bin = Cursor::new(Vec::new());
bin.write_u32(self.len() as u32)?;
for val in self.iter() {
let buf = val.to_binary()?;
bin.write_all(&buf)?;
}
Ok(bin.into_inner())
}
}
impl<T: FromBinary> FromBinary for Vec<T> {
fn from_binary<R: Read + Seek>(r: &mut R) -> Result<Self> {
let size = r.read_u32()? as usize;
let mut list = Vec::with_capacity(size);
for _ in 0..size {
list.push(T::from_binary(r)?);
}
Ok(list)
}
}
pub mod sync {
use std::ffi::CStr;
use std::io::{self, Read, Seek, SeekFrom};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use color_eyre::eyre::WrapErr;
use color_eyre::{Help, Report, Result, SectionExt};
macro_rules! make_read {
($func:ident, $read:ident, $type:ty) => {
fn $read(&mut self) -> io::Result<$type> {
ReadBytesExt::$func::<LittleEndian>(self)
}
fn $func(&mut self) -> Result<$type> {
let res =
ReadExt::$read(self).wrap_err(concat!("failed to read ", stringify!($type)));
if res.is_ok() {
return res;
}
let pos = self.stream_position();
if pos.is_ok() {
res.with_section(|| {
format!("{pos:#X} ({pos})", pos = pos.unwrap()).header("Position: ")
})
} else {
res
}
}
};
}
macro_rules! make_write {
($func:ident, $write:ident, $type:ty) => {
fn $write(&mut self, val: $type) -> io::Result<()> {
WriteBytesExt::$func::<LittleEndian>(self, val)
}
fn $func(&mut self, val: $type) -> Result<()> {
let res = WriteExt::$write(self, val)
.wrap_err(concat!("failed to write ", stringify!($type)));
if res.is_ok() {
return res;
}
let pos = self.stream_position();
if pos.is_ok() {
res.with_section(|| {
format!("{pos:#X} ({pos})", pos = pos.unwrap()).header("Position: ")
})
} else {
res
}
}
};
}
macro_rules! make_skip {
($func:ident, $read:ident, $type:ty) => {
fn $func(&mut self, cmp: $type) -> Result<()> {
let val = ReadExt::$read(self)?;
if val != cmp {
let pos = self.stream_position().unwrap_or(u64::MAX);
tracing::debug!(
pos,
expected = cmp,
actual = val,
"Unexpected value for skipped {}",
stringify!($type)
);
}
Ok(())
}
};
}
pub trait ReadExt: ReadBytesExt + Seek {
fn read_u8(&mut self) -> io::Result<u8> {
ReadBytesExt::read_u8(self)
}
make_read!(read_u32, read_u32_le, u32);
make_read!(read_u64, read_u64_le, u64);
make_skip!(skip_u8, read_u8, u8);
make_skip!(skip_u32, read_u32, u32);
// Implementation based on https://en.wikipedia.com/wiki/LEB128
fn read_uleb128(&mut self) -> io::Result<u64> {
let mut result: u64 = 0;
let mut shift: u64 = 0;
loop {
let byte = ReadExt::read_u8(self)? as u64;
result |= (byte & 0x7f) << shift;
if byte < 0x80 {
return Ok(result);
}
shift += 7;
}
}
fn skip_padding(&mut self) -> io::Result<()> {
let pos = self.stream_position()?;
let padding_size = 16 - (pos % 16);
if padding_size < 16 && padding_size > 0 {
tracing::trace!(pos, padding_size, "Skipping padding");
self.seek(SeekFrom::Current(padding_size as i64))?;
} else {
tracing::trace!(pos, padding_size, "No padding to skip");
}
Ok(())
}
fn read_string_len(&mut self, len: usize) -> Result<String> {
let pos = self.stream_position();
let res = read_string_len(self, len);
if res.is_ok() {
return res;
}
if pos.is_ok() {
res.with_section(|| {
format!("{pos:#X} ({pos})", pos = pos.unwrap()).header("Position: ")
})
} else {
res
}
}
}
pub trait WriteExt: WriteBytesExt + Seek {
fn write_u8(&mut self, val: u8) -> io::Result<()> {
WriteBytesExt::write_u8(self, val)
}
make_write!(write_u32, write_u32_le, u32);
make_write!(write_u64, write_u64_le, u64);
fn write_padding(&mut self) -> io::Result<usize> {
let pos = self.stream_position()?;
let size = 16 - (pos % 16) as usize;
tracing::trace!(padding_size = size, "Writing padding");
if size > 0 && size < 16 {
let buf = vec![0; size];
self.write_all(&buf)?;
Ok(size)
} else {
Ok(0)
}
}
}
impl<R: ReadBytesExt + Seek + ?Sized> ReadExt for R {}
impl<W: WriteBytesExt + Seek + ?Sized> WriteExt for W {}
pub(crate) fn _read_up_to<R>(r: &mut R, buf: &mut Vec<u8>) -> Result<usize>
where
R: Read + Seek,
{
let pos = r.stream_position()?;
let err = {
match r.read_exact(buf) {
Ok(_) => return Ok(buf.len()),
Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => {
r.seek(SeekFrom::Start(pos))?;
match r.read_to_end(buf) {
Ok(read) => return Ok(read),
Err(err) => err,
}
}
Err(err) => err,
}
};
Err(err).with_section(|| format!("{pos:#X} ({pos})").header("Position: "))
}
fn read_string_len(mut r: impl Read, len: usize) -> Result<String> {
let mut buf = vec![0; len];
r.read_exact(&mut buf)
.wrap_err_with(|| format!("Failed to read {} bytes", len))?;
let res = match CStr::from_bytes_until_nul(&buf) {
Ok(s) => {
let s = s.to_str()?;
Ok(s.to_string())
}
Err(_) => String::from_utf8(buf.clone()).map_err(Report::new),
};
res.wrap_err("Invalid binary for UTF8 string")
.with_section(|| format!("{}", String::from_utf8_lossy(&buf)).header("ASCI:"))
.with_section(|| format!("{:x?}", buf).header("Bytes:"))
}
}