use std::io::Read as _;
use std::process::Command;
use fstool::block::{BlockDevice, Qcow2Backend};
use tempfile::NamedTempFile;
fn which(tool: &str) -> bool {
Command::new("sh")
.arg("-c")
.arg(format!("command -v {tool}"))
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[test]
fn opens_qemu_img_created_image() {
if !which("qemu-img") {
eprintln!("skipping: qemu-img not installed");
return;
}
let tmp = NamedTempFile::new().unwrap();
let out = Command::new("qemu-img")
.args(["create", "-q", "-f", "qcow2"])
.arg(tmp.path())
.arg("64M")
.output()
.unwrap();
assert!(
out.status.success(),
"qemu-img create failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let back = Qcow2Backend::open(tmp.path()).unwrap();
assert_eq!(back.total_size(), 64 * 1024 * 1024);
assert_eq!(back.header().cluster_size(), 65536);
assert_eq!(back.header().version, 3);
}
#[test]
fn read_back_pattern_via_qemu_img_convert() {
if !which("qemu-img") {
eprintln!("skipping: qemu-img not installed");
return;
}
let raw = NamedTempFile::new().unwrap();
{
use std::io::Write as _;
let mut f = std::fs::File::create(raw.path()).unwrap();
f.set_len(4 * 1024 * 1024).unwrap();
f.write_all(b"hello qcow2 reader\n").unwrap();
use std::io::Seek as _;
use std::io::SeekFrom;
f.seek(SeekFrom::Start(65500)).unwrap();
f.write_all(&[0xAB; 200]).unwrap();
f.seek(SeekFrom::Start(2 * 1024 * 1024)).unwrap();
f.write_all(b"halfway through\n").unwrap();
f.sync_all().unwrap();
}
let qcow = NamedTempFile::new().unwrap();
let out = Command::new("qemu-img")
.args(["convert", "-f", "raw", "-O", "qcow2"])
.arg(raw.path())
.arg(qcow.path())
.output()
.unwrap();
assert!(
out.status.success(),
"qemu-img convert failed:\n{}",
String::from_utf8_lossy(&out.stderr)
);
let mut back = Qcow2Backend::open(qcow.path()).unwrap();
assert_eq!(back.total_size(), 4 * 1024 * 1024);
let mut head = [0u8; 32];
back.read_at(0, &mut head).unwrap();
assert_eq!(&head[..19], b"hello qcow2 reader\n");
let mut straddle = [0u8; 200];
back.read_at(65500, &mut straddle).unwrap();
assert!(straddle.iter().all(|&b| b == 0xAB));
let mut mid = [0u8; 16];
back.read_at(2 * 1024 * 1024, &mut mid).unwrap();
assert_eq!(&mid, b"halfway through\n");
let mut tail = [0xffu8; 4096];
back.read_at(3 * 1024 * 1024, &mut tail).unwrap();
assert!(tail.iter().all(|&b| b == 0), "tail should be zero");
use std::io::Seek as _;
use std::io::SeekFrom;
back.seek(SeekFrom::Start(0)).unwrap();
let mut all = Vec::new();
back.read_to_end(&mut all).unwrap();
assert_eq!(all.len(), 4 * 1024 * 1024);
assert_eq!(&all[..19], b"hello qcow2 reader\n");
}
#[test]
fn create_then_qemu_img_check() {
if !which("qemu-img") {
eprintln!("skipping: qemu-img not installed");
return;
}
let tmp = NamedTempFile::new().unwrap();
{
let mut back = Qcow2Backend::create(tmp.path(), 64 * 1024 * 1024, 65536).unwrap();
back.write_at(0, b"hello fresh qcow2\n").unwrap();
back.write_at(1024 * 1024, &[0xCDu8; 128]).unwrap();
back.write_at(63 * 1024 * 1024, &[0xEFu8; 4096]).unwrap();
back.sync().unwrap();
}
let info = Command::new("qemu-img")
.args(["info", "--output=json"])
.arg(tmp.path())
.output()
.unwrap();
assert!(
info.status.success(),
"qemu-img info failed:\n{}",
String::from_utf8_lossy(&info.stderr)
);
let s = String::from_utf8_lossy(&info.stdout);
assert!(s.contains("\"virtual-size\": 67108864"), "info:\n{s}");
assert!(s.contains("\"format\": \"qcow2\""), "info:\n{s}");
let check = Command::new("qemu-img")
.arg("check")
.arg(tmp.path())
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&check.stdout);
let stderr = String::from_utf8_lossy(&check.stderr);
assert!(
check.status.success(),
"qemu-img check failed:\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let mut back = Qcow2Backend::open(tmp.path()).unwrap();
let mut head = [0u8; 32];
back.read_at(0, &mut head).unwrap();
assert_eq!(&head[..18], b"hello fresh qcow2\n");
let mut mid = [0u8; 128];
back.read_at(1024 * 1024, &mut mid).unwrap();
assert!(mid.iter().all(|&b| b == 0xCD));
let mut tail = [0u8; 4096];
back.read_at(63 * 1024 * 1024, &mut tail).unwrap();
assert!(tail.iter().all(|&b| b == 0xEF));
let mut zeros = [0xffu8; 1024];
back.read_at(8 * 1024 * 1024, &mut zeros).unwrap();
assert!(zeros.iter().all(|&b| b == 0));
}
#[test]
fn ext_build_into_qcow2() {
if !which("qemu-img") || !which("e2fsck") {
eprintln!("skipping: qemu-img or e2fsck missing");
return;
}
let srcdir = tempfile::tempdir().unwrap();
std::fs::write(srcdir.path().join("hello"), b"in qcow2\n").unwrap();
std::fs::create_dir(srcdir.path().join("etc")).unwrap();
std::fs::write(srcdir.path().join("etc/conf"), b"k=v\n").unwrap();
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("disk.qcow2");
let bin = env!("CARGO_BIN_EXE_fstool");
let r = Command::new(bin)
.args(["create", "-t", "ext4"])
.arg(srcdir.path())
.arg("-o")
.arg(&out)
.output()
.unwrap();
assert!(
r.status.success(),
"create failed:\n{}",
String::from_utf8_lossy(&r.stderr)
);
let chk = Command::new("qemu-img")
.arg("check")
.arg(&out)
.output()
.unwrap();
assert!(
chk.status.success(),
"qemu-img check failed:\n{}",
String::from_utf8_lossy(&chk.stdout)
);
let raw = dir.path().join("disk.raw");
let cv = Command::new("qemu-img")
.args(["convert", "-O", "raw"])
.arg(&out)
.arg(&raw)
.output()
.unwrap();
assert!(cv.status.success(), "qemu-img convert failed");
let fsck = Command::new("e2fsck")
.arg("-fn")
.arg(&raw)
.output()
.unwrap();
assert!(
fsck.status.success(),
"e2fsck on converted ext4 failed:\n{}",
String::from_utf8_lossy(&fsck.stdout)
);
let ls = Command::new(bin)
.arg("ls")
.arg(&out)
.arg("/")
.output()
.unwrap();
assert!(ls.status.success());
let s = String::from_utf8_lossy(&ls.stdout);
assert!(s.contains("hello"));
assert!(s.contains("etc"));
let cat = Command::new(bin)
.arg("cat")
.arg(&out)
.arg("/etc/conf")
.output()
.unwrap();
assert!(cat.status.success());
assert_eq!(cat.stdout, b"k=v\n");
}
#[test]
fn build_partitioned_qcow2() {
if !which("qemu-img") {
eprintln!("skipping: qemu-img not installed");
return;
}
let srcdir = tempfile::tempdir().unwrap();
std::fs::write(srcdir.path().join("hello"), b"in partition 2\n").unwrap();
let dir = tempfile::tempdir().unwrap();
let spec_path = dir.path().join("spec.toml");
std::fs::write(
&spec_path,
format!(
r#"
[image]
size = "128MiB"
partition_table = "gpt"
[[partitions]]
name = "EFI"
type = "esp"
size = "48MiB"
[partitions.filesystem]
type = "fat32"
volume_label = "EFI"
[[partitions]]
name = "root"
type = "linux"
size = "remaining"
[partitions.filesystem]
type = "ext4"
source = "{}"
block_size = 1024
"#,
srcdir.path().display()
),
)
.unwrap();
let out = dir.path().join("disk.qcow2");
let bin = env!("CARGO_BIN_EXE_fstool");
let r = Command::new(bin)
.arg("build")
.arg(&spec_path)
.arg("-o")
.arg(&out)
.output()
.unwrap();
assert!(
r.status.success(),
"build failed:\n{}",
String::from_utf8_lossy(&r.stderr)
);
let chk = Command::new("qemu-img")
.arg("check")
.arg(&out)
.output()
.unwrap();
assert!(
chk.status.success(),
"qemu-img check failed:\n{}",
String::from_utf8_lossy(&chk.stdout)
);
let info = Command::new(bin).arg("info").arg(&out).output().unwrap();
assert!(info.status.success());
let s = String::from_utf8_lossy(&info.stdout);
assert!(s.contains("partition table:"));
assert!(s.contains("EFI"));
assert!(s.contains("root"));
let mut p2 = std::ffi::OsString::from(&out);
p2.push(":2");
let ls = Command::new(bin)
.arg("ls")
.arg(&p2)
.arg("/")
.output()
.unwrap();
assert!(ls.status.success(), "ls :2 failed");
let s = String::from_utf8_lossy(&ls.stdout);
assert!(s.contains("hello"));
}
#[test]
fn open_image_dispatches_to_qcow2() {
if !which("qemu-img") {
eprintln!("skipping: qemu-img not installed");
return;
}
let tmp = NamedTempFile::new().unwrap();
Command::new("qemu-img")
.args(["create", "-q", "-f", "qcow2"])
.arg(tmp.path())
.arg("32M")
.output()
.unwrap();
let mut dev = fstool::block::open_image(tmp.path()).unwrap();
assert_eq!(dev.total_size(), 32 * 1024 * 1024);
let mut buf = [0xffu8; 1024];
dev.read_at(0, &mut buf).unwrap();
assert!(buf.iter().all(|&b| b == 0));
}