Add the PureXML.Catalog namespace: parse an OASIS XML catalog (public, system, uri, rewriteSystem, rewriteURI, group/catalog by local name) into a Resolver mapping public/system identifiers and URI names to URIs, with longest-prefix rewriting and system-over-public precedence. entityResolver (loadingURI:) builds a Parsing.EntityResolver that resolves an external identifier through the catalog and loads the URI via an injected closure, so a catalog plus loader wires entities and DTDs in while the default keeps XXE closed. Closes #57 Closes #58 Closes #27
112 lines
4.5 KiB
Swift
112 lines
4.5 KiB
Swift
extension PureXML.Catalog {
|
|
/// A longest-prefix rewrite rule (`rewriteSystem`, `rewriteURI`).
|
|
struct RewriteRule: Sendable {
|
|
let startString: String
|
|
let rewritePrefix: String
|
|
}
|
|
}
|
|
|
|
public extension PureXML.Catalog {
|
|
/// An OASIS XML Catalog (the libxml2 `catalog.h` model): it maps public and
|
|
/// system identifiers, and URI names, to replacement URIs.
|
|
///
|
|
/// The catalog itself performs no I/O: it resolves an identifier to a URI
|
|
/// string. To actually load the target, build a ``PureXML/Parsing/EntityResolver``
|
|
/// with ``entityResolver(loadingURI:)`` and supply a closure that turns a
|
|
/// resolved URI into text. With no such closure nothing is loaded, so XXE
|
|
/// stays closed.
|
|
///
|
|
/// Supported entries (matched by local name, namespace-agnostic): `public`,
|
|
/// `system`, `uri`, `rewriteSystem`, `rewriteURI`, and `group` (recursed).
|
|
struct Resolver: Sendable {
|
|
private let systemMap: [String: String]
|
|
private let publicMap: [String: String]
|
|
private let uriMap: [String: String]
|
|
private let rewriteSystem: [RewriteRule]
|
|
private let rewriteURI: [RewriteRule]
|
|
|
|
/// Parses an OASIS XML catalog document.
|
|
public init(_ xml: String) throws {
|
|
self = try CatalogParser.parse(xml)
|
|
}
|
|
|
|
init(
|
|
systemMap: [String: String],
|
|
publicMap: [String: String],
|
|
uriMap: [String: String],
|
|
rewriteSystem: [RewriteRule],
|
|
rewriteURI: [RewriteRule],
|
|
) {
|
|
self.systemMap = systemMap
|
|
self.publicMap = publicMap
|
|
self.uriMap = uriMap
|
|
self.rewriteSystem = rewriteSystem
|
|
self.rewriteURI = rewriteURI
|
|
}
|
|
|
|
/// Resolves a system identifier: an exact `system` entry, else the longest
|
|
/// matching `rewriteSystem` prefix.
|
|
public func resolveSystem(_ systemID: String) -> String? {
|
|
systemMap[systemID] ?? Self.rewrite(systemID, with: rewriteSystem)
|
|
}
|
|
|
|
/// Resolves a public identifier against the `public` entries.
|
|
public func resolvePublic(_ publicID: String) -> String? {
|
|
publicMap[publicID]
|
|
}
|
|
|
|
/// Resolves a URI name: an exact `uri` entry, else the longest matching
|
|
/// `rewriteURI` prefix.
|
|
public func resolveURI(_ name: String) -> String? {
|
|
uriMap[name] ?? Self.rewrite(name, with: rewriteURI)
|
|
}
|
|
|
|
/// Resolves an external identifier the way a parser would: the system
|
|
/// identifier takes precedence (OASIS rule), then the public identifier.
|
|
public func resolveExternalIdentifier(publicID: String?, systemID: String?) -> String? {
|
|
if let systemID, let resolved = resolveSystem(systemID) {
|
|
return resolved
|
|
}
|
|
if let publicID { return resolvePublic(publicID) }
|
|
return nil
|
|
}
|
|
|
|
/// Builds a ``PureXML/Parsing/EntityResolver`` that resolves an external
|
|
/// entity or subset through this catalog and then loads the resolved URI
|
|
/// with `loadingURI`. An identifier the catalog cannot map, or a URI the
|
|
/// loader returns nil for, is refused, so the default posture stays closed.
|
|
public func entityResolver(
|
|
loadingURI: @escaping @Sendable (_ uri: String) -> String?,
|
|
) -> PureXML.Parsing.EntityResolver {
|
|
PureXML.Parsing.EntityResolver(
|
|
resolveEntity: { _, identifier in
|
|
load(identifier, loadingURI)
|
|
},
|
|
resolveExternalSubset: { identifier in
|
|
load(identifier, loadingURI)
|
|
},
|
|
)
|
|
}
|
|
|
|
private func load(
|
|
_ identifier: PureXML.Parsing.ExternalID,
|
|
_ loadingURI: @Sendable (String) -> String?,
|
|
) -> String? {
|
|
guard let uri = resolveExternalIdentifier(publicID: identifier.publicID, systemID: identifier.systemID) else {
|
|
return nil
|
|
}
|
|
return loadingURI(uri)
|
|
}
|
|
|
|
/// The rewrite with the longest matching `startString`, applied as a prefix
|
|
/// replacement.
|
|
private static func rewrite(_ input: String, with rules: [RewriteRule]) -> String? {
|
|
let best = rules
|
|
.filter { input.hasPrefix($0.startString) }
|
|
.max { $0.startString.count < $1.startString.count }
|
|
guard let best else { return nil }
|
|
return best.rewritePrefix + input.dropFirst(best.startString.count)
|
|
}
|
|
}
|
|
}
|