260 lines
9.3 KiB
Rust
260 lines
9.3 KiB
Rust
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
//! Length-prefixed `postcard` frame codec.
|
|
//!
|
|
//! Encodes and decodes one logical protocol message per record on a QUIC stream, using a length prefix so a reader can recover record boundaries from a byte stream.
|
|
|
|
/// Errors produced by the framing codec and its stream I/O helpers.
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum NetError {
|
|
/// A `postcard` serialization or deserialization operation failed.
|
|
#[error("postcard codec error: {0}")]
|
|
Postcard(#[from] postcard::Error),
|
|
/// An underlying byte-stream I/O operation failed.
|
|
#[error("i/o error: {0}")]
|
|
Io(#[from] std::io::Error),
|
|
/// A declared frame length exceeded the caller-supplied maximum, indicating a malicious or corrupt peer.
|
|
#[error("frame length {len} exceeds maximum {max}")]
|
|
FrameTooLarge {
|
|
/// The frame length declared by the length prefix, in bytes.
|
|
len: u64,
|
|
/// The maximum payload length accepted by the reader, in bytes.
|
|
max: usize,
|
|
},
|
|
/// The stream ended before a complete frame (prefix or payload) had been read.
|
|
#[error("unexpected end of stream while reading a frame")]
|
|
UnexpectedEof,
|
|
/// A varint length prefix was malformed: either overlong or otherwise invalid.
|
|
#[error("malformed varint length prefix")]
|
|
MalformedVarint,
|
|
/// Writing bytes to a quinn send stream failed.
|
|
#[error("quinn write error: {0}")]
|
|
Write(#[from] quinn::WriteError),
|
|
/// Reading an exact number of bytes from a quinn recv stream failed.
|
|
#[error("quinn read error: {0}")]
|
|
Read(#[from] quinn::ReadExactError),
|
|
}
|
|
|
|
/// The maximum payload length, in bytes, accepted on the control stream (64 KiB), matching the mod-payload cap. Higher-bandwidth tiers such as chunk streaming define their own caps.
|
|
pub const MAX_CONTROL_FRAME_LEN: usize = 64 * 1024;
|
|
|
|
/// The maximum number of bytes an unsigned LEB128 varint may occupy for a `u64` value (`ceil(64 / 7)`).
|
|
const MAX_VARINT_LEN: usize = 10;
|
|
|
|
/// Appends `value` to `buf` as an unsigned LEB128 varint.
|
|
///
|
|
/// Each byte carries seven value bits in little-endian group order; the high bit (`0x80`) is a continuation flag set on every byte except the last.
|
|
fn write_varint(value: u64, buf: &mut Vec<u8>) {
|
|
let mut remaining = value;
|
|
loop {
|
|
// Extract the low seven bits of the remaining value.
|
|
let mut byte = (remaining & 0x7f) as u8;
|
|
remaining >>= 7;
|
|
if remaining != 0 {
|
|
// Further bytes follow, so mark the continuation bit.
|
|
byte |= 0x80;
|
|
}
|
|
buf.push(byte);
|
|
if remaining == 0 {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Reads an unsigned LEB128 varint from the front of `bytes`, returning the decoded value and the number of bytes consumed.
|
|
fn read_varint(bytes: &[u8]) -> Result<(u64, usize), NetError> {
|
|
let mut value: u64 = 0;
|
|
let mut shift: u32 = 0;
|
|
for (index, &byte) in bytes.iter().enumerate() {
|
|
if index >= MAX_VARINT_LEN {
|
|
return Err(NetError::MalformedVarint);
|
|
}
|
|
// Accumulate the seven payload bits at their little-endian position.
|
|
value |= u64::from(byte & 0x7f) << shift;
|
|
if byte & 0x80 == 0 {
|
|
return Ok((value, index + 1));
|
|
}
|
|
shift += 7;
|
|
}
|
|
// The continuation bit was still set when the buffer ran out.
|
|
Err(NetError::UnexpectedEof)
|
|
}
|
|
|
|
/// Encodes `msg` as a single length-prefixed `postcard` frame into a freshly allocated buffer.
|
|
fn encode_frame<T: serde::Serialize>(msg: &T) -> Result<Vec<u8>, NetError> {
|
|
let payload = postcard::to_stdvec(msg)?;
|
|
let mut frame = Vec::new();
|
|
write_varint(payload.len() as u64, &mut frame);
|
|
frame.extend_from_slice(&payload);
|
|
Ok(frame)
|
|
}
|
|
|
|
/// Writes one length-prefixed `postcard` frame to a quinn send stream.
|
|
pub async fn write_frame<T: serde::Serialize>(
|
|
stream: &mut quinn::SendStream,
|
|
msg: &T,
|
|
) -> Result<(), NetError> {
|
|
let frame = encode_frame(msg)?;
|
|
stream.write_all(&frame).await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Reads one length-prefixed `postcard` frame from a quinn recv stream and decodes it.
|
|
pub async fn read_frame<T: serde::de::DeserializeOwned>(
|
|
stream: &mut quinn::RecvStream,
|
|
max_len: usize,
|
|
) -> Result<T, NetError> {
|
|
// The prefix length is not known in advance, so bytes are pulled one at a time until a byte without the continuation flag is read, then decoded by the shared pure helper.
|
|
let mut prefix = Vec::with_capacity(MAX_VARINT_LEN);
|
|
loop {
|
|
let mut byte = [0u8; 1];
|
|
stream.read_exact(&mut byte).await?;
|
|
prefix.push(byte[0]);
|
|
if byte[0] & 0x80 == 0 {
|
|
break;
|
|
}
|
|
if prefix.len() > MAX_VARINT_LEN {
|
|
return Err(NetError::MalformedVarint);
|
|
}
|
|
}
|
|
let (len, _consumed) = read_varint(&prefix)?;
|
|
|
|
// The declared length is validated before allocating the payload buffer.
|
|
let checked_len = check_frame_len(len, max_len)?;
|
|
let mut payload = vec![0u8; checked_len];
|
|
stream.read_exact(&mut payload).await?;
|
|
Ok(postcard::from_bytes(&payload)?)
|
|
}
|
|
|
|
/// Validates a declared frame length against `max_len`.
|
|
fn check_frame_len(len: u64, max_len: usize) -> Result<usize, NetError> {
|
|
if len > max_len as u64 {
|
|
return Err(NetError::FrameTooLarge { len, max: max_len });
|
|
}
|
|
#[expect(
|
|
clippy::cast_possible_truncation,
|
|
reason = "len <= max_len (usize) checked above"
|
|
)]
|
|
Ok(len as usize)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use shared::protocol::{
|
|
ClientHello, ControlMessage, FeatureFlags, PROTOCOL_VERSION, PlayerIdentity,
|
|
};
|
|
|
|
/// Round-trips a set of boundary values through the varint codec, asserting both the decoded value and the exact byte length consumed.
|
|
#[test]
|
|
fn varint_round_trip_boundaries() -> Result<(), NetError> {
|
|
let cases = [
|
|
0,
|
|
1,
|
|
127,
|
|
128,
|
|
16_383,
|
|
16_384,
|
|
u64::from(u32::MAX),
|
|
u64::MAX,
|
|
];
|
|
for value in cases {
|
|
let mut buf = Vec::new();
|
|
write_varint(value, &mut buf);
|
|
let (decoded, consumed) = read_varint(&buf)?;
|
|
assert_eq!(decoded, value, "decoded value mismatch");
|
|
assert_eq!(
|
|
consumed,
|
|
buf.len(),
|
|
"consumed length must equal encoded length"
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// A frame encodes as `[varint payload length][payload]`, and decoding the prefix recovers exactly the payload byte count.
|
|
#[test]
|
|
fn encode_frame_prefixes_payload_length() -> Result<(), NetError> {
|
|
let msg = ControlMessage::ClientHello(ClientHello {
|
|
protocol_version: PROTOCOL_VERSION,
|
|
client_build: "synvael-client-0.1.0".to_string(),
|
|
player_identity: PlayerIdentity {
|
|
display_name: "Player1".to_string(),
|
|
},
|
|
installed_packs: Vec::new(),
|
|
requested_features: FeatureFlags(0),
|
|
});
|
|
let payload = postcard::to_stdvec(&msg)?;
|
|
let frame = encode_frame(&msg)?;
|
|
|
|
let (declared_len, prefix_bytes) = read_varint(&frame)?;
|
|
assert_eq!(
|
|
declared_len,
|
|
payload.len() as u64,
|
|
"prefix must equal payload length"
|
|
);
|
|
assert_eq!(
|
|
&frame[prefix_bytes..],
|
|
payload.as_slice(),
|
|
"payload bytes must follow the prefix unchanged"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Confirms the documented one/two-byte boundary encodings so a regression in the continuation logic is caught directly.
|
|
#[test]
|
|
fn varint_boundary_lengths() {
|
|
let mut buf = Vec::new();
|
|
write_varint(127, &mut buf);
|
|
assert_eq!(buf.len(), 1, "127 must encode in a single byte");
|
|
|
|
buf.clear();
|
|
write_varint(128, &mut buf);
|
|
assert_eq!(buf.len(), 2, "128 must encode in two bytes");
|
|
}
|
|
|
|
/// A varint whose final byte still sets the continuation bit is a truncated buffer and must error rather than panic.
|
|
#[test]
|
|
fn varint_truncated_is_error() {
|
|
// Two bytes both flagged as "continued", with no terminating byte.
|
|
let truncated = [0x80u8, 0x80u8];
|
|
assert!(
|
|
read_varint(&truncated).is_err(),
|
|
"truncated varint must return an error"
|
|
);
|
|
}
|
|
|
|
/// An encoding longer than the ten bytes a `u64` can occupy is rejected as overlong rather than silently accepted.
|
|
#[test]
|
|
fn varint_overlong_is_error() {
|
|
// Eleven continuation bytes followed by a terminator exceeds the u64 limit.
|
|
let overlong = [0x80u8; 11];
|
|
assert!(
|
|
read_varint(&overlong).is_err(),
|
|
"overlong varint must return an error"
|
|
);
|
|
}
|
|
|
|
/// A declared length within the cap is accepted; one exceeding it is rejected as `FrameTooLarge` before any allocation.
|
|
#[test]
|
|
fn frame_len_bound_is_enforced() {
|
|
assert_eq!(
|
|
check_frame_len(64, 128).ok(),
|
|
Some(64),
|
|
"a length within the cap is accepted"
|
|
);
|
|
assert_eq!(
|
|
check_frame_len(128, 128).ok(),
|
|
Some(128),
|
|
"a length equal to the cap is accepted"
|
|
);
|
|
assert!(
|
|
matches!(
|
|
check_frame_len(129, 128),
|
|
Err(NetError::FrameTooLarge { len: 129, max: 128 })
|
|
),
|
|
"a length over the cap must be rejected",
|
|
);
|
|
}
|
|
}
|