diff --git a/README.md b/README.md index f5c6cd4..e381646 100644 --- a/README.md +++ b/README.md @@ -172,18 +172,19 @@ container. computed as a double, so `2^0.5` is `1.4142135623730951`. A negative number to a fractional power is refused, as having no real result. Numbers are read as decimal, so `010` is ten. Input over 256 bytes is - refused and exact powers are capped, so a message cannot make the bot - do unbounded work. Whole numbers below 1021 are written - exactly; other results in the shortest form that reads back as the - same double, in exponent notation from 1021 up and below - 10-6. Refused as too large or too small: any number whose - numerator or denominator reaches 4096 bits, wherever it appears, as - `go/constant` could hold it only rounded (`1e-1300 + 1`); a power - computed as a double whose base or result is outside the normal range - of a double, about 2.2e-308 to 1.8e308 in magnitude, where a double - keeps all its digits (`1e-400^0.5`); and a result other than zero - outside that range, as it is written through a double (`1e400`, - `2^-1400`). + refused, exact powers are capped, and every number is held as a + fraction, whole numbers too, under the 4096-bit limit below, so a + message cannot make the bot do unbounded work. Whole numbers below + 1021 are written exactly; other results in the shortest + form that reads back as the same double, in exponent notation from + 1021 up and below 10-6. Refused as too large or + too small: any number whose numerator or denominator reaches 4096 + bits, wherever it appears, as `go/constant` rounds a fraction that + grows that large (`1e-1300 + 1`); a power computed as a double whose + base or result is outside the normal range of a double, about 2.2e-308 + to 1.8e308 in magnitude, where a double keeps all its digits + (`1e-400^0.5`); and a result other than zero outside that range, as it + is written through a double (`1e400`, `2^-1400`). - **Failure is an exit.** If the chat client exits or the connection to it drops, the bot exits with an error and the container's restart policy starts both again. `SIGTERM` stops the bot, which stops the diff --git a/internal/calc/calc.go b/internal/calc/calc.go index c81247a..606a1f7 100644 --- a/internal/calc/calc.go +++ b/internal/calc/calc.go @@ -21,11 +21,15 @@ import ( "strings" ) -// MaxInputLength caps an expression, in bytes, and maxExactExponent caps -// a power computed exactly, so that a message cannot make the bot do -// unbounded work. +// MaxInputLength caps an expression, in bytes. With maxExactExponent, +// which caps a power computed exactly, and bitLimit, which caps every +// number, it keeps a message from making the bot do unbounded work. const MaxInputLength = 256 +// bitLimit caps the numerator and denominator of every number: see +// exact. +const bitLimit = 4096 + // maxExactExponent is the largest exponent, either way, of a power // computed exactly. Past it, x^n has a numerator or denominator of more // than 4096 bits, which go/constant holds only rounded, unless x is 0 or @@ -245,11 +249,11 @@ func number(tok string) (constant.Value, error) { return nil, ErrNotArithmetic } - // Read as FLOAT, which makes every literal decimal: as INT, a - // leading zero would make it octal. + // Read as FLOAT, which makes every literal decimal and a fraction + // (see exact): as INT, a leading zero would make it octal. v := constant.MakeFromLiteral(tok, token.FLOAT, 0) - // A literal such as 1e1300 or 1e-1300 is held rounded: see exact. + // A literal such as 1e1300 or 1e-1233 is past bitLimit: see exact. if !exact(v) { return nil, ErrOutOfRange } @@ -403,10 +407,13 @@ func nonNegativePower(x, y, n constant.Value) (constant.Value, error) { } // exactPower computes x^e by repeated squaring. x is not zero if e is -// negative. Each step's numbers stay small: go/constant holds one whose -// numerator or denominator reaches 4096 bits as a 512-bit float. +// negative. It starts from 1 as a fraction, a Float to go/constant, so +// that x^0 is a fraction like every other number (see exact). Each +// step's numbers stay small: go/constant holds one whose numerator or +// denominator reaches 4096 bits as a 512-bit float. func exactPower(x constant.Value, e int64) constant.Value { - result := constant.MakeInt64(1) + one := constant.MakeFloat64(1) + result := one for n := max(e, -e); n > 0; n >>= 1 { if n&1 == 1 { @@ -417,26 +424,29 @@ func exactPower(x constant.Value, e int64) constant.Value { } if e < 0 { - result = constant.BinaryOp(constant.MakeInt64(1), token.QUO, result) + result = constant.BinaryOp(one, token.QUO, result) } return result } -// exact reports whether go/constant holds v exactly. It holds a number -// as a fraction until its numerator or denominator reaches 4096 bits, -// then as a 512-bit float, and past that float's range as Unknown. A -// number not held exactly is refused wherever it appears: a sum can lose -// the answer entirely (7^1000*7^1000 + 5 - 7^1000*7^1000 would be 0), and -// a remainder, or whether an exponent is whole or odd, cannot be read -// from one. +// exact reports whether v is a fraction whose numerator and denominator +// are both below bitLimit bits, as every number here must be, so that +// each step of arithmetic stays small. go/constant never rounds an +// integer, however large, so every number is made a fraction: literals +// are read as FLOAT, and a power starts from the fraction 1. It rounds +// a fraction that grows past the limit, to a 512-bit float and past +// that float's range to Unknown, but not one it reads from a literal, +// such as 1e-1233, so the limit is checked here. +// +// A number that is not exact is refused wherever it appears: a sum can +// lose the answer entirely (7^1000*7^1000 + 5 - 7^1000*7^1000 would be +// 0), and a remainder, or whether an exponent is whole or odd, cannot be +// read from one. func exact(v constant.Value) bool { - switch constant.Val(v).(type) { - case int64, *big.Int, *big.Rat: - return true - default: - return false - } + r, ok := constant.Val(v).(*big.Rat) + + return ok && r.Num().BitLen() < bitLimit && r.Denom().BitLen() < bitLimit } // normal reports whether f is a normal double, finite and at least diff --git a/internal/calc/calc_test.go b/internal/calc/calc_test.go index cded4e9..21e40ee 100644 --- a/internal/calc/calc_test.go +++ b/internal/calc/calc_test.go @@ -130,6 +130,8 @@ func TestEvaluateModulo(t *testing.T) { // Both operands and their quotient are held exactly, but y times // the whole part of x/y is too large to be. "(5^860*3^630/7) % (5^860/2^998/2^998)": "0.5179219763783696", + // A whole number made from x^0, just below the 4096-bit limit. + "(3^0 + 3^0 + 3^0)^2583 % 10": "7", }) } @@ -243,6 +245,11 @@ func TestEvaluateOutOfRange(t *testing.T) { "0.1^800 * 0.1^800": calc.ErrOutOfRange, // Both operands are held exactly, but their quotient is not. "3^1365 % 7^-1000": calc.ErrOutOfRange, + // The same limit for a whole number made from x^0, which + // go/constant would hold as an integer and never round, and for + // a literal it reads exactly as a fraction past the limit. + "(2^0 + 2^0)^4095 % 10": calc.ErrOutOfRange, + "1e-1233 * 0": calc.ErrOutOfRange, // go/constant reads this literal as 0. "1e-999999999": calc.ErrOutOfRange, "1 / 1e-999999999": calc.ErrOutOfRange, @@ -288,6 +295,20 @@ func TestEvaluateBoundsWork(t *testing.T) { {in: strings.Repeat("9^", 127) + "9", err: calc.ErrOutOfRange}, // The largest power of 3 computed exactly, as often as fits. {in: "0" + strings.Repeat("*3^2583", 36), want: "0"}, + // Whole numbers made from x^0, through each operation. Held as + // integers, which go/constant never rounds, they would escape + // the 4096-bit limit: the first needs about 69 billion bits. + {in: "(((2^0+2^0)^4096)^4096)^4096", err: calc.ErrOutOfRange}, + {in: "(((0^0+0^0)^4096)^4096)^4096", err: calc.ErrOutOfRange}, + {in: "(((-2^0-2^0)^4096)^4096)^4096", err: calc.ErrOutOfRange}, + {in: "((2^0+2^0)^4000*(2^0+2^0)^4000)^4096", err: calc.ErrOutOfRange}, + {in: "((((2^0+2^0)/2^0)^4096)^4096)^4096", err: calc.ErrOutOfRange}, + {in: "((((2^0+2^0) % 3)^4096)^4096)^4096", err: calc.ErrOutOfRange}, + {in: "(((2^0+2^0)^4096)^4096)^4096 * 0", err: calc.ErrOutOfRange}, + // A fraction whose numerator and denominator are both just below + // the limit, and a literal whose exponent is too large to read. + {in: "(3^2583/5^1760)^4096", err: calc.ErrOutOfRange}, + {in: "1e99999999999999999999", err: calc.ErrOutOfRange}, } for _, c := range cases {