Skip to main content

mlt_core/decoder/property/
model.rs

1use std::borrow::Cow;
2use std::ops::Deref;
3#[cfg(feature = "unstable-v2")]
4use std::rc::Rc;
5
6#[cfg(feature = "unstable-v2")]
7use bitvec::order::Lsb0;
8#[cfg(feature = "unstable-v2")]
9use bitvec::slice::BitSlice;
10#[cfg(feature = "unstable-v2")]
11use bitvec::vec::BitVec;
12use enum_dispatch::enum_dispatch;
13
14#[cfg(feature = "unstable-v2")]
15use crate::decoder::Alp;
16use crate::decoder::RawStream;
17use crate::utils::Presence;
18use crate::{DecodeState, Lazy, PropKind};
19
20/// Property column representation, parameterized by decode state.
21///
22/// - `Property<'a>` / `Property<'a, Lazy>` - either raw bytes or decoded, in an [`crate::LazyParsed`] enum.
23/// - `Property<'a, Parsed>` - decoded [`ParsedProperty`] directly (no enum wrapper).
24pub type Property<'a, S = Lazy> =
25    <S as DecodeState>::LazyOrParsed<RawProperty<'a>, ParsedProperty<'a>>;
26
27/// Raw scalar column (bool, integer, or float) as read directly from the tile.
28#[derive(Debug, Clone, PartialEq)]
29pub struct RawScalar<'a> {
30    pub(crate) name: &'a str,
31    pub(crate) presence: RawPresence<'a>,
32    pub(crate) data: RawStream<'a>,
33}
34
35impl<'a> RawScalar<'a> {
36    pub(crate) fn new(name: &'a str, presence: RawPresence<'a>, data: RawStream<'a>) -> Self {
37        Self {
38            name,
39            presence,
40            data,
41        }
42    }
43}
44
45/// Raw float column as read directly from the tile.
46/// Its encodings differ in how many streams they use, so it carries an encoding enum like [`RawStrings`] rather than one stream.
47#[derive(Debug, Clone, PartialEq)]
48pub struct RawFloats<'a> {
49    pub name: &'a str,
50    pub presence: RawPresence<'a>,
51    pub encoding: RawFloatsEncoding<'a>,
52}
53
54impl<'a> RawFloats<'a> {
55    /// A column stored as one data stream.
56    pub(crate) fn single(name: &'a str, presence: RawPresence<'a>, data: RawStream<'a>) -> Self {
57        Self {
58            name,
59            presence,
60            encoding: RawFloatsEncoding::Single(data),
61        }
62    }
63}
64
65/// Raw encoding payload for a float column.
66#[derive(Debug, Clone, PartialEq)]
67pub enum RawFloatsEncoding<'a> {
68    /// One data stream, its own logical encoding saying how to read it.
69    Single(RawStream<'a>),
70    /// Codes first, then the distinct values they index.
71    ///
72    /// Requires the `unstable-v2` feature.
73    #[cfg(feature = "unstable-v2")]
74    Dictionary {
75        codes: RawStream<'a>,
76        dictionary: RawStream<'a>,
77    },
78    /// Integers scaled by a power of ten, in one stream, with the parameters in its header.
79    ///
80    /// Requires the `unstable-v2` feature.
81    #[cfg(feature = "unstable-v2")]
82    Alp { params: Alp, data: RawStream<'a> },
83}
84
85/// Raw string column as read directly from the tile.
86#[derive(Debug, Clone, PartialEq)]
87pub struct RawStrings<'a> {
88    pub name: &'a str,
89    pub presence: RawPresence<'a>,
90    pub encoding: RawStringsEncoding<'a>,
91}
92
93/// Raw encoding payload for a string column (plain, dictionary, or FSST variants).
94///
95/// `RawStream` order matches the encoder: see `StringEncoder.encode()`.
96#[derive(Debug, Clone, PartialEq)]
97pub enum RawStringsEncoding<'a> {
98    /// Plain: length stream + data stream
99    Plain(RawPlainData<'a>),
100    /// Dictionary: lengths + offsets + dictionary data
101    Dictionary {
102        plain_data: RawPlainData<'a>,
103        offsets: RawStream<'a>,
104        dict: DictLayout,
105    },
106    /// FSST plain (4 streams): symbol lengths, symbol table, value lengths, compressed corpus. No offsets.
107    FsstPlain(RawFsstData<'a>),
108    /// FSST dictionary (5 streams): symbol lengths, symbol table, value lengths, compressed corpus, offsets.
109    FsstDictionary {
110        fsst_data: RawFsstData<'a>,
111        offsets: RawStream<'a>,
112        dict: DictLayout,
113    },
114}
115
116/// How a dictionary's entries sit in its blob.
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, strum::EnumIter)]
118pub enum DictLayout {
119    /// Entries back to back, the lengths stream holding one length each.
120    Plain,
121    /// Entries with the prefix each shares with its predecessor factored out, so the blob holds
122    /// only suffixes and the lengths stream holds every prefix length then every suffix length.
123    #[cfg(feature = "unstable-v2")]
124    FrontCoded,
125}
126
127/// How a string column's values sit in the tile, as the extension bits of its
128/// leading stream name.
129///
130/// Decoding reconstructs the values and resolves this away, so it is readable
131/// only from a column that has not been decoded yet.
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
133pub enum StringLayout {
134    /// Lengths, then the values' bytes.
135    Plain,
136    /// Codes, then the distinct values' lengths and bytes.
137    Dict,
138    /// Lengths, then the FSST symbol table and the compressed corpus.
139    Fsst,
140    /// Codes, then the distinct values' lengths, the FSST symbol table and the corpus.
141    FsstDict,
142}
143
144/// How a column stores its values, beyond what its streams' encodings already say.
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
146pub struct ColumnStorage {
147    /// The layout of a string or shared-dictionary column; `None` for any other column.
148    pub string: Option<StringLayout>,
149    /// How the column's dictionary blob is laid out, when it carries one.
150    pub dictionary: Option<DictLayout>,
151}
152
153/// Raw encoding payload for a `SharedDict` column.
154///
155/// Unlike [`RawStringsEncoding`], shared dictionaries do NOT have their own offset stream.
156/// Instead, each child column has its own offset stream that references the shared dictionary.
157/// This is why only `Plain` and `FsstPlain` variants exist here.
158#[derive(Debug, Clone, PartialEq)]
159pub enum RawSharedDictEncoding<'a> {
160    /// Plain shared dict (2 streams): lengths + data.
161    Plain(RawPlainData<'a>),
162    /// FSST plain shared dict (4 streams): symbol lengths, symbol table, lengths, corpus.
163    FsstPlain(RawFsstData<'a>),
164}
165
166/// Raw shared-dictionary column as read directly from the tile.
167#[derive(Debug, Clone, PartialEq)]
168pub struct RawSharedDict<'a> {
169    pub name: &'a str,
170    pub encoding: RawSharedDictEncoding<'a>,
171    pub dict: DictLayout,
172    pub children: Vec<RawSharedDictItem<'a>>,
173}
174
175/// Raw property data as read directly from the tile.
176#[derive(Debug, PartialEq, Clone)]
177pub enum RawProperty<'a> {
178    Bool(RawScalar<'a>),
179    I8(RawScalar<'a>),
180    U8(RawScalar<'a>),
181    I32(RawScalar<'a>),
182    U32(RawScalar<'a>),
183    I64(RawScalar<'a>),
184    U64(RawScalar<'a>),
185    F32(RawFloats<'a>),
186    F64(RawFloats<'a>),
187    Str(RawStrings<'a>),
188    SharedDict(RawSharedDict<'a>),
189}
190
191/// Parsed property values in a typed enum form.
192#[derive(Clone, Debug, PartialEq, strum::IntoStaticStr)]
193#[strum(serialize_all = "snake_case")]
194#[enum_dispatch(Analyze)]
195pub enum ParsedProperty<'a> {
196    Bool(ParsedScalar<'a, bool>),
197    I8(ParsedScalar<'a, i8>),
198    U8(ParsedScalar<'a, u8>),
199    I32(ParsedScalar<'a, i32>),
200    U32(ParsedScalar<'a, u32>),
201    I64(ParsedScalar<'a, i64>),
202    U64(ParsedScalar<'a, u64>),
203    F32(ParsedScalar<'a, f32>),
204    F64(ParsedScalar<'a, f64>),
205    Str(ParsedStrings<'a>),
206    SharedDict(ParsedSharedDict<'a>),
207}
208
209macro_rules! impl_parsed_property_kind {
210    (
211        scalar { $($sv:ident),* $(,)? }
212        string { $($gv:ident),* $(,)? }
213    ) => {
214        impl ParsedProperty<'_> {
215            /// The column's data type. A shared dictionary is a way of storing strings
216            /// rather than a type of its own, so it reads as [`PropKind::Str`].
217            #[must_use]
218            pub fn kind(&self) -> PropKind {
219                match self {
220                    $(Self::$sv(_) => PropKind::$sv,)*
221                    $(Self::$gv(_) => PropKind::$gv,)*
222                    Self::SharedDict(_) => PropKind::Str,
223                }
224            }
225        }
226    };
227}
228
229with_kinds!(impl_parsed_property_kind);
230
231/// Decoded scalar property column (bool, integer, or float).
232///
233/// `presence` carries both the optional bitvector and the dense values.
234/// For a non-optional column, `presence` is [`Presence::AllPresent`] with all
235/// values inline. For an optional column, `presence` is [`Presence::Bits`] with
236/// `bits.count_ones() == values.len()`.
237#[derive(Clone, Debug, PartialEq)]
238pub struct ParsedScalar<'a, T: Copy + PartialEq> {
239    pub(crate) name: &'a str,
240    pub(crate) presence: Presence<'a, T>,
241}
242impl<'a, T: Copy + PartialEq> Deref for ParsedScalar<'a, T> {
243    type Target = Presence<'a, T>;
244    #[inline]
245    fn deref(&self) -> &Self::Target {
246        &self.presence
247    }
248}
249
250/// Per-feature byte range into a shared dictionary corpus.
251///
252/// `start` and `end` are signed byte offsets into the corpus string.
253/// The sentinel value [`DictRange::NULL`] (`-1, -1`) indicates a NULL (absent) entry.
254/// Equal `start` and `end` indicate an empty string.
255#[derive(Debug, Clone, Copy, PartialEq, Eq)]
256#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
257pub struct DictRange {
258    pub start: i32,
259    pub end: i32,
260}
261impl DictRange {
262    /// Sentinel value indicating a NULL (absent) entry.
263    pub const NULL: Self = Self { start: -1, end: -1 };
264}
265
266/// A single sub-property within a shared dictionary parsed value.
267#[derive(Debug, Clone, PartialEq, Eq)]
268#[cfg_attr(all(not(test), feature = "arbitrary"), derive(arbitrary::Arbitrary))]
269pub struct ParsedSharedDictItem<'a> {
270    /// The suffix name of this sub-property (appended to parent struct name).
271    pub(crate) suffix: &'a str,
272    /// Per-feature byte ranges into the parsed shared corpus.
273    /// Non-null entries indicate a present string stored as
274    /// `shared_dict.corpus()[start..end]`.
275    /// [`DictRange::NULL`] indicates a NULL value.
276    /// Equal `start` and `end` indicate an empty string.
277    pub(crate) ranges: Vec<DictRange>,
278}
279
280/// Parsed string values for a single property.
281#[derive(Debug, Clone, PartialEq, Eq)]
282pub struct ParsedStrings<'a> {
283    pub(crate) name: &'a str,
284    /// Per-feature cumulative end offsets into `data`.
285    /// Non-negative values indicate a present string and store its exclusive
286    /// end offset in `data`.
287    /// Negative values indicate NULL and encode the current offset as `-end-1`,
288    /// which is equivalent to `!end` in two's-complement form,
289    /// so the next item can still recover its start offset without scanning back
290    /// to the previous non-null value. This allows even the first item to be NULL.
291    /// In other words, if `lengths == [5, 5, -6, 8]`, then the strings are:
292    /// ```ignore
293    /// data[0..5], // 0th string
294    /// data[5..5], // 1st string is empty
295    /// NULL,       // 2nd string, offset stays 5 because -6 == -5-1
296    /// data[5..8], // 3rd string
297    /// ```
298    pub(crate) lengths: Vec<i32>,
299    pub(crate) data: Cow<'a, str>,
300}
301
302/// Parsed shared dictionary payload shared by one or more child string properties.
303#[derive(Debug, Clone, PartialEq, Eq)]
304pub struct ParsedSharedDict<'a> {
305    pub(crate) prefix: &'a str,
306    pub(crate) data: Cow<'a, str>,
307    pub(crate) items: Vec<ParsedSharedDictItem<'a>>,
308}
309
310/// A single child field within a `SharedDict` raw column
311#[derive(Clone, Debug, PartialEq)]
312pub struct RawSharedDictItem<'a> {
313    pub name: &'a str,
314    pub presence: RawPresence<'a>,
315    pub data: RawStream<'a>,
316}
317
318/// Raw plain data (length stream + data stream) borrowed from input bytes.
319#[derive(Debug, Clone, PartialEq)]
320pub struct RawPlainData<'a> {
321    pub lengths: RawStream<'a>,
322    pub data: RawStream<'a>,
323}
324
325/// Raw FSST-compressed data (4 streams) borrowed from input bytes.
326#[derive(Debug, Clone, PartialEq)]
327pub struct RawFsstData<'a> {
328    pub symbol_lengths: RawStream<'a>,
329    pub symbol_table: RawStream<'a>,
330    pub lengths: RawStream<'a>,
331    pub corpus: RawStream<'a>,
332}
333
334/// Raw presence/nullability data for a column.
335///
336/// `AllPresent` represents a non-optional column; other variants encode presence in a
337/// layer-format-specific way. Decode through [`RawPresence::decode_bits`]
338/// instead of matching on it.
339#[derive(Debug, Clone, PartialEq, Default)]
340pub enum RawPresence<'a> {
341    /// Non-optional column - every feature has a value; nothing is stored.
342    #[default]
343    AllPresent,
344    /// Tag `0x01`: bool-RLE presence stream with a full stream header.
345    Stream(RawStream<'a>),
346    /// Tag `0x02`: raw packed bitfield (LSB-first per byte, one bit per feature),
347    /// borrowed zero-copy from the tile bytes. No header precedes it.
348    /// Requires the `unstable-v2` feature.
349    #[cfg(feature = "unstable-v2")]
350    Bitfield(&'a BitSlice<u8, Lsb0>),
351    /// Tag `0x02`: presence that arrived run-coded or as a list of indices, so the
352    /// bits had to be built rather than borrowed.
353    ///
354    /// Behind an [`Rc`] because a shared field is read by every column that names it,
355    /// and one materialisation is enough for all of them: resolving a column's presence
356    /// costs a reference count either way, and only a reader that actually asks for the
357    /// bits of a field another column still holds pays to copy them.
358    /// Requires the `unstable-v2` feature.
359    #[cfg(feature = "unstable-v2")]
360    Decoded(Rc<BitVec<u8, Lsb0>>),
361}
362
363#[cfg(feature = "unstable-v2")]
364impl<'a> RawPresence<'a> {
365    /// Keep bits that were borrowed borrowed, and bits that were built built.
366    pub(crate) fn from_bits(bits: Cow<'a, BitSlice<u8, Lsb0>>) -> Self {
367        match bits {
368            Cow::Borrowed(bits) => Self::Bitfield(bits),
369            Cow::Owned(bits) => Self::Decoded(Rc::new(bits)),
370        }
371    }
372}
373
374impl RawPresence<'_> {
375    /// Whether this column carries presence data (some features may be null).
376    #[must_use]
377    pub(crate) fn is_optional(&self) -> bool {
378        !matches!(self, Self::AllPresent)
379    }
380}
381
382impl RawProperty<'_> {
383    /// How this column stores its values, or `None` when it has no layout to report.
384    #[must_use]
385    pub(crate) fn storage(&self) -> Option<ColumnStorage> {
386        let (string, dictionary) = match self {
387            Self::Str(column) => match &column.encoding {
388                RawStringsEncoding::Plain(_) => (StringLayout::Plain, None),
389                RawStringsEncoding::Dictionary { dict, .. } => (StringLayout::Dict, Some(*dict)),
390                RawStringsEncoding::FsstPlain(_) => (StringLayout::Fsst, None),
391                RawStringsEncoding::FsstDictionary { dict, .. } => {
392                    (StringLayout::FsstDict, Some(*dict))
393                }
394            },
395            // A shared dictionary's children are dictionary-coded strings, so it reports
396            // the layout its corpus is stored with, as a lone string column would.
397            Self::SharedDict(column) => match &column.encoding {
398                RawSharedDictEncoding::Plain(_) => (StringLayout::Dict, Some(column.dict)),
399                RawSharedDictEncoding::FsstPlain(_) => (StringLayout::FsstDict, Some(column.dict)),
400            },
401            Self::Bool(_)
402            | Self::I8(_)
403            | Self::U8(_)
404            | Self::I32(_)
405            | Self::U32(_)
406            | Self::I64(_)
407            | Self::U64(_)
408            | Self::F32(_)
409            | Self::F64(_) => return None,
410        };
411        Some(ColumnStorage {
412            string: Some(string),
413            dictionary,
414        })
415    }
416}