verit-core 0.2.0

Internal: portable core engine for Exavian Veritate. Not a public API — depend on `verit`.
Documentation
//! The self-description proof: given message bytes and *nothing else*,
//! recover the writer schema from the inline region and render every present
//! field — with its human-readable name — as JSON.

use std::fmt::Write as _;

use crate::error::{Error, Result};
use crate::message::{Budget, Message, Ref, StructReader};
use crate::resolve::Resolver;
use crate::schema::{Schema, Type};

/// Depth ceiling for `dump_json`. A malicious message can contain an offset
/// cycle (a struct field pointing back to an ancestor block), which would
/// otherwise recurse forever; this bounds it. Far deeper than any real data.
const MAX_DUMP_DEPTH: u32 = 128;

/// Decode a message using only its own bytes. Requires the message to have
/// been encoded with [`crate::SchemaMode::Inline`].
pub fn dump_json(buf: &[u8]) -> Result<String> {
    let msg = Message::parse(buf)?;
    let schema = msg.writer_schema()?.ok_or(Error::NoInlineSchema)?;
    dump_with_schema(&msg, &schema)
}

/// Render a **hash-only** message using a writer schema supplied from outside
/// the message — a registry, or a `.verit` file's schema section. Same output
/// as [`dump_json`]; the difference is only where the schema came from.
///
/// Records inside a `.verit` file are hash-only by design (the file stores each
/// schema once), so this is the entry point the file layer and `verit cat` use.
/// The schema id is checked against the message's, so a mismatched schema is
/// refused rather than used to misread the bytes.
pub fn dump_json_with(schema: &Schema, buf: &[u8]) -> Result<String> {
    let msg = Message::parse(buf)?;
    if msg.schema_id() != schema.id() {
        return Err(Error::SchemaIdMismatch {
            message: msg.schema_id(),
            expected: schema.id(),
        });
    }
    dump_with_schema(&msg, schema)
}

fn dump_with_schema(msg: &Message<'_>, schema: &Schema) -> Result<String> {
    let resolver = Resolver::identity(schema)?;
    // `dump_json` walks the entire message, so it is bounded by default: a
    // crafted offset-aliasing message trips TraversalBudgetExceeded rather than
    // doing work super-linear in the buffer. The depth limit below still guards
    // forged offset *cycles*; the budget guards *wide* amplification.
    let budget = Budget::new(msg.suggested_budget());
    let root = msg.root_bounded(&resolver, &budget)?;
    let mut out = String::new();
    write_struct(&mut out, schema, &root, 0)?;
    Ok(out)
}

fn write_struct(out: &mut String, schema: &Schema, sr: &StructReader, depth: u32) -> Result<()> {
    if depth > MAX_DUMP_DEPTH {
        return Err(Error::DepthLimitExceeded);
    }
    out.push('{');
    let mut first = true;
    for field in &sr.struct_def().fields {
        // `get_or_default` so a field with a custom default shows its effective
        // value in the self-description even when absent on the wire.
        if let Some(value) = sr.get_or_default(field.id)? {
            if !first {
                out.push(',');
            }
            first = false;
            json_string(out, &field.name);
            out.push(':');
            write_value(out, schema, &field.ty, &value, depth)?;
        }
    }
    out.push('}');
    Ok(())
}

