use clap::ValueEnum;
fn unparseable(kind: &str, accepted: &str, example: &str) -> String {
format!("not a valid {kind}. Expected {accepted}; example: {example}")
}
fn out_of_range(constraint: &str, example: &str) -> String {
format!("{constraint}; example: {example}")
}
const POSITIVE_INTEGER_ACCEPTED: &str = "a positive integer (>= 1)";
fn parse_positive_int<T>(
s: &str,
accepted: &str,
constraint: &str,
example: &str,
) -> Result<T, String>
where
T: std::str::FromStr + Default + PartialEq,
{
let val: T = s
.parse()
.map_err(|_| unparseable("integer", accepted, example))?;
if val == T::default() {
Err(out_of_range(constraint, example))
} else {
Ok(val)
}
}
fn parse_unit_interval(s: &str, key_label: &str, example: &str) -> Result<f64, String> {
let val: f64 = s
.parse()
.map_err(|_| unparseable("decimal", "a value in [0.0, 1.0]", example))?;
if (0.0..=1.0).contains(&val) {
Ok(val)
} else {
Err(out_of_range(
&format!("{key_label} must be between 0.0 and 1.0"),
example,
))
}
}
pub(crate) fn parse_min_confidence(s: &str) -> Result<f64, String> {
parse_unit_interval(s, "min_confidence", "0.85")
}
pub(crate) fn parse_ml_weight(s: &str) -> Result<f64, String> {
parse_unit_interval(s, "ml_weight", "0.5")
}
pub(crate) fn parse_entropy_bpe_max_bytes_per_token(s: &str) -> Result<f64, String> {
let value: f64 = s
.parse()
.map_err(|_| unparseable("decimal", "a finite value greater than 0.0", "2.2"))?;
if value.is_finite() && value > 0.0 {
Ok(value)
} else {
Err(out_of_range(
"entropy_bpe_max_bytes_per_token must be finite and greater than 0.0",
"2.2",
))
}
}
pub(crate) fn parse_entropy_threshold(s: &str) -> Result<f64, String> {
let value: f64 = s
.parse()
.map_err(|_| unparseable("decimal", "a value in [0.0, 8.0]", "4.5"))?;
if (0.0..=8.0).contains(&value) {
Ok(value)
} else {
Err(out_of_range(
"entropy_threshold must be a finite value between 0.0 and 8.0",
"4.5",
))
}
}
pub(crate) fn parse_verify_rate(s: &str) -> Result<f64, String> {
let val: f64 = s
.parse()
.map_err(|_| unparseable("number", "a positive rate in (0, 10000] rps", "50"))?;
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(crate) fn parse_ml_threshold(s: &str) -> Result<f64, String> {
let val: f64 = s
.parse()
.map_err(|_| unparseable("decimal", "a value in [0.0, 1.0]", "0.5"))?;
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(out_of_range(
"--ml-threshold must be between 0.0 and 1.0",
"0.5",
));
}
Ok(val)
}
pub(crate) fn parse_decode_depth(s: &str) -> Result<usize, String> {
let limit = keyhog_core::max_decode_depth_limit();
let val: usize = s
.parse()
.map_err(|_| unparseable("integer", &format!("an integer in [1, {limit}]"), "3"))?;
if (1..=limit).contains(&val) {
Ok(val)
} else {
Err(out_of_range(
&format!("decode depth must be between 1 and {limit}"),
"3",
))
}
}
pub(crate) fn parse_min_secret_len(s: &str) -> Result<usize, String> {
parse_positive_int(
s,
POSITIVE_INTEGER_ACCEPTED,
"--min-secret-len must be >= 1",
"16",
)
}
pub(crate) fn parse_positive_thread_count(s: &str) -> Result<usize, String> {
parse_positive_int(s, POSITIVE_INTEGER_ACCEPTED, "--threads must be >= 1", "4")
}
#[cfg(any(
feature = "git",
feature = "s3",
feature = "gcs",
feature = "azure",
feature = "github",
feature = "gitlab",
feature = "bitbucket"
))]
pub(crate) fn parse_positive_limit_count(s: &str) -> Result<usize, String> {
parse_positive_int(
s,
POSITIVE_INTEGER_ACCEPTED,
"limit count must be >= 1",
"100",
)
}
pub(crate) fn parse_positive_usize(s: &str) -> Result<usize, String> {
parse_positive_int(s, POSITIVE_INTEGER_ACCEPTED, "value must be >= 1", "1")
}
pub(crate) fn parse_daemon_request_timeout_secs(s: &str) -> Result<u64, String> {
parse_positive_int(
s,
"a positive number of seconds (>= 1)",
"--request-timeout-secs must be >= 1",
"30",
)
}
pub(crate) fn parse_positive_millis(s: &str) -> Result<u64, String> {
parse_positive_int(
s,
"a positive number of milliseconds (>= 1)",
"millisecond timeout must be >= 1",
"500",
)
}
pub(crate) 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)
}
}
fn parse_value_enum<T: ValueEnum>(value: &str) -> Option<T> {
T::from_str(value, true).ok()
}
pub(crate) fn value_enum_expected<T: ValueEnum>() -> String {
let mut expected = String::from("expected one of ");
for (index, variant) in T::value_variants().iter().enumerate() {
let Some(possible) = variant.to_possible_value() else {
panic!("ValueEnum variants used by config must have possible values");
};
if index != 0 {
expected.push_str(", ");
}
expected.push_str(possible.get_name());
}
expected
}
pub(crate) fn parse_severity_filter(s: &str) -> Option<crate::args::SeverityFilter> {
parse_value_enum(s)
}
pub(crate) fn parse_output_format(s: &str) -> Option<crate::args::OutputFormat> {
parse_value_enum(s)
}
pub(crate) fn parse_dedup_scope(s: &str) -> Option<crate::args::CliDedupScope> {
parse_value_enum(s)
}
#[cfg(test)]
#[path = "../tests/unit/value_parser_diagnostics.rs"]
mod tests;