1use anyhow::{Context, Result, bail};
2use chrono::{DateTime, Local};
3use serde::Serialize;
4use std::{
5 fs::{self, File},
6 os::unix::fs::FileTypeExt,
7 path::Path,
8 str::FromStr,
9 time::{SystemTime, UNIX_EPOCH},
10};
11use uuid::Uuid;
12
13pub fn open_path(path: &Path) -> Result<File> {
15 File::open(path)
16 .with_context(|| format!("failed to open '{}'", path.display()))
17}
18
19pub fn is_mounted(device: &Path) -> bool {
26 btrfs_uapi::filesystem::is_mounted(device).unwrap_or(false)
27}
28
29pub enum SizeFormat {
31 Raw,
33 HumanIec,
35 HumanSi,
37 Fixed(u64),
39}
40
41pub fn fmt_size(bytes: u64, mode: &SizeFormat) -> String {
43 match mode {
44 SizeFormat::Raw => bytes.to_string(),
45 SizeFormat::HumanIec => human_bytes(bytes),
46 SizeFormat::HumanSi => human_bytes_si(bytes),
47 SizeFormat::Fixed(divisor) => format!("{}", bytes / divisor),
48 }
49}
50
51#[allow(clippy::cast_precision_loss)]
53pub fn human_bytes(bytes: u64) -> String {
54 const UNITS: &[&str] = &["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
55 let mut value = bytes as f64;
56 let mut unit = 0;
57 while value >= 1024.0 && unit + 1 < UNITS.len() {
58 value /= 1024.0;
59 unit += 1;
60 }
61 format!("{value:.2}{}", UNITS[unit])
62}
63
64#[allow(clippy::cast_precision_loss)]
66pub fn human_bytes_si(bytes: u64) -> String {
67 const UNITS: &[&str] = &["B", "kB", "MB", "GB", "TB", "PB"];
68 let mut value = bytes as f64;
69 let mut unit = 0;
70 while value >= 1000.0 && unit + 1 < UNITS.len() {
71 value /= 1000.0;
72 unit += 1;
73 }
74 format!("{value:.2}{}", UNITS[unit])
75}
76
77pub fn format_time(t: SystemTime) -> String {
82 if t == UNIX_EPOCH {
83 return "-".to_string();
84 }
85 match DateTime::<Local>::from(t)
86 .format("%Y-%m-%d %H:%M:%S %z")
87 .to_string()
88 {
89 s if s.is_empty() => "-".to_string(),
90 s => s,
91 }
92}
93
94pub fn format_time_short(t: &SystemTime) -> String {
97 DateTime::<Local>::from(*t).format("%e.%b %T").to_string()
98}
99
100pub fn format_timespec(sec: u64, nsec: u32) -> String {
102 #[allow(clippy::cast_possible_wrap)] let sec_i64 = sec as i64;
104 match DateTime::from_timestamp(sec_i64, nsec) {
105 Some(utc) => {
106 let local = utc.with_timezone(&Local);
107 format!("{}.{} ({})", sec, nsec, local.format("%Y-%m-%d %H:%M:%S"))
108 }
109 None => format!("{sec}.{nsec}"),
110 }
111}
112
113pub fn parse_size_with_suffix(s: &str) -> Result<u64> {
115 let (num_str, suffix) = match s.find(|c: char| c.is_alphabetic()) {
116 Some(i) => (&s[..i], &s[i..]),
117 None => (s, ""),
118 };
119 let n: u64 = num_str
120 .parse()
121 .with_context(|| format!("invalid size number: '{num_str}'"))?;
122 let multiplier: u64 = match suffix.to_uppercase().as_str() {
123 "" => 1,
124 "K" => 1024,
125 "M" => 1024 * 1024,
126 "G" => 1024 * 1024 * 1024,
127 "T" => 1024u64.pow(4),
128 "P" => 1024u64.pow(5),
129 "E" => 1024u64.pow(6),
130 _ => anyhow::bail!("unknown size suffix: '{suffix}'"),
131 };
132 n.checked_mul(multiplier)
133 .ok_or_else(|| anyhow::anyhow!("size overflow: '{s}'"))
134}
135
136#[derive(Debug, Clone, Copy)]
141pub struct ParsedUuid(Uuid);
142
143impl std::ops::Deref for ParsedUuid {
144 type Target = Uuid;
145 fn deref(&self) -> &Uuid {
146 &self.0
147 }
148}
149
150pub fn parse_qgroupid(s: &str) -> anyhow::Result<u64> {
155 let (level_str, id_str) = s.split_once('/').ok_or_else(|| {
156 anyhow::anyhow!("invalid qgroup ID '{s}': expected <level>/<id>")
157 })?;
158 let level: u64 = level_str.parse().map_err(|_| {
159 anyhow::anyhow!("invalid qgroup level '{level_str}' in '{s}'")
160 })?;
161 let subvolid: u64 = id_str.parse().map_err(|_| {
162 anyhow::anyhow!("invalid qgroup subvolid '{id_str}' in '{s}'")
163 })?;
164 Ok((level << 48) | subvolid)
165}
166
167impl FromStr for ParsedUuid {
168 type Err = String;
169
170 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
171 match s {
172 "clear" => Ok(Self(Uuid::nil())),
173 "random" => Ok(Self(Uuid::new_v4())),
174 "time" => Ok(Self(Uuid::now_v7())),
175 _ => Uuid::parse_str(s)
176 .map(Self)
177 .map_err(|e| format!("invalid UUID: {e}")),
178 }
179 }
180}
181
182pub fn check_device_for_overwrite(device: &Path, force: bool) -> Result<()> {
187 let meta = fs::metadata(device).with_context(|| {
188 format!("cannot access device '{}'", device.display())
189 })?;
190
191 if !meta.file_type().is_block_device() {
192 bail!("'{}' is not a block device", device.display());
193 }
194
195 if is_device_mounted(device)? {
196 bail!(
197 "'{}' is mounted; refusing to use a mounted device",
198 device.display()
199 );
200 }
201
202 if !force && has_btrfs_superblock(device) {
203 bail!(
204 "'{}' already contains a btrfs filesystem; use -f to force",
205 device.display()
206 );
207 }
208
209 Ok(())
210}
211
212pub fn is_device_mounted(device: &Path) -> Result<bool> {
216 btrfs_uapi::filesystem::is_mounted(device).with_context(|| {
217 format!("cannot check mount status of '{}'", device.display())
218 })
219}
220
221pub fn has_btrfs_superblock(device: &Path) -> bool {
224 let Ok(mut file) = File::open(device) else {
225 return false;
226 };
227 match btrfs_disk::superblock::read_superblock(&mut file, 0) {
228 Ok(sb) => sb.magic_is_valid(),
229 Err(_) => false,
230 }
231}
232
233pub fn print_json(key: &str, data: &impl Serialize) -> Result<()> {
245 #[derive(Serialize)]
246 struct Header {
247 version: &'static str,
248 }
249
250 let mut map = serde_json::Map::new();
251 map.insert(
252 "__header".to_string(),
253 serde_json::to_value(Header { version: "1" })?,
254 );
255 map.insert(key.to_string(), serde_json::to_value(data)?);
256
257 serde_json::to_writer_pretty(std::io::stdout(), &map)?;
258 println!();
259 Ok(())
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
269 fn human_bytes_zero() {
270 assert_eq!(human_bytes(0), "0.00B");
271 }
272
273 #[test]
274 fn human_bytes_small() {
275 assert_eq!(human_bytes(1), "1.00B");
276 assert_eq!(human_bytes(1023), "1023.00B");
277 }
278
279 #[test]
280 fn human_bytes_exact_powers() {
281 assert_eq!(human_bytes(1024), "1.00KiB");
282 assert_eq!(human_bytes(1024 * 1024), "1.00MiB");
283 assert_eq!(human_bytes(1024 * 1024 * 1024), "1.00GiB");
284 assert_eq!(human_bytes(1024u64.pow(4)), "1.00TiB");
285 assert_eq!(human_bytes(1024u64.pow(5)), "1.00PiB");
286 }
287
288 #[test]
289 fn human_bytes_fractional() {
290 assert_eq!(
292 human_bytes(1024 * 1024 * 1024 + 512 * 1024 * 1024),
293 "1.50GiB"
294 );
295 }
296
297 #[test]
298 fn human_bytes_u64_max() {
299 let s = human_bytes(u64::MAX);
301 assert!(s.ends_with("PiB"), "expected PiB suffix, got: {s}");
302 }
303
304 #[test]
307 fn parse_size_bare_number() {
308 assert_eq!(parse_size_with_suffix("0").unwrap(), 0);
309 assert_eq!(parse_size_with_suffix("42").unwrap(), 42);
310 }
311
312 #[test]
313 fn parse_size_all_suffixes() {
314 assert_eq!(parse_size_with_suffix("1K").unwrap(), 1024);
315 assert_eq!(parse_size_with_suffix("1M").unwrap(), 1024 * 1024);
316 assert_eq!(parse_size_with_suffix("1G").unwrap(), 1024 * 1024 * 1024);
317 assert_eq!(parse_size_with_suffix("1T").unwrap(), 1024u64.pow(4));
318 assert_eq!(parse_size_with_suffix("1P").unwrap(), 1024u64.pow(5));
319 assert_eq!(parse_size_with_suffix("1E").unwrap(), 1024u64.pow(6));
320 }
321
322 #[test]
323 fn parse_size_case_insensitive() {
324 assert_eq!(parse_size_with_suffix("4k").unwrap(), 4 * 1024);
325 assert_eq!(
326 parse_size_with_suffix("2g").unwrap(),
327 2 * 1024 * 1024 * 1024
328 );
329 }
330
331 #[test]
332 fn parse_size_overflow() {
333 assert!(parse_size_with_suffix("16385P").is_err());
334 }
335
336 #[test]
337 fn parse_size_bad_number() {
338 assert!(parse_size_with_suffix("abcM").is_err());
339 assert!(parse_size_with_suffix("").is_err());
340 }
341
342 #[test]
343 fn parse_size_unknown_suffix() {
344 assert!(parse_size_with_suffix("10X").is_err());
345 }
346
347 #[test]
350 fn parse_qgroupid_level0() {
351 assert_eq!(parse_qgroupid("0/5").unwrap(), 5);
352 assert_eq!(parse_qgroupid("0/256").unwrap(), 256);
353 }
354
355 #[test]
356 fn parse_qgroupid_higher_level() {
357 assert_eq!(parse_qgroupid("1/256").unwrap(), (1u64 << 48) | 256);
358 assert_eq!(parse_qgroupid("2/0").unwrap(), 2u64 << 48);
359 }
360
361 #[test]
362 fn parse_qgroupid_missing_slash() {
363 assert!(parse_qgroupid("5").is_err());
364 }
365
366 #[test]
367 fn parse_qgroupid_bad_level() {
368 assert!(parse_qgroupid("abc/5").is_err());
369 }
370
371 #[test]
372 fn parse_qgroupid_bad_subvolid() {
373 assert!(parse_qgroupid("0/abc").is_err());
374 }
375
376 #[test]
379 fn parsed_uuid_clear() {
380 let u: ParsedUuid = "clear".parse().unwrap();
381 assert!(u.is_nil());
382 }
383
384 #[test]
385 fn parsed_uuid_random() {
386 let u: ParsedUuid = "random".parse().unwrap();
387 assert!(!u.is_nil());
388 }
389
390 #[test]
391 fn parsed_uuid_time() {
392 let u: ParsedUuid = "time".parse().unwrap();
393 assert!(!u.is_nil());
394 }
395
396 #[test]
397 fn parsed_uuid_explicit() {
398 let u: ParsedUuid =
399 "550e8400-e29b-41d4-a716-446655440000".parse().unwrap();
400 assert_eq!(u.to_string(), "550e8400-e29b-41d4-a716-446655440000");
401 }
402
403 #[test]
404 fn parsed_uuid_no_hyphens() {
405 let u: ParsedUuid = "550e8400e29b41d4a716446655440000".parse().unwrap();
406 assert_eq!(u.to_string(), "550e8400-e29b-41d4-a716-446655440000");
407 }
408
409 #[test]
410 fn parsed_uuid_invalid() {
411 assert!("not-a-uuid".parse::<ParsedUuid>().is_err());
412 assert!("".parse::<ParsedUuid>().is_err());
413 }
414}