#![doc = include_str!("../README.md")]
#![no_std]
#[cfg(feature = "alloc")]
extern crate alloc;
mod change;
mod record;
pub use change::Change;
pub use osc7501_parser as parser;
pub use record::Record;
use core::ops::{Deref, DerefMut};
use parser::{Action, AppId, RecordId, Report, State};
use record::StoredRecord;
pub const MIN_HOST_CAPACITY: usize = 64;
pub const MAX_RECORDS: usize = 256;
#[derive(Debug)]
pub struct Slot {
record: Option<StoredRecord>,
rank: u16,
}
impl Slot {
pub const EMPTY: Self = Self {
record: None,
rank: 0,
};
}
enum Storage<'a> {
Borrowed(&'a mut [Slot]),
#[cfg(feature = "alloc")]
Owned(alloc::vec::Vec<Slot>),
}
impl Storage<'_> {
const fn as_slice(&self) -> &[Slot] {
match self {
Self::Borrowed(slots) => slots,
#[cfg(feature = "alloc")]
Self::Owned(slots) => slots.as_slice(),
}
}
}
impl Deref for Storage<'_> {
type Target = [Slot];
fn deref(&self) -> &Self::Target {
self.as_slice()
}
}
impl DerefMut for Storage<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
Self::Borrowed(slots) => slots,
#[cfg(feature = "alloc")]
Self::Owned(slots) => slots,
}
}
}
pub struct Store<'a> {
slots: Storage<'a>,
length: u16,
}
#[cfg(feature = "alloc")]
impl Store<'static> {
pub fn new(capacity: usize) -> Result<Self, Error> {
validate_capacity(capacity)?;
let mut slots = alloc::vec::Vec::new();
slots.try_reserve_exact(capacity)?;
slots.resize_with(capacity, || Slot::EMPTY);
Ok(Self {
slots: Storage::Owned(slots),
length: 0,
})
}
}
impl<'a> Store<'a> {
pub fn from_slots(slots: &'a mut [Slot]) -> Result<Self, Error> {
validate_capacity(slots.len())?;
for slot in &mut *slots {
slot.record = None;
slot.rank = 0;
}
Ok(Self {
slots: Storage::Borrowed(slots),
length: 0,
})
}
#[must_use]
pub fn len(&self) -> usize {
usize::from(self.length)
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.length == 0
}
#[must_use]
pub const fn capacity(&self) -> usize {
self.slots.as_slice().len()
}
#[must_use]
pub fn get(&self, id: Option<RecordId<'_>>) -> Option<Record<'_>> {
self.find(id.map(RecordId::as_str)).map(StoredRecord::view)
}
pub fn iter(&self) -> impl Iterator<Item = Record<'_>> {
self.slots
.iter()
.filter_map(|slot| slot.record.as_ref().map(StoredRecord::view))
}
#[must_use]
pub fn effective_app(&self, id: Option<RecordId<'_>>) -> Option<AppId<'_>> {
let mut path = id.map(RecordId::as_str);
self.find(path)?;
loop {
if let Some(app) = self.find(path).and_then(|record| record.view().app()) {
return Some(app);
}
path = path?.rsplit_once('/').map(|(parent, _)| parent);
}
}
pub fn apply(&mut self, report: Report<'_>) -> Result<(), Error> {
self.apply_inner(report, |_| {}, false).map(|_| ())
}
fn apply_inner(
&mut self,
report: Report<'_>,
mut on_remove: impl FnMut(Record<'_>),
track_changes: bool,
) -> Result<Option<usize>, Error> {
let id = report.id().map(RecordId::as_str);
let Action::Set(state) = report.action() else {
self.remove_matching(
|record| match id {
None => true,
Some(parent) => record.id().is_some_and(|child| {
let child = child.as_str();
child == parent
|| child
.strip_prefix(parent)
.is_some_and(|suffix| suffix.starts_with('/'))
}),
},
&mut on_remove,
);
return Ok(None);
};
let prepared = StoredRecord::prepare(state, report)?;
let existing = self.slots.iter().position(|slot| {
slot.record
.as_ref()
.is_some_and(|record| record.matches_id(id))
});
let index = existing
.or_else(|| self.slots.iter().position(|slot| slot.record.is_none()))
.unwrap_or_else(|| {
self.slots
.iter()
.enumerate()
.min_by_key(|(_, slot)| slot.rank)
.map_or(0, |(index, _)| index)
});
let changed = track_changes
&& self.slots[index]
.record
.as_ref()
.is_none_or(|record| !record.same_fields(&prepared));
if let Some(record) = self.slots[index].record.as_ref() {
if existing.is_none() {
on_remove(record.view());
}
self.discard(index);
}
self.slots[index].record = Some(prepared);
self.slots[index].rank = self.length;
self.length += 1;
Ok(changed.then_some(index))
}
pub fn on_prompt(&mut self) {
self.remove_active(&mut |_| {});
}
pub fn on_process_exit(&mut self) {
self.on_prompt();
}
pub fn on_full_reset(&mut self) {
self.remove_matching(|_| true, &mut |_| {});
}
fn find(&self, id: Option<&str>) -> Option<&StoredRecord> {
self.slots
.iter()
.filter_map(|slot| slot.record.as_ref())
.find(|record| record.matches_id(id))
}
fn remove_active(&mut self, on_remove: &mut impl FnMut(Record<'_>)) {
self.remove_matching(
|record| matches!(record.state(), State::Working | State::Blocked),
on_remove,
);
}
fn remove_matching(
&mut self,
matches: impl Fn(Record<'_>) -> bool,
on_remove: &mut impl FnMut(Record<'_>),
) {
for index in 0..self.slots.len() {
if let Some(record) = self.slots[index].record.as_ref()
&& matches(record.view())
{
on_remove(record.view());
self.discard(index);
}
}
}
fn discard(&mut self, index: usize) {
let rank = self.slots[index].rank;
self.slots[index].record = None;
self.length -= 1;
for slot in &mut *self.slots {
if slot.record.is_some() && slot.rank > rank {
slot.rank -= 1;
}
}
}
}
fn validate_capacity(capacity: usize) -> Result<(), Error> {
if capacity == 0 || capacity > MAX_RECORDS {
return Err(Error::InvalidCapacity);
}
Ok(())
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("record capacity must be between 1 and 256")]
InvalidCapacity,
#[cfg(feature = "alloc")]
#[error("{0}")]
Allocation(#[from] alloc::collections::TryReserveError),
#[error("{0}")]
Parser(#[from] parser::Error),
}