use core::time::Duration;
use super::alphabet::Alphabet;
use crate::error::PolicyError;
pub const SECURE_MIN_HUMAN_LENGTH: usize = 8;
pub const DEFAULT_MAX_RAW_LEN: usize = 64;
pub const LEGACY_CIAO_LENGTH: usize = 6;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodePolicy {
alphabet: Alphabet,
length: usize,
max_raw_len: usize,
ttl: Duration,
}
impl CodePolicy {
pub fn default_human(ttl: Duration) -> Result<Self, PolicyError> {
Self::new(Alphabet::unambiguous(), SECURE_MIN_HUMAN_LENGTH, ttl)
}
pub fn new(alphabet: Alphabet, length: usize, ttl: Duration) -> Result<Self, PolicyError> {
if length == 0 {
return Err(PolicyError::ZeroLength);
}
if length < SECURE_MIN_HUMAN_LENGTH {
return Err(PolicyError::LengthBelowMinimum {
got: length,
min: SECURE_MIN_HUMAN_LENGTH,
});
}
Self::build(alphabet, length, ttl)
}
pub fn short_compat(
alphabet: Alphabet,
length: usize,
ttl: Duration,
) -> Result<Self, PolicyError> {
if length == 0 {
return Err(PolicyError::ZeroLength);
}
Self::build(alphabet, length, ttl)
}
pub fn legacy_ciao_6(ttl: Duration) -> Result<Self, PolicyError> {
Self::short_compat(Alphabet::unambiguous(), LEGACY_CIAO_LENGTH, ttl)
}
fn build(alphabet: Alphabet, length: usize, ttl: Duration) -> Result<Self, PolicyError> {
if ttl.is_zero() {
return Err(PolicyError::ZeroLength);
}
let max_raw_len = DEFAULT_MAX_RAW_LEN.max(length);
Ok(Self {
alphabet,
length,
max_raw_len,
ttl,
})
}
#[must_use]
pub fn alphabet(&self) -> &Alphabet {
&self.alphabet
}
#[must_use]
pub fn length(&self) -> usize {
self.length
}
#[must_use]
pub fn max_raw_len(&self) -> usize {
self.max_raw_len
}
#[must_use]
pub fn ttl(&self) -> Duration {
self.ttl
}
#[must_use]
pub fn approx_entropy_bits(&self) -> f64 {
(self.length as f64) * (self.alphabet.len() as f64).log2()
}
}
#[cfg(test)]
mod tests {
use super::*;
const HOUR: Duration = Duration::from_secs(3600);
#[test]
fn default_human_is_8_and_unambiguous() {
let p = CodePolicy::default_human(HOUR).unwrap();
assert_eq!(p.length(), 8);
assert_eq!(p.alphabet().len(), 31);
assert!((p.approx_entropy_bits() - 39.6).abs() < 0.2);
}
#[test]
fn new_rejects_below_minimum() {
let err = CodePolicy::new(Alphabet::unambiguous(), 6, HOUR).unwrap_err();
assert_eq!(err, PolicyError::LengthBelowMinimum { got: 6, min: 8 });
}
#[test]
fn short_compat_allows_six() {
let p = CodePolicy::legacy_ciao_6(HOUR).unwrap();
assert_eq!(p.length(), 6);
assert!((p.approx_entropy_bits() - 29.7).abs() < 0.2);
}
#[test]
fn zero_length_and_zero_ttl_rejected() {
assert_eq!(
CodePolicy::short_compat(Alphabet::unambiguous(), 0, HOUR),
Err(PolicyError::ZeroLength)
);
assert_eq!(
CodePolicy::default_human(Duration::ZERO),
Err(PolicyError::ZeroLength)
);
}
}