// Copyright 2019 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/file_system_access/chrome_file_system_access_permission_context.h"

#include <algorithm>
#include <iterator>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>

#include "base/auto_reset.h"
#include "base/base_paths.h"
#include "base/containers/to_vector.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/json/values_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/path_service.h"
#include "base/strings/escape.h"
#include "base/strings/strcat.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "base/types/expected.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/download/download_prefs.h"
#include "chrome/browser/file_system_access/file_system_access_features.h"
#include "chrome/browser/file_system_access/file_system_access_permission_request_manager.h"
#include "chrome/browser/permissions/one_time_permissions_tracker_factory.h"
#include "chrome/browser/permissions/one_time_permissions_tracker_observer.h"
#include "chrome/browser/permissions/permission_decision_auto_blocker_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/file_system_access/file_system_access_dialogs.h"
#include "chrome/browser/ui/file_system_access/file_system_access_restricted_directory_dialog.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/grit/generated_resources.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/guest_view/buildflags/buildflags.h"
#include "components/pdf/common/pdf_util.h"
#include "components/permissions/features.h"
#include "components/permissions/object_permission_context_base.h"
#include "components/permissions/permission_decision_auto_blocker.h"
#include "components/permissions/permission_uma_util.h"
#include "components/permissions/permission_util.h"
#include "components/safe_browsing/buildflags.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/disallow_activation_reason.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/security_principal.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/web_contents.h"
#include "extensions/buildflags/buildflags.h"
#include "storage/browser/file_system/external_mount_points.h"
#include "third_party/blink/public/common/features_generated.h"
#include "third_party/blink/public/mojom/file_system_access/file_system_access_manager.mojom.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/display/types/display_constants.h"
#include "url/gurl.h"
#include "url/origin.h"

#if BUILDFLAG(IS_ANDROID)
#include "base/android/apk_info.h"
#include "base/android/content_uri_utils.h"
#include "base/strings/string_util.h"
#include "chrome/browser/glic/host/guest_util.h"  // nogncheck
#include "chrome/browser/ui/android/tab_model/tab_model.h"
#include "chrome/browser/ui/android/tab_model/tab_model_list.h"
#else
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"  // nogncheck crbug.com/40147906
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "chrome/browser/ui/views/file_system_access/file_system_access_page_action_controller.h"
#include "chrome/browser/web_applications/proto/web_app_install_state.pb.h"  // nogncheck
#include "chrome/browser/web_applications/web_app_install_manager.h"
#include "chrome/browser/web_applications/web_app_install_manager_observer.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "components/tabs/public/tab_interface.h"
#if BUILDFLAG(ENABLE_PLATFORM_APPS)
#include "extensions/browser/extension_registry.h"  // nogncheck
#include "extensions/common/extension.h"
#endif  // BUILDFLAG(ENABLE_PLATFORM_APPS)
#endif  // BUILDFLAG(IS_ANDROID)
#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/ash/file_manager/fileapi_util.h"
#include "chrome/browser/ash/fusebox/fusebox_server.h"
#include "content/public/browser/storage_partition.h"
#endif  // BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(SAFE_BROWSING_DOWNLOAD_PROTECTION)
#include "chrome/browser/safe_browsing/download_protection/download_protection_service.h"
#include "chrome/browser/safe_browsing/safe_browsing_service.h"
#endif

#if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
#include "chrome/browser/enterprise/connectors/analysis/content_analysis_delegate.h"
#include "chrome/browser/safe_browsing/download_protection/download_protection_util.h"
#include "chrome/browser/ui/file_system_access/file_system_access_dangerous_file_dialog.h"
#include "components/safe_browsing/content/common/file_type_policies.h"
#endif

#if BUILDFLAG(ENABLE_EXTENSIONS_CORE) && BUILDFLAG(ENABLE_GUEST_VIEW)
#include "extensions/browser/guest_view/web_view/web_view_guest.h"
#endif  // BUILDFLAG(ENABLE_EXTENSIONS_CORE) && BUILDFLAG(ENABLE_GUEST_VIEW)

namespace {

#if BUILDFLAG(IS_CHROMEOS)
base::FilePath GetExternalPath(Profile* profile,
                               storage::FileSystemContext* file_system_context,
                               storage::ExternalMountPoints* mount_points,
                               const base::FilePath& virtual_path) {
  std::string ignored_mount_name;
  storage::FileSystemMountOption ignored_mount_option;
  base::FilePath physical_path;
  if (!mount_points || !mount_points->CrackVirtualPath(
                           virtual_path, &ignored_mount_name, nullptr, nullptr,
                           &physical_path, &ignored_mount_option)) {
    return base::FilePath();
  }

  base::FilePath resolved_path = physical_path;
  if (file_system_context && profile) {
    GURL external_gurl;
    if (file_manager::util::ConvertAbsoluteFilePathToFileSystemUrl(
            profile, physical_path, file_manager::util::GetFileManagerURL(),
            &external_gurl)) {
      storage::FileSystemURL external_cracked_url =
          file_system_context->CrackURLInFirstPartyContext(external_gurl);
      if (external_cracked_url.is_valid()) {
        base::FilePath fusebox_path =
            fusebox::Server::SubstituteFuseboxFilePath(external_cracked_url);
        if (!fusebox_path.empty()) {
          resolved_path = std::move(fusebox_path);
        }
      }
    }
  }
  return resolved_path;
}
#endif  // BUILDFLAG(IS_CHROMEOS)

using FileRequestData =
    FileSystemAccessPermissionRequestManager::FileRequestData;
using RequestAccess = FileSystemAccessPermissionRequestManager::Access;
using HandleType = content::FileSystemAccessPermissionContext::HandleType;
using UserAction = content::FileSystemAccessPermissionContext::UserAction;
using PersistedGrantStatus =
    ChromeFileSystemAccessPermissionContext::PersistedGrantStatus;
using GrantType = ChromeFileSystemAccessPermissionContext::GrantType;
using blink::mojom::PermissionStatus;
using permissions::PermissionAction;

// This long after the last top-level tab or window for an origin is closed (or
// is navigated to another origin), all the permissions for that origin will be
// revoked.
constexpr base::TimeDelta kPermissionRevocationTimeout = base::Seconds(5);

// Dictionary keys for the FILE_SYSTEM_ACCESS_CHOOSER_DATA setting.
// `kPermissionPathKey[] = "path"` is defined in the header file.
const char kPermissionDisplayNameKey[] = "display-name";
const char kPermissionIsDirectoryKey[] = "is-directory";
const char kPermissionWritableKey[] = "writable";
const char kPermissionReadableKey[] = "readable";
const char kDeprecatedPermissionLastUsedTimeKey[] = "time";

// Dictionary keys for the FILE_SYSTEM_LAST_PICKED_DIRECTORY website setting.
// Schema (per origin):
// {
//  ...
//   {
//     "default-id" : { "path" : <path> , "path-type" : <type>}
//     "custom-id-fruit" : { "path" : <path> , "path-type" : <type> }
//     "custom-id-flower" : { "path" : <path> , "path-type" : <type> }
//     ...
//   }
//  ...
// }
const char kDefaultLastPickedDirectoryKey[] = "default-id";
const char kCustomLastPickedDirectoryKey[] = "custom-id";
const char kPathKey[] = "path";
const char kDisplayNameKey[] = "display-name";
const char kPathTypeKey[] = "path-type";
const char kTimestampKey[] = "timestamp";

constexpr char kDefaultNotAllowedMessage[] =
    "Showing a file picker is not allowed.";

void ShowFileSystemAccessRestrictedDirectoryDialogOnUIThread(
    content::GlobalRenderFrameHostId frame_id,
    const url::Origin& origin,
    HandleType handle_type,
    base::OnceCallback<
        void(ChromeFileSystemAccessPermissionContext::SensitiveEntryResult)>
        callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  content::RenderFrameHost* rfh = content::RenderFrameHost::FromID(frame_id);
  if (!rfh || !rfh->IsActive()) {
    // Requested from a no longer valid RenderFrameHost.
    std::move(callback).Run(
        ChromeFileSystemAccessPermissionContext::SensitiveEntryResult::kAbort);
    return;
  }

  content::WebContents* web_contents =
      content::WebContents::FromRenderFrameHost(rfh);
  if (!web_contents) {
    // Requested from a worker, or a no longer existing tab.
    std::move(callback).Run(
        ChromeFileSystemAccessPermissionContext::SensitiveEntryResult::kAbort);
    return;
  }

  ShowFileSystemAccessRestrictedDirectoryDialog(
      origin, handle_type, std::move(callback), web_contents);
}

#if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
void ShowFileSystemAccessDangerousFileDialogOnUIThread(
    content::GlobalRenderFrameHostId frame_id,
    const url::Origin& origin,
    const content::PathInfo& path_info,
    base::OnceCallback<
        void(ChromeFileSystemAccessPermissionContext::SensitiveEntryResult)>
        callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  content::RenderFrameHost* rfh = content::RenderFrameHost::FromID(frame_id);
  if (!rfh || !rfh->IsActive()) {
    // Requested from a no longer valid RenderFrameHost.
    std::move(callback).Run(
        ChromeFileSystemAccessPermissionContext::SensitiveEntryResult::kAbort);
    return;
  }

  content::WebContents* web_contents =
      content::WebContents::FromRenderFrameHost(rfh);
  if (!web_contents) {
    // Requested from a worker, or a no longer existing tab.
    std::move(callback).Run(
        ChromeFileSystemAccessPermissionContext::SensitiveEntryResult::kAbort);
    return;
  }

  ShowFileSystemAccessDangerousFileDialog(origin, path_info,
                                          std::move(callback), web_contents);
}
#endif

#if BUILDFLAG(IS_WIN)
bool ContainsInvalidDNSCharacter(base::FilePath::StringType hostname) {
  for (base::FilePath::CharType c : hostname) {
    if (!((c >= L'A' && c <= L'Z') || (c >= L'a' && c <= L'z') ||
          (c >= L'0' && c <= L'9') || (c == L'.') || (c == L'-') ||
          (c == L'_'))) {
      return true;
    }
  }
  return false;
}

bool MaybeIsLocalUNCPath(const base::FilePath& path) {
  if (!path.IsNetwork()) {
    return false;
  }

  const std::vector<base::FilePath::StringType> components =
      path.GetComponents();

  // Check for server name that could represent a local system. We only
  // check for a very short list, as it is impossible to cover all different
  // variants on Windows.
  if (components.size() >= 2 &&
      (base::FilePath::CompareEqualIgnoreCase(components[1],
                                              FILE_PATH_LITERAL("localhost")) ||
       components[1] == FILE_PATH_LITERAL("127.0.0.1") ||
       components[1] == FILE_PATH_LITERAL(".") ||
       components[1] == FILE_PATH_LITERAL("?") ||
       ContainsInvalidDNSCharacter(components[1]))) {
    return true;
  }

  // Check *admin* shares only (drive admin like "C$" and named admin).
  // Note: the share component is typically components[2], but we scan all
  // components defensively in case the structure changes.
  for (size_t i = 2; i < components.size(); ++i) {
    const auto& component = components[i];

    // component ends with "$"
    if (!component.empty() && component.back() == L'$') {
      // Drive admin share: "C$".."Z$" (case-insensitive on the letter).
      if (component.size() == 2 &&
          ((component[0] >= L'A' && component[0] <= L'Z') ||
           (component[0] >= L'a' && component[0] <= L'z'))) {
        return true;
      }

      // Named admin shares: "ADMIN$", "IPC$", "PRINT$", and "FAX$"
      if (base::FilePath::CompareEqualIgnoreCase(component,
                                                 FILE_PATH_LITERAL("ADMIN$")) ||
          base::FilePath::CompareEqualIgnoreCase(component,
                                                 FILE_PATH_LITERAL("IPC$")) ||
          base::FilePath::CompareEqualIgnoreCase(component,
                                                 FILE_PATH_LITERAL("PRINT$")) ||
          base::FilePath::CompareEqualIgnoreCase(component,
                                                 FILE_PATH_LITERAL("FAX$"))) {
        return true;
      }

      // Otherwise, it is just a hidden share (e.g. "Share$")—do not block.
    }
  }

  return false;
}
#endif

// A wrapper around `base::NormalizeFilePath` that returns its result instead of
// using an out parameter.
base::FilePath NormalizeFilePath(const base::FilePath& path) {
  base::FilePath absolute_path = path;
  if (!absolute_path.IsAbsolute()) {
    absolute_path = base::MakeAbsoluteFilePath(absolute_path);
  }

  if (absolute_path.empty()) {
    return absolute_path;
  }

  // TODO(crbug.com/368130513O): On Windows, this call will fail if the target
  // file path is greater than MAX_PATH. We should decide how to handle this
  // scenario.
  // If the path is invalid, the `base::NormalizeFilePath` will also return
  // false, so we return the empty path.
  base::FilePath normalized_path;
  if (!base::NormalizeFilePath(absolute_path, &normalized_path)) {
    return absolute_path;
  }
  CHECK_EQ(path.empty(), normalized_path.empty());
  return normalized_path;
}

using BlockType = ChromeFileSystemAccessPermissionContext::BlockType;

std::unique_ptr<ChromeFileSystemAccessPermissionContext::BlockPathRules>
GenerateBlockPaths(bool should_normalize_file_path) {
  using BlockPath = ChromeFileSystemAccessPermissionContext::BlockPath;
  static constexpr BlockPath kBlockPaths[] = {
      // Don't allow users to share their entire home directory, entire desktop
      // or entire documents folder, but do allow sharing anything inside those
      // directories not otherwise blocked.
      BlockPath::CreateRelative(base::DIR_HOME, BlockType::kDontBlockChildren),
      BlockPath::CreateRelative(base::DIR_USER_DESKTOP,
                                BlockType::kDontBlockChildren),
      BlockPath::CreateRelative(chrome::DIR_USER_DOCUMENTS,
                                BlockType::kDontBlockChildren),
      // Similar restrictions for the downloads directory.
      BlockPath::CreateRelative(chrome::DIR_DEFAULT_DOWNLOADS,
                                BlockType::kDontBlockChildren),
      BlockPath::CreateRelative(chrome::DIR_DEFAULT_DOWNLOADS_SAFE,
                                BlockType::kDontBlockChildren),
      // The Chrome installation itself should not be modified by the web.
      BlockPath::CreateRelative(base::DIR_EXE, BlockType::kBlockAllChildren),
      BlockPath::CreateRelative(base::DIR_MODULE, BlockType::kBlockAllChildren),
      BlockPath::CreateRelative(base::DIR_ASSETS, BlockType::kBlockAllChildren),
      // And neither should the configuration of at least the currently running
      // Chrome instance (note that this does not take --user-data-dir command
      // line overrides into account).
      BlockPath::CreateRelative(chrome::DIR_USER_DATA,
                                BlockType::kBlockAllChildren),
      // ~/.ssh is pretty sensitive on all platforms, so block access to that.
      BlockPath::CreateRelative(base::DIR_HOME, FILE_PATH_LITERAL(".ssh"),
                                BlockType::kBlockAllChildren),
      // And limit access to ~/.gnupg as well.
      BlockPath::CreateRelative(base::DIR_HOME, FILE_PATH_LITERAL(".gnupg"),
                                BlockType::kBlockAllChildren),
#if BUILDFLAG(IS_WIN)
      // Some Windows specific directories to block, basically all apps, the
      // operating system itself, as well as configuration data for apps.
      BlockPath::CreateRelative(base::DIR_PROGRAM_FILES,
                                BlockType::kBlockAllChildren),
      BlockPath::CreateRelative(base::DIR_PROGRAM_FILESX86,
                                BlockType::kBlockAllChildren),
      BlockPath::CreateRelative(base::DIR_PROGRAM_FILES6432,
                                BlockType::kBlockAllChildren),
      BlockPath::CreateRelative(base::DIR_WINDOWS,
                                BlockType::kBlockAllChildren),
      BlockPath::CreateRelative(base::DIR_ROAMING_APP_DATA,
                                BlockType::kBlockAllChildren),
      BlockPath::CreateRelative(base::DIR_LOCAL_APP_DATA,
                                BlockType::kBlockAllChildren),
      BlockPath::CreateRelative(base::DIR_COMMON_APP_DATA,
                                BlockType::kBlockAllChildren),
      // Opening a file from an MTP device, such as a smartphone or a camera, is
      // implemented by Windows as opening a file in the temporary internet
      // files directory. To support that, allow opening files in that
      // directory, but not whole directories.
      BlockPath::CreateRelative(base::DIR_IE_INTERNET_CACHE,
                                BlockType::kBlockNestedDirectories),
      // Block */.git/hooks on Windows, see crbug.com/465668234.
      BlockPath::CreateSuffix(FILE_PATH_LITERAL(".git/hooks"),
                              BlockType::kBlockWrite),
#endif
#if BUILDFLAG(IS_MAC)
      // Similar Mac specific blocks.
      BlockPath::CreateRelative(base::DIR_APP_DATA,
                                BlockType::kBlockAllChildren),
      // Block access to the current bundle directory.
      BlockPath::CreateRelative(chrome::DIR_OUTER_BUNDLE,
                                BlockType::kBlockAllChildren),
      // Block access to the user's Applications directory.
      BlockPath::CreateRelative(base::DIR_HOME,
                                FILE_PATH_LITERAL("Applications"),
                                BlockType::kBlockAllChildren),
      // Block access to the root Applications directory.
      BlockPath::CreateAbsolute(FILE_PATH_LITERAL("/Applications"),
                                BlockType::kBlockAllChildren),
      BlockPath::CreateRelative(base::DIR_HOME, FILE_PATH_LITERAL("Library"),
                                BlockType::kBlockAllChildren),
      // Allow access to other cloud files, such as Google Drive.
      BlockPath::CreateRelative(base::DIR_HOME,
                                FILE_PATH_LITERAL("Library/CloudStorage"),
                                BlockType::kDontBlockChildren),
      // Allow the site to interact with data from its corresponding natively
      // installed (sandboxed) application. It would be nice to limit a site to
      // access only _its_ corresponding natively installed application, but
      // unfortunately there's no straightforward way to do that. See
      // https://crbug.com/40095723#comment23.
      BlockPath::CreateRelative(base::DIR_HOME,
                                FILE_PATH_LITERAL("Library/Containers"),
                                BlockType::kDontBlockChildren),
      // Allow access to iCloud files...
      BlockPath::CreateRelative(base::DIR_HOME,
                                FILE_PATH_LITERAL("Library/Mobile Documents"),
                                BlockType::kDontBlockChildren),
      // ... which may also appear at this directory.
      BlockPath::CreateRelative(
          base::DIR_HOME,
          FILE_PATH_LITERAL("Library/Mobile Documents/com~apple~CloudDocs"),
          BlockType::kDontBlockChildren),
#endif
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_ANDROID)
      // On Linux also block access to devices via /dev.
      BlockPath::CreateAbsolute(FILE_PATH_LITERAL("/dev"),
                                BlockType::kBlockAllChildren),
      // And security sensitive data in /proc and /sys.
      BlockPath::CreateAbsolute(FILE_PATH_LITERAL("/proc"),
                                BlockType::kBlockAllChildren),
      BlockPath::CreateAbsolute(FILE_PATH_LITERAL("/sys"),
                                BlockType::kBlockAllChildren),
      // And system files in /boot and /etc.
      BlockPath::CreateAbsolute(FILE_PATH_LITERAL("/boot"),
                                BlockType::kBlockAllChildren),
      BlockPath::CreateAbsolute(FILE_PATH_LITERAL("/etc"),
                                BlockType::kBlockAllChildren),
      // And block all of ~/.config, matching the similar restrictions on mac
      // and windows.
      BlockPath::CreateRelative(base::DIR_HOME, FILE_PATH_LITERAL(".config"),
                                BlockType::kBlockAllChildren),
      // Block ~/.dbus as well, just in case, although there probably isn't much
      // a website can do with access to that directory and its contents.
      BlockPath::CreateRelative(base::DIR_HOME, FILE_PATH_LITERAL(".dbus"),
                                BlockType::kBlockAllChildren),
      // And block all of ~/.cache, matching the similar restrictions on mac
      // and windows.
      BlockPath::CreateRelative(base::DIR_CACHE, BlockType::kBlockAllChildren),
#endif
#if BUILDFLAG(IS_ANDROID)
      BlockPath::CreateRelative(base::DIR_ANDROID_APP_DATA,
                                BlockType::kBlockAllChildren),
#endif
      // TODO(crbug.com/40095723): Refine this list, for example add
      // XDG_CONFIG_HOME when it is not set ~/.config?
  };

