/* 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 "debugger/Debugger.h"  // for Env, Debugger, ValueToIdentifier
#include "debugger/Object.h"    // for DebuggerObject
#include "debugger/Script.h"    // for DebuggerScript
#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 "util/Identifier.h"          // for IsIdentifier
#include "vm/Compartment.h"           // for Compartment
#include "vm/JSAtomUtils.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 mozilla::Maybe;
using mozilla::Nothing;
using mozilla::Some;

const JSClassOps DebuggerEnvironment::classOps_ = {
    .trace = CallTraceMethod<DebuggerEnvironment>,
};

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;
  }

  RootedId id(cx);
  if (!ValueToIdentifier(cx, args[0], &id)) {
    return false;
  }

  if (!environment->requireDebuggee(cx)) {
    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;
  }

  RootedId id(cx);
  if (!ValueToIdentifier(cx, args[0], &id)) {
    return false;
  }

  if (!environment->requireDebuggee(cx)) {
    return false;
  }

  return DebuggerEnvironment::getVariable(cx, environment, id, args.rval());
}

bool DebuggerEnvironment::CallData::setVariableMethod() {
  if (!args.requireAtLeast(cx, "Debugger.Environment.setVariable", 2)) {
    return false;
  }

  RootedId id(cx);
  if (!ValueToIdentifier(cx, args[0], &id)) {
    return false;
  }

  if (!environment->requireDebuggee(cx)) {
    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) {
  NewObjectKind newKind = GetNewObjectKind(referent);
  DebuggerEnvironment* obj = NewObjectWithGivenProto<DebuggerEnvironment>(
      cx, proto, {.newKind = newKind});
  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;
}
