/* 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 "mozilla/dom/DOMException.h"

#include "js/StructuredClone.h"
#include "js/TypeDecls.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/HoldDropJSObjects.h"
#include "mozilla/dom/DOMExceptionBinding.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/Exceptions.h"
#include "nsCOMPtr.h"
#include "nsContentUtils.h"
#include "nsIException.h"
#include "xpcprivate.h"

using namespace mozilla;
using namespace mozilla::dom;

enum DOM4ErrorTypeCodeMap {
  /* DOM4 errors from
     http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#domexception */
  IndexSizeError = DOMException_Binding::INDEX_SIZE_ERR,
  HierarchyRequestError = DOMException_Binding::HIERARCHY_REQUEST_ERR,
  WrongDocumentError = DOMException_Binding::WRONG_DOCUMENT_ERR,
  InvalidCharacterError = DOMException_Binding::INVALID_CHARACTER_ERR,
  NoModificationAllowedError =
      DOMException_Binding::NO_MODIFICATION_ALLOWED_ERR,
  NotFoundError = DOMException_Binding::NOT_FOUND_ERR,
  NotSupportedError = DOMException_Binding::NOT_SUPPORTED_ERR,
  // Can't remove until setNamedItem is removed
  InUseAttributeError = DOMException_Binding::INUSE_ATTRIBUTE_ERR,
  InvalidStateError = DOMException_Binding::INVALID_STATE_ERR,
  SyntaxError = DOMException_Binding::SYNTAX_ERR,
  InvalidModificationError = DOMException_Binding::INVALID_MODIFICATION_ERR,
  NamespaceError = DOMException_Binding::NAMESPACE_ERR,
  InvalidAccessError = DOMException_Binding::INVALID_ACCESS_ERR,
  TypeMismatchError = DOMException_Binding::TYPE_MISMATCH_ERR,
  SecurityError = DOMException_Binding::SECURITY_ERR,
  NetworkError = DOMException_Binding::NETWORK_ERR,
  AbortError = DOMException_Binding::ABORT_ERR,
  URLMismatchError = DOMException_Binding::URL_MISMATCH_ERR,
  QuotaExceededError = DOMException_Binding::QUOTA_EXCEEDED_ERR,
  TimeoutError = DOMException_Binding::TIMEOUT_ERR,
  InvalidNodeTypeError = DOMException_Binding::INVALID_NODE_TYPE_ERR,
  DataCloneError = DOMException_Binding::DATA_CLONE_ERR,
  EncodingError = 0,

  /* IndexedDB errors
     http://dvcs.w3.org/hg/IndexedDB/raw-file/tip/Overview.html#exceptions */
  UnknownError = 0,
  ConstraintError = 0,
  DataError = 0,
  TransactionInactiveError = 0,
  ReadOnlyError = 0,
  VersionError = 0,

  /* File API errors http://dev.w3.org/2006/webapi/FileAPI/#ErrorAndException */
  NotReadableError = 0,

  /* FileHandle API errors */
  FileHandleInactiveError = 0,

  /* WebCrypto errors
     https://dvcs.w3.org/hg/webcrypto-api/raw-file/tip/spec/Overview.html#dfn-DataError
   */
  OperationError = 0,

  /* Push API errors */
  NotAllowedError = 0,
};