  // ChromeOS supports multi-user sign-in. base::DIR_HOME only returns the
  // profile path for the primary user, the first user to sign in. We want to
  // use the `profile_path` instead since that's associated with user that
  // initiated this blocklist check.
  //
  // TODO(crbug.com/375490221): Improve the ChromeOS blocklist logic.
  constexpr bool kUseProfilePathForDirHome = BUILDFLAG(IS_CHROMEOS);
  // Populate the hard-coded rules.
  auto block_path_rules = std::make_unique<
      ChromeFileSystemAccessPermissionContext::BlockPathRules>();

  for (const auto& blocked_path : kBlockPaths) {
    base::FilePath path;
    switch (blocked_path.block_path_type) {
      case ChromeFileSystemAccessPermissionContext::BlockPathType::kAbsolute: {
        CHECK(blocked_path.path);
        path = base::FilePath(blocked_path.path);
        break;
      }
      case ChromeFileSystemAccessPermissionContext::BlockPathType::kRelative: {
        CHECK(blocked_path.base_path_key);
        if (kUseProfilePathForDirHome &&
            blocked_path.base_path_key == base::DIR_HOME) {
          block_path_rules->profile_based_block_path_rules_.emplace_back(
              blocked_path.path, blocked_path.block_type);
          continue;
        }

        if (!base::PathService::Get(blocked_path.base_path_key.value(),
                                    &path)) {
          continue;
        }

        if (blocked_path.path) {
          path = path.Append(blocked_path.path);
        }
        break;
      }
      case ChromeFileSystemAccessPermissionContext::BlockPathType::kSuffix: {
        block_path_rules->suffix_block_path_rules_.emplace_back(
            blocked_path.path, blocked_path.block_type);
        continue;
      }
    }

    block_path_rules->block_path_rules_.emplace_back(
        should_normalize_file_path ? NormalizeFilePath(path) : path,
        blocked_path.block_type);
  }

  return block_path_rules;
}

// Checks if `path` should be blocked by the `rules`.
// The BlockType of the nearest ancestor of a path to check is what
// ultimately determines if a path is blocked or not. If a blocked path is a
// descendent of another blocked path, then it may override the
// child-blocking policy of its ancestor. For example, if /home blocks all
// children, but /home/downloads does not, then /home/downloads/file.ext
// will *not* be blocked.
bool ShouldBlockAccessToPath(
    bool should_normalize_file_path,
    base::FilePath path,
    HandleType handle_type,
    UserAction user_action,
    std::vector<ChromeFileSystemAccessPermissionContext::BlockPathRule>
        extra_rules,
    ChromeFileSystemAccessPermissionContext::BlockPathRules block_path_rules,
    base::FilePath profile_path) {
  DCHECK(!path.empty());
  DCHECK(path.IsAbsolute());

  if (should_normalize_file_path) {
    path = NormalizeFilePath(path);
    profile_path = NormalizeFilePath(profile_path);
    for (auto& rule : extra_rules) {
      rule.path = NormalizeFilePath(rule.path);
    }
  }

#if BUILDFLAG(IS_WIN)
  // On Windows, local UNC paths are rejected, as UNC path can be written in a
  // way that can bypass the blocklist.
  if (MaybeIsLocalUNCPath(path)) {
    return true;
  }
#endif

  base::FilePath nearest_ancestor;
  BlockType nearest_ancestor_block_type = BlockType::kDontBlockChildren;
  auto should_block_with_rule = [&](const base::FilePath& block_path,
                                    BlockType block_type) -> bool {
    if (block_type == BlockType::kBlockWrite &&
        user_action != UserAction::kSave) {
      return false;
    }

    if (path == block_path || path.IsParent(block_path)) {
      LOG(ERROR) << "Blocking access to " << path
                 << " because it is a parent of " << block_path;
      return true;
    }

    if (block_path.IsParent(path) &&
        (nearest_ancestor.empty() || nearest_ancestor.IsParent(block_path))) {
      nearest_ancestor = block_path;
      nearest_ancestor_block_type = block_type;
    }
    return false;
  };

  for (const auto* block_rules_ptr :
       {&extra_rules, &block_path_rules.block_path_rules_}) {
    for (const auto& block : *block_rules_ptr) {
      if (should_block_with_rule(block.path, block.type)) {
        return true;
      }
    }
  }

  for (const auto& rule : block_path_rules.profile_based_block_path_rules_) {
    if (should_block_with_rule(
            rule.path ? profile_path.Append(rule.path) : profile_path,
            rule.type)) {
      return true;
    }
  }

  std::vector<base::FilePath::StringType> path_components =
      path.GetComponents();

  // Checks if the path components contain the components of the suffix rule.
  // For example, if the rule is `.git/hooks`, it will block paths like
  // `/foo/bar/.git/hooks`. The `std::search` identifies the matching subrange
  // and constructs a `current_path` from the root up to the end of the matched
  // subrange (e.g., `/foo/bar/.git/hooks`). This path is then evaluated against
  // the regular block rules.
  for (const auto& rule : block_path_rules.suffix_block_path_rules_) {
    base::FilePath rule_path(rule.path);
    std::vector<base::FilePath::StringType> rule_components =
        rule_path.GetComponents();
    if (rule_components.empty()) {
      continue;
    }

    auto it = path_components.begin();
    while (true) {
      it = std::search(it, path_components.end(), rule_components.begin(),
                       rule_components.end());
      if (it == path_components.end()) {
        break;
      }

      base::FilePath current_path = base::FilePath(path_components[0]);
      for (auto path_it = path_components.begin() + 1;
           path_it != it + rule_components.size(); ++path_it) {
        current_path = current_path.Append(*path_it);
      }

      if (should_block_with_rule(current_path, rule.type)) {
        return true;
      }

      ++it;
    }
  }

  // The path we're checking is not in a potentially blocked directory, or the
  // nearest ancestor does not block access to its children. Grant access.
  if (nearest_ancestor.empty() ||
      nearest_ancestor_block_type == BlockType::kDontBlockChildren) {
    VLOG(1) << "Not blocking access to " << path << " because it is inside "
            << nearest_ancestor << " and it's kDontBlockChildren";
    return false;
  }

  // The path we're checking is a file, and the nearest ancestor only blocks
  // access to directories. Grant access.
  if (handle_type == HandleType::kFile &&
      nearest_ancestor_block_type == BlockType::kBlockNestedDirectories) {
    VLOG(1) << "Not blocking access to " << path << " because it is inside "
            << nearest_ancestor << " and it's kBlockNestedDirectories";
    return false;
  }

  // The nearest ancestor blocks access to its children, so block access.
  VLOG(1) << "Blocking access to " << path << " because it is inside "
          << nearest_ancestor << " and it's kBlockAllChildren";
  return true;
}

// Returns true if `child_path` is the same as or a descendant of
// `parent_path`, ignoring case differences. Unlike
// `base::FilePath::IsParent()`, this handles case-variant paths returned by
// native pickers on case-insensitive filesystems.
bool IsPathOrDescendantIgnoreCase(
    const base::FilePath& parent_path,
    const std::vector<base::FilePath::StringType>& parent_components,
    const base::FilePath& child_path) {
  // Fast path: Exact match or case-sensitive parent match.
  if (child_path == parent_path || parent_path.IsParent(child_path)) {
    return true;
  }

  const std::vector<base::FilePath::StringType> child_components =
      child_path.GetComponents();
  if (parent_components.empty() ||
      parent_components.size() > child_components.size()) {
    return false;
  }

  return std::equal(parent_components.begin(), parent_components.end(),
                    child_components.begin(),
                    base::FilePath::CompareEqualIgnoreCase);
}

#if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
void DoSafeBrowsingCheckOnUIThread(
    content::GlobalRenderFrameHostId frame_id,
    std::unique_ptr<content::FileSystemAccessWriteItem> item,
    safe_browsing::CheckDownloadCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
#if BUILDFLAG(SAFE_BROWSING_DOWNLOAD_PROTECTION)
  safe_browsing::SafeBrowsingService* sb_service =
      g_browser_process->safe_browsing_service();
  if (!sb_service || !sb_service->download_protection_service() ||
      !sb_service->download_protection_service()->enabled()) {
    std::move(callback).Run(safe_browsing::DownloadCheckResult::UNKNOWN);
    return;
  }

  if (!item->browser_context) {
    content::RenderProcessHost* rph =
        content::RenderProcessHost::FromID(frame_id.child_id);
    if (!rph) {
      std::move(callback).Run(safe_browsing::DownloadCheckResult::UNKNOWN);
      return;
    }
    item->browser_context = rph->GetBrowserContext();
  }

  if (!item->web_contents) {
    content::RenderFrameHost* rfh = content::RenderFrameHost::FromID(frame_id);
    if (rfh) {
      DCHECK_NE(rfh->GetLifecycleState(),
                content::RenderFrameHost::LifecycleState::kPrerendering);
      item->web_contents =
          content::WebContents::FromRenderFrameHost(rfh)->GetWeakPtr();
    }
  }

  sb_service->download_protection_service()->CheckFileSystemAccessWrite(
      std::move(item), std::move(callback));
#else
  std::move(callback).Run(safe_browsing::DownloadCheckResult::UNKNOWN);
#endif  // BUILDFLAG(SAFE_BROWSING_DOWNLOAD_PROTECTION)
}

ChromeFileSystemAccessPermissionContext::AfterWriteCheckResult
InterpretSafeBrowsingResult(safe_browsing::DownloadCheckResult result) {
  using Result = safe_browsing::DownloadCheckResult;
  switch (result) {
    // Only allow downloads that are marked as SAFE or UNKNOWN by SafeBrowsing.
    // All other types are going to be blocked. UNKNOWN could be the result of a
    // failed safe browsing ping or if Safe Browsing is not enabled.
    case Result::UNKNOWN:
    case Result::SAFE:
    case Result::ALLOWLISTED_BY_POLICY:
    case Result::SENSITIVE_CONTENT_WARNING:
    case Result::DEEP_SCANNED_SAFE:
      return ChromeFileSystemAccessPermissionContext::AfterWriteCheckResult::
          kAllow;

    case Result::DANGEROUS:
    case Result::UNCOMMON:
    case Result::DANGEROUS_HOST:
    case Result::POTENTIALLY_UNWANTED:
    case Result::BLOCKED_PASSWORD_PROTECTED:
    case Result::BLOCKED_TOO_LARGE:
    case Result::DANGEROUS_ACCOUNT_COMPROMISE:
    case Result::BLOCKED_SCAN_FAILED:
    case Result::SENSITIVE_CONTENT_BLOCK:
    case Result::FORCE_SAVE_TO_GDRIVE:
    case Result::FORCE_SAVE_TO_ONEDRIVE:
      return ChromeFileSystemAccessPermissionContext::AfterWriteCheckResult::
          kBlock;

    // This shouldn't be returned for File System Access write checks.
    case Result::ASYNC_SCANNING:
    case Result::ASYNC_LOCAL_PASSWORD_SCANNING:
    case Result::PROMPT_FOR_SCANNING:
    case Result::PROMPT_FOR_LOCAL_PASSWORD_SCANNING:
    case Result::DEEP_SCANNED_FAILED:
    case Result::IMMEDIATE_DEEP_SCAN:
      NOTREACHED();
  }
  NOTREACHED();
}
#endif  // BUILDFLAG(SAFE_BROWSING_AVAILABLE)

std::string GenerateLastPickedDirectoryKey(const std::string& id) {
  return id.empty() ? kDefaultLastPickedDirectoryKey
                    : base::StrCat({kCustomLastPickedDirectoryKey, "-", id});
}

std::string_view PathAsPermissionKey(const base::FilePath& path) {
  return std::string_view(
      reinterpret_cast<const char*>(path.value().data()),
      path.value().size() * sizeof(base::FilePath::CharType));
}

std::string_view GetGrantKeyFromGrantType(GrantType type) {
  return type == GrantType::kWrite ? kPermissionWritableKey
                                   : kPermissionReadableKey;
}

#if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
safe_browsing::DownloadFileType::DangerLevel GetFileTypeDangerLevel(
    const base::FilePath& path) {
  // Passing an empty source URL and null prefs ensures the result reflects
  // only the configured danger level for the file type, without applying any
  // download-specific overrides.
  return safe_browsing::FileTypePolicies::GetInstance()->GetFileDangerLevel(
      path, GURL(), /*prefs=*/nullptr);
}
#endif

std::string StringOrEmpty(const std::string* s) {
  return s ? *s : std::string();
}

bool PathInfosContains(const std::vector<content::PathInfo>& path_infos,
                       const base::FilePath& path) {
  return std::ranges::any_of(path_infos,
                             [&path](const content::PathInfo& path_info) {
                               return path_info.path == path;
                             });
}

}  // namespace

ChromeFileSystemAccessPermissionContext::Grants::Grants() = default;
ChromeFileSystemAccessPermissionContext::Grants::~Grants() = default;
ChromeFileSystemAccessPermissionContext::Grants::Grants(Grants&&) = default;
ChromeFileSystemAccessPermissionContext::Grants&
ChromeFileSystemAccessPermissionContext::Grants::operator=(Grants&&) = default;

class ChromeFileSystemAccessPermissionContext::PermissionGrantImpl
    : public content::FileSystemAccessPermissionGrant {
 public:
  PermissionGrantImpl(
      base::WeakPtr<ChromeFileSystemAccessPermissionContext> context,
      const url::Origin& origin,
      const content::PathInfo& path_info,
      HandleType handle_type,
      GrantType type,
      UserAction user_action)
      : context_(std::move(context)),
        origin_(origin),
        handle_type_(handle_type),
        type_(type),
        path_info_(path_info),
        user_action_(user_action) {}

  // FileSystemAccessPermissionGrant:
  PermissionStatus GetStatus() override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    // TODO(crbug.com/40101962): Determine if this should return denied for
    // guard block, and how ancestor permission should be handled.
    if (status_ == PermissionStatus::ASK &&
        context_->CanAutoGrantViaPersistentPermission(origin_, path_info_.path,
                                                      handle_type_, type_)) {
      return PermissionStatus::GRANTED;
    }
    return status_;
  }

  PermissionStatus GetActivePermissionStatus() const {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    return status_;
  }

  base::FilePath GetPath() override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    return path_info_.path;
  }

  std::string GetDisplayName() override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    return path_info_.display_name;
  }

  void RequestPermission(
      content::GlobalRenderFrameHostId frame_id,
      UserActivationState user_activation_state,
      base::OnceCallback<void(PermissionRequestOutcome)> callback) override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    // Check if a permission request has already been processed previously. This
    // check is done first because we don't want to reset the status of a
    // permission if it has already been granted.
    if (GetActivePermissionStatus() != PermissionStatus::ASK || !context_) {
      if (GetActivePermissionStatus() == PermissionStatus::GRANTED) {
        SetStatus(PermissionStatus::GRANTED,
                  PersistedPermissionOptions::kUpdatePersistedPermission);
      }
      std::move(callback).Run(PermissionRequestOutcome::kRequestAborted);
      return;
    }

    if (type_ == GrantType::kWrite) {
      ContentSetting content_setting =
          context_->GetWriteGuardContentSetting(origin_);

      // Content setting grants write permission without asking.
      if (content_setting == CONTENT_SETTING_ALLOW) {
        PermissionRequestOutcome outcome =
            PermissionRequestOutcome::kGrantedByContentSetting;
        RecordPermissionRequestOutcome(outcome);
        // May destroy `this`.
        SetStatus(PermissionStatus::GRANTED,
                  PersistedPermissionOptions::kDoNotUpdatePersistedPermission);
        std::move(callback).Run(outcome);
        return;
      }

      // Content setting blocks write permission.
      if (content_setting == CONTENT_SETTING_BLOCK) {
        PermissionRequestOutcome outcome =
            PermissionRequestOutcome::kBlockedByContentSetting;
        RecordPermissionRequestOutcome(outcome);
        // May destroy `this`.
        SetStatus(PermissionStatus::DENIED,
                  PersistedPermissionOptions::kDoNotUpdatePersistedPermission);
        std::move(callback).Run(outcome);
        return;
      }
    }

    if (context_->CanAutoGrantViaPersistentPermission(origin_, path_info_.path,
                                                      handle_type_, type_)) {
      PermissionRequestOutcome outcome =
          PermissionRequestOutcome::kGrantedByPersistentPermission;
      RecordPermissionRequestOutcome(outcome);
      // May destroy `this`.
      SetStatus(PermissionStatus::GRANTED,
                PersistedPermissionOptions::kUpdatePersistedPermission);
      std::move(callback).Run(outcome);
      return;
    }

    if (context_->CanAutoGrantViaAncestorPersistentPermission(
            origin_, path_info_.path, type_)) {
      PermissionRequestOutcome outcome =
          PermissionRequestOutcome::kGrantedByAncestorPersistentPermission;
      RecordPermissionRequestOutcome(outcome);
      // May destroy `this`.
      SetStatus(PermissionStatus::GRANTED,
                PersistedPermissionOptions::kUpdatePersistedPermission);
      std::move(callback).Run(outcome);
      return;
    }

    // Otherwise, perform checks and ask the user for permission.

    content::RenderFrameHost* rfh = content::RenderFrameHost::FromID(frame_id);
    if (!rfh) {
      // Requested from a no longer valid RenderFrameHost.
      RunCallbackAndRecordPermissionRequestOutcome(
          std::move(callback), PermissionRequestOutcome::kInvalidFrame);
      return;
    }

    // Don't show request permission UI for an inactive RenderFrameHost as the
    // page might not distinguish properly between user denying the permission
    // and automatic rejection, leading to an inconsistent UX once the page
    // becomes active again.
    // - If this is called when RenderFrameHost is in BackForwardCache, evict
    //   the document from the cache.
    // - If this is called when RenderFrameHost is in prerendering, cancel
    //   prerendering.
    if (rfh->IsInactiveAndDisallowActivation(
            content::DisallowActivationReasonId::
                kFileSystemAccessPermissionRequest)) {
      RunCallbackAndRecordPermissionRequestOutcome(
          std::move(callback), PermissionRequestOutcome::kInvalidFrame);
      return;
    }
    // We don't allow file system access from fenced frames.
    if (rfh->IsNestedWithinFencedFrame()) {
      RunCallbackAndRecordPermissionRequestOutcome(
          std::move(callback), PermissionRequestOutcome::kInvalidFrame);
      return;
    }

    if (user_activation_state == UserActivationState::kRequired &&
        !rfh->HasTransientUserActivation()) {
      // No permission prompts without user activation.
      RunCallbackAndRecordPermissionRequestOutcome(
          std::move(callback), PermissionRequestOutcome::kNoUserActivation);
      return;
    }

    content::WebContents* web_contents =
        content::WebContents::FromRenderFrameHost(rfh);
    if (!web_contents) {
      // Requested from a worker, or a no longer existing tab.
      RunCallbackAndRecordPermissionRequestOutcome(
          std::move(callback), PermissionRequestOutcome::kInvalidFrame);
      return;
    }

    url::Origin embedding_origin = url::Origin::Create(
        permissions::PermissionUtil::GetLastCommittedOriginAsURL(
            rfh->GetMainFrame()));
    if (embedding_origin != origin_) {
      // Third party iframes are not allowed to request more permissions.
      RunCallbackAndRecordPermissionRequestOutcome(
          std::move(callback), PermissionRequestOutcome::kThirdPartyContext);
      return;
    }

