1use crate::WriteError;
169use crate::document::{Doc, Value};
170use crate::error::DocumentError;
171use crate::error::{OmnistError, ParseError};
172use crate::formats::float_fmt;
173use crate::formats::string_escape::{TOML_ESCAPES, write_quoted};
174use crate::formats::textpos::line_col_bytes;
175use crate::limits::{IntGuard, Limits, Resolved};
176use crate::report::{Severity, WriteReport};
177use indexmap::IndexMap;
178use num_bigint::BigInt;
179use std::collections::HashMap;
180use toml_edit::{Item, TableLike};
181
182pub fn read_toml(text: &str) -> Result<Doc, OmnistError> {
197 read_toml_resolved(text, Resolved::DEFAULT)
198}
199
200pub fn read_toml_with(text: &str, limits: &Limits) -> Result<Doc, OmnistError> {
213 read_toml_resolved(text, limits.resolve()?)
214}
215
216fn read_toml_resolved(text: &str, limits: Resolved) -> Result<Doc, OmnistError> {
217 limits.check_input_size(text.len())?;
219 let text = crate::bom::strip_leading_bom(text)
223 .map_err(|_| ParseError::codec_syntax(1, 1, crate::bom::DOUBLED_BOM_MESSAGE))?;
224 let mut guard = IntGuard::new(limits.max_int_digits);
225 let mut lexemes: HashMap<usize, Value> = HashMap::new();
226 let mut patched = text.to_string();
227 for (start, end) in prescan_numbers(text) {
234 if let Some(value) = number_value(&text[start..end], &mut guard) {
235 blank(&mut patched, start, end, &value);
236 lexemes.insert(start, value);
237 }
238 }
239 let parsed = toml_edit::Document::parse(patched.as_str())
240 .map_err(|e| toml_parse_error(&patched, &e, &limits))?;
241 let value = table_like_to_value(parsed.as_table(), &lexemes, &patched)?;
242 Ok(Doc::of_resolved(&value, limits).map_err(|e| limits.whole_document(e))?)
243}
244
245fn toml_parse_error(text: &str, e: &toml_edit::TomlError, limits: &Resolved) -> OmnistError {
248 if ["max recursion depth", "recursion limit"]
255 .iter()
256 .any(|m| e.message().contains(m))
257 {
258 return toml_recursion_error(limits).into();
259 }
260 let span = e.span().unwrap_or(0..0);
264 let (line, col) = line_col_bytes(text, span.start);
265 if e.message().contains("overflow")
270 && let Some(literal) = text.get(span.clone()).filter(|l| !l.is_empty())
271 {
272 return ParseError::codec_syntax(
273 line,
274 col,
275 format!(
276 "invalid TOML: integer literal {literal:?} is out of range for a 64-bit integer"
277 ),
278 )
279 .into();
280 }
281 ParseError::codec_syntax(line, col, format!("invalid TOML: {}", e.message())).into()
282}
283
284fn toml_recursion_error(limits: &Resolved) -> DocumentError {
289 DocumentError::with_code(
290 "$",
291 "document.limit.depth",
292 format!(
293 "nesting exceeds toml_edit's own recursion limit (about 80 levels), which applies \
294 below the configured maximum depth ({})",
295 limits.max_depth
296 ),
297 )
298}
299
300fn is_number_char(c: char) -> bool {
301 c.is_ascii_alphanumeric() || matches!(c, '_' | '+' | '-' | '.')
302}
303
304fn blank(patched: &mut String, start: usize, end: usize, value: &Value) {
307 let placeholder = if matches!(value, Value::Float(_)) {
308 "0.0"
309 } else {
310 "0"
311 };
312 let mut filler = String::from(placeholder);
313 filler.push_str(&" ".repeat(end - start - placeholder.len()));
314 patched.replace_range(start..end, &filler);
315}
316
317fn number_value(lexeme: &str, guard: &mut IntGuard) -> Option<Value> {
321 let cleaned: String = lexeme.chars().filter(|&c| c != '_').collect();
322 let (neg, body) = match cleaned.strip_prefix('-') {
323 Some(rest) => (true, rest),
324 None => (false, cleaned.strip_prefix('+').unwrap_or(&cleaned)),
325 };
326 let radix = match body.get(..2) {
327 Some("0x") => 16,
328 Some("0o") => 8,
329 Some("0b") => 2,
330 _ => 10,
331 };
332 if radix == 10 && body.contains(['.', 'e', 'E']) {
333 return Some(Value::Float(cleaned.parse().ok()?));
334 }
335 let digits = if radix == 10 { body } else { &body[2..] };
336 let magnitude = if radix == 10 && digits.len() > guard.max() {
337 guard.placeholder()
338 } else {
339 BigInt::parse_bytes(digits.as_bytes(), radix)?
340 };
341 Some(Value::Int(if neg { -magnitude } else { magnitude }))
342}
343
344static NUMBER_RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
345 regex::Regex::new(concat!(
346 r"^(?:[+-]?(?:0|[1-9](?:_?[0-9])*)",
347 r"|0x[0-9A-Fa-f](?:_?[0-9A-Fa-f])*",
348 r"|0o[0-7](?:_?[0-7])*",
349 r"|0b[01](?:_?[01])*",
350 r"|[+-]?(?:0|[1-9](?:_?[0-9])*)(?:\.[0-9](?:_?[0-9])*)?(?:[eE][+-]?[0-9](?:_?[0-9])*)?)$"
351 ))
352 .unwrap()
353});
354
355fn prescan_numbers(text: &str) -> Vec<(usize, usize)> {
362 let b = text.as_bytes();
363 let mut out = Vec::new();
364 let (mut i, mut depth, mut value) = (0, 0usize, false);
365 while i < b.len() {
366 match b[i] {
367 b'#' => {
368 while i < b.len() && b[i] != b'\n' {
369 i += 1;
370 }
371 }
372 q @ (b'"' | b'\'') => {
373 let multi = b[i..].starts_with(&[q, q, q]);
374 i += if multi { 3 } else { 1 };
375 while i < b.len() {
376 if q == b'"' && b[i] == b'\\' {
377 i += 2;
378 } else if multi && b[i..].starts_with(&[q, q, q]) {
379 i += 3;
380 break;
381 } else if !multi && (b[i] == q || b[i] == b'\n') {
382 i += 1;
383 break;
384 } else {
385 i += 1;
386 }
387 }
388 continue;
389 }
390 b'=' if depth == 0 => value = true,
391 b'[' | b'{' if value => depth += 1,
392 b']' | b'}' if value => depth = depth.saturating_sub(1),
393 b'\n' if depth == 0 => value = false,
394 c if value && is_number_char(char::from(c)) => {
395 let start = i;
396 while i < b.len() && is_number_char(char::from(b[i])) {
397 i += 1;
398 }
399 let lexeme = &text[start..i];
400 let after = text[i..].trim_start_matches([' ', '\t']);
401 let is_key = after.starts_with('=');
402 if !is_key && NUMBER_RE.is_match(lexeme) {
403 let prefixed = ["0x", "0o", "0b"].iter().any(|p| lexeme.starts_with(p));
404 let float = !prefixed && lexeme.contains(['.', 'e', 'E']);
405 let take = if float {
406 lexeme
407 .replace('_', "")
408 .parse::<f64>()
409 .is_ok_and(f64::is_infinite)
410 } else {
411 lexeme.len() >= 18
412 };
413 if take {
414 out.push((start, i));
415 }
416 }
417 continue;
418 }
419 _ => {}
420 }
421 i += 1;
422 }
423 out
424}
425
426fn table_like_to_value(
429 t: &dyn TableLike,
430 lexemes: &HashMap<usize, Value>,
431 src: &str,
432) -> Result<Value, ParseError> {
433 let mut map = IndexMap::new();
434 for (k, item) in t.iter() {
435 map.insert(k.to_string(), item_to_value(item, lexemes, src)?);
436 }
437 Ok(Value::Object(map))
438}
439
440fn item_to_value(
441 item: &Item,
442 lexemes: &HashMap<usize, Value>,
443 src: &str,
444) -> Result<Value, ParseError> {
445 match item {
446 Item::None => unreachable!(
453 "Item::None is only produced by toml_edit's mutation API, never by parsing text"
454 ),
455 Item::Value(v) => toml_value_to_value(v, lexemes, src),
456 Item::Table(t) => table_like_to_value(t, lexemes, src),
457 Item::ArrayOfTables(arr) => {
458 let mut out = Vec::with_capacity(arr.len());
459 for t in arr.iter() {
460 out.push(table_like_to_value(t, lexemes, src)?);
461 }
462 Ok(Value::Array(out))
463 }
464 }
465}
466
467fn toml_value_to_value(
468 v: &toml_edit::Value,
469 lexemes: &HashMap<usize, Value>,
470 src: &str,
471) -> Result<Value, ParseError> {
472 if let Some(read) = v.span().and_then(|s| lexemes.get(&s.start)) {
475 return Ok(read.clone());
476 }
477 match v {
478 toml_edit::Value::String(s) => Ok(Value::Str(s.value().clone())),
479 toml_edit::Value::Integer(i) => Ok(Value::Int((*i.value()).into())),
486 toml_edit::Value::Float(f) => Ok(Value::Float(*f.value())),
487 toml_edit::Value::Boolean(b) => Ok(Value::Bool(*b.value())),
488 toml_edit::Value::Datetime(dt) => {
497 check_fraction_digits(v, src)?;
498 let canonical = format_datetime(dt.value());
499 let inner = dt.value();
500 Ok(match (inner.date.is_some(), inner.time.is_some()) {
501 (true, true) => Value::Datetime(canonical),
502 (true, false) => Value::Date(canonical),
503 (false, true) => Value::Time(canonical),
504 (false, false) => Value::Str(canonical),
510 })
511 }
512 toml_edit::Value::Array(arr) => {
513 let mut out = Vec::with_capacity(arr.len());
514 for item in arr.iter() {
515 out.push(toml_value_to_value(item, lexemes, src)?);
516 }
517 Ok(Value::Array(out))
518 }
519 toml_edit::Value::InlineTable(it) => table_like_to_value(it, lexemes, src),
520 }
521}
522
523fn check_fraction_digits(v: &toml_edit::Value, src: &str) -> Result<(), ParseError> {
529 let span = v.span().unwrap_or(0..0);
530 let lit = src.get(span.clone()).unwrap_or("");
531 if crate::formats::fraction_inexact(lit) {
532 let (line, col) = line_col_bytes(src, span.start);
533 return Err(ParseError::codec_syntax(
534 line,
535 col,
536 "invalid TOML: a fractional second has more than six digits (the Document model holds at most microseconds); it would be altered",
537 ));
538 }
539 Ok(())
540}
541
542fn format_datetime(dt: &toml_edit::Datetime) -> String {
546 let mut out = String::new();
547 if let Some(d) = &dt.date {
548 out.push_str(&format!("{:04}-{:02}-{:02}", d.year, d.month, d.day));
549 }
550 if let Some(t) = &dt.time {
551 if dt.date.is_some() {
552 out.push('T');
553 }
554 out.push_str(&format!(
555 "{:02}:{:02}:{:02}",
556 t.hour,
557 t.minute,
558 t.second.unwrap_or(0)
559 ));
560 if let Some(ns) = t.nanosecond {
561 let micros = ns / 1000;
562 if micros > 0 {
563 out.push('.');
564 out.push_str(&format!("{micros:06}"));
565 }
566 }
567 }
568 if let Some(off) = &dt.offset {
569 match off {
570 toml_edit::Offset::Z => out.push_str("+00:00"),
571 toml_edit::Offset::Custom { minutes } => {
572 let sign = if *minutes < 0 { '-' } else { '+' };
573 let m = minutes.unsigned_abs();
574 out.push_str(&format!("{sign}{:02}:{:02}", m / 60, m % 60));
575 }
576 }
577 }
578 out
579}
580
581pub fn write_toml(
586 doc: &Doc,
587 strict: bool,
588 report: Option<&mut WriteReport>,
589) -> Result<String, WriteError> {
590 let mut rep = WriteReport::new();
591 add_interleaving_diagnostic(doc, &mut rep);
592 let grouped = doc.to_grouped();
593 let stripped = strip_nulls(grouped, "$")?;
594 let Value::Object(map) = &stripped else {
595 return Err(WriteError::new(
596 "TOML needs a top-level table (the root must be an object)",
597 ));
598 };
599 check_toml_grouped(&stripped, "$", &mut rep);
600 let mut out = String::new();
601 write_table_body(map, &mut out);
602 crate::report::finish_write(out, rep, strict, report)
603}
604
605fn add_interleaving_diagnostic(doc: &Doc, rep: &mut WriteReport) {
611 if doc.has_interleaving_loss() {
612 rep.add(
613 "$",
614 "format.interleaving-lost",
615 "cross-label interleaving could not be written; same-label edges were grouped",
616 Severity::Warning,
617 );
618 }
619}
620
621pub fn check_toml(doc: &Doc) -> WriteReport {
623 let mut rep = WriteReport::new();
624 add_interleaving_diagnostic(doc, &mut rep);
625 let grouped = doc.to_grouped();
626 check_toml_grouped(&grouped, "$", &mut rep);
627 rep
628}
629
630fn check_toml_grouped(node: &Value, path: &str, rep: &mut WriteReport) {
631 match node {
632 Value::Object(map) => {
633 for (label, child) in map {
634 match child {
635 Value::Null => {
636 rep.add(
637 crate::report::child_path(path, label, None),
638 "write.unsupported-value",
639 "null value has no TOML representation (TOML has no null token)",
640 Severity::Error,
641 );
642 }
643 Value::Array(items) => {
644 for (i, item) in items.iter().enumerate() {
645 let p = crate::report::child_path(
646 path,
647 label,
648 crate::report::occurrence_index(i, items.len()),
649 );
650 if matches!(item, Value::Null) {
651 rep.add(
652 p,
653 "write.unsupported-value",
654 "null value has no TOML representation (TOML has no null token)",
655 Severity::Error,
656 );
657 } else {
658 check_toml_grouped(item, &p, rep);
659 }
660 }
661 }
662 other => {
663 let p = crate::report::child_path(path, label, None);
664 check_toml_grouped(other, &p, rep);
665 }
666 }
667 }
668 }
669 Value::Array(items) => {
670 for (i, item) in items.iter().enumerate() {
671 let p = crate::report::child_path(
672 path,
673 "",
674 crate::report::occurrence_index(i, items.len()),
675 );
676 if matches!(item, Value::Null) {
677 rep.add(
678 p,
679 "write.unsupported-value",
680 "null value has no TOML representation (TOML has no null token)",
681 Severity::Error,
682 );
683 } else {
684 check_toml_grouped(item, &p, rep);
685 }
686 }
687 }
688 Value::Time(s) | Value::Datetime(s) if super::fraction_inexact(s) => {
691 rep.add(
692 path,
693 "format.temporal-stringified",
694 "fractional second has more than six digits; wrote as a quoted ISO-8601 string",
695 Severity::Warning,
696 );
697 }
698 Value::Time(s) if has_offset(s) => {
699 rep.add(
700 path,
701 "format.temporal-stringified",
702 "time with an offset has no native TOML literal; wrote as a quoted ISO-8601 string",
703 Severity::Warning,
704 );
705 }
706 _ => {}
707 }
708}
709
710pub(crate) struct Toml;
717
718impl crate::formats::Codec for Toml {
719 const NAME: &'static str = "toml";
720
721 fn read(text: &str) -> Result<Doc, OmnistError> {
722 read_toml(text)
723 }
724
725 fn write(doc: &Doc) -> Result<String, OmnistError> {
726 write_toml(doc, false, None).map_err(Into::into)
727 }
728
729 fn check(doc: &Doc) -> WriteReport {
730 check_toml(doc)
731 }
732}
733
734fn strip_nulls(node: Value, path: &str) -> Result<Value, WriteError> {
744 match node {
745 Value::Object(map) => {
746 let mut out = IndexMap::new();
747 for (label, child) in map {
748 match child {
749 Value::Null => {
750 let p = crate::report::child_path(path, &label, None);
751 return Err(crate::report::unsupported_value_error(
752 &p,
753 "null value has no TOML representation (TOML has no null token)",
754 ));
755 }
756 Value::Array(items) => {
757 let mut kept = Vec::with_capacity(items.len());
758 let count = items.len();
759 for (i, item) in items.into_iter().enumerate() {
760 let p = crate::report::child_path(
761 path,
762 &label,
763 crate::report::occurrence_index(i, count),
764 );
765 if matches!(item, Value::Null) {
766 return Err(crate::report::unsupported_value_error(
767 &p,
768 "null value has no TOML representation (TOML has no null token)",
769 ));
770 }
771 kept.push(strip_nulls(item, &p)?);
772 }
773 out.insert(label, Value::Array(kept));
774 }
775 other => {
776 let p = crate::report::child_path(path, &label, None);
777 out.insert(label, strip_nulls(other, &p)?);
778 }
779 }
780 }
781 Ok(Value::Object(out))
782 }
783 other => Ok(other),
784 }
785}
786
787fn write_table_body(map: &IndexMap<String, Value>, out: &mut String) {
788 for (k, v) in map {
789 write_key(k, out);
790 out.push_str(" = ");
791 write_inline_value(v, out);
792 out.push('\n');
793 }
794}
795
796fn write_inline_value(v: &Value, out: &mut String) {
797 match v {
798 Value::Object(map) => {
799 if map.is_empty() {
800 out.push_str("{}");
801 return;
802 }
803 out.push_str("{ ");
804 let mut first = true;
805 for (k, child) in map {
806 if !first {
807 out.push_str(", ");
808 }
809 first = false;
810 write_key(k, out);
811 out.push_str(" = ");
812 write_inline_value(child, out);
813 }
814 out.push_str(" }");
815 }
816 Value::Array(items) => {
817 if items.is_empty() {
818 out.push_str("[]");
819 return;
820 }
821 out.push('[');
822 let mut first = true;
823 for item in items {
824 if !first {
825 out.push_str(", ");
826 }
827 first = false;
828 write_inline_value(item, out);
829 }
830 out.push(']');
831 }
832 scalar => write_scalar(scalar, out),
833 }
834}
835
836fn write_scalar(v: &Value, out: &mut String) {
837 match v {
838 Value::Null => unreachable!("null values are stripped before writing (strip_nulls)"),
839 Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
840 Value::Int(i) => out.push_str(&i.to_string()),
841 Value::Float(x) => write_float(*x, out),
842 Value::Str(s) => write_toml_string(s, out),
847 Value::Date(s) => out.push_str(s),
848 Value::Datetime(s) => {
849 let text = super::temporal_text(s, true);
850 if super::fraction_inexact(&text) {
851 write_toml_string(&text, out);
852 } else {
853 out.push_str(&text);
854 }
855 }
856 Value::Time(s) => {
864 let text = super::temporal_text(s, false);
865 if has_offset(s) || super::fraction_inexact(&text) {
866 write_toml_string(&text, out);
867 } else {
868 out.push_str(&text);
869 }
870 }
871 Value::Object(_) | Value::Array(_) => {
872 unreachable!("write_scalar is only ever called on a leaf")
873 }
874 }
875}
876
877fn write_float(x: f64, out: &mut String) {
880 float_fmt::write_float(x, "nan", "inf", "-inf", out);
881}
882
883fn has_offset(s: &str) -> bool {
888 s.contains(['+', '-', 'Z', 'z'])
889}
890
891fn write_key(k: &str, out: &mut String) {
892 if is_bare_key(k) {
893 out.push_str(k);
894 } else {
895 write_toml_string(k, out);
896 }
897}
898
899fn is_bare_key(k: &str) -> bool {
900 !k.is_empty()
901 && k.chars()
902 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
903}
904
905fn write_toml_string(s: &str, out: &mut String) {
906 write_quoted(s, &TOML_ESCAPES, out);
907}
908
909#[cfg(test)]
910mod tests {
911 use super::*;
912 use crate::document::Scalar;
913
914 fn doc_of(v: Value) -> Doc {
915 Doc::of(&v).unwrap()
916 }
917
918 fn obj(pairs: Vec<(&str, Value)>) -> Value {
919 Value::Object(pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
920 }
921
922 #[test]
923 fn check_toml_reports_null_omitted_across_objects_and_arrays() {
924 let doc = doc_of(obj(vec![
925 ("null_field", Value::Null),
926 (
927 "nested",
928 obj(vec![
929 ("inner_null", Value::Null),
930 ("valid", Value::Int(1.into())),
931 ]),
932 ),
933 (
934 "arr",
935 Value::Array(vec![
936 Value::Null,
937 Value::Int(2.into()),
938 obj(vec![("arr_inner_null", Value::Null)]),
939 ]),
940 ),
941 ]));
942 let rep = check_toml(&doc);
943 assert_eq!(rep.adjustments().len(), 4);
944 assert_eq!(rep.adjustments()[0].path, "$.null_field");
945 assert_eq!(rep.adjustments()[0].code, "write.unsupported-value");
946 }
947
948 #[test]
949 fn check_toml_grouped_handles_array_root() {
950 let arr = Value::Array(vec![Value::Null, Value::Int(42.into())]);
951 let mut rep = WriteReport::new();
952 check_toml_grouped(&arr, "$", &mut rep);
953 assert_eq!(rep.adjustments().len(), 1);
954 assert_eq!(rep.adjustments()[0].path, "$[\"\"][0]");
956 }
957
958 #[test]
959 fn toml_value_to_value_defensive_fallback_on_a_bare_offset_datetime() {
960 let dt = toml_edit::Datetime {
967 date: None,
968 time: None,
969 offset: None,
970 };
971 let v = toml_edit::Value::Datetime(toml_edit::Formatted::new(dt));
972 assert_eq!(
973 toml_value_to_value(&v, &HashMap::new(), "").unwrap(),
974 Value::Str(String::new())
975 );
976 }
977
978 #[test]
981 fn reads_every_native_scalar_kind() {
982 let doc = read_toml("a = 1\nb = \"s\"\nc = true\nd = 1.5\ne = false\n").unwrap();
983 let root = doc.root();
984 assert_eq!(
985 *root.get_one("a").unwrap().value().unwrap(),
986 Scalar::Int((1).into())
987 );
988 assert_eq!(
989 *root.get_one("b").unwrap().value().unwrap(),
990 Scalar::Str("s".to_string())
991 );
992 assert_eq!(
993 *root.get_one("c").unwrap().value().unwrap(),
994 Scalar::Bool(true)
995 );
996 assert_eq!(
997 *root.get_one("d").unwrap().value().unwrap(),
998 Scalar::Float(1.5)
999 );
1000 assert_eq!(
1001 *root.get_one("e").unwrap().value().unwrap(),
1002 Scalar::Bool(false)
1003 );
1004 }
1005
1006 #[test]
1007 fn reads_nested_tables_and_arrays() {
1008 let doc = read_toml("arr = [1, 2, 3]\n[nested]\nx = 1\n").unwrap();
1009 let root = doc.root();
1010 let items: Vec<_> = root.get("arr");
1011 assert_eq!(items.len(), 3);
1012 let nested = root.get_one("nested").unwrap();
1013 assert_eq!(
1014 *nested.get_one("x").unwrap().value().unwrap(),
1015 Scalar::Int((1).into())
1016 );
1017 }
1018
1019 #[test]
1020 fn reads_array_of_tables() {
1021 let doc = read_toml("[[items]]\nx = 1\n[[items]]\nx = 2\n").unwrap();
1022 let root = doc.root();
1023 let items: Vec<_> = root.get("items");
1024 assert_eq!(items.len(), 2);
1025 assert_eq!(
1026 *items[0].get_one("x").unwrap().value().unwrap(),
1027 Scalar::Int((1).into())
1028 );
1029 assert_eq!(
1030 *items[1].get_one("x").unwrap().value().unwrap(),
1031 Scalar::Int((2).into())
1032 );
1033 }
1034
1035 #[test]
1036 fn invalid_toml_is_a_parse_error() {
1037 let err = read_toml("a = ").unwrap_err();
1038 assert!(matches!(err, OmnistError::Parse(_)));
1039 }
1040
1041 #[test]
1044 fn reads_local_date() {
1045 let doc = read_toml("d = 1979-05-27\n").unwrap();
1046 assert_eq!(
1047 *doc.root().get_one("d").unwrap().value().unwrap(),
1048 Scalar::Date("1979-05-27".to_string())
1049 );
1050 }
1051
1052 #[test]
1053 fn reads_local_time_with_fraction() {
1054 let doc = read_toml("t = 00:32:00.999999\n").unwrap();
1055 assert_eq!(
1056 *doc.root().get_one("t").unwrap().value().unwrap(),
1057 Scalar::Time("00:32:00.999999".to_string())
1058 );
1059 }
1060
1061 #[test]
1062 fn reads_local_time_without_fraction() {
1063 let doc = read_toml("t = 07:32:00\n").unwrap();
1064 assert_eq!(
1065 *doc.root().get_one("t").unwrap().value().unwrap(),
1066 Scalar::Time("07:32:00".to_string())
1067 );
1068 }
1069
1070 #[test]
1071 fn a_nonzero_digit_beyond_microseconds_fails_instead_of_truncating() {
1072 for text in [
1075 "t = 00:32:00.9999999\n",
1076 "t = 07:32:00.000000001\n",
1077 "t = 07:32:00.1234567891\n",
1078 ] {
1079 let err = read_toml(text).unwrap_err().to_string();
1080 assert!(err.contains("six digits"), "{text:?}: {err}");
1081 }
1082 }
1083
1084 #[test]
1085 fn an_inexact_fraction_is_written_quoted_and_reported() {
1086 for (v, text_in) in [
1087 (
1088 Value::Time("07:32:00.1234567".to_string()),
1089 "07:32:00.1234567",
1090 ),
1091 (
1092 Value::Datetime("1979-05-27T07:32:00.1234567+01:00".to_string()),
1093 "1979-05-27T07:32:00.1234567+01:00",
1094 ),
1095 ] {
1096 let doc = doc_of(obj(vec![("t", v)]));
1097 let mut report = WriteReport::new();
1098 let text = write_toml(&doc, false, Some(&mut report)).unwrap();
1099 assert_eq!(text, format!("t = \"{text_in}\"\n"));
1100 assert_eq!(report.adjustments().len(), 1);
1101 assert_eq!(report.adjustments()[0].code, "format.temporal-stringified");
1102 assert_eq!(check_toml(&doc).adjustments().len(), 1);
1103 assert!(read_toml(&text).is_ok());
1104 }
1105 let doc = doc_of(obj(vec![(
1107 "t",
1108 Value::Time("07:32:00.1234560".to_string()),
1109 )]));
1110 assert_eq!(
1111 write_toml(&doc, false, None).unwrap(),
1112 "t = 07:32:00.123456\n"
1113 );
1114 }
1115
1116 #[test]
1117 fn an_all_zero_fraction_reads_as_no_fraction() {
1118 for text in ["t = 07:32:00.000\n", "t = 07:32:00.0000000\n"] {
1119 let doc = read_toml(text).unwrap();
1120 assert_eq!(
1121 *doc.root().get_one("t").unwrap().value().unwrap(),
1122 Scalar::Time("07:32:00".to_string()),
1123 "{text:?}"
1124 );
1125 }
1126 }
1127
1128 #[test]
1129 fn zeros_beyond_microseconds_are_insignificant() {
1130 let doc = read_toml("t = 00:32:00.9999990\n").unwrap();
1131 assert_eq!(
1132 *doc.root().get_one("t").unwrap().value().unwrap(),
1133 Scalar::Time("00:32:00.999999".to_string())
1134 );
1135 }
1136
1137 #[test]
1138 fn reads_local_datetime() {
1139 let doc = read_toml("dt = 1979-05-27T07:32:00\n").unwrap();
1140 assert_eq!(
1141 *doc.root().get_one("dt").unwrap().value().unwrap(),
1142 Scalar::Datetime("1979-05-27T07:32:00".to_string())
1143 );
1144 }
1145
1146 #[test]
1147 fn reads_offset_datetime_z_normalizes_to_numeric_offset() {
1148 let doc = read_toml("dt = 1979-05-27T07:32:00Z\n").unwrap();
1149 assert_eq!(
1150 *doc.root().get_one("dt").unwrap().value().unwrap(),
1151 Scalar::Datetime("1979-05-27T07:32:00+00:00".to_string())
1152 );
1153 }
1154
1155 #[test]
1156 fn round_trips_offset_datetime_preserving_negative_offset() {
1157 let doc = read_toml("dt = 1979-05-27T07:32:00-07:00\n").unwrap();
1158 assert_eq!(
1159 *doc.root().get_one("dt").unwrap().value().unwrap(),
1160 Scalar::Datetime("1979-05-27T07:32:00-07:00".to_string())
1161 );
1162 let text = write_toml(&doc, false, None).unwrap();
1163 assert_eq!(text, "dt = 1979-05-27T07:32:00-07:00\n");
1164 let doc2 = read_toml(&text).unwrap();
1165 assert_eq!(
1166 *doc2.root().get_one("dt").unwrap().value().unwrap(),
1167 Scalar::Datetime("1979-05-27T07:32:00-07:00".to_string())
1168 );
1169 }
1170
1171 #[test]
1172 fn round_trips_offset_datetime_preserving_positive_offset_and_fraction() {
1173 let src = "dt = 1979-05-27T07:32:00.999999+07:00\n";
1174 let doc = read_toml(src).unwrap();
1175 let text = write_toml(&doc, false, None).unwrap();
1176 assert_eq!(text, src);
1177 }
1178
1179 #[test]
1180 fn space_separated_datetime_reads_as_t_joined_canonical_string() {
1181 let doc = read_toml("dt = 1979-05-27 07:32:00-07:00\n").unwrap();
1182 assert_eq!(
1183 *doc.root().get_one("dt").unwrap().value().unwrap(),
1184 Scalar::Datetime("1979-05-27T07:32:00-07:00".to_string())
1185 );
1186 }
1187
1188 #[test]
1191 fn round_trips_every_native_scalar_kind() {
1192 let v = obj(vec![
1193 ("a", Value::Int((42).into())),
1194 ("b", Value::Str("hello".to_string())),
1195 ("c", Value::Bool(true)),
1196 ("d", Value::Float(1.5)),
1197 ("e", Value::Bool(false)),
1198 ]);
1199 let doc = doc_of(v);
1200 let text = write_toml(&doc, false, None).unwrap();
1201 let doc2 = read_toml(&text).unwrap();
1202 let root = doc2.root();
1203 assert_eq!(
1204 *root.get_one("a").unwrap().value().unwrap(),
1205 Scalar::Int((42).into())
1206 );
1207 assert_eq!(
1208 *root.get_one("b").unwrap().value().unwrap(),
1209 Scalar::Str("hello".to_string())
1210 );
1211 assert_eq!(
1212 *root.get_one("c").unwrap().value().unwrap(),
1213 Scalar::Bool(true)
1214 );
1215 assert_eq!(
1216 *root.get_one("d").unwrap().value().unwrap(),
1217 Scalar::Float(1.5)
1218 );
1219 assert_eq!(
1220 *root.get_one("e").unwrap().value().unwrap(),
1221 Scalar::Bool(false)
1222 );
1223 }
1224
1225 #[test]
1226 fn round_trips_integral_float_at_and_above_1e17_boundary_issue_46() {
1227 for x in [1.0e17, 1.0e18, -1.23e17, 9.9e16_f64] {
1231 let doc = doc_of(obj(vec![("a", Value::Float(x))]));
1232 let text = write_toml(&doc, false, None).unwrap();
1233 let back = read_toml(&text).unwrap();
1234 assert_eq!(
1235 *back.root().get_one("a").unwrap().value().unwrap(),
1236 Scalar::Float(x),
1237 "x={x} text={text}"
1238 );
1239 }
1240 }
1241
1242 #[test]
1243 fn round_trips_local_date_time_and_datetime() {
1244 let v = obj(vec![
1249 ("d", Value::Date("1979-05-27".to_string())),
1250 ("t", Value::Time("07:32:00".to_string())),
1251 ("dt", Value::Datetime("1979-05-27T07:32:00".to_string())),
1252 ]);
1253 let doc = doc_of(v);
1254 let text = write_toml(&doc, false, None).unwrap();
1255 assert!(text.contains("d = 1979-05-27\n"));
1256 assert!(text.contains("t = 07:32:00\n"));
1257 assert!(text.contains("dt = 1979-05-27T07:32:00\n"));
1258 let doc2 = read_toml(&text).unwrap();
1259 let root = doc2.root();
1260 assert_eq!(
1261 *root.get_one("d").unwrap().value().unwrap(),
1262 Scalar::Date("1979-05-27".to_string())
1263 );
1264 assert_eq!(
1265 *root.get_one("t").unwrap().value().unwrap(),
1266 Scalar::Time("07:32:00".to_string())
1267 );
1268 assert_eq!(
1269 *root.get_one("dt").unwrap().value().unwrap(),
1270 Scalar::Datetime("1979-05-27T07:32:00".to_string())
1271 );
1272 }
1273
1274 #[test]
1275 fn a_genuine_time_value_carrying_an_offset_writes_as_a_quoted_string() {
1276 let v = obj(vec![("t", Value::Time("07:32:00+02:00".to_string()))]);
1282 let doc = doc_of(v);
1283 let text = write_toml(&doc, false, None).unwrap();
1284 assert!(text.contains("t = \"07:32:00+02:00\"\n"));
1285 }
1286
1287 #[test]
1288 fn round_trips_nested_table_and_array() {
1289 let v = obj(vec![
1290 (
1291 "nested",
1292 obj(vec![
1293 ("x", Value::Int((1).into())),
1294 ("y", Value::Str("z".to_string())),
1295 ]),
1296 ),
1297 (
1298 "arr",
1299 Value::Array(vec![
1300 Value::Int((1).into()),
1301 Value::Int((2).into()),
1302 Value::Int((3).into()),
1303 ]),
1304 ),
1305 ]);
1306 let doc = doc_of(v);
1307 let text = write_toml(&doc, false, None).unwrap();
1308 let doc2 = read_toml(&text).unwrap();
1309 let root = doc2.root();
1310 let nested = root.get_one("nested").unwrap();
1311 assert_eq!(
1312 *nested.get_one("x").unwrap().value().unwrap(),
1313 Scalar::Int((1).into())
1314 );
1315 assert_eq!(
1316 *nested.get_one("y").unwrap().value().unwrap(),
1317 Scalar::Str("z".to_string())
1318 );
1319 assert_eq!(root.get("arr").len(), 3);
1320 }
1321
1322 #[test]
1323 fn round_trips_array_of_tables() {
1324 let v = obj(vec![(
1325 "items",
1326 Value::Array(vec![
1327 obj(vec![("x", Value::Int((1).into()))]),
1328 obj(vec![("x", Value::Int((2).into()))]),
1329 ]),
1330 )]);
1331 let doc = doc_of(v);
1332 let text = write_toml(&doc, false, None).unwrap();
1333 let doc2 = read_toml(&text).unwrap();
1334 let items: Vec<_> = doc2.root().get("items");
1335 assert_eq!(items.len(), 2);
1336 assert_eq!(
1337 *items[0].get_one("x").unwrap().value().unwrap(),
1338 Scalar::Int((1).into())
1339 );
1340 assert_eq!(
1341 *items[1].get_one("x").unwrap().value().unwrap(),
1342 Scalar::Int((2).into())
1343 );
1344 }
1345
1346 #[test]
1347 fn round_trips_nan_and_infinity_natively() {
1348 let v = obj(vec![
1349 ("a", Value::Float(f64::NAN)),
1350 ("b", Value::Float(f64::INFINITY)),
1351 ("c", Value::Float(f64::NEG_INFINITY)),
1352 ]);
1353 let doc = doc_of(v);
1354 let text = write_toml(&doc, false, None).unwrap();
1355 assert!(text.contains("a = nan\n"));
1356 assert!(text.contains("b = inf\n"));
1357 assert!(text.contains("c = -inf\n"));
1358 let doc2 = read_toml(&text).unwrap();
1359 let root = doc2.root();
1360 assert!(matches!(
1361 root.get_one("a").unwrap().value().unwrap(),
1362 Scalar::Float(x) if x.is_nan()
1363 ));
1364 assert_eq!(
1365 *root.get_one("b").unwrap().value().unwrap(),
1366 Scalar::Float(f64::INFINITY)
1367 );
1368 assert_eq!(
1369 *root.get_one("c").unwrap().value().unwrap(),
1370 Scalar::Float(f64::NEG_INFINITY)
1371 );
1372 assert!(check_toml(&doc).is_empty());
1374 }
1375
1376 #[test]
1387 fn write_fails_unconditionally_on_null_field_lenient() {
1388 let v = obj(vec![("a", Value::Int((1).into())), ("b", Value::Null)]);
1389 let doc = doc_of(v);
1390 let mut rep = WriteReport::new();
1391 let err = write_toml(&doc, false, Some(&mut rep)).unwrap_err();
1392 assert!(err.to_string().contains("write.unsupported-value"));
1393 assert!(err.to_string().contains("$.b"));
1394 assert!(rep.is_empty());
1395 assert!(err.report().is_none());
1396 }
1397
1398 #[test]
1402 fn write_fails_unconditionally_on_null_array_item() {
1403 let v = obj(vec![(
1404 "c",
1405 Value::Array(vec![
1406 Value::Int((1).into()),
1407 Value::Null,
1408 Value::Int((2).into()),
1409 ]),
1410 )]);
1411 let doc = doc_of(v);
1412 let err = write_toml(&doc, false, None).unwrap_err();
1413 assert!(err.to_string().contains("write.unsupported-value"));
1414 assert!(err.to_string().contains("$.c[1]"));
1415 }
1416
1417 #[test]
1418 fn null_in_nested_table_records_nested_path_in_check() {
1419 let v = obj(vec![(
1420 "nested",
1421 obj(vec![("x", Value::Null), ("y", Value::Int((5).into()))]),
1422 )]);
1423 let doc = doc_of(v);
1424 let rep = check_toml(&doc);
1425 assert_eq!(rep.len(), 1);
1426 assert_eq!(rep.adjustments()[0].path, "$.nested.x");
1427 assert_eq!(rep.adjustments()[0].code, "write.unsupported-value");
1428 }
1429
1430 #[test]
1435 fn write_fails_unconditionally_on_null_strict() {
1436 let v = obj(vec![("a", Value::Int((1).into())), ("b", Value::Null)]);
1437 let doc = doc_of(v);
1438 let err = write_toml(&doc, true, None).unwrap_err();
1439 assert!(err.to_string().contains("$.b"));
1440 assert!(err.to_string().contains("write.unsupported-value"));
1441 assert!(err.report().is_none());
1442 }
1443
1444 #[test]
1445 fn check_toml_reports_without_producing_output() {
1446 let v = obj(vec![("a", Value::Null)]);
1447 let doc = doc_of(v);
1448 let rep = check_toml(&doc);
1449 assert_eq!(rep.len(), 1);
1450 assert_eq!(rep.adjustments()[0].code, "write.unsupported-value");
1451 }
1452
1453 #[test]
1456 fn non_table_root_is_a_write_error() {
1457 let doc = doc_of(Value::Int((5).into()));
1458 let err = write_toml(&doc, false, None).unwrap_err();
1459 assert!(err.to_string().contains("top-level table"));
1460 assert!(err.report().is_none());
1461 }
1462
1463 #[test]
1464 fn non_table_root_is_a_write_error_even_with_a_report_supplied() {
1465 let doc = doc_of(Value::Int((5).into()));
1466 let mut rep = WriteReport::new();
1467 let err = write_toml(&doc, false, Some(&mut rep)).unwrap_err();
1468 assert!(err.to_string().contains("top-level table"));
1469 assert!(rep.is_empty());
1470 }
1471
1472 #[test]
1473 fn empty_object_root_writes_empty_text() {
1474 let doc = doc_of(Value::Object(IndexMap::new()));
1475 let text = write_toml(&doc, false, None).unwrap();
1476 assert_eq!(text, "");
1477 }
1478
1479 #[test]
1482 fn a_bare_radix_prefix_names_the_literal_as_out_of_range() {
1483 for (text, literal) in [
1484 (
1485 "a = 0x
1486", "0x",
1487 ),
1488 (
1489 "a = 0o
1490", "0o",
1491 ),
1492 (
1493 "a = [0b]
1494",
1495 "0b",
1496 ),
1497 ] {
1498 let err = read_toml(text).unwrap_err();
1499 assert!(
1500 matches!(&err, OmnistError::Parse(e)
1501 if e.message.contains(&format!("integer literal {literal:?} is out of range"))),
1502 "{text:?} gave {err:?}"
1503 );
1504 }
1505 }
1506
1507 fn scalar_at(text: &str, label: &str) -> Scalar {
1509 read_toml(text)
1510 .unwrap()
1511 .root()
1512 .get_one(label)
1513 .unwrap()
1514 .value()
1515 .unwrap()
1516 .clone()
1517 }
1518
1519 fn int_at(text: &str, label: &str) -> String {
1521 let got = scalar_at(text, label);
1522 assert!(matches!(got, Scalar::Int(_)), "{got:?}");
1523 got.to_string()
1524 }
1525
1526 fn assert_int_digits(err: &OmnistError, path: &str) {
1527 assert!(
1528 matches!(err, OmnistError::Document(e)
1529 if e.code.as_deref() == Some("document.limit.int-digits")
1530 && e.path == path
1531 && e.message.contains("4300-digit limit")),
1532 "expected document.limit.int-digits at {path}, got {err:?}"
1533 );
1534 }
1535
1536 #[test]
1537 fn integer_at_4300_digits_reads_exactly() {
1538 let digits = "9".repeat(4300);
1541 assert_eq!(int_at(&format!("x = {digits}\n"), "x"), digits);
1542 assert_eq!(
1543 int_at(&format!("x = -{digits}\n"), "x"),
1544 format!("-{digits}")
1545 );
1546 }
1547
1548 #[test]
1549 fn huge_hex_literal_is_limited_by_its_decimal_digits() {
1550 let text = format!("x = 0x{}\n", "f".repeat(3600));
1553 assert_int_digits(&read_toml(&text).unwrap_err(), "$.x");
1554 let text = format!("x = 0x{}\n", "f".repeat(5000));
1556 assert_int_digits(&read_toml(&text).unwrap_err(), "$.x");
1557 }
1558
1559 #[test]
1560 fn a_long_binary_literal_within_the_decimal_limit_reads() {
1561 let text = format!("x = 0b{}\n", "1".repeat(4301));
1563 let expected: BigInt = (BigInt::from(1) << 4301) - 1;
1564 assert_eq!(scalar_at(&text, "x"), Scalar::Int(expected));
1565 }
1566
1567 #[test]
1568 fn integer_over_4300_digits_is_the_digit_cap_error() {
1569 let text = format!("x = {}\n", "9".repeat(4301));
1570 assert_int_digits(&read_toml(&text).unwrap_err(), "$.x");
1571 let text = format!("x = -{}\n", "9".repeat(4301));
1572 assert_int_digits(&read_toml(&text).unwrap_err(), "$.x");
1573 }
1574
1575 #[test]
1576 fn integer_beyond_64_bits_reads_exactly_in_every_radix() {
1577 let big = "9223372036854775808"; assert_eq!(int_at(&format!("x = {big}\n"), "x"), big);
1579 assert_eq!(
1580 int_at("x = -9223372036854775809\n", "x"),
1581 "-9223372036854775809"
1582 );
1583 assert_eq!(int_at("x = 9_223_372_036_854_775_808\n", "x"), big);
1584 assert_eq!(int_at("x = +9223372036854775808\n", "x"), big);
1585 assert_eq!(
1586 int_at("x = 0xFFFFFFFFFFFFFFFFFF\n", "x"),
1587 "4722366482869645213695"
1588 );
1589 assert_eq!(
1590 int_at("x = 0o7777777777777777777777\n", "x"),
1591 "73786976294838206463"
1592 );
1593 assert_eq!(
1594 int_at(
1595 "x = 0b1111111111111111111111111111111111111111111111111111111111111111111111\n",
1596 "x"
1597 )
1598 .to_string(),
1599 "1180591620717411303423"
1600 );
1601 }
1602
1603 #[test]
1604 fn a_decimal_literal_over_a_lowered_limit_is_refused_at_its_path() {
1605 let limits = crate::limits::Limits::default().with_max_int_digits(20);
1606 let err = read_toml_with("a = 1\nb = 123456789012345678901\n", &limits).unwrap_err();
1607 assert!(matches!(
1608 err,
1609 OmnistError::Document(ref e)
1610 if e.code.as_deref() == Some("document.limit.int-digits") && e.path == "$.b"
1611 ));
1612 let ok = read_toml_with("b = 12345678901234567890\n", &limits).unwrap();
1613 assert!(ok.root().get_one("b").is_ok());
1614 }
1615
1616 #[test]
1617 fn big_integers_are_found_by_path_inside_arrays_tables_and_inline_tables() {
1618 let big = "9".repeat(30);
1619 let text =
1620 format!("a = [1, {big}]\n[t]\nx = {{ y = {big} }}\n[[r]]\nz = {big}\n[[r]]\nz = 2\n");
1621 let doc = read_toml(&text).unwrap();
1622 let t = doc.root().get_one("t").unwrap();
1623 let x = t.get_one("x").unwrap();
1624 assert_eq!(
1625 x.get_one("y").unwrap().value(),
1626 Ok(&Scalar::Int(big.parse().unwrap()))
1627 );
1628 let limits = crate::limits::Limits::default().with_max_int_digits(20);
1630 for (text, path) in [
1631 (format!("a = [1, {big}]\n"), "$.a[1]"),
1632 (format!("x = {{ y = {big} }}\n"), "$.x.y"),
1633 (format!("[[r]]\nz = {big}\n[[r]]\nz = 2\n"), "$.r[0].z"),
1634 (format!("[t]\nz = {big}\n"), "$.t.z"),
1635 ] {
1636 let err = read_toml_with(&text, &limits).unwrap_err();
1637 assert!(
1638 matches!(&err, OmnistError::Document(e) if e.path == path
1639 && e.code.as_deref() == Some("document.limit.int-digits")),
1640 "{path}: {err:?}"
1641 );
1642 }
1643 }
1644
1645 #[test]
1646 fn a_syntax_error_wins_over_a_big_integer_wherever_it_is() {
1647 let big = "9".repeat(5000);
1648 let err = read_toml(&format!("a = {big}\nb = = 1\n")).unwrap_err();
1650 assert!(matches!(err, OmnistError::Parse(ref e) if e.line == 2));
1651 let err = read_toml(&format!("b = = 1\na = {big}\n")).unwrap_err();
1653 assert!(matches!(err, OmnistError::Parse(ref e) if e.line == 1));
1654 let text = format!("a = {big}\nb = 99999999999999999999\nc = 1\n");
1656 assert_int_digits(&read_toml(&text).unwrap_err(), "$.a");
1657 }
1658
1659 #[test]
1660 fn an_overflowing_float_reads_as_an_infinity() {
1661 assert_eq!(scalar_at("a = 1e999\n", "a"), Scalar::Float(f64::INFINITY));
1663 assert_eq!(
1664 scalar_at("a = -1e999\n", "a"),
1665 Scalar::Float(f64::NEG_INFINITY)
1666 );
1667 assert_eq!(scalar_at("a = +1e999\n", "a"), Scalar::Float(f64::INFINITY));
1668 assert_eq!(
1669 scalar_at("a = 1.5e999\n", "a"),
1670 Scalar::Float(f64::INFINITY)
1671 );
1672 assert_eq!(
1673 scalar_at("a = 1_0e9_99\n", "a"),
1674 Scalar::Float(f64::INFINITY)
1675 );
1676 assert_eq!(
1677 scalar_at("a = [1e999]\n", "a"),
1678 Scalar::Float(f64::INFINITY)
1679 );
1680 assert_eq!(scalar_at("a = 1e-999\n", "a"), Scalar::Float(0.0));
1682 }
1683
1684 #[test]
1685 fn many_big_integers_read_in_linear_time() {
1686 let n = 20_000;
1689 let big = "99999999999999999999";
1690 let start = std::time::Instant::now();
1691 let array = format!("a = [{}]\n", vec![big; n].join(", "));
1692 assert_eq!(read_toml(&array).unwrap().root().count("a"), n);
1693 let tables: String = (0..n)
1694 .map(|i| format!("t{i} = {{ x = {big} }}\n"))
1695 .collect();
1696 assert!(read_toml(&tables).is_ok());
1697 let floats = format!("f = [{}]\n", vec!["1e999"; n].join(", "));
1698 assert!(read_toml(&floats).is_ok());
1699 assert!(start.elapsed().as_secs() < 60, "{:?}", start.elapsed());
1701 }
1702
1703 #[test]
1704 fn the_big_number_scan_skips_keys_strings_comments_and_headers() {
1705 let big = "99999999999999999999";
1706 let text = format!(
1707 "# {big}\n\
1708 {big} = 1\n\
1709 k = \"{big}\" # {big}\n\
1710 s = '{big}'\n\
1711 m = \"\"\"\n{big} \\\" {big}\n\"\"\"\n\
1712 e = \"a\\\\\"\n\
1713 l = '''\n{big}\n'''\n\
1714 [88888888888888888888]\n\
1715 v = {big}\n\
1716 [[x]]\n\
1717 w = [\n {big},\n {{ {big} = 2, y = {big} }},\n 1e999,\n]\n"
1718 );
1719 let doc = read_toml(&text).unwrap();
1720 assert_eq!(scalar_at(&text, big), Scalar::Int(1.into()));
1722 assert_eq!(scalar_at(&text, "k"), Scalar::Str(big.to_string()));
1723 assert_eq!(scalar_at(&text, "s"), Scalar::Str(big.to_string()));
1724 let h = doc.root().get_one("88888888888888888888").unwrap();
1725 assert_eq!(h.get_one("v").unwrap().value().unwrap().to_string(), big);
1726 for bad in ["a = ]]]\n", "a = \"", "a = '''", "a = \"\\"] {
1728 assert!(read_toml(bad).is_err());
1729 }
1730 }
1731
1732 #[test]
1733 fn an_overflowing_float_with_a_long_spelling_reads_as_an_infinity() {
1734 let limits = crate::limits::Limits::default().with_max_int_digits(30);
1737 let doc = read_toml_with("k = 1.0000000000000000000000000001e999\n", &limits).unwrap();
1738 assert_eq!(
1739 *doc.root().get_one("k").unwrap().value().unwrap(),
1740 Scalar::Float(f64::INFINITY)
1741 );
1742 }
1743
1744 #[test]
1745 fn integer_at_i64_max_and_min_round_trip() {
1746 let text = format!("a = {}\nb = {}\n", i64::MAX, i64::MIN);
1747 let doc = read_toml(&text).unwrap();
1748 let root = doc.root();
1749 assert_eq!(
1750 *root.get_one("a").unwrap().value().unwrap(),
1751 Scalar::Int((i64::MAX).into())
1752 );
1753 assert_eq!(
1754 *root.get_one("b").unwrap().value().unwrap(),
1755 Scalar::Int((i64::MIN).into())
1756 );
1757 }
1758
1759 #[test]
1762 fn toml_edit_s_own_recursion_cap_fires_before_our_200_depth_guard_on_read() {
1763 let mut text = String::from("x = ");
1772 for _ in 0..250 {
1773 text.push_str("{ a = ");
1774 }
1775 text.push('1');
1776 for _ in 0..250 {
1777 text.push_str(" }");
1778 }
1779 text.push('\n');
1780 let err = read_toml(&text).unwrap_err();
1781 assert_depth_refusal(&err);
1782 }
1783
1784 fn assert_depth_refusal(err: &OmnistError) {
1789 assert!(
1790 matches!(err, OmnistError::Document(e)
1791 if e.code.as_deref() == Some("document.limit.depth")
1792 && e.path == "$"
1793 && e.message.contains("toml_edit")),
1794 "expected document.limit.depth, got {err:?}"
1795 );
1796 }
1797
1798 fn header_chain(n: usize, open: &str, close: &str) -> String {
1799 format!("{open}{}{close}\n", vec!["a"; n].join("."))
1800 }
1801
1802 #[test]
1803 fn deeply_nested_table_headers_are_a_depth_error_not_a_panic() {
1804 for (open, close) in [("[", "]"), ("[[", "]]")] {
1807 let ok = read_toml(&header_chain(80, open, close));
1808 assert!(ok.is_ok(), "80 levels of {open}: {ok:?}");
1809 for n in [81, 90, 199, 200, 201, 250, 1000] {
1810 let err = read_toml(&header_chain(n, open, close)).unwrap_err();
1811 assert_depth_refusal(&err);
1812 }
1813 }
1814 let limits = crate::limits::Limits::default().with_max_depth(250);
1816 let err = read_toml_with(&header_chain(200, "[", "]"), &limits).unwrap_err();
1817 assert_depth_refusal(&err);
1818 }
1819
1820 #[test]
1821 fn deeply_nested_inline_tables_arrays_and_dotted_keys_are_a_depth_error() {
1822 for n in [100usize, 200, 1000] {
1823 let inline = format!("a = {}1{}\n", "{a = ".repeat(n), "}".repeat(n));
1824 assert_depth_refusal(&read_toml(&inline).unwrap_err());
1825 let array = format!("a = {}1{}\n", "[".repeat(n), "]".repeat(n));
1826 assert_depth_refusal(&read_toml(&array).unwrap_err());
1827 let dotted = format!("{} = 1\n", vec!["a"; n].join("."));
1828 assert_depth_refusal(&read_toml(&dotted).unwrap_err());
1829 }
1830 }
1831
1832 #[test]
1836 fn an_over_cap_integer_before_a_too_deep_header_is_a_depth_error() {
1837 let digits = "9".repeat(5000);
1838 let text = format!("n = {digits}\n{}", header_chain(100, "[", "]"));
1839 assert_depth_refusal(&read_toml(&text).unwrap_err());
1840 }
1841
1842 #[test]
1843 fn deeply_nested_document_write_reuses_doc_construction_depth_guard() {
1844 let mut v = Value::Int((0).into());
1849 for _ in 0..=crate::document::MAX_DEPTH {
1850 v = obj(vec![("a", v)]);
1851 }
1852 assert!(Doc::of(&v).is_err());
1853 }
1854
1855 #[test]
1858 fn writes_quoted_key_for_non_bare_label() {
1859 let v = obj(vec![("has space", Value::Int((1).into()))]);
1860 let doc = doc_of(v);
1861 let text = write_toml(&doc, false, None).unwrap();
1862 assert_eq!(text, "\"has space\" = 1\n");
1863 let doc2 = read_toml(&text).unwrap();
1864 assert_eq!(
1865 *doc2.root().get_one("has space").unwrap().value().unwrap(),
1866 Scalar::Int((1).into())
1867 );
1868 }
1869
1870 #[test]
1871 fn string_with_control_char_and_quote_escapes_on_write() {
1872 let v = obj(vec![(
1873 "a",
1874 Value::Str("line\nbreak \"q\" \t tab".to_string()),
1875 )]);
1876 let doc = doc_of(v);
1877 let text = write_toml(&doc, false, None).unwrap();
1878 let doc2 = read_toml(&text).unwrap();
1879 assert_eq!(
1880 *doc2.root().get_one("a").unwrap().value().unwrap(),
1881 Scalar::Str("line\nbreak \"q\" \t tab".to_string())
1882 );
1883 }
1884
1885 #[test]
1886 fn time_shaped_string_with_offset_is_not_a_real_toml_time_and_stays_quoted() {
1887 let v = obj(vec![("a", Value::Str("07:32:00+01:00".to_string()))]);
1892 let doc = doc_of(v);
1893 let text = write_toml(&doc, false, None).unwrap();
1894 assert_eq!(text, "a = \"07:32:00+01:00\"\n");
1895 }
1896
1897 #[test]
1898 fn plain_string_that_looks_like_a_date_stays_quoted() {
1899 let v = obj(vec![("a", Value::Str("1979-05-27".to_string()))]);
1908 let doc = doc_of(v);
1909 let text = write_toml(&doc, false, None).unwrap();
1910 assert_eq!(text, "a = \"1979-05-27\"\n");
1911 let doc2 = read_toml(&text).unwrap();
1912 assert_eq!(
1913 *doc2.root().get_one("a").unwrap().value().unwrap(),
1914 Scalar::Str("1979-05-27".to_string())
1915 );
1916 }
1917
1918 #[test]
1919 fn float_integral_value_still_gets_a_decimal_point() {
1920 let v = obj(vec![("a", Value::Float(1.0))]);
1921 let doc = doc_of(v);
1922 let text = write_toml(&doc, false, None).unwrap();
1923 assert_eq!(text, "a = 1.0\n");
1924 }
1925
1926 #[test]
1927 fn float_non_integral_value_writes_default_repr() {
1928 let v = obj(vec![("a", Value::Float(1.25))]);
1929 let doc = doc_of(v);
1930 let text = write_toml(&doc, false, None).unwrap();
1931 assert_eq!(text, "a = 1.25\n");
1932 }
1933
1934 #[test]
1937 fn write_scalar_panics_on_null() {
1938 let result = std::panic::catch_unwind(|| {
1943 let mut out = String::new();
1944 write_scalar(&Value::Null, &mut out);
1945 });
1946 assert!(result.is_err());
1947 }
1948
1949 #[test]
1950 fn write_scalar_panics_on_a_non_leaf_value() {
1951 let result = std::panic::catch_unwind(|| {
1952 let mut out = String::new();
1953 write_scalar(&Value::Object(IndexMap::new()), &mut out);
1954 });
1955 assert!(result.is_err());
1956 }
1957
1958 #[test]
1959 fn item_to_value_panics_on_item_none() {
1960 let result = std::panic::catch_unwind(|| {
1964 let _ = item_to_value(&Item::None, &HashMap::new(), "");
1965 });
1966 assert!(result.is_err());
1967 }
1968
1969 #[test]
1970 fn nested_empty_table_writes_as_inline_empty_table() {
1971 let v = obj(vec![("t", Value::Object(IndexMap::new()))]);
1972 let doc = doc_of(v);
1973 let text = write_toml(&doc, false, None).unwrap();
1974 assert_eq!(text, "t = {}\n");
1975 }
1976
1977 #[test]
1978 fn write_inline_value_on_a_bare_empty_array_writes_the_empty_token() {
1979 let mut out = String::new();
1987 write_inline_value(&Value::Array(vec![]), &mut out);
1988 assert_eq!(out, "[]");
1989 }
1990
1991 #[test]
1992 fn line_col_reports_line_two_for_an_error_after_a_newline() {
1993 let err = read_toml("a = 1\nb = \n").unwrap_err();
1996 assert!(
1997 matches!(err, OmnistError::Parse(ref e) if e.line == 2),
1998 "got {err:?}"
1999 );
2000 }
2001
2002 #[test]
2003 fn write_toml_string_escapes_every_control_char_form() {
2004 let v = obj(vec![(
2005 "a",
2006 Value::Str("back\\slash cr\r back\u{08}space form\u{0c}feed ctl\u{01}".to_string()),
2007 )]);
2008 let doc = doc_of(v);
2009 let text = write_toml(&doc, false, None).unwrap();
2010 assert_eq!(
2011 text,
2012 "a = \"back\\\\slash cr\\r back\\bspace form\\ffeed ctl\\u0001\"\n"
2013 );
2014 let doc2 = read_toml(&text).unwrap();
2015 assert_eq!(
2016 *doc2.root().get_one("a").unwrap().value().unwrap(),
2017 Scalar::Str("back\\slash cr\r back\u{08}space form\u{0c}feed ctl\u{01}".to_string())
2018 );
2019 }
2020
2021 fn interleaved_doc() -> Doc {
2028 Doc::from_raw(crate::document::RawNode::Edges(vec![
2029 (
2030 "m".to_string(),
2031 crate::document::RawNode::Leaf(Scalar::Str("A".to_string())),
2032 ),
2033 (
2034 "x".to_string(),
2035 crate::document::RawNode::Leaf(Scalar::Str("X".to_string())),
2036 ),
2037 (
2038 "m".to_string(),
2039 crate::document::RawNode::Leaf(Scalar::Str("B".to_string())),
2040 ),
2041 ]))
2042 .unwrap()
2043 }
2044
2045 fn contiguous_repeat_doc() -> Doc {
2046 Doc::from_raw(crate::document::RawNode::Edges(vec![
2047 (
2048 "m".to_string(),
2049 crate::document::RawNode::Leaf(Scalar::Str("A".to_string())),
2050 ),
2051 (
2052 "m".to_string(),
2053 crate::document::RawNode::Leaf(Scalar::Str("B".to_string())),
2054 ),
2055 (
2056 "x".to_string(),
2057 crate::document::RawNode::Leaf(Scalar::Str("X".to_string())),
2058 ),
2059 ]))
2060 .unwrap()
2061 }
2062
2063 #[test]
2064 fn reports_interleaving_lost_on_write() {
2065 let doc = interleaved_doc();
2066 let mut report = crate::report::WriteReport::new();
2067 write_toml(&doc, false, Some(&mut report)).unwrap();
2068 let adjustments = report.adjustments();
2069 assert_eq!(adjustments.len(), 1);
2070 assert_eq!(adjustments[0].path, "$");
2071 assert_eq!(adjustments[0].code, "format.interleaving-lost");
2072 assert_eq!(adjustments[0].severity, crate::report::Severity::Warning);
2073 }
2074
2075 #[test]
2076 fn check_toml_reports_interleaving_lost() {
2077 let rep = check_toml(&interleaved_doc());
2078 assert_eq!(rep.adjustments().len(), 1);
2079 assert_eq!(rep.adjustments()[0].code, "format.interleaving-lost");
2080 }
2081
2082 #[test]
2083 fn contiguous_repeated_label_does_not_report_interleaving_lost() {
2084 let doc = contiguous_repeat_doc();
2085 let mut report = crate::report::WriteReport::new();
2086 write_toml(&doc, false, Some(&mut report)).unwrap();
2087 assert!(report.is_empty());
2088 assert!(check_toml(&doc).is_empty());
2089 }
2090}