use super::AprsError;
use super::telemetry::TelemetryDefinition;
pub const ADDRESSEE_MAX: usize = 9;
pub const MESSAGE_ID_MAX: usize = 5;
pub const TEXT_MAX: usize = 67;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Addressee {
field: [u8; ADDRESSEE_MAX],
len: usize,
}
impl Addressee {
pub const fn new(name: &[u8]) -> Result<Self, AprsError> {
if name.is_empty() {
return Err(AprsError::AddresseeEmpty);
}
if name.len() > ADDRESSEE_MAX {
return Err(AprsError::AddresseeTooLong { len: name.len() });
}
let mut field = [b' '; ADDRESSEE_MAX];
let mut i = 0;
while i < name.len() {
let byte = name[i];
if byte <= b' ' || byte > b'~' || byte == b':' {
return Err(AprsError::InvalidAddresseeChar { got: byte });
}
field[i] = byte;
i += 1;
}
Ok(Self {
field,
len: name.len(),
})
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
self.field.get(..self.len.min(ADDRESSEE_MAX)).unwrap_or(&[])
}
#[must_use]
pub const fn padded(&self) -> [u8; ADDRESSEE_MAX] {
self.field
}
const fn decode(field: [u8; ADDRESSEE_MAX]) -> Result<Self, AprsError> {
let mut len = ADDRESSEE_MAX;
while len > 0 && field[len - 1] == b' ' {
len -= 1;
}
if len == 0 {
return Err(AprsError::AddresseeEmpty);
}
let mut i = 0;
while i < len {
let byte = field[i];
if byte <= b' ' || byte > b'~' || byte == b':' {
return Err(AprsError::InvalidAddresseeChar { got: byte });
}
i += 1;
}
Ok(Self { field, len })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MessageContent<'a> {
Text {
text: &'a [u8],
id: Option<&'a [u8]>,
},
Ack {
id: &'a [u8],
},
Reject {
id: &'a [u8],
},
}
impl<'a> MessageContent<'a> {
const fn id_slice(&self) -> Option<&'a [u8]> {
match *self {
Self::Text { id, .. } => id,
Self::Ack { id } | Self::Reject { id } => Some(id),
}
}
#[must_use]
pub fn reply_ack(&self) -> Option<(&'a [u8], &'a [u8])> {
let id = self.id_slice()?;
let brace = id.iter().position(|&b| b == b'}')?;
Some((
id.get(..brace).unwrap_or(&[]),
id.get(brace + 1..).unwrap_or(&[]),
))
}
#[must_use]
pub fn acked_number(&self) -> Option<&'a [u8]> {
let id = match *self {
Self::Ack { id } | Self::Reject { id } => id,
Self::Text { .. } => return None,
};
Some(match id.iter().position(|&b| b == b'}') {
Some(brace) => id.get(..brace).unwrap_or(&[]),
None => id,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Message<'a> {
pub addressee: Addressee,
pub content: MessageContent<'a>,
}
impl<'a> Message<'a> {
const HEADER_LEN: usize = 1 + ADDRESSEE_MAX + 1;
#[must_use]
pub fn telemetry_definition(&self) -> Option<TelemetryDefinition<'a>> {
match self.content {
MessageContent::Text { text, .. } => TelemetryDefinition::parse(text),
MessageContent::Ack { .. } | MessageContent::Reject { .. } => None,
}
}
pub fn parse(info: &'a [u8]) -> Result<Self, AprsError> {
let dti = *info.first().ok_or(AprsError::Truncated {
expected: Self::HEADER_LEN,
got: info.len(),
})?;
if dti != b':' {
return Err(AprsError::InvalidDataType { got: dti });
}
let field: [u8; ADDRESSEE_MAX] = info
.get(1..1 + ADDRESSEE_MAX)
.and_then(|s| s.try_into().ok())
.ok_or(AprsError::Truncated {
expected: Self::HEADER_LEN,
got: info.len(),
})?;
let addressee = Addressee::decode(field)?;
let sep = *info.get(1 + ADDRESSEE_MAX).ok_or(AprsError::Truncated {
expected: Self::HEADER_LEN,
got: info.len(),
})?;
if sep != b':' {
return Err(AprsError::ExpectedByte {
expected: b':',
got: sep,
position: 1 + ADDRESSEE_MAX,
});
}
let body = info.get(Self::HEADER_LEN..).unwrap_or(&[]);
let content = Self::parse_body(body)?;
Ok(Self { addressee, content })
}
fn parse_body(body: &'a [u8]) -> Result<MessageContent<'a>, AprsError> {
let body = match body {
[rest @ .., b'\r' | b'\n'] => rest,
all => all,
};
let body = match body {
[rest @ .., b'\r'] => rest,
all => all,
};
if let Some(id) = body.strip_prefix(b"ack")
&& !id.is_empty()
&& !id.contains(&b'{')
&& check_id(id).is_ok()
{
return Ok(MessageContent::Ack { id });
}
if let Some(id) = body.strip_prefix(b"rej")
&& !id.is_empty()
&& !id.contains(&b'{')
&& check_id(id).is_ok()
{
return Ok(MessageContent::Reject { id });
}
match body.iter().rposition(|&b| b == b'{') {
Some(brace) if check_id(body.get(brace + 1..).unwrap_or(&[])).is_ok() => {
Ok(MessageContent::Text {
text: body.get(..brace).unwrap_or(&[]),
id: body.get(brace + 1..),
})
}
_ => Ok(MessageContent::Text {
text: body,
id: None,
}),
}
}
#[must_use]
pub const fn encoded_len(&self) -> usize {
Self::HEADER_LEN
+ match self.content {
MessageContent::Text { text, id } => {
text.len()
+ match id {
Some(id) => 1 + id.len(),
None => 0,
}
}
MessageContent::Ack { id } | MessageContent::Reject { id } => 3 + id.len(),
}
}
pub fn build(&self, buf: &mut [u8]) -> Result<usize, AprsError> {
let (prefix, text, id): (&[u8], &[u8], Option<&[u8]>) = match self.content {
MessageContent::Text { text, id } => (b"", text, id),
MessageContent::Ack { id } => (b"ack", b"", Some(id)),
MessageContent::Reject { id } => (b"rej", b"", Some(id)),
};
if let Some(id) = id {
check_id(id)?;
}
let needed = self.encoded_len();
let max = buf.len();
let out = buf
.get_mut(..needed)
.ok_or(AprsError::BufferTooSmall { needed, max })?;
let explicit_brace = matches!(self.content, MessageContent::Text { id: Some(_), .. });
let brace: &[u8] = if explicit_brace { b"{" } else { b"" };
let padded = self.addressee.padded();
let bytes = core::iter::once(&b':')
.chain(padded.iter())
.chain(core::iter::once(&b':'))
.chain(prefix.iter())
.chain(text.iter())
.chain(brace.iter())
.chain(id.unwrap_or(&[]).iter());
for (slot, byte) in out.iter_mut().zip(bytes) {
*slot = *byte;
}
Ok(needed)
}
}
const fn check_id(id: &[u8]) -> Result<(), AprsError> {
if id.is_empty() || id.len() > MESSAGE_ID_MAX {
Err(AprsError::MessageIdLengthInvalid { len: id.len() })
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn addressee(name: &[u8]) -> Addressee {
match Addressee::new(name) {
Ok(a) => a,
Err(e) => panic!("{e}"),
}
}
#[test]
fn message_with_id_round_trip() {
let msg = Message {
addressee: addressee(b"N0CALL"),
content: MessageContent::Text {
text: b"Testing",
id: Some(b"003"),
},
};
let mut buf = [0u8; 64];
let len = match msg.build(&mut buf) {
Ok(n) => n,
Err(e) => panic!("{e}"),
};
assert_eq!(&buf[..len], b":N0CALL :Testing{003");
assert_eq!(Message::parse(&buf[..len]), Ok(msg));
}
#[test]
fn message_without_id_round_trip() {
let msg = Message {
addressee: addressee(b"EMAIL-2"),
content: MessageContent::Text {
text: b"hi there",
id: None,
},
};
let mut buf = [0u8; 64];
let len = match msg.build(&mut buf) {
Ok(n) => n,
Err(e) => panic!("{e}"),
};
assert_eq!(&buf[..len], b":EMAIL-2 :hi there");
assert_eq!(Message::parse(&buf[..len]), Ok(msg));
}
#[test]
fn ack_and_rej_round_trip() {
let ack = Message {
addressee: addressee(b"N1CALL-14"),
content: MessageContent::Ack { id: b"003" },
};
let mut buf = [0u8; 32];
let len = match ack.build(&mut buf) {
Ok(n) => n,
Err(e) => panic!("{e}"),
};
assert_eq!(&buf[..len], b":N1CALL-14:ack003");
assert_eq!(Message::parse(&buf[..len]), Ok(ack));
let rej = Message {
addressee: addressee(b"N0CALL"),
content: MessageContent::Reject { id: b"9" },
};
let len = match rej.build(&mut buf) {
Ok(n) => n,
Err(e) => panic!("{e}"),
};
assert_eq!(&buf[..len], b":N0CALL :rej9");
assert_eq!(Message::parse(&buf[..len]), Ok(rej));
}
#[test]
fn ack_like_text_with_brace_is_text() {
let parsed = match Message::parse(b":N0CALL :acknowledge{7") {
Ok(m) => m,
Err(e) => panic!("{e}"),
};
assert_eq!(
parsed.content,
MessageContent::Text {
text: b"acknowledge",
id: Some(b"7"),
}
);
}
fn round_trip(content: MessageContent<'static>, wire: &[u8]) -> MessageContent<'static> {
let msg = Message {
addressee: addressee(b"WA6LDQ"),
content,
};
let mut buf = [0u8; 64];
let len = match msg.build(&mut buf) {
Ok(n) => n,
Err(e) => panic!("{e}"),
};
assert_eq!(&buf[..len], wire, "build is not byte-exact");
assert_eq!(Message::parse(wire), Ok(msg), "parse is not the inverse");
content
}
#[test]
fn reply_ack_splits_the_spec_forms() {
let with_ack = round_trip(
MessageContent::Text {
text: b"Okay",
id: Some(b"Re}1j"),
},
b":WA6LDQ :Okay{Re}1j",
);
assert_eq!(with_ack.reply_ack(), Some((&b"Re"[..], &b"1j"[..])));
assert_eq!(with_ack.acked_number(), None);
let bare = round_trip(
MessageContent::Text {
text: b"Okay",
id: Some(b"Re}"),
},
b":WA6LDQ :Okay{Re}",
);
assert_eq!(bare.reply_ack(), Some((&b"Re"[..], &b""[..])));
let plain = round_trip(
MessageContent::Text {
text: b"Okay",
id: Some(b"003"),
},
b":WA6LDQ :Okay{003",
);
assert_eq!(plain.reply_ack(), None);
let none = round_trip(
MessageContent::Text {
text: b"Okay",
id: None,
},
b":WA6LDQ :Okay",
);
assert_eq!(none.reply_ack(), None);
assert_eq!(none.acked_number(), None);
}
#[test]
fn acked_number_pulls_out_the_mm() {
let ack = round_trip(MessageContent::Ack { id: b"Re}1j" }, b":WA6LDQ :ackRe}1j");
assert_eq!(ack.acked_number(), Some(&b"Re"[..]));
assert_eq!(ack.reply_ack(), Some((&b"Re"[..], &b"1j"[..])));
let old = round_trip(MessageContent::Ack { id: b"003" }, b":WA6LDQ :ack003");
assert_eq!(old.acked_number(), Some(&b"003"[..]));
assert_eq!(old.reply_ack(), None);
let rej = round_trip(
MessageContent::Reject { id: b"Re}1j" },
b":WA6LDQ :rejRe}1j",
);
assert_eq!(rej.acked_number(), Some(&b"Re"[..]));
let rej_old = round_trip(MessageContent::Reject { id: b"9" }, b":WA6LDQ :rej9");
assert_eq!(rej_old.acked_number(), Some(&b"9"[..]));
assert_eq!(rej_old.reply_ack(), None);
}
#[test]
fn reply_ack_degenerate_ids_do_not_panic() {
let solo = round_trip(
MessageContent::Text {
text: b"x",
id: Some(b"}"),
},
b":WA6LDQ :x{}",
);
assert_eq!(solo.reply_ack(), Some((&b""[..], &b""[..])));
let solo_ack = round_trip(MessageContent::Ack { id: b"}" }, b":WA6LDQ :ack}");
assert_eq!(solo_ack.acked_number(), Some(&b""[..]));
let first = round_trip(
MessageContent::Text {
text: b"x",
id: Some(b"}1j"),
},
b":WA6LDQ :x{}1j",
);
assert_eq!(first.reply_ack(), Some((&b""[..], &b"1j"[..])));
let full = round_trip(
MessageContent::Text {
text: b"x",
id: Some(b"abcd}"),
},
b":WA6LDQ :x{abcd}",
);
assert_eq!(full.reply_ack(), Some((&b"abcd"[..], &b""[..])));
let two = round_trip(
MessageContent::Text {
text: b"x",
id: Some(b"a}b}c"),
},
b":WA6LDQ :x{a}b}c",
);
assert_eq!(two.reply_ack(), Some((&b"a"[..], &b"b}c"[..])));
let two_ack = round_trip(MessageContent::Ack { id: b"a}b}c" }, b":WA6LDQ :acka}b}c");
assert_eq!(two_ack.acked_number(), Some(&b"a"[..]));
let utf8 = round_trip(
MessageContent::Text {
text: b"x",
id: Some("\u{e9}}\u{e9}".as_bytes()),
},
b":WA6LDQ :x{\xc3\xa9}\xc3\xa9",
);
assert_eq!(utf8.reply_ack(), Some((&b"\xc3\xa9"[..], &b"\xc3\xa9"[..])));
let raw = round_trip(
MessageContent::Text {
text: b"x",
id: Some(b"\xff\xfe"),
},
b":WA6LDQ :x{\xff\xfe",
);
assert_eq!(raw.reply_ack(), None);
}
#[test]
fn accessors_are_total_on_hand_built_content() {
let empty_text = MessageContent::Text {
text: b"x",
id: Some(b""),
};
assert_eq!(empty_text.reply_ack(), None);
assert_eq!(empty_text.acked_number(), None);
let empty_ack = MessageContent::Ack { id: b"" };
assert_eq!(empty_ack.reply_ack(), None);
assert_eq!(empty_ack.acked_number(), Some(&b""[..]));
let msg = Message {
addressee: addressee(b"WA6LDQ"),
content: empty_ack,
};
let mut buf = [0u8; 64];
assert_eq!(
msg.build(&mut buf),
Err(AprsError::MessageIdLengthInvalid { len: 0 })
);
let long = MessageContent::Ack { id: b"aa}bbbb" };
assert_eq!(long.acked_number(), Some(&b"aa"[..]));
}
#[test]
fn addressee_validation() {
assert_eq!(Addressee::new(b""), Err(AprsError::AddresseeEmpty));
assert_eq!(
Addressee::new(b"ABCDEFGHIJ"),
Err(AprsError::AddresseeTooLong { len: 10 })
);
assert_eq!(
Addressee::new(b"A B"),
Err(AprsError::InvalidAddresseeChar { got: b' ' })
);
assert_eq!(
Addressee::new(b"A:B"),
Err(AprsError::InvalidAddresseeChar { got: b':' })
);
let a = addressee(b"N0CALL-11");
assert_eq!(a.as_bytes(), b"N0CALL-11");
assert_eq!(&a.padded(), b"N0CALL-11");
}
#[test]
fn a_brace_or_ack_that_is_not_an_id_stays_in_the_text() {
type Case = (&'static [u8], &'static [u8], Option<&'static [u8]>);
let cases: [Case; 9] = [
(
b"{EM|v1|ONLINE|0.0000|0.0000|Android",
b"{EM|v1|ONLINE|0.0000|0.0000|Android",
None,
),
(b"ack1/2} I have a LoRa", b"ack1/2} I have a LoRa", None),
(b"cost is {50 or so", b"cost is {50 or so", None),
(b"trailing brace{", b"trailing brace{", None),
(b"hello{12345", b"hello", Some(b"12345")),
(b"hello{123456", b"hello{123456", None),
(b"a{b}c{42", b"a{b}c", Some(b"42")),
(b"ack", b"ack", None),
(b"{", b"{", None),
];
let mut buf = [0u8; 96];
buf[..11].copy_from_slice(b":N0CALL :");
for (body, want_text, want_id) in cases {
let end = 11 + body.len();
buf[11..end].copy_from_slice(body);
let parsed = match Message::parse(&buf[..end]) {
Ok(m) => m,
Err(e) => panic!("{e} rejected a well-formed message"),
};
match parsed.content {
MessageContent::Text { text, id } => {
assert_eq!(text, want_text, "text of {body:?}");
assert_eq!(id, want_id, "id of {body:?}");
}
other => panic!("expected text for {body:?}, got {other:?}"),
}
}
}
#[test]
fn a_valid_ack_or_rej_is_unaffected() {
let mut buf = [0u8; 32];
buf[..11].copy_from_slice(b":N0CALL :");
for (body, want) in [
(&b"ack01"[..], MessageContent::Ack { id: b"01" }),
(&b"rej42"[..], MessageContent::Reject { id: b"42" }),
(&b"ackAB12C"[..], MessageContent::Ack { id: b"AB12C" }),
] {
let end = 11 + body.len();
buf[11..end].copy_from_slice(body);
let parsed = Message::parse(&buf[..end]).expect("a valid reply");
assert_eq!(parsed.content, want, "{body:?}");
let mut out = [0u8; 32];
let len = parsed.build(&mut out).expect("rebuild");
assert_eq!(&out[..len], &buf[..end], "{body:?}");
}
}
#[test]
fn parse_rejections() {
assert_eq!(
Message::parse(b":N0CALL"),
Err(AprsError::Truncated {
expected: 11,
got: 7
})
);
assert_eq!(
Message::parse(b">status"),
Err(AprsError::InvalidDataType { got: b'>' })
);
assert_eq!(
Message::parse(b":N0CALL hello"),
Err(AprsError::ExpectedByte {
expected: b':',
got: b' ',
position: 10
})
);
assert_eq!(
Message::parse(b": :text"),
Err(AprsError::AddresseeEmpty)
);
assert_eq!(
Message::parse(b":N0CALL :ack"),
Ok(Message {
addressee: addressee(b"N0CALL"),
content: MessageContent::Text {
text: b"ack",
id: None
},
})
);
}
#[test]
fn build_overflow_and_bad_id() {
let msg = Message {
addressee: addressee(b"N0CALL"),
content: MessageContent::Text {
text: b"Testing",
id: Some(b"003"),
},
};
let mut small = [0u8; 12];
assert_eq!(
msg.build(&mut small),
Err(AprsError::BufferTooSmall {
needed: 22,
max: 12
})
);
let bad = Message {
addressee: addressee(b"N0CALL"),
content: MessageContent::Ack { id: b"123456" },
};
let mut buf = [0u8; 64];
assert_eq!(
bad.build(&mut buf),
Err(AprsError::MessageIdLengthInvalid { len: 6 })
);
}
}