fix(emitting): escape CRLF per unicode scalar in double-quoted output
A CR+LF pair forms a single Swift extended grapheme cluster, so quote()'s Character-level switch matched neither '\\r' nor '\\n' and emitted the line break unescaped. The folding parser then read the dumped value back differently, breaking parse/dump/parse stability for any string containing CRLF (surfaced as a round-trip failure on the Zoom OpenAPI corpus seed under PUREYAML_RUN_FULL_CORPUS). Iterating over unicode scalars escapes CR and LF independently. Adds a dumper regression test.
This commit is contained in:
parent
1de1fcf2a8
commit
318913baeb
@ -9,6 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- Escape carriage returns and line feeds in double-quoted output per unicode
|
||||
scalar rather than per Character. A CR+LF pair forms a single Swift extended
|
||||
grapheme cluster, so the previous Character-level escape switch matched neither
|
||||
`\r` nor `\n` and emitted the pair unescaped; the folding parser then read it
|
||||
back differently, breaking parse/dump/parse stability for values containing
|
||||
CRLF (observed on the Zoom OpenAPI corpus seed). Added a dumper regression test.
|
||||
|
||||
- Tightened error detection so 15 more official YAML test suite cases are handled
|
||||
correctly, repairing four error-detection regressions and removing them from the
|
||||
conformance gap manifest. Tabs are now rejected when they would supply
|
||||
|
||||
@ -356,9 +356,13 @@ extension PureYAML.Emitting.Dumper {
|
||||
}
|
||||
|
||||
func quote(_ value: String) -> String {
|
||||
// Iterate over unicode scalars, not Characters: a CR+LF pair forms a
|
||||
// single extended grapheme cluster in Swift, so a Character-level switch
|
||||
// would match neither "\r" nor "\n" and emit the line break unescaped,
|
||||
// which does not round-trip through the folding parser.
|
||||
var output = "\""
|
||||
for character in value {
|
||||
switch character {
|
||||
for scalar in value.unicodeScalars {
|
||||
switch scalar {
|
||||
case "\"":
|
||||
output += "\\\""
|
||||
case "\\":
|
||||
@ -370,7 +374,7 @@ extension PureYAML.Emitting.Dumper {
|
||||
case "\t":
|
||||
output += "\\t"
|
||||
default:
|
||||
output.append(character)
|
||||
output.unicodeScalars.append(scalar)
|
||||
}
|
||||
}
|
||||
output += "\""
|
||||
|
||||
@ -275,3 +275,21 @@ struct DumperTests {
|
||||
#expect(dumper.dump(value) == PureYAML.dump(value))
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Dumper Line Break Escaping")
|
||||
struct DumperLineBreakEscapingTests {
|
||||
@Test("Escapes CR and LF in a CRLF pair so the value round-trips")
|
||||
func test_escapesCarriageReturnLineFeedPair() throws {
|
||||
// A CR+LF pair is a single Swift Character (extended grapheme cluster), so a
|
||||
// Character-level escape switch would emit it unescaped, and the folding
|
||||
// parser would not read it back. Each line break must escape independently.
|
||||
let value = PureYAML.Model.Value.string("a\r\nb\r\n\r\nc")
|
||||
let dumped = PureYAML.dump(value)
|
||||
|
||||
#expect(dumped == "\"a\\r\\nb\\r\\n\\r\\nc\"\n")
|
||||
#expect(!dumped.unicodeScalars.contains("\r"))
|
||||
|
||||
let reparsed = try PureYAML.parse(dumped)
|
||||
#expect(reparsed == value)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user