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}