chromium/chrome/browser/ash/file_system_provider/fake_provided_file_system.cc

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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chrome/browser/ash/file_system_provider/fake_provided_file_system.h"

#include <stddef.h>

#include <memory>
#include <utility>

#include "base/functional/bind.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "chrome/browser/ash/file_system_provider/provided_file_system_interface.h"
#include "components/services/filesystem/public/mojom/types.mojom.h"
#include "net/base/io_buffer.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash::file_system_provider {

const char kFakeFileText[] =
    "This is a testing file. Lorem ipsum dolor sit amet est.";

namespace {

const char kFakeFileName[] = "hello.txt";
const size_t kFakeFileSize = sizeof(kFakeFileText) - 1u;
const char kFakeFileModificationTime[] = "Fri, 25 Apr 2014 01:47:53";
const char kFakeFileMimeType[] = "text/plain";

constexpr base::FilePath::CharType kBadFakeEntryPath1[] =
    FILE_PATH_LITERAL("/bad1");
constexpr char kBadFakeEntryName1[] = "/bad1";
constexpr base::FilePath::CharType kBadFakeEntryPath2[] =
    FILE_PATH_LITERAL("/bad2");
constexpr char kBadFakeEntryName2[] = "bad2";

}  // namespace

const base::FilePath::CharType kFakeFilePath[] =
    FILE_PATH_LITERAL("/hello.txt");

constexpr char kFakeFileVersionTag[] = "versionA";

FakeEntry::FakeEntry() = default;

FakeEntry::FakeEntry(std::unique_ptr<EntryMetadata> metadata,
                     const std::string& contents)
    : metadata(std::move(metadata)), contents(contents) {}

FakeEntry::~FakeEntry() = default;

FakeProvidedFileSystem::FakeProvidedFileSystem(
    const ProvidedFileSystemInfo& file_system_info)
    : file_system_info_(file_system_info), last_file_handle_(0) {
  AddEntry(base::FilePath(FILE_PATH_LITERAL("/")), /*is_directory=*/true,
           /*name=*/"", /*size=*/0, /*modification_time=*/base::Time(),
           /*mime_type=*/"", /*cloud_file_info=*/nullptr, /*contents=*/"");

  base::Time modification_time;
  EXPECT_TRUE(
      base::Time::FromUTCString(kFakeFileModificationTime, &modification_time));
  AddEntry(base::FilePath(kFakeFilePath), false, kFakeFileName, kFakeFileSize,
           modification_time, kFakeFileMimeType,
           std::make_unique<CloudFileInfo>(kFakeFileVersionTag), kFakeFileText);

  // Add a set of bad entries, in the root directory, which should be filtered
  // out.
  AddEntry(base::FilePath(kBadFakeEntryPath1), false, kBadFakeEntryName1,
           kFakeFileSize, modification_time, kFakeFileMimeType,
           /*cloud_file_info=*/nullptr, kFakeFileText);
  AddEntry(base::FilePath(kBadFakeEntryPath2), false, kBadFakeEntryName2,
           kFakeFileSize, modification_time, kFakeFileMimeType,
           /*cloud_file_info=*/nullptr, kFakeFileText);
}

FakeProvidedFileSystem::~FakeProvidedFileSystem() = default;

void FakeProvidedFileSystem::AddEntry(
    const base::FilePath& entry_path,
    bool is_directory,
    const std::string& name,
    int64_t size,
    base::Time modification_time,
    std::string mime_type,
    std::unique_ptr<CloudFileInfo> cloud_file_info,
    std::string contents) {
  DCHECK(entries_.find(entry_path) == entries_.end())
      << "Already present " << entry_path;
  std::unique_ptr<EntryMetadata> metadata(new EntryMetadata);

  metadata->is_directory = std::make_unique<bool>(is_directory);
  metadata->name = std::make_unique<std::string>(name);
  metadata->size = std::make_unique<int64_t>(size);
  metadata->modification_time = std::make_unique<base::Time>(modification_time);
  metadata->mime_type = std::make_unique<std::string>(mime_type);
  metadata->cloud_file_info = std::move(cloud_file_info);

  entries_[entry_path] =
      std::make_unique<FakeEntry>(std::move(metadata), contents);
}

