Skip to main content

btrfs_cli/
property.rs

1use crate::{Format, Runnable};
2use anyhow::Result;
3use clap::Parser;
4use std::{
5    fs::File,
6    os::unix::{
7        fs::{FileTypeExt, MetadataExt},
8        io::AsFd,
9    },
10    path::Path,
11};
12
13mod get;
14mod list;
15mod set;
16
17pub use self::{get::*, list::*, set::*};
18
19/// Object type for property operations
20#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
21pub enum PropertyObjectType {
22    Inode,
23    Subvol,
24    Filesystem,
25    Device,
26}
27
28/// Modify properties of filesystem objects.
29///
30/// Get, set, and list properties of filesystem objects including subvolumes,
31/// inodes, the filesystem itself, and devices. Properties control various
32/// aspects of filesystem behavior such as read-only status, compression,
33/// and labels. Most property operations require CAP_SYS_ADMIN or appropriate
34/// filesystem permissions.
35#[derive(Parser, Debug)]
36pub struct PropertyCommand {
37    #[clap(subcommand)]
38    pub subcommand: PropertySubcommand,
39}
40
41impl Runnable for PropertyCommand {
42    fn run(&self, format: Format, dry_run: bool) -> Result<()> {
43        match &self.subcommand {
44            PropertySubcommand::Get(cmd) => cmd.run(format, dry_run),
45            PropertySubcommand::Set(cmd) => cmd.run(format, dry_run),
46            PropertySubcommand::List(cmd) => cmd.run(format, dry_run),
47        }
48    }
49}
50
51#[derive(Parser, Debug)]
52pub enum PropertySubcommand {
53    Get(PropertyGetCommand),
54    Set(PropertySetCommand),
55    List(PropertyListCommand),
56}
57
58/// Metadata attributes used to classify a filesystem object.
59struct ObjectAttrs {
60    /// Whether the path could be stat'd at all.
61    exists: bool,
62    /// Whether the path is a block device.
63    is_block_device: bool,
64    /// Inode number of the path.
65    ino: u64,
66    /// Whether `subvolume_info` succeeded on the path.
67    is_subvolume: bool,
68    /// Whether the path is the filesystem root (mount point).
69    is_fs_root: bool,
70}
71
72/// Classify a filesystem object based on its attributes.
73///
74/// Returns the list of applicable object types. The order matches the C
75/// reference: inode first, then device, subvol, filesystem.
76fn classify_object(attrs: &ObjectAttrs) -> Vec<PropertyObjectType> {
77    if !attrs.exists {
78        return Vec::new();
79    }
80
81    let mut types = Vec::new();
82
83    // All files on btrfs are inodes.
84    types.push(PropertyObjectType::Inode);
85
86    // Block devices get the Device type.
87    if attrs.is_block_device {
88        types.push(PropertyObjectType::Device);
89    }
90
91    // Subvolume roots have inode 256 (BTRFS_FIRST_FREE_OBJECTID) and
92    // respond to subvolume_info.
93    if attrs.is_subvolume && attrs.ino == 256 {
94        types.push(PropertyObjectType::Subvol);
95
96        // The filesystem root is a subvolume that is also a mount point.
97        if attrs.is_fs_root {
98            types.push(PropertyObjectType::Filesystem);
99        }
100    }
101
102    types
103}
104
105/// Probe the filesystem to build `ObjectAttrs` for the given path.
106fn probe_object_attrs(path: &Path) -> ObjectAttrs {
107    let metadata = match std::fs::metadata(path) {
108        Ok(m) => m,
109        Err(_) => {
110            return ObjectAttrs {
111                exists: false,
112                is_block_device: false,
113                ino: 0,
114                is_subvolume: false,
115                is_fs_root: false,
116            };
117        }
118    };
119
120    let is_block_device = metadata.file_type().is_block_device();
121    let ino = metadata.ino();
122
123    let is_subvolume = File::open(path).ok().is_some_and(|f| {
124        btrfs_uapi::subvolume::subvolume_info(f.as_fd()).is_ok()
125    });
126
127    let is_fs_root = is_filesystem_root(path).unwrap_or(false);
128
129    ObjectAttrs {
130        exists: true,
131        is_block_device,
132        ino,
133        is_subvolume,
134        is_fs_root,
135    }
136}
137
138/// Detect which object types a path could be.
139fn detect_object_types(path: &Path) -> Vec<PropertyObjectType> {
140    classify_object(&probe_object_attrs(path))
141}
142
143/// Check whether `path` is a filesystem root (mount point) by comparing
144/// device numbers with its parent directory.
145fn is_filesystem_root(path: &Path) -> Result<bool> {
146    let canonical = std::fs::canonicalize(path)?;
147    let parent = canonical.parent();
148
149    if let Some(parent) = parent {
150        let canonical_metadata = std::fs::metadata(&canonical)?;
151        let parent_metadata = std::fs::metadata(parent)?;
152
153        if canonical_metadata.dev() != parent_metadata.dev() {
154            return Ok(true);
155        }
156    }
157
158    Ok(false)
159}
160
161/// Return the property names applicable to the given object type.
162fn property_names(obj_type: PropertyObjectType) -> &'static [&'static str] {
163    match obj_type {
164        PropertyObjectType::Inode => &["compression"],
165        PropertyObjectType::Subvol => &["ro"],
166        PropertyObjectType::Filesystem | PropertyObjectType::Device => {
167            &["label"]
168        }
169    }
170}
171
172/// Return a human-readable description for a property name.
173fn property_description(name: &str) -> &'static str {
174    match name {
175        "ro" => "read-only status of a subvolume",
176        "label" => "label of the filesystem",
177        "compression" => "compression algorithm for the file or directory",
178        _ => "unknown property",
179    }
180}
181
182/// Check whether `name` is a valid property for `obj_type`.
183#[cfg(test)]
184fn is_valid_property(obj_type: PropertyObjectType, name: &str) -> bool {
185    property_names(obj_type).contains(&name)
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    fn attrs(
193        is_block_device: bool,
194        ino: u64,
195        is_subvolume: bool,
196        is_fs_root: bool,
197    ) -> ObjectAttrs {
198        ObjectAttrs {
199            exists: true,
200            is_block_device,
201            ino,
202            is_subvolume,
203            is_fs_root,
204        }
205    }
206
207    // --- classify_object ---
208
209    #[test]
210    fn classify_regular_file() {
211        let types = classify_object(&attrs(false, 1000, false, false));
212        assert_eq!(types, vec![PropertyObjectType::Inode]);
213    }
214
215    #[test]
216    fn classify_block_device() {
217        let types = classify_object(&attrs(true, 1000, false, false));
218        assert_eq!(
219            types,
220            vec![PropertyObjectType::Inode, PropertyObjectType::Device]
221        );
222    }
223
224    #[test]
225    fn classify_subvolume() {
226        let types = classify_object(&attrs(false, 256, true, false));
227        assert_eq!(
228            types,
229            vec![PropertyObjectType::Inode, PropertyObjectType::Subvol]
230        );
231    }
232
233    #[test]
234    fn classify_subvolume_at_mount_point() {
235        let types = classify_object(&attrs(false, 256, true, true));
236        assert_eq!(
237            types,
238            vec![
239                PropertyObjectType::Inode,
240                PropertyObjectType::Subvol,
241                PropertyObjectType::Filesystem,
242            ]
243        );
244    }
245
246    #[test]
247    fn classify_nonexistent_path() {
248        let a = ObjectAttrs {
249            exists: false,
250            is_block_device: false,
251            ino: 0,
252            is_subvolume: false,
253            is_fs_root: false,
254        };
255        assert!(classify_object(&a).is_empty());
256    }
257
258    #[test]
259    fn classify_subvolume_info_ok_but_wrong_ino() {
260        // subvolume_info succeeds but inode is not 256 — should not detect as subvol.
261        let types = classify_object(&attrs(false, 500, true, false));
262        assert_eq!(types, vec![PropertyObjectType::Inode]);
263    }
264
265    #[test]
266    fn classify_ino_256_but_not_subvolume() {
267        // Inode 256 but subvolume_info fails — should not detect as subvol.
268        let types = classify_object(&attrs(false, 256, false, false));
269        assert_eq!(types, vec![PropertyObjectType::Inode]);
270    }
271
272    #[test]
273    fn classify_fs_root_requires_subvolume() {
274        // is_fs_root but not a subvolume — filesystem type should not appear.
275        let types = classify_object(&attrs(false, 256, false, true));
276        assert_eq!(types, vec![PropertyObjectType::Inode]);
277    }
278
279    // --- property_names ---
280
281    #[test]
282    fn property_names_inode() {
283        assert_eq!(property_names(PropertyObjectType::Inode), &["compression"]);
284    }
285
286    #[test]
287    fn property_names_subvol() {
288        assert_eq!(property_names(PropertyObjectType::Subvol), &["ro"]);
289    }
290
291    #[test]
292    fn property_names_filesystem() {
293        assert_eq!(property_names(PropertyObjectType::Filesystem), &["label"]);
294    }
295
296    #[test]
297    fn property_names_device() {
298        assert_eq!(property_names(PropertyObjectType::Device), &["label"]);
299    }
300
301    // --- is_valid_property ---
302
303    #[test]
304    fn valid_property_ro_on_subvol() {
305        assert!(is_valid_property(PropertyObjectType::Subvol, "ro"));
306    }
307
308    #[test]
309    fn invalid_property_ro_on_inode() {
310        assert!(!is_valid_property(PropertyObjectType::Inode, "ro"));
311    }
312
313    #[test]
314    fn valid_property_label_on_filesystem() {
315        assert!(is_valid_property(PropertyObjectType::Filesystem, "label"));
316    }
317
318    #[test]
319    fn valid_property_label_on_device() {
320        assert!(is_valid_property(PropertyObjectType::Device, "label"));
321    }
322
323    #[test]
324    fn invalid_property_label_on_subvol() {
325        assert!(!is_valid_property(PropertyObjectType::Subvol, "label"));
326    }
327
328    #[test]
329    fn valid_property_compression_on_inode() {
330        assert!(is_valid_property(PropertyObjectType::Inode, "compression"));
331    }
332
333    #[test]
334    fn invalid_property_unknown() {
335        assert!(!is_valid_property(PropertyObjectType::Inode, "nosuch"));
336    }
337
338    // --- property_description ---
339
340    #[test]
341    fn description_known_properties() {
342        assert_eq!(
343            property_description("ro"),
344            "read-only status of a subvolume"
345        );
346        assert_eq!(property_description("label"), "label of the filesystem");
347        assert_eq!(
348            property_description("compression"),
349            "compression algorithm for the file or directory"
350        );
351    }
352
353    #[test]
354    fn description_unknown_property() {
355        assert_eq!(property_description("nosuch"), "unknown property");
356    }
357}