1use std::fmt;
7use std::time::Duration;
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum Platform {
15 Win,
16 Linux,
17 Darwin,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
22#[serde(rename_all = "lowercase")]
23pub enum Arch {
24 X86_64,
25 Arm64,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub struct Target {
31 pub platform: Platform,
32 pub arch: Arch,
33}
34
35impl Platform {
40 pub fn from_str_loose(s: &str) -> anyhow::Result<Self> {
45 match s.to_ascii_lowercase().as_str() {
46 "win" | "windows" => Ok(Self::Win),
47 "linux" => Ok(Self::Linux),
48 "darwin" | "macos" | "mac" => Ok(Self::Darwin),
49 other => anyhow::bail!("unknown platform: {other:?}"),
50 }
51 }
52}
53
54impl fmt::Display for Platform {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 match self {
57 Self::Win => write!(f, "win"),
58 Self::Linux => write!(f, "linux"),
59 Self::Darwin => write!(f, "darwin"),
60 }
61 }
62}
63
64impl Arch {
69 pub fn from_str_loose(s: &str) -> anyhow::Result<Self> {
74 match s.to_ascii_lowercase().as_str() {
75 "x86_64" | "x64" | "amd64" => Ok(Self::X86_64),
76 "arm64" | "aarch64" => Ok(Self::Arm64),
77 other => anyhow::bail!("unknown architecture: {other:?}"),
78 }
79 }
80}
81
82impl fmt::Display for Arch {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 match self {
85 Self::X86_64 => write!(f, "x86_64"),
86 Self::Arm64 => write!(f, "arm64"),
87 }
88 }
89}
90
91impl Target {
96 pub fn new(platform: Platform, arch: Arch) -> Self {
98 Self { platform, arch }
99 }
100
101 pub fn current() -> anyhow::Result<Self> {
103 let platform = if cfg!(target_os = "windows") {
104 Platform::Win
105 } else if cfg!(target_os = "linux") {
106 Platform::Linux
107 } else if cfg!(target_os = "macos") {
108 Platform::Darwin
109 } else {
110 anyhow::bail!("unsupported operating system");
111 };
112
113 let arch = match std::env::consts::ARCH {
114 "x86_64" => Arch::X86_64,
115 "aarch64" => Arch::Arm64,
116 other => anyhow::bail!("unsupported architecture: {other}"),
117 };
118
119 Ok(Self { platform, arch })
120 }
121}
122
123impl fmt::Display for Target {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 write!(f, "{}-{}", self.platform, self.arch)
126 }
127}
128
129pub fn format_size(bytes: u64) -> String {
135 const KB: f64 = 1024.0;
136 const MB: f64 = KB * 1024.0;
137 const GB: f64 = MB * 1024.0;
138 const TB: f64 = GB * 1024.0;
139
140 let b = bytes as f64;
141 if b < KB {
142 format!("{bytes} B")
143 } else if b < MB {
144 format!("{:.1} KB", b / KB)
145 } else if b < GB {
146 format!("{:.1} MB", b / MB)
147 } else if b < TB {
148 format!("{:.1} GB", b / GB)
149 } else {
150 format!("{:.1} TB", b / TB)
151 }
152}
153
154pub fn format_duration(duration: Duration) -> String {
158 let total_secs = duration.as_secs();
159 let millis = duration.subsec_millis();
160
161 if total_secs == 0 {
162 if millis == 0 {
163 return "0ms".to_string();
164 }
165 return format!("{millis}ms");
166 }
167
168 let minutes = total_secs / 60;
169 let secs = total_secs % 60;
170
171 if minutes > 0 {
172 if secs > 0 {
173 format!("{minutes}m {secs}s")
174 } else {
175 format!("{minutes}m")
176 }
177 } else if millis > 0 {
178 let frac = total_secs as f64 + millis as f64 / 1000.0;
180 format!("{frac:.1}s")
181 } else {
182 format!("{secs}s")
183 }
184}
185
186pub fn print_section(title: &str) {
188 let sep = "=".repeat(70);
189 println!("{sep}");
190 println!("{title}");
191 println!("{sep}");
192}
193
194#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
205 fn platform_display_roundtrip() {
206 for plat in [Platform::Win, Platform::Linux, Platform::Darwin] {
207 let s = plat.to_string();
208 let parsed = Platform::from_str_loose(&s).unwrap();
209 assert_eq!(plat, parsed, "round-trip failed for {s}");
210 }
211 }
212
213 #[test]
214 fn platform_from_str_loose_aliases() {
215 assert_eq!(Platform::from_str_loose("windows").unwrap(), Platform::Win);
216 assert_eq!(Platform::from_str_loose("WIN").unwrap(), Platform::Win);
217 assert_eq!(Platform::from_str_loose("macos").unwrap(), Platform::Darwin);
218 assert_eq!(Platform::from_str_loose("mac").unwrap(), Platform::Darwin);
219 assert_eq!(Platform::from_str_loose("LINUX").unwrap(), Platform::Linux);
220 }
221
222 #[test]
223 fn platform_from_str_loose_unknown() {
224 assert!(Platform::from_str_loose("freebsd").is_err());
225 }
226
227 #[test]
230 fn arch_display_roundtrip() {
231 for arch in [Arch::X86_64, Arch::Arm64] {
232 let s = arch.to_string();
233 let parsed = Arch::from_str_loose(&s).unwrap();
234 assert_eq!(arch, parsed, "round-trip failed for {s}");
235 }
236 }
237
238 #[test]
239 fn arch_from_str_loose_aliases() {
240 assert_eq!(Arch::from_str_loose("x64").unwrap(), Arch::X86_64);
241 assert_eq!(Arch::from_str_loose("amd64").unwrap(), Arch::X86_64);
242 assert_eq!(Arch::from_str_loose("AMD64").unwrap(), Arch::X86_64);
243 assert_eq!(Arch::from_str_loose("aarch64").unwrap(), Arch::Arm64);
244 assert_eq!(Arch::from_str_loose("ARM64").unwrap(), Arch::Arm64);
245 }
246
247 #[test]
248 fn arch_from_str_loose_unknown() {
249 assert!(Arch::from_str_loose("mips").is_err());
250 }
251
252 #[test]
255 fn target_current_succeeds() {
256 let t = Target::current().unwrap();
257 let _ = t.platform;
259 let _ = t.arch;
260 }
261
262 #[test]
263 fn target_display() {
264 let t = Target::new(Platform::Linux, Arch::X86_64);
265 assert_eq!(t.to_string(), "linux-x86_64");
266
267 let t2 = Target::new(Platform::Darwin, Arch::Arm64);
268 assert_eq!(t2.to_string(), "darwin-arm64");
269 }
270
271 #[test]
274 fn format_size_bytes() {
275 assert_eq!(format_size(0), "0 B");
276 assert_eq!(format_size(512), "512 B");
277 }
278
279 #[test]
280 fn format_size_kilobytes() {
281 assert_eq!(format_size(1024), "1.0 KB");
282 assert_eq!(format_size(1536), "1.5 KB");
283 }
284
285 #[test]
286 fn format_size_megabytes() {
287 assert_eq!(format_size(52_428_800), "50.0 MB");
288 let mb_52_3 = (52.3 * 1024.0 * 1024.0) as u64;
290 assert_eq!(format_size(mb_52_3), "52.3 MB");
291 }
292
293 #[test]
294 fn format_size_gigabytes() {
295 assert_eq!(format_size(1_073_741_824), "1.0 GB");
296 }
297
298 #[test]
301 fn format_duration_zero() {
302 assert_eq!(format_duration(Duration::ZERO), "0ms");
303 }
304
305 #[test]
306 fn format_duration_millis_only() {
307 assert_eq!(format_duration(Duration::from_millis(250)), "250ms");
308 }
309
310 #[test]
311 fn format_duration_fractional_seconds() {
312 assert_eq!(format_duration(Duration::from_millis(1500)), "1.5s");
313 }
314
315 #[test]
316 fn format_duration_whole_seconds() {
317 assert_eq!(format_duration(Duration::from_secs(5)), "5s");
318 }
319
320 #[test]
321 fn format_duration_minutes_and_seconds() {
322 assert_eq!(format_duration(Duration::from_secs(204)), "3m 24s");
323 }
324
325 #[test]
326 fn format_duration_exact_minutes() {
327 assert_eq!(format_duration(Duration::from_secs(120)), "2m");
328 }
329
330 #[test]
333 fn platform_serde_json_roundtrip() {
334 let p = Platform::Darwin;
335 let json = serde_json::to_string(&p).unwrap();
336 assert_eq!(json, "\"darwin\"");
337 let p2: Platform = serde_json::from_str(&json).unwrap();
338 assert_eq!(p, p2);
339 }
340
341 #[test]
342 fn arch_serde_json_roundtrip() {
343 let a = Arch::X86_64;
344 let json = serde_json::to_string(&a).unwrap();
345 assert_eq!(json, "\"x86_64\"");
346 let a2: Arch = serde_json::from_str(&json).unwrap();
347 assert_eq!(a, a2);
348 }
349}