Skip to main content

btrfs_cli/inspect/
list_chunks.rs

1use crate::{Format, Runnable, util::human_bytes};
2use anyhow::{Context, Result};
3use btrfs_uapi::{chunk::chunk_list, filesystem::filesystem_info};
4use clap::Parser;
5use std::{fs::File, os::unix::io::AsFd, path::PathBuf};
6
7/// List all chunks in the filesystem, one row per stripe
8///
9/// Enumerates every chunk across all devices by walking the chunk tree.
10/// For striped profiles (RAID0, RAID10, RAID5, RAID6) each logical chunk
11/// maps to multiple stripes on different devices, so it appears on multiple
12/// rows. For DUP each logical chunk maps to two physical stripes on the same
13/// device, so it also appears twice. For single and non-striped profiles
14/// there is a 1:1 correspondence between logical chunks and rows.
15///
16/// Requires CAP_SYS_ADMIN.
17///
18/// Columns:
19///
20/// Devid: btrfs device ID the stripe lives on.
21///
22/// PNumber: physical chunk index on this device, ordered by physical start
23/// offset (1-based).
24///
25/// Type/profile: block-group type (data, metadata, system) and replication
26/// profile (single, dup, raid0, raid1, ...).
27///
28/// PStart: physical byte offset of this stripe on the device.
29///
30/// Length: logical length of the chunk (shared by all its stripes).
31///
32/// PEnd: physical byte offset of the end of this stripe (PStart + Length).
33///
34/// LNumber: logical chunk index for this device, ordered by logical start
35/// offset (1-based); DUP stripes share the same value.
36///
37/// LStart: logical byte offset of the chunk in the filesystem address space.
38///
39/// Usage%: percentage of the chunk's logical space currently occupied
40/// (used / length * 100), sourced from the extent tree.
41#[derive(Parser, Debug)]
42pub struct ListChunksCommand {
43    /// Path to a file or directory on the btrfs filesystem
44    path: PathBuf,
45}
46
47/// One row in the output table.
48struct Row {
49    devid: u64,
50    pnumber: u64,
51    flags_str: String,
52    physical_start: u64,
53    length: u64,
54    physical_end: u64,
55    lnumber: u64,
56    logical_start: u64,
57    usage_pct: f64,
58}
59
60impl Runnable for ListChunksCommand {
61    fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
62        let file = File::open(&self.path).with_context(|| {
63            format!("failed to open '{}'", self.path.display())
64        })?;
65        let fd = file.as_fd();
66
67        let fs = filesystem_info(fd).with_context(|| {
68            format!(
69                "failed to get filesystem info for '{}'",
70                self.path.display()
71            )
72        })?;
73
74        println!("UUID: {}", fs.uuid.as_hyphenated());
75
76        let mut entries = chunk_list(fd).with_context(|| {
77            format!("failed to read chunk tree for '{}'", self.path.display())
78        })?;
79
80        if entries.is_empty() {
81            println!("no chunks found");
82            return Ok(());
83        }
84
85        // Sort by (devid, physical_start) to assign pnumber sequentially
86        // per device in physical order.
87        entries.sort_by_key(|e| (e.devid, e.physical_start));
88
89        // Assign pnumber (1-based, per devid) and lnumber (1-based,
90        // per devid, in the order we encounter logical chunks).
91        let mut rows: Vec<Row> = Vec::with_capacity(entries.len());
92        let mut pcount: Vec<(u64, u64)> = Vec::new(); // (devid, count)
93
94        // Build lnumber by iterating in the original (logical) order first.
95        // Re-sort a copy by (devid, logical_start) to assign lnumbers.
96        let mut logical_order = entries.clone();
97        logical_order.sort_by_key(|e| (e.devid, e.logical_start));
98        // Map (devid, logical_start) -> lnumber (1-based).
99        let mut lnumber_map: std::collections::HashMap<(u64, u64), u64> =
100            std::collections::HashMap::new();
101        {
102            let mut lcnt: Vec<(u64, u64)> = Vec::new();
103            for e in &logical_order {
104                let key = (e.devid, e.logical_start);
105                if !lnumber_map.contains_key(&key) {
106                    let cnt = get_or_insert_count(&mut lcnt, e.devid);
107                    lnumber_map.insert(key, cnt);
108                }
109            }
110        }
111
112        for e in &entries {
113            let pnumber = get_or_insert_count(&mut pcount, e.devid);
114            let lnumber =
115                *lnumber_map.get(&(e.devid, e.logical_start)).unwrap_or(&1);
116            let usage_pct = if e.length > 0 {
117                e.used as f64 / e.length as f64 * 100.0
118            } else {
119                0.0
120            };
121            rows.push(Row {
122                devid: e.devid,
123                pnumber,
124                flags_str: format_flags(e.flags),
125                physical_start: e.physical_start,
126                length: e.length,
127                physical_end: e.physical_start + e.length,
128                lnumber,
129                logical_start: e.logical_start,
130                usage_pct,
131            });
132        }
133        // Compute column widths.
134        let devid_w = col_w("Devid", rows.iter().map(|r| digits(r.devid)));
135        let pnum_w = col_w("PNumber", rows.iter().map(|r| digits(r.pnumber)));
136        let type_w =
137            col_w("Type/profile", rows.iter().map(|r| r.flags_str.len()));
138        let pstart_w = col_w(
139            "PStart",
140            rows.iter().map(|r| human_bytes(r.physical_start).len()),
141        );
142        let length_w =
143            col_w("Length", rows.iter().map(|r| human_bytes(r.length).len()));
144        let pend_w = col_w(
145            "PEnd",
146            rows.iter().map(|r| human_bytes(r.physical_end).len()),
147        );
148        let lnum_w = col_w("LNumber", rows.iter().map(|r| digits(r.lnumber)));
149        let lstart_w = col_w(
150            "LStart",
151            rows.iter().map(|r| human_bytes(r.logical_start).len()),
152        );
153        let usage_w = "Usage%".len().max("100.00".len());
154
155        // Header
156        println!(
157            "{:>devid_w$}  {:>pnum_w$}  {:type_w$}  {:>pstart_w$}  {:>length_w$}  {:>pend_w$}  {:>lnum_w$}  {:>lstart_w$}  {:>usage_w$}",
158            "Devid",
159            "PNumber",
160            "Type/profile",
161            "PStart",
162            "Length",
163            "PEnd",
164            "LNumber",
165            "LStart",
166            "Usage%",
167        );
168        // Separator
169        println!(
170            "{:->devid_w$}  {:->pnum_w$}  {:->type_w$}  {:->pstart_w$}  {:->length_w$}  {:->pend_w$}  {:->lnum_w$}  {:->lstart_w$}  {:->usage_w$}",
171            "", "", "", "", "", "", "", "", "",
172        );
173
174        // Rows
175        for r in &rows {
176            println!(
177                "{:>devid_w$}  {:>pnum_w$}  {:type_w$}  {:>pstart_w$}  {:>length_w$}  {:>pend_w$}  {:>lnum_w$}  {:>lstart_w$}  {:>usage_w$.2}",
178                r.devid,
179                r.pnumber,
180                r.flags_str,
181                human_bytes(r.physical_start),
182                human_bytes(r.length),
183                human_bytes(r.physical_end),
184                r.lnumber,
185                human_bytes(r.logical_start),
186                r.usage_pct,
187            );
188        }
189
190        Ok(())
191    }
192}
193
194/// Format `BlockGroupFlags` as `"<type>/<profile>"`, e.g. `"data/single"`.
195fn format_flags(flags: btrfs_uapi::space::BlockGroupFlags) -> String {
196    use btrfs_uapi::space::BlockGroupFlags as F;
197
198    let type_str = if flags.contains(F::DATA) {
199        "data"
200    } else if flags.contains(F::METADATA) {
201        "metadata"
202    } else if flags.contains(F::SYSTEM) {
203        "system"
204    } else {
205        "unknown"
206    };
207
208    let profile_str = if flags.contains(F::RAID10) {
209        "raid10"
210    } else if flags.contains(F::RAID1C4) {
211        "raid1c4"
212    } else if flags.contains(F::RAID1C3) {
213        "raid1c3"
214    } else if flags.contains(F::RAID1) {
215        "raid1"
216    } else if flags.contains(F::DUP) {
217        "dup"
218    } else if flags.contains(F::RAID0) {
219        "raid0"
220    } else if flags.contains(F::RAID5) {
221        "raid5"
222    } else if flags.contains(F::RAID6) {
223        "raid6"
224    } else {
225        "single"
226    };
227
228    format!("{type_str}/{profile_str}")
229}
230
231/// Increment the counter for `devid` in the vec, returning the new value
232/// (1-based).
233fn get_or_insert_count(counts: &mut Vec<(u64, u64)>, devid: u64) -> u64 {
234    if let Some(entry) = counts.iter_mut().find(|(d, _)| *d == devid) {
235        entry.1 += 1;
236        entry.1
237    } else {
238        counts.push((devid, 1));
239        1
240    }
241}
242
243/// Compute the display width for a column: the max of the header width and
244/// the widths of all data values.
245fn col_w(header: &str, values: impl Iterator<Item = usize>) -> usize {
246    values.fold(header.len(), |acc, v| acc.max(v))
247}
248
249/// Number of decimal digits in `n` (minimum 1).
250fn digits(n: u64) -> usize {
251    if n == 0 { 1 } else { n.ilog10() as usize + 1 }
252}