mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
topdown: fix format_int precision loss for integers larger than 64 bits (#8857)
## Description
`format_int(x, base)` corrupts integers that need more than 64 bits of
precision, in every base. It routes the value through
`builtins.NumberToFloat` (a `big.Float` with a 64-bit mantissa) then
`f.Int()`, so any integer above ~2^64 is rounded before formatting:
```rego
format_int(18446744073709551617, 16) # "10000000000000000" (should be "10000000000000001")
format_int(18446744073709551617, 10) # "18446744073709551616" (should be "...617")
```
`sprintf("%x", [18446744073709551617])` returns the correct
`10000000000000001`, so two builtins disagree on the same exact-integer
value.
## Fix
Format integer inputs through an exact `big.Int` (mirroring
`builtinSprintf`). Fractional/exponent inputs still fall through to the
existing float-truncation path, so `format_int(15.9, 16) == "f"` and
`format_int(-15.9, 16) == "-f"` are unchanged.
## Test
Added a golden case covering a >2^64 integer in bases 2/8/10/16,
negatives, and the fractional-truncation cases. Full `go test
./v1/topdown/` passes (900+ existing string golden cases, no
regressions).
---------
Signed-off-by: Synvoya <16019863+Synvoya@users.noreply.github.com>
Co-authored-by: Synvoya <16019863+Synvoya@users.noreply.github.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
# Exception Format is <test name>: <reason>
|
||||
"data/toplevel integer": "https://github.com/open-policy-agent/opa/issues/3711"
|
||||
"data/nested integer": "https://github.com/open-policy-agent/opa/issues/3711"
|
||||
"strings/format_int: bignum exact (>64-bit integer, all bases + negative)": "WASM cannot represent integers larger than 64 bits (see https://github.com/open-policy-agent/opa/issues/3711); this change fixes the Go topdown builtin."
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
cases:
|
||||
- note: "strings/format_int: bignum exact (>64-bit integer, all bases + negative)"
|
||||
query: data.generated.p = x
|
||||
modules:
|
||||
- |
|
||||
package generated
|
||||
|
||||
p := result if {
|
||||
result := {
|
||||
"b16": format_int(18446744073709551617, 16),
|
||||
"b10": format_int(18446744073709551617, 10),
|
||||
"b8": format_int(18446744073709551617, 8),
|
||||
"b2": format_int(18446744073709551617, 2),
|
||||
"neg16": format_int(-18446744073709551617, 16),
|
||||
"neg10": format_int(-18446744073709551617, 10),
|
||||
"frac16": format_int(15.9, 16),
|
||||
"fracneg16": format_int(-15.9, 16),
|
||||
}
|
||||
}
|
||||
data: {}
|
||||
want_result:
|
||||
- x:
|
||||
b16: "10000000000000001"
|
||||
b10: "18446744073709551617"
|
||||
b8: "2000000000000000000001"
|
||||
b2: "10000000000000000000000000000000000000000000000000000000000000001"
|
||||
neg16: "-10000000000000001"
|
||||
neg10: "-18446744073709551617"
|
||||
frac16: "f"
|
||||
fracneg16: "-f"
|
||||
@@ -129,11 +129,14 @@ func builtinFormatInt(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Ter
|
||||
}
|
||||
|
||||
var format string
|
||||
var radix int
|
||||
switch base {
|
||||
case ast.Number("2"):
|
||||
format = "%b"
|
||||
radix = 2
|
||||
case ast.Number("8"):
|
||||
format = "%o"
|
||||
radix = 8
|
||||
case ast.Number("10"):
|
||||
// Fast path: for numbers whose decimal string is already interned (e.g.
|
||||
// "0"–"100"), we can skip strconv.ParseInt entirely.
|
||||
@@ -144,12 +147,23 @@ func builtinFormatInt(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Ter
|
||||
return iter(ast.InternedIntegerString(i))
|
||||
}
|
||||
format = "%d"
|
||||
radix = 10
|
||||
case ast.Number("16"):
|
||||
format = "%x"
|
||||
radix = 16
|
||||
default:
|
||||
return builtins.NewOperandEnumErr(2, "2", "8", "10", "16")
|
||||
}
|
||||
|
||||
// For integer inputs, format the exact big.Int. Routing integers through a
|
||||
// float (as the fractional path below does) loses precision for values that
|
||||
// need more than a float64's 53-bit mantissa, e.g. 18446744073709551617.
|
||||
if i, ok := new(big.Int).SetString(string(input), 10); ok {
|
||||
return iter(ast.InternedTerm(i.Text(radix)))
|
||||
}
|
||||
|
||||
// Fractional inputs (e.g. 15.9) are truncated toward zero, matching the
|
||||
// historical behaviour: format_int(15.9, 16) == "f", format_int(-15.9, 16) == "-f".
|
||||
f := builtins.NumberToFloat(input)
|
||||
i, _ := f.Int(nil)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user