#![cfg(unix)]
use std::path::{Path, PathBuf};
use std::process::Command;
use fstool::block::{BlockDevice, DmgBackend};
fn which(tool: &str) -> Option<PathBuf> {
let out = Command::new("sh")
.arg("-c")
.arg(format!("command -v {tool}"))
.output()
.ok()?;
if !out.status.success() {
return None;
}
let s = String::from_utf8(out.stdout).ok()?;
let p = s.trim();
if p.is_empty() { None } else { Some(p.into()) }
}
fn hdiutil_usable() -> bool {
Command::new("hdiutil")
.arg("help")
.output()
.map(|o| o.status.success() || o.status.code() == Some(0))
.unwrap_or(false)
}
fn skip_unless_macos_hdiutil() -> bool {
if !cfg!(target_os = "macos") {
eprintln!("skipping: DMG e2e requires macOS (hdiutil)");
return true;
}
if which("hdiutil").is_none() {
eprintln!("skipping: hdiutil not found on PATH");
return true;
}
if !hdiutil_usable() {
eprintln!("skipping: hdiutil refused to run `hdiutil help`");
return true;
}
false
}
fn create_hdiutil_dmg(dir: &Path, name: &str, format: &str) -> Option<PathBuf> {
let path = dir.join(format!("{name}.dmg"));
let out = Command::new("hdiutil")
.args([
"create",
"-size",
"8m",
"-fs",
"HFS+",
"-layout",
"NONE",
"-volname",
"FstoolDmgTest",
"-format",
format,
])
.arg(&path)
.output()
.expect("hdiutil create failed to spawn");
if !out.status.success() {
eprintln!(
"skipping {format}: hdiutil create refused:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
return None;
}
Some(path)
}
fn dmg_read_every_sector(format: &str) {
if skip_unless_macos_hdiutil() {
return;
}
let dir = tempfile::tempdir().unwrap();
let Some(path) = create_hdiutil_dmg(dir.path(), "rt", format) else {
return;
};
let mut dmg = match DmgBackend::open(&path) {
Ok(d) => d,
Err(e) => {
panic!("DmgBackend::open({format}) failed: {e}");
}
};
assert!(
dmg.total_size() > 0,
"DMG {format} virtual size is 0 — koly trailer parse failed?"
);
assert!(
dmg.chunk_count() > 0,
"DMG {format} has zero chunks — mish parse failed?"
);
const STRIDE: usize = 64 * 1024;
let size = dmg.total_size();
let mut buf = vec![0u8; STRIDE];
let mut off = 0u64;
while off < size {
let n = STRIDE.min((size - off) as usize);
dmg.read_at(off, &mut buf[..n]).unwrap_or_else(|e| {
panic!("DMG {format} read_at(off={off}, n={n}): {e}");
});
off += n as u64;
}
}
#[test]
fn dmg_udrw_reads_every_sector() {
dmg_read_every_sector("UDRW");
}
#[test]
fn dmg_udzo_zlib_reads_every_sector() {
dmg_read_every_sector("UDZO");
}
#[test]
fn dmg_udbz_bzip2_reads_every_sector() {
dmg_read_every_sector("UDBZ");
}
#[test]
fn dmg_ulfo_lzfse_reads_every_sector() {
dmg_read_every_sector("ULFO");
}
#[test]
fn dmg_udzo_hfs_plus_walks_through_fstool() {
if skip_unless_macos_hdiutil() {
return;
}
let dir = tempfile::tempdir().unwrap();
let Some(path) = create_hdiutil_dmg(dir.path(), "hfs", "UDZO") else {
return;
};
let mut dmg = DmgBackend::open(&path).expect("DmgBackend::open");
let hfs = fstool::fs::hfs_plus::HfsPlus::open(&mut dmg).expect("HfsPlus::open");
let entries = hfs
.list_path(&mut dmg, "/")
.expect("list_path / on DMG-wrapped HFS+");
eprintln!(
"DMG-wrapped HFS+ root entries ({}): {:?}",
entries.len(),
entries.iter().map(|e| e.name.clone()).collect::<Vec<_>>()
);
}