1use crate::error::AprsError;
4use crate::position::{AprsPosition, parse_compressed_body, parse_uncompressed_body};
5
6#[derive(Debug, Clone, PartialEq)]
15pub struct AprsObject {
16 pub name: String,
18 pub live: bool,
20 pub timestamp: String,
22 pub position: AprsPosition,
24}
25
26#[derive(Debug, Clone, PartialEq)]
31pub struct AprsItem {
32 pub name: String,
34 pub live: bool,
36 pub position: AprsPosition,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Hash)]
45pub enum AprsQuery {
46 Position,
48 Status,
50 Message,
52 DirectionFinding,
54 Weather,
56 Telemetry,
58 Ping,
60 IGate,
62 Heard,
64 Other(String),
67}
68
69pub fn parse_aprs_object(info: &[u8]) -> Result<AprsObject, AprsError> {
77 if info.first() != Some(&b';') {
78 return Err(AprsError::InvalidFormat);
79 }
80 let name_bytes = info.get(1..10).ok_or(AprsError::InvalidFormat)?;
82 let flag = *info.get(10).ok_or(AprsError::InvalidFormat)?;
83 let live = match flag {
84 b'*' => true,
85 b'_' => false,
86 _ => return Err(AprsError::InvalidFormat),
87 };
88
89 let name = String::from_utf8_lossy(name_bytes).trim().to_string();
90
91 let pos_body = info.get(11..).ok_or(AprsError::InvalidFormat)?;
94 let ts_bytes = pos_body.get(..7).ok_or(AprsError::InvalidFormat)?;
95 let timestamp = String::from_utf8_lossy(ts_bytes).to_string();
96 let pos_data = pos_body.get(7..).ok_or(AprsError::InvalidFormat)?;
97
98 let first = *pos_data.first().ok_or(AprsError::InvalidFormat)?;
99 let position = if first.is_ascii_digit() {
100 parse_uncompressed_body(pos_data)?
101 } else {
102 parse_compressed_body(pos_data)?
103 };
104
105 Ok(AprsObject {
106 name,
107 live,
108 timestamp,
109 position,
110 })
111}
112
113pub fn parse_aprs_item(info: &[u8]) -> Result<AprsItem, AprsError> {
125 if info.first() != Some(&b')') {
126 return Err(AprsError::InvalidFormat);
127 }
128 let body = info.get(1..).ok_or(AprsError::InvalidFormat)?;
129
130 let search_len = std::cmp::min(body.len(), 9);
133 let search = body.get(..search_len).ok_or(AprsError::InvalidFormat)?;
134 let terminator_pos = search
135 .iter()
136 .position(|&b| b == b'!' || b == b'_')
137 .ok_or(AprsError::InvalidFormat)?;
138
139 if terminator_pos < 3 {
141 return Err(AprsError::InvalidFormat);
142 }
143
144 let term_byte = *body.get(terminator_pos).ok_or(AprsError::InvalidFormat)?;
145 let live = term_byte == b'!';
146 let name_bytes = body.get(..terminator_pos).ok_or(AprsError::InvalidFormat)?;
147 if name_bytes.iter().any(|&b| !(0x20..=0x7E).contains(&b)) {
149 return Err(AprsError::InvalidFormat);
150 }
151 let name = std::str::from_utf8(name_bytes)
152 .map_err(|_| AprsError::InvalidFormat)?
153 .to_owned();
154 let pos_data = body
155 .get(terminator_pos + 1..)
156 .ok_or(AprsError::InvalidFormat)?;
157
158 let first = *pos_data.first().ok_or(AprsError::InvalidFormat)?;
159 let position = if first.is_ascii_digit() {
160 parse_uncompressed_body(pos_data)?
161 } else {
162 parse_compressed_body(pos_data)?
163 };
164
165 Ok(AprsItem {
166 name,
167 live,
168 position,
169 })
170}
171
172pub fn parse_aprs_query(info: &[u8]) -> Result<AprsQuery, AprsError> {
179 if info.first() != Some(&b'?') {
180 return Err(AprsError::InvalidFormat);
181 }
182
183 let body_bytes = info.get(1..).unwrap_or(&[]);
184 let body = String::from_utf8_lossy(body_bytes);
185 let text = body.trim_end_matches('\r');
186
187 match text {
189 "APRSP" | "APRS?" => Ok(AprsQuery::Position),
190 "APRSS" => Ok(AprsQuery::Status),
191 "APRSM" => Ok(AprsQuery::Message),
192 "APRSD" => Ok(AprsQuery::DirectionFinding),
193 "APRST" | "APRST?" => Ok(AprsQuery::Telemetry),
194 "APRSH" => Ok(AprsQuery::Heard),
195 "WX" => Ok(AprsQuery::Weather),
196 "PING" | "PING?" => Ok(AprsQuery::Ping),
197 "IGATE" | "IGATE?" => Ok(AprsQuery::IGate),
198 _ => Ok(AprsQuery::Other(text.to_owned())),
199 }
200}
201
202#[cfg(test)]
207mod tests {
208 use super::*;
209
210 type TestResult = Result<(), Box<dyn std::error::Error>>;
211
212 #[test]
215 fn parse_object_live() -> TestResult {
216 let info = b";TORNADO *092345z4903.50N/07201.75W-Tornado warning";
217 let obj = parse_aprs_object(info)?;
218 assert_eq!(obj.name, "TORNADO");
219 assert!(obj.live);
220 assert_eq!(obj.timestamp, "092345z");
221 assert!(
222 (obj.position.latitude - 49.058_333).abs() < 0.001,
223 "lat check"
224 );
225 Ok(())
226 }
227
228 #[test]
229 fn parse_object_killed() -> TestResult {
230 let info = b";MARATHON _092345z4903.50N/07201.75W-Event over";
231 let obj = parse_aprs_object(info)?;
232 assert_eq!(obj.name, "MARATHON");
233 assert!(!obj.live);
234 Ok(())
235 }
236
237 #[test]
238 fn parse_object_rejects_bad_live_flag() {
239 let info = b";TORNADO X092345z4903.50N/07201.75W-";
240 assert!(parse_aprs_object(info).is_err(), "bad flag must error");
241 }
242
243 #[test]
246 fn parse_item_live() -> TestResult {
247 let info = b")AID#2!4903.50N/07201.75W-First aid";
248 let item = parse_aprs_item(info)?;
249 assert_eq!(item.name, "AID#2");
250 assert!(item.live);
251 assert!(
252 (item.position.latitude - 49.058_333).abs() < 0.001,
253 "lat check"
254 );
255 Ok(())
256 }
257
258 #[test]
259 fn parse_item_killed() -> TestResult {
260 let info = b")AID#2_4903.50N/07201.75W-Closed";
261 let item = parse_aprs_item(info)?;
262 assert!(!item.live);
263 Ok(())
264 }
265
266 #[test]
267 fn parse_item_short_name_rejected() {
268 let info = b")AB!4903.50N/07201.75W-";
270 assert!(
271 matches!(parse_aprs_item(info), Err(AprsError::InvalidFormat)),
272 "short name must be rejected",
273 );
274 }
275
276 #[test]
279 fn parse_query_position_aprsp() -> TestResult {
280 assert_eq!(parse_aprs_query(b"?APRSP")?, AprsQuery::Position);
281 Ok(())
282 }
283
284 #[test]
285 fn parse_query_position_aprs_question() -> TestResult {
286 assert_eq!(parse_aprs_query(b"?APRS?")?, AprsQuery::Position);
287 Ok(())
288 }
289
290 #[test]
291 fn parse_query_status() -> TestResult {
292 assert_eq!(parse_aprs_query(b"?APRSS")?, AprsQuery::Status);
293 Ok(())
294 }
295
296 #[test]
297 fn parse_query_message() -> TestResult {
298 assert_eq!(parse_aprs_query(b"?APRSM")?, AprsQuery::Message);
299 Ok(())
300 }
301
302 #[test]
303 fn parse_query_direction_finding() -> TestResult {
304 assert_eq!(parse_aprs_query(b"?APRSD")?, AprsQuery::DirectionFinding);
305 Ok(())
306 }
307
308 #[test]
309 fn parse_query_igate() -> TestResult {
310 assert_eq!(parse_aprs_query(b"?IGATE")?, AprsQuery::IGate);
311 Ok(())
312 }
313
314 #[test]
315 fn parse_query_ping() -> TestResult {
316 assert_eq!(parse_aprs_query(b"?PING?")?, AprsQuery::Ping);
317 Ok(())
318 }
319
320 #[test]
321 fn parse_query_weather() -> TestResult {
322 assert_eq!(parse_aprs_query(b"?WX")?, AprsQuery::Weather);
323 Ok(())
324 }
325
326 #[test]
327 fn parse_query_telemetry() -> TestResult {
328 assert_eq!(parse_aprs_query(b"?APRST")?, AprsQuery::Telemetry);
329 Ok(())
330 }
331
332 #[test]
333 fn parse_query_heard() -> TestResult {
334 assert_eq!(parse_aprs_query(b"?APRSH")?, AprsQuery::Heard);
335 Ok(())
336 }
337
338 #[test]
339 fn parse_query_other() -> TestResult {
340 assert_eq!(
341 parse_aprs_query(b"?FOOBAR")?,
342 AprsQuery::Other("FOOBAR".to_owned())
343 );
344 Ok(())
345 }
346
347 #[test]
348 fn parse_query_with_trailing_cr() -> TestResult {
349 assert_eq!(parse_aprs_query(b"?APRSP\r")?, AprsQuery::Position);
350 Ok(())
351 }
352}