use std::io;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error;
use tokio::io::{AsyncRead, AsyncReadExt};
use crate::frame;
pub const STATE_V2_MAX_FRAME: usize = 1024 * 1024;
pub const STATE_WIRE_VERSION: u16 = 4;
#[derive(Debug, Error)]
pub enum StateError {
#[error("json error: {0}")]
Json(#[from] serde_json::Error),
#[error("msgpack encode error: {0}")]
Encode(#[from] rmp_serde::encode::Error),
#[error("msgpack decode error: {0}")]
Decode(#[from] rmp_serde::decode::Error),
#[error("framing error: {0}")]
Frame(#[from] frame::FrameError),
#[error("state wire version mismatch: got {got}, expected {ours}")]
Version { got: u16, ours: u16 },
}
pub fn encode_v1(state: &Value) -> Result<Vec<u8>, StateError> {
let mut buf = serde_json::to_vec(state)?;
buf.push(b'\n');
Ok(buf)
}
pub fn decode_v1_line(line: &[u8]) -> Result<Value, StateError> {
let trimmed = line.strip_suffix(b"\n").unwrap_or(line);
Ok(serde_json::from_slice(trimmed)?)
}
#[derive(Debug, Serialize, Deserialize)]
struct StateFrameV2 {
#[serde(rename = "v")]
v: u16,
#[serde(rename = "s")]
s: Value,
}
#[derive(Serialize)]
struct StateFrameV2Ref<'a> {
#[serde(rename = "v")]
v: u16,
#[serde(rename = "s")]
s: &'a Value,
}
pub fn encode_v2(state: &Value) -> Result<Vec<u8>, StateError> {
let body = rmp_serde::to_vec_named(&StateFrameV2Ref {
v: STATE_WIRE_VERSION,
s: state,
})?;
Ok(frame::encode_frame(&body, STATE_V2_MAX_FRAME)?)
}
pub fn decode_v2(body: &[u8]) -> Result<Value, StateError> {
let frame: StateFrameV2 = rmp_serde::from_slice(body)?;
if frame.v != STATE_WIRE_VERSION {
return Err(StateError::Version {
got: frame.v,
ours: STATE_WIRE_VERSION,
});
}
Ok(frame.s)
}
enum StateFrame {
Value(Value),
Skip,
VersionMismatch { got: u16 },
}
pub const MAX_CONSECUTIVE_VERSION_MISMATCHES: u32 = 8;
static VERSION_MISMATCH_SKIPS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
pub fn version_mismatch_skips() -> u64 {
VERSION_MISMATCH_SKIPS.load(std::sync::atomic::Ordering::Relaxed)
}
struct MismatchRun(u32);
impl MismatchRun {
fn record(&mut self, got: u16) -> io::Result<()> {
VERSION_MISMATCH_SKIPS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
self.0 += 1;
if self.0 < MAX_CONSECUTIVE_VERSION_MISMATCHES {
return Ok(());
}
tracing::warn!(
got,
ours = STATE_WIRE_VERSION,
skipped = self.0,
"state format mismatch: the producer speaks a different state wire version"
);
Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"state format mismatch: producer sends state wire version {got}, \
this reader expects {STATE_WIRE_VERSION}"
),
))
}
}
fn v2_body_len(header: [u8; frame::HEADER_SIZE]) -> io::Result<usize> {
let len = u32::from_be_bytes(header) as usize;
if len == 0 || len > STATE_V2_MAX_FRAME {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"state v2 frame length out of range",
));
}
Ok(len)
}
fn decode_state_body(is_v2: bool, body: &[u8]) -> StateFrame {
if is_v2 {
match decode_v2(body) {
Ok(v) => StateFrame::Value(v),
Err(StateError::Version { got, .. }) => StateFrame::VersionMismatch { got },
Err(_) => StateFrame::Skip,
}
} else {
decode_v1_line(body).map_or(StateFrame::Skip, StateFrame::Value)
}
}
pub async fn read_state_value<R>(reader: &mut R) -> io::Result<Option<Value>>
where
R: AsyncRead + Unpin,
{
read_state_value_into(reader, &mut Vec::new()).await
}
pub async fn read_state_value_into<R>(
reader: &mut R,
buf: &mut Vec<u8>,
) -> io::Result<Option<Value>>
where
R: AsyncRead + Unpin,
{
let mut mismatches = MismatchRun(0);
loop {
match read_state_frame_into(reader, buf).await? {
Some(StateFrame::Value(v)) => return Ok(Some(v)),
Some(StateFrame::Skip) => continue,
Some(StateFrame::VersionMismatch { got }) => mismatches.record(got)?,
None => return Ok(None),
}
}
}
async fn read_state_frame_into<R>(
reader: &mut R,
buf: &mut Vec<u8>,
) -> io::Result<Option<StateFrame>>
where
R: AsyncRead + Unpin,
{
let mut first = [0u8; 1];
match reader.read_exact(&mut first).await {
Ok(_) => {}
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(e),
}
buf.clear();
if first[0] == 0x00 {
let mut rest = [0u8; frame::HEADER_SIZE - 1];
reader.read_exact(&mut rest).await?;
let len = v2_body_len([first[0], rest[0], rest[1], rest[2]])?;
buf.resize(len, 0);
reader.read_exact(buf.as_mut_slice()).await?;
Ok(Some(decode_state_body(true, buf)))
} else {
buf.push(first[0]);
let mut byte = [0u8; 1];
loop {
match reader.read_exact(&mut byte).await {
Ok(_) => {}
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(e),
}
if byte[0] == b'\n' {
break;
}
if buf.len() >= STATE_V2_MAX_FRAME {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"state v1 line exceeded the frame cap without a newline",
));
}
buf.push(byte[0]);
}
Ok(Some(decode_state_body(false, buf)))
}
}
pub fn read_state_value_blocking<R>(reader: &mut R) -> io::Result<Option<Value>>
where
R: io::Read,
{
let mut mismatches = MismatchRun(0);
loop {
match read_state_frame_blocking(reader)? {
Some(StateFrame::Value(v)) => return Ok(Some(v)),
Some(StateFrame::Skip) => continue,
Some(StateFrame::VersionMismatch { got }) => mismatches.record(got)?,
None => return Ok(None),
}
}
}
fn read_state_frame_blocking<R>(reader: &mut R) -> io::Result<Option<StateFrame>>
where
R: io::Read,
{
let mut first = [0u8; 1];
match reader.read_exact(&mut first) {
Ok(_) => {}
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(e),
}
if first[0] == 0x00 {
let mut rest = [0u8; frame::HEADER_SIZE - 1];
reader.read_exact(&mut rest)?;
let len = v2_body_len([first[0], rest[0], rest[1], rest[2]])?;
let mut body = vec![0u8; len];
reader.read_exact(&mut body)?;
Ok(Some(decode_state_body(true, &body)))
} else {
let mut line = vec![first[0]];
let mut byte = [0u8; 1];
loop {
match reader.read_exact(&mut byte) {
Ok(_) => {}
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(e),
}
if byte[0] == b'\n' {
break;
}
if line.len() >= STATE_V2_MAX_FRAME {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"state v1 line exceeded the frame cap without a newline",
));
}
line.push(byte[0]);
}
Ok(Some(decode_state_body(false, &line)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn sample() -> Value {
json!({
"armed": false,
"mode": "STABILIZE",
"battery": {"voltage": 16.4, "remaining": 87},
"gps": {"fix": 3, "sats": 14, "lat": 12.9716, "lon": 77.5946},
"attitude": {"roll": 0.01, "pitch": -0.02, "yaw": 1.57},
"link": {"rssi_dbm": -48, "valid_rx_packets_per_s": 630}
})
}
#[test]
fn v1_round_trip_with_newline() {
let state = sample();
let wire = encode_v1(&state).unwrap();
assert_eq!(*wire.last().unwrap(), b'\n');
let back = decode_v1_line(&wire).unwrap();
assert_eq!(state, back);
}
#[test]
fn v1_decode_tolerates_missing_newline() {
let state = sample();
let json = serde_json::to_vec(&state).unwrap();
assert_eq!(decode_v1_line(&json).unwrap(), state);
}
#[test]
fn v1_decode_rejects_malformed_json() {
assert!(decode_v1_line(b"{not json}\n").is_err());
}
#[test]
fn v2_round_trip() {
let state = sample();
let frame_bytes = encode_v2(&state).unwrap();
let body = &frame_bytes[frame::HEADER_SIZE..];
let raw: StateFrameV2 = rmp_serde::from_slice(body).unwrap();
assert_eq!(raw.v, STATE_WIRE_VERSION);
assert_eq!(raw.s, state);
let back = decode_v2(body).unwrap();
assert_eq!(state, back);
}
#[test]
fn version_matches_registry() {
assert_eq!(
STATE_WIRE_VERSION,
crate::contracts::contract_version("state.v2").unwrap()
);
}
#[test]
fn decode_v2_rejects_a_future_version() {
let body = rmp_serde::to_vec_named(&StateFrameV2Ref {
v: STATE_WIRE_VERSION + 1,
s: &sample(),
})
.unwrap();
match decode_v2(&body) {
Err(StateError::Version { got, ours }) => {
assert_eq!(got, STATE_WIRE_VERSION + 1);
assert_eq!(ours, STATE_WIRE_VERSION);
}
other => panic!("expected a Version error, got {other:?}"),
}
}
#[test]
fn v1_and_v2_preserve_the_same_fields() {
let state = sample();
let via_v1 = decode_v1_line(&encode_v1(&state).unwrap()).unwrap();
let frame_bytes = encode_v2(&state).unwrap();
let via_v2 = decode_v2(&frame_bytes[frame::HEADER_SIZE..]).unwrap();
assert_eq!(via_v1, via_v2);
}
#[test]
fn leading_byte_discriminates_the_two_wire_forms() {
assert_eq!(encode_v2(&sample()).unwrap()[0], 0x00);
assert_eq!(encode_v1(&sample()).unwrap()[0], b'{');
}
#[tokio::test]
async fn read_state_value_decodes_v1_and_v2_identically() {
let state = sample();
let mut v1 = std::io::Cursor::new(encode_v1(&state).unwrap());
let mut v2 = std::io::Cursor::new(encode_v2(&state).unwrap());
let from_v1 = read_state_value(&mut v1).await.unwrap().unwrap();
let from_v2 = read_state_value(&mut v2).await.unwrap().unwrap();
assert_eq!(from_v1, state);
assert_eq!(from_v2, state);
}
#[tokio::test]
async fn read_state_value_reads_a_mixed_v1_v2_stream_in_order() {
let a = json!({"mode": "STABILIZE"});
let b = json!({"mode": "GUIDED"});
let mut wire = encode_v1(&a).unwrap();
wire.extend(encode_v2(&b).unwrap());
let mut r = std::io::Cursor::new(wire);
assert_eq!(read_state_value(&mut r).await.unwrap().unwrap(), a);
assert_eq!(read_state_value(&mut r).await.unwrap().unwrap(), b);
assert!(read_state_value(&mut r).await.unwrap().is_none());
}
#[tokio::test]
async fn read_state_value_into_reuses_one_buffer_across_frames() {
let a = json!({"mode": "STABILIZE", "pad": "x".repeat(512)});
let b = json!({"mode": "GUIDED"});
let mut wire = encode_v2(&a).unwrap();
wire.extend(encode_v1(&b).unwrap());
wire.extend(encode_v2(&b).unwrap());
let mut r = std::io::Cursor::new(wire);
let mut buf = Vec::new();
assert_eq!(
read_state_value_into(&mut r, &mut buf)
.await
.unwrap()
.unwrap(),
a
);
let grown = buf.capacity();
assert!(grown >= 512);
assert_eq!(
read_state_value_into(&mut r, &mut buf)
.await
.unwrap()
.unwrap(),
b
);
assert_eq!(
read_state_value_into(&mut r, &mut buf)
.await
.unwrap()
.unwrap(),
b
);
assert_eq!(
buf.capacity(),
grown,
"the smaller frames reused the allocation"
);
assert!(read_state_value_into(&mut r, &mut buf)
.await
.unwrap()
.is_none());
}
#[tokio::test]
async fn read_state_value_skips_a_malformed_frame_then_reads_the_next() {
let good = sample();
let mut wire = b"{ not json }\n".to_vec();
wire.extend(encode_v2(&good).unwrap());
let mut r = std::io::Cursor::new(wire);
assert_eq!(read_state_value(&mut r).await.unwrap().unwrap(), good);
}
#[tokio::test]
async fn read_state_value_returns_none_on_clean_eof() {
let mut r = std::io::Cursor::new(Vec::new());
assert!(read_state_value(&mut r).await.unwrap().is_none());
}
#[test]
fn read_state_value_blocking_decodes_both_wire_forms() {
let state = sample();
let mut v1 = std::io::Cursor::new(encode_v1(&state).unwrap());
let mut v2 = std::io::Cursor::new(encode_v2(&state).unwrap());
assert_eq!(read_state_value_blocking(&mut v1).unwrap().unwrap(), state);
assert_eq!(read_state_value_blocking(&mut v2).unwrap().unwrap(), state);
}
fn foreign_version_frame() -> Vec<u8> {
let body = rmp_serde::to_vec_named(&StateFrameV2Ref {
v: STATE_WIRE_VERSION + 1,
s: &sample(),
})
.unwrap();
frame::encode_frame(&body, STATE_V2_MAX_FRAME).unwrap()
}
#[tokio::test]
async fn a_stream_of_foreign_versions_is_an_error_not_a_silent_stall() {
let mut wire = Vec::new();
for _ in 0..MAX_CONSECUTIVE_VERSION_MISMATCHES {
wire.extend(foreign_version_frame());
}
wire.extend(encode_v2(&sample()).unwrap());
let before = version_mismatch_skips();
let mut r = std::io::Cursor::new(wire.clone());
let err = read_state_value(&mut r).await.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
assert!(err.to_string().contains("state format mismatch"), "{err}");
assert!(version_mismatch_skips() >= before + MAX_CONSECUTIVE_VERSION_MISMATCHES as u64);
let mut r = std::io::Cursor::new(wire);
assert!(read_state_value_blocking(&mut r).is_err());
}
#[tokio::test]
async fn a_single_foreign_version_frame_is_skipped() {
let mut wire = foreign_version_frame();
wire.extend(encode_v2(&sample()).unwrap());
let mut r = std::io::Cursor::new(wire);
assert_eq!(read_state_value(&mut r).await.unwrap().unwrap(), sample());
}
}