Skip to main content

btrfs_cli/inspect/
list_chunks.rs

1use crate::{
2    Format, RunContext, Runnable,
3    filesystem::UnitMode,
4    util::{fmt_size, open_path},
5};
6use anyhow::{Context, Result, bail};
7use btrfs_disk::{
8    items::BlockGroupItem,
9    reader,
10    tree::{KeyType, TreeBlock},
11};
12use btrfs_uapi::{
13    chunk::chunk_list, filesystem::filesystem_info, space::BlockGroupFlags,
14};
15use clap::Parser;
16use cols::Cols;
17use std::{
18    cmp::Ordering,
19    collections::HashMap,
20    fs::File,
21    io::{Read, Seek},
22    os::unix::io::AsFd,
23    path::PathBuf,
24};
25
26/// List all chunks in the filesystem, one row per stripe
27///
28/// Enumerates every chunk across all devices by walking the chunk tree.
29/// For striped profiles (RAID0, RAID10, RAID5, RAID6) each logical chunk
30/// maps to multiple stripes on different devices, so it appears on multiple
31/// rows. For DUP each logical chunk maps to two physical stripes on the same
32/// device, so it also appears twice. For single and non-striped profiles
33/// there is a 1:1 correspondence between logical chunks and rows.
34///
35/// Requires CAP_SYS_ADMIN (unless --offline is used).
36///
37/// Columns:
38///
39/// Devid: btrfs device ID the stripe lives on.
40///
41/// PNumber: physical chunk index on this device, ordered by physical start
42/// offset (1-based).
43///
44/// Type/profile: block-group type (data, metadata, system) and replication
45/// profile (single, dup, raid0, raid1, ...).
46///
47/// PStart: physical byte offset of this stripe on the device.
48///
49/// Length: logical length of the chunk (shared by all its stripes).
50///
51/// PEnd: physical byte offset of the end of this stripe (PStart + Length).
52///
53/// LNumber: logical chunk index for this device, ordered by logical start
54/// offset (1-based); DUP stripes share the same value.
55///
56/// LStart: logical byte offset of the chunk in the filesystem address space.
57///
58/// Usage%: percentage of the chunk's logical space currently occupied
59/// (used / length * 100), sourced from the extent tree.
60#[derive(Parser, Debug)]
61#[allow(clippy::doc_markdown)]
62pub struct ListChunksCommand {
63    #[clap(flatten)]
64    pub units: UnitMode,
65
66    /// Sort output by the given columns (comma-separated).
67    /// Prepend - for descending order.
68    /// Keys: devid, pstart, lstart, usage, length, type, profile.
69    /// Default: devid,pstart.
70    #[clap(long, value_name = "KEYS")]
71    pub sort: Option<String>,
72
73    /// Read directly from an unmounted device or image file instead of
74    /// a mounted filesystem. Does not require CAP_SYS_ADMIN.
75    #[clap(long)]
76    pub offline: bool,
77
78    /// Path to a file or directory on the btrfs filesystem, or a block
79    /// device / image file when --device is used
80    path: PathBuf,
81}
82
83/// One row in the output table.
84struct Row {
85    devid: u64,
86    pnumber: u64,
87    flags_str: String,
88    physical_start: u64,
89    length: u64,
90    physical_end: u64,
91    lnumber: u64,
92    logical_start: u64,
93    usage_pct: f64,
94}
95
96impl Runnable for ListChunksCommand {
97    #[allow(clippy::too_many_lines, clippy::cast_precision_loss)]
98    fn run(&self, ctx: &RunContext) -> Result<()> {
99        let mode = self.units.resolve();
100        let fmt = |bytes| fmt_size(bytes, &mode);
101
102        let mut rows = if self.offline {
103            self.collect_offline()?
104        } else {
105            self.collect_online()?
106        };
107
108        if rows.is_empty() {
109            println!("no chunks found");
110            return Ok(());
111        }
112
113        // Apply user-specified sort if given.
114        if let Some(ref sort_str) = self.sort {
115            let specs = parse_sort_specs(sort_str)?;
116            rows.sort_by(|a, b| compare_rows(a, b, &specs));
117        }
118
119        match ctx.format {
120            Format::Modern => {
121                print_chunks_modern(&rows, &fmt);
122            }
123            Format::Text => {
124                print_chunks_text(&rows, &fmt);
125            }
126            Format::Json => unreachable!(),
127        }
128
129        Ok(())
130    }
131}
132
133impl ListChunksCommand {
134    /// Collect chunk data from a mounted filesystem via ioctls.
135    #[allow(clippy::cast_precision_loss)]
136    fn collect_online(&self) -> Result<Vec<Row>> {
137        let file = open_path(&self.path)?;
138        let fd = file.as_fd();
139
140        let fs = filesystem_info(fd).with_context(|| {
141            format!(
142                "failed to get filesystem info for '{}'",
143                self.path.display()
144            )
145        })?;
146        println!("UUID: {}", fs.uuid.as_hyphenated());
147
148        let mut entries = chunk_list(fd).with_context(|| {
149            format!("failed to read chunk tree for '{}'", self.path.display())
150        })?;
151
152        if entries.is_empty() {
153            return Ok(Vec::new());
154        }
155
156        entries.sort_by_key(|e| (e.devid, e.physical_start));
157
158        let mut rows: Vec<Row> = Vec::with_capacity(entries.len());
159        let mut pcount: Vec<(u64, u64)> = Vec::new();
160
161        let mut logical_order = entries.clone();
162        logical_order.sort_by_key(|e| (e.devid, e.logical_start));
163        let mut lnumber_map: HashMap<(u64, u64), u64> = HashMap::new();
164        {
165            let mut lcnt: Vec<(u64, u64)> = Vec::new();
166            for e in &logical_order {
167                let key = (e.devid, e.logical_start);
168                lnumber_map
169                    .entry(key)
170                    .or_insert_with(|| get_or_insert_count(&mut lcnt, e.devid));
171            }
172        }
173
174        for e in &entries {
175            let pnumber = get_or_insert_count(&mut pcount, e.devid);
176            let lnumber =
177                *lnumber_map.get(&(e.devid, e.logical_start)).unwrap_or(&1);
178            let usage_pct = if e.length > 0 {
179                e.used as f64 / e.length as f64 * 100.0
180            } else {
181                0.0
182            };
183            rows.push(Row {
184                devid: e.devid,
185                pnumber,
186                flags_str: format_flags(e.flags),
187                physical_start: e.physical_start,
188                length: e.length,
189                physical_end: e.physical_start + e.length,
190                lnumber,
191                logical_start: e.logical_start,
192                usage_pct,
193            });
194        }
195
196        Ok(rows)
197    }
198
199    /// Collect chunk data by reading directly from a device or image file.
200    #[allow(clippy::cast_precision_loss)]
201    fn collect_offline(&self) -> Result<Vec<Row>> {
202        let file = File::open(&self.path).with_context(|| {
203            format!("failed to open '{}'", self.path.display())
204        })?;
205
206        let mut open = reader::filesystem_open(file).with_context(|| {
207            format!(
208                "failed to open btrfs filesystem on '{}'",
209                self.path.display()
210            )
211        })?;
212
213        println!("UUID: {}", open.superblock.fsid.as_hyphenated());
214
215        // Collect block group usage from the extent tree.
216        let bg_used =
217            collect_block_group_usage(&mut open.reader, &open.tree_roots);
218
219        // Build one entry per stripe from the chunk cache.
220        let mut entries: Vec<OfflineEntry> = Vec::new();
221        for chunk in open.reader.chunk_cache().iter() {
222            let flags = BlockGroupFlags::from_bits_truncate(chunk.chunk_type);
223            let used = bg_used.get(&chunk.logical).copied().unwrap_or(0);
224            for stripe in &chunk.stripes {
225                entries.push(OfflineEntry {
226                    devid: stripe.devid,
227                    physical_start: stripe.offset,
228                    logical_start: chunk.logical,
229                    length: chunk.length,
230                    flags,
231                    used,
232                });
233            }
234        }
235
236        if entries.is_empty() {
237            return Ok(Vec::new());
238        }
239
240        entries.sort_by_key(|e| (e.devid, e.physical_start));
241
242        let mut rows: Vec<Row> = Vec::with_capacity(entries.len());
243        let mut pcount: Vec<(u64, u64)> = Vec::new();
244
245        let mut logical_order = entries.clone();
246        logical_order.sort_by_key(|e| (e.devid, e.logical_start));
247        let mut lnumber_map: HashMap<(u64, u64), u64> = HashMap::new();
248        {
249            let mut lcnt: Vec<(u64, u64)> = Vec::new();
250            for e in &logical_order {
251                let key = (e.devid, e.logical_start);
252                lnumber_map
253                    .entry(key)
254                    .or_insert_with(|| get_or_insert_count(&mut lcnt, e.devid));
255            }
256        }
257
258        for e in &entries {
259            let pnumber = get_or_insert_count(&mut pcount, e.devid);
260            let lnumber =
261                *lnumber_map.get(&(e.devid, e.logical_start)).unwrap_or(&1);
262            let usage_pct = if e.length > 0 {
263                e.used as f64 / e.length as f64 * 100.0
264            } else {
265                0.0
266            };
267            rows.push(Row {
268                devid: e.devid,
269                pnumber,
270                flags_str: format_flags(e.flags),
271                physical_start: e.physical_start,
272                length: e.length,
273                physical_end: e.physical_start + e.length,
274                lnumber,
275                logical_start: e.logical_start,
276                usage_pct,
277            });
278        }
279
280        Ok(rows)
281    }
282}
283
284/// Intermediate entry for offline chunk data, mirroring `ChunkEntry` from uapi.
285#[derive(Clone)]
286struct OfflineEntry {
287    devid: u64,
288    physical_start: u64,
289    logical_start: u64,
290    length: u64,
291    flags: BlockGroupFlags,
292    used: u64,
293}
294
295/// Walk the block group tree (or extent tree as fallback) to collect
296/// block group usage (logical_start -> used bytes).
297///
298/// Modern filesystems with `BLOCK_GROUP_TREE` (tree 11) store block group
299/// items there instead of the extent tree.
300fn collect_block_group_usage<R: Read + Seek>(
301    block_reader: &mut reader::BlockReader<R>,
302    tree_roots: &std::collections::BTreeMap<u64, (u64, u64)>,
303) -> HashMap<u64, u64> {
304    let mut bg_used: HashMap<u64, u64> = HashMap::new();
305
306    // Prefer the dedicated block group tree; fall back to the extent tree.
307    let bg_root = tree_roots
308        .get(&u64::from(btrfs_disk::raw::BTRFS_BLOCK_GROUP_TREE_OBJECTID))
309        .or_else(|| {
310            tree_roots
311                .get(&u64::from(btrfs_disk::raw::BTRFS_EXTENT_TREE_OBJECTID))
312        })
313        .map(|&(bytenr, _)| bytenr);
314
315    let Some(bg_root) = bg_root else {
316        return bg_used;
317    };
318
319    let mut visitor = |_raw: &[u8], block: &TreeBlock| {
320        if let TreeBlock::Leaf { items, data, .. } = block {
321            for item in items {
322                if item.key.key_type != KeyType::BlockGroupItem {
323                    continue;
324                }
325                let start =
326                    std::mem::size_of::<btrfs_disk::raw::btrfs_header>()
327                        + item.offset as usize;
328                let item_data = &data[start..][..item.size as usize];
329                if let Some(bg) = BlockGroupItem::parse(item_data) {
330                    bg_used.insert(item.key.objectid, bg.used);
331                }
332            }
333        }
334    };
335
336    let _ = reader::tree_walk_tolerant(
337        block_reader,
338        bg_root,
339        &mut visitor,
340        &mut |_, _| {},
341    );
342
343    bg_used
344}
345
346#[derive(Debug, Clone, Copy)]
347enum SortKey {
348    Devid,
349    PStart,
350    LStart,
351    Usage,
352    Length,
353    Type,
354    Profile,
355}
356
357#[derive(Debug, Clone, Copy)]
358struct SortSpec {
359    key: SortKey,
360    descending: bool,
361}
362
363fn parse_sort_specs(input: &str) -> Result<Vec<SortSpec>> {
364    let mut specs = Vec::new();
365    for token in input.split(',') {
366        let token = token.trim();
367        if token.is_empty() {
368            continue;
369        }
370        let (descending, name) = if let Some(rest) = token.strip_prefix('-') {
371            (true, rest)
372        } else if let Some(rest) = token.strip_prefix('+') {
373            (false, rest)
374        } else {
375            (false, token)
376        };
377        let key = match name {
378            "devid" => SortKey::Devid,
379            "pstart" => SortKey::PStart,
380            "lstart" => SortKey::LStart,
381            "usage" => SortKey::Usage,
382            "length" => SortKey::Length,
383            "type" => SortKey::Type,
384            "profile" => SortKey::Profile,
385            _ => bail!("unknown sort key: '{name}'"),
386        };
387        specs.push(SortSpec { key, descending });
388    }
389    Ok(specs)
390}
391
392fn type_ord(flags: &str) -> u8 {
393    if flags.starts_with("data/") {
394        0
395    } else if flags.starts_with("metadata/") {
396        1
397    } else if flags.starts_with("system/") {
398        2
399    } else {
400        3
401    }
402}
403
404fn profile_ord(flags: &str) -> u8 {
405    let profile = flags.rsplit('/').next().unwrap_or("");
406    match profile {
407        "single" => 0,
408        "dup" => 1,
409        "raid0" => 2,
410        "raid1" => 3,
411        "raid1c3" => 4,
412        "raid1c4" => 5,
413        "raid10" => 6,
414        "raid5" => 7,
415        "raid6" => 8,
416        _ => 9,
417    }
418}
419
420fn compare_rows(a: &Row, b: &Row, specs: &[SortSpec]) -> Ordering {
421    for spec in specs {
422        let ord = match spec.key {
423            SortKey::Devid => a.devid.cmp(&b.devid),
424            SortKey::PStart => a.physical_start.cmp(&b.physical_start),
425            SortKey::LStart => a.logical_start.cmp(&b.logical_start),
426            SortKey::Usage => a.usage_pct.total_cmp(&b.usage_pct),
427            SortKey::Length => a.length.cmp(&b.length),
428            SortKey::Type => {
429                type_ord(&a.flags_str).cmp(&type_ord(&b.flags_str))
430            }
431            SortKey::Profile => {
432                profile_ord(&a.flags_str).cmp(&profile_ord(&b.flags_str))
433            }
434        };
435        let ord = if spec.descending { ord.reverse() } else { ord };
436        if ord != Ordering::Equal {
437            return ord;
438        }
439    }
440    Ordering::Equal
441}
442
443/// Format `BlockGroupFlags` as `"<type>/<profile>"`, e.g. `"data/single"`.
444fn format_flags(flags: btrfs_uapi::space::BlockGroupFlags) -> String {
445    use btrfs_uapi::space::BlockGroupFlags as F;
446
447    let type_str = if flags.contains(F::DATA) {
448        "data"
449    } else if flags.contains(F::METADATA) {
450        "metadata"
451    } else if flags.contains(F::SYSTEM) {
452        "system"
453    } else {
454        "unknown"
455    };
456
457    let profile_str = if flags.contains(F::RAID10) {
458        "raid10"
459    } else if flags.contains(F::RAID1C4) {
460        "raid1c4"
461    } else if flags.contains(F::RAID1C3) {
462        "raid1c3"
463    } else if flags.contains(F::RAID1) {
464        "raid1"
465    } else if flags.contains(F::DUP) {
466        "dup"
467    } else if flags.contains(F::RAID0) {
468        "raid0"
469    } else if flags.contains(F::RAID5) {
470        "raid5"
471    } else if flags.contains(F::RAID6) {
472        "raid6"
473    } else {
474        "single"
475    };
476
477    format!("{type_str}/{profile_str}")
478}
479
480/// Increment the counter for `devid` in the vec, returning the new value
481/// (1-based).
482fn get_or_insert_count(counts: &mut Vec<(u64, u64)>, devid: u64) -> u64 {
483    if let Some(entry) = counts.iter_mut().find(|(d, _)| *d == devid) {
484        entry.1 += 1;
485        entry.1
486    } else {
487        counts.push((devid, 1));
488        1
489    }
490}
491
492/// Compute the display width for a column: the max of the header width and
493/// the widths of all data values.
494fn col_w(header: &str, values: impl Iterator<Item = usize>) -> usize {
495    values.fold(header.len(), std::cmp::Ord::max)
496}
497
498/// Number of decimal digits in `n` (minimum 1).
499fn digits(n: u64) -> usize {
500    if n == 0 { 1 } else { n.ilog10() as usize + 1 }
501}
502
503#[derive(Cols)]
504struct ChunkRow {
505    #[column(header = "DEVID", right)]
506    devid: u64,
507    #[column(header = "PNUM", right)]
508    pnumber: u64,
509    #[column(header = "TYPE/PROFILE")]
510    flags: String,
511    #[column(header = "PSTART", right)]
512    pstart: String,
513    #[column(header = "LENGTH", right)]
514    length: String,
515    #[column(header = "PEND", right)]
516    pend: String,
517    #[column(header = "LNUM", right)]
518    lnumber: u64,
519    #[column(header = "LSTART", right)]
520    lstart: String,
521    #[column(header = "USAGE%", right)]
522    usage: String,
523}
524
525fn print_chunks_modern(rows: &[Row], fmt: &dyn Fn(u64) -> String) {
526    let chunk_rows: Vec<ChunkRow> = rows
527        .iter()
528        .map(|r| ChunkRow {
529            devid: r.devid,
530            pnumber: r.pnumber,
531            flags: r.flags_str.clone(),
532            pstart: fmt(r.physical_start),
533            length: fmt(r.length),
534            pend: fmt(r.physical_end),
535            lnumber: r.lnumber,
536            lstart: fmt(r.logical_start),
537            usage: format!("{:.2}", r.usage_pct),
538        })
539        .collect();
540    let mut out = std::io::stdout().lock();
541    let _ = ChunkRow::print_table(&chunk_rows, &mut out);
542}
543
544fn print_chunks_text(rows: &[Row], fmt: &dyn Fn(u64) -> String) {
545    let devid_w = col_w("Devid", rows.iter().map(|r| digits(r.devid)));
546    let pnum_w = col_w("PNumber", rows.iter().map(|r| digits(r.pnumber)));
547    let type_w = col_w("Type/profile", rows.iter().map(|r| r.flags_str.len()));
548    let pstart_w =
549        col_w("PStart", rows.iter().map(|r| fmt(r.physical_start).len()));
550    let length_w = col_w("Length", rows.iter().map(|r| fmt(r.length).len()));
551    let pend_w = col_w("PEnd", rows.iter().map(|r| fmt(r.physical_end).len()));
552    let lnum_w = col_w("LNumber", rows.iter().map(|r| digits(r.lnumber)));
553    let lstart_w =
554        col_w("LStart", rows.iter().map(|r| fmt(r.logical_start).len()));
555    let usage_w = "Usage%".len().max("100.00".len());
556
557    println!(
558        "{:>devid_w$}  {:>pnum_w$}  {:type_w$}  {:>pstart_w$}  {:>length_w$}  {:>pend_w$}  {:>lnum_w$}  {:>lstart_w$}  {:>usage_w$}",
559        "Devid",
560        "PNumber",
561        "Type/profile",
562        "PStart",
563        "Length",
564        "PEnd",
565        "LNumber",
566        "LStart",
567        "Usage%",
568    );
569    println!(
570        "{:->devid_w$}  {:->pnum_w$}  {:->type_w$}  {:->pstart_w$}  {:->length_w$}  {:->pend_w$}  {:->lnum_w$}  {:->lstart_w$}  {:->usage_w$}",
571        "", "", "", "", "", "", "", "", "",
572    );
573    for r in rows {
574        println!(
575            "{:>devid_w$}  {:>pnum_w$}  {:type_w$}  {:>pstart_w$}  {:>length_w$}  {:>pend_w$}  {:>lnum_w$}  {:>lstart_w$}  {:>usage_w$.2}",
576            r.devid,
577            r.pnumber,
578            r.flags_str,
579            fmt(r.physical_start),
580            fmt(r.length),
581            fmt(r.physical_end),
582            r.lnumber,
583            fmt(r.logical_start),
584            r.usage_pct,
585        );
586    }
587}