fix(encode): fall back from the literal multiline form when unsafe

Assisted-by: GLM 5.3
This commit is contained in:
2026-09-17 23:06:25 +02:00
parent feef4fe9ea
commit ec0d7a0023
4 changed files with 69 additions and 2 deletions
+4
View File
@@ -50,6 +50,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
and nested arrays of tables: `[[a]]`, `b.c = 1`, `[[a]]`, `[a.b]` parses, as
the TOML examples in the spec shape it. The records of the previous element
falsely rejected the same paths in the next one.
- `UseLiteralMultiline` falls back to the escaped basic string when the value
cannot be carried verbatim by the literal form: a run of three single quotes,
a control character, or a lone carriage return. Such values previously
produced output that did not re-parse.
- A float with an exponent marker but no digits (`1e`, `0.0E`) is rejected;
the exponent requires at least one digit.
- A date-time offset outside 00:00 through 23:59 is rejected; such offsets
+4 -1
View File
@@ -351,7 +351,10 @@ out, err := interpres.NewEncoder().UseLiteralMultiline(80).Marshal(cfg)
```
Single-line strings keep the basic form regardless of the threshold, and a
threshold of `0` or less disables the option.
threshold of `0` or less disables the option. A string the literal form cannot
carry verbatim (an embedded run of three single quotes, a control character
other than tab, or a carriage return outside a CRLF pair) also keeps the basic
form, so the output always re-parses to the same value.
### Cancellation
+29 -1
View File
@@ -798,12 +798,40 @@ func (e *encoder) writeInlineTable(m map[string]any) error {
}
func (e *encoder) writeStringVal(s string) error {
if e.opts.literalMultilineAt > 0 && strings.ContainsRune(s, '\n') && len(s) >= e.opts.literalMultilineAt {
if e.opts.literalMultilineAt > 0 && strings.ContainsRune(s, '\n') &&
len(s) >= e.opts.literalMultilineAt && canBeLiteralMultiline(s) {
return writeLiteralMultilineString(&e.buf, s)
}
return writeQuotedString(&e.buf, s)
}
// canBeLiteralMultiline reports whether s can be carried verbatim by the
// literal ”'...”' form: the form has no escapes, so a run of three single
// quotes would close the delimiter early, and control characters beyond tab,
// and a carriage return outside a CRLF pair, have no representation at all.
// Anything else falls back to the escaped basic string.
func canBeLiteralMultiline(s string) bool {
if strings.Contains(s, "'''") {
return false
}
for i := 0; i < len(s); {
r, size := utf8.DecodeRuneInString(s[i:])
switch {
case r == '\t' || r == '\n':
case r == '\r':
if !strings.HasPrefix(s[i+size:], "\n") {
return false
}
default:
if r < 0x20 || r == 0x7f {
return false
}
}
i += size
}
return true
}
// writeLiteralMultilineString writes s as a TOML literal multi-line string,
// surrounded by triple single quotes. The opening delimiter is followed by a
// newline that the reader trims, so we always include one. The closing
+32
View File
@@ -267,6 +267,38 @@ func TestEncoderUseLiteralMultilineThresholdZero(t *testing.T) {
}
}
func TestEncoderLiteralMultilineFallsBackWhenUnsafe(t *testing.T) {
// The literal form carries the value verbatim, so content it cannot
// represent must fall back to the escaped basic string instead of
// producing output that does not re-parse.
cases := []struct {
name string
in string
}{
{"embedded delimiter", "before ''' after\nsecond line"},
{"control character", "a\x01b\nsecond"},
{"delete character", "a\x7fb\nsecond"},
{"lone carriage return", "first\rsecond\nthird"},
}
for _, c := range cases {
out, err := NewEncoder().UseLiteralMultiline(5).Marshal(map[string]any{"s": c.in})
if err != nil {
t.Fatalf("%s: marshal: %v", c.name, err)
}
if !bytes.HasPrefix(out, []byte("s = \"")) {
t.Errorf("%s: expected the basic quoted form, got:\n%s", c.name, out)
}
re, err := Parse(out)
if err != nil {
t.Errorf("%s: re-parse: %v\ndoc:\n%s", c.name, err, out)
continue
}
if re["s"] != c.in {
t.Errorf("%s: round-trip changed the value: %q", c.name, re["s"])
}
}
}
// marshalerFunc adapts a plain function value to the Marshaler interface.
// Tests use it to express "this field produces this TOML value" without a
// dedicated struct definition.