Skip to main content

mlt_core/dump/
decode.rs

1//! Typed decode of a single data blob, for callers that render the values themselves.
2
3use std::borrow::Cow;
4
5use serde::Serialize;
6use usize_cast::IntoUsize as _;
7
8use super::model::{BlobInfo, DecodeHint, DumpTree, Region};
9use crate::codecs::fsst::decode_fsst_bytes;
10use crate::decoder::strings::rebuild_dictionary;
11use crate::decoder::{DictLayout, DictionaryType, LengthType, RawFsstData, RawStream, StreamType};
12use crate::{Decoder, GeometryType, MltError, MltResult};
13
14/// One data blob decoded for display, capped at the caller's value count.
15///
16/// [`render`](super::render()) formats the same payloads as text.
17/// This is the structured form, for the wasm binding and anything else that lays out its own view.
18#[derive(Debug, Clone, PartialEq, Serialize)]
19#[serde(
20    tag = "kind",
21    rename_all = "camelCase",
22    rename_all_fields = "camelCase"
23)]
24pub enum DecodedBlob {
25    /// Values that survive an `f64` round-trip.
26    Numbers {
27        values: Vec<f64>,
28        /// Value count before the cap, or `None` when nothing was dropped.
29        truncated_from: Option<u32>,
30    },
31    /// 64-bit values, which an `f64` would round.
32    #[serde(rename = "bigints")]
33    BigInts {
34        values: Vec<i128>,
35        truncated_from: Option<u32>,
36    },
37    Bools {
38        values: Vec<bool>,
39        truncated_from: Option<u32>,
40    },
41    /// Values that name something: each `names` entry is what the `values` entry stands for.
42    Enum {
43        values: Vec<u32>,
44        names: Vec<Cow<'static, str>>,
45        truncated_from: Option<u32>,
46    },
47    /// The strings an FSST corpus stands for, which its own bytes do not say.
48    Strings {
49        values: Vec<String>,
50        truncated_from: Option<u32>,
51    },
52    /// A byte payload that is valid UTF-8.
53    Text { value: String },
54    /// A byte payload that is not.
55    Binary { len: u32 },
56    /// The payload did not decode.
57    Error { message: String },
58}
59
60/// Decode `data` the way `info`'s hint says to, keeping at most `max_values` values.
61///
62/// `max_values` of `0` keeps all of them.
63/// A bytes payload is one value rather than a list, so it crosses whole.
64/// Never panics: a decode failure comes back as [`DecodedBlob::Error`].
65pub fn decode_blob(
66    info: BlobInfo,
67    data: &[u8],
68    max_values: usize,
69    dec: &mut Decoder,
70) -> DecodedBlob {
71    // Bound memory/time per blob, as `render` does.
72    dec.reset_budget();
73    let meta = info.meta;
74    match info.hint {
75        DecodeHint::Presence => match RawStream::new(meta, data).decode_bitvec(dec) {
76            Ok(bits) => bools(bits.iter().by_vals().collect(), max_values),
77            Err(e) => error(&e),
78        },
79        #[cfg(feature = "unstable-v2")]
80        DecodeHint::PresenceCoded(coding) => {
81            use crate::codecs::presence_coding::{Unmetered, read};
82            match read(data, meta.num_values, coding, &mut Unmetered) {
83                Ok((_, bits)) => bools(bits.iter().by_vals().collect(), max_values),
84                Err(e) => DecodedBlob::Error {
85                    message: e.to_string(),
86                },
87            }
88        }
89        #[cfg(feature = "unstable-v2")]
90        DecodeHint::PackedBits => {
91            let n = meta.num_values.into_usize();
92            let available = data.len() * 8;
93            if available < n {
94                return DecodedBlob::Error {
95                    message: format!("needs {n} bits, got {available}"),
96                };
97            }
98            bools(
99                (0..n).map(|i| data[i / 8] >> (i % 8) & 1 == 1).collect(),
100                max_values,
101            )
102        }
103        #[cfg(feature = "unstable-v2")]
104        DecodeHint::Rans => DecodedBlob::Binary {
105            len: count(data.len()),
106        },
107        DecodeHint::Bool => match RawStream::new(meta, data).decode_bools(dec) {
108            Ok(v) => bools(v, max_values),
109            Err(e) => error(&e),
110        },
111        DecodeHint::GeometryType => geometry_types(
112            RawStream::new(meta, data).decode_ints::<u32>(dec),
113            max_values,
114        ),
115        DecodeHint::I32 => numbers(
116            RawStream::new(meta, data).decode_ints::<i32>(dec),
117            max_values,
118        ),
119        DecodeHint::U32 => numbers(
120            RawStream::new(meta, data).decode_ints::<u32>(dec),
121            max_values,
122        ),
123        DecodeHint::F32 => numbers(
124            RawStream::new(meta, data).decode_floats::<f32>(dec),
125            max_values,
126        ),
127        DecodeHint::F64 => numbers(
128            RawStream::new(meta, data).decode_floats::<f64>(dec),
129            max_values,
130        ),
131        DecodeHint::I64 => bigints(
132            RawStream::new(meta, data).decode_ints::<i64>(dec),
133            max_values,
134        ),
135        DecodeHint::U64 => bigints(
136            RawStream::new(meta, data).decode_ints::<u64>(dec),
137            max_values,
138        ),
139        #[cfg(feature = "unstable-v2")]
140        DecodeHint::Alp(params) => bigints(
141            RawStream::new(meta, data).decode_alp_codes(params, dec),
142            max_values,
143        ),
144        DecodeHint::Bytes => match std::str::from_utf8(data) {
145            Ok(s) => DecodedBlob::Text {
146                value: s.to_string(),
147            },
148            Err(_) => DecodedBlob::Binary {
149                len: count(data.len()),
150            },
151        },
152    }
153}
154
155/// As [`decode_blob`] for the payload at `index` of `tree`, which may need the streams beside it.
156///
157/// An FSST corpus is only codes into the symbol table next to it, so it decodes to the strings
158/// those codes expand to. Any other payload decodes exactly as [`decode_blob`] does.
159pub fn decode_region(
160    tree: &DumpTree,
161    buf: &[u8],
162    index: usize,
163    max_values: usize,
164    dec: &mut Decoder,
165) -> DecodedBlob {
166    let Some(region) = tree.regions.get(index) else {
167        return DecodedBlob::Error {
168            message: format!("the tree has no region {index}"),
169        };
170    };
171    let Some(info) = region.blob else {
172        return DecodedBlob::Error {
173            message: format!("region {index} carries no stream metadata"),
174        };
175    };
176    if let Some(streams) = fsst_streams(&tree.regions, index) {
177        dec.reset_budget();
178        let layout = corpus_layout(&tree.regions, buf, index);
179        return fsst_strings(streams, layout, buf, max_values, dec).unwrap_or_else(|e| error(&e));
180    }
181    match buf.get(region.offset..region.offset + region.len) {
182        Some(bytes) => decode_blob(info, bytes, max_values, dec),
183        None => DecodedBlob::Error {
184            message: format!("region {index} lies outside the tile buffer"),
185        },
186    }
187}
188
189/// The four streams an FSST corpus is read against, in the order `RawFsstData::new` takes them.
190///
191/// They are the streams of the corpus's own column, so a second of any kind there would make
192/// a pick a guess, and the corpus is then left to decode as bytes.
193fn fsst_streams(regions: &[Region], at: usize) -> Option<[&Region; 4]> {
194    let corpus = &regions[at];
195    if !matches!(
196        corpus.blob?.meta.stream_type,
197        StreamType::Data(DictionaryType::Single | DictionaryType::Shared)
198    ) {
199        return None;
200    }
201    // The corpus sits in its stream, which sits in the column.
202    let column = parent(regions, parent(regions, at)?)?;
203    let depth = regions[column].depth;
204    let after = &regions[column + 1..];
205    let column_streams = &after[..after
206        .iter()
207        .position(|r| r.depth <= depth)
208        .unwrap_or(after.len())];
209    let only = |want: StreamType| {
210        let mut hits = column_streams
211            .iter()
212            .filter(|r| r.blob.is_some_and(|b| b.meta.stream_type == want));
213        let first = hits.next()?;
214        hits.next().is_none().then_some(first)
215    };
216    Some([
217        only(StreamType::Length(LengthType::Symbol))?,
218        only(StreamType::Data(DictionaryType::Fsst))?,
219        only(StreamType::Length(LengthType::Dictionary))?,
220        corpus,
221    ])
222}
223
224/// The region `at` sits in.
225fn parent(regions: &[Region], at: usize) -> Option<usize> {
226    regions[..at]
227        .iter()
228        .rposition(|r| r.depth < regions[at].depth)
229}
230
231/// How the corpus at `at` lays its entries out, as its stream's encoding byte says.
232///
233/// Only a v2 layer has that byte, so a v1 corpus is always plain.
234#[cfg(feature = "unstable-v2")]
235fn corpus_layout(regions: &[Region], buf: &[u8], at: usize) -> DictLayout {
236    let is_v2 = regions[..at]
237        .iter()
238        .rposition(|r| r.depth == 0)
239        .and_then(|layer| {
240            regions[layer + 1..at]
241                .iter()
242                .find(|r| r.depth == 1 && r.label == "tag")
243        })
244        .and_then(|tag| buf.get(tag.offset))
245        == Some(&2);
246    let enc_byte = parent(regions, at).and_then(|stream| buf.get(regions[stream].offset));
247    match enc_byte {
248        Some(&b) if is_v2 => DictLayout::from_bits(b).unwrap_or(DictLayout::Plain),
249        _ => DictLayout::Plain,
250    }
251}
252
253#[cfg(not(feature = "unstable-v2"))]
254fn corpus_layout(_: &[Region], _: &[u8], _: usize) -> DictLayout {
255    DictLayout::Plain
256}
257
258fn fsst_strings(
259    [symbol_lengths, symbol_table, lengths, corpus]: [&Region; 4],
260    layout: DictLayout,
261    buf: &[u8],
262    max_values: usize,
263    dec: &mut Decoder,
264) -> MltResult<DecodedBlob> {
265    let stream = |r: &Region| -> MltResult<RawStream<'_>> {
266        let meta = r
267            .blob
268            .ok_or(MltError::MalformedFsst("a stream without metadata"))?
269            .meta;
270        let bytes = buf
271            .get(r.offset..r.offset + r.len)
272            .ok_or(MltError::MalformedFsst("a stream outside the tile"))?;
273        Ok(RawStream::new(meta, bytes))
274    };
275    let raw = RawFsstData::new(
276        stream(symbol_lengths)?,
277        stream(symbol_table)?,
278        stream(lengths)?,
279        stream(corpus)?,
280    )?;
281    let (bytes, lengths) = decode_fsst_bytes(raw, dec)?;
282    let (text, lengths) = rebuild_dictionary(layout, &lengths, &bytes)?;
283    if let Cow::Owned(entries) = &text {
284        dec.consume_items::<u8>(entries.len())?;
285    }
286    let (kept, truncated_from) = cap(lengths.into_owned(), max_values);
287
288    let mut rest = &*text;
289    let mut values = dec.alloc(kept.len())?;
290    for len in kept {
291        let (string, tail) = rest
292            .split_at_checked(len.into_usize())
293            .ok_or(MltError::MalformedFsst("string lengths overrun the corpus"))?;
294        dec.consume_items::<u8>(string.len())?;
295        values.push(string.to_owned());
296        rest = tail;
297    }
298    Ok(DecodedBlob::Strings {
299        values,
300        truncated_from,
301    })
302}
303
304fn numbers<T: Into<f64>>(res: MltResult<Vec<T>>, max_values: usize) -> DecodedBlob {
305    match res {
306        Ok(v) => {
307            let (v, truncated_from) = cap(v, max_values);
308            DecodedBlob::Numbers {
309                values: v.into_iter().map(Into::into).collect(),
310                truncated_from,
311            }
312        }
313        Err(e) => error(&e),
314    }
315}
316
317fn geometry_types(res: MltResult<Vec<u32>>, max_values: usize) -> DecodedBlob {
318    match res {
319        Ok(v) => {
320            let (values, truncated_from) = cap(v, max_values);
321            let names = values
322                .iter()
323                .map(|&v| {
324                    u8::try_from(v)
325                        .ok()
326                        .and_then(|b| GeometryType::try_from(b).ok())
327                        .map_or_else(
328                            || format!("unknown({v})").into(),
329                            |t| <&str>::from(t).into(),
330                        )
331                })
332                .collect();
333            DecodedBlob::Enum {
334                values,
335                names,
336                truncated_from,
337            }
338        }
339        Err(e) => error(&e),
340    }
341}
342
343fn bigints<T: Into<i128>>(res: MltResult<Vec<T>>, max_values: usize) -> DecodedBlob {
344    match res {
345        Ok(v) => {
346            let (v, truncated_from) = cap(v, max_values);
347            DecodedBlob::BigInts {
348                values: v.into_iter().map(Into::into).collect(),
349                truncated_from,
350            }
351        }
352        Err(e) => error(&e),
353    }
354}
355
356fn bools(v: Vec<bool>, max_values: usize) -> DecodedBlob {
357    let (values, truncated_from) = cap(v, max_values);
358    DecodedBlob::Bools {
359        values,
360        truncated_from,
361    }
362}
363
364fn error(e: &MltError) -> DecodedBlob {
365    DecodedBlob::Error {
366        message: e.to_string(),
367    }
368}
369
370/// Keep the first `max_values`, reporting the original count when that drops any.
371fn cap<T>(mut values: Vec<T>, max_values: usize) -> (Vec<T>, Option<u32>) {
372    let total = values.len();
373    if max_values == 0 || total <= max_values {
374        return (values, None);
375    }
376    values.truncate(max_values);
377    (values, Some(count(total)))
378}
379
380fn count(n: usize) -> u32 {
381    u32::try_from(n).unwrap_or(u32::MAX)
382}