PureXML/Tests/SchemaPatternFacetTests.swift
Mihaela Mihaljevic 62e53b0fc1 feat(schema): validate pattern facet regular expressions at compile time
Each pattern facet value is now compiled when the schema is, instead of only
lazily at instance time (try?) where an unparseable pattern was silently
ignored. A structurally malformed pattern (unbalanced parentheses, a quantifier
with nothing to repeat) is rejected.

The check is conservative against over-rejection: it rejects only the
unambiguous structural RegexError kinds (.unbalanced, .danglingQuantifier) and
tolerates the kinds that fire on otherwise-valid patterns the engine cannot
fully handle (.empty for the empty pattern, .unsupported for an untabulated
\p{Is...} block such as a surrogate or historic block, .badQuantifier for the
lenient {,m} form, and .badEscape/.badClass which could fire on valid character-
class subtraction). So a{2,1} and a trailing backslash remain accepted (an
under-rejection), but no valid schema is rejected.

XSTS: invalid schemas accepted 1129 to 1092 (-37); valid schemas rejected held
at 72; instance buckets unchanged.
2026-06-13 23:52:11 +02:00

56 lines
2.0 KiB
Swift

@testable import PureXML
import Testing
/// A `pattern` facet value must be a valid XSD regular expression. The pattern
/// was only compiled lazily at instance time (`try?`), so an unparseable one was
/// silently ignored and the schema accepted. Compile-time validation rejects the
/// unambiguous structural errors (unbalanced parentheses, a quantifier with
/// nothing to repeat) while tolerating constructs the engine merely does not
/// support on an otherwise-valid pattern (an untabulated `\p{Is...}` block, the
/// empty pattern, the lenient `{,m}` quantifier), so no valid schema is rejected.
@Suite("Pattern facet regex validity")
struct SchemaPatternFacetTests {
private func compile(_ pattern: String) throws {
_ = try PureXML.Schema.Document("""
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="v">
<xs:simpleType>
<xs:restriction base="xs:string"><xs:pattern value="\(pattern)"/></xs:restriction>
</xs:simpleType>
</xs:element>
</xs:schema>
""")
}
private func rejects(_ pattern: String) -> Bool {
do { try compile(pattern)
return false
} catch { return true }
}
@Test("a structurally malformed pattern is rejected")
func test_malformed() {
#expect(rejects("((a)"))
#expect(rejects("(a))"))
#expect(rejects("?a"))
#expect(rejects("*a"))
#expect(rejects("+a"))
}
@Test("a valid pattern compiles")
func test_valid() throws {
try compile("[a-z]+")
try compile("[0-9]{2,4}")
try compile("(ab)*c?")
}
@Test("a construct the engine does not support is not treated as a schema error")
func test_engineGapTolerated() throws {
// Empty pattern (valid: matches the empty string), an untabulated Unicode
// block escape, and the lenient `{,m}` quantifier are all accepted.
try compile("")
try compile(#"\p{IsHighSurrogates}"#)
try compile("[0-9]{,5}")
}
}