Skip to main content

btrfs_cli/
util.rs

1use anyhow::{Context, Result};
2use std::str::FromStr;
3use uuid::Uuid;
4
5/// Format a byte count as a human-readable string using binary prefixes.
6pub fn human_bytes(bytes: u64) -> String {
7    const UNITS: &[&str] = &["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
8    let mut value = bytes as f64;
9    let mut unit = 0;
10    while value >= 1024.0 && unit + 1 < UNITS.len() {
11        value /= 1024.0;
12        unit += 1;
13    }
14    if unit == 0 {
15        format!("{bytes}B")
16    } else {
17        format!("{value:.2}{}", UNITS[unit])
18    }
19}
20
21/// Parse a size string with an optional binary suffix (K, M, G, T, P, E).
22pub fn parse_size_with_suffix(s: &str) -> Result<u64> {
23    let (num_str, suffix) = match s.find(|c: char| c.is_alphabetic()) {
24        Some(i) => (&s[..i], &s[i..]),
25        None => (s, ""),
26    };
27    let n: u64 = num_str
28        .parse()
29        .with_context(|| format!("invalid size number: '{num_str}'"))?;
30    let multiplier: u64 = match suffix.to_uppercase().as_str() {
31        "" => 1,
32        "K" => 1024,
33        "M" => 1024 * 1024,
34        "G" => 1024 * 1024 * 1024,
35        "T" => 1024u64.pow(4),
36        "P" => 1024u64.pow(5),
37        "E" => 1024u64.pow(6),
38        _ => anyhow::bail!("unknown size suffix: '{suffix}'"),
39    };
40    n.checked_mul(multiplier)
41        .ok_or_else(|| anyhow::anyhow!("size overflow: '{s}'"))
42}
43
44/// A UUID value parsed from a CLI argument.
45///
46/// Accepts `clear` (nil UUID), `random` (random v4 UUID), `time` (v7
47/// time-ordered UUID), or any standard UUID string (with or without hyphens).
48#[derive(Debug, Clone, Copy)]
49pub struct ParsedUuid(Uuid);
50
51impl std::ops::Deref for ParsedUuid {
52    type Target = Uuid;
53    fn deref(&self) -> &Uuid {
54        &self.0
55    }
56}
57
58/// Parse a qgroup ID string of the form `"<level>/<subvolid>"` into a packed u64.
59///
60/// The packed form is `(level as u64) << 48 | subvolid`.
61/// Example: `"0/5"` → `5`, `"1/256"` → `0x0001_0000_0000_0100`.
62pub fn parse_qgroupid(s: &str) -> anyhow::Result<u64> {
63    let (level_str, id_str) = s
64        .split_once('/')
65        .ok_or_else(|| anyhow::anyhow!("invalid qgroup ID '{}': expected <level>/<id>", s))?;
66    let level: u64 = level_str
67        .parse()
68        .map_err(|_| anyhow::anyhow!("invalid qgroup level '{}' in '{}'", level_str, s))?;
69    let subvolid: u64 = id_str
70        .parse()
71        .map_err(|_| anyhow::anyhow!("invalid qgroup subvolid '{}' in '{}'", id_str, s))?;
72    Ok((level << 48) | subvolid)
73}
74
75impl FromStr for ParsedUuid {
76    type Err = String;
77
78    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
79        match s {
80            "clear" => Ok(Self(Uuid::nil())),
81            "random" => Ok(Self(Uuid::new_v4())),
82            "time" => Ok(Self(Uuid::now_v7())),
83            _ => Uuid::parse_str(s)
84                .map(Self)
85                .map_err(|e| format!("invalid UUID: {e}")),
86        }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    // --- human_bytes ---
95
96    #[test]
97    fn human_bytes_zero() {
98        assert_eq!(human_bytes(0), "0B");
99    }
100
101    #[test]
102    fn human_bytes_small() {
103        assert_eq!(human_bytes(1), "1B");
104        assert_eq!(human_bytes(1023), "1023B");
105    }
106
107    #[test]
108    fn human_bytes_exact_powers() {
109        assert_eq!(human_bytes(1024), "1.00KiB");
110        assert_eq!(human_bytes(1024 * 1024), "1.00MiB");
111        assert_eq!(human_bytes(1024 * 1024 * 1024), "1.00GiB");
112        assert_eq!(human_bytes(1024u64.pow(4)), "1.00TiB");
113        assert_eq!(human_bytes(1024u64.pow(5)), "1.00PiB");
114    }
115
116    #[test]
117    fn human_bytes_fractional() {
118        // 1.5 GiB = 1024^3 + 512*1024^2
119        assert_eq!(
120            human_bytes(1024 * 1024 * 1024 + 512 * 1024 * 1024),
121            "1.50GiB"
122        );
123    }
124
125    #[test]
126    fn human_bytes_u64_max() {
127        // Should not panic; lands in PiB range
128        let s = human_bytes(u64::MAX);
129        assert!(s.ends_with("PiB"), "expected PiB suffix, got: {s}");
130    }
131
132    // --- parse_size_with_suffix ---
133
134    #[test]
135    fn parse_size_bare_number() {
136        assert_eq!(parse_size_with_suffix("0").unwrap(), 0);
137        assert_eq!(parse_size_with_suffix("42").unwrap(), 42);
138    }
139
140    #[test]
141    fn parse_size_all_suffixes() {
142        assert_eq!(parse_size_with_suffix("1K").unwrap(), 1024);
143        assert_eq!(parse_size_with_suffix("1M").unwrap(), 1024 * 1024);
144        assert_eq!(parse_size_with_suffix("1G").unwrap(), 1024 * 1024 * 1024);
145        assert_eq!(parse_size_with_suffix("1T").unwrap(), 1024u64.pow(4));
146        assert_eq!(parse_size_with_suffix("1P").unwrap(), 1024u64.pow(5));
147        assert_eq!(parse_size_with_suffix("1E").unwrap(), 1024u64.pow(6));
148    }
149
150    #[test]
151    fn parse_size_case_insensitive() {
152        assert_eq!(parse_size_with_suffix("4k").unwrap(), 4 * 1024);
153        assert_eq!(
154            parse_size_with_suffix("2g").unwrap(),
155            2 * 1024 * 1024 * 1024
156        );
157    }
158
159    #[test]
160    fn parse_size_overflow() {
161        assert!(parse_size_with_suffix("16385P").is_err());
162    }
163
164    #[test]
165    fn parse_size_bad_number() {
166        assert!(parse_size_with_suffix("abcM").is_err());
167        assert!(parse_size_with_suffix("").is_err());
168    }
169
170    #[test]
171    fn parse_size_unknown_suffix() {
172        assert!(parse_size_with_suffix("10X").is_err());
173    }
174
175    // --- parse_qgroupid ---
176
177    #[test]
178    fn parse_qgroupid_level0() {
179        assert_eq!(parse_qgroupid("0/5").unwrap(), 5);
180        assert_eq!(parse_qgroupid("0/256").unwrap(), 256);
181    }
182
183    #[test]
184    fn parse_qgroupid_higher_level() {
185        assert_eq!(parse_qgroupid("1/256").unwrap(), (1u64 << 48) | 256);
186        assert_eq!(parse_qgroupid("2/0").unwrap(), 2u64 << 48);
187    }
188
189    #[test]
190    fn parse_qgroupid_missing_slash() {
191        assert!(parse_qgroupid("5").is_err());
192    }
193
194    #[test]
195    fn parse_qgroupid_bad_level() {
196        assert!(parse_qgroupid("abc/5").is_err());
197    }
198
199    #[test]
200    fn parse_qgroupid_bad_subvolid() {
201        assert!(parse_qgroupid("0/abc").is_err());
202    }
203
204    // --- ParsedUuid ---
205
206    #[test]
207    fn parsed_uuid_clear() {
208        let u: ParsedUuid = "clear".parse().unwrap();
209        assert!(u.is_nil());
210    }
211
212    #[test]
213    fn parsed_uuid_random() {
214        let u: ParsedUuid = "random".parse().unwrap();
215        assert!(!u.is_nil());
216    }
217
218    #[test]
219    fn parsed_uuid_time() {
220        let u: ParsedUuid = "time".parse().unwrap();
221        assert!(!u.is_nil());
222    }
223
224    #[test]
225    fn parsed_uuid_explicit() {
226        let u: ParsedUuid = "550e8400-e29b-41d4-a716-446655440000".parse().unwrap();
227        assert_eq!(u.to_string(), "550e8400-e29b-41d4-a716-446655440000");
228    }
229
230    #[test]
231    fn parsed_uuid_no_hyphens() {
232        let u: ParsedUuid = "550e8400e29b41d4a716446655440000".parse().unwrap();
233        assert_eq!(u.to_string(), "550e8400-e29b-41d4-a716-446655440000");
234    }
235
236    #[test]
237    fn parsed_uuid_invalid() {
238        assert!("not-a-uuid".parse::<ParsedUuid>().is_err());
239        assert!("".parse::<ParsedUuid>().is_err());
240    }
241}