Skip to main content

mlt_core/decoder/
root.rs

1use usize_cast::IntoUsize as _;
2
3use crate::LazyParsed::Raw;
4use crate::MltError::{
5    BufferUnderflow, GeometryWithoutStreams, InvalidSharedDictStreamCount, MissingGeometry,
6    MissingLayerName, MultipleGeometryColumns, MultipleIdColumns, SharedDictRequiresStreams,
7    TrailingLayerData, UnexpectedStructChildCount, UnsupportedStringStreamCount,
8};
9use crate::codecs::varint::parse_varint;
10use crate::decoder::{
11    Column, ColumnType, DictionaryType, Extent, Geometry, Id, Layer01, ParsedLayer01, RawFsstData,
12    RawGeometry, RawId, RawIdValue, RawPlainData, RawPresence, RawProperty, RawScalar,
13    RawSharedDict, RawSharedDictEncoding, RawSharedDictItem, RawStream, RawStrings,
14    RawStringsEncoding, StreamType,
15};
16use crate::errors::AsMltError as _;
17use crate::utils::{SetOptionOnce as _, parse_string};
18use crate::{Layer, Lazy, MltError, MltRefResult, MltResult, ParsedLayer};
19
20/// Default memory budget: 20 MiB.
21const DEFAULT_MAX_BYTES: u32 = 20 * 1024 * 1024;
22
23/// Stateful decoder that enforces a per-tile memory budget during decoding.
24///
25/// Pass a `Decoder` to every `raw.decode()` / `into_tile()` call and to
26/// `from_bytes`-style parsers. Each method charges the budget before
27/// performing heap allocations, so the total heap used never exceeds `max_bytes`
28/// (in bytes).
29///
30/// ```
31/// use mlt_core::Decoder;
32///
33/// // Default: 10 MiB budget.
34/// let mut dec = Decoder::default();
35///
36/// // Custom budget.
37/// let mut dec = Decoder::with_max_size(64 * 1024 * 1024);
38/// ```
39#[derive(Debug, Clone, PartialEq, Eq, Default)]
40pub struct Decoder {
41    /// Keep track of the memory used when decoding a tile: raw->parsed transition
42    budget: MemBudget,
43    /// Reusable scratch buffer for the physical u32 decode pass.
44    /// Held here so its heap allocation is reused across streams without extra cost.
45    pub(crate) buffer_u32: Vec<u32>,
46    /// Reusable scratch buffer for the physical u64 decode pass.
47    /// Held here so its heap allocation is reused across streams without extra cost.
48    pub(crate) buffer_u64: Vec<u64>,
49}
50
51impl Decoder {
52    /// Create a decoder with a custom memory budget (in bytes).
53    #[must_use]
54    pub fn with_max_size(max_bytes: u32) -> Self {
55        Self {
56            budget: MemBudget::with_max_size(max_bytes),
57            ..Default::default()
58        }
59    }
60
61    pub fn decode_all<'a>(
62        &mut self,
63        layers: impl IntoIterator<Item = Layer<'a>>,
64    ) -> MltResult<Vec<ParsedLayer<'a>>> {
65        layers
66            .into_iter()
67            .map(|l| l.decode_all(self))
68            .collect::<MltResult<_>>()
69    }
70
71    /// Allocate a `Vec<T>` with the given capacity, charging the decoder's budget for
72    /// `capacity * size_of::<T>()` bytes. Use this instead of `Vec::with_capacity` in decode paths.
73    #[inline]
74    pub(crate) fn alloc<T>(&mut self, capacity: usize) -> MltResult<Vec<T>> {
75        let bytes = capacity.checked_mul(size_of::<T>()).or_overflow()?;
76        let bytes_u32 = u32::try_from(bytes).or_overflow()?;
77        self.budget.consume(bytes_u32)?;
78        Ok(Vec::with_capacity(capacity))
79    }
80
81    /// Charge the budget for `size` raw bytes. Prefer [`consume_items`][Self::consume_items]
82    /// when charging for a known-type collection.
83    #[inline]
84    pub(crate) fn consume(&mut self, size: u32) -> MltResult<()> {
85        self.budget.consume(size)
86    }
87
88    /// Charge the budget for `count` items of type `T` (`count * size_of::<T>()` bytes).
89    #[inline]
90    pub(crate) fn consume_items<T>(&mut self, count: usize) -> MltResult<()> {
91        let bytes = count.checked_mul(size_of::<T>()).or_overflow()?;
92        self.budget.consume(u32::try_from(bytes).or_overflow()?)
93    }
94
95    #[inline]
96    pub(crate) fn adjust(&mut self, adjustment: u32) {
97        self.budget.adjust(adjustment);
98    }
99
100    /// Return the unused portion of a pre-charged allocation budget.
101    ///
102    /// Call this after fully populating a `Vec<T>` that was pre-allocated with [`Decoder::alloc`],
103    /// passing the same `alloc_size` that was given to `alloc`.
104    ///
105    /// Returns an error if the vector grew beyond `alloc_size` (malformed input caused more items
106    /// than declared). Subtracts `(alloc_size - buf.len()) * size_of::<T>()` from the budget.
107    #[inline]
108    pub(crate) fn adjust_alloc<T>(&mut self, buf: &[T], alloc_size: usize) -> MltResult<()> {
109        if buf.len() > alloc_size {
110            return Err(MltError::InvalidDecodingStreamSize(buf.len(), alloc_size));
111        }
112        // Return the unused portion of the pre-charged budget.
113        let unused = (alloc_size - buf.len()) * size_of::<T>();
114        // unused fits in u32: it's at most alloc_size * size_of::<T>(), which was checked to fit
115        // in u32 when alloc() was called. Using saturating_cast to avoid a fallible conversion.
116        #[expect(
117            clippy::cast_possible_truncation,
118            reason = "unused <= alloc_size * size_of::<T>() which was verified to fit in u32 by alloc()"
119        )]
120        self.budget.adjust(unused as u32);
121        Ok(())
122    }
123
124    #[must_use]
125    pub fn consumed(&self) -> u32 {
126        self.budget.consumed()
127    }
128
129    /// Reset the memory budget to zero, keeping scratch buffers allocated.
130    ///
131    /// Call this between tiles when reusing a single `Decoder` for multiple
132    /// decodes — the per-tile budget is enforced fresh, but the internal
133    /// `buffer_u32` / `buffer_u64` scratch space is retained so it doesn't
134    /// need to be re-allocated.
135    ///
136    /// # Safety / correctness precondition
137    ///
138    /// Only call this after dropping any decoded allocations returned from the
139    /// previous tile. Resetting the budget while earlier decoded outputs are
140    /// still alive makes the budget enforceable only per-tile and can bypass
141    /// the stronger guarantee that total live heap tracked by this decoder
142    /// never exceeds the configured maximum.
143    pub fn reset_budget(&mut self) {
144        self.budget.reset();
145    }
146}
147
148impl MemBudget {
149    /// Reset tracked usage for a new decode window.
150    ///
151    /// Callers must ensure that allocations accounted for by the previous
152    /// window are no longer live before resetting.
153    fn reset(&mut self) {
154        self.bytes_used = 0;
155    }
156}
157/// Stateful parser that enforces a memory budget during parsing (binary → raw structures).
158///
159/// The parse chain reserves memory before allocations so total heap stays within the limit.
160///
161/// ```
162/// use mlt_core::Parser;
163///
164/// # let bytes: &[u8] = &[];
165/// let mut parser = Parser::default();
166/// let layers = parser.parse_layers(bytes).expect("parse");
167///
168/// // Or with a custom limit:
169/// let mut parser = Parser::with_max_size(64 * 1024 * 1024);
170/// ```
171#[derive(Debug, Clone, PartialEq, Eq, Default)]
172pub struct Parser {
173    budget: MemBudget,
174}
175
176impl Parser {
177    /// Create a parser with a custom memory budget (in bytes).
178    #[must_use]
179    pub fn with_max_size(max_bytes: u32) -> Self {
180        Self {
181            budget: MemBudget::with_max_size(max_bytes),
182        }
183    }
184
185    /// Parse a sequence of binary layers, reserving decoded memory against this parser's budget.
186    pub fn parse_layers<'a>(&mut self, mut input: &'a [u8]) -> MltResult<Vec<Layer<'a>>> {
187        let mut result = Vec::new();
188        while !input.is_empty() {
189            let layer;
190            (input, layer) = Layer::from_bytes(input, self)?;
191            result.push(layer);
192        }
193        Ok(result)
194    }
195
196    /// Reserve `size` bytes from the parse budget. Used internally by the parse chain.
197    #[inline]
198    pub(crate) fn reserve(&mut self, size: u32) -> MltResult<()> {
199        self.budget.consume(size)
200    }
201
202    #[must_use]
203    pub fn reserved(&self) -> u32 {
204        self.budget.consumed()
205    }
206}
207
208#[derive(Debug, Clone, PartialEq, Eq)]
209struct MemBudget {
210    /// Hard ceiling: total decoded bytes may not exceed this value.
211    pub max_bytes: u32,
212    /// Running total of used bytes so far.
213    pub bytes_used: u32,
214}
215
216impl Default for MemBudget {
217    /// Create a decoder with the default 10 MiB memory budget.
218    fn default() -> Self {
219        Self::with_max_size(DEFAULT_MAX_BYTES)
220    }
221}
222
223impl MemBudget {
224    /// Create a decoder with a custom memory budget (in bytes).
225    #[must_use]
226    fn with_max_size(max_bytes: u32) -> Self {
227        Self {
228            max_bytes,
229            bytes_used: 0,
230        }
231    }
232
233    /// Adjust previous consumption by `- adjustment` bytes.  Will panic if used incorrectly.
234    #[inline]
235    fn adjust(&mut self, adjustment: u32) {
236        self.bytes_used = self.bytes_used.checked_sub(adjustment).unwrap();
237    }
238
239    /// Take `size` bytes from the allocation budget. Call this before the actual allocation.
240    #[inline]
241    fn consume(&mut self, size: u32) -> MltResult<()> {
242        let accumulator = &mut self.bytes_used;
243        let max_bytes = self.max_bytes;
244        if let Some(new_value) = accumulator.checked_add(size).filter(|&v| v <= max_bytes) {
245            *accumulator = new_value;
246            Ok(())
247        } else {
248            Err(MltError::MemoryLimitExceeded {
249                limit: max_bytes,
250                used: *accumulator,
251                requested: size,
252            })
253        }
254    }
255
256    fn consumed(&self) -> u32 {
257        self.bytes_used
258    }
259}
260
261impl<'a> Layer01<'a, Lazy> {
262    /// Parse `v01::Layer` metadata, reserving decoded memory against the parser's budget.
263    pub(crate) fn from_bytes(input: &'a [u8], parser: &mut Parser) -> MltResult<Self> {
264        let (input, layer_name) = parse_string(input)?;
265        if layer_name.is_empty() {
266            return Err(MissingLayerName);
267        }
268        let (input, extent) = parse_varint::<u32>(input)?;
269        let extent = Extent::new(extent)?;
270        let (input, column_count) = parse_varint::<u32>(input)?;
271
272        // Each column requires at least 1 byte (column type)
273        if input.len() < column_count.into_usize() {
274            return Err(BufferUnderflow(column_count, input.len()));
275        }
276
277        // !!!!!!!
278        // WARNING: make sure to never use `let (input, ...)` after this point: input var is reused
279        let (mut input, (col_info, prop_count)) = parse_columns_meta(input, column_count, parser)?;
280        #[cfg(fuzzing)]
281        let layer_order = col_info
282            .iter()
283            .map(|column| column.typ)
284            .map(crate::decoder::fuzzing::LayerOrdering::from)
285            .collect();
286
287        let mut properties = Vec::with_capacity(prop_count.into_usize());
288        let mut id_column: Option<Id> = None;
289        let mut geometry: Option<Geometry> = None;
290
291        for column in col_info {
292            use crate::decoder::RawProperty as RP;
293
294            let presence;
295            let value;
296            let name = column.name.unwrap_or("");
297
298            match column.typ {
299                ColumnType::Id | ColumnType::OptId => {
300                    (input, presence) = parse_optional(column.typ, input, parser)?;
301                    (input, value) = RawStream::from_bytes(input, parser)?;
302                    id_column.set_once(Raw(RawId {
303                        presence,
304                        value: RawIdValue::Id32(value),
305                    }))?;
306                }
307                ColumnType::LongId | ColumnType::OptLongId => {
308                    (input, presence) = parse_optional(column.typ, input, parser)?;
309                    (input, value) = RawStream::from_bytes(input, parser)?;
310                    id_column.set_once(Raw(RawId {
311                        presence,
312                        value: RawIdValue::Id64(value),
313                    }))?;
314                }
315                ColumnType::Geometry => {
316                    input = parse_geometry_column(input, &mut geometry, parser)?;
317                }
318                ColumnType::Bool | ColumnType::OptBool => {
319                    (input, presence) = parse_optional(column.typ, input, parser)?;
320                    (input, value) = RawStream::parse_bool(input, parser)?;
321                    properties.push(Raw(RP::Bool(RawScalar::new(name, presence, value))));
322                }
323                ColumnType::I8 | ColumnType::OptI8 => {
324                    (input, presence) = parse_optional(column.typ, input, parser)?;
325                    (input, value) = RawStream::from_bytes(input, parser)?;
326                    properties.push(Raw(RP::I8(RawScalar::new(name, presence, value))));
327                }
328                ColumnType::U8 | ColumnType::OptU8 => {
329                    (input, presence) = parse_optional(column.typ, input, parser)?;
330                    (input, value) = RawStream::from_bytes(input, parser)?;
331                    properties.push(Raw(RP::U8(RawScalar::new(name, presence, value))));
332                }
333                ColumnType::I32 | ColumnType::OptI32 => {
334                    (input, presence) = parse_optional(column.typ, input, parser)?;
335                    (input, value) = RawStream::from_bytes(input, parser)?;
336                    properties.push(Raw(RP::I32(RawScalar::new(name, presence, value))));
337                }
338                ColumnType::U32 | ColumnType::OptU32 => {
339                    (input, presence) = parse_optional(column.typ, input, parser)?;
340                    (input, value) = RawStream::from_bytes(input, parser)?;
341                    properties.push(Raw(RP::U32(RawScalar::new(name, presence, value))));
342                }
343                ColumnType::I64 | ColumnType::OptI64 => {
344                    (input, presence) = parse_optional(column.typ, input, parser)?;
345                    (input, value) = RawStream::from_bytes(input, parser)?;
346                    properties.push(Raw(RP::I64(RawScalar::new(name, presence, value))));
347                }
348                ColumnType::U64 | ColumnType::OptU64 => {
349                    (input, presence) = parse_optional(column.typ, input, parser)?;
350                    (input, value) = RawStream::from_bytes(input, parser)?;
351                    properties.push(Raw(RP::U64(RawScalar::new(name, presence, value))));
352                }
353                ColumnType::F32 | ColumnType::OptF32 => {
354                    (input, presence) = parse_optional(column.typ, input, parser)?;
355                    (input, value) = RawStream::from_bytes(input, parser)?;
356                    properties.push(Raw(RP::F32(RawScalar::new(name, presence, value))));
357                }
358                ColumnType::F64 | ColumnType::OptF64 => {
359                    (input, presence) = parse_optional(column.typ, input, parser)?;
360                    (input, value) = RawStream::from_bytes(input, parser)?;
361                    properties.push(Raw(RP::F64(RawScalar::new(name, presence, value))));
362                }
363                ColumnType::Str | ColumnType::OptStr => {
364                    let prop;
365                    (input, prop) = parse_str_column(input, name, column.typ, parser)?;
366                    properties.push(Raw(prop));
367                }
368                ColumnType::SharedDict => {
369                    let prop;
370                    (input, prop) = parse_shared_dict_column(input, &column, parser)?;
371                    properties.push(Raw(prop));
372                }
373            }
374        }
375        if input.is_empty() {
376            Ok(Layer01 {
377                name: layer_name,
378                extent,
379                id: id_column,
380                geometry: geometry.ok_or(MissingGeometry)?,
381                properties,
382                #[cfg(fuzzing)]
383                layer_order,
384            })
385        } else {
386            Err(TrailingLayerData(input.len()))
387        }
388    }
389
390    /// Decode all columns and transition to [`Layer01<Parsed>`].
391    ///
392    /// Consumes `self` (a `Layer01<Lazy>`) and returns a `Layer01<Parsed>` where every
393    /// column field holds its parsed value directly, enabling infallible readonly access.
394    pub fn decode_all(self, dec: &mut Decoder) -> MltResult<ParsedLayer01<'a>> {
395        Ok(Layer01 {
396            name: self.name,
397            extent: self.extent,
398            id: self.id.map(|id| id.into_parsed(dec)).transpose()?,
399            geometry: self.geometry.into_parsed(dec)?,
400            properties: self
401                .properties
402                .into_iter()
403                .map(|p| p.into_parsed(dec))
404                .collect::<MltResult<Vec<_>>>()?,
405            #[cfg(fuzzing)]
406            layer_order: self.layer_order,
407        })
408    }
409}
410
411fn parse_shared_dict_children<'a>(
412    mut input: &'a [u8],
413    column: &Column<'a>,
414    parser: &mut Parser,
415) -> MltRefResult<'a, Vec<RawSharedDictItem<'a>>> {
416    let mut children = Vec::with_capacity(column.children.len());
417    for child in &column.children {
418        let (inp, sc) = parse_varint::<u32>(input)?;
419        let (inp, presence) = parse_optional(child.typ, inp, parser)?;
420        let optional_stream_count = u32::from(presence.is_optional());
421        if let Some(data_count) = sc.checked_sub(optional_stream_count)
422            && data_count != 1
423        {
424            return Err(UnexpectedStructChildCount(data_count));
425        }
426        let (inp, data) = RawStream::from_bytes(inp, parser)?;
427        children.push(RawSharedDictItem {
428            name: child.name.unwrap_or(""),
429            presence,
430            data,
431        });
432        input = inp;
433    }
434    Ok((input, children))
435}
436
437fn parse_optional<'a>(
438    typ: ColumnType,
439    input: &'a [u8],
440    parser: &mut Parser,
441) -> MltRefResult<'a, RawPresence<'a>> {
442    if typ.is_optional() {
443        let (input, optional) = RawStream::parse_bool(input, parser)?;
444        Ok((input, RawPresence::Stream(optional)))
445    } else {
446        Ok((input, RawPresence::AllPresent))
447    }
448}
449
450fn parse_geometry_column<'a>(
451    input: &'a [u8],
452    geometry: &mut Option<Geometry<'a>>,
453    parser: &mut Parser,
454) -> MltResult<&'a [u8]> {
455    let (input, stream_count) = parse_varint::<u32>(input)?;
456    if stream_count == 0 {
457        return Err(GeometryWithoutStreams);
458    }
459    // Each stream requires at least 1 byte (physical stream type)
460    let stream_count_capa = stream_count.into_usize();
461    if input.len() < stream_count_capa {
462        return Err(BufferUnderflow(stream_count, input.len()));
463    }
464    // metadata
465    let (input, meta) = RawStream::from_bytes(input, parser)?;
466    // geometry items
467    let (input, items) = RawStream::parse_multiple(input, stream_count_capa - 1, parser)?;
468    geometry.set_once(Raw(RawGeometry { meta, items }))?;
469    Ok(input)
470}
471
472fn parse_str_column<'a>(
473    mut input: &'a [u8],
474    name: &'a str,
475    typ: ColumnType,
476    parser: &mut Parser,
477) -> MltRefResult<'a, RawProperty<'a>> {
478    let mut stream_count = {
479        let stream_count_u32;
480        (input, stream_count_u32) = parse_varint::<u32>(input)?;
481        stream_count_u32.into_usize()
482    };
483    let presence;
484    (input, presence) = parse_optional(typ, input, parser)?;
485    if presence.is_optional() {
486        if stream_count == 0 {
487            return Err(UnsupportedStringStreamCount(stream_count));
488        }
489        stream_count -= 1;
490    }
491    let mut str_streams = [None, None, None, None, None];
492    if stream_count > str_streams.len() {
493        return Err(UnsupportedStringStreamCount(stream_count));
494    }
495    for slot in str_streams.iter_mut().take(stream_count) {
496        let stream;
497        (input, stream) = RawStream::from_bytes(input, parser)?;
498        *slot = Some(stream);
499    }
500    let encoding = match str_streams {
501        [Some(s1), Some(s2), None, None, None] => {
502            RawStringsEncoding::plain(RawPlainData::new(s1, s2)?)
503        }
504        [Some(s1), Some(s2), Some(s3), None, None] => {
505            RawStringsEncoding::dictionary(RawPlainData::new(s1, s3)?, s2)?
506        }
507        [Some(s1), Some(s2), Some(s3), Some(s4), None] => {
508            RawStringsEncoding::fsst_plain(RawFsstData::new(s1, s2, s3, s4)?)
509        }
510        [Some(s1), Some(s2), Some(s3), Some(s4), Some(s5)] => {
511            RawStringsEncoding::fsst_dictionary(RawFsstData::new(s1, s2, s3, s4)?, s5)?
512        }
513        _ => Err(UnsupportedStringStreamCount(stream_count))?,
514    };
515    Ok((
516        input,
517        RawProperty::Str(RawStrings {
518            name,
519            presence,
520            encoding,
521        }),
522    ))
523}
524
525fn parse_shared_dict_column<'a>(
526    mut input: &'a [u8],
527    column: &Column<'a>,
528    parser: &mut Parser,
529) -> MltRefResult<'a, RawProperty<'a>> {
530    // Read header streams until we hit the dictionary DATA(Single|Shared) stream.
531    let stream_count;
532    (input, stream_count) = parse_varint::<u32>(input)?;
533    let mut dict_streams = [None, None, None, None, None];
534    let mut streams_taken = 0_usize;
535    while streams_taken < stream_count.into_usize() {
536        let stream;
537        (input, stream) = RawStream::from_bytes(input, parser)?;
538        let is_last = matches!(
539            stream.meta.stream_type,
540            StreamType::Data(DictionaryType::Single | DictionaryType::Shared)
541        );
542        dict_streams[streams_taken] = Some(stream);
543        streams_taken += 1;
544        if is_last {
545            break;
546        } else if streams_taken >= dict_streams.len() {
547            return Err(UnsupportedStringStreamCount(streams_taken + 1));
548        }
549    }
550    let children;
551    (input, children) = parse_shared_dict_children(input, column, parser)?;
552
553    // Validate stream_count: must equal dict_streams + children + optional_children.
554    let children_n = u32::try_from(children.len()).or_overflow()?;
555    let optional_n = children
556        .iter()
557        .filter(|c| c.presence.is_optional())
558        .count()
559        .try_into()
560        .or_overflow()?;
561    let dict_n = u32::try_from(streams_taken).or_overflow()?;
562    let expected = crate::utils::checked_sum3(dict_n, children_n, optional_n)?;
563    // Java's encoder had a bug (fixed) that overcounted by 1: dict + 2*N + 1.
564    // Accept that value too so that files produced by older Java encoders still parse.
565    let java_legacy = expected.checked_add(1).or_overflow()?;
566    if stream_count != expected && stream_count != java_legacy {
567        return Err(InvalidSharedDictStreamCount {
568            actual: stream_count,
569            expected,
570        });
571    }
572
573    let name = column.name.unwrap_or("");
574    let encoding = match dict_streams {
575        [Some(s1), Some(s2), None, None, None] => {
576            RawSharedDictEncoding::plain(RawPlainData::new(s1, s2)?)
577        }
578        [Some(s1), Some(s2), Some(s3), Some(s4), None] => {
579            RawSharedDictEncoding::fsst_plain(RawFsstData::new(s1, s2, s3, s4)?)
580        }
581        _ => Err(SharedDictRequiresStreams(streams_taken))?,
582    };
583    Ok((
584        input,
585        RawProperty::SharedDict(RawSharedDict {
586            name,
587            encoding,
588            children,
589        }),
590    ))
591}
592
593fn parse_columns_meta<'a>(
594    mut input: &'a [u8],
595    column_count: u32,
596    parser: &mut Parser,
597) -> MltRefResult<'a, (Vec<Column<'a>>, u32)> {
598    use crate::decoder::ColumnType::{Geometry, Id, LongId, OptId, OptLongId, SharedDict};
599
600    let mut col_info = Vec::with_capacity(column_count.into_usize());
601    let mut geometries = 0;
602    let mut ids = 0;
603    for _ in 0..column_count {
604        let mut typ;
605        (input, typ) = Column::from_bytes(input, parser)?;
606        match typ.typ {
607            Geometry => geometries += 1,
608            Id | OptId | LongId | OptLongId => ids += 1,
609            SharedDict => {
610                // Yes, we need to parse children right here; otherwise this messes up the next column
611                let child_column_count;
612                (input, child_column_count) = parse_varint::<u32>(input)?;
613
614                // Each column requires at least 1 byte (ColumnType without a name)
615                let child_col_capacity = child_column_count.into_usize();
616                if input.len() < child_col_capacity {
617                    return Err(BufferUnderflow(child_column_count, input.len()));
618                }
619                let mut children = Vec::with_capacity(child_col_capacity);
620                for _ in 0..child_column_count {
621                    let child;
622                    (input, child) = Column::from_bytes(input, parser)?;
623                    children.push(child);
624                }
625                typ.children = children;
626            }
627            _ => {}
628        }
629        col_info.push(typ);
630    }
631    if geometries > 1 {
632        return Err(MultipleGeometryColumns);
633    }
634    if ids > 1 {
635        return Err(MultipleIdColumns);
636    }
637
638    Ok((input, (col_info, column_count - geometries - ids)))
639}
640
641impl<'a> RawScalar<'a> {
642    fn new(name: &'a str, presence: RawPresence<'a>, data: RawStream<'a>) -> Self {
643        Self {
644            name,
645            presence,
646            data,
647        }
648    }
649}
650
651impl RawPresence<'_> {
652    /// Whether this column carries presence data (some features may be null).
653    #[must_use]
654    pub(crate) fn is_optional(&self) -> bool {
655        !matches!(self, Self::AllPresent)
656    }
657}
658
659#[cfg(test)]
660mod tests {
661    use crate::{MltError, Parser};
662
663    #[test]
664    fn parse_layers_rejects_empty_layer_name() {
665        let bytes = [
666            5, // layer size: tag byte + 4-byte body
667            1, // tag 0x01
668            0, // empty layer name
669            0x80, 0x20, // extent 4096
670            0,    // column count
671        ];
672
673        assert!(matches!(
674            Parser::default().parse_layers(&bytes),
675            Err(MltError::MissingLayerName)
676        ));
677    }
678}