PureXML/Sources/Parsing/EntityResolver.swift
Mihaela Mihaljevic c75c97aa9b feat(dtd): external and parameter entities, secure by default
Add internal parameter entities (parsed, stored, and expanded within the
DTD, including bare %name; references that inject markup declarations,
bounded by depth and the expansion budget). Record external general
entities and the external subset identifier, and load them only through an
injected EntityResolver (a struct of closures). The default refusing
resolver fetches nothing, so XXE stays closed: an external entity fails as
undefined and the external subset is left unread. Internal declarations win
over the external subset.

Closes #29
2026-06-08 03:08:20 +02:00

36 lines
1.9 KiB
Swift

public extension PureXML.Parsing {
/// An injected resolver for external entities and the external DTD subset.
///
/// PureXML never reads a file or opens a socket on its own. By default no
/// resolver is wired in (``refusing``), so every external reference is
/// refused: an external general entity then fails as undefined when used, and
/// an external subset is simply not loaded. This is the XXE-closed posture.
///
/// To opt in, supply closures, for example mapping a known set of system
/// identifiers to in-memory replacement text. A closure returns nil to refuse
/// an individual reference. It is a struct of closures rather than a protocol
/// so it composes with the value-typed parsing API and stays `Sendable`.
struct EntityResolver: Sendable {
/// Resolves an external general entity to its replacement text, given the
/// entity name and its external identifier. Returns nil to refuse, which
/// leaves the entity undefined (a reference to it then errors).
public var resolveEntity: @Sendable (_ name: String, _ id: ExternalID) -> String?
/// Resolves the external DTD subset (or an external identifier referenced
/// from a `<!DOCTYPE>`) to its declaration text. Returns nil to refuse,
/// which leaves the external subset unread.
public var resolveExternalSubset: @Sendable (_ id: ExternalID) -> String?
public init(
resolveEntity: @escaping @Sendable (_ name: String, _ id: ExternalID) -> String? = { _, _ in nil },
resolveExternalSubset: @escaping @Sendable (_ id: ExternalID) -> String? = { _ in nil },
) {
self.resolveEntity = resolveEntity
self.resolveExternalSubset = resolveExternalSubset
}
/// The default resolver: refuses every external reference, keeping XXE
/// and external-DTD fetching closed.
public static let refusing = EntityResolver()
}
}