#if BUILDFLAG(ENABLE_EXTENSIONS_CORE) && BUILDFLAG(ENABLE_GUEST_VIEW)
    // A permission request from a webview is normally delegated to its embedder
    // without showing a prompt. However, filesystem permissions are known to be
    // broken:
    // TODO(crbug.com/352520731): Remove this once the bug is fixed.
    // Until that's fixed, we auto-grant for WebUI embedders to enable use cases
    // that need this capability: crbug.com/391586357.
    if (auto* guest = extensions::WebViewGuest::FromRenderFrameHost(rfh);
        guest != nullptr && guest->IsOwnedByWebUI()) {
      PermissionRequestOutcome outcome =
          PermissionRequestOutcome::kGrantedByAncestorPersistentPermission;
      RecordPermissionRequestOutcome(outcome);
      // May destroy `this`.
      SetStatus(PermissionStatus::GRANTED,
                PersistedPermissionOptions::kDoNotUpdatePersistedPermission);
      std::move(callback).Run(outcome);
      return;
    }
#endif  // BUILDFLAG(ENABLE_EXTENSIONS_CORE) && BUILDFLAG(ENABLE_GUEST_VIEW)

    auto* request_manager =
        FileSystemAccessPermissionRequestManager::FromWebContents(web_contents);
    if (!request_manager) {
      // Extension contexts (popup, side panel) may not have a permission
      // request manager attached. Since the user already explicitly selected a
      // file/folder via the file picker dialog (which is a strong user
      // gesture), we can auto-grant the permission for extensions without
      // showing an additional prompt.
      bool is_extension =
          rfh->GetLastCommittedOrigin().scheme() == "chrome-extension";
      if (is_extension) {
        PermissionRequestOutcome outcome =
            PermissionRequestOutcome::kUserGranted;
        RecordPermissionRequestOutcome(outcome);
        // May destroy `this`.
        SetStatus(PermissionStatus::GRANTED,
                  PersistedPermissionOptions::kUpdatePersistedPermission);
        std::move(callback).Run(outcome);
        return;
      }

      RunCallbackAndRecordPermissionRequestOutcome(
          std::move(callback), PermissionRequestOutcome::kRequestAborted);
      return;
    }

    // Drop fullscreen mode so that the user sees the URL bar.
    auto blocker = web_contents->ForSecurityDropFullscreen(
        /*display_id=*/display::kInvalidDisplayId);
    if (!blocker) {
      RunCallbackAndRecordPermissionRequestOutcome(
          std::move(callback), PermissionRequestOutcome::kRequestAborted);
      return;
    }

    if (context_->IsEligibleToUpgradePermissionRequestToRestorePrompt(
            origin_, path_info_.path, handle_type_, user_action_, type_)) {
      std::vector<FileRequestData> request_data_list =
          context_->GetFileRequestDataForRestorePermissionPrompt(origin_);
      request_manager->AddRequest(
          {FileSystemAccessPermissionRequestManager::RequestType::
               kRestorePermissions,
           origin_, request_data_list},
          base::BindOnce(&PermissionGrantImpl::OnRestorePermissionRequestResult,
                         this, std::move(callback)),
          std::move(*blocker));
      return;
    }

    // If a website wants both read and write access, code in content will
    // request those as two separate requests. The |request_manager| will then
    // detect this and combine the two requests into one prompt. As such this
    // code does not have to have any way to request Access::kReadWrite.
    FileRequestData file_request_data = {path_info_, handle_type_,
                                         type_ == GrantType::kRead
                                             ? RequestAccess::kRead
                                             : RequestAccess::kWrite};
    request_manager->AddRequest(
        {FileSystemAccessPermissionRequestManager::RequestType::kNewPermission,
         origin_,
         {file_request_data}},
        base::BindOnce(&PermissionGrantImpl::OnPermissionRequestResult, this,
                       std::move(callback)),
        std::move(*blocker));
  }

  const url::Origin& origin() const {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    return origin_;
  }

  HandleType handle_type() const {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    return handle_type_;
  }

  GrantType type() const {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    return type_;
  }

  // `this` may be destroyed. A `FileSystemAccessPermissionGrant::Observer` may
  // destroy `this` when notified of this the status change.
  void SetStatus(PermissionStatus new_status,
                 PersistedPermissionOptions update_options) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    auto permission_changed = status_ != new_status;
    status_ = new_status;

    if (context_ &&
        update_options ==
            PersistedPermissionOptions::kUpdatePersistedPermission &&
        base::FeatureList::IsEnabled(
            features::kFileSystemAccessPersistentPermissions)) {
      const std::unique_ptr<Object> object = context_->GetGrantedObject(
          origin_, PathAsPermissionKey(path_info_.path));
      auto opposite_type =
          type_ == GrantType::kRead ? GrantType::kWrite : GrantType::kRead;
      if (new_status == PermissionStatus::GRANTED) {
        if (object) {
          // Persisted permissions include both read and write information in
          // one object. Figure out if the other grant type is already
          // persisted and update the existing one.
          auto type_exists =
              object->value.FindBool(GetGrantKeyFromGrantType(type_))
                  .value_or(false);
          auto opposite_type_exists =
              object->value.FindBool(GetGrantKeyFromGrantType(opposite_type))
                  .value_or(false);
          if (!type_exists && opposite_type_exists) {
            base::DictValue new_object = object->value.Clone();
            new_object.Set(GetGrantKeyFromGrantType(type_), true);
            context_->UpdateObjectPermission(origin_, object->value,
                                             std::move(new_object));
          }
        } else {
          base::DictValue grant = AsValue();
          context_->GrantObjectPermission(origin_, std::move(grant));
        }
        // Update visibility of icon when permission status is granted.
        context_->ScheduleUsageIconUpdate();
      } else if (object) {
        // Permission is not granted anymore. Remove the grant object entirely
        // if only this grant type exists in the grant object; otherwise, remove
        // the grant type key from the grant object.
        auto type_exists =
            object->value.FindBool(GetGrantKeyFromGrantType(type_))
                .value_or(false);
        auto opposite_type_exists =
            object->value.FindBool(GetGrantKeyFromGrantType(opposite_type))
                .value_or(false);
        if (type_exists) {
          if (opposite_type_exists) {
            base::DictValue new_object = object->value.Clone();
            new_object.Remove(GetGrantKeyFromGrantType(type_));
            context_->UpdateObjectPermission(origin_, object->value,
                                             std::move(new_object));
          } else {
            context_->RevokeObjectPermission(origin_, GetKey());
          }
        }
      }
    }

    if (permission_changed) {
      // May destroy `this`.
      NotifyPermissionStatusChanged();
    }
  }

  base::DictValue AsValue() const {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    base::DictValue value;
    value.Set(kPermissionPathKey, base::FilePathToValue(path_info_.path));
    value.Set(kPermissionDisplayNameKey, path_info_.display_name);
    value.Set(kPermissionIsDirectoryKey,
              handle_type_ == HandleType::kDirectory);
    value.Set(GetGrantKeyFromGrantType(type_), true);
    return value;
  }

  // Updates the in-memory permission grant for the `new_path` in the `grants`
  // map using the same grant from the `old_path`, and removes the grant entry
  // for the `old_path`.
  // If `allow_overwrite` is true, this will replace any pre-existing grant at
  // `new_path`.
  static void UpdateGrantPath(
      std::map<base::FilePath, raw_ptr<PermissionGrantImpl, CtnExperimental>>&
          grants,
      const content::PathInfo& old_path,
      const content::PathInfo& new_path,
      bool allow_overwrite) {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    auto old_path_it =
        std::ranges::find_if(grants, [&old_path](const auto& entry) {
          return entry.first == old_path.path;
        });

    if (old_path_it == grants.end()) {
      // There must be an entry for an ancestor of this entry. Nothing to do
      // here.
      //
      // TODO(crbug.com/40245144): Consolidate superfluous child grants
      // to support directory moves.
      return;
    }

    auto* const grant_to_move = old_path_it->second.get();

    if (allow_overwrite) {
      // Check for a collision at the new path. If a different grant already
      // exists at the destination, its status must be set to DENIED before it
      // is replaced in the `grants` map.
      //
      // This prevents a DCHECK failure in `PermissionGrantDestroyed()` that can
      // occur depending on object destruction order. Consider this scenario:
      //   1. `grant1` (for `handle1`) exists for `path1`.
      //   2. `handle2` (with `grant2`) is moved to `path1`.
      //   3. The `grants` map entry for `path1` is updated to point to
      //   `grant2`,
      //      orphaning `grant1`. `grant1` is now untracked but still `GRANTED`.
      //   4. If `handle2` is destroyed first, the map entry for `path1` is
      //   removed.
      //   5. When `handle1` is later destroyed, `PermissionGrantDestroyed()` is
      //      called for `grant1`. It fails a DCHECK because the grant is not in
      //      the map and its status is `GRANTED` instead of the expected
      //      `DENIED`.
      //
      // By setting the orphaned grant's status to DENIED here, the DCHECK will
      // pass regardless of destruction order.
      auto new_path_it = grants.find(new_path.path);
      if (new_path_it != grants.end() &&
          new_path_it->second.get() != grant_to_move) {
        // A different grant exists at the destination. Revoke it before it gets
        // orphaned.
        new_path_it->second->SetStatus(
            PermissionStatus::DENIED,
            // Only update the in-memory permission, as the persistent
            // permission should be updated by the call site.
            PersistedPermissionOptions::kDoNotUpdatePersistedPermission);
      }
    }

    grant_to_move->SetPath(new_path);

    // `insert_or_assign` is used when overwriting is allowed, as it will
    // replace any grant that already exists at the destination path. `emplace`
    // is used otherwise to preserve the old behavior of not overwriting
    // existing grants.
    grants.erase(old_path_it);
    if (allow_overwrite) {
      grants.insert_or_assign(new_path.path, grant_to_move);
    } else {
      grants.emplace(new_path.path, grant_to_move);
    }
  }

  // Downgrades the in-memory read permission grant. This is different from
  // ChromeFileSystemAccessPermissionContext::RevokeGrant in that this method
  // does not reset the persisted permission state.
  void DowngradeActiveReadGrant() {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    if (GetActivePermissionStatus() != PermissionStatus::GRANTED) {
      return;
    }

    // Updates the in-memory status of the grant synchronously. This ensures
    // that any existing handle instances that hold a `scoped_refptr` to this
    // grant will immediately see the updated permission status.
    //
    // The status is set to `DENIED` instead of `ASK`. This is critical to
    // prevent a race condition. The race may occur in
    // `PermissionGrantImpl::GetStatus()`, which checks
    // `CanAutoGrantViaPersistentPermission()` if the in-memory status is
    // `ASK`. Because the on-disk persisted permission is updated
    // asynchronously after a `remove()`, a subsequent query for a new handle
    // (e.g., from IndexedDB) could read the stale on-disk state and
    // incorrectly return `GRANTED`.
    SetStatus(PermissionStatus::DENIED,
              PersistedPermissionOptions::kDoNotUpdatePersistedPermission);
  }

 protected:
  ~PermissionGrantImpl() override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    if (context_) {
      context_->PermissionGrantDestroyed(this);
    }
  }

 private:
  void OnPermissionRequestResult(
      base::OnceCallback<void(PermissionRequestOutcome)> callback,
      PermissionAction result) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    if (context_) {
      context_->UpdateGrantsOnPermissionRequestResult(origin_);
    }

    switch (result) {
      case PermissionAction::GRANTED: {
        PermissionRequestOutcome outcome =
            PermissionRequestOutcome::kUserGranted;
        RecordPermissionRequestOutcome(outcome);
        // May destroy `this`.
        SetStatus(PermissionStatus::GRANTED,
                  PersistedPermissionOptions::kUpdatePersistedPermission);
        std::move(callback).Run(outcome);
        break;
      }
      case PermissionAction::DENIED: {
        PermissionRequestOutcome outcome =
            PermissionRequestOutcome::kUserDenied;
        RecordPermissionRequestOutcome(outcome);
        // May destroy `this`.
        SetStatus(PermissionStatus::DENIED,
                  PersistedPermissionOptions::kUpdatePersistedPermission);
        std::move(callback).Run(outcome);
        break;
      }
      case PermissionAction::DISMISSED:
      case PermissionAction::IGNORED:
        RunCallbackAndRecordPermissionRequestOutcome(
            std::move(callback), PermissionRequestOutcome::kUserDismissed);
        break;
      case PermissionAction::REVOKED:
      case PermissionAction::GRANTED_ONCE:
      case PermissionAction::NUM:
        NOTREACHED();
    }
  }

  void OnRestorePermissionRequestResult(
      base::OnceCallback<void(PermissionRequestOutcome)> callback,
      PermissionAction result) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    if (!context_) {
      std::move(callback).Run(PermissionRequestOutcome::kRequestAborted);
      return;
    }

    // TODO(crbug.com/40101962): Consider adding more `PermissionRequestOutcome`
    // types to account for "restore every time" case and invalid state case.

    if (context_->GetPersistedGrantType(origin_) !=
        PersistedGrantType::kDormant) {
      // User may have enabled the extended permission, or the persisted grant
      // status is changed while the prompt is shown.
      std::move(callback).Run(PermissionRequestOutcome::kRequestAborted);
      return;
    }

    switch (result) {
      case PermissionAction::GRANTED:
        context_->OnRestorePermissionAllowedEveryTime(origin_);
        base::UmaHistogramEnumeration(
            "Storage.FileSystemAccess.RestorePermissionPromptOutcome",
            RestorePermissionPromptOutcome::kAllowed);
        RunCallbackAndRecordPermissionRequestOutcome(
            std::move(callback),
            PermissionRequestOutcome::kGrantedByRestorePrompt);
        break;
      case PermissionAction::GRANTED_ONCE:
        context_->OnRestorePermissionAllowedOnce(origin_);
        base::UmaHistogramEnumeration(
            "Storage.FileSystemAccess.RestorePermissionPromptOutcome",
            RestorePermissionPromptOutcome::kAllowedOnce);
        RunCallbackAndRecordPermissionRequestOutcome(
            std::move(callback),
            PermissionRequestOutcome::kGrantedByRestorePrompt);
        break;
      case PermissionAction::DENIED:
        context_->OnRestorePermissionDeniedOrDismissed(origin_);
        base::UmaHistogramEnumeration(
            "Storage.FileSystemAccess.RestorePermissionPromptOutcome",
            RestorePermissionPromptOutcome::kRejected);
        RunCallbackAndRecordPermissionRequestOutcome(
            std::move(callback), PermissionRequestOutcome::kUserDenied);
        break;
      case PermissionAction::DISMISSED:
        context_->OnRestorePermissionDeniedOrDismissed(origin_);
        base::UmaHistogramEnumeration(
            "Storage.FileSystemAccess.RestorePermissionPromptOutcome",
            RestorePermissionPromptOutcome::kDismissed);
        RunCallbackAndRecordPermissionRequestOutcome(
            std::move(callback), PermissionRequestOutcome::kUserDismissed);
        break;
      case PermissionAction::IGNORED:
        // TODO(crbug.com/40101962): This action is not user-detectable,
        // consider replacing `PermissionRequestOutcome` with a more
        // appropriate type.
        context_->OnRestorePermissionIgnored(origin_);
        base::UmaHistogramEnumeration(
            "Storage.FileSystemAccess.RestorePermissionPromptOutcome",
            RestorePermissionPromptOutcome::kIgnored);
        RunCallbackAndRecordPermissionRequestOutcome(
            std::move(callback), PermissionRequestOutcome::kRequestAborted);
        break;
      case PermissionAction::REVOKED:
      case PermissionAction::NUM:
        NOTREACHED();
    }
  }

  void RecordPermissionRequestOutcome(PermissionRequestOutcome outcome) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    if (context_ &&
        (outcome ==
             PermissionRequestOutcome::kGrantedByAncestorPersistentPermission ||
         outcome == PermissionRequestOutcome::kGrantedByPersistentPermission ||
         outcome == PermissionRequestOutcome::kGrantedByRestorePrompt)) {
      context_->ScheduleUsageIconUpdate();
    }
    if (type_ == GrantType::kWrite) {
      base::UmaHistogramEnumeration(
          "Storage.FileSystemAccess.WritePermissionRequestOutcome", outcome);
      if (handle_type_ == HandleType::kDirectory) {
        base::UmaHistogramEnumeration(
            "Storage.FileSystemAccess.WritePermissionRequestOutcome.Directory",
            outcome);
      } else {
        base::UmaHistogramEnumeration(
            "Storage.FileSystemAccess.WritePermissionRequestOutcome.File",
            outcome);
      }
    } else {
      base::UmaHistogramEnumeration(
          "Storage.FileSystemAccess.ReadPermissionRequestOutcome", outcome);
      if (handle_type_ == HandleType::kDirectory) {
        base::UmaHistogramEnumeration(
            "Storage.FileSystemAccess.ReadPermissionRequestOutcome.Directory",
            outcome);
      } else {
        base::UmaHistogramEnumeration(
            "Storage.FileSystemAccess.ReadPermissionRequestOutcome.File",
            outcome);
      }
    }
  }

  void RunCallbackAndRecordPermissionRequestOutcome(
      base::OnceCallback<void(PermissionRequestOutcome)> callback,
      PermissionRequestOutcome outcome) {
    RecordPermissionRequestOutcome(outcome);

    std::move(callback).Run(outcome);
  }

  std::string_view GetKey() const {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    return PathAsPermissionKey(path_info_.path);
  }

  void SetPath(const content::PathInfo& new_path) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    if (path_info_ == new_path) {
      return;
    }

    path_info_ = new_path;

    if (base::FeatureList::IsEnabled(
            features::kFileSystemAccessPersistentPermissions)) {
      const std::unique_ptr<Object> object = context_->GetGrantedObject(
          origin_, PathAsPermissionKey(path_info_.path));
      if (object) {
        base::DictValue new_object = object->value.Clone();
        new_object.Set(kPermissionPathKey,
                       base::FilePathToValue(new_path.path));
        new_object.Set(kPermissionDisplayNameKey, new_path.display_name);
        context_->UpdateObjectPermission(origin_, object->value,
                                         std::move(new_object));
      }
    }

    // May destroy `this`.
    NotifyPermissionStatusChanged();
  }

  SEQUENCE_CHECKER(sequence_checker_);

  base::WeakPtr<ChromeFileSystemAccessPermissionContext> const context_;
  const url::Origin origin_;
  const HandleType handle_type_;
  const GrantType type_;
  // `path_info_.path` can be updated if the entry is moved.
  content::PathInfo path_info_;
  const UserAction user_action_;

  // This member should only be updated via SetStatus(), to make sure
  // observers are properly notified about any change in status.
  PermissionStatus status_ = PermissionStatus::ASK;
};

