Skip to main content

vortex_file/
open.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::sync::Arc;
5
6use futures::executor::block_on;
7use vortex_array::dtype::DType;
8use vortex_array::memory::MemorySessionExt;
9use vortex_array::session::ArraySessionExt;
10use vortex_buffer::Alignment;
11use vortex_buffer::ByteBuffer;
12use vortex_error::VortexError;
13use vortex_error::VortexExpect;
14use vortex_error::VortexResult;
15use vortex_error::vortex_err;
16use vortex_io::VortexReadAt;
17use vortex_io::session::RuntimeSessionExt;
18use vortex_layout::segments::InstrumentedSegmentCache;
19use vortex_layout::segments::NoOpSegmentCache;
20use vortex_layout::segments::SegmentCache;
21use vortex_layout::segments::SegmentCacheSourceAdapter;
22use vortex_layout::segments::SegmentId;
23use vortex_layout::segments::SegmentSource;
24use vortex_layout::segments::SharedSegmentSource;
25use vortex_layout::session::LayoutSessionExt;
26use vortex_metrics::DefaultMetricsRegistry;
27use vortex_metrics::Label;
28use vortex_metrics::MetricsRegistry;
29use vortex_session::VortexSession;
30use vortex_utils::aliases::hash_map::HashMap;
31
32use crate::DeserializeStep;
33use crate::EOF_SIZE;
34use crate::MAX_POSTSCRIPT_SIZE;
35use crate::VortexFile;
36use crate::footer::Footer;
37use crate::segments::BufferSegmentSource;
38use crate::segments::FileSegmentSource;
39use crate::segments::InitialReadSegmentCache;
40use crate::segments::RequestMetrics;
41
42const INITIAL_READ_SIZE: usize = MAX_POSTSCRIPT_SIZE as usize + EOF_SIZE;
43
44struct FooterRead {
45    footer: Footer,
46    initial_segments: HashMap<SegmentId, ByteBuffer>,
47}
48
49/// Open options for a Vortex file reader.
50///
51/// Options are session-bound because opening a file may need array, layout, dtype, runtime, and
52/// memory registries. Construct with [`OpenOptionsSessionExt::open_options`] for the common path.
53///
54/// The opener first resolves a [`Footer`], then creates a segment source for later scans. Known
55/// file metadata can be supplied up front to avoid IO during footer discovery.
56#[derive(Clone)]
57pub struct VortexOpenOptions {
58    /// The session to use for opening the file.
59    session: VortexSession,
60    /// Cache to use for file segments.
61    segment_cache: Option<Arc<dyn SegmentCache>>,
62    /// The number of bytes to read when parsing the footer.
63    initial_read_size: usize,
64    /// An optional, externally provided, file size.
65    file_size: Option<u64>,
66    /// An optional, externally provided, DType.
67    dtype: Option<DType>,
68    /// An optional, externally provided, file layout.
69    footer: Option<Footer>,
70    /// Whether to include user-defined metadata segments when opening the file.
71    include_metadata: bool,
72    /// A metrics registry for the file.
73    metrics_registry: Option<Arc<dyn MetricsRegistry>>,
74    /// Default labels applied to all the file's metrics
75    labels: Vec<Label>,
76    /// Whether to cache file's LayoutReader between scans
77    cache_layout_reader: bool,
78}
79
80/// Extension trait for constructing [`VortexOpenOptions`] from a session.
81pub trait OpenOptionsSessionExt:
82    ArraySessionExt + LayoutSessionExt + RuntimeSessionExt + MemorySessionExt
83{
84    /// Create a new [`VortexOpenOptions`] using the provided session to open a file.
85    fn open_options(&self) -> VortexOpenOptions {
86        VortexOpenOptions {
87            session: self.session(),
88            segment_cache: None,
89            initial_read_size: INITIAL_READ_SIZE,
90            file_size: None,
91            dtype: None,
92            footer: None,
93            include_metadata: false,
94            metrics_registry: None,
95            labels: Vec::default(),
96            cache_layout_reader: false,
97        }
98    }
99}
100impl<S: ArraySessionExt + LayoutSessionExt + RuntimeSessionExt + MemorySessionExt>
101    OpenOptionsSessionExt for S
102{
103}
104
105impl VortexOpenOptions {
106    /// Return the session this opener is bound to.
107    pub fn session(&self) -> &VortexSession {
108        &self.session
109    }
110
111    /// Configure how many bytes to read from the end of the file before parsing the footer.
112    ///
113    /// The actual read is at least large enough to contain the maximum postscript and EOF marker,
114    /// and no larger than the file. Increase this when you expect footer segments to be near the
115    /// end and want to avoid a second footer read.
116    pub fn with_initial_read_size(mut self, initial_read_size: usize) -> Self {
117        self.initial_read_size = initial_read_size;
118        self
119    }
120
121    /// Cache the file's [`LayoutReader`](vortex_layout::LayoutReader) and V2
122    /// [`ScanCache`](vortex_layout::scan::v2::ScanCache) between scans.
123    ///
124    /// This avoids rebuilding the reader tree or lowered scan plan, and re-reading dictionary
125    /// values, for repeated scans of the same [`VortexFile`], at the cost of keeping that state
126    /// alive for the lifetime of the file handle.
127    pub fn with_layout_reader_cache(mut self) -> Self {
128        self.cache_layout_reader = true;
129        self
130    }
131
132    /// Configure a custom [`SegmentCache`].
133    ///
134    /// The cache is checked before the underlying file segment source. Segments covered by the
135    /// initial footer read are also inserted into an internal first-read cache.
136    pub fn with_segment_cache(mut self, segment_cache: Arc<dyn SegmentCache>) -> Self {
137        self.segment_cache = Some(segment_cache);
138        self
139    }
140
141    /// Disable the configured segment cache.
142    ///
143    /// This is useful when deriving an opener for a source whose buffers have different memory
144    /// placement requirements from the configured host cache.
145    pub fn without_segment_cache(mut self) -> Self {
146        self.segment_cache = None;
147        self
148    }
149
150    /// Configure a known file size.
151    ///
152    /// This helps to prevent an I/O request to discover the size of the file.
153    /// Of course, all bets are off if you pass an incorrect value.
154    pub fn with_file_size(mut self, file_size: u64) -> Self {
155        self.file_size = Some(file_size);
156        self
157    }
158
159    /// Configure a known file size.
160    ///
161    /// This helps to prevent an I/O request to discover the size of the file.
162    /// Of course, all bets are off if you pass an incorrect value.
163    pub fn with_some_file_size(mut self, file_size: Option<u64>) -> Self {
164        self.file_size = file_size;
165        self
166    }
167
168    /// Configure a known DType.
169    ///
170    /// If this is provided, then the Vortex file may be opened with fewer I/O requests.
171    ///
172    /// For Vortex files that do not contain a `DType`, this is required.
173    pub fn with_dtype(mut self, dtype: DType) -> Self {
174        self.dtype = Some(dtype);
175        self
176    }
177
178    /// Configure a known file footer.
179    ///
180    /// If this is provided, then the Vortex file can be opened without performing any I/O.
181    /// Once open, the [`Footer`] can be accessed via [`crate::VortexFile::footer`].
182    pub fn with_footer(mut self, footer: Footer) -> Self {
183        self.dtype = Some(footer.layout().dtype().clone());
184        self.footer = Some(footer);
185        self
186    }
187
188    /// Include user-defined metadata segments when opening the file.
189    ///
190    /// By default, opening a file reads only the metadata required to interpret the layout.
191    /// Enabling this option loads all metadata segments named by the postscript, which may require
192    /// additional reads before the footer is returned.
193    pub fn include_metadata(mut self) -> Self {
194        self.include_metadata = true;
195        self
196    }
197
198    /// Configure whether user-defined metadata segments are included when opening the file.
199    ///
200    /// Enabling this option loads all metadata segments named by the postscript, which may require
201    /// additional reads before the footer is returned.
202    pub fn with_include_metadata(mut self, include_metadata: bool) -> Self {
203        self.include_metadata = include_metadata;
204        self
205    }
206
207    /// Configure a custom [`MetricsRegistry`] implementation.
208    pub fn with_metrics_registry(mut self, metrics: Arc<dyn MetricsRegistry>) -> Self {
209        self.metrics_registry = Some(metrics);
210        self
211    }
212
213    /// Adds labels to all the file's metrics.
214    pub fn with_labels(mut self, labels: Vec<Label>) -> Self {
215        self.labels.extend(labels);
216        self
217    }
218
219    /// Open a Vortex file using the provided I/O source.
220    ///
221    /// This is the most common way to open a [`VortexFile`] and tends to provide the best
222    /// out-of-the-box performance. The underlying I/O system will continue to be optimised for
223    /// different file systems and object stores so we encourage users to use this method
224    /// whenever possible and file issues if they encounter problems.
225    pub async fn open(self, source: Arc<dyn VortexReadAt>) -> VortexResult<VortexFile> {
226        self.open_read(source).await
227    }
228
229    /// Open a Vortex file from a filesystem path.
230    #[cfg(not(target_arch = "wasm32"))]
231    pub async fn open_path(self, path: impl AsRef<std::path::Path>) -> VortexResult<VortexFile> {
232        use vortex_io::std_file::FileReadAt;
233        let handle = self.session.handle();
234        let allocator = self.session.allocator();
235        let source = Arc::new(FileReadAt::open_with_allocator(path, handle, allocator)?);
236        self.open(source).await
237    }
238
239    /// Open a Vortex file from an in-memory buffer.
240    ///
241    /// This uses a `BufferSegmentSource` that resolves segments synchronously
242    /// by slicing the buffer directly, bypassing the async I/O pipeline.
243    ///
244    /// Segment cache and metrics registry settings are ignored for this path.
245    pub fn open_buffer<B: Into<ByteBuffer>>(self, buffer: B) -> VortexResult<VortexFile> {
246        let buffer: ByteBuffer = buffer.into();
247
248        if self.segment_cache.is_some() {
249            tracing::warn!("segment cache is ignored for in-memory `open_buffer`");
250        }
251        if self.metrics_registry.is_some() {
252            tracing::warn!("metrics registry is ignored for in-memory `open_buffer`");
253        }
254
255        let cache_layout_reader = self.cache_layout_reader;
256        let include_metadata = self.include_metadata;
257        let mut opts = self.with_initial_read_size(0);
258
259        let footer = match opts.footer.take() {
260            Some(footer) => footer,
261            None => block_on(opts.read_footer(&buffer))?.footer,
262        };
263        footer.validate_file_size(buffer.len() as u64)?;
264
265        let segment_source: Arc<dyn SegmentSource> = Arc::new(BufferSegmentSource::new(
266            buffer,
267            footer.segment_specs_with_metadata(),
268        ));
269        let metadata = if include_metadata {
270            block_on(resolve_metadata(&footer, Arc::clone(&segment_source)))?
271        } else {
272            Arc::new(HashMap::new())
273        };
274        let file = VortexFile::new(footer, segment_source, opts.session).with_metadata(metadata);
275        Ok(if cache_layout_reader {
276            file.with_caching()
277        } else {
278            file
279        })
280    }
281
282    /// Open a [`VortexFile`] using any [`VortexReadAt`] implementation.
283    ///
284    /// This is the common path for files, object stores, and custom random-access sources.
285    pub async fn open_read<R: VortexReadAt + Clone>(self, reader: R) -> VortexResult<VortexFile> {
286        let segment_cache = self
287            .segment_cache
288            .clone()
289            .unwrap_or_else(|| Arc::new(NoOpSegmentCache));
290
291        let metrics_registry = self
292            .metrics_registry
293            .clone()
294            .unwrap_or_else(|| Arc::new(DefaultMetricsRegistry::default()));
295
296        let FooterRead {
297            footer,
298            initial_segments,
299        } = if let Some(footer) = self.footer {
300            if let Some(file_size) = self.file_size {
301                footer.validate_file_size(file_size)?;
302            }
303            FooterRead {
304                footer,
305                initial_segments: HashMap::default(),
306            }
307        } else {
308            self.read_footer(&reader).await?
309        };
310
311        // With a segment cache, also store the segments that the footer read covered, so that a
312        // later open given this footer finds them there, instead of reading them again. Keep them
313        // in the initial map too: the cache can evict them, or not admit them at all, and the
314        // buffers are shared, not copied.
315        if self.segment_cache.is_some() {
316            for (id, buffer) in &initial_segments {
317                segment_cache.put(*id, buffer.clone()).await?;
318            }
319        }
320
321        let segment_cache = Arc::new(InstrumentedSegmentCache::new(
322            InitialReadSegmentCache {
323                initial: initial_segments,
324                fallback: segment_cache,
325            },
326            metrics_registry.as_ref(),
327            self.labels.clone(),
328        ));
329
330        let metrics = RequestMetrics::new(metrics_registry.as_ref(), self.labels);
331
332        // Create a segment source backed by the VortexRead implementation.
333        let segment_source = Arc::new(SharedSegmentSource::new(FileSegmentSource::open(
334            footer.segment_specs_with_metadata(),
335            reader,
336            self.session.handle(),
337            metrics,
338        )));
339
340        // Wrap up the segment source to first resolve segments from the initial read cache.
341        let segment_source: Arc<dyn SegmentSource> = Arc::new(SegmentCacheSourceAdapter::new(
342            segment_cache,
343            segment_source,
344        ));
345
346        let metadata = if self.include_metadata {
347            resolve_metadata(&footer, Arc::clone(&segment_source)).await?
348        } else {
349            Arc::new(HashMap::new())
350        };
351        let file =
352            VortexFile::new(footer, segment_source, self.session.clone()).with_metadata(metadata);
353        Ok(if self.cache_layout_reader {
354            file.with_caching()
355        } else {
356            file
357        })
358    }
359
360    async fn read_footer(&self, read: &dyn VortexReadAt) -> VortexResult<FooterRead> {
361        // Fetch the file size and perform the initial read.
362        let file_size = match self.file_size {
363            None => read.size().await?,
364            Some(file_size) => file_size,
365        };
366        let mut initial_read_size = self
367            .initial_read_size
368            // Make sure we read enough to cover the postscript
369            .max(MAX_POSTSCRIPT_SIZE as usize + EOF_SIZE);
370        if let Ok(file_size) = usize::try_from(file_size) {
371            initial_read_size = initial_read_size.min(file_size);
372        }
373
374        let initial_offset = file_size - initial_read_size as u64;
375        let initial_read: ByteBuffer = read
376            .read_at(initial_offset, initial_read_size, Alignment::none())
377            .await?
378            .try_into_host()?
379            .await?;
380
381        let mut deserializer = Footer::deserializer(initial_read, self.session.clone())
382            .with_size(file_size)
383            .with_some_dtype(self.dtype.clone());
384
385        let footer = loop {
386            match deserializer.deserialize()? {
387                DeserializeStep::NeedMoreData { offset, len } => {
388                    let more_data = read
389                        .read_at(offset, len, Alignment::none())
390                        .await?
391                        .try_into_host()?
392                        .await?;
393                    deserializer.prefix_data(more_data);
394                }
395                DeserializeStep::NeedFileSize => unreachable!("We passed file_size above"),
396                DeserializeStep::Done(footer) => break Ok::<_, VortexError>(footer),
397            }
398        }?;
399
400        // Segment specs describe the data file, not necessarily the byte stream used to
401        // deserialize a standalone cached footer. Validate them here, where we know this is the
402        // size of the actual data source (see issue #8819).
403        footer.validate_file_size(file_size)?;
404
405        // If the initial read happened to cover any segments, then we can populate the
406        // segment cache
407        let initial_offset = file_size - (deserializer.buffer().len() as u64);
408        let initial_segments =
409            Self::collect_initial_segments(initial_offset, deserializer.buffer(), &footer)?;
410
411        Ok(FooterRead {
412            footer,
413            initial_segments,
414        })
415    }
416
417    /// Collect segments that were covered by the initial read.
418    fn collect_initial_segments(
419        initial_offset: u64,
420        initial_read: &ByteBuffer,
421        footer: &Footer,
422    ) -> VortexResult<HashMap<SegmentId, ByteBuffer>> {
423        let mut initial_read_segments = HashMap::default();
424
425        // Iterate `segment_specs_with_metadata` (not just the segment map) so metadata segments
426        // covered by the initial read are cached too. Metadata segments are appended and not
427        // offset-sorted, so we skip per-segment rather than partition on offset.
428        for (idx, segment) in footer.segment_specs_with_metadata().iter().enumerate() {
429            if segment.offset < initial_offset {
430                continue;
431            }
432            let segment_id =
433                SegmentId::from(u32::try_from(idx).vortex_expect("Invalid segment ID"));
434            let offset =
435                usize::try_from(segment.offset - initial_offset).vortex_expect("Invalid offset");
436            // The segment map is validated against the file size before this method is called, but
437            // still bounds-check here so slicing never depends on that distant validation for panic
438            // safety (see issue #8819).
439            let end = offset
440                .checked_add(segment.length as usize)
441                .filter(|end| *end <= initial_read.len())
442                .ok_or_else(|| {
443                    vortex_err!(
444                        "Segment at offset {} with length {} is out of bounds of the \
445                         {}-byte initial read",
446                        segment.offset,
447                        segment.length,
448                        initial_read.len(),
449                    )
450                })?;
451            let buffer = initial_read.slice(offset..end).aligned(segment.alignment);
452            initial_read_segments.insert(segment_id, buffer);
453        }
454
455        Ok(initial_read_segments)
456    }
457}
458
459async fn resolve_metadata(
460    footer: &Footer,
461    segment_source: Arc<dyn SegmentSource>,
462) -> VortexResult<Arc<HashMap<String, ByteBuffer>>> {
463    let first_metadata_id = footer.segment_map().len();
464    let requests = footer
465        .metadata_segments()
466        .enumerate()
467        .map(|(index, (key, locator))| {
468            let id = u32::try_from(first_metadata_id + index).map(SegmentId::from);
469            let key = key.to_string();
470            let alignment = locator.alignment;
471            let segment_source = Arc::clone(&segment_source);
472            async move {
473                let handle = segment_source.request(id?).await?;
474                let buffer = handle.try_into_host()?.await?;
475                Ok::<_, VortexError>((
476                    key,
477                    ByteBuffer::copy_from_aligned(buffer.as_slice(), alignment),
478                ))
479            }
480        });
481    let metadata = futures::future::try_join_all(requests)
482        .await?
483        .into_iter()
484        .collect::<HashMap<_, _>>();
485
486    Ok(Arc::new(metadata))
487}
488
489#[cfg(feature = "object_store")]
490impl VortexOpenOptions {
491    /// Open a Vortex file from an `object_store` backend and path.
492    ///
493    /// `path` is the object's *literal* key, exactly as the store holds it.
494    pub async fn open_object_store(
495        self,
496        object_store: &Arc<dyn object_store::ObjectStore>,
497        path: object_store::path::Path,
498    ) -> VortexResult<VortexFile> {
499        use vortex_io::object_store::ObjectStoreReadAt;
500
501        let handle = self.session.handle();
502        let allocator = self.session.allocator();
503        let source = Arc::new(ObjectStoreReadAt::new_with_allocator(
504            Arc::clone(object_store),
505            path,
506            handle,
507            allocator,
508        ));
509        self.open(source).await
510    }
511}
512
513#[cfg(test)]
514mod tests {
515    use std::alloc::Layout;
516    use std::ptr::NonNull;
517    use std::sync::atomic::AtomicUsize;
518    use std::sync::atomic::Ordering;
519
520    use allocator_api2::alloc::AllocError;
521    use allocator_api2::alloc::Allocator;
522    use allocator_api2::alloc::Global;
523    use futures::future::BoxFuture;
524    use parking_lot::Mutex;
525    use vortex_array::IntoArray;
526    use vortex_array::buffer::BufferHandle;
527    use vortex_array::memory::BufferAllocatorRef;
528    use vortex_array::memory::MemorySessionExt;
529    use vortex_buffer::Alignment;
530    use vortex_buffer::Buffer;
531    use vortex_buffer::ByteBuffer;
532    use vortex_buffer::ByteBufferMut;
533    use vortex_error::vortex_bail;
534    use vortex_io::session::RuntimeSession;
535    use vortex_layout::session::LayoutSession;
536    use vortex_session::registry::Id;
537    use vortex_session::registry::ReadContext;
538
539    use super::*;
540    use crate::WriteOptionsSessionExt;
541    use crate::footer::SegmentSpec;
542
543    fn test_session() -> VortexSession {
544        let session = vortex_array::array_session()
545            .with::<LayoutSession>()
546            .with::<RuntimeSession>();
547        crate::register_default_encodings(&session);
548        crate::enable_all_registered_array_encodings(&session);
549        session
550    }
551
552    #[derive(Clone)]
553    // Define CountingRead struct
554    struct CountingRead<R> {
555        inner: R,
556        total_read: Arc<AtomicUsize>,
557        first_read_len: Arc<AtomicUsize>,
558        reads: Arc<Mutex<Vec<(u64, usize)>>>,
559    }
560
561    impl<R: VortexReadAt + Clone> VortexReadAt for CountingRead<R> {
562        fn size(&self) -> BoxFuture<'static, VortexResult<u64>> {
563            self.inner.size()
564        }
565
566        fn read_at(
567            &self,
568            offset: u64,
569            length: usize,
570            alignment: Alignment,
571        ) -> BoxFuture<'static, VortexResult<BufferHandle>> {
572            self.total_read.fetch_add(length, Ordering::Relaxed);
573            self.reads.lock().push((offset, length));
574            let _ = self.first_read_len.compare_exchange(
575                0,
576                length,
577                Ordering::Relaxed,
578                Ordering::Relaxed,
579            );
580            self.inner.read_at(offset, length, alignment)
581        }
582
583        fn concurrency(&self) -> usize {
584            self.inner.concurrency()
585        }
586    }
587
588    #[derive(Debug)]
589    struct CountingAllocator {
590        allocations: Arc<AtomicUsize>,
591    }
592
593    // SAFETY: this forwards memory operations to Global and only counts allocations.
594    unsafe impl Allocator for CountingAllocator {
595        fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
596            self.allocations.fetch_add(1, Ordering::Relaxed);
597            Global.allocate(layout)
598        }
599
600        unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
601            // SAFETY: ptr and layout came from Global.
602            unsafe { Global.deallocate(ptr, layout) }
603        }
604    }
605
606    #[tokio::test]
607    async fn test_initial_read_size() {
608        let session = vortex_array::array_session()
609            .with::<LayoutSession>()
610            .with::<RuntimeSession>();
611
612        crate::register_default_encodings(&session);
613        crate::enable_all_registered_array_encodings(&session);
614
615        // Create a large file (> 1MB)
616        let mut buf = ByteBufferMut::empty();
617
618        // 1.5M integers -> ~6MB. We use high-entropy (pseudo-random) values so the data does not
619        // compress well under any encoding (Sequence, RunEnd, Delta, ...), keeping the written
620        // file comfortably above 1MB.
621        let mut state = 0x9E37_79B9u32;
622        let array = Buffer::from(
623            (0i32..1_500_000)
624                .map(|_| {
625                    state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
626                    state as i32
627                })
628                .collect::<Vec<i32>>(),
629        )
630        .into_array();
631
632        session
633            .write_options()
634            .write(&mut buf, array.to_array_stream())
635            .await
636            .unwrap();
637
638        let buffer = ByteBuffer::from(buf);
639        assert!(
640            buffer.len() > 1024 * 1024,
641            "Buffer length is only {} bytes",
642            buffer.len()
643        );
644
645        let total_read = Arc::new(AtomicUsize::new(0));
646        let first_read_len = Arc::new(AtomicUsize::new(0));
647        let reader = CountingRead {
648            inner: buffer,
649            total_read: Arc::clone(&total_read),
650            first_read_len: Arc::clone(&first_read_len),
651            reads: Arc::new(Mutex::new(Vec::new())),
652        };
653
654        // Open the file
655        let _file = session.open_options().open_read(reader).await.unwrap();
656
657        // Assert that we read approximately the postscript size, not 1MB
658        let first = first_read_len.load(Ordering::Relaxed);
659        assert_eq!(
660            first,
661            MAX_POSTSCRIPT_SIZE as usize + EOF_SIZE,
662            "Read exactly the postscript size"
663        );
664        let read = total_read.load(Ordering::Relaxed);
665        assert!(read < 1024 * 1024, "Read {} bytes, expected < 1MB", read);
666    }
667
668    #[tokio::test]
669    async fn test_metadata_outside_initial_read_uses_targeted_read() -> VortexResult<()> {
670        let session = vortex_array::array_session()
671            .with::<LayoutSession>()
672            .with::<RuntimeSession>();
673        crate::register_default_encodings(&session);
674        crate::enable_all_registered_array_encodings(&session);
675
676        let metadata = ByteBuffer::copy_from(vec![0x5a; INITIAL_READ_SIZE * 2]);
677        let mut output = ByteBufferMut::empty();
678        let summary = session
679            .write_options()
680            .with_metadata_segment("outside", metadata.clone())
681            .write(
682                &mut output,
683                Buffer::from(vec![1u32]).into_array().to_array_stream(),
684            )
685            .await?;
686        let locator = *summary
687            .footer()
688            .metadata_segment("outside")
689            .vortex_expect("metadata locator");
690        let bytes = ByteBuffer::from(output);
691        assert!(locator.offset < bytes.len() as u64 - INITIAL_READ_SIZE as u64);
692
693        let default_total = Arc::new(AtomicUsize::new(0));
694        let default_reads = Arc::new(Mutex::new(Vec::new()));
695        let default_reader = CountingRead {
696            inner: bytes.clone(),
697            total_read: Arc::clone(&default_total),
698            first_read_len: Arc::new(AtomicUsize::new(0)),
699            reads: Arc::clone(&default_reads),
700        };
701        let default_file = session.open_options().open_read(default_reader).await?;
702        assert!(default_file.metadata_segment("outside").is_none());
703        assert!(
704            !default_reads
705                .lock()
706                .contains(&(locator.offset, locator.length as usize))
707        );
708        // A default open must not amplify into the metadata: total bytes read stay
709        // below the metadata segment's size (itself 2x the initial read).
710        assert!(
711            default_total.load(Ordering::Relaxed) < metadata.len(),
712            "default open read {} bytes; metadata segment is {}",
713            default_total.load(Ordering::Relaxed),
714            metadata.len()
715        );
716
717        let metadata_reads = Arc::new(Mutex::new(Vec::new()));
718        let metadata_reader = CountingRead {
719            inner: bytes,
720            total_read: Arc::new(AtomicUsize::new(0)),
721            first_read_len: Arc::new(AtomicUsize::new(0)),
722            reads: Arc::clone(&metadata_reads),
723        };
724        let file = session
725            .open_options()
726            .include_metadata()
727            .open_read(metadata_reader)
728            .await?;
729        assert_eq!(
730            file.metadata_segment("outside").map(ByteBuffer::as_slice),
731            Some(metadata.as_slice())
732        );
733        assert!(
734            metadata_reads
735                .lock()
736                .contains(&(locator.offset, locator.length as usize))
737        );
738
739        Ok(())
740    }
741
742    #[tokio::test]
743    async fn test_with_footer_include_metadata_open_buffer_resolves() -> VortexResult<()> {
744        let session = vortex_array::array_session()
745            .with::<LayoutSession>()
746            .with::<RuntimeSession>();
747        crate::register_default_encodings(&session);
748        crate::enable_all_registered_array_encodings(&session);
749
750        let value = ByteBuffer::copy_from(b"supplied-footer metadata");
751        let mut output = ByteBufferMut::empty();
752        let summary = session
753            .write_options()
754            .with_metadata_segment("key", value.clone())
755            .write(
756                &mut output,
757                Buffer::from(vec![1u32]).into_array().to_array_stream(),
758            )
759            .await?;
760        let footer = summary.footer().clone();
761        let bytes = ByteBuffer::from(output);
762
763        let file = session
764            .open_options()
765            .with_footer(footer.clone())
766            .include_metadata()
767            .open_buffer(bytes.clone())?;
768        assert_eq!(
769            file.metadata_segment("key").map(ByteBuffer::as_slice),
770            Some(value.as_slice())
771        );
772
773        let default = session
774            .open_options()
775            .with_footer(footer)
776            .open_buffer(bytes)?;
777        assert!(default.metadata_segment("key").is_none());
778
779        Ok(())
780    }
781
782    #[cfg(not(target_arch = "wasm32"))]
783    #[tokio::test]
784    async fn test_open_path_uses_memory_session_allocator() {
785        let session = vortex_array::array_session()
786            .with::<LayoutSession>()
787            .with::<RuntimeSession>();
788
789        crate::register_default_encodings(&session);
790        crate::enable_all_registered_array_encodings(&session);
791
792        let mut buf = ByteBufferMut::empty();
793        let array = Buffer::from((0i32..16_384).collect::<Vec<i32>>()).into_array();
794        session
795            .write_options()
796            .write(&mut buf, array.to_array_stream())
797            .await
798            .unwrap();
799
800        let file_path = std::env::temp_dir().join(format!(
801            "vortex-open-memory-session-{}.vx",
802            std::process::id()
803        ));
804        std::fs::write(&file_path, ByteBuffer::from(buf).as_slice()).unwrap();
805
806        let allocations = Arc::new(AtomicUsize::new(0));
807        let session = session.with_allocator(BufferAllocatorRef::new(CountingAllocator {
808            allocations: Arc::clone(&allocations),
809        }));
810
811        let _file = session.open_options().open_path(&file_path).await.unwrap();
812        std::fs::remove_file(&file_path).unwrap();
813
814        assert!(
815            allocations.load(Ordering::Relaxed) > 0,
816            "expected at least one host allocation from MemorySession"
817        );
818    }
819
820    /// `collect_initial_segments` must bounds-check the segment map against the initial read rather
821    /// than slicing unchecked, so a segment larger than the read returns an error (see issue #8819).
822    #[tokio::test]
823    async fn collect_initial_segments_rejects_out_of_bounds_segment() -> VortexResult<()> {
824        let session = test_session();
825
826        // A valid root layout is obtained by writing and parsing a small file.
827        // `collect_initial_segments` only consults the segment map, so the layout is irrelevant.
828        let mut buf = ByteBufferMut::empty();
829        let array = Buffer::from((0i32..16).collect::<Vec<i32>>()).into_array();
830        session
831            .write_options()
832            .write(&mut buf, array.to_array_stream())
833            .await?;
834        let footer = session
835            .open_options()
836            .read_footer(&ByteBuffer::from(buf))
837            .await?
838            .footer;
839
840        // Build a footer whose sole segment is far larger than the initial read below.
841        let bad_segments: Arc<[SegmentSpec]> = Arc::from([SegmentSpec {
842            offset: 0,
843            length: 1024,
844            alignment: Alignment::none(),
845        }]);
846        let bad_footer = Footer::new(
847            Arc::clone(footer.layout()),
848            bad_segments,
849            None,
850            ReadContext::new(Vec::<Id>::new()),
851        );
852
853        let initial_read = ByteBuffer::zeroed(16);
854        let Err(err) = VortexOpenOptions::collect_initial_segments(0, &initial_read, &bad_footer)
855        else {
856            vortex_bail!("collecting an out-of-bounds segment must return an error");
857        };
858        assert!(
859            err.to_string().contains("out of bounds"),
860            "unexpected error: {err}"
861        );
862
863        Ok(())
864    }
865
866    #[tokio::test]
867    async fn standalone_footer_round_trip() -> VortexResult<()> {
868        let session = test_session();
869
870        let mut file_bytes = ByteBufferMut::empty();
871        let values = (0i32..65_536)
872            .map(|value| value.wrapping_mul(1_664_525).wrapping_add(1_013_904_223))
873            .collect::<Vec<_>>();
874        let array = Buffer::from(values).into_array();
875        session
876            .write_options()
877            .write(&mut file_bytes, array.to_array_stream())
878            .await?;
879        let file_bytes = ByteBuffer::from(file_bytes);
880
881        let footer = session
882            .open_options()
883            .open_buffer(file_bytes.clone())?
884            .footer()
885            .clone();
886        let last_segment_end = footer
887            .segment_map()
888            .iter()
889            .map(|segment| segment.offset + u64::from(segment.length))
890            .max()
891            .unwrap_or_default();
892        let serialized_footer = footer.into_serializer().serialize()?;
893        let serialized_footer_size = serialized_footer.iter().map(ByteBuffer::len).sum();
894        assert!(last_segment_end > serialized_footer_size as u64);
895        let mut footer_bytes = ByteBufferMut::with_capacity(serialized_footer_size);
896        for buffer in serialized_footer {
897            footer_bytes.extend_from_slice(&buffer);
898        }
899
900        let mut deserializer = Footer::deserializer(footer_bytes.freeze(), session.clone())
901            .with_size(serialized_footer_size as u64);
902        let DeserializeStep::Done(cached_footer) = deserializer.deserialize()? else {
903            vortex_bail!("standalone footer bytes must be sufficient for deserialization");
904        };
905
906        session
907            .open_options()
908            .with_footer(cached_footer)
909            .open_buffer(file_bytes)?;
910
911        Ok(())
912    }
913}