use serde::Deserialize;
use std::collections::BTreeMap;
#[derive(Debug, Deserialize)]
pub struct VectorFile {
pub schema_version: u32,
pub ash_version: String,
#[serde(default)]
pub categories: BTreeMap<String, Vec<Vector>>,
#[serde(default)]
pub vectors: Vec<Vector>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Vector {
pub id: String,
#[serde(default)]
pub category: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub input: serde_json::Value,
#[serde(default)]
pub expected: serde_json::Value,
}
#[derive(Debug, Clone)]
pub struct AdapterResult {
pub output: Option<String>,
pub ok: bool,
pub error_code: Option<String>,
pub error_status: Option<u16>,
}
impl AdapterResult {
pub fn ok(output: impl Into<String>) -> Self {
Self {
output: Some(output.into()),
ok: true,
error_code: None,
error_status: None,
}
}
pub fn ok_bool(val: bool) -> Self {
Self {
output: Some(val.to_string()),
ok: true,
error_code: None,
error_status: None,
}
}
pub fn error(code: impl Into<String>, status: u16) -> Self {
Self {
output: None,
ok: false,
error_code: Some(code.into()),
error_status: Some(status),
}
}
pub fn skip() -> Self {
Self {
output: None,
ok: true,
error_code: None,
error_status: None,
}
}
}
pub trait AshAdapter {
fn canonicalize_json(&self, input: &str) -> AdapterResult { let _ = input; AdapterResult::skip() }
fn canonicalize_query(&self, input: &str) -> AdapterResult { let _ = input; AdapterResult::skip() }
fn canonicalize_urlencoded(&self, input: &str) -> AdapterResult { let _ = input; AdapterResult::skip() }
fn normalize_binding(&self, method: &str, path: &str, query: &str) -> AdapterResult { let _ = (method, path, query); AdapterResult::skip() }
fn hash_body(&self, body: &str) -> AdapterResult { let _ = body; AdapterResult::skip() }
fn derive_client_secret(&self, nonce: &str, context_id: &str, binding: &str) -> AdapterResult { let _ = (nonce, context_id, binding); AdapterResult::skip() }
fn build_proof(&self, secret: &str, ts: &str, binding: &str, body_hash: &str) -> AdapterResult { let _ = (secret, ts, binding, body_hash); AdapterResult::skip() }
fn timing_safe_equal(&self, a: &str, b: &str) -> AdapterResult { let _ = (a, b); AdapterResult::skip() }
fn validate_timestamp(&self, ts: &str) -> AdapterResult { let _ = ts; AdapterResult::skip() }
fn trigger_error(&self, input: &serde_json::Value) -> AdapterResult { let _ = input; AdapterResult::skip() }
fn extract_scoped_fields(&self, payload: &str, fields: &[String], strict: bool) -> AdapterResult { let _ = (payload, fields, strict); AdapterResult::skip() }
fn build_unified_proof(&self, input: &serde_json::Value) -> AdapterResult { let _ = input; AdapterResult::skip() }
}
pub fn load_vectors(data: &[u8]) -> Result<Vec<Vector>, String> {
let file: serde_json::Value =
serde_json::from_slice(data).map_err(|e| format!("Failed to parse vectors JSON: {}", e))?;
let mut all_vectors = Vec::new();
if let Some(obj) = file.as_object() {
for (key, val) in obj {
if matches!(
key.as_str(),
"schema_version"
| "ash_version"
| "generated_from"
| "generated_at"
| "generator_version"
| "platform"
) {
continue;
}
if let Some(arr) = val.as_array() {
for item in arr {
if let Ok(mut vec) = serde_json::from_value::<Vector>(item.clone()) {
if vec.category.is_empty() {
vec.category = key.clone();
}
all_vectors.push(vec);
}
}
}
}
}
Ok(all_vectors)
}
pub fn load_vectors_from_file(path: &str) -> Result<Vec<Vector>, String> {
let data = std::fs::read(path).map_err(|e| format!("Failed to read {}: {}", path, e))?;
load_vectors(&data)
}
#[derive(Debug, Clone)]
pub struct VectorResult {
pub id: String,
pub category: String,
pub passed: bool,
pub skipped: bool,
pub expected: String,
pub actual: String,
pub diff: Option<String>,
}
#[derive(Debug)]
pub struct TestReport {
pub results: Vec<VectorResult>,
pub total: usize,
pub passed: usize,
pub failed: usize,
pub skipped: usize,
}
impl TestReport {
pub fn all_passed(&self) -> bool {
self.failed == 0
}
pub fn failures(&self) -> Vec<&VectorResult> {
self.results.iter().filter(|r| !r.passed && !r.skipped).collect()
}
pub fn summary(&self) -> String {
format!(
"{}/{} passed, {} failed, {} skipped",
self.passed, self.total, self.failed, self.skipped
)
}
}
pub fn run_vectors(vectors: &[Vector], adapter: &dyn AshAdapter) -> TestReport {
let mut results = Vec::with_capacity(vectors.len());
let mut passed = 0;
let mut failed = 0;
let mut skipped = 0;
for vec in vectors {
let result = run_single_vector(vec, adapter);
if result.skipped {
skipped += 1;
} else if result.passed {
passed += 1;
} else {
failed += 1;
}
results.push(result);
}
TestReport {
total: vectors.len(),
passed,
failed,
skipped,
results,
}
}
fn run_single_vector(vec: &Vector, adapter: &dyn AshAdapter) -> VectorResult {
let category = vec.category.as_str();
let (adapter_result, expected_str) = match category {
"json_canonicalization" => {
let input = vec.input.get("input_json_text")
.or_else(|| vec.input.get("input"))
.and_then(|v| v.as_str())
.unwrap_or("");
let expected = vec.expected.get("canonical_json")
.or_else(|| vec.expected.as_str().map(|_| &vec.expected))
.and_then(|v| v.as_str())
.unwrap_or("");
(adapter.canonicalize_json(input), expected.to_string())
}
"query_canonicalization" => {
let input = vec.input.get("raw_query")
.or_else(|| vec.input.get("input"))
.and_then(|v| v.as_str())
.unwrap_or("");
let expected = vec.expected.get("canonical_query")
.or_else(|| vec.expected.as_str().map(|_| &vec.expected))
.and_then(|v| v.as_str())
.unwrap_or("");
(adapter.canonicalize_query(input), expected.to_string())
}
"urlencoded_canonicalization" => {
let input = vec.input.get("input")
.and_then(|v| v.as_str())
.unwrap_or("");
let expected = vec.expected.get("canonical")
.or_else(|| vec.expected.as_str().map(|_| &vec.expected))
.and_then(|v| v.as_str())
.unwrap_or("");
(adapter.canonicalize_urlencoded(input), expected.to_string())
}
"binding_normalization" => {
let method = vec.input.get("method").and_then(|v| v.as_str()).unwrap_or("");
let path = vec.input.get("path").and_then(|v| v.as_str()).unwrap_or("");
let query = vec.input.get("query").and_then(|v| v.as_str()).unwrap_or("");
let expected = vec.expected.get("binding")
.or_else(|| vec.expected.as_str().map(|_| &vec.expected))
.and_then(|v| v.as_str())
.unwrap_or("");
(adapter.normalize_binding(method, path, query), expected.to_string())
}
"body_hashing" => {
let input = vec.input.get("body")
.or_else(|| vec.input.get("input"))
.and_then(|v| v.as_str())
.unwrap_or("");
let expected = vec.expected.get("hash")
.or_else(|| vec.expected.as_str().map(|_| &vec.expected))
.and_then(|v| v.as_str())
.unwrap_or("");
(adapter.hash_body(input), expected.to_string())
}
"client_secret_derivation" => {
let nonce = vec.input.get("nonce").and_then(|v| v.as_str()).unwrap_or("");
let ctx = vec.input.get("context_id").and_then(|v| v.as_str()).unwrap_or("");
let binding = vec.input.get("binding").and_then(|v| v.as_str()).unwrap_or("");
let expected = vec.expected.get("client_secret")
.or_else(|| vec.expected.as_str().map(|_| &vec.expected))
.and_then(|v| v.as_str())
.unwrap_or("");
(adapter.derive_client_secret(nonce, ctx, binding), expected.to_string())
}
"proof_generation" => {
let secret = vec.input.get("client_secret").and_then(|v| v.as_str()).unwrap_or("");
let ts = vec.input.get("timestamp").and_then(|v| v.as_str()).unwrap_or("");
let binding = vec.input.get("binding").and_then(|v| v.as_str()).unwrap_or("");
let body_hash = vec.input.get("body_hash").and_then(|v| v.as_str()).unwrap_or("");
let expected = vec.expected.get("proof")
.or_else(|| vec.expected.as_str().map(|_| &vec.expected))
.and_then(|v| v.as_str())
.unwrap_or("");
(adapter.build_proof(secret, ts, binding, body_hash), expected.to_string())
}
"timing_safe_comparison" => {
let a = vec.input.get("a").and_then(|v| v.as_str()).unwrap_or("");
let b = vec.input.get("b").and_then(|v| v.as_str()).unwrap_or("");
let expected = vec.expected.get("equal")
.and_then(|v| v.as_bool())
.map(|b| b.to_string())
.unwrap_or_default();
(adapter.timing_safe_equal(a, b), expected)
}
"error_behavior" => {
let expected_code = vec.expected.get("error_code")
.and_then(|v| v.as_str())
.unwrap_or("");
let expected_status = vec.expected.get("http_status")
.and_then(|v| v.as_u64())
.unwrap_or(0) as u16;
let result = adapter.trigger_error(&vec.input);
let expected_str = format!("{}:{}", expected_code, expected_status);
let actual_str = if result.ok {
"ok".to_string()
} else {
format!("{}:{}", result.error_code.as_deref().unwrap_or(""), result.error_status.unwrap_or(0))
};
return VectorResult {
id: vec.id.clone(),
category: vec.category.clone(),
passed: !result.ok
&& result.error_code.as_deref() == Some(expected_code)
&& result.error_status == Some(expected_status),
skipped: result.output.is_none() && result.ok && result.error_code.is_none(),
expected: expected_str,
actual: actual_str,
diff: None,
};
}
"timestamp_validation" => {
let ts = vec.input.get("timestamp").and_then(|v| v.as_str()).unwrap_or("");
let should_pass = vec.expected.get("valid").and_then(|v| v.as_bool()).unwrap_or(false);
let result = adapter.validate_timestamp(ts);
let actual_ok = result.ok;
return VectorResult {
id: vec.id.clone(),
category: vec.category.clone(),
passed: actual_ok == should_pass,
skipped: result.output.is_none() && result.ok && result.error_code.is_none(),
expected: format!("valid={}", should_pass),
actual: format!("valid={}", actual_ok),
diff: if actual_ok != should_pass {
Some(format!("Expected valid={}, got valid={}", should_pass, actual_ok))
} else {
None
},
};
}
_ => {
return VectorResult {
id: vec.id.clone(),
category: vec.category.clone(),
passed: false,
skipped: true,
expected: String::new(),
actual: String::new(),
diff: Some(format!("Unknown category: {}", category)),
};
}
};
if adapter_result.output.is_none() && adapter_result.ok && adapter_result.error_code.is_none() {
return VectorResult {
id: vec.id.clone(),
category: vec.category.clone(),
passed: false,
skipped: true,
expected: expected_str,
actual: String::new(),
diff: None,
};
}
let actual = adapter_result.output.unwrap_or_default();
let pass = actual == expected_str;
VectorResult {
id: vec.id.clone(),
category: vec.category.clone(),
passed: pass,
skipped: false,
expected: expected_str.clone(),
actual: actual.clone(),
diff: if pass {
None
} else {
Some(format!("expected: {}\n actual: {}", expected_str, actual))
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_adapter_result_ok() {
let r = AdapterResult::ok("hello");
assert!(r.ok);
assert_eq!(r.output, Some("hello".to_string()));
assert!(r.error_code.is_none());
}
#[test]
fn test_adapter_result_error() {
let r = AdapterResult::error("ASH_VALIDATION_ERROR", 485);
assert!(!r.ok);
assert!(r.output.is_none());
assert_eq!(r.error_code, Some("ASH_VALIDATION_ERROR".to_string()));
assert_eq!(r.error_status, Some(485));
}
#[test]
fn test_adapter_result_skip() {
let r = AdapterResult::skip();
assert!(r.ok);
assert!(r.output.is_none());
}
#[test]
fn test_adapter_result_ok_bool() {
let r = AdapterResult::ok_bool(true);
assert_eq!(r.output, Some("true".to_string()));
}
#[test]
fn test_report_all_passed() {
let report = TestReport {
results: vec![],
total: 5,
passed: 5,
failed: 0,
skipped: 0,
};
assert!(report.all_passed());
assert_eq!(report.summary(), "5/5 passed, 0 failed, 0 skipped");
}
#[test]
fn test_report_with_failures() {
let report = TestReport {
results: vec![VectorResult {
id: "test_001".to_string(),
category: "json".to_string(),
passed: false,
skipped: false,
expected: "a".to_string(),
actual: "b".to_string(),
diff: Some("expected: a\n actual: b".to_string()),
}],
total: 1,
passed: 0,
failed: 1,
skipped: 0,
};
assert!(!report.all_passed());
assert_eq!(report.failures().len(), 1);
}
struct EmptyAdapter;
impl AshAdapter for EmptyAdapter {}
#[test]
fn test_empty_adapter_skips_all() {
let vec = Vector {
id: "test".to_string(),
category: "json_canonicalization".to_string(),
description: "test".to_string(),
input: serde_json::json!({"input_json_text": "{}"}),
expected: serde_json::json!({"canonical_json": "{}"}),
};
let report = run_vectors(&[vec], &EmptyAdapter);
assert_eq!(report.skipped, 1);
}
struct RustCoreAdapter;
impl AshAdapter for RustCoreAdapter {
fn canonicalize_json(&self, input: &str) -> AdapterResult {
match crate::ash_canonicalize_json(input) {
Ok(s) => AdapterResult::ok(s),
Err(e) => AdapterResult::error(e.code().as_str(), e.http_status()),
}
}
fn canonicalize_query(&self, input: &str) -> AdapterResult {
match crate::ash_canonicalize_query(input) {
Ok(s) => AdapterResult::ok(s),
Err(e) => AdapterResult::error(e.code().as_str(), e.http_status()),
}
}
fn hash_body(&self, body: &str) -> AdapterResult {
AdapterResult::ok(crate::ash_hash_body(body))
}
fn derive_client_secret(&self, nonce: &str, ctx: &str, binding: &str) -> AdapterResult {
match crate::ash_derive_client_secret(nonce, ctx, binding) {
Ok(s) => AdapterResult::ok(s),
Err(e) => AdapterResult::error(e.code().as_str(), e.http_status()),
}
}
fn build_proof(&self, secret: &str, ts: &str, binding: &str, body_hash: &str) -> AdapterResult {
match crate::ash_build_proof(secret, ts, binding, body_hash) {
Ok(s) => AdapterResult::ok(s),
Err(e) => AdapterResult::error(e.code().as_str(), e.http_status()),
}
}
fn timing_safe_equal(&self, a: &str, b: &str) -> AdapterResult {
AdapterResult::ok_bool(crate::ash_timing_safe_equal(a.as_bytes(), b.as_bytes()))
}
fn normalize_binding(&self, method: &str, path: &str, query: &str) -> AdapterResult {
match crate::ash_normalize_binding(method, path, query) {
Ok(s) => AdapterResult::ok(s),
Err(e) => AdapterResult::error(e.code().as_str(), e.http_status()),
}
}
}
#[test]
fn test_rust_core_adapter_json() {
let vec = Vector {
id: "json_inline".to_string(),
category: "json_canonicalization".to_string(),
description: "sort keys".to_string(),
input: serde_json::json!({"input_json_text": r#"{"z":1,"a":2}"#}),
expected: serde_json::json!({"canonical_json": r#"{"a":2,"z":1}"#}),
};
let report = run_vectors(&[vec], &RustCoreAdapter);
assert!(report.all_passed(), "Failures: {:?}", report.failures());
}
#[test]
fn test_rust_core_adapter_body_hash() {
let hash = crate::ash_hash_body("test");
let vec = Vector {
id: "hash_inline".to_string(),
category: "body_hashing".to_string(),
description: "hash test".to_string(),
input: serde_json::json!({"body": "test"}),
expected: serde_json::json!({"hash": hash}),
};
let report = run_vectors(&[vec], &RustCoreAdapter);
assert!(report.all_passed());
}
#[test]
fn test_rust_core_adapter_timing_safe() {
let vectors = vec![
Vector {
id: "ts_eq".to_string(),
category: "timing_safe_comparison".to_string(),
description: "equal".to_string(),
input: serde_json::json!({"a": "hello", "b": "hello"}),
expected: serde_json::json!({"equal": true}),
},
Vector {
id: "ts_neq".to_string(),
category: "timing_safe_comparison".to_string(),
description: "not equal".to_string(),
input: serde_json::json!({"a": "hello", "b": "world"}),
expected: serde_json::json!({"equal": false}),
},
];
let report = run_vectors(&vectors, &RustCoreAdapter);
assert!(report.all_passed());
assert_eq!(report.passed, 2);
}
}