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