struct ChromeFileSystemAccessPermissionContext::OriginState {
  // Raw pointers, owned collectively by all the handles that reference this
  // grant. When last reference goes away this state is cleared as well by
  // PermissionGrantDestroyed().
  std::map<base::FilePath, raw_ptr<PermissionGrantImpl, CtnExperimental>>
      read_grants;
  std::map<base::FilePath, raw_ptr<PermissionGrantImpl, CtnExperimental>>
      write_grants;

  // Stores paths whose read grants have been downgraded to ASK after a
  // remove() call and are eligible for restoration.
  std::set<base::FilePath> downgraded_read_paths;

  PersistedGrantStatus persisted_grant_status = PersistedGrantStatus::kLoaded;

  // Cached data about whether this origin has an actively installed web app.
  // This is used to determine the origin's extended permission eligibility.
  WebAppInstallStatus web_app_install_status = WebAppInstallStatus::kUnknown;

  // Timer that is triggered whenever the user navigates away from this origin.
  // This is used to give a website a little bit of time for background work
  // before revoking all permissions for the origin.
  std::unique_ptr<base::RetainingOneShotTimer> cleanup_timer;
};

ChromeFileSystemAccessPermissionContext::BlockPathRules::BlockPathRules() =
    default;
ChromeFileSystemAccessPermissionContext::BlockPathRules::~BlockPathRules() =
    default;
ChromeFileSystemAccessPermissionContext::BlockPathRules::BlockPathRules(
    const BlockPathRules& other) = default;
ChromeFileSystemAccessPermissionContext::BlockPathRules&
ChromeFileSystemAccessPermissionContext::BlockPathRules::operator=(
    const BlockPathRules& other) = default;

ChromeFileSystemAccessPermissionContext::
    ChromeFileSystemAccessPermissionContext(content::BrowserContext* context,
                                            const base::Clock* clock)
    : ObjectPermissionContextBase(
          ContentSettingsType::FILE_SYSTEM_WRITE_GUARD,
          ContentSettingsType::FILE_SYSTEM_ACCESS_CHOOSER_DATA,
          HostContentSettingsMapFactory::GetForProfile(context)),
      profile_(context),
      clock_(clock),
      should_normalize_file_path_(base::FeatureList::IsEnabled(
          features::kFileSystemAccessSymbolicLinkCheck)) {
  DETACH_FROM_SEQUENCE(sequence_checker_);
  content_settings_ = base::WrapRefCounted(
      HostContentSettingsMapFactory::GetForProfile(profile_));

#if BUILDFLAG(IS_ANDROID)
  one_time_permissions_tracker_.Observe(
      OneTimePermissionsTrackerFactory::GetForBrowserContext(context));
#else
  auto* provider = web_app::WebAppProvider::GetForWebApps(
      Profile::FromBrowserContext(profile_));
  if (provider) {
    install_manager_observation_.Observe(&provider->install_manager());
  }
  if (base::FeatureList::IsEnabled(
          features::kFileSystemAccessPersistentPermissions)) {
    one_time_permissions_tracker_.Observe(
        OneTimePermissionsTrackerFactory::GetForBrowserContext(context));

    // Deprecated persisted permission objects contains a timestamp key, used
    // in old implementation. Revoke them so that the state is reset for the new
    // persisted permission implementation.
    std::set<url::Origin> origins =
        ObjectPermissionContextBase::GetOriginsWithGrants();
    for (const auto& origin : origins) {
      for (const auto& object :
           ObjectPermissionContextBase::GetGrantedObjects(origin)) {
        if (object->value.contains(kDeprecatedPermissionLastUsedTimeKey)) {
          RevokeObjectPermission(origin, GetKeyForObject(object->value));
        }
      }
    }
  }
#endif
}

ChromeFileSystemAccessPermissionContext::
    ~ChromeFileSystemAccessPermissionContext() = default;

void ChromeFileSystemAccessPermissionContext::InitializeBlockPaths() {
  // This method should only be called when the `block_path_rules_status_` are
  // not initialized.
  CHECK_EQ(block_path_rules_status_, ChromeFileSystemAccessPermissionContext::
                                         BlockPathRulesStatus::kNotInitialized);
  InitializeBlockPathsInternal();
}

void ChromeFileSystemAccessPermissionContext::InitializeBlockPathsInternal() {
  block_path_rules_status_ = BlockPathRulesStatus::kInitializationStarted;
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
      base::BindOnce(&GenerateBlockPaths, should_normalize_file_path_),
      base::BindOnce(&ChromeFileSystemAccessPermissionContext::UpdateBlockPaths,
                     weak_factory_.GetWeakPtr()));
}

void ChromeFileSystemAccessPermissionContext::ResetBlockPathsForTesting() {
  InitializeBlockPathsInternal();
}

void ChromeFileSystemAccessPermissionContext::UpdateBlockPaths(
    std::unique_ptr<BlockPathRules> block_path_rules) {
  block_path_rules_ = std::move(block_path_rules);
  block_path_rules_status_ = BlockPathRulesStatus::kInitialized;
  block_rules_check_callbacks_.Notify(*block_path_rules_.get());
}

bool ChromeFileSystemAccessPermissionContext::RevokeActiveGrants(
    const url::Origin& origin,
    const base::FilePath& file_path) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  bool grant_revoked = false;

  auto origin_it = active_permissions_map_.find(origin);
  if (origin_it != active_permissions_map_.end()) {
    OriginState& origin_state = origin_it->second;

    if (file_path.empty()) {
      if (!origin_state.downgraded_read_paths.empty()) {
        origin_state.downgraded_read_paths.clear();
        grant_revoked = true;
      }
    } else {
      if (origin_state.downgraded_read_paths.erase(file_path)) {
        grant_revoked = true;
      }
    }

    for (auto grant_iter = origin_state.read_grants.begin(),
              grant_end = origin_state.read_grants.end();
         grant_iter != grant_end;) {
      // The grant may be removed from `read_grants`, so increase the iterator
      // before continuing.
      auto& grant = *(grant_iter++);
      if (file_path.empty() || grant.first == file_path) {
        if (grant.second) {
          grant.second->SetStatus(
              PermissionStatus::ASK,
              PersistedPermissionOptions::kDoNotUpdatePersistedPermission);
          grant_revoked = true;
        }
      }
    }
    for (auto grant_iter = origin_state.write_grants.begin(),
              grant_end = origin_state.write_grants.end();
         grant_iter != grant_end;) {
      // The grant may be removed from `write_grants`, so increase the iterator
      // before continuing.
      auto& grant = *(grant_iter++);
      if (file_path.empty() || grant.first == file_path) {
        if (grant.second) {
          grant.second->SetStatus(
              PermissionStatus::ASK,
              PersistedPermissionOptions::kDoNotUpdatePersistedPermission);
          grant_revoked = true;
        }
      }
    }
    // Only update `persisted_grant_status` if the state has not already been
    // set via tab backgrounding.
    if (file_path.empty() && origin_state.persisted_grant_status !=
                                 PersistedGrantStatus::kBackgrounded) {
      origin_state.persisted_grant_status = PersistedGrantStatus::kLoaded;
    }
  }
  return grant_revoked;
}

void ChromeFileSystemAccessPermissionContext::RevokeAllActiveGrants() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  for (auto& [origin, origin_state] : active_permissions_map_) {
    origin_state.downgraded_read_paths.clear();

    // Only update `persisted_grant_status` if the state has not already been
    // set via tab backgrounding. We do this before iterating over grants so
    // `FileSystemAccessPermissionGrant::Observer`s can update their state
    // correctly.
    if (origin_state.persisted_grant_status !=
        PersistedGrantStatus::kBackgrounded) {
      origin_state.persisted_grant_status = PersistedGrantStatus::kLoaded;
    }

    for (auto grant_iter = origin_state.read_grants.begin(),
              grant_end = origin_state.read_grants.end();
         grant_iter != grant_end;) {
      // The grant may be removed from `read_grants`, so increase the iterator
      // before continuing.
      auto& [_, grant] = *(grant_iter++);
      grant->SetStatus(
          PermissionStatus::ASK,
          PersistedPermissionOptions::kDoNotUpdatePersistedPermission);
    }
    for (auto grant_iter = origin_state.write_grants.begin(),
              grant_end = origin_state.write_grants.end();
         grant_iter != grant_end;) {
      // The grant may be removed from `write_grants`, so increase the iterator
      // before continuing.
      auto& [_, grant] = *(grant_iter++);
      grant->SetStatus(
          PermissionStatus::ASK,
          PersistedPermissionOptions::kDoNotUpdatePersistedPermission);
    }
  }
}

scoped_refptr<content::FileSystemAccessPermissionGrant>
ChromeFileSystemAccessPermissionContext::GetReadPermissionGrant(
    const url::Origin& origin,
    const content::PathInfo& path_info,
    HandleType handle_type,
    UserAction user_action) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // operator[] might insert a new OriginState in |active_permissions_map_|,
  // but that is exactly what we want.
  auto& origin_state = active_permissions_map_[origin];
  auto& existing_grant = origin_state.read_grants[path_info.path];
  scoped_refptr<PermissionGrantImpl> grant;

  if (existing_grant && existing_grant->handle_type() != handle_type) {
    // |path| changed from being a directory to being a file or vice versa,
    // don't just re-use the existing grant but revoke the old grant before
    // creating a new grant.
    existing_grant->SetStatus(
        PermissionStatus::DENIED,
        PersistedPermissionOptions::kUpdatePersistedPermission);
    existing_grant = nullptr;
  }

  bool creating_new_grant = !existing_grant;
  if (creating_new_grant) {
    grant = base::MakeRefCounted<PermissionGrantImpl>(
        weak_factory_.GetWeakPtr(), origin, path_info, handle_type,
        GrantType::kRead, user_action);
    existing_grant = grant.get();
  } else {
    grant = existing_grant;
  }

  const ContentSetting content_setting = GetReadGuardContentSetting(origin);
  switch (content_setting) {
    case CONTENT_SETTING_ALLOW:
      // Don't persist permissions when the origin is allowlisted.
      grant->SetStatus(
          PermissionStatus::GRANTED,
          PersistedPermissionOptions::kDoNotUpdatePersistedPermission);
      break;
    case CONTENT_SETTING_ASK:
      // If a parent directory is already readable this new grant should also be
      // readable.
      if (creating_new_grant && AncestorHasActivePermission(
                                    origin, path_info.path, GrantType::kRead)) {
        grant->SetStatus(
            PermissionStatus::GRANTED,
            PersistedPermissionOptions::kUpdatePersistedPermission);
        break;
      }
      switch (user_action) {
        case UserAction::kOpen:
        case UserAction::kSave:
          // Open and Save dialog only grant read access for individual files.
          if (handle_type == HandleType::kDirectory) {
            break;
          }
          [[fallthrough]];
        case UserAction::kDragAndDrop:
          // Drag&drop grants read access for all handles.
          grant->SetStatus(
              PermissionStatus::GRANTED,
              PersistedPermissionOptions::kUpdatePersistedPermission);
          break;
        case UserAction::kLoadFromStorage:
        case UserAction::kNone:
          break;
      }
      break;
    case CONTENT_SETTING_BLOCK:
      // Don't bother revoking persisted permissions. If the permissions have
      // not yet expired when the ContentSettingValue is changed, they will
      // effectively be reinstated.
      if (creating_new_grant) {
        grant->SetStatus(
            PermissionStatus::DENIED,
            PersistedPermissionOptions::kDoNotUpdatePersistedPermission);
      } else {
        // We won't revoke permission to an existing grant.
      }
      break;
    default:
      NOTREACHED();
  }

  if (HasGrantedActivePermissionStatus(grant.get())) {
    ScheduleUsageIconUpdate();
  }

  return grant;
}

scoped_refptr<content::FileSystemAccessPermissionGrant>
ChromeFileSystemAccessPermissionContext::GetWritePermissionGrant(
    const url::Origin& origin,
    const content::PathInfo& path_info,
    HandleType handle_type,
    UserAction user_action) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // operator[] might insert a new OriginState in |active_permissions_map_|,
  // but that is exactly what we want.
  auto& origin_state = active_permissions_map_[origin];
  auto& existing_grant = origin_state.write_grants[path_info.path];
  scoped_refptr<PermissionGrantImpl> grant;

  if (existing_grant && existing_grant->handle_type() != handle_type) {
    // |path| changed from being a directory to being a file or vice versa,
    // don't just re-use the existing grant but revoke the old grant before
    // creating a new grant.
    existing_grant->SetStatus(
        PermissionStatus::DENIED,
        PersistedPermissionOptions::kUpdatePersistedPermission);
    existing_grant = nullptr;
  }

  bool creating_new_grant = !existing_grant;
  if (creating_new_grant) {
    grant = base::MakeRefCounted<PermissionGrantImpl>(
        weak_factory_.GetWeakPtr(), origin, path_info, handle_type,
        GrantType::kWrite, user_action);
    existing_grant = grant.get();
  } else {
    grant = existing_grant;
  }

  const ContentSetting content_setting = GetWriteGuardContentSetting(origin);
  switch (content_setting) {
    case CONTENT_SETTING_ALLOW:
      // Don't persist permissions when the origin is allowlisted.
      grant->SetStatus(
          PermissionStatus::GRANTED,
          PersistedPermissionOptions::kDoNotUpdatePersistedPermission);
      break;
    case CONTENT_SETTING_ASK:
      // If a parent directory is already writable this new grant should also be
      // writable.
      if (creating_new_grant &&
          AncestorHasActivePermission(origin, path_info.path,
                                      GrantType::kWrite)) {
        grant->SetStatus(
            PermissionStatus::GRANTED,
            PersistedPermissionOptions::kUpdatePersistedPermission);
        break;
      }
      switch (user_action) {
        case UserAction::kSave:
          // Only automatically grant write access for save dialogs.
          grant->SetStatus(
              PermissionStatus::GRANTED,
              PersistedPermissionOptions::kUpdatePersistedPermission);
          break;
        case UserAction::kOpen:
        case UserAction::kDragAndDrop:
        case UserAction::kLoadFromStorage:
        case UserAction::kNone:
          break;
      }
      break;
    case CONTENT_SETTING_BLOCK:
      // Don't bother revoking persisted permissions. If the permissions have
      // not yet expired when the ContentSettingValue is changed, they will
      // effectively be reinstated.
      if (creating_new_grant) {
        grant->SetStatus(
            PermissionStatus::DENIED,
            PersistedPermissionOptions::kDoNotUpdatePersistedPermission);
      } else {
        // We won't revoke permission to an existing grant.
      }
      break;
    default:
      NOTREACHED();
  }

  if (HasGrantedActivePermissionStatus(grant.get())) {
    ScheduleUsageIconUpdate();
  }

  return grant;
}

// Return extended permission grants for an origin.
std::vector<std::unique_ptr<permissions::ObjectPermissionContextBase::Object>>
ChromeFileSystemAccessPermissionContext::GetExtendedPersistedObjects(
    const url::Origin& origin) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (GetPersistedGrantType(origin) == PersistedGrantType::kExtended) {
    // When the origin has extended permission enabled, all permissions objects
    // represent extended grants.
    return ObjectPermissionContextBase::GetGrantedObjects(origin);
  }

  return {};
}

