Binäre Kodierung für den Kern

Writer und Reader für Befehle, Snapshots und Aufzeichnungen. Beschädigte
Eingaben führen zu einem Fehler statt zu einem Absturz.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Steven Wroblewski
2026-09-30 20:17:02 +02:00
parent 477b9a41dd
commit 4eed8efed0
3 changed files with 249 additions and 0 deletions
+169
View File
@@ -0,0 +1,169 @@
//! Minimal little-endian binary encoding for commands, snapshots and
//! recordings.
use std::fmt;
/// Error returned when bytes cannot be decoded.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DecodeError {
/// The input ended before the value was complete.
UnexpectedEnd,
/// The input contained a value that is not allowed at this position.
InvalidValue(&'static str),
/// The input contained bytes after the last expected value.
TrailingBytes,
}
impl fmt::Display for DecodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DecodeError::UnexpectedEnd => write!(f, "unexpected end of input"),
DecodeError::InvalidValue(what) => write!(f, "invalid value: {what}"),
DecodeError::TrailingBytes => write!(f, "trailing bytes after the last value"),
}
}
}
impl std::error::Error for DecodeError {}
/// Appends values to a growing byte buffer.
#[derive(Debug, Default)]
pub struct Writer {
buf: Vec<u8>,
}
impl Writer {
pub fn new() -> Self {
Self::default()
}
pub fn u8(&mut self, value: u8) {
self.buf.push(value);
}
pub fn u16(&mut self, value: u16) {
self.buf.extend_from_slice(&value.to_le_bytes());
}
pub fn u32(&mut self, value: u32) {
self.buf.extend_from_slice(&value.to_le_bytes());
}
pub fn u64(&mut self, value: u64) {
self.buf.extend_from_slice(&value.to_le_bytes());
}
pub fn i32(&mut self, value: i32) {
self.buf.extend_from_slice(&value.to_le_bytes());
}
pub fn bool(&mut self, value: bool) {
self.buf.push(u8::from(value));
}
/// Writes bytes as they are, without a length.
pub fn raw(&mut self, value: &[u8]) {
self.buf.extend_from_slice(value);
}
/// Writes a `u32` length followed by the bytes.
pub fn bytes(&mut self, value: &[u8]) {
let len = u32::try_from(value.len()).expect("length fits in u32");
self.u32(len);
self.buf.extend_from_slice(value);
}
/// Writes a string as UTF-8 with a `u32` length in front.
pub fn str(&mut self, value: &str) {
self.bytes(value.as_bytes());
}
pub fn into_bytes(self) -> Vec<u8> {
self.buf
}
}
/// Reads values from a byte slice in the order they were written.
#[derive(Debug)]
pub struct Reader<'a> {
buf: &'a [u8],
pos: usize,
}
impl<'a> Reader<'a> {
pub fn new(buf: &'a [u8]) -> Self {
Self { buf, pos: 0 }
}
/// Number of bytes that have not been read yet.
pub fn remaining(&self) -> usize {
self.buf.len() - self.pos
}
/// Reads exactly `len` bytes.
pub fn raw(&mut self, len: usize) -> Result<&'a [u8], DecodeError> {
if len > self.remaining() {
return Err(DecodeError::UnexpectedEnd);
}
let slice = &self.buf[self.pos..self.pos + len];
self.pos += len;
Ok(slice)
}
fn array<const N: usize>(&mut self) -> Result<[u8; N], DecodeError> {
let mut out = [0u8; N];
out.copy_from_slice(self.raw(N)?);
Ok(out)
}
pub fn u8(&mut self) -> Result<u8, DecodeError> {
Ok(self.array::<1>()?[0])
}
pub fn u16(&mut self) -> Result<u16, DecodeError> {
Ok(u16::from_le_bytes(self.array()?))
}
pub fn u32(&mut self) -> Result<u32, DecodeError> {
Ok(u32::from_le_bytes(self.array()?))
}
pub fn u64(&mut self) -> Result<u64, DecodeError> {
Ok(u64::from_le_bytes(self.array()?))
}
pub fn i32(&mut self) -> Result<i32, DecodeError> {
Ok(i32::from_le_bytes(self.array()?))
}
pub fn bool(&mut self) -> Result<bool, DecodeError> {
match self.u8()? {
0 => Ok(false),
1 => Ok(true),
_ => Err(DecodeError::InvalidValue("bool")),
}
}
/// Reads a `u32` length followed by that many bytes.
pub fn bytes(&mut self) -> Result<&'a [u8], DecodeError> {
let len = self.u32()? as usize;
self.raw(len)
}
/// Reads a UTF-8 string with a `u32` length in front.
pub fn string(&mut self) -> Result<String, DecodeError> {
let bytes = self.bytes()?;
std::str::from_utf8(bytes)
.map(str::to_owned)
.map_err(|_| DecodeError::InvalidValue("utf-8 string"))
}
/// Succeeds only if every byte has been read.
pub fn finish(self) -> Result<(), DecodeError> {
if self.remaining() == 0 {
Ok(())
} else {
Err(DecodeError::TrailingBytes)
}
}
}
+2
View File
@@ -5,3 +5,5 @@
//! only with whole numbers. Given the same commands it produces the same
//! world on every machine. That is what lets every player simulate the whole
//! world and exchange nothing but commands.
pub mod codec;