use crate::caps::Capabilities;
use crate::error::{Error, Field};
use crate::limits::{
MAX_APP, MAX_BODY, MAX_ID, MAX_KEY, MAX_MSG_DECODED, MAX_MSG_ENCODED, MAX_TITLE_DECODED,
MAX_TITLE_ENCODED,
};
use crate::model::{App, Id, Kind, Message, Report, State};
use crate::text::Text;
fn is_space(b: &u8) -> bool {
matches!(b, b' ' | b'\t')
}
fn trim(mut s: &[u8]) -> &[u8] {
while let [first, rest @ ..] = s {
if !is_space(first) {
break;
}
s = rest;
}
while let [rest @ .., last] = s {
if !is_space(last) {
break;
}
s = rest;
}
s
}
fn is_value_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || matches!(b, b'_' | b'.' | b',' | b'+' | b'/' | b'=' | b'-')
}
enum Pair<'a> {
Ok(&'a str, &'a str),
BadValue(&'a str, usize),
Malformed,
}
fn split_pair(part: &[u8]) -> Pair<'_> {
let Some(eq) = part.iter().position(|&b| b == b'=') else {
return Pair::Malformed;
};
let key = trim(&part[..eq]);
let value = trim(&part[eq + 1..]);
if key.is_empty() || !key.iter().all(u8::is_ascii_lowercase) {
return Pair::Malformed;
}
let Ok(key) = core::str::from_utf8(key) else {
return Pair::Malformed;
};
match core::str::from_utf8(value) {
Ok(v) if v.bytes().all(is_value_byte) => Pair::Ok(key, v),
_ => Pair::BadValue(key, value.len()),
}
}
fn parse_progress(v: &str) -> Option<u8> {
if v.is_empty() || !v.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let digits = v.trim_start_matches('0');
if digits.len() > 3 {
return None;
}
let n: u16 = digits.bytes().fold(0, |n, b| n * 10 + u16::from(b - b'0'));
(n <= 100).then_some(n as u8)
}
fn check_limit(key: &str, len: usize) -> Result<(), Error> {
let (max, field) = match key {
"app" => (MAX_APP, Field::App),
"title" => (MAX_TITLE_ENCODED, Field::Title),
"msg" => (MAX_MSG_ENCODED, Field::Msg),
"id" => (MAX_ID, Field::Id),
_ => return Ok(()),
};
if len > max {
Err(Error::TooLong(field))
} else {
Ok(())
}
}
fn decode_text<'a>(
value: Option<&'a str>,
field: Field,
max_encoded: usize,
max_decoded: usize,
) -> Result<Option<Text<'a>>, Error> {
match value {
None | Some("") => Ok(None),
Some(v) => Text::encoded(v, field, max_encoded, max_decoded).map(Some),
}
}
pub fn parse_body(body: &[u8]) -> Result<Message<'_>, Error> {
if body.len() > MAX_BODY {
return Err(Error::SequenceTooLong);
}
let mut parts = body.split(|&b| b == b':');
let mut first = parts.next();
let feature = first.is_some_and(|p| trim(p) == b"?");
if feature {
first = None;
}
let mut state = None;
let mut id = None;
let mut kind = None;
let mut progress = None;
let mut app = None;
let mut title = None;
let mut msg = None;
let mut caps = Capabilities::default();
for part in first.into_iter().chain(parts) {
if let Some(eq) = part.iter().position(|&b| b == b'=') {
if trim(&part[..eq]).len() > MAX_KEY {
return Err(Error::TooLong(Field::Key));
}
}
let (key, value) = match split_pair(part) {
Pair::Ok(k, v) => (k, v),
Pair::BadValue("id", _) if !feature => return Err(Error::InvalidId),
Pair::BadValue(k, len) if !feature => {
check_limit(k, len)?;
continue;
}
Pair::BadValue(..) | Pair::Malformed => continue,
};
if feature {
caps.apply_pair(key, value);
continue;
}
check_limit(key, value.len())?;
match key {
"state" => state = Some(value),
"id" => id = Some(Id::new(value)?),
"kind" => kind = Some(value),
"progress" => progress = Some(value),
"app" => app = Some(value),
"title" => title = Some(value),
"msg" => msg = Some(value),
_ => {}
}
}
let title = decode_text(title, Field::Title, MAX_TITLE_ENCODED, MAX_TITLE_DECODED)?;
let msg = decode_text(msg, Field::Msg, MAX_MSG_ENCODED, MAX_MSG_DECODED)?;
if feature {
return Ok(Message::Feature(caps));
}
let state = match state {
None => return Err(Error::MissingState),
Some("clear") => return Ok(Message::Clear { id }),
Some(s) => State::from_wire(s).ok_or(Error::UnknownState)?,
};
Ok(Message::Report(Report {
state,
id,
kind: kind
.filter(|_| state == State::Blocked)
.and_then(Kind::from_wire),
progress: progress
.filter(|_| state.allows_progress())
.and_then(parse_progress),
app: app.and_then(|a| App::new(a).ok()),
title,
msg,
}))
}
#[cfg(test)]
mod tests {
use super::*;
fn report(body: &str) -> Report<'_> {
match parse_body(body.as_bytes()) {
Ok(Message::Report(r)) => r,
other => panic!("{body:?}: {other:?}"),
}
}
#[test]
fn spec_example() {
let r = report(
"state=blocked:kind=permission:app=terraform:msg=QXBwbHkgMyB0byBhZGQsIDEgdG8gY2hhbmdlLCAwIHRvIGRlc3Ryb3k/",
);
assert_eq!(r.state, State::Blocked);
assert_eq!(r.kind, Some(Kind::Permission));
assert_eq!(r.app.unwrap().as_str(), "terraform");
let mut buf = [0u8; 64];
assert_eq!(
r.msg.unwrap().decode_into(&mut buf),
Some("Apply 3 to add, 1 to change, 0 to destroy?")
);
}
#[test]
fn progress_values() {
for (v, want) in [
("0", Some(0)),
("100", Some(100)),
("0042", Some(42)),
("101", None),
("4.5", None),
("", None),
("99999999999999999999", None),
] {
assert_eq!(parse_progress(v), want, "{v}");
}
}
#[test]
fn whitespace_and_malformed_pairs() {
let r = report(" state = working : bogus : Foo=1 : progress= 7 :x=a b");
assert_eq!(r.state, State::Working);
assert_eq!(r.progress, Some(7));
}
#[test]
fn feature() {
assert_eq!(
parse_body(b"?"),
Ok(Message::Feature(Capabilities::default()))
);
let Ok(Message::Feature(c)) = parse_body(b"? : states=idle,done,future : kinds=") else {
panic!()
};
assert_eq!(
c.states().collect::<heapless_vec::V<State>>().0,
[Some(State::Idle), Some(State::Done), None, None, None]
);
assert_eq!(c.kinds().count(), 0);
}
mod heapless_vec {
pub struct V<T>(pub [Option<T>; 5]);
impl<T: Copy> FromIterator<T> for V<T> {
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
let mut a = [None; 5];
for (slot, x) in a.iter_mut().zip(iter) {
*slot = Some(x);
}
V(a)
}
}
}
}