1use crate::{Format, Runnable, util::open_path};
2use anyhow::{Context, Result};
3use btrfs_uapi::subvolume::{
4 SubvolumeFlags, SubvolumeListItem, subvolume_list,
5};
6use clap::Parser;
7use std::{
8 cmp::Ordering, fmt::Write as _, os::unix::io::AsFd, path::PathBuf,
9 str::FromStr,
10};
11
12const HEADING_PATH_FILTERING: &str = "Path filtering";
13const HEADING_FIELD_SELECTION: &str = "Field selection";
14const HEADING_TYPE_FILTERING: &str = "Type filtering";
15const HEADING_SORTING: &str = "Sorting";
16
17#[derive(Parser, Debug)]
24#[allow(clippy::struct_excessive_bools)]
25pub struct SubvolumeListCommand {
26 #[clap(short = 'o', long, help_heading = HEADING_PATH_FILTERING)]
28 only_below: bool,
29
30 #[clap(short = 'a', long, help_heading = HEADING_PATH_FILTERING)]
33 all: bool,
34
35 #[clap(short, long, help_heading = HEADING_FIELD_SELECTION)]
37 parent: bool,
38
39 #[clap(short = 'c', long, help_heading = HEADING_FIELD_SELECTION)]
41 ogeneration: bool,
42
43 #[clap(short, long, help_heading = HEADING_FIELD_SELECTION)]
46 generation: bool,
47
48 #[clap(short, long, help_heading = HEADING_FIELD_SELECTION)]
50 uuid: bool,
51
52 #[clap(short = 'Q', long, help_heading = HEADING_FIELD_SELECTION)]
54 parent_uuid: bool,
55
56 #[clap(short = 'R', long, help_heading = HEADING_FIELD_SELECTION)]
58 received_uuid: bool,
59
60 #[clap(short = 's', long, help_heading = HEADING_TYPE_FILTERING)]
62 snapshots_only: bool,
63
64 #[clap(short = 'r', long, help_heading = HEADING_TYPE_FILTERING)]
66 readonly: bool,
67
68 #[clap(short = 'd', long, help_heading = HEADING_TYPE_FILTERING)]
70 deleted: bool,
71
72 #[clap(short = 't', long, help_heading = "Other")]
74 table: bool,
75
76 #[clap(short = 'G', long, value_name = "[+|-]VALUE", allow_hyphen_values = true, help_heading = HEADING_SORTING)]
78 gen_filter: Option<GenFilter>,
79
80 #[clap(short = 'C', long, value_name = "[+|-]VALUE", allow_hyphen_values = true, help_heading = HEADING_SORTING)]
82 ogen_filter: Option<GenFilter>,
83
84 #[clap(
89 long,
90 value_name = "KEYS",
91 value_delimiter = ',',
92 allow_hyphen_values = true,
93 help_heading = HEADING_SORTING,
94 )]
95 sort: Vec<SortKey>,
96
97 path: PathBuf,
99}
100
101impl Runnable for SubvolumeListCommand {
102 fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
103 let file = open_path(&self.path)?;
104
105 let mut items = subvolume_list(file.as_fd()).with_context(|| {
106 format!("failed to list subvolumes for '{}'", self.path.display())
107 })?;
108
109 let top_id = btrfs_uapi::inode::lookup_path_rootid(file.as_fd())
110 .with_context(|| "failed to get root id for path")?;
111
112 if self.deleted {
117 items.retain(|item| item.parent_id == 0);
118 } else if !self.all {
119 items.retain(|item| item.parent_id != 0);
120 }
121 if self.readonly {
122 items.retain(|item| item.flags.contains(SubvolumeFlags::RDONLY));
123 }
124 if self.snapshots_only {
125 items.retain(|item| !item.parent_uuid.is_nil());
126 }
127 if let Some(ref f) = self.gen_filter {
128 items.retain(|item| f.matches(item.generation));
129 }
130 if let Some(ref f) = self.ogen_filter {
131 items.retain(|item| f.matches(item.otransid));
132 }
133 if self.only_below {
134 items.retain(|item| item.parent_id == top_id);
138 }
139
140 if self.all {
143 for item in &mut items {
144 if item.parent_id != 0
145 && item.parent_id != top_id
146 && !item.name.is_empty()
147 {
148 item.name = format!("<FS_TREE>/{}", item.name);
149 }
150 }
151 }
152
153 if self.sort.is_empty() {
155 items.sort_by_key(|item| item.root_id);
156 } else {
157 items.sort_by(|a, b| {
158 for key in &self.sort {
159 let ord = key.compare(a, b);
160 if ord != Ordering::Equal {
161 return ord;
162 }
163 }
164 Ordering::Equal
165 });
166 }
167
168 if self.table {
169 self.print_table(&items);
170 } else {
171 self.print_default(&items);
172 }
173
174 Ok(())
175 }
176}
177
178impl SubvolumeListCommand {
179 fn print_default(&self, items: &[SubvolumeListItem]) {
180 for item in items {
181 let name = if item.name.is_empty() {
182 "<unknown>"
183 } else {
184 &item.name
185 };
186
187 let mut line =
191 format!("ID {} gen {}", item.root_id, item.generation);
192
193 if self.ogeneration {
194 let _ = write!(line, " cgen {}", item.otransid);
195 }
196
197 let _ = write!(line, " top level {}", item.parent_id);
198
199 if self.parent {
200 let _ = write!(line, " parent {}", item.parent_id);
201 }
202
203 let _ = write!(line, " path {name}");
204
205 if self.uuid {
206 let _ = write!(line, " uuid {}", fmt_uuid(&item.uuid));
207 }
208
209 if self.parent_uuid {
210 let _ = write!(
211 line,
212 " parent_uuid {}",
213 fmt_uuid(&item.parent_uuid)
214 );
215 }
216
217 if self.received_uuid {
218 let _ = write!(
219 line,
220 " received_uuid {}",
221 fmt_uuid(&item.received_uuid)
222 );
223 }
224
225 println!("{line}");
226 }
227 }
228
229 fn print_table(&self, items: &[SubvolumeListItem]) {
230 let mut headers: Vec<&str> = vec!["ID", "gen"];
232 if self.ogeneration {
233 headers.push("cgen");
234 }
235 headers.push("top level");
236 if self.parent {
237 headers.push("parent");
238 }
239 headers.push("path");
240 if self.uuid {
241 headers.push("uuid");
242 }
243 if self.parent_uuid {
244 headers.push("parent_uuid");
245 }
246 if self.received_uuid {
247 headers.push("received_uuid");
248 }
249
250 println!("{}", headers.join("\t"));
252
253 let sep: Vec<String> =
255 headers.iter().map(|h| "-".repeat(h.len())).collect();
256 println!("{}", sep.join("\t"));
257
258 for item in items {
260 let name = if item.name.is_empty() {
261 "<unknown>"
262 } else {
263 &item.name
264 };
265
266 let mut cols: Vec<String> =
267 vec![item.root_id.to_string(), item.generation.to_string()];
268 if self.ogeneration {
269 cols.push(item.otransid.to_string());
270 }
271 cols.push(item.parent_id.to_string());
272 if self.parent {
273 cols.push(item.parent_id.to_string());
274 }
275 cols.push(name.to_string());
276 if self.uuid {
277 cols.push(fmt_uuid(&item.uuid));
278 }
279 if self.parent_uuid {
280 cols.push(fmt_uuid(&item.parent_uuid));
281 }
282 if self.received_uuid {
283 cols.push(fmt_uuid(&item.received_uuid));
284 }
285
286 println!("{}", cols.join("\t"));
287 }
288 }
289}
290
291fn fmt_uuid(u: &uuid::Uuid) -> String {
292 if u.is_nil() {
293 "-".to_string()
294 } else {
295 u.hyphenated().to_string()
296 }
297}
298
299#[derive(Debug, Clone)]
301pub enum GenFilter {
302 Exact(u64),
303 AtLeast(u64),
304 AtMost(u64),
305}
306
307impl GenFilter {
308 fn matches(&self, value: u64) -> bool {
309 match self {
310 GenFilter::Exact(v) => value == *v,
311 GenFilter::AtLeast(v) => value >= *v,
312 GenFilter::AtMost(v) => value <= *v,
313 }
314 }
315}
316
317impl FromStr for GenFilter {
318 type Err = String;
319
320 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
321 if let Some(rest) = s.strip_prefix('+') {
322 let v: u64 = rest
323 .parse()
324 .map_err(|_| format!("invalid number: '{rest}'"))?;
325 Ok(GenFilter::AtLeast(v))
326 } else if let Some(rest) = s.strip_prefix('-') {
327 let v: u64 = rest
328 .parse()
329 .map_err(|_| format!("invalid number: '{rest}'"))?;
330 Ok(GenFilter::AtMost(v))
331 } else {
332 let v: u64 =
333 s.parse().map_err(|_| format!("invalid number: '{s}'"))?;
334 Ok(GenFilter::Exact(v))
335 }
336 }
337}
338
339#[derive(Debug, Clone)]
341pub struct SortKey {
342 field: SortField,
343 descending: bool,
344}
345
346#[derive(Debug, Clone)]
347enum SortField {
348 Gen,
349 Ogen,
350 Rootid,
351 Path,
352}
353
354impl SortKey {
355 fn compare(
356 &self,
357 a: &SubvolumeListItem,
358 b: &SubvolumeListItem,
359 ) -> Ordering {
360 let ord = match self.field {
361 SortField::Gen => a.generation.cmp(&b.generation),
362 SortField::Ogen => a.otransid.cmp(&b.otransid),
363 SortField::Rootid => a.root_id.cmp(&b.root_id),
364 SortField::Path => a.name.cmp(&b.name),
365 };
366 if self.descending { ord.reverse() } else { ord }
367 }
368}
369
370impl FromStr for SortKey {
371 type Err = String;
372
373 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
374 let (descending, field_str) = if let Some(rest) = s.strip_prefix('-') {
375 (true, rest)
376 } else if let Some(rest) = s.strip_prefix('+') {
377 (false, rest)
378 } else {
379 (false, s)
380 };
381
382 let field = match field_str {
383 "gen" => SortField::Gen,
384 "ogen" => SortField::Ogen,
385 "rootid" => SortField::Rootid,
386 "path" => SortField::Path,
387 _ => {
388 return Err(format!(
389 "unknown sort key: '{field_str}' (expected gen, ogen, rootid, or path)"
390 ));
391 }
392 };
393
394 Ok(SortKey { field, descending })
395 }
396}