Reject top level declarations that omit a name attribute. Disallow derivation by restriction of xs:anySimpleType inside simpleType definitions. Require restriction facets length, minLength, maxLength, totalDigits, and fractionDigits to restrict or equal the base type facets. Updates the conformance baseline to 383 accepted invalid schemas.
268 lines
15 KiB
Swift
268 lines
15 KiB
Swift
extension PureXML.Schema.XSDParser {
|
|
/// Facet applicability to a simple type's variety (XSD 1.0 Part 2, 4.3): a
|
|
/// `list` may be constrained only by `length`/`minLength`/`maxLength`,
|
|
/// `pattern`, `enumeration`, and `whiteSpace`; a `union` only by `pattern` and
|
|
/// `enumeration`. A value-bound facet (`maxInclusive`, `totalDigits`, ...)
|
|
/// constrains an atomic value space, not a list or union, so restricting a list
|
|
/// by `maxInclusive` was wrongly accepted (the stF families).
|
|
///
|
|
/// The base's variety is resolved from the raw tree, since this runs before the
|
|
/// types are compiled; an atomic, unresolvable, or external base is skipped, so
|
|
/// no valid restriction is rejected.
|
|
static func simpleTypeVarietyFacetErrors(_ schema: XSDTree) -> [String] {
|
|
let bindings = namespaceBindings(schema)
|
|
let target = PureXML.Schema.XSDNode.attribute(schema, "targetNamespace")
|
|
var errors: [String] = []
|
|
forEachSimpleType(schema) { simpleType in
|
|
guard let restriction = PureXML.Schema.XSDNode.firstChild(simpleType, named: "restriction"),
|
|
let variety = restrictionBaseVariety(restriction, schema, bindings, target, [])
|
|
else { return }
|
|
let allowed = variety == "list" ? listFacets : unionFacets
|
|
let used = PureXML.Schema.XSDNode.elementChildren(restriction)
|
|
.compactMap(PureXML.Schema.XSDNode.localName)
|
|
.filter { facetNames.contains($0) }
|
|
for facet in used where !allowed.contains(facet) {
|
|
errors.append("the facet '\(facet)' does not apply to a \(variety) type")
|
|
}
|
|
}
|
|
return errors
|
|
}
|
|
|
|
/// A `default`/`fixed` value on an `element` or `attribute` must be a valid value
|
|
/// of its `type` (Attribute/Element Locally Valid, XSD 1.0); the two are
|
|
/// mutually exclusive (`src-element.1` / `src-attribute.1`: `default` and `fixed`
|
|
/// must not both be present); and an `attribute` carrying a `default` must have
|
|
/// `use="optional"` (`src-attribute.2`). Only a type that resolves to a built-in datatype is
|
|
/// value-checked, since the check runs before the types are compiled; a
|
|
/// `fixed="abc"` on an `xs:integer` was wrongly accepted. A named or inline user
|
|
/// type, or a type in another namespace, is left alone, so no valid value
|
|
/// constraint is rejected.
|
|
static func valueConstraintErrors(_ schema: XSDTree) -> [String] {
|
|
let bindings = namespaceBindings(schema)
|
|
var errors: [String] = []
|
|
forEachValueConstrained(schema) { node in
|
|
// The value constraint attributes are the unprefixed, no-namespace
|
|
// `default`/`fixed` on an XSD-namespace declaration; a foreign same-local
|
|
// attribute is not the value constraint and is not structurally checked.
|
|
if hasBothValueConstraints(node) {
|
|
errors.append("a '\(PureXML.Schema.XSDNode.localName(node) ?? "")' declaration may not have both a 'default' and a 'fixed' value constraint")
|
|
return
|
|
}
|
|
if hasDefaultWithNonOptionalUse(node) {
|
|
errors.append("an 'attribute' with a 'default' value constraint must have use='optional'")
|
|
return
|
|
}
|
|
guard let typeName = PureXML.Schema.XSDNode.attribute(node, "type") else { return }
|
|
let fixed = PureXML.Schema.XSDNode.attribute(node, "fixed")
|
|
guard let value = fixed ?? PureXML.Schema.XSDNode.attribute(node, "default") else { return }
|
|
let (prefix, local) = splitQName(typeName)
|
|
let uri = prefix.map { bindings[$0] } ?? bindings[""]
|
|
guard uri == xsdNamespace, let builtin = PureXML.Schema.BuiltinType(rawValue: local),
|
|
!PureXML.Schema.SimpleType(base: builtin).isValid(value)
|
|
else { return }
|
|
errors.append("the \(fixed != nil ? "fixed" : "default") value '\(value)' is not valid for type '\(typeName)'")
|
|
}
|
|
return errors
|
|
}
|
|
|
|
/// A `default`/`fixed` value on an `element` or `attribute` whose `type` names a
|
|
/// user-declared type in this schema's own target namespace must be a valid value
|
|
/// of that type (Element/Attribute Locally Valid, `e-props-correct.2` /
|
|
/// `a-props-correct.2`). This runs after the named types are compiled, so it
|
|
/// complements the built-in-only structure-time check: a `fixed="false"` on a
|
|
/// type restricting `xs:boolean` by `pattern="true"`, or a `default="Yes"` on a
|
|
/// complex type whose simple content extends `xs:boolean`, is now rejected.
|
|
///
|
|
/// The type name is resolved namespace-aware; only a type in the schema's own
|
|
/// target namespace (looked up in the compiled `types`) is checked. A built-in
|
|
/// (handled at structure time), an imported or otherwise foreign type, or an
|
|
/// inline anonymous type is left alone, so no valid value constraint is rejected.
|
|
/// The compiled `SimpleType` validator is the same one instance validation uses,
|
|
/// so its whitespace and facet handling match.
|
|
static func userTypeValueConstraintErrors(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext, _ types: [String: PureXML.Schema.ElementType]) -> [String] {
|
|
let bindings = context.namespaceBindings
|
|
var errors: [String] = []
|
|
forEachValueConstrained(schema) { node in
|
|
guard node.name?.namespaceURI == xsdNamespace,
|
|
let typeName = unprefixedValue(node, "type")
|
|
else { return }
|
|
let fixed = unprefixedValue(node, "fixed")
|
|
guard let value = fixed ?? unprefixedValue(node, "default") else { return }
|
|
let (prefix, local) = splitQName(typeName)
|
|
let uri = prefix.map { bindings[$0] } ?? bindings[""]
|
|
guard uri == context.targetNamespace, let resolved = types[local],
|
|
let simple = simpleContentType(of: resolved), !simple.isValid(value)
|
|
else { return }
|
|
errors.append("the \(fixed != nil ? "fixed" : "default") value '\(value)' is not valid for type '\(typeName)'")
|
|
}
|
|
return errors
|
|
}
|
|
|
|
/// The simple type a value constraint is validated against: a simple type
|
|
/// directly, or the simple content of a complex type. A complex type with
|
|
/// element-only, mixed, or empty content carries no simple value space here, so
|
|
/// it is not value-checked (a disclosed under-rejection).
|
|
private static func simpleContentType(of type: PureXML.Schema.ElementType) -> PureXML.Schema.SimpleType? {
|
|
switch type {
|
|
case let .simple(simple):
|
|
return simple
|
|
case let .complex(complex):
|
|
if case let .simpleContent(simple) = complex.content { return simple }
|
|
return nil
|
|
case .typeReference:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private static func forEachValueConstrained(_ node: XSDTree, _ visit: (XSDTree) -> Void) {
|
|
let local = PureXML.Schema.XSDNode.localName(node)
|
|
if local == "appinfo" || local == "documentation" { return }
|
|
if local == "element" || local == "attribute" { visit(node) }
|
|
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
|
forEachValueConstrained(child, visit)
|
|
}
|
|
}
|
|
|
|
/// The value of the unprefixed (no-namespace) attribute named `local`, or nil.
|
|
private static func unprefixedValue(_ node: XSDTree, _ local: String) -> String? {
|
|
node.attributes.first { $0.name.prefix == nil && $0.name.localName == local }?.value
|
|
}
|
|
|
|
/// Whether `node` carries the unprefixed (no-namespace) attribute named `local`.
|
|
private static func hasUnprefixedAttribute(_ node: XSDTree, _ local: String) -> Bool {
|
|
unprefixedValue(node, local) != nil
|
|
}
|
|
|
|
/// Whether an XSD-namespace declaration carries both a `default` and a `fixed`
|
|
/// value constraint, which `src-element.1` / `src-attribute.1` forbid.
|
|
private static func hasBothValueConstraints(_ node: XSDTree) -> Bool {
|
|
node.name?.namespaceURI == xsdNamespace
|
|
&& hasUnprefixedAttribute(node, "default")
|
|
&& hasUnprefixedAttribute(node, "fixed")
|
|
}
|
|
|
|
/// Whether an XSD-namespace `attribute` use carries a `default` together with a
|
|
/// `use` other than `optional`, which `src-attribute.2` forbids: a default value
|
|
/// makes the attribute optional, so `use="required"` or `use="prohibited"` is a
|
|
/// contradiction.
|
|
private static func hasDefaultWithNonOptionalUse(_ node: XSDTree) -> Bool {
|
|
guard node.name?.namespaceURI == xsdNamespace,
|
|
PureXML.Schema.XSDNode.localName(node) == "attribute",
|
|
hasUnprefixedAttribute(node, "default"),
|
|
let use = unprefixedValue(node, "use")
|
|
else { return false }
|
|
return use == "required" || use == "prohibited"
|
|
}
|
|
|
|
private static let facetNames: Set<String> = [
|
|
"minExclusive", "minInclusive", "maxExclusive", "maxInclusive", "totalDigits",
|
|
"fractionDigits", "length", "minLength", "maxLength", "enumeration", "whiteSpace", "pattern",
|
|
]
|
|
private static let listFacets: Set<String> = ["length", "minLength", "maxLength", "pattern", "enumeration", "whiteSpace"]
|
|
private static let unionFacets: Set<String> = ["pattern", "enumeration"]
|
|
private static let builtinListTypes: Set<String> = ["IDREFS", "ENTITIES", "NMTOKENS"]
|
|
|
|
private static func forEachSimpleType(_ node: XSDTree, _ visit: (XSDTree) -> Void) {
|
|
let local = PureXML.Schema.XSDNode.localName(node)
|
|
if local == "appinfo" || local == "documentation" { return }
|
|
if local == "simpleType" { visit(node) }
|
|
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
|
forEachSimpleType(child, visit)
|
|
}
|
|
}
|
|
|
|
/// The variety (`"list"`, `"union"`, or nil for atomic/unknown) a `restriction`
|
|
/// derives from: an inline `simpleType` base wins, otherwise the `base` QName.
|
|
private static func restrictionBaseVariety(_ restriction: XSDTree, _ schema: XSDTree, _ bindings: [String: String], _ target: String?, _ visited: Set<String>) -> String? {
|
|
if let inlineBase = PureXML.Schema.XSDNode.firstChild(restriction, named: "simpleType") {
|
|
return simpleTypeVariety(inlineBase, schema, bindings, target, visited)
|
|
}
|
|
guard let base = PureXML.Schema.XSDNode.attribute(restriction, "base") else { return nil }
|
|
return namedTypeVariety(base, schema, bindings, target, visited)
|
|
}
|
|
|
|
private static func simpleTypeVariety(_ simpleType: XSDTree, _ schema: XSDTree, _ bindings: [String: String], _ target: String?, _ visited: Set<String>) -> String? {
|
|
if PureXML.Schema.XSDNode.firstChild(simpleType, named: "list") != nil { return "list" }
|
|
if PureXML.Schema.XSDNode.firstChild(simpleType, named: "union") != nil { return "union" }
|
|
if let restriction = PureXML.Schema.XSDNode.firstChild(simpleType, named: "restriction") {
|
|
return restrictionBaseVariety(restriction, schema, bindings, target, visited)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
/// The variety of a named base, resolved namespace-aware so a same-local-name
|
|
/// collision never flips the variety: a base in the XSD namespace is a built-in
|
|
/// (only the list datatypes are list-variety; the rest atomic, nil); a base in
|
|
/// the schema's own target namespace resolves to a declared global `simpleType`
|
|
/// (cycle-guarded); a base in any other namespace is external and skipped (nil).
|
|
private static func namedTypeVariety(_ base: String, _ schema: XSDTree, _ bindings: [String: String], _ target: String?, _ visited: Set<String>) -> String? {
|
|
let (prefix, local) = splitQName(base)
|
|
let uri = prefix.map { bindings[$0] } ?? bindings[""]
|
|
if uri == xsdNamespace {
|
|
return builtinListTypes.contains(local) ? "list" : nil
|
|
}
|
|
guard uri == target, !visited.contains(local), let declared = globalSimpleType(named: local, schema) else { return nil }
|
|
return simpleTypeVariety(declared, schema, bindings, target, visited.union([local]))
|
|
}
|
|
|
|
private static func splitQName(_ value: String) -> (prefix: String?, local: String) {
|
|
let parts = value.split(separator: ":", maxSplits: 1).map(String.init)
|
|
return parts.count == 2 ? (parts[0], parts[1]) : (nil, value)
|
|
}
|
|
|
|
private static func globalSimpleType(named name: String, _ schema: XSDTree) -> XSDTree? {
|
|
PureXML.Schema.XSDNode.elementChildren(schema).first {
|
|
PureXML.Schema.XSDNode.localName($0) == "simpleType" && PureXML.Schema.XSDNode.attribute($0, "name") == name
|
|
}
|
|
}
|
|
|
|
/// The prefix-to-namespace bindings on the schema element (its `xmlns:prefix`
|
|
/// attributes; the default namespace under the empty key).
|
|
private static func namespaceBindings(_ schema: XSDTree) -> [String: String] {
|
|
var bindings: [String: String] = [:]
|
|
for attribute in schema.attributes {
|
|
if attribute.name.prefix == "xmlns" {
|
|
bindings[attribute.name.localName] = attribute.value
|
|
} else if attribute.name.prefix == nil, attribute.name.localName == "xmlns" {
|
|
bindings[""] = attribute.value
|
|
}
|
|
}
|
|
return bindings
|
|
}
|
|
|
|
/// Reject any restriction (in the XSD namespace) whose `base`
|
|
/// attribute resolves to `xs:anySimpleType` in the XSD namespace when defined directly under a simpleType.
|
|
static func anySimpleTypeRestrictionErrors(_ schema: XSDTree) -> [String] {
|
|
let targetNamespace = schema.attributes.first { $0.name.prefix == nil && $0.name.localName == "targetNamespace" }?.value ?? ""
|
|
if targetNamespace == xsdNamespace {
|
|
return []
|
|
}
|
|
let bindings = namespaceBindings(schema)
|
|
var errors: [String] = []
|
|
forEachRestrictionInSimpleType(schema) { node in
|
|
guard let base = PureXML.Schema.XSDNode.attribute(node, "base") else { return }
|
|
let (prefix, local) = splitQName(base)
|
|
let uri = prefix.map { bindings[$0] } ?? bindings[""]
|
|
if uri == xsdNamespace, local == "anySimpleType" {
|
|
errors.append("derivation by restriction of 'xs:anySimpleType' is not allowed")
|
|
}
|
|
}
|
|
return errors
|
|
}
|
|
|
|
private static func forEachRestrictionInSimpleType(_ node: XSDTree, _ visit: (XSDTree) -> Void) {
|
|
let local = PureXML.Schema.XSDNode.localName(node)
|
|
if local == "appinfo" || local == "documentation" { return }
|
|
if node.name?.namespaceURI == xsdNamespace, local == "simpleType" {
|
|
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
|
if child.name?.namespaceURI == xsdNamespace, PureXML.Schema.XSDNode.localName(child) == "restriction" {
|
|
visit(child)
|
|
}
|
|
}
|
|
}
|
|
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
|
forEachRestrictionInSimpleType(child, visit)
|
|
}
|
|
}
|
|
}
|