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"q") {
90 return Ok(Digipeater::QConstruct(
96 QConstruct::from_bytes(input),
97 Callsign::decode_textual(b"UNKNOWN").unwrap(), ));
99 }
100
101 let (call_bytes, heard) = if input.ends_with(b"*") {
103 (&input[..input.len() - 1], true)
104 } else {
105 (input, false)
106 };
107
108 let callsign = Callsign::decode_textual(call_bytes)
109 .map_err(|_| AprsError::InvalidVia { raw: input.to_vec() })?;
110
111 Ok(Digipeater::Callsign(callsign, heard))
112 }
113
114 pub fn encode_textual(&self, out: &mut Vec<u8>) {
116 match self {
117 Digipeater::Callsign(call, heard) => {
118 call.encode_textual(out);
119 if *heard {
120 out.push(b'*');
121 }
122 }
123 Digipeater::QConstruct(q, gw) => {
124 out.extend_from_slice(q.as_bytes());
125 out.push(b',');
126 gw.encode_textual(out);
127 }
128 }
129 }
130}
131
132impl fmt::Display for Digipeater {
133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134 match self {
135 Digipeater::Callsign(call, heard) => {
136 write!(f, "{call}")?;
137 if *heard {
138 write!(f, "*")?;
139 }
140 Ok(())
141 }
142 Digipeater::QConstruct(q, gw) => write!(f, "{q},{gw}"),
143 }
144 }
145}
146
147pub(crate) fn parse_via(bytes: &[u8]) -> Result<Vec<Digipeater>, AprsError> {
152 if bytes.is_empty() {
153 return Ok(Vec::new());
154 }
155
156 let mut result = Vec::new();
157 let mut iter = bytes.split(|&b| b == b',').peekable();
158
159 while let Some(element) = iter.next() {
160 if element.starts_with(b"q") {
161 let q = QConstruct::from_bytes(element);
162 let gw = if let Some(next) = iter.next() {
164 Callsign::decode_textual(next)
165 .map_err(|_| AprsError::InvalidVia { raw: next.to_vec() })?
166 } else {
167 Callsign::decode_textual(b"UNKNOWN").unwrap()
168 };
169 result.push(Digipeater::QConstruct(q, gw));
170 } else {
171 result.push(Digipeater::decode_textual(element)?);
172 }
173 }
174
175 Ok(result)
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn callsign_no_heard() {
184 let d = Digipeater::decode_textual(b"WIDE2-2").unwrap();
185 assert!(matches!(d, Digipeater::Callsign(_, false)));
186 }
187
188 #[test]
189 fn callsign_heard() {
190 let d = Digipeater::decode_textual(b"RELAY*").unwrap();
191 assert!(matches!(d, Digipeater::Callsign(_, true)));
192 }
193
194 #[test]
195 fn via_list_simple() {
196 let via = parse_via(b"WIDE1-1,WIDE2-2").unwrap();
197 assert_eq!(via.len(), 2);
198 }
199
200 #[test]
201 fn via_list_with_q_construct() {
202 let via = parse_via(b"RELAY*,qAR,KD9ABC").unwrap();
203 assert_eq!(via.len(), 2);
204 assert!(matches!(&via[1], Digipeater::QConstruct(QConstruct::Ar, _)));
205 }
206
207 #[test]
208 fn via_list_empty() {
209 let via = parse_via(b"").unwrap();
210 assert!(via.is_empty());
211 }
212
213 #[test]
214 fn encode_round_trip() {
215 let via = parse_via(b"WIDE1-1,RELAY*").unwrap();
216 let mut out = Vec::new();
217 for (i, d) in via.iter().enumerate() {
218 if i > 0 {
219 out.push(b',');
220 }
221 d.encode_textual(&mut out);
222 }
223 assert_eq!(out, b"WIDE1-1,RELAY*");
224 }
225}