1use std::fmt;
25use std::io;
26use std::marker::PhantomData;
27use std::path::Path;
28
29pub const MAX_DEPTH: usize = 8;
32
33pub trait Grammar {
35 type Kind: Copy;
37
38 const KINDS: &'static str;
40
41 fn kind(word: &str) -> Result<Self::Kind, String>;
46
47 fn shape(kind: Self::Kind) -> Shape;
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum Shape {
54 Text,
56 Set {
58 bare_file: bool,
60 },
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum Mode {
66 Plain,
68 Exact,
70 Every,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct Text<'a> {
78 pub text: &'a str,
80 pub at: usize,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct Rule<'a, K> {
87 pub parts: Vec<Part<'a, K>>,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct Part<'a, K> {
94 pub at: usize,
96 pub negated: bool,
98 pub kind: K,
100 pub value: Value<'a, K>,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
106pub enum Value<'a, K> {
107 Text(Text<'a>),
109 Set {
111 mode: Mode,
113 set: Set<'a, K>,
115 tail: Text<'a>,
117 },
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
122pub enum Set<'a, K> {
123 Rule(Rule<'a, K>),
125 File(SetFile<'a>),
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub struct SetFile<'a> {
132 pub path: Text<'a>,
134}
135
136impl SetFile<'_> {
137 pub fn read(&self, dir: Option<&Path>) -> io::Result<String> {
143 let path = Path::new(self.path.text);
144 std::fs::read_to_string(dir.map_or_else(|| path.to_path_buf(), |dir| dir.join(path)))
146 }
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct SyntaxError {
152 rule: String,
153 at: usize,
154 message: String,
155}
156
157impl SyntaxError {
158 pub fn new(rule: &str, at: usize, message: impl Into<String>) -> SyntaxError {
160 SyntaxError { rule: rule.to_owned(), at, message: message.into() }
161 }
162
163 pub fn column(&self) -> usize {
165 self.at + 1
166 }
167
168 pub fn message(&self) -> &str {
170 &self.message
171 }
172}
173
174impl fmt::Display for SyntaxError {
175 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176 write!(f, "bad rule {:?}: {} at column {}", self.rule, self.message, self.column())
177 }
178}
179
180impl std::error::Error for SyntaxError {}
181
182impl<'a, K: Copy> Rule<'a, K> {
183 pub fn parse<G: Grammar<Kind = K>>(rule: &'a str) -> Result<Rule<'a, K>, SyntaxError> {
189 let mut parser = Parser::<G> { src: rule, pos: 0, depth: 0, grammar: PhantomData };
190 let parsed = parser.rule(false)?;
191 debug_assert_eq!(parser.peek(), None, "{rule:?}");
193 Ok(parsed)
194 }
195}
196
197pub fn parts(rule: &str) -> Vec<&str> {
201 let mut parts = Vec::new();
202 let mut rest = rule;
203 loop {
204 let len = text_len(rest, false);
205 parts.push(rest[..len].trim());
206 match rest.get(len + 1..) {
207 Some(next) => rest = next,
208 None => return parts,
209 }
210 }
211}
212
213struct Parser<'a, G> {
214 src: &'a str,
215 pos: usize,
216 depth: usize,
218 grammar: PhantomData<G>,
219}
220
221impl<'a, G: Grammar> Parser<'a, G> {
222 fn rest(&self) -> &'a str {
223 &self.src[self.pos..]
224 }
225
226 fn skip_blanks(&mut self) {
227 self.pos = self.src.len() - self.rest().trim_start().len();
228 }
229
230 fn peek(&mut self) -> Option<char> {
232 self.skip_blanks();
233 self.rest().chars().next()
234 }
235
236 fn eat(&mut self, c: char) -> bool {
237 let found = self.peek() == Some(c);
238 if found {
239 self.pos += c.len_utf8();
240 }
241 found
242 }
243
244 fn error(&self, message: impl Into<String>) -> SyntaxError {
245 SyntaxError::new(self.src, self.pos, message)
246 }
247
248 fn rule(&mut self, nested: bool) -> Result<Rule<'a, G::Kind>, SyntaxError> {
250 let mut parts = Vec::new();
251 loop {
252 self.skip_blanks();
253 let at = self.pos;
254 let negated = self.eat('!');
255 parts.push(self.part(at, negated, nested)?);
256 if !self.eat('&') {
258 return Ok(Rule { parts });
259 }
260 }
261 }
262
263 fn part(&mut self, at: usize, negated: bool, nested: bool) -> Result<Part<'a, G::Kind>, SyntaxError> {
265 self.skip_blanks();
266 let rest = self.rest();
267 let word = kind_word(rest);
268 if word == 0 || !rest[word..].starts_with(':') {
269 return Err(self.error(format!("expected KIND:VALUE, KIND one of {}", G::KINDS)));
270 }
271 let kind = G::kind(&rest[..word]).map_err(|message| self.error(message))?;
272 self.pos += word + 1;
273 let value = match G::shape(kind) {
274 Shape::Text => Value::Text(self.text(nested)?),
275 Shape::Set { bare_file } => self.set_or_text(nested, bare_file)?,
276 };
277 Ok(Part { at, negated, kind, value })
278 }
279
280 fn set_or_text(&mut self, nested: bool, bare_file: bool) -> Result<Value<'a, G::Kind>, SyntaxError> {
282 self.skip_blanks();
283 let rest = self.rest();
284 let (mode, after) = match rest.as_bytes().first() {
285 Some(b'=') => (Mode::Exact, &rest[1..]),
286 Some(b'*') => (Mode::Every, &rest[1..]),
287 _ => (Mode::Plain, rest),
288 };
289 if after.starts_with('{') {
290 self.pos += rest.len() - after.len() + 1;
291 if self.depth >= MAX_DEPTH {
292 return Err(self.error(format!("bodies are nested deeper than {MAX_DEPTH} levels")));
293 }
294 self.depth += 1;
295 let set = if self.eat('@') { Set::File(SetFile { path: self.text(true)? }) } else { Set::Rule(self.rule(true)?) };
296 self.depth -= 1;
297 if !self.eat('}') {
298 return Err(self.error("expected `}`"));
299 }
300 return Ok(Value::Set { mode, set, tail: self.tail(nested) });
301 }
302 if bare_file && after.starts_with('@') {
303 self.pos += rest.len() - after.len() + 1;
304 let path = self.text(nested)?;
305 return Ok(Value::Set { mode, set: Set::File(SetFile { path }), tail: Text { text: "", at: self.pos } });
306 }
307 Ok(Value::Text(self.text(nested)?))
308 }
309
310 fn text(&mut self, nested: bool) -> Result<Text<'a>, SyntaxError> {
312 let text = self.tail(nested);
313 if text.text.is_empty() {
314 return Err(self.error("empty value"));
315 }
316 Ok(text)
317 }
318
319 fn tail(&mut self, nested: bool) -> Text<'a> {
321 self.skip_blanks();
322 let at = self.pos;
323 let len = text_len(self.rest(), nested);
324 self.pos += len;
325 Text { text: self.src[at..self.pos].trim_end(), at }
326 }
327}
328
329fn kind_word(s: &str) -> usize {
331 s.find(|c: char| !c.is_ascii_alphabetic()).unwrap_or(s.len())
332}
333
334fn joins(rest: &str) -> bool {
337 let rest = rest.trim_start();
338 let rest = rest.strip_prefix('!').unwrap_or(rest).trim_start();
339 let word = kind_word(rest);
340 word > 0 && rest[word..].starts_with(':')
341}
342
343fn text_len(s: &str, nested: bool) -> usize {
346 let mut depth = 0usize;
347 let mut chars = s.char_indices();
348 while let Some((i, c)) = chars.next() {
349 match c {
350 '\\' => {
351 chars.next();
352 }
353 '{' | '(' => depth += 1,
354 '}' | ')' if depth > 0 => depth -= 1,
355 '}' if nested => return i,
356 '&' if depth == 0 && joins(&s[i + 1..]) => return i,
357 _ => {}
358 }
359 }
360 s.len()
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366
367 struct Test;
370
371 impl Grammar for Test {
372 type Kind = &'static str;
373 const KINDS: &'static str = "text, set, bare";
374
375 fn kind(word: &str) -> Result<&'static str, String> {
376 ["text", "set", "bare"].into_iter().find(|kind| *kind == word).ok_or_else(|| format!("unknown kind {word:?}"))
377 }
378
379 fn shape(kind: &'static str) -> Shape {
380 match kind {
381 "text" => Shape::Text,
382 "set" => Shape::Set { bare_file: false },
383 _ => Shape::Set { bare_file: true },
384 }
385 }
386 }
387
388 fn shape(rule: &str) -> String {
390 fn of(rule: &Rule<'_, &str>) -> String {
391 let parts = rule.parts.iter().map(|part| {
392 let value = match &part.value {
393 Value::Text(text) => format!("'{}'", text.text),
394 Value::Set { mode, set, tail } => {
395 let mode = match mode {
396 Mode::Plain => "",
397 Mode::Exact => "=",
398 Mode::Every => "*",
399 };
400 let set = match set {
401 Set::Rule(rule) => format!("{{{}}}", of(rule)),
402 Set::File(file) => format!("@'{}'", file.path.text),
403 };
404 if tail.text.is_empty() { format!("{mode}{set}") } else { format!("{mode}{set}+'{}'", tail.text) }
405 }
406 };
407 format!("{}{}:{value}", if part.negated { "!" } else { "" }, part.kind)
408 });
409 parts.collect::<Vec<_>>().join(" & ")
410 }
411 match Rule::parse::<Test>(rule) {
412 Ok(parsed) => of(&parsed),
413 Err(e) => format!("error: {e}"),
414 }
415 }
416
417 #[test]
418 fn an_ampersand_joins_only_ahead_of_a_kind() {
419 assert_eq!(shape("text:a & !text:b&text:c"), "text:'a' & !text:'b' & text:'c'");
420 assert_eq!(shape("text:a&b"), "text:'a&b'");
421 assert_eq!(shape("text:&a/b&->f & text:N&&&a/b&#x"), "text:'&a/b&->f' & text:'N&&&a/b&#x'");
422 assert_eq!(shape(r"text:a\&text:b"), r"text:'a\&text:b'");
423 assert_eq!(shape("text:a&! text:b"), "text:'a' & !text:'b'");
424 assert_eq!(shape("text:(a&text:b) & text:c"), "text:'(a&text:b)' & text:'c'");
426 assert_eq!(shape(r"text:a\{ & text:b"), r"text:'a\{' & text:'b'");
427 assert_eq!(shape("text:^a{2,3}$ & text:b"), "text:'^a{2,3}$' & text:'b'");
428 assert_eq!(shape("text:a & "), "text:'a &'");
430 assert_eq!(parts("set:{text:a & text:b} & !text:c"), ["set:{text:a & text:b}", "!text:c"]);
431 assert_eq!(parts("text:a&b"), ["text:a&b"]);
432 assert_eq!(parts("text:a & !text:b"), ["text:a", "!text:b"]);
433 }
434
435 #[test]
436 fn sets_nest_rules_and_files() {
437 assert_eq!(shape("set:{text:a & !text:b} & text:c"), "set:{text:'a' & !text:'b'} & text:'c'");
438 assert_eq!(shape("set:={set:*{text:a}}"), "set:={set:*{text:'a'}}");
439 assert_eq!(shape("set:{ @ dir/list.txt }"), "set:@'dir/list.txt'");
440 assert_eq!(shape("set:{text:a}[x=1] & text:b"), "set:{text:'a'}+'[x=1]' & text:'b'");
441 assert_eq!(shape("set:@ohos:net"), "set:'@ohos:net'");
443 assert_eq!(shape("set:=name"), "set:'=name'");
444 assert_eq!(shape("bare:=@list.txt"), "bare:=@'list.txt'");
445 assert_eq!(shape("bare:@list.txt[x=1] & text:a"), "bare:@'list.txt[x=1]' & text:'a'");
446 assert_eq!(shape("text:{a}"), "text:'{a}'");
448 assert_eq!(shape("set:a{2}"), "set:'a{2}'");
449 assert_eq!(shape("set:{text:a{2}}"), "set:{text:'a{2}'}");
451 assert_eq!(shape("set:{text:a}b}"), "set:{text:'a'}+'b}'");
453 assert_eq!(shape("text:a}"), "text:'a}'");
454 }
455
456 #[test]
457 fn errors_say_what_and_where() {
458 for (rule, message) in [
459 ("bogus", "bad rule \"bogus\": expected KIND:VALUE, KIND one of text, set, bare at column 1"),
460 ("text:a & other:b", "bad rule \"text:a & other:b\": unknown kind \"other\" at column 10"),
461 ("text:", "bad rule \"text:\": empty value at column 6"),
462 ("set:{text:a", "bad rule \"set:{text:a\": expected `}` at column 12"),
463 ("set:{@}", "bad rule \"set:{@}\": empty value at column 7"),
464 ] {
465 assert_eq!(shape(rule), format!("error: {message}"), "{rule}");
466 }
467 let e = Rule::parse::<Test>("text:").unwrap_err();
468 assert_eq!((e.column(), e.message()), (6, "empty value"));
469 }
470
471 #[test]
472 fn sets_nest_up_to_the_limit() {
473 let nest = |n: usize| format!("{}text:a{}", "set:{".repeat(n), "}".repeat(n));
474 assert!(Rule::parse::<Test>(&nest(MAX_DEPTH)).is_ok());
475 for n in [MAX_DEPTH + 1, 100_000] {
477 assert!(shape(&nest(n)).contains(&format!("nested deeper than {MAX_DEPTH} levels")), "{n}");
478 }
479 let wide = vec![nest(MAX_DEPTH); 50].join(" & ");
481 assert!(Rule::parse::<Test>(&wide).is_ok());
482 }
483
484 #[test]
485 fn a_set_file_reads_relative_to_its_directory() {
486 let dir = std::env::temp_dir();
487 let name = format!("asc-query-set-{}.txt", std::process::id());
488 std::fs::write(dir.join(&name), "La;->m()V\n").unwrap();
489 let rule = format!("set:{{@{name}}}");
490 let parsed = Rule::parse::<Test>(&rule).unwrap();
491 let Value::Set { set: Set::File(file), .. } = &parsed.parts[0].value else { panic!("a file set") };
492 assert_eq!(file.read(Some(&dir)).unwrap(), "La;->m()V\n");
493 assert!(file.read(Some(Path::new("/nonexistent"))).is_err());
494 std::fs::remove_file(dir.join(&name)).unwrap();
495 }
496}