use std::collections::HashSet;
pub fn human_bytes(n: u64) -> String {
const U: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
let mut v = n as f64;
let mut i = 0;
while v >= 1024.0 && i < U.len() - 1 {
v /= 1024.0;
i += 1;
}
if i == 0 {
format!("{n} B")
} else {
format!("{v:.1} {}", U[i])
}
}
pub fn signed_bytes(n: i64) -> String {
let sign = if n >= 0 { "+" } else { "-" };
format!("{sign}{}", human_bytes(n.unsigned_abs()))
}
pub fn split_path(s: &str) -> Vec<String> {
let mut out = Vec::new();
let mut depth: i32 = 0;
let mut cur = String::new();
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
match c {
'<' | '(' | '[' => {
depth += 1;
cur.push(c);
}
'>' | ')' | ']' => {
depth -= 1;
cur.push(c);
}
':' if depth == 0 && chars.peek() == Some(&':') => {
chars.next();
out.push(std::mem::take(&mut cur));
}
_ => cur.push(c),
}
}
if !cur.is_empty() {
out.push(cur);
}
out.into_iter().filter(|s| !s.is_empty()).collect()
}
pub fn leading_ident(s: &str) -> &str {
let start = s
.char_indices()
.find(|(_, c)| c.is_alphanumeric() || *c == '_')
.map(|(i, _)| i)
.unwrap_or(s.len());
let rest = &s[start..];
let end = rest
.char_indices()
.find(|(_, c)| !(c.is_alphanumeric() || *c == '_'))
.map(|(i, _)| i)
.unwrap_or(rest.len());
&rest[..end]
}
pub fn strip_generics(s: &str) -> String {
let end = s.find(['<', '(']).unwrap_or(s.len());
let out = s[..end].trim_end_matches(':').trim();
if out.is_empty() { s.to_string() } else { out.to_string() }
}
pub fn is_primitive_or_keyword(s: &str) -> bool {
matches!(
s,
"str" | "char" | "bool" | "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8"
| "i16" | "i32" | "i64" | "i128" | "isize" | "f16" | "f32" | "f64" | "f128"
| "as" | "dyn" | "impl" | "fn" | "mut" | "ref" | "self" | "Self" | "crate"
| "super" | "move" | "unsafe" | "extern"
)
}
pub fn std_crates() -> HashSet<&'static str> {
[
"core",
"alloc",
"std",
"panic_abort",
"panic_unwind",
"compiler_builtins",
"unwind",
"proc_macro",
"test",
"std_detect",
"rustc_std_workspace_core",
"rustc_std_workspace_alloc",
"rustc_std_workspace_std",
"gimli",
"addr2line",
"object",
"miniz_oxide",
"hashbrown",
"rustc_demangle",
"backtrace",
]
.into_iter()
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn human_bytes_units() {
assert_eq!(human_bytes(0), "0 B");
assert_eq!(human_bytes(512), "512 B");
assert_eq!(human_bytes(1024), "1.0 KiB");
assert_eq!(human_bytes(1536), "1.5 KiB");
assert_eq!(human_bytes(1024 * 1024), "1.0 MiB");
assert_eq!(human_bytes(3 * 1024 * 1024 * 1024), "3.0 GiB");
}
#[test]
fn split_path_respects_generics() {
assert_eq!(split_path("a::b::c"), ["a", "b", "c"]);
assert_eq!(
split_path("serde_json::de::from_str::<alloc::string::String>"),
["serde_json", "de", "from_str", "<alloc::string::String>"]
);
assert_eq!(
split_path("<str as core::fmt::Display>::fmt"),
["<str as core::fmt::Display>", "fmt"]
);
assert!(split_path("").is_empty());
}
#[test]
fn leading_ident_skips_symbols() {
assert_eq!(leading_ident("regex_automata::dfa"), "regex_automata");
assert_eq!(leading_ident("<serde_json::Value as X>"), "serde_json");
assert_eq!(leading_ident("&mut alloc::vec::Vec"), "mut");
assert_eq!(leading_ident("*const u8"), "const");
}
#[test]
fn strip_generics_cuts_at_bracket() {
assert_eq!(strip_generics("foo::<u32>"), "foo");
assert_eq!(strip_generics("plain_name"), "plain_name");
assert_eq!(strip_generics("call(args)"), "call");
assert_eq!(strip_generics("<impl X>"), "<impl X>");
}
#[test]
fn primitives_and_std_recognized() {
assert!(is_primitive_or_keyword("u32"));
assert!(is_primitive_or_keyword("str"));
assert!(is_primitive_or_keyword("dyn"));
assert!(!is_primitive_or_keyword("serde"));
let std = std_crates();
assert!(std.contains("core"));
assert!(std.contains("std"));
assert!(!std.contains("serde"));
}
}