const BUCKET_DOMAIN_V1: &[u8] = b"rullst.feature-bucket.v1\0";
#[cfg_attr(mutants, mutants::skip)]
pub fn calculate_hash_bucket(flag: &str, identifier: &str) -> u32 {
let hash = bucket_hash(flag, identifier);
(hash % 100) as u32
}
fn bucket_hash(flag: &str, identifier: &str) -> u64 {
let mut context = ring::digest::Context::new(&ring::digest::SHA256);
context.update(BUCKET_DOMAIN_V1);
context.update(&(flag.len() as u64).to_le_bytes());
context.update(flag.as_bytes());
context.update(identifier.as_bytes());
let digest = context.finish();
let mut prefix = [0_u8; 8];
prefix.copy_from_slice(&digest.as_ref()[..8]);
u64::from_be_bytes(prefix)
}
pub fn parse_rollout(s: &str) -> Option<u32> {
let cleaned = s.trim().trim_end_matches('%');
cleaned.parse::<u32>().ok()
}
pub fn parse_variants(s: &str) -> Vec<(String, u32)> {
let mut parsed = Vec::new();
for part in s.split(',') {
let mut split = part.split(':');
if let (Some(name), Some(pct_str)) = (split.next(), split.next())
&& let Ok(pct) = pct_str.trim().parse::<u32>()
{
parsed.push((name.trim().to_string(), pct));
}
}
parsed
}
pub fn resolve_variant(variants: &[(String, u32)], bucket: u32) -> Option<String> {
let mut accumulator = 0;
for (name, pct) in variants {
accumulator += pct;
if bucket < accumulator {
return Some(name.clone());
}
}
None
}
#[cfg_attr(mutants, mutants::skip)]
pub(crate) fn parse_feature_string_value(
value: &str,
flag: &str,
identifier: Option<&str>,
) -> Option<String> {
let cleaned = value.trim();
if cleaned.is_empty() {
return None;
}
#[cfg_attr(mutants, mutants::skip)]
if cleaned == "true" || cleaned == "1" || cleaned == "yes" {
return Some("enabled".to_string());
}
if cleaned == "false" || cleaned == "0" || cleaned == "no" {
return Some("disabled".to_string());
}
if cleaned.ends_with('%')
&& let Some(pct) = parse_rollout(cleaned)
{
if let Some(ident) = identifier {
let bucket = calculate_hash_bucket(flag, ident);
return Some(if bucket < pct {
"enabled".to_string()
} else {
"disabled".to_string()
});
}
return Some("disabled".to_string());
}
if cleaned.contains(':') {
let variants = parse_variants(cleaned);
if !variants.is_empty()
&& let Some(ident) = identifier
{
let bucket = calculate_hash_bucket(flag, ident);
return resolve_variant(&variants, bucket);
}
}
Some(cleaned.to_string())
}