/* -*- 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 "WebIDLGlobalNameHash.h"
#include "js/GCAPI.h"
#include "mozilla/HashFunctions.h"
#include "mozilla/Maybe.h"
#include "mozilla/dom/DOMJSProxyHandler.h"
#include "mozilla/dom/RegisterBindings.h"
#include "nsIMemoryReporter.h"
#include "nsTHashtable.h"

namespace mozilla {
namespace dom {

struct MOZ_STACK_CLASS WebIDLNameTableKey
{
  explicit WebIDLNameTableKey(JSFlatString* aJSString)
    : mLength(js::GetFlatStringLength(aJSString))
  {
    mNogc.emplace();
    JSLinearString* jsString = js::FlatStringToLinearString(aJSString);
    if (js::LinearStringHasLatin1Chars(jsString)) {
      mLatin1String = reinterpret_cast<const char*>(
        js::GetLatin1LinearStringChars(*mNogc, jsString));
      mTwoBytesString = nullptr;
      mHash = mLatin1String ? HashString(mLatin1String, mLength) : 0;
    } else {
      mLatin1String = nullptr;
      mTwoBytesString = js::GetTwoByteLinearStringChars(*mNogc, jsString);
      mHash = mTwoBytesString ? HashString(mTwoBytesString, mLength) : 0;
    }
  }
  explicit WebIDLNameTableKey(const char* aString, size_t aLength)
    : mLatin1String(aString),
      mTwoBytesString(nullptr),
      mLength(aLength),
      mHash(HashString(aString, aLength))
  {
    MOZ_ASSERT(aString[aLength] == '\0');
  }

  Maybe<JS::AutoCheckCannotGC> mNogc;
  const char* mLatin1String;
  const char16_t* mTwoBytesString;
  size_t mLength;
  uint32_t mHash;
};

struct WebIDLNameTableEntry : public PLDHashEntryHdr
{
  typedef const WebIDLNameTableKey& KeyType;
  typedef const WebIDLNameTableKey* KeyTypePointer;

  explicit WebIDLNameTableEntry(KeyTypePointer aKey)
    : mNameOffset(0),
      mNameLength(0),
      mDefine(nullptr),
      mEnabled(nullptr)
  {}
  WebIDLNameTableEntry(WebIDLNameTableEntry&& aEntry)
    : mNameOffset(aEntry.mNameOffset),
      mNameLength(aEntry.mNameLength),
      mDefine(aEntry.mDefine),
      mEnabled(aEntry.mEnabled)
  {}
  ~WebIDLNameTableEntry()
  {}

  bool KeyEquals(KeyTypePointer aKey) const
  {
    if (mNameLength != aKey->mLength) {
      return false;
    }

    const char* name = WebIDLGlobalNameHash::sNames + mNameOffset;

    if (aKey->mLatin1String) {
      return PodEqual(aKey->mLatin1String, name, aKey->mLength);
    }

    return nsCharTraits<char16_t>::compareASCII(aKey->mTwoBytesString, name,
                                                aKey->mLength) == 0;
  }

  static KeyTypePointer KeyToPointer(KeyType aKey)
  {
    return &aKey;
  }

  static PLDHashNumber HashKey(KeyTypePointer aKey)
  {
    return aKey->mHash;
  }

  enum { ALLOW_MEMMOVE = true };

  uint16_t mNameOffset;
  uint16_t mNameLength;
  WebIDLGlobalNameHash::DefineGlobalName mDefine;
  // May be null if enabled unconditionally
  WebIDLGlobalNameHash::ConstructorEnabled* mEnabled;
};

static nsTHashtable<WebIDLNameTableEntry>* sWebIDLGlobalNames;

class WebIDLGlobalNamesHashReporter final : public nsIMemoryReporter
{
  MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf)

  ~WebIDLGlobalNamesHashReporter() {}

public:
  NS_DECL_ISUPPORTS

  NS_IMETHOD CollectReports(nsIHandleReportCallback* aHandleReport,
                            nsISupports* aData, bool aAnonymize) override
  {
    int64_t amount =
      sWebIDLGlobalNames ?
      sWebIDLGlobalNames->ShallowSizeOfIncludingThis(MallocSizeOf) : 0;

    MOZ_COLLECT_REPORT(
      "explicit/dom/webidl-globalnames", KIND_HEAP, UNITS_BYTES, amount,
      "Memory used by the hash table for WebIDL's global names.");

    return NS_OK;
  }
};

NS_IMPL_ISUPPORTS(WebIDLGlobalNamesHashReporter, nsIMemoryReporter)

/* static */
void
WebIDLGlobalNameHash::Init()
{
  sWebIDLGlobalNames = new nsTHashtable<WebIDLNameTableEntry>(sCount);
  RegisterWebIDLGlobalNames();

  RegisterStrongMemoryReporter(new WebIDLGlobalNamesHashReporter());
}

