1use std::io::Write;
21
22use arrow_array::RecordBatch;
23use bytes::Bytes;
24use prost::Message;
25
26use crate::{Error, Result};
27
28pub struct CacheEntryWriter<'a> {
35 writer: &'a mut dyn Write,
36 pos: usize,
38}
39
40impl<'a> CacheEntryWriter<'a> {
41 pub fn new(writer: &'a mut dyn Write) -> Self {
46 Self { writer, pos: 0 }
47 }
48
49 pub(crate) fn with_pos(writer: &'a mut dyn Write, pos: usize) -> Self {
52 Self { writer, pos }
53 }
54
55 pub fn write_u8(&mut self, value: u8) -> Result<()> {
57 self.writer.write_all(&[value])?;
58 self.pos += 1;
59 Ok(())
60 }
61
62 pub fn write_header<P: Message>(&mut self, header: &P) -> Result<()> {
64 let bytes = header.encode_to_vec();
65 let len = u32::try_from(bytes.len())
66 .map_err(|_| Error::io(format!("cache header too large: {} bytes", bytes.len())))?;
67 self.writer.write_all(&len.to_le_bytes())?;
68 self.writer.write_all(&bytes)?;
69 self.pos += 4 + bytes.len();
70 Ok(())
71 }
72
73 pub fn write_ipc(&mut self, batch: &RecordBatch) -> Result<()> {
75 lance_arrow::ipc::write_ipc_section(self.writer, &mut self.pos, batch)
76 .map_err(|e| Error::io(e.to_string()))
77 }
78
79 pub fn write_ipc_batches<I>(&mut self, batches: I) -> Result<()>
82 where
83 I: IntoIterator<Item = RecordBatch>,
84 {
85 lance_arrow::ipc::write_ipc_section_batches(self.writer, &mut self.pos, batches)
86 .map_err(|e| Error::io(e.to_string()))
87 }
88
89 pub fn write_raw(&mut self, bytes: &[u8]) -> Result<()> {
94 lance_arrow::ipc::write_len_prefixed_bytes(self.writer, bytes)
95 .map_err(|e| Error::io(e.to_string()))?;
96 self.pos += 8 + bytes.len();
97 Ok(())
98 }
99
100 pub fn raw_writer(&mut self) -> &mut dyn Write {
113 self.writer
114 }
115}
116
117pub struct CacheEntryReader<'a> {
123 data: &'a Bytes,
124 offset: usize,
125 version: u32,
126}
127
128impl<'a> CacheEntryReader<'a> {
129 pub fn new(data: &'a Bytes, offset: usize, version: u32) -> Self {
132 Self {
133 data,
134 offset,
135 version,
136 }
137 }
138
139 pub fn version(&self) -> u32 {
141 self.version
142 }
143
144 pub fn read_u8(&mut self) -> Result<u8> {
146 let bytes = self.data.as_ref();
147 let v = *bytes
148 .get(self.offset)
149 .ok_or_else(|| Error::io("cache entry: truncated, missing tag byte".to_string()))?;
150 self.offset += 1;
151 Ok(v)
152 }
153
154 pub fn read_header<P: Message + Default>(&mut self) -> Result<P> {
156 let bytes = self.data.as_ref();
157 let len_end = self
158 .offset
159 .checked_add(4)
160 .filter(|&e| e <= bytes.len())
161 .ok_or_else(|| Error::io("cache header: truncated length prefix".to_string()))?;
162 let len = u32::from_le_bytes(bytes[self.offset..len_end].try_into().unwrap()) as usize;
163 let data_end = len_end
164 .checked_add(len)
165 .filter(|&e| e <= bytes.len())
166 .ok_or_else(|| Error::io("cache header: truncated body".to_string()))?;
167 let msg = P::decode(&bytes[len_end..data_end])
168 .map_err(|e| Error::io(format!("cache header decode failed: {e}")))?;
169 self.offset = data_end;
170 Ok(msg)
171 }
172
173 pub fn read_ipc(&mut self) -> Result<RecordBatch> {
175 lance_arrow::ipc::read_ipc_section_at(self.data, &mut self.offset)
176 .map_err(|e| Error::io(e.to_string()))
177 }
178
179 pub fn read_ipc_batches(&mut self) -> Result<Vec<RecordBatch>> {
182 lance_arrow::ipc::read_ipc_section_batches_at(self.data, &mut self.offset)
183 .map_err(|e| Error::io(e.to_string()))
184 }
185
186 pub fn read_raw(&mut self) -> Result<Bytes> {
188 lance_arrow::ipc::read_len_prefixed_bytes_at(self.data, &mut self.offset)
189 .map_err(|e| Error::io(e.to_string()))
190 }
191
192 pub fn body(&self) -> Bytes {
200 self.data.slice(self.offset..)
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 use std::sync::Arc;
209
210 use arrow_array::{Int32Array, UInt64Array};
211 use arrow_schema::{DataType, Field, Schema};
212 use lance_arrow::ipc::IPC_SECTION_ALIGNMENT;
213
214 fn write_body(pos: usize, f: impl FnOnce(&mut CacheEntryWriter<'_>)) -> Bytes {
219 let mut buf = Vec::new();
220 let mut writer = CacheEntryWriter::with_pos(&mut buf, pos);
221 f(&mut writer);
222 Bytes::from(buf)
223 }
224
225 fn int_batch(values: Vec<i32>) -> RecordBatch {
226 let schema = Schema::new(vec![Field::new("i", DataType::Int32, false)]);
227 RecordBatch::try_new(schema.into(), vec![Arc::new(Int32Array::from(values))]).unwrap()
228 }
229
230 #[test]
231 fn test_u8_roundtrip_and_truncation() {
232 let data = write_body(0, |w| {
233 w.write_u8(7).unwrap();
234 w.write_u8(255).unwrap();
235 });
236 assert_eq!(data.as_ref(), &[7, 255]);
237
238 let mut reader = CacheEntryReader::new(&data, 0, 1);
239 assert_eq!(reader.read_u8().unwrap(), 7);
240 assert_eq!(reader.read_u8().unwrap(), 255);
241
242 let message = reader.read_u8().unwrap_err().to_string();
244 assert!(message.contains("missing tag byte"), "{message}");
245 }
246
247 #[test]
250 fn test_header_roundtrip_is_length_prefixed() {
251 let data = write_body(0, |w| w.write_header(&1234u64).unwrap());
252
253 let encoded_len = 1234u64.encoded_len();
254 assert_eq!(
255 u32::from_le_bytes(data[..4].try_into().unwrap()) as usize,
256 encoded_len
257 );
258 assert_eq!(data.len(), 4 + encoded_len);
259
260 let mut reader = CacheEntryReader::new(&data, 0, 1);
261 assert_eq!(reader.read_header::<u64>().unwrap(), 1234);
262 assert!(reader.body().is_empty());
264 }
265
266 #[test]
267 fn test_read_header_rejects_truncated_length_prefix() {
268 let data = Bytes::from_static(&[0, 0]);
269 let message = CacheEntryReader::new(&data, 0, 1)
270 .read_header::<u64>()
271 .unwrap_err()
272 .to_string();
273 assert!(message.contains("truncated length prefix"), "{message}");
274 }
275
276 #[test]
277 fn test_read_header_rejects_truncated_body() {
278 let mut data = 16u32.to_le_bytes().to_vec();
280 data.extend_from_slice(&[1, 2, 3]);
281 let data = Bytes::from(data);
282
283 let message = CacheEntryReader::new(&data, 0, 1)
284 .read_header::<u64>()
285 .unwrap_err()
286 .to_string();
287 assert!(message.contains("truncated body"), "{message}");
288 }
289
290 #[test]
293 fn test_read_header_rejects_undecodable_payload() {
294 let payload = [0x08u8, 0xFF, 0xFF, 0xFF];
296 let mut data = (payload.len() as u32).to_le_bytes().to_vec();
297 data.extend_from_slice(&payload);
298 let data = Bytes::from(data);
299
300 let message = CacheEntryReader::new(&data, 0, 1)
301 .read_header::<u64>()
302 .unwrap_err()
303 .to_string();
304 assert!(message.contains("decode failed"), "{message}");
305 }
306
307 #[test]
308 fn test_raw_roundtrip_leaves_the_rest_as_body() {
309 let data = write_body(0, |w| {
310 w.write_raw(&[1, 2, 3]).unwrap();
311 w.raw_writer().write_all(&[9, 9]).unwrap();
312 });
313 assert_eq!(data.len(), 13);
315
316 let mut reader = CacheEntryReader::new(&data, 0, 1);
317 assert_eq!(reader.read_raw().unwrap().as_ref(), &[1, 2, 3]);
318 assert_eq!(reader.body().as_ref(), &[9, 9]);
319 }
320
321 #[test]
322 fn test_reader_exposes_the_entry_version() {
323 let data = Bytes::from_static(&[0]);
324 assert_eq!(CacheEntryReader::new(&data, 0, 7).version(), 7);
325 }
326
327 #[test]
332 fn test_ipc_section_is_aligned_against_the_envelope() {
333 const ENVELOPE: usize = 13;
334 let batch = int_batch(vec![1, 2, 3]);
335
336 let mut buf = vec![0xAAu8; ENVELOPE];
337 let mut writer = CacheEntryWriter::with_pos(&mut buf, ENVELOPE);
338 writer.write_header(&1234u64).unwrap();
339 writer.write_ipc(&batch).unwrap();
340 let data = Bytes::from(buf);
341
342 let header_end = ENVELOPE + 4 + 1234u64.encoded_len();
343 let stream_start = header_end.next_multiple_of(IPC_SECTION_ALIGNMENT);
344 assert!(stream_start > header_end, "padding should be non-empty");
345 assert!(
346 data[header_end..stream_start].iter().all(|b| *b == 0),
347 "the gap must be zero padding"
348 );
349
350 let mut reader = CacheEntryReader::new(&data, ENVELOPE, 1);
351 assert_eq!(reader.read_header::<u64>().unwrap(), 1234);
352 assert_eq!(reader.read_ipc().unwrap(), batch);
353 assert!(reader.body().is_empty());
354 }
355
356 #[test]
357 fn test_ipc_batches_roundtrip() {
358 let batches = vec![int_batch(vec![1, 2]), int_batch(vec![3])];
359 let data = write_body(0, |w| w.write_ipc_batches(batches.clone()).unwrap());
360
361 let mut reader = CacheEntryReader::new(&data, 0, 1);
362 assert_eq!(reader.read_ipc_batches().unwrap(), batches);
363 }
364
365 #[test]
370 fn test_mixed_sections_stay_in_sync() {
371 let schema = Schema::new(vec![Field::new("u", DataType::UInt64, false)]);
372 let batch = RecordBatch::try_new(
373 schema.into(),
374 vec![Arc::new(UInt64Array::from(vec![u64::MAX, 0]))],
375 )
376 .unwrap();
377
378 let data = write_body(0, |w| {
379 w.write_u8(2).unwrap();
380 w.write_header(&99u64).unwrap();
381 w.write_ipc(&batch).unwrap();
382 w.write_raw(&[7, 7, 7]).unwrap();
383 });
384
385 let mut reader = CacheEntryReader::new(&data, 0, 1);
386 assert_eq!(reader.read_u8().unwrap(), 2);
387 assert_eq!(reader.read_header::<u64>().unwrap(), 99);
388 assert_eq!(reader.read_ipc().unwrap(), batch);
389 assert_eq!(reader.read_raw().unwrap().as_ref(), &[7, 7, 7]);
390 assert!(reader.body().is_empty());
391 }
392}