Bug 1846224 - Add a JavaScript per-realm locale override. r=jandem

Differential Revision: https://phabricator.services.mozilla.com/D184944
This commit is contained in:
Tom Schuster 2023-09-08 13:38:11 +00:00
Родитель 0e5d30db53
Коммит 847fe59347
11 изменённых файлов: 148 добавлений и 55 удалений

Просмотреть файл

@ -18,6 +18,7 @@
#include "jstypes.h" // JS_PUBLIC_API
#include "js/Class.h" // JSTraceOp
#include "js/RefCounted.h"
struct JS_PUBLIC_API JSContext;
class JS_PUBLIC_API JSObject;
@ -60,6 +61,14 @@ enum class WeakRefSpecifier {
EnabledWithoutCleanupSome
};
struct LocaleString : js::RefCounted<LocaleString> {
const char* chars_;
explicit LocaleString(const char* chars) : chars_(chars) {}
auto chars() const { return chars_; }
};
/**
* RealmCreationOptions specifies options relevant to creating a new realm, that
* are either immutable characteristics of that realm or that are discarded
@ -259,6 +268,9 @@ class JS_PUBLIC_API RealmCreationOptions {
return *this;
}
RefPtr<LocaleString> locale() const { return locale_; }
RealmCreationOptions& setLocaleCopyZ(const char* locale);
// Always use the fdlibm implementation of math functions instead of the
// platform native libc implementations. Useful for fingerprinting protection
// and cross-platform consistency.
@ -282,6 +294,7 @@ class JS_PUBLIC_API RealmCreationOptions {
Zone* zone_;
};
uint64_t profilerRealmID_ = 0;
RefPtr<LocaleString> locale_;
WeakRefSpecifier weakRefs_ = WeakRefSpecifier::Disabled;
bool invisibleToDebugger_ = false;
bool preserveJitCode_ = false;

Просмотреть файл

@ -7813,34 +7813,12 @@ static bool SetDefaultLocale(JSContext* cx, unsigned argc, Value* vp) {
}
if (args[0].isString() && !args[0].toString()->empty()) {
Rooted<JSLinearString*> str(cx, args[0].toString()->ensureLinear(cx));
if (!str) {
return false;
}
if (!StringIsAscii(str)) {
ReportUsageErrorASCII(cx, callee,
"First argument contains non-ASCII characters");
return false;
}
UniqueChars locale = JS_EncodeStringToASCII(cx, str);
RootedString str(cx, args[0].toString());
UniqueChars locale = StringToLocale(cx, callee, str);
if (!locale) {
return false;
}
bool containsOnlyValidBCP47Characters =
mozilla::IsAsciiAlpha(locale[0]) &&
std::all_of(locale.get(), locale.get() + str->length(), [](auto c) {
return mozilla::IsAsciiAlphanumeric(c) || c == '-';
});
if (!containsOnlyValidBCP47Characters) {
ReportUsageErrorASCII(cx, callee,
"First argument should be a BCP47 language tag");
return false;
}
if (!JS_SetDefaultLocale(cx->runtime(), locale.get())) {
ReportOutOfMemory(cx);
return false;

Просмотреть файл

@ -255,3 +255,36 @@ bool js::ParseDebugMetadata(JSContext* cx, JS::Handle<JSObject*> opts,
return true;
}
JS::UniqueChars js::StringToLocale(JSContext* cx, JS::Handle<JSObject*> callee,
JS::Handle<JSString*> str_) {
Rooted<JSLinearString*> str(cx, str_->ensureLinear(cx));
if (!str) {
return nullptr;
}
if (!StringIsAscii(str)) {
ReportUsageErrorASCII(cx, callee,
"First argument contains non-ASCII characters");
return nullptr;
}
UniqueChars locale = JS_EncodeStringToASCII(cx, str);
if (!locale) {
return nullptr;
}
bool containsOnlyValidBCP47Characters =
mozilla::IsAsciiAlpha(locale[0]) &&
std::all_of(locale.get(), locale.get() + str->length(), [](auto c) {
return mozilla::IsAsciiAlphanumeric(c) || c == '-';
});
if (!containsOnlyValidBCP47Characters) {
ReportUsageErrorASCII(cx, callee,
"First argument should be a BCP47 language tag");
return nullptr;
}
return locale;
}

Просмотреть файл

@ -59,6 +59,10 @@ JSObject* CreateScriptPrivate(JSContext* cx,
JS::MutableHandle<JS::Value> privateValue,
JS::MutableHandle<JSString*> elementAttributeName);
[[nodiscard]] JS::UniqueChars StringToLocale(JSContext* cx,
JS::Handle<JSObject*> callee,
JS::Handle<JSString*> str_);
} /* namespace js */
#endif /* builtin_TestingUtility_h */

