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#[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)]
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
58struct ObjectAttrs {
60 exists: bool,
62 is_block_device: bool,
64 ino: u64,
66 is_subvolume: bool,
68 is_fs_root: bool,
70}
71
72fn classify_object(attrs: &ObjectAttrs) -> Vec<PropertyObjectType> {
77 if !attrs.exists {
78 return Vec::new();
79 }
80
81 let mut types = Vec::new();
82
83 types.push(PropertyObjectType::Inode);
85
86 if attrs.is_block_device {
88 types.push(PropertyObjectType::Device);
89 }
90
91 if attrs.is_subvolume && attrs.ino == 256 {
94 types.push(PropertyObjectType::Subvol);
95
96 if attrs.is_fs_root {
98 types.push(PropertyObjectType::Filesystem);
99 }
100 }
101
102 types
103}
104
105fn 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
135fn detect_object_types(path: &Path) -> Vec<PropertyObjectType> {
137 classify_object(&probe_object_attrs(path))
138}
139
140fn 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
158fn 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
169fn 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#[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 #[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 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 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 let types = classify_object(&attrs(false, 256, false, true));
273 assert_eq!(types, vec![PropertyObjectType::Inode]);
274 }
275
276 #[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 #[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 #[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}