import Testing
@testable import PureXML
/// The DTD-subset strictness landed with the OASIS/NIST conformance pass:
/// unknown or malformed markup declarations, the exact `%name;` parameter-
/// reference grammar, strict declaration names, NDATA rules, notation tails,
/// conditional-section keywords and balance, external-subset error
/// propagation with text declarations, and PI target separation.
@Suite("DTD subset strictness")
struct DTDSubsetStrictnessTests {
private func limits() -> PureXML.Parsing.Limits {
PureXML.Parsing.Limits(allowDoctype: true)
}
private func parses(_ xml: String) -> Bool {
(try? PureXML.parse(xml, limits: limits())) != nil
}
private func parses(_ xml: String, externalSubset: String) -> Bool {
let resolver = PureXML.Parsing.EntityResolver(
resolveEntity: { _, _ in nil },
resolveExternalSubset: { _ in externalSubset },
)
return (try? PureXML.parse(xml, limits: limits(), resolver: resolver)) != nil
}
private let externalHost = "\n"
@Test("Unknown, lowercase, or space-separated declaration keywords are rejected")
func test_declarationKeywords() {
#expect(!parses("]>\n"))
#expect(!parses("]>\n"))
#expect(!parses("]>\n"))
#expect(parses("]>\n"))
}
@Test("A parameter-entity reference is '%' Name ';' exactly")
func test_parameterReferenceGrammar() {
let declare = "\">"
#expect(!parses("]>\n"))
#expect(!parses("\n"))
#expect(!parses("\n"))
#expect(parses("\n"))
}
@Test("Entity and parameter-entity names must start with a name-start character")
func test_strictEntityNames() {
#expect(!parses("]>\n"))
#expect(!parses("]>\n"))
#expect(parses("]>\n"))
}
@Test("NDATA requires whitespace and a strict notation name")
func test_ndataNames() {
let notation = ""
#expect(!parses("\(notation)]>\n"))
#expect(!parses("\(notation)]>\n"))
#expect(parses("\(notation)]>\n"))
}
@Test("A notation declaration requires its identifier and a clean tail")
func test_notationTail() {
#expect(!parses("]>\n"))
#expect(!parses("]>\n"))
#expect(parses("]>\n"))
}
@Test("An entity value literal admits no bare '%' or '&'")
func test_entityLiteralReferences() {
#expect(!parses(externalHost, externalSubset: ""))
#expect(!parses(externalHost, externalSubset: ""))
#expect(parses(externalHost, externalSubset: ""))
}
@Test("Conditional-section keywords are case-sensitive INCLUDE or IGNORE and must balance")
func test_conditionalSections() {
#expect(!parses(externalHost, externalSubset: "]]>"))
#expect(!parses(externalHost, externalSubset: "]]>"))
#expect(!parses(externalHost, externalSubset: ""))
#expect(!parses(externalHost, externalSubset: "] ]>"))
#expect(!parses(externalHost, externalSubset: ""))
#expect(!parses(externalHost, externalSubset: "]]>"))
#expect(parses(externalHost, externalSubset: "]]>"))
#expect(parses(externalHost, externalSubset: " ]]>"))
}
@Test("External-subset grammar violations reject the document")
func test_externalSubsetPropagates() {
#expect(!parses(externalHost, externalSubset: "]>"))
#expect(parses(externalHost, externalSubset: ""))
}
@Test("A text declaration allows version and requires encoding, never standalone")
func test_textDeclaration() {
#expect(parses(externalHost, externalSubset: "\n"))
#expect(parses(externalHost, externalSubset: "\n"))
#expect(!parses(externalHost, externalSubset: "\n"))
#expect(!parses(externalHost, externalSubset: "\n"))
}
@Test("A processing-instruction target must be separated from its data")
func test_processingInstructionSeparation() {
#expect(!parses("\n"))
#expect(parses("\n"))
#expect(parses("\n"))
}
@Test("The internal subset admits only declarations, PE references, and whitespace")
func test_subsetJunk() {
#expect(!parses(" junk ]>\n"))
#expect(!parses(externalHost, externalSubset: " ]]>"))
}
@Test("A PI target inside the subset must be separated from its data")
func test_subsetPISeparation() {
#expect(!parses("]>\n"))
#expect(parses("]>\n"))
}
@Test("Character references in a PE literal are expanded at declaration (Appendix D)")
func test_parameterEntityCharacterReferences() {
// The spec's tricky example: %xx; expands to %zz; which declares tricky.
let xml = """
' >
%xx;
]>
This sample shows a &tricky; method.
"""
let node = try? PureXML.parse(xml, limits: limits())
#expect(node != nil)
}
@Test("An external entity's text declaration is not part of its replacement")
func test_externalEntityTextDeclaration() {
let xml = "]>\n&outside;"
let resolver = PureXML.Parsing.EntityResolver(
resolveEntity: { _, _ in "ok" },
resolveExternalSubset: { _ in nil },
)
let node = try? PureXML.parse(xml, limits: limits(), resolver: resolver)
#expect(node != nil)
// The declaration must be stripped, not included as text.
guard let node, case let .document(children) = node,
case let .element(root)? = children.first(where: { $0.element != nil })
else { return }
#expect(root.children.allSatisfy { child in
if case let .text(value) = child { value == "ok" } else { false }
})
}
}