/* static */
void
WebIDLGlobalNameHash::Shutdown()
{
  delete sWebIDLGlobalNames;
}

/* static */
void
WebIDLGlobalNameHash::Register(uint16_t aNameOffset, uint16_t aNameLength,
                               DefineGlobalName aDefine,
                               ConstructorEnabled* aEnabled)
{
  const char* name = sNames + aNameOffset;
  WebIDLNameTableKey key(name, aNameLength);
  WebIDLNameTableEntry* entry = sWebIDLGlobalNames->PutEntry(key);
  entry->mNameOffset = aNameOffset;
  entry->mNameLength = aNameLength;
  entry->mDefine = aDefine;
  entry->mEnabled = aEnabled;
}

/* static */
void
WebIDLGlobalNameHash::Remove(const char* aName, uint32_t aLength)
{
  WebIDLNameTableKey key(aName, aLength);
  sWebIDLGlobalNames->RemoveEntry(key);
}

/* static */
bool
WebIDLGlobalNameHash::DefineIfEnabled(JSContext* aCx,
                                      JS::Handle<JSObject*> aObj,
                                      JS::Handle<jsid> aId,
                                      JS::MutableHandle<JS::PropertyDescriptor> aDesc,
                                      bool* aFound)
{
  MOZ_ASSERT(JSID_IS_STRING(aId), "Check for string id before calling this!");

  const WebIDLNameTableEntry* entry;
  {
    WebIDLNameTableKey key(JSID_TO_FLAT_STRING(aId));
    // Rooting analysis thinks nsTHashtable<...>::GetEntry may GC because it
    // ends up calling through PLDHashTableOps' matchEntry function pointer, but
    // we know WebIDLNameTableEntry::KeyEquals can't cause a GC.
    JS::AutoSuppressGCAnalysis suppress;
    entry = sWebIDLGlobalNames->GetEntry(key);
  }

  if (!entry) {
    *aFound = false;
    return true;
  }

  *aFound = true;

  ConstructorEnabled* checkEnabledForScope = entry->mEnabled;
  // We do the enabled check on the current compartment of aCx, but for the
  // actual object we pass in the underlying object in the Xray case.  That
  // way the callee can decide whether to allow access based on the caller
  // or the window being touched.
  JS::Rooted<JSObject*> global(aCx,
    js::CheckedUnwrap(aObj, /* stopAtWindowProxy = */ false));
  if (!global) {
    return Throw(aCx, NS_ERROR_DOM_SECURITY_ERR);
  }

  {
    // It's safe to pass "&global" here, because we've already unwrapped it, but
    // for general sanity better to not have debug code even having the
    // appearance of mutating things that opt code uses.
#ifdef DEBUG
    JS::Rooted<JSObject*> temp(aCx, global);
    DebugOnly<nsGlobalWindow*> win;
    MOZ_ASSERT(NS_SUCCEEDED(UNWRAP_OBJECT(Window, &temp, win)));
#endif
  }

  if (checkEnabledForScope && !checkEnabledForScope(aCx, global)) {
    return true;
  }

  // The DOM constructor resolve machinery interacts with Xrays in tricky
  // ways, and there are some asymmetries that are important to understand.
  //
  // In the regular (non-Xray) case, we only want to resolve constructors
  // once (so that if they're deleted, they don't reappear). We do this by
  // stashing the constructor in a slot on the global, such that we can see
  // during resolve whether we've created it already. This is rather
  // memory-intensive, so we don't try to maintain these semantics when
  // manipulating a global over Xray (so the properties just re-resolve if
  // they've been deleted).
  //
  // Unfortunately, there's a bit of an impedance-mismatch between the Xray
  // and non-Xray machinery. The Xray machinery wants an API that returns a
  // JS::PropertyDescriptor, so that the resolve hook doesn't have to get
  // snared up with trying to define a property on the Xray holder. At the
  // same time, the DefineInterface callbacks are set up to define things
  // directly on the global.  And re-jiggering them to return property
  // descriptors is tricky, because some DefineInterface callbacks define
  // multiple things (like the Image() alias for HTMLImageElement).
  //
  // So the setup is as-follows:
  //
  // * The resolve function takes a JS::PropertyDescriptor, but in the
  //   non-Xray case, callees may define things directly on the global, and
  //   set the value on the property descriptor to |undefined| to indicate
  //   that there's nothing more for the caller to do. We assert against
  //   this behavior in the Xray case.
  //
  // * We make sure that we do a non-Xray resolve first, so that all the
  //   slots are set up. In the Xray case, this means unwrapping and doing
  //   a non-Xray resolve before doing the Xray resolve.
  //
  // This all could use some grand refactoring, but for now we just limp
  // along.
  if (xpc::WrapperFactory::IsXrayWrapper(aObj)) {
    JS::Rooted<JSObject*> interfaceObject(aCx);
    {
      JSAutoCompartment ac(aCx, global);
      interfaceObject = entry->mDefine(aCx, global, aId, false);
    }
    if (NS_WARN_IF(!interfaceObject)) {
      return Throw(aCx, NS_ERROR_FAILURE);
    }
    if (!JS_WrapObject(aCx, &interfaceObject)) {
      return Throw(aCx, NS_ERROR_FAILURE);
    }

    FillPropertyDescriptor(aDesc, aObj, 0, JS::ObjectValue(*interfaceObject));
    return true;
  }

  JS::Rooted<JSObject*> interfaceObject(aCx,
                                        entry->mDefine(aCx, aObj, aId, true));
  if (NS_WARN_IF(!interfaceObject)) {
    return Throw(aCx, NS_ERROR_FAILURE);
  }

  // We've already defined the property.  We indicate this to the caller
  // by filling a property descriptor with JS::UndefinedValue() as the
  // value.  We still have to fill in a property descriptor, though, so
  // that the caller knows the property is in fact on this object.  It
  // doesn't matter what we pass for the "readonly" argument here.
  FillPropertyDescriptor(aDesc, aObj, JS::UndefinedValue(), false);

  return true;
}

/* static */
bool
WebIDLGlobalNameHash::MayResolve(jsid aId)
{
  WebIDLNameTableKey key(JSID_TO_FLAT_STRING(aId));
  // Rooting analysis thinks nsTHashtable<...>::Contains may GC because it ends
  // up calling through PLDHashTableOps' matchEntry function pointer, but we
  // know WebIDLNameTableEntry::KeyEquals can't cause a GC.
  JS::AutoSuppressGCAnalysis suppress;
  return sWebIDLGlobalNames->Contains(key);
}

/* static */
void
WebIDLGlobalNameHash::GetNames(JSContext* aCx, JS::Handle<JSObject*> aObj,
                               nsTArray<nsString>& aNames)
{
  for (auto iter = sWebIDLGlobalNames->Iter(); !iter.Done(); iter.Next()) {
    const WebIDLNameTableEntry* entry = iter.Get();
    if (!entry->mEnabled || entry->mEnabled(aCx, aObj)) {
      AppendASCIItoUTF16(nsDependentCString(sNames + entry->mNameOffset,
                                            entry->mNameLength),
                         *aNames.AppendElement());
    }
  }
}

} // namespace dom
} // namespace mozilla
