1mod field_sizes;
12mod file_layout;
13mod file_statistics;
14mod postscript;
15mod segment;
16
17use std::sync::Arc;
18
19mod serializer;
20pub use serializer::*;
21mod deserializer;
22pub use deserializer::*;
23pub use field_sizes::CompressedFieldSizes;
24pub use file_statistics::FileStatistics;
25use flatbuffers::root;
26use itertools::Itertools;
27pub use segment::*;
28use vortex_array::ArrayId;
29use vortex_array::dtype::DType;
30use vortex_array::flatbuffers::FlatBuffer;
31use vortex_buffer::ByteBuffer;
32use vortex_error::VortexResult;
33use vortex_error::vortex_bail;
34use vortex_error::vortex_err;
35use vortex_layout::LayoutEncodingId;
36use vortex_layout::LayoutRef;
37use vortex_layout::layout_from_flatbuffer_with_options;
38use vortex_session::VortexSession;
39use vortex_session::registry::ReadContext;
40
41use crate::flatbuffers::footer as fb;
42
43pub(crate) const MAX_METADATA_SEGMENTS: usize = 16;
46
47pub(crate) const MAX_METADATA_KEY_BYTES: usize = 64;
53
54pub(crate) type MetadataSegments = Arc<[(String, SegmentSpec)]>;
56
57#[derive(Debug, Clone)]
59pub struct Footer {
60 root_layout: LayoutRef,
61 segments: Arc<[SegmentSpec]>,
62 statistics: Option<FileStatistics>,
63 metadata: Arc<[(String, SegmentSpec)]>,
64 segments_with_metadata: Arc<[SegmentSpec]>,
67 segments_end: Option<u64>,
73 array_read_ctx: ReadContext,
75 approx_byte_size: Option<usize>,
77}
78
79impl Footer {
80 pub fn new(
81 root_layout: LayoutRef,
82 segments: Arc<[SegmentSpec]>,
83 statistics: Option<FileStatistics>,
84 array_read_ctx: ReadContext,
85 ) -> Self {
86 Self {
87 root_layout,
88 segments_with_metadata: Arc::clone(&segments),
89 segments_end: segments_end(&segments),
90 segments,
91 statistics,
92 metadata: Arc::from([]),
93 array_read_ctx,
94 approx_byte_size: None,
95 }
96 }
97
98 pub(crate) fn with_approx_byte_size(mut self, approx_byte_size: usize) -> Self {
99 self.approx_byte_size = Some(approx_byte_size);
100 self
101 }
102
103 pub(crate) fn from_flatbuffer(
105 footer_bytes: &[u8],
106 layout_bytes: FlatBuffer,
107 dtype: DType,
108 statistics: Option<FileStatistics>,
109 metadata: Arc<[(String, SegmentSpec)]>,
110 session: &VortexSession,
111 ) -> VortexResult<Self> {
112 let metadata_bytes: usize = metadata
113 .iter()
114 .map(|(key, _segment)| key.len() + size_of::<SegmentSpec>())
115 .sum();
116 let approx_byte_size = footer_bytes.len() + layout_bytes.len() + metadata_bytes;
117 let fb_footer = root::<fb::Footer>(footer_bytes)?;
118
119 let layout_specs = fb_footer.layout_specs();
121 #[expect(clippy::disallowed_methods, reason = "interning a dynamic id")]
122 let layout_ids: Arc<[_]> = layout_specs
123 .iter()
124 .flat_map(|e| e.iter())
125 .map(|encoding| LayoutEncodingId::new(encoding.id()))
126 .collect();
127 let layout_read_ctx = ReadContext::new(layout_ids);
128
129 let array_specs = fb_footer.array_specs();
131 #[expect(clippy::disallowed_methods, reason = "interning a dynamic id")]
132 let array_ids: Arc<[_]> = array_specs
133 .iter()
134 .flat_map(|e| e.iter())
135 .map(|encoding| ArrayId::new(encoding.id()))
136 .collect();
137 let array_read_ctx = ReadContext::new(array_ids);
138
139 let root_layout = layout_from_flatbuffer_with_options(
140 layout_bytes,
141 &dtype,
142 &layout_read_ctx,
143 &array_read_ctx,
144 session,
145 session.allows_unknown(),
146 )?;
147
148 let segments: Arc<[SegmentSpec]> = fb_footer
149 .segment_specs()
150 .ok_or_else(|| vortex_err!("FileLayout missing segment specs"))?
151 .iter()
152 .map(SegmentSpec::try_from)
153 .try_collect()?;
154
155 if !segments.is_sorted_by_key(|segment| segment.offset) {
157 vortex_bail!("Segment offsets are not ordered");
158 }
159
160 let segments_with_metadata = segments_with_metadata(&segments, &metadata);
161
162 Ok(Self {
163 root_layout,
164 segments_end: segments_end(&segments_with_metadata),
165 segments_with_metadata,
166 segments,
167 statistics,
168 metadata,
169 array_read_ctx,
170 approx_byte_size: Some(approx_byte_size),
171 })
172 }
173
174 pub fn layout(&self) -> &LayoutRef {
176 &self.root_layout
177 }
178
179 pub fn segment_map(&self) -> &Arc<[SegmentSpec]> {
181 &self.segments
182 }
183
184 pub fn statistics(&self) -> Option<&FileStatistics> {
186 self.statistics.as_ref()
187 }
188
189 pub fn metadata_segments(&self) -> impl Iterator<Item = (&str, &SegmentSpec)> {
191 self.metadata
192 .iter()
193 .map(|(key, segment)| (key.as_str(), segment))
194 }
195
196 pub fn metadata_segment(&self, key: &str) -> Option<&SegmentSpec> {
198 self.metadata
199 .iter()
200 .find_map(|(candidate, segment)| (candidate == key).then_some(segment))
201 }
202
203 pub(crate) fn with_metadata_segments(mut self, metadata: Arc<[(String, SegmentSpec)]>) -> Self {
204 self.segments_with_metadata = segments_with_metadata(&self.segments, &metadata);
205 self.segments_end = segments_end(&self.segments_with_metadata);
206 self.metadata = metadata;
207 self
208 }
209
210 pub(crate) fn segment_specs_with_metadata(&self) -> Arc<[SegmentSpec]> {
211 Arc::clone(&self.segments_with_metadata)
212 }
213
214 pub fn compressed_field_sizes(&self) -> VortexResult<CompressedFieldSizes> {
220 CompressedFieldSizes::try_new(&self.root_layout, &self.segments)
221 }
222
223 pub fn dtype(&self) -> &DType {
225 self.root_layout.dtype()
226 }
227
228 pub fn approx_byte_size(&self) -> Option<usize> {
230 self.approx_byte_size
231 }
232
233 pub fn row_count(&self) -> u64 {
235 self.root_layout.row_count()
236 }
237
238 pub(crate) fn validate_file_size(&self, file_size: u64) -> VortexResult<()> {
240 if self.segments_end.is_some_and(|end| end <= file_size) {
241 return Ok(());
242 }
243 validate_segments_within_file(&self.segments_with_metadata, file_size)
245 }
246
247 pub fn into_serializer(self) -> FooterSerializer {
249 FooterSerializer::new(self)
250 }
251
252 pub fn deserializer(eof_buffer: ByteBuffer, session: VortexSession) -> FooterDeserializer {
254 FooterDeserializer::new(eof_buffer, session)
255 }
256}
257
258fn segments_with_metadata(
259 segments: &Arc<[SegmentSpec]>,
260 metadata: &[(String, SegmentSpec)],
261) -> Arc<[SegmentSpec]> {
262 if metadata.is_empty() {
263 return Arc::clone(segments);
264 }
265 segments
266 .iter()
267 .copied()
268 .chain(metadata.iter().map(|(_, segment)| *segment))
269 .collect()
270}
271
272fn segments_end(segments: &[SegmentSpec]) -> Option<u64> {
273 segments.iter().try_fold(0u64, |end, segment| {
274 Some(end.max(segment.offset.checked_add(u64::from(segment.length))?))
275 })
276}
277
278fn validate_segments_within_file(segments: &[SegmentSpec], file_size: u64) -> VortexResult<()> {
284 for segment in segments {
285 let within_file = segment
286 .offset
287 .checked_add(segment.length as u64)
288 .is_some_and(|end| end <= file_size);
289 if !within_file {
290 vortex_bail!(
291 "Segment at offset {} with length {} extends past the end of the \
292 {file_size}-byte file",
293 segment.offset,
294 segment.length,
295 );
296 }
297 }
298 Ok(())
299}
300
301#[cfg(test)]
302mod tests {
303 use vortex_array::dtype::Nullability;
304 use vortex_array::dtype::PType;
305 use vortex_buffer::Alignment;
306 use vortex_layout::layouts::flat::FlatLayout;
307 use vortex_layout::segments::SegmentId;
308
309 use super::*;
310
311 fn segment(offset: u64, length: u32) -> SegmentSpec {
312 SegmentSpec {
313 offset,
314 length,
315 alignment: Alignment::none(),
316 }
317 }
318
319 fn footer_with_metadata_segment(metadata: SegmentSpec) -> Footer {
320 let layout = FlatLayout::new(
321 1,
322 DType::Primitive(PType::I32, Nullability::NonNullable),
323 SegmentId::from(0),
324 ReadContext::new([]),
325 )
326 .into_layout();
327 Footer::new(
328 layout,
329 Arc::from([segment(0, 100)]),
330 None,
331 ReadContext::new([]),
332 )
333 .with_metadata_segments(Arc::from([("some.key".to_string(), metadata)]))
334 }
335
336 #[test]
337 fn accepts_segments_within_file() -> VortexResult<()> {
338 validate_segments_within_file(&[segment(0, 100), segment(100, 50)], 150)?;
339 Ok(())
340 }
341
342 #[test]
343 fn rejects_segment_extending_past_end_of_file() {
344 let err =
345 validate_segments_within_file(&[segment(0, 100), segment(100, 51)], 150).unwrap_err();
346 assert!(err.to_string().contains("past the end"), "{err}");
347 }
348
349 #[test]
350 fn rejects_segment_offset_length_overflow() {
351 let err = validate_segments_within_file(&[segment(u64::MAX, 1)], u64::MAX).unwrap_err();
352 assert!(err.to_string().contains("past the end"), "{err}");
353 }
354
355 #[test]
356 fn segments_end_covers_overlapping_segments() {
357 assert_eq!(
361 segments_end(&[segment(0, 300), segment(100, 50)]),
362 Some(300)
363 );
364 assert_eq!(segments_end(&[]), Some(0));
365 assert_eq!(segments_end(&[segment(0, 1), segment(u64::MAX, 1)]), None);
366 }
367
368 #[test]
369 fn segments_with_metadata_appends_metadata_segments() {
370 let segments: Arc<[SegmentSpec]> = Arc::from([segment(0, 10), segment(10, 20)]);
371
372 let without = segments_with_metadata(&segments, &[]);
373 assert!(Arc::ptr_eq(&without, &segments));
374
375 let with = segments_with_metadata(&segments, &[("key".to_string(), segment(30, 5))]);
376 let spans: Vec<_> = with.iter().map(|s| (s.offset, s.length)).collect();
377 assert_eq!(spans, [(0, 10), (10, 20), (30, 5)]);
378 }
379
380 #[test]
381 fn rejects_metadata_segment_extending_past_end_of_file() {
382 let err = footer_with_metadata_segment(segment(100, 51))
383 .validate_file_size(150)
384 .unwrap_err();
385 assert!(err.to_string().contains("past the end"), "{err}");
386 }
387
388 #[test]
389 fn accepts_metadata_segment_within_file() -> VortexResult<()> {
390 footer_with_metadata_segment(segment(100, 50)).validate_file_size(150)?;
391 Ok(())
392 }
393}