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 summary(&self) -> PacketSummary<'_> {
148 PacketSummary::from_packet(self)
149 }
150
151 #[must_use]
153 pub fn to_json(&self) -> String {
154 diagnostic::packet_to_json(self)
155 }
156}
157
158#[derive(Clone, Copy, Debug, PartialEq)]
160pub struct PacketSummary<'a> {
161 pub source: &'a [u8],
163 pub destination: &'a [u8],
165 pub data_type: &'static str,
167 pub semantic: &'static str,
169 pub coordinates: Option<Coordinates>,
171 pub nmea_checksum: Option<NmeaChecksum>,
173 pub telemetry_sequence: Option<u16>,
175 pub mic_e_speed_course: Option<MicESpeedCourse>,
177}
178
179impl<'a> PacketSummary<'a> {
180 fn from_packet(packet: &'a ParsedPacket) -> Self {
181 let data = packet.aprs_data();
182 Self {
183 source: packet.source(),
184 destination: packet.destination(),
185 data_type: packet.data_type_identifier().name(),
186 semantic: data.kind_name(),
187 coordinates: summary_coordinates(data),
188 nmea_checksum: summary_nmea_checksum(data),
189 telemetry_sequence: summary_telemetry_sequence(data),
190 mic_e_speed_course: summary_mic_e_speed_course(data),
191 }
192 }
193}
194
195#[derive(Clone, Debug, Eq, PartialEq)]
197pub struct Engine {
198 policy: Policy,
199 counters: Counters,
200}
201
202impl Engine {
203 #[must_use]
205 pub fn new(policy: Policy) -> Self {
206 Self {
207 policy,
208 counters: Counters::default(),
209 }
210 }
211
212 pub fn process(&mut self, input: &[u8]) -> EngineResult {
214 match parse_packet(input) {
215 Ok(packet) => {
216 let semantic = packet.aprs_data();
217 match self.policy.evaluate(&packet, &semantic) {
218 PolicyDecision::Accept => {
219 self.counters.accepted = self.counters.accepted.saturating_add(1);
220 EngineResult::Accepted { packet }
221 }
222 PolicyDecision::Reject(reason) => {
223 self.counters.rejected = self.counters.rejected.saturating_add(1);
224 EngineResult::Rejected { packet, reason }
225 }
226 }
227 }
228 Err(error) => {
229 self.counters.malformed = self.counters.malformed.saturating_add(1);
230 EngineResult::ParseError(error)
231 }
232 }
233 }
234
235 #[must_use]
237 pub fn counters(&self) -> Counters {
238 self.counters
239 }
240}
241
242impl Default for Engine {
243 fn default() -> Self {
244 Self::new(Policy::default())
245 }
246}
247
248#[derive(Clone, Debug, PartialEq)]
250pub enum EngineResult {
251 Accepted {
253 packet: ParsedPacket,
255 },
256 Rejected {
258 packet: ParsedPacket,
260 reason: PolicyRejection,
262 },
263 ParseError(ParseError),
265}
266
267#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
269pub struct Counters {
270 pub accepted: u64,
272 pub rejected: u64,
274 pub malformed: u64,
276}
277
278#[derive(Clone, Debug, Eq, PartialEq)]
280pub struct Policy {
281 pub allow_unsupported: bool,
283 pub allow_malformed_semantics: bool,
285 pub max_path_components: usize,
287}
288
289impl Policy {
290 #[must_use]
292 pub fn strict() -> Self {
293 Self::default()
294 }
295
296 #[must_use]
298 pub fn permissive() -> Self {
299 Self {
300 allow_unsupported: true,
301 allow_malformed_semantics: true,
302 max_path_components: 9,
303 }
304 }
305
306 #[must_use]
308 pub fn evaluate(&self, packet: &ParsedPacket, semantic: &AprsData<'_>) -> PolicyDecision {
309 if packet.path_components.len() > self.max_path_components {
310 return PolicyDecision::Reject(PolicyRejection::PathTooLong);
311 }
312
313 match semantic {
314 AprsData::Malformed { .. } if !self.allow_malformed_semantics => {
315 PolicyDecision::Reject(PolicyRejection::MalformedSemantics)
316 }
317 AprsData::Unsupported { .. } if !self.allow_unsupported => {
318 PolicyDecision::Reject(PolicyRejection::UnsupportedSemantics)
319 }
320 _ => PolicyDecision::Accept,
321 }
322 }
323}
324
325impl Default for Policy {
326 fn default() -> Self {
327 Self {
328 allow_unsupported: false,
329 allow_malformed_semantics: false,
330 max_path_components: 9,
331 }
332 }
333}
334
335#[derive(Clone, Copy, Debug, Eq, PartialEq)]
337pub enum PolicyDecision {
338 Accept,
340 Reject(PolicyRejection),
342}
343
344#[derive(Clone, Copy, Debug, Eq, PartialEq)]
346pub enum PolicyRejection {
347 PathTooLong,
349 MalformedSemantics,
351 UnsupportedSemantics,
353}
354
355impl PolicyRejection {
356 #[must_use]
358 pub fn code(self) -> &'static str {
359 match self {
360 Self::PathTooLong => "policy.path_too_long",
361 Self::MalformedSemantics => "policy.malformed_semantics",
362 Self::UnsupportedSemantics => "policy.unsupported_semantics",
363 }
364 }
365}
366
367#[derive(Clone, Copy, Debug, Eq, PartialEq)]
369pub enum AprsData<'a> {
370 Status {
372 text: &'a [u8],
374 },
375 Position(Position<'a>),
377 TimestampedPosition(TimestampedPosition<'a>),
379 CompressedPosition(CompressedPosition<'a>),
381 Message(Message<'a>),
383 Object(Object<'a>),
385 Item(Item<'a>),
387 Weather(Weather<'a>),
389 Telemetry(Telemetry<'a>),
391 TelemetryMetadata(TelemetryMetadata<'a>),
393 Query(Query<'a>),
395 Capability(Capability<'a>),
397 Nmea(Nmea<'a>),
399 MicE(MicE<'a>),
401 Maidenhead(Maidenhead<'a>),
403 UserDefined(UserDefined<'a>),
405 ThirdParty(ThirdParty<'a>),
407 Unsupported {
409 identifier: u8,
411 information: &'a [u8],
413 },
414 Malformed {
416 identifier: u8,
418 information: &'a [u8],
420 },
421}
422
423impl AprsData<'_> {
424 #[must_use]
426 pub fn kind_name(&self) -> &'static str {
427 match self {
428 Self::Status { .. } => "status",
429 Self::Position(_) => "position",
430 Self::TimestampedPosition(_) => "timestamped_position",
431 Self::CompressedPosition(_) => "compressed_position",
432 Self::Message(_) => "message",
433 Self::Object(_) => "object",
434 Self::Item(_) => "item",
435 Self::Weather(_) => "weather",
436 Self::Telemetry(_) => "telemetry",
437 Self::TelemetryMetadata(_) => "telemetry_metadata",
438 Self::Query(_) => "query",
439 Self::Capability(_) => "capability",
440 Self::Nmea(_) => "nmea",
441 Self::MicE(_) => "mic_e",
442 Self::Maidenhead(_) => "maidenhead",
443 Self::UserDefined(_) => "user_defined",
444 Self::ThirdParty(_) => "third_party",
445 Self::Unsupported { .. } => "unsupported",
446 Self::Malformed { .. } => "malformed",
447 }
448 }
449}
450
451fn summary_coordinates(data: AprsData<'_>) -> Option<Coordinates> {
452 match data {
453 AprsData::Position(position) => position.coordinates(),
454 AprsData::TimestampedPosition(position) => position.position.coordinates(),
455 AprsData::CompressedPosition(position) => position.coordinates(),
456 AprsData::MicE(mic_e) => mic_e.coordinates(),
457 _ => None,
458 }
459}
460
461fn summary_nmea_checksum(data: AprsData<'_>) -> Option<NmeaChecksum> {
462 match data {
463 AprsData::Nmea(nmea) => nmea.checksum(),
464 _ => None,
465 }
466}
467
468fn summary_telemetry_sequence(data: AprsData<'_>) -> Option<u16> {
469 match data {
470 AprsData::Telemetry(telemetry) => telemetry.sequence_number(),
471 _ => None,
472 }
473}
474
475fn summary_mic_e_speed_course(data: AprsData<'_>) -> Option<MicESpeedCourse> {
476 match data {
477 AprsData::MicE(mic_e) => mic_e.speed_course(),
478 _ => None,
479 }
480}
481
482#[derive(Clone, Copy, Debug, Eq, PartialEq)]
484pub struct Position<'a> {
485 pub messaging: bool,
487 pub latitude: &'a [u8],
489 pub symbol_table: u8,
491 pub longitude: &'a [u8],
493 pub symbol_code: u8,
495 pub comment: &'a [u8],
497}
498
499impl Position<'_> {
500 #[must_use]
502 pub fn coordinates(&self) -> Option<Coordinates> {
503 Some(Coordinates {
504 latitude: decode_latitude(self.latitude)?,
505 longitude: decode_longitude(self.longitude)?,
506 })
507 }
508}
509
510#[derive(Clone, Copy, Debug, PartialEq)]
512pub struct Coordinates {
513 pub latitude: f64,
515 pub longitude: f64,
517}
518
519#[derive(Clone, Copy, Debug, Eq, PartialEq)]
521pub struct TimestampedPosition<'a> {
522 pub messaging: bool,
524 pub timestamp: &'a [u8],
526 pub position: Position<'a>,
528}
529
530#[derive(Clone, Copy, Debug, Eq, PartialEq)]
532pub struct CompressedPosition<'a> {
533 pub messaging: bool,
535 pub symbol_table: u8,
537 pub compressed_latitude: &'a [u8],
539 pub compressed_longitude: &'a [u8],
541 pub symbol_code: u8,
543 pub extension: &'a [u8],
545 pub compression_type: u8,
547 pub comment: &'a [u8],
549}
550
551impl CompressedPosition<'_> {
552 #[must_use]
554 pub fn coordinates(&self) -> Option<Coordinates> {
555 let y = decode_base91(self.compressed_latitude)?;
556 let x = decode_base91(self.compressed_longitude)?;
557
558 Some(Coordinates {
559 latitude: 90.0 - (y as f64 / 380_926.0),
560 longitude: -180.0 + (x as f64 / 190_463.0),
561 })
562 }
563}
564
565#[derive(Clone, Copy, Debug, Eq, PartialEq)]
567pub struct Message<'a> {
568 pub addressee: &'a [u8],
570 pub kind: MessageKind,
572 pub text: &'a [u8],
574 pub id: Option<&'a [u8]>,
576}
577
578#[derive(Clone, Copy, Debug, Eq, PartialEq)]
580pub enum MessageKind {
581 Message,
583 Ack,
585 Reject,
587 Bulletin,
589 Announcement,
591}
592
593#[derive(Clone, Copy, Debug, Eq, PartialEq)]
595pub struct Object<'a> {
596 pub name: &'a [u8],
598 pub live: bool,
600 pub timestamp: &'a [u8],
602 pub body: &'a [u8],
604}
605
606#[derive(Clone, Copy, Debug, Eq, PartialEq)]
608pub struct Item<'a> {
609 pub name: &'a [u8],
611 pub live: bool,
613 pub body: &'a [u8],
615}
616
617#[derive(Clone, Copy, Debug, Eq, PartialEq)]
619pub struct Weather<'a> {
620 pub report: &'a [u8],
622}
623
624impl Weather<'_> {
625 #[must_use]
627 pub fn fields(&self) -> WeatherFields<'_> {
628 WeatherFields {
629 timestamp: self
630 .report
631 .get(..6)
632 .filter(|value| value.iter().all(u8::is_ascii_digit)),
633 wind_direction_degrees: parse_tagged_u16(self.report, b'c', 3),
634 wind_speed_mph: parse_tagged_u16(self.report, b's', 3),
635 wind_gust_mph: parse_tagged_u16(self.report, b'g', 3),
636 temperature_fahrenheit: parse_tagged_i16(self.report, b't', 3),
637 rain_last_hour_hundredths_inch: parse_tagged_u16(self.report, b'r', 3),
638 rain_last_24_hours_hundredths_inch: parse_tagged_u16(self.report, b'p', 3),
639 rain_since_midnight_hundredths_inch: parse_tagged_u16(self.report, b'P', 3),
640 humidity_percent: parse_tagged_u16(self.report, b'h', 2).map(|value| {
641 if value == 0 {
642 100
643 } else {
644 value
645 }
646 }),
647 pressure_tenths_hpa: parse_tagged_u16(self.report, b'b', 5),
648 }
649 }
650}
651
652#[derive(Clone, Copy, Debug, Eq, PartialEq)]
654pub struct WeatherFields<'a> {
655 pub timestamp: Option<&'a [u8]>,
657 pub wind_direction_degrees: Option<u16>,
659 pub wind_speed_mph: Option<u16>,
661 pub wind_gust_mph: Option<u16>,
663 pub temperature_fahrenheit: Option<i16>,
665 pub rain_last_hour_hundredths_inch: Option<u16>,
667 pub rain_last_24_hours_hundredths_inch: Option<u16>,
669 pub rain_since_midnight_hundredths_inch: Option<u16>,
671 pub humidity_percent: Option<u16>,
673 pub pressure_tenths_hpa: Option<u16>,
675}
676
677#[derive(Clone, Copy, Debug, Eq, PartialEq)]
679pub struct Telemetry<'a> {
680 pub sequence: &'a [u8],
682 pub analog: [&'a [u8]; 5],
684 pub digital: Option<&'a [u8]>,
686}
687
688impl Telemetry<'_> {
689 #[must_use]
691 pub fn sequence_number(&self) -> Option<u16> {
692 parse_u16(self.sequence)
693 }
694
695 #[must_use]
697 pub fn analog_values(&self) -> Option<[u16; 5]> {
698 Some([
699 parse_u16(self.analog[0])?,
700 parse_u16(self.analog[1])?,
701 parse_u16(self.analog[2])?,
702 parse_u16(self.analog[3])?,
703 parse_u16(self.analog[4])?,
704 ])
705 }
706
707 #[must_use]
709 pub fn digital_bits(&self) -> Option<[bool; 8]> {
710 let digital = self.digital?;
711 if digital.len() != 8 {
712 return None;
713 }
714
715 let mut bits = [false; 8];
716 for (index, byte) in digital.iter().enumerate() {
717 bits[index] = match byte {
718 b'0' => false,
719 b'1' => true,
720 _ => return None,
721 };
722 }
723
724 Some(bits)
725 }
726}
727
728#[derive(Clone, Copy, Debug, Eq, PartialEq)]
730pub struct TelemetryMetadata<'a> {
731 pub addressee: &'a [u8],
733 pub kind: TelemetryMetadataKind,
735 pub body: &'a [u8],
737}
738
739impl<'a> TelemetryMetadata<'a> {
740 #[must_use]
742 pub fn fields(&self) -> Vec<&'a [u8]> {
743 self.body.split(|byte| *byte == b',').collect()
744 }
745}
746
747#[derive(Clone, Copy, Debug, Eq, PartialEq)]
749pub enum TelemetryMetadataKind {
750 ParameterNames,
752 Units,
754 Equations,
756 BitSense,
758}
759
760#[derive(Clone, Copy, Debug, Eq, PartialEq)]
762pub struct Query<'a> {
763 pub query: &'a [u8],
765}
766
767#[derive(Clone, Copy, Debug, Eq, PartialEq)]
769pub struct Capability<'a> {
770 pub body: &'a [u8],
772}
773
774#[derive(Clone, Copy, Debug, Eq, PartialEq)]
776pub struct Nmea<'a> {
777 pub sentence: &'a [u8],
779}
780
781impl Nmea<'_> {
782 #[must_use]
784 pub fn checksum(&self) -> Option<NmeaChecksum> {
785 let separator = self.sentence.iter().rposition(|byte| *byte == b'*')?;
786 let checksum = self.sentence.get(separator + 1..separator + 3)?;
787 if checksum.len() != 2 || self.sentence.get(separator + 3).is_some() {
788 return None;
789 }
790
791 let expected = parse_hex_byte(checksum)?;
792 let calculated = self.sentence[..separator]
793 .iter()
794 .fold(0u8, |accumulator, byte| accumulator ^ byte);
795
796 Some(NmeaChecksum {
797 expected,
798 calculated,
799 valid: expected == calculated,
800 })
801 }
802}
803
804#[derive(Clone, Copy, Debug, Eq, PartialEq)]
806pub struct NmeaChecksum {
807 pub expected: u8,
809 pub calculated: u8,
811 pub valid: bool,
813}
814
815#[derive(Clone, Copy, Debug, Eq, PartialEq)]
817pub struct MicE<'a> {
818 pub identifier: u8,
820 pub destination: &'a [u8],
822 pub body: &'a [u8],
824 pub status: Option<MicEStatus>,
826 pub latitude_digits: Option<[u8; 6]>,
828}
829
830impl MicE<'_> {
831 #[must_use]
833 pub fn coordinates(&self) -> Option<Coordinates> {
834 Some(Coordinates {
835 latitude: decode_mic_e_latitude(self.destination)?,
836 longitude: decode_mic_e_longitude(self.destination, self.body)?,
837 })
838 }
839
840 #[must_use]
842 pub fn speed_course(&self) -> Option<MicESpeedCourse> {
843 decode_mic_e_speed_course(self.body)
844 }
845}
846
847#[derive(Clone, Copy, Debug, Eq, PartialEq)]
849pub enum MicEStatus {
850 Custom([bool; 3]),
852}
853
854#[derive(Clone, Copy, Debug, Eq, PartialEq)]
856pub struct MicESpeedCourse {
857 pub speed_knots: u16,
859 pub course_degrees: u16,
861}
862
863#[derive(Clone, Copy, Debug, Eq, PartialEq)]
865pub struct Maidenhead<'a> {
866 pub locator: &'a [u8],
868 pub comment: &'a [u8],
870}
871
872#[derive(Clone, Copy, Debug, Eq, PartialEq)]
874pub struct UserDefined<'a> {
875 pub user_id: u8,
877 pub packet_type: u8,
879 pub body: &'a [u8],
881}
882
883#[derive(Clone, Copy, Debug, Eq, PartialEq)]
885pub struct ThirdParty<'a> {
886 pub body: &'a [u8],
888}
889
890impl ThirdParty<'_> {
891 pub fn nested_packet(&self) -> Result<ParsedPacket, ParseError> {
893 parse_packet(self.body)
894 }
895}
896
897#[derive(Clone, Copy, Debug, Eq, PartialEq)]
899pub enum DataTypeIdentifier {
900 PositionNoTimestamp,
902 PositionNoTimestampMessaging,
904 PositionWithTimestamp,
906 PositionWithTimestampMessaging,
908 Status,
910 Query,
912 Capability,
914 Message,
916 Object,
918 Item,
920 Weather,
922 Telemetry,
924 Nmea,
926 MicECurrent,
928 MicEOld,
930 Maidenhead,
932 UserDefined,
934 ThirdParty,
936 Unknown(u8),
938}
939
940impl DataTypeIdentifier {
941 fn from_byte(byte: u8) -> Self {
942 match byte {
943 b'!' => Self::PositionNoTimestamp,
944 b'=' => Self::PositionNoTimestampMessaging,
945 b'/' => Self::PositionWithTimestamp,
946 b'@' => Self::PositionWithTimestampMessaging,
947 b'>' => Self::Status,
948 b'?' => Self::Query,
949 b'<' => Self::Capability,
950 b':' => Self::Message,
951 b';' => Self::Object,
952 b')' => Self::Item,
953 b'_' => Self::Weather,
954 b'T' => Self::Telemetry,
955 b'$' => Self::Nmea,
956 b'`' => Self::MicECurrent,
957 b'\'' => Self::MicEOld,
958 b'[' => Self::Maidenhead,
959 b'{' => Self::UserDefined,
960 b'}' => Self::ThirdParty,
961 other => Self::Unknown(other),
962 }
963 }
964
965 fn as_byte(self) -> u8 {
966 match self {
967 Self::PositionNoTimestamp => b'!',
968 Self::PositionNoTimestampMessaging => b'=',
969 Self::PositionWithTimestamp => b'/',
970 Self::PositionWithTimestampMessaging => b'@',
971 Self::Status => b'>',
972 Self::Query => b'?',
973 Self::Capability => b'<',
974 Self::Message => b':',
975 Self::Object => b';',
976 Self::Item => b')',
977 Self::Weather => b'_',
978 Self::Telemetry => b'T',
979 Self::Nmea => b'$',
980 Self::MicECurrent => b'`',
981 Self::MicEOld => b'\'',
982 Self::Maidenhead => b'[',
983 Self::UserDefined => b'{',
984 Self::ThirdParty => b'}',
985 Self::Unknown(value) => value,
986 }
987 }
988
989 #[must_use]
991 pub fn name(self) -> &'static str {
992 match self {
993 Self::PositionNoTimestamp => "position_no_timestamp",
994 Self::PositionNoTimestampMessaging => "position_no_timestamp_messaging",
995 Self::PositionWithTimestamp => "position_with_timestamp",
996 Self::PositionWithTimestampMessaging => "position_with_timestamp_messaging",
997 Self::Status => "status",
998 Self::Query => "query",
999 Self::Capability => "capability",
1000 Self::Message => "message",
1001 Self::Object => "object",
1002 Self::Item => "item",
1003 Self::Weather => "weather",
1004 Self::Telemetry => "telemetry",
1005 Self::Nmea => "nmea",
1006 Self::MicECurrent => "mic_e_current",
1007 Self::MicEOld => "mic_e_old",
1008 Self::Maidenhead => "maidenhead",
1009 Self::UserDefined => "user_defined",
1010 Self::ThirdParty => "third_party",
1011 Self::Unknown(_) => "unknown",
1012 }
1013 }
1014}
1015
1016fn parse_aprs_data<'a>(
1017 identifier: DataTypeIdentifier,
1018 information: &'a [u8],
1019 destination: &'a [u8],
1020) -> AprsData<'a> {
1021 match identifier {
1022 DataTypeIdentifier::Status => AprsData::Status { text: information },
1023 DataTypeIdentifier::PositionNoTimestamp => parse_position(false, b'!', information),
1024 DataTypeIdentifier::PositionNoTimestampMessaging => parse_position(true, b'=', information),
1025 DataTypeIdentifier::PositionWithTimestamp => {
1026 parse_timestamped_position(false, b'/', information)
1027 }
1028 DataTypeIdentifier::PositionWithTimestampMessaging => {
1029 parse_timestamped_position(true, b'@', information)
1030 }
1031 DataTypeIdentifier::Message => parse_message(information),
1032 DataTypeIdentifier::Object => parse_object(information),
1033 DataTypeIdentifier::Item => parse_item(information),
1034 DataTypeIdentifier::Weather => AprsData::Weather(Weather {
1035 report: information,
1036 }),
1037 DataTypeIdentifier::Telemetry => parse_telemetry(information),
1038 DataTypeIdentifier::Query => AprsData::Query(Query { query: information }),
1039 DataTypeIdentifier::Capability => AprsData::Capability(Capability { body: information }),
1040 DataTypeIdentifier::Nmea => AprsData::Nmea(Nmea {
1041 sentence: information,
1042 }),
1043 DataTypeIdentifier::MicECurrent | DataTypeIdentifier::MicEOld => {
1044 parse_mic_e(identifier, information, destination)
1045 }
1046 DataTypeIdentifier::Maidenhead => parse_maidenhead(information),
1047 DataTypeIdentifier::UserDefined => parse_user_defined(information),
1048 DataTypeIdentifier::ThirdParty => AprsData::ThirdParty(ThirdParty { body: information }),
1049 other => AprsData::Unsupported {
1050 identifier: other.as_byte(),
1051 information,
1052 },
1053 }
1054}
1055
1056fn parse_mic_e<'a>(
1057 identifier: DataTypeIdentifier,
1058 information: &'a [u8],
1059 destination: &'a [u8],
1060) -> AprsData<'a> {
1061 AprsData::MicE(MicE {
1062 identifier: identifier.as_byte(),
1063 destination,
1064 body: information,
1065 status: decode_mic_e_status(destination),
1066 latitude_digits: decode_mic_e_latitude_digits(destination),
1067 })
1068}
1069
1070fn parse_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1071 if is_compressed_position(information) {
1072 return parse_compressed_position(messaging, identifier, information);
1073 }
1074
1075 if information.len() < 18 {
1076 return AprsData::Malformed {
1077 identifier,
1078 information,
1079 };
1080 }
1081
1082 let latitude = &information[..8];
1083 let symbol_table = information[8];
1084 let longitude = &information[9..18];
1085 let symbol_code = information[18];
1086 let comment = &information[19..];
1087
1088 if !is_latitude(latitude)
1089 || !is_symbol_table_identifier(symbol_table)
1090 || !is_longitude(longitude)
1091 || !is_printable_ascii(symbol_code)
1092 {
1093 return AprsData::Malformed {
1094 identifier,
1095 information,
1096 };
1097 }
1098
1099 AprsData::Position(Position {
1100 messaging,
1101 latitude,
1102 symbol_table,
1103 longitude,
1104 symbol_code,
1105 comment,
1106 })
1107}
1108
1109fn parse_timestamped_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1110 if information.len() < 8 {
1111 return AprsData::Malformed {
1112 identifier,
1113 information,
1114 };
1115 }
1116
1117 let timestamp = &information[..7];
1118 if !is_timestamp(timestamp) {
1119 return AprsData::Malformed {
1120 identifier,
1121 information,
1122 };
1123 }
1124
1125 match parse_position(messaging, identifier, &information[7..]) {
1126 AprsData::Position(position) => AprsData::TimestampedPosition(TimestampedPosition {
1127 messaging,
1128 timestamp,
1129 position,
1130 }),
1131 AprsData::CompressedPosition(position) => AprsData::CompressedPosition(position),
1132 _ => AprsData::Malformed {
1133 identifier,
1134 information,
1135 },
1136 }
1137}
1138
1139fn parse_compressed_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1140 if information.len() < 13 {
1141 return AprsData::Malformed {
1142 identifier,
1143 information,
1144 };
1145 }
1146
1147 let symbol_table = information[0];
1148 let compressed_latitude = &information[1..5];
1149 let compressed_longitude = &information[5..9];
1150 let symbol_code = information[9];
1151 let extension = &information[10..12];
1152 let compression_type = information[12];
1153 let comment = &information[13..];
1154
1155 if !is_symbol_table_identifier(symbol_table)
1156 || !compressed_latitude.iter().all(|byte| is_base91(*byte))
1157 || !compressed_longitude.iter().all(|byte| is_base91(*byte))
1158 || !is_printable_ascii(symbol_code)
1159 || !extension.iter().all(|byte| is_base91(*byte))
1160 || !is_base91(compression_type)
1161 {
1162 return AprsData::Malformed {
1163 identifier,
1164 information,
1165 };
1166 }
1167
1168 AprsData::CompressedPosition(CompressedPosition {
1169 messaging,
1170 symbol_table,
1171 compressed_latitude,
1172 compressed_longitude,
1173 symbol_code,
1174 extension,
1175 compression_type,
1176 comment,
1177 })
1178}
1179
1180fn parse_object(information: &[u8]) -> AprsData<'_> {
1181 if information.len() < 17 || !matches!(information[9], b'*' | b'_') {
1182 return AprsData::Malformed {
1183 identifier: b';',
1184 information,
1185 };
1186 }
1187
1188 AprsData::Object(Object {
1189 name: &information[..9],
1190 live: information[9] == b'*',
1191 timestamp: &information[10..17],
1192 body: &information[17..],
1193 })
1194}
1195
1196fn parse_item(information: &[u8]) -> AprsData<'_> {
1197 let Some(separator) = information
1198 .iter()
1199 .position(|byte| matches!(*byte, b'!' | b'_'))
1200 else {
1201 return AprsData::Malformed {
1202 identifier: b')',
1203 information,
1204 };
1205 };
1206
1207 if separator == 0 || separator > 9 {
1208 return AprsData::Malformed {
1209 identifier: b')',
1210 information,
1211 };
1212 }
1213
1214 AprsData::Item(Item {
1215 name: &information[..separator],
1216 live: information[separator] == b'!',
1217 body: &information[separator + 1..],
1218 })
1219}
1220
1221fn parse_message(information: &[u8]) -> AprsData<'_> {
1222 if information.len() < 10 || information[9] != b':' {
1223 return AprsData::Malformed {
1224 identifier: b':',
1225 information,
1226 };
1227 }
1228
1229 let addressee = &information[..9];
1230 let body = &information[10..];
1231 if let Some(kind) = classify_telemetry_metadata_kind(addressee) {
1232 return AprsData::TelemetryMetadata(TelemetryMetadata {
1233 addressee,
1234 kind,
1235 body,
1236 });
1237 }
1238
1239 let (text, id) = match body.iter().position(|byte| *byte == b'{') {
1240 Some(separator) => (&body[..separator], Some(&body[separator + 1..])),
1241 None => (body, None),
1242 };
1243 let kind = classify_message_kind(addressee, text);
1244
1245 AprsData::Message(Message {
1246 addressee,
1247 kind,
1248 text,
1249 id,
1250 })
1251}
1252
1253fn parse_telemetry(information: &[u8]) -> AprsData<'_> {
1254 if !information.starts_with(b"#") {
1255 return AprsData::Malformed {
1256 identifier: b'T',
1257 information,
1258 };
1259 }
1260
1261 let fields: Vec<&[u8]> = information[1..].split(|byte| *byte == b',').collect();
1262 if fields.len() < 6 || fields[..6].iter().any(|field| field.is_empty()) {
1263 return AprsData::Malformed {
1264 identifier: b'T',
1265 information,
1266 };
1267 }
1268
1269 AprsData::Telemetry(Telemetry {
1270 sequence: fields[0],
1271 analog: [fields[1], fields[2], fields[3], fields[4], fields[5]],
1272 digital: fields.get(6).copied().filter(|field| !field.is_empty()),
1273 })
1274}
1275
1276fn parse_maidenhead(information: &[u8]) -> AprsData<'_> {
1277 if information.len() < 6 {
1278 return AprsData::Malformed {
1279 identifier: b'[',
1280 information,
1281 };
1282 }
1283
1284 AprsData::Maidenhead(Maidenhead {
1285 locator: &information[..6],
1286 comment: &information[6..],
1287 })
1288}
1289
1290fn parse_user_defined(information: &[u8]) -> AprsData<'_> {
1291 if information.len() < 2 {
1292 return AprsData::Malformed {
1293 identifier: b'{',
1294 information,
1295 };
1296 }
1297
1298 AprsData::UserDefined(UserDefined {
1299 user_id: information[0],
1300 packet_type: information[1],
1301 body: &information[2..],
1302 })
1303}
1304
1305fn classify_telemetry_metadata_kind(addressee: &[u8]) -> Option<TelemetryMetadataKind> {
1306 match addressee.get(..5)? {
1307 b"PARM." => Some(TelemetryMetadataKind::ParameterNames),
1308 b"UNIT." => Some(TelemetryMetadataKind::Units),
1309 b"EQNS." => Some(TelemetryMetadataKind::Equations),
1310 b"BITS." => Some(TelemetryMetadataKind::BitSense),
1311 _ => None,
1312 }
1313}
1314
1315fn classify_message_kind(addressee: &[u8], text: &[u8]) -> MessageKind {
1316 if text.starts_with(b"ack") {
1317 MessageKind::Ack
1318 } else if text.starts_with(b"rej") {
1319 MessageKind::Reject
1320 } else if addressee.starts_with(b"BLN") && addressee.get(3).is_some_and(u8::is_ascii_digit) {
1321 MessageKind::Bulletin
1322 } else if addressee.starts_with(b"BLN") && addressee.get(3).is_some_and(u8::is_ascii_uppercase)
1323 {
1324 MessageKind::Announcement
1325 } else {
1326 MessageKind::Message
1327 }
1328}
1329
1330fn is_latitude(value: &[u8]) -> bool {
1331 value.len() == 8
1332 && value[0].is_ascii_digit()
1333 && value[1].is_ascii_digit()
1334 && value[2].is_ascii_digit()
1335 && value[3].is_ascii_digit()
1336 && value[4] == b'.'
1337 && value[5].is_ascii_digit()
1338 && value[6].is_ascii_digit()
1339 && matches!(value[7], b'N' | b'S')
1340}
1341
1342fn is_longitude(value: &[u8]) -> bool {
1343 value.len() == 9
1344 && value[0].is_ascii_digit()
1345 && value[1].is_ascii_digit()
1346 && value[2].is_ascii_digit()
1347 && value[3].is_ascii_digit()
1348 && value[4].is_ascii_digit()
1349 && value[5] == b'.'
1350 && value[6].is_ascii_digit()
1351 && value[7].is_ascii_digit()
1352 && matches!(value[8], b'E' | b'W')
1353}
1354
1355fn is_symbol_table_identifier(value: u8) -> bool {
1356 matches!(value, b'/' | b'\\') || value.is_ascii_alphanumeric()
1357}
1358
1359fn is_printable_ascii(value: u8) -> bool {
1360 (0x20..=0x7e).contains(&value)
1361}
1362
1363fn is_base91(value: u8) -> bool {
1364 (b'!'..=b'{').contains(&value)
1365}
1366
1367fn is_compressed_position(information: &[u8]) -> bool {
1368 information
1369 .first()
1370 .is_some_and(|byte| !byte.is_ascii_digit() && is_symbol_table_identifier(*byte))
1371 && information
1372 .get(1..13)
1373 .is_some_and(|bytes| bytes.iter().all(|byte| is_base91(*byte)))
1374}
1375
1376fn is_timestamp(value: &[u8]) -> bool {
1377 value.len() == 7
1378 && value[..6].iter().all(u8::is_ascii_digit)
1379 && matches!(value[6], b'z' | b'/' | b'h')
1380}
1381
1382fn decode_latitude(value: &[u8]) -> Option<f64> {
1383 if !is_latitude(value) {
1384 return None;
1385 }
1386
1387 let degrees = parse_u16(&value[..2])? as f64;
1388 let minutes = parse_fixed_minutes(&value[2..7])?;
1389 let sign = match value[7] {
1390 b'N' => 1.0,
1391 b'S' => -1.0,
1392 _ => return None,
1393 };
1394
1395 Some(sign * (degrees + minutes / 60.0))
1396}
1397
1398fn decode_longitude(value: &[u8]) -> Option<f64> {
1399 if !is_longitude(value) {
1400 return None;
1401 }
1402
1403 let degrees = parse_u16(&value[..3])? as f64;
1404 let minutes = parse_fixed_minutes(&value[3..8])?;
1405 let sign = match value[8] {
1406 b'E' => 1.0,
1407 b'W' => -1.0,
1408 _ => return None,
1409 };
1410
1411 Some(sign * (degrees + minutes / 60.0))
1412}
1413
1414fn parse_fixed_minutes(value: &[u8]) -> Option<f64> {
1415 if value.len() != 5 || value[2] != b'.' || !value[..2].iter().all(u8::is_ascii_digit) {
1416 return None;
1417 }
1418
1419 let whole = parse_u16(&value[..2])? as f64;
1420 let fraction = parse_u16(&value[3..])? as f64 / 100.0;
1421 Some(whole + fraction)
1422}
1423
1424fn decode_base91(value: &[u8]) -> Option<u32> {
1425 if value.len() != 4 || !value.iter().all(|byte| is_base91(*byte)) {
1426 return None;
1427 }
1428
1429 let mut decoded = 0u32;
1430 for byte in value {
1431 decoded = decoded * 91 + u32::from(byte - b'!');
1432 }
1433
1434 Some(decoded)
1435}
1436
1437fn parse_u16(value: &[u8]) -> Option<u16> {
1438 if value.is_empty() || !value.iter().all(u8::is_ascii_digit) {
1439 return None;
1440 }
1441
1442 let mut parsed = 0u16;
1443 for digit in value {
1444 parsed = parsed.checked_mul(10)?;
1445 parsed = parsed.checked_add(u16::from(digit - b'0'))?;
1446 }
1447
1448 Some(parsed)
1449}
1450
1451fn parse_i16(value: &[u8]) -> Option<i16> {
1452 if value.is_empty() {
1453 return None;
1454 }
1455
1456 let (sign, digits) = match value[0] {
1457 b'-' => (-1, &value[1..]),
1458 b'+' => (1, &value[1..]),
1459 _ => (1, value),
1460 };
1461
1462 let unsigned = parse_u16(digits)?;
1463 i16::try_from(unsigned).ok()?.checked_mul(sign)
1464}
1465
1466fn parse_hex_byte(value: &[u8]) -> Option<u8> {
1467 if value.len() != 2 {
1468 return None;
1469 }
1470
1471 Some(hex_value(value[0])? * 16 + hex_value(value[1])?)
1472}
1473
1474fn hex_value(value: u8) -> Option<u8> {
1475 match value {
1476 b'0'..=b'9' => Some(value - b'0'),
1477 b'A'..=b'F' => Some(value - b'A' + 10),
1478 b'a'..=b'f' => Some(value - b'a' + 10),
1479 _ => None,
1480 }
1481}
1482
1483fn parse_tagged_u16(report: &[u8], tag: u8, width: usize) -> Option<u16> {
1484 parse_tagged(report, tag, width).and_then(parse_u16)
1485}
1486
1487fn parse_tagged_i16(report: &[u8], tag: u8, width: usize) -> Option<i16> {
1488 parse_tagged(report, tag, width).and_then(parse_i16)
1489}
1490
1491fn parse_tagged(report: &[u8], tag: u8, width: usize) -> Option<&[u8]> {
1492 let start = report.iter().position(|byte| *byte == tag)? + 1;
1493 report.get(start..start + width)
1494}
1495
1496fn decode_mic_e_status(destination: &[u8]) -> Option<MicEStatus> {
1497 if destination.len() != 6 {
1498 return None;
1499 }
1500
1501 let bytes = destination.get(..3)?;
1502 Some(MicEStatus::Custom([
1503 mic_e_status_bit(bytes[0])?,
1504 mic_e_status_bit(bytes[1])?,
1505 mic_e_status_bit(bytes[2])?,
1506 ]))
1507}
1508
1509fn mic_e_status_bit(byte: u8) -> Option<bool> {
1510 match byte {
1511 b'0'..=b'9' | b'L' => Some(false),
1512 b'A'..=b'K' | b'P'..=b'Z' => Some(true),
1513 _ => None,
1514 }
1515}
1516
1517fn decode_mic_e_latitude_digits(destination: &[u8]) -> Option<[u8; 6]> {
1518 if destination.len() != 6 {
1519 return None;
1520 }
1521
1522 let mut digits = [0u8; 6];
1523 for (index, byte) in destination.iter().copied().enumerate() {
1524 digits[index] = mic_e_latitude_digit(byte)?;
1525 }
1526
1527 Some(digits)
1528}
1529
1530fn mic_e_latitude_digit(byte: u8) -> Option<u8> {
1531 match byte {
1532 b'0'..=b'9' => Some(byte - b'0'),
1533 b'A'..=b'J' => Some(byte - b'A'),
1534 b'P'..=b'Y' => Some(byte - b'P'),
1535 b'K' | b'L' | b'Z' => Some(0),
1536 _ => None,
1537 }
1538}
1539
1540fn decode_mic_e_latitude(destination: &[u8]) -> Option<f64> {
1541 let digits = decode_mic_e_latitude_digits(destination)?;
1542 let degrees = u16::from(digits[0]) * 10 + u16::from(digits[1]);
1543 let minutes = u16::from(digits[2]) * 10 + u16::from(digits[3]);
1544 let hundredths = u16::from(digits[4]) * 10 + u16::from(digits[5]);
1545 if degrees > 90 || minutes > 59 {
1546 return None;
1547 }
1548
1549 let sign = if mic_e_north(destination[3])? {
1550 1.0
1551 } else {
1552 -1.0
1553 };
1554 Some(sign * (f64::from(degrees) + (f64::from(minutes) + f64::from(hundredths) / 100.0) / 60.0))
1555}
1556
1557fn decode_mic_e_longitude(destination: &[u8], body: &[u8]) -> Option<f64> {
1558 if destination.len() != 6 || body.len() < 3 {
1559 return None;
1560 }
1561
1562 let mut degrees = i16::from(mic_e_body_value(body[0])?);
1563 if mic_e_longitude_offset(destination[4])? {
1564 degrees += 100;
1565 }
1566 if (180..=189).contains(°rees) {
1567 degrees -= 80;
1568 } else if (190..=199).contains(°rees) {
1569 degrees -= 190;
1570 }
1571
1572 let minutes = mic_e_body_value(body[1])?;
1573 let hundredths = mic_e_body_value(body[2])?;
1574 if !(0..=179).contains(°rees) || minutes > 59 || hundredths > 99 {
1575 return None;
1576 }
1577
1578 let sign = if mic_e_west(destination[5])? {
1579 -1.0
1580 } else {
1581 1.0
1582 };
1583 Some(sign * (f64::from(degrees) + (f64::from(minutes) + f64::from(hundredths) / 100.0) / 60.0))
1584}
1585
1586fn decode_mic_e_speed_course(body: &[u8]) -> Option<MicESpeedCourse> {
1587 if body.len() < 6 {
1588 return None;
1589 }
1590
1591 let speed_tens = u16::from(mic_e_body_value(body[3])?);
1592 let speed_units_course_hundreds = u16::from(mic_e_body_value(body[4])?);
1593 let course_remainder = u16::from(mic_e_body_value(body[5])?);
1594 let mut speed_knots = speed_tens * 10 + speed_units_course_hundreds / 10;
1595 if speed_knots >= 800 {
1596 speed_knots -= 800;
1597 }
1598
1599 Some(MicESpeedCourse {
1600 speed_knots,
1601 course_degrees: (speed_units_course_hundreds % 10) * 100 + course_remainder,
1602 })
1603}
1604
1605fn mic_e_body_value(byte: u8) -> Option<u8> {
1606 let value = byte.checked_sub(28)?;
1607 (value <= 99).then_some(value)
1608}
1609
1610fn mic_e_north(byte: u8) -> Option<bool> {
1611 match byte {
1612 b'0'..=b'9' | b'A'..=b'L' => Some(false),
1613 b'P'..=b'Z' => Some(true),
1614 _ => None,
1615 }
1616}
1617
1618fn mic_e_longitude_offset(byte: u8) -> Option<bool> {
1619 match byte {
1620 b'0'..=b'9' | b'A'..=b'L' => Some(false),
1621 b'P'..=b'Z' => Some(true),
1622 _ => None,
1623 }
1624}
1625
1626fn mic_e_west(byte: u8) -> Option<bool> {
1627 match byte {
1628 b'0'..=b'9' | b'A'..=b'L' => Some(false),
1629 b'P'..=b'Z' => Some(true),
1630 _ => None,
1631 }
1632}
1633
1634#[derive(Clone, Debug, Eq, PartialEq)]
1636pub enum ParseError {
1637 Empty,
1639 Oversized,
1641 MissingSeparator,
1643 EmptySegment,
1645 InvalidAddress,
1647}
1648
1649impl ParseError {
1650 #[must_use]
1652 pub fn code(&self) -> &'static str {
1653 match self {
1654 Self::Empty => "parse.empty",
1655 Self::Oversized => "parse.oversized",
1656 Self::MissingSeparator => "parse.missing_separator",
1657 Self::EmptySegment => "parse.empty_segment",
1658 Self::InvalidAddress => "parse.invalid_address",
1659 }
1660 }
1661}
1662
1663pub fn parse_packet(input: &[u8]) -> Result<ParsedPacket, ParseError> {
1669 parse_packet_with_options(input, ParseOptions::default())
1670}
1671
1672pub fn parse_packet_with_options(
1674 input: &[u8],
1675 options: ParseOptions,
1676) -> Result<ParsedPacket, ParseError> {
1677 if input.is_empty() {
1678 return Err(ParseError::Empty);
1679 }
1680
1681 if input.len() > options.max_packet_len {
1682 return Err(ParseError::Oversized);
1683 }
1684
1685 let source_end = input
1686 .iter()
1687 .position(|byte| *byte == b'>')
1688 .ok_or(ParseError::MissingSeparator)?;
1689 let payload_separator = input[source_end + 1..]
1690 .iter()
1691 .position(|byte| *byte == b':')
1692 .map(|offset| source_end + 1 + offset)
1693 .ok_or(ParseError::MissingSeparator)?;
1694
1695 let path_start = source_end + 1;
1696 let path_end = payload_separator;
1697 let payload_start = payload_separator + 1;
1698
1699 if source_end == 0 || path_start == path_end || payload_start == input.len() {
1700 return Err(ParseError::EmptySegment);
1701 }
1702
1703 let Some(path_components) = path_component_ranges(input, path_start, path_end) else {
1704 return Err(ParseError::InvalidAddress);
1705 };
1706
1707 if !is_ax25_like_source(&input[..source_end])
1708 || !path_components
1709 .iter()
1710 .all(|(start, end)| is_ax25_like_path_component(&input[*start..*end]))
1711 {
1712 return Err(ParseError::InvalidAddress);
1713 }
1714
1715 Ok(ParsedPacket {
1716 raw: RawPacket {
1717 bytes: input.to_vec(),
1718 },
1719 source_end,
1720 path_start,
1721 path_end,
1722 path_components,
1723 payload_start,
1724 })
1725}
1726
1727fn path_component_ranges(
1728 input: &[u8],
1729 path_start: usize,
1730 path_end: usize,
1731) -> Option<Vec<(usize, usize)>> {
1732 let mut components = Vec::new();
1733 let mut component_start = path_start;
1734
1735 for (offset, byte) in input[path_start..path_end].iter().enumerate() {
1736 if *byte == b',' {
1737 let index = path_start + offset;
1738 if component_start == index {
1739 return None;
1740 }
1741 components.push((component_start, index));
1742 component_start = index + 1;
1743 }
1744 }
1745
1746 if component_start == path_end {
1747 return None;
1748 }
1749
1750 components.push((component_start, path_end));
1751 Some(components)
1752}
1753
1754fn is_ax25_like_source(source: &[u8]) -> bool {
1755 is_ax25_like_address(source, false)
1756}
1757
1758fn is_ax25_like_path_component(component: &[u8]) -> bool {
1759 is_ax25_like_address(component, true)
1760}
1761
1762fn is_ax25_like_address(address: &[u8], allow_repeated_marker: bool) -> bool {
1763 let address = if allow_repeated_marker {
1764 address.strip_suffix(b"*").unwrap_or(address)
1765 } else {
1766 address
1767 };
1768
1769 if address.is_empty() || address.contains(&b'*') {
1770 return false;
1771 }
1772
1773 let (callsign, ssid) = match address.iter().position(|byte| *byte == b'-') {
1774 Some(separator) => (&address[..separator], Some(&address[separator + 1..])),
1775 None => (address, None),
1776 };
1777
1778 is_ax25_like_callsign(callsign) && ssid.map_or(true, is_ax25_like_ssid)
1779}
1780
1781fn is_ax25_like_callsign(callsign: &[u8]) -> bool {
1782 (1..=6).contains(&callsign.len())
1783 && callsign
1784 .iter()
1785 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit())
1786}
1787
1788fn is_ax25_like_ssid(ssid: &[u8]) -> bool {
1789 if ssid.is_empty() || ssid.len() > 2 || !ssid.iter().all(u8::is_ascii_digit) {
1790 return false;
1791 }
1792
1793 let mut value = 0u8;
1794 for digit in ssid {
1795 value = value * 10 + (digit - b'0');
1796 }
1797
1798 value <= 15
1799}