Skip to main content

maskforge_core/frontend/
resources.rs

1//! Discovers and indexes Draft 2020-12 schema resources.
2
3use 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(&registry)?;
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 &registry.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(&registry.documents)?,
1591        document_strings,
1592        map_bytes(&registry.retrieval_index)?,
1593        arc_str_map_keys(&registry.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}