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