#![cfg(test)]
use super::*;
fn build_suffix_args(base_len: usize, args: &[String], sched_args: &[String]) -> Result<Vec<u8>> {
build_suffix(
base_len,
&SuffixParams {
args,
sched_args,
..Default::default()
},
)
}
fn build_initramfs(
payload: &Path,
extra_binaries: &[(&str, &Path)],
args: &[String],
) -> Result<Vec<u8>> {
let base = build_initramfs_base(payload, extra_binaries, &[], false)?;
let suffix = build_suffix_args(base.len(), args, &[])?;
let mut archive = Vec::with_capacity(base.len() + suffix.len());
archive.extend_from_slice(&base);
archive.extend_from_slice(&suffix);
Ok(archive)
}
fn cpio_entry_names(archive: &[u8]) -> Vec<String> {
let mut names = Vec::new();
let mut remaining: &[u8] = archive;
while let Ok(reader) = cpio::newc::Reader::new(remaining) {
let name = reader.entry().name().to_string();
if reader.entry().is_trailer() {
break;
}
names.push(name);
remaining = reader.finish().unwrap();
}
names
}
fn cpio_entries(archive: &[u8]) -> Vec<(String, u32, u32, u32)> {
let mut entries = Vec::new();
let mut remaining: &[u8] = archive;
while let Ok(reader) = cpio::newc::Reader::new(remaining) {
if reader.entry().is_trailer() {
break;
}
let name = reader.entry().name().to_string();
let size = reader.entry().file_size();
let mode = reader.entry().mode();
let ino = reader.entry().ino();
entries.push((name, size, mode, ino));
remaining = reader.finish().unwrap();
}
entries
}
#[test]
fn cpio_header_format() {
let mut archive = Vec::new();
write_entry(&mut archive, "test", b"hello", 0o100644).unwrap();
assert_eq!(&archive[..6], b"070701");
}
#[test]
fn cpio_trailer() {
let mut archive = Vec::new();
write_entry(&mut archive, "test", b"data", 0o100755).unwrap();
cpio::newc::trailer(&mut archive as &mut dyn std::io::Write).unwrap();
let s = String::from_utf8_lossy(&archive);
assert!(s.contains("TRAILER!!!"));
}
#[test]
fn build_initramfs_has_init() {
let exe = crate::resolve_current_exe().unwrap();
let initrd = build_initramfs(&exe, &[], &[]).unwrap();
let s = String::from_utf8_lossy(&initrd);
assert!(s.contains("init"), "should contain init entry");
assert!(s.contains("TRAILER!!!"));
}
#[test]
fn build_initramfs_base_is_valid_cpio() {
let exe = crate::resolve_current_exe().unwrap();
let initrd = build_initramfs_base(&exe, &[], &[], false).unwrap();
assert_eq!(&initrd[..6], b"070701");
let full = build_initramfs(&exe, &[], &[]).unwrap();
assert!(initrd.len() <= full.len());
}
#[test]
fn build_initramfs_padded() {
let exe = crate::resolve_current_exe().unwrap();
let initrd = build_initramfs(&exe, &[], &[]).unwrap();
assert_eq!(initrd.len() % 512, 0);
}
#[test]
fn initramfs_nonexistent_file() {
let result = build_initramfs(Path::new("/nonexistent"), &[], &[]);
assert!(result.is_err());
}
#[test]
fn initramfs_nonexistent_extra_binary() {
let exe = crate::resolve_current_exe().unwrap();
let result = build_initramfs(&exe, &[("bad", Path::new("/nonexistent"))], &[]);
assert!(result.is_err());
}
#[test]
fn initramfs_with_args() {
let exe = crate::resolve_current_exe().unwrap();
let args = vec!["run".into(), "--json".into(), "scenario".into()];
let initrd = build_initramfs(&exe, &[], &args).unwrap();
let s = String::from_utf8_lossy(&initrd);
assert!(s.contains("args"));
}
#[test]
fn initramfs_empty_args() {
let exe = crate::resolve_current_exe().unwrap();
let initrd = build_initramfs(&exe, &[], &[]).unwrap();
assert_eq!(initrd.len() % 512, 0);
}
#[test]
fn suffix_adds_args_and_trailer() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let args = vec!["run".into(), "--json".into()];
let suffix = build_suffix_args(base.len(), &args, &[]).unwrap();
let s = String::from_utf8_lossy(&suffix);
assert!(s.contains("args"), "suffix should contain args entry");
assert!(s.contains("TRAILER!!!"), "suffix should contain trailer");
assert_eq!(
(base.len() + suffix.len()) % 512,
0,
"base+suffix should be 512-byte aligned"
);
}
#[test]
fn split_matches_monolithic() {
let exe = crate::resolve_current_exe().unwrap();
let args = vec!["run".into(), "--json".into(), "scenario".into()];
let monolithic = build_initramfs(&exe, &[], &args).unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let suffix = build_suffix_args(base.len(), &args, &[]).unwrap();
let mut split = Vec::with_capacity(base.len() + suffix.len());
split.extend_from_slice(&base);
split.extend_from_slice(&suffix);
assert_eq!(
monolithic, split,
"split path should produce identical output"
);
}
#[test]
fn suffix_different_args_differ() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let a = build_suffix_args(base.len(), &["a".into()], &[]).unwrap();
let b = build_suffix_args(base.len(), &["b".into()], &[]).unwrap();
assert_ne!(a, b, "different args should produce different suffixes");
}
#[test]
fn suffix_empty_args() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let suffix = build_suffix_args(base.len(), &[], &[]).unwrap();
assert_eq!((base.len() + suffix.len()) % 512, 0);
let s = String::from_utf8_lossy(&suffix);
assert!(s.contains("TRAILER!!!"));
}
#[test]
fn suffix_with_sched_enable() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let sched_enable = vec!["echo 1 > /sys/kernel/sched_ext/enable".to_string()];
let suffix = build_suffix(
base.len(),
&SuffixParams {
sched_enable: &sched_enable,
..Default::default()
},
)
.unwrap();
let mut archive = Vec::with_capacity(base.len() + suffix.len());
archive.extend_from_slice(&base);
archive.extend_from_slice(&suffix);
let entries = cpio_entries(&archive);
let entry = entries
.iter()
.find(|(name, ..)| name == "sched_enable")
.expect("sched_enable entry missing");
assert_eq!(
entry.1 as usize,
sched_enable[0].len(),
"sched_enable size should match joined content length",
);
assert_eq!(entry.2, 0o100755, "sched_enable must be executable");
}
#[test]
fn suffix_with_sched_disable() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let sched_disable = vec!["echo 0 > /sys/kernel/sched_ext/enable".to_string()];
let suffix = build_suffix(
base.len(),
&SuffixParams {
sched_disable: &sched_disable,
..Default::default()
},
)
.unwrap();
let mut archive = Vec::with_capacity(base.len() + suffix.len());
archive.extend_from_slice(&base);
archive.extend_from_slice(&suffix);
let entries = cpio_entries(&archive);
let entry = entries
.iter()
.find(|(name, ..)| name == "sched_disable")
.expect("sched_disable entry missing");
assert_eq!(entry.1 as usize, sched_disable[0].len());
assert_eq!(entry.2, 0o100755, "sched_disable must be executable");
}
#[test]
fn suffix_with_exec_cmd() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let cmd = "/usr/bin/stress-ng --cpu 1 --timeout 5s";
let suffix = build_suffix(
base.len(),
&SuffixParams {
exec_cmd: Some(cmd),
..Default::default()
},
)
.unwrap();
let mut archive = Vec::with_capacity(base.len() + suffix.len());
archive.extend_from_slice(&base);
archive.extend_from_slice(&suffix);
let entries = cpio_entries(&archive);
let entry = entries
.iter()
.find(|(name, ..)| name == "exec_cmd")
.expect("exec_cmd entry missing");
assert_eq!(entry.1 as usize, cmd.len());
assert_eq!(entry.2, 0o100644, "exec_cmd must be a plain data file");
}
#[test]
fn suffix_omits_empty_optional_entries() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let suffix = build_suffix(base.len(), &SuffixParams::default()).unwrap();
let mut archive = Vec::with_capacity(base.len() + suffix.len());
archive.extend_from_slice(&base);
archive.extend_from_slice(&suffix);
let names = cpio_entry_names(&archive);
assert!(!names.iter().any(|n| n == "sched_enable"));
assert!(!names.iter().any(|n| n == "sched_disable"));
assert!(!names.iter().any(|n| n == "exec_cmd"));
assert!(!names.iter().any(|n| n.starts_with("staging/")));
}
#[test]
fn suffix_emits_per_staged_scheduler_args_entries() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let staged = vec![
(
"mitosis_args_a".to_string(),
vec!["--slice-us".to_string(), "5000".to_string()],
),
(
"mitosis_args_b".to_string(),
vec!["--slice-us".to_string(), "20000".to_string()],
),
];
let suffix = build_suffix(
base.len(),
&SuffixParams {
staged_sched_args: &staged,
..Default::default()
},
)
.unwrap();
let mut archive = Vec::with_capacity(base.len() + suffix.len());
archive.extend_from_slice(&base);
archive.extend_from_slice(&suffix);
let entries = cpio_entries(&archive);
for (name, args) in &staged {
let archive_path = format!("staging/schedulers/{name}/sched_args");
let entry = entries
.iter()
.find(|(n, ..)| n == &archive_path)
.unwrap_or_else(|| panic!("staged args entry missing for {archive_path}"));
let expected = args.join("\n");
assert_eq!(
entry.1 as usize,
expected.len(),
"{archive_path} size mismatch",
);
assert_eq!(
entry.2, 0o100644,
"{archive_path} must be a plain data file (parser reads, init does not exec)",
);
}
}
#[test]
fn suffix_skips_staged_entries_with_empty_args() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let staged = vec![
("populated".to_string(), vec!["--flag".to_string()]),
("empty".to_string(), vec![]),
];
let suffix = build_suffix(
base.len(),
&SuffixParams {
staged_sched_args: &staged,
..Default::default()
},
)
.unwrap();
let mut archive = Vec::with_capacity(base.len() + suffix.len());
archive.extend_from_slice(&base);
archive.extend_from_slice(&suffix);
let names = cpio_entry_names(&archive);
assert!(
names
.iter()
.any(|n| n == "staging/schedulers/populated/sched_args"),
"populated args must emit an entry: {names:?}"
);
assert!(
!names
.iter()
.any(|n| n == "staging/schedulers/empty/sched_args"),
"empty args must NOT emit a zero-byte entry: {names:?}"
);
}
#[test]
fn try_cow_overlay_rejects_cross_region_span() {
use vm_memory::{GuestAddress, GuestMemory};
let region_a_size: usize = 64 * 1024;
let region_b_size: usize = 64 * 1024;
let region_a_start: u64 = 0;
let region_b_start: u64 = 1 << 20; let mem = vm_memory::GuestMemoryMmap::<()>::from_ranges(&[
(GuestAddress(region_a_start), region_a_size),
(GuestAddress(region_b_start), region_b_size),
])
.unwrap();
assert!(
mem.get_slice(GuestAddress(region_a_start), region_a_size)
.is_ok(),
"full-region slice must succeed"
);
let overrun_start = region_a_start + (region_a_size as u64 / 2);
let overrun_len = region_a_size; assert!(
mem.get_slice(GuestAddress(overrun_start), overrun_len)
.is_err(),
"cross-boundary slice must fail"
);
let gap_addr = (region_a_start + region_a_size as u64) + 0x1000;
assert!(
mem.get_slice(GuestAddress(gap_addr), 4).is_err(),
"gap-start slice must fail"
);
}
#[test]
fn try_cow_overlay_preserves_adjacent_region_bytes() {
use vm_memory::{Bytes, GuestAddress, GuestMemory};
let region_a_size: usize = 64 * 1024;
let region_b_size: usize = 64 * 1024;
let region_a_start: u64 = 0;
let region_b_start: u64 = 1 << 20;
let mem = vm_memory::GuestMemoryMmap::<()>::from_ranges(&[
(GuestAddress(region_a_start), region_a_size),
(GuestAddress(region_b_start), region_b_size),
])
.unwrap();
let marker: Vec<u8> = (0..region_b_size).map(|i| (i & 0xff) as u8).collect();
mem.write_slice(&marker, GuestAddress(region_b_start))
.unwrap();
let overrun_load_addr = region_a_start;
let overrun_len = (region_b_start + region_b_size as u64) as usize;
assert!(
mem.get_slice(GuestAddress(overrun_load_addr), overrun_len)
.is_err(),
"oversized overlay must be rejected before MAP_FIXED"
);
let mut readback = vec![0u8; region_b_size];
mem.read_slice(&mut readback, GuestAddress(region_b_start))
.unwrap();
assert_eq!(
readback, marker,
"region B must be untouched when bounds check rejects cow_overlay"
);
}
#[test]
fn load_initramfs_parts_sequential() {
let part1 = vec![0xAAu8; 4096];
let part2 = vec![0xBBu8; 512];
let mem =
vm_memory::GuestMemoryMmap::<()>::from_ranges(&[(vm_memory::GuestAddress(0), 16 << 20)])
.unwrap();
let (addr, size) = load_initramfs_parts(&mem, &[&part1, &part2], 0x200000).unwrap();
assert_eq!(addr, 0x200000);
assert_eq!(size, 4608);
let mut buf = vec![0u8; 4608];
use vm_memory::{Bytes, GuestAddress};
mem.read_slice(&mut buf, GuestAddress(0x200000)).unwrap();
assert_eq!(&buf[..4096], &part1[..]);
assert_eq!(&buf[4096..], &part2[..]);
}
#[test]
fn resolve_shared_libs_nonexistent_returns_error() {
let result = resolve_shared_libs(Path::new("/nonexistent/binary"));
assert!(result.is_err());
}
#[test]
fn resolve_shared_libs_non_elf_returns_empty() {
let _tempfile_keep_alive = tempfile::Builder::new()
.prefix("ktstr-test-resolve-nonelf-")
.tempfile()
.unwrap();
let tmp = _tempfile_keep_alive.path();
std::fs::write(tmp, b"not an elf").unwrap();
let result = resolve_shared_libs(tmp).unwrap();
assert!(result.found.is_empty());
assert!(result.missing.is_empty());
}
#[test]
fn resolve_shared_libs_dynamic_binary() {
let sh = Path::new("/bin/sh");
if sh.exists() {
let shared = resolve_shared_libs(sh).unwrap();
if !shared.found.is_empty() {
assert!(
shared.found.iter().any(|(g, _)| g.contains("libc")),
"dynamic binary should depend on libc: {:?}",
shared.found
);
for (g, _) in &shared.found {
assert!(!g.starts_with('/'), "guest path should be relative: {g}");
}
}
}
}
#[test]
fn elf_dynamic_needed_extracts_sonames() {
let sh = Path::new("/bin/sh");
if !sh.exists() || !is_elf(sh) {
skip!("/bin/sh not ELF");
}
let data = std::fs::read(sh).unwrap();
let elf = goblin::elf::Elf::parse(&data).unwrap();
let needed: Vec<&str> = elf.libraries.clone();
assert!(
needed.iter().any(|n| n.contains("libc")),
"/bin/sh should need libc: {:?}",
needed
);
}
#[test]
fn resolve_soname_finds_libc() {
let result = resolve_soname("libc.so.6", &ElfSearchPaths::default(), &[]);
assert!(
result.is_some(),
"should resolve libc.so.6 via default paths"
);
assert!(result.unwrap().is_file());
}
#[test]
fn resolve_soname_rpath_beats_runpath_when_both_present() {
let tmp = tempfile::TempDir::new().unwrap();
let rpath_dir = tmp.path().join("rpath");
let runpath_dir = tmp.path().join("runpath");
std::fs::create_dir_all(&rpath_dir).unwrap();
std::fs::create_dir_all(&runpath_dir).unwrap();
let soname = "libktstrfake-rpath-beats-runpath.so.1";
std::fs::write(rpath_dir.join(soname), b"rpath-copy").unwrap();
std::fs::write(runpath_dir.join(soname), b"runpath-copy").unwrap();
let paths = ElfSearchPaths {
rpath: vec![rpath_dir.clone()],
runpath: vec![runpath_dir.clone()],
};
let got = resolve_soname(soname, &paths, &[]).expect("should resolve");
assert_eq!(
got,
rpath_dir.join(soname),
"DT_RPATH must be preferred over DT_RUNPATH when both are \
populated (the LD_LIBRARY_PATH step separates them)"
);
}
#[test]
fn resolve_soname_runpath_beats_interp_hints() {
let tmp = tempfile::TempDir::new().unwrap();
let runpath_dir = tmp.path().join("runpath");
let interp_dir = tmp.path().join("interp");
std::fs::create_dir_all(&runpath_dir).unwrap();
std::fs::create_dir_all(&interp_dir).unwrap();
let soname = "libktstrfake-runpath-beats-interp.so.1";
std::fs::write(runpath_dir.join(soname), b"runpath-copy").unwrap();
std::fs::write(interp_dir.join(soname), b"interp-copy").unwrap();
let paths = ElfSearchPaths {
rpath: Vec::new(),
runpath: vec![runpath_dir.clone()],
};
let got =
resolve_soname(soname, &paths, std::slice::from_ref(&interp_dir)).expect("should resolve");
assert_eq!(
got,
runpath_dir.join(soname),
"DT_RUNPATH must be searched before interp-relative hints"
);
}
#[test]
fn resolve_soname_rpath_only_wins_when_runpath_empty() {
let tmp = tempfile::TempDir::new().unwrap();
let rpath_dir = tmp.path().join("rpath-legacy");
let interp_dir = tmp.path().join("interp");
std::fs::create_dir_all(&rpath_dir).unwrap();
std::fs::create_dir_all(&interp_dir).unwrap();
let soname = "libktstrfake-rpath-legacy.so.1";
std::fs::write(rpath_dir.join(soname), b"rpath-copy").unwrap();
std::fs::write(interp_dir.join(soname), b"interp-copy").unwrap();
let paths = ElfSearchPaths {
rpath: vec![rpath_dir.clone()],
runpath: Vec::new(),
};
let got =
resolve_soname(soname, &paths, std::slice::from_ref(&interp_dir)).expect("should resolve");
assert_eq!(
got,
rpath_dir.join(soname),
"legacy binary with DT_RPATH (no DT_RUNPATH) must resolve \
via DT_RPATH, not interp hints"
);
}
#[test]
fn suffix_with_sched_args() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let sched_args = vec!["--enable-borrow".into(), "--llc".into()];
let suffix = build_suffix_args(base.len(), &[], &sched_args).unwrap();
let s = String::from_utf8_lossy(&suffix);
assert!(
s.contains("sched_args"),
"suffix should contain sched_args entry"
);
assert!(s.contains("TRAILER!!!"));
assert_eq!((base.len() + suffix.len()) % 512, 0);
}
#[test]
fn suffix_without_sched_args_omits_entry() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let suffix = build_suffix_args(base.len(), &[], &[]).unwrap();
let s = String::from_utf8_lossy(&suffix);
assert!(
!s.contains("sched_args"),
"empty sched_args should not produce entry"
);
}
#[test]
fn shm_segment_name_format() {
let name = shm_segment_name(0xDEADBEEF);
assert!(name.starts_with("/ktstr-base-"));
assert!(name.contains("deadbeef"));
}
#[test]
fn is_deleted_self_returns_false_for_nonexistent() {
assert!(!is_deleted_self(Path::new("/nonexistent/binary")));
}
#[test]
fn is_deleted_self_returns_false_for_current() {
let exe = crate::resolve_current_exe().unwrap();
assert!(!is_deleted_self(&exe));
}
#[test]
fn shm_store_load_unlink_roundtrip() {
let hash = 0xABCD_EF01_2345_6789u64;
let data = vec![0x42u8; 1024];
shm_store_base(hash, &data).unwrap();
let loaded = shm_load_base(hash);
assert!(loaded.is_some());
assert_eq!(loaded.unwrap().as_ref(), &data[..]);
shm_unlink_base(hash);
assert!(shm_load_base(hash).is_none());
}
#[test]
fn shm_load_nonexistent_returns_none() {
let hash = 0xFFFF_FFFF_FFFF_FFFFu64;
shm_unlink_base(hash); assert!(shm_load_base(hash).is_none());
}
#[test]
fn shm_store_last_writer_wins_even_with_size_change() {
let hash = 0x1234_5678_9ABC_DEF0u64;
let d1 = vec![0x11u8; 64];
let d2 = vec![0x22u8; 128];
shm_store_base(hash, &d1).unwrap();
shm_store_base(hash, &d2).unwrap();
let loaded = shm_load_base(hash);
assert!(loaded.is_some());
assert_eq!(loaded.unwrap().as_ref(), &d2[..]);
shm_unlink_base(hash);
}
#[test]
fn shm_segment_name_unique_per_hash() {
let n1 = shm_segment_name(0);
let n2 = shm_segment_name(1);
assert_ne!(n1, n2);
assert!(n1.starts_with("/ktstr-base-"));
assert!(n2.starts_with("/ktstr-base-"));
}
#[test]
fn shm_unlink_nonexistent_is_noop() {
shm_unlink_base(0xDEAD_DEAD_DEAD_DEADu64);
}
#[test]
fn mapped_shm_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<MappedShm>();
}
#[test]
fn shm_load_base_holds_lock_until_drop() {
let hash = 0xD0D0_BEEF_F00D_BA5Eu64;
shm_unlink_base(hash); shm_store_base(hash, &vec![0x55u8; 256]).unwrap();
let loaded = shm_load_base(hash).expect("load must succeed");
let name = shm_segment_name(hash);
let fd2 = rustix::shm::open(
name.as_str(),
rustix::shm::OFlags::RDONLY,
rustix::fs::Mode::empty(),
)
.expect("second shm_open must succeed");
let err = rustix::fs::flock(&fd2, rustix::fs::FlockOperation::NonBlockingLockExclusive);
assert!(
matches!(err, Err(e) if e == rustix::io::Errno::WOULDBLOCK),
"LOCK_EX|LOCK_NB must be blocked by the live reader's LOCK_SH (got {err:?})",
);
drop(fd2);
drop(loaded);
let fd3 = rustix::shm::open(
name.as_str(),
rustix::shm::OFlags::RDONLY,
rustix::fs::Mode::empty(),
)
.expect("third shm_open must succeed");
rustix::fs::flock(&fd3, rustix::fs::FlockOperation::NonBlockingLockExclusive)
.expect("LOCK_EX|LOCK_NB must succeed after the MappedShm is dropped");
rustix::fs::flock(&fd3, rustix::fs::FlockOperation::Unlock).ok();
drop(fd3);
shm_unlink_base(hash);
}
#[test]
fn strip_debug_current_exe() {
let exe = crate::resolve_current_exe().unwrap();
let data = strip_debug(&exe).unwrap();
assert!(!data.is_empty());
assert_eq!(&data[..4], b"\x7fELF");
}
#[test]
fn strip_debug_nonexistent_fails() {
let result = strip_debug(Path::new("/nonexistent/binary"));
assert!(result.is_err());
}
#[test]
fn build_initramfs_base_contains_init() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let s = String::from_utf8_lossy(&base);
assert!(s.contains("init"), "base should contain init entry");
}
#[test]
fn build_initramfs_base_includes_extra_shared_libs() {
let exe = crate::resolve_current_exe().unwrap();
let sched = crate::test_support::require_binary("scx-ktstr");
let extras: Vec<(&str, &Path)> = vec![("scheduler", sched.as_path())];
let base = build_initramfs_base(&exe, &extras, &[], false).unwrap();
let s = String::from_utf8_lossy(&base);
assert!(
s.contains("lib64/libelf"),
"initramfs with scx-ktstr extra should contain libelf; \
resolved libs: {:?}",
resolve_shared_libs(sched.as_path()).unwrap().found
);
}
#[test]
fn load_initramfs_to_memory() {
let data = vec![0xAA; 4096];
let mem =
vm_memory::GuestMemoryMmap::<()>::from_ranges(&[(vm_memory::GuestAddress(0), 16 << 20)])
.unwrap();
let (addr, size) = load_initramfs_parts(&mem, &[&data], 0x200000).unwrap();
assert_eq!(addr, 0x200000);
assert_eq!(size, 4096);
let mut buf = vec![0u8; 4096];
use vm_memory::{Bytes, GuestAddress};
mem.read_slice(&mut buf, GuestAddress(0x200000)).unwrap();
assert_eq!(buf, data);
}
#[test]
fn busybox_with_include_files() {
let exe = crate::resolve_current_exe().unwrap();
let tmp_dir = tempfile::TempDir::new().unwrap();
let tmp = tmp_dir.path().join("included");
std::fs::write(&tmp, b"hello").unwrap();
let includes: Vec<(&str, &Path)> = vec![("include-files/test.txt", tmp.as_path())];
let base = build_initramfs_base(&exe, &[], &includes, true).unwrap();
let names = cpio_entry_names(&base);
assert!(
names.iter().any(|n| n == "bin/busybox"),
"busybox=true should have bin/busybox entry: {:?}",
names
);
}
#[test]
fn include_files_no_busybox_when_empty() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let names = cpio_entry_names(&base);
assert!(
!names.iter().any(|n| n == "bin/busybox"),
"busybox=false should not have bin/busybox entry: {:?}",
names
);
}
#[test]
fn include_files_preserves_mode() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let tmp = tmp_dir.path().join("script");
std::fs::write(&tmp, b"script content").unwrap();
std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o100755)).unwrap();
let exe = crate::resolve_current_exe().unwrap();
let includes: Vec<(&str, &Path)> = vec![("include-files/run.sh", tmp.as_path())];
let base = build_initramfs_base(&exe, &[], &includes, true).unwrap();
let s = String::from_utf8_lossy(&base);
assert!(
s.contains("include-files/run.sh"),
"include path should appear in cpio"
);
}
#[test]
fn include_files_elf_gets_shared_libs() {
let sh = Path::new("/bin/sh");
if !sh.exists() {
skip!("/bin/sh not found");
}
if !is_elf(sh) {
skip!("/bin/sh is not ELF");
}
let exe = crate::resolve_current_exe().unwrap();
let includes: Vec<(&str, &Path)> = vec![("include-files/sh", sh)];
let base = build_initramfs_base(&exe, &[], &includes, true).unwrap();
let s = String::from_utf8_lossy(&base);
let shared = resolve_shared_libs(sh).unwrap();
if !shared.found.is_empty() {
assert!(
shared.found.iter().any(|(g, _)| s.contains(g.as_str())),
"include ELF shared libs should appear in archive: {:?}",
shared.found
);
}
}
#[test]
fn include_files_non_elf_no_shared_libs() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let tmp = tmp_dir.path().join("hello.sh");
std::fs::write(&tmp, b"#!/bin/sh\necho hello\n").unwrap();
let exe = crate::resolve_current_exe().unwrap();
let includes: Vec<(&str, &Path)> = vec![("include-files/hello.sh", tmp.as_path())];
let base = build_initramfs_base(&exe, &[], &includes, true).unwrap();
let s = String::from_utf8_lossy(&base);
assert!(s.contains("include-files/hello.sh"));
}
#[test]
fn include_files_adds_directory_entries() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let tmp = tmp_dir.path().join("file.txt");
std::fs::write(&tmp, b"data").unwrap();
let exe = crate::resolve_current_exe().unwrap();
let includes: Vec<(&str, &Path)> =
vec![("include-files/subdir/nested/file.txt", tmp.as_path())];
let base = build_initramfs_base(&exe, &[], &includes, true).unwrap();
let s = String::from_utf8_lossy(&base);
assert!(s.contains("include-files"), "should have include-files dir");
assert!(
s.contains("include-files/subdir"),
"should have subdir entry"
);
assert!(
s.contains("include-files/subdir/nested"),
"should have nested subdir entry"
);
assert!(s.contains("bin"), "should have bin dir for busybox");
}
#[test]
fn is_elf_detects_elf_binary() {
let exe = crate::resolve_current_exe().unwrap();
assert!(is_elf(&exe), "test binary should be ELF");
}
#[test]
fn is_elf_rejects_non_elf() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let tmp = tmp_dir.path().join("not-elf");
std::fs::write(&tmp, b"not an elf file").unwrap();
assert!(!is_elf(&tmp));
}
#[test]
fn is_elf_rejects_short_file() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let tmp = tmp_dir.path().join("short-elf");
std::fs::write(&tmp, b"ab").unwrap();
assert!(!is_elf(&tmp));
}
#[test]
fn is_elf_nonexistent_returns_false() {
assert!(!is_elf(Path::new("/nonexistent/file")));
}
#[test]
fn include_files_rejects_path_traversal() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let tmp = tmp_dir.path().join("payload");
std::fs::write(&tmp, b"data").unwrap();
let exe = crate::resolve_current_exe().unwrap();
let includes: Vec<(&str, &Path)> = vec![("include-files/../etc/passwd", tmp.as_path())];
let result = build_initramfs_base(&exe, &[], &includes, true);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains(".."),
"error should mention path traversal: {err}"
);
}
#[test]
fn include_files_rejects_fifo() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let fifo_path = tmp_dir.path().join("fifo");
let c_path = std::ffi::CString::new(fifo_path.to_str().unwrap()).unwrap();
let rc = unsafe { libc::mkfifo(c_path.as_ptr(), 0o644) };
assert_eq!(
rc,
0,
"ktstr: mkfifo({}) failed -- test infrastructure broken",
fifo_path.display(),
);
let exe = crate::resolve_current_exe().unwrap();
let includes: Vec<(&str, &Path)> = vec![("include-files/pipe", fifo_path.as_path())];
let result = build_initramfs_base(&exe, &[], &includes, true);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("not a regular file"),
"error should reject FIFO: {err}"
);
}
#[test]
fn include_files_rejects_directory() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let dir_path = tmp_dir.path().join("mydir");
std::fs::create_dir(&dir_path).unwrap();
let exe = crate::resolve_current_exe().unwrap();
let includes: Vec<(&str, &Path)> = vec![("include-files/mydir", dir_path.as_path())];
let result = build_initramfs_base(&exe, &[], &includes, true);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("not a regular file"),
"error should reject directory: {err}"
);
}
#[test]
fn busybox_independent_of_include_files() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], true).unwrap();
let names = cpio_entry_names(&base);
assert!(
names.iter().any(|n| n == "bin/busybox"),
"busybox=true should have bin/busybox entry even without includes: {:?}",
names
);
}
#[test]
fn parse_ld_so_cache_finds_libc() {
let cache = parse_ld_so_cache(Path::new("/etc/ld.so.cache"));
assert!(
cache.contains_key("libc.so.6"),
"ld.so.cache should contain libc.so.6: found {} entries",
cache.len(),
);
let path = &cache["libc.so.6"];
assert!(
path.is_file(),
"cached libc path should exist: {}",
path.display()
);
}
#[test]
fn parse_ld_so_cache_nonexistent_returns_empty() {
let cache = parse_ld_so_cache(Path::new("/nonexistent/ld.so.cache"));
assert!(cache.is_empty());
}
#[test]
fn parse_ld_so_cache_bad_magic_returns_empty() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let tmp = tmp_dir.path().join("ldcache");
std::fs::write(&tmp, b"not a valid cache file").unwrap();
let cache = parse_ld_so_cache(&tmp);
assert!(cache.is_empty());
}
#[test]
fn parse_ld_so_cache_truncated_returns_empty() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let tmp = tmp_dir.path().join("ldcache");
let mut data = LD_CACHE_MAGIC.to_vec();
data.extend_from_slice(&[0u8; 10]); std::fs::write(&tmp, &data).unwrap();
let cache = parse_ld_so_cache(&tmp);
assert!(cache.is_empty());
}
#[test]
fn ld_so_cache_consistent_with_resolve_soname() {
let result = resolve_soname("libc.so.6", &ElfSearchPaths::default(), &[]);
assert!(
result.is_some(),
"resolve_soname should find libc.so.6 (cache or paths)"
);
assert!(result.unwrap().is_file());
}
#[test]
fn no_duplicate_cpio_entries() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let entries = cpio_entries(&base);
let mut seen = std::collections::HashSet::new();
let mut duplicates = Vec::new();
for (name, size, mode, ino) in &entries {
if !seen.insert(name.clone()) {
duplicates.push((name.clone(), *size, *mode, *ino));
}
}
assert!(
duplicates.is_empty(),
"archive contains duplicate entries: {:?}",
duplicates
);
}
#[test]
fn no_duplicate_entries_with_include_files() {
let exe = crate::resolve_current_exe().unwrap();
let tmp_dir_guard = tempfile::TempDir::new().unwrap();
let tmp_dir = tmp_dir_guard.path();
let lib_data = vec![0xCCu8; 4096];
let f1 = tmp_dir.join("libcustom1.so");
let f2 = tmp_dir.join("libcustom2.so");
let f3 = tmp_dir.join("libcustom3.so");
std::fs::write(&f1, &lib_data).unwrap();
std::fs::write(&f2, &lib_data).unwrap();
std::fs::write(&f3, &lib_data).unwrap();
let includes: Vec<(&str, &Path)> = vec![
("usr/local/custom/platform/lib/libcustom1.so", f1.as_path()),
("usr/local/custom/platform/lib/libcustom2.so", f2.as_path()),
("usr/local/custom/platform/lib/libcustom3.so", f3.as_path()),
];
let base = build_initramfs_base(&exe, &[], &includes, false).unwrap();
let entries = cpio_entries(&base);
let entry_names: Vec<&str> = entries.iter().map(|(n, _, _, _)| n.as_str()).collect();
for (archive_path, _) in &includes {
assert!(
entry_names.contains(archive_path),
"missing include file entry '{}'; archive entries: {:?}",
archive_path,
entry_names
);
}
for (archive_path, _) in &includes {
let entry = entries.iter().find(|(n, _, _, _)| n == archive_path);
assert!(
entry.is_some_and(|(_, size, _, _)| *size == lib_data.len() as u32),
"include file '{}' has wrong size: {:?}",
archive_path,
entry
);
}
assert!(entry_names.contains(&"usr"), "missing 'usr' dir entry");
assert!(
entry_names.contains(&"usr/local"),
"missing 'usr/local' dir entry"
);
assert!(
entry_names.contains(&"usr/local/custom"),
"missing 'usr/local/custom' dir entry"
);
assert!(
entry_names.contains(&"usr/local/custom/platform"),
"missing 'usr/local/custom/platform' dir entry"
);
assert!(
entry_names.contains(&"usr/local/custom/platform/lib"),
"missing 'usr/local/custom/platform/lib' dir entry"
);
let dir_pos = entries
.iter()
.position(|(n, _, _, _)| n == "usr/local/custom/platform/lib")
.unwrap();
for (archive_path, _) in &includes {
let file_pos = entries
.iter()
.position(|(n, _, _, _)| n == *archive_path)
.unwrap();
assert!(
dir_pos < file_pos,
"directory entry must precede file '{}': dir at {}, file at {}",
archive_path,
dir_pos,
file_pos
);
}
let mut seen = std::collections::HashSet::new();
let mut duplicates = Vec::new();
for (name, _, _, _) in &entries {
if !seen.insert(name.clone()) {
duplicates.push(name.clone());
}
}
assert!(
duplicates.is_empty(),
"duplicate entries in archive: {:?}",
duplicates
);
}
#[test]
fn include_elf_shared_libs_all_present_in_archive() {
let sh_path = Path::new("/bin/sh");
let sh_resolved = std::fs::canonicalize(sh_path).unwrap_or_else(|_| sh_path.to_path_buf());
let sh = sh_resolved.as_path();
if !sh.exists() || !is_elf(sh) {
skip!("/bin/sh not available or not ELF");
}
let exe = crate::resolve_current_exe().unwrap();
let includes: Vec<(&str, &Path)> = vec![("include-files/sh", sh)];
let base = build_initramfs_base(&exe, &[], &includes, false).unwrap();
let entries = cpio_entries(&base);
let entry_map: std::collections::HashMap<&str, (u32, u32, u32)> = entries
.iter()
.map(|(n, s, m, i)| (n.as_str(), (*s, *m, *i)))
.collect();
let shared = resolve_shared_libs(sh).unwrap();
for (guest_path, _host_path) in &shared.found {
assert!(
entry_map.contains_key(guest_path.as_str()),
"shared lib '{}' missing from archive; entries: {:?}",
guest_path,
entries
.iter()
.map(|(n, _, _, _)| n.as_str())
.collect::<Vec<_>>()
);
let (size, _, _) = entry_map[guest_path.as_str()];
assert!(
size > 0,
"shared lib '{}' has zero size in archive",
guest_path
);
}
assert!(
entry_map.contains_key("include-files/sh"),
"include file itself missing from archive"
);
}
#[test]
fn all_inode_zero_entries_have_nlink_one() {
let exe = crate::resolve_current_exe().unwrap();
let base = build_initramfs_base(&exe, &[], &[], false).unwrap();
let mut remaining: &[u8] = base.as_slice();
while let Ok(reader) = cpio::newc::Reader::new(remaining) {
if reader.entry().is_trailer() {
break;
}
let name = reader.entry().name().to_string();
let ino = reader.entry().ino();
let nlink = reader.entry().nlink();
assert_eq!(
ino, 0,
"entry '{}' has non-zero inode {}: risk of kernel hardlink confusion",
name, ino
);
assert_eq!(
nlink, 1,
"entry '{}' has nlink {}: kernel only hardlinks when nlink >= 2",
name, nlink
);
remaining = reader.finish().unwrap();
}
}
#[test]
fn lz4_legacy_compress_format() {
let data = vec![0xAAu8; 4096];
let compressed = lz4_legacy_compress(&data);
assert_eq!(
&compressed[..4],
&LZ4_LEGACY_MAGIC,
"output must start with LZ4 legacy magic 0x184C2102"
);
let chunk_size = u32::from_le_bytes(compressed[4..8].try_into().unwrap()) as usize;
assert!(
chunk_size > 0 && chunk_size < data.len(),
"compressed chunk should be non-empty and smaller than input: {}",
chunk_size
);
let decompressed = lz4_flex::block::decompress(&compressed[8..8 + chunk_size], data.len())
.expect("lz4 block decompress failed");
assert_eq!(decompressed, data);
}
#[test]
fn lz4_legacy_compress_large_input_splits_chunks() {
let data = vec![0xBBu8; LZ4_CHUNK_SIZE + 1024];
let compressed = lz4_legacy_compress(&data);
assert_eq!(&compressed[..4], &LZ4_LEGACY_MAGIC);
let mut pos = 4;
let mut chunk_count = 0;
let mut total_decompressed = Vec::new();
while pos + 4 <= compressed.len() {
let chunk_size = u32::from_le_bytes(compressed[pos..pos + 4].try_into().unwrap()) as usize;
if chunk_size == 0 {
break;
}
pos += 4;
let remaining_uncompressed = data.len() - total_decompressed.len();
let expected_chunk_len = remaining_uncompressed.min(LZ4_CHUNK_SIZE);
let decompressed =
lz4_flex::block::decompress(&compressed[pos..pos + chunk_size], expected_chunk_len)
.expect("lz4 block decompress failed");
total_decompressed.extend_from_slice(&decompressed);
pos += chunk_size;
chunk_count += 1;
}
assert!(
chunk_count >= 2,
"input > 8MB should produce >= 2 chunks, got {}",
chunk_count
);
assert_eq!(total_decompressed, data);
}
#[test]
fn lz4_legacy_compress_empty_input() {
let compressed = lz4_legacy_compress(&[]);
assert_eq!(compressed, LZ4_LEGACY_MAGIC);
}
fn build_synthetic_cpio(total_size: usize) -> Vec<u8> {
let mut archive = Vec::new();
write_entry(&mut archive, "lib", &[], 0o40755).unwrap();
write_entry(&mut archive, "data", &[], 0o40755).unwrap();
let mut rng_state = 0x12345678u64;
let entry_size = 256 * 1024; let mut entry_num = 0;
while archive.len() + entry_size < total_size {
let mut payload = vec![0u8; entry_size];
for byte in &mut payload {
rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
*byte = (rng_state >> 33) as u8;
}
let name = format!("lib/test_{entry_num:04}.so");
write_entry(&mut archive, &name, &payload, 0o100755).unwrap();
entry_num += 1;
}
if archive.len() < total_size {
let remaining = total_size - archive.len() - 200; let remaining = remaining.min(total_size);
let mut payload = vec![0u8; remaining];
for byte in &mut payload {
rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
*byte = (rng_state >> 33) as u8;
}
write_entry(&mut archive, "data/fill.bin", &payload, 0o100644).unwrap();
}
cpio::newc::trailer(&mut archive as &mut dyn std::io::Write).unwrap();
let pad = (512 - (archive.len() % 512)) % 512;
archive.extend(std::iter::repeat_n(0u8, pad));
archive
}
fn simulate_kernel_unlz4(input: &[u8]) -> Result<Vec<u8>, String> {
const UNCOMP_CHUNK_SIZE: usize = 8 << 20;
if input.len() < 4 {
return Err("input too short for magic".into());
}
let mut inp = 0usize; let mut size = input.len() as isize;
let magic = u32::from_le_bytes(input[inp..inp + 4].try_into().unwrap());
if magic != 0x184C2102 {
return Err(format!("invalid header: 0x{magic:08X}"));
}
inp += 4;
size -= 4;
let mut output = Vec::new();
loop {
if size < 4 {
break;
}
let chunksize = u32::from_le_bytes(input[inp..inp + 4].try_into().unwrap()) as usize;
if chunksize == 0x184C2102 {
inp += 4;
size -= 4;
continue;
}
if chunksize == 0 {
break;
}
inp += 4;
size -= 4;
let chunk_data = &input[inp..inp + chunksize];
let decompressed = lz4_flex::block::decompress(chunk_data, UNCOMP_CHUNK_SIZE)
.map_err(|e| format!("LZ4_decompress_safe failed: {e}"))?;
output.extend_from_slice(&decompressed);
size -= chunksize as isize;
if size == 0 {
break;
} else if size < 0 {
return Err("data corrupted: size went negative".into());
}
inp += chunksize;
}
Ok(output)
}
#[test]
fn lz4_legacy_kernel_unlz4_roundtrip() {
let small = build_synthetic_cpio(1 << 20); let compressed = lz4_legacy_compress(&small);
let decompressed =
simulate_kernel_unlz4(&compressed).expect("kernel unlz4 simulation failed on small input");
assert_eq!(decompressed, small);
let large = build_synthetic_cpio(10 << 20); let compressed = lz4_legacy_compress(&large);
let decompressed = simulate_kernel_unlz4(&compressed)
.expect("kernel unlz4 simulation failed on multi-chunk input");
assert_eq!(decompressed, large);
}
#[test]
fn lz4_legacy_kernel_unlz4_concatenated() {
let base = build_synthetic_cpio(2 << 20); let suffix_data = b"arg1\narg2\narg3\n";
let lz4_base = lz4_legacy_compress(&base);
let lz4_suffix = lz4_legacy_compress(suffix_data);
let mut combined = Vec::with_capacity(lz4_base.len() + lz4_suffix.len());
combined.extend_from_slice(&lz4_base);
combined.extend_from_slice(&lz4_suffix);
let decompressed = simulate_kernel_unlz4(&combined)
.expect("kernel unlz4 simulation failed on concatenated streams");
let mut expected = Vec::with_capacity(base.len() + suffix_data.len());
expected.extend_from_slice(&base);
expected.extend_from_slice(suffix_data);
assert_eq!(decompressed, expected);
}
#[test]
fn lz4_legacy_compress_c_compat() {
let lz4_check = std::process::Command::new("lz4").arg("--version").output();
if lz4_check.is_err() {
skip!("lz4 CLI not found");
}
let data = build_synthetic_cpio(2 << 20); let compressed = lz4_legacy_compress(&data);
let _compressed_keep_alive = tempfile::Builder::new()
.prefix("ktstr-test-lz4-compat-compressed-")
.suffix(".lz4")
.tempfile()
.unwrap();
let _decompressed_keep_alive = tempfile::Builder::new()
.prefix("ktstr-test-lz4-compat-decompressed-")
.suffix(".bin")
.tempfile()
.unwrap();
let compressed_path = _compressed_keep_alive.path();
let decompressed_path = _decompressed_keep_alive.path();
std::fs::write(compressed_path, &compressed).unwrap();
let output = std::process::Command::new("lz4")
.args(["-d", "-f", "--no-frame-crc"])
.arg(compressed_path)
.arg(decompressed_path)
.output()
.expect("lz4 -d failed to execute");
assert!(
output.status.success(),
"lz4 -d failed: stderr={}",
String::from_utf8_lossy(&output.stderr),
);
let result = std::fs::read(decompressed_path).unwrap();
assert_eq!(result.len(), data.len(), "decompressed size mismatch");
assert_eq!(&result[..], &data[..], "decompressed content mismatch");
}
#[test]
fn lz4_legacy_reference_cross_compat() {
let lz4_check = std::process::Command::new("lz4").arg("--version").output();
if lz4_check.is_err() {
skip!("lz4 CLI not found");
}
let data = build_synthetic_cpio(2 << 20);
let _input_keep_alive = tempfile::Builder::new()
.prefix("ktstr-test-lz4-ref-input-")
.suffix(".bin")
.tempfile()
.unwrap();
let _ref_keep_alive = tempfile::Builder::new()
.prefix("ktstr-test-lz4-ref-")
.suffix(".lz4")
.tempfile()
.unwrap();
let input_path = _input_keep_alive.path();
let ref_path = _ref_keep_alive.path();
std::fs::write(input_path, &data).unwrap();
let ref_output = std::process::Command::new("lz4")
.args(["-l", "-f"])
.arg(input_path)
.arg(ref_path)
.output()
.expect("lz4 -l failed to execute");
assert!(
ref_output.status.success(),
"lz4 -l failed: stderr={}",
String::from_utf8_lossy(&ref_output.stderr),
);
let ref_compressed = std::fs::read(ref_path).unwrap();
let ref_decompressed = simulate_kernel_unlz4(&ref_compressed)
.expect("kernel unlz4 simulation failed on lz4 -l output");
assert_eq!(
ref_decompressed, data,
"reference lz4 -l roundtrip mismatch"
);
let our_compressed = lz4_legacy_compress(&data);
let _our_lz4_keep_alive = tempfile::Builder::new()
.prefix("ktstr-test-lz4-ref-ours-")
.suffix(".lz4")
.tempfile()
.unwrap();
let _our_decompressed_keep_alive = tempfile::Builder::new()
.prefix("ktstr-test-lz4-ref-ours-decompressed-")
.suffix(".bin")
.tempfile()
.unwrap();
let our_lz4_path = _our_lz4_keep_alive.path();
let our_decompressed_path = _our_decompressed_keep_alive.path();
std::fs::write(our_lz4_path, &our_compressed).unwrap();
let our_output = std::process::Command::new("lz4")
.args(["-d", "-f", "--no-frame-crc"])
.arg(our_lz4_path)
.arg(our_decompressed_path)
.output()
.expect("lz4 -d on our output failed to execute");
assert!(
our_output.status.success(),
"lz4 -d on our output failed: stderr={}",
String::from_utf8_lossy(&our_output.stderr),
);
let our_result = std::fs::read(our_decompressed_path).unwrap();
assert_eq!(our_result, data, "our lz4 output cross-compat mismatch");
}