1use crate::CompressionType;
19use arrow_buffer::Buffer;
20use arrow_schema::ArrowError;
21use flatbuffers::FlatBufferBuilder;
22
23const LENGTH_NO_COMPRESSED_DATA: i64 = -1;
24const LENGTH_OF_PREFIX_DATA: i64 = 8;
25const DEFAULT_ZSTD_COMPRESSION_LEVEL: i32 = 3;
26
27#[derive(Default)]
33pub struct IpcWriteContext {
34 scratch: Vec<u8>,
35 reserve_scratch: bool,
36 fbb: FlatBufferBuilder<'static>,
37 #[cfg(feature = "zstd")]
38 compressor: Option<zstd::bulk::Compressor<'static>>,
39}
40
41impl IpcWriteContext {
42 pub(crate) fn mut_fbb(&mut self) -> &mut FlatBufferBuilder<'static> {
44 &mut self.fbb
45 }
46
47 pub fn set_reserve_scratch(&mut self, reserve: bool) {
51 self.reserve_scratch = reserve;
52 }
53 pub(crate) fn reserve_scratch_with_capacity(&mut self, additional: usize) {
56 if self.reserve_scratch {
57 self.scratch.reserve(additional);
58 }
59 }
60 pub(crate) fn scratch(&mut self) -> Vec<u8> {
61 std::mem::take(&mut self.scratch)
62 }
63
64 #[cfg(feature = "zstd")]
65 fn zstd_compressor(&mut self, level: i32) -> &mut zstd::bulk::Compressor<'static> {
66 self.compressor.get_or_insert_with(|| {
67 zstd::bulk::Compressor::new(level).expect("can use default compression level")
68 })
69 }
70}
71
72impl std::fmt::Debug for IpcWriteContext {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 let mut ds = f.debug_struct("IpcWriteContext");
75
76 #[cfg(feature = "zstd")]
77 ds.field(
78 "compressor",
79 &self.compressor.as_ref().map(|_| "zstd::bulk::Compressor"),
80 );
81
82 ds.finish()
83 }
84}
85
86#[deprecated(since = "59.1.0", note = "Use IpcWriteContext instead")]
88pub type CompressionContext = IpcWriteContext;
89
90pub struct DecompressionContext {
96 #[cfg(feature = "zstd")]
97 decompressor: Option<zstd::bulk::Decompressor<'static>>,
98}
99
100impl DecompressionContext {
101 pub(crate) fn new() -> Self {
102 Default::default()
103 }
104
105 #[cfg(feature = "zstd")]
106 fn zstd_decompressor(&mut self) -> &mut zstd::bulk::Decompressor<'static> {
107 self.decompressor.get_or_insert_with(|| {
108 zstd::bulk::Decompressor::new().expect("can create zstd decompressor")
109 })
110 }
111}
112
113#[expect(clippy::derivable_impls)]
114impl Default for DecompressionContext {
115 fn default() -> Self {
116 DecompressionContext {
117 #[cfg(feature = "zstd")]
118 decompressor: None,
119 }
120 }
121}
122
123impl std::fmt::Debug for DecompressionContext {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 let mut ds = f.debug_struct("DecompressionContext");
126
127 #[cfg(feature = "zstd")]
128 ds.field(
129 "decompressor",
130 &self
131 .decompressor
132 .as_ref()
133 .map(|_| "zstd::bulk::Decompressor"),
134 );
135
136 ds.finish()
137 }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub enum CompressionCodec {
143 Lz4Frame,
144 Zstd(i32),
145}
146
147impl TryFrom<CompressionType> for CompressionCodec {
148 type Error = ArrowError;
149
150 fn try_from(compression_type: CompressionType) -> Result<Self, ArrowError> {
151 match compression_type {
152 CompressionType::ZSTD => Ok(CompressionCodec::Zstd(DEFAULT_ZSTD_COMPRESSION_LEVEL)),
153 CompressionType::LZ4_FRAME => Ok(CompressionCodec::Lz4Frame),
154 other_type => Err(ArrowError::NotYetImplemented(format!(
155 "compression type {other_type:?} not supported "
156 ))),
157 }
158 }
159}
160
161impl CompressionCodec {
162 pub(crate) fn try_new_with_compression_level(
169 compression_type: CompressionType,
170 compression_level: i32,
171 ) -> Result<Self, ArrowError> {
172 match compression_type {
173 CompressionType::ZSTD => Ok(CompressionCodec::Zstd(compression_level)),
174 CompressionType::LZ4_FRAME => Ok(CompressionCodec::Lz4Frame),
175 other_type => Err(ArrowError::NotYetImplemented(format!(
176 "compression type {other_type:?} not supported "
177 ))),
178 }
179 }
180
181 pub(crate) fn compress_to_vec(
192 &self,
193 input: &[u8],
194 output: &mut Vec<u8>,
195 context: &mut IpcWriteContext,
196 ) -> Result<usize, ArrowError> {
197 let uncompressed_data_len = input.len();
198 let original_output_len = output.len();
199
200 if input.is_empty() {
201 } else {
203 output.extend_from_slice(&(uncompressed_data_len as i64).to_le_bytes());
205 self.compress(input, output, context)?;
206
207 let compression_len = output.len() - original_output_len;
208 if compression_len > uncompressed_data_len {
209 output.truncate(original_output_len);
214 output.extend_from_slice(&LENGTH_NO_COMPRESSED_DATA.to_le_bytes());
215 output.extend_from_slice(input);
216 }
217 }
218 Ok(output.len() - original_output_len)
219 }
220
221 pub(crate) fn decompress_to_buffer(
229 &self,
230 input: &Buffer,
231 context: &mut DecompressionContext,
232 ) -> Result<Buffer, ArrowError> {
233 let decompressed_length = read_uncompressed_size(input)?;
236 let buffer = if decompressed_length == 0 {
237 Buffer::from([])
239 } else if decompressed_length == LENGTH_NO_COMPRESSED_DATA {
240 input.slice(LENGTH_OF_PREFIX_DATA as usize)
242 } else if let Ok(decompressed_length) = usize::try_from(decompressed_length) {
243 let input_data = &input[(LENGTH_OF_PREFIX_DATA as usize)..];
245 let v = self.decompress(input_data, decompressed_length, context)?;
246 Buffer::from_vec(v)
247 } else {
248 return Err(ArrowError::IpcError(format!(
249 "Invalid uncompressed length: {decompressed_length}"
250 )));
251 };
252 Ok(buffer)
253 }
254
255 fn compress(
258 &self,
259 input: &[u8],
260 output: &mut Vec<u8>,
261 context: &mut IpcWriteContext,
262 ) -> Result<(), ArrowError> {
263 match self {
264 CompressionCodec::Lz4Frame => compress_lz4(input, output),
265 CompressionCodec::Zstd(level) => compress_zstd(input, output, context, *level),
266 }
267 }
268
269 fn decompress(
272 &self,
273 input: &[u8],
274 decompressed_size: usize,
275 context: &mut DecompressionContext,
276 ) -> Result<Vec<u8>, ArrowError> {
277 let ret = match self {
278 CompressionCodec::Lz4Frame => decompress_lz4(input, decompressed_size)?,
279 CompressionCodec::Zstd(_) => decompress_zstd(input, decompressed_size, context)?,
280 };
281 if ret.len() != decompressed_size {
282 return Err(ArrowError::IpcError(format!(
283 "Expected compressed length of {decompressed_size} got {}",
284 ret.len()
285 )));
286 }
287 Ok(ret)
288 }
289}
290
291#[cfg(feature = "lz4")]
292fn compress_lz4(input: &[u8], output: &mut Vec<u8>) -> Result<(), ArrowError> {
293 use std::io::Write;
294 let mut encoder = lz4_flex::frame::FrameEncoder::new(output);
295 encoder.write_all(input)?;
296 encoder
297 .finish()
298 .map_err(|e| ArrowError::ExternalError(Box::new(e)))?;
299 Ok(())
300}
301
302#[cfg(not(feature = "lz4"))]
303fn compress_lz4(_input: &[u8], _output: &mut Vec<u8>) -> Result<(), ArrowError> {
304 Err(ArrowError::InvalidArgumentError(
305 "lz4 IPC compression requires the lz4 feature".to_string(),
306 ))
307}
308
309#[cfg(feature = "lz4")]
310fn decompress_lz4(input: &[u8], decompressed_size: usize) -> Result<Vec<u8>, ArrowError> {
311 use std::io::Read;
312 let mut output = Vec::with_capacity(decompressed_size);
313 lz4_flex::frame::FrameDecoder::new(input).read_to_end(&mut output)?;
314 Ok(output)
315}
316
317#[cfg(not(feature = "lz4"))]
318fn decompress_lz4(_input: &[u8], _decompressed_size: usize) -> Result<Vec<u8>, ArrowError> {
319 Err(ArrowError::InvalidArgumentError(
320 "lz4 IPC decompression requires the lz4 feature".to_string(),
321 ))
322}
323
324#[cfg(feature = "zstd")]
325fn compress_zstd(
326 input: &[u8],
327 output: &mut Vec<u8>,
328 context: &mut IpcWriteContext,
329 level: i32,
330) -> Result<(), ArrowError> {
331 let start = output.len();
332 output.reserve(zstd::zstd_safe::compress_bound(input.len()));
333
334 let mut cursor = std::io::Cursor::new(output);
335 cursor.set_position(start as u64);
336 context
337 .zstd_compressor(level)
338 .compress_to_buffer(input, &mut cursor)?;
339
340 Ok(())
341}
342
343#[cfg(not(feature = "zstd"))]
344fn compress_zstd(
345 _input: &[u8],
346 _output: &mut Vec<u8>,
347 _context: &mut IpcWriteContext,
348 _level: i32,
349) -> Result<(), ArrowError> {
350 Err(ArrowError::InvalidArgumentError(
351 "zstd IPC compression requires the zstd feature".to_string(),
352 ))
353}
354
355#[cfg(feature = "zstd")]
356fn decompress_zstd(
357 input: &[u8],
358 decompressed_size: usize,
359 context: &mut DecompressionContext,
360) -> Result<Vec<u8>, ArrowError> {
361 let output = context
362 .zstd_decompressor()
363 .decompress(input, decompressed_size)?;
364 Ok(output)
365}
366
367#[cfg(not(feature = "zstd"))]
368fn decompress_zstd(
369 _input: &[u8],
370 _decompressed_size: usize,
371 _context: &mut DecompressionContext,
372) -> Result<Vec<u8>, ArrowError> {
373 Err(ArrowError::InvalidArgumentError(
374 "zstd IPC decompression requires the zstd feature".to_string(),
375 ))
376}
377
378#[inline]
385fn read_uncompressed_size(buffer: &[u8]) -> Result<i64, ArrowError> {
386 let len_buffer = buffer.get(..LENGTH_OF_PREFIX_DATA as usize).ok_or_else(|| {
387 ArrowError::IpcError(format!(
388 "Compressed IPC buffer is too short: expected at least {LENGTH_OF_PREFIX_DATA} bytes, got {}",
389 buffer.len()
390 ))
391 })?;
392 Ok(i64::from_le_bytes(len_buffer.try_into().unwrap()))
393}
394
395#[cfg(test)]
396mod tests {
397 #[test]
398 #[cfg(feature = "lz4")]
399 fn test_lz4_compression() {
400 let input_bytes = b"hello lz4";
401 let codec = super::CompressionCodec::Lz4Frame;
402 let mut output_bytes: Vec<u8> = Vec::new();
403 codec
404 .compress(input_bytes, &mut output_bytes, &mut Default::default())
405 .unwrap();
406 let result = codec
407 .decompress(
408 output_bytes.as_slice(),
409 input_bytes.len(),
410 &mut Default::default(),
411 )
412 .unwrap();
413 assert_eq!(input_bytes, result.as_slice());
414 }
415
416 #[test]
417 #[cfg(feature = "zstd")]
418 fn test_zstd_compression() {
419 let input_bytes = b"hello zstd";
420 let codec = super::CompressionCodec::Zstd(super::DEFAULT_ZSTD_COMPRESSION_LEVEL);
421 let mut output_bytes: Vec<u8> = Vec::new();
422 codec
423 .compress(input_bytes, &mut output_bytes, &mut Default::default())
424 .unwrap();
425 let result = codec
426 .decompress(
427 output_bytes.as_slice(),
428 input_bytes.len(),
429 &mut Default::default(),
430 )
431 .unwrap();
432 assert_eq!(input_bytes, result.as_slice());
433 }
434
435 #[test]
436 fn test_read_uncompressed_size_rejects_short_prefix() {
437 let err = super::read_uncompressed_size(&[1, 2, 3, 4, 5, 6, 7])
438 .expect_err("short compressed IPC prefix should return an error");
439
440 assert!(
441 err.to_string()
442 .contains("Compressed IPC buffer is too short"),
443 "unexpected error: {err}"
444 );
445 }
446
447 #[test]
448 #[cfg(feature = "lz4")]
449 fn test_compress_to_vec_writes_8_byte_length_prefix() {
450 let input_bytes = vec![42u8; 132];
453 let codec = super::CompressionCodec::Lz4Frame;
454 let mut output_bytes: Vec<u8> = Vec::new();
455 codec
456 .compress_to_vec(&input_bytes, &mut output_bytes, &mut Default::default())
457 .unwrap();
458
459 let prefix: [u8; 8] = output_bytes[..8].try_into().unwrap();
460 assert_eq!(i64::from_le_bytes(prefix), input_bytes.len() as i64);
461 }
462}