1use crate::{
2 Format, Runnable,
3 filesystem::UnitMode,
4 util::{fmt_size, open_path},
5};
6use anyhow::{Context, Result, bail};
7use btrfs_uapi::{chunk::chunk_list, filesystem::filesystem_info};
8use clap::Parser;
9use std::{cmp::Ordering, os::unix::io::AsFd, path::PathBuf};
10
11#[derive(Parser, Debug)]
46#[allow(clippy::doc_markdown)]
47pub struct ListChunksCommand {
48 #[clap(flatten)]
49 pub units: UnitMode,
50
51 #[clap(long, value_name = "KEYS")]
56 pub sort: Option<String>,
57
58 path: PathBuf,
60}
61
62struct Row {
64 devid: u64,
65 pnumber: u64,
66 flags_str: String,
67 physical_start: u64,
68 length: u64,
69 physical_end: u64,
70 lnumber: u64,
71 logical_start: u64,
72 usage_pct: f64,
73}
74
75impl Runnable for ListChunksCommand {
76 #[allow(clippy::too_many_lines, clippy::cast_precision_loss)]
77 fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
78 let mode = self.units.resolve();
79 let fmt = |bytes| fmt_size(bytes, &mode);
80 let file = open_path(&self.path)?;
81 let fd = file.as_fd();
82
83 let fs = filesystem_info(fd).with_context(|| {
84 format!(
85 "failed to get filesystem info for '{}'",
86 self.path.display()
87 )
88 })?;
89
90 println!("UUID: {}", fs.uuid.as_hyphenated());
91
92 let mut entries = chunk_list(fd).with_context(|| {
93 format!("failed to read chunk tree for '{}'", self.path.display())
94 })?;
95
96 if entries.is_empty() {
97 println!("no chunks found");
98 return Ok(());
99 }
100
101 entries.sort_by_key(|e| (e.devid, e.physical_start));
104
105 let mut rows: Vec<Row> = Vec::with_capacity(entries.len());
108 let mut pcount: Vec<(u64, u64)> = Vec::new(); let mut logical_order = entries.clone();
113 logical_order.sort_by_key(|e| (e.devid, e.logical_start));
114 let mut lnumber_map: std::collections::HashMap<(u64, u64), u64> =
116 std::collections::HashMap::new();
117 {
118 let mut lcnt: Vec<(u64, u64)> = Vec::new();
119 for e in &logical_order {
120 let key = (e.devid, e.logical_start);
121 lnumber_map
122 .entry(key)
123 .or_insert_with(|| get_or_insert_count(&mut lcnt, e.devid));
124 }
125 }
126
127 for e in &entries {
128 let pnumber = get_or_insert_count(&mut pcount, e.devid);
129 let lnumber =
130 *lnumber_map.get(&(e.devid, e.logical_start)).unwrap_or(&1);
131 let usage_pct = if e.length > 0 {
132 e.used as f64 / e.length as f64 * 100.0
133 } else {
134 0.0
135 };
136 rows.push(Row {
137 devid: e.devid,
138 pnumber,
139 flags_str: format_flags(e.flags),
140 physical_start: e.physical_start,
141 length: e.length,
142 physical_end: e.physical_start + e.length,
143 lnumber,
144 logical_start: e.logical_start,
145 usage_pct,
146 });
147 }
148
149 if let Some(ref sort_str) = self.sort {
151 let specs = parse_sort_specs(sort_str)?;
152 rows.sort_by(|a, b| compare_rows(a, b, &specs));
153 }
154
155 let devid_w = col_w("Devid", rows.iter().map(|r| digits(r.devid)));
157 let pnum_w = col_w("PNumber", rows.iter().map(|r| digits(r.pnumber)));
158 let type_w =
159 col_w("Type/profile", rows.iter().map(|r| r.flags_str.len()));
160 let pstart_w =
161 col_w("PStart", rows.iter().map(|r| fmt(r.physical_start).len()));
162 let length_w =
163 col_w("Length", rows.iter().map(|r| fmt(r.length).len()));
164 let pend_w =
165 col_w("PEnd", rows.iter().map(|r| fmt(r.physical_end).len()));
166 let lnum_w = col_w("LNumber", rows.iter().map(|r| digits(r.lnumber)));
167 let lstart_w =
168 col_w("LStart", rows.iter().map(|r| fmt(r.logical_start).len()));
169 let usage_w = "Usage%".len().max("100.00".len());
170
171 println!(
173 "{:>devid_w$} {:>pnum_w$} {:type_w$} {:>pstart_w$} {:>length_w$} {:>pend_w$} {:>lnum_w$} {:>lstart_w$} {:>usage_w$}",
174 "Devid",
175 "PNumber",
176 "Type/profile",
177 "PStart",
178 "Length",
179 "PEnd",
180 "LNumber",
181 "LStart",
182 "Usage%",
183 );
184 println!(
186 "{:->devid_w$} {:->pnum_w$} {:->type_w$} {:->pstart_w$} {:->length_w$} {:->pend_w$} {:->lnum_w$} {:->lstart_w$} {:->usage_w$}",
187 "", "", "", "", "", "", "", "", "",
188 );
189
190 for r in &rows {
192 println!(
193 "{:>devid_w$} {:>pnum_w$} {:type_w$} {:>pstart_w$} {:>length_w$} {:>pend_w$} {:>lnum_w$} {:>lstart_w$} {:>usage_w$.2}",
194 r.devid,
195 r.pnumber,
196 r.flags_str,
197 fmt(r.physical_start),
198 fmt(r.length),
199 fmt(r.physical_end),
200 r.lnumber,
201 fmt(r.logical_start),
202 r.usage_pct,
203 );
204 }
205
206 Ok(())
207 }
208}
209
210#[derive(Debug, Clone, Copy)]
211enum SortKey {
212 Devid,
213 PStart,
214 LStart,
215 Usage,
216 Length,
217 Type,
218 Profile,
219}
220
221#[derive(Debug, Clone, Copy)]
222struct SortSpec {
223 key: SortKey,
224 descending: bool,
225}
226
227fn parse_sort_specs(input: &str) -> Result<Vec<SortSpec>> {
228 let mut specs = Vec::new();
229 for token in input.split(',') {
230 let token = token.trim();
231 if token.is_empty() {
232 continue;
233 }
234 let (descending, name) = if let Some(rest) = token.strip_prefix('-') {
235 (true, rest)
236 } else if let Some(rest) = token.strip_prefix('+') {
237 (false, rest)
238 } else {
239 (false, token)
240 };
241 let key = match name {
242 "devid" => SortKey::Devid,
243 "pstart" => SortKey::PStart,
244 "lstart" => SortKey::LStart,
245 "usage" => SortKey::Usage,
246 "length" => SortKey::Length,
247 "type" => SortKey::Type,
248 "profile" => SortKey::Profile,
249 _ => bail!("unknown sort key: '{name}'"),
250 };
251 specs.push(SortSpec { key, descending });
252 }
253 Ok(specs)
254}
255
256fn type_ord(flags: &str) -> u8 {
257 if flags.starts_with("data/") {
258 0
259 } else if flags.starts_with("metadata/") {
260 1
261 } else if flags.starts_with("system/") {
262 2
263 } else {
264 3
265 }
266}
267
268fn profile_ord(flags: &str) -> u8 {
269 let profile = flags.rsplit('/').next().unwrap_or("");
270 match profile {
271 "single" => 0,
272 "dup" => 1,
273 "raid0" => 2,
274 "raid1" => 3,
275 "raid1c3" => 4,
276 "raid1c4" => 5,
277 "raid10" => 6,
278 "raid5" => 7,
279 "raid6" => 8,
280 _ => 9,
281 }
282}
283
284fn compare_rows(a: &Row, b: &Row, specs: &[SortSpec]) -> Ordering {
285 for spec in specs {
286 let ord = match spec.key {
287 SortKey::Devid => a.devid.cmp(&b.devid),
288 SortKey::PStart => a.physical_start.cmp(&b.physical_start),
289 SortKey::LStart => a.logical_start.cmp(&b.logical_start),
290 SortKey::Usage => a.usage_pct.total_cmp(&b.usage_pct),
291 SortKey::Length => a.length.cmp(&b.length),
292 SortKey::Type => {
293 type_ord(&a.flags_str).cmp(&type_ord(&b.flags_str))
294 }
295 SortKey::Profile => {
296 profile_ord(&a.flags_str).cmp(&profile_ord(&b.flags_str))
297 }
298 };
299 let ord = if spec.descending { ord.reverse() } else { ord };
300 if ord != Ordering::Equal {
301 return ord;
302 }
303 }
304 Ordering::Equal
305}
306
307fn format_flags(flags: btrfs_uapi::space::BlockGroupFlags) -> String {
309 use btrfs_uapi::space::BlockGroupFlags as F;
310
311 let type_str = if flags.contains(F::DATA) {
312 "data"
313 } else if flags.contains(F::METADATA) {
314 "metadata"
315 } else if flags.contains(F::SYSTEM) {
316 "system"
317 } else {
318 "unknown"
319 };
320
321 let profile_str = if flags.contains(F::RAID10) {
322 "raid10"
323 } else if flags.contains(F::RAID1C4) {
324 "raid1c4"
325 } else if flags.contains(F::RAID1C3) {
326 "raid1c3"
327 } else if flags.contains(F::RAID1) {
328 "raid1"
329 } else if flags.contains(F::DUP) {
330 "dup"
331 } else if flags.contains(F::RAID0) {
332 "raid0"
333 } else if flags.contains(F::RAID5) {
334 "raid5"
335 } else if flags.contains(F::RAID6) {
336 "raid6"
337 } else {
338 "single"
339 };
340
341 format!("{type_str}/{profile_str}")
342}
343
344fn get_or_insert_count(counts: &mut Vec<(u64, u64)>, devid: u64) -> u64 {
347 if let Some(entry) = counts.iter_mut().find(|(d, _)| *d == devid) {
348 entry.1 += 1;
349 entry.1
350 } else {
351 counts.push((devid, 1));
352 1
353 }
354}
355
356fn col_w(header: &str, values: impl Iterator<Item = usize>) -> usize {
359 values.fold(header.len(), std::cmp::Ord::max)
360}
361
362fn digits(n: u64) -> usize {
364 if n == 0 { 1 } else { n.ilog10() as usize + 1 }
365}