Skip to main content

osc7501_parser/
types.rs

1use crate::{Error, MAX_APP, MAX_DEPTH, MAX_ID, MAX_SEGMENT, base64};
2
3/// A record's state. Clearing is represented separately by [`Action::Clear`].
4#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5#[non_exhaustive]
6pub enum State {
7    /// Waiting for a new instruction, including after cancellation.
8    Idle,
9    /// Performing work without user intervention.
10    Working,
11    /// A result is ready and has not yet been acknowledged.
12    Done,
13    /// Cannot continue without user intervention.
14    Blocked,
15    /// Work failed and stopped.
16    Error,
17}
18
19impl State {
20    /// Returns the protocol spelling.
21    #[must_use]
22    pub const fn as_str(self) -> &'static str {
23        match self {
24            Self::Idle => "idle",
25            Self::Working => "working",
26            Self::Done => "done",
27            Self::Blocked => "blocked",
28            Self::Error => "error",
29        }
30    }
31
32    /// Recognizes a revision 0.4 state, preserving unknown states as absent.
33    #[must_use]
34    pub fn parse(value: &str) -> Option<Self> {
35        match value {
36            "idle" => Some(Self::Idle),
37            "working" => Some(Self::Working),
38            "done" => Some(Self::Done),
39            "blocked" => Some(Self::Blocked),
40            "error" => Some(Self::Error),
41            _ => None,
42        }
43    }
44}
45
46/// Why a blocked record needs the user.
47#[derive(Clone, Copy, Debug, Eq, PartialEq)]
48#[non_exhaustive]
49pub enum Kind {
50    /// Approval to perform an action.
51    Permission,
52    /// An answer to a question.
53    Question,
54    /// Authentication or credentials.
55    Auth,
56}
57
58impl Kind {
59    /// Returns the protocol spelling.
60    #[must_use]
61    pub const fn as_str(self) -> &'static str {
62        match self {
63            Self::Permission => "permission",
64            Self::Question => "question",
65            Self::Auth => "auth",
66        }
67    }
68
69    /// Recognizes a revision 0.4 blocking reason.
70    #[must_use]
71    pub fn parse(value: &str) -> Option<Self> {
72        match value {
73            "permission" => Some(Self::Permission),
74            "question" => Some(Self::Question),
75            "auth" => Some(Self::Auth),
76            _ => None,
77        }
78    }
79}
80
81/// An update or a subtree removal.
82#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub enum Action {
84    /// Completely replaces the addressed record.
85    Set(State),
86    /// Removes the record and its descendants; root clears everything.
87    Clear,
88}
89
90impl Action {
91    /// Returns the spelling used in the `state` field.
92    #[must_use]
93    pub const fn as_str(self) -> &'static str {
94        match self {
95            Self::Set(state) => state.as_str(),
96            Self::Clear => "clear",
97        }
98    }
99}
100
101/// A validated percentage in the inclusive range 0..=100.
102#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub struct Progress(u8);
104
105impl Progress {
106    /// Validates a percentage without allocating.
107    ///
108    /// # Errors
109    /// Returns [`Error::InvalidProgress`] for percentages above 100.
110    pub const fn new(value: u8) -> Result<Self, Error> {
111        if value <= 100 {
112            Ok(Self(value))
113        } else {
114            Err(Error::InvalidProgress)
115        }
116    }
117
118    /// Returns the percentage.
119    #[must_use]
120    pub const fn get(self) -> u8 {
121        self.0
122    }
123}
124
125impl TryFrom<u8> for Progress {
126    type Error = Error;
127
128    fn try_from(value: u8) -> Result<Self, Self::Error> {
129        Self::new(value)
130    }
131}
132
133pub(crate) fn segment_byte(byte: u8) -> bool {
134    byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'.' | b'+' | b'-')
135}
136
137/// A validated hierarchical identifier. Root is represented by `None`.
138#[derive(Clone, Copy, Debug, Eq, PartialEq)]
139pub struct RecordId<'a>(&'a str);
140
141impl<'a> RecordId<'a> {
142    /// Validates a non-root identifier without allocating.
143    /// Segments use ASCII letters, digits, `_`, `.`, `+`, or `-`.
144    ///
145    /// # Errors
146    /// Returns [`Error::InvalidRecordId`] if the identifier or any segment is
147    /// empty, contains forbidden characters, or exceeds a length or depth limit.
148    pub fn new(value: &'a str) -> Result<Self, Error> {
149        if value.is_empty() || value.len() > MAX_ID {
150            return Err(Error::InvalidRecordId);
151        }
152        let mut depth = 0;
153        for segment in value.split('/') {
154            depth += 1;
155            if depth > MAX_DEPTH
156                || segment.is_empty()
157                || segment.len() > MAX_SEGMENT
158                || !segment.bytes().all(segment_byte)
159            {
160                return Err(Error::InvalidRecordId);
161            }
162        }
163        Ok(Self(value))
164    }
165
166    /// Returns the borrowed identifier.
167    #[must_use]
168    pub const fn as_str(self) -> &'a str {
169        self.0
170    }
171}
172
173impl<'a> TryFrom<&'a str> for RecordId<'a> {
174    type Error = Error;
175
176    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
177        Self::new(value)
178    }
179}
180
181/// A validated stable application identifier.
182#[derive(Clone, Copy, Debug, Eq, PartialEq)]
183pub struct AppId<'a>(&'a str);
184
185impl<'a> AppId<'a> {
186    /// Validates a stable identifier without allocating.
187    ///
188    /// # Errors
189    /// Returns [`Error::InvalidAppId`] if the identifier is empty, exceeds
190    /// [`MAX_APP`], or contains characters other than ASCII letters, digits,
191    /// `_`, `.`, `+`, or `-`.
192    pub fn new(value: &'a str) -> Result<Self, Error> {
193        if value.is_empty() || value.len() > MAX_APP || !value.bytes().all(segment_byte) {
194            Err(Error::InvalidAppId)
195        } else {
196            Ok(Self(value))
197        }
198    }
199
200    /// Returns the borrowed identifier.
201    #[must_use]
202    pub const fn as_str(self) -> &'a str {
203        self.0
204    }
205}
206
207impl<'a> TryFrom<&'a str> for AppId<'a> {
208    type Error = Error;
209
210    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
211        Self::new(value)
212    }
213}
214
215/// Base64 text whose UTF-8 and absence of controls have already been validated.
216/// Equality compares the encoded form: padded and unpadded spellings can decode
217/// to the same text and still compare unequal.
218#[derive(Clone, Copy, Debug, Eq, PartialEq)]
219pub struct EncodedText<'a> {
220    pub(crate) encoded: &'a str,
221    pub(crate) decoded_len: usize,
222}
223
224impl<'a> EncodedText<'a> {
225    /// Returns the original encoded text.
226    #[must_use]
227    pub const fn as_base64(self) -> &'a str {
228        self.encoded
229    }
230
231    /// Exact output capacity needed to decode this value.
232    #[must_use]
233    pub const fn decoded_len(self) -> usize {
234        self.decoded_len
235    }
236
237    /// Decodes into caller-owned storage, returning a UTF-8 view of that storage.
238    ///
239    /// # Errors
240    /// Returns [`Error::BufferTooSmall`] when the destination is too small.
241    pub fn decode_into(self, output: &mut [u8]) -> Result<&str, Error> {
242        let length = base64::decode(self.encoded.as_bytes(), output)?;
243        core::str::from_utf8(&output[..length]).map_err(|_| Error::InvalidUtf8)
244    }
245}
246
247/// A validated report. Missing fields remove the prior record's fields.
248/// Equality compares parsed fields, including the original base64 text. Compare
249/// decoded text when checking whether reports have the same meaning.
250#[derive(Clone, Copy, Debug, Eq, PartialEq)]
251pub struct Report<'a> {
252    pub(crate) action: Action,
253    pub(crate) id: Option<RecordId<'a>>,
254    pub(crate) kind: Option<Kind>,
255    pub(crate) progress: Option<Progress>,
256    pub(crate) app: Option<AppId<'a>>,
257    pub(crate) title: Option<EncodedText<'a>>,
258    pub(crate) message: Option<EncodedText<'a>>,
259}
260
261impl<'a> Report<'a> {
262    /// Complete replacement or subtree clear.
263    #[must_use]
264    pub const fn action(&self) -> Action {
265        self.action
266    }
267
268    /// Addressed record; `None` means root.
269    #[must_use]
270    pub const fn id(&self) -> Option<RecordId<'a>> {
271        self.id
272    }
273
274    /// Present only for `blocked`.
275    #[must_use]
276    pub const fn kind(&self) -> Option<Kind> {
277        self.kind
278    }
279
280    /// Present only for `working` or `blocked`; absent means indeterminate.
281    #[must_use]
282    pub const fn progress(&self) -> Option<Progress> {
283        self.progress
284    }
285
286    /// Explicit application identifier, before ancestor inheritance.
287    #[must_use]
288    pub const fn app(&self) -> Option<AppId<'a>> {
289        self.app
290    }
291
292    /// Validated, lazily decoded label.
293    #[must_use]
294    pub const fn title(&self) -> Option<EncodedText<'a>> {
295        self.title
296    }
297
298    /// Validated, lazily decoded status message.
299    #[must_use]
300    pub const fn message(&self) -> Option<EncodedText<'a>> {
301        self.message
302    }
303}
304
305const DEFAULT_STATES: &[State] = &[
306    State::Idle,
307    State::Working,
308    State::Done,
309    State::Blocked,
310    State::Error,
311];
312const DEFAULT_KINDS: &[Kind] = &[Kind::Permission, Kind::Question, Kind::Auth];
313
314/// Feature detection reply. Unknown state and kind names are skipped.
315///
316/// Missing lists use the fixed revision 0.4 defaults.
317/// Equality compares the original lists, including their presence and unknown
318/// names. Use the iterators or support queries to compare known capabilities.
319#[derive(Clone, Copy, Debug, Eq, PartialEq)]
320pub struct Capabilities<'a> {
321    pub(crate) states: Option<&'a str>,
322    pub(crate) kinds: Option<&'a str>,
323}
324
325impl Capabilities<'_> {
326    /// Iterates advertised states recognized by this crate.
327    pub fn states(&self) -> impl Iterator<Item = State> {
328        let defaults = self.states.map_or(DEFAULT_STATES, |_| &[]);
329        self.states
330            .unwrap_or_default()
331            .split(',')
332            .filter_map(State::parse)
333            .chain(defaults.iter().copied())
334    }
335
336    /// Iterates advertised blocking reasons recognized by this crate.
337    pub fn kinds(&self) -> impl Iterator<Item = Kind> {
338        let defaults = self.kinds.map_or(DEFAULT_KINDS, |_| &[]);
339        self.kinds
340            .unwrap_or_default()
341            .split(',')
342            .filter_map(Kind::parse)
343            .chain(defaults.iter().copied())
344    }
345
346    /// Checks whether a known state is supported.
347    #[must_use]
348    pub fn supports_state(&self, state: State) -> bool {
349        self.states().any(|value| value == state)
350    }
351
352    /// Checks whether a known blocking reason is supported.
353    #[must_use]
354    pub fn supports_kind(&self, kind: Kind) -> bool {
355        self.kinds().any(|value| value == kind)
356    }
357}
358
359/// A recognized protocol event.
360#[derive(Clone, Copy, Debug, Eq, PartialEq)]
361#[non_exhaustive]
362pub enum Event<'a> {
363    /// A valid report, ready to apply atomically.
364    Report(Report<'a>),
365    /// Bare `?`: a query when read from a program, or a default reply from a terminal.
366    /// Direction is deliberately left to the caller because the bytes are identical.
367    Query,
368    /// A feature reply with extension pairs.
369    Capabilities(Capabilities<'a>),
370}
371
372/// A hard protocol bound that was exceeded.
373#[derive(Clone, Copy, Debug, Eq, PartialEq)]
374#[non_exhaustive]
375pub enum Limit {
376    /// Complete OSC frame.
377    Sequence,
378    /// Any key, including unknown keys.
379    Key,
380    /// Encoded or decoded message.
381    Message,
382    /// Encoded or decoded title.
383    Title,
384    /// Application identifier.
385    App,
386    /// Identifier length, segment length, or depth.
387    Id,
388}