1#[derive(Debug, Clone, PartialEq)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub struct AprsQuery {
11 pub query_type: Vec<u8>,
13 pub footprint: Option<QueryFootprint>,
15 pub trailing: Vec<u8>,
17}
18
19#[derive(Debug, Clone, PartialEq)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22pub struct QueryFootprint {
23 pub latitude: f64,
24 pub longitude: f64,
25 pub radius_km: f32,
26}
27
28impl AprsQuery {
29 pub(crate) fn parse(info: &[u8]) -> Self {
31 let body = info.get(1..).unwrap_or_default();
34
35 let second_q = body.iter().position(|&b| b == b'?');
36 let (query_type, after_type) = match second_q {
37 Some(pos) => (
38 body[..pos].to_vec(),
39 body.get(pos + 1..).unwrap_or_default(),
40 ),
41 None => (body.to_vec(), &b""[..]),
42 };
43
44 let (footprint, trailing) = parse_footprint(after_type);
45
46 Self {
47 query_type,
48 footprint,
49 trailing,
50 }
51 }
52
53 pub fn encode(&self) -> Vec<u8> {
54 let mut out = vec![b'?'];
55 out.extend_from_slice(&self.query_type);
56 out.push(b'?');
57 if let Some(ref fp) = self.footprint {
58 out.extend_from_slice(
59 format!("{},{},{}", fp.latitude, fp.longitude, fp.radius_km).as_bytes(),
60 );
61 }
62 out.extend_from_slice(&self.trailing);
63 out
64 }
65}
66
67fn parse_footprint(b: &[u8]) -> (Option<QueryFootprint>, Vec<u8>) {
68 if b.is_empty() {
69 return (None, vec![]);
70 }
71 let parts: Vec<&[u8]> = b.splitn(4, |&c| c == b',').collect();
72 if parts.len() >= 3 {
73 let lat = parse_f64(parts[0]);
74 let lon = parse_f64(parts[1]);
75 let radius = parse_f32(parts[2]);
76 if let (Some(lat), Some(lon), Some(radius)) = (lat, lon, radius) {
77 let trailing = parts.get(3).map(|p| p.to_vec()).unwrap_or_default();
78 return (
79 Some(QueryFootprint {
80 latitude: lat,
81 longitude: lon,
82 radius_km: radius,
83 }),
84 trailing,
85 );
86 }
87 }
88 (None, b.to_vec())
89}
90
91fn parse_f64(b: &[u8]) -> Option<f64> {
92 std::str::from_utf8(b).ok()?.trim().parse().ok()
93}
94
95fn parse_f32(b: &[u8]) -> Option<f32> {
96 std::str::from_utf8(b).ok()?.trim().parse().ok()
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn simple_aprs_query() {
105 let q = AprsQuery::parse(b"?APRS?");
106 assert_eq!(q.query_type, b"APRS");
107 assert!(q.footprint.is_none());
108 }
109
110 #[test]
111 fn query_with_footprint() {
112 let q = AprsQuery::parse(b"?APRS?49.0,-72.0,10");
113 assert_eq!(q.query_type, b"APRS");
114 let fp = q.footprint.unwrap();
115 assert!((fp.latitude - 49.0).abs() < 0.01);
116 assert!((fp.longitude - -72.0).abs() < 0.01);
117 assert!((fp.radius_km - 10.0).abs() < 0.01);
118 }
119
120 #[test]
121 fn no_second_question_mark() {
122 let q = AprsQuery::parse(b"?APRS");
124 assert_eq!(q.query_type, b"APRS");
125 }
126
127 #[test]
128 fn encode_round_trip() {
129 let raw = b"?IGATE?";
130 let q = AprsQuery::parse(raw);
131 assert_eq!(q.encode().as_slice(), raw.as_slice());
132 }
133}