PureXML/Sources/Validation/DTDValidations.swift
Mihaela Mihaljevic 7f1aada240 feat(dtd): conditional sections, notations, NDATA, external parameter entities
Deepen DTD processing in the doctype scanner:

- Conditional sections are processed, not skipped: INCLUDE sections have
  their declarations scanned, IGNORE sections are discarded, the keyword
  may be a parameter-entity reference, and nested sections are handled.
- <!NOTATION> declarations are parsed and stored on DocumentType.
- Unparsed entities (<!ENTITY ... NDATA n>) are parsed and stored with
  their notation, distinct from general entities.
- External parameter entities are loaded through the injected resolver so
  their declarations expand; the default refusing resolver loads nothing,
  keeping XXE closed.
- NOTATION attributes are a distinct type, validated against the declared
  notations (value must be listed and the notation declared).

Tracking unparsed entities also lets ENTITY/ENTITIES attribute values be
validated against the declared NDATA entities, closing the gap left when
#73 could only check name syntax.

Closes #72
2026-06-08 14:39:42 +02:00

63 lines
3.1 KiB
Swift

/// File-scope aliases for the DTD validation rules and helpers, kept out of the
/// `DTD` enum to avoid nesting a type two levels deep.
typealias DTDElement = PureXML.Model.Element
typealias DTDPath = [PureXML.Validation.PathKey]
typealias DTDFailure = PureXML.Validation.ValidationError
public extension PureXML.Validation {
/// DTD validation as composable rules over a ``DTDSchema`` document: every
/// element is checked against its content model and attribute declarations,
/// and ID/IDREF integrity is checked once over the whole tree. Replaces the
/// imperative walk with declared ``Validation`` values.
enum DTD {
/// A validator for a ``DTDSchema``. In strict mode every element must also
/// be declared.
static func validator(strict: Bool) -> Validator<DTDSchema> {
var validator = Validator<DTDSchema>.blank
.validating(contentModel, attributeDeclarations)
.validating(identifierIntegrity)
if strict { validator = validator.validating(undeclaredElement) }
return validator
}
/// Each element's content matches its `<!ELEMENT>` model.
static var contentModel: Validation<DTDElement, DTDSchema> {
.init(description: "Element content matches its DTD content model") { context in
let element = context.subject
guard let model = context.document.models[element.name.description] else { return [] }
return contentViolations(element, model: model, at: context.codingPath)
}
}
/// Each element's attributes satisfy their `<!ATTLIST>` declarations
/// (required presence, `#FIXED` value, and enumerations).
static var attributeDeclarations: Validation<DTDElement, DTDSchema> {
.init(description: "Element attributes satisfy their DTD declarations") { context in
let element = context.subject
guard let declarations = context.document.attributes[element.name.description] else { return [] }
let schema = context.document
return declarations.flatMap { declaration in
attributeViolations(declaration, on: element, schema: schema, at: context.codingPath)
}
}
}
/// In strict mode, every element is declared in the DTD.
static var undeclaredElement: Validation<DTDElement, DTDSchema> {
.init(description: "Element is declared in the DTD") { context in
context.document.models[context.subject.name.description] != nil
}
}
/// ID values are unique and every IDREF resolves. Runs once over the whole
/// tree (a reference may point forward), so it is gated to the root.
static var identifierIntegrity: Validation<PureXML.Model.Node, DTDSchema> {
.init(
description: "DTD ID values are unique and IDREFs resolve",
check: { context in identifierErrors(context.subject, schema: context.document, at: context.codingPath) },
when: { $0.codingPath.isEmpty },
)
}
}
}