extension PureXML.Regex { /// One NFA state: epsilon transitions and at most one character-class /// transition (Thompson construction). struct NFAState: Sendable { var epsilon: [Int] = [] var transition: CharTransition? } /// A character-consuming transition to another state. struct CharTransition: Sendable { let charClass: CharClass let target: Int } /// A compiled non-deterministic finite automaton. struct NFA: Sendable { var states: [NFAState] var start: Int var accept: Int /// Whether the automaton matches the whole string (XSD patterns are /// implicitly anchored at both ends). XSD regular expressions match a /// sequence of Unicode code points, not Swift extended grapheme clusters. func matchesWhole(_ string: String) -> Bool { var current = closure([start]) for scalar in string.unicodeScalars { var next: Set = [] for state in current { if let transition = states[state].transition, transition.charClass.matches(scalar) { next.insert(transition.target) } } if next.isEmpty { return false } current = closure(next) } return current.contains(accept) } private func closure(_ seed: Set) -> Set { var seen = seed var stack = Array(seed) while let state = stack.popLast() { for next in states[state].epsilon where seen.insert(next).inserted { stack.append(next) } } return seen } } /// Builds an ``NFA`` from a ``Node`` by Thompson construction. struct NFABuilder { private var states: [NFAState] = [] static func build(_ node: Node) -> NFA { var builder = NFABuilder() let fragment = builder.fragment(for: node) return NFA(states: builder.states, start: fragment.start, accept: fragment.accept) } private mutating func addState() -> Int { states.append(NFAState()) return states.count - 1 } /// `{n,m}` quantifiers unroll into automaton states. Mirroring the XSD /// content-model builder (`Schema.ContentNFABuilder`), each repetition is /// capped and the total state count is bounded: past the ceiling a /// repetition degrades to star, the libxml2 posture. Without this an /// `xs:pattern` such as `a{1000000000}` (the XSTS msMeta Regex set) /// allocates until the process is OOM-killed (#129). Real patterns are /// tens of states, so only pathological quantifiers reach the ceiling. private static let repeatUnrollCap = 16384 private static let totalStateCap = 1 << 20 private var stateBudgetExhausted: Bool { states.count >= Self.totalStateCap } private mutating func fragment(for node: Node) -> (start: Int, accept: Int) { switch node { case .empty: let start = addState() let accept = addState() states[start].epsilon.append(accept) return (start, accept) case let .characters(charClass): let start = addState() let accept = addState() states[start].transition = CharTransition(charClass: charClass, target: accept) return (start, accept) case let .concat(nodes): return concatenate(nodes) case let .alternate(nodes): return alternate(nodes) case let .repeated(inner, minimum, maximum): return repeatedFragment(inner, minimum: minimum, maximum: maximum) } } private mutating func concatenate(_ nodes: [Node]) -> (start: Int, accept: Int) { guard !nodes.isEmpty else { return fragment(for: .empty) } var first: Int? var last: Int? for node in nodes { let part = fragment(for: node) if let previous = last { states[previous].epsilon.append(part.start) } else { first = part.start } last = part.accept } return (first ?? addState(), last ?? addState()) } private mutating func alternate(_ nodes: [Node]) -> (start: Int, accept: Int) { let start = addState() let accept = addState() for node in nodes { let part = fragment(for: node) states[start].epsilon.append(part.start) states[part.accept].epsilon.append(accept) } return (start, accept) } private mutating func repeatedFragment(_ node: Node, minimum: Int, maximum: Int?) -> (start: Int, accept: Int) { let start = addState() var current = start let boundedMin = Swift.min(minimum, Self.repeatUnrollCap) var unrolled = 0 for _ in 0 ..< boundedMin { if stateBudgetExhausted { break } let part = fragment(for: node) states[current].epsilon.append(part.start) current = part.accept unrolled += 1 } let accept = addState() if stateBudgetExhausted || unrolled < boundedMin || minimum > Self.repeatUnrollCap { // An absurd minimum, or a build that hit the total state ceiling // mid-unroll, repeats freely (star) past the cap. appendStar(node, from: current, to: accept) } else if let maximum, maximum - boundedMin <= Self.repeatUnrollCap { appendOptional(node, count: maximum - boundedMin, from: current, to: accept) } else { appendStar(node, from: current, to: accept) } return (start, accept) } private mutating func appendOptional(_ node: Node, count: Int, from: Int, to accept: Int) { var current = from for _ in 0 ..< max(0, count) { if stateBudgetExhausted { // Ran out of state budget: let the remainder repeat freely. appendStar(node, from: current, to: accept) return } let part = fragment(for: node) states[current].epsilon.append(part.start) states[current].epsilon.append(accept) current = part.accept } states[current].epsilon.append(accept) } private mutating func appendStar(_ node: Node, from current: Int, to accept: Int) { let part = fragment(for: node) states[current].epsilon.append(part.start) states[current].epsilon.append(accept) states[part.accept].epsilon.append(part.start) states[part.accept].epsilon.append(accept) } } } public extension PureXML.Regex { /// A compiled regular expression in the XML Schema flavor: literals, `.`, the /// character-class escapes (`\d \D \w \W \s \S \i \I \c \C`), single-character /// escapes, character classes with ranges and negation, grouping, alternation /// (`|`), and the quantifiers `?`, `*`, `+`, `{n}`, `{n,}`, `{n,m}`. Character /// classes also support Unicode category and block escapes (`\p{L}`, `\P{Nd}`, /// `\p{IsBasicLatin}`) and subtraction (`[a-z-[aeiou]]`). Matching is /// whole-string (anchored), as XSD `pattern` facets require. Built on a /// Thompson NFA, so it runs in time linear in the input with no backtracking /// blow-up. struct Pattern: Sendable { private let nfa: NFA /// Compiles a pattern. public init(_ pattern: String) throws { nfa = try NFABuilder.build(RegexParser.parse(pattern)) } /// Whether `string` matches the pattern in full. public func matches(_ string: String) -> Bool { nfa.matchesWhole(string) } } /// Compiles and matches in one step. static func matches(_ pattern: String, _ string: String) throws -> Bool { try Pattern(pattern).matches(string) } }