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