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
|
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
|
the TOML examples in the spec shape it. The records of the previous element
|
||||||
falsely rejected the same paths in the next one.
|
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;
|
- A float with an exponent marker but no digits (`1e`, `0.0E`) is rejected;
|
||||||
the exponent requires at least one digit.
|
the exponent requires at least one digit.
|
||||||
- A date-time offset outside 00:00 through 23:59 is rejected; such offsets
|
- 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
|
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
|
### Cancellation
|
||||||
|
|
||||||
|
|||||||
@@ -798,12 +798,40 @@ func (e *encoder) writeInlineTable(m map[string]any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *encoder) writeStringVal(s string) 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 writeLiteralMultilineString(&e.buf, s)
|
||||||
}
|
}
|
||||||
return writeQuotedString(&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,
|
// writeLiteralMultilineString writes s as a TOML literal multi-line string,
|
||||||
// surrounded by triple single quotes. The opening delimiter is followed by a
|
// surrounded by triple single quotes. The opening delimiter is followed by a
|
||||||
// newline that the reader trims, so we always include one. The closing
|
// 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.
|
// marshalerFunc adapts a plain function value to the Marshaler interface.
|
||||||
// Tests use it to express "this field produces this TOML value" without a
|
// Tests use it to express "this field produces this TOML value" without a
|
||||||
// dedicated struct definition.
|
// dedicated struct definition.
|
||||||
|
|||||||
Reference in New Issue
Block a user