XSD 1.0 (Datatypes Appendix F) defines the regex escape \i as the characters that may begin an XML Name (Letter | '_' | ':') and \c as the characters that may appear in one (NameChar), citing the XML 1.0 Second Edition productions: the enumerated, Unicode-2.0-era character classes of XML 1.0 Appendix B (BaseChar, Ideographic, CombiningChar, Digit, Extender). \i and \c were wired to PureXML.Parsing.XMLCharacter, which correctly implements the Fifth Edition Name production used to parse live XML, but the two genuinely differ: the Fifth Edition admits whole blocks where the Second Edition has gaps, so \c over-accepted, e.g. the combining mark U+0346 (COMBINING BRIDGE ABOVE), which Appendix B's CombiningChar excludes (its first range ends at U+0345). Add XSDNameCharacter, transcribed clean-room from the Appendix B grammar (identical across editions one through four) and stored as sorted, non- overlapping code-point ranges with binary-search membership; the parser's own name check is untouched. The two tables reproduce the W3C XSTS \i/\c enumerations (reZ005, reZ006: 34516 and 35122 code points) exactly and in both directions, so the more restrictive definition rejects no spec-valid input: there is no false-positive surface. Verified over the full W3C XSTS corpus in release: invalid-instances-accepted 12 -> 11 (reZ006i, whose value is U+0346, now rejects), both false-positive buckets at 0. Full suite (1834 tests) green; an adversarial critic brute-forced the binary search over the whole code-point space (0 mismatches), confirmed the tables are sorted and non-overlapping and equal the oracle sets, and that the parser's Fifth Edition path is left intact. Found by triaging the invalid- instance tail to a single boundary code point (U+0345 valid vs U+0346 invalid) and reading the XML 1.0 Appendix B grammar.
66 lines
3.0 KiB
Swift
66 lines
3.0 KiB
Swift
import Testing
|
|
@testable import PureXML
|
|
|
|
/// The XSD `\i`/`\c` regex escapes use the XML 1.0 Second Edition name classes
|
|
/// (Appendix B), a fixed Unicode-2.0 repertoire, distinct from the parser's
|
|
/// Fifth Edition Name production. These exercise the boundary cases the W3C XSTS
|
|
/// `\i`/`\c` tests (reZ005, reZ006) probe and the deliberate divergence from
|
|
/// `PureXML.Parsing.XMLCharacter`.
|
|
@Suite("XSD \\i / \\c name-character classes (XML 1.0 Appendix B)")
|
|
struct XSDNameCharacterTests {
|
|
private typealias NameChar = PureXML.Regex.XSDNameCharacter
|
|
|
|
private func matches(_ pattern: String, _ scalar: UInt32) throws -> Bool {
|
|
try PureXML.Regex.matches(pattern, String(#require(Unicode.Scalar(scalar))))
|
|
}
|
|
|
|
@Test("\\c admits the Appendix B CombiningChar range but not the gap above it")
|
|
func test_combiningCharBoundary() throws {
|
|
// Appendix B CombiningChar begins [#x0300-#x0345]; U+0346 starts the gap.
|
|
// XSTS reZ006: U+0345 is a valid \c, U+0346 is not (the failing case).
|
|
#expect(NameChar.isNameChar("\u{0345}"))
|
|
#expect(!NameChar.isNameChar("\u{0346}"))
|
|
#expect(try matches("\\c", 0x0345))
|
|
#expect(try !matches("\\c", 0x0346))
|
|
// \C is the negation.
|
|
#expect(try !matches("\\C", 0x0345))
|
|
#expect(try matches("\\C", 0x0346))
|
|
}
|
|
|
|
@Test("\\c and \\i deliberately differ from the parser's Fifth Edition name check")
|
|
func test_divergesFromParser() {
|
|
// U+0346 is a Fifth Edition name character (the parser accepts it), but not
|
|
// an XML 1.0 Second Edition one, so XSD \c must reject it. This is the bug
|
|
// that wiring \c to the parser caused.
|
|
#expect(PureXML.Parsing.XMLCharacter.isNameChar("\u{0346}"))
|
|
#expect(!NameChar.isNameChar("\u{0346}"))
|
|
}
|
|
|
|
@Test("\\i is the name-start class: letters, '_' and ':', not digits or combining marks")
|
|
func test_nameStartClass() throws {
|
|
#expect(NameChar.isNameStart("a"))
|
|
#expect(NameChar.isNameStart("_"))
|
|
#expect(NameChar.isNameStart(":"))
|
|
// A digit may appear in a Name but may not start one.
|
|
#expect(!NameChar.isNameStart("0"))
|
|
#expect(NameChar.isNameChar("0"))
|
|
// A combining mark is a NameChar but not a name-start.
|
|
#expect(!NameChar.isNameStart("\u{0345}"))
|
|
#expect(NameChar.isNameChar("\u{0345}"))
|
|
// XSTS reZ005 boundary: '[' (U+005B) is neither.
|
|
#expect(try !matches("\\i", 0x005B))
|
|
#expect(try matches("\\i", 0x0061))
|
|
}
|
|
|
|
@Test("\\c covers the ASCII name punctuation and excludes a clear non-name char")
|
|
func test_asciiAndNegative() {
|
|
let nameChars: [Unicode.Scalar] = ["A", "z", "0", "9", "-", ".", "_", ":"]
|
|
for scalar in nameChars {
|
|
#expect(NameChar.isNameChar(scalar), "U+\(String(scalar.value, radix: 16)) is a NameChar")
|
|
}
|
|
// '+' (U+002B) and ' ' (space) are not NameChars in any edition.
|
|
#expect(!NameChar.isNameChar("+"))
|
|
#expect(!NameChar.isNameChar(" "))
|
|
}
|
|
}
|