nextjson 0.1.0

A dependency-free, no_std JSON and CBOR library with a schema-driven, visitor-free design.
Documentation
use nextjson::cross_format::{self, CborSink, EventSink};
use nextjson::{Error, Number, Result, Value, Write};

#[test]
fn nested_json_cbor_json_round_trip() {
    let expected = nextjson::json!({
        "name": "cross-format",
        "enabled": true,
        "ratio": -12.5,
        "values": [null, 0, 1, -7, "text", {"nested": [1, 2, 3]}]
    });
    let json = nextjson::nextencode(&expected).unwrap();
    let cbor = cross_format::json_to_cbor(&json).unwrap();
    let output = cross_format::cbor_to_json(&cbor).unwrap();
    let actual: Value = nextjson::nextdecode(&output).unwrap();
    assert_eq!(actual, expected);
}

#[test]
fn rust_128_bit_integer_domain_round_trips_through_cbor_bignums() {
    let json = nextjson::nextencode(&(u128::MAX, i128::MIN)).unwrap();
    let cbor = cross_format::json_to_cbor(&json).unwrap();
    assert!(cbor.contains(&0xc2));
    assert!(cbor.contains(&0xc3));
    let output = cross_format::cbor_to_json(&cbor).unwrap();
    assert_eq!(
        nextjson::nextdecode::<(u128, i128)>(&output).unwrap(),
        (u128::MAX, i128::MIN)
    );
}

#[test]
fn accepts_rfc_8949_definite_containers_and_half_float() {
    let document = [
        0xa3, 0x61, b'a', 0x01, 0x61, b'b', 0x82, 0xf5, 0xf6, 0x61, b'c', 0xf9, 0x3e, 0x00,
    ];
    let json = cross_format::cbor_to_json(&document).unwrap();
    let value: Value = nextjson::nextdecode(&json).unwrap();
    assert_eq!(
        value,
        nextjson::json!({"a": 1, "b": [true, null], "c": 1.5})
    );
}

#[test]
fn accepts_indefinite_text_chunks() {
    let document = [0x7f, 0x62, b'h', b'e', 0x63, b'l', b'l', b'o', 0xff];
    assert_eq!(
        cross_format::cbor_to_json(&document).unwrap(),
        br#""hello""#
    );
}

#[test]
fn rejects_cbor_values_that_json_cannot_represent() {
    for document in [
        &[0x41, 0x00][..],
        &[0xa1, 0x01, 0x02][..],
        &[0xf9, 0x7c, 0x00][..],
        &[0xc4, 0x01][..],
    ] {
        assert!(cross_format::cbor_to_json(document).is_err());
    }
}

#[test]
fn rejects_malformed_trailing_and_overdeep_cbor() {
    for document in [
        &[0x9f][..],
        &[0xff][..],
        &[0x01, 0x02][..],
        &[0x7f, 0xff, 0xff][..],
    ] {
        assert!(cross_format::cbor_to_json(document).is_err());
    }

    let nested = [0x9f, 0x9f, 0x01, 0xff, 0xff];
    let mut sink = nextjson::cross_format::JsonSink::new(Vec::new());
    assert!(cross_format::cbor_into_with_max_depth(&nested, 1, &mut sink).is_err());
}

struct RejectWriter;

impl Write for RejectWriter {
    fn write_all(&mut self, _buf: &[u8]) -> Result<()> {
        Err(Error::custom("intentional writer failure"))
    }
}

#[test]
fn cross_format_writer_errors_are_propagated() {
    let input = vec![b'x'; 9000];
    let json = nextjson::nextencode(&String::from_utf8(input).unwrap()).unwrap();
    assert!(cross_format::json_to_cbor_writer(&json, RejectWriter).is_err());
}

#[test]
fn event_sinks_reject_invalid_structure_order() {
    let mut sink = CborSink::new(Vec::new());
    assert!(sink.object_key("outside").is_err());

    let mut sink = CborSink::new(Vec::new());
    sink.begin_object().unwrap();
    sink.object_key("missing").unwrap();
    assert!(sink.end_object().is_err());

    let mut sink = CborSink::new(Vec::new());
    sink.null().unwrap();
    assert!(sink.boolean(true).is_err());
}