// Returns extended grants or active grants for an origin.
std::vector<std::unique_ptr<permissions::ObjectPermissionContextBase::Object>>
ChromeFileSystemAccessPermissionContext::GetGrantedObjects(
    const url::Origin& origin) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  auto persisted_grant_type = GetPersistedGrantType(origin);
  if (persisted_grant_type == PersistedGrantType::kExtended ||
      persisted_grant_type == PersistedGrantType::kShadow) {
    // Objects stored in content settings map via `ObjectPermissionContextBase`
    // represent a valid set of grants, if origin has extended or shadow grants.
    // In the case of shadow grants, it should be matching the set of active
    // permission map, but it may not have `PermissionStatus::GRANTED`, if the
    // page is refreshed and `FileSystemAccessPermissionGrant` is
    // garbage-collected, hence returning shadow grant objects directly here.
    return ObjectPermissionContextBase::GetGrantedObjects(origin);
  }

  // Otherwise, a valid set of grants are stored in the in-memory map
  // |active_permissions_map_|.
  // TODO(crbug.com/40276567): Update iteration logic below to handle the case
  // of write-only permission grants.
  std::vector<std::unique_ptr<Object>> objects;
  auto it = active_permissions_map_.find(origin);
  if (it != active_permissions_map_.end()) {
    for (const auto& grant : it->second.read_grants) {
      if (HasGrantedActivePermissionStatus(grant.second)) {
        auto value = grant.second->AsValue();

        // Persisted permissions include both read and write information in
        // one object. If a write grant for this origin/path exists, then
        // update the value to store a writable key as well.
        const auto& file_path = grant.first;
        auto write_grant_it = it->second.write_grants.find(file_path);
        if (write_grant_it != it->second.write_grants.end() &&
            HasGrantedActivePermissionStatus(write_grant_it->second)) {
          value.Set(kPermissionWritableKey, true);
        }

        objects.push_back(std::make_unique<Object>(
            origin, base::Value(std::move(value)),
            content_settings::SettingSource::kUser, IsOffTheRecord()));
      }
    }
  }

  return objects;
}

// Returns all origins' extended grants or active grants.
std::vector<std::unique_ptr<permissions::ObjectPermissionContextBase::Object>>
ChromeFileSystemAccessPermissionContext::GetAllGrantedObjects() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  std::vector<std::unique_ptr<Object>> all_objects;
  for (const auto& origin : GetOriginsWithGrants()) {
    auto objects = GetGrantedObjects(origin);
    std::ranges::move(objects, std::back_inserter(all_objects));
  }

  return all_objects;
}

// Returns origins that have either extended grants or active grants.
std::set<url::Origin>
ChromeFileSystemAccessPermissionContext::GetOriginsWithGrants() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  std::set<url::Origin> origins;

  if (base::FeatureList::IsEnabled(
          features::kFileSystemAccessPersistentPermissions)) {
    // Get origins with extended permissions.
    for (const url::Origin& origin :
         ObjectPermissionContextBase::GetOriginsWithGrants()) {
      if (OriginHasExtendedPermission(origin)) {
        origins.insert(origin);
      }
    }
  }

  // Add origins that have active, granted permission grants.
  for (const auto& it : active_permissions_map_) {
    if (std::ranges::any_of(it.second.read_grants,
                            [&](const auto& grant) {
                              return HasGrantedActivePermissionStatus(
                                  grant.second);
                            }) ||
        std::ranges::any_of(it.second.write_grants, [&](const auto& grant) {
          return HasGrantedActivePermissionStatus(grant.second);
        })) {
      origins.insert(it.first);
    }
  }

  return origins;
}

std::string ChromeFileSystemAccessPermissionContext::GetKeyForObject(
    const base::DictValue& object) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  const auto optional_path =
      base::ValueToFilePath(object.Find(kPermissionPathKey));
  DCHECK(optional_path);
  return std::string(PathAsPermissionKey(optional_path.value()));
}

bool ChromeFileSystemAccessPermissionContext::IsValidObject(
    const base::DictValue& dict) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (dict.size() < 3 || dict.size() > 5) {
    return false;
  }

  // At least one of the readable/writable keys needs to be set.
  if (!dict.FindBool(kPermissionWritableKey) &&
      !dict.FindBool(kPermissionReadableKey)) {
    return false;
  }

  if (!dict.contains(kPermissionPathKey) ||
      !dict.FindBool(kPermissionIsDirectoryKey) ||
      dict.contains(kDeprecatedPermissionLastUsedTimeKey)) {
    return false;
  }

  return true;
}

std::u16string ChromeFileSystemAccessPermissionContext::GetObjectDisplayName(
    const base::DictValue& object) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  const auto optional_path =
      base::ValueToFilePath(object.Find(kPermissionPathKey));
  DCHECK(optional_path);
  return optional_path->LossyDisplayName();
}

ContentSetting
ChromeFileSystemAccessPermissionContext::GetReadGuardContentSetting(
    const url::Origin& origin) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return content_settings_->GetContentSetting(
      origin.GetURL(), origin.GetURL(),
      ContentSettingsType::FILE_SYSTEM_READ_GUARD);
}

ContentSetting
ChromeFileSystemAccessPermissionContext::GetWriteGuardContentSetting(
    const url::Origin& origin) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return content_settings_->GetContentSetting(
      origin.GetURL(), origin.GetURL(),
      ContentSettingsType::FILE_SYSTEM_WRITE_GUARD);
}

std::vector<base::FilePath>
ChromeFileSystemAccessPermissionContext::GetGrantedPaths(
    const url::Origin& origin) {
  return base::ToVector(GetGrantedObjects(origin), [](const auto& object) {
    const auto* path = object->value.Find(kPermissionPathKey);
    DCHECK(path);
    return base::ValueToFilePath(path).value();
  });
}

bool ChromeFileSystemAccessPermissionContext::CanObtainReadPermission(
    const url::Origin& origin) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return GetReadGuardContentSetting(origin) == CONTENT_SETTING_ASK ||
         GetReadGuardContentSetting(origin) == CONTENT_SETTING_ALLOW;
}

bool ChromeFileSystemAccessPermissionContext::CanObtainWritePermission(
    const url::Origin& origin) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return GetWriteGuardContentSetting(origin) == CONTENT_SETTING_ASK ||
         GetWriteGuardContentSetting(origin) == CONTENT_SETTING_ALLOW;
}

bool ChromeFileSystemAccessPermissionContext::IsFileTypeDangerous(
    const base::FilePath& path) {
#if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
  return GetFileTypeDangerLevel(path) ==
         safe_browsing::DownloadFileType::DANGEROUS;
#else
  return false;
#endif
}

void ChromeFileSystemAccessPermissionContext::ConfirmSensitiveEntryAccess(
    const url::Origin& origin,
    const content::PathInfo& path_info,
    HandleType handle_type,
    UserAction user_action,
    content::GlobalRenderFrameHostId frame_id,
    base::OnceCallback<void(SensitiveEntryResult)> callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  const base::TimeTicks start_time = base::TimeTicks::Now();

  auto after_blocklist_check_callback = base::BindOnce(
      &ChromeFileSystemAccessPermissionContext::DidCheckPathAgainstBlocklist,
      GetWeakPtr(), origin, path_info, handle_type, user_action, frame_id,
      start_time, std::move(callback));
  CheckPathAgainstBlocklist(path_info, handle_type, user_action,
                            std::move(after_blocklist_check_callback));
}

void ChromeFileSystemAccessPermissionContext::CheckPathsAgainstEnterprisePolicy(
    std::vector<content::PathInfo> entries,
    content::GlobalRenderFrameHostId frame_id,
    EntriesAllowedByEnterprisePolicyCallback callback) {
#if BUILDFLAG(ENTERPRISE_CLOUD_CONTENT_ANALYSIS)
  // Get WebContents pointer in order to perform enterprise content analysis.
  content::RenderFrameHost* rfh = content::RenderFrameHost::FromID(frame_id);
  content::WebContents* web_contents = nullptr;
  if (!entries.empty()) {
    if (rfh && rfh->IsActive()) {
      web_contents = content::WebContents::FromRenderFrameHost(rfh);
    }
  }

  if (!web_contents) {
    std::move(callback).Run(std::move(entries));
    return;
  }

  Profile* browser_profile = Profile::FromBrowserContext(profile());
  enterprise_connectors::ContentAnalysisDelegate::Data data;
  if (!enterprise_connectors::ContentAnalysisDelegate::IsEnabled(
          browser_profile, web_contents->GetLastCommittedURL(), &data,
          enterprise_connectors::AnalysisConnector::FILE_ATTACHED)) {
    std::move(callback).Run(std::move(entries));
    return;
  }

  data.reason =
      enterprise_connectors::ContentAnalysisRequest::FILE_PICKER_DIALOG;
  data.initiating_frame_id = frame_id;

#if BUILDFLAG(IS_CHROMEOS)
  storage::FileSystemContext* file_system_context = nullptr;
  if (rfh) {
    content::SiteInstance* site_instance = rfh->GetSiteInstance();
    if (site_instance && browser_profile) {
      file_system_context = browser_profile->GetStoragePartition(site_instance)
                                ->GetFileSystemContext();
    }
  }
  storage::ExternalMountPoints* mount_points =
      storage::ExternalMountPoints::GetSystemInstance();
#endif

  // Resolve virtual paths for kExternal files to their physical paths
  // so they can be scanned, but keep the original entries (with virtual paths)
  // to return to the caller.
  data.paths.reserve(entries.size());
  for (const auto& entry : entries) {
    base::FilePath path_to_scan = entry.path;
#if BUILDFLAG(IS_CHROMEOS)
    if (entry.type == content::PathType::kExternal) {
      base::FilePath resolved_path = GetExternalPath(
          browser_profile, file_system_context, mount_points, entry.path);
      if (!resolved_path.empty()) {
        path_to_scan = std::move(resolved_path);
      }
    }
#endif  // BUILDFLAG(IS_CHROMEOS)
    data.paths.push_back(std::move(path_to_scan));
  }

  // CreateForFilesInWebContents() only handles real OS files. Any kExternal
  // entries that failed to resolve will be ignored by the scanner and
  // reconciled based on the policy's default action (fail-open or fail-closed).
  // TODO(crbug.com/535207208): Add a test to validate that unscannedFileEvent
  // is reported for these unresolved files.
  enterprise_connectors::ContentAnalysisDelegate::CreateForFilesInWebContents(
      web_contents, std::move(data),
      base::BindOnce(
          &ChromeFileSystemAccessPermissionContext::OnContentAnalysisComplete,
          weak_factory_.GetWeakPtr(), std::move(entries), std::move(callback)),
      enterprise_connectors::DeepScanAccessPoint::UPLOAD);
#else
  std::move(callback).Run(std::move(entries));
#endif  // BUILDFLAG(ENTERPRISE_CLOUD_CONTENT_ANALYSIS)
}

#if BUILDFLAG(ENTERPRISE_CLOUD_CONTENT_ANALYSIS)

// TODO(crbug.com/534804380): Remove the unused `paths` parameter.
void ChromeFileSystemAccessPermissionContext::OnContentAnalysisComplete(
    std::vector<content::PathInfo> entries,
    EntriesAllowedByEnterprisePolicyCallback callback,
    std::vector<base::FilePath> paths,
    std::vector<bool> allowed) {
  CHECK_EQ(paths.size(), allowed.size());
  CHECK_EQ(paths.size(), entries.size());

  std::vector<content::PathInfo> result_entries;
  for (size_t i = 0; i < paths.size(); ++i) {
    if (allowed[i]) {
      result_entries.emplace_back(entries[i].type, std::move(entries[i].path),
                                  std::move(entries[i].display_name));
    }
  }

  std::move(callback).Run(std::move(result_entries));
}

#endif  // BUILDFLAG(ENTERPRISE_CLOUD_CONTENT_ANALYSIS)

void ChromeFileSystemAccessPermissionContext::
    CheckShouldBlockAccessToPathAndReply(
        base::FilePath path,
        HandleType handle_type,
        UserAction user_action,
        std::vector<BlockPathRule> extra_rules,
        base::OnceCallback<void(bool)> callback,
        BlockPathRules block_path_rules) {
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
      base::BindOnce(&ShouldBlockAccessToPath, should_normalize_file_path_,
                     std::move(path), handle_type, user_action,
                     std::move(extra_rules), std::move(block_path_rules),
                     profile_path_override_.value_or(profile_->GetPath())),
      std::move(callback));
}

void ChromeFileSystemAccessPermissionContext::CheckPathAgainstBlocklist(
    const content::PathInfo& path_info,
    HandleType handle_type,
    UserAction user_action,
    base::OnceCallback<void(bool)> callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (path_info.type == content::PathType::kExternal) {
    std::move(callback).Run(/*should_block=*/false);
    return;
  }

#if BUILDFLAG(IS_ANDROID)
  // The only check for content-URIs is that they are not from an internal
  // FileProvider.
  if (path_info.path.IsContentUri()) {
    std::move(callback).Run(base::IsContentUriFromThisApp(path_info.path));
    return;
  }
#endif  // BUILDFLAG(IS_ANDROID)

  // Unlike the DIR_USER_DATA check, this handles the --user-data-dir override.
  // We check for the user data dir in two different ways: directly, via the
  // profile manager, where it exists (it does not in unit tests), and via the
  // profile's directory, assuming the profile dir is a child of the user data
  // dir.
  std::vector<BlockPathRule> extra_rules;
  extra_rules.emplace_back(profile_->GetPath().DirName(),
                           BlockType::kBlockAllChildren);
  if (g_browser_process->profile_manager()) {
    extra_rules.emplace_back(
        g_browser_process->profile_manager()->user_data_dir(),
        BlockType::kBlockAllChildren);
  }

  switch (block_path_rules_status_) {
    case BlockPathRulesStatus::kInitialized:
      // If the `block_path_rules_status_` is already initilizaed, we can just
      // post the task to a anonymous blocking traits.
      CheckShouldBlockAccessToPathAndReply(
          path_info.path, handle_type, user_action, std::move(extra_rules),
          std::move(callback), *block_path_rules_.get());
      return;

    case BlockPathRulesStatus::kNotInitialized:
      // If the `block_path_rules_status_` is `kNotInitialized`, lazy initialize
      // the `block_path_rules_`.
      // This will make the status `kInitializationStarted`, so fallthrough to
      // the next block.
      InitializeBlockPaths();
      [[fallthrough]];

    case BlockPathRulesStatus::kInitializationStarted:
      // The check must be performed after the rules initialization is done.
      block_rules_check_subscription_.push_back(
          block_rules_check_callbacks_.Add(base::BindOnce(
              &ChromeFileSystemAccessPermissionContext::
                  CheckShouldBlockAccessToPathAndReply,
              weak_factory_.GetWeakPtr(), path_info.path, handle_type,
              user_action, std::move(extra_rules), std::move(callback))));
      break;
  }
}

void ChromeFileSystemAccessPermissionContext::PerformAfterWriteChecks(
    std::unique_ptr<content::FileSystemAccessWriteItem> item,
    content::GlobalRenderFrameHostId frame_id,
    base::OnceCallback<void(AfterWriteCheckResult)> callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
#if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(
          &DoSafeBrowsingCheckOnUIThread, frame_id, std::move(item),
          base::BindOnce(
              [](scoped_refptr<base::TaskRunner> task_runner,
                 base::OnceCallback<void(AfterWriteCheckResult result)>
                     callback,
                 safe_browsing::DownloadCheckResult result) {
                task_runner->PostTask(
                    FROM_HERE,
                    base::BindOnce(std::move(callback),
                                   InterpretSafeBrowsingResult(result)));
              },
              base::SequencedTaskRunner::GetCurrentDefault(),
              std::move(callback))));
#else
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(
          [](base::OnceCallback<void(AfterWriteCheckResult result)> callback) {
            std::move(callback).Run(AfterWriteCheckResult::kAllow);
          },
          std::move(callback)));
#endif
}

base::expected<void, std::string>
ChromeFileSystemAccessPermissionContext::CanShowFilePicker(
    content::RenderFrameHost* rfh) {
#if BUILDFLAG(ENABLE_EXTENSIONS_CORE) && BUILDFLAG(ENABLE_GUEST_VIEW)
  // Because permission is scoped to profile, <webview> and <controlledframe>,
  // despite having isolated StoragePartition, will share File System Access
  // permission with the rest of the profile. Therefore, we want to disable FSA
  // for these contexts. However, <webview> is allowed to use FSA in avoidance
  // of breaking existing usage.
  if (auto* guest = extensions::WebViewGuest::FromRenderFrameHost(rfh);
      guest != nullptr) {
    // Disables file picker for <controlledframe> but allows <webview> to use
    // it.
    // TODO(crbug.com/40066989): Fix origin-keyed permission sharing between
    // <webview> and rest of profile.
    if (guest->IsOwnedByControlledFrameEmbedder()) {
      return base::unexpected(kDefaultNotAllowedMessage);
    }
    return base::ok();
  }
#endif  // BUILDFLAG(ENABLE_EXTENSIONS_CORE) && BUILDFLAG(ENABLE_GUEST_VIEW)

  // Because permission is scoped to the profile, guest contexts (like
  // <controlledframe> and SlimWebView), despite having isolated
  // StoragePartitions, would share File System Access permissions with the rest
  // of the profile. Therefore, we disable File System Access for guest
  // contexts. Note that on desktop, <webview> is explicitly allowed to use FSA
  // in the block above to avoid breaking existing usage.
  if (rfh->GetSiteInstance()->GetSecurityPrincipal().IsGuest()) {
#if BUILDFLAG(IS_ANDROID)
    // Allow Glic guest contexts to use File System Access API file pickers.
    content::WebContents* web_contents =
        content::WebContents::FromRenderFrameHost(rfh);
    if (glic::IsGlicGuest(web_contents) &&
        glic::GetGuestOrigin().IsSameOriginWith(
            rfh->GetLastCommittedOrigin())) {
      return base::ok();
    }
#endif  // BUILDFLAG(IS_ANDROID)
    return base::unexpected(kDefaultNotAllowedMessage);
  }

  // Disable any other non-default StoragePartition contexts. However, unique
  // schemes (e.g. isolated-app://) are exempt here.
  if (rfh->GetStoragePartition() !=
          rfh->GetBrowserContext()->GetDefaultStoragePartition() &&
      rfh->GetLastCommittedURL().SchemeIsHTTPOrHTTPS()) {
    return base::unexpected(kDefaultNotAllowedMessage);
  }

  return base::ok();
}

