| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| use pyo3::prelude::*; |
| use std::fs::File; |
| use std::io::{BufReader, BufWriter, Read, Write}; |
| use crate::errors::KernelError; |
|
|
| |
|
|
| |
| const CHUNK_SIZE: usize = 8 * 1024 * 1024; |
|
|
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| #[pyfunction] |
| #[pyo3(signature = (input_path, output_path, level = 3))] |
| pub fn compress_shard( |
| py: Python<'_>, |
| input_path: String, |
| output_path: String, |
| level: i32, |
| ) -> PyResult<(u64, u64)> { |
| py.allow_threads(move || { |
| compress_internal(&input_path, &output_path, level) |
| .map_err(|e| pyo3::exceptions::PyIOError::new_err(e.to_string())) |
| }) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| #[pyfunction] |
| pub fn decompress_shard( |
| py: Python<'_>, |
| input_path: String, |
| output_path: String, |
| ) -> PyResult<u64> { |
| py.allow_threads(move || { |
| decompress_internal(&input_path, &output_path) |
| .map_err(|e| pyo3::exceptions::PyIOError::new_err(e.to_string())) |
| }) |
| } |
|
|
| |
|
|
| fn compress_internal( |
| input_path: &str, |
| output_path: &str, |
| level: i32, |
| ) -> Result<(u64, u64), KernelError> { |
| let clamped_level = level.clamp(1, 22); |
|
|
| let input_file = File::open(input_path) |
| .map_err(|e| KernelError::IoError(format!("Cannot open {}: {}", input_path, e)))?; |
| let original_size = input_file.metadata() |
| .map(|m| m.len()) |
| .unwrap_or(0); |
|
|
| let mut reader = BufReader::with_capacity(CHUNK_SIZE, input_file); |
|
|
| let output_file = File::create(output_path) |
| .map_err(|e| KernelError::IoError(format!("Cannot create {}: {}", output_path, e)))?; |
| let writer = BufWriter::with_capacity(CHUNK_SIZE, output_file); |
|
|
| |
| let mut encoder = zstd::Encoder::new(writer, clamped_level) |
| .map_err(|e| KernelError::CompressionError(e.to_string()))?; |
|
|
| |
| let mut buf = vec![0u8; CHUNK_SIZE]; |
| loop { |
| let n = reader.read(&mut buf) |
| .map_err(|e| KernelError::IoError(e.to_string()))?; |
| if n == 0 { break; } |
|
|
| encoder.write_all(&buf[..n]) |
| .map_err(|e| KernelError::CompressionError(e.to_string()))?; |
| } |
|
|
| let mut compressed_writer = encoder.finish() |
| .map_err(|e| KernelError::CompressionError(e.to_string()))?; |
|
|
| compressed_writer.flush() |
| .map_err(|e| KernelError::IoError(e.to_string()))?; |
|
|
| let compressed_size = std::fs::metadata(output_path) |
| .map(|m| m.len()) |
| .unwrap_or(0); |
|
|
| Ok((original_size, compressed_size)) |
| } |
|
|
| fn decompress_internal( |
| input_path: &str, |
| output_path: &str, |
| ) -> Result<u64, KernelError> { |
| let input_file = File::open(input_path) |
| .map_err(|e| KernelError::IoError(format!("Cannot open {}: {}", input_path, e)))?; |
| let reader = BufReader::with_capacity(CHUNK_SIZE, input_file); |
|
|
| let output_file = File::create(output_path) |
| .map_err(|e| KernelError::IoError(format!("Cannot create {}: {}", output_path, e)))?; |
| let mut writer = BufWriter::with_capacity(CHUNK_SIZE, output_file); |
|
|
| let mut decoder = zstd::Decoder::new(reader) |
| .map_err(|e| KernelError::CompressionError(e.to_string()))?; |
|
|
| let mut buf = vec![0u8; CHUNK_SIZE]; |
| let mut total_bytes = 0u64; |
|
|
| loop { |
| let n = decoder.read(&mut buf) |
| .map_err(|e| KernelError::CompressionError(e.to_string()))?; |
| if n == 0 { break; } |
|
|
| writer.write_all(&buf[..n]) |
| .map_err(|e| KernelError::IoError(e.to_string()))?; |
| total_bytes += n as u64; |
| } |
|
|
| writer.flush() |
| .map_err(|e| KernelError::IoError(e.to_string()))?; |
|
|
| Ok(total_bytes) |
| } |
|
|
| |
|
|
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use std::io::Write; |
|
|
| #[test] |
| fn test_compress_decompress_roundtrip() { |
| let dir = tempfile::tempdir().expect("create temp dir"); |
| let input = dir.path().join("input.bin"); |
| let compressed = dir.path().join("input.bin.zst"); |
| let decompressed = dir.path().join("output.bin"); |
|
|
| |
| let test_data = b"Hello SAIL! This is a test of the kernel compressor.".repeat(1000); |
| std::fs::write(&input, &test_data).unwrap(); |
|
|
| |
| let (orig, comp) = compress_internal( |
| input.to_str().unwrap(), |
| compressed.to_str().unwrap(), |
| DEFAULT_ZSTD_LEVEL, |
| ).unwrap(); |
|
|
| assert!(comp < orig, "Compressed should be smaller than original"); |
|
|
| |
| let decompressed_bytes = decompress_internal( |
| compressed.to_str().unwrap(), |
| decompressed.to_str().unwrap(), |
| ).unwrap(); |
|
|
| |
| let result = std::fs::read(&decompressed).unwrap(); |
| assert_eq!(result, test_data, "Round-trip decompression must be lossless"); |
| assert_eq!(decompressed_bytes, test_data.len() as u64); |
| } |
|
|
| #[test] |
| fn test_invalid_input_path() { |
| let result = compress_internal("/nonexistent/path/file.bin", "/tmp/out.zst", 3); |
| assert!(result.is_err(), "Should fail on missing input"); |
| } |
| } |
|
|