codlet_core/code/
generate.rs1use super::normalize::normalize;
7use super::policy::CodePolicy;
8use crate::error::{CodeInputError, RandomError};
9use crate::rng::RandomSource;
10use crate::secret::PlainCode;
11
12pub fn generate_code<R: RandomSource>(
25 policy: &CodePolicy,
26 rng: &mut R,
27) -> Result<PlainCode, RandomError> {
28 let alphabet = policy.alphabet();
29 let ceiling = alphabet.unbiased_ceiling();
30 let mut out = String::with_capacity(policy.length());
31
32 while out.len() < policy.length() {
33 let mut buf = [0u8; 1];
34 rng.fill_bytes(&mut buf)?;
36 let b = buf[0];
37 if (b as usize) < ceiling {
38 out.push(alphabet.symbol_for_byte(b) as char);
39 }
40 }
42
43 Ok(PlainCode::new(out))
44}
45
46pub fn validate_code_input(raw: &str, policy: &CodePolicy) -> Result<String, CodeInputError> {
57 if raw.is_empty() {
58 return Err(CodeInputError::Empty);
59 }
60 if raw.len() > policy.max_raw_len() {
61 return Err(CodeInputError::TooLongRaw);
62 }
63 let normalized = normalize(raw);
64 if normalized.is_empty() {
65 return Err(CodeInputError::Empty);
66 }
67 if normalized.chars().count() != policy.length() {
70 return Err(CodeInputError::WrongLength);
71 }
72 let alphabet = policy.alphabet();
73 if !normalized.bytes().all(|b| alphabet.contains(b)) {
74 return Err(CodeInputError::UnsupportedCharacters);
75 }
76 Ok(normalized)
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::code::alphabet::Alphabet;
83 use crate::rng::{AlwaysFailRandom, FixedBytesRandom, SystemRandom};
84 use core::time::Duration;
85
86 fn human() -> CodePolicy {
87 CodePolicy::default_human(Duration::from_secs(3600)).unwrap()
88 }
89
90 #[test]
91 fn generated_code_matches_policy_length_and_alphabet() {
92 let policy = human();
93 let mut rng = SystemRandom::new();
94 let code = generate_code(&policy, &mut rng).unwrap();
95 assert_eq!(code.expose().chars().count(), policy.length());
96 let alpha = policy.alphabet();
97 assert!(code.expose().bytes().all(|b| alpha.contains(b)));
98 }
99
100 #[test]
101 fn rng_failure_fails_closed() {
102 let policy = human();
104 let mut rng = AlwaysFailRandom;
105 assert_eq!(generate_code(&policy, &mut rng), Err(RandomError));
106 }
107
108 #[test]
109 fn rejection_sampling_discards_bytes_at_or_above_ceiling() {
110 let policy = CodePolicy::legacy_ciao_6(Duration::from_secs(3600)).unwrap();
113 let alpha = Alphabet::unambiguous();
114 assert_eq!(alpha.unbiased_ceiling(), 248);
115 let mut rng = FixedBytesRandom::new(vec![248, 0]);
117 let code = generate_code(&policy, &mut rng).unwrap();
118 let first = alpha.symbols()[0] as char;
119 assert_eq!(code.expose(), &first.to_string().repeat(6));
120 }
121
122 #[test]
123 fn validate_accepts_normalizes_and_rejects() {
124 let policy = human(); assert_eq!(
127 validate_code_input("abcd-2345", &policy).unwrap(),
128 "ABCD2345"
129 );
130 assert_eq!(validate_code_input("", &policy), Err(CodeInputError::Empty));
131 assert_eq!(
132 validate_code_input("ABCD234", &policy),
133 Err(CodeInputError::WrongLength)
134 );
135 assert_eq!(
137 validate_code_input("ABCD2340", &policy),
138 Err(CodeInputError::UnsupportedCharacters)
139 );
140 let long = "A".repeat(policy.max_raw_len() + 1);
142 assert_eq!(
143 validate_code_input(&long, &policy),
144 Err(CodeInputError::TooLongRaw)
145 );
146 }
147}