From 54c6032a9a41ffcd4c3d185f27252fa4c5b15067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Thu, 17 Sep 2026 23:06:59 +0200 Subject: [PATCH] fix(encode): propagate invalid UTF-8 key errors from headers and inline tables Assisted-by: GLM 5.3 --- CHANGELOG.md | 4 ++++ encode.go | 48 +++++++++++++++++++++++++++++++----------------- encode_test.go | 11 +++++++++++ 3 files changed, 46 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49e0513..3ced0e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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. +- `Marshal` returns an error for a table header key or an inline-table key that + is not valid UTF-8, the way scalar keys already did, instead of silently + emitting corrupt TOML (a header that lost its key, an inline table with a + missing key). - 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 diff --git a/encode.go b/encode.go index 8537add..f2318eb 100644 --- a/encode.go +++ b/encode.go @@ -579,7 +579,9 @@ func (e *encoder) emitDoc(doc *tomlDoc, prefix []string) error { path := append(append([]string{}, prefix...), t.key) e.writeBlankLine() e.buf.WriteByte('[') - writeKeyPath(&e.buf, path) + if err := e.writeKeyPath(path); err != nil { + return err + } e.buf.WriteString("]\n") if err := e.emitDoc(t.doc, path); err != nil { return err @@ -590,7 +592,9 @@ func (e *encoder) emitDoc(doc *tomlDoc, prefix []string) error { for _, sub := range a.docs { e.writeBlankLine() e.buf.WriteString("[[") - writeKeyPath(&e.buf, path) + if err := e.writeKeyPath(path); err != nil { + return err + } e.buf.WriteString("]]\n") if err := e.emitDoc(sub, path); err != nil { return err @@ -615,7 +619,9 @@ func (e *encoder) emitDoc(doc *tomlDoc, prefix []string) error { path := append(append([]string{}, prefix...), ent.key) e.writeBlankLine() e.buf.WriteByte('[') - writeKeyPath(&e.buf, path) + if err := e.writeKeyPath(path); err != nil { + return err + } e.buf.WriteString("]\n") if err := e.emitDoc(ent.doc, path); err != nil { return err @@ -625,7 +631,9 @@ func (e *encoder) emitDoc(doc *tomlDoc, prefix []string) error { for _, sub := range ent.docs { e.writeBlankLine() e.buf.WriteString("[[") - writeKeyPath(&e.buf, path) + if err := e.writeKeyPath(path); err != nil { + return err + } e.buf.WriteString("]]\n") if err := e.emitDoc(sub, path); err != nil { return err @@ -637,10 +645,9 @@ func (e *encoder) emitDoc(doc *tomlDoc, prefix []string) error { } func (e *encoder) writeKV(key string, val any) error { - if !utf8.ValidString(key) { - return fmt.Errorf("interpres: key %q is not valid UTF-8", key) + if err := e.writeKey(key); err != nil { + return err } - e.writeKey(key) e.buf.WriteString(" = ") if err := e.writeValue(val); err != nil { return err @@ -649,25 +656,30 @@ func (e *encoder) writeKV(key string, val any) error { return nil } -func writeKeyPath(buf *bytes.Buffer, path []string) { +func (e *encoder) writeKeyPath(path []string) error { for i, p := range path { if i > 0 { - buf.WriteByte('.') + e.buf.WriteByte('.') } - if isBareKey(p) { - buf.WriteString(p) - continue + if err := e.writeKey(p); err != nil { + return err } - writeQuotedString(buf, p) } + return nil } -func (e *encoder) writeKey(key string) { +// writeKey writes one key, bare when it qualifies and quoted otherwise. A key +// that is not valid UTF-8 is an error; writing it anyway would emit corrupt +// TOML, because the quoted form has no representation for it. +func (e *encoder) writeKey(key string) error { if isBareKey(key) { e.buf.WriteString(key) - return + return nil } - writeQuotedString(&e.buf, key) + if !utf8.ValidString(key) { + return fmt.Errorf("interpres: key %q is not valid UTF-8", key) + } + return writeQuotedString(&e.buf, key) } // writeQuotedString writes s as a TOML basic string (double-quoted) to buf. @@ -787,7 +799,9 @@ func (e *encoder) writeInlineTable(m map[string]any) error { if i > 0 { e.buf.WriteString(", ") } - e.writeKey(k) + if err := e.writeKey(k); err != nil { + return err + } e.buf.WriteString(" = ") if err := e.writeValue(m[k]); err != nil { return err diff --git a/encode_test.go b/encode_test.go index adb4e18..90652f3 100644 --- a/encode_test.go +++ b/encode_test.go @@ -1128,6 +1128,17 @@ func TestMarshalKeyRequiresUTF8(t *testing.T) { if _, err := Marshal(m); err == nil { t.Errorf("expected error for invalid UTF-8 key") } + // The check must reach the keys of table headers and of inline tables + // nested inside value arrays, not only scalar keys: both write keys + // through the same path. + nested := map[string]any{"\xff": map[string]any{"k": "v"}} + if _, err := Marshal(nested); err == nil { + t.Errorf("expected error for invalid UTF-8 table header key") + } + inline := map[string]any{"mix": []any{1, map[string]any{"\xff": 1}}} + if _, err := Marshal(inline); err == nil { + t.Errorf("expected error for invalid UTF-8 inline table key") + } } func TestMarshalStringRequiresUTF8(t *testing.T) {