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