1use crate::{Format, Runnable, util::human_bytes};
2use anyhow::{Context, Result};
3use btrfs_uapi::{chunk::chunk_list, filesystem::filesystem_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).with_context(|| {
63 format!("failed to open '{}'", self.path.display())
64 })?;
65 let fd = file.as_fd();
66
67 let fs = filesystem_info(fd).with_context(|| {
68 format!(
69 "failed to get filesystem info for '{}'",
70 self.path.display()
71 )
72 })?;
73
74 println!("UUID: {}", fs.uuid.as_hyphenated());
75
76 let mut entries = chunk_list(fd).with_context(|| {
77 format!("failed to read chunk tree for '{}'", self.path.display())
78 })?;
79
80 if entries.is_empty() {
81 println!("no chunks found");
82 return Ok(());
83 }
84
85 entries.sort_by_key(|e| (e.devid, e.physical_start));
88
89 let mut rows: Vec<Row> = Vec::with_capacity(entries.len());
92 let mut pcount: Vec<(u64, u64)> = Vec::new(); let mut logical_order = entries.clone();
97 logical_order.sort_by_key(|e| (e.devid, e.logical_start));
98 let mut lnumber_map: std::collections::HashMap<(u64, u64), u64> =
100 std::collections::HashMap::new();
101 {
102 let mut lcnt: Vec<(u64, u64)> = Vec::new();
103 for e in &logical_order {
104 let key = (e.devid, e.logical_start);
105 if !lnumber_map.contains_key(&key) {
106 let cnt = get_or_insert_count(&mut lcnt, e.devid);
107 lnumber_map.insert(key, cnt);
108 }
109 }
110 }
111
112 for e in &entries {
113 let pnumber = get_or_insert_count(&mut pcount, e.devid);
114 let lnumber =
115 *lnumber_map.get(&(e.devid, e.logical_start)).unwrap_or(&1);
116 let usage_pct = if e.length > 0 {
117 e.used as f64 / e.length as f64 * 100.0
118 } else {
119 0.0
120 };
121 rows.push(Row {
122 devid: e.devid,
123 pnumber,
124 flags_str: format_flags(e.flags),
125 physical_start: e.physical_start,
126 length: e.length,
127 physical_end: e.physical_start + e.length,
128 lnumber,
129 logical_start: e.logical_start,
130 usage_pct,
131 });
132 }
133 let devid_w = col_w("Devid", rows.iter().map(|r| digits(r.devid)));
135 let pnum_w = col_w("PNumber", rows.iter().map(|r| digits(r.pnumber)));
136 let type_w =
137 col_w("Type/profile", rows.iter().map(|r| r.flags_str.len()));
138 let pstart_w = col_w(
139 "PStart",
140 rows.iter().map(|r| human_bytes(r.physical_start).len()),
141 );
142 let length_w =
143 col_w("Length", rows.iter().map(|r| human_bytes(r.length).len()));
144 let pend_w = col_w(
145 "PEnd",
146 rows.iter().map(|r| human_bytes(r.physical_end).len()),
147 );
148 let lnum_w = col_w("LNumber", rows.iter().map(|r| digits(r.lnumber)));
149 let lstart_w = col_w(
150 "LStart",
151 rows.iter().map(|r| human_bytes(r.logical_start).len()),
152 );
153 let usage_w = "Usage%".len().max("100.00".len());
154
155 println!(
157 "{:>devid_w$} {:>pnum_w$} {:type_w$} {:>pstart_w$} {:>length_w$} {:>pend_w$} {:>lnum_w$} {:>lstart_w$} {:>usage_w$}",
158 "Devid",
159 "PNumber",
160 "Type/profile",
161 "PStart",
162 "Length",
163 "PEnd",
164 "LNumber",
165 "LStart",
166 "Usage%",
167 );
168 println!(
170 "{:->devid_w$} {:->pnum_w$} {:->type_w$} {:->pstart_w$} {:->length_w$} {:->pend_w$} {:->lnum_w$} {:->lstart_w$} {:->usage_w$}",
171 "", "", "", "", "", "", "", "", "",
172 );
173
174 for r in &rows {
176 println!(
177 "{:>devid_w$} {:>pnum_w$} {:type_w$} {:>pstart_w$} {:>length_w$} {:>pend_w$} {:>lnum_w$} {:>lstart_w$} {:>usage_w$.2}",
178 r.devid,
179 r.pnumber,
180 r.flags_str,
181 human_bytes(r.physical_start),
182 human_bytes(r.length),
183 human_bytes(r.physical_end),
184 r.lnumber,
185 human_bytes(r.logical_start),
186 r.usage_pct,
187 );
188 }
189
190 Ok(())
191 }
192}
193
194fn format_flags(flags: btrfs_uapi::space::BlockGroupFlags) -> String {
196 use btrfs_uapi::space::BlockGroupFlags as F;
197
198 let type_str = if flags.contains(F::DATA) {
199 "data"
200 } else if flags.contains(F::METADATA) {
201 "metadata"
202 } else if flags.contains(F::SYSTEM) {
203 "system"
204 } else {
205 "unknown"
206 };
207
208 let profile_str = if flags.contains(F::RAID10) {
209 "raid10"
210 } else if flags.contains(F::RAID1C4) {
211 "raid1c4"
212 } else if flags.contains(F::RAID1C3) {
213 "raid1c3"
214 } else if flags.contains(F::RAID1) {
215 "raid1"
216 } else if flags.contains(F::DUP) {
217 "dup"
218 } else if flags.contains(F::RAID0) {
219 "raid0"
220 } else if flags.contains(F::RAID5) {
221 "raid5"
222 } else if flags.contains(F::RAID6) {
223 "raid6"
224 } else {
225 "single"
226 };
227
228 format!("{type_str}/{profile_str}")
229}
230
231fn get_or_insert_count(counts: &mut Vec<(u64, u64)>, devid: u64) -> u64 {
234 if let Some(entry) = counts.iter_mut().find(|(d, _)| *d == devid) {
235 entry.1 += 1;
236 entry.1
237 } else {
238 counts.push((devid, 1));
239 1
240 }
241}
242
243fn col_w(header: &str, values: impl Iterator<Item = usize>) -> usize {
246 values.fold(header.len(), |acc, v| acc.max(v))
247}
248
249fn digits(n: u64) -> usize {
251 if n == 0 { 1 } else { n.ilog10() as usize + 1 }
252}