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 reject_invalid_nmea_checksum: bool,
310 pub max_path_components: usize,
312}
313
314impl Policy {
315 #[must_use]
317 pub fn strict() -> Self {
318 Self::default()
319 }
320
321 #[must_use]
323 pub fn permissive() -> Self {
324 Self {
325 allow_unsupported: true,
326 allow_malformed_semantics: true,
327 reject_invalid_nmea_checksum: false,
328 max_path_components: 9,
329 }
330 }
331
332 #[must_use]
334 pub fn evaluate(&self, packet: &ParsedPacket, semantic: &AprsData<'_>) -> PolicyDecision {
335 if packet.path_components.len() > self.max_path_components {
336 return PolicyDecision::Reject(PolicyRejection::PathTooLong);
337 }
338
339 if self.reject_invalid_nmea_checksum
340 && matches!(
341 semantic,
342 AprsData::Nmea(nmea) if nmea.checksum().is_some_and(|checksum| !checksum.valid)
343 )
344 {
345 return PolicyDecision::Reject(PolicyRejection::InvalidNmeaChecksum);
346 }
347
348 match semantic {
349 AprsData::Malformed { .. } if !self.allow_malformed_semantics => {
350 PolicyDecision::Reject(PolicyRejection::MalformedSemantics)
351 }
352 AprsData::Unsupported { .. } if !self.allow_unsupported => {
353 PolicyDecision::Reject(PolicyRejection::UnsupportedSemantics)
354 }
355 _ => PolicyDecision::Accept,
356 }
357 }
358}
359
360impl Default for Policy {
361 fn default() -> Self {
362 Self {
363 allow_unsupported: false,
364 allow_malformed_semantics: false,
365 reject_invalid_nmea_checksum: false,
366 max_path_components: 9,
367 }
368 }
369}
370
371#[derive(Clone, Copy, Debug, Eq, PartialEq)]
373pub enum PolicyDecision {
374 Accept,
376 Reject(PolicyRejection),
378}
379
380#[derive(Clone, Copy, Debug, Eq, PartialEq)]
382pub enum PolicyRejection {
383 PathTooLong,
385 MalformedSemantics,
387 UnsupportedSemantics,
389 InvalidNmeaChecksum,
391}
392
393impl PolicyRejection {
394 #[must_use]
396 pub fn code(self) -> &'static str {
397 match self {
398 Self::PathTooLong => "policy.path_too_long",
399 Self::MalformedSemantics => "policy.malformed_semantics",
400 Self::UnsupportedSemantics => "policy.unsupported_semantics",
401 Self::InvalidNmeaChecksum => "policy.nmea_checksum_mismatch",
402 }
403 }
404}
405
406#[derive(Clone, Copy, Debug, Eq, PartialEq)]
408pub enum AprsData<'a> {
409 Status {
411 text: &'a [u8],
413 },
414 Position(Position<'a>),
416 TimestampedPosition(TimestampedPosition<'a>),
418 CompressedPosition(CompressedPosition<'a>),
420 Message(Message<'a>),
422 Object(Object<'a>),
424 Item(Item<'a>),
426 Weather(Weather<'a>),
428 Telemetry(Telemetry<'a>),
430 TelemetryMetadata(TelemetryMetadata<'a>),
432 Query(Query<'a>),
434 Capability(Capability<'a>),
436 Nmea(Nmea<'a>),
438 MicE(MicE<'a>),
440 Maidenhead(Maidenhead<'a>),
442 UserDefined(UserDefined<'a>),
444 ThirdParty(ThirdParty<'a>),
446 Unsupported {
448 identifier: u8,
450 information: &'a [u8],
452 },
453 Malformed {
455 identifier: u8,
457 information: &'a [u8],
459 },
460}
461
462impl AprsData<'_> {
463 #[must_use]
465 pub fn kind_name(&self) -> &'static str {
466 match self {
467 Self::Status { .. } => "status",
468 Self::Position(_) => "position",
469 Self::TimestampedPosition(_) => "timestamped_position",
470 Self::CompressedPosition(_) => "compressed_position",
471 Self::Message(_) => "message",
472 Self::Object(_) => "object",
473 Self::Item(_) => "item",
474 Self::Weather(_) => "weather",
475 Self::Telemetry(_) => "telemetry",
476 Self::TelemetryMetadata(_) => "telemetry_metadata",
477 Self::Query(_) => "query",
478 Self::Capability(_) => "capability",
479 Self::Nmea(_) => "nmea",
480 Self::MicE(_) => "mic_e",
481 Self::Maidenhead(_) => "maidenhead",
482 Self::UserDefined(_) => "user_defined",
483 Self::ThirdParty(_) => "third_party",
484 Self::Unsupported { .. } => "unsupported",
485 Self::Malformed { .. } => "malformed",
486 }
487 }
488}
489
490fn summary_coordinates(data: AprsData<'_>) -> Option<Coordinates> {
491 match data {
492 AprsData::Position(position) => position.coordinates(),
493 AprsData::TimestampedPosition(position) => position.position.coordinates(),
494 AprsData::CompressedPosition(position) => position.coordinates(),
495 AprsData::MicE(mic_e) => mic_e.coordinates(),
496 _ => None,
497 }
498}
499
500fn summary_nmea_checksum(data: AprsData<'_>) -> Option<NmeaChecksum> {
501 match data {
502 AprsData::Nmea(nmea) => nmea.checksum(),
503 _ => None,
504 }
505}
506
507fn summary_telemetry_sequence(data: AprsData<'_>) -> Option<u16> {
508 match data {
509 AprsData::Telemetry(telemetry) => telemetry.sequence_number(),
510 _ => None,
511 }
512}
513
514fn summary_mic_e_speed_course(data: AprsData<'_>) -> Option<MicESpeedCourse> {
515 match data {
516 AprsData::MicE(mic_e) => mic_e.speed_course(),
517 _ => None,
518 }
519}
520
521#[derive(Clone, Copy, Debug, Eq, PartialEq)]
523pub struct Position<'a> {
524 pub messaging: bool,
526 pub latitude: &'a [u8],
528 pub symbol_table: u8,
530 pub longitude: &'a [u8],
532 pub symbol_code: u8,
534 pub comment: &'a [u8],
536}
537
538impl Position<'_> {
539 #[must_use]
541 pub fn coordinates(&self) -> Option<Coordinates> {
542 Some(Coordinates {
543 latitude: decode_latitude(self.latitude)?,
544 longitude: decode_longitude(self.longitude)?,
545 })
546 }
547}
548
549#[derive(Clone, Copy, Debug, PartialEq)]
551pub struct Coordinates {
552 pub latitude: f64,
554 pub longitude: f64,
556}
557
558#[derive(Clone, Copy, Debug, Eq, PartialEq)]
560pub struct TimestampedPosition<'a> {
561 pub messaging: bool,
563 pub timestamp: &'a [u8],
565 pub position: Position<'a>,
567}
568
569#[derive(Clone, Copy, Debug, Eq, PartialEq)]
571pub struct CompressedPosition<'a> {
572 pub messaging: bool,
574 pub symbol_table: u8,
576 pub compressed_latitude: &'a [u8],
578 pub compressed_longitude: &'a [u8],
580 pub symbol_code: u8,
582 pub extension: &'a [u8],
584 pub compression_type: u8,
586 pub comment: &'a [u8],
588}
589
590impl CompressedPosition<'_> {
591 #[must_use]
593 pub fn coordinates(&self) -> Option<Coordinates> {
594 let y = decode_base91(self.compressed_latitude)?;
595 let x = decode_base91(self.compressed_longitude)?;
596
597 Some(Coordinates {
598 latitude: 90.0 - (y as f64 / 380_926.0),
599 longitude: -180.0 + (x as f64 / 190_463.0),
600 })
601 }
602}
603
604#[derive(Clone, Copy, Debug, Eq, PartialEq)]
606pub struct Message<'a> {
607 pub addressee: &'a [u8],
609 pub kind: MessageKind,
611 pub text: &'a [u8],
613 pub id: Option<&'a [u8]>,
615}
616
617#[derive(Clone, Copy, Debug, Eq, PartialEq)]
619pub enum MessageKind {
620 Message,
622 Ack,
624 Reject,
626 Bulletin,
628 Announcement,
630}
631
632#[derive(Clone, Copy, Debug, Eq, PartialEq)]
634pub struct Object<'a> {
635 pub name: &'a [u8],
637 pub live: bool,
639 pub timestamp: &'a [u8],
641 pub body: &'a [u8],
643}
644
645#[derive(Clone, Copy, Debug, Eq, PartialEq)]
647pub struct Item<'a> {
648 pub name: &'a [u8],
650 pub live: bool,
652 pub body: &'a [u8],
654}
655
656#[derive(Clone, Copy, Debug, Eq, PartialEq)]
658pub struct Weather<'a> {
659 pub report: &'a [u8],
661}
662
663impl Weather<'_> {
664 #[must_use]
666 pub fn fields(&self) -> WeatherFields<'_> {
667 WeatherFields {
668 timestamp: self
669 .report
670 .get(..6)
671 .filter(|value| value.iter().all(u8::is_ascii_digit)),
672 wind_direction_degrees: parse_tagged_u16(self.report, b'c', 3),
673 wind_speed_mph: parse_tagged_u16(self.report, b's', 3),
674 wind_gust_mph: parse_tagged_u16(self.report, b'g', 3),
675 temperature_fahrenheit: parse_tagged_i16(self.report, b't', 3),
676 rain_last_hour_hundredths_inch: parse_tagged_u16(self.report, b'r', 3),
677 rain_last_24_hours_hundredths_inch: parse_tagged_u16(self.report, b'p', 3),
678 rain_since_midnight_hundredths_inch: parse_tagged_u16(self.report, b'P', 3),
679 humidity_percent: parse_tagged_u16(self.report, b'h', 2).map(|value| {
680 if value == 0 {
681 100
682 } else {
683 value
684 }
685 }),
686 pressure_tenths_hpa: parse_tagged_u16(self.report, b'b', 5),
687 luminosity_watts_per_square_meter: parse_tagged_u16(self.report, b'L', 3),
688 luminosity_1000_plus_watts_per_square_meter: parse_tagged_u16(self.report, b'l', 3)
689 .map(|value| value + 1000),
690 snow_last_24_hours_inches: parse_tagged_u16(self.report, b'S', 3),
691 raw_rain_counter: parse_tagged_u16(self.report, b'#', 3),
692 }
693 }
694}
695
696#[derive(Clone, Copy, Debug, Eq, PartialEq)]
698pub struct WeatherFields<'a> {
699 pub timestamp: Option<&'a [u8]>,
701 pub wind_direction_degrees: Option<u16>,
703 pub wind_speed_mph: Option<u16>,
705 pub wind_gust_mph: Option<u16>,
707 pub temperature_fahrenheit: Option<i16>,
709 pub rain_last_hour_hundredths_inch: Option<u16>,
711 pub rain_last_24_hours_hundredths_inch: Option<u16>,
713 pub rain_since_midnight_hundredths_inch: Option<u16>,
715 pub humidity_percent: Option<u16>,
717 pub pressure_tenths_hpa: Option<u16>,
719 pub luminosity_watts_per_square_meter: Option<u16>,
721 pub luminosity_1000_plus_watts_per_square_meter: Option<u16>,
723 pub snow_last_24_hours_inches: Option<u16>,
725 pub raw_rain_counter: Option<u16>,
727}
728
729#[derive(Clone, Copy, Debug, Eq, PartialEq)]
731pub struct Telemetry<'a> {
732 pub sequence: &'a [u8],
734 pub analog: [&'a [u8]; 5],
736 pub digital: Option<&'a [u8]>,
738}
739
740impl Telemetry<'_> {
741 #[must_use]
743 pub fn sequence_number(&self) -> Option<u16> {
744 parse_u16(self.sequence)
745 }
746
747 #[must_use]
749 pub fn analog_values(&self) -> Option<[u16; 5]> {
750 Some([
751 parse_u16(self.analog[0])?,
752 parse_u16(self.analog[1])?,
753 parse_u16(self.analog[2])?,
754 parse_u16(self.analog[3])?,
755 parse_u16(self.analog[4])?,
756 ])
757 }
758
759 #[must_use]
761 pub fn digital_bits(&self) -> Option<[bool; 8]> {
762 let digital = self.digital?;
763 if digital.len() != 8 {
764 return None;
765 }
766
767 let mut bits = [false; 8];
768 for (index, byte) in digital.iter().enumerate() {
769 bits[index] = match byte {
770 b'0' => false,
771 b'1' => true,
772 _ => return None,
773 };
774 }
775
776 Some(bits)
777 }
778}
779
780#[derive(Clone, Copy, Debug, Eq, PartialEq)]
782pub struct TelemetryMetadata<'a> {
783 pub addressee: &'a [u8],
785 pub kind: TelemetryMetadataKind,
787 pub body: &'a [u8],
789}
790
791impl<'a> TelemetryMetadata<'a> {
792 #[must_use]
794 pub fn fields(&self) -> Vec<&'a [u8]> {
795 self.body.split(|byte| *byte == b',').collect()
796 }
797}
798
799#[derive(Clone, Copy, Debug, Eq, PartialEq)]
801pub enum TelemetryMetadataKind {
802 ParameterNames,
804 Units,
806 Equations,
808 BitSense,
810}
811
812#[derive(Clone, Copy, Debug, Eq, PartialEq)]
814pub struct Query<'a> {
815 pub query: &'a [u8],
817}
818
819#[derive(Clone, Copy, Debug, Eq, PartialEq)]
821pub struct Capability<'a> {
822 pub body: &'a [u8],
824}
825
826#[derive(Clone, Copy, Debug, Eq, PartialEq)]
828pub struct Nmea<'a> {
829 pub sentence: &'a [u8],
831}
832
833impl Nmea<'_> {
834 #[must_use]
836 pub fn checksum(&self) -> Option<NmeaChecksum> {
837 let separator = self.sentence.iter().rposition(|byte| *byte == b'*')?;
838 let checksum = self.sentence.get(separator + 1..separator + 3)?;
839 if checksum.len() != 2 || self.sentence.get(separator + 3).is_some() {
840 return None;
841 }
842
843 let expected = parse_hex_byte(checksum)?;
844 let calculated = self.sentence[..separator]
845 .iter()
846 .fold(0u8, |accumulator, byte| accumulator ^ byte);
847
848 Some(NmeaChecksum {
849 expected,
850 calculated,
851 valid: expected == calculated,
852 })
853 }
854}
855
856#[derive(Clone, Copy, Debug, Eq, PartialEq)]
858pub struct NmeaChecksum {
859 pub expected: u8,
861 pub calculated: u8,
863 pub valid: bool,
865}
866
867#[derive(Clone, Copy, Debug, Eq, PartialEq)]
869pub struct MicE<'a> {
870 pub identifier: u8,
872 pub destination: &'a [u8],
874 pub body: &'a [u8],
876 pub status: Option<MicEStatus>,
878 pub latitude_digits: Option<[u8; 6]>,
880}
881
882impl MicE<'_> {
883 #[must_use]
885 pub fn coordinates(&self) -> Option<Coordinates> {
886 Some(Coordinates {
887 latitude: decode_mic_e_latitude(self.destination)?,
888 longitude: decode_mic_e_longitude(self.destination, self.body)?,
889 })
890 }
891
892 #[must_use]
894 pub fn speed_course(&self) -> Option<MicESpeedCourse> {
895 decode_mic_e_speed_course(self.body)
896 }
897}
898
899#[derive(Clone, Copy, Debug, Eq, PartialEq)]
901pub enum MicEStatus {
902 Custom([bool; 3]),
904}
905
906#[derive(Clone, Copy, Debug, Eq, PartialEq)]
908pub struct MicESpeedCourse {
909 pub speed_knots: u16,
911 pub course_degrees: u16,
913}
914
915#[derive(Clone, Copy, Debug, Eq, PartialEq)]
917pub struct Maidenhead<'a> {
918 pub locator: &'a [u8],
920 pub comment: &'a [u8],
922}
923
924#[derive(Clone, Copy, Debug, Eq, PartialEq)]
926pub struct UserDefined<'a> {
927 pub user_id: u8,
929 pub packet_type: u8,
931 pub body: &'a [u8],
933}
934
935#[derive(Clone, Copy, Debug, Eq, PartialEq)]
937pub struct ThirdParty<'a> {
938 pub body: &'a [u8],
940}
941
942impl ThirdParty<'_> {
943 pub fn nested_packet(&self) -> Result<ParsedPacket, ParseError> {
945 parse_packet(self.body)
946 }
947}
948
949#[derive(Clone, Copy, Debug, Eq, PartialEq)]
951pub enum DataTypeIdentifier {
952 PositionNoTimestamp,
954 PositionNoTimestampMessaging,
956 PositionWithTimestamp,
958 PositionWithTimestampMessaging,
960 Status,
962 Query,
964 Capability,
966 Message,
968 Object,
970 Item,
972 Weather,
974 Telemetry,
976 Nmea,
978 MicECurrent,
980 MicEOld,
982 Maidenhead,
984 UserDefined,
986 ThirdParty,
988 Unknown(u8),
990}
991
992impl DataTypeIdentifier {
993 fn from_byte(byte: u8) -> Self {
994 match byte {
995 b'!' => Self::PositionNoTimestamp,
996 b'=' => Self::PositionNoTimestampMessaging,
997 b'/' => Self::PositionWithTimestamp,
998 b'@' => Self::PositionWithTimestampMessaging,
999 b'>' => Self::Status,
1000 b'?' => Self::Query,
1001 b'<' => Self::Capability,
1002 b':' => Self::Message,
1003 b';' => Self::Object,
1004 b')' => Self::Item,
1005 b'_' => Self::Weather,
1006 b'T' => Self::Telemetry,
1007 b'$' => Self::Nmea,
1008 b'`' => Self::MicECurrent,
1009 b'\'' => Self::MicEOld,
1010 b'[' => Self::Maidenhead,
1011 b'{' => Self::UserDefined,
1012 b'}' => Self::ThirdParty,
1013 other => Self::Unknown(other),
1014 }
1015 }
1016
1017 fn as_byte(self) -> u8 {
1018 match self {
1019 Self::PositionNoTimestamp => b'!',
1020 Self::PositionNoTimestampMessaging => b'=',
1021 Self::PositionWithTimestamp => b'/',
1022 Self::PositionWithTimestampMessaging => b'@',
1023 Self::Status => b'>',
1024 Self::Query => b'?',
1025 Self::Capability => b'<',
1026 Self::Message => b':',
1027 Self::Object => b';',
1028 Self::Item => b')',
1029 Self::Weather => b'_',
1030 Self::Telemetry => b'T',
1031 Self::Nmea => b'$',
1032 Self::MicECurrent => b'`',
1033 Self::MicEOld => b'\'',
1034 Self::Maidenhead => b'[',
1035 Self::UserDefined => b'{',
1036 Self::ThirdParty => b'}',
1037 Self::Unknown(value) => value,
1038 }
1039 }
1040
1041 #[must_use]
1043 pub fn name(self) -> &'static str {
1044 match self {
1045 Self::PositionNoTimestamp => "position_no_timestamp",
1046 Self::PositionNoTimestampMessaging => "position_no_timestamp_messaging",
1047 Self::PositionWithTimestamp => "position_with_timestamp",
1048 Self::PositionWithTimestampMessaging => "position_with_timestamp_messaging",
1049 Self::Status => "status",
1050 Self::Query => "query",
1051 Self::Capability => "capability",
1052 Self::Message => "message",
1053 Self::Object => "object",
1054 Self::Item => "item",
1055 Self::Weather => "weather",
1056 Self::Telemetry => "telemetry",
1057 Self::Nmea => "nmea",
1058 Self::MicECurrent => "mic_e_current",
1059 Self::MicEOld => "mic_e_old",
1060 Self::Maidenhead => "maidenhead",
1061 Self::UserDefined => "user_defined",
1062 Self::ThirdParty => "third_party",
1063 Self::Unknown(_) => "unknown",
1064 }
1065 }
1066}
1067
1068fn parse_aprs_data<'a>(
1069 identifier: DataTypeIdentifier,
1070 information: &'a [u8],
1071 destination: &'a [u8],
1072) -> AprsData<'a> {
1073 match identifier {
1074 DataTypeIdentifier::Status => AprsData::Status { text: information },
1075 DataTypeIdentifier::PositionNoTimestamp => parse_position(false, b'!', information),
1076 DataTypeIdentifier::PositionNoTimestampMessaging => parse_position(true, b'=', information),
1077 DataTypeIdentifier::PositionWithTimestamp => {
1078 parse_timestamped_position(false, b'/', information)
1079 }
1080 DataTypeIdentifier::PositionWithTimestampMessaging => {
1081 parse_timestamped_position(true, b'@', information)
1082 }
1083 DataTypeIdentifier::Message => parse_message(information),
1084 DataTypeIdentifier::Object => parse_object(information),
1085 DataTypeIdentifier::Item => parse_item(information),
1086 DataTypeIdentifier::Weather => AprsData::Weather(Weather {
1087 report: information,
1088 }),
1089 DataTypeIdentifier::Telemetry => parse_telemetry(information),
1090 DataTypeIdentifier::Query => AprsData::Query(Query { query: information }),
1091 DataTypeIdentifier::Capability => AprsData::Capability(Capability { body: information }),
1092 DataTypeIdentifier::Nmea => AprsData::Nmea(Nmea {
1093 sentence: information,
1094 }),
1095 DataTypeIdentifier::MicECurrent | DataTypeIdentifier::MicEOld => {
1096 parse_mic_e(identifier, information, destination)
1097 }
1098 DataTypeIdentifier::Maidenhead => parse_maidenhead(information),
1099 DataTypeIdentifier::UserDefined => parse_user_defined(information),
1100 DataTypeIdentifier::ThirdParty => AprsData::ThirdParty(ThirdParty { body: information }),
1101 other => AprsData::Unsupported {
1102 identifier: other.as_byte(),
1103 information,
1104 },
1105 }
1106}
1107
1108fn parse_mic_e<'a>(
1109 identifier: DataTypeIdentifier,
1110 information: &'a [u8],
1111 destination: &'a [u8],
1112) -> AprsData<'a> {
1113 AprsData::MicE(MicE {
1114 identifier: identifier.as_byte(),
1115 destination,
1116 body: information,
1117 status: decode_mic_e_status(destination),
1118 latitude_digits: decode_mic_e_latitude_digits(destination),
1119 })
1120}
1121
1122fn parse_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1123 if is_compressed_position(information) {
1124 return parse_compressed_position(messaging, identifier, information);
1125 }
1126
1127 if information.len() < 18 {
1128 return AprsData::Malformed {
1129 identifier,
1130 information,
1131 };
1132 }
1133
1134 let latitude = &information[..8];
1135 let symbol_table = information[8];
1136 let longitude = &information[9..18];
1137 let symbol_code = information[18];
1138 let comment = &information[19..];
1139
1140 if !is_latitude(latitude)
1141 || !is_symbol_table_identifier(symbol_table)
1142 || !is_longitude(longitude)
1143 || !is_printable_ascii(symbol_code)
1144 {
1145 return AprsData::Malformed {
1146 identifier,
1147 information,
1148 };
1149 }
1150
1151 AprsData::Position(Position {
1152 messaging,
1153 latitude,
1154 symbol_table,
1155 longitude,
1156 symbol_code,
1157 comment,
1158 })
1159}
1160
1161fn parse_timestamped_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1162 if information.len() < 8 {
1163 return AprsData::Malformed {
1164 identifier,
1165 information,
1166 };
1167 }
1168
1169 let timestamp = &information[..7];
1170 if !is_timestamp(timestamp) {
1171 return AprsData::Malformed {
1172 identifier,
1173 information,
1174 };
1175 }
1176
1177 match parse_position(messaging, identifier, &information[7..]) {
1178 AprsData::Position(position) => AprsData::TimestampedPosition(TimestampedPosition {
1179 messaging,
1180 timestamp,
1181 position,
1182 }),
1183 AprsData::CompressedPosition(position) => AprsData::CompressedPosition(position),
1184 _ => AprsData::Malformed {
1185 identifier,
1186 information,
1187 },
1188 }
1189}
1190
1191fn parse_compressed_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1192 if information.len() < 13 {
1193 return AprsData::Malformed {
1194 identifier,
1195 information,
1196 };
1197 }
1198
1199 let symbol_table = information[0];
1200 let compressed_latitude = &information[1..5];
1201 let compressed_longitude = &information[5..9];
1202 let symbol_code = information[9];
1203 let extension = &information[10..12];
1204 let compression_type = information[12];
1205 let comment = &information[13..];
1206
1207 if !is_symbol_table_identifier(symbol_table)
1208 || !compressed_latitude.iter().all(|byte| is_base91(*byte))
1209 || !compressed_longitude.iter().all(|byte| is_base91(*byte))
1210 || !is_printable_ascii(symbol_code)
1211 || !extension.iter().all(|byte| is_base91(*byte))
1212 || !is_base91(compression_type)
1213 {
1214 return AprsData::Malformed {
1215 identifier,
1216 information,
1217 };
1218 }
1219
1220 AprsData::CompressedPosition(CompressedPosition {
1221 messaging,
1222 symbol_table,
1223 compressed_latitude,
1224 compressed_longitude,
1225 symbol_code,
1226 extension,
1227 compression_type,
1228 comment,
1229 })
1230}
1231
1232fn parse_object(information: &[u8]) -> AprsData<'_> {
1233 if information.len() < 17 || !matches!(information[9], b'*' | b'_') {
1234 return AprsData::Malformed {
1235 identifier: b';',
1236 information,
1237 };
1238 }
1239
1240 AprsData::Object(Object {
1241 name: &information[..9],
1242 live: information[9] == b'*',
1243 timestamp: &information[10..17],
1244 body: &information[17..],
1245 })
1246}
1247
1248fn parse_item(information: &[u8]) -> AprsData<'_> {
1249 let Some(separator) = information
1250 .iter()
1251 .position(|byte| matches!(*byte, b'!' | b'_'))
1252 else {
1253 return AprsData::Malformed {
1254 identifier: b')',
1255 information,
1256 };
1257 };
1258
1259 if separator == 0 || separator > 9 {
1260 return AprsData::Malformed {
1261 identifier: b')',
1262 information,
1263 };
1264 }
1265
1266 AprsData::Item(Item {
1267 name: &information[..separator],
1268 live: information[separator] == b'!',
1269 body: &information[separator + 1..],
1270 })
1271}
1272
1273fn parse_message(information: &[u8]) -> AprsData<'_> {
1274 if information.len() < 10 || information[9] != b':' {
1275 return AprsData::Malformed {
1276 identifier: b':',
1277 information,
1278 };
1279 }
1280
1281 let addressee = &information[..9];
1282 let body = &information[10..];
1283 if let Some(kind) = classify_telemetry_metadata_kind(addressee) {
1284 return AprsData::TelemetryMetadata(TelemetryMetadata {
1285 addressee,
1286 kind,
1287 body,
1288 });
1289 }
1290
1291 let (text, id) = match body.iter().position(|byte| *byte == b'{') {
1292 Some(separator) => (&body[..separator], Some(&body[separator + 1..])),
1293 None => (body, None),
1294 };
1295 let kind = classify_message_kind(addressee, text);
1296
1297 AprsData::Message(Message {
1298 addressee,
1299 kind,
1300 text,
1301 id,
1302 })
1303}
1304
1305fn parse_telemetry(information: &[u8]) -> AprsData<'_> {
1306 if !information.starts_with(b"#") {
1307 return AprsData::Malformed {
1308 identifier: b'T',
1309 information,
1310 };
1311 }
1312
1313 let fields: Vec<&[u8]> = information[1..].split(|byte| *byte == b',').collect();
1314 if fields.len() < 6 || fields[..6].iter().any(|field| field.is_empty()) {
1315 return AprsData::Malformed {
1316 identifier: b'T',
1317 information,
1318 };
1319 }
1320
1321 AprsData::Telemetry(Telemetry {
1322 sequence: fields[0],
1323 analog: [fields[1], fields[2], fields[3], fields[4], fields[5]],
1324 digital: fields.get(6).copied().filter(|field| !field.is_empty()),
1325 })
1326}
1327
1328fn parse_maidenhead(information: &[u8]) -> AprsData<'_> {
1329 if information.len() < 6 {
1330 return AprsData::Malformed {
1331 identifier: b'[',
1332 information,
1333 };
1334 }
1335
1336 AprsData::Maidenhead(Maidenhead {
1337 locator: &information[..6],
1338 comment: &information[6..],
1339 })
1340}
1341
1342fn parse_user_defined(information: &[u8]) -> AprsData<'_> {
1343 if information.len() < 2 {
1344 return AprsData::Malformed {
1345 identifier: b'{',
1346 information,
1347 };
1348 }
1349
1350 AprsData::UserDefined(UserDefined {
1351 user_id: information[0],
1352 packet_type: information[1],
1353 body: &information[2..],
1354 })
1355}
1356
1357fn classify_telemetry_metadata_kind(addressee: &[u8]) -> Option<TelemetryMetadataKind> {
1358 match addressee.get(..5)? {
1359 b"PARM." => Some(TelemetryMetadataKind::ParameterNames),
1360 b"UNIT." => Some(TelemetryMetadataKind::Units),
1361 b"EQNS." => Some(TelemetryMetadataKind::Equations),
1362 b"BITS." => Some(TelemetryMetadataKind::BitSense),
1363 _ => None,
1364 }
1365}
1366
1367fn classify_message_kind(addressee: &[u8], text: &[u8]) -> MessageKind {
1368 if text.starts_with(b"ack") {
1369 MessageKind::Ack
1370 } else if text.starts_with(b"rej") {
1371 MessageKind::Reject
1372 } else if addressee.starts_with(b"BLN") && addressee.get(3).is_some_and(u8::is_ascii_digit) {
1373 MessageKind::Bulletin
1374 } else if addressee.starts_with(b"BLN") && addressee.get(3).is_some_and(u8::is_ascii_uppercase)
1375 {
1376 MessageKind::Announcement
1377 } else {
1378 MessageKind::Message
1379 }
1380}
1381
1382fn is_latitude(value: &[u8]) -> bool {
1383 value.len() == 8
1384 && value[0].is_ascii_digit()
1385 && value[1].is_ascii_digit()
1386 && value[2].is_ascii_digit()
1387 && value[3].is_ascii_digit()
1388 && value[4] == b'.'
1389 && value[5].is_ascii_digit()
1390 && value[6].is_ascii_digit()
1391 && matches!(value[7], b'N' | b'S')
1392}
1393
1394fn is_longitude(value: &[u8]) -> bool {
1395 value.len() == 9
1396 && value[0].is_ascii_digit()
1397 && value[1].is_ascii_digit()
1398 && value[2].is_ascii_digit()
1399 && value[3].is_ascii_digit()
1400 && value[4].is_ascii_digit()
1401 && value[5] == b'.'
1402 && value[6].is_ascii_digit()
1403 && value[7].is_ascii_digit()
1404 && matches!(value[8], b'E' | b'W')
1405}
1406
1407fn is_symbol_table_identifier(value: u8) -> bool {
1408 matches!(value, b'/' | b'\\') || value.is_ascii_alphanumeric()
1409}
1410
1411fn is_printable_ascii(value: u8) -> bool {
1412 (0x20..=0x7e).contains(&value)
1413}
1414
1415fn is_base91(value: u8) -> bool {
1416 (b'!'..=b'{').contains(&value)
1417}
1418
1419fn is_compressed_position(information: &[u8]) -> bool {
1420 information
1421 .first()
1422 .is_some_and(|byte| !byte.is_ascii_digit() && is_symbol_table_identifier(*byte))
1423 && information
1424 .get(1..13)
1425 .is_some_and(|bytes| bytes.iter().all(|byte| is_base91(*byte)))
1426}
1427
1428fn is_timestamp(value: &[u8]) -> bool {
1429 value.len() == 7
1430 && value[..6].iter().all(u8::is_ascii_digit)
1431 && matches!(value[6], b'z' | b'/' | b'h')
1432}
1433
1434fn decode_latitude(value: &[u8]) -> Option<f64> {
1435 if !is_latitude(value) {
1436 return None;
1437 }
1438
1439 let degrees = parse_u16(&value[..2])? as f64;
1440 let minutes = parse_fixed_minutes(&value[2..7])?;
1441 let sign = match value[7] {
1442 b'N' => 1.0,
1443 b'S' => -1.0,
1444 _ => return None,
1445 };
1446
1447 Some(sign * (degrees + minutes / 60.0))
1448}
1449
1450fn decode_longitude(value: &[u8]) -> Option<f64> {
1451 if !is_longitude(value) {
1452 return None;
1453 }
1454
1455 let degrees = parse_u16(&value[..3])? as f64;
1456 let minutes = parse_fixed_minutes(&value[3..8])?;
1457 let sign = match value[8] {
1458 b'E' => 1.0,
1459 b'W' => -1.0,
1460 _ => return None,
1461 };
1462
1463 Some(sign * (degrees + minutes / 60.0))
1464}
1465
1466fn parse_fixed_minutes(value: &[u8]) -> Option<f64> {
1467 if value.len() != 5 || value[2] != b'.' || !value[..2].iter().all(u8::is_ascii_digit) {
1468 return None;
1469 }
1470
1471 let whole = parse_u16(&value[..2])? as f64;
1472 let fraction = parse_u16(&value[3..])? as f64 / 100.0;
1473 Some(whole + fraction)
1474}
1475
1476fn decode_base91(value: &[u8]) -> Option<u32> {
1477 if value.len() != 4 || !value.iter().all(|byte| is_base91(*byte)) {
1478 return None;
1479 }
1480
1481 let mut decoded = 0u32;
1482 for byte in value {
1483 decoded = decoded * 91 + u32::from(byte - b'!');
1484 }
1485
1486 Some(decoded)
1487}
1488
1489fn parse_u16(value: &[u8]) -> Option<u16> {
1490 if value.is_empty() || !value.iter().all(u8::is_ascii_digit) {
1491 return None;
1492 }
1493
1494 let mut parsed = 0u16;
1495 for digit in value {
1496 parsed = parsed.checked_mul(10)?;
1497 parsed = parsed.checked_add(u16::from(digit - b'0'))?;
1498 }
1499
1500 Some(parsed)
1501}
1502
1503fn parse_i16(value: &[u8]) -> Option<i16> {
1504 if value.is_empty() {
1505 return None;
1506 }
1507
1508 let (sign, digits) = match value[0] {
1509 b'-' => (-1, &value[1..]),
1510 b'+' => (1, &value[1..]),
1511 _ => (1, value),
1512 };
1513
1514 let unsigned = parse_u16(digits)?;
1515 i16::try_from(unsigned).ok()?.checked_mul(sign)
1516}
1517
1518fn parse_hex_byte(value: &[u8]) -> Option<u8> {
1519 if value.len() != 2 {
1520 return None;
1521 }
1522
1523 Some(hex_value(value[0])? * 16 + hex_value(value[1])?)
1524}
1525
1526fn hex_value(value: u8) -> Option<u8> {
1527 match value {
1528 b'0'..=b'9' => Some(value - b'0'),
1529 b'A'..=b'F' => Some(value - b'A' + 10),
1530 b'a'..=b'f' => Some(value - b'a' + 10),
1531 _ => None,
1532 }
1533}
1534
1535fn parse_tagged_u16(report: &[u8], tag: u8, width: usize) -> Option<u16> {
1536 parse_tagged(report, tag, width).and_then(parse_u16)
1537}
1538
1539fn parse_tagged_i16(report: &[u8], tag: u8, width: usize) -> Option<i16> {
1540 parse_tagged(report, tag, width).and_then(parse_i16)
1541}
1542
1543fn parse_tagged(report: &[u8], tag: u8, width: usize) -> Option<&[u8]> {
1544 let start = report.iter().position(|byte| *byte == tag)? + 1;
1545 report.get(start..start + width)
1546}
1547
1548fn decode_mic_e_status(destination: &[u8]) -> Option<MicEStatus> {
1549 if destination.len() != 6 {
1550 return None;
1551 }
1552
1553 let bytes = destination.get(..3)?;
1554 Some(MicEStatus::Custom([
1555 mic_e_status_bit(bytes[0])?,
1556 mic_e_status_bit(bytes[1])?,
1557 mic_e_status_bit(bytes[2])?,
1558 ]))
1559}
1560
1561fn mic_e_status_bit(byte: u8) -> Option<bool> {
1562 match byte {
1563 b'0'..=b'9' | b'L' => Some(false),
1564 b'A'..=b'K' | b'P'..=b'Z' => Some(true),
1565 _ => None,
1566 }
1567}
1568
1569fn decode_mic_e_latitude_digits(destination: &[u8]) -> Option<[u8; 6]> {
1570 if destination.len() != 6 {
1571 return None;
1572 }
1573
1574 let mut digits = [0u8; 6];
1575 for (index, byte) in destination.iter().copied().enumerate() {
1576 digits[index] = mic_e_latitude_digit(byte)?;
1577 }
1578
1579 Some(digits)
1580}
1581
1582fn mic_e_latitude_digit(byte: u8) -> Option<u8> {
1583 match byte {
1584 b'0'..=b'9' => Some(byte - b'0'),
1585 b'A'..=b'J' => Some(byte - b'A'),
1586 b'P'..=b'Y' => Some(byte - b'P'),
1587 b'K' | b'L' | b'Z' => Some(0),
1588 _ => None,
1589 }
1590}
1591
1592fn decode_mic_e_latitude(destination: &[u8]) -> Option<f64> {
1593 let digits = decode_mic_e_latitude_digits(destination)?;
1594 let degrees = u16::from(digits[0]) * 10 + u16::from(digits[1]);
1595 let minutes = u16::from(digits[2]) * 10 + u16::from(digits[3]);
1596 let hundredths = u16::from(digits[4]) * 10 + u16::from(digits[5]);
1597 if degrees > 90 || minutes > 59 {
1598 return None;
1599 }
1600
1601 let sign = if mic_e_north(destination[3])? {
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_longitude(destination: &[u8], body: &[u8]) -> Option<f64> {
1610 if destination.len() != 6 || body.len() < 3 {
1611 return None;
1612 }
1613
1614 let mut degrees = i16::from(mic_e_body_value(body[0])?);
1615 if mic_e_longitude_offset(destination[4])? {
1616 degrees += 100;
1617 }
1618 if (180..=189).contains(°rees) {
1619 degrees -= 80;
1620 } else if (190..=199).contains(°rees) {
1621 degrees -= 190;
1622 }
1623
1624 let minutes = mic_e_body_value(body[1])?;
1625 let hundredths = mic_e_body_value(body[2])?;
1626 if !(0..=179).contains(°rees) || minutes > 59 || hundredths > 99 {
1627 return None;
1628 }
1629
1630 let sign = if mic_e_west(destination[5])? {
1631 -1.0
1632 } else {
1633 1.0
1634 };
1635 Some(sign * (f64::from(degrees) + (f64::from(minutes) + f64::from(hundredths) / 100.0) / 60.0))
1636}
1637
1638fn decode_mic_e_speed_course(body: &[u8]) -> Option<MicESpeedCourse> {
1639 if body.len() < 6 {
1640 return None;
1641 }
1642
1643 let speed_tens = u16::from(mic_e_body_value(body[3])?);
1644 let speed_units_course_hundreds = u16::from(mic_e_body_value(body[4])?);
1645 let course_remainder = u16::from(mic_e_body_value(body[5])?);
1646 let mut speed_knots = speed_tens * 10 + speed_units_course_hundreds / 10;
1647 if speed_knots >= 800 {
1648 speed_knots -= 800;
1649 }
1650
1651 Some(MicESpeedCourse {
1652 speed_knots,
1653 course_degrees: (speed_units_course_hundreds % 10) * 100 + course_remainder,
1654 })
1655}
1656
1657fn mic_e_body_value(byte: u8) -> Option<u8> {
1658 let value = byte.checked_sub(28)?;
1659 (value <= 99).then_some(value)
1660}
1661
1662fn mic_e_north(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
1670fn mic_e_longitude_offset(byte: u8) -> Option<bool> {
1671 match byte {
1672 b'0'..=b'9' | b'A'..=b'L' => Some(false),
1673 b'P'..=b'Z' => Some(true),
1674 _ => None,
1675 }
1676}
1677
1678fn mic_e_west(byte: u8) -> Option<bool> {
1679 match byte {
1680 b'0'..=b'9' | b'A'..=b'L' => Some(false),
1681 b'P'..=b'Z' => Some(true),
1682 _ => None,
1683 }
1684}
1685
1686#[derive(Clone, Debug, Eq, PartialEq)]
1688pub enum ParseError {
1689 Empty,
1691 Oversized,
1693 MissingSeparator,
1695 EmptySegment,
1697 InvalidAddress,
1699}
1700
1701impl ParseError {
1702 #[must_use]
1704 pub fn code(&self) -> &'static str {
1705 match self {
1706 Self::Empty => "parse.empty",
1707 Self::Oversized => "parse.oversized",
1708 Self::MissingSeparator => "parse.missing_separator",
1709 Self::EmptySegment => "parse.empty_segment",
1710 Self::InvalidAddress => "parse.invalid_address",
1711 }
1712 }
1713}
1714
1715pub fn parse_packet(input: &[u8]) -> Result<ParsedPacket, ParseError> {
1721 parse_packet_with_options(input, ParseOptions::default())
1722}
1723
1724pub fn parse_packet_with_options(
1726 input: &[u8],
1727 options: ParseOptions,
1728) -> Result<ParsedPacket, ParseError> {
1729 if input.is_empty() {
1730 return Err(ParseError::Empty);
1731 }
1732
1733 if input.len() > options.max_packet_len {
1734 return Err(ParseError::Oversized);
1735 }
1736
1737 let source_end = input
1738 .iter()
1739 .position(|byte| *byte == b'>')
1740 .ok_or(ParseError::MissingSeparator)?;
1741 let payload_separator = input[source_end + 1..]
1742 .iter()
1743 .position(|byte| *byte == b':')
1744 .map(|offset| source_end + 1 + offset)
1745 .ok_or(ParseError::MissingSeparator)?;
1746
1747 let path_start = source_end + 1;
1748 let path_end = payload_separator;
1749 let payload_start = payload_separator + 1;
1750
1751 if source_end == 0 || path_start == path_end || payload_start == input.len() {
1752 return Err(ParseError::EmptySegment);
1753 }
1754
1755 let Some(path_components) = path_component_ranges(input, path_start, path_end) else {
1756 return Err(ParseError::InvalidAddress);
1757 };
1758
1759 if !is_ax25_like_source(&input[..source_end])
1760 || !path_components
1761 .iter()
1762 .all(|(start, end)| is_ax25_like_path_component(&input[*start..*end]))
1763 {
1764 return Err(ParseError::InvalidAddress);
1765 }
1766
1767 Ok(ParsedPacket {
1768 raw: RawPacket {
1769 bytes: input.to_vec(),
1770 },
1771 source_end,
1772 path_start,
1773 path_end,
1774 path_components,
1775 payload_start,
1776 })
1777}
1778
1779fn path_component_ranges(
1780 input: &[u8],
1781 path_start: usize,
1782 path_end: usize,
1783) -> Option<Vec<(usize, usize)>> {
1784 let mut components = Vec::new();
1785 let mut component_start = path_start;
1786
1787 for (offset, byte) in input[path_start..path_end].iter().enumerate() {
1788 if *byte == b',' {
1789 let index = path_start + offset;
1790 if component_start == index {
1791 return None;
1792 }
1793 components.push((component_start, index));
1794 component_start = index + 1;
1795 }
1796 }
1797
1798 if component_start == path_end {
1799 return None;
1800 }
1801
1802 components.push((component_start, path_end));
1803 Some(components)
1804}
1805
1806fn is_ax25_like_source(source: &[u8]) -> bool {
1807 is_ax25_like_address(source, false)
1808}
1809
1810fn is_ax25_like_path_component(component: &[u8]) -> bool {
1811 is_ax25_like_address(component, true)
1812}
1813
1814fn is_ax25_like_address(address: &[u8], allow_repeated_marker: bool) -> bool {
1815 let address = if allow_repeated_marker {
1816 address.strip_suffix(b"*").unwrap_or(address)
1817 } else {
1818 address
1819 };
1820
1821 if address.is_empty() || address.contains(&b'*') {
1822 return false;
1823 }
1824
1825 let (callsign, ssid) = match address.iter().position(|byte| *byte == b'-') {
1826 Some(separator) => (&address[..separator], Some(&address[separator + 1..])),
1827 None => (address, None),
1828 };
1829
1830 is_ax25_like_callsign(callsign) && ssid.map_or(true, is_ax25_like_ssid)
1831}
1832
1833fn is_ax25_like_callsign(callsign: &[u8]) -> bool {
1834 (1..=6).contains(&callsign.len())
1835 && callsign
1836 .iter()
1837 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit())
1838}
1839
1840fn is_ax25_like_ssid(ssid: &[u8]) -> bool {
1841 if ssid.is_empty() || ssid.len() > 2 || !ssid.iter().all(u8::is_ascii_digit) {
1842 return false;
1843 }
1844
1845 let mut value = 0u8;
1846 for digit in ssid {
1847 value = value * 10 + (digit - b'0');
1848 }
1849
1850 value <= 15
1851}