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)]
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#[allow(clippy::struct_excessive_bools)]
61struct ObjectAttrs {
62 exists: bool,
64 is_block_device: bool,
66 ino: u64,
68 is_subvolume: bool,
70 is_fs_root: bool,
72}
73
74fn classify_object(attrs: &ObjectAttrs) -> Vec<PropertyObjectType> {
79 if !attrs.exists {
80 return Vec::new();
81 }
82
83 let mut types = Vec::new();
84
85 types.push(PropertyObjectType::Inode);
87
88 if attrs.is_block_device {
90 types.push(PropertyObjectType::Device);
91 }
92
93 if attrs.is_subvolume && attrs.ino == 256 {
96 types.push(PropertyObjectType::Subvol);
97
98 if attrs.is_fs_root {
100 types.push(PropertyObjectType::Filesystem);
101 }
102 }
103
104 types
105}
106
107fn 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
137fn detect_object_types(path: &Path) -> Vec<PropertyObjectType> {
139 classify_object(&probe_object_attrs(path))
140}
141
142fn 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
160fn 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
171fn 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#[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 #[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 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 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 let types = classify_object(&attrs(false, 256, false, true));
275 assert_eq!(types, vec![PropertyObjectType::Inode]);
276 }
277
278 #[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 #[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 #[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}