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