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
9pub 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
34pub 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}