Replace procedure name string in packet with u32, remove HeaderRef.

This commit is contained in:
Michael Mikovsky
2026-05-28 12:56:59 -06:00
parent 99579495a5
commit 84ac117ee0
5 changed files with 127 additions and 177 deletions
+50 -63
View File
@@ -1,47 +1,54 @@
extern crate alloc;
use alloc::string::String;
use alloc::vec::Vec;
use crate::{DeserializeError, SerializeError};
/// Fully decoded UnShell test packet.
///
/// The current protocol tests route only on hook id, hook end state, and absolute
/// path. `procedure_id` is therefore a compact numeric contract id instead of a
/// string label; application code can maintain its own id-to-name table outside the
/// hot packet path if it needs human-readable names.
#[derive(Debug)]
pub struct Packet {
pub hook_id: u16,
pub end_hook: bool,
pub path: Vec<u32>,
// ── body (routers never read below this line) ──
pub procedure_id: String,
pub procedure_id: u32,
pub data: Vec<u8>,
}
/// Returned by `deserialize_header` — only what a router needs.
/// `body_remainder` is a raw slice into the original buffer so the
/// entire body can be forwarded without touching it.
#[derive(Debug)]
pub struct HeaderRef<'buf> {
pub hook_id: u16,
pub end_hook: bool,
pub path: &'buf [u32],
pub body_remainder: &'buf [u8],
}
impl Packet {
/// Serializes the packet into the crate's current little-endian frame format.
///
/// Layout:
/// - fixed header: `hook_id: u16`, `flags: u8`, padding, `path_len: u32`
/// - path: `path_len` little-endian `u32` segments
/// - body: `body_len: u32`, `procedure_id: u32`, raw `data`
///
/// Keeping `procedure_id` fixed-width removes the old string length and UTF-8
/// validation path. That makes deserialization a single full-packet parse,
/// which matches how the endpoint mock transports actually consume packets.
pub fn serialize(&self) -> Result<Vec<u8>, SerializeError> {
let proc_id_bytes = self.procedure_id.as_bytes();
let path_len = u32::try_from(self.path.len()).map_err(|_| SerializeError::PathTooLarge)?;
let path_len = self.path.len() as u32;
let proc_id_len =
u32::try_from(proc_id_bytes.len()).map_err(|_| SerializeError::ProcIdTooLarge)?;
// body = proc_id_len field + proc_id bytes + data bytes
// body = fixed procedure_id field + data bytes
let body_payload_len = 4usize
.checked_add(proc_id_bytes.len())
.and_then(|n| n.checked_add(self.data.len()))
.checked_add(self.data.len())
.ok_or(SerializeError::BodyTooLarge)?;
let body_len = u32::try_from(body_payload_len).map_err(|_| SerializeError::BodyTooLarge)?;
let total = 8 + (self.path.len() * 4) + 4 + body_payload_len;
let path_bytes = self
.path
.len()
.checked_mul(4)
.ok_or(SerializeError::PathTooLarge)?;
let total = 8usize
.checked_add(path_bytes)
.and_then(|n| n.checked_add(4))
.and_then(|n| n.checked_add(body_payload_len))
.ok_or(SerializeError::BodyTooLarge)?;
let mut buf = Vec::with_capacity(total);
// ── header ────────────────────────────────────────────────────────────
@@ -56,16 +63,19 @@ impl Packet {
// ── body ──────────────────────────────────────────────────────────────
buf.extend_from_slice(&body_len.to_le_bytes());
buf.extend_from_slice(&proc_id_len.to_le_bytes());
buf.extend_from_slice(proc_id_bytes);
buf.extend_from_slice(&self.procedure_id.to_le_bytes());
buf.extend_from_slice(&self.data);
Ok(buf)
}
/// Deserialize only the header — O(path_len), body bytes are never read.
/// A router can inspect `HeaderRef` then forward the original buffer as-is.
pub fn deserialize_header(buf: &[u8]) -> Result<HeaderRef<'_>, DeserializeError> {
/// Deserializes a full packet from untrusted transport bytes.
///
/// This parser intentionally consumes the complete packet shape. The old
/// partial parse path was removed because current routing tests and mock
/// transports always deserialize before calling endpoint routing, so keeping a
/// borrowed header API only preserved unused unsafe casting complexity.
pub fn deserialize(buf: &[u8]) -> Result<Self, DeserializeError> {
// fixed prefix: hook_id (2) + flags (1) + padding (1) + path_len (4)
if buf.len() < 8 {
return Err(DeserializeError::BufferTooShort);
@@ -85,25 +95,13 @@ impl Packet {
return Err(DeserializeError::BufferTooShort);
}
// Cast the buffer slice to a u32 slice.
// This requires alignment. rkyv handles this, but for a manual cast:
let path_ptr = buf[path_start..path_end].as_ptr() as *const u32;
let path = unsafe { core::slice::from_raw_parts(path_ptr, path_len) };
Ok(HeaderRef {
hook_id,
end_hook,
path,
body_remainder: &buf[path_end..],
})
}
/// Full deserialization. Parses the header then the body.
pub fn deserialize(buf: &[u8]) -> Result<Self, DeserializeError> {
let header = Self::deserialize_header(buf)?;
let body_buf = header.body_remainder;
let mut path = Vec::with_capacity(path_len);
for chunk in buf[path_start..path_end].chunks_exact(4) {
path.push(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
}
// body_len prefix
let body_buf = &buf[path_end..];
if body_buf.len() < 4 {
return Err(DeserializeError::BufferTooShort);
}
@@ -117,31 +115,20 @@ impl Packet {
return Err(DeserializeError::BodyLengthMismatch);
}
// proc_id_len + proc_id
// procedure_id + data
let inner = &body_buf[4..body_end];
if inner.len() < 4 {
return Err(DeserializeError::BufferTooShort);
}
let proc_id_len = u32::from_le_bytes([inner[0], inner[1], inner[2], inner[3]]) as usize;
let procedure_id = u32::from_le_bytes([inner[0], inner[1], inner[2], inner[3]]);
let proc_id_start = 4usize;
let proc_id_end = proc_id_start
.checked_add(proc_id_len)
.ok_or(DeserializeError::ProcIdTooLong)?;
if inner.len() < proc_id_end {
return Err(DeserializeError::BufferTooShort);
}
let procedure_id = core::str::from_utf8(&inner[proc_id_start..proc_id_end])
.map_err(|_| DeserializeError::InvalidUtf8)?;
let data = inner[proc_id_end..].to_vec();
let data = inner[4..].to_vec();
Ok(Self {
hook_id: header.hook_id,
end_hook: header.end_hook,
path: header.path.to_vec(),
procedure_id: procedure_id.into(),
hook_id,
end_hook,
path,
procedure_id,
data,
})
}