1use serde_json::{Map, Number, Value};
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct YamlError {
42 pub line: usize,
43 pub col: usize,
44 pub msg: String,
45}
46
47impl std::fmt::Display for YamlError {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 write!(f, "line {}, column {}: {}", self.line, self.col, self.msg)
50 }
51}
52
53impl std::error::Error for YamlError {}
54
55pub fn parse(src: &str) -> Result<Value, YamlError> {
57 let src = src.strip_prefix('\u{feff}').unwrap_or(src);
58 let raw: Vec<&str> = src
59 .split('\n')
60 .map(|l| l.strip_suffix('\r').unwrap_or(l))
61 .collect();
62 let lines = logical_lines(&raw)?;
63 let mut p = Parser { raw, lines, pos: 0 };
64 let Some(first) = p.peek().cloned() else {
65 return Ok(Value::Null);
66 };
67 let v = p.parse_node(first.indent, first.indent)?;
68 if let Some(l) = p.peek() {
69 return Err(err(l, 0, "unexpected content after the document root"));
70 }
71 Ok(v)
72}
73
74pub fn parse_inline(src: &str) -> Result<Value, YamlError> {
79 let line = Line {
80 no: 1,
81 indent: 0,
82 text: src.trim().to_string(),
83 };
84 let (v, rest, _plain) = parse_inline_value(&line, &line.text, 0)?;
85 if !rest.trim().is_empty() {
86 return Err(err(
87 &line,
88 line.text.len() - rest.len(),
89 "trailing characters after the value",
90 ));
91 }
92 Ok(v)
93}
94
95#[derive(Debug, Clone)]
102struct Line {
103 no: usize,
104 indent: usize,
105 text: String,
106}
107
108fn err(l: &Line, col_in_text: usize, msg: impl Into<String>) -> YamlError {
109 YamlError {
110 line: l.no,
111 col: l.indent + col_in_text + 1,
112 msg: msg.into(),
113 }
114}
115
116fn logical_lines(raw: &[&str]) -> Result<Vec<Line>, YamlError> {
122 let mut out = Vec::new();
123 let mut seen_doc_start = false;
124 for (i, &line) in raw.iter().enumerate() {
125 let no = i + 1;
126 let trimmed = line.trim_start_matches(' ');
127 let indent = line.len() - trimmed.len();
128 if trimmed.trim().is_empty() || trimmed.starts_with('#') {
129 continue;
130 }
131 if indent == 0 {
132 if trimmed.starts_with('%') {
133 continue; }
135 if trimmed == "---" || trimmed.starts_with("--- ") {
136 if seen_doc_start || !out.is_empty() {
137 return Err(YamlError {
138 line: no,
139 col: 1,
140 msg: "multiple YAML documents are not supported (one document per file)"
141 .into(),
142 });
143 }
144 seen_doc_start = true;
145 let rest = trimmed[3..].trim();
146 if rest.is_empty() {
147 continue;
148 }
149 out.push(Line {
151 no,
152 indent: 4,
153 text: strip_comment(rest),
154 });
155 continue;
156 }
157 if trimmed == "..." {
158 break; }
160 }
161 out.push(Line {
162 no,
163 indent,
164 text: strip_comment(trimmed),
165 });
166 }
167 Ok(out)
168}
169
170fn strip_comment(text: &str) -> String {
173 let bytes = text.as_bytes();
174 let mut i = 0;
175 let mut in_single = false;
176 let mut in_double = false;
177 while i < bytes.len() {
178 let b = bytes[i];
179 if in_double {
180 if b == b'\\' {
181 i += 2;
182 continue;
183 }
184 if b == b'"' {
185 in_double = false;
186 }
187 } else if in_single {
188 if b == b'\'' {
189 if i + 1 < bytes.len() && bytes[i + 1] == b'\'' {
190 i += 2; continue;
192 }
193 in_single = false;
194 }
195 } else if b == b'"' && quote_opens(bytes, i) {
196 in_double = true;
197 } else if b == b'\'' && quote_opens(bytes, i) {
198 in_single = true;
199 } else if b == b'#' && (i == 0 || bytes[i - 1] == b' ' || bytes[i - 1] == b'\t') {
200 return text[..i].trim_end().to_string();
201 }
202 i += 1;
203 }
204 text.trim_end().to_string()
205}
206
207fn quote_opens(bytes: &[u8], i: usize) -> bool {
211 let mut j = i;
212 while j > 0 && bytes[j - 1] == b' ' {
213 j -= 1;
214 }
215 j == 0 || matches!(bytes[j - 1], b':' | b'-' | b'[' | b'{' | b',')
216}
217
218struct Parser<'s> {
223 raw: Vec<&'s str>,
226 lines: Vec<Line>,
227 pos: usize,
228}
229
230impl Parser<'_> {
231 fn peek(&self) -> Option<&Line> {
232 self.lines.get(self.pos)
233 }
234
235 fn parse_node(&mut self, indent: usize, parent_indent: usize) -> Result<Value, YamlError> {
240 let Some(line) = self.peek().cloned() else {
241 return Ok(Value::Null);
242 };
243 check_tab(&line)?;
244 if is_seq_item(&line.text) {
245 return self.parse_sequence(indent);
246 }
247 if let Some(header) = line
248 .text
249 .strip_prefix('|')
250 .or_else(|| line.text.strip_prefix('>'))
251 {
252 let literal = line.text.starts_with('|');
253 self.pos += 1;
254 return self.parse_block_scalar(&line, parent_indent, literal, header, 0);
255 }
256 if split_key(&line)?.is_some() {
257 return self.parse_mapping(indent);
258 }
259 self.pos += 1;
262 let text = self.gather_flow(&line, &line.text, indent)?;
263 let (v, rest, plain) = parse_inline_value(&line, &text, 0)?;
264 if !rest.trim().is_empty() {
265 return Err(err(
266 &line,
267 text.len() - rest.len(),
268 "trailing characters after the value",
269 ));
270 }
271 if plain {
272 return self.fold_plain_continuation(v, indent);
273 }
274 Ok(v)
275 }
276
277 fn parse_sequence(&mut self, indent: usize) -> Result<Value, YamlError> {
278 let mut items = Vec::new();
279 while let Some(line) = self.peek().cloned() {
280 check_tab(&line)?;
281 if line.indent < indent {
282 break;
283 }
284 if line.indent > indent {
285 return Err(err(&line, 0, "unexpected indentation inside a sequence"));
286 }
287 if !is_seq_item(&line.text) {
288 break; }
290 let rest = line.text[1..].trim_start_matches(' ');
291 if rest.is_empty() {
292 self.pos += 1;
294 match self.peek() {
295 Some(next) if next.indent > indent => {
296 let ni = next.indent;
297 items.push(self.parse_node(ni, indent)?);
298 }
299 _ => items.push(Value::Null),
300 }
301 continue;
302 }
303 let inner_indent = indent + (line.text.len() - rest.len());
307 self.lines[self.pos] = Line {
308 no: line.no,
309 indent: inner_indent,
310 text: rest.to_string(),
311 };
312 items.push(self.parse_node(inner_indent, indent)?);
313 }
314 Ok(Value::Array(items))
315 }
316
317 fn parse_mapping(&mut self, indent: usize) -> Result<Value, YamlError> {
318 let mut map = Map::new();
319 while let Some(line) = self.peek().cloned() {
320 check_tab(&line)?;
321 if line.indent < indent {
322 break;
323 }
324 if line.indent > indent {
325 return Err(err(&line, 0, "unexpected indentation inside a mapping"));
326 }
327 if is_seq_item(&line.text) {
328 return Err(err(
329 &line,
330 0,
331 "a sequence item is not allowed here (expected a `key: value`)",
332 ));
333 }
334 let Some((key, rest)) = split_key(&line)? else {
335 return Err(err(&line, 0, "expected a `key: value` mapping entry"));
336 };
337 if map.contains_key(&key) {
338 return Err(err(&line, 0, format!("duplicate mapping key {key:?}")));
339 }
340 let rest = rest.to_string();
341 self.pos += 1;
342 let value = self.parse_mapping_value(&line, indent, &rest)?;
343 map.insert(key, value);
344 }
345 Ok(Value::Object(map))
346 }
347
348 fn parse_mapping_value(
351 &mut self,
352 line: &Line,
353 indent: usize,
354 rest: &str,
355 ) -> Result<Value, YamlError> {
356 let rest_col = line.text.len() - rest.len();
357 if rest.is_empty() {
358 return match self.peek() {
361 Some(next) if next.indent > indent => {
362 let ni = next.indent;
363 self.parse_node(ni, indent)
364 }
365 Some(next) if next.indent == indent && is_seq_item(&next.text) => {
366 self.parse_sequence(indent)
367 }
368 _ => Ok(Value::Null),
369 };
370 }
371 if let Some(header) = rest.strip_prefix('|').or_else(|| rest.strip_prefix('>')) {
372 let literal = rest.starts_with('|');
373 return self.parse_block_scalar(line, indent, literal, header, rest_col);
374 }
375 let text = self.gather_flow(line, rest, indent)?;
376 let (v, tail, plain) = parse_inline_value(line, &text, rest_col)?;
377 if !tail.trim().is_empty() {
378 let col = rest_col + (text.len() - tail.len());
379 return Err(err(line, col, "trailing characters after the value"));
380 }
381 if plain {
382 return self.fold_plain_continuation(v, indent);
383 }
384 Ok(v)
385 }
386
387 fn gather_flow(
390 &mut self,
391 line: &Line,
392 start: &str,
393 indent: usize,
394 ) -> Result<String, YamlError> {
395 let t = start.trim_start();
396 if !(t.starts_with('[') || t.starts_with('{')) {
397 return Ok(start.to_string());
398 }
399 let _ = indent;
402 let mut buf = start.to_string();
403 while !flow_balanced(&buf) {
404 match self.peek() {
405 Some(next) => {
406 buf.push(' ');
407 buf.push_str(&next.text);
408 self.pos += 1;
409 }
410 None => {
411 return Err(err(
412 line,
413 line.text.len() - start.len(),
414 "unterminated flow collection (missing `]` or `}`)",
415 ));
416 }
417 }
418 }
419 Ok(buf)
420 }
421
422 fn fold_plain_continuation(&mut self, v: Value, indent: usize) -> Result<Value, YamlError> {
425 let mut pieces: Vec<String> = Vec::new();
426 while let Some(next) = self.peek() {
427 if next.indent <= indent || is_seq_item(&next.text) || split_key(next)?.is_some() {
428 break;
429 }
430 pieces.push(next.text.trim().to_string());
431 self.pos += 1;
432 }
433 if pieces.is_empty() {
434 return Ok(v);
435 }
436 let mut s = match v {
438 Value::String(s) => s,
439 Value::Null => String::new(),
440 other => other.to_string(),
441 };
442 for p in pieces {
443 if !s.is_empty() {
444 s.push(' ');
445 }
446 s.push_str(&p);
447 }
448 Ok(Value::String(s))
449 }
450
451 fn parse_block_scalar(
456 &mut self,
457 line: &Line,
458 parent_indent: usize,
459 literal: bool,
460 header: &str,
461 header_col: usize,
462 ) -> Result<Value, YamlError> {
463 let mut chomp = Chomp::Clip;
464 let mut explicit_indent: Option<usize> = None;
465 for ch in header.trim().chars() {
466 match ch {
467 '-' => chomp = Chomp::Strip,
468 '+' => chomp = Chomp::Keep,
469 d @ '1'..='9' => {
470 explicit_indent = Some(parent_indent + (d as usize - '0' as usize))
471 }
472 _ => {
473 return Err(err(
474 line,
475 header_col,
476 format!("bad block scalar header {:?}", header.trim()),
477 ));
478 }
479 }
480 }
481 let mut body: Vec<&str> = Vec::new();
483 let mut idx = line.no; while idx < self.raw.len() {
485 let r = self.raw[idx];
486 let ind = r.len() - r.trim_start_matches(' ').len();
487 if r.trim().is_empty() {
488 body.push("");
489 idx += 1;
490 continue;
491 }
492 if ind <= parent_indent {
493 break;
494 }
495 body.push(r);
496 idx += 1;
497 }
498 while self.peek().is_some_and(|l| l.no <= idx) {
500 self.pos += 1;
501 }
502 let block_indent = explicit_indent.or_else(|| {
504 body.iter()
505 .find(|l| !l.trim().is_empty())
506 .map(|l| l.len() - l.trim_start_matches(' ').len())
507 });
508 let Some(block_indent) = block_indent else {
509 return Ok(Value::String(match chomp {
511 Chomp::Keep => "\n".repeat(body.len()),
512 _ => String::new(),
513 }));
514 };
515 let mut content: Vec<String> = Vec::with_capacity(body.len());
516 for (k, l) in body.iter().enumerate() {
517 if l.trim().is_empty() {
518 content.push(String::new());
519 continue;
520 }
521 let ind = l.len() - l.trim_start_matches(' ').len();
522 if ind < block_indent {
523 let bad = Line {
524 no: line.no + 1 + k,
525 indent: ind,
526 text: l.trim().to_string(),
527 };
528 return Err(err(
529 &bad,
530 0,
531 "block scalar line is indented less than the block",
532 ));
533 }
534 content.push(l[block_indent..].to_string());
535 }
536 let trailing = content.iter().rev().take_while(|l| l.is_empty()).count();
538 let text_lines = &content[..content.len() - trailing];
539 let mut text = if literal {
540 text_lines.join("\n")
541 } else {
542 fold_lines(text_lines)
543 };
544 match chomp {
545 Chomp::Strip => {}
546 Chomp::Clip => {
547 if !text.is_empty() {
548 text.push('\n');
549 }
550 }
551 Chomp::Keep => {
552 if !text.is_empty() {
553 text.push('\n');
554 }
555 text.push_str(&"\n".repeat(trailing));
556 }
557 }
558 Ok(Value::String(text))
559 }
560}
561
562fn fold_lines(lines: &[String]) -> String {
566 let mut out = String::new();
567 for (i, l) in lines.iter().enumerate() {
568 if i == 0 {
569 out.push_str(l);
570 continue;
571 }
572 let prev = &lines[i - 1];
573 if l.is_empty() {
574 out.push('\n');
575 } else if prev.is_empty() {
576 out.push_str(l);
577 } else if l.starts_with(' ') || prev.starts_with(' ') {
578 out.push('\n');
579 out.push_str(l);
580 } else {
581 out.push(' ');
582 out.push_str(l);
583 }
584 }
585 out
586}
587
588#[derive(Clone, Copy)]
589enum Chomp {
590 Clip,
591 Strip,
592 Keep,
593}
594
595fn check_tab(line: &Line) -> Result<(), YamlError> {
599 if line.text.starts_with('\t') {
600 return Err(err(
601 line,
602 0,
603 "tab indentation is not allowed in YAML (use spaces)",
604 ));
605 }
606 Ok(())
607}
608
609fn is_seq_item(text: &str) -> bool {
611 text == "-" || text.starts_with("- ")
612}
613
614fn flow_balanced(s: &str) -> bool {
618 let bytes = s.as_bytes();
619 let mut depth: i32 = 0;
620 let mut i = 0;
621 let mut in_single = false;
622 let mut in_double = false;
623 while i < bytes.len() {
624 let b = bytes[i];
625 if in_double {
626 if b == b'\\' {
627 i += 2;
628 continue;
629 }
630 if b == b'"' {
631 in_double = false;
632 }
633 } else if in_single {
634 if b == b'\'' {
635 if i + 1 < bytes.len() && bytes[i + 1] == b'\'' {
636 i += 2;
637 continue;
638 }
639 in_single = false;
640 }
641 } else {
642 match b {
643 b'"' => in_double = true,
644 b'\'' => in_single = true,
645 b'[' | b'{' => depth += 1,
646 b']' | b'}' => depth -= 1,
647 _ => {}
648 }
649 }
650 i += 1;
651 }
652 depth <= 0
653}
654
655fn split_key(line: &Line) -> Result<Option<(String, &str)>, YamlError> {
659 let text = line.text.as_str();
660 if text.starts_with('[') || text.starts_with('{') || is_seq_item(text) {
661 return Ok(None);
662 }
663 if text.starts_with("? ") || text == "?" {
664 return Err(err(
665 line,
666 0,
667 "complex mapping keys (`? `) are not supported",
668 ));
669 }
670 if text.starts_with('"') || text.starts_with('\'') {
671 let (key, after) = parse_quoted(line, text, 0)?;
672 let after_trim = after.trim_start();
673 if let Some(rest) = after_trim.strip_prefix(':')
674 && (rest.is_empty() || rest.starts_with(' '))
675 {
676 return Ok(Some((key, rest.trim_start_matches(' '))));
677 }
678 return Ok(None);
679 }
680 let bytes = text.as_bytes();
681 for i in 0..bytes.len() {
682 if bytes[i] == b':' && (i + 1 == bytes.len() || bytes[i + 1] == b' ') {
683 let key = text[..i].trim_end();
684 if key.is_empty() {
685 return Err(err(line, i, "empty mapping key"));
686 }
687 if key.starts_with('&') || key.starts_with('*') || key.starts_with('!') {
688 return Err(err(line, 0, "anchors, aliases and tags are not supported"));
689 }
690 if key == "<<" {
691 return Err(err(line, 0, "merge keys (`<<`) are not supported"));
692 }
693 let rest = text[i + 1..].trim_start_matches(' ');
694 return Ok(Some((key.to_string(), rest)));
695 }
696 }
697 Ok(None)
698}
699
700fn parse_inline_value<'a>(
709 line: &Line,
710 s: &'a str,
711 col: usize,
712) -> Result<(Value, &'a str, bool), YamlError> {
713 let t = s.trim_start_matches(' ');
714 let col = col + (s.len() - t.len());
715 if t.is_empty() {
716 return Ok((Value::Null, t, false));
717 }
718 match t.as_bytes()[0] {
719 b'"' | b'\'' => {
720 let (v, rest) = parse_quoted(line, t, col)?;
721 Ok((Value::String(v), rest, false))
722 }
723 b'[' => parse_flow_seq(line, t, col).map(|(v, r)| (v, r, false)),
724 b'{' => parse_flow_map(line, t, col).map(|(v, r)| (v, r, false)),
725 b'&' | b'*' | b'!' => Err(err(
726 line,
727 col,
728 "anchors, aliases and tags are not supported",
729 )),
730 b'@' | b'`' => Err(err(
731 line,
732 col,
733 "reserved indicator at the start of a plain scalar",
734 )),
735 _ => Ok((type_plain(t, line, col)?, "", true)),
736 }
737}
738
739fn take_flow_plain(s: &str, in_map: bool) -> (&str, &str) {
742 let bytes = s.as_bytes();
743 let mut i = 0;
744 while i < bytes.len() {
745 let b = bytes[i];
746 if b == b',' || b == b']' || b == b'}' {
747 break;
748 }
749 if in_map
750 && b == b':'
751 && (i + 1 == bytes.len() || matches!(bytes[i + 1], b' ' | b',' | b'}' | b']'))
752 {
753 break;
754 }
755 i += 1;
756 }
757 (s[..i].trim(), &s[i..])
758}
759
760fn parse_flow_seq<'a>(line: &Line, s: &'a str, col: usize) -> Result<(Value, &'a str), YamlError> {
761 let mut rest = &s[1..];
762 let mut items = Vec::new();
763 loop {
764 rest = rest.trim_start();
765 if rest.is_empty() {
766 return Err(err(line, col, "unterminated flow sequence (missing `]`)"));
767 }
768 if let Some(r) = rest.strip_prefix(']') {
769 return Ok((Value::Array(items), r));
770 }
771 let (v, r) = parse_flow_element(line, rest, col + (s.len() - rest.len()), false)?;
772 items.push(v);
773 rest = r.trim_start();
774 if let Some(r) = rest.strip_prefix(',') {
775 rest = r;
776 } else if !rest.starts_with(']') {
777 return Err(err(
778 line,
779 col + (s.len() - rest.len()),
780 "expected `,` or `]` in a flow sequence",
781 ));
782 }
783 }
784}
785
786fn parse_flow_map<'a>(line: &Line, s: &'a str, col: usize) -> Result<(Value, &'a str), YamlError> {
787 let mut rest = &s[1..];
788 let mut map = Map::new();
789 loop {
790 rest = rest.trim_start();
791 if rest.is_empty() {
792 return Err(err(line, col, "unterminated flow mapping (missing `}`)"));
793 }
794 if let Some(r) = rest.strip_prefix('}') {
795 return Ok((Value::Object(map), r));
796 }
797 let key_col = col + (s.len() - rest.len());
798 let (key, r) = if rest.starts_with('"') || rest.starts_with('\'') {
799 parse_quoted(line, rest, key_col)?
800 } else {
801 let (k, r) = take_flow_plain(rest, true);
802 if k.is_empty() {
803 return Err(err(line, key_col, "empty key in a flow mapping"));
804 }
805 (k.to_string(), r)
806 };
807 if map.contains_key(&key) {
808 return Err(err(line, key_col, format!("duplicate mapping key {key:?}")));
809 }
810 rest = r.trim_start();
811 let value = if let Some(r) = rest.strip_prefix(':') {
812 let (v, r2) =
813 parse_flow_element(line, r.trim_start(), col + (s.len() - r.len()), true)?;
814 rest = r2;
815 v
816 } else {
817 Value::Null };
819 map.insert(key, value);
820 rest = rest.trim_start();
821 if let Some(r) = rest.strip_prefix(',') {
822 rest = r;
823 } else if !rest.starts_with('}') {
824 return Err(err(
825 line,
826 col + (s.len() - rest.len()),
827 "expected `,` or `}` in a flow mapping",
828 ));
829 }
830 }
831}
832
833fn parse_flow_element<'a>(
836 line: &Line,
837 s: &'a str,
838 col: usize,
839 in_map: bool,
840) -> Result<(Value, &'a str), YamlError> {
841 match s.as_bytes().first() {
842 None => Ok((Value::Null, s)),
843 Some(b'[') => parse_flow_seq(line, s, col),
844 Some(b'{') => parse_flow_map(line, s, col),
845 Some(b'"') | Some(b'\'') => {
846 let (v, r) = parse_quoted(line, s, col)?;
847 Ok((Value::String(v), r))
848 }
849 Some(b'&') | Some(b'*') | Some(b'!') => Err(err(
850 line,
851 col,
852 "anchors, aliases and tags are not supported",
853 )),
854 Some(_) => {
855 let (raw, r) = take_flow_plain(s, in_map);
856 Ok((type_plain(raw, line, col)?, r))
857 }
858 }
859}
860
861fn parse_quoted<'a>(line: &Line, s: &'a str, col: usize) -> Result<(String, &'a str), YamlError> {
864 let quote = s.as_bytes()[0];
865 let mut out = String::new();
866 let mut chars = s[1..].char_indices().peekable();
867 while let Some((i, c)) = chars.next() {
868 let abs = 1 + i;
869 if quote == b'\'' {
870 if c == '\'' {
871 if let Some(&(_, '\'')) = chars.peek() {
872 chars.next();
873 out.push('\'');
874 continue;
875 }
876 return Ok((out, &s[abs + 1..]));
877 }
878 out.push(c);
879 continue;
880 }
881 if c == '"' {
882 return Ok((out, &s[abs + 1..]));
883 }
884 if c == '\\' {
885 let Some((_, e)) = chars.next() else {
886 return Err(err(
887 line,
888 col + abs,
889 "unterminated escape in a double-quoted scalar",
890 ));
891 };
892 match e {
893 'n' => out.push('\n'),
894 't' => out.push('\t'),
895 'r' => out.push('\r'),
896 '0' => out.push('\0'),
897 'a' => out.push('\u{7}'),
898 'b' => out.push('\u{8}'),
899 'e' => out.push('\u{1b}'),
900 'f' => out.push('\u{c}'),
901 'v' => out.push('\u{b}'),
902 ' ' => out.push(' '),
903 '/' => out.push('/'),
904 '"' => out.push('"'),
905 '\\' => out.push('\\'),
906 'x' | 'u' | 'U' => {
907 let n = match e {
908 'x' => 2,
909 'u' => 4,
910 _ => 8,
911 };
912 let mut code = 0u32;
913 for _ in 0..n {
914 let Some((_, h)) = chars.next() else {
915 return Err(err(line, col + abs, "truncated \\x/\\u escape"));
916 };
917 let Some(d) = h.to_digit(16) else {
918 return Err(err(line, col + abs, "bad hex digit in \\x/\\u escape"));
919 };
920 code = code * 16 + d;
921 }
922 let Some(ch) = char::from_u32(code) else {
923 return Err(err(line, col + abs, "escape is not a valid unicode scalar"));
924 };
925 out.push(ch);
926 }
927 other => {
928 return Err(err(
929 line,
930 col + abs,
931 format!("unknown escape `\\{other}` in a double-quoted scalar"),
932 ));
933 }
934 }
935 continue;
936 }
937 out.push(c);
938 }
939 Err(err(
940 line,
941 col,
942 "unterminated quoted scalar (multi-line quoted scalars are not supported)",
943 ))
944}
945
946fn type_plain(raw: &str, line: &Line, col: usize) -> Result<Value, YamlError> {
948 let t = raw.trim();
949 if t.is_empty() || t == "~" || t == "null" || t == "Null" || t == "NULL" {
950 return Ok(Value::Null);
951 }
952 match t {
953 "true" | "True" | "TRUE" => return Ok(Value::Bool(true)),
954 "false" | "False" | "FALSE" => return Ok(Value::Bool(false)),
955 _ => {}
956 }
957 if let Some(n) = parse_int(t) {
958 return Ok(n);
959 }
960 if is_float(t) {
961 return match t.parse::<f64>() {
962 Ok(f) if f.is_finite() => Number::from_f64(f)
963 .map(Value::Number)
964 .ok_or_else(|| err(line, col, "float not representable")),
965 _ => Err(err(line, col, format!("float {t:?} is out of range"))),
966 };
967 }
968 let lower = t.trim_start_matches(['-', '+']).to_ascii_lowercase();
969 if lower == ".inf" || lower == ".nan" {
970 return Err(err(
971 line,
972 col,
973 format!("non-finite float {t:?} cannot be represented in JSON"),
974 ));
975 }
976 Ok(Value::String(t.to_string()))
977}
978
979fn parse_int(t: &str) -> Option<Value> {
981 let (neg, body) = match t.strip_prefix('-') {
982 Some(b) => (true, b),
983 None => (false, t.strip_prefix('+').unwrap_or(t)),
984 };
985 let (radix, digits) = if let Some(h) = body.strip_prefix("0x") {
986 (16, h)
987 } else if let Some(o) = body.strip_prefix("0o") {
988 (8, o)
989 } else {
990 (10, body)
991 };
992 if digits.is_empty() || !digits.chars().all(|c| c.is_digit(radix)) {
993 return None;
994 }
995 let mag = u64::from_str_radix(digits, radix).ok()?;
996 if neg {
997 if mag <= i64::MAX as u64 + 1 {
998 return Some(Value::from(-(mag as i128) as i64));
999 }
1000 return None;
1001 }
1002 Some(Value::from(mag))
1003}
1004
1005fn is_float(t: &str) -> bool {
1008 let s = t.strip_prefix(['-', '+']).unwrap_or(t);
1009 let (mant, exp) = match s.find(['e', 'E']) {
1010 Some(i) => (&s[..i], Some(&s[i + 1..])),
1011 None => (s, None),
1012 };
1013 if let Some(e) = exp {
1014 let e = e.strip_prefix(['-', '+']).unwrap_or(e);
1015 if e.is_empty() || !e.bytes().all(|b| b.is_ascii_digit()) {
1016 return false;
1017 }
1018 }
1019 let (int, frac) = match mant.split_once('.') {
1020 Some((i, f)) => (i, Some(f)),
1021 None => (mant, None),
1022 };
1023 let int_ok = int.bytes().all(|b| b.is_ascii_digit());
1024 match frac {
1025 Some(f) => {
1026 int_ok && f.bytes().all(|b| b.is_ascii_digit()) && !(int.is_empty() && f.is_empty())
1027 }
1028 None => exp.is_some() && !int.is_empty() && int_ok,
1029 }
1030}
1031
1032#[cfg(test)]
1033mod tests {
1034 use super::*;
1035 use serde_json::json;
1036
1037 fn y(src: &str) -> Value {
1038 parse(src).unwrap_or_else(|e| panic!("yaml parse failed: {e}\n---\n{src}"))
1039 }
1040
1041 fn e(src: &str) -> YamlError {
1042 parse(src).expect_err("expected a parse error")
1043 }
1044
1045 #[test]
1046 fn empty_and_scalar_documents() {
1047 assert_eq!(y(""), Value::Null);
1048 assert_eq!(y("# only a comment\n\n"), Value::Null);
1049 assert_eq!(y("42"), json!(42));
1050 assert_eq!(y("--- hello\n"), json!("hello"));
1051 assert_eq!(
1052 y("---\nkey: v\n...\nignored: after end\n"),
1053 json!({"key": "v"})
1054 );
1055 }
1056
1057 #[test]
1058 fn nested_mappings_and_sequences() {
1059 let v = y(r#"
1060model: gpt-5
1061limits:
1062 max_steps: 5
1063 max_depth: 2
1064mcp_servers:
1065 - name: fs
1066 endpoint: https://fs.example/mcp
1067 tags:
1068 "*": [untrusted_input, egress]
1069 - name: q
1070 endpoint: "https://q.example/mcp"
1071subscribe:
1072- queue://inbox
1073- queue://dead-letter
1074"#);
1075 assert_eq!(
1076 v,
1077 json!({
1078 "model": "gpt-5",
1079 "limits": {"max_steps": 5, "max_depth": 2},
1080 "mcp_servers": [
1081 {"name": "fs", "endpoint": "https://fs.example/mcp",
1082 "tags": {"*": ["untrusted_input", "egress"]}},
1083 {"name": "q", "endpoint": "https://q.example/mcp"}
1084 ],
1085 "subscribe": ["queue://inbox", "queue://dead-letter"]
1086 })
1087 );
1088 }
1089
1090 #[test]
1091 fn nested_sequences_and_null_items() {
1092 assert_eq!(y("- - a\n - b\n- - c\n"), json!([["a", "b"], ["c"]]));
1093 assert_eq!(y("-\n x: 1\n- \n-\n"), json!([{"x": 1}, null, null]));
1094 assert_eq!(
1095 y("- x: 1\n y: 2\n- z: 3\n"),
1096 json!([{"x": 1, "y": 2}, {"z": 3}])
1097 );
1098 assert_eq!(y("k:\n - a\n - b\n"), json!({"k": ["a", "b"]}));
1100 }
1101
1102 #[test]
1103 fn scalar_typing_follows_the_core_schema() {
1104 let v = y(r#"
1105n: null
1106t: ~
1107e:
1108b1: true
1109b2: False
1110i1: 42
1111i2: -7
1112i3: 0x1F
1113i4: 0o17
1114f1: 1.5
1115f2: -2.
1116f3: .5
1117f4: 1e3
1118s1: yes
1119s2: on
1120s3: 12:30
1121s4: 1_000
1122s5: hello world
1123s6: 3 apples
1124big: 18446744073709551615
1125"#);
1126 assert_eq!(v["n"], Value::Null);
1127 assert_eq!(v["t"], Value::Null);
1128 assert_eq!(v["e"], Value::Null);
1129 assert_eq!(v["b1"], json!(true));
1130 assert_eq!(v["b2"], json!(false));
1131 assert_eq!(v["i1"], json!(42));
1132 assert_eq!(v["i2"], json!(-7));
1133 assert_eq!(v["i3"], json!(31));
1134 assert_eq!(v["i4"], json!(15));
1135 assert_eq!(v["f1"], json!(1.5));
1136 assert_eq!(v["f2"], json!(-2.0));
1137 assert_eq!(v["f3"], json!(0.5));
1138 assert_eq!(v["f4"], json!(1000.0));
1139 assert_eq!(v["s1"], json!("yes"));
1140 assert_eq!(v["s2"], json!("on"));
1141 assert_eq!(v["s3"], json!("12:30"));
1142 assert_eq!(v["s4"], json!("1_000"));
1143 assert_eq!(v["s5"], json!("hello world"));
1144 assert_eq!(v["s6"], json!("3 apples"));
1145 assert_eq!(v["big"], json!(18446744073709551615u64));
1146 }
1147
1148 #[test]
1149 fn quoted_scalars_and_escapes() {
1150 let v = y(r##"
1151a: "line\nbreak \"q\" \u00e9 \x41"
1152b: 'it''s # not a comment'
1153c: "#not a comment"
1154d: plain # a comment
1155e: "true"
1156f: '42'
1157"##);
1158 assert_eq!(v["a"], json!("line\nbreak \"q\" é A"));
1159 assert_eq!(v["b"], json!("it's # not a comment"));
1160 assert_eq!(v["c"], json!("#not a comment"));
1161 assert_eq!(v["d"], json!("plain"));
1162 assert_eq!(v["e"], json!("true"));
1163 assert_eq!(v["f"], json!("42"));
1164 }
1165
1166 #[test]
1167 fn urls_and_colons_do_not_split_keys() {
1168 let v = y("endpoint: https://host:8443/mcp\ntime: 12:30\nk: a:b\n");
1169 assert_eq!(v["endpoint"], json!("https://host:8443/mcp"));
1170 assert_eq!(v["time"], json!("12:30"));
1171 assert_eq!(v["k"], json!("a:b"));
1172 }
1173
1174 #[test]
1175 fn flow_collections_inline_and_multiline() {
1176 let v = y(r#"
1177a: [1, "two", three, [4, 5], {x: y}]
1178b: {k: v, n: 2, list: [a, b], "quoted key": 'q', empty}
1179c: [
1180 one,
1181 two,
1182]
1183d: []
1184e: {}
1185"#);
1186 assert_eq!(v["a"], json!([1, "two", "three", [4, 5], {"x": "y"}]));
1187 assert_eq!(
1188 v["b"],
1189 json!({"k": "v", "n": 2, "list": ["a", "b"], "quoted key": "q", "empty": null})
1190 );
1191 assert_eq!(v["c"], json!(["one", "two"]));
1192 assert_eq!(v["d"], json!([]));
1193 assert_eq!(v["e"], json!({}));
1194 }
1195
1196 #[test]
1197 fn block_scalars_literal_and_folded() {
1198 let v = y("lit: |\n line one\n line two\n\n after blank\nnext: 1\n");
1199 assert_eq!(v["lit"], json!("line one\nline two\n\nafter blank\n"));
1200 assert_eq!(v["next"], json!(1));
1201
1202 let v = y("fold: >\n a b\n c d\n\n e\n");
1203 assert_eq!(v["fold"], json!("a b c d\ne\n"));
1204
1205 let v = y("s: |-\n x\n y\n\n\nk: |+\n z\n\n\nafter: 2\n");
1207 assert_eq!(v["s"], json!("x\ny"));
1208 assert_eq!(v["k"], json!("z\n\n\n"));
1209 assert_eq!(v["after"], json!(2));
1210
1211 let v = y("ind: |2\n keep two extra\n one extra\n");
1212 assert_eq!(v["ind"], json!(" keep two extra\n one extra\n"));
1213
1214 let v = y("script: |\n echo hi # not a comment\n\n # nor this\n");
1216 assert_eq!(
1217 v["script"],
1218 json!("echo hi # not a comment\n\n# nor this\n")
1219 );
1220
1221 let v = y("- |\n first\n- second\n");
1223 assert_eq!(v, json!(["first\n", "second"]));
1224
1225 assert_eq!(y("e: |\nnext: 1\n"), json!({"e": "", "next": 1}));
1227 }
1228
1229 #[test]
1230 fn plain_scalars_fold_over_continuation_lines() {
1231 let v =
1232 y("instruction: read the report\n and summarize it\n in three bullets\nmodel: m\n");
1233 assert_eq!(
1234 v["instruction"],
1235 json!("read the report and summarize it in three bullets")
1236 );
1237 assert_eq!(v["model"], json!("m"));
1238 }
1239
1240 #[test]
1241 fn comments_bom_crlf_and_directives() {
1242 let src =
1243 "\u{feff}%YAML 1.2\r\n---\r\n# comment\r\nkey: value # trailing\r\n\r\nother: 2\r\n";
1244 assert_eq!(y(src), json!({"key": "value", "other": 2}));
1245 }
1246
1247 #[test]
1248 fn errors_name_line_and_column() {
1249 let x = e("a: 1\n\tb: 2\n");
1250 assert_eq!(x.line, 2);
1251 assert!(x.msg.contains("tab"), "{x}");
1252
1253 let x = e("a: 1\na: 2\n");
1254 assert_eq!(x.line, 2);
1255 assert!(x.msg.contains("duplicate"), "{x}");
1256
1257 let x = e("a: &anchor 1\n");
1258 assert!(x.msg.contains("anchors"), "{x}");
1259 let x = e("a: *alias\n");
1260 assert!(x.msg.contains("anchors"), "{x}");
1261 let x = e("a: !!str 1\n");
1262 assert!(x.msg.contains("tags") || x.msg.contains("anchors"), "{x}");
1263 let x = e("<<: {a: 1}\n");
1264 assert!(x.msg.contains("merge"), "{x}");
1265 let x = e("? complex\n: key\n");
1266 assert!(x.msg.contains("complex"), "{x}");
1267
1268 let x = e("a: 1\n---\nb: 2\n");
1269 assert!(x.msg.contains("multiple"), "{x}");
1270
1271 let x = e("a: [1, 2\n");
1272 assert!(x.msg.contains("unterminated"), "{x}");
1273 let x = e("a: \"unterminated\n");
1274 assert!(x.msg.contains("unterminated"), "{x}");
1275
1276 let x = e("a: .inf\n");
1277 assert!(x.msg.contains("non-finite"), "{x}");
1278
1279 let x = e("a:\n b: 1\n c: 2\n");
1280 assert_eq!(x.line, 3);
1281 assert!(x.msg.contains("indentation"), "{x}");
1282
1283 let x = e("- a\nb: 1\n");
1284 assert!(x.msg.contains("unexpected content"), "{x}");
1285 }
1286
1287 #[test]
1288 fn json_is_valid_yaml_for_the_flow_subset() {
1289 let src = r#"{"model": "m", "limits": {"max_steps": 3}, "subscribe": ["a", "b"], "flag": true, "n": null}"#;
1291 let from_yaml = y(src);
1292 let from_json: Value = serde_json::from_str(src).unwrap();
1293 assert_eq!(from_yaml, from_json);
1294 }
1295
1296 #[test]
1297 fn inline_values_for_env_and_flags() {
1298 assert_eq!(parse_inline("12").unwrap(), json!(12));
1299 assert_eq!(parse_inline(" true ").unwrap(), json!(true));
1300 assert_eq!(parse_inline("[a, b, 3]").unwrap(), json!(["a", "b", 3]));
1301 assert_eq!(parse_inline("{k: v}").unwrap(), json!({"k": "v"}));
1302 assert_eq!(parse_inline("plain text").unwrap(), json!("plain text"));
1303 assert_eq!(parse_inline("\"12\"").unwrap(), json!("12"));
1304 assert!(parse_inline("[1, 2").is_err());
1305 assert!(parse_inline("\"a\" tail").is_err());
1306 }
1307}