From 93991d123c8f4dd5e7038dbade8488966b26b6f1 Mon Sep 17 00:00:00 2001 From: gOOvER Date: Mon, 18 May 2026 21:33:06 +0200 Subject: [PATCH 1/3] fix: preserve integer precision in YAML parser (UseNumber + normalizeYamlTypes) --- parser/parser.go | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/parser/parser.go b/parser/parser.go index a0482d86..b402043d 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -462,9 +462,12 @@ func (f *ConfigurationFile) parseYamlFile(file ufs.File) error { } var jsonData interface{} - if err := json.Unmarshal(data, &jsonData); err != nil { + yamlDecoder := json.NewDecoder(bytes.NewReader(data)) + yamlDecoder.UseNumber() + if err := yamlDecoder.Decode(&jsonData); err != nil { return err } + jsonData = normalizeYamlTypes(jsonData) marshaled, err := yaml.Marshal(jsonData) if err != nil { @@ -536,6 +539,34 @@ func (f *ConfigurationFile) parseTomlFile(file ufs.File) error { return nil } +// normalizeYamlTypes converts json.Number values (produced by UseNumber()) into +// proper Go numeric types so that yaml.Marshal writes integers as plain integers +// instead of float64 scientific notation (e.g. 1.5e+18 for large Snowflake IDs). +func normalizeYamlTypes(value interface{}) interface{} { + switch typed := value.(type) { + case map[string]interface{}: + for key, item := range typed { + typed[key] = normalizeYamlTypes(item) + } + return typed + case []interface{}: + for i := range typed { + typed[i] = normalizeYamlTypes(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() + default: + return value + } +} + func normalizeTomlTypes(value interface{}) interface{} { switch typed := value.(type) { case map[string]interface{}: From cc436aeb3fad8a46d4c2dfd25b0afeef9c3b84eb Mon Sep 17 00:00:00 2001 From: gOOvER Date: Mon, 18 May 2026 21:38:27 +0200 Subject: [PATCH 2/3] fix: multiple parser bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UnmarshalJSON: guard against nil pointer panic on missing 'file'/'parser' keys - LookupConfigurationValue: resolve each {{ config.x }} placeholder independently so composite values like '{{ config.a }}:{{ config.b }}' expand correctly - setValueWithSjson: drop implicit string→int coercion in default case to preserve the original value type - parseTextFile: honour if_value condition (was silently ignored before) - parseXmlFile: remove existing attribute before CreateAttr to prevent duplicates on repeated parses --- parser/helpers.go | 45 +++++++++++++++++++-------------------------- parser/parser.go | 28 +++++++++++++++++++++++----- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/parser/helpers.go b/parser/helpers.go index e34013b1..887ea000 100644 --- a/parser/helpers.go +++ b/parser/helpers.go @@ -185,11 +185,7 @@ func (cfr *ConfigurationFileReplacement) setValueWithSjson(jsonStr string, path } setValue = value default: - if v, err := strconv.Atoi(value); err == nil { - setValue = v - } else { - setValue = value - } + setValue = value } } @@ -208,29 +204,26 @@ func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplac // If there is a match, lookup the value in the configuration for the Daemon. If no key // is found, just return the string representation, otherwise use the value from the // daemon configuration here. - huntPath := configMatchRegex.ReplaceAllString( - configMatchRegex.FindString(cfr.ReplaceWith.String()), "$1", - ) + result := cfr.ReplaceWith.String() + for _, placeholder := range configMatchRegex.FindAllString(result, -1) { + keyPath := configMatchRegex.ReplaceAllString(placeholder, "$1") - var path []string - for _, value := range strings.Split(huntPath, ".") { - path = append(path, strcase.ToSnake(value)) - } - - // Look for the key in the configuration file, and if found return that value to the - // calling function. - match, _, _, err := jsonparser.Get(f.configuration, path...) - if err != nil { - if err != jsonparser.KeyPathNotFoundError { - return string(match), err + var path []string + for _, part := range strings.Split(keyPath, ".") { + path = append(path, strcase.ToSnake(part)) } - log.WithFields(log.Fields{"path": path, "filename": f.FileName}).Debug("attempted to load a configuration value that does not exist") - - // If there is no key, keep the original value intact, that way it is obvious there - // is a replace issue at play. - return string(match), nil - } else { - return configMatchRegex.ReplaceAllString(cfr.ReplaceWith.String(), string(match)), nil + // Look for the key in the Wings configuration and substitute the placeholder. + match, _, _, err := jsonparser.Get(f.configuration, path...) + if err != nil { + if err != jsonparser.KeyPathNotFoundError { + return result, err + } + log.WithFields(log.Fields{"path": path, "filename": f.FileName}).Debug("attempted to load a configuration value that does not exist") + // Leave placeholder intact so the misconfiguration is visible. + continue + } + result = strings.Replace(result, placeholder, string(match), 1) } + return result, nil } diff --git a/parser/parser.go b/parser/parser.go index b402043d..926f7843 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -128,17 +128,28 @@ func (f *ConfigurationFile) UnmarshalJSON(data []byte) error { return err } - if err := json.Unmarshal(*m["file"], &f.FileName); err != nil { + fileRaw, ok := m["file"] + if !ok || fileRaw == nil { + return errors.New("parser: configuration file missing required 'file' key") + } + if err := json.Unmarshal(*fileRaw, &f.FileName); err != nil { return err } - if err := json.Unmarshal(*m["parser"], &f.Parser); err != nil { + parserRaw, ok := m["parser"] + if !ok || parserRaw == nil { + return errors.New("parser: configuration file missing required 'parser' key") + } + if err := json.Unmarshal(*parserRaw, &f.Parser); err != nil { return err } - if err := json.Unmarshal(*m["replace"], &f.Replace); err != nil { - log.WithField("file", f.FileName).WithField("error", err).Warn("failed to unmarshal configuration file replacement") - + if replaceRaw, ok := m["replace"]; ok && replaceRaw != nil { + if err := json.Unmarshal(*replaceRaw, &f.Replace); err != nil { + log.WithField("file", f.FileName).WithField("error", err).Warn("failed to unmarshal configuration file replacement") + f.Replace = []ConfigurationFileReplacement{} + } + } else { f.Replace = []ConfigurationFileReplacement{} } @@ -292,6 +303,7 @@ func (f *ConfigurationFile) parseXmlFile(file ufs.File) error { k := xmlValueMatchRegex.ReplaceAllString(value, "$1") v := xmlValueMatchRegex.ReplaceAllString(value, "$2") + element.RemoveAttr(k) element.CreateAttr(k, v) } else { element.SetText(value) @@ -616,6 +628,12 @@ func (f *ConfigurationFile) parseTextFile(file ufs.File) error { if !bytes.HasPrefix(line, []byte(replace.Match)) { continue } + // If an if_value is set, only replace when the remainder of the line matches. + if replace.IfValue != "" { + if string(bytes.TrimPrefix(line, []byte(replace.Match))) != replace.IfValue { + continue + } + } b.Write(replace.ReplaceWith.Bytes()) replaced = true } From 5344177dc0ca6212409d5e599b1624e89066853c Mon Sep 17 00:00:00 2001 From: gOOvER Date: Mon, 18 May 2026 21:50:07 +0200 Subject: [PATCH 3/3] fix: address Copilot review comments --- parser/helpers.go | 18 +++++++++++------- parser/parser.go | 19 ++++++++++++++----- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/parser/helpers.go b/parser/helpers.go index 887ea000..7f2f539b 100644 --- a/parser/helpers.go +++ b/parser/helpers.go @@ -204,8 +204,11 @@ func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplac // If there is a match, lookup the value in the configuration for the Daemon. If no key // is found, just return the string representation, otherwise use the value from the // daemon configuration here. - result := cfr.ReplaceWith.String() - for _, placeholder := range configMatchRegex.FindAllString(result, -1) { + var lookupErr error + result := configMatchRegex.ReplaceAllStringFunc(cfr.ReplaceWith.String(), func(placeholder string) string { + if lookupErr != nil { + return placeholder + } keyPath := configMatchRegex.ReplaceAllString(placeholder, "$1") var path []string @@ -217,13 +220,14 @@ func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplac match, _, _, err := jsonparser.Get(f.configuration, path...) if err != nil { if err != jsonparser.KeyPathNotFoundError { - return result, err + lookupErr = err + return placeholder } log.WithFields(log.Fields{"path": path, "filename": f.FileName}).Debug("attempted to load a configuration value that does not exist") // Leave placeholder intact so the misconfiguration is visible. - continue + return placeholder } - result = strings.Replace(result, placeholder, string(match), 1) - } - return result, nil + return string(match) + }) + return result, lookupErr } diff --git a/parser/parser.go b/parser/parser.go index 926f7843..cd394c67 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -144,13 +144,11 @@ func (f *ConfigurationFile) UnmarshalJSON(data []byte) error { return err } + f.Replace = []ConfigurationFileReplacement{} if replaceRaw, ok := m["replace"]; ok && replaceRaw != nil { if err := json.Unmarshal(*replaceRaw, &f.Replace); err != nil { log.WithField("file", f.FileName).WithField("error", err).Warn("failed to unmarshal configuration file replacement") - f.Replace = []ConfigurationFileReplacement{} } - } else { - f.Replace = []ConfigurationFileReplacement{} } // test if "create_file" exists, if not just assume true @@ -567,13 +565,22 @@ func normalizeYamlTypes(value interface{}) interface{} { } return typed case json.Number: + s := typed.String() + // Preserve float representation: if the number contains '.', 'e' or 'E' + // it was originally a float and must not be coerced to int64. + if strings.ContainsAny(s, ".eE") { + if floatVal, err := typed.Float64(); err == nil { + return floatVal + } + return s + } if intVal, err := typed.Int64(); err == nil { return intVal } if floatVal, err := typed.Float64(); err == nil { return floatVal } - return typed.String() + return s default: return value } @@ -629,8 +636,10 @@ func (f *ConfigurationFile) parseTextFile(file ufs.File) error { continue } // If an if_value is set, only replace when the remainder of the line matches. + // Trim trailing \r\n so Windows line endings do not break the comparison. if replace.IfValue != "" { - if string(bytes.TrimPrefix(line, []byte(replace.Match))) != replace.IfValue { + remainder := bytes.TrimRight(bytes.TrimPrefix(line, []byte(replace.Match)), "\r\n") + if string(remainder) != replace.IfValue { continue } }