1use anyhow::{Context, Result};
2use std::str::FromStr;
3use uuid::Uuid;
4
5pub 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
21pub 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#[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
58pub 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 #[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 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 let s = human_bytes(u64::MAX);
129 assert!(s.ends_with("PiB"), "expected PiB suffix, got: {s}");
130 }
131
132 #[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 #[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 #[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}