#![forbid(unsafe_code)]
mod diagnostic;
mod transport;
#[cfg(feature = "serde")]
pub mod serde_support;
pub use transport::{
oversized_input_error, read_all_with_limit, LineTransport, PacketSink, PacketSource,
TransportErrorCode, DEFAULT_TRANSPORT_READ_LIMIT,
};
pub const MAX_PACKET_LEN: usize = 512;
pub const DEFAULT_PARSE_OPTIONS: ParseOptions = ParseOptions {
max_packet_len: MAX_PACKET_LEN,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ParseOptions {
pub max_packet_len: usize,
}
impl ParseOptions {
#[must_use]
pub const fn new(max_packet_len: usize) -> Self {
Self { max_packet_len }
}
}
impl Default for ParseOptions {
fn default() -> Self {
DEFAULT_PARSE_OPTIONS
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RawPacket {
bytes: Vec<u8>,
}
impl RawPacket {
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParsedPacket {
raw: RawPacket,
source_end: usize,
path_start: usize,
path_end: usize,
path_components: Vec<(usize, usize)>,
payload_start: usize,
}
impl ParsedPacket {
#[must_use]
pub fn raw(&self) -> &RawPacket {
&self.raw
}
#[must_use]
pub fn source(&self) -> &[u8] {
&self.raw.bytes[..self.source_end]
}
#[must_use]
pub fn path(&self) -> &[u8] {
&self.raw.bytes[self.path_start..self.path_end]
}
#[must_use]
pub fn destination(&self) -> &[u8] {
let (start, end) = self.path_components[0];
&self.raw.bytes[start..end]
}
#[must_use]
pub fn digipeaters(&self) -> Vec<&[u8]> {
self.path_components[1..]
.iter()
.map(|(start, end)| &self.raw.bytes[*start..*end])
.collect()
}
#[must_use]
pub fn path_components(&self) -> Vec<&[u8]> {
self.path_components
.iter()
.map(|(start, end)| &self.raw.bytes[*start..*end])
.collect()
}
#[must_use]
pub fn payload(&self) -> &[u8] {
&self.raw.bytes[self.payload_start..]
}
#[must_use]
pub fn data_type_identifier(&self) -> DataTypeIdentifier {
DataTypeIdentifier::from_byte(self.raw.bytes[self.payload_start])
}
#[must_use]
pub fn information(&self) -> &[u8] {
&self.raw.bytes[self.payload_start + 1..]
}
#[must_use]
pub fn aprs_data(&self) -> AprsData<'_> {
parse_aprs_data(
self.data_type_identifier(),
self.information(),
self.destination(),
)
}
#[must_use]
pub fn summary(&self) -> PacketSummary<'_> {
PacketSummary::from_packet(self)
}
#[must_use]
pub fn to_json(&self) -> String {
diagnostic::packet_to_json(self)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PacketSummary<'a> {
pub source: &'a [u8],
pub destination: &'a [u8],
pub data_type: &'static str,
pub semantic: &'static str,
pub coordinates: Option<Coordinates>,
pub nmea_checksum: Option<NmeaChecksum>,
pub telemetry_sequence: Option<u16>,
pub mic_e_speed_course: Option<MicESpeedCourse>,
}
impl<'a> PacketSummary<'a> {
fn from_packet(packet: &'a ParsedPacket) -> Self {
let data = packet.aprs_data();
Self {
source: packet.source(),
destination: packet.destination(),
data_type: packet.data_type_identifier().name(),
semantic: data.kind_name(),
coordinates: summary_coordinates(data),
nmea_checksum: summary_nmea_checksum(data),
telemetry_sequence: summary_telemetry_sequence(data),
mic_e_speed_course: summary_mic_e_speed_course(data),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Engine {
policy: Policy,
counters: Counters,
}
impl Engine {
#[must_use]
pub fn new(policy: Policy) -> Self {
Self {
policy,
counters: Counters::default(),
}
}
pub fn process(&mut self, input: &[u8]) -> EngineResult {
match parse_packet(input) {
Ok(packet) => {
let semantic = packet.aprs_data();
match self.policy.evaluate(&packet, &semantic) {
PolicyDecision::Accept => {
self.counters.accepted = self.counters.accepted.saturating_add(1);
EngineResult::Accepted { packet }
}
PolicyDecision::Reject(reason) => {
self.counters.rejected = self.counters.rejected.saturating_add(1);
EngineResult::Rejected { packet, reason }
}
}
}
Err(error) => {
self.counters.malformed = self.counters.malformed.saturating_add(1);
EngineResult::ParseError(error)
}
}
}
pub fn process_packets<I, P>(&mut self, packets: I) -> Vec<EngineResult>
where
I: IntoIterator<Item = P>,
P: AsRef<[u8]>,
{
packets
.into_iter()
.map(|packet| self.process(packet.as_ref()))
.collect()
}
pub fn process_source<S>(&mut self, source: &mut S) -> Result<Vec<EngineResult>, S::Error>
where
S: PacketSource,
{
Ok(self.process_packets(source.recv_packets()?))
}
#[must_use]
pub fn counters(&self) -> Counters {
self.counters
}
}
impl Default for Engine {
fn default() -> Self {
Self::new(Policy::default())
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum EngineResult {
Accepted {
packet: ParsedPacket,
},
Rejected {
packet: ParsedPacket,
reason: PolicyRejection,
},
ParseError(ParseError),
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Counters {
pub accepted: u64,
pub rejected: u64,
pub malformed: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Policy {
pub allow_unsupported: bool,
pub allow_malformed_semantics: bool,
pub max_path_components: usize,
}
impl Policy {
#[must_use]
pub fn strict() -> Self {
Self::default()
}
#[must_use]
pub fn permissive() -> Self {
Self {
allow_unsupported: true,
allow_malformed_semantics: true,
max_path_components: 9,
}
}
#[must_use]
pub fn evaluate(&self, packet: &ParsedPacket, semantic: &AprsData<'_>) -> PolicyDecision {
if packet.path_components.len() > self.max_path_components {
return PolicyDecision::Reject(PolicyRejection::PathTooLong);
}
match semantic {
AprsData::Malformed { .. } if !self.allow_malformed_semantics => {
PolicyDecision::Reject(PolicyRejection::MalformedSemantics)
}
AprsData::Unsupported { .. } if !self.allow_unsupported => {
PolicyDecision::Reject(PolicyRejection::UnsupportedSemantics)
}
_ => PolicyDecision::Accept,
}
}
}
impl Default for Policy {
fn default() -> Self {
Self {
allow_unsupported: false,
allow_malformed_semantics: false,
max_path_components: 9,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PolicyDecision {
Accept,
Reject(PolicyRejection),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PolicyRejection {
PathTooLong,
MalformedSemantics,
UnsupportedSemantics,
}
impl PolicyRejection {
#[must_use]
pub fn code(self) -> &'static str {
match self {
Self::PathTooLong => "policy.path_too_long",
Self::MalformedSemantics => "policy.malformed_semantics",
Self::UnsupportedSemantics => "policy.unsupported_semantics",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AprsData<'a> {
Status {
text: &'a [u8],
},
Position(Position<'a>),
TimestampedPosition(TimestampedPosition<'a>),
CompressedPosition(CompressedPosition<'a>),
Message(Message<'a>),
Object(Object<'a>),
Item(Item<'a>),
Weather(Weather<'a>),
Telemetry(Telemetry<'a>),
TelemetryMetadata(TelemetryMetadata<'a>),
Query(Query<'a>),
Capability(Capability<'a>),
Nmea(Nmea<'a>),
MicE(MicE<'a>),
Maidenhead(Maidenhead<'a>),
UserDefined(UserDefined<'a>),
ThirdParty(ThirdParty<'a>),
Unsupported {
identifier: u8,
information: &'a [u8],
},
Malformed {
identifier: u8,
information: &'a [u8],
},
}
impl AprsData<'_> {
#[must_use]
pub fn kind_name(&self) -> &'static str {
match self {
Self::Status { .. } => "status",
Self::Position(_) => "position",
Self::TimestampedPosition(_) => "timestamped_position",
Self::CompressedPosition(_) => "compressed_position",
Self::Message(_) => "message",
Self::Object(_) => "object",
Self::Item(_) => "item",
Self::Weather(_) => "weather",
Self::Telemetry(_) => "telemetry",
Self::TelemetryMetadata(_) => "telemetry_metadata",
Self::Query(_) => "query",
Self::Capability(_) => "capability",
Self::Nmea(_) => "nmea",
Self::MicE(_) => "mic_e",
Self::Maidenhead(_) => "maidenhead",
Self::UserDefined(_) => "user_defined",
Self::ThirdParty(_) => "third_party",
Self::Unsupported { .. } => "unsupported",
Self::Malformed { .. } => "malformed",
}
}
}
fn summary_coordinates(data: AprsData<'_>) -> Option<Coordinates> {
match data {
AprsData::Position(position) => position.coordinates(),
AprsData::TimestampedPosition(position) => position.position.coordinates(),
AprsData::CompressedPosition(position) => position.coordinates(),
AprsData::MicE(mic_e) => mic_e.coordinates(),
_ => None,
}
}
fn summary_nmea_checksum(data: AprsData<'_>) -> Option<NmeaChecksum> {
match data {
AprsData::Nmea(nmea) => nmea.checksum(),
_ => None,
}
}
fn summary_telemetry_sequence(data: AprsData<'_>) -> Option<u16> {
match data {
AprsData::Telemetry(telemetry) => telemetry.sequence_number(),
_ => None,
}
}
fn summary_mic_e_speed_course(data: AprsData<'_>) -> Option<MicESpeedCourse> {
match data {
AprsData::MicE(mic_e) => mic_e.speed_course(),
_ => None,
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Position<'a> {
pub messaging: bool,
pub latitude: &'a [u8],
pub symbol_table: u8,
pub longitude: &'a [u8],
pub symbol_code: u8,
pub comment: &'a [u8],
}
impl Position<'_> {
#[must_use]
pub fn coordinates(&self) -> Option<Coordinates> {
Some(Coordinates {
latitude: decode_latitude(self.latitude)?,
longitude: decode_longitude(self.longitude)?,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Coordinates {
pub latitude: f64,
pub longitude: f64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TimestampedPosition<'a> {
pub messaging: bool,
pub timestamp: &'a [u8],
pub position: Position<'a>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CompressedPosition<'a> {
pub messaging: bool,
pub symbol_table: u8,
pub compressed_latitude: &'a [u8],
pub compressed_longitude: &'a [u8],
pub symbol_code: u8,
pub extension: &'a [u8],
pub compression_type: u8,
pub comment: &'a [u8],
}
impl CompressedPosition<'_> {
#[must_use]
pub fn coordinates(&self) -> Option<Coordinates> {
let y = decode_base91(self.compressed_latitude)?;
let x = decode_base91(self.compressed_longitude)?;
Some(Coordinates {
latitude: 90.0 - (y as f64 / 380_926.0),
longitude: -180.0 + (x as f64 / 190_463.0),
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Message<'a> {
pub addressee: &'a [u8],
pub kind: MessageKind,
pub text: &'a [u8],
pub id: Option<&'a [u8]>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MessageKind {
Message,
Ack,
Reject,
Bulletin,
Announcement,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Object<'a> {
pub name: &'a [u8],
pub live: bool,
pub timestamp: &'a [u8],
pub body: &'a [u8],
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Item<'a> {
pub name: &'a [u8],
pub live: bool,
pub body: &'a [u8],
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Weather<'a> {
pub report: &'a [u8],
}
impl Weather<'_> {
#[must_use]
pub fn fields(&self) -> WeatherFields<'_> {
WeatherFields {
timestamp: self
.report
.get(..6)
.filter(|value| value.iter().all(u8::is_ascii_digit)),
wind_direction_degrees: parse_tagged_u16(self.report, b'c', 3),
wind_speed_mph: parse_tagged_u16(self.report, b's', 3),
wind_gust_mph: parse_tagged_u16(self.report, b'g', 3),
temperature_fahrenheit: parse_tagged_i16(self.report, b't', 3),
rain_last_hour_hundredths_inch: parse_tagged_u16(self.report, b'r', 3),
rain_last_24_hours_hundredths_inch: parse_tagged_u16(self.report, b'p', 3),
rain_since_midnight_hundredths_inch: parse_tagged_u16(self.report, b'P', 3),
humidity_percent: parse_tagged_u16(self.report, b'h', 2).map(|value| {
if value == 0 {
100
} else {
value
}
}),
pressure_tenths_hpa: parse_tagged_u16(self.report, b'b', 5),
luminosity_watts_per_square_meter: parse_tagged_u16(self.report, b'L', 3),
luminosity_1000_plus_watts_per_square_meter: parse_tagged_u16(self.report, b'l', 3)
.map(|value| value + 1000),
snow_last_24_hours_inches: parse_tagged_u16(self.report, b'S', 3),
raw_rain_counter: parse_tagged_u16(self.report, b'#', 3),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WeatherFields<'a> {
pub timestamp: Option<&'a [u8]>,
pub wind_direction_degrees: Option<u16>,
pub wind_speed_mph: Option<u16>,
pub wind_gust_mph: Option<u16>,
pub temperature_fahrenheit: Option<i16>,
pub rain_last_hour_hundredths_inch: Option<u16>,
pub rain_last_24_hours_hundredths_inch: Option<u16>,
pub rain_since_midnight_hundredths_inch: Option<u16>,
pub humidity_percent: Option<u16>,
pub pressure_tenths_hpa: Option<u16>,
pub luminosity_watts_per_square_meter: Option<u16>,
pub luminosity_1000_plus_watts_per_square_meter: Option<u16>,
pub snow_last_24_hours_inches: Option<u16>,
pub raw_rain_counter: Option<u16>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Telemetry<'a> {
pub sequence: &'a [u8],
pub analog: [&'a [u8]; 5],
pub digital: Option<&'a [u8]>,
}
impl Telemetry<'_> {
#[must_use]
pub fn sequence_number(&self) -> Option<u16> {
parse_u16(self.sequence)
}
#[must_use]
pub fn analog_values(&self) -> Option<[u16; 5]> {
Some([
parse_u16(self.analog[0])?,
parse_u16(self.analog[1])?,
parse_u16(self.analog[2])?,
parse_u16(self.analog[3])?,
parse_u16(self.analog[4])?,
])
}
#[must_use]
pub fn digital_bits(&self) -> Option<[bool; 8]> {
let digital = self.digital?;
if digital.len() != 8 {
return None;
}
let mut bits = [false; 8];
for (index, byte) in digital.iter().enumerate() {
bits[index] = match byte {
b'0' => false,
b'1' => true,
_ => return None,
};
}
Some(bits)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TelemetryMetadata<'a> {
pub addressee: &'a [u8],
pub kind: TelemetryMetadataKind,
pub body: &'a [u8],
}
impl<'a> TelemetryMetadata<'a> {
#[must_use]
pub fn fields(&self) -> Vec<&'a [u8]> {
self.body.split(|byte| *byte == b',').collect()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TelemetryMetadataKind {
ParameterNames,
Units,
Equations,
BitSense,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Query<'a> {
pub query: &'a [u8],
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Capability<'a> {
pub body: &'a [u8],
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Nmea<'a> {
pub sentence: &'a [u8],
}
impl Nmea<'_> {
#[must_use]
pub fn checksum(&self) -> Option<NmeaChecksum> {
let separator = self.sentence.iter().rposition(|byte| *byte == b'*')?;
let checksum = self.sentence.get(separator + 1..separator + 3)?;
if checksum.len() != 2 || self.sentence.get(separator + 3).is_some() {
return None;
}
let expected = parse_hex_byte(checksum)?;
let calculated = self.sentence[..separator]
.iter()
.fold(0u8, |accumulator, byte| accumulator ^ byte);
Some(NmeaChecksum {
expected,
calculated,
valid: expected == calculated,
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct NmeaChecksum {
pub expected: u8,
pub calculated: u8,
pub valid: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MicE<'a> {
pub identifier: u8,
pub destination: &'a [u8],
pub body: &'a [u8],
pub status: Option<MicEStatus>,
pub latitude_digits: Option<[u8; 6]>,
}
impl MicE<'_> {
#[must_use]
pub fn coordinates(&self) -> Option<Coordinates> {
Some(Coordinates {
latitude: decode_mic_e_latitude(self.destination)?,
longitude: decode_mic_e_longitude(self.destination, self.body)?,
})
}
#[must_use]
pub fn speed_course(&self) -> Option<MicESpeedCourse> {
decode_mic_e_speed_course(self.body)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MicEStatus {
Custom([bool; 3]),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MicESpeedCourse {
pub speed_knots: u16,
pub course_degrees: u16,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Maidenhead<'a> {
pub locator: &'a [u8],
pub comment: &'a [u8],
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct UserDefined<'a> {
pub user_id: u8,
pub packet_type: u8,
pub body: &'a [u8],
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ThirdParty<'a> {
pub body: &'a [u8],
}
impl ThirdParty<'_> {
pub fn nested_packet(&self) -> Result<ParsedPacket, ParseError> {
parse_packet(self.body)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DataTypeIdentifier {
PositionNoTimestamp,
PositionNoTimestampMessaging,
PositionWithTimestamp,
PositionWithTimestampMessaging,
Status,
Query,
Capability,
Message,
Object,
Item,
Weather,
Telemetry,
Nmea,
MicECurrent,
MicEOld,
Maidenhead,
UserDefined,
ThirdParty,
Unknown(u8),
}
impl DataTypeIdentifier {
fn from_byte(byte: u8) -> Self {
match byte {
b'!' => Self::PositionNoTimestamp,
b'=' => Self::PositionNoTimestampMessaging,
b'/' => Self::PositionWithTimestamp,
b'@' => Self::PositionWithTimestampMessaging,
b'>' => Self::Status,
b'?' => Self::Query,
b'<' => Self::Capability,
b':' => Self::Message,
b';' => Self::Object,
b')' => Self::Item,
b'_' => Self::Weather,
b'T' => Self::Telemetry,
b'$' => Self::Nmea,
b'`' => Self::MicECurrent,
b'\'' => Self::MicEOld,
b'[' => Self::Maidenhead,
b'{' => Self::UserDefined,
b'}' => Self::ThirdParty,
other => Self::Unknown(other),
}
}
fn as_byte(self) -> u8 {
match self {
Self::PositionNoTimestamp => b'!',
Self::PositionNoTimestampMessaging => b'=',
Self::PositionWithTimestamp => b'/',
Self::PositionWithTimestampMessaging => b'@',
Self::Status => b'>',
Self::Query => b'?',
Self::Capability => b'<',
Self::Message => b':',
Self::Object => b';',
Self::Item => b')',
Self::Weather => b'_',
Self::Telemetry => b'T',
Self::Nmea => b'$',
Self::MicECurrent => b'`',
Self::MicEOld => b'\'',
Self::Maidenhead => b'[',
Self::UserDefined => b'{',
Self::ThirdParty => b'}',
Self::Unknown(value) => value,
}
}
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::PositionNoTimestamp => "position_no_timestamp",
Self::PositionNoTimestampMessaging => "position_no_timestamp_messaging",
Self::PositionWithTimestamp => "position_with_timestamp",
Self::PositionWithTimestampMessaging => "position_with_timestamp_messaging",
Self::Status => "status",
Self::Query => "query",
Self::Capability => "capability",
Self::Message => "message",
Self::Object => "object",
Self::Item => "item",
Self::Weather => "weather",
Self::Telemetry => "telemetry",
Self::Nmea => "nmea",
Self::MicECurrent => "mic_e_current",
Self::MicEOld => "mic_e_old",
Self::Maidenhead => "maidenhead",
Self::UserDefined => "user_defined",
Self::ThirdParty => "third_party",
Self::Unknown(_) => "unknown",
}
}
}
fn parse_aprs_data<'a>(
identifier: DataTypeIdentifier,
information: &'a [u8],
destination: &'a [u8],
) -> AprsData<'a> {
match identifier {
DataTypeIdentifier::Status => AprsData::Status { text: information },
DataTypeIdentifier::PositionNoTimestamp => parse_position(false, b'!', information),
DataTypeIdentifier::PositionNoTimestampMessaging => parse_position(true, b'=', information),
DataTypeIdentifier::PositionWithTimestamp => {
parse_timestamped_position(false, b'/', information)
}
DataTypeIdentifier::PositionWithTimestampMessaging => {
parse_timestamped_position(true, b'@', information)
}
DataTypeIdentifier::Message => parse_message(information),
DataTypeIdentifier::Object => parse_object(information),
DataTypeIdentifier::Item => parse_item(information),
DataTypeIdentifier::Weather => AprsData::Weather(Weather {
report: information,
}),
DataTypeIdentifier::Telemetry => parse_telemetry(information),
DataTypeIdentifier::Query => AprsData::Query(Query { query: information }),
DataTypeIdentifier::Capability => AprsData::Capability(Capability { body: information }),
DataTypeIdentifier::Nmea => AprsData::Nmea(Nmea {
sentence: information,
}),
DataTypeIdentifier::MicECurrent | DataTypeIdentifier::MicEOld => {
parse_mic_e(identifier, information, destination)
}
DataTypeIdentifier::Maidenhead => parse_maidenhead(information),
DataTypeIdentifier::UserDefined => parse_user_defined(information),
DataTypeIdentifier::ThirdParty => AprsData::ThirdParty(ThirdParty { body: information }),
other => AprsData::Unsupported {
identifier: other.as_byte(),
information,
},
}
}
fn parse_mic_e<'a>(
identifier: DataTypeIdentifier,
information: &'a [u8],
destination: &'a [u8],
) -> AprsData<'a> {
AprsData::MicE(MicE {
identifier: identifier.as_byte(),
destination,
body: information,
status: decode_mic_e_status(destination),
latitude_digits: decode_mic_e_latitude_digits(destination),
})
}
fn parse_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
if is_compressed_position(information) {
return parse_compressed_position(messaging, identifier, information);
}
if information.len() < 18 {
return AprsData::Malformed {
identifier,
information,
};
}
let latitude = &information[..8];
let symbol_table = information[8];
let longitude = &information[9..18];
let symbol_code = information[18];
let comment = &information[19..];
if !is_latitude(latitude)
|| !is_symbol_table_identifier(symbol_table)
|| !is_longitude(longitude)
|| !is_printable_ascii(symbol_code)
{
return AprsData::Malformed {
identifier,
information,
};
}
AprsData::Position(Position {
messaging,
latitude,
symbol_table,
longitude,
symbol_code,
comment,
})
}
fn parse_timestamped_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
if information.len() < 8 {
return AprsData::Malformed {
identifier,
information,
};
}
let timestamp = &information[..7];
if !is_timestamp(timestamp) {
return AprsData::Malformed {
identifier,
information,
};
}
match parse_position(messaging, identifier, &information[7..]) {
AprsData::Position(position) => AprsData::TimestampedPosition(TimestampedPosition {
messaging,
timestamp,
position,
}),
AprsData::CompressedPosition(position) => AprsData::CompressedPosition(position),
_ => AprsData::Malformed {
identifier,
information,
},
}
}
fn parse_compressed_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
if information.len() < 13 {
return AprsData::Malformed {
identifier,
information,
};
}
let symbol_table = information[0];
let compressed_latitude = &information[1..5];
let compressed_longitude = &information[5..9];
let symbol_code = information[9];
let extension = &information[10..12];
let compression_type = information[12];
let comment = &information[13..];
if !is_symbol_table_identifier(symbol_table)
|| !compressed_latitude.iter().all(|byte| is_base91(*byte))
|| !compressed_longitude.iter().all(|byte| is_base91(*byte))
|| !is_printable_ascii(symbol_code)
|| !extension.iter().all(|byte| is_base91(*byte))
|| !is_base91(compression_type)
{
return AprsData::Malformed {
identifier,
information,
};
}
AprsData::CompressedPosition(CompressedPosition {
messaging,
symbol_table,
compressed_latitude,
compressed_longitude,
symbol_code,
extension,
compression_type,
comment,
})
}
fn parse_object(information: &[u8]) -> AprsData<'_> {
if information.len() < 17 || !matches!(information[9], b'*' | b'_') {
return AprsData::Malformed {
identifier: b';',
information,
};
}
AprsData::Object(Object {
name: &information[..9],
live: information[9] == b'*',
timestamp: &information[10..17],
body: &information[17..],
})
}
fn parse_item(information: &[u8]) -> AprsData<'_> {
let Some(separator) = information
.iter()
.position(|byte| matches!(*byte, b'!' | b'_'))
else {
return AprsData::Malformed {
identifier: b')',
information,
};
};
if separator == 0 || separator > 9 {
return AprsData::Malformed {
identifier: b')',
information,
};
}
AprsData::Item(Item {
name: &information[..separator],
live: information[separator] == b'!',
body: &information[separator + 1..],
})
}
fn parse_message(information: &[u8]) -> AprsData<'_> {
if information.len() < 10 || information[9] != b':' {
return AprsData::Malformed {
identifier: b':',
information,
};
}
let addressee = &information[..9];
let body = &information[10..];
if let Some(kind) = classify_telemetry_metadata_kind(addressee) {
return AprsData::TelemetryMetadata(TelemetryMetadata {
addressee,
kind,
body,
});
}
let (text, id) = match body.iter().position(|byte| *byte == b'{') {
Some(separator) => (&body[..separator], Some(&body[separator + 1..])),
None => (body, None),
};
let kind = classify_message_kind(addressee, text);
AprsData::Message(Message {
addressee,
kind,
text,
id,
})
}
fn parse_telemetry(information: &[u8]) -> AprsData<'_> {
if !information.starts_with(b"#") {
return AprsData::Malformed {
identifier: b'T',
information,
};
}
let fields: Vec<&[u8]> = information[1..].split(|byte| *byte == b',').collect();
if fields.len() < 6 || fields[..6].iter().any(|field| field.is_empty()) {
return AprsData::Malformed {
identifier: b'T',
information,
};
}
AprsData::Telemetry(Telemetry {
sequence: fields[0],
analog: [fields[1], fields[2], fields[3], fields[4], fields[5]],
digital: fields.get(6).copied().filter(|field| !field.is_empty()),
})
}
fn parse_maidenhead(information: &[u8]) -> AprsData<'_> {
if information.len() < 6 {
return AprsData::Malformed {
identifier: b'[',
information,
};
}
AprsData::Maidenhead(Maidenhead {
locator: &information[..6],
comment: &information[6..],
})
}
fn parse_user_defined(information: &[u8]) -> AprsData<'_> {
if information.len() < 2 {
return AprsData::Malformed {
identifier: b'{',
information,
};
}
AprsData::UserDefined(UserDefined {
user_id: information[0],
packet_type: information[1],
body: &information[2..],
})
}
fn classify_telemetry_metadata_kind(addressee: &[u8]) -> Option<TelemetryMetadataKind> {
match addressee.get(..5)? {
b"PARM." => Some(TelemetryMetadataKind::ParameterNames),
b"UNIT." => Some(TelemetryMetadataKind::Units),
b"EQNS." => Some(TelemetryMetadataKind::Equations),
b"BITS." => Some(TelemetryMetadataKind::BitSense),
_ => None,
}
}
fn classify_message_kind(addressee: &[u8], text: &[u8]) -> MessageKind {
if text.starts_with(b"ack") {
MessageKind::Ack
} else if text.starts_with(b"rej") {
MessageKind::Reject
} else if addressee.starts_with(b"BLN") && addressee.get(3).is_some_and(u8::is_ascii_digit) {
MessageKind::Bulletin
} else if addressee.starts_with(b"BLN") && addressee.get(3).is_some_and(u8::is_ascii_uppercase)
{
MessageKind::Announcement
} else {
MessageKind::Message
}
}
fn is_latitude(value: &[u8]) -> bool {
value.len() == 8
&& value[0].is_ascii_digit()
&& value[1].is_ascii_digit()
&& value[2].is_ascii_digit()
&& value[3].is_ascii_digit()
&& value[4] == b'.'
&& value[5].is_ascii_digit()
&& value[6].is_ascii_digit()
&& matches!(value[7], b'N' | b'S')
}
fn is_longitude(value: &[u8]) -> bool {
value.len() == 9
&& value[0].is_ascii_digit()
&& value[1].is_ascii_digit()
&& value[2].is_ascii_digit()
&& value[3].is_ascii_digit()
&& value[4].is_ascii_digit()
&& value[5] == b'.'
&& value[6].is_ascii_digit()
&& value[7].is_ascii_digit()
&& matches!(value[8], b'E' | b'W')
}
fn is_symbol_table_identifier(value: u8) -> bool {
matches!(value, b'/' | b'\\') || value.is_ascii_alphanumeric()
}
fn is_printable_ascii(value: u8) -> bool {
(0x20..=0x7e).contains(&value)
}
fn is_base91(value: u8) -> bool {
(b'!'..=b'{').contains(&value)
}
fn is_compressed_position(information: &[u8]) -> bool {
information
.first()
.is_some_and(|byte| !byte.is_ascii_digit() && is_symbol_table_identifier(*byte))
&& information
.get(1..13)
.is_some_and(|bytes| bytes.iter().all(|byte| is_base91(*byte)))
}
fn is_timestamp(value: &[u8]) -> bool {
value.len() == 7
&& value[..6].iter().all(u8::is_ascii_digit)
&& matches!(value[6], b'z' | b'/' | b'h')
}
fn decode_latitude(value: &[u8]) -> Option<f64> {
if !is_latitude(value) {
return None;
}
let degrees = parse_u16(&value[..2])? as f64;
let minutes = parse_fixed_minutes(&value[2..7])?;
let sign = match value[7] {
b'N' => 1.0,
b'S' => -1.0,
_ => return None,
};
Some(sign * (degrees + minutes / 60.0))
}
fn decode_longitude(value: &[u8]) -> Option<f64> {
if !is_longitude(value) {
return None;
}
let degrees = parse_u16(&value[..3])? as f64;
let minutes = parse_fixed_minutes(&value[3..8])?;
let sign = match value[8] {
b'E' => 1.0,
b'W' => -1.0,
_ => return None,
};
Some(sign * (degrees + minutes / 60.0))
}
fn parse_fixed_minutes(value: &[u8]) -> Option<f64> {
if value.len() != 5 || value[2] != b'.' || !value[..2].iter().all(u8::is_ascii_digit) {
return None;
}
let whole = parse_u16(&value[..2])? as f64;
let fraction = parse_u16(&value[3..])? as f64 / 100.0;
Some(whole + fraction)
}
fn decode_base91(value: &[u8]) -> Option<u32> {
if value.len() != 4 || !value.iter().all(|byte| is_base91(*byte)) {
return None;
}
let mut decoded = 0u32;
for byte in value {
decoded = decoded * 91 + u32::from(byte - b'!');
}
Some(decoded)
}
fn parse_u16(value: &[u8]) -> Option<u16> {
if value.is_empty() || !value.iter().all(u8::is_ascii_digit) {
return None;
}
let mut parsed = 0u16;
for digit in value {
parsed = parsed.checked_mul(10)?;
parsed = parsed.checked_add(u16::from(digit - b'0'))?;
}
Some(parsed)
}
fn parse_i16(value: &[u8]) -> Option<i16> {
if value.is_empty() {
return None;
}
let (sign, digits) = match value[0] {
b'-' => (-1, &value[1..]),
b'+' => (1, &value[1..]),
_ => (1, value),
};
let unsigned = parse_u16(digits)?;
i16::try_from(unsigned).ok()?.checked_mul(sign)
}
fn parse_hex_byte(value: &[u8]) -> Option<u8> {
if value.len() != 2 {
return None;
}
Some(hex_value(value[0])? * 16 + hex_value(value[1])?)
}
fn hex_value(value: u8) -> Option<u8> {
match value {
b'0'..=b'9' => Some(value - b'0'),
b'A'..=b'F' => Some(value - b'A' + 10),
b'a'..=b'f' => Some(value - b'a' + 10),
_ => None,
}
}
fn parse_tagged_u16(report: &[u8], tag: u8, width: usize) -> Option<u16> {
parse_tagged(report, tag, width).and_then(parse_u16)
}
fn parse_tagged_i16(report: &[u8], tag: u8, width: usize) -> Option<i16> {
parse_tagged(report, tag, width).and_then(parse_i16)
}
fn parse_tagged(report: &[u8], tag: u8, width: usize) -> Option<&[u8]> {
let start = report.iter().position(|byte| *byte == tag)? + 1;
report.get(start..start + width)
}
fn decode_mic_e_status(destination: &[u8]) -> Option<MicEStatus> {
if destination.len() != 6 {
return None;
}
let bytes = destination.get(..3)?;
Some(MicEStatus::Custom([
mic_e_status_bit(bytes[0])?,
mic_e_status_bit(bytes[1])?,
mic_e_status_bit(bytes[2])?,
]))
}
fn mic_e_status_bit(byte: u8) -> Option<bool> {
match byte {
b'0'..=b'9' | b'L' => Some(false),
b'A'..=b'K' | b'P'..=b'Z' => Some(true),
_ => None,
}
}
fn decode_mic_e_latitude_digits(destination: &[u8]) -> Option<[u8; 6]> {
if destination.len() != 6 {
return None;
}
let mut digits = [0u8; 6];
for (index, byte) in destination.iter().copied().enumerate() {
digits[index] = mic_e_latitude_digit(byte)?;
}
Some(digits)
}
fn mic_e_latitude_digit(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'A'..=b'J' => Some(byte - b'A'),
b'P'..=b'Y' => Some(byte - b'P'),
b'K' | b'L' | b'Z' => Some(0),
_ => None,
}
}
fn decode_mic_e_latitude(destination: &[u8]) -> Option<f64> {
let digits = decode_mic_e_latitude_digits(destination)?;
let degrees = u16::from(digits[0]) * 10 + u16::from(digits[1]);
let minutes = u16::from(digits[2]) * 10 + u16::from(digits[3]);
let hundredths = u16::from(digits[4]) * 10 + u16::from(digits[5]);
if degrees > 90 || minutes > 59 {
return None;
}
let sign = if mic_e_north(destination[3])? {
1.0
} else {
-1.0
};
Some(sign * (f64::from(degrees) + (f64::from(minutes) + f64::from(hundredths) / 100.0) / 60.0))
}
fn decode_mic_e_longitude(destination: &[u8], body: &[u8]) -> Option<f64> {
if destination.len() != 6 || body.len() < 3 {
return None;
}
let mut degrees = i16::from(mic_e_body_value(body[0])?);
if mic_e_longitude_offset(destination[4])? {
degrees += 100;
}
if (180..=189).contains(°rees) {
degrees -= 80;
} else if (190..=199).contains(°rees) {
degrees -= 190;
}
let minutes = mic_e_body_value(body[1])?;
let hundredths = mic_e_body_value(body[2])?;
if !(0..=179).contains(°rees) || minutes > 59 || hundredths > 99 {
return None;
}
let sign = if mic_e_west(destination[5])? {
-1.0
} else {
1.0
};
Some(sign * (f64::from(degrees) + (f64::from(minutes) + f64::from(hundredths) / 100.0) / 60.0))
}
fn decode_mic_e_speed_course(body: &[u8]) -> Option<MicESpeedCourse> {
if body.len() < 6 {
return None;
}
let speed_tens = u16::from(mic_e_body_value(body[3])?);
let speed_units_course_hundreds = u16::from(mic_e_body_value(body[4])?);
let course_remainder = u16::from(mic_e_body_value(body[5])?);
let mut speed_knots = speed_tens * 10 + speed_units_course_hundreds / 10;
if speed_knots >= 800 {
speed_knots -= 800;
}
Some(MicESpeedCourse {
speed_knots,
course_degrees: (speed_units_course_hundreds % 10) * 100 + course_remainder,
})
}
fn mic_e_body_value(byte: u8) -> Option<u8> {
let value = byte.checked_sub(28)?;
(value <= 99).then_some(value)
}
fn mic_e_north(byte: u8) -> Option<bool> {
match byte {
b'0'..=b'9' | b'A'..=b'L' => Some(false),
b'P'..=b'Z' => Some(true),
_ => None,
}
}
fn mic_e_longitude_offset(byte: u8) -> Option<bool> {
match byte {
b'0'..=b'9' | b'A'..=b'L' => Some(false),
b'P'..=b'Z' => Some(true),
_ => None,
}
}
fn mic_e_west(byte: u8) -> Option<bool> {
match byte {
b'0'..=b'9' | b'A'..=b'L' => Some(false),
b'P'..=b'Z' => Some(true),
_ => None,
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ParseError {
Empty,
Oversized,
MissingSeparator,
EmptySegment,
InvalidAddress,
}
impl ParseError {
#[must_use]
pub fn code(&self) -> &'static str {
match self {
Self::Empty => "parse.empty",
Self::Oversized => "parse.oversized",
Self::MissingSeparator => "parse.missing_separator",
Self::EmptySegment => "parse.empty_segment",
Self::InvalidAddress => "parse.invalid_address",
}
}
}
pub fn parse_packet(input: &[u8]) -> Result<ParsedPacket, ParseError> {
parse_packet_with_options(input, ParseOptions::default())
}
pub fn parse_packet_with_options(
input: &[u8],
options: ParseOptions,
) -> Result<ParsedPacket, ParseError> {
if input.is_empty() {
return Err(ParseError::Empty);
}
if input.len() > options.max_packet_len {
return Err(ParseError::Oversized);
}
let source_end = input
.iter()
.position(|byte| *byte == b'>')
.ok_or(ParseError::MissingSeparator)?;
let payload_separator = input[source_end + 1..]
.iter()
.position(|byte| *byte == b':')
.map(|offset| source_end + 1 + offset)
.ok_or(ParseError::MissingSeparator)?;
let path_start = source_end + 1;
let path_end = payload_separator;
let payload_start = payload_separator + 1;
if source_end == 0 || path_start == path_end || payload_start == input.len() {
return Err(ParseError::EmptySegment);
}
let Some(path_components) = path_component_ranges(input, path_start, path_end) else {
return Err(ParseError::InvalidAddress);
};
if !is_ax25_like_source(&input[..source_end])
|| !path_components
.iter()
.all(|(start, end)| is_ax25_like_path_component(&input[*start..*end]))
{
return Err(ParseError::InvalidAddress);
}
Ok(ParsedPacket {
raw: RawPacket {
bytes: input.to_vec(),
},
source_end,
path_start,
path_end,
path_components,
payload_start,
})
}
fn path_component_ranges(
input: &[u8],
path_start: usize,
path_end: usize,
) -> Option<Vec<(usize, usize)>> {
let mut components = Vec::new();
let mut component_start = path_start;
for (offset, byte) in input[path_start..path_end].iter().enumerate() {
if *byte == b',' {
let index = path_start + offset;
if component_start == index {
return None;
}
components.push((component_start, index));
component_start = index + 1;
}
}
if component_start == path_end {
return None;
}
components.push((component_start, path_end));
Some(components)
}
fn is_ax25_like_source(source: &[u8]) -> bool {
is_ax25_like_address(source, false)
}
fn is_ax25_like_path_component(component: &[u8]) -> bool {
is_ax25_like_address(component, true)
}
fn is_ax25_like_address(address: &[u8], allow_repeated_marker: bool) -> bool {
let address = if allow_repeated_marker {
address.strip_suffix(b"*").unwrap_or(address)
} else {
address
};
if address.is_empty() || address.contains(&b'*') {
return false;
}
let (callsign, ssid) = match address.iter().position(|byte| *byte == b'-') {
Some(separator) => (&address[..separator], Some(&address[separator + 1..])),
None => (address, None),
};
is_ax25_like_callsign(callsign) && ssid.map_or(true, is_ax25_like_ssid)
}
fn is_ax25_like_callsign(callsign: &[u8]) -> bool {
(1..=6).contains(&callsign.len())
&& callsign
.iter()
.all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit())
}
fn is_ax25_like_ssid(ssid: &[u8]) -> bool {
if ssid.is_empty() || ssid.len() > 2 || !ssid.iter().all(u8::is_ascii_digit) {
return false;
}
let mut value = 0u8;
for digit in ssid {
value = value * 10 + (digit - b'0');
}
value <= 15
}