use super::error::CfiError;
use super::table::{CATEGORIES, CategoryEntry, GroupEntry};
pub(super) fn validate(candidate: &[u8; 6]) -> Result<(), CfiError> {
validate_character_classes(candidate)?;
validate_taxonomy(candidate)?;
Ok(())
}
fn validate_character_classes(candidate: &[u8; 6]) -> Result<(), CfiError> {
for (i, &byte) in candidate.iter().enumerate() {
if !byte.is_ascii_uppercase() {
return Err(CfiError::InvalidCharacter {
character: byte as char,
position: (i + 1) as u8,
});
}
}
Ok(())
}
fn validate_taxonomy(candidate: &[u8; 6]) -> Result<(), CfiError> {
let category_code = candidate[0];
let category = find_category(category_code).ok_or(CfiError::UnknownCategory {
code: category_code as char,
})?;
let group_code = candidate[1];
let group = find_group(category, group_code).ok_or(CfiError::UnknownGroup {
category: category_code as char,
code: group_code as char,
})?;
for (i, &code) in candidate[2..].iter().enumerate() {
if !attr_allows(group.attrs[i], code) {
return Err(CfiError::InvalidAttribute {
category: category_code as char,
group: group_code as char,
index: (i + 1) as u8,
code: code as char,
});
}
}
Ok(())
}
pub(super) fn find_category(code: u8) -> Option<&'static CategoryEntry> {
let index = CATEGORIES.binary_search_by_key(&code, |c| c.code).ok()?;
Some(&CATEGORIES[index])
}
pub(super) fn find_group(
category: &'static CategoryEntry,
code: u8,
) -> Option<&'static GroupEntry> {
let index = category
.groups
.binary_search_by_key(&code, |g| g.code)
.ok()?;
Some(&category.groups[index])
}
#[inline]
fn attr_allows(mask: u32, code: u8) -> bool {
(mask >> (code - b'A')) & 1 == 1
}
#[cfg_attr(
not(any(feature = "arbitrary", feature = "proptest")),
allow(dead_code)
)]
pub(super) fn nth_letter(mask: u32, n: usize) -> u8 {
let count = mask.count_ones() as usize;
let target = n % count;
(0u8..26)
.filter(|bit| (mask >> bit) & 1 == 1)
.nth(target)
.map(|bit| b'A' + bit)
.expect("attribute masks in the generated table are always non-zero")
}
#[cfg(test)]
mod tests {
use super::*;
fn candidate(s: &str) -> [u8; 6] {
let bytes = s.as_bytes();
let mut out = [0u8; 6];
out.copy_from_slice(bytes);
out
}
#[test]
fn accepts_known_valid_cfis() {
for s in [
"ESVUFR", "ESVTOB", "DBFTFB", "MCATXB", "OCASNS", ] {
assert!(validate(&candidate(s)).is_ok(), "{s} should be valid");
}
}
#[test]
fn rejects_unknown_category() {
let err = validate(&candidate("QSVUFR")).unwrap_err();
assert_eq!(err, CfiError::UnknownCategory { code: 'Q' });
}
#[test]
fn rejects_unknown_group() {
let err = validate(&candidate("EZVUFR")).unwrap_err();
assert_eq!(
err,
CfiError::UnknownGroup {
category: 'E',
code: 'Z',
}
);
}
#[test]
fn rejects_invalid_attribute() {
let err = validate(&candidate("ESXUFR")).unwrap_err();
assert_eq!(
err,
CfiError::InvalidAttribute {
category: 'E',
group: 'S',
index: 1,
code: 'X',
}
);
}
#[test]
fn rejects_lowercase_as_character_class() {
let err = validate(&candidate("esvufr")).unwrap_err();
assert_eq!(
err,
CfiError::InvalidCharacter {
character: 'e',
position: 1,
}
);
}
#[test]
fn rejects_digit_as_character_class() {
let err = validate(&candidate("ESVUF1")).unwrap_err();
assert_eq!(
err,
CfiError::InvalidCharacter {
character: '1',
position: 6,
}
);
}
}