void ChromeFileSystemAccessPermissionContext::DidCheckPathAgainstBlocklist(
    const url::Origin& origin,
    const content::PathInfo& path_info,
    HandleType handle_type,
    UserAction user_action,
    content::GlobalRenderFrameHostId frame_id,
    const base::TimeTicks start_time,
    base::OnceCallback<void(SensitiveEntryResult)> callback,
    bool should_block) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  base::UmaHistogramTimes(
      "Storage.FileSystemAccess.ConfirmSensitiveEntryAccessDuration",
      base::TimeTicks::Now() - start_time);

  if (user_action == UserAction::kNone) {
    std::move(callback).Run(should_block ? SensitiveEntryResult::kAbort
                                         : SensitiveEntryResult::kAllowed);
    return;
  }

  if (should_block) {
    auto result_callback =
        base::BindPostTaskToCurrentDefault(std::move(callback));
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(&ShowFileSystemAccessRestrictedDirectoryDialogOnUIThread,
                       frame_id, origin, handle_type,
                       std::move(result_callback)));
    return;
  }

#if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
  // If attempting to save a file with a dangerous extension, prompt the user
  // to make them confirm they actually want to save the file.
  if (handle_type == HandleType::kFile && user_action == UserAction::kSave) {
    // See https://crbug.com/40059513#comment5 for justification for why we show
    // the prompt if `danger_level` is ALLOW_ON_USER_GESTURE as well as
    // DANGEROUS.
    auto danger_level = GetFileTypeDangerLevel(path_info.path);
    if (danger_level == safe_browsing::DownloadFileType::DANGEROUS ||
        danger_level ==
            safe_browsing::DownloadFileType::ALLOW_ON_USER_GESTURE) {
      auto result_callback =
          base::BindPostTaskToCurrentDefault(std::move(callback));
      content::GetUIThreadTaskRunner({})->PostTask(
          FROM_HERE,
          base::BindOnce(&ShowFileSystemAccessDangerousFileDialogOnUIThread,
                         frame_id, origin, path_info,
                         std::move(result_callback)));
      return;
    }
  }
#endif

  std::move(callback).Run(SensitiveEntryResult::kAllowed);
}

void ChromeFileSystemAccessPermissionContext::MaybeEvictEntries(
    base::DictValue& dict) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  size_t num_candidates = dict.size();
  if (dict.contains(kDefaultLastPickedDirectoryKey)) {
    num_candidates--;
  }

  if (num_candidates <= max_ids_per_origin_) {
    return;
  }

  const size_t entries_to_remove = num_candidates - max_ids_per_origin_;
  std::vector<std::pair<base::Time, std::string>> oldest_entries;
  oldest_entries.reserve(entries_to_remove);

  for (auto entry : dict) {
    // Don't evict the default ID.
    if (entry.first == kDefaultLastPickedDirectoryKey) {
      continue;
    }
    // If the data is corrupted and `entry.second` is for some reason not a
    // dict, it should be first in line for eviction.
    auto timestamp = base::Time::Min();
    if (base::DictValue* as_dict = entry.second.GetIfDict()) {
      timestamp = base::ValueToTime(as_dict->Find(kTimestampKey))
                      .value_or(base::Time::Min());
    }

    if (oldest_entries.size() < entries_to_remove) {
      oldest_entries.emplace_back(timestamp, entry.first);
      if (oldest_entries.size() == entries_to_remove) {
        std::ranges::make_heap(oldest_entries);
      }
    } else if (timestamp < oldest_entries.front().first) {
      std::ranges::pop_heap(oldest_entries);
      oldest_entries.back() = {timestamp, entry.first};
      std::ranges::push_heap(oldest_entries);
    }
  }

  for (const auto& entry : oldest_entries) {
    bool did_remove_entry = dict.Remove(entry.second);
    DCHECK(did_remove_entry);
  }
}

void ChromeFileSystemAccessPermissionContext::SetLastPickedDirectory(
    const url::Origin& origin,
    const std::string& id,
    const content::PathInfo& path_info) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  base::Value value = content_settings()->GetWebsiteSetting(
      origin.GetURL(), origin.GetURL(),
      ContentSettingsType::FILE_SYSTEM_LAST_PICKED_DIRECTORY,
      /*info=*/nullptr);
  if (!value.is_dict()) {
    value = base::Value(base::Value::Type::DICT);
  }

  base::DictValue& dict = value.GetDict();
  // Create an entry into the nested dictionary.
  base::DictValue entry;
  entry.Set(kPathKey, base::FilePathToValue(path_info.path));
  entry.Set(kPathTypeKey, static_cast<int>(path_info.type));
  entry.Set(kDisplayNameKey, path_info.display_name);
  entry.Set(kTimestampKey, base::TimeToValue(clock_->Now()));

  dict.Set(GenerateLastPickedDirectoryKey(id), std::move(entry));

  MaybeEvictEntries(dict);

  content_settings_->SetWebsiteSettingDefaultScope(
      origin.GetURL(), origin.GetURL(),
      ContentSettingsType::FILE_SYSTEM_LAST_PICKED_DIRECTORY,
      base::Value(std::move(dict)));
}

content::PathInfo
ChromeFileSystemAccessPermissionContext::GetLastPickedDirectory(
    const url::Origin& origin,
    const std::string& id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  base::Value value = content_settings()->GetWebsiteSetting(
      origin.GetURL(), origin.GetURL(),
      ContentSettingsType::FILE_SYSTEM_LAST_PICKED_DIRECTORY,
      /*info=*/nullptr);

  content::PathInfo path_info;
  const auto* dict = value.GetIfDict();
  if (!dict) {
    return path_info;
  }

  const auto* entry = dict->FindDict(GenerateLastPickedDirectoryKey(id));
  if (!entry) {
    return path_info;
  }

  auto type_int = entry->FindInt(kPathTypeKey)
                      .value_or(static_cast<int>(content::PathType::kLocal));
  path_info.type = type_int == static_cast<int>(content::PathType::kExternal)
                       ? content::PathType::kExternal
                       : content::PathType::kLocal;
  path_info.path =
      base::ValueToFilePath(entry->Find(kPathKey)).value_or(base::FilePath());
  path_info.display_name = StringOrEmpty(entry->FindString(kDisplayNameKey));
  return path_info;
}

base::FilePath
ChromeFileSystemAccessPermissionContext::GetWellKnownDirectoryPath(
    blink::mojom::WellKnownDirectory directory,
    const url::Origin& origin) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // PDF viewer uses the default Download directory set in browser, if possible.
  if (directory == blink::mojom::WellKnownDirectory::kDirDownloads &&
      IsPdfExtensionOrigin(origin)) {
    base::FilePath profile_download_path =
        DownloadPrefs::FromBrowserContext(profile())->DownloadPath();
    if (!profile_download_path.empty()) {
      return profile_download_path;
    }
  }

  int key = base::PATH_START;
  switch (directory) {
    case blink::mojom::WellKnownDirectory::kDirDesktop:
      key = base::DIR_USER_DESKTOP;
      break;
    case blink::mojom::WellKnownDirectory::kDirDocuments:
      key = chrome::DIR_USER_DOCUMENTS;
      break;
    case blink::mojom::WellKnownDirectory::kDirDownloads:
      key = chrome::DIR_DEFAULT_DOWNLOADS;
      break;
    case blink::mojom::WellKnownDirectory::kDirMusic:
      key = chrome::DIR_USER_MUSIC;
      break;
    case blink::mojom::WellKnownDirectory::kDirPictures:
      key = chrome::DIR_USER_PICTURES;
      break;
    case blink::mojom::WellKnownDirectory::kDirVideos:
      key = chrome::DIR_USER_VIDEOS;
      break;
  }
  base::FilePath directory_path;
  base::PathService::Get(key, &directory_path);
  return directory_path;
}

std::u16string ChromeFileSystemAccessPermissionContext::GetPickerTitle(
    const blink::mojom::FilePickerOptionsPtr& options) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Returning the empty string will fall back to the platform default for the
  // given picker type.
  std::u16string title;
  switch (options->type_specific_options->which()) {
    case blink::mojom::TypeSpecificFilePickerOptionsUnion::Tag::
        kDirectoryPickerOptions:
      title = l10n_util::GetStringUTF16(
          options->type_specific_options->get_directory_picker_options()
                      ->permission_mode ==
                  blink::mojom::FileSystemAccessPermissionMode::kReadWrite
              ? IDS_FILE_SYSTEM_ACCESS_CHOOSER_OPEN_WRITABLE_DIRECTORY_TITLE
              : IDS_FILE_SYSTEM_ACCESS_CHOOSER_OPEN_READABLE_DIRECTORY_TITLE);
      break;
    case blink::mojom::TypeSpecificFilePickerOptionsUnion::Tag::
        kSaveFilePickerOptions:
      title = l10n_util::GetStringUTF16(
          IDS_FILE_SYSTEM_ACCESS_CHOOSER_OPEN_SAVE_FILE_TITLE);
      break;
    case blink::mojom::TypeSpecificFilePickerOptionsUnion::Tag::
        kOpenFilePickerOptions:
      title = l10n_util::GetStringUTF16(
          options->type_specific_options->get_open_file_picker_options()
                  ->can_select_multiple_files
              ? IDS_FILE_SYSTEM_ACCESS_CHOOSER_OPEN_READABLE_FILES_TITLE
              : IDS_FILE_SYSTEM_ACCESS_CHOOSER_OPEN_READABLE_FILE_TITLE);
      break;
  }
  return title;
}

void ChromeFileSystemAccessPermissionContext::NotifyEntryMoved(
    const url::Origin& origin,
    const content::PathInfo& old_path,
    const content::PathInfo& new_path) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (old_path == new_path) {
    return;
  }

  // It's possible `new_path` already has existing persistent permission.
  // See crbug.com/423663220.
  bool allow_overwrite = base::FeatureList::IsEnabled(
      features::kFileSystemAccessMoveWithOverwrite);

  bool updated = false;
  bool old_path_was_downgraded = false;
  auto it = active_permissions_map_.find(origin);
  if (it != active_permissions_map_.end()) {
    // TODO(crbug.com/40245144): Consolidate superfluous child grants.
    PermissionGrantImpl::UpdateGrantPath(it->second.write_grants, old_path,
                                         new_path, allow_overwrite);
    PermissionGrantImpl::UpdateGrantPath(it->second.read_grants, old_path,
                                         new_path, allow_overwrite);
    if (base::FeatureList::IsEnabled(
            blink::features::kFileSystemAccessRevokeReadOnRemove) &&
        it->second.downgraded_read_paths.erase(old_path.path)) {
      // The downgraded read grant moved along with the entry; carry the
      // downgraded state to `new_path` so a later write there can restore it.
      it->second.downgraded_read_paths.insert(new_path.path);
      old_path_was_downgraded = true;
    }
    updated = true;
  }
  if (base::FeatureList::IsEnabled(
          features::kFileSystemAccessPersistentPermissions)) {
    // Active grants are a subset of persisted grants, so we also need to update
    // persisted grants, in case it's not covered by `UpdateGrantPath()` above.
    const std::unique_ptr<Object> object =
        GetGrantedObject(origin, PathAsPermissionKey(old_path.path));
    if (object) {
      if (allow_overwrite) {
        // Revoke any pre-existing permission at the destination first. This is
        // a no-op if no permission exists. Otherwise this will notify
        // permission observers twice: once for revocation and once for update.
        const std::string new_key(PathAsPermissionKey(new_path.path));
        RevokeObjectPermission(origin, new_key);
      }

      base::DictValue new_object = object->value.Clone();
      new_object.Set(kPermissionPathKey, base::FilePathToValue(new_path.path));
      new_object.Set(kPermissionDisplayNameKey, new_path.display_name);
      UpdateObjectPermission(origin, object->value, std::move(new_object));
      updated = true;
    }
  }

  if (updated) {
    ScheduleUsageIconUpdate();
  }

  if (base::FeatureList::IsEnabled(
          blink::features::kFileSystemAccessRevokeReadOnRemove) &&
      !old_path_was_downgraded) {
    // Only restore if the moved entry was readable at `old_path`; otherwise
    // the origin has not authored the content now at `new_path`.
    MaybeRestoreReadPermission(origin, new_path.path);
  }
}

void ChromeFileSystemAccessPermissionContext::NotifyEntryRemoved(
    const url::Origin& origin,
    const content::PathInfo& path) {
  CHECK(base::FeatureList::IsEnabled(
      blink::features::kFileSystemAccessRevokeReadOnRemove));
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (AncestorHasActivePermission(origin, path.path, GrantType::kRead)) {
    // If `path` has an active read grant inherited from its ancestor, don't
    // downgrade its permission, as it will still get ancestor grant by default.
    return;
  }

  const std::vector<base::FilePath::StringType> removed_components =
      path.path.GetComponents();
  auto is_path_or_descendant = [&](const base::FilePath& file_path) {
    return IsPathOrDescendantIgnoreCase(path.path, removed_components,
                                        file_path);
  };

  bool updated = false;
  auto it = active_permissions_map_.find(origin);
  if (it != active_permissions_map_.end()) {
    auto& origin_state = it->second;
    // Always insert the removed path itself into downgraded read paths.
    origin_state.downgraded_read_paths.insert(path.path);
    updated = true;

    // Revoke active read grants for the removed entry and its descendants.
    for (auto& [grant_path, grant] : origin_state.read_grants) {
      if (!is_path_or_descendant(grant_path)) {
        continue;
      }
      grant->DowngradeActiveReadGrant();
      origin_state.downgraded_read_paths.insert(grant_path);
    }
  }

  if (base::FeatureList::IsEnabled(
          features::kFileSystemAccessPersistentPermissions)) {
    // Active grants are a subset of persisted grants, so we also need to update
    // persisted grants, which is not covered by `DowngradeActiveReadGrant()`
    // above.
    // Revoke persisted read grants for the removed entry and its descendants.
    for (const auto& object : GetGrantedObjects(origin)) {
      std::optional<base::FilePath> grant_path =
          base::ValueToFilePath(object->value.Find(kPermissionPathKey));
      if (!grant_path || !is_path_or_descendant(*grant_path)) {
        continue;
      }

      base::DictValue new_object = object->value.Clone();
      new_object.Set(GetGrantKeyFromGrantType(GrantType::kRead), false);
      UpdateObjectPermission(origin, object->value, std::move(new_object));
      updated = true;
    }
  }

  if (updated) {
    ScheduleUsageIconUpdate();
  }
}

void ChromeFileSystemAccessPermissionContext::NotifyEntryModified(
    const url::Origin& origin,
    const content::PathInfo& path) {
  CHECK(base::FeatureList::IsEnabled(
      blink::features::kFileSystemAccessRevokeReadOnRemove));
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  MaybeRestoreReadPermission(origin, path.path);
}

void ChromeFileSystemAccessPermissionContext::MaybeRestoreReadPermission(
    const url::Origin& origin,
    const base::FilePath& path) {
  auto it = active_permissions_map_.find(origin);
  if (it == active_permissions_map_.end()) {
    return;
  }
  OriginState& origin_state = it->second;

  // Return early if the path was not previously downgraded.
  if (!origin_state.downgraded_read_paths.erase(path)) {
    return;
  }

  // Set the grant's status back to GRANTED if it was previously downgraded.
  auto grant_it = origin_state.read_grants.find(path);
  // Exclude the case where the path does not exist in the read_grants map.
  if (grant_it != origin_state.read_grants.end()) {
    // Since `NotifyEntryRemoved()` revokes both the active and persistent read
    // permissions, this call must restore both to ensure consistency.
    grant_it->second->SetStatus(
        PermissionStatus::GRANTED,
        PersistedPermissionOptions::kUpdatePersistedPermission);
  }
}

void ChromeFileSystemAccessPermissionContext::
    OnFileCreatedFromShowSaveFilePicker(const GURL& file_picker_binding_context,
                                        const storage::FileSystemURL& url) {
  file_created_from_show_save_file_picker_callback_list_.Notify(
      file_picker_binding_context, url);
}

base::CallbackListSubscription ChromeFileSystemAccessPermissionContext::
    AddFileCreatedFromShowSaveFilePickerCallback(
        FileCreatedFromShowSaveFilePickerCallbackList::CallbackType callback) {
  return file_created_from_show_save_file_picker_callback_list_.Add(
      std::move(callback));
}

ChromeFileSystemAccessPermissionContext::Grants
ChromeFileSystemAccessPermissionContext::ConvertObjectsToGrants(
    std::vector<std::unique_ptr<Object>> objects) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  ChromeFileSystemAccessPermissionContext::Grants grants;

  for (const auto& object : objects) {
    if (!IsValidObject(object->value)) {
      continue;
    }

    const base::DictValue& object_dict = object->value;
    const base::FilePath path =
        base::ValueToFilePath(object_dict.Find(kPermissionPathKey)).value();
    std::string display_name =
        StringOrEmpty(object_dict.FindString(kPermissionDisplayNameKey));
    if (display_name.empty()) {
      display_name = path.BaseName().AsUTF8Unsafe();
    }
    if (path.empty() || display_name.empty()) {
      continue;
    }
    HandleType handle_type =
        object_dict.FindBool(kPermissionIsDirectoryKey).value()
            ? HandleType::kDirectory
            : HandleType::kFile;
    bool is_write_grant =
        object_dict.FindBool(kPermissionWritableKey).value_or(false);
    bool is_read_grant =
        object_dict.FindBool(kPermissionReadableKey).value_or(false);

    if (handle_type == HandleType::kDirectory) {
      if (is_write_grant &&
          !PathInfosContains(grants.directory_write_grants, path)) {
        grants.directory_write_grants.emplace_back(path, display_name);
      }
      if (is_read_grant &&
          !PathInfosContains(grants.directory_read_grants, path)) {
        grants.directory_read_grants.emplace_back(path, display_name);
      }
    }
    if (handle_type == HandleType::kFile) {
      if (is_write_grant &&
          !PathInfosContains(grants.file_write_grants, path)) {
        grants.file_write_grants.emplace_back(path, display_name);
      }
      if (is_read_grant && !PathInfosContains(grants.file_read_grants, path)) {
        grants.file_read_grants.emplace_back(path, display_name);
      }
    }
  }

  return grants;
}

void ChromeFileSystemAccessPermissionContext::
    CreatePersistedGrantsFromActiveGrants(const url::Origin& origin) {
  if (base::FeatureList::IsEnabled(
          features::kFileSystemAccessPersistentPermissions)) {
    auto origin_it = active_permissions_map_.find(origin);
    if (origin_it != active_permissions_map_.end()) {
      OriginState& origin_state = origin_it->second;
      for (auto& read_grant : origin_state.read_grants) {
        if (HasGrantedActivePermissionStatus(read_grant.second)) {
          read_grant.second->SetStatus(
              PermissionStatus::GRANTED,
              PersistedPermissionOptions::kUpdatePersistedPermission);
        }
      }
      for (auto& write_grant : origin_state.write_grants) {
        if (HasGrantedActivePermissionStatus(write_grant.second)) {
          write_grant.second->SetStatus(
              PermissionStatus::GRANTED,
              PersistedPermissionOptions::kUpdatePersistedPermission);
        }
      }
    }
  }
}

