aprs_parser/
callsign.rs

1use std::fmt::{Display, Formatter};
2use std::io::{self, Write};
3
4use EncodeError;
5
6pub enum CallsignField {
7    Destination,
8    Source,
9    Via(bool),
10}
11
12#[derive(Eq, PartialEq, Debug, Clone, Hash)]
13pub struct Callsign {
14    call: String,
15    ssid: Option<String>,
16}
17
18impl Callsign {
19    /// Create a new callsign.
20    /// SSID is parsed out.
21    pub fn new(s: impl AsRef<str>) -> Option<Self> {
22        match s.as_ref().split_once('-') {
23            Some((call, ssid)) => {
24                if call.is_empty() || ssid.is_empty() {
25                    None
26                } else {
27                    Some(Callsign::new_with_ssid(call.to_owned(), ssid))
28                }
29            }
30
31            None => Some(Callsign::new_no_ssid(s.as_ref())),
32        }
33    }
34
35    /// Create a new callsign
36    /// Note: If you need a callsign with an SSID, use `new_with_ssid`.
37    pub fn new_no_ssid(call: impl Into<String>) -> Callsign {
38        let call = call.into();
39        let ssid = None;
40        Callsign { call, ssid }
41    }
42
43    pub fn new_with_ssid(call: impl Into<String>, ssid: impl Into<String>) -> Callsign {
44        let call = call.into();
45        let ssid = ssid.into();
46
47        let ssid = if ssid == "0" { None } else { Some(ssid) };
48
49        Callsign { call, ssid }
50    }
51
52    pub fn call(&self) -> &str {
53        &self.call
54    }
55
56    pub fn ssid(&self) -> Option<&str> {
57        self.ssid.as_deref()
58    }
59
60    pub fn encode_textual<W: Write>(&self, heard: bool, w: &mut W) -> io::Result<()> {
61        write!(w, "{}", self)?;
62
63        if heard {
64            write!(w, "*")?;
65        }
66
67        Ok(())
68    }
69
70    pub fn decode_textual(bytes: &[u8]) -> Option<(Self, bool)> {
71        let (bytes, heard) = if bytes.last() == Some(&b'*') {
72            (&bytes[0..(bytes.len() - 1)], true)
73        } else {
74            (bytes, false)
75        };
76
77        let s = std::str::from_utf8(bytes).ok()?;
78
79        Self::new(s).map(|c| (c, heard))
80    }
81
82    pub fn encode_ax25<W: Write>(
83        &self,
84        buf: &mut W,
85        field: CallsignField,
86        has_more: bool,
87    ) -> Result<(), EncodeError> {
88        // callsign requirements:
89        // <= 6 bytes long
90        // all alphanumeric uppercase
91        // ssid missing or a number between 0 and 15
92
93        let call = self.call.as_bytes();
94        if call.len() > 6 {
95            return Err(EncodeError::InvalidCallsign(self.clone()));
96        }
97
98        let ssid: u8 = self
99            .ssid
100            .clone()
101            .map(|x| x.parse().ok())
102            .unwrap_or(Some(0))
103            .ok_or_else(|| EncodeError::InvalidCallsign(self.clone()))?;
104
105        if ssid > 15 {
106            return Err(EncodeError::InvalidCallsign(self.clone()));
107        }
108
109        let has_more = if has_more { 0 } else { 1 };
110
111        for c in call {
112            if !c.is_ascii_alphanumeric() {
113                return Err(EncodeError::InvalidCallsign(self.clone()));
114            }
115
116            buf.write_all(&[c.to_ascii_uppercase() << 1])?;
117        }
118
119        for _ in call.len()..6 {
120            buf.write_all(&[b' ' << 1])?;
121        }
122
123        match field {
124            CallsignField::Destination => {
125                buf.write_all(&[0b11100000 | (ssid << 1) | has_more])?;
126            }
127            CallsignField::Source => {
128                buf.write_all(&[0b01100000 | (ssid << 1) | has_more])?;
129            }
130            CallsignField::Via(heard) => {
131                let heard = if heard { 1 } else { 0 };
132
133                buf.write_all(&[0b01100000 | (ssid << 1) | (heard << 7) | has_more])?;
134            }
135        }
136
137        Ok(())
138    }
139
140    // Returns self, heard, and the flag for if there's another callsign after
141    pub fn decode_ax25(data: &[u8]) -> Option<(Self, bool, bool)> {
142        if data.len() != 7 {
143            return None;
144        }
145
146        let mut call = String::new();
147        let mut found_space = false;
148        for d in &data[0..6] {
149            // LSB must be cleared
150            if (d & 0x01) != 0 {
151                return None;
152            }
153            let d = d >> 1;
154
155            if d == b' ' {
156                found_space = true;
157                continue;
158            }
159
160            // non-space char after a space
161            if found_space {
162                return None;
163            }
164
165            if !d.is_ascii_alphanumeric() {
166                return None;
167            }
168
169            call.push(d.to_ascii_uppercase().into());
170        }
171
172        let s = data[6];
173        let heard = (s & 0x80) != 0;
174        let has_more = (s & 0x01) == 0;
175        let ssid = (s & 0x1E) >> 1;
176
177        let ssid = if ssid == 0 {
178            None
179        } else {
180            Some(format!("{}", ssid))
181        };
182
183        Some((Callsign { call, ssid }, heard, has_more))
184    }
185}
186
187impl Display for Callsign {
188    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
189        write!(f, "{}", self.call)?;
190
191        if let Some(ssid) = &self.ssid {
192            if !ssid.is_empty() {
193                write!(f, "-{}", ssid)?;
194            }
195        }
196
197        Ok(())
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn parse_callsign() {
207        assert_eq!(
208            Callsign::decode_textual(&b"ABCDEF"[..]),
209            Some((Callsign::new("ABCDEF").unwrap(), false))
210        );
211    }
212
213    #[test]
214    fn parse_heard_callsign() {
215        assert_eq!(
216            Callsign::decode_textual(&b"ABCDEF*"[..]),
217            Some((Callsign::new("ABCDEF").unwrap(), true))
218        );
219    }
220
221    #[test]
222    fn parse_with_ssid() {
223        assert_eq!(
224            Callsign::decode_textual(&b"ABCDEF-2"[..]),
225            Some((Callsign::new_with_ssid("ABCDEF", "2"), false))
226        );
227
228        assert_eq!(
229            Callsign::decode_textual(&b"ABCDEF-0"[..]),
230            Some((Callsign::new_no_ssid("ABCDEF"), false))
231        );
232    }
233
234    #[test]
235    fn omit_end_spaces() {
236        assert_eq!(
237            Callsign::decode_ax25(&[172, 138, 114, 64, 64, 64, 1]),
238            Some((Callsign::new_no_ssid("VE9"), false, false))
239        )
240    }
241
242    #[test]
243    fn spaces_in_middle() {
244        assert_eq!(Callsign::decode_ax25(&[172, 64, 114, 64, 64, 64, 1]), None)
245    }
246
247    #[test]
248    fn uppercase_callsign() {
249        assert_eq!(
250            Callsign::decode_ax25(&[236, 202, 114, 64, 64, 64, 1]),
251            Some((Callsign::new_no_ssid("VE9"), false, false))
252        );
253
254        let mut buf = vec![];
255        let c = Callsign::new_no_ssid("ve9");
256        c.encode_ax25(&mut buf, CallsignField::Destination, false)
257            .unwrap();
258        assert_eq!(&[172, 138, 114, 64, 64, 64, 225][..], buf);
259
260        // sanity check with callsign that's already uppercased
261        let mut buf = vec![];
262        let c = Callsign::new_no_ssid("VE9");
263        c.encode_ax25(&mut buf, CallsignField::Destination, false)
264            .unwrap();
265        assert_eq!(&[172, 138, 114, 64, 64, 64, 225][..], buf);
266    }
267
268    #[test]
269    fn non_alphanumeric() {
270        let mut buf = vec![];
271        assert!(matches!(
272        Callsign::new_no_ssid("VE9---").encode_ax25(
273            &mut buf,
274            CallsignField::Destination,
275            false
276        ),
277        Err(EncodeError::InvalidCallsign(c)) if c == Callsign::new_no_ssid(
278            "VE9---"
279        )));
280    }
281
282    #[test]
283    fn callsign_too_long() {
284        let mut buf = vec![];
285        assert!(matches!(
286            Callsign::new_no_ssid("VE9ABCD").encode_ax25(
287                &mut buf,
288                CallsignField::Source,
289                false
290            ),
291            Err(EncodeError::InvalidCallsign(c)) if c == Callsign::new_no_ssid(
292                "VE9ABCD"
293            )
294        ));
295    }
296
297    #[test]
298    fn empty_callsign() {
299        assert_eq!(Callsign::decode_textual("-3".as_bytes()), None);
300    }
301
302    #[test]
303    fn empty_ssid() {
304        assert_eq!(Callsign::decode_textual("ABCDEF-".as_bytes()), None);
305    }
306
307    #[test]
308    fn non_utf8() {
309        assert_eq!(Callsign::decode_textual(&b"ABCDEF\xF0\xA4\xAD"[..]), None);
310    }
311
312    #[test]
313    fn textual_no_ssid() {
314        let c = Callsign::new_no_ssid("ABCDEF");
315
316        assert_eq!("ABCDEF", format!("{}", c));
317
318        let mut buf = vec![];
319        c.encode_textual(true, &mut buf).unwrap();
320        assert_eq!(&b"ABCDEF*"[..], buf);
321
322        buf.clear();
323        c.encode_textual(false, &mut buf).unwrap();
324        assert_eq!(&b"ABCDEF"[..], buf);
325    }
326
327    #[test]
328    fn textual_with_ssid() {
329        let c = Callsign::new_with_ssid("ABCDEF", "XF");
330
331        assert_eq!("ABCDEF-XF", format!("{}", c));
332
333        let mut buf = vec![];
334        c.encode_textual(true, &mut buf).unwrap();
335        assert_eq!(&b"ABCDEF-XF*"[..], buf);
336
337        buf.clear();
338        c.encode_textual(false, &mut buf).unwrap();
339        assert_eq!(&b"ABCDEF-XF"[..], buf);
340    }
341
342    #[test]
343    fn textual_non_ascii_characters() {
344        let c = Callsign::new_with_ssid("ABCDEF\x001\x002", "XF\x002\x003");
345
346        assert_eq!("ABCDEF\x001\x002-XF\x002\x003", format!("{}", c));
347
348        let mut buf = vec![];
349        c.encode_textual(true, &mut buf).unwrap();
350        assert_eq!(&b"ABCDEF\x001\x002-XF\x002\x003*"[..], buf);
351
352        buf.clear();
353        c.encode_textual(false, &mut buf).unwrap();
354        assert_eq!(&b"ABCDEF\x001\x002-XF\x002\x003"[..], buf);
355    }
356
357    #[test]
358    fn display_no_ssid() {
359        assert_eq!("ABCDEF", format!("{}", Callsign::new_no_ssid("ABCDEF")));
360    }
361
362    #[test]
363    fn display_with_ssid() {
364        assert_eq!(
365            "ABCDEF-12",
366            format!("{}", Callsign::new_with_ssid("ABCDEF", "12"))
367        );
368    }
369}