reifydb_testing/testscript/
parser.rs1use std::{collections::HashSet, error, fmt};
5
6use crate::testscript::command::{Argument, Block, Command};
7
8#[derive(Debug, Clone)]
9pub struct ParseError {
10 pub message: String,
11 pub line: u32,
12 pub column: usize,
13 pub input: LocatedSpan,
14 pub code: String,
15}
16
17impl fmt::Display for ParseError {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19 write!(f, "Parse error at line {}:{}: {}", self.line, self.column, self.message)
20 }
21}
22
23impl error::Error for ParseError {}
24
25#[derive(Debug, Clone)]
26pub struct LocatedSpan {
27 column: usize,
28 line: u32,
29 line_text: String,
30}
31
32impl LocatedSpan {
33 fn new(_line_start: usize, column: usize, line: u32, line_text: String) -> Self {
34 LocatedSpan {
35 column,
36 line,
37 line_text,
38 }
39 }
40
41 pub fn location_line(&self) -> u32 {
42 self.line
43 }
44
45 pub fn get_column(&self) -> usize {
46 self.column
47 }
48
49 pub fn get_utf8_column(&self) -> usize {
50 self.column
51 }
52
53 pub fn get_line_beginning(&self) -> &[u8] {
54 self.line_text.as_bytes()
55 }
56}
57
58pub(crate) fn parse(input: &str) -> Result<Vec<Block>, ParseError> {
59 let mut parser = Parser::new(input);
60 parser.parse_blocks()
61}
62
63#[cfg(test)]
64pub(crate) fn parse_command(input: &str) -> Result<Command, ParseError> {
65 let mut parser = Parser::new(input);
66 parser.parse_command()
67}
68
69struct Parser<'a> {
70 input: &'a str,
71 pos: usize,
72 line: u32,
73 column: usize,
74 line_start_pos: usize,
75}
76
77impl<'a> Parser<'a> {
78 fn new(input: &'a str) -> Self {
79 Parser {
80 input,
81 pos: 0,
82 line: 1,
83 column: 1,
84 line_start_pos: 0,
85 }
86 }
87
88 fn current_char(&self) -> Option<char> {
89 self.input[self.pos..].chars().next()
90 }
91
92 fn peek_char(&self) -> Option<char> {
93 self.current_char()
94 }
95
96 fn peek_str(&self, n: usize) -> &str {
97 let end = (self.pos + n).min(self.input.len());
98
99 let mut safe_end = end;
100 while safe_end > self.pos && !self.input.is_char_boundary(safe_end) {
101 safe_end -= 1;
102 }
103
104 &self.input[self.pos..safe_end]
105 }
106
107 fn advance(&mut self) -> Option<char> {
108 if let Some(ch) = self.current_char() {
109 self.pos += ch.len_utf8();
110 if ch == '\n' {
111 self.line += 1;
112 self.column = 1;
113 self.line_start_pos = self.pos;
114 } else {
115 self.column += 1;
116 }
117 Some(ch)
118 } else {
119 None
120 }
121 }
122
123 fn skip_whitespace(&mut self) {
124 while let Some(ch) = self.peek_char() {
125 if ch.is_whitespace() && ch != '\n' {
126 self.advance();
127 } else {
128 break;
129 }
130 }
131 }
132
133 fn skip_line(&mut self) {
134 while let Some(ch) = self.peek_char() {
135 if ch == '\n' {
136 self.advance();
137 break;
138 }
139 self.advance();
140 }
141 }
142
143 fn is_at_end(&self) -> bool {
144 self.pos >= self.input.len()
145 }
146
147 fn error(&self, message: impl Into<String>) -> ParseError {
148 let line_end = self.input[self.line_start_pos..]
149 .find('\n')
150 .map(|i| self.line_start_pos + i)
151 .unwrap_or(self.input.len());
152 let line_text = &self.input[self.line_start_pos..line_end];
153
154 ParseError {
155 message: message.into(),
156 line: self.line,
157 column: self.column,
158 input: LocatedSpan::new(self.line_start_pos, self.column, self.line, line_text.to_string()),
159 code: format!("{:?}", line_text),
160 }
161 }
162
163 fn parse_blocks(&mut self) -> Result<Vec<Block>, ParseError> {
164 let mut blocks = Vec::new();
165
166 while !self.is_at_end() {
167 if let Some(block) = self.parse_block()? {
168 blocks.push(block);
169 }
170 }
171
172 Ok(blocks)
173 }
174
175 fn parse_block(&mut self) -> Result<Option<Block>, ParseError> {
176 let line_number = self.line;
177 let literal_start = self.pos;
178
179 let commands = self.parse_commands()?;
180
181 let literal_end = self.pos;
182 let literal = self.input[literal_start..literal_end].to_string();
183
184 if self.is_at_end() && commands.is_empty() {
185 return Ok(Some(Block {
186 literal,
187 commands,
188 line_number,
189 }));
190 }
191
192 if commands.is_empty() {
193 return Ok(None);
194 }
195
196 if !self.parse_separator()? {
197 return Err(self.error("Expected --- separator"));
198 }
199
200 self.parse_output()?;
201
202 Ok(Some(Block {
203 literal,
204 commands,
205 line_number,
206 }))
207 }
208
209 fn parse_commands(&mut self) -> Result<Vec<Command>, ParseError> {
210 let mut commands = Vec::new();
211
212 loop {
213 if self.skip_empty_or_comment_line() {
214 continue;
215 }
216
217 if self.is_at_end() {
218 break;
219 }
220
221 if self.peek_str(3) == "---" && !commands.is_empty() {
222 break;
223 }
224
225 if let Some(ch) = self.peek_char()
226 && ch.is_whitespace() && ch != '\n'
227 {
228 return Err(self.error("Command cannot start with whitespace"));
229 }
230
231 match self.parse_command() {
232 Ok(cmd) => commands.push(cmd),
233 Err(e) => {
234 if self.peek_str(3) == "---" && commands.is_empty() {
235 return Err(e);
236 }
237 return Err(e);
238 }
239 }
240 }
241
242 Ok(commands)
243 }
244
245 fn parse_command(&mut self) -> Result<Command, ParseError> {
246 let line_number = self.line;
247
248 let silent = if self.peek_char() == Some('(') {
249 self.advance();
250 self.skip_whitespace();
251 true
252 } else {
253 false
254 };
255
256 let mut tags = HashSet::new();
257 let mut prefix = None;
258
259 let saved_pos = self.pos;
260 self.skip_whitespace();
261 if let Ok(s) = self.parse_string() {
262 self.skip_whitespace();
263 if self.peek_char() == Some(':') {
264 self.advance();
265 self.skip_whitespace();
266 prefix = Some(s);
267 } else {
268 self.pos = saved_pos;
269 }
270 }
271
272 self.skip_whitespace();
273 if let Some(parsed_tags) = self.parse_taglist()? {
274 tags.extend(parsed_tags);
275 }
276 self.skip_whitespace();
277
278 let fail = if self.peek_char() == Some('!') {
279 self.advance();
280 self.skip_whitespace();
281 true
282 } else {
283 false
284 };
285
286 if self.peek_char() == Some('>') {
287 self.advance();
288 self.skip_whitespace();
289
290 let fail = if !fail && self.peek_char() == Some('!') {
291 self.advance();
292 self.skip_whitespace();
293 true
294 } else {
295 fail
296 };
297 let name = self.parse_line_continuation()?;
298 return Ok(Command {
299 name,
300 args: Vec::new(),
301 tags,
302 prefix,
303 silent,
304 fail,
305 line_number,
306 });
307 }
308
309 self.skip_whitespace();
310 let name = self.parse_string().map_err(|_| self.error("Expected command name"))?;
311
312 let mut args = Vec::new();
313 loop {
314 self.skip_whitespace();
315 if self.peek_char() == Some('[')
316 && let Some(parsed_tags) = self.parse_taglist()?
317 {
318 tags.extend(parsed_tags);
319 break;
320 }
321
322 if silent && self.peek_char() == Some(')') {
323 break;
324 }
325
326 if self.peek_char() == Some('#') || self.peek_str(2) == "//" {
327 break;
328 }
329
330 if self.peek_char() == Some('\n') || self.is_at_end() {
331 break;
332 }
333
334 let saved_pos = self.pos;
335 let saved_line = self.line;
336 let saved_column = self.column;
337 let saved_line_start = self.line_start_pos;
338 match self.parse_argument() {
339 Ok(arg) => args.push(arg),
340 Err(_) => {
341 self.pos = saved_pos;
342 self.line = saved_line;
343 self.column = saved_column;
344 self.line_start_pos = saved_line_start;
345 break;
346 }
347 }
348 }
349
350 if silent {
351 self.skip_whitespace();
352 if self.peek_char() != Some(')') {
353 return Err(self.error("Expected closing ) for silent command"));
354 }
355 self.advance();
356 }
357
358 self.skip_whitespace();
359 if self.peek_char() == Some('#') || self.peek_str(2) == "//" {
360 self.skip_line();
361 } else if self.peek_char() == Some('\n') {
362 self.advance();
363 } else if !self.is_at_end() {
364 return Err(self.error("Expected end of line"));
365 }
366
367 Ok(Command {
368 name,
369 args,
370 tags,
371 prefix,
372 silent,
373 fail,
374 line_number,
375 })
376 }
377
378 fn parse_argument(&mut self) -> Result<Argument, ParseError> {
379 let saved_pos = self.pos;
380 let saved_line = self.line;
381 let saved_column = self.column;
382 let saved_line_start = self.line_start_pos;
383
384 self.skip_whitespace();
385 if let Ok(key) = self.parse_string() {
386 if self.peek_char() == Some('=') {
387 self.advance();
388
389 let value = if matches!(self.peek_char(), Some(ch) if ch.is_whitespace())
390 || matches!(self.peek_char(), Some('[' | ')' | '#'))
391 || self.peek_char().is_none() || self.peek_str(2) == "//"
392 {
393 String::new()
394 } else {
395 match self.parse_string() {
396 Ok(v) => v,
397 Err(_) => {
398 let check_pos = self.pos;
399 let check_line = self.line;
400 let check_column = self.column;
401 let check_line_start = self.line_start_pos;
402
403 if self.parse_string().is_ok() {
404 if self.peek_char() == Some('=') {
405 self.pos = check_pos;
406 self.line = check_line;
407 self.column = check_column;
408 self.line_start_pos = check_line_start;
409 String::new()
410 } else {
411 self.pos = saved_pos;
412 self.line = saved_line;
413 self.column = saved_column;
414 self.line_start_pos = saved_line_start;
415 return Err(self.error(
416 "Expected argument value after =",
417 ));
418 }
419 } else {
420 self.pos = saved_pos;
421 self.line = saved_line;
422 self.column = saved_column;
423 self.line_start_pos = saved_line_start;
424 return Err(
425 self.error("Expected argument value after =")
426 );
427 }
428 }
429 }
430 };
431 return Ok(Argument {
432 key: Some(key),
433 value,
434 });
435 }
436
437 return Ok(Argument {
438 key: None,
439 value: key,
440 });
441 }
442
443 self.pos = saved_pos;
444 Err(self.error("Expected argument"))
445 }
446
447 fn parse_taglist(&mut self) -> Result<Option<HashSet<String>>, ParseError> {
448 if self.peek_char() != Some('[') {
449 return Ok(None);
450 }
451
452 self.advance();
453 let mut tags = HashSet::new();
454
455 loop {
456 self.skip_whitespace();
457
458 if self.peek_char() == Some(']') {
459 if tags.is_empty() {
460 return Err(self.error("Empty tag list"));
461 }
462 self.advance();
463 break;
464 }
465
466 self.skip_whitespace();
467 let tag = self.parse_string().map_err(|_| self.error("Expected tag name"))?;
468 tags.insert(tag);
469
470 self.skip_whitespace();
471 if self.peek_char() == Some(',') {
472 self.advance();
473 self.skip_whitespace();
474 } else if self.peek_char() == Some(' ') {
475 self.skip_whitespace();
476 }
477 }
478
479 Ok(Some(tags))
480 }
481
482 fn parse_string(&mut self) -> Result<String, ParseError> {
483 match self.peek_char() {
484 Some('\'') => self.parse_quoted_string('\''),
485 Some('"') => self.parse_quoted_string('"'),
486 _ => self.parse_unquoted_string(),
487 }
488 }
489
490 fn parse_unquoted_string(&mut self) -> Result<String, ParseError> {
491 let mut result = String::new();
492
493 match self.peek_char() {
494 Some(ch) if ch.is_alphanumeric() || ch == '_' => {
495 result.push(ch);
496 self.advance();
497 }
498 _ => return Err(self.error("Expected string")),
499 }
500
501 while let Some(ch) = self.peek_char() {
502 if ch.is_alphanumeric() || "_-./@".contains(ch) {
503 result.push(ch);
504 self.advance();
505 } else {
506 break;
507 }
508 }
509
510 Ok(result)
511 }
512
513 fn parse_quoted_string(&mut self, quote: char) -> Result<String, ParseError> {
514 let mut result = String::new();
515
516 if self.peek_char() != Some(quote) {
517 return Err(self.error(format!("Expected {} quote", quote)));
518 }
519 self.advance();
520
521 while let Some(ch) = self.peek_char() {
522 if ch == quote {
523 self.advance();
524 return Ok(result);
525 } else if ch == '\\' {
526 self.advance();
527 match self.peek_char() {
528 Some('\'') => {
529 result.push('\'');
530 self.advance();
531 }
532 Some('"') => {
533 result.push('"');
534 self.advance();
535 }
536 Some('\\') => {
537 result.push('\\');
538 self.advance();
539 }
540 Some('0') => {
541 result.push('\0');
542 self.advance();
543 }
544 Some('n') => {
545 result.push('\n');
546 self.advance();
547 }
548 Some('r') => {
549 result.push('\r');
550 self.advance();
551 }
552 Some('t') => {
553 result.push('\t');
554 self.advance();
555 }
556 Some('x') => {
557 self.advance();
558 let hex = self.parse_hex_digits(2, 2)?;
559 let byte = u8::from_str_radix(&hex, 16)
560 .map_err(|_| self.error("Invalid hex escape"))?;
561 result.push(char::from(byte));
562 }
563 Some('u') => {
564 self.advance();
565 if self.peek_char() != Some('{') {
566 return Err(self.error("Expected { after \\u"));
567 }
568 self.advance();
569 let hex = self.parse_hex_digits(1, 6)?;
570 if self.peek_char() != Some('}') {
571 return Err(self.error("Expected } after unicode escape"));
572 }
573 self.advance();
574 let codepoint = u32::from_str_radix(&hex, 16)
575 .map_err(|_| self.error("Invalid unicode escape"))?;
576 let ch = char::from_u32(codepoint)
577 .ok_or_else(|| self.error("Invalid unicode codepoint"))?;
578 result.push(ch);
579 }
580 _ => {
581 return Err(self.error("Invalid escape sequence"));
582 }
583 }
584 } else {
585 result.push(ch);
586 self.advance();
587 }
588 }
589
590 Err(self.error(format!("Unterminated string (missing {})", quote)))
591 }
592
593 fn parse_hex_digits(&mut self, min: usize, max: usize) -> Result<String, ParseError> {
594 let mut hex = String::new();
595 for i in 0..max {
596 match self.peek_char() {
597 Some(ch) if ch.is_ascii_hexdigit() => {
598 hex.push(ch);
599 self.advance();
600 }
601 _ => {
602 if i < min {
603 return Err(self.error(format!("Expected at least {} hex digits", min)));
604 }
605 break;
606 }
607 }
608 }
609 if hex.len() < min {
610 return Err(self.error(format!("Expected at least {} hex digits", min)));
611 }
612 Ok(hex)
613 }
614
615 fn skip_empty_or_comment_line(&mut self) -> bool {
616 let saved_pos = self.pos;
617
618 self.skip_whitespace();
619
620 if self.peek_char() == Some('#') || self.peek_str(2) == "//" {
621 self.skip_line();
622 return true;
623 }
624
625 if self.peek_char() == Some('\n') {
626 self.advance();
627 return true;
628 }
629
630 self.pos = saved_pos;
631 false
632 }
633
634 fn parse_separator(&mut self) -> Result<bool, ParseError> {
635 if self.peek_str(3) != "---" {
636 return Ok(false);
637 }
638
639 self.advance();
640 self.advance();
641 self.advance();
642
643 match self.peek_char() {
644 Some('\r') => {
645 self.advance();
646 if self.peek_char() == Some('\n') {
647 self.advance();
648 }
649 Ok(true)
650 }
651 Some('\n') => {
652 self.advance();
653 Ok(true)
654 }
655 None => Ok(true),
656 _ => Err(self.error("Separator must be followed by newline or EOF")),
657 }
658 }
659
660 fn parse_output(&mut self) -> Result<(), ParseError> {
661 if self.peek_char() == Some('\n') || self.is_at_end() {
662 if self.peek_char() == Some('\n') {
663 self.advance();
664 }
665 return Ok(());
666 }
667
668 let mut last_was_newline = false;
669 while !self.is_at_end() {
670 let ch = self.advance().unwrap();
671 if ch == '\n' {
672 if last_was_newline {
673 break;
674 }
675 last_was_newline = true;
676 } else {
677 last_was_newline = false;
678 }
679 }
680
681 Ok(())
682 }
683
684 fn parse_line_continuation(&mut self) -> Result<String, ParseError> {
685 let mut result = String::new();
686
687 loop {
688 while let Some(ch) = self.peek_char() {
689 if ch == '\n' {
690 break;
691 }
692 result.push(ch);
693 self.advance();
694 }
695
696 if result.ends_with('\\') {
697 result.pop();
698 if self.peek_char() == Some('\n') {
699 self.advance();
700 continue;
701 }
702 }
703
704 if self.peek_char() == Some('\n') {
705 self.advance();
706 }
707
708 break;
709 }
710
711 Ok(result)
712 }
713}