1use std::ops::RangeInclusive;
19use std::str::FromStr;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct Arg {
24 pub text: String,
27 pub quoted: bool,
29}
30
31impl Arg {
32 pub fn is_empty(&self) -> bool {
34 !self.quoted && self.text.is_empty()
35 }
36
37 pub fn int<T: FromStr>(&self) -> Option<T> {
40 if self.text.is_empty() || !self.text.bytes().all(|b| b.is_ascii_digit()) {
41 return None;
42 }
43 self.text.parse().ok()
44 }
45
46 pub fn hex(&self) -> Option<u64> {
48 let t = self.text.trim_start_matches("0x").trim_start_matches("0X");
49 if t.is_empty() || t.len() > 16 {
50 return None;
51 }
52 u64::from_str_radix(t, 16).ok()
53 }
54
55 pub fn str(&self) -> Option<&str> {
57 self.quoted.then_some(self.text.as_str())
58 }
59
60 pub fn text(&self) -> Option<&str> {
62 (!self.is_empty()).then_some(self.text.as_str())
63 }
64
65 pub fn group(&self) -> Option<Vec<Arg>> {
68 if self.quoted {
69 return None;
70 }
71 let inner = self.text.strip_prefix('(')?;
72 let inner = inner.strip_suffix(')').unwrap_or(inner);
73 Some(split(inner))
74 }
75}
76
77pub fn split(s: &str) -> Vec<Arg> {
84 let s = s.trim();
85 if s.is_empty() {
86 return Vec::new();
87 }
88 let mut out = Vec::new();
89 let mut cur = String::new();
90 let mut quoted = false;
91 let mut in_quote = false;
92 let mut depth = 0usize;
93 let mut chars = s.chars().peekable();
94 while let Some(c) = chars.next() {
95 if in_quote {
96 match c {
97 '"' => {
98 in_quote = false;
99 if depth > 0 {
100 cur.push('"');
101 }
102 }
103 '\\' => match chars.peek().copied() {
104 Some(_) => {
105 let mut look = chars.clone();
106 let hex: String = look.by_ref().take(2).collect();
107 match (depth, u8::from_str_radix(&hex, 16)) {
108 (0, Ok(b))
109 if hex.len() == 2 && hex.bytes().all(|b| b.is_ascii_hexdigit()) =>
110 {
111 cur.push(b as char);
112 chars = look;
113 }
114 _ => cur.push('\\'),
115 }
116 }
117 None => cur.push('\\'),
118 },
119 _ => cur.push(c),
120 }
121 continue;
122 }
123 match c {
124 '"' => {
125 in_quote = true;
126 if depth > 0 {
127 cur.push('"');
128 } else {
129 quoted = true;
130 cur.retain(|c| !c.is_whitespace());
132 }
133 }
134 '(' => {
135 depth += 1;
136 cur.push(c);
137 }
138 ')' => {
139 depth = depth.saturating_sub(1);
140 cur.push(c);
141 }
142 ',' if depth == 0 => {
143 out.push(finish(&mut cur, quoted));
144 quoted = false;
145 }
146 _ if quoted && depth == 0 => {} _ => cur.push(c),
148 }
149 }
150 out.push(finish(&mut cur, quoted));
151 out
152}
153
154fn finish(cur: &mut String, quoted: bool) -> Arg {
155 let text = std::mem::take(cur);
156 Arg {
157 text: if quoted {
158 text
159 } else {
160 text.trim().to_string()
161 },
162 quoted,
163 }
164}
165
166pub fn strip_prefix<'a>(line: &'a str, prefix: &str) -> Option<&'a str> {
170 let line = line.trim();
171 let head = line.get(..prefix.len())?;
172 if !head.eq_ignore_ascii_case(prefix) {
173 return None;
174 }
175 let rest = line[prefix.len()..].trim_start().strip_prefix(':')?;
176 Some(rest.trim())
177}
178
179pub fn parse_ranges(s: &str) -> Option<Vec<RangeInclusive<u32>>> {
182 let s = s.trim();
183 let s = s.strip_prefix('(').unwrap_or(s);
184 let s = s.strip_suffix(')').unwrap_or(s).trim();
185 if s.is_empty() {
186 return Some(Vec::new());
187 }
188 s.split(',')
189 .map(|part| {
190 let part = part.trim();
191 let (a, b) = part.split_once('-').unwrap_or((part, part));
192 let a: u32 = a.trim().parse().ok()?;
193 let b: u32 = b.trim().parse().ok()?;
194 (a <= b).then_some(a..=b)
195 })
196 .collect()
197}
198
199pub fn parse_values(s: &str) -> Option<Vec<u32>> {
202 let mut out = Vec::new();
203 for r in parse_ranges(s)? {
204 if r.end() - r.start() >= 1024 {
205 return None;
206 }
207 out.extend(r);
208 }
209 Some(out)
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 fn texts(s: &str) -> Vec<(String, bool)> {
217 split(s).into_iter().map(|a| (a.text, a.quoted)).collect()
218 }
219
220 #[test]
221 fn splitting() {
222 assert!(split("").is_empty());
223 assert_eq!(texts(","), vec![("".into(), false), ("".into(), false)]);
224 assert_eq!(
225 texts(r#" 1 , "a,\22b\22\5C" ,, "" "#),
226 vec![
227 ("1".into(), false),
228 (r#"a,"b"\"#.into(), true),
229 ("".into(), false),
230 ("".into(), true)
231 ]
232 );
233 assert_eq!(texts(r#""\41\4""#), vec![("A\\4".into(), true)]);
234 assert_eq!(texts(r#""open, end"#), vec![("open, end".into(), true)]);
235 let g = split(r#"(2,"A,B","x",7),,(0-4)"#);
236 assert_eq!(g.len(), 3);
237 let inner = g[0].group().unwrap();
238 assert_eq!(inner[1].str(), Some("A,B"));
239 assert_eq!(inner[3].int::<u8>(), Some(7));
240 assert_eq!(parse_values(&g[2].text), Some(vec![0, 1, 2, 3, 4]));
241 }
242
243 #[test]
245 fn backslash_is_not_a_quote_escape() {
246 assert_eq!(
247 texts(r#""C:\",1,"x""#),
248 vec![
249 ("C:\\".into(), true),
250 ("1".into(), false),
251 ("x".into(), true)
252 ]
253 );
254 assert_eq!(texts(r#""a\22b""#), vec![("a\"b".into(), true)]);
255 }
256
257 #[test]
258 fn values_and_prefix() {
259 let a = split(r#"0011,"8BE3",a3fb477,"12""#);
260 assert_eq!(a[0].int::<u8>(), Some(11));
261 assert_eq!(a[1].hex(), Some(0x8be3));
262 assert_eq!(a[2].hex(), Some(0xa3fb477));
263 assert_eq!(a[3].int::<u8>(), Some(12));
264 assert_eq!(a[1].int::<u8>(), None);
265 assert_eq!(strip_prefix("\r\n+creg : 1\r\n", "+CREG"), Some("1"));
266 assert_eq!(strip_prefix("+CREGX: 1", "+CREG"), None);
267 assert_eq!(strip_prefix("+CR", "+CREG"), None);
268 assert_eq!(parse_ranges("(0-1,3)"), Some(vec![0..=1, 3..=3]));
269 assert_eq!(parse_ranges("1-0"), None);
270 assert_eq!(parse_values("0-100000"), None);
271 }
272}