1use alloc::{
14 borrow::ToOwned,
15 boxed::Box,
16 collections::BTreeMap,
17 string::{String, ToString},
18 sync::Arc,
19 vec::Vec,
20};
21use serde_json::{Map, Value};
22
23use crate::{Error, Limits, Registry};
24
25const KEYWORDS: &[&str] = &[
27 "@base",
28 "@container",
29 "@context",
30 "@direction",
31 "@graph",
32 "@id",
33 "@import",
34 "@included",
35 "@index",
36 "@json",
37 "@language",
38 "@list",
39 "@nest",
40 "@none",
41 "@prefix",
42 "@propagate",
43 "@protected",
44 "@reverse",
45 "@set",
46 "@type",
47 "@value",
48 "@version",
49 "@vocab",
50];
51
52#[must_use]
54pub(crate) fn is_keyword(value: &str) -> bool {
55 KEYWORDS.contains(&value)
56}
57
58fn ends_with_gen_delim(value: &str) -> bool {
61 matches!(
62 value.chars().next_back(),
63 Some(':' | '/' | '?' | '#' | '[' | ']' | '@')
64 )
65}
66
67#[derive(Clone, Debug, Eq, PartialEq)]
69pub(crate) enum TypeMapping {
70 Id,
72 Vocab,
74 Json,
76 None,
78 Datatype(String),
80}
81
82#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
84pub(crate) struct Container {
85 pub list: bool,
86 pub set: bool,
87 pub language: bool,
88 pub index: bool,
89 pub id: bool,
90 pub kind: bool,
91 pub graph: bool,
92}
93
94impl Container {
95 fn add(&mut self, value: &str) {
96 match value {
97 "@list" => self.list = true,
98 "@set" => self.set = true,
99 "@language" => self.language = true,
100 "@index" => self.index = true,
101 "@id" => self.id = true,
102 "@type" => self.kind = true,
103 "@graph" => self.graph = true,
104 _ => {}
105 }
106 }
107}
108
109#[derive(Clone, Debug, Default, Eq, PartialEq)]
111pub(crate) struct TermDefinition {
112 pub iri: Option<String>,
115 pub type_mapping: Option<TypeMapping>,
116 pub container: Container,
117 pub language: Option<Option<String>>,
119 pub reverse: bool,
120 pub prefix: bool,
121 pub protected: bool,
122 pub scoped: Option<Box<Value>>,
125}
126
127#[derive(Clone, Debug, Default)]
134pub(crate) struct ActiveContext {
135 terms: Arc<BTreeMap<String, TermDefinition>>,
136 base: Option<String>,
137 vocab: Option<String>,
138 language: Option<String>,
139}
140
141impl ActiveContext {
142 pub(crate) fn is_empty(&self) -> bool {
144 self.terms.is_empty()
145 && self.base.is_none()
146 && self.vocab.is_none()
147 && self.language.is_none()
148 }
149
150 pub(crate) fn term(&self, name: &str) -> Option<&TermDefinition> {
151 self.terms.get(name)
152 }
153
154 pub(crate) fn vocab(&self) -> Option<&str> {
155 self.vocab.as_deref()
156 }
157
158 pub(crate) fn language(&self) -> Option<&str> {
159 self.language.as_deref()
160 }
161
162 pub(crate) fn base(&self) -> Option<&str> {
163 self.base.as_deref()
164 }
165
166 pub(crate) fn terms(&self) -> impl Iterator<Item = (&String, &TermDefinition)> {
168 self.terms.iter()
169 }
170}
171
172pub(crate) struct Session<'a> {
175 pub registry: &'a Registry,
176 pub cache: &'a dyn ContextCache,
177 pub limits: Limits,
178 pub contexts: usize,
179 pub nodes: usize,
180 pub unresolved: Vec<String>,
181 pub rdf: bool,
184}
185
186impl<'a> Session<'a> {
187 pub(crate) fn new(registry: &'a Registry, limits: Limits) -> Self {
188 Self::with_cache(registry, &NoCache, limits)
189 }
190
191 pub(crate) fn with_cache(
192 registry: &'a Registry,
193 cache: &'a dyn ContextCache,
194 limits: Limits,
195 ) -> Self {
196 Self {
197 registry,
198 cache,
199 limits,
200 contexts: 0,
201 nodes: 0,
202 unresolved: Vec::new(),
203 rdf: false,
204 }
205 }
206
207 pub(crate) fn charge_node(&mut self) -> Result<(), Error> {
208 self.nodes += 1;
209 if self.nodes > self.limits.max_nodes {
210 return Err(Error::NodeBudgetExceeded);
211 }
212 Ok(())
213 }
214
215 fn charge_context(&mut self) -> Result<(), Error> {
216 self.contexts += 1;
217 if self.contexts > self.limits.max_contexts {
218 return Err(Error::ContextBudgetExceeded);
219 }
220 Ok(())
221 }
222
223 fn note_unresolved(&mut self, iri: &str) {
224 if !self.unresolved.iter().any(|seen| seen == iri) {
225 self.unresolved.push(iri.to_owned());
226 }
227 }
228}
229
230#[derive(Debug)]
236pub struct ProcessedContext {
237 pub(crate) active: ActiveContext,
238 pub(crate) charged: usize,
239 pub(crate) unresolved: Vec<String>,
240 pub(crate) source: Option<Value>,
242}
243
244pub trait ContextCache {
255 fn get(&self, key: &str) -> Option<Arc<ProcessedContext>>;
257 fn put(&self, key: String, context: Arc<ProcessedContext>);
259}
260
261#[derive(Clone, Copy, Debug, Default)]
263pub struct NoCache;
264
265impl ContextCache for NoCache {
266 fn get(&self, _key: &str) -> Option<Arc<ProcessedContext>> {
267 None
268 }
269
270 fn put(&self, _key: String, _context: Arc<ProcessedContext>) {}
271}
272
273pub(crate) fn cache_key(local: &Value, limits: &Limits) -> String {
276 let mut key = serde_json::to_string(local).unwrap_or_default();
277 key.push_str(&alloc::format!(
278 "\u{0}{}\u{0}{}",
279 limits.max_contexts,
280 limits.max_terms
281 ));
282 key
283}
284
285const ACTIVITYSTREAMS: &str = "https://www.w3.org/ns/activitystreams";
291
292pub(crate) fn process(
293 active: &ActiveContext,
294 local: &Value,
295 session: &mut Session<'_>,
296 remote: &mut Vec<String>,
297) -> Result<ActiveContext, Error> {
298 let cacheable = active.is_empty()
302 && remote.is_empty()
303 && session.contexts == 0
304 && session.unresolved.is_empty();
305 if !cacheable {
306 return process_uncached(active, local, session, remote);
307 }
308 let key = cache_key(local, &session.limits);
309 if let Some(cached) = session.cache.get(&key) {
310 session.contexts = cached.charged;
311 if session.contexts > session.limits.max_contexts {
312 return Err(Error::ContextBudgetExceeded);
313 }
314 for iri in &cached.unresolved {
315 session.note_unresolved(iri);
316 }
317 return Ok(cached.active.clone());
318 }
319 let result = process_uncached(active, local, session, remote)?;
320 session.cache.put(
321 key,
322 Arc::new(ProcessedContext {
323 active: result.clone(),
324 charged: session.contexts,
325 unresolved: session.unresolved.clone(),
326 source: None,
327 }),
328 );
329 Ok(result)
330}
331
332fn process_uncached(
333 active: &ActiveContext,
334 local: &Value,
335 session: &mut Session<'_>,
336 remote: &mut Vec<String>,
337) -> Result<ActiveContext, Error> {
338 let mut result = active.clone();
339 let entries: Vec<&Value> = match local {
340 Value::Array(items) => items.iter().collect(),
341 other => Vec::from([other]),
342 };
343
344 for entry in entries {
345 session.charge_context()?;
346 match entry {
347 Value::Null => {
349 let base = result.base.clone();
350 result = ActiveContext {
351 base,
352 ..ActiveContext::default()
353 };
354 }
355 Value::String(iri) => {
356 let resolved = resolve_context_iri(&result, iri);
357 let document = match session.registry.resolve(&resolved) {
358 Some(document) => document.clone(),
359 None => {
368 session.note_unresolved(&resolved);
369 match session.registry.resolve(ACTIVITYSTREAMS) {
370 Some(document) if resolved != ACTIVITYSTREAMS => document.clone(),
371 _ => continue,
372 }
373 }
374 };
375 if remote.iter().any(|seen| seen == &resolved) {
376 return Err(Error::CyclicContext(resolved));
377 }
378 remote.push(resolved);
379 let outcome = process(&result, &document, session, remote);
380 remote.pop();
381 result = outcome?;
382 }
383 Value::Object(map) => apply_term_map(&mut result, map, session)?,
384 _ => return Err(Error::InvalidContext),
385 }
386 }
387
388 Ok(result)
389}
390
391fn resolve_context_iri(active: &ActiveContext, iri: &str) -> String {
393 if is_absolute_iri(iri) {
394 return iri.to_owned();
395 }
396 match active.base() {
397 Some(base) => resolve_against(base, iri),
398 None => iri.to_owned(),
399 }
400}
401
402fn apply_term_map(
404 result: &mut ActiveContext,
405 map: &Map<String, Value>,
406 session: &mut Session<'_>,
407) -> Result<(), Error> {
408 if let Some(base) = map.get("@base") {
409 match base {
410 Value::Null => result.base = None,
411 Value::String(value) => result.base = Some(value.clone()),
412 _ => return Err(Error::InvalidContext),
413 }
414 }
415 if let Some(vocab) = map.get("@vocab") {
416 match vocab {
417 Value::Null => result.vocab = None,
418 Value::String(value) => {
421 result.vocab = expand_iri(result, value, true, true);
422 }
423 _ => return Err(Error::InvalidContext),
424 }
425 }
426 if let Some(language) = map.get("@language") {
427 match language {
428 Value::Null => result.language = None,
429 Value::String(value) => result.language = Some(value.to_lowercase()),
430 _ => return Err(Error::InvalidContext),
431 }
432 }
433
434 let protected = map
435 .get("@protected")
436 .and_then(Value::as_bool)
437 .unwrap_or(false);
438 let mut defined = BTreeMap::new();
439 for key in map.keys() {
440 if is_keyword(key) {
441 continue;
442 }
443 create_term_definition(result, map, key, &mut defined, protected, session)?;
444 }
445 Ok(())
446}
447
448fn create_term_definition(
454 active: &mut ActiveContext,
455 local: &Map<String, Value>,
456 term: &str,
457 defined: &mut BTreeMap<String, bool>,
458 protected: bool,
459 session: &mut Session<'_>,
460) -> Result<(), Error> {
461 match defined.get(term) {
462 Some(true) => return Ok(()),
463 Some(false) => return Err(Error::CyclicContext(term.to_owned())),
464 None => {}
465 }
466 if active.terms.len() >= session.limits.max_terms {
467 return Err(Error::TermBudgetExceeded);
468 }
469 defined.insert(term.to_owned(), false);
470
471 let value = local.get(term).unwrap_or(&Value::Null);
472 let mut definition = TermDefinition {
473 protected,
474 ..TermDefinition::default()
475 };
476
477 match value {
478 Value::Null => {}
480 Value::String(iri) => {
481 definition.iri =
482 expand_iri_while_defining(active, local, iri, defined, protected, session)?;
483 definition.prefix = ends_with_gen_delim(iri);
484 }
485 Value::Object(map) => {
486 if map.get("@id").is_some_and(Value::is_null) {
487 Arc::make_mut(&mut active.terms).insert(term.to_owned(), definition);
489 defined.insert(term.to_owned(), true);
490 return Ok(());
491 }
492 if let Some(Value::Bool(flag)) = map.get("@protected") {
493 definition.protected = *flag;
494 }
495 if let Some(reverse) = map.get("@reverse") {
496 let Value::String(iri) = reverse else {
497 return Err(Error::InvalidTermDefinition(term.to_owned()));
498 };
499 definition.iri =
500 expand_iri_while_defining(active, local, iri, defined, protected, session)?;
501 definition.reverse = true;
502 } else if let Some(id) = map.get("@id") {
503 let Value::String(iri) = id else {
504 return Err(Error::InvalidTermDefinition(term.to_owned()));
505 };
506 definition.iri =
507 expand_iri_while_defining(active, local, iri, defined, protected, session)?;
508 } else {
509 definition.iri =
512 expand_iri_while_defining(active, local, term, defined, protected, session)?;
513 }
514
515 if let Some(kind) = map.get("@type") {
516 let Value::String(value) = kind else {
517 return Err(Error::InvalidTermDefinition(term.to_owned()));
518 };
519 definition.type_mapping = Some(match value.as_str() {
520 "@id" => TypeMapping::Id,
521 "@vocab" => TypeMapping::Vocab,
522 "@json" => TypeMapping::Json,
523 "@none" => TypeMapping::None,
524 other => TypeMapping::Datatype(
525 expand_iri_while_defining(
526 active, local, other, defined, protected, session,
527 )?
528 .unwrap_or_else(|| other.to_owned()),
529 ),
530 });
531 }
532
533 match map.get("@container") {
534 Some(Value::String(value)) => definition.container.add(value),
535 Some(Value::Array(values)) => {
536 for value in values {
537 let Value::String(value) = value else {
538 return Err(Error::InvalidTermDefinition(term.to_owned()));
539 };
540 definition.container.add(value);
541 }
542 }
543 Some(_) => return Err(Error::InvalidTermDefinition(term.to_owned())),
544 None => {}
545 }
546
547 match map.get("@language") {
548 Some(Value::Null) => definition.language = Some(None),
549 Some(Value::String(value)) => {
550 definition.language = Some(Some(value.to_lowercase()));
551 }
552 Some(_) => return Err(Error::InvalidTermDefinition(term.to_owned())),
553 None => {}
554 }
555
556 if let Some(Value::Bool(flag)) = map.get("@prefix") {
557 definition.prefix = *flag;
558 }
559 if let Some(scoped) = map.get("@context") {
560 definition.scoped = Some(Box::new(scoped.clone()));
561 }
562 }
563 _ => return Err(Error::InvalidTermDefinition(term.to_owned())),
564 }
565
566 Arc::make_mut(&mut active.terms).insert(term.to_owned(), definition);
567 defined.insert(term.to_owned(), true);
568 Ok(())
569}
570
571fn expand_iri_while_defining(
578 active: &mut ActiveContext,
579 local: &Map<String, Value>,
580 value: &str,
581 defined: &mut BTreeMap<String, bool>,
582 protected: bool,
583 session: &mut Session<'_>,
584) -> Result<Option<String>, Error> {
585 if is_keyword(value) {
586 return Ok(Some(value.to_owned()));
587 }
588 if value.starts_with('@') {
589 return Ok(None);
590 }
591
592 if local.contains_key(value) && defined.get(value) != Some(&true) {
593 create_term_definition(active, local, value, defined, protected, session)?;
594 }
595 if let Some(definition) = active.term(value) {
596 return Ok(definition.iri.clone());
597 }
598
599 if let Some(index) = value.find(':')
600 && index > 0
601 {
602 let (prefix, rest) = value.split_at(index);
603 let suffix = &rest[1..];
604 if prefix == "_" || suffix.starts_with("//") {
605 return Ok(Some(value.to_owned()));
606 }
607 if local.contains_key(prefix) && defined.get(prefix) != Some(&true) {
608 create_term_definition(active, local, prefix, defined, protected, session)?;
609 }
610 if let Some(definition) = active.term(prefix)
611 && definition.prefix
612 && let Some(iri) = &definition.iri
613 {
614 let mut expanded = iri.clone();
615 expanded.push_str(suffix);
616 return Ok(Some(expanded));
617 }
618 return Ok(Some(value.to_owned()));
619 }
620
621 if let Some(prefix) = active.vocab() {
622 let mut expanded = prefix.to_owned();
623 expanded.push_str(value);
624 return Ok(Some(expanded));
625 }
626 Ok(Some(value.to_owned()))
627}
628
629pub(crate) fn expand_iri(
635 active: &ActiveContext,
636 value: &str,
637 vocab: bool,
638 document_relative: bool,
639) -> Option<String> {
640 if is_keyword(value) {
641 return Some(value.to_owned());
642 }
643 if value.starts_with('@') {
644 return None;
645 }
646
647 if vocab && let Some(definition) = active.term(value) {
648 return definition.iri.clone();
649 }
650
651 if let Some(index) = value.find(':')
652 && index > 0
653 {
654 let (prefix, rest) = value.split_at(index);
655 let suffix = &rest[1..];
656 if prefix == "_" || suffix.starts_with("//") {
657 return Some(value.to_owned());
658 }
659 if let Some(definition) = active.term(prefix)
660 && definition.prefix
661 && let Some(iri) = &definition.iri
662 {
663 let mut expanded = iri.clone();
664 expanded.push_str(suffix);
665 return Some(expanded);
666 }
667 return Some(value.to_owned());
668 }
669
670 if vocab && let Some(prefix) = active.vocab() {
671 let mut expanded = prefix.to_owned();
672 expanded.push_str(value);
673 return Some(expanded);
674 }
675 if document_relative && let Some(base) = active.base() {
676 return Some(resolve_against(base, value));
677 }
678 Some(value.to_owned())
679}
680
681pub(crate) fn is_absolute_iri(value: &str) -> bool {
683 let Some(index) = value.find(':') else {
684 return false;
685 };
686 if index == 0 {
687 return false;
688 }
689 let scheme = &value[..index];
690 scheme.starts_with(|c: char| c.is_ascii_alphabetic())
691 && scheme
692 .chars()
693 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
694}
695
696pub(crate) fn resolve_against(base: &str, reference: &str) -> String {
699 use iri_string::types::{IriAbsoluteStr, IriReferenceStr};
700
701 let Ok(base) = IriAbsoluteStr::new(base) else {
702 return reference.to_owned();
703 };
704 let Ok(reference) = IriReferenceStr::new(reference) else {
705 return reference.to_owned();
706 };
707 reference.resolve_against(base).to_string()
708}