fix(encode): fall back from the literal multiline form when unsafe
Assisted-by: GLM 5.3
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user