/* -*- 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 "GetFilesHelper.h"
#include "mozilla/dom/ContentChild.h"
#include "mozilla/dom/ContentParent.h"
#include "mozilla/dom/Directory.h"
#include "mozilla/dom/FileBlobImpl.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/UnionTypes.h"
#include "mozilla/dom/IPCBlobUtils.h"
#include "mozilla/ipc/IPCStreamUtils.h"
#include "FileSystemUtils.h"
#include "nsNetCID.h"
#include "nsProxyRelease.h"

namespace mozilla::dom {

namespace {

// This class is used in the DTOR of GetFilesHelper to release resources in the
// correct thread.
class ReleaseRunnable final : public Runnable {
 public:
  static void MaybeReleaseOnMainThread(
      nsTArray<RefPtr<Promise>>&& aPromises,
      nsTArray<RefPtr<GetFilesCallback>>&& aCallbacks) {
    if (NS_IsMainThread()) {
      return;
    }

    RefPtr<ReleaseRunnable> runnable =
        new ReleaseRunnable(std::move(aPromises), std::move(aCallbacks));
    FileSystemUtils::DispatchRunnable(nullptr, runnable.forget());
  }

  NS_IMETHOD
  Run() override {
    MOZ_ASSERT(NS_IsMainThread());

    mPromises.Clear();
    mCallbacks.Clear();

    return NS_OK;
  }

 private:
  ReleaseRunnable(nsTArray<RefPtr<Promise>>&& aPromises,
                  nsTArray<RefPtr<GetFilesCallback>>&& aCallbacks)
      : Runnable("dom::ReleaseRunnable"),
        mPromises(std::move(aPromises)),
        mCallbacks(std::move(aCallbacks)) {}

  nsTArray<RefPtr<Promise>> mPromises;
  nsTArray<RefPtr<GetFilesCallback>> mCallbacks;
};

}  // namespace

///////////////////////////////////////////////////////////////////////////////
// GetFilesHelper Base class

already_AddRefed<GetFilesHelper> GetFilesHelper::Create(
    const nsTArray<OwningFileOrDirectory>& aFilesOrDirectory,
    bool aRecursiveFlag, ErrorResult& aRv) {
  RefPtr<GetFilesHelper> helper;

  if (XRE_IsParentProcess()) {
    helper = new GetFilesHelper(aRecursiveFlag);
  } else {
    helper = new GetFilesHelperChild(aRecursiveFlag);
  }

  nsAutoString directoryPath;

  for (uint32_t i = 0; i < aFilesOrDirectory.Length(); ++i) {
    const OwningFileOrDirectory& data = aFilesOrDirectory[i];
    if (data.IsFile()) {
      if (!helper->mTargetBlobImplArray.AppendElement(data.GetAsFile()->Impl(),
                                                      fallible)) {
        aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
        return nullptr;
      }
    } else {
      MOZ_ASSERT(data.IsDirectory());

      // We support the upload of only 1 top-level directory from our
      // directory picker. This means that we cannot have more than 1
      // Directory object in aFilesOrDirectory array.
      MOZ_ASSERT(directoryPath.IsEmpty());

      RefPtr<Directory> directory = data.GetAsDirectory();
      MOZ_ASSERT(directory);

      aRv = directory->GetFullRealPath(directoryPath);
      if (NS_WARN_IF(aRv.Failed())) {
        return nullptr;
      }
    }
  }

  // No directories to explore.
  if (directoryPath.IsEmpty()) {
    helper->mListingCompleted = true;
    return helper.forget();
  }

  MOZ_ASSERT(helper->mTargetBlobImplArray.IsEmpty());
  helper->SetDirectoryPath(directoryPath);

  helper->Work(aRv);
  if (NS_WARN_IF(aRv.Failed())) {
    return nullptr;
  }

  return helper.forget();
}

GetFilesHelper::GetFilesHelper(bool aRecursiveFlag)
    : Runnable("GetFilesHelper"),
      GetFilesHelperBase(aRecursiveFlag),
      mListingCompleted(false),
      mErrorResult(NS_OK),
      mMutex("GetFilesHelper::mMutex"),
      mCanceled(false) {}

GetFilesHelper::~GetFilesHelper() {
  ReleaseRunnable::MaybeReleaseOnMainThread(std::move(mPromises),
                                            std::move(mCallbacks));
}

void GetFilesHelper::AddPromise(Promise* aPromise) {
  MOZ_ASSERT(aPromise);

  // Still working.
  if (!mListingCompleted) {
    mPromises.AppendElement(aPromise);
    return;
  }

  MOZ_ASSERT(mPromises.IsEmpty());
  ResolveOrRejectPromise(aPromise);
}

void GetFilesHelper::AddCallback(GetFilesCallback* aCallback) {
  MOZ_ASSERT(aCallback);

  // Still working.
  if (!mListingCompleted) {
    mCallbacks.AppendElement(aCallback);
    return;
  }

  MOZ_ASSERT(mCallbacks.IsEmpty());
  RunCallback(aCallback);
}

void GetFilesHelper::Unlink() {
  mPromises.Clear();
  mCallbacks.Clear();

  {
    MutexAutoLock lock(mMutex);
    mCanceled = true;
  }

  Cancel();
}

void GetFilesHelper::Traverse(nsCycleCollectionTraversalCallback& cb) {
  GetFilesHelper* tmp = this;
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromises);
}

void GetFilesHelper::Work(ErrorResult& aRv) {
  nsCOMPtr<nsIEventTarget> target =
      do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
  MOZ_ASSERT(target);

  aRv = target->Dispatch(this, NS_DISPATCH_NORMAL);
}

NS_IMETHODIMP
GetFilesHelper::Run() {
  MOZ_ASSERT(!mDirectoryPath.IsEmpty());
  MOZ_ASSERT(!mListingCompleted);

  // First step is to retrieve the list of file paths.
  // This happens in the I/O thread.
  if (!NS_IsMainThread()) {
    RunIO();

    // If this operation has been canceled, we don't have to go back to
    // main-thread.
    if (IsCanceled()) {
      return NS_OK;
    }

    RefPtr<Runnable> runnable = this;
    return FileSystemUtils::DispatchRunnable(nullptr, runnable.forget());
  }

  // We are here, but we should not do anything on this thread because, in the
  // meantime, the operation has been canceled.
  if (IsCanceled()) {
    return NS_OK;
  }

  OperationCompleted();
  return NS_OK;
}

void GetFilesHelper::OperationCompleted() {
  // We mark the operation as completed here.
  mListingCompleted = true;

  // Let's process the pending promises.
  nsTArray<RefPtr<Promise>> promises = std::move(mPromises);

  for (uint32_t i = 0; i < promises.Length(); ++i) {
    ResolveOrRejectPromise(promises[i]);
  }

  // Let's process the pending callbacks.
  nsTArray<RefPtr<GetFilesCallback>> callbacks = std::move(mCallbacks);

  for (uint32_t i = 0; i < callbacks.Length(); ++i) {
    RunCallback(callbacks[i]);
  }
}

void GetFilesHelper::RunIO() {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(!mDirectoryPath.IsEmpty());
  MOZ_ASSERT(!mListingCompleted);

  nsCOMPtr<nsIFile> file;
  mErrorResult = NS_NewLocalFile(mDirectoryPath, true, getter_AddRefs(file));
  if (NS_WARN_IF(NS_FAILED(mErrorResult))) {
    return;
  }

  nsAutoString leafName;
  mErrorResult = file->GetLeafName(leafName);
  if (NS_WARN_IF(NS_FAILED(mErrorResult))) {
    return;
  }

  nsAutoString domPath;
  domPath.AssignLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL);
  domPath.Append(leafName);

  mErrorResult = ExploreDirectory(domPath, file);
}

nsresult GetFilesHelperBase::ExploreDirectory(const nsAString& aDOMPath,
                                              nsIFile* aFile) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(aFile);

  // We check if this operation has to be terminated at each recursion.
  if (IsCanceled()) {
    return NS_OK;
  }

  nsCOMPtr<nsIDirectoryEnumerator> entries;
  nsresult rv = aFile->GetDirectoryEntries(getter_AddRefs(entries));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  for (;;) {
    nsCOMPtr<nsIFile> currFile;
    if (NS_WARN_IF(NS_FAILED(entries->GetNextFile(getter_AddRefs(currFile)))) ||
        !currFile) {
      break;
    }
    bool isLink, isSpecial, isFile, isDir;
    if (NS_WARN_IF(NS_FAILED(currFile->IsSymlink(&isLink)) ||
                   NS_FAILED(currFile->IsSpecial(&isSpecial))) ||
        isSpecial ||
        // Although we allow explicit individual selection of symlinks via the
        // file picker, we do not process symlinks in directory traversal.  Our
        // specific policy decision is documented at
        // https://bugzilla.mozilla.org/show_bug.cgi?id=1813299#c20
        isLink) {
      continue;
    }

    if (NS_WARN_IF(NS_FAILED(currFile->IsFile(&isFile)) ||
                   NS_FAILED(currFile->IsDirectory(&isDir))) ||
        !(isFile || isDir)) {
      continue;
    }

    // The new domPath
    nsAutoString domPath;
    domPath.Assign(aDOMPath);
    if (!aDOMPath.EqualsLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL)) {
      domPath.AppendLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL);
    }

    nsAutoString leafName;
    if (NS_WARN_IF(NS_FAILED(currFile->GetLeafName(leafName)))) {
      continue;
    }
    domPath.Append(leafName);

    if (isFile) {
      RefPtr<BlobImpl> blobImpl = new FileBlobImpl(currFile);
      blobImpl->SetDOMPath(domPath);

      if (!mTargetBlobImplArray.AppendElement(blobImpl, fallible)) {
        return NS_ERROR_OUT_OF_MEMORY;
      }

      continue;
    }

    MOZ_ASSERT(isDir);
    if (!mRecursiveFlag) {
      continue;
    }

    // Recursive.
    rv = ExploreDirectory(domPath, currFile);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
  }

  return NS_OK;
}

void GetFilesHelper::ResolveOrRejectPromise(Promise* aPromise) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(mListingCompleted);
  MOZ_ASSERT(aPromise);

  Sequence<RefPtr<File>> files;

  if (NS_SUCCEEDED(mErrorResult)) {
    for (uint32_t i = 0; i < mTargetBlobImplArray.Length(); ++i) {
      RefPtr<File> domFile =
          File::Create(aPromise->GetParentObject(), mTargetBlobImplArray[i]);
      if (NS_WARN_IF(!domFile)) {
        mErrorResult = NS_ERROR_FAILURE;
        files.Clear();
        break;
      }

      if (!files.AppendElement(domFile, fallible)) {
        mErrorResult = NS_ERROR_OUT_OF_MEMORY;
        files.Clear();
        break;
      }
    }
  }

  // Error propagation.
  if (NS_FAILED(mErrorResult)) {
    aPromise->MaybeReject(mErrorResult);
    return;
  }

  aPromise->MaybeResolve(files);
}

void GetFilesHelper::RunCallback(GetFilesCallback* aCallback) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(mListingCompleted);
  MOZ_ASSERT(aCallback);

  aCallback->Callback(mErrorResult, mTargetBlobImplArray);
}

///////////////////////////////////////////////////////////////////////////////
// GetFilesHelperChild class

void GetFilesHelperChild::Work(ErrorResult& aRv) {
  ContentChild* cc = ContentChild::GetSingleton();
  if (NS_WARN_IF(!cc)) {
    aRv.Throw(NS_ERROR_FAILURE);
    return;
  }

  aRv = nsID::GenerateUUIDInPlace(mUUID);
  if (NS_WARN_IF(aRv.Failed())) {
    return;
  }

  mPendingOperation = true;
  cc->CreateGetFilesRequest(mDirectoryPath, mRecursiveFlag, mUUID, this);
}

void GetFilesHelperChild::Cancel() {
  if (!mPendingOperation) {
    return;
  }

  ContentChild* cc = ContentChild::GetSingleton();
  if (NS_WARN_IF(!cc)) {
    return;
  }

  mPendingOperation = false;
  cc->DeleteGetFilesRequest(mUUID, this);
}

bool GetFilesHelperChild::AppendBlobImpl(BlobImpl* aBlobImpl) {
  MOZ_ASSERT(mPendingOperation);
  MOZ_ASSERT(aBlobImpl);
  MOZ_ASSERT(aBlobImpl->IsFile());

  return mTargetBlobImplArray.AppendElement(aBlobImpl, fallible);
}

void GetFilesHelperChild::Finished(nsresult aError) {
  MOZ_ASSERT(mPendingOperation);
  MOZ_ASSERT(NS_SUCCEEDED(mErrorResult));

  mPendingOperation = false;
  mErrorResult = aError;

  OperationCompleted();
}

///////////////////////////////////////////////////////////////////////////////
// GetFilesHelperParent class

class GetFilesHelperParentCallback final : public GetFilesCallback {
 public:
  explicit GetFilesHelperParentCallback(GetFilesHelperParent* aParent)
      : mParent(aParent) {
    MOZ_ASSERT(aParent);
  }

  void Callback(nsresult aStatus,
                const FallibleTArray<RefPtr<BlobImpl>>& aBlobImpls) override {
    if (NS_FAILED(aStatus)) {
      mParent->mContentParent->SendGetFilesResponseAndForget(
          mParent->mUUID, GetFilesResponseFailure(aStatus));
      return;
    }

    GetFilesResponseSuccess success;

    nsTArray<IPCBlob>& ipcBlobs = success.blobs();
    ipcBlobs.SetLength(aBlobImpls.Length());

    for (uint32_t i = 0; i < aBlobImpls.Length(); ++i) {
      nsresult rv = IPCBlobUtils::Serialize(aBlobImpls[i], ipcBlobs[i]);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        mParent->mContentParent->SendGetFilesResponseAndForget(
            mParent->mUUID, GetFilesResponseFailure(NS_ERROR_OUT_OF_MEMORY));
        return;
      }
    }

    mParent->mContentParent->SendGetFilesResponseAndForget(mParent->mUUID,
                                                           success);
  }

 private:
  // Raw pointer because this callback is kept alive by this parent object.
  GetFilesHelperParent* mParent;
};

GetFilesHelperParent::GetFilesHelperParent(const nsID& aUUID,
                                           ContentParent* aContentParent,
                                           bool aRecursiveFlag)
    : GetFilesHelper(aRecursiveFlag),
      mContentParent(aContentParent),
      mUUID(aUUID) {}

GetFilesHelperParent::~GetFilesHelperParent() {
  NS_ReleaseOnMainThread("GetFilesHelperParent::mContentParent",
                         mContentParent.forget());
}

/* static */
already_AddRefed<GetFilesHelperParent> GetFilesHelperParent::Create(
    const nsID& aUUID, const nsAString& aDirectoryPath, bool aRecursiveFlag,
    ContentParent* aContentParent, ErrorResult& aRv) {
  MOZ_ASSERT(aContentParent);

  RefPtr<GetFilesHelperParent> helper =
      new GetFilesHelperParent(aUUID, aContentParent, aRecursiveFlag);
  helper->SetDirectoryPath(aDirectoryPath);

  helper->Work(aRv);
  if (NS_WARN_IF(aRv.Failed())) {
    return nullptr;
  }

  RefPtr<GetFilesHelperParentCallback> callback =
      new GetFilesHelperParentCallback(helper);
  helper->AddCallback(callback);

  return helper.forget();
}

}  // namespace mozilla::dom
