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    let resolver = Resolver::identity(&schema)?;
23    // `dump_json` walks the entire message, so it is bounded by default: a
24    // crafted offset-aliasing message trips TraversalBudgetExceeded rather than
25    // doing work super-linear in the buffer. The depth limit below still guards
26    // forged offset *cycles*; the budget guards *wide* amplification.
27    let budget = Budget::new(msg.suggested_budget());
28    let root = msg.root_bounded(&resolver, &budget)?;
29    let mut out = String::new();
30    write_struct(&mut out, &schema, &root, 0)?;
31    Ok(out)
32}
33
34fn write_struct(out: &mut String, schema: &Schema, sr: &StructReader, depth: u32) -> Result<()> {
35    if depth > MAX_DUMP_DEPTH {
36        return Err(Error::DepthLimitExceeded);
37    }
38    out.push('{');
39    let mut first = true;
40    for field in &sr.struct_def().fields {
41        // `get_or_default` so a field with a custom default shows its effective
42        // value in the self-description even when absent on the wire.
43        if let Some(value) = sr.get_or_default(field.id)? {
44            if !first {
45                out.push(',');
46            }
47            first = false;
48            json_string(out, &field.name);
49            out.push(':');
50            write_value(out, schema, &field.ty, &value, depth)?;
51        }
52    }
53    out.push('}');
54    Ok(())
55}
56
57fn write_value(
58    out: &mut String,
59    schema: &Schema,
60    ty: &Type,
61    value: &Ref,
62    depth: u32,
63) -> Result<()> {
64    if depth > MAX_DUMP_DEPTH {
65        return Err(Error::DepthLimitExceeded);
66    }
67    match value {
68        Ref::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
69        Ref::U8(x) => {
70            let _ = write!(out, "{x}");
71        }
72        Ref::U16(x) => {
73            let _ = write!(out, "{x}");
74        }
75        Ref::U32(x) => {
76            let _ = write!(out, "{x}");
77        }
78        Ref::U64(x) => {
79            let _ = write!(out, "{x}");
80        }
81        Ref::I8(x) => {
82            let _ = write!(out, "{x}");
83        }
84        Ref::I16(x) => {
85            let _ = write!(out, "{x}");
86        }
87        Ref::I32(x) => {
88            let _ = write!(out, "{x}");
89        }
90        Ref::I64(x) => {
91            let _ = write!(out, "{x}");
92        }
93        Ref::F32(x) => write_float(out, f64::from(*x)),
94        Ref::F64(x) => write_float(out, *x),
95        Ref::Str(s) => json_string(out, s),
96        Ref::Bytes(b) => {
97            out.push('"');
98            for byte in *b {
99                let _ = write!(out, "{byte:02x}");
100            }
101            out.push('"');
102        }
103        Ref::Enum(v) => {
104            let name = match ty {
105                Type::Enum(i) => schema.enum_def_unchecked(*i).name_of(*v),
106                _ => None,
107            };
108            match name {
109                Some(n) => json_string(out, n),
110                None => {
111                    let _ = write!(out, "{v}");
112                }
113            }
114        }
115        Ref::Struct(sr) => write_struct(out, schema, sr, depth + 1)?,
116        Ref::List(lr) => {
117            let elem_ty = match ty {
118                Type::List(e) => e.as_ref(),
119                _ => return Err(Error::Internal("list value with non-list schema type")),
120            };
121            out.push('[');
122            for i in 0..lr.len() {
123                if i > 0 {
124                    out.push(',');
125                }
126                let elem = lr.get(i)?;
127                write_value(out, schema, elem_ty, &elem, depth + 1)?;
128            }
129            out.push(']');
130        }
131        Ref::Map(mr) => {
132            // A map renders as a JSON object; keys are stringified (JSON object
133            // keys must be strings) and, being canonical-sorted, come out ordered.
134            let val_ty = match ty {
135                Type::Map(_, v) => v.as_ref(),
136                _ => return Err(Error::Internal("map value with non-map schema type")),
137            };
138            out.push('{');
139            for i in 0..mr.len() {
140                if i > 0 {
141                    out.push(',');
142                }
143                let (k, v) = mr.get(i)?;
144                write_map_key(out, &k);
145                out.push(':');
146                write_value(out, schema, val_ty, &v, depth + 1)?;
147            }
148            out.push('}');
149        }
150        Ref::Union(u) => {
151            // A union renders as a single-entry object: the variant tag (as a
152            // string key) to the variant's value.
153            let variants = match ty {
154                Type::Union(vs) => vs,
155                _ => return Err(Error::Internal("union value with non-union schema type")),
156            };
157            let tag = u.tag();
158            let vty = variants.get(tag as usize).ok_or(Error::BadUnionTag(tag))?;
159            out.push('{');
160            json_string(out, &tag.to_string());
161            out.push(':');
162            write_value(out, schema, vty, &u.value()?, depth + 1)?;
163            out.push('}');
164        }
165    }
166    Ok(())
167}
168
169/// Render a map key as a JSON string (JSON object keys are always strings).
170fn write_map_key(out: &mut String, key: &Ref) {
171    match key {
172        Ref::Str(s) => json_string(out, s),
173        Ref::Bool(b) => json_string(out, if *b { "true" } else { "false" }),
174        Ref::U8(x) => json_string(out, &x.to_string()),
175        Ref::U16(x) => json_string(out, &x.to_string()),
176        Ref::U32(x) => json_string(out, &x.to_string()),
177        Ref::U64(x) => json_string(out, &x.to_string()),
178        Ref::I8(x) => json_string(out, &x.to_string()),
179        Ref::I16(x) => json_string(out, &x.to_string()),
180        Ref::I32(x) => json_string(out, &x.to_string()),
181        Ref::I64(x) => json_string(out, &x.to_string()),
182        Ref::Enum(x) => json_string(out, &x.to_string()),
183        // Not a valid key type (schema validation forbids it); render defensively.
184        _ => json_string(out, key.kind()),
185    }
186}
187
188fn write_float(out: &mut String, x: f64) {
189    if x.is_finite() {
190        let _ = write!(out, "{x}");
191    } else {
192        // JSON has no NaN/Infinity.
193        out.push_str("null");
194    }
195}
196
197fn json_string(out: &mut String, s: &str) {
198    out.push('"');
199    for c in s.chars() {
200        match c {
201            '"' => out.push_str("\\\""),
202            '\\' => out.push_str("\\\\"),
203            '\n' => out.push_str("\\n"),
204            '\r' => out.push_str("\\r"),
205            '\t' => out.push_str("\\t"),
206            c if (c as u32) < 0x20 => {
207                let _ = write!(out, "\\u{:04x}", c as u32);
208            }
209            c => out.push(c),
210        }
211    }
212    out.push('"');
213}