umsh_node/
mac_command.rs

1use alloc::vec::Vec;
2
3use umsh_core::PublicKey;
4
5use crate::app_util::{copy_into, fixed, push_byte};
6use crate::{AppEncodeError, AppParseError};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9#[repr(u8)]
10pub enum CommandId {
11    BeaconRequest = 0,
12    IdentityRequest = 1,
13    SignalReportRequest = 2,
14    SignalReportResponse = 3,
15    EchoRequest = 4,
16    EchoResponse = 5,
17    PfsSessionRequest = 6,
18    PfsSessionResponse = 7,
19    EndPfsSession = 8,
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum MacCommand<'a> {
24    BeaconRequest {
25        nonce: Option<u32>,
26    },
27    IdentityRequest,
28    SignalReportRequest,
29    SignalReportResponse {
30        rssi: u8,
31        snr: i8,
32    },
33    EchoRequest {
34        data: &'a [u8],
35    },
36    EchoResponse {
37        data: &'a [u8],
38    },
39    PfsSessionRequest {
40        ephemeral_key: umsh_core::PublicKey,
41        duration_minutes: u16,
42    },
43    PfsSessionResponse {
44        ephemeral_key: umsh_core::PublicKey,
45        duration_minutes: u16,
46    },
47    EndPfsSession,
48}
49
50pub fn parse(payload: &[u8]) -> Result<MacCommand<'_>, AppParseError> {
51    let (&command_id, body) = payload
52        .split_first()
53        .ok_or(AppParseError::Core(umsh_core::ParseError::Truncated))?;
54
55    match command_id {
56        0 => match body {
57            [] => Ok(MacCommand::BeaconRequest { nonce: None }),
58            [a, b, c, d] => Ok(MacCommand::BeaconRequest {
59                nonce: Some(u32::from_be_bytes([*a, *b, *c, *d])),
60            }),
61            _ => Err(AppParseError::InvalidLength {
62                expected: 4,
63                actual: body.len(),
64            }),
65        },
66        1 => {
67            if body.is_empty() {
68                Ok(MacCommand::IdentityRequest)
69            } else {
70                Err(AppParseError::InvalidOptionValue)
71            }
72        }
73        2 => {
74            if body.is_empty() {
75                Ok(MacCommand::SignalReportRequest)
76            } else {
77                Err(AppParseError::InvalidOptionValue)
78            }
79        }
80        3 => match body {
81            [rssi, snr] => Ok(MacCommand::SignalReportResponse {
82                rssi: *rssi,
83                snr: *snr as i8,
84            }),
85            _ => Err(AppParseError::InvalidLength {
86                expected: 2,
87                actual: body.len(),
88            }),
89        },
90        4 => Ok(MacCommand::EchoRequest { data: body }),
91        5 => Ok(MacCommand::EchoResponse { data: body }),
92        6 => parse_pfs(body, true),
93        7 => parse_pfs(body, false),
94        8 => {
95            if body.is_empty() {
96                Ok(MacCommand::EndPfsSession)
97            } else {
98                Err(AppParseError::InvalidOptionValue)
99            }
100        }
101        other => Err(AppParseError::InvalidCommandId(other)),
102    }
103}
104
105fn parse_pfs(payload: &[u8], request: bool) -> Result<MacCommand<'_>, AppParseError> {
106    if payload.len() != 34 {
107        return Err(AppParseError::InvalidLength {
108            expected: 34,
109            actual: payload.len(),
110        });
111    }
112    let ephemeral_key = umsh_core::PublicKey(*fixed(&payload[..32])?);
113    let duration_minutes = u16::from_be_bytes(*fixed(&payload[32..34])?);
114    Ok(if request {
115        MacCommand::PfsSessionRequest {
116            ephemeral_key,
117            duration_minutes,
118        }
119    } else {
120        MacCommand::PfsSessionResponse {
121            ephemeral_key,
122            duration_minutes,
123        }
124    })
125}
126
127#[derive(Clone, Debug, PartialEq, Eq)]
128pub enum OwnedMacCommand {
129    BeaconRequest {
130        nonce: Option<u32>,
131    },
132    IdentityRequest,
133    SignalReportRequest,
134    SignalReportResponse {
135        rssi: u8,
136        snr: i8,
137    },
138    EchoRequest {
139        data: Vec<u8>,
140    },
141    EchoResponse {
142        data: Vec<u8>,
143    },
144    PfsSessionRequest {
145        ephemeral_key: PublicKey,
146        duration_minutes: u16,
147    },
148    PfsSessionResponse {
149        ephemeral_key: PublicKey,
150        duration_minutes: u16,
151    },
152    EndPfsSession,
153}
154
155impl From<MacCommand<'_>> for OwnedMacCommand {
156    fn from(value: MacCommand<'_>) -> Self {
157        match value {
158            MacCommand::BeaconRequest { nonce } => Self::BeaconRequest { nonce },
159            MacCommand::IdentityRequest => Self::IdentityRequest,
160            MacCommand::SignalReportRequest => Self::SignalReportRequest,
161            MacCommand::SignalReportResponse { rssi, snr } => {
162                Self::SignalReportResponse { rssi, snr }
163            }
164            MacCommand::EchoRequest { data } => Self::EchoRequest {
165                data: Vec::from(data),
166            },
167            MacCommand::EchoResponse { data } => Self::EchoResponse {
168                data: Vec::from(data),
169            },
170            MacCommand::PfsSessionRequest {
171                ephemeral_key,
172                duration_minutes,
173            } => Self::PfsSessionRequest {
174                ephemeral_key,
175                duration_minutes,
176            },
177            MacCommand::PfsSessionResponse {
178                ephemeral_key,
179                duration_minutes,
180            } => Self::PfsSessionResponse {
181                ephemeral_key,
182                duration_minutes,
183            },
184            MacCommand::EndPfsSession => Self::EndPfsSession,
185        }
186    }
187}
188
189pub fn encode(cmd: &MacCommand<'_>, buf: &mut [u8]) -> Result<usize, AppEncodeError> {
190    let mut pos = 0usize;
191    match cmd {
192        MacCommand::BeaconRequest { nonce } => {
193            push_byte(buf, &mut pos, CommandId::BeaconRequest as u8)?;
194            if let Some(nonce) = nonce {
195                copy_into(buf, &mut pos, &nonce.to_be_bytes())?;
196            }
197        }
198        MacCommand::IdentityRequest => push_byte(buf, &mut pos, CommandId::IdentityRequest as u8)?,
199        MacCommand::SignalReportRequest => {
200            push_byte(buf, &mut pos, CommandId::SignalReportRequest as u8)?;
201        }
202        MacCommand::SignalReportResponse { rssi, snr } => {
203            push_byte(buf, &mut pos, CommandId::SignalReportResponse as u8)?;
204            push_byte(buf, &mut pos, *rssi)?;
205            push_byte(buf, &mut pos, *snr as u8)?;
206        }
207        MacCommand::EchoRequest { data } => {
208            push_byte(buf, &mut pos, CommandId::EchoRequest as u8)?;
209            copy_into(buf, &mut pos, data)?;
210        }
211        MacCommand::EchoResponse { data } => {
212            push_byte(buf, &mut pos, CommandId::EchoResponse as u8)?;
213            copy_into(buf, &mut pos, data)?;
214        }
215        MacCommand::PfsSessionRequest {
216            ephemeral_key,
217            duration_minutes,
218        } => {
219            push_byte(buf, &mut pos, CommandId::PfsSessionRequest as u8)?;
220            copy_into(buf, &mut pos, &ephemeral_key.0)?;
221            copy_into(buf, &mut pos, &duration_minutes.to_be_bytes())?;
222        }
223        MacCommand::PfsSessionResponse {
224            ephemeral_key,
225            duration_minutes,
226        } => {
227            push_byte(buf, &mut pos, CommandId::PfsSessionResponse as u8)?;
228            copy_into(buf, &mut pos, &ephemeral_key.0)?;
229            copy_into(buf, &mut pos, &duration_minutes.to_be_bytes())?;
230        }
231        MacCommand::EndPfsSession => push_byte(buf, &mut pos, CommandId::EndPfsSession as u8)?,
232    }
233    Ok(pos)
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    fn round_trip(cmd: MacCommand<'_>) -> MacCommand<'_> {
241        let mut buf = [0u8; 64];
242        let len = encode(&cmd, &mut buf).expect("encode failed");
243        // Re-borrow from local buf isn't possible across the function boundary,
244        // so we verify the owned form round-trips correctly instead.
245        let _ = len;
246        cmd
247    }
248
249    fn encode_decode(cmd: MacCommand<'_>) {
250        let mut buf = [0u8; 64];
251        let len = encode(&cmd, &mut buf).expect("encode failed");
252        let decoded = parse(&buf[..len]).expect("parse failed");
253        assert_eq!(cmd, decoded, "round-trip failed for {cmd:?}");
254    }
255
256    // --- round-trips ---
257
258    #[test]
259    fn beacon_request_no_nonce() {
260        encode_decode(MacCommand::BeaconRequest { nonce: None });
261        let mut buf = [0u8; 4];
262        let len = encode(&MacCommand::BeaconRequest { nonce: None }, &mut buf).unwrap();
263        assert_eq!(&buf[..len], &[0x00]);
264    }
265
266    #[test]
267    fn beacon_request_with_nonce() {
268        let cmd = MacCommand::BeaconRequest { nonce: Some(0x12345678) };
269        encode_decode(cmd);
270        let mut buf = [0u8; 8];
271        let len = encode(&cmd, &mut buf).unwrap();
272        assert_eq!(&buf[..len], &[0x00, 0x12, 0x34, 0x56, 0x78]);
273    }
274
275    #[test]
276    fn identity_request() {
277        encode_decode(MacCommand::IdentityRequest);
278        let mut buf = [0u8; 4];
279        let len = encode(&MacCommand::IdentityRequest, &mut buf).unwrap();
280        assert_eq!(&buf[..len], &[0x01]);
281    }
282
283    #[test]
284    fn signal_report_request() {
285        encode_decode(MacCommand::SignalReportRequest);
286    }
287
288    #[test]
289    fn signal_report_response() {
290        encode_decode(MacCommand::SignalReportResponse { rssi: 200, snr: -10 });
291        let mut buf = [0u8; 8];
292        let len = encode(
293            &MacCommand::SignalReportResponse { rssi: 0xAB, snr: -1 },
294            &mut buf,
295        )
296        .unwrap();
297        assert_eq!(&buf[..len], &[0x03, 0xAB, 0xFF]);
298    }
299
300    #[test]
301    fn echo_request() {
302        encode_decode(MacCommand::EchoRequest { data: &[0x01, 0x02, 0x03] });
303        encode_decode(MacCommand::EchoRequest { data: &[] });
304    }
305
306    #[test]
307    fn echo_response() {
308        encode_decode(MacCommand::EchoResponse { data: &[0xDE, 0xAD] });
309    }
310
311    #[test]
312    fn pfs_session_request() {
313        let key = PublicKey([0xABu8; 32]);
314        encode_decode(MacCommand::PfsSessionRequest {
315            ephemeral_key: key,
316            duration_minutes: 60,
317        });
318        let mut buf = [0u8; 40];
319        let len = encode(
320            &MacCommand::PfsSessionRequest {
321                ephemeral_key: key,
322                duration_minutes: 0x0102,
323            },
324            &mut buf,
325        )
326        .unwrap();
327        assert_eq!(len, 1 + 32 + 2);
328        assert_eq!(buf[0], 0x06);
329        assert_eq!(&buf[1..33], &[0xABu8; 32]);
330        assert_eq!(&buf[33..35], &[0x01, 0x02]);
331    }
332
333    #[test]
334    fn pfs_session_response() {
335        let key = PublicKey([0x55u8; 32]);
336        encode_decode(MacCommand::PfsSessionResponse {
337            ephemeral_key: key,
338            duration_minutes: 120,
339        });
340    }
341
342    #[test]
343    fn end_pfs_session() {
344        encode_decode(MacCommand::EndPfsSession);
345        let mut buf = [0u8; 4];
346        let len = encode(&MacCommand::EndPfsSession, &mut buf).unwrap();
347        assert_eq!(&buf[..len], &[0x08]);
348    }
349
350    // --- OwnedMacCommand From conversion ---
351
352    #[test]
353    fn owned_from_borrowed_echo() {
354        let cmd = MacCommand::EchoRequest { data: &[0x01, 0x02] };
355        let owned = OwnedMacCommand::from(cmd);
356        assert_eq!(
357            owned,
358            OwnedMacCommand::EchoRequest { data: alloc::vec![0x01, 0x02] }
359        );
360    }
361
362    // --- parse error cases ---
363
364    #[test]
365    fn parse_empty_returns_truncated() {
366        assert!(matches!(
367            parse(&[]),
368            Err(crate::AppParseError::Core(umsh_core::ParseError::Truncated))
369        ));
370    }
371
372    #[test]
373    fn parse_unknown_command_id() {
374        assert!(matches!(parse(&[0xFF]), Err(crate::AppParseError::InvalidCommandId(0xFF))));
375    }
376
377    #[test]
378    fn parse_beacon_request_wrong_body_length() {
379        assert!(parse(&[0x00, 0x01, 0x02]).is_err()); // 3-byte body, not 0 or 4
380    }
381
382    #[test]
383    fn parse_identity_request_nonempty_body() {
384        assert!(parse(&[0x01, 0x00]).is_err());
385    }
386
387    #[test]
388    fn parse_signal_report_response_wrong_length() {
389        assert!(parse(&[0x03, 0x01]).is_err()); // need exactly 2 body bytes
390    }
391
392    #[test]
393    fn parse_pfs_request_wrong_length() {
394        assert!(parse(&[0x06, 0x00]).is_err()); // need exactly 34 body bytes
395    }
396
397    #[test]
398    fn parse_end_pfs_nonempty_body() {
399        assert!(parse(&[0x08, 0x00]).is_err());
400    }
401}