chromium/chrome/services/file_util/zip_file_creator.cc

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/services/file_util/zip_file_creator.h"

#include <utility>

#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/strcat.h"
#include "components/services/filesystem/public/mojom/types.mojom.h"
#include "third_party/zlib/google/zip.h"

namespace chrome {
namespace {

// Output operator for logging.
std::ostream& operator<<(std::ostream& out, const base::File::Error error) {
  switch (error) {
    case base::File::FILE_OK:
      return out << "FILE_OK";
#define ENTRY(S)      \
  case base::File::S: \
    return out << #S;
      ENTRY(FILE_ERROR_FAILED);
      ENTRY(FILE_ERROR_IN_USE);
      ENTRY(FILE_ERROR_EXISTS);
      ENTRY(FILE_ERROR_NOT_FOUND);
      ENTRY(FILE_ERROR_ACCESS_DENIED);
      ENTRY(FILE_ERROR_TOO_MANY_OPENED);
      ENTRY(FILE_ERROR_NO_MEMORY);
      ENTRY(FILE_ERROR_NO_SPACE);
      ENTRY(FILE_ERROR_NOT_A_DIRECTORY);
      ENTRY(FILE_ERROR_INVALID_OPERATION);
      ENTRY(FILE_ERROR_SECURITY);
      ENTRY(FILE_ERROR_ABORT);
      ENTRY(FILE_ERROR_NOT_A_FILE);
      ENTRY(FILE_ERROR_NOT_EMPTY);
      ENTRY(FILE_ERROR_INVALID_URL);
      ENTRY(FILE_ERROR_IO);
#undef ENTRY
    default:
      return out << "File::Error("
                 << static_cast<std::underlying_type_t<base::File::Error>>(
                        error)
                 << ")";
  }
}

std::string Redact(const std::string& s) {
  return LOG_IS_ON(INFO) ? base::StrCat({"'", s, "'"}) : "(redacted)";
}

std::string Redact(const base::FilePath& path) {
  return Redact(path.value());
}

// A zip::FileAccessor that talks to a file system through the Mojo
// filesystem::mojom::Directory.
class MojoFileAccessor : public zip::FileAccessor {
 public:
  explicit MojoFileAccessor(
      mojo::PendingRemote<filesystem::mojom::Directory> src_dir)
      : src_dir_(std::move(src_dir)) {}

  MojoFileAccessor(const MojoFileAccessor&) = delete;
  MojoFileAccessor& operator=(const MojoFileAccessor&) = delete;

  ~MojoFileAccessor() override = default;

  bool Open(const zip::Paths paths,
            std::vector<base::File>* const files) override {
    DCHECK(!paths.empty());

    std::vector<filesystem::mojom::FileOpenDetailsPtr> details;
    details.reserve(paths.size());

    for (const base::FilePath& path : paths) {
      DCHECK(!path.IsAbsolute());
      filesystem::mojom::FileOpenDetailsPtr open_details =
          filesystem::mojom::FileOpenDetails::New();
      open_details->path = path.value();
      open_details->open_flags =
          filesystem::mojom::kFlagOpen | filesystem::mojom::kFlagRead;
      details.push_back(std::move(open_details));
    }

    std::vector<filesystem::mojom::FileOpenResultPtr> results;
    if (!src_dir_->OpenFileHandles(std::move(details), &results)) {
      LOG(ERROR) << "Cannot open " << Redact(paths.front()) << " and "
                 << (paths.size() - 1) << " other files";
      return false;
    }

    files->reserve(files->size() + results.size());
    for (const filesystem::mojom::FileOpenResultPtr& result : results)
      files->push_back(std::move(result->file_handle));

    return true;
  }

