Skip to main content

osc7501_store/
record.rs

1use crate::parser::{
2    AppId, EncodedText, Error, Kind, MAX_APP, MAX_ID, MAX_MESSAGE, MAX_TITLE, Progress, RecordId,
3    Report, State,
4};
5
6#[derive(Debug)]
7struct Text<const N: usize> {
8    bytes: [u8; N],
9    length: usize,
10}
11
12impl<const N: usize> Text<N> {
13    const EMPTY: Self = Self {
14        bytes: [0; N],
15        length: 0,
16    };
17
18    fn copy(value: &str) -> Result<Self, Error> {
19        let mut text = Self {
20            bytes: [0; N],
21            length: value.len(),
22        };
23        text.bytes
24            .get_mut(..value.len())
25            .ok_or(Error::BufferTooSmall)?
26            .copy_from_slice(value.as_bytes());
27        Ok(text)
28    }
29
30    fn decode(&mut self, value: EncodedText<'_>) -> Result<(), Error> {
31        value.decode_into(&mut self.bytes)?;
32        self.length = value.decoded_len();
33        Ok(())
34    }
35
36    fn as_str(&self) -> Option<&str> {
37        core::str::from_utf8(&self.bytes[..self.length]).ok()
38    }
39}
40
41#[derive(Debug)]
42pub(crate) struct StoredRecord {
43    state: State,
44    id: Option<Text<MAX_ID>>,
45    app: Option<Text<MAX_APP>>,
46    title: Option<Text<MAX_TITLE>>,
47    message: Option<Text<MAX_MESSAGE>>,
48    kind: Option<Kind>,
49    progress: Option<Progress>,
50}
51
52impl StoredRecord {
53    pub(crate) fn prepare(state: State, report: Report<'_>) -> Result<Self, Error> {
54        let mut prepared = Self {
55            state,
56            id: report.id().map(|id| Text::copy(id.as_str())).transpose()?,
57            app: report
58                .app()
59                .map(|app| Text::copy(app.as_str()))
60                .transpose()?,
61            title: None,
62            message: None,
63            kind: report.kind(),
64            progress: report.progress(),
65        };
66        // Decode into the prepared record to avoid large intermediate text copies.
67        if let Some(title) = report.title() {
68            prepared.title.insert(Text::EMPTY).decode(title)?;
69        }
70        if let Some(message) = report.message() {
71            prepared.message.insert(Text::EMPTY).decode(message)?;
72        }
73        Ok(prepared)
74    }
75
76    pub(crate) fn matches_id(&self, id: Option<&str>) -> bool {
77        match (&self.id, id) {
78            (None, None) => true,
79            (Some(stored), Some(id)) => stored.bytes[..stored.length] == *id.as_bytes(),
80            _ => false,
81        }
82    }
83
84    pub(crate) const fn view(&self) -> Record<'_> {
85        Record { stored: self }
86    }
87
88    pub(crate) fn same_fields(&self, other: &Self) -> bool {
89        let left = self.view();
90        let right = other.view();
91        left.state() == right.state()
92            && left.id() == right.id()
93            && left.app() == right.app()
94            && left.title() == right.title()
95            && left.message() == right.message()
96            && left.kind() == right.kind()
97            && left.progress() == right.progress()
98    }
99}
100
101/// A read-only view of a current record. Text is decoded, plain, and untrusted.
102#[derive(Clone, Copy, Debug)]
103pub struct Record<'a> {
104    stored: &'a StoredRecord,
105}
106
107impl<'a> Record<'a> {
108    /// Returns the current state; clear is a removal action and is never stored.
109    #[must_use]
110    pub const fn state(self) -> State {
111        self.stored.state
112    }
113
114    /// Returns the validated identifier; `None` means root.
115    #[must_use]
116    pub fn id(self) -> Option<RecordId<'a>> {
117        RecordId::new(self.stored.id.as_ref()?.as_str()?).ok()
118    }
119
120    /// Returns the explicit app, before ancestor inheritance.
121    #[must_use]
122    pub fn app(self) -> Option<AppId<'a>> {
123        AppId::new(self.stored.app.as_ref()?.as_str()?).ok()
124    }
125
126    /// Returns the decoded label, preserving absent and explicitly empty text.
127    #[must_use]
128    pub fn title(self) -> Option<&'a str> {
129        self.stored.title.as_ref()?.as_str()
130    }
131
132    /// Returns the decoded message, preserving absent and explicitly empty text.
133    #[must_use]
134    pub fn message(self) -> Option<&'a str> {
135        self.stored.message.as_ref()?.as_str()
136    }
137
138    /// Returns the blocking reason, present only for blocked records.
139    #[must_use]
140    pub const fn kind(self) -> Option<Kind> {
141        self.stored.kind
142    }
143
144    /// Returns the percentage; absent means indeterminate.
145    #[must_use]
146    pub const fn progress(self) -> Option<Progress> {
147        self.stored.progress
148    }
149}