PureXML/Tests/DTDTests.swift
Mihaela Mihaljevic 83e6e27d12 style(format): adopt --importgrouping testable-bottom from canonical rules-swift
Group @testable imports at the bottom, per public/rules-swift's swiftformat. A
behavior-preserving import-ordering reformat across 252 files (mostly tests).
--trailingclosures was deliberately not adopted (cosmetic, and swiftformat flags
it as caution-worthy).
2026-06-18 20:33:07 +02:00

103 lines
3.7 KiB
Swift

import Testing
@testable import PureXML
@Suite("DTD and entities")
struct DTDTests {
private let dtdAllowed = PureXML.Parsing.Limits(allowDoctype: true)
@Test("DOCTYPE is rejected by default")
func test_doctypeRejectedByDefault() {
#expect(throws: PureXML.Parsing.ParseError.self) {
try PureXML.parse("<!DOCTYPE r [<!ENTITY x \"y\">]><r>&x;</r>")
}
}
@Test("An opt-in internal entity expands in text")
func test_internalEntityInText() throws {
let xml = "<!DOCTYPE r [<!ENTITY who \"World\">]><r>Hello &who;!</r>"
let node = try PureXML.parse(xml, limits: dtdAllowed)
#expect(rootText(node) == "Hello World!")
}
@Test("An opt-in internal entity expands in an attribute value")
func test_internalEntityInAttribute() throws {
let xml = "<!DOCTYPE r [<!ENTITY v \"42\">]><r a=\"&v;\"/>"
let node = try PureXML.parse(xml, limits: dtdAllowed)
#expect(rootElement(node)?.attributes.first?.value == "42")
}
@Test("Predefined entities still work with a DTD present")
func test_predefinedWithDTD() throws {
let node = try PureXML.parse("<!DOCTYPE r []><r>a &amp; b</r>", limits: dtdAllowed)
#expect(rootText(node) == "a & b")
}
@Test("An undeclared entity is rejected")
func test_undeclaredEntity() {
#expect(throws: PureXML.Parsing.ParseError.self) {
try PureXML.parse("<!DOCTYPE r []><r>&missing;</r>", limits: dtdAllowed)
}
}
@Test("A recursive entity is rejected")
func test_recursiveEntity() {
let xml = "<!DOCTYPE r [<!ENTITY a \"&a;\">]><r>&a;</r>"
expectError(xml, limits: dtdAllowed) { error in
if case .recursiveEntity = error { return true }
return false
}
}
@Test("Billion-laughs amplification is rejected")
func test_billionLaughs() {
let xml = "<!DOCTYPE r ["
+ "<!ENTITY l0 \"AAAAAAAAAA\">"
+ "<!ENTITY l1 \"&l0;&l0;&l0;&l0;&l0;&l0;&l0;&l0;&l0;&l0;\">"
+ "<!ENTITY l2 \"&l1;&l1;&l1;&l1;&l1;&l1;&l1;&l1;&l1;&l1;\">"
+ "]><r>&l2;</r>"
let limits = PureXML.Parsing.Limits(allowDoctype: true, maxEntityExpansion: 100)
expectError(xml, limits: limits) { error in
if case .amplificationLimitExceeded = error { return true }
return false
}
}
@Test("External entities are refused, not loaded (XXE stays closed)")
func test_externalEntityRefused() {
// The external entity is not stored, so referencing it fails as undeclared
// rather than reading the file: external resolution never happens.
let xml = "<!DOCTYPE r [<!ENTITY ext SYSTEM \"file:///etc/passwd\">]><r>&ext;</r>"
expectError(xml, limits: dtdAllowed) { error in
if case .undefinedEntity = error { return true }
return false
}
}
private func expectError(
_ xml: String,
limits: PureXML.Parsing.Limits,
matching predicate: (PureXML.Parsing.ParseError) -> Bool,
) {
do {
_ = try PureXML.parse(xml, limits: limits)
Issue.record("expected a parse error")
} catch let error as PureXML.Parsing.ParseError {
#expect(predicate(error))
} catch {
Issue.record("unexpected error: \(error)")
}
}
private func rootElement(_ node: PureXML.Model.Node) -> PureXML.Model.Element? {
guard case let .document(children) = node else { return nil }
for child in children {
if case let .element(element) = child { return element }
}
return nil
}
private func rootText(_ node: PureXML.Model.Node) -> String? {
rootElement(node)?.text
}
}