Skip to content

Commit d46fbd4

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 c6c1b6b commit d46fbd4

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
@@ -220,33 +220,37 @@ const kyamlFoldStr = "\\\n"
220220
func (ky *Encoder) renderString(val string, indent int, flags flagMask, out io.Writer) error {
221221
lazyQuote := flags&flagLazyQuote != 0
222222
compact := flags&flagCompact != 0
223+
multi := strings.Contains(val, "\n")
223224

224-
// If no newlines, just use standard Go quoting.
225-
if compact || !strings.Contains(val, "\n") {
226-
if lazyQuote && !needsQuotes(val) {
227-
fmt.Fprint(out, val)
228-
} else {
229-
fmt.Fprint(out, strconv.Quote(val))
230-
}
225+
if !multi && lazyQuote && !needsQuotes(val) {
226+
fmt.Fprint(out, val)
231227
return nil
232228
}
233229

234-
// The input has at least one newline. We will use YAML's line folding to
235-
// make the output more readable.
230+
// What to print when we find a newline in the input.
231+
newline := "\\n"
232+
if !compact {
233+
// We use YAML's line folding to make the output more readable.
234+
newline += kyamlFoldStr
235+
}
236+
236237
//
237238
// The rest of this is borrowed from Go's strconv.Quote implementation.
238-
239-
s := val
239+
//
240240

241241
// accumulate into a buffer
242242
buf := &bytes.Buffer{}
243243

244-
// opening quote and fold
245-
fmt.Fprint(buf, `"`, kyamlFoldStr)
244+
// opening quote
245+
fmt.Fprint(buf, `"`)
246+
if multi && !compact {
247+
fmt.Fprintf(buf, kyamlFoldStr)
248+
}
246249

247250
// Iterating a string with invalid UTF8 returns RuneError rather than the
248251
// bytes, so we iterate the string and decode the runes. This is a bit
249252
// slower, but gives us a better result.
253+
s := val
250254
for width := 0; len(s) > 0; s = s[width:] {
251255
r := rune(s[0])
252256
width = 1
@@ -258,15 +262,17 @@ func (ky *Encoder) renderString(val string, indent int, flags flagMask, out io.W
258262
fmt.Fprintf(buf, "%02x", s[0])
259263
continue
260264
}
261-
ky.appendEscapedRune(r, indent, buf)
265+
ky.appendEscapedRune(r, indent, newline, buf)
262266
}
263267

264268
// closing quote
265269
afterNewline := buf.Bytes()[len(buf.Bytes())-1] == '\n'
266-
if !afterNewline {
267-
fmt.Fprint(buf, kyamlFoldStr)
270+
if multi && !compact {
271+
if !afterNewline {
272+
fmt.Fprint(buf, kyamlFoldStr)
273+
}
274+
ky.writeIndent(indent, buf)
268275
}
269-
ky.writeIndent(indent, buf)
270276
fmt.Fprint(buf, `"`)
271277

272278
fmt.Fprint(out, buf.String())
@@ -415,7 +421,7 @@ func parseTimestamp(s string) (time.Time, bool) {
415421
}
416422

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

421427
if afterNewline {
@@ -451,14 +457,25 @@ func (ky *Encoder) appendEscapedRune(r rune, indent int, buf *bytes.Buffer) {
451457
case '\f':
452458
buf.WriteString(`\f`)
453459
case '\n':
454-
buf.WriteString(`\n`)
455-
buf.WriteString(kyamlFoldStr)
460+
buf.WriteString(newline)
456461
case '\r':
457462
buf.WriteString(`\r`)
458463
case '\t':
459464
buf.WriteString(`\t`)
460465
case '\v':
461466
buf.WriteString(`\v`)
467+
case '\x00':
468+
buf.WriteString(`\0`)
469+
case '\x1b':
470+
buf.WriteString(`\e`)
471+
case '\x85':
472+
buf.WriteString(`\N`)
473+
case '\xa0':
474+
buf.WriteString(`\_`)
475+
case '\u2028':
476+
buf.WriteString(`\L`)
477+
case '\u2029':
478+
buf.WriteString(`\P`)
462479
default:
463480
const hexits = "0123456789abcdef"
464481
switch {

kyaml/kyaml_test.go

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

0 commit comments

Comments
 (0)