Skip to content

Commit abc1add

Browse files
committed
kyaml: Implement escaping closer to YAML spec
It's unclear if we actually need this. The round-trip test works without it. The spec describes escapes for things like space and forward-slash, which seem wrong to escape, so I may be misreading it.
1 parent 7749171 commit abc1add

File tree

2 files changed

+132
-20
lines changed

2 files changed

+132
-20
lines changed

kyaml/kyaml.go

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -237,33 +237,37 @@ const kyamlFoldStr = "\\\n"
237237
func (ky *Encoder) renderString(val string, indent int, flags flagMask, out io.Writer) error {
238238
lazyQuote := flags&flagLazyQuote != 0
239239
compact := flags&flagCompact != 0
240+
multi := strings.Contains(val, "\n")
240241

241-
// If no newlines, just use standard Go quoting.
242-
if compact || !strings.Contains(val, "\n") {
243-
if lazyQuote && !needsQuotes(val) {
244-
fmt.Fprint(out, val)
245-
} else {
246-
fmt.Fprint(out, strconv.Quote(val))
247-
}
242+
if !multi && lazyQuote && !needsQuotes(val) {
243+
fmt.Fprint(out, val)
248244
return nil
249245
}
250246

251-
// The input has at least one newline. We will use YAML's line folding to
252-
// make the output more readable.
247+
// What to print when we find a newline in the input.
248+
newline := "\\n"
249+
if !compact {
250+
// We use YAML's line folding to make the output more readable.
251+
newline += kyamlFoldStr
252+
}
253+
253254
//
254255
// The rest of this is borrowed from Go's strconv.Quote implementation.
255-
256-
s := val
256+
//
257257

258258
// accumulate into a buffer
259259
buf := &bytes.Buffer{}
260260

261-
// opening quote and fold
262-
fmt.Fprint(buf, `"`, kyamlFoldStr)
261+
// opening quote
262+
fmt.Fprint(buf, `"`)
263+
if multi && !compact {
264+
fmt.Fprint(buf, kyamlFoldStr)
265+
}
263266

264267
// Iterating a string with invalid UTF8 returns RuneError rather than the
265268
// bytes, so we iterate the string and decode the runes. This is a bit
266269
// slower, but gives us a better result.
270+
s := val
267271
for width := 0; len(s) > 0; s = s[width:] {
268272
r := rune(s[0])
269273
width = 1
@@ -275,15 +279,17 @@ func (ky *Encoder) renderString(val string, indent int, flags flagMask, out io.W
275279
fmt.Fprintf(buf, "%02x", s[0])
276280
continue
277281
}
278-
ky.appendEscapedRune(r, indent, buf)
282+
ky.appendEscapedRune(r, indent, newline, buf)
279283
}
280284

281285
// closing quote
282286
afterNewline := buf.Bytes()[len(buf.Bytes())-1] == '\n'
283-
if !afterNewline {
284-
fmt.Fprint(buf, kyamlFoldStr)
287+
if multi && !compact {
288+
if !afterNewline {
289+
fmt.Fprint(buf, kyamlFoldStr)
290+
}
291+
ky.writeIndent(indent, buf)
285292
}
286-
ky.writeIndent(indent, buf)
287293
fmt.Fprint(buf, `"`)
288294

289295
fmt.Fprint(out, buf.String())
@@ -433,7 +439,7 @@ func parseTimestamp(s string) (time.Time, bool) {
433439
}
434440

435441
// We use a buffer here so we can peek backwards.
436-
func (ky *Encoder) appendEscapedRune(r rune, indent int, buf *bytes.Buffer) {
442+
func (ky *Encoder) appendEscapedRune(r rune, indent int, newline string, buf *bytes.Buffer) {
437443
afterNewline := buf.Bytes()[len(buf.Bytes())-1] == '\n'
438444

439445
if afterNewline {
@@ -469,14 +475,25 @@ func (ky *Encoder) appendEscapedRune(r rune, indent int, buf *bytes.Buffer) {
469475
case '\f':
470476
buf.WriteString(`\f`)
471477
case '\n':
472-
buf.WriteString(`\n`)
473-
buf.WriteString(kyamlFoldStr)
478+
buf.WriteString(newline)
474479
case '\r':
475480
buf.WriteString(`\r`)
476481
case '\t':
477482
buf.WriteString(`\t`)
478483
case '\v':
479484
buf.WriteString(`\v`)
485+
case '\x00':
486+
buf.WriteString(`\0`)
487+
case '\x1b':
488+
buf.WriteString(`\e`)
489+
case '\x85':
490+
buf.WriteString(`\N`)
491+
case '\xa0':
492+
buf.WriteString(`\_`)
493+
case '\u2028':
494+
buf.WriteString(`\L`)
495+
case '\u2029':
496+
buf.WriteString(`\P`)
480497
default:
481498
const hexits = "0123456789abcdef"
482499
switch {

kyaml/kyaml_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1970,3 +1970,98 @@ func TestIsTypeAmbiguous(t *testing.T) {
19701970
})
19711971
}
19721972
}
1973+
1974+
func TestRenderStringEscapes(t *testing.T) {
1975+
simpleCases := []struct {
1976+
name string
1977+
input rune
1978+
expect string
1979+
}{{
1980+
name: "backslash",
1981+
input: '\\',
1982+
expect: `"\\"`,
1983+
}, {
1984+
name: "dquote",
1985+
input: '"',
1986+
expect: `"\""`,
1987+
}, {
1988+
name: "bell",
1989+
input: '\a',
1990+
expect: `"\a"`,
1991+
}, {
1992+
name: "backspace",
1993+
input: '\b',
1994+
expect: `"\b"`,
1995+
}, {
1996+
name: "ff",
1997+
input: '\f',
1998+
expect: `"\f"`,
1999+
}, {
2000+
name: "nl",
2001+
input: '\n',
2002+
expect: "\"\\\n \\n\\\n\"",
2003+
}, {
2004+
name: "cr",
2005+
input: '\r',
2006+
expect: `"\r"`,
2007+
}, {
2008+
name: "tab",
2009+
input: '\t',
2010+
expect: `"\t"`,
2011+
}, {
2012+
name: "vtab",
2013+
input: '\v',
2014+
expect: `"\v"`,
2015+
}, {
2016+
name: "null",
2017+
input: '\x00',
2018+
expect: `"\0"`,
2019+
}, {
2020+
name: "esc",
2021+
input: '\x1b',
2022+
expect: `"\e"`,
2023+
}, {
2024+
name: "nextline",
2025+
input: '\u0085',
2026+
expect: `"\N"`,
2027+
}, {
2028+
name: "nbsp",
2029+
input: '\u00a0',
2030+
expect: `"\_"`,
2031+
}, {
2032+
name: "linesep",
2033+
input: '\u2028',
2034+
expect: `"\L"`,
2035+
}, {
2036+
name: "parasep",
2037+
input: '\u2029',
2038+
expect: `"\P"`,
2039+
}, {
2040+
name: "x01",
2041+
input: '\x01',
2042+
expect: `"\x01"`,
2043+
}, {
2044+
name: "uffff",
2045+
input: '\uffff',
2046+
expect: `"\uffff"`,
2047+
}, {
2048+
name: "U0010ffff",
2049+
input: '\U0010ffff',
2050+
expect: `"\U0010ffff"`,
2051+
}}
2052+
2053+
for _, tt := range simpleCases {
2054+
t.Run(tt.name, func(t *testing.T) {
2055+
ky := &Encoder{}
2056+
buf := &bytes.Buffer{}
2057+
err := ky.renderString(string(tt.input), 0, 0, buf)
2058+
if err != nil {
2059+
t.Fatalf("renderString(%q) returned error: %v", tt.input, err)
2060+
}
2061+
if result := buf.String(); result != tt.expect {
2062+
t.Errorf("renderString(%q): want %q, got %q", tt.input, tt.expect, result)
2063+
}
2064+
})
2065+
}
2066+
2067+
}

0 commit comments

Comments
 (0)