use crate::{CommandGroup, Runnable};
use anyhow::Result;
use clap::Parser;
use std::{
fs::{self, File},
os::unix::{
fs::{FileTypeExt, MetadataExt},
io::AsFd,
},
path::Path,
};
mod get;
mod list;
mod set;
pub use self::{get::*, list::*, set::*};
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum PropertyObjectType {
Inode,
Subvol,
Filesystem,
Device,
}
#[derive(Parser, Debug)]
#[allow(clippy::doc_markdown)]
#[clap(arg_required_else_help = true)]
pub struct PropertyCommand {
#[clap(subcommand)]
pub subcommand: PropertySubcommand,
}
impl CommandGroup for PropertyCommand {
fn leaf(&self) -> &dyn Runnable {
match &self.subcommand {
PropertySubcommand::Get(cmd) => cmd,
PropertySubcommand::Set(cmd) => cmd,
PropertySubcommand::List(cmd) => cmd,
}
}
}
#[derive(Parser, Debug)]
pub enum PropertySubcommand {
Get(PropertyGetCommand),
Set(PropertySetCommand),
List(PropertyListCommand),
}
#[allow(clippy::struct_excessive_bools)]
struct ObjectAttrs {
exists: bool,
is_block_device: bool,
ino: u64,
is_subvolume: bool,
is_fs_root: bool,
}
fn classify_object(attrs: &ObjectAttrs) -> Vec<PropertyObjectType> {
if !attrs.exists {
return Vec::new();
}
let mut types = Vec::new();
types.push(PropertyObjectType::Inode);
if attrs.is_block_device {
types.push(PropertyObjectType::Device);
}
if attrs.is_subvolume && attrs.ino == 256 {
types.push(PropertyObjectType::Subvol);
if attrs.is_fs_root {
types.push(PropertyObjectType::Filesystem);
}
}
types
}
fn probe_object_attrs(path: &Path) -> ObjectAttrs {
let Ok(metadata) = fs::metadata(path) else {
return ObjectAttrs {
exists: false,
is_block_device: false,
ino: 0,
is_subvolume: false,
is_fs_root: false,
};
};
let is_block_device = metadata.file_type().is_block_device();
let ino = metadata.ino();
let is_subvolume = File::open(path).ok().is_some_and(|f| {
btrfs_uapi::subvolume::subvolume_info(f.as_fd()).is_ok()
});
let is_fs_root = is_filesystem_root(path).unwrap_or(false);
ObjectAttrs {
exists: true,
is_block_device,
ino,
is_subvolume,
is_fs_root,
}
}
fn detect_object_types(path: &Path) -> Vec<PropertyObjectType> {
classify_object(&probe_object_attrs(path))
}
fn is_filesystem_root(path: &Path) -> Result<bool> {
let canonical = fs::canonicalize(path)?;
let parent = canonical.parent();
if let Some(parent) = parent {
let canonical_metadata = fs::metadata(&canonical)?;
let parent_metadata = fs::metadata(parent)?;
if canonical_metadata.dev() != parent_metadata.dev() {
return Ok(true);
}
}
Ok(false)
}
fn property_names(obj_type: PropertyObjectType) -> &'static [&'static str] {
match obj_type {
PropertyObjectType::Inode => &["compression"],
PropertyObjectType::Subvol => &["ro"],
PropertyObjectType::Filesystem | PropertyObjectType::Device => {
&["label"]
}
}
}
fn property_description(name: &str) -> &'static str {
match name {
"ro" => "read-only status of a subvolume",
"label" => "label of the filesystem",
"compression" => "compression algorithm for the file or directory",
_ => "unknown property",
}
}
#[cfg(test)]
fn is_valid_property(obj_type: PropertyObjectType, name: &str) -> bool {
property_names(obj_type).contains(&name)
}
#[cfg(test)]
mod tests {
use super::*;
fn attrs(
is_block_device: bool,
ino: u64,
is_subvolume: bool,
is_fs_root: bool,
) -> ObjectAttrs {
ObjectAttrs {
exists: true,
is_block_device,
ino,
is_subvolume,
is_fs_root,
}
}
#[test]
fn classify_regular_file() {
let types = classify_object(&attrs(false, 1000, false, false));
assert_eq!(types, vec![PropertyObjectType::Inode]);
}
#[test]
fn classify_block_device() {
let types = classify_object(&attrs(true, 1000, false, false));
assert_eq!(
types,
vec![PropertyObjectType::Inode, PropertyObjectType::Device]
);
}
#[test]
fn classify_subvolume() {
let types = classify_object(&attrs(false, 256, true, false));
assert_eq!(
types,
vec![PropertyObjectType::Inode, PropertyObjectType::Subvol]
);
}
#[test]
fn classify_subvolume_at_mount_point() {
let types = classify_object(&attrs(false, 256, true, true));
assert_eq!(
types,
vec![
PropertyObjectType::Inode,
PropertyObjectType::Subvol,
PropertyObjectType::Filesystem,
]
);
}
#[test]
fn classify_nonexistent_path() {
let a = ObjectAttrs {
exists: false,
is_block_device: false,
ino: 0,
is_subvolume: false,
is_fs_root: false,
};
assert!(classify_object(&a).is_empty());
}
#[test]
fn classify_subvolume_info_ok_but_wrong_ino() {
let types = classify_object(&attrs(false, 500, true, false));
assert_eq!(types, vec![PropertyObjectType::Inode]);
}
#[test]
fn classify_ino_256_but_not_subvolume() {
let types = classify_object(&attrs(false, 256, false, false));
assert_eq!(types, vec![PropertyObjectType::Inode]);
}
#[test]
fn classify_fs_root_requires_subvolume() {
let types = classify_object(&attrs(false, 256, false, true));
assert_eq!(types, vec![PropertyObjectType::Inode]);
}
#[test]
fn property_names_inode() {
assert_eq!(property_names(PropertyObjectType::Inode), &["compression"]);
}
#[test]
fn property_names_subvol() {
assert_eq!(property_names(PropertyObjectType::Subvol), &["ro"]);
}
#[test]
fn property_names_filesystem() {
assert_eq!(property_names(PropertyObjectType::Filesystem), &["label"]);
}
#[test]
fn property_names_device() {
assert_eq!(property_names(PropertyObjectType::Device), &["label"]);
}
#[test]
fn valid_property_ro_on_subvol() {
assert!(is_valid_property(PropertyObjectType::Subvol, "ro"));
}
#[test]
fn invalid_property_ro_on_inode() {
assert!(!is_valid_property(PropertyObjectType::Inode, "ro"));
}
#[test]
fn valid_property_label_on_filesystem() {
assert!(is_valid_property(PropertyObjectType::Filesystem, "label"));
}
#[test]
fn valid_property_label_on_device() {
assert!(is_valid_property(PropertyObjectType::Device, "label"));
}
#[test]
fn invalid_property_label_on_subvol() {
assert!(!is_valid_property(PropertyObjectType::Subvol, "label"));
}
#[test]
fn valid_property_compression_on_inode() {
assert!(is_valid_property(PropertyObjectType::Inode, "compression"));
}
#[test]
fn invalid_property_unknown() {
assert!(!is_valid_property(PropertyObjectType::Inode, "nosuch"));
}
#[test]
fn description_known_properties() {
assert_eq!(
property_description("ro"),
"read-only status of a subvolume"
);
assert_eq!(property_description("label"), "label of the filesystem");
assert_eq!(
property_description("compression"),
"compression algorithm for the file or directory"
);
}
#[test]
fn description_unknown_property() {
assert_eq!(property_description("nosuch"), "unknown property");
}
}