mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-09 06:47:59 -06:00
Rebuild protocol runtime from scratch
Implement an aligned two-section frame format, a compiled prefix router, a minimal pending and active hook engine, and a header-first receive path that only decodes payloads on local delivery. Recreate the protocol-focused test suite and document the explicit framing deviation in src/protocol/PROTOCOL_CHANGES.md.
This commit is contained in:
+111
-148
@@ -1,32 +1,28 @@
|
||||
//! Framed packet encoding and decoding.
|
||||
//!
|
||||
//! This module provides the `FrameCodec` trait, which abstracts the conversion
|
||||
//! between owned packet structures and the canonical length-prefixed wire format.
|
||||
|
||||
use alloc::{boxed::Box, vec::Vec};
|
||||
use core::fmt;
|
||||
use rkyv::{Serialize, access, deserialize, rancor::Error, to_bytes, util::AlignedVec};
|
||||
use core::{fmt, mem};
|
||||
use rkyv::{
|
||||
Serialize, access, deserialize, rancor::Error, to_bytes,
|
||||
util::AlignedVec,
|
||||
};
|
||||
|
||||
use super::types::{
|
||||
ArchivedCallMessage, ArchivedDataMessage, ArchivedFaultMessage, ArchivedPacketHeader,
|
||||
};
|
||||
use crate::protocol::{CallMessage, DataMessage, FaultMessage, PacketHeader, PacketType};
|
||||
|
||||
/// Archived-section alignment guaranteed by the frame format.
|
||||
pub const SECTION_ALIGN: usize = 16;
|
||||
|
||||
/// Owned framed packet bytes.
|
||||
pub type FrameBytes = Box<[u8]>;
|
||||
pub type FrameBytes = AlignedVec<SECTION_ALIGN>;
|
||||
|
||||
/// Framing or archive failure.
|
||||
#[derive(Debug)]
|
||||
pub enum FrameError {
|
||||
/// The frame is truncated or contains trailing bytes.
|
||||
Truncated,
|
||||
/// Header bytes were not a valid archive.
|
||||
InvalidHeader(Error),
|
||||
/// Payload bytes were not a valid archive.
|
||||
InvalidPayload(Error),
|
||||
/// Serialization failed.
|
||||
Serialize(Error),
|
||||
/// The framed section exceeded the `u32` wire limit.
|
||||
LengthOverflow,
|
||||
}
|
||||
|
||||
@@ -44,180 +40,110 @@ impl fmt::Display for FrameError {
|
||||
|
||||
impl core::error::Error for FrameError {}
|
||||
|
||||
/// A view into a framed packet, providing access to archived sections.
|
||||
/// Parsed frame with one owned header and a borrowed payload section.
|
||||
pub struct ParsedFrame<'a> {
|
||||
header: PacketHeader,
|
||||
payload_bytes: &'a [u8],
|
||||
}
|
||||
|
||||
impl<'a> ParsedFrame<'a> {
|
||||
/// Returns the deserialized packet header.
|
||||
///
|
||||
/// The header is owned by `ParsedFrame` because decoding must validate it
|
||||
/// before any routing decision is made.
|
||||
#[must_use]
|
||||
pub fn header(&self) -> &PacketHeader {
|
||||
&self.header
|
||||
}
|
||||
|
||||
/// Returns the header packet type for quick dispatch.
|
||||
#[must_use]
|
||||
pub fn packet_type(&self) -> PacketType {
|
||||
self.header.packet_type
|
||||
}
|
||||
|
||||
/// Returns the raw archived payload section.
|
||||
#[must_use]
|
||||
pub fn payload_bytes(&self) -> &'a [u8] {
|
||||
self.payload_bytes
|
||||
}
|
||||
|
||||
/// Clones the decoded header out of the parsed frame.
|
||||
#[must_use]
|
||||
pub fn deserialize_header(&self) -> PacketHeader {
|
||||
self.header.clone()
|
||||
}
|
||||
|
||||
/// Consumes the parsed frame and returns its owned header and borrowed payload.
|
||||
#[must_use]
|
||||
pub fn into_parts(self) -> (PacketHeader, &'a [u8]) {
|
||||
(self.header, self.payload_bytes)
|
||||
}
|
||||
|
||||
/// Deserializes the payload as a [`CallMessage`].
|
||||
pub fn deserialize_call(&self) -> Result<CallMessage, FrameError> {
|
||||
deserialize_archived_bytes::<ArchivedCallMessage, CallMessage>(self.payload_bytes)
|
||||
}
|
||||
|
||||
/// Deserializes the payload as a [`DataMessage`].
|
||||
pub fn deserialize_data(&self) -> Result<DataMessage, FrameError> {
|
||||
deserialize_archived_bytes::<ArchivedDataMessage, DataMessage>(self.payload_bytes)
|
||||
}
|
||||
|
||||
/// Deserializes the payload as a [`FaultMessage`].
|
||||
pub fn deserialize_fault(&self) -> Result<FaultMessage, FrameError> {
|
||||
deserialize_archived_bytes::<ArchivedFaultMessage, FaultMessage>(self.payload_bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for framing and unframing packets.
|
||||
pub trait FrameCodec {
|
||||
/// Encodes a packet header and payload into the canonical framed representation.
|
||||
fn encode_packet<P>(header: &PacketHeader, payload: &P) -> Result<FrameBytes, FrameError>
|
||||
where
|
||||
P: for<'a> Serialize<
|
||||
rkyv::api::high::HighSerializer<
|
||||
AlignedVec,
|
||||
rkyv::ser::allocator::ArenaHandle<'a>,
|
||||
Error,
|
||||
>,
|
||||
>;
|
||||
|
||||
/// Decodes a framed packet into a borrowed parsed view.
|
||||
fn decode_frame(bytes: &[u8]) -> Result<ParsedFrame<'_>, FrameError>;
|
||||
}
|
||||
|
||||
/// Default implementation of the `FrameCodec` using `rkyv`.
|
||||
pub struct RkyvCodec;
|
||||
|
||||
impl FrameCodec for RkyvCodec {
|
||||
fn encode_packet<P>(header: &PacketHeader, payload: &P) -> Result<FrameBytes, FrameError>
|
||||
where
|
||||
P: for<'a> Serialize<
|
||||
rkyv::api::high::HighSerializer<
|
||||
AlignedVec,
|
||||
rkyv::ser::allocator::ArenaHandle<'a>,
|
||||
Error,
|
||||
>,
|
||||
>,
|
||||
{
|
||||
// WARNING: framed packets move as one contiguous buffer across the core boundary.
|
||||
// Keeping ownership here avoids hidden copies later in routing code.
|
||||
let header_bytes = to_bytes::<Error>(header).map_err(FrameError::Serialize)?;
|
||||
let payload_bytes = to_bytes::<Error>(payload).map_err(FrameError::Serialize)?;
|
||||
let header_len =
|
||||
u32::try_from(header_bytes.len()).map_err(|_| FrameError::LengthOverflow)?;
|
||||
let payload_len =
|
||||
u32::try_from(payload_bytes.len()).map_err(|_| FrameError::LengthOverflow)?;
|
||||
|
||||
let mut frame = Vec::with_capacity(8 + header_bytes.len() + payload_bytes.len());
|
||||
frame.extend_from_slice(&header_len.to_be_bytes());
|
||||
frame.extend_from_slice(&header_bytes);
|
||||
frame.extend_from_slice(&payload_len.to_be_bytes());
|
||||
frame.extend_from_slice(&payload_bytes);
|
||||
Ok(frame.into_boxed_slice())
|
||||
}
|
||||
|
||||
fn decode_frame(bytes: &[u8]) -> Result<ParsedFrame<'_>, FrameError> {
|
||||
if bytes.len() < 8 {
|
||||
return Err(FrameError::Truncated);
|
||||
}
|
||||
|
||||
let header_len = u32::from_be_bytes(
|
||||
bytes
|
||||
.get(0..4)
|
||||
.ok_or(FrameError::Truncated)?
|
||||
.try_into()
|
||||
.expect("slice width checked"),
|
||||
) as usize;
|
||||
let header_start = 4usize;
|
||||
let header_end = header_start + header_len;
|
||||
if header_end + 4 > bytes.len() {
|
||||
return Err(FrameError::Truncated);
|
||||
}
|
||||
|
||||
let payload_len = u32::from_be_bytes(
|
||||
bytes
|
||||
.get(header_end..header_end + 4)
|
||||
.ok_or(FrameError::Truncated)?
|
||||
.try_into()
|
||||
.expect("slice width checked"),
|
||||
) as usize;
|
||||
let payload_start = header_end + 4;
|
||||
let payload_end = payload_start + payload_len;
|
||||
if payload_end != bytes.len() {
|
||||
return Err(FrameError::Truncated);
|
||||
}
|
||||
|
||||
// WARNING: the wire format puts a 4-byte length prefix before each archived section.
|
||||
// That means the section start is not guaranteed to satisfy rkyv's aligned-access
|
||||
// requirements. The header is copied into one temporary `AlignedVec` here because
|
||||
// routing cannot proceed safely without a validated header.
|
||||
let aligned_header = align_section(
|
||||
bytes
|
||||
.get(header_start..header_end)
|
||||
.ok_or(FrameError::Truncated)?,
|
||||
);
|
||||
let archived_header = access::<ArchivedPacketHeader, Error>(&aligned_header)
|
||||
.map_err(FrameError::InvalidHeader)?;
|
||||
let header = deserialize::<PacketHeader, Error>(archived_header)
|
||||
.map_err(FrameError::InvalidHeader)?;
|
||||
|
||||
Ok(ParsedFrame {
|
||||
header,
|
||||
payload_bytes: bytes
|
||||
.get(payload_start..payload_end)
|
||||
.ok_or(FrameError::Truncated)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Encodes a packet header and payload using the default codec.
|
||||
/// Encodes a packet header and payload using the aligned two-section frame format.
|
||||
pub fn encode_packet<P>(header: &PacketHeader, payload: &P) -> Result<FrameBytes, FrameError>
|
||||
where
|
||||
P: for<'a> Serialize<
|
||||
rkyv::api::high::HighSerializer<AlignedVec, rkyv::ser::allocator::ArenaHandle<'a>, Error>,
|
||||
rkyv::api::high::HighSerializer<
|
||||
AlignedVec,
|
||||
rkyv::ser::allocator::ArenaHandle<'a>,
|
||||
Error,
|
||||
>,
|
||||
>,
|
||||
{
|
||||
RkyvCodec::encode_packet(header, payload)
|
||||
let header_bytes: FrameBytes = to_bytes::<Error>(header).map_err(FrameError::Serialize)?;
|
||||
let payload_bytes: FrameBytes = to_bytes::<Error>(payload).map_err(FrameError::Serialize)?;
|
||||
let header_len = u32::try_from(header_bytes.len()).map_err(|_| FrameError::LengthOverflow)?;
|
||||
let payload_len =
|
||||
u32::try_from(payload_bytes.len()).map_err(|_| FrameError::LengthOverflow)?;
|
||||
|
||||
let header_start = 8usize;
|
||||
let payload_start = align_up(header_start + header_bytes.len(), SECTION_ALIGN);
|
||||
let total_len = payload_start + payload_bytes.len();
|
||||
|
||||
let mut frame = FrameBytes::with_capacity(total_len);
|
||||
frame.extend_from_slice(&header_len.to_be_bytes());
|
||||
frame.extend_from_slice(&payload_len.to_be_bytes());
|
||||
frame.extend_from_slice(&header_bytes);
|
||||
append_padding(&mut frame, payload_start - (header_start + header_bytes.len()));
|
||||
frame.extend_from_slice(&payload_bytes);
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
/// Decodes a framed packet using the default codec.
|
||||
/// Decodes one aligned two-section frame.
|
||||
pub fn decode_frame(bytes: &[u8]) -> Result<ParsedFrame<'_>, FrameError> {
|
||||
RkyvCodec::decode_frame(bytes)
|
||||
if bytes.len() < 8 {
|
||||
return Err(FrameError::Truncated);
|
||||
}
|
||||
|
||||
let header_len = read_u32(bytes, 0)? as usize;
|
||||
let payload_len = read_u32(bytes, 4)? as usize;
|
||||
let header_start = 8usize;
|
||||
let header_end = header_start + header_len;
|
||||
if header_end > bytes.len() {
|
||||
return Err(FrameError::Truncated);
|
||||
}
|
||||
|
||||
let payload_start = align_up(header_end, SECTION_ALIGN);
|
||||
let payload_end = payload_start + payload_len;
|
||||
if payload_end != bytes.len() {
|
||||
return Err(FrameError::Truncated);
|
||||
}
|
||||
|
||||
let header = deserialize_section::<ArchivedPacketHeader, PacketHeader>(
|
||||
bytes.get(header_start..header_end).ok_or(FrameError::Truncated)?,
|
||||
FrameError::InvalidHeader,
|
||||
)?;
|
||||
|
||||
Ok(ParsedFrame {
|
||||
header,
|
||||
payload_bytes: bytes
|
||||
.get(payload_start..payload_end)
|
||||
.ok_or(FrameError::Truncated)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Deserializes a standalone archived byte section.
|
||||
/// Deserializes one archived byte section.
|
||||
pub fn deserialize_archived_bytes<A, T>(bytes: &[u8]) -> Result<T, FrameError>
|
||||
where
|
||||
A: rkyv::Portable
|
||||
@@ -225,16 +151,53 @@ where
|
||||
T: rkyv::Archive,
|
||||
A: rkyv::Deserialize<T, rkyv::api::high::HighDeserializer<Error>>,
|
||||
{
|
||||
let aligned = align_section(bytes);
|
||||
let archived = access::<A, Error>(&aligned).map_err(FrameError::InvalidPayload)?;
|
||||
deserialize::<T, Error>(archived).map_err(FrameError::InvalidPayload)
|
||||
deserialize_section::<A, T>(bytes, FrameError::InvalidPayload)
|
||||
}
|
||||
|
||||
fn align_section(bytes: &[u8]) -> AlignedVec {
|
||||
// The framed wire format prefixes each archived section with a 4-byte length,
|
||||
// so callers cannot rely on the borrowed slice meeting rkyv's alignment.
|
||||
// Copying into `AlignedVec` keeps the alignment fix local and predictable.
|
||||
let mut aligned = AlignedVec::with_capacity(bytes.len());
|
||||
aligned.extend_from_slice(bytes);
|
||||
aligned
|
||||
fn read_u32(bytes: &[u8], start: usize) -> Result<u32, FrameError> {
|
||||
let end = start + 4;
|
||||
Ok(u32::from_be_bytes(
|
||||
bytes
|
||||
.get(start..end)
|
||||
.ok_or(FrameError::Truncated)?
|
||||
.try_into()
|
||||
.expect("slice width checked"),
|
||||
))
|
||||
}
|
||||
|
||||
fn append_padding(frame: &mut AlignedVec, padding: usize) {
|
||||
if padding > 0 {
|
||||
frame.resize(frame.len() + padding, 0);
|
||||
}
|
||||
}
|
||||
|
||||
fn align_up(offset: usize, alignment: usize) -> usize {
|
||||
let mask = alignment - 1;
|
||||
(offset + mask) & !mask
|
||||
}
|
||||
|
||||
fn deserialize_section<A, T>(
|
||||
bytes: &[u8],
|
||||
invalid: fn(Error) -> FrameError,
|
||||
) -> Result<T, FrameError>
|
||||
where
|
||||
A: rkyv::Portable
|
||||
+ for<'b> rkyv::bytecheck::CheckBytes<rkyv::api::high::HighValidator<'b, Error>>,
|
||||
T: rkyv::Archive,
|
||||
A: rkyv::Deserialize<T, rkyv::api::high::HighDeserializer<Error>>,
|
||||
{
|
||||
if is_aligned_for::<A>(bytes) {
|
||||
let archived = access::<A, Error>(bytes).map_err(invalid)?;
|
||||
return deserialize::<T, Error>(archived).map_err(invalid);
|
||||
}
|
||||
|
||||
let mut aligned: FrameBytes = FrameBytes::with_capacity(bytes.len());
|
||||
aligned.extend_from_slice(bytes);
|
||||
let archived = access::<A, Error>(&aligned).map_err(invalid)?;
|
||||
deserialize::<T, Error>(archived).map_err(invalid)
|
||||
}
|
||||
|
||||
fn is_aligned_for<A>(bytes: &[u8]) -> bool {
|
||||
let alignment = mem::align_of::<A>();
|
||||
alignment <= 1 || (bytes.as_ptr() as usize).is_multiple_of(alignment)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user