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("]>&x;") } } @Test("An opt-in internal entity expands in text") func test_internalEntityInText() throws { let xml = "]>Hello &who;!" 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 = "]>" 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("a & b", 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("&missing;", limits: dtdAllowed) } } @Test("A recursive entity is rejected") func test_recursiveEntity() { let xml = "]>&a;" 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 = "" + "" + "" + "]>&l2;" 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 = "]>&ext;" 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 } }