fn write_value(
    out: &mut String,
    schema: &Schema,
    ty: &Type,
    value: &Ref,
    depth: u32,
) -> Result<()> {
    if depth > MAX_DUMP_DEPTH {
        return Err(Error::DepthLimitExceeded);
    }
    match value {
        Ref::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
        Ref::U8(x) => {
            let _ = write!(out, "{x}");
        }
        Ref::U16(x) => {
            let _ = write!(out, "{x}");
        }
        Ref::U32(x) => {
            let _ = write!(out, "{x}");
        }
        Ref::U64(x) => {
            let _ = write!(out, "{x}");
        }
        Ref::I8(x) => {
            let _ = write!(out, "{x}");
        }
        Ref::I16(x) => {
            let _ = write!(out, "{x}");
        }
        Ref::I32(x) => {
            let _ = write!(out, "{x}");
        }
        Ref::I64(x) => {
            let _ = write!(out, "{x}");
        }
        Ref::F32(x) => write_float(out, f64::from(*x)),
        Ref::F64(x) => write_float(out, *x),
        Ref::Str(s) => json_string(out, s),
        Ref::Bytes(b) => {
            out.push('"');
            for byte in *b {
                let _ = write!(out, "{byte:02x}");
            }
            out.push('"');
        }
        Ref::Enum(v) => {
            let name = match ty {
                Type::Enum(i) => schema.enum_def_unchecked(*i).name_of(*v),
                _ => None,
            };
            match name {
                Some(n) => json_string(out, n),
                None => {
                    let _ = write!(out, "{v}");
                }
            }
        }
        Ref::Struct(sr) => write_struct(out, schema, sr, depth + 1)?,
        Ref::List(lr) => {
            let elem_ty = match ty {
                Type::List(e) => e.as_ref(),
                _ => return Err(Error::Internal("list value with non-list schema type")),
            };
            out.push('[');
            for i in 0..lr.len() {
                if i > 0 {
                    out.push(',');
                }
                let elem = lr.get(i)?;
                write_value(out, schema, elem_ty, &elem, depth + 1)?;
            }
            out.push(']');
        }
        Ref::Map(mr) => {
            // A map renders as a JSON object; keys are stringified (JSON object
            // keys must be strings) and, being canonical-sorted, come out ordered.
            let val_ty = match ty {
                Type::Map(_, v) => v.as_ref(),
                _ => return Err(Error::Internal("map value with non-map schema type")),
            };
            out.push('{');
            for i in 0..mr.len() {
                if i > 0 {
                    out.push(',');
                }
                let (k, v) = mr.get(i)?;
                write_map_key(out, &k);
                out.push(':');
                write_value(out, schema, val_ty, &v, depth + 1)?;
            }
            out.push('}');
        }
        Ref::Union(u) => {
            // A union renders as a single-entry object: the variant tag (as a
            // string key) to the variant's value.
            let variants = match ty {
                Type::Union(vs) => vs,
                _ => return Err(Error::Internal("union value with non-union schema type")),
            };
            let tag = u.tag();
            let vty = variants.get(tag as usize).ok_or(Error::BadUnionTag(tag))?;
            out.push('{');
            json_string(out, &tag.to_string());
            out.push(':');
            write_value(out, schema, vty, &u.value()?, depth + 1)?;
            out.push('}');
        }
    }
    Ok(())
}

/// Render a map key as a JSON string (JSON object keys are always strings).
fn write_map_key(out: &mut String, key: &Ref) {
    match key {
        Ref::Str(s) => json_string(out, s),
        Ref::Bool(b) => json_string(out, if *b { "true" } else { "false" }),
        Ref::U8(x) => json_string(out, &x.to_string()),
        Ref::U16(x) => json_string(out, &x.to_string()),
        Ref::U32(x) => json_string(out, &x.to_string()),
        Ref::U64(x) => json_string(out, &x.to_string()),
        Ref::I8(x) => json_string(out, &x.to_string()),
        Ref::I16(x) => json_string(out, &x.to_string()),
        Ref::I32(x) => json_string(out, &x.to_string()),
        Ref::I64(x) => json_string(out, &x.to_string()),
        Ref::Enum(x) => json_string(out, &x.to_string()),
        // Not a valid key type (schema validation forbids it); render defensively.
        _ => json_string(out, key.kind()),
    }
}

fn write_float(out: &mut String, x: f64) {
    if x.is_finite() {
        let _ = write!(out, "{x}");
    } else {
        // JSON has no NaN/Infinity.
        out.push_str("null");
    }
}

fn json_string(out: &mut String, s: &str) {
    out.push('"');
    for c in s.chars() {
        match c {
            '"' => out.push_str("\\\""),
            '\\' => out.push_str("\\\\"),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            c if (c as u32) < 0x20 => {
                let _ = write!(out, "\\u{:04x}", c as u32);
            }
            c => out.push(c),
        }
    }
    out.push('"');
}