Просмотреть файл

@ -0,0 +1,19 @@
// |jit-test| skip-if: typeof Intl === 'undefined'
function test(locale, timeZone) {
let global = newGlobal({locale, forceUTC: true});
const constructors = ["Collator", "DateTimeFormat", "ListFormat",
"NumberFormat", "PluralRules", "RelativeTimeFormat"];
for (const constructor of constructors) {
let intl = new global.Intl[constructor];
assertEq(intl.resolvedOptions().locale, locale);
}
const date = new global.Date(2012, 0, 10);
let tzRE = /\(([^\)]+)\)/;
assertEq(tzRE.exec(date)[1], timeZone)
}
test("de-CH", "Koordinierte Weltzeit");
test("en", "Coordinated Universal Time");

Просмотреть файл

@ -1722,6 +1722,24 @@ JS::RealmCreationOptions& JS::RealmCreationOptions::setCoopAndCoepEnabled(
return *this;
}
JS::RealmCreationOptions& JS::RealmCreationOptions::setLocaleCopyZ(
const char* locale) {
const size_t size = strlen(locale) + 1;
AutoEnterOOMUnsafeRegion oomUnsafe;
char* memoryPtr = js_pod_malloc<char>(sizeof(LocaleString) + size);
if (!memoryPtr) {
oomUnsafe.crash("RealmCreationOptions::setLocaleCopyZ");
}
char* localePtr = memoryPtr + sizeof(LocaleString);
memcpy(localePtr, locale, size);
locale_ = new (memoryPtr) LocaleString(localePtr);
return *this;
}
const JS::RealmBehaviors& JS::RealmBehaviorsRef(JS::Realm* realm) {
return realm->behaviors();
}

Просмотреть файл

