/* 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 vm_Compartment_inl_h
#define vm_Compartment_inl_h

#include "vm/Compartment.h"

#include "jsapi.h"
#include "jsfriendapi.h"

#include "builtin/Number.h"
#include "js/CallArgs.h"
#include "js/friend/ErrorMessages.h"  // js::GetErrorMessage, JSMSG_*
#include "js/Wrapper.h"
#include "vm/Iteration.h"
#include "vm/JSObject.h"

#include "vm/JSContext-inl.h"

struct JSClass;

inline js::StringWrapperMap::Ptr JS::Compartment::lookupWrapper(
    JSString* str) const {
  return zone()->crossZoneStringWrappers().lookup(str);
}

inline bool JS::Compartment::wrap(JSContext* cx, JS::MutableHandleValue vp) {
  /* Only GC things have to be wrapped or copied. */
  if (!vp.isGCThing()) {
    return true;
  }

  /*
   * Symbols are GC things, but never need to be wrapped or copied because
   * they are always allocated in the atoms zone. They still need to be
   * marked in the new compartment's zone, however.
   */
  if (vp.isSymbol()) {
    cx->markAtomValue(vp);
    return true;
  }

  /* Handle strings. */
  if (vp.isString()) {
    JS::RootedString str(cx, vp.toString());
    if (!wrap(cx, &str)) {
      return false;
    }
    vp.setString(str);
    return true;
  }

  if (vp.isBigInt()) {
    JS::RootedBigInt bi(cx, vp.toBigInt());
    if (!wrap(cx, &bi)) {
      return false;
    }
    vp.setBigInt(bi);
    return true;
  }

  MOZ_ASSERT(vp.isObject());

  /*
   * All that's left are objects.
   *
   * Object wrapping isn't the fastest thing in the world, in part because
   * we have to unwrap and invoke the prewrap hook to find the identity
   * object before we even start checking the cache. Neither of these
   * operations are needed in the common case, where we're just wrapping
   * a plain JS object from the wrappee's side of the membrane to the
   * wrapper's side.
   *
   * To optimize this, we note that the cache should only ever contain
   * identity objects - that is to say, objects that serve as the
   * canonical representation for a unique object identity observable by
   * script. Unwrap and prewrap are both steps that we take to get to the
   * identity of an incoming objects, and as such, they shuld never map
   * one identity object to another object. This means that we can safely
   * check the cache immediately, and only risk false negatives. Do this
   * in opt builds, and do both in debug builds so that we can assert
   * that we get the same answer.
   */
#ifdef DEBUG
  JS::AssertValueIsNotGray(vp);
  JS::RootedObject cacheResult(cx);
#endif
  if (js::ObjectWrapperMap::Ptr p = lookupWrapper(&vp.toObject())) {
#ifdef DEBUG
    cacheResult = p->value().get();
#else
    vp.setObject(*p->value().get());
    return true;
#endif
  }

  JS::RootedObject obj(cx, &vp.toObject());
  if (!wrap(cx, &obj)) {
    return false;
  }
  vp.setObject(*obj);
  MOZ_ASSERT_IF(cacheResult, obj == cacheResult);
  return true;
}

inline bool JS::Compartment::wrap(JSContext* cx,
                                  MutableHandle<mozilla::Maybe<Value>> vp) {
  if (vp.get().isNothing()) {
    return true;
  }

  return wrap(cx, MutableHandle<Value>::fromMarkedLocation(vp.get().ptr()));
}

