use fs_core::{BlockRead, FileDevice};
#[cfg(any(target_os = "macos", target_os = "linux"))]
const IMAGE_BYTES: u64 = 4 * 1024 * 1024;
#[test]
fn a_regular_file_still_reports_its_metadata_length() {
let dir = std::env::temp_dir().join(format!("fs_core_dns_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create the fixture directory");
let full = dir.join("full.img");
std::fs::write(&full, vec![7u8; 3000]).expect("write the non-empty image");
let dev = FileDevice::open(&full).expect("open a regular file");
assert_eq!(
dev.size_bytes(),
3000,
"a regular file's size is its length"
);
let empty = dir.join("empty.img");
std::fs::write(&empty, []).expect("write the empty image");
let dev = FileDevice::open(&empty).expect("an empty regular file still opens");
assert_eq!(
dev.size_bytes(),
0,
"0 is the right answer for an empty regular file, and must stay an answer"
);
std::fs::remove_file(&full).ok();
std::fs::remove_file(&empty).ok();
std::fs::remove_dir(&dir).ok();
}
#[cfg(unix)]
#[test]
fn a_device_whose_size_cannot_be_measured_refuses_to_open() {
let err = match FileDevice::open("/dev/zero") {
Err(e) => e,
Ok(dev) => panic!(
"/dev/zero opened and reported {} bytes; a size nobody measured must not \
be handed out as though it were one",
dev.size_bytes()
),
};
assert!(
matches!(err, fs_core::Error::Io(_)),
"expected an Io error naming the failed measurement, got {err:?}"
);
}
#[cfg(target_os = "macos")]
mod macos {
use super::*;
use std::path::PathBuf;
use std::process::Command;
struct Attached {
dir: PathBuf,
dev: String,
}
impl Drop for Attached {
fn drop(&mut self) {
Command::new("hdiutil")
.args(["detach", &self.dev, "-force"])
.output()
.ok();
std::fs::remove_dir_all(&self.dir).ok();
}
}
fn attach() -> Attached {
attach_with_block_size(None)
}
fn attach_with_block_size(block_size: Option<u32>) -> Attached {
let dir = std::env::temp_dir().join(format!(
"fs_core_attach_{}_{:?}",
std::process::id(),
std::thread::current().id()
));
std::fs::create_dir_all(&dir).expect("create the fixture directory");
let img = dir.join("raw.img");
std::fs::write(&img, vec![0xABu8; IMAGE_BYTES as usize]).expect("write the raw image");
assert_eq!(
std::fs::metadata(&img).expect("stat the image").len(),
IMAGE_BYTES,
"the backing file is not the length this test assumes"
);
let mut cmd = Command::new("hdiutil");
cmd.args(["attach", "-nomount"]);
let bs;
if let Some(n) = block_size {
bs = n.to_string();
cmd.args(["-blocksize", &bs]);
}
cmd.args([
"-imagekey",
"diskimage-class=CRawDiskImage",
img.to_str().expect("image path is utf-8"),
]);
let out = cmd.output().expect("run hdiutil");
assert!(
out.status.success(),
"hdiutil attach failed, so no device node was tested: status={:?} stderr={}",
out.status.code(),
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
let dev = stdout
.split_whitespace()
.find(|t| t.starts_with("/dev/disk"))
.unwrap_or_else(|| panic!("hdiutil named no /dev/disk device; stdout was {stdout:?}"))
.to_string();
Attached { dir, dev }
}
#[test]
fn a_block_device_node_reports_its_real_length() {
let a = attach();
let dev = FileDevice::open(&a.dev).expect("open the block device node");
assert_eq!(
dev.size_bytes(),
IMAGE_BYTES,
"{} reported {} bytes; st_size for a device node is 0, so this is \
the metadata length leaking through rather than a measurement",
a.dev,
dev.size_bytes()
);
}
#[test]
fn a_character_device_node_reports_its_real_length() {
let a = attach();
let raw = a.dev.replace("/dev/disk", "/dev/rdisk");
let dev = FileDevice::open(&raw).expect("open the character device node");
assert_eq!(
dev.size_bytes(),
IMAGE_BYTES,
"{raw} reported {} bytes rather than its real length",
dev.size_bytes()
);
}
#[test]
fn the_block_size_comes_from_the_device_rather_than_a_constant() {
let a = attach_with_block_size(Some(4096));
let dev = FileDevice::open(&a.dev).expect("open the 4096-byte-block device node");
assert_eq!(
dev.size_bytes(),
IMAGE_BYTES,
"{} reported {} bytes; 524288 would mean the block count was \
multiplied by an assumed 512 rather than the device's own 4096",
a.dev,
dev.size_bytes()
);
}
#[test]
fn streaming_a_raw_device_yields_its_bytes_rather_than_nothing() {
use std::io::Read;
let a = attach();
let dev = FileDevice::open(&a.dev).expect("open the block device node");
let mut streamer = fs_core::BlockReadStreamer::new(dev);
let mut got = Vec::new();
streamer.read_to_end(&mut got).expect("stream the device");
assert_eq!(
got.len() as u64,
IMAGE_BYTES,
"read_to_end over a raw device returned {} bytes",
got.len()
);
assert!(
got.iter().all(|b| *b == 0xAB),
"the bytes streamed off the device are not the bytes written to it"
);
}
}
#[cfg(target_os = "linux")]
mod linux {
use super::*;
use std::path::PathBuf;
use std::process::Command;
fn is_root() -> bool {
unsafe { geteuid() == 0 }
}
fn can_attach_loop_devices() -> bool {
match std::fs::read_to_string("/proc/self/status") {
Ok(status) => cap_eff_has_sys_admin(&status).unwrap_or_else(is_root),
Err(_) => is_root(),
}
}
fn cap_eff_has_sys_admin(status: &str) -> Option<bool> {
const CAP_SYS_ADMIN: u32 = 21;
let hex = status
.lines()
.find_map(|line| line.strip_prefix("CapEff:"))?
.trim();
let bits = u64::from_str_radix(hex, 16).ok()?;
Some(bits & (1 << CAP_SYS_ADMIN) != 0)
}
#[test]
fn the_loop_capability_is_read_from_cap_eff_not_the_uid() {
let root_without_caps = "Name:\ttest\nUid:\t0\t0\t0\t0\nCapEff:\t0000000000000000\n";
assert_eq!(cap_eff_has_sys_admin(root_without_caps), Some(false));
let full = "CapEff:\t000001ffffffffff\n";
assert_eq!(cap_eff_has_sys_admin(full), Some(true));
let sys_admin_only = "CapEff:\t0000000000200000\n";
assert_eq!(cap_eff_has_sys_admin(sys_admin_only), Some(true));
let everything_but = "CapEff:\t000001ffffdfffff\n";
assert_eq!(cap_eff_has_sys_admin(everything_but), Some(false));
assert_eq!(cap_eff_has_sys_admin("Name:\ttest\n"), None);
}
unsafe extern "C" {
fn geteuid() -> u32;
}
struct Loop {
dir: PathBuf,
dev: String,
}
impl Drop for Loop {
fn drop(&mut self) {
Command::new("losetup")
.args(["-d", &self.dev])
.output()
.ok();
std::fs::remove_dir_all(&self.dir).ok();
}
}
fn attach() -> Loop {
let dir = std::env::temp_dir().join(format!("fs_core_loop_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create the fixture directory");
let img = dir.join("raw.img");
std::fs::write(&img, vec![0xABu8; IMAGE_BYTES as usize]).expect("write the raw image");
let out = Command::new("losetup")
.args(["--find", "--show", img.to_str().expect("path is utf-8")])
.output()
.expect("run losetup");
assert!(
out.status.success(),
"losetup failed, so no device node was tested: stderr={}",
String::from_utf8_lossy(&out.stderr)
);
let dev = String::from_utf8_lossy(&out.stdout).trim().to_string();
assert!(
dev.starts_with("/dev/loop"),
"losetup named {dev:?} rather than a loop device"
);
Loop { dir, dev }
}
#[test]
fn a_loop_device_reports_its_real_length() {
if !can_attach_loop_devices() {
assert!(
std::env::var_os("AM_FS_CORE_ALLOW_UNPRIVILEGED_SKIP").is_some(),
"the Linux device-size probe's SUCCESS path was NOT exercised: \
losetup needs CAP_SYS_ADMIN, which this process does not have \
(as root with capabilities dropped, or not as root). Run the suite \
with it (`cargo test --test device_node_size` as root) to cover it, \
or set AM_FS_CORE_ALLOW_UNPRIVILEGED_SKIP=1 to accept the gap. \
Note the FAILURE path of the same probe IS covered here \
unprivileged, by a_device_whose_size_cannot_be_measured_refuses_to_open."
);
return;
}
let l = attach();
let dev = FileDevice::open(&l.dev).expect("open the loop device");
assert_eq!(
dev.size_bytes(),
IMAGE_BYTES,
"{} reported {} bytes rather than its real length",
l.dev,
dev.size_bytes()
);
}
}