use std::collections::BTreeMap;
use std::fs;
use std::io::{self, Read, Write};
use std::path::Path;
use std::sync::Mutex;
use std::time::SystemTime;
use rayon::prelude::*;
use crate::frame::compress::lz4f_compress_begin;
use crate::frame::header::lz4f_compress_frame_bound;
use crate::frame::types::{BlockMode, ContentChecksum};
use crate::frame::{lz4f_compress_frame_using_cdict, Lz4FCDict};
use crate::io::compress_frame::{compress_frame_chunk, CfcParameters, CompressResources};
use crate::io::file_io::{open_dst_file, open_src_file, NUL_MARK, STDIN_MARK};
use crate::io::prefs::{display_level, Prefs, KB, MB};
use crate::util::set_file_stat;
use crate::xxhash::Xxh32State;
const CHUNK_SIZE: usize = 4 * MB;
const PREFIX_SIZE: usize = 64 * KB;
struct SyncCDictPtr(*const Lz4FCDict);
unsafe impl Send for SyncCDictPtr {}
unsafe impl Sync for SyncCDictPtr {}
impl SyncCDictPtr {
#[inline]
fn as_ptr(&self) -> *const Lz4FCDict {
self.0
}
}
struct WriteRegister {
expected_rank: u64,
pending: Mutex<BTreeMap<u64, Vec<u8>>>,
total_csize: u64,
block_size: usize,
}
impl WriteRegister {
fn new(block_size: usize) -> Self {
WriteRegister {
expected_rank: 0,
pending: Mutex::new(BTreeMap::new()),
total_csize: 0,
block_size,
}
}
fn insert(&self, chunk_id: u64, data: Vec<u8>) {
self.pending.lock().unwrap().insert(chunk_id, data);
}
fn drain_in_order(
&mut self,
write_fn: &mut dyn FnMut(&[u8]) -> io::Result<()>,
) -> io::Result<()> {
let mut pending = self.pending.lock().unwrap();
while let Some(entry) = pending.first_entry() {
let id = *entry.key();
if id != self.expected_rank {
break;
}
let data = entry.remove();
self.total_csize += data.len() as u64;
drop(pending); write_fn(&data)?;
{
let processed = self.expected_rank * self.block_size as u64;
let ratio = if processed > 0 {
self.total_csize as f64 / processed as f64 * 100.0
} else {
0.0
};
display_level(
2,
&format!("\rRead : {} MiB ==> {:.2}% ", processed >> 20, ratio),
);
}
self.expected_rank += 1;
pending = self.pending.lock().unwrap();
}
Ok(())
}
}
fn read_to_capacity(reader: &mut dyn Read, buf: &mut [u8]) -> io::Result<usize> {
let mut total = 0;
while total < buf.len() {
match reader.read(&mut buf[total..]) {
Ok(0) => break,
Ok(n) => total += n,
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
Ok(total)
}
fn copy_file_stat(src: &str, dst: &str) -> io::Result<()> {
let m = fs::metadata(src)?;
let mtime = m.modified().unwrap_or(SystemTime::UNIX_EPOCH);
#[cfg(unix)]
let (uid, gid, mode) = {
use std::os::unix::fs::MetadataExt;
(m.uid(), m.gid(), m.mode())
};
#[cfg(not(unix))]
let (uid, gid, mode) = (0u32, 0u32, 0o644u32);
set_file_stat(Path::new(dst), mtime, uid, gid, mode)
}
struct Chunk {
data: Vec<u8>,
prefix: Option<Vec<u8>>,
}
pub fn compress_filename_mt(
in_stream_size: &mut u64,
ress: &mut CompressResources,
src_filename: &str,
dst_filename: &str,
compression_level: i32,
io_prefs: &Prefs,
) -> io::Result<()> {
let mut src_reader = open_src_file(src_filename)?;
let dst_file = open_dst_file(dst_filename, io_prefs)?;
let dst_is_stdout = dst_file.is_stdout;
let mut dst_writer: Box<dyn Write> = Box::new(dst_file);
let mut prefs = ress.prepared_prefs;
prefs.compression_level = compression_level;
if io_prefs.content_size_flag {
let file_size = if src_filename != STDIN_MARK {
fs::metadata(src_filename).map(|m| m.len()).unwrap_or(0)
} else {
0
};
prefs.frame_info.content_size = file_size;
if file_size == 0 {
display_level(3, "Warning : cannot determine input content size \n");
}
}
let cdict_ptr = ress.cdict_ptr();
let mut first_buf = vec![0u8; CHUNK_SIZE];
let read_size = read_to_capacity(&mut *src_reader, &mut first_buf)?;
first_buf.truncate(read_size);
let mut filesize: u64 = read_size as u64;
let mut compressedfilesize: u64 = 0;
if read_size < CHUNK_SIZE {
let max_dst = lz4f_compress_frame_bound(read_size, Some(&prefs));
let mut dst_buf = vec![0u8; max_dst];
let c_size = lz4f_compress_frame_using_cdict(
&mut ress.ctx,
&mut dst_buf,
&first_buf,
cdict_ptr,
Some(&prefs),
)
.map_err(|e| io::Error::other(format!("Compression failed: {}", e)))?;
compressedfilesize = c_size as u64;
display_level(
2,
&format!(
"\rRead : {} MiB ==> {:.2}% ",
filesize >> 20,
compressedfilesize as f64 / filesize.max(1) as f64 * 100.0,
),
);
dst_writer.write_all(&dst_buf[..c_size]).map_err(|_| {
io::Error::new(
io::ErrorKind::WriteZero,
"Write error : failed writing single-block compressed frame",
)
})?;
} else {
let linked_blocks = prefs.frame_info.block_mode == BlockMode::Linked;
let use_checksum = prefs.frame_info.content_checksum_flag == ContentChecksum::Enabled;
let header_size = lz4f_compress_begin(&mut ress.ctx, &mut ress.dst_buffer, Some(&prefs))
.map_err(|e| io::Error::other(format!("File header generation failed : {}", e)))?;
dst_writer
.write_all(&ress.dst_buffer[..header_size])
.map_err(|_| {
io::Error::new(
io::ErrorKind::WriteZero,
"Write error : cannot write header",
)
})?;
compressedfilesize += header_size as u64;
if use_checksum {
prefs.frame_info.content_checksum_flag = ContentChecksum::Disabled;
}
let batch_size = (io_prefs.nb_workers as usize).max(1);
let max_cblock_size = lz4f_compress_frame_bound(CHUNK_SIZE, Some(&prefs));
let sync_cdict = SyncCDictPtr(cdict_ptr);
let mut write_register = WriteRegister::new(CHUNK_SIZE);
let mut xxh32 = if use_checksum {
let mut h = Xxh32State::new(0);
h.update(&first_buf);
Some(h)
} else {
None
};
let mut last_suffix: Option<Vec<u8>> = if linked_blocks && read_size >= PREFIX_SIZE {
Some(first_buf[read_size - PREFIX_SIZE..].to_vec())
} else {
None
};
let mut pending: Option<Chunk> = Some(Chunk {
data: first_buf,
prefix: None,
});
let mut eof = false;
loop {
let mut batch: Vec<Chunk> = Vec::with_capacity(batch_size);
if let Some(c) = pending.take() {
let short = c.data.len() < CHUNK_SIZE;
batch.push(c);
if short {
eof = true;
}
}
while !eof && batch.len() < batch_size {
let mut buf = vec![0u8; CHUNK_SIZE];
let n = read_to_capacity(&mut *src_reader, &mut buf)?;
if n == 0 {
eof = true;
break;
}
buf.truncate(n);
filesize += n as u64;
if let Some(ref mut h) = xxh32 {
h.update(&buf);
}
let prefix = last_suffix.take();
if linked_blocks && n >= PREFIX_SIZE {
last_suffix = Some(buf[n - PREFIX_SIZE..].to_vec());
}
let short = n < CHUNK_SIZE;
batch.push(Chunk { data: buf, prefix });
if short {
eof = true;
}
}
if batch.is_empty() {
break;
}
let batch_results: Vec<io::Result<Vec<u8>>> = batch
.into_par_iter()
.map(|chunk| -> io::Result<Vec<u8>> {
let mut dst_buf = vec![0u8; max_cblock_size];
let params = CfcParameters {
prefs: &prefs,
cdict: sync_cdict.as_ptr(),
};
let c_size = compress_frame_chunk(
¶ms,
&mut dst_buf,
&chunk.data,
chunk.prefix.as_deref(),
)?;
dst_buf.truncate(c_size);
Ok(dst_buf)
})
.collect();
for result in batch_results {
let c_data = result?;
write_register.insert(write_register.expected_rank, c_data);
write_register.drain_in_order(&mut |bytes| {
dst_writer.write_all(bytes).map_err(|_| {
io::Error::new(
io::ErrorKind::WriteZero,
"Write error : cannot write compressed block",
)
})
})?;
}
if eof {
break;
}
}
compressedfilesize += write_register.total_csize;
let mut end_buf = [0u8; 8];
let end_size = if use_checksum {
if let Some(h) = xxh32 {
let crc = h.digest();
end_buf[4..8].copy_from_slice(&crc.to_le_bytes());
8
} else {
4
}
} else {
4
};
dst_writer.write_all(&end_buf[..end_size]).map_err(|_| {
io::Error::new(
io::ErrorKind::WriteZero,
"Write error : cannot write end of frame",
)
})?;
compressedfilesize += end_size as u64;
}
drop(dst_writer);
if src_filename != STDIN_MARK && !dst_is_stdout && dst_filename != NUL_MARK {
let _ = copy_file_stat(src_filename, dst_filename);
}
if io_prefs.remove_src_file && src_filename != STDIN_MARK {
fs::remove_file(src_filename).map_err(|e| {
io::Error::new(e.kind(), format!("Remove error : {}: {}", src_filename, e))
})?;
}
display_level(2, &format!("\r{:79}\r", ""));
display_level(
2,
&format!(
"Compressed {} bytes into {} bytes ==> {:.2}%\n",
filesize,
compressedfilesize,
compressedfilesize as f64 / filesize.max(1) as f64 * 100.0,
),
);
*in_stream_size = filesize;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::io::compress_frame::CompressResources;
use crate::io::prefs::Prefs;
use tempfile::TempDir;
#[test]
fn write_register_drains_in_order() {
let mut wr = WriteRegister::new(CHUNK_SIZE);
wr.insert(2, vec![2u8; 4]);
wr.insert(0, vec![0u8; 4]);
wr.insert(1, vec![1u8; 4]);
let mut written: Vec<u8> = Vec::new();
wr.drain_in_order(&mut |bytes| {
written.extend_from_slice(bytes);
Ok(())
})
.unwrap();
assert_eq!(&written[0..4], &[0u8; 4]);
assert_eq!(&written[4..8], &[1u8; 4]);
assert_eq!(&written[8..12], &[2u8; 4]);
assert_eq!(wr.expected_rank, 3);
assert_eq!(wr.total_csize, 12);
}
#[test]
fn write_register_stops_at_gap() {
let mut wr = WriteRegister::new(CHUNK_SIZE);
wr.insert(0, vec![0u8; 4]);
wr.insert(2, vec![2u8; 4]);
let mut written: Vec<u8> = Vec::new();
wr.drain_in_order(&mut |bytes| {
written.extend_from_slice(bytes);
Ok(())
})
.unwrap();
assert_eq!(written.len(), 4);
assert_eq!(wr.expected_rank, 1);
}
#[test]
fn compress_filename_mt_round_trip_small_file() {
let dir = TempDir::new().unwrap();
let src = dir.path().join("input.bin");
let dst = dir.path().join("output.lz4");
let original = b"Hello MT compression round-trip test!".repeat(100);
std::fs::write(&src, &original).unwrap();
let mut prefs = Prefs::default();
prefs.nb_workers = 2;
let mut ress = CompressResources::new(&prefs).expect("resources");
let mut in_size = 0u64;
compress_filename_mt(
&mut in_size,
&mut ress,
src.to_str().unwrap(),
dst.to_str().unwrap(),
1,
&prefs,
)
.expect("MT compress small");
assert_eq!(in_size, original.len() as u64);
assert!(dst.exists());
assert!(dst.metadata().unwrap().len() > 0);
}
#[test]
fn compress_filename_mt_round_trip_multi_block() {
let dir = TempDir::new().unwrap();
let src = dir.path().join("input_large.bin");
let dst = dir.path().join("output_large.lz4");
let pattern: Vec<u8> = (0u8..=255).cycle().take(5 * MB).collect();
std::fs::write(&src, &pattern).unwrap();
let mut prefs = Prefs::default();
prefs.nb_workers = 2;
let mut ress = CompressResources::new(&prefs).expect("resources");
let mut in_size = 0u64;
compress_filename_mt(
&mut in_size,
&mut ress,
src.to_str().unwrap(),
dst.to_str().unwrap(),
1,
&prefs,
)
.expect("MT compress multi-block");
assert_eq!(in_size, pattern.len() as u64);
assert!(dst.exists());
assert!(dst.metadata().unwrap().len() > 0);
}
}