FakeEntry* FakeProvidedFileSystem::GetEntry(
    const base::FilePath& entry_path) const {
  const Entries::const_iterator entry_it = entries_.find(entry_path);
  if (entry_it == entries_.end())
    return nullptr;

  return entry_it->second.get();
}

base::File::Error FakeProvidedFileSystem::CopyOrMoveEntry(
    const base::FilePath& source_path,
    const base::FilePath& target_path,
    bool is_move) {
  // Check that the source entry exists.
  const FakeEntry* source_entry = GetEntry(source_path);
  if (!source_entry) {
    return base::File::FILE_ERROR_NOT_FOUND;
  }
  // Delete `target_path` if it already exists, since AddEntry DCHECKs that
  // there's no existing entry.
  switch (auto err = DoDeleteEntry(target_path, /*recursive=*/false)) {
    case base::File::Error::FILE_OK:
    case base::File::Error::FILE_ERROR_NOT_FOUND:
      break;
    default:
      return err;
  }
  // Copy source entry.
  DCHECK_NE(source_entry->metadata, nullptr);
  DCHECK_NE(source_entry->metadata->is_directory, nullptr);
  DCHECK_NE(source_entry->metadata->name, nullptr);
  DCHECK_NE(source_entry->metadata->size, nullptr);
  DCHECK_NE(source_entry->metadata->modification_time, nullptr);
  DCHECK_NE(source_entry->metadata->mime_type, nullptr);

  auto cloud_file_info =
      (source_entry->metadata->cloud_file_info)
          ? std::make_unique<CloudFileInfo>(
                source_entry->metadata->cloud_file_info->version_tag)
          : nullptr;

  AddEntry(target_path, *(source_entry->metadata->is_directory),
           *(source_entry->metadata->name), *(source_entry->metadata->size),
           *(source_entry->metadata->modification_time),
           *(source_entry->metadata->mime_type), std::move(cloud_file_info),
           source_entry->contents);
  if (is_move) {
    entries_.erase(source_path);
  }
  return base::File::FILE_OK;
}

AbortCallback FakeProvidedFileSystem::RequestUnmount(
    storage::AsyncFileUtil::StatusCallback callback) {
  return PostAbortableTask(
      base::BindOnce(std::move(callback), base::File::FILE_OK));
}

AbortCallback FakeProvidedFileSystem::GetMetadata(
    const base::FilePath& entry_path,
    ProvidedFileSystemInterface::MetadataFieldMask fields,
    ProvidedFileSystemInterface::GetMetadataCallback callback) {
  const Entries::const_iterator entry_it = entries_.find(entry_path);

  if (entry_it == entries_.end()) {
    return PostAbortableTask(base::BindOnce(std::move(callback), nullptr,
                                            base::File::FILE_ERROR_NOT_FOUND));
  }

  std::unique_ptr<EntryMetadata> metadata(new EntryMetadata);
  if (fields & ProvidedFileSystemInterface::METADATA_FIELD_IS_DIRECTORY) {
    metadata->is_directory =
        std::make_unique<bool>(*entry_it->second->metadata->is_directory);
  }
  if (fields & ProvidedFileSystemInterface::METADATA_FIELD_NAME)
    metadata->name =
        std::make_unique<std::string>(*entry_it->second->metadata->name);
  if (fields & ProvidedFileSystemInterface::METADATA_FIELD_SIZE)
    metadata->size =
        std::make_unique<int64_t>(*entry_it->second->metadata->size);
  if (fields & ProvidedFileSystemInterface::METADATA_FIELD_MODIFICATION_TIME) {
    metadata->modification_time = std::make_unique<base::Time>(
        *entry_it->second->metadata->modification_time);
  }
  if (fields & ProvidedFileSystemInterface::METADATA_FIELD_MIME_TYPE &&
      entry_it->second->metadata->mime_type.get()) {
    metadata->mime_type =
        std::make_unique<std::string>(*entry_it->second->metadata->mime_type);
  }
  if (fields & ProvidedFileSystemInterface::METADATA_FIELD_THUMBNAIL &&
      entry_it->second->metadata->thumbnail.get()) {
    metadata->thumbnail =
        std::make_unique<std::string>(*entry_it->second->metadata->thumbnail);
  }
  // Make a copy of the `CloudFileInfo` to pass to the callback.
  if (fields & ProvidedFileSystemInterface::METADATA_FIELD_CLOUD_FILE_INFO &&
      entry_it->second->metadata->cloud_file_info.get()) {
    metadata->cloud_file_info = std::make_unique<CloudFileInfo>(
        entry_it->second->metadata->cloud_file_info->version_tag);
  }

  return PostAbortableTask(base::BindOnce(
      std::move(callback), std::move(metadata), base::File::FILE_OK));
}

