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 }
672 }
673}
674
675#[derive(Clone, Copy, Debug, Eq, PartialEq)]
677pub struct WeatherFields<'a> {
678 pub timestamp: Option<&'a [u8]>,
680 pub wind_direction_degrees: Option<u16>,
682 pub wind_speed_mph: Option<u16>,
684 pub wind_gust_mph: Option<u16>,
686 pub temperature_fahrenheit: Option<i16>,
688 pub rain_last_hour_hundredths_inch: Option<u16>,
690 pub rain_last_24_hours_hundredths_inch: Option<u16>,
692 pub rain_since_midnight_hundredths_inch: Option<u16>,
694 pub humidity_percent: Option<u16>,
696 pub pressure_tenths_hpa: Option<u16>,
698}
699
700#[derive(Clone, Copy, Debug, Eq, PartialEq)]
702pub struct Telemetry<'a> {
703 pub sequence: &'a [u8],
705 pub analog: [&'a [u8]; 5],
707 pub digital: Option<&'a [u8]>,
709}
710
711impl Telemetry<'_> {
712 #[must_use]
714 pub fn sequence_number(&self) -> Option<u16> {
715 parse_u16(self.sequence)
716 }
717
718 #[must_use]
720 pub fn analog_values(&self) -> Option<[u16; 5]> {
721 Some([
722 parse_u16(self.analog[0])?,
723 parse_u16(self.analog[1])?,
724 parse_u16(self.analog[2])?,
725 parse_u16(self.analog[3])?,
726 parse_u16(self.analog[4])?,
727 ])
728 }
729
730 #[must_use]
732 pub fn digital_bits(&self) -> Option<[bool; 8]> {
733 let digital = self.digital?;
734 if digital.len() != 8 {
735 return None;
736 }
737
738 let mut bits = [false; 8];
739 for (index, byte) in digital.iter().enumerate() {
740 bits[index] = match byte {
741 b'0' => false,
742 b'1' => true,
743 _ => return None,
744 };
745 }
746
747 Some(bits)
748 }
749}
750
751#[derive(Clone, Copy, Debug, Eq, PartialEq)]
753pub struct TelemetryMetadata<'a> {
754 pub addressee: &'a [u8],
756 pub kind: TelemetryMetadataKind,
758 pub body: &'a [u8],
760}
761
762impl<'a> TelemetryMetadata<'a> {
763 #[must_use]
765 pub fn fields(&self) -> Vec<&'a [u8]> {
766 self.body.split(|byte| *byte == b',').collect()
767 }
768}
769
770#[derive(Clone, Copy, Debug, Eq, PartialEq)]
772pub enum TelemetryMetadataKind {
773 ParameterNames,
775 Units,
777 Equations,
779 BitSense,
781}
782
783#[derive(Clone, Copy, Debug, Eq, PartialEq)]
785pub struct Query<'a> {
786 pub query: &'a [u8],
788}
789
790#[derive(Clone, Copy, Debug, Eq, PartialEq)]
792pub struct Capability<'a> {
793 pub body: &'a [u8],
795}
796
797#[derive(Clone, Copy, Debug, Eq, PartialEq)]
799pub struct Nmea<'a> {
800 pub sentence: &'a [u8],
802}
803
804impl Nmea<'_> {
805 #[must_use]
807 pub fn checksum(&self) -> Option<NmeaChecksum> {
808 let separator = self.sentence.iter().rposition(|byte| *byte == b'*')?;
809 let checksum = self.sentence.get(separator + 1..separator + 3)?;
810 if checksum.len() != 2 || self.sentence.get(separator + 3).is_some() {
811 return None;
812 }
813
814 let expected = parse_hex_byte(checksum)?;
815 let calculated = self.sentence[..separator]
816 .iter()
817 .fold(0u8, |accumulator, byte| accumulator ^ byte);
818
819 Some(NmeaChecksum {
820 expected,
821 calculated,
822 valid: expected == calculated,
823 })
824 }
825}
826
827#[derive(Clone, Copy, Debug, Eq, PartialEq)]
829pub struct NmeaChecksum {
830 pub expected: u8,
832 pub calculated: u8,
834 pub valid: bool,
836}
837
838#[derive(Clone, Copy, Debug, Eq, PartialEq)]
840pub struct MicE<'a> {
841 pub identifier: u8,
843 pub destination: &'a [u8],
845 pub body: &'a [u8],
847 pub status: Option<MicEStatus>,
849 pub latitude_digits: Option<[u8; 6]>,
851}
852
853impl MicE<'_> {
854 #[must_use]
856 pub fn coordinates(&self) -> Option<Coordinates> {
857 Some(Coordinates {
858 latitude: decode_mic_e_latitude(self.destination)?,
859 longitude: decode_mic_e_longitude(self.destination, self.body)?,
860 })
861 }
862
863 #[must_use]
865 pub fn speed_course(&self) -> Option<MicESpeedCourse> {
866 decode_mic_e_speed_course(self.body)
867 }
868}
869
870#[derive(Clone, Copy, Debug, Eq, PartialEq)]
872pub enum MicEStatus {
873 Custom([bool; 3]),
875}
876
877#[derive(Clone, Copy, Debug, Eq, PartialEq)]
879pub struct MicESpeedCourse {
880 pub speed_knots: u16,
882 pub course_degrees: u16,
884}
885
886#[derive(Clone, Copy, Debug, Eq, PartialEq)]
888pub struct Maidenhead<'a> {
889 pub locator: &'a [u8],
891 pub comment: &'a [u8],
893}
894
895#[derive(Clone, Copy, Debug, Eq, PartialEq)]
897pub struct UserDefined<'a> {
898 pub user_id: u8,
900 pub packet_type: u8,
902 pub body: &'a [u8],
904}
905
906#[derive(Clone, Copy, Debug, Eq, PartialEq)]
908pub struct ThirdParty<'a> {
909 pub body: &'a [u8],
911}
912
913impl ThirdParty<'_> {
914 pub fn nested_packet(&self) -> Result<ParsedPacket, ParseError> {
916 parse_packet(self.body)
917 }
918}
919
920#[derive(Clone, Copy, Debug, Eq, PartialEq)]
922pub enum DataTypeIdentifier {
923 PositionNoTimestamp,
925 PositionNoTimestampMessaging,
927 PositionWithTimestamp,
929 PositionWithTimestampMessaging,
931 Status,
933 Query,
935 Capability,
937 Message,
939 Object,
941 Item,
943 Weather,
945 Telemetry,
947 Nmea,
949 MicECurrent,
951 MicEOld,
953 Maidenhead,
955 UserDefined,
957 ThirdParty,
959 Unknown(u8),
961}
962
963impl DataTypeIdentifier {
964 fn from_byte(byte: u8) -> Self {
965 match byte {
966 b'!' => Self::PositionNoTimestamp,
967 b'=' => Self::PositionNoTimestampMessaging,
968 b'/' => Self::PositionWithTimestamp,
969 b'@' => Self::PositionWithTimestampMessaging,
970 b'>' => Self::Status,
971 b'?' => Self::Query,
972 b'<' => Self::Capability,
973 b':' => Self::Message,
974 b';' => Self::Object,
975 b')' => Self::Item,
976 b'_' => Self::Weather,
977 b'T' => Self::Telemetry,
978 b'$' => Self::Nmea,
979 b'`' => Self::MicECurrent,
980 b'\'' => Self::MicEOld,
981 b'[' => Self::Maidenhead,
982 b'{' => Self::UserDefined,
983 b'}' => Self::ThirdParty,
984 other => Self::Unknown(other),
985 }
986 }
987
988 fn as_byte(self) -> u8 {
989 match self {
990 Self::PositionNoTimestamp => b'!',
991 Self::PositionNoTimestampMessaging => b'=',
992 Self::PositionWithTimestamp => b'/',
993 Self::PositionWithTimestampMessaging => b'@',
994 Self::Status => b'>',
995 Self::Query => b'?',
996 Self::Capability => b'<',
997 Self::Message => b':',
998 Self::Object => b';',
999 Self::Item => b')',
1000 Self::Weather => b'_',
1001 Self::Telemetry => b'T',
1002 Self::Nmea => b'$',
1003 Self::MicECurrent => b'`',
1004 Self::MicEOld => b'\'',
1005 Self::Maidenhead => b'[',
1006 Self::UserDefined => b'{',
1007 Self::ThirdParty => b'}',
1008 Self::Unknown(value) => value,
1009 }
1010 }
1011
1012 #[must_use]
1014 pub fn name(self) -> &'static str {
1015 match self {
1016 Self::PositionNoTimestamp => "position_no_timestamp",
1017 Self::PositionNoTimestampMessaging => "position_no_timestamp_messaging",
1018 Self::PositionWithTimestamp => "position_with_timestamp",
1019 Self::PositionWithTimestampMessaging => "position_with_timestamp_messaging",
1020 Self::Status => "status",
1021 Self::Query => "query",
1022 Self::Capability => "capability",
1023 Self::Message => "message",
1024 Self::Object => "object",
1025 Self::Item => "item",
1026 Self::Weather => "weather",
1027 Self::Telemetry => "telemetry",
1028 Self::Nmea => "nmea",
1029 Self::MicECurrent => "mic_e_current",
1030 Self::MicEOld => "mic_e_old",
1031 Self::Maidenhead => "maidenhead",
1032 Self::UserDefined => "user_defined",
1033 Self::ThirdParty => "third_party",
1034 Self::Unknown(_) => "unknown",
1035 }
1036 }
1037}
1038
1039fn parse_aprs_data<'a>(
1040 identifier: DataTypeIdentifier,
1041 information: &'a [u8],
1042 destination: &'a [u8],
1043) -> AprsData<'a> {
1044 match identifier {
1045 DataTypeIdentifier::Status => AprsData::Status { text: information },
1046 DataTypeIdentifier::PositionNoTimestamp => parse_position(false, b'!', information),
1047 DataTypeIdentifier::PositionNoTimestampMessaging => parse_position(true, b'=', information),
1048 DataTypeIdentifier::PositionWithTimestamp => {
1049 parse_timestamped_position(false, b'/', information)
1050 }
1051 DataTypeIdentifier::PositionWithTimestampMessaging => {
1052 parse_timestamped_position(true, b'@', information)
1053 }
1054 DataTypeIdentifier::Message => parse_message(information),
1055 DataTypeIdentifier::Object => parse_object(information),
1056 DataTypeIdentifier::Item => parse_item(information),
1057 DataTypeIdentifier::Weather => AprsData::Weather(Weather {
1058 report: information,
1059 }),
1060 DataTypeIdentifier::Telemetry => parse_telemetry(information),
1061 DataTypeIdentifier::Query => AprsData::Query(Query { query: information }),
1062 DataTypeIdentifier::Capability => AprsData::Capability(Capability { body: information }),
1063 DataTypeIdentifier::Nmea => AprsData::Nmea(Nmea {
1064 sentence: information,
1065 }),
1066 DataTypeIdentifier::MicECurrent | DataTypeIdentifier::MicEOld => {
1067 parse_mic_e(identifier, information, destination)
1068 }
1069 DataTypeIdentifier::Maidenhead => parse_maidenhead(information),
1070 DataTypeIdentifier::UserDefined => parse_user_defined(information),
1071 DataTypeIdentifier::ThirdParty => AprsData::ThirdParty(ThirdParty { body: information }),
1072 other => AprsData::Unsupported {
1073 identifier: other.as_byte(),
1074 information,
1075 },
1076 }
1077}
1078
1079fn parse_mic_e<'a>(
1080 identifier: DataTypeIdentifier,
1081 information: &'a [u8],
1082 destination: &'a [u8],
1083) -> AprsData<'a> {
1084 AprsData::MicE(MicE {
1085 identifier: identifier.as_byte(),
1086 destination,
1087 body: information,
1088 status: decode_mic_e_status(destination),
1089 latitude_digits: decode_mic_e_latitude_digits(destination),
1090 })
1091}
1092
1093fn parse_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1094 if is_compressed_position(information) {
1095 return parse_compressed_position(messaging, identifier, information);
1096 }
1097
1098 if information.len() < 18 {
1099 return AprsData::Malformed {
1100 identifier,
1101 information,
1102 };
1103 }
1104
1105 let latitude = &information[..8];
1106 let symbol_table = information[8];
1107 let longitude = &information[9..18];
1108 let symbol_code = information[18];
1109 let comment = &information[19..];
1110
1111 if !is_latitude(latitude)
1112 || !is_symbol_table_identifier(symbol_table)
1113 || !is_longitude(longitude)
1114 || !is_printable_ascii(symbol_code)
1115 {
1116 return AprsData::Malformed {
1117 identifier,
1118 information,
1119 };
1120 }
1121
1122 AprsData::Position(Position {
1123 messaging,
1124 latitude,
1125 symbol_table,
1126 longitude,
1127 symbol_code,
1128 comment,
1129 })
1130}
1131
1132fn parse_timestamped_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1133 if information.len() < 8 {
1134 return AprsData::Malformed {
1135 identifier,
1136 information,
1137 };
1138 }
1139
1140 let timestamp = &information[..7];
1141 if !is_timestamp(timestamp) {
1142 return AprsData::Malformed {
1143 identifier,
1144 information,
1145 };
1146 }
1147
1148 match parse_position(messaging, identifier, &information[7..]) {
1149 AprsData::Position(position) => AprsData::TimestampedPosition(TimestampedPosition {
1150 messaging,
1151 timestamp,
1152 position,
1153 }),
1154 AprsData::CompressedPosition(position) => AprsData::CompressedPosition(position),
1155 _ => AprsData::Malformed {
1156 identifier,
1157 information,
1158 },
1159 }
1160}
1161
1162fn parse_compressed_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1163 if information.len() < 13 {
1164 return AprsData::Malformed {
1165 identifier,
1166 information,
1167 };
1168 }
1169
1170 let symbol_table = information[0];
1171 let compressed_latitude = &information[1..5];
1172 let compressed_longitude = &information[5..9];
1173 let symbol_code = information[9];
1174 let extension = &information[10..12];
1175 let compression_type = information[12];
1176 let comment = &information[13..];
1177
1178 if !is_symbol_table_identifier(symbol_table)
1179 || !compressed_latitude.iter().all(|byte| is_base91(*byte))
1180 || !compressed_longitude.iter().all(|byte| is_base91(*byte))
1181 || !is_printable_ascii(symbol_code)
1182 || !extension.iter().all(|byte| is_base91(*byte))
1183 || !is_base91(compression_type)
1184 {
1185 return AprsData::Malformed {
1186 identifier,
1187 information,
1188 };
1189 }
1190
1191 AprsData::CompressedPosition(CompressedPosition {
1192 messaging,
1193 symbol_table,
1194 compressed_latitude,
1195 compressed_longitude,
1196 symbol_code,
1197 extension,
1198 compression_type,
1199 comment,
1200 })
1201}
1202
1203fn parse_object(information: &[u8]) -> AprsData<'_> {
1204 if information.len() < 17 || !matches!(information[9], b'*' | b'_') {
1205 return AprsData::Malformed {
1206 identifier: b';',
1207 information,
1208 };
1209 }
1210
1211 AprsData::Object(Object {
1212 name: &information[..9],
1213 live: information[9] == b'*',
1214 timestamp: &information[10..17],
1215 body: &information[17..],
1216 })
1217}
1218
1219fn parse_item(information: &[u8]) -> AprsData<'_> {
1220 let Some(separator) = information
1221 .iter()
1222 .position(|byte| matches!(*byte, b'!' | b'_'))
1223 else {
1224 return AprsData::Malformed {
1225 identifier: b')',
1226 information,
1227 };
1228 };
1229
1230 if separator == 0 || separator > 9 {
1231 return AprsData::Malformed {
1232 identifier: b')',
1233 information,
1234 };
1235 }
1236
1237 AprsData::Item(Item {
1238 name: &information[..separator],
1239 live: information[separator] == b'!',
1240 body: &information[separator + 1..],
1241 })
1242}
1243
1244fn parse_message(information: &[u8]) -> AprsData<'_> {
1245 if information.len() < 10 || information[9] != b':' {
1246 return AprsData::Malformed {
1247 identifier: b':',
1248 information,
1249 };
1250 }
1251
1252 let addressee = &information[..9];
1253 let body = &information[10..];
1254 if let Some(kind) = classify_telemetry_metadata_kind(addressee) {
1255 return AprsData::TelemetryMetadata(TelemetryMetadata {
1256 addressee,
1257 kind,
1258 body,
1259 });
1260 }
1261
1262 let (text, id) = match body.iter().position(|byte| *byte == b'{') {
1263 Some(separator) => (&body[..separator], Some(&body[separator + 1..])),
1264 None => (body, None),
1265 };
1266 let kind = classify_message_kind(addressee, text);
1267
1268 AprsData::Message(Message {
1269 addressee,
1270 kind,
1271 text,
1272 id,
1273 })
1274}
1275
1276fn parse_telemetry(information: &[u8]) -> AprsData<'_> {
1277 if !information.starts_with(b"#") {
1278 return AprsData::Malformed {
1279 identifier: b'T',
1280 information,
1281 };
1282 }
1283
1284 let fields: Vec<&[u8]> = information[1..].split(|byte| *byte == b',').collect();
1285 if fields.len() < 6 || fields[..6].iter().any(|field| field.is_empty()) {
1286 return AprsData::Malformed {
1287 identifier: b'T',
1288 information,
1289 };
1290 }
1291
1292 AprsData::Telemetry(Telemetry {
1293 sequence: fields[0],
1294 analog: [fields[1], fields[2], fields[3], fields[4], fields[5]],
1295 digital: fields.get(6).copied().filter(|field| !field.is_empty()),
1296 })
1297}
1298
1299fn parse_maidenhead(information: &[u8]) -> AprsData<'_> {
1300 if information.len() < 6 {
1301 return AprsData::Malformed {
1302 identifier: b'[',
1303 information,
1304 };
1305 }
1306
1307 AprsData::Maidenhead(Maidenhead {
1308 locator: &information[..6],
1309 comment: &information[6..],
1310 })
1311}
1312
1313fn parse_user_defined(information: &[u8]) -> AprsData<'_> {
1314 if information.len() < 2 {
1315 return AprsData::Malformed {
1316 identifier: b'{',
1317 information,
1318 };
1319 }
1320
1321 AprsData::UserDefined(UserDefined {
1322 user_id: information[0],
1323 packet_type: information[1],
1324 body: &information[2..],
1325 })
1326}
1327
1328fn classify_telemetry_metadata_kind(addressee: &[u8]) -> Option<TelemetryMetadataKind> {
1329 match addressee.get(..5)? {
1330 b"PARM." => Some(TelemetryMetadataKind::ParameterNames),
1331 b"UNIT." => Some(TelemetryMetadataKind::Units),
1332 b"EQNS." => Some(TelemetryMetadataKind::Equations),
1333 b"BITS." => Some(TelemetryMetadataKind::BitSense),
1334 _ => None,
1335 }
1336}
1337
1338fn classify_message_kind(addressee: &[u8], text: &[u8]) -> MessageKind {
1339 if text.starts_with(b"ack") {
1340 MessageKind::Ack
1341 } else if text.starts_with(b"rej") {
1342 MessageKind::Reject
1343 } else if addressee.starts_with(b"BLN") && addressee.get(3).is_some_and(u8::is_ascii_digit) {
1344 MessageKind::Bulletin
1345 } else if addressee.starts_with(b"BLN") && addressee.get(3).is_some_and(u8::is_ascii_uppercase)
1346 {
1347 MessageKind::Announcement
1348 } else {
1349 MessageKind::Message
1350 }
1351}
1352
1353fn is_latitude(value: &[u8]) -> bool {
1354 value.len() == 8
1355 && value[0].is_ascii_digit()
1356 && value[1].is_ascii_digit()
1357 && value[2].is_ascii_digit()
1358 && value[3].is_ascii_digit()
1359 && value[4] == b'.'
1360 && value[5].is_ascii_digit()
1361 && value[6].is_ascii_digit()
1362 && matches!(value[7], b'N' | b'S')
1363}
1364
1365fn is_longitude(value: &[u8]) -> bool {
1366 value.len() == 9
1367 && value[0].is_ascii_digit()
1368 && value[1].is_ascii_digit()
1369 && value[2].is_ascii_digit()
1370 && value[3].is_ascii_digit()
1371 && value[4].is_ascii_digit()
1372 && value[5] == b'.'
1373 && value[6].is_ascii_digit()
1374 && value[7].is_ascii_digit()
1375 && matches!(value[8], b'E' | b'W')
1376}
1377
1378fn is_symbol_table_identifier(value: u8) -> bool {
1379 matches!(value, b'/' | b'\\') || value.is_ascii_alphanumeric()
1380}
1381
1382fn is_printable_ascii(value: u8) -> bool {
1383 (0x20..=0x7e).contains(&value)
1384}
1385
1386fn is_base91(value: u8) -> bool {
1387 (b'!'..=b'{').contains(&value)
1388}
1389
1390fn is_compressed_position(information: &[u8]) -> bool {
1391 information
1392 .first()
1393 .is_some_and(|byte| !byte.is_ascii_digit() && is_symbol_table_identifier(*byte))
1394 && information
1395 .get(1..13)
1396 .is_some_and(|bytes| bytes.iter().all(|byte| is_base91(*byte)))
1397}
1398
1399fn is_timestamp(value: &[u8]) -> bool {
1400 value.len() == 7
1401 && value[..6].iter().all(u8::is_ascii_digit)
1402 && matches!(value[6], b'z' | b'/' | b'h')
1403}
1404
1405fn decode_latitude(value: &[u8]) -> Option<f64> {
1406 if !is_latitude(value) {
1407 return None;
1408 }
1409
1410 let degrees = parse_u16(&value[..2])? as f64;
1411 let minutes = parse_fixed_minutes(&value[2..7])?;
1412 let sign = match value[7] {
1413 b'N' => 1.0,
1414 b'S' => -1.0,
1415 _ => return None,
1416 };
1417
1418 Some(sign * (degrees + minutes / 60.0))
1419}
1420
1421fn decode_longitude(value: &[u8]) -> Option<f64> {
1422 if !is_longitude(value) {
1423 return None;
1424 }
1425
1426 let degrees = parse_u16(&value[..3])? as f64;
1427 let minutes = parse_fixed_minutes(&value[3..8])?;
1428 let sign = match value[8] {
1429 b'E' => 1.0,
1430 b'W' => -1.0,
1431 _ => return None,
1432 };
1433
1434 Some(sign * (degrees + minutes / 60.0))
1435}
1436
1437fn parse_fixed_minutes(value: &[u8]) -> Option<f64> {
1438 if value.len() != 5 || value[2] != b'.' || !value[..2].iter().all(u8::is_ascii_digit) {
1439 return None;
1440 }
1441
1442 let whole = parse_u16(&value[..2])? as f64;
1443 let fraction = parse_u16(&value[3..])? as f64 / 100.0;
1444 Some(whole + fraction)
1445}
1446
1447fn decode_base91(value: &[u8]) -> Option<u32> {
1448 if value.len() != 4 || !value.iter().all(|byte| is_base91(*byte)) {
1449 return None;
1450 }
1451
1452 let mut decoded = 0u32;
1453 for byte in value {
1454 decoded = decoded * 91 + u32::from(byte - b'!');
1455 }
1456
1457 Some(decoded)
1458}
1459
1460fn parse_u16(value: &[u8]) -> Option<u16> {
1461 if value.is_empty() || !value.iter().all(u8::is_ascii_digit) {
1462 return None;
1463 }
1464
1465 let mut parsed = 0u16;
1466 for digit in value {
1467 parsed = parsed.checked_mul(10)?;
1468 parsed = parsed.checked_add(u16::from(digit - b'0'))?;
1469 }
1470
1471 Some(parsed)
1472}
1473
1474fn parse_i16(value: &[u8]) -> Option<i16> {
1475 if value.is_empty() {
1476 return None;
1477 }
1478
1479 let (sign, digits) = match value[0] {
1480 b'-' => (-1, &value[1..]),
1481 b'+' => (1, &value[1..]),
1482 _ => (1, value),
1483 };
1484
1485 let unsigned = parse_u16(digits)?;
1486 i16::try_from(unsigned).ok()?.checked_mul(sign)
1487}
1488
1489fn parse_hex_byte(value: &[u8]) -> Option<u8> {
1490 if value.len() != 2 {
1491 return None;
1492 }
1493
1494 Some(hex_value(value[0])? * 16 + hex_value(value[1])?)
1495}
1496
1497fn hex_value(value: u8) -> Option<u8> {
1498 match value {
1499 b'0'..=b'9' => Some(value - b'0'),
1500 b'A'..=b'F' => Some(value - b'A' + 10),
1501 b'a'..=b'f' => Some(value - b'a' + 10),
1502 _ => None,
1503 }
1504}
1505
1506fn parse_tagged_u16(report: &[u8], tag: u8, width: usize) -> Option<u16> {
1507 parse_tagged(report, tag, width).and_then(parse_u16)
1508}
1509
1510fn parse_tagged_i16(report: &[u8], tag: u8, width: usize) -> Option<i16> {
1511 parse_tagged(report, tag, width).and_then(parse_i16)
1512}
1513
1514fn parse_tagged(report: &[u8], tag: u8, width: usize) -> Option<&[u8]> {
1515 let start = report.iter().position(|byte| *byte == tag)? + 1;
1516 report.get(start..start + width)
1517}
1518
1519fn decode_mic_e_status(destination: &[u8]) -> Option<MicEStatus> {
1520 if destination.len() != 6 {
1521 return None;
1522 }
1523
1524 let bytes = destination.get(..3)?;
1525 Some(MicEStatus::Custom([
1526 mic_e_status_bit(bytes[0])?,
1527 mic_e_status_bit(bytes[1])?,
1528 mic_e_status_bit(bytes[2])?,
1529 ]))
1530}
1531
1532fn mic_e_status_bit(byte: u8) -> Option<bool> {
1533 match byte {
1534 b'0'..=b'9' | b'L' => Some(false),
1535 b'A'..=b'K' | b'P'..=b'Z' => Some(true),
1536 _ => None,
1537 }
1538}
1539
1540fn decode_mic_e_latitude_digits(destination: &[u8]) -> Option<[u8; 6]> {
1541 if destination.len() != 6 {
1542 return None;
1543 }
1544
1545 let mut digits = [0u8; 6];
1546 for (index, byte) in destination.iter().copied().enumerate() {
1547 digits[index] = mic_e_latitude_digit(byte)?;
1548 }
1549
1550 Some(digits)
1551}
1552
1553fn mic_e_latitude_digit(byte: u8) -> Option<u8> {
1554 match byte {
1555 b'0'..=b'9' => Some(byte - b'0'),
1556 b'A'..=b'J' => Some(byte - b'A'),
1557 b'P'..=b'Y' => Some(byte - b'P'),
1558 b'K' | b'L' | b'Z' => Some(0),
1559 _ => None,
1560 }
1561}
1562
1563fn decode_mic_e_latitude(destination: &[u8]) -> Option<f64> {
1564 let digits = decode_mic_e_latitude_digits(destination)?;
1565 let degrees = u16::from(digits[0]) * 10 + u16::from(digits[1]);
1566 let minutes = u16::from(digits[2]) * 10 + u16::from(digits[3]);
1567 let hundredths = u16::from(digits[4]) * 10 + u16::from(digits[5]);
1568 if degrees > 90 || minutes > 59 {
1569 return None;
1570 }
1571
1572 let sign = if mic_e_north(destination[3])? {
1573 1.0
1574 } else {
1575 -1.0
1576 };
1577 Some(sign * (f64::from(degrees) + (f64::from(minutes) + f64::from(hundredths) / 100.0) / 60.0))
1578}
1579
1580fn decode_mic_e_longitude(destination: &[u8], body: &[u8]) -> Option<f64> {
1581 if destination.len() != 6 || body.len() < 3 {
1582 return None;
1583 }
1584
1585 let mut degrees = i16::from(mic_e_body_value(body[0])?);
1586 if mic_e_longitude_offset(destination[4])? {
1587 degrees += 100;
1588 }
1589 if (180..=189).contains(°rees) {
1590 degrees -= 80;
1591 } else if (190..=199).contains(°rees) {
1592 degrees -= 190;
1593 }
1594
1595 let minutes = mic_e_body_value(body[1])?;
1596 let hundredths = mic_e_body_value(body[2])?;
1597 if !(0..=179).contains(°rees) || minutes > 59 || hundredths > 99 {
1598 return None;
1599 }
1600
1601 let sign = if mic_e_west(destination[5])? {
1602 -1.0
1603 } else {
1604 1.0
1605 };
1606 Some(sign * (f64::from(degrees) + (f64::from(minutes) + f64::from(hundredths) / 100.0) / 60.0))
1607}
1608
1609fn decode_mic_e_speed_course(body: &[u8]) -> Option<MicESpeedCourse> {
1610 if body.len() < 6 {
1611 return None;
1612 }
1613
1614 let speed_tens = u16::from(mic_e_body_value(body[3])?);
1615 let speed_units_course_hundreds = u16::from(mic_e_body_value(body[4])?);
1616 let course_remainder = u16::from(mic_e_body_value(body[5])?);
1617 let mut speed_knots = speed_tens * 10 + speed_units_course_hundreds / 10;
1618 if speed_knots >= 800 {
1619 speed_knots -= 800;
1620 }
1621
1622 Some(MicESpeedCourse {
1623 speed_knots,
1624 course_degrees: (speed_units_course_hundreds % 10) * 100 + course_remainder,
1625 })
1626}
1627
1628fn mic_e_body_value(byte: u8) -> Option<u8> {
1629 let value = byte.checked_sub(28)?;
1630 (value <= 99).then_some(value)
1631}
1632
1633fn mic_e_north(byte: u8) -> Option<bool> {
1634 match byte {
1635 b'0'..=b'9' | b'A'..=b'L' => Some(false),
1636 b'P'..=b'Z' => Some(true),
1637 _ => None,
1638 }
1639}
1640
1641fn mic_e_longitude_offset(byte: u8) -> Option<bool> {
1642 match byte {
1643 b'0'..=b'9' | b'A'..=b'L' => Some(false),
1644 b'P'..=b'Z' => Some(true),
1645 _ => None,
1646 }
1647}
1648
1649fn mic_e_west(byte: u8) -> Option<bool> {
1650 match byte {
1651 b'0'..=b'9' | b'A'..=b'L' => Some(false),
1652 b'P'..=b'Z' => Some(true),
1653 _ => None,
1654 }
1655}
1656
1657#[derive(Clone, Debug, Eq, PartialEq)]
1659pub enum ParseError {
1660 Empty,
1662 Oversized,
1664 MissingSeparator,
1666 EmptySegment,
1668 InvalidAddress,
1670}
1671
1672impl ParseError {
1673 #[must_use]
1675 pub fn code(&self) -> &'static str {
1676 match self {
1677 Self::Empty => "parse.empty",
1678 Self::Oversized => "parse.oversized",
1679 Self::MissingSeparator => "parse.missing_separator",
1680 Self::EmptySegment => "parse.empty_segment",
1681 Self::InvalidAddress => "parse.invalid_address",
1682 }
1683 }
1684}
1685
1686pub fn parse_packet(input: &[u8]) -> Result<ParsedPacket, ParseError> {
1692 parse_packet_with_options(input, ParseOptions::default())
1693}
1694
1695pub fn parse_packet_with_options(
1697 input: &[u8],
1698 options: ParseOptions,
1699) -> Result<ParsedPacket, ParseError> {
1700 if input.is_empty() {
1701 return Err(ParseError::Empty);
1702 }
1703
1704 if input.len() > options.max_packet_len {
1705 return Err(ParseError::Oversized);
1706 }
1707
1708 let source_end = input
1709 .iter()
1710 .position(|byte| *byte == b'>')
1711 .ok_or(ParseError::MissingSeparator)?;
1712 let payload_separator = input[source_end + 1..]
1713 .iter()
1714 .position(|byte| *byte == b':')
1715 .map(|offset| source_end + 1 + offset)
1716 .ok_or(ParseError::MissingSeparator)?;
1717
1718 let path_start = source_end + 1;
1719 let path_end = payload_separator;
1720 let payload_start = payload_separator + 1;
1721
1722 if source_end == 0 || path_start == path_end || payload_start == input.len() {
1723 return Err(ParseError::EmptySegment);
1724 }
1725
1726 let Some(path_components) = path_component_ranges(input, path_start, path_end) else {
1727 return Err(ParseError::InvalidAddress);
1728 };
1729
1730 if !is_ax25_like_source(&input[..source_end])
1731 || !path_components
1732 .iter()
1733 .all(|(start, end)| is_ax25_like_path_component(&input[*start..*end]))
1734 {
1735 return Err(ParseError::InvalidAddress);
1736 }
1737
1738 Ok(ParsedPacket {
1739 raw: RawPacket {
1740 bytes: input.to_vec(),
1741 },
1742 source_end,
1743 path_start,
1744 path_end,
1745 path_components,
1746 payload_start,
1747 })
1748}
1749
1750fn path_component_ranges(
1751 input: &[u8],
1752 path_start: usize,
1753 path_end: usize,
1754) -> Option<Vec<(usize, usize)>> {
1755 let mut components = Vec::new();
1756 let mut component_start = path_start;
1757
1758 for (offset, byte) in input[path_start..path_end].iter().enumerate() {
1759 if *byte == b',' {
1760 let index = path_start + offset;
1761 if component_start == index {
1762 return None;
1763 }
1764 components.push((component_start, index));
1765 component_start = index + 1;
1766 }
1767 }
1768
1769 if component_start == path_end {
1770 return None;
1771 }
1772
1773 components.push((component_start, path_end));
1774 Some(components)
1775}
1776
1777fn is_ax25_like_source(source: &[u8]) -> bool {
1778 is_ax25_like_address(source, false)
1779}
1780
1781fn is_ax25_like_path_component(component: &[u8]) -> bool {
1782 is_ax25_like_address(component, true)
1783}
1784
1785fn is_ax25_like_address(address: &[u8], allow_repeated_marker: bool) -> bool {
1786 let address = if allow_repeated_marker {
1787 address.strip_suffix(b"*").unwrap_or(address)
1788 } else {
1789 address
1790 };
1791
1792 if address.is_empty() || address.contains(&b'*') {
1793 return false;
1794 }
1795
1796 let (callsign, ssid) = match address.iter().position(|byte| *byte == b'-') {
1797 Some(separator) => (&address[..separator], Some(&address[separator + 1..])),
1798 None => (address, None),
1799 };
1800
1801 is_ax25_like_callsign(callsign) && ssid.map_or(true, is_ax25_like_ssid)
1802}
1803
1804fn is_ax25_like_callsign(callsign: &[u8]) -> bool {
1805 (1..=6).contains(&callsign.len())
1806 && callsign
1807 .iter()
1808 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit())
1809}
1810
1811fn is_ax25_like_ssid(ssid: &[u8]) -> bool {
1812 if ssid.is_empty() || ssid.len() > 2 || !ssid.iter().all(u8::is_ascii_digit) {
1813 return false;
1814 }
1815
1816 let mut value = 0u8;
1817 for digit in ssid {
1818 value = value * 10 + (digit - b'0');
1819 }
1820
1821 value <= 15
1822}