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