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 AprsData::MicE(MicE {
1208 identifier: identifier.as_byte(),
1209 destination,
1210 body: information,
1211 status: decode_mic_e_status(destination),
1212 latitude_digits: decode_mic_e_latitude_digits(destination),
1213 })
1214}
1215
1216fn parse_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1217 if is_compressed_position(information) {
1218 return parse_compressed_position(messaging, identifier, information);
1219 }
1220
1221 if information.len() < 19 {
1222 return AprsData::Malformed {
1223 identifier,
1224 information,
1225 };
1226 }
1227
1228 let latitude = &information[..8];
1229 let symbol_table = information[8];
1230 let longitude = &information[9..18];
1231 let symbol_code = information[18];
1232 let comment = &information[19..];
1233
1234 if !is_latitude(latitude)
1235 || !is_symbol_table_identifier(symbol_table)
1236 || !is_longitude(longitude)
1237 || !is_printable_ascii(symbol_code)
1238 {
1239 return AprsData::Malformed {
1240 identifier,
1241 information,
1242 };
1243 }
1244
1245 AprsData::Position(Position {
1246 messaging,
1247 latitude,
1248 symbol_table,
1249 longitude,
1250 symbol_code,
1251 comment,
1252 })
1253}
1254
1255fn coordinates_from_position_body(body: &[u8]) -> Option<Coordinates> {
1256 if is_compressed_position(body) {
1257 let AprsData::CompressedPosition(position) = parse_compressed_position(false, b'!', body)
1258 else {
1259 return None;
1260 };
1261 return position.coordinates();
1262 }
1263
1264 let AprsData::Position(position) = parse_position(false, b'!', body) else {
1265 return None;
1266 };
1267 position.coordinates()
1268}
1269
1270fn parse_timestamped_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1271 if information.len() < 8 {
1272 return AprsData::Malformed {
1273 identifier,
1274 information,
1275 };
1276 }
1277
1278 let timestamp = &information[..7];
1279 if !is_timestamp(timestamp) {
1280 return AprsData::Malformed {
1281 identifier,
1282 information,
1283 };
1284 }
1285
1286 match parse_position(messaging, identifier, &information[7..]) {
1287 AprsData::Position(position) => AprsData::TimestampedPosition(TimestampedPosition {
1288 messaging,
1289 timestamp,
1290 position,
1291 }),
1292 AprsData::CompressedPosition(position) => AprsData::CompressedPosition(position),
1293 _ => AprsData::Malformed {
1294 identifier,
1295 information,
1296 },
1297 }
1298}
1299
1300fn parse_compressed_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1301 if information.len() < 13 {
1302 return AprsData::Malformed {
1303 identifier,
1304 information,
1305 };
1306 }
1307
1308 let symbol_table = information[0];
1309 let compressed_latitude = &information[1..5];
1310 let compressed_longitude = &information[5..9];
1311 let symbol_code = information[9];
1312 let extension = &information[10..12];
1313 let compression_type = information[12];
1314 let comment = &information[13..];
1315
1316 if !is_symbol_table_identifier(symbol_table)
1317 || !compressed_latitude.iter().all(|byte| is_base91(*byte))
1318 || !compressed_longitude.iter().all(|byte| is_base91(*byte))
1319 || !is_printable_ascii(symbol_code)
1320 || !extension.iter().all(|byte| is_base91(*byte))
1321 || !is_base91(compression_type)
1322 {
1323 return AprsData::Malformed {
1324 identifier,
1325 information,
1326 };
1327 }
1328
1329 AprsData::CompressedPosition(CompressedPosition {
1330 messaging,
1331 symbol_table,
1332 compressed_latitude,
1333 compressed_longitude,
1334 symbol_code,
1335 extension,
1336 compression_type,
1337 comment,
1338 })
1339}
1340
1341fn parse_object(information: &[u8]) -> AprsData<'_> {
1342 if information.len() < 17 || !matches!(information[9], b'*' | b'_') {
1343 return AprsData::Malformed {
1344 identifier: b';',
1345 information,
1346 };
1347 }
1348
1349 AprsData::Object(Object {
1350 name: &information[..9],
1351 live: information[9] == b'*',
1352 timestamp: &information[10..17],
1353 body: &information[17..],
1354 })
1355}
1356
1357fn parse_item(information: &[u8]) -> AprsData<'_> {
1358 let Some(separator) = information
1359 .iter()
1360 .position(|byte| matches!(*byte, b'!' | b'_'))
1361 else {
1362 return AprsData::Malformed {
1363 identifier: b')',
1364 information,
1365 };
1366 };
1367
1368 if separator == 0 || separator > 9 {
1369 return AprsData::Malformed {
1370 identifier: b')',
1371 information,
1372 };
1373 }
1374
1375 AprsData::Item(Item {
1376 name: &information[..separator],
1377 live: information[separator] == b'!',
1378 body: &information[separator + 1..],
1379 })
1380}
1381
1382fn parse_message(information: &[u8]) -> AprsData<'_> {
1383 if information.len() < 10 || information[9] != b':' {
1384 return AprsData::Malformed {
1385 identifier: b':',
1386 information,
1387 };
1388 }
1389
1390 let addressee = &information[..9];
1391 let body = &information[10..];
1392 if let Some(kind) = classify_telemetry_metadata_kind(addressee) {
1393 return AprsData::TelemetryMetadata(TelemetryMetadata {
1394 addressee,
1395 kind,
1396 body,
1397 });
1398 }
1399
1400 let (text, id) = match body.iter().position(|byte| *byte == b'{') {
1401 Some(separator) => (&body[..separator], Some(&body[separator + 1..])),
1402 None => (body, None),
1403 };
1404 let kind = classify_message_kind(addressee, text);
1405
1406 AprsData::Message(Message {
1407 addressee,
1408 kind,
1409 text,
1410 id,
1411 })
1412}
1413
1414fn parse_telemetry(information: &[u8]) -> AprsData<'_> {
1415 if !information.starts_with(b"#") {
1416 return AprsData::Malformed {
1417 identifier: b'T',
1418 information,
1419 };
1420 }
1421
1422 let fields: Vec<&[u8]> = information[1..].split(|byte| *byte == b',').collect();
1423 if fields.len() < 6 || fields[..6].iter().any(|field| field.is_empty()) {
1424 return AprsData::Malformed {
1425 identifier: b'T',
1426 information,
1427 };
1428 }
1429
1430 AprsData::Telemetry(Telemetry {
1431 sequence: fields[0],
1432 analog: [fields[1], fields[2], fields[3], fields[4], fields[5]],
1433 digital: fields.get(6).copied().filter(|field| !field.is_empty()),
1434 })
1435}
1436
1437fn parse_maidenhead(information: &[u8]) -> AprsData<'_> {
1438 if information.len() < 6 {
1439 return AprsData::Malformed {
1440 identifier: b'[',
1441 information,
1442 };
1443 }
1444
1445 AprsData::Maidenhead(Maidenhead {
1446 locator: &information[..6],
1447 comment: &information[6..],
1448 })
1449}
1450
1451fn parse_user_defined(information: &[u8]) -> AprsData<'_> {
1452 if information.len() < 2 {
1453 return AprsData::Malformed {
1454 identifier: b'{',
1455 information,
1456 };
1457 }
1458
1459 AprsData::UserDefined(UserDefined {
1460 user_id: information[0],
1461 packet_type: information[1],
1462 body: &information[2..],
1463 })
1464}
1465
1466fn classify_telemetry_metadata_kind(addressee: &[u8]) -> Option<TelemetryMetadataKind> {
1467 match addressee.get(..5)? {
1468 b"PARM." => Some(TelemetryMetadataKind::ParameterNames),
1469 b"UNIT." => Some(TelemetryMetadataKind::Units),
1470 b"EQNS." => Some(TelemetryMetadataKind::Equations),
1471 b"BITS." => Some(TelemetryMetadataKind::BitSense),
1472 _ => None,
1473 }
1474}
1475
1476fn classify_message_kind(addressee: &[u8], text: &[u8]) -> MessageKind {
1477 if text.starts_with(b"ack") {
1478 MessageKind::Ack
1479 } else if text.starts_with(b"rej") {
1480 MessageKind::Reject
1481 } else if addressee.starts_with(b"BLN") && addressee.get(3).is_some_and(u8::is_ascii_digit) {
1482 MessageKind::Bulletin
1483 } else if addressee.starts_with(b"BLN") && addressee.get(3).is_some_and(u8::is_ascii_uppercase)
1484 {
1485 MessageKind::Announcement
1486 } else {
1487 MessageKind::Message
1488 }
1489}
1490
1491fn is_latitude(value: &[u8]) -> bool {
1492 value.len() == 8
1493 && value[0].is_ascii_digit()
1494 && value[1].is_ascii_digit()
1495 && value[2].is_ascii_digit()
1496 && value[3].is_ascii_digit()
1497 && value[4] == b'.'
1498 && value[5].is_ascii_digit()
1499 && value[6].is_ascii_digit()
1500 && matches!(value[7], b'N' | b'S')
1501}
1502
1503fn is_longitude(value: &[u8]) -> bool {
1504 value.len() == 9
1505 && value[0].is_ascii_digit()
1506 && value[1].is_ascii_digit()
1507 && value[2].is_ascii_digit()
1508 && value[3].is_ascii_digit()
1509 && value[4].is_ascii_digit()
1510 && value[5] == b'.'
1511 && value[6].is_ascii_digit()
1512 && value[7].is_ascii_digit()
1513 && matches!(value[8], b'E' | b'W')
1514}
1515
1516fn is_symbol_table_identifier(value: u8) -> bool {
1517 matches!(value, b'/' | b'\\') || value.is_ascii_alphanumeric()
1518}
1519
1520fn is_printable_ascii(value: u8) -> bool {
1521 (0x20..=0x7e).contains(&value)
1522}
1523
1524fn is_base91(value: u8) -> bool {
1525 (b'!'..=b'{').contains(&value)
1526}
1527
1528fn is_compressed_position(information: &[u8]) -> bool {
1529 information
1530 .first()
1531 .is_some_and(|byte| !byte.is_ascii_digit() && is_symbol_table_identifier(*byte))
1532 && information
1533 .get(1..13)
1534 .is_some_and(|bytes| bytes.iter().all(|byte| is_base91(*byte)))
1535}
1536
1537fn is_timestamp(value: &[u8]) -> bool {
1538 value.len() == 7
1539 && value[..6].iter().all(u8::is_ascii_digit)
1540 && matches!(value[6], b'z' | b'/' | b'h')
1541}
1542
1543fn decode_latitude(value: &[u8]) -> Option<f64> {
1544 if !is_latitude(value) {
1545 return None;
1546 }
1547
1548 let degrees = parse_u16(&value[..2])? as f64;
1549 let minutes = parse_fixed_minutes(&value[2..7])?;
1550 let sign = match value[7] {
1551 b'N' => 1.0,
1552 b'S' => -1.0,
1553 _ => return None,
1554 };
1555
1556 Some(sign * (degrees + minutes / 60.0))
1557}
1558
1559fn decode_longitude(value: &[u8]) -> Option<f64> {
1560 if !is_longitude(value) {
1561 return None;
1562 }
1563
1564 let degrees = parse_u16(&value[..3])? as f64;
1565 let minutes = parse_fixed_minutes(&value[3..8])?;
1566 let sign = match value[8] {
1567 b'E' => 1.0,
1568 b'W' => -1.0,
1569 _ => return None,
1570 };
1571
1572 Some(sign * (degrees + minutes / 60.0))
1573}
1574
1575fn parse_fixed_minutes(value: &[u8]) -> Option<f64> {
1576 if value.len() != 5 || value[2] != b'.' || !value[..2].iter().all(u8::is_ascii_digit) {
1577 return None;
1578 }
1579
1580 let whole = parse_u16(&value[..2])? as f64;
1581 let fraction = parse_u16(&value[3..])? as f64 / 100.0;
1582 Some(whole + fraction)
1583}
1584
1585fn decode_base91(value: &[u8]) -> Option<u32> {
1586 if value.len() != 4 || !value.iter().all(|byte| is_base91(*byte)) {
1587 return None;
1588 }
1589
1590 let mut decoded = 0u32;
1591 for byte in value {
1592 decoded = decoded * 91 + u32::from(byte - b'!');
1593 }
1594
1595 Some(decoded)
1596}
1597
1598fn parse_u16(value: &[u8]) -> Option<u16> {
1599 if value.is_empty() || !value.iter().all(u8::is_ascii_digit) {
1600 return None;
1601 }
1602
1603 let mut parsed = 0u16;
1604 for digit in value {
1605 parsed = parsed.checked_mul(10)?;
1606 parsed = parsed.checked_add(u16::from(digit - b'0'))?;
1607 }
1608
1609 Some(parsed)
1610}
1611
1612fn parse_i16(value: &[u8]) -> Option<i16> {
1613 if value.is_empty() {
1614 return None;
1615 }
1616
1617 let (sign, digits) = match value[0] {
1618 b'-' => (-1, &value[1..]),
1619 b'+' => (1, &value[1..]),
1620 _ => (1, value),
1621 };
1622
1623 let unsigned = parse_u16(digits)?;
1624 i16::try_from(unsigned).ok()?.checked_mul(sign)
1625}
1626
1627fn parse_hex_byte(value: &[u8]) -> Option<u8> {
1628 if value.len() != 2 {
1629 return None;
1630 }
1631
1632 Some(hex_value(value[0])? * 16 + hex_value(value[1])?)
1633}
1634
1635fn hex_value(value: u8) -> Option<u8> {
1636 match value {
1637 b'0'..=b'9' => Some(value - b'0'),
1638 b'A'..=b'F' => Some(value - b'A' + 10),
1639 b'a'..=b'f' => Some(value - b'a' + 10),
1640 _ => None,
1641 }
1642}
1643
1644fn parse_tagged_u16(report: &[u8], tag: u8, width: usize) -> Option<u16> {
1645 parse_tagged(report, tag, width).and_then(parse_u16)
1646}
1647
1648fn parse_tagged_i16(report: &[u8], tag: u8, width: usize) -> Option<i16> {
1649 parse_tagged(report, tag, width).and_then(parse_i16)
1650}
1651
1652fn parse_tagged(report: &[u8], tag: u8, width: usize) -> Option<&[u8]> {
1653 let start = report.iter().position(|byte| *byte == tag)? + 1;
1654 report.get(start..start + width)
1655}
1656
1657fn decode_mic_e_status(destination: &[u8]) -> Option<MicEStatus> {
1658 if destination.len() != 6 {
1659 return None;
1660 }
1661
1662 let bytes = destination.get(..3)?;
1663 Some(MicEStatus::Custom([
1664 mic_e_status_bit(bytes[0])?,
1665 mic_e_status_bit(bytes[1])?,
1666 mic_e_status_bit(bytes[2])?,
1667 ]))
1668}
1669
1670fn decode_mic_e_message_code(destination: &[u8]) -> Option<MicEMessageCode> {
1671 if destination.len() != 6 {
1672 return None;
1673 }
1674
1675 let mut bits = [MicEMessageBit::Zero; 3];
1676 for (index, byte) in destination[..3].iter().copied().enumerate() {
1677 bits[index] = mic_e_message_bit(byte)?;
1678 }
1679
1680 let code = message_code_number([
1681 !matches!(bits[0], MicEMessageBit::Zero),
1682 !matches!(bits[1], MicEMessageBit::Zero),
1683 !matches!(bits[2], MicEMessageBit::Zero),
1684 ]);
1685
1686 if code == 7 {
1687 return Some(MicEMessageCode::Emergency);
1688 }
1689
1690 let has_standard = bits
1691 .iter()
1692 .any(|bit| matches!(bit, MicEMessageBit::StandardOne));
1693 let has_custom = bits
1694 .iter()
1695 .any(|bit| matches!(bit, MicEMessageBit::CustomOne));
1696
1697 if has_standard && !has_custom {
1698 return standard_mic_e_message(code).map(MicEMessageCode::Standard);
1699 }
1700
1701 if has_custom && !has_standard {
1702 return Some(MicEMessageCode::Custom(code));
1703 }
1704
1705 None
1706}
1707
1708#[derive(Clone, Copy)]
1709enum MicEMessageBit {
1710 Zero,
1711 StandardOne,
1712 CustomOne,
1713}
1714
1715fn mic_e_message_bit(byte: u8) -> Option<MicEMessageBit> {
1716 match byte {
1717 b'0'..=b'9' | b'L' => Some(MicEMessageBit::Zero),
1718 b'A'..=b'K' => Some(MicEMessageBit::StandardOne),
1719 b'P'..=b'Z' => Some(MicEMessageBit::CustomOne),
1720 _ => None,
1721 }
1722}
1723
1724fn message_code_number(bits: [bool; 3]) -> u8 {
1725 match bits {
1726 [true, true, true] => 0,
1727 [true, true, false] => 1,
1728 [true, false, true] => 2,
1729 [true, false, false] => 3,
1730 [false, true, true] => 4,
1731 [false, true, false] => 5,
1732 [false, false, true] => 6,
1733 [false, false, false] => 7,
1734 }
1735}
1736
1737fn standard_mic_e_message(code: u8) -> Option<MicEStandardMessage> {
1738 match code {
1739 0 => Some(MicEStandardMessage::OffDuty),
1740 1 => Some(MicEStandardMessage::EnRoute),
1741 2 => Some(MicEStandardMessage::InService),
1742 3 => Some(MicEStandardMessage::Returning),
1743 4 => Some(MicEStandardMessage::Committed),
1744 5 => Some(MicEStandardMessage::Special),
1745 6 => Some(MicEStandardMessage::Priority),
1746 _ => None,
1747 }
1748}
1749
1750fn mic_e_status_bit(byte: u8) -> Option<bool> {
1751 match byte {
1752 b'0'..=b'9' | b'L' => Some(false),
1753 b'A'..=b'K' | b'P'..=b'Z' => Some(true),
1754 _ => None,
1755 }
1756}
1757
1758fn decode_mic_e_latitude_digits(destination: &[u8]) -> Option<[u8; 6]> {
1759 if destination.len() != 6 {
1760 return None;
1761 }
1762
1763 let mut digits = [0u8; 6];
1764 for (index, byte) in destination.iter().copied().enumerate() {
1765 digits[index] = mic_e_latitude_digit(byte)?;
1766 }
1767
1768 Some(digits)
1769}
1770
1771fn mic_e_latitude_digit(byte: u8) -> Option<u8> {
1772 match byte {
1773 b'0'..=b'9' => Some(byte - b'0'),
1774 b'A'..=b'J' => Some(byte - b'A'),
1775 b'P'..=b'Y' => Some(byte - b'P'),
1776 b'K' | b'L' | b'Z' => Some(0),
1777 _ => None,
1778 }
1779}
1780
1781fn decode_mic_e_latitude(destination: &[u8]) -> Option<f64> {
1782 let digits = decode_mic_e_latitude_digits(destination)?;
1783 let degrees = u16::from(digits[0]) * 10 + u16::from(digits[1]);
1784 let minutes = u16::from(digits[2]) * 10 + u16::from(digits[3]);
1785 let hundredths = u16::from(digits[4]) * 10 + u16::from(digits[5]);
1786 if degrees > 90 || minutes > 59 {
1787 return None;
1788 }
1789
1790 let sign = if mic_e_north(destination[3])? {
1791 1.0
1792 } else {
1793 -1.0
1794 };
1795 Some(sign * (f64::from(degrees) + (f64::from(minutes) + f64::from(hundredths) / 100.0) / 60.0))
1796}
1797
1798fn decode_mic_e_longitude(destination: &[u8], body: &[u8]) -> Option<f64> {
1799 if destination.len() != 6 || body.len() < 3 {
1800 return None;
1801 }
1802
1803 let mut degrees = i16::from(mic_e_body_value(body[0])?);
1804 if mic_e_longitude_offset(destination[4])? {
1805 degrees += 100;
1806 }
1807 if (180..=189).contains(°rees) {
1808 degrees -= 80;
1809 } else if (190..=199).contains(°rees) {
1810 degrees -= 190;
1811 }
1812
1813 let minutes = mic_e_body_value(body[1])?;
1814 let hundredths = mic_e_body_value(body[2])?;
1815 if !(0..=179).contains(°rees) || minutes > 59 || hundredths > 99 {
1816 return None;
1817 }
1818
1819 let sign = if mic_e_west(destination[5])? {
1820 -1.0
1821 } else {
1822 1.0
1823 };
1824 Some(sign * (f64::from(degrees) + (f64::from(minutes) + f64::from(hundredths) / 100.0) / 60.0))
1825}
1826
1827fn decode_mic_e_speed_course(body: &[u8]) -> Option<MicESpeedCourse> {
1828 if body.len() < 6 {
1829 return None;
1830 }
1831
1832 let speed_tens = u16::from(mic_e_body_value(body[3])?);
1833 let speed_units_course_hundreds = u16::from(mic_e_body_value(body[4])?);
1834 let course_remainder = u16::from(mic_e_body_value(body[5])?);
1835 let mut speed_knots = speed_tens * 10 + speed_units_course_hundreds / 10;
1836 if speed_knots >= 800 {
1837 speed_knots -= 800;
1838 }
1839
1840 Some(MicESpeedCourse {
1841 speed_knots,
1842 course_degrees: (speed_units_course_hundreds % 10) * 100 + course_remainder,
1843 })
1844}
1845
1846fn mic_e_body_value(byte: u8) -> Option<u8> {
1847 let value = byte.checked_sub(28)?;
1848 (value <= 99).then_some(value)
1849}
1850
1851fn mic_e_north(byte: u8) -> Option<bool> {
1852 match byte {
1853 b'0'..=b'9' | b'A'..=b'L' => Some(false),
1854 b'P'..=b'Z' => Some(true),
1855 _ => None,
1856 }
1857}
1858
1859fn mic_e_longitude_offset(byte: u8) -> Option<bool> {
1860 match byte {
1861 b'0'..=b'9' | b'A'..=b'L' => Some(false),
1862 b'P'..=b'Z' => Some(true),
1863 _ => None,
1864 }
1865}
1866
1867fn mic_e_west(byte: u8) -> Option<bool> {
1868 match byte {
1869 b'0'..=b'9' | b'A'..=b'L' => Some(false),
1870 b'P'..=b'Z' => Some(true),
1871 _ => None,
1872 }
1873}
1874
1875#[derive(Clone, Debug, Eq, PartialEq)]
1877pub enum ParseError {
1878 Empty,
1880 Oversized,
1882 MissingSeparator,
1884 EmptySegment,
1886 InvalidAddress,
1888}
1889
1890impl ParseError {
1891 #[must_use]
1893 pub fn code(&self) -> &'static str {
1894 match self {
1895 Self::Empty => "parse.empty",
1896 Self::Oversized => "parse.oversized",
1897 Self::MissingSeparator => "parse.missing_separator",
1898 Self::EmptySegment => "parse.empty_segment",
1899 Self::InvalidAddress => "parse.invalid_address",
1900 }
1901 }
1902}
1903
1904pub fn parse_packet(input: &[u8]) -> Result<ParsedPacket, ParseError> {
1910 parse_packet_with_options(input, ParseOptions::default())
1911}
1912
1913pub fn parse_packet_with_options(
1915 input: &[u8],
1916 options: ParseOptions,
1917) -> Result<ParsedPacket, ParseError> {
1918 if input.is_empty() {
1919 return Err(ParseError::Empty);
1920 }
1921
1922 if input.len() > options.max_packet_len {
1923 return Err(ParseError::Oversized);
1924 }
1925
1926 let source_end = input
1927 .iter()
1928 .position(|byte| *byte == b'>')
1929 .ok_or(ParseError::MissingSeparator)?;
1930 let payload_separator = input[source_end + 1..]
1931 .iter()
1932 .position(|byte| *byte == b':')
1933 .map(|offset| source_end + 1 + offset)
1934 .ok_or(ParseError::MissingSeparator)?;
1935
1936 let path_start = source_end + 1;
1937 let path_end = payload_separator;
1938 let payload_start = payload_separator + 1;
1939
1940 if source_end == 0 || path_start == path_end || payload_start == input.len() {
1941 return Err(ParseError::EmptySegment);
1942 }
1943
1944 let Some(path_components) = path_component_ranges(input, path_start, path_end) else {
1945 return Err(ParseError::InvalidAddress);
1946 };
1947
1948 if !is_ax25_like_source(&input[..source_end])
1949 || !path_components
1950 .iter()
1951 .all(|(start, end)| is_ax25_like_path_component(&input[*start..*end]))
1952 {
1953 return Err(ParseError::InvalidAddress);
1954 }
1955
1956 Ok(ParsedPacket {
1957 raw: RawPacket {
1958 bytes: input.to_vec(),
1959 },
1960 source_end,
1961 path_start,
1962 path_end,
1963 path_components,
1964 payload_start,
1965 })
1966}
1967
1968fn path_component_ranges(
1969 input: &[u8],
1970 path_start: usize,
1971 path_end: usize,
1972) -> Option<Vec<(usize, usize)>> {
1973 let mut components = Vec::new();
1974 let mut component_start = path_start;
1975
1976 for (offset, byte) in input[path_start..path_end].iter().enumerate() {
1977 if *byte == b',' {
1978 let index = path_start + offset;
1979 if component_start == index {
1980 return None;
1981 }
1982 components.push((component_start, index));
1983 component_start = index + 1;
1984 }
1985 }
1986
1987 if component_start == path_end {
1988 return None;
1989 }
1990
1991 components.push((component_start, path_end));
1992 Some(components)
1993}
1994
1995fn is_ax25_like_source(source: &[u8]) -> bool {
1996 is_ax25_like_address(source, false)
1997}
1998
1999fn is_ax25_like_path_component(component: &[u8]) -> bool {
2000 is_ax25_like_address(component, true)
2001}
2002
2003fn is_ax25_like_address(address: &[u8], allow_repeated_marker: bool) -> bool {
2004 let address = if allow_repeated_marker {
2005 address.strip_suffix(b"*").unwrap_or(address)
2006 } else {
2007 address
2008 };
2009
2010 if address.is_empty() || address.contains(&b'*') {
2011 return false;
2012 }
2013
2014 let (callsign, ssid) = match address.iter().position(|byte| *byte == b'-') {
2015 Some(separator) => (&address[..separator], Some(&address[separator + 1..])),
2016 None => (address, None),
2017 };
2018
2019 is_ax25_like_callsign(callsign) && ssid.map_or(true, is_ax25_like_ssid)
2020}
2021
2022fn is_ax25_like_callsign(callsign: &[u8]) -> bool {
2023 (1..=6).contains(&callsign.len())
2024 && callsign
2025 .iter()
2026 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit())
2027}
2028
2029fn is_ax25_like_ssid(ssid: &[u8]) -> bool {
2030 if ssid.is_empty() || ssid.len() > 2 || !ssid.iter().all(u8::is_ascii_digit) {
2031 return false;
2032 }
2033
2034 let mut value = 0u8;
2035 for digit in ssid {
2036 value = value * 10 + (digit - b'0');
2037 }
2038
2039 value <= 15
2040}