AbortCallback FakeProvidedFileSystem::GetActions(
    const std::vector<base::FilePath>& entry_paths,
    ProvidedFileSystemInterface::GetActionsCallback callback) {
  const std::vector<Action> actions;
  return PostAbortableTask(
      base::BindOnce(std::move(callback), actions, base::File::FILE_OK));
}

AbortCallback FakeProvidedFileSystem::ExecuteAction(
    const std::vector<base::FilePath>& entry_paths,
    const std::string& action_id,
    storage::AsyncFileUtil::StatusCallback callback) {
  return PostAbortableTask(
      base::BindOnce(std::move(callback), base::File::FILE_OK));
}

AbortCallback FakeProvidedFileSystem::ReadDirectory(
    const base::FilePath& directory_path,
    storage::AsyncFileUtil::ReadDirectoryCallback callback) {
  storage::AsyncFileUtil::EntryList entry_list;

  for (Entries::const_iterator it = entries_.begin(); it != entries_.end();
       ++it) {
    const base::FilePath file_path = it->first;
    if (directory_path == file_path.DirName()) {
      const EntryMetadata* const metadata = it->second->metadata.get();
      filesystem::mojom::FsFileType entry_type =
          *metadata->is_directory ? filesystem::mojom::FsFileType::DIRECTORY
                                  : filesystem::mojom::FsFileType::REGULAR_FILE;
      if (*metadata->name == kBadFakeEntryName2) {
        entry_type = static_cast<filesystem::mojom::FsFileType>(7);
      }
      entry_list.emplace_back(base::FilePath(*metadata->name), entry_type);
    }
  }

  return PostAbortableTask(base::BindOnce(callback, base::File::FILE_OK,
                                          entry_list, /*has_more=*/false));
}

AbortCallback FakeProvidedFileSystem::OpenFile(const base::FilePath& entry_path,
                                               OpenFileMode mode,
                                               OpenFileCallback callback) {
  const Entries::const_iterator entry_it = entries_.find(entry_path);

  if (entry_it == entries_.end()) {
    return PostAbortableTask(base::BindOnce(
        std::move(callback), /*file_handle=*/0,
        base::File::FILE_ERROR_NOT_FOUND, /*cloud_file_info=*/nullptr));
  }

  FakeEntry& entry = *entry_it->second;
  if (mode == OPEN_FILE_MODE_WRITE && flush_required_) {
    DCHECK(!entry.write_buffer);
    entry.write_buffer = entry.contents;
  }

  // Make a copy of the `EntryMetadata` to pass to the callback.
  std::unique_ptr<EntryMetadata> metadata =
      (entry.metadata) ? std::make_unique<EntryMetadata>() : nullptr;
  if (entry.metadata && entry.metadata->cloud_file_info) {
    metadata->cloud_file_info = std::make_unique<CloudFileInfo>(
        entry.metadata->cloud_file_info->version_tag);
  }
  if (entry.metadata && entry.metadata->size) {
    metadata->size = std::make_unique<int64_t>(*entry.metadata->size);
  }

  const int file_handle = ++last_file_handle_;
  opened_files_[file_handle] = OpenedFile(entry_path, mode);
  return PostAbortableTask(base::BindOnce(std::move(callback), file_handle,
                                          base::File::FILE_OK,
                                          std::move(metadata)));
}

