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!(
88            detect_compression(0xFF | (1 << 11)),
89            CompressionAlgorithm::Zlib
90        );
91    }
92
93    #[test]
94    fn test_decompress_zlib() {
95        use flate2::write::ZlibEncoder;
96        use flate2::Compression;
97        use std::io::Write;
98
99        let original = b"Hello, InnoDB compression test data!";
100        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
101        encoder.write_all(original).unwrap();
102        let compressed = encoder.finish().unwrap();
103
104        let result = decompress_zlib(&compressed).unwrap();
105        assert_eq!(result, original);
106    }
107
108    #[test]
109    fn test_decompress_lz4() {
110        let original = b"Hello, LZ4 compression test data for InnoDB!";
111        let compressed = lz4_flex::compress_prepend_size(original);
112        // lz4_flex::compress_prepend_size adds 4-byte length prefix,
113        // but decompress expects just the compressed data with known length
114        let result = lz4_flex::decompress(&compressed[4..], original.len());
115        assert!(result.is_ok());
116        assert_eq!(result.unwrap(), original);
117    }
118
119    #[test]
120    fn test_is_hole_punched() {
121        let page_size = 16384u32;
122        let mut page = vec![0u8; page_size as usize];
123        // All zeros = hole punched
124        assert!(is_hole_punched(&page, page_size));
125
126        // Some data in the first part, zeros in the last quarter
127        page[0] = 0xFF;
128        page[100] = 0xAB;
129        assert!(is_hole_punched(&page, page_size));
130
131        // Non-zero byte in the last quarter = not hole punched
132        page[page_size as usize - 10] = 0x01;
133        assert!(!is_hole_punched(&page, page_size));
134    }
135}