Skip to main content

verit_core/
dump.rs

1//! The self-description proof: given message bytes and *nothing else*,
2//! recover the writer schema from the inline region and render every present
3//! field — with its human-readable name — as JSON.
4
5use std::fmt::Write as _;
6
7use crate::error::{Error, Result};
8use crate::message::{Budget, Message, Ref, StructReader};
9use crate::resolve::Resolver;
10use crate::schema::{Schema, Type};
11
12/// Depth ceiling for `dump_json`. A malicious message can contain an offset
13/// cycle (a struct field pointing back to an ancestor block), which would
14/// otherwise recurse forever; this bounds it. Far deeper than any real data.
15const MAX_DUMP_DEPTH: u32 = 128;
16
17/// Decode a message using only its own bytes. Requires the message to have
18/// been encoded with [`crate::SchemaMode::Inline`].
19pub fn dump_json(buf: &[u8]) -> Result<String> {
20    let msg = Message::parse(buf)?;
21    let schema = msg.writer_schema()?.ok_or(Error::NoInlineSchema)?;
22    dump_with_schema(&msg, &schema)
23}
24
25/// Render a **hash-only** message using a writer schema supplied from outside
26/// the message — a registry, or a `.verit` file's schema section. Same output
27/// as [`dump_json`]; the difference is only where the schema came from.
28///
29/// Records inside a `.verit` file are hash-only by design (the file stores each
30/// schema once), so this is the entry point the file layer and `verit cat` use.
31/// The schema id is checked against the message's, so a mismatched schema is
32/// refused rather than used to misread the bytes.
33pub fn dump_json_with(schema: &Schema, buf: &[u8]) -> Result<String> {
34    let msg = Message::parse(buf)?;
35    if msg.schema_id() != schema.id() {
36        return Err(Error::SchemaIdMismatch {
37            message: msg.schema_id(),
38            expected: schema.id(),
39        });
40    }
41    dump_with_schema(&msg, schema)
42}
43
44fn dump_with_schema(msg: &Message<'_>, schema: &Schema) -> Result<String> {
45    let resolver = Resolver::identity(schema)?;
46    // `dump_json` walks the entire message, so it is bounded by default: a
47    // crafted offset-aliasing message trips TraversalBudgetExceeded rather than
48    // doing work super-linear in the buffer. The depth limit below still guards
49    // forged offset *cycles*; the budget guards *wide* amplification.
50    let budget = Budget::new(msg.suggested_budget());
51    let root = msg.root_bounded(&resolver, &budget)?;
52    let mut out = String::new();
53    write_struct(&mut out, schema, &root, 0)?;
54    Ok(out)
55}
56
57fn write_struct(out: &mut String, schema: &Schema, sr: &StructReader, depth: u32) -> Result<()> {
58    if depth > MAX_DUMP_DEPTH {
59        return Err(Error::DepthLimitExceeded);
60    }
61    out.push('{');
62    let mut first = true;
63    for field in &sr.struct_def().fields {
64        // `get_or_default` so a field with a custom default shows its effective
65        // value in the self-description even when absent on the wire.
66        if let Some(value) = sr.get_or_default(field.id)? {
67            if !first {
68                out.push(',');
69            }
70            first = false;
71            json_string(out, &field.name);
72            out.push(':');
73            write_value(out, schema, &field.ty, &value, depth)?;
74        }
75    }
76    out.push('}');
77    Ok(())
78}
79
80fn write_value(
81    out: &mut String,
82    schema: &Schema,
83    ty: &Type,
84    value: &Ref,
85    depth: u32,
86) -> Result<()> {
87    if depth > MAX_DUMP_DEPTH {
88        return Err(Error::DepthLimitExceeded);
89    }
90    match value {
91        Ref::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
92        Ref::U8(x) => {
93            let _ = write!(out, "{x}");
94        }
95        Ref::U16(x) => {
96            let _ = write!(out, "{x}");
97        }
98        Ref::U32(x) => {
99            let _ = write!(out, "{x}");
100        }
101        Ref::U64(x) => {
102            let _ = write!(out, "{x}");
103        }
104        Ref::I8(x) => {
105            let _ = write!(out, "{x}");
106        }
107        Ref::I16(x) => {
108            let _ = write!(out, "{x}");
109        }
110        Ref::I32(x) => {
111            let _ = write!(out, "{x}");
112        }
113        Ref::I64(x) => {
114            let _ = write!(out, "{x}");
115        }
116        Ref::F32(x) => write_float(out, f64::from(*x)),
117        Ref::F64(x) => write_float(out, *x),
118        Ref::Str(s) => json_string(out, s),
119        Ref::Bytes(b) => {
120            out.push('"');
121            for byte in *b {
122                let _ = write!(out, "{byte:02x}");
123            }
124            out.push('"');
125        }
126        Ref::Enum(v) => {
127            let name = match ty {
128                Type::Enum(i) => schema.enum_def_unchecked(*i).name_of(*v),
129                _ => None,
130            };
131            match name {
132                Some(n) => json_string(out, n),
133                None => {
134                    let _ = write!(out, "{v}");
135                }
136            }
137        }
138        Ref::Struct(sr) => write_struct(out, schema, sr, depth + 1)?,
139        Ref::List(lr) => {
140            let elem_ty = match ty {
141                Type::List(e) => e.as_ref(),
142                _ => return Err(Error::Internal("list value with non-list schema type")),
143            };
144            out.push('[');
145            for i in 0..lr.len() {
146                if i > 0 {
147                    out.push(',');
148                }
149                let elem = lr.get(i)?;
150                write_value(out, schema, elem_ty, &elem, depth + 1)?;
151            }
152            out.push(']');
153        }
154        Ref::Map(mr) => {
155            // A map renders as a JSON object; keys are stringified (JSON object
156            // keys must be strings) and, being canonical-sorted, come out ordered.
157            let val_ty = match ty {
158                Type::Map(_, v) => v.as_ref(),
159                _ => return Err(Error::Internal("map value with non-map schema type")),
160            };
161            out.push('{');
162            for i in 0..mr.len() {
163                if i > 0 {
164                    out.push(',');
165                }
166                let (k, v) = mr.get(i)?;
167                write_map_key(out, &k);
168                out.push(':');
169                write_value(out, schema, val_ty, &v, depth + 1)?;
170            }
171            out.push('}');
172        }
173        Ref::Union(u) => {
174            // A union renders as a single-entry object: the variant tag (as a
175            // string key) to the variant's value.
176            let variants = match ty {
177                Type::Union(vs) => vs,
178                _ => return Err(Error::Internal("union value with non-union schema type")),
179            };
180            let tag = u.tag();
181            let vty = variants.get(tag as usize).ok_or(Error::BadUnionTag(tag))?;
182            out.push('{');
183            json_string(out, &tag.to_string());
184            out.push(':');
185            write_value(out, schema, vty, &u.value()?, depth + 1)?;
186            out.push('}');
187        }
188    }
189    Ok(())
190}
191
192/// Render a map key as a JSON string (JSON object keys are always strings).
193fn write_map_key(out: &mut String, key: &Ref) {
194    match key {
195        Ref::Str(s) => json_string(out, s),
196        Ref::Bool(b) => json_string(out, if *b { "true" } else { "false" }),
197        Ref::U8(x) => json_string(out, &x.to_string()),
198        Ref::U16(x) => json_string(out, &x.to_string()),
199        Ref::U32(x) => json_string(out, &x.to_string()),
200        Ref::U64(x) => json_string(out, &x.to_string()),
201        Ref::I8(x) => json_string(out, &x.to_string()),
202        Ref::I16(x) => json_string(out, &x.to_string()),
203        Ref::I32(x) => json_string(out, &x.to_string()),
204        Ref::I64(x) => json_string(out, &x.to_string()),
205        Ref::Enum(x) => json_string(out, &x.to_string()),
206        // Not a valid key type (schema validation forbids it); render defensively.
207        _ => json_string(out, key.kind()),
208    }
209}
210
211fn write_float(out: &mut String, x: f64) {
212    if x.is_finite() {
213        let _ = write!(out, "{x}");
214    } else {
215        // JSON has no NaN/Infinity.
216        out.push_str("null");
217    }
218}
219
220fn json_string(out: &mut String, s: &str) {
221    out.push('"');
222    for c in s.chars() {
223        match c {
224            '"' => out.push_str("\\\""),
225            '\\' => out.push_str("\\\\"),
226            '\n' => out.push_str("\\n"),
227            '\r' => out.push_str("\\r"),
228            '\t' => out.push_str("\\t"),
229            c if (c as u32) < 0x20 => {
230                let _ = write!(out, "\\u{:04x}", c as u32);
231            }
232            c => out.push(c),
233        }
234    }
235    out.push('"');
236}