1use crate::CompressionType;
6use arrow_buffer::Buffer;
7use arrow_schema::ArrowError;
8use flatbuffers::FlatBufferBuilder;
9
10const LENGTH_NO_COMPRESSED_DATA: i64 = -1;
11const LENGTH_OF_PREFIX_DATA: i64 = 8;
12const DEFAULT_ZSTD_COMPRESSION_LEVEL: i32 = 3;
13
14#[derive(Default)]
22pub struct IpcWriteContext {
23 scratch: Vec<u8>,
24 reserve_scratch: bool,
25 fbb: FlatBufferBuilder<'static>,
26 #[cfg(feature = "zstd")]
27 compressor: Option<zstd::bulk::Compressor<'static>>,
28 #[cfg(feature = "lz4")]
29 lz4_compressor: Option<Lz4Compressor>,
30}
31
32impl IpcWriteContext {
33 pub(crate) fn mut_fbb(&mut self) -> &mut FlatBufferBuilder<'static> {
35 &mut self.fbb
36 }
37
38 pub fn set_reserve_scratch(&mut self, reserve: bool) {
42 self.reserve_scratch = reserve;
43 }
44 pub(crate) fn reserve_scratch_with_capacity(&mut self, additional: usize) {
47 if self.reserve_scratch {
48 self.scratch.reserve(additional);
49 }
50 }
51 pub(crate) fn scratch(&mut self) -> Vec<u8> {
52 std::mem::take(&mut self.scratch)
53 }
54
55 #[cfg(feature = "zstd")]
56 fn zstd_compressor(&mut self, level: i32) -> &mut zstd::bulk::Compressor<'static> {
57 self.compressor.get_or_insert_with(|| {
58 zstd::bulk::Compressor::new(level).expect("can use default compression level")
59 })
60 }
61
62 #[cfg(feature = "lz4")]
63 fn lz4_compressor(&mut self) -> &mut Lz4Compressor {
64 self.lz4_compressor.get_or_insert_with(Lz4Compressor::new)
65 }
66}
67
68impl std::fmt::Debug for IpcWriteContext {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 let mut ds = f.debug_struct("IpcWriteContext");
71
72 #[cfg(feature = "zstd")]
73 ds.field(
74 "compressor",
75 &self.compressor.as_ref().map(|_| "zstd::bulk::Compressor"),
76 );
77
78 #[cfg(feature = "lz4")]
79 ds.field(
80 "lz4_compressor",
81 &self.lz4_compressor.as_ref().map(|_| "Lz4Compressor"),
82 );
83
84 ds.finish()
85 }
86}
87
88#[deprecated(since = "59.1.0", note = "Use IpcWriteContext instead")]
90pub type CompressionContext = IpcWriteContext;
91
92pub struct DecompressionContext {
98 #[cfg(feature = "zstd")]
99 decompressor: Option<zstd::bulk::Decompressor<'static>>,
100}
101
102impl DecompressionContext {
103 pub(crate) fn new() -> Self {
104 Default::default()
105 }
106
107 #[cfg(feature = "zstd")]
108 fn zstd_decompressor(&mut self) -> &mut zstd::bulk::Decompressor<'static> {
109 self.decompressor.get_or_insert_with(|| {
110 zstd::bulk::Decompressor::new().expect("can create zstd decompressor")
111 })
112 }
113}
114
115#[expect(clippy::derivable_impls)]
116impl Default for DecompressionContext {
117 fn default() -> Self {
118 DecompressionContext {
119 #[cfg(feature = "zstd")]
120 decompressor: None,
121 }
122 }
123}
124
125impl std::fmt::Debug for DecompressionContext {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 let mut ds = f.debug_struct("DecompressionContext");
128
129 #[cfg(feature = "zstd")]
130 ds.field(
131 "decompressor",
132 &self
133 .decompressor
134 .as_ref()
135 .map(|_| "zstd::bulk::Decompressor"),
136 );
137
138 ds.finish()
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub enum CompressionCodec {
145 Lz4Frame,
146 Zstd(i32),
147}
148
149impl TryFrom<CompressionType> for CompressionCodec {
150 type Error = ArrowError;
151
152 fn try_from(compression_type: CompressionType) -> Result<Self, ArrowError> {
153 match compression_type {
154 CompressionType::ZSTD => Ok(CompressionCodec::Zstd(DEFAULT_ZSTD_COMPRESSION_LEVEL)),
155 CompressionType::LZ4_FRAME => Ok(CompressionCodec::Lz4Frame),
156 other_type => Err(ArrowError::NotYetImplemented(format!(
157 "compression type {other_type:?} not supported "
158 ))),
159 }
160 }
161}
162
163impl CompressionCodec {
164 pub(crate) fn try_new_with_compression_level(
171 compression_type: CompressionType,
172 compression_level: i32,
173 ) -> Result<Self, ArrowError> {
174 match compression_type {
175 CompressionType::ZSTD => Ok(CompressionCodec::Zstd(compression_level)),
176 CompressionType::LZ4_FRAME => Ok(CompressionCodec::Lz4Frame),
177 other_type => Err(ArrowError::NotYetImplemented(format!(
178 "compression type {other_type:?} not supported "
179 ))),
180 }
181 }
182
183 pub(crate) fn compress_to_vec(
194 &self,
195 input: &[u8],
196 output: &mut Vec<u8>,
197 context: &mut IpcWriteContext,
198 ) -> Result<usize, ArrowError> {
199 let uncompressed_data_len = input.len();
200 let original_output_len = output.len();
201
202 if input.is_empty() {
203 } else {
205 output.extend_from_slice(&(uncompressed_data_len as i64).to_le_bytes());
207 self.compress(input, output, context)?;
208
209 let compression_len = output.len() - original_output_len;
210 if compression_len > uncompressed_data_len {
211 output.truncate(original_output_len);
216 output.extend_from_slice(&LENGTH_NO_COMPRESSED_DATA.to_le_bytes());
217 output.extend_from_slice(input);
218 }
219 }
220 Ok(output.len() - original_output_len)
221 }
222
223 pub(crate) fn decompress_to_buffer(
231 &self,
232 input: &Buffer,
233 context: &mut DecompressionContext,
234 ) -> Result<Buffer, ArrowError> {
235 let decompressed_length = read_uncompressed_size(input)?;
238 let buffer = if decompressed_length == 0 {
239 Buffer::from([])
241 } else if decompressed_length == LENGTH_NO_COMPRESSED_DATA {
242 input.slice(LENGTH_OF_PREFIX_DATA as usize)
244 } else if let Ok(decompressed_length) = usize::try_from(decompressed_length) {
245 let input_data = &input[(LENGTH_OF_PREFIX_DATA as usize)..];
247 let v = self.decompress(input_data, decompressed_length, context)?;
248 Buffer::from_vec(v)
249 } else {
250 return Err(ArrowError::IpcError(format!(
251 "Invalid uncompressed length: {decompressed_length}"
252 )));
253 };
254 Ok(buffer)
255 }
256
257 fn compress(
260 &self,
261 input: &[u8],
262 output: &mut Vec<u8>,
263 context: &mut IpcWriteContext,
264 ) -> Result<(), ArrowError> {
265 match self {
266 CompressionCodec::Lz4Frame => compress_lz4(input, output, context),
267 CompressionCodec::Zstd(level) => compress_zstd(input, output, context, *level),
268 }
269 }
270
271 fn decompress(
274 &self,
275 input: &[u8],
276 decompressed_size: usize,
277 context: &mut DecompressionContext,
278 ) -> Result<Vec<u8>, ArrowError> {
279 let ret = match self {
280 CompressionCodec::Lz4Frame => decompress_lz4(input, decompressed_size)?,
281 CompressionCodec::Zstd(_) => decompress_zstd(input, decompressed_size, context)?,
282 };
283 if ret.len() != decompressed_size {
284 return Err(ArrowError::IpcError(format!(
285 "Expected decompressed length of {decompressed_size} got {}",
286 ret.len()
287 )));
288 }
289 Ok(ret)
290 }
291}
292
293#[cfg(feature = "lz4")]
298struct Lz4Compressor {
299 encoder: lz4_flex::frame::FrameEncoder<Vec<u8>>,
300}
301
302#[cfg(feature = "lz4")]
303impl Lz4Compressor {
304 fn new() -> Self {
305 let frame_info =
309 lz4_flex::frame::FrameInfo::new().block_size(lz4_flex::frame::BlockSize::Max256KB);
310 Self {
311 encoder: lz4_flex::frame::FrameEncoder::with_frame_info(frame_info, Vec::new()),
312 }
313 }
314
315 fn compress_to_buffer(&mut self, input: &[u8], output: &mut Vec<u8>) -> Result<(), ArrowError> {
317 use std::io::Write;
318 debug_assert!(!input.is_empty());
320 std::mem::swap(self.encoder.get_mut(), output);
322 let result = self
323 .encoder
324 .write_all(input)
325 .map_err(ArrowError::from)
326 .and_then(|()| {
327 self.encoder
328 .try_finish()
329 .map_err(|e| ArrowError::ExternalError(Box::new(e)))
330 });
331 std::mem::swap(self.encoder.get_mut(), output);
332 if result.is_err() {
333 *self = Self::new();
335 }
336 result
337 }
338}
339
340#[cfg(feature = "lz4")]
341fn compress_lz4(
342 input: &[u8],
343 output: &mut Vec<u8>,
344 context: &mut IpcWriteContext,
345) -> Result<(), ArrowError> {
346 context.lz4_compressor().compress_to_buffer(input, output)
347}
348
349#[cfg(not(feature = "lz4"))]
350fn compress_lz4(
351 _input: &[u8],
352 _output: &mut Vec<u8>,
353 _context: &mut IpcWriteContext,
354) -> Result<(), ArrowError> {
355 Err(ArrowError::InvalidArgumentError(
356 "lz4 IPC compression requires the lz4 feature".to_string(),
357 ))
358}
359
360#[cfg(feature = "lz4")]
361fn decompress_lz4(input: &[u8], decompressed_size: usize) -> Result<Vec<u8>, ArrowError> {
362 use std::io::Read;
363 let mut output = Vec::with_capacity(decompressed_size.min(crate::reader::MAX_PREALLOC_BYTES));
364 let mut decoder = lz4_flex::frame::FrameDecoder::new(input);
365 decoder
366 .by_ref()
367 .take(decompressed_size as u64)
368 .read_to_end(&mut output)?;
369
370 if decoder.read(&mut [0])? != 0 {
372 return Err(ArrowError::IpcError(format!(
373 "LZ4 decompressed buffer exceeds advertised size of {decompressed_size}"
374 )));
375 }
376 Ok(output)
377}
378
379#[cfg(not(feature = "lz4"))]
380fn decompress_lz4(_input: &[u8], _decompressed_size: usize) -> Result<Vec<u8>, ArrowError> {
381 Err(ArrowError::InvalidArgumentError(
382 "lz4 IPC decompression requires the lz4 feature".to_string(),
383 ))
384}
385
386#[cfg(feature = "zstd")]
387fn compress_zstd(
388 input: &[u8],
389 output: &mut Vec<u8>,
390 context: &mut IpcWriteContext,
391 level: i32,
392) -> Result<(), ArrowError> {
393 let start = output.len();
394 output.reserve(zstd::zstd_safe::compress_bound(input.len()));
395
396 let mut cursor = std::io::Cursor::new(output);
397 cursor.set_position(start as u64);
398 context
399 .zstd_compressor(level)
400 .compress_to_buffer(input, &mut cursor)?;
401
402 Ok(())
403}
404
405#[cfg(not(feature = "zstd"))]
406fn compress_zstd(
407 _input: &[u8],
408 _output: &mut Vec<u8>,
409 _context: &mut IpcWriteContext,
410 _level: i32,
411) -> Result<(), ArrowError> {
412 Err(ArrowError::InvalidArgumentError(
413 "zstd IPC compression requires the zstd feature".to_string(),
414 ))
415}
416
417#[cfg(feature = "zstd")]
418fn decompress_zstd(
419 input: &[u8],
420 decompressed_size: usize,
421 context: &mut DecompressionContext,
422) -> Result<Vec<u8>, ArrowError> {
423 if decompressed_size > crate::reader::MAX_PREALLOC_BYTES {
424 use std::io::Read;
426 let mut output =
427 Vec::with_capacity(decompressed_size.min(crate::reader::MAX_PREALLOC_BYTES));
428 let mut decoder = zstd::stream::read::Decoder::with_buffer(input)?;
429 decoder
430 .by_ref()
431 .take(decompressed_size as u64)
432 .read_to_end(&mut output)?;
433
434 if decoder.read(&mut [0])? != 0 {
436 return Err(ArrowError::IpcError(format!(
437 "ZSTD decompressed buffer exceeds advertised size of {decompressed_size}"
438 )));
439 }
440 return Ok(output);
441 }
442 let output = context
443 .zstd_decompressor()
444 .decompress(input, decompressed_size)?;
445 Ok(output)
446}
447
448#[cfg(not(feature = "zstd"))]
449fn decompress_zstd(
450 _input: &[u8],
451 _decompressed_size: usize,
452 _context: &mut DecompressionContext,
453) -> Result<Vec<u8>, ArrowError> {
454 Err(ArrowError::InvalidArgumentError(
455 "zstd IPC decompression requires the zstd feature".to_string(),
456 ))
457}
458
459#[inline]
466fn read_uncompressed_size(buffer: &[u8]) -> Result<i64, ArrowError> {
467 let len_buffer = buffer.get(..LENGTH_OF_PREFIX_DATA as usize).ok_or_else(|| {
468 ArrowError::IpcError(format!(
469 "Compressed IPC buffer is too short: expected at least {LENGTH_OF_PREFIX_DATA} bytes, got {}",
470 buffer.len()
471 ))
472 })?;
473 Ok(i64::from_le_bytes(len_buffer.try_into().unwrap()))
474}
475
476#[cfg(test)]
477mod tests {
478 #[test]
479 #[cfg(feature = "lz4")]
480 fn test_lz4_compression() {
481 let input_bytes = b"hello lz4";
482 let codec = super::CompressionCodec::Lz4Frame;
483 let mut output_bytes: Vec<u8> = Vec::new();
484 codec
485 .compress(input_bytes, &mut output_bytes, &mut Default::default())
486 .unwrap();
487 let result = codec
488 .decompress(
489 output_bytes.as_slice(),
490 input_bytes.len(),
491 &mut Default::default(),
492 )
493 .unwrap();
494 assert_eq!(input_bytes, result.as_slice());
495 }
496
497 #[test]
498 #[cfg(feature = "lz4")]
499 fn test_lz4_decompression_rejects_output_exceeding_advertised_size() {
500 let input_bytes = b"hello lz4";
501 let codec = super::CompressionCodec::Lz4Frame;
502 let mut compressed = Vec::new();
503 codec
504 .compress(input_bytes, &mut compressed, &mut Default::default())
505 .unwrap();
506
507 let err = codec
508 .decompress(&compressed, input_bytes.len() - 1, &mut Default::default())
509 .expect_err("output larger than the advertised size should fail");
510
511 assert!(
512 err.to_string().contains("exceeds advertised size"),
513 "unexpected error: {err}"
514 );
515 }
516
517 #[test]
518 #[cfg(feature = "lz4")]
519 fn test_lz4_compression_multiple_frames() {
520 let codec = super::CompressionCodec::Lz4Frame;
521 let mut context = super::IpcWriteContext::default();
522 let large: Vec<u8> = (0..200_000u32)
524 .flat_map(|i| (i % 1000).to_le_bytes())
525 .collect();
526 let small = b"hello lz4".to_vec();
527 for input in [&large, &small, &large] {
528 let mut output = b"prefix".to_vec();
529 codec.compress(input, &mut output, &mut context).unwrap();
530 assert_eq!(&output[..6], b"prefix");
531 let result = codec
532 .decompress(&output[6..], input.len(), &mut Default::default())
533 .unwrap();
534 assert_eq!(input, &result);
535 }
536 }
537
538 #[test]
539 #[cfg(feature = "zstd")]
540 fn test_zstd_compression() {
541 let input_bytes = b"hello zstd";
542 let codec = super::CompressionCodec::Zstd(super::DEFAULT_ZSTD_COMPRESSION_LEVEL);
543 let mut output_bytes: Vec<u8> = Vec::new();
544 codec
545 .compress(input_bytes, &mut output_bytes, &mut Default::default())
546 .unwrap();
547 let result = codec
548 .decompress(
549 output_bytes.as_slice(),
550 input_bytes.len(),
551 &mut Default::default(),
552 )
553 .unwrap();
554 assert_eq!(input_bytes, result.as_slice());
555 }
556
557 #[test]
559 #[cfg(any(feature = "lz4", feature = "zstd"))]
560 fn test_decompression_beyond_prealloc() {
561 let input = vec![7u8; crate::reader::MAX_PREALLOC_BYTES + 2];
562 for codec in [
563 #[cfg(feature = "lz4")]
564 super::CompressionCodec::Lz4Frame,
565 #[cfg(feature = "zstd")]
566 super::CompressionCodec::Zstd(super::DEFAULT_ZSTD_COMPRESSION_LEVEL),
567 ] {
568 let mut compressed = Vec::new();
569 codec
570 .compress(&input, &mut compressed, &mut Default::default())
571 .unwrap();
572 let decompress = |len| codec.decompress(&compressed, len, &mut Default::default());
573 assert_eq!(decompress(input.len()).unwrap(), input, "{codec:?}");
574 assert!(decompress(input.len() - 1).is_err(), "{codec:?}");
575 assert!(decompress(usize::MAX).is_err(), "{codec:?}");
576 }
577 }
578
579 #[test]
580 fn test_read_uncompressed_size_rejects_short_prefix() {
581 let err = super::read_uncompressed_size(&[1, 2, 3, 4, 5, 6, 7])
582 .expect_err("short compressed IPC prefix should return an error");
583
584 assert!(
585 err.to_string()
586 .contains("Compressed IPC buffer is too short"),
587 "unexpected error: {err}"
588 );
589 }
590
591 #[test]
592 #[cfg(feature = "lz4")]
593 fn test_compress_to_vec_writes_8_byte_length_prefix() {
594 let input_bytes = vec![42u8; 132];
597 let codec = super::CompressionCodec::Lz4Frame;
598 let mut output_bytes: Vec<u8> = Vec::new();
599 codec
600 .compress_to_vec(&input_bytes, &mut output_bytes, &mut Default::default())
601 .unwrap();
602
603 let prefix: [u8; 8] = output_bytes[..8].try_into().unwrap();
604 assert_eq!(i64::from_le_bytes(prefix), input_bytes.len() as i64);
605 }
606}