1#![forbid(unsafe_code)]
2
3mod diagnostic;
9mod transport;
10
11#[cfg(feature = "serde")]
12pub mod serde_support;
13
14pub use transport::LineTransport;
15
16pub const MAX_PACKET_LEN: usize = 512;
18
19pub const DEFAULT_PARSE_OPTIONS: ParseOptions = ParseOptions {
21 max_packet_len: MAX_PACKET_LEN,
22};
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub struct ParseOptions {
30 pub max_packet_len: usize,
32}
33
34impl ParseOptions {
35 #[must_use]
37 pub const fn new(max_packet_len: usize) -> Self {
38 Self { max_packet_len }
39 }
40}
41
42impl Default for ParseOptions {
43 fn default() -> Self {
44 DEFAULT_PARSE_OPTIONS
45 }
46}
47
48#[derive(Clone, Debug, Eq, PartialEq)]
50pub struct RawPacket {
51 bytes: Vec<u8>,
52}
53
54impl RawPacket {
55 #[must_use]
57 pub fn as_bytes(&self) -> &[u8] {
58 &self.bytes
59 }
60}
61
62#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct ParsedPacket {
65 raw: RawPacket,
66 source_end: usize,
67 path_start: usize,
68 path_end: usize,
69 path_components: Vec<(usize, usize)>,
70 payload_start: usize,
71}
72
73impl ParsedPacket {
74 #[must_use]
76 pub fn raw(&self) -> &RawPacket {
77 &self.raw
78 }
79
80 #[must_use]
82 pub fn source(&self) -> &[u8] {
83 &self.raw.bytes[..self.source_end]
84 }
85
86 #[must_use]
88 pub fn path(&self) -> &[u8] {
89 &self.raw.bytes[self.path_start..self.path_end]
90 }
91
92 #[must_use]
94 pub fn destination(&self) -> &[u8] {
95 let (start, end) = self.path_components[0];
96 &self.raw.bytes[start..end]
97 }
98
99 #[must_use]
101 pub fn digipeaters(&self) -> Vec<&[u8]> {
102 self.path_components[1..]
103 .iter()
104 .map(|(start, end)| &self.raw.bytes[*start..*end])
105 .collect()
106 }
107
108 #[must_use]
110 pub fn path_components(&self) -> Vec<&[u8]> {
111 self.path_components
112 .iter()
113 .map(|(start, end)| &self.raw.bytes[*start..*end])
114 .collect()
115 }
116
117 #[must_use]
119 pub fn payload(&self) -> &[u8] {
120 &self.raw.bytes[self.payload_start..]
121 }
122
123 #[must_use]
125 pub fn data_type_identifier(&self) -> DataTypeIdentifier {
126 DataTypeIdentifier::from_byte(self.raw.bytes[self.payload_start])
127 }
128
129 #[must_use]
131 pub fn information(&self) -> &[u8] {
132 &self.raw.bytes[self.payload_start + 1..]
133 }
134
135 #[must_use]
137 pub fn aprs_data(&self) -> AprsData<'_> {
138 parse_aprs_data(
139 self.data_type_identifier(),
140 self.information(),
141 self.destination(),
142 )
143 }
144
145 #[must_use]
147 pub fn to_json(&self) -> String {
148 diagnostic::packet_to_json(self)
149 }
150}
151
152#[derive(Clone, Debug, Eq, PartialEq)]
154pub struct Engine {
155 policy: Policy,
156 counters: Counters,
157}
158
159impl Engine {
160 #[must_use]
162 pub fn new(policy: Policy) -> Self {
163 Self {
164 policy,
165 counters: Counters::default(),
166 }
167 }
168
169 pub fn process(&mut self, input: &[u8]) -> EngineResult {
171 match parse_packet(input) {
172 Ok(packet) => {
173 let semantic = packet.aprs_data();
174 match self.policy.evaluate(&packet, &semantic) {
175 PolicyDecision::Accept => {
176 self.counters.accepted = self.counters.accepted.saturating_add(1);
177 EngineResult::Accepted { packet }
178 }
179 PolicyDecision::Reject(reason) => {
180 self.counters.rejected = self.counters.rejected.saturating_add(1);
181 EngineResult::Rejected { packet, reason }
182 }
183 }
184 }
185 Err(error) => {
186 self.counters.malformed = self.counters.malformed.saturating_add(1);
187 EngineResult::ParseError(error)
188 }
189 }
190 }
191
192 #[must_use]
194 pub fn counters(&self) -> Counters {
195 self.counters
196 }
197}
198
199impl Default for Engine {
200 fn default() -> Self {
201 Self::new(Policy::default())
202 }
203}
204
205#[derive(Clone, Debug, PartialEq)]
207pub enum EngineResult {
208 Accepted {
210 packet: ParsedPacket,
212 },
213 Rejected {
215 packet: ParsedPacket,
217 reason: PolicyRejection,
219 },
220 ParseError(ParseError),
222}
223
224#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
226pub struct Counters {
227 pub accepted: u64,
229 pub rejected: u64,
231 pub malformed: u64,
233}
234
235#[derive(Clone, Debug, Eq, PartialEq)]
237pub struct Policy {
238 pub allow_unsupported: bool,
240 pub allow_malformed_semantics: bool,
242 pub max_path_components: usize,
244}
245
246impl Policy {
247 #[must_use]
249 pub fn strict() -> Self {
250 Self::default()
251 }
252
253 #[must_use]
255 pub fn permissive() -> Self {
256 Self {
257 allow_unsupported: true,
258 allow_malformed_semantics: true,
259 max_path_components: 9,
260 }
261 }
262
263 #[must_use]
265 pub fn evaluate(&self, packet: &ParsedPacket, semantic: &AprsData<'_>) -> PolicyDecision {
266 if packet.path_components.len() > self.max_path_components {
267 return PolicyDecision::Reject(PolicyRejection::PathTooLong);
268 }
269
270 match semantic {
271 AprsData::Malformed { .. } if !self.allow_malformed_semantics => {
272 PolicyDecision::Reject(PolicyRejection::MalformedSemantics)
273 }
274 AprsData::Unsupported { .. } if !self.allow_unsupported => {
275 PolicyDecision::Reject(PolicyRejection::UnsupportedSemantics)
276 }
277 _ => PolicyDecision::Accept,
278 }
279 }
280}
281
282impl Default for Policy {
283 fn default() -> Self {
284 Self {
285 allow_unsupported: false,
286 allow_malformed_semantics: false,
287 max_path_components: 9,
288 }
289 }
290}
291
292#[derive(Clone, Copy, Debug, Eq, PartialEq)]
294pub enum PolicyDecision {
295 Accept,
297 Reject(PolicyRejection),
299}
300
301#[derive(Clone, Copy, Debug, Eq, PartialEq)]
303pub enum PolicyRejection {
304 PathTooLong,
306 MalformedSemantics,
308 UnsupportedSemantics,
310}
311
312impl PolicyRejection {
313 #[must_use]
315 pub fn code(self) -> &'static str {
316 match self {
317 Self::PathTooLong => "policy.path_too_long",
318 Self::MalformedSemantics => "policy.malformed_semantics",
319 Self::UnsupportedSemantics => "policy.unsupported_semantics",
320 }
321 }
322}
323
324#[derive(Clone, Copy, Debug, Eq, PartialEq)]
326pub enum AprsData<'a> {
327 Status {
329 text: &'a [u8],
331 },
332 Position(Position<'a>),
334 TimestampedPosition(TimestampedPosition<'a>),
336 CompressedPosition(CompressedPosition<'a>),
338 Message(Message<'a>),
340 Object(Object<'a>),
342 Item(Item<'a>),
344 Weather(Weather<'a>),
346 Telemetry(Telemetry<'a>),
348 TelemetryMetadata(TelemetryMetadata<'a>),
350 Query(Query<'a>),
352 Capability(Capability<'a>),
354 Nmea(Nmea<'a>),
356 MicE(MicE<'a>),
358 Maidenhead(Maidenhead<'a>),
360 UserDefined(UserDefined<'a>),
362 ThirdParty(ThirdParty<'a>),
364 Unsupported {
366 identifier: u8,
368 information: &'a [u8],
370 },
371 Malformed {
373 identifier: u8,
375 information: &'a [u8],
377 },
378}
379
380impl AprsData<'_> {
381 #[must_use]
383 pub fn kind_name(&self) -> &'static str {
384 match self {
385 Self::Status { .. } => "status",
386 Self::Position(_) => "position",
387 Self::TimestampedPosition(_) => "timestamped_position",
388 Self::CompressedPosition(_) => "compressed_position",
389 Self::Message(_) => "message",
390 Self::Object(_) => "object",
391 Self::Item(_) => "item",
392 Self::Weather(_) => "weather",
393 Self::Telemetry(_) => "telemetry",
394 Self::TelemetryMetadata(_) => "telemetry_metadata",
395 Self::Query(_) => "query",
396 Self::Capability(_) => "capability",
397 Self::Nmea(_) => "nmea",
398 Self::MicE(_) => "mic_e",
399 Self::Maidenhead(_) => "maidenhead",
400 Self::UserDefined(_) => "user_defined",
401 Self::ThirdParty(_) => "third_party",
402 Self::Unsupported { .. } => "unsupported",
403 Self::Malformed { .. } => "malformed",
404 }
405 }
406}
407
408#[derive(Clone, Copy, Debug, Eq, PartialEq)]
410pub struct Position<'a> {
411 pub messaging: bool,
413 pub latitude: &'a [u8],
415 pub symbol_table: u8,
417 pub longitude: &'a [u8],
419 pub symbol_code: u8,
421 pub comment: &'a [u8],
423}
424
425impl Position<'_> {
426 #[must_use]
428 pub fn coordinates(&self) -> Option<Coordinates> {
429 Some(Coordinates {
430 latitude: decode_latitude(self.latitude)?,
431 longitude: decode_longitude(self.longitude)?,
432 })
433 }
434}
435
436#[derive(Clone, Copy, Debug, PartialEq)]
438pub struct Coordinates {
439 pub latitude: f64,
441 pub longitude: f64,
443}
444
445#[derive(Clone, Copy, Debug, Eq, PartialEq)]
447pub struct TimestampedPosition<'a> {
448 pub messaging: bool,
450 pub timestamp: &'a [u8],
452 pub position: Position<'a>,
454}
455
456#[derive(Clone, Copy, Debug, Eq, PartialEq)]
458pub struct CompressedPosition<'a> {
459 pub messaging: bool,
461 pub symbol_table: u8,
463 pub compressed_latitude: &'a [u8],
465 pub compressed_longitude: &'a [u8],
467 pub symbol_code: u8,
469 pub extension: &'a [u8],
471 pub compression_type: u8,
473 pub comment: &'a [u8],
475}
476
477impl CompressedPosition<'_> {
478 #[must_use]
480 pub fn coordinates(&self) -> Option<Coordinates> {
481 let y = decode_base91(self.compressed_latitude)?;
482 let x = decode_base91(self.compressed_longitude)?;
483
484 Some(Coordinates {
485 latitude: 90.0 - (y as f64 / 380_926.0),
486 longitude: -180.0 + (x as f64 / 190_463.0),
487 })
488 }
489}
490
491#[derive(Clone, Copy, Debug, Eq, PartialEq)]
493pub struct Message<'a> {
494 pub addressee: &'a [u8],
496 pub kind: MessageKind,
498 pub text: &'a [u8],
500 pub id: Option<&'a [u8]>,
502}
503
504#[derive(Clone, Copy, Debug, Eq, PartialEq)]
506pub enum MessageKind {
507 Message,
509 Ack,
511 Reject,
513 Bulletin,
515 Announcement,
517}
518
519#[derive(Clone, Copy, Debug, Eq, PartialEq)]
521pub struct Object<'a> {
522 pub name: &'a [u8],
524 pub live: bool,
526 pub timestamp: &'a [u8],
528 pub body: &'a [u8],
530}
531
532#[derive(Clone, Copy, Debug, Eq, PartialEq)]
534pub struct Item<'a> {
535 pub name: &'a [u8],
537 pub live: bool,
539 pub body: &'a [u8],
541}
542
543#[derive(Clone, Copy, Debug, Eq, PartialEq)]
545pub struct Weather<'a> {
546 pub report: &'a [u8],
548}
549
550impl Weather<'_> {
551 #[must_use]
553 pub fn fields(&self) -> WeatherFields<'_> {
554 WeatherFields {
555 timestamp: self
556 .report
557 .get(..6)
558 .filter(|value| value.iter().all(u8::is_ascii_digit)),
559 wind_direction_degrees: parse_tagged_u16(self.report, b'c', 3),
560 wind_speed_mph: parse_tagged_u16(self.report, b's', 3),
561 wind_gust_mph: parse_tagged_u16(self.report, b'g', 3),
562 temperature_fahrenheit: parse_tagged_i16(self.report, b't', 3),
563 rain_last_hour_hundredths_inch: parse_tagged_u16(self.report, b'r', 3),
564 rain_last_24_hours_hundredths_inch: parse_tagged_u16(self.report, b'p', 3),
565 rain_since_midnight_hundredths_inch: parse_tagged_u16(self.report, b'P', 3),
566 humidity_percent: parse_tagged_u16(self.report, b'h', 2).map(|value| {
567 if value == 0 {
568 100
569 } else {
570 value
571 }
572 }),
573 pressure_tenths_hpa: parse_tagged_u16(self.report, b'b', 5),
574 }
575 }
576}
577
578#[derive(Clone, Copy, Debug, Eq, PartialEq)]
580pub struct WeatherFields<'a> {
581 pub timestamp: Option<&'a [u8]>,
583 pub wind_direction_degrees: Option<u16>,
585 pub wind_speed_mph: Option<u16>,
587 pub wind_gust_mph: Option<u16>,
589 pub temperature_fahrenheit: Option<i16>,
591 pub rain_last_hour_hundredths_inch: Option<u16>,
593 pub rain_last_24_hours_hundredths_inch: Option<u16>,
595 pub rain_since_midnight_hundredths_inch: Option<u16>,
597 pub humidity_percent: Option<u16>,
599 pub pressure_tenths_hpa: Option<u16>,
601}
602
603#[derive(Clone, Copy, Debug, Eq, PartialEq)]
605pub struct Telemetry<'a> {
606 pub sequence: &'a [u8],
608 pub analog: [&'a [u8]; 5],
610 pub digital: Option<&'a [u8]>,
612}
613
614impl Telemetry<'_> {
615 #[must_use]
617 pub fn sequence_number(&self) -> Option<u16> {
618 parse_u16(self.sequence)
619 }
620
621 #[must_use]
623 pub fn analog_values(&self) -> Option<[u16; 5]> {
624 Some([
625 parse_u16(self.analog[0])?,
626 parse_u16(self.analog[1])?,
627 parse_u16(self.analog[2])?,
628 parse_u16(self.analog[3])?,
629 parse_u16(self.analog[4])?,
630 ])
631 }
632
633 #[must_use]
635 pub fn digital_bits(&self) -> Option<[bool; 8]> {
636 let digital = self.digital?;
637 if digital.len() != 8 {
638 return None;
639 }
640
641 let mut bits = [false; 8];
642 for (index, byte) in digital.iter().enumerate() {
643 bits[index] = match byte {
644 b'0' => false,
645 b'1' => true,
646 _ => return None,
647 };
648 }
649
650 Some(bits)
651 }
652}
653
654#[derive(Clone, Copy, Debug, Eq, PartialEq)]
656pub struct TelemetryMetadata<'a> {
657 pub addressee: &'a [u8],
659 pub kind: TelemetryMetadataKind,
661 pub body: &'a [u8],
663}
664
665impl<'a> TelemetryMetadata<'a> {
666 #[must_use]
668 pub fn fields(&self) -> Vec<&'a [u8]> {
669 self.body.split(|byte| *byte == b',').collect()
670 }
671}
672
673#[derive(Clone, Copy, Debug, Eq, PartialEq)]
675pub enum TelemetryMetadataKind {
676 ParameterNames,
678 Units,
680 Equations,
682 BitSense,
684}
685
686#[derive(Clone, Copy, Debug, Eq, PartialEq)]
688pub struct Query<'a> {
689 pub query: &'a [u8],
691}
692
693#[derive(Clone, Copy, Debug, Eq, PartialEq)]
695pub struct Capability<'a> {
696 pub body: &'a [u8],
698}
699
700#[derive(Clone, Copy, Debug, Eq, PartialEq)]
702pub struct Nmea<'a> {
703 pub sentence: &'a [u8],
705}
706
707impl Nmea<'_> {
708 #[must_use]
710 pub fn checksum(&self) -> Option<NmeaChecksum> {
711 let separator = self.sentence.iter().rposition(|byte| *byte == b'*')?;
712 let checksum = self.sentence.get(separator + 1..separator + 3)?;
713 if checksum.len() != 2 || self.sentence.get(separator + 3).is_some() {
714 return None;
715 }
716
717 let expected = parse_hex_byte(checksum)?;
718 let calculated = self.sentence[..separator]
719 .iter()
720 .fold(0u8, |accumulator, byte| accumulator ^ byte);
721
722 Some(NmeaChecksum {
723 expected,
724 calculated,
725 valid: expected == calculated,
726 })
727 }
728}
729
730#[derive(Clone, Copy, Debug, Eq, PartialEq)]
732pub struct NmeaChecksum {
733 pub expected: u8,
735 pub calculated: u8,
737 pub valid: bool,
739}
740
741#[derive(Clone, Copy, Debug, Eq, PartialEq)]
743pub struct MicE<'a> {
744 pub identifier: u8,
746 pub destination: &'a [u8],
748 pub body: &'a [u8],
750 pub status: Option<MicEStatus>,
752 pub latitude_digits: Option<[u8; 6]>,
754}
755
756impl MicE<'_> {
757 #[must_use]
759 pub fn coordinates(&self) -> Option<Coordinates> {
760 Some(Coordinates {
761 latitude: decode_mic_e_latitude(self.destination)?,
762 longitude: decode_mic_e_longitude(self.destination, self.body)?,
763 })
764 }
765
766 #[must_use]
768 pub fn speed_course(&self) -> Option<MicESpeedCourse> {
769 decode_mic_e_speed_course(self.body)
770 }
771}
772
773#[derive(Clone, Copy, Debug, Eq, PartialEq)]
775pub enum MicEStatus {
776 Custom([bool; 3]),
778}
779
780#[derive(Clone, Copy, Debug, Eq, PartialEq)]
782pub struct MicESpeedCourse {
783 pub speed_knots: u16,
785 pub course_degrees: u16,
787}
788
789#[derive(Clone, Copy, Debug, Eq, PartialEq)]
791pub struct Maidenhead<'a> {
792 pub locator: &'a [u8],
794 pub comment: &'a [u8],
796}
797
798#[derive(Clone, Copy, Debug, Eq, PartialEq)]
800pub struct UserDefined<'a> {
801 pub user_id: u8,
803 pub packet_type: u8,
805 pub body: &'a [u8],
807}
808
809#[derive(Clone, Copy, Debug, Eq, PartialEq)]
811pub struct ThirdParty<'a> {
812 pub body: &'a [u8],
814}
815
816impl ThirdParty<'_> {
817 pub fn nested_packet(&self) -> Result<ParsedPacket, ParseError> {
819 parse_packet(self.body)
820 }
821}
822
823#[derive(Clone, Copy, Debug, Eq, PartialEq)]
825pub enum DataTypeIdentifier {
826 PositionNoTimestamp,
828 PositionNoTimestampMessaging,
830 PositionWithTimestamp,
832 PositionWithTimestampMessaging,
834 Status,
836 Query,
838 Capability,
840 Message,
842 Object,
844 Item,
846 Weather,
848 Telemetry,
850 Nmea,
852 MicECurrent,
854 MicEOld,
856 Maidenhead,
858 UserDefined,
860 ThirdParty,
862 Unknown(u8),
864}
865
866impl DataTypeIdentifier {
867 fn from_byte(byte: u8) -> Self {
868 match byte {
869 b'!' => Self::PositionNoTimestamp,
870 b'=' => Self::PositionNoTimestampMessaging,
871 b'/' => Self::PositionWithTimestamp,
872 b'@' => Self::PositionWithTimestampMessaging,
873 b'>' => Self::Status,
874 b'?' => Self::Query,
875 b'<' => Self::Capability,
876 b':' => Self::Message,
877 b';' => Self::Object,
878 b')' => Self::Item,
879 b'_' => Self::Weather,
880 b'T' => Self::Telemetry,
881 b'$' => Self::Nmea,
882 b'`' => Self::MicECurrent,
883 b'\'' => Self::MicEOld,
884 b'[' => Self::Maidenhead,
885 b'{' => Self::UserDefined,
886 b'}' => Self::ThirdParty,
887 other => Self::Unknown(other),
888 }
889 }
890
891 fn as_byte(self) -> u8 {
892 match self {
893 Self::PositionNoTimestamp => b'!',
894 Self::PositionNoTimestampMessaging => b'=',
895 Self::PositionWithTimestamp => b'/',
896 Self::PositionWithTimestampMessaging => b'@',
897 Self::Status => b'>',
898 Self::Query => b'?',
899 Self::Capability => b'<',
900 Self::Message => b':',
901 Self::Object => b';',
902 Self::Item => b')',
903 Self::Weather => b'_',
904 Self::Telemetry => b'T',
905 Self::Nmea => b'$',
906 Self::MicECurrent => b'`',
907 Self::MicEOld => b'\'',
908 Self::Maidenhead => b'[',
909 Self::UserDefined => b'{',
910 Self::ThirdParty => b'}',
911 Self::Unknown(value) => value,
912 }
913 }
914
915 #[must_use]
917 pub fn name(self) -> &'static str {
918 match self {
919 Self::PositionNoTimestamp => "position_no_timestamp",
920 Self::PositionNoTimestampMessaging => "position_no_timestamp_messaging",
921 Self::PositionWithTimestamp => "position_with_timestamp",
922 Self::PositionWithTimestampMessaging => "position_with_timestamp_messaging",
923 Self::Status => "status",
924 Self::Query => "query",
925 Self::Capability => "capability",
926 Self::Message => "message",
927 Self::Object => "object",
928 Self::Item => "item",
929 Self::Weather => "weather",
930 Self::Telemetry => "telemetry",
931 Self::Nmea => "nmea",
932 Self::MicECurrent => "mic_e_current",
933 Self::MicEOld => "mic_e_old",
934 Self::Maidenhead => "maidenhead",
935 Self::UserDefined => "user_defined",
936 Self::ThirdParty => "third_party",
937 Self::Unknown(_) => "unknown",
938 }
939 }
940}
941
942fn parse_aprs_data<'a>(
943 identifier: DataTypeIdentifier,
944 information: &'a [u8],
945 destination: &'a [u8],
946) -> AprsData<'a> {
947 match identifier {
948 DataTypeIdentifier::Status => AprsData::Status { text: information },
949 DataTypeIdentifier::PositionNoTimestamp => parse_position(false, b'!', information),
950 DataTypeIdentifier::PositionNoTimestampMessaging => parse_position(true, b'=', information),
951 DataTypeIdentifier::PositionWithTimestamp => {
952 parse_timestamped_position(false, b'/', information)
953 }
954 DataTypeIdentifier::PositionWithTimestampMessaging => {
955 parse_timestamped_position(true, b'@', information)
956 }
957 DataTypeIdentifier::Message => parse_message(information),
958 DataTypeIdentifier::Object => parse_object(information),
959 DataTypeIdentifier::Item => parse_item(information),
960 DataTypeIdentifier::Weather => AprsData::Weather(Weather {
961 report: information,
962 }),
963 DataTypeIdentifier::Telemetry => parse_telemetry(information),
964 DataTypeIdentifier::Query => AprsData::Query(Query { query: information }),
965 DataTypeIdentifier::Capability => AprsData::Capability(Capability { body: information }),
966 DataTypeIdentifier::Nmea => AprsData::Nmea(Nmea {
967 sentence: information,
968 }),
969 DataTypeIdentifier::MicECurrent | DataTypeIdentifier::MicEOld => {
970 parse_mic_e(identifier, information, destination)
971 }
972 DataTypeIdentifier::Maidenhead => parse_maidenhead(information),
973 DataTypeIdentifier::UserDefined => parse_user_defined(information),
974 DataTypeIdentifier::ThirdParty => AprsData::ThirdParty(ThirdParty { body: information }),
975 other => AprsData::Unsupported {
976 identifier: other.as_byte(),
977 information,
978 },
979 }
980}
981
982fn parse_mic_e<'a>(
983 identifier: DataTypeIdentifier,
984 information: &'a [u8],
985 destination: &'a [u8],
986) -> AprsData<'a> {
987 AprsData::MicE(MicE {
988 identifier: identifier.as_byte(),
989 destination,
990 body: information,
991 status: decode_mic_e_status(destination),
992 latitude_digits: decode_mic_e_latitude_digits(destination),
993 })
994}
995
996fn parse_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
997 if is_compressed_position(information) {
998 return parse_compressed_position(messaging, identifier, information);
999 }
1000
1001 if information.len() < 18 {
1002 return AprsData::Malformed {
1003 identifier,
1004 information,
1005 };
1006 }
1007
1008 let latitude = &information[..8];
1009 let symbol_table = information[8];
1010 let longitude = &information[9..18];
1011 let symbol_code = information[18];
1012 let comment = &information[19..];
1013
1014 if !is_latitude(latitude)
1015 || !is_symbol_table_identifier(symbol_table)
1016 || !is_longitude(longitude)
1017 || !is_printable_ascii(symbol_code)
1018 {
1019 return AprsData::Malformed {
1020 identifier,
1021 information,
1022 };
1023 }
1024
1025 AprsData::Position(Position {
1026 messaging,
1027 latitude,
1028 symbol_table,
1029 longitude,
1030 symbol_code,
1031 comment,
1032 })
1033}
1034
1035fn parse_timestamped_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1036 if information.len() < 8 {
1037 return AprsData::Malformed {
1038 identifier,
1039 information,
1040 };
1041 }
1042
1043 let timestamp = &information[..7];
1044 if !is_timestamp(timestamp) {
1045 return AprsData::Malformed {
1046 identifier,
1047 information,
1048 };
1049 }
1050
1051 match parse_position(messaging, identifier, &information[7..]) {
1052 AprsData::Position(position) => AprsData::TimestampedPosition(TimestampedPosition {
1053 messaging,
1054 timestamp,
1055 position,
1056 }),
1057 AprsData::CompressedPosition(position) => AprsData::CompressedPosition(position),
1058 _ => AprsData::Malformed {
1059 identifier,
1060 information,
1061 },
1062 }
1063}
1064
1065fn parse_compressed_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1066 if information.len() < 13 {
1067 return AprsData::Malformed {
1068 identifier,
1069 information,
1070 };
1071 }
1072
1073 let symbol_table = information[0];
1074 let compressed_latitude = &information[1..5];
1075 let compressed_longitude = &information[5..9];
1076 let symbol_code = information[9];
1077 let extension = &information[10..12];
1078 let compression_type = information[12];
1079 let comment = &information[13..];
1080
1081 if !is_symbol_table_identifier(symbol_table)
1082 || !compressed_latitude.iter().all(|byte| is_base91(*byte))
1083 || !compressed_longitude.iter().all(|byte| is_base91(*byte))
1084 || !is_printable_ascii(symbol_code)
1085 || !extension.iter().all(|byte| is_base91(*byte))
1086 || !is_base91(compression_type)
1087 {
1088 return AprsData::Malformed {
1089 identifier,
1090 information,
1091 };
1092 }
1093
1094 AprsData::CompressedPosition(CompressedPosition {
1095 messaging,
1096 symbol_table,
1097 compressed_latitude,
1098 compressed_longitude,
1099 symbol_code,
1100 extension,
1101 compression_type,
1102 comment,
1103 })
1104}
1105
1106fn parse_object(information: &[u8]) -> AprsData<'_> {
1107 if information.len() < 17 || !matches!(information[9], b'*' | b'_') {
1108 return AprsData::Malformed {
1109 identifier: b';',
1110 information,
1111 };
1112 }
1113
1114 AprsData::Object(Object {
1115 name: &information[..9],
1116 live: information[9] == b'*',
1117 timestamp: &information[10..17],
1118 body: &information[17..],
1119 })
1120}
1121
1122fn parse_item(information: &[u8]) -> AprsData<'_> {
1123 let Some(separator) = information
1124 .iter()
1125 .position(|byte| matches!(*byte, b'!' | b'_'))
1126 else {
1127 return AprsData::Malformed {
1128 identifier: b')',
1129 information,
1130 };
1131 };
1132
1133 if separator == 0 || separator > 9 {
1134 return AprsData::Malformed {
1135 identifier: b')',
1136 information,
1137 };
1138 }
1139
1140 AprsData::Item(Item {
1141 name: &information[..separator],
1142 live: information[separator] == b'!',
1143 body: &information[separator + 1..],
1144 })
1145}
1146
1147fn parse_message(information: &[u8]) -> AprsData<'_> {
1148 if information.len() < 10 || information[9] != b':' {
1149 return AprsData::Malformed {
1150 identifier: b':',
1151 information,
1152 };
1153 }
1154
1155 let addressee = &information[..9];
1156 let body = &information[10..];
1157 if let Some(kind) = classify_telemetry_metadata_kind(addressee) {
1158 return AprsData::TelemetryMetadata(TelemetryMetadata {
1159 addressee,
1160 kind,
1161 body,
1162 });
1163 }
1164
1165 let (text, id) = match body.iter().position(|byte| *byte == b'{') {
1166 Some(separator) => (&body[..separator], Some(&body[separator + 1..])),
1167 None => (body, None),
1168 };
1169 let kind = classify_message_kind(addressee, text);
1170
1171 AprsData::Message(Message {
1172 addressee,
1173 kind,
1174 text,
1175 id,
1176 })
1177}
1178
1179fn parse_telemetry(information: &[u8]) -> AprsData<'_> {
1180 if !information.starts_with(b"#") {
1181 return AprsData::Malformed {
1182 identifier: b'T',
1183 information,
1184 };
1185 }
1186
1187 let fields: Vec<&[u8]> = information[1..].split(|byte| *byte == b',').collect();
1188 if fields.len() < 6 || fields[..6].iter().any(|field| field.is_empty()) {
1189 return AprsData::Malformed {
1190 identifier: b'T',
1191 information,
1192 };
1193 }
1194
1195 AprsData::Telemetry(Telemetry {
1196 sequence: fields[0],
1197 analog: [fields[1], fields[2], fields[3], fields[4], fields[5]],
1198 digital: fields.get(6).copied().filter(|field| !field.is_empty()),
1199 })
1200}
1201
1202fn parse_maidenhead(information: &[u8]) -> AprsData<'_> {
1203 if information.len() < 6 {
1204 return AprsData::Malformed {
1205 identifier: b'[',
1206 information,
1207 };
1208 }
1209
1210 AprsData::Maidenhead(Maidenhead {
1211 locator: &information[..6],
1212 comment: &information[6..],
1213 })
1214}
1215
1216fn parse_user_defined(information: &[u8]) -> AprsData<'_> {
1217 if information.len() < 2 {
1218 return AprsData::Malformed {
1219 identifier: b'{',
1220 information,
1221 };
1222 }
1223
1224 AprsData::UserDefined(UserDefined {
1225 user_id: information[0],
1226 packet_type: information[1],
1227 body: &information[2..],
1228 })
1229}
1230
1231fn classify_telemetry_metadata_kind(addressee: &[u8]) -> Option<TelemetryMetadataKind> {
1232 match addressee.get(..5)? {
1233 b"PARM." => Some(TelemetryMetadataKind::ParameterNames),
1234 b"UNIT." => Some(TelemetryMetadataKind::Units),
1235 b"EQNS." => Some(TelemetryMetadataKind::Equations),
1236 b"BITS." => Some(TelemetryMetadataKind::BitSense),
1237 _ => None,
1238 }
1239}
1240
1241fn classify_message_kind(addressee: &[u8], text: &[u8]) -> MessageKind {
1242 if text.starts_with(b"ack") {
1243 MessageKind::Ack
1244 } else if text.starts_with(b"rej") {
1245 MessageKind::Reject
1246 } else if addressee.starts_with(b"BLN") && addressee.get(3).is_some_and(u8::is_ascii_digit) {
1247 MessageKind::Bulletin
1248 } else if addressee.starts_with(b"BLN") && addressee.get(3).is_some_and(u8::is_ascii_uppercase)
1249 {
1250 MessageKind::Announcement
1251 } else {
1252 MessageKind::Message
1253 }
1254}
1255
1256fn is_latitude(value: &[u8]) -> bool {
1257 value.len() == 8
1258 && value[0].is_ascii_digit()
1259 && value[1].is_ascii_digit()
1260 && value[2].is_ascii_digit()
1261 && value[3].is_ascii_digit()
1262 && value[4] == b'.'
1263 && value[5].is_ascii_digit()
1264 && value[6].is_ascii_digit()
1265 && matches!(value[7], b'N' | b'S')
1266}
1267
1268fn is_longitude(value: &[u8]) -> bool {
1269 value.len() == 9
1270 && value[0].is_ascii_digit()
1271 && value[1].is_ascii_digit()
1272 && value[2].is_ascii_digit()
1273 && value[3].is_ascii_digit()
1274 && value[4].is_ascii_digit()
1275 && value[5] == b'.'
1276 && value[6].is_ascii_digit()
1277 && value[7].is_ascii_digit()
1278 && matches!(value[8], b'E' | b'W')
1279}
1280
1281fn is_symbol_table_identifier(value: u8) -> bool {
1282 matches!(value, b'/' | b'\\') || value.is_ascii_alphanumeric()
1283}
1284
1285fn is_printable_ascii(value: u8) -> bool {
1286 (0x20..=0x7e).contains(&value)
1287}
1288
1289fn is_base91(value: u8) -> bool {
1290 (b'!'..=b'{').contains(&value)
1291}
1292
1293fn is_compressed_position(information: &[u8]) -> bool {
1294 information
1295 .first()
1296 .is_some_and(|byte| !byte.is_ascii_digit() && is_symbol_table_identifier(*byte))
1297 && information
1298 .get(1..13)
1299 .is_some_and(|bytes| bytes.iter().all(|byte| is_base91(*byte)))
1300}
1301
1302fn is_timestamp(value: &[u8]) -> bool {
1303 value.len() == 7
1304 && value[..6].iter().all(u8::is_ascii_digit)
1305 && matches!(value[6], b'z' | b'/' | b'h')
1306}
1307
1308fn decode_latitude(value: &[u8]) -> Option<f64> {
1309 if !is_latitude(value) {
1310 return None;
1311 }
1312
1313 let degrees = parse_u16(&value[..2])? as f64;
1314 let minutes = parse_fixed_minutes(&value[2..7])?;
1315 let sign = match value[7] {
1316 b'N' => 1.0,
1317 b'S' => -1.0,
1318 _ => return None,
1319 };
1320
1321 Some(sign * (degrees + minutes / 60.0))
1322}
1323
1324fn decode_longitude(value: &[u8]) -> Option<f64> {
1325 if !is_longitude(value) {
1326 return None;
1327 }
1328
1329 let degrees = parse_u16(&value[..3])? as f64;
1330 let minutes = parse_fixed_minutes(&value[3..8])?;
1331 let sign = match value[8] {
1332 b'E' => 1.0,
1333 b'W' => -1.0,
1334 _ => return None,
1335 };
1336
1337 Some(sign * (degrees + minutes / 60.0))
1338}
1339
1340fn parse_fixed_minutes(value: &[u8]) -> Option<f64> {
1341 if value.len() != 5 || value[2] != b'.' || !value[..2].iter().all(u8::is_ascii_digit) {
1342 return None;
1343 }
1344
1345 let whole = parse_u16(&value[..2])? as f64;
1346 let fraction = parse_u16(&value[3..])? as f64 / 100.0;
1347 Some(whole + fraction)
1348}
1349
1350fn decode_base91(value: &[u8]) -> Option<u32> {
1351 if value.len() != 4 || !value.iter().all(|byte| is_base91(*byte)) {
1352 return None;
1353 }
1354
1355 let mut decoded = 0u32;
1356 for byte in value {
1357 decoded = decoded * 91 + u32::from(byte - b'!');
1358 }
1359
1360 Some(decoded)
1361}
1362
1363fn parse_u16(value: &[u8]) -> Option<u16> {
1364 if value.is_empty() || !value.iter().all(u8::is_ascii_digit) {
1365 return None;
1366 }
1367
1368 let mut parsed = 0u16;
1369 for digit in value {
1370 parsed = parsed.checked_mul(10)?;
1371 parsed = parsed.checked_add(u16::from(digit - b'0'))?;
1372 }
1373
1374 Some(parsed)
1375}
1376
1377fn parse_i16(value: &[u8]) -> Option<i16> {
1378 if value.is_empty() {
1379 return None;
1380 }
1381
1382 let (sign, digits) = match value[0] {
1383 b'-' => (-1, &value[1..]),
1384 b'+' => (1, &value[1..]),
1385 _ => (1, value),
1386 };
1387
1388 let unsigned = parse_u16(digits)?;
1389 i16::try_from(unsigned).ok()?.checked_mul(sign)
1390}
1391
1392fn parse_hex_byte(value: &[u8]) -> Option<u8> {
1393 if value.len() != 2 {
1394 return None;
1395 }
1396
1397 Some(hex_value(value[0])? * 16 + hex_value(value[1])?)
1398}
1399
1400fn hex_value(value: u8) -> Option<u8> {
1401 match value {
1402 b'0'..=b'9' => Some(value - b'0'),
1403 b'A'..=b'F' => Some(value - b'A' + 10),
1404 b'a'..=b'f' => Some(value - b'a' + 10),
1405 _ => None,
1406 }
1407}
1408
1409fn parse_tagged_u16(report: &[u8], tag: u8, width: usize) -> Option<u16> {
1410 parse_tagged(report, tag, width).and_then(parse_u16)
1411}
1412
1413fn parse_tagged_i16(report: &[u8], tag: u8, width: usize) -> Option<i16> {
1414 parse_tagged(report, tag, width).and_then(parse_i16)
1415}
1416
1417fn parse_tagged(report: &[u8], tag: u8, width: usize) -> Option<&[u8]> {
1418 let start = report.iter().position(|byte| *byte == tag)? + 1;
1419 report.get(start..start + width)
1420}
1421
1422fn decode_mic_e_status(destination: &[u8]) -> Option<MicEStatus> {
1423 if destination.len() != 6 {
1424 return None;
1425 }
1426
1427 let bytes = destination.get(..3)?;
1428 Some(MicEStatus::Custom([
1429 mic_e_status_bit(bytes[0])?,
1430 mic_e_status_bit(bytes[1])?,
1431 mic_e_status_bit(bytes[2])?,
1432 ]))
1433}
1434
1435fn mic_e_status_bit(byte: u8) -> Option<bool> {
1436 match byte {
1437 b'0'..=b'9' | b'L' => Some(false),
1438 b'A'..=b'K' | b'P'..=b'Z' => Some(true),
1439 _ => None,
1440 }
1441}
1442
1443fn decode_mic_e_latitude_digits(destination: &[u8]) -> Option<[u8; 6]> {
1444 if destination.len() != 6 {
1445 return None;
1446 }
1447
1448 let mut digits = [0u8; 6];
1449 for (index, byte) in destination.iter().copied().enumerate() {
1450 digits[index] = mic_e_latitude_digit(byte)?;
1451 }
1452
1453 Some(digits)
1454}
1455
1456fn mic_e_latitude_digit(byte: u8) -> Option<u8> {
1457 match byte {
1458 b'0'..=b'9' => Some(byte - b'0'),
1459 b'A'..=b'J' => Some(byte - b'A'),
1460 b'P'..=b'Y' => Some(byte - b'P'),
1461 b'K' | b'L' | b'Z' => Some(0),
1462 _ => None,
1463 }
1464}
1465
1466fn decode_mic_e_latitude(destination: &[u8]) -> Option<f64> {
1467 let digits = decode_mic_e_latitude_digits(destination)?;
1468 let degrees = u16::from(digits[0]) * 10 + u16::from(digits[1]);
1469 let minutes = u16::from(digits[2]) * 10 + u16::from(digits[3]);
1470 let hundredths = u16::from(digits[4]) * 10 + u16::from(digits[5]);
1471 if degrees > 90 || minutes > 59 {
1472 return None;
1473 }
1474
1475 let sign = if mic_e_north(destination[3])? {
1476 1.0
1477 } else {
1478 -1.0
1479 };
1480 Some(sign * (f64::from(degrees) + (f64::from(minutes) + f64::from(hundredths) / 100.0) / 60.0))
1481}
1482
1483fn decode_mic_e_longitude(destination: &[u8], body: &[u8]) -> Option<f64> {
1484 if destination.len() != 6 || body.len() < 3 {
1485 return None;
1486 }
1487
1488 let mut degrees = i16::from(mic_e_body_value(body[0])?);
1489 if mic_e_longitude_offset(destination[4])? {
1490 degrees += 100;
1491 }
1492 if (180..=189).contains(°rees) {
1493 degrees -= 80;
1494 } else if (190..=199).contains(°rees) {
1495 degrees -= 190;
1496 }
1497
1498 let minutes = mic_e_body_value(body[1])?;
1499 let hundredths = mic_e_body_value(body[2])?;
1500 if !(0..=179).contains(°rees) || minutes > 59 || hundredths > 99 {
1501 return None;
1502 }
1503
1504 let sign = if mic_e_west(destination[5])? {
1505 -1.0
1506 } else {
1507 1.0
1508 };
1509 Some(sign * (f64::from(degrees) + (f64::from(minutes) + f64::from(hundredths) / 100.0) / 60.0))
1510}
1511
1512fn decode_mic_e_speed_course(body: &[u8]) -> Option<MicESpeedCourse> {
1513 if body.len() < 6 {
1514 return None;
1515 }
1516
1517 let speed_tens = u16::from(mic_e_body_value(body[3])?);
1518 let speed_units_course_hundreds = u16::from(mic_e_body_value(body[4])?);
1519 let course_remainder = u16::from(mic_e_body_value(body[5])?);
1520 let mut speed_knots = speed_tens * 10 + speed_units_course_hundreds / 10;
1521 if speed_knots >= 800 {
1522 speed_knots -= 800;
1523 }
1524
1525 Some(MicESpeedCourse {
1526 speed_knots,
1527 course_degrees: (speed_units_course_hundreds % 10) * 100 + course_remainder,
1528 })
1529}
1530
1531fn mic_e_body_value(byte: u8) -> Option<u8> {
1532 let value = byte.checked_sub(28)?;
1533 (value <= 99).then_some(value)
1534}
1535
1536fn mic_e_north(byte: u8) -> Option<bool> {
1537 match byte {
1538 b'0'..=b'9' | b'A'..=b'L' => Some(false),
1539 b'P'..=b'Z' => Some(true),
1540 _ => None,
1541 }
1542}
1543
1544fn mic_e_longitude_offset(byte: u8) -> Option<bool> {
1545 match byte {
1546 b'0'..=b'9' | b'A'..=b'L' => Some(false),
1547 b'P'..=b'Z' => Some(true),
1548 _ => None,
1549 }
1550}
1551
1552fn mic_e_west(byte: u8) -> Option<bool> {
1553 match byte {
1554 b'0'..=b'9' | b'A'..=b'L' => Some(false),
1555 b'P'..=b'Z' => Some(true),
1556 _ => None,
1557 }
1558}
1559
1560#[derive(Clone, Debug, Eq, PartialEq)]
1562pub enum ParseError {
1563 Empty,
1565 Oversized,
1567 MissingSeparator,
1569 EmptySegment,
1571 InvalidAddress,
1573}
1574
1575impl ParseError {
1576 #[must_use]
1578 pub fn code(&self) -> &'static str {
1579 match self {
1580 Self::Empty => "parse.empty",
1581 Self::Oversized => "parse.oversized",
1582 Self::MissingSeparator => "parse.missing_separator",
1583 Self::EmptySegment => "parse.empty_segment",
1584 Self::InvalidAddress => "parse.invalid_address",
1585 }
1586 }
1587}
1588
1589pub fn parse_packet(input: &[u8]) -> Result<ParsedPacket, ParseError> {
1595 parse_packet_with_options(input, ParseOptions::default())
1596}
1597
1598pub fn parse_packet_with_options(
1600 input: &[u8],
1601 options: ParseOptions,
1602) -> Result<ParsedPacket, ParseError> {
1603 if input.is_empty() {
1604 return Err(ParseError::Empty);
1605 }
1606
1607 if input.len() > options.max_packet_len {
1608 return Err(ParseError::Oversized);
1609 }
1610
1611 let source_end = input
1612 .iter()
1613 .position(|byte| *byte == b'>')
1614 .ok_or(ParseError::MissingSeparator)?;
1615 let payload_separator = input[source_end + 1..]
1616 .iter()
1617 .position(|byte| *byte == b':')
1618 .map(|offset| source_end + 1 + offset)
1619 .ok_or(ParseError::MissingSeparator)?;
1620
1621 let path_start = source_end + 1;
1622 let path_end = payload_separator;
1623 let payload_start = payload_separator + 1;
1624
1625 if source_end == 0 || path_start == path_end || payload_start == input.len() {
1626 return Err(ParseError::EmptySegment);
1627 }
1628
1629 let Some(path_components) = path_component_ranges(input, path_start, path_end) else {
1630 return Err(ParseError::InvalidAddress);
1631 };
1632
1633 if !is_ax25_like_source(&input[..source_end])
1634 || !path_components
1635 .iter()
1636 .all(|(start, end)| is_ax25_like_path_component(&input[*start..*end]))
1637 {
1638 return Err(ParseError::InvalidAddress);
1639 }
1640
1641 Ok(ParsedPacket {
1642 raw: RawPacket {
1643 bytes: input.to_vec(),
1644 },
1645 source_end,
1646 path_start,
1647 path_end,
1648 path_components,
1649 payload_start,
1650 })
1651}
1652
1653fn path_component_ranges(
1654 input: &[u8],
1655 path_start: usize,
1656 path_end: usize,
1657) -> Option<Vec<(usize, usize)>> {
1658 let mut components = Vec::new();
1659 let mut component_start = path_start;
1660
1661 for (offset, byte) in input[path_start..path_end].iter().enumerate() {
1662 if *byte == b',' {
1663 let index = path_start + offset;
1664 if component_start == index {
1665 return None;
1666 }
1667 components.push((component_start, index));
1668 component_start = index + 1;
1669 }
1670 }
1671
1672 if component_start == path_end {
1673 return None;
1674 }
1675
1676 components.push((component_start, path_end));
1677 Some(components)
1678}
1679
1680fn is_ax25_like_source(source: &[u8]) -> bool {
1681 is_ax25_like_address(source, false)
1682}
1683
1684fn is_ax25_like_path_component(component: &[u8]) -> bool {
1685 is_ax25_like_address(component, true)
1686}
1687
1688fn is_ax25_like_address(address: &[u8], allow_repeated_marker: bool) -> bool {
1689 let address = if allow_repeated_marker {
1690 address.strip_suffix(b"*").unwrap_or(address)
1691 } else {
1692 address
1693 };
1694
1695 if address.is_empty() || address.contains(&b'*') {
1696 return false;
1697 }
1698
1699 let (callsign, ssid) = match address.iter().position(|byte| *byte == b'-') {
1700 Some(separator) => (&address[..separator], Some(&address[separator + 1..])),
1701 None => (address, None),
1702 };
1703
1704 is_ax25_like_callsign(callsign) && ssid.map_or(true, is_ax25_like_ssid)
1705}
1706
1707fn is_ax25_like_callsign(callsign: &[u8]) -> bool {
1708 (1..=6).contains(&callsign.len())
1709 && callsign
1710 .iter()
1711 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit())
1712}
1713
1714fn is_ax25_like_ssid(ssid: &[u8]) -> bool {
1715 if ssid.is_empty() || ssid.len() > 2 || !ssid.iter().all(u8::is_ascii_digit) {
1716 return false;
1717 }
1718
1719 let mut value = 0u8;
1720 for digit in ssid {
1721 value = value * 10 + (digit - b'0');
1722 }
1723
1724 value <= 15
1725}