From 2aa4a82499d4becd2284cdb482213d541b8804dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 16:29:10 +0200 Subject: Adding upstream version 86.0.1. Signed-off-by: Daniel Baumann --- js/src/shell/.eslintrc.js | 16 + js/src/shell/Makefile.in | 16 + js/src/shell/ModuleLoader.cpp | 574 ++ js/src/shell/ModuleLoader.h | 75 + js/src/shell/OSObject.cpp | 1186 +++ js/src/shell/OSObject.h | 43 + js/src/shell/StringUtils.h | 147 + js/src/shell/WasmTesting.cpp | 75 + js/src/shell/WasmTesting.h | 46 + js/src/shell/fuzz-flags.txt | 83 + js/src/shell/js-gdb.py | 21 + js/src/shell/js.cpp | 12030 ++++++++++++++++++++++ js/src/shell/jsoptparse.cpp | 616 ++ js/src/shell/jsoptparse.h | 303 + js/src/shell/jsrtfuzzing/jsrtfuzzing-example.js | 39 + js/src/shell/jsrtfuzzing/jsrtfuzzing.cpp | 143 + js/src/shell/jsrtfuzzing/jsrtfuzzing.h | 28 + js/src/shell/jsshell.cpp | 127 + js/src/shell/jsshell.h | 279 + js/src/shell/moz.build | 48 + 20 files changed, 15895 insertions(+) create mode 100644 js/src/shell/.eslintrc.js create mode 100644 js/src/shell/Makefile.in create mode 100644 js/src/shell/ModuleLoader.cpp create mode 100644 js/src/shell/ModuleLoader.h create mode 100644 js/src/shell/OSObject.cpp create mode 100644 js/src/shell/OSObject.h create mode 100644 js/src/shell/StringUtils.h create mode 100644 js/src/shell/WasmTesting.cpp create mode 100644 js/src/shell/WasmTesting.h create mode 100644 js/src/shell/fuzz-flags.txt create mode 100644 js/src/shell/js-gdb.py create mode 100644 js/src/shell/js.cpp create mode 100644 js/src/shell/jsoptparse.cpp create mode 100644 js/src/shell/jsoptparse.h create mode 100644 js/src/shell/jsrtfuzzing/jsrtfuzzing-example.js create mode 100644 js/src/shell/jsrtfuzzing/jsrtfuzzing.cpp create mode 100644 js/src/shell/jsrtfuzzing/jsrtfuzzing.h create mode 100644 js/src/shell/jsshell.cpp create mode 100644 js/src/shell/jsshell.h create mode 100644 js/src/shell/moz.build (limited to 'js/src/shell') diff --git a/js/src/shell/.eslintrc.js b/js/src/shell/.eslintrc.js new file mode 100644 index 0000000000..029c345790 --- /dev/null +++ b/js/src/shell/.eslintrc.js @@ -0,0 +1,16 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +module.exports = { + "plugins": [ + "spidermonkey-js" + ], + + "overrides": [{ + "files": ["*.js"], + "processor": "spidermonkey-js/processor", + }], +}; diff --git a/js/src/shell/Makefile.in b/js/src/shell/Makefile.in new file mode 100644 index 0000000000..ebc58cfc7f --- /dev/null +++ b/js/src/shell/Makefile.in @@ -0,0 +1,16 @@ +# -*- Mode: makefile -*- +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +ifdef QEMU_EXE +MOZ_POST_PROGRAM_COMMAND = $(topsrcdir)/build/qemu-wrap --qemu $(QEMU_EXE) --libdir $(CROSS_LIB) +endif + +include $(topsrcdir)/config/rules.mk + +# Install versioned binary for parallel installability in Linux distributions +install:: $(PROGRAM) + cp $^ $^$(MOZJS_MAJOR_VERSION) + $(SYSINSTALL) $^$(MOZJS_MAJOR_VERSION) $(DESTDIR)$(bindir) diff --git a/js/src/shell/ModuleLoader.cpp b/js/src/shell/ModuleLoader.cpp new file mode 100644 index 0000000000..8127787d53 --- /dev/null +++ b/js/src/shell/ModuleLoader.cpp @@ -0,0 +1,574 @@ +/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 + * -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "shell/ModuleLoader.h" + +#include "mozilla/DebugOnly.h" +#include "mozilla/TextUtils.h" + +#include "NamespaceImports.h" + +#include "js/Modules.h" +#include "js/SourceText.h" +#include "js/StableStringChars.h" +#include "shell/jsshell.h" +#include "shell/OSObject.h" +#include "shell/StringUtils.h" +#include "util/Text.h" +#include "vm/JSAtom.h" +#include "vm/JSContext.h" +#include "vm/StringType.h" + +using namespace js; +using namespace js::shell; + +static constexpr char16_t JavaScriptScheme[] = u"javascript:"; + +static bool IsJavaScriptURL(HandleLinearString path) { + return StringStartsWith(path, JavaScriptScheme); +} + +static JSString* ExtractJavaScriptURLSource(JSContext* cx, + HandleLinearString path) { + MOZ_ASSERT(IsJavaScriptURL(path)); + + const size_t schemeLength = js_strlen(JavaScriptScheme); + return SubString(cx, path, schemeLength); +} + +bool ModuleLoader::init(JSContext* cx, HandleString loadPath) { + loadPathStr = AtomizeString(cx, loadPath, PinAtom); + if (!loadPathStr) { + return false; + } + + MOZ_ASSERT(IsAbsolutePath(loadPathStr)); + + char16_t sep = PathSeparator; + pathSeparatorStr = AtomizeChars(cx, &sep, 1); + if (!pathSeparatorStr) { + return false; + } + + JSRuntime* rt = cx->runtime(); + JS::SetModuleResolveHook(rt, ModuleLoader::ResolveImportedModule); + JS::SetModuleMetadataHook(rt, ModuleLoader::GetImportMetaProperties); + JS::SetModuleDynamicImportHook(rt, ModuleLoader::ImportModuleDynamically); + + return true; +} + +// static +JSObject* ModuleLoader::ResolveImportedModule( + JSContext* cx, JS::HandleValue referencingPrivate, + JS::HandleString specifier) { + ShellContext* scx = GetShellContext(cx); + return scx->moduleLoader->resolveImportedModule(cx, referencingPrivate, + specifier); +} + +// static +bool ModuleLoader::GetImportMetaProperties(JSContext* cx, + JS::HandleValue privateValue, + JS::HandleObject metaObject) { + ShellContext* scx = GetShellContext(cx); + return scx->moduleLoader->populateImportMeta(cx, privateValue, metaObject); +} + +// static +bool ModuleLoader::ImportModuleDynamically(JSContext* cx, + JS::HandleValue referencingPrivate, + JS::HandleString specifier, + JS::HandleObject promise) { + ShellContext* scx = GetShellContext(cx); + return scx->moduleLoader->dynamicImport(cx, referencingPrivate, specifier, + promise); +} + +bool ModuleLoader::loadRootModule(JSContext* cx, HandleString path) { + RootedValue rval(cx); + if (!loadAndExecute(cx, path, &rval)) { + return false; + } + + if (cx->options().topLevelAwait()) { + RootedObject evaluationPromise(cx, &rval.toObject()); + if (evaluationPromise == nullptr) { + return false; + } + + return JS::ThrowOnModuleEvaluationFailure(cx, evaluationPromise); + } + return true; +} + +bool ModuleLoader::registerTestModule(JSContext* cx, HandleString specifier, + HandleModuleObject module) { + RootedLinearString path(cx, resolve(cx, specifier, UndefinedHandleValue)); + if (!path) { + return false; + } + + path = normalizePath(cx, path); + if (!path) { + return false; + } + + return addModuleToRegistry(cx, path, module); +} + +bool ModuleLoader::loadAndExecute(JSContext* cx, HandleString path, + MutableHandleValue rval) { + RootedObject module(cx, loadAndParse(cx, path)); + if (!module) { + return false; + } + + if (!JS::ModuleInstantiate(cx, module)) { + return false; + } + + return JS::ModuleEvaluate(cx, module, rval); +} + +JSObject* ModuleLoader::resolveImportedModule( + JSContext* cx, JS::HandleValue referencingPrivate, + JS::HandleString specifier) { + RootedLinearString path(cx, resolve(cx, specifier, referencingPrivate)); + if (!path) { + return nullptr; + } + + return loadAndParse(cx, path); +} + +bool ModuleLoader::populateImportMeta(JSContext* cx, + JS::HandleValue privateValue, + JS::HandleObject metaObject) { + RootedLinearString path(cx); + if (!privateValue.isUndefined()) { + if (!getScriptPath(cx, privateValue, &path)) { + return false; + } + } + + if (!path) { + path = NewStringCopyZ(cx, "(unknown)"); + if (!path) { + return false; + } + } + + RootedValue pathValue(cx, StringValue(path)); + return JS_DefineProperty(cx, metaObject, "url", pathValue, JSPROP_ENUMERATE); +} + +bool ModuleLoader::dynamicImport(JSContext* cx, + JS::HandleValue referencingPrivate, + JS::HandleString specifier, + JS::HandleObject promise) { + // To make this more realistic, use a promise to delay the import and make it + // happen asynchronously. This method packages up the arguments and creates a + // resolved promise, which on fullfillment calls doDynamicImport with the + // original arguments. + + MOZ_ASSERT(promise); + RootedValue specifierValue(cx, StringValue(specifier)); + RootedValue promiseValue(cx, ObjectValue(*promise)); + RootedObject closure(cx, JS_NewPlainObject(cx)); + if (!closure || + !JS_DefineProperty(cx, closure, "referencingPrivate", referencingPrivate, + JSPROP_ENUMERATE) || + !JS_DefineProperty(cx, closure, "specifier", specifierValue, + JSPROP_ENUMERATE) || + !JS_DefineProperty(cx, closure, "promise", promiseValue, + JSPROP_ENUMERATE)) { + return false; + } + + RootedFunction onResolved( + cx, NewNativeFunction(cx, DynamicImportDelayFulfilled, 1, nullptr)); + if (!onResolved) { + return false; + } + + RootedFunction onRejected( + cx, NewNativeFunction(cx, DynamicImportDelayRejected, 1, nullptr)); + if (!onRejected) { + return false; + } + + RootedObject delayPromise(cx); + RootedValue closureValue(cx, ObjectValue(*closure)); + delayPromise = PromiseObject::unforgeableResolve(cx, closureValue); + if (!delayPromise) { + return false; + } + + return JS::AddPromiseReactions(cx, delayPromise, onResolved, onRejected); +} + +bool ModuleLoader::DynamicImportDelayFulfilled(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject closure(cx, &args[0].toObject()); + + RootedValue referencingPrivate(cx); + RootedValue specifierValue(cx); + RootedValue promiseValue(cx); + if (!JS_GetProperty(cx, closure, "referencingPrivate", &referencingPrivate) || + !JS_GetProperty(cx, closure, "specifier", &specifierValue) || + !JS_GetProperty(cx, closure, "promise", &promiseValue)) { + return false; + } + + RootedString specifier(cx, specifierValue.toString()); + RootedObject promise(cx, &promiseValue.toObject()); + + ShellContext* scx = GetShellContext(cx); + return scx->moduleLoader->doDynamicImport(cx, referencingPrivate, specifier, + promise); +} + +bool ModuleLoader::DynamicImportDelayRejected(JSContext* cx, unsigned argc, + Value* vp) { + MOZ_CRASH("This promise should never be rejected"); +} + +bool ModuleLoader::doDynamicImport(JSContext* cx, + JS::HandleValue referencingPrivate, + JS::HandleString specifier, + JS::HandleObject promise) { + // Exceptions during dynamic import are handled by calling + // FinishDynamicModuleImport with a pending exception on the context. + RootedValue rval(cx); + bool ok = tryDynamicImport(cx, referencingPrivate, specifier, promise, &rval); + if (cx->options().topLevelAwait()) { + JSObject* evaluationObject = ok ? &rval.toObject() : nullptr; + RootedObject evaluationPromise(cx, evaluationObject); + return JS::FinishDynamicModuleImport( + cx, evaluationPromise, referencingPrivate, specifier, promise); + } + JS::DynamicImportStatus status = + ok ? JS::DynamicImportStatus::Ok : JS::DynamicImportStatus::Failed; + return JS::FinishDynamicModuleImport_NoTLA(cx, status, referencingPrivate, + specifier, promise); +} + +bool ModuleLoader::tryDynamicImport(JSContext* cx, + JS::HandleValue referencingPrivate, + JS::HandleString specifier, + JS::HandleObject promise, + JS::MutableHandleValue rval) { + RootedLinearString path(cx, resolve(cx, specifier, referencingPrivate)); + if (!path) { + return false; + } + + return loadAndExecute(cx, path, rval); +} + +JSLinearString* ModuleLoader::resolve(JSContext* cx, HandleString nameArg, + HandleValue referencingInfo) { + if (nameArg->length() == 0) { + JS_ReportErrorASCII(cx, "Invalid module specifier"); + return nullptr; + } + + RootedLinearString name(cx, JS_EnsureLinearString(cx, nameArg)); + if (!name) { + return nullptr; + } + + if (IsJavaScriptURL(name) || IsAbsolutePath(name)) { + return name; + } + + // Treat |name| as a relative path if it starts with either "./" or "../". + bool isRelative = + StringStartsWith(name, u"./") || StringStartsWith(name, u"../") +#ifdef XP_WIN + || StringStartsWith(name, u".\\") || StringStartsWith(name, u"..\\") +#endif + ; + + RootedString path(cx, loadPathStr); + + if (isRelative) { + if (referencingInfo.isUndefined()) { + JS_ReportErrorASCII(cx, "No referencing module for relative import"); + return nullptr; + } + + RootedLinearString refPath(cx); + if (!getScriptPath(cx, referencingInfo, &refPath)) { + return nullptr; + } + + if (!refPath) { + JS_ReportErrorASCII(cx, "No path set for referencing module"); + return nullptr; + } + + int32_t sepIndex = LastIndexOf(refPath, u'/'); +#ifdef XP_WIN + sepIndex = std::max(sepIndex, LastIndexOf(refPath, u'\\')); +#endif + if (sepIndex >= 0) { + path = SubString(cx, refPath, 0, sepIndex); + if (!path) { + return nullptr; + } + } + } + + RootedString result(cx); + RootedString pathSep(cx, pathSeparatorStr); + result = JS_ConcatStrings(cx, path, pathSep); + if (!result) { + return nullptr; + } + + result = JS_ConcatStrings(cx, result, name); + if (!result) { + return nullptr; + } + + return JS_EnsureLinearString(cx, result); +} + +JSObject* ModuleLoader::loadAndParse(JSContext* cx, HandleString pathArg) { + RootedLinearString path(cx, JS_EnsureLinearString(cx, pathArg)); + if (!path) { + return nullptr; + } + + path = normalizePath(cx, path); + if (!path) { + return nullptr; + } + + RootedObject module(cx); + if (!lookupModuleInRegistry(cx, path, &module)) { + return nullptr; + } + + if (module) { + return module; + } + + UniqueChars filename = JS_EncodeStringToLatin1(cx, path); + if (!filename) { + return nullptr; + } + + JS::CompileOptions options(cx); + options.setFileAndLine(filename.get(), 1); + + RootedString source(cx, fetchSource(cx, path)); + if (!source) { + return nullptr; + } + + JS::AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, source)) { + return nullptr; + } + + const char16_t* chars = stableChars.twoByteRange().begin().get(); + JS::SourceText srcBuf; + if (!srcBuf.init(cx, chars, source->length(), + JS::SourceOwnership::Borrowed)) { + return nullptr; + } + + module = JS::CompileModule(cx, options, srcBuf); + if (!module) { + return nullptr; + } + + RootedObject info(cx, CreateScriptPrivate(cx, path)); + if (!info) { + return nullptr; + } + + JS::SetModulePrivate(module, ObjectValue(*info)); + + if (!addModuleToRegistry(cx, path, module)) { + return nullptr; + } + + return module; +} + +bool ModuleLoader::lookupModuleInRegistry(JSContext* cx, HandleString path, + MutableHandleObject moduleOut) { + moduleOut.set(nullptr); + + RootedObject registry(cx, getOrCreateModuleRegistry(cx)); + if (!registry) { + return false; + } + + RootedValue pathValue(cx, StringValue(path)); + RootedValue moduleValue(cx); + if (!JS::MapGet(cx, registry, pathValue, &moduleValue)) { + return false; + } + + if (!moduleValue.isUndefined()) { + moduleOut.set(&moduleValue.toObject()); + } + + return true; +} + +bool ModuleLoader::addModuleToRegistry(JSContext* cx, HandleString path, + HandleObject module) { + RootedObject registry(cx, getOrCreateModuleRegistry(cx)); + if (!registry) { + return false; + } + + RootedValue pathValue(cx, StringValue(path)); + RootedValue moduleValue(cx, ObjectValue(*module)); + return JS::MapSet(cx, registry, pathValue, moduleValue); +} + +JSObject* ModuleLoader::getOrCreateModuleRegistry(JSContext* cx) { + Handle global = cx->global(); + RootedValue value(cx, global->getReservedSlot(GlobalAppSlotModuleRegistry)); + if (!value.isUndefined()) { + return &value.toObject(); + } + + JSObject* registry = JS::NewMapObject(cx); + if (!registry) { + return nullptr; + } + + global->setReservedSlot(GlobalAppSlotModuleRegistry, ObjectValue(*registry)); + return registry; +} + +bool ModuleLoader::getScriptPath(JSContext* cx, HandleValue privateValue, + MutableHandle pathOut) { + pathOut.set(nullptr); + + RootedObject infoObj(cx, &privateValue.toObject()); + RootedValue pathValue(cx); + if (!JS_GetProperty(cx, infoObj, "path", &pathValue)) { + return false; + } + + if (pathValue.isUndefined()) { + return true; + } + + RootedString path(cx, pathValue.toString()); + pathOut.set(JS_EnsureLinearString(cx, path)); + return pathOut; +} + +JSLinearString* ModuleLoader::normalizePath(JSContext* cx, + HandleLinearString pathArg) { + RootedLinearString path(cx, pathArg); + + if (IsJavaScriptURL(path)) { + return path; + } + +#ifdef XP_WIN + // Replace all forward slashes with backward slashes. + path = ReplaceCharGlobally(cx, path, u'/', PathSeparator); + if (!path) { + return nullptr; + } + + // Remove the drive letter, if present. + RootedLinearString drive(cx); + if (path->length() > 2 && mozilla::IsAsciiAlpha(CharAt(path, 0)) && + CharAt(path, 1) == u':' && CharAt(path, 2) == u'\\') { + drive = SubString(cx, path, 0, 2); + path = SubString(cx, path, 2); + if (!drive || !path) { + return nullptr; + } + } +#endif // XP_WIN + + // Normalize the path by removing redundant path components. + Rooted> components(cx); + size_t lastSep = 0; + while (lastSep < path->length()) { + int32_t i = IndexOf(path, PathSeparator, lastSep); + if (i < 0) { + i = path->length(); + } + + RootedLinearString part(cx, SubString(cx, path, lastSep, i)); + if (!part) { + return nullptr; + } + + lastSep = i + 1; + + // Remove "." when preceded by a path component. + if (StringEquals(part, u".") && !components.empty()) { + continue; + } + + if (StringEquals(part, u"..") && !components.empty()) { + // Replace "./.." with "..". + if (StringEquals(components.back(), u".")) { + components.back() = part; + continue; + } + + // When preceded by a non-empty path component, remove ".." and the + // preceding component, unless the preceding component is also "..". + if (!StringEquals(components.back(), u"") && + !StringEquals(components.back(), u"..")) { + components.popBack(); + continue; + } + } + + if (!components.append(part)) { + return nullptr; + } + } + + RootedLinearString pathSep(cx, pathSeparatorStr); + RootedString normalized(cx, JoinStrings(cx, components, pathSep)); + if (!normalized) { + return nullptr; + } + +#ifdef XP_WIN + if (drive) { + normalized = JS_ConcatStrings(cx, drive, normalized); + if (!normalized) { + return nullptr; + } + } +#endif + + return JS_EnsureLinearString(cx, normalized); +} + +JSString* ModuleLoader::fetchSource(JSContext* cx, HandleLinearString path) { + if (IsJavaScriptURL(path)) { + return ExtractJavaScriptURLSource(cx, path); + } + + RootedString resolvedPath(cx, ResolvePath(cx, path, RootRelative)); + if (!resolvedPath) { + return nullptr; + } + + return FileAsString(cx, resolvedPath); +} diff --git a/js/src/shell/ModuleLoader.h b/js/src/shell/ModuleLoader.h new file mode 100644 index 0000000000..c0709c3001 --- /dev/null +++ b/js/src/shell/ModuleLoader.h @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef shell_ModuleLoader_h +#define shell_ModuleLoader_h + +#include "builtin/ModuleObject.h" +#include "gc/Rooting.h" +#include "js/RootingAPI.h" + +namespace js { +namespace shell { + +class ModuleLoader { + public: + bool init(JSContext* cx, HandleString loadPath); + bool loadRootModule(JSContext* cx, HandleString path); + + // Testing hook to register a module that wasn't loaded by the module loader. + bool registerTestModule(JSContext* cx, HandleString specifier, + HandleModuleObject module); + + private: + static JSObject* ResolveImportedModule(JSContext* cx, + HandleValue referencingPrivate, + HandleString specifier); + static bool GetImportMetaProperties(JSContext* cx, HandleValue privateValue, + HandleObject metaObject); + static bool ImportModuleDynamically(JSContext* cx, + HandleValue referencingPrivate, + HandleString specifier, + HandleObject promise); + + static bool DynamicImportDelayFulfilled(JSContext* cx, unsigned argc, + Value* vp); + static bool DynamicImportDelayRejected(JSContext* cx, unsigned argc, + Value* vp); + + bool loadAndExecute(JSContext* cx, HandleString path, MutableHandleValue); + JSObject* resolveImportedModule(JSContext* cx, HandleValue referencingPrivate, + HandleString specifier); + bool populateImportMeta(JSContext* cx, HandleValue privateValue, + HandleObject metaObject); + bool dynamicImport(JSContext* cx, HandleValue referencingPrivate, + HandleString specifier, HandleObject promise); + bool doDynamicImport(JSContext* cx, HandleValue referencingPrivate, + HandleString specifier, HandleObject promise); + bool tryDynamicImport(JSContext* cx, HandleValue referencingPrivate, + HandleString specifier, HandleObject promise, + MutableHandleValue rval); + JSObject* loadAndParse(JSContext* cx, HandleString path); + bool lookupModuleInRegistry(JSContext* cx, HandleString path, + MutableHandleObject moduleOut); + bool addModuleToRegistry(JSContext* cx, HandleString path, + HandleObject module); + JSLinearString* resolve(JSContext* cx, HandleString name, + HandleValue referencingInfo); + bool getScriptPath(JSContext* cx, HandleValue privateValue, + MutableHandle pathOut); + JSLinearString* normalizePath(JSContext* cx, HandleLinearString path); + JSObject* getOrCreateModuleRegistry(JSContext* cx); + JSString* fetchSource(JSContext* cx, HandleLinearString path); + + // The following are used for pinned atoms which do not need rooting. + JSAtom* loadPathStr = nullptr; + JSAtom* pathSeparatorStr = nullptr; +} JS_HAZ_NON_GC_POINTER; + +} // namespace shell +} // namespace js + +#endif // shell_ModuleLoader_h diff --git a/js/src/shell/OSObject.cpp b/js/src/shell/OSObject.cpp new file mode 100644 index 0000000000..f64c4d2560 --- /dev/null +++ b/js/src/shell/OSObject.cpp @@ -0,0 +1,1186 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// OSObject.h - os object for exposing posix system calls in the JS shell + +#include "shell/OSObject.h" + +#include "mozilla/ScopeExit.h" +#include "mozilla/TextUtils.h" + +#include +#include +#ifdef XP_WIN +# include +# include +# include +# include +#else +# include +# include +# include +# include +#endif + +#include "jsapi.h" +// For JSFunctionSpecWithHelp +#include "jsfriendapi.h" + +#include "gc/FreeOp.h" +#include "js/CharacterEncoding.h" +#include "js/Conversions.h" +#include "js/experimental/TypedData.h" // JS_NewUint8Array +#include "js/Object.h" // JS::GetReservedSlot +#include "js/PropertySpec.h" +#include "js/Value.h" // JS::Value +#include "js/Wrapper.h" +#include "shell/jsshell.h" +#include "shell/StringUtils.h" +#include "util/StringBuffer.h" +#include "util/Text.h" +#include "util/Windows.h" +#include "vm/JSObject.h" +#include "vm/TypedArrayObject.h" + +#include "vm/JSObject-inl.h" + +#ifdef XP_WIN +# ifndef PATH_MAX +# define PATH_MAX (MAX_PATH > _MAX_DIR ? MAX_PATH : _MAX_DIR) +# endif +# define getcwd _getcwd +#else +# include +#endif + +using js::shell::RCFile; + +namespace js { +namespace shell { + +bool IsAbsolutePath(JSLinearString* filename) { + size_t length = filename->length(); + +#ifdef XP_WIN + // On Windows there are various forms of absolute paths (see + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx + // for details): + // + // "\..." + // "\\..." + // "C:\..." + // + // The first two cases are handled by the common test below so we only need a + // specific test for the last one here. + + if (length > 3 && mozilla::IsAsciiAlpha(CharAt(filename, 0)) && + CharAt(filename, 1) == u':' && CharAt(filename, 2) == u'\\') { + return true; + } +#endif + + return length > 0 && CharAt(filename, 0) == PathSeparator; +} + +/* + * Resolve a (possibly) relative filename to an absolute path. If + * |scriptRelative| is true, then the result will be relative to the directory + * containing the currently-running script, or the current working directory if + * the currently-running script is "-e" (namely, you're using it from the + * command line.) Otherwise, it will be relative to the current working + * directory. + */ +JSString* ResolvePath(JSContext* cx, HandleString filenameStr, + PathResolutionMode resolveMode) { + if (!filenameStr) { +#ifdef XP_WIN + return JS_NewStringCopyZ(cx, "nul"); +#else + return JS_NewStringCopyZ(cx, "/dev/null"); +#endif + } + + RootedLinearString str(cx, JS_EnsureLinearString(cx, filenameStr)); + if (!str) { + return nullptr; + } + + if (IsAbsolutePath(str)) { + return str; + } + + UniqueChars filename = JS_EncodeStringToLatin1(cx, str); + if (!filename) { + return nullptr; + } + + JS::AutoFilename scriptFilename; + if (resolveMode == ScriptRelative) { + // Get the currently executing script's name. + if (!DescribeScriptedCaller(cx, &scriptFilename)) { + return nullptr; + } + + if (!scriptFilename.get()) { + return nullptr; + } + + if (strcmp(scriptFilename.get(), "-e") == 0 || + strcmp(scriptFilename.get(), "typein") == 0) { + resolveMode = RootRelative; + } + } + + char buffer[PATH_MAX + 1]; + if (resolveMode == ScriptRelative) { +#ifdef XP_WIN + // The docs say it can return EINVAL, but the compiler says it's void + _splitpath(scriptFilename.get(), nullptr, buffer, nullptr, nullptr); +#else + strncpy(buffer, scriptFilename.get(), PATH_MAX); + if (buffer[PATH_MAX - 1] != '\0') { + return nullptr; + } + + // dirname(buffer) might return buffer, or it might return a + // statically-allocated string + memmove(buffer, dirname(buffer), strlen(buffer) + 1); +#endif + } else { + const char* cwd = getcwd(buffer, PATH_MAX); + if (!cwd) { + return nullptr; + } + } + + size_t len = strlen(buffer); + buffer[len] = '/'; + strncpy(buffer + len + 1, filename.get(), sizeof(buffer) - (len + 1)); + if (buffer[PATH_MAX] != '\0') { + return nullptr; + } + + return JS_NewStringCopyZ(cx, buffer); +} + +JSObject* FileAsTypedArray(JSContext* cx, JS::HandleString pathnameStr) { + UniqueChars pathname = JS_EncodeStringToLatin1(cx, pathnameStr); + if (!pathname) { + return nullptr; + } + + FILE* file = fopen(pathname.get(), "rb"); + if (!file) { + /* + * Use Latin1 variant here because the encoding of the return value of + * strerror function can be non-UTF-8. + */ + JS_ReportErrorLatin1(cx, "can't open %s: %s", pathname.get(), + strerror(errno)); + return nullptr; + } + AutoCloseFile autoClose(file); + + RootedObject obj(cx); + if (fseek(file, 0, SEEK_END) != 0) { + pathname = JS_EncodeStringToUTF8(cx, pathnameStr); + if (!pathname) { + return nullptr; + } + JS_ReportErrorUTF8(cx, "can't seek end of %s", pathname.get()); + } else { + size_t len = ftell(file); + if (fseek(file, 0, SEEK_SET) != 0) { + pathname = JS_EncodeStringToUTF8(cx, pathnameStr); + if (!pathname) { + return nullptr; + } + JS_ReportErrorUTF8(cx, "can't seek start of %s", pathname.get()); + } else { + if (len > ArrayBufferObject::maxBufferByteLength()) { + JS_ReportErrorUTF8(cx, "file %s is too large for a Uint8Array", + pathname.get()); + return nullptr; + } + obj = JS_NewUint8Array(cx, len); + if (!obj) { + return nullptr; + } + js::TypedArrayObject& ta = obj->as(); + if (ta.isSharedMemory()) { + // Must opt in to use shared memory. For now, don't. + // + // (It is incorrect to read into the buffer without + // synchronization since that can create a race. A + // lock here won't fix it - both sides must + // participate. So what one must do is to create a + // temporary buffer, read into that, and use a + // race-safe primitive to copy memory into the + // buffer.) + pathname = JS_EncodeStringToUTF8(cx, pathnameStr); + if (!pathname) { + return nullptr; + } + JS_ReportErrorUTF8(cx, "can't read %s: shared memory buffer", + pathname.get()); + return nullptr; + } + char* buf = static_cast(ta.dataPointerUnshared()); + size_t cc = fread(buf, 1, len, file); + if (cc != len) { + if (ptrdiff_t(cc) < 0) { + /* + * Use Latin1 variant here because the encoding of the return + * value of strerror function can be non-UTF-8. + */ + JS_ReportErrorLatin1(cx, "can't read %s: %s", pathname.get(), + strerror(errno)); + } else { + pathname = JS_EncodeStringToUTF8(cx, pathnameStr); + if (!pathname) { + return nullptr; + } + JS_ReportErrorUTF8(cx, "can't read %s: short read", pathname.get()); + } + obj = nullptr; + } + } + } + return obj; +} + +/** + * Return the current working directory or |null| on failure. + */ +UniqueChars GetCWD() { + char buffer[PATH_MAX + 1]; + const char* cwd = getcwd(buffer, PATH_MAX); + if (!cwd) { + return UniqueChars(); + } + return js::DuplicateString(buffer); +} + +static bool ReadFile(JSContext* cx, unsigned argc, Value* vp, + bool scriptRelative) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() < 1 || args.length() > 2) { + JS_ReportErrorNumberASCII( + cx, js::shell::my_GetErrorMessage, nullptr, + args.length() < 1 ? JSSMSG_NOT_ENOUGH_ARGS : JSSMSG_TOO_MANY_ARGS, + "snarf"); + return false; + } + + if (!args[0].isString() || (args.length() == 2 && !args[1].isString())) { + JS_ReportErrorNumberASCII(cx, js::shell::my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "snarf"); + return false; + } + + RootedString givenPath(cx, args[0].toString()); + RootedString str( + cx, js::shell::ResolvePath( + cx, givenPath, scriptRelative ? ScriptRelative : RootRelative)); + if (!str) { + return false; + } + + if (args.length() > 1) { + JSString* opt = JS::ToString(cx, args[1]); + if (!opt) { + return false; + } + bool match; + if (!JS_StringEqualsLiteral(cx, opt, "binary", &match)) { + return false; + } + if (match) { + JSObject* obj; + if (!(obj = FileAsTypedArray(cx, str))) { + return false; + } + args.rval().setObject(*obj); + return true; + } + } + + if (!(str = FileAsString(cx, str))) { + return false; + } + args.rval().setString(str); + return true; +} + +static bool osfile_readFile(JSContext* cx, unsigned argc, Value* vp) { + return ReadFile(cx, argc, vp, false); +} + +static bool osfile_readRelativeToScript(JSContext* cx, unsigned argc, + Value* vp) { + return ReadFile(cx, argc, vp, true); +} + +static bool ListDir(JSContext* cx, unsigned argc, Value* vp, + PathResolutionMode resolveMode) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "os.file.listDir requires 1 argument"); + return false; + } + + if (!args[0].isString()) { + JS_ReportErrorNumberASCII(cx, js::shell::my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "os.file.listDir"); + return false; + } + + RootedString givenPath(cx, args[0].toString()); + RootedString str(cx, ResolvePath(cx, givenPath, resolveMode)); + if (!str) { + return false; + } + + UniqueChars pathname = JS_EncodeStringToLatin1(cx, str); + if (!pathname) { + JS_ReportErrorASCII(cx, "os.file.listDir cannot convert path to Latin1"); + return false; + } + + RootedValueVector elems(cx); + auto append = [&](const char* name) -> bool { + if (!(str = JS_NewStringCopyZ(cx, name))) { + return false; + } + if (!elems.append(StringValue(str))) { + js::ReportOutOfMemory(cx); + return false; + } + return true; + }; + +#if defined(XP_UNIX) + { + DIR* dir = opendir(pathname.get()); + if (!dir) { + JS_ReportErrorASCII(cx, "os.file.listDir is unable to open: %s", + pathname.get()); + return false; + } + auto close = mozilla::MakeScopeExit([&] { + if (closedir(dir) != 0) { + MOZ_CRASH("Could not close dir"); + } + }); + + while (struct dirent* entry = readdir(dir)) { + if (!append(entry->d_name)) { + return false; + } + } + } +#elif defined(XP_WIN) + { + const size_t pathlen = strlen(pathname.get()); + Vector pattern(cx); + if (!pattern.append(pathname.get(), pathlen) || + !pattern.append(PathSeparator) || !pattern.append("*", 2)) { + js::ReportOutOfMemory(cx); + return false; + } + + WIN32_FIND_DATA FindFileData; + HANDLE hFind = FindFirstFile(pattern.begin(), &FindFileData); + auto close = mozilla::MakeScopeExit([&] { + if (!FindClose(hFind)) { + MOZ_CRASH("Could not close Find"); + } + }); + for (bool found = (hFind != INVALID_HANDLE_VALUE); found; + found = FindNextFile(hFind, &FindFileData)) { + if (!append(FindFileData.cFileName)) { + return false; + } + } + } +#endif + + JSObject* array = JS::NewArrayObject(cx, elems); + if (!array) { + return false; + } + + args.rval().setObject(*array); + return true; +} + +static bool osfile_listDir(JSContext* cx, unsigned argc, Value* vp) { + return ListDir(cx, argc, vp, RootRelative); +} + +static bool osfile_listDirRelativeToScript(JSContext* cx, unsigned argc, + Value* vp) { + return ListDir(cx, argc, vp, ScriptRelative); +} + +static bool osfile_writeTypedArrayToFile(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 2 || !args[0].isString() || !args[1].isObject() || + !args[1].toObject().is()) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "writeTypedArrayToFile"); + return false; + } + + RootedString givenPath(cx, args[0].toString()); + RootedString str(cx, ResolvePath(cx, givenPath, RootRelative)); + if (!str) { + return false; + } + + UniqueChars filename = JS_EncodeStringToLatin1(cx, str); + if (!filename) { + return false; + } + + FILE* file = fopen(filename.get(), "wb"); + if (!file) { + /* + * Use Latin1 variant here because the encoding of the return value of + * strerror function can be non-UTF-8. + */ + JS_ReportErrorLatin1(cx, "can't open %s: %s", filename.get(), + strerror(errno)); + return false; + } + AutoCloseFile autoClose(file); + + TypedArrayObject* obj = &args[1].toObject().as(); + + if (obj->isSharedMemory()) { + // Must opt in to use shared memory. For now, don't. + // + // See further comments in FileAsTypedArray, above. + filename = JS_EncodeStringToUTF8(cx, str); + if (!filename) { + return false; + } + JS_ReportErrorUTF8(cx, "can't write %s: shared memory buffer", + filename.get()); + return false; + } + void* buf = obj->dataPointerUnshared(); + size_t length = obj->length().get(); + if (fwrite(buf, obj->bytesPerElement(), length, file) != length || + !autoClose.release()) { + filename = JS_EncodeStringToUTF8(cx, str); + if (!filename) { + return false; + } + JS_ReportErrorUTF8(cx, "can't write %s", filename.get()); + return false; + } + + args.rval().setUndefined(); + return true; +} + +/* static */ +RCFile* RCFile::create(JSContext* cx, const char* filename, const char* mode) { + FILE* fp = fopen(filename, mode); + if (!fp) { + return nullptr; + } + + RCFile* file = cx->new_(fp); + if (!file) { + fclose(fp); + return nullptr; + } + + return file; +} + +void RCFile::close() { + if (fp) { + fclose(fp); + } + fp = nullptr; +} + +bool RCFile::release() { + if (--numRefs) { + return false; + } + this->close(); + return true; +} + +class FileObject : public NativeObject { + enum : uint32_t { FILE_SLOT = 0, NUM_SLOTS }; + + public: + static const JSClass class_; + + static FileObject* create(JSContext* cx, RCFile* file) { + FileObject* obj = js::NewBuiltinClassInstance(cx); + if (!obj) { + return nullptr; + } + + InitReservedSlot(obj, FILE_SLOT, file, MemoryUse::FileObjectFile); + file->acquire(); + return obj; + } + + static void finalize(JSFreeOp* fop, JSObject* obj) { + FileObject* fileObj = &obj->as(); + RCFile* file = fileObj->rcFile(); + fop->removeCellMemory(obj, sizeof(*file), MemoryUse::FileObjectFile); + if (file->release()) { + fop->deleteUntracked(file); + } + } + + bool isOpen() { + RCFile* file = rcFile(); + return file && file->isOpen(); + } + + void close() { + if (!isOpen()) { + return; + } + rcFile()->close(); + } + + RCFile* rcFile() { + return reinterpret_cast( + JS::GetReservedSlot(this, FILE_SLOT).toPrivate()); + } +}; + +static const JSClassOps FileObjectClassOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + FileObject::finalize, // finalize + nullptr, // call + nullptr, // hasInstance + nullptr, // construct + nullptr, // trace +}; + +const JSClass FileObject::class_ = { + "File", + JSCLASS_HAS_RESERVED_SLOTS(FileObject::NUM_SLOTS) | + JSCLASS_FOREGROUND_FINALIZE, + &FileObjectClassOps}; + +static FileObject* redirect(JSContext* cx, HandleString relFilename, + RCFile** globalFile) { + RootedString filename(cx, ResolvePath(cx, relFilename, RootRelative)); + if (!filename) { + return nullptr; + } + UniqueChars filenameABS = JS_EncodeStringToLatin1(cx, filename); + if (!filenameABS) { + return nullptr; + } + RCFile* file = RCFile::create(cx, filenameABS.get(), "wb"); + if (!file) { + /* + * Use Latin1 variant here because the encoding of the return value of + * strerror function can be non-UTF-8. + */ + JS_ReportErrorLatin1(cx, "cannot redirect to %s: %s", filenameABS.get(), + strerror(errno)); + return nullptr; + } + + // Grant the global gOutFile ownership of the new file, release ownership + // of its old file, and return a FileObject owning the old file. + file->acquire(); // Global owner of new file + + FileObject* fileObj = + FileObject::create(cx, *globalFile); // Newly created owner of old file + if (!fileObj) { + file->release(); + return nullptr; + } + + (*globalFile)->release(); // Release (global) ownership of old file. + *globalFile = file; + + return fileObj; +} + +static bool Redirect(JSContext* cx, const CallArgs& args, RCFile** outFile) { + if (args.length() > 1) { + JS_ReportErrorNumberASCII(cx, js::shell::my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "redirect"); + return false; + } + + RCFile* oldFile = *outFile; + RootedObject oldFileObj(cx, FileObject::create(cx, oldFile)); + if (!oldFileObj) { + return false; + } + + if (args.get(0).isUndefined()) { + args.rval().setObject(*oldFileObj); + return true; + } + + if (args[0].isObject()) { + Rooted fileObj(cx, + args[0].toObject().maybeUnwrapIf()); + if (!fileObj) { + JS_ReportErrorNumberASCII(cx, js::shell::my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "redirect"); + return false; + } + + // Passed in a FileObject. Create a FileObject for the previous + // global file, and set the global file to the passed-in one. + *outFile = fileObj->rcFile(); + (*outFile)->acquire(); + oldFile->release(); + + args.rval().setObject(*oldFileObj); + return true; + } + + RootedString filename(cx); + if (!args[0].isNull()) { + filename = JS::ToString(cx, args[0]); + if (!filename) { + return false; + } + } + + if (!redirect(cx, filename, outFile)) { + return false; + } + + args.rval().setObject(*oldFileObj); + return true; +} + +static bool osfile_redirectOutput(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + ShellContext* scx = GetShellContext(cx); + return Redirect(cx, args, scx->outFilePtr); +} + +static bool osfile_redirectError(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + ShellContext* scx = GetShellContext(cx); + return Redirect(cx, args, scx->errFilePtr); +} + +static bool osfile_close(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted fileObj(cx); + if (args.get(0).isObject()) { + fileObj = args[0].toObject().maybeUnwrapIf(); + } + + if (!fileObj) { + JS_ReportErrorNumberASCII(cx, js::shell::my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "close"); + return false; + } + + fileObj->close(); + + args.rval().setUndefined(); + return true; +} + +// clang-format off +static const JSFunctionSpecWithHelp osfile_functions[] = { + JS_FN_HELP("readFile", osfile_readFile, 1, 0, +"readFile(filename, [\"binary\"])", +" Read entire contents of filename. Returns a string, unless \"binary\" is passed\n" +" as the second argument, in which case it returns a Uint8Array. Filename is\n" +" relative to the current working directory."), + + JS_FN_HELP("readRelativeToScript", osfile_readRelativeToScript, 1, 0, +"readRelativeToScript(filename, [\"binary\"])", +" Read filename into returned string. Filename is relative to the directory\n" +" containing the current script."), + + JS_FN_HELP("listDir", osfile_listDir, 1, 0, +"listDir(dirname)", +" Read entire contents of a directory. The \"dirname\" parameter is relate to the\n" +" current working directory. Returns a list of filenames within the given directory.\n" +" Note that \".\" and \"..\" are also listed."), + + JS_FN_HELP("listDirRelativeToScript", osfile_listDirRelativeToScript, 1, 0, +"listDirRelativeToScript(dirname)", +" Same as \"listDir\" except that the \"dirname\" is relative to the directory\n" +" containing the current script."), + + JS_FS_HELP_END +}; +// clang-format on + +// clang-format off +static const JSFunctionSpecWithHelp osfile_unsafe_functions[] = { + JS_FN_HELP("writeTypedArrayToFile", osfile_writeTypedArrayToFile, 2, 0, +"writeTypedArrayToFile(filename, data)", +" Write the contents of a typed array to the named file."), + + JS_FN_HELP("redirect", osfile_redirectOutput, 1, 0, +"redirect([path-or-object])", +" Redirect print() output to the named file.\n" +" Return an opaque object representing the previous destination, which\n" +" may be passed into redirect() later to restore the output."), + + JS_FN_HELP("redirectErr", osfile_redirectError, 1, 0, +"redirectErr([path-or-object])", +" Same as redirect(), but for printErr"), + + JS_FN_HELP("close", osfile_close, 1, 0, +"close(object)", +" Close the file returned by an earlier redirect call."), + + JS_FS_HELP_END +}; +// clang-format on + +static bool ospath_isAbsolute(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1 || !args[0].isString()) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "isAbsolute"); + return false; + } + + RootedLinearString str(cx, JS_EnsureLinearString(cx, args[0].toString())); + if (!str) { + return false; + } + + args.rval().setBoolean(IsAbsolutePath(str)); + return true; +} + +static bool ospath_join(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() < 1) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "join"); + return false; + } + + // This function doesn't take into account some aspects of Windows paths, + // e.g. the drive letter is always reset when an absolute path is appended. + + JSStringBuilder buffer(cx); + + for (unsigned i = 0; i < args.length(); i++) { + if (!args[i].isString()) { + JS_ReportErrorASCII(cx, "join expects string arguments only"); + return false; + } + + RootedLinearString str(cx, JS_EnsureLinearString(cx, args[i].toString())); + if (!str) { + return false; + } + + if (IsAbsolutePath(str)) { + MOZ_ALWAYS_TRUE(buffer.resize(0)); + } else if (i != 0) { + UniqueChars path = JS_EncodeStringToLatin1(cx, str); + if (!path) { + return false; + } + + if (!buffer.append(PathSeparator)) { + return false; + } + } + + if (!buffer.append(args[i].toString())) { + return false; + } + } + + JSString* result = buffer.finishString(); + if (!result) { + return false; + } + + args.rval().setString(result); + return true; +} + +// clang-format off +static const JSFunctionSpecWithHelp ospath_functions[] = { + JS_FN_HELP("isAbsolute", ospath_isAbsolute, 1, 0, +"isAbsolute(path)", +" Return whether the given path is absolute."), + + JS_FN_HELP("join", ospath_join, 1, 0, +"join(paths...)", +" Join one or more path components in a platform independent way."), + + JS_FS_HELP_END +}; +// clang-format on + +static bool os_getenv(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() < 1) { + JS_ReportErrorASCII(cx, "os.getenv requires 1 argument"); + return false; + } + RootedString key(cx, ToString(cx, args[0])); + if (!key) { + return false; + } + UniqueChars keyBytes = JS_EncodeStringToUTF8(cx, key); + if (!keyBytes) { + return false; + } + + if (const char* valueBytes = getenv(keyBytes.get())) { + RootedString value(cx, JS_NewStringCopyZ(cx, valueBytes)); + if (!value) { + return false; + } + args.rval().setString(value); + } else { + args.rval().setUndefined(); + } + return true; +} + +static bool os_getpid(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 0) { + JS_ReportErrorASCII(cx, "os.getpid takes no arguments"); + return false; + } + args.rval().setInt32(getpid()); + return true; +} + +#if !defined(XP_WIN) + +// There are two possible definitions of strerror_r floating around. The GNU +// one returns a char* which may or may not be the buffer you passed in. The +// other one returns an integer status code, and always writes the result into +// the provided buffer. + +inline char* strerror_message(int result, char* buffer) { + return result == 0 ? buffer : nullptr; +} + +inline char* strerror_message(char* result, char* buffer) { return result; } + +#endif + +static void ReportSysError(JSContext* cx, const char* prefix) { + char buffer[200]; + +#if defined(XP_WIN) + strerror_s(buffer, sizeof(buffer), errno); + const char* errstr = buffer; +#else + const char* errstr = + strerror_message(strerror_r(errno, buffer, sizeof(buffer)), buffer); +#endif + + if (!errstr) { + errstr = "unknown error"; + } + + /* + * Use Latin1 variant here because the encoding of the return value of + * strerror_s and strerror_r function can be non-UTF-8. + */ + JS_ReportErrorLatin1(cx, "%s: %s", prefix, errstr); +} + +static bool os_system(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() == 0) { + JS_ReportErrorASCII(cx, "os.system requires 1 argument"); + return false; + } + + JSString* str = JS::ToString(cx, args[0]); + if (!str) { + return false; + } + + UniqueChars command = JS_EncodeStringToLatin1(cx, str); + if (!command) { + return false; + } + + int result = system(command.get()); + if (result == -1) { + ReportSysError(cx, "system call failed"); + return false; + } + + args.rval().setInt32(result); + return true; +} + +#ifndef XP_WIN +static bool os_spawn(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() == 0) { + JS_ReportErrorASCII(cx, "os.spawn requires 1 argument"); + return false; + } + + JSString* str = JS::ToString(cx, args[0]); + if (!str) { + return false; + } + + UniqueChars command = JS_EncodeStringToLatin1(cx, str); + if (!command) { + return false; + } + + int32_t childPid = fork(); + if (childPid == -1) { + ReportSysError(cx, "fork failed"); + return false; + } + + if (childPid) { + args.rval().setInt32(childPid); + return true; + } + + // We are in the child + + const char* cmd[] = {"sh", "-c", nullptr, nullptr}; + cmd[2] = command.get(); + + execvp("sh", (char* const*)cmd); + exit(1); +} + +static bool os_kill(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + int32_t pid; + if (args.length() < 1) { + JS_ReportErrorASCII(cx, "os.kill requires 1 argument"); + return false; + } + if (!JS::ToInt32(cx, args[0], &pid)) { + return false; + } + + // It is too easy to kill yourself accidentally with os.kill("goose"). + if (pid == 0 && !args[0].isInt32()) { + JS_ReportErrorASCII(cx, "os.kill requires numeric pid"); + return false; + } + + int signal = SIGINT; + if (args.length() > 1) { + if (!JS::ToInt32(cx, args[1], &signal)) { + return false; + } + } + + int status = kill(pid, signal); + if (status == -1) { + ReportSysError(cx, "kill failed"); + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool os_waitpid(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + int32_t pid; + if (args.length() == 0) { + pid = -1; + } else { + if (!JS::ToInt32(cx, args[0], &pid)) { + return false; + } + } + + bool nohang = false; + if (args.length() >= 2) { + nohang = JS::ToBoolean(args[1]); + } + + int status = 0; + pid_t result = waitpid(pid, &status, nohang ? WNOHANG : 0); + if (result == -1) { + ReportSysError(cx, "os.waitpid failed"); + return false; + } + + RootedObject info(cx, JS_NewPlainObject(cx)); + if (!info) { + return false; + } + + RootedValue v(cx); + if (result != 0) { + v.setInt32(result); + if (!JS_DefineProperty(cx, info, "pid", v, JSPROP_ENUMERATE)) { + return false; + } + if (WIFEXITED(status)) { + v.setInt32(WEXITSTATUS(status)); + if (!JS_DefineProperty(cx, info, "exitStatus", v, JSPROP_ENUMERATE)) { + return false; + } + } + } + + args.rval().setObject(*info); + return true; +} +#endif + +// clang-format off +static const JSFunctionSpecWithHelp os_functions[] = { + JS_FN_HELP("getenv", os_getenv, 1, 0, +"getenv(variable)", +" Get the value of an environment variable."), + + JS_FN_HELP("getpid", os_getpid, 0, 0, +"getpid()", +" Return the current process id."), + + JS_FN_HELP("system", os_system, 1, 0, +"system(command)", +" Execute command on the current host, returning result code or throwing an\n" +" exception on failure."), + +#ifndef XP_WIN + JS_FN_HELP("spawn", os_spawn, 1, 0, +"spawn(command)", +" Start up a separate process running the given command. Returns the pid."), + + JS_FN_HELP("kill", os_kill, 1, 0, +"kill(pid[, signal])", +" Send a signal to the given pid. The default signal is SIGINT. The signal\n" +" passed in must be numeric, if given."), + + JS_FN_HELP("waitpid", os_waitpid, 1, 0, +"waitpid(pid[, nohang])", +" Calls waitpid(). 'nohang' is a boolean indicating whether to pass WNOHANG.\n" +" The return value is an object containing a 'pid' field, if a process was waitable\n" +" and an 'exitStatus' field if a pid exited."), +#endif + + JS_FS_HELP_END +}; +// clang-format on + +bool DefineOS(JSContext* cx, HandleObject global, bool fuzzingSafe, + RCFile** shellOut, RCFile** shellErr) { + RootedObject obj(cx, JS_NewPlainObject(cx)); + if (!obj || !JS_DefineProperty(cx, global, "os", obj, 0)) { + return false; + } + + if (!fuzzingSafe) { + if (!JS_DefineFunctionsWithHelp(cx, obj, os_functions)) { + return false; + } + } + + RootedObject osfile(cx, JS_NewPlainObject(cx)); + if (!osfile || !JS_DefineFunctionsWithHelp(cx, osfile, osfile_functions) || + !JS_DefineProperty(cx, obj, "file", osfile, 0)) { + return false; + } + + if (!fuzzingSafe) { + if (!JS_DefineFunctionsWithHelp(cx, osfile, osfile_unsafe_functions)) { + return false; + } + } + + if (!GenerateInterfaceHelp(cx, osfile, "os.file")) { + return false; + } + + RootedObject ospath(cx, JS_NewPlainObject(cx)); + if (!ospath || !JS_DefineFunctionsWithHelp(cx, ospath, ospath_functions) || + !JS_DefineProperty(cx, obj, "path", ospath, 0) || + !GenerateInterfaceHelp(cx, ospath, "os.path")) { + return false; + } + + if (!GenerateInterfaceHelp(cx, obj, "os")) { + return false; + } + + ShellContext* scx = GetShellContext(cx); + scx->outFilePtr = shellOut; + scx->errFilePtr = shellErr; + + // For backwards compatibility, expose various os.file.* functions as + // direct methods on the global. + struct Export { + const char* src; + const char* dst; + }; + + const Export osfile_exports[] = { + {"readFile", "read"}, + {"readFile", "snarf"}, + {"readRelativeToScript", "readRelativeToScript"}, + }; + + for (auto pair : osfile_exports) { + if (!CreateAlias(cx, pair.dst, osfile, pair.src)) { + return false; + } + } + + if (!fuzzingSafe) { + const Export unsafe_osfile_exports[] = {{"redirect", "redirect"}, + {"redirectErr", "redirectErr"}}; + + for (auto pair : unsafe_osfile_exports) { + if (!CreateAlias(cx, pair.dst, osfile, pair.src)) { + return false; + } + } + } + + return true; +} + +} // namespace shell +} // namespace js diff --git a/js/src/shell/OSObject.h b/js/src/shell/OSObject.h new file mode 100644 index 0000000000..1ae23eae8e --- /dev/null +++ b/js/src/shell/OSObject.h @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// OSObject.h - os object for exposing posix system calls in the JS shell + +#ifndef shell_OSObject_h +#define shell_OSObject_h + +#include "jsapi.h" + +namespace js { +namespace shell { + +#ifdef XP_WIN +constexpr char PathSeparator = '\\'; +#else +constexpr char PathSeparator = '/'; +#endif + +struct RCFile; + +/* Define an os object on the given global object. */ +bool DefineOS(JSContext* cx, JS::HandleObject global, bool fuzzingSafe, + RCFile** shellOut, RCFile** shellErr); + +enum PathResolutionMode { RootRelative, ScriptRelative }; + +bool IsAbsolutePath(JSLinearString* filename); + +JSString* ResolvePath(JSContext* cx, JS::HandleString filenameStr, + PathResolutionMode resolveMode); + +JSObject* FileAsTypedArray(JSContext* cx, JS::HandleString pathnameStr); + +JS::UniqueChars GetCWD(); + +} // namespace shell +} // namespace js + +#endif /* shell_OSObject_h */ diff --git a/js/src/shell/StringUtils.h b/js/src/shell/StringUtils.h new file mode 100644 index 0000000000..be604eddee --- /dev/null +++ b/js/src/shell/StringUtils.h @@ -0,0 +1,147 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* String utility functions used by the module loader. */ + +#ifndef shell_StringUtils_h +#define shell_StringUtils_h + +#include "jsapi.h" + +#include "js/StableStringChars.h" + +namespace js { +namespace shell { + +inline char16_t CharAt(JSLinearString* str, size_t index) { + return str->latin1OrTwoByteChar(index); +} + +inline JSLinearString* SubString(JSContext* cx, JSLinearString* str, + size_t start, size_t end) { + MOZ_ASSERT(start <= str->length()); + MOZ_ASSERT(end >= start && end <= str->length()); + return NewDependentString(cx, str, start, end - start); +} + +inline JSLinearString* SubString(JSContext* cx, JSLinearString* str, + size_t start) { + return SubString(cx, str, start, str->length()); +} + +template +bool StringStartsWith(JSLinearString* str, + const char16_t (&chars)[NullTerminatedLength]) { + MOZ_ASSERT(NullTerminatedLength > 0); + const size_t length = NullTerminatedLength - 1; + MOZ_ASSERT(chars[length] == '\0'); + + if (str->length() < length) { + return false; + } + + for (size_t i = 0; i < length; i++) { + if (CharAt(str, i) != chars[i]) { + return false; + } + } + + return true; +} + +template +bool StringEquals(JSLinearString* str, + const char16_t (&chars)[NullTerminatedLength]) { + MOZ_ASSERT(NullTerminatedLength > 0); + const size_t length = NullTerminatedLength - 1; + MOZ_ASSERT(chars[length] == '\0'); + + return str->length() == length && StringStartsWith(str, chars); +} + +inline int32_t IndexOf(HandleLinearString str, char16_t target, + size_t start = 0) { + int32_t length = str->length(); + for (int32_t i = start; i < length; i++) { + if (CharAt(str, i) == target) { + return i; + } + } + + return -1; +} + +inline int32_t LastIndexOf(HandleLinearString str, char16_t target) { + int32_t length = str->length(); + for (int32_t i = length - 1; i >= 0; i--) { + if (CharAt(str, i) == target) { + return i; + } + } + + return -1; +} + +inline JSLinearString* ReplaceCharGlobally(JSContext* cx, + HandleLinearString str, + char16_t target, + char16_t replacement) { + int32_t i = IndexOf(str, target); + if (i == -1) { + return str; + } + + JS::AutoStableStringChars chars(cx); + if (!chars.initTwoByte(cx, str)) { + return nullptr; + } + + Vector buf(cx); + if (!buf.append(chars.twoByteChars(), str->length())) { + return nullptr; + } + + for (; i < int32_t(buf.length()); i++) { + if (buf[i] == target) { + buf[i] = replacement; + } + } + + RootedString result(cx, JS_NewUCStringCopyN(cx, buf.begin(), buf.length())); + if (!result) { + return nullptr; + } + + return JS_EnsureLinearString(cx, result); +} + +inline JSString* JoinStrings(JSContext* cx, + Handle> strings, + HandleLinearString separator) { + RootedString result(cx, JS_GetEmptyString(cx)); + + for (size_t i = 0; i < strings.length(); i++) { + HandleString str = strings[i]; + if (i != 0) { + result = JS_ConcatStrings(cx, result, separator); + if (!result) { + return nullptr; + } + } + + result = JS_ConcatStrings(cx, result, str); + if (!result) { + return nullptr; + } + } + + return result; +} + +} // namespace shell +} // namespace js + +#endif // shell_StringUtils_h diff --git a/js/src/shell/WasmTesting.cpp b/js/src/shell/WasmTesting.cpp new file mode 100644 index 0000000000..dcbbe7f4a0 --- /dev/null +++ b/js/src/shell/WasmTesting.cpp @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * + * Copyright 2015 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "shell/WasmTesting.h" + +#include +#include +#include + +#include "wasm/WasmTypes.h" + +using namespace js; +using namespace js::wasm; + +extern "C" { +bool wasm_text_to_binary(const char16_t* text, size_t text_len, + uint8_t** out_bytes, size_t* out_bytes_len, + uint8_t** out_error, size_t* out_error_len); +void wasm_code_offsets(const uint8_t* bytes, size_t bytes_len, + uint32_t** out_offsets, size_t* out_offset_len); +} // extern "C" + +bool wasm::TextToBinary(const char16_t* text, size_t textLen, Bytes* bytes, + UniqueChars* error) { + uint8_t* outBytes = nullptr; + size_t outBytesLength = 0; + + uint8_t* outError = nullptr; + size_t outErrorLength = 0; + + bool result = wasm_text_to_binary(text, textLen, &outBytes, &outBytesLength, + &outError, &outErrorLength); + + if (result) { + MOZ_ASSERT(outBytes); + MOZ_ASSERT(outBytesLength > 0); + bytes->replaceRawBuffer(outBytes, outBytesLength); + return true; + } + + MOZ_ASSERT(outError); + MOZ_ASSERT(outErrorLength > 0); + *error = UniqueChars{(char*)outError}; + return false; +} + +void wasm::CodeOffsets(const uint8_t* bytes, size_t bytesLen, + Uint32Vector* offsets) { + uint32_t* outOffsets = nullptr; + size_t outOffsetsLength = 0; + + wasm_code_offsets(bytes, bytesLen, &outOffsets, &outOffsetsLength); + + if (outOffsets) { + MOZ_ASSERT(outOffsetsLength > 0); + offsets->replaceRawBuffer(outOffsets, outOffsetsLength); + } else { + offsets->clear(); + } +} diff --git a/js/src/shell/WasmTesting.h b/js/src/shell/WasmTesting.h new file mode 100644 index 0000000000..fc6fc88118 --- /dev/null +++ b/js/src/shell/WasmTesting.h @@ -0,0 +1,46 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * + * Copyright 2020 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef shell_wasm_h +#define shell_wasm_h + +#include "wasm/WasmTypes.h" + +namespace js { +namespace wasm { + +// Translate the textual representation of a wasm module (given by a +// char16_t array + length) into serialized bytes. If there is an error +// other than out-of-memory an error message string will be stored in 'error'. + +extern MOZ_MUST_USE bool TextToBinary(const char16_t* text, size_t textLen, + Bytes* bytes, UniqueChars* error); + +// Decode the binary wasm module given and return the offsets of all +// instructions inside of the the code section. +// +// This function is used exclusively for testing and handles errors by +// returning an empty offset array. + +extern void CodeOffsets(const uint8_t* bytes, size_t bytesLen, + Uint32Vector* offsets); + +} // namespace wasm +} // namespace js + +#endif // shell_wasm_h diff --git a/js/src/shell/fuzz-flags.txt b/js/src/shell/fuzz-flags.txt new file mode 100644 index 0000000000..2c091c875c --- /dev/null +++ b/js/src/shell/fuzz-flags.txt @@ -0,0 +1,83 @@ +# This lists all the possible flags we'd like to see tested out in the shell by +# fuzzers. A non-empty line not starting with # should be considered a valid +# one. Note the following flag is recommended in ALL the cases: --fuzzing-safe + +# general jit flags +--baseline-eager +--blinterp +--no-blinterp +--blinterp-eager +--cache-ir-stubs=off +--cache-ir-stubs=on +--ion-check-range-analysis +--ion-eager +--ion-edgecase-analysis=off +--ion-edgecase-analysis=on +--ion-extra-checks +--ion-gvn=off +--ion-gvn=on +--ion-inlining=off +--ion-inlining=on +--ion-instruction-reordering=off +--ion-instruction-reordering=on +--ion-licm=off +--ion-licm=on +--ion-limit-script-size=off +--ion-limit-script-size=on +--ion-offthread-compile=off +--ion-osr=off +--ion-osr=on +--ion-pgo=off +--ion-pgo=on +--ion-range-analysis=off +--ion-range-analysis=on +--ion-regalloc=testbed +--ion-scalar-replacement=off +--ion-scalar-replacement=on +--ion-warmup-threshold=0 +--ion-warmup-threshold=10 +--ion-warmup-threshold=100 +--no-native-regexp +--nursery-strings=off +--nursery-strings=on +--nursery-bigints=off +--nursery-bigints=on +--spectre-mitigations=off +--spectre-mitigations=on +--more-compartments +--fast-warmup +--warp-generator +--warp-async +--no-warp-generator +--no-warp-async + +# GC-related +# These 2 flags can cause the shell to slow down +# --gc-zeal=2 +# --gc-zeal=10 +--no-cgc +--no-ggc +--no-incremental-gc + +# wasm flags +--wasm-gc +--wasm-compiler=baseline +--wasm-compiler=optimizing +--wasm-compiler=baseline+optimizing +--test-wasm-await-tier2 +--wasm-disable-huge-memory + +# CPU instruction set-related +--no-sse3 +--no-ssse3 +--no-sse41 +--no-sse42 + +# arm specific, no-ops on other platforms. +--arm-sim-icache-checks +--arm-asm-nop-fill=1 +--arm-hwcap=vfp + +# Profiling, code coverage, and debugging +# --dump-bytecode option implies --code-coverage +--dump-bytecode diff --git a/js/src/shell/js-gdb.py b/js/src/shell/js-gdb.py new file mode 100644 index 0000000000..1125e8e5ff --- /dev/null +++ b/js/src/shell/js-gdb.py @@ -0,0 +1,21 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +""" GDB Python customization auto-loader for JS shell binary """ + +# This script will be installed into $objdir/dist/bin. Add $objdir to gdb's +# source search path and load in the Gecko+JS init file. + +import gdb +import os +import re + +from os.path import abspath, dirname + +devel_objdir = abspath(os.path.join(dirname(__file__), "..", "..")) +m = re.search(r"[\w ]+: (.*)", gdb.execute("show dir", False, True)) +if m and devel_objdir not in m.group(1).split(":"): + gdb.execute("set dir {}:{}".format(devel_objdir, m.group(1))) + +gdb.execute("source -s build/.gdbinit.loader") diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp new file mode 100644 index 0000000000..0b0083eba9 --- /dev/null +++ b/js/src/shell/js.cpp @@ -0,0 +1,12030 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* JS shell. */ + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Atomics.h" +#include "mozilla/Attributes.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/mozalloc.h" +#include "mozilla/PodOperations.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Sprintf.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/UniquePtrExtensions.h" // UniqueFreePtr +#include "mozilla/Unused.h" +#include "mozilla/Utf8.h" +#include "mozilla/Variant.h" + +#include +#include +#ifdef XP_WIN +# include +# include +#endif +#include +#include +#if defined(XP_WIN) +# include /* for isatty() */ +#endif +#include +#if defined(MALLOC_H) +# include MALLOC_H /* for malloc_usable_size, malloc_size, _msize */ +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef XP_UNIX +# include +# include +# include +# include +#endif +#ifdef XP_LINUX +# include +#endif + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "jstypes.h" +#ifndef JS_WITHOUT_NSPR +# include "prerror.h" +# include "prlink.h" +#endif + +#include "builtin/Array.h" +#include "builtin/MapObject.h" +#include "builtin/ModuleObject.h" +#include "builtin/RegExp.h" +#include "builtin/TestingFunctions.h" +#include "debugger/DebugAPI.h" +#include "frontend/BytecodeCompilation.h" +#include "frontend/BytecodeCompiler.h" +#include "frontend/BytecodeEmitter.h" +#include "frontend/CompilationInfo.h" +#ifdef JS_ENABLE_SMOOSH +# include "frontend/Frontend2.h" +#endif +#include "frontend/ModuleSharedContext.h" +#include "frontend/Parser.h" +#include "frontend/SourceNotes.h" // SrcNote, SrcNoteType, SrcNoteIterator +#include "gc/PublicIterators.h" +#include "gc/GC-inl.h" // ZoneCellIter +#ifdef JS_SIMULATOR_ARM +# include "jit/arm/Simulator-arm.h" +#endif +#ifdef JS_SIMULATOR_MIPS32 +# include "jit/mips32/Simulator-mips32.h" +#endif +#ifdef JS_SIMULATOR_MIPS64 +# include "jit/mips64/Simulator-mips64.h" +#endif +#include "jit/CacheIRHealth.h" +#include "jit/InlinableNatives.h" +#include "jit/Ion.h" +#include "jit/JitcodeMap.h" +#include "jit/shared/CodeGenerator-shared.h" +#include "js/Array.h" // JS::NewArrayObject +#include "js/ArrayBuffer.h" // JS::{CreateMappedArrayBufferContents,NewMappedArrayBufferWithContents,IsArrayBufferObject,GetArrayBufferLengthAndData} +#include "js/BuildId.h" // JS::BuildIdCharVector, JS::SetProcessBuildIdOp +#include "js/CharacterEncoding.h" // JS::StringIsASCII +#include "js/CompilationAndEvaluation.h" +#include "js/CompileOptions.h" +#include "js/ContextOptions.h" // JS::ContextOptions{,Ref} +#include "js/Debug.h" +#include "js/Equality.h" // JS::SameValue +#include "js/ErrorReport.h" // JS::PrintError +#include "js/Exception.h" // JS::StealPendingExceptionStack +#include "js/experimental/CodeCoverage.h" // js::EnableCodeCoverage +#include "js/experimental/CTypes.h" // JS::InitCTypesClass +#include "js/experimental/Intl.h" // JS::AddMoz{DateTimeFormat,DisplayNames}Constructor +#include "js/experimental/JitInfo.h" // JSJit{Getter,Setter,Method}CallArgs, JSJitGetterInfo, JSJit{Getter,Setter}Op, JSJitInfo +#include "js/experimental/SourceHook.h" // js::{Set,Forget,}SourceHook +#include "js/experimental/TypedData.h" // JS_NewUint8Array +#include "js/friend/DumpFunctions.h" // JS::FormatStackDump +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/friend/StackLimits.h" // js::CheckRecursionLimitConservative +#include "js/friend/WindowProxy.h" // js::IsWindowProxy, js::SetWindowProxyClass, js::ToWindowProxyIfWindow, js::ToWindowIfWindowProxy +#include "js/GCAPI.h" // JS::AutoCheckCannotGC +#include "js/GCVector.h" +#include "js/Initialization.h" +#include "js/JSON.h" +#include "js/MemoryFunctions.h" +#include "js/Modules.h" // JS::GetModulePrivate, JS::SetModule{DynamicImport,Metadata,Resolve}Hook, JS::SetModulePrivate +#include "js/Object.h" // JS::GetClass, JS::GetCompartment, JS::GetReservedSlot, JS::SetReservedSlot +#include "js/OffThreadScriptCompilation.h" // JS::SetUseOffThreadParseGlobal, js::UseOffThreadParseGlobal +#include "js/Printf.h" +#include "js/PropertySpec.h" +#include "js/Realm.h" +#include "js/RegExp.h" // JS::ObjectIsRegExp +#include "js/SourceText.h" +#include "js/StableStringChars.h" +#include "js/StructuredClone.h" +#include "js/SweepingAPI.h" +#include "js/Transcoding.h" // JS::TranscodeBuffer, JS::TranscodeRange +#include "js/Warnings.h" // JS::SetWarningReporter +#include "js/WasmModule.h" // JS::WasmModule +#include "js/Wrapper.h" +#include "shell/jsoptparse.h" +#include "shell/jsshell.h" +#include "shell/OSObject.h" +#include "shell/WasmTesting.h" +#include "threading/ConditionVariable.h" +#include "threading/ExclusiveData.h" +#include "threading/LockGuard.h" +#include "threading/Thread.h" +#include "util/CompleteFile.h" // js::FileContents, js::ReadCompleteFile +#include "util/DifferentialTesting.h" +#include "util/StringBuffer.h" +#include "util/Text.h" +#include "util/Windows.h" +#include "vm/ArgumentsObject.h" +#include "vm/Compression.h" +#include "vm/HelperThreads.h" +#include "vm/JSAtom.h" +#include "vm/JSContext.h" +#include "vm/JSFunction.h" +#include "vm/JSObject.h" +#include "vm/JSScript.h" +#include "vm/ModuleBuilder.h" // js::ModuleBuilder +#include "vm/Monitor.h" +#include "vm/MutexIDs.h" +#include "vm/Printer.h" // QuoteString +#include "vm/PromiseObject.h" // js::PromiseObject +#include "vm/Shape.h" +#include "vm/SharedArrayObject.h" +#include "vm/Time.h" +#include "vm/ToSource.h" // js::ValueToSource +#include "vm/TypedArrayObject.h" +#include "vm/WrapperObject.h" +#include "wasm/WasmJS.h" + +#include "vm/Compartment-inl.h" +#include "vm/ErrorObject-inl.h" +#include "vm/Interpreter-inl.h" +#include "vm/JSObject-inl.h" +#include "vm/Realm-inl.h" +#include "vm/Stack-inl.h" + +using namespace js; +using namespace js::cli; +using namespace js::shell; + +using JS::AutoStableStringChars; +using JS::CompileOptions; + +using js::shell::RCFile; + +using mozilla::ArrayEqual; +using mozilla::AsVariant; +using mozilla::Atomic; +using mozilla::MakeScopeExit; +using mozilla::Maybe; +using mozilla::Nothing; +using mozilla::NumberEqualsInt32; +using mozilla::TimeDuration; +using mozilla::TimeStamp; +using mozilla::Utf8Unit; +using mozilla::Variant; + +enum JSShellExitCode { + EXITCODE_RUNTIME_ERROR = 3, + EXITCODE_FILE_NOT_FOUND = 4, + EXITCODE_OUT_OF_MEMORY = 5, + EXITCODE_TIMEOUT = 6 +}; + +/* + * Note: This limit should match the stack limit set by the browser in + * js/xpconnect/src/XPCJSContext.cpp + */ +#if defined(MOZ_ASAN) || (defined(DEBUG) && !defined(XP_WIN)) +static const size_t gMaxStackSize = 2 * 128 * sizeof(size_t) * 1024; +#else +static const size_t gMaxStackSize = 128 * sizeof(size_t) * 1024; +#endif + +/* + * Limit the timeout to 30 minutes to prevent an overflow on platfoms + * that represent the time internally in microseconds using 32-bit int. + */ +static const double MAX_TIMEOUT_SECONDS = 1800.0; + +// Not necessarily in sync with the browser +#ifdef ENABLE_SHARED_MEMORY +# define SHARED_MEMORY_DEFAULT 1 +#else +# define SHARED_MEMORY_DEFAULT 0 +#endif + +// Fuzzing support for JS runtime fuzzing +#ifdef FUZZING_INTERFACES +# include "shell/jsrtfuzzing/jsrtfuzzing.h" +static bool fuzzDoDebug = !!getenv("MOZ_FUZZ_DEBUG"); +static bool fuzzHaveModule = !!getenv("FUZZER"); +#endif // FUZZING_INTERFACES + +// Code to support GCOV code coverage measurements on standalone shell +#ifdef MOZ_CODE_COVERAGE +# if defined(__GNUC__) && !defined(__clang__) +extern "C" void __gcov_dump(); +extern "C" void __gcov_reset(); + +void counters_dump(int) { __gcov_dump(); } + +void counters_reset(int) { __gcov_reset(); } +# else +void counters_dump(int) { /* Do nothing */ +} + +void counters_reset(int) { /* Do nothing */ +} +# endif + +static void InstallCoverageSignalHandlers() { +# ifndef XP_WIN + fprintf(stderr, "[CodeCoverage] Setting handlers for process %d.\n", + getpid()); + + struct sigaction dump_sa; + dump_sa.sa_handler = counters_dump; + dump_sa.sa_flags = SA_RESTART; + sigemptyset(&dump_sa.sa_mask); + mozilla::DebugOnly r1 = sigaction(SIGUSR1, &dump_sa, nullptr); + MOZ_ASSERT(r1 == 0, "Failed to install GCOV SIGUSR1 handler"); + + struct sigaction reset_sa; + reset_sa.sa_handler = counters_reset; + reset_sa.sa_flags = SA_RESTART; + sigemptyset(&reset_sa.sa_mask); + mozilla::DebugOnly r2 = sigaction(SIGUSR2, &reset_sa, nullptr); + MOZ_ASSERT(r2 == 0, "Failed to install GCOV SIGUSR2 handler"); +# endif +} +#endif + +// An off-thread parse or decode job. +class js::shell::OffThreadJob { + enum State { + RUNNING, // Working; no token. + DONE, // Finished; have token. + CANCELLED // Cancelled due to error. + }; + + public: + using Source = mozilla::Variant; + + OffThreadJob(ShellContext* sc, ScriptKind kind, Source&& source); + ~OffThreadJob(); + + void cancel(); + void markDone(JS::OffThreadToken* newToken); + JS::OffThreadToken* waitUntilDone(JSContext* cx); + + char16_t* sourceChars() { return source.as().get(); } + JS::TranscodeBuffer& xdrBuffer() { return source.as(); } + + public: + const int32_t id; + const ScriptKind kind; + + private: + js::Monitor& monitor; + State state; + JS::OffThreadToken* token; + Source source; +}; + +static OffThreadJob* NewOffThreadJob(JSContext* cx, ScriptKind kind, + OffThreadJob::Source&& source) { + ShellContext* sc = GetShellContext(cx); + UniquePtr job( + cx->new_(sc, kind, std::move(source))); + if (!job) { + return nullptr; + } + + if (!sc->offThreadJobs.append(job.get())) { + job->cancel(); + JS_ReportErrorASCII(cx, "OOM adding off-thread job"); + return nullptr; + } + + return job.release(); +} + +static OffThreadJob* GetSingleOffThreadJob(JSContext* cx, ScriptKind kind) { + ShellContext* sc = GetShellContext(cx); + const auto& jobs = sc->offThreadJobs; + if (jobs.empty()) { + JS_ReportErrorASCII(cx, "No off-thread jobs are pending"); + return nullptr; + } + + if (jobs.length() > 1) { + JS_ReportErrorASCII( + cx, "Multiple off-thread jobs are pending: must specify job ID"); + return nullptr; + } + + OffThreadJob* job = jobs[0]; + if (job->kind != kind) { + JS_ReportErrorASCII(cx, "Off-thread job is the wrong kind"); + return nullptr; + } + + return job; +} + +static OffThreadJob* LookupOffThreadJobByID(JSContext* cx, ScriptKind kind, + int32_t id) { + if (id <= 0) { + JS_ReportErrorASCII(cx, "Bad off-thread job ID"); + return nullptr; + } + + ShellContext* sc = GetShellContext(cx); + const auto& jobs = sc->offThreadJobs; + if (jobs.empty()) { + JS_ReportErrorASCII(cx, "No off-thread jobs are pending"); + return nullptr; + } + + OffThreadJob* job = nullptr; + for (auto someJob : jobs) { + if (someJob->id == id) { + job = someJob; + break; + } + } + + if (!job) { + JS_ReportErrorASCII(cx, "Off-thread job not found"); + return nullptr; + } + + if (job->kind != kind) { + JS_ReportErrorASCII(cx, "Off-thread job is the wrong kind"); + return nullptr; + } + + return job; +} + +static OffThreadJob* LookupOffThreadJobForArgs(JSContext* cx, ScriptKind kind, + const CallArgs& args, + size_t arg) { + // If the optional ID argument isn't present, get the single pending job. + if (args.length() <= arg) { + return GetSingleOffThreadJob(cx, kind); + } + + // Lookup the job using the specified ID. + int32_t id = 0; + RootedValue value(cx, args[arg]); + if (!ToInt32(cx, value, &id)) { + return nullptr; + } + + return LookupOffThreadJobByID(cx, kind, id); +} + +static void DeleteOffThreadJob(JSContext* cx, OffThreadJob* job) { + ShellContext* sc = GetShellContext(cx); + for (size_t i = 0; i < sc->offThreadJobs.length(); i++) { + if (sc->offThreadJobs[i] == job) { + sc->offThreadJobs.erase(&sc->offThreadJobs[i]); + js_delete(job); + return; + } + } + + MOZ_CRASH("Off-thread job not found"); +} + +static void CancelOffThreadJobsForContext(JSContext* cx) { + // Parse jobs may be blocked waiting on GC. + gc::FinishGC(cx); + + // Wait for jobs belonging to this context. + ShellContext* sc = GetShellContext(cx); + while (!sc->offThreadJobs.empty()) { + OffThreadJob* job = sc->offThreadJobs.popCopy(); + job->waitUntilDone(cx); + js_delete(job); + } +} + +static void CancelOffThreadJobsForRuntime(JSContext* cx) { + // Parse jobs may be blocked waiting on GC. + gc::FinishGC(cx); + + // Cancel jobs belonging to this runtime. + CancelOffThreadParses(cx->runtime()); + ShellContext* sc = GetShellContext(cx); + while (!sc->offThreadJobs.empty()) { + js_delete(sc->offThreadJobs.popCopy()); + } +} + +mozilla::Atomic gOffThreadJobSerial(1); + +OffThreadJob::OffThreadJob(ShellContext* sc, ScriptKind kind, Source&& source) + : id(gOffThreadJobSerial++), + kind(kind), + monitor(sc->offThreadMonitor), + state(RUNNING), + token(nullptr), + source(std::move(source)) { + MOZ_RELEASE_ASSERT(id > 0, "Off-thread job IDs exhausted"); +} + +OffThreadJob::~OffThreadJob() { MOZ_ASSERT(state != RUNNING); } + +void OffThreadJob::cancel() { + MOZ_ASSERT(state == RUNNING); + MOZ_ASSERT(!token); + + state = CANCELLED; +} + +void OffThreadJob::markDone(JS::OffThreadToken* newToken) { + AutoLockMonitor alm(monitor); + MOZ_ASSERT(state == RUNNING); + MOZ_ASSERT(!token); + MOZ_ASSERT(newToken); + + token = newToken; + state = DONE; + alm.notifyAll(); +} + +JS::OffThreadToken* OffThreadJob::waitUntilDone(JSContext* cx) { + AutoLockMonitor alm(monitor); + MOZ_ASSERT(state != CANCELLED); + + while (state != DONE) { + alm.wait(); + } + + MOZ_ASSERT(token); + return token; +} + +struct ShellCompartmentPrivate { + GCPtrObject grayRoot; +}; + +struct MOZ_STACK_CLASS EnvironmentPreparer + : public js::ScriptEnvironmentPreparer { + explicit EnvironmentPreparer(JSContext* cx) { + js::SetScriptEnvironmentPreparer(cx, this); + } + void invoke(JS::HandleObject global, Closure& closure) override; +}; + +const char* shell::selfHostedXDRPath = nullptr; +bool shell::encodeSelfHostedCode = false; +bool shell::enableCodeCoverage = false; +bool shell::enableDisassemblyDumps = false; +bool shell::offthreadCompilation = false; +bool shell::enableAsmJS = false; +bool shell::enableWasm = false; +bool shell::enableSharedMemory = SHARED_MEMORY_DEFAULT; +bool shell::enableWasmBaseline = false; +bool shell::enableWasmOptimizing = false; +bool shell::enableWasmReftypes = true; +#ifdef ENABLE_WASM_FUNCTION_REFERENCES +bool shell::enableWasmFunctionReferences = false; +#endif +#ifdef ENABLE_WASM_GC +bool shell::enableWasmGc = false; +#endif +#ifdef ENABLE_WASM_MULTI_VALUE +bool shell::enableWasmMultiValue = true; +#endif +#ifdef ENABLE_WASM_SIMD +bool shell::enableWasmSimd = true; +#endif +#ifdef ENABLE_WASM_SIMD_WORMHOLE +bool shell::enableWasmSimdWormhole = false; +#endif +bool shell::enableWasmVerbose = false; +bool shell::enableTestWasmAwaitTier2 = false; +bool shell::enableSourcePragmas = true; +#ifdef ENABLE_WASM_EXCEPTIONS +bool shell::enableWasmExceptions = false; +#endif +bool shell::enableAsyncStacks = false; +bool shell::enableAsyncStackCaptureDebuggeeOnly = false; +bool shell::enableStreams = false; +bool shell::enableReadableByteStreams = false; +bool shell::enableBYOBStreamReaders = false; +bool shell::enableWritableStreams = false; +bool shell::enableReadableStreamPipeTo = false; +bool shell::enableWeakRefs = false; +bool shell::enableToSource = false; +bool shell::enablePropertyErrorMessageFix = false; +bool shell::enableIteratorHelpers = false; +bool shell::enablePrivateClassFields = false; +bool shell::enablePrivateClassMethods = false; +bool shell::enableTopLevelAwait = false; +bool shell::useOffThreadParseGlobal = true; +#ifdef JS_GC_ZEAL +uint32_t shell::gZealBits = 0; +uint32_t shell::gZealFrequency = 0; +#endif +bool shell::printTiming = false; +RCFile* shell::gErrFile = nullptr; +RCFile* shell::gOutFile = nullptr; +bool shell::reportWarnings = true; +bool shell::compileOnly = false; +bool shell::fuzzingSafe = false; +bool shell::disableOOMFunctions = false; +bool shell::defaultToSameCompartment = true; + +#ifdef DEBUG +bool shell::dumpEntrainedVariables = false; +bool shell::OOM_printAllocationCount = false; +#endif + +UniqueChars shell::processWideModuleLoadPath; + +static bool SetTimeoutValue(JSContext* cx, double t); + +static void KillWatchdog(JSContext* cx); + +static bool ScheduleWatchdog(JSContext* cx, double t); + +static void CancelExecution(JSContext* cx); + +enum class ShellGlobalKind { + GlobalObject, + WindowProxy, +}; + +static JSObject* NewGlobalObject(JSContext* cx, JS::RealmOptions& options, + JSPrincipals* principals, ShellGlobalKind kind, + bool immutablePrototype); + +/* + * A toy WindowProxy class for the shell. This is intended for testing code + * where global |this| is a WindowProxy. All requests are forwarded to the + * underlying global and no navigation is supported. + */ +const JSClass ShellWindowProxyClass = + PROXY_CLASS_DEF("ShellWindowProxy", JSCLASS_HAS_RESERVED_SLOTS(1)); + +JSObject* NewShellWindowProxy(JSContext* cx, JS::HandleObject global) { + MOZ_ASSERT(global->is()); + + js::WrapperOptions options; + options.setClass(&ShellWindowProxyClass); + + JSAutoRealm ar(cx, global); + JSObject* obj = + js::Wrapper::NewSingleton(cx, global, &js::Wrapper::singleton, options); + MOZ_ASSERT_IF(obj, js::IsWindowProxy(obj)); + return obj; +} + +/* + * A toy principals type for the shell. + * + * In the shell, a principal is simply a 32-bit mask: P subsumes Q if the + * set bits in P are a superset of those in Q. Thus, the principal 0 is + * subsumed by everything, and the principal ~0 subsumes everything. + * + * As a special case, a null pointer as a principal is treated like 0xffff. + * + * The 'newGlobal' function takes an option indicating which principal the + * new global should have; 'evaluate' does for the new code. + */ +class ShellPrincipals final : public JSPrincipals { + uint32_t bits; + + static uint32_t getBits(JSPrincipals* p) { + if (!p) { + return 0xffff; + } + return static_cast(p)->bits; + } + + public: + explicit ShellPrincipals(uint32_t bits, int32_t refcount = 0) : bits(bits) { + this->refcount = refcount; + } + + bool write(JSContext* cx, JSStructuredCloneWriter* writer) override { + // The shell doesn't have a read principals hook, so it doesn't really + // matter what we write here, but we have to write something so the + // fuzzer is happy. + return JS_WriteUint32Pair(writer, bits, 0); + } + + bool isSystemOrAddonPrincipal() override { return true; } + + static void destroy(JSPrincipals* principals) { + MOZ_ASSERT(principals != &fullyTrusted); + MOZ_ASSERT(principals->refcount == 0); + js_delete(static_cast(principals)); + } + + static bool subsumes(JSPrincipals* first, JSPrincipals* second) { + uint32_t firstBits = getBits(first); + uint32_t secondBits = getBits(second); + return (firstBits | secondBits) == firstBits; + } + + static JSSecurityCallbacks securityCallbacks; + + // Fully-trusted principals singleton. + static ShellPrincipals fullyTrusted; +}; + +JSSecurityCallbacks ShellPrincipals::securityCallbacks = { + nullptr, // contentSecurityPolicyAllows + subsumes}; + +// The fully-trusted principal subsumes all other principals. +ShellPrincipals ShellPrincipals::fullyTrusted(-1, 1); + +#ifdef EDITLINE +extern "C" { +extern MOZ_EXPORT char* readline(const char* prompt); +extern MOZ_EXPORT void add_history(char* line); +} // extern "C" +#endif + +ShellContext::ShellContext(JSContext* cx) + : isWorker(false), + lastWarningEnabled(false), + trackUnhandledRejections(true), + timeoutInterval(-1.0), + startTime(PRMJ_Now()), + serviceInterrupt(false), + haveInterruptFunc(false), + interruptFunc(cx, NullValue()), + lastWarning(cx, NullValue()), + promiseRejectionTrackerCallback(cx, NullValue()), + unhandledRejectedPromises(cx), + watchdogLock(mutexid::ShellContextWatchdog), + exitCode(0), + quitting(false), + readLineBufPos(0), + errFilePtr(nullptr), + outFilePtr(nullptr), + offThreadMonitor(mutexid::ShellOffThreadState), + finalizationRegistryCleanupCallbacks(cx) {} + +ShellContext::~ShellContext() { MOZ_ASSERT(offThreadJobs.empty()); } + +ShellContext* js::shell::GetShellContext(JSContext* cx) { + ShellContext* sc = static_cast(JS_GetContextPrivate(cx)); + MOZ_ASSERT(sc); + return sc; +} + +static void TraceGrayRoots(JSTracer* trc, void* data) { + JSRuntime* rt = trc->runtime(); + for (ZonesIter zone(rt, SkipAtoms); !zone.done(); zone.next()) { + for (CompartmentsInZoneIter comp(zone); !comp.done(); comp.next()) { + auto priv = static_cast( + JS_GetCompartmentPrivate(comp.get())); + if (priv) { + TraceNullableEdge(trc, &priv->grayRoot, "test gray root"); + } + } + } +} + +static mozilla::UniqueFreePtr GetLine(FILE* file, const char* prompt) { +#ifdef EDITLINE + /* + * Use readline only if file is stdin, because there's no way to specify + * another handle. Are other filehandles interactive? + */ + if (file == stdin) { + mozilla::UniqueFreePtr linep(readline(prompt)); + /* + * We set it to zero to avoid complaining about inappropriate ioctl + * for device in the case of EOF. Looks like errno == 251 if line is + * finished with EOF and errno == 25 (EINVAL on Mac) if there is + * nothing left to read. + */ + if (errno == 251 || errno == 25 || errno == EINVAL) { + errno = 0; + } + if (!linep) { + return nullptr; + } + if (linep[0] != '\0') { + add_history(linep.get()); + } + return linep; + } +#endif + + size_t len = 0; + if (*prompt != '\0' && gOutFile->isOpen()) { + fprintf(gOutFile->fp, "%s", prompt); + fflush(gOutFile->fp); + } + + size_t size = 80; + mozilla::UniqueFreePtr buffer(static_cast(malloc(size))); + if (!buffer) { + return nullptr; + } + + char* current = buffer.get(); + do { + while (true) { + if (fgets(current, size - len, file)) { + break; + } + if (errno != EINTR) { + return nullptr; + } + } + + len += strlen(current); + char* t = buffer.get() + len - 1; + if (*t == '\n') { + /* Line was read. We remove '\n' and exit. */ + *t = '\0'; + break; + } + + if (len + 1 == size) { + size = size * 2; + char* raw = buffer.release(); + char* tmp = static_cast(realloc(raw, size)); + if (!tmp) { + free(raw); + return nullptr; + } + buffer.reset(tmp); + } + current = buffer.get() + len; + } while (true); + return buffer; +} + +static bool ShellInterruptCallback(JSContext* cx) { + ShellContext* sc = GetShellContext(cx); + if (!sc->serviceInterrupt) { + return true; + } + + // Reset serviceInterrupt. CancelExecution or InterruptIf will set it to + // true to distinguish watchdog or user triggered interrupts. + // Do this first to prevent other interrupts that may occur while the + // user-supplied callback is executing from re-entering the handler. + sc->serviceInterrupt = false; + + bool result; + if (sc->haveInterruptFunc) { + bool wasAlreadyThrowing = cx->isExceptionPending(); + JS::AutoSaveExceptionState savedExc(cx); + JSAutoRealm ar(cx, &sc->interruptFunc.toObject()); + RootedValue rval(cx); + + // Report any exceptions thrown by the JS interrupt callback, but do + // *not* keep it on the cx. The interrupt handler is invoked at points + // that are not expected to throw catchable exceptions, like at + // JSOp::RetRval. + // + // If the interrupted JS code was already throwing, any exceptions + // thrown by the interrupt handler are silently swallowed. + { + Maybe are; + if (!wasAlreadyThrowing) { + are.emplace(cx); + } + result = JS_CallFunctionValue(cx, nullptr, sc->interruptFunc, + JS::HandleValueArray::empty(), &rval); + } + savedExc.restore(); + + if (rval.isBoolean()) { + result = rval.toBoolean(); + } else { + result = false; + } + } else { + result = false; + } + + if (!result && sc->exitCode == 0) { + static const char msg[] = "Script terminated by interrupt handler.\n"; + fputs(msg, stderr); + + sc->exitCode = EXITCODE_TIMEOUT; + } + + return result; +} + +/* + * Some UTF-8 files, notably those written using Notepad, have a Unicode + * Byte-Order-Mark (BOM) as their first character. This is useless (byte-order + * is meaningless for UTF-8) but causes a syntax error unless we skip it. + */ +static void SkipUTF8BOM(FILE* file) { + int ch1 = fgetc(file); + int ch2 = fgetc(file); + int ch3 = fgetc(file); + + // Skip the BOM + if (ch1 == 0xEF && ch2 == 0xBB && ch3 == 0xBF) { + return; + } + + // No BOM - revert + if (ch3 != EOF) { + ungetc(ch3, file); + } + if (ch2 != EOF) { + ungetc(ch2, file); + } + if (ch1 != EOF) { + ungetc(ch1, file); + } +} + +void EnvironmentPreparer::invoke(HandleObject global, Closure& closure) { + MOZ_ASSERT(JS_IsGlobalObject(global)); + + JSContext* cx = TlsContext.get(); + MOZ_ASSERT(!JS_IsExceptionPending(cx)); + + AutoRealm ar(cx, global); + AutoReportException are(cx); + if (!closure(cx)) { + return; + } +} + +JSObject* js::shell::CreateScriptPrivate(JSContext* cx, HandleString path) { + RootedObject info(cx, JS_NewPlainObject(cx)); + if (!info) { + return nullptr; + } + + if (path) { + RootedValue pathValue(cx, StringValue(path)); + if (!JS_DefineProperty(cx, info, "path", pathValue, JSPROP_ENUMERATE)) { + return nullptr; + } + } + + return info; +} + +static bool RegisterScriptPathWithModuleLoader(JSContext* cx, + HandleScript script, + const char* filename) { + // Set the private value associated with a script to a object containing the + // script's filename so that the module loader can use it to resolve + // relative imports. + + RootedString path(cx, JS_NewStringCopyZ(cx, filename)); + if (!path) { + return false; + } + + MOZ_ASSERT(JS::GetScriptPrivate(script).isUndefined()); + RootedObject infoObject(cx, CreateScriptPrivate(cx, path)); + if (!infoObject) { + return false; + } + + JS::SetScriptPrivate(script, ObjectValue(*infoObject)); + return true; +} + +enum class CompileUtf8 { + InflateToUtf16, + DontInflate, +}; + +static MOZ_MUST_USE bool RunFile(JSContext* cx, const char* filename, + FILE* file, CompileUtf8 compileMethod, + bool compileOnly) { + SkipUTF8BOM(file); + + int64_t t1 = PRMJ_Now(); + RootedScript script(cx); + + { + CompileOptions options(cx); + options.setIntroductionType("js shell file") + .setFileAndLine(filename, 1) + .setIsRunOnce(true) + .setNoScriptRval(true); + + if (compileMethod == CompileUtf8::DontInflate) { + script = JS::CompileUtf8File(cx, options, file); + } else { + fprintf(stderr, "(compiling '%s' after inflating to UTF-16)\n", filename); + + FileContents buffer(cx); + if (!ReadCompleteFile(cx, file, buffer)) { + return false; + } + + size_t length = buffer.length(); + auto chars = UniqueTwoByteChars( + UTF8CharsToNewTwoByteCharsZ( + cx, + JS::UTF8Chars(reinterpret_cast(buffer.begin()), + buffer.length()), + &length, js::MallocArena) + .get()); + if (!chars) { + return false; + } + + JS::SourceText source; + if (!source.init(cx, std::move(chars), length)) { + return false; + } + + script = JS::Compile(cx, options, source); + } + + if (!script) { + return false; + } + } + + if (!RegisterScriptPathWithModuleLoader(cx, script, filename)) { + return false; + } + +#ifdef DEBUG + if (dumpEntrainedVariables) { + AnalyzeEntrainedVariables(cx, script); + } +#endif + if (!compileOnly) { + if (!JS_ExecuteScript(cx, script)) { + return false; + } + int64_t t2 = PRMJ_Now() - t1; + if (printTiming) { + printf("runtime = %.3f ms\n", double(t2) / PRMJ_USEC_PER_MSEC); + } + } + return true; +} + +static MOZ_MUST_USE bool RunModule(JSContext* cx, const char* filename, + bool compileOnly) { + ShellContext* sc = GetShellContext(cx); + + RootedString path(cx, JS_NewStringCopyZ(cx, filename)); + if (!path) { + return false; + } + + path = ResolvePath(cx, path, RootRelative); + if (!path) { + return false; + } + + return sc->moduleLoader->loadRootModule(cx, path); +} + +static void ShellCleanupFinalizationRegistryCallback(JSFunction* doCleanup, + JSObject* incumbentGlobal, + void* data) { + // In the browser this queues a task. Shell jobs correspond to microtasks so + // we arrange for cleanup to happen after all jobs/microtasks have run. The + // incumbent global is ignored in the shell. + + auto sc = static_cast(data); + AutoEnterOOMUnsafeRegion oomUnsafe; + if (!sc->finalizationRegistryCleanupCallbacks.append(doCleanup)) { + oomUnsafe.crash("ShellCleanupFinalizationRegistryCallback"); + } +} + +// Run any FinalizationRegistry cleanup tasks and return whether any ran. +static bool MaybeRunFinalizationRegistryCleanupTasks(JSContext* cx) { + ShellContext* sc = GetShellContext(cx); + MOZ_ASSERT(!sc->quitting); + + Rooted callbacks(cx); + std::swap(callbacks.get(), sc->finalizationRegistryCleanupCallbacks.get()); + + bool ranTasks = false; + + RootedFunction callback(cx); + for (JSFunction* f : callbacks) { + callback = f; + + AutoRealm ar(cx, f); + + { + AutoReportException are(cx); + RootedValue unused(cx); + mozilla::Unused << JS_CallFunction(cx, nullptr, callback, + HandleValueArray::empty(), &unused); + } + + ranTasks = true; + + if (sc->quitting) { + break; + } + } + + return ranTasks; +} + +static bool EnqueueJob(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!IsFunctionObject(args.get(0))) { + JS_ReportErrorASCII(cx, "EnqueueJob's first argument must be a function"); + return false; + } + + args.rval().setUndefined(); + + RootedObject job(cx, &args[0].toObject()); + return js::EnqueueJob(cx, job); +} + +static void RunShellJobs(JSContext* cx) { + ShellContext* sc = GetShellContext(cx); + if (sc->quitting) { + return; + } + + while (true) { + // Run microtasks. + js::RunJobs(cx); + if (sc->quitting) { + return; + } + + // Run tasks (only finalization registry clean tasks are possible). + bool ranTasks = MaybeRunFinalizationRegistryCleanupTasks(cx); + if (!ranTasks) { + break; + } + } +} + +static bool DrainJobQueue(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (GetShellContext(cx)->quitting) { + JS_ReportErrorASCII( + cx, "Mustn't drain the job queue when the shell is quitting"); + return false; + } + + RunShellJobs(cx); + + if (GetShellContext(cx)->quitting) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool GlobalOfFirstJobInQueue(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + RootedObject job(cx, cx->internalJobQueue->maybeFront()); + if (!job) { + JS_ReportErrorASCII(cx, "Job queue is empty"); + return false; + } + + RootedObject global(cx, &job->nonCCWGlobal()); + if (!cx->compartment()->wrap(cx, &global)) { + return false; + } + + args.rval().setObject(*global); + return true; +} + +static bool TrackUnhandledRejections(JSContext* cx, JS::HandleObject promise, + JS::PromiseRejectionHandlingState state) { + ShellContext* sc = GetShellContext(cx); + if (!sc->trackUnhandledRejections) { + return true; + } + +#if defined(DEBUG) || defined(JS_OOM_BREAKPOINT) + if (cx->runningOOMTest) { + // When OOM happens, we cannot reliably track the set of unhandled + // promise rejections. Throw error only when simulated OOM is used + // *and* promises are used in the test. + JS_ReportErrorASCII( + cx, + "Can't track unhandled rejections while running simulated OOM " + "test. Call ignoreUnhandledRejections before using oomTest etc."); + return false; + } +#endif + + if (!sc->unhandledRejectedPromises) { + sc->unhandledRejectedPromises = SetObject::create(cx); + if (!sc->unhandledRejectedPromises) { + return false; + } + } + + RootedValue promiseVal(cx, ObjectValue(*promise)); + + AutoRealm ar(cx, sc->unhandledRejectedPromises); + if (!cx->compartment()->wrap(cx, &promiseVal)) { + return false; + } + + switch (state) { + case JS::PromiseRejectionHandlingState::Unhandled: + if (!SetObject::add(cx, sc->unhandledRejectedPromises, promiseVal)) { + return false; + } + break; + case JS::PromiseRejectionHandlingState::Handled: + bool deleted = false; + if (!SetObject::delete_(cx, sc->unhandledRejectedPromises, promiseVal, + &deleted)) { + return false; + } + // We can't MOZ_ASSERT(deleted) here, because it's possible we failed to + // add the promise in the first place, due to OOM. + break; + } + + return true; +} + +static void ForwardingPromiseRejectionTrackerCallback( + JSContext* cx, bool mutedErrors, JS::HandleObject promise, + JS::PromiseRejectionHandlingState state, void* data) { + AutoReportException are(cx); + + if (!TrackUnhandledRejections(cx, promise, state)) { + return; + } + + RootedValue callback(cx, + GetShellContext(cx)->promiseRejectionTrackerCallback); + if (callback.isNull()) { + return; + } + + AutoRealm ar(cx, &callback.toObject()); + + FixedInvokeArgs<2> args(cx); + args[0].setObject(*promise); + args[1].setInt32(static_cast(state)); + + if (!JS_WrapValue(cx, args[0])) { + return; + } + + RootedValue rval(cx); + (void)Call(cx, callback, UndefinedHandleValue, args, &rval); +} + +static bool SetPromiseRejectionTrackerCallback(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!IsFunctionObject(args.get(0))) { + JS_ReportErrorASCII( + cx, + "setPromiseRejectionTrackerCallback expects a function as its sole " + "argument"); + return false; + } + + GetShellContext(cx)->promiseRejectionTrackerCallback = args[0]; + + args.rval().setUndefined(); + return true; +} + +// clang-format off +#define LIT(NAME) #NAME, +static const char* telemetryNames[JS_TELEMETRY_END + 1] = { + MAP_JS_TELEMETRY(LIT) + "JS_TELEMETRY_END" +}; +#undef LIT +// clang-format on + +// Telemetry can be executed from multiple threads, and the callback is +// responsible to avoid contention on the recorded telemetry data. +static Mutex* telemetryLock = nullptr; +class MOZ_RAII AutoLockTelemetry : public LockGuard { + using Base = LockGuard; + + public: + AutoLockTelemetry() : Base(*telemetryLock) { MOZ_ASSERT(telemetryLock); } +}; + +using TelemetryData = uint32_t; +using TelemetryVec = Vector; +static mozilla::Array telemetryResults; +static void AccumulateTelemetryDataCallback(int id, uint32_t sample, + const char* key) { + AutoLockTelemetry alt; + // We ignore OOMs while writting teleemtry data. + if (telemetryResults[id].append(sample)) { + return; + } +} + +static void WriteTelemetryDataToDisk(const char* dir) { + const int pathLen = 260; + char fileName[pathLen]; + Fprinter output; + auto initOutput = [&](const char* name) -> bool { + if (SprintfLiteral(fileName, "%s%s.csv", dir, name) >= pathLen) { + return false; + } + FILE* file = fopen(fileName, "a"); + if (!file) { + return false; + } + output.init(file); + return true; + }; + + for (size_t id = 0; id < JS_TELEMETRY_END; id++) { + auto clear = MakeScopeExit([&] { telemetryResults[id].clearAndFree(); }); + if (!initOutput(telemetryNames[id])) { + continue; + } + for (uint32_t data : telemetryResults[id]) { + output.printf("%u\n", data); + } + output.finish(); + } +} + +#undef MAP_TELEMETRY + +static bool BoundToAsyncStack(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + RootedValue function(cx, GetFunctionNativeReserved(&args.callee(), 0)); + RootedObject options( + cx, &GetFunctionNativeReserved(&args.callee(), 1).toObject()); + + RootedSavedFrame stack(cx, nullptr); + bool isExplicit; + + RootedValue v(cx); + + if (!JS_GetProperty(cx, options, "stack", &v)) { + return false; + } + if (!v.isObject() || !v.toObject().is()) { + JS_ReportErrorASCII(cx, + "The 'stack' property must be a SavedFrame object."); + return false; + } + stack = &v.toObject().as(); + + if (!JS_GetProperty(cx, options, "cause", &v)) { + return false; + } + RootedString causeString(cx, ToString(cx, v)); + if (!causeString) { + MOZ_ASSERT(cx->isExceptionPending()); + return false; + } + + UniqueChars cause = JS_EncodeStringToUTF8(cx, causeString); + if (!cause) { + MOZ_ASSERT(cx->isExceptionPending()); + return false; + } + + if (!JS_GetProperty(cx, options, "explicit", &v)) { + return false; + } + isExplicit = v.isUndefined() ? true : ToBoolean(v); + + auto kind = + (isExplicit ? JS::AutoSetAsyncStackForNewCalls::AsyncCallKind::EXPLICIT + : JS::AutoSetAsyncStackForNewCalls::AsyncCallKind::IMPLICIT); + + JS::AutoSetAsyncStackForNewCalls asasfnckthxbye(cx, stack, cause.get(), kind); + return Call(cx, UndefinedHandleValue, function, JS::HandleValueArray::empty(), + args.rval()); +} + +static bool BindToAsyncStack(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 2) { + JS_ReportErrorASCII(cx, "bindToAsyncStack takes exactly two arguments."); + return false; + } + + if (!args[0].isObject() || !IsCallable(args[0])) { + JS_ReportErrorASCII( + cx, "bindToAsyncStack's first argument should be a function."); + return false; + } + + if (!args[1].isObject()) { + JS_ReportErrorASCII( + cx, "bindToAsyncStack's second argument should be an object."); + return false; + } + + RootedFunction bound(cx, NewFunctionWithReserved(cx, BoundToAsyncStack, 0, 0, + "bindToAsyncStack thunk")); + if (!bound) { + return false; + } + SetFunctionNativeReserved(bound, 0, args[0]); + SetFunctionNativeReserved(bound, 1, args[1]); + + args.rval().setObject(*bound); + return true; +} + +#ifdef JS_HAS_INTL_API +static bool AddIntlExtras(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.get(0).isObject()) { + JS_ReportErrorASCII(cx, "addIntlExtras must be passed an object"); + return false; + } + JS::RootedObject intl(cx, &args[0].toObject()); + + static const JSFunctionSpec funcs[] = { + JS_SELF_HOSTED_FN("getCalendarInfo", "Intl_getCalendarInfo", 1, 0), + JS_SELF_HOSTED_FN("getLocaleInfo", "Intl_getLocaleInfo", 1, 0), + JS_SELF_HOSTED_FN("getDisplayNames", "Intl_getDisplayNames", 2, 0), + JS_FS_END}; + + if (!JS_DefineFunctions(cx, intl, funcs)) { + return false; + } + + if (!JS::AddMozDateTimeFormatConstructor(cx, intl)) { + return false; + } + + if (!JS::AddMozDisplayNamesConstructor(cx, intl)) { + return false; + } + + args.rval().setUndefined(); + return true; +} +#endif // JS_HAS_INTL_API + +static MOZ_MUST_USE bool EvalUtf8AndPrint(JSContext* cx, const char* bytes, + size_t length, int lineno, + bool compileOnly) { + // Eval. + JS::CompileOptions options(cx); + options.setIntroductionType("js shell interactive") + .setIsRunOnce(true) + .setFileAndLine("typein", lineno); + + JS::SourceText srcBuf; + if (!srcBuf.init(cx, bytes, length, JS::SourceOwnership::Borrowed)) { + return false; + } + + RootedScript script(cx, JS::Compile(cx, options, srcBuf)); + if (!script) { + return false; + } + if (compileOnly) { + return true; + } + RootedValue result(cx); + if (!JS_ExecuteScript(cx, script, &result)) { + return false; + } + + if (!result.isUndefined() && gOutFile->isOpen()) { + // Print. + RootedString str(cx, JS_ValueToSource(cx, result)); + if (!str) { + return false; + } + + UniqueChars utf8chars = JS_EncodeStringToUTF8(cx, str); + if (!utf8chars) { + return false; + } + fprintf(gOutFile->fp, "%s\n", utf8chars.get()); + } + return true; +} + +static MOZ_MUST_USE bool ReadEvalPrintLoop(JSContext* cx, FILE* in, + bool compileOnly) { + ShellContext* sc = GetShellContext(cx); + int lineno = 1; + bool hitEOF = false; + + do { + /* + * Accumulate lines until we get a 'compilable unit' - one that either + * generates an error (before running out of source) or that compiles + * cleanly. This should be whenever we get a complete statement that + * coincides with the end of a line. + */ + int startline = lineno; + typedef Vector CharBuffer; + RootedObject globalLexical(cx, &cx->global()->lexicalEnvironment()); + CharBuffer buffer(cx); + do { + ScheduleWatchdog(cx, -1); + sc->serviceInterrupt = false; + errno = 0; + + mozilla::UniqueFreePtr line = + GetLine(in, startline == lineno ? "js> " : ""); + if (!line) { + if (errno) { + /* + * Use Latin1 variant here because strerror(errno)'s + * encoding depends on the user's C locale. + */ + JS_ReportErrorLatin1(cx, "%s", strerror(errno)); + return false; + } + hitEOF = true; + break; + } + + if (!buffer.append(line.get(), strlen(line.get())) || + !buffer.append('\n')) { + return false; + } + + lineno++; + if (!ScheduleWatchdog(cx, sc->timeoutInterval)) { + hitEOF = true; + break; + } + } while (!JS_Utf8BufferIsCompilableUnit(cx, cx->global(), buffer.begin(), + buffer.length())); + + if (hitEOF && buffer.empty()) { + break; + } + + { + // Report exceptions but keep going. + AutoReportException are(cx); + mozilla::Unused << EvalUtf8AndPrint(cx, buffer.begin(), buffer.length(), + startline, compileOnly); + } + + // If a let or const fail to initialize they will remain in an unusable + // without further intervention. This call cleans up the global scope, + // setting uninitialized lexicals to undefined so that they may still + // be used. This behavior is _only_ acceptable in the context of the repl. + if (JS::ForceLexicalInitialization(cx, globalLexical) && + gErrFile->isOpen()) { + fputs( + "Warning: According to the standard, after the above exception,\n" + "Warning: the global bindings should be permanently uninitialized.\n" + "Warning: We have non-standard-ly initialized them to `undefined`" + "for you.\nWarning: This nicety only happens in the JS shell.\n", + stderr); + } + + RunShellJobs(cx); + } while (!hitEOF && !sc->quitting); + + if (gOutFile->isOpen()) { + fprintf(gOutFile->fp, "\n"); + } + + return true; +} + +enum FileKind { + FileScript, // UTF-8, directly parsed as such + FileScriptUtf16, // FileScript, but inflate to UTF-16 before parsing + FileModule, +}; + +static void ReportCantOpenErrorUnknownEncoding(JSContext* cx, + const char* filename) { + /* + * Filenames are in some random system encoding. *Probably* it's UTF-8, + * but no guarantees. + * + * strerror(errno)'s encoding, in contrast, depends on the user's C locale. + * + * Latin-1 is possibly wrong for both of these -- but at least if it's + * wrong it'll produce mojibake *safely*. Run with Latin-1 til someone + * complains. + */ + JS_ReportErrorNumberLatin1(cx, my_GetErrorMessage, nullptr, JSSMSG_CANT_OPEN, + filename, strerror(errno)); +} + +static MOZ_MUST_USE bool Process(JSContext* cx, const char* filename, + bool forceTTY, FileKind kind) { + FILE* file; + if (forceTTY || !filename || strcmp(filename, "-") == 0) { + file = stdin; + } else { + file = fopen(filename, "rb"); + if (!file) { + ReportCantOpenErrorUnknownEncoding(cx, filename); + return false; + } + } + AutoCloseFile autoClose(file); + + if (!forceTTY && !isatty(fileno(file))) { + // It's not interactive - just execute it. + switch (kind) { + case FileScript: + if (!RunFile(cx, filename, file, CompileUtf8::DontInflate, + compileOnly)) { + return false; + } + break; + case FileScriptUtf16: + if (!RunFile(cx, filename, file, CompileUtf8::InflateToUtf16, + compileOnly)) { + return false; + } + break; + case FileModule: + if (!RunModule(cx, filename, compileOnly)) { + return false; + } + break; + default: + MOZ_CRASH("Impossible FileKind!"); + } + } else { + // It's an interactive filehandle; drop into read-eval-print loop. + MOZ_ASSERT(kind == FileScript); + if (!ReadEvalPrintLoop(cx, file, compileOnly)) { + return false; + } + } + return true; +} + +#ifdef XP_WIN +# define GET_FD_FROM_FILE(a) int(_get_osfhandle(fileno(a))) +#else +# define GET_FD_FROM_FILE(a) fileno(a) +#endif + +static bool CreateMappedArrayBuffer(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() < 1 || args.length() > 3) { + JS_ReportErrorNumberASCII( + cx, my_GetErrorMessage, nullptr, + args.length() < 1 ? JSSMSG_NOT_ENOUGH_ARGS : JSSMSG_TOO_MANY_ARGS, + "createMappedArrayBuffer"); + return false; + } + + RootedString rawFilenameStr(cx, JS::ToString(cx, args[0])); + if (!rawFilenameStr) { + return false; + } + // It's a little bizarre to resolve relative to the script, but for testing + // I need a file at a known location, and the only good way I know of to do + // that right now is to include it in the repo alongside the test script. + // Bug 944164 would introduce an alternative. + JSString* filenameStr = ResolvePath(cx, rawFilenameStr, ScriptRelative); + if (!filenameStr) { + return false; + } + UniqueChars filename = JS_EncodeStringToLatin1(cx, filenameStr); + if (!filename) { + return false; + } + + uint32_t offset = 0; + if (args.length() >= 2) { + if (!JS::ToUint32(cx, args[1], &offset)) { + return false; + } + } + + bool sizeGiven = false; + uint32_t size; + if (args.length() >= 3) { + if (!JS::ToUint32(cx, args[2], &size)) { + return false; + } + sizeGiven = true; + if (size == 0) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_ARRAY_LENGTH); + return false; + } + } + + FILE* file = fopen(filename.get(), "rb"); + if (!file) { + ReportCantOpenErrorUnknownEncoding(cx, filename.get()); + return false; + } + AutoCloseFile autoClose(file); + + struct stat st; + if (fstat(fileno(file), &st) < 0) { + JS_ReportErrorASCII(cx, "Unable to stat file"); + return false; + } + + if ((st.st_mode & S_IFMT) != S_IFREG) { + JS_ReportErrorASCII(cx, "Path is not a regular file"); + return false; + } + + if (!sizeGiven) { + if (off_t(offset) >= st.st_size) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_OFFSET_LARGER_THAN_FILESIZE); + return false; + } + size = st.st_size - offset; + } + + void* contents = + JS::CreateMappedArrayBufferContents(GET_FD_FROM_FILE(file), offset, size); + if (!contents) { + JS_ReportErrorASCII(cx, + "failed to allocate mapped array buffer contents " + "(possibly due to bad alignment)"); + return false; + } + + RootedObject obj(cx, + JS::NewMappedArrayBufferWithContents(cx, size, contents)); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +#undef GET_FD_FROM_FILE + +static bool AddPromiseReactions(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 3) { + JS_ReportErrorNumberASCII( + cx, my_GetErrorMessage, nullptr, + args.length() < 3 ? JSSMSG_NOT_ENOUGH_ARGS : JSSMSG_TOO_MANY_ARGS, + "addPromiseReactions"); + return false; + } + + RootedObject promise(cx); + if (args[0].isObject()) { + promise = &args[0].toObject(); + } + + if (!promise || !JS::IsPromiseObject(promise)) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "addPromiseReactions"); + return false; + } + + RootedObject onResolve(cx); + if (args[1].isObject()) { + onResolve = &args[1].toObject(); + } + + RootedObject onReject(cx); + if (args[2].isObject()) { + onReject = &args[2].toObject(); + } + + if (!onResolve || !onResolve->is() || !onReject || + !onReject->is()) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "addPromiseReactions"); + return false; + } + + return JS::AddPromiseReactions(cx, promise, onResolve, onReject); +} + +static bool IgnoreUnhandledRejections(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + ShellContext* sc = GetShellContext(cx); + sc->trackUnhandledRejections = false; + + args.rval().setUndefined(); + return true; +} + +static bool Options(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + JS::ContextOptions oldContextOptions = JS::ContextOptionsRef(cx); + for (unsigned i = 0; i < args.length(); i++) { + RootedString str(cx, JS::ToString(cx, args[i])); + if (!str) { + return false; + } + + RootedLinearString opt(cx, str->ensureLinear(cx)); + if (!opt) { + return false; + } + + if (StringEqualsLiteral(opt, "throw_on_asmjs_validation_failure")) { + JS::ContextOptionsRef(cx).toggleThrowOnAsmJSValidationFailure(); + } else if (StringEqualsLiteral(opt, "strict_mode")) { + JS::ContextOptionsRef(cx).toggleStrictMode(); + } else { + UniqueChars optChars = QuoteString(cx, opt, '"'); + if (!optChars) { + return false; + } + + JS_ReportErrorASCII(cx, + "unknown option name %s." + " The valid names are " + "throw_on_asmjs_validation_failure and strict_mode.", + optChars.get()); + return false; + } + } + + UniqueChars names = DuplicateString(""); + bool found = false; + if (names && oldContextOptions.throwOnAsmJSValidationFailure()) { + names = JS_sprintf_append(std::move(names), "%s%s", found ? "," : "", + "throw_on_asmjs_validation_failure"); + found = true; + } + if (names && oldContextOptions.strictMode()) { + names = JS_sprintf_append(std::move(names), "%s%s", found ? "," : "", + "strict_mode"); + found = true; + } + if (!names) { + JS_ReportOutOfMemory(cx); + return false; + } + + JSString* str = JS_NewStringCopyZ(cx, names.get()); + if (!str) { + return false; + } + args.rval().setString(str); + return true; +} + +static bool LoadScript(JSContext* cx, unsigned argc, Value* vp, + bool scriptRelative) { + CallArgs args = CallArgsFromVp(argc, vp); + + RootedString str(cx); + for (unsigned i = 0; i < args.length(); i++) { + str = JS::ToString(cx, args[i]); + if (!str) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "load"); + return false; + } + + str = ResolvePath(cx, str, scriptRelative ? ScriptRelative : RootRelative); + if (!str) { + JS_ReportErrorASCII(cx, "unable to resolve path"); + return false; + } + + UniqueChars filename = JS_EncodeStringToLatin1(cx, str); + if (!filename) { + return false; + } + + errno = 0; + + CompileOptions opts(cx); + opts.setIntroductionType("js shell load") + .setIsRunOnce(true) + .setNoScriptRval(true); + + RootedValue unused(cx); + if (!(compileOnly + ? JS::CompileUtf8Path(cx, opts, filename.get()) != nullptr + : JS::EvaluateUtf8Path(cx, opts, filename.get(), &unused))) { + return false; + } + } + + args.rval().setUndefined(); + return true; +} + +static bool Load(JSContext* cx, unsigned argc, Value* vp) { + return LoadScript(cx, argc, vp, false); +} + +static bool LoadScriptRelativeToScript(JSContext* cx, unsigned argc, + Value* vp) { + return LoadScript(cx, argc, vp, true); +} + +// Populate |options| with the options given by |opts|'s properties. If we +// need to convert a filename to a C string, let fileNameBytes own the +// bytes. +static bool ParseCompileOptions(JSContext* cx, CompileOptions& options, + HandleObject opts, UniqueChars& fileNameBytes) { + RootedValue v(cx); + RootedString s(cx); + + if (!JS_GetProperty(cx, opts, "isRunOnce", &v)) { + return false; + } + if (!v.isUndefined()) { + options.setIsRunOnce(ToBoolean(v)); + } + + if (!JS_GetProperty(cx, opts, "noScriptRval", &v)) { + return false; + } + if (!v.isUndefined()) { + options.setNoScriptRval(ToBoolean(v)); + } + + if (!JS_GetProperty(cx, opts, "fileName", &v)) { + return false; + } + if (v.isNull()) { + options.setFile(nullptr); + } else if (!v.isUndefined()) { + s = ToString(cx, v); + if (!s) { + return false; + } + fileNameBytes = JS_EncodeStringToLatin1(cx, s); + if (!fileNameBytes) { + return false; + } + options.setFile(fileNameBytes.get()); + } + + if (!JS_GetProperty(cx, opts, "skipFileNameValidation", &v)) { + return false; + } + if (!v.isUndefined()) { + options.setSkipFilenameValidation(ToBoolean(v)); + } + + if (!JS_GetProperty(cx, opts, "element", &v)) { + return false; + } + if (v.isObject()) { + RootedObject infoObject(cx, CreateScriptPrivate(cx)); + if (!infoObject) { + return false; + } + RootedValue elementValue(cx, v); + if (!JS_WrapValue(cx, &elementValue)) { + return false; + } + if (!JS_DefineProperty(cx, infoObject, "element", elementValue, 0)) { + return false; + } + options.setPrivateValue(ObjectValue(*infoObject)); + } + + if (!JS_GetProperty(cx, opts, "elementAttributeName", &v)) { + return false; + } + if (!v.isUndefined()) { + s = ToString(cx, v); + if (!s) { + return false; + } + options.setElementAttributeName(s); + } + + if (!JS_GetProperty(cx, opts, "lineNumber", &v)) { + return false; + } + if (!v.isUndefined()) { + uint32_t u; + if (!ToUint32(cx, v, &u)) { + return false; + } + options.setLine(u); + } + + if (!JS_GetProperty(cx, opts, "columnNumber", &v)) { + return false; + } + if (!v.isUndefined()) { + int32_t c; + if (!ToInt32(cx, v, &c)) { + return false; + } + options.setColumn(c); + } + + if (!JS_GetProperty(cx, opts, "sourceIsLazy", &v)) { + return false; + } + if (v.isBoolean()) { + options.setSourceIsLazy(v.toBoolean()); + } + + if (!JS_GetProperty(cx, opts, "forceFullParse", &v)) { + return false; + } + if (v.isBoolean() && v.toBoolean()) { + options.setForceFullParse(); + } + + return true; +} + +static void my_LargeAllocFailCallback() { + JSContext* cx = TlsContext.get(); + if (!cx || cx->isHelperThreadContext()) { + return; + } + + MOZ_ASSERT(!JS::RuntimeHeapIsBusy()); + + JS::PrepareForFullGC(cx); + cx->runtime()->gc.gc(GC_SHRINK, JS::GCReason::SHARED_MEMORY_LIMIT); +} + +static const uint32_t CacheEntry_SOURCE = 0; +static const uint32_t CacheEntry_BYTECODE = 1; +static const uint32_t CacheEntry_KIND = 2; + +enum class BytecodeCacheKind : uint32_t { + Undefined = 0, + Script, + Stencil, +}; + +static const JSClass CacheEntry_class = {"CacheEntryObject", + JSCLASS_HAS_RESERVED_SLOTS(3)}; + +static bool CacheEntry(JSContext* cx, unsigned argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1 || !args[0].isString()) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "CacheEntry"); + return false; + } + + RootedObject obj(cx, JS_NewObject(cx, &CacheEntry_class)); + if (!obj) { + return false; + } + + JS::SetReservedSlot(obj, CacheEntry_SOURCE, args[0]); + JS::SetReservedSlot(obj, CacheEntry_BYTECODE, UndefinedValue()); + JS::SetReservedSlot(obj, CacheEntry_KIND, + Int32Value(int32_t(BytecodeCacheKind::Undefined))); + args.rval().setObject(*obj); + return true; +} + +static bool CacheEntry_isCacheEntry(JSObject* cache) { + return cache->hasClass(&CacheEntry_class); +} + +static JSString* CacheEntry_getSource(JSContext* cx, HandleObject cache) { + MOZ_ASSERT(CacheEntry_isCacheEntry(cache)); + Value v = JS::GetReservedSlot(cache, CacheEntry_SOURCE); + if (!v.isString()) { + JS_ReportErrorASCII( + cx, "CacheEntry_getSource: Unexpected type of source reserved slot."); + return nullptr; + } + + return v.toString(); +} + +static BytecodeCacheKind CacheEntry_getKind(JSContext* cx, HandleObject cache) { + MOZ_ASSERT(CacheEntry_isCacheEntry(cache)); + Value v = JS::GetReservedSlot(cache, CacheEntry_KIND); + return BytecodeCacheKind(v.toInt32()); +} + +static void CacheEntry_setKind(JSContext* cx, HandleObject cache, + BytecodeCacheKind kind) { + MOZ_ASSERT(CacheEntry_isCacheEntry(cache)); + JS::SetReservedSlot(cache, CacheEntry_KIND, Int32Value(int32_t(kind))); +} + +static uint8_t* CacheEntry_getBytecode(JSContext* cx, HandleObject cache, + size_t* length) { + MOZ_ASSERT(CacheEntry_isCacheEntry(cache)); + Value v = JS::GetReservedSlot(cache, CacheEntry_BYTECODE); + if (!v.isObject() || !v.toObject().is()) { + JS_ReportErrorASCII( + cx, + "CacheEntry_getBytecode: Unexpected type of bytecode reserved slot."); + return nullptr; + } + + ArrayBufferObject* arrayBuffer = &v.toObject().as(); + *length = arrayBuffer->byteLength().get(); + return arrayBuffer->dataPointer(); +} + +static bool CacheEntry_setBytecode(JSContext* cx, HandleObject cache, + uint8_t* buffer, uint32_t length) { + MOZ_ASSERT(CacheEntry_isCacheEntry(cache)); + + using BufferContents = ArrayBufferObject::BufferContents; + + BufferContents contents = BufferContents::createMalloced(buffer); + Rooted arrayBuffer( + cx, + ArrayBufferObject::createForContents(cx, BufferSize(length), contents)); + if (!arrayBuffer) { + return false; + } + + JS::SetReservedSlot(cache, CacheEntry_BYTECODE, ObjectValue(*arrayBuffer)); + return true; +} + +static bool ConvertTranscodeResultToJSException(JSContext* cx, + JS::TranscodeResult rv) { + switch (rv) { + case JS::TranscodeResult_Ok: + return true; + + default: + [[fallthrough]]; + case JS::TranscodeResult_Failure: + MOZ_ASSERT(!cx->isExceptionPending()); + JS_ReportErrorASCII(cx, "generic warning"); + return false; + case JS::TranscodeResult_Failure_BadBuildId: + MOZ_ASSERT(!cx->isExceptionPending()); + JS_ReportErrorASCII(cx, "the build-id does not match"); + return false; + case JS::TranscodeResult_Failure_RunOnceNotSupported: + MOZ_ASSERT(!cx->isExceptionPending()); + JS_ReportErrorASCII(cx, "run-once script are not supported by XDR"); + return false; + case JS::TranscodeResult_Failure_AsmJSNotSupported: + MOZ_ASSERT(!cx->isExceptionPending()); + JS_ReportErrorASCII(cx, "Asm.js is not supported by XDR"); + return false; + case JS::TranscodeResult_Failure_BadDecode: + MOZ_ASSERT(!cx->isExceptionPending()); + JS_ReportErrorASCII(cx, "XDR data corruption"); + return false; + case JS::TranscodeResult_Failure_WrongCompileOption: + MOZ_ASSERT(!cx->isExceptionPending()); + JS_ReportErrorASCII( + cx, "Compile options differs from Compile options of the encoding"); + return false; + case JS::TranscodeResult_Failure_NotInterpretedFun: + MOZ_ASSERT(!cx->isExceptionPending()); + JS_ReportErrorASCII(cx, + "Only interepreted functions are supported by XDR"); + return false; + + case JS::TranscodeResult_Throw: + MOZ_ASSERT(cx->isExceptionPending()); + return false; + } +} + +static bool Evaluate(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() < 1 || args.length() > 2) { + JS_ReportErrorNumberASCII( + cx, my_GetErrorMessage, nullptr, + args.length() < 1 ? JSSMSG_NOT_ENOUGH_ARGS : JSSMSG_TOO_MANY_ARGS, + "evaluate"); + return false; + } + + RootedString code(cx, nullptr); + RootedObject cacheEntry(cx, nullptr); + if (args[0].isString()) { + code = args[0].toString(); + } else if (args[0].isObject() && + CacheEntry_isCacheEntry(&args[0].toObject())) { + cacheEntry = &args[0].toObject(); + code = CacheEntry_getSource(cx, cacheEntry); + if (!code) { + return false; + } + } + + if (!code || (args.length() == 2 && args[1].isPrimitive())) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "evaluate"); + return false; + } + + CompileOptions options(cx); + UniqueChars fileNameBytes; + RootedString displayURL(cx); + RootedString sourceMapURL(cx); + RootedObject global(cx, nullptr); + bool catchTermination = false; + bool loadBytecode = false; + bool saveBytecode = false; + bool saveIncrementalBytecode = false; + bool assertEqBytecode = false; + JS::RootedObjectVector envChain(cx); + RootedObject callerGlobal(cx, cx->global()); + + options.setIntroductionType("js shell evaluate") + .setFileAndLine("@evaluate", 1); + + global = JS::CurrentGlobalOrNull(cx); + MOZ_ASSERT(global); + + if (args.length() == 2) { + RootedObject opts(cx, &args[1].toObject()); + RootedValue v(cx); + + if (!ParseCompileOptions(cx, options, opts, fileNameBytes)) { + return false; + } + + if (!JS_GetProperty(cx, opts, "displayURL", &v)) { + return false; + } + if (!v.isUndefined()) { + displayURL = ToString(cx, v); + if (!displayURL) { + return false; + } + } + + if (!JS_GetProperty(cx, opts, "sourceMapURL", &v)) { + return false; + } + if (!v.isUndefined()) { + sourceMapURL = ToString(cx, v); + if (!sourceMapURL) { + return false; + } + } + + if (!JS_GetProperty(cx, opts, "global", &v)) { + return false; + } + if (!v.isUndefined()) { + if (v.isObject()) { + global = js::CheckedUnwrapDynamic(&v.toObject(), cx, + /* stopAtWindowProxy = */ false); + if (!global) { + return false; + } + } + if (!global || !(JS::GetClass(global)->flags & JSCLASS_IS_GLOBAL)) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "\"global\" passed to evaluate()", "not a global object"); + return false; + } + } + + if (!JS_GetProperty(cx, opts, "catchTermination", &v)) { + return false; + } + if (!v.isUndefined()) { + catchTermination = ToBoolean(v); + } + + if (!JS_GetProperty(cx, opts, "loadBytecode", &v)) { + return false; + } + if (!v.isUndefined()) { + loadBytecode = ToBoolean(v); + } + + if (!JS_GetProperty(cx, opts, "saveBytecode", &v)) { + return false; + } + if (!v.isUndefined()) { + saveBytecode = ToBoolean(v); + } + + if (!JS_GetProperty(cx, opts, "saveIncrementalBytecode", &v)) { + return false; + } + if (!v.isUndefined()) { + saveIncrementalBytecode = ToBoolean(v); + } + + if (!JS_GetProperty(cx, opts, "assertEqBytecode", &v)) { + return false; + } + if (!v.isUndefined()) { + assertEqBytecode = ToBoolean(v); + } + + if (!JS_GetProperty(cx, opts, "envChainObject", &v)) { + return false; + } + if (!v.isUndefined()) { + if (loadBytecode) { + JS_ReportErrorASCII(cx, + "Can't use both loadBytecode and envChainObject"); + return false; + } + + if (!v.isObject()) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "\"envChainObject\" passed to evaluate()", "not an object"); + return false; + } else if (v.toObject().is()) { + JS_ReportErrorASCII( + cx, + "\"envChainObject\" passed to evaluate() should not be a global"); + return false; + } else if (!envChain.append(&v.toObject())) { + JS_ReportOutOfMemory(cx); + return false; + } + } + + // We cannot load or save the bytecode if we have no object where the + // bytecode cache is stored. + if (loadBytecode || saveBytecode || saveIncrementalBytecode) { + if (!cacheEntry) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "evaluate"); + return false; + } + if (saveIncrementalBytecode && saveBytecode) { + JS_ReportErrorASCII( + cx, + "saveIncrementalBytecode and saveBytecode cannot be used" + " at the same time."); + return false; + } + } + } + + AutoStableStringChars codeChars(cx); + if (!codeChars.initTwoByte(cx, code)) { + return false; + } + + JS::TranscodeBuffer loadBuffer; + JS::TranscodeBuffer saveBuffer; + BytecodeCacheKind loadCacheKind = BytecodeCacheKind::Undefined; + BytecodeCacheKind saveCacheKind = BytecodeCacheKind::Undefined; + + if (loadBytecode) { + size_t loadLength = 0; + uint8_t* loadData = nullptr; + loadData = CacheEntry_getBytecode(cx, cacheEntry, &loadLength); + if (!loadData) { + return false; + } + if (!loadBuffer.append(loadData, loadLength)) { + JS_ReportOutOfMemory(cx); + return false; + } + loadCacheKind = CacheEntry_getKind(cx, cacheEntry); + } + + if (envChain.length() != 0) { + options.setNonSyntacticScope(true); + } + + { + JSAutoRealm ar(cx, global); + RootedScript script(cx); + + { + if (loadBytecode) { + JS::TranscodeResult rv; + if (saveIncrementalBytecode) { + bool useStencilXDR = !options.useOffThreadParseGlobal; + if (useStencilXDR) { + if (CacheEntry_getKind(cx, cacheEntry) != + BytecodeCacheKind::Stencil) { + // This can happen. + JS_ReportErrorASCII( + cx, + "if both loadBytecode and saveIncrementalBytecode are set " + "and --off-thread-parse-global isn't used, bytecode should " + "have been saved with saveIncrementalBytecode"); + return false; + } + } else { + if (CacheEntry_getKind(cx, cacheEntry) != + BytecodeCacheKind::Script) { + // NOTE: This shouldn't happen unless the cache is used across + // processes with different --no-off-thread-parse-global option. + JS_ReportErrorASCII( + cx, + "if both loadBytecode and saveIncrementalBytecode are set " + "and --off-thread-parse-global is used, bytecode " + "should have been saved with saveBytecode"); + return false; + } + } + + rv = JS::DecodeScriptAndStartIncrementalEncoding(cx, options, + loadBuffer, &script); + if (!ConvertTranscodeResultToJSException(cx, rv)) { + return false; + } + } else if (loadCacheKind == BytecodeCacheKind::Script) { + rv = JS::DecodeScript(cx, options, loadBuffer, &script); + if (!ConvertTranscodeResultToJSException(cx, rv)) { + return false; + } + } else { + MOZ_ASSERT(loadCacheKind == BytecodeCacheKind::Stencil); + rv = JS::DecodeScriptMaybeStencil(cx, options, loadBuffer, &script); + if (!ConvertTranscodeResultToJSException(cx, rv)) { + return false; + } + } + } else { + mozilla::Range chars = codeChars.twoByteRange(); + JS::SourceText srcBuf; + if (!srcBuf.init(cx, chars.begin().get(), chars.length(), + JS::SourceOwnership::Borrowed)) { + return false; + } + + if (saveIncrementalBytecode) { + script = JS::CompileAndStartIncrementalEncoding(cx, options, srcBuf); + if (!script) { + return false; + } + } else { + script = JS::Compile(cx, options, srcBuf); + if (!script) { + return false; + } + } + } + } + + if (displayURL && !script->scriptSource()->hasDisplayURL()) { + UniqueTwoByteChars chars = JS_CopyStringCharsZ(cx, displayURL); + if (!chars) { + return false; + } + if (!script->scriptSource()->setDisplayURL(cx, std::move(chars))) { + return false; + } + } + if (sourceMapURL && !script->scriptSource()->hasSourceMapURL()) { + UniqueTwoByteChars chars = JS_CopyStringCharsZ(cx, sourceMapURL); + if (!chars) { + return false; + } + if (!script->scriptSource()->setSourceMapURL(cx, std::move(chars))) { + return false; + } + } + + if (!JS_ExecuteScript(cx, envChain, script, args.rval())) { + if (catchTermination && !JS_IsExceptionPending(cx)) { + JSAutoRealm ar1(cx, callerGlobal); + JSString* str = JS_NewStringCopyZ(cx, "terminated"); + if (!str) { + return false; + } + args.rval().setString(str); + return true; + } + return false; + } + + // Encode the bytecode after the execution of the script. + if (saveBytecode) { + JS::TranscodeResult rv = JS::EncodeScript(cx, saveBuffer, script); + if (!ConvertTranscodeResultToJSException(cx, rv)) { + return false; + } + saveCacheKind = BytecodeCacheKind::Script; + } + + // Serialize the encoded bytecode, recorded before the execution, into a + // buffer which can be deserialized linearly. + if (saveIncrementalBytecode) { + if (!FinishIncrementalEncoding(cx, script, saveBuffer)) { + return false; + } + if (options.useStencilXDR) { + saveCacheKind = BytecodeCacheKind::Stencil; + } else { + saveCacheKind = BytecodeCacheKind::Script; + } + } + } + + if (saveBytecode || saveIncrementalBytecode) { + // If we are both loading and saving, we assert that we are going to + // replace the current bytecode by the same stream of bytes. + if (loadBytecode && assertEqBytecode) { + if (saveBuffer.length() != loadBuffer.length()) { + char loadLengthStr[16]; + SprintfLiteral(loadLengthStr, "%zu", loadBuffer.length()); + char saveLengthStr[16]; + SprintfLiteral(saveLengthStr, "%zu", saveBuffer.length()); + + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_CACHE_EQ_SIZE_FAILED, loadLengthStr, + saveLengthStr); + return false; + } + + if (!ArrayEqual(loadBuffer.begin(), saveBuffer.begin(), + loadBuffer.length())) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_CACHE_EQ_CONTENT_FAILED); + return false; + } + } + + size_t saveLength = saveBuffer.length(); + if (saveLength >= INT32_MAX) { + JS_ReportErrorASCII(cx, "Cannot save large cache entry content"); + return false; + } + uint8_t* saveData = saveBuffer.extractOrCopyRawBuffer(); + if (!CacheEntry_setBytecode(cx, cacheEntry, saveData, saveLength)) { + js_free(saveData); + return false; + } + MOZ_ASSERT(saveCacheKind != BytecodeCacheKind::Undefined); + CacheEntry_setKind(cx, cacheEntry, saveCacheKind); + } + + return JS_WrapValue(cx, args.rval()); +} + +JSString* js::shell::FileAsString(JSContext* cx, JS::HandleString pathnameStr) { + UniqueChars pathname = JS_EncodeStringToLatin1(cx, pathnameStr); + if (!pathname) { + return nullptr; + } + + FILE* file; + + file = fopen(pathname.get(), "rb"); + if (!file) { + ReportCantOpenErrorUnknownEncoding(cx, pathname.get()); + return nullptr; + } + + AutoCloseFile autoClose(file); + + struct stat st; + if (fstat(fileno(file), &st) != 0) { + JS_ReportErrorUTF8(cx, "can't stat %s", pathname.get()); + return nullptr; + } + + if ((st.st_mode & S_IFMT) != S_IFREG) { + JS_ReportErrorUTF8(cx, "can't read non-regular file %s", pathname.get()); + return nullptr; + } + + if (fseek(file, 0, SEEK_END) != 0) { + pathname = JS_EncodeStringToUTF8(cx, pathnameStr); + if (!pathname) { + return nullptr; + } + JS_ReportErrorUTF8(cx, "can't seek end of %s", pathname.get()); + return nullptr; + } + + long endPos = ftell(file); + if (endPos < 0) { + JS_ReportErrorUTF8(cx, "can't read length of %s", pathname.get()); + return nullptr; + } + + size_t len = endPos; + if (fseek(file, 0, SEEK_SET) != 0) { + pathname = JS_EncodeStringToUTF8(cx, pathnameStr); + if (!pathname) { + return nullptr; + } + JS_ReportErrorUTF8(cx, "can't seek start of %s", pathname.get()); + return nullptr; + } + + UniqueChars buf(js_pod_malloc(len + 1)); + if (!buf) { + JS_ReportErrorUTF8(cx, "out of memory reading %s", pathname.get()); + return nullptr; + } + + size_t cc = fread(buf.get(), 1, len, file); + if (cc != len) { + if (ptrdiff_t(cc) < 0) { + ReportCantOpenErrorUnknownEncoding(cx, pathname.get()); + } else { + pathname = JS_EncodeStringToUTF8(cx, pathnameStr); + if (!pathname) { + return nullptr; + } + JS_ReportErrorUTF8(cx, "can't read %s: short read", pathname.get()); + } + return nullptr; + } + + UniqueTwoByteChars ucbuf( + JS::LossyUTF8CharsToNewTwoByteCharsZ(cx, JS::UTF8Chars(buf.get(), len), + &len, js::MallocArena) + .get()); + if (!ucbuf) { + pathname = JS_EncodeStringToUTF8(cx, pathnameStr); + if (!pathname) { + return nullptr; + } + JS_ReportErrorUTF8(cx, "Invalid UTF-8 in file '%s'", pathname.get()); + return nullptr; + } + + return JS_NewUCStringCopyN(cx, ucbuf.get(), len); +} + +/* + * Function to run scripts and return compilation + execution time. Semantics + * are closely modelled after the equivalent function in WebKit, as this is used + * to produce benchmark timings by SunSpider. + */ +static bool Run(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 1) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "run"); + return false; + } + + RootedString str(cx, JS::ToString(cx, args[0])); + if (!str) { + return false; + } + args[0].setString(str); + + str = FileAsString(cx, str); + if (!str) { + return false; + } + + AutoStableStringChars chars(cx); + if (!chars.initTwoByte(cx, str)) { + return false; + } + + JS::SourceText srcBuf; + if (!srcBuf.init(cx, chars.twoByteRange().begin().get(), str->length(), + JS::SourceOwnership::Borrowed)) { + return false; + } + + RootedScript script(cx); + int64_t startClock = PRMJ_Now(); + { + /* FIXME: This should use UTF-8 (bug 987069). */ + UniqueChars filename = JS_EncodeStringToLatin1(cx, str); + if (!filename) { + return false; + } + + JS::CompileOptions options(cx); + options.setIntroductionType("js shell run") + .setFileAndLine(filename.get(), 1) + .setIsRunOnce(true) + .setNoScriptRval(true); + + script = JS::Compile(cx, options, srcBuf); + if (!script) { + return false; + } + } + + if (!JS_ExecuteScript(cx, script)) { + return false; + } + + int64_t endClock = PRMJ_Now(); + + args.rval().setDouble((endClock - startClock) / double(PRMJ_USEC_PER_MSEC)); + return true; +} + +/* + * function readline() + * Provides a hook for scripts to read a line from stdin. + */ +static bool ReadLine(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + +#define BUFSIZE 256 + FILE* from = stdin; + size_t buflength = 0; + size_t bufsize = BUFSIZE; + char* buf = (char*)JS_malloc(cx, bufsize); + if (!buf) { + return false; + } + + bool sawNewline = false; + size_t gotlength; + while ((gotlength = js_fgets(buf + buflength, bufsize - buflength, from)) > + 0) { + buflength += gotlength; + + /* Are we done? */ + if (buf[buflength - 1] == '\n') { + buf[buflength - 1] = '\0'; + sawNewline = true; + break; + } else if (buflength < bufsize - 1) { + break; + } + + /* Else, grow our buffer for another pass. */ + char* tmp; + bufsize *= 2; + if (bufsize > buflength) { + tmp = static_cast(JS_realloc(cx, buf, bufsize / 2, bufsize)); + } else { + JS_ReportOutOfMemory(cx); + tmp = nullptr; + } + + if (!tmp) { + JS_free(cx, buf); + return false; + } + + buf = tmp; + } + + /* Treat the empty string specially. */ + if (buflength == 0) { + args.rval().set(feof(from) ? NullValue() : JS_GetEmptyStringValue(cx)); + JS_free(cx, buf); + return true; + } + + /* Shrink the buffer to the real size. */ + char* tmp = static_cast(JS_realloc(cx, buf, bufsize, buflength)); + if (!tmp) { + JS_free(cx, buf); + return false; + } + + buf = tmp; + + /* + * Turn buf into a JSString. Note that buflength includes the trailing null + * character. + */ + JSString* str = + JS_NewStringCopyN(cx, buf, sawNewline ? buflength - 1 : buflength); + JS_free(cx, buf); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/* + * function readlineBuf() + * Provides a hook for scripts to emulate readline() using a string object. + */ +static bool ReadLineBuf(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + ShellContext* sc = GetShellContext(cx); + + if (!args.length()) { + if (!sc->readLineBuf) { + JS_ReportErrorASCII(cx, + "No source buffer set. You must initially " + "call readlineBuf with an argument."); + return false; + } + + char* currentBuf = sc->readLineBuf.get() + sc->readLineBufPos; + size_t buflen = strlen(currentBuf); + + if (!buflen) { + args.rval().setNull(); + return true; + } + + size_t len = 0; + while (len < buflen) { + if (currentBuf[len] == '\n') { + break; + } + len++; + } + + JSString* str = JS_NewStringCopyUTF8N(cx, JS::UTF8Chars(currentBuf, len)); + if (!str) { + return false; + } + + if (currentBuf[len] == '\0') { + sc->readLineBufPos += len; + } else { + sc->readLineBufPos += len + 1; + } + + args.rval().setString(str); + return true; + } + + if (args.length() == 1) { + sc->readLineBuf = nullptr; + sc->readLineBufPos = 0; + + RootedString str(cx, JS::ToString(cx, args[0])); + if (!str) { + return false; + } + sc->readLineBuf = JS_EncodeStringToUTF8(cx, str); + if (!sc->readLineBuf) { + return false; + } + + args.rval().setUndefined(); + return true; + } + + JS_ReportErrorASCII(cx, "Must specify at most one argument"); + return false; +} + +static bool PutStr(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 0) { + if (!gOutFile->isOpen()) { + JS_ReportErrorASCII(cx, "output file is closed"); + return false; + } + + RootedString str(cx, JS::ToString(cx, args[0])); + if (!str) { + return false; + } + UniqueChars bytes = JS_EncodeStringToUTF8(cx, str); + if (!bytes) { + return false; + } + fputs(bytes.get(), gOutFile->fp); + fflush(gOutFile->fp); + } + + args.rval().setUndefined(); + return true; +} + +static bool Now(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + double now = PRMJ_Now() / double(PRMJ_USEC_PER_MSEC); + args.rval().setDouble(now); + return true; +} + +static bool CpuNow(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + double now = double(std::clock()) / double(CLOCKS_PER_SEC); + args.rval().setDouble(now); + return true; +} + +static bool PrintInternal(JSContext* cx, const CallArgs& args, RCFile* file) { + if (!file->isOpen()) { + JS_ReportErrorASCII(cx, "output file is closed"); + return false; + } + + for (unsigned i = 0; i < args.length(); i++) { + RootedString str(cx, JS::ToString(cx, args[i])); + if (!str) { + return false; + } + UniqueChars bytes = JS_EncodeStringToUTF8(cx, str); + if (!bytes) { + return false; + } + fprintf(file->fp, "%s%s", i ? " " : "", bytes.get()); + } + + fputc('\n', file->fp); + fflush(file->fp); + + args.rval().setUndefined(); + return true; +} + +static bool Print(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); +#ifdef FUZZING_INTERFACES + if (fuzzHaveModule && !fuzzDoDebug) { + // When fuzzing and not debugging, suppress any print() output, + // as it slows down fuzzing and makes libFuzzer's output hard + // to read. + args.rval().setUndefined(); + return true; + } +#endif // FUZZING_INTERFACES + return PrintInternal(cx, args, gOutFile); +} + +static bool PrintErr(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return PrintInternal(cx, args, gErrFile); +} + +static bool Help(JSContext* cx, unsigned argc, Value* vp); + +static bool Quit(JSContext* cx, unsigned argc, Value* vp) { + ShellContext* sc = GetShellContext(cx); + + // Print a message to stderr in differential testing to help jsfunfuzz + // find uncatchable-exception bugs. + if (js::SupportDifferentialTesting()) { + fprintf(stderr, "quit called\n"); + } + + CallArgs args = CallArgsFromVp(argc, vp); + int32_t code; + if (!ToInt32(cx, args.get(0), &code)) { + return false; + } + + // The fuzzers check the shell's exit code and assume a value >= 128 means + // the process crashed (for instance, SIGSEGV will result in code 139). On + // POSIX platforms, the exit code is 8-bit and negative values can also + // result in an exit code >= 128. We restrict the value to range [0, 127] to + // avoid false positives. + if (code < 0 || code >= 128) { + JS_ReportErrorASCII(cx, "quit exit code should be in range 0-127"); + return false; + } + + js::StopDrainingJobQueue(cx); + sc->exitCode = code; + sc->quitting = true; + return false; +} + +static bool StartTimingMutator(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() > 0) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_TOO_MANY_ARGS, "startTimingMutator"); + return false; + } + + if (!cx->runtime()->gc.stats().startTimingMutator()) { + JS_ReportErrorASCII( + cx, "StartTimingMutator should only be called from outside of GC"); + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool StopTimingMutator(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() > 0) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_TOO_MANY_ARGS, "stopTimingMutator"); + return false; + } + + double mutator_ms, gc_ms; + if (!cx->runtime()->gc.stats().stopTimingMutator(mutator_ms, gc_ms)) { + JS_ReportErrorASCII(cx, + "stopTimingMutator called when not timing the mutator"); + return false; + } + double total_ms = mutator_ms + gc_ms; + if (total_ms > 0 && gOutFile->isOpen()) { + fprintf(gOutFile->fp, "Mutator: %.3fms (%.1f%%), GC: %.3fms (%.1f%%)\n", + mutator_ms, mutator_ms / total_ms * 100.0, gc_ms, + gc_ms / total_ms * 100.0); + } + + args.rval().setUndefined(); + return true; +} + +static const char* ToSource(JSContext* cx, HandleValue vp, UniqueChars* bytes) { + RootedString str(cx, JS_ValueToSource(cx, vp)); + if (str) { + *bytes = JS_EncodeStringToUTF8(cx, str); + if (*bytes) { + return bytes->get(); + } + } + JS_ClearPendingException(cx); + return "<>"; +} + +static bool AssertEq(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!(args.length() == 2 || (args.length() == 3 && args[2].isString()))) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + (args.length() < 2) ? JSSMSG_NOT_ENOUGH_ARGS + : (args.length() == 3) ? JSSMSG_INVALID_ARGS + : JSSMSG_TOO_MANY_ARGS, + "assertEq"); + return false; + } + + bool same; + if (!JS::SameValue(cx, args[0], args[1], &same)) { + return false; + } + if (!same) { + UniqueChars bytes0, bytes1; + const char* actual = ToSource(cx, args[0], &bytes0); + const char* expected = ToSource(cx, args[1], &bytes1); + if (args.length() == 2) { + JS_ReportErrorNumberUTF8(cx, my_GetErrorMessage, nullptr, + JSSMSG_ASSERT_EQ_FAILED, actual, expected); + } else { + RootedString message(cx, args[2].toString()); + UniqueChars bytes2 = QuoteString(cx, message); + if (!bytes2) { + return false; + } + JS_ReportErrorNumberUTF8(cx, my_GetErrorMessage, nullptr, + JSSMSG_ASSERT_EQ_FAILED_MSG, actual, expected, + bytes2.get()); + } + return false; + } + args.rval().setUndefined(); + return true; +} + +static JSScript* GetTopScript(JSContext* cx) { + NonBuiltinScriptFrameIter iter(cx); + return iter.done() ? nullptr : iter.script(); +} + +static bool GetScriptAndPCArgs(JSContext* cx, CallArgs& args, + MutableHandleScript scriptp, int32_t* ip) { + RootedScript script(cx, GetTopScript(cx)); + *ip = 0; + if (!args.get(0).isUndefined()) { + HandleValue v = args[0]; + unsigned intarg = 0; + if (v.isObject() && JS::GetClass(&v.toObject()) == &JSFunction::class_) { + script = TestingFunctionArgumentToScript(cx, v); + if (!script) { + return false; + } + intarg++; + } + if (!args.get(intarg).isUndefined()) { + if (!JS::ToInt32(cx, args[intarg], ip)) { + return false; + } + if ((uint32_t)*ip >= script->length()) { + JS_ReportErrorASCII(cx, "Invalid PC"); + return false; + } + } + } + + scriptp.set(script); + + return true; +} + +static bool LineToPC(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() == 0) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_LINE2PC_USAGE); + return false; + } + + RootedScript script(cx, GetTopScript(cx)); + int32_t lineArg = 0; + if (args[0].isObject() && args[0].toObject().is()) { + script = TestingFunctionArgumentToScript(cx, args[0]); + if (!script) { + return false; + } + lineArg++; + } + + uint32_t lineno; + if (!ToUint32(cx, args.get(lineArg), &lineno)) { + return false; + } + + jsbytecode* pc = LineNumberToPC(script, lineno); + if (!pc) { + return false; + } + args.rval().setInt32(script->pcToOffset(pc)); + return true; +} + +static bool PCToLine(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedScript script(cx); + int32_t i; + unsigned lineno; + + if (!GetScriptAndPCArgs(cx, args, &script, &i)) { + return false; + } + lineno = PCToLineNumber(script, script->offsetToPC(i)); + if (!lineno) { + return false; + } + args.rval().setInt32(lineno); + return true; +} + +#if defined(DEBUG) || defined(JS_JITSPEW) + +static MOZ_MUST_USE bool SrcNotes(JSContext* cx, HandleScript script, + Sprinter* sp) { + if (!sp->put("\nSource notes:\n") || + !sp->jsprintf("%4s %4s %6s %5s %6s %-10s %s\n", "ofs", "line", "column", + "pc", "delta", "desc", "args") || + !sp->put("---- ---- ------ ----- ------ ---------- ------\n")) { + return false; + } + + unsigned offset = 0; + unsigned lineno = script->lineno(); + unsigned column = script->column(); + SrcNote* notes = script->notes(); + for (SrcNoteIterator iter(notes); !iter.atEnd(); ++iter) { + auto sn = *iter; + + unsigned delta = sn->delta(); + offset += delta; + SrcNoteType type = sn->type(); + const char* name = sn->name(); + if (!sp->jsprintf("%3u: %4u %6u %5u [%4u] %-10s", unsigned(sn - notes), + lineno, column, offset, delta, name)) { + return false; + } + + switch (type) { + case SrcNoteType::Null: + case SrcNoteType::AssignOp: + case SrcNoteType::Breakpoint: + case SrcNoteType::StepSep: + case SrcNoteType::XDelta: + break; + + case SrcNoteType::ColSpan: { + uint32_t colspan = SrcNote::ColSpan::getSpan(sn); + if (!sp->jsprintf(" colspan %u", colspan)) { + return false; + } + column += colspan; + break; + } + + case SrcNoteType::SetLine: + lineno = SrcNote::SetLine::getLine(sn, script->lineno()); + if (!sp->jsprintf(" lineno %u", lineno)) { + return false; + } + column = 0; + break; + + case SrcNoteType::NewLine: + ++lineno; + column = 0; + break; + + default: + MOZ_ASSERT_UNREACHABLE("unrecognized srcnote"); + } + if (!sp->put("\n")) { + return false; + } + } + + return true; +} + +static bool Notes(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + Sprinter sprinter(cx); + if (!sprinter.init()) { + return false; + } + + for (unsigned i = 0; i < args.length(); i++) { + RootedScript script(cx, TestingFunctionArgumentToScript(cx, args[i])); + if (!script) { + return false; + } + + if (!SrcNotes(cx, script, &sprinter)) { + return false; + } + } + + JSString* str = JS_NewStringCopyZ(cx, sprinter.string()); + if (!str) { + return false; + } + args.rval().setString(str); + return true; +} + +static const char* TryNoteName(TryNoteKind kind) { + switch (kind) { + case TryNoteKind::Catch: + return "catch"; + case TryNoteKind::Finally: + return "finally"; + case TryNoteKind::ForIn: + return "for-in"; + case TryNoteKind::ForOf: + return "for-of"; + case TryNoteKind::Loop: + return "loop"; + case TryNoteKind::ForOfIterClose: + return "for-of-iterclose"; + case TryNoteKind::Destructuring: + return "destructuring"; + } + + MOZ_CRASH("Bad TryNoteKind"); +} + +static MOZ_MUST_USE bool TryNotes(JSContext* cx, HandleScript script, + Sprinter* sp) { + if (!sp->put( + "\nException table:\nkind stack start end\n")) { + return false; + } + + for (const TryNote& tn : script->trynotes()) { + if (!sp->jsprintf(" %-16s %6u %8u %8u\n", TryNoteName(tn.kind()), + tn.stackDepth, tn.start, tn.start + tn.length)) { + return false; + } + } + return true; +} + +static MOZ_MUST_USE bool ScopeNotes(JSContext* cx, HandleScript script, + Sprinter* sp) { + if (!sp->put("\nScope notes:\n index parent start end\n")) { + return false; + } + + for (const ScopeNote& note : script->scopeNotes()) { + if (note.index == ScopeNote::NoScopeIndex) { + if (!sp->jsprintf("%8s ", "(none)")) { + return false; + } + } else { + if (!sp->jsprintf("%8u ", note.index.index)) { + return false; + } + } + if (note.parent == ScopeNote::NoScopeIndex) { + if (!sp->jsprintf("%8s ", "(none)")) { + return false; + } + } else { + if (!sp->jsprintf("%8u ", note.parent)) { + return false; + } + } + if (!sp->jsprintf("%8u %8u\n", note.start, note.start + note.length)) { + return false; + } + } + return true; +} + +static MOZ_MUST_USE bool GCThings(JSContext* cx, HandleScript script, + Sprinter* sp) { + if (!sp->put("\nGC things:\n index type value\n")) { + return false; + } + + size_t i = 0; + for (JS::GCCellPtr gcThing : script->gcthings()) { + if (!sp->jsprintf("%8zu ", i)) { + return false; + } + if (gcThing.is()) { + if (!sp->put("BigInt ")) { + return false; + } + gcThing.as().dump(*sp); + if (!sp->put("\n")) { + return false; + } + } else if (gcThing.is()) { + if (!sp->put("Scope ")) { + return false; + } + Rooted scope(cx, &gcThing.as()); + if (!Scope::dumpForDisassemble(cx, scope, *sp, + " ")) { + return false; + } + if (!sp->put("\n")) { + return false; + } + } else if (gcThing.is()) { + JSObject* obj = &gcThing.as(); + if (obj->is()) { + if (!sp->put("Function ")) { + return false; + } + RootedFunction fun(cx, &obj->as()); + if (fun->displayAtom()) { + Rooted name(cx, fun->displayAtom()); + UniqueChars utf8chars = JS_EncodeStringToUTF8(cx, name); + if (!utf8chars) { + return false; + } + if (!sp->put(utf8chars.get())) { + return false; + } + } else { + if (!sp->put("(anonymous)")) { + return false; + } + } + + if (fun->hasBaseScript()) { + BaseScript* script = fun->baseScript(); + if (!sp->jsprintf(" @ %u:%u\n", script->lineno(), script->column())) { + return false; + } + } else { + if (!sp->put(" (no script)\n")) { + return false; + } + } + } else { + if (obj->is()) { + if (!sp->put("RegExp ")) { + return false; + } + } else { + if (!sp->put("Object ")) { + return false; + } + } + + RootedValue objValue(cx, ObjectValue(*obj)); + RootedString str(cx, ValueToSource(cx, objValue)); + if (!str) { + return false; + } + UniqueChars utf8chars = JS_EncodeStringToUTF8(cx, str); + if (!utf8chars) { + return false; + } + if (!sp->put(utf8chars.get())) { + return false; + } + + if (!sp->put("\n")) { + return false; + } + } + } else if (gcThing.is()) { + if (!sp->put("Atom ")) { + return false; + } + RootedAtom atom(cx, &gcThing.as().asAtom()); + UniqueChars chars = QuoteString(cx, atom, '"'); + if (!chars) { + return false; + } + if (!sp->put(chars.get())) { + return false; + } + if (!sp->put("\n")) { + return false; + } + } else { + if (!sp->put("Unknown\n")) { + return false; + } + } + i++; + } + + return true; +} + +static MOZ_MUST_USE bool DisassembleScript(JSContext* cx, HandleScript script, + HandleFunction fun, bool lines, + bool recursive, bool sourceNotes, + bool gcThings, Sprinter* sp) { + if (fun) { + if (!sp->put("flags:")) { + return false; + } + if (fun->isLambda()) { + if (!sp->put(" LAMBDA")) { + return false; + } + } + if (fun->needsCallObject()) { + if (!sp->put(" NEEDS_CALLOBJECT")) { + return false; + } + } + if (fun->needsExtraBodyVarEnvironment()) { + if (!sp->put(" NEEDS_EXTRABODYVARENV")) { + return false; + } + } + if (fun->needsNamedLambdaEnvironment()) { + if (!sp->put(" NEEDS_NAMEDLAMBDAENV")) { + return false; + } + } + if (fun->isConstructor()) { + if (!sp->put(" CONSTRUCTOR")) { + return false; + } + } + if (fun->isSelfHostedBuiltin()) { + if (!sp->put(" SELF_HOSTED")) { + return false; + } + } + if (fun->isArrow()) { + if (!sp->put(" ARROW")) { + return false; + } + } + if (!sp->put("\n")) { + return false; + } + } + + if (!Disassemble(cx, script, lines, sp)) { + return false; + } + if (sourceNotes) { + if (!SrcNotes(cx, script, sp)) { + return false; + } + } + if (!TryNotes(cx, script, sp)) { + return false; + } + if (!ScopeNotes(cx, script, sp)) { + return false; + } + if (gcThings) { + if (!GCThings(cx, script, sp)) { + return false; + } + } + + if (recursive) { + for (JS::GCCellPtr gcThing : script->gcthings()) { + if (!gcThing.is()) { + continue; + } + + JSObject* obj = &gcThing.as(); + if (obj->is()) { + if (!sp->put("\n")) { + return false; + } + + RootedFunction fun(cx, &obj->as()); + if (fun->isInterpreted()) { + RootedScript script(cx, JSFunction::getOrCreateScript(cx, fun)); + if (!script || !DisassembleScript(cx, script, fun, lines, recursive, + sourceNotes, gcThings, sp)) { + return false; + } + } else { + if (!sp->put("[native code]\n")) { + return false; + } + } + } + } + } + + return true; +} + +namespace { + +struct DisassembleOptionParser { + unsigned argc; + Value* argv; + bool lines; + bool recursive; + bool sourceNotes; + bool gcThings; + + DisassembleOptionParser(unsigned argc, Value* argv) + : argc(argc), + argv(argv), + lines(false), + recursive(false), + sourceNotes(true), + gcThings(false) {} + + bool parse(JSContext* cx) { + /* Read options off early arguments */ + while (argc > 0 && argv[0].isString()) { + JSString* str = argv[0].toString(); + JSLinearString* linearStr = JS_EnsureLinearString(cx, str); + if (!linearStr) { + return false; + } + if (JS_LinearStringEqualsLiteral(linearStr, "-l")) { + lines = true; + } else if (JS_LinearStringEqualsLiteral(linearStr, "-r")) { + recursive = true; + } else if (JS_LinearStringEqualsLiteral(linearStr, "-S")) { + sourceNotes = false; + } else if (JS_LinearStringEqualsLiteral(linearStr, "-g")) { + gcThings = true; + } else { + break; + } + argv++; + argc--; + } + return true; + } +}; + +} /* anonymous namespace */ + +static bool DisassembleToSprinter(JSContext* cx, unsigned argc, Value* vp, + Sprinter* sprinter) { + CallArgs args = CallArgsFromVp(argc, vp); + DisassembleOptionParser p(args.length(), args.array()); + if (!p.parse(cx)) { + return false; + } + + if (p.argc == 0) { + /* Without arguments, disassemble the current script. */ + RootedScript script(cx, GetTopScript(cx)); + if (script) { + JSAutoRealm ar(cx, script); + if (!Disassemble(cx, script, p.lines, sprinter)) { + return false; + } + if (!SrcNotes(cx, script, sprinter)) { + return false; + } + if (!TryNotes(cx, script, sprinter)) { + return false; + } + if (!ScopeNotes(cx, script, sprinter)) { + return false; + } + if (p.gcThings) { + if (!GCThings(cx, script, sprinter)) { + return false; + } + } + } + } else { + for (unsigned i = 0; i < p.argc; i++) { + RootedFunction fun(cx); + RootedScript script(cx); + RootedValue value(cx, p.argv[i]); + if (value.isObject() && value.toObject().is()) { + script = value.toObject().as().maybeScript(); + } else { + script = TestingFunctionArgumentToScript(cx, value, fun.address()); + } + if (!script) { + return false; + } + if (!DisassembleScript(cx, script, fun, p.lines, p.recursive, + p.sourceNotes, p.gcThings, sprinter)) { + return false; + } + } + } + + return !sprinter->hadOutOfMemory(); +} + +static bool DisassembleToString(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + Sprinter sprinter(cx); + if (!sprinter.init()) { + return false; + } + if (!DisassembleToSprinter(cx, args.length(), vp, &sprinter)) { + return false; + } + + JS::ConstUTF8CharsZ utf8chars(sprinter.string(), strlen(sprinter.string())); + JSString* str = JS_NewStringCopyUTF8Z(cx, utf8chars); + if (!str) { + return false; + } + args.rval().setString(str); + return true; +} + +static bool Disassemble(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!gOutFile->isOpen()) { + JS_ReportErrorASCII(cx, "output file is closed"); + return false; + } + + Sprinter sprinter(cx); + if (!sprinter.init()) { + return false; + } + if (!DisassembleToSprinter(cx, args.length(), vp, &sprinter)) { + return false; + } + + fprintf(gOutFile->fp, "%s\n", sprinter.string()); + args.rval().setUndefined(); + return true; +} + +static bool DisassFile(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!gOutFile->isOpen()) { + JS_ReportErrorASCII(cx, "output file is closed"); + return false; + } + + /* Support extra options at the start, just like Disassemble. */ + DisassembleOptionParser p(args.length(), args.array()); + if (!p.parse(cx)) { + return false; + } + + if (!p.argc) { + args.rval().setUndefined(); + return true; + } + + // We should change DisassembleOptionParser to store CallArgs. + JSString* str = JS::ToString(cx, HandleValue::fromMarkedLocation(&p.argv[0])); + if (!str) { + return false; + } + UniqueChars filename = JS_EncodeStringToLatin1(cx, str); + if (!filename) { + return false; + } + RootedScript script(cx); + + { + CompileOptions options(cx); + options.setIntroductionType("js shell disFile") + .setFileAndLine(filename.get(), 1) + .setIsRunOnce(true) + .setNoScriptRval(true); + + script = JS::CompileUtf8Path(cx, options, filename.get()); + if (!script) { + return false; + } + } + + Sprinter sprinter(cx); + if (!sprinter.init()) { + return false; + } + if (!DisassembleScript(cx, script, nullptr, p.lines, p.recursive, + p.sourceNotes, p.gcThings, &sprinter)) { + return false; + } + + fprintf(gOutFile->fp, "%s\n", sprinter.string()); + + args.rval().setUndefined(); + return true; +} + +static bool DisassWithSrc(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!gOutFile->isOpen()) { + JS_ReportErrorASCII(cx, "output file is closed"); + return false; + } + + const size_t lineBufLen = 512; + unsigned len, line1, line2, bupline; + char linebuf[lineBufLen]; + static const char sep[] = ";-------------------------"; + + RootedScript script(cx); + for (unsigned i = 0; i < args.length(); i++) { + script = TestingFunctionArgumentToScript(cx, args[i]); + if (!script) { + return false; + } + + if (!script->filename()) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_FILE_SCRIPTS_ONLY); + return false; + } + + FILE* file = fopen(script->filename(), "rb"); + if (!file) { + /* FIXME: script->filename() should become UTF-8 (bug 987069). */ + ReportCantOpenErrorUnknownEncoding(cx, script->filename()); + return false; + } + auto closeFile = MakeScopeExit([file] { fclose(file); }); + + jsbytecode* pc = script->code(); + jsbytecode* end = script->codeEnd(); + + Sprinter sprinter(cx); + if (!sprinter.init()) { + return false; + } + + /* burn the leading lines */ + line2 = PCToLineNumber(script, pc); + for (line1 = 0; line1 < line2 - 1; line1++) { + char* tmp = fgets(linebuf, lineBufLen, file); + if (!tmp) { + /* FIXME: This should use UTF-8 (bug 987069). */ + JS_ReportErrorLatin1(cx, "failed to read %s fully", script->filename()); + return false; + } + } + + bupline = 0; + while (pc < end) { + line2 = PCToLineNumber(script, pc); + + if (line2 < line1) { + if (bupline != line2) { + bupline = line2; + if (!sprinter.jsprintf("%s %3u: BACKUP\n", sep, line2)) { + return false; + } + } + } else { + if (bupline && line1 == line2) { + if (!sprinter.jsprintf("%s %3u: RESTORE\n", sep, line2)) { + return false; + } + } + bupline = 0; + while (line1 < line2) { + if (!fgets(linebuf, lineBufLen, file)) { + /* + * FIXME: script->filename() should become UTF-8 + * (bug 987069). + */ + JS_ReportErrorNumberLatin1(cx, my_GetErrorMessage, nullptr, + JSSMSG_UNEXPECTED_EOF, + script->filename()); + return false; + } + line1++; + if (!sprinter.jsprintf("%s %3u: %s", sep, line1, linebuf)) { + return false; + } + } + } + + len = + Disassemble1(cx, script, pc, script->pcToOffset(pc), true, &sprinter); + if (!len) { + return false; + } + + pc += len; + } + + fprintf(gOutFile->fp, "%s\n", sprinter.string()); + } + + args.rval().setUndefined(); + return true; +} + +#endif /* defined(DEBUG) || defined(JS_JITSPEW) */ + +#ifdef JS_CACHEIR_SPEW +static bool RateMyCacheIR(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + js::jit::CacheIRHealth cih; + RootedScript script(cx); + + // In the case that we are calling this function from the shell and + // the environment variable is not set, AutoSpewChannel automatically + // sets and unsets the proper channel for the duration of spewing + // a health report. + AutoSpewChannel channel(cx, SpewChannel::RateMyCacheIR, script); + if (!argc) { + // Calling RateMyCacheIR without any arguments will create health + // reports for all scripts in the zone. + for (auto base = cx->zone()->cellIter(); !base.done(); + base.next()) { + if (!base->hasJitScript() || base->selfHosted()) { + continue; + } + + script = base->asJSScript(); + cih.rateScript(cx, script, js::jit::SpewContext::Shell); + } + } else { + RootedValue value(cx, args.get(0)); + + if (value.isObject() && value.toObject().is()) { + script = value.toObject().as().maybeScript(); + } else { + script = TestingFunctionArgumentToScript(cx, args.get(0)); + } + + if (!script) { + return false; + } + + cih.rateScript(cx, script, js::jit::SpewContext::Shell); + } + + args.rval().setUndefined(); + return true; +} +#endif /* JS_CACHEIR_SPEW */ + +/* Pretend we can always preserve wrappers for dummy DOM objects. */ +static bool DummyPreserveWrapperCallback(JSContext* cx, HandleObject obj) { + return true; +} + +static bool DummyHasReleasedWrapperCallback(HandleObject obj) { return true; } + +static bool Intern(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + JSString* str = JS::ToString(cx, args.get(0)); + if (!str) { + return false; + } + + AutoStableStringChars strChars(cx); + if (!strChars.initTwoByte(cx, str)) { + return false; + } + + mozilla::Range chars = strChars.twoByteRange(); + + if (!JS_AtomizeAndPinUCStringN(cx, chars.begin().get(), chars.length())) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool Crash(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() == 0) { + MOZ_CRASH("forced crash"); + } + RootedString message(cx, JS::ToString(cx, args[0])); + if (!message) { + return false; + } + UniqueChars utf8chars = JS_EncodeStringToUTF8(cx, message); + if (!utf8chars) { + return false; + } + if (args.get(1).isObject()) { + RootedValue v(cx); + RootedObject opts(cx, &args[1].toObject()); + if (!JS_GetProperty(cx, opts, "suppress_minidump", &v)) { + return false; + } + if (v.isBoolean() && v.toBoolean()) { + js::NoteIntentionalCrash(); + } + } +#ifndef DEBUG + MOZ_ReportCrash(utf8chars.get(), __FILE__, __LINE__); +#endif + MOZ_CRASH_UNSAFE(utf8chars.get()); +} + +static bool GetSLX(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedScript script(cx); + + script = TestingFunctionArgumentToScript(cx, args.get(0)); + if (!script) { + return false; + } + args.rval().setInt32(GetScriptLineExtent(script)); + return true; +} + +static bool ThrowError(JSContext* cx, unsigned argc, Value* vp) { + JS_ReportErrorASCII(cx, "This is an error"); + return false; +} + +static bool CopyErrorReportToObject(JSContext* cx, JSErrorReport* report, + HandleObject obj) { + RootedString nameStr(cx); + if (report->exnType == JSEXN_WARN) { + nameStr = JS_NewStringCopyZ(cx, "Warning"); + if (!nameStr) { + return false; + } + } else { + nameStr = GetErrorTypeName(cx, report->exnType); + // GetErrorTypeName doesn't set an exception, but + // can fail for InternalError or non-error objects. + if (!nameStr) { + nameStr = cx->runtime()->emptyString; + } + } + RootedValue nameVal(cx, StringValue(nameStr)); + if (!DefineDataProperty(cx, obj, cx->names().name, nameVal)) { + return false; + } + + RootedString messageStr(cx, report->newMessageString(cx)); + if (!messageStr) { + return false; + } + RootedValue messageVal(cx, StringValue(messageStr)); + if (!DefineDataProperty(cx, obj, cx->names().message, messageVal)) { + return false; + } + + RootedValue linenoVal(cx, Int32Value(report->lineno)); + if (!DefineDataProperty(cx, obj, cx->names().lineNumber, linenoVal)) { + return false; + } + + RootedValue columnVal(cx, Int32Value(report->column)); + if (!DefineDataProperty(cx, obj, cx->names().columnNumber, columnVal)) { + return false; + } + + RootedObject notesArray(cx, CreateErrorNotesArray(cx, report)); + if (!notesArray) { + return false; + } + + RootedValue notesArrayVal(cx, ObjectValue(*notesArray)); + return DefineDataProperty(cx, obj, cx->names().notes, notesArrayVal); +} + +static bool CreateErrorReport(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // We don't have a stack here, so just initialize with null. + JS::ExceptionStack exnStack(cx, args.get(0), nullptr); + JS::ErrorReportBuilder report(cx); + if (!report.init(cx, exnStack, JS::ErrorReportBuilder::WithSideEffects)) { + return false; + } + + MOZ_ASSERT(!report.report()->isWarning()); + + RootedObject obj(cx, JS_NewPlainObject(cx)); + if (!obj) { + return false; + } + + RootedString toString(cx, + NewStringCopyUTF8Z(cx, report.toStringResult())); + if (!toString) { + return false; + } + + if (!JS_DefineProperty(cx, obj, "toStringResult", toString, + JSPROP_ENUMERATE)) { + return false; + } + + if (!CopyErrorReportToObject(cx, report.report(), obj)) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +#define LAZY_STANDARD_CLASSES + +/* A class for easily testing the inner/outer object callbacks. */ +typedef struct ComplexObject { + bool isInner; + bool frozen; + JSObject* inner; + JSObject* outer; +} ComplexObject; + +static bool sandbox_enumerate(JSContext* cx, JS::HandleObject obj, + JS::MutableHandleIdVector properties, + bool enumerableOnly) { + RootedValue v(cx); + + if (!JS_GetProperty(cx, obj, "lazy", &v)) { + return false; + } + + if (!ToBoolean(v)) { + return true; + } + + return JS_NewEnumerateStandardClasses(cx, obj, properties, enumerableOnly); +} + +static bool sandbox_resolve(JSContext* cx, HandleObject obj, HandleId id, + bool* resolvedp) { + RootedValue v(cx); + if (!JS_GetProperty(cx, obj, "lazy", &v)) { + return false; + } + + if (ToBoolean(v)) { + return JS_ResolveStandardClass(cx, obj, id, resolvedp); + } + return true; +} + +static const JSClassOps sandbox_classOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + sandbox_enumerate, // newEnumerate + sandbox_resolve, // resolve + nullptr, // mayResolve + nullptr, // finalize + nullptr, // call + nullptr, // hasInstance + nullptr, // construct + JS_GlobalObjectTraceHook, // trace +}; + +static const JSClass sandbox_class = {"sandbox", JSCLASS_GLOBAL_FLAGS, + &sandbox_classOps}; + +static void SetStandardRealmOptions(JS::RealmOptions& options) { + options.creationOptions() + .setSharedMemoryAndAtomicsEnabled(enableSharedMemory) + .setCoopAndCoepEnabled(false) + .setStreamsEnabled(enableStreams) + .setReadableByteStreamsEnabled(enableReadableByteStreams) + .setBYOBStreamReadersEnabled(enableBYOBStreamReaders) + .setWritableStreamsEnabled(enableWritableStreams) + .setReadableStreamPipeToEnabled(enableReadableStreamPipeTo) + .setWeakRefsEnabled(enableWeakRefs + ? JS::WeakRefSpecifier::EnabledWithCleanupSome + : JS::WeakRefSpecifier::Disabled) + .setToSourceEnabled(enableToSource) + .setPropertyErrorMessageFixEnabled(enablePropertyErrorMessageFix) + .setIteratorHelpersEnabled(enableIteratorHelpers); +} + +static MOZ_MUST_USE bool CheckRealmOptions(JSContext* cx, + JS::RealmOptions& options, + JSPrincipals* principals) { + JS::RealmCreationOptions& creationOptions = options.creationOptions(); + if (creationOptions.compartmentSpecifier() != + JS::CompartmentSpecifier::ExistingCompartment) { + return true; + } + + JS::Compartment* comp = creationOptions.compartment(); + + // All realms in a compartment must be either system or non-system. + bool isSystem = + principals && principals == cx->runtime()->trustedPrincipals(); + if (isSystem != IsSystemCompartment(comp)) { + JS_ReportErrorASCII(cx, + "Cannot create system and non-system realms in the " + "same compartment"); + return false; + } + + // Debugger visibility is per-compartment, not per-realm, so make sure the + // requested visibility matches the existing compartment's. + if (creationOptions.invisibleToDebugger() != comp->invisibleToDebugger()) { + JS_ReportErrorASCII(cx, + "All the realms in a compartment must have " + "the same debugger visibility"); + return false; + } + + return true; +} + +static JSObject* NewSandbox(JSContext* cx, bool lazy) { + JS::RealmOptions options; + SetStandardRealmOptions(options); + + if (defaultToSameCompartment) { + options.creationOptions().setExistingCompartment(cx->global()); + } else { + options.creationOptions().setNewCompartmentAndZone(); + } + + JSPrincipals* principals = nullptr; + if (!CheckRealmOptions(cx, options, principals)) { + return nullptr; + } + + RootedObject obj(cx, + JS_NewGlobalObject(cx, &sandbox_class, principals, + JS::DontFireOnNewGlobalHook, options)); + if (!obj) { + return nullptr; + } + + { + JSAutoRealm ar(cx, obj); + if (!lazy && !JS::InitRealmStandardClasses(cx)) { + return nullptr; + } + + RootedValue value(cx, BooleanValue(lazy)); + if (!JS_DefineProperty(cx, obj, "lazy", value, + JSPROP_PERMANENT | JSPROP_READONLY)) { + return nullptr; + } + + JS_FireOnNewGlobalObject(cx, obj); + } + + if (!cx->compartment()->wrap(cx, &obj)) { + return nullptr; + } + return obj; +} + +static bool EvalInContext(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "evalcx", 1)) { + return false; + } + + RootedString str(cx, ToString(cx, args[0])); + if (!str) { + return false; + } + + RootedObject sobj(cx); + if (args.hasDefined(1)) { + sobj = ToObject(cx, args[1]); + if (!sobj) { + return false; + } + } + + AutoStableStringChars strChars(cx); + if (!strChars.initTwoByte(cx, str)) { + return false; + } + + mozilla::Range chars = strChars.twoByteRange(); + size_t srclen = chars.length(); + const char16_t* src = chars.begin().get(); + + bool lazy = false; + if (srclen == 4) { + if (src[0] == 'l' && src[1] == 'a' && src[2] == 'z' && src[3] == 'y') { + lazy = true; + srclen = 0; + } + } + + if (!sobj) { + sobj = NewSandbox(cx, lazy); + if (!sobj) { + return false; + } + } + + if (srclen == 0) { + args.rval().setObject(*sobj); + return true; + } + + JS::AutoFilename filename; + unsigned lineno; + + DescribeScriptedCaller(cx, &filename, &lineno); + { + sobj = UncheckedUnwrap(sobj, true); + + JSAutoRealm ar(cx, sobj); + + sobj = ToWindowIfWindowProxy(sobj); + + if (!JS_IsGlobalObject(sobj)) { + JS_ReportErrorASCII(cx, "Invalid scope argument to evalcx"); + return false; + } + + JS::CompileOptions opts(cx); + opts.setFileAndLine(filename.get(), lineno); + + JS::SourceText srcBuf; + if (!srcBuf.init(cx, src, srclen, JS::SourceOwnership::Borrowed) || + !JS::Evaluate(cx, opts, srcBuf, args.rval())) { + return false; + } + } + + if (!cx->compartment()->wrap(cx, args.rval())) { + return false; + } + + return true; +} + +static bool EnsureGeckoProfilingStackInstalled(JSContext* cx, + ShellContext* sc) { + if (cx->geckoProfiler().infraInstalled()) { + MOZ_ASSERT(sc->geckoProfilingStack); + return true; + } + + MOZ_ASSERT(!sc->geckoProfilingStack); + sc->geckoProfilingStack = MakeUnique(); + if (!sc->geckoProfilingStack) { + JS_ReportOutOfMemory(cx); + return false; + } + + SetContextProfilingStack(cx, sc->geckoProfilingStack.get()); + return true; +} + +struct WorkerInput { + JSRuntime* parentRuntime; + UniqueTwoByteChars chars; + size_t length; + + WorkerInput(JSRuntime* parentRuntime, UniqueTwoByteChars chars, size_t length) + : parentRuntime(parentRuntime), chars(std::move(chars)), length(length) {} + + ~WorkerInput() = default; +}; + +static void DestroyShellCompartmentPrivate(JSFreeOp* fop, + JS::Compartment* compartment) { + auto priv = static_cast( + JS_GetCompartmentPrivate(compartment)); + js_delete(priv); +} + +static void SetWorkerContextOptions(JSContext* cx); +static bool ShellBuildId(JS::BuildIdCharVector* buildId); + +JSObject* GetElementCallback(JSContext* cx, JS::HandleValue value) { + RootedValue privateValue(cx, value); + if (!privateValue.isObject()) { + return nullptr; + } + + RootedObject infoObject(cx, + CheckedUnwrapStatic(privateValue.toObjectOrNull())); + AutoRealm ar(cx, infoObject); + + RootedValue elementValue(cx); + if (!JS_GetProperty(cx, infoObject, "element", &elementValue)) { + // This shouldn't happen in the shell, as ParseCompileOptions always + // creates the infoObject with this property. In any case, this callback + // must not leave an exception pending, so: + MOZ_CRASH("error getting source element"); + } + + if (elementValue.isObject()) { + return &elementValue.toObject(); + } + return nullptr; +} + +static void WorkerMain(WorkerInput* input) { + MOZ_ASSERT(input->parentRuntime); + + JSContext* cx = JS_NewContext(8L * 1024L * 1024L, input->parentRuntime); + if (!cx) { + return; + } + + ShellContext* sc = js_new(cx); + if (!sc) { + return; + } + + auto guard = mozilla::MakeScopeExit([&] { + CancelOffThreadJobsForContext(cx); + sc->markObservers.reset(); + JS_SetContextPrivate(cx, nullptr); + js_delete(sc); + JS_DestroyContext(cx); + js_delete(input); + }); + + sc->isWorker = true; + + JS_SetContextPrivate(cx, sc); + JS_SetGrayGCRootsTracer(cx, TraceGrayRoots, nullptr); + SetWorkerContextOptions(cx); + + JS_SetFutexCanWait(cx); + JS::SetWarningReporter(cx, WarningReporter); + js::SetPreserveWrapperCallbacks(cx, DummyPreserveWrapperCallback, + DummyHasReleasedWrapperCallback); + JS_InitDestroyPrincipalsCallback(cx, ShellPrincipals::destroy); + JS_SetDestroyCompartmentCallback(cx, DestroyShellCompartmentPrivate); + JS::SetGetElementCallback(cx, &GetElementCallback); + + js::SetWindowProxyClass(cx, &ShellWindowProxyClass); + + js::UseInternalJobQueues(cx); + + JS::SetHostCleanupFinalizationRegistryCallback( + cx, ShellCleanupFinalizationRegistryCallback, sc); + + if (!JS::InitSelfHostedCode(cx)) { + return; + } + + EnvironmentPreparer environmentPreparer(cx); + + do { + JS::RealmOptions realmOptions; + SetStandardRealmOptions(realmOptions); + + RootedObject global(cx, NewGlobalObject(cx, realmOptions, nullptr, + ShellGlobalKind::WindowProxy, + /* immutablePrototype = */ true)); + if (!global) { + break; + } + + JSAutoRealm ar(cx, global); + + JS::ConstUTF8CharsZ path(processWideModuleLoadPath.get(), + strlen(processWideModuleLoadPath.get())); + RootedString moduleLoadPath(cx, JS_NewStringCopyUTF8Z(cx, path)); + if (!moduleLoadPath) { + return; + } + sc->moduleLoader = js::MakeUnique(); + if (!sc->moduleLoader || !sc->moduleLoader->init(cx, moduleLoadPath)) { + return; + } + + JS::CompileOptions options(cx); + options.setFileAndLine("", 1).setIsRunOnce(true); + + AutoReportException are(cx); + JS::SourceText srcBuf; + if (!srcBuf.init(cx, input->chars.get(), input->length, + JS::SourceOwnership::Borrowed)) { + break; + } + + RootedScript script(cx, JS::Compile(cx, options, srcBuf)); + if (!script) { + break; + } + RootedValue result(cx); + JS_ExecuteScript(cx, script, &result); + } while (0); + + KillWatchdog(cx); + JS_SetGrayGCRootsTracer(cx, nullptr, nullptr); +} + +// Workers can spawn other workers, so we need a lock to access workerThreads. +static Mutex* workerThreadsLock = nullptr; +static Vector workerThreads; + +class MOZ_RAII AutoLockWorkerThreads : public LockGuard { + using Base = LockGuard; + + public: + AutoLockWorkerThreads() : Base(*workerThreadsLock) { + MOZ_ASSERT(workerThreadsLock); + } +}; + +static bool EvalInWorker(JSContext* cx, unsigned argc, Value* vp) { + if (!CanUseExtraThreads()) { + JS_ReportErrorASCII(cx, "Can't create threads with --no-threads"); + return false; + } + + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.get(0).isString()) { + JS_ReportErrorASCII(cx, "Invalid arguments"); + return false; + } + +#if defined(DEBUG) || defined(JS_OOM_BREAKPOINT) + if (cx->runningOOMTest) { + JS_ReportErrorASCII( + cx, "Can't create threads while running simulated OOM test"); + return false; + } +#endif + + if (!args[0].toString()->ensureLinear(cx)) { + return false; + } + + if (!workerThreadsLock) { + workerThreadsLock = js_new(mutexid::ShellWorkerThreads); + if (!workerThreadsLock) { + ReportOutOfMemory(cx); + return false; + } + } + + JSLinearString* str = &args[0].toString()->asLinear(); + + UniqueTwoByteChars chars(js_pod_malloc(str->length())); + if (!chars) { + ReportOutOfMemory(cx); + return false; + } + + CopyChars(chars.get(), *str); + + WorkerInput* input = js_new(JS_GetParentRuntime(cx), + std::move(chars), str->length()); + if (!input) { + ReportOutOfMemory(cx); + return false; + } + + Thread* thread; + { + AutoEnterOOMUnsafeRegion oomUnsafe; + thread = js_new( + Thread::Options().setStackSize(gMaxStackSize + 512 * 1024)); + if (!thread || !thread->init(WorkerMain, input)) { + oomUnsafe.crash("EvalInWorker"); + } + } + + AutoLockWorkerThreads alwt; + if (!workerThreads.append(thread)) { + ReportOutOfMemory(cx); + thread->join(); + js_delete(thread); + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool ShapeOf(JSContext* cx, unsigned argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.get(0).isObject()) { + JS_ReportErrorASCII(cx, "shapeOf: object expected"); + return false; + } + JSObject* obj = &args[0].toObject(); + args.rval().set(JS_NumberValue(double(uintptr_t(obj->shape()) >> 3))); + return true; +} + +static bool GroupOf(JSContext* cx, unsigned argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.get(0).isObject()) { + JS_ReportErrorASCII(cx, "groupOf: object expected"); + return false; + } + RootedObject obj(cx, &args[0].toObject()); + ObjectGroup* group = obj->group(); + args.rval().set(JS_NumberValue(double(uintptr_t(group) >> 3))); + return true; +} + +static bool UnwrappedObjectsHaveSameShape(JSContext* cx, unsigned argc, + JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.get(0).isObject() || !args.get(1).isObject()) { + JS_ReportErrorASCII(cx, "2 objects expected"); + return false; + } + + RootedObject obj1(cx, UncheckedUnwrap(&args[0].toObject())); + RootedObject obj2(cx, UncheckedUnwrap(&args[1].toObject())); + + args.rval().setBoolean(obj1->shape() == obj2->shape()); + return true; +} + +static bool Sleep_fn(JSContext* cx, unsigned argc, Value* vp) { + ShellContext* sc = GetShellContext(cx); + CallArgs args = CallArgsFromVp(argc, vp); + + TimeDuration duration = TimeDuration::FromSeconds(0.0); + if (args.length() > 0) { + double t_secs; + if (!ToNumber(cx, args[0], &t_secs)) { + return false; + } + if (mozilla::IsNaN(t_secs)) { + JS_ReportErrorASCII(cx, "sleep interval is not a number"); + return false; + } + + duration = TimeDuration::FromSeconds(std::max(0.0, t_secs)); + const TimeDuration MAX_TIMEOUT_INTERVAL = + TimeDuration::FromSeconds(MAX_TIMEOUT_SECONDS); + if (duration > MAX_TIMEOUT_INTERVAL) { + JS_ReportErrorASCII(cx, "Excessive sleep interval"); + return false; + } + } + { + LockGuard guard(sc->watchdogLock); + TimeStamp toWakeup = TimeStamp::Now() + duration; + for (;;) { + sc->sleepWakeup.wait_for(guard, duration); + if (sc->serviceInterrupt) { + break; + } + auto now = TimeStamp::Now(); + if (now >= toWakeup) { + break; + } + duration = toWakeup - now; + } + } + args.rval().setUndefined(); + return !sc->serviceInterrupt; +} + +static void KillWatchdog(JSContext* cx) { + ShellContext* sc = GetShellContext(cx); + Maybe thread; + + { + LockGuard guard(sc->watchdogLock); + std::swap(sc->watchdogThread, thread); + if (thread) { + // The watchdog thread becoming Nothing is its signal to exit. + sc->watchdogWakeup.notify_one(); + } + } + if (thread) { + thread->join(); + } + + MOZ_ASSERT(!sc->watchdogThread); +} + +static void WatchdogMain(JSContext* cx) { + ThisThread::SetName("JS Watchdog"); + + ShellContext* sc = GetShellContext(cx); + + { + LockGuard guard(sc->watchdogLock); + while (sc->watchdogThread) { + auto now = TimeStamp::Now(); + if (sc->watchdogTimeout && now >= sc->watchdogTimeout.value()) { + /* + * The timeout has just expired. Request an interrupt callback + * outside the lock. + */ + sc->watchdogTimeout = Nothing(); + { + UnlockGuard unlock(guard); + CancelExecution(cx); + } + + /* Wake up any threads doing sleep. */ + sc->sleepWakeup.notify_all(); + } else { + if (sc->watchdogTimeout) { + /* + * Time hasn't expired yet. Simulate an interrupt callback + * which doesn't abort execution. + */ + JS_RequestInterruptCallback(cx); + } + + TimeDuration sleepDuration = sc->watchdogTimeout + ? TimeDuration::FromSeconds(0.1) + : TimeDuration::Forever(); + sc->watchdogWakeup.wait_for(guard, sleepDuration); + } + } + } +} + +static bool ScheduleWatchdog(JSContext* cx, double t) { + ShellContext* sc = GetShellContext(cx); + + if (t <= 0) { + LockGuard guard(sc->watchdogLock); + sc->watchdogTimeout = Nothing(); + return true; + } + + auto interval = TimeDuration::FromSeconds(t); + auto timeout = TimeStamp::Now() + interval; + LockGuard guard(sc->watchdogLock); + if (!sc->watchdogThread) { + MOZ_ASSERT(!sc->watchdogTimeout); + sc->watchdogThread.emplace(); + AutoEnterOOMUnsafeRegion oomUnsafe; + if (!sc->watchdogThread->init(WatchdogMain, cx)) { + oomUnsafe.crash("watchdogThread.init"); + } + } else if (!sc->watchdogTimeout || timeout < sc->watchdogTimeout.value()) { + sc->watchdogWakeup.notify_one(); + } + sc->watchdogTimeout = Some(timeout); + return true; +} + +static void KillWorkerThreads(JSContext* cx) { + MOZ_ASSERT_IF(!CanUseExtraThreads(), workerThreads.empty()); + + if (!workerThreadsLock) { + MOZ_ASSERT(workerThreads.empty()); + return; + } + + while (true) { + // We need to leave the AutoLockWorkerThreads scope before we call + // js::Thread::join, to avoid deadlocks when AutoLockWorkerThreads is + // used by the worker thread. + Thread* thread; + { + AutoLockWorkerThreads alwt; + if (workerThreads.empty()) { + break; + } + thread = workerThreads.popCopy(); + } + thread->join(); + js_delete(thread); + } + + workerThreads.clearAndFree(); + + js_delete(workerThreadsLock); + workerThreadsLock = nullptr; +} + +static void CancelExecution(JSContext* cx) { + ShellContext* sc = GetShellContext(cx); + sc->serviceInterrupt = true; + JS_RequestInterruptCallback(cx); +} + +static bool SetTimeoutValue(JSContext* cx, double t) { + if (mozilla::IsNaN(t)) { + JS_ReportErrorASCII(cx, "timeout is not a number"); + return false; + } + const TimeDuration MAX_TIMEOUT_INTERVAL = + TimeDuration::FromSeconds(MAX_TIMEOUT_SECONDS); + if (TimeDuration::FromSeconds(t) > MAX_TIMEOUT_INTERVAL) { + JS_ReportErrorASCII(cx, "Excessive timeout value"); + return false; + } + GetShellContext(cx)->timeoutInterval = t; + if (!ScheduleWatchdog(cx, t)) { + JS_ReportErrorASCII(cx, "Failed to create the watchdog"); + return false; + } + return true; +} + +static bool Timeout(JSContext* cx, unsigned argc, Value* vp) { + ShellContext* sc = GetShellContext(cx); + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() == 0) { + args.rval().setNumber(sc->timeoutInterval); + return true; + } + + if (args.length() > 2) { + JS_ReportErrorASCII(cx, "Wrong number of arguments"); + return false; + } + + double t; + if (!ToNumber(cx, args[0], &t)) { + return false; + } + + if (args.length() > 1) { + RootedValue value(cx, args[1]); + if (!value.isObject() || !value.toObject().is()) { + JS_ReportErrorASCII(cx, "Second argument must be a timeout function"); + return false; + } + sc->interruptFunc = value; + sc->haveInterruptFunc = true; + } + + args.rval().setUndefined(); + return SetTimeoutValue(cx, t); +} + +static bool InterruptIf(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "Wrong number of arguments"); + return false; + } + + if (ToBoolean(args[0])) { + GetShellContext(cx)->serviceInterrupt = true; + JS_RequestInterruptCallback(cx); + } + + args.rval().setUndefined(); + return true; +} + +static bool InvokeInterruptCallbackWrapper(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "Wrong number of arguments"); + return false; + } + + GetShellContext(cx)->serviceInterrupt = true; + JS_RequestInterruptCallback(cx); + bool interruptRv = CheckForInterrupt(cx); + + // The interrupt handler could have set a pending exception. Since we call + // back into JS, don't have it see the pending exception. If we have an + // uncatchable exception that's not propagating a debug mode forced + // return, return. + if (!interruptRv && !cx->isExceptionPending() && + !cx->isPropagatingForcedReturn()) { + return false; + } + + JS::AutoSaveExceptionState savedExc(cx); + + FixedInvokeArgs<1> iargs(cx); + + iargs[0].setBoolean(interruptRv); + + RootedValue rv(cx); + if (!js::Call(cx, args[0], UndefinedHandleValue, iargs, &rv)) { + return false; + } + + args.rval().setUndefined(); + return interruptRv; +} + +static bool SetInterruptCallback(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "Wrong number of arguments"); + return false; + } + + RootedValue value(cx, args[0]); + if (!value.isObject() || !value.toObject().is()) { + JS_ReportErrorASCII(cx, "Argument must be a function"); + return false; + } + GetShellContext(cx)->interruptFunc = value; + GetShellContext(cx)->haveInterruptFunc = true; + + args.rval().setUndefined(); + return true; +} + +static bool SetJitCompilerOption(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (args.length() != 2) { + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments."); + return false; + } + + if (!args[0].isString()) { + ReportUsageErrorASCII(cx, callee, "First argument must be a String."); + return false; + } + + if (!args[1].isInt32()) { + ReportUsageErrorASCII(cx, callee, "Second argument must be an Int32."); + return false; + } + + // Disallow setting JIT options when there are worker threads, to avoid + // races. + if (workerThreadsLock) { + ReportUsageErrorASCII( + cx, callee, "Can't set JIT options when there are worker threads."); + return false; + } + + JSLinearString* strArg = JS_EnsureLinearString(cx, args[0].toString()); + if (!strArg) { + return false; + } + +#define JIT_COMPILER_MATCH(key, string) \ + else if (JS_LinearStringEqualsLiteral(strArg, string)) opt = \ + JSJITCOMPILER_##key; + + JSJitCompilerOption opt = JSJITCOMPILER_NOT_AN_OPTION; + if (false) { + } + JIT_COMPILER_OPTIONS(JIT_COMPILER_MATCH); +#undef JIT_COMPILER_MATCH + + if (opt == JSJITCOMPILER_NOT_AN_OPTION) { + ReportUsageErrorASCII( + cx, callee, + "First argument does not name a valid option (see jsapi.h)."); + return false; + } + + int32_t number = args[1].toInt32(); + if (number < 0) { + number = -1; + } + + // Disallow enabling or disabling the Baseline Interpreter at runtime. + // Enabling is a problem because the Baseline Interpreter code is only + // present if the interpreter was enabled when the JitRuntime was created. + // To support disabling we would have to discard all JitScripts. Furthermore, + // we really want JitOptions to be immutable after startup so it's better to + // use shell flags. + if (opt == JSJITCOMPILER_BASELINE_INTERPRETER_ENABLE && + bool(number) != jit::IsBaselineInterpreterEnabled()) { + JS_ReportErrorASCII(cx, + "Enabling or disabling the Baseline Interpreter at " + "runtime is not supported."); + return false; + } + + // Throw if disabling the JITs and there's JIT code on the stack, to avoid + // assertion failures. + if ((opt == JSJITCOMPILER_BASELINE_ENABLE || + opt == JSJITCOMPILER_ION_ENABLE) && + number == 0) { + js::jit::JitActivationIterator iter(cx); + if (!iter.done()) { + JS_ReportErrorASCII(cx, + "Can't turn off JITs with JIT code on the stack."); + return false; + } + } + + // Throw if trying to disable all the Wasm compilers. The logic here is that + // if we're trying to disable a compiler that is currently enabled and that is + // the last compiler enabled then we must throw. + // + // Note that this check does not prevent an error from being thrown later. + // Actual compiler availability is dynamic and depends on other conditions, + // such as other options set and whether a debugger is present. + if ((opt == JSJITCOMPILER_WASM_JIT_BASELINE || + opt == JSJITCOMPILER_WASM_JIT_OPTIMIZING) && + number == 0) { + uint32_t baseline, optimizing; + MOZ_ALWAYS_TRUE(JS_GetGlobalJitCompilerOption( + cx, JSJITCOMPILER_WASM_JIT_BASELINE, &baseline)); + MOZ_ALWAYS_TRUE(JS_GetGlobalJitCompilerOption( + cx, JSJITCOMPILER_WASM_JIT_OPTIMIZING, &optimizing)); + if (baseline + optimizing == 1) { + if ((opt == JSJITCOMPILER_WASM_JIT_BASELINE && baseline) || + (opt == JSJITCOMPILER_WASM_JIT_OPTIMIZING && optimizing)) { + JS_ReportErrorASCII( + cx, + "Disabling all the Wasm compilers at runtime is not supported."); + return false; + } + } + } + + // JIT compiler options are process-wide, so we have to stop off-thread + // compilations for all runtimes to avoid races. + WaitForAllHelperThreads(); + + // Only release JIT code for the current runtime because there's no good + // way to discard code for other runtimes. + ReleaseAllJITCode(cx->runtime()->defaultFreeOp()); + + JS_SetGlobalJitCompilerOption(cx, opt, uint32_t(number)); + + args.rval().setUndefined(); + return true; +} + +static bool EnableLastWarning(JSContext* cx, unsigned argc, Value* vp) { + ShellContext* sc = GetShellContext(cx); + CallArgs args = CallArgsFromVp(argc, vp); + + sc->lastWarningEnabled = true; + sc->lastWarning.setNull(); + + args.rval().setUndefined(); + return true; +} + +static bool DisableLastWarning(JSContext* cx, unsigned argc, Value* vp) { + ShellContext* sc = GetShellContext(cx); + CallArgs args = CallArgsFromVp(argc, vp); + + sc->lastWarningEnabled = false; + sc->lastWarning.setNull(); + + args.rval().setUndefined(); + return true; +} + +static bool GetLastWarning(JSContext* cx, unsigned argc, Value* vp) { + ShellContext* sc = GetShellContext(cx); + CallArgs args = CallArgsFromVp(argc, vp); + + if (!sc->lastWarningEnabled) { + JS_ReportErrorASCII(cx, "Call enableLastWarning first."); + return false; + } + + if (!JS_WrapValue(cx, &sc->lastWarning)) { + return false; + } + + args.rval().set(sc->lastWarning); + return true; +} + +static bool ClearLastWarning(JSContext* cx, unsigned argc, Value* vp) { + ShellContext* sc = GetShellContext(cx); + CallArgs args = CallArgsFromVp(argc, vp); + + if (!sc->lastWarningEnabled) { + JS_ReportErrorASCII(cx, "Call enableLastWarning first."); + return false; + } + + sc->lastWarning.setNull(); + + args.rval().setUndefined(); + return true; +} + +#if defined(DEBUG) || defined(JS_JITSPEW) +static bool StackDump(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!gOutFile->isOpen()) { + JS_ReportErrorASCII(cx, "output file is closed"); + return false; + } + + bool showArgs = ToBoolean(args.get(0)); + bool showLocals = ToBoolean(args.get(1)); + bool showThisProps = ToBoolean(args.get(2)); + + JS::UniqueChars buf = + JS::FormatStackDump(cx, showArgs, showLocals, showThisProps); + if (!buf) { + fputs("Failed to format JavaScript stack for dump\n", gOutFile->fp); + JS_ClearPendingException(cx); + } else { + fputs(buf.get(), gOutFile->fp); + } + + args.rval().setUndefined(); + return true; +} +#endif + +static bool StackPointerInfo(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Copy the truncated stack pointer to the result. This value is not used + // as a pointer but as a way to measure frame-size from JS. + args.rval().setInt32(int32_t(reinterpret_cast(&args) & 0xfffffff)); + return true; +} + +static bool Elapsed(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() == 0) { + double d = PRMJ_Now() - GetShellContext(cx)->startTime; + args.rval().setDouble(d); + return true; + } + JS_ReportErrorASCII(cx, "Wrong number of arguments"); + return false; +} + +static bool Compile(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "compile", 1)) { + return false; + } + if (!args[0].isString()) { + const char* typeName = InformalValueTypeName(args[0]); + JS_ReportErrorASCII(cx, "expected string to compile, got %s", typeName); + return false; + } + + RootedObject global(cx, JS::CurrentGlobalOrNull(cx)); + JSString* scriptContents = args[0].toString(); + + AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, scriptContents)) { + return false; + } + + JS::CompileOptions options(cx); + options.setIntroductionType("js shell compile") + .setFileAndLine("", 1) + .setIsRunOnce(true) + .setNoScriptRval(true); + + JS::SourceText srcBuf; + if (!srcBuf.init(cx, stableChars.twoByteRange().begin().get(), + scriptContents->length(), JS::SourceOwnership::Borrowed)) { + return false; + } + + RootedScript script(cx, JS::Compile(cx, options, srcBuf)); + if (!script) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +static ShellCompartmentPrivate* EnsureShellCompartmentPrivate(JSContext* cx) { + Compartment* comp = cx->compartment(); + auto priv = + static_cast(JS_GetCompartmentPrivate(comp)); + if (!priv) { + priv = cx->new_(); + JS_SetCompartmentPrivate(cx->compartment(), priv); + } + return priv; +} + +static bool ParseModule(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "parseModule", 1)) { + return false; + } + + if (!args[0].isString()) { + const char* typeName = InformalValueTypeName(args[0]); + JS_ReportErrorASCII(cx, "expected string to compile, got %s", typeName); + return false; + } + + JSString* scriptContents = args[0].toString(); + + UniqueChars filename; + CompileOptions options(cx); + if (args.length() > 1) { + if (!args[1].isString()) { + const char* typeName = InformalValueTypeName(args[1]); + JS_ReportErrorASCII(cx, "expected filename string, got %s", typeName); + return false; + } + + RootedString str(cx, args[1].toString()); + filename = JS_EncodeStringToLatin1(cx, str); + if (!filename) { + return false; + } + + options.setFileAndLine(filename.get(), 1); + } else { + options.setFileAndLine("", 1); + } + options.setModule(); + + AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, scriptContents)) { + return false; + } + + const char16_t* chars = stableChars.twoByteRange().begin().get(); + JS::SourceText srcBuf; + if (!srcBuf.init(cx, chars, scriptContents->length(), + JS::SourceOwnership::Borrowed)) { + return false; + } + + RootedObject module(cx, frontend::CompileModule(cx, options, srcBuf)); + if (!module) { + return false; + } + + args.rval().setObject(*module); + return true; +} + +// A JSObject that holds XDRBuffer. +class XDRBufferObject : public NativeObject { + static const size_t VECTOR_SLOT = 0; + static const unsigned RESERVED_SLOTS = 1; + + public: + static const JSClassOps classOps_; + static const JSClass class_; + + inline static MOZ_MUST_USE XDRBufferObject* create(JSContext* cx, + JS::TranscodeBuffer&& buf); + + JS::TranscodeBuffer* data() const { + Value value = getReservedSlot(VECTOR_SLOT); + auto buf = static_cast(value.toPrivate()); + MOZ_ASSERT(buf); + return buf; + } + + bool hasData() const { + // Data may not be present if we hit OOM in initialization. + return !getReservedSlot(VECTOR_SLOT).isUndefined(); + } + + static void finalize(JSFreeOp* fop, JSObject* obj); +}; + +/*static */ const JSClassOps XDRBufferObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + XDRBufferObject::finalize, // finalize + nullptr, // call + nullptr, // hasInstance + nullptr, // construct + nullptr, // trace +}; + +/*static */ const JSClass XDRBufferObject::class_ = { + "XDRBufferObject", + JSCLASS_HAS_RESERVED_SLOTS(XDRBufferObject::RESERVED_SLOTS) | + JSCLASS_BACKGROUND_FINALIZE, + &XDRBufferObject::classOps_}; + +XDRBufferObject* XDRBufferObject::create(JSContext* cx, + JS::TranscodeBuffer&& buf) { + XDRBufferObject* bufObj = + NewObjectWithGivenProto(cx, nullptr); + if (!bufObj) { + return nullptr; + } + + auto heapBuf = cx->make_unique(std::move(buf)); + if (!heapBuf) { + return nullptr; + } + + size_t len = heapBuf->length(); + InitReservedSlot(bufObj, VECTOR_SLOT, heapBuf.release(), len, + MemoryUse::XDRBufferElements); + + return bufObj; +} + +void XDRBufferObject::finalize(JSFreeOp* fop, JSObject* obj) { + XDRBufferObject* buf = &obj->as(); + if (buf->hasData()) { + fop->delete_(buf, buf->data(), buf->data()->length(), + MemoryUse::XDRBufferElements); + } +} + +static bool CodeModule(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "codeModule", 1)) { + return false; + } + + if (!args[0].isObject() || !args[0].toObject().is()) { + const char* typeName = InformalValueTypeName(args[0]); + JS_ReportErrorASCII(cx, "expected module object, got %s", typeName); + return false; + } + + RootedModuleObject modObject(cx, &args[0].toObject().as()); + if (modObject->status() >= MODULE_STATUS_LINKING) { + JS_ReportErrorASCII(cx, "cannot encode module after instantiation."); + return false; + } + + JS::TranscodeBuffer buf; + XDREncoder xdrEncoder_(cx, buf); + XDRResult res = xdrEncoder_.codeModuleObject(&modObject); + if (res.isErr()) { + return false; + } + + XDRBufferObject* xdrBuf = XDRBufferObject::create(cx, std::move(buf)); + if (!xdrBuf) { + return false; + } + args.rval().setObject(*xdrBuf); + return true; +} + +static bool DecodeModule(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "decodeModule", 1)) { + return false; + } + + if (!args[0].isObject() || !args[0].toObject().is()) { + const char* typeName = InformalValueTypeName(args[0]); + JS_ReportErrorASCII(cx, "expected XDRBufferObject to compile, got %s", + typeName); + return false; + } + + JS::CompileOptions options(cx); + options.setModule(); + + XDRDecoder xdrDecoder_(cx, &options, + *args[0].toObject().as().data()); + RootedModuleObject modObject(cx, nullptr); + XDRResult res = xdrDecoder_.codeModuleObject(&modObject); + if (res.isErr()) { + return false; + } + + if (!ModuleObject::Freeze(cx, modObject)) { + return false; + } + + args.rval().setObject(*modObject); + return true; +} + +static bool RegisterModule(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "registerModule", 2)) { + return false; + } + + if (!args[0].isString()) { + const char* typeName = InformalValueTypeName(args[0]); + JS_ReportErrorASCII(cx, "expected string, got %s", typeName); + return false; + } + + if (!args[1].isObject() || !args[1].toObject().is()) { + const char* typeName = InformalValueTypeName(args[0]); + JS_ReportErrorASCII(cx, "expected module, got %s", typeName); + return false; + } + + ShellContext* sc = GetShellContext(cx); + RootedString specifier(cx, args[0].toString()); + RootedModuleObject module(cx, &args[1].toObject().as()); + + if (!sc->moduleLoader->registerTestModule(cx, specifier, module)) { + return false; + } + + args.rval().setObject(*module); + return true; +} + +enum class DumpType { + ParseNode, + Stencil, +}; + +template +static bool DumpAST(JSContext* cx, const JS::ReadOnlyCompileOptions& options, + const Unit* units, size_t length, + js::frontend::CompilationStencil& stencil, + js::frontend::CompilationState& compilationState, + js::frontend::ParseGoal goal) { + using namespace js::frontend; + + Parser parser(cx, options, units, length, false, + stencil, compilationState, nullptr, + nullptr); + if (!parser.checkOptions()) { + return false; + } + + // Emplace the top-level stencil. + MOZ_ASSERT(compilationState.scriptData.length() == + CompilationStencil::TopLevelIndex); + if (!compilationState.scriptData.emplaceBack()) { + ReportOutOfMemory(cx); + return false; + } + if (!compilationState.scriptExtra.emplaceBack()) { + ReportOutOfMemory(cx); + return false; + } + + js::frontend::ParseNode* pn; + if (goal == frontend::ParseGoal::Script) { + pn = parser.parse(); + } else { + if (!GlobalObject::ensureModulePrototypesCreated(cx, cx->global())) { + return false; + } + + ModuleBuilder builder(cx, &parser); + + SourceExtent extent = SourceExtent::makeGlobalExtent(length); + ModuleSharedContext modulesc(cx, stencil, builder, extent); + pn = parser.moduleBody(&modulesc); + } + + if (!pn) { + return false; + } + +#if defined(DEBUG) + js::Fprinter out(stderr); + DumpParseTree(pn, out); +#endif + + return true; +} + +template +static bool DumpStencil(JSContext* cx, + const JS::ReadOnlyCompileOptions& options, + const Unit* units, size_t length, + js::frontend::ParseGoal goal) { + Rooted> stencil(cx); + + JS::SourceText srcBuf; + if (!srcBuf.init(cx, units, length, JS::SourceOwnership::Borrowed)) { + return false; + } + + if (goal == frontend::ParseGoal::Script) { + stencil = frontend::CompileGlobalScriptToStencil(cx, options, srcBuf, + ScopeKind::Global); + } else { + stencil = frontend::ParseModuleToStencil(cx, options, srcBuf); + } + + if (!stencil) { + return false; + } + +#if defined(DEBUG) || defined(JS_JITSPEW) + stencil->dump(); +#endif + + return true; +} + +static bool FrontendTest(JSContext* cx, unsigned argc, Value* vp, + const char* funcName, DumpType dumpType) { + using namespace js::frontend; + + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, funcName, 1)) { + return false; + } + if (!args[0].isString()) { + const char* typeName = InformalValueTypeName(args[0]); + JS_ReportErrorASCII(cx, "expected string to parse, got %s", typeName); + return false; + } + + frontend::ParseGoal goal = frontend::ParseGoal::Script; +#ifdef JS_ENABLE_SMOOSH + bool smoosh = false; +#endif + bool forceFullParse = false; + + if (args.length() >= 2) { + if (!args[1].isObject()) { + const char* typeName = InformalValueTypeName(args[1]); + JS_ReportErrorASCII(cx, "expected object (options) to parse, got %s", + typeName); + return false; + } + RootedObject objOptions(cx, &args[1].toObject()); + + RootedValue optionModule(cx); + if (!JS_GetProperty(cx, objOptions, "module", &optionModule)) { + return false; + } + + if (optionModule.isBoolean()) { + if (optionModule.toBoolean()) { + goal = frontend::ParseGoal::Module; + } + } else if (!optionModule.isUndefined()) { + const char* typeName = InformalValueTypeName(optionModule); + JS_ReportErrorASCII(cx, "option `module` should be a boolean, got %s", + typeName); + return false; + } + + RootedValue forceFullParseValue(cx); + if (!JS_GetProperty(cx, objOptions, "forceFullParse", + &forceFullParseValue)) { + return false; + } + + if (forceFullParseValue.isBoolean() && forceFullParseValue.toBoolean()) { + forceFullParse = true; + } + +#ifdef JS_ENABLE_SMOOSH + bool found = false; + if (!JS_HasProperty(cx, objOptions, "rustFrontend", &found)) { + return false; + } + if (found) { + JS_ReportErrorASCII(cx, "'rustFrontend' option is renamed to 'smoosh'"); + return false; + } + + RootedValue optionSmoosh(cx); + if (!JS_GetProperty(cx, objOptions, "smoosh", &optionSmoosh)) { + return false; + } + + if (optionSmoosh.isBoolean()) { + smoosh = optionSmoosh.toBoolean(); + } else if (!optionSmoosh.isUndefined()) { + const char* typeName = InformalValueTypeName(optionSmoosh); + JS_ReportErrorASCII(cx, "option `smoosh` should be a boolean, got %s", + typeName); + return false; + } +#endif // JS_ENABLE_SMOOSH + } + + JSString* scriptContents = args[0].toString(); + RootedLinearString linearString(cx, scriptContents->ensureLinear(cx)); + if (!linearString) { + return false; + } + + bool isAscii = false; + if (linearString->hasLatin1Chars()) { + JS::AutoCheckCannotGC nogc; + isAscii = JS::StringIsASCII(mozilla::Span( + reinterpret_cast(linearString->latin1Chars(nogc)), + linearString->length())); + } + + AutoStableStringChars stableChars(cx); + if (isAscii) { + if (!stableChars.init(cx, scriptContents)) { + return false; + } + MOZ_ASSERT(stableChars.isLatin1()); + } else { + if (!stableChars.initTwoByte(cx, scriptContents)) { + return false; + } + } + + size_t length = scriptContents->length(); +#ifdef JS_ENABLE_SMOOSH + if (dumpType == DumpType::ParseNode) { + if (smoosh) { + if (isAscii) { + const Latin1Char* chars = stableChars.latin1Range().begin().get(); + + if (goal == frontend::ParseGoal::Script) { + if (!SmooshParseScript(cx, chars, length)) { + return false; + } + } else { + if (!SmooshParseModule(cx, chars, length)) { + return false; + } + } + args.rval().setUndefined(); + return true; + } + JS_ReportErrorASCII(cx, + "SmooshMonkey does not support non-ASCII chars yet"); + return false; + } + } +#endif // JS_ENABLE_SMOOSH + + CompileOptions options(cx); + options.setIntroductionType("js shell parse") + .setFileAndLine("", 1) + .setIsRunOnce(true) + .setNoScriptRval(true); + + if (forceFullParse) { + options.setForceFullParse(); + } + + if (goal == frontend::ParseGoal::Module) { + // See frontend::CompileModule. + options.setForceStrictMode(); + options.allowHTMLComments = false; + } + + if (dumpType == DumpType::Stencil) { +#ifdef JS_ENABLE_SMOOSH + if (smoosh) { + if (isAscii) { + if (goal == frontend::ParseGoal::Script) { + const Latin1Char* latin1 = stableChars.latin1Range().begin().get(); + auto utf8 = reinterpret_cast(latin1); + JS::SourceText srcBuf; + if (!srcBuf.init(cx, utf8, length, JS::SourceOwnership::Borrowed)) { + return false; + } + + bool unimplemented; + Rooted> stencil( + cx, Smoosh::compileGlobalScriptToStencil(cx, options, srcBuf, + &unimplemented)); + if (!stencil) { + return false; + } + +# ifdef DEBUG + stencil->dump(); +# endif + } else { + JS_ReportErrorASCII(cx, + "SmooshMonkey does not support module stencil"); + return false; + } + args.rval().setUndefined(); + return true; + } + JS_ReportErrorASCII(cx, + "SmooshMonkey does not support non-ASCII chars yet"); + return false; + } +#endif // JS_ENABLE_SMOOSH + + if (isAscii) { + const Latin1Char* latin1 = stableChars.latin1Range().begin().get(); + auto utf8 = reinterpret_cast(latin1); + if (!DumpStencil(cx, options, utf8, length, goal)) { + return false; + } + } else { + MOZ_ASSERT(stableChars.isTwoByte()); + const char16_t* chars = stableChars.twoByteRange().begin().get(); + if (!DumpStencil(cx, options, chars, length, goal)) { + return false; + } + } + + args.rval().setUndefined(); + return true; + } + + js::Rooted stencil( + cx, js::frontend::CompilationStencil(cx, options)); + if (goal == frontend::ParseGoal::Script) { + if (!stencil.get().input.initForGlobal(cx)) { + return false; + } + } else { + if (!stencil.get().input.initForModule(cx)) { + return false; + } + } + + LifoAllocScope allocScope(&cx->tempLifoAlloc()); + frontend::CompilationState compilationState(cx, allocScope, options, + stencil.get()); + + if (isAscii) { + const Latin1Char* latin1 = stableChars.latin1Range().begin().get(); + auto utf8 = reinterpret_cast(latin1); + if (!DumpAST(cx, options, utf8, length, stencil.get(), + compilationState, goal)) { + return false; + } + } else { + MOZ_ASSERT(stableChars.isTwoByte()); + const char16_t* chars = stableChars.twoByteRange().begin().get(); + if (!DumpAST(cx, options, chars, length, stencil.get(), + compilationState, goal)) { + return false; + } + } + args.rval().setUndefined(); + return true; +} + +static bool DumpStencil(JSContext* cx, unsigned argc, Value* vp) { + return FrontendTest(cx, argc, vp, "dumpStencil", DumpType::Stencil); +} + +static bool Parse(JSContext* cx, unsigned argc, Value* vp) { + // Parse returns local scope information with variables ordered + // differently, depending on the underlying JIT implementation. + if (js::SupportDifferentialTesting()) { + JS_ReportErrorASCII(cx, + "Function not available in differential testing mode."); + return false; + } + + return FrontendTest(cx, argc, vp, "parse", DumpType::ParseNode); +} + +static bool SyntaxParse(JSContext* cx, unsigned argc, Value* vp) { + using namespace js::frontend; + + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "syntaxParse", 1)) { + return false; + } + if (!args[0].isString()) { + const char* typeName = InformalValueTypeName(args[0]); + JS_ReportErrorASCII(cx, "expected string to parse, got %s", typeName); + return false; + } + + JSString* scriptContents = args[0].toString(); + + CompileOptions options(cx); + options.setIntroductionType("js shell syntaxParse") + .setFileAndLine("", 1); + + AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, scriptContents)) { + return false; + } + + const char16_t* chars = stableChars.twoByteRange().begin().get(); + size_t length = scriptContents->length(); + + js::Rooted stencil( + cx, js::frontend::CompilationStencil(cx, options)); + if (!stencil.get().input.initForGlobal(cx)) { + return false; + } + + LifoAllocScope allocScope(&cx->tempLifoAlloc()); + frontend::CompilationState compilationState(cx, allocScope, options, + stencil.get()); + + Parser parser( + cx, options, chars, length, false, stencil.get(), compilationState, + nullptr, nullptr); + if (!parser.checkOptions()) { + return false; + } + + bool succeeded = parser.parse(); + if (cx->isExceptionPending()) { + return false; + } + + if (!succeeded && !parser.hadAbortedSyntaxParse()) { + // If no exception is posted, either there was an OOM or a language + // feature unhandled by the syntax parser was encountered. + MOZ_ASSERT(cx->runtime()->hadOutOfMemory); + return false; + } + + args.rval().setBoolean(succeeded); + return true; +} + +static void OffThreadCompileScriptCallback(JS::OffThreadToken* token, + void* callbackData) { + auto job = static_cast(callbackData); + job->markDone(token); +} + +static bool OffThreadCompileScript(JSContext* cx, unsigned argc, Value* vp) { + if (!CanUseExtraThreads()) { + JS_ReportErrorASCII(cx, + "Can't use offThreadCompileScript with --no-threads"); + return false; + } + + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "offThreadCompileScript", 1)) { + return false; + } + if (!args[0].isString()) { + const char* typeName = InformalValueTypeName(args[0]); + JS_ReportErrorASCII(cx, "expected string to parse, got %s", typeName); + return false; + } + + UniqueChars fileNameBytes; + CompileOptions options(cx); + options.setIntroductionType("js shell offThreadCompileScript") + .setFileAndLine("", 1); + + if (args.length() >= 2) { + if (args[1].isPrimitive()) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "evaluate"); + return false; + } + + RootedObject opts(cx, &args[1].toObject()); + if (!ParseCompileOptions(cx, options, opts, fileNameBytes)) { + return false; + } + } + + // These option settings must override whatever the caller requested. + options.setIsRunOnce(true).setSourceIsLazy(false); + + // We assume the caller wants caching if at all possible, ignoring + // heuristics that make sense for a real browser. + options.forceAsync = true; + + JSString* scriptContents = args[0].toString(); + AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, scriptContents)) { + return false; + } + + size_t length = scriptContents->length(); + const char16_t* chars = stableChars.twoByteChars(); + + // Make sure we own the string's chars, so that they are not freed before + // the compilation is finished. + UniqueTwoByteChars ownedChars; + if (stableChars.maybeGiveOwnershipToCaller()) { + ownedChars.reset(const_cast(chars)); + } else { + ownedChars.reset(cx->pod_malloc(length)); + if (!ownedChars) { + return false; + } + + mozilla::PodCopy(ownedChars.get(), chars, length); + } + + if (!JS::CanCompileOffThread(cx, options, length)) { + JS_ReportErrorASCII(cx, "cannot compile code on worker thread"); + return false; + } + + OffThreadJob* job = NewOffThreadJob( + cx, ScriptKind::Script, OffThreadJob::Source(std::move(ownedChars))); + if (!job) { + return false; + } + + JS::SourceText srcBuf; + if (!srcBuf.init(cx, job->sourceChars(), length, + JS::SourceOwnership::Borrowed) || + !JS::CompileOffThread(cx, options, srcBuf, OffThreadCompileScriptCallback, + job)) { + job->cancel(); + DeleteOffThreadJob(cx, job); + return false; + } + + args.rval().setInt32(job->id); + return true; +} + +static bool runOffThreadScript(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (OffThreadParsingMustWaitForGC(cx->runtime())) { + gc::FinishGC(cx); + } + + OffThreadJob* job = + LookupOffThreadJobForArgs(cx, ScriptKind::Script, args, 0); + if (!job) { + return false; + } + + JS::OffThreadToken* token = job->waitUntilDone(cx); + MOZ_ASSERT(token); + + RootedScript script(cx, JS::FinishOffThreadScript(cx, token)); + DeleteOffThreadJob(cx, job); + if (!script) { + return false; + } + + return JS_ExecuteScript(cx, script, args.rval()); +} + +static bool OffThreadCompileModule(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1 || !args[0].isString()) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "offThreadCompileModule"); + return false; + } + + UniqueChars fileNameBytes; + CompileOptions options(cx); + options.setIntroductionType("js shell offThreadCompileModule") + .setFileAndLine("", 1); + options.setIsRunOnce(true).setSourceIsLazy(false); + options.forceAsync = true; + + JSString* scriptContents = args[0].toString(); + AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, scriptContents)) { + return false; + } + + size_t length = scriptContents->length(); + const char16_t* chars = stableChars.twoByteChars(); + + // Make sure we own the string's chars, so that they are not freed before + // the compilation is finished. + UniqueTwoByteChars ownedChars; + if (stableChars.maybeGiveOwnershipToCaller()) { + ownedChars.reset(const_cast(chars)); + } else { + ownedChars.reset(cx->pod_malloc(length)); + if (!ownedChars) { + return false; + } + + mozilla::PodCopy(ownedChars.get(), chars, length); + } + + if (!JS::CanCompileOffThread(cx, options, length)) { + JS_ReportErrorASCII(cx, "cannot compile code on worker thread"); + return false; + } + + OffThreadJob* job = NewOffThreadJob( + cx, ScriptKind::Module, OffThreadJob::Source(std::move(ownedChars))); + if (!job) { + return false; + } + + JS::SourceText srcBuf; + if (!srcBuf.init(cx, job->sourceChars(), length, + JS::SourceOwnership::Borrowed) || + !JS::CompileOffThreadModule(cx, options, srcBuf, + OffThreadCompileScriptCallback, job)) { + job->cancel(); + DeleteOffThreadJob(cx, job); + return false; + } + + args.rval().setInt32(job->id); + return true; +} + +static bool FinishOffThreadModule(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (OffThreadParsingMustWaitForGC(cx->runtime())) { + gc::FinishGC(cx); + } + + OffThreadJob* job = + LookupOffThreadJobForArgs(cx, ScriptKind::Module, args, 0); + if (!job) { + return false; + } + + JS::OffThreadToken* token = job->waitUntilDone(cx); + MOZ_ASSERT(token); + + RootedObject module(cx, JS::FinishOffThreadModule(cx, token)); + DeleteOffThreadJob(cx, job); + if (!module) { + return false; + } + + args.rval().setObject(*module); + return true; +} + +static bool OffThreadDecodeScript(JSContext* cx, unsigned argc, Value* vp) { + if (!CanUseExtraThreads()) { + JS_ReportErrorASCII(cx, + "Can't use offThreadDecodeScript with --no-threads"); + return false; + } + + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "offThreadDecodeScript", 1)) { + return false; + } + if (!args[0].isObject() || !CacheEntry_isCacheEntry(&args[0].toObject())) { + const char* typeName = InformalValueTypeName(args[0]); + JS_ReportErrorASCII(cx, "expected cache entry, got %s", typeName); + return false; + } + RootedObject cacheEntry(cx, &args[0].toObject()); + + UniqueChars fileNameBytes; + CompileOptions options(cx); + options.setIntroductionType("js shell offThreadDecodeScript") + .setFileAndLine("", 1); + // NOTE: If --off-thread-parse-global is not used, input can be either script + // for saveBytecode, or stencil for saveIncrementalBytecode. + options.useOffThreadParseGlobal = + CacheEntry_getKind(cx, cacheEntry) == BytecodeCacheKind::Script; + options.useStencilXDR = + CacheEntry_getKind(cx, cacheEntry) == BytecodeCacheKind::Stencil; + + if (args.length() >= 2) { + if (args[1].isPrimitive()) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "evaluate"); + return false; + } + + RootedObject opts(cx, &args[1].toObject()); + if (!ParseCompileOptions(cx, options, opts, fileNameBytes)) { + return false; + } + } + + // These option settings must override whatever the caller requested, and + // this should match `Evaluate` that encodes the script. + options.setIsRunOnce(false).setSourceIsLazy(false); + + // We assume the caller wants caching if at all possible, ignoring + // heuristics that make sense for a real browser. + options.forceAsync = true; + + JS::TranscodeBuffer loadBuffer; + size_t loadLength = 0; + uint8_t* loadData = nullptr; + loadData = CacheEntry_getBytecode(cx, cacheEntry, &loadLength); + if (!loadData) { + return false; + } + if (!loadBuffer.append(loadData, loadLength)) { + JS_ReportOutOfMemory(cx); + return false; + } + + if (!JS::CanDecodeOffThread(cx, options, loadLength)) { + JS_ReportErrorASCII(cx, "cannot compile code on worker thread"); + return false; + } + + OffThreadJob* job = + NewOffThreadJob(cx, ScriptKind::DecodeScript, + OffThreadJob::Source(std::move(loadBuffer))); + if (!job) { + return false; + } + + if (!JS::DecodeOffThreadScript(cx, options, job->xdrBuffer(), 0, + OffThreadCompileScriptCallback, job)) { + job->cancel(); + DeleteOffThreadJob(cx, job); + return false; + } + + args.rval().setInt32(job->id); + return true; +} + +static bool runOffThreadDecodedScript(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (OffThreadParsingMustWaitForGC(cx->runtime())) { + gc::FinishGC(cx); + } + + OffThreadJob* job = + LookupOffThreadJobForArgs(cx, ScriptKind::DecodeScript, args, 0); + if (!job) { + return false; + } + + JS::OffThreadToken* token = job->waitUntilDone(cx); + MOZ_ASSERT(token); + + RootedScript script(cx, JS::FinishOffThreadScriptDecoder(cx, token)); + DeleteOffThreadJob(cx, job); + if (!script) { + return false; + } + + return JS_ExecuteScript(cx, script, args.rval()); +} + +class AutoCStringVector { + Vector argv_; + + public: + explicit AutoCStringVector(JSContext* cx) : argv_(cx) {} + ~AutoCStringVector() { + for (size_t i = 0; i < argv_.length(); i++) { + js_free(argv_[i]); + } + } + bool append(UniqueChars&& arg) { + if (!argv_.append(arg.get())) { + return false; + } + + // Now owned by this vector. + mozilla::Unused << arg.release(); + return true; + } + char* const* get() const { return argv_.begin(); } + size_t length() const { return argv_.length(); } + char* operator[](size_t i) const { return argv_[i]; } + void replace(size_t i, UniqueChars arg) { + js_free(argv_[i]); + argv_[i] = arg.release(); + } +}; + +#if defined(XP_WIN) +static bool EscapeForShell(JSContext* cx, AutoCStringVector& argv) { + // Windows will break arguments in argv by various spaces, so we wrap each + // argument in quotes and escape quotes within. Even with quotes, \ will be + // treated like an escape character, so inflate each \ to \\. + + for (size_t i = 0; i < argv.length(); i++) { + if (!argv[i]) { + continue; + } + + size_t newLen = 3; // quotes before and after and null-terminator + for (char* p = argv[i]; *p; p++) { + newLen++; + if (*p == '\"' || *p == '\\') { + newLen++; + } + } + + auto escaped = cx->make_pod_array(newLen); + if (!escaped) { + return false; + } + + char* src = argv[i]; + char* dst = escaped.get(); + *dst++ = '\"'; + while (*src) { + if (*src == '\"' || *src == '\\') { + *dst++ = '\\'; + } + *dst++ = *src++; + } + *dst++ = '\"'; + *dst++ = '\0'; + MOZ_ASSERT(escaped.get() + newLen == dst); + + argv.replace(i, std::move(escaped)); + } + return true; +} +#endif + +static bool ReadAll(int fd, wasm::Bytes* bytes) { + size_t lastLength = bytes->length(); + while (true) { + static const int ChunkSize = 64 * 1024; + if (!bytes->growBy(ChunkSize)) { + return false; + } + + intptr_t readCount; + while (true) { + readCount = read(fd, bytes->begin() + lastLength, ChunkSize); + if (readCount >= 0) { + break; + } + if (errno != EINTR) { + return false; + } + } + + if (readCount < ChunkSize) { + bytes->shrinkTo(lastLength + readCount); + if (readCount == 0) { + return true; + } + } + + lastLength = bytes->length(); + } +} + +static bool WriteAll(int fd, const uint8_t* bytes, size_t length) { + while (length > 0) { + int written = write(fd, bytes, length); + if (written < 0) { + if (errno == EINTR) { + continue; + } + return false; + } + MOZ_ASSERT(unsigned(written) <= length); + length -= written; + bytes += written; + } + + return true; +} + +class AutoPipe { + int fds_[2]; + + public: + AutoPipe() { + fds_[0] = -1; + fds_[1] = -1; + } + + ~AutoPipe() { + if (fds_[0] != -1) { + close(fds_[0]); + } + if (fds_[1] != -1) { + close(fds_[1]); + } + } + + bool init() { +#ifdef XP_WIN + return !_pipe(fds_, 4096, O_BINARY); +#else + return !pipe(fds_); +#endif + } + + int reader() const { + MOZ_ASSERT(fds_[0] != -1); + return fds_[0]; + } + + int writer() const { + MOZ_ASSERT(fds_[1] != -1); + return fds_[1]; + } + + void closeReader() { + MOZ_ASSERT(fds_[0] != -1); + close(fds_[0]); + fds_[0] = -1; + } + + void closeWriter() { + MOZ_ASSERT(fds_[1] != -1); + close(fds_[1]); + fds_[1] = -1; + } +}; + +int shell::sArgc; +char** shell::sArgv; + +static const char sWasmCompileAndSerializeFlag[] = + "--wasm-compile-and-serialize"; +static Vector sCompilerProcessFlags; + +static bool CompileAndSerializeInSeparateProcess(JSContext* cx, + const uint8_t* bytecode, + size_t bytecodeLength, + wasm::Bytes* serialized) { + AutoPipe stdIn, stdOut; + if (!stdIn.init() || !stdOut.init()) { + return false; + } + + AutoCStringVector argv(cx); + + UniqueChars argv0 = DuplicateString(cx, sArgv[0]); + if (!argv0 || !argv.append(std::move(argv0))) { + return false; + } + + // Put compiler flags first since they must precede the non-option + // file-descriptor args (passed on Windows, below). + for (unsigned i = 0; i < sCompilerProcessFlags.length(); i++) { + UniqueChars flags = DuplicateString(cx, sCompilerProcessFlags[i]); + if (!flags || !argv.append(std::move(flags))) { + return false; + } + } + + UniqueChars arg; + + arg = DuplicateString(sWasmCompileAndSerializeFlag); + if (!arg || !argv.append(std::move(arg))) { + return false; + } + +#ifdef XP_WIN + // The spawned process will have all the stdIn/stdOut file handles open, but + // without the power of fork, we need some other way to communicate the + // integer fd values so we encode them in argv and WasmCompileAndSerialize() + // has a matching #ifdef XP_WIN to parse them out. Communicate both ends of + // both pipes so the child process can closed the unused ends. + + arg = JS_smprintf("%d", stdIn.reader()); + if (!arg || !argv.append(std::move(arg))) { + return false; + } + + arg = JS_smprintf("%d", stdIn.writer()); + if (!arg || !argv.append(std::move(arg))) { + return false; + } + + arg = JS_smprintf("%d", stdOut.reader()); + if (!arg || !argv.append(std::move(arg))) { + return false; + } + + arg = JS_smprintf("%d", stdOut.writer()); + if (!arg || !argv.append(std::move(arg))) { + return false; + } +#endif + + // Required by both _spawnv and exec. + if (!argv.append(nullptr)) { + return false; + } + +#ifdef XP_WIN + if (!EscapeForShell(cx, argv)) { + return false; + } + + int childPid = _spawnv(P_NOWAIT, sArgv[0], argv.get()); + if (childPid == -1) { + return false; + } +#else + pid_t childPid = fork(); + switch (childPid) { + case -1: + return false; + case 0: + // In the child process. Redirect stdin/stdout to the respective ends of + // the pipes. Closing stdIn.writer() is necessary for stdin to hit EOF. + // This case statement must not return before exec() takes over. Rather, + // exit(-1) is used to return failure to the parent process. + if (dup2(stdIn.reader(), STDIN_FILENO) == -1) { + exit(-1); + } + if (dup2(stdOut.writer(), STDOUT_FILENO) == -1) { + exit(-1); + } + close(stdIn.reader()); + close(stdIn.writer()); + close(stdOut.reader()); + close(stdOut.writer()); + execv(sArgv[0], argv.get()); + exit(-1); + } +#endif + + // In the parent process. Closing stdOut.writer() is necessary for + // stdOut.reader() below to hit EOF. + stdIn.closeReader(); + stdOut.closeWriter(); + + if (!WriteAll(stdIn.writer(), bytecode, bytecodeLength)) { + return false; + } + + stdIn.closeWriter(); + + if (!ReadAll(stdOut.reader(), serialized)) { + return false; + } + + stdOut.closeReader(); + + int status; +#ifdef XP_WIN + if (_cwait(&status, childPid, WAIT_CHILD) == -1) { + return false; + } +#else + while (true) { + if (waitpid(childPid, &status, 0) >= 0) { + break; + } + if (errno != EINTR) { + return false; + } + } +#endif + + return status == 0; +} + +static bool WasmCompileAndSerialize(JSContext* cx) { + MOZ_ASSERT(wasm::CodeCachingAvailable(cx)); + +#ifdef XP_WIN + // See CompileAndSerializeInSeparateProcess for why we've had to smuggle + // these fd values through argv. Closing the writing ends is necessary for + // the reading ends to hit EOF. + int flagIndex = 0; + for (; flagIndex < sArgc; flagIndex++) { + if (!strcmp(sArgv[flagIndex], sWasmCompileAndSerializeFlag)) { + break; + } + } + MOZ_RELEASE_ASSERT(flagIndex < sArgc); + + int fdsIndex = flagIndex + 1; + MOZ_RELEASE_ASSERT(fdsIndex + 4 == sArgc); + + int stdInReader = atoi(sArgv[fdsIndex + 0]); + int stdInWriter = atoi(sArgv[fdsIndex + 1]); + int stdOutReader = atoi(sArgv[fdsIndex + 2]); + int stdOutWriter = atoi(sArgv[fdsIndex + 3]); + + int stdIn = stdInReader; + close(stdInWriter); + close(stdOutReader); + int stdOut = stdOutWriter; +#else + int stdIn = STDIN_FILENO; + int stdOut = STDOUT_FILENO; +#endif + + wasm::MutableBytes bytecode = js_new(); + if (!ReadAll(stdIn, &bytecode->bytes)) { + return false; + } + + wasm::Bytes serialized; + if (!wasm::CompileAndSerialize(*bytecode, &serialized)) { + return false; + } + + if (!WriteAll(stdOut, serialized.begin(), serialized.length())) { + return false; + } + + return true; +} + +static bool WasmCompileInSeparateProcess(JSContext* cx, unsigned argc, + Value* vp) { + if (!wasm::CodeCachingAvailable(cx)) { + JS_ReportErrorASCII(cx, "WebAssembly caching not supported"); + return false; + } + + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "wasmCompileInSeparateProcess", 1)) { + return false; + } + + SharedMem bytecode; + size_t numBytes; + if (!args[0].isObject() || + !IsBufferSource(&args[0].toObject(), &bytecode, &numBytes)) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Argument must be a buffer source"); + return false; + } + + wasm::Bytes serialized; + if (!CompileAndSerializeInSeparateProcess(cx, bytecode.unwrap(), numBytes, + &serialized)) { + if (!cx->isExceptionPending()) { + JS_ReportErrorASCII(cx, "creating and executing child process"); + } + return false; + } + + RootedObject module(cx); + if (!wasm::DeserializeModule(cx, serialized, &module)) { + return false; + } + + args.rval().setObject(*module); + return true; +} + +static bool DecompileFunction(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() < 1 || !args[0].isObject() || + !args[0].toObject().is()) { + args.rval().setUndefined(); + return true; + } + RootedFunction fun(cx, &args[0].toObject().as()); + JSString* result = JS_DecompileFunction(cx, fun); + if (!result) { + return false; + } + args.rval().setString(result); + return true; +} + +static bool DecompileThisScript(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + NonBuiltinScriptFrameIter iter(cx); + if (iter.done()) { + args.rval().setString(cx->runtime()->emptyString); + return true; + } + + { + JSAutoRealm ar(cx, iter.script()); + + RootedScript script(cx, iter.script()); + JSString* result = JS_DecompileScript(cx, script); + if (!result) { + return false; + } + + args.rval().setString(result); + } + + return JS_WrapValue(cx, args.rval()); +} + +static bool ValueToSource(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + JSString* str = ValueToSource(cx, args.get(0)); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +static bool ThisFilename(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + JS::AutoFilename filename; + if (!DescribeScriptedCaller(cx, &filename) || !filename.get()) { + args.rval().setString(cx->runtime()->emptyString); + return true; + } + + JSString* str = JS_NewStringCopyZ(cx, filename.get()); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +static bool WrapWithProto(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + Value obj = args.get(0); + Value proto = args.get(1); + if (!obj.isObject() || !proto.isObjectOrNull()) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "wrapWithProto"); + return false; + } + + // Disallow constructing (deeply) nested wrapper chains, to avoid running + // out of stack space in isCallable/isConstructor. See bug 1126105. + if (IsWrapper(&obj.toObject())) { + JS_ReportErrorASCII(cx, "wrapWithProto cannot wrap a wrapper"); + return false; + } + + WrapperOptions options(cx); + options.setProto(proto.toObjectOrNull()); + JSObject* wrapped = Wrapper::New(cx, &obj.toObject(), + &Wrapper::singletonWithPrototype, options); + if (!wrapped) { + return false; + } + + args.rval().setObject(*wrapped); + return true; +} + +static bool NewGlobal(JSContext* cx, unsigned argc, Value* vp) { + JS::RealmOptions options; + JS::RealmCreationOptions& creationOptions = options.creationOptions(); + JS::RealmBehaviors& behaviors = options.behaviors(); + ShellGlobalKind kind = ShellGlobalKind::WindowProxy; + bool immutablePrototype = true; + + SetStandardRealmOptions(options); + + // Default to creating the global in the current compartment unless + // --more-compartments is used. + if (defaultToSameCompartment) { + creationOptions.setExistingCompartment(cx->global()); + } else { + creationOptions.setNewCompartmentAndZone(); + } + + 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); + + if (!JS_GetProperty(cx, opts, "invisibleToDebugger", &v)) { + return false; + } + if (v.isBoolean()) { + creationOptions.setInvisibleToDebugger(v.toBoolean()); + } + + if (!JS_GetProperty(cx, opts, "sameZoneAs", &v)) { + return false; + } + if (v.isObject()) { + creationOptions.setNewCompartmentInExistingZone( + UncheckedUnwrap(&v.toObject())); + } + + if (!JS_GetProperty(cx, opts, "sameCompartmentAs", &v)) { + return false; + } + if (v.isObject()) { + creationOptions.setExistingCompartment(UncheckedUnwrap(&v.toObject())); + } + + if (!JS_GetProperty(cx, opts, "newCompartment", &v)) { + return false; + } + if (v.isBoolean() && v.toBoolean()) { + creationOptions.setNewCompartmentAndZone(); + } + + if (!JS_GetProperty(cx, opts, "disableLazyParsing", &v)) { + return false; + } + if (v.isBoolean()) { + behaviors.setDisableLazyParsing(v.toBoolean()); + } + + if (!JS_GetProperty(cx, opts, "discardSource", &v)) { + return false; + } + if (v.isBoolean()) { + behaviors.setDiscardSource(v.toBoolean()); + } + + if (!JS_GetProperty(cx, opts, "useWindowProxy", &v)) { + return false; + } + if (v.isBoolean()) { + kind = v.toBoolean() ? ShellGlobalKind::WindowProxy + : ShellGlobalKind::GlobalObject; + } + + if (!JS_GetProperty(cx, opts, "immutablePrototype", &v)) { + return false; + } + if (v.isBoolean()) { + immutablePrototype = v.toBoolean(); + } + + if (!JS_GetProperty(cx, opts, "enableWritableStreams", &v)) { + return false; + } + if (v.isBoolean()) { + creationOptions.setWritableStreamsEnabled(v.toBoolean()); + } + + if (!JS_GetProperty(cx, opts, "enableReadableStreamPipeTo", &v)) { + return false; + } + if (v.isBoolean()) { + creationOptions.setReadableStreamPipeToEnabled(v.toBoolean()); + } + + if (!JS_GetProperty(cx, opts, "systemPrincipal", &v)) { + return false; + } + if (v.isBoolean()) { + principals.reset(&ShellPrincipals::fullyTrusted); + } + + if (!JS_GetProperty(cx, opts, "principal", &v)) { + return false; + } + if (!v.isUndefined()) { + uint32_t bits; + if (!ToUint32(cx, v, &bits)) { + return false; + } + JSPrincipals* newPrincipals = cx->new_(bits); + if (!newPrincipals) { + return false; + } + principals.reset(newPrincipals); + } + + if (!JS_GetProperty(cx, opts, "enableCoopAndCoep", &v)) { + return false; + } + if (v.isBoolean()) { + creationOptions.setCoopAndCoepEnabled(v.toBoolean()); + } + + // On the web, the SharedArrayBuffer constructor is not installed as a + // global property in pages that aren't isolated in a separate process (and + // thus can't allow the structured cloning of shared memory). Specify false + // for this option to reproduce this behavior. + if (!JS_GetProperty(cx, opts, "defineSharedArrayBufferConstructor", &v)) { + return false; + } + if (v.isBoolean()) { + creationOptions.setDefineSharedArrayBufferConstructor(v.toBoolean()); + } + } + + if (!CheckRealmOptions(cx, options, principals.get())) { + return false; + } + + RootedObject global(cx, NewGlobalObject(cx, options, principals.get(), kind, + immutablePrototype)); + if (!global) { + return false; + } + + RootedObject wrapped(cx, ToWindowProxyIfWindow(global)); + if (!JS_WrapObject(cx, &wrapped)) { + return false; + } + + args.rval().setObject(*wrapped); + return true; +} + +static bool NukeCCW(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1 || !args[0].isObject() || + !IsCrossCompartmentWrapper(&args[0].toObject())) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "nukeCCW"); + return false; + } + + NukeCrossCompartmentWrapper(cx, &args[0].toObject()); + args.rval().setUndefined(); + return true; +} + +static bool NukeAllCCWs(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 0) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "nukeAllCCWs"); + return false; + } + + NukeCrossCompartmentWrappers(cx, AllCompartments(), cx->realm(), + NukeWindowReferences, NukeAllReferences); + args.rval().setUndefined(); + return true; +} + +static bool RecomputeWrappers(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() > 2) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "recomputeWrappers"); + return false; + } + + JS::Compartment* sourceComp = nullptr; + if (args.get(0).isObject()) { + sourceComp = JS::GetCompartment(UncheckedUnwrap(&args[0].toObject())); + } + + JS::Compartment* targetComp = nullptr; + if (args.get(1).isObject()) { + targetComp = JS::GetCompartment(UncheckedUnwrap(&args[1].toObject())); + } + + struct SingleOrAllCompartments final : public CompartmentFilter { + JS::Compartment* comp; + explicit SingleOrAllCompartments(JS::Compartment* c) : comp(c) {} + virtual bool match(JS::Compartment* c) const override { + return !comp || comp == c; + } + }; + + if (!js::RecomputeWrappers(cx, SingleOrAllCompartments(sourceComp), + SingleOrAllCompartments(targetComp))) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool DumpObjectWrappers(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + bool printedHeader = false; + for (ZonesIter zone(cx->runtime(), WithAtoms); !zone.done(); zone.next()) { + bool printedZoneInfo = false; + for (CompartmentsInZoneIter comp(zone); !comp.done(); comp.next()) { + bool printedCompartmentInfo = false; + for (Compartment::ObjectWrapperEnum e(comp); !e.empty(); e.popFront()) { + JSObject* wrapper = e.front().value().unbarrieredGet(); + JSObject* wrapped = e.front().key(); + if (!printedHeader) { + fprintf(stderr, "Cross-compartment object wrappers:\n"); + printedHeader = true; + } + if (!printedZoneInfo) { + fprintf(stderr, " Zone %p:\n", zone.get()); + printedZoneInfo = true; + } + if (!printedCompartmentInfo) { + fprintf(stderr, " Compartment %p:\n", comp.get()); + printedCompartmentInfo = true; + } + fprintf(stderr, + " Object wrapper %p -> %p in zone %p compartment %p\n", + wrapper, wrapped, wrapped->zone(), wrapped->compartment()); + } + } + } + + if (!printedHeader) { + fprintf(stderr, "No cross-compartment object wrappers.\n"); + } + + args.rval().setUndefined(); + return true; +} + +static bool GetMaxArgs(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setInt32(ARGS_LENGTH_MAX); + return true; +} + +static bool IsHTMLDDA_Call(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // These are the required conditions under which this object may be called + // by test262 tests, and the required behavior under those conditions. + if (args.length() == 0 || + (args[0].isString() && args[0].toString()->length() == 0)) { + args.rval().setNull(); + return true; + } + + JS_ReportErrorASCII( + cx, "IsHTMLDDA object is being called in an impermissible manner"); + return false; +} + +static bool CreateIsHTMLDDA(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + static const JSClassOps classOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + nullptr, // finalize + IsHTMLDDA_Call, // call + nullptr, // hasInstance + nullptr, // construct + nullptr, // trace + }; + + static const JSClass cls = { + "IsHTMLDDA", + JSCLASS_EMULATES_UNDEFINED, + &classOps, + }; + + JSObject* obj = JS_NewObject(cx, &cls); + if (!obj) { + return false; + } + args.rval().setObject(*obj); + return true; +} + +static bool GetSelfHostedValue(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1 || !args[0].isString()) { + JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, + JSSMSG_INVALID_ARGS, "getSelfHostedValue"); + return false; + } + RootedAtom srcAtom(cx, ToAtom(cx, args[0])); + if (!srcAtom) { + return false; + } + RootedPropertyName srcName(cx, srcAtom->asPropertyName()); + return cx->runtime()->cloneSelfHostedValue(cx, srcName, args.rval()); +} + +class ShellSourceHook : public SourceHook { + // The function we should call to lazily retrieve source code. + PersistentRootedFunction fun; + + public: + ShellSourceHook(JSContext* cx, JSFunction& fun) : fun(cx, &fun) {} + + bool load(JSContext* cx, const char* filename, char16_t** twoByteSource, + char** utf8Source, size_t* length) override { + MOZ_ASSERT((twoByteSource != nullptr) != (utf8Source != nullptr), + "must be called requesting only one of UTF-8 or UTF-16 source"); + + RootedString str(cx, JS_NewStringCopyZ(cx, filename)); + if (!str) { + return false; + } + RootedValue filenameValue(cx, StringValue(str)); + + RootedValue result(cx); + if (!Call(cx, UndefinedHandleValue, fun, HandleValueArray(filenameValue), + &result)) { + return false; + } + + str = JS::ToString(cx, result); + if (!str) { + return false; + } + + Rooted linear(cx, str->ensureLinear(cx)); + if (!linear) { + return false; + } + + if (twoByteSource) { + *length = JS_GetStringLength(linear); + + *twoByteSource = cx->pod_malloc(*length); + if (!*twoByteSource) { + return false; + } + + CopyChars(*twoByteSource, *linear); + } else { + MOZ_ASSERT(utf8Source != nullptr); + + *length = JS::GetDeflatedUTF8StringLength(linear); + + *utf8Source = cx->pod_malloc(*length); + if (!*utf8Source) { + return false; + } + + mozilla::DebugOnly dstLen = JS::DeflateStringToUTF8Buffer( + linear, mozilla::Span(*utf8Source, *length)); + MOZ_ASSERT(dstLen == *length); + } + + return true; + } +}; + +static bool WithSourceHook(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (args.length() != 2) { + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments."); + return false; + } + + if (!args[0].isObject() || !args[0].toObject().is() || + !args[1].isObject() || !args[1].toObject().is()) { + ReportUsageErrorASCII(cx, callee, + "First and second arguments must be functions."); + return false; + } + + mozilla::UniquePtr hook = + mozilla::MakeUnique(cx, + args[0].toObject().as()); + if (!hook) { + return false; + } + + mozilla::UniquePtr savedHook = js::ForgetSourceHook(cx); + js::SetSourceHook(cx, std::move(hook)); + + RootedObject fun(cx, &args[1].toObject()); + bool result = Call(cx, UndefinedHandleValue, fun, + JS::HandleValueArray::empty(), args.rval()); + js::SetSourceHook(cx, std::move(savedHook)); + return result; +} + +static void PrintProfilerEvents_Callback(const char* msg, const char* details) { + fprintf(stderr, "PROFILER EVENT: %s %s\n", msg, details); +} + +static bool PrintProfilerEvents(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (cx->runtime()->geckoProfiler().enabled()) { + js::RegisterContextProfilingEventMarker(cx, &PrintProfilerEvents_Callback); + } + args.rval().setUndefined(); + return true; +} + +#ifdef SINGLESTEP_PROFILING +static void SingleStepCallback(void* arg, jit::Simulator* sim, void* pc) { + JSContext* cx = reinterpret_cast(arg); + + // If profiling is not enabled, don't do anything. + if (!cx->runtime()->geckoProfiler().enabled()) { + return; + } + + JS::ProfilingFrameIterator::RegisterState state; + state.pc = pc; +# if defined(JS_SIMULATOR_ARM) + state.sp = (void*)sim->get_register(jit::Simulator::sp); + state.lr = (void*)sim->get_register(jit::Simulator::lr); + state.fp = (void*)sim->get_register(jit::Simulator::fp); +# elif defined(JS_SIMULATOR_MIPS64) || defined(JS_SIMULATOR_MIPS32) + state.sp = (void*)sim->getRegister(jit::Simulator::sp); + state.lr = (void*)sim->getRegister(jit::Simulator::ra); + state.fp = (void*)sim->getRegister(jit::Simulator::fp); +# else +# error "NYI: Single-step profiling support" +# endif + + mozilla::DebugOnly lastStackAddress = nullptr; + StackChars stack; + uint32_t frameNo = 0; + AutoEnterOOMUnsafeRegion oomUnsafe; + for (JS::ProfilingFrameIterator i(cx, state); !i.done(); ++i) { + MOZ_ASSERT(i.stackAddress() != nullptr); + MOZ_ASSERT(lastStackAddress <= i.stackAddress()); + lastStackAddress = i.stackAddress(); + JS::ProfilingFrameIterator::Frame frames[16]; + uint32_t nframes = i.extractStack(frames, 0, 16); + for (uint32_t i = 0; i < nframes; i++) { + if (frameNo > 0) { + if (!stack.append(",", 1)) { + oomUnsafe.crash("stack.append"); + } + } + if (!stack.append(frames[i].label, strlen(frames[i].label))) { + oomUnsafe.crash("stack.append"); + } + frameNo++; + } + } + + ShellContext* sc = GetShellContext(cx); + + // Only append the stack if it differs from the last stack. + if (sc->stacks.empty() || sc->stacks.back().length() != stack.length() || + !ArrayEqual(sc->stacks.back().begin(), stack.begin(), stack.length())) { + if (!sc->stacks.append(std::move(stack))) { + oomUnsafe.crash("stacks.append"); + } + } +} +#endif + +static bool EnableSingleStepProfiling(JSContext* cx, unsigned argc, Value* vp) { +#ifdef SINGLESTEP_PROFILING + CallArgs args = CallArgsFromVp(argc, vp); + + jit::Simulator* sim = cx->simulator(); + sim->enable_single_stepping(SingleStepCallback, cx); + + args.rval().setUndefined(); + return true; +#else + JS_ReportErrorASCII(cx, "single-step profiling not enabled on this platform"); + return false; +#endif +} + +static bool DisableSingleStepProfiling(JSContext* cx, unsigned argc, + Value* vp) { +#ifdef SINGLESTEP_PROFILING + CallArgs args = CallArgsFromVp(argc, vp); + + jit::Simulator* sim = cx->simulator(); + sim->disable_single_stepping(); + + ShellContext* sc = GetShellContext(cx); + + RootedValueVector elems(cx); + for (size_t i = 0; i < sc->stacks.length(); i++) { + JSString* stack = + JS_NewUCStringCopyN(cx, sc->stacks[i].begin(), sc->stacks[i].length()); + if (!stack) { + return false; + } + if (!elems.append(StringValue(stack))) { + return false; + } + } + + JSObject* array = JS::NewArrayObject(cx, elems); + if (!array) { + return false; + } + + sc->stacks.clear(); + args.rval().setObject(*array); + return true; +#else + JS_ReportErrorASCII(cx, "single-step profiling not enabled on this platform"); + return false; +#endif +} + +static bool IsLatin1(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + bool isLatin1 = + args.get(0).isString() && args[0].toString()->hasLatin1Chars(); + args.rval().setBoolean(isLatin1); + return true; +} + +static bool EnableGeckoProfiling(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!EnsureGeckoProfilingStackInstalled(cx, GetShellContext(cx))) { + return false; + } + + cx->runtime()->geckoProfiler().enableSlowAssertions(false); + cx->runtime()->geckoProfiler().enable(true); + + args.rval().setUndefined(); + return true; +} + +static bool EnableGeckoProfilingWithSlowAssertions(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setUndefined(); + + if (cx->runtime()->geckoProfiler().enabled()) { + // If profiling already enabled with slow assertions disabled, + // this is a no-op. + if (cx->runtime()->geckoProfiler().slowAssertionsEnabled()) { + return true; + } + + // Slow assertions are off. Disable profiling before re-enabling + // with slow assertions on. + cx->runtime()->geckoProfiler().enable(false); + } + + if (!EnsureGeckoProfilingStackInstalled(cx, GetShellContext(cx))) { + return false; + } + + cx->runtime()->geckoProfiler().enableSlowAssertions(true); + cx->runtime()->geckoProfiler().enable(true); + + return true; +} + +static bool DisableGeckoProfiling(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setUndefined(); + + if (!cx->runtime()->geckoProfiler().enabled()) { + return true; + } + + cx->runtime()->geckoProfiler().enable(false); + return true; +} + +// Global mailbox that is used to communicate a shareable object value from one +// worker to another. +// +// These object types are shareable: +// +// - SharedArrayBuffer +// - WasmMemoryObject (when constructed with shared:true) +// - WasmModuleObject +// +// For the SharedArrayBuffer and WasmMemoryObject we transmit the underlying +// SharedArrayRawBuffer ("SARB"). For the WasmModuleObject we transmit the +// underlying JS::WasmModule. The transmitted types are refcounted. When they +// are in the mailbox their reference counts are at least 1, accounting for the +// reference from the mailbox. +// +// The lock guards the mailbox variable and prevents a race where two workers +// try to set the mailbox at the same time to replace an object that is only +// referenced from the mailbox: the workers will both decrement the reference +// count on the old object, and one of those decrements will be on a garbage +// object. We could implement this with atomics and a CAS loop but it's not +// worth the bother. +// +// Note that if a thread reads the mailbox repeatedly it will get distinct +// objects on each read. The alternatives are to cache created objects locally, +// but this retains storage we don't need to retain, or to somehow clear the +// mailbox locally, but this creates a coordination headache. Buyer beware. + +enum class MailboxTag { + Empty, + SharedArrayBuffer, + WasmMemory, + WasmModule, + Number, +}; + +struct SharedObjectMailbox { + union Value { + struct { + SharedArrayRawBuffer* buffer; + BufferSize length; + } sarb; + JS::WasmModule* module; + double number; + + Value() : number(0.0) {} + }; + + MailboxTag tag = MailboxTag::Empty; + Value val; +}; + +typedef ExclusiveData SOMailbox; + +// Never null after successful initialization. +static SOMailbox* sharedObjectMailbox; + +static bool InitSharedObjectMailbox() { + sharedObjectMailbox = js_new(mutexid::ShellObjectMailbox); + return sharedObjectMailbox != nullptr; +} + +static void DestructSharedObjectMailbox() { + // All workers need to have terminated at this point. + + { + auto mbx = sharedObjectMailbox->lock(); + switch (mbx->tag) { + case MailboxTag::Empty: + case MailboxTag::Number: + break; + case MailboxTag::SharedArrayBuffer: + case MailboxTag::WasmMemory: + mbx->val.sarb.buffer->dropReference(); + break; + case MailboxTag::WasmModule: + mbx->val.module->Release(); + break; + default: + MOZ_CRASH(); + } + } + + js_delete(sharedObjectMailbox); + sharedObjectMailbox = nullptr; +} + +static bool GetSharedObject(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject newObj(cx); + + { + auto mbx = sharedObjectMailbox->lock(); + switch (mbx->tag) { + case MailboxTag::Empty: { + break; + } + case MailboxTag::Number: { + args.rval().setNumber(mbx->val.number); + return true; + } + case MailboxTag::SharedArrayBuffer: + case MailboxTag::WasmMemory: { + // Flag was set in the sender; ensure it is set in the receiver. + MOZ_ASSERT( + cx->realm()->creationOptions().getSharedMemoryAndAtomicsEnabled()); + + // The protocol for creating a SAB requires the refcount to be + // incremented prior to the SAB creation. + + SharedArrayRawBuffer* buf = mbx->val.sarb.buffer; + BufferSize length = mbx->val.sarb.length; + if (!buf->addReference()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_SC_SAB_REFCNT_OFLO); + return false; + } + + // If the allocation fails we must decrement the refcount before + // returning. + + Rooted maybesab( + cx, SharedArrayBufferObject::New(cx, buf, length)); + if (!maybesab) { + buf->dropReference(); + return false; + } + + // At this point the SAB was created successfully and it owns the + // refcount-increase on the buffer that we performed above. So even + // if we fail to allocate along any path below we must not decrement + // the refcount; the garbage collector must be allowed to handle + // that via finalization of the orphaned SAB object. + + if (mbx->tag == MailboxTag::SharedArrayBuffer) { + newObj = maybesab; + } else { + if (!GlobalObject::ensureConstructor(cx, cx->global(), + JSProto_WebAssembly)) { + return false; + } + RootedObject proto( + cx, &cx->global()->getPrototype(JSProto_WasmMemory).toObject()); + newObj = WasmMemoryObject::create(cx, maybesab, proto); + MOZ_ASSERT_IF(newObj, newObj->as().isShared()); + if (!newObj) { + return false; + } + } + + break; + } + case MailboxTag::WasmModule: { + // Flag was set in the sender; ensure it is set in the receiver. + MOZ_ASSERT( + cx->realm()->creationOptions().getSharedMemoryAndAtomicsEnabled()); + + if (!GlobalObject::ensureConstructor(cx, cx->global(), + JSProto_WebAssembly)) { + return false; + } + + // WasmModuleObject::create() increments the refcount on the module + // and signals an error and returns null if that fails. + newObj = mbx->val.module->createObject(cx); + if (!newObj) { + return false; + } + break; + } + default: { + MOZ_CRASH(); + } + } + } + + args.rval().setObjectOrNull(newObj); + return true; +} + +static bool SetSharedObject(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + MailboxTag tag = MailboxTag::Empty; + SharedObjectMailbox::Value value; + + // Increase refcounts when we obtain the value to avoid operating on dead + // storage during self-assignment. + + if (args.get(0).isObject()) { + RootedObject obj(cx, &args[0].toObject()); + if (obj->is()) { + Rooted sab(cx, + &obj->as()); + tag = MailboxTag::SharedArrayBuffer; + value.sarb.buffer = sab->rawBufferObject(); + value.sarb.length = sab->byteLength(); + if (!value.sarb.buffer->addReference()) { + JS_ReportErrorASCII(cx, + "Reference count overflow on SharedArrayBuffer"); + return false; + } + } else if (obj->is()) { + // Here we must transmit sab.byteLength() as the length; the SARB has its + // own notion of the length which may be greater, and that's fine. + if (obj->as().isShared()) { + Rooted sab( + cx, &obj->as() + .buffer() + .as()); + tag = MailboxTag::WasmMemory; + value.sarb.buffer = sab->rawBufferObject(); + value.sarb.length = sab->byteLength(); + if (!value.sarb.buffer->addReference()) { + JS_ReportErrorASCII(cx, + "Reference count overflow on SharedArrayBuffer"); + return false; + } + } else { + JS_ReportErrorASCII(cx, "Invalid argument to SetSharedObject"); + return false; + } + } else if (JS::IsWasmModuleObject(obj)) { + tag = MailboxTag::WasmModule; + value.module = JS::GetWasmModule(obj).forget().take(); + } else { + JS_ReportErrorASCII(cx, "Invalid argument to SetSharedObject"); + return false; + } + } else if (args.get(0).isNumber()) { + tag = MailboxTag::Number; + value.number = args.get(0).toNumber(); + // Nothing + } else if (args.get(0).isNullOrUndefined()) { + // Nothing + } else { + JS_ReportErrorASCII(cx, "Invalid argument to SetSharedObject"); + return false; + } + + { + auto mbx = sharedObjectMailbox->lock(); + + switch (mbx->tag) { + case MailboxTag::Empty: + case MailboxTag::Number: + break; + case MailboxTag::SharedArrayBuffer: + case MailboxTag::WasmMemory: + mbx->val.sarb.buffer->dropReference(); + break; + case MailboxTag::WasmModule: + mbx->val.module->Release(); + break; + default: + MOZ_CRASH(); + } + + mbx->tag = tag; + mbx->val = value; + } + + args.rval().setUndefined(); + return true; +} + +typedef Vector Uint8Vector; + +class StreamCacheEntry : public AtomicRefCounted, + public JS::OptimizedEncodingListener { + typedef AtomicRefCounted AtomicBase; + + Uint8Vector bytes_; + ExclusiveData optimized_; + + public: + explicit StreamCacheEntry(Uint8Vector&& original) + : bytes_(std::move(original)), + optimized_(mutexid::ShellStreamCacheEntryState) {} + + // Implement JS::OptimizedEncodingListener: + + MozExternalRefCountType MOZ_XPCOM_ABI AddRef() override { + AtomicBase::AddRef(); + return 1; // unused + } + MozExternalRefCountType MOZ_XPCOM_ABI Release() override { + AtomicBase::Release(); + return 0; // unused + } + + const Uint8Vector& bytes() const { return bytes_; } + + void storeOptimizedEncoding(JS::UniqueOptimizedEncodingBytes src) override { + MOZ_ASSERT(src->length() > 0); + + // Tolerate races since a single StreamCacheEntry object can be used as + // the source of multiple streaming compilations. + auto dstBytes = optimized_.lock(); + if (dstBytes->length() > 0) { + return; + } + + if (!dstBytes->resize(src->length())) { + return; + } + memcpy(dstBytes->begin(), src->begin(), src->length()); + } + + bool hasOptimizedEncoding() const { return !optimized_.lock()->empty(); } + const Uint8Vector& optimizedEncoding() const { + return optimized_.lock().get(); + } +}; + +typedef RefPtr StreamCacheEntryPtr; + +class StreamCacheEntryObject : public NativeObject { + static const unsigned CACHE_ENTRY_SLOT = 0; + static const JSClassOps classOps_; + static const JSPropertySpec properties_; + + static void finalize(JSFreeOp*, JSObject* obj) { + obj->as().cache().Release(); + } + + static bool cachedGetter(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.thisv().isObject() || + !args.thisv().toObject().is()) { + return false; + } + + StreamCacheEntryObject& obj = + args.thisv().toObject().as(); + args.rval().setBoolean(obj.cache().hasOptimizedEncoding()); + return true; + } + static bool getBuffer(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.thisv().isObject() || + !args.thisv().toObject().is()) { + return false; + } + + auto& bytes = + args.thisv().toObject().as().cache().bytes(); + RootedArrayBufferObject buffer( + cx, ArrayBufferObject::createZeroed(cx, BufferSize(bytes.length()))); + if (!buffer) { + return false; + } + + memcpy(buffer->dataPointer(), bytes.begin(), bytes.length()); + + args.rval().setObject(*buffer); + return true; + } + + public: + static const unsigned RESERVED_SLOTS = 1; + static const JSClass class_; + static const JSPropertySpec properties[]; + + static bool construct(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "streamCacheEntry", 1)) { + return false; + } + + SharedMem ptr; + size_t numBytes; + if (!args[0].isObject() || + !IsBufferSource(&args[0].toObject(), &ptr, &numBytes)) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Argument must be an ArrayBuffer"); + return false; + } + + Uint8Vector bytes; + if (!bytes.resize(numBytes)) { + return false; + } + + memcpy(bytes.begin(), ptr.unwrap(), numBytes); + + RefPtr cache = + cx->new_(std::move(bytes)); + if (!cache) { + return false; + } + + RootedNativeObject obj( + cx, NewObjectWithGivenProto(cx, nullptr)); + if (!obj) { + return false; + } + obj->initReservedSlot(CACHE_ENTRY_SLOT, + PrivateValue(cache.forget().take())); + + if (!JS_DefineProperty(cx, obj, "cached", cachedGetter, nullptr, 0)) { + return false; + } + if (!JS_DefineFunction(cx, obj, "getBuffer", getBuffer, 0, 0)) { + return false; + } + + args.rval().setObject(*obj); + return true; + } + + StreamCacheEntry& cache() const { + return *(StreamCacheEntry*)getReservedSlot(CACHE_ENTRY_SLOT).toPrivate(); + } +}; + +const JSClassOps StreamCacheEntryObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + StreamCacheEntryObject::finalize, // finalize + nullptr, // call + nullptr, // hasInstance + nullptr, // construct + nullptr, // trace +}; + +const JSClass StreamCacheEntryObject::class_ = { + "StreamCacheEntryObject", + JSCLASS_HAS_RESERVED_SLOTS(StreamCacheEntryObject::RESERVED_SLOTS) | + JSCLASS_BACKGROUND_FINALIZE, + &StreamCacheEntryObject::classOps_}; + +struct BufferStreamJob { + Variant source; + Thread thread; + JS::StreamConsumer* consumer; + + BufferStreamJob(Uint8Vector&& source, JS::StreamConsumer* consumer) + : source(AsVariant(std::move(source))), consumer(consumer) {} + BufferStreamJob(StreamCacheEntry& source, JS::StreamConsumer* consumer) + : source(AsVariant(&source)), consumer(consumer) {} +}; + +struct BufferStreamState { + Vector, 0, SystemAllocPolicy> jobs; + size_t delayMillis; + size_t chunkSize; + bool shutdown; + + BufferStreamState() : delayMillis(1), chunkSize(10), shutdown(false) {} + + ~BufferStreamState() { MOZ_ASSERT(jobs.empty()); } +}; + +static ExclusiveWaitableData* bufferStreamState; + +static void BufferStreamMain(BufferStreamJob* job) { + const uint8_t* bytes; + size_t byteLength; + JS::OptimizedEncodingListener* listener; + if (job->source.is()) { + StreamCacheEntry& cache = *job->source.as(); + if (cache.hasOptimizedEncoding()) { + const Uint8Vector& optimized = cache.optimizedEncoding(); + job->consumer->consumeOptimizedEncoding(optimized.begin(), + optimized.length()); + goto done; + } + + bytes = cache.bytes().begin(); + byteLength = cache.bytes().length(); + listener = &cache; + } else { + bytes = job->source.as().begin(); + byteLength = job->source.as().length(); + listener = nullptr; + } + + size_t byteOffset; + byteOffset = 0; + while (true) { + if (byteOffset == byteLength) { + job->consumer->streamEnd(listener); + break; + } + + bool shutdown; + size_t delayMillis; + size_t chunkSize; + { + auto state = bufferStreamState->lock(); + shutdown = state->shutdown; + delayMillis = state->delayMillis; + chunkSize = state->chunkSize; + } + + if (shutdown) { + job->consumer->streamError(JSMSG_STREAM_CONSUME_ERROR); + break; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(delayMillis)); + + chunkSize = std::min(chunkSize, byteLength - byteOffset); + + if (!job->consumer->consumeChunk(bytes + byteOffset, chunkSize)) { + break; + } + + byteOffset += chunkSize; + } + +done: + auto state = bufferStreamState->lock(); + size_t jobIndex = 0; + while (state->jobs[jobIndex].get() != job) { + jobIndex++; + } + job->thread.detach(); // quiet assert in ~Thread() called by erase(). + state->jobs.erase(state->jobs.begin() + jobIndex); + if (state->jobs.empty()) { + state.notify_all(/* jobs empty */); + } +} + +static bool EnsureLatin1CharsLinearString(JSContext* cx, HandleValue value, + UniqueChars* result) { + if (!value.isString()) { + result->reset(nullptr); + return true; + } + RootedString str(cx, value.toString()); + if (!str->isLinear() || !str->hasLatin1Chars()) { + JS_ReportErrorASCII(cx, + "only latin1 chars and linear strings are expected"); + return false; + } + + // Use JS_EncodeStringToLatin1 to null-terminate. + *result = JS_EncodeStringToLatin1(cx, str); + return !!*result; +} + +static bool ConsumeBufferSource(JSContext* cx, JS::HandleObject obj, + JS::MimeType, JS::StreamConsumer* consumer) { + { + RootedValue url(cx); + if (!JS_GetProperty(cx, obj, "url", &url)) { + return false; + } + UniqueChars urlChars; + if (!EnsureLatin1CharsLinearString(cx, url, &urlChars)) { + return false; + } + + RootedValue mapUrl(cx); + if (!JS_GetProperty(cx, obj, "sourceMappingURL", &mapUrl)) { + return false; + } + UniqueChars mapUrlChars; + if (!EnsureLatin1CharsLinearString(cx, mapUrl, &mapUrlChars)) { + return false; + } + + consumer->noteResponseURLs(urlChars.get(), mapUrlChars.get()); + } + + UniquePtr job; + + SharedMem dataPointer; + size_t byteLength; + if (IsBufferSource(obj, &dataPointer, &byteLength)) { + Uint8Vector bytes; + if (!bytes.resize(byteLength)) { + JS_ReportOutOfMemory(cx); + return false; + } + + memcpy(bytes.begin(), dataPointer.unwrap(), byteLength); + job = cx->make_unique(std::move(bytes), consumer); + } else if (obj->is()) { + job = cx->make_unique( + obj->as().cache(), consumer); + } else { + JS_ReportErrorASCII( + cx, + "shell streaming consumes a buffer source (buffer or view) " + "or StreamCacheEntryObject"); + return false; + } + if (!job) { + return false; + } + + BufferStreamJob* jobPtr = job.get(); + + { + auto state = bufferStreamState->lock(); + MOZ_ASSERT(!state->shutdown); + if (!state->jobs.append(std::move(job))) { + JS_ReportOutOfMemory(cx); + return false; + } + } + + { + AutoEnterOOMUnsafeRegion oomUnsafe; + if (!jobPtr->thread.init(BufferStreamMain, jobPtr)) { + oomUnsafe.crash("ConsumeBufferSource"); + } + } + + return true; +} + +static void ReportStreamError(JSContext* cx, size_t errorNumber) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, errorNumber); +} + +static bool SetBufferStreamParams(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "setBufferStreamParams", 2)) { + return false; + } + + double delayMillis; + if (!ToNumber(cx, args[0], &delayMillis)) { + return false; + } + + double chunkSize; + if (!ToNumber(cx, args[1], &chunkSize)) { + return false; + } + + { + auto state = bufferStreamState->lock(); + state->delayMillis = delayMillis; + state->chunkSize = chunkSize; + } + + args.rval().setUndefined(); + return true; +} + +static void ShutdownBufferStreams() { + auto state = bufferStreamState->lock(); + state->shutdown = true; + while (!state->jobs.empty()) { + state.wait(/* jobs empty */); + } + state->jobs.clearAndFree(); +} + +static bool DumpScopeChain(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (js::SupportDifferentialTesting()) { + ReportUsageErrorASCII( + cx, callee, "Function not available in differential testing mode."); + return false; + } + + if (args.length() != 1) { + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + if (!args[0].isObject() || !(args[0].toObject().is() || + args[0].toObject().is())) { + ReportUsageErrorASCII( + cx, callee, "Argument must be an interpreted function or a module"); + return false; + } + + RootedObject obj(cx, &args[0].toObject()); + RootedScript script(cx); + + if (obj->is()) { + RootedFunction fun(cx, &obj->as()); + if (!fun->isInterpreted()) { + ReportUsageErrorASCII(cx, callee, + "Argument must be an interpreted function"); + return false; + } + script = JSFunction::getOrCreateScript(cx, fun); + if (!script) { + return false; + } + } else { + script = obj->as().maybeScript(); + if (!script) { + JS_ReportErrorASCII(cx, "module does not have an associated script"); + return false; + } + } + + script->bodyScope()->dump(); + + args.rval().setUndefined(); + return true; +} + +// For testing gray marking, grayRoot() will heap-allocate an address +// where we can store a JSObject*, and create a new object if one doesn't +// already exist. +// +// Note that EnsureGrayRoot() will blacken the returned object, so it will not +// actually end up marked gray until the following GC clears the black bit +// (assuming nothing is holding onto it.) +// +// The idea is that you can set up a whole graph of objects to be marked gray, +// hanging off of the object returned from grayRoot(). Then you GC to clear the +// black bits and set the gray bits. +// +// To test grayness, register the objects of interest with addMarkObservers(), +// which takes an Array of objects (which will be marked black at the time +// they're passed in). Their mark bits may be retrieved at any time with +// getMarks(), in the form of an array of strings with each index corresponding +// to the original objects passed to addMarkObservers(). + +static bool EnsureGrayRoot(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + auto priv = EnsureShellCompartmentPrivate(cx); + if (!priv) { + return false; + } + + if (!priv->grayRoot) { + if (!(priv->grayRoot = NewTenuredDenseEmptyArray(cx, nullptr))) { + return false; + } + } + + // Barrier to enforce the invariant that JS does not touch gray objects. + JSObject* obj = priv->grayRoot; + JS::ExposeObjectToActiveJS(obj); + + args.rval().setObject(*obj); + return true; +} + +static MarkBitObservers* EnsureMarkBitObservers(JSContext* cx) { + ShellContext* sc = GetShellContext(cx); + if (!sc->markObservers) { + auto* observers = + cx->new_(cx->runtime(), NonshrinkingGCObjectVector()); + if (!observers) { + return nullptr; + } + sc->markObservers.reset(observers); + } + return sc->markObservers.get(); +} + +static bool ClearMarkObservers(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + auto markObservers = EnsureMarkBitObservers(cx); + if (!markObservers) { + return false; + } + + markObservers->get().clear(); + + args.rval().setUndefined(); + return true; +} + +static bool AddMarkObservers(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + auto markObservers = EnsureMarkBitObservers(cx); + if (!markObservers) { + return false; + } + + if (!args.get(0).isObject()) { + JS_ReportErrorASCII(cx, "argument must be an Array of objects"); + return false; + } + + RootedObject observersArg(cx, &args[0].toObject()); + uint32_t length; + if (!GetLengthProperty(cx, observersArg, &length)) { + return false; + } + + RootedValue value(cx); + RootedObject object(cx); + for (uint32_t i = 0; i < length; i++) { + if (!JS_GetElement(cx, observersArg, i, &value)) { + return false; + } + + if (!value.isObject()) { + JS_ReportErrorASCII(cx, "argument must be an Array of objects"); + return false; + } + + object = &value.toObject(); + if (gc::IsInsideNursery(object)) { + // WeakCaches are not swept during a minor GC. To prevent + // nursery-allocated contents from having the mark bits be deceptively + // black until the second GC, they would need to be marked weakly (cf + // NurseryAwareHashMap). It is simpler to evict the nursery to prevent + // nursery objects from being observed. + cx->runtime()->gc.evictNursery(); + } + + if (!markObservers->get().append(object)) { + return false; + } + } + + args.rval().setInt32(length); + return true; +} + +static bool GetMarks(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + auto& observers = GetShellContext(cx)->markObservers; + if (!observers) { + args.rval().setUndefined(); + return true; + } + + size_t length = observers->get().length(); + Rooted ret(cx, js::NewDenseEmptyArray(cx)); + if (!ret) { + return false; + } + + for (uint32_t i = 0; i < length; i++) { + const char* color; + JSObject* obj = observers->get()[i]; + if (!obj) { + color = "dead"; + } else { + gc::TenuredCell* cell = &obj->asTenured(); + if (cell->isMarkedGray()) { + color = "gray"; + } else if (cell->isMarkedBlack()) { + color = "black"; + } else { + color = "unmarked"; + } + } + JSString* s = JS_NewStringCopyZ(cx, color); + if (!s) { + return false; + } + if (!NewbornArrayPush(cx, ret, StringValue(s))) { + return false; + } + } + + args.rval().setObject(*ret); + return true; +} + +namespace js { +namespace shell { + +class ShellAutoEntryMonitor : JS::dbg::AutoEntryMonitor { + Vector log; + bool oom; + bool enteredWithoutExit; + + public: + explicit ShellAutoEntryMonitor(JSContext* cx) + : AutoEntryMonitor(cx), oom(false), enteredWithoutExit(false) {} + + ~ShellAutoEntryMonitor() { MOZ_ASSERT(!enteredWithoutExit); } + + void Entry(JSContext* cx, JSFunction* function, JS::HandleValue asyncStack, + const char* asyncCause) override { + MOZ_ASSERT(!enteredWithoutExit); + enteredWithoutExit = true; + + RootedString displayId(cx, JS_GetFunctionDisplayId(function)); + if (displayId) { + UniqueChars displayIdStr = JS_EncodeStringToUTF8(cx, displayId); + if (!displayIdStr) { + // We report OOM in buildResult. + cx->recoverFromOutOfMemory(); + oom = true; + return; + } + oom = !log.append(std::move(displayIdStr)); + return; + } + + oom = !log.append(DuplicateString("anonymous")); + } + + void Entry(JSContext* cx, JSScript* script, JS::HandleValue asyncStack, + const char* asyncCause) override { + MOZ_ASSERT(!enteredWithoutExit); + enteredWithoutExit = true; + + UniqueChars label(JS_smprintf("eval:%s", JS_GetScriptFilename(script))); + oom = !label || !log.append(std::move(label)); + } + + void Exit(JSContext* cx) override { + MOZ_ASSERT(enteredWithoutExit); + enteredWithoutExit = false; + } + + bool buildResult(JSContext* cx, MutableHandleValue resultValue) { + if (oom) { + JS_ReportOutOfMemory(cx); + return false; + } + + RootedObject result(cx, JS::NewArrayObject(cx, log.length())); + if (!result) { + return false; + } + + for (size_t i = 0; i < log.length(); i++) { + char* name = log[i].get(); + RootedString string(cx, Atomize(cx, name, strlen(name))); + if (!string) { + return false; + } + RootedValue value(cx, StringValue(string)); + if (!JS_SetElement(cx, result, i, value)) { + return false; + } + } + + resultValue.setObject(*result.get()); + return true; + } +}; + +} // namespace shell +} // namespace js + +static bool EntryPoints(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "Wrong number of arguments"); + return false; + } + + RootedObject opts(cx, ToObject(cx, args[0])); + if (!opts) { + return false; + } + + // { function: f } --- Call f. + { + RootedValue fun(cx), dummy(cx); + + if (!JS_GetProperty(cx, opts, "function", &fun)) { + return false; + } + if (!fun.isUndefined()) { + js::shell::ShellAutoEntryMonitor sarep(cx); + if (!Call(cx, UndefinedHandleValue, fun, JS::HandleValueArray::empty(), + &dummy)) { + return false; + } + return sarep.buildResult(cx, args.rval()); + } + } + + // { object: o, property: p, value: v } --- Fetch o[p], or if + // v is present, assign o[p] = v. + { + RootedValue objectv(cx), propv(cx), valuev(cx); + + if (!JS_GetProperty(cx, opts, "object", &objectv) || + !JS_GetProperty(cx, opts, "property", &propv)) + return false; + if (!objectv.isUndefined() && !propv.isUndefined()) { + RootedObject object(cx, ToObject(cx, objectv)); + if (!object) { + return false; + } + + RootedString string(cx, ToString(cx, propv)); + if (!string) { + return false; + } + RootedId id(cx); + if (!JS_StringToId(cx, string, &id)) { + return false; + } + + if (!JS_GetProperty(cx, opts, "value", &valuev)) { + return false; + } + + js::shell::ShellAutoEntryMonitor sarep(cx); + + if (!valuev.isUndefined()) { + if (!JS_SetPropertyById(cx, object, id, valuev)) { + return false; + } + } else { + if (!JS_GetPropertyById(cx, object, id, &valuev)) { + return false; + } + } + + return sarep.buildResult(cx, args.rval()); + } + } + + // { ToString: v } --- Apply JS::ToString to v. + { + RootedValue v(cx); + + if (!JS_GetProperty(cx, opts, "ToString", &v)) { + return false; + } + if (!v.isUndefined()) { + js::shell::ShellAutoEntryMonitor sarep(cx); + if (!JS::ToString(cx, v)) { + return false; + } + return sarep.buildResult(cx, args.rval()); + } + } + + // { ToNumber: v } --- Apply JS::ToNumber to v. + { + RootedValue v(cx); + double dummy; + + if (!JS_GetProperty(cx, opts, "ToNumber", &v)) { + return false; + } + if (!v.isUndefined()) { + js::shell::ShellAutoEntryMonitor sarep(cx); + if (!JS::ToNumber(cx, v, &dummy)) { + return false; + } + return sarep.buildResult(cx, args.rval()); + } + } + + // { eval: code } --- Apply ToString and then Evaluate to code. + { + RootedValue code(cx), dummy(cx); + + if (!JS_GetProperty(cx, opts, "eval", &code)) { + return false; + } + if (!code.isUndefined()) { + RootedString codeString(cx, ToString(cx, code)); + if (!codeString) { + return false; + } + + AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, codeString)) { + return false; + } + JS::SourceText srcBuf; + if (!srcBuf.init(cx, stableChars.twoByteRange().begin().get(), + codeString->length(), JS::SourceOwnership::Borrowed)) { + return false; + } + + CompileOptions options(cx); + options.setIntroductionType("entryPoint eval") + .setFileAndLine("entryPoint eval", 1); + + js::shell::ShellAutoEntryMonitor sarep(cx); + if (!JS::Evaluate(cx, options, srcBuf, &dummy)) { + return false; + } + return sarep.buildResult(cx, args.rval()); + } + } + + JS_ReportErrorASCII(cx, "bad 'params' object"); + return false; +} + +static bool WasmTextToBinary(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (!args.requireAtLeast(cx, "wasmTextToBinary", 1)) { + return false; + } + + if (!args[0].isString()) { + ReportUsageErrorASCII(cx, callee, "First argument must be a String"); + return false; + } + + size_t textLen = args[0].toString()->length(); + + AutoStableStringChars twoByteChars(cx); + if (!twoByteChars.initTwoByte(cx, args[0].toString())) { + return false; + } + + wasm::Bytes bytes; + UniqueChars error; + if (!wasm::TextToBinary(twoByteChars.twoByteChars(), textLen, &bytes, + &error)) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_WASM_TEXT_FAIL, + error.get() ? error.get() : "out of memory"); + return false; + } + + RootedObject binary(cx, JS_NewUint8Array(cx, bytes.length())); + if (!binary) { + return false; + } + + memcpy(binary->as().dataPointerUnshared(), bytes.begin(), + bytes.length()); + + args.rval().setObject(*binary); + return true; +} + +static bool WasmCodeOffsets(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (!args.requireAtLeast(cx, "wasmCodeOffsets", 1)) { + return false; + } + + if (!args.get(0).isObject()) { + JS_ReportErrorASCII(cx, "argument is not an object"); + return false; + } + + SharedMem bytes; + size_t byteLength; + + JSObject* bufferObject = &args[0].toObject(); + JSObject* unwrappedBufferObject = CheckedUnwrapStatic(bufferObject); + if (!unwrappedBufferObject || + !IsBufferSource(unwrappedBufferObject, &bytes, &byteLength)) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_WASM_BAD_BUF_ARG); + return false; + } + + wasm::Uint32Vector offsets; + wasm::CodeOffsets(bytes.unwrap(), byteLength, &offsets); + + RootedObject jsOffsets(cx, JS::NewArrayObject(cx, offsets.length())); + if (!jsOffsets) { + return false; + } + for (size_t i = 0; i < offsets.length(); i++) { + uint32_t offset = offsets[i]; + RootedValue offsetVal(cx, NumberValue(offset)); + if (!JS_SetElement(cx, jsOffsets, i, offsetVal)) { + return false; + } + } + args.rval().setObject(*jsOffsets); + return true; +} + +#ifndef __AFL_HAVE_MANUAL_CONTROL +# define __AFL_LOOP(x) true +#endif + +static bool WasmLoop(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (args.length() < 1 || args.length() > 2) { + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + if (!args[0].isString()) { + ReportUsageErrorASCII(cx, callee, "First argument must be a String"); + return false; + } + + RootedObject importObj(cx); + if (!args.get(1).isUndefined()) { + if (!args.get(1).isObject()) { + ReportUsageErrorASCII(cx, callee, + "Second argument, if present, must be an Object"); + return false; + } + importObj = &args[1].toObject(); + } + + RootedString givenPath(cx, args[0].toString()); + RootedString filename(cx, ResolvePath(cx, givenPath, RootRelative)); + if (!filename) { + return false; + } + + while (__AFL_LOOP(1000)) { + Rooted ret(cx, FileAsTypedArray(cx, filename)); + if (!ret) { + return false; + } + + Rooted typedArray(cx, &ret->as()); + RootedWasmInstanceObject instanceObj(cx); + if (!wasm::Eval(cx, typedArray, importObj, &instanceObj)) { + // Clear any pending exceptions, we don't care about them + cx->clearPendingException(); + } + } + +#ifdef __AFL_HAVE_MANUAL_CONTROL // to silence unreachable code warning + return true; +#endif +} + +static constexpr uint32_t DOM_OBJECT_SLOT = 0; +static constexpr uint32_t DOM_OBJECT_SLOT2 = 1; + +static const JSClass* GetDomClass(); + +static JSObject* GetDOMPrototype(JSContext* cx, JSObject* global); + +static const JSClass TransplantableDOMObjectClass = { + "TransplantableDOMObject", + JSCLASS_IS_DOMJSCLASS | JSCLASS_HAS_RESERVED_SLOTS(1)}; + +static const JSClass TransplantableDOMProxyObjectClass = + PROXY_CLASS_DEF("TransplantableDOMProxyObject", + JSCLASS_IS_DOMJSCLASS | JSCLASS_HAS_RESERVED_SLOTS(1)); + +class TransplantableDOMProxyHandler final : public ForwardingProxyHandler { + public: + static const TransplantableDOMProxyHandler singleton; + static const char family; + + constexpr TransplantableDOMProxyHandler() : ForwardingProxyHandler(&family) {} + + // These two proxy traps are called in |js::DeadProxyTargetValue|, which in + // turn is called when nuking proxies. Because this proxy can temporarily be + // without an object in its private slot, see |EnsureExpandoObject|, the + // default implementation inherited from ForwardingProxyHandler can't be used, + // since it tries to derive the callable/constructible value from the target. + bool isCallable(JSObject* obj) const override { return false; } + bool isConstructor(JSObject* obj) const override { return false; } + + // Simplified implementation of |DOMProxyHandler::GetAndClearExpandoObject|. + static JSObject* GetAndClearExpandoObject(JSObject* obj) { + const Value& v = GetProxyPrivate(obj); + if (v.isUndefined()) { + return nullptr; + } + + JSObject* expandoObject = &v.toObject(); + SetProxyPrivate(obj, UndefinedValue()); + return expandoObject; + } + + // Simplified implementation of |DOMProxyHandler::EnsureExpandoObject|. + static JSObject* EnsureExpandoObject(JSContext* cx, JS::HandleObject obj) { + const Value& v = GetProxyPrivate(obj); + if (v.isObject()) { + return &v.toObject(); + } + MOZ_ASSERT(v.isUndefined()); + + JSObject* expando = JS_NewObjectWithGivenProto(cx, nullptr, nullptr); + if (!expando) { + return nullptr; + } + SetProxyPrivate(obj, ObjectValue(*expando)); + return expando; + } +}; + +const TransplantableDOMProxyHandler TransplantableDOMProxyHandler::singleton; +const char TransplantableDOMProxyHandler::family = 0; + +enum TransplantObjectSlots { + TransplantSourceObject = 0, +}; + +static bool TransplantObject(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedFunction callee(cx, &args.callee().as()); + + if (args.length() != 1 || !args[0].isObject()) { + JS_ReportErrorASCII(cx, "transplant() must be called with an object"); + return false; + } + + // |newGlobal| needs to be a GlobalObject. + RootedObject newGlobal( + cx, js::CheckedUnwrapDynamic(&args[0].toObject(), cx, + /* stopAtWindowProxy = */ false)); + if (!newGlobal) { + ReportAccessDenied(cx); + return false; + } + if (!JS_IsGlobalObject(newGlobal)) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, + "\"global\" passed to transplant()", "not a global object"); + return false; + } + + const Value& reserved = + GetFunctionNativeReserved(callee, TransplantSourceObject); + RootedObject source(cx, CheckedUnwrapStatic(&reserved.toObject())); + if (!source) { + ReportAccessDenied(cx); + return false; + } + MOZ_ASSERT(source->getClass()->isDOMClass()); + + // The following steps aim to replicate the behavior of UpdateReflectorGlobal + // in dom/bindings/BindingUtils.cpp. In detail: + // 1. Check the recursion depth using CheckRecursionLimitConservative. + // 2. Enter the target compartment. + // 3. Clone the source object using JS_CloneObject. + // 4. Check if new wrappers can be created if source and target are in + // different compartments. + // 5. Copy all properties from source to a temporary holder object. + // 6. Actually transplant the object. + // 7. And finally copy the properties back to the source object. + // + // As an extension to the algorithm in UpdateReflectorGlobal, we also allow + // to transplant an object into the same compartment as the source object to + // cover all operations supported by JS_TransplantObject. + + if (!CheckRecursionLimitConservative(cx)) { + return false; + } + + bool isProxy = IsProxy(source); + RootedObject expandoObject(cx); + if (isProxy) { + expandoObject = + TransplantableDOMProxyHandler::GetAndClearExpandoObject(source); + } + + JSAutoRealm ar(cx, newGlobal); + + RootedObject proto(cx); + if (JS::GetClass(source) == GetDomClass()) { + proto = GetDOMPrototype(cx, newGlobal); + } else { + proto = JS::GetRealmObjectPrototype(cx); + } + if (!proto) { + return false; + } + + RootedObject target(cx, JS_CloneObject(cx, source, proto)); + if (!target) { + return false; + } + + if (JS::GetCompartment(source) != JS::GetCompartment(target) && + !AllowNewWrapper(JS::GetCompartment(source), target)) { + JS_ReportErrorASCII(cx, "Cannot transplant into nuked compartment"); + return false; + } + + RootedObject copyFrom(cx, isProxy ? expandoObject : source); + RootedObject propertyHolder(cx, + JS_NewObjectWithGivenProto(cx, nullptr, nullptr)); + if (!propertyHolder) { + return false; + } + + if (!JS_CopyOwnPropertiesAndPrivateFields(cx, propertyHolder, copyFrom)) { + return false; + } + + JS::SetReservedSlot(target, DOM_OBJECT_SLOT, + JS::GetReservedSlot(source, DOM_OBJECT_SLOT)); + JS::SetReservedSlot(source, DOM_OBJECT_SLOT, JS::PrivateValue(nullptr)); + if (JS::GetClass(source) == GetDomClass()) { + JS::SetReservedSlot(target, DOM_OBJECT_SLOT2, + JS::GetReservedSlot(source, DOM_OBJECT_SLOT2)); + JS::SetReservedSlot(source, DOM_OBJECT_SLOT2, UndefinedValue()); + } + + source = JS_TransplantObject(cx, source, target); + if (!source) { + return false; + } + + RootedObject copyTo(cx); + if (isProxy) { + copyTo = TransplantableDOMProxyHandler::EnsureExpandoObject(cx, source); + if (!copyTo) { + return false; + } + } else { + copyTo = source; + } + if (!JS_CopyOwnPropertiesAndPrivateFields(cx, copyTo, propertyHolder)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool TransplantableObject(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (args.length() > 1) { + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + bool createProxy = false; + RootedObject source(cx); + if (args.length() == 1 && !args[0].isUndefined()) { + if (!args[0].isObject()) { + ReportUsageErrorASCII(cx, callee, "Argument must be an object"); + return false; + } + + RootedObject options(cx, &args[0].toObject()); + RootedValue value(cx); + + if (!JS_GetProperty(cx, options, "proxy", &value)) { + return false; + } + createProxy = JS::ToBoolean(value); + + if (!JS_GetProperty(cx, options, "object", &value)) { + return false; + } + if (!value.isUndefined()) { + if (!value.isObject()) { + ReportUsageErrorASCII(cx, callee, "'object' option must be an object"); + return false; + } + + source = &value.toObject(); + if (JS::GetClass(source) != GetDomClass()) { + ReportUsageErrorASCII(cx, callee, "Object not a FakeDOMObject"); + return false; + } + + // |source| must be a tenured object to be transplantable. + if (gc::IsInsideNursery(source)) { + JS_GC(cx); + + MOZ_ASSERT(!gc::IsInsideNursery(source), + "Live objects should be tenured after one GC, because " + "the nursery has only a single generation"); + } + } + } + + if (!source) { + if (!createProxy) { + source = NewBuiltinClassInstance(cx, &TransplantableDOMObjectClass, + TenuredObject); + if (!source) { + return false; + } + + JS::SetReservedSlot(source, DOM_OBJECT_SLOT, JS::PrivateValue(nullptr)); + } else { + JSObject* expando = JS_NewPlainObject(cx); + if (!expando) { + return false; + } + RootedValue expandoVal(cx, ObjectValue(*expando)); + + ProxyOptions options; + options.setClass(&TransplantableDOMProxyObjectClass); + options.setLazyProto(true); + + source = NewProxyObject(cx, &TransplantableDOMProxyHandler::singleton, + expandoVal, nullptr, options); + if (!source) { + return false; + } + + SetProxyReservedSlot(source, DOM_OBJECT_SLOT, JS::PrivateValue(nullptr)); + } + } + + jsid emptyId = NameToId(cx->names().empty); + RootedObject transplant( + cx, NewFunctionByIdWithReserved(cx, TransplantObject, 0, 0, emptyId)); + if (!transplant) { + return false; + } + + SetFunctionNativeReserved(transplant, TransplantSourceObject, + ObjectValue(*source)); + + RootedObject result(cx, JS_NewPlainObject(cx)); + if (!result) { + return false; + } + + RootedValue sourceVal(cx, ObjectValue(*source)); + RootedValue transplantVal(cx, ObjectValue(*transplant)); + if (!JS_DefineProperty(cx, result, "object", sourceVal, 0) || + !JS_DefineProperty(cx, result, "transplant", transplantVal, 0)) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +// clang-format off +static const JSFunctionSpecWithHelp shell_functions[] = { + JS_FN_HELP("options", Options, 0, 0, +"options([option ...])", +" Get or toggle JavaScript options."), + + JS_FN_HELP("load", Load, 1, 0, +"load(['foo.js' ...])", +" Load files named by string arguments. Filename is relative to the\n" +" current working directory."), + + JS_FN_HELP("loadRelativeToScript", LoadScriptRelativeToScript, 1, 0, +"loadRelativeToScript(['foo.js' ...])", +" Load files named by string arguments. Filename is relative to the\n" +" calling script."), + + JS_FN_HELP("evaluate", Evaluate, 2, 0, +"evaluate(code[, options])", +" Evaluate code as though it were the contents of a file.\n" +" options is an optional object that may have these properties:\n" +" isRunOnce: use the isRunOnce compiler option (default: false)\n" +" noScriptRval: use the no-script-rval compiler option (default: false)\n" +" fileName: filename for error messages and debug info\n" +" skipFileNameValidation: skip the filename-validation callback\n" +" lineNumber: starting line number for error messages and debug info\n" +" columnNumber: starting column number for error messages and debug info\n" +" global: global in which to execute the code\n" +" newContext: if true, create and use a new cx (default: false)\n" +" catchTermination: if true, catch termination (failure without\n" +" an exception value, as for slow scripts or out-of-memory)\n" +" and return 'terminated'\n" +" element: if present with value |v|, convert |v| to an object |o| and\n" +" mark the source as being attached to the DOM element |o|. If the\n" +" property is omitted or |v| is null, don't attribute the source to\n" +" any DOM element.\n" +" elementAttributeName: if present and not undefined, the name of\n" +" property of 'element' that holds this code. This is what\n" +" Debugger.Source.prototype.elementAttributeName returns.\n" +" sourceMapURL: if present with value |v|, convert |v| to a string, and\n" +" provide that as the code's source map URL. If omitted, attach no\n" +" source map URL to the code (although the code may provide one itself,\n" +" via a //#sourceMappingURL comment).\n" +" sourceIsLazy: if present and true, indicates that, after compilation, \n" +" script source should not be cached by the JS engine and should be \n" +" lazily loaded from the embedding as-needed.\n" +" forceFullParse: if present and true, disable syntax-parse.\n" +" loadBytecode: if true, and if the source is a CacheEntryObject,\n" +" the bytecode would be loaded and decoded from the cache entry instead\n" +" of being parsed, then it would be executed as usual.\n" +" saveBytecode: if true, and if the source is a CacheEntryObject,\n" +" the bytecode would be encoded and saved into the cache entry after\n" +" the script execution.\n" +" The encoded bytecode's kind is 'script'\n" +" saveIncrementalBytecode: if true, and if the source is a\n" +" CacheEntryObject, the bytecode would be incrementally encoded and\n" +" saved into the cache entry.\n" +" If --off-thread-parse-global is not used, the encoded bytecode's\n" +" kind is 'stencil'. If not, the encoded bytecode's kind is 'script'\n" +" If both loadBytecode and saveIncrementalBytecode are set,\n" +" and --off-thread-parse-global is not used, the input bytecode's\n" +" kind should be 'stencil'." +" assertEqBytecode: if true, and if both loadBytecode and either\n" +" saveBytecode or saveIncrementalBytecode is true, then the loaded\n" +" bytecode and the encoded bytecode are compared.\n" +" and an assertion is raised if they differ.\n" +" envChainObject: object to put on the scope chain, with its fields added\n" +" as var bindings, akin to how elements are added to the environment in\n" +" event handlers in Gecko.\n" +), + + JS_FN_HELP("run", Run, 1, 0, +"run('foo.js')", +" Run the file named by the first argument, returning the number of\n" +" of milliseconds spent compiling and executing it."), + + JS_FN_HELP("readline", ReadLine, 0, 0, +"readline()", +" Read a single line from stdin."), + + JS_FN_HELP("readlineBuf", ReadLineBuf, 1, 0, +"readlineBuf([ buf ])", +" Emulate readline() on the specified string. The first call with a string\n" +" argument sets the source buffer. Subsequent calls without an argument\n" +" then read from this buffer line by line.\n"), + + JS_FN_HELP("print", Print, 0, 0, +"print([exp ...])", +" Evaluate and print expressions to stdout."), + + JS_FN_HELP("printErr", PrintErr, 0, 0, +"printErr([exp ...])", +" Evaluate and print expressions to stderr."), + + JS_FN_HELP("putstr", PutStr, 0, 0, +"putstr([exp])", +" Evaluate and print expression without newline."), + + JS_FN_HELP("dateNow", Now, 0, 0, +"dateNow()", +" Return the current time with sub-ms precision."), + + JS_FN_HELP("help", Help, 0, 0, +"help([function or interface object or /pattern/])", +" Display usage and help messages."), + + JS_FN_HELP("quit", Quit, 0, 0, +"quit()", +" Quit the shell."), + + JS_FN_HELP("assertEq", AssertEq, 2, 0, +"assertEq(actual, expected[, msg])", +" Throw if the first two arguments are not the same (both +0 or both -0,\n" +" both NaN, or non-zero and ===)."), + + JS_FN_HELP("startTimingMutator", StartTimingMutator, 0, 0, +"startTimingMutator()", +" Start accounting time to mutator vs GC."), + + JS_FN_HELP("stopTimingMutator", StopTimingMutator, 0, 0, +"stopTimingMutator()", +" Stop accounting time to mutator vs GC and dump the results."), + + JS_FN_HELP("throwError", ThrowError, 0, 0, +"throwError()", +" Throw an error from JS_ReportError."), + + JS_FN_HELP("createErrorReport", CreateErrorReport, 1, 0, +"createErrorReport(value)", +" Create an JS::ErrorReportBuilder object from the given value and serialize\n" +" to an object."), + +#if defined(DEBUG) || defined(JS_JITSPEW) + JS_FN_HELP("disassemble", DisassembleToString, 1, 0, +"disassemble([fun/code])", +" Return the disassembly for the given function or code.\n" +" All disassembly functions take these options as leading string arguments:\n" +" \"-r\" (disassemble recursively)\n" +" \"-l\" (show line numbers)\n" +" \"-S\" (omit source notes)"), + + JS_FN_HELP("dis", Disassemble, 1, 0, +"dis([fun/code])", +" Disassemble functions into bytecodes."), + + JS_FN_HELP("disfile", DisassFile, 1, 0, +"disfile('foo.js')", +" Disassemble script file into bytecodes.\n"), + + JS_FN_HELP("dissrc", DisassWithSrc, 1, 0, +"dissrc([fun/code])", +" Disassemble functions with source lines."), + + JS_FN_HELP("notes", Notes, 1, 0, +"notes([fun])", +" Show source notes for functions."), + + JS_FN_HELP("stackDump", StackDump, 3, 0, +"stackDump(showArgs, showLocals, showThisProps)", +" Tries to print a lot of information about the current stack. \n" +" Similar to the DumpJSStack() function in the browser."), + +#endif + + JS_FN_HELP("intern", Intern, 1, 0, +"intern(str)", +" Internalize str in the atom table."), + + JS_FN_HELP("getslx", GetSLX, 1, 0, +"getslx(obj)", +" Get script line extent."), + + JS_FN_HELP("evalcx", EvalInContext, 1, 0, +"evalcx(s[, o])", +" Evaluate s in optional sandbox object o.\n" +" if (s == '' && !o) return new o with eager standard classes\n" +" if (s == 'lazy' && !o) return new o with lazy standard classes"), + + JS_FN_HELP("evalInWorker", EvalInWorker, 1, 0, +"evalInWorker(str)", +" Evaluate 'str' in a separate thread with its own runtime.\n"), + + JS_FN_HELP("getSharedObject", GetSharedObject, 0, 0, +"getSharedObject()", +" Retrieve the shared object from the cross-worker mailbox.\n" +" The object retrieved may not be identical to the object that was\n" +" installed, but it references the same shared memory.\n" +" getSharedObject performs an ordering memory barrier.\n"), + + JS_FN_HELP("setSharedObject", SetSharedObject, 0, 0, +"setSharedObject(obj)", +" Install the shared object in the cross-worker mailbox. The object\n" +" may be null. setSharedObject performs an ordering memory barrier.\n"), + + JS_FN_HELP("getSharedArrayBuffer", GetSharedObject, 0, 0, +"getSharedArrayBuffer()", +" Obsolete alias for getSharedObject().\n"), + + JS_FN_HELP("setSharedArrayBuffer", SetSharedObject, 0, 0, +"setSharedArrayBuffer(obj)", +" Obsolete alias for setSharedObject(obj).\n"), + + JS_FN_HELP("shapeOf", ShapeOf, 1, 0, +"shapeOf(obj)", +" Get the shape of obj (an implementation detail)."), + + JS_FN_HELP("groupOf", GroupOf, 1, 0, +"groupOf(obj)", +" Get the group of obj (an implementation detail)."), + + JS_FN_HELP("unwrappedObjectsHaveSameShape", UnwrappedObjectsHaveSameShape, 2, 0, +"unwrappedObjectsHaveSameShape(obj1, obj2)", +" Returns true iff obj1 and obj2 have the same shape, false otherwise. Both\n" +" objects are unwrapped first, so this can be used on objects from different\n" +" globals."), + +#ifdef DEBUG + JS_FN_HELP("arrayInfo", ArrayInfo, 1, 0, +"arrayInfo(a1, a2, ...)", +" Report statistics about arrays."), +#endif + + JS_FN_HELP("sleep", Sleep_fn, 1, 0, +"sleep(dt)", +" Sleep for dt seconds."), + + JS_FN_HELP("compile", Compile, 1, 0, +"compile(code)", +" Compiles a string to bytecode, potentially throwing."), + + JS_FN_HELP("parseModule", ParseModule, 1, 0, +"parseModule(code)", +" Parses source text as a module and returns a Module object."), + + JS_FN_HELP("codeModule", CodeModule, 1, 0, +"codeModule(module)", +" Takes an uninstantiated ModuleObject and returns a XDR bytecode\n" +" representation of that ModuleObject."), + + JS_FN_HELP("decodeModule", DecodeModule, 1, 0, +"decodeModule(code)", +" Takes a XDR bytecode representation of an uninstantiated ModuleObject and\n" +" returns a ModuleObject."), + + JS_FN_HELP("registerModule", RegisterModule, 2, 0, +"registerModule(specifier, module)", +" Register a module with the module loader, so that subsequent import from\n" +" |specifier| will resolve to |module|. Returns |module|."), + + JS_FN_HELP("dumpStencil", DumpStencil, 1, 0, +"dumpStencil(code, [options])", +" Parses a string and returns string that represents stencil.\n" +" See parse function's help for options"), + + JS_FN_HELP("parse", Parse, 1, 0, +"parse(code, [options])", +" Parses a string, potentially throwing. If present, |options| may\n" +" have properties saying how the code should be compiled:\n" +" module: if present and true, compile the source as module.\n" +" forceFullParse: if present and true, disable syntax-parse.\n" +" smoosh: if present and true, use SmooshMonkey."), + + JS_FN_HELP("syntaxParse", SyntaxParse, 1, 0, +"syntaxParse(code)", +" Check the syntax of a string, returning success value"), + + JS_FN_HELP("offThreadCompileScript", OffThreadCompileScript, 1, 0, +"offThreadCompileScript(code[, options])", +" Compile |code| on a helper thread, returning a job ID.\n" +" To wait for the compilation to finish and run the code, call\n" +" |runOffThreadScript| passing the job ID. If present, |options| may\n" +" have properties saying how the code should be compiled:\n" +" noScriptRval: use the no-script-rval compiler option (default: false)\n" +" fileName: filename for error messages and debug info\n" +" lineNumber: starting line number for error messages and debug info\n" +" columnNumber: starting column number for error messages and debug info\n" +" element: if present with value |v|, convert |v| to an object |o| and\n" +" mark the source as being attached to the DOM element |o|. If the\n" +" property is omitted or |v| is null, don't attribute the source to\n" +" any DOM element.\n" +" elementAttributeName: if present and not undefined, the name of\n" +" property of 'element' that holds this code. This is what\n" +" Debugger.Source.prototype.elementAttributeName returns."), + + JS_FN_HELP("runOffThreadScript", runOffThreadScript, 0, 0, +"runOffThreadScript([jobID])", +" Wait for an off-thread compilation job to complete. The job ID can be\n" +" ommitted if there is only one job pending. If an error occurred,\n" +" throw the appropriate exception; otherwise, run the script and return\n" +" its value."), + + JS_FN_HELP("offThreadCompileModule", OffThreadCompileModule, 1, 0, +"offThreadCompileModule(code)", +" Compile |code| on a helper thread, returning a job ID. To wait for the\n" +" compilation to finish and and get the module record object call\n" +" |finishOffThreadModule| passing the job ID."), + + JS_FN_HELP("finishOffThreadModule", FinishOffThreadModule, 0, 0, +"finishOffThreadModule([jobID])", +" Wait for an off-thread compilation job to complete. The job ID can be\n" +" ommitted if there is only one job pending. If an error occurred,\n" +" throw the appropriate exception; otherwise, return the module record object."), + + JS_FN_HELP("offThreadDecodeScript", OffThreadDecodeScript, 1, 0, +"offThreadDecodeScript(cacheEntry[, options])", +" Decode |code| on a helper thread, returning a job ID. To wait for the\n" +" decoding to finish and run the code, call |runOffThreadDecodeScript| passing\n" +" the job ID. If present, |options| may have properties saying how the code\n" +" should be compiled (see also offThreadCompileScript)."), + + JS_FN_HELP("runOffThreadDecodedScript", runOffThreadDecodedScript, 0, 0, +"runOffThreadDecodedScript([jobID])", +" Wait for off-thread decoding to complete. The job ID can be ommitted if there\n" +" is only one job pending. If an error occurred, throw the appropriate\n" +" exception; otherwise, run the script and return its value."), + + JS_FN_HELP("timeout", Timeout, 1, 0, +"timeout([seconds], [func])", +" Get/Set the limit in seconds for the execution time for the current context.\n" +" When the timeout expires the current interrupt callback is invoked.\n" +" The timeout is used just once. If the callback returns a falsy value, the\n" +" script is aborted. A negative value for seconds (this is the default) cancels\n" +" any pending timeout.\n" +" If a second argument is provided, it is installed as the interrupt handler,\n" +" exactly as if by |setInterruptCallback|.\n"), + + JS_FN_HELP("interruptIf", InterruptIf, 1, 0, +"interruptIf(cond)", +" Requests interrupt callback if cond is true. If a callback function is set via\n" +" |timeout| or |setInterruptCallback|, it will be called. No-op otherwise."), + + JS_FN_HELP("invokeInterruptCallback", InvokeInterruptCallbackWrapper, 0, 0, +"invokeInterruptCallback(fun)", +" Forcefully set the interrupt flag and invoke the interrupt handler. If a\n" +" callback function is set via |timeout| or |setInterruptCallback|, it will\n" +" be called. Before returning, fun is called with the return value of the\n" +" interrupt handler."), + + JS_FN_HELP("setInterruptCallback", SetInterruptCallback, 1, 0, +"setInterruptCallback(func)", +" Sets func as the interrupt callback function.\n" +" Calling this function will replace any callback set by |timeout|.\n" +" If the callback returns a falsy value, the script is aborted.\n"), + + JS_FN_HELP("setJitCompilerOption", SetJitCompilerOption, 2, 0, +"setJitCompilerOption(