igc/records/
a_record.rs

1use std::fmt;
2
3use crate::util::DisplayOption;
4use crate::util::Manufacturer;
5use crate::util::ParseError;
6
7/// Represents the FVU ID record
8#[derive(Debug, PartialEq, Eq)]
9pub struct ARecord<'a> {
10    pub manufacturer: Manufacturer<'a>,
11    pub unique_id: &'a str,
12    pub id_extension: Option<&'a str>,
13}
14
15impl<'a> ARecord<'a> {
16    pub fn new(
17        manufacturer: Manufacturer<'a>,
18        unique_id: &'a str,
19        id_extension: Option<&'a str>,
20    ) -> ARecord<'a> {
21        ARecord {
22            manufacturer,
23            unique_id,
24            id_extension,
25        }
26    }
27
28    /// Parse an IGC A Record string
29    ///
30    /// ```
31    /// # use igc::records::ARecord;
32    /// # use igc::util::Manufacturer;
33    /// let record = ARecord::parse("ACAMWatFoo").unwrap();
34    /// assert_eq!(record.manufacturer, Manufacturer::CambridgeAeroInstruments);
35    /// assert_eq!(record.unique_id, "Wat");
36    /// assert_eq!(record.id_extension, Some("Foo"));
37    /// ```
38    pub fn parse(line: &'a str) -> Result<Self, ParseError> {
39        assert_eq!(&line[0..1], "A");
40
41        if line.len() < 7 {
42            return Err(ParseError::SyntaxError);
43        }
44
45        // check for old spec format (e.g. `AC00069`)
46        //
47        // all known loggers that use this old format use numeric serial numbers
48        // so we assume that to be a sufficient heuristic to detect this format
49        if line.bytes().skip(2).take(5).all(|b| b.is_ascii_digit()) {
50            let manufacturer_byte = line.as_bytes()[1];
51            if !manufacturer_byte.is_ascii() {
52                return Err(ParseError::NonASCIICharacters);
53            }
54
55            let id_extension = if line.len() > 7 {
56                Some(&line[7..])
57            } else {
58                None
59            };
60
61            let manufacturer = Manufacturer::parse_single_char(manufacturer_byte);
62            return Ok(ARecord::new(manufacturer, &line[2..7], id_extension));
63        }
64
65        // check for old spec format with three-letter manufacturer (e.g. `AFIL01460FLIGHT:1`)
66        //
67        // all known loggers that use this old format use numeric serial numbers
68        // so we assume that to be a sufficient heuristic to detect this format
69        if line.len() >= 9 && line.bytes().skip(4).take(5).all(|b| b.is_ascii_digit()) {
70            if !line.bytes().take(4).all(|b| b.is_ascii()) {
71                return Err(ParseError::NonASCIICharacters);
72            }
73
74            let id_extension = if line.len() > 9 {
75                Some(&line[9..])
76            } else {
77                None
78            };
79
80            let manufacturer = Manufacturer::parse_triple_char(&line[1..4]);
81            return Ok(ARecord::new(manufacturer, &line[4..9], id_extension));
82        }
83
84        if !line.bytes().take(7).all(|b| b.is_ascii()) {
85            return Err(ParseError::NonASCIICharacters);
86        }
87
88        let manufacturer = Manufacturer::parse_triple_char(&line[1..4]);
89        let unique_id = &line[4..7];
90        let id_extension = if line.len() > 7 {
91            Some(&line[7..])
92        } else {
93            None
94        };
95
96        Ok(ARecord::new(manufacturer, unique_id, id_extension))
97    }
98}
99
100impl<'a> fmt::Display for ARecord<'a> {
101    /// Formats this record as it should appear in an IGC file.
102    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
103        write!(
104            f,
105            "A{}{}{}",
106            DisplayOption(self.manufacturer.to_triple_char()),
107            self.unique_id,
108            DisplayOption(self.id_extension)
109        )
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::{ARecord, Manufacturer};
116
117    #[test]
118    fn arecord_parse() {
119        assert_eq!(
120            ARecord::parse("ACAMWatFoo").unwrap(),
121            ARecord::new(Manufacturer::CambridgeAeroInstruments, "Wat", Some("Foo"))
122        );
123
124        assert_eq!(
125            ARecord::parse("ACAMWatFoo").unwrap(),
126            ARecord::new(Manufacturer::CambridgeAeroInstruments, "Wat", Some("Foo"))
127        );
128
129        // from https://skylines.aero/files/th_46eg6ng1.igc
130        assert_eq!(
131            ARecord::parse("AFLA6NG").unwrap(),
132            ARecord::new(Manufacturer::Flarm, "6NG", None)
133        );
134
135        // from http://www.gliding.ch/images/news/lx20/fichiers_igc.htm
136        assert_eq!(
137            ARecord::parse("AC00069").unwrap(),
138            ARecord::new(Manufacturer::CambridgeAeroInstruments, "00069", None)
139        );
140
141        // from LX8000 (see `example.igc`)
142        assert_eq!(
143            ARecord::parse("ALXVK4AFLIGHT:1").unwrap(),
144            ARecord::new(Manufacturer::LxNav, "K4A", Some("FLIGHT:1"))
145        );
146
147        // from https://github.com/XCSoar/XCSoar/blob/v6.8.11/test/data/lxn_to_igc/18BF14K1.igc
148        assert_eq!(
149            ARecord::parse("AFIL01460FLIGHT:1").unwrap(),
150            ARecord::new(Manufacturer::Filser, "01460", Some("FLIGHT:1"))
151        );
152
153        assert_eq!(
154            ARecord::parse("AX00000").unwrap(),
155            ARecord::new(Manufacturer::UnknownSingle(b'X'), "00000", None)
156        );
157
158        assert_eq!(
159            ARecord::parse("AXYZABC:foobar").unwrap(),
160            ARecord::new(Manufacturer::UnknownTriple("XYZ"), "ABC", Some(":foobar"))
161        );
162
163        assert_eq!(
164            ARecord::parse("AWIN000").unwrap(),
165            ARecord::new(Manufacturer::UnknownTriple("WIN"), "000", None)
166        );
167    }
168
169    #[test]
170    fn parse_with_invalid_char_boundary() {
171        assert!(ARecord::parse("A0ꢀ").is_err());
172    }
173
174    #[test]
175    fn arecord_fmt() {
176        assert_eq!(
177            format!(
178                "{}",
179                ARecord::new(Manufacturer::CambridgeAeroInstruments, "Wat", Some("Foo"))
180            ),
181            "ACAMWatFoo"
182        );
183    }
184
185    proptest! {
186        #[test]
187        #[allow(unused_must_use)]
188        fn parse_doesnt_crash(s in "A\\PC*") {
189            ARecord::parse(&s);
190        }
191    }
192}