use oxirs_samm::parser::parse_aspect_from_string;
use proptest::prelude::*;
use scirs2_core::random::{rng, Random, Rng, RngExt};
fn urn_component() -> impl Strategy<Value = String> {
"[a-z0-9-]{1,20}"
}
fn samm_namespace() -> impl Strategy<Value = String> {
(urn_component(), urn_component(), urn_component())
.prop_map(|(org, name, version)| format!("urn:samm:{}:{}#{}", org, version, name))
}
fn random_string() -> impl Strategy<Value = String> {
prop_oneof![
"[a-zA-Z0-9_-]{1,100}",
Just(String::new()),
prop::collection::vec(any::<char>(), 1000..10000)
.prop_map(|chars| chars.into_iter().collect()),
"[\\x00-\\x1F\\x7F-\\xFF]{1,50}",
"\\PC{1,50}",
"[\"'\\\\]{1,50}",
]
}
fn random_ttl() -> impl Strategy<Value = String> {
prop_oneof![
Just(String::new()),
Just(" \n\t\r ".to_string()),
Just("@prefix : <invalid> .".to_string()),
Just(":Aspect a samm:Aspect .".to_string()),
Just("@prefix samm: . :A a samm:Aspect".to_string()),
(random_string(), random_string()).prop_map(|(prefix, suffix)| {
format!("@prefix x: <urn:{}> . x:{} a x:Thing .", prefix, suffix)
}),
Just(
r#"
@prefix samm: <urn:samm:org.eclipse.esmf.samm:meta-model:2.3.0#> .
:A a samm:Aspect ; samm:properties ( ( ( ( :p ) ) ) ) .
"#
.to_string()
),
Just(
r#"
@prefix samm: <urn:samm:org.eclipse.esmf.samm:meta-model:2.3.0#> .
:A samm:properties :A .
"#
.to_string()
),
Just("@prefix samm: <urn:test#> . :A a [ a [ a samm:Aspect .".to_string()),
Just("@prefix 日本語: <urn:test#> . 日本語:テスト a samm:Aspect .".to_string()),
random_string().prop_map(|s| format!("@prefix x: <\x00\x01{}> .", s)),
"[a-z]{10000,20000}".prop_map(|s| format!("@prefix x: <urn:{}> .", s)),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn fuzz_parser_no_panic(input in random_ttl(), namespace in samm_namespace()) {
let runtime = tokio::runtime::Runtime::new().unwrap();
let _ = runtime.block_on(async {
parse_aspect_from_string(&input, &namespace).await
});
}
#[test]
fn fuzz_empty_input(whitespace in "[ \t\n\r]{0,100}") {
let runtime = tokio::runtime::Runtime::new().unwrap();
let result = runtime.block_on(async {
parse_aspect_from_string(&whitespace, "urn:samm:org.example:1.0.0#Test").await
});
assert!(result.is_err());
}
#[test]
fn fuzz_long_urns(
namespace in "[a-z]{100,1000}",
version in "[0-9]{50,100}",
element in "[a-z]{100,500}"
) {
let urn = format!(
"urn:samm:{}:{}#{}",
namespace,
version,
element
);
let input = format!(r#"
@prefix samm: <urn:samm:org.eclipse.esmf.samm:meta-model:2.3.0#> .
@prefix : <{}> .
:Aspect a samm:Aspect .
"#, urn);
let runtime = tokio::runtime::Runtime::new().unwrap();
let _ = runtime.block_on(async {
parse_aspect_from_string(&input, &urn).await
});
}
#[test]
fn fuzz_special_characters(special in "[\\x00-\\x1F\\x7F-\\xFF]{1,20}") {
let input = format!(r#"
@prefix samm: <urn:samm:org.eclipse.esmf.samm:meta-model:2.3.0#> .
@prefix : <urn:samm:org.example:1.0.0#> .
:Aspect a samm:Aspect ;
samm:preferredName "{}"@en .
"#, special.escape_default());
let runtime = tokio::runtime::Runtime::new().unwrap();
let _ = runtime.block_on(async {
parse_aspect_from_string(&input, "urn:samm:org.example:1.0.0#Aspect").await
});
}
#[test]
fn fuzz_random_valid_structure(
num_properties in 0usize..20,
namespace in urn_component()
) {
let urn = format!("urn:samm:org.example:1.0.0#{}", namespace);
let mut input = format!(r#"
@prefix samm: <urn:samm:org.eclipse.esmf.samm:meta-model:2.3.0#> .
@prefix samm-c: <urn:samm:org.eclipse.esmf.samm:characteristic:2.3.0#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix : <urn:samm:org.example:1.0.0#> .
:{} a samm:Aspect ;
samm:preferredName "Test"@en ;
samm:properties ( "#, namespace);
for i in 0..num_properties {
input.push_str(&format!(":prop{} ", i));
}
input.push_str(") .\n\n");
for i in 0..num_properties {
input.push_str(&format!(r#"
:prop{} a samm:Property ;
samm:characteristic [ a samm:Characteristic ; samm:dataType xsd:string ] .
"#, i));
}
let runtime = tokio::runtime::Runtime::new().unwrap();
let result = runtime.block_on(async {
parse_aspect_from_string(&input, &urn).await
});
if let Ok(aspect) = result {
assert_eq!(aspect.properties.len(), num_properties);
}
}
}
#[tokio::test]
async fn test_null_bytes() {
let input = "Aspect\x00a samm:Aspect";
let result = parse_aspect_from_string(input, "urn:test#").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_extremely_nested_structures() {
let mut input = String::from(
r#"
@prefix samm: <urn:samm:org.eclipse.esmf.samm:meta-model:2.3.0#> .
@prefix : <urn:test#> .
:A a samm:Aspect ; samm:properties ( "#,
);
for _ in 0..100 {
input.push_str("( ");
}
input.push_str(":p ");
for _ in 0..100 {
input.push_str(") ");
}
input.push_str(") .");
let result = parse_aspect_from_string(&input, "urn:test#A").await;
assert!(result.is_err() || result.is_ok());
}
#[tokio::test]
async fn test_circular_references() {
let input = r#"
@prefix samm: <urn:samm:org.eclipse.esmf.samm:meta-model:2.3.0#> .
@prefix : <urn:test#> .
:A a samm:Aspect ;
samm:properties ( :B ) .
:B a samm:Property ;
samm:characteristic :C .
:C a samm:Characteristic ;
samm:dataType :A .
"#;
let result = parse_aspect_from_string(input, "urn:test#A").await;
assert!(result.is_err() || result.is_ok());
}
#[tokio::test]
async fn test_malformed_uris() {
let inputs = vec![
"@prefix x: <> .",
"@prefix x: < > .",
"@prefix x: <urn:> .",
"@prefix x: <urn:samm> .",
"@prefix x: <http://> .",
"@prefix x: <urn:samm:org.example> .", "@prefix x: <\\x00\\x01> .",
];
for input in inputs {
let result = parse_aspect_from_string(input, "urn:test#A").await;
assert!(result.is_err() || result.is_ok());
}
}
#[tokio::test]
async fn test_unicode_edge_cases() {
let inputs = vec![
"@prefix \u{202E}x: <urn:test#> .",
"@prefix\u{200B}x: <urn:test#> .",
"@prefix x\u{0301}: <urn:test#> .",
"@prefix 😀: <urn:test#> .",
"@prefix 𝕩: <urn:test#> .",
];
for input in inputs {
let result = parse_aspect_from_string(input, "urn:test#A").await;
assert!(result.is_err() || result.is_ok());
}
}
#[tokio::test]
async fn test_memory_exhaustion_protection() {
let long_string = "a".repeat(10_000_000); let input = format!(
r#"
@prefix samm: <urn:samm:org.eclipse.esmf.samm:meta-model:2.3.0#> .
@prefix : <urn:test#> .
:A a samm:Aspect ; samm:description "{}"@en .
"#,
long_string
);
let start = std::time::Instant::now();
let result = parse_aspect_from_string(&input, "urn:test#A").await;
let elapsed = start.elapsed();
assert!(elapsed.as_secs() < 5, "Parser took too long: {:?}", elapsed);
assert!(result.is_err() || result.is_ok());
}
#[tokio::test]
async fn test_concurrent_fuzz() {
use scirs2_core::random::rng;
let mut main_rng = rng();
let handles: Vec<_> = (0..20)
.map(|i| {
let seed = main_rng.random();
tokio::spawn(async move {
let mut local_rng = Random::seed(seed);
for _ in 0..10 {
let num_props = (local_rng.random::<u64>() % 10) as usize;
let input = generate_random_ttl(num_props, &mut local_rng);
let _ = parse_aspect_from_string(
&input,
&format!("urn:samm:test:1.0.0#Aspect{}", i),
)
.await;
}
})
})
.collect();
for handle in handles {
handle.await.unwrap();
}
}
fn generate_random_ttl<R: Rng>(num_properties: usize, rng: &mut Random<R>) -> String {
let mut ttl = String::from(
r#"
@prefix samm: <urn:samm:org.eclipse.esmf.samm:meta-model:2.3.0#> .
@prefix samm-c: <urn:samm:org.eclipse.esmf.samm:characteristic:2.3.0#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix : <urn:samm:test:1.0.0#> .
:TestAspect a samm:Aspect ;
samm:preferredName "Test"@en ;
samm:properties ( "#,
);
for i in 0..num_properties {
ttl.push_str(&format!(":prop{} ", i));
}
ttl.push_str(") .\n\n");
for i in 0..num_properties {
let random_choice = (rng.random::<u64>() % 5) as usize;
let data_type = match random_choice {
0 => "xsd:string",
1 => "xsd:int",
2 => "xsd:float",
3 => "xsd:boolean",
_ => "xsd:dateTime",
};
ttl.push_str(&format!(
r#"
:prop{} a samm:Property ;
samm:preferredName "Property {}"@en ;
samm:characteristic [ a samm:Characteristic ; samm:dataType {} ] .
"#,
i, i, data_type
));
}
ttl
}
#[tokio::test]
async fn test_syntax_error_recovery() {
let test_cases = vec![
(
"Missing closing bracket",
r#"@prefix samm: <urn:samm:org.eclipse.esmf.samm:meta-model:2.3.0#> .
:A a samm:Aspect ; samm:properties ( :p ."#,
),
(
"Missing semicolon",
r#"@prefix samm: <urn:samm:org.eclipse.esmf.samm:meta-model:2.3.0#> .
:A a samm:Aspect samm:properties () ."#,
),
(
"Invalid prefix",
r#"@prefix samm <urn:test#> .
:A a samm:Aspect ."#,
),
(
"Typo in keyword",
r#"@prefi samm: <urn:samm:org.eclipse.esmf.samm:meta-model:2.3.0#> .
:A a samm:Aspect ."#,
),
];
for (name, input) in test_cases {
let result = parse_aspect_from_string(input, "urn:test#A").await;
if let Err(e) = result {
let error_msg = format!("{}", e);
assert!(!error_msg.is_empty(), "Empty error for: {}", name);
}
}
}