@ -156,7 +156,8 @@ class DateTimeHelper {
static double UTC(DateTimeInfo::ForceUTC forceUTC, double t);
static JSString* timeZoneComment(JSContext* cx,
DateTimeInfo::ForceUTC forceUTC,
double utcTime, double localTime);
const char* locale, double utcTime,
double localTime);
#if !JS_HAS_INTL_API
static size_t formatTime(DateTimeInfo::ForceUTC forceUTC, char* buf,
size_t buflen, const char* fmt, double utcTime,
@ -2944,8 +2945,8 @@ static bool date_toJSON(JSContext* cx, unsigned argc, Value* vp) {
#if JS_HAS_INTL_API
JSString* DateTimeHelper::timeZoneComment(JSContext* cx,
DateTimeInfo::ForceUTC forceUTC,
double utcTime, double localTime) {
const char* locale = cx->runtime()->getDefaultLocale();
const char* locale, double utcTime,
double localTime) {
if (!locale) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEFAULT_LOCALE_ERROR);
@ -3018,7 +3019,8 @@ size_t DateTimeHelper::formatTime(DateTimeInfo::ForceUTC forceUTC, char* buf,
JSString* DateTimeHelper::timeZoneComment(JSContext* cx,
DateTimeInfo::ForceUTC forceUTC,
double utcTime, double localTime) {
const char* locale, double utcTime,
double localTime) {
char tzbuf[100];
size_t tzlen =
@ -3055,7 +3057,7 @@ JSString* DateTimeHelper::timeZoneComment(JSContext* cx,
enum class FormatSpec { DateTime, Date, Time };
static bool FormatDate(JSContext* cx, DateTimeInfo::ForceUTC forceUTC,
double utcTime, FormatSpec format,
const char* locale, double utcTime, FormatSpec format,
MutableHandleValue rval) {
if (!std::isfinite(utcTime)) {
rval.setString(cx->names().Invalid_Date_);
@ -3091,8 +3093,8 @@ static bool FormatDate(JSContext* cx, DateTimeInfo::ForceUTC forceUTC,
// also means the time zone string may not fit into Latin-1.
// Get a time zone string from the OS or ICU to include as a comment.
timeZoneComment =
DateTimeHelper::timeZoneComment(cx, forceUTC, utcTime, localTime);
timeZoneComment = DateTimeHelper::timeZoneComment(cx, forceUTC, locale,
utcTime, localTime);
if (!timeZoneComment) {
return false;
}
@ -3142,9 +3144,12 @@ static bool FormatDate(JSContext* cx, DateTimeInfo::ForceUTC forceUTC,
}
#if !JS_HAS_INTL_API
static bool ToLocaleFormatHelper(JSContext* cx, DateTimeInfo::ForceUTC forceUTC,
double utcTime, const char* format,
MutableHandleValue rval) {
static bool ToLocaleFormatHelper(JSContext* cx, DateObject* unwrapped,
const char* format, MutableHandleValue rval) {
DateTimeInfo::ForceUTC forceUTC = unwrapped->forceUTC();
const char* locale = unwrapped->realm()->getLocale();
double utcTime = unwrapped->UTCTime().toNumber();
char buf[100];
if (!std::isfinite(utcTime)) {
strcpy(buf, "InvalidDate");
@ -3157,7 +3162,8 @@ static bool ToLocaleFormatHelper(JSContext* cx, DateTimeInfo::ForceUTC forceUTC,
/* If it failed, default to toString. */
if (result_len == 0) {
return FormatDate(cx, forceUTC, utcTime, FormatSpec::DateTime, rval);
return FormatDate(cx, forceUTC, locale, utcTime, FormatSpec::DateTime,
rval);
}
/* Hacked check against undesired 2-digit year 00/00/00 form. */
@ -3212,9 +3218,7 @@ static bool date_toLocaleString(JSContext* cx, unsigned argc, Value* vp) {
# endif
;
return ToLocaleFormatHelper(cx, unwrapped->forceUTC(),
unwrapped->UTCTime().toNumber(), format,
args.rval());
return ToLocaleFormatHelper(cx, unwrapped, format, args.rval());
}
static bool date_toLocaleDateString(JSContext* cx, unsigned argc, Value* vp) {
@ -3240,9 +3244,7 @@ static bool date_toLocaleDateString(JSContext* cx, unsigned argc, Value* vp) {
# endif
;
return ToLocaleFormatHelper(cx, unwrapped->forceUTC(),
unwrapped->UTCTime().toNumber(), format,
args.rval());
return ToLocaleFormatHelper(cx, unwrapped, format, args.rval());
}
static bool date_toLocaleTimeString(JSContext* cx, unsigned argc, Value* vp) {
@ -3256,9 +3258,7 @@ static bool date_toLocaleTimeString(JSContext* cx, unsigned argc, Value* vp) {
return false;
}
return ToLocaleFormatHelper(cx, unwrapped->forceUTC(),
unwrapped->UTCTime().toNumber(), "%X",
args.rval());
return ToLocaleFormatHelper(cx, unwrapped, "%X", args.rval());
}
#endif /* !JS_HAS_INTL_API */
@ -3272,8 +3272,9 @@ static bool date_toTimeString(JSContext* cx, unsigned argc, Value* vp) {
return false;
}
return FormatDate(cx, unwrapped->forceUTC(), unwrapped->UTCTime().toNumber(),
FormatSpec::Time, args.rval());
return FormatDate(cx, unwrapped->forceUTC(), unwrapped->realm()->getLocale(),
unwrapped->UTCTime().toNumber(), FormatSpec::Time,
args.rval());
}
static bool date_toDateString(JSContext* cx, unsigned argc, Value* vp) {
@ -3286,8 +3287,9 @@ static bool date_toDateString(JSContext* cx, unsigned argc, Value* vp) {
return false;
}
return FormatDate(cx, unwrapped->forceUTC(), unwrapped->UTCTime().toNumber(),
FormatSpec::Date, args.rval());
return FormatDate(cx, unwrapped->forceUTC(), unwrapped->realm()->getLocale(),
unwrapped->UTCTime().toNumber(), FormatSpec::Date,
args.rval());
}
static bool date_toSource(JSContext* cx, unsigned argc, Value* vp) {
@ -3323,8 +3325,9 @@ bool date_toString(JSContext* cx, unsigned argc, Value* vp) {
return false;
}
return FormatDate(cx, unwrapped->forceUTC(), unwrapped->UTCTime().toNumber(),
FormatSpec::DateTime, args.rval());
return FormatDate(cx, unwrapped->forceUTC(), unwrapped->realm()->getLocale(),
unwrapped->UTCTime().toNumber(), FormatSpec::DateTime,
args.rval());
}
bool js::date_valueOf(JSContext* cx, unsigned argc, Value* vp) {
@ -3480,8 +3483,8 @@ static bool NewDateObject(JSContext* cx, const CallArgs& args, ClippedTime t) {
}
static bool ToDateString(JSContext* cx, const CallArgs& args, ClippedTime t) {
return FormatDate(cx, ForceUTC(cx->realm()), t.toDouble(),
FormatSpec::DateTime, args.rval());
return FormatDate(cx, ForceUTC(cx->realm()), cx->realm()->getLocale(),
t.toDouble(), FormatSpec::DateTime, args.rval());
}
static bool DateNoArguments(JSContext* cx, const CallArgs& args) {

Просмотреть файл

@ -6776,6 +6776,9 @@ static bool WrapWithProto(JSContext* cx, unsigned argc, Value* vp) {
}
static bool NewGlobal(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
RootedObject callee(cx, &args.callee());
JS::RealmOptions options;
JS::RealmCreationOptions& creationOptions = options.creationOptions();
JS::RealmBehaviors& behaviors = options.behaviors();
@ -6794,7 +6797,6 @@ static bool NewGlobal(JSContext* cx, unsigned argc, Value* vp) {
JS::AutoHoldPrincipals principals(cx);
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() == 1 && args[0].isObject()) {
RootedObject opts(cx, &args[0].toObject());
RootedValue v(cx);
@ -6914,6 +6916,18 @@ static bool NewGlobal(JSContext* cx, unsigned argc, Value* vp) {
if (v.isBoolean()) {
creationOptions.setAlwaysUseFdlibm(v.toBoolean());
}
if (!JS_GetProperty(cx, opts, "locale", &v)) {
return false;
}
if (v.isString()) {
RootedString str(cx, v.toString());
UniqueChars locale = StringToLocale(cx, callee, str);
if (!locale) {
return false;
}
creationOptions.setLocaleCopyZ(locale.get());
}
}
if (!CheckRealmOptions(cx, options, principals.get())) {

Просмотреть файл

@ -488,6 +488,14 @@ void Realm::clearScriptCounts() { zone()->clearScriptCounts(this); }
void Realm::clearScriptLCov() { zone()->clearScriptLCov(this); }
const char* Realm::getLocale() const {
if (RefPtr<LocaleString> locale = creationOptions_.locale()) {
return locale->chars();
}
return runtime_->getDefaultLocale();
}
void ObjectRealm::addSizeOfExcludingThis(
mozilla::MallocSizeOf mallocSizeOf, size_t* innerViewsArg,
size_t* objectMetadataTablesArg,

Просмотреть файл

@ -673,6 +673,9 @@ class JS::Realm : public JS::shadow::Realm {
bool shouldCaptureStackForThrow();
// Returns the locale for this realm. (Pointer must NOT be freed!)
const char* getLocale() const;
// Initializes randomNumberGenerator if needed.
mozilla::non_crypto::XorShift128PlusRNG& getOrCreateRandomNumberGenerator();

Просмотреть файл

@ -1593,7 +1593,7 @@ static bool intrinsic_RuntimeDefaultLocale(JSContext* cx, unsigned argc,
CallArgs args = CallArgsFromVp(argc, vp);
MOZ_ASSERT(args.length() == 0);
const char* locale = cx->runtime()->getDefaultLocale();
const char* locale = cx->realm()->getLocale();
if (!locale) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEFAULT_LOCALE_ERROR);
@ -1622,7 +1622,7 @@ static bool intrinsic_IsRuntimeDefaultLocale(JSContext* cx, unsigned argc,
return true;
}
const char* locale = cx->runtime()->getDefaultLocale();
const char* locale = cx->realm()->getLocale();
if (!locale) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_DEFAULT_LOCALE_ERROR);