vortex_file/footer/
mod.rs1mod 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_buffer::ByteBuffer;
31use vortex_error::VortexResult;
32use vortex_error::vortex_bail;
33use vortex_error::vortex_err;
34use vortex_flatbuffers::FlatBuffer;
35use vortex_flatbuffers::footer as fb;
36use vortex_layout::LayoutEncodingId;
37use vortex_layout::LayoutRef;
38use vortex_layout::layout_from_flatbuffer_with_options;
39use vortex_session::VortexSession;
40use vortex_session::registry::ReadContext;
41
42pub(crate) const MAX_METADATA_SEGMENTS: usize = 16;
45
46pub(crate) const MAX_METADATA_KEY_BYTES: usize = 64;
52
53pub(crate) type MetadataSegments = Arc<[(String, SegmentSpec)]>;
55
56#[derive(Debug, Clone)]
58pub struct Footer {
59 root_layout: LayoutRef,
60 segments: Arc<[SegmentSpec]>,
61 statistics: Option<FileStatistics>,
62 metadata: Arc<[(String, SegmentSpec)]>,
63 array_read_ctx: ReadContext,
65 approx_byte_size: Option<usize>,
67}
68
69impl Footer {
70 pub fn new(
71 root_layout: LayoutRef,
72 segments: Arc<[SegmentSpec]>,
73 statistics: Option<FileStatistics>,
74 array_read_ctx: ReadContext,
75 ) -> Self {
76 Self {
77 root_layout,
78 segments,
79 statistics,
80 metadata: Arc::from([]),
81 array_read_ctx,
82 approx_byte_size: None,
83 }
84 }
85
86 pub(crate) fn with_approx_byte_size(mut self, approx_byte_size: usize) -> Self {
87 self.approx_byte_size = Some(approx_byte_size);
88 self
89 }
90
91 pub(crate) fn from_flatbuffer(
93 footer_bytes: &[u8],
94 layout_bytes: FlatBuffer,
95 dtype: DType,
96 statistics: Option<FileStatistics>,
97 metadata: Arc<[(String, SegmentSpec)]>,
98 session: &VortexSession,
99 ) -> VortexResult<Self> {
100 let metadata_bytes: usize = metadata
101 .iter()
102 .map(|(key, _segment)| key.len() + size_of::<SegmentSpec>())
103 .sum();
104 let approx_byte_size = footer_bytes.len() + layout_bytes.len() + metadata_bytes;
105 let fb_footer = root::<fb::Footer>(footer_bytes)?;
106
107 let layout_specs = fb_footer.layout_specs();
109 #[expect(clippy::disallowed_methods, reason = "interning a dynamic id")]
110 let layout_ids: Arc<[_]> = layout_specs
111 .iter()
112 .flat_map(|e| e.iter())
113 .map(|encoding| LayoutEncodingId::new(encoding.id()))
114 .collect();
115 let layout_read_ctx = ReadContext::new(layout_ids);
116
117 let array_specs = fb_footer.array_specs();
119 #[expect(clippy::disallowed_methods, reason = "interning a dynamic id")]
120 let array_ids: Arc<[_]> = array_specs
121 .iter()
122 .flat_map(|e| e.iter())
123 .map(|encoding| ArrayId::new(encoding.id()))
124 .collect();
125 let array_read_ctx = ReadContext::new(array_ids);
126
127 let root_layout = layout_from_flatbuffer_with_options(
128 layout_bytes,
129 &dtype,
130 &layout_read_ctx,
131 &array_read_ctx,
132 session,
133 session.allows_unknown(),
134 )?;
135
136 let segments: Arc<[SegmentSpec]> = fb_footer
137 .segment_specs()
138 .ok_or_else(|| vortex_err!("FileLayout missing segment specs"))?
139 .iter()
140 .map(SegmentSpec::try_from)
141 .try_collect()?;
142
143 if !segments.is_sorted_by_key(|segment| segment.offset) {
145 vortex_bail!("Segment offsets are not ordered");
146 }
147
148 Ok(Self {
149 root_layout,
150 segments,
151 statistics,
152 metadata,
153 array_read_ctx,
154 approx_byte_size: Some(approx_byte_size),
155 })
156 }
157
158 pub fn layout(&self) -> &LayoutRef {
160 &self.root_layout
161 }
162
163 pub fn segment_map(&self) -> &Arc<[SegmentSpec]> {
165 &self.segments
166 }
167
168 pub fn statistics(&self) -> Option<&FileStatistics> {
170 self.statistics.as_ref()
171 }
172
173 pub fn metadata_segments(&self) -> impl Iterator<Item = (&str, &SegmentSpec)> {
175 self.metadata
176 .iter()
177 .map(|(key, segment)| (key.as_str(), segment))
178 }
179
180 pub fn metadata_segment(&self, key: &str) -> Option<&SegmentSpec> {
182 self.metadata
183 .iter()
184 .find_map(|(candidate, segment)| (candidate == key).then_some(segment))
185 }
186
187 pub(crate) fn with_metadata_segments(mut self, metadata: Arc<[(String, SegmentSpec)]>) -> Self {
188 self.metadata = metadata;
189 self
190 }
191
192 pub(crate) fn segment_specs_with_metadata(&self) -> Arc<[SegmentSpec]> {
193 self.segments
194 .iter()
195 .copied()
196 .chain(self.metadata.iter().map(|(_, segment)| *segment))
197 .collect()
198 }
199
200 pub fn compressed_field_sizes(&self) -> VortexResult<CompressedFieldSizes> {
206 CompressedFieldSizes::try_new(&self.root_layout, &self.segments)
207 }
208
209 pub fn dtype(&self) -> &DType {
211 self.root_layout.dtype()
212 }
213
214 pub fn approx_byte_size(&self) -> Option<usize> {
216 self.approx_byte_size
217 }
218
219 pub fn row_count(&self) -> u64 {
221 self.root_layout.row_count()
222 }
223
224 pub(crate) fn validate_file_size(&self, file_size: u64) -> VortexResult<()> {
226 validate_segments_within_file(&self.segments, file_size)
227 }
228
229 pub fn into_serializer(self) -> FooterSerializer {
231 FooterSerializer::new(self)
232 }
233
234 pub fn deserializer(eof_buffer: ByteBuffer, session: VortexSession) -> FooterDeserializer {
236 FooterDeserializer::new(eof_buffer, session)
237 }
238}
239
240fn validate_segments_within_file(segments: &[SegmentSpec], file_size: u64) -> VortexResult<()> {
246 for segment in segments {
247 let within_file = segment
248 .offset
249 .checked_add(segment.length as u64)
250 .is_some_and(|end| end <= file_size);
251 if !within_file {
252 vortex_bail!(
253 "Segment at offset {} with length {} extends past the end of the \
254 {file_size}-byte file",
255 segment.offset,
256 segment.length,
257 );
258 }
259 }
260 Ok(())
261}
262
263#[cfg(test)]
264mod tests {
265 use vortex_buffer::Alignment;
266
267 use super::*;
268
269 fn segment(offset: u64, length: u32) -> SegmentSpec {
270 SegmentSpec {
271 offset,
272 length,
273 alignment: Alignment::none(),
274 }
275 }
276
277 #[test]
278 fn accepts_segments_within_file() -> VortexResult<()> {
279 validate_segments_within_file(&[segment(0, 100), segment(100, 50)], 150)?;
280 Ok(())
281 }
282
283 #[test]
284 fn rejects_segment_extending_past_end_of_file() {
285 let err =
286 validate_segments_within_file(&[segment(0, 100), segment(100, 51)], 150).unwrap_err();
287 assert!(err.to_string().contains("past the end"), "{err}");
288 }
289
290 #[test]
291 fn rejects_segment_offset_length_overflow() {
292 let err = validate_segments_within_file(&[segment(u64::MAX, 1)], u64::MAX).unwrap_err();
293 assert!(err.to_string().contains("past the end"), "{err}");
294 }
295}