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",