Skip to main content

libaprs_engine/
service.rs

1//! Runtime-neutral service building blocks.
2//!
3//! These helpers keep storage, networking, clocks, and task runtimes
4//! application-owned while covering common ingestion policy needs.
5
6use crate::AprsData;
7
8/// Duplicate suppression decision for a packet byte sequence.
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum DuplicateDecision {
11    /// Packet bytes were not seen in the retained window.
12    New,
13    /// Packet bytes match a retained packet in the window.
14    Duplicate,
15}
16
17impl DuplicateDecision {
18    /// Stable machine-readable decision code.
19    #[must_use]
20    pub const fn code(self) -> &'static str {
21        match self {
22            Self::New => "duplicate.new",
23            Self::Duplicate => "duplicate.duplicate",
24        }
25    }
26}
27
28/// Bounded exact-byte duplicate window.
29#[derive(Clone, Debug, Eq, PartialEq)]
30pub struct DuplicateWindow {
31    capacity: usize,
32    packets: Vec<Vec<u8>>,
33}
34
35impl DuplicateWindow {
36    /// Creates a duplicate window retaining at most `capacity` packet byte
37    /// sequences. A zero-capacity window never retains packets.
38    #[must_use]
39    pub const fn new(capacity: usize) -> Self {
40        Self {
41            capacity,
42            packets: Vec::new(),
43        }
44    }
45
46    /// Observes packet bytes and reports whether the exact bytes were recently
47    /// retained.
48    pub fn observe(&mut self, packet: &[u8]) -> DuplicateDecision {
49        if self.packets.iter().any(|existing| existing == packet) {
50            return DuplicateDecision::Duplicate;
51        }
52
53        if self.capacity > 0 {
54            if self.packets.len() == self.capacity {
55                self.packets.remove(0);
56            }
57            self.packets.push(packet.to_vec());
58        }
59
60        DuplicateDecision::New
61    }
62
63    /// Returns the number of packet byte sequences currently retained.
64    #[must_use]
65    pub fn retained_len(&self) -> usize {
66        self.packets.len()
67    }
68
69    /// Returns the configured retention capacity.
70    #[must_use]
71    pub const fn capacity(&self) -> usize {
72        self.capacity
73    }
74}
75
76/// Packet-rate budget decision.
77#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78pub enum RateLimitDecision {
79    /// The caller may process this packet.
80    Allowed,
81    /// The caller-owned budget has been exhausted.
82    Limited,
83}
84
85impl RateLimitDecision {
86    /// Stable machine-readable decision code.
87    #[must_use]
88    pub const fn code(self) -> &'static str {
89        match self {
90            Self::Allowed => "rate.allowed",
91            Self::Limited => "rate.limited",
92        }
93    }
94}
95
96/// Caller-reset packet-rate budget.
97#[derive(Clone, Copy, Debug, Eq, PartialEq)]
98pub struct PacketRateBudget {
99    limit: u64,
100    remaining: u64,
101}
102
103impl PacketRateBudget {
104    /// Creates a packet-rate budget with `limit` allowed packets per
105    /// caller-owned window.
106    #[must_use]
107    pub const fn new(limit: u64) -> Self {
108        Self {
109            limit,
110            remaining: limit,
111        }
112    }
113
114    /// Attempts to consume one packet from the current budget.
115    pub fn try_consume(&mut self) -> RateLimitDecision {
116        if self.remaining == 0 {
117            return RateLimitDecision::Limited;
118        }
119
120        self.remaining -= 1;
121        RateLimitDecision::Allowed
122    }
123
124    /// Resets the current budget back to the configured limit.
125    pub fn reset(&mut self) {
126        self.remaining = self.limit;
127    }
128
129    /// Returns the configured packet limit.
130    #[must_use]
131    pub const fn limit(&self) -> u64 {
132        self.limit
133    }
134
135    /// Returns the remaining packet budget in the current caller-owned window.
136    #[must_use]
137    pub const fn remaining(&self) -> u64 {
138        self.remaining
139    }
140}
141
142/// Semantic packet family for service-level blocklists.
143#[derive(Clone, Copy, Debug, Eq, PartialEq)]
144pub enum SemanticFamily {
145    /// Status report.
146    Status,
147    /// Position without timestamp.
148    Position,
149    /// Timestamped position.
150    TimestampedPosition,
151    /// Compressed position.
152    CompressedPosition,
153    /// Message, bulletin, announcement, acknowledgement, or rejection.
154    Message,
155    /// Object report.
156    Object,
157    /// Item report.
158    Item,
159    /// Weather report.
160    Weather,
161    /// Telemetry report.
162    Telemetry,
163    /// Telemetry metadata message.
164    TelemetryMetadata,
165    /// Query packet.
166    Query,
167    /// Station capabilities packet.
168    Capability,
169    /// NMEA sentence packet.
170    Nmea,
171    /// Mic-E packet.
172    MicE,
173    /// Maidenhead locator packet.
174    Maidenhead,
175    /// User-defined packet.
176    UserDefined,
177    /// Third-party traffic packet.
178    ThirdParty,
179    /// Unsupported data type identifier.
180    Unsupported,
181    /// Codec-valid but semantically malformed packet.
182    Malformed,
183}
184
185impl SemanticFamily {
186    /// Classifies a semantic packet view into a stable family.
187    #[must_use]
188    pub const fn from_aprs_data(data: &AprsData<'_>) -> Self {
189        match data {
190            AprsData::Status { .. } => Self::Status,
191            AprsData::Position(_) => Self::Position,
192            AprsData::TimestampedPosition(_) => Self::TimestampedPosition,
193            AprsData::CompressedPosition(_) => Self::CompressedPosition,
194            AprsData::Message(_) => Self::Message,
195            AprsData::Object(_) => Self::Object,
196            AprsData::Item(_) => Self::Item,
197            AprsData::Weather(_) => Self::Weather,
198            AprsData::Telemetry(_) => Self::Telemetry,
199            AprsData::TelemetryMetadata(_) => Self::TelemetryMetadata,
200            AprsData::Query(_) => Self::Query,
201            AprsData::Capability(_) => Self::Capability,
202            AprsData::Nmea(_) => Self::Nmea,
203            AprsData::MicE(_) => Self::MicE,
204            AprsData::Maidenhead(_) => Self::Maidenhead,
205            AprsData::UserDefined(_) => Self::UserDefined,
206            AprsData::ThirdParty(_) => Self::ThirdParty,
207            AprsData::Unsupported { .. } => Self::Unsupported,
208            AprsData::Malformed { .. } => Self::Malformed,
209        }
210    }
211
212    /// Stable machine-readable family code.
213    #[must_use]
214    pub const fn code(self) -> &'static str {
215        match self {
216            Self::Status => "status",
217            Self::Position => "position",
218            Self::TimestampedPosition => "timestamped_position",
219            Self::CompressedPosition => "compressed_position",
220            Self::Message => "message",
221            Self::Object => "object",
222            Self::Item => "item",
223            Self::Weather => "weather",
224            Self::Telemetry => "telemetry",
225            Self::TelemetryMetadata => "telemetry_metadata",
226            Self::Query => "query",
227            Self::Capability => "capability",
228            Self::Nmea => "nmea",
229            Self::MicE => "mic_e",
230            Self::Maidenhead => "maidenhead",
231            Self::UserDefined => "user_defined",
232            Self::ThirdParty => "third_party",
233            Self::Unsupported => "unsupported",
234            Self::Malformed => "malformed",
235        }
236    }
237}
238
239/// Runtime-neutral semantic-family blocklist helper.
240#[derive(Clone, Copy, Debug, Eq, PartialEq)]
241pub struct SemanticBlocklist<'a> {
242    families: &'a [SemanticFamily],
243}
244
245impl<'a> SemanticBlocklist<'a> {
246    /// Creates a blocklist over caller-owned semantic-family storage.
247    #[must_use]
248    pub const fn new(families: &'a [SemanticFamily]) -> Self {
249        Self { families }
250    }
251
252    /// Returns true when the semantic packet family is blocklisted.
253    #[must_use]
254    pub fn rejects(&self, data: &AprsData<'_>) -> bool {
255        let family = SemanticFamily::from_aprs_data(data);
256        self.families.contains(&family)
257    }
258
259    /// Returns the caller-owned blocked families.
260    #[must_use]
261    pub const fn families(&self) -> &'a [SemanticFamily] {
262        self.families
263    }
264}