  bool List(const base::FilePath& path,
            std::vector<base::FilePath>* const files,
            std::vector<base::FilePath>* const subdirs) override {
    DCHECK(!path.IsAbsolute());
    DCHECK(files);
    DCHECK(subdirs);

    // |dir_remote| is the directory that is open if |path| is not empty. Note
    // that it must be defined outside of the else block so it does not get
    // deleted before |dir| is used (which would make |dir| invalid).
    mojo::Remote<filesystem::mojom::Directory> dir_remote;
    filesystem::mojom::Directory* dir = nullptr;
    if (path.empty()) {
      dir = src_dir_.get();
    } else {
      base::File::Error error;
      src_dir_->OpenDirectory(
          path.value(), dir_remote.BindNewPipeAndPassReceiver(),
          filesystem::mojom::kFlagRead | filesystem::mojom::kFlagOpen, &error);
      if (error != base::File::Error::FILE_OK) {
        LOG(ERROR) << "Cannot open " << Redact(path) << ": " << error;
        return false;
      }
      dir = dir_remote.get();
    }

    std::optional<std::vector<filesystem::mojom::DirectoryEntryPtr>> contents;
    base::File::Error error;
    dir->Read(&error, &contents);
    if (error != base::File::Error::FILE_OK) {
      LOG(ERROR) << "Cannot list content of " << Redact(path) << ": " << error;
      return false;
    }

    if (!contents)
      return true;

    for (const filesystem::mojom::DirectoryEntryPtr& entry : *contents) {
      (entry->type == filesystem::mojom::FsFileType::DIRECTORY ? subdirs
                                                               : files)
          ->push_back(path.Append(entry->name));
    }

    return true;
  }

  bool GetInfo(const base::FilePath& path, Info* const info) override {
    DCHECK(!path.IsAbsolute());
    DCHECK(info);

    base::File::Error error;
    filesystem::mojom::FileInformationPtr file_info;
    src_dir_->StatFile(path.value(), &error, &file_info);
    if (error != base::File::Error::FILE_OK) {
      LOG(ERROR) << "Cannot stat " << Redact(path) << ": " << error;
      return false;
    }

    info->is_directory =
        file_info->type == filesystem::mojom::FsFileType::DIRECTORY;
    info->last_modified =
        base::Time::FromSecondsSinceUnixEpoch(file_info->mtime);
    return true;
  }

 private:
  // Interface ptr to the source directory.
  const mojo::Remote<filesystem::mojom::Directory> src_dir_;
};

}  // namespace

ZipFileCreator::ZipFileCreator(PendingCreator receiver)
    : receiver_(this, std::move(receiver)) {
  receiver_.set_disconnect_handler(
      base::BindOnce(&ZipFileCreator::OnDisconnect, base::AdoptRef(this)));
}

ZipFileCreator::~ZipFileCreator() {
  DCHECK(cancelled_.IsSet());
}

void ZipFileCreator::CreateZipFile(
    PendingDirectory src_dir,
    const std::vector<base::FilePath>& relative_paths,
    base::File zip_file,
    PendingListener listener) {
  DCHECK(zip_file.IsValid());

  for (const base::FilePath& path : relative_paths) {
    if (path.IsAbsolute() || path.ReferencesParent()) {
      // Paths are expected to be relative. If there are not, the API is used
      // incorrectly and this is an error.
      Listener(std::move(listener))->OnFinished(/*success=*/false);
      return;
    }
  }

  runner_->PostTask(
      FROM_HERE, base::BindOnce(&ZipFileCreator::WriteZipFile, this,
                                std::move(src_dir), std::move(relative_paths),
                                std::move(zip_file), std::move(listener)));
}

void ZipFileCreator::WriteZipFile(
    PendingDirectory src_dir,
    const std::vector<base::FilePath>& relative_paths,
    base::File zip_file,
    PendingListener pending_listener) const {
  MojoFileAccessor file_accessor(std::move(src_dir));
  const Listener listener(std::move(pending_listener));
  const bool success = zip::Zip({
      .file_accessor = &file_accessor,
      .dest_fd = zip_file.GetPlatformFile(),
      .src_files = relative_paths,
      .progress_callback = base::BindRepeating(&ZipFileCreator::OnProgress,
                                               this, std::cref(listener)),
      .progress_period = base::Seconds(1),
      .recursive = true,
      .continue_on_error = true,
  });

  listener->OnFinished(success);
}

bool ZipFileCreator::OnProgress(const Listener& listener,
                                const zip::Progress& progress) const {
  listener->OnProgress(progress.bytes, progress.files, progress.directories);
  return !cancelled_.IsSet();
}

void ZipFileCreator::OnDisconnect() {
  DCHECK(receiver_.is_bound());
  receiver_.reset();
  DCHECK(!cancelled_.IsSet());
  cancelled_.Set();
}

}  // namespace chrome