From 9372c98749c2a0ea275c52474c3bf3db3879fd3a Mon Sep 17 00:00:00 2001 From: HyperGaming99 <130169800+HyperGaming99@users.noreply.github.com> Date: Sun, 28 Jun 2026 10:29:20 +0200 Subject: [PATCH 1/3] fix(parser): write numeric/bool values unquoted for newly created config files When a server is first created its configuration file is often generated from scratch, so the target key does not exist yet. setValueWithSjson previously inferred a value's type solely by mirroring the type already present in the document, which meant a freshly created key had no type to mirror and every value fell back to a quoted string. Numeric values such as ports were therefore written as `server-port: "25565"` instead of `server-port: 25565` in YAML (and quoted in JSON/TOML). Changes: - Infer the natural type from the value itself when the key does not exist yet, writing clean JSON numbers and booleans unquoted. - Honour an explicitly declared numeric type from the egg definition (previously only an explicit boolean type was honoured). - Replace the lax gjson.Parse number check with a strict canonical JSON-number match so values like "007", "+5", "1.20.1" or "25565x" stay strings instead of being coerced (which could also have produced invalid JSON via SetRaw). Existing string-typed keys are intentionally left untouched, so values like numeric passwords in an existing template are not coerced to numbers. --- parser/helpers.go | 52 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/parser/helpers.go b/parser/helpers.go index b536ea68..c7fb2da4 100644 --- a/parser/helpers.go +++ b/parser/helpers.go @@ -35,6 +35,20 @@ var configMatchRegex = regexp.MustCompile(`{{\s?config\.([\w.-]+)\s?}}`) // noinspection RegExpRedundantEscape var xmlValueMatchRegex = regexp.MustCompile(`^\[([\w]+)='(.*)'\]$`) +// jsonNumberRegex matches a canonical JSON number exactly as defined by the JSON +// grammar. We deliberately use this instead of gjson.Parse/json.Valid, which +// accept lax forms such as "007", "+5", "1.20.1" or "25565x". A value is only +// written unquoted via sjson.SetRaw when it matches this pattern, guaranteeing +// the result is valid JSON and the exact literal (including large integers) is +// preserved. +var jsonNumberRegex = regexp.MustCompile(`^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$`) + +// isJSONNumber reports whether s is a canonical JSON number that is safe to write +// unquoted into a document. +func isJSONNumber(s string) bool { + return jsonNumberRegex.MatchString(s) +} + // Iterate over an unstructured JSON/YAML/etc. interface and set all of the required // key/value pairs for the configuration file. // @@ -156,7 +170,8 @@ func (cfr *ConfigurationFileReplacement) setValueWithSjson(jsonStr string, path } var setValue interface{} - if cfr.ReplaceWith.Type() == jsonparser.Boolean { + switch { + case cfr.ReplaceWith.Type() == jsonparser.Boolean: // Explicit boolean type declared in the egg definition. v, err := strconv.ParseBool(value) if err != nil { @@ -164,26 +179,41 @@ func (cfr *ConfigurationFileReplacement) setValueWithSjson(jsonStr string, path return sjson.Set(jsonStr, path, value) } setValue = v - } else { - // Mirror the type already present in the document so booleans and numbers - // survive template expansion (panel always sends values as JSON strings). + case cfr.ReplaceWith.Type() == jsonparser.Number && isJSONNumber(value): + // Explicit numeric type declared in the egg definition. Write the literal + // as-is via SetRaw to avoid float64 precision loss for large integers (> 2^53). + return sjson.SetRaw(jsonStr, path, value) + default: + // The panel expands template variables and sends every value as a JSON + // string, so mirror the type already present in the document where possible + // to keep booleans and numbers unquoted. existing := gjson.Get(jsonStr, path) - switch existing.Type { - case gjson.True, gjson.False: + switch { + case existing.Type == gjson.True || existing.Type == gjson.False: v, err := strconv.ParseBool(value) if err != nil { log.WithFields(log.Fields{"value": value, "path": path, "match": cfr.Match}).Warn("cannot parse replacement as boolean, falling back to string value") return sjson.Set(jsonStr, path, value) } setValue = v - case gjson.Number: + case existing.Type == gjson.Number && isJSONNumber(value): // Write the numeric literal as-is via SetRaw to avoid float64 precision - // loss for large integers (> 2^53). Fall back to string if the incoming - // value is not a valid JSON number. - if gjson.Parse(value).Type == gjson.Number { + // loss for large integers (> 2^53). + return sjson.SetRaw(jsonStr, path, value) + case !existing.Exists(): + // The key does not exist yet. This is the common case when a server is + // first created and its configuration file is generated from scratch: + // there is no existing type to mirror. Infer the natural type from the + // value itself so numeric values such as ports are written unquoted + // (e.g. `server-port: 25565` instead of `server-port: "25565"`). + switch { + case value == "true" || value == "false": + setValue = value == "true" + case isJSONNumber(value): return sjson.SetRaw(jsonStr, path, value) + default: + setValue = value } - setValue = value default: setValue = value } From 081e75eb61141df62b75bc49d9123ebabb0838f7 Mon Sep 17 00:00:00 2001 From: HyperGaming99 <130169800+HyperGaming99@users.noreply.github.com> Date: Sun, 28 Jun 2026 10:41:43 +0200 Subject: [PATCH 2/3] refactor(parser): extract shared setBool helper The explicit-boolean and mirror-existing-boolean cases in setValueWithSjson contained identical parse/log/fallback logic. Extract a setBool helper so the two paths stay in sync. --- parser/helpers.go | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/parser/helpers.go b/parser/helpers.go index c7fb2da4..6a99e3da 100644 --- a/parser/helpers.go +++ b/parser/helpers.go @@ -173,12 +173,7 @@ func (cfr *ConfigurationFileReplacement) setValueWithSjson(jsonStr string, path switch { case cfr.ReplaceWith.Type() == jsonparser.Boolean: // Explicit boolean type declared in the egg definition. - v, err := strconv.ParseBool(value) - if err != nil { - log.WithFields(log.Fields{"value": value, "path": path, "match": cfr.Match}).Warn("cannot parse replacement as boolean, falling back to string value") - return sjson.Set(jsonStr, path, value) - } - setValue = v + return cfr.setBool(jsonStr, path, value) case cfr.ReplaceWith.Type() == jsonparser.Number && isJSONNumber(value): // Explicit numeric type declared in the egg definition. Write the literal // as-is via SetRaw to avoid float64 precision loss for large integers (> 2^53). @@ -190,12 +185,7 @@ func (cfr *ConfigurationFileReplacement) setValueWithSjson(jsonStr string, path existing := gjson.Get(jsonStr, path) switch { case existing.Type == gjson.True || existing.Type == gjson.False: - v, err := strconv.ParseBool(value) - if err != nil { - log.WithFields(log.Fields{"value": value, "path": path, "match": cfr.Match}).Warn("cannot parse replacement as boolean, falling back to string value") - return sjson.Set(jsonStr, path, value) - } - setValue = v + return cfr.setBool(jsonStr, path, value) case existing.Type == gjson.Number && isJSONNumber(value): // Write the numeric literal as-is via SetRaw to avoid float64 precision // loss for large integers (> 2^53). @@ -222,6 +212,18 @@ func (cfr *ConfigurationFileReplacement) setValueWithSjson(jsonStr string, path return sjson.Set(jsonStr, path, setValue) } +// setBool parses value as a boolean and writes it to path unquoted. If value is +// not a valid boolean it logs a warning and falls back to writing the raw string, +// keeping the explicit-type and mirror-existing-type boolean paths in sync. +func (cfr *ConfigurationFileReplacement) setBool(jsonStr, path, value string) (string, error) { + v, err := strconv.ParseBool(value) + if err != nil { + log.WithFields(log.Fields{"value": value, "path": path, "match": cfr.Match}).Warn("cannot parse replacement as boolean, falling back to string value") + return sjson.Set(jsonStr, path, value) + } + return sjson.Set(jsonStr, path, v) +} + // Looks up a configuration value on the Daemon given a dot-notated syntax. func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplacement) (result string, err error) { // If this is not something that we can do a regex lookup on then just continue From 4921827591d6411b29b5930ce6c019834e781e35 Mon Sep 17 00:00:00 2001 From: HyperGaming99 <130169800+HyperGaming99@users.noreply.github.com> Date: Sun, 28 Jun 2026 11:25:25 +0200 Subject: [PATCH 3/3] fix(parser): warn when an explicit numeric egg value is not a number The explicit-Number path previously matched on `type == Number && isJSONNumber`, so when an egg declared a numeric type but the expanded value was not a canonical JSON number (e.g. "25565x" or "007"), the case was skipped and the value silently fell through to the mirror/default branch and was written as a string. Extract a setNumber helper that, like setBool, logs a warning and falls back to writing the raw string when the value is not a canonical JSON number, so a misconfigured egg is diagnosable. --- parser/helpers.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/parser/helpers.go b/parser/helpers.go index 6a99e3da..cf0bc457 100644 --- a/parser/helpers.go +++ b/parser/helpers.go @@ -174,10 +174,9 @@ func (cfr *ConfigurationFileReplacement) setValueWithSjson(jsonStr string, path case cfr.ReplaceWith.Type() == jsonparser.Boolean: // Explicit boolean type declared in the egg definition. return cfr.setBool(jsonStr, path, value) - case cfr.ReplaceWith.Type() == jsonparser.Number && isJSONNumber(value): - // Explicit numeric type declared in the egg definition. Write the literal - // as-is via SetRaw to avoid float64 precision loss for large integers (> 2^53). - return sjson.SetRaw(jsonStr, path, value) + case cfr.ReplaceWith.Type() == jsonparser.Number: + // Explicit numeric type declared in the egg definition. + return cfr.setNumber(jsonStr, path, value) default: // The panel expands template variables and sends every value as a JSON // string, so mirror the type already present in the document where possible @@ -224,6 +223,19 @@ func (cfr *ConfigurationFileReplacement) setBool(jsonStr, path, value string) (s return sjson.Set(jsonStr, path, v) } +// setNumber writes a canonical JSON number to path unquoted via SetRaw to avoid +// float64 precision loss for large integers (> 2^53). If value is not a canonical +// JSON number it logs a warning and falls back to writing the raw string, so a +// misconfigured egg that declares a numeric type for a non-numeric value is +// diagnosable (mirroring setBool). +func (cfr *ConfigurationFileReplacement) setNumber(jsonStr, path, value string) (string, error) { + if !isJSONNumber(value) { + log.WithFields(log.Fields{"value": value, "path": path, "match": cfr.Match}).Warn("cannot parse replacement as number, falling back to string value") + return sjson.Set(jsonStr, path, value) + } + return sjson.SetRaw(jsonStr, path, value) +} + // Looks up a configuration value on the Daemon given a dot-notated syntax. func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplacement) (result string, err error) { // If this is not something that we can do a regex lookup on then just continue