Skip to main content

btrfs_cli/
property.rs

1use crate::{Format, Runnable};
2use anyhow::Result;
3use clap::Parser;
4use std::{
5    fs::{self, 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 Ok(metadata) = fs::metadata(path) else {
108        return ObjectAttrs {
109            exists: false,
110            is_block_device: false,
111            ino: 0,
112            is_subvolume: false,
113            is_fs_root: false,
114        };
115    };
116
117    let is_block_device = metadata.file_type().is_block_device();
118    let ino = metadata.ino();
119
120    let is_subvolume = File::open(path).ok().is_some_and(|f| {
121        btrfs_uapi::subvolume::subvolume_info(f.as_fd()).is_ok()
122    });
123
124    let is_fs_root = is_filesystem_root(path).unwrap_or(false);
125
126    ObjectAttrs {
127        exists: true,
128        is_block_device,
129        ino,
130        is_subvolume,
131        is_fs_root,
132    }
133}
134
135/// Detect which object types a path could be.
136fn detect_object_types(path: &Path) -> Vec<PropertyObjectType> {
137    classify_object(&probe_object_attrs(path))
138}
139
140/// Check whether `path` is a filesystem root (mount point) by comparing
141/// device numbers with its parent directory.
142fn is_filesystem_root(path: &Path) -> Result<bool> {
143    let canonical = fs::canonicalize(path)?;
144    let parent = canonical.parent();
145
146    if let Some(parent) = parent {
147        let canonical_metadata = fs::metadata(&canonical)?;
148        let parent_metadata = fs::metadata(parent)?;
149
150        if canonical_metadata.dev() != parent_metadata.dev() {
151            return Ok(true);
152        }
153    }
154
155    Ok(false)
156}
157
158/// Return the property names applicable to the given object type.
159fn property_names(obj_type: PropertyObjectType) -> &'static [&'static str] {
160    match obj_type {
161        PropertyObjectType::Inode => &["compression"],
162        PropertyObjectType::Subvol => &["ro"],
163        PropertyObjectType::Filesystem | PropertyObjectType::Device => {
164            &["label"]
165        }
166    }
167}
168
169/// Return a human-readable description for a property name.
170fn property_description(name: &str) -> &'static str {
171    match name {
172        "ro" => "read-only status of a subvolume",
173        "label" => "label of the filesystem",
174        "compression" => "compression algorithm for the file or directory",
175        _ => "unknown property",
176    }
177}
178
179/// Check whether `name` is a valid property for `obj_type`.
180#[cfg(test)]
181fn is_valid_property(obj_type: PropertyObjectType, name: &str) -> bool {
182    property_names(obj_type).contains(&name)
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    fn attrs(
190        is_block_device: bool,
191        ino: u64,
192        is_subvolume: bool,
193        is_fs_root: bool,
194    ) -> ObjectAttrs {
195        ObjectAttrs {
196            exists: true,
197            is_block_device,
198            ino,
199            is_subvolume,
200            is_fs_root,
201        }
202    }
203
204    // --- classify_object ---
205
206    #[test]
207    fn classify_regular_file() {
208        let types = classify_object(&attrs(false, 1000, false, false));
209        assert_eq!(types, vec![PropertyObjectType::Inode]);
210    }
211
212    #[test]
213    fn classify_block_device() {
214        let types = classify_object(&attrs(true, 1000, false, false));
215        assert_eq!(
216            types,
217            vec![PropertyObjectType::Inode, PropertyObjectType::Device]
218        );
219    }
220
221    #[test]
222    fn classify_subvolume() {
223        let types = classify_object(&attrs(false, 256, true, false));
224        assert_eq!(
225            types,
226            vec![PropertyObjectType::Inode, PropertyObjectType::Subvol]
227        );
228    }
229
230    #[test]
231    fn classify_subvolume_at_mount_point() {
232        let types = classify_object(&attrs(false, 256, true, true));
233        assert_eq!(
234            types,
235            vec![
236                PropertyObjectType::Inode,
237                PropertyObjectType::Subvol,
238                PropertyObjectType::Filesystem,
239            ]
240        );
241    }
242
243    #[test]
244    fn classify_nonexistent_path() {
245        let a = ObjectAttrs {
246            exists: false,
247            is_block_device: false,
248            ino: 0,
249            is_subvolume: false,
250            is_fs_root: false,
251        };
252        assert!(classify_object(&a).is_empty());
253    }
254
255    #[test]
256    fn classify_subvolume_info_ok_but_wrong_ino() {
257        // subvolume_info succeeds but inode is not 256 — should not detect as subvol.
258        let types = classify_object(&attrs(false, 500, true, false));
259        assert_eq!(types, vec![PropertyObjectType::Inode]);
260    }
261
262    #[test]
263    fn classify_ino_256_but_not_subvolume() {
264        // Inode 256 but subvolume_info fails — should not detect as subvol.
265        let types = classify_object(&attrs(false, 256, false, false));
266        assert_eq!(types, vec![PropertyObjectType::Inode]);
267    }
268
269    #[test]
270    fn classify_fs_root_requires_subvolume() {
271        // is_fs_root but not a subvolume — filesystem type should not appear.
272        let types = classify_object(&attrs(false, 256, false, true));
273        assert_eq!(types, vec![PropertyObjectType::Inode]);
274    }
275
276    // --- property_names ---
277
278    #[test]
279    fn property_names_inode() {
280        assert_eq!(property_names(PropertyObjectType::Inode), &["compression"]);
281    }
282
283    #[test]
284    fn property_names_subvol() {
285        assert_eq!(property_names(PropertyObjectType::Subvol), &["ro"]);
286    }
287
288    #[test]
289    fn property_names_filesystem() {
290        assert_eq!(property_names(PropertyObjectType::Filesystem), &["label"]);
291    }
292
293    #[test]
294    fn property_names_device() {
295        assert_eq!(property_names(PropertyObjectType::Device), &["label"]);
296    }
297
298    // --- is_valid_property ---
299
300    #[test]
301    fn valid_property_ro_on_subvol() {
302        assert!(is_valid_property(PropertyObjectType::Subvol, "ro"));
303    }
304
305    #[test]
306    fn invalid_property_ro_on_inode() {
307        assert!(!is_valid_property(PropertyObjectType::Inode, "ro"));
308    }
309
310    #[test]
311    fn valid_property_label_on_filesystem() {
312        assert!(is_valid_property(PropertyObjectType::Filesystem, "label"));
313    }
314
315    #[test]
316    fn valid_property_label_on_device() {
317        assert!(is_valid_property(PropertyObjectType::Device, "label"));
318    }
319
320    #[test]
321    fn invalid_property_label_on_subvol() {
322        assert!(!is_valid_property(PropertyObjectType::Subvol, "label"));
323    }
324
325    #[test]
326    fn valid_property_compression_on_inode() {
327        assert!(is_valid_property(PropertyObjectType::Inode, "compression"));
328    }
329
330    #[test]
331    fn invalid_property_unknown() {
332        assert!(!is_valid_property(PropertyObjectType::Inode, "nosuch"));
333    }
334
335    // --- property_description ---
336
337    #[test]
338    fn description_known_properties() {
339        assert_eq!(
340            property_description("ro"),
341            "read-only status of a subvolume"
342        );
343        assert_eq!(property_description("label"), "label of the filesystem");
344        assert_eq!(
345            property_description("compression"),
346            "compression algorithm for the file or directory"
347        );
348    }
349
350    #[test]
351    fn description_unknown_property() {
352        assert_eq!(property_description("nosuch"), "unknown property");
353    }
354}