chromium/chrome/browser/chromeos/extensions/file_system_provider/file_system_provider_api.cc

// Copyright 2013 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/browser/chromeos/extensions/file_system_provider/file_system_provider_api.h"

#include <string>
#include <utility>
#include <vector>

#include "base/files/file.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/trace_event/trace_event.h"
#include "base/values.h"
#include "chrome/browser/chromeos/extensions/file_system_provider/provider_function.h"
#include "chrome/browser/chromeos/extensions/file_system_provider/service_worker_lifetime_manager.h"
#include "chrome/browser/extensions/chrome_extension_function_details.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/extensions/api/file_system_provider.h"
#include "chromeos/crosapi/mojom/file_system_provider.mojom.h"
#include "storage/browser/file_system/watcher_manager.h"

#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/ash/crosapi/crosapi_ash.h"
#include "chrome/browser/ash/crosapi/crosapi_manager.h"
#include "chrome/browser/ash/crosapi/file_system_provider_service_ash.h"
#include "chrome/browser/ash/guest_os/guest_os_terminal.h"
#include "chrome/common/webui_url_constants.h"
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

namespace extensions {
namespace {

constexpr const char kInterfaceUnavailable[] = "interface unavailable";

api::file_system_provider::FileSystemInfo ConvertFileSystemInfoMojomToExtension(
    crosapi::mojom::FileSystemInfoPtr info) {
  using api::file_system_provider::OpenedFile;
  using api::file_system_provider::Watcher;
  api::file_system_provider::FileSystemInfo item;
  item.file_system_id = info->metadata->file_system_id->id;
  item.display_name = info->metadata->display_name;
  item.writable = info->metadata->writable;
  item.opened_files_limit = info->metadata->opened_files_limit;
  item.supports_notify_tag = info->metadata->supports_notify;

  for (const auto& watcher : info->watchers) {
    Watcher watcher_item;
    watcher_item.entry_path = watcher->entry_path.value();
    watcher_item.recursive = watcher->recursive;
    if (!watcher->last_tag.empty()) {
      watcher_item.last_tag = watcher->last_tag;
    }
    item.watchers.push_back(std::move(watcher_item));
  }

  for (const auto& opened_file : info->opened_files) {
    OpenedFile opened_file_item;
    opened_file_item.open_request_id = opened_file->open_request_id;
    opened_file_item.file_path = opened_file->file_path;
    switch (opened_file->mode) {
      case crosapi::mojom::OpenFileMode::kRead:
        opened_file_item.mode =
            extensions::api::file_system_provider::OpenFileMode::kRead;
        break;
      case crosapi::mojom::OpenFileMode::kWrite:
        opened_file_item.mode =
            extensions::api::file_system_provider::OpenFileMode::kWrite;
        break;
    }
    item.opened_files.push_back(std::move(opened_file_item));
  }
  return item;
}

// Converts the change type from the IDL type to a mojom type. |changed_type|
// must be specified (not CHANGE_TYPE_NONE).
crosapi::mojom::FSPChangeType ParseChangeType(
    const api::file_system_provider::ChangeType& change_type) {
  switch (change_type) {
    case api::file_system_provider::ChangeType::kChanged:
      return crosapi::mojom::FSPChangeType::kChanged;
    case api::file_system_provider::ChangeType::kDeleted:
      return crosapi::mojom::FSPChangeType::kDeleted;
    default:
      break;
  }
  NOTREACHED_IN_MIGRATION();
  return crosapi::mojom::FSPChangeType::kChanged;
}

crosapi::mojom::CloudFileInfoPtr ParseCloudFileInfo(
    const std::optional<api::file_system_provider::CloudFileInfo>&
        cloud_file_info) {
  if (!cloud_file_info.has_value()) {
    return nullptr;
  }
  return crosapi::mojom::CloudFileInfo::New(
      cloud_file_info.value().version_tag);
}

// Convert the change from the IDL type to mojom type.
crosapi::mojom::FSPChangePtr ParseChange(
    const api::file_system_provider::Change& change) {
  crosapi::mojom::FSPChangePtr result = crosapi::mojom::FSPChange::New();
  result->path = base::FilePath::FromUTF8Unsafe(change.entry_path);
  result->type = ParseChangeType(change.change_type);
  result->cloud_file_info = ParseCloudFileInfo(change.cloud_file_info);
  return result;
}

// Converts a list of child changes from the IDL type to mojom type.
std::vector<crosapi::mojom::FSPChangePtr> ParseChanges(
    const std::vector<api::file_system_provider::Change>& changes) {
  std::vector<crosapi::mojom::FSPChangePtr> results;
  for (const auto& change : changes) {
    results.push_back(ParseChange(change));
  }
  return results;
}

}  // namespace

std::string FileSystemProviderBase::GetProviderId() const {
#if BUILDFLAG(IS_CHROMEOS_ASH)
  // Terminal app is the only non-extension to use fsp.
  if (!extension()) {
    CHECK(url::IsSameOriginWith(source_url(),
                                GURL(chrome::kChromeUIUntrustedTerminalURL)));
    return guest_os::kTerminalSystemAppId;
  }
#endif
  return extension_id();
}

void FileSystemProviderBase::RespondWithError(const std::string& error) {
  if (error.empty()) {
    Respond(NoArguments());
  } else {
    Respond(Error(error));
  }
}

#if BUILDFLAG(IS_CHROMEOS_LACROS)
bool FileSystemProviderBase::MountFinishedInterfaceAvailable() {
  auto* service = chromeos::LacrosService::Get();
  return service->GetInterfaceVersion<
             crosapi::mojom::FileSystemProviderService>() >=
         int{crosapi::mojom::FileSystemProviderService::MethodMinVersions::
                 kMountFinishedMinVersion};
}

bool FileSystemProviderBase::OperationFinishedInterfaceAvailable() {
  auto* service = chromeos::LacrosService::Get();
  return service->GetInterfaceVersion<
             crosapi::mojom::FileSystemProviderService>() >=
         int{crosapi::mojom::FileSystemProviderService::MethodMinVersions::
                 kOperationFinishedMinVersion};
}

bool FileSystemProviderBase::OpenFileFinishedSuccessfullyInterfaceAvailable() {
  auto* service = chromeos::LacrosService::Get();
  return service->GetInterfaceVersion<
             crosapi::mojom::FileSystemProviderService>() >=
         int{crosapi::mojom::FileSystemProviderService::MethodMinVersions::
                 kOpenFileFinishedSuccessfullyMinVersion};
}

mojo::Remote<crosapi::mojom::FileSystemProviderService>&
FileSystemProviderBase::GetRemote() {
  auto* service = chromeos::LacrosService::Get();
  return service->GetRemote<crosapi::mojom::FileSystemProviderService>();
}
#endif

ExtensionFunction::ResponseAction FileSystemProviderMountFunction::Run() {
  using api::file_system_provider::Mount::Params;
  const std::optional<Params> params(Params::Create(args()));
  EXTENSION_FUNCTION_VALIDATE(params);

  // It's an error if the file system Id is empty.
  if (params->options.file_system_id.empty()) {
    return RespondNow(
        Error(FileErrorToString(base::File::FILE_ERROR_INVALID_OPERATION)));
  }

  // It's an error if the display name is empty.
  if (params->options.display_name.empty()) {
    return RespondNow(
        Error(FileErrorToString(base::File::FILE_ERROR_INVALID_OPERATION)));
  }

  // If the opened files limit is set, then it must be larger or equal than 0.
  if (params->options.opened_files_limit &&
      *params->options.opened_files_limit < 0) {
    return RespondNow(
        Error(FileErrorToString(base::File::FILE_ERROR_INVALID_OPERATION)));
  }

  bool persistent =
      params->options.persistent ? *params->options.persistent : true;
  crosapi::mojom::FileSystemMetadataPtr metadata =
      crosapi::mojom::FileSystemMetadata::New();
  metadata->file_system_id = crosapi::mojom::FileSystemId::New();
  metadata->file_system_id->provider = GetProviderId();
  metadata->file_system_id->id = params->options.file_system_id;
  metadata->display_name = params->options.display_name;
  metadata->writable =
      params->options.writable ? *params->options.writable : false;
  metadata->opened_files_limit = base::saturated_cast<uint32_t>(
      params->options.opened_files_limit ? *params->options.opened_files_limit
                                         : 0);
  metadata->supports_notify = params->options.supports_notify_tag
                                  ? *params->options.supports_notify_tag
                                  : false;

  auto callback =
      base::BindOnce(&FileSystemProviderMountFunction::RespondWithError, this);
#if BUILDFLAG(IS_CHROMEOS_ASH)
  crosapi::CrosapiManager::Get()
      ->crosapi_ash()
      ->file_system_provider_service_ash()
      ->MountWithProfile(std::move(metadata), persistent, std::move(callback),
                         Profile::FromBrowserContext(browser_context()));
#else
  if (!OperationFinishedInterfaceAvailable())
    return RespondNow(Error(kInterfaceUnavailable));
  GetRemote()->Mount(std::move(metadata), persistent, std::move(callback));

#endif
  return RespondLater();
}

ExtensionFunction::ResponseAction FileSystemProviderUnmountFunction::Run() {
  using api::file_system_provider::Unmount::Params;
  std::optional<Params> params(Params::Create(args()));
  EXTENSION_FUNCTION_VALIDATE(params);

  auto id = crosapi::mojom::FileSystemId::New();
  id->provider = GetProviderId();
  id->id = params->options.file_system_id;
  auto callback = base::BindOnce(
      &FileSystemProviderUnmountFunction::RespondWithError, this);
#if BUILDFLAG(IS_CHROMEOS_ASH)
  crosapi::CrosapiManager::Get()
      ->crosapi_ash()
      ->file_system_provider_service_ash()
      ->UnmountWithProfile(std::move(id), std::move(callback),
                           Profile::FromBrowserContext(browser_context()));
#else
  if (!OperationFinishedInterfaceAvailable())
    return RespondNow(Error(kInterfaceUnavailable));
  GetRemote()->Unmount(std::move(id), std::move(callback));
#endif
  return RespondLater();
}

ExtensionFunction::ResponseAction FileSystemProviderGetAllFunction::Run() {
  auto callback =
      base::BindOnce(&FileSystemProviderGetAllFunction::RespondWithInfos, this);
#if BUILDFLAG(IS_CHROMEOS_ASH)
  crosapi::CrosapiManager::Get()
      ->crosapi_ash()
      ->file_system_provider_service_ash()
      ->GetAllWithProfile(GetProviderId(), std::move(callback),
                          Profile::FromBrowserContext(browser_context()));
#else
  if (!OperationFinishedInterfaceAvailable())
    return RespondNow(Error(kInterfaceUnavailable));
  GetRemote()->GetAll(GetProviderId(), std::move(callback));
#endif
  return RespondLater();
}

void FileSystemProviderGetAllFunction::RespondWithInfos(
    std::vector<crosapi::mojom::FileSystemInfoPtr> infos) {
  using api::file_system_provider::FileSystemInfo;
  std::vector<FileSystemInfo> items;
  for (auto& info : infos) {
    items.push_back(ConvertFileSystemInfoMojomToExtension(std::move(info)));
  }
  Respond(
      ArgumentList(api::file_system_provider::GetAll::Results::Create(items)));
}

ExtensionFunction::ResponseAction FileSystemProviderGetFunction::Run() {
  using api::file_system_provider::Get::Params;
  std::optional<Params> params(Params::Create(args()));
  EXTENSION_FUNCTION_VALIDATE(params);

  auto id = crosapi::mojom::FileSystemId::New();
  id->provider = GetProviderId();
  id->id = params->file_system_id;
  auto callback =
      base::BindOnce(&FileSystemProviderGetFunction::RespondWithInfo, this);
#if BUILDFLAG(IS_CHROMEOS_ASH)
  crosapi::CrosapiManager::Get()
      ->crosapi_ash()
      ->file_system_provider_service_ash()
      ->GetWithProfile(std::move(id), std::move(callback),
                       Profile::FromBrowserContext(browser_context()));
#else
  if (!OperationFinishedInterfaceAvailable())
    return RespondNow(Error(kInterfaceUnavailable));
  GetRemote()->Get(std::move(id), std::move(callback));
#endif
  return RespondLater();
}

void FileSystemProviderGetFunction::RespondWithInfo(
    crosapi::mojom::FileSystemInfoPtr info) {
  using api::file_system_provider::FileSystemInfo;
  if (!info) {
    Respond(Error(FileErrorToString(base::File::FILE_ERROR_NOT_FOUND)));
    return;
  }
  auto result = ConvertFileSystemInfoMojomToExtension(std::move(info));
  Respond(ArgumentList(
      api::file_system_provider::Get::Results::Create(std::move(result))));
}

ExtensionFunction::ResponseAction FileSystemProviderNotifyFunction::Run() {
  using api::file_system_provider::Notify::Params;
  std::optional<Params> params(Params::Create(args()));
  EXTENSION_FUNCTION_VALIDATE(params);

  auto callback =
      base::BindOnce(&FileSystemProviderNotifyFunction::RespondWithError, this);
  auto id = crosapi::mojom::FileSystemId::New();
  id->provider = GetProviderId();
  id->id = params->options.file_system_id;

  crosapi::mojom::FSPWatcherPtr watcher = crosapi::mojom::FSPWatcher::New();
  watcher->entry_path =
      base::FilePath::FromUTF8Unsafe(params->options.observed_path);
  watcher->recursive = params->options.recursive;
  watcher->last_tag = params->options.tag ? *params->options.tag : "";
  crosapi::mojom::FSPChangeType type =
      ParseChangeType(params->options.change_type);
  std::vector<crosapi::mojom::FSPChangePtr> changes;
  if (params->options.changes) {
    changes = ParseChanges(*params->options.changes);
  }

#if BUILDFLAG(IS_CHROMEOS_ASH)
  crosapi::CrosapiManager::Get()
      ->crosapi_ash()
      ->file_system_provider_service_ash()
      ->NotifyWithProfile(std::move(id), std::move(watcher), type,
                          std::move(changes), std::move(callback),
                          Profile::FromBrowserContext(browser_context()));
#else
  if (!OperationFinishedInterfaceAvailable())
    return RespondNow(Error(kInterfaceUnavailable));
  GetRemote()->Notify(std::move(id), std::move(watcher), type,
                      std::move(changes), std::move(callback));
#endif
  return RespondLater();
}

void FileSystemProviderNotifyFunction::OnNotifyCompleted(
    base::File::Error result) {
  if (result != base::File::FILE_OK) {
    Respond(Error(FileErrorToString(result)));
    return;
  }

  Respond(NoArguments());
}

bool FileSystemProviderInternal::ForwardMountResult(int64_t request_id,
                                                    base::Value::List& args) {
  auto* profile = Profile::FromBrowserContext(browser_context());
  auto* sw_lifetime_manager =
      extensions::file_system_provider::ServiceWorkerLifetimeManager::Get(
          profile);
  sw_lifetime_manager->FinishRequest({
      extension_id(),
      /*file_system_id=*/"",
      request_id,
  });
  auto callback =
      base::BindOnce(&FileSystemProviderInternal::RespondWithError, this);
#if BUILDFLAG(IS_CHROMEOS_ASH)
  crosapi::CrosapiManager::Get()
      ->crosapi_ash()
      ->file_system_provider_service_ash()
      ->MountFinishedWithProfile(extension_id(), request_id, std::move(args),
                                 std::move(callback), profile);
  return true;
#else
  if (!MountFinishedInterfaceAvailable())
    return false;
  GetRemote()->MountFinished(extension_id(), request_id, std::move(args),
                             std::move(callback));
  return true;
#endif
}

ExtensionFunction::ResponseAction
FileSystemProviderInternalRespondToMountRequestFunction::Run() {
  using api::file_system_provider_internal::RespondToMountRequest::Params;
  std::optional<Params> params(Params::Create(args()));
  EXTENSION_FUNCTION_VALIDATE(params);

  int64_t request_id = params->request_id;
  bool result = ForwardMountResult(request_id, mutable_args());
  if (!result)
    Respond(Error(kInterfaceUnavailable));
  return RespondLater();
}

bool FileSystemProviderInternal::ForwardOperationResultImpl(
    crosapi::mojom::FSPOperationResponse response,
    crosapi::mojom::FileSystemIdPtr file_system_id,
    int request_id,
    base::Value::List args) {
  auto* profile = Profile::FromBrowserContext(browser_context());
  auto* sw_lifetime_manager =
      extensions::file_system_provider::ServiceWorkerLifetimeManager::Get(
          profile);
  sw_lifetime_manager->FinishRequest({
      file_system_id->provider,
      file_system_id->id,
      request_id,
  });
  auto callback =
      base::BindOnce(&FileSystemProviderInternal::RespondWithError, this);
#if BUILDFLAG(IS_CHROMEOS_ASH)
  crosapi::CrosapiManager::Get()
      ->crosapi_ash()
      ->file_system_provider_service_ash()
      ->OperationFinishedWithProfile(response, std::move(file_system_id),
                                     request_id, std::move(args),
                                     std::move(callback), profile);
  return true;
#else
  if (!OperationFinishedInterfaceAvailable()) {
    return false;
  }
  GetRemote()->OperationFinished(response, std::move(file_system_id),
                                 request_id, std::move(args),
                                 std::move(callback));
  return true;
#endif
}

ExtensionFunction::ResponseAction
FileSystemProviderInternalUnmountRequestedSuccessFunction::Run() {
  using api::file_system_provider_internal::UnmountRequestedSuccess::Params;
  std::optional<Params> params(Params::Create(args()));
  EXTENSION_FUNCTION_VALIDATE(params);

  bool result = ForwardOperationResult(
      params, mutable_args(),
      crosapi::mojom::FSPOperationResponse::kUnmountSuccess);
  if (!result)
    Respond(Error(kInterfaceUnavailable));
  return RespondLater();
}

ExtensionFunction::ResponseAction
FileSystemProviderInternalGetMetadataRequestedSuccessFunction::Run() {
  using api::file_system_provider_internal::GetMetadataRequestedSuccess::Params;
  std::optional<Params> params(Params::Create(args()));
  EXTENSION_FUNCTION_VALIDATE(params);

  bool result = ForwardOperationResult(
      params, mutable_args(),
      crosapi::mojom::FSPOperationResponse::kGetEntryMetadataSuccess);
  if (!result)
    return RespondNow(Error(kInterfaceUnavailable));
  return RespondLater();
}

ExtensionFunction::ResponseAction
FileSystemProviderInternalGetActionsRequestedSuccessFunction::Run() {
  using api::file_system_provider_internal::GetActionsRequestedSuccess::Params;
  std::optional<Params> params(Params::Create(args()));
  EXTENSION_FUNCTION_VALIDATE(params);
  bool result = ForwardOperationResult(
      params, mutable_args(),
      crosapi::mojom::FSPOperationResponse::kGetActionsSuccess);
  if (!result)
    return RespondNow(Error(kInterfaceUnavailable));
  return RespondLater();
}

ExtensionFunction::ResponseAction
FileSystemProviderInternalReadDirectoryRequestedSuccessFunction::Run() {
  using api::file_system_provider_internal::ReadDirectoryRequestedSuccess::
      Params;
  std::optional<Params> params(Params::Create(args()));
  EXTENSION_FUNCTION_VALIDATE(params);
  bool result = ForwardOperationResult(
      params, mutable_args(),
      crosapi::mojom::FSPOperationResponse::kReadDirectorySuccess);
  if (!result)
    return RespondNow(Error(kInterfaceUnavailable));
  return RespondLater();
}

ExtensionFunction::ResponseAction
FileSystemProviderInternalReadFileRequestedSuccessFunction::Run() {
  TRACE_EVENT0("file_system_provider", "ReadFileRequestedSuccess");
  using api::file_system_provider_internal::ReadFileRequestedSuccess::Params;

  // TODO(crbug.com/40221395): Improve performance by removing copy.
  std::optional<Params> params(Params::Create(args()));
  EXTENSION_FUNCTION_VALIDATE(params);
  bool result = ForwardOperationResult(
      params, mutable_args(),
      crosapi::mojom::FSPOperationResponse::kReadFileSuccess);
  if (!result)
    return RespondNow(Error(kInterfaceUnavailable));
  return RespondLater();
}

ExtensionFunction::ResponseAction
FileSystemProviderInternalOpenFileRequestedSuccessFunction::Run() {
  TRACE_EVENT0("file_system_provider", "OpenFileRequestedSuccess");
  using api::file_system_provider_internal::OpenFileRequestedSuccess::Params;

  // TODO(crbug.com/40221395): Improve performance by removing copy.
  std::optional<Params> params(Params::Create(args()));
  EXTENSION_FUNCTION_VALIDATE(params);
  bool result = ForwardOpenFileFinishedSuccessullyResult(std::move(params),
                                                         mutable_args());
  if (!result) {
    return RespondNow(Error(kInterfaceUnavailable));
  }
  return RespondLater();
}

bool FileSystemProviderInternal::ForwardOpenFileFinishedSuccessullyResult(
    std::optional<
        api::file_system_provider_internal::OpenFileRequestedSuccess::Params>
        params,
    base::Value::List& args) {
#if BUILDFLAG(IS_CHROMEOS_LACROS)
  if (!OpenFileFinishedSuccessfullyInterfaceAvailable()) {
    // The `kGenericSuccess` requires 3 args to deserialize on the Ash side. If
    // the IDL supports the optional `EntryMetadata` struct, it will always
    // contain 4 (either an empty base::Value() or the `EntryMetadata`). To
    // ensure we can successfully deserialize, remove the extra args.
    mutable_args().resize(3);
    return ForwardOperationResult(
        params, mutable_args(),
        crosapi::mojom::FSPOperationResponse::kGenericSuccess);
  }
#endif

  crosapi::mojom::FileSystemIdPtr file_system_id;
  int64_t request_id;
  GetOperationMetadata(params, &file_system_id, &request_id);
  auto* profile = Profile::FromBrowserContext(browser_context());
  auto* sw_lifetime_manager =
      extensions::file_system_provider::ServiceWorkerLifetimeManager::Get(
          profile);
  sw_lifetime_manager->FinishRequest({
      file_system_id->provider,
      file_system_id->id,
      request_id,
  });
  auto callback =
      base::BindOnce(&FileSystemProviderInternal::RespondWithError, this);
#if BUILDFLAG(IS_CHROMEOS_ASH)
  crosapi::CrosapiManager::Get()
      ->crosapi_ash()
      ->file_system_provider_service_ash()
      ->OpenFileFinishedSuccessfullyWithProfile(
          std::move(file_system_id), request_id, std::move(mutable_args()),
          std::move(callback), profile);
#else
  GetRemote()->OpenFileFinishedSuccessfully(
      std::move(file_system_id), request_id, std::move(mutable_args()),
      std::move(callback));
#endif
  return true;
}

ExtensionFunction::ResponseAction
FileSystemProviderInternalOperationRequestedSuccessFunction::Run() {
  using api::file_system_provider_internal::OperationRequestedSuccess::Params;
  std::optional<Params> params(Params::Create(args()));
  EXTENSION_FUNCTION_VALIDATE(params);

  bool result = ForwardOperationResult(
      params, mutable_args(),
      crosapi::mojom::FSPOperationResponse::kGenericSuccess);
  if (!result)
    return RespondNow(Error(kInterfaceUnavailable));
  return RespondLater();
}

ExtensionFunction::ResponseAction
FileSystemProviderInternalOperationRequestedErrorFunction::Run() {
  using api::file_system_provider_internal::OperationRequestedError::Params;
  std::optional<Params> params(Params::Create(args()));
  EXTENSION_FUNCTION_VALIDATE(params);

  if (params->error == api::file_system_provider::ProviderError::kOk) {
    // It's incorrect to pass OK as an error code.
    return ValidationFailure(this);
  }

  bool result = ForwardOperationResult(
      params, mutable_args(),
      crosapi::mojom::FSPOperationResponse::kGenericFailure);
  if (!result)
    return RespondNow(Error(kInterfaceUnavailable));
  return RespondLater();
}

}  // namespace extensions