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