Skip to main content

mlt_core/dump/
model.rs

1//! Data model for the annotated binary dump (see [`crate::dump`]).
2
3use std::fmt::{Display, Formatter, Result as FmtResult};
4
5use serde::ser::SerializeStruct as _;
6use serde::{Serialize, Serializer};
7
8#[cfg(feature = "unstable-v2")]
9use crate::codecs::presence_coding::PresenceCoding;
10#[cfg(feature = "unstable-v2")]
11use crate::wire::Alp;
12use crate::wire::StreamMeta;
13
14/// Whether a region is tile metadata or an opaque data payload.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
16#[serde(rename_all = "camelCase")]
17pub enum RegionKind {
18    /// Framing, schema, or stream-header bytes, annotated byte- and bit-for-byte.
19    Meta,
20    /// A stream payload, rendered as raw hex plus best-effort decoded values.
21    DataBlob,
22}
23
24/// How a [`RegionKind::DataBlob`] payload is decoded for display.
25///
26/// Best-effort: on any decode error the renderer falls back to raw hex.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
28#[serde(tag = "kind", rename_all = "camelCase")]
29pub enum DecodeHint {
30    /// Nullability bitmap (byte-RLE -> packed bits).
31    Presence,
32    /// Boolean data stream (byte-RLE -> bools).
33    Bool,
34    /// Signed 32-bit integers (`i8`/`i32` columns).
35    I32,
36    /// Unsigned 32-bit integers (`u8`/`u32` columns, offsets, lengths).
37    U32,
38    /// The geometry types stream: `u32`s that each name a [`GeometryType`](crate::GeometryType).
39    GeometryType,
40    /// Signed 64-bit integers (`i64` columns).
41    I64,
42    /// Framed, Exception-Free ALP offsets, put back on their frame of reference.
43    #[cfg(feature = "unstable-v2")]
44    Alp(#[serde(serialize_with = "serialize_alp")] Alp),
45    /// Unsigned 64-bit integers (`u64` columns, 64-bit ids).
46    U64,
47    /// 32-bit floats.
48    F32,
49    /// 64-bit floats.
50    F64,
51    /// Opaque bytes (string / dictionary / FSST payloads); shown as hex + UTF-8 preview.
52    Bytes,
53    /// A raw LSB0 bitfield, not a stream: `ceil(num_values/8)` packed bytes.
54    #[cfg(feature = "unstable-v2")]
55    PackedBits,
56    /// Presence bits that are not laid out as bits: runs, or a list of indices.
57    #[cfg(feature = "unstable-v2")]
58    PresenceCoded(PresenceCoding),
59    /// A rANS vertex payload, which only decodes with the layer's topology, so its parts are shown instead.
60    #[cfg(feature = "unstable-v2")]
61    Rans,
62}
63
64/// Framed, Exception-Free ALP's parameters flattened beside the hint's tag, keeping `Alp`'s fields crate-private.
65#[cfg(feature = "unstable-v2")]
66fn serialize_alp<S: Serializer>(alp: &Alp, s: S) -> Result<S::Ok, S::Error> {
67    let mut st = s.serialize_struct("Alp", 3)?;
68    st.serialize_field("e", &alp.scale.e)?;
69    st.serialize_field("f", &alp.scale.f)?;
70    st.serialize_field("base", &alp.base)?;
71    st.end()
72}
73
74/// One sub-field of a bit-packed byte, e.g. a nibble of `stream_type`.
75#[derive(Debug, Clone, Serialize)]
76pub struct BitField {
77    /// Inclusive high bit index (7..=0, MSB first).
78    hi: u8,
79    /// Inclusive low bit index.
80    lo: u8,
81    /// The extracted field value, shifted down to bit 0.
82    #[serde(serialize_with = "serialize_as_number")]
83    raw: u64,
84    /// Human-readable meaning, e.g. `"physical = VarInt"`.
85    meaning: String,
86}
87
88impl BitField {
89    /// One field of a packed byte, located by the same mask constant the parser reads it
90    /// with, so the dump cannot drift from the wire format.
91    ///
92    /// `raw` is the masked bits shifted down to bit 0, so it renders as the field's own
93    /// value rather than its in-byte position. `mask` must be one contiguous run of bits.
94    pub fn mask(mask: u8, byte: u8, meaning: impl Into<String>) -> Self {
95        let (hi, lo) = mask_bounds(mask);
96        Self {
97            hi,
98            lo,
99            raw: u64::from((byte & mask) >> lo),
100            meaning: meaning.into(),
101        }
102    }
103
104    /// A one-bit flag, worded as a phrase instead of a `0`/`1`.
105    ///
106    /// The renderer already prefixes every bit line with `bit N = V ->`, so `when_set`
107    /// and `when_clear` should say what the bit asserts about the tile rather than
108    /// repeat its value.
109    #[must_use]
110    pub fn flag(mask: u8, byte: u8, when_set: &str, when_clear: &str) -> Self {
111        debug_assert_eq!(mask.count_ones(), 1, "a flag occupies exactly one bit");
112        let meaning = if byte & mask == 0 {
113            when_clear
114        } else {
115            when_set
116        };
117        Self::mask(mask, byte, meaning)
118    }
119
120    /// What this field means in prose, without the `bit N = V ->` prefix
121    /// [`Display`] puts in front of it.
122    #[must_use]
123    pub fn meaning(&self) -> &str {
124        &self.meaning
125    }
126}
127
128impl Display for BitField {
129    /// `bit 7 = 1 -> meaning`, or `bits 6-4 = 011 -> meaning` for a multi-bit field.
130    ///
131    /// The value is printed as wide as the field, so the leading zeros of a nibble are
132    /// not mistaken for a narrower field. The caller adds any indent and color.
133    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
134        if self.hi == self.lo {
135            write!(f, "bit {}", self.hi)?;
136        } else {
137            write!(f, "bits {}-{}", self.hi, self.lo)?;
138        }
139        let width = usize::from(self.hi - self.lo + 1);
140        write!(f, " = {:0width$b} -> {}", self.raw, self.meaning)
141    }
142}
143
144/// Inclusive `(hi, lo)` bit indices spanned by `mask`.
145#[expect(
146    clippy::cast_possible_truncation,
147    reason = "a u8 mask spans at most 8 bits"
148)]
149fn mask_bounds(mask: u8) -> (u8, u8) {
150    debug_assert!(mask != 0, "a bit field needs at least one bit");
151    let lo = mask.trailing_zeros() as u8;
152    let hi = (u8::BITS - 1 - mask.leading_zeros()) as u8;
153    debug_assert_eq!(
154        u32::from(hi - lo) + 1,
155        mask.count_ones(),
156        "a bit field's mask must be one contiguous run of bits"
157    );
158    (hi, lo)
159}
160
161/// Stream metadata attached to a [`RegionKind::DataBlob`] so the renderer can decode it.
162#[derive(Debug, Clone, Copy)]
163pub struct BlobInfo {
164    pub meta: StreamMeta,
165    pub hint: DecodeHint,
166}
167
168impl Serialize for BlobInfo {
169    /// Flattens `meta` into display strings, so the wire vocabulary stays out of the JSON API.
170    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
171        let mut st = s.serialize_struct("BlobInfo", 5)?;
172        st.serialize_field("streamType", &self.meta.stream_type.to_string())?;
173        st.serialize_field("logical", &self.meta.encoding.logical.to_string())?;
174        st.serialize_field("physical", &self.meta.encoding.physical.to_string())?;
175        st.serialize_field("numValues", &self.meta.num_values)?;
176        st.serialize_field("hint", &self.hint)?;
177        st.end()
178    }
179}
180
181/// Serialize an offset or a count as a 32-bit number.
182///
183/// Only the payload values a tile actually holds are 64-bit, and a transport that maps
184/// 64-bit integers to `BigInt` would otherwise hand every offset across as one.
185fn serialize_as_number<T, S>(v: &T, s: S) -> Result<S::Ok, S::Error>
186where
187    T: Copy + TryInto<u32>,
188    S: Serializer,
189{
190    let n = (*v)
191        .try_into()
192        .map_err(|_| serde::ser::Error::custom("offset or count exceeds u32"))?;
193    s.serialize_u32(n)
194}
195
196/// A single annotated span of the tile buffer.
197///
198/// Emitted in pre-order. Containers bracket their children and may overlap them.
199/// Leaf regions partition the buffer exactly; the coverage test relies on this.
200#[derive(Debug, Clone, Serialize)]
201#[serde(rename_all = "camelCase")]
202pub struct Region {
203    /// Absolute byte offset into the tile buffer.
204    #[serde(serialize_with = "serialize_as_number")]
205    pub offset: usize,
206    #[serde(serialize_with = "serialize_as_number")]
207    pub len: usize,
208    /// Nesting depth, for indentation.
209    #[serde(serialize_with = "serialize_as_number")]
210    pub depth: usize,
211    /// Short label, e.g. `"column[2].type"` or `"num_values"`.
212    pub label: String,
213    /// Rendered scalar value (varint value, string, enum name), if any.
214    pub value: Option<String>,
215    /// Bit-level breakdown; empty unless this is a bit-packed byte.
216    pub bits: Vec<BitField>,
217    pub kind: RegionKind,
218    /// True for structural groups that span their children (excluded from coverage).
219    pub container: bool,
220    /// Present for `DataBlob` regions that carry decodable stream metadata.
221    pub blob: Option<BlobInfo>,
222}
223
224/// The full annotation of a tile: a flat, depth-tagged region list.
225#[derive(Serialize)]
226#[serde(rename_all = "camelCase")]
227pub struct DumpTree {
228    #[serde(serialize_with = "serialize_as_number")]
229    pub buf_len: usize,
230    /// Regions in pre-order (containers before their children).
231    pub regions: Vec<Region>,
232}
233
234/// Label of the synthetic leaf covering the bytes a bailed-out walk never reached.
235pub const UNANNOTATED: &str = "<unannotated>";
236
237/// The `idx`-th top-level layer's regions, or `None` when the tile has no such layer.
238///
239/// Containers span their children, so the layer is selected by byte range, not by depth.
240#[must_use]
241pub fn filter_layer(tree: &DumpTree, idx: usize) -> Option<DumpTree> {
242    let layer = tree
243        .regions
244        .iter()
245        .filter(|r| r.depth == 0 && r.container)
246        .nth(idx)?;
247    let start = layer.offset;
248    let end = layer.offset + layer.len;
249    let regions = tree
250        .regions
251        .iter()
252        .filter(|r| r.offset >= start && r.offset + r.len <= end)
253        .cloned()
254        .collect();
255    Some(DumpTree {
256        buf_len: tree.buf_len,
257        regions,
258    })
259}