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