1use crate::{CommandGroup, 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#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
21pub enum PropertyObjectType {
22 Inode,
23 Subvol,
24 Filesystem,
25 Device,
26}
27
28#[derive(Parser, Debug)]
36#[allow(clippy::doc_markdown)]
37#[clap(arg_required_else_help = true)]
38pub struct PropertyCommand {
39 #[clap(subcommand)]
40 pub subcommand: PropertySubcommand,
41}
42
43impl CommandGroup for PropertyCommand {
44 fn leaf(&self) -> &dyn Runnable {
45 match &self.subcommand {
46 PropertySubcommand::Get(cmd) => cmd,
47 PropertySubcommand::Set(cmd) => cmd,
48 PropertySubcommand::List(cmd) => cmd,
49 }
50 }
51}
52
53#[derive(Parser, Debug)]
54pub enum PropertySubcommand {
55 Get(PropertyGetCommand),
56 Set(PropertySetCommand),
57 List(PropertyListCommand),
58}
59
60#[allow(clippy::struct_excessive_bools)]
62struct ObjectAttrs {
63 exists: bool,
65 is_block_device: bool,
67 ino: u64,
69 is_subvolume: bool,
71 is_fs_root: bool,
73}
74
75fn classify_object(attrs: &ObjectAttrs) -> Vec<PropertyObjectType> {
80 if !attrs.exists {
81 return Vec::new();
82 }
83
84 let mut types = Vec::new();
85
86 types.push(PropertyObjectType::Inode);
88
89 if attrs.is_block_device {
91 types.push(PropertyObjectType::Device);
92 }
93
94 if attrs.is_subvolume && attrs.ino == 256 {
97 types.push(PropertyObjectType::Subvol);
98
99 if attrs.is_fs_root {
101 types.push(PropertyObjectType::Filesystem);
102 }
103 }
104
105 types
106}
107
108fn probe_object_attrs(path: &Path) -> ObjectAttrs {
110 let Ok(metadata) = fs::metadata(path) else {
111 return ObjectAttrs {
112 exists: false,
113 is_block_device: false,
114 ino: 0,
115 is_subvolume: false,
116 is_fs_root: false,
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
138fn detect_object_types(path: &Path) -> Vec<PropertyObjectType> {
140 classify_object(&probe_object_attrs(path))
141}
142
143fn is_filesystem_root(path: &Path) -> Result<bool> {
146 let canonical = fs::canonicalize(path)?;
147 let parent = canonical.parent();
148
149 if let Some(parent) = parent {
150 let canonical_metadata = fs::metadata(&canonical)?;
151 let parent_metadata = fs::metadata(parent)?;
152
153 if canonical_metadata.dev() != parent_metadata.dev() {
154 return Ok(true);
155 }
156 }
157
158 Ok(false)
159}
160
161fn 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
172fn 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#[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 #[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 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 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 let types = classify_object(&attrs(false, 256, false, true));
276 assert_eq!(types, vec![PropertyObjectType::Inode]);
277 }
278
279 #[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 #[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 #[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}