mod busybox;
mod util_linux;
pub use busybox::BusyboxBlockTools;
pub use util_linux::UtilLinuxBlockTools;
use std::path::Path;
use crate::error::{Result, SnapshotError};
pub trait BlockTools: Send + Sync {
fn attach_loop(&self, backing: &Path, read_only: bool) -> Result<String>;
fn detach_loop(&self, device: &str) -> Result<()>;
fn device_sectors(&self, device: &str) -> Result<u64>;
}
fn parse_loop_device(what: &str, stdout: &[u8]) -> Result<String> {
let text = String::from_utf8_lossy(stdout);
let device = text.trim();
match device.strip_prefix("/dev/loop") {
Some(index) if !index.is_empty() && index.bytes().all(|byte| byte.is_ascii_digit()) => {
Ok(device.to_owned())
}
_ => Err(SnapshotError::DeviceMapper(format!(
"{what} printed {device:?}, not a /dev/loopN device"
))),
}
}
fn verify_attached(tools: &dyn BlockTools, device: &str, backing: &Path) -> Result<()> {
match loop_backing_file(device)? {
Some(actual) => verify_reported_backing(tools, device, backing, &actual),
None => Ok(()),
}
}
fn verify_reported_backing(
tools: &dyn BlockTools,
device: &str,
backing: &Path,
actual: &str,
) -> Result<()> {
let matches = actual == backing.to_string_lossy()
|| std::fs::canonicalize(backing).is_ok_and(|path| path.to_string_lossy() == actual);
if matches {
return Ok(());
}
let detach_note = match tools.detach_loop(device) {
Ok(()) => String::new(),
Err(detach) => format!("; and detaching it again failed: {detach}"),
};
Err(SnapshotError::DeviceMapper(format!(
"{device} backs {actual}, not {}{detach_note}",
backing.display()
)))
}
pub(super) fn loop_backing_file(device: &str) -> Result<Option<String>> {
let Some(name) = Path::new(device).file_name().and_then(|name| name.to_str()) else {
return Ok(None);
};
match std::fs::read_to_string(format!("/sys/block/{name}/loop/backing_file")) {
Ok(path) => Ok(Some(path.trim().to_owned())),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(error.into()),
}
}
#[cfg(target_os = "linux")]
pub fn device_major_minor(path: &str) -> Result<(u32, u32)> {
use std::os::unix::fs::{FileTypeExt as _, MetadataExt as _};
let metadata = std::fs::metadata(path)
.map_err(|e| SnapshotError::DeviceMapper(format!("stat {path}: {e}")))?;
if !metadata.file_type().is_block_device() {
return Err(SnapshotError::DeviceMapper(format!(
"{path} is not a block device"
)));
}
let dev = metadata.rdev();
let narrow = |what: &str, value: u64| {
u32::try_from(value).map_err(|_| {
SnapshotError::DeviceMapper(format!("{path}: {what} {value} out of range"))
})
};
Ok((
narrow("major", nix::sys::stat::major(dev))?,
narrow("minor", nix::sys::stat::minor(dev))?,
))
}
#[cfg(target_os = "linux")]
pub fn mknod_blkdev(node_path: &Path, major: u32, minor: u32) -> Result<()> {
use nix::sys::stat::{Mode, SFlag, makedev, mknod};
mknod(
node_path,
SFlag::S_IFBLK,
Mode::S_IRUSR | Mode::S_IWUSR,
makedev(u64::from(major), u64::from(minor)),
)
.map_err(|e| SnapshotError::DeviceMapper(format!("mknod {}: {e}", node_path.display())))
}
#[cfg(not(target_os = "linux"))]
pub fn device_major_minor(path: &str) -> Result<(u32, u32)> {
Err(SnapshotError::Unavailable(format!(
"block device numbers for {path}: Linux-only"
)))
}
#[cfg(not(target_os = "linux"))]
pub fn mknod_blkdev(node_path: &Path, _major: u32, _minor: u32) -> Result<()> {
Err(SnapshotError::Unavailable(format!(
"mknod {}: Linux-only",
node_path.display()
)))
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_os = "linux")]
#[test]
fn device_major_minor_rejects_regular_files() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("plain");
std::fs::write(&file, b"x").unwrap();
let err = device_major_minor(file.to_str().unwrap()).unwrap_err();
assert!(err.to_string().contains("not a block device"), "{err}");
}
#[cfg(target_os = "linux")]
#[test]
fn device_major_minor_reports_missing_paths() {
let err = device_major_minor("/nonexistent/arcbox-blkdev").unwrap_err();
assert!(err.to_string().contains("stat"), "{err}");
}
struct DetachSpy {
detached: std::sync::Mutex<Vec<String>>,
fails: bool,
}
impl DetachSpy {
fn new(fails: bool) -> Self {
Self {
detached: std::sync::Mutex::new(Vec::new()),
fails,
}
}
fn detached(&self) -> Vec<String> {
self.detached.lock().unwrap().clone()
}
}
impl BlockTools for DetachSpy {
fn attach_loop(&self, _backing: &Path, _read_only: bool) -> Result<String> {
unreachable!("verification never attaches")
}
fn detach_loop(&self, device: &str) -> Result<()> {
self.detached.lock().unwrap().push(device.to_owned());
if self.fails {
return Err(SnapshotError::DeviceMapper("device busy".into()));
}
Ok(())
}
fn device_sectors(&self, _device: &str) -> Result<u64> {
unreachable!("verification never sizes")
}
}
#[test]
fn a_device_backing_the_expected_file_verifies_without_a_detach() {
let spy = DetachSpy::new(false);
verify_reported_backing(
&spy,
"/dev/loop0",
Path::new("/tmp/cow.img"),
"/tmp/cow.img",
)
.unwrap();
assert_eq!(spy.detached(), Vec::<String>::new());
}
#[test]
fn a_device_backing_something_else_is_given_back_and_reported() {
let spy = DetachSpy::new(false);
let err = verify_reported_backing(
&spy,
"/dev/loop0",
Path::new("/tmp/cow.img"),
"/tmp/other.img",
)
.unwrap_err();
assert!(
err.to_string().contains("/dev/loop0 backs /tmp/other.img"),
"{err}"
);
assert_eq!(spy.detached(), ["/dev/loop0"]);
}
#[test]
fn a_mismatch_whose_detach_also_fails_reports_both() {
let spy = DetachSpy::new(true);
let err = verify_reported_backing(
&spy,
"/dev/loop0",
Path::new("/tmp/cow.img"),
"/tmp/other.img",
)
.unwrap_err();
assert!(err.to_string().contains("not /tmp/cow.img"), "{err}");
assert!(
err.to_string().contains("detaching it again failed"),
"{err}"
);
}
#[test]
fn a_printed_device_is_a_loop_device_with_the_newline_stripped() {
assert_eq!(
parse_loop_device("losetup -f", b"/dev/loop3\n").unwrap(),
"/dev/loop3"
);
assert_eq!(
parse_loop_device("losetup -f", b"/dev/loop12").unwrap(),
"/dev/loop12"
);
for garbage in [
&b""[..],
b"\n",
b"/dev/loop",
b"/dev/loopX",
b"loop3\n",
b"/dev/sda1\n",
b"/dev/loop3 (lost)\n",
] {
let err = parse_loop_device("losetup -f", garbage).unwrap_err();
assert!(
err.to_string().contains("not a /dev/loopN"),
"{garbage:?}: {err}"
);
}
}
}