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::fs_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)
63            .with_context(|| format!("failed to open '{}'", self.path.display()))?;
64        let fd = file.as_fd();
65
66        let fs = fs_info(fd).with_context(|| {
67            format!(
68                "failed to get filesystem info for '{}'",
69                self.path.display()
70            )
71        })?;
72
73        println!("UUID: {}", fs.uuid.as_hyphenated());
74
75        let mut entries = chunk_list(fd)
76            .with_context(|| format!("failed to read chunk tree for '{}'", self.path.display()))?;
77
78        if entries.is_empty() {
79            println!("no chunks found");
80            return Ok(());
81        }
82
83        // Sort by (devid, physical_start) to assign pnumber sequentially
84        // per device in physical order.
85        entries.sort_by_key(|e| (e.devid, e.physical_start));
86
87        // Assign pnumber (1-based, per devid) and lnumber (1-based,
88        // per devid, in the order we encounter logical chunks).
89        let mut rows: Vec<Row> = Vec::with_capacity(entries.len());
90        let mut pcount: Vec<(u64, u64)> = Vec::new(); // (devid, count)
91
92        // Build lnumber by iterating in the original (logical) order first.
93        // Re-sort a copy by (devid, logical_start) to assign lnumbers.
94        let mut logical_order = entries.clone();
95        logical_order.sort_by_key(|e| (e.devid, e.logical_start));
96        // Map (devid, logical_start) -> lnumber (1-based).
97        let mut lnumber_map: std::collections::HashMap<(u64, u64), u64> =
98            std::collections::HashMap::new();
99        {
100            let mut lcnt: Vec<(u64, u64)> = Vec::new();
101            for e in &logical_order {
102                let key = (e.devid, e.logical_start);
103                if !lnumber_map.contains_key(&key) {
104                    let cnt = get_or_insert_count(&mut lcnt, e.devid);
105                    lnumber_map.insert(key, cnt);
106                }
107            }
108        }
109
110        for e in &entries {
111            let pnumber = get_or_insert_count(&mut pcount, e.devid);
112            let lnumber = *lnumber_map.get(&(e.devid, e.logical_start)).unwrap_or(&1);
113            let usage_pct = if e.length > 0 {
114                e.used as f64 / e.length as f64 * 100.0
115            } else {
116                0.0
117            };
118            rows.push(Row {
119                devid: e.devid,
120                pnumber,
121                flags_str: format_flags(e.flags),
122                physical_start: e.physical_start,
123                length: e.length,
124                physical_end: e.physical_start + e.length,
125                lnumber,
126                logical_start: e.logical_start,
127                usage_pct,
128            });
129        }
130        // Compute column widths.
131        let devid_w = col_w("Devid", rows.iter().map(|r| digits(r.devid)));
132        let pnum_w = col_w("PNumber", rows.iter().map(|r| digits(r.pnumber)));
133        let type_w = col_w("Type/profile", rows.iter().map(|r| r.flags_str.len()));
134        let pstart_w = col_w(
135            "PStart",
136            rows.iter().map(|r| human_bytes(r.physical_start).len()),
137        );
138        let length_w = col_w("Length", rows.iter().map(|r| human_bytes(r.length).len()));
139        let pend_w = col_w(
140            "PEnd",
141            rows.iter().map(|r| human_bytes(r.physical_end).len()),
142        );
143        let lnum_w = col_w("LNumber", rows.iter().map(|r| digits(r.lnumber)));
144        let lstart_w = col_w(
145            "LStart",
146            rows.iter().map(|r| human_bytes(r.logical_start).len()),
147        );
148        let usage_w = "Usage%".len().max("100.00".len());
149
150        // Header
151        println!(
152            "{:>devid_w$}  {:>pnum_w$}  {:type_w$}  {:>pstart_w$}  {:>length_w$}  {:>pend_w$}  {:>lnum_w$}  {:>lstart_w$}  {:>usage_w$}",
153            "Devid",
154            "PNumber",
155            "Type/profile",
156            "PStart",
157            "Length",
158            "PEnd",
159            "LNumber",
160            "LStart",
161            "Usage%",
162        );
163        // Separator
164        println!(
165            "{:->devid_w$}  {:->pnum_w$}  {:->type_w$}  {:->pstart_w$}  {:->length_w$}  {:->pend_w$}  {:->lnum_w$}  {:->lstart_w$}  {:->usage_w$}",
166            "", "", "", "", "", "", "", "", "",
167        );
168
169        // Rows
170        for r in &rows {
171            println!(
172                "{:>devid_w$}  {:>pnum_w$}  {:type_w$}  {:>pstart_w$}  {:>length_w$}  {:>pend_w$}  {:>lnum_w$}  {:>lstart_w$}  {:>usage_w$.2}",
173                r.devid,
174                r.pnumber,
175                r.flags_str,
176                human_bytes(r.physical_start),
177                human_bytes(r.length),
178                human_bytes(r.physical_end),
179                r.lnumber,
180                human_bytes(r.logical_start),
181                r.usage_pct,
182            );
183        }
184
185        Ok(())
186    }
187}
188
189/// Format `BlockGroupFlags` as `"<type>/<profile>"`, e.g. `"data/single"`.
190fn format_flags(flags: btrfs_uapi::space::BlockGroupFlags) -> String {
191    use btrfs_uapi::space::BlockGroupFlags as F;
192
193    let type_str = if flags.contains(F::DATA) {
194        "data"
195    } else if flags.contains(F::METADATA) {
196        "metadata"
197    } else if flags.contains(F::SYSTEM) {
198        "system"
199    } else {
200        "unknown"
201    };
202
203    let profile_str = if flags.contains(F::RAID10) {
204        "raid10"
205    } else if flags.contains(F::RAID1C4) {
206        "raid1c4"
207    } else if flags.contains(F::RAID1C3) {
208        "raid1c3"
209    } else if flags.contains(F::RAID1) {
210        "raid1"
211    } else if flags.contains(F::DUP) {
212        "dup"
213    } else if flags.contains(F::RAID0) {
214        "raid0"
215    } else if flags.contains(F::RAID5) {
216        "raid5"
217    } else if flags.contains(F::RAID6) {
218        "raid6"
219    } else {
220        "single"
221    };
222
223    format!("{type_str}/{profile_str}")
224}
225
226/// Increment the counter for `devid` in the vec, returning the new value
227/// (1-based).
228fn get_or_insert_count(counts: &mut Vec<(u64, u64)>, devid: u64) -> u64 {
229    if let Some(entry) = counts.iter_mut().find(|(d, _)| *d == devid) {
230        entry.1 += 1;
231        entry.1
232    } else {
233        counts.push((devid, 1));
234        1
235    }
236}
237
238/// Compute the display width for a column: the max of the header width and
239/// the widths of all data values.
240fn col_w(header: &str, values: impl Iterator<Item = usize>) -> usize {
241    values.fold(header.len(), |acc, v| acc.max(v))
242}
243
244/// Number of decimal digits in `n` (minimum 1).
245fn digits(n: u64) -> usize {
246    if n == 0 { 1 } else { n.ilog10() as usize + 1 }
247}