Skip to main content

uart_fpl/
handshake.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Channel 0 dynamic protocol negotiation state machines and binary serialization.
6
7use crate::error::HandshakeError;
8use crate::frame::{FrameType, ProtocolId};
9
10const STATUS_SUCCESS: u8 = 0;
11const STATUS_NO_COMMON_PROTOCOL: u8 = 1;
12const STATUS_MALFORMED_REQUEST: u8 = 2;
13
14const PROTOCOL_COUNT_LEN: usize = 1;
15const PROTOCOL_ID_LEN: usize = std::mem::size_of::<u32>();
16const STATUS_LEN: usize = 1;
17const RESPONSE_LEN: usize = STATUS_LEN + PROTOCOL_ID_LEN;
18
19/// Negotiation status reported by the target in a handshake response.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum HandshakeStatus {
22    /// Negotiation succeeded; a mutually supported protocol was selected.
23    Success,
24    /// Negotiation failed; no common protocol exists between host and target.
25    NoCommonProtocol,
26    /// Request payload was malformed or could not be parsed.
27    MalformedRequest,
28    /// Unrecognized handshake status reported by peer.
29    Unknown(u8),
30}
31
32impl From<u8> for HandshakeStatus {
33    fn from(val: u8) -> Self {
34        match val {
35            STATUS_SUCCESS => HandshakeStatus::Success,
36            STATUS_NO_COMMON_PROTOCOL => HandshakeStatus::NoCommonProtocol,
37            STATUS_MALFORMED_REQUEST => HandshakeStatus::MalformedRequest,
38            other => HandshakeStatus::Unknown(other),
39        }
40    }
41}
42
43impl From<HandshakeStatus> for u8 {
44    fn from(status: HandshakeStatus) -> Self {
45        match status {
46            HandshakeStatus::Success => STATUS_SUCCESS,
47            HandshakeStatus::NoCommonProtocol => STATUS_NO_COMMON_PROTOCOL,
48            HandshakeStatus::MalformedRequest => STATUS_MALFORMED_REQUEST,
49            HandshakeStatus::Unknown(val) => val,
50        }
51    }
52}
53
54/// Structured handshake request containing a prioritized list of proposed protocols.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub(crate) struct HandshakeRequest {
57    /// Proposed protocols in host preference order.
58    pub protocols: Vec<ProtocolId>,
59}
60
61impl HandshakeRequest {
62    /// Serializes the request into wire bytes: `[count (1B), proto0 (4B BE), ...]`.
63    pub fn serialize(&self) -> Result<Vec<u8>, HandshakeError> {
64        if self.protocols.len() > u8::MAX as usize {
65            return Err(HandshakeError::MalformedPayload);
66        }
67        let mut data =
68            Vec::with_capacity(PROTOCOL_COUNT_LEN + PROTOCOL_ID_LEN * self.protocols.len());
69        data.push(self.protocols.len() as u8);
70        for &proto in &self.protocols {
71            data.extend_from_slice(&proto.wire_id().to_be_bytes());
72        }
73        Ok(data)
74    }
75}
76
77impl TryFrom<&[u8]> for HandshakeRequest {
78    type Error = HandshakeError;
79
80    /// Deserializes a request from wire bytes.
81    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
82        if bytes.len() < PROTOCOL_COUNT_LEN {
83            return Err(HandshakeError::MalformedPayload);
84        }
85        let count = bytes[0] as usize;
86        let expected_len = PROTOCOL_COUNT_LEN + PROTOCOL_ID_LEN * count;
87        if bytes.len() < expected_len {
88            return Err(HandshakeError::MalformedPayload);
89        }
90        let protocols = bytes[PROTOCOL_COUNT_LEN..expected_len]
91            .chunks_exact(PROTOCOL_ID_LEN)
92            .map(|chunk| ProtocolId::from(u32::from_be_bytes(chunk.try_into().unwrap())))
93            .collect();
94        Ok(Self { protocols })
95    }
96}
97
98/// Structured handshake response containing negotiation status and selected protocol.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct HandshakeResponse {
101    /// Result status of negotiation.
102    pub status: HandshakeStatus,
103    /// Mutually selected protocol, if successful.
104    pub selected: Option<ProtocolId>,
105}
106
107impl HandshakeResponse {
108    /// Serializes the response into wire bytes: `[status (1B), selected_proto (4B BE)]`.
109    ///
110    /// # Errors
111    /// Currently infallible for valid status values, but returns [`HandshakeError`]
112    /// if binary serialization invariants are violated.
113    pub fn serialize(&self) -> Result<Vec<u8>, HandshakeError> {
114        let mut data = Vec::with_capacity(RESPONSE_LEN);
115        data.push(u8::from(self.status));
116        let wire_id = self.selected.map(|p| p.wire_id()).unwrap_or(0);
117        data.extend_from_slice(&wire_id.to_be_bytes());
118        Ok(data)
119    }
120}
121
122impl TryFrom<&[u8]> for HandshakeResponse {
123    type Error = HandshakeError;
124
125    /// Deserializes a response from wire bytes.
126    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
127        if bytes.len() < RESPONSE_LEN {
128            return Err(HandshakeError::MalformedPayload);
129        }
130        let status = HandshakeStatus::from(bytes[0]);
131        let val = u32::from_be_bytes(bytes[STATUS_LEN..RESPONSE_LEN].try_into().unwrap());
132        let selected =
133            if status == HandshakeStatus::Success { Some(ProtocolId::from(val)) } else { None };
134        Ok(Self { status, selected })
135    }
136}
137
138/// Host-side handshake coordinator.
139#[derive(Debug, Clone)]
140pub struct HostHandshake {
141    proposed: Vec<ProtocolId>,
142}
143
144impl HostHandshake {
145    /// Creates a new host handshake coordinator with the list of proposed protocols in priority order.
146    pub fn new(proposed: Vec<ProtocolId>) -> Self {
147        Self { proposed }
148    }
149
150    /// Returns the list of proposed protocols.
151    pub fn proposed(&self) -> &[ProtocolId] {
152        &self.proposed
153    }
154
155    /// Generates the initial negotiation request frame type and serialized payload.
156    ///
157    /// # Errors
158    /// Returns [`HandshakeError::MalformedPayload`] if the number of proposed protocols
159    /// exceeds `u8::MAX`.
160    pub fn start(&self) -> Result<(FrameType, Vec<u8>), HandshakeError> {
161        let req = HandshakeRequest { protocols: self.proposed.clone() };
162        Ok((FrameType::NegotiateReq, req.serialize()?))
163    }
164
165    /// Evaluates a target response payload and returns the mutually agreed protocol.
166    ///
167    /// # Errors
168    /// - [`HandshakeError::MalformedPayload`]: If `payload` is shorter than 5 bytes or cannot be parsed.
169    /// - [`HandshakeError::NoCommonProtocol`]: If target reported no mutually supported protocol.
170    /// - [`HandshakeError::ProtocolNotProposed`]: If target selected a protocol not proposed by this host.
171    /// - [`HandshakeError::UnsupportedStatus`]: If target returned an unrecognized status code.
172    pub fn handle_response(&self, payload: &[u8]) -> Result<ProtocolId, HandshakeError> {
173        let resp = HandshakeResponse::try_from(payload)?;
174        match resp.status {
175            HandshakeStatus::Success => {
176                let selected = resp.selected.ok_or(HandshakeError::NoCommonProtocol)?;
177                if self.proposed.contains(&selected) {
178                    Ok(selected)
179                } else {
180                    Err(HandshakeError::ProtocolNotProposed)
181                }
182            }
183            HandshakeStatus::NoCommonProtocol => Err(HandshakeError::NoCommonProtocol),
184            HandshakeStatus::MalformedRequest => Err(HandshakeError::MalformedPayload),
185            HandshakeStatus::Unknown(code) => Err(HandshakeError::UnsupportedStatus(code)),
186        }
187    }
188}
189
190/// Target-side handshake coordinator.
191///
192/// # Idempotent Handshake Retransmission Contract
193/// In lossy UART links, the target's initial [`HandshakeResponse`] may be dropped or
194/// corrupted in flight. When this occurs, the host's timeout will cause it to retransmit
195/// the initial `NegotiateReq` with the identical `session_id`.
196///
197/// Target implementations (e.g. `receiver_task`) MUST treat repeated `NegotiateReq`
198/// frames with the active `session_id` as idempotent retransmissions: the target must
199/// re-emit the cached `NegotiateResp` frame rather than rejecting the frame as unexpected
200/// or resetting the transport state. If a `NegotiateReq` arrives with a *new* session ID,
201/// the target must reset and negotiate the new session.
202#[derive(Debug, Clone)]
203pub struct TargetHandshake {
204    supported: Vec<ProtocolId>,
205}
206
207impl TargetHandshake {
208    /// Creates a new target handshake handler with the list of supported protocols.
209    pub fn new(supported: Vec<ProtocolId>) -> Self {
210        Self { supported }
211    }
212
213    /// Returns the list of supported protocols.
214    pub fn supported(&self) -> &[ProtocolId] {
215        &self.supported
216    }
217
218    /// Evaluates an incoming host request payload and generates the serialized response.
219    /// Returns `(selected_protocol, response_payload)`.
220    ///
221    /// # Errors
222    /// Returns [`HandshakeError::MalformedPayload`] if `payload` cannot be decoded into
223    /// a valid [`HandshakeRequest`].
224    pub fn handle_request(
225        &self,
226        payload: &[u8],
227    ) -> Result<(Option<ProtocolId>, Vec<u8>), HandshakeError> {
228        let req = HandshakeRequest::try_from(payload)?;
229        let selected = req.protocols.into_iter().find(|p| self.supported.contains(p));
230        let status = if selected.is_some() {
231            HandshakeStatus::Success
232        } else {
233            HandshakeStatus::NoCommonProtocol
234        };
235        let resp = HandshakeResponse { status, selected };
236        Ok((selected, resp.serialize()?))
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    fn run_handshake(
245        host_proposed: Vec<ProtocolId>,
246        target_supported: Vec<ProtocolId>,
247    ) -> Result<ProtocolId, HandshakeError> {
248        let host = HostHandshake::new(host_proposed);
249        let target = TargetHandshake::new(target_supported);
250
251        let (_, req_payload) = host.start()?;
252        let (selected, resp_payload) = target.handle_request(&req_payload)?;
253        if selected.is_some() {
254            assert_eq!(resp_payload[0], u8::from(HandshakeStatus::Success));
255        }
256
257        host.handle_response(&resp_payload)
258    }
259
260    #[fuchsia::test]
261    fn test_handshake_matrix() {
262        struct TestCase {
263            host: Vec<ProtocolId>,
264            target: Vec<ProtocolId>,
265            expected: Result<ProtocolId, HandshakeError>,
266        }
267
268        let cases = [
269            TestCase {
270                host: vec![ProtocolId::ResendSP],
271                target: vec![ProtocolId::ResendSP],
272                expected: Ok(ProtocolId::ResendSP),
273            },
274            TestCase {
275                host: vec![ProtocolId::Unknown(99)],
276                target: vec![ProtocolId::Unknown(99)],
277                expected: Ok(ProtocolId::Unknown(99)),
278            },
279            // Host priority ordering: host prefers Unknown(99) over ResendSP
280            TestCase {
281                host: vec![ProtocolId::Unknown(99), ProtocolId::ResendSP],
282                target: vec![ProtocolId::ResendSP, ProtocolId::Unknown(99)],
283                expected: Ok(ProtocolId::Unknown(99)),
284            },
285            // Forward compatibility: host proposes unknown protocol first, target selects known fallback
286            TestCase {
287                host: vec![ProtocolId::Unknown(999), ProtocolId::ResendSP],
288                target: vec![ProtocolId::ResendSP],
289                expected: Ok(ProtocolId::ResendSP),
290            },
291            // Forward compatibility: both support custom/unknown protocol
292            TestCase {
293                host: vec![ProtocolId::Unknown(42)],
294                target: vec![ProtocolId::Unknown(42)],
295                expected: Ok(ProtocolId::Unknown(42)),
296            },
297            // Incompatible protocols
298            TestCase {
299                host: vec![ProtocolId::ResendSP],
300                target: vec![ProtocolId::Unknown(99)],
301                expected: Err(HandshakeError::NoCommonProtocol),
302            },
303            // Target supports nothing
304            TestCase {
305                host: vec![ProtocolId::ResendSP],
306                target: vec![],
307                expected: Err(HandshakeError::NoCommonProtocol),
308            },
309        ];
310
311        for (i, case) in cases.into_iter().enumerate() {
312            let result = run_handshake(case.host, case.target);
313            assert_eq!(result, case.expected, "Failed test case {i}");
314        }
315    }
316
317    #[fuchsia::test]
318    fn test_unproposed_protocol_selection_rejected_by_host() {
319        let host = HostHandshake::new(vec![ProtocolId::ResendSP]);
320        let mut fake_resp = Vec::new();
321        fake_resp.push(u8::from(HandshakeStatus::Success));
322        fake_resp.extend_from_slice(&ProtocolId::Unknown(42).wire_id().to_be_bytes());
323
324        assert_eq!(host.handle_response(&fake_resp), Err(HandshakeError::ProtocolNotProposed));
325    }
326
327    #[fuchsia::test]
328    fn test_malformed_payload_rejections() {
329        let target = TargetHandshake::new(vec![ProtocolId::ResendSP]);
330        assert_eq!(target.handle_request(&[][..]), Err(HandshakeError::MalformedPayload));
331        // Count claims 2 protocols (expected 1 + 8 = 9 bytes), but only 5 bytes provided
332        assert_eq!(
333            target.handle_request(&[2, 0, 0, 0, 1][..]),
334            Err(HandshakeError::MalformedPayload)
335        );
336
337        let host = HostHandshake::new(vec![ProtocolId::ResendSP]);
338        assert_eq!(host.handle_response(&[][..]), Err(HandshakeError::MalformedPayload));
339        assert_eq!(host.handle_response(&[0; 4][..]), Err(HandshakeError::MalformedPayload));
340
341        // Host proposals exceeding u8::MAX
342        let overflow_host = HostHandshake::new(vec![ProtocolId::ResendSP; 256]);
343        assert_eq!(overflow_host.start(), Err(HandshakeError::MalformedPayload));
344    }
345
346    #[fuchsia::test]
347    fn test_unsupported_status_rejection() {
348        let host = HostHandshake::new(vec![ProtocolId::ResendSP]);
349        let fake_resp = vec![99u8, 0, 0, 0, 1];
350        assert_eq!(host.handle_response(&fake_resp), Err(HandshakeError::UnsupportedStatus(99)));
351
352        assert_eq!(HandshakeStatus::from(99), HandshakeStatus::Unknown(99));
353        assert_eq!(u8::from(HandshakeStatus::Unknown(99)), 99);
354    }
355}