use serde_json::Value;
use crate::test_fixtures;
use crate::{Error, ErrorKind, FF1};
fn python_class(kind: ErrorKind) -> &'static str {
match kind {
ErrorKind::KeyLength => "KeyLengthError",
ErrorKind::Radix => "RadixError",
ErrorKind::Length => "LengthError",
ErrorKind::ValueRange => "ValueRangeError",
ErrorKind::TweakLength => "TweakLengthError",
ErrorKind::Alphabet => "AlphabetError",
ErrorKind::AlphabetRequired => "FF1Error",
}
}
const ALL_KINDS: [ErrorKind; 7] = [
ErrorKind::KeyLength,
ErrorKind::Radix,
ErrorKind::Length,
ErrorKind::ValueRange,
ErrorKind::TweakLength,
ErrorKind::Alphabet,
ErrorKind::AlphabetRequired,
];
fn cases() -> Option<Vec<Value>> {
let doc = test_fixtures::load("validation_cases.json")?;
Some(doc["cases"].as_array().expect("cases").clone())
}
fn hex(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("hex"))
.collect()
}
fn opt_usize(v: &Value, key: &str) -> Option<usize> {
v.get(key)
.map(|n| usize::try_from(n.as_u64().expect("unsigned")).expect("fits usize on 64-bit"))
}
fn outcome(case: &Value) -> Result<(), Error> {
let construct = &case["construct"];
let key_len = usize::try_from(construct["key_len"].as_u64().expect("key_len")).expect("small");
let key: Vec<u8> = (0..key_len)
.map(|i| u8::try_from(i).expect("small"))
.collect();
let tweak = construct
.get("tweak_hex")
.map(|t| hex(t.as_str().expect("hex")));
let radix = u32::try_from(construct["radix"].as_u64().expect("radix")).expect("u32");
let mut builder = FF1::builder(&key, radix);
if let Some(alphabet) = construct.get("alphabet") {
builder = builder.alphabet(alphabet.as_str().expect("str"));
}
if let Some(t) = &tweak {
builder = builder.tweak(t);
}
if let Some(n) = opt_usize(construct, "min_tweak_len") {
builder = builder.min_tweak_len(n);
}
if let Some(n) = opt_usize(construct, "max_tweak_len") {
builder = builder.max_tweak_len(n);
}
let ff1 = builder.build()?;
let Some(call) = case.get("call") else {
return Ok(());
};
let call_tweak = call.get("tweak_hex").map(|t| hex(t.as_str().expect("hex")));
let tweak = call_tweak.as_deref();
let numerals = || -> Vec<u16> {
call["x"]
.as_array()
.expect("x")
.iter()
.map(|n| u16::try_from(n.as_u64().expect("numeral")).expect("u16"))
.collect()
};
let text = || call["s"].as_str().expect("s");
match call["op"].as_str().expect("op") {
"encrypt_numerals" => ff1.encrypt_numerals(&numerals(), tweak).map(drop),
"decrypt_numerals" => ff1.decrypt_numerals(&numerals(), tweak).map(drop),
"encrypt" => ff1.encrypt(text(), tweak).map(drop),
"decrypt" => ff1.decrypt(text(), tweak).map(drop),
op => panic!("unknown op {op}"),
}
}
#[test]
fn every_shared_case_has_the_same_outcome() {
let mut ran = 0;
let mut python_only = 0;
let Some(cases) = cases() else {
return;
};
for case in cases {
let id = case["id"].as_str().expect("id");
if case.get("python_only").is_some() {
python_only += 1;
continue;
}
let expect = case["expect"].as_str().expect("expect");
match outcome(&case) {
Ok(()) => assert_eq!(expect, "accepted", "{id}: accepted, expected {expect}"),
Err(err) => {
assert_eq!(python_class(err.kind()), expect, "{id}: {err}");
assert_eq!(
err.to_string(),
case["message"].as_str().expect("message"),
"{id}"
);
}
}
ran += 1;
}
assert!(ran >= 50, "only {ran} shared cases ran");
assert_eq!(python_only, 1, "python_only cases changed; review each one");
}
#[test]
fn every_error_kind_has_a_shared_case() {
let Some(cases) = cases() else {
return;
};
let expects: Vec<String> = cases
.iter()
.filter(|c| c.get("python_only").is_none())
.map(|c| c["expect"].as_str().expect("expect").to_string())
.collect();
for kind in ALL_KINDS {
let class = python_class(kind);
assert!(
expects.iter().any(|e| e == class),
"no shared case for {kind:?} ({class})"
);
}
assert!(expects.iter().any(|e| e == "accepted"));
}