Skip to main content

soe_protocol/
protocol.rs

1//! Core protocol enumerations: OP codes and disconnect reasons.
2
3/// The packet OP codes used in the SOE protocol. All packets are prefixed with a
4/// big-endian `u16` OP code.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6#[repr(u16)]
7pub enum OpCode {
8    /// Used to request the start of a session.
9    SessionRequest = 0x01,
10    /// Used to confirm the start of a session, and set connection details.
11    SessionResponse = 0x02,
12    /// Used to encapsulate two or more SOE protocol packets.
13    MultiPacket = 0x03,
14    /// Used to indicate that a party is closing the session.
15    Disconnect = 0x05,
16    /// Used to keep a session alive when no data has been received for some time.
17    Heartbeat = 0x06,
18    /// Network status request. Exact usage is not fully understood.
19    NetStatusRequest = 0x07,
20    /// Network status response. Exact usage is not fully understood.
21    NetStatusResponse = 0x08,
22    /// Used to transfer small buffers of application data.
23    ReliableData = 0x09,
24    /// Used to transfer large buffers of application data in multiple fragments.
25    ReliableDataFragment = 0x0D,
26    /// Used to acknowledge a single reliable data packet.
27    Acknowledge = 0x11,
28    /// Used to acknowledge all reliable data packets up to a particular sequence.
29    AcknowledgeAll = 0x15,
30    /// Indicates the receiver has no session associated with the sender's address.
31    UnknownSender = 0x1D,
32    /// Used to request that a session be remapped to another port.
33    RemapConnection = 0x1E,
34}
35
36impl OpCode {
37    /// Attempts to convert a raw `u16` into an [`OpCode`].
38    pub fn from_u16(value: u16) -> Option<Self> {
39        Some(match value {
40            0x01 => Self::SessionRequest,
41            0x02 => Self::SessionResponse,
42            0x03 => Self::MultiPacket,
43            0x05 => Self::Disconnect,
44            0x06 => Self::Heartbeat,
45            0x07 => Self::NetStatusRequest,
46            0x08 => Self::NetStatusResponse,
47            0x09 => Self::ReliableData,
48            0x0D => Self::ReliableDataFragment,
49            0x11 => Self::Acknowledge,
50            0x15 => Self::AcknowledgeAll,
51            0x1D => Self::UnknownSender,
52            0x1E => Self::RemapConnection,
53            _ => return None,
54        })
55    }
56
57    /// Returns the raw `u16` value of this OP code.
58    pub fn as_u16(self) -> u16 {
59        self as u16
60    }
61
62    /// Returns `true` if this packet type is used outside of an established
63    /// session context (and hence is not CRC-checked or compressed).
64    pub fn is_contextless(self) -> bool {
65        matches!(
66            self,
67            Self::SessionRequest
68                | Self::SessionResponse
69                | Self::NetStatusRequest
70                | Self::NetStatusResponse
71                | Self::UnknownSender
72                | Self::RemapConnection
73        )
74    }
75}
76
77/// The possible session termination reasons.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
79#[repr(u16)]
80pub enum DisconnectReason {
81    /// No reason can be given for the disconnect.
82    None = 0,
83    /// An ICMP error occurred, forcing the disconnect.
84    IcmpError = 1,
85    /// The other party has let the session become inactive.
86    Timeout = 2,
87    /// Internal: the other party has sent a disconnect.
88    OtherSideTerminated = 3,
89    /// The session manager has been disposed of (e.g. shutting down).
90    ManagerDeleted = 4,
91    /// Internal: a session request attempt has failed.
92    ConnectFail = 5,
93    /// The application is terminating the session.
94    Application = 6,
95    /// Internal: the other party is unreachable.
96    UnreachableConnection = 7,
97    /// A data sequence was not acknowledged quickly enough.
98    UnacknowledgedTimeout = 8,
99    /// A session request failed; a new attempt should be made after a short delay.
100    NewConnectionAttempt = 9,
101    /// The application did not accept a session request.
102    ConnectionRefused = 10,
103    /// The proper session negotiation flow has not been observed.
104    ConnectError = 11,
105    /// A session request was probably looped back to the sender.
106    ConnectingToSelf = 12,
107    /// Reliable data is being sent too fast to be processed.
108    ReliableOverflow = 13,
109    /// The session manager has been orphaned by the application.
110    ApplicationReleased = 14,
111    /// A corrupt packet was received.
112    CorruptPacket = 15,
113    /// The requested SOE protocol version or application protocol is invalid.
114    ProtocolMismatch = 16,
115}
116
117impl DisconnectReason {
118    /// Converts a raw `u16` into a [`DisconnectReason`], mapping unrecognized
119    /// values to [`DisconnectReason::None`].
120    pub fn from_u16(value: u16) -> Self {
121        match value {
122            1 => Self::IcmpError,
123            2 => Self::Timeout,
124            3 => Self::OtherSideTerminated,
125            4 => Self::ManagerDeleted,
126            5 => Self::ConnectFail,
127            6 => Self::Application,
128            7 => Self::UnreachableConnection,
129            8 => Self::UnacknowledgedTimeout,
130            9 => Self::NewConnectionAttempt,
131            10 => Self::ConnectionRefused,
132            11 => Self::ConnectError,
133            12 => Self::ConnectingToSelf,
134            13 => Self::ReliableOverflow,
135            14 => Self::ApplicationReleased,
136            15 => Self::CorruptPacket,
137            16 => Self::ProtocolMismatch,
138            _ => Self::None,
139        }
140    }
141
142    /// Returns the raw `u16` value of this reason.
143    pub fn as_u16(self) -> u16 {
144        self as u16
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn opcode_round_trip() {
154        for raw in [
155            0x01u16, 0x02, 0x03, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0D, 0x11, 0x15, 0x1D, 0x1E,
156        ] {
157            let op = OpCode::from_u16(raw).unwrap();
158            assert_eq!(op.as_u16(), raw);
159        }
160        assert!(OpCode::from_u16(0x00).is_none());
161        assert!(OpCode::from_u16(0xFFFF).is_none());
162    }
163
164    #[test]
165    fn contextless_classification() {
166        assert!(OpCode::SessionRequest.is_contextless());
167        assert!(OpCode::RemapConnection.is_contextless());
168        assert!(!OpCode::ReliableData.is_contextless());
169        assert!(!OpCode::Disconnect.is_contextless());
170    }
171
172    #[test]
173    fn disconnect_reason_round_trip() {
174        for raw in 0u16..=16 {
175            assert_eq!(DisconnectReason::from_u16(raw).as_u16(), raw);
176        }
177        assert_eq!(DisconnectReason::from_u16(999), DisconnectReason::None);
178    }
179}