import Testing @testable import PureXML @Suite("External and parameter entities") struct EntityResolverTests { private let dtdAllowed = PureXML.Parsing.Limits(allowDoctype: true) // MARK: Parameter entities @Test("A parameter entity is expanded inside a general entity value") func test_parameterEntityInValue() throws { let xml = """ ]>&msg; """ let node = try PureXML.parse(xml, limits: dtdAllowed) #expect(text(of: node) == "hello world") } @Test("A later parameter entity composes an earlier one") func test_parameterEntityComposition() throws { let xml = """ ]>&e; """ let node = try PureXML.parse(xml, limits: dtdAllowed) #expect(text(of: node) == "xyz") } @Test("A bare parameter-entity reference injects markup declarations") func test_parameterEntityInjectsDeclarations() throws { let xml = """ "> %decls; ]>&inner; """ let node = try PureXML.parse(xml, limits: dtdAllowed) #expect(text(of: node) == "deep") } @Test("Parameter entities are recorded on the document type") func test_parameterEntitiesRecorded() throws { let xml = "]>" let parsed = try PureXML.Parsing.Parser().parseWithDocumentType(xml, limits: dtdAllowed) #expect(parsed.documentType.parameterEntities["p"] == "v") } // MARK: External entities @Test("An external entity is refused by default, keeping XXE closed") func test_externalEntityRefusedByDefault() { let xml = "]>&ext;" #expect(throws: PureXML.Parsing.ParseError.self) { _ = try PureXML.parse(xml, limits: dtdAllowed) } } @Test("An external entity is recorded with its system identifier") func test_externalEntityRecorded() throws { let xml = "]>" let parsed = try PureXML.Parsing.Parser().parseWithDocumentType(xml, limits: dtdAllowed) #expect(parsed.documentType.externalEntities["ext"] == .init(systemID: "urn:x")) } @Test("An injected resolver supplies external entity replacement text") func test_resolverSuppliesEntity() throws { let xml = "]>&ext;" let resolver = PureXML.Parsing.EntityResolver( resolveEntity: { _, id in id.systemID == "urn:greeting" ? "resolved" : nil }, ) let node = try PureXML.parse(xml, limits: dtdAllowed, resolver: resolver) #expect(text(of: node) == "resolved") } @Test("A resolver that refuses a specific entity leaves it undefined") func test_resolverRefusesSpecific() { let xml = "]>&ext;" let resolver = PureXML.Parsing.EntityResolver(resolveEntity: { _, _ in nil }) #expect(throws: PureXML.Parsing.ParseError.self) { _ = try PureXML.parse(xml, limits: dtdAllowed, resolver: resolver) } } @Test("A PUBLIC external entity carries both identifiers to the resolver") func test_publicIdentifier() throws { let xml = "]>&ext;" let resolver = PureXML.Parsing.EntityResolver( resolveEntity: { _, id in "\(id.publicID ?? "nil")|\(id.systemID)" }, ) let node = try PureXML.parse(xml, limits: dtdAllowed, resolver: resolver) #expect(text(of: node) == "-//x//EN|urn:x") } // MARK: External subset @Test("The external subset identifier is recorded from the DOCTYPE") func test_externalSubsetRecorded() throws { let xml = "" let parsed = try PureXML.Parsing.Parser().parseWithDocumentType(xml, limits: dtdAllowed) #expect(parsed.documentType.externalSubset == .init(systemID: "urn:dtd")) } @Test("The external subset is loaded through the resolver and its entities used") func test_externalSubsetLoaded() throws { let xml = "&fromdtd;" let resolver = PureXML.Parsing.EntityResolver( resolveExternalSubset: { $0.systemID == "urn:dtd" ? "" : nil }, ) let node = try PureXML.parse(xml, limits: dtdAllowed, resolver: resolver) #expect(text(of: node) == "external") } @Test("Internal declarations win over the external subset") func test_internalWinsOverExternal() throws { let xml = "]>&e;" let resolver = PureXML.Parsing.EntityResolver( resolveExternalSubset: { _ in "" }, ) let node = try PureXML.parse(xml, limits: dtdAllowed, resolver: resolver) #expect(text(of: node) == "inner") } @Test("The external subset is not fetched without a resolver") func test_externalSubsetNotFetchedByDefault() throws { let xml = "&fromdtd;" // Nothing external is fetched. Per production 68 the undeclared // reference is a validity (not well-formedness) issue when an unread // external subset might have declared it, so the reference stays // literal in the text. let node = try PureXML.parse(xml, limits: dtdAllowed) #expect(text(of: node) == "&fromdtd;") } // MARK: Helpers private func text(of node: PureXML.Model.Node) -> String { var result = "" collect(node, into: &result) return result } private func collect(_ node: PureXML.Model.Node, into result: inout String) { switch node { case let .document(children): for child in children { collect(child, into: &result) } case let .element(element): for child in element.children { collect(child, into: &result) } case let .text(value), let .cdata(value): result += value default: break } } }