verit 0.2.0

Exavian Veritate — zero-copy, self-describing, schema-evolvable binary serialization, safe on untrusted bytes, no unsafe, byte-identical across independent implementations.
Documentation
//! Codegen proofs: the generated identity fast path returns exactly what the
//! dynamic reader returns; the generated encoder produces bytes the dynamic
//! reader accepts; and when the writer schema differs, the generated reader
//! transparently falls back to the resolver (evolution still works).

#[rustfmt::skip]
#[path = "../examples/bench/verit_gen.rs"]
mod vg;

use verit::{
    codegen, encode, Budget, Dt, Error, Message, Resolver, SchemaBuilder, SchemaMode, Value,
};

fn sample_args<'a>(points: &'a [vg::PointArgs], codes: &'a [u32]) -> vg::LogEventArgs<'a> {
    vg::LogEventArgs {
        service: Some("checkout"),
        level: Some(2),
        message: Some("gateway timed out"),
        latency_us: Some(2_500_000),
        points: Some(points),
        codes: Some(codes),
    }
}

#[test]
fn generated_module_is_current() {
    // Committed generated code must match what the generator emits today.
    let regenerated = codegen::generate_rust(&vg::schema()).unwrap();
    let committed = include_str!("../examples/bench/verit_gen.rs");
    assert_eq!(
        regenerated.replace("\r\n", "\n"),
        committed.replace("\r\n", "\n"),
        "examples/bench/verit_gen.rs is stale — run `cargo run --example gen`"
    );
}

#[test]
fn generated_encoder_roundtrips_through_dynamic_reader() {
    let points = vec![
        vg::PointArgs { x: 1.5, y: -0.5 },
        vg::PointArgs { x: 2.0, y: 4.0 },
    ];
    let codes = vec![7u32, 11, 13];
    let bytes = vg::encode_log_event(&sample_args(&points, &codes), SchemaMode::HashOnly).unwrap();

    let schema = vg::schema();
    let resolver = Resolver::identity(&schema).unwrap();
    let msg = Message::parse(&bytes).unwrap();
    assert_eq!(msg.schema_id(), vg::SCHEMA_ID);
    let root = msg.root(&resolver).unwrap();
    assert_eq!(root.get_str(1).unwrap(), Some("checkout"));
    assert_eq!(root.get_u32(2).unwrap(), Some(2));
    assert_eq!(root.get_str(3).unwrap(), Some("gateway timed out"));
    assert_eq!(root.get_u32(4).unwrap(), Some(2_500_000));
    let pts = root.get_list(5).unwrap().unwrap();
    assert_eq!(pts.len(), 2);
    match pts.get(1).unwrap() {
        verit::Ref::Struct(p) => {
            assert_eq!(p.get_f64(1).unwrap(), Some(2.0));
            assert_eq!(p.get_f64(2).unwrap(), Some(4.0));
        }
        other => panic!("expected struct, got {}", other.kind()),
    }
    let cs = root.get_list(6).unwrap().unwrap();
    assert_eq!(cs.len(), 3);
}

#[test]
fn fast_path_matches_dynamic_reader_exactly() {
    let points = vec![vg::PointArgs { x: 9.25, y: -3.5 }];
    let codes = vec![42u32];
    let bytes = vg::encode_log_event(&sample_args(&points, &codes), SchemaMode::HashOnly).unwrap();

    let schema = vg::schema();
    let resolver = Resolver::identity(&schema).unwrap();
    let msg = Message::parse(&bytes).unwrap();

    let fast = vg::LogEventRef::read(&msg, &resolver).unwrap();
    assert!(
        matches!(fast, vg::LogEventRef::Fast { .. }),
        "identity must take the fast path"
    );
    let dynamic = msg.root(&resolver).unwrap();

    assert_eq!(fast.service().unwrap(), dynamic.get_str(1).unwrap());
    assert_eq!(fast.level().unwrap(), dynamic.get_u32(2).unwrap());
    assert_eq!(fast.message().unwrap(), dynamic.get_str(3).unwrap());
    assert_eq!(fast.latency_us().unwrap(), dynamic.get_u32(4).unwrap());
    let fp = fast.points().unwrap().unwrap();
    assert_eq!(fp.len(), 1);
    let p0 = fp.get(0).unwrap();
    assert_eq!(p0.x().unwrap(), Some(9.25));
    assert_eq!(p0.y().unwrap(), Some(-3.5));
    assert_eq!(fast.codes().unwrap().unwrap().get(0).unwrap(), 42);
}

#[test]
fn absent_fields_are_none_on_the_fast_path() {
    let args = vg::LogEventArgs {
        service: None,
        level: Some(1),
        message: None,
        latency_us: None,
        points: None,
        codes: None,
    };
    let bytes = vg::encode_log_event(&args, SchemaMode::HashOnly).unwrap();
    let schema = vg::schema();
    let resolver = Resolver::identity(&schema).unwrap();
    let msg = Message::parse(&bytes).unwrap();
    let root = vg::LogEventRef::read(&msg, &resolver).unwrap();
    assert_eq!(root.service().unwrap(), None);
    assert_eq!(root.level().unwrap(), Some(1));
    assert!(root.points().unwrap().is_none());
}

