TestingUtility.cpp (11858B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- 2 * vim: set ts=8 sts=2 et sw=2 tw=80: 3 * This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "builtin/TestingUtility.h" 8 9 #ifdef JS_HAS_INTL_API 10 # include "mozilla/intl/TimeZone.h" 11 #endif 12 13 #include <stdint.h> // uint32_t 14 #include <string.h> 15 16 #include "jsapi.h" // JS_NewPlainObject, JS_WrapValue 17 18 #ifdef JS_HAS_INTL_API 19 # include "builtin/intl/CommonFunctions.h" 20 #endif 21 #include "frontend/CompilationStencil.h" // js::frontend::CompilationStencil 22 #include "js/CharacterEncoding.h" // JS_EncodeStringToUTF8 23 #include "js/ColumnNumber.h" // JS::ColumnNumberOneOrigin 24 #include "js/CompileOptions.h" // JS::CompileOptions 25 #include "js/Conversions.h" // JS::ToBoolean, JS::ToString, JS::ToUint32, JS::ToInt32 26 #include "js/PropertyAndElement.h" // JS_GetProperty, JS_DefineProperty 27 #include "js/PropertyDescriptor.h" // JSPROP_ENUMERATE 28 #include "js/RealmOptions.h" // JS::RealmBehaviors 29 #include "js/RootingAPI.h" // JS::Rooted, JS::Handle 30 #include "js/Utility.h" // JS::UniqueChars 31 #include "js/Value.h" // JS::Value, JS::StringValue 32 #include "vm/JSContext.h" // JS::ReportUsageErrorASCII 33 #include "vm/JSScript.h" 34 #include "vm/Realm.h" // JS::Realm 35 36 bool js::ParseCompileOptions(JSContext* cx, JS::CompileOptions& options, 37 JS::Handle<JSObject*> opts, 38 JS::UniqueChars* fileNameBytes) { 39 JS::Rooted<JS::Value> v(cx); 40 JS::Rooted<JSString*> s(cx); 41 42 if (!JS_GetProperty(cx, opts, "isRunOnce", &v)) { 43 return false; 44 } 45 if (!v.isUndefined()) { 46 options.setIsRunOnce(JS::ToBoolean(v)); 47 } 48 49 if (!JS_GetProperty(cx, opts, "noScriptRval", &v)) { 50 return false; 51 } 52 if (!v.isUndefined()) { 53 options.setNoScriptRval(JS::ToBoolean(v)); 54 } 55 56 if (!JS_GetProperty(cx, opts, "fileName", &v)) { 57 return false; 58 } 59 if (v.isNull()) { 60 options.setFile(nullptr); 61 } else if (!v.isUndefined()) { 62 s = JS::ToString(cx, v); 63 if (!s) { 64 return false; 65 } 66 if (fileNameBytes) { 67 *fileNameBytes = JS_EncodeStringToUTF8(cx, s); 68 if (!*fileNameBytes) { 69 return false; 70 } 71 options.setFile(fileNameBytes->get()); 72 } 73 } 74 75 if (!JS_GetProperty(cx, opts, "skipFileNameValidation", &v)) { 76 return false; 77 } 78 if (!v.isUndefined()) { 79 options.setSkipFilenameValidation(JS::ToBoolean(v)); 80 } 81 82 if (!JS_GetProperty(cx, opts, "lineNumber", &v)) { 83 return false; 84 } 85 if (!v.isUndefined()) { 86 uint32_t u; 87 if (!JS::ToUint32(cx, v, &u)) { 88 return false; 89 } 90 options.setLine(u); 91 } 92 93 if (!JS_GetProperty(cx, opts, "columnNumber", &v)) { 94 return false; 95 } 96 if (!v.isUndefined()) { 97 int32_t c; 98 if (!JS::ToInt32(cx, v, &c)) { 99 return false; 100 } 101 if (c < 1) { 102 c = 1; 103 } 104 options.setColumn(JS::ColumnNumberOneOrigin(c)); 105 } 106 107 if (!JS_GetProperty(cx, opts, "sourceIsLazy", &v)) { 108 return false; 109 } 110 if (v.isBoolean()) { 111 options.setSourceIsLazy(v.toBoolean()); 112 } 113 114 if (!JS_GetProperty(cx, opts, "forceFullParse", &v)) { 115 return false; 116 } 117 bool forceFullParseIsSet = !v.isUndefined(); 118 if (v.isBoolean() && v.toBoolean()) { 119 options.setForceFullParse(); 120 } 121 122 if (!JS_GetProperty(cx, opts, "eagerDelazificationStrategy", &v)) { 123 return false; 124 } 125 if (forceFullParseIsSet && !v.isUndefined()) { 126 JS_ReportErrorASCII( 127 cx, "forceFullParse and eagerDelazificationStrategy are both set."); 128 return false; 129 } 130 if (v.isString()) { 131 s = JS::ToString(cx, v); 132 if (!s) { 133 return false; 134 } 135 136 JSLinearString* str = JS_EnsureLinearString(cx, s); 137 if (!str) { 138 return false; 139 } 140 141 bool found = false; 142 JS::DelazificationOption strategy = JS::DelazificationOption::OnDemandOnly; 143 144 #define MATCH_AND_SET_STRATEGY_(NAME) \ 145 if (!found && JS_LinearStringEqualsLiteral(str, #NAME)) { \ 146 strategy = JS::DelazificationOption::NAME; \ 147 found = true; \ 148 } 149 150 FOREACH_DELAZIFICATION_STRATEGY(MATCH_AND_SET_STRATEGY_); 151 #undef MATCH_AND_SET_STRATEGY_ 152 #undef FOR_STRATEGY_NAMES 153 154 if (!found) { 155 JS_ReportErrorASCII(cx, 156 "eagerDelazificationStrategy does not match any " 157 "DelazificationOption."); 158 return false; 159 } 160 options.setEagerDelazificationStrategy(strategy); 161 } 162 163 return true; 164 } 165 166 bool js::ParseSourceOptions(JSContext* cx, JS::Handle<JSObject*> opts, 167 JS::MutableHandle<JSString*> displayURL, 168 JS::MutableHandle<JSString*> sourceMapURL) { 169 JS::Rooted<JS::Value> v(cx); 170 171 if (!JS_GetProperty(cx, opts, "displayURL", &v)) { 172 return false; 173 } 174 if (!v.isUndefined()) { 175 displayURL.set(ToString(cx, v)); 176 if (!displayURL) { 177 return false; 178 } 179 } 180 181 if (!JS_GetProperty(cx, opts, "sourceMapURL", &v)) { 182 return false; 183 } 184 if (!v.isUndefined()) { 185 sourceMapURL.set(ToString(cx, v)); 186 if (!sourceMapURL) { 187 return false; 188 } 189 } 190 191 return true; 192 } 193 194 bool js::SetSourceOptions(JSContext* cx, FrontendContext* fc, 195 ScriptSource* source, 196 JS::Handle<JSString*> displayURL, 197 JS::Handle<JSString*> sourceMapURL) { 198 if (displayURL && !source->hasDisplayURL()) { 199 JS::UniqueTwoByteChars chars = JS_CopyStringCharsZ(cx, displayURL); 200 if (!chars) { 201 return false; 202 } 203 if (!source->setDisplayURL(fc, std::move(chars))) { 204 return false; 205 } 206 } 207 if (sourceMapURL && !source->hasSourceMapURL()) { 208 JS::UniqueTwoByteChars chars = JS_CopyStringCharsZ(cx, sourceMapURL); 209 if (!chars) { 210 return false; 211 } 212 if (!source->setSourceMapURL(fc, std::move(chars))) { 213 return false; 214 } 215 } 216 217 return true; 218 } 219 220 JSObject* js::CreateScriptPrivate(JSContext* cx, 221 JS::Handle<JSString*> path /* = nullptr */) { 222 JS::Rooted<JSObject*> info(cx, JS_NewPlainObject(cx)); 223 if (!info) { 224 return nullptr; 225 } 226 227 if (path) { 228 JS::Rooted<JS::Value> pathValue(cx, JS::StringValue(path)); 229 if (!JS_DefineProperty(cx, info, "path", pathValue, JSPROP_ENUMERATE)) { 230 return nullptr; 231 } 232 } 233 234 return info; 235 } 236 237 bool js::ParseDebugMetadata(JSContext* cx, JS::Handle<JSObject*> opts, 238 JS::MutableHandle<JS::Value> privateValue, 239 JS::MutableHandle<JSString*> elementAttributeName) { 240 JS::Rooted<JS::Value> v(cx); 241 JS::Rooted<JSString*> s(cx); 242 243 if (!JS_GetProperty(cx, opts, "element", &v)) { 244 return false; 245 } 246 if (v.isObject()) { 247 JS::Rooted<JSObject*> infoObject(cx, CreateScriptPrivate(cx)); 248 if (!infoObject) { 249 return false; 250 } 251 JS::Rooted<JS::Value> elementValue(cx, v); 252 if (!JS_WrapValue(cx, &elementValue)) { 253 return false; 254 } 255 if (!JS_DefineProperty(cx, infoObject, "element", elementValue, 0)) { 256 return false; 257 } 258 privateValue.set(JS::ObjectValue(*infoObject)); 259 } 260 261 if (!JS_GetProperty(cx, opts, "elementAttributeName", &v)) { 262 return false; 263 } 264 if (!v.isUndefined()) { 265 s = ToString(cx, v); 266 if (!s) { 267 return false; 268 } 269 elementAttributeName.set(s); 270 } 271 272 return true; 273 } 274 275 static JS::UniqueChars StringToAscii(JSContext* cx, 276 JS::Handle<JSObject*> callee, 277 JS::Handle<JSString*> str) { 278 JS::Rooted<JSLinearString*> linear(cx, str->ensureLinear(cx)); 279 if (!linear) { 280 return nullptr; 281 } 282 283 if (!StringIsAscii(linear)) { 284 ReportUsageErrorASCII(cx, callee, 285 "First argument contains non-ASCII characters"); 286 return nullptr; 287 } 288 289 return JS_EncodeStringToASCII(cx, linear); 290 } 291 292 JS::UniqueChars js::StringToLocale(JSContext* cx, JS::Handle<JSObject*> callee, 293 JS::Handle<JSString*> str) { 294 UniqueChars locale = StringToAscii(cx, callee, str); 295 if (!locale) { 296 return nullptr; 297 } 298 299 bool containsOnlyValidBCP47Characters = 300 mozilla::IsAsciiAlpha(locale[0]) && 301 std::all_of(locale.get(), locale.get() + str->length(), [](auto c) { 302 return mozilla::IsAsciiAlphanumeric(c) || c == '-'; 303 }); 304 305 if (!containsOnlyValidBCP47Characters) { 306 ReportUsageErrorASCII(cx, callee, 307 "First argument should be a BCP47 language tag"); 308 return nullptr; 309 } 310 311 return locale; 312 } 313 314 /* 315 * Validate time zone input. Accepts the following formats: 316 * - "America/Chicago" (raw time zone) 317 * - ":America/Chicago" 318 * - "/this-part-is-ignored/zoneinfo/America/Chicago" 319 * - ":/this-part-is-ignored/zoneinfo/America/Chicago" 320 * - "/etc/localtime" 321 * - ":/etc/localtime" 322 * Once the raw time zone is parsed out of the string, it is checked 323 * against the time zones from GetAvailableTimeZones(). Throws an 324 * Error if the time zone is invalid. 325 */ 326 static bool ValidateTimeZone(JSContext* cx, const char* timeZone, 327 js::AllowTimeZoneLink allowLink) { 328 const char* timeZonePart; 329 if (allowLink == js::AllowTimeZoneLink::Yes) { 330 static constexpr char zoneInfo[] = "/zoneinfo/"; 331 static constexpr size_t zoneInfoLength = sizeof(zoneInfo) - 1; 332 333 size_t i = 0; 334 if (timeZone[i] == ':') { 335 ++i; 336 } 337 const char* zoneInfoPtr = strstr(timeZone, zoneInfo); 338 timeZonePart = timeZone[i] == '/' && zoneInfoPtr 339 ? zoneInfoPtr + zoneInfoLength 340 : timeZone + i; 341 if (!*timeZonePart) { 342 JS_ReportErrorASCII(cx, "Invalid time zone format"); 343 return false; 344 } 345 346 if (!strcmp(timeZonePart, "/etc/localtime")) { 347 return true; 348 } 349 } else { 350 timeZonePart = timeZone; 351 } 352 353 #if defined(JS_HAS_INTL_API) && !defined(__wasi__) 354 auto timeZones = mozilla::intl::TimeZone::GetAvailableTimeZones(); 355 if (timeZones.isErr()) { 356 js::intl::ReportInternalError(cx, timeZones.unwrapErr()); 357 return false; 358 } 359 for (auto timeZoneName : timeZones.unwrap()) { 360 if (timeZoneName.isErr()) { 361 js::intl::ReportInternalError(cx); 362 return false; 363 } 364 365 if (!strcmp(timeZonePart, timeZoneName.unwrap().data())) { 366 return true; 367 } 368 } 369 370 JS_ReportErrorASCII(cx, "Unsupported time zone name: %s", timeZonePart); 371 return false; 372 #else 373 return true; 374 #endif 375 } 376 377 JS::UniqueChars js::StringToTimeZone(JSContext* cx, 378 JS::Handle<JSObject*> callee, 379 JS::Handle<JSString*> str, 380 AllowTimeZoneLink allowLink) { 381 UniqueChars timeZone = StringToAscii(cx, callee, str); 382 if (!timeZone) { 383 return nullptr; 384 } 385 386 if (!ValidateTimeZone(cx, timeZone.get(), allowLink)) { 387 return nullptr; 388 } 389 390 return timeZone; 391 } 392 393 bool js::ValidateLazinessOfStencilAndGlobal(JSContext* cx, 394 const JS::Stencil* stencil) { 395 if (cx->realm()->behaviors().discardSource() && stencil->canLazilyParse()) { 396 JS_ReportErrorASCII(cx, 397 "Stencil compiled with with lazy parse option cannot " 398 "be used in a realm with discardSource"); 399 return false; 400 } 401 402 return true; 403 } 404 405 bool js::ValidateModuleCompileOptions(JSContext* cx, 406 JS::CompileOptions& options) { 407 if (options.lineno == 0) { 408 JS_ReportErrorASCII(cx, "Module cannot be compiled with lineNumber == 0"); 409 return false; 410 } 411 412 if (!options.filename()) { 413 JS_ReportErrorASCII(cx, "Module should have filename"); 414 return false; 415 } 416 417 return true; 418 }