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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user