// Copyright 2024 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/ash/fileapi/diversion_backend_delegate.h"
#include <memory>
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/raw_ref.h"
#include "base/strings/strcat.h"
#include "base/test/test_future.h"
#include "base/types/expected.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/browser_task_environment.h"
#include "net/base/io_buffer.h"
#include "net/base/test_completion_callback.h"
#include "storage/browser/file_system/async_file_util_adapter.h"
#include "storage/browser/file_system/local_file_util.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 "testing/gtest/include/gtest/gtest.h"
namespace ash {
namespace {
base::expected<std::string, net::Error> ReadFromReader(
storage::FileStreamReader& reader,
size_t bytes_to_read) {
std::string result;
size_t total_bytes_read = 0;
while (total_bytes_read < bytes_to_read) {
scoped_refptr<net::IOBufferWithSize> buf(
base::MakeRefCounted<net::IOBufferWithSize>(bytes_to_read -
total_bytes_read));
net::TestCompletionCallback callback;
int rv = reader.Read(buf.get(), buf->size(), callback.callback());
if (rv == net::ERR_IO_PENDING) {
rv = callback.WaitForResult();
}
if (rv < 0) {
return base::unexpected(static_cast<net::Error>(rv));
} else if (rv == 0) {
break;
}
total_bytes_read += rv;
result.append(buf->data(), rv);
}
return result;
}
// A simple list of 0 or 1 FileSystemURLs that CHECK-fails when CheckAllowed is
// passed that FileSystemURL.
class DenyList {
public:
void CheckAllowed(const storage::FileSystemURL& fs_url) const {
if (!disabled_ && fs_url_.is_valid()) {
CHECK(fs_url_ != fs_url);
}
}
void Deny(const storage::FileSystemURL& fs_url) {
CHECK(fs_url.is_valid());
CHECK(!fs_url_.is_valid());
fs_url_ = fs_url;
}
void set_disabled(bool disabled) { disabled_ = disabled; }
private:
bool disabled_ = false;
storage::FileSystemURL fs_url_;
};
// Wraps a LocalFileUtil (which implements the FileSystemFileUtil interface)
// with a DenyList. Unless disabled, it checks that various FileSystemFileUtil
// methods are not passed any FileSystemURLs on the deny list.
class DeniableFileUtil : public storage::LocalFileUtil {
public:
explicit DeniableFileUtil(const DenyList& deny_list)
: deny_list_(deny_list) {}
base::File::Error EnsureFileExists(
storage::FileSystemOperationContext* context,
const storage::FileSystemURL& url,
bool* created) override {
deny_list_->CheckAllowed(url);
return storage::LocalFileUtil::EnsureFileExists(context, url, created);
}
base::File::Error GetFileInfo(storage::FileSystemOperationContext* context,
const storage::FileSystemURL& url,
base::File::Info* file_info,
base::FilePath* platform_file) override {
deny_list_->CheckAllowed(url);
return storage::LocalFileUtil::GetFileInfo(context, url, file_info,
platform_file);
}
base::File::Error Truncate(storage::FileSystemOperationContext* context,
const storage::FileSystemURL& url,
int64_t length) override {
deny_list_->CheckAllowed(url);
return storage::LocalFileUtil::Truncate(context, url, length);
}
base::File::Error DeleteFile(storage::FileSystemOperationContext* context,
const storage::FileSystemURL& url) override {
deny_list_->CheckAllowed(url);
return storage::LocalFileUtil::DeleteFile(context, url);
}
private:
raw_ref<const DenyList> deny_list_;
};
static int fake_fsb_delegate_create_file_stream_writer_count = 0;
class FakeFSBDelegate : public FileSystemBackendDelegate {
public:
explicit FakeFSBDelegate(
scoped_refptr<base::SingleThreadTaskRunner> task_runner,
const DenyList& deny_list)
: task_runner_(std::move(task_runner)),
adapter_(std::make_unique<DeniableFileUtil>(deny_list)) {}
FakeFSBDelegate(const FakeFSBDelegate&) = delete;
FakeFSBDelegate& operator=(const FakeFSBDelegate&) = delete;
storage::AsyncFileUtil* GetAsyncFileUtil(
storage::FileSystemType type) override {
CHECK_EQ(storage::kFileSystemTypeLocal, type);
return &adapter_;
}
std::unique_ptr<storage::FileStreamReader> CreateFileStreamReader(
const storage::FileSystemURL& url,
int64_t offset,
int64_t max_bytes_to_read,
const base::Time& expected_modification_time,
storage::FileSystemContext* context) override {
CHECK_EQ(storage::kFileSystemTypeLocal, url.type());
return storage::FileStreamReader::CreateForLocalFile(
task_runner_.get(), url.path(), offset, expected_modification_time);
}
std::unique_ptr<storage::FileStreamWriter> CreateFileStreamWriter(
const storage::FileSystemURL& url,
int64_t offset,
storage::FileSystemContext* context) override {
CHECK_EQ(storage::kFileSystemTypeLocal, url.type());
fake_fsb_delegate_create_file_stream_writer_count++;
return storage::FileStreamWriter::CreateForLocalFile(
task_runner_.get(), url.path(), offset,
storage::FileStreamWriter::OPEN_EXISTING_FILE);
}
storage::WatcherManager* GetWatcherManager(
storage::FileSystemType type) override {
NOTREACHED();
}
private:
scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
storage::AsyncFileUtilAdapter adapter_;
};
} // namespace
class DiversionBackendDelegateTest : public testing::Test,
public testing::WithParamInterface<int> {
public:
DiversionBackendDelegateTest()
: task_environment_(base::test::TaskEnvironment::MainThreadType::IO,
base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
// The int returned by GetParam() ranges from 0 to (1 << kNumParamBits).
static constexpr int kNumParamBits = 2;
bool ShouldCopy() const { return (1 << 0) & GetParam(); }
bool ShouldDestExists() const { return (1 << 1) & GetParam(); }
static std::string DescribeParams(
const testing::TestParamInfo<ParamType>& info) {
return base::StrCat({
"Should",
(((1 << 0) & info.param) ? "CopyAnd" : "MoveAnd"),
(((1 << 1) & info.param) ? "DestExists" : "NotDestExists"),
});
}
protected:
void SetUp() override {
ASSERT_TRUE(fs_context_temp_dir_.CreateUniqueTempDir());
ASSERT_TRUE(general_temp_dir_.CreateUniqueTempDir());
static constexpr bool is_incognito = false;
scoped_refptr<storage::MockQuotaManager> quota_manager =
base::MakeRefCounted<storage::MockQuotaManager>(
is_incognito, fs_context_temp_dir_.GetPath(),
base::SingleThreadTaskRunner::GetCurrentDefault(),
base::MakeRefCounted<storage::MockSpecialStoragePolicy>());
scoped_refptr<storage::MockQuotaManagerProxy> quota_manager_proxy =
base::MakeRefCounted<storage::MockQuotaManagerProxy>(
quota_manager.get(),
base::SingleThreadTaskRunner::GetCurrentDefault());
fs_context_ = CreateFileSystemContextForTesting(
quota_manager_proxy, fs_context_temp_dir_.GetPath());
}
void SynchronousWrite(storage::FileStreamWriter& writer, std::string s) {
scoped_refptr<net::StringIOBuffer> buffer =
base::MakeRefCounted<net::StringIOBuffer>(s);
writer.Write(
buffer.get(), buffer->size(),
base::BindOnce([](base::RepeatingClosure quit_closure,
int byte_count_or_error_code) { quit_closure.Run(); },
task_environment_.QuitClosure()));
task_environment_.RunUntilQuit();
}
std::unique_ptr<storage::FileSystemOperationContext> CreateFSOContext() {
return std::make_unique<storage::FileSystemOperationContext>(
fs_context_.get());
}
storage::FileSystemURL CreateFSURL(const char* basename) {
return storage::FileSystemURL::CreateForTest(
blink::StorageKey::CreateFromStringForTesting("chrome-extension://xxx"),
storage::kFileSystemTypeExternal, base::FilePath(basename),
"fake_mount_filesystem_id", storage::kFileSystemTypeLocal,
general_temp_dir_.GetPath().Append(base::FilePath(basename)),
"fake_filesystem_id", storage::FileSystemMountOption());
}
void AssertEnsureFileExistsReturns(storage::AsyncFileUtil* async_file_util,
const storage::FileSystemURL& fs_url,
base::File::Error expected_error) {
async_file_util->EnsureFileExists(
CreateFSOContext(), fs_url,
base::BindOnce(
[](base::File::Error expected_error,
base::RepeatingClosure quit_closure, base::File::Error error,
bool created) {
ASSERT_EQ(expected_error, error);
ASSERT_TRUE(created);
quit_closure.Run();
},
expected_error, task_environment_.QuitClosure()));
task_environment_.RunUntilQuit();
}
void AssertGetFileInfoReturns(storage::AsyncFileUtil* async_file_util,
const storage::FileSystemURL& fs_url,
base::File::Error expected_error) {
async_file_util->GetFileInfo(
CreateFSOContext(), fs_url,
{storage::FileSystemOperation::GetMetadataField::kIsDirectory},
base::BindOnce(
[](base::File::Error expected_error,
base::RepeatingClosure quit_closure, base::File::Error error,
const base::File::Info& file_info) {
ASSERT_EQ(expected_error, error);
quit_closure.Run();
},
expected_error, task_environment_.QuitClosure()));
task_environment_.RunUntilQuit();
}
void RunBasic(DiversionBackendDelegate::Policy policy);
static const char* kExpectedContents;
static const char* kFragments[];
content::BrowserTaskEnvironment task_environment_;
base::ScopedTempDir fs_context_temp_dir_;
base::ScopedTempDir general_temp_dir_;
scoped_refptr<storage::FileSystemContext> fs_context_;
};
const char* DiversionBackendDelegateTest::kExpectedContents =
"Lorem ipsum.Dolor sit.Amet.";
const char* DiversionBackendDelegateTest::kFragments[] = {
"Lorem ipsum.",
"Dolor sit.",
"Amet.",
};
void DiversionBackendDelegateTest::RunBasic(
DiversionBackendDelegate::Policy policy) {
ASSERT_TRUE(
::content::BrowserThread::CurrentlyOn(content::BrowserThread::IO));
const bool should_divert =
policy != DiversionBackendDelegate::Policy::kDoNotDivert;
const bool should_isolate =
policy == DiversionBackendDelegate::Policy::kDivertIsolated;
const char* basename = "";
switch (policy) {
case DiversionBackendDelegate::Policy::kDoNotDivert:
basename = "diversion.dat.some_other_extension";
break;
case DiversionBackendDelegate::Policy::kDivertIsolated:
basename = "diversion.dat.crdownload";
break;
case DiversionBackendDelegate::Policy::kDivertMingled:
basename = "diversion.dat.cros_divert_mingled_test";
break;
}
storage::FileSystemURL fs_url0 = CreateFSURL(basename);
DenyList deny_list;
if (should_isolate) {
deny_list.Deny(fs_url0);
}
fake_fsb_delegate_create_file_stream_writer_count = 0;
DiversionBackendDelegate delegate(std::make_unique<FakeFSBDelegate>(
task_environment_.GetMainThreadTaskRunner(), deny_list));
base::FilePath temp_dir;
ASSERT_TRUE(base::GetTempDir(&temp_dir));
delegate.OverrideTmpfileDirForTesting(temp_dir);
// Simulate the "download a file" workflow that first writes to a temporary
// file (fs_url0) before moving that over the ulimate destination (fs_url1).
//
// The final state should be the same, regardless of should_divert, in that
// the fs_url0 file does not exist but the fs_url1 file does exist (and its
// contents match the kExpectedContents).
//
// We also test copying (instead of moving) fs_url0 to fs_url1, depending on
// ShouldCopy(). The "download a file" workflow always moves, but copying
// should work too (where the final state is that both fs_url0 and fs_url1
// files exist and have the kExpectedContents).
storage::FileSystemURL fs_url1 = CreateFSURL("diversion.dat");
ASSERT_EQ(policy, delegate.ShouldDivertForTesting(fs_url0));
// The final state should be indifferent to whether or not fs_url1 already
// exists and, if it does, whether it's longer than kExpectedContents.
if (ShouldDestExists()) {
ASSERT_TRUE(base::WriteFile(fs_url1.path(), std::string(100, 'x')));
}
// The storage backends are generally happier, when calling
// CreateFileStreamWriter, if the file already 'exists'. This doesn't
// necessarily mean existence from the kernel's point of view, just from the
// //storage/browser/file_system virtual file system's point of view.
//
// We therefore call EnsureFileExists, here, before CreateFileStreamWriter,
// further below.
storage::AsyncFileUtil* async_file_util =
delegate.GetAsyncFileUtil(fs_url0.type());
{
AssertGetFileInfoReturns(async_file_util, fs_url0,
base::File::FILE_ERROR_NOT_FOUND);
AssertEnsureFileExistsReturns(async_file_util, fs_url0,
base::File::FILE_OK);
AssertGetFileInfoReturns(async_file_util, fs_url0, base::File::FILE_OK);
}
// Make multiple incremental writes, from multiple FileStreamWriter objects,
// similar to what happens when web apps use the FSA (File System Access)
// JavaScript API. In this unit test, the DiversionBackendDelegate is
// ultimately backed by a local file system. In production, though, FSA calls
// connected to the ODFS (One Drive File System) FSP (File System Provider)
// backend can be problematic (quadratic complexity in performance), when
// using multiple FileStreamWriter objects, because of ODFS's "each
// FileStreamWriter corresponds to a complete, one-shot upload" model.
//
// The whole point of a DiversionBackendDelegate is to buffer FSA's multiple
// FileStreamWriters so that ODFS only sees a single FileStreamWriter. The
// number of FakeFSBDelegate FileStreamWriter's created should be 0 or 3
// depending on should_divert;
{
int64_t offset = 0;
for (const char* fragment : kFragments) {
std::unique_ptr<storage::FileStreamWriter> writer =
delegate.CreateFileStreamWriter(fs_url0, offset, fs_context_.get());
SynchronousWrite(*writer, fragment);
offset += strlen(fragment);
}
EXPECT_EQ(should_divert ? 0 : 3,
fake_fsb_delegate_create_file_stream_writer_count);
}
// We have just written to fs_url0, so we should be able to read the contents
// back immediately (e.g. to compute a hash value for malware detection).
{
std::unique_ptr<storage::FileStreamReader> reader =
delegate.CreateFileStreamReader(fs_url0, 0,
std::numeric_limits<int64_t>::max(),
base::Time(), fs_context_.get());
base::expected<std::string, net::Error> read_from_reader_contents =
ReadFromReader(*reader, 100);
reader.reset();
ASSERT_TRUE(read_from_reader_contents.has_value());
EXPECT_EQ(kExpectedContents, read_from_reader_contents.value());
}
// Even though, in our unit test, our DiversionBackendDelegate is ultimately
// backed by a local file system, whether or not fs_url0 exists on that local
// file system depends on whether the DiversionBackendDelegate diverted
// (instead of passing through) that FileSystemURL.
{
EXPECT_NE(should_divert, base::PathExists(fs_url0.path()));
EXPECT_EQ(ShouldDestExists(), base::PathExists(fs_url1.path()));
}
// Copying or moving that fs_url0 file to fs_url1 should materialize it (if
// diverted) on the DiversionBackendDelegate's wrappee backend.
//
// Moving materializes fs_url1. Copying materializes both fs_url0 and fs_url1
// and materializing fs_url0 (which is on the deny_list, if should_isolate)
// requires temporarily disabling that deny_list.
if (ShouldCopy()) {
deny_list.set_disabled(true);
async_file_util->CopyFileLocal(
CreateFSOContext(), fs_url0, fs_url1, {},
base::BindRepeating(
[](int64_t size_arg_for_copy_file_progress_callback) {
// No-op.
}),
base::BindOnce(
[](base::RepeatingClosure quit_closure, base::File::Error error) {
ASSERT_EQ(base::File::FILE_OK, error);
quit_closure.Run();
},
task_environment_.QuitClosure()));
task_environment_.RunUntilQuit();
deny_list.set_disabled(false);
} else {
async_file_util->MoveFileLocal(
CreateFSOContext(), fs_url0, fs_url1, {},
base::BindOnce(
[](base::RepeatingClosure quit_closure, base::File::Error error) {
ASSERT_EQ(base::File::FILE_OK, error);
quit_closure.Run();
},
task_environment_.QuitClosure()));
task_environment_.RunUntilQuit();
}
// Check the final state, per "The final state should be the same" above.
{
EXPECT_EQ(ShouldCopy(), base::PathExists(fs_url0.path()));
EXPECT_TRUE(base::PathExists(fs_url1.path()));
if (ShouldCopy()) {
std::string contents0;
ASSERT_TRUE(base::ReadFileToString(fs_url0.path(), &contents0));
EXPECT_EQ(kExpectedContents, contents0);
}
std::string contents1;
ASSERT_TRUE(base::ReadFileToString(fs_url1.path(), &contents1));
EXPECT_EQ(kExpectedContents, contents1);
}
}
TEST_P(DiversionBackendDelegateTest, BasicDoNotDivert) {
RunBasic(DiversionBackendDelegate::Policy::kDoNotDivert);
}
TEST_P(DiversionBackendDelegateTest, BasicDivertIsolated) {
RunBasic(DiversionBackendDelegate::Policy::kDivertIsolated);
}
TEST_P(DiversionBackendDelegateTest, BasicDivertMingled) {
RunBasic(DiversionBackendDelegate::Policy::kDivertMingled);
}
INSTANTIATE_TEST_SUITE_P(
,
DiversionBackendDelegateTest,
testing::Range(0, 1 << DiversionBackendDelegateTest::kNumParamBits),
&DiversionBackendDelegateTest::DescribeParams);
TEST_F(DiversionBackendDelegateTest, Timeout) {
ASSERT_TRUE(
::content::BrowserThread::CurrentlyOn(content::BrowserThread::IO));
DenyList deny_list;
fake_fsb_delegate_create_file_stream_writer_count = 0;
DiversionBackendDelegate delegate(std::make_unique<FakeFSBDelegate>(
task_environment_.GetMainThreadTaskRunner(), deny_list));
base::FilePath temp_dir;
ASSERT_TRUE(base::GetTempDir(&temp_dir));
delegate.OverrideTmpfileDirForTesting(temp_dir);
storage::FileSystemURL fs_url0 = CreateFSURL("diversion.dat.crdownload");
ASSERT_EQ(DiversionBackendDelegate::Policy::kDivertIsolated,
delegate.ShouldDivertForTesting(fs_url0));
storage::AsyncFileUtil* async_file_util =
delegate.GetAsyncFileUtil(fs_url0.type());
{
AssertGetFileInfoReturns(async_file_util, fs_url0,
base::File::FILE_ERROR_NOT_FOUND);
AssertEnsureFileExistsReturns(async_file_util, fs_url0,
base::File::FILE_OK);
AssertGetFileInfoReturns(async_file_util, fs_url0, base::File::FILE_OK);
}
{
int64_t offset = 0;
for (const char* fragment : kFragments) {
std::unique_ptr<storage::FileStreamWriter> writer =
delegate.CreateFileStreamWriter(fs_url0, offset, fs_context_.get());
SynchronousWrite(*writer, fragment);
offset += strlen(fragment);
}
}
// The code above is similar to the DiversionBackendDelegateTest.Basic test,
// although it stops before the CopyFileLocal or MoveFileLocal call and there
// is no fs_url1 variable, only fs_url0. The code below is different.
{
ASSERT_FALSE(base::PathExists(fs_url0.path()));
task_environment_.FastForwardBy(delegate.IdleTimeoutForTesting());
ASSERT_TRUE(base::PathExists(fs_url0.path()));
std::string contents0;
ASSERT_TRUE(base::ReadFileToString(fs_url0.path(), &contents0));
EXPECT_EQ(kExpectedContents, contents0);
}
}
} // namespace ash