[go: nahoru, domu]

Implement subresource loading with Web Bundles

- Adds WebBundleLoader class which loads a WebBundle resource using
  ThreadableLoader, and creates a WebBundleSubresourceLoaderFactory.

- LinkWebBundle is registered in ResourceFetcher, and used to forward
  subresource requests that match the corresponding <link>'s resources=
  attribute to the WebBundleSubresourceLoaderFactory. This is done via
  the SubresourceWebBundle interface.

Bug: 1082020
Change-Id: I3f26bd6b1fbd5855caa5175cdcb3db0fe103a093
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2291809
Reviewed-by: Kinuko Yasuda <kinuko@chromium.org>
Reviewed-by: Tsuyoshi Horo <horo@chromium.org>
Reviewed-by: Hayato Ito <hayato@chromium.org>
Commit-Queue: Kunihiko Sakamoto <ksakamoto@chromium.org>
Cr-Commit-Position: refs/heads/master@{#788842}
diff --git a/third_party/blink/renderer/core/html/link_web_bundle.cc b/third_party/blink/renderer/core/html/link_web_bundle.cc
index 630a103..15a54a9 100644
--- a/third_party/blink/renderer/core/html/link_web_bundle.cc
+++ b/third_party/blink/renderer/core/html/link_web_bundle.cc
@@ -4,12 +4,134 @@
 
 #include "third_party/blink/renderer/core/html/link_web_bundle.h"
 
+#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-blink.h"
+#include "third_party/blink/renderer/core/html/html_link_element.h"
+#include "third_party/blink/renderer/core/loader/threadable_loader.h"
+#include "third_party/blink/renderer/core/loader/threadable_loader_client.h"
+#include "third_party/blink/renderer/platform/loader/cors/cors.h"
+#include "third_party/blink/renderer/platform/loader/fetch/bytes_consumer.h"
+#include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h"
+#include "third_party/blink/renderer/platform/loader/fetch/url_loader/web_bundle_subresource_loader.h"
+
 namespace blink {
 
+// WebBundleLoader is responsible for loading a WebBundle resource.
+class WebBundleLoader : public GarbageCollected<WebBundleLoader>,
+                        public ThreadableLoaderClient {
+  USING_GARBAGE_COLLECTED_MIXIN(WebBundleLoader);
+
+ public:
+  WebBundleLoader(LinkWebBundle& link_web_bundle,
+                  ExecutionContext& execution_context,
+                  const KURL& url)
+      : link_web_bundle_(&link_web_bundle),
+        pending_factory_receiver_(
+            loader_factory_.BindNewPipeAndPassReceiver()) {
+    ResourceRequest request(url);
+    request.SetUseStreamOnResponse(true);
+    // TODO(crbug.com/1082020): Revisit these once the fetch and process the
+    // linked resource algorithm [1] for <link rel=webbundle> is defined.
+    // [1]
+    // https://html.spec.whatwg.org/multipage/semantics.html#fetch-and-process-the-linked-resource
+    request.SetRequestContext(mojom::blink::RequestContextType::SUBRESOURCE);
+    request.SetMode(network::mojom::blink::RequestMode::kCors);
+    request.SetCredentialsMode(network::mojom::blink::CredentialsMode::kOmit);
+
+    ResourceLoaderOptions resource_loader_options;
+    resource_loader_options.data_buffering_policy = kDoNotBufferData;
+
+    loader_ = MakeGarbageCollected<ThreadableLoader>(execution_context, this,
+                                                     resource_loader_options);
+    loader_->Start(std::move(request));
+  }
+
+  void Trace(Visitor* visitor) const override {
+    visitor->Trace(link_web_bundle_);
+    visitor->Trace(loader_);
+  }
+
+  bool HasLoaded() const { return !failed_; }
+
+  mojo::PendingRemote<network::mojom::blink::URLLoaderFactory>
+  GetURLLoaderFactory() {
+    mojo::PendingRemote<network::mojom::blink::URLLoaderFactory> factory_clone;
+    loader_factory_->Clone(factory_clone.InitWithNewPipeAndPassReceiver());
+    return factory_clone;
+  }
+
+  // ThreadableLoaderClient
+  void DidReceiveResponse(uint64_t, const ResourceResponse& response) override {
+    if (!cors::IsOkStatus(response.HttpStatusCode()))
+      failed_ = true;
+    // TODO(crbug.com/1082020): Check response headers, as spec'ed in
+    // https://wicg.github.io/webpackage/draft-yasskin-wpack-bundled-exchanges.html#name-serving-constraints.
+  }
+
+  void DidStartLoadingResponseBody(BytesConsumer& consumer) override {
+    DCHECK(pending_factory_receiver_);
+    CreateWebBundleSubresourceLoaderFactory(
+        mojo::PendingReceiver<network::mojom::URLLoaderFactory>(
+            pending_factory_receiver_.PassPipe()),
+        consumer.DrainAsDataPipe());
+    // TODO(crbug.com/1082020): Set |failed_| to true on metadata parse error,
+    // so that "error" event is dispatched.
+  }
+
+  void DidFinishLoading(uint64_t) override { link_web_bundle_->NotifyLoaded(); }
+  void DidFail(const ResourceError&) override { DidFailInternal(); }
+  void DidFailRedirectCheck() override { DidFailInternal(); }
+
+ private:
+  void DidFailInternal() {
+    if (pending_factory_receiver_) {
+      // If we haven't create a WebBundleSubresourceLoaderFactory, create it
+      // with an empty bundle body so that requests to
+      // |pending_factory_receiver_| are processed (and fail).
+      CreateWebBundleSubresourceLoaderFactory(
+          mojo::PendingReceiver<network::mojom::URLLoaderFactory>(
+              pending_factory_receiver_.PassPipe()),
+          mojo::ScopedDataPipeConsumerHandle());
+    }
+    failed_ = true;
+    link_web_bundle_->NotifyLoaded();
+  }
+
+  Member<LinkWebBundle> link_web_bundle_;
+  Member<ThreadableLoader> loader_;
+  mojo::Remote<network::mojom::blink::URLLoaderFactory> loader_factory_;
+  mojo::PendingReceiver<network::mojom::blink::URLLoaderFactory>
+      pending_factory_receiver_;
+  bool failed_ = false;
+};
+
 LinkWebBundle::LinkWebBundle(HTMLLinkElement* owner) : LinkResource(owner) {}
+LinkWebBundle::~LinkWebBundle() = default;
+
+void LinkWebBundle::Trace(Visitor* visitor) const {
+  visitor->Trace(bundle_loader_);
+  LinkResource::Trace(visitor);
+  SubresourceWebBundle::Trace(visitor);
+}
+
+void LinkWebBundle::NotifyLoaded() {
+  if (owner_)
+    owner_->ScheduleEvent();
+}
 
 void LinkWebBundle::Process() {
-  // TODO(crbug.com/1082020): Implement this.
+  if (!owner_ || !owner_->GetDocument().GetFrame())
+    return;
+  if (!owner_->ShouldLoadLink())
+    return;
+
+  ResourceFetcher* resource_fetcher = owner_->GetDocument().Fetcher();
+  if (!resource_fetcher)
+    return;
+
+  bundle_loader_ = MakeGarbageCollected<WebBundleLoader>(
+      *this, *owner_->GetDocument().GetExecutionContext(), owner_->Href());
+
+  resource_fetcher->AddSubresourceWebBundle(*this);
 }
 
 LinkResource::LinkResourceType LinkWebBundle::GetType() const {
@@ -17,10 +139,28 @@
 }
 
 bool LinkWebBundle::HasLoaded() const {
-  return false;
+  return bundle_loader_ && bundle_loader_->HasLoaded();
 }
 
-void LinkWebBundle::OwnerRemoved() {}
+void LinkWebBundle::OwnerRemoved() {
+  if (!owner_)
+    return;
+  ResourceFetcher* resource_fetcher = owner_->GetDocument().Fetcher();
+  if (!resource_fetcher)
+    return;
+  resource_fetcher->RemoveSubresourceWebBundle(*this);
+  bundle_loader_ = nullptr;
+}
+
+bool LinkWebBundle::CanHandleRequest(const KURL& url) const {
+  return owner_ && owner_->ValidResourceUrls().Contains(url);
+}
+
+mojo::PendingRemote<network::mojom::blink::URLLoaderFactory>
+LinkWebBundle::GetURLLoaderFactory() {
+  DCHECK(bundle_loader_);
+  return bundle_loader_->GetURLLoaderFactory();
+}
 
 // static
 KURL LinkWebBundle::ParseResourceUrl(const AtomicString& str) {
diff --git a/third_party/blink/renderer/core/html/link_web_bundle.h b/third_party/blink/renderer/core/html/link_web_bundle.h
index 9d786edd..96cabd1 100644
--- a/third_party/blink/renderer/core/html/link_web_bundle.h
+++ b/third_party/blink/renderer/core/html/link_web_bundle.h
@@ -5,31 +5,50 @@
 #ifndef THIRD_PARTY_BLINK_RENDERER_CORE_HTML_LINK_WEB_BUNDLE_H_
 #define THIRD_PARTY_BLINK_RENDERER_CORE_HTML_LINK_WEB_BUNDLE_H_
 
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "services/network/public/mojom/url_loader_factory.mojom-blink.h"
 #include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/core/html/link_resource.h"
+#include "third_party/blink/renderer/platform/loader/fetch/subresource_web_bundle.h"
 #include "third_party/blink/renderer/platform/weborigin/kurl.h"
 
 namespace blink {
 
+class WebBundleLoader;
+
 // LinkWebBundle is used in the Subresource loading with Web Bundles feature.
 // See crbug.com/1082020 for details.
 // A <link rel="webbundle" ...> element creates LinkWebBundle.
-class CORE_EXPORT LinkWebBundle final : public LinkResource {
+class CORE_EXPORT LinkWebBundle final : public LinkResource,
+                                        public SubresourceWebBundle {
+  USING_GARBAGE_COLLECTED_MIXIN(LinkWebBundle);
+
  public:
   explicit LinkWebBundle(HTMLLinkElement* owner);
-  ~LinkWebBundle() override = default;
+  ~LinkWebBundle() override;
+  void Trace(Visitor* visitor) const override;
 
+  void NotifyLoaded();
+
+  // LinkResource overrides:
   void Process() override;
   LinkResourceType GetType() const override;
   bool HasLoaded() const override;
   void OwnerRemoved() override;
 
+  // SubresourceWebBundle overrides:
+  bool CanHandleRequest(const KURL& url) const override;
+  mojo::PendingRemote<network::mojom::blink::URLLoaderFactory>
+  GetURLLoaderFactory() override;
+
   // Parse the given |str| as a url. If |str| doesn't meet the criteria which
   // WebBundles specification requires, this returns invalid empty KURL as an
   // error.
   // See
   // https://wicg.github.io/webpackage/draft-yasskin-wpack-bundled-exchanges.html#name-parsing-the-index-section
   static KURL ParseResourceUrl(const AtomicString& str);
+
+  Member<WebBundleLoader> bundle_loader_;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/platform/loader/BUILD.gn b/third_party/blink/renderer/platform/loader/BUILD.gn
index 900230a..1f64193 100644
--- a/third_party/blink/renderer/platform/loader/BUILD.gn
+++ b/third_party/blink/renderer/platform/loader/BUILD.gn
@@ -104,6 +104,7 @@
     "fetch/source_keyed_cached_metadata_handler.h",
     "fetch/stale_revalidation_resource_client.cc",
     "fetch/stale_revalidation_resource_client.h",
+    "fetch/subresource_web_bundle.h",
     "fetch/text_resource_decoder_options.cc",
     "fetch/text_resource_decoder_options.h",
     "fetch/trust_token_params_conversion.cc",
diff --git a/third_party/blink/renderer/platform/loader/fetch/resource_fetcher.cc b/third_party/blink/renderer/platform/loader/fetch/resource_fetcher.cc
index be894cdc..2d452ac 100644
--- a/third_party/blink/renderer/platform/loader/fetch/resource_fetcher.cc
+++ b/third_party/blink/renderer/platform/loader/fetch/resource_fetcher.cc
@@ -37,6 +37,7 @@
 #include "base/metrics/histogram_macros.h"
 #include "base/time/time.h"
 #include "services/network/public/cpp/request_mode.h"
+#include "services/network/public/mojom/url_loader_factory.mojom-blink.h"
 #include "third_party/blink/public/common/features.h"
 #include "third_party/blink/public/common/mime_util/mime_util.h"
 #include "third_party/blink/public/common/thread_safe_browser_interface_broker_proxy.h"
@@ -69,6 +70,7 @@
 #include "third_party/blink/renderer/platform/loader/fetch/resource_loading_log.h"
 #include "third_party/blink/renderer/platform/loader/fetch/resource_timing_info.h"
 #include "third_party/blink/renderer/platform/loader/fetch/stale_revalidation_resource_client.h"
+#include "third_party/blink/renderer/platform/loader/fetch/subresource_web_bundle.h"
 #include "third_party/blink/renderer/platform/loader/fetch/unique_identifier.h"
 #include "third_party/blink/renderer/platform/mhtml/archive_resource.h"
 #include "third_party/blink/renderer/platform/mhtml/mhtml_archive.h"
@@ -1260,6 +1262,15 @@
     const ResourceLoaderOptions& options) {
   DCHECK(!GetProperties().IsDetached());
   DCHECK(loader_factory_);
+  for (auto& bundle : subresource_web_bundles_) {
+    if (!bundle->CanHandleRequest(request.Url()))
+      continue;
+    ResourceLoaderOptions new_options(options);
+    new_options.url_loader_factory = base::MakeRefCounted<base::RefCountedData<
+        mojo::PendingRemote<network::mojom::blink::URLLoaderFactory>>>(
+        bundle->GetURLLoaderFactory());
+    return loader_factory_->CreateURLLoader(request, new_options, task_runner_);
+  }
   return loader_factory_->CreateURLLoader(request, options, task_runner_);
 }
 
@@ -2227,6 +2238,17 @@
   return frame_or_worker_scheduler_.get();
 }
 
+void ResourceFetcher::AddSubresourceWebBundle(
+    SubresourceWebBundle& subresource_web_bundle) {
+  DCHECK(RuntimeEnabledFeatures::SubresourceWebBundlesEnabled());
+  subresource_web_bundles_.insert(&subresource_web_bundle);
+}
+
+void ResourceFetcher::RemoveSubresourceWebBundle(
+    SubresourceWebBundle& subresource_web_bundle) {
+  subresource_web_bundles_.erase(&subresource_web_bundle);
+}
+
 void ResourceFetcher::Trace(Visitor* visitor) const {
   visitor->Trace(context_);
   visitor->Trace(properties_);
@@ -2245,6 +2267,7 @@
   visitor->Trace(matched_preloads_);
   visitor->Trace(resource_timing_info_map_);
   visitor->Trace(blob_registry_remote_);
+  visitor->Trace(subresource_web_bundles_);
 }
 
 // static
diff --git a/third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h b/third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h
index 1e52ae1..c49fea4 100644
--- a/third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h
+++ b/third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h
@@ -61,6 +61,7 @@
 class ResourceError;
 class ResourceLoadObserver;
 class ResourceTimingInfo;
+class SubresourceWebBundle;
 class WebURLLoader;
 struct ResourceFetcherInit;
 struct ResourceLoaderOptions;
@@ -282,6 +283,9 @@
     scheduler_->SetThrottleOptionOverride(throttle_option_override);
   }
 
+  void AddSubresourceWebBundle(SubresourceWebBundle& subresource_web_bundle);
+  void RemoveSubresourceWebBundle(SubresourceWebBundle& subresource_web_bundle);
+
  private:
   friend class ResourceCacheValidationSuppressor;
   enum class StopFetchingTarget {
@@ -437,6 +441,8 @@
                  HeapMojoWrapperMode::kWithoutContextObserver>
       blob_registry_remote_;
 
+  HeapHashSet<Member<SubresourceWebBundle>> subresource_web_bundles_;
+
   // This is not in the bit field below because we want to use AutoReset.
   bool is_in_request_resource_ = false;
 
diff --git a/third_party/blink/renderer/platform/loader/fetch/subresource_web_bundle.h b/third_party/blink/renderer/platform/loader/fetch/subresource_web_bundle.h
new file mode 100644
index 0000000..6079ec8
--- /dev/null
+++ b/third_party/blink/renderer/platform/loader/fetch/subresource_web_bundle.h
@@ -0,0 +1,31 @@
+// Copyright 2020 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 THIRD_PARTY_BLINK_RENDERER_PLATFORM_LOADER_FETCH_SUBRESOURCE_WEB_BUNDLE_H_
+#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_LOADER_FETCH_SUBRESOURCE_WEB_BUNDLE_H_
+
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "services/network/public/mojom/url_loader_factory.mojom-blink-forward.h"
+#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
+#include "third_party/blink/renderer/platform/platform_export.h"
+
+namespace blink {
+
+class KURL;
+
+// SubresourceWebBundle is attached to ResourceFetcher and used to intercept
+// subresource requests for a certain set of URLs and serve responses from a
+// WebBundle. This is used for Subresource loading with Web Bundles
+// (https://github.com/WICG/webpackage/blob/master/explainers/subresource-loading.md).
+class PLATFORM_EXPORT SubresourceWebBundle : public GarbageCollectedMixin {
+ public:
+  void Trace(Visitor* visitor) const override {}
+  virtual bool CanHandleRequest(const KURL& url) const = 0;
+  virtual mojo::PendingRemote<network::mojom::blink::URLLoaderFactory>
+  GetURLLoaderFactory() = 0;
+};
+
+}  // namespace blink
+
+#endif  // THIRD_PARTY_BLINK_RENDERER_PLATFORM_LOADER_FETCH_SUBRESOURCE_WEB_BUNDLE_H_
diff --git a/third_party/blink/web_tests/external/wpt/lint.ignore b/third_party/blink/web_tests/external/wpt/lint.ignore
index 23cf860..40b7cb2 100644
--- a/third_party/blink/web_tests/external/wpt/lint.ignore
+++ b/third_party/blink/web_tests/external/wpt/lint.ignore
@@ -45,6 +45,7 @@
 TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.wasm
 TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.bmp
 TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.sxg
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.wbn
 
 ## .gitignore
 W3C-TEST.ORG: .gitignore
@@ -706,6 +707,7 @@
 # Web Bundle files have hard-coded URLs
 WEB-PLATFORM.TEST:web-bundle/resources/generate-test-wbns.sh
 WEB-PLATFORM.TEST:web-bundle/resources/wbn/*.wbn
+WEB-PLATFORM.TEST:web-bundle/subresource-loading/subresource-loading-from-web-bundle.tentative.html
 
 # Tests that depend on resources in /gen/ in Chromium:
 # https://github.com/web-platform-tests/wpt/issues/16455
diff --git a/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic/resource1.js b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic/resource1.js
new file mode 100644
index 0000000..7bd43736
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic/resource1.js
@@ -0,0 +1 @@
+export const result = 'resource1 from network';
diff --git a/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic/resource2.js b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic/resource2.js
new file mode 100644
index 0000000..09a4ee7
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic/resource2.js
@@ -0,0 +1 @@
+export const result = 'resource2 from network';
diff --git a/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic/resource3.js b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic/resource3.js
new file mode 100644
index 0000000..851168c
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic/resource3.js
@@ -0,0 +1 @@
+export const result = 'resource3 from network';
diff --git a/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic/resource4.js b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic/resource4.js
new file mode 100644
index 0000000..ea456b8
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic/resource4.js
@@ -0,0 +1 @@
+export const result = 'resource4 from network';
diff --git a/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic1/resource1.js b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic1/resource1.js
new file mode 100644
index 0000000..6fd1de6
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic1/resource1.js
@@ -0,0 +1 @@
+export const result = 'resource1 from dynamic1.wbn';
diff --git a/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic1/resource2.js b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic1/resource2.js
new file mode 100644
index 0000000..3cf012b
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic1/resource2.js
@@ -0,0 +1 @@
+export const result = 'resource2 from dynamic1.wbn';
diff --git a/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic1/resource3.js b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic1/resource3.js
new file mode 100644
index 0000000..c8a4e25
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic1/resource3.js
@@ -0,0 +1 @@
+export const result = 'resource3 from dynamic1.wbn';
diff --git a/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic1/resource4.js b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic1/resource4.js
new file mode 100644
index 0000000..0b2f792
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic1/resource4.js
@@ -0,0 +1 @@
+export const result = 'resource4 from dynamic1.wbn';
diff --git a/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic2/resource1.js b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic2/resource1.js
new file mode 100644
index 0000000..c6f751ce
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic2/resource1.js
@@ -0,0 +1 @@
+export const result = 'resource1 from dynamic2.wbn';
diff --git a/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic2/resource2.js b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic2/resource2.js
new file mode 100644
index 0000000..b4278ee1c
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic2/resource2.js
@@ -0,0 +1 @@
+export const result = 'resource2 from dynamic2.wbn';
diff --git a/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic2/resource3.js b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic2/resource3.js
new file mode 100644
index 0000000..0dad7ea
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic2/resource3.js
@@ -0,0 +1 @@
+export const result = 'resource3 from dynamic2.wbn';
diff --git a/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic2/resource4.js b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic2/resource4.js
new file mode 100644
index 0000000..e51cf7f
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/web-bundle/resources/dynamic2/resource4.js
@@ -0,0 +1 @@
+export const result = 'resource4 from dynamic2.wbn';
diff --git a/third_party/blink/web_tests/external/wpt/web-bundle/resources/generate-test-wbns.sh b/third_party/blink/web_tests/external/wpt/web-bundle/resources/generate-test-wbns.sh
index 820bb12..c3dd8eb 100755
--- a/third_party/blink/web_tests/external/wpt/web-bundle/resources/generate-test-wbns.sh
+++ b/third_party/blink/web_tests/external/wpt/web-bundle/resources/generate-test-wbns.sh
@@ -10,11 +10,33 @@
 
 # TODO: Stop hard-coding "web-platform.test" when generating Web Bundles on the
 # fly.
-wpt_test_origin=https://web-platform.test:8444
+wpt_test_https_origin=https://web-platform.test:8444
+wpt_test_http_origin=http://web-platform.test:8001
 
 gen-bundle \
   -version b1 \
-  -baseURL $wpt_test_origin/web-bundle/resources/wbn/ \
-  -primaryURL $wpt_test_origin/web-bundle/resources/wbn/location.html \
+  -baseURL $wpt_test_https_origin/web-bundle/resources/wbn/ \
+  -primaryURL $wpt_test_https_origin/web-bundle/resources/wbn/location.html \
   -dir location/ \
   -o wbn/location.wbn
+
+gen-bundle \
+  -version b1 \
+  -baseURL https://subresource-wbn.example/ \
+  -primaryURL https://subresource-wbn.example/root.js \
+  -dir subresource/ \
+  -o wbn/subresource.wbn
+
+gen-bundle \
+  -version b1 \
+  -baseURL $wpt_test_http_origin/web-bundle/resources/dynamic/ \
+  -primaryURL $wpt_test_http_origin/web-bundle/resources/dynamic/resource1.js \
+  -dir dynamic1/ \
+  -o wbn/dynamic1.wbn
+
+gen-bundle \
+  -version b1 \
+  -baseURL $wpt_test_http_origin/web-bundle/resources/dynamic/ \
+  -primaryURL $wpt_test_http_origin/web-bundle/resources/dynamic/resource1.js \
+  -dir dynamic2/ \
+  -o wbn/dynamic2.wbn
diff --git a/third_party/blink/web_tests/external/wpt/web-bundle/resources/subresource/root.js b/third_party/blink/web_tests/external/wpt/web-bundle/resources/subresource/root.js
new file mode 100644
index 0000000..2c2a465
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/web-bundle/resources/subresource/root.js
@@ -0,0 +1 @@
+export * from './submodule.js';
diff --git a/third_party/blink/web_tests/external/wpt/web-bundle/resources/subresource/submodule.js b/third_party/blink/web_tests/external/wpt/web-bundle/resources/subresource/submodule.js
new file mode 100644
index 0000000..4561aaf0
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/web-bundle/resources/subresource/submodule.js
@@ -0,0 +1 @@
+export const result = 'OK';
diff --git a/third_party/blink/web_tests/external/wpt/web-bundle/resources/wbn/dynamic1.wbn b/third_party/blink/web_tests/external/wpt/web-bundle/resources/wbn/dynamic1.wbn
new file mode 100644
index 0000000..22db19e7
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/web-bundle/resources/wbn/dynamic1.wbn
Binary files differ
diff --git a/third_party/blink/web_tests/external/wpt/web-bundle/resources/wbn/dynamic2.wbn b/third_party/blink/web_tests/external/wpt/web-bundle/resources/wbn/dynamic2.wbn
new file mode 100644
index 0000000..44c4b1e
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/web-bundle/resources/wbn/dynamic2.wbn
Binary files differ
diff --git a/third_party/blink/web_tests/external/wpt/web-bundle/resources/wbn/subresource.wbn b/third_party/blink/web_tests/external/wpt/web-bundle/resources/wbn/subresource.wbn
new file mode 100644
index 0000000..395039dc
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/web-bundle/resources/wbn/subresource.wbn
Binary files differ
diff --git a/third_party/blink/web_tests/external/wpt/web-bundle/subresource-loading/subresource-loading-from-web-bundle.tentative.html b/third_party/blink/web_tests/external/wpt/web-bundle/subresource-loading/subresource-loading-from-web-bundle.tentative.html
new file mode 100644
index 0000000..e352646
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/web-bundle/subresource-loading/subresource-loading-from-web-bundle.tentative.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<title>Subresource loading with link rel="webbundle"</title>
+<link
+  rel="help"
+  href="https://github.com/WICG/webpackage/blob/master/explainers/subresource-loading.md"
+/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+  <link rel="webbundle" href="../resources/wbn/subresource.wbn"
+        resources="https://subresource-wbn.example/root.js https://subresource-wbn.example/submodule.js" />
+  <script>
+    promise_test(async () => {
+      const module = await import('https://subresource-wbn.example/root.js');
+      assert_equals(module.result, 'OK');
+    }, "Subresource loading with WebBundle");
+
+    promise_test(async () => {
+      const link = document.createElement("link");
+      link.rel = "webbundle";
+      link.href = "../resources/wbn/dynamic1.wbn";
+      link.resources.add('http://web-platform.test:8001/web-bundle/resources/dynamic/resource1.js',
+                         'http://web-platform.test:8001/web-bundle/resources/dynamic/resource2.js',
+                         'http://web-platform.test:8001/web-bundle/resources/dynamic/resource4.js');
+      document.body.appendChild(link);
+
+      const module = await import('http://web-platform.test:8001/web-bundle/resources/dynamic/resource1.js');
+      assert_equals(module.result, 'resource1 from dynamic1.wbn');
+
+      link.href = "../resources/wbn/dynamic2.wbn";
+      const module2 = await import('http://web-platform.test:8001/web-bundle/resources/dynamic/resource2.js');
+      assert_equals(module2.result, 'resource2 from dynamic2.wbn');
+
+      // A resource not specified in the resources attribute, but in the bundle.
+      const module3 = await import('http://web-platform.test:8001/web-bundle/resources/dynamic/resource3.js');
+      assert_equals(module3.result, 'resource3 from network');
+
+      document.body.removeChild(link);
+      const module4 = await import('http://web-platform.test:8001/web-bundle/resources/dynamic/resource4.js');
+      assert_equals(module4.result, 'resource4 from network');
+    }, 'Dynamically adding / updating / removing "<link rel=webbundle>"');
+
+    promise_test(() => {
+      return new Promise((resolve, reject) => {
+        const link = document.createElement("link");
+        link.rel = "webbundle";
+        link.href = "../resources/wbn/dynamic1.wbn?test-event";
+        link.>
+        link.>
+        document.body.appendChild(link);
+      });
+    }, '<link rel="webbundle"> fires a load event on load success');
+
+    promise_test(() => {
+      return new Promise((resolve, reject) => {
+        const link = document.createElement("link");
+        link.rel = "webbundle";
+        link.href = "../resources/wbn/nonexistent.wbn";
+        link.>
+        link.>
+        document.body.appendChild(link);
+      });
+    }, '<link rel="webbundle"> fires an error event on load failure');
+  </script>
+</body>