//! Framed packet encoding and decoding. use core::{fmt, mem}; use rkyv::{ Serialize, access, api::high::to_bytes_in, deserialize, rancor::Error, 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. /// /// The protocol aligns both archived sections so `rkyv` can usually validate and deserialize /// them without first copying into a temporary aligned buffer. /// /// # Example /// ```rust /// use unshell::protocol::SECTION_ALIGN; /// assert_eq!(SECTION_ALIGN, 16); /// ``` pub const SECTION_ALIGN: usize = 16; /// Owned framed packet bytes. /// /// This is the concrete buffer type returned by [`encode_packet`]. It keeps archived packet bytes /// aligned according to [`SECTION_ALIGN`] so decode can often stay zero-copy. /// /// # Example /// ```rust /// use unshell::protocol::{CallMessage, FrameBytes, PacketHeader, PacketType, encode_packet}; /// let header = PacketHeader { /// packet_type: PacketType::Call, /// src_path: vec!["root".into()], /// dst_path: vec!["root".into(), "worker".into()], /// dst_leaf: Some("service".into()), /// hook_id: None, /// }; /// let message = CallMessage { /// procedure_id: "example.service.v1.invoke".into(), /// data: vec![], /// response_hook: None, /// }; /// let frame: FrameBytes = encode_packet(&header, &message)?; /// assert!(!frame.is_empty()); /// # Ok::<(), unshell::protocol::FrameError>(()) /// ``` pub type FrameBytes = AlignedVec; /// Framing or archive failure. #[derive(Debug)] pub enum FrameError { /// The byte slice ended before a full frame could be decoded. Truncated, /// The archived header bytes failed validation or deserialization. InvalidHeader(Error), /// The archived payload bytes failed validation or deserialization. InvalidPayload(Error), /// Serializing one header or payload section failed. Serialize(Error), /// One archived section grew beyond the `u32` length prefix supported by the format. LengthOverflow, } impl fmt::Display for FrameError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Truncated => f.write_str("truncated frame"), Self::InvalidHeader(error) => write!(f, "invalid archived header: {error}"), Self::InvalidPayload(error) => write!(f, "invalid archived payload: {error}"), Self::Serialize(error) => write!(f, "serialization failed: {error}"), Self::LengthOverflow => f.write_str("framed section exceeds u32 length"), } } } impl core::error::Error for FrameError {} /// Parsed frame with one owned header and a borrowed payload section. /// /// The frame decoder eagerly materializes the routing header into owned Rust values, but keeps /// the payload section borrowed so callers can choose which concrete payload type to decode. /// /// # Example /// ```rust /// use unshell::protocol::{CallMessage, PacketHeader, PacketType, decode_frame, encode_packet}; /// let header = PacketHeader { /// packet_type: PacketType::Call, /// src_path: vec!["root".into()], /// dst_path: vec!["root".into(), "worker".into()], /// dst_leaf: Some("service".into()), /// hook_id: None, /// }; /// let message = CallMessage { /// procedure_id: "example.service.v1.invoke".into(), /// data: vec![7; 4], /// response_hook: None, /// }; /// let frame = encode_packet(&header, &message)?; /// let parsed = decode_frame(&frame)?; /// assert_eq!(parsed.packet_type(), PacketType::Call); /// let decoded = parsed.deserialize_call()?; /// assert_eq!(decoded.data.len(), 4); /// # Ok::<(), unshell::protocol::FrameError>(()) /// ``` pub struct ParsedFrame<'a> { header: PacketHeader, payload_bytes: &'a [u8], } impl<'a> ParsedFrame<'a> { #[must_use] /// Returns the decoded packet header. /// /// This exists so callers can inspect routing metadata before deciding which payload schema /// to decode. /// /// # Example /// ```rust /// use unshell::protocol::{CallMessage, PacketHeader, PacketType, decode_frame, encode_packet}; /// let header = PacketHeader { /// packet_type: PacketType::Call, /// src_path: vec!["root".into()], /// dst_path: vec!["worker".into()], /// dst_leaf: None, /// hook_id: None, /// }; /// let frame = encode_packet(&header, &CallMessage { /// procedure_id: "example.invoke".into(), /// data: vec![], /// response_hook: None, /// })?; /// let parsed = decode_frame(&frame)?; /// assert_eq!(parsed.header().packet_type, PacketType::Call); /// # Ok::<(), unshell::protocol::FrameError>(()) /// ``` pub fn header(&self) -> &PacketHeader { &self.header } #[must_use] /// Returns the packet class from the decoded header. /// /// This exists as a cheap dispatch helper so callers do not have to reach into the header /// struct directly when branching on payload type. /// /// # Example /// ```rust /// use unshell::protocol::{CallMessage, PacketHeader, PacketType, decode_frame, encode_packet}; /// let header = PacketHeader { /// packet_type: PacketType::Call, /// src_path: vec!["root".into()], /// dst_path: vec!["worker".into()], /// dst_leaf: None, /// hook_id: None, /// }; /// let frame = encode_packet(&header, &CallMessage { /// procedure_id: "example.invoke".into(), /// data: vec![], /// response_hook: None, /// })?; /// let parsed = decode_frame(&frame)?; /// assert!(matches!(parsed.packet_type(), PacketType::Call)); /// # Ok::<(), unshell::protocol::FrameError>(()) /// ``` pub fn packet_type(&self) -> PacketType { self.header.packet_type } #[must_use] /// Returns the borrowed payload section bytes. /// /// This exists for callers that embed their own archived application payloads inside protocol /// `data` fields and want to defer typed decoding. /// /// # Example /// ```rust /// use unshell::protocol::{CallMessage, PacketHeader, PacketType, decode_frame, encode_packet}; /// let header = PacketHeader { /// packet_type: PacketType::Call, /// src_path: vec!["root".into()], /// dst_path: vec!["worker".into()], /// dst_leaf: None, /// hook_id: None, /// }; /// let frame = encode_packet(&header, &CallMessage { /// procedure_id: "example.invoke".into(), /// data: vec![1, 2, 3], /// response_hook: None, /// })?; /// let parsed = decode_frame(&frame)?; /// assert!(!parsed.payload_bytes().is_empty()); /// # Ok::<(), unshell::protocol::FrameError>(()) /// ``` pub fn payload_bytes(&self) -> &'a [u8] { self.payload_bytes } #[must_use] /// Splits the parsed frame into its owned header and borrowed payload bytes. /// /// This exists when callers want to take ownership of the decoded header while still choosing /// how and when to interpret the payload bytes. /// /// # Example /// ```rust /// use unshell::protocol::{CallMessage, PacketHeader, PacketType, decode_frame, encode_packet}; /// let header = PacketHeader { /// packet_type: PacketType::Call, /// src_path: vec!["root".into()], /// dst_path: vec!["worker".into()], /// dst_leaf: None, /// hook_id: None, /// }; /// let frame = encode_packet(&header, &CallMessage { /// procedure_id: "example.invoke".into(), /// data: vec![], /// response_hook: None, /// })?; /// let parsed = decode_frame(&frame)?; /// let (owned_header, payload) = parsed.into_parts(); /// assert_eq!(owned_header.packet_type, PacketType::Call); /// assert!(!payload.is_empty()); /// # Ok::<(), unshell::protocol::FrameError>(()) /// ``` pub fn into_parts(self) -> (PacketHeader, &'a [u8]) { (self.header, self.payload_bytes) } /// Deserializes the payload section as a [`CallMessage`]. /// /// This exists so callers can decode a validated `Call` packet payload without spelling the /// archived-type details themselves. /// /// # Example /// ```rust /// use unshell::protocol::{CallMessage, PacketHeader, PacketType, decode_frame, encode_packet}; /// let message = CallMessage { /// procedure_id: "example.invoke".into(), /// data: vec![1], /// response_hook: None, /// }; /// let frame = encode_packet(&PacketHeader { /// packet_type: PacketType::Call, /// src_path: vec!["root".into()], /// dst_path: vec!["worker".into()], /// dst_leaf: None, /// hook_id: None, /// }, &message)?; /// let parsed = decode_frame(&frame)?; /// assert_eq!(parsed.deserialize_call()?.procedure_id, message.procedure_id); /// # Ok::<(), unshell::protocol::FrameError>(()) /// ``` pub fn deserialize_call(&self) -> Result { self.deserialize_payload::() } /// Deserializes the payload section as a [`DataMessage`]. /// /// This exists so callers can decode hook `Data` payloads without reaching for the generic /// archived helper directly. /// /// # Example /// ```rust /// use unshell::protocol::{DataMessage, PacketHeader, PacketType, decode_frame, encode_packet}; /// let message = DataMessage { /// procedure_id: "example.invoke".into(), /// data: vec![1], /// end_hook: false, /// }; /// let frame = encode_packet(&PacketHeader { /// packet_type: PacketType::Data, /// src_path: vec!["worker".into()], /// dst_path: vec!["root".into()], /// dst_leaf: None, /// hook_id: Some(7), /// }, &message)?; /// let parsed = decode_frame(&frame)?; /// assert!(!parsed.deserialize_data()?.end_hook); /// # Ok::<(), unshell::protocol::FrameError>(()) /// ``` pub fn deserialize_data(&self) -> Result { self.deserialize_payload::() } /// Deserializes the payload section as a [`FaultMessage`]. /// /// This exists so callers can decode protocol faults with the same selective API used for /// call and data packets. /// /// # Example /// ```rust /// use unshell::protocol::{FaultMessage, PacketHeader, PacketType, ProtocolFault, decode_frame, encode_packet}; /// let frame = encode_packet(&PacketHeader { /// packet_type: PacketType::Fault, /// src_path: vec!["worker".into()], /// dst_path: vec!["root".into()], /// dst_leaf: None, /// hook_id: Some(7), /// }, &FaultMessage { fault: ProtocolFault::INTERNAL_ERROR })?; /// let parsed = decode_frame(&frame)?; /// assert_eq!(parsed.deserialize_fault()?.fault, ProtocolFault::INTERNAL_ERROR); /// # Ok::<(), unshell::protocol::FrameError>(()) /// ``` pub fn deserialize_fault(&self) -> Result { self.deserialize_payload::() } fn deserialize_payload(&self) -> Result where A: rkyv::Portable + for<'b> rkyv::bytecheck::CheckBytes>, T: rkyv::Archive, A: rkyv::Deserialize>, { deserialize_archived_bytes::(self.payload_bytes) } } /// Encodes a packet header and payload using the aligned two-section frame format. /// /// The frame starts with two big-endian `u32` lengths, followed by an aligned archived header /// section and an aligned archived payload section. Both sections use [`SECTION_ALIGN`] so the /// archived bytes can usually be accessed without a fallback copy on decode. /// /// # Example /// ```rust /// use unshell::protocol::{CallMessage, PacketHeader, PacketType, encode_packet}; /// let frame = encode_packet( /// &PacketHeader { /// packet_type: PacketType::Call, /// src_path: vec!["root".into()], /// dst_path: vec!["worker".into()], /// dst_leaf: Some("service".into()), /// hook_id: None, /// }, /// &CallMessage { /// procedure_id: "example.invoke".into(), /// data: vec![1, 2, 3], /// response_hook: None, /// }, /// )?; /// assert!(frame.len() >= 8); /// # Ok::<(), unshell::protocol::FrameError>(()) /// ``` pub fn encode_packet

