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
645impl Object<'_> {
646 #[must_use]
649 pub fn coordinates(&self) -> Option<Coordinates> {
650 coordinates_from_position_body(self.body)
651 }
652}
653
654#[derive(Clone, Copy, Debug, Eq, PartialEq)]
656pub struct Item<'a> {
657 pub name: &'a [u8],
659 pub live: bool,
661 pub body: &'a [u8],
663}
664
665impl Item<'_> {
666 #[must_use]
669 pub fn coordinates(&self) -> Option<Coordinates> {
670 coordinates_from_position_body(self.body)
671 }
672}
673
674#[derive(Clone, Copy, Debug, Eq, PartialEq)]
676pub struct Weather<'a> {
677 pub report: &'a [u8],
679}
680
681impl Weather<'_> {
682 #[must_use]
684 pub fn fields(&self) -> WeatherFields<'_> {
685 WeatherFields {
686 timestamp: self
687 .report
688 .get(..6)
689 .filter(|value| value.iter().all(u8::is_ascii_digit)),
690 wind_direction_degrees: parse_tagged_u16(self.report, b'c', 3),
691 wind_speed_mph: parse_tagged_u16(self.report, b's', 3),
692 wind_gust_mph: parse_tagged_u16(self.report, b'g', 3),
693 temperature_fahrenheit: parse_tagged_i16(self.report, b't', 3),
694 rain_last_hour_hundredths_inch: parse_tagged_u16(self.report, b'r', 3),
695 rain_last_24_hours_hundredths_inch: parse_tagged_u16(self.report, b'p', 3),
696 rain_since_midnight_hundredths_inch: parse_tagged_u16(self.report, b'P', 3),
697 humidity_percent: parse_tagged_u16(self.report, b'h', 2).map(|value| {
698 if value == 0 {
699 100
700 } else {
701 value
702 }
703 }),
704 pressure_tenths_hpa: parse_tagged_u16(self.report, b'b', 5),
705 luminosity_watts_per_square_meter: parse_tagged_u16(self.report, b'L', 3),
706 luminosity_1000_plus_watts_per_square_meter: parse_tagged_u16(self.report, b'l', 3)
707 .map(|value| value + 1000),
708 snow_last_24_hours_inches: parse_tagged_u16(self.report, b'S', 3),
709 raw_rain_counter: parse_tagged_u16(self.report, b'#', 3),
710 }
711 }
712}
713
714#[derive(Clone, Copy, Debug, Eq, PartialEq)]
716pub struct WeatherFields<'a> {
717 pub timestamp: Option<&'a [u8]>,
719 pub wind_direction_degrees: Option<u16>,
721 pub wind_speed_mph: Option<u16>,
723 pub wind_gust_mph: Option<u16>,
725 pub temperature_fahrenheit: Option<i16>,
727 pub rain_last_hour_hundredths_inch: Option<u16>,
729 pub rain_last_24_hours_hundredths_inch: Option<u16>,
731 pub rain_since_midnight_hundredths_inch: Option<u16>,
733 pub humidity_percent: Option<u16>,
735 pub pressure_tenths_hpa: Option<u16>,
737 pub luminosity_watts_per_square_meter: Option<u16>,
739 pub luminosity_1000_plus_watts_per_square_meter: Option<u16>,
741 pub snow_last_24_hours_inches: Option<u16>,
743 pub raw_rain_counter: Option<u16>,
745}
746
747#[derive(Clone, Copy, Debug, Eq, PartialEq)]
749pub struct Telemetry<'a> {
750 pub sequence: &'a [u8],
752 pub analog: [&'a [u8]; 5],
754 pub digital: Option<&'a [u8]>,
756}
757
758impl Telemetry<'_> {
759 #[must_use]
761 pub fn sequence_number(&self) -> Option<u16> {
762 parse_u16(self.sequence)
763 }
764
765 #[must_use]
767 pub fn analog_values(&self) -> Option<[u16; 5]> {
768 Some([
769 parse_u16(self.analog[0])?,
770 parse_u16(self.analog[1])?,
771 parse_u16(self.analog[2])?,
772 parse_u16(self.analog[3])?,
773 parse_u16(self.analog[4])?,
774 ])
775 }
776
777 #[must_use]
779 pub fn digital_bits(&self) -> Option<[bool; 8]> {
780 let digital = self.digital?;
781 if digital.len() != 8 {
782 return None;
783 }
784
785 let mut bits = [false; 8];
786 for (index, byte) in digital.iter().enumerate() {
787 bits[index] = match byte {
788 b'0' => false,
789 b'1' => true,
790 _ => return None,
791 };
792 }
793
794 Some(bits)
795 }
796}
797
798#[derive(Clone, Copy, Debug, Eq, PartialEq)]
800pub struct TelemetryMetadata<'a> {
801 pub addressee: &'a [u8],
803 pub kind: TelemetryMetadataKind,
805 pub body: &'a [u8],
807}
808
809impl<'a> TelemetryMetadata<'a> {
810 #[must_use]
812 pub fn fields(&self) -> Vec<&'a [u8]> {
813 self.body.split(|byte| *byte == b',').collect()
814 }
815}
816
817#[derive(Clone, Copy, Debug, Eq, PartialEq)]
819pub enum TelemetryMetadataKind {
820 ParameterNames,
822 Units,
824 Equations,
826 BitSense,
828}
829
830#[derive(Clone, Copy, Debug, Eq, PartialEq)]
832pub struct Query<'a> {
833 pub query: &'a [u8],
835}
836
837#[derive(Clone, Copy, Debug, Eq, PartialEq)]
839pub struct Capability<'a> {
840 pub body: &'a [u8],
842}
843
844#[derive(Clone, Copy, Debug, Eq, PartialEq)]
846pub struct Nmea<'a> {
847 pub sentence: &'a [u8],
849}
850
851impl Nmea<'_> {
852 #[must_use]
854 pub fn talker_id(&self) -> Option<&[u8]> {
855 let address = self.address_field()?;
856 (address.len() >= 2).then_some(&address[..2])
857 }
858
859 #[must_use]
861 pub fn sentence_id(&self) -> Option<&[u8]> {
862 let address = self.address_field()?;
863 (address.len() >= 5).then_some(&address[2..5])
864 }
865
866 #[must_use]
868 pub fn data_fields(&self) -> Vec<&[u8]> {
869 let body = self.body_without_checksum();
870 let mut fields = body.split(|byte| *byte == b',');
871 let _address = fields.next();
872 fields.collect()
873 }
874
875 #[must_use]
877 pub fn checksum(&self) -> Option<NmeaChecksum> {
878 let separator = self.sentence.iter().rposition(|byte| *byte == b'*')?;
879 let checksum = self.sentence.get(separator + 1..separator + 3)?;
880 if checksum.len() != 2 || self.sentence.get(separator + 3).is_some() {
881 return None;
882 }
883
884 let expected = parse_hex_byte(checksum)?;
885 let calculated = self.sentence[..separator]
886 .iter()
887 .fold(0u8, |accumulator, byte| accumulator ^ byte);
888
889 Some(NmeaChecksum {
890 expected,
891 calculated,
892 valid: expected == calculated,
893 })
894 }
895
896 fn address_field(&self) -> Option<&[u8]> {
897 let body = self.body_without_checksum();
898 let end = body
899 .iter()
900 .position(|byte| *byte == b',')
901 .unwrap_or(body.len());
902 let address = &body[..end];
903 (address.len() >= 5 && address.iter().all(u8::is_ascii_alphanumeric)).then_some(address)
904 }
905
906 fn body_without_checksum(&self) -> &[u8] {
907 match self.sentence.iter().rposition(|byte| *byte == b'*') {
908 Some(separator) => &self.sentence[..separator],
909 None => self.sentence,
910 }
911 }
912}
913
914#[derive(Clone, Copy, Debug, Eq, PartialEq)]
916pub struct NmeaChecksum {
917 pub expected: u8,
919 pub calculated: u8,
921 pub valid: bool,
923}
924
925#[derive(Clone, Copy, Debug, Eq, PartialEq)]
927pub struct MicE<'a> {
928 pub identifier: u8,
930 pub destination: &'a [u8],
932 pub body: &'a [u8],
934 pub status: Option<MicEStatus>,
936 pub latitude_digits: Option<[u8; 6]>,
938}
939
940impl MicE<'_> {
941 #[must_use]
943 pub fn coordinates(&self) -> Option<Coordinates> {
944 Some(Coordinates {
945 latitude: decode_mic_e_latitude(self.destination)?,
946 longitude: decode_mic_e_longitude(self.destination, self.body)?,
947 })
948 }
949
950 #[must_use]
952 pub fn speed_course(&self) -> Option<MicESpeedCourse> {
953 decode_mic_e_speed_course(self.body)
954 }
955
956 #[must_use]
958 pub fn message_code(&self) -> Option<MicEMessageCode> {
959 decode_mic_e_message_code(self.destination)
960 }
961}
962
963#[derive(Clone, Copy, Debug, Eq, PartialEq)]
965pub enum MicEStatus {
966 Custom([bool; 3]),
968}
969
970#[derive(Clone, Copy, Debug, Eq, PartialEq)]
972pub enum MicEMessageCode {
973 Standard(MicEStandardMessage),
975 Custom(u8),
977 Emergency,
979}
980
981#[derive(Clone, Copy, Debug, Eq, PartialEq)]
983pub enum MicEStandardMessage {
984 OffDuty,
986 EnRoute,
988 InService,
990 Returning,
992 Committed,
994 Special,
996 Priority,
998}
999
1000#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1002pub struct MicESpeedCourse {
1003 pub speed_knots: u16,
1005 pub course_degrees: u16,
1007}
1008
1009#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1011pub struct Maidenhead<'a> {
1012 pub locator: &'a [u8],
1014 pub comment: &'a [u8],
1016}
1017
1018#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1020pub struct UserDefined<'a> {
1021 pub user_id: u8,
1023 pub packet_type: u8,
1025 pub body: &'a [u8],
1027}
1028
1029#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1031pub struct ThirdParty<'a> {
1032 pub body: &'a [u8],
1034}
1035
1036impl ThirdParty<'_> {
1037 pub fn nested_packet(&self) -> Result<ParsedPacket, ParseError> {
1039 parse_packet(self.body)
1040 }
1041}
1042
1043#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1045pub enum DataTypeIdentifier {
1046 PositionNoTimestamp,
1048 PositionNoTimestampMessaging,
1050 PositionWithTimestamp,
1052 PositionWithTimestampMessaging,
1054 Status,
1056 Query,
1058 Capability,
1060 Message,
1062 Object,
1064 Item,
1066 Weather,
1068 Telemetry,
1070 Nmea,
1072 MicECurrent,
1074 MicEOld,
1076 Maidenhead,
1078 UserDefined,
1080 ThirdParty,
1082 Unknown(u8),
1084}
1085
1086impl DataTypeIdentifier {
1087 fn from_byte(byte: u8) -> Self {
1088 match byte {
1089 b'!' => Self::PositionNoTimestamp,
1090 b'=' => Self::PositionNoTimestampMessaging,
1091 b'/' => Self::PositionWithTimestamp,
1092 b'@' => Self::PositionWithTimestampMessaging,
1093 b'>' => Self::Status,
1094 b'?' => Self::Query,
1095 b'<' => Self::Capability,
1096 b':' => Self::Message,
1097 b';' => Self::Object,
1098 b')' => Self::Item,
1099 b'_' => Self::Weather,
1100 b'T' => Self::Telemetry,
1101 b'$' => Self::Nmea,
1102 b'`' => Self::MicECurrent,
1103 b'\'' => Self::MicEOld,
1104 b'[' => Self::Maidenhead,
1105 b'{' => Self::UserDefined,
1106 b'}' => Self::ThirdParty,
1107 other => Self::Unknown(other),
1108 }
1109 }
1110
1111 fn as_byte(self) -> u8 {
1112 match self {
1113 Self::PositionNoTimestamp => b'!',
1114 Self::PositionNoTimestampMessaging => b'=',
1115 Self::PositionWithTimestamp => b'/',
1116 Self::PositionWithTimestampMessaging => b'@',
1117 Self::Status => b'>',
1118 Self::Query => b'?',
1119 Self::Capability => b'<',
1120 Self::Message => b':',
1121 Self::Object => b';',
1122 Self::Item => b')',
1123 Self::Weather => b'_',
1124 Self::Telemetry => b'T',
1125 Self::Nmea => b'$',
1126 Self::MicECurrent => b'`',
1127 Self::MicEOld => b'\'',
1128 Self::Maidenhead => b'[',
1129 Self::UserDefined => b'{',
1130 Self::ThirdParty => b'}',
1131 Self::Unknown(value) => value,
1132 }
1133 }
1134
1135 #[must_use]
1137 pub fn name(self) -> &'static str {
1138 match self {
1139 Self::PositionNoTimestamp => "position_no_timestamp",
1140 Self::PositionNoTimestampMessaging => "position_no_timestamp_messaging",
1141 Self::PositionWithTimestamp => "position_with_timestamp",
1142 Self::PositionWithTimestampMessaging => "position_with_timestamp_messaging",
1143 Self::Status => "status",
1144 Self::Query => "query",
1145 Self::Capability => "capability",
1146 Self::Message => "message",
1147 Self::Object => "object",
1148 Self::Item => "item",
1149 Self::Weather => "weather",
1150 Self::Telemetry => "telemetry",
1151 Self::Nmea => "nmea",
1152 Self::MicECurrent => "mic_e_current",
1153 Self::MicEOld => "mic_e_old",
1154 Self::Maidenhead => "maidenhead",
1155 Self::UserDefined => "user_defined",
1156 Self::ThirdParty => "third_party",
1157 Self::Unknown(_) => "unknown",
1158 }
1159 }
1160}
1161
1162fn parse_aprs_data<'a>(
1163 identifier: DataTypeIdentifier,
1164 information: &'a [u8],
1165 destination: &'a [u8],
1166) -> AprsData<'a> {
1167 match identifier {
1168 DataTypeIdentifier::Status => AprsData::Status { text: information },
1169 DataTypeIdentifier::PositionNoTimestamp => parse_position(false, b'!', information),
1170 DataTypeIdentifier::PositionNoTimestampMessaging => parse_position(true, b'=', information),
1171 DataTypeIdentifier::PositionWithTimestamp => {
1172 parse_timestamped_position(false, b'/', information)
1173 }
1174 DataTypeIdentifier::PositionWithTimestampMessaging => {
1175 parse_timestamped_position(true, b'@', information)
1176 }
1177 DataTypeIdentifier::Message => parse_message(information),
1178 DataTypeIdentifier::Object => parse_object(information),
1179 DataTypeIdentifier::Item => parse_item(information),
1180 DataTypeIdentifier::Weather => AprsData::Weather(Weather {
1181 report: information,
1182 }),
1183 DataTypeIdentifier::Telemetry => parse_telemetry(information),
1184 DataTypeIdentifier::Query => AprsData::Query(Query { query: information }),
1185 DataTypeIdentifier::Capability => AprsData::Capability(Capability { body: information }),
1186 DataTypeIdentifier::Nmea => AprsData::Nmea(Nmea {
1187 sentence: information,
1188 }),
1189 DataTypeIdentifier::MicECurrent | DataTypeIdentifier::MicEOld => {
1190 parse_mic_e(identifier, information, destination)
1191 }
1192 DataTypeIdentifier::Maidenhead => parse_maidenhead(information),
1193 DataTypeIdentifier::UserDefined => parse_user_defined(information),
1194 DataTypeIdentifier::ThirdParty => AprsData::ThirdParty(ThirdParty { body: information }),
1195 other => AprsData::Unsupported {
1196 identifier: other.as_byte(),
1197 information,
1198 },
1199 }
1200}
1201
1202fn parse_mic_e<'a>(
1203 identifier: DataTypeIdentifier,
1204 information: &'a [u8],
1205 destination: &'a [u8],
1206) -> AprsData<'a> {
1207 if information.len() < 3 {
1208 return AprsData::Malformed {
1209 identifier: identifier.as_byte(),
1210 information,
1211 };
1212 }
1213
1214 AprsData::MicE(MicE {
1215 identifier: identifier.as_byte(),
1216 destination,
1217 body: information,
1218 status: decode_mic_e_status(destination),
1219 latitude_digits: decode_mic_e_latitude_digits(destination),
1220 })
1221}
1222
1223fn parse_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1224 if is_compressed_position(information) {
1225 return parse_compressed_position(messaging, identifier, information);
1226 }
1227
1228 if information.len() < 19 {
1229 return AprsData::Malformed {
1230 identifier,
1231 information,
1232 };
1233 }
1234
1235 let latitude = &information[..8];
1236 let symbol_table = information[8];
1237 let longitude = &information[9..18];
1238 let symbol_code = information[18];
1239 let comment = &information[19..];
1240
1241 if !is_latitude(latitude)
1242 || !is_symbol_table_identifier(symbol_table)
1243 || !is_longitude(longitude)
1244 || !is_printable_ascii(symbol_code)
1245 {
1246 return AprsData::Malformed {
1247 identifier,
1248 information,
1249 };
1250 }
1251
1252 AprsData::Position(Position {
1253 messaging,
1254 latitude,
1255 symbol_table,
1256 longitude,
1257 symbol_code,
1258 comment,
1259 })
1260}
1261
1262fn coordinates_from_position_body(body: &[u8]) -> Option<Coordinates> {
1263 if is_compressed_position(body) {
1264 let AprsData::CompressedPosition(position) = parse_compressed_position(false, b'!', body)
1265 else {
1266 return None;
1267 };
1268 return position.coordinates();
1269 }
1270
1271 let AprsData::Position(position) = parse_position(false, b'!', body) else {
1272 return None;
1273 };
1274 position.coordinates()
1275}
1276
1277fn parse_timestamped_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1278 if information.len() < 8 {
1279 return AprsData::Malformed {
1280 identifier,
1281 information,
1282 };
1283 }
1284
1285 let timestamp = &information[..7];
1286 if !is_timestamp(timestamp) {
1287 return AprsData::Malformed {
1288 identifier,
1289 information,
1290 };
1291 }
1292
1293 match parse_position(messaging, identifier, &information[7..]) {
1294 AprsData::Position(position) => AprsData::TimestampedPosition(TimestampedPosition {
1295 messaging,
1296 timestamp,
1297 position,
1298 }),
1299 AprsData::CompressedPosition(position) => AprsData::CompressedPosition(position),
1300 _ => AprsData::Malformed {
1301 identifier,
1302 information,
1303 },
1304 }
1305}
1306
1307fn parse_compressed_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1308 if information.len() < 13 {
1309 return AprsData::Malformed {
1310 identifier,
1311 information,
1312 };
1313 }
1314
1315 let symbol_table = information[0];
1316 let compressed_latitude = &information[1..5];
1317 let compressed_longitude = &information[5..9];
1318 let symbol_code = information[9];
1319 let extension = &information[10..12];
1320 let compression_type = information[12];
1321 let comment = &information[13..];
1322
1323 if !is_symbol_table_identifier(symbol_table)
1324 || !compressed_latitude.iter().all(|byte| is_base91(*byte))
1325 || !compressed_longitude.iter().all(|byte| is_base91(*byte))
1326 || !is_printable_ascii(symbol_code)
1327 || !extension.iter().all(|byte| is_base91(*byte))
1328 || !is_base91(compression_type)
1329 {
1330 return AprsData::Malformed {
1331 identifier,
1332 information,
1333 };
1334 }
1335
1336 AprsData::CompressedPosition(CompressedPosition {
1337 messaging,
1338 symbol_table,
1339 compressed_latitude,
1340 compressed_longitude,
1341 symbol_code,
1342 extension,
1343 compression_type,
1344 comment,
1345 })
1346}
1347
1348fn parse_object(information: &[u8]) -> AprsData<'_> {
1349 if information.len() < 17
1350 || !matches!(information[9], b'*' | b'_')
1351 || !is_timestamp(&information[10..17])
1352 {
1353 return AprsData::Malformed {
1354 identifier: b';',
1355 information,
1356 };
1357 }
1358
1359 AprsData::Object(Object {
1360 name: &information[..9],
1361 live: information[9] == b'*',
1362 timestamp: &information[10..17],
1363 body: &information[17..],
1364 })
1365}
1366
1367fn parse_item(information: &[u8]) -> AprsData<'_> {
1368 let Some(separator) = information
1369 .iter()
1370 .position(|byte| matches!(*byte, b'!' | b'_'))
1371 else {
1372 return AprsData::Malformed {
1373 identifier: b')',
1374 information,
1375 };
1376 };
1377
1378 if separator == 0 || separator > 9 {
1379 return AprsData::Malformed {
1380 identifier: b')',
1381 information,
1382 };
1383 }
1384
1385 AprsData::Item(Item {
1386 name: &information[..separator],
1387 live: information[separator] == b'!',
1388 body: &information[separator + 1..],
1389 })
1390}
1391
1392fn parse_message(information: &[u8]) -> AprsData<'_> {
1393 if information.len() < 10 || information[9] != b':' {
1394 return AprsData::Malformed {
1395 identifier: b':',
1396 information,
1397 };
1398 }
1399
1400 let addressee = &information[..9];
1401 let body = &information[10..];
1402 if let Some(kind) = classify_telemetry_metadata_kind(addressee) {
1403 return AprsData::TelemetryMetadata(TelemetryMetadata {
1404 addressee,
1405 kind,
1406 body,
1407 });
1408 }
1409
1410 let (text, id) = match body.iter().position(|byte| *byte == b'{') {
1411 Some(separator) => (&body[..separator], Some(&body[separator + 1..])),
1412 None => (body, None),
1413 };
1414 let kind = classify_message_kind(addressee, text);
1415
1416 AprsData::Message(Message {
1417 addressee,
1418 kind,
1419 text,
1420 id,
1421 })
1422}
1423
1424fn parse_telemetry(information: &[u8]) -> AprsData<'_> {
1425 if !information.starts_with(b"#") {
1426 return AprsData::Malformed {
1427 identifier: b'T',
1428 information,
1429 };
1430 }
1431
1432 let fields: Vec<&[u8]> = information[1..].split(|byte| *byte == b',').collect();
1433 if fields.len() < 6 || fields[..6].iter().any(|field| field.is_empty()) {
1434 return AprsData::Malformed {
1435 identifier: b'T',
1436 information,
1437 };
1438 }
1439
1440 AprsData::Telemetry(Telemetry {
1441 sequence: fields[0],
1442 analog: [fields[1], fields[2], fields[3], fields[4], fields[5]],
1443 digital: fields.get(6).copied().filter(|field| !field.is_empty()),
1444 })
1445}
1446
1447fn parse_maidenhead(information: &[u8]) -> AprsData<'_> {
1448 if information.len() < 6 || !is_maidenhead_locator(&information[..6]) {
1449 return AprsData::Malformed {
1450 identifier: b'[',
1451 information,
1452 };
1453 }
1454
1455 AprsData::Maidenhead(Maidenhead {
1456 locator: &information[..6],
1457 comment: &information[6..],
1458 })
1459}
1460
1461fn parse_user_defined(information: &[u8]) -> AprsData<'_> {
1462 if information.len() < 2 {
1463 return AprsData::Malformed {
1464 identifier: b'{',
1465 information,
1466 };
1467 }
1468
1469 AprsData::UserDefined(UserDefined {
1470 user_id: information[0],
1471 packet_type: information[1],
1472 body: &information[2..],
1473 })
1474}
1475
1476fn classify_telemetry_metadata_kind(addressee: &[u8]) -> Option<TelemetryMetadataKind> {
1477 match addressee.get(..5)? {
1478 b"PARM." => Some(TelemetryMetadataKind::ParameterNames),
1479 b"UNIT." => Some(TelemetryMetadataKind::Units),
1480 b"EQNS." => Some(TelemetryMetadataKind::Equations),
1481 b"BITS." => Some(TelemetryMetadataKind::BitSense),
1482 _ => None,
1483 }
1484}
1485
1486fn classify_message_kind(addressee: &[u8], text: &[u8]) -> MessageKind {
1487 if text.starts_with(b"ack") {
1488 MessageKind::Ack
1489 } else if text.starts_with(b"rej") {
1490 MessageKind::Reject
1491 } else if addressee.starts_with(b"BLN") && addressee.get(3).is_some_and(u8::is_ascii_digit) {
1492 MessageKind::Bulletin
1493 } else if addressee.starts_with(b"BLN") && addressee.get(3).is_some_and(u8::is_ascii_uppercase)
1494 {
1495 MessageKind::Announcement
1496 } else {
1497 MessageKind::Message
1498 }
1499}
1500
1501fn is_latitude(value: &[u8]) -> bool {
1502 if !(value.len() == 8
1503 && value[0].is_ascii_digit()
1504 && value[1].is_ascii_digit()
1505 && value[2].is_ascii_digit()
1506 && value[3].is_ascii_digit()
1507 && value[4] == b'.'
1508 && value[5].is_ascii_digit()
1509 && value[6].is_ascii_digit()
1510 && matches!(value[7], b'N' | b'S'))
1511 {
1512 return false;
1513 }
1514
1515 coordinate_in_range(&value[..2], &value[2..7], 90)
1516}
1517
1518fn is_longitude(value: &[u8]) -> bool {
1519 if !(value.len() == 9
1520 && value[0].is_ascii_digit()
1521 && value[1].is_ascii_digit()
1522 && value[2].is_ascii_digit()
1523 && value[3].is_ascii_digit()
1524 && value[4].is_ascii_digit()
1525 && value[5] == b'.'
1526 && value[6].is_ascii_digit()
1527 && value[7].is_ascii_digit()
1528 && matches!(value[8], b'E' | b'W'))
1529 {
1530 return false;
1531 }
1532
1533 coordinate_in_range(&value[..3], &value[3..8], 180)
1534}
1535
1536fn coordinate_in_range(degrees: &[u8], minutes: &[u8], max_degrees: u16) -> bool {
1537 let Some(degrees) = parse_u16(degrees) else {
1538 return false;
1539 };
1540 let Some(minutes) = parse_fixed_minutes(minutes) else {
1541 return false;
1542 };
1543
1544 degrees < max_degrees || (degrees == max_degrees && minutes == 0.0)
1545}
1546
1547fn is_symbol_table_identifier(value: u8) -> bool {
1548 matches!(value, b'/' | b'\\') || value.is_ascii_alphanumeric()
1549}
1550
1551fn is_printable_ascii(value: u8) -> bool {
1552 (0x20..=0x7e).contains(&value)
1553}
1554
1555fn is_base91(value: u8) -> bool {
1556 (b'!'..=b'{').contains(&value)
1557}
1558
1559fn is_compressed_position(information: &[u8]) -> bool {
1560 information
1561 .first()
1562 .is_some_and(|byte| !byte.is_ascii_digit() && is_symbol_table_identifier(*byte))
1563 && information
1564 .get(1..13)
1565 .is_some_and(|bytes| bytes.iter().all(|byte| is_base91(*byte)))
1566}
1567
1568fn is_timestamp(value: &[u8]) -> bool {
1569 value.len() == 7
1570 && value[..6].iter().all(u8::is_ascii_digit)
1571 && matches!(value[6], b'z' | b'/' | b'h')
1572}
1573
1574fn is_maidenhead_locator(value: &[u8]) -> bool {
1575 value.len() == 6
1576 && is_ascii_alpha_range(value[0], b'A', b'R')
1577 && is_ascii_alpha_range(value[1], b'A', b'R')
1578 && value[2].is_ascii_digit()
1579 && value[3].is_ascii_digit()
1580 && is_ascii_alpha_range(value[4], b'A', b'X')
1581 && is_ascii_alpha_range(value[5], b'A', b'X')
1582}
1583
1584fn is_ascii_alpha_range(value: u8, start: u8, end: u8) -> bool {
1585 let uppercase = value.to_ascii_uppercase();
1586 (start..=end).contains(&uppercase)
1587}
1588
1589fn decode_latitude(value: &[u8]) -> Option<f64> {
1590 if !is_latitude(value) {
1591 return None;
1592 }
1593
1594 let degrees = parse_u16(&value[..2])? as f64;
1595 let minutes = parse_fixed_minutes(&value[2..7])?;
1596 let sign = match value[7] {
1597 b'N' => 1.0,
1598 b'S' => -1.0,
1599 _ => return None,
1600 };
1601
1602 Some(sign * (degrees + minutes / 60.0))
1603}
1604
1605fn decode_longitude(value: &[u8]) -> Option<f64> {
1606 if !is_longitude(value) {
1607 return None;
1608 }
1609
1610 let degrees = parse_u16(&value[..3])? as f64;
1611 let minutes = parse_fixed_minutes(&value[3..8])?;
1612 let sign = match value[8] {
1613 b'E' => 1.0,
1614 b'W' => -1.0,
1615 _ => return None,
1616 };
1617
1618 Some(sign * (degrees + minutes / 60.0))
1619}
1620
1621fn parse_fixed_minutes(value: &[u8]) -> Option<f64> {
1622 if value.len() != 5 || value[2] != b'.' || !value[..2].iter().all(u8::is_ascii_digit) {
1623 return None;
1624 }
1625
1626 let whole = parse_u16(&value[..2])? as f64;
1627 let fraction = parse_u16(&value[3..])? as f64 / 100.0;
1628 Some(whole + fraction)
1629}
1630
1631fn decode_base91(value: &[u8]) -> Option<u32> {
1632 if value.len() != 4 || !value.iter().all(|byte| is_base91(*byte)) {
1633 return None;
1634 }
1635
1636 let mut decoded = 0u32;
1637 for byte in value {
1638 decoded = decoded * 91 + u32::from(byte - b'!');
1639 }
1640
1641 Some(decoded)
1642}
1643
1644fn parse_u16(value: &[u8]) -> Option<u16> {
1645 if value.is_empty() || !value.iter().all(u8::is_ascii_digit) {
1646 return None;
1647 }
1648
1649 let mut parsed = 0u16;
1650 for digit in value {
1651 parsed = parsed.checked_mul(10)?;
1652 parsed = parsed.checked_add(u16::from(digit - b'0'))?;
1653 }
1654
1655 Some(parsed)
1656}
1657
1658fn parse_i16(value: &[u8]) -> Option<i16> {
1659 if value.is_empty() {
1660 return None;
1661 }
1662
1663 let (sign, digits) = match value[0] {
1664 b'-' => (-1, &value[1..]),
1665 b'+' => (1, &value[1..]),
1666 _ => (1, value),
1667 };
1668
1669 let unsigned = parse_u16(digits)?;
1670 i16::try_from(unsigned).ok()?.checked_mul(sign)
1671}
1672
1673fn parse_hex_byte(value: &[u8]) -> Option<u8> {
1674 if value.len() != 2 {
1675 return None;
1676 }
1677
1678 Some(hex_value(value[0])? * 16 + hex_value(value[1])?)
1679}
1680
1681fn hex_value(value: u8) -> Option<u8> {
1682 match value {
1683 b'0'..=b'9' => Some(value - b'0'),
1684 b'A'..=b'F' => Some(value - b'A' + 10),
1685 b'a'..=b'f' => Some(value - b'a' + 10),
1686 _ => None,
1687 }
1688}
1689
1690fn parse_tagged_u16(report: &[u8], tag: u8, width: usize) -> Option<u16> {
1691 parse_tagged(report, tag, width).and_then(parse_u16)
1692}
1693
1694fn parse_tagged_i16(report: &[u8], tag: u8, width: usize) -> Option<i16> {
1695 parse_tagged(report, tag, width).and_then(parse_i16)
1696}
1697
1698fn parse_tagged(report: &[u8], tag: u8, width: usize) -> Option<&[u8]> {
1699 let start = report.iter().position(|byte| *byte == tag)? + 1;
1700 report.get(start..start + width)
1701}
1702
1703fn decode_mic_e_status(destination: &[u8]) -> Option<MicEStatus> {
1704 if destination.len() != 6 {
1705 return None;
1706 }
1707
1708 let bytes = destination.get(..3)?;
1709 Some(MicEStatus::Custom([
1710 mic_e_status_bit(bytes[0])?,
1711 mic_e_status_bit(bytes[1])?,
1712 mic_e_status_bit(bytes[2])?,
1713 ]))
1714}
1715
1716fn decode_mic_e_message_code(destination: &[u8]) -> Option<MicEMessageCode> {
1717 if destination.len() != 6 {
1718 return None;
1719 }
1720
1721 let mut bits = [MicEMessageBit::Zero; 3];
1722 for (index, byte) in destination[..3].iter().copied().enumerate() {
1723 bits[index] = mic_e_message_bit(byte)?;
1724 }
1725
1726 let code = message_code_number([
1727 !matches!(bits[0], MicEMessageBit::Zero),
1728 !matches!(bits[1], MicEMessageBit::Zero),
1729 !matches!(bits[2], MicEMessageBit::Zero),
1730 ]);
1731
1732 if code == 7 {
1733 return Some(MicEMessageCode::Emergency);
1734 }
1735
1736 let has_standard = bits
1737 .iter()
1738 .any(|bit| matches!(bit, MicEMessageBit::StandardOne));
1739 let has_custom = bits
1740 .iter()
1741 .any(|bit| matches!(bit, MicEMessageBit::CustomOne));
1742
1743 if has_standard && !has_custom {
1744 return standard_mic_e_message(code).map(MicEMessageCode::Standard);
1745 }
1746
1747 if has_custom && !has_standard {
1748 return Some(MicEMessageCode::Custom(code));
1749 }
1750
1751 None
1752}
1753
1754#[derive(Clone, Copy)]
1755enum MicEMessageBit {
1756 Zero,
1757 StandardOne,
1758 CustomOne,
1759}
1760
1761fn mic_e_message_bit(byte: u8) -> Option<MicEMessageBit> {
1762 match byte {
1763 b'0'..=b'9' | b'L' => Some(MicEMessageBit::Zero),
1764 b'A'..=b'K' => Some(MicEMessageBit::StandardOne),
1765 b'P'..=b'Z' => Some(MicEMessageBit::CustomOne),
1766 _ => None,
1767 }
1768}
1769
1770fn message_code_number(bits: [bool; 3]) -> u8 {
1771 match bits {
1772 [true, true, true] => 0,
1773 [true, true, false] => 1,
1774 [true, false, true] => 2,
1775 [true, false, false] => 3,
1776 [false, true, true] => 4,
1777 [false, true, false] => 5,
1778 [false, false, true] => 6,
1779 [false, false, false] => 7,
1780 }
1781}
1782
1783fn standard_mic_e_message(code: u8) -> Option<MicEStandardMessage> {
1784 match code {
1785 0 => Some(MicEStandardMessage::OffDuty),
1786 1 => Some(MicEStandardMessage::EnRoute),
1787 2 => Some(MicEStandardMessage::InService),
1788 3 => Some(MicEStandardMessage::Returning),
1789 4 => Some(MicEStandardMessage::Committed),
1790 5 => Some(MicEStandardMessage::Special),
1791 6 => Some(MicEStandardMessage::Priority),
1792 _ => None,
1793 }
1794}
1795
1796fn mic_e_status_bit(byte: u8) -> Option<bool> {
1797 match byte {
1798 b'0'..=b'9' | b'L' => Some(false),
1799 b'A'..=b'K' | b'P'..=b'Z' => Some(true),
1800 _ => None,
1801 }
1802}
1803
1804fn decode_mic_e_latitude_digits(destination: &[u8]) -> Option<[u8; 6]> {
1805 if destination.len() != 6 {
1806 return None;
1807 }
1808
1809 let mut digits = [0u8; 6];
1810 for (index, byte) in destination.iter().copied().enumerate() {
1811 digits[index] = mic_e_latitude_digit(byte)?;
1812 }
1813
1814 Some(digits)
1815}
1816
1817fn mic_e_latitude_digit(byte: u8) -> Option<u8> {
1818 match byte {
1819 b'0'..=b'9' => Some(byte - b'0'),
1820 b'A'..=b'J' => Some(byte - b'A'),
1821 b'P'..=b'Y' => Some(byte - b'P'),
1822 b'K' | b'L' | b'Z' => Some(0),
1823 _ => None,
1824 }
1825}
1826
1827fn decode_mic_e_latitude(destination: &[u8]) -> Option<f64> {
1828 let digits = decode_mic_e_latitude_digits(destination)?;
1829 let degrees = u16::from(digits[0]) * 10 + u16::from(digits[1]);
1830 let minutes = u16::from(digits[2]) * 10 + u16::from(digits[3]);
1831 let hundredths = u16::from(digits[4]) * 10 + u16::from(digits[5]);
1832 if degrees > 90 || minutes > 59 {
1833 return None;
1834 }
1835
1836 let sign = if mic_e_north(destination[3])? {
1837 1.0
1838 } else {
1839 -1.0
1840 };
1841 Some(sign * (f64::from(degrees) + (f64::from(minutes) + f64::from(hundredths) / 100.0) / 60.0))
1842}
1843
1844fn decode_mic_e_longitude(destination: &[u8], body: &[u8]) -> Option<f64> {
1845 if destination.len() != 6 || body.len() < 3 {
1846 return None;
1847 }
1848
1849 let mut degrees = i16::from(mic_e_body_value(body[0])?);
1850 if mic_e_longitude_offset(destination[4])? {
1851 degrees += 100;
1852 }
1853 if (180..=189).contains(°rees) {
1854 degrees -= 80;
1855 } else if (190..=199).contains(°rees) {
1856 degrees -= 190;
1857 }
1858
1859 let minutes = mic_e_body_value(body[1])?;
1860 let hundredths = mic_e_body_value(body[2])?;
1861 if !(0..=179).contains(°rees) || minutes > 59 || hundredths > 99 {
1862 return None;
1863 }
1864
1865 let sign = if mic_e_west(destination[5])? {
1866 -1.0
1867 } else {
1868 1.0
1869 };
1870 Some(sign * (f64::from(degrees) + (f64::from(minutes) + f64::from(hundredths) / 100.0) / 60.0))
1871}
1872
1873fn decode_mic_e_speed_course(body: &[u8]) -> Option<MicESpeedCourse> {
1874 if body.len() < 6 {
1875 return None;
1876 }
1877
1878 let speed_tens = u16::from(mic_e_body_value(body[3])?);
1879 let speed_units_course_hundreds = u16::from(mic_e_body_value(body[4])?);
1880 let course_remainder = u16::from(mic_e_body_value(body[5])?);
1881 let mut speed_knots = speed_tens * 10 + speed_units_course_hundreds / 10;
1882 if speed_knots >= 800 {
1883 speed_knots -= 800;
1884 }
1885
1886 Some(MicESpeedCourse {
1887 speed_knots,
1888 course_degrees: (speed_units_course_hundreds % 10) * 100 + course_remainder,
1889 })
1890}
1891
1892fn mic_e_body_value(byte: u8) -> Option<u8> {
1893 let value = byte.checked_sub(28)?;
1894 (value <= 99).then_some(value)
1895}
1896
1897fn mic_e_north(byte: u8) -> Option<bool> {
1898 match byte {
1899 b'0'..=b'9' | b'A'..=b'L' => Some(false),
1900 b'P'..=b'Z' => Some(true),
1901 _ => None,
1902 }
1903}
1904
1905fn mic_e_longitude_offset(byte: u8) -> Option<bool> {
1906 match byte {
1907 b'0'..=b'9' | b'A'..=b'L' => Some(false),
1908 b'P'..=b'Z' => Some(true),
1909 _ => None,
1910 }
1911}
1912
1913fn mic_e_west(byte: u8) -> Option<bool> {
1914 match byte {
1915 b'0'..=b'9' | b'A'..=b'L' => Some(false),
1916 b'P'..=b'Z' => Some(true),
1917 _ => None,
1918 }
1919}
1920
1921#[derive(Clone, Debug, Eq, PartialEq)]
1923pub enum ParseError {
1924 Empty,
1926 Oversized,
1928 MissingSeparator,
1930 EmptySegment,
1932 InvalidAddress,
1934}
1935
1936impl ParseError {
1937 #[must_use]
1939 pub fn code(&self) -> &'static str {
1940 match self {
1941 Self::Empty => "parse.empty",
1942 Self::Oversized => "parse.oversized",
1943 Self::MissingSeparator => "parse.missing_separator",
1944 Self::EmptySegment => "parse.empty_segment",
1945 Self::InvalidAddress => "parse.invalid_address",
1946 }
1947 }
1948}
1949
1950pub fn parse_packet(input: &[u8]) -> Result<ParsedPacket, ParseError> {
1956 parse_packet_with_options(input, ParseOptions::default())
1957}
1958
1959pub fn parse_packet_with_options(
1961 input: &[u8],
1962 options: ParseOptions,
1963) -> Result<ParsedPacket, ParseError> {
1964 if input.is_empty() {
1965 return Err(ParseError::Empty);
1966 }
1967
1968 if input.len() > options.max_packet_len {
1969 return Err(ParseError::Oversized);
1970 }
1971
1972 let source_end = input
1973 .iter()
1974 .position(|byte| *byte == b'>')
1975 .ok_or(ParseError::MissingSeparator)?;
1976 let payload_separator = input[source_end + 1..]
1977 .iter()
1978 .position(|byte| *byte == b':')
1979 .map(|offset| source_end + 1 + offset)
1980 .ok_or(ParseError::MissingSeparator)?;
1981
1982 let path_start = source_end + 1;
1983 let path_end = payload_separator;
1984 let payload_start = payload_separator + 1;
1985
1986 if source_end == 0 || path_start == path_end || payload_start == input.len() {
1987 return Err(ParseError::EmptySegment);
1988 }
1989
1990 let Some(path_components) = path_component_ranges(input, path_start, path_end) else {
1991 return Err(ParseError::InvalidAddress);
1992 };
1993
1994 if !is_ax25_like_source(&input[..source_end])
1995 || !path_components
1996 .iter()
1997 .all(|(start, end)| is_ax25_like_path_component(&input[*start..*end]))
1998 {
1999 return Err(ParseError::InvalidAddress);
2000 }
2001
2002 Ok(ParsedPacket {
2003 raw: RawPacket {
2004 bytes: input.to_vec(),
2005 },
2006 source_end,
2007 path_start,
2008 path_end,
2009 path_components,
2010 payload_start,
2011 })
2012}
2013
2014fn path_component_ranges(
2015 input: &[u8],
2016 path_start: usize,
2017 path_end: usize,
2018) -> Option<Vec<(usize, usize)>> {
2019 let mut components = Vec::new();
2020 let mut component_start = path_start;
2021
2022 for (offset, byte) in input[path_start..path_end].iter().enumerate() {
2023 if *byte == b',' {
2024 let index = path_start + offset;
2025 if component_start == index {
2026 return None;
2027 }
2028 components.push((component_start, index));
2029 component_start = index + 1;
2030 }
2031 }
2032
2033 if component_start == path_end {
2034 return None;
2035 }
2036
2037 components.push((component_start, path_end));
2038 Some(components)
2039}
2040
2041fn is_ax25_like_source(source: &[u8]) -> bool {
2042 is_ax25_like_address(source, false)
2043}
2044
2045fn is_ax25_like_path_component(component: &[u8]) -> bool {
2046 is_ax25_like_address(component, true)
2047}
2048
2049fn is_ax25_like_address(address: &[u8], allow_repeated_marker: bool) -> bool {
2050 let address = if allow_repeated_marker {
2051 address.strip_suffix(b"*").unwrap_or(address)
2052 } else {
2053 address
2054 };
2055
2056 if address.is_empty() || address.contains(&b'*') {
2057 return false;
2058 }
2059
2060 let (callsign, ssid) = match address.iter().position(|byte| *byte == b'-') {
2061 Some(separator) => (&address[..separator], Some(&address[separator + 1..])),
2062 None => (address, None),
2063 };
2064
2065 is_ax25_like_callsign(callsign) && ssid.map_or(true, is_ax25_like_ssid)
2066}
2067
2068fn is_ax25_like_callsign(callsign: &[u8]) -> bool {
2069 (1..=6).contains(&callsign.len())
2070 && callsign
2071 .iter()
2072 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit())
2073}
2074
2075fn is_ax25_like_ssid(ssid: &[u8]) -> bool {
2076 if ssid.is_empty() || ssid.len() > 2 || !ssid.iter().all(u8::is_ascii_digit) {
2077 return false;
2078 }
2079
2080 let mut value = 0u8;
2081 for digit in ssid {
2082 value = value * 10 + (digit - b'0');
2083 }
2084
2085 value <= 15
2086}