namespace js {
namespace detail {

/**
 * Return the name of class T as a static null-terminated ASCII string constant
 * (for error messages).
 */
template <class T>
const char* ClassName() {
  return T::class_.name;
}

template <class T, class ErrorCallback>
[[nodiscard]] T* UnwrapAndTypeCheckValueSlowPath(JSContext* cx,
                                                 HandleValue value,
                                                 ErrorCallback throwTypeError) {
  JSObject* obj = nullptr;
  if (value.isObject()) {
    obj = &value.toObject();
    if (IsWrapper(obj)) {
      obj = CheckedUnwrapStatic(obj);
      if (!obj) {
        ReportAccessDenied(cx);
        return nullptr;
      }
    }
  }

  if (!obj || !obj->is<T>()) {
    throwTypeError();
    return nullptr;
  }

  return &obj->as<T>();
}

template <class ErrorCallback>
[[nodiscard]] JSObject* UnwrapAndTypeCheckValueSlowPath(
    JSContext* cx, HandleValue value, const JSClass* clasp,
    ErrorCallback throwTypeError) {
  JSObject* obj = nullptr;
  if (value.isObject()) {
    obj = &value.toObject();
    if (IsWrapper(obj)) {
      obj = CheckedUnwrapStatic(obj);
      if (!obj) {
        ReportAccessDenied(cx);
        return nullptr;
      }
    }
  }

  if (!obj || !obj->hasClass(clasp)) {
    throwTypeError();
    return nullptr;
  }

  return obj;
}

}  // namespace detail

/**
 * Remove all wrappers from `val` and try to downcast the result to class `T`.
 *
 * DANGER: The result may not be same-compartment with `cx`.
 *
 * This calls `throwTypeError` if the value isn't an object, cannot be
 * unwrapped, or isn't an instance of the expected type. `throwTypeError` must
 * in fact throw a TypeError (or OOM trying).
 */
template <class T, class ErrorCallback>
[[nodiscard]] inline T* UnwrapAndTypeCheckValue(JSContext* cx,
                                                HandleValue value,
                                                ErrorCallback throwTypeError) {
  cx->check(value);

  static_assert(!std::is_convertible_v<T*, Wrapper*>,
                "T can't be a Wrapper type; this function discards wrappers");

  if (value.isObject() && value.toObject().is<T>()) {
    return &value.toObject().as<T>();
  }

  return detail::UnwrapAndTypeCheckValueSlowPath<T>(cx, value, throwTypeError);
}

/**
 * Remove all wrappers from |val| and try to downcast the result to an object of
 * the class |clasp|.
 *
 * DANGER: The result may not be same-compartment with |cx|.
 *
 * This calls |throwTypeError| if the value isn't an object, cannot be
 * unwrapped, or isn't an instance of the expected type.  |throwTypeError| must
 * in fact throw a TypeError (or OOM trying).
 */
template <class ErrorCallback>
[[nodiscard]] inline JSObject* UnwrapAndTypeCheckValue(
    JSContext* cx, HandleValue value, const JSClass* clasp,
    ErrorCallback throwTypeError) {
  cx->check(value);

  if (value.isObject() && value.toObject().hasClass(clasp)) {
    return &value.toObject();
  }

  return detail::UnwrapAndTypeCheckValueSlowPath(cx, value, clasp,
                                                 throwTypeError);
}

/**
 * Remove all wrappers from `args.thisv()` and try to downcast the result to
 * class `T`.
 *
 * DANGER: The result may not be same-compartment with `cx`.
 *
 * This throws a TypeError if the value isn't an object, cannot be unwrapped,
 * or isn't an instance of the expected type.
 */
template <class T>
[[nodiscard]] inline T* UnwrapAndTypeCheckThis(JSContext* cx,
                                               const CallArgs& args,
                                               const char* methodName) {
  HandleValue thisv = args.thisv();
  return UnwrapAndTypeCheckValue<T>(cx, thisv, [cx, methodName, thisv] {
    JS_ReportErrorNumberLatin1(cx, GetErrorMessage, nullptr,
                               JSMSG_INCOMPATIBLE_PROTO, detail::ClassName<T>(),
                               methodName, InformalValueTypeName(thisv));
  });
}

/**
 * Remove all wrappers from `args[argIndex]` and try to downcast the result to
 * class `T`.
 *
 * DANGER: The result may not be same-compartment with `cx`.
 *
 * This throws a TypeError if the specified argument is missing, isn't an
 * object, cannot be unwrapped, or isn't an instance of the expected type.
 */
template <class T>
[[nodiscard]] inline T* UnwrapAndTypeCheckArgument(JSContext* cx,
                                                   CallArgs& args,
                                                   const char* methodName,
                                                   int argIndex) {
  HandleValue val = args.get(argIndex);
  return UnwrapAndTypeCheckValue<T>(cx, val, [cx, val, methodName, argIndex] {
    Int32ToCStringBuf cbuf;
    char* numStr = Int32ToCString(&cbuf, argIndex + 1);
    MOZ_ASSERT(numStr);
    JS_ReportErrorNumberLatin1(
        cx, GetErrorMessage, nullptr, JSMSG_WRONG_TYPE_ARG, numStr, methodName,
        detail::ClassName<T>(), InformalValueTypeName(val));
  });
}

/**
 * Unwrap an object of a known type.
 *
 * If `obj` is an object of class T, this returns a pointer to that object. If
 * `obj` is a wrapper for such an object, this tries to unwrap the object and
 * return a pointer to it. If access is denied, or `obj` was a wrapper but has
 * been nuked, this reports an error and returns null.
 *
 * In all other cases, the behavior is undefined, so call this only if `obj` is
 * known to have been an object of class T, or a wrapper to a T, at some point.
 */
template <class T>
[[nodiscard]] inline T* UnwrapAndDowncastObject(JSContext* cx, JSObject* obj) {
  static_assert(!std::is_convertible_v<T*, Wrapper*>,
                "T can't be a Wrapper type; this function discards wrappers");

  if (IsProxy(obj)) {
    if (JS_IsDeadWrapper(obj)) {
      JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                JSMSG_DEAD_OBJECT);
      return nullptr;
    }

    // It would probably be OK to do an unchecked unwrap here, but we allow
    // arbitrary security policies, so check anyway.
    obj = obj->maybeUnwrapAs<T>();
    if (!obj) {
      ReportAccessDenied(cx);
      return nullptr;
    }
  }