struct BorrowProbe {
    start: usize,
    end: usize,
    observed_borrow: bool,
}

impl BorrowProbe {
    fn reject(&self) -> Result<()> {
        Err(Error::custom("unexpected event for borrow probe"))
    }
}

impl EventSink for BorrowProbe {
    fn null(&mut self) -> Result<()> {
        self.reject()
    }

    fn boolean(&mut self, _value: bool) -> Result<()> {
        self.reject()
    }

    fn number(&mut self, _value: Number) -> Result<()> {
        self.reject()
    }

    fn string(&mut self, value: &str) -> Result<()> {
        let pointer = value.as_ptr() as usize;
        self.observed_borrow = pointer >= self.start && pointer < self.end;
        Ok(())
    }

    fn begin_array(&mut self) -> Result<()> {
        self.reject()
    }

    fn end_array(&mut self) -> Result<()> {
        self.reject()
    }

    fn begin_object(&mut self) -> Result<()> {
        self.reject()
    }

    fn object_key(&mut self, _key: &str) -> Result<()> {
        self.reject()
    }

    fn end_object(&mut self) -> Result<()> {
        self.reject()
    }
}

#[test]
fn unescaped_json_and_definite_cbor_strings_are_forwarded_by_borrow() {
    let json = br#""borrowed-json""#;
    let mut json_probe = BorrowProbe {
        start: json.as_ptr() as usize,
        end: json.as_ptr() as usize + json.len(),
        observed_borrow: false,
    };
    cross_format::json_into(json, &mut json_probe).unwrap();
    assert!(json_probe.observed_borrow);

    let cbor = [
        0x6d, b'b', b'o', b'r', b'r', b'o', b'w', b'e', b'd', b'-', b'c', b'b', b'o', b'r',
    ];
    let mut cbor_probe = BorrowProbe {
        start: cbor.as_ptr() as usize,
        end: cbor.as_ptr() as usize + cbor.len(),
        observed_borrow: false,
    };
    cross_format::cbor_into(&cbor, &mut cbor_probe).unwrap();
    assert!(cbor_probe.observed_borrow);
}

struct DeterministicRng(u64);

impl DeterministicRng {
    fn next(&mut self) -> u64 {
        self.0 = self
            .0
            .wrapping_mul(6_364_136_223_846_793_005)
            .wrapping_add(1_442_695_040_888_963_407);
        self.0
    }
}

fn generated_value(rng: &mut DeterministicRng, depth: u8) -> Value {
    let kind = if depth == 0 {
        rng.next() % 5
    } else {
        rng.next() % 7
    };
    match kind {
        0 => Value::Null,
        1 => Value::Bool(rng.next() & 1 == 0),
        2 => Value::from(rng.next() as i64),
        3 => Value::from((rng.next() % 1_000_000) as f64 / 17.0 - 20_000.0),
        4 => {
            const TEXT: &[&str] = &["plain", "quote\"slash\\", "line\nfeed", "unicode-δΈ­", ""];
            Value::from(TEXT[(rng.next() as usize) % TEXT.len()])
        }
        5 => {
            let length = (rng.next() % 5) as usize;
            Value::Array(
                (0..length)
                    .map(|_| generated_value(rng, depth - 1))
                    .collect(),
            )
        }
        _ => {
            let length = (rng.next() % 5) as usize;
            let mut object = nextjson::Map::new();
            for index in 0..length {
                object.insert(
                    format!("key-{index}-{}", rng.next() % 11),
                    generated_value(rng, depth - 1),
                );
            }
            Value::Object(object)
        }
    }
}

#[test]
fn deterministic_cross_format_corpus_preserves_semantics() {
    let mut rng = DeterministicRng(0x4e65_7874_4a73_6f6e);
    for case in 0..1_000 {
        let expected = generated_value(&mut rng, 4);
        let json = nextjson::nextencode(&expected).unwrap();
        let cbor = cross_format::json_to_cbor(&json).unwrap();
        let output = cross_format::cbor_to_json(&cbor).unwrap();
        let actual: Value = nextjson::nextdecode(&output).unwrap();
        assert_eq!(actual, expected, "cross-format mismatch in case {case}");
    }
}