1#![forbid(unsafe_code)]
2
3mod diagnostic;
9mod transport;
10
11#[cfg(feature = "serde")]
12pub mod serde_support;
13
14pub use transport::{
15 oversized_input_error, read_all_with_limit, LineTransport, PacketSink, PacketSource,
16 TransportErrorCode, DEFAULT_TRANSPORT_READ_LIMIT,
17};
18
19pub const MAX_PACKET_LEN: usize = 512;
21
22pub const DEFAULT_PARSE_OPTIONS: ParseOptions = ParseOptions {
24 max_packet_len: MAX_PACKET_LEN,
25};
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub struct ParseOptions {
33 pub max_packet_len: usize,
35}
36
37impl ParseOptions {
38 #[must_use]
40 pub const fn new(max_packet_len: usize) -> Self {
41 Self { max_packet_len }
42 }
43}
44
45impl Default for ParseOptions {
46 fn default() -> Self {
47 DEFAULT_PARSE_OPTIONS
48 }
49}
50
51#[derive(Clone, Debug, Eq, PartialEq)]
53pub struct RawPacket {
54 bytes: Vec<u8>,
55}
56
57impl RawPacket {
58 #[must_use]
60 pub fn as_bytes(&self) -> &[u8] {
61 &self.bytes
62 }
63}
64
65#[derive(Clone, Debug, Eq, PartialEq)]
67pub struct ParsedPacket {
68 raw: RawPacket,
69 source_end: usize,
70 path_start: usize,
71 path_end: usize,
72 path_components: Vec<(usize, usize)>,
73 payload_start: usize,
74}
75
76impl ParsedPacket {
77 #[must_use]
79 pub fn raw(&self) -> &RawPacket {
80 &self.raw
81 }
82
83 #[must_use]
85 pub fn source(&self) -> &[u8] {
86 &self.raw.bytes[..self.source_end]
87 }
88
89 #[must_use]
91 pub fn path(&self) -> &[u8] {
92 &self.raw.bytes[self.path_start..self.path_end]
93 }
94
95 #[must_use]
97 pub fn destination(&self) -> &[u8] {
98 let (start, end) = self.path_components[0];
99 &self.raw.bytes[start..end]
100 }
101
102 #[must_use]
104 pub fn digipeaters(&self) -> Vec<&[u8]> {
105 self.path_components[1..]
106 .iter()
107 .map(|(start, end)| &self.raw.bytes[*start..*end])
108 .collect()
109 }
110
111 #[must_use]
113 pub fn path_components(&self) -> Vec<&[u8]> {
114 self.path_components
115 .iter()
116 .map(|(start, end)| &self.raw.bytes[*start..*end])
117 .collect()
118 }
119
120 #[must_use]
122 pub fn payload(&self) -> &[u8] {
123 &self.raw.bytes[self.payload_start..]
124 }
125
126 #[must_use]
128 pub fn data_type_identifier(&self) -> DataTypeIdentifier {
129 DataTypeIdentifier::from_byte(self.raw.bytes[self.payload_start])
130 }
131
132 #[must_use]
134 pub fn information(&self) -> &[u8] {
135 &self.raw.bytes[self.payload_start + 1..]
136 }
137
138 #[must_use]
140 pub fn aprs_data(&self) -> AprsData<'_> {
141 parse_aprs_data(
142 self.data_type_identifier(),
143 self.information(),
144 self.destination(),
145 )
146 }
147
148 #[must_use]
150 pub fn summary(&self) -> PacketSummary<'_> {
151 PacketSummary::from_packet(self)
152 }
153
154 #[must_use]
156 pub fn to_json(&self) -> String {
157 diagnostic::packet_to_json(self)
158 }
159}
160
161#[derive(Clone, Copy, Debug, PartialEq)]
163pub struct PacketSummary<'a> {
164 pub source: &'a [u8],
166 pub destination: &'a [u8],
168 pub data_type: &'static str,
170 pub semantic: &'static str,
172 pub coordinates: Option<Coordinates>,
174 pub nmea_checksum: Option<NmeaChecksum>,
176 pub telemetry_sequence: Option<u16>,
178 pub mic_e_speed_course: Option<MicESpeedCourse>,
180}
181
182impl<'a> PacketSummary<'a> {
183 fn from_packet(packet: &'a ParsedPacket) -> Self {
184 let data = packet.aprs_data();
185 Self {
186 source: packet.source(),
187 destination: packet.destination(),
188 data_type: packet.data_type_identifier().name(),
189 semantic: data.kind_name(),
190 coordinates: summary_coordinates(data),
191 nmea_checksum: summary_nmea_checksum(data),
192 telemetry_sequence: summary_telemetry_sequence(data),
193 mic_e_speed_course: summary_mic_e_speed_course(data),
194 }
195 }
196}
197
198#[derive(Clone, Debug, Eq, PartialEq)]
200pub struct Engine {
201 policy: Policy,
202 counters: Counters,
203}
204
205impl Engine {
206 #[must_use]
208 pub fn new(policy: Policy) -> Self {
209 Self {
210 policy,
211 counters: Counters::default(),
212 }
213 }
214
215 pub fn process(&mut self, input: &[u8]) -> EngineResult {
217 match parse_packet(input) {
218 Ok(packet) => {
219 let semantic = packet.aprs_data();
220 match self.policy.evaluate(&packet, &semantic) {
221 PolicyDecision::Accept => {
222 self.counters.accepted = self.counters.accepted.saturating_add(1);
223 EngineResult::Accepted { packet }
224 }
225 PolicyDecision::Reject(reason) => {
226 self.counters.rejected = self.counters.rejected.saturating_add(1);
227 EngineResult::Rejected { packet, reason }
228 }
229 }
230 }
231 Err(error) => {
232 self.counters.malformed = self.counters.malformed.saturating_add(1);
233 EngineResult::ParseError(error)
234 }
235 }
236 }
237
238 pub fn process_packets<I, P>(&mut self, packets: I) -> Vec<EngineResult>
240 where
241 I: IntoIterator<Item = P>,
242 P: AsRef<[u8]>,
243 {
244 packets
245 .into_iter()
246 .map(|packet| self.process(packet.as_ref()))
247 .collect()
248 }
249
250 pub fn process_source<S>(&mut self, source: &mut S) -> Result<Vec<EngineResult>, S::Error>
252 where
253 S: PacketSource,
254 {
255 Ok(self.process_packets(source.recv_packets()?))
256 }
257
258 #[must_use]
260 pub fn counters(&self) -> Counters {
261 self.counters
262 }
263}
264
265impl Default for Engine {
266 fn default() -> Self {
267 Self::new(Policy::default())
268 }
269}
270
271#[derive(Clone, Debug, PartialEq)]
273pub enum EngineResult {
274 Accepted {
276 packet: ParsedPacket,
278 },
279 Rejected {
281 packet: ParsedPacket,
283 reason: PolicyRejection,
285 },
286 ParseError(ParseError),
288}
289
290#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
292pub struct Counters {
293 pub accepted: u64,
295 pub rejected: u64,
297 pub malformed: u64,
299}
300
301#[derive(Clone, Debug, Eq, PartialEq)]
303pub struct Policy {
304 pub allow_unsupported: bool,
306 pub allow_malformed_semantics: bool,
308 pub max_path_components: usize,
310}
311
312impl Policy {
313 #[must_use]
315 pub fn strict() -> Self {
316 Self::default()
317 }
318
319 #[must_use]
321 pub fn permissive() -> Self {
322 Self {
323 allow_unsupported: true,
324 allow_malformed_semantics: true,
325 max_path_components: 9,
326 }
327 }
328
329 #[must_use]
331 pub fn evaluate(&self, packet: &ParsedPacket, semantic: &AprsData<'_>) -> PolicyDecision {
332 if packet.path_components.len() > self.max_path_components {
333 return PolicyDecision::Reject(PolicyRejection::PathTooLong);
334 }
335
336 match semantic {
337 AprsData::Malformed { .. } if !self.allow_malformed_semantics => {
338 PolicyDecision::Reject(PolicyRejection::MalformedSemantics)
339 }
340 AprsData::Unsupported { .. } if !self.allow_unsupported => {
341 PolicyDecision::Reject(PolicyRejection::UnsupportedSemantics)
342 }
343 _ => PolicyDecision::Accept,
344 }
345 }
346}
347
348impl Default for Policy {
349 fn default() -> Self {
350 Self {
351 allow_unsupported: false,
352 allow_malformed_semantics: false,
353 max_path_components: 9,
354 }
355 }
356}
357
358#[derive(Clone, Copy, Debug, Eq, PartialEq)]
360pub enum PolicyDecision {
361 Accept,
363 Reject(PolicyRejection),
365}
366
367#[derive(Clone, Copy, Debug, Eq, PartialEq)]
369pub enum PolicyRejection {
370 PathTooLong,
372 MalformedSemantics,
374 UnsupportedSemantics,
376}
377
378impl PolicyRejection {
379 #[must_use]
381 pub fn code(self) -> &'static str {
382 match self {
383 Self::PathTooLong => "policy.path_too_long",
384 Self::MalformedSemantics => "policy.malformed_semantics",
385 Self::UnsupportedSemantics => "policy.unsupported_semantics",
386 }
387 }
388}
389
390#[derive(Clone, Copy, Debug, Eq, PartialEq)]
392pub enum AprsData<'a> {
393 Status {
395 text: &'a [u8],
397 },
398 Position(Position<'a>),
400 TimestampedPosition(TimestampedPosition<'a>),
402 CompressedPosition(CompressedPosition<'a>),
404 Message(Message<'a>),
406 Object(Object<'a>),
408 Item(Item<'a>),
410 Weather(Weather<'a>),
412 Telemetry(Telemetry<'a>),
414 TelemetryMetadata(TelemetryMetadata<'a>),
416 Query(Query<'a>),
418 Capability(Capability<'a>),
420 Nmea(Nmea<'a>),
422 MicE(MicE<'a>),
424 Maidenhead(Maidenhead<'a>),
426 UserDefined(UserDefined<'a>),
428 ThirdParty(ThirdParty<'a>),
430 Unsupported {
432 identifier: u8,
434 information: &'a [u8],
436 },
437 Malformed {
439 identifier: u8,
441 information: &'a [u8],
443 },
444}
445
446impl AprsData<'_> {
447 #[must_use]
449 pub fn kind_name(&self) -> &'static str {
450 match self {
451 Self::Status { .. } => "status",
452 Self::Position(_) => "position",
453 Self::TimestampedPosition(_) => "timestamped_position",
454 Self::CompressedPosition(_) => "compressed_position",
455 Self::Message(_) => "message",
456 Self::Object(_) => "object",
457 Self::Item(_) => "item",
458 Self::Weather(_) => "weather",
459 Self::Telemetry(_) => "telemetry",
460 Self::TelemetryMetadata(_) => "telemetry_metadata",
461 Self::Query(_) => "query",
462 Self::Capability(_) => "capability",
463 Self::Nmea(_) => "nmea",
464 Self::MicE(_) => "mic_e",
465 Self::Maidenhead(_) => "maidenhead",
466 Self::UserDefined(_) => "user_defined",
467 Self::ThirdParty(_) => "third_party",
468 Self::Unsupported { .. } => "unsupported",
469 Self::Malformed { .. } => "malformed",
470 }
471 }
472}
473
474fn summary_coordinates(data: AprsData<'_>) -> Option<Coordinates> {
475 match data {
476 AprsData::Position(position) => position.coordinates(),
477 AprsData::TimestampedPosition(position) => position.position.coordinates(),
478 AprsData::CompressedPosition(position) => position.coordinates(),
479 AprsData::MicE(mic_e) => mic_e.coordinates(),
480 _ => None,
481 }
482}
483
484fn summary_nmea_checksum(data: AprsData<'_>) -> Option<NmeaChecksum> {
485 match data {
486 AprsData::Nmea(nmea) => nmea.checksum(),
487 _ => None,
488 }
489}
490
491fn summary_telemetry_sequence(data: AprsData<'_>) -> Option<u16> {
492 match data {
493 AprsData::Telemetry(telemetry) => telemetry.sequence_number(),
494 _ => None,
495 }
496}
497
498fn summary_mic_e_speed_course(data: AprsData<'_>) -> Option<MicESpeedCourse> {
499 match data {
500 AprsData::MicE(mic_e) => mic_e.speed_course(),
501 _ => None,
502 }
503}
504
505#[derive(Clone, Copy, Debug, Eq, PartialEq)]
507pub struct Position<'a> {
508 pub messaging: bool,
510 pub latitude: &'a [u8],
512 pub symbol_table: u8,
514 pub longitude: &'a [u8],
516 pub symbol_code: u8,
518 pub comment: &'a [u8],
520}
521
522impl Position<'_> {
523 #[must_use]
525 pub fn coordinates(&self) -> Option<Coordinates> {
526 Some(Coordinates {
527 latitude: decode_latitude(self.latitude)?,
528 longitude: decode_longitude(self.longitude)?,
529 })
530 }
531}
532
533#[derive(Clone, Copy, Debug, PartialEq)]
535pub struct Coordinates {
536 pub latitude: f64,
538 pub longitude: f64,
540}
541
542#[derive(Clone, Copy, Debug, Eq, PartialEq)]
544pub struct TimestampedPosition<'a> {
545 pub messaging: bool,
547 pub timestamp: &'a [u8],
549 pub position: Position<'a>,
551}
552
553#[derive(Clone, Copy, Debug, Eq, PartialEq)]
555pub struct CompressedPosition<'a> {
556 pub messaging: bool,
558 pub symbol_table: u8,
560 pub compressed_latitude: &'a [u8],
562 pub compressed_longitude: &'a [u8],
564 pub symbol_code: u8,
566 pub extension: &'a [u8],
568 pub compression_type: u8,
570 pub comment: &'a [u8],
572}
573
574impl CompressedPosition<'_> {
575 #[must_use]
577 pub fn coordinates(&self) -> Option<Coordinates> {
578 let y = decode_base91(self.compressed_latitude)?;
579 let x = decode_base91(self.compressed_longitude)?;
580
581 Some(Coordinates {
582 latitude: 90.0 - (y as f64 / 380_926.0),
583 longitude: -180.0 + (x as f64 / 190_463.0),
584 })
585 }
586}
587
588#[derive(Clone, Copy, Debug, Eq, PartialEq)]
590pub struct Message<'a> {
591 pub addressee: &'a [u8],
593 pub kind: MessageKind,
595 pub text: &'a [u8],
597 pub id: Option<&'a [u8]>,
599}
600
601#[derive(Clone, Copy, Debug, Eq, PartialEq)]
603pub enum MessageKind {
604 Message,
606 Ack,
608 Reject,
610 Bulletin,
612 Announcement,
614}
615
616#[derive(Clone, Copy, Debug, Eq, PartialEq)]
618pub struct Object<'a> {
619 pub name: &'a [u8],
621 pub live: bool,
623 pub timestamp: &'a [u8],
625 pub body: &'a [u8],
627}
628
629#[derive(Clone, Copy, Debug, Eq, PartialEq)]
631pub struct Item<'a> {
632 pub name: &'a [u8],
634 pub live: bool,
636 pub body: &'a [u8],
638}
639
640#[derive(Clone, Copy, Debug, Eq, PartialEq)]
642pub struct Weather<'a> {
643 pub report: &'a [u8],
645}
646
647impl Weather<'_> {
648 #[must_use]
650 pub fn fields(&self) -> WeatherFields<'_> {
651 WeatherFields {
652 timestamp: self
653 .report
654 .get(..6)
655 .filter(|value| value.iter().all(u8::is_ascii_digit)),
656 wind_direction_degrees: parse_tagged_u16(self.report, b'c', 3),
657 wind_speed_mph: parse_tagged_u16(self.report, b's', 3),
658 wind_gust_mph: parse_tagged_u16(self.report, b'g', 3),
659 temperature_fahrenheit: parse_tagged_i16(self.report, b't', 3),
660 rain_last_hour_hundredths_inch: parse_tagged_u16(self.report, b'r', 3),
661 rain_last_24_hours_hundredths_inch: parse_tagged_u16(self.report, b'p', 3),
662 rain_since_midnight_hundredths_inch: parse_tagged_u16(self.report, b'P', 3),
663 humidity_percent: parse_tagged_u16(self.report, b'h', 2).map(|value| {
664 if value == 0 {
665 100
666 } else {
667 value
668 }
669 }),
670 pressure_tenths_hpa: parse_tagged_u16(self.report, b'b', 5),
671 luminosity_watts_per_square_meter: parse_tagged_u16(self.report, b'L', 3),
672 luminosity_1000_plus_watts_per_square_meter: parse_tagged_u16(self.report, b'l', 3)
673 .map(|value| value + 1000),
674 snow_last_24_hours_inches: parse_tagged_u16(self.report, b'S', 3),
675 raw_rain_counter: parse_tagged_u16(self.report, b'#', 3),
676 }
677 }
678}
679
680#[derive(Clone, Copy, Debug, Eq, PartialEq)]
682pub struct WeatherFields<'a> {
683 pub timestamp: Option<&'a [u8]>,
685 pub wind_direction_degrees: Option<u16>,
687 pub wind_speed_mph: Option<u16>,
689 pub wind_gust_mph: Option<u16>,
691 pub temperature_fahrenheit: Option<i16>,
693 pub rain_last_hour_hundredths_inch: Option<u16>,
695 pub rain_last_24_hours_hundredths_inch: Option<u16>,
697 pub rain_since_midnight_hundredths_inch: Option<u16>,
699 pub humidity_percent: Option<u16>,
701 pub pressure_tenths_hpa: Option<u16>,
703 pub luminosity_watts_per_square_meter: Option<u16>,
705 pub luminosity_1000_plus_watts_per_square_meter: Option<u16>,
707 pub snow_last_24_hours_inches: Option<u16>,
709 pub raw_rain_counter: Option<u16>,
711}
712
713#[derive(Clone, Copy, Debug, Eq, PartialEq)]
715pub struct Telemetry<'a> {
716 pub sequence: &'a [u8],
718 pub analog: [&'a [u8]; 5],
720 pub digital: Option<&'a [u8]>,
722}
723
724impl Telemetry<'_> {
725 #[must_use]
727 pub fn sequence_number(&self) -> Option<u16> {
728 parse_u16(self.sequence)
729 }
730
731 #[must_use]
733 pub fn analog_values(&self) -> Option<[u16; 5]> {
734 Some([
735 parse_u16(self.analog[0])?,
736 parse_u16(self.analog[1])?,
737 parse_u16(self.analog[2])?,
738 parse_u16(self.analog[3])?,
739 parse_u16(self.analog[4])?,
740 ])
741 }
742
743 #[must_use]
745 pub fn digital_bits(&self) -> Option<[bool; 8]> {
746 let digital = self.digital?;
747 if digital.len() != 8 {
748 return None;
749 }
750
751 let mut bits = [false; 8];
752 for (index, byte) in digital.iter().enumerate() {
753 bits[index] = match byte {
754 b'0' => false,
755 b'1' => true,
756 _ => return None,
757 };
758 }
759
760 Some(bits)
761 }
762}
763
764#[derive(Clone, Copy, Debug, Eq, PartialEq)]
766pub struct TelemetryMetadata<'a> {
767 pub addressee: &'a [u8],
769 pub kind: TelemetryMetadataKind,
771 pub body: &'a [u8],
773}
774
775impl<'a> TelemetryMetadata<'a> {
776 #[must_use]
778 pub fn fields(&self) -> Vec<&'a [u8]> {
779 self.body.split(|byte| *byte == b',').collect()
780 }
781}
782
783#[derive(Clone, Copy, Debug, Eq, PartialEq)]
785pub enum TelemetryMetadataKind {
786 ParameterNames,
788 Units,
790 Equations,
792 BitSense,
794}
795
796#[derive(Clone, Copy, Debug, Eq, PartialEq)]
798pub struct Query<'a> {
799 pub query: &'a [u8],
801}
802
803#[derive(Clone, Copy, Debug, Eq, PartialEq)]
805pub struct Capability<'a> {
806 pub body: &'a [u8],
808}
809
810#[derive(Clone, Copy, Debug, Eq, PartialEq)]
812pub struct Nmea<'a> {
813 pub sentence: &'a [u8],
815}
816
817impl Nmea<'_> {
818 #[must_use]
820 pub fn checksum(&self) -> Option<NmeaChecksum> {
821 let separator = self.sentence.iter().rposition(|byte| *byte == b'*')?;
822 let checksum = self.sentence.get(separator + 1..separator + 3)?;
823 if checksum.len() != 2 || self.sentence.get(separator + 3).is_some() {
824 return None;
825 }
826
827 let expected = parse_hex_byte(checksum)?;
828 let calculated = self.sentence[..separator]
829 .iter()
830 .fold(0u8, |accumulator, byte| accumulator ^ byte);
831
832 Some(NmeaChecksum {
833 expected,
834 calculated,
835 valid: expected == calculated,
836 })
837 }
838}
839
840#[derive(Clone, Copy, Debug, Eq, PartialEq)]
842pub struct NmeaChecksum {
843 pub expected: u8,
845 pub calculated: u8,
847 pub valid: bool,
849}
850
851#[derive(Clone, Copy, Debug, Eq, PartialEq)]
853pub struct MicE<'a> {
854 pub identifier: u8,
856 pub destination: &'a [u8],
858 pub body: &'a [u8],
860 pub status: Option<MicEStatus>,
862 pub latitude_digits: Option<[u8; 6]>,
864}
865
866impl MicE<'_> {
867 #[must_use]
869 pub fn coordinates(&self) -> Option<Coordinates> {
870 Some(Coordinates {
871 latitude: decode_mic_e_latitude(self.destination)?,
872 longitude: decode_mic_e_longitude(self.destination, self.body)?,
873 })
874 }
875
876 #[must_use]
878 pub fn speed_course(&self) -> Option<MicESpeedCourse> {
879 decode_mic_e_speed_course(self.body)
880 }
881}
882
883#[derive(Clone, Copy, Debug, Eq, PartialEq)]
885pub enum MicEStatus {
886 Custom([bool; 3]),
888}
889
890#[derive(Clone, Copy, Debug, Eq, PartialEq)]
892pub struct MicESpeedCourse {
893 pub speed_knots: u16,
895 pub course_degrees: u16,
897}
898
899#[derive(Clone, Copy, Debug, Eq, PartialEq)]
901pub struct Maidenhead<'a> {
902 pub locator: &'a [u8],
904 pub comment: &'a [u8],
906}
907
908#[derive(Clone, Copy, Debug, Eq, PartialEq)]
910pub struct UserDefined<'a> {
911 pub user_id: u8,
913 pub packet_type: u8,
915 pub body: &'a [u8],
917}
918
919#[derive(Clone, Copy, Debug, Eq, PartialEq)]
921pub struct ThirdParty<'a> {
922 pub body: &'a [u8],
924}
925
926impl ThirdParty<'_> {
927 pub fn nested_packet(&self) -> Result<ParsedPacket, ParseError> {
929 parse_packet(self.body)
930 }
931}
932
933#[derive(Clone, Copy, Debug, Eq, PartialEq)]
935pub enum DataTypeIdentifier {
936 PositionNoTimestamp,
938 PositionNoTimestampMessaging,
940 PositionWithTimestamp,
942 PositionWithTimestampMessaging,
944 Status,
946 Query,
948 Capability,
950 Message,
952 Object,
954 Item,
956 Weather,
958 Telemetry,
960 Nmea,
962 MicECurrent,
964 MicEOld,
966 Maidenhead,
968 UserDefined,
970 ThirdParty,
972 Unknown(u8),
974}
975
976impl DataTypeIdentifier {
977 fn from_byte(byte: u8) -> Self {
978 match byte {
979 b'!' => Self::PositionNoTimestamp,
980 b'=' => Self::PositionNoTimestampMessaging,
981 b'/' => Self::PositionWithTimestamp,
982 b'@' => Self::PositionWithTimestampMessaging,
983 b'>' => Self::Status,
984 b'?' => Self::Query,
985 b'<' => Self::Capability,
986 b':' => Self::Message,
987 b';' => Self::Object,
988 b')' => Self::Item,
989 b'_' => Self::Weather,
990 b'T' => Self::Telemetry,
991 b'$' => Self::Nmea,
992 b'`' => Self::MicECurrent,
993 b'\'' => Self::MicEOld,
994 b'[' => Self::Maidenhead,
995 b'{' => Self::UserDefined,
996 b'}' => Self::ThirdParty,
997 other => Self::Unknown(other),
998 }
999 }
1000
1001 fn as_byte(self) -> u8 {
1002 match self {
1003 Self::PositionNoTimestamp => b'!',
1004 Self::PositionNoTimestampMessaging => b'=',
1005 Self::PositionWithTimestamp => b'/',
1006 Self::PositionWithTimestampMessaging => b'@',
1007 Self::Status => b'>',
1008 Self::Query => b'?',
1009 Self::Capability => b'<',
1010 Self::Message => b':',
1011 Self::Object => b';',
1012 Self::Item => b')',
1013 Self::Weather => b'_',
1014 Self::Telemetry => b'T',
1015 Self::Nmea => b'$',
1016 Self::MicECurrent => b'`',
1017 Self::MicEOld => b'\'',
1018 Self::Maidenhead => b'[',
1019 Self::UserDefined => b'{',
1020 Self::ThirdParty => b'}',
1021 Self::Unknown(value) => value,
1022 }
1023 }
1024
1025 #[must_use]
1027 pub fn name(self) -> &'static str {
1028 match self {
1029 Self::PositionNoTimestamp => "position_no_timestamp",
1030 Self::PositionNoTimestampMessaging => "position_no_timestamp_messaging",
1031 Self::PositionWithTimestamp => "position_with_timestamp",
1032 Self::PositionWithTimestampMessaging => "position_with_timestamp_messaging",
1033 Self::Status => "status",
1034 Self::Query => "query",
1035 Self::Capability => "capability",
1036 Self::Message => "message",
1037 Self::Object => "object",
1038 Self::Item => "item",
1039 Self::Weather => "weather",
1040 Self::Telemetry => "telemetry",
1041 Self::Nmea => "nmea",
1042 Self::MicECurrent => "mic_e_current",
1043 Self::MicEOld => "mic_e_old",
1044 Self::Maidenhead => "maidenhead",
1045 Self::UserDefined => "user_defined",
1046 Self::ThirdParty => "third_party",
1047 Self::Unknown(_) => "unknown",
1048 }
1049 }
1050}
1051
1052fn parse_aprs_data<'a>(
1053 identifier: DataTypeIdentifier,
1054 information: &'a [u8],
1055 destination: &'a [u8],
1056) -> AprsData<'a> {
1057 match identifier {
1058 DataTypeIdentifier::Status => AprsData::Status { text: information },
1059 DataTypeIdentifier::PositionNoTimestamp => parse_position(false, b'!', information),
1060 DataTypeIdentifier::PositionNoTimestampMessaging => parse_position(true, b'=', information),
1061 DataTypeIdentifier::PositionWithTimestamp => {
1062 parse_timestamped_position(false, b'/', information)
1063 }
1064 DataTypeIdentifier::PositionWithTimestampMessaging => {
1065 parse_timestamped_position(true, b'@', information)
1066 }
1067 DataTypeIdentifier::Message => parse_message(information),
1068 DataTypeIdentifier::Object => parse_object(information),
1069 DataTypeIdentifier::Item => parse_item(information),
1070 DataTypeIdentifier::Weather => AprsData::Weather(Weather {
1071 report: information,
1072 }),
1073 DataTypeIdentifier::Telemetry => parse_telemetry(information),
1074 DataTypeIdentifier::Query => AprsData::Query(Query { query: information }),
1075 DataTypeIdentifier::Capability => AprsData::Capability(Capability { body: information }),
1076 DataTypeIdentifier::Nmea => AprsData::Nmea(Nmea {
1077 sentence: information,
1078 }),
1079 DataTypeIdentifier::MicECurrent | DataTypeIdentifier::MicEOld => {
1080 parse_mic_e(identifier, information, destination)
1081 }
1082 DataTypeIdentifier::Maidenhead => parse_maidenhead(information),
1083 DataTypeIdentifier::UserDefined => parse_user_defined(information),
1084 DataTypeIdentifier::ThirdParty => AprsData::ThirdParty(ThirdParty { body: information }),
1085 other => AprsData::Unsupported {
1086 identifier: other.as_byte(),
1087 information,
1088 },
1089 }
1090}
1091
1092fn parse_mic_e<'a>(
1093 identifier: DataTypeIdentifier,
1094 information: &'a [u8],
1095 destination: &'a [u8],
1096) -> AprsData<'a> {
1097 AprsData::MicE(MicE {
1098 identifier: identifier.as_byte(),
1099 destination,
1100 body: information,
1101 status: decode_mic_e_status(destination),
1102 latitude_digits: decode_mic_e_latitude_digits(destination),
1103 })
1104}
1105
1106fn parse_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1107 if is_compressed_position(information) {
1108 return parse_compressed_position(messaging, identifier, information);
1109 }
1110
1111 if information.len() < 18 {
1112 return AprsData::Malformed {
1113 identifier,
1114 information,
1115 };
1116 }
1117
1118 let latitude = &information[..8];
1119 let symbol_table = information[8];
1120 let longitude = &information[9..18];
1121 let symbol_code = information[18];
1122 let comment = &information[19..];
1123
1124 if !is_latitude(latitude)
1125 || !is_symbol_table_identifier(symbol_table)
1126 || !is_longitude(longitude)
1127 || !is_printable_ascii(symbol_code)
1128 {
1129 return AprsData::Malformed {
1130 identifier,
1131 information,
1132 };
1133 }
1134
1135 AprsData::Position(Position {
1136 messaging,
1137 latitude,
1138 symbol_table,
1139 longitude,
1140 symbol_code,
1141 comment,
1142 })
1143}
1144
1145fn parse_timestamped_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1146 if information.len() < 8 {
1147 return AprsData::Malformed {
1148 identifier,
1149 information,
1150 };
1151 }
1152
1153 let timestamp = &information[..7];
1154 if !is_timestamp(timestamp) {
1155 return AprsData::Malformed {
1156 identifier,
1157 information,
1158 };
1159 }
1160
1161 match parse_position(messaging, identifier, &information[7..]) {
1162 AprsData::Position(position) => AprsData::TimestampedPosition(TimestampedPosition {
1163 messaging,
1164 timestamp,
1165 position,
1166 }),
1167 AprsData::CompressedPosition(position) => AprsData::CompressedPosition(position),
1168 _ => AprsData::Malformed {
1169 identifier,
1170 information,
1171 },
1172 }
1173}
1174
1175fn parse_compressed_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1176 if information.len() < 13 {
1177 return AprsData::Malformed {
1178 identifier,
1179 information,
1180 };
1181 }
1182
1183 let symbol_table = information[0];
1184 let compressed_latitude = &information[1..5];
1185 let compressed_longitude = &information[5..9];
1186 let symbol_code = information[9];
1187 let extension = &information[10..12];
1188 let compression_type = information[12];
1189 let comment = &information[13..];
1190
1191 if !is_symbol_table_identifier(symbol_table)
1192 || !compressed_latitude.iter().all(|byte| is_base91(*byte))
1193 || !compressed_longitude.iter().all(|byte| is_base91(*byte))
1194 || !is_printable_ascii(symbol_code)
1195 || !extension.iter().all(|byte| is_base91(*byte))
1196 || !is_base91(compression_type)
1197 {
1198 return AprsData::Malformed {
1199 identifier,
1200 information,
1201 };
1202 }
1203
1204 AprsData::CompressedPosition(CompressedPosition {
1205 messaging,
1206 symbol_table,
1207 compressed_latitude,
1208 compressed_longitude,
1209 symbol_code,
1210 extension,
1211 compression_type,
1212 comment,
1213 })
1214}
1215
1216fn parse_object(information: &[u8]) -> AprsData<'_> {
1217 if information.len() < 17 || !matches!(information[9], b'*' | b'_') {
1218 return AprsData::Malformed {
1219 identifier: b';',
1220 information,
1221 };
1222 }
1223
1224 AprsData::Object(Object {
1225 name: &information[..9],
1226 live: information[9] == b'*',
1227 timestamp: &information[10..17],
1228 body: &information[17..],
1229 })
1230}
1231
1232fn parse_item(information: &[u8]) -> AprsData<'_> {
1233 let Some(separator) = information
1234 .iter()
1235 .position(|byte| matches!(*byte, b'!' | b'_'))
1236 else {
1237 return AprsData::Malformed {
1238 identifier: b')',
1239 information,
1240 };
1241 };
1242
1243 if separator == 0 || separator > 9 {
1244 return AprsData::Malformed {
1245 identifier: b')',
1246 information,
1247 };
1248 }
1249
1250 AprsData::Item(Item {
1251 name: &information[..separator],
1252 live: information[separator] == b'!',
1253 body: &information[separator + 1..],
1254 })
1255}
1256
1257fn parse_message(information: &[u8]) -> AprsData<'_> {
1258 if information.len() < 10 || information[9] != b':' {
1259 return AprsData::Malformed {
1260 identifier: b':',
1261 information,
1262 };
1263 }
1264
1265 let addressee = &information[..9];
1266 let body = &information[10..];
1267 if let Some(kind) = classify_telemetry_metadata_kind(addressee) {
1268 return AprsData::TelemetryMetadata(TelemetryMetadata {
1269 addressee,
1270 kind,
1271 body,
1272 });
1273 }
1274
1275 let (text, id) = match body.iter().position(|byte| *byte == b'{') {
1276 Some(separator) => (&body[..separator], Some(&body[separator + 1..])),
1277 None => (body, None),
1278 };
1279 let kind = classify_message_kind(addressee, text);
1280
1281 AprsData::Message(Message {
1282 addressee,
1283 kind,
1284 text,
1285 id,
1286 })
1287}
1288
1289fn parse_telemetry(information: &[u8]) -> AprsData<'_> {
1290 if !information.starts_with(b"#") {
1291 return AprsData::Malformed {
1292 identifier: b'T',
1293 information,
1294 };
1295 }
1296
1297 let fields: Vec<&[u8]> = information[1..].split(|byte| *byte == b',').collect();
1298 if fields.len() < 6 || fields[..6].iter().any(|field| field.is_empty()) {
1299 return AprsData::Malformed {
1300 identifier: b'T',
1301 information,
1302 };
1303 }
1304
1305 AprsData::Telemetry(Telemetry {
1306 sequence: fields[0],
1307 analog: [fields[1], fields[2], fields[3], fields[4], fields[5]],
1308 digital: fields.get(6).copied().filter(|field| !field.is_empty()),
1309 })
1310}
1311
1312fn parse_maidenhead(information: &[u8]) -> AprsData<'_> {
1313 if information.len() < 6 {
1314 return AprsData::Malformed {
1315 identifier: b'[',
1316 information,
1317 };
1318 }
1319
1320 AprsData::Maidenhead(Maidenhead {
1321 locator: &information[..6],
1322 comment: &information[6..],
1323 })
1324}
1325
1326fn parse_user_defined(information: &[u8]) -> AprsData<'_> {
1327 if information.len() < 2 {
1328 return AprsData::Malformed {
1329 identifier: b'{',
1330 information,
1331 };
1332 }
1333
1334 AprsData::UserDefined(UserDefined {
1335 user_id: information[0],
1336 packet_type: information[1],
1337 body: &information[2..],
1338 })
1339}
1340
1341fn classify_telemetry_metadata_kind(addressee: &[u8]) -> Option<TelemetryMetadataKind> {
1342 match addressee.get(..5)? {
1343 b"PARM." => Some(TelemetryMetadataKind::ParameterNames),
1344 b"UNIT." => Some(TelemetryMetadataKind::Units),
1345 b"EQNS." => Some(TelemetryMetadataKind::Equations),
1346 b"BITS." => Some(TelemetryMetadataKind::BitSense),
1347 _ => None,
1348 }
1349}
1350
1351fn classify_message_kind(addressee: &[u8], text: &[u8]) -> MessageKind {
1352 if text.starts_with(b"ack") {
1353 MessageKind::Ack
1354 } else if text.starts_with(b"rej") {
1355 MessageKind::Reject
1356 } else if addressee.starts_with(b"BLN") && addressee.get(3).is_some_and(u8::is_ascii_digit) {
1357 MessageKind::Bulletin
1358 } else if addressee.starts_with(b"BLN") && addressee.get(3).is_some_and(u8::is_ascii_uppercase)
1359 {
1360 MessageKind::Announcement
1361 } else {
1362 MessageKind::Message
1363 }
1364}
1365
1366fn is_latitude(value: &[u8]) -> bool {
1367 value.len() == 8
1368 && value[0].is_ascii_digit()
1369 && value[1].is_ascii_digit()
1370 && value[2].is_ascii_digit()
1371 && value[3].is_ascii_digit()
1372 && value[4] == b'.'
1373 && value[5].is_ascii_digit()
1374 && value[6].is_ascii_digit()
1375 && matches!(value[7], b'N' | b'S')
1376}
1377
1378fn is_longitude(value: &[u8]) -> bool {
1379 value.len() == 9
1380 && value[0].is_ascii_digit()
1381 && value[1].is_ascii_digit()
1382 && value[2].is_ascii_digit()
1383 && value[3].is_ascii_digit()
1384 && value[4].is_ascii_digit()
1385 && value[5] == b'.'
1386 && value[6].is_ascii_digit()
1387 && value[7].is_ascii_digit()
1388 && matches!(value[8], b'E' | b'W')
1389}
1390
1391fn is_symbol_table_identifier(value: u8) -> bool {
1392 matches!(value, b'/' | b'\\') || value.is_ascii_alphanumeric()
1393}
1394
1395fn is_printable_ascii(value: u8) -> bool {
1396 (0x20..=0x7e).contains(&value)
1397}
1398
1399fn is_base91(value: u8) -> bool {
1400 (b'!'..=b'{').contains(&value)
1401}
1402
1403fn is_compressed_position(information: &[u8]) -> bool {
1404 information
1405 .first()
1406 .is_some_and(|byte| !byte.is_ascii_digit() && is_symbol_table_identifier(*byte))
1407 && information
1408 .get(1..13)
1409 .is_some_and(|bytes| bytes.iter().all(|byte| is_base91(*byte)))
1410}
1411
1412fn is_timestamp(value: &[u8]) -> bool {
1413 value.len() == 7
1414 && value[..6].iter().all(u8::is_ascii_digit)
1415 && matches!(value[6], b'z' | b'/' | b'h')
1416}
1417
1418fn decode_latitude(value: &[u8]) -> Option<f64> {
1419 if !is_latitude(value) {
1420 return None;
1421 }
1422
1423 let degrees = parse_u16(&value[..2])? as f64;
1424 let minutes = parse_fixed_minutes(&value[2..7])?;
1425 let sign = match value[7] {
1426 b'N' => 1.0,
1427 b'S' => -1.0,
1428 _ => return None,
1429 };
1430
1431 Some(sign * (degrees + minutes / 60.0))
1432}
1433
1434fn decode_longitude(value: &[u8]) -> Option<f64> {
1435 if !is_longitude(value) {
1436 return None;
1437 }
1438
1439 let degrees = parse_u16(&value[..3])? as f64;
1440 let minutes = parse_fixed_minutes(&value[3..8])?;
1441 let sign = match value[8] {
1442 b'E' => 1.0,
1443 b'W' => -1.0,
1444 _ => return None,
1445 };
1446
1447 Some(sign * (degrees + minutes / 60.0))
1448}
1449
1450fn parse_fixed_minutes(value: &[u8]) -> Option<f64> {
1451 if value.len() != 5 || value[2] != b'.' || !value[..2].iter().all(u8::is_ascii_digit) {
1452 return None;
1453 }
1454
1455 let whole = parse_u16(&value[..2])? as f64;
1456 let fraction = parse_u16(&value[3..])? as f64 / 100.0;
1457 Some(whole + fraction)
1458}
1459
1460fn decode_base91(value: &[u8]) -> Option<u32> {
1461 if value.len() != 4 || !value.iter().all(|byte| is_base91(*byte)) {
1462 return None;
1463 }
1464
1465 let mut decoded = 0u32;
1466 for byte in value {
1467 decoded = decoded * 91 + u32::from(byte - b'!');
1468 }
1469
1470 Some(decoded)
1471}
1472
1473fn parse_u16(value: &[u8]) -> Option<u16> {
1474 if value.is_empty() || !value.iter().all(u8::is_ascii_digit) {
1475 return None;
1476 }
1477
1478 let mut parsed = 0u16;
1479 for digit in value {
1480 parsed = parsed.checked_mul(10)?;
1481 parsed = parsed.checked_add(u16::from(digit - b'0'))?;
1482 }
1483
1484 Some(parsed)
1485}
1486
1487fn parse_i16(value: &[u8]) -> Option<i16> {
1488 if value.is_empty() {
1489 return None;
1490 }
1491
1492 let (sign, digits) = match value[0] {
1493 b'-' => (-1, &value[1..]),
1494 b'+' => (1, &value[1..]),
1495 _ => (1, value),
1496 };
1497
1498 let unsigned = parse_u16(digits)?;
1499 i16::try_from(unsigned).ok()?.checked_mul(sign)
1500}
1501
1502fn parse_hex_byte(value: &[u8]) -> Option<u8> {
1503 if value.len() != 2 {
1504 return None;
1505 }
1506
1507 Some(hex_value(value[0])? * 16 + hex_value(value[1])?)
1508}
1509
1510fn hex_value(value: u8) -> Option<u8> {
1511 match value {
1512 b'0'..=b'9' => Some(value - b'0'),
1513 b'A'..=b'F' => Some(value - b'A' + 10),
1514 b'a'..=b'f' => Some(value - b'a' + 10),
1515 _ => None,
1516 }
1517}
1518
1519fn parse_tagged_u16(report: &[u8], tag: u8, width: usize) -> Option<u16> {
1520 parse_tagged(report, tag, width).and_then(parse_u16)
1521}
1522
1523fn parse_tagged_i16(report: &[u8], tag: u8, width: usize) -> Option<i16> {
1524 parse_tagged(report, tag, width).and_then(parse_i16)
1525}
1526
1527fn parse_tagged(report: &[u8], tag: u8, width: usize) -> Option<&[u8]> {
1528 let start = report.iter().position(|byte| *byte == tag)? + 1;
1529 report.get(start..start + width)
1530}
1531
1532fn decode_mic_e_status(destination: &[u8]) -> Option<MicEStatus> {
1533 if destination.len() != 6 {
1534 return None;
1535 }
1536
1537 let bytes = destination.get(..3)?;
1538 Some(MicEStatus::Custom([
1539 mic_e_status_bit(bytes[0])?,
1540 mic_e_status_bit(bytes[1])?,
1541 mic_e_status_bit(bytes[2])?,
1542 ]))
1543}
1544
1545fn mic_e_status_bit(byte: u8) -> Option<bool> {
1546 match byte {
1547 b'0'..=b'9' | b'L' => Some(false),
1548 b'A'..=b'K' | b'P'..=b'Z' => Some(true),
1549 _ => None,
1550 }
1551}
1552
1553fn decode_mic_e_latitude_digits(destination: &[u8]) -> Option<[u8; 6]> {
1554 if destination.len() != 6 {
1555 return None;
1556 }
1557
1558 let mut digits = [0u8; 6];
1559 for (index, byte) in destination.iter().copied().enumerate() {
1560 digits[index] = mic_e_latitude_digit(byte)?;
1561 }
1562
1563 Some(digits)
1564}
1565
1566fn mic_e_latitude_digit(byte: u8) -> Option<u8> {
1567 match byte {
1568 b'0'..=b'9' => Some(byte - b'0'),
1569 b'A'..=b'J' => Some(byte - b'A'),
1570 b'P'..=b'Y' => Some(byte - b'P'),
1571 b'K' | b'L' | b'Z' => Some(0),
1572 _ => None,
1573 }
1574}
1575
1576fn decode_mic_e_latitude(destination: &[u8]) -> Option<f64> {
1577 let digits = decode_mic_e_latitude_digits(destination)?;
1578 let degrees = u16::from(digits[0]) * 10 + u16::from(digits[1]);
1579 let minutes = u16::from(digits[2]) * 10 + u16::from(digits[3]);
1580 let hundredths = u16::from(digits[4]) * 10 + u16::from(digits[5]);
1581 if degrees > 90 || minutes > 59 {
1582 return None;
1583 }
1584
1585 let sign = if mic_e_north(destination[3])? {
1586 1.0
1587 } else {
1588 -1.0
1589 };
1590 Some(sign * (f64::from(degrees) + (f64::from(minutes) + f64::from(hundredths) / 100.0) / 60.0))
1591}
1592
1593fn decode_mic_e_longitude(destination: &[u8], body: &[u8]) -> Option<f64> {
1594 if destination.len() != 6 || body.len() < 3 {
1595 return None;
1596 }
1597
1598 let mut degrees = i16::from(mic_e_body_value(body[0])?);
1599 if mic_e_longitude_offset(destination[4])? {
1600 degrees += 100;
1601 }
1602 if (180..=189).contains(°rees) {
1603 degrees -= 80;
1604 } else if (190..=199).contains(°rees) {
1605 degrees -= 190;
1606 }
1607
1608 let minutes = mic_e_body_value(body[1])?;
1609 let hundredths = mic_e_body_value(body[2])?;
1610 if !(0..=179).contains(°rees) || minutes > 59 || hundredths > 99 {
1611 return None;
1612 }
1613
1614 let sign = if mic_e_west(destination[5])? {
1615 -1.0
1616 } else {
1617 1.0
1618 };
1619 Some(sign * (f64::from(degrees) + (f64::from(minutes) + f64::from(hundredths) / 100.0) / 60.0))
1620}
1621
1622fn decode_mic_e_speed_course(body: &[u8]) -> Option<MicESpeedCourse> {
1623 if body.len() < 6 {
1624 return None;
1625 }
1626
1627 let speed_tens = u16::from(mic_e_body_value(body[3])?);
1628 let speed_units_course_hundreds = u16::from(mic_e_body_value(body[4])?);
1629 let course_remainder = u16::from(mic_e_body_value(body[5])?);
1630 let mut speed_knots = speed_tens * 10 + speed_units_course_hundreds / 10;
1631 if speed_knots >= 800 {
1632 speed_knots -= 800;
1633 }
1634
1635 Some(MicESpeedCourse {
1636 speed_knots,
1637 course_degrees: (speed_units_course_hundreds % 10) * 100 + course_remainder,
1638 })
1639}
1640
1641fn mic_e_body_value(byte: u8) -> Option<u8> {
1642 let value = byte.checked_sub(28)?;
1643 (value <= 99).then_some(value)
1644}
1645
1646fn mic_e_north(byte: u8) -> Option<bool> {
1647 match byte {
1648 b'0'..=b'9' | b'A'..=b'L' => Some(false),
1649 b'P'..=b'Z' => Some(true),
1650 _ => None,
1651 }
1652}
1653
1654fn mic_e_longitude_offset(byte: u8) -> Option<bool> {
1655 match byte {
1656 b'0'..=b'9' | b'A'..=b'L' => Some(false),
1657 b'P'..=b'Z' => Some(true),
1658 _ => None,
1659 }
1660}
1661
1662fn mic_e_west(byte: u8) -> Option<bool> {
1663 match byte {
1664 b'0'..=b'9' | b'A'..=b'L' => Some(false),
1665 b'P'..=b'Z' => Some(true),
1666 _ => None,
1667 }
1668}
1669
1670#[derive(Clone, Debug, Eq, PartialEq)]
1672pub enum ParseError {
1673 Empty,
1675 Oversized,
1677 MissingSeparator,
1679 EmptySegment,
1681 InvalidAddress,
1683}
1684
1685impl ParseError {
1686 #[must_use]
1688 pub fn code(&self) -> &'static str {
1689 match self {
1690 Self::Empty => "parse.empty",
1691 Self::Oversized => "parse.oversized",
1692 Self::MissingSeparator => "parse.missing_separator",
1693 Self::EmptySegment => "parse.empty_segment",
1694 Self::InvalidAddress => "parse.invalid_address",
1695 }
1696 }
1697}
1698
1699pub fn parse_packet(input: &[u8]) -> Result<ParsedPacket, ParseError> {
1705 parse_packet_with_options(input, ParseOptions::default())
1706}
1707
1708pub fn parse_packet_with_options(
1710 input: &[u8],
1711 options: ParseOptions,
1712) -> Result<ParsedPacket, ParseError> {
1713 if input.is_empty() {
1714 return Err(ParseError::Empty);
1715 }
1716
1717 if input.len() > options.max_packet_len {
1718 return Err(ParseError::Oversized);
1719 }
1720
1721 let source_end = input
1722 .iter()
1723 .position(|byte| *byte == b'>')
1724 .ok_or(ParseError::MissingSeparator)?;
1725 let payload_separator = input[source_end + 1..]
1726 .iter()
1727 .position(|byte| *byte == b':')
1728 .map(|offset| source_end + 1 + offset)
1729 .ok_or(ParseError::MissingSeparator)?;
1730
1731 let path_start = source_end + 1;
1732 let path_end = payload_separator;
1733 let payload_start = payload_separator + 1;
1734
1735 if source_end == 0 || path_start == path_end || payload_start == input.len() {
1736 return Err(ParseError::EmptySegment);
1737 }
1738
1739 let Some(path_components) = path_component_ranges(input, path_start, path_end) else {
1740 return Err(ParseError::InvalidAddress);
1741 };
1742
1743 if !is_ax25_like_source(&input[..source_end])
1744 || !path_components
1745 .iter()
1746 .all(|(start, end)| is_ax25_like_path_component(&input[*start..*end]))
1747 {
1748 return Err(ParseError::InvalidAddress);
1749 }
1750
1751 Ok(ParsedPacket {
1752 raw: RawPacket {
1753 bytes: input.to_vec(),
1754 },
1755 source_end,
1756 path_start,
1757 path_end,
1758 path_components,
1759 payload_start,
1760 })
1761}
1762
1763fn path_component_ranges(
1764 input: &[u8],
1765 path_start: usize,
1766 path_end: usize,
1767) -> Option<Vec<(usize, usize)>> {
1768 let mut components = Vec::new();
1769 let mut component_start = path_start;
1770
1771 for (offset, byte) in input[path_start..path_end].iter().enumerate() {
1772 if *byte == b',' {
1773 let index = path_start + offset;
1774 if component_start == index {
1775 return None;
1776 }
1777 components.push((component_start, index));
1778 component_start = index + 1;
1779 }
1780 }
1781
1782 if component_start == path_end {
1783 return None;
1784 }
1785
1786 components.push((component_start, path_end));
1787 Some(components)
1788}
1789
1790fn is_ax25_like_source(source: &[u8]) -> bool {
1791 is_ax25_like_address(source, false)
1792}
1793
1794fn is_ax25_like_path_component(component: &[u8]) -> bool {
1795 is_ax25_like_address(component, true)
1796}
1797
1798fn is_ax25_like_address(address: &[u8], allow_repeated_marker: bool) -> bool {
1799 let address = if allow_repeated_marker {
1800 address.strip_suffix(b"*").unwrap_or(address)
1801 } else {
1802 address
1803 };
1804
1805 if address.is_empty() || address.contains(&b'*') {
1806 return false;
1807 }
1808
1809 let (callsign, ssid) = match address.iter().position(|byte| *byte == b'-') {
1810 Some(separator) => (&address[..separator], Some(&address[separator + 1..])),
1811 None => (address, None),
1812 };
1813
1814 is_ax25_like_callsign(callsign) && ssid.map_or(true, is_ax25_like_ssid)
1815}
1816
1817fn is_ax25_like_callsign(callsign: &[u8]) -> bool {
1818 (1..=6).contains(&callsign.len())
1819 && callsign
1820 .iter()
1821 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit())
1822}
1823
1824fn is_ax25_like_ssid(ssid: &[u8]) -> bool {
1825 if ssid.is_empty() || ssid.len() > 2 || !ssid.iter().all(u8::is_ascii_digit) {
1826 return false;
1827 }
1828
1829 let mut value = 0u8;
1830 for digit in ssid {
1831 value = value * 10 + (digit - b'0');
1832 }
1833
1834 value <= 15
1835}