summaryrefslogtreecommitdiffstats
path: root/js/src/debugger/Environment.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/debugger/Environment.cpp')
-rw-r--r--js/src/debugger/Environment.cpp665
1 files changed, 665 insertions, 0 deletions
diff --git a/js/src/debugger/Environment.cpp b/js/src/debugger/Environment.cpp
new file mode 100644
index 0000000000..f01a5a596e
--- /dev/null
+++ b/js/src/debugger/Environment.cpp
@@ -0,0 +1,665 @@
+/* -*- 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/. */
+
+#include "debugger/Environment-inl.h"
+
+#include "mozilla/Assertions.h" // for AssertionConditionType
+#include "mozilla/Maybe.h" // for Maybe, Some, Nothing
+#include "mozilla/Vector.h" // for Vector
+
+#include <string.h> // for strlen, size_t
+#include <utility> // for move
+
+#include "debugger/Debugger.h" // for Env, Debugger, ValueToIdentifier
+#include "debugger/Object.h" // for DebuggerObject
+#include "debugger/Script.h" // for DebuggerScript
+#include "frontend/BytecodeCompiler.h" // for IsIdentifier
+#include "gc/Tracer.h" // for TraceManuallyBarrieredCrossCompartmentEdge
+#include "js/CallArgs.h" // for CallArgs
+#include "js/friend/ErrorMessages.h" // for GetErrorMessage, JSMSG_*
+#include "js/HeapAPI.h" // for IsInsideNursery
+#include "js/RootingAPI.h" // for Rooted, MutableHandle
+#include "vm/Compartment.h" // for Compartment
+#include "vm/JSAtom.h" // for Atomize
+#include "vm/JSContext.h" // for JSContext
+#include "vm/JSFunction.h" // for JSFunction
+#include "vm/JSObject.h" // for JSObject, RequireObject,
+#include "vm/NativeObject.h" // for NativeObject, JSObject::is
+#include "vm/Realm.h" // for AutoRealm, ErrorCopier
+#include "vm/Scope.h" // for ScopeKind, ScopeKindString
+#include "vm/StringType.h" // for JSAtom
+
+#include "gc/StableCellHasher-inl.h"
+#include "vm/Compartment-inl.h" // for Compartment::wrap
+#include "vm/EnvironmentObject-inl.h" // for JSObject::enclosingEnvironment
+#include "vm/JSObject-inl.h" // for IsInternalFunctionObject, NewObjectWithGivenProtoAndKind
+#include "vm/ObjectOperations-inl.h" // for HasProperty, GetProperty
+#include "vm/Realm-inl.h" // for AutoRealm::AutoRealm
+
+namespace js {
+class GlobalObject;
+}
+
+using namespace js;
+
+using js::frontend::IsIdentifier;
+using mozilla::Maybe;
+using mozilla::Nothing;
+using mozilla::Some;
+
+const JSClassOps DebuggerEnvironment::classOps_ = {
+ nullptr, // addProperty
+ nullptr, // delProperty
+ nullptr, // enumerate
+ nullptr, // newEnumerate
+ nullptr, // resolve
+ nullptr, // mayResolve
+ nullptr, // finalize
+ nullptr, // call
+ nullptr, // construct
+ CallTraceMethod<DebuggerEnvironment>, // trace
+};
+
+const JSClass DebuggerEnvironment::class_ = {
+ "Environment",
+ JSCLASS_HAS_RESERVED_SLOTS(DebuggerEnvironment::RESERVED_SLOTS),
+ &classOps_};
+
+void DebuggerEnvironment::trace(JSTracer* trc) {
+ // There is a barrier on private pointers, so the Unbarriered marking
+ // is okay.
+ if (Env* referent = maybeReferent()) {
+ TraceManuallyBarrieredCrossCompartmentEdge(trc, this, &referent,
+ "Debugger.Environment referent");
+ if (referent != maybeReferent()) {
+ setReservedSlotGCThingAsPrivateUnbarriered(ENV_SLOT, referent);
+ }
+ }
+}
+
+static DebuggerEnvironment* DebuggerEnvironment_checkThis(
+ JSContext* cx, const CallArgs& args) {
+ JSObject* thisobj = RequireObject(cx, args.thisv());
+ if (!thisobj) {
+ return nullptr;
+ }
+ if (!thisobj->is<DebuggerEnvironment>()) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_INCOMPATIBLE_PROTO, "Debugger.Environment",
+ "method", thisobj->getClass()->name);
+ return nullptr;
+ }
+
+ return &thisobj->as<DebuggerEnvironment>();
+}
+
+struct MOZ_STACK_CLASS DebuggerEnvironment::CallData {
+ JSContext* cx;
+ const CallArgs& args;
+
+ Handle<DebuggerEnvironment*> environment;
+
+ CallData(JSContext* cx, const CallArgs& args,
+ Handle<DebuggerEnvironment*> env)
+ : cx(cx), args(args), environment(env) {}
+
+ bool typeGetter();
+ bool scopeKindGetter();
+ bool parentGetter();
+ bool objectGetter();
+ bool calleeScriptGetter();
+ bool inspectableGetter();
+ bool optimizedOutGetter();
+
+ bool namesMethod();
+ bool findMethod();
+ bool getVariableMethod();
+ bool setVariableMethod();
+
+ using Method = bool (CallData::*)();
+
+ template <Method MyMethod>
+ static bool ToNative(JSContext* cx, unsigned argc, Value* vp);
+};
+
+template <DebuggerEnvironment::CallData::Method MyMethod>
+/* static */
+bool DebuggerEnvironment::CallData::ToNative(JSContext* cx, unsigned argc,
+ Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ Rooted<DebuggerEnvironment*> environment(
+ cx, DebuggerEnvironment_checkThis(cx, args));
+ if (!environment) {
+ return false;
+ }
+
+ CallData data(cx, args, environment);
+ return (data.*MyMethod)();
+}
+
+/* static */
+bool DebuggerEnvironment::construct(JSContext* cx, unsigned argc, Value* vp) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR,
+ "Debugger.Environment");
+ return false;
+}
+
+static bool IsDeclarative(Env* env) {
+ return env->is<DebugEnvironmentProxy>() &&
+ env->as<DebugEnvironmentProxy>().isForDeclarative();
+}
+
+template <typename T>
+static bool IsDebugEnvironmentWrapper(Env* env) {
+ return env->is<DebugEnvironmentProxy>() &&
+ env->as<DebugEnvironmentProxy>().environment().is<T>();
+}
+
+bool DebuggerEnvironment::CallData::typeGetter() {
+ if (!environment->requireDebuggee(cx)) {
+ return false;
+ }
+
+ DebuggerEnvironmentType type = environment->type();
+
+ const char* s;
+ switch (type) {
+ case DebuggerEnvironmentType::Declarative:
+ s = "declarative";
+ break;
+ case DebuggerEnvironmentType::With:
+ s = "with";
+ break;
+ case DebuggerEnvironmentType::Object:
+ s = "object";
+ break;
+ }
+
+ JSAtom* str = Atomize(cx, s, strlen(s));
+ if (!str) {
+ return false;
+ }
+
+ args.rval().setString(str);
+ return true;
+}
+
+bool DebuggerEnvironment::CallData::scopeKindGetter() {
+ if (!environment->requireDebuggee(cx)) {
+ return false;
+ }
+
+ Maybe<ScopeKind> kind = environment->scopeKind();
+ if (kind.isSome()) {
+ const char* s = ScopeKindString(*kind);
+ JSAtom* str = Atomize(cx, s, strlen(s));
+ if (!str) {
+ return false;
+ }
+ args.rval().setString(str);
+ } else {
+ args.rval().setNull();
+ }
+
+ return true;
+}
+
+bool DebuggerEnvironment::CallData::parentGetter() {
+ if (!environment->requireDebuggee(cx)) {
+ return false;
+ }
+
+ Rooted<DebuggerEnvironment*> result(cx);
+ if (!environment->getParent(cx, &result)) {
+ return false;
+ }
+
+ args.rval().setObjectOrNull(result);
+ return true;
+}
+
+bool DebuggerEnvironment::CallData::objectGetter() {
+ if (!environment->requireDebuggee(cx)) {
+ return false;
+ }
+
+ if (environment->type() == DebuggerEnvironmentType::Declarative) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_DEBUG_NO_ENV_OBJECT);
+ return false;
+ }
+
+ Rooted<DebuggerObject*> result(cx);
+ if (!environment->getObject(cx, &result)) {
+ return false;
+ }
+
+ args.rval().setObject(*result);
+ return true;
+}
+
+bool DebuggerEnvironment::CallData::calleeScriptGetter() {
+ if (!environment->requireDebuggee(cx)) {
+ return false;
+ }
+
+ Rooted<DebuggerScript*> result(cx);
+ if (!environment->getCalleeScript(cx, &result)) {
+ return false;
+ }
+
+ args.rval().setObjectOrNull(result);
+ return true;
+}
+
+bool DebuggerEnvironment::CallData::inspectableGetter() {
+ args.rval().setBoolean(environment->isDebuggee());
+ return true;
+}
+
+bool DebuggerEnvironment::CallData::optimizedOutGetter() {
+ args.rval().setBoolean(environment->isOptimized());
+ return true;
+}
+
+bool DebuggerEnvironment::CallData::namesMethod() {
+ if (!environment->requireDebuggee(cx)) {
+ return false;
+ }
+
+ RootedIdVector ids(cx);
+ if (!DebuggerEnvironment::getNames(cx, environment, &ids)) {
+ return false;
+ }
+
+ JSObject* obj = IdVectorToArray(cx, ids);
+ if (!obj) {
+ return false;
+ }
+
+ args.rval().setObject(*obj);
+ return true;
+}
+
+bool DebuggerEnvironment::CallData::findMethod() {
+ if (!args.requireAtLeast(cx, "Debugger.Environment.find", 1)) {
+ return false;
+ }
+
+ if (!environment->requireDebuggee(cx)) {
+ return false;
+ }
+
+ RootedId id(cx);
+ if (!ValueToIdentifier(cx, args[0], &id)) {
+ return false;
+ }
+
+ Rooted<DebuggerEnvironment*> result(cx);
+ if (!DebuggerEnvironment::find(cx, environment, id, &result)) {
+ return false;
+ }
+
+ args.rval().setObjectOrNull(result);
+ return true;
+}
+
+bool DebuggerEnvironment::CallData::getVariableMethod() {
+ if (!args.requireAtLeast(cx, "Debugger.Environment.getVariable", 1)) {
+ return false;
+ }
+
+ if (!environment->requireDebuggee(cx)) {
+ return false;
+ }
+
+ RootedId id(cx);
+ if (!ValueToIdentifier(cx, args[0], &id)) {
+ return false;
+ }
+
+ return DebuggerEnvironment::getVariable(cx, environment, id, args.rval());
+}
+
+bool DebuggerEnvironment::CallData::setVariableMethod() {
+ if (!args.requireAtLeast(cx, "Debugger.Environment.setVariable", 2)) {
+ return false;
+ }
+
+ if (!environment->requireDebuggee(cx)) {
+ return false;
+ }
+
+ RootedId id(cx);
+ if (!ValueToIdentifier(cx, args[0], &id)) {
+ return false;
+ }
+
+ if (!DebuggerEnvironment::setVariable(cx, environment, id, args[1])) {
+ return false;
+ }
+
+ args.rval().setUndefined();
+ return true;
+}
+
+bool DebuggerEnvironment::requireDebuggee(JSContext* cx) const {
+ if (!isDebuggee()) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_DEBUG_NOT_DEBUGGEE, "Debugger.Environment",
+ "environment");
+
+ return false;
+ }
+
+ return true;
+}
+
+const JSPropertySpec DebuggerEnvironment::properties_[] = {
+ JS_DEBUG_PSG("type", typeGetter),
+ JS_DEBUG_PSG("scopeKind", scopeKindGetter),
+ JS_DEBUG_PSG("parent", parentGetter),
+ JS_DEBUG_PSG("object", objectGetter),
+ JS_DEBUG_PSG("calleeScript", calleeScriptGetter),
+ JS_DEBUG_PSG("inspectable", inspectableGetter),
+ JS_DEBUG_PSG("optimizedOut", optimizedOutGetter),
+ JS_PS_END};
+
+const JSFunctionSpec DebuggerEnvironment::methods_[] = {
+ JS_DEBUG_FN("names", namesMethod, 0), JS_DEBUG_FN("find", findMethod, 1),
+ JS_DEBUG_FN("getVariable", getVariableMethod, 1),
+ JS_DEBUG_FN("setVariable", setVariableMethod, 2), JS_FS_END};
+
+/* static */
+NativeObject* DebuggerEnvironment::initClass(JSContext* cx,
+ Handle<GlobalObject*> global,
+ HandleObject dbgCtor) {
+ return InitClass(cx, dbgCtor, nullptr, nullptr, "Environment", construct, 0,
+ properties_, methods_, nullptr, nullptr);
+}
+
+/* static */
+DebuggerEnvironment* DebuggerEnvironment::create(
+ JSContext* cx, HandleObject proto, HandleObject referent,
+ Handle<NativeObject*> debugger) {
+ DebuggerEnvironment* obj =
+ IsInsideNursery(referent)
+ ? NewObjectWithGivenProto<DebuggerEnvironment>(cx, proto)
+ : NewTenuredObjectWithGivenProto<DebuggerEnvironment>(cx, proto);
+ if (!obj) {
+ return nullptr;
+ }
+
+ obj->setReservedSlotGCThingAsPrivate(ENV_SLOT, referent);
+ obj->setReservedSlot(OWNER_SLOT, ObjectValue(*debugger));
+
+ return obj;
+}
+
+/* static */
+DebuggerEnvironmentType DebuggerEnvironment::type() const {
+ // Don't bother switching compartments just to check env's type.
+ if (IsDeclarative(referent())) {
+ return DebuggerEnvironmentType::Declarative;
+ }
+ if (IsDebugEnvironmentWrapper<WithEnvironmentObject>(referent())) {
+ return DebuggerEnvironmentType::With;
+ }
+ return DebuggerEnvironmentType::Object;
+}
+
+mozilla::Maybe<ScopeKind> DebuggerEnvironment::scopeKind() const {
+ if (!referent()->is<DebugEnvironmentProxy>()) {
+ return Nothing();
+ }
+ EnvironmentObject& env =
+ referent()->as<DebugEnvironmentProxy>().environment();
+ Scope* scope = GetEnvironmentScope(env);
+ return scope ? Some(scope->kind()) : Nothing();
+}
+
+bool DebuggerEnvironment::getParent(
+ JSContext* cx, MutableHandle<DebuggerEnvironment*> result) const {
+ // Don't bother switching compartments just to get env's parent.
+ Rooted<Env*> parent(cx, referent()->enclosingEnvironment());
+ if (!parent) {
+ result.set(nullptr);
+ return true;
+ }
+
+ return owner()->wrapEnvironment(cx, parent, result);
+}
+
+bool DebuggerEnvironment::getObject(
+ JSContext* cx, MutableHandle<DebuggerObject*> result) const {
+ MOZ_ASSERT(type() != DebuggerEnvironmentType::Declarative);
+
+ // Don't bother switching compartments just to get env's object.
+ RootedObject object(cx);
+ if (IsDebugEnvironmentWrapper<WithEnvironmentObject>(referent())) {
+ object.set(&referent()
+ ->as<DebugEnvironmentProxy>()
+ .environment()
+ .as<WithEnvironmentObject>()
+ .object());
+ } else if (IsDebugEnvironmentWrapper<NonSyntacticVariablesObject>(
+ referent())) {
+ object.set(&referent()
+ ->as<DebugEnvironmentProxy>()
+ .environment()
+ .as<NonSyntacticVariablesObject>());
+ } else {
+ object.set(referent());
+ MOZ_ASSERT(!object->is<DebugEnvironmentProxy>());
+ }
+
+ return owner()->wrapDebuggeeObject(cx, object, result);
+}
+
+bool DebuggerEnvironment::getCalleeScript(
+ JSContext* cx, MutableHandle<DebuggerScript*> result) const {
+ if (!referent()->is<DebugEnvironmentProxy>()) {
+ result.set(nullptr);
+ return true;
+ }
+
+ JSObject& scope = referent()->as<DebugEnvironmentProxy>().environment();
+ if (!scope.is<CallObject>()) {
+ result.set(nullptr);
+ return true;
+ }
+
+ Rooted<BaseScript*> script(cx, scope.as<CallObject>().callee().baseScript());
+
+ DebuggerScript* scriptObject = owner()->wrapScript(cx, script);
+ if (!scriptObject) {
+ return false;
+ }
+
+ result.set(scriptObject);
+ return true;
+}
+
+bool DebuggerEnvironment::isDebuggee() const {
+ MOZ_ASSERT(referent());
+ MOZ_ASSERT(!referent()->is<EnvironmentObject>());
+
+ return owner()->observesGlobal(&referent()->nonCCWGlobal());
+}
+
+bool DebuggerEnvironment::isOptimized() const {
+ return referent()->is<DebugEnvironmentProxy>() &&
+ referent()->as<DebugEnvironmentProxy>().isOptimizedOut();
+}
+
+/* static */
+bool DebuggerEnvironment::getNames(JSContext* cx,
+ Handle<DebuggerEnvironment*> environment,
+ MutableHandleIdVector result) {
+ MOZ_ASSERT(environment->isDebuggee());
+ MOZ_ASSERT(result.empty());
+
+ Rooted<Env*> referent(cx, environment->referent());
+ {
+ Maybe<AutoRealm> ar;
+ ar.emplace(cx, referent);
+
+ ErrorCopier ec(ar);
+ if (!GetPropertyKeys(cx, referent, JSITER_HIDDEN, result)) {
+ return false;
+ }
+ }
+
+ result.eraseIf([](PropertyKey key) {
+ return !key.isAtom() || !IsIdentifier(key.toAtom());
+ });
+
+ for (size_t i = 0; i < result.length(); ++i) {
+ cx->markAtom(result[i].toAtom());
+ }
+
+ return true;
+}
+
+/* static */
+bool DebuggerEnvironment::find(JSContext* cx,
+ Handle<DebuggerEnvironment*> environment,
+ HandleId id,
+ MutableHandle<DebuggerEnvironment*> result) {
+ MOZ_ASSERT(environment->isDebuggee());
+
+ Rooted<Env*> env(cx, environment->referent());
+ Debugger* dbg = environment->owner();
+
+ {
+ Maybe<AutoRealm> ar;
+ ar.emplace(cx, env);
+
+ cx->markId(id);
+
+ // This can trigger resolve hooks.
+ ErrorCopier ec(ar);
+ for (; env; env = env->enclosingEnvironment()) {
+ bool found;
+ if (!HasProperty(cx, env, id, &found)) {
+ return false;
+ }
+ if (found) {
+ break;
+ }
+ }
+ }
+
+ if (!env) {
+ result.set(nullptr);
+ return true;
+ }
+
+ return dbg->wrapEnvironment(cx, env, result);
+}
+
+/* static */
+bool DebuggerEnvironment::getVariable(JSContext* cx,
+ Handle<DebuggerEnvironment*> environment,
+ HandleId id, MutableHandleValue result) {
+ MOZ_ASSERT(environment->isDebuggee());
+
+ Rooted<Env*> referent(cx, environment->referent());
+ Debugger* dbg = environment->owner();
+
+ {
+ Maybe<AutoRealm> ar;
+ ar.emplace(cx, referent);
+
+ cx->markId(id);
+
+ // This can trigger getters.
+ ErrorCopier ec(ar);
+
+ bool found;
+ if (!HasProperty(cx, referent, id, &found)) {
+ return false;
+ }
+ if (!found) {
+ result.setUndefined();
+ return true;
+ }
+
+ // For DebugEnvironmentProxys, we get sentinel values for optimized out
+ // slots and arguments instead of throwing (the default behavior).
+ //
+ // See wrapDebuggeeValue for how the sentinel values are wrapped.
+ if (referent->is<DebugEnvironmentProxy>()) {
+ Rooted<DebugEnvironmentProxy*> env(
+ cx, &referent->as<DebugEnvironmentProxy>());
+ if (!DebugEnvironmentProxy::getMaybeSentinelValue(cx, env, id, result)) {
+ return false;
+ }
+ } else {
+ if (!GetProperty(cx, referent, referent, id, result)) {
+ return false;
+ }
+ }
+ }
+
+ // When we've faked up scope chain objects for optimized-out scopes,
+ // declarative environments may contain internal JSFunction objects, which
+ // we shouldn't expose to the user.
+ if (result.isObject()) {
+ RootedObject obj(cx, &result.toObject());
+ if (obj->is<JSFunction>() &&
+ IsInternalFunctionObject(obj->as<JSFunction>()))
+ result.setMagic(JS_OPTIMIZED_OUT);
+ }
+
+ return dbg->wrapDebuggeeValue(cx, result);
+}
+
+/* static */
+bool DebuggerEnvironment::setVariable(JSContext* cx,
+ Handle<DebuggerEnvironment*> environment,
+ HandleId id, HandleValue value_) {
+ MOZ_ASSERT(environment->isDebuggee());
+
+ Rooted<Env*> referent(cx, environment->referent());
+ Debugger* dbg = environment->owner();
+
+ RootedValue value(cx, value_);
+ if (!dbg->unwrapDebuggeeValue(cx, &value)) {
+ return false;
+ }
+
+ {
+ Maybe<AutoRealm> ar;
+ ar.emplace(cx, referent);
+ if (!cx->compartment()->wrap(cx, &value)) {
+ return false;
+ }
+ cx->markId(id);
+
+ // This can trigger setters.
+ ErrorCopier ec(ar);
+
+ // Make sure the environment actually has the specified binding.
+ bool found;
+ if (!HasProperty(cx, referent, id, &found)) {
+ return false;
+ }
+ if (!found) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_DEBUG_VARIABLE_NOT_FOUND);
+ return false;
+ }
+
+ // Just set the property.
+ if (!SetProperty(cx, referent, id, value)) {
+ return false;
+ }
+ }
+
+ return true;
+}