Implemented filesystem:// with the network service.
Bug: 797292
Cq-Include-Trybots: master.tryserver.chromium.linux:linux_mojo
Change-Id: If5e18eb719ea282861ac82d89b0ec7e999dcfde8
Reviewed-on: https://chromium-review.googlesource.com/982752
Commit-Queue: Chris Mumford <[email protected]>
Reviewed-by: John Abd-El-Malek <[email protected]>
Reviewed-by: Chris Mumford <[email protected]>
Reviewed-by: Daniel Murphy <[email protected]>
Cr-Commit-Position: refs/heads/master@{#560115}
diff --git a/content/browser/BUILD.gn b/content/browser/BUILD.gn
index aa06986..3101e59 100644
--- a/content/browser/BUILD.gn
+++ b/content/browser/BUILD.gn
@@ -762,6 +762,8 @@
"file_url_loader_factory.h",
"fileapi/browser_file_system_helper.cc",
"fileapi/browser_file_system_helper.h",
+ "fileapi/file_system_url_loader_factory.cc",
+ "fileapi/file_system_url_loader_factory.h",
"fileapi/fileapi_message_filter.cc",
"fileapi/fileapi_message_filter.h",
"find_request_manager.cc",
diff --git a/content/browser/fileapi/file_system_url_loader_factory.cc b/content/browser/fileapi/file_system_url_loader_factory.cc
new file mode 100644
index 0000000..2052bf1
--- /dev/null
+++ b/content/browser/fileapi/file_system_url_loader_factory.cc
@@ -0,0 +1,643 @@
+// 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/fileapi/file_system_url_loader_factory.h"
+
+#include <algorithm>
+#include <map>
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "base/macros.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
+#include "base/sequenced_task_runner.h"
+#include "base/strings/stringprintf.h"
+#include "base/task_scheduler/post_task.h"
+#include "base/task_scheduler/task_traits.h"
+#include "build/build_config.h"
+#include "components/services/filesystem/public/interfaces/types.mojom.h"
+#include "content/browser/child_process_security_policy_impl.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/render_process_host.h"
+#include "content/public/common/child_process_host.h"
+#include "mojo/public/cpp/bindings/binding_set.h"
+#include "mojo/public/cpp/system/string_data_pipe_producer.h"
+#include "net/base/directory_listing.h"
+#include "net/base/io_buffer.h"
+#include "net/base/mime_sniffer.h"
+#include "net/base/mime_util.h"
+#include "net/http/http_byte_range.h"
+#include "net/http/http_util.h"
+#include "storage/browser/fileapi/file_stream_reader.h"
+#include "storage/browser/fileapi/file_system_context.h"
+#include "storage/browser/fileapi/file_system_operation_runner.h"
+#include "storage/browser/fileapi/file_system_url.h"
+#include "storage/common/fileapi/file_system_util.h"
+
+using filesystem::mojom::DirectoryEntry;
+using storage::FileStreamReader;
+using storage::FileSystemContext;
+using storage::FileSystemOperation;
+using storage::FileSystemRequestInfo;
+using storage::FileSystemURL;
+using storage::VirtualPath;
+
+namespace content {
+namespace {
+
+struct FactoryParams {
+ int render_process_host_id;
+ int frame_tree_node_id;
+ scoped_refptr<FileSystemContext> file_system_context;
+ std::string storage_domain;
+};
+
+constexpr size_t kDefaultFileSystemUrlPipeSize = 65536;
+
+// Implementation sniffs the first file chunk to determine the mime-type.
+static_assert(kDefaultFileSystemUrlPipeSize >= net::kMaxBytesToSniff,
+ "Default file data pipe size must be at least as large as a "
+ "MIME-type sniffing buffer.");
+
+scoped_refptr<net::HttpResponseHeaders> CreateHttpResponseHeaders(
+ int response_code) {
+ std::string raw_headers;
+ raw_headers.append(base::StringPrintf("HTTP/1.1 %d OK", response_code));
+
+ // Tell WebKit never to cache this content.
+ raw_headers.append(1, '\0');
+ raw_headers.append(net::HttpRequestHeaders::kCacheControl);
+ raw_headers.append(": no-cache");
+
+ raw_headers.append(2, '\0');
+ return base::MakeRefCounted<net::HttpResponseHeaders>(raw_headers);
+}
+
+bool GetMimeType(const FileSystemURL& url, std::string* mime_type) {
+ DCHECK(url.is_valid());
+ base::FilePath::StringType extension = url.path().Extension();
+ if (!extension.empty())
+ extension = extension.substr(1);
+ return net::GetWellKnownMimeTypeFromExtension(extension, mime_type);
+}
+
+// Common implementation shared between the file and directory URLLoaders.
+class FileSystemEntryURLLoader
+ : public network::mojom::URLLoader,
+ public base::SupportsWeakPtr<FileSystemEntryURLLoader> {
+ public:
+ explicit FileSystemEntryURLLoader(FactoryParams params)
+ : binding_(this), params_(std::move(params)) {}
+
+ // network::mojom::URLLoader:
+ void FollowRedirect() override {}
+ void ProceedWithResponse() override {}
+ void SetPriority(net::RequestPriority priority,
+ int32_t intra_priority_value) override {}
+ void PauseReadingBodyFromNet() override {}
+ void ResumeReadingBodyFromNet() override {}
+
+ protected:
+ virtual void FileSystemIsMounted() = 0;
+
+ void Start(const network::ResourceRequest& request,
+ network::mojom::URLLoaderRequest loader,
+ network::mojom::URLLoaderClientPtrInfo client_info,
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner) {
+ io_task_runner->PostTask(
+ FROM_HERE,
+ base::BindOnce(&FileSystemEntryURLLoader::StartOnIOThread, AsWeakPtr(),
+ request, std::move(loader), std::move(client_info)));
+ }
+
+ void MaybeDeleteSelf() {
+ if (!binding_.is_bound() && !client_.is_bound())
+ delete this;
+ }
+
+ void OnClientComplete(network::URLLoaderCompletionStatus status) {
+ client_->OnComplete(status);
+ client_.reset();
+ MaybeDeleteSelf();
+ }
+
+ void OnClientComplete(base::File::Error file_error) {
+ OnClientComplete(net::FileErrorToNetError(file_error));
+ }
+
+ void OnClientComplete(net::Error net_error) {
+ OnClientComplete(network::URLLoaderCompletionStatus(net_error));
+ }
+
+ mojo::Binding<network::mojom::URLLoader> binding_;
+ network::mojom::URLLoaderClientPtr client_;
+ FactoryParams params_;
+ std::unique_ptr<mojo::StringDataPipeProducer> data_producer_;
+ net::HttpByteRange byte_range_;
+ FileSystemURL url_;
+
+ private:
+ void StartOnIOThread(const network::ResourceRequest& request,
+ network::mojom::URLLoaderRequest loader,
+ network::mojom::URLLoaderClientPtrInfo client_info) {
+ binding_.Bind(std::move(loader));
+ binding_.set_connection_error_handler(base::BindOnce(
+ &FileSystemEntryURLLoader::OnConnectionError, base::Unretained(this)));
+
+ client_.Bind(std::move(client_info));
+
+ if (!request.url.is_valid()) {
+ OnClientComplete(net::ERR_INVALID_URL);
+ return;
+ }
+
+ if (params_.render_process_host_id != ChildProcessHost::kInvalidUniqueID &&
+ !ChildProcessSecurityPolicyImpl::GetInstance()->CanRequestURL(
+ params_.render_process_host_id, request.url)) {
+ DVLOG(1) << "Denied unauthorized request for "
+ << request.url.possibly_invalid_spec();
+ OnClientComplete(net::ERR_INVALID_URL);
+ return;
+ }
+
+ std::string range_header;
+ if (request.headers.GetHeader(net::HttpRequestHeaders::kRange,
+ &range_header)) {
+ std::vector<net::HttpByteRange> ranges;
+ if (net::HttpUtil::ParseRangeHeader(range_header, &ranges)) {
+ if (ranges.size() == 1) {
+ byte_range_ = ranges[0];
+ } else {
+ // We don't support multiple range requests in one single URL request.
+ // TODO(adamk): decide whether we want to support multiple range
+ // requests.
+ OnClientComplete(net::ERR_REQUEST_RANGE_NOT_SATISFIABLE);
+ return;
+ }
+ }
+ }
+
+ url_ = params_.file_system_context->CrackURL(request.url);
+ if (!url_.is_valid()) {
+ const FileSystemRequestInfo request_info = {request.url, nullptr,
+ params_.storage_domain,
+ params_.frame_tree_node_id};
+ params_.file_system_context->AttemptAutoMountForURLRequest(
+ request_info,
+ base::BindOnce(&FileSystemEntryURLLoader::DidAttemptAutoMount,
+ AsWeakPtr(), request));
+ return;
+ }
+ FileSystemIsMounted();
+ }
+
+ void DidAttemptAutoMount(const network::ResourceRequest& request,
+ base::File::Error result) {
+ if (result != base::File::Error::FILE_OK) {
+ OnClientComplete(result);
+ return;
+ }
+ url_ = params_.file_system_context->CrackURL(request.url);
+ if (!url_.is_valid()) {
+ OnClientComplete(net::ERR_FILE_NOT_FOUND);
+ return;
+ }
+ FileSystemIsMounted();
+ }
+
+ void OnConnectionError() {
+ binding_.Close();
+ MaybeDeleteSelf();
+ }
+
+ DISALLOW_COPY_AND_ASSIGN(FileSystemEntryURLLoader);
+};
+
+class FileSystemDirectoryURLLoader : public FileSystemEntryURLLoader {
+ public:
+ static void CreateAndStart(
+ const network::ResourceRequest& request,
+ network::mojom::URLLoaderRequest loader,
+ network::mojom::URLLoaderClientPtrInfo client_info,
+ FactoryParams params,
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner) {
+ // Owns itself. Will live as long as its URLLoader and URLLoaderClientPtr
+ // bindings are alive - essentially until either the client gives up or all
+ // file directory has been sent to it.
+ auto* filesystem_loader =
+ new FileSystemDirectoryURLLoader(std::move(params));
+ filesystem_loader->Start(request, std::move(loader), std::move(client_info),
+ io_task_runner);
+ }
+
+ private:
+ explicit FileSystemDirectoryURLLoader(FactoryParams params)
+ : FileSystemEntryURLLoader(params) {}
+
+ void FileSystemIsMounted() override {
+ DCHECK(url_.is_valid());
+ if (!params_.file_system_context->CanServeURLRequest(url_)) {
+ // In incognito mode the API is not usable and there should be no data.
+ if (VirtualPath::IsRootPath(url_.virtual_path())) {
+ // Return an empty directory if the filesystem root is queried.
+ DidReadDirectory(base::File::FILE_OK, std::vector<DirectoryEntry>(),
+ /*has_more=*/false);
+ return;
+ }
+ // In incognito mode the API is not usable and there should be no data.
+ OnClientComplete(net::ERR_FILE_NOT_FOUND);
+ return;
+ }
+ params_.file_system_context->operation_runner()->ReadDirectory(
+ url_,
+ base::BindRepeating(&FileSystemDirectoryURLLoader::DidReadDirectory,
+ base::AsWeakPtr(this)));
+ }
+
+ void DidReadDirectory(base::File::Error result,
+ std::vector<DirectoryEntry> entries,
+ bool has_more) {
+ if (result != base::File::FILE_OK) {
+ net::Error rv = net::ERR_FILE_NOT_FOUND;
+ if (result == base::File::FILE_ERROR_INVALID_URL)
+ rv = net::ERR_INVALID_URL;
+ OnClientComplete(rv);
+ return;
+ }
+
+ if (data_.empty()) {
+ base::FilePath relative_path = url_.path();
+#if defined(OS_POSIX)
+ relative_path =
+ base::FilePath(FILE_PATH_LITERAL("/") + relative_path.value());
+#endif
+ const base::string16& title = relative_path.LossyDisplayName();
+ data_.append(net::GetDirectoryListingHeader(title));
+ }
+
+ entries_.insert(entries_.end(), entries.begin(), entries.end());
+
+ if (!has_more) {
+ if (entries_.size())
+ GetMetadata(/*index=*/0);
+ else
+ WriteDirectoryData();
+ }
+ }
+
+ void GetMetadata(size_t index) {
+ const DirectoryEntry& entry = entries_[index];
+ const FileSystemURL entry_url =
+ params_.file_system_context->CreateCrackedFileSystemURL(
+ url_.origin(), url_.type(),
+ url_.path().Append(base::FilePath(entry.name)));
+ DCHECK(entry_url.is_valid());
+ params_.file_system_context->operation_runner()->GetMetadata(
+ entry_url,
+ FileSystemOperation::GET_METADATA_FIELD_SIZE |
+ FileSystemOperation::GET_METADATA_FIELD_LAST_MODIFIED,
+ base::BindRepeating(&FileSystemDirectoryURLLoader::DidGetMetadata,
+ base::AsWeakPtr(this), index));
+ }
+
+ void DidGetMetadata(size_t index,
+ base::File::Error result,
+ const base::File::Info& file_info) {
+ if (result != base::File::FILE_OK) {
+ OnClientComplete(result);
+ return;
+ }
+
+ const DirectoryEntry& entry = entries_[index];
+ const base::string16& name = base::FilePath(entry.name).LossyDisplayName();
+ data_.append(net::GetDirectoryListingEntry(
+ name, std::string(),
+ entry.type == filesystem::mojom::FsFileType::DIRECTORY, file_info.size,
+ file_info.last_modified));
+
+ if (index < entries_.size() - 1)
+ GetMetadata(index + 1);
+ else
+ WriteDirectoryData();
+ }
+
+ void WriteDirectoryData() {
+ mojo::DataPipe pipe(std::max(data_.size(), kDefaultFileSystemUrlPipeSize));
+ if (!pipe.consumer_handle.is_valid()) {
+ OnClientComplete(net::ERR_FAILED);
+ return;
+ }
+
+ network::ResourceResponseHead head;
+ head.mime_type = "text/plain";
+ head.charset = "utf-8";
+ head.content_length = data_.size();
+ head.headers = CreateHttpResponseHeaders(200);
+
+ client_->OnReceiveResponse(head, /*downloaded_file=*/nullptr);
+ client_->OnStartLoadingResponseBody(std::move(pipe.consumer_handle));
+
+ data_producer_ = std::make_unique<mojo::StringDataPipeProducer>(
+ std::move(pipe.producer_handle));
+
+ data_producer_->Write(
+ base::StringPiece(data_),
+ mojo::StringDataPipeProducer::AsyncWritingMode::
+ STRING_STAYS_VALID_UNTIL_COMPLETION,
+ base::BindOnce(&FileSystemDirectoryURLLoader::OnDirectoryWritten,
+ base::Unretained(this)));
+ }
+
+ void OnDirectoryWritten(MojoResult result) {
+ // All the data has been written now. Close the data pipe. The consumer will
+ // be notified that there will be no more data to read from now.
+ data_producer_.reset();
+ directory_data_ = nullptr;
+ entries_.clear();
+ data_.clear();
+
+ OnClientComplete(result == MOJO_RESULT_OK ? net::OK : net::ERR_FAILED);
+ }
+
+ std::string data_;
+ std::vector<DirectoryEntry> entries_;
+ scoped_refptr<net::IOBuffer> directory_data_;
+
+ DISALLOW_COPY_AND_ASSIGN(FileSystemDirectoryURLLoader);
+};
+
+class FileSystemFileURLLoader : public FileSystemEntryURLLoader {
+ public:
+ static void CreateAndStart(
+ const network::ResourceRequest& request,
+ network::mojom::URLLoaderRequest loader,
+ network::mojom::URLLoaderClientPtrInfo client_info,
+ FactoryParams params,
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner) {
+ // Owns itself. Will live as long as its URLLoader and URLLoaderClientPtr
+ // bindings are alive - essentially until either the client gives up or all
+ // file data has been sent to it.
+ auto* filesystem_loader =
+ new FileSystemFileURLLoader(std::move(params), request, io_task_runner);
+
+ filesystem_loader->Start(request, std::move(loader), std::move(client_info),
+ io_task_runner);
+ }
+
+ private:
+ FileSystemFileURLLoader(
+ FactoryParams params,
+ const network::ResourceRequest& request,
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner)
+ : FileSystemEntryURLLoader(std::move(params)),
+ original_request_(request),
+ io_task_runner_(io_task_runner) {}
+
+ void FileSystemIsMounted() override {
+ DCHECK(url_.is_valid());
+ if (!params_.file_system_context->CanServeURLRequest(url_)) {
+ // In incognito mode the API is not usable and there should be no data.
+ OnClientComplete(net::ERR_FILE_NOT_FOUND);
+ return;
+ }
+ params_.file_system_context->operation_runner()->GetMetadata(
+ url_,
+ FileSystemOperation::GET_METADATA_FIELD_IS_DIRECTORY |
+ FileSystemOperation::GET_METADATA_FIELD_SIZE,
+ base::AdaptCallbackForRepeating(base::BindOnce(
+ &FileSystemFileURLLoader::DidGetMetadata, base::AsWeakPtr(this))));
+ }
+
+ void DidGetMetadata(base::File::Error error_code,
+ const base::File::Info& file_info) {
+ if (error_code != base::File::FILE_OK) {
+ OnClientComplete(error_code == base::File::FILE_ERROR_INVALID_URL
+ ? net::ERR_INVALID_URL
+ : net::ERR_FILE_NOT_FOUND);
+ return;
+ }
+
+ if (!byte_range_.ComputeBounds(file_info.size)) {
+ OnClientComplete(net::ERR_REQUEST_RANGE_NOT_SATISFIABLE);
+ return;
+ }
+
+ if (file_info.is_directory) {
+ // Redirect to the directory URLLoader.
+ GURL::Replacements replacements;
+ std::string new_path = original_request_.url.path();
+ new_path.push_back('/');
+ replacements.SetPathStr(new_path);
+ const GURL directory_url =
+ original_request_.url.ReplaceComponents(replacements);
+
+ net::RedirectInfo redirect_info;
+ redirect_info.new_method = "GET";
+ redirect_info.status_code = 301;
+ head_.headers = CreateHttpResponseHeaders(redirect_info.status_code);
+ redirect_info.new_url =
+ original_request_.url.ReplaceComponents(replacements);
+ head_.encoded_data_length = 0;
+ client_->OnReceiveRedirect(redirect_info, head_);
+
+ // Restart the request with a directory loader.
+ network::ResourceRequest new_request = original_request_;
+ new_request.url = redirect_info.new_url;
+ FileSystemDirectoryURLLoader::CreateAndStart(
+ new_request, binding_.Unbind(), client_.PassInterface(),
+ std::move(params_), io_task_runner_);
+ MaybeDeleteSelf();
+ return;
+ }
+
+ remaining_bytes_ = byte_range_.last_byte_position() -
+ byte_range_.first_byte_position() + 1;
+ DCHECK_GE(remaining_bytes_, 0);
+
+ DCHECK(!reader_.get());
+ reader_ = params_.file_system_context->CreateFileStreamReader(
+ url_, byte_range_.first_byte_position(), remaining_bytes_,
+ base::Time());
+
+ mojo::DataPipe pipe(remaining_bytes_);
+ if (!pipe.consumer_handle.is_valid()) {
+ OnClientComplete(net::ERR_FAILED);
+ return;
+ }
+ consumer_handle_ = std::move(pipe.consumer_handle);
+
+ head_.mime_type = "text/html"; // Will sniff file and possibly override.
+ head_.charset = "utf-8";
+ head_.content_length = remaining_bytes_;
+ head_.headers = CreateHttpResponseHeaders(200);
+
+ data_producer_ = std::make_unique<mojo::StringDataPipeProducer>(
+ std::move(pipe.producer_handle));
+
+ file_data_ = new net::IOBuffer(kDefaultFileSystemUrlPipeSize);
+ ReadMoreFileData();
+ }
+
+ void ReadMoreFileData() {
+ int64_t bytes_to_read = std::min(
+ static_cast<int64_t>(kDefaultFileSystemUrlPipeSize), remaining_bytes_);
+ if (!bytes_to_read) {
+ OnFileWritten(MOJO_RESULT_OK);
+ return;
+ }
+ net::CompletionCallback read_callback = base::BindRepeating(
+ &FileSystemFileURLLoader::DidReadMoreFileData, base::AsWeakPtr(this));
+ const int rv =
+ reader_->Read(file_data_.get(), bytes_to_read, read_callback);
+ if (rv == net::ERR_IO_PENDING) {
+ // async callback will be called.
+ return;
+ }
+ std::move(read_callback).Run(rv);
+ }
+
+ void DidReadMoreFileData(int result) {
+ if (result <= 0) {
+ OnFileWritten(result);
+ return;
+ }
+
+ if (consumer_handle_.is_valid()) {
+ if (byte_range_.first_byte_position() == 0) {
+ // Only sniff for mime-type in the first block of the file.
+ std::string type_hint;
+ GetMimeType(url_, &type_hint);
+ SniffMimeType(file_data_->data(), result, url_.ToGURL(), type_hint,
+ net::ForceSniffFileUrlsForHtml::kDisabled,
+ &head_.mime_type);
+ }
+
+ client_->OnReceiveResponse(head_, /*downloaded_file=*/nullptr);
+ client_->OnStartLoadingResponseBody(std::move(consumer_handle_));
+ }
+ remaining_bytes_ -= result;
+ DCHECK_GE(remaining_bytes_, 0);
+
+ WriteFileData(result);
+ }
+
+ void WriteFileData(int bytes_read) {
+ data_producer_->Write(
+ base::StringPiece(file_data_->data(), bytes_read),
+ mojo::StringDataPipeProducer::AsyncWritingMode::
+ STRING_STAYS_VALID_UNTIL_COMPLETION,
+ base::BindOnce(&FileSystemFileURLLoader::OnFileDataWritten,
+ base::AsWeakPtr(this)));
+ }
+
+ void OnFileDataWritten(MojoResult result) {
+ if (result != MOJO_RESULT_OK || remaining_bytes_ == 0) {
+ OnFileWritten(result);
+ return;
+ }
+ ReadMoreFileData();
+ }
+
+ void OnFileWritten(MojoResult result) {
+ // All the data has been written now. Close the data pipe. The consumer will
+ // be notified that there will be no more data to read from now.
+ data_producer_.reset();
+ file_data_ = nullptr;
+
+ OnClientComplete(result == MOJO_RESULT_OK ? net::OK : net::ERR_FAILED);
+ }
+
+ int64_t remaining_bytes_ = 0;
+ mojo::ScopedDataPipeConsumerHandle consumer_handle_;
+ std::unique_ptr<FileStreamReader> reader_;
+ scoped_refptr<net::IOBuffer> file_data_;
+ network::ResourceResponseHead head_;
+ const network::ResourceRequest original_request_;
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner_;
+
+ DISALLOW_COPY_AND_ASSIGN(FileSystemFileURLLoader);
+};
+
+// A URLLoaderFactory used for the filesystem:// scheme used when the Network
+// Service is enabled.
+class FileSystemURLLoaderFactory : public network::mojom::URLLoaderFactory {
+ public:
+ FileSystemURLLoaderFactory(
+ FactoryParams params,
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner)
+ : params_(std::move(params)), io_task_runner_(io_task_runner) {}
+
+ ~FileSystemURLLoaderFactory() override = default;
+
+ network::mojom::URLLoaderFactoryPtr CreateBinding() {
+ network::mojom::URLLoaderFactoryPtr factory;
+ bindings_.AddBinding(this, mojo::MakeRequest(&factory));
+ return factory;
+ }
+
+ private:
+ void CreateLoaderAndStart(network::mojom::URLLoaderRequest loader,
+ int32_t routing_id,
+ int32_t request_id,
+ uint32_t options,
+ const network::ResourceRequest& request,
+ network::mojom::URLLoaderClientPtr client,
+ const net::MutableNetworkTrafficAnnotationTag&
+ traffic_annotation) override {
+ DVLOG(1) << "CreateLoaderAndStart: " << request.url;
+
+ const std::string path = request.url.path();
+
+ // If the path ends with a /, we know it's a directory. If the path refers
+ // to a directory and gets dispatched to FileSystemFileURLLoader, that class
+ // will redirect to FileSystemDirectoryURLLoader.
+ if (!path.empty() && path.back() == '/') {
+ FileSystemDirectoryURLLoader::CreateAndStart(request, std::move(loader),
+ client.PassInterface(),
+ params_, io_task_runner_);
+ return;
+ }
+
+ FileSystemFileURLLoader::CreateAndStart(request, std::move(loader),
+ client.PassInterface(), params_,
+ io_task_runner_);
+ }
+
+ void Clone(network::mojom::URLLoaderFactoryRequest loader) override {
+ bindings_.AddBinding(this, std::move(loader));
+ }
+
+ const FactoryParams params_;
+ mojo::BindingSet<network::mojom::URLLoaderFactory> bindings_;
+ scoped_refptr<base::SequencedTaskRunner> io_task_runner_;
+
+ DISALLOW_COPY_AND_ASSIGN(FileSystemURLLoaderFactory);
+};
+
+} // anonymous namespace
+
+std::unique_ptr<network::mojom::URLLoaderFactory>
+CreateFileSystemURLLoaderFactory(
+ RenderFrameHost* render_frame_host,
+ bool is_navigation,
+ scoped_refptr<FileSystemContext> file_system_context,
+ const std::string& storage_domain) {
+ // Get the RPH ID for security checks for non-navigation resource requests.
+ int render_process_host_id = is_navigation
+ ? ChildProcessHost::kInvalidUniqueID
+ : render_frame_host->GetProcess()->GetID();
+
+ FactoryParams params = {render_process_host_id,
+ render_frame_host->GetFrameTreeNodeId(),
+ file_system_context, storage_domain};
+
+ return std::make_unique<FileSystemURLLoaderFactory>(
+ std::move(params),
+ BrowserThread::GetTaskRunnerForThread(BrowserThread::IO));
+}
+
+} // namespace content
diff --git a/content/browser/fileapi/file_system_url_loader_factory.h b/content/browser/fileapi/file_system_url_loader_factory.h
new file mode 100644
index 0000000..2cfda1e1
--- /dev/null
+++ b/content/browser/fileapi/file_system_url_loader_factory.h
@@ -0,0 +1,31 @@
+// 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.
+
+#ifndef CONTENT_BROWSER_FILEAPI_FILE_SYSTEM_URL_LOADER_FACTORY_H_
+#define CONTENT_BROWSER_FILEAPI_FILE_SYSTEM_URL_LOADER_FACTORY_H_
+
+#include <memory>
+#include <string>
+
+#include "base/memory/ref_counted.h"
+#include "content/common/content_export.h"
+#include "services/network/public/mojom/url_loader_factory.mojom.h"
+#include "storage/browser/fileapi/file_system_context.h"
+
+namespace content {
+
+class RenderFrameHost;
+
+// Create a URLLoaderFactory to serve filesystem: requests from the given
+// |file_system_context| and |storage_domain|.
+CONTENT_EXPORT std::unique_ptr<network::mojom::URLLoaderFactory>
+CreateFileSystemURLLoaderFactory(
+ RenderFrameHost* render_frame_host,
+ bool is_navigation,
+ scoped_refptr<storage::FileSystemContext> file_system_context,
+ const std::string& storage_domain);
+
+} // namespace content
+
+#endif // CONTENT_BROWSER_FILEAPI_FILE_SYSTEM_URL_LOADER_FACTORY_H_
diff --git a/content/browser/fileapi/file_system_url_loader_factory_browsertest.cc b/content/browser/fileapi/file_system_url_loader_factory_browsertest.cc
new file mode 100644
index 0000000..79fd5803
--- /dev/null
+++ b/content/browser/fileapi/file_system_url_loader_factory_browsertest.cc
@@ -0,0 +1,817 @@
+// 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 <algorithm>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/i18n/unicodestring.h"
+#include "base/rand_util.h"
+#include "base/test/scoped_feature_list.h"
+#include "build/build_config.h"
+#include "content/browser/fileapi/file_system_url_loader_factory.h"
+#include "content/browser/web_contents/web_contents_impl.h"
+#include "content/public/test/content_browser_test.h"
+#include "content/shell/browser/shell.h"
+#include "net/base/mime_util.h"
+#include "net/http/http_util.h"
+#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "services/network/public/cpp/features.h"
+#include "services/network/public/cpp/shared_url_loader_factory.h"
+#include "services/network/public/mojom/url_loader_factory.mojom.h"
+#include "services/network/test/test_url_loader_client.h"
+#include "storage/browser/fileapi/external_mount_points.h"
+#include "storage/browser/fileapi/file_system_context.h"
+#include "storage/browser/fileapi/file_system_file_util.h"
+#include "storage/browser/fileapi/file_system_operation_context.h"
+#include "storage/browser/fileapi/file_system_url.h"
+#include "storage/browser/test/async_file_test_helper.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_context.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/icu/source/i18n/unicode/datefmt.h"
+#include "third_party/icu/source/i18n/unicode/regex.h"
+
+using content::AsyncFileTestHelper;
+using network::mojom::URLLoaderFactory;
+using storage::FileSystemContext;
+using storage::FileSystemOperationContext;
+using storage::FileSystemURL;
+
+namespace content {
+namespace {
+
+// We always use the TEMPORARY FileSystem in these tests.
+const char kFileSystemURLPrefix[] = "filesystem:http://remote/temporary/";
+
+const char kValidExternalMountPoint[] = "mnt_name";
+
+const char kTestFileData[] = "0123456789";
+
+void FillBuffer(char* buffer, size_t len) {
+ base::RandBytes(buffer, len);
+}
+
+// An auto mounter that will try to mount anything for |storage_domain| =
+// "automount", but will only succeed for the mount point "mnt_name".
+bool TestAutoMountForURLRequest(
+ const storage::FileSystemRequestInfo& request_info,
+ const storage::FileSystemURL& filesystem_url,
+ base::OnceCallback<void(base::File::Error result)> callback) {
+ if (request_info.storage_domain != "automount")
+ return false;
+
+ std::vector<base::FilePath::StringType> components;
+ filesystem_url.path().GetComponents(&components);
+ std::string mount_point = base::FilePath(components[0]).AsUTF8Unsafe();
+
+ if (mount_point == kValidExternalMountPoint) {
+ storage::ExternalMountPoints::GetSystemInstance()->RegisterFileSystem(
+ kValidExternalMountPoint, storage::kFileSystemTypeTest,
+ storage::FileSystemMountOption(), base::FilePath());
+ std::move(callback).Run(base::File::FILE_OK);
+ } else {
+ std::move(callback).Run(base::File::FILE_ERROR_NOT_FOUND);
+ }
+ return true;
+}
+
+void ReadDataPipeInternal(mojo::DataPipeConsumerHandle handle,
+ std::string* result,
+ base::OnceClosure quit_closure) {
+ while (true) {
+ uint32_t num_bytes;
+ const void* buffer = nullptr;
+ MojoResult rv =
+ handle.BeginReadData(&buffer, &num_bytes, MOJO_READ_DATA_FLAG_NONE);
+ switch (rv) {
+ case MOJO_RESULT_BUSY:
+ case MOJO_RESULT_INVALID_ARGUMENT:
+ NOTREACHED();
+ return;
+ case MOJO_RESULT_FAILED_PRECONDITION:
+ std::move(quit_closure).Run();
+ return;
+ case MOJO_RESULT_SHOULD_WAIT:
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE, base::BindOnce(&ReadDataPipeInternal, handle, result,
+ std::move(quit_closure)));
+ return;
+ case MOJO_RESULT_OK:
+ EXPECT_NE(nullptr, buffer);
+ EXPECT_GT(num_bytes, 0u);
+ uint32_t before_size = result->size();
+ result->append(static_cast<const char*>(buffer), num_bytes);
+ uint32_t read_size = result->size() - before_size;
+ EXPECT_EQ(num_bytes, read_size);
+ rv = handle.EndReadData(read_size);
+ EXPECT_EQ(MOJO_RESULT_OK, rv);
+ break;
+ }
+ }
+ NOTREACHED();
+ return;
+}
+
+std::string ReadDataPipe(mojo::ScopedDataPipeConsumerHandle handle) {
+ EXPECT_TRUE(handle.is_valid());
+ if (!handle.is_valid())
+ return "";
+ std::string result;
+ base::RunLoop loop;
+ ReadDataPipeInternal(handle.get(), &result, loop.QuitClosure());
+ loop.Run();
+ return result;
+}
+
+// Directory listings can have a HTML header in the file to format the response.
+// This function determines if a single line in the response is for a directory
+// entry.
+bool IsDirectoryListingLine(const std::string& line) {
+ return line.find("<script>addRow(\"") == 0;
+}
+
+// Is the line a title inserted by net::GetDirectoryListingHeader?
+bool IsDirectoryListingTitle(const std::string& line) {
+ return line.find("<script>start(\"") == 0;
+}
+
+void ShutdownFileSystemContextOnIOThread(
+ scoped_refptr<FileSystemContext> file_system_context) {
+ if (!file_system_context)
+ return;
+ file_system_context->Shutdown();
+ file_system_context = nullptr;
+}
+
+} // namespace
+
+class FileSystemURLLoaderFactoryTest : public ContentBrowserTest {
+ protected:
+ FileSystemURLLoaderFactoryTest() : weak_factory_(this) {}
+ ~FileSystemURLLoaderFactoryTest() override = default;
+
+ void SetUpOnMainThread() override {
+ feature_list_.InitAndEnableFeature(network::features::kNetworkService);
+
+ ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
+
+ special_storage_policy_ = new MockSpecialStoragePolicy;
+ file_system_context_ =
+ CreateFileSystemContextForTesting(nullptr, temp_dir_.GetPath());
+
+ // We use the main thread so that we can get the root path synchronously.
+ file_system_context_->OpenFileSystem(
+ GURL("http://remote/"), storage::kFileSystemTypeTemporary,
+ storage::OPEN_FILE_SYSTEM_CREATE_IF_NONEXISTENT,
+ base::BindOnce(&FileSystemURLLoaderFactoryTest::OnOpenFileSystem,
+ weak_factory_.GetWeakPtr()));
+ base::RunLoop().RunUntilIdle();
+ ContentBrowserTest::SetUpOnMainThread();
+ }
+
+ void TearDownOnMainThread() override {
+ loader_.reset();
+ content::BrowserThread::PostTask(
+ content::BrowserThread::IO, FROM_HERE,
+ base::BindOnce(&ShutdownFileSystemContextOnIOThread,
+ std::move(file_system_context_)));
+ special_storage_policy_ = nullptr;
+ // FileReader posts a task to close the file in destructor.
+ base::RunLoop().RunUntilIdle();
+ ContentBrowserTest::TearDownOnMainThread();
+ }
+
+ void SetUpAutoMountContext(base::FilePath* mnt_point) {
+ *mnt_point = temp_dir_.GetPath().AppendASCII("auto_mount_dir");
+ ASSERT_TRUE(base::CreateDirectory(*mnt_point));
+
+ std::vector<std::unique_ptr<storage::FileSystemBackend>>
+ additional_providers;
+ additional_providers.push_back(std::make_unique<TestFileSystemBackend>(
+ base::ThreadTaskRunnerHandle::Get().get(), *mnt_point));
+
+ std::vector<storage::URLRequestAutoMountHandler> handlers = {
+ base::BindRepeating(&TestAutoMountForURLRequest)};
+
+ file_system_context_ = CreateFileSystemContextWithAutoMountersForTesting(
+ nullptr, std::move(additional_providers), handlers,
+ temp_dir_.GetPath());
+ }
+
+ void SetFileUpAutoMountContext() {
+ base::FilePath mnt_point;
+ SetUpAutoMountContext(&mnt_point);
+
+ ASSERT_EQ(static_cast<int>(sizeof(kTestFileData)) - 1,
+ base::WriteFile(mnt_point.AppendASCII("foo"), kTestFileData,
+ sizeof(kTestFileData) - 1));
+ }
+
+ FileSystemURL CreateURL(const base::FilePath& file_path) {
+ return file_system_context_->CreateCrackedFileSystemURL(
+ GURL("http://remote"), storage::kFileSystemTypeTemporary, file_path);
+ }
+
+ void CreateDirectory(const base::StringPiece& dir_name) {
+ base::FilePath path = base::FilePath().AppendASCII(dir_name);
+ std::unique_ptr<FileSystemOperationContext> context(NewOperationContext());
+ ASSERT_EQ(base::File::FILE_OK,
+ file_util()->CreateDirectory(context.get(), CreateURL(path),
+ false /* exclusive */,
+ false /* recursive */));
+ }
+
+ void WriteFile(const base::StringPiece& file_name,
+ const char* buf,
+ int buf_size) {
+ FileSystemURL url = file_system_context_->CreateCrackedFileSystemURL(
+ GURL("http://remote"), storage::kFileSystemTypeTemporary,
+ base::FilePath().AppendASCII(file_name));
+ ASSERT_EQ(base::File::FILE_OK,
+ AsyncFileTestHelper::CreateFileWithData(
+ file_system_context_.get(), url, buf, buf_size));
+ }
+
+ void EnsureFileExists(const base::StringPiece file_name) {
+ base::FilePath path = base::FilePath().AppendASCII(file_name);
+ std::unique_ptr<FileSystemOperationContext> context(NewOperationContext());
+ ASSERT_EQ(
+ base::File::FILE_OK,
+ file_util()->EnsureFileExists(context.get(), CreateURL(path), nullptr));
+ }
+
+ void TruncateFile(const base::StringPiece file_name, int64_t length) {
+ base::FilePath path = base::FilePath().AppendASCII(file_name);
+ std::unique_ptr<FileSystemOperationContext> context(NewOperationContext());
+ ASSERT_EQ(base::File::FILE_OK,
+ file_util()->Truncate(context.get(), CreateURL(path), length));
+ }
+
+ // If |size| is negative, the reported size is ignored.
+ void VerifyListingEntry(const std::string& entry_line,
+ const std::string& name,
+ const std::string& url,
+ bool is_directory,
+ int64_t size) {
+#define NUMBER "([0-9-]*)"
+#define STR "([^\"]*)"
+ icu::UnicodeString pattern("^<script>addRow\\(\"" STR "\",\"" STR
+ "\",(0|1)," NUMBER ",\"" STR "\"," NUMBER
+ ",\"" STR "\"\\);</script>");
+#undef NUMBER
+#undef STR
+ icu::UnicodeString input(entry_line.c_str());
+
+ UErrorCode status = U_ZERO_ERROR;
+ icu::RegexMatcher match(pattern, input, 0, status);
+
+ EXPECT_TRUE(match.find());
+ EXPECT_EQ(7, match.groupCount());
+ EXPECT_EQ(icu::UnicodeString(name.c_str()), match.group(1, status));
+ EXPECT_EQ(icu::UnicodeString(url.c_str()), match.group(2, status));
+ EXPECT_EQ(icu::UnicodeString(is_directory ? "1" : "0"),
+ match.group(3, status));
+ if (size >= 0) {
+ icu::UnicodeString size_string(
+ base::FormatBytesUnlocalized(size).c_str());
+ EXPECT_EQ(size_string, match.group(5, status));
+ }
+
+ icu::UnicodeString date_ustr(match.group(7, status));
+ std::unique_ptr<icu::DateFormat> formatter(
+ icu::DateFormat::createDateTimeInstance(icu::DateFormat::kShort));
+ UErrorCode parse_status = U_ZERO_ERROR;
+ UDate udate = formatter->parse(date_ustr, parse_status);
+ EXPECT_TRUE(U_SUCCESS(parse_status));
+ base::Time date = base::Time::FromJsTime(udate);
+ EXPECT_FALSE(date.is_null());
+ }
+
+ GURL CreateFileSystemURL(const std::string& path) {
+ return GURL(kFileSystemURLPrefix + path);
+ }
+
+ std::unique_ptr<network::TestURLLoaderClient> TestLoad(const GURL& url) {
+ auto client = TestLoadHelper(url, /*extra_headers=*/nullptr,
+ file_system_context_.get());
+ client->RunUntilComplete();
+ return client;
+ }
+
+ std::unique_ptr<network::TestURLLoaderClient> TestLoadWithContext(
+ const GURL& url,
+ FileSystemContext* file_system_context) {
+ auto client =
+ TestLoadHelper(url, /*extra_headers=*/nullptr, file_system_context);
+ client->RunUntilComplete();
+ return client;
+ }
+
+ std::unique_ptr<network::TestURLLoaderClient> TestLoadWithHeaders(
+ const GURL& url,
+ const net::HttpRequestHeaders* extra_headers) {
+ auto client =
+ TestLoadHelper(url, extra_headers, file_system_context_.get());
+ client->RunUntilComplete();
+ return client;
+ }
+
+ std::unique_ptr<network::TestURLLoaderClient> TestLoadNoRun(const GURL& url) {
+ return TestLoadHelper(url, /*extra_headers=*/nullptr,
+ file_system_context_.get());
+ }
+
+ // |temp_dir_| must be deleted last.
+ base::ScopedTempDir temp_dir_;
+ network::mojom::URLLoaderPtr loader_;
+
+ private:
+ storage::FileSystemFileUtil* file_util() {
+ return file_system_context_->sandbox_delegate()->sync_file_util();
+ }
+
+ FileSystemOperationContext* NewOperationContext() {
+ FileSystemOperationContext* context(
+ new FileSystemOperationContext(file_system_context_.get()));
+ context->set_allowed_bytes_growth(1024);
+ return context;
+ }
+
+ void OnOpenFileSystem(const GURL& root_url,
+ const std::string& name,
+ base::File::Error result) {
+ ASSERT_EQ(base::File::FILE_OK, result);
+ }
+
+ RenderFrameHost* render_frame_host() const {
+ return shell()->web_contents()->GetMainFrame();
+ }
+
+ // Starts |request| using |loader_factory| and sets |out_loader| and
+ // |out_loader_client| to the resulting URLLoader and its URLLoaderClient. The
+ // caller can then use functions like client.RunUntilComplete() to wait for
+ // completion.
+ void StartRequest(
+ URLLoaderFactory* loader_factory,
+ const network::ResourceRequest& request,
+ network::mojom::URLLoaderPtr* out_loader,
+ std::unique_ptr<network::TestURLLoaderClient>* out_loader_client) {
+ *out_loader_client = std::make_unique<network::TestURLLoaderClient>();
+ loader_factory->CreateLoaderAndStart(
+ mojo::MakeRequest(out_loader), 0, 0, network::mojom::kURLLoadOptionNone,
+ request, (*out_loader_client)->CreateInterfacePtr(),
+ net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
+ }
+
+ content::WebContents* GetWebContents() { return shell()->web_contents(); }
+
+ std::unique_ptr<network::TestURLLoaderClient> TestLoadHelper(
+ const GURL& url,
+ const net::HttpRequestHeaders* extra_headers,
+ FileSystemContext* file_system_context) {
+ network::ResourceRequest request;
+ request.url = url;
+ if (extra_headers)
+ request.headers.MergeFrom(*extra_headers);
+ const std::string storage_domain = url.GetOrigin().host();
+
+ auto factory = content::CreateFileSystemURLLoaderFactory(
+ render_frame_host(), /*is_navigation=*/false, file_system_context,
+ storage_domain);
+ std::unique_ptr<network::TestURLLoaderClient> client;
+ StartRequest(factory.get(), request, &loader_, &client);
+ return client;
+ }
+
+ base::test::ScopedFeatureList feature_list_;
+ scoped_refptr<MockSpecialStoragePolicy> special_storage_policy_;
+ scoped_refptr<FileSystemContext> file_system_context_;
+ base::WeakPtrFactory<FileSystemURLLoaderFactoryTest> weak_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(FileSystemURLLoaderFactoryTest);
+};
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest, DirectoryListing) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ CreateDirectory("foo");
+ CreateDirectory("foo/bar");
+ CreateDirectory("foo/bar/baz");
+
+ EnsureFileExists("foo/bar/hoge");
+ TruncateFile("foo/bar/hoge", 10);
+
+ auto client = TestLoad(CreateFileSystemURL("foo/bar/"));
+
+ ASSERT_TRUE(client->has_received_response());
+ ASSERT_TRUE(client->has_received_completion());
+
+ std::string response_text = ReadDataPipe(client->response_body_release());
+ EXPECT_GT(response_text.size(), 0ul);
+
+ std::istringstream in(response_text);
+ std::string line;
+
+ std::string listing_header;
+ std::vector<std::string> listing_entries;
+ while (!!std::getline(in, line)) {
+ if (listing_header.empty() && IsDirectoryListingTitle(line)) {
+ listing_header = line;
+ continue;
+ }
+ if (IsDirectoryListingLine(line))
+ listing_entries.push_back(line);
+ }
+
+#if defined(OS_WIN)
+ EXPECT_EQ("<script>start(\"foo\\\\bar\");</script>", listing_header);
+#elif defined(OS_POSIX)
+ EXPECT_EQ("<script>start(\"/foo/bar\");</script>", listing_header);
+#endif
+
+ ASSERT_EQ(2U, listing_entries.size());
+ std::sort(listing_entries.begin(), listing_entries.end());
+ VerifyListingEntry(listing_entries[0], "baz", "baz", true, 0);
+ VerifyListingEntry(listing_entries[1], "hoge", "hoge", false, 10);
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest, InvalidURL) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ auto client = TestLoad(GURL("filesystem:/foo/bar/baz"));
+ ASSERT_FALSE(client->has_received_response());
+ ASSERT_TRUE(client->has_received_completion());
+ EXPECT_EQ(net::ERR_INVALID_URL, client->completion_status().error_code);
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest, NoSuchRoot) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ auto client = TestLoad(GURL("filesystem:http://remote/persistent/somedir/"));
+ ASSERT_FALSE(client->has_received_response());
+ ASSERT_TRUE(client->has_received_completion());
+ EXPECT_EQ(net::ERR_FILE_NOT_FOUND, client->completion_status().error_code);
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest, NoSuchDirectory) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ auto client = TestLoad(CreateFileSystemURL("somedir/"));
+ ASSERT_FALSE(client->has_received_response());
+ ASSERT_TRUE(client->has_received_completion());
+ EXPECT_EQ(net::ERR_FILE_NOT_FOUND, client->completion_status().error_code);
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest, Cancel) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ CreateDirectory("foo");
+ auto client = TestLoadNoRun(CreateFileSystemURL("foo/"));
+ ASSERT_FALSE(client->has_received_response());
+ ASSERT_FALSE(client->has_received_completion());
+
+ client.reset();
+ loader_.reset();
+ base::RunLoop().RunUntilIdle();
+ // If we get here, success! we didn't crash!
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest, Incognito) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ CreateDirectory("foo");
+
+ scoped_refptr<FileSystemContext> file_system_context =
+ CreateIncognitoFileSystemContextForTesting(nullptr, temp_dir_.GetPath());
+
+ auto client =
+ TestLoadWithContext(CreateFileSystemURL("/"), file_system_context.get());
+ ASSERT_TRUE(client->has_received_response());
+ ASSERT_TRUE(client->has_received_completion());
+
+ std::string response_text = ReadDataPipe(client->response_body_release());
+ EXPECT_GT(response_text.size(), 0ul);
+
+ std::istringstream in(response_text);
+
+ int num_entries = 0;
+ std::string line;
+ while (!!std::getline(in, line)) {
+ if (IsDirectoryListingLine(line))
+ num_entries++;
+ }
+
+ EXPECT_EQ(0, num_entries);
+
+ client = TestLoadWithContext(CreateFileSystemURL("foo"),
+ file_system_context.get());
+ ASSERT_FALSE(client->has_received_response());
+ ASSERT_TRUE(client->has_received_completion());
+ EXPECT_EQ(net::ERR_FILE_NOT_FOUND, client->completion_status().error_code);
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest,
+ AutoMountDirectoryListing) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ base::FilePath mnt_point;
+ SetUpAutoMountContext(&mnt_point);
+ EXPECT_TRUE(base::CreateDirectory(mnt_point));
+ EXPECT_TRUE(base::CreateDirectory(mnt_point.AppendASCII("foo")));
+ EXPECT_EQ(10,
+ base::WriteFile(mnt_point.AppendASCII("bar"), "1234567890", 10));
+
+ auto client =
+ TestLoad(GURL("filesystem:http://automount/external/mnt_name/"));
+
+ ASSERT_TRUE(client->has_received_response());
+ EXPECT_TRUE(client->has_received_completion());
+
+ std::string response_text = ReadDataPipe(client->response_body_release());
+ EXPECT_GT(response_text.size(), 0ul);
+
+ std::istringstream in(response_text);
+ std::string line;
+ EXPECT_TRUE(std::getline(in, line)); // |line| contains the temp dir path.
+
+ // Result order is not guaranteed, so sort the results.
+ std::vector<std::string> listing_entries;
+ while (!!std::getline(in, line)) {
+ if (IsDirectoryListingLine(line))
+ listing_entries.push_back(line);
+ }
+
+ ASSERT_EQ(2U, listing_entries.size());
+ std::sort(listing_entries.begin(), listing_entries.end());
+ VerifyListingEntry(listing_entries[0], "bar", "bar", false, 10);
+ VerifyListingEntry(listing_entries[1], "foo", "foo", true, -1);
+
+ EXPECT_TRUE(
+ storage::ExternalMountPoints::GetSystemInstance()->RevokeFileSystem(
+ kValidExternalMountPoint));
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest, AutoMountInvalidRoot) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ base::FilePath mnt_point;
+ SetUpAutoMountContext(&mnt_point);
+ auto client = TestLoad(GURL("filesystem:http://automount/external/invalid"));
+
+ EXPECT_FALSE(client->has_received_response());
+ EXPECT_TRUE(client->has_received_completion());
+ EXPECT_EQ(net::ERR_FILE_NOT_FOUND, client->completion_status().error_code);
+
+ EXPECT_FALSE(
+ storage::ExternalMountPoints::GetSystemInstance()->RevokeFileSystem(
+ "invalid"));
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest, AutoMountNoHandler) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ base::FilePath mnt_point;
+ SetUpAutoMountContext(&mnt_point);
+ auto client = TestLoad(GURL("filesystem:http://noauto/external/mnt_name"));
+
+ EXPECT_FALSE(client->has_received_response());
+ EXPECT_TRUE(client->has_received_completion());
+ EXPECT_EQ(net::ERR_FILE_NOT_FOUND, client->completion_status().error_code);
+
+ EXPECT_FALSE(
+ storage::ExternalMountPoints::GetSystemInstance()->RevokeFileSystem(
+ kValidExternalMountPoint));
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest, FileTest) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ WriteFile("file1.dat", kTestFileData, base::size(kTestFileData) - 1);
+ auto client = TestLoad(CreateFileSystemURL("file1.dat"));
+
+ EXPECT_TRUE(client->has_received_response());
+ EXPECT_TRUE(client->has_received_completion());
+ std::string response_text = ReadDataPipe(client->response_body_release());
+ EXPECT_EQ(kTestFileData, response_text);
+ ASSERT_TRUE(client->response_head().headers) << "No response headers";
+ EXPECT_EQ(200, client->response_head().headers->response_code());
+ std::string cache_control;
+ EXPECT_TRUE(client->response_head().headers->GetNormalizedHeader(
+ "cache-control", &cache_control));
+ EXPECT_EQ("no-cache", cache_control);
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest,
+ FileTestFullSpecifiedRange) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ const size_t buffer_size = 4000;
+ std::unique_ptr<char[]> buffer(new char[buffer_size]);
+ FillBuffer(buffer.get(), buffer_size);
+ WriteFile("bigfile", buffer.get(), buffer_size);
+
+ const size_t first_byte_position = 500;
+ const size_t last_byte_position = buffer_size - first_byte_position;
+ std::string partial_buffer_string(buffer.get() + first_byte_position,
+ buffer.get() + last_byte_position + 1);
+
+ net::HttpRequestHeaders headers;
+ headers.SetHeader(
+ net::HttpRequestHeaders::kRange,
+ net::HttpByteRange::Bounded(first_byte_position, last_byte_position)
+ .GetHeaderValue());
+ auto client = TestLoadWithHeaders(CreateFileSystemURL("bigfile"), &headers);
+
+ ASSERT_TRUE(client->has_received_response());
+ EXPECT_TRUE(client->has_received_completion());
+ std::string response_text = ReadDataPipe(client->response_body_release());
+ EXPECT_TRUE(partial_buffer_string == response_text);
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest,
+ FileTestHalfSpecifiedRange) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ const size_t buffer_size = 4000;
+ std::unique_ptr<char[]> buffer(new char[buffer_size]);
+ FillBuffer(buffer.get(), buffer_size);
+ WriteFile("bigfile", buffer.get(), buffer_size);
+
+ const size_t first_byte_position = 500;
+ std::string partial_buffer_string(buffer.get() + first_byte_position,
+ buffer.get() + buffer_size);
+
+ net::HttpRequestHeaders headers;
+ headers.SetHeader(
+ net::HttpRequestHeaders::kRange,
+ net::HttpByteRange::RightUnbounded(first_byte_position).GetHeaderValue());
+ auto client = TestLoadWithHeaders(CreateFileSystemURL("bigfile"), &headers);
+ ASSERT_TRUE(client->has_received_response());
+ EXPECT_TRUE(client->has_received_completion());
+ // Don't use EXPECT_EQ, it will print out a lot of garbage if check failed.
+ std::string response_text = ReadDataPipe(client->response_body_release());
+ EXPECT_TRUE(partial_buffer_string == response_text);
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest,
+ FileTestMultipleRangesNotSupported) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ WriteFile("file1.dat", kTestFileData, base::size(kTestFileData) - 1);
+ net::HttpRequestHeaders headers;
+ headers.SetHeader(net::HttpRequestHeaders::kRange,
+ "bytes=0-5,10-200,200-300");
+ auto client = TestLoadWithHeaders(CreateFileSystemURL("file1.dat"), &headers);
+ EXPECT_FALSE(client->has_received_response());
+ EXPECT_TRUE(client->has_received_completion());
+ EXPECT_EQ(net::ERR_REQUEST_RANGE_NOT_SATISFIABLE,
+ client->completion_status().error_code);
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest, FileRangeOutOfBounds) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ WriteFile("file1.dat", kTestFileData, base::size(kTestFileData) - 1);
+ net::HttpRequestHeaders headers;
+ headers.SetHeader(net::HttpRequestHeaders::kRange,
+ net::HttpByteRange::Bounded(500, 1000).GetHeaderValue());
+ auto client = TestLoadWithHeaders(CreateFileSystemURL("file1.dat"), &headers);
+
+ EXPECT_FALSE(client->has_received_response());
+ EXPECT_TRUE(client->has_received_completion());
+ EXPECT_EQ(net::ERR_REQUEST_RANGE_NOT_SATISFIABLE,
+ client->completion_status().error_code);
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest, FileDirRedirect) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ CreateDirectory("dir");
+ auto client = TestLoad(CreateFileSystemURL("dir"));
+
+ EXPECT_TRUE(client->has_received_response());
+ EXPECT_TRUE(client->has_received_completion());
+ EXPECT_TRUE(client->has_received_redirect());
+ EXPECT_EQ(301, client->redirect_info().status_code);
+ EXPECT_EQ(CreateFileSystemURL("dir/"), client->redirect_info().new_url);
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest, FileNoSuchRoot) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ auto client = TestLoad(GURL("filesystem:http://remote/persistent/somefile"));
+ EXPECT_FALSE(client->has_received_response());
+ EXPECT_TRUE(client->has_received_completion());
+ EXPECT_EQ(net::ERR_FILE_NOT_FOUND, client->completion_status().error_code);
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest, NoSuchFile) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ auto client = TestLoad(CreateFileSystemURL("somefile"));
+ EXPECT_FALSE(client->has_received_response());
+ EXPECT_TRUE(client->has_received_completion());
+ EXPECT_EQ(net::ERR_FILE_NOT_FOUND, client->completion_status().error_code);
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest, FileCancel) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ WriteFile("file1.dat", kTestFileData, base::size(kTestFileData) - 1);
+ auto client = TestLoadNoRun(CreateFileSystemURL("file1.dat"));
+
+ // client.reset();
+ base::RunLoop().RunUntilIdle();
+ // If we get here, success! we didn't crash!
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest, FileGetMimeType) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ std::string file_data =
+ "<!DOCTYPE HTML><html><head>test</head>"
+ "<body>foo</body></html>";
+ const char kFilename[] = "hoge.html";
+ WriteFile(kFilename, file_data.data(), file_data.size());
+
+ std::string mime_type_direct;
+ base::FilePath::StringType extension =
+ base::FilePath().AppendASCII(kFilename).Extension();
+ if (!extension.empty())
+ extension = extension.substr(1);
+ EXPECT_TRUE(
+ net::GetWellKnownMimeTypeFromExtension(extension, &mime_type_direct));
+
+ auto client = TestLoad(CreateFileSystemURL(kFilename));
+ EXPECT_TRUE(client->has_received_response());
+ EXPECT_TRUE(client->has_received_completion());
+
+ EXPECT_EQ(mime_type_direct, client->response_head().mime_type);
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest, FileIncognito) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ WriteFile("file", kTestFileData, base::size(kTestFileData) - 1);
+
+ // Creates a new filesystem context for incognito mode.
+ scoped_refptr<FileSystemContext> file_system_context =
+ CreateIncognitoFileSystemContextForTesting(nullptr, temp_dir_.GetPath());
+
+ // The request should return NOT_FOUND error if it's in incognito mode.
+ auto client = TestLoadWithContext(CreateFileSystemURL("file"),
+ file_system_context.get());
+ EXPECT_FALSE(client->has_received_response());
+ EXPECT_TRUE(client->has_received_completion());
+ EXPECT_EQ(net::ERR_FILE_NOT_FOUND, client->completion_status().error_code);
+
+ // Make sure it returns success with regular (non-incognito) context.
+ client = TestLoad(CreateFileSystemURL("file"));
+ ASSERT_TRUE(client->has_received_response());
+ EXPECT_TRUE(client->has_received_completion());
+ std::string response_text = ReadDataPipe(client->response_body_release());
+ EXPECT_EQ(kTestFileData, response_text);
+ EXPECT_EQ(200, client->response_head().headers->response_code());
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest, FileAutoMountFileTest) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ SetFileUpAutoMountContext();
+ auto client =
+ TestLoad(GURL("filesystem:http://automount/external/mnt_name/foo"));
+
+ ASSERT_TRUE(client->has_received_response());
+ EXPECT_TRUE(client->has_received_completion());
+ std::string response_text = ReadDataPipe(client->response_body_release());
+ EXPECT_EQ(kTestFileData, response_text);
+ EXPECT_EQ(200, client->response_head().headers->response_code());
+
+ std::string cache_control;
+ EXPECT_TRUE(client->response_head().headers->GetNormalizedHeader(
+ "cache-control", &cache_control));
+ EXPECT_EQ("no-cache", cache_control);
+
+ ASSERT_TRUE(
+ storage::ExternalMountPoints::GetSystemInstance()->RevokeFileSystem(
+ kValidExternalMountPoint));
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest,
+ FileAutoMountInvalidRoot) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ SetFileUpAutoMountContext();
+ auto client =
+ TestLoad(GURL("filesystem:http://automount/external/invalid/foo"));
+
+ EXPECT_FALSE(client->has_received_response());
+ EXPECT_TRUE(client->has_received_completion());
+ EXPECT_EQ(net::ERR_FILE_NOT_FOUND, client->completion_status().error_code);
+
+ ASSERT_FALSE(
+ storage::ExternalMountPoints::GetSystemInstance()->RevokeFileSystem(
+ "invalid"));
+}
+
+IN_PROC_BROWSER_TEST_F(FileSystemURLLoaderFactoryTest, FileAutoMountNoHandler) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ SetFileUpAutoMountContext();
+ auto client =
+ TestLoad(GURL("filesystem:http://noauto/external/mnt_name/foo"));
+
+ EXPECT_FALSE(client->has_received_response());
+ EXPECT_TRUE(client->has_received_completion());
+ EXPECT_EQ(net::ERR_FILE_NOT_FOUND, client->completion_status().error_code);
+
+ ASSERT_FALSE(
+ storage::ExternalMountPoints::GetSystemInstance()->RevokeFileSystem(
+ kValidExternalMountPoint));
+}
+
+} // namespace content
diff --git a/content/browser/frame_host/render_frame_host_impl.cc b/content/browser/frame_host/render_frame_host_impl.cc
index 93ec8eeb..a524724 100644
--- a/content/browser/frame_host/render_frame_host_impl.cc
+++ b/content/browser/frame_host/render_frame_host_impl.cc
@@ -36,6 +36,7 @@
#include "content/browser/dom_storage/dom_storage_context_wrapper.h"
#include "content/browser/download/mhtml_generation_manager.h"
#include "content/browser/file_url_loader_factory.h"
+#include "content/browser/fileapi/file_system_url_loader_factory.h"
#include "content/browser/frame_host/cross_process_frame_connector.h"
#include "content/browser/frame_host/debug_urls.h"
#include "content/browser/frame_host/frame_tree.h"
@@ -3588,6 +3589,7 @@
(!is_same_document || is_first_navigation)) {
subresource_loader_factories =
std::make_unique<URLLoaderFactoryBundleInfo>();
+ BrowserContext* browser_context = GetSiteInstance()->GetBrowserContext();
// NOTE: On Network Service navigations, we want to ensure that a frame is
// given everything it will need to load any accessible subresources. We
// however only do this for cross-document navigations, because the
@@ -3595,7 +3597,7 @@
network::mojom::URLLoaderFactoryPtrInfo default_factory_info;
StoragePartitionImpl* storage_partition =
static_cast<StoragePartitionImpl*>(BrowserContext::GetStoragePartition(
- GetSiteInstance()->GetBrowserContext(), GetSiteInstance()));
+ browser_context, GetSiteInstance()));
if (subresource_loader_params &&
subresource_loader_params->loader_factory_info.is_valid()) {
// If the caller has supplied a default URLLoaderFactory override (for
@@ -3643,7 +3645,7 @@
if (common_params.url.SchemeIsFile()) {
// Only file resources can load file subresources
auto file_factory = std::make_unique<FileURLLoaderFactory>(
- GetProcess()->GetBrowserContext()->GetPath(),
+ browser_context->GetPath(),
base::CreateSequencedTaskRunnerWithTraits(
{base::MayBlock(), base::TaskPriority::BACKGROUND,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}));
@@ -3651,6 +3653,22 @@
std::move(file_factory));
}
+ StoragePartition* partition =
+ BrowserContext::GetStoragePartition(browser_context, GetSiteInstance());
+ std::string storage_domain;
+ if (site_instance_) {
+ std::string partition_name;
+ bool in_memory;
+ GetContentClient()->browser()->GetStoragePartitionConfigForSite(
+ browser_context, site_instance_->GetSiteURL(), true, &storage_domain,
+ &partition_name, &in_memory);
+ }
+ non_network_url_loader_factories_.emplace(
+ url::kFileSystemScheme,
+ content::CreateFileSystemURLLoaderFactory(
+ this, /*is_navigation=*/false, partition->GetFileSystemContext(),
+ storage_domain));
+
GetContentClient()
->browser()
->RegisterNonNetworkSubresourceURLLoaderFactories(
diff --git a/content/browser/loader/navigation_url_loader_impl.cc b/content/browser/loader/navigation_url_loader_impl.cc
index 6b24a53..76023c2 100644
--- a/content/browser/loader/navigation_url_loader_impl.cc
+++ b/content/browser/loader/navigation_url_loader_impl.cc
@@ -18,6 +18,7 @@
#include "content/browser/blob_storage/chrome_blob_storage_context.h"
#include "content/browser/devtools/render_frame_devtools_agent_host.h"
#include "content/browser/file_url_loader_factory.h"
+#include "content/browser/fileapi/file_system_url_loader_factory.h"
#include "content/browser/frame_host/frame_tree_node.h"
#include "content/browser/frame_host/navigation_request_info.h"
#include "content/browser/loader/navigation_loader_interceptor.h"
@@ -1265,6 +1266,7 @@
network::mojom::URLLoaderFactoryPtrInfo proxied_factory_info;
network::mojom::URLLoaderFactoryRequest proxied_factory_request;
+ auto* partition = static_cast<StoragePartitionImpl*>(storage_partition);
if (frame_tree_node) {
// |frame_tree_node| may be null in some unit test environments.
GetContentClient()
@@ -1290,9 +1292,15 @@
proxied_factory_request = std::move(factory_request);
proxied_factory_info = std::move(factory_info);
}
+
+ const std::string storage_domain;
+ non_network_url_loader_factories_[url::kFileSystemScheme] =
+ CreateFileSystemURLLoaderFactory(frame_tree_node->current_frame_host(),
+ /*is_navigation=*/true,
+ partition->GetFileSystemContext(),
+ storage_domain);
}
- auto* partition = static_cast<StoragePartitionImpl*>(storage_partition);
non_network_url_loader_factories_[url::kFileScheme] =
std::make_unique<FileURLLoaderFactory>(
partition->browser_context()->GetPath(),
diff --git a/content/browser/network_service_browsertest.cc b/content/browser/network_service_browsertest.cc
index ae882db..1e329cf5 100644
--- a/content/browser/network_service_browsertest.cc
+++ b/content/browser/network_service_browsertest.cc
@@ -18,6 +18,7 @@
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
+#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "net/dns/mock_host_resolver.h"
#include "services/network/public/cpp/features.h"
@@ -112,12 +113,21 @@
WebUIControllerFactory::RegisterFactory(&factory_);
}
- bool CheckCanLoadHttp() {
- GURL test_url = embedded_test_server()->GetURL("/echo");
+ bool ExecuteScript(const std::string& script) {
+ bool xhr_result = false;
+ // The JS call will fail if disallowed because the process will be killed.
+ bool execute_result =
+ ExecuteScriptAndExtractBool(shell(), script, &xhr_result);
+ return xhr_result && execute_result;
+ }
+
+ bool FetchResource(const GURL& url) {
+ if (!url.is_valid())
+ return false;
std::string script(
"var xhr = new XMLHttpRequest();"
"xhr.open('GET', '");
- script += test_url.spec() +
+ script += url.spec() +
"', true);"
"xhr.onload = function (e) {"
" if (xhr.readyState === 4) {"
@@ -128,11 +138,11 @@
" window.domAutomationController.send(false);"
"};"
"xhr.send(null)";
- bool xhr_result = false;
- // The JS call will fail if disallowed because the process will be killed.
- bool execute_result =
- ExecuteScriptAndExtractBool(shell(), script, &xhr_result);
- return xhr_result && execute_result;
+ return ExecuteScript(script);
+ }
+
+ bool CheckCanLoadHttp() {
+ return FetchResource(embedded_test_server()->GetURL("/echo"));
}
void SetUpOnMainThread() override {
@@ -144,6 +154,7 @@
// Since we assume exploited renderer process, it can bypass the same origin
// policy at will. Simulate that by passing the disable-web-security flag.
command_line->AppendSwitch(switches::kDisableWebSecurity);
+ IsolateAllSitesForTesting(command_line);
}
private:
@@ -170,6 +181,20 @@
ASSERT_TRUE(CheckCanLoadHttp());
}
+// Verifies the filesystem URLLoaderFactory's check, using
+// ChildProcessSecurityPolicyImpl::CanRequestURL is properly rejected.
+IN_PROC_BROWSER_TEST_F(NetworkServiceBrowserTest,
+ FileSystemBindingsCorrectOrigin) {
+ GURL test_url("chrome://webui/nobinding/");
+ NavigateToURL(shell(), test_url);
+
+ // Note: must be filesystem scheme (obviously).
+ // file: is not a safe web scheme (see IsWebSafeScheme),
+ // and /etc/passwd fails the CanCommitURL check.
+ GURL file_url("filesystem:file:///etc/passwd");
+ EXPECT_FALSE(FetchResource(file_url));
+}
+
class NetworkServiceInProcessBrowserTest : public ContentBrowserTest {
public:
NetworkServiceInProcessBrowserTest() {
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 6cffa731..b3f539f 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -723,6 +723,7 @@
"../browser/download/mhtml_generation_browsertest.cc",
"../browser/download/save_package_browsertest.cc",
"../browser/fileapi/file_system_browsertest.cc",
+ "../browser/fileapi/file_system_url_loader_factory_browsertest.cc",
"../browser/fileapi/fileapi_browsertest.cc",
"../browser/find_request_manager_browsertest.cc",
"../browser/frame_host/blocked_scheme_navigation_browsertest.cc",
@@ -937,6 +938,7 @@
"//services/video_capture/public/mojom:constants",
"//services/viz/privileged/interfaces",
"//storage/browser",
+ "//storage/browser:test_support",
"//testing/gmock",
"//testing/gtest",
"//third_party/blink/public:blink",