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

#include "FileSystemDirectoryHandle.h"
#include "FileSystemFileHandle.h"
#include "fs/FileSystemRequestHandler.h"
#include "js/StructuredClone.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/dom/FileSystemHandleBinding.h"
#include "mozilla/dom/FileSystemLog.h"
#include "mozilla/dom/FileSystemManager.h"
#include "mozilla/dom/Promise-inl.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/StorageManager.h"
#include "mozilla/dom/StructuredCloneHolder.h"
#include "mozilla/dom/quota/QuotaCommon.h"
#include "mozilla/ipc/PBackgroundSharedTypes.h"
#include "nsJSPrincipals.h"
#include "nsString.h"
#include "prio.h"
#include "private/pprio.h"
#include "xpcpublic.h"

namespace mozilla::dom {

namespace {

bool ConstructHandleMetadata(JSContext* aCx, nsIGlobalObject* aGlobal,
                             JSStructuredCloneReader* aReader,
                             const bool aDirectory,
                             fs::FileSystemEntryMetadata& aMetadata) {
  using namespace mozilla::dom::fs;

  EntryId entryId;
  if (!entryId.SetLength(32u, fallible)) {
    return false;
  }

  if (!JS_ReadBytes(aReader, static_cast<void*>(entryId.BeginWriting()), 32u)) {
    return false;
  }

  Name name;
  if (!StructuredCloneHolder::ReadString(aReader, name)) {
    return false;
  }

  mozilla::ipc::PrincipalInfo storageKey;
  if (!nsJSPrincipals::ReadPrincipalInfo(aReader, storageKey)) {
    return false;
  }

  QM_TRY_UNWRAP(auto hasEqualStorageKey,
                aGlobal->HasEqualStorageKey(storageKey), false);

  if (!hasEqualStorageKey) {
    LOG(("Blocking deserialization of %s due to cross-origin",
         NS_ConvertUTF16toUTF8(name).get()));
    return false;
  }

  LOG_VERBOSE(("Deserializing %s", NS_ConvertUTF16toUTF8(name).get()));

  aMetadata = fs::FileSystemEntryMetadata(entryId, name, aDirectory);
  return true;
}

}  // namespace

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileSystemHandle)
  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END

NS_IMPL_CYCLE_COLLECTING_ADDREF(FileSystemHandle)
NS_IMPL_CYCLE_COLLECTING_RELEASE(FileSystemHandle)

NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FileSystemHandle)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(FileSystemHandle)
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal)
  // Don't unlink mManager!
  NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(FileSystemHandle)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mManager)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END

FileSystemHandle::FileSystemHandle(
    nsIGlobalObject* aGlobal, RefPtr<FileSystemManager>& aManager,
    const fs::FileSystemEntryMetadata& aMetadata,
    fs::FileSystemRequestHandler* aRequestHandler)
    : mGlobal(aGlobal),
      mManager(aManager),
      mMetadata(aMetadata),
      mRequestHandler(aRequestHandler) {
  MOZ_ASSERT(!mMetadata.entryId().IsEmpty());
}

// WebIDL Boilerplate

nsIGlobalObject* FileSystemHandle::GetParentObject() const { return mGlobal; }

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

// WebIDL Interface

void FileSystemHandle::GetName(nsAString& aResult) {
  aResult = mMetadata.entryName();
}

already_AddRefed<Promise> FileSystemHandle::IsSameEntry(
    FileSystemHandle& aOther, ErrorResult& aError) const {
  RefPtr<Promise> promise = Promise::Create(GetParentObject(), aError);
  if (aError.Failed()) {
    return nullptr;
  }

  // Handles the case of "dir = createdir foo; removeEntry(foo); file =
  // createfile foo; issameentry(dir, file)"
  const bool result = mMetadata.entryId().Equals(aOther.mMetadata.entryId()) &&
                      Kind() == aOther.Kind();
  promise->MaybeResolve(result);

  return promise.forget();
}

already_AddRefed<Promise> FileSystemHandle::Move(const nsAString& aName,
                                                 ErrorResult& aError) {
  LOG(("Move %s to %s", NS_ConvertUTF16toUTF8(mMetadata.entryName()).get(),
       NS_ConvertUTF16toUTF8(aName).get()));

  fs::EntryId parent;  // empty means same directory
  return Move(parent, aName, aError);
}

already_AddRefed<Promise> FileSystemHandle::Move(
    FileSystemDirectoryHandle& aParent, ErrorResult& aError) {
  LOG(("Move %s to %s/%s", NS_ConvertUTF16toUTF8(mMetadata.entryName()).get(),
       NS_ConvertUTF16toUTF8(aParent.mMetadata.entryName()).get(),
       NS_ConvertUTF16toUTF8(mMetadata.entryName()).get()));
  return Move(aParent, mMetadata.entryName(), aError);
}

already_AddRefed<Promise> FileSystemHandle::Move(
    FileSystemDirectoryHandle& aParent, const nsAString& aName,
    ErrorResult& aError) {
  LOG(("Move %s to %s/%s", NS_ConvertUTF16toUTF8(mMetadata.entryName()).get(),
       NS_ConvertUTF16toUTF8(aParent.mMetadata.entryName()).get(),
       NS_ConvertUTF16toUTF8(aName).get()));
  return Move(aParent.mMetadata.entryId(), aName, aError);
}

