#![cfg(all(target_os = "linux", feature = "fuse"))]
use std::io::Write;
use std::path::Path;
use std::process::Command;
use std::time::{Duration, Instant};
use fstool::block::{FileBackend, MemoryBackend};
use fstool::fs::ext::{Ext, FormatOpts};
use fstool::fs::ramfs::Ramfs;
use fstool::fs::{FileMeta, FileSource, Filesystem};
use fstool::fuse_adapter::FstoolFs;
use tempfile::{NamedTempFile, TempDir};
fn fuse_usable() -> bool {
use std::fs::OpenOptions;
if OpenOptions::new()
.read(true)
.write(true)
.open("/dev/fuse")
.is_err()
{
return false;
}
Command::new("sh")
.arg("-c")
.arg("command -v fusermount3 || command -v fusermount")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn build_seed_image(path: &Path) -> Vec<(&'static str, &'static [u8])> {
let opts = FormatOpts {
inodes_count: 64,
..FormatOpts::default()
};
let size = opts.blocks_count as u64 * opts.block_size as u64;
let mut dev = FileBackend::create(path, size).expect("create image");
let mut ext = Ext::format_with(&mut dev, &opts).expect("format ext2");
let seeds: Vec<(&'static str, &'static [u8])> = vec![
("hello.txt", b"hello from fuse\n"),
("greeting.txt", b"konnichiwa\n"),
];
for (name, body) in &seeds {
let mut src = NamedTempFile::new().expect("seed src");
src.as_file_mut().write_all(body).expect("write seed");
ext.add_file_to(
&mut dev,
2, name.as_bytes(),
FileSource::HostPath(src.path().to_path_buf()),
FileMeta {
mode: 0o644,
mtime: 0,
..Default::default()
},
)
.expect("add seed file");
drop(src);
}
let etc_ino = ext
.add_dir_to(&mut dev, 2, b"etc", FileMeta::with_mode(0o755))
.expect("mkdir /etc");
let mut conf_src = NamedTempFile::new().expect("conf src");
conf_src
.as_file_mut()
.write_all(b"answer=42\n")
.expect("write conf");
ext.add_file_to(
&mut dev,
etc_ino,
b"conf",
FileSource::HostPath(conf_src.path().to_path_buf()),
FileMeta::with_mode(0o644),
)
.expect("add /etc/conf");
drop(conf_src);
ext.flush(&mut dev).expect("flush ext");
{
use fstool::block::BlockDevice;
dev.sync().expect("sync");
}
drop(dev);
seeds
}
fn wait_until_mounted(mountpoint: &Path) {
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
if let Ok(mut rd) = std::fs::read_dir(mountpoint)
&& rd.next().is_some()
{
return;
}
std::thread::sleep(Duration::from_millis(50));
}
panic!("mountpoint never became readable within 5s");
}
#[test]
fn fuse_kernel_roundtrip_ext2() {
if !fuse_usable() {
eprintln!("skipping: /dev/fuse not usable or fusermount missing");
return;
}
let img = NamedTempFile::new().expect("img tempfile");
let seeds = build_seed_image(img.path());
let mountdir = TempDir::new().expect("mount tempdir");
let mountpoint = mountdir.path().to_path_buf();
let dev: Box<dyn fstool::block::BlockDevice + Send> =
Box::new(FileBackend::open(img.path()).expect("open image"));
let mut dev = dev;
let ext = Ext::open(dev.as_mut()).expect("open ext");
let fs: Box<dyn fstool::fs::Filesystem + Send> = Box::new(ext);
let session = FstoolFs::new(fs, dev, "ext")
.spawn_mount(&mountpoint)
.expect("spawn_mount");
wait_until_mounted(&mountpoint);
for (name, body) in &seeds {
let path = mountpoint.join(name);
let got = std::fs::read(&path)
.unwrap_or_else(|e| panic!("read {} via FUSE failed: {e}", path.display()));
assert_eq!(&got[..], *body, "content mismatch for {}", path.display());
}
let names: std::collections::HashSet<String> = std::fs::read_dir(&mountpoint)
.expect("readdir root")
.map(|e| e.expect("entry").file_name().to_string_lossy().into_owned())
.collect();
for want in ["hello.txt", "greeting.txt", "etc"] {
assert!(
names.contains(want),
"missing {want} in root listing: {names:?}"
);
}
let meta = std::fs::metadata(mountpoint.join("hello.txt")).expect("stat hello.txt");
assert!(meta.is_file(), "hello.txt not a regular file");
assert_eq!(meta.len() as usize, b"hello from fuse\n".len());
let conf = std::fs::read(mountpoint.join("etc/conf")).expect("read /etc/conf");
assert_eq!(&conf[..], b"answer=42\n");
drop(session);
std::thread::sleep(Duration::from_millis(200));
}
#[test]
fn fuse_kernel_roundtrip_ramfs_readwrite() {
if !fuse_usable() {
eprintln!("skipping: /dev/fuse not usable or fusermount missing");
return;
}
let mut dev: Box<dyn fstool::block::BlockDevice + Send> = Box::new(MemoryBackend::new(0));
let mut r = Ramfs::new();
r.create_dir(dev.as_mut(), Path::new("/etc"), FileMeta::default())
.unwrap();
let seed = b"seeded ramfs\n";
r.create_file(
dev.as_mut(),
Path::new("/etc/motd"),
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(seed.to_vec())),
len: seed.len() as u64,
},
FileMeta::default(),
)
.unwrap();
let fs: Box<dyn Filesystem + Send> = Box::new(r);
let mountdir = TempDir::new().expect("mount tempdir");
let mountpoint = mountdir.path().to_path_buf();
let session = FstoolFs::new(fs, dev, "ramfs")
.spawn_mount(&mountpoint)
.expect("spawn_mount");
wait_until_mounted(&mountpoint);
let got = std::fs::read(mountpoint.join("etc/motd")).expect("read seeded");
assert_eq!(&got[..], seed);
let body = b"written through fuse\n";
std::fs::write(mountpoint.join("fresh.txt"), body).expect("write via fuse");
let back = std::fs::read(mountpoint.join("fresh.txt")).expect("read back");
assert_eq!(&back[..], body);
std::fs::create_dir(mountpoint.join("sub")).expect("mkdir via fuse");
std::fs::write(mountpoint.join("sub/inner"), b"x").expect("nested write");
assert_eq!(std::fs::read(mountpoint.join("sub/inner")).unwrap(), b"x");
std::fs::remove_file(mountpoint.join("sub/inner")).expect("unlink");
std::fs::remove_dir(mountpoint.join("sub")).expect("rmdir");
assert!(std::fs::metadata(mountpoint.join("sub")).is_err());
let names: std::collections::HashSet<String> = std::fs::read_dir(&mountpoint)
.expect("readdir root")
.map(|e| e.expect("entry").file_name().to_string_lossy().into_owned())
.collect();
assert!(names.contains("fresh.txt"), "fresh.txt missing: {names:?}");
assert!(names.contains("etc"), "etc missing: {names:?}");
drop(session);
std::thread::sleep(Duration::from_millis(200));
}