turbo_persistence/
compression.rs1#[cfg(not(miri))]
2use std::cell::RefCell;
3use std::{mem::MaybeUninit, rc::Rc, sync::Arc};
4
5#[cfg(not(miri))]
6use anyhow::Context;
7use anyhow::{Result, ensure};
8#[cfg(not(miri))]
9use lz4_flex::block::{
10 CompressTable, compress_into_with_table, decompress_into, get_maximum_output_size,
11};
12
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15#[repr(u8)]
16pub enum Compression {
17 #[default]
19 Lz4 = 0,
20 Zstd3 = 1,
22}
23
24#[cfg(not(miri))]
25thread_local! {
26 static LZ4_COMPRESS_TABLE: RefCell<CompressTable> = RefCell::new(CompressTable::large());
29
30 static ZSTD_DECOMPRESSOR: RefCell<zstd::bulk::Decompressor<'static>> = RefCell::new(
33 zstd::bulk::Decompressor::new().expect("zstd decompressor initialization should succeed")
34 );
35}
36
37fn decompress_block(
39 compression: Compression,
40 block: &[u8],
41 dest: &mut [u8],
42 expected_len: u32,
43) -> Result<()> {
44 debug_assert!(
45 expected_len > 0,
46 "decompress_block called with uncompressed_length=0; uncompressed blocks are served \
47 directly from their backing"
48 );
49 #[cfg(not(miri))]
50 {
51 let bytes_written = match compression {
52 Compression::Lz4 => decompress_into(block, dest).map_err(anyhow::Error::from),
53 Compression::Zstd3 => ZSTD_DECOMPRESSOR.with_borrow_mut(|decompressor| {
54 decompressor
55 .decompress_to_buffer(block, dest)
56 .map_err(anyhow::Error::from)
57 }),
58 }
59 .with_context(|| {
60 format!(
61 "Failed to decompress {compression:?} block ({} bytes compressed, {} bytes \
62 uncompressed)",
63 block.len(),
64 expected_len
65 )
66 })?;
67 ensure!(
68 bytes_written == expected_len as usize,
69 "Decompressed length does not match expected length: decompressed {bytes_written} \
70 bytes, expected {expected_len}"
71 );
72 }
73 #[cfg(miri)]
74 {
75 let _ = compression;
77 ensure!(
78 block.len() == expected_len as usize,
79 "Miri builds skip compression, so a compressed block cannot be read under Miri"
80 );
81 dest.copy_from_slice(block);
82 }
83 Ok(())
84}
85
86pub(crate) fn decompress_into_arc(
91 compression: Compression,
92 uncompressed_length: u32,
93 block: &[u8],
94) -> Result<Arc<[u8]>> {
95 let buffer: Arc<[MaybeUninit<u8>]> = Arc::new_uninit_slice(uncompressed_length as usize);
98 let mut buffer = unsafe { buffer.assume_init() };
101 let dest = Arc::get_mut(&mut buffer).expect("Arc refcount should be 1");
103 decompress_block(compression, block, dest, uncompressed_length)?;
104 Ok(buffer)
105}
106
107pub(crate) fn decompress_into_rc(
109 compression: Compression,
110 uncompressed_length: u32,
111 block: &[u8],
112) -> Result<Rc<[u8]>> {
113 let buffer: Rc<[MaybeUninit<u8>]> = Rc::new_uninit_slice(uncompressed_length as usize);
114 let mut buffer = unsafe { buffer.assume_init() };
117 let dest = Rc::get_mut(&mut buffer).expect("Rc refcount should be 1");
118 decompress_block(compression, block, dest, uncompressed_length)?;
119 Ok(buffer)
120}
121
122pub fn checksum_block(data: &[u8]) -> u32 {
124 crc32fast::hash(data)
125}
126
127pub(crate) struct Compressor {
129 compression: Compression,
130 #[cfg(not(miri))]
131 zstd: Option<zstd::bulk::Compressor<'static>>,
132}
133
134impl Compressor {
135 pub(crate) fn new(compression: Compression) -> Result<Self> {
136 #[cfg(not(miri))]
137 let zstd = match compression {
138 Compression::Zstd3 => {
139 Some(zstd::bulk::Compressor::new(3).context("Failed to create zstd compressor")?)
140 }
141 Compression::Lz4 => None,
142 };
143 Ok(Self {
144 compression,
145 #[cfg(not(miri))]
146 zstd,
147 })
148 }
149
150 #[tracing::instrument(level = "trace", skip_all)]
152 pub(crate) fn compress_into_buffer(
153 &mut self,
154 block: &[u8],
155 buffer: &mut Vec<u8>,
156 ) -> Result<()> {
157 buffer.clear();
158 #[cfg(not(miri))]
159 match self.compression {
160 Compression::Lz4 => {
161 let max_output_size = get_maximum_output_size(block.len());
162 buffer.reserve(max_output_size);
163 let output =
169 unsafe { std::slice::from_raw_parts_mut(buffer.as_mut_ptr(), max_output_size) };
170 let compressed_len = LZ4_COMPRESS_TABLE
171 .with_borrow_mut(|table| compress_into_with_table(block, output, table))
172 .context("LZ4 compression failed")?;
173 unsafe { buffer.set_len(compressed_len) };
175 }
176 Compression::Zstd3 => {
177 buffer.reserve(zstd::zstd_safe::compress_bound(block.len()));
178 self.zstd
179 .as_mut()
180 .expect("zstd compressor not initialized")
181 .compress_to_buffer(block, buffer)
182 .context("zstd compression failed")?;
183 }
184 }
185 #[cfg(miri)]
186 {
187 let _ = self.compression;
191 buffer.extend_from_slice(block);
192 }
193 Ok(())
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn compression_round_trips() {
203 let input = b"turbo persistence compression ".repeat(1024);
204 for compression in [Compression::Lz4, Compression::Zstd3] {
205 let mut compressor = Compressor::new(compression).unwrap();
206 let mut compressed = Vec::new();
207 compressor
208 .compress_into_buffer(&input, &mut compressed)
209 .unwrap();
210 let output = decompress_into_arc(compression, input.len() as u32, &compressed).unwrap();
211 assert_eq!(&*output, input);
212 }
213 }
214}