Skip to main content

crush_parallel/
block.rs

1//! Per-block compression and decompression helpers.
2
3use crate::config::EngineConfiguration;
4use crate::format::{BlockFlags, BlockHeader};
5use crush_core::error::{CrushError, Result};
6use libdeflater::{CompressionLvl, Compressor, Decompressor};
7
8/// Result of compressing a single block.
9pub struct CompressedBlock {
10    pub header: BlockHeader,
11    pub payload: Vec<u8>,
12}
13
14/// Resolve a config-level `compression_level` (0-9) to libdeflater's `CompressionLvl`.
15///
16/// Exposed to `engine.rs` so the resolution happens once on the driver thread,
17/// not once per block.
18///
19/// # Errors
20///
21/// Returns [`CrushError::InvalidConfig`] if `level` is outside libdeflater's 0-12 range.
22pub(crate) fn resolve_compression_level(level: u8) -> Result<CompressionLvl> {
23    CompressionLvl::new(i32::from(level))
24        .map_err(|_| CrushError::InvalidConfig(format!("invalid compression level {level}")))
25}
26
27/// Compress one block of input data using a caller-provided, worker-pooled `Compressor`.
28///
29/// If the compressed output would exceed `config.max_expansion_ratio * input.len()`,
30/// the block is stored uncompressed (raw) with the `stored` flag set.
31///
32/// # Errors
33///
34/// Returns an error if DEFLATE encoding fails internally.
35pub fn compress_block(
36    compressor: &mut Compressor,
37    input: &[u8],
38    block_index: usize,
39    config: &EngineConfiguration,
40) -> Result<CompressedBlock> {
41    let checksum = if config.checksums {
42        crc32fast::hash(input)
43    } else {
44        0
45    };
46
47    let in_len = input.len();
48    let uncompressed_size = u32::try_from(in_len).map_err(|_| {
49        CrushError::InvalidConfig(format!(
50            "block {block_index} input length {in_len} exceeds u32::MAX"
51        ))
52    })?;
53
54    // deflate_compress_bound gives the exact worst-case output size — no over-allocation.
55    let buf_size = compressor.deflate_compress_bound(in_len);
56    let mut compressed = vec![0u8; buf_size];
57
58    let bytes_written = compressor
59        .deflate_compress(input, &mut compressed)
60        .map_err(|e| {
61            CrushError::InvalidFormat(format!(
62                "DEFLATE encode error at block {block_index}: {e:?}"
63            ))
64        })?;
65    debug_assert!(
66        bytes_written <= buf_size,
67        "libdeflater returned {bytes_written} > capacity {buf_size}"
68    );
69    compressed.truncate(bytes_written);
70
71    // Fall back to raw storage if compressed output is not smaller enough.
72    // Precision loss from usize→f64 is acceptable for this ratio heuristic.
73    #[allow(clippy::cast_precision_loss)]
74    let use_stored =
75        in_len > 0 && (compressed.len() as f64 / in_len as f64) > config.max_expansion_ratio;
76
77    let (payload, flags, cs) = if use_stored {
78        (
79            input.to_vec(),
80            BlockFlags::default().with_stored(),
81            uncompressed_size,
82        )
83    } else {
84        let cs = u32::try_from(compressed.len()).map_err(|_| {
85            CrushError::InvalidConfig(format!("compressed block {block_index} exceeds u32::MAX"))
86        })?;
87        (compressed, BlockFlags::default(), cs)
88    };
89
90    Ok(CompressedBlock {
91        header: BlockHeader {
92            compressed_size: cs,
93            uncompressed_size,
94            checksum,
95            flags,
96        },
97        payload,
98    })
99}
100
101/// Decompress one block payload directly into a caller-provided output slice.
102///
103/// `output.len()` MUST equal `header.uncompressed_size`.
104///
105/// This is the hot-path variant: worker threads call it via `par_iter_mut` over
106/// disjoint slices of the final output buffer, so there is no per-block
107/// `Vec<u8>` allocation and no final copy into place.
108///
109/// # Errors
110///
111/// - [`CrushError::InvalidFormat`] if `output.len()` disagrees with `header.uncompressed_size`,
112///   if the decoder reports a length mismatch, or if DEFLATE decoding fails.
113/// - [`CrushError::ChecksumMismatch`] if checksums are enabled and CRC32 fails.
114pub fn decompress_block_into(
115    decompressor: &mut Decompressor,
116    header: &BlockHeader,
117    payload: &[u8],
118    output: &mut [u8],
119    block_index: u64,
120    checksums_enabled: bool,
121) -> Result<()> {
122    let expected_size = header.uncompressed_size as usize;
123    if output.len() != expected_size {
124        return Err(CrushError::InvalidFormat(format!(
125            "block {block_index} output buffer size {} != header uncompressed_size {expected_size}",
126            output.len()
127        )));
128    }
129
130    if header.flags.stored() {
131        if payload.len() != expected_size {
132            return Err(CrushError::InvalidFormat(format!(
133                "stored block {block_index} payload length {} != uncompressed_size {expected_size}",
134                payload.len()
135            )));
136        }
137        output.copy_from_slice(payload);
138    } else {
139        let bytes_out = decompressor
140            .deflate_decompress(payload, output)
141            .map_err(|e| {
142                CrushError::InvalidFormat(format!(
143                    "DEFLATE decode error at block {block_index}: {e:?}"
144                ))
145            })?;
146        if bytes_out != expected_size {
147            return Err(CrushError::InvalidFormat(format!(
148                "block {block_index} uncompressed size mismatch: header {expected_size} vs decoded {bytes_out}"
149            )));
150        }
151    }
152
153    if checksums_enabled && header.checksum != 0 {
154        let actual = crc32fast::hash(output);
155        if actual != header.checksum {
156            return Err(CrushError::ChecksumMismatch {
157                block_index,
158                expected: header.checksum,
159                actual,
160            });
161        }
162    }
163
164    Ok(())
165}
166
167/// Allocating wrapper around [`decompress_block_into`] — convenience for callers
168/// that want an owned `Vec<u8>` (e.g. random-access `decompress_block`).
169///
170/// The hot path in `engine::decompress_from_reader` does NOT use this; it calls
171/// `decompress_block_into` against a pre-allocated slice.
172///
173/// # Errors
174///
175/// Same as [`decompress_block_into`].
176pub fn decompress_block_payload(
177    decompressor: &mut Decompressor,
178    header: &BlockHeader,
179    payload: &[u8],
180    block_index: u64,
181    checksums_enabled: bool,
182) -> Result<Vec<u8>> {
183    let expected_size = header.uncompressed_size as usize;
184    let mut out = vec![0u8; expected_size];
185    decompress_block_into(
186        decompressor,
187        header,
188        payload,
189        &mut out,
190        block_index,
191        checksums_enabled,
192    )?;
193    Ok(out)
194}