1use crate::callsign::Callsign;
2use crate::error::AprsError;
3use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub enum QConstruct {
11 Ac,
13 Ax,
15 Ao,
17 Ar,
19 As,
21 At,
23 Ai,
25 AoRf,
27 ArRf,
29 Az,
31 Unknown(String),
33}
34
35impl QConstruct {
36 fn from_bytes(bytes: &[u8]) -> Self {
37 match bytes {
38 b"qAC" => QConstruct::Ac,
39 b"qAX" => QConstruct::Ax,
40 b"qAO" => QConstruct::Ao,
41 b"qAR" => QConstruct::Ar,
42 b"qAS" => QConstruct::As,
43 b"qAT" => QConstruct::At,
44 b"qAI" => QConstruct::Ai,
45 b"qAo" => QConstruct::AoRf,
46 b"qAr" => QConstruct::ArRf,
47 b"qAZ" => QConstruct::Az,
48 other => QConstruct::Unknown(String::from_utf8_lossy(other).into_owned()),
49 }
50 }
51
52 fn as_bytes(&self) -> &[u8] {
53 match self {
54 QConstruct::Ac => b"qAC",
55 QConstruct::Ax => b"qAX",
56 QConstruct::Ao => b"qAO",
57 QConstruct::Ar => b"qAR",
58 QConstruct::As => b"qAS",
59 QConstruct::At => b"qAT",
60 QConstruct::Ai => b"qAI",
61 QConstruct::AoRf => b"qAo",
62 QConstruct::ArRf => b"qAr",
63 QConstruct::Az => b"qAZ",
64 QConstruct::Unknown(s) => s.as_bytes(),
65 }
66 }
67}
68
69impl fmt::Display for QConstruct {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 write!(f, "{}", String::from_utf8_lossy(self.as_bytes()))
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Hash)]
77#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
78pub enum Digipeater {
79 Callsign(Callsign, bool),
81 QConstruct(QConstruct, Callsign),
83}
84
85impl Digipeater {
86 pub fn decode_textual(input: &[u8]) -> Result<Self, AprsError> {
88 if input.starts_with(b"qA") {
91 return Ok(Digipeater::QConstruct(
97 QConstruct::from_bytes(input),
98 Callsign::decode_textual(b"UNKNOWN").unwrap(), ));
100 }
101
102 let (call_bytes, heard) = if input.ends_with(b"*") {
104 (&input[..input.len() - 1], true)
105 } else {
106 (input, false)
107 };
108
109 let callsign = Callsign::decode_textual(call_bytes).map_err(|_| AprsError::InvalidVia {
110 raw: input.to_vec(),
111 })?;
112
113 Ok(Digipeater::Callsign(callsign, heard))
114 }
115
116 pub fn encode_textual(&self, out: &mut Vec<u8>) {
118 match self {
119 Digipeater::Callsign(call, heard) => {
120 call.encode_textual(out);
121 if *heard {
122 out.push(b'*');
123 }
124 }
125 Digipeater::QConstruct(q, gw) => {
126 out.extend_from_slice(q.as_bytes());
127 out.push(b',');
128 gw.encode_textual(out);
129 }
130 }
131 }
132}
133
134impl fmt::Display for Digipeater {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 match self {
137 Digipeater::Callsign(call, heard) => {
138 write!(f, "{call}")?;
139 if *heard {
140 write!(f, "*")?;
141 }
142 Ok(())
143 }
144 Digipeater::QConstruct(q, gw) => write!(f, "{q},{gw}"),
145 }
146 }
147}
148
149pub(crate) fn parse_via(bytes: &[u8]) -> Result<Vec<Digipeater>, AprsError> {
154 if bytes.is_empty() {
155 return Ok(Vec::new());
156 }
157
158 let mut result = Vec::new();
159 let mut iter = bytes.split(|&b| b == b',').peekable();
160
161 while let Some(element) = iter.next() {
162 if element.starts_with(b"qA") {
163 let q = QConstruct::from_bytes(element);
164 let gw = if let Some(next) = iter.next() {
166 Callsign::decode_textual(next)
167 .map_err(|_| AprsError::InvalidVia { raw: next.to_vec() })?
168 } else {
169 Callsign::decode_textual(b"UNKNOWN").unwrap()
170 };
171 result.push(Digipeater::QConstruct(q, gw));
172 } else {
173 result.push(Digipeater::decode_textual(element)?);
174 }
175 }
176
177 Ok(result)
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn callsign_no_heard() {
186 let d = Digipeater::decode_textual(b"WIDE2-2").unwrap();
187 assert!(matches!(d, Digipeater::Callsign(_, false)));
188 }
189
190 #[test]
191 fn callsign_heard() {
192 let d = Digipeater::decode_textual(b"RELAY*").unwrap();
193 assert!(matches!(d, Digipeater::Callsign(_, true)));
194 }
195
196 #[test]
197 fn via_list_simple() {
198 let via = parse_via(b"WIDE1-1,WIDE2-2").unwrap();
199 assert_eq!(via.len(), 2);
200 }
201
202 #[test]
203 fn via_list_with_q_construct() {
204 let via = parse_via(b"RELAY*,qAR,KD9ABC").unwrap();
205 assert_eq!(via.len(), 2);
206 assert!(matches!(&via[1], Digipeater::QConstruct(QConstruct::Ar, _)));
207 }
208
209 #[test]
210 fn via_list_empty() {
211 let via = parse_via(b"").unwrap();
212 assert!(via.is_empty());
213 }
214
215 #[test]
216 fn encode_round_trip() {
217 let via = parse_via(b"WIDE1-1,RELAY*").unwrap();
218 let mut out = Vec::new();
219 for (i, d) in via.iter().enumerate() {
220 if i > 0 {
221 out.push(b',');
222 }
223 d.encode_textual(&mut out);
224 }
225 assert_eq!(out, b"WIDE1-1,RELAY*");
226 }
227}