Skip to main content

jsonld_syntax/context/
try_from_json.rs

1use super::{
2    Context,
3    ContextEntry,
4    Definition,
5    TermDefinition,
6    definition,
7    term_definition::{self, InvalidNest},
8};
9use crate::{Container, ErrorCode, Keyword, Nullable, TryFromJson};
10use iri_rs::IriRefBuf;
11
12#[derive(Debug, Clone, thiserror::Error)]
13/// Error raised when a JSON value is not a valid context.
14pub enum InvalidContext {
15    #[error("Invalid IRI reference: {0}")]
16    /// A context entry was a string that is not a valid IRI reference. Holds
17    /// the rejected string.
18    InvalidIriRef(String),
19
20    #[error("Unexpected {0}")]
21    /// A value had the wrong JSON kind. Holds the kind that was found and the
22    /// kinds that would have been accepted.
23    Unexpected(jstrict::Kind, &'static [jstrict::Kind]),
24
25    #[error("Invalid `@direction`")]
26    /// A `@direction` entry was a string other than `"ltr"` or `"rtl"`.
27    InvalidDirection,
28
29    #[error("Duplicate key")]
30    /// The same key appeared twice in a context definition or in a term
31    /// definition.
32    DuplicateKey,
33
34    #[error("Invalid term definition")]
35    /// A term definition, or a structured entry of a context definition such as
36    /// `@type` or `@version`, held an entry or a value the grammar does not
37    /// allow there.
38    InvalidTermDefinition,
39
40    #[error("Invalid `@nest` value `{0}`")]
41    /// A `@nest` entry held a keyword other than `@nest`. Holds the rejected
42    /// string.
43    InvalidNestValue(String),
44
45    #[error("Context too deeply nested")]
46    /// Scoped-context nesting exceeds [`MAX_CONTEXT_DEPTH`].
47    TooDeep,
48}
49
50/// Maximum nesting depth of scoped contexts (`@context` inside a term
51/// definition) accepted when converting JSON into a [`Context`].
52///
53/// Deeper nesting is refused with [`InvalidContext::TooDeep`]. Genuine contexts
54/// nest a handful of levels at most.
55pub const MAX_CONTEXT_DEPTH: usize = 128;
56
57/// Stack left below which converting a nested scoped context moves to a fresh
58/// stack segment. It has to exceed what one nesting level uses, which is
59/// largest in unoptimized builds.
60const RED_ZONE: usize = 128 * 1024;
61
62/// Size of each fresh stack segment.
63const SEGMENT: usize = 2 * 1024 * 1024;
64
65/// Converts a scoped context nested in a term definition, on a fresh stack
66/// segment when the current one is nearly used.
67///
68/// The conversion recurses once per nesting level, and an embedder may call it
69/// on a thread whose stack it has already partly used, so without this a
70/// nesting well inside [`MAX_CONTEXT_DEPTH`] could abort the process on a stack
71/// overflow instead of converting or failing with
72/// [`InvalidContext::TooDeep`].
73fn nested_context_try_from_json(value: &jstrict::Value, depth: usize) -> Result<Context, InvalidContext> {
74    stacker::maybe_grow(RED_ZONE, SEGMENT, || context_try_from_json(value, depth))
75}
76
77impl InvalidContext {
78    /// Returns the JSON-LD error code this error reports as.
79    #[must_use]
80    pub fn code(&self) -> ErrorCode {
81        match self {
82            Self::InvalidIriRef(_) => ErrorCode::InvalidIriMapping,
83            Self::Unexpected(_, _) => ErrorCode::InvalidContextEntry,
84            Self::InvalidDirection => ErrorCode::InvalidBaseDirection,
85            Self::DuplicateKey => ErrorCode::DuplicateKey,
86            Self::InvalidTermDefinition => ErrorCode::InvalidTermDefinition,
87            Self::InvalidNestValue(_) => ErrorCode::InvalidNestValue,
88            Self::TooDeep => ErrorCode::ContextOverflow,
89        }
90    }
91}
92
93impl From<crate::Unexpected> for InvalidContext {
94    fn from(crate::Unexpected(u, e): crate::Unexpected) -> Self {
95        Self::Unexpected(u, e)
96    }
97}
98
99impl TryFromJson for TermDefinition {
100    type Error = InvalidContext;
101
102    fn try_from_json(value: &jstrict::Value) -> Result<Self, InvalidContext> {
103        term_definition_try_from_json(value, 0)
104    }
105}
106
107/// Stores an entry value into an `Option` field, failing with
108/// [`InvalidContext::DuplicateKey`] when the field was already set — the same
109/// policy applied to duplicate term bindings.
110macro_rules! set_unique {
111    ($field:expr, $value:expr) => {
112        if $field.replace($value).is_some() {
113            return Err(InvalidContext::DuplicateKey);
114        }
115    };
116}
117
118fn term_definition_try_from_json(value: &jstrict::Value, depth: usize) -> Result<TermDefinition, InvalidContext> {
119    match value {
120        jstrict::Value::String(s) => Ok(TermDefinition::Simple(term_definition::Simple(s.as_str().to_owned()))),
121        jstrict::Value::Object(o) => {
122            let mut def = term_definition::Expanded::new();
123
124            for jstrict::object::Entry { key, value } in o {
125                match Keyword::try_from(key.as_str()) {
126                    Ok(Keyword::Id) => set_unique!(def.id, Nullable::try_from_json(value)?),
127                    Ok(Keyword::Type) => set_unique!(def.type_, Nullable::try_from_json(value)?),
128                    Ok(Keyword::Context) => set_unique!(def.context, Box::new(nested_context_try_from_json(value, depth + 1)?)),
129                    Ok(Keyword::Reverse) => set_unique!(def.reverse, definition::Key::try_from_json(value)?),
130                    Ok(Keyword::Index) => set_unique!(def.index, term_definition::Index::try_from_json(value)?),
131                    Ok(Keyword::Language) => set_unique!(def.language, Nullable::try_from_json(value)?),
132                    Ok(Keyword::Direction) => set_unique!(def.direction, Nullable::try_from_json(value)?),
133                    Ok(Keyword::Container) => {
134                        let container = match value {
135                            jstrict::Value::Null => Nullable::Null,
136                            other => {
137                                let container = Container::try_from_json(other)?;
138                                Nullable::Some(container)
139                            }
140                        };
141
142                        set_unique!(def.container, container);
143                    }
144                    Ok(Keyword::Nest) => set_unique!(def.nest, term_definition::Nest::try_from_json(value)?),
145                    Ok(Keyword::Prefix) => set_unique!(def.prefix, bool::try_from_json(value)?),
146                    Ok(Keyword::Propagate) => set_unique!(def.propagate, bool::try_from_json(value)?),
147                    Ok(Keyword::Protected) => set_unique!(def.protected, bool::try_from_json(value)?),
148                    _ => return Err(InvalidContext::InvalidTermDefinition),
149                }
150            }
151
152            Ok(TermDefinition::Expanded(Box::new(def)))
153        }
154        unexpected => Err(InvalidContext::Unexpected(unexpected.kind(), &[jstrict::Kind::String, jstrict::Kind::Object])),
155    }
156}
157
158impl TryFromJson for term_definition::Type {
159    type Error = InvalidContext;
160
161    fn try_from_json(value: &jstrict::Value) -> Result<Self, InvalidContext> {
162        match value {
163            jstrict::Value::String(s) => Ok(Self::from(s.as_str().to_owned())),
164            unexpected => Err(InvalidContext::Unexpected(unexpected.kind(), &[jstrict::Kind::String])),
165        }
166    }
167}
168
169impl TryFromJson for definition::TypeContainer {
170    type Error = InvalidContext;
171
172    fn try_from_json(value: &jstrict::Value) -> Result<Self, InvalidContext> {
173        match value {
174            jstrict::Value::String(s) => match Keyword::try_from(s.as_str()) {
175                Ok(Keyword::Set) => Ok(Self::Set),
176                _ => Err(InvalidContext::InvalidTermDefinition),
177            },
178            unexpected => Err(InvalidContext::Unexpected(unexpected.kind(), &[jstrict::Kind::String])),
179        }
180    }
181}
182
183impl TryFromJson for definition::Type {
184    type Error = InvalidContext;
185
186    fn try_from_json(value: &jstrict::Value) -> Result<Self, InvalidContext> {
187        match value {
188            jstrict::Value::Object(o) => {
189                let mut container = None;
190                let mut protected = None;
191
192                for jstrict::object::Entry { key, value } in o {
193                    match Keyword::try_from(key.as_str()) {
194                        Ok(Keyword::Container) => {
195                            if container.replace(definition::TypeContainer::try_from_json(value)?).is_some() {
196                                return Err(InvalidContext::DuplicateKey);
197                            }
198                        }
199                        Ok(Keyword::Protected) => {
200                            if protected.replace(bool::try_from_json(value)?).is_some() {
201                                return Err(InvalidContext::DuplicateKey);
202                            }
203                        }
204                        _ => return Err(InvalidContext::InvalidTermDefinition),
205                    }
206                }
207
208                match container {
209                    Some(container) => Ok(Self { container, protected }),
210                    None => Err(InvalidContext::InvalidTermDefinition),
211                }
212            }
213            unexpected => Err(InvalidContext::Unexpected(unexpected.kind(), &[jstrict::Kind::Object])),
214        }
215    }
216}
217
218impl TryFromJson for definition::Version {
219    type Error = InvalidContext;
220
221    fn try_from_json(value: &jstrict::Value) -> Result<Self, InvalidContext> {
222        match value {
223            jstrict::Value::Number(n) => match n.as_str() {
224                "1.1" => Ok(Self::V1_1),
225                _ => Err(InvalidContext::InvalidTermDefinition),
226            },
227            unexpected => Err(InvalidContext::Unexpected(unexpected.kind(), &[jstrict::Kind::Number])),
228        }
229    }
230}
231
232impl TryFromJson for definition::Vocab {
233    type Error = InvalidContext;
234
235    fn try_from_json(value: &jstrict::Value) -> Result<Self, InvalidContext> {
236        match value {
237            jstrict::Value::String(s) => Ok(Self::from(s.as_str().to_owned())),
238            unexpected => Err(InvalidContext::Unexpected(unexpected.kind(), &[jstrict::Kind::String])),
239        }
240    }
241}
242
243impl TryFromJson for term_definition::Id {
244    type Error = InvalidContext;
245
246    fn try_from_json(value: &jstrict::Value) -> Result<Self, InvalidContext> {
247        match value {
248            jstrict::Value::String(s) => Ok(Self::from(s.as_str().to_owned())),
249            unexpected => Err(InvalidContext::Unexpected(unexpected.kind(), &[jstrict::Kind::String])),
250        }
251    }
252}
253
254impl TryFromJson for definition::Key {
255    type Error = InvalidContext;
256
257    fn try_from_json(value: &jstrict::Value) -> Result<Self, Self::Error> {
258        match value {
259            jstrict::Value::String(s) => Ok(Self::from(s.as_str().to_owned())),
260            unexpected => Err(InvalidContext::Unexpected(unexpected.kind(), &[jstrict::Kind::String])),
261        }
262    }
263}
264
265impl TryFromJson for term_definition::Index {
266    type Error = InvalidContext;
267
268    fn try_from_json(value: &jstrict::Value) -> Result<Self, InvalidContext> {
269        match value {
270            jstrict::Value::String(s) => Ok(Self::from(s.as_str().to_owned())),
271            unexpected => Err(InvalidContext::Unexpected(unexpected.kind(), &[jstrict::Kind::String])),
272        }
273    }
274}
275
276impl TryFromJson for term_definition::Nest {
277    type Error = InvalidContext;
278
279    fn try_from_json(value: &jstrict::Value) -> Result<Self, InvalidContext> {
280        match value {
281            jstrict::Value::String(s) => match Self::try_from(s.as_str().to_owned()) {
282                Ok(nest) => Ok(nest),
283                Err(InvalidNest(s)) => Err(InvalidContext::InvalidNestValue(s)),
284            },
285            unexpected => Err(InvalidContext::Unexpected(unexpected.kind(), &[jstrict::Kind::String])),
286        }
287    }
288}
289
290impl TryFromJson for Context {
291    type Error = InvalidContext;
292
293    fn try_from_json(value: &jstrict::Value) -> Result<Self, InvalidContext> {
294        context_try_from_json(value, 0)
295    }
296}
297
298fn context_try_from_json(value: &jstrict::Value, depth: usize) -> Result<Context, InvalidContext> {
299    if depth >= MAX_CONTEXT_DEPTH {
300        return Err(InvalidContext::TooDeep);
301    }
302
303    match value {
304        jstrict::Value::Array(a) => {
305            let mut many = Vec::with_capacity(a.len());
306
307            for item in a {
308                many.push(context_entry_try_from_json(item, depth)?);
309            }
310
311            Ok(Context::Many(many))
312        }
313        context => Ok(Context::One(context_entry_try_from_json(context, depth)?)),
314    }
315}
316
317impl TryFromJson for ContextEntry {
318    type Error = InvalidContext;
319
320    fn try_from_json(value: &jstrict::Value) -> Result<Self, InvalidContext> {
321        context_entry_try_from_json(value, 0)
322    }
323}
324
325fn context_entry_try_from_json(value: &jstrict::Value, depth: usize) -> Result<ContextEntry, InvalidContext> {
326    match value {
327        jstrict::Value::Null => Ok(ContextEntry::Null),
328        jstrict::Value::String(s) => match IriRefBuf::new(s.as_str().to_owned()) {
329            Ok(iri_ref) => Ok(ContextEntry::IriRef(iri_ref)),
330            Err(e) => Err(InvalidContext::InvalidIriRef(e.0)),
331        },
332        jstrict::Value::Object(o) => {
333            let mut def = Definition::new();
334
335            for jstrict::object::Entry { key, value } in o {
336                match Keyword::try_from(key.as_str()) {
337                    Ok(Keyword::Base) => set_unique!(def.base, Nullable::try_from_json(value)?),
338                    Ok(Keyword::Import) => set_unique!(def.import, IriRefBuf::try_from_json(value)?),
339                    Ok(Keyword::Language) => set_unique!(def.language, Nullable::try_from_json(value)?),
340                    Ok(Keyword::Direction) => set_unique!(def.direction, Nullable::try_from_json(value)?),
341                    Ok(Keyword::Propagate) => set_unique!(def.propagate, bool::try_from_json(value)?),
342                    Ok(Keyword::Protected) => set_unique!(def.protected, bool::try_from_json(value)?),
343                    Ok(Keyword::Type) => set_unique!(def.type_, definition::Type::try_from_json(value)?),
344                    Ok(Keyword::Version) => set_unique!(def.version, definition::Version::try_from_json(value)?),
345                    Ok(Keyword::Vocab) => set_unique!(def.vocab, Nullable::try_from_json(value)?),
346                    _ => {
347                        let term_def = match value {
348                            jstrict::Value::Null => Nullable::Null,
349                            other => Nullable::Some(term_definition_try_from_json(other, depth)?),
350                        };
351
352                        if def.bindings.insert_with(key.as_str().to_owned().into(), term_def).is_some() {
353                            return Err(InvalidContext::DuplicateKey);
354                        }
355                    }
356                }
357            }
358
359            Ok(ContextEntry::Definition(def))
360        }
361        unexpected => Err(InvalidContext::Unexpected(
362            unexpected.kind(),
363            &[jstrict::Kind::Null, jstrict::Kind::String, jstrict::Kind::Object],
364        )),
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    #![allow(clippy::unwrap_used)]
371    use super::*;
372    use crate::Parse;
373
374    fn nested_json(levels: usize) -> jstrict::Value {
375        let mut json = String::new();
376        for _ in 0..levels {
377            json.push_str("{\"a\":{\"@context\":");
378        }
379        json.push_str("{}");
380        for _ in 0..levels {
381            json.push_str("}}");
382        }
383
384        jstrict::Value::parse_str(&json).unwrap().0
385    }
386
387    /// A crafted deeply-nested `@context` must fail with an error instead of
388    /// exhausting the native stack during conversion.
389    #[test]
390    fn deeply_nested_context_conversion_fails_gracefully() {
391        let json = nested_json(MAX_CONTEXT_DEPTH + 1);
392        assert!(matches!(Context::try_from_json(&json), Err(InvalidContext::TooDeep)));
393    }
394
395    #[test]
396    fn reasonable_nesting_converts() {
397        let json = nested_json(MAX_CONTEXT_DEPTH - 1);
398        assert!(Context::try_from_json(&json).is_ok());
399    }
400
401    /// The deepest accepted nesting converts, and one level more is refused,
402    /// on a thread whose stack is far smaller than the conversion of that
403    /// nesting needs, so an embedder that already used most of its stack gets
404    /// a result rather than an abort.
405    #[test]
406    fn nesting_up_to_the_bound_converts_on_a_small_stack() {
407        let deepest = nested_json(MAX_CONTEXT_DEPTH - 1);
408        let too_deep = nested_json(MAX_CONTEXT_DEPTH + 1);
409
410        let (converted, refused) = std::thread::Builder::new()
411            .stack_size(256 * 1024)
412            .spawn(move || (Context::try_from_json(&deepest).is_ok(), Context::try_from_json(&too_deep)))
413            .unwrap()
414            .join()
415            .unwrap();
416
417        assert!(converted);
418        assert!(matches!(refused, Err(InvalidContext::TooDeep)));
419    }
420
421    /// Duplicate keyword entries fail like duplicate term bindings do,
422    /// instead of silently keeping the last value.
423    #[test]
424    fn duplicate_keyword_entries_are_rejected() {
425        let json = jstrict::Value::parse_str(r#"{"@vocab": "http://a/", "@vocab": "http://b/"}"#).unwrap().0;
426        assert!(matches!(Context::try_from_json(&json), Err(InvalidContext::DuplicateKey)));
427
428        let json = jstrict::Value::parse_str(r#"{"@id": "http://a/x", "@id": "http://b/x"}"#).unwrap().0;
429        assert!(matches!(TermDefinition::try_from_json(&json), Err(InvalidContext::DuplicateKey)));
430    }
431}