From: Francis De Brabandere <francisdb@gmail.com> The custom pow()-based real-number conversion was historical: when this code was written, wine could not link against ucrtbase, so the lexer reimplemented the conversion. That constraint is gone, and the custom code had two correctness gaps - subnormal literals (e.g. 1e-309) rounded to zero because pow(10, 309) overflows to infinity, and values near DBL_MAX could round through the same infinity edge. Hand the literal span to _wcstod_l with a C locale instead, which handles rounding, subnormals, and overflow per IEEE 754 - and is locale-invariant by construction, matching VBScript source semantics where the decimal separator is always '.' regardless of runtime SetLocale. Above-DBL_MAX literals (1e309, 1.8e308) continue to raise err 1031 as before. --- dlls/vbscript/lex.c | 120 +++++++++++++++++++--------------- dlls/vbscript/tests/lang.vbs | 25 +++++++ dlls/vbscript/vbscript.h | 1 + dlls/vbscript/vbscript_main.c | 1 + 4 files changed, 93 insertions(+), 54 deletions(-) diff --git a/dlls/vbscript/lex.c b/dlls/vbscript/lex.c index 81cc67e037b..880c0ed5ded 100644 --- a/dlls/vbscript/lex.c +++ b/dlls/vbscript/lex.c @@ -17,7 +17,9 @@ */ #include <assert.h> +#include <errno.h> #include <limits.h> +#include <locale.h> #include <math.h> #include "vbscript.h" @@ -266,90 +268,100 @@ static int parse_date_literal(parser_ctx_t *ctx, DATE *ret) return tDate; } +/* Cached C locale, lazily created and reused for the lifetime of the process. + * Used by parse_numeric_literal to feed _wcstod_l for double conversion that + * is locale-invariant regardless of the host's runtime locale. */ +static _locale_t c_locale; + +static _locale_t get_c_locale(void) +{ + _locale_t l; + + if(c_locale) + return c_locale; + + if(!(l = _create_locale(LC_ALL, "C"))) + return NULL; + if(InterlockedCompareExchangePointer((void **)&c_locale, l, NULL)) + _free_locale(l); + return c_locale; +} + +void release_c_locale(void) +{ + if(c_locale) { + _free_locale(c_locale); + c_locale = NULL; + } +} + static int parse_numeric_literal(parser_ctx_t *ctx, void **ret) { - BOOL use_int = TRUE; + const WCHAR *start = ctx->ptr; + BOOL is_double = FALSE, overflow = FALSE; LONGLONG d = 0, hlp; - int exp = 0; + _locale_t locale; + WCHAR buf[64]; + WCHAR *endptr; + size_t len; double r; if(*ctx->ptr == '0' && !('0' <= ctx->ptr[1] && ctx->ptr[1] <= '9') && ctx->ptr[1] != '.') return *ctx->ptr++; + /* Walk integer digits, accumulating into d while it fits. */ while(ctx->ptr < ctx->end && is_digit(*ctx->ptr)) { - hlp = d*10 + *(ctx->ptr++) - '0'; - if(d>MAXLONGLONG/10 || hlp<0) { - exp++; - break; + if(!overflow) { + hlp = d*10 + *ctx->ptr - '0'; + if(d > MAXLONGLONG/10 || hlp < 0) + overflow = TRUE; + else + d = hlp; } - else - d = hlp; - } - while(ctx->ptr < ctx->end && is_digit(*ctx->ptr)) { - exp++; ctx->ptr++; } if(*ctx->ptr == '.') { - use_int = FALSE; + is_double = TRUE; ctx->ptr++; - - while(ctx->ptr < ctx->end && is_digit(*ctx->ptr)) { - hlp = d*10 + *(ctx->ptr++) - '0'; - if(d>MAXLONGLONG/10 || hlp<0) - break; - - d = hlp; - exp--; - } while(ctx->ptr < ctx->end && is_digit(*ctx->ptr)) ctx->ptr++; } if(*ctx->ptr == 'e' || *ctx->ptr == 'E') { - int e = 0, sign = 1; - + is_double = TRUE; ctx->ptr++; - if(*ctx->ptr == '-') { - ctx->ptr++; - sign = -1; - }else if(*ctx->ptr == '+') { + if(*ctx->ptr == '+' || *ctx->ptr == '-') ctx->ptr++; - } - - if(!is_digit(*ctx->ptr)) { + if(!is_digit(*ctx->ptr)) return lex_error(ctx, MAKE_VBSERROR(VBSE_INVALID_NUMBER)); - } - - use_int = FALSE; - - do { - e = e*10 + *(ctx->ptr++) - '0'; - if(sign == -1 && -e+exp < -(INT_MAX/100)) { - /* The literal will be rounded to 0 anyway. */ - while(is_digit(*ctx->ptr)) - ctx->ptr++; - *(double*)ret = 0; - return tDouble; - } - - if(sign*e + exp > INT_MAX/100) { - return lex_error(ctx, MAKE_VBSERROR(VBSE_INVALID_NUMBER)); - } - } while(is_digit(*ctx->ptr)); - - exp += sign*e; + while(is_digit(*ctx->ptr)) + ctx->ptr++; } - if(use_int && (LONG)d == d) { + if(!is_double && !overflow && (LONG)d == d) { *(LONG*)ret = d; return tInt; } - r = exp>=0 ? d*pow(10, exp) : d/pow(10, -exp); - if(isinf(r)) { + /* Delegate the real-number conversion to ucrtbase's locale-aware parser + * with a C locale, which handles rounding, subnormals, and overflow per + * IEEE 754. The lexer above has already determined the literal span; we + * just need to hand wcstod a null-terminated copy. */ + len = ctx->ptr - start; + if(len >= ARRAY_SIZE(buf)) + return lex_error(ctx, MAKE_VBSERROR(VBSE_INVALID_NUMBER)); + memcpy(buf, start, len * sizeof(WCHAR)); + buf[len] = 0; + + locale = get_c_locale(); + if(!locale) + return lex_error(ctx, MAKE_VBSERROR(VBSE_OUT_OF_MEMORY)); + errno = 0; + r = _wcstod_l(buf, &endptr, locale); + + if(errno == ERANGE && isinf(r)) return lex_error(ctx, MAKE_VBSERROR(VBSE_INVALID_NUMBER)); - } *(double*)ret = r; return tDouble; diff --git a/dlls/vbscript/tests/lang.vbs b/dlls/vbscript/tests/lang.vbs index d8d612bd389..287a04d913f 100644 --- a/dlls/vbscript/tests/lang.vbs +++ b/dlls/vbscript/tests/lang.vbs @@ -439,6 +439,31 @@ Call ok(getVT(# 1/1/2011 #) = "VT_DATE", "getVT(# 1/1/2011 #) is not VT_DATE") Call ok(getVT(1e2) = "VT_R8", "getVT(1e2) is not VT_R8") Call ok(getVT(1e0) = "VT_R8", "getVT(1e0) is not VT_R8") Call ok(getVT(0.1e2) = "VT_R8", "getVT(0.1e2) is not VT_R8") +' Subnormal doubles: literals smaller than DBL_MIN (~1e-308) parse as positive +' subnormals down to ~5e-324; anything smaller rounds to zero. +Call ok(1e-309 > 0, "1e-309 should be subnormal positive, got " & 1e-309) +Call ok(1e-320 > 0, "1e-320 should be subnormal positive, got " & 1e-320) +Call ok(5e-324 > 0, "5e-324 should be subnormal positive, got " & 5e-324) +Call ok(-1e-309 < 0, "-1e-309 should be subnormal negative, got " & -1e-309) +Call ok(getVT(1e-309) = "VT_R8", "getVT(1e-309) = " & getVT(1e-309)) +Call ok(1e-400 = 0, "1e-400 should round to 0, got " & 1e-400) +' Boundary at DBL_MAX: the parser must not collapse e308 to infinity. +Call ok(1e308 > 0, "1e308 should be finite positive, got " & 1e308) +Call ok(1.7976931348623157e308 > 1e307, "DBL_MAX should be larger than 1e307") +Call ok(getVT(1.7976931348623157e308) = "VT_R8", "getVT(DBL_MAX) is not VT_R8") + +' Above DBL_MAX must raise err 1031 (Invalid number) at compile time, not +' silently overflow to infinity. +Sub TestNumericOverflow + On Error Resume Next + Err.Clear : Execute "Dim r : r = 1e309" + Call ok(Err.Number = 1031, "1e309 should err 1031, got " & Err.Number) + Err.Clear : Execute "Dim r : r = -1e309" + Call ok(Err.Number = 1031, "-1e309 should err 1031, got " & Err.Number) + Err.Clear : Execute "Dim r : r = 1.8e308" + Call ok(Err.Number = 1031, "1.8e308 should err 1031, got " & Err.Number) +End Sub +Call TestNumericOverflow Call ok(getVT(1 & 100000) = "VT_BSTR", "getVT(1 & 100000) is not VT_BSTR") Call ok(getVT(-empty) = "VT_I2", "getVT(-empty) = " & getVT(-empty)) Call ok(getVT(-null) = "VT_NULL", "getVT(-null) = " & getVT(-null)) diff --git a/dlls/vbscript/vbscript.h b/dlls/vbscript/vbscript.h index eda5d8740b4..7e902badb0f 100644 --- a/dlls/vbscript/vbscript.h +++ b/dlls/vbscript/vbscript.h @@ -436,6 +436,7 @@ HRESULT get_builtin_id(BuiltinDisp*,const WCHAR*,DISPID*); HRESULT array_access(SAFEARRAY *array, DISPPARAMS *dp, VARIANT **ret); void release_regexp_typelib(void); +void release_c_locale(void); HRESULT get_dispatch_typeinfo(ITypeInfo**); static inline BOOL is_int32(double d) diff --git a/dlls/vbscript/vbscript_main.c b/dlls/vbscript/vbscript_main.c index fe63c7660a7..4f424a97727 100644 --- a/dlls/vbscript/vbscript_main.c +++ b/dlls/vbscript/vbscript_main.c @@ -272,6 +272,7 @@ BOOL WINAPI DllMain(HINSTANCE hInstDLL, DWORD fdwReason, LPVOID lpv) if (lpv) break; if (dispatch_typeinfo) ITypeInfo_Release(dispatch_typeinfo); release_regexp_typelib(); + release_c_locale(); } return TRUE; -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10937