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