1use std::mem::size_of;
4use std::sync::Arc;
5
6use fluent_uri::{Uri, UriRef};
7use rustc_hash::{FxHashMap, FxHashSet};
8
9use super::ast::{self, Ast};
10use crate::error::{CompileError, ErrorCode, LimitKind, Stage};
11use crate::ir::{AnchorId, ResourceId};
12
13#[repr(transparent)]
14#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
15pub struct DocumentId(pub(crate) u32);
16
17#[repr(transparent)]
18#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
19pub struct SchemaLocationId(u32);
20
21impl SchemaLocationId {
22 pub(crate) const fn get(self) -> u32 {
23 self.0
24 }
25}
26
27#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
28pub struct SchemaResourceLimits {
29 pub max_documents: usize,
30 pub max_resources: usize,
31 pub max_total_input_bytes: usize,
32 pub max_document_bytes: usize,
33 pub max_ast_nodes: usize,
34 pub max_ast_bytes: usize,
35 pub max_uri_bytes: usize,
36 pub max_total_uri_bytes: usize,
37 pub max_anchors_per_resource: usize,
38 pub max_total_anchors: usize,
39 pub max_references: usize,
40 pub max_schema_locations: usize,
41 pub max_resource_depth: u32,
42 pub max_resolution_work: usize,
43 pub max_retained_graph_bytes: usize,
44 pub max_peak_build_bytes: usize,
45}
46
47impl Default for SchemaResourceLimits {
48 fn default() -> Self {
49 Self {
50 max_documents: 128,
51 max_resources: 4_096,
52 max_total_input_bytes: 16 << 20,
53 max_document_bytes: 4 << 20,
54 max_ast_nodes: 1_000_000,
55 max_ast_bytes: 128 << 20,
56 max_uri_bytes: 16 << 10,
57 max_total_uri_bytes: 4 << 20,
58 max_anchors_per_resource: 4_096,
59 max_total_anchors: 65_536,
60 max_references: 1_000_000,
61 max_schema_locations: 1_000_000,
62 max_resource_depth: 1_024,
63 max_resolution_work: 4_000_000,
64 max_retained_graph_bytes: 128 << 20,
65 max_peak_build_bytes: 256 << 20,
66 }
67 }
68}
69
70#[derive(Clone, Debug)]
71struct RegisteredDocument {
72 retrieval_uri: Option<Arc<str>>,
73 text: Arc<str>,
74}
75
76#[derive(Debug)]
77pub struct SchemaRegistry {
78 documents: Vec<RegisteredDocument>,
79 retrieval_index: FxHashMap<Arc<str>, DocumentId>,
80 limits: SchemaResourceLimits,
81 total_input_bytes: usize,
82 total_uri_bytes: usize,
83}
84
85#[derive(Debug)]
86pub(crate) struct ParsedDocument {
87 pub(crate) retrieval_uri: Option<Arc<str>>,
88 pub(crate) text: Arc<str>,
89 pub(crate) ast: Ast,
90 pub(crate) locations: FxHashMap<Box<str>, SchemaLocationId>,
91 ast_bytes: usize,
92}
93
94#[derive(Debug)]
95pub(crate) struct SchemaLocation {
96 pub(crate) document: DocumentId,
97 pub(crate) resource: ResourceId,
98 pub(crate) document_pointer: Box<str>,
99 pub(crate) base_uri: Option<Arc<str>>,
100}
101
102#[derive(Debug)]
103pub(crate) struct SchemaResource {
104 pub(crate) canonical_uri: Option<Arc<str>>,
105 pub(crate) document: DocumentId,
106 pub(crate) root: SchemaLocationId,
107 pub(crate) static_anchors: Box<[AnchorId]>,
108 pub(crate) dynamic_anchors: Box<[AnchorId]>,
109 pub(crate) locations: FxHashMap<Box<str>, SchemaLocationId>,
110 pub(crate) anchor_names: FxHashMap<Box<str>, AnchorId>,
111}
112
113#[derive(Debug)]
114pub(crate) struct AnchorRecord {
115 pub(crate) resource: ResourceId,
116 pub(crate) name: Box<str>,
117 pub(crate) target: SchemaLocationId,
118 pub(crate) dynamic: bool,
119}
120
121#[derive(Clone, Copy, PartialEq, Eq, Debug)]
122pub(crate) enum ReferenceKind {
123 Static,
124 Dynamic,
125}
126
127impl ReferenceKind {
128 const fn index(self) -> usize {
129 match self {
130 Self::Static => 0,
131 Self::Dynamic => 1,
132 }
133 }
134}
135
136#[derive(Debug)]
137pub(crate) struct ReferenceRecord {
138 pub(crate) location: SchemaLocationId,
139 pub(crate) kind: ReferenceKind,
140 pub(crate) text: Box<str>,
141 pub(crate) base_uri: Option<Arc<str>>,
142}
143
144#[derive(Clone, Copy, Debug)]
145pub(crate) struct ResolvedReferenceRecord {
146 pub(crate) source: SchemaLocationId,
147 pub(crate) kind: ReferenceKind,
148 pub(crate) resolved: ResolvedReference,
149}
150
151#[derive(Debug)]
152pub(crate) struct ResourceGraph {
153 pub(crate) documents: Box<[ParsedDocument]>,
154 pub(crate) resources: Box<[SchemaResource]>,
155 pub(crate) locations: Box<[SchemaLocation]>,
156 pub(crate) anchors: Box<[AnchorRecord]>,
157 pub(crate) resolved_references: Box<[ResolvedReferenceRecord]>,
158 reference_index: Box<[[u32; 2]]>,
159 pub(crate) uri_index: FxHashMap<Arc<str>, ResourceId>,
160 pub(crate) retained_bytes: usize,
161 pub(crate) peak_build_bytes: usize,
162}
163
164#[cfg(feature = "bench-internals")]
165#[derive(Clone, Copy, Debug, Default)]
166pub(crate) struct ResourceDiscoveryProfile {
167 pub(crate) document_parse_ns: u64,
168 pub(crate) resource_uri_anchor_index_ns: u64,
169 pub(crate) canonicalize_ns: u64,
170 pub(crate) reference_resolution_ns: u64,
171 pub(crate) graph_freeze_ns: u64,
172}
173
174#[derive(Debug)]
175pub(crate) struct ResolvedUri {
176 pub(crate) resource: Arc<str>,
177 pub(crate) fragment: Option<String>,
178}
179
180#[derive(Clone, Copy, PartialEq, Eq, Debug)]
181pub(crate) enum FragmentTarget {
182 ResourceRoot(SchemaLocationId),
183 JsonPointer(SchemaLocationId),
184 Anchor(AnchorId),
185}
186
187#[derive(Clone, Copy, PartialEq, Eq, Debug)]
188pub(crate) struct ResolvedReference {
189 pub(crate) resource: ResourceId,
190 pub(crate) target: FragmentTarget,
191}
192
193impl ResourceGraph {
194 pub(crate) fn root_location(
195 &self,
196 retrieval_uri: Option<&str>,
197 root_text: &str,
198 ) -> Result<SchemaLocationId, CompileError> {
199 if let Some(uri) = retrieval_uri {
200 let canonical = parse_absolute_resource_uri(uri, "retrieval URI")?;
201 let resource = self
202 .uri_index
203 .get(canonical.as_str())
204 .copied()
205 .ok_or_else(|| {
206 resource_error(
207 ErrorCode::ReferenceResolution,
208 "root retrieval URI is not registered",
209 Some(canonical),
210 )
211 })?;
212 return self
213 .resources
214 .get(usize::try_from(resource.get()).map_err(|_| limit_overflow())?)
215 .map(|record| record.root)
216 .ok_or_else(|| malformed("root resource ID is invalid"));
217 }
218 let mut found = None;
219 for (index, document) in self.documents.iter().enumerate() {
220 if document.retrieval_uri.is_none() && document.text.as_ref() == root_text {
221 if found.is_some() {
222 return Err(malformed("anonymous root document is ambiguous"));
223 }
224 let _ = u32::try_from(index).map_err(|_| limit_overflow())?;
225 found = document.locations.get("").copied();
226 }
227 }
228 found.ok_or_else(|| malformed("anonymous root document is not registered"))
229 }
230
231 pub(crate) fn document_location(
232 &self,
233 document: DocumentId,
234 pointer: &str,
235 ) -> Option<SchemaLocationId> {
236 self.documents
237 .get(usize::try_from(document.0).ok()?)?
238 .locations
239 .get(pointer)
240 .copied()
241 }
242
243 pub(crate) fn location_ast(&self, location: SchemaLocationId) -> Result<&Ast, CompileError> {
244 let location = self
245 .locations
246 .get(usize::try_from(location.0).map_err(|_| limit_overflow())?)
247 .ok_or_else(|| malformed("schema location ID is invalid"))?;
248 let document = self
249 .documents
250 .get(usize::try_from(location.document.0).map_err(|_| limit_overflow())?)
251 .ok_or_else(|| malformed("document ID is invalid"))?;
252 ast_at_pointer(&document.ast, &location.document_pointer)
253 }
254
255 pub(crate) fn target_location(
256 &self,
257 target: FragmentTarget,
258 ) -> Result<SchemaLocationId, CompileError> {
259 match target {
260 FragmentTarget::ResourceRoot(location) | FragmentTarget::JsonPointer(location) => {
261 Ok(location)
262 }
263 FragmentTarget::Anchor(anchor) => self
264 .anchors
265 .get(usize::try_from(anchor.get()).map_err(|_| limit_overflow())?)
266 .map(|record| record.target)
267 .ok_or_else(|| malformed("anchor ID is invalid")),
268 }
269 }
270
271 #[cfg(test)]
272 pub(crate) fn resolve_reference(
273 &self,
274 base_uri: Option<&str>,
275 reference: &str,
276 ) -> Result<ResolvedReference, CompileError> {
277 let resolved = resolve_uri_reference(base_uri, reference)?;
278 let resource = self.uri_index.get(resolved.resource.as_ref()).copied();
279 resolve_fragment_target(
280 &self.resources,
281 resource,
282 resolved.fragment.as_deref(),
283 reference,
284 )
285 }
286
287 pub(crate) fn resolved_reference_from(
288 &self,
289 location: SchemaLocationId,
290 kind: ReferenceKind,
291 ) -> Result<ResolvedReference, CompileError> {
292 let index = self
293 .reference_index
294 .get(usize::try_from(location.0).map_err(|_| limit_overflow())?)
295 .ok_or_else(|| malformed("reference location index is invalid"))?[kind.index()];
296 let record = self
297 .resolved_references
298 .get(usize::try_from(index).map_err(|_| limit_overflow())?)
299 .ok_or_else(|| malformed("resolved reference occurrence is not indexed"))?;
300 if record.source != location || record.kind != kind {
301 return Err(malformed("resolved reference occurrence is inconsistent"));
302 }
303 Ok(record.resolved)
304 }
305}
306
307#[cfg(test)]
308fn resolve_fragment_target(
309 resources: &[SchemaResource],
310 resource: Option<ResourceId>,
311 fragment: Option<&str>,
312 reference: &str,
313) -> Result<ResolvedReference, CompileError> {
314 let resource = resource.ok_or_else(|| {
315 resource_error(
316 ErrorCode::ReferenceResolution,
317 "schema resource is not registered",
318 Some(reference.to_owned()),
319 )
320 })?;
321 let resource_record = resources
322 .get(usize::try_from(resource.0).map_err(|_| limit_overflow())?)
323 .ok_or_else(|| malformed("resource ID is invalid"))?;
324 let target = match fragment {
325 None | Some("") => FragmentTarget::ResourceRoot(resource_record.root),
326 Some(pointer) if pointer.starts_with('/') => {
327 validate_json_pointer(pointer)?;
328 let location = resource_record
329 .locations
330 .get(pointer)
331 .copied()
332 .ok_or_else(|| {
333 resource_error(
334 ErrorCode::ReferenceResolution,
335 "JSON Pointer does not identify a schema location",
336 Some(reference.to_owned()),
337 )
338 })?;
339 FragmentTarget::JsonPointer(location)
340 }
341 Some(anchor) => {
342 if !valid_anchor(anchor) {
343 return Err(resource_error(
344 ErrorCode::ReferenceResolution,
345 "plain-name fragment is not a valid anchor",
346 Some(reference.to_owned()),
347 ));
348 }
349 let anchor = resource_record
350 .anchor_names
351 .get(anchor)
352 .copied()
353 .ok_or_else(|| {
354 resource_error(
355 ErrorCode::ReferenceResolution,
356 "plain-name anchor is not registered in the resource",
357 Some(reference.to_owned()),
358 )
359 })?;
360 FragmentTarget::Anchor(anchor)
361 }
362 };
363 Ok(ResolvedReference { resource, target })
364}
365
366impl SchemaRegistry {
367 #[must_use]
368 pub fn new(limits: SchemaResourceLimits) -> Self {
369 Self {
370 documents: Vec::new(),
371 retrieval_index: FxHashMap::default(),
372 limits,
373 total_input_bytes: 0,
374 total_uri_bytes: 0,
375 }
376 }
377
378 pub fn insert(
379 &mut self,
380 retrieval_uri: impl AsRef<str>,
381 schema_text: impl Into<Arc<str>>,
382 ) -> Result<DocumentId, CompileError> {
383 self.insert_inner(Some(retrieval_uri.as_ref()), schema_text.into())
384 }
385
386 pub fn insert_anonymous(
387 &mut self,
388 schema_text: impl Into<Arc<str>>,
389 ) -> Result<DocumentId, CompileError> {
390 self.insert_inner(None, schema_text.into())
391 }
392
393 fn insert_inner(
394 &mut self,
395 retrieval_uri: Option<&str>,
396 text: Arc<str>,
397 ) -> Result<DocumentId, CompileError> {
398 check_limit(
399 LimitKind::DocumentCount,
400 self.documents.len().checked_add(1),
401 self.limits.max_documents,
402 )?;
403 check_limit(
404 LimitKind::InputBytes,
405 Some(text.len()),
406 self.limits.max_document_bytes,
407 )?;
408 let total_input = self.total_input_bytes.checked_add(text.len());
409 check_limit(
410 LimitKind::InputBytes,
411 total_input,
412 self.limits.max_total_input_bytes,
413 )?;
414
415 let retrieval_uri = match retrieval_uri {
416 Some(uri) => {
417 let canonical = parse_absolute_resource_uri(uri, "retrieval URI")?;
418 check_limit(
419 LimitKind::UriBytes,
420 Some(canonical.len()),
421 self.limits.max_uri_bytes,
422 )?;
423 let total_uri = self.total_uri_bytes.checked_add(canonical.len());
424 check_limit(
425 LimitKind::UriBytes,
426 total_uri,
427 self.limits.max_total_uri_bytes,
428 )?;
429 if self.retrieval_index.contains_key(canonical.as_str()) {
430 return Err(resource_error(
431 ErrorCode::DuplicateResource,
432 "duplicate retrieval URI",
433 Some(canonical),
434 ));
435 }
436 self.total_uri_bytes = total_uri.ok_or_else(limit_overflow)?;
437 Some(Arc::<str>::from(canonical))
438 }
439 None => None,
440 };
441
442 let id = DocumentId(u32::try_from(self.documents.len()).map_err(|_| limit_overflow())?);
443 self.documents
444 .try_reserve_exact(1)
445 .map_err(|_| limit_overflow())?;
446 if let Some(uri) = &retrieval_uri {
447 self.retrieval_index
448 .try_reserve(1)
449 .map_err(|_| limit_overflow())?;
450 self.retrieval_index.insert(uri.clone(), id);
451 }
452 self.documents.push(RegisteredDocument {
453 retrieval_uri,
454 text,
455 });
456 self.total_input_bytes = total_input.ok_or_else(limit_overflow)?;
457 Ok(id)
458 }
459
460 pub(crate) fn discover(self) -> Result<ResourceGraph, CompileError> {
461 ResourceBuilder::new(self)?.discover()
462 }
463
464 #[cfg(feature = "bench-internals")]
465 pub(crate) fn discover_profiled(
466 self,
467 ) -> Result<(ResourceGraph, ResourceDiscoveryProfile), CompileError> {
468 let mut profile = ResourceDiscoveryProfile::default();
469 let started = std::time::Instant::now();
470 let builder = ResourceBuilder::new(self)?;
471 profile.document_parse_ns = elapsed_ns(started);
472 builder.discover_profiled(profile)
473 }
474}
475
476#[cfg(all(test, feature = "alloc-probe"))]
477pub(crate) fn discover_memory_for_probe(
478 registry: SchemaRegistry,
479) -> Result<(usize, usize), CompileError> {
480 let graph = registry.discover()?;
481 Ok((graph.peak_build_bytes, graph.retained_bytes))
482}
483
484impl Default for SchemaRegistry {
485 fn default() -> Self {
486 Self::new(SchemaResourceLimits::default())
487 }
488}
489
490struct MutableResource {
491 canonical_uri: Option<Arc<str>>,
492 document: DocumentId,
493 root: SchemaLocationId,
494 static_anchors: Vec<AnchorId>,
495 dynamic_anchors: Vec<AnchorId>,
496 locations: FxHashMap<Box<str>, SchemaLocationId>,
497 anchor_names: FxHashMap<Box<str>, AnchorId>,
498}
499
500struct WorkItem<'a> {
501 schema: &'a Ast,
502 document_pointer: String,
503 resource_pointer: String,
504 base_uri: Option<Arc<str>>,
505 resource: ResourceId,
506 resource_depth: u32,
507 root: bool,
508 legacy_ids: bool,
509}
510
511struct ResourceBuilder {
512 limits: SchemaResourceLimits,
513 documents: Vec<ParsedDocument>,
514 resources: Vec<MutableResource>,
515 locations: Vec<SchemaLocation>,
516 anchors: Vec<AnchorRecord>,
517 references: Vec<ReferenceRecord>,
518 resolved_references: Vec<ResolvedReferenceRecord>,
519 reference_index: Vec<[u32; 2]>,
520 uri_index: FxHashMap<Arc<str>, ResourceId>,
521 counted_uris: FxHashSet<Arc<str>>,
522 memory: ResourceMemory,
523 total_uri_bytes: usize,
524 work: usize,
525}
526
527#[derive(Clone, Copy, Debug, Default)]
528struct ResourceMemory {
529 current_build: usize,
530 peak_build: usize,
531 retained_frozen: usize,
532}
533
534impl ResourceMemory {
535 fn observe_build(
536 &mut self,
537 retained: usize,
538 temporary: usize,
539 limit: usize,
540 ) -> Result<(), CompileError> {
541 let total = retained.checked_add(temporary).ok_or_else(limit_overflow)?;
542 self.current_build = retained;
543 self.peak_build = self.peak_build.max(total);
544 check_limit(LimitKind::ResourceBuildBytes, Some(total), limit)
545 }
546
547 fn freeze(&mut self, bytes: usize, limit: usize) -> Result<(), CompileError> {
548 self.retained_frozen = bytes;
549 check_limit(LimitKind::ResourceGraphBytes, Some(bytes), limit)
550 }
551}
552
553impl ResourceBuilder {
554 fn new(registry: SchemaRegistry) -> Result<Self, CompileError> {
555 let input_floor = registry_build_bytes(®istry)?;
556 check_limit(
557 LimitKind::ResourceBuildBytes,
558 Some(input_floor),
559 registry.limits.max_peak_build_bytes,
560 )?;
561 let mut registered = registry.documents;
562 registered.sort_by(|left, right| {
563 left.retrieval_uri
564 .as_deref()
565 .cmp(&right.retrieval_uri.as_deref())
566 .then_with(|| left.text.as_ref().cmp(right.text.as_ref()))
567 });
568 let mut documents = Vec::new();
569 documents
570 .try_reserve_exact(registered.len())
571 .map_err(|_| limit_overflow())?;
572 let parse_floor = input_floor
573 .checked_add(vec_bytes(&documents)?)
574 .ok_or_else(limit_overflow)?;
575 check_limit(
576 LimitKind::ResourceBuildBytes,
577 Some(parse_floor),
578 registry.limits.max_peak_build_bytes,
579 )?;
580 let mut ast_nodes = 0usize;
581 let mut ast_bytes = 0usize;
582 for document in registered {
583 let remaining_nodes = registry
584 .limits
585 .max_ast_nodes
586 .checked_sub(ast_nodes)
587 .ok_or_else(limit_overflow)?;
588 let remaining_bytes = registry
589 .limits
590 .max_ast_bytes
591 .checked_sub(ast_bytes)
592 .ok_or_else(limit_overflow)?;
593 let remaining_build = registry
594 .limits
595 .max_peak_build_bytes
596 .checked_sub(parse_floor)
597 .and_then(|bytes| bytes.checked_sub(ast_bytes))
598 .ok_or_else(|| {
599 let observed = parse_floor
600 .checked_add(ast_bytes)
601 .map_or(usize::MAX, |value| value);
602 resource_limit_error(
603 LimitKind::ResourceBuildBytes,
604 observed,
605 registry.limits.max_peak_build_bytes,
606 )
607 })?;
608 let (parsed, parsed_memory) = ast::parse_bounded(
609 &document.text,
610 remaining_nodes,
611 remaining_bytes.min(remaining_build),
612 )?;
613 ast_nodes = ast_nodes
614 .checked_add(parsed_memory.nodes)
615 .ok_or_else(limit_overflow)?;
616 ast_bytes = ast_bytes
617 .checked_add(parsed_memory.bytes)
618 .ok_or_else(limit_overflow)?;
619 documents.push(ParsedDocument {
620 retrieval_uri: document.retrieval_uri,
621 text: document.text,
622 ast: parsed,
623 locations: FxHashMap::default(),
624 ast_bytes: parsed_memory.bytes,
625 });
626 }
627 let mut counted_uris = FxHashSet::default();
628 counted_uris
629 .try_reserve(documents.len())
630 .map_err(|_| limit_overflow())?;
631 for document in &documents {
632 if let Some(uri) = &document.retrieval_uri {
633 counted_uris.insert(uri.clone());
634 }
635 }
636 let mut builder = Self {
637 limits: registry.limits,
638 documents,
639 resources: Vec::new(),
640 locations: Vec::new(),
641 anchors: Vec::new(),
642 references: Vec::new(),
643 resolved_references: Vec::new(),
644 reference_index: Vec::new(),
645 uri_index: FxHashMap::default(),
646 counted_uris,
647 memory: ResourceMemory {
648 current_build: 0,
649 peak_build: parse_floor
650 .checked_add(ast_bytes)
651 .ok_or_else(limit_overflow)?,
652 retained_frozen: 0,
653 },
654 total_uri_bytes: registry.total_uri_bytes,
655 work: 0,
656 };
657 builder.observe_build_memory(0)?;
658 Ok(builder)
659 }
660
661 fn discover(mut self) -> Result<ResourceGraph, CompileError> {
662 for document_index in 0..self.documents.len() {
663 let document = DocumentId(u32::try_from(document_index).map_err(|_| limit_overflow())?);
664 self.discover_document(document)?;
665 }
666 self.materialize_pointer_targets()?;
667 self.canonicalize_resource_ids()?;
668 self.validate_references()?;
669 self.freeze()
670 }
671
672 #[cfg(feature = "bench-internals")]
673 fn discover_profiled(
674 mut self,
675 mut profile: ResourceDiscoveryProfile,
676 ) -> Result<(ResourceGraph, ResourceDiscoveryProfile), CompileError> {
677 let started = std::time::Instant::now();
678 for document_index in 0..self.documents.len() {
679 let document = DocumentId(u32::try_from(document_index).map_err(|_| limit_overflow())?);
680 self.discover_document(document)?;
681 }
682 self.materialize_pointer_targets()?;
683 profile.resource_uri_anchor_index_ns = elapsed_ns(started);
684
685 let started = std::time::Instant::now();
686 self.canonicalize_resource_ids()?;
687 profile.canonicalize_ns = elapsed_ns(started);
688
689 let started = std::time::Instant::now();
690 self.validate_references()?;
691 profile.reference_resolution_ns = elapsed_ns(started);
692
693 let started = std::time::Instant::now();
694 let graph = self.freeze()?;
695 profile.graph_freeze_ns = elapsed_ns(started);
696 Ok((graph, profile))
697 }
698
699 fn freeze(mut self) -> Result<ResourceGraph, CompileError> {
700 let resource_output = self
701 .resources
702 .len()
703 .checked_mul(size_of::<SchemaResource>())
704 .ok_or_else(limit_overflow)?;
705 self.observe_build_memory(resource_output)?;
706 let mut resources = Vec::new();
707 resources
708 .try_reserve_exact(self.resources.len())
709 .map_err(|_| limit_overflow())?;
710 for resource in self.resources {
711 resources.push(SchemaResource {
712 canonical_uri: resource.canonical_uri,
713 document: resource.document,
714 root: resource.root,
715 static_anchors: resource.static_anchors.into_boxed_slice(),
716 dynamic_anchors: resource.dynamic_anchors.into_boxed_slice(),
717 locations: resource.locations,
718 anchor_names: resource.anchor_names,
719 });
720 }
721 let retained_bytes = frozen_graph_bytes(
722 &self.documents,
723 &resources,
724 &self.locations,
725 &self.anchors,
726 &self.resolved_references,
727 &self.reference_index,
728 &self.uri_index,
729 )?;
730 self.memory
731 .freeze(retained_bytes, self.limits.max_retained_graph_bytes)?;
732 let peak_build_bytes = self.memory.peak_build.max(retained_bytes);
733 Ok(ResourceGraph {
734 documents: self.documents.into_boxed_slice(),
735 resources: resources.into_boxed_slice(),
736 locations: self.locations.into_boxed_slice(),
737 anchors: self.anchors.into_boxed_slice(),
738 resolved_references: self.resolved_references.into_boxed_slice(),
739 reference_index: self.reference_index.into_boxed_slice(),
740 uri_index: self.uri_index,
741 retained_bytes,
742 peak_build_bytes,
743 })
744 }
745
746 fn canonicalize_resource_ids(&mut self) -> Result<(), CompileError> {
747 let old = std::mem::take(&mut self.resources);
748 let old_bytes = vec_bytes(&old)?;
749 let mut indexed = Vec::new();
750 indexed
751 .try_reserve_exact(old.len())
752 .map_err(|_| limit_overflow())?;
753 for entry in old.into_iter().enumerate() {
754 indexed.push(entry);
755 }
756 self.observe_build_memory(
757 old_bytes
758 .checked_add(vec_bytes(&indexed)?)
759 .ok_or_else(limit_overflow)?,
760 )?;
761 indexed.sort_by(|left, right| {
762 left.1
763 .canonical_uri
764 .as_deref()
765 .cmp(&right.1.canonical_uri.as_deref())
766 });
767 let mut remap = Vec::new();
768 remap
769 .try_reserve_exact(indexed.len())
770 .map_err(|_| limit_overflow())?;
771 remap.resize(indexed.len(), ResourceId(u32::MAX));
772 self.resources
773 .try_reserve_exact(indexed.len())
774 .map_err(|_| limit_overflow())?;
775 self.observe_build_memory(checked_sum([vec_bytes(&indexed)?, vec_bytes(&remap)?])?)?;
776 for (new_index, (old_index, resource)) in indexed.into_iter().enumerate() {
777 let new = ResourceId(u32::try_from(new_index).map_err(|_| limit_overflow())?);
778 remap[old_index] = new;
779 self.resources.push(resource);
780 }
781 for location in &mut self.locations {
782 location.resource = *remap
783 .get(usize::try_from(location.resource.get()).map_err(|_| limit_overflow())?)
784 .ok_or_else(|| malformed("resource remap is invalid"))?;
785 }
786 for anchor in &mut self.anchors {
787 anchor.resource = *remap
788 .get(usize::try_from(anchor.resource.get()).map_err(|_| limit_overflow())?)
789 .ok_or_else(|| malformed("resource remap is invalid"))?;
790 }
791 for resource in self.uri_index.values_mut() {
792 *resource = *remap
793 .get(usize::try_from(resource.get()).map_err(|_| limit_overflow())?)
794 .ok_or_else(|| malformed("resource remap is invalid"))?;
795 }
796 self.observe_build_memory(0)?;
797 Ok(())
798 }
799
800 fn validate_references(&mut self) -> Result<(), CompileError> {
801 self.resolved_references
802 .try_reserve_exact(self.references.len())
803 .map_err(|_| limit_overflow())?;
804 self.reference_index
805 .try_reserve_exact(self.locations.len())
806 .map_err(|_| limit_overflow())?;
807 self.reference_index
808 .resize(self.locations.len(), [u32::MAX; 2]);
809 for index in 0..self.references.len() {
810 self.bump_work(1)?;
811 let reference = self
812 .references
813 .get(index)
814 .ok_or_else(|| malformed("reference index is invalid"))?;
815 let location = self
816 .locations
817 .get(usize::try_from(reference.location.0).map_err(|_| limit_overflow())?)
818 .ok_or_else(|| malformed("reference location is invalid"))?;
819 let resource = self
820 .resources
821 .get(usize::try_from(location.resource.0).map_err(|_| limit_overflow())?)
822 .ok_or_else(|| malformed("reference resource is invalid"))?;
823 if resource.document != location.document {
824 return Err(malformed("resource and schema location documents differ"));
825 }
826 let keyword = match reference.kind {
827 ReferenceKind::Static => "$ref",
828 ReferenceKind::Dynamic => "$dynamicRef",
829 };
830 let pointer = if location.document_pointer.is_empty() {
831 format!("/{keyword}")
832 } else {
833 format!("{}/{keyword}", location.document_pointer)
834 };
835 let result = (|| {
836 let (target_resource, fragment) = if let Some(base) = reference.base_uri.as_deref()
837 {
838 let resolved = resolve_uri_reference(Some(base), &reference.text)?;
839 let resource = self
840 .uri_index
841 .get(resolved.resource.as_ref())
842 .copied()
843 .ok_or_else(|| {
844 resource_error(
845 ErrorCode::ReferenceResolution,
846 "schema resource is not registered",
847 Some(reference.text.to_string()),
848 )
849 })?;
850 (resource, resolved.fragment)
851 } else if UriRef::parse(reference.text.as_ref())
852 .map_err(|_| {
853 resource_error(
854 ErrorCode::InvalidUri,
855 "invalid URI reference",
856 Some(reference.text.to_string()),
857 )
858 })?
859 .has_scheme()
860 {
861 let resolved = resolve_uri_reference(None, &reference.text)?;
862 let resource = self
863 .uri_index
864 .get(resolved.resource.as_ref())
865 .copied()
866 .ok_or_else(|| {
867 resource_error(
868 ErrorCode::ReferenceResolution,
869 "schema resource is not registered",
870 Some(reference.text.to_string()),
871 )
872 })?;
873 (resource, resolved.fragment)
874 } else {
875 (location.resource, same_document_fragment(&reference.text)?)
876 };
877 let target = self
878 .resources
879 .get(usize::try_from(target_resource.0).map_err(|_| limit_overflow())?)
880 .ok_or_else(|| malformed("resolved resource ID is invalid"))?;
881 let target = match fragment.as_deref() {
882 None | Some("") => FragmentTarget::ResourceRoot(target.root),
883 Some(pointer) if pointer.starts_with('/') => {
884 validate_json_pointer(pointer)?;
885 let location = target.locations.get(pointer).copied().ok_or_else(|| {
886 resource_error(
887 ErrorCode::ReferenceResolution,
888 "JSON Pointer does not identify a schema location",
889 Some(reference.text.to_string()),
890 )
891 })?;
892 FragmentTarget::JsonPointer(location)
893 }
894 Some(anchor) => {
895 if !valid_anchor(anchor) {
896 return Err(resource_error(
897 ErrorCode::ReferenceResolution,
898 "plain-name anchor is not registered in the resource",
899 Some(reference.text.to_string()),
900 ));
901 }
902 let anchor = target.anchor_names.get(anchor).copied().ok_or_else(|| {
903 resource_error(
904 ErrorCode::ReferenceResolution,
905 "plain-name anchor is not registered in the resource",
906 Some(reference.text.to_string()),
907 )
908 })?;
909 FragmentTarget::Anchor(anchor)
910 }
911 };
912 Ok(ResolvedReference {
913 resource: target_resource,
914 target,
915 })
916 })();
917 let resolved = result
918 .map_err(|error: CompileError| error.with_pointer(pointer).with_keyword(keyword))?;
919 let resolved_index =
920 u32::try_from(self.resolved_references.len()).map_err(|_| limit_overflow())?;
921 self.resolved_references.push(ResolvedReferenceRecord {
922 source: reference.location,
923 kind: reference.kind,
924 resolved,
925 });
926 let location_index =
927 usize::try_from(reference.location.0).map_err(|_| limit_overflow())?;
928 let slot = self
929 .reference_index
930 .get_mut(location_index)
931 .ok_or_else(|| malformed("reference location index is invalid"))?;
932 if slot[reference.kind.index()] != u32::MAX {
933 return Err(malformed(
934 "duplicate reference occurrence at one schema location",
935 ));
936 }
937 slot[reference.kind.index()] = resolved_index;
938 }
939 self.observe_build_memory(0)?;
940 Ok(())
941 }
942
943 fn discover_document(&mut self, document: DocumentId) -> Result<(), CompileError> {
944 let index = usize::try_from(document.0).map_err(|_| limit_overflow())?;
945 let retrieval_uri = self
946 .documents
947 .get(index)
948 .ok_or_else(|| malformed("document ID is invalid"))?
949 .retrieval_uri
950 .clone();
951 let ast = {
952 let parsed = self
953 .documents
954 .get_mut(index)
955 .ok_or_else(|| malformed("document ID is invalid"))?;
956 std::mem::replace(&mut parsed.ast, Ast::Bool(false))
957 };
958 let result = (|| {
959 let legacy_ids = object_string(&ast, "$schema")?.is_some_and(is_legacy_metaschema);
960 let root_id = object_string(&ast, if legacy_ids { "id" } else { "$id" })?;
961 let root_uri = match (root_id, legacy_ids) {
962 (Some(id), true) => {
963 Some(resolve_uri_reference(retrieval_uri.as_deref(), id)?.resource)
964 }
965 (Some(id), false) => Some(Arc::<str>::from(resolve_resource_id(
966 retrieval_uri.as_deref(),
967 id,
968 )?)),
969 (None, _) => retrieval_uri.clone(),
970 };
971 let resource = self.push_resource(document, root_uri.clone(), 0)?;
972 if let Some(retrieval) = &retrieval_uri {
973 if root_uri.as_deref() != Some(retrieval.as_ref()) {
974 self.push_uri_alias(retrieval.clone(), resource)?;
975 }
976 }
977 let mut stack = Vec::new();
978 stack.try_reserve(1).map_err(|_| limit_overflow())?;
979 stack.push(WorkItem {
980 schema: &ast,
981 document_pointer: String::new(),
982 resource_pointer: String::new(),
983 base_uri: root_uri.or(retrieval_uri),
984 resource,
985 resource_depth: 0,
986 root: true,
987 legacy_ids,
988 });
989 self.discover_stack(document, stack)
990 })();
991 let parsed = self
992 .documents
993 .get_mut(index)
994 .ok_or_else(|| malformed("document ID is invalid"))?;
995 parsed.ast = ast;
996 result
997 }
998
999 fn discover_stack(
1000 &mut self,
1001 document: DocumentId,
1002 mut stack: Vec<WorkItem<'_>>,
1003 ) -> Result<(), CompileError> {
1004 while let Some(item) = stack.pop() {
1005 self.bump_work(1)?;
1006 let mut active_resource = item.resource;
1007 let mut active_base = item.base_uri;
1008 let mut resource_pointer = item.resource_pointer;
1009 let mut resource_depth = item.resource_depth;
1010 if !item.root {
1011 let id_keyword = if item.legacy_ids { "id" } else { "$id" };
1012 if let Some(id) = object_string(item.schema, id_keyword)? {
1013 let legacy = item
1014 .legacy_ids
1015 .then(|| resolve_uri_reference(active_base.as_deref(), id))
1016 .transpose()?;
1017 if let Some(resolved) = legacy.filter(|value| {
1018 value
1019 .fragment
1020 .as_deref()
1021 .is_some_and(|fragment| !fragment.is_empty())
1022 }) {
1023 let uri = resolved.resource;
1024 self.push_uri_alias(uri.clone(), active_resource)?;
1025 active_base = Some(uri);
1026 } else {
1027 resource_depth =
1028 resource_depth.checked_add(1).ok_or_else(limit_overflow)?;
1029 check_limit(
1030 LimitKind::ResourceDepth,
1031 usize::try_from(resource_depth).ok(),
1032 usize::try_from(self.limits.max_resource_depth)
1033 .map_err(|_| limit_overflow())?,
1034 )?;
1035 let canonical = resolve_resource_id(active_base.as_deref(), id)?;
1036 let uri = Arc::<str>::from(canonical);
1037 active_resource =
1038 self.push_resource(document, Some(uri.clone()), resource_depth)?;
1039 active_base = Some(uri);
1040 resource_pointer.clear();
1041 }
1042 }
1043 }
1044 if let Some(existing) = self
1045 .documents
1046 .get(document.0 as usize)
1047 .and_then(|entry| entry.locations.get(item.document_pointer.as_str()))
1048 .and_then(|id| self.locations.get(id.get() as usize))
1049 {
1050 if existing.resource == active_resource {
1051 continue;
1052 }
1053 }
1054 let location = self.push_location(
1055 document,
1056 active_resource,
1057 &item.document_pointer,
1058 &resource_pointer,
1059 active_base.clone(),
1060 )?;
1061 if resource_pointer.is_empty() {
1062 let resource_index =
1063 usize::try_from(active_resource.0).map_err(|_| limit_overflow())?;
1064 let entry = self
1065 .resources
1066 .get_mut(resource_index)
1067 .ok_or_else(|| malformed("resource ID is invalid"))?;
1068 entry.root = location;
1069 }
1070 self.index_keywords(
1071 item.schema,
1072 location,
1073 active_resource,
1074 active_base.clone(),
1075 item.legacy_ids,
1076 )?;
1077 let placement = ChildPlacement {
1078 base_uri: active_base,
1079 resource: active_resource,
1080 resource_depth,
1081 legacy_ids: item.legacy_ids,
1082 };
1083 push_schema_children(
1084 item.schema,
1085 &item.document_pointer,
1086 &resource_pointer,
1087 &placement,
1088 &mut stack,
1089 )?;
1090 self.observe_build_memory(work_stack_bytes(&stack)?)?;
1091 }
1092 Ok(())
1093 }
1094
1095 fn materialize_pointer_targets(&mut self) -> Result<(), CompileError> {
1096 let mut index = 0usize;
1097 while index < self.references.len() {
1098 let reference = self
1099 .references
1100 .get(index)
1101 .ok_or_else(|| malformed("reference index is invalid"))?;
1102 let source = self
1103 .locations
1104 .get(reference.location.get() as usize)
1105 .ok_or_else(|| malformed("reference location is invalid"))?;
1106 let source_resource = source.resource;
1107 let text = reference.text.clone();
1108 let base = reference.base_uri.clone();
1109 index = index.checked_add(1).ok_or_else(limit_overflow)?;
1110 let (resource, fragment) = if let Some(base) = base.as_deref() {
1111 let resolved = resolve_uri_reference(Some(base), &text)?;
1112 let Some(resource) = self.uri_index.get(resolved.resource.as_ref()).copied() else {
1113 continue;
1114 };
1115 (resource, resolved.fragment)
1116 } else if UriRef::parse(text.as_ref())
1117 .map_err(|_| resource_error(ErrorCode::InvalidUri, "invalid URI reference", None))?
1118 .has_scheme()
1119 {
1120 let resolved = resolve_uri_reference(None, &text)?;
1121 let Some(resource) = self.uri_index.get(resolved.resource.as_ref()).copied() else {
1122 continue;
1123 };
1124 (resource, resolved.fragment)
1125 } else {
1126 (source_resource, same_document_fragment(&text)?)
1127 };
1128 let Some(pointer) = fragment.as_deref().filter(|value| value.starts_with('/')) else {
1129 continue;
1130 };
1131 validate_json_pointer(pointer)?;
1132 if self
1133 .resources
1134 .get(resource.get() as usize)
1135 .is_some_and(|entry| entry.locations.contains_key(pointer))
1136 {
1137 continue;
1138 }
1139 self.materialize_pointer_schema(resource, pointer)?;
1140 }
1141 Ok(())
1142 }
1143
1144 fn materialize_pointer_schema(
1145 &mut self,
1146 resource: ResourceId,
1147 pointer: &str,
1148 ) -> Result<(), CompileError> {
1149 let resource_record = self
1150 .resources
1151 .get(resource.get() as usize)
1152 .ok_or_else(|| malformed("resource ID is invalid"))?;
1153 let root = self
1154 .locations
1155 .get(resource_record.root.get() as usize)
1156 .ok_or_else(|| malformed("resource root is invalid"))?;
1157 let document = root.document;
1158 let document_pointer = if root.document_pointer.is_empty() {
1159 pointer.to_owned()
1160 } else {
1161 format!("{}{pointer}", root.document_pointer)
1162 };
1163 let base_uri = resource_record.canonical_uri.clone();
1164 let document_index = document.0 as usize;
1165 if let Some(existing) = self
1166 .documents
1167 .get(document_index)
1168 .and_then(|entry| entry.locations.get(document_pointer.as_str()))
1169 .copied()
1170 {
1171 return self.push_location_alias(resource, pointer, existing);
1172 }
1173 let ast = {
1174 let parsed = self
1175 .documents
1176 .get_mut(document_index)
1177 .ok_or_else(|| malformed("document ID is invalid"))?;
1178 std::mem::replace(&mut parsed.ast, Ast::Bool(false))
1179 };
1180 let legacy_ids = object_string(&ast, "$schema")?.is_some_and(is_legacy_metaschema);
1181 let result = (|| match ast_at_pointer(&ast, &document_pointer) {
1182 Ok(schema) if matches!(schema, Ast::Obj(_) | Ast::Bool(_)) => {
1183 let mut stack = Vec::new();
1184 stack.try_reserve_exact(1).map_err(|_| limit_overflow())?;
1185 stack.push(WorkItem {
1186 schema,
1187 document_pointer,
1188 resource_pointer: pointer.to_owned(),
1189 base_uri,
1190 resource,
1191 resource_depth: 0,
1192 root: true,
1193 legacy_ids,
1194 });
1195 self.discover_stack(document, stack)
1196 }
1197 Ok(_) | Err(_) => Ok(()),
1198 })();
1199 self.documents
1200 .get_mut(document_index)
1201 .ok_or_else(|| malformed("document ID is invalid"))?
1202 .ast = ast;
1203 result
1204 }
1205
1206 fn push_resource(
1207 &mut self,
1208 document: DocumentId,
1209 canonical_uri: Option<Arc<str>>,
1210 _depth: u32,
1211 ) -> Result<ResourceId, CompileError> {
1212 check_limit(
1213 LimitKind::ResourceCount,
1214 self.resources.len().checked_add(1),
1215 self.limits.max_resources,
1216 )?;
1217 let id = ResourceId(u32::try_from(self.resources.len()).map_err(|_| limit_overflow())?);
1218 if let Some(uri) = &canonical_uri {
1219 if self.uri_index.contains_key(uri.as_ref()) {
1220 return Err(resource_error(
1221 ErrorCode::DuplicateResource,
1222 "duplicate canonical schema-resource URI",
1223 Some(uri.to_string()),
1224 ));
1225 }
1226 check_limit(
1227 LimitKind::UriBytes,
1228 Some(uri.len()),
1229 self.limits.max_uri_bytes,
1230 )?;
1231 if self.counted_uris.insert(uri.clone()) {
1232 self.total_uri_bytes = self
1233 .total_uri_bytes
1234 .checked_add(uri.len())
1235 .ok_or_else(limit_overflow)?;
1236 check_limit(
1237 LimitKind::UriBytes,
1238 Some(self.total_uri_bytes),
1239 self.limits.max_total_uri_bytes,
1240 )?;
1241 }
1242 self.uri_index
1243 .try_reserve(1)
1244 .map_err(|_| limit_overflow())?;
1245 self.uri_index.insert(uri.clone(), id);
1246 }
1247 self.charge(size_of::<MutableResource>())?;
1248 self.resources
1249 .try_reserve(1)
1250 .map_err(|_| limit_overflow())?;
1251 self.resources.push(MutableResource {
1252 canonical_uri,
1253 document,
1254 root: SchemaLocationId(0),
1255 static_anchors: Vec::new(),
1256 dynamic_anchors: Vec::new(),
1257 locations: FxHashMap::default(),
1258 anchor_names: FxHashMap::default(),
1259 });
1260 self.observe_build_memory(0)?;
1261 Ok(id)
1262 }
1263
1264 fn push_uri_alias(&mut self, uri: Arc<str>, resource: ResourceId) -> Result<(), CompileError> {
1265 if let Some(existing) = self.uri_index.get(uri.as_ref()) {
1266 if *existing == resource {
1267 return Ok(());
1268 }
1269 return Err(resource_error(
1270 ErrorCode::DuplicateResource,
1271 "retrieval URI collides with another schema resource",
1272 Some(uri.to_string()),
1273 ));
1274 }
1275 self.uri_index
1276 .try_reserve(1)
1277 .map_err(|_| limit_overflow())?;
1278 self.uri_index.insert(uri, resource);
1279 self.observe_build_memory(0)?;
1280 Ok(())
1281 }
1282
1283 fn push_location(
1284 &mut self,
1285 document: DocumentId,
1286 resource: ResourceId,
1287 document_pointer: &str,
1288 resource_pointer: &str,
1289 base_uri: Option<Arc<str>>,
1290 ) -> Result<SchemaLocationId, CompileError> {
1291 check_limit(
1292 LimitKind::SchemaLocationCount,
1293 self.locations.len().checked_add(1),
1294 self.limits.max_schema_locations,
1295 )?;
1296 let bytes = size_of::<SchemaLocation>()
1297 .checked_add(document_pointer.len())
1298 .and_then(|value| value.checked_add(resource_pointer.len()))
1299 .ok_or_else(limit_overflow)?;
1300 self.charge(bytes)?;
1301 let id =
1302 SchemaLocationId(u32::try_from(self.locations.len()).map_err(|_| limit_overflow())?);
1303 self.locations
1304 .try_reserve(1)
1305 .map_err(|_| limit_overflow())?;
1306 let document_index = usize::try_from(document.0).map_err(|_| limit_overflow())?;
1307 let parsed_document = self
1308 .documents
1309 .get_mut(document_index)
1310 .ok_or_else(|| malformed("document ID is invalid"))?;
1311 parsed_document
1312 .locations
1313 .try_reserve(1)
1314 .map_err(|_| limit_overflow())?;
1315 if parsed_document
1316 .locations
1317 .insert(document_pointer.into(), id)
1318 .is_some()
1319 {
1320 return Err(malformed("duplicate physical schema location"));
1321 }
1322 let index_key: Box<str> = resource_pointer.into();
1323 let resource_index = usize::try_from(resource.0).map_err(|_| limit_overflow())?;
1324 let resource_record = self
1325 .resources
1326 .get_mut(resource_index)
1327 .ok_or_else(|| malformed("resource ID is invalid"))?;
1328 resource_record
1329 .locations
1330 .try_reserve(1)
1331 .map_err(|_| limit_overflow())?;
1332 match resource_record.locations.entry(index_key) {
1333 std::collections::hash_map::Entry::Occupied(_) => {
1334 return Err(malformed("duplicate schema location inside one resource"));
1335 }
1336 std::collections::hash_map::Entry::Vacant(entry) => {
1337 entry.insert(id);
1338 }
1339 }
1340 self.locations.push(SchemaLocation {
1341 document,
1342 resource,
1343 document_pointer: document_pointer.into(),
1344 base_uri,
1345 });
1346 self.observe_build_memory(0)?;
1347 Ok(id)
1348 }
1349
1350 fn push_location_alias(
1351 &mut self,
1352 resource: ResourceId,
1353 pointer: &str,
1354 location: SchemaLocationId,
1355 ) -> Result<(), CompileError> {
1356 self.charge(
1357 pointer
1358 .len()
1359 .checked_add(size_of::<(Box<str>, SchemaLocationId)>())
1360 .ok_or_else(limit_overflow)?,
1361 )?;
1362 let record = self
1363 .resources
1364 .get_mut(resource.get() as usize)
1365 .ok_or_else(|| malformed("resource ID is invalid"))?;
1366 record
1367 .locations
1368 .try_reserve(1)
1369 .map_err(|_| limit_overflow())?;
1370 match record.locations.entry(pointer.into()) {
1371 std::collections::hash_map::Entry::Occupied(entry) if *entry.get() == location => {}
1372 std::collections::hash_map::Entry::Occupied(_) => {
1373 return Err(malformed("duplicate schema location inside one resource"));
1374 }
1375 std::collections::hash_map::Entry::Vacant(entry) => {
1376 entry.insert(location);
1377 }
1378 }
1379 self.observe_build_memory(0)
1380 }
1381
1382 fn index_keywords(
1383 &mut self,
1384 schema: &Ast,
1385 location: SchemaLocationId,
1386 resource: ResourceId,
1387 base_uri: Option<Arc<str>>,
1388 legacy_ids: bool,
1389 ) -> Result<(), CompileError> {
1390 if let Some(anchor) = object_string(schema, "$anchor")? {
1391 self.push_anchor(resource, location, anchor, false)?;
1392 }
1393 if let Some(anchor) = object_string(schema, "$dynamicAnchor")? {
1394 self.push_anchor(resource, location, anchor, true)?;
1395 }
1396 if legacy_ids {
1397 if let Some(id) = object_string(schema, "id")? {
1398 let resolved = resolve_uri_reference(base_uri.as_deref(), id)?;
1399 if let Some(anchor) = resolved
1400 .fragment
1401 .as_deref()
1402 .filter(|name| valid_anchor(name))
1403 {
1404 self.push_anchor(resource, location, anchor, false)?;
1405 }
1406 }
1407 }
1408 for (keyword, kind) in [
1409 ("$ref", ReferenceKind::Static),
1410 ("$dynamicRef", ReferenceKind::Dynamic),
1411 ] {
1412 if let Some(reference) = object_string(schema, keyword)? {
1413 check_limit(
1414 LimitKind::ReferenceCount,
1415 self.references.len().checked_add(1),
1416 self.limits.max_references,
1417 )?;
1418 check_limit(
1419 LimitKind::UriBytes,
1420 Some(reference.len()),
1421 self.limits.max_uri_bytes,
1422 )?;
1423 UriRef::parse(reference).map_err(|_| {
1424 resource_error(
1425 ErrorCode::InvalidUri,
1426 "invalid URI reference",
1427 Some(reference.to_owned()),
1428 )
1429 .with_keyword(keyword)
1430 })?;
1431 self.total_uri_bytes = self
1432 .total_uri_bytes
1433 .checked_add(reference.len())
1434 .ok_or_else(limit_overflow)?;
1435 check_limit(
1436 LimitKind::UriBytes,
1437 Some(self.total_uri_bytes),
1438 self.limits.max_total_uri_bytes,
1439 )?;
1440 self.charge(
1441 size_of::<ReferenceRecord>()
1442 .checked_add(reference.len())
1443 .ok_or_else(limit_overflow)?,
1444 )?;
1445 self.references
1446 .try_reserve(1)
1447 .map_err(|_| limit_overflow())?;
1448 self.references.push(ReferenceRecord {
1449 location,
1450 kind,
1451 text: reference.into(),
1452 base_uri: base_uri.clone(),
1453 });
1454 self.observe_build_memory(0)?;
1455 }
1456 }
1457 Ok(())
1458 }
1459
1460 fn push_anchor(
1461 &mut self,
1462 resource: ResourceId,
1463 target: SchemaLocationId,
1464 name: &str,
1465 dynamic: bool,
1466 ) -> Result<(), CompileError> {
1467 if !valid_anchor(name) {
1468 return Err(resource_error(
1469 ErrorCode::Malformed,
1470 "invalid schema anchor name",
1471 Some(name.to_owned()),
1472 ));
1473 }
1474 let resource_index = usize::try_from(resource.0).map_err(|_| limit_overflow())?;
1475 let anchor_count = {
1476 let entry = self
1477 .resources
1478 .get(resource_index)
1479 .ok_or_else(|| malformed("resource ID is invalid"))?;
1480 entry
1481 .static_anchors
1482 .len()
1483 .checked_add(entry.dynamic_anchors.len())
1484 };
1485 check_limit(
1486 LimitKind::AnchorCount,
1487 anchor_count.and_then(|count| count.checked_add(1)),
1488 self.limits.max_anchors_per_resource,
1489 )?;
1490 check_limit(
1491 LimitKind::AnchorCount,
1492 self.anchors.len().checked_add(1),
1493 self.limits.max_total_anchors,
1494 )?;
1495 if self
1496 .resources
1497 .get(resource_index)
1498 .ok_or_else(|| malformed("resource ID is invalid"))?
1499 .anchor_names
1500 .contains_key(name)
1501 {
1502 return Err(resource_error(
1503 ErrorCode::DuplicateAnchor,
1504 "duplicate static or dynamic anchor in one resource",
1505 Some(name.to_owned()),
1506 ));
1507 }
1508 self.charge(
1509 size_of::<AnchorRecord>()
1510 .checked_add(name.len())
1511 .ok_or_else(limit_overflow)?,
1512 )?;
1513 let id = AnchorId(u32::try_from(self.anchors.len()).map_err(|_| limit_overflow())?);
1514 self.anchors.try_reserve(1).map_err(|_| limit_overflow())?;
1515 self.anchors.push(AnchorRecord {
1516 resource,
1517 name: name.into(),
1518 target,
1519 dynamic,
1520 });
1521 let entry = self
1522 .resources
1523 .get_mut(resource_index)
1524 .ok_or_else(|| malformed("resource ID is invalid"))?;
1525 let run = if dynamic {
1526 &mut entry.dynamic_anchors
1527 } else {
1528 &mut entry.static_anchors
1529 };
1530 run.try_reserve(1).map_err(|_| limit_overflow())?;
1531 run.push(id);
1532 entry
1533 .anchor_names
1534 .try_reserve(1)
1535 .map_err(|_| limit_overflow())?;
1536 entry.anchor_names.insert(name.into(), id);
1537 self.observe_build_memory(0)?;
1538 Ok(())
1539 }
1540
1541 fn bump_work(&mut self, amount: usize) -> Result<(), CompileError> {
1542 self.work = self.work.checked_add(amount).ok_or_else(limit_overflow)?;
1543 check_limit(
1544 LimitKind::ResolutionWork,
1545 Some(self.work),
1546 self.limits.max_resolution_work,
1547 )
1548 }
1549
1550 fn charge(&mut self, bytes: usize) -> Result<(), CompileError> {
1551 let projected = self
1552 .memory
1553 .current_build
1554 .checked_add(bytes)
1555 .ok_or_else(limit_overflow)?;
1556 check_limit(
1557 LimitKind::ResourceBuildBytes,
1558 Some(projected),
1559 self.limits.max_peak_build_bytes,
1560 )
1561 }
1562
1563 fn observe_build_memory(&mut self, temporary: usize) -> Result<(), CompileError> {
1564 let retained = mutable_builder_bytes(self)?;
1565 self.memory
1566 .observe_build(retained, temporary, self.limits.max_peak_build_bytes)
1567 }
1568}
1569
1570fn vec_bytes<T>(values: &Vec<T>) -> Result<usize, CompileError> {
1571 values
1572 .capacity()
1573 .checked_mul(size_of::<T>())
1574 .ok_or_else(limit_overflow)
1575}
1576
1577fn registry_build_bytes(registry: &SchemaRegistry) -> Result<usize, CompileError> {
1578 let mut document_strings = 0usize;
1579 for document in ®istry.documents {
1580 document_strings = checked_sum([
1581 document_strings,
1582 arc_str_bytes(document.text.len())?,
1583 document
1584 .retrieval_uri
1585 .as_deref()
1586 .map_or(Ok(0), |uri| arc_str_bytes(uri.len()))?,
1587 ])?;
1588 }
1589 checked_sum([
1590 vec_bytes(®istry.documents)?,
1591 document_strings,
1592 map_bytes(®istry.retrieval_index)?,
1593 arc_str_map_keys(®istry.retrieval_index)?,
1594 ])
1595}
1596
1597fn slice_bytes<T>(values: &[T]) -> Result<usize, CompileError> {
1598 values
1599 .len()
1600 .checked_mul(size_of::<T>())
1601 .ok_or_else(limit_overflow)
1602}
1603
1604fn map_bytes<K, V>(map: &FxHashMap<K, V>) -> Result<usize, CompileError> {
1605 map.capacity()
1606 .checked_mul(2)
1607 .and_then(|buckets| buckets.checked_mul(size_of::<(K, V)>().checked_add(1)?))
1608 .and_then(|bytes| bytes.checked_add(16))
1609 .ok_or_else(limit_overflow)
1610}
1611
1612fn set_bytes<T>(set: &FxHashSet<T>) -> Result<usize, CompileError> {
1613 set.capacity()
1614 .checked_mul(2)
1615 .and_then(|buckets| buckets.checked_mul(size_of::<T>().checked_add(1)?))
1616 .and_then(|bytes| bytes.checked_add(16))
1617 .ok_or_else(limit_overflow)
1618}
1619
1620fn box_str_map_keys<V>(map: &FxHashMap<Box<str>, V>) -> Result<usize, CompileError> {
1621 map.keys()
1622 .try_fold(0usize, |total, key| total.checked_add(key.len()))
1623 .ok_or_else(limit_overflow)
1624}
1625
1626fn arc_str_map_keys<V>(map: &FxHashMap<Arc<str>, V>) -> Result<usize, CompileError> {
1627 map.keys().try_fold(0usize, |total, key| {
1628 total
1629 .checked_add(arc_str_bytes(key.len())?)
1630 .ok_or_else(limit_overflow)
1631 })
1632}
1633
1634fn arc_str_set_keys(set: &FxHashSet<Arc<str>>) -> Result<usize, CompileError> {
1635 set.iter().try_fold(0usize, |total, key| {
1636 total
1637 .checked_add(arc_str_bytes(key.len())?)
1638 .ok_or_else(limit_overflow)
1639 })
1640}
1641
1642fn arc_str_bytes(len: usize) -> Result<usize, CompileError> {
1643 len.checked_add(
1644 size_of::<usize>()
1645 .checked_mul(2)
1646 .ok_or_else(limit_overflow)?,
1647 )
1648 .ok_or_else(limit_overflow)
1649}
1650
1651fn work_stack_bytes(stack: &Vec<WorkItem<'_>>) -> Result<usize, CompileError> {
1652 let strings = stack.iter().try_fold(0usize, |total, item| {
1653 total
1654 .checked_add(item.document_pointer.capacity())?
1655 .checked_add(item.resource_pointer.capacity())
1656 });
1657 vec_bytes(stack)?
1658 .checked_add(strings.ok_or_else(limit_overflow)?)
1659 .ok_or_else(limit_overflow)
1660}
1661
1662fn checked_sum(values: impl IntoIterator<Item = usize>) -> Result<usize, CompileError> {
1663 values
1664 .into_iter()
1665 .try_fold(0usize, |total, value| total.checked_add(value))
1666 .ok_or_else(limit_overflow)
1667}
1668
1669fn mutable_builder_bytes(builder: &ResourceBuilder) -> Result<usize, CompileError> {
1670 let mut document_heaps = 0usize;
1671 for document in &builder.documents {
1672 document_heaps = checked_sum([
1673 document_heaps,
1674 arc_str_bytes(document.text.len())?,
1675 document
1676 .retrieval_uri
1677 .as_deref()
1678 .map_or(Ok(0), |uri| arc_str_bytes(uri.len()))?,
1679 document.ast_bytes,
1680 map_bytes(&document.locations)?,
1681 box_str_map_keys(&document.locations)?,
1682 ])?;
1683 }
1684 let mut resource_heaps = 0usize;
1685 for resource in &builder.resources {
1686 resource_heaps = checked_sum([
1687 resource_heaps,
1688 vec_bytes(&resource.static_anchors)?,
1689 vec_bytes(&resource.dynamic_anchors)?,
1690 map_bytes(&resource.locations)?,
1691 box_str_map_keys(&resource.locations)?,
1692 map_bytes(&resource.anchor_names)?,
1693 box_str_map_keys(&resource.anchor_names)?,
1694 resource
1695 .canonical_uri
1696 .as_deref()
1697 .map_or(Ok(0), |uri| arc_str_bytes(uri.len()))?,
1698 ])?;
1699 }
1700 let mut location_strings = 0usize;
1701 for location in &builder.locations {
1702 location_strings = checked_sum([
1703 location_strings,
1704 location.document_pointer.len(),
1705 location
1706 .base_uri
1707 .as_deref()
1708 .map_or(Ok(0), |uri| arc_str_bytes(uri.len()))?,
1709 ])?;
1710 }
1711 let anchor_strings = builder.anchors.iter().try_fold(0usize, |total, anchor| {
1712 total
1713 .checked_add(arc_str_bytes(anchor.name.len())?)
1714 .ok_or_else(limit_overflow)
1715 })?;
1716 let mut reference_strings = 0usize;
1717 for reference in &builder.references {
1718 reference_strings = checked_sum([
1719 reference_strings,
1720 arc_str_bytes(reference.text.len())?,
1721 reference
1722 .base_uri
1723 .as_deref()
1724 .map_or(Ok(0), |uri| arc_str_bytes(uri.len()))?,
1725 ])?;
1726 }
1727 checked_sum([
1728 vec_bytes(&builder.documents)?,
1729 document_heaps,
1730 vec_bytes(&builder.resources)?,
1731 resource_heaps,
1732 vec_bytes(&builder.locations)?,
1733 location_strings,
1734 vec_bytes(&builder.anchors)?,
1735 anchor_strings,
1736 vec_bytes(&builder.references)?,
1737 reference_strings,
1738 vec_bytes(&builder.resolved_references)?,
1739 vec_bytes(&builder.reference_index)?,
1740 map_bytes(&builder.uri_index)?,
1741 arc_str_map_keys(&builder.uri_index)?,
1742 set_bytes(&builder.counted_uris)?,
1743 arc_str_set_keys(&builder.counted_uris)?,
1744 ])
1745}
1746
1747fn frozen_graph_bytes(
1748 documents: &[ParsedDocument],
1749 resources: &[SchemaResource],
1750 locations: &[SchemaLocation],
1751 anchors: &[AnchorRecord],
1752 resolved_references: &[ResolvedReferenceRecord],
1753 reference_index: &[[u32; 2]],
1754 uri_index: &FxHashMap<Arc<str>, ResourceId>,
1755) -> Result<usize, CompileError> {
1756 let mut document_heaps = 0usize;
1757 for document in documents {
1758 document_heaps = checked_sum([
1759 document_heaps,
1760 arc_str_bytes(document.text.len())?,
1761 document
1762 .retrieval_uri
1763 .as_deref()
1764 .map_or(Ok(0), |uri| arc_str_bytes(uri.len()))?,
1765 document.ast_bytes,
1766 map_bytes(&document.locations)?,
1767 ])?;
1768 }
1769 let mut resource_heaps = 0usize;
1770 for resource in resources {
1771 resource_heaps = checked_sum([
1772 resource_heaps,
1773 slice_bytes(&resource.static_anchors)?,
1774 slice_bytes(&resource.dynamic_anchors)?,
1775 map_bytes(&resource.locations)?,
1776 box_str_map_keys(&resource.locations)?,
1777 map_bytes(&resource.anchor_names)?,
1778 box_str_map_keys(&resource.anchor_names)?,
1779 resource
1780 .canonical_uri
1781 .as_deref()
1782 .map_or(Ok(0), |uri| arc_str_bytes(uri.len()))?,
1783 ])?;
1784 }
1785 let mut location_strings = 0usize;
1786 for location in locations {
1787 location_strings = checked_sum([
1788 location_strings,
1789 location.document_pointer.len(),
1790 location
1791 .base_uri
1792 .as_deref()
1793 .map_or(Ok(0), |uri| arc_str_bytes(uri.len()))?,
1794 ])?;
1795 }
1796 let anchor_strings = anchors.iter().try_fold(0usize, |total, anchor| {
1797 total
1798 .checked_add(arc_str_bytes(anchor.name.len())?)
1799 .ok_or_else(limit_overflow)
1800 })?;
1801 checked_sum([
1802 slice_bytes(documents)?,
1803 document_heaps,
1804 slice_bytes(resources)?,
1805 resource_heaps,
1806 slice_bytes(locations)?,
1807 location_strings,
1808 slice_bytes(anchors)?,
1809 anchor_strings,
1810 slice_bytes(resolved_references)?,
1811 slice_bytes(reference_index)?,
1812 map_bytes(uri_index)?,
1813 arc_str_map_keys(uri_index)?,
1814 ])
1815}
1816
1817pub(crate) fn resolve_uri_reference(
1818 base: Option<&str>,
1819 reference: &str,
1820) -> Result<ResolvedUri, CompileError> {
1821 let reference_uri = UriRef::parse(reference).map_err(|_| {
1822 resource_error(
1823 ErrorCode::InvalidUri,
1824 "invalid URI reference",
1825 Some(reference.to_owned()),
1826 )
1827 })?;
1828 let resolved = if let Some(base) = base {
1829 let base_uri = Uri::parse(base).map_err(|_| {
1830 resource_error(
1831 ErrorCode::InvalidUri,
1832 "invalid base URI",
1833 Some(base.to_owned()),
1834 )
1835 })?;
1836 reference_uri.resolve_against(&base_uri).map_err(|_| {
1837 resource_error(
1838 ErrorCode::InvalidUri,
1839 "URI reference cannot be resolved against its base",
1840 Some(reference.to_owned()),
1841 )
1842 })?
1843 } else if reference_uri.has_scheme() {
1844 Uri::parse(reference)
1845 .map_err(|_| {
1846 resource_error(
1847 ErrorCode::InvalidUri,
1848 "invalid absolute URI reference",
1849 Some(reference.to_owned()),
1850 )
1851 })?
1852 .to_owned()
1853 } else {
1854 return Err(resource_error(
1855 ErrorCode::ReferenceResolution,
1856 "relative URI reference has no usable base",
1857 Some(reference.to_owned()),
1858 ));
1859 };
1860 let normalized = resolved.normalize();
1861 let fragment = normalized
1862 .fragment()
1863 .map(|value| {
1864 value
1865 .decode()
1866 .to_string()
1867 .map(|value| value.into_owned())
1868 .map_err(|_| {
1869 resource_error(
1870 ErrorCode::InvalidUri,
1871 "URI fragment is not valid UTF-8 after percent decoding",
1872 Some(reference.to_owned()),
1873 )
1874 })
1875 })
1876 .transpose()?;
1877 Ok(ResolvedUri {
1878 resource: Arc::<str>::from(normalized.strip_fragment().as_str()),
1879 fragment,
1880 })
1881}
1882
1883fn same_document_fragment(reference: &str) -> Result<Option<String>, CompileError> {
1884 let parsed = UriRef::parse(reference).map_err(|_| {
1885 resource_error(
1886 ErrorCode::InvalidUri,
1887 "invalid URI reference",
1888 Some(reference.to_owned()),
1889 )
1890 })?;
1891 if parsed.has_scheme() || !parsed.path().is_empty() || parsed.query().is_some() {
1892 return Err(resource_error(
1893 ErrorCode::ReferenceResolution,
1894 "relative URI reference has no usable base",
1895 Some(reference.to_owned()),
1896 ));
1897 }
1898 parsed
1899 .fragment()
1900 .map(|fragment| {
1901 fragment
1902 .decode()
1903 .to_string()
1904 .map(|value| value.into_owned())
1905 .map_err(|_| {
1906 resource_error(
1907 ErrorCode::InvalidUri,
1908 "URI fragment is not valid UTF-8 after percent decoding",
1909 Some(reference.to_owned()),
1910 )
1911 })
1912 })
1913 .transpose()
1914}
1915
1916fn resolve_resource_id(base: Option<&str>, value: &str) -> Result<String, CompileError> {
1917 let resolved = resolve_uri_reference(base, value)?;
1918 if resolved
1919 .fragment
1920 .as_deref()
1921 .is_some_and(|fragment| !fragment.is_empty())
1922 {
1923 return Err(resource_error(
1924 ErrorCode::InvalidUri,
1925 "$id must not contain a non-empty fragment",
1926 Some(value.to_owned()),
1927 )
1928 .with_keyword("$id"));
1929 }
1930 Ok(resolved.resource.to_string())
1931}
1932
1933fn parse_absolute_resource_uri(value: &str, label: &'static str) -> Result<String, CompileError> {
1934 let uri = Uri::parse(value)
1935 .map_err(|_| resource_error(ErrorCode::InvalidUri, label, Some(value.to_owned())))?;
1936 if uri.fragment().is_some_and(|fragment| !fragment.is_empty()) {
1937 return Err(resource_error(
1938 ErrorCode::InvalidUri,
1939 "retrieval URI must not contain a non-empty fragment",
1940 Some(value.to_owned()),
1941 ));
1942 }
1943 Ok(uri.normalize().strip_fragment().as_str().to_owned())
1944}
1945
1946fn object_string<'a>(schema: &'a Ast, keyword: &str) -> Result<Option<&'a str>, CompileError> {
1947 let Ast::Obj(fields) = schema else {
1948 return Ok(None);
1949 };
1950 let mut found = None;
1951 for (name, value) in fields {
1952 if name != keyword {
1953 continue;
1954 }
1955 if found.is_some() {
1956 return Err(malformed("duplicate schema keyword").with_keyword(keyword));
1957 }
1958 let Ast::Str(value) = value else {
1959 return Err(
1960 malformed("schema identifier, anchor, or reference must be a string")
1961 .with_keyword(keyword),
1962 );
1963 };
1964 found = Some(value.as_str());
1965 }
1966 Ok(found)
1967}
1968
1969fn push_schema_children<'a>(
1970 schema: &'a Ast,
1971 document_pointer: &str,
1972 resource_pointer: &str,
1973 placement: &ChildPlacement,
1974 stack: &mut Vec<WorkItem<'a>>,
1975) -> Result<(), CompileError> {
1976 let Ast::Obj(fields) = schema else {
1977 return Ok(());
1978 };
1979 for (keyword, value) in fields.iter().rev() {
1980 if single_schema_keyword(keyword) {
1981 if matches!(value, Ast::Obj(_) | Ast::Bool(_)) {
1982 push_child(
1983 stack,
1984 value,
1985 document_pointer,
1986 resource_pointer,
1987 keyword,
1988 placement,
1989 )?;
1990 }
1991 } else if map_schema_keyword(keyword) {
1992 if let Ast::Obj(children) = value {
1993 for (name, child) in children.iter().rev() {
1994 if matches!(child, Ast::Obj(_) | Ast::Bool(_)) {
1995 let segment = pointer_pair(keyword, name)?;
1996 push_child(
1997 stack,
1998 child,
1999 document_pointer,
2000 resource_pointer,
2001 &segment,
2002 placement,
2003 )?;
2004 }
2005 }
2006 }
2007 } else if array_schema_keyword(keyword) {
2008 if let Ast::Arr(children) = value {
2009 for (index, child) in children.iter().enumerate().rev() {
2010 if matches!(child, Ast::Obj(_) | Ast::Bool(_)) {
2011 let segment = format!("{keyword}/{index}");
2012 push_child(
2013 stack,
2014 child,
2015 document_pointer,
2016 resource_pointer,
2017 &segment,
2018 placement,
2019 )?;
2020 }
2021 }
2022 }
2023 }
2024 }
2025 Ok(())
2026}
2027
2028struct ChildPlacement {
2029 base_uri: Option<Arc<str>>,
2030 resource: ResourceId,
2031 resource_depth: u32,
2032 legacy_ids: bool,
2033}
2034
2035fn push_child<'a>(
2036 stack: &mut Vec<WorkItem<'a>>,
2037 schema: &'a Ast,
2038 document_pointer: &str,
2039 resource_pointer: &str,
2040 segment: &str,
2041 placement: &ChildPlacement,
2042) -> Result<(), CompileError> {
2043 stack.try_reserve(1).map_err(|_| limit_overflow())?;
2044 stack.push(WorkItem {
2045 schema,
2046 document_pointer: append_pointer(document_pointer, segment)?,
2047 resource_pointer: append_pointer(resource_pointer, segment)?,
2048 base_uri: placement.base_uri.clone(),
2049 resource: placement.resource,
2050 resource_depth: placement.resource_depth,
2051 root: false,
2052 legacy_ids: placement.legacy_ids,
2053 });
2054 Ok(())
2055}
2056
2057fn append_pointer(parent: &str, segment: &str) -> Result<String, CompileError> {
2058 let capacity = parent
2059 .len()
2060 .checked_add(segment.len())
2061 .and_then(|len| len.checked_add(1))
2062 .ok_or_else(limit_overflow)?;
2063 let mut output = new_string_with_capacity(capacity)?;
2064 output.push_str(parent);
2065 output.push('/');
2066 output.push_str(segment);
2067 Ok(output)
2068}
2069
2070#[cfg(test)]
2071fn pointer_segment(value: &str) -> Result<String, CompileError> {
2072 let capacity = pointer_segment_len(value)?;
2073 let mut output = new_string_with_capacity(capacity)?;
2074 push_pointer_segment(&mut output, value);
2075 Ok(output)
2076}
2077
2078fn pointer_pair(first: &str, second: &str) -> Result<String, CompileError> {
2079 let capacity = pointer_segment_len(first)?
2080 .checked_add(pointer_segment_len(second)?)
2081 .and_then(|len| len.checked_add(1))
2082 .ok_or_else(limit_overflow)?;
2083 let mut output = new_string_with_capacity(capacity)?;
2084 push_pointer_segment(&mut output, first);
2085 output.push('/');
2086 push_pointer_segment(&mut output, second);
2087 Ok(output)
2088}
2089
2090fn pointer_segment_len(value: &str) -> Result<usize, CompileError> {
2091 let escapes = value
2092 .bytes()
2093 .filter(|byte| matches!(byte, b'~' | b'/'))
2094 .count();
2095 value.len().checked_add(escapes).ok_or_else(limit_overflow)
2096}
2097
2098fn new_string_with_capacity(capacity: usize) -> Result<String, CompileError> {
2099 let mut output = String::new();
2100 output
2101 .try_reserve_exact(capacity)
2102 .map_err(|_| limit_overflow())?;
2103 Ok(output)
2104}
2105
2106fn push_pointer_segment(output: &mut String, value: &str) {
2107 for ch in value.chars() {
2108 match ch {
2109 '~' => output.push_str("~0"),
2110 '/' => output.push_str("~1"),
2111 _ => output.push(ch),
2112 }
2113 }
2114}
2115
2116fn is_legacy_metaschema(uri: &str) -> bool {
2117 matches!(
2118 uri.trim_end_matches('#'),
2119 "http://json-schema.org/draft-04/schema"
2120 | "http://json-schema.org/draft-06/schema"
2121 | "http://json-schema.org/draft-07/schema"
2122 )
2123}
2124
2125fn single_schema_keyword(keyword: &str) -> bool {
2126 matches!(
2127 keyword,
2128 "additionalProperties"
2129 | "unevaluatedProperties"
2130 | "propertyNames"
2131 | "contains"
2132 | "unevaluatedItems"
2133 | "items"
2134 | "not"
2135 | "if"
2136 | "then"
2137 | "else"
2138 | "contentSchema"
2139 )
2140}
2141
2142fn map_schema_keyword(keyword: &str) -> bool {
2143 matches!(
2144 keyword,
2145 "$defs" | "definitions" | "properties" | "patternProperties" | "dependentSchemas"
2146 )
2147}
2148
2149fn array_schema_keyword(keyword: &str) -> bool {
2150 matches!(keyword, "prefixItems" | "allOf" | "anyOf" | "oneOf")
2151}
2152
2153fn valid_anchor(value: &str) -> bool {
2154 let mut bytes = value.bytes();
2155 matches!(bytes.next(), Some(byte) if byte.is_ascii_alphabetic())
2156 && bytes
2157 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b':' | b'.'))
2158}
2159
2160fn validate_json_pointer(pointer: &str) -> Result<(), CompileError> {
2161 for token in pointer[1..].split('/') {
2162 let mut bytes = token.bytes();
2163 while let Some(byte) = bytes.next() {
2164 if byte == b'~' && !matches!(bytes.next(), Some(b'0' | b'1')) {
2165 return Err(resource_error(
2166 ErrorCode::ReferenceResolution,
2167 "JSON Pointer contains an invalid escape",
2168 Some(pointer.to_owned()),
2169 ));
2170 }
2171 }
2172 }
2173 Ok(())
2174}
2175
2176fn ast_at_pointer<'a>(root: &'a Ast, pointer: &str) -> Result<&'a Ast, CompileError> {
2177 if pointer.is_empty() {
2178 return Ok(root);
2179 }
2180 let Some(path) = pointer.strip_prefix('/') else {
2181 return Err(malformed("document pointer is not absolute"));
2182 };
2183 let mut current = root;
2184 for encoded in path.split('/') {
2185 let token = decode_pointer_token(encoded)?;
2186 current = match current {
2187 Ast::Obj(fields) => fields
2188 .iter()
2189 .find_map(|(name, value)| (name == &token).then_some(value))
2190 .ok_or_else(|| malformed("document pointer object key is missing"))?,
2191 Ast::Arr(items) => {
2192 if token.starts_with('0') && token.len() > 1 {
2193 return Err(malformed("document pointer array index has a leading zero"));
2194 }
2195 let index = token
2196 .parse::<usize>()
2197 .map_err(|_| malformed("document pointer array index is invalid"))?;
2198 items
2199 .get(index)
2200 .ok_or_else(|| malformed("document pointer array index is out of range"))?
2201 }
2202 _ => return Err(malformed("document pointer traverses a scalar")),
2203 };
2204 }
2205 Ok(current)
2206}
2207
2208fn decode_pointer_token(token: &str) -> Result<String, CompileError> {
2209 let mut output = String::new();
2210 output
2211 .try_reserve_exact(token.len())
2212 .map_err(|_| limit_overflow())?;
2213 let mut chars = token.chars();
2214 while let Some(ch) = chars.next() {
2215 if ch != '~' {
2216 output.push(ch);
2217 continue;
2218 }
2219 match chars.next() {
2220 Some('0') => output.push('~'),
2221 Some('1') => output.push('/'),
2222 _ => return Err(malformed("document pointer escape is invalid")),
2223 }
2224 }
2225 Ok(output)
2226}
2227
2228fn check_limit(kind: LimitKind, observed: Option<usize>, limit: usize) -> Result<(), CompileError> {
2229 let observed = observed.ok_or_else(limit_overflow)?;
2230 if observed <= limit {
2231 return Ok(());
2232 }
2233 Err(resource_limit_error(kind, observed, limit))
2234}
2235
2236fn resource_limit_error(kind: LimitKind, observed: usize, limit: usize) -> CompileError {
2237 let mut error = CompileError::new(
2238 ErrorCode::InternalLimitExceeded,
2239 Stage::L1,
2240 "schema resource limit exceeded",
2241 );
2242 error.limit = Some((kind, observed, limit));
2243 error
2244}
2245
2246#[cfg(feature = "bench-internals")]
2247fn elapsed_ns(started: std::time::Instant) -> u64 {
2248 u64::try_from(started.elapsed().as_nanos()).unwrap_or(u64::MAX)
2249}
2250
2251fn limit_overflow() -> CompileError {
2252 CompileError::new(
2253 ErrorCode::InternalLimitExceeded,
2254 Stage::L1,
2255 "schema resource size overflow or allocation failure",
2256 )
2257}
2258
2259fn malformed(message: &'static str) -> CompileError {
2260 CompileError::new(ErrorCode::Malformed, Stage::L1, message)
2261}
2262
2263fn resource_error(
2264 code: ErrorCode,
2265 message: &'static str,
2266 observed: Option<String>,
2267) -> CompileError {
2268 let mut error = CompileError::new(code, Stage::L1, message);
2269 error.observed = observed;
2270 error
2271}
2272
2273#[cfg(test)]
2274mod tests {
2275 use super::*;
2276
2277 #[test]
2278 fn pointer_helpers_escape_in_one_pass_and_preserve_unicode() {
2279 assert_eq!(pointer_segment("a/b~c/λ").unwrap(), "a~1b~0c~1λ");
2280 assert_eq!(
2281 pointer_pair("properties", "a/b~c").unwrap(),
2282 "properties/a~1b~0c"
2283 );
2284 assert_eq!(
2285 append_pointer("/properties", "a~1b").unwrap(),
2286 "/properties/a~1b"
2287 );
2288 let error = new_string_with_capacity(usize::MAX).expect_err("capacity must fail");
2289 assert_eq!(error.code, ErrorCode::InternalLimitExceeded);
2290 }
2291
2292 #[test]
2293 fn uri_resolution_keeps_fragment_kinds_distinct_and_decodes_percent_encoding() {
2294 let pointer = resolve_uri_reference(
2295 Some("https://example.com/schemas/root.json"),
2296 "child.json#/properties/a%7E1b",
2297 )
2298 .expect("pointer");
2299 assert_eq!(
2300 pointer.resource.as_ref(),
2301 "https://example.com/schemas/child.json"
2302 );
2303 assert_eq!(pointer.fragment.as_deref(), Some("/properties/a~1b"));
2304
2305 let anchor =
2306 resolve_uri_reference(Some("https://example.com/root"), "#node").expect("anchor");
2307 assert_eq!(anchor.fragment.as_deref(), Some("node"));
2308 let root = resolve_uri_reference(Some("https://example.com/root"), "#").expect("root");
2309 assert_eq!(root.fragment.as_deref(), Some(""));
2310 }
2311
2312 #[test]
2313 fn discovery_indexes_nested_resources_and_separate_anchor_namespaces() {
2314 let mut registry = SchemaRegistry::default();
2315 registry
2316 .insert(
2317 "https://example.com/root",
2318 r##"{
2319 "$anchor":"root",
2320 "$defs":{
2321 "child":{"$id":"child","$anchor":"same","type":"string"},
2322 "other":{"$id":"other","$anchor":"same","$dynamicAnchor":"dyn"}
2323 }
2324 }"##,
2325 )
2326 .expect("insert");
2327 let graph = registry.discover().expect("discover");
2328 assert_eq!(graph.documents.len(), 1);
2329 assert_eq!(graph.resources.len(), 3);
2330 assert_eq!(graph.anchors.len(), 4);
2331 assert!(graph.uri_index.contains_key("https://example.com/child"));
2332 assert!(graph.uri_index.contains_key("https://example.com/other"));
2333 }
2334
2335 #[test]
2336 fn duplicate_anchor_across_static_and_dynamic_tables_is_rejected() {
2337 let mut registry = SchemaRegistry::default();
2338 registry
2339 .insert(
2340 "https://example.com/root",
2341 r##"{"$anchor":"same","$dynamicAnchor":"same"}"##,
2342 )
2343 .expect("insert");
2344 let error = registry.discover().expect_err("duplicate anchor");
2345 assert_eq!(error.code, ErrorCode::DuplicateAnchor);
2346 }
2347
2348 #[test]
2349 fn fragment_resolution_distinguishes_root_pointer_and_anchor() {
2350 let mut registry = SchemaRegistry::default();
2351 registry
2352 .insert(
2353 "https://example.com/root",
2354 r##"{
2355 "$anchor":"rootAnchor",
2356 "properties":{"a/b~c":{"$anchor":"valueAnchor","type":"string"}}
2357 }"##,
2358 )
2359 .expect("insert");
2360 let graph = registry.discover().expect("discover");
2361 let root = graph
2362 .resolve_reference(Some("https://example.com/root"), "#")
2363 .expect("root");
2364 assert!(matches!(root.target, FragmentTarget::ResourceRoot(_)));
2365 let pointer = graph
2366 .resolve_reference(Some("https://example.com/root"), "#%2Fproperties%2Fa~1b~0c")
2367 .expect("pointer");
2368 assert!(matches!(pointer.target, FragmentTarget::JsonPointer(_)));
2369 let anchor = graph
2370 .resolve_reference(Some("https://example.com/root"), "#valueAnchor")
2371 .expect("anchor");
2372 assert!(matches!(anchor.target, FragmentTarget::Anchor(_)));
2373 let error = graph
2374 .resolve_reference(Some("https://example.com/root"), "#/properties/a~2b")
2375 .expect_err("invalid pointer escape");
2376 assert_eq!(error.code, ErrorCode::ReferenceResolution);
2377 }
2378
2379 #[test]
2380 fn reference_materializes_a_schema_outside_known_keyword_paths() {
2381 let mut registry = SchemaRegistry::default();
2382 registry
2383 .insert(
2384 "https://example.com/root",
2385 r##"{
2386 "$schema":"http://json-schema.org/draft-04/schema#",
2387 "port":{"type":"string"},
2388 "properties":{"port":{"$ref":"#/port"}}
2389 }"##,
2390 )
2391 .expect("insert");
2392 let graph = registry.discover().expect("discover");
2393 let resolved = graph
2394 .resolve_reference(Some("https://example.com/root"), "#/port")
2395 .expect("pointer target");
2396 assert!(matches!(resolved.target, FragmentTarget::JsonPointer(_)));
2397 }
2398
2399 #[test]
2400 fn containing_resource_pointer_aliases_an_embedded_resource_root() {
2401 let mut registry = SchemaRegistry::default();
2402 registry
2403 .insert(
2404 "https://example.com/root",
2405 r##"{
2406 "$defs":{"child":{"$id":"child","type":"string"}},
2407 "properties":{"value":{"$ref":"#/$defs/child"}}
2408 }"##,
2409 )
2410 .expect("insert");
2411 let graph = registry.discover().expect("discover");
2412 let resolved = graph
2413 .resolve_reference(Some("https://example.com/root"), "#/$defs/child")
2414 .expect("embedded root");
2415 assert!(matches!(resolved.target, FragmentTarget::JsonPointer(_)));
2416 assert_eq!(graph.resources.len(), 2);
2417 }
2418
2419 #[test]
2420 fn legacy_fragment_id_is_indexed_as_a_plain_name_anchor() {
2421 let mut registry = SchemaRegistry::default();
2422 registry
2423 .insert(
2424 "https://example.com/root",
2425 r##"{
2426 "$schema":"http://json-schema.org/draft-04/schema#",
2427 "id":"https://example.com/root#root",
2428 "definitions":{"capability":{"id":"https://example.com/root#capability","type":"string"}},
2429 "properties":{"value":{"$ref":"#capability"}}
2430 }"##,
2431 )
2432 .expect("insert");
2433 let graph = registry.discover().expect("discover");
2434 let resolved = graph
2435 .resolve_reference(Some("https://example.com/root"), "#capability")
2436 .expect("legacy anchor");
2437 assert!(matches!(resolved.target, FragmentTarget::Anchor(_)));
2438 let root = graph
2439 .resolve_reference(Some("https://example.com/root"), "#root")
2440 .expect("root anchor");
2441 assert!(matches!(root.target, FragmentTarget::Anchor(_)));
2442 }
2443
2444 #[test]
2445 fn retrieval_alias_and_nested_id_both_identify_the_root_location() {
2446 let mut registry = SchemaRegistry::default();
2447 registry
2448 .insert(
2449 "https://retrieval.example/schema",
2450 r##"{"$id":"https://canonical.example/root","$anchor":"node"}"##,
2451 )
2452 .expect("insert");
2453 let graph = registry.discover().expect("discover");
2454 let retrieval = graph
2455 .resolve_reference(None, "https://retrieval.example/schema")
2456 .expect("retrieval alias");
2457 let canonical = graph
2458 .resolve_reference(None, "https://canonical.example/root")
2459 .expect("canonical identifier");
2460 assert_eq!(retrieval.resource, canonical.resource);
2461 }
2462
2463 #[test]
2464 fn duplicate_canonical_resource_uri_is_rejected() {
2465 let mut registry = SchemaRegistry::default();
2466 registry
2467 .insert(
2468 "https://example.com/a",
2469 r##"{"$id":"https://example.com/shared"}"##,
2470 )
2471 .expect("a");
2472 registry
2473 .insert(
2474 "https://example.com/b",
2475 r##"{"$id":"https://example.com/shared"}"##,
2476 )
2477 .expect("b");
2478 let error = registry.discover().expect_err("duplicate canonical URI");
2479 assert_eq!(error.code, ErrorCode::DuplicateResource);
2480 }
2481
2482 #[test]
2483 fn registry_discovery_order_is_independent_of_insertion_order() {
2484 fn build(reverse: bool) -> Vec<Option<String>> {
2485 let mut registry = SchemaRegistry::default();
2486 let documents = [
2487 ("https://example.com/a", r##"{"$defs":{"x":{"$id":"x"}}}"##),
2488 ("https://example.com/b", "true"),
2489 ];
2490 for index in if reverse { [1, 0] } else { [0, 1] } {
2491 registry
2492 .insert(documents[index].0, documents[index].1)
2493 .expect("insert");
2494 }
2495 registry
2496 .discover()
2497 .expect("discover")
2498 .resources
2499 .iter()
2500 .map(|resource| resource.canonical_uri.as_deref().map(str::to_owned))
2501 .collect()
2502 }
2503 assert_eq!(build(false), build(true));
2504 }
2505
2506 #[test]
2507 fn anonymous_document_needs_a_base_only_for_relative_references() {
2508 let mut registry = SchemaRegistry::default();
2509 registry
2510 .insert_anonymous(r##"{"$ref":"https://example.com/target"}"##)
2511 .expect("anonymous");
2512 registry
2513 .insert("https://example.com/target", "true")
2514 .expect("target");
2515 registry.discover().expect("absolute reference");
2516
2517 let mut registry = SchemaRegistry::default();
2518 registry
2519 .insert_anonymous(r##"{"$ref":"relative"}"##)
2520 .expect("anonymous");
2521 let error = registry
2522 .discover()
2523 .expect_err("relative reference without base");
2524 assert_eq!(error.code, ErrorCode::ReferenceResolution);
2525 }
2526
2527 #[test]
2528 fn document_count_limit_is_exact() {
2529 let limits = SchemaResourceLimits {
2530 max_documents: 1,
2531 ..SchemaResourceLimits::default()
2532 };
2533 let mut registry = SchemaRegistry::new(limits);
2534 registry
2535 .insert("https://example.com/a", "true")
2536 .expect("exact cap");
2537 let error = registry
2538 .insert("https://example.com/b", "true")
2539 .expect_err("cap plus one");
2540 assert_eq!(error.limit, Some((LimitKind::DocumentCount, 2, 1)));
2541 }
2542
2543 fn assert_limit(error: CompileError, kind: LimitKind) {
2544 assert_eq!(error.code, ErrorCode::InternalLimitExceeded);
2545 assert_eq!(error.limit.map(|entry| entry.0), Some(kind));
2546 }
2547
2548 #[test]
2549 fn input_byte_limits_are_exact() {
2550 let limits = SchemaResourceLimits {
2551 max_document_bytes: 4,
2552 max_total_input_bytes: 8,
2553 ..SchemaResourceLimits::default()
2554 };
2555 let mut registry = SchemaRegistry::new(limits);
2556 registry.insert_anonymous("true").expect("per-document cap");
2557 registry.insert_anonymous("null").expect("aggregate cap");
2558
2559 let mut per_document = SchemaRegistry::new(SchemaResourceLimits {
2560 max_document_bytes: 3,
2561 ..limits
2562 });
2563 assert_limit(
2564 per_document
2565 .insert_anonymous("true")
2566 .expect_err("cap plus one"),
2567 LimitKind::InputBytes,
2568 );
2569 let mut aggregate = SchemaRegistry::new(SchemaResourceLimits {
2570 max_total_input_bytes: 7,
2571 ..limits
2572 });
2573 aggregate.insert_anonymous("true").expect("first document");
2574 assert_limit(
2575 aggregate
2576 .insert_anonymous("null")
2577 .expect_err("cap plus one"),
2578 LimitKind::InputBytes,
2579 );
2580 }
2581
2582 #[test]
2583 fn uri_byte_limits_are_exact() {
2584 let uri = "https://e.test/a";
2585 let exact = uri.len();
2586 let mut registry = SchemaRegistry::new(SchemaResourceLimits {
2587 max_uri_bytes: exact,
2588 max_total_uri_bytes: exact,
2589 ..SchemaResourceLimits::default()
2590 });
2591 registry.insert(uri, "true").expect("exact URI cap");
2592 registry.discover().expect("exact aggregate URI cap");
2593
2594 let mut one_short = SchemaRegistry::new(SchemaResourceLimits {
2595 max_uri_bytes: exact - 1,
2596 ..SchemaResourceLimits::default()
2597 });
2598 assert_limit(
2599 one_short.insert(uri, "true").expect_err("URI cap plus one"),
2600 LimitKind::UriBytes,
2601 );
2602 let mut total_short = SchemaRegistry::new(SchemaResourceLimits {
2603 max_total_uri_bytes: exact - 1,
2604 ..SchemaResourceLimits::default()
2605 });
2606 assert_limit(
2607 total_short
2608 .insert(uri, "true")
2609 .expect_err("total URI cap plus one"),
2610 LimitKind::UriBytes,
2611 );
2612 }
2613
2614 #[test]
2615 fn resource_and_depth_limits_are_exact() {
2616 let schema = r##"{"$defs":{"x":{"$id":"x"}}}"##;
2617 let mut registry = SchemaRegistry::new(SchemaResourceLimits {
2618 max_resources: 2,
2619 max_resource_depth: 1,
2620 ..SchemaResourceLimits::default()
2621 });
2622 registry
2623 .insert("https://e.test/root", schema)
2624 .expect("insert");
2625 registry.discover().expect("exact resource and depth caps");
2626
2627 for (kind, limits) in [
2628 (
2629 LimitKind::ResourceCount,
2630 SchemaResourceLimits {
2631 max_resources: 1,
2632 ..SchemaResourceLimits::default()
2633 },
2634 ),
2635 (
2636 LimitKind::ResourceDepth,
2637 SchemaResourceLimits {
2638 max_resource_depth: 0,
2639 ..SchemaResourceLimits::default()
2640 },
2641 ),
2642 ] {
2643 let mut registry = SchemaRegistry::new(limits);
2644 registry
2645 .insert("https://e.test/root", schema)
2646 .expect("insert");
2647 assert_limit(registry.discover().expect_err("cap plus one"), kind);
2648 }
2649 }
2650
2651 #[test]
2652 fn anchor_limits_are_exact() {
2653 let one = r##"{"$anchor":"a"}"##;
2654 let two = r##"{"$anchor":"a","$defs":{"x":{"$anchor":"b"}}}"##;
2655 let mut exact = SchemaRegistry::new(SchemaResourceLimits {
2656 max_anchors_per_resource: 2,
2657 max_total_anchors: 2,
2658 ..SchemaResourceLimits::default()
2659 });
2660 exact.insert("https://e.test/root", two).expect("insert");
2661 exact.discover().expect("exact anchor caps");
2662
2663 let mut per_resource = SchemaRegistry::new(SchemaResourceLimits {
2664 max_anchors_per_resource: 1,
2665 ..SchemaResourceLimits::default()
2666 });
2667 per_resource
2668 .insert("https://e.test/root", two)
2669 .expect("insert");
2670 assert_limit(
2671 per_resource
2672 .discover()
2673 .expect_err("per-resource cap plus one"),
2674 LimitKind::AnchorCount,
2675 );
2676
2677 let mut total = SchemaRegistry::new(SchemaResourceLimits {
2678 max_total_anchors: 1,
2679 ..SchemaResourceLimits::default()
2680 });
2681 total.insert("https://e.test/root", one).expect("root");
2682 total.insert("https://e.test/other", one).expect("other");
2683 assert_limit(
2684 total.discover().expect_err("total cap plus one"),
2685 LimitKind::AnchorCount,
2686 );
2687 }
2688
2689 #[test]
2690 fn reference_location_and_work_limits_are_exact() {
2691 let schema = r##"{"$ref":"#","$defs":{"x":true}}"##;
2692 let mut exact = SchemaRegistry::new(SchemaResourceLimits {
2693 max_references: 1,
2694 max_schema_locations: 2,
2695 max_resolution_work: 3,
2696 ..SchemaResourceLimits::default()
2697 });
2698 exact.insert("https://e.test/root", schema).expect("insert");
2699 exact
2700 .discover()
2701 .expect("exact reference/location/work caps");
2702
2703 for (kind, limits) in [
2704 (
2705 LimitKind::ReferenceCount,
2706 SchemaResourceLimits {
2707 max_references: 0,
2708 ..SchemaResourceLimits::default()
2709 },
2710 ),
2711 (
2712 LimitKind::SchemaLocationCount,
2713 SchemaResourceLimits {
2714 max_schema_locations: 1,
2715 ..SchemaResourceLimits::default()
2716 },
2717 ),
2718 (
2719 LimitKind::ResolutionWork,
2720 SchemaResourceLimits {
2721 max_resolution_work: 2,
2722 ..SchemaResourceLimits::default()
2723 },
2724 ),
2725 ] {
2726 let mut registry = SchemaRegistry::new(limits);
2727 registry
2728 .insert("https://e.test/root", schema)
2729 .expect("insert");
2730 assert_limit(registry.discover().expect_err("cap plus one"), kind);
2731 }
2732 }
2733
2734 #[test]
2735 fn retained_graph_limit_is_exact() {
2736 let uri = "https://e.test/root";
2737 let mut probe = SchemaRegistry::default();
2738 probe.insert(uri, "true").expect("insert");
2739 let retained = probe.discover().expect("probe").retained_bytes;
2740 assert!(retained > 0);
2741
2742 let mut exact = SchemaRegistry::new(SchemaResourceLimits {
2743 max_retained_graph_bytes: retained,
2744 ..SchemaResourceLimits::default()
2745 });
2746 exact.insert(uri, "true").expect("insert");
2747 exact.discover().expect("exact retained cap");
2748
2749 let mut one_short = SchemaRegistry::new(SchemaResourceLimits {
2750 max_retained_graph_bytes: retained - 1,
2751 ..SchemaResourceLimits::default()
2752 });
2753 one_short.insert(uri, "true").expect("insert");
2754 assert_limit(
2755 one_short.discover().expect_err("retained cap plus one"),
2756 LimitKind::ResourceGraphBytes,
2757 );
2758 }
2759
2760 #[test]
2761 fn peak_build_limit_is_exact() {
2762 let uri = "https://e.test/root";
2763 let schema = r##"{"$defs":{"x":{"type":"string"}},"$ref":"#/$defs/x"}"##;
2764 let mut probe = SchemaRegistry::default();
2765 probe.insert(uri, schema).expect("insert");
2766 let peak = probe.discover().expect("probe").peak_build_bytes;
2767 assert!(peak > schema.len());
2768
2769 let mut exact = SchemaRegistry::new(SchemaResourceLimits {
2770 max_peak_build_bytes: peak,
2771 ..SchemaResourceLimits::default()
2772 });
2773 exact.insert(uri, schema).expect("insert");
2774 exact.discover().expect("exact peak cap");
2775
2776 let mut one_short = SchemaRegistry::new(SchemaResourceLimits {
2777 max_peak_build_bytes: peak - 1,
2778 ..SchemaResourceLimits::default()
2779 });
2780 one_short.insert(uri, schema).expect("insert");
2781 assert_limit(
2782 one_short.discover().expect_err("peak cap plus one"),
2783 LimitKind::ResourceBuildBytes,
2784 );
2785 }
2786
2787 #[test]
2788 fn invalid_anchor_and_missing_fragment_targets_fail_during_freeze() {
2789 for schema in [
2790 r##"{"$anchor":"1bad"}"##,
2791 r##"{"$ref":"#/missing"}"##,
2792 r##"{"$ref":"#missing"}"##,
2793 r##"{"$ref":"https://missing.test/schema"}"##,
2794 r##"{"$ref":"#%GG"}"##,
2795 ] {
2796 let mut registry = SchemaRegistry::default();
2797 registry
2798 .insert("https://e.test/root", schema)
2799 .expect("insert");
2800 assert!(
2801 registry.discover().is_err(),
2802 "schema unexpectedly froze: {schema}"
2803 );
2804 }
2805 }
2806
2807 #[test]
2808 fn unresolved_references_report_keyword_pointer_and_reference_text() {
2809 for (schema, keyword) in [
2810 (r##"{"$ref":"missing.json"}"##, "$ref"),
2811 (r##"{"$dynamicRef":"#missing"}"##, "$dynamicRef"),
2812 ] {
2813 let mut registry = SchemaRegistry::default();
2814 registry
2815 .insert("https://e.test/root", schema)
2816 .expect("insert");
2817 let error = registry.discover().expect_err("unresolved reference");
2818 let expected_pointer = format!("/{keyword}");
2819 assert_eq!(error.code, ErrorCode::ReferenceResolution);
2820 assert_eq!(error.keyword.as_deref(), Some(keyword));
2821 assert_eq!(
2822 error.json_pointer_path.as_deref(),
2823 Some(expected_pointer.as_str())
2824 );
2825 assert!(error
2826 .observed
2827 .as_deref()
2828 .is_some_and(|text| !text.is_empty()));
2829 }
2830 }
2831
2832 #[test]
2833 fn same_anchor_name_in_distinct_resources_is_valid() {
2834 let mut registry = SchemaRegistry::default();
2835 registry
2836 .insert(
2837 "https://e.test/root",
2838 r##"{"$anchor":"same","$defs":{"x":{"$id":"x","$anchor":"same"}}}"##,
2839 )
2840 .expect("insert");
2841 registry.discover().expect("distinct anchor namespaces");
2842 }
2843}