Fix TOML encoder to quote keys containing special characters (#2648)

* Initial plan

* Fix TOML encoder to quote keys with special characters

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/b2b52954-d13f-4e67-831a-16fdd3378de5

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

* Add test for dotted table section header with special character key

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/12c783dd-8b7f-43bf-b71a-e7a0b5e55fea

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

* Apply De Morgan's law to tomlKey condition to fix staticcheck QF1001

Agent-Logs-Url: https://github.com/mikefarah/yq/sessions/eeab0316-309f-418f-b357-11bbacffb471

Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
This commit is contained in:
Copilot 2026-04-12 14:27:20 +10:00 committed by GitHub
parent 8c018da9c9
commit c47fe40a30
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 70 additions and 12 deletions

View File

@ -69,6 +69,27 @@ func (te *tomlEncoder) CanHandleAliases() bool {
// ---- helpers ---- // ---- helpers ----
// tomlKey returns the key quoted if it contains characters that are not valid
// in a TOML bare key. TOML bare keys may only contain ASCII letters, ASCII
// digits, underscores, and dashes.
func tomlKey(key string) string {
for _, r := range key {
if (r < 'A' || r > 'Z') && (r < 'a' || r > 'z') && (r < '0' || r > '9') && r != '_' && r != '-' {
return fmt.Sprintf("%q", key)
}
}
return key
}
// tomlDottedKey joins path components, quoting any that require it.
func tomlDottedKey(path []string) string {
parts := make([]string, len(path))
for i, p := range path {
parts[i] = tomlKey(p)
}
return strings.Join(parts, ".")
}
func (te *tomlEncoder) writeComment(w io.Writer, comment string) error { func (te *tomlEncoder) writeComment(w io.Writer, comment string) error {
if comment == "" { if comment == "" {
return nil return nil
@ -148,9 +169,10 @@ func (te *tomlEncoder) encodeTopLevelEntry(w io.Writer, path []string, node *Can
} }
if allMaps { if allMaps {
key := path[len(path)-1] key := path[len(path)-1]
quotedKey := tomlKey(key)
for _, it := range node.Content { for _, it := range node.Content {
// [[key]] then body // [[key]] then body
if _, err := w.Write([]byte("[[" + key + "]]\n")); err != nil { if _, err := w.Write([]byte("[[" + quotedKey + "]]\n")); err != nil {
return err return err
} }
if err := te.encodeMappingBodyWithPath(w, []string{key}, it); err != nil { if err := te.encodeMappingBodyWithPath(w, []string{key}, it); err != nil {
@ -185,7 +207,7 @@ func (te *tomlEncoder) writeAttribute(w io.Writer, key string, value *CandidateN
} }
// Write the attribute // Write the attribute
line := key + " = " + te.formatScalar(value) line := tomlKey(key) + " = " + te.formatScalar(value)
// Add line comment if present // Add line comment if present
if value.LineComment != "" { if value.LineComment != "" {
@ -210,7 +232,7 @@ func (te *tomlEncoder) writeArrayAttribute(w io.Writer, key string, seq *Candida
// Handle empty arrays // Handle empty arrays
if len(seq.Content) == 0 { if len(seq.Content) == 0 {
line := key + " = []" line := tomlKey(key) + " = []"
if seq.LineComment != "" { if seq.LineComment != "" {
lineComment := strings.TrimSpace(seq.LineComment) lineComment := strings.TrimSpace(seq.LineComment)
if !strings.HasPrefix(lineComment, "#") { if !strings.HasPrefix(lineComment, "#") {
@ -233,7 +255,7 @@ func (te *tomlEncoder) writeArrayAttribute(w io.Writer, key string, seq *Candida
if hasElementComments { if hasElementComments {
// Write multiline array format with comments // Write multiline array format with comments
if _, err := w.Write([]byte(key + " = [\n")); err != nil { if _, err := w.Write([]byte(tomlKey(key) + " = [\n")); err != nil {
return err return err
} }
@ -324,7 +346,7 @@ func (te *tomlEncoder) writeArrayAttribute(w io.Writer, key string, seq *Candida
} }
} }
line := key + " = [" + strings.Join(items, ", ") + "]" line := tomlKey(key) + " = [" + strings.Join(items, ", ") + "]"
// Add line comment if present // Add line comment if present
if seq.LineComment != "" { if seq.LineComment != "" {
@ -372,21 +394,21 @@ func (te *tomlEncoder) mappingToInlineTable(m *CandidateNode) (string, error) {
v := m.Content[i+1] v := m.Content[i+1]
switch v.Kind { switch v.Kind {
case ScalarNode: case ScalarNode:
parts = append(parts, fmt.Sprintf("%s = %s", k, te.formatScalar(v))) parts = append(parts, fmt.Sprintf("%s = %s", tomlKey(k), te.formatScalar(v)))
case SequenceNode: case SequenceNode:
// inline array in inline table // inline array in inline table
arr, err := te.sequenceToInlineArray(v) arr, err := te.sequenceToInlineArray(v)
if err != nil { if err != nil {
return "", err return "", err
} }
parts = append(parts, fmt.Sprintf("%s = %s", k, arr)) parts = append(parts, fmt.Sprintf("%s = %s", tomlKey(k), arr))
case MappingNode: case MappingNode:
// nested inline table // nested inline table
inline, err := te.mappingToInlineTable(v) inline, err := te.mappingToInlineTable(v)
if err != nil { if err != nil {
return "", err return "", err
} }
parts = append(parts, fmt.Sprintf("%s = %s", k, inline)) parts = append(parts, fmt.Sprintf("%s = %s", tomlKey(k), inline))
default: default:
return "", fmt.Errorf("unsupported inline table value kind: %v", v.Kind) return "", fmt.Errorf("unsupported inline table value kind: %v", v.Kind)
} }
@ -399,7 +421,7 @@ func (te *tomlEncoder) writeInlineTableAttribute(w io.Writer, key string, m *Can
if err != nil { if err != nil {
return err return err
} }
_, err = w.Write([]byte(key + " = " + inline + "\n")) _, err = w.Write([]byte(tomlKey(key) + " = " + inline + "\n"))
return err return err
} }
@ -421,7 +443,7 @@ func (te *tomlEncoder) writeTableHeader(w io.Writer, path []string, m *Candidate
} }
// Write table header [a.b.c] // Write table header [a.b.c]
header := "[" + strings.Join(path, ".") + "]\n" header := "[" + tomlDottedKey(path) + "]\n"
_, err := w.Write([]byte(header)) _, err := w.Write([]byte(header))
return err return err
} }
@ -488,7 +510,7 @@ func (te *tomlEncoder) encodeSeparateMapping(w io.Writer, path []string, m *Cand
} }
} }
if allMaps { if allMaps {
key := strings.Join(append(append([]string{}, path...), k), ".") key := tomlDottedKey(append(append([]string{}, path...), k))
for _, it := range v.Content { for _, it := range v.Content {
if _, err := w.Write([]byte("[[" + key + "]]\n")); err != nil { if _, err := w.Write([]byte("[[" + key + "]]\n")); err != nil {
return err return err
@ -586,7 +608,7 @@ func (te *tomlEncoder) encodeMappingBodyWithPath(w io.Writer, path []string, m *
} }
} }
if allMaps { if allMaps {
dotted := strings.Join(append(append([]string{}, path...), k), ".") dotted := tomlDottedKey(append(append([]string{}, path...), k))
for _, it := range v.Content { for _, it := range v.Content {
if _, err := w.Write([]byte("[[" + dotted + "]]\n")); err != nil { if _, err := w.Write([]byte("[[" + dotted + "]]\n")); err != nil {
return err return err

View File

@ -287,6 +287,18 @@ var expectedSubArrays = `array:
- {} - {}
` `
// Keys with special characters that require quoting in TOML
var rtSpecialKeyInlineTable = `host = { "http://sealos.hub:5000" = { capabilities = ["pull", "resolve", "push"], skip_verify = true } }
`
var rtSpecialKeyTableSection = `["/tmp/blah"]
value = "hello"
`
var rtSpecialKeyDottedTableSection = `[servers."http://localhost:8080"]
ip = "127.0.0.1"
`
var tomlScenarios = []formatScenario{ var tomlScenarios = []formatScenario{
{ {
skipDoc: true, skipDoc: true,
@ -614,6 +626,30 @@ var tomlScenarios = []formatScenario{
expected: tomlTableWithComments, expected: tomlTableWithComments,
scenarioType: "roundtrip", scenarioType: "roundtrip",
}, },
{
skipDoc: true,
description: "Roundtrip: key with special characters in inline table",
input: rtSpecialKeyInlineTable,
expression: ".",
expected: rtSpecialKeyInlineTable,
scenarioType: "roundtrip",
},
{
skipDoc: true,
description: "Roundtrip: key with special characters in table section",
input: rtSpecialKeyTableSection,
expression: ".",
expected: rtSpecialKeyTableSection,
scenarioType: "roundtrip",
},
{
skipDoc: true,
description: "Roundtrip: special character key in dotted table section header",
input: rtSpecialKeyDottedTableSection,
expression: ".",
expected: rtSpecialKeyDottedTableSection,
scenarioType: "roundtrip",
},
} }
func testTomlScenario(t *testing.T, s formatScenario) { func testTomlScenario(t *testing.T, s formatScenario) {