use std::ffi::OsString;
use super::*;
use crate::compat::host::Cores;
use crate::compat::zstd::cli;
struct Trickle {
data: Vec<u8>,
at: usize,
step: usize,
}
impl Read for Trickle {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let n = self.step.min(buf.len()).min(self.data.len() - self.at);
buf[..n].copy_from_slice(&self.data[self.at..self.at + n]);
self.at += n;
Ok(n)
}
}
fn pool(data: Vec<u8>, job: usize) -> ReadPool {
ReadPool::new(
Box::new(Trickle {
data,
at: 0,
step: 7,
}),
job,
)
}
#[test]
fn a_fill_reads_one_whole_job_when_fewer_than_asked_are_loaded() {
let mut rp = pool((0..100u8).collect(), 32);
assert_eq!(rp.fill(4).unwrap(), 32);
assert_eq!(rp.loaded().len(), 32);
assert_eq!(rp.fill(32).unwrap(), 0);
rp.consume(30);
assert_eq!(rp.fill(4).unwrap(), 32);
assert_eq!(rp.loaded(), &(30..64u8).collect::<Vec<_>>()[..]);
rp.consume(34);
assert_eq!(rp.fill(1000).unwrap(), 32);
rp.consume(32);
assert_eq!(rp.fill(1).unwrap(), 4);
rp.consume(4);
assert_eq!(rp.fill(1).unwrap(), 0);
assert_eq!(rp.fill(1).unwrap(), 0);
assert!(rp.loaded().is_empty());
}
struct Dir(std::path::PathBuf);
impl Dir {
fn new(tag: &str) -> Dir {
static N: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
let d = std::env::temp_dir().join(format!(
"stuffr-zstd-fileio-{tag}-{}-{}",
std::process::id(),
N.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
));
let _ = fs::remove_dir_all(&d);
fs::create_dir_all(&d).unwrap();
Dir(d)
}
}
impl Drop for Dir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn command(args: &[&str]) -> Command {
let args: Vec<OsString> = args.iter().map(OsString::from).collect();
let (mut out, mut err) = (Vec::new(), Vec::new());
cli::parse("zstd", &|_| None, &args, Cores::SANDBOX, &mut out, &mut err).expect("parses")
}
fn sparse_write(dir: &Dir, blocks: &[Vec<u8>]) -> (Vec<u8>, u64) {
let cmd = command(&["--sparse", "-d"]);
let mut host = Host::Real;
let (mut out, mut err) = (Vec::new(), Vec::new());
let mut io = Io::new(&cmd, &mut host, &mut out, &mut err);
assert_eq!(io.prefs.sparse, 2);
let path = dir.0.join("sparse");
io.sink = Sink::File(Box::new(FileSink {
w: FileW::Raw(File::create(&path).unwrap()),
tmp: None,
path: path.clone(),
name: b"sparse".to_vec(),
guard: None,
}));
for b in blocks {
io.write_block(b).unwrap();
}
io.sparse_end().unwrap();
let (closed, _) = io.close_sink().unwrap();
closed.unwrap();
drop(io);
assert!(err.is_empty());
#[cfg(unix)]
let allocated = {
use std::os::unix::fs::MetadataExt;
fs::metadata(&path).unwrap().blocks()
};
#[cfg(not(unix))]
let allocated = 0;
(fs::read(&path).unwrap(), allocated)
}
#[test]
fn the_sparse_writer_keeps_the_bytes_and_ends_with_an_explicit_zero() {
let d = Dir::new("sparse");
let mut a = vec![0u8; 100_000];
a[40_000] = 9;
a[40_001] = 8;
let b = vec![0u8; 70_003];
let mut c = vec![0u8; 13];
c[12] = 5;
let (bytes, _) = sparse_write(&d, &[a.clone(), b.clone(), c.clone()]);
let want: Vec<u8> = [a, b, c].concat();
assert_eq!(bytes.len(), want.len());
assert!(bytes == want);
let (bytes, _) = sparse_write(&d, &[vec![0u8; 5000]]);
assert_eq!(bytes, vec![0u8; 5000]);
let (bytes, _) = sparse_write(&d, &[vec![1, 0, 0]]);
assert_eq!(bytes, vec![1, 0, 0]);
}
#[test]
fn the_names_follow_the_suffix_rules() {
assert_eq!(compressed_name(b"a", None, ".zst"), b"a.zst");
assert_eq!(compressed_name(b"d/a", Some(b"o"), ".zst"), b"o/a.zst");
assert_eq!(compressed_name(b"d/a", Some(b"o/"), ".zst"), b"o/a.zst");
assert_eq!(compressed_name(STDINMARK, Some(b"o"), ".zst"), STDOUTMARK);
let cmd = command(&["-d"]);
let mut host = Host::Real;
let (mut out, mut err) = (Vec::new(), Vec::new());
let mut io = Io::new(&cmd, &mut host, &mut out, &mut err);
let mut name = |src: &[u8], dir: Option<&[u8]>| decompressed_name(&mut io, src, dir);
assert_eq!(name(b"a.zst", None).unwrap(), b"a");
assert_eq!(name(b"a.tzst", None).unwrap(), b"a.tar");
assert_eq!(name(b"a.zstd", None).unwrap(), b"a");
assert_eq!(name(b"a.tlz4", None).unwrap(), b"a.tar");
assert_eq!(name(b"d/a.gz", Some(b"o")).unwrap(), b"o/a");
assert!(name(b".zst", None).is_none());
assert!(name(b"a", None).is_none());
assert!(name(b"a.ZST", None).is_none());
assert!(name(b"d.x/a", None).is_none());
drop(io);
let err = String::from_utf8(err).unwrap();
assert_eq!(err.matches("unknown suffix").count(), 4, "{err}");
}
#[test]
fn parameter_bounds_are_libzstds() {
let ok = |o: &EncodeOptions| check_cparam_bounds(o).is_ok();
let with = |k: CParamKey, v: u32| EncodeOptions {
overrides: vec![(k, v)],
..EncodeOptions::default()
};
assert!(ok(&EncodeOptions::default()));
assert!(ok(&with(CParamKey::Strategy, 9)));
assert!(!ok(&with(CParamKey::Strategy, 10)));
assert!(!ok(&with(CParamKey::WindowLog, 9)));
assert!(ok(&with(CParamKey::WindowLog, 31)));
assert!(!ok(&with(CParamKey::MinMatch, 8)));
assert!(ok(&with(CParamKey::TargetLength, 1 << 17)));
assert!(!ok(&with(CParamKey::TargetLength, (1 << 17) + 1)));
let tcb = |v| EncodeOptions {
target_cblock_size: v,
..EncodeOptions::default()
};
assert!(ok(&tcb(1)));
assert!(ok(&tcb(u32::MAX)), "(int)-1 is raised to the minimum");
assert!(!ok(&tcb(200_000)));
let ldm = |l: Ldm| EncodeOptions {
ldm_params: l,
..EncodeOptions::default()
};
assert!(!ok(&ldm(Ldm {
hash_rate_log: 26,
..Ldm::default()
})));
assert!(ok(&ldm(Ldm {
hash_rate_log: 25,
..Ldm::default()
})));
}
#[test]
fn highbit64_is_fio_highbit64() {
assert_eq!(highbit64(1), 0);
assert_eq!(highbit64(2), 1);
assert_eq!(highbit64(3), 1);
assert_eq!(highbit64(1 << 31), 31);
assert_eq!(highbit64(u64::MAX), 63);
}
#[cfg(all(target_os = "linux", feature = "zstd-c"))]
fn sigint_blocked(tid: &str) -> Option<bool> {
let status = fs::read_to_string(format!("/proc/self/task/{tid}/status")).ok()?;
let hex = status
.lines()
.find_map(|l| l.strip_prefix("SigBlk:"))?
.trim();
let mask = u64::from_str_radix(hex, 16).expect("SigBlk is hex");
Some(mask & (1 << (libc::SIGINT - 1)) != 0)
}
#[cfg(all(target_os = "linux", feature = "zstd-c"))]
fn named_tasks(name: &str, me: &str) -> Vec<(String, bool)> {
let mut found = Vec::new();
for entry in fs::read_dir("/proc/self/task").unwrap() {
let tid = entry.unwrap().file_name().into_string().unwrap();
if tid == me {
continue;
}
let comm = fs::read_to_string(format!("/proc/self/task/{tid}/comm")).unwrap_or_default();
if comm.trim_end() != name {
continue;
}
if let Some(blocked) = sigint_blocked(&tid) {
found.push((tid, blocked));
}
}
found
}
#[cfg(all(target_os = "linux", feature = "zstd-c"))]
#[test]
fn libzstd_workers_are_born_with_sigint_blocked() {
const NAME: &str = "stuffr-zsigblk";
std::thread::Builder::new()
.name(NAME.into())
.spawn(|| {
let me = unsafe { libc::gettid() }.to_string();
unsafe {
let mut set: libc::sigset_t = std::mem::zeroed();
libc::sigemptyset(&mut set);
libc::sigaddset(&mut set, libc::SIGINT);
libc::pthread_sigmask(libc::SIG_UNBLOCK, &set, std::ptr::null_mut());
}
assert_eq!(
sigint_blocked(&me),
Some(false),
"the test starts unblocked"
);
let cmd = command(&["-T2"]);
assert_eq!(cmd.prefs.workers, 2);
let mut host = Host::Real;
let (mut out, mut err) = (Vec::new(), Vec::new());
let mut io = Io::new(&cmd, &mut host, &mut out, &mut err);
io.prefs.sparse = 0;
io.sink = Sink::Stdout;
let mut ress = io
.create_cresources(&cmd, FILESIZE_UNKNOWN, cmd.level)
.expect("the context");
let mut seed = 0x2545_f491_4f6c_dd1du64;
for (frame, len) in [(1, 64usize << 10), (2, 2 << 20)] {
let data: Vec<u8> = (0..len)
.map(|_| {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
b"abcdefgh ijklmnop"[(seed % 17) as usize]
})
.collect();
let mut rp = ReadPool::new(Box::new(io::Cursor::new(data)), CSTREAM_IN_SIZE);
let mut read = 0;
io.compress_zstd_frame(&mut ress, &mut rp, b"in", len as u64, &mut read)
.expect("compresses");
assert_eq!(read, len as u64);
let workers = named_tasks(NAME, &me);
if frame == 1 {
assert!(workers.is_empty(), "frame 1 started workers: {workers:?}");
} else {
assert!(workers.len() >= 2, "no libzstd workers seen: {workers:?}");
let unblocked: Vec<_> = workers.iter().filter(|(_, b)| !b).collect();
assert!(
unblocked.is_empty(),
"libzstd workers with SIGINT unblocked: {unblocked:?}"
);
}
assert_eq!(sigint_blocked(&me), Some(false), "the mask is restored");
}
drop(ress);
drop(io);
assert!(!out.is_empty());
})
.unwrap()
.join()
.unwrap();
}