use crate::error::FsError; use crate::lz4; use lancer_core::fs::{BLOCK_SIZE_MAX_LOG2, BLOCK_SIZE_MIN_LOG2, Compression, CompressionPolicy}; const LZ4_HASH_SIZE: usize = 1 << 12; pub struct CompressResult { pub compressed_len: usize, pub physical_log2: u8, pub algorithm: Compression, pub logical_size: u32, } pub fn compress_block( data: &[u8], effective_policy: Compression, scratch: &mut [u8], hash_table: &mut [u16; LZ4_HASH_SIZE], ) -> CompressResult { let logical_size = data.len() as u32; let logical_log2 = smallest_pow2_log2(data.len()); match effective_policy { Compression::None => CompressResult { compressed_len: 0, physical_log2: logical_log2, algorithm: Compression::None, logical_size, }, Compression::Lz4 => match lz4::compress(data, scratch, hash_table) { Some(compressed_len) if compressed_len <= data.len() / 2 => { let physical_log2 = smallest_pow2_log2(compressed_len); CompressResult { compressed_len, physical_log2, algorithm: Compression::Lz4, logical_size, } } _ => CompressResult { compressed_len: 0, physical_log2: logical_log2, algorithm: Compression::None, logical_size, }, }, } } pub fn decompress_block( src: &[u8], compression: Compression, logical_size: u32, dst: &mut [u8], ) -> Result { match compression { Compression::None => { let len = (logical_size as usize).min(src.len()).min(dst.len()); dst[..len].copy_from_slice(&src[..len]); Ok(len) } Compression::Lz4 => { let out_len = (logical_size as usize).min(dst.len()); lz4::decompress(src, &mut dst[..out_len]) } } } pub fn effective_compression( inode_policy: CompressionPolicy, parent_effective: Compression, ) -> Compression { match inode_policy { CompressionPolicy::Lz4 => Compression::Lz4, CompressionPolicy::Disabled => Compression::None, CompressionPolicy::Inherit => parent_effective, } } #[cfg(test)] mod tests { use super::*; #[test] fn compress_none_passthrough() { let data = [0x42_u8; 4096]; let mut scratch = [0u8; 8192]; let mut table = [0u16; LZ4_HASH_SIZE]; let result = compress_block(&data, Compression::None, &mut scratch, &mut table); assert_eq!(result.compressed_len, 0); assert_eq!(result.algorithm, Compression::None); assert_eq!(result.logical_size, 4096); } #[test] fn compress_lz4_compressible_data() { let data = [0u8; 4096]; let mut scratch = [0u8; 8192]; let mut table = [0u16; LZ4_HASH_SIZE]; let result = compress_block(&data, Compression::Lz4, &mut scratch, &mut table); assert_eq!(result.algorithm, Compression::Lz4); assert!(result.compressed_len > 0); assert!(result.compressed_len <= 2048); assert_eq!(result.logical_size, 4096); } #[test] fn compress_lz4_incompressible_falls_back() { let mut data = [0u8; 4096]; let mut state: u64 = 0xDEAD_BEEF_CAFE_BABE; data.iter_mut().for_each(|byte| { state = state .wrapping_mul(6364136223846793005) .wrapping_add(1442695040888963407); *byte = (state >> 33) as u8; }); let mut scratch = [0u8; 8192]; let mut table = [0u16; LZ4_HASH_SIZE]; let result = compress_block(&data, Compression::Lz4, &mut scratch, &mut table); assert_eq!(result.algorithm, Compression::None); assert_eq!(result.compressed_len, 0); } #[test] fn decompress_none_copies_data() { let src = [0xAB_u8; 4096]; let mut dst = [0u8; 4096]; let len = decompress_block(&src, Compression::None, 4096, &mut dst).unwrap(); assert_eq!(len, 4096); assert_eq!(dst, src); } #[test] fn compress_decompress_lz4_roundtrip() { let mut data = [0u8; 4096]; (0..64).for_each(|i| { data[i * 64..i * 64 + 8].copy_from_slice(&(i as u64).to_le_bytes()); data[i * 64 + 8..i * 64 + 64].fill(0xCC); }); let mut scratch = [0u8; 8192]; let mut table = [0u16; LZ4_HASH_SIZE]; let result = compress_block(&data, Compression::Lz4, &mut scratch, &mut table); match result.algorithm { Compression::Lz4 => { let mut decompressed = [0u8; 4096]; let dlen = decompress_block( &scratch[..result.compressed_len], Compression::Lz4, result.logical_size, &mut decompressed, ) .unwrap(); assert_eq!(&decompressed[..dlen], &data[..dlen]); } Compression::None => {} } } #[test] fn effective_compression_inherit_uses_parent() { assert_eq!( effective_compression(CompressionPolicy::Inherit, Compression::Lz4), Compression::Lz4 ); assert_eq!( effective_compression(CompressionPolicy::Inherit, Compression::None), Compression::None ); } #[test] fn effective_compression_explicit_overrides() { assert_eq!( effective_compression(CompressionPolicy::Lz4, Compression::None), Compression::Lz4 ); assert_eq!( effective_compression(CompressionPolicy::Disabled, Compression::Lz4), Compression::None ); } #[test] fn smallest_pow2_log2_boundary_values() { assert_eq!(smallest_pow2_log2(1), BLOCK_SIZE_MIN_LOG2); assert_eq!(smallest_pow2_log2(4096), BLOCK_SIZE_MIN_LOG2); assert_eq!(smallest_pow2_log2(4097), BLOCK_SIZE_MIN_LOG2 + 1); assert_eq!(smallest_pow2_log2(65536), BLOCK_SIZE_MAX_LOG2); assert_eq!(smallest_pow2_log2(65537), BLOCK_SIZE_MAX_LOG2); } } fn smallest_pow2_log2(size: usize) -> u8 { (BLOCK_SIZE_MIN_LOG2..=BLOCK_SIZE_MAX_LOG2) .find(|&log2| (1usize << log2) >= size) .unwrap_or(BLOCK_SIZE_MAX_LOG2) }