Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ require (
github.com/nwaples/rardecode/v2 v2.1.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
Expand Down Expand Up @@ -164,7 +164,7 @@ require (
golang.org/x/mod v0.26.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af
golang.org/x/tools v0.35.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect
Expand Down
89 changes: 89 additions & 0 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"strconv"
"strings"
"time"

"emperror.dev/errors"
"github.com/apex/log"
Expand All @@ -14,6 +15,7 @@ import (
"github.com/goccy/go-json"
"github.com/icza/dyno"
"github.com/magiconair/properties"
"github.com/pelletier/go-toml/v2"
"github.com/tidwall/pretty"
"gopkg.in/ini.v1"
"gopkg.in/yaml.v3"
Expand All @@ -30,6 +32,7 @@ const (
Ini = "ini"
Json = "json"
Xml = "xml"
Toml = "toml"
)

type ReplaceValue struct {
Expand Down Expand Up @@ -228,6 +231,8 @@ func (f *ConfigurationFile) Parse(file ufs.File) error {
err = f.parseIniFile(file)
case Xml:
err = f.parseXmlFile(file)
case Toml:
err = f.parseTomlFile(file)
}
return err
}
Expand Down Expand Up @@ -480,6 +485,90 @@ func (f *ConfigurationFile) parseYamlFile(file ufs.File) error {
return nil
}

// Parses a toml file and updates any matching key/value pairs before persisting
// it back to the disk.
func (f *ConfigurationFile) parseTomlFile(file ufs.File) error {
b, err := io.ReadAll(file)
if err != nil {
return err
}

i := make(map[string]interface{})
if err := toml.Unmarshal(b, &i); err != nil {
return err
}

// Convert to JSON to reuse IterateOverJson for value replacement.
jsonBytes, err := json.Marshal(dyno.ConvertMapI2MapS(i))
if err != nil {
return err
}

data, err := f.IterateOverJson(jsonBytes)
if err != nil {
return err
}

var jsonData interface{}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
if err := decoder.Decode(&jsonData); err != nil {
return err
}
jsonData = normalizeTomlTypes(jsonData)

// Remarshal back to TOML format.
marshaled, err := toml.Marshal(jsonData)
if err != nil {
return err
}

if _, err := file.Seek(0, io.SeekStart); err != nil {
return err
}
if err := file.Truncate(0); err != nil {
return err
}

if _, err := io.Copy(file, bytes.NewReader(marshaled)); err != nil {
return errors.Wrap(err, "parser: failed to write toml file to disk")
}
return nil
}

func normalizeTomlTypes(value interface{}) interface{} {
switch typed := value.(type) {
case map[string]interface{}:
for key, item := range typed {
typed[key] = normalizeTomlTypes(item)
}
return typed
case []interface{}:
for i := range typed {
typed[i] = normalizeTomlTypes(typed[i])
}
return typed
case json.Number:
if intVal, err := typed.Int64(); err == nil {
return intVal
}
if floatVal, err := typed.Float64(); err == nil {
return floatVal
}
return typed.String()
case string:
if timeVal, err := time.Parse(time.RFC3339Nano, typed); err == nil {
return timeVal
}
if timeVal, err := time.Parse(time.RFC3339, typed); err == nil {
return timeVal
}
return typed
Comment on lines +559 to +566
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Time string coercion may produce false positives on plain string values.

After the JSON round-trip, both TOML native datetimes and plain string values that happen to match RFC 3339 format are indistinguishable — they're all strings. This means a TOML value like event_id = "2024-01-15T10:30:00Z" (intentionally a string) would be silently converted to time.Time, altering the output type.

This is an inherent limitation of the JSON round-trip approach and probably acceptable for the game-config use case. Just worth being aware of if TOML configs with date-like string values are encountered.

🤖 Prompt for AI Agents
In `@parser/parser.go` around lines 559 - 566, The string branch in parser.go
currently attempts to parse any string as RFC3339/RFC3339Nano which wrongly
converts plain strings that happen to match the date format; update the case
string block (the code that calls time.Parse for RFC3339Nano/RFC3339) to first
perform a cheap strict check (e.g., require the string contains a 'T' and either
'Z' or a timezone offset sign '+'/'-', or other narrow pattern) and only then
call time.Parse; otherwise return the original typed string. This keeps the
time.Parse attempts but prevents false positives for ordinary strings without an
explicit datetime pattern.

default:
return value
}
}

// Parses a text file using basic find and replace. This is a highly inefficient method of
// scanning a file and performing a replacement. You should attempt to use anything other
// than this function where possible.
Expand Down
Loading