A repeating model whose optional trailing particle and leading particle share a name,
(c, b?, c?)*, is ambiguous, as libxml2/Xerces reject it: after b the next c could be the
trailing c? or the looped leading c, two distinct particles. The compositional determinism
check formed the internal sequence gap before the enclosing repetition's loop edge was
added, so the combined {trailing, leading} followpos was never checked, an invalid-schemas-
accepted false negative the finite XSTS corpus did not contain.
The sequence now carries each trailing end-item's internal suffix in its lastFollow, so a
parent gap or the loop edge (withRepeatEdge) forms the end-item's full followpos. The
carried suffix is always a subset of the true followpos, so the check only catches more
genuine conflicts and never over-rejects: the non-repeated (c, b?, c?) and disjoint shapes
like (a, b?)* stay valid.
Found by the new generated-schema determinism differential against libxml2. Verified over
the full W3C XSTS corpus in release (valid-schemas-rejected/valid-instances-rejected stay
0, invalid-*-accepted unchanged at 8/14), full suite on macOS/Linux/WASI, and an
adversarial over-rejection critic finding no valid schema newly rejected over 47 shapes.
The related ambiguity through two references to one group in a repeating sequence stays an
under-rejection (the cached group identity cannot distinguish a looped from a trailing
occurrence without per-occurrence tracking), named in DeterminismDifferentialTests.
56 lines
3.2 KiB
Swift
56 lines
3.2 KiB
Swift
import Testing
|
|
@testable import PureXML
|
|
|
|
/// A repeating model whose optional trailing particle and leading particle share a name,
|
|
/// `(c, b?, c?)*`, is ambiguous: after `b` the next `c` could be the trailing `c?` or the
|
|
/// looped leading `c`, two distinct particles. The compositional determinism check
|
|
/// emitted the internal sequence gap before the enclosing repetition's loop edge, so the
|
|
/// combined `{trailing, leading}` followpos was never formed. The fix carries each
|
|
/// trailing end-item's internal suffix in the sequence's `lastFollow`, so the loop (and a
|
|
/// parent gap) form the full followpos. Found by the generated-schema determinism
|
|
/// differential against libxml2 (`DeterminismDifferentialTests`).
|
|
@Suite("UPA on a repeating sequence's trailing/leading overlap")
|
|
struct DeterminismSequenceFollowposTests {
|
|
private let opt = " minOccurs=\"0\""
|
|
private let star = " maxOccurs=\"unbounded\""
|
|
private let optStar = " minOccurs=\"0\" maxOccurs=\"unbounded\""
|
|
|
|
private func element(_ name: String, _ occurrence: String = "") -> String {
|
|
"<xs:element name=\"\(name)\"\(occurrence)/>"
|
|
}
|
|
|
|
private func sequence(_ occurrence: String, _ members: String...) -> String {
|
|
"<xs:sequence\(occurrence)>\(members.joined())</xs:sequence>"
|
|
}
|
|
|
|
private func accepts(_ content: String) -> Bool {
|
|
let schema = "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">"
|
|
+ "<xs:element name=\"d\"><xs:complexType>\(content)</xs:complexType></xs:element></xs:schema>"
|
|
return (try? PureXML.Schema.Document(schema)) != nil
|
|
}
|
|
|
|
@Test("a repeating sequence whose trailing optional and leading particle overlap is rejected")
|
|
func test_repeatingTrailingLeadingOverlapRejected() {
|
|
// (c, b?, c?)*: after b, the trailing c? and the looped leading c both match c.
|
|
#expect(!accepts(sequence(optStar, element("c"), element("b", opt), element("c", opt))))
|
|
// (a, b?, a?)*: the same shape, leading and trailing a.
|
|
#expect(!accepts(sequence(star, element("a"), element("b", opt), element("a", opt))))
|
|
// (a?, b, a?)*: leading optional a and trailing optional a, around a required b.
|
|
#expect(!accepts(sequence(star, element("a", opt), element("b"), element("a", opt))))
|
|
}
|
|
|
|
@Test("the same sequences NOT repeated, or without the overlap, stay valid")
|
|
func test_noOverRejection() {
|
|
// (c, b?, c?) NOT repeated: the trailing c is the only c after b, unambiguous.
|
|
#expect(accepts(sequence("", element("c"), element("b", opt), element("c", opt))))
|
|
// (a, b?)*: distinct names, the loop's a does not overlap the trailing b.
|
|
#expect(accepts(sequence(optStar, element("a"), element("b", opt))))
|
|
// (a, b, c)*: all distinct, the loop's a follows the required c unambiguously.
|
|
#expect(accepts(sequence(star, element("a"), element("b"), element("c"))))
|
|
// a*: a single particle looping is not two occurrences (the loop-fold must not flag).
|
|
#expect(accepts(sequence("", element("a", optStar))))
|
|
// (c, b?, d?)*: the trailing d does not share the leading c's name.
|
|
#expect(accepts(sequence(star, element("c"), element("b", opt), element("d", opt))))
|
|
}
|
|
}
|