PureXML/Sources/Schema/SchemaWildcardNamespace.swift
Mihaela Mihaljevic e016775a82 feat(schema): validate wildcard namespace constraint value space
An any/anyAttribute namespace constraint is ('##any' | '##other') standing
alone, or a list of namespace URIs and the ##targetNamespace/##local tokens.
The wildcard parser mapped any unrecognised token to a literal URI, so a
misspelled token (##all, ##target), an ##any/##other inside a list, or any
other ##-prefixed token was accepted.

The unprefixed namespace value is now tokenised and checked against that
grammar, with ##any/##other admitted only as the whole value. A prefixed
(foreign) attribute sharing the local name is the author's own and is not
checked, so no valid schema is rejected.

XSTS: invalid schemas accepted 736 to 687, valid schemas rejected held at 72,
instance buckets unchanged.
2026-06-14 11:00:45 +02:00

36 lines
1.8 KiB
Swift

extension PureXML.Schema.XSDParser {
/// Validity of an `any`/`anyAttribute` `namespace` constraint (XSD 1.0 Structures):
///
/// namespace ::= ('##any' | '##other')
/// | List of (anyURI | '##targetNamespace' | '##local')
///
/// So `##any` and `##other` stand alone, and a list admits only namespace URIs
/// and the `##targetNamespace`/`##local` tokens. A misspelled token (`##all`,
/// `##target`) or `##any`/`##other` placed inside a list (`##any ##other`) was
/// accepted, because the wildcard parser mapped any unrecognised token to a
/// literal URI; it is now rejected at compile time.
static func wildcardNamespaceErrors(_ node: XSDTree) -> [String] {
// The wildcard's own constraint is the unprefixed `namespace`; a prefixed
// (foreign) attribute that merely shares the local name is the author's own
// and is not checked, as `attributeApplicabilityErrors` also requires.
guard let raw = node.attributes.first(where: { $0.name.prefix == nil && $0.name.localName == "namespace" })?.value
else { return [] }
return validWildcardNamespace(raw)
? []
: ["the wildcard namespace '\(raw)' is not a valid namespace constraint"]
}
private static func validWildcardNamespace(_ raw: String) -> Bool {
let tokens = raw.split(whereSeparator: \.isWhitespace).map(String.init)
// `##any`/`##other` are valid only as the whole value, never as a list item.
if tokens == ["##any"] || tokens == ["##other"] { return true }
return tokens.allSatisfy { token in
switch token {
case "##targetNamespace", "##local": true
case "##any", "##other": false
default: !token.hasPrefix("##")
}
}
}
}