use std::borrow::Cow;
use std::cmp::min;
use std::ffi::OsStr;
use std::ffi::OsString;
use std::fmt::Display;
use std::io::Write as _;
use std::path::Path;
use std::path::PathBuf;
use std::str;
use anyhow::Context as _;
use anyhow::Result;
use tempfile::NamedTempFile;
use tempfile::TempDir;
use crate::util::join;
use crate::util::output;
use crate::util::run;
use crate::util::vec_to_path_buf;
const LOSETUP: &str = "losetup";
const MKBTRFS: &str = "mkfs.btrfs";
const MOUNT: &str = "mount";
const UMOUNT: &str = "umount";
#[derive(Debug)]
struct LoopDev {
device: PathBuf,
}
impl LoopDev {
pub fn empty() -> Result<Self> {
let mut path = output(LOSETUP, ["--find", "--nooverlap"])?;
let _last = path.pop();
let path = vec_to_path_buf(path)?;
Ok(Self { device: path })
}
pub fn new(size: usize) -> Result<Self> {
static EMPTY: [u8; 4096] = [0; 4096];
let mut file = NamedTempFile::new().context("failed to create temporary file")?;
let mut n = 0;
while n < size {
let length = min(size - n, EMPTY.len());
let count = file
.write(&EMPTY[..length])
.context("failed to zero out temporary file")?;
n += count;
}
let mut slf = Self::empty()?;
let () = slf.attach(file.path())?;
Ok(slf)
}
fn attach(&mut self, path: &Path) -> Result<()> {
let () = run(LOSETUP, [&self.device, path]).with_context(|| {
format!(
"failed to attach file {} to loop device {}",
path.display(),
self.device.display()
)
})?;
Ok(())
}
fn destroy(&mut self) -> Result<()> {
let () = run(LOSETUP, [OsStr::new("--detach"), self.device.as_os_str()])?;
Ok(())
}
#[inline]
pub fn path(&self) -> &Path {
&self.device
}
}
impl Drop for LoopDev {
fn drop(&mut self) {
let _ = self.destroy().unwrap_or_else(|error| {
panic!(
"failed to detach loop device {}: {error}",
self.device.display()
)
});
}
}
#[derive(Debug)]
pub struct BtrfsDev {
device: LoopDev,
}
impl BtrfsDev {
pub fn new(size: usize) -> Result<Self> {
let device = LoopDev::new(size)?;
let () = run(MKBTRFS, [device.path()])?;
Ok(Self { device })
}
pub fn with_default() -> Result<Self> {
Self::new(128 * 1024 * 1024)
}
#[inline]
pub fn path(&self) -> &Path {
self.device.path()
}
}
#[derive(Debug, Default)]
pub struct MountBuilder {
directory: Option<PathBuf>,
options: Option<String>,
arguments: Vec<String>,
}
impl MountBuilder {
pub fn directory<D>(mut self, directory: D) -> Self
where
D: AsRef<Path>,
{
self.directory = Some(directory.as_ref().to_path_buf());
self
}
pub fn options<O, S>(mut self, options: O) -> Self
where
O: IntoIterator<Item = S>,
S: AsRef<str> + Display,
{
self.options = join(',', options);
self
}
pub fn arguments<A, S>(mut self, args: A) -> Self
where
A: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.arguments = args.into_iter().map(|s| s.as_ref().to_string()).collect();
self
}
pub fn mount<P>(self, device: P) -> Result<Mount>
where
P: AsRef<Path>,
{
let device = device.as_ref();
let directory = if let Some(directory) = self.directory {
Directory::Existing(directory)
} else {
Directory::Temporary(TempDir::new()?)
};
let () = if let Some(options) = self.options {
let args = [
device.as_os_str(),
directory.path().as_os_str(),
OsStr::new("-o"),
options.as_ref(),
];
let args = args.into_iter().map(Cow::from).chain(
self
.arguments
.into_iter()
.map(OsString::from)
.map(Cow::Owned),
);
run(MOUNT, args)?
} else {
let args = [device.as_os_str(), directory.path().as_os_str()];
let args = args.into_iter().map(Cow::from).chain(
self
.arguments
.into_iter()
.map(OsString::from)
.map(Cow::Owned),
);
run(MOUNT, args)?
};
let mount = Mount { directory };
Ok(mount)
}
}
#[derive(Debug)]
enum Directory {
Existing(PathBuf),
Temporary(TempDir),
}
impl Directory {
fn path(&self) -> &Path {
match self {
Self::Existing(path) => path,
Self::Temporary(temp) => temp.path(),
}
}
}
#[derive(Debug)]
pub struct Mount {
directory: Directory,
}
impl Mount {
pub fn new(device: &Path) -> Result<Self> {
Self::builder().mount(device)
}
pub fn builder() -> MountBuilder {
MountBuilder::default()
}
#[inline]
pub fn path(&self) -> &Path {
self.directory.path()
}
}
impl Drop for Mount {
fn drop(&mut self) {
let () = run(UMOUNT, [self.directory.path()]).unwrap_or_else(|error| {
panic!(
"failed to unmount {}: {error}",
self.directory.path().display()
)
});
}
}
pub fn with_btrfs<F>(f: F)
where
F: FnOnce(&Path),
{
let loopdev = BtrfsDev::with_default().unwrap();
let mount = Mount::new(loopdev.path()).unwrap();
f(mount.path())
}
pub fn with_two_btrfs<F>(f: F)
where
F: FnOnce(&Path, &Path),
{
let loopdev1 = BtrfsDev::with_default().unwrap();
let mount1 = Mount::new(loopdev1.path()).unwrap();
let loopdev2 = BtrfsDev::with_default().unwrap();
let mount2 = Mount::new(loopdev2.path()).unwrap();
f(mount1.path(), mount2.path())
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
#[serial]
fn create_destroy_loopdev() {
let _loopdev = LoopDev::new(1024).unwrap();
}
#[test]
#[serial]
fn create_mount_btrfs() {
let loopdev = BtrfsDev::with_default().unwrap();
let _mount = Mount::new(loopdev.path()).unwrap();
}
}