#define DOM4_MSG_DEF(name, message, nsresult) \
  {(nsresult), name, #name, message},
#define DOM_MSG_DEF(val, message) \
  {(val), NS_ERROR_GET_CODE(val), #val, message},

static constexpr struct ResultStruct {
  nsresult mNSResult;
  uint16_t mCode;
  const char* mName;
  const char* mMessage;
} sDOMErrorMsgMap[] = {
#include "domerr.msg"
};

#undef DOM4_MSG_DEF
#undef DOM_MSG_DEF

static void NSResultToNameAndMessage(nsresult aNSResult, nsCString& aName,
                                     nsCString& aMessage, uint16_t* aCode) {
  aName.Truncate();
  aMessage.Truncate();
  *aCode = 0;
  for (const auto& entry : sDOMErrorMsgMap) {
    if (aNSResult == entry.mNSResult) {
      aName.Rebind(entry.mName, strlen(entry.mName));
      aMessage.Rebind(entry.mMessage, strlen(entry.mMessage));
      *aCode = entry.mCode;
      return;
    }
  }

  NS_WARNING("Huh, someone is throwing non-DOM errors using the DOM module!");
}

nsresult NS_GetNameAndMessageForDOMNSResult(nsresult aNSResult,
                                            nsACString& aName,
                                            nsACString& aMessage,
                                            uint16_t* aCode) {
  nsCString name;
  nsCString message;
  uint16_t code = 0;
  NSResultToNameAndMessage(aNSResult, name, message, &code);

  if (!name.IsEmpty() && !message.IsEmpty()) {
    aName = std::move(name);
    aMessage = std::move(message);
    if (aCode) {
      *aCode = code;
    }
    return NS_OK;
  }

  return NS_ERROR_NOT_AVAILABLE;
}

namespace mozilla::dom {

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Exception)
  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
  NS_INTERFACE_MAP_ENTRY(Exception)
  NS_INTERFACE_MAP_ENTRY(nsIException)
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END

NS_IMPL_CYCLE_COLLECTING_ADDREF(Exception)
NS_IMPL_CYCLE_COLLECTING_RELEASE(Exception)

NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_WITH_JS_MEMBERS(Exception,
                                                      (mLocation, mData),
                                                      (mThrownJSVal))

Exception::Exception(const nsACString& aMessage, nsresult aResult,
                     const nsACString& aName, nsIStackFrame* aLocation,
                     nsISupports* aData)
    : mMessage(aMessage),
      mResult(aResult),
      mName(aName),
      mData(aData),
      mHoldingJSVal(false) {
  if (aLocation) {
    mLocation = aLocation;
  } else {
    mLocation = GetCurrentJSStack();
    // it is legal for there to be no active JS stack, if C++ code
    // is operating on a JS-implemented interface pointer without
    // having been called in turn by JS.  This happens in the JS
    // component loader.
  }
}

Exception::Exception(nsCString&& aMessage, nsresult aResult, nsCString&& aName)
    : mMessage(std::move(aMessage)),
      mResult(aResult),
      mName(std::move(aName)),
      mHoldingJSVal(false) {}

Exception::~Exception() {
  if (mHoldingJSVal) {
    MOZ_ASSERT(NS_IsMainThread());

    mozilla::DropJSObjects(this);
  }
}

bool Exception::StealJSVal(JS::Value* aVp) {
  MOZ_ASSERT(NS_IsMainThread());

  if (mHoldingJSVal) {
    *aVp = mThrownJSVal;

    mozilla::DropJSObjects(this);
    mHoldingJSVal = false;
    return true;
  }

  return false;
}

void Exception::StowJSVal(JS::Value& aVp) {
  MOZ_ASSERT(NS_IsMainThread());

  mThrownJSVal = aVp;
  if (!mHoldingJSVal) {
    mozilla::HoldJSObjects(this);
    mHoldingJSVal = true;
  }
}

void Exception::GetName(nsAString& aName) {
  if (!mName.IsEmpty()) {
    CopyUTF8toUTF16(mName, aName);
  } else {
    aName.Truncate();

    const char* name = nullptr;
    nsXPCException::NameAndFormatForNSResult(mResult, &name, nullptr);

    if (name) {
      CopyUTF8toUTF16(mozilla::MakeStringSpan(name), aName);
    }
  }
}

void Exception::GetFilename(JSContext* aCx, nsACString& aFilename) {
  if (mLocation) {
    mLocation->GetFilename(aCx, aFilename);
    return;
  }

  aFilename.Truncate();
}

void Exception::ToString(JSContext* aCx, nsACString& _retval) {
  static const char defaultMsg[] = "<no message>";
  static const char defaultLocation[] = "<unknown>";
  static const char format[] = "[Exception... \"%s\"  nsresult: \"0x%" PRIx32
                               " (%s)\"  location: \"%s\"  data: %s]";

  nsCString location;

  if (mLocation) {
    // we need to free this if it does not fail
    mLocation->ToString(aCx, location);
  }

  if (location.IsEmpty()) {
    location.Assign(defaultLocation);
  }

  const char* msg = mMessage.IsEmpty() ? nullptr : mMessage.get();

  const char* resultName = mName.IsEmpty() ? nullptr : mName.get();
  if (!resultName && !nsXPCException::NameAndFormatForNSResult(
                         mResult, &resultName, (!msg) ? &msg : nullptr)) {
    if (!msg) {
      msg = defaultMsg;
    }
    resultName = "<unknown>";
  }
  const char* data = mData ? "yes" : "no";

  _retval.Truncate();
  _retval.AppendPrintf(format, msg, static_cast<uint32_t>(mResult), resultName,
                       location.get(), data);
}

JSObject* Exception::WrapObject(JSContext* cx,
                                JS::Handle<JSObject*> aGivenProto) {
  return Exception_Binding::Wrap(cx, this, aGivenProto);
}

void Exception::GetMessageMoz(nsString& retval) {
  CopyUTF8toUTF16(mMessage, retval);
}

uint32_t Exception::Result() const { return (uint32_t)mResult; }

uint32_t Exception::SourceId(JSContext* aCx) const {
  if (mLocation) {
    return mLocation->GetSourceId(aCx);
  }

  return 0;
}

uint32_t Exception::LineNumber(JSContext* aCx) const {
  if (mLocation) {
    return mLocation->GetLineNumber(aCx);
  }

  return 0;
}

uint32_t Exception::ColumnNumber(JSContext* aCx) const {
  if (mLocation) {
    return mLocation->GetColumnNumber(aCx);
  }

  return 0;
}

already_AddRefed<nsIStackFrame> Exception::GetLocation() const {
  nsCOMPtr<nsIStackFrame> location = mLocation;
  return location.forget();
}

nsISupports* Exception::GetData() const { return mData; }

void Exception::GetStack(JSContext* aCx, nsAString& aStack) const {
  if (mLocation) {
    mLocation->GetFormattedStack(aCx, aStack);
  }
}

void Exception::Stringify(JSContext* aCx, nsString& retval) {
  nsCString str;
  ToString(aCx, str);
  CopyUTF8toUTF16(str, retval);
}

DOMException::DOMException(nsresult aRv, const nsACString& aMessage,
                           const nsACString& aName, uint16_t aCode,
                           nsIStackFrame* aLocation)
    : Exception(aMessage, aRv, aName, aLocation, nullptr), mCode(aCode) {}
DOMException::DOMException(nsresult aRv, nsCString&& aMessage,
                           nsCString&& aName, uint16_t aCode)
    : Exception(std::move(aMessage), aRv, std::move(aName)), mCode(aCode) {}

void DOMException::ToString(JSContext* aCx, nsACString& aReturn) {
  aReturn.Truncate();

  static const char defaultMsg[] = "<no message>";
  static const char defaultLocation[] = "<unknown>";
  static const char defaultName[] = "<unknown>";
  static const char format[] =
      "[Exception... \"%s\"  code: \"%d\" nsresult: \"0x%" PRIx32
      " (%s)\"  location: \"%s\"]";

  nsAutoCString location;

  if (location.IsEmpty()) {
    location = defaultLocation;
  }

  const char* msg = !mMessage.IsEmpty() ? mMessage.get() : defaultMsg;
  const char* resultName = !mName.IsEmpty() ? mName.get() : defaultName;

  aReturn.AppendPrintf(format, msg, mCode, static_cast<uint32_t>(mResult),
                       resultName, location.get());
}

void DOMException::GetName(nsString& retval) { CopyUTF8toUTF16(mName, retval); }

already_AddRefed<DOMException> DOMException::Constructor(
    GlobalObject& /* unused */, const nsAString& aMessage,
    const Optional<nsAString>& aName) {
  nsresult exceptionResult = NS_OK;
  uint16_t exceptionCode = 0;
  nsCString name("Error"_ns);

  if (aName.WasPassed()) {
    CopyUTF16toUTF8(aName.Value(), name);
    for (const auto& entry : sDOMErrorMsgMap) {
      if (name.EqualsASCII(entry.mName)) {
        exceptionResult = entry.mNSResult;
        exceptionCode = entry.mCode;
        break;
      }
    }
  }

  RefPtr<DOMException> retval = new DOMException(
      exceptionResult, NS_ConvertUTF16toUTF8(aMessage), name, exceptionCode);
  return retval.forget();
}

JSObject* DOMException::WrapObject(JSContext* aCx,
                                   JS::Handle<JSObject*> aGivenProto) {
  return DOMException_Binding::Wrap(aCx, this, aGivenProto);
}

/* static */
already_AddRefed<DOMException> DOMException::Create(nsresult aRv) {
  nsCString name;
  nsCString message;
  uint16_t code;
  NSResultToNameAndMessage(aRv, name, message, &code);
  RefPtr<DOMException> inst = new DOMException(aRv, message, name, code);
  return inst.forget();
}

/* static */
already_AddRefed<DOMException> DOMException::Create(
    nsresult aRv, const nsACString& aMessage) {
  nsCString name;
  nsCString message;
  uint16_t code;
  NSResultToNameAndMessage(aRv, name, message, &code);
  RefPtr<DOMException> inst = new DOMException(aRv, aMessage, name, code);
  return inst.forget();
}

static bool ReadAsCString(JSContext* aCx, JSStructuredCloneReader* aReader,
                          nsCString& aString) {
  JS::Rooted<JSString*> jsMessage(aCx);
  if (!JS_ReadString(aReader, &jsMessage)) {
    return false;
  }
  return AssignJSString(aCx, aString, jsMessage);
}

already_AddRefed<DOMException> DOMException::ReadStructuredClone(
    JSContext* aCx, nsIGlobalObject* aGlobal,
    JSStructuredCloneReader* aReader) {
  uint32_t reserved;
  nsresult rv;
  nsCString message;
  nsCString name;
  uint16_t code;

  if (!JS_ReadBytes(aReader, &reserved, 4) || !JS_ReadBytes(aReader, &rv, 4) ||
      !ReadAsCString(aCx, aReader, message) ||
      !ReadAsCString(aCx, aReader, name) || !JS_ReadBytes(aReader, &code, 2)) {
    return nullptr;
  };

  return do_AddRef(
      new DOMException(rv, std::move(message), std::move(name), code));
}

bool DOMException::WriteStructuredClone(
    JSContext* aCx, JSStructuredCloneWriter* aWriter) const {
  JS::Rooted<JS::Value> messageValue(aCx);
  JS::Rooted<JS::Value> nameValue(aCx);
  if (!NonVoidByteStringToJsval(aCx, mMessage, &messageValue) ||
      !NonVoidByteStringToJsval(aCx, mName, &nameValue)) {
    return false;
  }

  JS::Rooted<JSString*> message(aCx, messageValue.toString());
  JS::Rooted<JSString*> name(aCx, nameValue.toString());

  static_assert(sizeof(nsresult) == 4);

  // A reserved field. Use this to indicate stack serialization support etc.
  uint32_t reserved = 0;
  return JS_WriteBytes(aWriter, &reserved, 4) &&
         JS_WriteBytes(aWriter, &mResult, 4) &&
         JS_WriteString(aWriter, message) && JS_WriteString(aWriter, name) &&
         JS_WriteBytes(aWriter, &mCode, 2);
};

}  // namespace mozilla::dom
