chromium/content/browser/file_system_access/file_system_access_file_handle_impl_unittest.cc

// 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 "content/browser/file_system_access/file_system_access_file_handle_impl.h"

#include <limits>
#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#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/gmock_callback_support.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "build/build_config.h"
#include "content/browser/file_system_access/features.h"
#include "content/browser/file_system_access/fixed_file_system_access_permission_grant.h"
#include "content/browser/file_system_access/mock_file_system_access_permission_grant.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_web_contents_factory.h"
#include "content/test/test_web_contents.h"
#include "file_system_access_directory_handle_impl.h"
#include "mock_file_system_access_permission_context.h"
#include "net/base/io_buffer.h"
#include "net/base/net_errors.h"
#include "net/base/test_completion_callback.h"
#include "storage/browser/blob/blob_storage_context.h"
#include "storage/browser/file_system/file_stream_reader.h"
#include "storage/browser/quota/quota_manager_proxy.h"
#include "storage/browser/test/async_file_test_helper.h"
#include "storage/browser/test/mock_quota_manager.h"
#include "storage/browser/test/mock_quota_manager_proxy.h"
#include "storage/browser/test/mock_special_storage_policy.h"
#include "storage/browser/test/test_file_system_context.h"
#include "storage/common/file_system/file_system_types.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features_generated.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/blink/public/mojom/permissions/permission_status.mojom-shared.h"
#include "url/gurl.h"

#if BUILDFLAG(IS_ANDROID)
#include "base/android/path_utils.h"
#include "base/strings/escape.h"
#include "base/test/android/content_uri_test_utils.h"
#endif

