From: Francis De Brabandere <francisdb@gmail.com> The SetLocale/GetLocale commit stored the new LCID on the script context but nothing read it: FormatNumber, FormatCurrency, FormatPercent and FormatDateTime all delegated to oleaut32 helpers that hardcode LOCALE_USER_DEFAULT, so scripts could not actually switch their formatting locale. Replicate the relevant oleaut32 number/currency format logic with the script LCID parameterized, and call GetDateFormatW / GetTimeFormatW directly for FormatDateTime (vbGeneralDate still falls back to VarFormatDateTime). The thread locale is intentionally left untouched: a new test in run.c verifies SetLocale does not leak into GetThreadLocale(). The script LCID is passed explicitly wherever formatting needs it. Expected Format* values in lang.vbs were captured with cscript on Windows (LCIDs 1031/1033/1041). Also reorder SetLocale to validate before mutating ctx->lcid. --- dlls/vbscript/global.c | 356 ++++++++++++++++++++++++----------- dlls/vbscript/tests/lang.vbs | 37 ++++ dlls/vbscript/tests/run.c | 11 ++ 3 files changed, 298 insertions(+), 106 deletions(-) diff --git a/dlls/vbscript/global.c b/dlls/vbscript/global.c index bda190624ce..6ec7c154dd0 100644 --- a/dlls/vbscript/global.c +++ b/dlls/vbscript/global.c @@ -2235,45 +2235,37 @@ static HRESULT Global_SetLocale(BuiltinDisp *This, VARIANT *args, unsigned args_ TRACE("%s\n", args_cnt ? debugstr_variant(args) : "()"); - if(!args_cnt) { - This->ctx->lcid = This->ctx->host_lcid; - return return_int(res, old_lcid); - } - - switch(V_VT(args)) { - case VT_NULL: - return MAKE_VBSERROR(VBSE_ILLEGAL_NULL_USE); - case VT_EMPTY: - This->ctx->lcid = This->ctx->host_lcid; - return return_int(res, old_lcid); - case VT_BSTR: { - int i; - /* Try numeric conversion first (e.g. "1033") */ - hres = to_int(args, &i); - if(SUCCEEDED(hres)) { + if(!args_cnt || V_VT(args) == VT_EMPTY) { + new_lcid = This->ctx->host_lcid; + }else { + switch(V_VT(args)) { + case VT_NULL: + return MAKE_VBSERROR(VBSE_ILLEGAL_NULL_USE); + case VT_BSTR: { + int i; + /* Try numeric conversion first (e.g. "1033"), then locale name (e.g. "en-us"). */ + if(SUCCEEDED(to_int(args, &i))) + new_lcid = i; + else + new_lcid = LocaleNameToLCID(V_BSTR(args), 0); + break; + } + default: { + int i; + hres = to_int(args, &i); + if(FAILED(hres)) + return hres; new_lcid = i; - }else { - /* Try as locale name (e.g. "en-us") */ - new_lcid = LocaleNameToLCID(V_BSTR(args), 0); + break; } - break; - } - default: { - int i; - hres = to_int(args, &i); - if(FAILED(hres)) - return hres; - new_lcid = i; - break; - } + } + + if(!IsValidLocale(new_lcid, LCID_INSTALLED)) + return MAKE_VBSERROR(VBSE_LOCALE_SETTING_NOT_SUPPORTED); } This->ctx->lcid = new_lcid; - return_int(res, old_lcid); - if(!IsValidLocale(new_lcid, LCID_INSTALLED)) - return MAKE_VBSERROR(VBSE_LOCALE_SETTING_NOT_SUPPORTED); - - return S_OK; + return return_int(res, old_lcid); } static HRESULT Global_DateValue(BuiltinDisp *This, VARIANT *arg, unsigned args_cnt, VARIANT *res) @@ -3331,99 +3323,213 @@ static HRESULT Global_ScriptEngineBuildVersion(BuiltinDisp *This, VARIANT *arg, return return_int(res, VBSCRIPT_BUILD_VERSION); } -static HRESULT Global_FormatNumber(BuiltinDisp *This, VARIANT *args, unsigned args_cnt, VARIANT *res) +#define LOCNUM_INTO(lcid, type, field) GetLocaleInfoW((lcid), (type)|LOCALE_RETURN_NUMBER, \ + (LPWSTR)&(field), sizeof(field)/sizeof(WCHAR)) + +/* LCID_US gives the intermediate double-to-BSTR conversion a stable canonical + * form (period decimal, no grouping) that GetNumberFormatW / GetCurrencyFormatW + * will then reformat using the target locale. */ +#define LCID_EN_US MAKELCID(MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), SORT_DEFAULT) + +static HRESULT format_number_lcid(LCID lcid, VARIANT *var, int ndigits, int nleading, + int nparens, int ngrouping, BSTR *out) { - union - { - struct - { - int num_dig, inc_lead, use_parens, group; - } s; - int val[4]; - } int_args = { .s.num_dig = -1, .s.inc_lead = -2, .s.use_parens = -2, .s.group = -2 }; + WCHAR buff[256], decimal[8], thousands[8]; + NUMBERFMTW fmt; + VARIANT v; HRESULT hres; - BSTR str; - int i; - TRACE("\n"); + *out = NULL; + V_VT(&v) = VT_EMPTY; + hres = VariantCopyInd(&v, var); + if(FAILED(hres)) return hres; + hres = VariantChangeTypeEx(&v, &v, LCID_EN_US, 0, VT_BSTR); + if(FAILED(hres)) return hres; - assert(1 <= args_cnt && args_cnt <= 5); + if(ndigits < 0) + LOCNUM_INTO(lcid, LOCALE_IDIGITS, fmt.NumDigits); + else + fmt.NumDigits = ndigits; - for (i = 1; i < args_cnt; ++i) - { - if (V_VT(args+i) == VT_ERROR) continue; - if (V_VT(args+i) == VT_NULL) return MAKE_VBSERROR(VBSE_ILLEGAL_NULL_USE); - if (FAILED(hres = to_int(args+i, &int_args.val[i-1]))) return hres; + if(nleading == -2) + LOCNUM_INTO(lcid, LOCALE_ILZERO, fmt.LeadingZero); + else + fmt.LeadingZero = nleading == -1 ? 1 : 0; + + if(ngrouping == -2) { + WCHAR grouping[10] = {0}; + GetLocaleInfoW(lcid, LOCALE_SGROUPING, grouping, ARRAY_SIZE(grouping)); + fmt.Grouping = grouping[2] == '2' ? 32 : grouping[0] - '0'; + }else { + fmt.Grouping = ngrouping == -1 ? 3 : 0; } - hres = VarFormatNumber(args, int_args.s.num_dig, int_args.s.inc_lead, int_args.s.use_parens, - int_args.s.group, 0, &str); - if (FAILED(hres)) return hres; + if(nparens == -2) + LOCNUM_INTO(lcid, LOCALE_INEGNUMBER, fmt.NegativeOrder); + else + fmt.NegativeOrder = nparens == -1 ? 0 : 1; - return return_bstr(res, str); + fmt.lpDecimalSep = decimal; + GetLocaleInfoW(lcid, LOCALE_SDECIMAL, decimal, ARRAY_SIZE(decimal)); + fmt.lpThousandSep = thousands; + GetLocaleInfoW(lcid, LOCALE_STHOUSAND, thousands, ARRAY_SIZE(thousands)); + + if(!GetNumberFormatW(lcid, 0, V_BSTR(&v), &fmt, buff, ARRAY_SIZE(buff))) { + SysFreeString(V_BSTR(&v)); + return DISP_E_TYPEMISMATCH; + } + SysFreeString(V_BSTR(&v)); + + *out = SysAllocString(buff); + return *out ? S_OK : E_OUTOFMEMORY; } -static HRESULT Global_FormatCurrency(BuiltinDisp *This, VARIANT *args, unsigned args_cnt, VARIANT *res) +static HRESULT format_currency_lcid(LCID lcid, VARIANT *var, int ndigits, int nleading, + int nparens, int ngrouping, BSTR *out) { - union - { - struct - { - int num_dig, inc_lead, use_parens, group; - } s; - int val[4]; - } int_args = { .s.num_dig = -1, .s.inc_lead = -2, .s.use_parens = -2, .s.group = -2 }; + WCHAR buff[256], decimal[8], thousands[4], currency[13]; + CURRENCYFMTW fmt; + VARIANT v; + HRESULT hres; + CY cy; + + *out = NULL; + if(V_VT(var) == VT_BSTR || V_VT(var) == (VT_BSTR|VT_BYREF)) { + hres = VarCyFromStr(V_ISBYREF(var) ? *V_BSTRREF(var) : V_BSTR(var), lcid, 0, &cy); + if(FAILED(hres)) return hres; + V_VT(&v) = VT_CY; + V_CY(&v) = cy; + }else { + V_VT(&v) = VT_EMPTY; + hres = VariantCopyInd(&v, var); + if(FAILED(hres)) return hres; + } + hres = VariantChangeTypeEx(&v, &v, lcid, 0, VT_BSTR); + if(FAILED(hres)) return hres; + + if(ndigits < 0) + LOCNUM_INTO(lcid, LOCALE_IDIGITS, fmt.NumDigits); + else + fmt.NumDigits = ndigits; + + if(nleading == -2) + LOCNUM_INTO(lcid, LOCALE_ILZERO, fmt.LeadingZero); + else + fmt.LeadingZero = nleading == -1 ? 1 : 0; + + if(ngrouping == -2) { + WCHAR grouping[10] = {0}; + GetLocaleInfoW(lcid, LOCALE_SGROUPING, grouping, ARRAY_SIZE(grouping)); + fmt.Grouping = grouping[2] == '2' ? 32 : grouping[0] - '0'; + }else { + fmt.Grouping = ngrouping == -1 ? 3 : 0; + } + + if(nparens == -2) + LOCNUM_INTO(lcid, LOCALE_INEGCURR, fmt.NegativeOrder); + else + fmt.NegativeOrder = nparens == -1 ? 0 : 1; + + LOCNUM_INTO(lcid, LOCALE_ICURRENCY, fmt.PositiveOrder); + fmt.lpDecimalSep = decimal; + GetLocaleInfoW(lcid, LOCALE_SDECIMAL, decimal, ARRAY_SIZE(decimal)); + fmt.lpThousandSep = thousands; + GetLocaleInfoW(lcid, LOCALE_STHOUSAND, thousands, ARRAY_SIZE(thousands)); + fmt.lpCurrencySymbol = currency; + GetLocaleInfoW(lcid, LOCALE_SCURRENCY, currency, ARRAY_SIZE(currency)); + + if(!GetCurrencyFormatW(lcid, 0, V_BSTR(&v), &fmt, buff, ARRAY_SIZE(buff))) { + SysFreeString(V_BSTR(&v)); + return DISP_E_TYPEMISMATCH; + } + SysFreeString(V_BSTR(&v)); + + *out = SysAllocString(buff); + return *out ? S_OK : E_OUTOFMEMORY; +} + +static HRESULT parse_format_args(VARIANT *args, unsigned args_cnt, int *vals) +{ + HRESULT hres; + unsigned i; + for(i = 1; i < args_cnt; ++i) { + if(V_VT(args+i) == VT_ERROR) continue; + if(V_VT(args+i) == VT_NULL) return MAKE_VBSERROR(VBSE_ILLEGAL_NULL_USE); + hres = to_int(args+i, &vals[i-1]); + if(FAILED(hres)) return hres; + } + return S_OK; +} + +static HRESULT Global_FormatNumber(BuiltinDisp *This, VARIANT *args, unsigned args_cnt, VARIANT *res) +{ + int a[4] = {-1, -2, -2, -2}; HRESULT hres; BSTR str; - int i; TRACE("\n"); - assert(1 <= args_cnt && args_cnt <= 5); - for (i = 1; i < args_cnt; ++i) - { - if (V_VT(args+i) == VT_ERROR) continue; - if (V_VT(args+i) == VT_NULL) return MAKE_VBSERROR(VBSE_ILLEGAL_NULL_USE); - if (FAILED(hres = to_int(args+i, &int_args.val[i-1]))) return hres; - } + hres = parse_format_args(args, args_cnt, a); + if(FAILED(hres)) return hres; + hres = format_number_lcid(This->ctx->lcid, args, a[0], a[1], a[2], a[3], &str); + if(FAILED(hres)) return hres; + return return_bstr(res, str); +} + +static HRESULT Global_FormatCurrency(BuiltinDisp *This, VARIANT *args, unsigned args_cnt, VARIANT *res) +{ + int a[4] = {-1, -2, -2, -2}; + HRESULT hres; + BSTR str; - hres = VarFormatCurrency(args, int_args.s.num_dig, int_args.s.inc_lead, int_args.s.use_parens, - int_args.s.group, 0, &str); - if (FAILED(hres)) return hres; + TRACE("\n"); + assert(1 <= args_cnt && args_cnt <= 5); + hres = parse_format_args(args, args_cnt, a); + if(FAILED(hres)) return hres; + hres = format_currency_lcid(This->ctx->lcid, args, a[0], a[1], a[2], a[3], &str); + if(FAILED(hres)) return hres; return return_bstr(res, str); } static HRESULT Global_FormatPercent(BuiltinDisp *This, VARIANT *args, unsigned args_cnt, VARIANT *res) { - union - { - struct - { - int num_dig, inc_lead, use_parens, group; - } s; - int val[4]; - } int_args = { .s.num_dig = -1, .s.inc_lead = -2, .s.use_parens = -2, .s.group = -2 }; + int a[4] = {-1, -2, -2, -2}; + WCHAR buff[258]; + DWORD len; + VARIANT v; HRESULT hres; BSTR str; - int i; TRACE("\n"); - assert(1 <= args_cnt && args_cnt <= 5); - for (i = 1; i < args_cnt; ++i) - { - if (V_VT(args+i) == VT_ERROR) continue; - if (V_VT(args+i) == VT_NULL) return MAKE_VBSERROR(VBSE_ILLEGAL_NULL_USE); - if (FAILED(hres = to_int(args+i, &int_args.val[i-1]))) return hres; - } - - hres = VarFormatPercent(args, int_args.s.num_dig, int_args.s.inc_lead, int_args.s.use_parens, - int_args.s.group, 0, &str); - if (FAILED(hres)) return hres; + hres = parse_format_args(args, args_cnt, a); + if(FAILED(hres)) return hres; + V_VT(&v) = VT_EMPTY; + hres = VariantCopyInd(&v, args); + if(FAILED(hres)) return hres; + hres = VariantChangeTypeEx(&v, &v, This->ctx->lcid, 0, VT_R8); + if(FAILED(hres)) return hres; + if(V_R8(&v) > (1e300 / 100.0)) return DISP_E_OVERFLOW; + V_R8(&v) *= 100.0; + + hres = format_number_lcid(This->ctx->lcid, &v, a[0], a[1], a[2], a[3], &str); + if(FAILED(hres)) return hres; + + len = lstrlenW(str); + if(len && str[len-1] == ')') { + memcpy(buff, str, (len-1) * sizeof(WCHAR)); + lstrcpyW(buff + len - 1, L"%)"); + }else { + memcpy(buff, str, len * sizeof(WCHAR)); + lstrcpyW(buff + len, L"%"); + } + SysFreeString(str); + str = SysAllocString(buff); + if(!str) return E_OUTOFMEMORY; return return_bstr(res, str); } @@ -3436,6 +3542,10 @@ static HRESULT Global_GetLocale(BuiltinDisp *This, VARIANT *args, unsigned args_ static HRESULT Global_FormatDateTime(BuiltinDisp *This, VARIANT *args, unsigned args_cnt, VARIANT *res) { int format = 0; + LCID lcid = This->ctx->lcid; + WCHAR buff[256]; + VARIANT v; + SYSTEMTIME st; HRESULT hres; BSTR str; @@ -3443,21 +3553,55 @@ static HRESULT Global_FormatDateTime(BuiltinDisp *This, VARIANT *args, unsigned assert(1 <= args_cnt && args_cnt <= 2); - if (V_VT(args) == VT_NULL) + if(V_VT(args) == VT_NULL) return MAKE_VBSERROR(VBSE_TYPE_MISMATCH); - if (args_cnt == 2) - { - if (V_VT(args+1) == VT_NULL) return MAKE_VBSERROR(VBSE_ILLEGAL_NULL_USE); - if (V_VT(args+1) != VT_ERROR) - { - if (FAILED(hres = to_int(args+1, &format))) return hres; - } + if(args_cnt == 2 && V_VT(args+1) != VT_ERROR) { + if(V_VT(args+1) == VT_NULL) return MAKE_VBSERROR(VBSE_ILLEGAL_NULL_USE); + hres = to_int(args+1, &format); + if(FAILED(hres)) return hres; } - hres = VarFormatDateTime(args, format, 0, &str); - if (FAILED(hres)) return hres; + /* vbGeneralDate (0) falls back to oleaut32 — the "show date if date, time if + * time, both otherwise" logic depends on the tokenizer; ctx->lcid does not + * yet propagate. */ + if(format == 0) { + hres = VarFormatDateTime(args, format, 0, &str); + if(FAILED(hres)) return hres; + return return_bstr(res, str); + } + + V_VT(&v) = VT_EMPTY; + hres = VariantCopyInd(&v, args); + if(FAILED(hres)) return hres; + hres = VariantChangeTypeEx(&v, &v, lcid, 0, VT_DATE); + if(FAILED(hres)) return hres; + if(!VariantTimeToSystemTime(V_DATE(&v), &st)) + return E_INVALIDARG; + + switch(format) { + case 1: /* vbLongDate */ + if(!GetDateFormatW(lcid, DATE_LONGDATE, &st, NULL, buff, ARRAY_SIZE(buff))) + return E_FAIL; + break; + case 2: /* vbShortDate */ + if(!GetDateFormatW(lcid, DATE_SHORTDATE, &st, NULL, buff, ARRAY_SIZE(buff))) + return E_FAIL; + break; + case 3: /* vbLongTime */ + if(!GetTimeFormatW(lcid, 0, &st, NULL, buff, ARRAY_SIZE(buff))) + return E_FAIL; + break; + case 4: /* vbShortTime */ + if(!GetTimeFormatW(lcid, TIME_NOSECONDS|TIME_FORCE24HOURFORMAT, &st, NULL, buff, ARRAY_SIZE(buff))) + return E_FAIL; + break; + default: + return E_INVALIDARG; + } + str = SysAllocString(buff); + if(!str) return E_OUTOFMEMORY; return return_bstr(res, str); } diff --git a/dlls/vbscript/tests/lang.vbs b/dlls/vbscript/tests/lang.vbs index d139a2bf282..3b290a5a014 100644 --- a/dlls/vbscript/tests/lang.vbs +++ b/dlls/vbscript/tests/lang.vbs @@ -3584,4 +3584,41 @@ Call ok(Err.Number = 506, "New non-class variable: err.number = " & Err.Number) On Error GoTo 0 +' === GetLocale / SetLocale / locale-sensitive Format* === +Dim origLcid +origLcid = GetLocale() +Call ok(origLcid > 0, "GetLocale initial: " & origLcid) + +' SetLocale returns the previous LCID. +Call ok(SetLocale(1033) = origLcid, "SetLocale(1033) return value") +Call ok(GetLocale() = 1033, "GetLocale after SetLocale(1033): " & GetLocale()) + +' en-US formatting +Call ok(FormatNumber(1234567.89) = "1,234,567.89", "FormatNumber en-US: " & FormatNumber(1234567.89)) +Call ok(FormatCurrency(1234567.89) = "$1,234,567.89", "FormatCurrency en-US: " & FormatCurrency(1234567.89)) +Call ok(FormatPercent(0.1234) = "12.34%", "FormatPercent en-US: " & FormatPercent(0.1234)) +Call ok(FormatDateTime(DateSerial(2026,3,15), 1) = "Sunday, March 15, 2026", _ + "FormatDateTime en-US: " & FormatDateTime(DateSerial(2026,3,15), 1)) + +' de-DE: '.' thousands, ',' decimal +Call SetLocale(1031) +Call ok(GetLocale() = 1031, "GetLocale after SetLocale(1031)") +Call ok(FormatNumber(1234567.89) = "1.234.567,89", "FormatNumber de-DE: " & FormatNumber(1234567.89)) +Call ok(FormatPercent(0.1234) = "12,34%", "FormatPercent de-DE: " & FormatPercent(0.1234)) + +' ja-JP: Anglo number formatting +Call SetLocale(1041) +Call ok(GetLocale() = 1041, "GetLocale after SetLocale(1041)") +Call ok(FormatNumber(1234567.89) = "1,234,567.89", "FormatNumber ja-JP: " & FormatNumber(1234567.89)) +Call ok(FormatPercent(0.1234) = "12.34%", "FormatPercent ja-JP: " & FormatPercent(0.1234)) + +' CStr goes through VariantChangeType with LCID=0 (LOCALE_USER_DEFAULT). +' Plumbing ctx->lcid into that path is follow-up work. +Call SetLocale(1031) +Call todo_wine_ok(CStr(1.5) = "1,5", "CStr(1.5) de-DE: " & CStr(1.5)) + +' Restore original locale. +Call SetLocale(origLcid) +Call ok(GetLocale() = origLcid, "restore: GetLocale = " & GetLocale()) + reportSuccess() diff --git a/dlls/vbscript/tests/run.c b/dlls/vbscript/tests/run.c index f4b4829f902..3d347583c63 100644 --- a/dlls/vbscript/tests/run.c +++ b/dlls/vbscript/tests/run.c @@ -3842,12 +3842,23 @@ static void run_from_res(const char *name) test_name = ""; } +static void test_setlocale_does_not_touch_thread_locale(void) +{ + LCID thread_lcid_before = GetThreadLocale(); + parse_script_w(L"SetLocale 1036\n"); + ok(GetThreadLocale() == thread_lcid_before, + "SetLocale leaked to thread locale: before=%#lx after=%#lx\n", + thread_lcid_before, GetThreadLocale()); +} + static void run_tests(void) { HRESULT hres; strict_dispid_check = TRUE; + test_setlocale_does_not_touch_thread_locale(); + parse_script_w(L""); parse_script_w(L"' empty ;"); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10504