use arrayvec::{self, ArrayVec};
use crate::header::Yaz0Header;
use std::io::Write;
use std::sync::mpsc::{self, Sender};
use crate::Error;
pub struct Yaz0Writer<'a, W: 'a>
where
W: Write,
{
writer: &'a mut W,
}
#[derive(Debug, Clone, Copy)]
struct Run {
pub cursor: usize,
pub length: usize,
}
impl Run {
pub fn zero() -> Run {
Run {
cursor: 0,
length: 0,
}
}
pub fn swap_if_better(self, other: Run) -> Run {
if self.length > other.length {
self
} else {
other
}
}
}
#[derive(Debug)]
pub struct ProgressMsg {
pub read_head: usize,
}
fn find_naive_run(src: &[u8], cursor: usize, lookback: usize) -> Run {
let search_start = cursor.saturating_sub(lookback);
let mut run = Run::zero();
for search_head in search_start..cursor {
let mut runlength = 0;
while runlength < src.len() - cursor {
if src[search_head + runlength] != src[cursor + runlength] {
break;
}
runlength += 1;
}
run = run.swap_if_better(Run {
cursor: search_head,
length: runlength,
})
}
run
}
fn find_lookahead_run(src: &[u8], cursor: usize, lookback: usize) -> (bool, Run) {
let run = find_naive_run(src, cursor, lookback);
if run.length >= 3 {
let lookahead_run = find_naive_run(src, cursor + 1, lookback);
if lookahead_run.length >= run.length + 2 {
return (true, lookahead_run);
}
}
return (false, run);
}
fn write_run<A>(read_head: usize, run: &Run, destination: &mut ArrayVec<A>) -> usize
where
A: arrayvec::Array<Item = u8>,
{
let dist = read_head - run.cursor - 1;
if run.length >= 0x12 {
destination.push((dist as u32 >> 8) as u8);
destination.push((dist as u32 & 0xff) as u8);
let actual_runlength = run.length.min(0xff + 0x12); destination.push((actual_runlength - 0x12) as u8);
return actual_runlength;
} else {
destination.push(((run.length as u8 - 2) << 4) | (dist as u32 >> 8) as u8);
destination.push((dist as u32 & 0xff) as u8);
return run.length;
}
}
fn compress_lookaround(
src: &[u8],
level: CompressionLevel,
progress_tx: Sender<ProgressMsg>,
) -> Vec<u8> {
let quality = match level {
CompressionLevel::Naive { quality } => quality,
CompressionLevel::Lookahead { quality } => quality,
};
const MAX_LOOKBACK: usize = 0x1000;
let lookback = (MAX_LOOKBACK as f32 / (10. / quality as f32)).floor() as usize;
let mut lookahead_cache: Option<Run> = None;
let mut read_head = 0;
let mut encoded = Vec::new();
while read_head < src.len() {
let mut codon: u8 = 0x0;
let mut packets = ArrayVec::<[u8; 24]>::new();
let mut packet_n = 0;
while packet_n < 8 {
let (hit_lookahead, best_run) = if let Some(cache) = lookahead_cache.take() {
(false, cache)
} else {
match level {
CompressionLevel::Lookahead { .. } => {
find_lookahead_run(src, read_head, lookback)
}
CompressionLevel::Naive { .. } => {
(false, find_naive_run(src, read_head, lookback))
}
}
};
if hit_lookahead {
lookahead_cache = Some(best_run);
}
if best_run.length >= 3 && !hit_lookahead {
read_head += write_run(read_head, &best_run, &mut packets);
} else {
if read_head >= src.len() {
break;
}
packets.push(src[read_head]);
codon |= 0x80 >> packet_n;
read_head += 1;
}
packet_n += 1;
}
encoded.push(codon);
encoded.extend(&packets);
if read_head % 10 == 0 || read_head == src.len() - 1 {
let _ = progress_tx.send(ProgressMsg { read_head });
}
}
encoded
}
fn compress_with_progress(
data: &[u8],
level: CompressionLevel,
progress_tx: Sender<ProgressMsg>,
) -> Vec<u8> {
match level {
CompressionLevel::Naive { .. } | CompressionLevel::Lookahead { .. } => {
compress_lookaround(data, level, progress_tx)
}
}
}
fn compress(data: &[u8], level: CompressionLevel) -> Vec<u8> {
let (tx, _) = mpsc::channel();
compress_with_progress(data, level, tx)
}
impl<'a, W> Yaz0Writer<'a, W>
where
W: Write,
{
pub fn new(writer: &'a mut W) -> Yaz0Writer<W>
where
W: Write,
{
Yaz0Writer { writer }
}
pub fn compress_and_write(self, data: &[u8], level: CompressionLevel) -> Result<(), Error> {
let header = Yaz0Header::new(data.len());
header.write(self.writer)?;
let compressed = compress(data, level);
self.writer.write_all(&compressed)?;
Ok(())
}
pub fn compress_and_write_with_progress(
self,
data: &[u8],
level: CompressionLevel,
progress_tx: Sender<ProgressMsg>,
) -> Result<(), Error> {
let header = Yaz0Header::new(data.len());
header.write(self.writer)?;
let compressed = compress_with_progress(data, level, progress_tx);
self.writer.write_all(&compressed)?;
Ok(())
}
}
#[derive(Clone, Copy)]
pub enum CompressionLevel {
Naive {
quality: usize
},
Lookahead {
quality: usize
},
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
#[test]
#[cfg_attr(rustfmt, rustfmt_skip)] fn deflate_naive() {
const Q: CompressionLevel = CompressionLevel::Naive {quality: 10};
assert_eq!(compress(&[12, 34, 56], Q), [0xe0, 12, 34, 56]);
assert_eq!(
compress(&[0, 1, 2, 0xa, 0, 1, 2, 3, 0xb, 0, 1, 2, 3, 4, 5, 6, 7], Q),
[
0xf6, 0, 1, 2, 0xa,
0x10, 0x03,
3, 0xb,
0x20, 0x04,
0xf0, 4, 5, 6, 7,
]
);
}
#[test]
#[cfg_attr(rustfmt, rustfmt_skip)] fn deflate_with_lookahead() {
const Q: CompressionLevel = CompressionLevel::Lookahead {quality: 10};
assert_eq!(
compress(&[0, 0, 0, 0xa, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xa], Q),
[
0xfa, 0, 0, 0, 10, 0,
0x70, 0x00,
0xa,
]
);
}
#[test]
#[cfg_attr(rustfmt, rustfmt_skip)]
fn deflate_run() {
const Q: CompressionLevel = CompressionLevel::Lookahead {quality: 10};
assert_eq!(compress(&[0;30], Q), [0x80, 0, 0, 0, 11]);
}
#[test]
fn inverts() {
use crate::inflate::Yaz0Archive;
use rand::distributions::Standard;
use rand::{self, Rng};
use std::io::Cursor;
for _ in 0..10 {
let data: Vec<u8> = rand::thread_rng().sample_iter(&Standard).take(50).collect();
let mut deflated = Vec::new();
Yaz0Writer::new(&mut deflated)
.compress_and_write(&data, CompressionLevel::Lookahead { quality: 10 })
.expect("Could not deflate");
let inflated = Yaz0Archive::new(Cursor::new(deflated))
.expect("Error creating Yaz0Archive")
.decompress()
.expect("Error deflating Yaz0 archive");
assert_eq!(inflated, data);
}
}
#[test]
#[ignore]
fn inverts_test_file() {
use indicatif::{ProgressBar, ProgressDrawTarget};
use crate::inflate::Yaz0Archive;
use std::io::Cursor;
use std::thread;
let data: &[u8] = include_bytes!("../data/test");
let (tx, rx) = mpsc::channel::<ProgressMsg>();
let pb = ProgressBar::new(data.len() as u64);
pb.set_draw_target(ProgressDrawTarget::stdout());
thread::spawn(move || {
while let Ok(progress) = rx.recv() {
pb.set_position(progress.read_head as u64);
}
});
let mut deflated = Vec::new();
Yaz0Writer::new(&mut deflated)
.compress_and_write_with_progress(
&data,
CompressionLevel::Lookahead { quality: 10 },
tx,
)
.expect("Could not deflate");
let reader = Cursor::new(&deflated);
let inflated = Yaz0Archive::new(reader)
.expect("Error creating Yaz0Archive")
.decompress()
.expect("Error deflating Yaz0 archive");
println!(
"original: {:#x} / compressed (w/ header): {:#x} ({:.3}%)",
data.len(),
deflated.len(),
deflated.len() as f64 * 100. / data.len() as f64
);
assert_eq!(inflated, data);
}
}