blob: e1b9e77fa725782052c80feaf74b7a878164ef0c [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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/safe_move_helper.h"
#include <memory>
#include <string>
#include <utility>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/containers/contains.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/guid.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
#include "content/browser/file_system_access/file_system_access_write_lock_manager.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_context.h"
#include "content/browser/file_system_access/safe_move_helper.h"
#include "content/public/test/browser_task_environment.h"
#include "storage/browser/file_system/file_system_operation.h"
#include "storage/browser/quota/quota_manager_proxy.h"
#include "storage/browser/test/async_file_test_helper.h"
#include "storage/browser/test/test_file_system_backend.h"
#include "storage/browser/test/test_file_system_context.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/blink/public/mojom/file_system_access/file_system_access_error.mojom.h"
#include "url/gurl.h"
using blink::mojom::FileSystemAccessStatus;
using storage::FileSystemURL;
using testing::_;
using testing::AllOf;
using testing::Eq;
using testing::Field;
namespace content {
namespace {
class MockQuarantine : public quarantine::mojom::Quarantine {
public:
MockQuarantine() = default;
void QuarantineFile(const base::FilePath& full_path,
const GURL& source_url,
const GURL& referrer_url,
const std::string& client_guid,
QuarantineFileCallback callback) override {
paths.push_back(full_path);
std::move(callback).Run(result);
}
void MakeSecurityCheckFail() {
result = quarantine::mojom::QuarantineFileResult::SECURITY_CHECK_FAILED;
}
std::vector<base::FilePath> paths;
quarantine::mojom::QuarantineFileResult result =
quarantine::mojom::QuarantineFileResult::OK;
};
// File System Backend that can notify whenever a FileSystemOperation is
// created. This lets tests simulate race conditions between file operations and
// other work.
class TestFileSystemBackend : public storage::TestFileSystemBackend {
public:
TestFileSystemBackend(base::SequencedTaskRunner* task_runner,
const base::FilePath& base_path)
: storage::TestFileSystemBackend(task_runner, base_path) {}
std::unique_ptr<storage::FileSystemOperation> CreateFileSystemOperation(
const storage::FileSystemURL& url,
storage::FileSystemContext* context,
base::File::Error* error_code) const override {
if (operation_created_callback_)
std::move(operation_created_callback_).Run(url);
return storage::TestFileSystemBackend::CreateFileSystemOperation(
url, context, error_code);
}
void SetOperationCreatedCallback(
base::OnceCallback<void(const storage::FileSystemURL&)> callback) {
operation_created_callback_ = std::move(callback);
}
private:
mutable base::OnceCallback<void(const storage::FileSystemURL&)>
operation_created_callback_;
};
} // namespace
std::string GetHexEncodedString(const std::string& input) {
return base::HexEncode(base::as_bytes(base::make_span(input)));
}
class SafeMoveHelperTest : public testing::Test {
public:
SafeMoveHelperTest()
: task_environment_(base::test::TaskEnvironment::MainThreadType::IO) {}
virtual FileSystemAccessPermissionContext* permission_context() {
return nullptr;
}
void SetUp() override {
ASSERT_TRUE(dir_.CreateUniqueTempDir());
std::vector<std::unique_ptr<storage::FileSystemBackend>>
additional_providers;
additional_providers.push_back(std::make_unique<TestFileSystemBackend>(
base::ThreadTaskRunnerHandle::Get().get(), dir_.GetPath()));
test_file_system_backend_ =
static_cast<TestFileSystemBackend*>(additional_providers[0].get());
file_system_context_ =
storage::CreateFileSystemContextWithAdditionalProvidersForTesting(
base::ThreadTaskRunnerHandle::Get(),
base::ThreadTaskRunnerHandle::Get(),
/*quota_manager_proxy=*/nullptr, std::move(additional_providers),
dir_.GetPath());
test_dest_url_ = file_system_context_->CreateCrackedFileSystemURL(
kTestStorageKey, storage::kFileSystemTypeLocal,
dir_.GetPath().AppendASCII("dest"));
test_source_url_ = file_system_context_->CreateCrackedFileSystemURL(
kTestStorageKey, storage::kFileSystemTypeLocal,
dir_.GetPath().AppendASCII("source"));
ASSERT_EQ(base::File::FILE_OK,
storage::AsyncFileTestHelper::CreateFile(
file_system_context_.get(), test_source_url_));
chrome_blob_context_ = base::MakeRefCounted<ChromeBlobStorageContext>();
chrome_blob_context_->InitializeOnIOThread(base::FilePath(),
base::FilePath(), nullptr);
blob_context_ = chrome_blob_context_->context();
manager_ = base::MakeRefCounted<FileSystemAccessManagerImpl>(
file_system_context_, chrome_blob_context_,
/*permission_context=*/permission_context(),
/*off_the_record=*/false);
quarantine_callback_ = base::BindLambdaForTesting(
[&](mojo::PendingReceiver<quarantine::mojom::Quarantine> receiver) {
quarantine_receivers_.Add(&quarantine_, std::move(receiver));
});
auto lock = manager_->TakeWriteLock(
test_dest_url_,
FileSystemAccessWriteLockManager::WriteLockType::kShared);
ASSERT_TRUE(lock.has_value());
helper_ = std::make_unique<SafeMoveHelper>(
manager_->AsWeakPtr(),
FileSystemAccessManagerImpl::BindingContext(kTestStorageKey, kTestURL,
kFrameId),
test_source_url_, test_dest_url_,
storage::FileSystemOperation::OPTION_PRESERVE_DESTINATION_PERMISSIONS,
quarantine_callback_,
/*has_transient_user_activation=*/false);
}
void TearDown() override {
manager_.reset();
task_environment_.RunUntilIdle();
EXPECT_TRUE(dir_.Delete());
}
protected:
const GURL kTestURL = GURL("https://example.com/test");
const blink::StorageKey kTestStorageKey =
blink::StorageKey::CreateFromStringForTesting("https://example.com/test");
const int kProcessId = 1;
const int kFrameRoutingId = 2;
const GlobalRenderFrameHostId kFrameId{kProcessId, kFrameRoutingId};
BrowserTaskEnvironment task_environment_;
base::ScopedTempDir dir_;
scoped_refptr<storage::FileSystemContext> file_system_context_;
TestFileSystemBackend* test_file_system_backend_;
scoped_refptr<ChromeBlobStorageContext> chrome_blob_context_;
storage::BlobStorageContext* blob_context_;
scoped_refptr<FileSystemAccessManagerImpl> manager_;
FileSystemURL test_dest_url_;
FileSystemURL test_source_url_;
MockQuarantine quarantine_;
mojo::ReceiverSet<quarantine::mojom::Quarantine> quarantine_receivers_;
download::QuarantineConnectionCallback quarantine_callback_;
scoped_refptr<FixedFileSystemAccessPermissionGrant> permission_grant_ =
base::MakeRefCounted<FixedFileSystemAccessPermissionGrant>(
FixedFileSystemAccessPermissionGrant::PermissionStatus::GRANTED,
base::FilePath());
std::unique_ptr<SafeMoveHelper> helper_;
};
TEST_F(SafeMoveHelperTest, HashSimpleOK) {
EXPECT_TRUE(base::WriteFile(test_source_url_.path(), "abc"));
base::RunLoop loop;
helper_->ComputeHashForSourceFileForTesting(base::BindLambdaForTesting(
[&](base::File::Error result, const std::string& hash_value,
int64_t size) {
EXPECT_EQ(base::File::FILE_OK, result);
EXPECT_EQ(
"BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD",
GetHexEncodedString(hash_value));
EXPECT_EQ(3, size);
loop.Quit();
}));
loop.Run();
}
TEST_F(SafeMoveHelperTest, HashEmptyOK) {
base::RunLoop loop;
helper_->ComputeHashForSourceFileForTesting(base::BindLambdaForTesting(
[&](base::File::Error result, const std::string& hash_value,
int64_t size) {
EXPECT_EQ(base::File::FILE_OK, result);
EXPECT_EQ(
"E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
GetHexEncodedString(hash_value));
EXPECT_EQ(0, size);
loop.Quit();
}));
loop.Run();
}
TEST_F(SafeMoveHelperTest, HashNonExistingFileFails) {
ASSERT_EQ(base::File::FILE_OK,
storage::AsyncFileTestHelper::Remove(file_system_context_.get(),
test_source_url_,
/*recursive=*/false));
base::RunLoop loop;
helper_->ComputeHashForSourceFileForTesting(base::BindLambdaForTesting(
[&](base::File::Error result, const std::string& hash_value,
int64_t size) {
EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND, result);
loop.Quit();
}));
loop.Run();
}
TEST_F(SafeMoveHelperTest, HashLargerFileOK) {
size_t target_size = 9 * 1024u;
std::string file_data(target_size, '0');
EXPECT_TRUE(base::WriteFile(test_source_url_.path(), file_data));
base::RunLoop loop;
helper_->ComputeHashForSourceFileForTesting(base::BindLambdaForTesting(
[&](base::File::Error result, const std::string& hash_value,
int64_t size) {
EXPECT_EQ(base::File::FILE_OK, result);
EXPECT_EQ(
"34A82D28CB1E0BA92CADC4BE8497DC9EEA9AC4F63B9C445A9E52D298990AC491",
GetHexEncodedString(hash_value));
EXPECT_EQ(static_cast<int64_t>(target_size), size);
loop.Quit();
}));
loop.Run();
}
TEST_F(SafeMoveHelperTest, Simple) {
EXPECT_TRUE(base::WriteFile(test_source_url_.path(), "abc"));
base::RunLoop loop;
helper_->Start(base::BindLambdaForTesting(
[&](blink::mojom::FileSystemAccessErrorPtr result) {
EXPECT_EQ(result->status, FileSystemAccessStatus::kOk);
loop.Quit();
}));
loop.Run();
EXPECT_FALSE(storage::AsyncFileTestHelper::FileExists(
file_system_context_.get(), test_source_url_,
storage::AsyncFileTestHelper::kDontCheckSize));
EXPECT_TRUE(storage::AsyncFileTestHelper::FileExists(
file_system_context_.get(), test_dest_url_, 3));
}
TEST_F(SafeMoveHelperTest, DestExists) {
EXPECT_TRUE(base::WriteFile(test_source_url_.path(), "abc"));
ASSERT_EQ(base::File::FILE_OK,
storage::AsyncFileTestHelper::CreateFile(file_system_context_.get(),
test_dest_url_));
EXPECT_TRUE(base::WriteFile(test_dest_url_.path(), "hi"));
base::RunLoop loop;
helper_->Start(base::BindLambdaForTesting(
[&](blink::mojom::FileSystemAccessErrorPtr result) {
EXPECT_EQ(result->status, FileSystemAccessStatus::kOk);
loop.Quit();
}));
loop.Run();
EXPECT_FALSE(storage::AsyncFileTestHelper::FileExists(
file_system_context_.get(), test_source_url_,
storage::AsyncFileTestHelper::kDontCheckSize));
EXPECT_TRUE(storage::AsyncFileTestHelper::FileExists(
file_system_context_.get(), test_dest_url_, 3));
}
TEST_F(SafeMoveHelperTest, SecurityCheckFailed) {
quarantine_.MakeSecurityCheckFail();
EXPECT_TRUE(base::WriteFile(test_source_url_.path(), "abc"));
base::RunLoop loop;
helper_->Start(base::BindLambdaForTesting(
[&](blink::mojom::FileSystemAccessErrorPtr result) {
EXPECT_EQ(result->status, FileSystemAccessStatus::kOperationAborted);
loop.Quit();
}));
loop.Run();
// Even though the file failed quarantine, it's already been moved. There's
// not much we can do other than return an error.
EXPECT_FALSE(storage::AsyncFileTestHelper::FileExists(
file_system_context_.get(), test_source_url_,
storage::AsyncFileTestHelper::kDontCheckSize));
EXPECT_TRUE(storage::AsyncFileTestHelper::FileExists(
file_system_context_.get(), test_dest_url_, 3));
}
class SafeMoveHelperAfterWriteChecksTest : public SafeMoveHelperTest {
public:
FileSystemAccessPermissionContext* permission_context() override {
return &permission_context_;
}
protected:
testing::StrictMock<MockFileSystemAccessPermissionContext>
permission_context_;
};
TEST_F(SafeMoveHelperAfterWriteChecksTest, Allow) {
EXPECT_TRUE(base::WriteFile(test_source_url_.path(), "abc"));
std::string expected_hash;
ASSERT_TRUE(base::HexStringToString(
"BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD",
&expected_hash));
EXPECT_CALL(
permission_context_,
PerformAfterWriteChecks_(
AllOf(
Field(&FileSystemAccessWriteItem::target_file_path,
Eq(test_dest_url_.path())),
Field(&FileSystemAccessWriteItem::full_path,
Eq(test_source_url_.path())),
Field(&FileSystemAccessWriteItem::sha256_hash, Eq(expected_hash)),
Field(&FileSystemAccessWriteItem::size, Eq(3)),
Field(&FileSystemAccessWriteItem::frame_url, Eq(kTestURL)),
Field(&FileSystemAccessWriteItem::has_user_gesture, Eq(false))),
kFrameId, _))
.WillOnce(base::test::RunOnceCallback<2>(
FileSystemAccessPermissionContext::AfterWriteCheckResult::kAllow));
helper_->Start(base::BindLambdaForTesting(
[](blink::mojom::FileSystemAccessErrorPtr result) {
EXPECT_EQ(result->status, FileSystemAccessStatus::kOk);
}));
task_environment_.RunUntilIdle();
EXPECT_FALSE(storage::AsyncFileTestHelper::FileExists(
file_system_context_.get(), test_source_url_,
storage::AsyncFileTestHelper::kDontCheckSize));
EXPECT_TRUE(storage::AsyncFileTestHelper::FileExists(
file_system_context_.get(), test_dest_url_, 3));
}
TEST_F(SafeMoveHelperAfterWriteChecksTest, Block) {
EXPECT_TRUE(base::WriteFile(test_source_url_.path(), "abc"));
EXPECT_CALL(permission_context_, PerformAfterWriteChecks_(_, kFrameId, _))
.WillOnce(base::test::RunOnceCallback<2>(
FileSystemAccessPermissionContext::AfterWriteCheckResult::kBlock));
helper_->Start(base::BindLambdaForTesting(
[](blink::mojom::FileSystemAccessErrorPtr result) {
EXPECT_EQ(result->status, FileSystemAccessStatus::kOperationAborted);
}));
task_environment_.RunUntilIdle();
// File should not have been moved.
EXPECT_TRUE(storage::AsyncFileTestHelper::FileExists(
file_system_context_.get(), test_source_url_, 3));
EXPECT_FALSE(storage::AsyncFileTestHelper::FileExists(
file_system_context_.get(), test_dest_url_,
storage::AsyncFileTestHelper::kDontCheckSize));
}
} // namespace content