M1a: deterministischer Rust-Kern #3

Merged
steven merged 15 commits from m1a-rust-kern into main 2026-10-01 06:03:36 +00:00
3 changed files with 249 additions and 0 deletions
Showing only changes of commit 4eed8efed0 - Show all commits
+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 //! 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 on every machine. That is what lets every player simulate the whole
//! world and exchange nothing but commands. //! world and exchange nothing but commands.
pub mod codec;
+78
View File
@@ -0,0 +1,78 @@
use grovewright_core::codec::{DecodeError, Reader, Writer};
#[test]
fn every_kind_of_value_survives_a_round_trip() {
let mut w = Writer::new();
w.u8(0xAB);
w.u16(0xBEEF);
w.u32(0xDEAD_BEEF);
w.u64(0x0123_4567_89AB_CDEF);
w.i32(-42);
w.bool(true);
w.bool(false);
w.bytes(&[1, 2, 3]);
w.str("Grüner Hain");
w.raw(&[9, 9]);
let bytes = w.into_bytes();
let mut r = Reader::new(&bytes);
assert_eq!(r.u8(), Ok(0xAB));
assert_eq!(r.u16(), Ok(0xBEEF));
assert_eq!(r.u32(), Ok(0xDEAD_BEEF));
assert_eq!(r.u64(), Ok(0x0123_4567_89AB_CDEF));
assert_eq!(r.i32(), Ok(-42));
assert_eq!(r.bool(), Ok(true));
assert_eq!(r.bool(), Ok(false));
assert_eq!(r.bytes(), Ok(&[1u8, 2, 3][..]));
assert_eq!(r.string(), Ok("Grüner Hain".to_owned()));
assert_eq!(r.raw(2), Ok(&[9u8, 9][..]));
assert_eq!(r.finish(), Ok(()));
}
#[test]
fn integers_are_little_endian() {
let mut w = Writer::new();
w.u32(0x0102_0304);
assert_eq!(w.into_bytes(), vec![4, 3, 2, 1]);
}
#[test]
fn reading_past_the_end_fails() {
let mut r = Reader::new(&[1, 2]);
assert_eq!(r.u32(), Err(DecodeError::UnexpectedEnd));
}
#[test]
fn an_oversized_length_fails_without_panicking() {
let mut w = Writer::new();
w.u32(u32::MAX);
let bytes = w.into_bytes();
let mut r = Reader::new(&bytes);
assert_eq!(r.bytes(), Err(DecodeError::UnexpectedEnd));
}
#[test]
fn invalid_bool_and_invalid_utf8_are_rejected() {
let mut r = Reader::new(&[2]);
assert_eq!(r.bool(), Err(DecodeError::InvalidValue("bool")));
let mut w = Writer::new();
w.bytes(&[0xFF, 0xFE]);
let bytes = w.into_bytes();
let mut r = Reader::new(&bytes);
assert_eq!(r.string(), Err(DecodeError::InvalidValue("utf-8 string")));
}
#[test]
fn finish_rejects_bytes_that_were_not_read() {
let r = Reader::new(&[0]);
assert_eq!(r.finish(), Err(DecodeError::TrailingBytes));
}
#[test]
fn remaining_counts_down_as_values_are_read() {
let mut r = Reader::new(&[1, 2, 3]);
assert_eq!(r.remaining(), 3);
r.u8().unwrap();
assert_eq!(r.remaining(), 2);
}