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 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 #[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 #[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 #[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()); }
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()); }
391
392 #[test]
393 fn parse_pfs_request_wrong_length() {
394 assert!(parse(&[0x06, 0x00]).is_err()); }
396
397 #[test]
398 fn parse_end_pfs_nonempty_body() {
399 assert!(parse(&[0x08, 0x00]).is_err());
400 }
401}