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#[derive(Parser, Debug)]
61#[allow(clippy::doc_markdown)]
62pub struct ListChunksCommand {
63 #[clap(flatten)]
64 pub units: UnitMode,
65
66 #[clap(long, value_name = "KEYS")]
71 pub sort: Option<String>,
72
73 #[clap(long)]
76 pub offline: bool,
77
78 path: PathBuf,
81}
82
83struct 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 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 #[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 #[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 let bg_used =
217 collect_block_group_usage(&mut open.reader, &open.tree_roots);
218
219 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#[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
295fn 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 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
443fn 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
480fn 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
492fn col_w(header: &str, values: impl Iterator<Item = usize>) -> usize {
495 values.fold(header.len(), std::cmp::Ord::max)
496}
497
498fn 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}