void ChromeFileSystemAccessPermissionContext::RevokeGrant(
    const url::Origin& origin,
    const base::FilePath& file_path) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  bool grant_revoked = false;
  if (base::FeatureList::IsEnabled(
          features::kFileSystemAccessPersistentPermissions)) {
    auto key = PathAsPermissionKey(file_path);
    const std::unique_ptr<Object> object = GetGrantedObject(origin, key);
    if (object) {
      RevokeObjectPermission(origin, key);
      grant_revoked = true;
    }
  }

  if (RevokeActiveGrants(origin, file_path)) {
    grant_revoked = true;
  }

  if (grant_revoked) {
    ScheduleUsageIconUpdate();
  }
}

void ChromeFileSystemAccessPermissionContext::RevokeGrants(
    const url::Origin& origin) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  bool grant_revoked = false;
  if (base::FeatureList::IsEnabled(
          features::kFileSystemAccessPersistentPermissions)) {
    grant_revoked =
        ObjectPermissionContextBase::RevokeObjectPermissions(origin);
    content_settings_->SetContentSettingDefaultScope(
        origin.GetURL(), origin.GetURL(),
        ContentSettingsType::FILE_SYSTEM_ACCESS_EXTENDED_PERMISSION,
        ContentSetting::CONTENT_SETTING_DEFAULT);
  }

  if (RevokeActiveGrants(origin)) {
    grant_revoked = true;
  }

  if (grant_revoked) {
    ScheduleUsageIconUpdate();
  }
}

bool ChromeFileSystemAccessPermissionContext::OriginHasReadAccess(
    const url::Origin& origin) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // First, check if an origin has read access granted via active permissions.
  auto it = active_permissions_map_.find(origin);
  if (it != active_permissions_map_.end()) {
    return std::ranges::any_of(it->second.read_grants, [&](const auto& grant) {
      return HasGrantedActivePermissionStatus(grant.second);
    });
  }
  if (!base::FeatureList::IsEnabled(
          features::kFileSystemAccessPersistentPermissions)) {
    return false;
  }

  // Check if an origin has read access granted via extended permissions.
  std::vector<std::unique_ptr<Object>> extended_grant_objects =
      GetExtendedPersistedObjects(origin);
  if (extended_grant_objects.empty()) {
    return false;
  }
  return std::ranges::any_of(extended_grant_objects, [&](const auto& grant) {
    return grant->value.FindBool(kPermissionReadableKey).value_or(false);
  });
}

bool ChromeFileSystemAccessPermissionContext::OriginHasWriteAccess(
    const url::Origin& origin) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // First, check if an origin has write access granted via active permissions.
  auto it = active_permissions_map_.find(origin);
  if (it != active_permissions_map_.end()) {
    return std::ranges::any_of(it->second.write_grants, [&](const auto& grant) {
      return HasGrantedActivePermissionStatus(grant.second);
    });
  }
  if (!base::FeatureList::IsEnabled(
          features::kFileSystemAccessPersistentPermissions)) {
    return false;
  }

  // Check if an origin has write access granted via extended permissions.
  std::vector<std::unique_ptr<Object>> extended_grant_objects =
      GetExtendedPersistedObjects(origin);
  if (extended_grant_objects.empty()) {
    return false;
  }
  return std::ranges::any_of(extended_grant_objects, [&](const auto& grant) {
    return grant->value.FindBool(kPermissionWritableKey).value_or(false);
  });
}

// All tabs for a given origin have been backgrounded or cleared in the past
// 16 hours. When this happens, we update the given origin's `OriginState` to
// note that all tabs were recently backgrounded.
void ChromeFileSystemAccessPermissionContext::OnAllTabsInBackgroundTimerExpired(
    const url::Origin& origin,
    const OneTimePermissionsTrackerObserver::BackgroundExpiryType&
        expiry_type) {
  if (
#if !BUILDFLAG(IS_ANDROID)
      !base::FeatureList::IsEnabled(
          features::kFileSystemAccessPersistentPermissions) ||
#endif
      expiry_type != BackgroundExpiryType::kLongTimeout) {
    return;
  }
  SetPersistedGrantStatus(origin, PersistedGrantStatus::kBackgrounded);
  if (RevokeActiveGrants(origin)) {
    permissions::PermissionUmaUtil::RecordOneTimePermissionEvent(
        ContentSettingsType::FILE_SYSTEM_WRITE_GUARD,
        permissions::OneTimePermissionEvent::EXPIRED_IN_BACKGROUND);
    ScheduleUsageIconUpdate();
  }
}

void ChromeFileSystemAccessPermissionContext::OnLastPageFromOriginClosed(
    const url::Origin& origin) {
  CleanupPermissions(origin);
}

void ChromeFileSystemAccessPermissionContext::OnShutdown() {
  one_time_permissions_tracker_.Reset();
}

#if !BUILDFLAG(IS_ANDROID)
void ChromeFileSystemAccessPermissionContext::OnWebAppInstalled(
    const webapps::AppId& app_id) {
  if (!base::FeatureList::IsEnabled(
          features::kFileSystemAccessPersistentPermissions)) {
    return;
  }

  auto* provider = web_app::WebAppProvider::GetForWebApps(
      Profile::FromBrowserContext(profile()));
  const auto& registrar = provider->registrar_unsafe();
  // TODO(crbug.com/340952100): Evaluate call sites of IsInstallState for
  // correctness.
  if (registrar.GetInstallState(app_id) !=
      web_app::proto::InstallState::INSTALLED_WITH_OS_INTEGRATION) {
    return;
  }

  // TODO(crbug.com/40283362): Ensure that `GetAppScope` retrieves the correct
  // GURL when Scope Extensions is launched, which allows web apps to have more
  // than one origin as a scope.
  const auto gurl = registrar.GetAppScope(app_id);
  if (!gurl.is_valid()) {
    return;
  }
  const auto origin = url::Origin::Create(gurl);
  auto origin_it = active_permissions_map_.find(origin);
  if (origin_it == active_permissions_map_.end()) {
    // Ignore the origin if it does not have any active permissions.
    return;
  }

  // Update the cache value for web app state.
  OriginState& origin_state = origin_it->second;
  origin_state.web_app_install_status = WebAppInstallStatus::kInstalled;

  // Update the persisted grants, if needed.
  auto content_setting_value = content_settings_->GetContentSetting(
      origin.GetURL(), origin.GetURL(),
      ContentSettingsType::FILE_SYSTEM_ACCESS_EXTENDED_PERMISSION);
  if (content_setting_value == ContentSetting::CONTENT_SETTING_ALLOW ||
      content_setting_value == ContentSetting::CONTENT_SETTING_BLOCK) {
    // The user has already enabled or disabled extended permissions from the
    // Restore Prompt or Page Info bubble. Installing a WebApp should not
    // change the extended permission state.
    return;
  }
  UpgradeToExtendedPermission(origin);
}

void ChromeFileSystemAccessPermissionContext::OnWebAppInstalledWithOsHooks(
    const webapps::AppId& app_id) {
  // TODO(crbug.com/340952100): Remove the method after the InstallState is
  // saved in the database & available from OnWebAppInstalled.
  OnWebAppInstalled(app_id);
}

void ChromeFileSystemAccessPermissionContext::OnWebAppWillBeUninstalled(
    const webapps::AppId& app_id) {
  if (!base::FeatureList::IsEnabled(
          features::kFileSystemAccessPersistentPermissions)) {
    return;
  }

  auto* provider = web_app::WebAppProvider::GetForWebApps(
      Profile::FromBrowserContext(profile()));
  const auto& registrar = provider->registrar_unsafe();
  auto gurl = registrar.GetAppScope(app_id);
  if (!gurl.is_valid()) {
    return;
  }
  const auto origin = url::Origin::Create(gurl);
  auto origin_it = active_permissions_map_.find(origin);
  if (origin_it == active_permissions_map_.end()) {
    // Ignore the origin if it does not have any active permission.
    return;
  }

  // Update the cache value for web app state.
  OriginState& origin_state = origin_it->second;
  origin_state.web_app_install_status = WebAppInstallStatus::kUninstalled;

  // Update the persisted grants, if needed.
  auto content_setting_value = content_settings_->GetContentSetting(
      origin.GetURL(), origin.GetURL(),
      ContentSettingsType::FILE_SYSTEM_ACCESS_EXTENDED_PERMISSION);
  if (content_setting_value == ContentSetting::CONTENT_SETTING_ALLOW ||
      content_setting_value == ContentSetting::CONTENT_SETTING_BLOCK) {
    // The user has already enabled or disabled extended permissions from the
    // Restore Prompt or Page Info bubble. Uninstalling a WebApp should not
    // change the extended permission state.
    return;
  }
  RemoveExtendedPermission(origin);
}

void ChromeFileSystemAccessPermissionContext::
    OnWebAppInstallManagerDestroyed() {
  install_manager_observation_.Reset();
}
#endif

void ChromeFileSystemAccessPermissionContext::NavigatedAwayFromOrigin(
    const url::Origin& origin) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!base::FeatureList::IsEnabled(
          features::kFileSystemAccessPersistentPermissions)) {
    auto it = active_permissions_map_.find(origin);
    // If we have no permissions for the origin, there is nothing to do.
    if (it == active_permissions_map_.end()) {
      return;
    }

    // Start a timer to possibly clean up permissions for this origin.
    if (!it->second.cleanup_timer) {
      it->second.cleanup_timer = std::make_unique<base::RetainingOneShotTimer>(
          FROM_HERE, kPermissionRevocationTimeout,
          base::BindRepeating(
              &ChromeFileSystemAccessPermissionContext::MaybeCleanupPermissions,
              base::Unretained(this), origin));
    }
    it->second.cleanup_timer->Reset();
  }
}

void ChromeFileSystemAccessPermissionContext::TriggerTimersForTesting() {
  for (const auto& it : active_permissions_map_) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    if (it.second.cleanup_timer) {
      auto task = it.second.cleanup_timer->user_task();
      it.second.cleanup_timer->Stop();
      task.Run();
    }
  }
}

void ChromeFileSystemAccessPermissionContext::MaybeCleanupPermissions(
    const url::Origin& origin) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Iterate over all top-level frames by iterating over all tabs in all browser
  // windows. This also counts PWAs in windows without tab strips.
#if BUILDFLAG(IS_ANDROID)
  for (TabModel* tabs : TabModelList::models()) {
    if (tabs->GetProfile() != profile()) {
      continue;
    }
    int tab_count = tabs->GetTabCount();
    for (int i = 0; i < tab_count; ++i) {
      content::WebContents* web_contents = tabs->GetWebContentsAt(i);
      if (!web_contents) {
        continue;
      }
      url::Origin tab_origin = url::Origin::Create(
          permissions::PermissionUtil::GetLastCommittedOriginAsURL(
              web_contents->GetPrimaryMainFrame()));
      // Found a tab for this origin, so early exit and don't revoke grants.
      if (tab_origin == origin) {
        return;
      }
    }
  }
#else
  bool found_origin = false;
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [this, &origin,
       &found_origin](BrowserWindowInterface* browser_window_interface) {
        if (browser_window_interface->GetProfile() != profile()) {
          return true;
        }
        TabStripModel* tabs = browser_window_interface->GetTabStripModel();
        int tab_count = tabs->count();
        for (int i = 0; i < tab_count; ++i) {
          content::WebContents* web_contents = tabs->GetWebContentsAt(i);
          if (!web_contents) {
            continue;
          }
          url::Origin tab_origin = url::Origin::Create(
              permissions::PermissionUtil::GetLastCommittedOriginAsURL(
                  web_contents->GetPrimaryMainFrame()));
          // Found a tab for this origin, so early exit and don't revoke grants.
          if (tab_origin == origin) {
            found_origin = true;
            return false;
          }
        }
        return true;
      });
  if (found_origin) {
    return;
  }
#endif

  CleanupPermissions(origin);
}

void ChromeFileSystemAccessPermissionContext::CleanupPermissions(
    const url::Origin& origin) {
  // TODO(crbug.com/40101962): Remove this custom implementation to handle site
  // navigation, with the launch of Persistent Permissions.
  if (base::FeatureList::IsEnabled(
          features::kFileSystemAccessPersistentPermissions)) {
    // Clear the grants that should not be carried across sessions.
    if (!OriginHasExtendedPermission(origin) &&
        GetPersistedGrantStatus(origin) == PersistedGrantStatus::kLoaded) {
      RevokeObjectPermissions(origin);
    }
    // Reset the persisted grant status to the default state.
    SetPersistedGrantStatus(origin, PersistedGrantStatus::kLoaded);
  }
  // Revoke the active grants, setting the status to `ASK`.
  if (RevokeActiveGrants(origin)) {
    ScheduleUsageIconUpdate();
  }
}

void ChromeFileSystemAccessPermissionContext::
    OnRestorePermissionAllowedEveryTime(const url::Origin& origin) {
  UpdateGrantsOnRestorePermissionAllowed(origin);
  content_settings_->SetContentSettingDefaultScope(
      origin.GetURL(), origin.GetURL(),
      ContentSettingsType::FILE_SYSTEM_ACCESS_EXTENDED_PERMISSION,
      ContentSetting::CONTENT_SETTING_ALLOW);
}

void ChromeFileSystemAccessPermissionContext::OnRestorePermissionAllowedOnce(
    const url::Origin& origin) {
  UpdateGrantsOnRestorePermissionAllowed(origin);
}

void ChromeFileSystemAccessPermissionContext::
    UpdateGrantsOnRestorePermissionAllowed(const url::Origin& origin) {
  // Set `PersistedGrantStatus::kCurrent` so that Persisted grants are now
  // updated from dormant grants to extended/shadow grants.
  SetPersistedGrantStatus(origin, PersistedGrantStatus::kCurrent);

  auto it = active_permissions_map_.find(origin);
  if (it == active_permissions_map_.end()) {
    return;
  }
  // Use the persisted grants to find the matching active permission, and
  // set it to `granted`.
  for (const auto& dormant_grant :
       ObjectPermissionContextBase::GetGrantedObjects(origin)) {
    const base::DictValue& object_dict = dormant_grant->value;
    base::FilePath path =
        base::ValueToFilePath(object_dict.Find(kPermissionPathKey)).value();
    auto handle_type = object_dict.FindBool(kPermissionIsDirectoryKey).value()
                           ? HandleType::kDirectory
                           : HandleType::kFile;
    if (object_dict.FindBool(kPermissionReadableKey).value_or(false)) {
      auto& read_grant = it->second.read_grants[path];
      if (read_grant && read_grant->handle_type() == handle_type) {
        read_grant->SetStatus(
            PermissionStatus::GRANTED,
            PersistedPermissionOptions::kDoNotUpdatePersistedPermission);
      }
    }
    if (object_dict.FindBool(kPermissionWritableKey).value_or(false)) {
      auto& write_grant = it->second.write_grants[path];
      if (write_grant && write_grant->handle_type() == handle_type) {
        write_grant->SetStatus(
            PermissionStatus::GRANTED,
            PersistedPermissionOptions::kDoNotUpdatePersistedPermission);
      }
    }
  }
}

void ChromeFileSystemAccessPermissionContext::
    OnRestorePermissionDeniedOrDismissed(const url::Origin& origin) {
  // Both denying and dismissing the restore prompt count as a `dismiss`
  // action, for embargo purposes.
  PermissionDecisionAutoBlockerFactory::GetForProfile(
      Profile::FromBrowserContext(profile()))
      ->RecordDismissAndEmbargo(
          origin.GetURL(),
          ContentSettingsType::FILE_SYSTEM_ACCESS_RESTORE_PERMISSION, false);
  UpdateGrantsOnRestorePermissionNotAllowed(origin);
}

void ChromeFileSystemAccessPermissionContext::OnRestorePermissionIgnored(
    const url::Origin& origin) {
  PermissionDecisionAutoBlockerFactory::GetForProfile(
      Profile::FromBrowserContext(profile()))
      ->RecordIgnoreAndEmbargo(
          origin.GetURL(),
          ContentSettingsType::FILE_SYSTEM_ACCESS_RESTORE_PERMISSION, false);
  UpdateGrantsOnRestorePermissionNotAllowed(origin);
}

void ChromeFileSystemAccessPermissionContext::
    UpdateGrantsOnRestorePermissionNotAllowed(const url::Origin& origin) {
  SetPersistedGrantStatus(origin, PersistedGrantStatus::kCurrent);
  // Revoke all of the persistent permissions for the given origin.
  if (!OriginHasExtendedPermission(origin)) {
    ObjectPermissionContextBase::RevokeObjectPermissions(origin);
  }
}

void ChromeFileSystemAccessPermissionContext::
    UpdateGrantsOnPermissionRequestResult(const url::Origin& origin) {
  if (!base::FeatureList::IsEnabled(
          features::kFileSystemAccessPersistentPermissions)) {
    return;
  }

  if (GetPersistedGrantStatus(origin) != PersistedGrantStatus::kCurrent) {
    // Requesting permission triggered the regular permission prompt, not the
    // restore permission prompt. Clear persisted grants and reset the grant
    // status so that dormant grants are not carried over to the next session.
    SetPersistedGrantStatus(origin, PersistedGrantStatus::kCurrent);
    if (!OriginHasExtendedPermission(origin)) {
      ObjectPermissionContextBase::RevokeObjectPermissions(origin);
    }
  }
}

bool ChromeFileSystemAccessPermissionContext::AncestorHasActivePermission(
    const url::Origin& origin,
    const base::FilePath& path,
    GrantType grant_type) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  auto it = active_permissions_map_.find(origin);
  if (it == active_permissions_map_.end()) {
    return false;
  }
  const auto& relevant_grants = grant_type == GrantType::kWrite
                                    ? it->second.write_grants
                                    : it->second.read_grants;
  if (relevant_grants.empty()) {
    return false;
  }

  // Permissions are inherited from the closest ancestor.
  for (base::FilePath parent = path.DirName(); parent != parent.DirName();
       parent = parent.DirName()) {
    auto i = relevant_grants.find(parent);
    if (i != relevant_grants.end() && i->second &&
        HasGrantedActivePermissionStatus(i->second)) {
      return true;
    }
  }
  return false;
}