AbortCallback FakeProvidedFileSystem::CloseFile(
    int file_handle,
    storage::AsyncFileUtil::StatusCallback callback) {
  const auto opened_file_it = opened_files_.find(file_handle);

  if (opened_file_it == opened_files_.end()) {
    return PostAbortableTask(
        base::BindOnce(std::move(callback), base::File::FILE_ERROR_NOT_FOUND));
  }

  opened_files_.erase(opened_file_it);
  return PostAbortableTask(
      base::BindOnce(std::move(callback), base::File::FILE_OK));
}

AbortCallback FakeProvidedFileSystem::ReadFile(
    int file_handle,
    net::IOBuffer* buffer,
    int64_t offset,
    int length,
    ProvidedFileSystemInterface::ReadChunkReceivedCallback callback) {
  const auto opened_file_it = opened_files_.find(file_handle);

  if (opened_file_it == opened_files_.end() ||
      opened_file_it->second.file_path.AsUTF8Unsafe() != kFakeFilePath) {
    return PostAbortableTask(
        base::BindOnce(callback, /*chunk_length=*/0, /*has_more=*/false,
                       base::File::FILE_ERROR_INVALID_OPERATION));
  }

  const Entries::const_iterator entry_it =
      entries_.find(opened_file_it->second.file_path);
  if (entry_it == entries_.end()) {
    return PostAbortableTask(
        base::BindOnce(callback, /*chunk_length=*/0, /*has_more=*/false,
                       base::File::FILE_ERROR_INVALID_OPERATION));
  }

  // Send the response byte by byte.
  int64_t current_offset = offset;
  int current_length = length;

  // Reading behind EOF is fine, it will just return 0 bytes.
  if (current_offset >= *entry_it->second->metadata->size || !current_length) {
    return PostAbortableTask(base::BindOnce(callback, /*chunk_length=*/0,
                                            /*has_more=*/false,
                                            base::File::FILE_OK));
  }

  const FakeEntry* const entry = entry_it->second.get();
  std::vector<int> task_ids;
  while (current_offset < *entry->metadata->size && current_length) {
    buffer->data()[current_offset - offset] = entry->contents[current_offset];
    const bool has_more =
        (current_offset + 1 < *entry->metadata->size) && (current_length - 1);
    const int task_id = tracker_.PostTask(
        base::SingleThreadTaskRunner::GetCurrentDefault().get(), FROM_HERE,
        base::BindOnce(callback, /*chunk_length=*/1, has_more,
                       base::File::FILE_OK));
    task_ids.push_back(task_id);
    current_offset++;
    current_length--;
  }

  return base::BindOnce(&FakeProvidedFileSystem::AbortMany,
                        weak_ptr_factory_.GetWeakPtr(), task_ids);
}

AbortCallback FakeProvidedFileSystem::CreateDirectory(
    const base::FilePath& directory_path,
    bool recursive,
    storage::AsyncFileUtil::StatusCallback callback) {
  // No-op if the directory already exists.
  if (GetEntry(directory_path)) {
    return PostAbortableTask(
        base::BindOnce(std::move(callback), base::File::FILE_OK));
  }
  // Create directories recursively.
  std::vector<base::FilePath::StringType> components =
      directory_path.GetComponents();
  CHECK_EQ(components[0], "/");
  base::FilePath path(components[0]);
  for (size_t i = 1; i < components.size(); ++i) {
    path = path.AppendASCII(components[i]);
    if (GetEntry(path)) {
      continue;
    }
    // If `recursive` is set to false, only the last component of the provided
    // path should be new.
    if (!recursive && i < components.size() - 1) {
      return PostAbortableTask(base::BindOnce(
          std::move(callback), base::File::FILE_ERROR_INVALID_OPERATION));
    }
    AddEntry(path, /*is_directory=*/true, path.BaseName().value(), /*size=*/0,
             base::Time(), /*mime_type=*/"", /*cloud_file_info=*/nullptr,
             /*contents=*/"");
  }
  return PostAbortableTask(
      base::BindOnce(std::move(callback), base::File::FILE_OK));
}

