Skip to main content

btrfs_cli/inspect/
list_chunks.rs

1use crate::{
2    Format, Runnable,
3    filesystem::UnitMode,
4    util::{fmt_size, open_path},
5};
6use anyhow::{Context, Result, bail};
7use btrfs_uapi::{chunk::chunk_list, filesystem::filesystem_info};
8use clap::Parser;
9use std::{cmp::Ordering, os::unix::io::AsFd, path::PathBuf};
10
11/// List all chunks in the filesystem, one row per stripe
12///
13/// Enumerates every chunk across all devices by walking the chunk tree.
14/// For striped profiles (RAID0, RAID10, RAID5, RAID6) each logical chunk
15/// maps to multiple stripes on different devices, so it appears on multiple
16/// rows. For DUP each logical chunk maps to two physical stripes on the same
17/// device, so it also appears twice. For single and non-striped profiles
18/// there is a 1:1 correspondence between logical chunks and rows.
19///
20/// Requires CAP_SYS_ADMIN.
21///
22/// Columns:
23///
24/// Devid: btrfs device ID the stripe lives on.
25///
26/// PNumber: physical chunk index on this device, ordered by physical start
27/// offset (1-based).
28///
29/// Type/profile: block-group type (data, metadata, system) and replication
30/// profile (single, dup, raid0, raid1, ...).
31///
32/// PStart: physical byte offset of this stripe on the device.
33///
34/// Length: logical length of the chunk (shared by all its stripes).
35///
36/// PEnd: physical byte offset of the end of this stripe (PStart + Length).
37///
38/// LNumber: logical chunk index for this device, ordered by logical start
39/// offset (1-based); DUP stripes share the same value.
40///
41/// LStart: logical byte offset of the chunk in the filesystem address space.
42///
43/// Usage%: percentage of the chunk's logical space currently occupied
44/// (used / length * 100), sourced from the extent tree.
45#[derive(Parser, Debug)]
46pub struct ListChunksCommand {
47    #[clap(flatten)]
48    pub units: UnitMode,
49
50    /// Sort output by the given columns (comma-separated).
51    /// Prepend - for descending order.
52    /// Keys: devid, pstart, lstart, usage, length, type, profile.
53    /// Default: devid,pstart.
54    #[clap(long, value_name = "KEYS")]
55    pub sort: Option<String>,
56
57    /// Path to a file or directory on the btrfs filesystem
58    path: PathBuf,
59}
60
61/// One row in the output table.
62struct Row {
63    devid: u64,
64    pnumber: u64,
65    flags_str: String,
66    physical_start: u64,
67    length: u64,
68    physical_end: u64,
69    lnumber: u64,
70    logical_start: u64,
71    usage_pct: f64,
72}
73
74impl Runnable for ListChunksCommand {
75    fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
76        let mode = self.units.resolve();
77        let fmt = |bytes| fmt_size(bytes, &mode);
78        let file = open_path(&self.path)?;
79        let fd = file.as_fd();
80
81        let fs = filesystem_info(fd).with_context(|| {
82            format!(
83                "failed to get filesystem info for '{}'",
84                self.path.display()
85            )
86        })?;
87
88        println!("UUID: {}", fs.uuid.as_hyphenated());
89
90        let mut entries = chunk_list(fd).with_context(|| {
91            format!("failed to read chunk tree for '{}'", self.path.display())
92        })?;
93
94        if entries.is_empty() {
95            println!("no chunks found");
96            return Ok(());
97        }
98
99        // Sort by (devid, physical_start) to assign pnumber sequentially
100        // per device in physical order.
101        entries.sort_by_key(|e| (e.devid, e.physical_start));
102
103        // Assign pnumber (1-based, per devid) and lnumber (1-based,
104        // per devid, in the order we encounter logical chunks).
105        let mut rows: Vec<Row> = Vec::with_capacity(entries.len());
106        let mut pcount: Vec<(u64, u64)> = Vec::new(); // (devid, count)
107
108        // Build lnumber by iterating in the original (logical) order first.
109        // Re-sort a copy by (devid, logical_start) to assign lnumbers.
110        let mut logical_order = entries.clone();
111        logical_order.sort_by_key(|e| (e.devid, e.logical_start));
112        // Map (devid, logical_start) -> lnumber (1-based).
113        let mut lnumber_map: std::collections::HashMap<(u64, u64), u64> =
114            std::collections::HashMap::new();
115        {
116            let mut lcnt: Vec<(u64, u64)> = Vec::new();
117            for e in &logical_order {
118                let key = (e.devid, e.logical_start);
119                lnumber_map
120                    .entry(key)
121                    .or_insert_with(|| get_or_insert_count(&mut lcnt, e.devid));
122            }
123        }
124
125        for e in &entries {
126            let pnumber = get_or_insert_count(&mut pcount, e.devid);
127            let lnumber =
128                *lnumber_map.get(&(e.devid, e.logical_start)).unwrap_or(&1);
129            let usage_pct = if e.length > 0 {
130                e.used as f64 / e.length as f64 * 100.0
131            } else {
132                0.0
133            };
134            rows.push(Row {
135                devid: e.devid,
136                pnumber,
137                flags_str: format_flags(e.flags),
138                physical_start: e.physical_start,
139                length: e.length,
140                physical_end: e.physical_start + e.length,
141                lnumber,
142                logical_start: e.logical_start,
143                usage_pct,
144            });
145        }
146
147        // Apply user-specified sort if given.
148        if let Some(ref sort_str) = self.sort {
149            let specs = parse_sort_specs(sort_str)?;
150            rows.sort_by(|a, b| compare_rows(a, b, &specs));
151        }
152
153        // Compute column widths.
154        let devid_w = col_w("Devid", rows.iter().map(|r| digits(r.devid)));
155        let pnum_w = col_w("PNumber", rows.iter().map(|r| digits(r.pnumber)));
156        let type_w =
157            col_w("Type/profile", rows.iter().map(|r| r.flags_str.len()));
158        let pstart_w =
159            col_w("PStart", rows.iter().map(|r| fmt(r.physical_start).len()));
160        let length_w =
161            col_w("Length", rows.iter().map(|r| fmt(r.length).len()));
162        let pend_w =
163            col_w("PEnd", rows.iter().map(|r| fmt(r.physical_end).len()));
164        let lnum_w = col_w("LNumber", rows.iter().map(|r| digits(r.lnumber)));
165        let lstart_w =
166            col_w("LStart", rows.iter().map(|r| fmt(r.logical_start).len()));
167        let usage_w = "Usage%".len().max("100.00".len());
168
169        // Header
170        println!(
171            "{:>devid_w$}  {:>pnum_w$}  {:type_w$}  {:>pstart_w$}  {:>length_w$}  {:>pend_w$}  {:>lnum_w$}  {:>lstart_w$}  {:>usage_w$}",
172            "Devid",
173            "PNumber",
174            "Type/profile",
175            "PStart",
176            "Length",
177            "PEnd",
178            "LNumber",
179            "LStart",
180            "Usage%",
181        );
182        // Separator
183        println!(
184            "{:->devid_w$}  {:->pnum_w$}  {:->type_w$}  {:->pstart_w$}  {:->length_w$}  {:->pend_w$}  {:->lnum_w$}  {:->lstart_w$}  {:->usage_w$}",
185            "", "", "", "", "", "", "", "", "",
186        );
187
188        // Rows
189        for r in &rows {
190            println!(
191                "{:>devid_w$}  {:>pnum_w$}  {:type_w$}  {:>pstart_w$}  {:>length_w$}  {:>pend_w$}  {:>lnum_w$}  {:>lstart_w$}  {:>usage_w$.2}",
192                r.devid,
193                r.pnumber,
194                r.flags_str,
195                fmt(r.physical_start),
196                fmt(r.length),
197                fmt(r.physical_end),
198                r.lnumber,
199                fmt(r.logical_start),
200                r.usage_pct,
201            );
202        }
203
204        Ok(())
205    }
206}
207
208#[derive(Debug, Clone, Copy)]
209enum SortKey {
210    Devid,
211    PStart,
212    LStart,
213    Usage,
214    Length,
215    Type,
216    Profile,
217}
218
219#[derive(Debug, Clone, Copy)]
220struct SortSpec {
221    key: SortKey,
222    descending: bool,
223}
224
225fn parse_sort_specs(input: &str) -> Result<Vec<SortSpec>> {
226    let mut specs = Vec::new();
227    for token in input.split(',') {
228        let token = token.trim();
229        if token.is_empty() {
230            continue;
231        }
232        let (descending, name) = if let Some(rest) = token.strip_prefix('-') {
233            (true, rest)
234        } else if let Some(rest) = token.strip_prefix('+') {
235            (false, rest)
236        } else {
237            (false, token)
238        };
239        let key = match name {
240            "devid" => SortKey::Devid,
241            "pstart" => SortKey::PStart,
242            "lstart" => SortKey::LStart,
243            "usage" => SortKey::Usage,
244            "length" => SortKey::Length,
245            "type" => SortKey::Type,
246            "profile" => SortKey::Profile,
247            _ => bail!("unknown sort key: '{name}'"),
248        };
249        specs.push(SortSpec { key, descending });
250    }
251    Ok(specs)
252}
253
254fn type_ord(flags: &str) -> u8 {
255    if flags.starts_with("data/") {
256        0
257    } else if flags.starts_with("metadata/") {
258        1
259    } else if flags.starts_with("system/") {
260        2
261    } else {
262        3
263    }
264}
265
266fn profile_ord(flags: &str) -> u8 {
267    let profile = flags.rsplit('/').next().unwrap_or("");
268    match profile {
269        "single" => 0,
270        "dup" => 1,
271        "raid0" => 2,
272        "raid1" => 3,
273        "raid1c3" => 4,
274        "raid1c4" => 5,
275        "raid10" => 6,
276        "raid5" => 7,
277        "raid6" => 8,
278        _ => 9,
279    }
280}
281
282fn compare_rows(a: &Row, b: &Row, specs: &[SortSpec]) -> Ordering {
283    for spec in specs {
284        let ord = match spec.key {
285            SortKey::Devid => a.devid.cmp(&b.devid),
286            SortKey::PStart => a.physical_start.cmp(&b.physical_start),
287            SortKey::LStart => a.logical_start.cmp(&b.logical_start),
288            SortKey::Usage => a.usage_pct.total_cmp(&b.usage_pct),
289            SortKey::Length => a.length.cmp(&b.length),
290            SortKey::Type => {
291                type_ord(&a.flags_str).cmp(&type_ord(&b.flags_str))
292            }
293            SortKey::Profile => {
294                profile_ord(&a.flags_str).cmp(&profile_ord(&b.flags_str))
295            }
296        };
297        let ord = if spec.descending { ord.reverse() } else { ord };
298        if ord != Ordering::Equal {
299            return ord;
300        }
301    }
302    Ordering::Equal
303}
304
305/// Format `BlockGroupFlags` as `"<type>/<profile>"`, e.g. `"data/single"`.
306fn format_flags(flags: btrfs_uapi::space::BlockGroupFlags) -> String {
307    use btrfs_uapi::space::BlockGroupFlags as F;
308
309    let type_str = if flags.contains(F::DATA) {
310        "data"
311    } else if flags.contains(F::METADATA) {
312        "metadata"
313    } else if flags.contains(F::SYSTEM) {
314        "system"
315    } else {
316        "unknown"
317    };
318
319    let profile_str = if flags.contains(F::RAID10) {
320        "raid10"
321    } else if flags.contains(F::RAID1C4) {
322        "raid1c4"
323    } else if flags.contains(F::RAID1C3) {
324        "raid1c3"
325    } else if flags.contains(F::RAID1) {
326        "raid1"
327    } else if flags.contains(F::DUP) {
328        "dup"
329    } else if flags.contains(F::RAID0) {
330        "raid0"
331    } else if flags.contains(F::RAID5) {
332        "raid5"
333    } else if flags.contains(F::RAID6) {
334        "raid6"
335    } else {
336        "single"
337    };
338
339    format!("{type_str}/{profile_str}")
340}
341
342/// Increment the counter for `devid` in the vec, returning the new value
343/// (1-based).
344fn get_or_insert_count(counts: &mut Vec<(u64, u64)>, devid: u64) -> u64 {
345    if let Some(entry) = counts.iter_mut().find(|(d, _)| *d == devid) {
346        entry.1 += 1;
347        entry.1
348    } else {
349        counts.push((devid, 1));
350        1
351    }
352}
353
354/// Compute the display width for a column: the max of the header width and
355/// the widths of all data values.
356fn col_w(header: &str, values: impl Iterator<Item = usize>) -> usize {
357    values.fold(header.len(), |acc, v| acc.max(v))
358}
359
360/// Number of decimal digits in `n` (minimum 1).
361fn digits(n: u64) -> usize {
362    if n == 0 { 1 } else { n.ilog10() as usize + 1 }
363}