Skip to main content

vortex_array/
serde.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::borrow::Cow;
5use std::fmt::Debug;
6use std::fmt::Formatter;
7use std::iter;
8use std::sync::Arc;
9
10use flatbuffers::FlatBufferBuilder;
11use flatbuffers::Follow;
12use flatbuffers::WIPOffset;
13use flatbuffers::root;
14use vortex_buffer::Alignment;
15use vortex_buffer::ByteBuffer;
16use vortex_error::VortexError;
17use vortex_error::VortexExpect;
18use vortex_error::VortexResult;
19use vortex_error::vortex_bail;
20use vortex_error::vortex_err;
21use vortex_error::vortex_panic;
22use vortex_flatbuffers::FlatBuffer;
23use vortex_flatbuffers::WriteFlatBuffer;
24use vortex_flatbuffers::array as fba;
25use vortex_flatbuffers::array::Compression;
26use vortex_session::VortexSession;
27use vortex_session::registry::ReadContext;
28use vortex_utils::aliases::hash_map::HashMap;
29
30use crate::ArrayContext;
31use crate::ArrayRef;
32use crate::ArraySlots;
33use crate::array::ArrayDeserialization;
34use crate::array::ArrayId;
35use crate::array::new_foreign_array;
36use crate::buffer::BufferHandle;
37use crate::dtype::DType;
38use crate::dtype::TryFromBytes;
39use crate::session::ArraySessionExt;
40use crate::stats::StatsSet;
41
42/// Options for serializing an array.
43#[derive(Default, Debug)]
44pub struct SerializeOptions {
45    /// The starting position within an external stream or file. This offset is used to compute
46    /// appropriate padding to enable zero-copy reads.
47    pub offset: usize,
48    /// Whether to include sufficient zero-copy padding.
49    pub include_padding: bool,
50}
51
52impl ArrayRef {
53    /// Serialize the array into a sequence of byte buffers that should be written contiguously.
54    /// This function returns a vec to avoid copying data buffers.
55    ///
56    /// Optionally, padding can be included to guarantee buffer alignment and ensure zero-copy
57    /// reads within the context of an external file or stream. In this case, the alignment of
58    /// the first byte buffer should be respected when writing the buffers to the stream or file.
59    ///
60    /// The format of this blob is a sequence of data buffers, possible with prefixed padding,
61    /// followed by a flatbuffer containing an [`fba::Array`] message, and ending with a
62    /// little-endian u32 describing the length of the flatbuffer message.
63    pub fn serialize(
64        &self,
65        ctx: &ArrayContext,
66        session: &VortexSession,
67        options: &SerializeOptions,
68    ) -> VortexResult<Vec<ByteBuffer>> {
69        // Resolve the wire representation once. Serializers may choose historical IDs and may
70        // provide downgraded buffers or children that differ from the in-memory array tree.
71        let root = ArrayNodeFlatBuffer::try_new(ctx, session, self)?;
72        let array_buffers = root.array.buffers();
73
74        // Allocate result buffers, including a possible padding buffer for each.
75        let mut buffers = vec![];
76        let mut fb_buffers = Vec::with_capacity(buffers.capacity());
77
78        // If we're including padding, we need to find the maximum required buffer alignment.
79        let max_alignment = array_buffers
80            .iter()
81            .map(|buf| buf.alignment())
82            .chain(iter::once(FlatBuffer::alignment()))
83            .max()
84            .unwrap_or_else(FlatBuffer::alignment);
85
86        // Create a shared buffer of zeros we can use for padding
87        let zeros = ByteBuffer::zeroed(max_alignment.as_usize());
88
89        // We push an empty buffer with the maximum alignment, so then subsequent buffers
90        // will be aligned. For subsequent buffers, we always push a 1-byte alignment.
91        buffers.push(ByteBuffer::zeroed_aligned(0, max_alignment));
92
93        // Keep track of where we are in the "file" to calculate padding.
94        let mut pos = options.offset;
95
96        // Push all the array buffers with padding as necessary.
97        for buffer in array_buffers {
98            let padding = if options.include_padding {
99                let padding = pos.next_multiple_of(buffer.alignment().as_usize()) - pos;
100                if padding > 0 {
101                    pos += padding;
102                    buffers.push(zeros.slice(0..padding));
103                }
104                padding
105            } else {
106                0
107            };
108
109            fb_buffers.push(fba::Buffer::new(
110                u16::try_from(padding).vortex_expect("padding fits into u16"),
111                buffer.alignment().exponent(),
112                Compression::None,
113                u32::try_from(buffer.len())
114                    .map_err(|_| vortex_err!("All buffers must fit into u32 for serialization"))?,
115            ));
116
117            pos += buffer.len();
118            buffers.push(buffer.aligned(Alignment::none()));
119        }
120
121        // Set up the flatbuffer builder
122        let mut fbb = FlatBufferBuilder::new();
123
124        let fb_root = root.try_write_flatbuffer(&mut fbb)?;
125
126        let fb_buffers = fbb.create_vector(&fb_buffers);
127        let fb_array = fba::Array::create(
128            &mut fbb,
129            &fba::ArrayArgs {
130                root: Some(fb_root),
131                buffers: Some(fb_buffers),
132            },
133        );
134        fbb.finish_minimal(fb_array);
135        let (fb_vec, fb_start) = fbb.collapse();
136        let fb_end = fb_vec.len();
137        let fb_buffer = ByteBuffer::from(fb_vec).slice(fb_start..fb_end);
138        let fb_length = fb_buffer.len();
139
140        if options.include_padding {
141            let padding = pos.next_multiple_of(FlatBuffer::alignment().as_usize()) - pos;
142            if padding > 0 {
143                buffers.push(zeros.slice(0..padding));
144            }
145        }
146        buffers.push(fb_buffer);
147
148        // Finally, we write down the u32 length for the flatbuffer.
149        buffers.push(ByteBuffer::from(
150            u32::try_from(fb_length)
151                .map_err(|_| vortex_err!("Array metadata flatbuffer must fit into u32 for serialization. Array encoding tree is too large."))?
152                .to_le_bytes()
153                .to_vec(),
154        ));
155
156        Ok(buffers)
157    }
158}
159
160#[derive(Clone, Debug)]
161struct ArraySerializationTree {
162    source: ArrayRef,
163    serialized_id: ArrayId,
164    metadata: Vec<u8>,
165    buffers: Vec<ByteBuffer>,
166    children: Vec<ArraySerializationTree>,
167}
168
169impl ArraySerializationTree {
170    fn try_new(session: &VortexSession, source: &ArrayRef) -> VortexResult<Self> {
171        let Some(serialization) = session.array_serialize(source)? else {
172            vortex_bail!(
173                "Array {} does not support serialization",
174                source.encoding_id()
175            );
176        };
177        let children = serialization
178            .children
179            .iter()
180            .map(|child| Self::try_new(session, child))
181            .collect::<VortexResult<Vec<_>>>()?;
182
183        Ok(Self {
184            source: source.clone(),
185            serialized_id: serialization.serialized_id,
186            metadata: serialization.metadata,
187            buffers: serialization.buffers,
188            children,
189        })
190    }
191
192    fn nbuffers_recursive(&self) -> usize {
193        self.buffers.len()
194            + self
195                .children
196                .iter()
197                .map(Self::nbuffers_recursive)
198                .sum::<usize>()
199    }
200
201    fn buffers(&self) -> Vec<ByteBuffer> {
202        let mut buffers = Vec::with_capacity(self.nbuffers_recursive());
203        self.append_buffers(&mut buffers);
204        buffers
205    }
206
207    fn append_buffers(&self, buffers: &mut Vec<ByteBuffer>) {
208        buffers.extend(self.buffers.iter().cloned());
209        for child in &self.children {
210            child.append_buffers(buffers);
211        }
212    }
213}
214
215/// A utility struct for creating an [`fba::ArrayNode`] flatbuffer.
216pub struct ArrayNodeFlatBuffer<'a> {
217    ctx: &'a ArrayContext,
218    array: ArraySerializationTree,
219}
220
221impl<'a> ArrayNodeFlatBuffer<'a> {
222    pub fn try_new(
223        ctx: &'a ArrayContext,
224        session: &'a VortexSession,
225        array: &ArrayRef,
226    ) -> VortexResult<Self> {
227        let array = ArraySerializationTree::try_new(session, array)?;
228        let n_buffers_recursive = array.nbuffers_recursive();
229        if n_buffers_recursive > u16::MAX as usize {
230            vortex_bail!(
231                "Array and all descendent arrays can have at most u16::MAX buffers: {}",
232                n_buffers_recursive
233            );
234        };
235        Ok(Self { ctx, array })
236    }
237
238    pub fn try_write_flatbuffer<'fb>(
239        &self,
240        fbb: &mut FlatBufferBuilder<'fb>,
241    ) -> VortexResult<WIPOffset<fba::ArrayNode<'fb>>> {
242        self.try_write_node(fbb, &self.array, 0)
243    }
244
245    fn try_write_node<'fb>(
246        &self,
247        fbb: &mut FlatBufferBuilder<'fb>,
248        array: &ArraySerializationTree,
249        buffer_idx: u16,
250    ) -> VortexResult<WIPOffset<fba::ArrayNode<'fb>>> {
251        let encoding_idx = self.ctx.intern(&array.serialized_id).ok_or_else(|| {
252            vortex_err!(
253                "Serialized array ID {} not permitted by ctx",
254                array.serialized_id
255            )
256        })?;
257
258        let metadata = Some(fbb.create_vector(array.metadata.as_slice()));
259
260        // Assign buffer indices for all child arrays.
261        let nbuffers = u16::try_from(array.buffers.len())
262            .map_err(|_| vortex_err!("Array can have at most u16::MAX buffers"))?;
263        let mut child_buffer_idx = buffer_idx + nbuffers;
264
265        let children = array
266            .children
267            .iter()
268            .map(|child| {
269                // Update the number of buffers required.
270                let msg = self.try_write_node(fbb, child, child_buffer_idx)?;
271
272                child_buffer_idx = u16::try_from(child.nbuffers_recursive())
273                    .ok()
274                    .and_then(|nbuffers| nbuffers.checked_add(child_buffer_idx))
275                    .ok_or_else(|| vortex_err!("Too many buffers (u16) for Array"))?;
276
277                Ok(msg)
278            })
279            .collect::<VortexResult<Vec<_>>>()?;
280        let children = Some(fbb.create_vector(&children));
281
282        let buffers = Some(fbb.create_vector_from_iter((0..nbuffers).map(|i| i + buffer_idx)));
283        let stats = Some(array.source.statistics().write_flatbuffer(fbb)?);
284
285        Ok(fba::ArrayNode::create(
286            fbb,
287            &fba::ArrayNodeArgs {
288                encoding: encoding_idx,
289                metadata,
290                children,
291                buffers,
292                stats,
293            },
294        ))
295    }
296}
297
298/// To minimize the serialized form, arrays do not persist their own dtype and length. Instead,
299/// parent arrays pass this information down during deserialization.
300pub trait ArrayChildren {
301    /// Returns the nth child of the array with the given dtype and length.
302    fn get(&self, index: usize, dtype: &DType, len: usize) -> VortexResult<ArrayRef>;
303
304    /// The number of children.
305    fn len(&self) -> usize;
306
307    /// Returns true if there are no children.
308    fn is_empty(&self) -> bool {
309        self.len() == 0
310    }
311}
312
313impl<T: AsRef<[ArrayRef]>> ArrayChildren for T {
314    fn get(&self, index: usize, dtype: &DType, len: usize) -> VortexResult<ArrayRef> {
315        let array = self.as_ref()[index].clone();
316        assert_eq!(array.len(), len);
317        assert_eq!(array.dtype(), dtype);
318        Ok(array)
319    }
320
321    fn len(&self) -> usize {
322        self.as_ref().len()
323    }
324}
325
326/// [`SerializedArray`] represents a parsed but not-yet-decoded deserialized array.
327/// It contains all the information from the serialized form, without anything extra. i.e.
328/// it is missing a [`DType`] and `len`, and the `encoding_id` is not yet resolved to a concrete
329/// vtable.
330///
331/// An [`SerializedArray`] can be fully decoded into an [`ArrayRef`] using the `decode` function.
332#[derive(Clone)]
333pub struct SerializedArray {
334    // Typed as fb::ArrayNode
335    flatbuffer: FlatBuffer,
336    // The location of the current fb::ArrayNode
337    flatbuffer_loc: usize,
338    buffers: Arc<[BufferHandle]>,
339}
340
341impl Debug for SerializedArray {
342    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
343        f.debug_struct("SerializedArray")
344            .field("encoding_id", &self.encoding_id())
345            .field("children", &(0..self.nchildren()).map(|i| self.child(i)))
346            .field(
347                "buffers",
348                &(0..self.nbuffers()).map(|i| self.buffer(i).ok()),
349            )
350            .field("metadata", &self.metadata())
351            .finish()
352    }
353}
354
355impl SerializedArray {
356    /// Decode an [`SerializedArray`] into an [`ArrayRef`].
357    pub fn decode(
358        &self,
359        dtype: &DType,
360        len: usize,
361        ctx: &ReadContext,
362        session: &VortexSession,
363    ) -> VortexResult<ArrayRef> {
364        let encoding_idx = self.flatbuffer().encoding();
365        let encoding_id = ctx
366            .resolve(encoding_idx)
367            .ok_or_else(|| vortex_err!("Unknown encoding index: {}", encoding_idx))?;
368        let Some(plugin) = session.arrays().registry().get(&encoding_id) else {
369            if session.allows_unknown() {
370                return self.decode_foreign(encoding_id, dtype, len, ctx);
371            }
372            vortex_bail!("Unknown encoding: {}", encoding_id);
373        };
374
375        let children = SerializedArrayChildren {
376            ser: self,
377            ctx,
378            session,
379        };
380
381        let buffers = self.collect_buffers()?;
382
383        let decoded = plugin.deserialize(
384            ArrayDeserialization::new(
385                encoding_id,
386                dtype,
387                len,
388                self.metadata(),
389                &buffers,
390                &children,
391            ),
392            session,
393        )?;
394
395        assert_eq!(
396            decoded.len(),
397            len,
398            "Array decoded from {} has incorrect length {}, expected {}",
399            encoding_id,
400            decoded.len(),
401            len
402        );
403        assert_eq!(
404            decoded.dtype(),
405            dtype,
406            "Array decoded from {} has incorrect dtype {}, expected {}",
407            encoding_id,
408            decoded.dtype(),
409            dtype,
410        );
411
412        assert!(
413            plugin.is_supported_encoding(&decoded.encoding_id()),
414            "Array decoded from {} has incorrect encoding {}",
415            encoding_id,
416            decoded.encoding_id(),
417        );
418
419        // Populate statistics from the serialized array.
420        if let Some(stats) = self.flatbuffer().stats() {
421            decoded
422                .statistics()
423                .set_iter(StatsSet::from_flatbuffer(&stats, dtype, session)?.into_iter());
424        }
425
426        Ok(decoded)
427    }
428
429    fn decode_foreign(
430        &self,
431        encoding_id: ArrayId,
432        dtype: &DType,
433        len: usize,
434        ctx: &ReadContext,
435    ) -> VortexResult<ArrayRef> {
436        let children = (0..self.nchildren())
437            .map(|idx| {
438                let child = self.child(idx);
439                let child_encoding_idx = child.flatbuffer().encoding();
440                let child_encoding_id = ctx
441                    .resolve(child_encoding_idx)
442                    .ok_or_else(|| vortex_err!("Unknown encoding index: {}", child_encoding_idx))?;
443                child
444                    .decode_foreign(child_encoding_id, dtype, len, ctx)
445                    .map(Some)
446            })
447            .collect::<VortexResult<ArraySlots>>()?;
448
449        new_foreign_array(
450            encoding_id,
451            dtype.clone(),
452            len,
453            self.metadata().to_vec(),
454            self.collect_buffers()?.into_owned(),
455            children,
456        )
457    }
458
459    /// Returns the array encoding.
460    pub fn encoding_id(&self) -> u16 {
461        self.flatbuffer().encoding()
462    }
463
464    /// Returns the array metadata bytes.
465    pub fn metadata(&self) -> &[u8] {
466        self.flatbuffer()
467            .metadata()
468            .map(|metadata| metadata.bytes())
469            .unwrap_or(&[])
470    }
471
472    /// Returns the number of children.
473    pub fn nchildren(&self) -> usize {
474        self.flatbuffer()
475            .children()
476            .map_or(0, |children| children.len())
477    }
478
479    /// Returns the nth child of the array.
480    pub fn child(&self, idx: usize) -> SerializedArray {
481        let children = self
482            .flatbuffer()
483            .children()
484            .vortex_expect("Expected array to have children");
485        if idx >= children.len() {
486            vortex_panic!(
487                "Invalid child index {} for array with {} children",
488                idx,
489                children.len()
490            );
491        }
492        self.with_root(children.get(idx))
493    }
494
495    /// Returns the number of buffers.
496    pub fn nbuffers(&self) -> usize {
497        self.flatbuffer()
498            .buffers()
499            .map_or(0, |buffers| buffers.len())
500    }
501
502    /// Returns the nth buffer of the current array.
503    pub fn buffer(&self, idx: usize) -> VortexResult<BufferHandle> {
504        let buffer_idx = self
505            .flatbuffer()
506            .buffers()
507            .ok_or_else(|| vortex_err!("Array has no buffers"))?
508            .get(idx);
509        self.buffers
510            .get(buffer_idx as usize)
511            .cloned()
512            .ok_or_else(|| {
513                vortex_err!(
514                    "Invalid buffer index {} for array with {} buffers",
515                    buffer_idx,
516                    self.nbuffers()
517                )
518            })
519    }
520
521    /// Returns all buffers for the current array node.
522    ///
523    /// If buffer indices are contiguous, returns a zero-copy borrowed slice.
524    /// Otherwise falls back to collecting each buffer individually.
525    fn collect_buffers(&self) -> VortexResult<Cow<'_, [BufferHandle]>> {
526        let Some(fb_buffers) = self.flatbuffer().buffers() else {
527            return Ok(Cow::Borrowed(&[]));
528        };
529        let count = fb_buffers.len();
530        if count == 0 {
531            return Ok(Cow::Borrowed(&[]));
532        }
533        let start = fb_buffers.get(0) as usize;
534        let contiguous = fb_buffers
535            .iter()
536            .enumerate()
537            .all(|(i, idx)| idx as usize == start + i);
538        if contiguous {
539            self.buffers.get(start..start + count).map_or_else(
540                || {
541                    vortex_bail!(
542                        "buffer indices {}..{} out of range for {} buffers",
543                        start,
544                        start + count,
545                        self.buffers.len()
546                    )
547                },
548                |slice| Ok(Cow::Borrowed(slice)),
549            )
550        } else {
551            (0..count)
552                .map(|idx| self.buffer(idx))
553                .collect::<VortexResult<Vec<_>>>()
554                .map(Cow::Owned)
555        }
556    }
557
558    /// Returns the buffer lengths as stored in the flatbuffer metadata.
559    ///
560    /// This reads the buffer descriptors from the flatbuffer, which contain the
561    /// serialized length of each buffer. This is useful for displaying buffer sizes
562    /// without needing to access the actual buffer data.
563    pub fn buffer_lengths(&self) -> Vec<usize> {
564        let fb_array = root::<fba::Array>(self.flatbuffer.as_ref())
565            .vortex_expect("SerializedArray flatbuffer must be a valid Array");
566        fb_array
567            .buffers()
568            .map(|buffers| buffers.iter().map(|b| b.length() as usize).collect())
569            .unwrap_or_default()
570    }
571
572    /// Validate and align the array tree flatbuffer, returning the aligned buffer and root location.
573    fn validate_array_tree(array_tree: impl Into<ByteBuffer>) -> VortexResult<(FlatBuffer, usize)> {
574        let fb_buffer = FlatBuffer::align_from(array_tree.into());
575        let fb_array = root::<fba::Array>(fb_buffer.as_ref())?;
576        let fb_root = fb_array
577            .root()
578            .ok_or_else(|| vortex_err!("Array must have a root node"))?;
579        let flatbuffer_loc = fb_root._tab.loc();
580        Ok((fb_buffer, flatbuffer_loc))
581    }
582
583    /// Create an [`SerializedArray`] from a pre-existing array tree flatbuffer and pre-resolved buffer
584    /// handles.
585    ///
586    /// The caller is responsible for resolving buffers from whatever source (device segments, host
587    /// overrides, or a mix). The buffers must be in the same order as the `Array.buffers` descriptor
588    /// list in the flatbuffer.
589    pub fn from_flatbuffer_with_buffers(
590        array_tree: impl Into<ByteBuffer>,
591        buffers: Vec<BufferHandle>,
592    ) -> VortexResult<Self> {
593        let (flatbuffer, flatbuffer_loc) = Self::validate_array_tree(array_tree)?;
594        Ok(SerializedArray {
595            flatbuffer,
596            flatbuffer_loc,
597            buffers: buffers.into(),
598        })
599    }
600
601    /// Create an [`SerializedArray`] from a raw array tree flatbuffer (metadata only).
602    ///
603    /// This constructor creates a `SerializedArray` with no buffer data, useful for
604    /// inspecting the metadata when the actual buffer data is not needed
605    /// (e.g., displaying buffer sizes from inlined array tree metadata).
606    ///
607    /// Note: Calling `buffer()` on the returned `SerializedArray` will fail since
608    /// no actual buffer data is available.
609    pub fn from_array_tree(array_tree: impl Into<ByteBuffer>) -> VortexResult<Self> {
610        let (flatbuffer, flatbuffer_loc) = Self::validate_array_tree(array_tree)?;
611        Ok(SerializedArray {
612            flatbuffer,
613            flatbuffer_loc,
614            buffers: Arc::new([]),
615        })
616    }
617
618    /// Returns the root ArrayNode flatbuffer.
619    fn flatbuffer(&self) -> fba::ArrayNode<'_> {
620        unsafe { fba::ArrayNode::follow(self.flatbuffer.as_ref(), self.flatbuffer_loc) }
621    }
622
623    /// Returns a new [`SerializedArray`] with the given node as the root
624    // TODO(ngates): we may want a wrapper that avoids this clone.
625    fn with_root(&self, root: fba::ArrayNode) -> Self {
626        let mut this = self.clone();
627        this.flatbuffer_loc = root._tab.loc();
628        this
629    }
630
631    /// Create an [`SerializedArray`] from a pre-existing flatbuffer (ArrayNode) and a segment containing
632    /// only the data buffers (without the flatbuffer suffix).
633    ///
634    /// This is used when the flatbuffer is stored separately in layout metadata (e.g., when
635    /// `FLAT_LAYOUT_INLINE_ARRAY_NODE` is enabled).
636    pub fn from_flatbuffer_and_segment(
637        array_tree: ByteBuffer,
638        segment: BufferHandle,
639    ) -> VortexResult<Self> {
640        // HashMap::new doesn't allocate when empty, so this has no overhead
641        Self::from_flatbuffer_and_segment_with_overrides(array_tree, segment, &HashMap::new())
642    }
643
644    /// Create an [`SerializedArray`] from a pre-existing flatbuffer (ArrayNode) and a segment,
645    /// substituting host-resident buffer overrides for specific buffer indices.
646    ///
647    /// Buffers whose index appears in `buffer_overrides` are resolved from the provided
648    /// host data instead of the segment. All other buffers are sliced from the segment
649    /// using the padding and alignment described in the flatbuffer.
650    pub fn from_flatbuffer_and_segment_with_overrides(
651        array_tree: ByteBuffer,
652        segment: BufferHandle,
653        buffer_overrides: &HashMap<u32, ByteBuffer>,
654    ) -> VortexResult<Self> {
655        // We align each buffer individually, so we remove alignment requirements on the segment
656        // for host-resident buffers. Device buffers are sliced directly.
657        let segment = segment.ensure_aligned(Alignment::none())?;
658
659        // this can't return the validated array because there is no lifetime to give it, so we
660        // need to cast it below, which is safe.
661        let (fb_buffer, flatbuffer_loc) = Self::validate_array_tree(array_tree)?;
662        // SAFETY: fb_buffer was already validated by validate_array_tree above.
663        let fb_array = unsafe { fba::root_as_array_unchecked(fb_buffer.as_ref()) };
664
665        let mut offset = 0usize;
666        let buffers = fb_array
667            .buffers()
668            .unwrap_or_default()
669            .iter()
670            .enumerate()
671            .map(|(idx, fb_buf)| {
672                let idx = u32::try_from(idx).vortex_expect("buffer count must fit in u32");
673
674                // The padding, length, and resulting offsets all come from the flatbuffer, which
675                // may be corrupt. Use checked arithmetic so malformed metadata returns a
676                // `VortexError` rather than panicking (see issue #8819).
677                let buffer_len = fb_buf.length() as usize;
678                let start = offset
679                    .checked_add(fb_buf.padding() as usize)
680                    .ok_or_else(|| {
681                        vortex_err!("Buffer {idx} offset overflows when adding its padding")
682                    })?;
683                let end = start.checked_add(buffer_len).ok_or_else(|| {
684                    vortex_err!("Buffer {idx} offset overflows when adding its length")
685                })?;
686
687                // The alignment exponent comes from the flatbuffer and may be corrupt, so validate
688                // it rather than panicking on a too-large shift (see issue #8819).
689                let alignment =
690                    Alignment::try_from_untrusted_exponent(fb_buf.alignment_exponent())?;
691                let handle = if let Some(host_data) = buffer_overrides.get(&idx) {
692                    BufferHandle::new_host(host_data.clone()).ensure_aligned(alignment)?
693                } else {
694                    // Bounds-check against the segment so an out-of-range buffer returns a
695                    // `VortexError` rather than panicking when slicing (see issue #8819).
696                    if end > segment.len() {
697                        vortex_bail!(
698                            "Buffer {idx} at offset {start} with length {buffer_len} is out of \
699                             bounds of the {}-byte segment",
700                            segment.len(),
701                        );
702                    }
703                    segment.slice(start..end).ensure_aligned(alignment)?
704                };
705
706                offset = end;
707                Ok(handle)
708            })
709            .collect::<VortexResult<Arc<[_]>>>()?;
710
711        Ok(SerializedArray {
712            flatbuffer: fb_buffer,
713            flatbuffer_loc,
714            buffers,
715        })
716    }
717}
718
719struct SerializedArrayChildren<'a> {
720    ser: &'a SerializedArray,
721    ctx: &'a ReadContext,
722    session: &'a VortexSession,
723}
724
725impl ArrayChildren for SerializedArrayChildren<'_> {
726    fn get(&self, index: usize, dtype: &DType, len: usize) -> VortexResult<ArrayRef> {
727        self.ser
728            .child(index)
729            .decode(dtype, len, self.ctx, self.session)
730    }
731
732    fn len(&self) -> usize {
733        self.ser.nchildren()
734    }
735}
736
737impl TryFrom<ByteBuffer> for SerializedArray {
738    type Error = VortexError;
739
740    fn try_from(value: ByteBuffer) -> Result<Self, Self::Error> {
741        // The final 4 bytes contain the length of the flatbuffer.
742        if value.len() < 4 {
743            vortex_bail!("SerializedArray buffer is too short");
744        }
745
746        // We align each buffer individually, so we remove alignment requirements on the buffer.
747        let value = value.aligned(Alignment::none());
748
749        let fb_length = u32::try_from_le_bytes(&value.as_slice()[value.len() - 4..])? as usize;
750        if value.len() < 4 + fb_length {
751            vortex_bail!("SerializedArray buffer is too short for flatbuffer");
752        }
753
754        let fb_offset = value.len() - 4 - fb_length;
755        let array_tree = value.slice(fb_offset..fb_offset + fb_length);
756        let segment = BufferHandle::new_host(value.slice(0..fb_offset));
757
758        Self::from_flatbuffer_and_segment(array_tree, segment)
759    }
760}
761
762impl TryFrom<BufferHandle> for SerializedArray {
763    type Error = VortexError;
764
765    fn try_from(value: BufferHandle) -> Result<Self, Self::Error> {
766        Self::try_from(value.try_to_host_sync()?)
767    }
768}
769
770#[cfg(test)]
771mod tests {
772    use std::sync::atomic::AtomicUsize;
773    use std::sync::atomic::Ordering;
774
775    use vortex_buffer::ByteBufferMut;
776    use vortex_error::vortex_ensure;
777    use vortex_session::registry::CachedId;
778
779    use super::*;
780    use crate::Array;
781    use crate::ArrayPlugin;
782    use crate::ArraySerialization;
783    use crate::ArrayVTable;
784    use crate::IntoArray;
785    use crate::array_session;
786    use crate::arrays::Primitive;
787    use crate::arrays::PrimitiveArray;
788
789    static SERIALIZER_CALLS: AtomicUsize = AtomicUsize::new(0);
790
791    fn old_primitive_id() -> ArrayId {
792        ArrayVTable::id(&Primitive)
793    }
794
795    fn new_primitive_id() -> ArrayId {
796        static ID: CachedId = CachedId::new("vortex.test.primitive_v2");
797        *ID
798    }
799
800    #[derive(Debug)]
801    struct VersionedPrimitivePlugin;
802
803    impl ArrayPlugin for VersionedPrimitivePlugin {
804        fn id(&self) -> ArrayId {
805            old_primitive_id()
806        }
807
808        fn serialized_ids(&self) -> Vec<ArrayId> {
809            vec![old_primitive_id(), new_primitive_id()]
810        }
811
812        fn serialize(
813            &self,
814            array: &ArrayRef,
815            _session: &VortexSession,
816        ) -> VortexResult<Option<ArraySerialization>> {
817            vortex_ensure!(
818                array.encoding_id() == self.id(),
819                "versioned primitive serializer received {}",
820                array.encoding_id(),
821            );
822
823            let serialized_id = if array.len() <= 4 {
824                old_primitive_id()
825            } else {
826                new_primitive_id()
827            };
828
829            Ok(Some(ArraySerialization::from_array(
830                serialized_id,
831                array,
832                vec![],
833            )))
834        }
835
836        fn deserialize(
837            &self,
838            parts: ArrayDeserialization<'_>,
839            session: &VortexSession,
840        ) -> VortexResult<ArrayRef> {
841            vortex_ensure!(
842                parts.serialized_id == old_primitive_id()
843                    || parts.serialized_id == new_primitive_id(),
844                "versioned primitive deserializer does not recognize {}",
845                parts.serialized_id,
846            );
847            vortex_ensure!(
848                parts.serialized_id != old_primitive_id() || parts.len <= 4,
849                "old primitive wire ID cannot represent length {}",
850                parts.len,
851            );
852            Ok(Array::<Primitive>::try_from_parts(ArrayVTable::deserialize(
853                &Primitive,
854                parts.dtype,
855                parts.len,
856                parts.metadata,
857                parts.buffers,
858                parts.children,
859                session,
860            )?)?
861            .into_array())
862        }
863    }
864
865    #[derive(Debug)]
866    struct CountingVersionedPrimitivePlugin;
867
868    impl ArrayPlugin for CountingVersionedPrimitivePlugin {
869        fn id(&self) -> ArrayId {
870            VersionedPrimitivePlugin.id()
871        }
872
873        fn serialized_ids(&self) -> Vec<ArrayId> {
874            VersionedPrimitivePlugin.serialized_ids()
875        }
876
877        fn serialize(
878            &self,
879            array: &ArrayRef,
880            session: &VortexSession,
881        ) -> VortexResult<Option<ArraySerialization>> {
882            SERIALIZER_CALLS.fetch_add(1, Ordering::Relaxed);
883            VersionedPrimitivePlugin.serialize(array, session)
884        }
885
886        fn deserialize(
887            &self,
888            parts: ArrayDeserialization<'_>,
889            session: &VortexSession,
890        ) -> VortexResult<ArrayRef> {
891            VersionedPrimitivePlugin.deserialize(parts, session)
892        }
893    }
894
895    fn versioned_primitive_session() -> VortexSession {
896        let session = array_session();
897        session.arrays().register(VersionedPrimitivePlugin);
898        session
899    }
900
901    fn restricted_context(ids: &[ArrayId]) -> ArrayContext {
902        ArrayContext::new(ids.to_vec()).with_allowed_ids(ids.iter().copied().collect())
903    }
904
905    fn serialize_blob(
906        array: &ArrayRef,
907        ctx: &ArrayContext,
908        session: &VortexSession,
909    ) -> VortexResult<ByteBuffer> {
910        let mut blob = ByteBufferMut::empty();
911        for buffer in array.serialize(ctx, session, &SerializeOptions::default())? {
912            blob.extend_from_slice(buffer.as_ref());
913        }
914        Ok(blob.freeze())
915    }
916
917    #[test]
918    fn one_serializer_selects_the_earliest_lossless_wire_id() -> VortexResult<()> {
919        let session = array_session();
920        session.arrays().register(CountingVersionedPrimitivePlugin);
921        let ctx = restricted_context(&[old_primitive_id(), new_primitive_id()]);
922        let array = PrimitiveArray::from_iter([1i32, 2, 3, 4]).into_array();
923
924        SERIALIZER_CALLS.store(0, Ordering::Relaxed);
925        let serialized = SerializedArray::try_from(serialize_blob(&array, &ctx, &session)?)?;
926        assert_eq!(SERIALIZER_CALLS.load(Ordering::Relaxed), 1);
927        assert_eq!(
928            ReadContext::new(ctx.to_ids()).resolve(serialized.encoding_id()),
929            Some(old_primitive_id())
930        );
931        Ok(())
932    }
933
934    #[test]
935    fn serializer_uses_a_newer_id_only_when_the_old_variant_cannot_represent_the_value()
936    -> VortexResult<()> {
937        let session = versioned_primitive_session();
938        let ctx = restricted_context(&[old_primitive_id(), new_primitive_id()]);
939        let array = PrimitiveArray::from_iter(0..8i32).into_array();
940        let serialized = SerializedArray::try_from(serialize_blob(&array, &ctx, &session)?)?;
941        let read_ctx = ReadContext::new(ctx.to_ids());
942
943        assert_eq!(
944            read_ctx.resolve(serialized.encoding_id()),
945            Some(new_primitive_id())
946        );
947        let decoded = serialized.decode(array.dtype(), array.len(), &read_ctx, &session)?;
948        assert_eq!(decoded.encoding_id(), old_primitive_id());
949        Ok(())
950    }
951
952    #[test]
953    fn serialization_fails_when_serialized_id_is_not_permitted() -> VortexResult<()> {
954        let session = versioned_primitive_session();
955        let ctx = restricted_context(&[old_primitive_id()]);
956        let array = PrimitiveArray::from_iter(0..8i32).into_array();
957
958        let error = array
959            .serialize(&ctx, &session, &SerializeOptions::default())
960            .expect_err("the serialized ID is not permitted");
961        assert!(error.to_string().contains("not permitted by ctx"));
962        Ok(())
963    }
964
965    #[test]
966    fn old_reader_rejects_a_new_serialized_id() -> VortexResult<()> {
967        let writer_session = versioned_primitive_session();
968        let ctx = restricted_context(&[new_primitive_id()]);
969        let array = PrimitiveArray::from_iter(0..8i32).into_array();
970        let serialized = SerializedArray::try_from(serialize_blob(&array, &ctx, &writer_session)?)?;
971        let read_ctx = ReadContext::new(ctx.to_ids());
972
973        let old_session = array_session();
974        let error = serialized
975            .decode(array.dtype(), array.len(), &read_ctx, &old_session)
976            .expect_err("an old reader must not recognize the new wire ID");
977        assert!(error.to_string().contains("Unknown encoding"));
978        Ok(())
979    }
980
981    #[test]
982    fn deserializer_enforces_the_exact_wire_id_contract() -> VortexResult<()> {
983        let session = versioned_primitive_session();
984        let write_ctx = restricted_context(&[new_primitive_id()]);
985        let array = PrimitiveArray::from_iter(0..8i32).into_array();
986        let serialized = SerializedArray::try_from(serialize_blob(&array, &write_ctx, &session)?)?;
987
988        // Interpret the encoded index as the old ID to simulate a file that uses the old tag for
989        // a representation outside that tag's frozen contract.
990        let error = serialized
991            .decode(
992                array.dtype(),
993                array.len(),
994                &ReadContext::new([old_primitive_id()]),
995                &session,
996            )
997            .expect_err("the old wire contract must be enforced by the current deserializer");
998        assert!(error.to_string().contains("old primitive wire ID"));
999        Ok(())
1000    }
1001
1002    /// A corrupt array tree can declare a buffer that extends past the backing segment. Slicing
1003    /// such a buffer must return a [`VortexError`] rather than panicking (see issue #8819).
1004    #[test]
1005    fn from_flatbuffer_and_segment_rejects_out_of_bounds_buffer() -> VortexResult<()> {
1006        let session = array_session();
1007        let array_ctx = ArrayContext::empty();
1008
1009        // Serialize a simple array so we have a valid array tree flatbuffer whose declared buffer
1010        // lengths describe the trailing data segment.
1011        let serialized = PrimitiveArray::from_iter([1i32, 2, 3, 4])
1012            .into_array()
1013            .serialize(&array_ctx, &session, &SerializeOptions::default())?;
1014
1015        let mut concat = ByteBufferMut::empty();
1016        for buf in serialized {
1017            concat.extend_from_slice(buf.as_ref());
1018        }
1019        let value = concat.freeze().aligned(Alignment::none());
1020
1021        // Split the blob into the trailing flatbuffer and the leading data segment, mirroring
1022        // `SerializedArray::try_from`.
1023        let fb_length = u32::try_from_le_bytes(&value.as_slice()[value.len() - 4..])? as usize;
1024        let fb_offset = value.len() - 4 - fb_length;
1025        assert!(
1026            fb_offset > 0,
1027            "the array must have at least one data buffer"
1028        );
1029        let array_tree = value.slice(fb_offset..fb_offset + fb_length);
1030
1031        // Truncate the data segment by one byte so the declared buffer no longer fits.
1032        let truncated = BufferHandle::new_host(value.slice(0..fb_offset - 1));
1033
1034        let Some(err) = SerializedArray::from_flatbuffer_and_segment(array_tree, truncated).err()
1035        else {
1036            vortex_bail!("out-of-bounds buffer must be rejected");
1037        };
1038        assert!(
1039            err.to_string().contains("out of bounds"),
1040            "unexpected error: {err}"
1041        );
1042
1043        Ok(())
1044    }
1045
1046    /// A corrupt array tree can declare a buffer alignment of up to 2^63, which the copy that
1047    /// satisfies it allocates as slack. It must be rejected (see issue #8819).
1048    #[test]
1049    fn from_flatbuffer_and_segment_rejects_excessive_buffer_alignment() -> VortexResult<()> {
1050        // Padding and length fit the segment exactly, so the exponent is the only defect.
1051        // `validate_array_tree` only requires a root node to be present, so an empty one will do.
1052        let mut fbb = FlatBufferBuilder::new();
1053        let fb_root = fba::ArrayNode::create(&mut fbb, &fba::ArrayNodeArgs::default());
1054        let fb_buffers = fbb.create_vector(&[fba::Buffer::new(0, 40, Compression::None, 4)]);
1055        let fb_array = fba::Array::create(
1056            &mut fbb,
1057            &fba::ArrayArgs {
1058                root: Some(fb_root),
1059                buffers: Some(fb_buffers),
1060            },
1061        );
1062        fbb.finish_minimal(fb_array);
1063        let (fb_vec, fb_start) = fbb.collapse();
1064        let fb_end = fb_vec.len();
1065        let array_tree = ByteBuffer::from(fb_vec).slice(fb_start..fb_end);
1066
1067        let segment = BufferHandle::new_host(ByteBuffer::from(vec![0u8; 4]));
1068        let Some(err) = SerializedArray::from_flatbuffer_and_segment(array_tree, segment).err()
1069        else {
1070            vortex_bail!("excessive buffer alignment must be rejected");
1071        };
1072        assert!(
1073            err.to_string().contains("exceeds"),
1074            "unexpected error: {err}"
1075        );
1076
1077        Ok(())
1078    }
1079}