fix(regex): reject a malformed \p{...} property name in a pattern facet (#145)

A \p{...}/\P{...} property name that can be no XSD charProp under any
repertoire (empty, the bare prefix Is, a non-block name carrying a non-letter,
or Is followed by a non-block character) is now a schema-validity error,
distinct from a well-formed but unrecognised category or block name, which
stays a lenient engine limitation.

XSTS invalid-schemas-accepted 203 to 200, no other bucket moved.
This commit is contained in:
Mihaela Mihaljevic 2026-06-17 14:53:32 +02:00
parent 7e30526b2c
commit 947278bb56
5 changed files with 34 additions and 3 deletions

View File

@ -22,6 +22,8 @@ Second pre-release. Continues the W3C XSTS schema-validity campaign: **invalid-s
### Fixed
- XSTS invalid-schemas-accepted (#145): a `pattern` facet whose `\p{...}` or `\P{...}` property name can be no XSD `charProp` (`IsCategory | IsBlock`) under any repertoire is now rejected as a schema-validity error: an empty name, the bare prefix `Is`, a non-block name carrying a non-letter (`\p{\L}`), or `Is` followed by a non-block character. A well-formed but unrecognised category or block name (such as an unknown `Is`-block) stays a lenient engine limitation rather than an error, so only the unambiguously malformed forms reject. Caught three invalid schemas: invalid-schemas-accepted 203 → 200, no other bucket moved.
- XSTS invalid-schemas-accepted (#145): two attribute-declaration validity rules. An attribute `ref` may not also carry `form` (the form comes from the referenced global declaration; `src-attribute.3.2`), and no attribute declaration may be named `xmlns` (that name is reserved for namespace declarations; no-xmlns). Caught four invalid schemas: invalid-schemas-accepted 207 → 203, no other bucket moved.
- XSTS invalid-schemas-accepted (#145): a referencing `attributeGroup` (one carrying a `ref`) is held to its `(annotation?)` content model. It names a definition elsewhere, so it may not itself declare attributes, nested attributeGroup references, or an `anyAttribute`; only a defining `attributeGroup` (with `name`) carries those (`attgD`). Caught three invalid schemas: invalid-schemas-accepted 210 → 207, no other bucket moved.

View File

@ -133,11 +133,28 @@ extension PureXML.Regex {
guard peek() == "}" else { throw RegexError.badClass }
advance()
guard let predicate = CategoryPredicate(name: name, negated: negated) else {
throw RegexError.unsupported("\\p{\(name)}")
// An unknown name is by default a limit of this engine, not an error
// (stay lenient). But a name that can be NO XSD charProp at all is
// unambiguously invalid and rejects, independent of which categories
// or blocks this engine happens to recognise.
throw Self.isMalformedProperty(name) ? RegexError.invalidProperty : RegexError.unsupported("\\p{\(name)}")
}
return predicate
}
/// Whether `name` can be no XSD `charProp` (`IsCategory | IsBlock`) under any
/// repertoire: empty, the bare `Is` prefix, a non-block name (no `Is` prefix)
/// carrying a non-letter, or `Is` followed by a non-`[A-Za-z0-9-]` character.
/// A well-formed but unrecognised name (an unknown block such as `IsaA0-a9`)
/// is NOT malformed and stays a lenient `unsupported` limit.
private static func isMalformedProperty(_ name: String) -> Bool {
if name.isEmpty || name == "Is" { return true }
guard name.hasPrefix("Is") else {
return name.contains { !$0.isLetter }
}
return name.dropFirst(2).contains { !($0.isLetter || $0.isNumber || $0 == "-") }
}
private func classFrom(_ member: ClassMember) -> CharClass {
switch member {
case let .scalar(value): .single(value)

View File

@ -20,6 +20,12 @@ extension PureXML.Regex {
/// A character class opened with `[` that the expression ends before
/// closing with `]` (`a[`, `[a[:xyz:`). Unambiguously invalid.
case unterminatedClass
/// The `{...}` of a `\p`/`\P` escape whose content can be no XSD charProp:
/// empty, the bare prefix `Is`, a non-block name carrying a character that
/// is not a letter, or `Is` followed by a non-block-name character
/// (`\p{\L}`, `\p{Is}`). Unambiguously invalid, distinct from an unknown but
/// well-formed category or block name (which stays an `unsupported` limit).
case invalidProperty
var description: String {
switch self {
@ -34,6 +40,7 @@ extension PureXML.Regex {
case .reversedQuantifier: "quantifier minimum exceeds maximum"
case .incompleteEscape: "an escape '\\' has no following character"
case .unterminatedClass: "a character class is not closed with ']'"
case .invalidProperty: "a \\p{...} property name is malformed"
}
}
}

View File

@ -80,7 +80,7 @@ extension PureXML.Schema {
private static func rejectsPattern(_ error: PureXML.Regex.RegexError) -> Bool {
switch error {
case .unbalanced, .danglingQuantifier, .reversedRange, .emptyClass, .reversedQuantifier,
.incompleteEscape, .unterminatedClass:
.incompleteEscape, .unterminatedClass, .invalidProperty:
true
case .unsupported, .badQuantifier, .badEscape, .badClass:
false

View File

@ -120,8 +120,13 @@ struct XSTSSuiteTests {
/// Rejecting an attribute `ref` that also carries `form` (src-attribute.3.2) and
/// an attribute declaration named `xmlns` (no-xmlns) then caught four more
/// invalid schemas: invalid-schemas accepted 207 → 203, no other bucket moved.
/// Rejecting a `\p{...}` property whose name can be no XSD charProp under any
/// repertoire (empty, the bare `Is`, a non-block name with a non-letter, or `Is`
/// followed by a non-block character: `\p{\L}`, `\p{Is}`) while staying lenient
/// on a well-formed but unrecognised block name then caught three more invalid
/// schemas: invalid-schemas accepted 203 → 200, no other bucket moved.
private let knownSchemaValidRejected = 1
private let knownSchemaInvalidAccepted = 203
private let knownSchemaInvalidAccepted = 200
private let knownInstanceValidRejected = 3
private let knownInstanceInvalidAccepted = 132