use std::io::{self, Read, Write};
use crate::block::compress::{compress_bound, compress_fast};
use crate::io::file_io::{open_dst_file, open_src_file, STDOUT_MARK};
use crate::io::prefs::{
final_time_display, Prefs, LEGACY_BLOCKSIZE, LEGACY_MAGICNUMBER, MAGICNUMBER_SIZE,
};
use crate::timefn::get_time;
extern "C" {
fn clock() -> libc::clock_t;
}
const LEGACY_BLOCK_HEADER_SIZE: usize = 4;
#[derive(Debug, Clone, Copy, Default)]
pub struct LegacyResult {
pub bytes_read: u64,
pub bytes_written: u64,
}
fn compress_block_fast(src: &[u8], dst: &mut Vec<u8>, clevel: i32) -> io::Result<usize> {
let acceleration = if clevel < 0 { -clevel } else { 0 };
let bound = compress_bound(src.len() as i32) as usize;
dst.resize(bound + LEGACY_BLOCK_HEADER_SIZE, 0);
let c_size = compress_fast(src, &mut dst[LEGACY_BLOCK_HEADER_SIZE..], acceleration)
.map_err(|e| io::Error::other(format!("fast compression failed: {:?}", e)))?;
dst[..4].copy_from_slice(&(c_size as u32).to_le_bytes());
Ok(c_size + LEGACY_BLOCK_HEADER_SIZE)
}
fn compress_block_hc(src: &[u8], dst: &mut Vec<u8>, clevel: i32) -> io::Result<usize> {
let bound = compress_bound(src.len() as i32) as usize;
dst.resize(bound + LEGACY_BLOCK_HEADER_SIZE, 0);
let c_size = unsafe {
crate::hc::api::compress_hc(
src.as_ptr(),
dst[LEGACY_BLOCK_HEADER_SIZE..].as_mut_ptr(),
src.len() as i32,
bound as i32,
clevel,
)
};
if c_size < 0 {
return Err(io::Error::other("HC compression failed"));
}
let c_size = c_size as usize;
dst[..4].copy_from_slice(&(c_size as u32).to_le_bytes());
Ok(c_size + LEGACY_BLOCK_HEADER_SIZE)
}
fn compress_legacy_internal(
input_filename: &str,
output_filename: &str,
compressionlevel: i32,
prefs: &Prefs,
) -> io::Result<LegacyResult> {
let mut src_reader = open_src_file(input_filename)?;
let mut dst_file = open_dst_file(output_filename, prefs)?;
let magic_bytes = LEGACY_MAGICNUMBER.to_le_bytes();
dst_file.write_all(&magic_bytes)?;
let mut bytes_read: u64 = 0;
let mut bytes_written: u64 = MAGICNUMBER_SIZE as u64;
let mut src_buf = vec![0u8; LEGACY_BLOCKSIZE];
let mut cmp_buf: Vec<u8> = Vec::with_capacity(
compress_bound(LEGACY_BLOCKSIZE as i32) as usize + LEGACY_BLOCK_HEADER_SIZE,
);
let use_hc = compressionlevel >= 3;
loop {
let mut total_read = 0usize;
while total_read < LEGACY_BLOCKSIZE {
match src_reader.read(&mut src_buf[total_read..]) {
Ok(0) => break,
Ok(n) => total_read += n,
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
if total_read == 0 {
break; }
bytes_read += total_read as u64;
let chunk = &src_buf[..total_read];
let written = if use_hc {
compress_block_hc(chunk, &mut cmp_buf, compressionlevel)?
} else {
compress_block_fast(chunk, &mut cmp_buf, compressionlevel)?
};
dst_file.write_all(&cmp_buf[..written])?;
bytes_written += written as u64;
}
dst_file.flush()?;
let ratio = if bytes_read == 0 {
100.0
} else {
(bytes_written as f64) / (bytes_read as f64) * 100.0
};
crate::io::prefs::display_level(
2,
&format!(
"\r{:79}\r",
"" ),
);
crate::io::prefs::display_level(
2,
&format!(
"Compressed {} bytes into {} bytes ==> {:.2}% \n",
bytes_read, bytes_written, ratio
),
);
Ok(LegacyResult {
bytes_read,
bytes_written,
})
}
pub fn compress_filename_legacy(
src: &str,
dst: &str,
compressionlevel: i32,
prefs: &Prefs,
) -> io::Result<LegacyResult> {
let time_start = get_time();
let cpu_start = unsafe { clock() };
let result = compress_legacy_internal(src, dst, compressionlevel, prefs);
let processed = result.as_ref().map(|r| r.bytes_read).unwrap_or(0);
final_time_display(time_start, cpu_start, processed);
result
}
pub fn compress_multiple_filenames_legacy(
srcs: &[&str],
suffix: &str,
compressionlevel: i32,
prefs: &Prefs,
) -> io::Result<()> {
let time_start = get_time();
let cpu_start = unsafe { clock() };
let mut missed_files: usize = 0;
let mut total_processed: u64 = 0;
let suffix_is_stdout = suffix == STDOUT_MARK;
for &src in srcs {
let dst: String = if suffix_is_stdout {
STDOUT_MARK.to_owned()
} else {
format!("{}{}", src, suffix)
};
match compress_legacy_internal(src, &dst, compressionlevel, prefs) {
Ok(res) => total_processed += res.bytes_read,
Err(_) => missed_files += 1,
}
}
final_time_display(time_start, cpu_start, total_processed);
if missed_files > 0 {
Err(io::Error::other(format!(
"{} file(s) could not be compressed",
missed_files
)))
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::io::prefs::Prefs;
fn compress_to_bytes(data: &[u8], clevel: i32) -> Vec<u8> {
let dir = tempfile::tempdir().unwrap();
let src_path = dir.path().join("input.bin");
let dst_path = dir.path().join("output.lz4");
std::fs::write(&src_path, data).unwrap();
let prefs = Prefs::default();
let _result = compress_filename_legacy(
src_path.to_str().unwrap(),
dst_path.to_str().unwrap(),
clevel,
&prefs,
)
.unwrap();
std::fs::read(&dst_path).unwrap()
}
#[test]
fn magic_number_written_first() {
let output = compress_to_bytes(b"hello world", 1);
assert!(output.len() >= 4);
let magic = u32::from_le_bytes([output[0], output[1], output[2], output[3]]);
assert_eq!(magic, LEGACY_MAGICNUMBER);
}
#[test]
fn fast_mode_block_header_present() {
let data = vec![0u8; 1024];
let output = compress_to_bytes(&data, 1); assert!(output.len() > MAGICNUMBER_SIZE + LEGACY_BLOCK_HEADER_SIZE);
}
#[test]
fn hc_mode_block_header_present() {
let data = vec![0u8; 1024];
let output = compress_to_bytes(&data, 9); assert!(output.len() > MAGICNUMBER_SIZE + LEGACY_BLOCK_HEADER_SIZE);
}
#[test]
fn hc_produces_output_no_larger_than_fast_for_compressible() {
let data = vec![b'A'; 16 * 1024];
let fast_out = compress_to_bytes(&data, 1);
let hc_out = compress_to_bytes(&data, 9);
assert!(
hc_out.len() <= fast_out.len(),
"HC ({}) should not be larger than fast ({})",
hc_out.len(),
fast_out.len()
);
}
#[test]
fn round_trip_fast_mode() {
let original = b"The quick brown fox jumps over the lazy dog.";
let compressed = compress_to_bytes(original, 1);
assert!(compressed.len() >= 8);
let block_size =
u32::from_le_bytes([compressed[4], compressed[5], compressed[6], compressed[7]])
as usize;
assert!(compressed.len() >= 8 + block_size);
let compressed_block = &compressed[8..8 + block_size];
let decompressed =
crate::block::decompress_block_to_vec(compressed_block, original.len() * 2);
assert_eq!(&decompressed[..original.len()], original);
}
#[test]
fn round_trip_hc_mode() {
let original = b"The quick brown fox jumps over the lazy dog.";
let compressed = compress_to_bytes(original, 9);
assert!(compressed.len() >= 8);
let block_size =
u32::from_le_bytes([compressed[4], compressed[5], compressed[6], compressed[7]])
as usize;
assert!(compressed.len() >= 8 + block_size);
let compressed_block = &compressed[8..8 + block_size];
let decompressed =
crate::block::decompress_block_to_vec(compressed_block, original.len() * 2);
assert_eq!(&decompressed[..original.len()], original);
}
#[test]
fn bytes_read_matches_input_size() {
let data = b"sample data for size check";
let dir = tempfile::tempdir().unwrap();
let src_path = dir.path().join("input.bin");
let dst_path = dir.path().join("output.lz4");
std::fs::write(&src_path, data).unwrap();
let prefs = Prefs::default();
let result = compress_filename_legacy(
src_path.to_str().unwrap(),
dst_path.to_str().unwrap(),
1,
&prefs,
)
.unwrap();
assert_eq!(result.bytes_read, data.len() as u64);
assert!(result.bytes_written > MAGICNUMBER_SIZE as u64);
}
#[test]
fn compress_multiple_filenames_legacy_ok() {
let dir = tempfile::tempdir().unwrap();
let src1 = dir.path().join("a.txt");
let src2 = dir.path().join("b.txt");
std::fs::write(&src1, b"file a content").unwrap();
std::fs::write(&src2, b"file b content").unwrap();
let prefs = Prefs::default();
let srcs = [src1.to_str().unwrap(), src2.to_str().unwrap()];
let result = compress_multiple_filenames_legacy(&srcs, ".lz4", 1, &prefs);
assert!(result.is_ok());
assert!(dir.path().join("a.txt.lz4").exists());
assert!(dir.path().join("b.txt.lz4").exists());
}
#[test]
fn compress_nonexistent_src_returns_err() {
let dir = tempfile::tempdir().unwrap();
let dst_path = dir.path().join("out.lz4");
let prefs = Prefs::default();
let result = compress_filename_legacy(
"/nonexistent/file/that/cannot/exist.bin",
dst_path.to_str().unwrap(),
1,
&prefs,
);
assert!(result.is_err());
}
}