#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![warn(clippy::all)]
use ic_json::Json;
use std::collections::BTreeMap;
use std::path::PathBuf;
pub struct VectorFile {
pub algorithm: String,
pub source: String,
pub cases: Vec<BTreeMap<String, String>>,
pub path: PathBuf,
}
pub fn vectors_dir() -> PathBuf {
if let Ok(dir) = std::env::var("IC_TEST_VECTORS") {
return PathBuf::from(dir);
}
let mut here = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default());
for _ in 0..4 {
let candidate = here.join("testvectors");
if candidate.is_dir() {
return candidate;
}
if !here.pop() {
break;
}
}
PathBuf::from("testvectors")
}
impl VectorFile {
pub fn load(name: &str) -> Option<VectorFile> {
Self::load_from(&vectors_dir(), name)
}
pub fn load_from(dir: &std::path::Path, name: &str) -> Option<VectorFile> {
let path = dir.join(format!("{name}.json"));
let text = std::fs::read_to_string(&path).ok()?;
let parsed = ic_json::parse(&text)
.unwrap_or_else(|e| panic!("{} is present but not valid JSON: {e}", path.display()));
let algorithm = parsed
.get("algorithm")
.and_then(|v| v.as_str())
.filter(|s| !s.trim().is_empty())
.unwrap_or_else(|| panic!("{} does not say which algorithm it is for", path.display()))
.to_string();
let source = parsed
.get("source")
.and_then(|v| v.as_str())
.filter(|s| !s.trim().is_empty())
.unwrap_or_else(|| {
panic!(
"{} does not cite where its values came from. A vector whose \
provenance is unknown cannot establish that an implementation \
interoperates, which is the only reason to have one.",
path.display()
)
})
.to_string();
let raw = match parsed.get("cases") {
Some(Json::Array(items)) => items.clone(),
_ => panic!("{} has no \"cases\" array", path.display()),
};
let mut cases = Vec::new();
for (index, item) in raw.iter().enumerate() {
let Json::Object(fields) = item else {
panic!("{}: case {index} is not an object", path.display());
};
let mut case = BTreeMap::new();
for (key, value) in fields {
if let Some(text) = value.as_str() {
case.insert(key.clone(), text.to_string());
}
}
cases.push(case);
}
Some(VectorFile {
algorithm,
source,
cases,
path,
})
}
pub fn load_or_report(name: &str) -> Option<VectorFile> {
match Self::load(name) {
Some(file) => {
println!(
"vectors: {} cases for {} from {} ({})",
file.cases.len(),
file.algorithm,
file.source,
file.path.display()
);
Some(file)
}
None => {
println!(
"vectors: SKIPPED {name} -- no file at {}. \
See testvectors/README.md.",
vectors_dir().join(format!("{name}.json")).display()
);
None
}
}
}
}
pub fn hex_field(case: &BTreeMap<String, String>, name: &str) -> Vec<u8> {
let text = case
.get(name)
.unwrap_or_else(|| panic!("a case is missing the field {name:?}"));
unhex(text).unwrap_or_else(|| panic!("field {name:?} is not valid hex: {text:?}"))
}
pub fn optional_hex_field(case: &BTreeMap<String, String>, name: &str) -> Option<Vec<u8>> {
case.get(name).map(|text| {
unhex(text).unwrap_or_else(|| panic!("field {name:?} is not valid hex: {text:?}"))
})
}
fn unhex(text: &str) -> Option<Vec<u8>> {
let cleaned: Vec<u8> = text.bytes().filter(|b| !b.is_ascii_whitespace()).collect();
if cleaned.len() % 2 != 0 {
return None;
}
let mut out = Vec::with_capacity(cleaned.len() / 2);
for pair in cleaned.chunks(2) {
let hi = (pair[0] as char).to_digit(16)?;
let lo = (pair[1] as char).to_digit(16)?;
out.push((hi * 16 + lo) as u8);
}
Some(out)
}
pub fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hex_decoding_is_forgiving_about_layout_and_strict_about_content() {
assert_eq!(unhex("00ff").unwrap(), vec![0x00, 0xff]);
assert_eq!(unhex("00FF").unwrap(), vec![0x00, 0xff]);
assert_eq!(unhex("00 ff\n").unwrap(), vec![0x00, 0xff]);
assert_eq!(unhex("").unwrap(), Vec::<u8>::new());
assert!(unhex("0").is_none(), "odd length");
assert!(unhex("zz").is_none(), "not hex");
}
#[test]
fn hex_round_trips() {
let bytes = [0x00u8, 0x0f, 0xa5, 0xff];
assert_eq!(unhex(&hex(&bytes)).unwrap(), bytes);
}
#[test]
fn a_vector_file_without_provenance_is_refused() {
let unique = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let root = vectors_dir()
.parent()
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."));
let dir = root
.join("target")
.join("ic-vectors-tests")
.join(format!("{}-{unique}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir)
.unwrap_or_else(|e| panic!("could not create {}: {e}", dir.display()));
let write = |name: &str, body: &str| {
let path = dir.join(format!("{name}.json"));
std::fs::write(&path, body)
.unwrap_or_else(|e| panic!("could not write {}: {e}", path.display()));
};
write(
"cited",
r#"{"algorithm":"aes-kw","source":"RFC 3394 section 4.1",
"cases":[{"key":"00","pt":"11","ct":"22"}]}"#,
);
let file = VectorFile::load_from(&dir, "cited").expect("a cited file must load");
assert_eq!(file.source, "RFC 3394 section 4.1");
assert_eq!(file.algorithm, "aes-kw");
assert_eq!(file.cases.len(), 1);
assert!(VectorFile::load_from(&dir, "no-such-file").is_none());
for (name, body) in [
(
"uncited",
r#"{"algorithm":"aes-kw","cases":[{"key":"00"}]}"#,
),
(
"blank-source",
r#"{"algorithm":"aes-kw","source":" ","cases":[{"key":"00"}]}"#,
),
("unnamed", r#"{"source":"RFC 3394","cases":[{"key":"00"}]}"#),
] {
write(name, body);
let outcome = std::panic::catch_unwind(|| VectorFile::load_from(&dir, name));
assert!(
outcome.is_err(),
"{name} loaded despite not saying where its values came from"
);
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn an_absent_file_is_none() {
assert!(VectorFile::load("a-name-no-file-will-ever-have").is_none());
}
#[test]
fn the_bundled_key_wrap_vectors_load() {
let file =
VectorFile::load("aes-kw").expect("testvectors/aes-kw.json is in the repository");
assert_eq!(file.algorithm, "aes-kw");
assert!(
file.source.contains("3394"),
"the source should be recorded"
);
assert_eq!(file.cases.len(), 6, "RFC 3394 publishes six");
for case in &file.cases {
assert!(!hex_field(case, "key").is_empty());
assert!(!hex_field(case, "pt").is_empty());
assert!(!hex_field(case, "ct").is_empty());
}
}
}