1#![forbid(unsafe_code)]
2
3mod transport;
9
10pub mod encoder;
11pub mod service;
12
13#[cfg(feature = "serde")]
14pub mod serde_support;
15
16#[cfg(feature = "metrics")]
17pub mod metrics_support {
18 use crate::Counters;
25
26 pub const ACCEPTED_PACKETS_TOTAL: &str = "libaprs_engine_packets_accepted_total";
28 pub const REJECTED_PACKETS_TOTAL: &str = "libaprs_engine_packets_rejected_total";
30 pub const MALFORMED_PACKETS_TOTAL: &str = "libaprs_engine_packets_malformed_total";
32
33 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
35 pub struct CounterMetric {
36 pub name: &'static str,
38 pub value: u64,
40 }
41
42 pub trait MetricsRecorder {
44 fn record_counter(&mut self, metric: CounterMetric);
46 }
47
48 #[must_use]
50 pub const fn counter_metrics(counters: Counters) -> [CounterMetric; 3] {
51 [
52 CounterMetric {
53 name: ACCEPTED_PACKETS_TOTAL,
54 value: counters.accepted,
55 },
56 CounterMetric {
57 name: REJECTED_PACKETS_TOTAL,
58 value: counters.rejected,
59 },
60 CounterMetric {
61 name: MALFORMED_PACKETS_TOTAL,
62 value: counters.malformed,
63 },
64 ]
65 }
66
67 pub fn record_counters(recorder: &mut impl MetricsRecorder, counters: Counters) {
69 for metric in counter_metrics(counters) {
70 recorder.record_counter(metric);
71 }
72 }
73}
74
75pub use transport::{
76 oversized_input_error, read_all_with_limit, LineTransport, PacketSink, PacketSource,
77 TransportErrorCode, DEFAULT_TRANSPORT_READ_LIMIT,
78};
79
80pub const MAX_PACKET_LEN: usize = 512;
82
83pub const EVENT_RAW_BYTE_LIMIT: usize = MAX_PACKET_LEN + 1;
85
86pub const DEFAULT_PARSE_OPTIONS: ParseOptions = ParseOptions {
88 max_packet_len: MAX_PACKET_LEN,
89};
90
91#[derive(Clone, Copy, Debug, Eq, PartialEq)]
96pub struct ParseOptions {
97 pub max_packet_len: usize,
99}
100
101impl ParseOptions {
102 #[must_use]
104 pub const fn new(max_packet_len: usize) -> Self {
105 Self { max_packet_len }
106 }
107}
108
109impl Default for ParseOptions {
110 fn default() -> Self {
111 DEFAULT_PARSE_OPTIONS
112 }
113}
114
115#[derive(Clone, Debug, Eq, PartialEq)]
117pub struct RawPacket {
118 bytes: Vec<u8>,
119}
120
121impl RawPacket {
122 #[must_use]
124 pub fn as_bytes(&self) -> &[u8] {
125 &self.bytes
126 }
127}
128
129#[derive(Clone, Debug, Eq, PartialEq)]
131pub struct ParsedPacket {
132 raw: RawPacket,
133 source_end: usize,
134 path_start: usize,
135 path_end: usize,
136 path_components: Vec<(usize, usize)>,
137 payload_start: usize,
138}
139
140impl ParsedPacket {
141 #[must_use]
143 pub fn raw(&self) -> &RawPacket {
144 &self.raw
145 }
146
147 #[must_use]
149 pub fn source(&self) -> &[u8] {
150 &self.raw.bytes[..self.source_end]
151 }
152
153 #[must_use]
155 pub fn path(&self) -> &[u8] {
156 &self.raw.bytes[self.path_start..self.path_end]
157 }
158
159 #[must_use]
161 pub fn destination(&self) -> &[u8] {
162 let (start, end) = self.path_components[0];
163 &self.raw.bytes[start..end]
164 }
165
166 #[must_use]
168 pub fn digipeaters(&self) -> Vec<&[u8]> {
169 self.path_components[1..]
170 .iter()
171 .map(|(start, end)| &self.raw.bytes[*start..*end])
172 .collect()
173 }
174
175 #[must_use]
177 pub fn path_components(&self) -> Vec<&[u8]> {
178 self.path_components
179 .iter()
180 .map(|(start, end)| &self.raw.bytes[*start..*end])
181 .collect()
182 }
183
184 #[must_use]
186 pub fn payload(&self) -> &[u8] {
187 &self.raw.bytes[self.payload_start..]
188 }
189
190 #[must_use]
192 pub fn data_type_identifier(&self) -> DataTypeIdentifier {
193 DataTypeIdentifier::from_byte(self.raw.bytes[self.payload_start])
194 }
195
196 #[must_use]
198 pub fn information(&self) -> &[u8] {
199 &self.raw.bytes[self.payload_start + 1..]
200 }
201
202 #[must_use]
204 pub fn aprs_data(&self) -> AprsData<'_> {
205 parse_aprs_data(
206 self.data_type_identifier(),
207 self.information(),
208 self.destination(),
209 )
210 }
211
212 #[must_use]
214 pub fn summary(&self) -> PacketSummary<'_> {
215 PacketSummary::from_packet(self)
216 }
217
218 #[cfg(feature = "serde")]
220 #[must_use]
221 pub fn to_diagnostic(&self) -> serde_support::PacketDiagnostic {
222 serde_support::PacketDiagnostic::from_packet(self)
223 }
224}
225
226#[derive(Clone, Copy, Debug, PartialEq)]
228pub struct PacketSummary<'a> {
229 pub source: &'a [u8],
231 pub destination: &'a [u8],
233 pub data_type: &'static str,
235 pub semantic: &'static str,
237 pub coordinates: Option<Coordinates>,
239 pub nmea_checksum: Option<NmeaChecksum>,
241 pub telemetry_sequence: Option<u16>,
243 pub mic_e_speed_course: Option<MicESpeedCourse>,
245}
246
247impl<'a> PacketSummary<'a> {
248 fn from_packet(packet: &'a ParsedPacket) -> Self {
249 let data = packet.aprs_data();
250 Self {
251 source: packet.source(),
252 destination: packet.destination(),
253 data_type: packet.data_type_identifier().name(),
254 semantic: data.kind_name(),
255 coordinates: summary_coordinates(data),
256 nmea_checksum: summary_nmea_checksum(data),
257 telemetry_sequence: summary_telemetry_sequence(data),
258 mic_e_speed_course: summary_mic_e_speed_course(data),
259 }
260 }
261}
262
263#[derive(Clone, Copy, Debug, Eq, PartialEq)]
265pub enum DiagnosticLayer {
266 Parse,
268 Policy,
270 Transport,
272}
273
274impl DiagnosticLayer {
275 #[must_use]
277 pub const fn code(self) -> &'static str {
278 match self {
279 Self::Parse => "parse",
280 Self::Policy => "policy",
281 Self::Transport => "transport",
282 }
283 }
284}
285
286#[derive(Clone, Copy, Debug, Eq, PartialEq)]
288pub struct ErrorDiagnostic {
289 pub layer: DiagnosticLayer,
291 pub code: &'static str,
293 pub name: &'static str,
295 pub description: &'static str,
297 pub remediation: &'static str,
299}
300
301#[derive(Clone, Copy, Debug, Eq, PartialEq)]
303pub enum SupportStatus {
304 Supported,
306 Partial,
308 Unsupported,
310}
311
312impl SupportStatus {
313 #[must_use]
315 pub const fn code(self) -> &'static str {
316 match self {
317 Self::Supported => "supported",
318 Self::Partial => "partial",
319 Self::Unsupported => "unsupported",
320 }
321 }
322}
323
324#[derive(Clone, Copy, Debug, Eq, PartialEq)]
326pub struct SupportItem {
327 pub kind: &'static str,
329 pub status: SupportStatus,
331 pub notes: &'static str,
333}
334
335#[derive(Clone, Copy, Debug, Eq, PartialEq)]
337pub struct TransportSupport {
338 pub crate_name: &'static str,
340 pub boundary: &'static str,
342 pub status: SupportStatus,
344 pub notes: &'static str,
346}
347
348#[derive(Clone, Copy, Debug, Eq, PartialEq)]
350pub struct SupportMatrix {
351 pub schema_version: u8,
353 pub semantic_families: &'static [SupportItem],
355 pub transport_adapters: &'static [TransportSupport],
357 pub diagnostic_layers: &'static [DiagnosticLayer],
359}
360
361#[must_use]
363pub const fn support_matrix() -> SupportMatrix {
364 SupportMatrix {
365 schema_version: 1,
366 semantic_families: SEMANTIC_SUPPORT,
367 transport_adapters: TRANSPORT_SUPPORT,
368 diagnostic_layers: DIAGNOSTIC_LAYERS,
369 }
370}
371
372const DIAGNOSTIC_LAYERS: &[DiagnosticLayer] = &[
373 DiagnosticLayer::Parse,
374 DiagnosticLayer::Policy,
375 DiagnosticLayer::Transport,
376];
377
378const SEMANTIC_SUPPORT: &[SupportItem] = &[
379 SupportItem {
380 kind: "status",
381 status: SupportStatus::Supported,
382 notes: "status text bytes are preserved",
383 },
384 SupportItem {
385 kind: "position",
386 status: SupportStatus::Supported,
387 notes: "uncompressed and compressed coordinates are decoded; weather-symbol positions expose weather reports",
388 },
389 SupportItem {
390 kind: "message",
391 status: SupportStatus::Supported,
392 notes:
393 "messages, acknowledgements, rejections, bulletins, and announcements are classified",
394 },
395 SupportItem {
396 kind: "object",
397 status: SupportStatus::Supported,
398 notes: "object name, liveness, timestamp, body, coordinates, and supported weather are exposed",
399 },
400 SupportItem {
401 kind: "item",
402 status: SupportStatus::Supported,
403 notes: "item name, liveness, body, coordinates, and supported weather are exposed",
404 },
405 SupportItem {
406 kind: "weather",
407 status: SupportStatus::Supported,
408 notes:
409 "positionless, uncompressed, and compressed weather-symbol reports expose common fields",
410 },
411 SupportItem {
412 kind: "telemetry",
413 status: SupportStatus::Supported,
414 notes: "sequence, analogue values, digital bits, and metadata packets are exposed",
415 },
416 SupportItem {
417 kind: "nmea",
418 status: SupportStatus::Supported,
419 notes: "sentence identifiers and checksum diagnostics are exposed",
420 },
421 SupportItem {
422 kind: "mic_e",
423 status: SupportStatus::Partial,
424 notes: "destination-derived status, latitude digits, speed, and course helpers are exposed",
425 },
426 SupportItem {
427 kind: "third_party",
428 status: SupportStatus::Partial,
429 notes: "nested packet bytes must pass the codec envelope before explicit caller parsing",
430 },
431 SupportItem {
432 kind: "unsupported",
433 status: SupportStatus::Supported,
434 notes: "unknown identifiers remain explicit and byte-preserving",
435 },
436 SupportItem {
437 kind: "malformed",
438 status: SupportStatus::Supported,
439 notes: "codec-valid but semantically malformed packets remain visible to policy",
440 },
441];
442
443const TRANSPORT_SUPPORT: &[TransportSupport] = &[
444 TransportSupport {
445 crate_name: "aprs-transport-file",
446 boundary: "newline-separated files and stdin-style byte streams",
447 status: SupportStatus::Supported,
448 notes: "bounded file and packet-line reads",
449 },
450 TransportSupport {
451 crate_name: "aprs-transport-tcp",
452 boundary: "blocking TCP or Read packet streams",
453 status: SupportStatus::Supported,
454 notes: "caller owns socket timeouts and reconnect behavior",
455 },
456 TransportSupport {
457 crate_name: "aprs-transport-aprs-is",
458 boundary: "APRS-IS login framing, q-construct diagnostics, and server line filtering",
459 status: SupportStatus::Supported,
460 notes: "profile validation is available; authentication and reconnect loops stay application-owned",
461 },
462 TransportSupport {
463 crate_name: "aprs-transport-kiss",
464 boundary: "KISS frame encoding and decoding",
465 status: SupportStatus::Supported,
466 notes: "invalid escapes and oversized frames fail closed",
467 },
468 TransportSupport {
469 crate_name: "aprs-transport-serial",
470 boundary: "serial-like byte readers",
471 status: SupportStatus::Supported,
472 notes: "serial configuration stays application-owned",
473 },
474 TransportSupport {
475 crate_name: "aprs-transport-udp",
476 boundary: "UDP datagram payloads",
477 status: SupportStatus::Supported,
478 notes: "datagram length is bounded before parsing",
479 },
480 TransportSupport {
481 crate_name: "aprs-transport-http",
482 boundary: "HTTP request body bytes",
483 status: SupportStatus::Supported,
484 notes: "body and packet-line limits are enforced by helpers",
485 },
486 TransportSupport {
487 crate_name: "aprs-transport-file-watch",
488 boundary: "append-only packet logs",
489 status: SupportStatus::Supported,
490 notes: "appended byte ranges and packet lines are bounded",
491 },
492 TransportSupport {
493 crate_name: "aprs-transport-mqtt",
494 boundary: "MQTT topics and payload copies",
495 status: SupportStatus::Supported,
496 notes: "broker sessions, authentication, and reconnects stay application-owned",
497 },
498 TransportSupport {
499 crate_name: "aprs-transport-ax25",
500 boundary: "AX.25 UI frames",
501 status: SupportStatus::Supported,
502 notes: "oversized UI frames fail closed before payload extraction",
503 },
504 TransportSupport {
505 crate_name: "aprs-transport-corpus",
506 boundary: "fixture and corpus replay",
507 status: SupportStatus::Supported,
508 notes: "stable ordering and per-file limits for tests",
509 },
510 TransportSupport {
511 crate_name: "aprs-transport-channel",
512 boundary: "in-process packet channels",
513 status: SupportStatus::Supported,
514 notes: "caller-owned channel capacity controls backpressure",
515 },
516 TransportSupport {
517 crate_name: "aprs-transport-async",
518 boundary: "runtime-neutral async byte splitting",
519 status: SupportStatus::Supported,
520 notes: "runtime, timeouts, and cancellation stay caller-owned",
521 },
522];
523
524#[derive(Clone, Debug, Eq, PartialEq)]
526pub struct Engine {
527 policy: Policy,
528 counters: Counters,
529}
530
531impl Engine {
532 #[must_use]
534 pub fn new(policy: Policy) -> Self {
535 Self {
536 policy,
537 counters: Counters::default(),
538 }
539 }
540
541 pub fn process(&mut self, input: &[u8]) -> EngineResult {
543 match parse_packet(input) {
544 Ok(packet) => {
545 let semantic = packet.aprs_data();
546 match self.policy.evaluate(&packet, &semantic) {
547 PolicyDecision::Accept => {
548 self.counters.accepted = self.counters.accepted.saturating_add(1);
549 EngineResult::Accepted { packet }
550 }
551 PolicyDecision::Reject(reason) => {
552 self.counters.rejected = self.counters.rejected.saturating_add(1);
553 EngineResult::Rejected { packet, reason }
554 }
555 }
556 }
557 Err(error) => {
558 self.counters.malformed = self.counters.malformed.saturating_add(1);
559 EngineResult::ParseError(error)
560 }
561 }
562 }
563
564 pub fn process_event(&mut self, input: &[u8]) -> EngineEvent {
566 match parse_packet(input) {
567 Ok(packet) => {
568 let semantic = packet.aprs_data();
569 match self.policy.evaluate(&packet, &semantic) {
570 PolicyDecision::Accept => {
571 self.counters.accepted = self.counters.accepted.saturating_add(1);
572 EngineEvent::Accepted(AcceptedPacketEvent { packet })
573 }
574 PolicyDecision::Reject(reason) => {
575 self.counters.rejected = self.counters.rejected.saturating_add(1);
576 EngineEvent::Rejected(PolicyRejectedPacketEvent {
577 packet,
578 reason,
579 diagnostic: reason.diagnostic(),
580 })
581 }
582 }
583 }
584 Err(error) => {
585 self.counters.malformed = self.counters.malformed.saturating_add(1);
586 let diagnostic = error.diagnostic();
587 EngineEvent::Malformed(MalformedPacketEvent {
588 raw: input.iter().copied().take(EVENT_RAW_BYTE_LIMIT).collect(),
589 raw_truncated: input.len() > EVENT_RAW_BYTE_LIMIT,
590 error,
591 diagnostic,
592 })
593 }
594 }
595 }
596
597 pub fn process_packets<I, P>(&mut self, packets: I) -> Vec<EngineResult>
599 where
600 I: IntoIterator<Item = P>,
601 P: AsRef<[u8]>,
602 {
603 packets
604 .into_iter()
605 .map(|packet| self.process(packet.as_ref()))
606 .collect()
607 }
608
609 pub fn process_source<S>(&mut self, source: &mut S) -> Result<Vec<EngineResult>, S::Error>
611 where
612 S: PacketSource,
613 {
614 Ok(self.process_packets(source.recv_packets()?))
615 }
616
617 #[must_use]
619 pub fn counters(&self) -> Counters {
620 self.counters
621 }
622}
623
624impl Default for Engine {
625 fn default() -> Self {
626 Self::new(Policy::default())
627 }
628}
629
630#[derive(Clone, Debug, PartialEq)]
632pub enum EngineResult {
633 Accepted {
635 packet: ParsedPacket,
637 },
638 Rejected {
640 packet: ParsedPacket,
642 reason: PolicyRejection,
644 },
645 ParseError(ParseError),
647}
648
649#[derive(Clone, Copy, Debug, Eq, PartialEq)]
651pub enum EngineEventKind {
652 Accepted,
654 PolicyRejected,
656 Malformed,
658 TransportFailure,
660}
661
662impl EngineEventKind {
663 #[must_use]
665 pub const fn code(self) -> &'static str {
666 match self {
667 Self::Accepted => "accepted",
668 Self::PolicyRejected => "policy_rejected",
669 Self::Malformed => "malformed",
670 Self::TransportFailure => "transport_failure",
671 }
672 }
673}
674
675#[derive(Clone, Debug, PartialEq)]
677pub enum EngineEvent {
678 Accepted(AcceptedPacketEvent),
680 Rejected(PolicyRejectedPacketEvent),
682 Malformed(MalformedPacketEvent),
684}
685
686impl EngineEvent {
687 #[must_use]
689 pub const fn kind(&self) -> EngineEventKind {
690 match self {
691 Self::Accepted(_) => EngineEventKind::Accepted,
692 Self::Rejected(_) => EngineEventKind::PolicyRejected,
693 Self::Malformed(_) => EngineEventKind::Malformed,
694 }
695 }
696}
697
698#[derive(Clone, Debug, PartialEq)]
700pub struct AcceptedPacketEvent {
701 pub packet: ParsedPacket,
703}
704
705impl AcceptedPacketEvent {
706 #[must_use]
708 pub const fn kind(&self) -> EngineEventKind {
709 EngineEventKind::Accepted
710 }
711}
712
713#[derive(Clone, Debug, PartialEq)]
715pub struct PolicyRejectedPacketEvent {
716 pub packet: ParsedPacket,
718 pub reason: PolicyRejection,
720 pub diagnostic: ErrorDiagnostic,
722}
723
724impl PolicyRejectedPacketEvent {
725 #[must_use]
727 pub const fn kind(&self) -> EngineEventKind {
728 EngineEventKind::PolicyRejected
729 }
730}
731
732#[derive(Clone, Debug, Eq, PartialEq)]
734pub struct MalformedPacketEvent {
735 pub raw: Vec<u8>,
740 pub raw_truncated: bool,
742 pub error: ParseError,
744 pub diagnostic: ErrorDiagnostic,
746}
747
748impl MalformedPacketEvent {
749 #[must_use]
751 pub const fn kind(&self) -> EngineEventKind {
752 EngineEventKind::Malformed
753 }
754}
755
756#[derive(Clone, Copy, Debug, Eq, PartialEq)]
758pub struct TransportFailureEvent {
759 pub code: TransportErrorCode,
761 pub diagnostic: ErrorDiagnostic,
763}
764
765impl TransportFailureEvent {
766 #[must_use]
768 pub fn from_code(code: TransportErrorCode) -> Self {
769 Self {
770 code,
771 diagnostic: code.diagnostic(),
772 }
773 }
774
775 #[must_use]
777 pub const fn kind(&self) -> EngineEventKind {
778 EngineEventKind::TransportFailure
779 }
780}
781
782#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
784pub struct Counters {
785 pub accepted: u64,
787 pub rejected: u64,
789 pub malformed: u64,
791}
792
793#[derive(Clone, Debug, Eq, PartialEq)]
795pub struct Policy {
796 pub allow_unsupported: bool,
798 pub allow_malformed_semantics: bool,
800 pub reject_invalid_nmea_checksum: bool,
802 pub max_path_components: usize,
804}
805
806impl Policy {
807 #[must_use]
809 pub fn strict() -> Self {
810 Self::default()
811 }
812
813 #[must_use]
815 pub fn permissive() -> Self {
816 Self {
817 allow_unsupported: true,
818 allow_malformed_semantics: true,
819 reject_invalid_nmea_checksum: false,
820 max_path_components: 9,
821 }
822 }
823
824 #[must_use]
826 pub fn evaluate(&self, packet: &ParsedPacket, semantic: &AprsData<'_>) -> PolicyDecision {
827 if packet.path_components.len() > self.max_path_components {
828 return PolicyDecision::Reject(PolicyRejection::PathTooLong);
829 }
830
831 if self.reject_invalid_nmea_checksum
832 && matches!(
833 semantic,
834 AprsData::Nmea(nmea) if nmea.checksum().is_some_and(|checksum| !checksum.valid)
835 )
836 {
837 return PolicyDecision::Reject(PolicyRejection::InvalidNmeaChecksum);
838 }
839
840 match semantic {
841 AprsData::Malformed { .. } if !self.allow_malformed_semantics => {
842 PolicyDecision::Reject(PolicyRejection::MalformedSemantics)
843 }
844 AprsData::Unsupported { .. } if !self.allow_unsupported => {
845 PolicyDecision::Reject(PolicyRejection::UnsupportedSemantics)
846 }
847 _ => PolicyDecision::Accept,
848 }
849 }
850}
851
852impl Default for Policy {
853 fn default() -> Self {
854 Self {
855 allow_unsupported: false,
856 allow_malformed_semantics: false,
857 reject_invalid_nmea_checksum: false,
858 max_path_components: 9,
859 }
860 }
861}
862
863#[derive(Clone, Copy, Debug, Eq, PartialEq)]
865pub enum PolicyDecision {
866 Accept,
868 Reject(PolicyRejection),
870}
871
872#[derive(Clone, Copy, Debug, Eq, PartialEq)]
874pub enum PolicyRejection {
875 PathTooLong,
877 MalformedSemantics,
879 UnsupportedSemantics,
881 InvalidNmeaChecksum,
883}
884
885impl PolicyRejection {
886 #[must_use]
888 pub fn code(self) -> &'static str {
889 match self {
890 Self::PathTooLong => "policy.path_too_long",
891 Self::MalformedSemantics => "policy.malformed_semantics",
892 Self::UnsupportedSemantics => "policy.unsupported_semantics",
893 Self::InvalidNmeaChecksum => "policy.nmea_checksum_mismatch",
894 }
895 }
896
897 #[must_use]
899 pub fn diagnostic(self) -> ErrorDiagnostic {
900 match self {
901 Self::PathTooLong => ErrorDiagnostic {
902 layer: DiagnosticLayer::Policy,
903 code: self.code(),
904 name: "path_too_long",
905 description: "packet path contains more components than policy permits",
906 remediation: "raise Policy::max_path_components only after reviewing path abuse risk",
907 },
908 Self::MalformedSemantics => ErrorDiagnostic {
909 layer: DiagnosticLayer::Policy,
910 code: self.code(),
911 name: "malformed_semantics",
912 description: "packet passed codec validation but the APRS semantic payload is malformed",
913 remediation: "inspect the preserved raw bytes and keep strict policy enabled for untrusted inputs",
914 },
915 Self::UnsupportedSemantics => ErrorDiagnostic {
916 layer: DiagnosticLayer::Policy,
917 code: self.code(),
918 name: "unsupported_semantics",
919 description: "packet uses an unsupported APRS semantic family or identifier",
920 remediation: "use permissive policy only for corpus collection or add explicit support before accepting",
921 },
922 Self::InvalidNmeaChecksum => ErrorDiagnostic {
923 layer: DiagnosticLayer::Policy,
924 code: self.code(),
925 name: "nmea_checksum_mismatch",
926 description: "NMEA sentence has a present checksum that does not match the calculated value",
927 remediation: "treat the packet as untrusted and investigate upstream data corruption or spoofing",
928 },
929 }
930 }
931}
932
933#[derive(Clone, Copy, Debug, Eq, PartialEq)]
935pub enum AprsData<'a> {
936 Status {
938 text: &'a [u8],
940 },
941 Position(Position<'a>),
943 TimestampedPosition(TimestampedPosition<'a>),
945 CompressedPosition(CompressedPosition<'a>),
947 Message(Message<'a>),
949 Object(Object<'a>),
951 Item(Item<'a>),
953 Weather(Weather<'a>),
955 Telemetry(Telemetry<'a>),
957 TelemetryMetadata(TelemetryMetadata<'a>),
959 Query(Query<'a>),
961 Capability(Capability<'a>),
963 Nmea(Nmea<'a>),
965 MicE(MicE<'a>),
967 Maidenhead(Maidenhead<'a>),
969 UserDefined(UserDefined<'a>),
971 ThirdParty(ThirdParty<'a>),
973 Unsupported {
975 identifier: u8,
977 information: &'a [u8],
979 },
980 Malformed {
982 identifier: u8,
984 information: &'a [u8],
986 },
987}
988
989impl AprsData<'_> {
990 #[must_use]
992 pub fn kind_name(&self) -> &'static str {
993 match self {
994 Self::Status { .. } => "status",
995 Self::Position(_) => "position",
996 Self::TimestampedPosition(_) => "timestamped_position",
997 Self::CompressedPosition(_) => "compressed_position",
998 Self::Message(_) => "message",
999 Self::Object(_) => "object",
1000 Self::Item(_) => "item",
1001 Self::Weather(_) => "weather",
1002 Self::Telemetry(_) => "telemetry",
1003 Self::TelemetryMetadata(_) => "telemetry_metadata",
1004 Self::Query(_) => "query",
1005 Self::Capability(_) => "capability",
1006 Self::Nmea(_) => "nmea",
1007 Self::MicE(_) => "mic_e",
1008 Self::Maidenhead(_) => "maidenhead",
1009 Self::UserDefined(_) => "user_defined",
1010 Self::ThirdParty(_) => "third_party",
1011 Self::Unsupported { .. } => "unsupported",
1012 Self::Malformed { .. } => "malformed",
1013 }
1014 }
1015}
1016
1017fn summary_coordinates(data: AprsData<'_>) -> Option<Coordinates> {
1018 match data {
1019 AprsData::Position(position) => position.coordinates(),
1020 AprsData::TimestampedPosition(position) => position.position.coordinates(),
1021 AprsData::CompressedPosition(position) => position.coordinates(),
1022 AprsData::MicE(mic_e) => mic_e.coordinates(),
1023 _ => None,
1024 }
1025}
1026
1027fn summary_nmea_checksum(data: AprsData<'_>) -> Option<NmeaChecksum> {
1028 match data {
1029 AprsData::Nmea(nmea) => nmea.checksum(),
1030 _ => None,
1031 }
1032}
1033
1034fn summary_telemetry_sequence(data: AprsData<'_>) -> Option<u16> {
1035 match data {
1036 AprsData::Telemetry(telemetry) => telemetry.sequence_number(),
1037 _ => None,
1038 }
1039}
1040
1041fn summary_mic_e_speed_course(data: AprsData<'_>) -> Option<MicESpeedCourse> {
1042 match data {
1043 AprsData::MicE(mic_e) => mic_e.speed_course(),
1044 _ => None,
1045 }
1046}
1047
1048#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1050pub struct Position<'a> {
1051 pub messaging: bool,
1053 pub latitude: &'a [u8],
1055 pub symbol_table: u8,
1057 pub longitude: &'a [u8],
1059 pub symbol_code: u8,
1061 pub comment: &'a [u8],
1063}
1064
1065impl<'a> Position<'a> {
1066 #[must_use]
1068 pub fn coordinates(&self) -> Option<Coordinates> {
1069 Some(Coordinates {
1070 latitude: decode_latitude(self.latitude)?,
1071 longitude: decode_longitude(self.longitude)?,
1072 })
1073 }
1074
1075 #[must_use]
1078 pub fn weather(&self) -> Option<Weather<'a>> {
1079 weather_from_uncompressed_position(self)
1080 }
1081}
1082
1083#[derive(Clone, Copy, Debug, PartialEq)]
1085pub struct Coordinates {
1086 pub latitude: f64,
1088 pub longitude: f64,
1090}
1091
1092#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1094pub struct TimestampedPosition<'a> {
1095 pub messaging: bool,
1097 pub timestamp: &'a [u8],
1099 pub position: Position<'a>,
1101}
1102
1103impl<'a> TimestampedPosition<'a> {
1104 #[must_use]
1107 pub fn weather(&self) -> Option<Weather<'a>> {
1108 self.position.weather()
1109 }
1110}
1111
1112#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1114pub struct CompressedPosition<'a> {
1115 pub messaging: bool,
1117 pub symbol_table: u8,
1119 pub compressed_latitude: &'a [u8],
1121 pub compressed_longitude: &'a [u8],
1123 pub symbol_code: u8,
1125 pub extension: &'a [u8],
1127 pub compression_type: u8,
1129 pub comment: &'a [u8],
1131}
1132
1133impl<'a> CompressedPosition<'a> {
1134 #[must_use]
1136 pub fn coordinates(&self) -> Option<Coordinates> {
1137 let y = decode_base91(self.compressed_latitude)?;
1138 let x = decode_base91(self.compressed_longitude)?;
1139
1140 Some(Coordinates {
1141 latitude: 90.0 - (y as f64 / 380_926.0),
1142 longitude: -180.0 + (x as f64 / 190_463.0),
1143 })
1144 }
1145
1146 #[must_use]
1150 pub fn weather(&self) -> Option<Weather<'a>> {
1151 weather_from_compressed_position(self)
1152 }
1153}
1154
1155#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1157pub struct Message<'a> {
1158 pub addressee: &'a [u8],
1160 pub kind: MessageKind,
1162 pub text: &'a [u8],
1164 pub id: Option<&'a [u8]>,
1166}
1167
1168#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1170pub enum MessageKind {
1171 Message,
1173 Ack,
1175 Reject,
1177 Bulletin,
1179 Announcement,
1181}
1182
1183#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1185pub struct Object<'a> {
1186 pub name: &'a [u8],
1188 pub live: bool,
1190 pub timestamp: &'a [u8],
1192 pub body: &'a [u8],
1194}
1195
1196impl<'a> Object<'a> {
1197 #[must_use]
1200 pub fn coordinates(&self) -> Option<Coordinates> {
1201 coordinates_from_position_body(self.body)
1202 }
1203
1204 #[must_use]
1207 pub fn weather(&self) -> Option<Weather<'a>> {
1208 weather_from_position_body(self.body)
1209 }
1210}
1211
1212#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1214pub struct Item<'a> {
1215 pub name: &'a [u8],
1217 pub live: bool,
1219 pub body: &'a [u8],
1221}
1222
1223impl<'a> Item<'a> {
1224 #[must_use]
1227 pub fn coordinates(&self) -> Option<Coordinates> {
1228 coordinates_from_position_body(self.body)
1229 }
1230
1231 #[must_use]
1234 pub fn weather(&self) -> Option<Weather<'a>> {
1235 weather_from_position_body(self.body)
1236 }
1237}
1238
1239#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1241pub struct Weather<'a> {
1242 pub report: &'a [u8],
1244}
1245
1246impl Weather<'_> {
1247 #[must_use]
1249 pub fn fields(&self) -> WeatherFields<'_> {
1250 WeatherFields {
1251 timestamp: self
1252 .report
1253 .get(..6)
1254 .filter(|value| value.iter().all(u8::is_ascii_digit)),
1255 wind_direction_degrees: parse_tagged_u16(self.report, b'c', 3),
1256 wind_speed_mph: parse_tagged_u16(self.report, b's', 3),
1257 wind_gust_mph: parse_tagged_u16(self.report, b'g', 3),
1258 temperature_fahrenheit: parse_tagged_i16(self.report, b't', 3),
1259 rain_last_hour_hundredths_inch: parse_tagged_u16(self.report, b'r', 3),
1260 rain_last_24_hours_hundredths_inch: parse_tagged_u16(self.report, b'p', 3),
1261 rain_since_midnight_hundredths_inch: parse_tagged_u16(self.report, b'P', 3),
1262 humidity_percent: parse_tagged_u16(self.report, b'h', 2).map(|value| {
1263 if value == 0 {
1264 100
1265 } else {
1266 value
1267 }
1268 }),
1269 pressure_tenths_hpa: parse_tagged_u16(self.report, b'b', 5),
1270 luminosity_watts_per_square_meter: parse_tagged_u16(self.report, b'L', 3),
1271 luminosity_1000_plus_watts_per_square_meter: parse_tagged_u16(self.report, b'l', 3)
1272 .map(|value| value + 1000),
1273 snow_last_24_hours_inches: parse_tagged_u16(self.report, b'S', 3),
1274 raw_rain_counter: parse_tagged_u16(self.report, b'#', 3),
1275 }
1276 }
1277}
1278
1279#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1281pub struct WeatherFields<'a> {
1282 pub timestamp: Option<&'a [u8]>,
1284 pub wind_direction_degrees: Option<u16>,
1286 pub wind_speed_mph: Option<u16>,
1288 pub wind_gust_mph: Option<u16>,
1290 pub temperature_fahrenheit: Option<i16>,
1292 pub rain_last_hour_hundredths_inch: Option<u16>,
1294 pub rain_last_24_hours_hundredths_inch: Option<u16>,
1296 pub rain_since_midnight_hundredths_inch: Option<u16>,
1298 pub humidity_percent: Option<u16>,
1300 pub pressure_tenths_hpa: Option<u16>,
1302 pub luminosity_watts_per_square_meter: Option<u16>,
1304 pub luminosity_1000_plus_watts_per_square_meter: Option<u16>,
1306 pub snow_last_24_hours_inches: Option<u16>,
1308 pub raw_rain_counter: Option<u16>,
1310}
1311
1312#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1314pub struct Telemetry<'a> {
1315 pub sequence: &'a [u8],
1317 pub analog: [&'a [u8]; 5],
1319 pub digital: Option<&'a [u8]>,
1321}
1322
1323impl Telemetry<'_> {
1324 #[must_use]
1326 pub fn sequence_number(&self) -> Option<u16> {
1327 parse_u16(self.sequence)
1328 }
1329
1330 #[must_use]
1332 pub fn analog_values(&self) -> Option<[u16; 5]> {
1333 Some([
1334 parse_u16(self.analog[0])?,
1335 parse_u16(self.analog[1])?,
1336 parse_u16(self.analog[2])?,
1337 parse_u16(self.analog[3])?,
1338 parse_u16(self.analog[4])?,
1339 ])
1340 }
1341
1342 #[must_use]
1344 pub fn digital_bits(&self) -> Option<[bool; 8]> {
1345 let digital = self.digital?;
1346 if digital.len() != 8 {
1347 return None;
1348 }
1349
1350 let mut bits = [false; 8];
1351 for (index, byte) in digital.iter().enumerate() {
1352 bits[index] = match byte {
1353 b'0' => false,
1354 b'1' => true,
1355 _ => return None,
1356 };
1357 }
1358
1359 Some(bits)
1360 }
1361}
1362
1363#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1365pub struct TelemetryMetadata<'a> {
1366 pub addressee: &'a [u8],
1368 pub kind: TelemetryMetadataKind,
1370 pub body: &'a [u8],
1372}
1373
1374impl<'a> TelemetryMetadata<'a> {
1375 #[must_use]
1377 pub fn fields(&self) -> Vec<&'a [u8]> {
1378 self.body.split(|byte| *byte == b',').collect()
1379 }
1380}
1381
1382#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1384pub enum TelemetryMetadataKind {
1385 ParameterNames,
1387 Units,
1389 Equations,
1391 BitSense,
1393}
1394
1395#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1397pub struct Query<'a> {
1398 pub query: &'a [u8],
1400}
1401
1402#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1404pub struct Capability<'a> {
1405 pub body: &'a [u8],
1407}
1408
1409#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1411pub struct Nmea<'a> {
1412 pub sentence: &'a [u8],
1414}
1415
1416impl Nmea<'_> {
1417 #[must_use]
1419 pub fn talker_id(&self) -> Option<&[u8]> {
1420 let address = self.address_field()?;
1421 (address.len() >= 2).then_some(&address[..2])
1422 }
1423
1424 #[must_use]
1426 pub fn sentence_id(&self) -> Option<&[u8]> {
1427 let address = self.address_field()?;
1428 (address.len() >= 5).then_some(&address[2..5])
1429 }
1430
1431 #[must_use]
1433 pub fn data_fields(&self) -> Vec<&[u8]> {
1434 let body = self.body_without_checksum();
1435 let mut fields = body.split(|byte| *byte == b',');
1436 let _address = fields.next();
1437 fields.collect()
1438 }
1439
1440 #[must_use]
1442 pub fn checksum(&self) -> Option<NmeaChecksum> {
1443 let separator = self.sentence.iter().rposition(|byte| *byte == b'*')?;
1444 let checksum = self.sentence.get(separator + 1..separator + 3)?;
1445 if checksum.len() != 2 || self.sentence.get(separator + 3).is_some() {
1446 return None;
1447 }
1448
1449 let expected = parse_hex_byte(checksum)?;
1450 let calculated = self.sentence[..separator]
1451 .iter()
1452 .fold(0u8, |accumulator, byte| accumulator ^ byte);
1453
1454 Some(NmeaChecksum {
1455 expected,
1456 calculated,
1457 valid: expected == calculated,
1458 })
1459 }
1460
1461 fn address_field(&self) -> Option<&[u8]> {
1462 let body = self.body_without_checksum();
1463 let end = body
1464 .iter()
1465 .position(|byte| *byte == b',')
1466 .unwrap_or(body.len());
1467 let address = &body[..end];
1468 (address.len() >= 5 && address.iter().all(u8::is_ascii_alphanumeric)).then_some(address)
1469 }
1470
1471 fn body_without_checksum(&self) -> &[u8] {
1472 match self.sentence.iter().rposition(|byte| *byte == b'*') {
1473 Some(separator) => &self.sentence[..separator],
1474 None => self.sentence,
1475 }
1476 }
1477}
1478
1479#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1481pub struct NmeaChecksum {
1482 pub expected: u8,
1484 pub calculated: u8,
1486 pub valid: bool,
1488}
1489
1490#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1492pub struct MicE<'a> {
1493 pub identifier: u8,
1495 pub destination: &'a [u8],
1497 pub body: &'a [u8],
1499 pub status: Option<MicEStatus>,
1501 pub latitude_digits: Option<[u8; 6]>,
1503}
1504
1505impl MicE<'_> {
1506 #[must_use]
1508 pub fn coordinates(&self) -> Option<Coordinates> {
1509 Some(Coordinates {
1510 latitude: decode_mic_e_latitude(self.destination)?,
1511 longitude: decode_mic_e_longitude(self.destination, self.body)?,
1512 })
1513 }
1514
1515 #[must_use]
1517 pub fn speed_course(&self) -> Option<MicESpeedCourse> {
1518 decode_mic_e_speed_course(self.body)
1519 }
1520
1521 #[must_use]
1523 pub fn message_code(&self) -> Option<MicEMessageCode> {
1524 decode_mic_e_message_code(self.destination)
1525 }
1526}
1527
1528#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1530pub enum MicEStatus {
1531 Custom([bool; 3]),
1533}
1534
1535#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1537pub enum MicEMessageCode {
1538 Standard(MicEStandardMessage),
1540 Custom(u8),
1542 Emergency,
1544}
1545
1546#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1548pub enum MicEStandardMessage {
1549 OffDuty,
1551 EnRoute,
1553 InService,
1555 Returning,
1557 Committed,
1559 Special,
1561 Priority,
1563}
1564
1565#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1567pub struct MicESpeedCourse {
1568 pub speed_knots: u16,
1570 pub course_degrees: u16,
1572}
1573
1574#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1576pub struct Maidenhead<'a> {
1577 pub locator: &'a [u8],
1579 pub comment: &'a [u8],
1581}
1582
1583#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1585pub struct UserDefined<'a> {
1586 pub user_id: u8,
1588 pub packet_type: u8,
1590 pub body: &'a [u8],
1592}
1593
1594#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1596pub struct ThirdParty<'a> {
1597 pub body: &'a [u8],
1599}
1600
1601impl ThirdParty<'_> {
1602 pub fn nested_packet(&self) -> Result<ParsedPacket, ParseError> {
1604 parse_packet(self.body)
1605 }
1606}
1607
1608#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1610pub enum DataTypeIdentifier {
1611 PositionNoTimestamp,
1613 PositionNoTimestampMessaging,
1615 PositionWithTimestamp,
1617 PositionWithTimestampMessaging,
1619 Status,
1621 Query,
1623 Capability,
1625 Message,
1627 Object,
1629 Item,
1631 Weather,
1633 Telemetry,
1635 Nmea,
1637 MicECurrent,
1639 MicEOld,
1641 Maidenhead,
1643 UserDefined,
1645 ThirdParty,
1647 Unknown(u8),
1649}
1650
1651impl DataTypeIdentifier {
1652 fn from_byte(byte: u8) -> Self {
1653 match byte {
1654 b'!' => Self::PositionNoTimestamp,
1655 b'=' => Self::PositionNoTimestampMessaging,
1656 b'/' => Self::PositionWithTimestamp,
1657 b'@' => Self::PositionWithTimestampMessaging,
1658 b'>' => Self::Status,
1659 b'?' => Self::Query,
1660 b'<' => Self::Capability,
1661 b':' => Self::Message,
1662 b';' => Self::Object,
1663 b')' => Self::Item,
1664 b'_' => Self::Weather,
1665 b'T' => Self::Telemetry,
1666 b'$' => Self::Nmea,
1667 b'`' => Self::MicECurrent,
1668 b'\'' => Self::MicEOld,
1669 b'[' => Self::Maidenhead,
1670 b'{' => Self::UserDefined,
1671 b'}' => Self::ThirdParty,
1672 other => Self::Unknown(other),
1673 }
1674 }
1675
1676 fn as_byte(self) -> u8 {
1677 match self {
1678 Self::PositionNoTimestamp => b'!',
1679 Self::PositionNoTimestampMessaging => b'=',
1680 Self::PositionWithTimestamp => b'/',
1681 Self::PositionWithTimestampMessaging => b'@',
1682 Self::Status => b'>',
1683 Self::Query => b'?',
1684 Self::Capability => b'<',
1685 Self::Message => b':',
1686 Self::Object => b';',
1687 Self::Item => b')',
1688 Self::Weather => b'_',
1689 Self::Telemetry => b'T',
1690 Self::Nmea => b'$',
1691 Self::MicECurrent => b'`',
1692 Self::MicEOld => b'\'',
1693 Self::Maidenhead => b'[',
1694 Self::UserDefined => b'{',
1695 Self::ThirdParty => b'}',
1696 Self::Unknown(value) => value,
1697 }
1698 }
1699
1700 #[must_use]
1702 pub fn name(self) -> &'static str {
1703 match self {
1704 Self::PositionNoTimestamp => "position_no_timestamp",
1705 Self::PositionNoTimestampMessaging => "position_no_timestamp_messaging",
1706 Self::PositionWithTimestamp => "position_with_timestamp",
1707 Self::PositionWithTimestampMessaging => "position_with_timestamp_messaging",
1708 Self::Status => "status",
1709 Self::Query => "query",
1710 Self::Capability => "capability",
1711 Self::Message => "message",
1712 Self::Object => "object",
1713 Self::Item => "item",
1714 Self::Weather => "weather",
1715 Self::Telemetry => "telemetry",
1716 Self::Nmea => "nmea",
1717 Self::MicECurrent => "mic_e_current",
1718 Self::MicEOld => "mic_e_old",
1719 Self::Maidenhead => "maidenhead",
1720 Self::UserDefined => "user_defined",
1721 Self::ThirdParty => "third_party",
1722 Self::Unknown(_) => "unknown",
1723 }
1724 }
1725}
1726
1727fn parse_aprs_data<'a>(
1728 identifier: DataTypeIdentifier,
1729 information: &'a [u8],
1730 destination: &'a [u8],
1731) -> AprsData<'a> {
1732 match identifier {
1733 DataTypeIdentifier::Status => AprsData::Status { text: information },
1734 DataTypeIdentifier::PositionNoTimestamp => parse_position(false, b'!', information),
1735 DataTypeIdentifier::PositionNoTimestampMessaging => parse_position(true, b'=', information),
1736 DataTypeIdentifier::PositionWithTimestamp => {
1737 parse_timestamped_position(false, b'/', information)
1738 }
1739 DataTypeIdentifier::PositionWithTimestampMessaging => {
1740 parse_timestamped_position(true, b'@', information)
1741 }
1742 DataTypeIdentifier::Message => parse_message(information),
1743 DataTypeIdentifier::Object => parse_object(information),
1744 DataTypeIdentifier::Item => parse_item(information),
1745 DataTypeIdentifier::Weather => parse_weather(information),
1746 DataTypeIdentifier::Telemetry => parse_telemetry(information),
1747 DataTypeIdentifier::Query => AprsData::Query(Query { query: information }),
1748 DataTypeIdentifier::Capability => AprsData::Capability(Capability { body: information }),
1749 DataTypeIdentifier::Nmea => AprsData::Nmea(Nmea {
1750 sentence: information,
1751 }),
1752 DataTypeIdentifier::MicECurrent | DataTypeIdentifier::MicEOld => {
1753 parse_mic_e(identifier, information, destination)
1754 }
1755 DataTypeIdentifier::Maidenhead => parse_maidenhead(information),
1756 DataTypeIdentifier::UserDefined => parse_user_defined(information),
1757 DataTypeIdentifier::ThirdParty => parse_third_party(information),
1758 other => AprsData::Unsupported {
1759 identifier: other.as_byte(),
1760 information,
1761 },
1762 }
1763}
1764
1765fn parse_weather(information: &[u8]) -> AprsData<'_> {
1766 if information.is_empty() {
1767 return AprsData::Malformed {
1768 identifier: b'_',
1769 information,
1770 };
1771 }
1772
1773 AprsData::Weather(Weather {
1774 report: information,
1775 })
1776}
1777
1778fn parse_third_party(information: &[u8]) -> AprsData<'_> {
1779 if parse_packet(information).is_err() {
1780 return AprsData::Malformed {
1781 identifier: b'}',
1782 information,
1783 };
1784 }
1785
1786 AprsData::ThirdParty(ThirdParty { body: information })
1787}
1788
1789fn parse_mic_e<'a>(
1790 identifier: DataTypeIdentifier,
1791 information: &'a [u8],
1792 destination: &'a [u8],
1793) -> AprsData<'a> {
1794 if information.len() < 3 {
1795 return AprsData::Malformed {
1796 identifier: identifier.as_byte(),
1797 information,
1798 };
1799 }
1800
1801 AprsData::MicE(MicE {
1802 identifier: identifier.as_byte(),
1803 destination,
1804 body: information,
1805 status: decode_mic_e_status(destination),
1806 latitude_digits: decode_mic_e_latitude_digits(destination),
1807 })
1808}
1809
1810fn parse_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1811 if is_compressed_position(information) {
1812 return parse_compressed_position(messaging, identifier, information);
1813 }
1814
1815 if information.len() < 19 {
1816 return AprsData::Malformed {
1817 identifier,
1818 information,
1819 };
1820 }
1821
1822 let latitude = &information[..8];
1823 let symbol_table = information[8];
1824 let longitude = &information[9..18];
1825 let symbol_code = information[18];
1826 let comment = &information[19..];
1827
1828 if !is_latitude(latitude)
1829 || !is_symbol_table_identifier(symbol_table)
1830 || !is_longitude(longitude)
1831 || !is_printable_ascii(symbol_code)
1832 {
1833 return AprsData::Malformed {
1834 identifier,
1835 information,
1836 };
1837 }
1838
1839 AprsData::Position(Position {
1840 messaging,
1841 latitude,
1842 symbol_table,
1843 longitude,
1844 symbol_code,
1845 comment,
1846 })
1847}
1848
1849fn coordinates_from_position_body(body: &[u8]) -> Option<Coordinates> {
1850 if is_compressed_position(body) {
1851 let AprsData::CompressedPosition(position) = parse_compressed_position(false, b'!', body)
1852 else {
1853 return None;
1854 };
1855 return position.coordinates();
1856 }
1857
1858 let AprsData::Position(position) = parse_position(false, b'!', body) else {
1859 return None;
1860 };
1861 position.coordinates()
1862}
1863
1864fn weather_from_uncompressed_position<'a>(position: &Position<'a>) -> Option<Weather<'a>> {
1865 if position.symbol_code == b'_' && !position.comment.is_empty() {
1866 return Some(Weather {
1867 report: position.comment,
1868 });
1869 }
1870
1871 None
1872}
1873
1874fn weather_from_compressed_position<'a>(position: &CompressedPosition<'a>) -> Option<Weather<'a>> {
1875 if position.symbol_code == b'_' && !position.comment.is_empty() {
1876 return Some(Weather {
1877 report: position.comment,
1878 });
1879 }
1880
1881 None
1882}
1883
1884fn weather_from_position_body(body: &[u8]) -> Option<Weather<'_>> {
1885 match parse_position(false, b'!', body) {
1886 AprsData::Position(position) => position.weather(),
1887 AprsData::CompressedPosition(position) => position.weather(),
1888 _ => None,
1889 }
1890}
1891
1892fn parse_timestamped_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1893 if information.len() < 8 {
1894 return AprsData::Malformed {
1895 identifier,
1896 information,
1897 };
1898 }
1899
1900 let timestamp = &information[..7];
1901 if !is_timestamp(timestamp) {
1902 return AprsData::Malformed {
1903 identifier,
1904 information,
1905 };
1906 }
1907
1908 match parse_position(messaging, identifier, &information[7..]) {
1909 AprsData::Position(position) => AprsData::TimestampedPosition(TimestampedPosition {
1910 messaging,
1911 timestamp,
1912 position,
1913 }),
1914 AprsData::CompressedPosition(position) => AprsData::CompressedPosition(position),
1915 _ => AprsData::Malformed {
1916 identifier,
1917 information,
1918 },
1919 }
1920}
1921
1922fn parse_compressed_position(messaging: bool, identifier: u8, information: &[u8]) -> AprsData<'_> {
1923 if information.len() < 13 {
1924 return AprsData::Malformed {
1925 identifier,
1926 information,
1927 };
1928 }
1929
1930 let symbol_table = information[0];
1931 let compressed_latitude = &information[1..5];
1932 let compressed_longitude = &information[5..9];
1933 let symbol_code = information[9];
1934 let extension = &information[10..12];
1935 let compression_type = information[12];
1936 let comment = &information[13..];
1937
1938 if !is_symbol_table_identifier(symbol_table)
1939 || !compressed_latitude.iter().all(|byte| is_base91(*byte))
1940 || !compressed_longitude.iter().all(|byte| is_base91(*byte))
1941 || !is_printable_ascii(symbol_code)
1942 || !extension.iter().all(|byte| is_base91(*byte))
1943 || !is_base91(compression_type)
1944 {
1945 return AprsData::Malformed {
1946 identifier,
1947 information,
1948 };
1949 }
1950
1951 AprsData::CompressedPosition(CompressedPosition {
1952 messaging,
1953 symbol_table,
1954 compressed_latitude,
1955 compressed_longitude,
1956 symbol_code,
1957 extension,
1958 compression_type,
1959 comment,
1960 })
1961}
1962
1963fn parse_object(information: &[u8]) -> AprsData<'_> {
1964 if information.len() < 17
1965 || !matches!(information[9], b'*' | b'_')
1966 || !is_timestamp(&information[10..17])
1967 {
1968 return AprsData::Malformed {
1969 identifier: b';',
1970 information,
1971 };
1972 }
1973
1974 AprsData::Object(Object {
1975 name: &information[..9],
1976 live: information[9] == b'*',
1977 timestamp: &information[10..17],
1978 body: &information[17..],
1979 })
1980}
1981
1982fn parse_item(information: &[u8]) -> AprsData<'_> {
1983 let Some(separator) = information
1984 .iter()
1985 .position(|byte| matches!(*byte, b'!' | b'_'))
1986 else {
1987 return AprsData::Malformed {
1988 identifier: b')',
1989 information,
1990 };
1991 };
1992
1993 if separator == 0 || separator > 9 {
1994 return AprsData::Malformed {
1995 identifier: b')',
1996 information,
1997 };
1998 }
1999
2000 AprsData::Item(Item {
2001 name: &information[..separator],
2002 live: information[separator] == b'!',
2003 body: &information[separator + 1..],
2004 })
2005}
2006
2007fn parse_message(information: &[u8]) -> AprsData<'_> {
2008 if information.len() < 10 || information[9] != b':' {
2009 return AprsData::Malformed {
2010 identifier: b':',
2011 information,
2012 };
2013 }
2014
2015 let addressee = &information[..9];
2016 let body = &information[10..];
2017 if let Some(kind) = classify_telemetry_metadata_kind(addressee) {
2018 return AprsData::TelemetryMetadata(TelemetryMetadata {
2019 addressee,
2020 kind,
2021 body,
2022 });
2023 }
2024
2025 let (text, id) = match body.iter().position(|byte| *byte == b'{') {
2026 Some(separator) => (&body[..separator], Some(&body[separator + 1..])),
2027 None => (body, None),
2028 };
2029 let kind = classify_message_kind(addressee, text);
2030
2031 AprsData::Message(Message {
2032 addressee,
2033 kind,
2034 text,
2035 id,
2036 })
2037}
2038
2039fn parse_telemetry(information: &[u8]) -> AprsData<'_> {
2040 if !information.starts_with(b"#") {
2041 return AprsData::Malformed {
2042 identifier: b'T',
2043 information,
2044 };
2045 }
2046
2047 let fields: Vec<&[u8]> = information[1..].split(|byte| *byte == b',').collect();
2048 if fields.len() < 6 || fields[..6].iter().any(|field| field.is_empty()) {
2049 return AprsData::Malformed {
2050 identifier: b'T',
2051 information,
2052 };
2053 }
2054
2055 AprsData::Telemetry(Telemetry {
2056 sequence: fields[0],
2057 analog: [fields[1], fields[2], fields[3], fields[4], fields[5]],
2058 digital: fields.get(6).copied().filter(|field| !field.is_empty()),
2059 })
2060}
2061
2062fn parse_maidenhead(information: &[u8]) -> AprsData<'_> {
2063 if information.len() < 6 || !is_maidenhead_locator(&information[..6]) {
2064 return AprsData::Malformed {
2065 identifier: b'[',
2066 information,
2067 };
2068 }
2069
2070 AprsData::Maidenhead(Maidenhead {
2071 locator: &information[..6],
2072 comment: &information[6..],
2073 })
2074}
2075
2076fn parse_user_defined(information: &[u8]) -> AprsData<'_> {
2077 if information.len() < 2 {
2078 return AprsData::Malformed {
2079 identifier: b'{',
2080 information,
2081 };
2082 }
2083
2084 AprsData::UserDefined(UserDefined {
2085 user_id: information[0],
2086 packet_type: information[1],
2087 body: &information[2..],
2088 })
2089}
2090
2091fn classify_telemetry_metadata_kind(addressee: &[u8]) -> Option<TelemetryMetadataKind> {
2092 match addressee.get(..5)? {
2093 b"PARM." => Some(TelemetryMetadataKind::ParameterNames),
2094 b"UNIT." => Some(TelemetryMetadataKind::Units),
2095 b"EQNS." => Some(TelemetryMetadataKind::Equations),
2096 b"BITS." => Some(TelemetryMetadataKind::BitSense),
2097 _ => None,
2098 }
2099}
2100
2101fn classify_message_kind(addressee: &[u8], text: &[u8]) -> MessageKind {
2102 if text.starts_with(b"ack") {
2103 MessageKind::Ack
2104 } else if text.starts_with(b"rej") {
2105 MessageKind::Reject
2106 } else if addressee.starts_with(b"BLN") && addressee.get(3).is_some_and(u8::is_ascii_digit) {
2107 MessageKind::Bulletin
2108 } else if addressee.starts_with(b"BLN") && addressee.get(3).is_some_and(u8::is_ascii_uppercase)
2109 {
2110 MessageKind::Announcement
2111 } else {
2112 MessageKind::Message
2113 }
2114}
2115
2116fn is_latitude(value: &[u8]) -> bool {
2117 if !(value.len() == 8
2118 && value[0].is_ascii_digit()
2119 && value[1].is_ascii_digit()
2120 && value[2].is_ascii_digit()
2121 && value[3].is_ascii_digit()
2122 && value[4] == b'.'
2123 && value[5].is_ascii_digit()
2124 && value[6].is_ascii_digit()
2125 && matches!(value[7], b'N' | b'S'))
2126 {
2127 return false;
2128 }
2129
2130 coordinate_in_range(&value[..2], &value[2..7], 90)
2131}
2132
2133fn is_longitude(value: &[u8]) -> bool {
2134 if !(value.len() == 9
2135 && value[0].is_ascii_digit()
2136 && value[1].is_ascii_digit()
2137 && value[2].is_ascii_digit()
2138 && value[3].is_ascii_digit()
2139 && value[4].is_ascii_digit()
2140 && value[5] == b'.'
2141 && value[6].is_ascii_digit()
2142 && value[7].is_ascii_digit()
2143 && matches!(value[8], b'E' | b'W'))
2144 {
2145 return false;
2146 }
2147
2148 coordinate_in_range(&value[..3], &value[3..8], 180)
2149}
2150
2151fn coordinate_in_range(degrees: &[u8], minutes: &[u8], max_degrees: u16) -> bool {
2152 let Some(degrees) = parse_u16(degrees) else {
2153 return false;
2154 };
2155 let Some(minutes) = parse_fixed_minutes(minutes) else {
2156 return false;
2157 };
2158
2159 degrees < max_degrees || (degrees == max_degrees && minutes == 0.0)
2160}
2161
2162fn is_symbol_table_identifier(value: u8) -> bool {
2163 matches!(value, b'/' | b'\\') || value.is_ascii_alphanumeric()
2164}
2165
2166fn is_printable_ascii(value: u8) -> bool {
2167 (0x20..=0x7e).contains(&value)
2168}
2169
2170fn is_base91(value: u8) -> bool {
2171 (b'!'..=b'{').contains(&value)
2172}
2173
2174fn is_compressed_position(information: &[u8]) -> bool {
2175 information
2176 .first()
2177 .is_some_and(|byte| !byte.is_ascii_digit() && is_symbol_table_identifier(*byte))
2178 && information
2179 .get(1..13)
2180 .is_some_and(|bytes| bytes.iter().all(|byte| is_base91(*byte)))
2181}
2182
2183fn is_timestamp(value: &[u8]) -> bool {
2184 value.len() == 7
2185 && value[..6].iter().all(u8::is_ascii_digit)
2186 && matches!(value[6], b'z' | b'/' | b'h')
2187}
2188
2189fn is_maidenhead_locator(value: &[u8]) -> bool {
2190 value.len() == 6
2191 && is_ascii_alpha_range(value[0], b'A', b'R')
2192 && is_ascii_alpha_range(value[1], b'A', b'R')
2193 && value[2].is_ascii_digit()
2194 && value[3].is_ascii_digit()
2195 && is_ascii_alpha_range(value[4], b'A', b'X')
2196 && is_ascii_alpha_range(value[5], b'A', b'X')
2197}
2198
2199fn is_ascii_alpha_range(value: u8, start: u8, end: u8) -> bool {
2200 let uppercase = value.to_ascii_uppercase();
2201 (start..=end).contains(&uppercase)
2202}
2203
2204fn decode_latitude(value: &[u8]) -> Option<f64> {
2205 if !is_latitude(value) {
2206 return None;
2207 }
2208
2209 let degrees = parse_u16(&value[..2])? as f64;
2210 let minutes = parse_fixed_minutes(&value[2..7])?;
2211 let sign = match value[7] {
2212 b'N' => 1.0,
2213 b'S' => -1.0,
2214 _ => return None,
2215 };
2216
2217 Some(sign * (degrees + minutes / 60.0))
2218}
2219
2220fn decode_longitude(value: &[u8]) -> Option<f64> {
2221 if !is_longitude(value) {
2222 return None;
2223 }
2224
2225 let degrees = parse_u16(&value[..3])? as f64;
2226 let minutes = parse_fixed_minutes(&value[3..8])?;
2227 let sign = match value[8] {
2228 b'E' => 1.0,
2229 b'W' => -1.0,
2230 _ => return None,
2231 };
2232
2233 Some(sign * (degrees + minutes / 60.0))
2234}
2235
2236fn parse_fixed_minutes(value: &[u8]) -> Option<f64> {
2237 if value.len() != 5 || value[2] != b'.' || !value[..2].iter().all(u8::is_ascii_digit) {
2238 return None;
2239 }
2240
2241 let whole = parse_u16(&value[..2])? as f64;
2242 let fraction = parse_u16(&value[3..])? as f64 / 100.0;
2243 Some(whole + fraction)
2244}
2245
2246fn decode_base91(value: &[u8]) -> Option<u32> {
2247 if value.len() != 4 || !value.iter().all(|byte| is_base91(*byte)) {
2248 return None;
2249 }
2250
2251 let mut decoded = 0u32;
2252 for byte in value {
2253 decoded = decoded * 91 + u32::from(byte - b'!');
2254 }
2255
2256 Some(decoded)
2257}
2258
2259fn parse_u16(value: &[u8]) -> Option<u16> {
2260 if value.is_empty() || !value.iter().all(u8::is_ascii_digit) {
2261 return None;
2262 }
2263
2264 let mut parsed = 0u16;
2265 for digit in value {
2266 parsed = parsed.checked_mul(10)?;
2267 parsed = parsed.checked_add(u16::from(digit - b'0'))?;
2268 }
2269
2270 Some(parsed)
2271}
2272
2273fn parse_i16(value: &[u8]) -> Option<i16> {
2274 if value.is_empty() {
2275 return None;
2276 }
2277
2278 let (sign, digits) = match value[0] {
2279 b'-' => (-1, &value[1..]),
2280 b'+' => (1, &value[1..]),
2281 _ => (1, value),
2282 };
2283
2284 let unsigned = parse_u16(digits)?;
2285 i16::try_from(unsigned).ok()?.checked_mul(sign)
2286}
2287
2288fn parse_hex_byte(value: &[u8]) -> Option<u8> {
2289 if value.len() != 2 {
2290 return None;
2291 }
2292
2293 Some(hex_value(value[0])? * 16 + hex_value(value[1])?)
2294}
2295
2296fn hex_value(value: u8) -> Option<u8> {
2297 match value {
2298 b'0'..=b'9' => Some(value - b'0'),
2299 b'A'..=b'F' => Some(value - b'A' + 10),
2300 b'a'..=b'f' => Some(value - b'a' + 10),
2301 _ => None,
2302 }
2303}
2304
2305fn parse_tagged_u16(report: &[u8], tag: u8, width: usize) -> Option<u16> {
2306 parse_tagged(report, tag, width).and_then(parse_u16)
2307}
2308
2309fn parse_tagged_i16(report: &[u8], tag: u8, width: usize) -> Option<i16> {
2310 parse_tagged(report, tag, width).and_then(parse_i16)
2311}
2312
2313fn parse_tagged(report: &[u8], tag: u8, width: usize) -> Option<&[u8]> {
2314 let start = report.iter().position(|byte| *byte == tag)? + 1;
2315 report.get(start..start + width)
2316}
2317
2318fn decode_mic_e_status(destination: &[u8]) -> Option<MicEStatus> {
2319 if destination.len() != 6 {
2320 return None;
2321 }
2322
2323 let bytes = destination.get(..3)?;
2324 Some(MicEStatus::Custom([
2325 mic_e_status_bit(bytes[0])?,
2326 mic_e_status_bit(bytes[1])?,
2327 mic_e_status_bit(bytes[2])?,
2328 ]))
2329}
2330
2331fn decode_mic_e_message_code(destination: &[u8]) -> Option<MicEMessageCode> {
2332 if destination.len() != 6 {
2333 return None;
2334 }
2335
2336 let mut bits = [MicEMessageBit::Zero; 3];
2337 for (index, byte) in destination[..3].iter().copied().enumerate() {
2338 bits[index] = mic_e_message_bit(byte)?;
2339 }
2340
2341 let code = message_code_number([
2342 !matches!(bits[0], MicEMessageBit::Zero),
2343 !matches!(bits[1], MicEMessageBit::Zero),
2344 !matches!(bits[2], MicEMessageBit::Zero),
2345 ]);
2346
2347 if code == 7 {
2348 return Some(MicEMessageCode::Emergency);
2349 }
2350
2351 let has_standard = bits
2352 .iter()
2353 .any(|bit| matches!(bit, MicEMessageBit::StandardOne));
2354 let has_custom = bits
2355 .iter()
2356 .any(|bit| matches!(bit, MicEMessageBit::CustomOne));
2357
2358 if has_standard && !has_custom {
2359 return standard_mic_e_message(code).map(MicEMessageCode::Standard);
2360 }
2361
2362 if has_custom && !has_standard {
2363 return Some(MicEMessageCode::Custom(code));
2364 }
2365
2366 None
2367}
2368
2369#[derive(Clone, Copy)]
2370enum MicEMessageBit {
2371 Zero,
2372 StandardOne,
2373 CustomOne,
2374}
2375
2376fn mic_e_message_bit(byte: u8) -> Option<MicEMessageBit> {
2377 match byte {
2378 b'0'..=b'9' | b'L' => Some(MicEMessageBit::Zero),
2379 b'A'..=b'K' => Some(MicEMessageBit::StandardOne),
2380 b'P'..=b'Z' => Some(MicEMessageBit::CustomOne),
2381 _ => None,
2382 }
2383}
2384
2385fn message_code_number(bits: [bool; 3]) -> u8 {
2386 match bits {
2387 [true, true, true] => 0,
2388 [true, true, false] => 1,
2389 [true, false, true] => 2,
2390 [true, false, false] => 3,
2391 [false, true, true] => 4,
2392 [false, true, false] => 5,
2393 [false, false, true] => 6,
2394 [false, false, false] => 7,
2395 }
2396}
2397
2398fn standard_mic_e_message(code: u8) -> Option<MicEStandardMessage> {
2399 match code {
2400 0 => Some(MicEStandardMessage::OffDuty),
2401 1 => Some(MicEStandardMessage::EnRoute),
2402 2 => Some(MicEStandardMessage::InService),
2403 3 => Some(MicEStandardMessage::Returning),
2404 4 => Some(MicEStandardMessage::Committed),
2405 5 => Some(MicEStandardMessage::Special),
2406 6 => Some(MicEStandardMessage::Priority),
2407 _ => None,
2408 }
2409}
2410
2411fn mic_e_status_bit(byte: u8) -> Option<bool> {
2412 match byte {
2413 b'0'..=b'9' | b'L' => Some(false),
2414 b'A'..=b'K' | b'P'..=b'Z' => Some(true),
2415 _ => None,
2416 }
2417}
2418
2419fn decode_mic_e_latitude_digits(destination: &[u8]) -> Option<[u8; 6]> {
2420 if destination.len() != 6 {
2421 return None;
2422 }
2423
2424 let mut digits = [0u8; 6];
2425 for (index, byte) in destination.iter().copied().enumerate() {
2426 digits[index] = mic_e_latitude_digit(byte)?;
2427 }
2428
2429 Some(digits)
2430}
2431
2432fn mic_e_latitude_digit(byte: u8) -> Option<u8> {
2433 match byte {
2434 b'0'..=b'9' => Some(byte - b'0'),
2435 b'A'..=b'J' => Some(byte - b'A'),
2436 b'P'..=b'Y' => Some(byte - b'P'),
2437 b'K' | b'L' | b'Z' => Some(0),
2438 _ => None,
2439 }
2440}
2441
2442fn decode_mic_e_latitude(destination: &[u8]) -> Option<f64> {
2443 let digits = decode_mic_e_latitude_digits(destination)?;
2444 let degrees = u16::from(digits[0]) * 10 + u16::from(digits[1]);
2445 let minutes = u16::from(digits[2]) * 10 + u16::from(digits[3]);
2446 let hundredths = u16::from(digits[4]) * 10 + u16::from(digits[5]);
2447 if degrees > 90 || minutes > 59 {
2448 return None;
2449 }
2450
2451 let sign = if mic_e_north(destination[3])? {
2452 1.0
2453 } else {
2454 -1.0
2455 };
2456 Some(sign * (f64::from(degrees) + (f64::from(minutes) + f64::from(hundredths) / 100.0) / 60.0))
2457}
2458
2459fn decode_mic_e_longitude(destination: &[u8], body: &[u8]) -> Option<f64> {
2460 if destination.len() != 6 || body.len() < 3 {
2461 return None;
2462 }
2463
2464 let mut degrees = i16::from(mic_e_body_value(body[0])?);
2465 if mic_e_longitude_offset(destination[4])? {
2466 degrees += 100;
2467 }
2468 if (180..=189).contains(°rees) {
2469 degrees -= 80;
2470 } else if (190..=199).contains(°rees) {
2471 degrees -= 190;
2472 }
2473
2474 let minutes = mic_e_body_value(body[1])?;
2475 let hundredths = mic_e_body_value(body[2])?;
2476 if !(0..=179).contains(°rees) || minutes > 59 || hundredths > 99 {
2477 return None;
2478 }
2479
2480 let sign = if mic_e_west(destination[5])? {
2481 -1.0
2482 } else {
2483 1.0
2484 };
2485 Some(sign * (f64::from(degrees) + (f64::from(minutes) + f64::from(hundredths) / 100.0) / 60.0))
2486}
2487
2488fn decode_mic_e_speed_course(body: &[u8]) -> Option<MicESpeedCourse> {
2489 if body.len() < 6 {
2490 return None;
2491 }
2492
2493 let speed_tens = u16::from(mic_e_body_value(body[3])?);
2494 let speed_units_course_hundreds = u16::from(mic_e_body_value(body[4])?);
2495 let course_remainder = u16::from(mic_e_body_value(body[5])?);
2496 let mut speed_knots = speed_tens * 10 + speed_units_course_hundreds / 10;
2497 if speed_knots >= 800 {
2498 speed_knots -= 800;
2499 }
2500
2501 Some(MicESpeedCourse {
2502 speed_knots,
2503 course_degrees: (speed_units_course_hundreds % 10) * 100 + course_remainder,
2504 })
2505}
2506
2507fn mic_e_body_value(byte: u8) -> Option<u8> {
2508 let value = byte.checked_sub(28)?;
2509 (value <= 99).then_some(value)
2510}
2511
2512fn mic_e_north(byte: u8) -> Option<bool> {
2513 match byte {
2514 b'0'..=b'9' | b'A'..=b'L' => Some(false),
2515 b'P'..=b'Z' => Some(true),
2516 _ => None,
2517 }
2518}
2519
2520fn mic_e_longitude_offset(byte: u8) -> Option<bool> {
2521 match byte {
2522 b'0'..=b'9' | b'A'..=b'L' => Some(false),
2523 b'P'..=b'Z' => Some(true),
2524 _ => None,
2525 }
2526}
2527
2528fn mic_e_west(byte: u8) -> Option<bool> {
2529 match byte {
2530 b'0'..=b'9' | b'A'..=b'L' => Some(false),
2531 b'P'..=b'Z' => Some(true),
2532 _ => None,
2533 }
2534}
2535
2536#[derive(Clone, Debug, Eq, PartialEq)]
2538pub enum ParseError {
2539 Empty,
2541 Oversized,
2543 MissingSeparator,
2545 EmptySegment,
2547 InvalidAddress,
2549}
2550
2551impl ParseError {
2552 #[must_use]
2554 pub fn code(&self) -> &'static str {
2555 match self {
2556 Self::Empty => "parse.empty",
2557 Self::Oversized => "parse.oversized",
2558 Self::MissingSeparator => "parse.missing_separator",
2559 Self::EmptySegment => "parse.empty_segment",
2560 Self::InvalidAddress => "parse.invalid_address",
2561 }
2562 }
2563
2564 #[must_use]
2566 pub fn diagnostic(&self) -> ErrorDiagnostic {
2567 match self {
2568 Self::Empty => ErrorDiagnostic {
2569 layer: DiagnosticLayer::Parse,
2570 code: self.code(),
2571 name: "empty",
2572 description: "no packet bytes were supplied to the codec boundary",
2573 remediation: "drop empty transport records before calling parse_packet",
2574 },
2575 Self::Oversized => ErrorDiagnostic {
2576 layer: DiagnosticLayer::Parse,
2577 code: self.code(),
2578 name: "oversized",
2579 description: "packet exceeds the configured parser byte limit",
2580 remediation: "reject the input or lower upstream batch sizes before parsing",
2581 },
2582 Self::MissingSeparator => ErrorDiagnostic {
2583 layer: DiagnosticLayer::Parse,
2584 code: self.code(),
2585 name: "missing_separator",
2586 description: "packet is missing the required source>path:payload separators",
2587 remediation: "only send source>path:payload APRS packet bytes into the codec",
2588 },
2589 Self::EmptySegment => ErrorDiagnostic {
2590 layer: DiagnosticLayer::Parse,
2591 code: self.code(),
2592 name: "empty_segment",
2593 description: "packet contains an empty source, path, or payload segment",
2594 remediation: "reject the input and inspect upstream framing before retrying",
2595 },
2596 Self::InvalidAddress => ErrorDiagnostic {
2597 layer: DiagnosticLayer::Parse,
2598 code: self.code(),
2599 name: "invalid_address",
2600 description:
2601 "packet source or path contains bytes outside the conservative address set",
2602 remediation:
2603 "preserve the raw bytes for review and reject malformed address metadata",
2604 },
2605 }
2606 }
2607}
2608
2609pub fn parse_packet(input: &[u8]) -> Result<ParsedPacket, ParseError> {
2615 parse_packet_with_options(input, ParseOptions::default())
2616}
2617
2618pub fn parse_packet_with_options(
2620 input: &[u8],
2621 options: ParseOptions,
2622) -> Result<ParsedPacket, ParseError> {
2623 if input.is_empty() {
2624 return Err(ParseError::Empty);
2625 }
2626
2627 if input.len() > options.max_packet_len {
2628 return Err(ParseError::Oversized);
2629 }
2630
2631 let source_end = input
2632 .iter()
2633 .position(|byte| *byte == b'>')
2634 .ok_or(ParseError::MissingSeparator)?;
2635 let payload_separator = input[source_end + 1..]
2636 .iter()
2637 .position(|byte| *byte == b':')
2638 .map(|offset| source_end + 1 + offset)
2639 .ok_or(ParseError::MissingSeparator)?;
2640
2641 let path_start = source_end + 1;
2642 let path_end = payload_separator;
2643 let payload_start = payload_separator + 1;
2644
2645 if source_end == 0 || path_start == path_end || payload_start == input.len() {
2646 return Err(ParseError::EmptySegment);
2647 }
2648
2649 let Some(path_components) = path_component_ranges(input, path_start, path_end) else {
2650 return Err(ParseError::InvalidAddress);
2651 };
2652
2653 if !is_ax25_like_source(&input[..source_end])
2654 || !path_components
2655 .iter()
2656 .all(|(start, end)| is_ax25_like_path_component(&input[*start..*end]))
2657 {
2658 return Err(ParseError::InvalidAddress);
2659 }
2660
2661 Ok(ParsedPacket {
2662 raw: RawPacket {
2663 bytes: input.to_vec(),
2664 },
2665 source_end,
2666 path_start,
2667 path_end,
2668 path_components,
2669 payload_start,
2670 })
2671}
2672
2673fn path_component_ranges(
2674 input: &[u8],
2675 path_start: usize,
2676 path_end: usize,
2677) -> Option<Vec<(usize, usize)>> {
2678 let mut components = Vec::new();
2679 let mut component_start = path_start;
2680
2681 for (offset, byte) in input[path_start..path_end].iter().enumerate() {
2682 if *byte == b',' {
2683 let index = path_start + offset;
2684 if component_start == index {
2685 return None;
2686 }
2687 components.push((component_start, index));
2688 component_start = index + 1;
2689 }
2690 }
2691
2692 if component_start == path_end {
2693 return None;
2694 }
2695
2696 components.push((component_start, path_end));
2697 Some(components)
2698}
2699
2700fn is_ax25_like_source(source: &[u8]) -> bool {
2701 is_ax25_like_address(source, false)
2702}
2703
2704fn is_ax25_like_path_component(component: &[u8]) -> bool {
2705 is_ax25_like_address(component, true)
2706}
2707
2708fn is_ax25_like_address(address: &[u8], allow_repeated_marker: bool) -> bool {
2709 let address = if allow_repeated_marker {
2710 address.strip_suffix(b"*").unwrap_or(address)
2711 } else {
2712 address
2713 };
2714
2715 if address.is_empty() || address.contains(&b'*') {
2716 return false;
2717 }
2718
2719 let (callsign, ssid) = match address.iter().position(|byte| *byte == b'-') {
2720 Some(separator) => (&address[..separator], Some(&address[separator + 1..])),
2721 None => (address, None),
2722 };
2723
2724 is_ax25_like_callsign(callsign) && ssid.map_or(true, is_ax25_like_ssid)
2725}
2726
2727fn is_ax25_like_callsign(callsign: &[u8]) -> bool {
2728 (1..=6).contains(&callsign.len())
2729 && callsign
2730 .iter()
2731 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit())
2732}
2733
2734fn is_ax25_like_ssid(ssid: &[u8]) -> bool {
2735 if ssid.is_empty() || ssid.len() > 2 || !ssid.iter().all(u8::is_ascii_digit) {
2736 return false;
2737 }
2738
2739 let mut value = 0u8;
2740 for digit in ssid {
2741 value = value * 10 + (digit - b'0');
2742 }
2743
2744 value <= 15
2745}