  return &obj->as<T>();
}

/**
 * Unwrap an object of a known (but not compile-time-known) class.
 *
 * If |obj| is an object with class |clasp|, this returns |obj|.  If |obj| is a
 * wrapper for such an object, this tries to unwrap the object and return a
 * pointer to it.  If access is denied, or |obj| was a wrapper but has been
 * nuked, this reports an error and returns null.
 *
 * In all other cases, the behavior is undefined, so call this only if |obj| is
 * known to have had class |clasp|, or been a wrapper to such an object, at some
 * point.
 */
[[nodiscard]] inline JSObject* UnwrapAndDowncastObject(JSContext* cx,
                                                       JSObject* obj,
                                                       const JSClass* clasp) {
  if (IsProxy(obj)) {
    if (JS_IsDeadWrapper(obj)) {
      JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                JSMSG_DEAD_OBJECT);
      return nullptr;
    }

    // It would probably be OK to do an unchecked unwrap here, but we allow
    // arbitrary security policies, so check anyway.
    obj = obj->maybeUnwrapAs(clasp);
    if (!obj) {
      ReportAccessDenied(cx);
      return nullptr;
    }
  }

  MOZ_ASSERT(obj->hasClass(clasp));
  return obj;
}

/**
 * Unwrap a value of a known type. See UnwrapAndDowncastObject.
 */
template <class T>
[[nodiscard]] inline T* UnwrapAndDowncastValue(JSContext* cx,
                                               const Value& value) {
  return UnwrapAndDowncastObject<T>(cx, &value.toObject());
}

/**
 * Unwrap an object of a known (but not compile-time-known) class.  See
 * UnwrapAndDowncastObject.
 */
[[nodiscard]] inline JSObject* UnwrapAndDowncastValue(JSContext* cx,
                                                      const Value& value,
                                                      const JSClass* clasp) {
  return UnwrapAndDowncastObject(cx, &value.toObject(), clasp);
}

}  // namespace js

MOZ_ALWAYS_INLINE bool JS::Compartment::objectMaybeInIteration(JSObject* obj) {
  MOZ_ASSERT(obj->compartment() == this);

  js::NativeIteratorListIter iter(&enumerators_);

  // If the list is empty, we're not iterating any objects.
  if (iter.done()) {
    return false;
  }

  // If the list contains a single object, check if it's |obj|.
  js::NativeIterator* next = iter.next();
  if (iter.done()) {
    return next->objectBeingIterated() == obj;
  }

  return true;
}

#endif /* vm_Compartment_inl_h */
