use crate::callsign::Callsign;
use crate::error::AprsError;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum QConstruct {
Ac,
Ax,
Ao,
Ar,
As,
At,
Ai,
AoRf,
ArRf,
Az,
Unknown(String),
}
impl QConstruct {
fn from_bytes(bytes: &[u8]) -> Self {
match bytes {
b"qAC" => QConstruct::Ac,
b"qAX" => QConstruct::Ax,
b"qAO" => QConstruct::Ao,
b"qAR" => QConstruct::Ar,
b"qAS" => QConstruct::As,
b"qAT" => QConstruct::At,
b"qAI" => QConstruct::Ai,
b"qAo" => QConstruct::AoRf,
b"qAr" => QConstruct::ArRf,
b"qAZ" => QConstruct::Az,
other => QConstruct::Unknown(String::from_utf8_lossy(other).into_owned()),
}
}
fn as_bytes(&self) -> &[u8] {
match self {
QConstruct::Ac => b"qAC",
QConstruct::Ax => b"qAX",
QConstruct::Ao => b"qAO",
QConstruct::Ar => b"qAR",
QConstruct::As => b"qAS",
QConstruct::At => b"qAT",
QConstruct::Ai => b"qAI",
QConstruct::AoRf => b"qAo",
QConstruct::ArRf => b"qAr",
QConstruct::Az => b"qAZ",
QConstruct::Unknown(s) => s.as_bytes(),
}
}
}
impl fmt::Display for QConstruct {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", String::from_utf8_lossy(self.as_bytes()))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Digipeater {
Callsign(Callsign, bool),
QConstruct(QConstruct, Callsign),
}
impl Digipeater {
pub fn decode_textual(input: &[u8]) -> Result<Self, AprsError> {
if input.starts_with(b"q") {
return Ok(Digipeater::QConstruct(
QConstruct::from_bytes(input),
Callsign::decode_textual(b"UNKNOWN").unwrap(), ));
}
let (call_bytes, heard) = if input.ends_with(b"*") {
(&input[..input.len() - 1], true)
} else {
(input, false)
};
let callsign = Callsign::decode_textual(call_bytes)
.map_err(|_| AprsError::InvalidVia { raw: input.to_vec() })?;
Ok(Digipeater::Callsign(callsign, heard))
}
pub fn encode_textual(&self, out: &mut Vec<u8>) {
match self {
Digipeater::Callsign(call, heard) => {
call.encode_textual(out);
if *heard {
out.push(b'*');
}
}
Digipeater::QConstruct(q, gw) => {
out.extend_from_slice(q.as_bytes());
out.push(b',');
gw.encode_textual(out);
}
}
}
}
impl fmt::Display for Digipeater {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Digipeater::Callsign(call, heard) => {
write!(f, "{call}")?;
if *heard {
write!(f, "*")?;
}
Ok(())
}
Digipeater::QConstruct(q, gw) => write!(f, "{q},{gw}"),
}
}
}
pub(crate) fn parse_via(bytes: &[u8]) -> Result<Vec<Digipeater>, AprsError> {
if bytes.is_empty() {
return Ok(Vec::new());
}
let mut result = Vec::new();
let mut iter = bytes.split(|&b| b == b',').peekable();
while let Some(element) = iter.next() {
if element.starts_with(b"q") {
let q = QConstruct::from_bytes(element);
let gw = if let Some(next) = iter.next() {
Callsign::decode_textual(next)
.map_err(|_| AprsError::InvalidVia { raw: next.to_vec() })?
} else {
Callsign::decode_textual(b"UNKNOWN").unwrap()
};
result.push(Digipeater::QConstruct(q, gw));
} else {
result.push(Digipeater::decode_textual(element)?);
}
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn callsign_no_heard() {
let d = Digipeater::decode_textual(b"WIDE2-2").unwrap();
assert!(matches!(d, Digipeater::Callsign(_, false)));
}
#[test]
fn callsign_heard() {
let d = Digipeater::decode_textual(b"RELAY*").unwrap();
assert!(matches!(d, Digipeater::Callsign(_, true)));
}
#[test]
fn via_list_simple() {
let via = parse_via(b"WIDE1-1,WIDE2-2").unwrap();
assert_eq!(via.len(), 2);
}
#[test]
fn via_list_with_q_construct() {
let via = parse_via(b"RELAY*,qAR,KD9ABC").unwrap();
assert_eq!(via.len(), 2);
assert!(matches!(&via[1], Digipeater::QConstruct(QConstruct::Ar, _)));
}
#[test]
fn via_list_empty() {
let via = parse_via(b"").unwrap();
assert!(via.is_empty());
}
#[test]
fn encode_round_trip() {
let via = parse_via(b"WIDE1-1,RELAY*").unwrap();
let mut out = Vec::new();
for (i, d) in via.iter().enumerate() {
if i > 0 {
out.push(b',');
}
d.encode_textual(&mut out);
}
assert_eq!(out, b"WIDE1-1,RELAY*");
}
}