PureYAML/Tests/DoubleQuotedEscapeTests.swift
Mihaela M b7bcfe1a01 fix(parsing): enforce the full YAML 1.2 double-quoted escape table
The double-quoted escape decoder recognized only a subset of escapes
(" \ \/ \b \f \n \r \t \u) and silently passed any other character
through. That accepted invalid escapes such as \. and \', and mangled
valid ones the subset omitted: \x and \U hex escapes, \v \e \0, the named
\N \_ \L \P, and a backslash before a space, were emitted as their literal
letters (\x0d became the text x0d).

The decoder now maps every escape in the YAML 1.2 table to its character,
reads \x (8-bit) and \U (32-bit) hex escapes alongside \u, treats a
backslash before a line break as an escaped line break that joins the
lines with no separator, and rejects an unknown or truncated escape with
a diagnostic. Single-character escapes move to a lookup table so the
decoder stays within the complexity limit.

Measured against the official YAML test-suite, parse-level conformance
rises from 247/333 (74.2%) to 249/333 (74.8%) as two invalid documents
are now rejected, and value-level from 166 to 170 of comparable cases
(90.4%) as the previously-mangled escapes decode correctly, with no
regression.
2026-06-27 16:19:25 +02:00

51 lines
1.7 KiB
Swift

@testable import PureYAML
import Testing
@Suite("Double-quoted escape sequences")
struct DoubleQuotedEscapeTests {
@Test("Every YAML 1.2 escape decodes to its character", arguments: [
("\"\\0\"\n", "\u{00}"),
("\"\\a\"\n", "\u{07}"),
("\"\\b\"\n", "\u{08}"),
("\"\\t\"\n", "\t"),
("\"\\n\"\n", "\n"),
("\"\\v\"\n", "\u{0B}"),
("\"\\f\"\n", "\u{0C}"),
("\"\\r\"\n", "\r"),
("\"\\e\"\n", "\u{1B}"),
("\"a\\ b\"\n", "a b"),
("\"\\\"\"\n", "\""),
("\"\\/\"\n", "/"),
("\"\\\\\"\n", "\\"),
("\"\\N\"\n", "\u{85}"),
("\"\\_\"\n", "\u{A0}"),
("\"\\L\"\n", "\u{2028}"),
("\"\\P\"\n", "\u{2029}"),
("\"\\x41\"\n", "A"),
("\"\\u0041\"\n", "A"),
("\"\\U0001F600\"\n", "\u{1F600}"),
])
func test_validEscapeDecodes(_ yaml: String, _ expected: String) throws {
#expect(try PureYAML.parse(yaml) == .string(expected))
}
@Test("An unknown or truncated escape is rejected", arguments: [
"\"\\.\"\n", // not an escape character
"\"\\'\"\n", // single quote is not escaped in a double-quoted scalar
"\"\\q\"\n", // unknown letter
"\"\\x4\"\n", // truncated hex
"\"\\U12\"\n", // truncated 32-bit hex
])
func test_invalidEscapeIsRejected(_ yaml: String) throws {
#expect(throws: PureYAML.Parsing.ParseError.self) {
try PureYAML.parse(yaml)
}
}
@Test("An escaped line break joins the lines with no separator")
func test_escapedLineBreakJoinsLines() throws {
#expect(try PureYAML.parse("\"a\\\n b\"\n") == .string("ab"))
#expect(try PureYAML.parse("\"one \\\n two\"\n") == .string("one two"))
}
}