use std::str::FromStr;
#[allow(dead_code)]
pub fn parse_number<T: FromStr>(s: &str) -> Option<T> {
let cleaned = s.trim().replace([',', '_'], "");
cleaned.parse::<T>().ok()
}
#[allow(dead_code)]
pub fn to_bytes(value: f64, unit: &str) -> Option<u64> {
let mul = match unit.trim().to_ascii_uppercase().as_str() {
"B" => 1.0,
"KB" => 1_000.0,
"KIB" => 1024.0,
"MB" => 1_000_000.0,
"MIB" => 1024.0_f64.powi(2),
"GB" => 1_000_000_000.0,
"GIB" => 1024.0_f64.powi(3),
"TB" => 1_000_000_000_000.0,
"TIB" => 1024.0_f64.powi(4),
_ => return None,
};
let bytes = value * mul;
if bytes.is_finite() && bytes >= 0.0 {
Some(bytes as u64)
} else {
None
}
}
pub fn strip_control_chars(s: &str) -> String {
s.chars().filter(|c| !c.is_control()).collect()
}
pub fn sanitize_label_value(s: &str) -> String {
const MAX_LABEL_VALUE_LENGTH: usize = 1024;
let trimmed = s.trim();
let cleaned = trimmed.trim_matches('"');
let stripped = strip_control_chars(cleaned);
if stripped.len() > MAX_LABEL_VALUE_LENGTH {
let mut end = MAX_LABEL_VALUE_LENGTH;
while !stripped.is_char_boundary(end) {
end -= 1;
}
stripped[..end].to_string()
} else {
stripped
}
}
pub fn sanitize_label_name(s: &str) -> String {
let mut sanitized = String::with_capacity(s.len().max(1));
for (index, character) in s.chars().enumerate() {
if index == 0 && character.is_ascii_digit() {
sanitized.push('_');
}
if character.is_ascii_alphanumeric() {
sanitized.push(character.to_ascii_lowercase());
} else {
sanitized.push('_');
}
}
if sanitized.is_empty() {
sanitized.push('_');
}
sanitized
}
#[allow(dead_code)]
pub fn after_colon_trimmed(line: &str) -> Option<&str> {
line.split_once(':').map(|x| x.1).map(|s| s.trim())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_number_util() {
assert_eq!(parse_number::<u32>("1_234"), Some(1234));
assert_eq!(parse_number::<u64>("1,234,567"), Some(1_234_567));
assert_eq!(parse_number::<f64>(" 3.1234 "), Some(3.1234));
assert_eq!(parse_number::<i32>("abc"), None);
}
#[test]
fn test_to_bytes() {
assert_eq!(to_bytes(1.0, "B"), Some(1));
assert_eq!(to_bytes(1.0, "KB"), Some(1_000));
assert_eq!(to_bytes(1.0, "KiB"), Some(1024));
assert_eq!(to_bytes(1.5, "MiB"), Some((1.5 * 1024.0 * 1024.0) as u64));
assert_eq!(to_bytes(2.0, "GB"), Some(2_000_000_000));
assert_eq!(to_bytes(1.0, "unknown"), None);
}
#[test]
fn test_sanitize_label_value() {
assert_eq!(sanitize_label_value(r#" "hello" "#), "hello".to_string());
assert_eq!(sanitize_label_value("world"), "world".to_string());
}
#[test]
fn test_strip_control_chars() {
assert_eq!(strip_control_chars("hello"), "hello");
assert_eq!(strip_control_chars(""), "");
assert_eq!(strip_control_chars("\x1b[2J"), "[2J");
assert_eq!(strip_control_chars("abc\x1b[2Jdef"), "abc[2Jdef");
assert_eq!(strip_control_chars("\x00\x07\x0b"), "");
assert_eq!(strip_control_chars("a\nb\rc"), "abc");
}
#[test]
fn test_sanitize_label_value_strips_ansi_escape() {
assert_eq!(sanitize_label_value("\x1b[2Jmalicious"), "[2Jmalicious");
assert_eq!(sanitize_label_value("\"\x1b[2JEvil GPU\""), "[2JEvil GPU");
}
#[test]
fn test_sanitize_label_value_utf8_boundary_truncation() {
const MAX_LABEL_VALUE_LENGTH: usize = 1024;
let mut s = String::with_capacity(MAX_LABEL_VALUE_LENGTH + 8);
s.push_str(&"a".repeat(MAX_LABEL_VALUE_LENGTH - 1));
s.push('가'); let out = sanitize_label_value(&s);
assert!(out.len() <= MAX_LABEL_VALUE_LENGTH);
assert!(!out.contains('가'));
assert_eq!(out.len(), MAX_LABEL_VALUE_LENGTH - 1);
}
#[test]
fn test_sanitize_label_name() {
assert_eq!(sanitize_label_name("Driver Version"), "driver_version");
assert_eq!(sanitize_label_name("GPU Type"), "gpu_type");
assert_eq!(sanitize_label_name("Thermal Pressure"), "thermal_pressure");
assert_eq!(sanitize_label_name("Architecture"), "architecture");
assert_eq!(sanitize_label_name("pcie-gen-current"), "pcie_gen_current");
assert_eq!(sanitize_label_name("UPPER_CASE"), "upper_case");
assert_eq!(sanitize_label_name("already_valid"), "already_valid");
assert_eq!(sanitize_label_name("Source: Fan"), "source__fan");
assert_eq!(sanitize_label_name("3D Engine"), "_3d_engine");
assert_eq!(sanitize_label_name("GPU.Temp/Limit"), "gpu_temp_limit");
assert_eq!(sanitize_label_name("온도"), "__");
assert_eq!(sanitize_label_name(""), "_");
}
#[test]
fn sanitized_label_names_always_match_prometheus_grammar() {
for input in [
"Source: Fan",
"3D Engine",
"GPU.Temp/Limit",
"punctuation!@#$%^&*()",
"온도",
"",
] {
let sanitized = sanitize_label_name(input);
let mut characters = sanitized.chars();
let first = characters.next().expect("sanitized name is never empty");
assert!(
first.is_ascii_alphabetic() || first == '_',
"invalid first character in {sanitized:?} from {input:?}"
);
assert!(
characters.all(|character| character.is_ascii_alphanumeric() || character == '_'),
"invalid character in {sanitized:?} from {input:?}"
);
}
}
#[test]
fn test_after_colon_trimmed() {
assert_eq!(after_colon_trimmed("Key: Value"), Some("Value"));
assert_eq!(after_colon_trimmed("NoColon"), None);
}
}