1use crate::error::AprsError;
2use std::fmt;
3
4const MAX_CALL_LEN: usize = 9;
7
8#[derive(Clone, PartialEq, Eq, Hash)]
10struct CallBuf {
11 bytes: [u8; MAX_CALL_LEN],
12 len: u8,
13}
14
15impl CallBuf {
16 fn as_str(&self) -> &str {
17 std::str::from_utf8(&self.bytes[..self.len as usize]).expect("callsign is always ASCII")
18 }
19}
20
21const MAX_SSID_LEN: usize = 6;
25
26#[derive(Clone, PartialEq, Eq, Hash)]
28struct SsidBuf {
29 bytes: [u8; MAX_SSID_LEN],
30 len: u8,
31}
32
33impl SsidBuf {
34 fn from_uppercased(src: &[u8]) -> Self {
35 let mut bytes = [0u8; MAX_SSID_LEN];
36 for (i, &b) in src.iter().enumerate() {
37 bytes[i] = b.to_ascii_uppercase();
38 }
39 SsidBuf {
40 bytes,
41 len: src.len() as u8,
42 }
43 }
44
45 fn as_str(&self) -> &str {
46 std::str::from_utf8(&self.bytes[..self.len as usize]).expect("ssid is always ASCII")
47 }
48}
49
50#[derive(Clone, PartialEq, Eq, Hash)]
57pub struct Callsign {
58 call: CallBuf,
59 ssid: Option<SsidBuf>,
60}
61
62#[cfg(feature = "serde")]
63impl serde::Serialize for Callsign {
64 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
65 s.serialize_str(&self.to_string())
66 }
67}
68
69#[cfg(feature = "serde")]
70impl<'de> serde::Deserialize<'de> for Callsign {
71 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
72 let s = String::deserialize(d)?;
73 Callsign::decode_textual(s.as_bytes()).map_err(serde::de::Error::custom)
74 }
75}
76
77impl Callsign {
78 pub fn decode_textual(input: &[u8]) -> Result<Self, AprsError> {
80 let (call_bytes, ssid) = if let Some(pos) = input.iter().position(|&b| b == b'-') {
81 let ssid = parse_ssid(&input[pos + 1..]).ok_or_else(|| AprsError::InvalidCallsign {
85 raw: input.to_vec(),
86 })?;
87 (&input[..pos], ssid)
88 } else {
89 (input, None)
90 };
91
92 let call = parse_call(call_bytes).ok_or_else(|| AprsError::InvalidCallsign {
93 raw: input.to_vec(),
94 })?;
95
96 Ok(Callsign { call, ssid })
97 }
98
99 pub fn decode_ax25(bytes: &[u8]) -> Result<(Self, bool), AprsError> {
105 if bytes.len() < 7 {
106 return Err(AprsError::TruncatedPacket {
107 expected: 7,
108 got: bytes.len(),
109 });
110 }
111 let mut raw_call = [b' '; 6];
112 for i in 0..6 {
113 let shifted = bytes[i];
114 if shifted & 0x01 != 0 {
116 return Err(AprsError::InvalidCallsign {
117 raw: bytes[..7].to_vec(),
118 });
119 }
120 raw_call[i] = shifted >> 1;
121 }
122 let end = raw_call
124 .iter()
125 .rposition(|&b| b != b' ')
126 .map(|p| p + 1)
127 .unwrap_or(0);
128 let call = parse_call(&raw_call[..end]).ok_or_else(|| AprsError::InvalidCallsign {
129 raw: bytes[..7].to_vec(),
130 })?;
131
132 let ssid_byte = bytes[6];
133 let ssid_val = (ssid_byte >> 1) & 0x0F;
134 let ssid = if ssid_val == 0 {
136 None
137 } else {
138 let mut tmp = [0u8; 2];
139 let s: &[u8] = if ssid_val >= 10 {
140 tmp[0] = b'0' + ssid_val / 10;
141 tmp[1] = b'0' + ssid_val % 10;
142 &tmp[..2]
143 } else {
144 tmp[0] = b'0' + ssid_val;
145 &tmp[..1]
146 };
147 Some(SsidBuf::from_uppercased(s))
148 };
149 let eoa = ssid_byte & 0x01 != 0;
150
151 Ok((Callsign { call, ssid }, eoa))
152 }
153
154 pub fn as_str(&self) -> &str {
155 self.call.as_str()
156 }
157
158 pub fn ssid(&self) -> Option<&str> {
160 self.ssid.as_ref().map(SsidBuf::as_str)
161 }
162
163 pub fn ssid_numeric(&self) -> Option<u8> {
166 self.ssid()
167 .and_then(|s| s.parse::<u8>().ok())
168 .filter(|v| *v <= 15)
169 }
170
171 pub fn encode_textual(&self, out: &mut Vec<u8>) {
173 out.extend_from_slice(self.call.as_str().as_bytes());
174 if let Some(ref ssid) = self.ssid {
175 out.push(b'-');
176 out.extend_from_slice(ssid.as_str().as_bytes());
177 }
178 }
179
180 pub fn encode_ax25(&self, out: &mut Vec<u8>, eoa: bool) {
185 let call = self.call.as_str().as_bytes();
186 for i in 0..6 {
187 let b = if i < call.len() { call[i] } else { b' ' };
188 out.push(b << 1);
189 }
190 let ssid_val = self.ssid_numeric().unwrap_or(0) & 0x0F;
191 let eoa_bit: u8 = if eoa { 0x01 } else { 0x00 };
192 out.push(0x60 | (ssid_val << 1) | eoa_bit);
194 }
195}
196
197impl fmt::Debug for Callsign {
198 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199 write!(f, "{self}")
200 }
201}
202
203impl fmt::Display for Callsign {
204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205 write!(f, "{}", self.call.as_str())?;
206 if let Some(ref ssid) = self.ssid {
207 write!(f, "-{}", ssid.as_str())?;
208 }
209 Ok(())
210 }
211}
212
213fn parse_call(bytes: &[u8]) -> Option<CallBuf> {
216 if bytes.is_empty() || bytes.len() > MAX_CALL_LEN {
217 return None;
218 }
219 let mut buf = [0u8; MAX_CALL_LEN];
220 for (i, &b) in bytes.iter().enumerate() {
221 if !b.is_ascii_alphanumeric() {
222 return None;
223 }
224 buf[i] = b.to_ascii_uppercase();
225 }
226 Some(CallBuf {
227 bytes: buf,
228 len: bytes.len() as u8,
229 })
230}
231
232fn parse_ssid(bytes: &[u8]) -> Option<Option<SsidBuf>> {
240 if bytes.is_empty() || bytes.len() > MAX_SSID_LEN {
241 return None;
242 }
243 if bytes.iter().all(u8::is_ascii_digit) {
244 let mut val: u8 = 0;
246 for &b in bytes {
247 val = val.checked_mul(10)?.checked_add(b - b'0')?;
248 }
249 if val > 15 {
250 return None;
251 }
252 if val == 0 {
253 return Some(None);
254 }
255 let mut tmp = [0u8; 2];
256 let s: &[u8] = if val >= 10 {
257 tmp[0] = b'0' + val / 10;
258 tmp[1] = b'0' + val % 10;
259 &tmp[..2]
260 } else {
261 tmp[0] = b'0' + val;
262 &tmp[..1]
263 };
264 return Some(Some(SsidBuf::from_uppercased(s)));
265 }
266 if !bytes.iter().all(u8::is_ascii_alphanumeric) {
268 return None;
269 }
270 Some(Some(SsidBuf::from_uppercased(bytes)))
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn textual_no_ssid() {
279 let c = Callsign::decode_textual(b"W1AW").unwrap();
280 assert_eq!(c.as_str(), "W1AW");
281 assert_eq!(c.ssid(), None);
282 }
283
284 #[test]
285 fn textual_with_ssid() {
286 let c = Callsign::decode_textual(b"W1AW-9").unwrap();
287 assert_eq!(c.as_str(), "W1AW");
288 assert_eq!(c.ssid(), Some("9"));
289 assert_eq!(c.ssid_numeric(), Some(9));
290 }
291
292 #[test]
293 fn textual_ssid_15() {
294 let c = Callsign::decode_textual(b"N0CALL-15").unwrap();
295 assert_eq!(c.ssid(), Some("15"));
296 assert_eq!(c.ssid_numeric(), Some(15));
297 }
298
299 #[test]
300 fn textual_ssid_16_invalid() {
301 assert!(Callsign::decode_textual(b"N0CALL-16").is_err());
302 }
303
304 #[test]
305 fn textual_ssid_0_normalized_to_none() {
306 let c = Callsign::decode_textual(b"W1AW-0").unwrap();
307 assert_eq!(c.ssid(), None);
308 assert_eq!(c.to_string(), "W1AW");
309 }
310
311 #[test]
312 fn textual_alphanumeric_ssid_dstar() {
313 let c = Callsign::decode_textual(b"K0HRV-S").unwrap();
315 assert_eq!(c.as_str(), "K0HRV");
316 assert_eq!(c.ssid(), Some("S"));
317 assert_eq!(c.ssid_numeric(), None);
318 assert_eq!(c.to_string(), "K0HRV-S");
319 }
320
321 #[test]
322 fn textual_lowercase_normalized() {
323 let c = Callsign::decode_textual(b"w1aw").unwrap();
324 assert_eq!(c.as_str(), "W1AW");
325 }
326
327 #[test]
328 fn textual_empty_invalid() {
329 assert!(Callsign::decode_textual(b"").is_err());
330 }
331
332 #[test]
333 fn display_no_ssid() {
334 let c = Callsign::decode_textual(b"W1AW").unwrap();
335 assert_eq!(c.to_string(), "W1AW");
336 }
337
338 #[test]
339 fn display_with_ssid() {
340 let c = Callsign::decode_textual(b"W1AW-9").unwrap();
341 assert_eq!(c.to_string(), "W1AW-9");
342 }
343
344 #[test]
345 fn ax25_round_trip() {
346 let original = Callsign::decode_textual(b"W1AW-9").unwrap();
347 let mut encoded = Vec::new();
348 original.encode_ax25(&mut encoded, true);
349 assert_eq!(encoded.len(), 7);
350 let (decoded, eoa) = Callsign::decode_ax25(&encoded).unwrap();
351 assert_eq!(decoded, original);
352 assert!(eoa);
353 }
354
355 #[test]
356 fn encode_textual_round_trip() {
357 let original = Callsign::decode_textual(b"KD9ABC-3").unwrap();
358 let mut out = Vec::new();
359 original.encode_textual(&mut out);
360 let decoded = Callsign::decode_textual(&out).unwrap();
361 assert_eq!(decoded, original);
362 }
363}