///|
priv struct SchemaCheck {
findings : Array[@json.Diagnostic]
owners : Array[@reference.UriReference?]
mut owner : @reference.UriReference?
max_findings : Int32
mut complete : Bool
mut stopped : Bool
mut comparison_work : Int32
}
///|
fn SchemaCheck::emit(self : SchemaCheck, d : @json.Diagnostic) -> Unit {
if self.stopped {
return
}
if d.code() == @json.LimitExceeded {
self.complete = false
}
if self.findings.length() < self.max_findings {
self.findings.push(d)
self.owners.push(self.owner)
} else {
let limit = @json.Diagnostic::new(
@json.LimitExceeded,
"Schema finding limit exceeded",
)
if self.findings.is_empty() {
self.findings.push(limit)
self.owners.push(self.owner)
} else {
self.findings[self.findings.length() - 1] = limit
}
self.complete = false
self.stopped = true
}
}
///|
fn SchemaCheck::exhaust(
self : SchemaCheck,
message : String,
path : SchemaPath,
) -> Unit {
self.emit(
@json.Diagnostic::new(@json.LimitExceeded, message, path=path_tokens(path)),
)
self.complete = false
self.stopped = true
}
///|
fn SchemaCheck::pattern(
self : SchemaCheck,
source : String,
path : SchemaPath,
limits : @json.Limits,
) -> Unit {
match check_pattern(source, limits~) {
Ok(_) => ()
Err(error) => {
self.emit(
@json.Diagnostic::new(
error.code(),
error.message() +
" at decoded-pattern UTF-16 offset " +
error.offset().to_string(),
path=path_tokens(path),
),
)
if error.code() == @json.LimitExceeded {
self.complete = false
}
}
}
}
///|
fn SchemaCheck::enum_unique(
self : SchemaCheck,
items : Array[@json.Value],
path : SchemaPath,
limits : @json.Limits,
) -> Unit {
let sizes = []
for item in items {
match @json.write(item, limits~) {
Ok(text) => sizes.push(text.length())
Err(d) => {
self.emit(with_path(d, path))
self.complete = false
return
}
}
}
for i in 0.. self.comparison_work ||
sizes[j] > self.comparison_work - sizes[i] {
self.exhaust("Schema enum comparison work limit exceeded", path)
return
}
self.comparison_work -= sizes[i] + sizes[j]
match items[i].equals(items[j], numeric=true, limits~) {
Ok(true) => {
self.emit(
invalid(
"Enum values must be mathematically unique",
path=path_tokens(path, suffix=[i.to_string()]),
),
)
break
}
Ok(false) => ()
Err(d) => {
self.emit(with_path(d, path))
self.complete = false
return
}
}
}
if self.stopped {
return
}
}
}
///|
fn retrieval_base_valid(base : @reference.UriReference) -> Bool {
base.is_absolute() && base.fragment() is None
}
///|
fn pointer_key(key : String?, token : String) -> String? {
match key {
Some(key) =>
Some(
key + "/" + token.replace(old="~", new="~0").replace(old="/", new="~1"),
)
None => None
}
}
///|
fn check_schema(
value : @json.Value,
root : @json.Value,
path : Array[String],
base : @reference.UriReference?,
resources : @reference.ResourceSet,
max_nodes : Int32,
max_edges : Int32,
max_findings : Int32,
max_comparison_work : Int32,
limits : @json.Limits,
) -> Report {
let state = new_check(max_findings, max_comparison_work, limits)
let mut references_complete = true
let mut initial = RootPath
for token in path {
initial = PathStep(initial, token)
}
if base is Some(base) && !retrieval_base_valid(base) {
state.emit(
@json.Diagnostic::new(
@json.InvalidReference,
"Schema retrieval base must be absolute and fragment-free",
),
)
return {
findings: state.findings,
complete: false,
references_complete: false,
resolutions: [],
owners: state.owners,
}
}
let pointer = match @reference.JsonPointer::from_tokens(path, limits~) {
Ok(pointer) => pointer
Err(d) => {
state.emit(with_path(d, initial))
return {
findings: state.findings,
complete: false,
references_complete: false,
resolutions: [],
owners: state.owners,
}
}
}
// A registered base names the physical document; otherwise `root` is the
// caller's input document under the optional base.
let registered = match base {
Some(base) => resources.get(base)
None => None
}
let index = match registered {
Some(resource) => {
let roots = resource.schema_roots()
if pointer.resolve(resource.value(), limits~) is Ok(_) {
roots.push(pointer)
}
let trusted = @json.Limits::unlimited()
let replaced = @reference.Resource::new(
resource.uri(),
resource.value(),
aliases=resource.aliases(),
schema_roots=roots,
limits=trusted,
)
match replaced {
Ok(replaced) => {
let identity = resource.uri().identity()
let all = resources
.resources()
.map(r => if r.uri().identity() == identity { replaced } else { r })
match
@reference.ResourceSet::new(
all,
root_base=resources.root_base(),
limits=trusted,
) {
Ok(set) => @reference.SchemaIndex::new(set, limits~)
Err(_) => @reference.SchemaIndex::new(resources, limits~)
}
}
Err(_) => @reference.SchemaIndex::new(resources, limits~)
}
}
None =>
@reference.SchemaIndex::new(
resources,
input=Some(root),
input_base=base,
// The submitted path is advisory when it is absent from `root`.
input_roots=if pointer.resolve(root, limits~) is Ok(_) {
[pointer]
} else {
[]
},
limits~,
)
}
for d in index.diagnostics() {
state.emit(d)
}
if !index.complete() {
references_complete = false
}
let document = match registered {
Some(resource) => Some(resource.uri())
None => base
}
walk_schema(
state,
value,
Some(root),
initial,
pointer,
document,
index,
references_complete,
max_nodes,
max_edges,
limits,
)
}
///|
fn new_check(
max_findings : Int32,
max_comparison_work : Int32,
limits : @json.Limits,
) -> SchemaCheck {
{
findings: [],
owners: [],
owner: None,
max_findings: if max_findings < limits.max_findings() {
max_findings
} else {
limits.max_findings()
},
complete: true,
stopped: false,
comparison_work: max_comparison_work,
}
}
///|
fn walk_schema(
state : SchemaCheck,
value : @json.Value,
root : @json.Value?,
initial : SchemaPath,
pointer : @reference.JsonPointer,
document : @reference.UriReference?,
index : @reference.SchemaIndex,
references_complete : Bool,
max_nodes : Int32,
max_edges : Int32,
limits : @json.Limits,
) -> Report {
let resolutions : Array[@reference.Resolution] = []
let mut references_complete = references_complete
let mut root_checked = root is None
if max_nodes < 1 {
state.exhaust("Schema traversal limit exceeded", initial)
}
match @json.write(value, limits~) {
Ok(_) => ()
Err(d) => {
state.emit(with_path(d, initial))
state.complete = false
state.stopped = true
}
}
// The submitted schema keeps the physical identity at `path` only when it is
// exactly the value there; otherwise it is checked as a separate input.
let (scope, key) = match index.locate(document, pointer, limits~) {
Ok(location) if location.target().equals(value, limits~) is Ok(true) =>
(index.scope(location), Some(location.identity()))
_ => (document, None)
}
let stack = [(value, initial, scope, key, false, document)]
state.owner = document
let mut reserved = 1
let mut edges = 0
let visited : Set[String] = Set([])
while !state.stopped &&
stack.pop() is Some((current, path, scope, key, scoped, owner)) {
state.owner = owner
if key is Some(key) && !visited.add_and_check(key) {
continue
}
// An indexed location carries its scope, including its own `$id`;
// otherwise the walker applies the node's `$id` itself.
let (scope, scoped) = match key {
Some(key) =>
match index.indexed_scope(key) {
Some(indexed) => (indexed, true)
None => (scope, scoped)
}
None => (scope, scoped)
}
guard current.kind() is @json.Object(fields) else {
if !schema_kind(current) {
state.emit(
invalid(
"Expected an object or boolean schema",
path=path_tokens(path),
),
)
}
continue
}
if current.get("$schema") is Some(declaration) {
match decode_keyword("$schema", declaration, limits~) {
Ok(Dialect(ref_)) =>
if !draft07(ref_.text()) {
state.emit(
@json.Diagnostic::new(
@json.UnsupportedDialect,
"Only draft-07 is supported",
path=path_tokens(PathStep(path, "$schema")),
),
)
state.complete = false
continue
}
Ok(_) => abort("Dialect decoding invariant")
Err(d) => state.emit(with_path(d, PathStep(path, "$schema")))
}
}
if current.get("$ref") is Some(ref_value) {
let at = PathStep(path, "$ref")
let reference = match decode_keyword("$ref", ref_value, limits~) {
Ok(Ref(ref_)) => ref_
Ok(_) => abort("Reference decoding invariant")
Err(d) => {
state.emit(with_path(d, at))
continue
}
}
if edges >= max_edges {
state.exhaust("Schema reference edge limit exceeded", at)
break
}
edges += 1
if !root_checked && root is Some(root) {
match @json.write(root, limits~) {
Err(d) => {
state.emit(d)
state.complete = false
state.stopped = true
break
}
Ok(_) => root_checked = true
}
}
let resolution = index.resolve(reference, base=scope, limits~)
resolutions.push(resolution)
if !resolution.complete() {
references_complete = false
}
guard resolution.target() is Some(target) else {
for d in resolution.diagnostics() {
state.emit(
@json.Diagnostic::new(d.code(), d.message(), path=path_tokens(at)),
)
}
continue
}
if !schema_kind(target.target()) {
state.emit(
@json.Diagnostic::new(
@json.WrongType,
"Reference target must be an object or boolean schema",
path=path_tokens(at),
),
)
continue
}
if !visited.contains(target.identity()) {
let mut target_path = RootPath
for token in target.pointer().tokens() {
target_path = PathStep(target_path, token)
}
if reserved >= max_nodes {
state.exhaust("Schema traversal limit exceeded", at)
break
}
reserved += 1
stack.push(
(
target.target(),
target_path,
target.resource(),
Some(target.identity()),
false,
target.resource(),
),
)
}
continue
}
let scope = if scoped {
scope
} else {
match current.get("$id") {
Some(id) =>
match decode_keyword("$id", id, limits~) {
Ok(Id(id)) => @reference.schema_scope(id, scope, limits~)
_ => scope
}
None => scope
}
}
fields.sort_by((a, b) => a.0.lexical_compare(b.0))
let children = []
for (name, item) in fields {
if state.stopped {
break
}
if !all_names.contains(name) {
continue
}
let at = PathStep(path, name)
match decode_keyword(name, item, limits~) {
Err(d) => state.emit(with_path(d, at))
Ok(Enum(items)) => state.enum_unique(items, at, limits)
Ok(Pattern(source)) => state.pattern(source, at, limits)
Ok(_) => ()
}
if name == "patternProperties" && item.kind() is @json.Object(entries) {
entries.sort_by((a, b) => a.0.lexical_compare(b.0))
for (pattern, _) in entries {
state.pattern(pattern, PathStep(at, pattern), limits)
}
}
for entry in @domain.draft07_subschemas(name, item) {
if reserved >= max_nodes {
state.exhaust("Schema traversal limit exceeded", at)
break
}
reserved += 1
let mut child_path = at
let mut child_key = pointer_key(key, name)
for token in entry.0 {
child_path = PathStep(child_path, token)
child_key = pointer_key(child_key, token)
}
children.push((entry.1, child_path, scope, child_key, false, owner))
}
}
for i in 0.. Report {
let state = new_check(max_findings, max_comparison_work, limits)
let mut initial = RootPath
for token in path {
initial = PathStep(initial, token)
}
let pointer = match @reference.JsonPointer::from_tokens(path, limits~) {
Ok(pointer) => pointer
Err(d) => {
state.emit(with_path(d, initial))
return {
findings: state.findings,
complete: false,
references_complete: false,
resolutions: [],
owners: state.owners,
}
}
}
walk_schema(
state,
schema.value,
None,
initial,
pointer,
resource,
index,
true,
max_nodes,
max_edges,
limits,
)
}