use std::ops::RangeInclusive;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Arg {
pub text: String,
pub quoted: bool,
}
impl Arg {
pub fn is_empty(&self) -> bool {
!self.quoted && self.text.is_empty()
}
pub fn int<T: FromStr>(&self) -> Option<T> {
if self.text.is_empty() || !self.text.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
self.text.parse().ok()
}
pub fn hex(&self) -> Option<u64> {
let t = self.text.trim_start_matches("0x").trim_start_matches("0X");
if t.is_empty() || t.len() > 16 {
return None;
}
u64::from_str_radix(t, 16).ok()
}
pub fn str(&self) -> Option<&str> {
self.quoted.then_some(self.text.as_str())
}
pub fn text(&self) -> Option<&str> {
(!self.is_empty()).then_some(self.text.as_str())
}
pub fn group(&self) -> Option<Vec<Arg>> {
if self.quoted {
return None;
}
let inner = self.text.strip_prefix('(')?;
let inner = inner.strip_suffix(')').unwrap_or(inner);
Some(split(inner))
}
}
pub fn split(s: &str) -> Vec<Arg> {
let s = s.trim();
if s.is_empty() {
return Vec::new();
}
let mut out = Vec::new();
let mut cur = String::new();
let mut quoted = false;
let mut in_quote = false;
let mut depth = 0usize;
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if in_quote {
match c {
'"' => {
in_quote = false;
if depth > 0 {
cur.push('"');
}
}
'\\' => match chars.peek().copied() {
Some(_) => {
let mut look = chars.clone();
let hex: String = look.by_ref().take(2).collect();
match (depth, u8::from_str_radix(&hex, 16)) {
(0, Ok(b))
if hex.len() == 2 && hex.bytes().all(|b| b.is_ascii_hexdigit()) =>
{
cur.push(b as char);
chars = look;
}
_ => cur.push('\\'),
}
}
None => cur.push('\\'),
},
_ => cur.push(c),
}
continue;
}
match c {
'"' => {
in_quote = true;
if depth > 0 {
cur.push('"');
} else {
quoted = true;
cur.retain(|c| !c.is_whitespace());
}
}
'(' => {
depth += 1;
cur.push(c);
}
')' => {
depth = depth.saturating_sub(1);
cur.push(c);
}
',' if depth == 0 => {
out.push(finish(&mut cur, quoted));
quoted = false;
}
_ if quoted && depth == 0 => {} _ => cur.push(c),
}
}
out.push(finish(&mut cur, quoted));
out
}
fn finish(cur: &mut String, quoted: bool) -> Arg {
let text = std::mem::take(cur);
Arg {
text: if quoted {
text
} else {
text.trim().to_string()
},
quoted,
}
}
pub fn strip_prefix<'a>(line: &'a str, prefix: &str) -> Option<&'a str> {
let line = line.trim();
let head = line.get(..prefix.len())?;
if !head.eq_ignore_ascii_case(prefix) {
return None;
}
let rest = line[prefix.len()..].trim_start().strip_prefix(':')?;
Some(rest.trim())
}
pub fn parse_ranges(s: &str) -> Option<Vec<RangeInclusive<u32>>> {
let s = s.trim();
let s = s.strip_prefix('(').unwrap_or(s);
let s = s.strip_suffix(')').unwrap_or(s).trim();
if s.is_empty() {
return Some(Vec::new());
}
s.split(',')
.map(|part| {
let part = part.trim();
let (a, b) = part.split_once('-').unwrap_or((part, part));
let a: u32 = a.trim().parse().ok()?;
let b: u32 = b.trim().parse().ok()?;
(a <= b).then_some(a..=b)
})
.collect()
}
pub fn parse_values(s: &str) -> Option<Vec<u32>> {
let mut out = Vec::new();
for r in parse_ranges(s)? {
if r.end() - r.start() >= 1024 {
return None;
}
out.extend(r);
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn texts(s: &str) -> Vec<(String, bool)> {
split(s).into_iter().map(|a| (a.text, a.quoted)).collect()
}
#[test]
fn splitting() {
assert!(split("").is_empty());
assert_eq!(texts(","), vec![("".into(), false), ("".into(), false)]);
assert_eq!(
texts(r#" 1 , "a,\22b\22\5C" ,, "" "#),
vec![
("1".into(), false),
(r#"a,"b"\"#.into(), true),
("".into(), false),
("".into(), true)
]
);
assert_eq!(texts(r#""\41\4""#), vec![("A\\4".into(), true)]);
assert_eq!(texts(r#""open, end"#), vec![("open, end".into(), true)]);
let g = split(r#"(2,"A,B","x",7),,(0-4)"#);
assert_eq!(g.len(), 3);
let inner = g[0].group().unwrap();
assert_eq!(inner[1].str(), Some("A,B"));
assert_eq!(inner[3].int::<u8>(), Some(7));
assert_eq!(parse_values(&g[2].text), Some(vec![0, 1, 2, 3, 4]));
}
#[test]
fn backslash_is_not_a_quote_escape() {
assert_eq!(
texts(r#""C:\",1,"x""#),
vec![
("C:\\".into(), true),
("1".into(), false),
("x".into(), true)
]
);
assert_eq!(texts(r#""a\22b""#), vec![("a\"b".into(), true)]);
}
#[test]
fn values_and_prefix() {
let a = split(r#"0011,"8BE3",a3fb477,"12""#);
assert_eq!(a[0].int::<u8>(), Some(11));
assert_eq!(a[1].hex(), Some(0x8be3));
assert_eq!(a[2].hex(), Some(0xa3fb477));
assert_eq!(a[3].int::<u8>(), Some(12));
assert_eq!(a[1].int::<u8>(), None);
assert_eq!(strip_prefix("\r\n+creg : 1\r\n", "+CREG"), Some("1"));
assert_eq!(strip_prefix("+CREGX: 1", "+CREG"), None);
assert_eq!(strip_prefix("+CR", "+CREG"), None);
assert_eq!(parse_ranges("(0-1,3)"), Some(vec![0..=1, 3..=3]));
assert_eq!(parse_ranges("1-0"), None);
assert_eq!(parse_values("0-100000"), None);
}
}