/* 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/IndexedDatabase.h"

#include "IDBDatabase.h"
#include "IndexedDatabaseInlines.h"
#include "MainThreadUtils.h"
#include "jsapi.h"
#include "mozilla/EndianUtils.h"
#include "mozilla/dom/FileBlobImpl.h"
#include "mozilla/dom/FileList.h"
#include "mozilla/dom/StructuredCloneTags.h"
#include "mozilla/dom/URLSearchParams.h"
#include "mozilla/dom/WorkerPrivate.h"
#include "mozilla/dom/WorkerScope.h"
#include "nsIFile.h"
#include "nsIGlobalObject.h"
#include "nsQueryObject.h"
#include "nsString.h"

namespace mozilla::dom::indexedDB {
namespace {
struct MOZ_STACK_CLASS MutableFileData final {
  nsString type;
  nsString name;

  MOZ_COUNTED_DEFAULT_CTOR(MutableFileData)

  MOZ_COUNTED_DTOR(MutableFileData)
};

struct MOZ_STACK_CLASS BlobOrFileData final {
  uint32_t tag = 0;
  uint64_t size = 0;
  nsString type;
  nsString name;
  int64_t lastModifiedDate = INT64_MAX;

  MOZ_COUNTED_DEFAULT_CTOR(BlobOrFileData)

  MOZ_COUNTED_DTOR(BlobOrFileData)
};

struct MOZ_STACK_CLASS WasmModuleData final {
  uint32_t bytecodeIndex;
  uint32_t compiledIndex;
  uint32_t flags;

  explicit WasmModuleData(uint32_t aFlags)
      : bytecodeIndex(0), compiledIndex(0), flags(aFlags) {
    MOZ_COUNT_CTOR(WasmModuleData);
  }

  MOZ_COUNTED_DTOR(WasmModuleData)
};

bool StructuredCloneReadString(JSStructuredCloneReader* aReader,
                               nsCString& aString) {
  uint32_t length;
  if (!JS_ReadBytes(aReader, &length, sizeof(uint32_t))) {
    NS_WARNING("Failed to read length!");
    return false;
  }
  length = NativeEndian::swapFromLittleEndian(length);

  if (!aString.SetLength(length, fallible)) {
    NS_WARNING("Out of memory?");
    return false;
  }
  char* const buffer = aString.BeginWriting();

  if (!JS_ReadBytes(aReader, buffer, length)) {
    NS_WARNING("Failed to read type!");
    return false;
  }

  return true;
}

bool ReadFileHandle(JSStructuredCloneReader* aReader,
                    MutableFileData* aRetval) {
  static_assert(SCTAG_DOM_MUTABLEFILE == 0xFFFF8004, "Update me!");
  MOZ_ASSERT(aReader && aRetval);

  nsCString type;
  if (!StructuredCloneReadString(aReader, type)) {
    return false;
  }
  CopyUTF8toUTF16(type, aRetval->type);

  nsCString name;
  if (!StructuredCloneReadString(aReader, name)) {
    return false;
  }
  CopyUTF8toUTF16(name, aRetval->name);

  return true;
}

bool ReadBlobOrFile(JSStructuredCloneReader* aReader, uint32_t aTag,
                    BlobOrFileData* aRetval) {
  static_assert(SCTAG_DOM_BLOB == 0xffff8001 &&
                    SCTAG_DOM_FILE_WITHOUT_LASTMODIFIEDDATE == 0xffff8002 &&
                    SCTAG_DOM_FILE == 0xffff8005,
                "Update me!");

  MOZ_ASSERT(aReader);
  MOZ_ASSERT(aTag == SCTAG_DOM_FILE ||
             aTag == SCTAG_DOM_FILE_WITHOUT_LASTMODIFIEDDATE ||
             aTag == SCTAG_DOM_BLOB);
  MOZ_ASSERT(aRetval);

  aRetval->tag = aTag;

  uint64_t size;
  if (NS_WARN_IF(!JS_ReadBytes(aReader, &size, sizeof(uint64_t)))) {
    return false;
  }

  aRetval->size = NativeEndian::swapFromLittleEndian(size);

  nsCString type;
  if (NS_WARN_IF(!StructuredCloneReadString(aReader, type))) {
    return false;
  }

  CopyUTF8toUTF16(type, aRetval->type);

  // Blobs are done.
  if (aTag == SCTAG_DOM_BLOB) {
    return true;
  }

  MOZ_ASSERT(aTag == SCTAG_DOM_FILE ||
             aTag == SCTAG_DOM_FILE_WITHOUT_LASTMODIFIEDDATE);

  int64_t lastModifiedDate;
  if (aTag == SCTAG_DOM_FILE_WITHOUT_LASTMODIFIEDDATE) {
    lastModifiedDate = INT64_MAX;
  } else {
    if (NS_WARN_IF(!JS_ReadBytes(aReader, &lastModifiedDate,
                                 sizeof(lastModifiedDate)))) {
      return false;
    }
    lastModifiedDate = NativeEndian::swapFromLittleEndian(lastModifiedDate);
  }

  aRetval->lastModifiedDate = lastModifiedDate;

  nsCString name;
  if (NS_WARN_IF(!StructuredCloneReadString(aReader, name))) {
    return false;
  }

  CopyUTF8toUTF16(name, aRetval->name);

  return true;
}

bool ReadWasmModule(JSStructuredCloneReader* aReader, WasmModuleData* aRetval) {
  static_assert(SCTAG_DOM_WASM_MODULE == 0xFFFF8006, "Update me!");
  MOZ_ASSERT(aReader && aRetval);

  uint32_t bytecodeIndex;
  uint32_t compiledIndex;
  if (NS_WARN_IF(!JS_ReadUint32Pair(aReader, &bytecodeIndex, &compiledIndex))) {
    return false;
  }

  aRetval->bytecodeIndex = bytecodeIndex;
  aRetval->compiledIndex = compiledIndex;

  return true;
}

template <typename StructuredCloneFile>
class ValueDeserializationHelper;

class ValueDeserializationHelperBase {
 public:
  static bool CreateAndWrapWasmModule(JSContext* aCx,
                                      const StructuredCloneFileBase& aFile,
                                      const WasmModuleData& aData,
                                      JS::MutableHandle<JSObject*> aResult) {
    MOZ_ASSERT(aCx);
    MOZ_ASSERT(aFile.Type() == StructuredCloneFileBase::eWasmBytecode);

    // Both on the parent and child side, just create a plain object here,
    // support for de-serialization of WebAssembly.Modules has been removed in
    // bug 1561876. Full removal is tracked in bug 1487479.

    JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx));
    if (NS_WARN_IF(!obj)) {
      return false;
    }

    aResult.set(obj);
    return true;
  }

  template <typename StructuredCloneFile>
  static already_AddRefed<File> CreateUnwrappedFile(
      JSContext* aCx, IDBDatabase* aDatabase, const StructuredCloneFile& aFile,
      const BlobOrFileData& aData) {
    MOZ_ASSERT(aCx);
    MOZ_ASSERT(aData.tag == SCTAG_DOM_FILE ||
               aData.tag == SCTAG_DOM_FILE_WITHOUT_LASTMODIFIEDDATE);
    MOZ_ASSERT(aFile.Type() == StructuredCloneFileBase::eBlob);

    const auto blob = ValueDeserializationHelper<StructuredCloneFile>::GetBlob(
        aCx, aDatabase, aFile);
    if (NS_WARN_IF(!blob)) {
      return nullptr;
    }

    blob->Impl()->SetLazyData(aData.name, aData.type, aData.size,
                              aData.lastModifiedDate * PR_USEC_PER_MSEC);

    MOZ_ASSERT(blob->IsFile());
    RefPtr<File> file = blob->ToFile();
    MOZ_ASSERT(file);

    return file.forget();
  }

  template <typename StructuredCloneFile>
  static bool CreateAndWrapBlobOrFile(JSContext* aCx, IDBDatabase* aDatabase,
                                      const StructuredCloneFile& aFile,
                                      const BlobOrFileData& aData,
                                      JS::MutableHandle<JSObject*> aResult) {
    MOZ_ASSERT(aCx);
    MOZ_ASSERT(aData.tag == SCTAG_DOM_FILE ||
               aData.tag == SCTAG_DOM_FILE_WITHOUT_LASTMODIFIEDDATE ||
               aData.tag == SCTAG_DOM_BLOB);
    MOZ_ASSERT(aFile.Type() == StructuredCloneFileBase::eBlob);

    if (aData.tag == SCTAG_DOM_FILE ||
        aData.tag == SCTAG_DOM_FILE_WITHOUT_LASTMODIFIEDDATE) {
      RefPtr<File> file =
          ValueDeserializationHelper<StructuredCloneFile>::CreateUnwrappedFile(
              aCx, aDatabase, aFile, aData);
      return WrapAsJSObject(aCx, file, aResult);
    }

    const auto blob = ValueDeserializationHelper<StructuredCloneFile>::GetBlob(
        aCx, aDatabase, aFile);
    if (NS_WARN_IF(!blob)) {
      return false;
    }

    MOZ_ASSERT(aData.tag == SCTAG_DOM_BLOB);
    blob->Impl()->SetLazyData(VoidString(), aData.type, aData.size, INT64_MAX);
    MOZ_ASSERT(!blob->IsFile());

    // XXX The comment below is somewhat confusing, since it seems to imply
    // that this branch is only executed when called from ActorsParent, but
    // it's executed from both the parent and the child side code.

    // ActorsParent sends here a kind of half blob and half file wrapped into
    // a DOM File object. DOM File and DOM Blob are a WebIDL wrapper around a
    // BlobImpl object. SetLazyData() has just changed the BlobImpl to be a
    // Blob (see the previous assert), but 'blob' still has the WebIDL DOM
    // File wrapping.
    // Before exposing it to content, we must recreate a DOM Blob object.

    const RefPtr<Blob> exposedBlob =
        Blob::Create(blob->GetParentObject(), blob->Impl());
    if (NS_WARN_IF(!exposedBlob)) {
      return false;
    }

    return WrapAsJSObject(aCx, exposedBlob, aResult);
  }
};

template <>
class ValueDeserializationHelper<StructuredCloneFileParent>
    : public ValueDeserializationHelperBase {
 public:
  static bool CreateAndWrapMutableFile(JSContext* aCx,
                                       StructuredCloneFileParent& aFile,
                                       const MutableFileData& aData,
                                       JS::MutableHandle<JSObject*> aResult) {
    MOZ_ASSERT(aCx);
    MOZ_ASSERT(aFile.Type() == StructuredCloneFileBase::eBlob);

    // We are in an IDB SQLite schema upgrade where we don't care about a real
    // 'MutableFile', but we just care of having a proper |mType| flag.

    aFile.MutateType(StructuredCloneFileBase::eMutableFile);

    // Just make a dummy object.
    JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx));

    if (NS_WARN_IF(!obj)) {
      return false;
    }

    aResult.set(obj);
    return true;
  }

  static RefPtr<Blob> GetBlob(JSContext* aCx, IDBDatabase* aDatabase,
                              const StructuredCloneFileParent& aFile) {
    // This is chrome code, so there is no parent, but still we want to set a
    // correct parent for the new File object.
    const auto global = [aDatabase, aCx]() -> nsCOMPtr<nsIGlobalObject> {
      if (NS_IsMainThread()) {
        if (aDatabase && aDatabase->GetParentObject()) {
          return aDatabase->GetParentObject();
        }
        return xpc::CurrentNativeGlobal(aCx);
      }
      const WorkerPrivate* const workerPrivate =
          GetCurrentThreadWorkerPrivate();
      MOZ_ASSERT(workerPrivate);

      WorkerGlobalScope* const globalScope = workerPrivate->GlobalScope();
      MOZ_ASSERT(globalScope);

      return do_QueryObject(globalScope);
    }();

    MOZ_ASSERT(global);

    // We do not have an mBlob but do have a DatabaseFileInfo.
    //
    // If we are creating an index, we do need a real-looking Blob/File instance
    // because the index's key path can reference their properties.  Rather than
    // create a fake-looking object, create a real Blob.
    //
    // If we are in a schema upgrade, we don't strictly need that, but we do not
    // need to optimize for that, and create it anyway.
    const nsCOMPtr<nsIFile> file = aFile.FileInfo().GetFileForFileInfo();
    if (!file) {
      return nullptr;
    }

    const auto impl = MakeRefPtr<FileBlobImpl>(file);
    impl->SetFileId(aFile.FileInfo().Id());
    return File::Create(global, impl);
  }
};

template <>
class ValueDeserializationHelper<StructuredCloneFileChild>
    : public ValueDeserializationHelperBase {
 public:
  static bool CreateAndWrapMutableFile(JSContext* aCx,
                                       StructuredCloneFileChild& aFile,
                                       const MutableFileData& aData,
                                       JS::MutableHandle<JSObject*> aResult) {
    MOZ_ASSERT(aCx);
    MOZ_ASSERT(aFile.Type() == StructuredCloneFileBase::eMutableFile);

    return false;
  }

  static RefPtr<Blob> GetBlob(JSContext* aCx, IDBDatabase* aDatabase,
                              const StructuredCloneFileChild& aFile) {
    if (aFile.HasBlob()) {
      return aFile.BlobPtr();
    }

    MOZ_CRASH("Expected a StructuredCloneFile with a Blob");
  }
};

}  // namespace

template <typename StructuredCloneReadInfo>
JSObject* CommonStructuredCloneReadCallback(
    JSContext* aCx, JSStructuredCloneReader* aReader,
    const JS::CloneDataPolicy& aCloneDataPolicy, uint32_t aTag, uint32_t aData,
    StructuredCloneReadInfo* aCloneReadInfo, IDBDatabase* aDatabase) {
  // We need to statically assert that our tag values are what we expect
  // so that if people accidentally change them they notice.
  static_assert(SCTAG_DOM_BLOB == 0xffff8001 &&
                    SCTAG_DOM_FILE_WITHOUT_LASTMODIFIEDDATE == 0xffff8002 &&
                    SCTAG_DOM_MUTABLEFILE == 0xffff8004 &&
                    SCTAG_DOM_FILE == 0xffff8005 &&
                    SCTAG_DOM_WASM_MODULE == 0xffff8006 &&
                    SCTAG_DOM_URLSEARCHPARAMS == 0xffff8014 &&
                    SCTAG_DOM_FILELIST == 0xffff8003,
                "You changed our structured clone tag values and just ate "
                "everyone's IndexedDB data.  I hope you are happy.");

  if (aTag == SCTAG_DOM_URLSEARCHPARAMS) {
    // Protect the result from a moving GC in ~RefPtr.
    JS::Rooted<JSObject*> result(aCx);

    {
      // Scope for the RefPtr below.

      nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
      if (!global) {
        return nullptr;
      }

      RefPtr<URLSearchParams> params = new URLSearchParams(global);

      uint32_t paramCount;
      uint32_t zero;
      if (!JS_ReadUint32Pair(aReader, &paramCount, &zero)) {
        return nullptr;
      }

      nsAutoString key;
      nsAutoString value;
      for (uint32_t index = 0; index < paramCount; index++) {
        if (!StructuredCloneHolder::ReadString(aReader, key) ||
            !StructuredCloneHolder::ReadString(aReader, value)) {
          return nullptr;
        }
        params->Append(NS_ConvertUTF16toUTF8(key),
                       NS_ConvertUTF16toUTF8(value));
      }

      if (!WrapAsJSObject(aCx, params, &result)) {
        return nullptr;
      }
    }

    return result;
  }

  using StructuredCloneFile =
      typename StructuredCloneReadInfo::StructuredCloneFile;

  if (aTag == SCTAG_DOM_FILELIST) {
    const auto& files = aCloneReadInfo->Files();

    uint32_t fileListLength = aData;

    if (fileListLength > files.Length()) {
      MOZ_ASSERT(false, "Bad file list length value!");

      return nullptr;
    }

    // We need to ensure that all RAII smart pointers which may trigger GC are
    // destroyed on return prior to this JS::Rooted being destroyed and
    // unrooting the pointer. This scope helps make this intent more explicit.
    JS::Rooted<JSObject*> obj(aCx);
    {
      nsCOMPtr<nsIGlobalObject> global = xpc::CurrentNativeGlobal(aCx);
      if (!global) {
        MOZ_ASSERT(false, "Could not access global!");

        return nullptr;
      }

      RefPtr<FileList> fileList = new FileList(global);

      for (uint32_t i = 0u; i < fileListLength; ++i) {
        uint32_t tag = UINT32_MAX;
        uint32_t index = UINT32_MAX;
        if (!JS_ReadUint32Pair(aReader, &tag, &index)) {
          return nullptr;
        }

        if (tag != SCTAG_DOM_FILE) {
          MOZ_ASSERT(false, "Unexpected tag!");

          return nullptr;
        }

        if (uint64_t(index) >= files.Length()) {
          MOZ_ASSERT(false, "Bad index!");

          return nullptr;
        }

        BlobOrFileData data;
        if (NS_WARN_IF(!ReadBlobOrFile(aReader, tag, &data))) {
          return nullptr;
        }

        auto& fileObj = aCloneReadInfo->MutableFile(index);

        RefPtr<File> file = ValueDeserializationHelper<
            StructuredCloneFile>::CreateUnwrappedFile(aCx, aDatabase, fileObj,
                                                      data);
        if (!file) {
          MOZ_ASSERT(false, "Could not deserialize file!");

          return nullptr;
        }

        if (!fileList->Append(file)) {
          MOZ_ASSERT(false, "Could not extend filelist!");

          return nullptr;
        }
      }

      if (!WrapAsJSObject(aCx, fileList, &obj)) {
        return nullptr;
      }
    }

    return obj;
  }

  if (aTag == SCTAG_DOM_FILE_WITHOUT_LASTMODIFIEDDATE ||
      aTag == SCTAG_DOM_BLOB || aTag == SCTAG_DOM_FILE ||
      aTag == SCTAG_DOM_MUTABLEFILE || aTag == SCTAG_DOM_WASM_MODULE) {
    JS::Rooted<JSObject*> result(aCx);

    if (aTag == SCTAG_DOM_WASM_MODULE) {
      WasmModuleData data(aData);
      if (NS_WARN_IF(!ReadWasmModule(aReader, &data))) {
        return nullptr;
      }

      MOZ_ASSERT(data.compiledIndex == data.bytecodeIndex + 1);
      MOZ_ASSERT(!data.flags);

      const auto& files = aCloneReadInfo->Files();
      if (data.bytecodeIndex >= files.Length() ||
          data.compiledIndex >= files.Length()) {
        MOZ_ASSERT(false, "Bad index value!");
        return nullptr;
      }

      const auto& file = files[data.bytecodeIndex];

      if (NS_WARN_IF(!ValueDeserializationHelper<StructuredCloneFile>::
                         CreateAndWrapWasmModule(aCx, file, data, &result))) {
        return nullptr;
      }

      return result;
    }

    if (aData >= aCloneReadInfo->Files().Length()) {
      MOZ_ASSERT(false, "Bad index value!");
      return nullptr;
    }

    auto& file = aCloneReadInfo->MutableFile(aData);

    if (aTag == SCTAG_DOM_MUTABLEFILE) {
      MutableFileData data;
      if (NS_WARN_IF(!ReadFileHandle(aReader, &data))) {
        return nullptr;
      }

      if (NS_WARN_IF(!ValueDeserializationHelper<StructuredCloneFile>::
                         CreateAndWrapMutableFile(aCx, file, data, &result))) {
        return nullptr;
      }

      return result;
    }

    BlobOrFileData data;
    if (NS_WARN_IF(!ReadBlobOrFile(aReader, aTag, &data))) {
      return nullptr;
    }

    if (NS_WARN_IF(!ValueDeserializationHelper<
                   StructuredCloneFile>::CreateAndWrapBlobOrFile(aCx, aDatabase,
                                                                 file, data,
                                                                 &result))) {
      return nullptr;
    }

    return result;
  }

  return StructuredCloneHolder::ReadFullySerializableObjects(aCx, aReader, aTag,
                                                             true);
}

template JSObject* CommonStructuredCloneReadCallback(
    JSContext* aCx, JSStructuredCloneReader* aReader,
    const JS::CloneDataPolicy& aCloneDataPolicy, uint32_t aTag, uint32_t aData,
    StructuredCloneReadInfoChild* aCloneReadInfo, IDBDatabase* aDatabase);

template JSObject* CommonStructuredCloneReadCallback(
    JSContext* aCx, JSStructuredCloneReader* aReader,
    const JS::CloneDataPolicy& aCloneDataPolicy, uint32_t aTag, uint32_t aData,
    StructuredCloneReadInfoParent* aCloneReadInfo, IDBDatabase* aDatabase);
}  // namespace mozilla::dom::indexedDB
