tomlproc 0.1.2

A self-contained TOML 1.1.0 parser and serializer with no external dependencies
Documentation
//! A deterministic smoke fuzzer.
//!
//! The parser must answer every input with a value or an error, never a panic
//! -- no out-of-bounds index, no slice landing inside a multi-byte character,
//! no arithmetic overflow. Whatever it does accept must also survive a
//! serialize/re-parse round-trip.

#![cfg(feature = "alloc")]

use tomlproc::{parse, to_string, to_string_pretty};

/// xorshift64*, so a failure reproduces from its seed alone.
struct Rng(u64);

impl Rng {
    fn next(&mut self) -> u64 {
        self.0 ^= self.0 >> 12;
        self.0 ^= self.0 << 25;
        self.0 ^= self.0 >> 27;
        self.0.wrapping_mul(0x2545_F491_4F6C_DD1D)
    }

    fn below(&mut self, bound: usize) -> usize {
        (self.next() % bound as u64) as usize
    }

    fn pick<T: Copy>(&mut self, choices: &[T]) -> T {
        choices[self.below(choices.len())]
    }
}

/// Fragments chosen to sit right on the parser's decision points.
const PIECES: &[&str] = &[
    "a",
    "b.c",
    "\"q\"",
    "'l'",
    "=",
    " ",
    "\t",
    "\n",
    "\r\n",
    "\r",
    "#c",
    ".",
    ",",
    "[",
    "]",
    "[[",
    "]]",
    "{",
    "}",
    "\"",
    "'",
    "\"\"\"",
    "'''",
    "\\",
    "\\u00e9",
    "\\U0001F600",
    "\\q",
    "1",
    "0x1",
    "0b_",
    "1_0",
    "01",
    "1.",
    ".5",
    "1e",
    "+",
    "-",
    "inf",
    "nan",
    "true",
    "false",
    "1979-05-27",
    "07:32:00",
    "1979-05-27T07:32:00Z",
    "1979-05-27 07:32:00",
    "é",
    "\u{1}",
    "\u{feff}",
    "\u{7f}",
    "9223372036854775808",
    "x = ",
    "[t]",
    "[[t]]",
    // TOML 1.1 additions.
    "\\x41",
    "\\xzz",
    "\\e",
    "07:32",
    "2010-02-03 14:15",
    "{ a = 1, }",
    "{\n",
    ",\n}",
];

#[test]
fn never_panics_and_round_trips() {
    let mut rng = Rng(0x5EED_1979_0527_0732);
    for _ in 0..40_000 {
        let mut input = String::new();
        for _ in 0..rng.below(24) {
            input.push_str(rng.pick(PIECES));
        }
        // Any outcome is fine as long as it is an outcome.
        let Ok(table) = parse(&input) else { continue };
        let written = to_string(&table);
        let reparsed = parse(&written)
            .unwrap_or_else(|e| panic!("input {input:?} wrote {written:?} which failed: {e}"));
        // Compare the serialized forms: a document holding a NaN is never
        // equal to itself.
        assert_eq!(written, to_string(&reparsed), "input {input:?}");

        let pretty = to_string_pretty(&table);
        let reparsed = parse(&pretty).unwrap_or_else(|e| {
            panic!("input {input:?} pretty-wrote {pretty:?} which failed: {e}")
        });
        assert_eq!(
            written,
            to_string(&reparsed),
            "input {input:?} pretty-wrote {pretty:?}"
        );
    }
}

#[test]
fn never_panics_on_arbitrary_bytes() {
    let mut rng = Rng(0x1234_5678_9ABC_DEF0);
    for _ in 0..40_000 {
        let bytes: Vec<u8> = (0..rng.below(48)).map(|_| rng.below(256) as u8).collect();
        // Only valid UTF-8 can reach the parser, which takes a `&str`.
        if let Ok(input) = str::from_utf8(&bytes) {
            let _ = parse(input);
        }
    }
}

#[test]
fn truncations_of_a_real_document_never_panic() {
    let document = include_str!("../README.md");
    for end in 0..document.len() {
        if document.is_char_boundary(end) {
            let _ = parse(&document[..end]);
        }
    }
}

/// Recording spans must not change what the parser accepts, and every span it
/// records must be a real, character-aligned slice of the input.
#[test]
fn spans_are_always_valid_slices() {
    let mut rng = Rng(0x5A11_0000_1979_0527);
    for _ in 0..20_000 {
        let mut input = String::new();
        for _ in 0..rng.below(24) {
            input.push_str(rng.pick(PIECES));
        }
        let plain = parse(&input);
        let spanned = tomlproc::parse_spans(&input);
        assert_eq!(plain.is_ok(), spanned.is_ok(), "input {input:?}");
        let Ok((table, spans)) = spanned else {
            continue;
        };

        for (path, span) in spans.iter() {
            // Slicing panics unless the range is in bounds and lands on
            // character boundaries.
            let text = &input[span.value.clone()];
            let whole = &input[span.range.clone()];
            assert!(whole.contains(text), "input {input:?}: {path}");
            assert!(
                span.line >= 1 && span.column >= 1,
                "input {input:?}: {path}"
            );
            assert!(
                table.get_path(path).is_some(),
                "input {input:?}: {path} is not in the document"
            );
        }
    }
}

/// Whatever the parser accepts must survive a trip through serde unchanged.
#[cfg(feature = "serde")]
#[test]
fn serde_round_trips_what_the_parser_accepts() {
    let mut rng = Rng(0x5E12_DE00_1979_0527);
    for _ in 0..20_000 {
        let mut input = String::new();
        for _ in 0..rng.below(24) {
            input.push_str(rng.pick(PIECES));
        }
        let Ok(table) = parse(&input) else { continue };

        // Compared as text throughout: a document holding a NaN is never equal
        // to itself.
        let value = tomlproc::serde::to_value(&table).expect("a table always serializes");
        let serialized = value.as_table().expect("a table serializes as a table");
        assert_eq!(to_string(serialized), to_string(&table), "input {input:?}");

        let back: tomlproc::Table =
            tomlproc::serde::from_value(value).expect("a table deserializes");
        assert_eq!(to_string(&back), to_string(&table), "input {input:?}");

        let text = tomlproc::serde::to_string(&table).expect("a table serializes to a document");
        assert_eq!(text, to_string(&table), "input {input:?}");
    }
}