Skip to main content

po_wire/
frame_type.rs

1//! Frame type definitions for Protocol Orzatty.
2//!
3//! Each frame type uses 4 bits (lower nibble of byte 0), supporting up to 16 types.
4
5use core::fmt;
6use crate::error::WireError;
7
8/// The type of a PO frame, encoded in the lower 4 bits of the first header byte.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10#[repr(u8)]
11pub enum FrameType {
12    /// Application data (chat messages, RPC payloads, etc.)
13    Data = 0x00,
14
15    /// Initiate cryptographic handshake (sends Ed25519 pubkey + X25519 ephemeral).
16    HandshakeInit = 0x01,
17
18    /// Reply to handshake with responder's keys.
19    HandshakeReply = 0x02,
20
21    /// Confirm handshake — session key is now active.
22    HandshakeComplete = 0x03,
23
24    /// Keep-alive ping. Should be sent with CONTROL flag.
25    Ping = 0x04,
26
27    /// Ping response. Should be sent with CONTROL flag.
28    Pong = 0x05,
29
30    /// Graceful connection close. Should be sent with CONTROL flag.
31    Close = 0x06,
32
33    /// File transfer metadata (name, size, hash).
34    FileHeader = 0x07,
35
36    /// File data chunk (sequential binary payload).
37    FileChunk = 0x08,
38
39    /// Acknowledgement of a received frame or operation.
40    Ack = 0x09,
41}
42
43impl FrameType {
44    /// Try to convert a raw `u8` nibble (0x00–0x0F) into a `FrameType`.
45    #[inline]
46    pub const fn from_u8(value: u8) -> Result<Self, WireError> {
47        match value {
48            0x00 => Ok(Self::Data),
49            0x01 => Ok(Self::HandshakeInit),
50            0x02 => Ok(Self::HandshakeReply),
51            0x03 => Ok(Self::HandshakeComplete),
52            0x04 => Ok(Self::Ping),
53            0x05 => Ok(Self::Pong),
54            0x06 => Ok(Self::Close),
55            0x07 => Ok(Self::FileHeader),
56            0x08 => Ok(Self::FileChunk),
57            0x09 => Ok(Self::Ack),
58            other => Err(WireError::UnknownFrameType(other)),
59        }
60    }
61
62    /// Returns `true` if this is a handshake-related frame.
63    #[inline]
64    pub const fn is_handshake(&self) -> bool {
65        matches!(self, Self::HandshakeInit | Self::HandshakeReply | Self::HandshakeComplete)
66    }
67
68    /// Returns `true` if this is a control frame (non-application data).
69    #[inline]
70    pub const fn is_control(&self) -> bool {
71        matches!(self, Self::Ping | Self::Pong | Self::Close)
72    }
73
74    /// Returns `true` if this carries file transfer data.
75    #[inline]
76    pub const fn is_file_transfer(&self) -> bool {
77        matches!(self, Self::FileHeader | Self::FileChunk)
78    }
79}
80
81impl fmt::Display for FrameType {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        match self {
84            Self::Data => write!(f, "DATA"),
85            Self::HandshakeInit => write!(f, "HS_INIT"),
86            Self::HandshakeReply => write!(f, "HS_REPLY"),
87            Self::HandshakeComplete => write!(f, "HS_COMPLETE"),
88            Self::Ping => write!(f, "PING"),
89            Self::Pong => write!(f, "PONG"),
90            Self::Close => write!(f, "CLOSE"),
91            Self::FileHeader => write!(f, "FILE_HDR"),
92            Self::FileChunk => write!(f, "FILE_CHUNK"),
93            Self::Ack => write!(f, "ACK"),
94        }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn all_types_roundtrip() {
104        let types = [
105            (0x00, FrameType::Data),
106            (0x01, FrameType::HandshakeInit),
107            (0x02, FrameType::HandshakeReply),
108            (0x03, FrameType::HandshakeComplete),
109            (0x04, FrameType::Ping),
110            (0x05, FrameType::Pong),
111            (0x06, FrameType::Close),
112            (0x07, FrameType::FileHeader),
113            (0x08, FrameType::FileChunk),
114            (0x09, FrameType::Ack),
115        ];
116        for (byte, expected) in types {
117            assert_eq!(FrameType::from_u8(byte), Ok(expected));
118            assert_eq!(expected as u8, byte);
119        }
120    }
121
122    #[test]
123    fn unknown_type_rejected() {
124        for byte in 0x0A..=0x0F {
125            assert!(matches!(
126                FrameType::from_u8(byte),
127                Err(WireError::UnknownFrameType(_))
128            ));
129        }
130    }
131
132    #[test]
133    fn classification() {
134        assert!(FrameType::HandshakeInit.is_handshake());
135        assert!(FrameType::HandshakeReply.is_handshake());
136        assert!(FrameType::HandshakeComplete.is_handshake());
137        assert!(!FrameType::Data.is_handshake());
138
139        assert!(FrameType::Ping.is_control());
140        assert!(FrameType::Pong.is_control());
141        assert!(FrameType::Close.is_control());
142        assert!(!FrameType::Data.is_control());
143
144        assert!(FrameType::FileHeader.is_file_transfer());
145        assert!(FrameType::FileChunk.is_file_transfer());
146        assert!(!FrameType::Data.is_file_transfer());
147    }
148}