mirror of
https://github.com/apple/pkl.git
synced 2026-09-09 19:31:51 +02:00
Escape characters that YAML cannot carry literally (#1846)
YamlEmitter only routed a string into YamlEscaper when it held a character below 0x20, so 0x7F, the C1 block, U+2028 and U+2029 went out as plain scalars. They are not `c-printable` (YAML 1.2 spec 5.1), and NEL, LS and PS are line breaks in YAML 1.1, so the rendered document is either rejected or silently split by a reader. The escapes added in #1165 for 0x85, 0xA0, U+2028, and U+2029 could only fire when some other character happened to open that gate. Share one predicate between the two sites that decide, and give 0x7F-0x9F the 8-bit escape they lack.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -42,6 +42,17 @@ public abstract class YamlEmitter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells if {@code ch} has to go through {@link YamlEscaper} instead of being emitted literally.
|
||||||
|
*
|
||||||
|
* <p>0x7F, the C1 block, U+FFFE and U+FFFF are outside {@code c-printable} (YAML 1.2 spec 5.1),
|
||||||
|
* and NEL, LS and PS are line breaks in YAML 1.1. Note that 0x0A is handled by its own {@code
|
||||||
|
* case} in {@link #emit(String, StringBuilder, boolean)} and never reaches here.
|
||||||
|
*/
|
||||||
|
private static boolean getNeedsEscaping(char ch) {
|
||||||
|
return ch < 0x20 || ch >= 0x7F && ch <= 0x9F || ch == 0x2028 || ch == 0x2029 || ch >= 0xFFFE;
|
||||||
|
}
|
||||||
|
|
||||||
public void emit(String str, StringBuilder currIndent, boolean isKey) {
|
public void emit(String str, StringBuilder currIndent, boolean isKey) {
|
||||||
if (isReservedWord(str)) {
|
if (isReservedWord(str)) {
|
||||||
emitSingleQuotedString(str, -1);
|
emitSingleQuotedString(str, -1);
|
||||||
@@ -102,7 +113,7 @@ public abstract class YamlEmitter {
|
|||||||
case 'o':
|
case 'o':
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
needsEscaping = first < 0x20;
|
needsEscaping = getNeedsEscaping(first);
|
||||||
hasNonNumberChar = true;
|
hasNonNumberChar = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +180,7 @@ public abstract class YamlEmitter {
|
|||||||
'o',
|
'o',
|
||||||
'x' -> {}
|
'x' -> {}
|
||||||
default -> {
|
default -> {
|
||||||
needsEscaping = needsEscaping || ch < 0x20;
|
needsEscaping = needsEscaping || getNeedsEscaping(ch);
|
||||||
hasNonNumberChar = true;
|
hasNonNumberChar = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ public final class YamlEscaper extends AbstractCharEscaper {
|
|||||||
for (var i = 0; i < 0x20; i++) {
|
for (var i = 0; i < 0x20; i++) {
|
||||||
REPLACEMENTS[i] = IoUtils.toHexEscape(i);
|
REPLACEMENTS[i] = IoUtils.toHexEscape(i);
|
||||||
}
|
}
|
||||||
|
// 0x7F and the C1 block are not `c-printable` either (spec 5.1), so they cannot be emitted
|
||||||
|
// literally. ns-esc-8-bit covers them; 0x85 is overridden with its named escape below.
|
||||||
|
for (var i = 0x7F; i <= 0x9F; i++) {
|
||||||
|
REPLACEMENTS[i] = IoUtils.toHexEscape(i);
|
||||||
|
}
|
||||||
// ns-esc-null
|
// ns-esc-null
|
||||||
REPLACEMENTS[0x00] = "\\0";
|
REPLACEMENTS[0x00] = "\\0";
|
||||||
// ns-esc-bell
|
// ns-esc-bell
|
||||||
@@ -68,6 +73,14 @@ public final class YamlEscaper extends AbstractCharEscaper {
|
|||||||
@Override
|
@Override
|
||||||
protected @Nullable String findReplacement(char ch) {
|
protected @Nullable String findReplacement(char ch) {
|
||||||
//noinspection UnnecessaryUnicodeEscape
|
//noinspection UnnecessaryUnicodeEscape
|
||||||
return ch <= 0xA0 ? REPLACEMENTS[ch] : ch == '\u2028' ? "\\L" : ch == '\u2029' ? "\\P" : null;
|
return ch <= 0xA0
|
||||||
|
? REPLACEMENTS[ch]
|
||||||
|
: ch == '\u2028'
|
||||||
|
? "\\L"
|
||||||
|
: ch == '\u2029'
|
||||||
|
? "\\P"
|
||||||
|
// U+FFFE and U+FFFF are not `c-printable` either (spec 5.1), and no named or 8-bit
|
||||||
|
// escape covers them; ns-esc-16-bit does.
|
||||||
|
: ch >= 0xFFFE ? IoUtils.toUnicodeEscape(ch) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -39,6 +39,17 @@ h4 = "one\rtwo"
|
|||||||
h5 = "\u{7}"
|
h5 = "\u{7}"
|
||||||
h6 = "one\u{7}two"
|
h6 = "one\u{7}two"
|
||||||
|
|
||||||
|
// characters outside `c-printable` (YAML 1.2 spec 5.1), and YAML 1.1 line breaks
|
||||||
|
h7 = "\u{7f}"
|
||||||
|
h8 = "one\u{7f}two"
|
||||||
|
h9 = "one\u{80}two"
|
||||||
|
h10 = "one\u{85}two"
|
||||||
|
h11 = "one\u{9f}two"
|
||||||
|
h12 = "one\u{2028}two"
|
||||||
|
h13 = "one\u{2029}two"
|
||||||
|
h14 = "one\u{fffe}two"
|
||||||
|
h15 = "one\u{ffff}two"
|
||||||
|
|
||||||
// control characters in multiline strings
|
// control characters in multiline strings
|
||||||
i1 = "one\ttwo\nthree"
|
i1 = "one\ttwo\nthree"
|
||||||
i2 = "one\rtwo\nthree"
|
i2 = "one\rtwo\nthree"
|
||||||
|
|||||||
+9
@@ -53,6 +53,15 @@ h3: "\r"
|
|||||||
h4: "one\rtwo"
|
h4: "one\rtwo"
|
||||||
h5: "\a"
|
h5: "\a"
|
||||||
h6: "one\atwo"
|
h6: "one\atwo"
|
||||||
|
h7: "\x7f"
|
||||||
|
h8: "one\x7ftwo"
|
||||||
|
h9: "one\x80two"
|
||||||
|
h10: "one\Ntwo"
|
||||||
|
h11: "one\x9ftwo"
|
||||||
|
h12: "one\Ltwo"
|
||||||
|
h13: "one\Ptwo"
|
||||||
|
h14: "one\ufffetwo"
|
||||||
|
h15: "one\ufffftwo"
|
||||||
i1: "one\ttwo\nthree"
|
i1: "one\ttwo\nthree"
|
||||||
i2: "one\rtwo\nthree"
|
i2: "one\rtwo\nthree"
|
||||||
i3: "one\atwo\nthree"
|
i3: "one\atwo\nthree"
|
||||||
|
|||||||
Reference in New Issue
Block a user