bool ChromeFileSystemAccessPermissionContext::HasGrantedActivePermissionStatus(
    const PermissionGrantImpl* grant) const {
  return grant &&
         grant->GetActivePermissionStatus() == PermissionStatus::GRANTED;
}

bool ChromeFileSystemAccessPermissionContext::
    IsEligibleToUpgradePermissionRequestToRestorePrompt(
        const url::Origin& origin,
        const base::FilePath& file_path,
        HandleType handle_type,
        UserAction user_action,
        GrantType grant_type) {
#if BUILDFLAG(IS_ANDROID)
  // TODO(crbug.com/40101963): Enable when android persisted permissions are
  // implemented.
  return false;
#else
  if (!base::FeatureList::IsEnabled(
          features::kFileSystemAccessPersistentPermissions)) {
    return false;
  }
  const bool origin_is_embargoed =
      PermissionDecisionAutoBlockerFactory::GetForProfile(
          Profile::FromBrowserContext(profile()))
          ->IsEmbargoed(
              origin.GetURL(),
              ContentSettingsType::FILE_SYSTEM_ACCESS_RESTORE_PERMISSION);
  if (origin_is_embargoed) {
    return false;
  }

  if (GetPersistedGrantType(origin) != PersistedGrantType::kDormant) {
    return false;
  }

#if BUILDFLAG(ENABLE_PLATFORM_APPS)
  // The restore prompt is not displayed when there is a platform app installed,
  // because there is no valid UI element to display the restore prompt from.
  const extensions::ExtensionRegistry* registry =
      extensions::ExtensionRegistry::Get(profile());
  const extensions::Extension* app =
      registry ? registry->enabled_extensions().GetExtensionOrAppByURL(
                     origin.GetURL())
               : nullptr;
  if (app && app->is_platform_app()) {
    return false;
  }
#endif

  // While this method is called from `RequestPermission`, which implies that
  // a `PermissionGrantImpl` exists - we want to insert the origin into the
  // permissions map if it does not exist, in order to cover cases of shutdown
  // or page navigation.
  auto& origin_state = active_permissions_map_[origin];

  // If an origin's grants have been revoked from being backgrounded, or
  // the permission request is on a handle retrieved from IndexedDB, then
  // the restore prompt may be eligible if requesting a permission on a handle,
  // which is previously granted (i.e. dormant grant exists for this file path).
  if (origin_state.persisted_grant_status ==
          PersistedGrantStatus::kBackgrounded ||
      user_action == UserAction::kLoadFromStorage) {
    return HasPersistedGrantObject(origin, file_path, handle_type, grant_type);
  }

  return false;
#endif  // BUILDFLAG(IS_ANDROID)
}

std::vector<FileRequestData> ChromeFileSystemAccessPermissionContext::
    GetFileRequestDataForRestorePermissionPrompt(const url::Origin& origin) {
  std::vector<FileRequestData> file_request_data_list;
  auto dormant_grants = ObjectPermissionContextBase::GetGrantedObjects(origin);
  for (const auto& dormant_grant : dormant_grants) {
    if (!IsValidObject(dormant_grant->value)) {
      continue;
    }
    const base::DictValue& object_dict = dormant_grant->value;
    base::FilePath path =
        base::ValueToFilePath(object_dict.Find(kPermissionPathKey)).value();
    std::string display_name =
        StringOrEmpty(object_dict.FindString(kPermissionDisplayNameKey));
    file_request_data_list.emplace_back(
        content::PathInfo(path, !display_name.empty()
                                    ? display_name
                                    : path.BaseName().AsUTF8Unsafe()),
        object_dict.FindBool(kPermissionIsDirectoryKey).value_or(false)
            ? HandleType::kDirectory
            : HandleType::kFile,
        object_dict.FindBool(kPermissionWritableKey).value_or(false)
            ? RequestAccess::kWrite
            : RequestAccess::kRead);
  }
  return file_request_data_list;
}

bool ChromeFileSystemAccessPermissionContext::HasPersistedGrantObject(
    const url::Origin& origin,
    const base::FilePath& file_path,
    HandleType handle_type,
    GrantType grant_type) {
  auto persisted_grants =
      ObjectPermissionContextBase::GetGrantedObjects(origin);
  return std::ranges::any_of(persisted_grants, [&](const auto& object) {
    return HasMatchingValue(object->value, file_path, handle_type, grant_type);
  });
}

bool ChromeFileSystemAccessPermissionContext::HasMatchingValue(
    const base::DictValue& value,
    const base::FilePath& file_path,
    HandleType handle_type,
    GrantType grant_type) {
  return ValueToFilePath(value.Find(kPermissionPathKey)).value() == file_path &&
         value.FindBool(kPermissionIsDirectoryKey).value_or(false) ==
             (handle_type == HandleType::kDirectory) &&
         value.FindBool(GetGrantKeyFromGrantType(grant_type)).value_or(false);
}

void ChromeFileSystemAccessPermissionContext::
    SetOriginHasExtendedPermissionForTesting(const url::Origin& origin) {
  CHECK(base::FeatureList::IsEnabled(
      features::kFileSystemAccessPersistentPermissions));
  content_settings_->SetContentSettingDefaultScope(
      origin.GetURL(), origin.GetURL(),
      ContentSettingsType::FILE_SYSTEM_ACCESS_EXTENDED_PERMISSION,
      ContentSetting::CONTENT_SETTING_ALLOW);
}

scoped_refptr<content::FileSystemAccessPermissionGrant>
ChromeFileSystemAccessPermissionContext::
    GetExtendedReadPermissionGrantForTesting(  // IN-TEST
        const url::Origin& origin,
        const content::PathInfo& path_info,
        HandleType handle_type) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  auto grant =
      GetReadPermissionGrant(origin, path_info, handle_type, UserAction::kOpen);

  static_cast<PermissionGrantImpl*>(grant.get())
      ->SetStatus(PermissionStatus::GRANTED,
                  PersistedPermissionOptions::kUpdatePersistedPermission);
  return grant;
}

scoped_refptr<content::FileSystemAccessPermissionGrant>
ChromeFileSystemAccessPermissionContext::
    GetExtendedWritePermissionGrantForTesting(  // IN-TEST
        const url::Origin& origin,
        const content::PathInfo& path_info,
        HandleType handle_type) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  auto grant = GetWritePermissionGrant(origin, path_info, handle_type,
                                       UserAction::kSave);

  static_cast<PermissionGrantImpl*>(grant.get())
      ->SetStatus(PermissionStatus::GRANTED,
                  PersistedPermissionOptions::kUpdatePersistedPermission);
  return grant;
}

base::AutoReset<std::optional<base::FilePath>>
ChromeFileSystemAccessPermissionContext::OverrideProfilePathForTesting(
    const base::FilePath& profile_path_override) {
  return base::AutoReset<std::optional<base::FilePath>>(&profile_path_override_,
                                                        profile_path_override);
}

void ChromeFileSystemAccessPermissionContext::Shutdown() {
  FlushScheduledSaveSettingsCalls();
  permissions::ObjectPermissionContextBase::Shutdown();
}

bool ChromeFileSystemAccessPermissionContext::
    CanAutoGrantViaPersistentPermission(const url::Origin& origin,
                                        const base::FilePath& path,
                                        HandleType handle_type,
                                        GrantType grant_type) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!base::FeatureList::IsEnabled(
          features::kFileSystemAccessPersistentPermissions)) {
    return false;
  }

  auto persisted_grant_type = GetPersistedGrantType(origin);
  if (!(persisted_grant_type == PersistedGrantType::kExtended ||
        persisted_grant_type == PersistedGrantType::kShadow)) {
    // Only shadow or extended grants are auto-granted.
    return false;
  }

  auto object = GetGrantedObject(origin, PathAsPermissionKey(path));
  return object &&
         HasMatchingValue(object->value, path, handle_type, grant_type);
}

bool ChromeFileSystemAccessPermissionContext::
    CanAutoGrantViaAncestorPersistentPermission(const url::Origin& origin,
                                                const base::FilePath& path,
                                                GrantType grant_type) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!base::FeatureList::IsEnabled(
          features::kFileSystemAccessPersistentPermissions)) {
    return false;
  }

  auto persisted_grant_type = GetPersistedGrantType(origin);
  if (!(persisted_grant_type == PersistedGrantType::kExtended ||
        persisted_grant_type == PersistedGrantType::kShadow)) {
    // Only shadow or extended grants are auto-granted.
    return false;
  }

  if (GetGrantedObjects(origin).empty()) {
    // Return early if the origin does not have any grant objects.
    return false;
  }

  for (base::FilePath parent = path.DirName(); parent != parent.DirName();
       parent = parent.DirName()) {
    auto object = GetGrantedObject(origin, PathAsPermissionKey(parent));
    if (object && HasMatchingValue(object->value, parent,
                                   HandleType::kDirectory, grant_type)) {
      return true;
    }
  }
  return false;
}

bool ChromeFileSystemAccessPermissionContext::OriginHasExtendedPermission(
    const url::Origin& origin) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

#if BUILDFLAG(IS_ANDROID)
  // TODO(crbug.com/40101963): Enable when android persisted permissions are
  // implemented.
  return false;
#else
  if (!base::FeatureList::IsEnabled(
          features::kFileSystemAccessPersistentPermissions)) {
    return false;
  }

  auto content_setting_value = content_settings_->GetContentSetting(
      origin.GetURL(), origin.GetURL(),
      ContentSettingsType::FILE_SYSTEM_ACCESS_EXTENDED_PERMISSION);
  if (content_setting_value == ContentSetting::CONTENT_SETTING_ALLOW) {
    return true;
  }
  if (content_setting_value == ContentSetting::CONTENT_SETTING_BLOCK) {
    return false;
  }

  // If user has not set the extended permission preference, the extended
  // permission state depends on whether the origin has an web app actively
  // installed. First, check the cached value.
  auto& origin_state = active_permissions_map_[origin];
  if (origin_state.web_app_install_status != WebAppInstallStatus::kUnknown) {
    return origin_state.web_app_install_status ==
           WebAppInstallStatus::kInstalled;
  }
  // No cached value for web app install status. Retrieve the install status.
  DCHECK(profile());
  auto* web_app_provider = web_app::WebAppProvider::GetForWebApps(
      Profile::FromBrowserContext(profile()));
  if (!web_app_provider) {
    return false;
  }
  auto app_id = web_app_provider->registrar_unsafe().FindBestAppWithUrlInScope(
      origin.GetURL(), web_app::WebAppFilter::InstalledInChrome());

  auto app_has_os_integration =
      app_id.has_value() &&
      web_app_provider->registrar_unsafe().GetInstallState(app_id.value()) ==
          web_app::proto::InstallState::INSTALLED_WITH_OS_INTEGRATION;
  // Update the cached value.
  origin_state.web_app_install_status = app_has_os_integration
                                            ? WebAppInstallStatus::kInstalled
                                            : WebAppInstallStatus::kUninstalled;
  return app_has_os_integration;
#endif  // BUILDFLAG(IS_ANDROID)
}

void ChromeFileSystemAccessPermissionContext::SetOriginExtendedPermissionByUser(
    const url::Origin& origin) {
  if (!base::FeatureList::IsEnabled(
          features::kFileSystemAccessPersistentPermissions)) {
    return;
  }
  const bool has_extended_permission = OriginHasExtendedPermission(origin);
  content_settings_->SetContentSettingDefaultScope(
      origin.GetURL(), origin.GetURL(),
      ContentSettingsType::FILE_SYSTEM_ACCESS_EXTENDED_PERMISSION,
      ContentSetting::CONTENT_SETTING_ALLOW);
  // Only update object permissions in the case that the origin did not
  // already have extended permissions.
  if (!has_extended_permission) {
    UpgradeToExtendedPermission(origin);
  }
}

void ChromeFileSystemAccessPermissionContext::
    RemoveOriginExtendedPermissionByUser(const url::Origin& origin) {
  if (!base::FeatureList::IsEnabled(
          features::kFileSystemAccessPersistentPermissions)) {
    return;
  }
  const bool has_extended_permission = OriginHasExtendedPermission(origin);
  content_settings_->SetContentSettingDefaultScope(
      origin.GetURL(), origin.GetURL(),
      ContentSettingsType::FILE_SYSTEM_ACCESS_EXTENDED_PERMISSION,
      ContentSetting::CONTENT_SETTING_BLOCK);
  // Only update object permissions in the case that the origin already had
  // extended permissions.
  if (has_extended_permission) {
    RemoveExtendedPermission(origin);
  }
}

void ChromeFileSystemAccessPermissionContext::RemoveExtendedPermission(
    const url::Origin& origin) {
  auto origin_it = active_permissions_map_.find(origin);
  if (origin_it == active_permissions_map_.end()) {
    // Ignore the origin if it does not have any active permissions.
    return;
  }
  OriginState& origin_state = origin_it->second;
  // Re-create shadow grants based on active grants.
  RevokeObjectPermissions(origin);
  CreatePersistedGrantsFromActiveGrants(origin);
  ScheduleUsageIconUpdate();
  origin_state.persisted_grant_status = PersistedGrantStatus::kCurrent;
}

void ChromeFileSystemAccessPermissionContext::UpgradeToExtendedPermission(
    const url::Origin& origin) {
  auto origin_it = active_permissions_map_.find(origin);
  if (origin_it == active_permissions_map_.end()) {
    // Ignore the origin if it does not have any active permissions.
    return;
  }
  OriginState& origin_state = origin_it->second;
  if (origin_state.persisted_grant_status == PersistedGrantStatus::kCurrent) {
    // Previously, the given origin's persisted grants were shadow grants, and
    // installing a WebApp or enabling extended permissions from the Page Info
    // UI promotes these grants to extended grants.
    // The persisted grants are not affected, given that they are now
    // considered extended grants.
    return;
  }
  // Previously, the given origin's persisted grants were dormant grants and
  // therefore should not be promoted to extended grants. The dormant grants
  // are cleared so that they cannot be considered extended grants.
  RevokeObjectPermissions(origin);
  ScheduleUsageIconUpdate();
  origin_state.persisted_grant_status = PersistedGrantStatus::kCurrent;
}

ChromeFileSystemAccessPermissionContext::PersistedGrantType
ChromeFileSystemAccessPermissionContext::GetPersistedGrantType(
    const url::Origin& origin) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (OriginHasExtendedPermission(origin)) {
    return PersistedGrantType::kExtended;
  }

  switch (GetPersistedGrantStatus(origin)) {
    case PersistedGrantStatus::kBackgrounded:
    case PersistedGrantStatus::kLoaded:
      return PersistedGrantType::kDormant;
    case PersistedGrantStatus::kCurrent:
      return PersistedGrantType::kShadow;
  }
}

PersistedGrantStatus
ChromeFileSystemAccessPermissionContext::GetPersistedGrantStatus(
    const url::Origin& origin) const {
  auto origin_it = active_permissions_map_.find(origin);
  if (origin_it != active_permissions_map_.end()) {
    return origin_it->second.persisted_grant_status;
  }
  // Return the default persisted grant status in the case that the origin is
  // not found in the active permissions map.
  return PersistedGrantStatus::kLoaded;
}

void ChromeFileSystemAccessPermissionContext::SetPersistedGrantStatus(
    const url::Origin& origin,
    PersistedGrantStatus persisted_grant_status) {
  // Insert the origin into the permissions map if it does not exist.
  auto& origin_state = active_permissions_map_[origin];
  origin_state.persisted_grant_status = persisted_grant_status;
}

void ChromeFileSystemAccessPermissionContext::PermissionGrantDestroyed(
    PermissionGrantImpl* grant) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  auto it = active_permissions_map_.find(grant->origin());
  if (it == active_permissions_map_.end()) {
    return;
  }

  auto& grants = grant->type() == GrantType::kRead ? it->second.read_grants
                                                   : it->second.write_grants;
  auto grant_it = grants.find(grant->GetPath());
  // Any non-denied permission grants should have still been in our grants
  // list. If this invariant is violated we would have permissions that might
  // be granted but won't be visible in any UI because the permission context
  // isn't tracking them anymore.
  if (grant_it == grants.end()) {
    DCHECK_EQ(PermissionStatus::DENIED, grant->GetActivePermissionStatus());
    return;
  }

  // The grant in |grants| for this path might have been replaced with a
  // different grant. Only erase if it actually matches the grant that was
  // destroyed.
  if (grant_it->second == grant) {
    grants.erase(grant_it);
  }

  ScheduleUsageIconUpdate();
}

void ChromeFileSystemAccessPermissionContext::ScheduleUsageIconUpdate() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (usage_icon_update_scheduled_) {
    return;
  }
  usage_icon_update_scheduled_ = true;
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(
          &ChromeFileSystemAccessPermissionContext::DoUsageIconUpdate,
          weak_factory_.GetWeakPtr()));
}

void ChromeFileSystemAccessPermissionContext::DoUsageIconUpdate() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  usage_icon_update_scheduled_ = false;
#if !BUILDFLAG(IS_ANDROID)
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [this](BrowserWindowInterface* browser_window_interface) {
        if (browser_window_interface->GetProfile() != profile()) {
          return true;
        }
        tabs::TabInterface* const tab_interface =
            browser_window_interface->GetActiveTabInterface();
        // TODO(crbug.com/411109399): DoUsageIconUpdate() can be run during
        // browser destruction, and therefore we need to check for null here.
        // This should be updated to never run during browser destruction.
        if (!tab_interface) {
          return true;
        }
        auto* const tab_features = tab_interface->GetTabFeatures();
        CHECK(tab_features);
        UpdatePageAction(
            tab_features->file_system_access_page_action_controller());
        return true;
      });
#endif
}

base::WeakPtr<ChromeFileSystemAccessPermissionContext>
ChromeFileSystemAccessPermissionContext::GetWeakPtr() {
  return weak_factory_.GetWeakPtr();
}

#if !BUILDFLAG(IS_ANDROID)
void ChromeFileSystemAccessPermissionContext::UpdatePageAction(
    FileSystemAccessPageActionController* controller) {
  CHECK(controller);
  controller->UpdateVisibility();
}

bool ChromeFileSystemAccessPermissionContext::
    IsPathInDowngradedReadPathsForTesting(const url::Origin& origin,
                                          const base::FilePath& path) const {
  auto it = active_permissions_map_.find(origin);
  if (it == active_permissions_map_.end()) {
    return false;
  }
  return it->second.downgraded_read_paths.contains(path);
}
#endif
