Skip to main content

vortex_file/footer/
deserializer.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::sync::Arc;
5
6use flatbuffers::root;
7use vortex_array::dtype::DType;
8use vortex_buffer::ByteBuffer;
9use vortex_buffer::ByteBufferMut;
10use vortex_error::VortexExpect;
11use vortex_error::VortexResult;
12use vortex_error::vortex_bail;
13use vortex_error::vortex_err;
14use vortex_flatbuffers::FlatBuffer;
15use vortex_flatbuffers::ReadFlatBuffer;
16use vortex_session::VortexSession;
17
18use crate::EOF_SIZE;
19use crate::Footer;
20use crate::MAGIC_BYTES;
21use crate::VERSION;
22use crate::footer::FileStatistics;
23use crate::footer::SegmentSpec;
24use crate::footer::postscript::Postscript;
25use crate::footer::postscript::PostscriptSegment;
26
27/// Deserialize a footer from the end of a Vortex file or created from a
28/// [`crate::footer::FooterSerializer`].
29///
30/// The deserializer is incremental because callers may initially read only the tail of a file. Call
31/// [`deserialize`](Self::deserialize) until it returns [`DeserializeStep::Done`]. If it asks for
32/// [`DeserializeStep::NeedMoreData`], prefix the requested bytes with [`prefix_data`](Self::prefix_data).
33/// If it asks for [`DeserializeStep::NeedFileSize`], call [`with_size`](Self::with_size) and retry.
34pub struct FooterDeserializer {
35    // A buffer representing the end of a Vortex file.
36    // During deserialization, we may need to expand this buffer by requesting more data from
37    // the caller.
38    buffer: ByteBuffer,
39    // The session to use for deserialization.
40    session: VortexSession,
41    // The DType, if provided externally.
42    dtype: Option<DType>,
43
44    // Internal state that we accumulate
45
46    // The size of the file containing the serialized footer. For a standalone footer, this is the
47    // size of the footer blob rather than the data file described by the footer.
48    file_size: Option<u64>,
49    // The postscript, once we've parsed it.
50    postscript: Option<Postscript>,
51}
52
53impl FooterDeserializer {
54    pub(super) fn new(initial_read: ByteBuffer, session: VortexSession) -> Self {
55        Self {
56            buffer: initial_read,
57            session,
58            dtype: None,
59            file_size: None,
60            postscript: None,
61        }
62    }
63
64    /// Provide the file dtype externally.
65    ///
66    /// This is required for files written with [`VortexWriteOptions::exclude_dtype`](crate::VortexWriteOptions::exclude_dtype).
67    pub fn with_dtype(mut self, dtype: DType) -> Self {
68        self.dtype = Some(dtype);
69        self
70    }
71
72    /// Provide or clear the externally known file dtype.
73    pub fn with_some_dtype(mut self, dtype: Option<DType>) -> Self {
74        self.dtype = dtype;
75        self
76    }
77
78    /// Provide the size of the file containing this serialized footer.
79    ///
80    /// For a footer read from the end of a Vortex file, this is the file size. For a standalone
81    /// blob created by [`crate::footer::FooterSerializer`] with its default offset, this is the
82    /// size of that blob, not the size of the data file described by the footer.
83    pub fn with_size(mut self, file_size: u64) -> Self {
84        self.file_size = Some(file_size);
85        self
86    }
87
88    /// Provide or clear the size of the file containing this serialized footer.
89    pub fn with_some_size(mut self, file_size: Option<u64>) -> Self {
90        self.file_size = file_size;
91        self
92    }
93
94    /// Prefix more data to the existing buffer when requested by the deserializer.
95    pub fn prefix_data(&mut self, more_data: ByteBuffer) {
96        let mut buffer = ByteBufferMut::with_capacity(self.buffer.len() + more_data.len());
97        buffer.extend_from_slice(&more_data);
98        buffer.extend_from_slice(&self.buffer);
99        self.buffer = buffer.freeze();
100    }
101
102    /// Advance footer deserialization.
103    ///
104    /// Returns the next missing input requirement or the finished [`Footer`].
105    pub fn deserialize(&mut self) -> VortexResult<DeserializeStep> {
106        let postscript = if let Some(postscript) = &self.postscript {
107            postscript
108        } else {
109            self.postscript = Some(self.parse_postscript(&self.buffer)?);
110            self.postscript
111                .as_ref()
112                .vortex_expect("Just set postscript")
113        };
114
115        // If we haven't been provided a DType, we must read one from the file.
116        let dtype_segment = self
117            .dtype
118            .is_none()
119            .then(|| {
120                postscript.dtype.as_ref().ok_or_else(|| {
121                    vortex_err!(
122                        "Vortex file doesn't embed a DType and none provided to VortexOpenOptions"
123                    )
124                })
125            })
126            .transpose()?;
127
128        // The other postscript segments are required, so now we figure out our the offset that
129        // contains all the required segments.
130
131        // The initial offset is the file size minus the size of our initial read.
132        let Some(file_size) = self.file_size else {
133            return Ok(DeserializeStep::NeedFileSize);
134        };
135        let initial_offset = file_size
136            .checked_sub(self.buffer.len() as u64)
137            .ok_or_else(|| {
138                vortex_err!(
139                    "Footer buffer length {} exceeds declared file size {file_size}",
140                    self.buffer.len()
141                )
142            })?;
143
144        let mut read_more_offset = initial_offset;
145        if let Some(dtype_segment) = &dtype_segment {
146            read_more_offset = read_more_offset.min(dtype_segment.offset);
147        }
148        if let Some(stats_segment) = &postscript.statistics {
149            read_more_offset = read_more_offset.min(stats_segment.offset);
150        }
151        read_more_offset = read_more_offset.min(postscript.layout.offset);
152        read_more_offset = read_more_offset.min(postscript.footer.offset);
153
154        // Read more bytes if necessary.
155        if read_more_offset < initial_offset {
156            tracing::trace!(
157                "Initial read from {initial_offset} did not cover all footer segments, reading from {read_more_offset}"
158            );
159            return Ok(DeserializeStep::NeedMoreData {
160                offset: read_more_offset,
161                len: usize::try_from(initial_offset - read_more_offset)?,
162            });
163        }
164
165        // Now we read our initial segments.
166        let dtype = dtype_segment
167            .map(|segment| self.parse_dtype(initial_offset, &self.buffer, segment))
168            .transpose()?
169            .unwrap_or_else(|| self.dtype.clone().vortex_expect("DType was provided"));
170        let file_stats = postscript
171            .statistics
172            .as_ref()
173            .map(|segment| {
174                self.parse_file_statistics(
175                    initial_offset,
176                    &self.buffer,
177                    segment,
178                    &dtype,
179                    &self.session,
180                )
181            })
182            .transpose()?;
183        let metadata = postscript
184            .metadata
185            .iter()
186            .map(|metadata| {
187                let segment = SegmentSpec {
188                    offset: metadata.segment.offset,
189                    length: metadata.segment.length,
190                    alignment: metadata.segment.alignment,
191                };
192                let end = segment
193                    .offset
194                    .checked_add(u64::from(segment.length))
195                    .ok_or_else(|| {
196                        vortex_err!("Metadata segment {} range overflowed u64", metadata.key)
197                    })?;
198                if end > file_size {
199                    vortex_bail!(
200                        "Metadata segment {} range {}..{} exceeds file size {}",
201                        metadata.key,
202                        segment.offset,
203                        end,
204                        file_size
205                    );
206                }
207                let offset = usize::try_from(segment.offset)?;
208                if !segment.alignment.is_offset_aligned(offset) {
209                    vortex_bail!(
210                        "Metadata segment {} offset {} is not aligned to {}",
211                        metadata.key,
212                        segment.offset,
213                        segment.alignment
214                    );
215                }
216                Ok((metadata.key.clone(), segment))
217            })
218            .collect::<VortexResult<Arc<[_]>>>()?;
219
220        Ok(DeserializeStep::Done(self.parse_footer(
221            initial_offset,
222            &self.buffer,
223            postscript,
224            dtype,
225            file_stats,
226            metadata,
227        )?))
228    }
229
230    /// The current buffer being used for deserialization.
231    pub fn buffer(&self) -> &ByteBuffer {
232        &self.buffer
233    }
234
235    /// Parse the postscript from the initial read.
236    fn parse_postscript(&self, initial_read: &[u8]) -> VortexResult<Postscript> {
237        if initial_read.len() < EOF_SIZE {
238            vortex_bail!(
239                "Initial read must be at least EOF_SIZE ({}) bytes",
240                EOF_SIZE
241            );
242        }
243        let eof_loc = initial_read.len() - EOF_SIZE;
244        let magic_bytes_loc = eof_loc + (EOF_SIZE - MAGIC_BYTES.len());
245
246        let magic_number = &initial_read[magic_bytes_loc..];
247        if magic_number != MAGIC_BYTES {
248            vortex_bail!("Malformed file, invalid magic bytes, got {magic_number:?}")
249        }
250
251        let version = u16::from_le_bytes(
252            initial_read[eof_loc..eof_loc + 2]
253                .try_into()
254                .map_err(|e| vortex_err!("Version was not a u16 {e}"))?,
255        );
256        if version != VERSION {
257            vortex_bail!("Malformed file, unsupported version {version}")
258        }
259
260        let ps_size = u16::from_le_bytes(
261            initial_read[eof_loc + 2..eof_loc + 4]
262                .try_into()
263                .map_err(|e| vortex_err!("Postscript size was not a u16 {e}"))?,
264        ) as usize;
265
266        if initial_read.len() < ps_size + EOF_SIZE {
267            vortex_bail!(
268                "Initial read must be at least {} bytes to include the Postscript",
269                ps_size + EOF_SIZE
270            );
271        }
272
273        Postscript::read_flatbuffer_bytes(&initial_read[eof_loc - ps_size..eof_loc])
274    }
275
276    /// Parse the DType from the initial read.
277    fn parse_dtype(
278        &self,
279        initial_offset: u64,
280        initial_read: &[u8],
281        segment: &PostscriptSegment,
282    ) -> VortexResult<DType> {
283        let sliced_buffer = FlatBuffer::copy_from(checked_segment_slice(
284            initial_read,
285            initial_offset,
286            segment,
287        )?);
288        DType::from_flatbuffer(sliced_buffer, &self.session)
289    }
290
291    /// Parse the [`FileStatistics`] from the initial read buffer.
292    fn parse_file_statistics(
293        &self,
294        initial_offset: u64,
295        initial_read: &[u8],
296        segment: &PostscriptSegment,
297        dtype: &DType,
298        session: &VortexSession,
299    ) -> VortexResult<FileStatistics> {
300        let sliced_buffer = checked_segment_slice(initial_read, initial_offset, segment)?;
301
302        let fb = root::<vortex_flatbuffers::footer::FileStatistics>(sliced_buffer)?;
303        FileStatistics::from_flatbuffer(&fb, dtype, session)
304    }
305
306    /// Parse the rest of the footer from the initial read.
307    fn parse_footer(
308        &self,
309        initial_offset: u64,
310        initial_read: &[u8],
311        postscript: &Postscript,
312        dtype: DType,
313        file_stats: Option<FileStatistics>,
314        metadata: Arc<[(String, SegmentSpec)]>,
315    ) -> VortexResult<Footer> {
316        let footer_segment = &postscript.footer;
317        let footer_bytes = checked_segment_slice(initial_read, initial_offset, footer_segment)?;
318
319        let layout_segment = &postscript.layout;
320        let layout_bytes = FlatBuffer::copy_from(checked_segment_slice(
321            initial_read,
322            initial_offset,
323            layout_segment,
324        )?);
325
326        Footer::from_flatbuffer(
327            footer_bytes,
328            layout_bytes,
329            dtype,
330            file_stats,
331            metadata,
332            &self.session,
333        )
334    }
335}
336
337fn checked_segment_slice<'a>(
338    read: &'a [u8],
339    read_offset: u64,
340    segment: &PostscriptSegment,
341) -> VortexResult<&'a [u8]> {
342    let offset = usize::try_from(segment.offset.checked_sub(read_offset).ok_or_else(|| {
343        vortex_err!(
344            "Segment offset {} is smaller than file read offset {read_offset}",
345            segment.offset
346        )
347    })?)?;
348    offset
349        .checked_add(segment.length as usize)
350        .and_then(|end| read.get(offset..end))
351        .ok_or_else(|| {
352            vortex_err!(
353                "Segment length {} (at offset {}) out of bounds of slice of length {}",
354                segment.length,
355                offset,
356                read.len()
357            )
358        })
359}
360
361#[cfg(test)]
362mod tests {
363    use rstest::rstest;
364    use vortex_array::array_session;
365    use vortex_array::dtype::Nullability;
366    use vortex_array::dtype::PType;
367    use vortex_flatbuffers::WriteFlatBufferExt;
368
369    use super::*;
370
371    fn segment(offset: u64, length: u32) -> PostscriptSegment {
372        PostscriptSegment {
373            offset,
374            length,
375            alignment: FlatBuffer::alignment(),
376        }
377    }
378
379    #[test]
380    fn in_bounds_segment_slice() -> VortexResult<()> {
381        let read: Vec<u8> = (0u8..10).collect();
382        let sliced = checked_segment_slice(&read, 100, &segment(104, 4))?;
383        assert_eq!(sliced, &read[4..8]);
384        Ok(())
385    }
386
387    #[rstest]
388    #[case::offset_before_read_start(100, 99, 4, "smaller than file read offset")]
389    #[case::end_past_buffer(100, 105, 6, "out of bounds")]
390    #[case::offset_past_buffer(100, 120, 1, "out of bounds")]
391    #[case::end_overflows_usize(0, u64::MAX, u32::MAX, "out of bounds")]
392    fn out_of_bounds_segment_slice(
393        #[case] read_offset: u64,
394        #[case] segment_offset: u64,
395        #[case] segment_length: u32,
396        #[case] expected: &str,
397    ) {
398        let read = [0u8; 10];
399        let err =
400            checked_segment_slice(&read, read_offset, &segment(segment_offset, segment_length))
401                .unwrap_err();
402        assert!(err.to_string().contains(expected), "{err}");
403    }
404
405    fn eof_buffer(postscript: &Postscript) -> VortexResult<ByteBuffer> {
406        let postscript_bytes = postscript.write_flatbuffer_bytes()?;
407        let mut buffer = ByteBufferMut::with_capacity(postscript_bytes.len() + EOF_SIZE);
408        buffer.extend_from_slice(&postscript_bytes);
409        buffer.extend_from_slice(&VERSION.to_le_bytes());
410        buffer.extend_from_slice(&u16::try_from(postscript_bytes.len())?.to_le_bytes());
411        buffer.extend_from_slice(&MAGIC_BYTES);
412        Ok(buffer.freeze())
413    }
414
415    #[rstest]
416    #[case::length_past_eof(segment(0, u32::MAX))]
417    #[case::offset_overflow(segment(u64::MAX, u32::MAX))]
418    fn deserialize_rejects_out_of_bounds_footer_segment(
419        #[case] footer_segment: PostscriptSegment,
420    ) -> VortexResult<()> {
421        let postscript = Postscript {
422            dtype: None,
423            layout: segment(0, 1),
424            statistics: None,
425            footer: footer_segment,
426            metadata: Vec::new(),
427        };
428        let buffer = eof_buffer(&postscript)?;
429        let file_size = buffer.len() as u64;
430
431        let mut deserializer = FooterDeserializer::new(buffer, array_session())
432            .with_dtype(DType::Primitive(PType::I32, Nullability::NonNullable))
433            .with_size(file_size);
434        let err = deserializer.deserialize().unwrap_err();
435        assert!(err.to_string().contains("out of bounds"), "{err}");
436        Ok(())
437    }
438
439    #[test]
440    fn deserialize_rejects_buffer_larger_than_declared_size() -> VortexResult<()> {
441        let postscript = Postscript {
442            dtype: None,
443            layout: segment(0, 1),
444            statistics: None,
445            footer: segment(1, 1),
446            metadata: Vec::new(),
447        };
448        let buffer = eof_buffer(&postscript)?;
449        let declared_size = buffer.len() as u64 - 1;
450
451        let mut deserializer = FooterDeserializer::new(buffer, array_session())
452            .with_dtype(DType::Primitive(PType::I32, Nullability::NonNullable))
453            .with_size(declared_size);
454        let err = deserializer.deserialize().unwrap_err();
455        assert!(err.to_string().contains("exceeds declared"), "{err}");
456        Ok(())
457    }
458}
459
460#[derive(Debug)]
461/// Result of one [`FooterDeserializer::deserialize`] step.
462pub enum DeserializeStep {
463    /// Additional data needed to continue deserialization.
464    NeedMoreData {
465        /// Absolute file offset to read from.
466        offset: u64,
467        /// Number of bytes to read and prefix into the deserializer.
468        len: usize,
469    },
470    /// The total file size is required before offsets can be resolved.
471    NeedFileSize,
472    /// Footer deserialization is complete.
473    Done(Footer),
474}