already_AddRefed<Promise> FileSystemHandle::Move(const fs::EntryId& aParentId,
                                                 const nsAString& aName,
                                                 ErrorResult& aError) {
  RefPtr<Promise> promise = Promise::Create(GetParentObject(), aError);
  if (aError.Failed()) {
    return nullptr;
  }

  fs::FileSystemChildMetadata newMetadata;
  newMetadata.parentId() = aParentId;
  newMetadata.childName() = aName;
  if (!aParentId.IsEmpty()) {
    mRequestHandler->MoveEntry(mManager, this, &mMetadata, newMetadata, promise,
                               aError);
  } else {
    mRequestHandler->RenameEntry(mManager, this, &mMetadata,
                                 newMetadata.childName(), promise, aError);
  }
  if (aError.Failed()) {
    return nullptr;
  }

  // Other handles to this will be broken, and the spec is ok with this, but we
  // need to update our EntryId and name
  promise->AddCallbacksWithCycleCollectedArgs(
      [newMetadata](JSContext* aCx, JS::Handle<JS::Value> aValue,
                    ErrorResult& aRv, FileSystemHandle* aHandle) {
        // XXX Fix entryId!
        LOG(("Changing FileSystemHandle name from %s to %s",
             NS_ConvertUTF16toUTF8(aHandle->mMetadata.entryName()).get(),
             NS_ConvertUTF16toUTF8(newMetadata.childName()).get()));
        aHandle->mMetadata.entryName() = newMetadata.childName();
      },
      [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv,
         FileSystemHandle* aHandle) {
        LOG(("reject of move for %s",
             NS_ConvertUTF16toUTF8(aHandle->mMetadata.entryName()).get()));
      },
      RefPtr(this));

  return promise.forget();
}

// [Serializable] implementation

// static
already_AddRefed<FileSystemHandle> FileSystemHandle::ReadStructuredClone(
    JSContext* aCx, nsIGlobalObject* aGlobal,
    JSStructuredCloneReader* aReader) {
  LOG_VERBOSE(("Reading File/DirectoryHandle"));

  uint32_t kind = UINT32_MAX;

  if (!JS_ReadBytes(aReader, reinterpret_cast<void*>(&kind),
                    sizeof(uint32_t))) {
    return nullptr;
  }

  if (kind == static_cast<uint32_t>(FileSystemHandleKind::Directory)) {
    RefPtr<FileSystemHandle> result =
        FileSystemHandle::ConstructDirectoryHandle(aCx, aGlobal, aReader);
    return result.forget();
  }

  if (kind == static_cast<uint32_t>(FileSystemHandleKind::File)) {
    RefPtr<FileSystemHandle> result =
        FileSystemHandle::ConstructFileHandle(aCx, aGlobal, aReader);
    return result.forget();
  }

  return nullptr;
}

bool FileSystemHandle::WriteStructuredClone(
    JSContext* aCx, JSStructuredCloneWriter* aWriter) const {
  LOG_VERBOSE(("Writing File/DirectoryHandle"));
  MOZ_ASSERT(mMetadata.entryId().Length() == 32);

  auto kind = static_cast<uint32_t>(Kind());
  if (NS_WARN_IF(!JS_WriteBytes(aWriter, static_cast<void*>(&kind),
                                sizeof(uint32_t)))) {
    return false;
  }

  if (NS_WARN_IF(!JS_WriteBytes(
          aWriter, static_cast<const void*>(mMetadata.entryId().get()),
          mMetadata.entryId().Length()))) {
    return false;
  }

  if (!StructuredCloneHolder::WriteString(aWriter, mMetadata.entryName())) {
    return false;
  }

  // Needed to make sure the destination nsIGlobalObject is from the same
  // origin/principal
  QM_TRY_INSPECT(const auto& storageKey, mGlobal->GetStorageKey(), false);

  return nsJSPrincipals::WritePrincipalInfo(aWriter, storageKey);
}

// static
already_AddRefed<FileSystemFileHandle> FileSystemHandle::ConstructFileHandle(
    JSContext* aCx, nsIGlobalObject* aGlobal,
    JSStructuredCloneReader* aReader) {
  LOG(("Reading FileHandle"));

  fs::FileSystemEntryMetadata metadata;
  if (!ConstructHandleMetadata(aCx, aGlobal, aReader, /* aDirectory */ false,
                               metadata)) {
    return nullptr;
  }

  RefPtr<StorageManager> storageManager = aGlobal->GetStorageManager();
  if (!storageManager) {
    return nullptr;
  }

  // Note that the actor may not exist or may not be connected yet.
  RefPtr<FileSystemManager> fileSystemManager =
      storageManager->GetFileSystemManager();

  RefPtr<FileSystemFileHandle> fsHandle =
      new FileSystemFileHandle(aGlobal, fileSystemManager, metadata);

  return fsHandle.forget();
}

// static
already_AddRefed<FileSystemDirectoryHandle>
FileSystemHandle::ConstructDirectoryHandle(JSContext* aCx,
                                           nsIGlobalObject* aGlobal,
                                           JSStructuredCloneReader* aReader) {
  LOG(("Reading DirectoryHandle"));

  fs::FileSystemEntryMetadata metadata;
  if (!ConstructHandleMetadata(aCx, aGlobal, aReader, /* aDirectory */ true,
                               metadata)) {
    return nullptr;
  }

  RefPtr<StorageManager> storageManager = aGlobal->GetStorageManager();
  if (!storageManager) {
    return nullptr;
  }

  // Note that the actor may not exist or may not be connected yet.
  RefPtr<FileSystemManager> fileSystemManager =
      storageManager->GetFileSystemManager();

  RefPtr<FileSystemDirectoryHandle> fsHandle =
      new FileSystemDirectoryHandle(aGlobal, fileSystemManager, metadata);

  return fsHandle.forget();
}

}  // namespace mozilla::dom