(header: &PacketHeader, payload: &P) -> Result where P: for<'a> Serialize< rkyv::api::high::HighSerializer, Error>, >, { let header_start = align_up(8usize, SECTION_ALIGN); // Reserve enough space for the framing prefix plus a typical header/payload pair so the // common encode path avoids early growth reallocations inside `to_bytes_in`. let mut frame = FrameBytes::with_capacity(header_start + 256); frame.resize(header_start, 0); frame = to_bytes_in::<_, Error>(header, frame).map_err(FrameError::Serialize)?; let header_len = u32::try_from(frame.len() - header_start).map_err(|_| FrameError::LengthOverflow)?; let payload_start = align_up(frame.len(), SECTION_ALIGN); frame.resize(payload_start, 0); frame = to_bytes_in::<_, Error>(payload, frame).map_err(FrameError::Serialize)?; let payload_len = u32::try_from(frame.len() - payload_start).map_err(|_| FrameError::LengthOverflow)?; frame[0..4].copy_from_slice(&header_len.to_be_bytes()); frame[4..8].copy_from_slice(&payload_len.to_be_bytes()); Ok(frame) } /// Decodes one aligned two-section frame. /// /// This rejects trailing bytes instead of silently ignoring them, so callers can treat one byte /// slice as exactly one protocol frame. /// /// # Example /// ```rust /// use unshell::protocol::{CallMessage, PacketHeader, PacketType, decode_frame, encode_packet}; /// let frame = encode_packet( /// &PacketHeader { /// packet_type: PacketType::Call, /// src_path: vec!["root".into()], /// dst_path: vec!["worker".into()], /// dst_leaf: Some("service".into()), /// hook_id: None, /// }, /// &CallMessage { /// procedure_id: "example.invoke".into(), /// data: vec![1, 2, 3], /// response_hook: None, /// }, /// )?; /// let parsed = decode_frame(&frame)?; /// assert_eq!(parsed.packet_type(), PacketType::Call); /// # Ok::<(), unshell::protocol::FrameError>(()) /// ``` pub fn decode_frame(bytes: &[u8]) -> Result, FrameError> { let (header_bytes, payload_bytes) = split_frame_sections(bytes)?; let header = deserialize_section::( header_bytes, FrameError::InvalidHeader, )?; Ok(ParsedFrame { header, payload_bytes, }) } /// Deserializes one archived byte section. /// /// Payload bytes normally come from [`decode_frame`] or one of [`ParsedFrame`]`'s` /// `deserialize_*` helpers. This function remains public for callers that archive nested /// application payloads inside protocol `data` fields. /// /// # Example /// ```rust /// use rkyv::{Archive, Deserialize, Serialize}; /// use unshell::protocol::deserialize_archived_bytes; /// /// #[derive(Archive, Serialize, Deserialize, Debug, PartialEq)] /// struct Example { /// value: u32, /// } /// /// let bytes = rkyv::to_bytes::(&Example { value: 7 }).unwrap(); /// let decoded = deserialize_archived_bytes::<::Archived, Example>(&bytes)?; /// assert_eq!(decoded, Example { value: 7 }); /// # Ok::<(), unshell::protocol::FrameError>(()) /// ``` pub fn deserialize_archived_bytes(bytes: &[u8]) -> Result where A: rkyv::Portable + for<'b> rkyv::bytecheck::CheckBytes>, T: rkyv::Archive, A: rkyv::Deserialize>, { deserialize_section::(bytes, FrameError::InvalidPayload) } fn read_u32(bytes: &[u8], start: usize) -> Result { let end = start + 4; Ok(u32::from_be_bytes( bytes .get(start..end) .ok_or(FrameError::Truncated)? .try_into() .expect("slice width checked"), )) } fn split_frame_sections(bytes: &[u8]) -> Result<(&[u8], &[u8]), FrameError> { 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 = align_up(8usize, SECTION_ALIGN); 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() { // Framed packets do not permit trailing bytes. Treating the slice as exactly one frame // keeps stream framing bugs visible instead of silently accepting concatenated payloads. return Err(FrameError::Truncated); } Ok(( bytes .get(header_start..header_end) .ok_or(FrameError::Truncated)?, bytes .get(payload_start..payload_end) .ok_or(FrameError::Truncated)?, )) } fn align_up(offset: usize, alignment: usize) -> usize { let mask = alignment - 1; (offset + mask) & !mask } fn deserialize_section( bytes: &[u8], invalid: fn(Error) -> FrameError, ) -> Result where A: rkyv::Portable + for<'b> rkyv::bytecheck::CheckBytes>, T: rkyv::Archive, A: rkyv::Deserialize>, { if is_aligned_for::(bytes) { let archived = access::(bytes).map_err(invalid)?; return deserialize::(archived).map_err(invalid); } // Archived types may require stronger alignment than a borrowed byte slice can guarantee. // Copy into an aligned buffer so callers can still decode valid frames from arbitrary input // sources instead of rejecting them purely for allocation layout reasons. let mut aligned: FrameBytes = FrameBytes::with_capacity(bytes.len()); aligned.extend_from_slice(bytes); let archived = access::(&aligned).map_err(invalid)?; deserialize::(archived).map_err(invalid) } fn is_aligned_for(bytes: &[u8]) -> bool { let alignment = mem::align_of::(); alignment <= 1 || (bytes.as_ptr() as usize).is_multiple_of(alignment) }