Skip to main content

jsonld_context_processing/
stack.rs

1use jsonld_core::HashMap;
2use parking_lot::Mutex;
3use std::{hash::Hash, sync::Arc};
4
5/// Maximum depth of the remote context chain.
6///
7/// The [context processing algorithm][1] mandates a processor-defined limit
8/// on the number of entries in the `remote contexts` array; exceeding it is a
9/// `context overflow` error. This bounds the damage a hostile loader can do
10/// by serving an endless chain of distinct context IRIs.
11///
12/// [1]: <https://www.w3.org/TR/json-ld11-api/#context-processing-algorithm>
13pub const MAX_REMOTE_CONTEXTS: usize = 64;
14
15/// Single frame of the context processing stack.
16struct StackNode<I> {
17    /// Previous frame.
18    previous: Option<Arc<StackNode<I>>>,
19
20    /// URL of the last loaded context.
21    url: I,
22}
23
24impl<I> StackNode<I> {
25    /// Creates a stack frame recording the load of `url` on top of `previous`.
26    fn new(previous: Option<Arc<StackNode<I>>>, url: I) -> StackNode<I> {
27        StackNode { previous, url }
28    }
29
30    /// Checks whether this frame or any frame below it holds `url`.
31    fn contains(&self, url: &I) -> bool
32    where
33        I: PartialEq,
34    {
35        if self.url == *url {
36            true
37        } else {
38            match &self.previous {
39                Some(prev) => prev.contains(url),
40                None => false,
41            }
42        }
43    }
44}
45
46/// The chain of remote context URLs currently being processed.
47///
48/// This is the specification's `remote contexts` array. It serves two purposes:
49/// spotting a context that includes itself, and bounding the length of a remote
50/// context chain at [`MAX_REMOTE_CONTEXTS`]. Whether it is empty also tells the
51/// algorithm whether the context being processed came from a remote document,
52/// which is what decides if its `@base` entry applies.
53///
54/// Implemented as an immutable singly-linked list behind [`Arc`]s, so the copy
55/// each recursive call receives is a pointer bump rather than a clone of the
56/// chain.
57///
58/// Every copy of a stack also shares one record of the contexts dereferenced
59/// since the stack was created, which is how one run of the algorithm honours
60/// step 5.2.4: "If context was previously dereferenced, then the processor MUST
61/// NOT do a further dereference". The chain is per branch of the recursion; the
62/// record is per run.
63pub struct ProcessingStack<I> {
64    head: Option<Arc<StackNode<I>>>,
65    dereferenced: Arc<Mutex<HashMap<I, Arc<jsonld_syntax::context::Context>>>>,
66}
67
68impl<I> Clone for ProcessingStack<I> {
69    // Written by hand because a derive would demand `I: Clone`, while copying
70    // the stack only bumps the two `Arc`s it holds.
71    fn clone(&self) -> Self {
72        Self {
73            head: self.head.clone(),
74            dereferenced: Arc::clone(&self.dereferenced),
75        }
76    }
77}
78
79impl<I> ProcessingStack<I> {
80    /// Creates an empty stack, meaning no remote context is being processed,
81    /// with an empty record of dereferenced contexts.
82    #[must_use]
83    pub fn new() -> Self {
84        Self {
85            head: None,
86            dereferenced: Arc::new(Mutex::new(HashMap::default())),
87        }
88    }
89
90    /// Checks whether no remote context has been entered, i.e. the context being
91    /// processed is not itself remote.
92    #[must_use]
93    pub fn is_empty(&self) -> bool {
94        self.head.is_none()
95    }
96
97    /// Returns the number of remote contexts on the stack.
98    ///
99    /// Walks the chain, so this is linear in the stack depth.
100    #[must_use]
101    pub fn len(&self) -> usize {
102        let mut len = 0;
103        let mut node = &self.head;
104        while let Some(frame) = node {
105            len += 1;
106            node = &frame.previous;
107        }
108        len
109    }
110
111    /// Checks whether `url` is already on the stack, meaning entering it would
112    /// close a loop.
113    pub fn cycle(&self, url: &I) -> bool
114    where
115        I: PartialEq,
116    {
117        match &self.head {
118            Some(head) => head.contains(url),
119            None => false,
120        }
121    }
122
123    /// Pushes `url` onto the stack, unless it is already there.
124    ///
125    /// Returns `true` when the URL was added, and `false` when it was already on
126    /// the stack and nothing changed. What `false` means is version-dependent:
127    /// JSON-LD 1.0 treats it as a recursive context inclusion error, while 1.1
128    /// skips the context only while validating a scoped context (step 5.2.2)
129    /// and otherwise processes it again, which is [`Self::enter`].
130    pub fn push(&mut self, url: I) -> bool
131    where
132        I: PartialEq,
133    {
134        if self.cycle(&url) {
135            false
136        } else {
137            self.enter(url);
138            true
139        }
140    }
141
142    /// Pushes `url` onto the stack whether or not it is already there.
143    ///
144    /// This is step 5.2.3's "add context to remote contexts": under JSON-LD 1.1
145    /// a context named again outside scoped-context validation is processed
146    /// again, and each processing counts toward [`MAX_REMOTE_CONTEXTS`].
147    pub fn enter(&mut self, url: I) {
148        let previous = self.head.take();
149        self.head = Some(Arc::new(StackNode::new(previous, url)));
150    }
151
152    /// The `@context` of the document dereferenced for `url` earlier in this
153    /// run, if there was one (step 5.2.4).
154    pub fn dereferenced(&self, url: &I) -> Option<Arc<jsonld_syntax::context::Context>>
155    where
156        I: Eq + Hash,
157    {
158        self.dereferenced.lock().get(url).map(Arc::clone)
159    }
160
161    /// Records the `@context` dereferenced for `url`, so the rest of this run
162    /// reuses it instead of dereferencing `url` again (step 5.2.4).
163    pub fn remember_dereferenced(&self, url: I, context: Arc<jsonld_syntax::context::Context>)
164    where
165        I: Eq + Hash,
166    {
167        self.dereferenced.lock().insert(url, context);
168    }
169}
170
171impl<I> Default for ProcessingStack<I> {
172    fn default() -> Self {
173        Self::new()
174    }
175}