use super::normalize::normalize;
use super::policy::CodePolicy;
use crate::error::{CodeInputError, RandomError};
use crate::rng::RandomSource;
use crate::secret::PlainCode;
pub fn generate_code<R: RandomSource>(
policy: &CodePolicy,
rng: &mut R,
) -> Result<PlainCode, RandomError> {
let alphabet = policy.alphabet();
let ceiling = alphabet.unbiased_ceiling();
let mut out = String::with_capacity(policy.length());
while out.len() < policy.length() {
let mut buf = [0u8; 1];
rng.fill_bytes(&mut buf)?;
let b = buf[0];
if (b as usize) < ceiling {
out.push(alphabet.symbol_for_byte(b) as char);
}
}
Ok(PlainCode::new(out))
}
pub fn validate_code_input(raw: &str, policy: &CodePolicy) -> Result<String, CodeInputError> {
if raw.is_empty() {
return Err(CodeInputError::Empty);
}
if raw.len() > policy.max_raw_len() {
return Err(CodeInputError::TooLongRaw);
}
let normalized = normalize(raw);
if normalized.is_empty() {
return Err(CodeInputError::Empty);
}
if normalized.chars().count() != policy.length() {
return Err(CodeInputError::WrongLength);
}
let alphabet = policy.alphabet();
if !normalized.bytes().all(|b| alphabet.contains(b)) {
return Err(CodeInputError::UnsupportedCharacters);
}
Ok(normalized)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::code::alphabet::Alphabet;
use crate::rng::{AlwaysFailRandom, FixedBytesRandom, SystemRandom};
use core::time::Duration;
fn human() -> CodePolicy {
CodePolicy::default_human(Duration::from_secs(3600)).unwrap()
}
#[test]
fn generated_code_matches_policy_length_and_alphabet() {
let policy = human();
let mut rng = SystemRandom::new();
let code = generate_code(&policy, &mut rng).unwrap();
assert_eq!(code.expose().chars().count(), policy.length());
let alpha = policy.alphabet();
assert!(code.expose().bytes().all(|b| alpha.contains(b)));
}
#[test]
fn rng_failure_fails_closed() {
let policy = human();
let mut rng = AlwaysFailRandom;
assert_eq!(generate_code(&policy, &mut rng), Err(RandomError));
}
#[test]
fn rejection_sampling_discards_bytes_at_or_above_ceiling() {
let policy = CodePolicy::legacy_ciao_6(Duration::from_secs(3600)).unwrap();
let alpha = Alphabet::unambiguous();
assert_eq!(alpha.unbiased_ceiling(), 248);
let mut rng = FixedBytesRandom::new(vec![248, 0]);
let code = generate_code(&policy, &mut rng).unwrap();
let first = alpha.symbols()[0] as char;
assert_eq!(code.expose(), &first.to_string().repeat(6));
}
#[test]
fn validate_accepts_normalizes_and_rejects() {
let policy = human(); assert_eq!(
validate_code_input("abcd-2345", &policy).unwrap(),
"ABCD2345"
);
assert_eq!(validate_code_input("", &policy), Err(CodeInputError::Empty));
assert_eq!(
validate_code_input("ABCD234", &policy),
Err(CodeInputError::WrongLength)
);
assert_eq!(
validate_code_input("ABCD2340", &policy),
Err(CodeInputError::UnsupportedCharacters)
);
let long = "A".repeat(policy.max_raw_len() + 1);
assert_eq!(
validate_code_input(&long, &policy),
Err(CodeInputError::TooLongRaw)
);
}
}