use super::*;
use crate::fs_btrfs_test_support::{btrfs_fixture, removal_allowed};
use crate::fs_kind::{is_subvolume_root, BTRFS_SUBVOLUME_ROOT_INODE};
use crate::test_support::{fixture, remove};
use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
const GENERIC_VALUES: [u64; 3] = [0x5000_940e, 0x5000_940f, 0x5000_9417];
const WIDE_VALUES: [u64; 3] = [0x9000_940e, 0x9000_940f, 0x9000_9417];
#[test]
fn the_encoding_reproduces_the_clone_request() {
assert_eq!(ioc_write(9, std::mem::size_of::<libc::c_int>()), libc::FICLONE);
}
#[test]
fn the_request_numbers_equal_the_header_values_of_every_encoding() {
for (layout, expected) in [(GENERIC_IOC_LAYOUT, GENERIC_VALUES), (WIDE_DIRECTION_IOC_LAYOUT, WIDE_VALUES), (PARISC_IOC_LAYOUT, WIDE_VALUES)] {
let computed: [u64; 3] = [
u64::from(ioc_write_with(layout, 14, VOLUME_ARGUMENT_BYTES)),
u64::from(ioc_write_with(layout, 15, VOLUME_ARGUMENT_BYTES)),
u64::from(ioc_write_with(layout, 23, VOLUME_ARGUMENT_BYTES)),
];
assert_eq!(computed, expected, "{layout:?}");
}
let on_this_architecture: [u64; 3] = [BTRFS_IOC_SUBVOL_CREATE as u64, BTRFS_IOC_SNAP_DESTROY as u64, BTRFS_IOC_SNAP_CREATE_V2 as u64];
let wide: bool = HOST_IOC_LAYOUT == WIDE_DIRECTION_IOC_LAYOUT;
assert_eq!(on_this_architecture, if wide { WIDE_VALUES } else { GENERIC_VALUES });
}
#[test]
fn the_argument_structures_have_the_size_the_requests_state() {
assert_eq!(std::mem::size_of::<VolArgs>(), VOLUME_ARGUMENT_BYTES);
assert_eq!(std::mem::size_of::<VolArgsV2>(), VOLUME_ARGUMENT_BYTES);
assert_eq!(std::mem::offset_of!(VolArgs, name), 8);
assert_eq!(std::mem::offset_of!(VolArgsV2, name), 8 + 8 + 8 + 32);
}
#[test]
fn a_name_that_is_not_one_component_is_refused_before_any_call() {
let root: PathBuf = fixture("fs-ioctl-names");
let long: String = "n".repeat(300);
for text in ["", ".", "..", "a/b", "a\0b", long.as_str()] {
let name = std::ffi::OsStr::new(text);
for error in [
subvolume_create(root.as_path(), name).expect_err("create refuses"),
snapshot_create(root.as_path(), root.as_path(), name).expect_err("snapshot refuses"),
snapshot_destroy(root.as_path(), name).expect_err("destroy refuses"),
] {
assert_eq!(error.errno, libc::EINVAL, "{error}");
}
}
remove(root.as_path());
}
#[test]
fn a_missing_parent_is_a_typed_error() {
let root: PathBuf = fixture("fs-ioctl-parent");
let error: PrimitiveError = subvolume_create(root.join("absent").as_path(), std::ffi::OsStr::new("x")).expect_err("no parent");
assert_eq!(error.errno, libc::ENOENT);
remove(root.as_path());
}
#[test]
fn a_path_without_a_name_cannot_be_split() {
let error: PrimitiveError = split_parent(Path::new("/"), "BTRFS_IOC_SUBVOL_CREATE").expect_err("the root has no name");
assert_eq!(error.errno, libc::EINVAL);
let (parent, name) = split_parent(Path::new("relative"), "x").expect("a bare name");
assert_eq!((parent, name), (PathBuf::from("."), std::ffi::OsString::from("relative")));
}
#[test]
fn off_btrfs_the_ioctls_report_a_missing_feature() {
let root: PathBuf = fixture("fs-ioctl-off");
if crate::fs_kind::filesystem_kind(root.as_path()) == Ok(crate::fs_kind::FilesystemKind::Btrfs) {
remove(root.as_path());
return;
}
let error: PrimitiveError = subvolume_create_at(root.join("sub").as_path()).expect_err("not btrfs");
assert!(error.means_feature_missing(), "{error}");
remove(root.as_path());
}
#[test]
fn btrfs_create_snapshot_and_destroy_each_work_once() {
let Some(root) = btrfs_fixture("fs-ioctl-real") else { return };
let source: PathBuf = root.join("source");
subvolume_create_at(source.as_path()).expect("create");
assert_eq!(std::fs::metadata(&source).expect("stat").ino(), BTRFS_SUBVOLUME_ROOT_INODE);
std::fs::write(source.join("file"), b"content").expect("file in the source");
let copy: PathBuf = root.join("copy");
snapshot_create_at(source.as_path(), copy.as_path()).expect("snapshot");
assert_eq!(is_subvolume_root(copy.as_path()), Ok(true));
assert_eq!(std::fs::read(copy.join("file")).expect("file in the snapshot"), b"content");
let again = snapshot_create_at(source.as_path(), copy.as_path()).expect_err("the name exists");
assert_eq!(again.errno, libc::EEXIST);
let plain: PathBuf = root.join("plain");
std::fs::create_dir(&plain).expect("plain directory");
let not_a_subvolume = snapshot_create_at(plain.as_path(), root.join("from-plain").as_path()).expect_err("a plain directory is no source");
assert!(not_a_subvolume.means_feature_missing(), "{not_a_subvolume}");
std::fs::remove_file(copy.join("file")).expect("empty the snapshot");
if removal_allowed(root.as_path()) {
subvolume_destroy_at(copy.as_path()).expect("destroy where the mount allows it");
} else {
let refused = subvolume_destroy_at(copy.as_path()).expect_err("the mount option is absent");
assert_eq!(refused.errno, libc::EPERM, "{refused}");
std::fs::remove_dir(©).expect("rmdir removes an empty subvolume");
}
assert!(!copy.exists());
remove(root.as_path());
}
#[test]
fn btrfs_a_subvolume_with_a_nested_subvolume_is_not_destroyed_in_one_step() {
let Some(root) = btrfs_fixture("fs-ioctl-nested") else { return };
if !removal_allowed(root.as_path()) {
remove(root.as_path());
return;
}
let outer: PathBuf = root.join("outer");
subvolume_create_at(outer.as_path()).expect("outer");
subvolume_create_at(outer.join("inner").as_path()).expect("inner");
let refused = subvolume_destroy_at(outer.as_path()).expect_err("a nested subvolume blocks the destroy");
assert_eq!(refused.errno, libc::ENOTEMPTY, "{refused}");
subvolume_destroy_at(outer.join("inner").as_path()).expect("inner first");
subvolume_destroy_at(outer.as_path()).expect("then outer");
remove(root.as_path());
}