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