1#![doc = include_str!("../README.md")]
2#![no_std]
3
4#[cfg(feature = "alloc")]
5extern crate alloc;
6
7mod change;
8mod record;
9
10pub use change::Change;
11pub use osc7501_parser as parser;
13pub use record::Record;
14
15use core::ops::{Deref, DerefMut};
16use parser::{Action, AppId, RecordId, Report, State};
17use record::StoredRecord;
18
19pub const MIN_HOST_CAPACITY: usize = 64;
21pub const MAX_RECORDS: usize = 256;
23
24#[derive(Debug)]
26pub struct Slot {
27 record: Option<StoredRecord>,
28 rank: u16,
29}
30
31impl Slot {
32 pub const EMPTY: Self = Self {
34 record: None,
35 rank: 0,
36 };
37}
38
39enum Storage<'a> {
40 Borrowed(&'a mut [Slot]),
41 #[cfg(feature = "alloc")]
42 Owned(alloc::vec::Vec<Slot>),
43}
44
45impl Storage<'_> {
46 const fn as_slice(&self) -> &[Slot] {
47 match self {
48 Self::Borrowed(slots) => slots,
49 #[cfg(feature = "alloc")]
50 Self::Owned(slots) => slots.as_slice(),
51 }
52 }
53}
54
55impl Deref for Storage<'_> {
56 type Target = [Slot];
57
58 fn deref(&self) -> &Self::Target {
59 self.as_slice()
60 }
61}
62
63impl DerefMut for Storage<'_> {
64 fn deref_mut(&mut self) -> &mut Self::Target {
65 match self {
66 Self::Borrowed(slots) => slots,
67 #[cfg(feature = "alloc")]
68 Self::Owned(slots) => slots,
69 }
70 }
71}
72
73pub struct Store<'a> {
81 slots: Storage<'a>,
82 length: u16,
83}
84
85#[cfg(feature = "alloc")]
86impl Store<'static> {
87 pub fn new(capacity: usize) -> Result<Self, Error> {
95 validate_capacity(capacity)?;
96 let mut slots = alloc::vec::Vec::new();
97 slots.try_reserve_exact(capacity)?;
98 slots.resize_with(capacity, || Slot::EMPTY);
99 Ok(Self {
100 slots: Storage::Owned(slots),
101 length: 0,
102 })
103 }
104}
105
106impl<'a> Store<'a> {
107 pub fn from_slots(slots: &'a mut [Slot]) -> Result<Self, Error> {
115 validate_capacity(slots.len())?;
116 for slot in &mut *slots {
117 slot.record = None;
118 slot.rank = 0;
119 }
120 Ok(Self {
121 slots: Storage::Borrowed(slots),
122 length: 0,
123 })
124 }
125
126 #[must_use]
128 pub fn len(&self) -> usize {
129 usize::from(self.length)
130 }
131
132 #[must_use]
134 pub const fn is_empty(&self) -> bool {
135 self.length == 0
136 }
137
138 #[must_use]
140 pub const fn capacity(&self) -> usize {
141 self.slots.as_slice().len()
142 }
143
144 #[must_use]
146 pub fn get(&self, id: Option<RecordId<'_>>) -> Option<Record<'_>> {
147 self.find(id.map(RecordId::as_str)).map(StoredRecord::view)
148 }
149
150 pub fn iter(&self) -> impl Iterator<Item = Record<'_>> {
152 self.slots
153 .iter()
154 .filter_map(|slot| slot.record.as_ref().map(StoredRecord::view))
155 }
156
157 #[must_use]
160 pub fn effective_app(&self, id: Option<RecordId<'_>>) -> Option<AppId<'_>> {
161 let mut path = id.map(RecordId::as_str);
162 self.find(path)?;
163 loop {
164 if let Some(app) = self.find(path).and_then(|record| record.view().app()) {
165 return Some(app);
166 }
167 path = path?.rsplit_once('/').map(|(parent, _)| parent);
168 }
169 }
170
171 pub fn apply(&mut self, report: Report<'_>) -> Result<(), Error> {
184 self.apply_inner(report, |_| {}, false).map(|_| ())
185 }
186
187 fn apply_inner(
188 &mut self,
189 report: Report<'_>,
190 mut on_remove: impl FnMut(Record<'_>),
191 track_changes: bool,
192 ) -> Result<Option<usize>, Error> {
193 let id = report.id().map(RecordId::as_str);
194 let Action::Set(state) = report.action() else {
195 self.remove_matching(
196 |record| match id {
197 None => true,
198 Some(parent) => record.id().is_some_and(|child| {
199 let child = child.as_str();
200 child == parent
201 || child
202 .strip_prefix(parent)
203 .is_some_and(|suffix| suffix.starts_with('/'))
204 }),
205 },
206 &mut on_remove,
207 );
208 return Ok(None);
209 };
210
211 let prepared = StoredRecord::prepare(state, report)?;
212 let existing = self.slots.iter().position(|slot| {
213 slot.record
214 .as_ref()
215 .is_some_and(|record| record.matches_id(id))
216 });
217 let index = existing
218 .or_else(|| self.slots.iter().position(|slot| slot.record.is_none()))
219 .unwrap_or_else(|| {
220 self.slots
221 .iter()
222 .enumerate()
223 .min_by_key(|(_, slot)| slot.rank)
224 .map_or(0, |(index, _)| index)
225 });
226 let changed = track_changes
227 && self.slots[index]
228 .record
229 .as_ref()
230 .is_none_or(|record| !record.same_fields(&prepared));
231 if let Some(record) = self.slots[index].record.as_ref() {
232 if existing.is_none() {
233 on_remove(record.view());
234 }
235 self.discard(index);
236 }
237 self.slots[index].record = Some(prepared);
238 self.slots[index].rank = self.length;
239 self.length += 1;
240 Ok(changed.then_some(index))
241 }
242
243 pub fn on_prompt(&mut self) {
247 self.remove_active(&mut |_| {});
248 }
249
250 pub fn on_process_exit(&mut self) {
255 self.on_prompt();
256 }
257
258 pub fn on_full_reset(&mut self) {
262 self.remove_matching(|_| true, &mut |_| {});
263 }
264
265 fn find(&self, id: Option<&str>) -> Option<&StoredRecord> {
266 self.slots
267 .iter()
268 .filter_map(|slot| slot.record.as_ref())
269 .find(|record| record.matches_id(id))
270 }
271
272 fn remove_active(&mut self, on_remove: &mut impl FnMut(Record<'_>)) {
273 self.remove_matching(
274 |record| matches!(record.state(), State::Working | State::Blocked),
275 on_remove,
276 );
277 }
278
279 fn remove_matching(
280 &mut self,
281 matches: impl Fn(Record<'_>) -> bool,
282 on_remove: &mut impl FnMut(Record<'_>),
283 ) {
284 for index in 0..self.slots.len() {
285 if let Some(record) = self.slots[index].record.as_ref()
286 && matches(record.view())
287 {
288 on_remove(record.view());
289 self.discard(index);
290 }
291 }
292 }
293
294 fn discard(&mut self, index: usize) {
295 let rank = self.slots[index].rank;
296 self.slots[index].record = None;
297 self.length -= 1;
298 for slot in &mut *self.slots {
300 if slot.record.is_some() && slot.rank > rank {
301 slot.rank -= 1;
302 }
303 }
304 }
305}
306
307fn validate_capacity(capacity: usize) -> Result<(), Error> {
308 if capacity == 0 || capacity > MAX_RECORDS {
309 return Err(Error::InvalidCapacity);
310 }
311 Ok(())
312}
313
314#[derive(Debug, thiserror::Error)]
316#[non_exhaustive]
317pub enum Error {
318 #[error("record capacity must be between 1 and 256")]
320 InvalidCapacity,
321 #[cfg(feature = "alloc")]
323 #[error("{0}")]
324 Allocation(#[from] alloc::collections::TryReserveError),
325 #[error("{0}")]
327 Parser(#[from] parser::Error),
328}