text: // Copyright 2020 The Chromium Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  #include "content/browser/native_io/native_io_host.h"
  
  #include <algorithm>
  #include <string>
  #include <utility>
  #include <vector>
  
  #include "base/bind.h"
  #include "base/files/file_enumerator.h"
  #include "base/files/file_path.h"
  #include "base/files/file_util.h"
  #include "base/memory/scoped_refptr.h"
  #include "base/sequenced_task_runner.h"
  #include "base/task/post_task.h"
  #include "base/task/task_traits.h"
  #include "base/task/thread_pool.h"
  #include "base/task_runner.h"
  #include "content/browser/native_io/native_io_context.h"
  #include "content/browser/native_io/native_io_file_host.h"
  #include "mojo/public/cpp/bindings/message.h"
  #include "mojo/public/cpp/bindings/pending_receiver.h"
  #include "third_party/blink/public/mojom/native_io/native_io.mojom.h"
  
  namespace content {
  
  namespace {
  
  bool IsValidNativeIONameCharacter(char name_char) {
    return base::IsAsciiLower(name_char) || base::IsAsciiDigit(name_char) ||
           name_char == '_';
  }
  
  bool IsValidNativeIOName(const std::string& name) {
    if (name.empty())
      return false;
  
    return std::all_of(name.begin(), name.end(), &IsValidNativeIONameCharacter);
  }
  
  base::FilePath GetNativeIOFilePath(const base::FilePath& root_path,
                                     const std::string& name) {
    DCHECK(IsValidNativeIOName(name));
  
    // This simple implementation assumes that the name doesn't have any special
    // meaning to the host operating system.
    base::FilePath file_path = root_path.AppendASCII(name);
    DCHECK(root_path.IsParent(file_path));
    return file_path;
  }
  
  // Creates a task runner suitable for running file I/O tasks.
  scoped_refptr<base::TaskRunner> CreateFileTaskRunner() {
    // We use a SequencedTaskRunner so that there is a global ordering to an
    // origin's directory operations.
    return base::CreateSequencedTaskRunner({
        // Needed for file I/O.
        base::MayBlock(),
        base::ThreadPool(),
  
        // Reasonable compromise, given that a few database operations are
        // blocking, while most operations are not. We should be able to do better
        // when we get scheduling APIs on the Web Platform.
        base::TaskPriority::USER_VISIBLE,
  
        // BLOCK_SHUTDOWN is definitely not appropriate. We might be able to move
        // to CONTINUE_ON_SHUTDOWN after very careful analysis.
        base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN,
    });
  }
  
  // Performs the file I/O work in OpenFile().
  base::File DoOpenFile(const base::FilePath& root_path,
                        const std::string& name) {
    DCHECK(IsValidNativeIOName(name));
  
    // Lazily create the origin's directory.
    base::File::Error error;
    if (!base::CreateDirectoryAndGetError(root_path, &error))
      return base::File();
  
    return base::File(GetNativeIOFilePath(root_path, name),
                      base::File::FLAG_OPEN_ALWAYS | base::File::FLAG_READ |
                          base::File::FLAG_WRITE | base::File::FLAG_SHARE_DELETE);
  }
  
  // Performs the file I/O work in DeleteFile().
  bool DoDeleteFile(const base::FilePath& root_path, const std::string& name) {
    DCHECK(IsValidNativeIOName(name));
  
    // If the origin's directory wasn't created yet, there's nothing to delete.
    if (!base::PathExists(root_path))
      return true;
  
    return base::DeleteFile(GetNativeIOFilePath(root_path, name),
                            /*recursive=*/false);
  }
  
  using GetAllFileNamesResult =
      std::pair<base::File::Error, std::vector<std::string>>;
  
  // Performs the file I/O work in GetAllFileNames().
  GetAllFileNamesResult DoGetAllFileNames(const base::FilePath& root_path) {
    std::vector<std::string> result;
  
    // If the origin's directory wasn't created yet, there's no file to report.
    if (!base::PathExists(root_path))
      return {base::File::FILE_OK, std::move(result)};
  
    base::FileEnumerator file_enumerator(
        root_path, /*recursive=*/false, base::FileEnumerator::FILES,
        /*pattern=*/base::FilePath::StringType(),
        base::FileEnumerator::FolderSearchPolicy::ALL,
        base::FileEnumerator::ErrorPolicy::STOP_ENUMERATION);
  
    // TODO(pwnall): The result vector can grow to an unbounded size. Add a limit
    //               parameter with a reasonable upper bound.
  
    for (base::FilePath file_path = file_enumerator.Next(); !file_path.empty();
         file_path = file_enumerator.Next()) {
      // If the file name has a non-ASCII character, |file_name| will be the empty
      // string. This will correctly be flagged as corruption by the check below.
      std::string file_name = file_path.BaseName().MaybeAsASCII();
  
      // Chrome's NativeIO implementation only creates files that have valid
      // NativeIO names. Any other file names imply directory corruption.
      if (!IsValidNativeIOName(file_name)) {
        // TODO(pwnall): Figure out the corruption handling strategy. We could
        //               silently ignore the corrupted file, delete it, or stop
        //               and report an error.
        continue;
      }
      result.push_back(std::move(file_name));
    }
  
    // Don't return a partial list of files if an error occurred. The partial list
    // isn't meaningful, and may be useful information for a compromised renderer.
    //
    // TODO(pwnall): Reconsider this if we end up making NativeIO unusually
    //               friendly to corruption recovery.
    base::File::Error enumeration_error = file_enumerator.GetError();
    if (enumeration_error != base::File::FILE_OK)
      result.clear();
  
    return {enumeration_error, std::move(result)};
  }
  
  // Reports the result of the file I/O work in GetAllFileNames().
  void DidGetAllFileNames(
      blink::mojom::NativeIOHost::GetAllFileNamesCallback callback,
      GetAllFileNamesResult result) {
    std::move(callback).Run(result.first == base::File::FILE_OK,
                            std::move(result.second));
  }
  
  }  // namespace
  
  NativeIOHost::NativeIOHost(NativeIOContext* context,
                             const url::Origin& origin,
                             base::FilePath root_path)
      : root_path_(std::move(root_path)),
        context_(context),
        origin_(origin),
        file_task_runner_(CreateFileTaskRunner()) {
    DCHECK(!root_path_.empty());
    DCHECK(context != nullptr);
  
    // base::Unretained is safe here because this NativeIOHost owns |receivers_|.
    // So, the unretained NativeIOHost is guaranteed to outlive |receivers_| and
    // the closure that it uses.
    receivers_.set_disconnect_handler(base::BindRepeating(
        &NativeIOHost::OnReceiverDisconnect, base::Unretained(this)));
  }
  
  NativeIOHost::~NativeIOHost() {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  }
  
  void NativeIOHost::BindReceiver(
      mojo::PendingReceiver<blink::mojom::NativeIOHost> receiver) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  
    receivers_.Add(this, std::move(receiver));
  }
  
  void NativeIOHost::OpenFile(
      const std::string& name,
      mojo::PendingReceiver<blink::mojom::NativeIOFileHost> file_host_receiver,
      OpenFileCallback callback) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  
    if (!IsValidNativeIOName(name)) {
      mojo::ReportBadMessage("Invalid file name");
      std::move(callback).Run(base::File());
      return;
    }
  
    if (open_file_hosts_.find(name) != open_file_hosts_.end()) {
      // TODO(pwnall): Report that the file is locked.
      std::move(callback).Run(base::File());
      return;
    }
  
    auto insert_result = io_pending_files_.insert(name);
    bool insert_success = insert_result.second;
    if (!insert_success) {
      // TODO(pwnall): Report that the file is locked.
      std::move(callback).Run(base::File());
      return;
    }
  
    file_task_runner_->PostTaskAndReplyWithResult(
        FROM_HERE, base::BindOnce(&DoOpenFile, root_path_, name),
        base::BindOnce(&NativeIOHost::DidOpenFile, weak_factory_.GetWeakPtr(),
                       name, std::move(file_host_receiver), std::move(callback)));
  }
  
  void NativeIOHost::DeleteFile(const std::string& name,
                                DeleteFileCallback callback) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  
    if (!IsValidNativeIOName(name)) {
      mojo::ReportBadMessage("Invalid file name");
      std::move(callback).Run(false);
      return;
    }
  
    if (open_file_hosts_.find(name) != open_file_hosts_.end()) {
      // TODO(pwnall): Report that the file is locked.
      std::move(callback).Run(false);
      return;
    }
  
    auto insert_result = io_pending_files_.insert(name);
    bool insert_success = insert_result.second;
    if (!insert_success) {
      // TODO(pwnall): Report that the file is locked.
      std::move(callback).Run(false);
      return;
    }
  
    file_task_runner_->PostTaskAndReplyWithResult(
        FROM_HERE, base::BindOnce(&DoDeleteFile, root_path_, name),
        base::BindOnce(&NativeIOHost::DidDeleteFile, weak_factory_.GetWeakPtr(),
                       name, std::move(callback)));
  }
  
  void NativeIOHost::GetAllFileNames(GetAllFileNamesCallback callback) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  
    file_task_runner_->PostTaskAndReplyWithResult(
        FROM_HERE, base::BindOnce(&DoGetAllFileNames, root_path_),
        base::BindOnce(&DidGetAllFileNames, std::move(callback)));
  }
  
  void NativeIOHost::OnFileClose(NativeIOFileHost* file_host) {
    DCHECK(open_file_hosts_.count(file_host->file_name()) > 0);
    DCHECK_EQ(open_file_hosts_[file_host->file_name()].get(), file_host);
  
    open_file_hosts_.erase(file_host->file_name());
  }
  
  void NativeIOHost::OnReceiverDisconnect() {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  
    context_->OnHostReceiverDisconnect(this);
  }
  
  void NativeIOHost::DidOpenFile(
      const std::string& name,
      mojo::PendingReceiver<blink::mojom::NativeIOFileHost> file_host_receiver,
      OpenFileCallback callback,
      base::File file) {
    DCHECK(io_pending_files_.count(name));
    DCHECK(!open_file_hosts_.count(name));
    io_pending_files_.erase(name);
  
    if (!file.IsValid()) {
      std::move(callback).Run(std::move(file));
      return;
    }
  
    open_file_hosts_.insert(
        {name, std::make_unique<NativeIOFileHost>(std::move(file_host_receiver),
                                                  this, name)});
  
    std::move(callback).Run(std::move(file));
    return;
  }
  
  void NativeIOHost::DidDeleteFile(const std::string& name,
                                   DeleteFileCallback callback,
                                   bool success) {
    DCHECK(io_pending_files_.count(name));
    DCHECK(!open_file_hosts_.count(name));
    io_pending_files_.erase(name);
  
    std::move(callback).Run(success);
    return;
  }
  
  }  // namespace content
  