#[cfg(test)]
use std::{fs, path::Path};
use rustc_hash::FxHashMap;
use super::{
automaton::Automaton,
regex::{Regex, RegexInstructions},
};
use crate::parsing::serialization::Serialize;
#[cfg(test)]
const AUTOMATON_CACHE: &str = "src/parsing/automaton_cache";
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum StdLibParser {
Jwt,
}
#[cfg(test)]
impl StdLibParser {
pub(super) fn serialization_file(&self) -> String {
format!("{}/{:?}", AUTOMATON_CACHE, self)
}
}
type LibraryData = &'static [(StdLibParser, &'static dyn Fn() -> Regex, &'static [u8])];
type ParsingLibrary = FxHashMap<StdLibParser, Automaton>;
fn spec_library_data() -> LibraryData {
&[(
StdLibParser::Jwt,
&spec_jwt as &'static dyn Fn() -> Regex,
include_bytes!("automaton_cache/Jwt") as &'static [u8],
)]
}
pub fn spec_library() -> ParsingLibrary {
spec_library_data()
.iter()
.map(|(name, _, serialization)| {
assert!(
!serialization.is_empty(),
"Empty serialisation data for {:?}. The bootstrapping of the serialisation process has not been conducted. (see documentation of `midnight_circuits::parsing::specs`)",
*name
);
(*name, Automaton::deserialize_unwrap(serialization))
})
.collect::<FxHashMap<_, _>>()
}
fn spec_jwt() -> Regex {
let string = |marker: usize| -> Regex {
Regex::json_string().replace_markers(&|m| if m == 1 { Some(marker) } else { None })
};
let field = |name: &str, content: Regex| -> Regex {
Regex::spaced_cat([format!("\"{name}\"").into(), ":".into(), content])
};
let string_field = |name: &str, marker: usize| -> Regex { field(name, string(marker)) };
let int_field = |name: &str| -> Regex { field(name, Regex::digit().non_empty_list()) };
let collec = |opening: &str, items: Vec<Regex>, closing: &str| -> Regex {
Regex::spaced_separated_cat(items, ",".into())
.spaced_delimited(opening.into(), closing.into())
};
let string_list = string(0)
.spaced_separated_list(",".into())
.spaced_delimited("[".into(), "]".into());
let credential_schema = field(
"credentialSchema",
collec(
"{",
vec![string_field("id", 0), string_field("type", 0)],
"}",
)
.spaced_separated_list(",".into())
.spaced_delimited("[".into(), "]".into()),
);
let public_key_jwk = field(
"publicKeyJwk",
collec(
"{",
vec![
string_field("kty", 0),
string_field("crv", 0),
string_field("x", 5), string_field("y", 6), ],
"}",
),
);
let credential_subject = field(
"credentialSubject",
collec(
"{",
vec![
string_field("nationalId", 1), string_field("familyName", 2), string_field("givenName", 3), public_key_jwk,
string_field("id", 0),
string_field("birthDate", 4), ],
"}",
),
);
let issuer = field(
"issuer",
Regex::union([
string(0),
collec(
"{",
vec![string_field("id", 0), string_field("type", 0)],
"}",
),
collec("{", vec![string_field("id", 0)], "}"),
]),
);
let credential_status = field(
"credentialStatus",
collec(
"{",
vec![
string_field("statusPurpose", 0),
int_field("statusListIndex"),
string_field("id", 0),
string_field("type", 0),
string_field("statusListCredential", 0),
],
"}",
),
);
collec(
"{",
vec![
string_field("iss", 0),
string_field("sub", 0),
int_field("nbf"),
int_field("exp"),
field(
"vc",
collec(
"{",
vec![
credential_subject.clone().or(Regex::spaced_cat([
credential_schema,
",".into(),
credential_subject,
])),
field("type", string_list.clone()),
field("@context", string_list),
issuer,
credential_status,
],
"}",
),
),
],
"}",
)
}
#[cfg(test)]
fn check_serialization(checks: &ParsingLibrary) {
let mut recompile = false;
for (parser, automaton) in checks {
let file_name = parser.serialization_file();
assert!(
Path::new(&file_name).exists(),
"serialisation file {file_name} does not exist! Follow the documentation of `midnight_circuits::parsing::specs` for instructions on how to add a new parser to the standard library."
);
let previous_data = fs::read(file_name.clone()).unwrap();
let mut current_data = Vec::new();
automaton.serialize(&mut current_data);
if previous_data.is_empty() {
println!("-> bootstrapping the serialisation of {:?}. Recompilation will be necessary so that the executable contains the correct serialised data.", parser);
recompile = true;
fs::write(file_name, ¤t_data).unwrap();
} else {
assert!(
current_data == previous_data,
"The serialisation data of the parsing library (parser name: {:?}) is not up to date. If this is intentional, clear the content of {}, and run the test again to replace its content.",
parser, parser.serialization_file()
);
println!("-> serialisation data of {:?} is up to date.", parser)
}
println!(">> Serialisation checks completed.\n======");
assert!(
!recompile,
"The executable has to be re-compiled so that the serialisation data is up-to-date."
);
}
}
#[cfg(test)]
mod tests {
use std::time::Instant;
use rustc_hash::{FxBuildHasher, FxHashMap};
use super::{
spec_library_data,
StdLibParser::{self, Jwt},
};
use crate::parsing::{automaton::Automaton, spec_library, specs::check_serialization};
fn configure_serialisation() {
let lib_data = spec_library_data();
println!("======\nRecomputing the parsing library automata...");
let mut lib = FxHashMap::with_capacity_and_hasher(lib_data.len(), FxBuildHasher);
let start = Instant::now();
for (name, spec, _) in lib_data {
let start_local = Instant::now();
let automaton = spec().to_automaton();
println!(
"-> Generated {:?} automaton in {:?}",
*name,
start_local.elapsed()
);
lib.insert(*name, automaton);
}
println!(
">> Full parsing library re-computed in {:?}!\n======\n>> Now checking the consistency of serialised data.",
start.elapsed()
);
check_serialization(&lib)
}
fn specs_one_test(
spec_library: &FxHashMap<StdLibParser, Automaton>,
spec: StdLibParser,
accepted: &[(&str, &[(usize, &str)])],
rejected: &[&str],
) {
let automaton = spec_library.get(&spec).unwrap();
println!("\n\n** TEST of the spec {:?}", spec);
accepted.iter().enumerate().for_each(|(index,&(s,expected_outputs))| {
println!("\n -> accepting test nb. {index}");
let s_bytes = s.as_bytes();
let (v,output_automaton,interrupted) = automaton.run(s_bytes);
let counter = v.len() - 1;
assert!(!interrupted,
"input was unexpectedly rejected after being stuck after {} transitions, reading character '{}' (byte {}). Partial input read before the interruption:\n\n{}\n\n(i.e., bytes [{:?}])",
counter,
s_bytes[counter] as char,
s_bytes[counter],
&s[..counter],
&s_bytes[..counter],
);
let mut outputs = FxHashMap::with_capacity_and_hasher(2, FxBuildHasher);
for (&o,&i) in output_automaton.iter().zip(s_bytes) {
if o != 0 {
outputs.entry(o).or_insert(vec![]).push(i);
}
}
let state = v[counter];
assert!(
automaton.final_states.contains(&state),
"input was unexpectedly rejected (automaton run ended up in the non-final state {} after {} transitions)", state, counter
);
if let Some(n) = outputs.iter().find(|&(i,_)|
expected_outputs.iter().all(|(j,_)| i != j)
){
panic!(
"[test of spec {:?}, nb. {index}]: the input {s} is accepted as expected, but it has been marked with a {}, which is unexpected\nThe automaton reached the final state {} in {} transitions.",
spec, n.0, state, counter
)
}
for (i,expected_output) in expected_outputs {
let expected_output_bytes = expected_output.as_bytes();
match outputs.get(i) {
None => panic!(
"[test of spec {:?}, nb. {index}]: the input {s} is accepted as expected, but it has no marker {i}, which is unexpected\nThe automaton reached the final state {} in {} transitions.",
spec, state, counter
),
Some(output_bytes) => {
assert!(output_bytes == expected_output_bytes,
"[test of spec {:?}, nb. {index}]: the input {s} is accepted as expected, but the output marked {i} is\n \"{}\"\ninstead of\n \"{}\"\nwhich is unexpected. The automaton reached the final state {} in {} transitions.",
spec,
String::from_utf8_lossy(output_bytes),
String::from_utf8_lossy(expected_output_bytes),
state,
counter
);
}
}
}
println!("... which is accepted as expected with the correct outputs (the automaton reached the final state {} in {} transitions). The outputs are:", state, counter);
for (i,o) in expected_outputs {
println!(" - {i}: {o}")
}
});
rejected.iter().enumerate().for_each(|(index,s)| {
println!("\n -> rejecting test nb. {index}");
let s_bytes = s.as_bytes();
let (v,output,interrupted) = automaton.run(s_bytes);
let counter = v.len() - 1;
if interrupted {
println!(
"... which is rejected as expected. The automaton run was stuck after {} transitions, reading character '{}' (byte {}). Partial input read before the interruption:\n\n{}\n",
counter,
s_bytes[counter] as char,
s_bytes[counter],
&s[..counter],
)
} else {
let state = v[counter];
if automaton.final_states.contains(&state) {
let mut outputs = FxHashMap::with_capacity_and_hasher(2, FxBuildHasher);
for (&o,&i) in output.iter().zip(s_bytes) {
if o != 0 {
outputs.entry(o).or_insert(vec![]).push(i);
}
}
let mut outputs_str = String::new();
for (i,o) in outputs {
outputs_str.push_str(&format!(" - {i}: {}\n", String::from_utf8_lossy(&o)))
}
panic!(
"input was unexpectedly accepted (reached final state {} after {} transitions). The outputs were:\n{}",
state, counter, outputs_str
)
}
println!("... which is rejected as expected (the automaton run ended up in the non-final state {} after {} transitions).", state, counter)
}
});
}
#[test]
fn specs_test() {
configure_serialisation();
println!(">> Now configuring the spec library for tests... (using the serialised data)");
let start = Instant::now();
let spec_library = spec_library();
println!(">> Configuration completed in {:?}", start.elapsed());
for (name, automaton) in &spec_library {
println!(
" - {:?} automaton: {} states, {} transitions",
name,
automaton.nb_states,
automaton.transitions.len()
)
}
let accepted0: Vec<(&str, &[(usize, &str)])> = vec![
(
FULL_INPUT_JWT,
&[
(1, "12345"),
(2, "Wonderland"),
(3, "Alice"),
(4, "2000-11-13"),
(5, "S0kj3ydSeF86LU9BpHuVntMFN8SCKcHyci1tXFbRW8M"),
(6, "dux8h-QcIA3aZG9CSPIltDwVvOkf0kfJRJLH7K1KSlQ"),
],
),
(
MINIMAL_JWT,
&[
(1, "id"),
(2, "fn"),
(3, "gn"),
(4, "bd"),
(5, "x"),
(6, "y"),
],
),
];
let rejected0: Vec<&str> =
vec!["hello world", &FULL_INPUT_JWT[..1000], &MINIMAL_JWT[..600]];
specs_one_test(&spec_library, Jwt, &accepted0, &rejected0);
}
const FULL_INPUT_JWT: &str = r#"{
"iss":"did:prism:954e59ea4c212f4b4be8688bd3fe63dd7079d218ef6282205a70131f87f2887c",
"sub":"did:prism:73bb516fe88beec5b3b8d283eaec5964d1c13cd54ef8f1784217f4fe42688626:CtQBCtEBEkgKFG15LWF1dGgta2V5LW1pZG5pZ2h0EARKLgoJc2VjcDI1NmsxEiECS0kj3ydSeF86LU9BpHuVntMFN8SCKcHyci1tXFbRW8MSOwoHbWFzdGVyMBABSi4KCXNlY3AyNTZrMRIhAimWDggNDswAIJWKbexkfDxV0PEa58tcVcS1dk2phkDjGkgKDmFnZW50LWJhc2UtdXJsEhBMaW5rZWRSZXNvdXJjZVYxGiRodHRwOi8vMTkyLjE2OC4xLjg2OjgzMDAvY2xvdWQtYWdlbnQ",
"nbf":1740482175,
"exp":1740485775,
"vc":{
"credentialSchema":[
{
"id":"http:\/\/192.168.1.86:8400\/cloud-agent\/schema-registry\/schemas\/2fcfeeae-9532-3869-ad89-cdf5060c3a3c",
"type":"CredentialSchema2022"
}
],
"credentialSubject":{
"nationalId":"12345",
"familyName":"Wonderland",
"givenName":"Alice",
"publicKeyJwk":{
"kty":"EC",
"crv":"secp256k1",
"x":"S0kj3ydSeF86LU9BpHuVntMFN8SCKcHyci1tXFbRW8M",
"y":"dux8h-QcIA3aZG9CSPIltDwVvOkf0kfJRJLH7K1KSlQ"
},
"id":"did:prism:73bb516fe88beec5b3b8d283eaec5964d1c13cd54ef8f1784217f4fe42688626:CtQBCtEBEkgKFG15LWF1dGgta2V5LW1pZG5pZ2h0EARKLgoJc2VjcDI1NmsxEiECS0kj3ydSeF86LU9BpHuVntMFN8SCKcHyci1tXFbRW8MSOwoHbWFzdGVyMBABSi4KCXNlY3AyNTZrMRIhAimWDggNDswAIJWKbexkfDxV0PEa58tcVcS1dk2phkDjGkgKDmFnZW50LWJhc2UtdXJsEhBMaW5rZWRSZXNvdXJjZVYxGiRodHRwOi8vMTkyLjE2OC4xLjg2OjgzMDAvY2xvdWQtYWdlbnQ",
"birthDate":"2000-11-13"
},
"type":[
"VerifiableCredential"
],
"@context":[
"https:\/\/www.w3.org\/2018\/credentials\/v1"
],
"issuer":{
"id":"did:prism:954e59ea4c212f4b4be8688bd3fe63dd7079d218ef6282205a70131f87f2887c",
"type":"Profile"
},
"credentialStatus":{
"statusPurpose":"Revocation",
"statusListIndex":3,
"id":"http:\/\/192.168.1.86:8400\/cloud-agent\/credential-status\/2054e2ea-f191-4640-86dd-6dde6b2f77f7#3",
"type":"StatusList2021Entry",
"statusListCredential":"http:\/\/192.168.1.86:8400\/cloud-agent\/credential-status\/2054e2ea-f191-4640-86dd-6dde6b2f77f7"
}
}
}"#;
const MINIMAL_JWT: &str = r#"{
"iss" : "",
"sub" : "",
"nbf" : 0,
"exp" : 1,
"vc" : {
"credentialSubject" : {
"nationalId" : "id",
"familyName" : "fn",
"givenName" : "gn",
"publicKeyJwk" : {
"kty" : "",
"crv" : "",
"x" : "x",
"y" : "y"
},
"id" : "",
"birthDate" : "bd"
},
"type" : [],
"@context" : [],
"issuer" : "",
"credentialStatus" : {
"statusPurpose" : "",
"statusListIndex" : 3,
"id" : "",
"type" : "",
"statusListCredential" : ""
}
}
}"#;
}