1use 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#[derive(Clone)]
57pub struct VortexOpenOptions {
58 session: VortexSession,
60 segment_cache: Option<Arc<dyn SegmentCache>>,
62 initial_read_size: usize,
64 file_size: Option<u64>,
66 dtype: Option<DType>,
68 footer: Option<Footer>,
70 include_metadata: bool,
72 metrics_registry: Option<Arc<dyn MetricsRegistry>>,
74 labels: Vec<Label>,
76 cache_layout_reader: bool,
78}
79
80pub trait OpenOptionsSessionExt:
82 ArraySessionExt + LayoutSessionExt + RuntimeSessionExt + MemorySessionExt
83{
84 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 pub fn session(&self) -> &VortexSession {
108 &self.session
109 }
110
111 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 pub fn with_layout_reader_cache(mut self) -> Self {
128 self.cache_layout_reader = true;
129 self
130 }
131
132 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 pub fn without_segment_cache(mut self) -> Self {
146 self.segment_cache = None;
147 self
148 }
149
150 pub fn with_file_size(mut self, file_size: u64) -> Self {
155 self.file_size = Some(file_size);
156 self
157 }
158
159 pub fn with_some_file_size(mut self, file_size: Option<u64>) -> Self {
164 self.file_size = file_size;
165 self
166 }
167
168 pub fn with_dtype(mut self, dtype: DType) -> Self {
174 self.dtype = Some(dtype);
175 self
176 }
177
178 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 pub fn include_metadata(mut self) -> Self {
194 self.include_metadata = true;
195 self
196 }
197
198 pub fn with_include_metadata(mut self, include_metadata: bool) -> Self {
203 self.include_metadata = include_metadata;
204 self
205 }
206
207 pub fn with_metrics_registry(mut self, metrics: Arc<dyn MetricsRegistry>) -> Self {
209 self.metrics_registry = Some(metrics);
210 self
211 }
212
213 pub fn with_labels(mut self, labels: Vec<Label>) -> Self {
215 self.labels.extend(labels);
216 self
217 }
218
219 pub async fn open(self, source: Arc<dyn VortexReadAt>) -> VortexResult<VortexFile> {
226 self.open_read(source).await
227 }
228
229 #[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 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 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 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 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 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 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 .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 footer.validate_file_size(file_size)?;
404
405 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 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 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 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 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 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 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 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 let mut buf = ByteBufferMut::empty();
617
618 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 let _file = session.open_options().open_read(reader).await.unwrap();
656
657 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 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 #[tokio::test]
823 async fn collect_initial_segments_rejects_out_of_bounds_segment() -> VortexResult<()> {
824 let session = test_session();
825
826 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 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}