AbortCallback FakeProvidedFileSystem::DeleteEntry(
    const base::FilePath& entry_path,
    bool recursive,
    storage::AsyncFileUtil::StatusCallback callback) {
  auto err = DoDeleteEntry(entry_path, recursive);
  return PostAbortableTask(base::BindOnce(std::move(callback), err));
}

base::File::Error FakeProvidedFileSystem::DoDeleteEntry(
    const base::FilePath& entry_path,
    bool recursive) {
  // Check that the entry to remove exists.
  if (!GetEntry(entry_path)) {
    return base::File::FILE_ERROR_NOT_FOUND;
  }
  // If `recursive` is false, check that `entry_path` is not parent of any entry
  // path in `entries_`.
  if (!recursive) {
    const Entries::const_iterator it =
        base::ranges::find_if(entries_, [entry_path](auto& entry_it) {
          return entry_path.IsParent(entry_it.first);
        });
    if (it != entries_.end()) {
      return base::File::FILE_ERROR_INVALID_OPERATION;
    }
  }
  // Erase the entry with path `entry_path`, as well as all child entries.
  for (Entries::const_iterator entry_it = entries_.begin();
       entry_it != entries_.end();) {
    if (entry_path == entry_it->first || entry_path.IsParent(entry_it->first)) {
      entry_it = entries_.erase(entry_it);
    } else {
      ++entry_it;
    }
  }
  return base::File::FILE_OK;
}

AbortCallback FakeProvidedFileSystem::CreateFile(
    const base::FilePath& file_path,
    storage::AsyncFileUtil::StatusCallback callback) {
  if (GetEntry(file_path)) {
    return PostAbortableTask(
        base::BindOnce(std::move(callback), base::File::FILE_ERROR_EXISTS));
  }
  AddEntry(file_path, /*is_directory=*/false, file_path.BaseName().value(),
           /*size=*/0, base::Time(), kFakeFileMimeType,
           /*cloud_file_info=*/nullptr, std::string());
  return PostAbortableTask(
      base::BindOnce(std::move(callback), base::File::FILE_OK));
}

AbortCallback FakeProvidedFileSystem::CopyEntry(
    const base::FilePath& source_path,
    const base::FilePath& target_path,
    storage::AsyncFileUtil::StatusCallback callback) {
  return PostAbortableTask(base::BindOnce(
      std::move(callback),
      CopyOrMoveEntry(source_path, target_path, /*is_move=*/false)));
}

AbortCallback FakeProvidedFileSystem::MoveEntry(
    const base::FilePath& source_path,
    const base::FilePath& target_path,
    storage::AsyncFileUtil::StatusCallback callback) {
  return PostAbortableTask(base::BindOnce(
      std::move(callback),
      CopyOrMoveEntry(source_path, target_path, /*is_move=*/true)));
}

AbortCallback FakeProvidedFileSystem::Truncate(
    const base::FilePath& file_path,
    int64_t length,
    storage::AsyncFileUtil::StatusCallback callback) {
  FakeEntry* const entry = GetEntry(file_path);
  if (!entry) {
    return PostAbortableTask(
        base::BindOnce(std::move(callback), base::File::FILE_ERROR_NOT_FOUND));
  }
  *entry->metadata->size = length;
  entry->contents.resize(*entry->metadata->size);
  return PostAbortableTask(
      base::BindOnce(std::move(callback), base::File::FILE_OK));
}

