diff --git a/CHANGELOG.md b/CHANGELOG.md index 375b503d..f99f9bba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Fixed a bug in YAML and TOML parsers that caused them to fail when parsing non-base10 numbers (e.g. hex, binary, octal). +- Fixed a bug in the `toInt` function that caused it to fail when parsing non-base10 numbers (e.g. hex, binary, octal). - XML child element ordering now has more comprehensive round-trip handling. Thanks @takeokunn. ## [v3.4.1] - 2026-03-30 diff --git a/execution/func_to_int.go b/execution/func_to_int.go index d8c38723..578d334d 100644 --- a/execution/func_to_int.go +++ b/execution/func_to_int.go @@ -19,7 +19,7 @@ var FuncToInt = NewFunc( return nil, err } - i, err := strconv.ParseInt(stringValue, 10, 64) + i, err := strconv.ParseInt(stringValue, 0, 64) if err != nil { return nil, err } diff --git a/parsing/toml/toml_reader.go b/parsing/toml/toml_reader.go index 56f56f3e..55d3aec8 100644 --- a/parsing/toml/toml_reader.go +++ b/parsing/toml/toml_reader.go @@ -150,7 +150,7 @@ func (j *tomlReader) readNode(p *unstable.Parser, n *unstable.Node) (string, *mo } return "", model.NewFloatValue(f), nil case unstable.Integer: - i64, err := strconv.ParseInt(string(n.Data), 10, 64) + i64, err := strconv.ParseInt(string(n.Data), 0, 64) if err != nil { return "", nil, err } diff --git a/parsing/toml/toml_reader_test.go b/parsing/toml/toml_reader_test.go index 42ae43dc..911aa0ce 100644 --- a/parsing/toml/toml_reader_test.go +++ b/parsing/toml/toml_reader_test.go @@ -514,6 +514,44 @@ func TestTomlReader_EdgeCases(t *testing.T) { }) } +func TestTomlReader_NonBase10Numbers(t *testing.T) { + t.Run("hex", tomlReaderTest([]byte(`val = 0x12`), func() *model.Value { + res := model.NewMapValue() + _ = res.SetMapKey("val", model.NewIntValue(18)) + return res + })) + + t.Run("octal", tomlReaderTest([]byte(`val = 0o7`), func() *model.Value { + res := model.NewMapValue() + _ = res.SetMapKey("val", model.NewIntValue(7)) + return res + })) + + t.Run("binary", tomlReaderTest([]byte(`val = 0b111`), func() *model.Value { + res := model.NewMapValue() + _ = res.SetMapKey("val", model.NewIntValue(7)) + return res + })) + + t.Run("underscore decimal", tomlReaderTest([]byte(`val = 12_000`), func() *model.Value { + res := model.NewMapValue() + _ = res.SetMapKey("val", model.NewIntValue(12000)) + return res + })) + + t.Run("negative underscore decimal", tomlReaderTest([]byte(`val = -12_000`), func() *model.Value { + res := model.NewMapValue() + _ = res.SetMapKey("val", model.NewIntValue(-12000)) + return res + })) + + t.Run("negative int", tomlReaderTest([]byte(`val = -42`), func() *model.Value { + res := model.NewMapValue() + _ = res.SetMapKey("val", model.NewIntValue(-42)) + return res + })) +} + func TestTomlReader_TimeStrings(t *testing.T) { // Local date t.Run("local date string", func(t *testing.T) { diff --git a/parsing/yaml/yaml_reader.go b/parsing/yaml/yaml_reader.go index ed82d8f1..6cef7fa2 100644 --- a/parsing/yaml/yaml_reader.go +++ b/parsing/yaml/yaml_reader.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "strconv" + "strings" "github.com/tomwright/dasel/v3/model" "github.com/tomwright/dasel/v3/parsing" @@ -96,11 +97,11 @@ func (yv *yamlValue) UnmarshalYAML(value *yaml.Node) error { case "!!bool": yv.value = model.NewBoolValue(value.Value == "true") case "!!int": - i, err := strconv.Atoi(value.Value) + i, err := parseYAMLInt(value.Value) if err != nil { return err } - yv.value = model.NewIntValue(int64(i)) + yv.value = model.NewIntValue(i) case "!!float": f, err := strconv.ParseFloat(value.Value, 64) if err != nil { @@ -186,3 +187,25 @@ func (yv *yamlValue) UnmarshalYAML(value *yaml.Node) error { } return nil } + +func parseYAMLInt(s string) (int64, error) { + // Strip leading sign for prefix detection. + clean := s + if len(clean) > 0 && (clean[0] == '+' || clean[0] == '-') { + clean = clean[1:] + } + + switch { + case strings.HasPrefix(clean, "0x") || strings.HasPrefix(clean, "0X"): + return strconv.ParseInt(s, 0, 64) + case strings.HasPrefix(clean, "0o") || strings.HasPrefix(clean, "0O"): + return strconv.ParseInt(s, 0, 64) + case strings.HasPrefix(clean, "0b") || strings.HasPrefix(clean, "0B"): + return strconv.ParseInt(s, 0, 64) + default: + // YAML 1.2 allows underscores in decimal integers (e.g. 1_000). + // strconv.ParseInt with base 10 does not support underscores, + // so we strip them before parsing. + return strconv.ParseInt(strings.ReplaceAll(s, "_", ""), 10, 64) + } +} diff --git a/parsing/yaml/yaml_test.go b/parsing/yaml/yaml_test.go index 9093d4df..131ab8e9 100644 --- a/parsing/yaml/yaml_test.go +++ b/parsing/yaml/yaml_test.go @@ -204,6 +204,161 @@ name2: Tom `, }.run) + t.Run("base numbers", func(t *testing.T) { + t.Run("standard", rwTestCase{ + in: `10 +`, + out: `10 +`, + }.run) + + t.Run("zero", rwTestCase{ + in: `0 +`, + out: `0 +`, + }.run) + + t.Run("negative", rwTestCase{ + in: `-42 +`, + out: `-42 +`, + }.run) + + t.Run("hex lowercase", rwTestCase{ + in: `0x10 +`, + out: `16 +`, + }.run) + + t.Run("hex uppercase letters", rwTestCase{ + in: `0xff +`, + out: `255 +`, + }.run) + + t.Run("octal", rwTestCase{ + in: `0o10 +`, + out: `8 +`, + }.run) + + t.Run("binary", rwTestCase{ + in: `0b10 +`, + out: `2 +`, + }.run) + + t.Run("leading zero is decimal", rwTestCase{ + in: `010 +`, + out: `10 +`, + }.run) + + t.Run("hex in map", rwTestCase{ + in: `val: 0x10 +`, + out: `val: 16 +`, + }.run) + + t.Run("octal in map", rwTestCase{ + in: `val: 0o77 +`, + out: `val: 63 +`, + }.run) + + t.Run("mixed types in map", rwTestCase{ + in: `dec: 42 +hex: 0xff +oct: 0o77 +bin: 0b1010 +`, + out: `dec: 42 +hex: 255 +oct: 63 +bin: 10 +`, + }.run) + + t.Run("positive sign", rwTestCase{ + in: `+42 +`, + out: `42 +`, + }.run) + + t.Run("positive hex", rwTestCase{ + in: `+0x10 +`, + out: `16 +`, + }.run) + + t.Run("positive octal", rwTestCase{ + in: `+0o10 +`, + out: `8 +`, + }.run) + + t.Run("positive binary", rwTestCase{ + in: `+0b10 +`, + out: `2 +`, + }.run) + + t.Run("negative hex", rwTestCase{ + in: `-0x10 +`, + out: `-16 +`, + }.run) + + t.Run("negative octal", rwTestCase{ + in: `-0o10 +`, + out: `-8 +`, + }.run) + + t.Run("negative binary", rwTestCase{ + in: `-0b10 +`, + out: `-2 +`, + }.run) + + t.Run("underscore decimal", rwTestCase{ + in: `1_000 +`, + out: `1000 +`, + }.run) + + t.Run("underscore hex", rwTestCase{ + in: `0xFF_FF +`, + out: `65535 +`, + }.run) + + t.Run("underscore binary", rwTestCase{ + in: `0b1010_1010 +`, + out: `170 +`, + }.run) + }) + t.Run("bounded yaml expansion", func(t *testing.T) { in := `a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"] b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a] diff --git a/selector/parser/parser_test.go b/selector/parser/parser_test.go index 0d875031..0851b725 100644 --- a/selector/parser/parser_test.go +++ b/selector/parser/parser_test.go @@ -61,6 +61,10 @@ func TestParser_Parse_HappyPath(t *testing.T) { input: "42", expected: ast.NumberIntExpr{Value: 42}, }.run) + t.Run("leading zero decimal", happyTestCase{ + input: "010", + expected: ast.NumberIntExpr{Value: 10}, + }.run) t.Run("float", happyTestCase{ input: "42.1", expected: ast.NumberFloatExpr{Value: 42.1},