Skip to main content

osc7501_parser/
parse.rs

1use crate::{
2    Action, AppId, Capabilities, EncodedText, Error, Event, Kind, Limit, MAX_APP, MAX_DEPTH,
3    MAX_ID, MAX_KEY, MAX_MESSAGE, MAX_MESSAGE_ENCODED, MAX_SEGMENT, MAX_SEQUENCE, MAX_TITLE,
4    MAX_TITLE_ENCODED, Progress, RecordId, Report, State, base64,
5};
6
7const PREFIX: &[u8] = b"\x1b]7501;";
8
9/// Parses exactly one OSC 7501 sequence terminated by BEL or ESC-backslash.
10///
11/// `Ok(None)` means an ignored report (missing/unknown state or invalid id).
12/// Malformed pairs and unknown keys are skipped, and duplicates use the last
13/// syntactically valid pair. Every report text pair and hard cap is checked,
14/// including pairs overridden later. Capability replies ignore unknown pairs
15/// within the frame cap. Nothing is returned until validation completes.
16///
17/// # Errors
18/// Returns [`Error`] for invalid or oversized framing, or a rejected report.
19pub fn parse_sequence(input: &[u8]) -> Result<Option<Event<'_>>, Error> {
20    if input.len() > MAX_SEQUENCE {
21        return Err(Error::Limit(Limit::Sequence));
22    }
23    let rest = input.strip_prefix(PREFIX).ok_or(Error::Framing)?;
24    let body = rest
25        .strip_suffix(b"\x1b\\")
26        .or_else(|| rest.strip_suffix(b"\x07"))
27        .ok_or(Error::Framing)?;
28    if body.iter().any(|&byte| matches!(byte, 0x1b | 0x07)) {
29        return Err(Error::Framing);
30    }
31    parse(body)
32}
33
34/// Parses a body after a terminal dispatcher has consumed `OSC 7501;` and ST.
35///
36/// Uses the longer two-byte ST when accounting for the whole-sequence cap.
37/// Use [`parse_sequence`] to accept a BEL frame at the exact 4096-byte boundary.
38///
39/// # Errors
40/// Returns [`Error`] for an oversized body or a report that violates a hard cap
41/// or text rule.
42pub fn parse_body(body: &[u8]) -> Result<Option<Event<'_>>, Error> {
43    if body.len() > MAX_SEQUENCE - PREFIX.len() - 2 {
44        return Err(Error::Limit(Limit::Sequence));
45    }
46    parse(body)
47}
48
49fn trim(mut bytes: &[u8]) -> &[u8] {
50    while bytes.first().is_some_and(u8::is_ascii_whitespace) {
51        bytes = &bytes[1..];
52    }
53    while bytes.last().is_some_and(u8::is_ascii_whitespace) {
54        bytes = &bytes[..bytes.len() - 1];
55    }
56    bytes
57}
58
59fn value_byte(byte: u8) -> bool {
60    byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'.' | b',' | b'+' | b'/' | b'=' | b'-')
61}
62
63fn pairs(
64    body: &[u8],
65    check_report_limits: bool,
66) -> impl Iterator<Item = Result<(&[u8], &[u8]), Error>> {
67    body.split(|&byte| byte == b':').filter_map(move |pair| {
68        let equals = pair.iter().position(|&byte| byte == b'=')?;
69        let key = trim(&pair[..equals]);
70        let value = trim(&pair[equals + 1..]);
71        if check_report_limits {
72            if key.len() > MAX_KEY {
73                return Some(Err(Error::Limit(Limit::Key)));
74            }
75            let limit = match key {
76                b"msg" if value.len() > MAX_MESSAGE_ENCODED => Some(Limit::Message),
77                b"title" if value.len() > MAX_TITLE_ENCODED => Some(Limit::Title),
78                b"app" if value.len() > MAX_APP => Some(Limit::App),
79                b"id"
80                    if value.len() > MAX_ID
81                        || value.split(|&byte| byte == b'/').count() > MAX_DEPTH
82                        || value
83                            .split(|&byte| byte == b'/')
84                            .any(|segment| segment.len() > MAX_SEGMENT) =>
85                {
86                    Some(Limit::Id)
87                }
88                _ => None,
89            };
90            if let Some(limit) = limit {
91                return Some(Err(Error::Limit(limit)));
92            }
93        }
94        if key.is_empty()
95            || !key.iter().all(u8::is_ascii_lowercase)
96            || (key != b"id"
97                && !value
98                    .iter()
99                    .copied()
100                    .fold(true, |valid, byte| valid & value_byte(byte)))
101        {
102            return None;
103        }
104        Some(Ok((key, value)))
105    })
106}
107
108fn ascii(bytes: &[u8]) -> Result<&str, Error> {
109    core::str::from_utf8(bytes).map_err(|_| Error::InvalidUtf8)
110}
111
112fn text(value: &[u8], cap: usize, limit: Limit) -> Result<EncodedText<'_>, Error> {
113    let mut scratch = [0; MAX_MESSAGE];
114    let length = base64::decode(value, &mut scratch[..cap]).map_err(|error| match error {
115        Error::BufferTooSmall => Error::Limit(limit),
116        error => error,
117    })?;
118    let decoded = core::str::from_utf8(&scratch[..length]).map_err(|_| Error::InvalidUtf8)?;
119    let controls = if decoded.is_ascii() {
120        decoded.bytes().fold(0_u8, |found, byte| {
121            found | u8::from(byte < 32 || byte == 127)
122        }) != 0
123    } else {
124        decoded.chars().any(char::is_control)
125    };
126    if controls {
127        return Err(Error::ControlCharacter);
128    }
129    Ok(EncodedText {
130        encoded: ascii(value)?,
131        decoded_len: length,
132    })
133}
134
135fn progress(value: &[u8]) -> Option<Progress> {
136    if value.is_empty() {
137        return None;
138    }
139    let mut percentage: u8 = 0;
140    for &byte in value {
141        if !byte.is_ascii_digit() {
142            return None;
143        }
144        percentage = percentage.checked_mul(10)?.checked_add(byte - b'0')?;
145        if percentage > 100 {
146            return None;
147        }
148    }
149    Progress::new(percentage).ok()
150}
151
152fn parse(body: &[u8]) -> Result<Option<Event<'_>>, Error> {
153    let body = trim(body);
154    if body == b"?" {
155        return Ok(Some(Event::Query));
156    }
157    if let Some(body) = body
158        .strip_prefix(b"?")
159        .map(trim)
160        .and_then(|body| body.strip_prefix(b":"))
161    {
162        let mut capabilities = Capabilities {
163            states: None,
164            kinds: None,
165        };
166        for pair in pairs(body, false) {
167            let (key, value) = pair?;
168            match key {
169                b"states" => capabilities.states = Some(ascii(value)?),
170                b"kinds" => capabilities.kinds = Some(ascii(value)?),
171                _ => {}
172            }
173        }
174        return Ok(Some(Event::Capabilities(capabilities)));
175    }
176    let mut action = None;
177    let mut raw_id = None;
178    let mut report = Report {
179        action: Action::Clear,
180        id: None,
181        kind: None,
182        progress: None,
183        app: None,
184        title: None,
185        message: None,
186    };
187    for pair in pairs(body, true) {
188        let (key, value) = pair?;
189        match key {
190            b"state" => {
191                action = if value == b"clear" {
192                    Some(Action::Clear)
193                } else {
194                    State::parse(ascii(value)?).map(Action::Set)
195                }
196            }
197            b"id" => raw_id = Some(value),
198            b"kind" => report.kind = Kind::parse(ascii(value)?),
199            b"progress" => report.progress = progress(value),
200            b"app" => report.app = AppId::new(ascii(value)?).ok(),
201            b"title" => report.title = Some(text(value, MAX_TITLE, Limit::Title)?),
202            b"msg" => report.message = Some(text(value, MAX_MESSAGE, Limit::Message)?),
203            _ => {}
204        }
205    }
206    let Some(action) = action else {
207        return Ok(None);
208    };
209    if let Some(id) = raw_id {
210        let Some(id) = core::str::from_utf8(id)
211            .ok()
212            .and_then(|value| RecordId::new(value).ok())
213        else {
214            return Ok(None);
215        };
216        report.id = Some(id);
217    }
218    report.action = action;
219    if action != Action::Set(State::Blocked) {
220        report.kind = None;
221    }
222    if !matches!(action, Action::Set(State::Working | State::Blocked)) {
223        report.progress = None;
224    }
225    Ok(Some(Event::Report(report)))
226}