pub fn parse_min_confidence(s: &str) -> Result<f64, String> {
let val: f64 = s
.parse()
.map_err(|_| format!("'{}' is not a valid floating point number", s))?;
if (0.0..=1.0).contains(&val) {
Ok(val)
} else {
Err(format!(
"min_confidence must be between 0.0 and 1.0, got {}",
val
))
}
}
pub fn parse_verify_rate(s: &str) -> Result<f64, String> {
let val: f64 = s
.parse()
.map_err(|_| format!("'{s}' is not a valid floating point number"))?;
if !val.is_finite() {
return Err(format!("--verify-rate must be a finite number, got {val}"));
}
if val <= 0.0 {
return Err(format!(
"--verify-rate must be > 0 rps, got {val} \
(use --no-verify to disable verification entirely)"
));
}
if val > 10_000.0 {
return Err(format!(
"--verify-rate {val} exceeds the 10_000 rps sanity cap; \
no real provider permits that rate from a single IP"
));
}
Ok(val)
}
pub fn parse_ml_threshold(s: &str) -> Result<f64, String> {
let val: f64 = s
.parse()
.map_err(|_| format!("'{s}' is not a valid floating point number"))?;
if !val.is_finite() {
return Err(format!(
"--ml-threshold must be a finite number (no NaN/Inf), got {val}"
));
}
if !(0.0..=1.0).contains(&val) {
return Err(format!(
"--ml-threshold must be between 0.0 and 1.0, got {val}"
));
}
Ok(val)
}
pub fn parse_decode_depth(s: &str) -> Result<usize, String> {
let val: usize = s
.parse()
.map_err(|_| format!("'{}' is not a valid positive integer", s))?;
if (1..=10).contains(&val) {
Ok(val)
} else {
Err(format!(
"decode depth must be between 1 and 10, got {}",
val
))
}
}
pub fn parse_positive_thread_count(s: &str) -> Result<usize, String> {
let val: usize = s
.parse()
.map_err(|_| format!("'{s}' is not a valid positive integer"))?;
if val == 0 {
Err("--threads must be greater than zero".to_string())
} else {
Ok(val)
}
}
pub fn parse_byte_size(s: &str) -> Result<usize, String> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Ok(0);
}
let split_idx = trimmed
.find(|c: char| !c.is_ascii_digit() && c != '.')
.unwrap_or(trimmed.len());
let (num_part, suffix) = trimmed.split_at(split_idx);
let suffix_upper = suffix.trim().to_ascii_uppercase();
let multiplier: u64 = match suffix_upper.as_str() {
"" => {
return Err(format!(
"byte size '{trimmed}' is missing a unit. Use `B`, `K`/`KB`, `M`/`MB`, `G`/`GB`, or `T`/`TB`."
));
}
"B" => 1,
"K" | "KB" | "KIB" => 1024,
"M" | "MB" | "MIB" => 1024 * 1024,
"G" | "GB" | "GIB" => 1024 * 1024 * 1024,
"T" | "TB" | "TIB" => 1024_u64.pow(4),
other => {
return Err(format!(
"unknown size suffix '{other}'. Supported: B, K/KB, M/MB, G/GB, T/TB"
))
}
};
if let Ok(n_int) = num_part.parse::<u64>() {
let bytes = n_int.checked_mul(multiplier).ok_or_else(|| {
format!(
"byte size '{trimmed}' overflows u64 ({} * {} bytes)",
n_int, multiplier
)
})?;
let cap = usize::MAX / 2;
if bytes as u128 > cap as u128 {
return Err(format!(
"byte size '{trimmed}' exceeds the {cap}-byte sanity cap"
));
}
usize::try_from(bytes).map_err(|_| {
format!(
"byte size '{trimmed}' overflows usize (max {} bytes on this platform)",
usize::MAX
)
})
} else {
let n: f64 = num_part
.parse()
.map_err(|e| format!("bad number '{num_part}': {e}"))?;
if !n.is_finite() || n < 0.0 {
return Err(format!(
"byte size must be a finite, non-negative number, got: {num_part}"
));
}
let bytes_f = n * multiplier as f64;
let max_safe = 2.0_f64.powi(64);
if !bytes_f.is_finite() || bytes_f < 0.0 || bytes_f >= max_safe {
return Err(format!(
"byte size '{trimmed}' overflows usize (max {} bytes)",
usize::MAX
));
}
Ok(bytes_f as usize)
}
}
pub fn parse_detectors_verb(s: &str) -> Result<String, String> {
if s.eq_ignore_ascii_case("list") {
Ok("list".to_string())
} else {
Err(format!(
"unknown verb '{s}'. `keyhog detectors` lists detectors by default; \
the only accepted positional verb is `list`. Use \
`keyhog detectors --detectors <DIR>` (optionally `--search`, \
`--audit`, `--fix`, `--json`)."
))
}
}
pub fn parse_severity_filter(s: &str) -> Option<crate::args::SeverityFilter> {
use crate::args::SeverityFilter as S;
match s.to_lowercase().as_str() {
"info" => Some(S::Info),
"low" => Some(S::Low),
"medium" => Some(S::Medium),
"high" => Some(S::High),
"critical" => Some(S::Critical),
_ => None,
}
}
pub fn parse_output_format(s: &str) -> Option<crate::args::OutputFormat> {
use crate::args::OutputFormat as F;
match s.to_lowercase().as_str() {
"json" => Some(F::Json),
"jsonl" => Some(F::Jsonl),
"sarif" => Some(F::Sarif),
"text" => Some(F::Text),
_ => None,
}
}
pub fn parse_dedup_scope(s: &str) -> Option<crate::args::CliDedupScope> {
use crate::args::CliDedupScope as D;
match s.to_lowercase().as_str() {
"credential" => Some(D::Credential),
"file" => Some(D::File),
"none" => Some(D::None),
_ => None,
}
}