#![allow(clippy::unwrap_used, clippy::expect_used)]
use iri_rs::IriBuf;
use jsonld::{
Expand,
RemoteDocument,
syntax::{ErrorCode, Parse},
};
use std::{collections::HashMap, thread};
const TOKIO_WORKER_STACK: usize = 2 * 1024 * 1024;
const NEARLY_EXHAUSTED_STACK: usize = 256 * 1024;
const LIMIT: usize = jsonld::context_processing::MAX_REMOTE_CONTEXTS;
fn expand_on_stack(input: String, documents: Vec<(String, String)>, stack: usize) -> Result<usize, ErrorCode> {
thread::Builder::new()
.stack_size(stack)
.spawn(move || {
let loader: HashMap<IriBuf, RemoteDocument> = documents
.into_iter()
.map(|(url, body)| {
let (json, _) = jsonld::syntax::Value::parse_str(&body).unwrap();
let url = IriBuf::new(url).unwrap();
(url.clone(), RemoteDocument::new(Some(url), None, json))
})
.collect();
let runtime = tokio::runtime::Builder::new_current_thread().build().unwrap();
let (json, _) = jsonld::syntax::Value::parse_str(&input).unwrap();
runtime.block_on(async { json.expand(&loader).await.map(|expanded| expanded.len()).map_err(|error| error.code()) })
})
.unwrap()
.join()
.unwrap()
}
fn node_under(url: &str) -> String {
format!(r#"{{"@context": "{url}", "@id": "http://example.com/node", "t": "v"}}"#)
}
fn two_document_cycle() -> Vec<(String, String)> {
vec![
(
"http://example.com/terms.jsonld".to_owned(),
r#"{"@context": {"t": "http://example.com/terms#t", "u": "http://example.com/terms#u", "w": {"@id": "http://example.com/terms#w", "@type": "@id"}}}"#
.to_owned(),
),
(
"http://example.com/cycle-a.jsonld".to_owned(),
r#"{"@context": ["terms.jsonld", "cycle-b.jsonld"]}"#.to_owned(),
),
("http://example.com/cycle-b.jsonld".to_owned(), r#"{"@context": "cycle-a.jsonld"}"#.to_owned()),
]
}
fn chain(length: usize) -> Vec<(String, String)> {
(0..length)
.map(|index| {
let url = format!("http://example.com/chain-{index}.jsonld");
let body = if index + 1 == length {
r#"{"@context": {"t": "http://example.com/chain#t"}}"#.to_owned()
} else {
format!(
r#"{{"@context": ["chain-{}.jsonld", {{"c{index}": "http://example.com/chain#c{index}"}}]}}"#,
index + 1
)
};
(url, body)
})
.collect()
}
#[test]
fn a_two_document_reference_cycle_ends_in_a_context_overflow_on_a_tokio_worker_stack() {
let result = expand_on_stack(node_under("http://example.com/cycle-a.jsonld"), two_document_cycle(), TOKIO_WORKER_STACK);
assert_eq!(result, Err(ErrorCode::ContextOverflow));
}
#[test]
fn a_two_document_reference_cycle_ends_in_a_context_overflow_on_a_nearly_exhausted_stack() {
let result = expand_on_stack(node_under("http://example.com/cycle-a.jsonld"), two_document_cycle(), NEARLY_EXHAUSTED_STACK);
assert_eq!(result, Err(ErrorCode::ContextOverflow));
}
#[test]
fn a_context_including_itself_ends_in_a_context_overflow_on_a_nearly_exhausted_stack() {
let documents = vec![(
"http://example.com/loop.jsonld".to_owned(),
r#"{"@context": ["loop.jsonld", {"t": "http://example.com/loop#t"}]}"#.to_owned(),
)];
let result = expand_on_stack(node_under("http://example.com/loop.jsonld"), documents, NEARLY_EXHAUSTED_STACK);
assert_eq!(result, Err(ErrorCode::ContextOverflow));
}
#[test]
fn two_documents_importing_each_other_end_in_an_invalid_context_entry() {
let documents = vec![
(
"http://example.com/import-a.jsonld".to_owned(),
r#"{"@context": {"@import": "import-b.jsonld", "t": "http://example.com/import#t"}}"#.to_owned(),
),
(
"http://example.com/import-b.jsonld".to_owned(),
r#"{"@context": {"@import": "import-a.jsonld"}}"#.to_owned(),
),
];
let result = expand_on_stack(node_under("http://example.com/import-a.jsonld"), documents, NEARLY_EXHAUSTED_STACK);
assert_eq!(result, Err(ErrorCode::InvalidContextEntry));
}
#[test]
fn an_import_whose_scoped_context_names_the_importing_document_terminates() {
let documents = vec![
(
"http://example.com/scoped-a.jsonld".to_owned(),
r#"{"@context": {"@import": "scoped-b.jsonld", "t": "http://example.com/scoped#t"}}"#.to_owned(),
),
(
"http://example.com/scoped-b.jsonld".to_owned(),
r#"{"@context": {"s": {"@id": "http://example.com/scoped#s", "@context": "scoped-a.jsonld"}}}"#.to_owned(),
),
];
let result = expand_on_stack(node_under("http://example.com/scoped-a.jsonld"), documents, NEARLY_EXHAUSTED_STACK);
assert_eq!(result, Ok(1));
}
#[test]
fn a_chain_exactly_at_the_limit_is_processed_on_a_nearly_exhausted_stack() {
let result = expand_on_stack(node_under("http://example.com/chain-0.jsonld"), chain(LIMIT), NEARLY_EXHAUSTED_STACK);
assert_eq!(result, Ok(1));
}
#[test]
fn a_chain_exactly_at_the_limit_is_processed_on_a_tokio_worker_stack() {
let result = expand_on_stack(node_under("http://example.com/chain-0.jsonld"), chain(LIMIT), TOKIO_WORKER_STACK);
assert_eq!(result, Ok(1));
}
fn nested_scoped_context(depth: usize) -> String {
let mut context = r#"{"t": "http://example.com/nested#t"}"#.to_owned();
for level in (0..depth).rev() {
context = format!(r#"{{"s{level}": {{"@id": "http://example.com/nested#s{level}", "@context": {context}}}}}"#);
}
format!(r#"{{"@context": {context}, "@id": "http://example.com/node", "t": "v"}}"#)
}
#[test]
fn nested_scoped_contexts_are_processed_on_a_nearly_exhausted_stack() {
let result = expand_on_stack(nested_scoped_context(24), Vec::new(), NEARLY_EXHAUSTED_STACK);
assert!(result.is_ok(), "{result:?}");
}
#[test]
fn scoped_contexts_nested_past_the_depth_bound_end_in_a_context_overflow() {
let result = expand_on_stack(nested_scoped_context(200), Vec::new(), NEARLY_EXHAUSTED_STACK);
assert_eq!(result, Err(ErrorCode::ContextOverflow));
}
#[test]
fn a_chain_one_past_the_limit_ends_in_a_context_overflow() {
let result = expand_on_stack(node_under("http://example.com/chain-0.jsonld"), chain(LIMIT + 1), NEARLY_EXHAUSTED_STACK);
assert_eq!(result, Err(ErrorCode::ContextOverflow));
}