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

#include "storage/browser/file_system/file_system_context.h"

#include <stddef.h>

#include <array>

#include "base/files/scoped_temp_dir.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/stringprintf.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "build/build_config.h"
#include "components/services/storage/public/cpp/quota_error_or.h"
#include "storage/browser/file_system/external_mount_points.h"
#include "storage/browser/file_system/file_system_backend.h"
#include "storage/browser/file_system/isolated_context.h"
#include "storage/browser/file_system/open_file_system_mode.h"
#include "storage/browser/quota/quota_manager_proxy.h"
#include "storage/browser/test/mock_quota_manager.h"
#include "storage/browser/test/mock_special_storage_policy.h"
#include "storage/browser/test/test_file_system_backend.h"
#include "storage/browser/test/test_file_system_options.h"
#include "testing/gtest/include/gtest/gtest.h"

#define FPL(x) FILE_PATH_LITERAL(x)

#if defined(FILE_PATH_USES_DRIVE_LETTERS)
#define DRIVE FPL("C:")
#else
#define DRIVE
#endif

namespace storage {

namespace {

const char kTestOrigin[] = "http://chromium.org/";

GURL CreateRawFileSystemURL(const std::string& type_str,
                            const std::string& fs_id) {
  std::string url_str =
      base::StringPrintf("filesystem:http://chromium.org/%s/%s/root/file",
                         type_str.c_str(), fs_id.c_str());
  return GURL(url_str);
}

class FileSystemContextTest : public testing::Test {
 public:
  FileSystemContextTest() = default;

  void SetUp() override {
    ASSERT_TRUE(data_dir_.CreateUniqueTempDir());

    storage_policy_ = base::MakeRefCounted<MockSpecialStoragePolicy>();

    mock_quota_manager_ = base::MakeRefCounted<MockQuotaManager>(
        false /* is_incognito */, data_dir_.GetPath(),
        base::SingleThreadTaskRunner::GetCurrentDefault().get(),
        storage_policy_.get());
  }

 protected:
  scoped_refptr<FileSystemContext> CreateFileSystemContextForTest(
      scoped_refptr<ExternalMountPoints> external_mount_points) {
    std::vector<std::unique_ptr<storage::FileSystemBackend>>
        additional_providers;
    additional_providers.push_back(std::make_unique<TestFileSystemBackend>(
        base::SingleThreadTaskRunner::GetCurrentDefault().get(),
        data_dir_.GetPath()));
    return FileSystemContext::Create(
        base::SingleThreadTaskRunner::GetCurrentDefault(),
        base::SingleThreadTaskRunner::GetCurrentDefault(),
        std::move(external_mount_points), storage_policy_,
        mock_quota_manager_->proxy(), std::move(additional_providers),
        std::vector<URLRequestAutoMountHandler>(), data_dir_.GetPath(),
        CreateAllowFileAccessOptions());
  }

  QuotaManagerProxy* proxy() { return mock_quota_manager_->proxy(); }

  // Verifies a *valid* filesystem url has expected values.
  void ExpectFileSystemURLMatches(const FileSystemURL& url,
                                  const GURL& expect_origin,
                                  FileSystemType expect_mount_type,
                                  FileSystemType expect_type,
                                  const base::FilePath& expect_path,
                                  const base::FilePath& expect_virtual_path,
                                  const std::string& expect_filesystem_id) {
    EXPECT_TRUE(url.is_valid());

    EXPECT_EQ(expect_origin, url.origin().GetURL());
    EXPECT_EQ(expect_mount_type, url.mount_type());
    EXPECT_EQ(expect_type, url.type());
    EXPECT_EQ(expect_path, url.path());
    EXPECT_EQ(expect_virtual_path, url.virtual_path());
    EXPECT_EQ(expect_filesystem_id, url.filesystem_id());
  }

  inline static std::optional<FileSystemURL> last_resolved_url_ = std::nullopt;

 private:
  base::ScopedTempDir data_dir_;
  base::test::TaskEnvironment task_environment_;
  scoped_refptr<SpecialStoragePolicy> storage_policy_;
  scoped_refptr<MockQuotaManager> mock_quota_manager_;

