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());
}
#[test]
fn the_requests_for_the_destroy_by_id_equal_the_header_values_of_every_encoding() {
const GENERIC_DESTROY_V2: u32 = 0x5000_943f;
const WIDE_DESTROY_V2: u32 = 0x9000_943f;
const LOOKUP: u32 = 0xd000_9412;
for (layout, destroy) in [(GENERIC_IOC_LAYOUT, GENERIC_DESTROY_V2), (WIDE_DIRECTION_IOC_LAYOUT, WIDE_DESTROY_V2), (PARISC_IOC_LAYOUT, WIDE_DESTROY_V2)] {
assert_eq!(ioc_write_with(layout, 63, VOLUME_ARGUMENT_BYTES), destroy, "{layout:?}");
assert_eq!(ioc_read_write_with(layout, 18, VOLUME_ARGUMENT_BYTES), LOOKUP, "{layout:?}");
}
assert_eq!(BTRFS_IOC_INO_LOOKUP as u64, u64::from(LOOKUP));
let wide: bool = HOST_IOC_LAYOUT == WIDE_DIRECTION_IOC_LAYOUT;
assert_eq!(BTRFS_IOC_SNAP_DESTROY_V2 as u64, u64::from(if wide { WIDE_DESTROY_V2 } else { GENERIC_DESTROY_V2 }));
}
#[test]
fn btrfs_the_destroy_by_id_removes_the_subvolume_whose_id_it_is_given() {
let Some(root) = btrfs_fixture("fs-ioctl-by-id") else { return };
let (first, second) = (root.join("first"), root.join("second"));
subvolume_create_at(first.as_path()).expect("first");
subvolume_create_at(second.as_path()).expect("second");
let first_id: u64 = subvolume_id(&std::fs::File::open(first.as_path()).expect("open"), first.as_path()).expect("id of the first");
let second_id: u64 = subvolume_id(&std::fs::File::open(second.as_path()).expect("open"), second.as_path()).expect("id of the second");
assert_ne!(first_id, second_id);
std::fs::write(first.join("file"), b"x").expect("file");
assert_eq!(subvolume_id(&std::fs::File::open(first.join("file")).expect("open"), first.as_path()).expect("id of a file"), first_id);
std::fs::remove_file(first.join("file")).expect("empty it");
std::fs::rename(first.as_path(), root.join("renamed")).expect("rename");
let through: std::fs::File = std::fs::File::open(root.as_path()).expect("any directory of the filesystem");
if removal_allowed(root.as_path()) {
snapshot_destroy_by_id(&through, first_id, first.as_path()).expect("destroy by id");
assert!(!root.join("renamed").exists(), "the subvolume with that id is gone, under its new name");
assert!(second.is_dir(), "the other one is untouched");
} else {
let refused = snapshot_destroy_by_id(&through, first_id, first.as_path()).expect_err("the mount option is absent");
assert_eq!(refused.errno, libc::EPERM, "{refused}");
assert!(root.join("renamed").is_dir() && second.is_dir());
}
remove(root.as_path());
}