aprs_parser/
packet.rs

1use std::borrow::Cow;
2use std::io::Write;
3
4use callsign::CallsignField;
5use AprsMessage;
6use AprsMicE;
7use AprsPosition;
8use AprsStatus;
9use Callsign;
10use DecodeError;
11use EncodeError;
12use Via;
13
14#[derive(PartialEq, Debug, Clone)]
15pub struct AprsPacket {
16    pub from: Callsign,
17    pub via: Vec<Via>,
18    pub data: AprsData,
19}
20
21impl AprsPacket {
22    pub fn decode_textual(s: &[u8]) -> Result<Self, DecodeError> {
23        let header_delimiter = s
24            .iter()
25            .position(|x| *x == b':')
26            .ok_or_else(|| DecodeError::InvalidPacket(s.to_owned()))?;
27        let (header, rest) = s.split_at(header_delimiter);
28        let body = &rest[1..];
29
30        let from_delimiter = header
31            .iter()
32            .position(|x| *x == b'>')
33            .ok_or_else(|| DecodeError::InvalidPacket(s.to_owned()))?;
34        let (from, rest) = header.split_at(from_delimiter);
35        let (from, _) = Callsign::decode_textual(from)
36            .ok_or_else(|| DecodeError::InvalidCallsign(from.to_owned()))?;
37
38        let to_and_via = &rest[1..];
39        let mut to_and_via = to_and_via.split(|x| *x == b',');
40
41        let to = to_and_via
42            .next()
43            .ok_or_else(|| DecodeError::InvalidPacket(s.to_owned()))?;
44        let (to, _) = Callsign::decode_textual(to)
45            .ok_or_else(|| DecodeError::InvalidCallsign(to.to_owned()))?;
46
47        let mut via = vec![];
48        for v in to_and_via {
49            via.push(Via::decode_textual(v).ok_or_else(|| DecodeError::InvalidVia(v.to_owned()))?);
50        }
51
52        // if our Via path looks like A,B,C*,D,E
53        // this really means A*,B*,C*,D,E
54        // so we need to propagate the `heard` flag backwards
55        let mut heard = false;
56        for v in via.iter_mut().rev() {
57            if let Some((_, c_heard)) = v.callsign_mut() {
58                if !heard {
59                    heard = *c_heard;
60                }
61                *c_heard = heard;
62            }
63        }
64
65        let data = AprsData::decode(body, to)?;
66
67        Ok(AprsPacket { from, via, data })
68    }
69
70    pub fn to(&self) -> Option<&Callsign> {
71        self.data.to()
72    }
73
74    /// Used for encoding a packet into ASCII for transmission on the internet (APRS-IS)
75    pub fn encode_textual<W: Write>(&self, buf: &mut W) -> Result<(), EncodeError> {
76        // logic to clear extraneous asterisks
77        let mut via = self.via.clone();
78        let mut heard = false;
79        for v in via.iter_mut().rev() {
80            if let Some((_, c_heard)) = v.callsign_mut() {
81                if !heard {
82                    heard = *c_heard;
83                } else {
84                    *c_heard = false;
85                }
86            }
87        }
88
89        self.from.encode_textual(false, buf)?;
90        write!(buf, ">")?;
91        self.data.dest_field().encode_textual(false, buf)?;
92        for v in &via {
93            write!(buf, ",")?;
94            v.encode_textual(buf)?;
95        }
96        write!(buf, ":")?;
97        self.data.encode(buf)?;
98
99        Ok(())
100    }
101
102    /// Used for decoding a packet received over the air (via KISS or otherwise)
103    pub fn decode_ax25(data: &[u8]) -> Result<Self, DecodeError> {
104        let dest_bytes = data
105            .get(0..7)
106            .ok_or_else(|| DecodeError::InvalidPacket(data.to_owned()))?;
107        let (to, _, has_more) = Callsign::decode_ax25(dest_bytes)
108            .ok_or_else(|| DecodeError::InvalidCallsign(dest_bytes.to_owned()))?;
109
110        if !has_more {
111            return Err(DecodeError::InvalidPacket(data.to_owned()));
112        }
113
114        let src_bytes = data
115            .get(7..14)
116            .ok_or_else(|| DecodeError::InvalidPacket(data.to_owned()))?;
117        let (from, _, mut has_more) = Callsign::decode_ax25(src_bytes)
118            .ok_or_else(|| DecodeError::InvalidCallsign(src_bytes.to_owned()))?;
119
120        let mut i = 14;
121        let mut via = vec![];
122        while has_more {
123            let v_bytes = data
124                .get(i..(i + 7))
125                .ok_or_else(|| DecodeError::InvalidPacket(data.to_owned()))?;
126
127            // vias received over AX.25 are going to be callsigns only
128            // no Q-constructs
129            let (v, heard, more) = Callsign::decode_ax25(v_bytes)
130                .ok_or_else(|| DecodeError::InvalidCallsign(v_bytes.to_owned()))?;
131
132            via.push(Via::Callsign(v, heard));
133            has_more = more;
134            i += 7;
135        }
136
137        // verify control field and protocol id
138        if data.get(i..(i + 2)) != Some(&[0x03, 0xf0]) {
139            return Err(DecodeError::InvalidPacket(data.to_owned()));
140        }
141        i += 2;
142
143        // remainder is the information field
144        let data = AprsData::decode(data.get(i..).unwrap_or(&[]), to)?;
145
146        Ok(Self { data, from, via })
147    }
148
149    /// Used for encoding a packet for transmission on the air (via KISS or otherwise)
150    pub fn encode_ax25<W: Write>(&self, buf: &mut W) -> Result<(), EncodeError> {
151        // Destination address
152        self.data
153            .dest_field()
154            .encode_ax25(buf, CallsignField::Destination, true)?;
155
156        let via_calls: Vec<_> = self.via.iter().filter_map(|v| v.callsign()).collect();
157
158        // Source address
159        let has_more = !via_calls.is_empty();
160        self.from
161            .encode_ax25(buf, CallsignField::Source, has_more)?;
162
163        // Digipeater addresses
164        if let Some(((last_v, last_heard), vs)) = via_calls.split_last() {
165            for (v, heard) in vs {
166                v.encode_ax25(buf, CallsignField::Via(*heard), true)?;
167            }
168
169            last_v.encode_ax25(buf, CallsignField::Via(*last_heard), false)?;
170        }
171
172        // Control field - hardcoded to UI
173        // Protocol ID - hardcoded to no layer 3
174        buf.write_all(&[0x03, 0xf0])?;
175
176        // Information field
177        self.data.encode(buf)?;
178
179        Ok(())
180    }
181}
182
183#[derive(PartialEq, Debug, Clone)]
184pub enum AprsData {
185    Position(AprsPosition),
186    Message(AprsMessage),
187    Status(AprsStatus),
188    MicE(AprsMicE),
189    Unknown(Callsign),
190}
191
192impl AprsData {
193    pub fn to(&self) -> Option<&Callsign> {
194        match self {
195            AprsData::Position(p) => Some(&p.to),
196            AprsData::Message(m) => Some(&m.to),
197            AprsData::Status(s) => Some(&s.to),
198            AprsData::MicE(_) => None,
199            AprsData::Unknown(to) => Some(to),
200        }
201    }
202
203    fn dest_field(&self) -> Cow<Callsign> {
204        match self {
205            AprsData::Position(p) => Cow::Borrowed(&p.to),
206            AprsData::Message(m) => Cow::Borrowed(&m.to),
207            AprsData::Status(s) => Cow::Borrowed(&s.to),
208            AprsData::MicE(m) => Cow::Owned(m.encode_destination()),
209            AprsData::Unknown(to) => Cow::Borrowed(to),
210        }
211    }
212
213    fn decode(s: &[u8], to: Callsign) -> Result<Self, DecodeError> {
214        Ok(match *s.first().unwrap_or(&0) {
215            b':' => AprsData::Message(AprsMessage::decode(&s[1..], to)?),
216            b'!' | b'/' | b'=' | b'@' => AprsData::Position(AprsPosition::decode(s, to)?),
217            b'>' => AprsData::Status(AprsStatus::decode(&s[1..], to)?),
218            0x1c | b'`' => AprsData::MicE(AprsMicE::decode(&s[1..], to, true)?),
219            0x1d | b'\'' => AprsData::MicE(AprsMicE::decode(&s[1..], to, false)?),
220            _ => AprsData::Unknown(to),
221        })
222    }
223
224    fn encode<W: Write>(&self, buf: &mut W) -> Result<(), EncodeError> {
225        match self {
226            Self::Position(p) => {
227                p.encode(buf)?;
228            }
229            Self::Message(m) => {
230                m.encode(buf)?;
231            }
232            Self::Status(st) => {
233                st.encode(buf)?;
234            }
235            Self::MicE(m) => {
236                m.encode(buf)?;
237            }
238            Self::Unknown(_) => return Err(EncodeError::InvalidData),
239        }
240
241        Ok(())
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use mic_e::{Course, Message, Speed};
249    use AprsCst;
250    use Latitude;
251    use Longitude;
252    use Precision;
253    use QConstruct;
254    use Timestamp;
255
256    #[test]
257    fn parse() {
258        let result = AprsPacket::decode_textual(r"ID17F2>APRS,qAS,dl4mea:/074849h4821.61N\01224.49E^322/103/A=003054 !W09! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1".as_bytes()).unwrap();
259        assert_eq!(result.from, Callsign::new_no_ssid("ID17F2"));
260        assert_eq!(result.to(), Some(&Callsign::new_no_ssid("APRS")));
261        assert_eq!(
262            result.via,
263            vec![
264                Via::QConstruct(QConstruct::AS),
265                Via::Callsign(Callsign::new_no_ssid("dl4mea"), false),
266            ]
267        );
268
269        match result.data {
270            AprsData::Position(position) => {
271                assert_eq!(position.timestamp, Some(Timestamp::HHMMSS(7, 48, 49)));
272                assert_eq!(position.latitude.value(), 48.36016666666667);
273                assert_eq!(position.longitude.value(), 12.408166666666666);
274                assert_eq!(
275                    position.comment,
276                    b"322/103/A=003054 !W09! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1"
277                );
278            }
279            _ => panic!("Unexpected data type"),
280        }
281    }
282
283    #[test]
284    fn parse_message() {
285        let result = AprsPacket::decode_textual(
286            &b"IC17F2>Aprs,qAX,dl4mea::DEST     :Hello World! This msg has a : colon {3a2B975"[..],
287        )
288        .unwrap();
289        assert_eq!(result.from, Callsign::new_no_ssid("IC17F2"));
290        assert_eq!(result.to(), Some(&Callsign::new_no_ssid("Aprs")));
291        assert_eq!(
292            result.via,
293            vec![
294                Via::QConstruct(QConstruct::AX),
295                Via::Callsign(Callsign::new_no_ssid("dl4mea"), false),
296            ]
297        );
298
299        match result.data {
300            AprsData::Message(msg) => {
301                assert_eq!(msg.addressee, b"DEST");
302                assert_eq!(msg.text, b"Hello World! This msg has a : colon ");
303                assert_eq!(msg.id, Some(b"3a2B975".to_vec()));
304            }
305            _ => panic!("Unexpected data type"),
306        }
307    }
308
309    #[test]
310    fn parse_status() {
311        let result =
312            AprsPacket::decode_textual(&b"3D17F2>APRS,qAU,dl4mea:>312359zStatus seems okay!"[..])
313                .unwrap();
314        assert_eq!(result.from, Callsign::new_no_ssid("3D17F2"));
315        assert_eq!(result.to(), Some(&Callsign::new_no_ssid("APRS")));
316        assert_eq!(
317            result.via,
318            vec![
319                Via::QConstruct(QConstruct::AU),
320                Via::Callsign(Callsign::new_no_ssid("dl4mea"), false),
321            ]
322        );
323
324        match result.data {
325            AprsData::Status(msg) => {
326                assert_eq!(msg.timestamp(), Some(&Timestamp::DDHHMM(31, 23, 59)));
327                assert_eq!(msg.comment(), b"Status seems okay!");
328            }
329            _ => panic!("Unexpected data type"),
330        }
331    }
332
333    #[test]
334    fn encode_ax25_basic() {
335        let encoded_ax25 = vec![
336            0x82, 0xa0, 0x9c, 0xaa, 0x62, 0x72, 0xe0, 0xac, 0x8a, 0x72, 0x84, 0x86, 0xa2, 0x60,
337            0xac, 0x8a, 0x72, 0x88, 0x8e, 0xa0, 0xe0, 0xac, 0x8a, 0x72, 0x8e, 0x8c, 0x92, 0xe4,
338            0xac, 0x8a, 0x72, 0x8c, 0xa0, 0x8e, 0xe0, 0xae, 0x92, 0x88, 0x8a, 0x66, 0x40, 0x61,
339            0x03, 0xf0, 0x21, 0x34, 0x36, 0x32, 0x37, 0x2e, 0x32, 0x30, 0x4e, 0x53, 0x30, 0x36,
340            0x36, 0x33, 0x31, 0x2e, 0x31, 0x39, 0x57, 0x23, 0x50, 0x48, 0x47, 0x35, 0x34, 0x36,
341            0x30, 0x2f, 0x57, 0x33, 0x20, 0x4d, 0x41, 0x52, 0x43, 0x41, 0x4e, 0x20, 0x55, 0x49,
342            0x44, 0x49, 0x47, 0x49, 0x20, 0x42, 0x4f, 0x49, 0x45, 0x53, 0x54, 0x4f, 0x57, 0x4e,
343            0x2c, 0x20, 0x4e, 0x42,
344        ];
345
346        let encoded_ascii = b"VE9BCQ>APNU19,VE9DGP,VE9GFI-2,VE9FPG*,WIDE3:!4627.20NS06631.19W#PHG5460/W3 MARCAN UIDIGI BOIESTOWN, NB";
347
348        // ascii -> ax25
349        let decoded_from_ascii = AprsPacket::decode_textual(&encoded_ascii[..]).unwrap();
350        let mut actual_ax25 = vec![];
351        decoded_from_ascii.encode_ax25(&mut actual_ax25).unwrap();
352        assert_eq!(encoded_ax25, actual_ax25);
353
354        // ax25 -> ascii
355        let decoded_from_ax25 = AprsPacket::decode_ax25(&encoded_ax25).unwrap();
356        let mut actual_ascii = vec![];
357        decoded_from_ax25.encode_textual(&mut actual_ascii).unwrap();
358        assert_eq!(encoded_ascii[..], actual_ascii);
359
360        // both -> packet
361        assert_eq!(decoded_from_ascii, decoded_from_ax25);
362    }
363
364    #[test]
365    fn parse_packet_mic_e() {
366        let result = AprsPacket::decode_textual(
367            &br#"DF1CHB-9>UQ0RT6,ARISS,APRSAT,WIDE1-1,qAU,DB0KOE-12:`|9g"H?>/>"4z}="#[..],
368        )
369        .unwrap();
370
371        assert_eq!(
372            AprsPacket {
373                from: Callsign::new_with_ssid("DF1CHB", "9"),
374                via: vec![
375                    Via::Callsign(Callsign::new_no_ssid("ARISS"), false),
376                    Via::Callsign(Callsign::new_no_ssid("APRSAT"), false),
377                    Via::Callsign(Callsign::new_with_ssid("WIDE1", "1"), false),
378                    Via::QConstruct(QConstruct::AU),
379                    Via::Callsign(Callsign::new_with_ssid("DB0KOE", "12"), false)
380                ],
381                data: AprsData::MicE(AprsMicE {
382                    latitude: Latitude::new(51.041).unwrap(),
383                    longitude: Longitude::new(6.495833333333334).unwrap(),
384                    precision: Precision::HundredthMinute,
385                    message: Message::M1,
386                    speed: Speed::new(64).unwrap(),
387                    course: Course::new(35).unwrap(),
388                    symbol_table: b'/',
389                    symbol_code: b'>',
390                    comment: br#">"4z}="#.to_vec(),
391                    current: true
392                })
393            },
394            result
395        );
396    }
397
398    #[test]
399    fn encode_edge_case() {
400        let packet = AprsPacket {
401            from: Callsign::new_no_ssid("D9KS3"),
402            via: vec![],
403            data: AprsData::Position(AprsPosition {
404                to: Callsign::new_no_ssid("NOBODY"),
405                timestamp: None,
406                messaging_supported: true,
407                latitude: Latitude::new(33.999999999999).unwrap(),
408                longitude: Longitude::new(33.999999999999).unwrap(),
409                precision: Precision::HundredthMinute,
410                symbol_table: '/',
411                symbol_code: 'c',
412                comment: b"Hello world".to_vec(),
413                cst: AprsCst::Uncompressed,
414            }),
415        };
416
417        let mut buf = vec![];
418        packet.encode_textual(&mut buf).unwrap();
419        assert_eq!(
420            "D9KS3>NOBODY:=3400.00N/03400.00EcHello world",
421            String::from_utf8(buf).unwrap()
422        );
423    }
424
425    #[test]
426    fn encode_with_ssid_0() {
427        let packet = AprsPacket {
428            from: Callsign::new_with_ssid("D9KS3", "0"),
429            via: vec![Via::Callsign(Callsign::new("D9KS7-0").unwrap(), false)],
430            data: AprsData::Position(AprsPosition {
431                to: Callsign::new_with_ssid("NOBODY", "0"),
432                timestamp: None,
433                messaging_supported: true,
434                latitude: Latitude::new(3.95).unwrap(),
435                longitude: Longitude::new(-4.58).unwrap(),
436                precision: Precision::HundredthMinute,
437                symbol_table: '/',
438                symbol_code: 'c',
439                comment: b"Hello world".to_vec(),
440                cst: AprsCst::Uncompressed,
441            }),
442        };
443
444        let mut buf = vec![];
445        packet.encode_textual(&mut buf).unwrap();
446        assert_eq!(
447            "D9KS3>NOBODY,D9KS7:=0357.00N/00434.80WcHello world",
448            String::from_utf8(buf).unwrap()
449        );
450    }
451
452    #[test]
453    fn e2e_serialize_deserialize() {
454        let valids = vec![
455            r"3D17F2>APRS,qAS,DL4MEA:/074849h4821.61N\01224.49E^322/103/A=003054 !W09! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1",
456            r"3D17F2>APRS,qAS,DL4MEA:@074849h4821.61N\01224.49E^322/103/A=003054 !W09! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1",
457            r"ID17F2>APRS,qAS,DL4MEA:!4821.61N\01224.49E^322/103/A=003054 !W09! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1",
458            r"3D17F2>APRS,qAS,DL4MEA:!48  .  N\01200.00E^322/103/A=003054 !W09! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1",
459            r"3D17F2>APRS,qAS,DL4MEA:=4821.61N\01224.49E^322/103/A=003054 !W09! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1",
460            r"ID17F2>APRS,qAS,DL4MEA::DEST     :Hello World! This msg has a : colon {32975",
461            r"IC17F2>APRS,qAS,DL4MEA::DESTINATI:Hello World! This msg has a : colon ",
462            r"ICA7F2>APRS,qAS,DL4MEA:>312359zStatus seems okay!",
463            r"ICA3F2>APRS,qAS,DL4MEA:>184050hAlso with HMS format...",
464            "VE9MP-12>T5RX8P,VE9GFI-2,WIDE1*,WIDE2-1,qAR,VE9QLE-10:`]Q\x1cl|ok/'\"4<}Nick - Monitoring IRG|!\"&7'M|!wTD!|3",
465            r#"DF1CHB-9>UQ0RT6,ARISS,APRSAT,WIDE1-1,qAU,DB0KOE-1:`|9g\"H?>/>\"4z}="#,
466        ];
467
468        for v in valids {
469            let mut buf = vec![];
470            let packet = AprsPacket::decode_textual(v.as_bytes()).unwrap();
471            packet.encode_textual(&mut buf).unwrap();
472            assert_eq!(buf, v.as_bytes(), "\n{}\n{}", buf.escape_ascii(), v);
473        }
474    }
475
476    #[test]
477    fn e2e_serialize_deserialize_ax25() {
478        let originals = vec![
479            r"3D17F2>APRS,qAS,DL4MEA*:/074849h4821.61N\01224.49E^322/103/A=003054 !W09! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1",
480            r"3D17F2>APRS,qAS,DL4MEA:@074849h4821.61N\01224.49E^322/103/A=003054 !W09! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1",
481            r"ID17F2>APRS,qAS,dl4mea:!4821.61N\01224.49E^322/103/A=003054 !W09! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1",
482            r"3D17F2>APRS,qAS,DL4MEA:!48  .  N\01200.00E^322/103/A=003054 !W09! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1",
483            r"3D17F2>APRS,qAS,DL4MEA:=4821.61N\01224.49E^322/103/A=003054 !W09! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1",
484            r"ID17F2>APRS,qAS,DL4MEA::DEST     :Hello World! This msg has a : colon {32975",
485            r"IC17F2>APRS,qAS,DL4MEA::DESTINATI:Hello World! This msg has a : colon ",
486            r"ICA7F2>APRS,qAS,DL4MEA:>312359zStatus seems okay!",
487            r"ICA3F2>APRS,qAS,DL4MEA:>184050hAlso with HMS format...",
488            "VE9MP-12>T5RX8P,VE9GFI-2,WIDE1*,WIDE2-1,qAR,VE9QLE-10:`]Q\x1cl|ok/'\"4<}Nick - Monitoring IRG|!\"&7'M|!wTD!|3",
489            r#"DF1CHB-9>UQ0RT6,ARISS,APRSAT,WIDE1-1,qAU,DB0KOE-1:`|9g\"H?>/>\"4z}="#,
490            // 0 to 8 via callsigns
491            r"ICA3F2>APRS:>184050hAlso with HMS format...",
492            r"ICA3F2>APRS,qAS:>184050hAlso with HMS format...",
493            r"ICA3F2>APRS,ABC,qAS:>184050hAlso with HMS format...",
494            r"ICA3F2>APRS,ABC,DEF,qAS:>184050hAlso with HMS format...",
495            r"ICA3F2>APRS,ABC,DEF,HIJ,qAS:>184050hAlso with HMS format...",
496            r"ICA3F2>APRS,ABC,DEF,HIJ,KLM,qAS:>184050hAlso with HMS format...",
497            r"ICA3F2>APRS,ABC,DEF,NIJ,KLM,QRZ,qAS:>184050hAlso with HMS format...",
498            r"ICA3F2>APRS,ABC,DEF,HIK,ASD,NADL,ASKJ,qAS:>184050hAlso with HMS format...",
499            r"ICA3F2>APRS,ABC,DEF,HIK,ASD,NADL,ASKJ,SDKKA,qAS:>184050hAlso with HMS format...",
500            r"ICA3F2>APRS,ABC,DEF,HIK,ASD,NADL,ASKJ,SDKKA,ABC,qAS:>184050hAlso with HMS format...",
501        ];
502
503        // capitalized and q-codes removed
504        let expected = vec![
505            r"3D17F2>APRS,DL4MEA*:/074849h4821.61N\01224.49E^322/103/A=003054 !W09! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1",
506            r"3D17F2>APRS,DL4MEA:@074849h4821.61N\01224.49E^322/103/A=003054 !W09! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1",
507            r"ID17F2>APRS,DL4MEA:!4821.61N\01224.49E^322/103/A=003054 !W09! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1",
508            r"3D17F2>APRS,DL4MEA:!48  .  N\01200.00E^322/103/A=003054 !W09! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1",
509            r"3D17F2>APRS,DL4MEA:=4821.61N\01224.49E^322/103/A=003054 !W09! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1",
510            r"ID17F2>APRS,DL4MEA::DEST     :Hello World! This msg has a : colon {32975",
511            r"IC17F2>APRS,DL4MEA::DESTINATI:Hello World! This msg has a : colon ",
512            r"ICA7F2>APRS,DL4MEA:>312359zStatus seems okay!",
513            r"ICA3F2>APRS,DL4MEA:>184050hAlso with HMS format...",
514            "VE9MP-12>T5RX8P,VE9GFI-2,WIDE1*,WIDE2-1,VE9QLE-10:`]Q\x1cl|ok/'\"4<}Nick - Monitoring IRG|!\"&7'M|!wTD!|3",
515            r#"DF1CHB-9>UQ0RT6,ARISS,APRSAT,WIDE1-1,DB0KOE-1:`|9g\"H?>/>\"4z}="#,
516            // 0 to 8 via callsigns
517            r"ICA3F2>APRS:>184050hAlso with HMS format...",
518            r"ICA3F2>APRS:>184050hAlso with HMS format...",
519            r"ICA3F2>APRS,ABC:>184050hAlso with HMS format...",
520            r"ICA3F2>APRS,ABC,DEF:>184050hAlso with HMS format...",
521            r"ICA3F2>APRS,ABC,DEF,HIJ:>184050hAlso with HMS format...",
522            r"ICA3F2>APRS,ABC,DEF,HIJ,KLM:>184050hAlso with HMS format...",
523            r"ICA3F2>APRS,ABC,DEF,NIJ,KLM,QRZ:>184050hAlso with HMS format...",
524            r"ICA3F2>APRS,ABC,DEF,HIK,ASD,NADL,ASKJ:>184050hAlso with HMS format...",
525            r"ICA3F2>APRS,ABC,DEF,HIK,ASD,NADL,ASKJ,SDKKA:>184050hAlso with HMS format...",
526            r"ICA3F2>APRS,ABC,DEF,HIK,ASD,NADL,ASKJ,SDKKA,ABC:>184050hAlso with HMS format...",
527        ];
528
529        for (o, e) in originals.iter().zip(expected.iter()) {
530            let o_packet = AprsPacket::decode_textual(o.as_bytes()).unwrap();
531
532            let mut o_ax25 = vec![];
533            o_packet.encode_ax25(&mut o_ax25).unwrap();
534            let o_pkt_from_ax25 = AprsPacket::decode_ax25(&o_ax25).unwrap();
535
536            let mut o_re_encoded = vec![];
537            o_pkt_from_ax25.encode_textual(&mut o_re_encoded).unwrap();
538
539            // text -> packet -> ax25 -> packet -> text
540            assert_eq!(
541                e.as_bytes(),
542                o_re_encoded,
543                "\n{}\n{}",
544                e,
545                String::from_utf8_lossy(&o_re_encoded)
546            );
547
548            // o(text) -> packet -> ax25
549            // VS.
550            // e(text) -> packet -> ax25
551            let e_packet = AprsPacket::decode_textual(e.as_bytes()).unwrap();
552            let mut e_ax25 = vec![];
553            e_packet.encode_ax25(&mut e_ax25).unwrap();
554
555            assert_eq!(e_ax25, o_ax25);
556        }
557    }
558
559    #[test]
560    fn e2e_invalid_string_msg() {
561        let original = b"ICA7F2>Aprs,qAS,dl4mea::DEST     :Hello World! This msg has raw bytes that are invalid utf8! \xc3\x28 {32975";
562
563        let mut buf = vec![];
564        let decoded = AprsPacket::decode_textual(&original[..]).unwrap();
565        decoded.encode_textual(&mut buf).unwrap();
566        assert_eq!(buf, original);
567    }
568}