1use alloc::string::String;
2use alloc::vec::Vec;
3
4use bitflags::bitflags;
5use umsh_core::options::{OptionDecoder, OptionEncoder, parse_be_i32, parse_be_u32};
6
7use crate::app_util::parse_utf8;
8use crate::location::NodeLocation;
9use crate::{AppEncodeError, AppParseError};
10
11mod opt {
12 pub const NAME: u16 = 0;
13 pub const LOCATION: u16 = 1;
14 pub const ALTITUDE: u16 = 2;
15 pub const TIMESTAMP: u16 = 3;
16 pub const SUPPORTED_REGIONS: u16 = 4;
17}
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum NodeRole {
21 Unspecified,
22 Repeater,
23 Chat,
24 Tracker,
25 Sensor,
26 Bridge,
27 ChatRoom,
28 TemporarySession,
29 Unknown(u8),
31}
32
33impl NodeRole {
34 pub fn from_byte(value: u8) -> Self {
35 match value {
36 0 => Self::Unspecified,
37 1 => Self::Repeater,
38 2 => Self::Chat,
39 3 => Self::Tracker,
40 4 => Self::Sensor,
41 5 => Self::Bridge,
42 6 => Self::ChatRoom,
43 7 => Self::TemporarySession,
44 n => Self::Unknown(n),
45 }
46 }
47
48 pub fn as_byte(self) -> u8 {
49 match self {
50 Self::Unspecified => 0,
51 Self::Repeater => 1,
52 Self::Chat => 2,
53 Self::Tracker => 3,
54 Self::Sensor => 4,
55 Self::Bridge => 5,
56 Self::ChatRoom => 6,
57 Self::TemporarySession => 7,
58 Self::Unknown(n) => n,
59 }
60 }
61}
62
63bitflags! {
64 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
65 pub struct NodeCapabilities: u8 {
66 const REPEATER = 0x01;
67 const MOBILE = 0x02;
68 const TEXT_MESSAGES = 0x04;
69 const TELEMETRY = 0x08;
70 const CHAT_ROOM = 0x10;
71 const COAP = 0x20;
72 }
73}
74
75#[derive(Clone, Debug, PartialEq, Eq)]
76pub struct NodeIdentityPayload {
77 pub role: NodeRole,
78 pub capabilities: NodeCapabilities,
79 pub name: Option<String>,
81 pub location: Option<NodeLocation>,
83 pub altitude_m: Option<i32>,
85 pub timestamp: Option<u32>,
87 pub supported_regions: Option<Vec<u8>>,
89 pub signature: Option<[u8; 64]>,
91}
92
93impl NodeIdentityPayload {
94 pub fn from_bytes(payload: &[u8]) -> Result<NodeIdentityPayload, AppParseError> {
95 if payload.len() < 2 {
96 return Err(AppParseError::Core(umsh_core::ParseError::Truncated));
97 }
98
99 let role = NodeRole::from_byte(payload[0]);
100 let capabilities = NodeCapabilities::from_bits_truncate(payload[1]);
101 let remaining = &payload[2..];
102
103 let mut name = None;
104 let mut location = None;
105 let mut altitude_m = None;
106 let mut timestamp = None;
107 let mut supported_regions = None;
108
109 let mut decoder = OptionDecoder::new(remaining);
110 for result in decoder.by_ref() {
111 let (number, value) = result?;
112 match number {
113 opt::NAME => name = Some(String::from(parse_utf8(value)?)),
114 opt::LOCATION => {
115 location = Some(NodeLocation::from_bytes(value));
117 }
118 opt::ALTITUDE => altitude_m = Some(parse_be_i32(value)?),
119 opt::TIMESTAMP => timestamp = Some(parse_be_u32(value)?),
120 opt::SUPPORTED_REGIONS => {
121 if value.len() % 2 != 0 {
122 return Err(AppParseError::InvalidOptionValue);
123 }
124 supported_regions = Some(Vec::from(value));
125 }
126 _ => {} }
128 }
129
130 let sig_bytes = decoder.remainder();
131 let signature = match sig_bytes.len() {
132 0 => None,
133 64 => Some(
134 sig_bytes
135 .try_into()
136 .map_err(|_| AppParseError::InvalidLength {
137 expected: 64,
138 actual: sig_bytes.len(),
139 })?,
140 ),
141 n => {
142 return Err(AppParseError::InvalidLength {
143 expected: 64,
144 actual: n,
145 });
146 }
147 };
148
149 Ok(NodeIdentityPayload {
150 role,
151 capabilities,
152 name,
153 location,
154 altitude_m,
155 timestamp,
156 supported_regions,
157 signature,
158 })
159 }
160
161 pub fn encode(&self, buf: &mut [u8]) -> Result<usize, AppEncodeError> {
162 if buf.len() < 2 {
163 return Err(AppEncodeError::BufferTooSmall);
164 }
165 buf[0] = self.role.as_byte();
166 buf[1] = self.capabilities.bits();
167 let mut pos = 2;
168
169 {
170 let mut enc = OptionEncoder::new(&mut buf[pos..]);
171 if let Some(name) = self.name.as_deref() {
172 enc.put(opt::NAME, name.as_bytes())?;
173 }
174 if let Some(loc) = self.location {
175 enc.put(opt::LOCATION, loc.as_bytes())?;
176 }
177 if let Some(alt) = self.altitude_m {
178 enc.put_i32(opt::ALTITUDE, alt)?;
179 }
180 if let Some(ts) = self.timestamp {
181 enc.put_u32(opt::TIMESTAMP, ts)?;
182 }
183 if let Some(regions) = self.supported_regions.as_deref() {
184 enc.put(opt::SUPPORTED_REGIONS, regions)?;
185 }
186 if self.signature.is_some() {
187 enc.end_marker()?;
188 }
189 pos += enc.finish();
190 }
191
192 if let Some(sig) = &self.signature {
193 if pos + 64 > buf.len() {
194 return Err(AppEncodeError::BufferTooSmall);
195 }
196 buf[pos..pos + 64].copy_from_slice(sig);
197 pos += 64;
198 }
199
200 Ok(pos)
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 fn round_trip(id: &NodeIdentityPayload) -> bool {
209 let mut buf = [0u8; 256];
210 let len = id.encode(&mut buf).expect("encode failed");
211 let decoded = NodeIdentityPayload::from_bytes(&buf[..len]).expect("parse failed");
212 decoded == *id
213 }
214
215 #[test]
216 fn minimal_two_bytes() {
217 let id = NodeIdentityPayload {
218 role: NodeRole::Chat,
219 capabilities: NodeCapabilities::TEXT_MESSAGES,
220 name: None,
221 location: None,
222 altitude_m: None,
223 timestamp: None,
224 supported_regions: None,
225 signature: None,
226 };
227 let mut buf = [0u8; 16];
228 let len = id.encode(&mut buf).unwrap();
229 assert_eq!(len, 2);
230 assert_eq!(buf[0], 2); assert_eq!(buf[1], NodeCapabilities::TEXT_MESSAGES.bits());
232 assert!(round_trip(&id));
233 }
234
235 #[test]
236 fn name_only() {
237 let id = NodeIdentityPayload {
238 role: NodeRole::Unspecified,
239 capabilities: NodeCapabilities::empty(),
240 name: Some("Alice".into()),
241 location: None,
242 altitude_m: None,
243 timestamp: None,
244 supported_regions: None,
245 signature: None,
246 };
247 assert!(round_trip(&id));
248 }
249
250 #[test]
251 fn all_options() {
252 let loc = NodeLocation::from_bytes(&[0x2B, 0x95, 0x51]);
253 let id = NodeIdentityPayload {
254 role: NodeRole::Repeater,
255 capabilities: NodeCapabilities::REPEATER | NodeCapabilities::TEXT_MESSAGES,
256 name: Some("tower".into()),
257 location: Some(loc),
258 altitude_m: Some(1500),
259 timestamp: Some(1_700_000_000),
260 supported_regions: Some(vec![0x00, 0x01, 0x00, 0x02]),
261 signature: None,
262 };
263 assert!(round_trip(&id));
264 }
265
266 #[test]
267 fn negative_altitude() {
268 let id = NodeIdentityPayload {
269 role: NodeRole::Sensor,
270 capabilities: NodeCapabilities::empty(),
271 name: None,
272 location: None,
273 altitude_m: Some(-430), timestamp: None,
275 supported_regions: None,
276 signature: None,
277 };
278 assert!(round_trip(&id));
279 }
280
281 #[test]
282 fn altitude_zero() {
283 let id = NodeIdentityPayload {
284 role: NodeRole::Sensor,
285 capabilities: NodeCapabilities::empty(),
286 name: None,
287 location: None,
288 altitude_m: Some(0),
289 timestamp: None,
290 supported_regions: None,
291 signature: None,
292 };
293 assert!(round_trip(&id));
294 }
295
296 #[test]
297 fn with_signature() {
298 let id = NodeIdentityPayload {
299 role: NodeRole::Chat,
300 capabilities: NodeCapabilities::empty(),
301 name: Some("Bob".into()),
302 location: None,
303 altitude_m: None,
304 timestamp: Some(1_700_000_000),
305 supported_regions: None,
306 signature: Some([0xAAu8; 64]),
307 };
308 assert!(round_trip(&id));
309 }
310}