Skip to main content

parquet_variant/variant/
metadata.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use crate::decoder::{OffsetSizeBytes, map_bytes_to_offsets};
19use crate::utils::{
20    first_byte_from_slice, overflow_error, slice_from_slice, slice_from_slice_at_offset,
21    string_from_slice, try_binary_search_range_by,
22};
23
24use arrow_schema::ArrowError;
25
26/// Header structure for [`VariantMetadata`]
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub(crate) struct VariantMetadataHeader {
29    version: u8,
30    is_sorted: bool,
31    /// Note: This is `offset_size_minus_one` + 1
32    offset_size: OffsetSizeBytes,
33}
34
35// According to the spec this is currently always = 1, and so we store this const for validation
36// purposes and to make that visible.
37const CORRECT_VERSION_VALUE: u8 = 1;
38
39// The metadata header occupies one byte; use a named constant for readability
40const NUM_HEADER_BYTES: u32 = 1;
41
42impl VariantMetadataHeader {
43    // Hide the cast
44    const fn offset_size(&self) -> u32 {
45        self.offset_size as u32
46    }
47
48    // Avoid materializing this offset, since it's cheaply and safely computable
49    const fn first_offset_byte(&self) -> u32 {
50        NUM_HEADER_BYTES + self.offset_size()
51    }
52
53    /// Tries to construct the variant metadata header, which has the form
54    ///
55    /// ```text
56    ///              7     6  5   4  3             0
57    ///             +-------+---+---+---------------+
58    /// header      |       |   |   |    version    |
59    ///             +-------+---+---+---------------+
60    ///                 ^         ^
61    ///                 |         +-- sorted_strings
62    ///                 +-- offset_size_minus_one
63    /// ```
64    ///
65    /// The version is a 4-bit value that must always contain the value 1.
66    /// - sorted_strings is a 1-bit value indicating whether dictionary strings are sorted and unique.
67    /// - offset_size_minus_one is a 2-bit value providing the number of bytes per dictionary size and offset field.
68    /// - The actual number of bytes, offset_size, is offset_size_minus_one + 1
69    pub(crate) fn try_new(header_byte: u8) -> Result<Self, ArrowError> {
70        let version = header_byte & 0x0F; // First four bits
71        if version != CORRECT_VERSION_VALUE {
72            let err_msg = format!(
73                "The version bytes in the header is not {CORRECT_VERSION_VALUE}, got {version:b}",
74            );
75            return Err(ArrowError::InvalidArgumentError(err_msg));
76        }
77        let is_sorted = (header_byte & 0x10) != 0; // Fifth bit
78        let offset_size_minus_one = header_byte >> 6; // Last two bits
79        Ok(Self {
80            version,
81            is_sorted,
82            offset_size: OffsetSizeBytes::try_new(offset_size_minus_one)?,
83        })
84    }
85}
86
87/// [`Variant`] Metadata
88///
89/// See the [Variant Spec] file for more information
90///
91/// # Validation
92///
93/// Every instance of variant metadata is either _valid_ or _invalid_. depending on whether the
94/// underlying bytes are a valid encoding of variant metadata (see below).
95///
96/// Instances produced by [`Self::try_new`] or [`Self::with_full_validation`] are fully _validated_. They always
97/// contain _valid_ data, and infallible accesses such as iteration and indexing are panic-free. The
98/// validation cost is linear in the number of underlying bytes.
99///
100/// Instances produced by [`Self::new`] are _unvalidated_ and so they may contain either _valid_ or
101/// _invalid_ data. Infallible accesses such as iteration and indexing will panic if the underlying
102/// bytes are _invalid_, and fallible alternatives such as [`Self::iter_try`] and [`Self::get`] are
103/// provided as panic-free alternatives. [`Self::with_full_validation`] can also be used to _validate_ an
104/// _unvalidated_ instance, if desired.
105///
106/// _Unvalidated_ instances can be constructed in constant time. This can be useful if the caller
107/// knows the underlying bytes were already validated previously, or if the caller intends to
108/// perform a small number of (fallible) accesses to a large dictionary.
109///
110/// A _validated_ variant [metadata instance guarantees that:
111///
112/// - header byte is valid
113/// - dictionary size is in bounds
114/// - offset array content is in-bounds
115/// - first offset is zero
116/// - last offset is in-bounds
117/// - all other offsets are in-bounds (*)
118/// - all offsets are non-decreasing (*)
119/// - all values are valid utf-8 (*)
120///
121/// NOTE: [`Self::new`] only skips expensive (non-constant cost) validation checks (marked by `(*)`
122/// in the list above); it panics any of the other checks fails.
123///
124/// # Safety
125///
126/// Even an _invalid_ variant metadata instance is still _safe_ to use in the Rust sense. Accessing
127/// it with infallible methods may cause panics but will never lead to undefined behavior.
128///
129/// [`Variant`]: crate::Variant
130/// [Variant Spec]: https://github.com/apache/parquet-format/blob/master/VariantEncoding.md#metadata-encoding
131#[derive(Debug, Clone, PartialEq)]
132pub struct VariantMetadata<'m> {
133    /// (Only) the bytes that make up this metadata instance.
134    pub(crate) bytes: &'m [u8],
135    header: VariantMetadataHeader,
136    dictionary_size: u32,
137    first_value_byte: u32,
138    validated: bool,
139}
140
141// We don't want this to grow because it increases the size of VariantList and VariantObject, which
142// could increase the size of Variant. All those size increases could hurt performance.
143#[cfg(target_pointer_width = "64")]
144const _: () = crate::utils::expect_size_of::<VariantMetadata>(32);
145
146#[cfg(target_pointer_width = "32")]
147const _: () = crate::utils::expect_size_of::<VariantMetadata>(20);
148
149/// The canonical byte slice corresponding to an empty metadata dictionary.
150///
151/// ```
152/// # use parquet_variant::{EMPTY_VARIANT_METADATA_BYTES, VariantMetadata, WritableMetadataBuilder};
153/// let mut metadata_builder = WritableMetadataBuilder::default();
154/// metadata_builder.finish();
155/// let metadata_bytes = metadata_builder.into_inner();
156/// assert_eq!(&metadata_bytes, EMPTY_VARIANT_METADATA_BYTES);
157/// ```
158pub const EMPTY_VARIANT_METADATA_BYTES: &[u8] = &[1, 0, 0];
159
160/// The empty metadata dictionary.
161///
162/// ```
163/// # use parquet_variant::{EMPTY_VARIANT_METADATA, VariantMetadata, WritableMetadataBuilder};
164/// let mut metadata_builder = WritableMetadataBuilder::default();
165/// metadata_builder.finish();
166/// let metadata_bytes = metadata_builder.into_inner();
167/// let empty_metadata = VariantMetadata::try_new(&metadata_bytes).unwrap();
168/// assert_eq!(empty_metadata, EMPTY_VARIANT_METADATA);
169/// ```
170pub const EMPTY_VARIANT_METADATA: VariantMetadata = VariantMetadata {
171    bytes: EMPTY_VARIANT_METADATA_BYTES,
172    header: VariantMetadataHeader {
173        version: CORRECT_VERSION_VALUE,
174        is_sorted: false,
175        offset_size: OffsetSizeBytes::One,
176    },
177    dictionary_size: 0,
178    first_value_byte: 3,
179    validated: true,
180};
181
182impl<'m> VariantMetadata<'m> {
183    /// Attempts to interpret `bytes` as a variant metadata instance, with full [validation] of all
184    /// dictionary entries.
185    ///
186    /// [validation]: Self#Validation
187    pub fn try_new(bytes: &'m [u8]) -> Result<Self, ArrowError> {
188        Self::try_new_with_shallow_validation(bytes)?.with_full_validation()
189    }
190
191    /// Interprets `bytes` as a variant metadata instance, without attempting to [validate] dictionary
192    /// entries. Panics if basic sanity checking fails, and subsequent infallible accesses such as
193    /// indexing and iteration could also panic if the underlying bytes are invalid.
194    ///
195    /// This constructor can be a useful lightweight alternative to [`Self::try_new`] if the bytes
196    /// were already validated previously by other means, or if the caller expects a small number of
197    /// accesses to a large dictionary (preferring to use a small number of fallible accesses as
198    /// needed, instead of paying expensive full validation up front).
199    ///
200    /// [validate]: Self#Validation
201    ///
202    /// # Panics
203    ///
204    /// Panics if basic sanity checking fails. Use [`Self::try_new`] for a fallible version.
205    pub fn new(bytes: &'m [u8]) -> Self {
206        Self::try_new_with_shallow_validation(bytes).expect("Invalid variant metadata")
207    }
208
209    // The actual constructor, which performs only basic (constant-const) validation.
210    pub(crate) fn try_new_with_shallow_validation(bytes: &'m [u8]) -> Result<Self, ArrowError> {
211        let header_byte = first_byte_from_slice(bytes)?;
212        let header = VariantMetadataHeader::try_new(header_byte)?;
213
214        // First element after header is dictionary size; the offset array immediately follows.
215        let dictionary_size =
216            header
217                .offset_size
218                .unpack_u32_at_offset(bytes, NUM_HEADER_BYTES as usize, 0)?;
219
220        // Calculate the starting offset of the dictionary string bytes.
221        //
222        // There are dict_size + 1 offsets, and the value bytes immediately follow
223        // = (dict_size + 1) * offset_size + header.first_offset_byte()
224        let first_value_byte = dictionary_size
225            .checked_add(1)
226            .and_then(|n| n.checked_mul(header.offset_size()))
227            .and_then(|n| n.checked_add(header.first_offset_byte()))
228            .ok_or_else(|| overflow_error("offset of variant metadata dictionary"))?;
229
230        let mut new_self = Self {
231            bytes,
232            header,
233            dictionary_size,
234            first_value_byte,
235            validated: false,
236        };
237
238        // Validate just the first and last offset, ignoring the other offsets and all value bytes.
239        let first_offset = new_self.get_offset(0)?;
240        if first_offset != 0 {
241            return Err(ArrowError::InvalidArgumentError(format!(
242                "First offset is not zero: {first_offset}"
243            )));
244        }
245
246        // Use the last offset to upper-bound the byte slice
247        let last_offset = new_self
248            .get_offset(dictionary_size as _)?
249            .checked_add(first_value_byte)
250            .ok_or_else(|| overflow_error("variant metadata size"))?;
251        new_self.bytes = slice_from_slice(bytes, ..last_offset as _)?;
252        Ok(new_self)
253    }
254
255    /// The number of metadata dictionary entries
256    pub fn len(&self) -> usize {
257        self.dictionary_size as _
258    }
259
260    /// True if this metadata dictionary contains no entries
261    pub fn is_empty(&self) -> bool {
262        self.len() == 0
263    }
264
265    /// True if this instance is fully [validated] for panic-free infallible accesses.
266    ///
267    /// [validated]: Self#Validation
268    pub fn is_fully_validated(&self) -> bool {
269        self.validated
270    }
271
272    /// Performs a full [validation] of this metadata dictionary and returns the result.
273    ///
274    /// [validation]: Self#Validation
275    pub fn with_full_validation(mut self) -> Result<Self, ArrowError> {
276        if !self.validated {
277            let offset_bytes = slice_from_slice(
278                self.bytes,
279                self.header.first_offset_byte() as _..self.first_value_byte as _,
280            )?;
281
282            // Verify the string values in the dictionary are UTF-8 encoded strings.
283            let value_buffer =
284                string_from_slice(self.bytes, 0, self.first_value_byte as _..self.bytes.len())?;
285
286            let mut offsets = map_bytes_to_offsets(offset_bytes, self.header.offset_size);
287            let mut current_offset = offsets.next().unwrap_or(0);
288
289            if self.header.is_sorted {
290                // Validate the dictionary values are unique and lexicographically sorted
291                //
292                // Since we use the offsets to access dictionary values, this also validates
293                // offsets are in-bounds and monotonically increasing
294                let mut prev_value: Option<&str> = None;
295                for next_offset in offsets {
296                    let current_value = value_buffer.get(current_offset..next_offset).ok_or_else(
297                        || {
298                            ArrowError::InvalidArgumentError(format!(
299                                "range {current_offset}..{next_offset} is invalid or out of bounds"
300                            ))
301                        },
302                    )?;
303
304                    if let Some(prev_val) = prev_value
305                        && current_value <= prev_val
306                    {
307                        return Err(ArrowError::InvalidArgumentError(
308                            "dictionary values are not unique and ordered".to_string(),
309                        ));
310                    }
311
312                    prev_value = Some(current_value);
313                    current_offset = next_offset;
314                }
315            } else {
316                // Slicing each dictionary value validates that offsets are in-bounds, non-decreasing,
317                // and land on UTF-8 character boundaries. Equal offsets are legal: they encode an
318                // empty dictionary entry.
319                for next_offset in offsets {
320                    value_buffer
321                        .get(current_offset..next_offset)
322                        .ok_or_else(|| {
323                            ArrowError::InvalidArgumentError(format!(
324                                "range {current_offset}..{next_offset} is invalid or out of bounds"
325                            ))
326                        })?;
327                    current_offset = next_offset;
328                }
329            }
330
331            self.validated = true;
332        }
333        Ok(self)
334    }
335
336    /// Whether the dictionary keys are sorted and unique
337    pub fn is_sorted(&self) -> bool {
338        self.header.is_sorted
339    }
340
341    /// The variant protocol version
342    pub const fn version(&self) -> u8 {
343        self.header.version
344    }
345
346    /// Gets an offset into the dictionary entry by index.
347    ///
348    /// This offset is an index into the dictionary, at the boundary between string `i-1` and string
349    /// `i`. See [`Self::get`] to retrieve a specific dictionary entry.
350    fn get_offset(&self, i: usize) -> Result<u32, ArrowError> {
351        let offset_byte_range = self.header.first_offset_byte() as _..self.first_value_byte as _;
352        let bytes = slice_from_slice(self.bytes, offset_byte_range)?;
353        self.header.offset_size.unpack_u32(bytes, i)
354    }
355
356    /// Returns the total size, in bytes, of the metadata.
357    ///
358    /// Note this value may be smaller than what was passed to [`Self::new`] or
359    /// [`Self::try_new`] if the input was larger than necessary to encode the
360    /// metadata dictionary.
361    pub fn size(&self) -> usize {
362        self.bytes.len()
363    }
364
365    /// Attempts to retrieve a dictionary entry by index, failing if out of bounds or if the
366    /// underlying bytes are [invalid].
367    ///
368    /// [invalid]: Self#Validation
369    pub fn get(&self, i: usize) -> Result<&'m str, ArrowError> {
370        let byte_range = self.get_offset(i)? as _..self.get_offset(i + 1)? as _;
371        if !self.validated {
372            return string_from_slice(self.bytes, self.first_value_byte as _, byte_range);
373        }
374
375        // Full validation already proved every dictionary entry is valid UTF-8, so validating
376        // again here would charge that cost once per field access instead of once per buffer.
377        let value_bytes =
378            slice_from_slice_at_offset(self.bytes, self.first_value_byte as _, byte_range)?;
379
380        debug_assert!(std::str::from_utf8(value_bytes).is_ok());
381
382        // SAFETY: `validated` is set only by `with_full_validation`, which proved that every
383        // dictionary entry, including this one, is valid UTF-8.
384        Ok(unsafe { str::from_utf8_unchecked(value_bytes) })
385    }
386
387    // Helper method used by our `impl Index` and also by `get_entry`. Panics if the underlying
388    // bytes are invalid. Needed because the `Index` trait forces the returned result to have the
389    // lifetime of `self` instead of the string's own (longer) lifetime `'m`.
390    fn get_impl(&self, i: usize) -> &'m str {
391        self.get(i).expect("Invalid metadata dictionary entry")
392    }
393
394    /// Attempts to resolve `field_name` to its field id under the assumption that `field_name` is
395    /// itself a slice of this dictionary's value region, as is the case for every field name
396    /// obtained from [`VariantObject::field_name`] or [`Self::get`] on the same metadata instance.
397    /// Returns `None` when that assumption does not hold, so callers must be prepared to fall back
398    /// to a name-based search such as [`Self::get_entry`].
399    ///
400    /// This is much cheaper than searching by name, because a borrowed field name already encodes
401    /// its own field id: it belongs to the entry whose dictionary offset equals its distance from
402    /// the start of the value region. Finding that entry is a binary search over the
403    /// (non-decreasing) offset array, comparing integers rather than decoding and comparing
404    /// dictionary strings at every step, and it needs no string comparison to confirm the hit.
405    ///
406    /// # Correctness
407    ///
408    /// This is only attempted for a [sorted] dictionary. The spec requires dictionary entries to
409    /// be unique only when `sorted_strings` is set, so an unsorted dictionary may legally hold the
410    /// same string at more than one field id. Resolving a borrowed name by the id it came from
411    /// would then disagree with a resolution by name, and callers rely on a name mapping to a
412    /// single id: [`ObjectBuilder`] detects duplicate fields by comparing field ids, so two ids
413    /// naming the same string would build an object whose field names are not unique. Requiring
414    /// sortedness makes the two resolutions agree, because validation rejects a sorted dictionary
415    /// with duplicate entries.
416    ///
417    /// A returned field id is also always verified, so this never reports an id whose entry is not
418    /// `field_name` itself, even for [invalid] metadata whose offsets are arbitrary. The candidate
419    /// entry is accepted only when it starts at `field_name`'s address and has exactly
420    /// `field_name`'s length, which makes the entry's bytes and `field_name`'s bytes the same
421    /// bytes. (Two live allocations cannot overlap, so an address inside our own byte range
422    /// belongs to our own bytes. In the degenerate zero-length case the two are both empty and
423    /// therefore still equal.)
424    ///
425    /// [`ObjectBuilder`]: crate::ObjectBuilder
426    /// [`VariantObject::field_name`]: crate::VariantObject::field_name
427    /// [invalid]: Self#Validation
428    /// [sorted]: Self::is_sorted
429    pub(crate) fn borrowed_field_id(&self, field_name: &str) -> Option<u32> {
430        // An unsorted dictionary may hold duplicate entries, which would make the id a name was
431        // borrowed from differ from the id a search by name returns; see "Correctness" above.
432        if !self.is_sorted() {
433            return None;
434        }
435
436        // Addresses are compared as integers and never dereferenced, so this stays safe even when
437        // `field_name` borrows from an unrelated allocation.
438        let value_region_start = (self.bytes.as_ptr() as usize) + self.first_value_byte as usize;
439        let value_region_end = (self.bytes.as_ptr() as usize) + self.bytes.len();
440        let field_name_start = field_name.as_ptr() as usize;
441        if field_name_start < value_region_start || field_name_start >= value_region_end {
442            return None;
443        }
444        let field_name_offset = u32::try_from(field_name_start - value_region_start).ok()?;
445
446        // Hoist the offset array out of the search, so each step is just an unaligned load.
447        let offset_byte_range = self.header.first_offset_byte() as _..self.first_value_byte as _;
448        let offsets = slice_from_slice(self.bytes, offset_byte_range).ok()?;
449        let offset_size = self.header.offset_size;
450        let cmp = |i| {
451            Some(
452                offset_size
453                    .unpack_u32(offsets, i)
454                    .ok()?
455                    .cmp(&field_name_offset),
456            )
457        };
458        let field_id = try_binary_search_range_by(0..self.len(), cmp)?.ok()?;
459
460        // Verify that this entry has exactly `field_name`'s bytes; see "Correctness" above.
461        let entry_end = offset_size.unpack_u32(offsets, field_id + 1).ok()?;
462        let entry_len = entry_end.checked_sub(field_name_offset)?;
463        (entry_len as usize == field_name.len()).then_some(field_id as u32)
464    }
465
466    /// Attempts to retrieve a dictionary entry and its field id, returning None if the requested field
467    /// name is not present. The search cost is logarithmic if [`Self::is_sorted`] and linear
468    /// otherwise.
469    ///
470    /// # Panics
471    ///
472    /// Panics if the underlying bytes are [invalid].
473    ///
474    /// [invalid]: Self#Validation
475    pub fn get_entry(&self, field_name: &str) -> Option<(u32, &'m str)> {
476        // A field name borrowed from this dictionary's (sorted) value region resolves without
477        // any string comparisons.
478        if let Some(field_id) = self.borrowed_field_id(field_name) {
479            return Some((field_id, self.get_impl(field_id as _)));
480        }
481
482        let field_id = if self.is_sorted() && self.len() > 10 {
483            // Binary search is faster for a not-tiny sorted metadata dictionary
484            let cmp = |i| Some(self.get_impl(i).cmp(field_name));
485            try_binary_search_range_by(0..self.len(), cmp)?.ok()?
486        } else {
487            // Fall back to Linear search for tiny or unsorted dictionary
488            (0..self.len()).find(|i| self.get_impl(*i) == field_name)?
489        };
490        Some((field_id as u32, self.get_impl(field_id)))
491    }
492
493    /// Returns an iterator that attempts to visit all dictionary entries, producing `Err` if the
494    /// iterator encounters [invalid] data.
495    ///
496    /// [invalid]: Self#Validation
497    pub fn iter_try(&self) -> impl Iterator<Item = Result<&'m str, ArrowError>> + '_ {
498        (0..self.len()).map(|i| self.get(i))
499    }
500
501    /// Iterates over all dictionary entries. When working with [unvalidated] input, consider
502    /// [`Self::iter_try`] to avoid panics due to invalid data.
503    ///
504    /// [unvalidated]: Self#Validation
505    ///
506    /// # Panics
507    ///
508    /// Panics if the underlying bytes are invalid. Use [`Self::iter_try`] for a
509    /// fallible version.
510    pub fn iter(&self) -> impl Iterator<Item = &'m str> + '_ {
511        self.iter_try()
512            .map(|result| result.expect("Invalid metadata dictionary entry"))
513    }
514}
515
516/// Retrieves the ith dictionary entry, panicking if the index is out of bounds. Accessing
517/// [unvalidated] input could also panic if the underlying bytes are invalid.
518///
519/// [unvalidated]: Self#Validation
520impl std::ops::Index<usize> for VariantMetadata<'_> {
521    type Output = str;
522
523    fn index(&self, i: usize) -> &str {
524        self.get_impl(i)
525    }
526}
527
528#[cfg(test)]
529mod tests {
530
531    use crate::VariantBuilder;
532
533    use super::*;
534
535    /// `"cat"`, `"dog"` – valid metadata
536    #[test]
537    fn try_new_ok_inline() {
538        let bytes = &[
539            0b0000_0001, // header, offset_size_minus_one=0 and version=1
540            0x02,        // dictionary_size (2 strings)
541            0x00,
542            0x03,
543            0x06,
544            b'c',
545            b'a',
546            b't',
547            b'd',
548            b'o',
549            b'g',
550        ];
551
552        let md = VariantMetadata::try_new(bytes).expect("should parse");
553        assert_eq!(md.len(), 2);
554        // Fields
555        assert_eq!(&md[0], "cat");
556        assert_eq!(&md[1], "dog");
557
558        // Offsets
559        assert_eq!(md.get_offset(0).unwrap(), 0x00);
560        assert_eq!(md.get_offset(1).unwrap(), 0x03);
561        assert_eq!(md.get_offset(2).unwrap(), 0x06);
562
563        let err = md.get_offset(3).unwrap_err();
564        assert!(
565            matches!(err, ArrowError::InvalidArgumentError(_)),
566            "unexpected error: {err:?}"
567        );
568
569        let fields: Vec<(usize, &str)> = md.iter().enumerate().collect();
570        assert_eq!(fields, vec![(0usize, "cat"), (1usize, "dog")]);
571    }
572
573    /// Too short buffer test (missing one required offset).
574    /// Should error with "metadata shorter than dictionary_size implies".
575    #[test]
576    fn try_new_missing_last_value() {
577        let bytes = &[
578            0b0000_0001, // header, offset_size_minus_one=0 and version=1
579            0x02,        // dictionary_size = 2
580            0x00,
581            0x01,
582            0x02,
583            b'a',
584            b'b', // <-- we'll remove this
585        ];
586
587        let working_md = VariantMetadata::try_new(bytes).expect("should parse");
588        assert_eq!(working_md.len(), 2);
589        assert_eq!(&working_md[0], "a");
590        assert_eq!(&working_md[1], "b");
591
592        let truncated = &bytes[..bytes.len() - 1];
593
594        let err = VariantMetadata::try_new(truncated).unwrap_err();
595        assert!(
596            matches!(err, ArrowError::InvalidArgumentError(_)),
597            "unexpected error: {err:?}"
598        );
599    }
600
601    #[test]
602    fn try_new_fails_non_monotonic() {
603        // 'cat', 'dog', 'lamb'
604        let bytes = &[
605            0b0000_0001, // header, offset_size_minus_one=0 and version=1
606            0x03,        // dictionary_size
607            0x00,
608            0x02,
609            0x01, // Doesn't increase monotonically
610            0x10,
611            b'c',
612            b'a',
613            b't',
614            b'd',
615            b'o',
616            b'g',
617            b'l',
618            b'a',
619            b'm',
620            b'b',
621        ];
622
623        let err = VariantMetadata::try_new(bytes).unwrap_err();
624        assert!(
625            matches!(err, ArrowError::InvalidArgumentError(_)),
626            "unexpected error: {err:?}"
627        );
628    }
629
630    #[test]
631    fn try_new_fails_non_monotonic2() {
632        // this test case checks whether offsets are monotonic in the full validation logic.
633
634        // 'cat', 'dog', 'lamb', "eel"
635        let bytes = &[
636            0b0000_0001, // header, offset_size_minus_one=0 and version=1
637            4,           // dictionary_size
638            0x00,
639            0x02,
640            0x01, // Doesn't increase monotonically
641            0x10,
642            13,
643            b'c',
644            b'a',
645            b't',
646            b'd',
647            b'o',
648            b'g',
649            b'l',
650            b'a',
651            b'm',
652            b'b',
653            b'e',
654            b'e',
655            b'l',
656        ];
657
658        let err = VariantMetadata::try_new(bytes).unwrap_err();
659
660        assert!(
661            matches!(err, ArrowError::InvalidArgumentError(_)),
662            "unexpected error: {err:?}"
663        );
664    }
665
666    #[test]
667    fn try_new_truncated_offsets_inline() {
668        // Missing final offset
669        let bytes = &[0b0000_0001, 0x02, 0x00, 0x01];
670
671        let err = VariantMetadata::try_new(bytes).unwrap_err();
672        assert!(
673            matches!(err, ArrowError::InvalidArgumentError(_)),
674            "unexpected error: {err:?}"
675        );
676    }
677
678    #[test]
679    fn empty_string_is_valid() {
680        let bytes = &[
681            0b0001_0001, // header: offset_size_minus_one=0, ordered=1, version=1
682            1,
683            0x00,
684            0x00,
685        ];
686        let metadata = VariantMetadata::try_new(bytes).unwrap();
687        assert_eq!(&metadata[0], "");
688
689        let bytes = &[
690            0b0001_0001, // header: offset_size_minus_one=0, ordered=1, version=1
691            2,
692            0x00,
693            0x00,
694            0x02,
695            b'h',
696            b'i',
697        ];
698        let metadata = VariantMetadata::try_new(bytes).unwrap();
699        assert_eq!(&metadata[0], "");
700        assert_eq!(&metadata[1], "hi");
701
702        let bytes = &[
703            0b0001_0001, // header: offset_size_minus_one=0, ordered=1, version=1
704            2,
705            0x00,
706            0x02,
707            0x02, // empty string is allowed, but must be first in a sorted dict
708            b'h',
709            b'i',
710        ];
711        let err = VariantMetadata::try_new(bytes).unwrap_err();
712        assert!(
713            matches!(err, ArrowError::InvalidArgumentError(_)),
714            "unexpected error: {err:?}"
715        );
716
717        let bytes = &[
718            0b0000_0001, // header: offset_size_minus_one=0, ordered=0, version=1
719            2,
720            0x00,
721            0x02,
722            0x02, // an unsorted dict may hold an empty string anywhere
723            b'h',
724            b'i',
725        ];
726        let metadata = VariantMetadata::try_new(bytes).unwrap();
727        assert_eq!(&metadata[0], "hi");
728        assert_eq!(&metadata[1], "");
729    }
730
731    /// Builds a metadata dictionary containing `field_names`, in the order given.
732    fn metadata_bytes_for(field_names: &[&str]) -> Vec<u8> {
733        let mut builder = VariantBuilder::new().with_field_names(field_names.iter().copied());
734        let mut object = builder.new_object();
735        for name in field_names {
736            object.insert(name, 1i32);
737        }
738        object.finish();
739        builder.finish().0
740    }
741
742    /// Every entry of `metadata` must resolve to its own field id through the general name search
743    /// and through an owned (non-borrowed) copy of the name. The borrowed fast path must agree
744    /// whenever it applies, which is only for a sorted dictionary.
745    fn assert_lookups_agree(metadata: &VariantMetadata<'_>) {
746        for i in 0..metadata.len() {
747            let borrowed = metadata.get(i).unwrap();
748            let owned = borrowed.to_string();
749
750            assert_eq!(
751                metadata.borrowed_field_id(borrowed),
752                metadata.is_sorted().then_some(i as u32),
753                "borrowed lookup of {borrowed:?} (field id {i})"
754            );
755            assert_eq!(
756                metadata.get_entry(borrowed),
757                Some((i as u32, borrowed)),
758                "get_entry of borrowed {borrowed:?} (field id {i})"
759            );
760            assert_eq!(
761                metadata.get_entry(owned.as_str()),
762                Some((i as u32, borrowed)),
763                "get_entry of owned {borrowed:?} (field id {i})"
764            );
765            // An owned copy does not borrow from the dictionary, so it cannot take the fast path.
766            assert_eq!(metadata.borrowed_field_id(owned.as_str()), None);
767        }
768    }
769
770    /// Like [`assert_lookups_agree`], but tolerates the fast path declining an entry, as it may
771    /// for a dictionary whose offset array does not increase strictly.
772    fn assert_lookups_agree_or_decline(metadata: &VariantMetadata<'_>) {
773        for i in 0..metadata.len() {
774            let borrowed = metadata.get(i).unwrap();
775            if let Some(field_id) = metadata.borrowed_field_id(borrowed) {
776                assert_eq!(field_id, i as u32, "borrowed lookup of {borrowed:?}");
777            }
778            assert_eq!(
779                metadata.get_entry(borrowed),
780                Some((i as u32, borrowed)),
781                "get_entry of borrowed {borrowed:?} (field id {i})"
782            );
783        }
784    }
785
786    #[test]
787    fn test_borrowed_field_id_sorted_dictionary() {
788        // More than 10 entries, so `get_entry` uses its binary search path.
789        let names: Vec<String> = (0..64).map(|i| format!("field_{i:03}")).collect();
790        let names: Vec<&str> = names.iter().map(String::as_str).collect();
791        let bytes = metadata_bytes_for(&names);
792        let metadata = VariantMetadata::try_new(&bytes).unwrap();
793
794        assert!(metadata.is_sorted());
795        assert_eq!(metadata.len(), 64);
796        assert_lookups_agree(&metadata);
797
798        assert_eq!(metadata.get_entry("field_999"), None);
799        assert_eq!(metadata.borrowed_field_id("field_999"), None);
800    }
801
802    #[test]
803    fn test_borrowed_field_id_unsorted_dictionary() {
804        let bytes = metadata_bytes_for(&["zebra", "apple", "mango", "kiwi"]);
805        let metadata = VariantMetadata::try_new(&bytes).unwrap();
806
807        assert!(!metadata.is_sorted());
808        assert_eq!(metadata.len(), 4);
809        // An unsorted dictionary may hold duplicate entries, so the fast path never applies to it
810        // and every name falls back to the general search.
811        assert_lookups_agree(&metadata);
812    }
813
814    /// A dictionary that is not marked sorted may legally repeat a string, and the two field ids
815    /// naming it must not become distinguishable by where the caller's name was borrowed from:
816    /// `ObjectBuilder` detects duplicate fields by field id, so a name must map to a single id.
817    #[test]
818    fn test_borrowed_field_id_unsorted_dictionary_with_duplicate_entries() {
819        let bytes = &[
820            0b0000_0001, // header: offset_size_minus_one=0, sorted=0, version=1
821            2,           // dictionary_size
822            0x00,
823            0x01,
824            0x02,
825            b'a',
826            b'a',
827        ];
828        let metadata = VariantMetadata::try_new(bytes).unwrap();
829        assert!(!metadata.is_sorted());
830        assert_eq!(metadata.get(0).unwrap(), "a");
831        assert_eq!(metadata.get(1).unwrap(), "a");
832
833        // Whichever entry a name was borrowed from, it resolves to the first entry naming it.
834        let owned = String::from("a");
835        for name in [metadata.get(0).unwrap(), metadata.get(1).unwrap(), &owned] {
836            assert_eq!(metadata.borrowed_field_id(name), None);
837            assert_eq!(metadata.get_entry(name), Some((0, "a")));
838        }
839    }
840
841    #[test]
842    fn test_borrowed_field_id_ignores_names_from_another_dictionary() {
843        let this_bytes = metadata_bytes_for(&["alpha", "beta", "gamma"]);
844        let this = VariantMetadata::try_new(&this_bytes).unwrap();
845
846        // A different dictionary that shares some names, with different field ids for them.
847        let other_bytes = metadata_bytes_for(&["delta", "gamma", "beta", "alpha"]);
848        let other = VariantMetadata::try_new(&other_bytes).unwrap();
849
850        for i in 0..other.len() {
851            let name = other.get(i).unwrap();
852            // A copy that borrows from neither dictionary, to compare results against.
853            let unborrowed_name: String = name.chars().collect();
854            // The name borrows from `other`, so `this` must not take the fast path for it...
855            assert_eq!(this.borrowed_field_id(name), None);
856            // ...and the general search must still return `this`'s own field id for that name.
857            assert_eq!(this.get_entry(name), this.get_entry(&unborrowed_name));
858        }
859
860        assert_eq!(this.get_entry(other.get(3).unwrap()), Some((0, "alpha")));
861        assert_eq!(this.get_entry(other.get(0).unwrap()), None);
862    }
863
864    #[test]
865    fn test_borrowed_field_id_rejects_substring_of_an_entry() {
866        // Dictionary ["x", "yy"], stored as the bytes "xyy". A slice of entry 1 that starts one
867        // byte into the value region shares its start offset with entry 1 without being equal to
868        // it, which the fast path must detect rather than reporting a bogus field id. The
869        // dictionary is sorted, so the fast path applies to it.
870        let bytes = &[
871            0b0001_0001, // header: offset_size_minus_one=0, ordered=1, version=1
872            2,           // dictionary_size
873            0x00,
874            0x01,
875            0x03,
876            b'x',
877            b'y',
878            b'y',
879        ];
880        let metadata = VariantMetadata::try_new(bytes).unwrap();
881        assert!(metadata.is_sorted());
882        assert_eq!(metadata.get(0).unwrap(), "x");
883        assert_eq!(metadata.get(1).unwrap(), "yy");
884        assert_eq!(
885            metadata.borrowed_field_id(metadata.get(1).unwrap()),
886            Some(1)
887        );
888
889        let prefix_of_entry_1 = &metadata.get(1).unwrap()[..1];
890        assert_eq!(prefix_of_entry_1, "y");
891        assert_eq!(metadata.borrowed_field_id(prefix_of_entry_1), None);
892        assert_eq!(metadata.get_entry(prefix_of_entry_1), None);
893
894        // A slice that starts inside an entry, at an offset no entry starts at, is also rejected.
895        let suffix_of_entry_1 = &metadata.get(1).unwrap()[1..];
896        assert_eq!(metadata.borrowed_field_id(suffix_of_entry_1), None);
897    }
898
899    #[test]
900    fn test_borrowed_field_id_with_empty_field_name() {
901        let bytes = &[
902            0b0000_0001, // header: offset_size_minus_one=0, ordered=0, version=1
903            2,           // dictionary_size
904            0x00,
905            0x02,
906            0x02, // an unsorted dict may hold an empty string anywhere
907            b'h',
908            b'i',
909        ];
910        let metadata = VariantMetadata::try_new(bytes).unwrap();
911        assert!(!metadata.is_sorted());
912        assert_eq!(metadata.get(0).unwrap(), "hi");
913        assert_eq!(metadata.get(1).unwrap(), "");
914
915        assert_eq!(
916            metadata.get_entry(metadata.get(0).unwrap()),
917            Some((0, "hi"))
918        );
919        assert_eq!(metadata.get_entry(metadata.get(1).unwrap()), Some((1, "")));
920        assert_eq!(metadata.get_entry(""), Some((1, "")));
921        assert_eq!(metadata.borrowed_field_id(metadata.get(0).unwrap()), None);
922    }
923
924    #[test]
925    fn test_borrowed_field_id_with_leading_empty_field_name() {
926        // A sorted dictionary can only hold an empty string as its first entry, where it shares a
927        // start offset with the entry after it. The fast path is free to decline such an
928        // ambiguous offset, but must never report the wrong field id for it.
929        let bytes = &[
930            0b0001_0001, // header: offset_size_minus_one=0, ordered=1, version=1
931            2,           // dictionary_size
932            0x00,
933            0x00,
934            0x02,
935            b'h',
936            b'i',
937        ];
938        let metadata = VariantMetadata::try_new(bytes).unwrap();
939        assert!(metadata.is_sorted());
940        assert_eq!(metadata.get(0).unwrap(), "");
941        assert_eq!(metadata.get(1).unwrap(), "hi");
942
943        assert_lookups_agree_or_decline(&metadata);
944        assert_eq!(metadata.get_entry(""), Some((0, "")));
945    }
946
947    #[test]
948    fn test_compare_sorted_dictionary_with_unsorted_dictionary() {
949        // create a sorted object
950        let mut b = VariantBuilder::new();
951        let mut o = b.new_object();
952
953        o.insert("a", false);
954        o.insert("b", false);
955
956        o.finish();
957
958        let (m, _) = b.finish();
959
960        let m1 = VariantMetadata::new(&m);
961        assert!(m1.is_sorted());
962
963        // Create metadata with an unsorted dictionary (field names are "a", "a", "b")
964        // Since field names are not unique, it is considered not sorted.
965        let metadata_bytes = vec![
966            0b0000_0001,
967            3, // dictionary size
968            0, // "a"
969            1, // "a"
970            2, // "b"
971            3,
972            b'a',
973            b'a',
974            b'b',
975        ];
976        let m2 = VariantMetadata::try_new(&metadata_bytes).unwrap();
977        assert!(!m2.is_sorted());
978
979        assert_ne!(m1, m2);
980    }
981
982    #[test]
983    fn test_compare_sorted_dictionary_with_sorted_dictionary() {
984        // create a sorted object
985        let mut b = VariantBuilder::new();
986        let mut o = b.new_object();
987
988        o.insert("a", false);
989        o.insert("b", false);
990
991        o.finish();
992
993        let (m, _) = b.finish();
994
995        let m1 = VariantMetadata::new(&m);
996        let m2 = VariantMetadata::new(&m);
997
998        assert_eq!(m1, m2);
999    }
1000
1001    #[test]
1002    fn test_empty_string_field_names() {
1003        // Field names are added to the dictionary in insertion order, so this dictionary is
1004        // unsorted, and the empty field name makes its last two offsets equal.
1005        let mut b = VariantBuilder::new().with_field_names(["b", "a", ""]);
1006        let mut o = b.new_object();
1007
1008        o.insert("b", false);
1009        o.insert("a", false);
1010        o.insert("", false);
1011
1012        o.finish();
1013
1014        let (m, _) = b.finish();
1015
1016        let metadata = VariantMetadata::try_new(&m).unwrap();
1017        assert!(!metadata.is_sorted());
1018        assert_eq!(metadata.iter().collect::<Vec<_>>(), vec!["b", "a", ""]);
1019    }
1020
1021    /// Builds a metadata buffer directly, so that offsets which a builder would never emit can be
1022    /// exercised. `values` is the raw dictionary value region.
1023    fn raw_metadata(is_sorted: bool, offsets: &[u8], values: &[u8]) -> Vec<u8> {
1024        let mut bytes = vec![0x01 | (u8::from(is_sorted) << 4)];
1025        bytes.push(offsets.len() as u8 - 1); // dictionary_size
1026        bytes.extend_from_slice(offsets);
1027        bytes.extend_from_slice(values);
1028        bytes
1029    }
1030
1031    /// `get` skips UTF-8 revalidation once the metadata is fully validated, so full validation has
1032    /// to reject offsets that split a multi-byte character. Otherwise a validated instance could
1033    /// hand out a `&str` over a partial character.
1034    #[test]
1035    fn full_validation_rejects_offsets_splitting_a_character() {
1036        // "é" is two bytes, so an offset of 1 lands inside it. The value region as a whole is
1037        // still valid UTF-8, so only the per-entry check can catch this.
1038        for is_sorted in [false, true] {
1039            let bytes = raw_metadata(is_sorted, &[0, 1, 2], "é".as_bytes());
1040
1041            // Shallow validation looks only at the first and last offset, so it accepts.
1042            let shallow = VariantMetadata::try_new_with_shallow_validation(&bytes).unwrap();
1043            assert!(!shallow.is_fully_validated());
1044            // ... and the fallible accessor reports the bad entry rather than panicking.
1045            assert!(shallow.get(0).is_err());
1046
1047            // Full validation must reject the buffer outright.
1048            assert!(
1049                VariantMetadata::try_new(&bytes).is_err(),
1050                "is_sorted={is_sorted}: full validation accepted offsets splitting a character"
1051            );
1052        }
1053    }
1054
1055    /// Validated and unvalidated instances must agree on every entry, including multi-byte
1056    /// characters and empty entries, since they take different code paths inside `get`.
1057    #[test]
1058    fn validated_and_unvalidated_get_agree() {
1059        // Unsorted so that the dictionary order below is preserved verbatim.
1060        let values = "aé€\u{10348}"; // 1, 2, 3 and 4 byte characters
1061        let offsets: &[u8] = &[0, 1, 1, 3, 6, 10]; // note the repeated 1: an empty entry
1062        let bytes = raw_metadata(false, offsets, values.as_bytes());
1063
1064        let validated = VariantMetadata::try_new(&bytes).unwrap();
1065        assert!(validated.is_fully_validated());
1066        let unvalidated = VariantMetadata::try_new_with_shallow_validation(&bytes).unwrap();
1067        assert!(!unvalidated.is_fully_validated());
1068
1069        let expected = ["a", "", "é", "€", "\u{10348}"];
1070        assert_eq!(validated.len(), expected.len());
1071        for (i, want) in expected.iter().enumerate() {
1072            assert_eq!(validated.get(i).unwrap(), *want, "validated get({i})");
1073            assert_eq!(unvalidated.get(i).unwrap(), *want, "unvalidated get({i})");
1074        }
1075
1076        // Out of bounds stays an error on both paths, rather than reading past the offset array.
1077        assert!(validated.get(expected.len()).is_err());
1078        assert!(unvalidated.get(expected.len()).is_err());
1079    }
1080
1081    /// An unvalidated instance still reports invalid UTF-8 through the fallible accessor.
1082    #[test]
1083    fn unvalidated_get_still_reports_invalid_utf8() {
1084        let bytes = raw_metadata(false, &[0, 1], &[0xFF]);
1085        let unvalidated = VariantMetadata::try_new_with_shallow_validation(&bytes).unwrap();
1086        assert!(unvalidated.get(0).is_err());
1087        assert!(VariantMetadata::try_new(&bytes).is_err());
1088    }
1089}