From c1c267fd36b0fcac8c8871232eecc1e360173990 Mon Sep 17 00:00:00 2001 From: Denys Vlasenko Date: Sun, 25 Jun 2023 17:42:05 +0200 Subject: shell/math: bash-compatible handling of too large numbers function old new delta parse_with_base - 170 +170 evaluate_string 1477 1309 -168 ------------------------------------------------------------------------------ (add/remove: 1/0 grow/shrink: 0/1 up/down: 170/-168) Total: 2 bytes Signed-off-by: Denys Vlasenko --- shell/ash_test/ash-arith/arith-bignum1.right | 13 ++++ shell/ash_test/ash-arith/arith-bignum1.tests | 17 ++++++ shell/ash_test/ash-arith/arith.right | 3 +- shell/ash_test/ash-arith/arith.tests | 6 +- shell/hush_test/hush-arith/arith-bignum1.right | 13 ++++ shell/hush_test/hush-arith/arith-bignum1.tests | 17 ++++++ shell/hush_test/hush-arith/arith.right | 3 +- shell/hush_test/hush-arith/arith.tests | 6 +- shell/math.c | 84 +++++++++++++++----------- 9 files changed, 118 insertions(+), 44 deletions(-) create mode 100644 shell/ash_test/ash-arith/arith-bignum1.right create mode 100755 shell/ash_test/ash-arith/arith-bignum1.tests create mode 100644 shell/hush_test/hush-arith/arith-bignum1.right create mode 100755 shell/hush_test/hush-arith/arith-bignum1.tests diff --git a/shell/ash_test/ash-arith/arith-bignum1.right b/shell/ash_test/ash-arith/arith-bignum1.right new file mode 100644 index 000000000..42a8016ec --- /dev/null +++ b/shell/ash_test/ash-arith/arith-bignum1.right @@ -0,0 +1,13 @@ +18 digits: 999999999999999999 +19 digits: -8446744073709551617 +20 digits: 7766279631452241919 +18 digits- -999999999999999999 +19 digits- 8446744073709551617 +20 digits- -7766279631452241919 +Hex base#: +16 digits: 9876543210abcedf +17 digits: 876543210abcedfc +18 digits: 76543210abcedfcc +16 digits: 6789abcdef543121 +17 digits: 789abcdef5431204 +18 digits: 89abcdef54312034 diff --git a/shell/ash_test/ash-arith/arith-bignum1.tests b/shell/ash_test/ash-arith/arith-bignum1.tests new file mode 100755 index 000000000..ef8f928bc --- /dev/null +++ b/shell/ash_test/ash-arith/arith-bignum1.tests @@ -0,0 +1,17 @@ +exec 2>&1 +# If the number does not fit in 64 bits, bash uses truncated 64-bit value +# (essentially, it does not check for overflow in "n = n * base + digit" +# calculation). +echo 18 digits: $((999999999999999999)) +echo 19 digits: $((9999999999999999999)) +echo 20 digits: $((99999999999999999999)) +echo 18 digits- $((-999999999999999999)) +echo 19 digits- $((-9999999999999999999)) +echo 20 digits- $((-99999999999999999999)) +echo "Hex base#:" +printf '16 digits: %016x\n' $((16#9876543210abcedf)) +printf '17 digits: %016x\n' $((16#9876543210abcedfc)) +printf '18 digits: %016x\n' $((16#9876543210abcedfcc)) +printf '16 digits: %016x\n' $((-16#9876543210abcedf)) +printf '17 digits: %016x\n' $((-16#9876543210abcedfc)) +printf '18 digits: %016x\n' $((-16#9876543210abcedfcc)) diff --git a/shell/ash_test/ash-arith/arith.right b/shell/ash_test/ash-arith/arith.right index 8bc78b8d1..b2c3f56d8 100644 --- a/shell/ash_test/ash-arith/arith.right +++ b/shell/ash_test/ash-arith/arith.right @@ -80,8 +80,9 @@ other bases 62 62 63 63 missing number after base -0 0 +./arith.tests: line 161: arithmetic syntax error ./arith.tests: line 162: arithmetic syntax error +./arith.tests: line 163: arithmetic syntax error ./arith.tests: line 164: divide by zero ./arith.tests: let: line 165: arithmetic syntax error ./arith.tests: line 166: arithmetic syntax error diff --git a/shell/ash_test/ash-arith/arith.tests b/shell/ash_test/ash-arith/arith.tests index b9cb8ba4c..42cd7fdbf 100755 --- a/shell/ash_test/ash-arith/arith.tests +++ b/shell/ash_test/ash-arith/arith.tests @@ -155,12 +155,12 @@ echo 63 $(( 64#_ )) #ash# # weird bases (error) #ash# echo $(( 3425#56 )) -echo missing number after base -echo 0 $(( 2# )) # these should generate errors +echo missing number after base +( echo $(( 2# )) ) ( echo $(( 7 = 43 )) ) -#ash# echo $(( 2#44 )) +( echo $(( 2#44 )) ) ( echo $(( 44 / 0 )) ) ( let 'jv += $iv' ) ( echo $(( jv += \$iv )) ) diff --git a/shell/hush_test/hush-arith/arith-bignum1.right b/shell/hush_test/hush-arith/arith-bignum1.right new file mode 100644 index 000000000..42a8016ec --- /dev/null +++ b/shell/hush_test/hush-arith/arith-bignum1.right @@ -0,0 +1,13 @@ +18 digits: 999999999999999999 +19 digits: -8446744073709551617 +20 digits: 7766279631452241919 +18 digits- -999999999999999999 +19 digits- 8446744073709551617 +20 digits- -7766279631452241919 +Hex base#: +16 digits: 9876543210abcedf +17 digits: 876543210abcedfc +18 digits: 76543210abcedfcc +16 digits: 6789abcdef543121 +17 digits: 789abcdef5431204 +18 digits: 89abcdef54312034 diff --git a/shell/hush_test/hush-arith/arith-bignum1.tests b/shell/hush_test/hush-arith/arith-bignum1.tests new file mode 100755 index 000000000..ef8f928bc --- /dev/null +++ b/shell/hush_test/hush-arith/arith-bignum1.tests @@ -0,0 +1,17 @@ +exec 2>&1 +# If the number does not fit in 64 bits, bash uses truncated 64-bit value +# (essentially, it does not check for overflow in "n = n * base + digit" +# calculation). +echo 18 digits: $((999999999999999999)) +echo 19 digits: $((9999999999999999999)) +echo 20 digits: $((99999999999999999999)) +echo 18 digits- $((-999999999999999999)) +echo 19 digits- $((-9999999999999999999)) +echo 20 digits- $((-99999999999999999999)) +echo "Hex base#:" +printf '16 digits: %016x\n' $((16#9876543210abcedf)) +printf '17 digits: %016x\n' $((16#9876543210abcedfc)) +printf '18 digits: %016x\n' $((16#9876543210abcedfcc)) +printf '16 digits: %016x\n' $((-16#9876543210abcedf)) +printf '17 digits: %016x\n' $((-16#9876543210abcedfc)) +printf '18 digits: %016x\n' $((-16#9876543210abcedfcc)) diff --git a/shell/hush_test/hush-arith/arith.right b/shell/hush_test/hush-arith/arith.right index df8154f97..d3a978611 100644 --- a/shell/hush_test/hush-arith/arith.right +++ b/shell/hush_test/hush-arith/arith.right @@ -82,7 +82,8 @@ other bases 62 62 63 63 missing number after base -0 0 +hush: arithmetic syntax error +hush: arithmetic syntax error hush: arithmetic syntax error hush: divide by zero hush: can't execute 'let': No such file or directory diff --git a/shell/hush_test/hush-arith/arith.tests b/shell/hush_test/hush-arith/arith.tests index 6b707486c..9f0399816 100755 --- a/shell/hush_test/hush-arith/arith.tests +++ b/shell/hush_test/hush-arith/arith.tests @@ -159,12 +159,12 @@ echo 63 $(( 64#_ )) #ash# # weird bases (error) #ash# echo $(( 3425#56 )) -echo missing number after base -echo 0 $(( 2# )) # these should generate errors +echo missing number after base +( echo $(( 2# )) ) ( echo $(( 7 = 43 )) ) -#ash# echo $(( 2#44 )) +( echo $(( 2#44 )) ) ( echo $(( 44 / 0 )) ) ( let 'jv += $iv' ) ( echo $(( jv += \$iv )) ) diff --git a/shell/math.c b/shell/math.c index ac758639f..fbf5c587e 100644 --- a/shell/math.c +++ b/shell/math.c @@ -531,29 +531,11 @@ static const char op_tokens[] ALIGN1 = { #define END_POINTER (&op_tokens[sizeof(op_tokens)-1]) #if ENABLE_FEATURE_SH_MATH_BASE -static arith_t strto_arith_t(const char *nptr, char **endptr) +static arith_t parse_with_base(const char *nptr, char **endptr, unsigned base) { - unsigned base; - arith_t n; - -# if ENABLE_FEATURE_SH_MATH_64 - n = strtoull(nptr, endptr, 0); -# else - n = strtoul(nptr, endptr, 0); -# endif - if (**endptr != '#' - || (*nptr < '1' || *nptr > '9') - || (n < 2 || n > 64) - ) { - return n; - } + arith_t n = 0; + const char *start = nptr; - /* It's "N#nnnn" or "NN#nnnn" syntax, NN can't start with 0, - * NN is in 2..64 range. - */ - base = (unsigned)n; - n = 0; - nptr = *endptr + 1; for (;;) { unsigned digit = (unsigned)*nptr - '0'; if (digit >= 10 /* not 0..9 */ @@ -582,15 +564,52 @@ static arith_t strto_arith_t(const char *nptr, char **endptr) n = n * base + digit; nptr++; } - /* Note: we do not set errno on bad chars, we just set a pointer - * to the first invalid char. For example, this allows - * "N#" (empty "nnnn" part): 64#+1 is a valid expression, - * it means 64# + 1, whereas 64#~... is not, since ~ is not a valid - * operator. - */ *endptr = (char*)nptr; + /* "64#" and "64#+1" used to be valid expressions, but bash 5.2.15 + * no longer allow such, detect this: + */ +// NB: bash allows $((0x)), this is probably a bug... + if (nptr == start) + *endptr = NULL; /* there weren't any digits, bad */ return n; } + +static arith_t strto_arith_t(const char *nptr, char **endptr) +{ +/* NB: we do not use strtoull here to be bash-compatible: + * $((99999999999999999999)) is 7766279631452241919 + * (the 64-bit truncated value). + */ + unsigned base; + + /* nptr[0] is '0'..'9' here */ + + base = nptr[0] - '0'; + if (base == 0) { /* nptr[0] is '0' */ + base = 8; + if ((nptr[1] | 0x20) == 'x') { + base = 16; + nptr += 2; + } +// NB: bash allows $((0x)), this is probably a bug... + return parse_with_base(nptr, endptr, base); + } + + if (nptr[1] == '#') { + if (base > 1) + return parse_with_base(nptr + 2, endptr, base); + /* else: bash says "invalid arithmetic base" */ + } + + if (isdigit(nptr[1]) && nptr[2] == '#') { + base = 10 * base + (nptr[1] - '0'); + if (base >= 2 && base <= 64) + return parse_with_base(nptr + 3, endptr, base); + /* else: bash says "invalid arithmetic base" */ + } + + return parse_with_base(nptr, endptr, 10); +} #else /* !ENABLE_FEATURE_SH_MATH_BASE */ # if ENABLE_FEATURE_SH_MATH_64 # define strto_arith_t(nptr, endptr) strtoull(nptr, endptr, 0) @@ -702,7 +721,6 @@ evaluate_string(arith_state_t *math_state, const char *expr) dbg("[%d] var:IGNORED", (int)(numstackptr - numstack)); expr = p; numstackptr->var_name = NULL; - push_num0: numstackptr->val = 0; } push_num: @@ -715,11 +733,12 @@ evaluate_string(arith_state_t *math_state, const char *expr) /* Number */ char *end; numstackptr->var_name = NULL; - errno = 0; /* code is smaller compared to using &expr here: */ numstackptr->val = strto_arith_t(expr, &end); expr = end; dbg("[%d] val:%lld", (int)(numstackptr - numstack), numstackptr->val); + if (!expr) /* example: $((10#)) */ + goto syntax_err; /* A number can't be followed by another number, or a variable name. * We'd catch this later anyway, but this would require numstack[] * to be ~twice as deep to handle strings where _every_ char is @@ -728,13 +747,6 @@ evaluate_string(arith_state_t *math_state, const char *expr) */ if (isalnum(*expr) || *expr == '_') goto syntax_err; - if (errno) { -// TODO: bash 5.2.15 does not catch ERANGE (some older version did?). -// $((99999999999999999999)) is 7766279631452241919 (the 64-bit truncated value). -// Our BASE# code does this as well: try $((10#99999999999999999999)), -// but the "ordinary" code path with strtoull() does not do this. - goto push_num0; /* bash compat */ - } goto push_num; } -- cgit v1.2.3