jsonld_context_processing/algorithm/mod.rs
1//! # Two implementations, one algorithm: keep `sync.rs` in step
2//!
3//! This module, together with `define.rs` and `iri.rs`, holds the `async`
4//! implementation of the [context processing algorithm][1] — the one that can
5//! call the [`Loader`] to fetch remote contexts and `@import`ed documents.
6//! `sync.rs`, in this same directory, holds a second, hand-maintained
7//! implementation of the *same* algorithm with `async`, `.await` and the boxed
8//! [`Recursion`] stripped out and recursion made direct. It is used as a fast path whenever
9//! `requires_loader` proves the input contains no remote `@context` IRI and no
10//! `@import`, which is the common case in real payloads.
11//!
12//! Which of the two runs is invisible to callers: for any input both must
13//! produce the same active context and the same error. **A change to the
14//! algorithm here therefore has to be mirrored in `sync.rs`, and a change there
15//! mirrored back here.** Nothing enforces this — the two are separate function
16//! bodies with no shared code, so a fix applied to only one silently makes the
17//! result depend on whether the input happened to reference a remote context.
18//! The mirrored pairs are:
19//!
20//! | this module | `sync.rs` |
21//! |---|---|
22//! | `process_context` (`mod.rs`) | `process_context_sync` |
23//! | `define` (`define.rs`) | `define_sync` |
24//! | `expand_iri_with` (`iri.rs`) | `expand_iri_with_sync` |
25//!
26//! `sync.rs` does guard the one class of divergence it can detect — reaching a
27//! code path that would need the loader — by returning
28//! [`Error::LoadingDocumentFailed`]. That protects against `requires_loader`
29//! drifting out of step with the algorithm body; it says nothing about the two
30//! implementations otherwise agreeing.
31//!
32//! [1]: https://www.w3.org/TR/json-ld11-api/#context-processing-algorithm
33
34use std::{hash::Hash, sync::Arc};
35
36use crate::{Error, Options, Process, Processed, ProcessingCache, ProcessingResult, ProcessingStack, WarningHandler, cache::cache_key};
37use iri_rs::{Iri, IriBuf};
38use jsonld_core::{Context, Environment, ExtractContext, Loader, ProcessingMode, Term};
39use jsonld_syntax::{self as syntax, Nullable};
40use rdfx::vocabulary::VocabularyMut;
41
42mod define;
43mod iri;
44mod merged;
45mod native_stack;
46mod sync;
47
48pub use define::*;
49pub use iri::*;
50pub use merged::*;
51use native_stack::Recursion;
52use sync::{process_context_sync, requires_loader};
53use syntax::context::definition::KeyOrKeywordRef;
54
55/// Checks an `@vocab` value against the JSON-LD 1.0 rule.
56///
57/// 1.0 required the value to be an absolute IRI or a blank node identifier;
58/// document-relative resolution only arrived in 1.1. A bare `_:` prefix counts,
59/// as it is what makes `@vocab` map properties to blank nodes.
60pub(crate) fn is_valid_vocab_json_ld_1_0(value: &syntax::context::definition::Vocab) -> bool {
61 value.as_str().starts_with("_:") || Iri::new(value.as_str()).is_ok()
62}
63
64impl Process for syntax::context::Context {
65 async fn process_full<N, L, W>(
66 &self,
67 vocabulary: &mut N,
68 active_context: &Context<N::Iri, N::BlankId>,
69 loader: &L,
70 base_url: Option<N::Iri>,
71 options: Options,
72 mut warnings: W,
73 ) -> Result<Processed<'_, N::Iri, N::BlankId>, Error<L::Error>>
74 where
75 N: VocabularyMut,
76 N::Iri: Clone + Eq + Hash,
77 N::BlankId: Clone + PartialEq,
78 L: Loader,
79 W: WarningHandler<N>,
80 {
81 // Fast path: no remote `@context` IRIs and no `@import` anywhere in
82 // the tree means the entire algorithm runs synchronously — no
83 // boxed `Recursion` per level, no future state machine.
84 if !requires_loader(self) {
85 return process_context_sync(
86 Environment {
87 vocabulary,
88 loader,
89 warnings: &mut warnings,
90 },
91 active_context,
92 self,
93 ProcessingStack::default(),
94 base_url,
95 options,
96 0,
97 );
98 }
99
100 process_context(
101 Environment {
102 vocabulary,
103 loader,
104 warnings: &mut warnings,
105 },
106 active_context,
107 self,
108 ProcessingStack::default(),
109 base_url,
110 options,
111 true,
112 )
113 .await
114 }
115
116 async fn process_full_with_cache<N, L, W>(
117 &self,
118 vocabulary: &mut N,
119 active_context: &Context<N::Iri, N::BlankId>,
120 loader: &L,
121 base_url: Option<N::Iri>,
122 options: Options,
123 mut warnings: W,
124 cache: &ProcessingCache<N::Iri, N::BlankId>,
125 ) -> Result<Processed<'_, N::Iri, N::BlankId>, Error<L::Error>>
126 where
127 N: VocabularyMut,
128 N::Iri: Clone + Eq + Hash,
129 N::BlankId: Clone + Eq + Hash,
130 L: Loader,
131 W: WarningHandler<N>,
132 {
133 let key = cache_key(active_context, self, base_url.as_ref(), options);
134
135 if let Some(cached) = cache.get(key, active_context, self, base_url.as_ref(), options) {
136 return Ok(Processed::new(self, (*cached).clone()));
137 }
138
139 // Retained for the cache entry: `base_url` is moved into processing.
140 let entry_active = active_context.clone();
141 let entry_base_url = base_url.clone();
142
143 let processed = if requires_loader(self) {
144 process_context(
145 Environment {
146 vocabulary,
147 loader,
148 warnings: &mut warnings,
149 },
150 active_context,
151 self,
152 ProcessingStack::default(),
153 base_url,
154 options,
155 true,
156 )
157 .await?
158 } else {
159 process_context_sync(
160 Environment {
161 vocabulary,
162 loader,
163 warnings: &mut warnings,
164 },
165 active_context,
166 self,
167 ProcessingStack::default(),
168 base_url,
169 options,
170 0,
171 )?
172 };
173
174 cache.insert(key, entry_active, self.clone(), entry_base_url, options, Arc::new(processed.processed.clone()));
175 Ok(processed)
176 }
177}
178
179/// Resolves `iri_ref` against `base_iri` and interns the result in `vocabulary`.
180///
181/// With no base IRI, `iri_ref` must already be an absolute IRI. Returns `None`
182/// when the reference cannot be turned into one either way.
183fn resolve_iri<V, I>(vocabulary: &mut V, iri_ref: iri_rs::IriRef<&str>, base_iri: Option<&I>) -> Option<I>
184where
185 V: rdfx::vocabulary::IriVocabularyMut<Iri = I>,
186{
187 if let Some(base_iri) = base_iri {
188 let base = vocabulary.iri(base_iri)?;
189 let resolved = iri_ref.resolved(&base).ok()?;
190 let iri_buf = IriBuf::try_from(resolved).ok()?;
191 Some(vocabulary.insert_owned(iri_buf))
192 } else {
193 let iri = Iri::try_from(iri_ref).ok()?;
194 Some(vocabulary.insert(iri))
195 }
196}
197
198/// Runs the [context processing algorithm][1], layering `local_context` on top
199/// of `active_context`.
200///
201/// Recurses for each remote `@context` IRI, fetching it through the loader, and
202/// for each term definition's scoped `@context` via `define`. `remote_contexts`
203/// carries the chain of remote context URLs entered so far, which bounds that
204/// recursion; a top-level call starts from an empty [`ProcessingStack`].
205///
206/// `validate_scoped_context` is the algorithm's input of the same name: `true`
207/// for a document's own `@context` and for contexts applied while expanding,
208/// `false` only when Create Term Definition validates a scoped `@context`
209/// (step 21.3). Only then is a context already in `remote_contexts` skipped
210/// (step 5.2.2); anywhere else a repeat is processed again, because JSON-LD
211/// processes the entries of a `@context` array in order.
212///
213/// Every mutation is applied to a clone of `active_context`, so the caller's
214/// context is left untouched.
215///
216/// This is the `async` half of a mirrored pair; see the module documentation
217/// before changing it.
218///
219/// [1]: https://www.w3.org/TR/json-ld11-api/#context-processing-algorithm
220async fn process_context<'l: 'a, 'a, N, L, W>(
221 mut env: Environment<'a, N, L, W>,
222 active_context: &'a Context<N::Iri, N::BlankId>,
223 local_context: &'l syntax::context::Context,
224 mut remote_contexts: ProcessingStack<N::Iri>,
225 base_url: Option<N::Iri>,
226 mut options: Options,
227 validate_scoped_context: bool,
228) -> ProcessingResult<'l, N::Iri, N::BlankId, L::Error>
229where
230 N: VocabularyMut,
231 N::Iri: Clone + Eq + Hash,
232 N::BlankId: Clone + PartialEq,
233 L: Loader,
234 W: WarningHandler<N>,
235{
236 // 1) Initialize result to the result of cloning active context.
237 let mut result = active_context.clone();
238 result.set_processing_mode(options.processing_mode);
239
240 // 2) If `local_context` is an object containing the member @propagate,
241 // its value MUST be boolean true or false, set `propagate` to that value.
242 if let syntax::context::Context::One(syntax::ContextEntry::Definition(def)) = local_context
243 && let Some(propagate) = def.propagate
244 {
245 if options.processing_mode == ProcessingMode::JsonLd1_0 {
246 return Err(Error::InvalidContextEntry);
247 }
248
249 options.propagate = propagate;
250 }
251
252 // 3) If propagate is false, and result does not have a previous context,
253 // set previous context in result to active context.
254 if !options.propagate && result.previous_context().is_none() {
255 result.set_previous_context(active_context.clone());
256 }
257
258 // 4) If local context is not an array, set it to an array containing only local context.
259 // 5) For each item context in local context:
260 for context in local_context {
261 match context {
262 // 5.1) If context is null:
263 syntax::ContextEntry::Null => {
264 // If `override_protected` is false and `active_context` contains any protected term
265 // definitions, an invalid context nullification has been detected and processing
266 // is aborted.
267 if !options.override_protected && result.has_protected_items() {
268 return Err(Error::InvalidContextNullification);
269 }
270 // Otherwise, initialize result as a newly-initialized active context, setting
271 // previous_context in result to the previous value of result if propagate is
272 // false. Continue with the next context.
273 let previous_result = result;
274
275 // Initialize `result` as a newly-initialized active context, setting both
276 // `base_iri` and `original_base_url` to the value of `original_base_url` in
277 // active context, ...
278 result = Context::new(active_context.original_base_url().cloned());
279 result.set_processing_mode(options.processing_mode);
280
281 // ... and, if `propagate` is `false`, `previous_context` in `result` to the
282 // previous value of `result`.
283 if !options.propagate {
284 result.set_previous_context(previous_result);
285 }
286 }
287
288 // 5.2) If context is a string,
289 syntax::ContextEntry::IriRef(iri_ref) => {
290 // Initialize `context` to the result of resolving context against base URL.
291 // If base URL is not a valid IRI, then context MUST be a valid IRI, otherwise
292 // a loading document failed error has been detected and processing is aborted.
293 let context_iri = resolve_iri(env.vocabulary, iri_ref.as_ref(), base_url.as_ref()).ok_or(Error::LoadingDocumentFailed)?;
294
295 // 5.2.2) If validate scoped context is false, and remote contexts already
296 // includes context do not process context further and continue to any next
297 // context in local context.
298 //
299 // JSON-LD 1.0 had no such allowance: a context already in remote contexts is a
300 // recursive context inclusion error, whatever the caller.
301 if remote_contexts.cycle(&context_iri) {
302 if options.processing_mode == ProcessingMode::JsonLd1_0 {
303 return Err(Error::RecursiveContextInclusion);
304 }
305 if !validate_scoped_context {
306 continue;
307 }
308 }
309
310 // 5.2.3) If the number of entries in the `remote_contexts` array exceeds a
311 // processor defined limit, a context overflow error has been detected and
312 // processing is aborted; otherwise, add context to remote contexts.
313 if remote_contexts.len() >= crate::MAX_REMOTE_CONTEXTS {
314 return Err(Error::ContextOverflow);
315 }
316 remote_contexts.enter(context_iri.clone());
317
318 // 5.2.4) If context was previously dereferenced, then the processor MUST NOT do a
319 // further dereference, and context is set to the previously established internal
320 // representation: set `context_document` to the previously dereferenced document,
321 // and set loaded context to the value of the @context entry from the document in
322 // context document.
323 //
324 // 5.2.5) Otherwise, set `context document` to the RemoteDocument obtained by
325 // dereferencing context using the LoadDocumentCallback, passing context for url,
326 // and http://www.w3.org/ns/json-ld#context for profile and for requestProfile.
327 //
328 // If context cannot be dereferenced, or the document from context document cannot
329 // be transformed into the internal representation , a loading remote context
330 // failed error has been detected and processing is aborted.
331 // If the document has no top-level map with an @context entry, an invalid remote
332 // context has been detected and processing is aborted.
333 // Set loaded context to the value of that entry.
334 let loaded_context = if let Some(previously) = remote_contexts.dereferenced(&context_iri) {
335 previously
336 } else {
337 let loaded = Arc::new(
338 env.loader
339 .load_with(env.vocabulary, context_iri.clone())
340 .await?
341 .into_document()
342 .into_ld_context()
343 .map_err(Error::ContextExtractionFailed)?,
344 );
345 remote_contexts.remember_dereferenced(context_iri.clone(), Arc::clone(&loaded));
346 loaded
347 };
348
349 // 5.2.6) Set result to the result of recursively calling this algorithm, passing
350 // result for active context, loaded context for local context, the documentUrl of
351 // context document for base URL, a copy of remote contexts, and validate scoped
352 // context.
353 let new_options = Options {
354 processing_mode: options.processing_mode,
355 override_protected: false,
356 propagate: true,
357 vocab: options.vocab,
358 };
359
360 let r = Recursion::new(process_context(
361 Environment {
362 vocabulary: env.vocabulary,
363 loader: env.loader,
364 warnings: env.warnings,
365 },
366 &result,
367 &loaded_context,
368 remote_contexts.clone(),
369 Some(context_iri),
370 new_options,
371 validate_scoped_context,
372 ))
373 .await?;
374
375 result = r.into_processed();
376 }
377
378 // 5.4) Context definition.
379 syntax::ContextEntry::Definition(context) => {
380 // 5.5) If context has a @version entry:
381 if context.version.is_some() {
382 // 5.5.2) If processing mode is set to json-ld-1.0, a processing mode conflict
383 // error has been detected.
384 if options.processing_mode == ProcessingMode::JsonLd1_0 {
385 return Err(Error::ProcessingModeConflict);
386 }
387 }
388
389 // 5.6) If context has an @import entry:
390 let import_context = match &context.import {
391 Some(import_value) => {
392 // 5.6.1) If processing mode is json-ld-1.0, an invalid context entry error
393 // has been detected.
394 if options.processing_mode == ProcessingMode::JsonLd1_0 {
395 return Err(Error::InvalidContextEntry);
396 }
397
398 // 5.6.3) Initialize import to the result of resolving the value of
399 // @import.
400 let import = resolve_iri(env.vocabulary, import_value.as_ref(), base_url.as_ref()).ok_or(Error::InvalidImportValue)?;
401
402 // 5.6.4) Dereference import.
403 let import_context = env
404 .loader
405 .load_with(env.vocabulary, import.clone())
406 .await?
407 .into_document()
408 .into_ld_context()
409 .map_err(Error::ContextExtractionFailed)?;
410
411 // If the dereferenced document has no top-level map with an @context
412 // entry, or if the value of @context is not a context definition
413 // (i.e., it is not an map), an invalid remote context has been
414 // detected and processing is aborted; otherwise, set import context
415 // to the value of that entry.
416 match &import_context {
417 syntax::context::Context::One(syntax::ContextEntry::Definition(import_context_def)) => {
418 // If `import_context` has a @import entry, an invalid context entry
419 // error has been detected and processing is aborted.
420 if import_context_def.import.is_some() {
421 return Err(Error::InvalidContextEntry);
422 }
423 }
424 _ => {
425 return Err(Error::InvalidRemoteContext);
426 }
427 }
428
429 // Set `context` to the result of merging context into
430 // `import_context`, replacing common entries with those from
431 // `context`.
432 Some(import_context)
433 }
434 None => None,
435 };
436
437 let context = Merged::new(context, import_context);
438
439 // 5.7) If context has a @base entry and remote contexts is empty, i.e.,
440 // the currently being processed context is not a remote context:
441 if remote_contexts.is_empty() {
442 // Initialize value to the value associated with the @base entry.
443 if let Some(value) = context.base() {
444 match value {
445 syntax::Nullable::Null => {
446 // If value is null, remove the base IRI of result.
447 result.set_base_iri(None);
448 }
449 syntax::Nullable::Some(iri_ref) => {
450 if let Ok(iri) = Iri::try_from(iri_ref.as_ref()) {
451 result.set_base_iri(Some(env.vocabulary.insert(iri)));
452 } else {
453 let resolved = resolve_iri(env.vocabulary, iri_ref.as_ref(), result.base_iri()).ok_or(Error::InvalidBaseIri)?;
454 result.set_base_iri(Some(resolved));
455 }
456 }
457 }
458 }
459 }
460
461 // 5.8) If context has a @vocab entry:
462 // Initialize value to the value associated with the @vocab entry.
463 if let Some(value) = context.vocab() {
464 match value {
465 syntax::Nullable::Null => {
466 // If value is null, remove any vocabulary mapping from result.
467 result.set_vocabulary(None);
468 }
469 syntax::Nullable::Some(value) => {
470 // Otherwise, if value is an IRI or blank node identifier, the
471 // vocabulary mapping of result is set to the result of IRI
472 // expanding value using true for document relative. If it is not
473 // an IRI, or a blank node identifier, an invalid vocab mapping
474 // error has been detected and processing is aborted.
475 // NOTE: The use of blank node identifiers to value for @vocab is
476 // obsolete, and may be removed in a future version of JSON-LD.
477 //
478 // Document-relative expansion of `@vocab` is a JSON-LD 1.1 addition:
479 // 1.0 required an absolute IRI or blank node identifier, and rejects
480 // `""` and `/relative` (`expand#t0115`, `expand#t0116`). A bare `_:`
481 // prefix stays valid in 1.0 (`expand#t0075`).
482 let json_ld_1_0 = options.processing_mode == ProcessingMode::JsonLd1_0;
483 if json_ld_1_0 && !is_valid_vocab_json_ld_1_0(value) {
484 return Err(Error::InvalidVocabMapping);
485 }
486
487 match expand_iri_simple(&mut env, &result, Nullable::Some(value.into()), !json_ld_1_0, Some(options.vocab))? {
488 Some(arc) if matches!(arc.as_ref(), Term::Id(_)) => {
489 let term = std::sync::Arc::try_unwrap(arc).unwrap_or_else(|a| (*a).clone());
490 result.set_vocabulary(Some(term));
491 }
492 _ => return Err(Error::InvalidVocabMapping),
493 }
494 }
495 }
496 }
497
498 // 5.9) If context has a @language entry:
499 if let Some(value) = context.language() {
500 match value {
501 Nullable::Null => {
502 // 5.9.2) If value is null, remove any default language from result.
503 result.set_default_language(None);
504 }
505 Nullable::Some(tag) => {
506 result.set_default_language(Some(tag.to_owned()));
507 }
508 }
509 }
510
511 // 5.10) If context has a @direction entry:
512 if let Some(value) = context.direction() {
513 // 5.10.1) If processing mode is json-ld-1.0, an invalid context entry error
514 // has been detected and processing is aborted.
515 if options.processing_mode == ProcessingMode::JsonLd1_0 {
516 return Err(Error::InvalidContextEntry);
517 }
518
519 match value {
520 Nullable::Null => {
521 // 5.10.3) If value is null, remove any base direction from result.
522 result.set_default_base_direction(None);
523 }
524 Nullable::Some(dir) => {
525 result.set_default_base_direction(Some(dir));
526 }
527 }
528 }
529
530 // 5.12) Create a map `defined` to keep track of whether or not a term
531 // has already been defined or is currently being defined during recursion.
532 let mut defined = DefinedTerms::new();
533 let protected = context.protected().unwrap_or(false);
534
535 // 5.13) For each key-value pair in context where key is not
536 // @base, @direction, @import, @language, @propagate, @protected, @version,
537 // or @vocab,
538 // invoke the Create Term Definition algorithm passing result for
539 // active context, context for local context, key, defined, base URL,
540 // and the value of the @protected entry from context, if any, for protected.
541 // (and the value of override protected)
542 if context.type_().is_some() {
543 define(
544 Environment {
545 vocabulary: env.vocabulary,
546 loader: env.loader,
547 warnings: env.warnings,
548 },
549 &mut result,
550 &context,
551 KeyOrKeywordRef::Keyword(syntax::Keyword::Type),
552 &mut defined,
553 remote_contexts.clone(),
554 base_url.clone(),
555 protected,
556 options,
557 )
558 .await?;
559 }
560
561 for (key, _binding) in context.bindings() {
562 define(
563 Environment {
564 vocabulary: env.vocabulary,
565 loader: env.loader,
566 warnings: env.warnings,
567 },
568 &mut result,
569 &context,
570 key.into(),
571 &mut defined,
572 remote_contexts.clone(),
573 base_url.clone(),
574 protected,
575 options,
576 )
577 .await?;
578 }
579 }
580 }
581 }
582
583 Ok(Processed::new(local_context, result))
584}