#[test]
fn evolution_falls_back_to_dynamic_transparently() {
    // A v0 writer: fewer fields, u16 latency, no Point type at all.
    let v0 = SchemaBuilder::new()
        .add_struct(
            "LogEvent",
            vec![
                (1, "service", Dt::Str),
                (4, "latency_us", Dt::U16), // widens to the generated u32
            ],
        )
        .build("LogEvent")
        .unwrap();
    let bytes = encode(
        &v0,
        &Value::Struct(vec![(1, Value::str("legacy")), (4, Value::U16(999))]),
        SchemaMode::HashOnly,
    )
    .unwrap();

    // Resolve old writer against the generated (reader) schema once.
    let reader_schema = vg::schema();
    let resolver = Resolver::new(&v0, &reader_schema).unwrap();
    let msg = Message::parse(&bytes).unwrap();
    let root = vg::LogEventRef::read(&msg, &resolver).unwrap();
    assert!(
        matches!(root, vg::LogEventRef::Dynamic(_)),
        "foreign schema id must take the dynamic path"
    );
    // Same typed API, old data: renamed/missing/widened all behave.
    assert_eq!(root.service().unwrap(), Some("legacy"));
    assert_eq!(root.latency_us().unwrap(), Some(999), "u16 widened to u32");
    assert_eq!(root.message().unwrap(), None, "field absent in v0");
    assert!(root.points().unwrap().is_none(), "list absent in v0");
}

#[test]
fn inline_mode_embeds_the_schema() {
    let bytes = vg::encode_log_event(&sample_args(&[], &[]), SchemaMode::Inline).unwrap();
    let msg = Message::parse(&bytes).unwrap();
    let recovered = msg.writer_schema().unwrap().unwrap();
    assert_eq!(recovered.id(), vg::SCHEMA_ID);
    assert!(verit::dump_json(&bytes)
        .unwrap()
        .contains("\"service\":\"checkout\""));
}

#[test]
fn bounded_fast_path_matches_unbounded_and_charges() {
    let points = vec![
        vg::PointArgs { x: 1.0, y: 2.0 },
        vg::PointArgs { x: 3.0, y: 4.0 },
    ];
    let codes = vec![5u32, 7, 9];
    let bytes = vg::encode_log_event(&sample_args(&points, &codes), SchemaMode::HashOnly).unwrap();
    let schema = vg::schema();
    let resolver = Resolver::identity(&schema).unwrap();
    let msg = Message::parse(&bytes).unwrap();

    let budget = Budget::new(msg.suggested_budget());
    let start = budget.remaining();
    let root = vg::LogEventRef::read_bounded(&msg, &resolver, &budget).unwrap();
    assert!(
        matches!(root, vg::LogEventRef::Fast { .. }),
        "identity must stay on the fast path"
    );

    // Same values as the unbounded fast path, and offset-follows charged.
    assert_eq!(root.service().unwrap(), Some("checkout"));
    assert_eq!(root.latency_us().unwrap(), Some(2_500_000));
    let pts = root.points().unwrap().unwrap();
    assert_eq!(pts.get(1).unwrap().y().unwrap(), Some(4.0));
    let cs = root.codes().unwrap().unwrap();
    assert_eq!(cs.get(2).unwrap(), 9);
    // Bulk paths still work bounded (region charged at open, no per-element cost).
    assert_eq!(cs.values().unwrap().sum::<u32>(), 21);
    assert!(
        budget.remaining() < start,
        "offset-follows must charge the budget"
    );
}

#[test]
fn bounded_fast_path_trips_on_hostile_list_count() {
    let points = vec![vg::PointArgs { x: 1.0, y: 2.0 }];
    let codes = vec![5u32];
    let mut bytes =
        vg::encode_log_event(&sample_args(&points, &codes), SchemaMode::HashOnly).unwrap();
    let schema = vg::schema();
    let resolver = Resolver::identity(&schema).unwrap();

    // Forge a ~4-billion-element codes list: overwrite the list header count.
    // Layout: sparse LogEvent root — bitmap byte at +0, codes offset slot at
    // +24 (see the generated getters); the u32 at that offset is the count.
    let root_off = Message::parse(&bytes).unwrap().root_offset() as usize;
    let list_off =
        u32::from_le_bytes(bytes[root_off + 24..root_off + 28].try_into().unwrap()) as usize;
    bytes[list_off..list_off + 4].copy_from_slice(&u32::MAX.to_le_bytes());

    let msg = Message::parse(&bytes).unwrap();
    // Bounded: opening the list charges count*stride, which blows the budget —
    // a typed error before any element work happens.
    let budget = Budget::new(msg.suggested_budget());
    let root = vg::LogEventRef::read_bounded(&msg, &resolver, &budget).unwrap();
    match root.codes() {
        Err(Error::TraversalBudgetExceeded) => {}
        Err(other) => panic!("expected TraversalBudgetExceeded, got {other}"),
        Ok(_) => panic!("hostile count must trip the budget"),
    }
    // Unbounded stays lazy and memory-safe: the header opens; element reads
    // past the buffer are bounds-checked errors, never OOB.
    let free = vg::LogEventRef::read(&msg, &resolver).unwrap();
    let list = free.codes().unwrap().unwrap();
    assert!(list.get(1_000_000).is_err());
}

#[test]
fn codegen_rejects_unsupported_shapes() {
    let s = SchemaBuilder::new()
        .add_struct("S", vec![(1, "m", Dt::list(Dt::list(Dt::U8)))])
        .build("S")
        .unwrap();
    assert!(codegen::generate_rust(&s).is_err());

    let bad_name = SchemaBuilder::new()
        .add_struct("S", vec![(1, "match", Dt::U8)])
        .build("S")
        .unwrap();
    assert!(codegen::generate_rust(&bad_name).is_err());
}