AbortCallback FakeProvidedFileSystem::WriteFile(
    int file_handle,
    net::IOBuffer* buffer,
    int64_t offset,
    int length,
    storage::AsyncFileUtil::StatusCallback callback) {
  const auto opened_file_it = opened_files_.find(file_handle);

  if (opened_file_it == opened_files_.end() ||
      !GetEntry(opened_file_it->second.file_path)) {
    return PostAbortableTask(base::BindOnce(
        std::move(callback), base::File::FILE_ERROR_INVALID_OPERATION));
  }

  FakeEntry* const entry = GetEntry(opened_file_it->second.file_path);
  if (!entry) {
    return PostAbortableTask(
        base::BindOnce(std::move(callback), base::File::FILE_ERROR_NOT_FOUND));
  }
  std::string& write_buffer =
      entry->write_buffer ? *entry->write_buffer : entry->contents;
  int64_t buffer_size = static_cast<int64_t>(write_buffer.size());
  if (offset > buffer_size) {
    return PostAbortableTask(base::BindOnce(
        std::move(callback), base::File::FILE_ERROR_INVALID_OPERATION));
  }

  // Allocate the string size in advance.
  if (offset + length > buffer_size) {
    if (!entry->write_buffer) {
      // Only update metadata if we are writing contents directly.
      *entry->metadata->size = offset + length;
      // Update the version when the contents change.
      if (entry->metadata->cloud_file_info.get()) {
        entry->metadata->cloud_file_info->version_tag += "1";
      }
    }
    write_buffer.resize(*entry->metadata->size);
  }

  write_buffer.replace(offset, length, buffer->data(), length);

  return PostAbortableTask(
      base::BindOnce(std::move(callback), base::File::FILE_OK));
}

AbortCallback FakeProvidedFileSystem::FlushFile(
    int file_handle,
    storage::AsyncFileUtil::StatusCallback callback) {
  const auto opened_file_it = opened_files_.find(file_handle);

  if (opened_file_it == opened_files_.end() ||
      !GetEntry(opened_file_it->second.file_path)) {
    return PostAbortableTask(base::BindOnce(
        std::move(callback), base::File::FILE_ERROR_INVALID_OPERATION));
  }

  FakeEntry* const entry = GetEntry(opened_file_it->second.file_path);
  if (!entry) {
    return PostAbortableTask(
        base::BindOnce(std::move(callback), base::File::FILE_ERROR_NOT_FOUND));
  }

  if (entry->write_buffer) {
    *entry->metadata->size = entry->write_buffer->size();
    entry->contents = std::move(*entry->write_buffer);
    entry->write_buffer = std::nullopt;
  }
  return PostAbortableTask(
      base::BindOnce(std::move(callback), base::File::FILE_OK));
}

AbortCallback FakeProvidedFileSystem::AddWatcher(
    const GURL& origin,
    const base::FilePath& entry_watcher,
    bool recursive,
    bool persistent,
    storage::AsyncFileUtil::StatusCallback callback,
    storage::WatcherManager::NotificationCallback notification_callback) {
  // Check if watcher already exists.
  const WatcherKey key(entry_watcher, recursive);
  const Watchers::iterator it = watchers_.find(key);
  if (it != watchers_.end()) {
    return PostAbortableTask(
        base::BindOnce(std::move(callback), base::File::FILE_OK));
  }

  Subscriber subscriber;
  subscriber.origin = origin;
  subscriber.persistent = persistent;
  subscriber.notification_callback = std::move(notification_callback);

  // Add watcher.
  Watcher* const watcher = &watchers_[key];
  watcher->entry_path = entry_watcher;
  watcher->recursive = recursive;
  watcher->subscribers[origin] = subscriber;

  // Notify observers.
  for (auto& observer : observers_) {
    observer.OnWatcherListChanged(file_system_info_, watchers_);
  }

  return PostAbortableTask(
      base::BindOnce(std::move(callback), base::File::FILE_OK));
}