namespace content {

PermissionStatus;
FileSystemURL;

class FileSystemAccessFileHandleImplTest : public testing::Test {};

class FileSystemAccessAccessHandleTest
    : public FileSystemAccessFileHandleImplTest {};

class FileSystemAccessAccessHandleIncognitoTest
    : public FileSystemAccessAccessHandleTest {};

#if BUILDFLAG(IS_ANDROID)
class FileSystemAccessAccessHandleContentUriTest
    : public FileSystemAccessAccessHandleTest {
  void SetUp() override {
    // AccessHandles are only allowed for temporary file systems.
    SetupHelper(storage::kFileSystemTypeLocal, /*is_incognito=*/false,
                /*use_content_uri=*/true);
  }
};
#endif

TEST_F(FileSystemAccessFileHandleImplTest, CreateFileWriterOverLimitNotOK) {}

TEST_F(FileSystemAccessFileHandleImplTest, CreateFileWriterSiloedMode) {}

TEST_F(FileSystemAccessFileHandleImplTest, CreateFileWriterExclusiveMode) {}

TEST_F(FileSystemAccessFileHandleImplTest,
       CreateFileWriterWithExistingSwapFile) {}

#if BUILDFLAG(IS_ANDROID)
TEST_F(FileSystemAccessAccessHandleContentUriTest, CreateFileWriter) {
  base::test::TestFuture<
      blink::mojom::FileSystemAccessErrorPtr,
      mojo::PendingRemote<blink::mojom::FileSystemAccessFileWriter>>
      future;
  handle_->CreateFileWriter(
      /*keep_existing_data=*/false,
      /*auto_close=*/false,
      blink::mojom::FileSystemAccessWritableFileStreamLockMode::kSiloed,
      future.GetCallback());
  blink::mojom::FileSystemAccessErrorPtr result;
  mojo::PendingRemote<blink::mojom::FileSystemAccessFileWriter> writer_remote;
  std::tie(result, writer_remote) = future.Take();
  EXPECT_EQ(result->status, blink::mojom::FileSystemAccessStatus::kOk);
  EXPECT_TRUE(writer_remote.is_valid());

  // Swap file should be created in cache dir.
  base::FilePath cache_dir;
  EXPECT_TRUE(base::android::GetCacheDirectory(&cache_dir));
  auto escaped = base::EscapeAllExceptUnreserved(test_file_url_.path().value());
  base::FilePath swap = cache_dir.Append("FileSystemAPISwap")
                            .Append(escaped)
                            .AddExtension(".crswap");
  EXPECT_TRUE(base::PathExists(swap));
}
#endif

TEST_F(FileSystemAccessFileHandleImplTest, Remove_NoWriteAccess) {}

TEST_F(FileSystemAccessFileHandleImplTest, Remove_HasWriteAccess) {}

TEST_F(FileSystemAccessAccessHandleTest, OpenAccessHandle) {}

TEST_F(FileSystemAccessAccessHandleIncognitoTest, OpenAccessHandle) {}

TEST_F(FileSystemAccessAccessHandleTest, OpenAccessHandleLockModes_Readwrite) {}

TEST_F(FileSystemAccessAccessHandleTest, OpenAccessHandleLockModes_ReadOnly) {}

TEST_F(FileSystemAccessAccessHandleTest,
       OpenAccessHandleLockModes_ReadwriteUnsafe) {}

TEST_F(FileSystemAccessFileHandleImplTest, Rename_NoWriteAccess) {}

TEST_F(FileSystemAccessFileHandleImplTest, Rename_HasWriteAccess) {}

TEST_F(FileSystemAccessFileHandleImplTest, Move_NoWriteAccess) {}

TEST_F(FileSystemAccessFileHandleImplTest, Move_NoDestWriteAccess) {}

TEST_F(FileSystemAccessFileHandleImplTest, Move_HasDestWriteAccess) {}

#if BUILDFLAG(IS_MAC)
// Tests that swap file cloning (i.e. creating a swap file using underlying
// platform support for copy-on-write files) behaves as expected. Swap file
// cloning requires storage::kFileSystemTypeLocal.
class FileSystemAccessFileHandleSwapFileCloningTest
    : public FileSystemAccessFileHandleImplTest {
 public:
  enum class CloneFileResult {
    kDidNotAttempt,
    kAttemptedAndAborted,
    kAttemptedAndCompletedUnexpectedly,
    kAttemptedAndCompletedAsExpected
  };

  void SetUp() override {
    SetupHelper(storage::kFileSystemTypeLocal, /*is_incognito=*/false,
                /*use_content_uri=*/false);
  }

  CloneFileResult GetCloneFileResult(
      const std::unique_ptr<FileSystemAccessFileHandleImpl>& handle) {
    auto maybe_clone_result = handle->get_swap_file_clone_result_for_testing();

    if (!maybe_clone_result.has_value()) {
      return CloneFileResult::kDidNotAttempt;
    }

    if (maybe_clone_result.value() == base::File::Error::FILE_ERROR_ABORT) {
      return CloneFileResult::kAttemptedAndAborted;
    }

    // We should not attempt to clone the file if the swap file exists. Other
    // errors are okay.
    if (maybe_clone_result.value() == base::File::Error::FILE_ERROR_EXISTS) {
      return CloneFileResult::kAttemptedAndCompletedUnexpectedly;
    }

    // Ideally we could just check that the result is FILE_OK, but
    // clonefile() may spuriously fail. See https://crbug.com/1439179. For the
    // purposes of these tests, we'll consider these spurious errors as
    // "expected".
    return CloneFileResult::kAttemptedAndCompletedAsExpected;
  }
};

TEST_F(FileSystemAccessFileHandleSwapFileCloningTest, BasicClone) {
  base::test::TestFuture<
      blink::mojom::FileSystemAccessErrorPtr,
      mojo::PendingRemote<blink::mojom::FileSystemAccessFileWriter>>
      future;
  handle_->CreateFileWriter(
      /*keep_existing_data=*/true,
      /*auto_close=*/false,
      blink::mojom::FileSystemAccessWritableFileStreamLockMode::kSiloed,
      future.GetCallback());
  blink::mojom::FileSystemAccessErrorPtr result;
  mojo::PendingRemote<blink::mojom::FileSystemAccessFileWriter> writer_remote;
  std::tie(result, writer_remote) = future.Take();
  EXPECT_EQ(result->status, blink::mojom::FileSystemAccessStatus::kOk);
  EXPECT_TRUE(writer_remote.is_valid());
  EXPECT_EQ(GetCloneFileResult(handle_),
            CloneFileResult::kAttemptedAndCompletedAsExpected);
}

TEST_F(FileSystemAccessFileHandleSwapFileCloningTest,
       IgnoringExistingDataDoesNotClone) {
  base::test::TestFuture<
      blink::mojom::FileSystemAccessErrorPtr,
      mojo::PendingRemote<blink::mojom::FileSystemAccessFileWriter>>
      future;
  handle_->CreateFileWriter(
      /*keep_existing_data=*/false,
      /*auto_close=*/false,
      blink::mojom::FileSystemAccessWritableFileStreamLockMode::kSiloed,
      future.GetCallback());
  blink::mojom::FileSystemAccessErrorPtr result;
  mojo::PendingRemote<blink::mojom::FileSystemAccessFileWriter> writer_remote;
  std::tie(result, writer_remote) = future.Take();
  EXPECT_EQ(result->status, blink::mojom::FileSystemAccessStatus::kOk);
  EXPECT_TRUE(writer_remote.is_valid());
  EXPECT_EQ(GetCloneFileResult(handle_), CloneFileResult::kDidNotAttempt);
}

TEST_F(FileSystemAccessFileHandleSwapFileCloningTest, HandleExistingSwapFile) {
  const FileSystemURL swap_url =
      file_system_context_->CreateCrackedFileSystemURL(
          test_src_storage_key_, storage::kFileSystemTypeLocal,
          dir_.GetPath().AppendASCII("test.crswap"));

  // Create pre-existing swap file.
  ASSERT_EQ(base::File::FILE_OK, storage::AsyncFileTestHelper::CreateFile(
                                     file_system_context_.get(), swap_url));

  // Creating the writer still succeeds, even though clonefile() will fail if
  // the destination file already exists.
  base::test::TestFuture<
      blink::mojom::FileSystemAccessErrorPtr,
      mojo::PendingRemote<blink::mojom::FileSystemAccessFileWriter>>
      future;
  handle_->CreateFileWriter(
      /*keep_existing_data=*/true,
      /*auto_close=*/false,
      blink::mojom::FileSystemAccessWritableFileStreamLockMode::kSiloed,
      future.GetCallback());
  blink::mojom::FileSystemAccessErrorPtr result;
  mojo::PendingRemote<blink::mojom::FileSystemAccessFileWriter> writer_remote;
  std::tie(result, writer_remote) = future.Take();
  EXPECT_EQ(result->status, blink::mojom::FileSystemAccessStatus::kOk);
  EXPECT_TRUE(writer_remote.is_valid());
  EXPECT_EQ(GetCloneFileResult(handle_),
            CloneFileResult::kAttemptedAndCompletedAsExpected);
}

TEST_F(FileSystemAccessFileHandleSwapFileCloningTest, HandleCloneFailure) {
  handle_->set_swap_file_cloning_will_fail_for_testing();

  // Creating the writer still succeeds, even if cloning fails.
  base::test::TestFuture<
      blink::mojom::FileSystemAccessErrorPtr,
      mojo::PendingRemote<blink::mojom::FileSystemAccessFileWriter>>
      future;
  handle_->CreateFileWriter(
      /*keep_existing_data=*/true,
      /*auto_close=*/false,
      blink::mojom::FileSystemAccessWritableFileStreamLockMode::kSiloed,
      future.GetCallback());
  blink::mojom::FileSystemAccessErrorPtr result;
  mojo::PendingRemote<blink::mojom::FileSystemAccessFileWriter> writer_remote;
  std::tie(result, writer_remote) = future.Take();
  EXPECT_EQ(result->status, blink::mojom::FileSystemAccessStatus::kOk);
  EXPECT_TRUE(writer_remote.is_valid());
  EXPECT_EQ(GetCloneFileResult(handle_), CloneFileResult::kAttemptedAndAborted);
}
#endif  // BUILDFLAG(IS_MAC)

// Uses a mock permission context to ensure the correct permission grant for the
// target file (and parent, for renames) is used, since moves retrieve the
// target's permission grant via GetSharedHandleStateForNonSandboxedPath() which
// always returns GRANTED for tests without a permission context.
//
// Moves do not call GetSharedHandleStateForNonSandboxedPath() on the
// destination directory, so the above tests are sufficient to test moves
// without access to the destination directory. These tests always grant write
// access to the destination directory.
class FileSystemAccessFileHandleImplMovePermissionsTest
    : public FileSystemAccessFileHandleImplTest,
      public testing::WithParamInterface<std::tuple<bool, bool>> {};

TEST_P(FileSystemAccessFileHandleImplMovePermissionsTest,
       Rename_HasTargetWriteAccess) {}

TEST_P(FileSystemAccessFileHandleImplMovePermissionsTest,
       Rename_AskTargetWriteAccess) {}

TEST_P(FileSystemAccessFileHandleImplMovePermissionsTest, Rename_SameFile) {}

TEST_P(FileSystemAccessFileHandleImplMovePermissionsTest, Move) {}

TEST_P(FileSystemAccessFileHandleImplMovePermissionsTest,
       Move_NoTargetWriteAccessFails) {}

TEST_P(FileSystemAccessFileHandleImplMovePermissionsTest, Move_SameFile) {}

INSTANTIATE_TEST_SUITE_P();

}  // namespace content