use crate::error::AstCoreError;
use crate::id::NodeId;
use alloc::string::String;
use alloc::vec::Vec;
pub const SNAPSHOT_MAGIC: [u8; 8] = *b"ZDDSAST\0";
pub const SNAPSHOT_VERSION: u16 = 4;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SnapshotWriter {
out: Vec<u8>,
}
impl SnapshotWriter {
#[must_use]
pub const fn new() -> Self {
Self { out: Vec::new() }
}
#[must_use]
pub fn with_header() -> Self {
let mut writer = Self::new();
writer.out.extend_from_slice(&SNAPSHOT_MAGIC);
writer.u16(SNAPSHOT_VERSION);
writer
}
#[must_use]
pub fn len(&self) -> usize {
self.out.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.out.is_empty()
}
pub fn bool(&mut self, value: bool) {
self.out.push(u8::from(value));
}
pub fn u8(&mut self, value: u8) {
self.out.push(value);
}
pub fn u16(&mut self, value: u16) {
self.out.extend_from_slice(&value.to_be_bytes());
}
pub fn u32(&mut self, value: u32) {
self.out.extend_from_slice(&value.to_be_bytes());
}
pub fn u64(&mut self, value: u64) {
self.out.extend_from_slice(&value.to_be_bytes());
}
pub fn i64(&mut self, value: i64) {
self.out.extend_from_slice(&value.to_be_bytes());
}
pub fn i128(&mut self, value: i128) {
self.out.extend_from_slice(&value.to_be_bytes());
}
pub fn bytes(&mut self, value: &[u8]) -> Result<(), AstCoreError> {
let len = u32::try_from(value.len()).map_err(|_| AstCoreError::Exhausted)?;
self.u32(len);
self.out.extend_from_slice(value);
Ok(())
}
pub fn str(&mut self, value: &str) -> Result<(), AstCoreError> {
self.bytes(value.as_bytes())
}
pub fn node_id(&mut self, value: NodeId) {
self.out.extend_from_slice(&value.to_bytes());
}
#[must_use]
pub fn finish(self) -> Vec<u8> {
self.out
}
}
#[derive(Debug, Clone)]
pub struct SnapshotReader<'a> {
input: &'a [u8],
offset: usize,
}
impl<'a> SnapshotReader<'a> {
#[must_use]
pub const fn new(input: &'a [u8]) -> Self {
Self { input, offset: 0 }
}
pub fn with_header(input: &'a [u8]) -> Result<Self, AstCoreError> {
let mut reader = Self::new(input);
let magic = reader.take(SNAPSHOT_MAGIC.len(), "snapshot magic")?;
if magic != SNAPSHOT_MAGIC {
return Err(AstCoreError::MalformedSnapshot {
offset: 0,
expected: "snapshot magic ZDDSAST",
});
}
let version = reader.u16()?;
if version != SNAPSHOT_VERSION {
return Err(AstCoreError::MalformedSnapshot {
offset: SNAPSHOT_MAGIC.len(),
expected: "snapshot version 1",
});
}
Ok(reader)
}
#[must_use]
pub const fn remaining(&self) -> usize {
self.input.len().saturating_sub(self.offset)
}
#[must_use]
pub const fn offset(&self) -> usize {
self.offset
}
fn take(&mut self, count: usize, expected: &'static str) -> Result<&'a [u8], AstCoreError> {
let start = self.offset;
let end = start
.checked_add(count)
.ok_or(AstCoreError::MalformedSnapshot {
offset: start,
expected,
})?;
let slice = self
.input
.get(start..end)
.ok_or(AstCoreError::MalformedSnapshot {
offset: start,
expected,
})?;
self.offset = end;
Ok(slice)
}
fn fixed<const N: usize>(&mut self, expected: &'static str) -> Result<[u8; N], AstCoreError> {
let start = self.offset;
let slice = self.take(N, expected)?;
slice
.try_into()
.map_err(|_| AstCoreError::MalformedSnapshot {
offset: start,
expected,
})
}
pub fn bool(&mut self) -> Result<bool, AstCoreError> {
let start = self.offset;
match self.u8()? {
0 => Ok(false),
1 => Ok(true),
_ => Err(AstCoreError::MalformedSnapshot {
offset: start,
expected: "boolean 0 or 1",
}),
}
}
pub fn u8(&mut self) -> Result<u8, AstCoreError> {
Ok(u8::from_be_bytes(self.fixed::<1>("u8")?))
}
pub fn u16(&mut self) -> Result<u16, AstCoreError> {
Ok(u16::from_be_bytes(self.fixed::<2>("u16")?))
}
pub fn u32(&mut self) -> Result<u32, AstCoreError> {
Ok(u32::from_be_bytes(self.fixed::<4>("u32")?))
}
pub fn u64(&mut self) -> Result<u64, AstCoreError> {
Ok(u64::from_be_bytes(self.fixed::<8>("u64")?))
}
pub fn i64(&mut self) -> Result<i64, AstCoreError> {
Ok(i64::from_be_bytes(self.fixed::<8>("i64")?))
}
pub fn i128(&mut self) -> Result<i128, AstCoreError> {
Ok(i128::from_be_bytes(self.fixed::<16>("i128")?))
}
pub fn byte_vec(&mut self) -> Result<Vec<u8>, AstCoreError> {
let len = self.u32()? as usize;
Ok(self.take(len, "byte string payload")?.to_vec())
}
pub fn string(&mut self) -> Result<String, AstCoreError> {
let start = self.offset;
let bytes = self.byte_vec()?;
String::from_utf8(bytes).map_err(|_| AstCoreError::MalformedSnapshot {
offset: start,
expected: "UTF-8 string payload",
})
}
pub fn node_id(&mut self) -> Result<NodeId, AstCoreError> {
Ok(NodeId::from_bytes(self.fixed::<16>("node id")?))
}
pub fn expect_end(&mut self) -> Result<(), AstCoreError> {
if self.remaining() == 0 {
Ok(())
} else {
Err(AstCoreError::MalformedSnapshot {
offset: self.offset,
expected: "end of snapshot",
})
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use super::*;
use crate::id::IdSpace;
use alloc::string::ToString;
use alloc::vec;
#[test]
fn every_primitive_round_trips() {
let id = NodeId::from_parts(IdSpace::BUILTIN, 42);
let mut w = SnapshotWriter::new();
w.bool(true);
w.u8(0x7f);
w.u16(0xbeef);
w.u32(0xdead_beef);
w.u64(u64::MAX);
w.i64(i64::MIN);
w.i128(-170_141_183_460_469_231_731_687_303_715_884_105_728);
w.str("Robot::Pose").expect("str");
w.bytes(&[1, 2, 3]).expect("bytes");
w.node_id(id);
let encoded = w.finish();
let mut r = SnapshotReader::new(&encoded);
assert_eq!(r.bool(), Ok(true));
assert_eq!(r.u8(), Ok(0x7f));
assert_eq!(r.u16(), Ok(0xbeef));
assert_eq!(r.u32(), Ok(0xdead_beef));
assert_eq!(r.u64(), Ok(u64::MAX));
assert_eq!(r.i64(), Ok(i64::MIN));
assert_eq!(r.i128(), Ok(i128::MIN));
assert_eq!(r.string(), Ok("Robot::Pose".to_string()));
assert_eq!(r.byte_vec(), Ok(vec![1, 2, 3]));
assert_eq!(r.node_id(), Ok(id));
assert_eq!(r.expect_end(), Ok(()));
}
#[test]
fn encoding_the_same_values_twice_yields_identical_bytes() {
let build = || {
let mut w = SnapshotWriter::new();
w.u32(7);
w.str("Pose").expect("str");
w.node_id(NodeId::from_parts(IdSpace::MODEL, 3));
w.finish()
};
assert_eq!(build(), build());
}
#[test]
fn a_truncated_stream_is_an_error_not_a_partial_value() {
let mut w = SnapshotWriter::new();
w.u64(1);
let mut encoded = w.finish();
let _ = encoded.pop();
let mut r = SnapshotReader::new(&encoded);
assert!(matches!(
r.u64(),
Err(AstCoreError::MalformedSnapshot { .. })
));
}
#[test]
fn a_length_prefix_beyond_the_buffer_is_rejected() {
let encoded = vec![0xff, 0xff, 0xff, 0xff];
let mut r = SnapshotReader::new(&encoded);
assert!(matches!(
r.string(),
Err(AstCoreError::MalformedSnapshot { .. })
));
}
#[test]
fn trailing_bytes_are_rejected_by_expect_end() {
let mut w = SnapshotWriter::new();
w.u8(1);
let mut encoded = w.finish();
encoded.push(0);
let mut r = SnapshotReader::new(&encoded);
assert_eq!(r.u8(), Ok(1));
assert!(r.expect_end().is_err());
}
#[test]
fn non_utf8_string_payload_is_rejected() {
let mut w = SnapshotWriter::new();
w.bytes(&[0xff, 0xfe]).expect("bytes");
let encoded = w.finish();
let mut r = SnapshotReader::new(&encoded);
assert!(r.string().is_err());
}
#[test]
fn a_boolean_byte_outside_zero_and_one_is_rejected() {
let encoded = vec![2];
let mut r = SnapshotReader::new(&encoded);
assert!(matches!(
r.bool(),
Err(AstCoreError::MalformedSnapshot { .. })
));
}
#[test]
fn the_header_round_trips_and_a_wrong_magic_is_rejected() {
let w = SnapshotWriter::with_header();
let encoded = w.finish();
let mut r = SnapshotReader::with_header(&encoded).expect("header");
assert_eq!(r.expect_end(), Ok(()));
let mut wrong = encoded.clone();
wrong[0] = b'X';
assert!(SnapshotReader::with_header(&wrong).is_err());
}
#[test]
fn an_unknown_version_is_rejected() {
let mut w = SnapshotWriter::new();
w.u8(b'Z');
w.u8(b'D');
w.u8(b'D');
w.u8(b'S');
w.u8(b'A');
w.u8(b'S');
w.u8(b'T');
w.u8(0);
w.u16(999);
let encoded = w.finish();
assert!(SnapshotReader::with_header(&encoded).is_err());
}
#[test]
fn a_buffer_too_short_for_the_header_is_rejected() {
assert!(SnapshotReader::with_header(&[0u8; 3]).is_err());
}
}