void FakeProvidedFileSystem::RemoveWatcher(
    const GURL& origin,
    const base::FilePath& entry_path,
    bool recursive,
    storage::AsyncFileUtil::StatusCallback callback) {
  // Remove watcher.
  const WatcherKey key(entry_path, recursive);
  const auto it = watchers_.find(key);
  if (it != watchers_.end()) {
    watchers_.erase(it);
  }

  // Notify observers.
  for (auto& observer : observers_) {
    observer.OnWatcherListChanged(file_system_info_, watchers_);
  }

  std::move(callback).Run(base::File::FILE_OK);
}

const ProvidedFileSystemInfo& FakeProvidedFileSystem::GetFileSystemInfo()
    const {
  return file_system_info_;
}

OperationRequestManager* FakeProvidedFileSystem::GetRequestManager() {
  NOTREACHED_IN_MIGRATION();
  return nullptr;
}

Watchers* FakeProvidedFileSystem::GetWatchers() {
  return &watchers_;
}

const OpenedFiles& FakeProvidedFileSystem::GetOpenedFiles() const {
  return opened_files_;
}

void FakeProvidedFileSystem::AddObserver(ProvidedFileSystemObserver* observer) {
  DCHECK(observer);
  observers_.AddObserver(observer);
}

void FakeProvidedFileSystem::RemoveObserver(
    ProvidedFileSystemObserver* observer) {
  DCHECK(observer);
  observers_.RemoveObserver(observer);
}

void FakeProvidedFileSystem::Notify(
    const base::FilePath& entry_path,
    bool recursive,
    storage::WatcherManager::ChangeType change_type,
    std::unique_ptr<ProvidedFileSystemObserver::Changes> changes,
    const std::string& tag,
    storage::AsyncFileUtil::StatusCallback callback) {
  // Very simple implementation that unconditionally calls notification
  // callbacks and notifies observers of the change.

  const WatcherKey key(entry_path, recursive);
  const auto& watcher_it = watchers_.find(key);
  if (watcher_it == watchers_.end()) {
    std::move(callback).Run(base::File::FILE_ERROR_NOT_FOUND);
    return;
  }

  const ProvidedFileSystemObserver::Changes& changes_ref = *changes.get();

  // Call all notification callbacks (if any).
  for (const auto& subscriber_it : watcher_it->second.subscribers) {
    const storage::WatcherManager::NotificationCallback& notification_callback =
        subscriber_it.second.notification_callback;
    if (!notification_callback.is_null()) {
      notification_callback.Run(change_type);
    }
  }

  // Notify all observers.
  for (auto& observer : observers_) {
    observer.OnWatcherChanged(file_system_info_, watcher_it->second,
                              change_type, changes_ref, base::DoNothing());
  }
}

void FakeProvidedFileSystem::Configure(
    storage::AsyncFileUtil::StatusCallback callback) {
  NOTREACHED_IN_MIGRATION();
  std::move(callback).Run(base::File::FILE_ERROR_SECURITY);
}

base::WeakPtr<ProvidedFileSystemInterface>
FakeProvidedFileSystem::GetWeakPtr() {
  return weak_ptr_factory_.GetWeakPtr();
}

std::unique_ptr<ScopedUserInteraction>
FakeProvidedFileSystem::StartUserInteraction() {
  return nullptr;
}

base::WeakPtr<FakeProvidedFileSystem> FakeProvidedFileSystem::GetFakeWeakPtr() {
  return weak_ptr_factory_.GetWeakPtr();
}

AbortCallback FakeProvidedFileSystem::PostAbortableTask(
    base::OnceClosure callback) {
  const int task_id =
      tracker_.PostTask(base::SingleThreadTaskRunner::GetCurrentDefault().get(),
                        FROM_HERE, std::move(callback));
  return base::BindOnce(&FakeProvidedFileSystem::Abort,
                        weak_ptr_factory_.GetWeakPtr(), task_id);
}

void FakeProvidedFileSystem::Abort(int task_id) {
  tracker_.TryCancel(task_id);
}

void FakeProvidedFileSystem::AbortMany(const std::vector<int>& task_ids) {
  for (int task_id : task_ids) {
    tracker_.TryCancel(task_id);
  }
}

}  // namespace ash::file_system_provider