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#[derive(Parser, Debug)]
42pub struct ListChunksCommand {
43 path: PathBuf,
45}
46
47struct 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 entries.sort_by_key(|e| (e.devid, e.physical_start));
86
87 let mut rows: Vec<Row> = Vec::with_capacity(entries.len());
90 let mut pcount: Vec<(u64, u64)> = Vec::new(); let mut logical_order = entries.clone();
95 logical_order.sort_by_key(|e| (e.devid, e.logical_start));
96 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 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 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 println!(
165 "{:->devid_w$} {:->pnum_w$} {:->type_w$} {:->pstart_w$} {:->length_w$} {:->pend_w$} {:->lnum_w$} {:->lstart_w$} {:->usage_w$}",
166 "", "", "", "", "", "", "", "", "",
167 );
168
169 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
189fn 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
226fn 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
238fn col_w(header: &str, values: impl Iterator<Item = usize>) -> usize {
241 values.fold(header.len(), |acc, v| acc.max(v))
242}
243
244fn digits(n: u64) -> usize {
246 if n == 0 { 1 } else { n.ilog10() as usize + 1 }
247}