Skip to main content

idb/innodb/
compression.rs

1//! Tablespace compression detection and decompression.
2//!
3//! Detects the compression algorithm from FSP flags (bits 11-12) and provides
4//! zlib and LZ4 decompression helpers for compressed page data.
5
6use flate2::read::ZlibDecoder;
7use std::io::Read;
8
9/// Compression algorithm detected or used for a page.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum CompressionAlgorithm {
12    None,
13    Zlib,
14    Lz4,
15}
16
17/// Detect the compression algorithm from FSP space flags.
18///
19/// Compression type is stored in bits 11-12 of FSP flags (MySQL 8.0+).
20/// - 0 = none
21/// - 1 = zlib
22/// - 2 = lz4
23pub fn detect_compression(fsp_flags: u32) -> CompressionAlgorithm {
24    let comp_bits = (fsp_flags >> 11) & 0x03;
25    match comp_bits {
26        1 => CompressionAlgorithm::Zlib,
27        2 => CompressionAlgorithm::Lz4,
28        _ => CompressionAlgorithm::None,
29    }
30}
31
32/// Decompress zlib-compressed page data.
33///
34/// Returns the decompressed data, or None if decompression fails.
35pub fn decompress_zlib(compressed: &[u8]) -> Option<Vec<u8>> {
36    let mut decoder = ZlibDecoder::new(compressed);
37    let mut decompressed = Vec::new();
38    decoder.read_to_end(&mut decompressed).ok()?;
39    Some(decompressed)
40}
41
42/// Decompress LZ4-compressed page data.
43///
44/// `uncompressed_len` is the expected output size (typically the page size).
45/// Returns the decompressed data, or None if decompression fails.
46pub fn decompress_lz4(compressed: &[u8], uncompressed_len: usize) -> Option<Vec<u8>> {
47    lz4_flex::decompress(compressed, uncompressed_len).ok()
48}
49
50/// Check if a page appears to be a hole-punched page.
51///
52/// Hole-punched pages have their data zeroed out after the compressed content.
53/// The FIL header is preserved, and the actual data is followed by trailing zeros.
54pub fn is_hole_punched(page_data: &[u8], page_size: u32) -> bool {
55    if page_data.len() < page_size as usize {
56        return false;
57    }
58
59    // A hole-punched page has trailing zeros. Check the last quarter of the page.
60    let check_start = (page_size as usize * 3) / 4;
61    page_data[check_start..page_size as usize]
62        .iter()
63        .all(|&b| b == 0)
64}
65
66impl std::fmt::Display for CompressionAlgorithm {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            CompressionAlgorithm::None => write!(f, "None"),
70            CompressionAlgorithm::Zlib => write!(f, "Zlib"),
71            CompressionAlgorithm::Lz4 => write!(f, "LZ4"),
72        }
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn test_detect_compression() {
82        assert_eq!(detect_compression(0), CompressionAlgorithm::None);
83        assert_eq!(detect_compression(1 << 11), CompressionAlgorithm::Zlib);
84        assert_eq!(detect_compression(2 << 11), CompressionAlgorithm::Lz4);
85        assert_eq!(detect_compression(3 << 11), CompressionAlgorithm::None);
86        // Other bits set shouldn't affect compression detection
87        assert_eq!(detect_compression(0xFF | (1 << 11)), CompressionAlgorithm::Zlib);
88    }
89
90    #[test]
91    fn test_decompress_zlib() {
92        use flate2::write::ZlibEncoder;
93        use flate2::Compression;
94        use std::io::Write;
95
96        let original = b"Hello, InnoDB compression test data!";
97        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
98        encoder.write_all(original).unwrap();
99        let compressed = encoder.finish().unwrap();
100
101        let result = decompress_zlib(&compressed).unwrap();
102        assert_eq!(result, original);
103    }
104
105    #[test]
106    fn test_decompress_lz4() {
107        let original = b"Hello, LZ4 compression test data for InnoDB!";
108        let compressed = lz4_flex::compress_prepend_size(original);
109        // lz4_flex::compress_prepend_size adds 4-byte length prefix,
110        // but decompress expects just the compressed data with known length
111        let result = lz4_flex::decompress(&compressed[4..], original.len());
112        assert!(result.is_ok());
113        assert_eq!(result.unwrap(), original);
114    }
115
116    #[test]
117    fn test_is_hole_punched() {
118        let page_size = 16384u32;
119        let mut page = vec![0u8; page_size as usize];
120        // All zeros = hole punched
121        assert!(is_hole_punched(&page, page_size));
122
123        // Some data in the first part, zeros in the last quarter
124        page[0] = 0xFF;
125        page[100] = 0xAB;
126        assert!(is_hole_punched(&page, page_size));
127
128        // Non-zero byte in the last quarter = not hole punched
129        page[page_size as usize - 10] = 0x01;
130        assert!(!is_hole_punched(&page, page_size));
131    }
132}