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 } }) } }