use std::{cmp, ffi::CStr, io};
use libc::c_void;
use zstd_sys::{
ZSTD_CHAINLOG_MIN, ZSTD_CONTENTSIZE_ERROR, ZSTD_CONTENTSIZE_UNKNOWN,
ZSTD_DCtx_setMaxWindowSize, ZSTD_HASHLOG_MIN, ZSTD_SEARCHLOG_MIN, ZSTD_WINDOWLOG_MIN,
ZSTD_compress_advanced, ZSTD_compressBound, ZSTD_compressionParameters, ZSTD_createCCtx,
ZSTD_createDCtx, ZSTD_decompress_usingDict, ZSTD_findDecompressedSize, ZSTD_frameParameters,
ZSTD_freeCCtx, ZSTD_freeDCtx, ZSTD_getErrorName, ZSTD_isError, ZSTD_parameters, ZSTD_strategy,
};
const ZSTD_WINDOWLOG_MAX: u32 = 30;
const ZSTD_HASHLOG_MAX: u32 = 30;
fn log_base2(x: u64) -> u32 {
64 - x.leading_zeros()
}
fn clamp(v: u32, min: u32, max: u32) -> u32 {
cmp::max(min, cmp::min(v, max))
}
fn explain_error(code: usize) -> &'static str {
unsafe {
let name = ZSTD_getErrorName(code);
let cstr = CStr::from_ptr(name);
cstr.to_str().expect("zstd error is utf-8")
}
}
pub fn diff(base: &[u8], data: &[u8]) -> io::Result<Vec<u8>> {
let log = log_base2((data.len() + base.len() + 1) as u64);
let wlog = clamp(log, ZSTD_WINDOWLOG_MIN, ZSTD_WINDOWLOG_MAX);
let hlog = clamp(log, ZSTD_HASHLOG_MIN, ZSTD_HASHLOG_MAX);
let cparams = ZSTD_compressionParameters {
windowLog: wlog,
chainLog: ZSTD_CHAINLOG_MIN, hashLog: hlog,
searchLog: ZSTD_SEARCHLOG_MIN, minMatch: 7, targetLength: 0, strategy: ZSTD_strategy::ZSTD_fast,
};
let fparams = ZSTD_frameParameters {
contentSizeFlag: 1, checksumFlag: 0, noDictIDFlag: 1, };
let params = ZSTD_parameters {
cParams: cparams,
fParams: fparams,
};
unsafe {
let cctx = ZSTD_createCCtx();
if cctx.is_null() {
return Err(io::Error::other("cannot create CCtx"));
}
let max_outsize = ZSTD_compressBound(data.len());
let mut buf: Vec<u8> = vec![0; max_outsize];
let outsize = ZSTD_compress_advanced(
cctx,
buf.as_mut_ptr() as *mut c_void,
buf.len(),
data.as_ptr() as *const c_void,
data.len(),
base.as_ptr() as *const c_void,
base.len(),
params,
);
ZSTD_freeCCtx(cctx);
if ZSTD_isError(outsize) != 0 {
let msg = format!("cannot compress ({})", explain_error(outsize));
Err(io::Error::other(msg))
} else {
buf.set_len(outsize);
Ok(buf)
}
}
}
pub fn apply(base: &[u8], delta: &[u8]) -> io::Result<Vec<u8>> {
unsafe {
let dctx = ZSTD_createDCtx();
if dctx.is_null() {
return Err(io::Error::other("cannot create DCtx"));
}
ZSTD_DCtx_setMaxWindowSize(dctx, 1 << ZSTD_WINDOWLOG_MAX);
let size = ZSTD_findDecompressedSize(delta.as_ptr() as *const c_void, delta.len()) as usize;
if size == ZSTD_CONTENTSIZE_ERROR as usize || size == ZSTD_CONTENTSIZE_UNKNOWN as usize {
ZSTD_freeDCtx(dctx);
let msg = "cannot get decompress size";
return Err(io::Error::other(msg));
}
let mut buf = vec![0u8; size];
let outsize = ZSTD_decompress_usingDict(
dctx,
buf.as_mut_ptr() as *mut c_void,
size,
delta.as_ptr() as *const c_void,
delta.len(),
base.as_ptr() as *const c_void,
base.len(),
);
ZSTD_freeDCtx(dctx);
if ZSTD_isError(outsize) != 0 {
let msg = format!("cannot decompress ({})", explain_error(outsize));
Err(io::Error::other(msg))
} else if outsize != size {
let msg = format!("decompress size mismatch (expected {size}, got {outsize})");
Err(io::Error::other(msg))
} else {
Ok(buf)
}
}
}
#[cfg(test)]
mod tests {
use chacha20::ChaCha20Rng;
use quickcheck::quickcheck;
use rand::{Rng, SeedableRng};
use super::*;
fn check_round_trip(base: &[u8], data: &[u8]) -> bool {
let delta = diff(base, data).expect("delta");
let reconstructed = apply(base, &delta).expect("apply");
reconstructed[..] == data[..]
}
#[test]
fn test_round_trip_manual() {
assert!(check_round_trip(b"", b""));
assert!(check_round_trip(b"123", b""));
assert!(check_round_trip(b"", b"123"));
assert!(check_round_trip(b"1234567890", b"3"));
assert!(check_round_trip(b"3", b"1234567890"));
}
#[test]
fn test_delta_efficiency() {
let mut base = vec![0u8; 1000000];
let mut rng = ChaCha20Rng::from_seed([0; 32]);
rng.fill_bytes(&mut base);
let mut data = base.clone();
data[0] ^= 1;
data[10000] ^= 3;
data[900000] ^= 7;
let delta = diff(&base, &data).expect("diff");
assert!(delta.len() < 200);
}
quickcheck! {
fn test_round_trip_quickcheck(a: Vec<u8>, b: Vec<u8>) -> bool {
check_round_trip(&a, &b)
}
}
}