PureXML/Sources/Parsing/ParseError.swift
Mihaela Mihaljevic dd0ff5d086 feat(parsing): fourth W3C-suite strictness pass, declaration grammar
Make the DTD declaration grammar strict: <!ENTITY> requires its whitespace
(after the keyword, around %, after the name), a real name (not #DEFAULT),
nothing but '>' after the value, NDATA preceded by whitespace and forbidden
on parameter entities; entity-value literals are validated (XML Chars only,
every & a complete reference); SYSTEM/PUBLIC external identifiers parse
strictly (PUBLIC needs both literals for entities and the DOCTYPE, public-only
stays legal for notations) with PubidChar validation; <!ATTLIST> bodies are
validated against XML 1.0 productions 52-60 via a new DTDAttListGrammar
(spacing, attribute types, enumerations, quoted defaults); the reserved xml
PI target is rejected inside the subset. Also: strict UTF-8 decoding (no
silent replacement of malformed sequences), XML Char checks in attribute
values and CDATA, the declaration's version/encoding value grammar, and
required whitespace between pseudo-attributes. The strict machinery lives in
DoctypeScannerStrict/DTDAttListGrammar to stay under the length caps.

W3C xmltest: not-wf/sa 162/186 rejected (from 129; 80 at first contact),
valid/sa 118/119, zero false positives across all 846 tests at every step.
The remaining 24-case baseline is exactly: the entity replacement-text class
(19) and the deliberate internal-subset extensions this package supports as
features (conditional sections, PEs in the internal subset: 5).

Part of #120, #105
2026-06-10 05:34:11 +02:00

136 lines
7.2 KiB
Swift

public extension PureXML.Parsing {
/// Parser error with source position information when available.
enum ParseError: Swift.Error, Equatable, Sendable, CustomStringConvertible {
case emptyDocument
case unexpectedEndOfInput(Mark)
case unexpectedCharacter(Character, Mark)
case unterminatedTag(Mark)
case unterminatedComment(Mark)
case unterminatedCDATA(Mark)
case mismatchedEndTag(expected: String, found: String, Mark)
case duplicateAttribute(name: String, Mark)
case expectedName(Mark)
case expectedEquals(Mark)
case unquotedAttributeValue(Mark)
case invalidReference(String, Mark)
case undefinedEntity(name: String, Mark)
case recursiveEntity(name: String, Mark)
case amplificationLimitExceeded(Mark)
case unexpectedEndTag(name: String, Mark)
case undefinedNamespacePrefix(prefix: String, Mark)
case junkAfterDocumentElement(Mark)
case nestingTooDeep(limit: Int, Mark)
case nameTooLong(limit: Int, Mark)
case contentTooLong(limit: Int, Mark)
/// A `<!DOCTYPE ...>` declaration was found. DTD processing is disabled by
/// default as a security posture (XXE, entity-expansion DoS); enabling it
/// is a deliberate future opt-in, not silent behavior.
case unsupportedDoctype(Mark)
/// Raised when raw bytes cannot be decoded in the detected encoding (for
/// example an odd-length UTF-16 stream).
case malformedEncoding
/// Raised by entry points whose parsing path is not implemented yet.
case notImplemented(String)
/// The XML declaration `<?xml ... ?>` is malformed: its pseudo-attributes
/// are out of order, unknown, or carry an illegal value.
case malformedDeclaration(Mark)
/// A comment contains `--`, which XML 1.0 forbids inside comments.
case doubleHyphenInComment(Mark)
/// An attribute value contains a raw `<`, which must be escaped.
case rawLessThanInAttribute(Mark)
/// Character data contains the literal `]]>` sequence, which must be
/// escaped outside a CDATA section.
case cdataCloseInContent(Mark)
/// Two attributes are not separated by whitespace.
case missingSpaceBeforeAttribute(Mark)
/// A character outside the XML `Char` production appears in content.
case invalidCharacter(Mark)
/// A processing instruction uses the reserved target `xml` (any case),
/// which is only legal as the document's XML declaration.
case reservedProcessingInstructionTarget(Mark)
/// An `<!ELEMENT>` content model does not match the XML 1.0 grammar.
case invalidContentModel(element: String, Mark)
/// An `<!ENTITY>` declaration does not match the XML 1.0 grammar.
case invalidEntityDeclaration(Mark)
/// An `<!ATTLIST>` declaration does not match the XML 1.0 grammar.
case invalidAttributeListDeclaration(Mark)
/// A public identifier literal contains a character outside `PubidChar`.
case invalidPublicIdentifier(Mark)
public var description: String {
switch self {
case .emptyDocument:
"document is empty"
case let .unexpectedEndOfInput(mark):
"unexpected end of input at \(mark)"
case let .unexpectedCharacter(character, mark):
"unexpected character '\(character)' at \(mark)"
case let .unterminatedTag(mark):
"unterminated tag starting at \(mark)"
case let .unterminatedComment(mark):
"unterminated comment starting at \(mark)"
case let .unterminatedCDATA(mark):
"unterminated CDATA section starting at \(mark)"
case let .mismatchedEndTag(expected, found, mark):
"expected </\(expected)> but found </\(found)> at \(mark)"
case let .duplicateAttribute(name, mark):
"duplicate attribute '\(name)' at \(mark)"
case let .expectedName(mark):
"expected a name at \(mark)"
case let .expectedEquals(mark):
"expected '=' after attribute name at \(mark)"
case let .unquotedAttributeValue(mark):
"attribute value must be quoted at \(mark)"
case let .invalidReference(reference, mark):
"invalid reference '\(reference)' at \(mark)"
case let .undefinedEntity(name, mark):
"entity '&\(name);' is not declared at \(mark)"
case let .recursiveEntity(name, mark):
"entity '&\(name);' refers to itself at \(mark)"
case let .amplificationLimitExceeded(mark):
"entity expansion exceeds the amplification limit at \(mark)"
case let .unexpectedEndTag(name, mark):
"unexpected end tag </\(name)> at \(mark)"
case let .undefinedNamespacePrefix(prefix, mark):
"namespace prefix '\(prefix)' is not bound at \(mark)"
case let .junkAfterDocumentElement(mark):
"content after the root element at \(mark)"
case let .nestingTooDeep(limit, mark):
"element nesting exceeds the limit of \(limit) at \(mark)"
case let .nameTooLong(limit, mark):
"name exceeds the length limit of \(limit) at \(mark)"
case let .contentTooLong(limit, mark):
"content exceeds the length limit of \(limit) at \(mark)"
case let .unsupportedDoctype(mark):
"DTD processing is disabled (DOCTYPE at \(mark))"
case .malformedEncoding:
"input bytes are malformed for the detected encoding"
case let .notImplemented(detail):
"not implemented: \(detail)"
case let .doubleHyphenInComment(mark):
"'--' is not allowed inside a comment at \(mark)"
case let .rawLessThanInAttribute(mark):
"raw '<' is not allowed in an attribute value at \(mark)"
case let .cdataCloseInContent(mark):
"']]>' must be escaped in character data at \(mark)"
case let .missingSpaceBeforeAttribute(mark):
"attributes must be separated by whitespace at \(mark)"
case let .invalidCharacter(mark):
"character is not allowed in XML content at \(mark)"
case let .reservedProcessingInstructionTarget(mark):
"the processing-instruction target 'xml' is reserved at \(mark)"
case let .invalidContentModel(element, mark):
"the content model of <!ELEMENT \(element)> is malformed at \(mark)"
case let .invalidEntityDeclaration(mark):
"malformed <!ENTITY> declaration at \(mark)"
case let .invalidAttributeListDeclaration(mark):
"malformed <!ATTLIST> declaration at \(mark)"
case let .invalidPublicIdentifier(mark):
"public identifier contains an illegal character at \(mark)"
case let .malformedDeclaration(mark):
"malformed XML declaration at \(mark)"
}
}
}
}