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}