PureXML/Tests/ContentNFAStateBudgetTests.swift
Mihaela Mihaljevic 68273205f6 feat(schema): full NameAndTypeOK type derivation in element restriction
Complete the element-vs-element type clause of Particle Valid (Restriction):
the restricting element's type must be, or derive by restriction from, the base
element's type. Previously only names were compared, so an unrelated, wider, or
ur-type element type was accepted under restriction.

Enrich the content-model particle: Term.element carries the element type's
derivation identity (the resolved type name, anyType for an absent type, nil for
an inline anonymous type). Record named simpleType restriction derivations in the
derivation table alongside complex ones. The element-vs-element check walks the
user restriction chain and then the built-in lattice to decide derivation by
name; an ur-type base, a union base (membership is not a restriction chain), and
an inline anonymous type are permitted, so no valid restriction is rejected.

XSTS: invalid schemas accepted 616 to 594, invalid instances accepted 165 to 160
(more invalid schemas rejected up front), valid schemas rejected held at 72,
valid instances rejected held at 214.
2026-06-14 13:47:18 +02:00

53 lines
2.4 KiB
Swift

@testable import PureXML
import Testing
/// Guards the content-model NFA builder against occurrence-explosion (#129).
/// The per-particle unroll cap bounds one repetition, but nested high-`maxOccurs`
/// particles multiply: the XSTS msMeta Particles set drove the builder to 8 GB
/// and OOM-killed the suite. A total state ceiling caps the whole automaton,
/// degrading further repetition to star. This locks that ceiling in: the
/// pathological model must build, stay bounded, and still match without
/// crashing, while ordinary models are untouched.
@Suite("Content NFA state budget")
struct ContentNFAStateBudgetTests {
private func element(_ name: String, min: Int, max: Int?) -> PureXML.Schema.Particle {
.init(minOccurs: min, maxOccurs: max, term: .element(name: .init(name), type: nil, typeName: nil))
}
/// A sequence repeated `min` times, each repeat holding an element repeated
/// `min` times: naively `min * min` automaton states.
private func nested(min: Int) -> PureXML.Schema.Particle {
let inner = element("a", min: min, max: min)
return .init(
minOccurs: min,
maxOccurs: min,
term: .group(.init(compositor: .sequence, particles: [inner])),
)
}
@Test("A nested occurrence explosion stays bounded instead of exhausting memory")
func test_nestedExplosionBounded() {
// 16384 * 16384 ~ 2.7e8 states without a total cap; the ceiling is 2^20.
let nfa = PureXML.Schema.ContentNFABuilder.build(nested(min: 16384))
// Bounded to the ceiling plus at most one in-flight term expansion,
// far below the naive product that previously allocated until death.
#expect(nfa.states.count < 1_500_000)
// The degraded automaton must still answer queries without crashing.
let names = Array(repeating: PureXML.Model.QualifiedName("a"), count: 32)
_ = nfa.matchesWhole(names)
}
@Test("Ordinary content models are unaffected by the ceiling")
func test_ordinaryModelUnchanged() {
let particle = PureXML.Schema.Particle(
minOccurs: 1,
maxOccurs: nil,
term: .group(.init(compositor: .sequence, particles: [element("a", min: 1, max: 1)])),
)
let nfa = PureXML.Schema.ContentNFABuilder.build(particle)
#expect(nfa.states.count < 64)
#expect(nfa.matchesWhole([.init("a"), .init("a"), .init("a")]))
#expect(!nfa.matchesWhole([.init("b")]))
}
}