  // Mock FileSystemBackend implementation which saves the last resolved URL.
  class TestFileSystemBackend : public storage::TestFileSystemBackend {
   public:
    TestFileSystemBackend(base::SequencedTaskRunner* task_runner,
                          const base::FilePath& base_path)
        : storage::TestFileSystemBackend(task_runner, base_path) {}

    void ResolveURL(const FileSystemURL& url,
                    OpenFileSystemMode mode,
                    ResolveURLCallback callback) override {
      last_resolved_url_ = url;
    }
  };
};

// It is not valid to pass nullptr ExternalMountPoints to FileSystemContext on
// ChromeOS.
#if !BUILDFLAG(IS_CHROMEOS)
TEST_F(FileSystemContextTest, NullExternalMountPoints) {
  scoped_refptr<FileSystemContext> file_system_context =
      CreateFileSystemContextForTest(/*external_mount_points=*/nullptr);

  // Cracking system external mount and isolated mount points should work.
  std::string isolated_name = "root";
  IsolatedContext::ScopedFSHandle isolated_fs =
      IsolatedContext::GetInstance()->RegisterFileSystemForPath(
          kFileSystemTypeLocal, std::string(),
          base::FilePath(DRIVE FPL("/test/isolated/root")), &isolated_name);
  std::string isolated_id = isolated_fs.id();
  // Register system external mount point.
  ASSERT_TRUE(ExternalMountPoints::GetSystemInstance()->RegisterFileSystem(
      "system", kFileSystemTypeLocal, FileSystemMountOption(),
      base::FilePath(DRIVE FPL("/test/sys/"))));

  FileSystemURL cracked_isolated =
      file_system_context->CrackURLInFirstPartyContext(
          CreateRawFileSystemURL("isolated", isolated_id));

  ExpectFileSystemURLMatches(
      cracked_isolated, GURL(kTestOrigin), kFileSystemTypeIsolated,
      kFileSystemTypeLocal,
      base::FilePath(DRIVE FPL("/test/isolated/root/file"))
          .NormalizePathSeparators(),
      base::FilePath::FromUTF8Unsafe(isolated_id)
          .Append(FPL("root/file"))
          .NormalizePathSeparators(),
      isolated_id);

  FileSystemURL cracked_external =
      file_system_context->CrackURLInFirstPartyContext(
          CreateRawFileSystemURL("external", "system"));

  ExpectFileSystemURLMatches(
      cracked_external, GURL(kTestOrigin), kFileSystemTypeExternal,
      kFileSystemTypeLocal,
      base::FilePath(DRIVE FPL("/test/sys/root/file"))
          .NormalizePathSeparators(),
      base::FilePath(FPL("system/root/file")).NormalizePathSeparators(),
      "system");

  IsolatedContext::GetInstance()->RevokeFileSystem(isolated_id);
  ExternalMountPoints::GetSystemInstance()->RevokeFileSystem("system");
}
#endif  // !defiend(OS_CHROMEOS)

TEST_F(FileSystemContextTest, FileSystemContextKeepsMountPointsAlive) {
  scoped_refptr<ExternalMountPoints> mount_points =
      ExternalMountPoints::CreateRefCounted();

  // Register system external mount point.
  ASSERT_TRUE(mount_points->RegisterFileSystem(
      "system", kFileSystemTypeLocal, FileSystemMountOption(),
      base::FilePath(DRIVE FPL("/test/sys/"))));

  scoped_refptr<FileSystemContext> file_system_context =
      CreateFileSystemContextForTest(std::move(mount_points));

  // FileSystemContext should keep a reference to the |mount_points|, so it
  // should be able to resolve the URL.
  FileSystemURL cracked_external =
      file_system_context->CrackURLInFirstPartyContext(
          CreateRawFileSystemURL("external", "system"));

  ExpectFileSystemURLMatches(
      cracked_external, GURL(kTestOrigin), kFileSystemTypeExternal,
      kFileSystemTypeLocal,
      base::FilePath(DRIVE FPL("/test/sys/root/file"))
          .NormalizePathSeparators(),
      base::FilePath(FPL("system/root/file")).NormalizePathSeparators(),
      "system");

  // No need to revoke the registered filesystem since |mount_points| lifetime
  // is bound to this test.
}

TEST_F(FileSystemContextTest, ResolveURLOnOpenFileSystem_CustomBucket) {
  scoped_refptr<FileSystemContext> file_system_context =
      CreateFileSystemContextForTest(/*external_mount_points=*/nullptr);
  const auto open_callback = base::BindLambdaForTesting(
      [&](const FileSystemURL& root_url, const std::string& name,
          base::File::Error error) { return; });
  const auto storage_key =
      blink::StorageKey::CreateFromStringForTesting("http://host/test.crswap");
  base::test::TestFuture<storage::QuotaErrorOr<storage::BucketInfo>>
      bucket_future;
  proxy()->CreateBucketForTesting(
      storage_key, "custom_bucket",
      base::SequencedTaskRunner::GetCurrentDefault(),
      bucket_future.GetCallback());
  ASSERT_OK_AND_ASSIGN(auto bucket, bucket_future.Take());
  ASSERT_FALSE(last_resolved_url_.has_value());

  file_system_context->ResolveURLOnOpenFileSystemForTesting(
      storage_key, bucket.ToBucketLocator(), kFileSystemTypeTest,
      OpenFileSystemMode::OPEN_FILE_SYSTEM_CREATE_IF_NONEXISTENT,
      std::move(open_callback));
  ASSERT_TRUE(last_resolved_url_.has_value());
  ASSERT_EQ(last_resolved_url_.value().bucket(), bucket.ToBucketLocator());
}

// Verifies that `OpenFileSystem()` rejects attempts to open a filesystem when
// the requested storage bucket belongs to a different `blink::StorageKey`.
//
// The test constructs a spoofed `storage::BucketLocator` pointing to a bucket
// ID owned by another storage key and invokes `OpenFileSystem()`. The operation
// must validate the bucket's actual owner from quota management and fail with
// `base::File::FILE_ERROR_FAILED`.
TEST_F(FileSystemContextTest, OpenFileSystem_OtherStorageKeyBucket) {
  scoped_refptr<FileSystemContext> file_system_context =
      CreateFileSystemContextForTest(/*external_mount_points=*/nullptr);
  base::RunLoop run_loop;
  base::File::Error open_error = base::File::FILE_OK;
  const auto open_callback = base::BindLambdaForTesting(
      [&](const FileSystemURL& root_url, const std::string& name,
          base::File::Error error) {
        open_error = error;
        run_loop.Quit();
      });
  const auto storage_key =
      blink::StorageKey::CreateFromStringForTesting("http://host/test.crswap");
  const auto other_storage_key =
      blink::StorageKey::CreateFromStringForTesting("http://other/test.crswap");
  base::test::TestFuture<storage::QuotaErrorOr<storage::BucketInfo>>
      bucket_future;
  proxy()->CreateBucketForTesting(
      other_storage_key, "custom_bucket",
      base::SequencedTaskRunner::GetCurrentDefault(),
      bucket_future.GetCallback());
  ASSERT_OK_AND_ASSIGN(auto bucket, bucket_future.Take());

  // Construct a bucket locator spoofing ownership under `storage_key` while
  // referencing the bucket ID registered to `other_storage_key`.
  storage::BucketLocator spoofed_bucket = bucket.ToBucketLocator();
  spoofed_bucket.storage_key = storage_key;

  file_system_context->OpenFileSystem(
      storage_key, spoofed_bucket, kFileSystemTypeTemporary,
      OpenFileSystemMode::OPEN_FILE_SYSTEM_CREATE_IF_NONEXISTENT,
      std::move(open_callback));
  run_loop.Run();
  EXPECT_EQ(base::File::FILE_ERROR_FAILED, open_error);
}

TEST_F(FileSystemContextTest, CrackFileSystemURL) {
  scoped_refptr<ExternalMountPoints> external_mount_points =
      ExternalMountPoints::CreateRefCounted();
  scoped_refptr<FileSystemContext> file_system_context =
      CreateFileSystemContextForTest(external_mount_points);

  // Register an isolated mount point.
  std::string isolated_file_system_name = "root";
  IsolatedContext::ScopedFSHandle isolated_fs =
      IsolatedContext::GetInstance()->RegisterFileSystemForPath(
          kFileSystemTypeLocal, std::string(),
          base::FilePath(DRIVE FPL("/test/isolated/root")),
          &isolated_file_system_name);
  const std::string kIsolatedFileSystemID = isolated_fs.id();
  // Register system external mount point.
  ASSERT_TRUE(ExternalMountPoints::GetSystemInstance()->RegisterFileSystem(
      "system", kFileSystemTypeLocal, FileSystemMountOption(),
      base::FilePath(DRIVE FPL("/test/sys/"))));
  ASSERT_TRUE(ExternalMountPoints::GetSystemInstance()->RegisterFileSystem(
      "ext", kFileSystemTypeLocal, FileSystemMountOption(),
      base::FilePath(DRIVE FPL("/test/ext"))));
  // Register a system external mount point with the same name/id as the
  // registered isolated mount point.
  ASSERT_TRUE(ExternalMountPoints::GetSystemInstance()->RegisterFileSystem(
      kIsolatedFileSystemID, kFileSystemTypeLocal, FileSystemMountOption(),
      base::FilePath(DRIVE FPL("/test/system/isolated"))));
  // Add a mount points with the same name as a system mount point to
  // FileSystemContext's external mount points.
  ASSERT_TRUE(external_mount_points->RegisterFileSystem(
      "ext", kFileSystemTypeLocal, FileSystemMountOption(),
      base::FilePath(DRIVE FPL("/test/local/ext/"))));

  const base::FilePath kVirtualPathNoRoot = base::FilePath(FPL("root/file"));

  struct TestCase {
    // Test case values.
    std::string root;
    std::string type_str;

    // Expected test results.
    bool expect_is_valid;
    FileSystemType expect_mount_type;
    FileSystemType expect_type;
    const base::FilePath::CharType* expect_path;
    std::string expect_filesystem_id;
  };

  const auto kTestCases = std::to_array<TestCase>({
      // Following should not be handled by the url crackers:
      {
          "pers_mount", "persistent", true /* is_valid */,
          kFileSystemTypePersistent, kFileSystemTypePersistent,
          FPL("pers_mount/root/file"), std::string() /* filesystem id */
      },
      {
          "temp_mount", "temporary", true /* is_valid */,
          kFileSystemTypeTemporary, kFileSystemTypeTemporary,
          FPL("temp_mount/root/file"), std::string() /* filesystem id */
      },
      // Should be cracked by isolated mount points:
      {kIsolatedFileSystemID, "isolated", true /* is_valid */,
       kFileSystemTypeIsolated, kFileSystemTypeLocal,
       DRIVE FPL("/test/isolated/root/file"), kIsolatedFileSystemID},
      // Should be cracked by system mount points:
      {"system", "external", true /* is_valid */, kFileSystemTypeExternal,
       kFileSystemTypeLocal, DRIVE FPL("/test/sys/root/file"), "system"},
      {kIsolatedFileSystemID, "external", true /* is_valid */,
       kFileSystemTypeExternal, kFileSystemTypeLocal,
       DRIVE FPL("/test/system/isolated/root/file"), kIsolatedFileSystemID},
      // Should be cracked by FileSystemContext's ExternalMountPoints.
      {"ext", "external", true /* is_valid */, kFileSystemTypeExternal,
       kFileSystemTypeLocal, DRIVE FPL("/test/local/ext/root/file"), "ext"},
      // Test for invalid filesystem url (made invalid by adding invalid
      // filesystem type).
      {"sytem", "external", false /* is_valid */,
       // The rest of values will be ignored.
       kFileSystemTypeUnknown, kFileSystemTypeUnknown, FPL(""), std::string()},
      // Test for URL with non-existing filesystem id.
      {"invalid", "external", false /* is_valid */,
       // The rest of values will be ignored.
       kFileSystemTypeUnknown, kFileSystemTypeUnknown, FPL(""), std::string()},
  });

  for (size_t i = 0; i < std::size(kTestCases); ++i) {
    const base::FilePath virtual_path =
        base::FilePath::FromASCII(kTestCases[i].root)
            .Append(kVirtualPathNoRoot);

    GURL raw_url =
        CreateRawFileSystemURL(kTestCases[i].type_str, kTestCases[i].root);
    FileSystemURL cracked_url =
        file_system_context->CrackURLInFirstPartyContext(raw_url);

    SCOPED_TRACE(testing::Message() << "Test case " << i << ": "
                                    << "Cracking URL: " << raw_url);

    EXPECT_EQ(kTestCases[i].expect_is_valid, cracked_url.is_valid());
    if (!kTestCases[i].expect_is_valid)
      continue;

    ExpectFileSystemURLMatches(
        cracked_url, GURL(kTestOrigin), kTestCases[i].expect_mount_type,
        kTestCases[i].expect_type,
        base::FilePath(kTestCases[i].expect_path).NormalizePathSeparators(),
        virtual_path.NormalizePathSeparators(),
        kTestCases[i].expect_filesystem_id);
  }

  IsolatedContext::GetInstance()->RevokeFileSystemByPath(
      base::FilePath(DRIVE FPL("/test/isolated/root")));
  ExternalMountPoints::GetSystemInstance()->RevokeFileSystem("system");
  ExternalMountPoints::GetSystemInstance()->RevokeFileSystem("ext");
  ExternalMountPoints::GetSystemInstance()->RevokeFileSystem(
      kIsolatedFileSystemID);
}

TEST_F(FileSystemContextTest, CanServeURLRequest) {
  scoped_refptr<ExternalMountPoints> external_mount_points =
      ExternalMountPoints::CreateRefCounted();
  scoped_refptr<FileSystemContext> context =
      CreateFileSystemContextForTest(std::move(external_mount_points));

  // A request for a sandbox mount point should be served.
  FileSystemURL cracked_url = context->CrackURLInFirstPartyContext(
      CreateRawFileSystemURL("persistent", "pers_mount"));
  EXPECT_EQ(kFileSystemTypePersistent, cracked_url.mount_type());
  EXPECT_TRUE(context->CanServeURLRequest(cracked_url));

  // A request for an isolated mount point should NOT be served.
  std::string isolated_fs_name = "root";
  IsolatedContext::ScopedFSHandle isolated_fs =
      IsolatedContext::GetInstance()->RegisterFileSystemForPath(
          kFileSystemTypeLocal, std::string(),
          base::FilePath(DRIVE FPL("/test/isolated/root")), &isolated_fs_name);
  std::string isolated_fs_id = isolated_fs.id();
  cracked_url = context->CrackURLInFirstPartyContext(
      CreateRawFileSystemURL("isolated", isolated_fs_id));
  EXPECT_EQ(kFileSystemTypeIsolated, cracked_url.mount_type());
  EXPECT_FALSE(context->CanServeURLRequest(cracked_url));

  // A request for an external mount point should be served.
  const std::string kExternalMountName = "ext_mount";
  ASSERT_TRUE(ExternalMountPoints::GetSystemInstance()->RegisterFileSystem(
      kExternalMountName, kFileSystemTypeLocal, FileSystemMountOption(),
      base::FilePath()));
  cracked_url = context->CrackURLInFirstPartyContext(
      CreateRawFileSystemURL("external", kExternalMountName));
  EXPECT_EQ(kFileSystemTypeExternal, cracked_url.mount_type());
  EXPECT_TRUE(context->CanServeURLRequest(cracked_url));

  ExternalMountPoints::GetSystemInstance()->RevokeFileSystem(
      kExternalMountName);
  IsolatedContext::GetInstance()->RevokeFileSystem(isolated_fs_id);
}

// Ensures that a backend exists for each common isolated file system type.
// See http://crbug.com/447027
TEST_F(FileSystemContextTest, IsolatedFileSystemsTypesHandled) {
  // This does not provide any "additional" file system handlers. In particular,
  // on Chrome OS it does not provide ash::FileSystemBackend.
  scoped_refptr<FileSystemContext> file_system_context =
      CreateFileSystemContextForTest(/*external_mount_points=*/nullptr);

  // Isolated file system types are handled.
  EXPECT_TRUE(
      file_system_context->GetFileSystemBackend(kFileSystemTypeIsolated));
  EXPECT_TRUE(
      file_system_context->GetFileSystemBackend(kFileSystemTypeDragged));
  EXPECT_TRUE(file_system_context->GetFileSystemBackend(
      kFileSystemTypeForTransientFile));
  EXPECT_TRUE(file_system_context->GetFileSystemBackend(kFileSystemTypeLocal));
  EXPECT_TRUE(file_system_context->GetFileSystemBackend(
      kFileSystemTypeLocalForPlatformApp));
}

}  // namespace

}  // namespace storage
