Import Cobalt 23.lts.3.310436
diff --git a/base/values.cc b/base/values.cc
index ade6b6f..476c94c 100644
--- a/base/values.cc
+++ b/base/values.cc
@@ -415,7 +415,6 @@
}
bool Value::RemovePath(std::initializer_list<StringPiece> path) {
- DCHECK_GE(path.size(), 2u) << "Use RemoveKey() for a path of length 1.";
return RemovePath(make_span(path.begin(), path.size()));
}
diff --git a/build/toolchain/cc_wrapper.gni b/build/toolchain/cc_wrapper.gni
index f2d368d..bdb8cea 100644
--- a/build/toolchain/cc_wrapper.gni
+++ b/build/toolchain/cc_wrapper.gni
@@ -41,6 +41,15 @@
}
}
+if (is_starboard) {
+ declare_args() {
+ # Set to false to completely ignore the cc_wrapper.
+ enable_cc_wrapper = true
+ }
+ assert(enable_cc_wrapper || cc_wrapper == "",
+ "Do not set `cc_wrapper` if you set `enable_cc_wrapper` to false.")
+}
+
if (is_starboard && getenv("SCCACHE") == "") {
enable_sccache = host_os == "win" && cobalt_fastbuild
}
@@ -51,13 +60,14 @@
_set_sccache_gcs_oauth_url = getenv("SCCACHE_GCS_OAUTH_URL") != ""
_set_sccache_gcs_rw_mode = getenv("SCCACHE_GCS_RW_MODE") != ""
_set_sccache_dir = getenv("SCCACHE_DIR") != ""
- assert((_set_sccache_dir) || (_set_sccache_gcs_bucket &&
- (_set_sccache_gcs_key_path || _set_sccache_gcs_oauth_url) &&
- _set_sccache_gcs_rw_mode),
+ assert(_set_sccache_dir ||
+ (_set_sccache_gcs_bucket &&
+ (_set_sccache_gcs_key_path || _set_sccache_gcs_oauth_url) &&
+ _set_sccache_gcs_rw_mode),
"Set Sccache environment variables before use.")
}
-if (is_starboard && cc_wrapper == "") {
+if (is_starboard && cc_wrapper == "" && enable_cc_wrapper) {
# TODO(https://crbug.com/gn/273): Use sccache locally as well.
if (enable_sccache) {
cc_wrapper = "sccache"
diff --git a/build/toolchain/win/msvc_toolchain.gni b/build/toolchain/win/msvc_toolchain.gni
index 52225b0..92c98d4 100644
--- a/build/toolchain/win/msvc_toolchain.gni
+++ b/build/toolchain/win/msvc_toolchain.gni
@@ -141,6 +141,9 @@
rspfile_content = "{{inputs_newline}}"
}
+ # TODO(b/217794556): All following linker tools should list the PDB file in
+ # their outputs. It has been removed as it is not always generated, and
+ # ninja will treat the missing file as a dirty edge.
tool("solink") {
# E.g. "foo.dll":
dllname = "{{output_dir}}/{{target_output_name}}{{output_extension}}"
@@ -157,13 +160,11 @@
outputs = [
dllname,
libname,
- pdbname,
]
link_output = libname
depend_output = libname
runtime_outputs = [
dllname,
- pdbname,
]
# Since the above commands only updates the .lib file when it changes, ask
@@ -190,7 +191,6 @@
description = "LINK_MODULE(DLL) {{output}}"
outputs = [
dllname,
- pdbname,
]
runtime_outputs = outputs
@@ -212,7 +212,6 @@
description = "LINK {{output}}"
outputs = [
exename,
- pdbname,
]
runtime_outputs = outputs
diff --git a/cobalt/base/source_location.h b/cobalt/base/source_location.h
index e4a5e5c..dfedda2 100644
--- a/cobalt/base/source_location.h
+++ b/cobalt/base/source_location.h
@@ -34,6 +34,7 @@
//
// Line and column numbers are 1-based.
struct SourceLocation {
+ SourceLocation() = default;
SourceLocation(const std::string& file_path, int line_number,
int column_number)
: file_path(file_path),
diff --git a/cobalt/base/statistics.h b/cobalt/base/statistics.h
index 90dbe3e..b154573 100644
--- a/cobalt/base/statistics.h
+++ b/cobalt/base/statistics.h
@@ -17,8 +17,10 @@
#include <algorithm>
#include <functional>
+#include <string>
#include <vector>
+#include "cobalt/base/c_val.h"
#include "starboard/types.h"
namespace base {
@@ -66,7 +68,7 @@
internal::DefaultSampleToValueFunc>
class Statistics {
public:
- constexpr Statistics() = default;
+ explicit Statistics(const std::string& metric_name) {}
void AddSample(DividendType dividend, DivisorType divisor) {}
int64_t accumulated_dividend() const { return 0; }
@@ -85,7 +87,11 @@
internal::DefaultSampleToValueFunc>
class Statistics {
public:
- constexpr Statistics() = default;
+ explicit Statistics(const std::string& metric_name)
+ : last_sample_value_(metric_name + ".Latest", 0,
+ metric_name + " most recent value."),
+ median_sample_value_(metric_name + ".Median", 0,
+ metric_name + " median value.") {}
void AddSample(DividendType dividend, DivisorType divisor) {
static_assert(MaxSamples > 0, "MaxSamples has to be greater than 0.");
@@ -111,6 +117,9 @@
min_ = max_ = value;
first_sample_added_ = true;
}
+ last_sample_value_ = value;
+ // TODO(b/258531018) this should be optimized in future.
+ median_sample_value_ = GetMedian();
}
int64_t accumulated_dividend() const { return accumulated_dividend_; }
@@ -174,6 +183,9 @@
Sample samples_[MaxSamples] = {}; // Ring buffer for samples.
size_t number_of_samples_ = 0; // Number of samples in |samples_|.
size_t first_sample_index_ = 0; // Index of the first sample in |samples_|.
+
+ base::CVal<int64_t> last_sample_value_;
+ base::CVal<int64_t> median_sample_value_;
};
#endif // defined(COBALT_BUILD_TYPE_GOLD)
diff --git a/cobalt/base/statistics_test.cc b/cobalt/base/statistics_test.cc
index beefdb9..60fcd09 100644
--- a/cobalt/base/statistics_test.cc
+++ b/cobalt/base/statistics_test.cc
@@ -20,7 +20,7 @@
namespace {
TEST(StatisticsTest, ZeroSamples) {
- Statistics<int, int, 1> statistics;
+ Statistics<int, int, 1> statistics("test1");
EXPECT_EQ(statistics.accumulated_dividend(), 0);
EXPECT_EQ(statistics.accumulated_divisor(), 0);
@@ -31,7 +31,7 @@
}
TEST(StatisticsTest, SingleSample) {
- Statistics<int, int, 1> statistics;
+ Statistics<int, int, 1> statistics("test2");
statistics.AddSample(100, 10);
@@ -44,7 +44,7 @@
}
TEST(StatisticsTest, NotCrashOnZeroDivisor) {
- Statistics<int, int, 4> statistics;
+ Statistics<int, int, 4> statistics("test3");
statistics.AddSample(100, 0);
@@ -58,7 +58,7 @@
}
TEST(StatisticsTest, MultipleSamples) {
- Statistics<int, int, 4> statistics;
+ Statistics<int, int, 4> statistics("test4");
statistics.AddSample(10, 2);
statistics.AddSample(20, 2);
@@ -83,8 +83,8 @@
}
TEST(StatisticsTest, MultipleSamplesDifferentOrders) {
- Statistics<int, int, 10> statistics;
- Statistics<int, int, 10> statistics_reversed;
+ Statistics<int, int, 10> statistics("test5");
+ Statistics<int, int, 10> statistics_reversed("reverse test5");
for (int i = 0; i < 10; ++i) {
statistics.AddSample(i * i, i * 3);
@@ -105,16 +105,16 @@
}
TEST(StatisticsTest, MoreSamplesThanCapacity) {
- Statistics<int, int, 1> statistics_1;
- Statistics<int, int, 10> statistics_10;
+ Statistics<int, int, 1> statistics_1("test6");
+ Statistics<int, int, 10> statistics_10("test7");
for (int i = 0; i < 5; ++i) {
statistics_1.AddSample(i * i, i * 3);
statistics_10.AddSample(i * i, i * 3);
}
- Statistics<int, int, 1> statistics_1_median_reference;
- Statistics<int, int, 10> statistics_10_median_reference;
+ Statistics<int, int, 1> statistics_1_median_reference("test8");
+ Statistics<int, int, 10> statistics_10_median_reference("test9");
for (int i = 0; i < 10; ++i) {
statistics_1.AddSample(i * i * i, i * 5);
@@ -145,7 +145,7 @@
}
TEST(StatisticsTest, MedianWithOverflow) {
- Statistics<int, int, 3> statistics;
+ Statistics<int, int, 3> statistics("test10");
statistics.AddSample(1, 1);
statistics.AddSample(11, 1);
diff --git a/cobalt/base/tokens.h b/cobalt/base/tokens.h
index 5629893..0b82b64 100644
--- a/cobalt/base/tokens.h
+++ b/cobalt/base/tokens.h
@@ -92,6 +92,7 @@
MacroOpWithNameOnly(offline) \
MacroOpWithNameOnly(online) \
MacroOpWithNameOnly(open) \
+ MacroOpWithNameOnly(pointercancel) \
MacroOpWithNameOnly(pointerdown) \
MacroOpWithNameOnly(pointerenter) \
MacroOpWithNameOnly(pointerleave) \
diff --git a/cobalt/bindings/contexts.py b/cobalt/bindings/contexts.py
index fb03640..10b02da 100644
--- a/cobalt/bindings/contexts.py
+++ b/cobalt/bindings/contexts.py
@@ -345,7 +345,9 @@
return base_type + '*'
if is_any_type(idl_type) or is_array_buffer_or_view_type(idl_type):
return 'const ::cobalt::script::ScriptValue<%s>*' % base_type
- elif idl_type.is_string_type or idl_type.is_interface_type:
+ elif cobalt_type_is_optional(idl_type) or is_sequence_type(idl_type) or (
+ idl_type.is_string_type or idl_type.is_interface_type or
+ idl_type.is_union_type):
return 'const %s&' % base_type
return base_type
diff --git a/cobalt/bindings/shared/idl_conditional_macros.h b/cobalt/bindings/shared/idl_conditional_macros.h
index 3dd9d74..a34191f 100644
--- a/cobalt/bindings/shared/idl_conditional_macros.h
+++ b/cobalt/bindings/shared/idl_conditional_macros.h
@@ -27,8 +27,4 @@
// attribute.
#define COBALT_ENABLE_ON_SCREEN_KEYBOARD
-// This is used to conditionally define setMaxVideoCapabilities() in
-// HTMLVideoElement.
-#define COBALT_ENABLE_SET_MAX_VIDEO_CAPABILITIES
-
#endif // COBALT_BINDINGS_SHARED_IDL_CONDITIONAL_MACROS_H_
diff --git a/cobalt/bindings/testing/array_buffers_test.cc b/cobalt/bindings/testing/array_buffers_test.cc
index e14289c..bf4139e 100644
--- a/cobalt/bindings/testing/array_buffers_test.cc
+++ b/cobalt/bindings/testing/array_buffers_test.cc
@@ -61,24 +61,23 @@
}
{
- std::unique_ptr<script::PreallocatedArrayBufferData> preallocated_data(
- new script::PreallocatedArrayBufferData(256));
- EXPECT_EQ(256, preallocated_data->byte_length());
- for (int i = 0; i < preallocated_data->byte_length(); i++) {
- reinterpret_cast<uint8_t*>(preallocated_data->data())[i] = i;
+ script::PreallocatedArrayBufferData preallocated_data(256);
+ EXPECT_EQ(256, preallocated_data.byte_length());
+ for (int i = 0; i < preallocated_data.byte_length(); i++) {
+ preallocated_data.data()[i] = i;
}
- void* data_pointer = preallocated_data->data();
-
+ void* data_pointer = preallocated_data.data();
auto array_buffer = script::ArrayBuffer::New(global_environment_,
std::move(preallocated_data));
+
+ EXPECT_EQ(nullptr, preallocated_data.data());
EXPECT_EQ(256, array_buffer->ByteLength());
EXPECT_EQ(data_pointer, array_buffer->Data());
+
for (int i = 0; i < 256; i++) {
EXPECT_EQ(i, reinterpret_cast<uint8_t*>(array_buffer->Data())[i]);
}
-
- EXPECT_EQ(nullptr, preallocated_data.get());
}
}
diff --git a/cobalt/black_box_tests/black_box_tests.py b/cobalt/black_box_tests/black_box_tests.py
index 34b2ffe..510e893 100644
--- a/cobalt/black_box_tests/black_box_tests.py
+++ b/cobalt/black_box_tests/black_box_tests.py
@@ -62,17 +62,24 @@
_TESTS_NO_SIGNAL = [
'allow_eval',
'compression_test',
+ 'default_site_can_load',
'disable_eval_with_csp',
+ 'h5vcc_storage_write_verify_test',
'http_cache',
'persistent_cookie',
+ 'service_worker_cache_keys_test',
+ 'service_worker_get_registrations_test',
+ 'service_worker_fetch_test',
+ 'service_worker_message_test',
+ 'service_worker_test',
+ 'service_worker_persist_test',
'soft_mic_platform_service_test',
'text_encoding_test',
'web_debugger',
'web_platform_tests',
'web_worker_test',
'worker_csp_test',
- 'service_worker_message_test',
- 'service_worker_test',
+ 'worker_load_test',
]
# These tests can only be run on platforms whose app launcher can send deep
# links.
@@ -93,7 +100,7 @@
"""Base class for Cobalt black box test cases."""
def __init__(self, *args, **kwargs):
- super(BlackBoxTestCase, self).__init__(*args, **kwargs)
+ super().__init__(*args, **kwargs)
self.launcher_params = _launcher_params
self.platform_config = build.GetPlatformConfig(_launcher_params.platform)
self.cobalt_config = self.platform_config.GetApplicationConfiguration(
@@ -109,7 +116,7 @@
super(BlackBoxTestCase, cls).tearDownClass()
logging.info('Done %s', cls.__name__)
- def CreateCobaltRunner(self, url, target_params=None):
+ def CreateCobaltRunner(self, url=None, target_params=None):
all_target_params = list(target_params) if target_params else []
if _launcher_params.target_params is not None:
all_target_params += _launcher_params.target_params
@@ -182,13 +189,12 @@
# be able to bind correctly with incomplete support of IPv6
if device_id and IsValidIpAddress(device_id):
_launcher_params.target_params.append(
- '--dev_servers_listen_ip={}'.format(device_id))
+ f'--dev_servers_listen_ip={device_id}')
elif IsValidIpAddress(server_binding_address):
_launcher_params.target_params.append(
- '--dev_servers_listen_ip={}'.format(server_binding_address))
+ f'--dev_servers_listen_ip={server_binding_address}')
_launcher_params.target_params.append(
- '--web-platform-test-server=http://web-platform.test:{}'.format(
- wpt_http_port))
+ f'--web-platform-test-server=http://web-platform.test:{wpt_http_port}')
# Port used to create the proxy server. If not specified, a random free
# port is used.
@@ -196,8 +202,8 @@
proxy_port = str(self.GetUnusedPort([server_binding_address]))
if proxy_address is None:
proxy_address = server_binding_address
- _launcher_params.target_params.append('--proxy=%s:%s' %
- (proxy_address, proxy_port))
+ _launcher_params.target_params.append(
+ f'--proxy={proxy_address}:{proxy_port}')
self.proxy_port = proxy_port
self.test_name = test_name
@@ -221,10 +227,10 @@
out_dir = _launcher_params.out_directory
is_ci = out_dir and 'mh_lab' in out_dir # pylint: disable=unsupported-membership-test
- target = (_launcher_params.platform, _launcher_params.config)
- if is_ci and '{}/{}'.format(*target) in _DISABLED_BLACKBOXTEST_CONFIGS:
+ if is_ci and (f'{_launcher_params.platform}/{_launcher_params.config}'
+ in _DISABLED_BLACKBOXTEST_CONFIGS):
logging.warning('Blackbox tests disabled for platform:%s config:%s',
- *target)
+ _launcher_params.platform, _launcher_params.config)
return 0
logging.info('Using proxy port: %s', self.proxy_port)
@@ -238,8 +244,9 @@
importlib.import_module(_TEST_DIR_PATH + self.test_name))
else:
suite = LoadTests(_launcher_params)
+ # Using verbosity=2 to log individual test function names and results.
return_code = not unittest.TextTestRunner(
- verbosity=0, stream=sys.stdout).run(suite).wasSuccessful()
+ verbosity=2, stream=sys.stdout).run(suite).wasSuccessful()
return return_code
def GetUnusedPort(self, addresses):
diff --git a/cobalt/black_box_tests/testdata/black_box_js_test_utils.js b/cobalt/black_box_tests/testdata/black_box_js_test_utils.js
index d488a55..e4dfc8a 100644
--- a/cobalt/black_box_tests/testdata/black_box_js_test_utils.js
+++ b/cobalt/black_box_tests/testdata/black_box_js_test_utils.js
@@ -1,3 +1,17 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
// This file provides JavaScript-side environment and test utility functions.
// The following constants and logics are used in communication with the python
// tests. Anyone making changes here should also ensure corresponding changes
@@ -9,63 +23,105 @@
const SETUP_DONE_MESSAGE = 'JavaScript_setup_done';
const EFFECT_AFTER_VISIBILITY_CHANGE_TIMEOUT_SECONDS = 5;
-function notReached() {
- // Show the stack that triggered this function.
- try { throw Error('') } catch (error_object) {
- console.log(`${error_object.stack}`);
- }
- document.body.setAttribute(TEST_STATUS_ELEMENT_NAME, FAILURE_MESSAGE);
+let tearDown = () => {};
+function setTearDown(fn) {
+ tearDown = fn;
}
-function assertTrue(result) {
- if (!result){
- notReached();
+function failed() {
+ return document.body.getAttribute(TEST_STATUS_ELEMENT_NAME) === FAILURE_MESSAGE;
+}
+
+function printError(error) {
+ if (!error) {
+ return;
+ }
+ if (!error.message && !error.stack) {
+ console.error(error);
+ return;
+ }
+ if (error.stack) {
+ console.error('\n' + error.stack);
+ return;
+ }
+ console.error(error.message);
+}
+
+function notReached(error) {
+ if (failed()) {
+ console.error('Already failed.');
+ return;
+ }
+ if (!error) {
+ error = Error('');
+ }
+ Promise.resolve(tearDown()).then(() => {
+ printError(error);
+ document.body.setAttribute(TEST_STATUS_ELEMENT_NAME, FAILURE_MESSAGE);
+ });
+}
+
+function assertTrue(result, msg) {
+ if (!result) {
+ const errorMessage = '\n' +
+ 'Black Box Test Assertion failed: \n' +
+ 'expected: true\n' +
+ 'but got: ' + result +
+ (msg ? `\n${msg}` : '');
+ notReached(new Error(errorMessage));
}
}
function assertFalse(result) {
- if (result){
- notReached();
+ if (result) {
+ const errorMessage = '\n' +
+ 'Black Box Test Assertion failed: \n' +
+ 'expected: false\n' +
+ 'but got: ' + result +
+ (msg ? `\n${msg}` : '');
+ notReached(new Error(errorMessage));
}
}
-function assertEqual(expected, result) {
+function assertEqual(expected, result, msg) {
if (expected !== result) {
- console.log('\n' +
+ const errorMessage = '\n' +
'Black Box Test Equal Test Assertion failed: \n' +
'expected: ' + expected + '\n' +
- 'but got: ' + result);
- notReached();
+ 'but got: ' + result +
+ (msg ? `\n${msg}` : '');
+ notReached(new Error(errorMessage));
}
}
-function assertIncludes(expected, result) {
+function assertIncludes(expected, result, msg) {
if (!result || !result.includes(expected)) {
- console.log('\n' +
+ const errorMessage = '\n' +
'Black Box Test Equal Test Assertion failed: \n' +
'expected includes: ' + expected + '\n' +
- 'but got: ' + result);
- notReached();
+ 'but got: ' + result +
+ (msg ? `\n${msg}` : '');
+ notReached(new Error(errorMessage));
}
}
-function assertNotEqual(expected, result) {
+function assertNotEqual(expected, result, msg) {
if (expected === result) {
- console.log('\n' +
+ const errorMessage = '\n' +
'Black Box Test Unequal Assertion failed: \n' +
- 'both are: ' + expected);
- notReached();
+ 'both are: ' + expected +
+ (msg ? `\n${msg}` : '');
+ notReached(new Error(errorMessage));
}
}
function onEndTest() {
- if (document.body.getAttribute(TEST_STATUS_ELEMENT_NAME) === FAILURE_MESSAGE) {
+ if (failed()) {
return;
}
document.body.setAttribute(TEST_STATUS_ELEMENT_NAME, SUCCESS_MESSAGE);
}
-
class TimerTestCase {
constructor(name, ExpectedCallTimes) {
this.name = name;
diff --git a/cobalt/black_box_tests/testdata/h5vcc_storage_write_verify_test.html b/cobalt/black_box_tests/testdata/h5vcc_storage_write_verify_test.html
new file mode 100644
index 0000000..25c748b
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/h5vcc_storage_write_verify_test.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+
+<html>
+ <head></head>
+ <body>
+ <script src='black_box_js_test_utils.js'></script>
+ <script src='h5vcc_storage_write_verify_test.js'></script>
+ </body>
+</html>
diff --git a/cobalt/black_box_tests/testdata/h5vcc_storage_write_verify_test.js b/cobalt/black_box_tests/testdata/h5vcc_storage_write_verify_test.js
new file mode 100644
index 0000000..3bd4a20
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/h5vcc_storage_write_verify_test.js
@@ -0,0 +1,84 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+'use strict';
+
+function failTest() {
+ notReached();
+ onEndTest();
+}
+
+function assertWrite(bytes, test_string) {
+ let response = h5vcc.storage.writeTest(bytes, test_string);
+ if (response.error != "") {
+ console.log(`assertWrite(${bytes}, ${test_string}), error: ${response.error}`);
+ failTest();
+ return false;
+ }
+ if (response.bytes_written != bytes) {
+ console.log(`assertWrite(${bytes}, ${test_string}), bytes_written (${response.bytes_written}) not equal to bytes`);
+ failTest();
+ return false;
+ }
+ return true;
+}
+
+function assertVerify(bytes, test_string) {
+ let response = h5vcc.storage.verifyTest(bytes, test_string);
+ if (response.error != "") {
+ console.log(`assertVerify(${bytes}, ${test_string}), error: ${response.error}`);
+ failTest();
+ return false;
+ }
+ if (response.bytes_read != bytes) {
+ console.log(`assertVerify(${bytes}, ${test_string}), bytes_read (${response.bytes_read}) not equal to bytes`);
+ failTest();
+ return false;
+ }
+ if (!response.verified) {
+ console.log(`assertVerify(${bytes}, ${test_string}), response not verified`);
+ failTest();
+ return false;
+ }
+ return true;
+}
+
+function h5vccStorageWriteVerifyTest() {
+ if (!h5vcc.storage) {
+ console.log("h5vcc.storage does not exist");
+ return;
+ }
+
+ if (!h5vcc.storage.writeTest || !h5vcc.storage.verifyTest) {
+ console.log("writeTest and verifyTest do not exist");
+ return;
+ }
+
+ if (!assertWrite(1, "a"))
+ return;
+ if (!assertVerify(1, "a"))
+ return;
+
+ if (!assertWrite(100, "a"))
+ return;
+ if (!assertVerify(100, "a"))
+ return;
+
+ if (!assertWrite(24 * 1024 * 1024, "foobar"))
+ return;
+ if (!assertVerify(24 * 1024 * 1024, "foobar"))
+ return;
+}
+
+h5vccStorageWriteVerifyTest();
+onEndTest();
diff --git a/cobalt/black_box_tests/testdata/http_cache.html b/cobalt/black_box_tests/testdata/http_cache.html
index e1f2e48..edabfb3 100644
--- a/cobalt/black_box_tests/testdata/http_cache.html
+++ b/cobalt/black_box_tests/testdata/http_cache.html
@@ -50,6 +50,27 @@
// the same url is used both times.
var timestampString = '?t=' + initialLoadTime;
+ // Ensure that the cache is enabled.
+ h5vcc.storage.enableCache();
+
+ // Make sure caching quotas are configured for required resources
+ const quota = 1024 * 1024 * 1; // set 1MB for each
+ let quotas = h5vcc.storage.getQuota();
+ // required by test
+ quotas.uncompiled_js = quota;
+ quotas.image = quota;
+ quotas.css = quota;
+ // Adjust "other" to match the total
+ quotas.other = quotas.total -
+ quotas.html - quotas.css - quotas.image - quotas.font - quotas.splash -
+ quotas.uncompiled_js - quotas.compiled_js - quotas.cache_api -
+ quotas.service_worker_js;
+ const response = h5vcc.storage.setQuota(quotas);
+ if(!response.success) {
+ console.log("Failed to set cache quotas:" + response.error)
+ notReached();
+ }
+
// Validate the tranferSize attribute of each performance entry specified
// in CACHED_ELEMENT_LOCATIONS and UNCACHED_ELEMENT_LOCATIONS.
function checkTransferSizes() {
diff --git a/cobalt/black_box_tests/testdata/service_worker_cache_keys_test.html b/cobalt/black_box_tests/testdata/service_worker_cache_keys_test.html
new file mode 100644
index 0000000..c5a57e9
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/service_worker_cache_keys_test.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<!--
+ Copyright 2022 The Cobalt Authors. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!--
+ This is a basic test of the cache.keys() for service workers.
+-->
+
+<html>
+<head>
+ <title>Cobalt Service Worker cache.keys() Test</title>
+ <script src='black_box_js_test_utils.js'></script>
+</head>
+<body>
+ <script>
+ const unregisterAll = () => navigator.serviceWorker.getRegistrations().then(registrations =>
+ Promise.all(registrations.map(r => r.unregister())));
+ const fail = msg => {
+ if (msg) {
+ console.error(msg);
+ }
+ unregisterAll().then(notReached);
+ };
+ const timeoutId = window.setTimeout(fail, 10000);
+ const success = () => unregisterAll().then(() => {
+ clearTimeout(timeoutId);
+ onEndTest();
+ });
+ const assertEquals = (expected, actual, msg) => {
+ if (expected !== actual) {
+ const errorMessage = `Expected: '${expected}', but was '${actual}'`;
+ fail(msg ? `${msg}(${errorMessage})` : errorMessage);
+ }
+ };
+ const workerPostMessage = message => navigator.serviceWorker.getRegistrations()
+ .then(registrations => {
+ let sent = false;
+ registrations.forEach(registration => {
+ if (registration.active) {
+ registration.active.postMessage(message);
+ sent = true;
+ }
+ });
+ if (!sent) {
+ fail('Unable to post message to active service worker.');
+ }
+ });
+ let activeServiceWorker = null;
+ navigator.serviceWorker.onmessage = event => {
+ if (event.data === 'end-test') {
+ success();
+ return;
+ }
+ fail(event.data);
+ };
+
+ unregisterAll()
+ .then(() => navigator.serviceWorker.register('service_worker_cache_keys_test.js'))
+ .catch(fail);
+
+ navigator.serviceWorker.ready.then(() => {
+ workerPostMessage('start-test');
+ });
+
+ setupFinished();
+ </script>
+</body>
+</html>
diff --git a/cobalt/black_box_tests/testdata/service_worker_cache_keys_test.js b/cobalt/black_box_tests/testdata/service_worker_cache_keys_test.js
new file mode 100644
index 0000000..a80fe9d
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/service_worker_cache_keys_test.js
@@ -0,0 +1,70 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+const postMessage = message => {
+ const options = {
+ includeUncontrolled: false, type: 'window'
+ };
+ self.clients.matchAll(options).then(clients => {
+ clients.forEach(c => {
+ c.postMessage(message);
+ });
+ });
+};
+
+const postError = message => {
+ postMessage(new Error(message).stack);
+};
+
+const assertEquals = (expected, actual, message) => {
+ if (expected !== actual) {
+ postError(`\nExpected: ${expected}\nActual: ${actual}\n${message || ''}`);
+ throw new Error();
+ }
+};
+
+const assertTrue = (actual, message) => {
+ assertEquals(true, actual, message);
+};
+
+self.addEventListener("install", event => {
+ self.skipWaiting();
+});
+
+self.addEventListener('activate', event => {
+ self.clients.claim();
+});
+
+self.addEventListener('message', async event => {
+ if (event.data === 'start-test') {
+ const cache = await caches.open('test-cache');
+ if ((await cache.keys()).length !== 0) {
+ const keys = await cache.keys();
+ await Promise.all(keys.map(k => cache.delete(k.url)));
+ }
+ assertEquals(0, (await cache.keys()).length);
+ await cache.put('http://www.example.com/a', new Response('a'));
+ await cache.put('http://www.example.com/b', new Response('b'));
+ await cache.put('http://www.example.com/c', new Response('c'));
+ const keys = await cache.keys();
+ assertEquals(3, keys.length);
+ const urls = new Set(keys.map(k => k.url));
+ assertTrue(urls.has('http://www.example.com/a'));
+ assertTrue(urls.has('http://www.example.com/b'));
+ assertTrue(urls.has('http://www.example.com/c'));
+ postMessage('end-test');
+ return;
+ }
+ postMessage(`${JSON.stringify(event.data)}-unexpected-message`);
+});
diff --git a/cobalt/black_box_tests/testdata/service_worker_csp_test.html b/cobalt/black_box_tests/testdata/service_worker_csp_test.html
new file mode 100644
index 0000000..ff058e3
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/service_worker_csp_test.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+ Copyright 2023 The Cobalt Authors. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<head>
+ <title>Verify that service worker registration correctly follow CSP</title>
+ <script src='black_box_js_test_utils.js'></script>
+</head>
+
+<body>
+ <script>
+ var window_onerror_count = 0;
+ window.onerror = (message, filename, lineno, colno, error) => {
+ ++window_onerror_count;
+ // Note: Worker execution errors currently don't pass line or column
+ // number in the error message.
+ assertIncludes('SecurityError', message);
+ assertIncludes('worker_csp_test.js', filename);
+ }
+
+ // This worker attempts to do an XHR request that is blocked by CSP.
+ navigator.serviceWorker.register(
+ 'worker_csp_test.js', { scope: './' })
+ .then(registration => {
+ notReached();
+ })
+ .catch(error => {
+ console.log(`Registration failed: ${error}`);
+ assertIncludes('TypeError', error);
+ });
+
+ window.setTimeout(
+ () => {
+ assertEqual(0, window_onerror_count);
+ onEndTest();
+ }, 250);
+ </script>
+</body>
diff --git a/cobalt/black_box_tests/testdata/service_worker_fetch_resource.js b/cobalt/black_box_tests/testdata/service_worker_fetch_resource.js
new file mode 100644
index 0000000..ccee89c
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/service_worker_fetch_resource.js
@@ -0,0 +1 @@
+window.activeServiceWorker.postMessage("script-not-intercepted");
diff --git a/cobalt/black_box_tests/testdata/service_worker_fetch_test.html b/cobalt/black_box_tests/testdata/service_worker_fetch_test.html
new file mode 100644
index 0000000..a325b69
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/service_worker_fetch_test.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<!--
+ Copyright 2022 The Cobalt Authors. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!--
+ This is a basic test of the fetch event for service workers.
+-->
+
+<html>
+<head>
+ <title>Cobalt Service Worker Fetch Test</title>
+ <script src='black_box_js_test_utils.js'></script>
+</head>
+<body>
+ <script>
+ const unregisterAll = () => navigator.serviceWorker.getRegistrations().then(registrations =>
+ Promise.all(registrations.map(r => r.unregister())));
+ const unregisterAndNotReached = () => unregisterAll().then(notReached);
+
+ const fetch_resource = () => fetch('service_worker_fetch_resource.js').then(r => r.text());
+ const timeoutId = window.setTimeout(unregisterAndNotReached, 10000);
+ window.activeServiceWorker = null;
+ navigator.serviceWorker.onmessage = event => {
+ if (event.data.test && event.data.test.indexOf('check-script') === 0) {
+ const script = document.createElement('script');
+ script.src = 'service_worker_fetch_resource.js';
+ document.body.appendChild(script);
+ return;
+ }
+ if (event.data.test && event.data.test.indexOf('check-fetch') === 0) {
+ if (event.data.result === 'failed') {
+ unregisterAndNotReached();
+ return;
+ }
+ fetch_resource().then(body => {
+ window.activeServiceWorker.postMessage({test: event.data.test, result: body});
+ });
+ return;
+ }
+ if (event.data === 'end-test') {
+ unregisterAll().then(() => {
+ clearTimeout(timeoutId);
+ onEndTest();
+ });
+ return;
+ }
+ unregisterAndNotReached();
+ };
+
+ navigator.serviceWorker.ready.then(registration => {
+ window.activeServiceWorker = registration.active;
+ window.activeServiceWorker.postMessage('start-test');
+ });
+
+ unregisterAll().then(fetch_resource).then(body => {
+ if ('window.activeServiceWorker.postMessage("script-not-intercepted");\n' !== body) {
+ unregisterAndNotReached();
+ return;
+ }
+ navigator.serviceWorker.register('service_worker_fetch_test.js').catch(() => {
+ unregisterAndNotReached();
+ });
+ });
+
+ setupFinished();
+ </script>
+</body>
+</html>
diff --git a/cobalt/black_box_tests/testdata/service_worker_fetch_test.js b/cobalt/black_box_tests/testdata/service_worker_fetch_test.js
new file mode 100644
index 0000000..22da79e
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/service_worker_fetch_test.js
@@ -0,0 +1,94 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+const interceptedBody = 'window.activeServiceWorker.postMessage("script-intercepted");';
+let shouldIntercept = false;
+
+const postMessage = message => {
+ const options = {
+ includeUncontrolled: false, type: 'window'
+ };
+ self.clients.matchAll(options).then(clients => {
+ clients.forEach(c => {
+ c.postMessage(message);
+ });
+ });
+};
+
+self.addEventListener("install", event => {
+ self.skipWaiting();
+});
+
+self.addEventListener('activate', event => {
+ self.clients.claim();
+});
+
+let fetchEventCount = 0;
+let exceptScriptIntercepted = false;
+
+self.addEventListener('message', event => {
+ if (event.data === 'start-test') {
+ shouldIntercept = true;
+ postMessage({test: 'check-fetch-intercepted'});
+ return;
+ }
+ if (event.data.test === 'check-fetch-intercepted') {
+ if (interceptedBody !== event.data.result) {
+ postMessage({test: 'check-fetch-intercepted', result: 'failed'});
+ return;
+ }
+ shouldIntercept = false;
+ postMessage({test: 'check-fetch-not-intercepted'});
+ return;
+ }
+ if (event.data.test === 'check-fetch-not-intercepted') {
+ if (interceptedBody === event.data.result) {
+ postMessage({test: 'check-fetch-not-intercepted', result: 'failed'});
+ return;
+ }
+ shouldIntercept = true;
+ exceptScriptIntercepted = true;
+ postMessage({test: 'check-script-intercepted'});
+ return;
+ }
+ if (event.data === 'script-intercepted') {
+ if (!exceptScriptIntercepted) {
+ postMessage('script-intercepted-unexpected');
+ }
+ shouldIntercept = false;
+ exceptScriptIntercepted = false;
+ postMessage({test: 'check-script-not-intercepted'})
+ return;
+ }
+ if (event.data === 'script-not-intercepted') {
+ if (exceptScriptIntercepted) {
+ postMessage('script-not-intercepted-unexpected');
+ return;
+ }
+ if (fetchEventCount !== 4) {
+ postMessage('fetch-event-count-incorrect');
+ return;
+ }
+ postMessage('end-test');
+ return;
+ }
+ postMessage(`${JSON.stringify(event.data)}-unexpected-message`);
+});
+
+self.addEventListener('fetch', event => {
+ fetchEventCount++;
+ if (shouldIntercept) {
+ event.respondWith(Promise.resolve(new Response(interceptedBody)));
+ }
+});
diff --git a/cobalt/black_box_tests/testdata/service_worker_get_registrations_test.html b/cobalt/black_box_tests/testdata/service_worker_get_registrations_test.html
new file mode 100644
index 0000000..21cf71d
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/service_worker_get_registrations_test.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<!--
+ Copyright 2022 The Cobalt Authors. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!--
+ This is a basic test of the getRegistrations() for service workers.
+-->
+
+<html>
+<head>
+ <title>Cobalt Service Worker getRegistrations() Test</title>
+ <script src='black_box_js_test_utils.js'></script>
+</head>
+<body>
+ <script>
+ // TODO: check multiple registrations.
+ const unregisterAll = () => navigator.serviceWorker.getRegistrations().then(registrations =>
+ Promise.all(registrations.map(r => r.unregister())));
+ const fail = msg => {
+ if (msg) {
+ console.error(msg);
+ }
+ unregisterAll().then(notReached);
+ };
+ const timeoutId = window.setTimeout(fail, 10000);
+ const success = () => unregisterAll().then(() => {
+ clearTimeout(timeoutId);
+ onEndTest();
+ });
+ const assertEquals = (expected, actual, msg) => {
+ if (expected !== actual) {
+ const errorMessage = `Expected: '${expected}', but was '${actual}'`;
+ fail(msg ? `${msg}(${errorMessage})` : errorMessage);
+ }
+ };
+ const workerPostMessage = message => navigator.serviceWorker.getRegistrations()
+ .then(registrations => {
+ let sent = false;
+ registrations.forEach(registration => {
+ if (registration.active) {
+ registration.active.postMessage(message);
+ sent = true;
+ }
+ });
+ if (!sent) {
+ fail('Unable to post message to active service worker.');
+ }
+ });
+ let activeServiceWorker = null;
+ navigator.serviceWorker.onmessage = event => {
+ if (event.data === 'check-get-registrations') {
+ navigator.serviceWorker.getRegistrations().then(registrations => {
+ assertEquals(1, registrations.length);
+ console.log(JSON.stringify(registrations[0]));
+ workerPostMessage('check-get-registrations-ok');
+ });
+ return;
+ }
+ if (event.data === 'end-test') {
+ success();
+ return;
+ }
+ fail();
+ };
+
+ unregisterAll()
+ .then(() => navigator.serviceWorker.getRegistrations())
+ .then(registrations => {
+ assertEquals(0, registrations.length);
+ })
+ .then(() => navigator.serviceWorker.register('service_worker_get_registrations_test.js'))
+ .catch(fail);
+
+ navigator.serviceWorker.ready.then(() => {
+ workerPostMessage('start-test');
+ });
+
+ setupFinished();
+ </script>
+</body>
+</html>
diff --git a/cobalt/black_box_tests/testdata/service_worker_get_registrations_test.js b/cobalt/black_box_tests/testdata/service_worker_get_registrations_test.js
new file mode 100644
index 0000000..7d1be14
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/service_worker_get_registrations_test.js
@@ -0,0 +1,44 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+const postMessage = message => {
+ const options = {
+ includeUncontrolled: false, type: 'window'
+ };
+ self.clients.matchAll(options).then(clients => {
+ clients.forEach(c => {
+ c.postMessage(message);
+ });
+ });
+};
+
+self.addEventListener("install", event => {
+ self.skipWaiting();
+});
+
+self.addEventListener('activate', event => {
+ self.clients.claim();
+});
+
+self.addEventListener('message', event => {
+ if (event.data === 'start-test') {
+ postMessage('check-get-registrations');
+ return;
+ }
+ if (event.data === 'check-get-registrations-ok') {
+ postMessage('end-test');
+ return;
+ }
+ postMessage(`${JSON.stringify(event.data)}-unexpected-message`);
+});
diff --git a/cobalt/black_box_tests/testdata/service_worker_persist_test.html b/cobalt/black_box_tests/testdata/service_worker_persist_test.html
new file mode 100644
index 0000000..6d6e629
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/service_worker_persist_test.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!--
+ Copyright 2022 The Cobalt Authors. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!--
+ Note: This is a test of the Service Worker Registration Lifetime.
+-->
+
+<html>
+
+<head>
+ <title>Cobalt Service Worker Test</title>
+ <script src='black_box_js_test_utils.js'></script>
+</head>
+
+<body>
+ <script src='service_worker_persist_test_script.js'></script>
+</body>
+
+</html>
diff --git a/cobalt/black_box_tests/testdata/service_worker_persist_test_script.js b/cobalt/black_box_tests/testdata/service_worker_persist_test_script.js
new file mode 100644
index 0000000..5638ecd
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/service_worker_persist_test_script.js
@@ -0,0 +1,171 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+/**
+* @param {KeyboardEvent} event
+*/
+window.onkeydown = function(event) {
+ assertEqual(undefined, self.clients);
+
+ if (!('serviceWorker' in navigator)) {
+ console.log("serviceWorker not in navigator, ending test");
+ onEndTest();
+ }
+
+ navigator.serviceWorker.ready.then(function (registration) {
+ assertNotEqual(null, registration);
+ console.log('(Expected) Registration ready promise',
+ registration, 'with active worker ', registration.active);
+ assertNotEqual(null, registration.active);
+ });
+
+ navigator.serviceWorker.oncontrollerchange = function (event) {
+ console.log('Got oncontrollerchange event', event.target);
+ }
+
+ navigator.serviceWorker.onmessage = function (event) {
+ console.log('Got onmessage event', event.target, event.data);
+ }
+
+ if (event.key == 0) {
+ test_successful_registration();
+ } else if (event.key == 1) {
+ test_persistent_registration();
+ } else if (event.key == 2) {
+ test_persistent_registration_does_not_exist();
+ }
+}
+
+function test_successful_registration() {
+ console.log('test_successful_registration()');
+ navigator.serviceWorker.register('service_worker_test_worker.js', {
+ scope: '/bar/registration/scope',
+ }).then(function (registration) {
+ console.log('(Expected) Registration Succeeded:',
+ registration);
+ assertNotEqual(null, registration);
+ // The default value for RegistrationOptions.type is
+ // 'imports'.
+ assertEqual('imports', registration.updateViaCache);
+ assertTrue(registration.scope.endsWith(
+ 'bar/registration/scope'));
+
+ // Check that the registration has an activated worker after
+ // some time. The delay used here should be long enough for
+ // the service worker to complete activating and have the
+ // state 'activated'. It has to be longer than the combined
+ // delays in the install or activate event handlers.
+ window.setTimeout(function () {
+ // Since these events are asynchronous, the service
+ // worker can be either of these states.
+ console.log(
+ 'Registration active check after timeout',
+ registration);
+ assertNotEqual(null, registration.active);
+ registration.active.postMessage(
+ 'Registration is active after waiting.');
+ console.log('Registration active',
+ registration.active, 'state:',
+ registration.active.state);
+ assertEqual('activated',
+ registration.active.state);
+ onEndTest();
+ }, 1000);
+ }, function (error) {
+ console.log(`(Unexpected): ${error}`, error);
+ notReached();
+ onEndTest();
+ });
+}
+
+function test_persistent_registration() {
+ console.log("test_persistent_registration()");
+ navigator.serviceWorker.getRegistration(
+ '/bar/registration/scope')
+ .then(function (registration) {
+ console.log('(Expected) getRegistration Succeeded:',
+ registration);
+ assertNotEqual(null, registration);
+ assertEqual('imports', registration.updateViaCache);
+ assertTrue(registration.scope.endsWith(
+ 'bar/registration/scope'));
+
+ window.setTimeout(function () {
+ // TODO(b/259731731): Activate persisted registrations
+ // correctly before enabling these activation checks.
+
+ // console.log(
+ // 'Registration active check after timeout',
+ // registration);
+ // assertNotEqual(null, registration.active);
+ // registration.active.postMessage(
+ // 'Registration is active after waiting.');
+ // console.log('Registration active',
+ // registration.active, 'state:',
+ // registration.active.state);
+ // assertEqual('activated',
+ // registration.active.state);
+
+ registration.unregister()
+ .then(function (success) {
+ console.log('(Expected) unregister success :',
+ success);
+ // Finally, test getRegistration for the
+ // unregistered scope.
+ navigator.serviceWorker.getRegistration(
+ 'bar/registration/scope')
+ .then(function (registration) {
+ console.log('(Expected) ' +
+ 'getRegistration Succeeded: ',
+ registration);
+ assertTrue(null == registration ||
+ undefined == registration);
+ onEndTest();
+ }, function (error) {
+ console.log(`(Unexpected): ${error}`,
+ error);
+ notReached();
+ onEndTest();
+ });
+ }, function (error) {
+ console.log('(Unexpected) unregister ' +
+ `${error}`, error);
+ assertIncludes('SecurityError: ', `${error}`);
+ notReached();
+ onEndTest();
+ });
+ }, 1000);
+ }, function (error) {
+ console.log(`(Unexpected): ${error}`, error);
+ notReached();
+ onEndTest();
+ });
+}
+
+function test_persistent_registration_does_not_exist() {
+ console.log("test_persistent_registration_does_not_exist()");
+ navigator.serviceWorker.getRegistration(
+ '/bar/registration/scope')
+ .then(function (registration) {
+ console.log('(Expected) getRegistration Succeeded:',
+ registration);
+ assertEqual(null, registration);
+ onEndTest();
+ }, function (error) {
+ console.log(`(Unexpected): ${error}`, error);
+ notReached();
+ onEndTest();
+ });
+}
diff --git a/cobalt/black_box_tests/testdata/service_worker_test_claimable.js b/cobalt/black_box_tests/testdata/service_worker_test_claimable.js
index 191c964..de15b34 100644
--- a/cobalt/black_box_tests/testdata/service_worker_test_claimable.js
+++ b/cobalt/black_box_tests/testdata/service_worker_test_claimable.js
@@ -37,7 +37,7 @@
console.log('oninstall event received', e);
// Using a delay long enough to make it clearly visible in the log that the
// event is extended, and is delaying the activate event and ready promise.
- e.waitUntil(delay_promise(1000).then(() => console.log('Promised delay.'), () => console.log('\nPromised rejected.\n')));
+ e.waitUntil(delay_promise(100).then(() => console.log('Promised delay.'), () => console.log('\nPromised rejected.\n')));
}
self.onactivate = function (e) {
@@ -53,7 +53,7 @@
};
// Using a delay long enough to make it clearly visible in the log that the
// event is extended.
- e.waitUntil(delay_promise(1000).then(function () {
+ e.waitUntil(delay_promise(100).then(function () {
console.log('self.clients.matchAll(options)');
e.waitUntil(self.clients.matchAll(options).then(function (clients) {
console.log('(Expected) self.clients.matchAll():', clients.length, clients);
diff --git a/cobalt/black_box_tests/testdata/service_worker_test_script.js b/cobalt/black_box_tests/testdata/service_worker_test_script.js
index 0428eab..55d40eb 100644
--- a/cobalt/black_box_tests/testdata/service_worker_test_script.js
+++ b/cobalt/black_box_tests/testdata/service_worker_test_script.js
@@ -12,495 +12,289 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-var expected_event_count = 0;
-var expected_event_count_offset = 0;
+const eventCounts = {};
+const countEvent = (fnName, expectedEventCount) => {
+ if (fnName in eventCounts) {
+ eventCounts[fnName]++;
+ } else {
+ eventCounts[fnName] = 1;
+ }
+ if (expectedEventCount) {
+ assertEqual(expectedEventCount, eventCounts[fnName])
+ }
+};
-function total_events() {
- return expected_event_count + expected_event_count_offset;
-}
+const unregisterAll = () => navigator.serviceWorker.getRegistrations().then(registrations =>
+ Promise.all(registrations.map(r => r.unregister())));
+setTearDown(unregisterAll);
+const err = msg => new Error(msg);
+const fail = msg => {
+ const userDefinedError = new Error(msg);
+ return error => {
+ printError(userDefinedError);
+ return unregisterAll().then(() => notReached(error));
+ };
+};
+const timeoutId = window.setTimeout(fail('timeout'), 10000);
+const done = () => {
+ window.clearTimeout(timeoutId);
+ console.log('Done!');
+ onEndTest();
+};
-function count_event(expected_value) {
- expected_event_count += 1;
- if (expected_value) {
- assertEqual(expected_value, expected_event_count)
- }
- return expected_event_count;
-}
-
-function next_function(next_func) {
- expected_event_count_offset += expected_event_count;
- expected_event_count = 0;
- console.log(next_func.name);
- setTimeout(function () { next_func(); }, 0);
-}
-
-assertEqual(undefined, self.clients);
-
-if ('serviceWorker' in navigator) {
- console.log('Starting tests');
- test_main();
-}
+const expectEquals = (expected, actual, msg) => {
+ window.assertEqual(expected, actual, new Error(msg || ''));
+};
setupFinished();
-// This 7 second delay has to be long enough to guarantee that the test has
-// finished and allow for time to see any spurious events that may arrive later.
-window.setTimeout(
- () => {
- console.log('Total Events:', total_events())
- assertEqual(44, total_events());
- onEndTest();
- // Exit 3 seconds after ending the test instead of relying on the test
- // runner to force the exit.
- setTimeout(function () { window.close(); }, 3000);
- }, 7000);
-function test_main() {
- navigator.serviceWorker.ready.then(function (registration) {
- assertNotEqual(null, registration);
- console.log('(Expected) Registration ready promise',
- registration, 'with active worker ', registration.active);
- assertNotEqual(null, registration.active);
- count_event();
- });
+assertFalse(!!self.clients);
+assertTrue(!!navigator.serviceWorker);
- navigator.serviceWorker.oncontrollerchange = function (event) {
- console.log('Got oncontrollerchange event', event.target);
- count_event(17);
+navigator.serviceWorker.ready.then(registration => {
+ assertTrue(!!registration);
+ assertTrue(!!registration.active);
+ countEvent('testMain', /*expectedEventCount=*/1);
+});
+
+navigator.serviceWorker.oncontrollerchange = event => {
+ countEvent('testMain', /*expectedEventCount=*/2);
+};
+
+const promiseForEach = (xs, fn) => new Promise((resolve, reject) => {
+ let index = -1;
+ const doNext = () => {
+ if (failed()) {
+ reject(new Error(`Tests failed. Current sequence index is ${index}.`));
+ }
+ index++;
+ if (index === xs.length) {
+ resolve();
+ return;
+ }
+ fn(xs[index]).then(doNext).catch(reject);
+ };
+ doNext();
+});
+
+const promiseSequence = fns => promiseForEach(fns, fn => fn());
+
+const promiseMap = (xs, fn) => Promise.all(xs.map(fn));
+
+const checkInvalidUrls = () => promiseForEach(
+ [
+ 'http://..:..',
+ 'arg:service_worker_test_worker.js',
+ 'http:%2fservice_worker_test_worker.js',
+ ],
+ url => navigator.serviceWorker.register(url)
+ .then(fail(`URL ${url} did not raise error.`))
+ .catch(error => {
+ assertEqual('TypeError', error.name, `url: ${url}`);
+ })
+);
+
+const checkInvalidScopes = () => promiseForEach(
+ [
+ ['service_worker_test_worker.js', {scope: 'arg:/'}, 'TypeError'],
+ ['service_worker_test_worker.js', {scope: '/foo'}, 'SecurityError'],
+ ['service_worker_test_worker.js', {scope: '/%5c'}, 'TypeError'],
+ ],
+ args => navigator.serviceWorker.register(args[0], args[1])
+ .then(fail(`Scope ${args[1].scope} did not raise error.`))
+ .catch(error => {
+ assertEqual(args[2], error.name);
+ })
+);
+
+const checkNotTrustedAndEquivalentJob = () => promiseMap(
+ Array.from({length: 15}),
+ () => navigator.serviceWorker.register('http://www.example.com/', {scope: '/'})
+ .then(fail('Untrusted URL did not raise error'))
+ .catch(error => {
+ assertEqual('SecurityError', error.name);
+ })
+);
+
+const checkSuccessfulRegistration = () => Promise.all([
+ navigator.serviceWorker.register('service_worker_test_worker.js', {
+ scope: '/bar/registration/scope',
+ })
+ .then(registration => {
+ assertTrue(!!registration);
+ assertEqual('imports', registration.updateViaCache);
+ assertTrue(registration.scope.endsWith('bar/registration/scope'));
+
+ let worker = registration.installing;
+ if (worker) {
+ worker.postMessage(
+ 'Registration with scope successful, ' +
+ 'current worker state is installing.');
+ }
+ worker = registration.waiting;
+ if (worker) {
+ worker.postMessage(
+ 'Registration with scope successful, ' +
+ 'current worker state is waiting.');
+ }
+ worker = registration.active;
+ if (worker) {
+ worker.postMessage(
+ 'Registration with scope successful, ' +
+ 'current worker state is active.');
}
- navigator.serviceWorker.onmessage = function (event) {
- console.log('Got onmessage event', event.target, event.data);
- }
+ registration.onupdatefound = event => {
+ assertTrue(event.target.scope.endsWith('bar/registration/scope'));
+ };
- next_function(test_invalid_script_url);
-}
+ return navigator.serviceWorker.ready.then(registration => {
+ assertTrue(!!registration.active);
+ registration.active.postMessage('Registration is active after waiting.');
+ assertEqual('activating', registration.active.state);
-function test_invalid_script_url() {
- navigator.serviceWorker.register('http://..:..'
- ).then(function (registration) {
- console.log('(Unexpected):', registration);
- notReached();
- }, function (error) {
- console.log(`(Expected) Test invalid script URL: ${error}`, error);
- assertIncludes('TypeError', `${error}`);
- count_event(1);
- next_function(test_script_url_that_is_not_http);
- });
-}
-
-function test_script_url_that_is_not_http() {
- navigator.serviceWorker.register('arg:service_worker_test_worker.js'
- ).then(function (registration) {
- console.log('(Unexpected):', registration);
- notReached();
- }, function (error) {
- console.log('(Expected) Test script URL that is not http ' +
- `or https: ${error}`, error);
- assertIncludes('TypeError', `${error}`);
- count_event(1);
- next_function(test_script_url_with_escaped_slash);
- });
-}
-
-function test_script_url_with_escaped_slash() {
- navigator.serviceWorker.register('http:%2fservice_worker_test_worker.js'
- ).then(function (registration) {
- console.log('(Unexpected):', registration);
- notReached();
- }, function (error) {
- console.log('(Expected) Test script URL with escaped slash:',
- `${error}`, error);
- assertIncludes('TypeError', `${error}`);
- count_event(1);
- next_function(test_invalid_scope_url);
- });
-}
-
-function test_invalid_scope_url() {
- navigator.serviceWorker.register('service_worker_test_worker.js', {
- scope: 'http://..:..',
- }).then(function (registration) {
- console.log('(Unexpected):', registration);
- notReached();
- }, function (error) {
- console.log(`(Expected) Test invalid scope URL: ${error}`, error);
- assertIncludes('TypeError', `${error}`);
- count_event(1);
- next_function(test_scope_url_that_is_not_http);
- });
-}
-
-function test_scope_url_that_is_not_http() {
- navigator.serviceWorker.register('service_worker_test_worker.js', {
- scope: 'arg:/',
- }).then(function (registration) {
- console.log('(Unexpected):', registration);
- notReached();
- }, function (error) {
- console.log('(Expected) Test scope URL that is not http ' +
- `or https: ${error}`, error);
- assertIncludes('TypeError', `${error}`);
- count_event(1);
- next_function(test_scope_url_that_is_not_allowed);
- });
-}
-
-function test_scope_url_that_is_not_allowed() {
- navigator.serviceWorker.register('service_worker_test_worker.js', {
- scope: '/foo',
- }).then(function (registration) {
- console.log('(Unexpected):', registration);
- notReached();
- }, function (error) {
- console.log('(Expected) Test scope URL that is not allowed:' +
- `${error}`, error);
- assertIncludes('SecurityError', `${error}`);
- count_event(1);
- next_function(test_scope_url_with_escaped_slash());
- });
-}
-
-function test_scope_url_with_escaped_slash() {
- navigator.serviceWorker.register('service_worker_test_worker.js', {
- scope: '/%5c',
- }).then(function (registration) {
- console.log('(Unexpected):', registration);
- notReached();
- }, function (error) {
- console.log('(Expected) Test scope URL with escaped slash:',
- `${error}`, error);
- assertIncludes('TypeError', `${error}`);
- count_event(1);
- next_function(test_script_url_not_trusted_and_equivalent_job);
- });
-}
-
-function test_script_url_not_trusted_and_equivalent_job() {
- // Repeat a few times to test the 'equivalent job' and finish job
- // logic.
- var repeat_count = 15;
- for (let repeated = 0; repeated < repeat_count; repeated++) {
- navigator.serviceWorker.register('http://www.example.com/', {
- scope: '/',
- }).then(function (registration) {
- console.log('(Unexpected):', registration);
- notReached();
- }, function (error) {
- console.log(`(Expected): ${error}`, error);
- assertIncludes('SecurityError: ', `${error}`);
- if (count_event() >= repeat_count) {
- next_function(test_script_url_and_referrer_origin_not_same);
- }
- });
- }
-}
-
-function test_script_url_and_referrer_origin_not_same() {
- navigator.serviceWorker.register('http://127.0.0.100:2345/')
- .then(function (registration) {
- console.log('(Unexpected):', registration);
- notReached();
- }, function (error) {
- console.log(`(Expected): ${error}`, error);
- assertIncludes('SecurityError: ', `${error}`);
- count_event(1);
- next_function(test_scope_url_and_referrer_origin_not_same);
- });
-}
-
-function test_scope_url_and_referrer_origin_not_same() {
- navigator.serviceWorker.register('service_worker_test_worker.js', {
- scope: 'http://127.0.0.100:2345/',
- }).then(function (registration) {
- console.log('(Unexpected):', registration);
- notReached();
- }, function (error) {
- console.log(`(Expected): ${error}`, error);
- assertIncludes('SecurityError: ', `${error}`);
- count_event(1);
- next_function(test_url_not_javascript_mime_type());
- });
-}
-
-function test_url_not_javascript_mime_type() {
- navigator.serviceWorker.register('service_worker_test.html'
- ).then(function (registration) {
- console.log('(Unexpected):', registration);
- notReached();
- }, function (error) {
- console.log('(Expected) Test script URL that is not JavaScript MIME ' +
- `type: ${error}`, error);
- assertIncludes('SecurityError', `${error}`);
- count_event(1);
- next_function(test_url_not_exist);
- });
-}
-
-function test_url_not_exist() {
- navigator.serviceWorker.register('service_worker_test_nonexist.js'
- ).then(function (registration) {
- console.log('(Unexpected):', registration);
- notReached();
- }, function (error) {
- console.log('(Expected) Test script URL that does not exist:' +
- `${error}`, error);
- assertIncludes('NetworkError', `${error}`);
- count_event(1);
-
- // Finally, test a succeeding registration.
- next_function(test_successful_registration());
- });
-}
-
-function test_successful_registration() {
- console.log('test_successful_registration()');
- navigator.serviceWorker.register('service_worker_test_worker.js', {
- scope: '/bar/registration/scope',
- }).then(function (registration) {
- console.log('(Expected) Registration Succeeded:',
- registration);
- assertNotEqual(null, registration);
- // The default value for RegistrationOptions.type is
- // 'imports'.
- assertEqual('imports', registration.updateViaCache);
- assertTrue(registration.scope.endsWith(
- 'bar/registration/scope'));
- count_event(1);
-
- worker = registration.installing;
- if (worker) {
- worker.postMessage(
- 'Registration with scope successful, ' +
- 'current worker state is installing.');
- }
- worker = registration.waiting;
- if (worker) {
- worker.postMessage(
- 'Registration with scope successful, ' +
- 'current worker state is waiting.');
- }
- worker = registration.active;
- if (worker) {
- worker.postMessage(
- 'Registration with scope successful, ' +
- 'current worker state is active.');
- }
-
- registration.onupdatefound = function (event) {
- console.log('Got onupdatefound event',
- event.target.scope);
- assertTrue(event.target.scope.endsWith(
- 'bar/registration/scope'));
- }
-
- // Check that the registration has an activated worker after
- // some time. The delay used here should be long enough for
- // the service worker to complete activating and have the
- // state 'activated'. It has to be longer than the combined
- // delays in the install or activate event handlers.
- window.setTimeout(function () {
- // Since these events are asynchronous, the service
- // worker can be either of these states.
- console.log(
- 'Registration active check after timeout',
- registration);
- assertNotEqual(null, registration.active);
- registration.active.postMessage(
- 'Registration is active after waiting.');
- console.log('Registration active',
- registration.active, 'state:',
- registration.active.state);
- assertEqual('activated',
- registration.active.state);
- count_event(8);
- registration.active.onstatechange =
- function (event) {
- console.log('Got onstatechange event',
- event.target.state);
- }
-
- // Repeat a few times to test the 'equivalent job' and
- // finish job logic.
- for (let repeated = 0; repeated < 5; repeated++) {
- registration.update().then(function (registration) {
- console.log('(Expected) Registration update ' +
- 'Succeeded:', registration);
- assertNotEqual(null, registration);
- count_event();
- }, function (error) {
- console.log(`(Unexpected): ${error}`, error);
- notReached();
- });
- }
-
- registration.unregister()
- .then(function (success) {
- console.log('(Expected) unregister success :',
- success);
- count_event(14);
- // Finally, test getRegistration for the
- // unregistered scope.
- navigator.serviceWorker.getRegistration(
- 'bar/registration/scope')
- .then(function (registration) {
- console.log('(Expected) ' +
- 'getRegistration Succeeded: ',
- registration);
- assertTrue(null == registration ||
- undefined == registration);
- count_event(15);
- }, function (error) {
- console.log(`(Unexpected): ${error}`,
- error);
- notReached();
- });
-
- test_claimable_worker();
- }, function (error) {
- console.log('(Unexpected) unregister ' +
- `${error}`, error);
- assertIncludes('SecurityError: ', `${error}`);
- notReached();
- });
- }, 1000);
-
- // Test getRegistration for a non-registered scope.
- navigator.serviceWorker.getRegistration('/bo/gus')
- .then(function (registration) {
- console.log('(Expected) getRegistration Succeeded:',
- registration);
- assertTrue(null == registration ||
- undefined == registration);
- count_event();
- }, function (error) {
- console.log(`(Unexpected): ${error}`, error);
- notReached();
- });
-
- // Test getRegistration for a deeper registered scope.
- navigator.serviceWorker.getRegistration(
+ return promiseSequence([
+ () => promiseMap(
+ Array.from({length: 5}),
+ () => registration.update()
+ .then(registration => {
+ assertTrue(!!registration);
+ })
+ ).catch(fail('getRegistration() failed.')),
+ () => navigator.serviceWorker.getRegistration('/bo/gus')
+ .then(registration => {
+ assertFalse(!!registration);
+ })
+ .catch(fail('getRegistration() failed.')),
+ () => navigator.serviceWorker.getRegistration(
'/bar/registration/scope/deeper')
- .then(function (registration) {
- console.log('(Expected) getRegistration Succeeded:',
- registration);
- assertNotEqual(null, registration);
- assertEqual('imports', registration.updateViaCache);
- assertTrue(registration.scope.endsWith(
- 'bar/registration/scope'));
- count_event();
- }, function (error) {
- console.log(`(Unexpected): ${error}`, error);
- notReached();
- });
-
- // Test getRegistration for a shallower registered scope.
- navigator.serviceWorker.getRegistration('registration')
- .then(function (registration) {
- console.log('(Expected) getRegistration Succeeded:',
- registration);
- assertTrue(null == registration ||
- undefined == registration);
- count_event();
- }, function (error) {
- console.log(`(Unexpected): ${error}`, error);
- notReached();
- });
-
- // Test getRegistration for a non-registered scope.
- navigator.serviceWorker.getRegistration()
- .then(function (registration) {
- console.log('(Expected) getRegistration Succeeded:',
- registration);
- // TODO(b/234659851): Investigate whether this
- // should return a registration or not, in this case
- // where there is a registration with a scope.
- assertTrue(null == registration ||
- undefined == registration);
- count_event();
- }, function (error) {
- console.log(`(Unexpected): ${error}`, error);
- notReached();
- });
-
- // Finally, test getRegistration for a registered scope.
- navigator.serviceWorker.getRegistration(
+ .then(registration => {
+ assertTrue(!!registration);
+ assertEqual('imports', registration.updateViaCache);
+ assertTrue(registration.scope.endsWith(
+ 'bar/registration/scope'));
+ })
+ .catch(fail('getRegistration() failed.')),
+ () => navigator.serviceWorker.getRegistration('registration')
+ .then(registration => {
+ assertFalse(!!registration);
+ })
+ .catch(fail('getRegistration() failed.')),
+ () => navigator.serviceWorker.getRegistration()
+ .then(registration => {
+ assertFalse(!!registration);
+ })
+ .catch(fail),
+ () => navigator.serviceWorker.getRegistration(
'/bar/registration/scope')
- .then(function (registration) {
- console.log('(Expected) getRegistration Succeeded:',
- registration);
- assertNotEqual(null, registration);
- assertEqual('imports', registration.updateViaCache);
- assertTrue(registration.scope.endsWith(
- 'bar/registration/scope'));
- count_event();
- }, function (error) {
- console.log(`(Unexpected): ${error}`, error);
- notReached();
- });
- }, function (error) {
- console.log(`(Unexpected): ${error}`, error);
- notReached();
+ .then(registration => {
+ assertTrue(!!registration);
+ assertEqual('imports', registration.updateViaCache);
+ assertTrue(registration.scope.endsWith(
+ 'bar/registration/scope'));
+ })
+ .catch(fail('getRegistration() failed.')),
+ () => registration.unregister()
+ .then(success => {
+ assertTrue(success);
+ })
+ .then(() =>
+ navigator.serviceWorker.getRegistration('bar/registration/scope'))
+ .then(registration => {
+ assertFalse(!!registration)
+ })
+ .catch(fail('getRegistration() failed after unregister.')),
+ ]);
});
-}
+ }),
+]);
-function test_claimable_worker() {
- console.log('test_claimable_worker()');
- console.log('Adding ready promise.');
- navigator.serviceWorker.ready.then(function (
- registration) {
- assertNotEqual(null, registration);
- console.log('(Expected) Registration ready promise',
- registration, ' with active worker ',
- registration.active);
- assertNotEqual(null, registration.active);
- registration.active.postMessage(
- 'Registration ready received for claimable worker.');
- count_event();
- registration.unregister()
- .then(function (success) {
- // Even a claimed registration will successfully
- // unregister because unregistration takes effect after
- // the page is unloaded, so it's not blocked by being
- // the active service worker.
- console.log('(Expected) unregister success :',
- success);
- count_event(18);
- }, function (error) {
- console.log('(Unexpected) unregister ' +
- `${error}`, error);
- assertIncludes('SecurityError: ', `${error}`);
- notReached();
- });
- console.log('unregister started.');
+const checkClaimableWorker = () => Promise.all([
+ new Promise((resolve, reject) => {
+ let messageCount = 0;
+ navigator.serviceWorker.onmessage = event => {
+ console.log('onmessage: ' + messageCount)
+ messageCount++;
+ // Expecting two messages:
+ // - when sent a message
+ // - when claimed
+ if (messageCount === 2) {
+ setTimeout(resolve);
+ return;
+ }
+ };
+ }),
+ navigator.serviceWorker.ready
+ .then(registration => {
+ assertTrue(!!registration);
+ assertTrue(!!registration.active);
+ registration.
+ registration.active.postMessage(
+ 'Registration ready received for claimable worker.');
+ return registration.unregister();
+ })
+ .then(success => {
+ assertTrue(success);
+ })
+ .catch(fail),
+ navigator.serviceWorker.register(
+ 'service_worker_test_claimable.js', {scope: './'})
+ .then(registration => {
+ let worker = registration.installing;
+ if (worker) {
+ worker.postMessage(
+ 'Registration successful, current worker state is ' +
+ 'installing.');
+ }
+ worker = registration.waiting;
+ if (worker) {
+ worker.postMessage(
+ 'Registration successful, current worker state is ' +
+ 'waiting.');
+ }
+ worker = registration.active;
+ if (worker) {
+ worker.postMessage(
+ 'Registration successful, current worker state is ' +
+ 'active.');
+ }
+ // TODO (b/259734597) : This registration is persisting since it is
+ // not unregistered and h5vcc storage clearCache does not correctly clear
+ // registrations.
+ // TODO (b/259731731) : Adding registration.unregister() causes memory
+ // leaks.
+ })
+ .catch(fail),
+]);
- });
-
- navigator.serviceWorker.register('service_worker_test_claimable.js', {
- scope: './',
- }).then(function (registration) {
- worker = registration.installing;
- if (worker) {
- console.log('claimable worker registered (installing).');
- worker.postMessage(
- 'Registration successful, current worker state is ' +
- 'installing.');
- }
- worker = registration.waiting;
- if (worker) {
- console.log('claimable worker registered (waiting).');
- worker.postMessage(
- 'Registration successful, current worker state is ' +
- 'waiting.');
- }
- worker = registration.active;
- if (worker) {
- console.log('claimable worker registered (active).');
- worker.postMessage(
- 'Registration successful, current worker state is ' +
- 'active.');
- }
- }, function (error) {
- console.log(`(Unexpected): ${error}`, error);
- notReached();
- });
-}
+promiseSequence([
+ unregisterAll,
+ checkInvalidUrls,
+ checkInvalidScopes,
+ checkNotTrustedAndEquivalentJob,
+ () => navigator.serviceWorker.register('http://127.0.0.100:2345/')
+ .then(fail(`Invalid URL did not raise error.`))
+ .catch(error => {
+ assertEqual('SecurityError', error.name);
+ }),
+ () => navigator.serviceWorker.register('service_worker_test_worker.js', {
+ scope: 'http://127.0.0.100:2345/',
+ })
+ .then(fail(`Invalid scope did not raise error.`))
+ .catch(error => {
+ assertEqual('SecurityError', error.name);
+ }),
+ () => navigator.serviceWorker.register('service_worker_test.html')
+ .then(fail(`HTML did not raise error.`))
+ .catch(error => {
+ assertEqual('SecurityError', error.name);
+ }),
+ () => navigator.serviceWorker.register('service_worker_test_nonexist.js')
+ .then(fail(`Unknown script did not raise error.`))
+ .catch(error => {
+ assertEqual('NetworkError', error.name);
+ }),
+ checkSuccessfulRegistration,
+ unregisterAll,
+ checkClaimableWorker,
+]).then(done).catch(fail('Error not caught.'));
diff --git a/cobalt/black_box_tests/testdata/worker_csp_test.html b/cobalt/black_box_tests/testdata/worker_csp_test.html
index fdb1cea..ba09f97 100644
--- a/cobalt/black_box_tests/testdata/worker_csp_test.html
+++ b/cobalt/black_box_tests/testdata/worker_csp_test.html
@@ -20,20 +20,33 @@
<script src='black_box_js_test_utils.js'></script>
</head>
-<body style="background-color: white;">
- <h1>
- <span>ID element</span>
- </h1>
+<body>
<script>
- var worker = new Worker('worker_csp_test.js');
+ var worker;
+ var window_onerror_count = 0;
+ window.onerror = (message, filename, lineno, colno, error) => {
+ ++window_onerror_count;
+ // Note: Worker execution errors currently don't pass line or column
+ // number in the error message.
+ assertIncludes('SecurityError', message);
+ assertIncludes('worker_csp_test.js', filename);
+ assertEqual(1, window_onerror_count);
+ window.setTimeout(
+ () => {
+ worker.terminate();
+ onEndTest();
+ }, 250);
+ }
+
+ // This worker attempts to do an XHR request that is blocked by CSP.
+ worker = new Worker('worker_csp_test.js');
worker.onmessage = function (event) {
notReached();
};
+ worker.onerror = function (event) {
+ // Note: The Worker's onerror handler (incorrectly) isn't called.
+ notReached();
+ };
- window.setTimeout(
- () => {
- worker.terminate();
- onEndTest();
- }, 250);
</script>
</body>
diff --git a/cobalt/black_box_tests/testdata/worker_load_csp_test.html b/cobalt/black_box_tests/testdata/worker_load_csp_test.html
new file mode 100644
index 0000000..f265f05
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/worker_load_csp_test.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<!--
+ Copyright 2023 The Cobalt Authors. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<head>
+ <title>Verify that worker loading correctly follows CSP</title>
+ <script src='black_box_js_test_utils.js'></script>
+</head>
+
+<body>
+ <script>
+ var window_onerror_count = 0;
+ window.onerror = (message, filename, lineno, colno, error) => {
+ ++window_onerror_count;
+ if (message.includes('blocked_worker.js')) {
+ assertIncludes('rejected by security policy', message);
+ assertIncludes('worker_load_csp_test.html', filename);
+ } else {
+ notReached();
+ }
+ if (window_onerror_count == 1) {
+ window.setTimeout(
+ () => {
+ assertEqual(1, window_onerror_count);
+ onEndTest();
+ }, 250);
+
+ }
+ }
+
+ // This worker is blocked because the URL isn't allowed by CSP.
+ try {
+ var blocked_worker = new Worker('https://www.google.com/blocked_worker.js');
+ blocked_worker.onerror = function (event) {
+ // Note: The Worker's onerror handler (incorrectly) isn't called.
+ notReached();
+ };
+ } catch (error) {
+ // The error is thrown asynchronously after the Worker constructor.
+ notReached();
+ }
+ </script>
+</body>
diff --git a/cobalt/black_box_tests/testdata/worker_load_test.html b/cobalt/black_box_tests/testdata/worker_load_test.html
new file mode 100644
index 0000000..7f13191
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/worker_load_test.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<!--
+ Copyright 2023 The Cobalt Authors. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<head>
+ <title>Verify that worker loading is blocked and reported as expected</title>
+ <script src='black_box_js_test_utils.js'></script>
+</head>
+
+<body>
+ <script>
+ var window_onerror_count = 0;
+ window.onerror = (message, filename, lineno, colno, error) => {
+ ++window_onerror_count;
+ console.log(message);
+ if (message.includes('nonexisting_worker.js')) {
+ assertIncludes('aborted or failed with code 404', message);
+ assertIncludes('worker_load_test.html', filename);
+ } else {
+ notReached();
+ }
+ if (window_onerror_count == 1) {
+ window.setTimeout(
+ () => {
+ assertEqual(1, window_onerror_count);
+ onEndTest();
+ }, 250);
+
+ }
+ }
+
+ // This worker is blocked because the URL can't resolve.
+ try {
+ var blocked_worker = new Worker('..:/blocked_worker.js');
+ notReached();
+ } catch(error) {
+ console.log(error);
+ assertEqual('SyntaxError', error.name);
+ }
+
+ // This worker is blocked because the script does not exist.
+ try {
+ var nonexisting_worker = new Worker('nonexisting_worker.js');
+ nonexisting_worker.onerror = function (event) {
+ // Note: The Worker's onerror handler (incorrectly) isn't called.
+ notReached();
+ };
+ } catch (error) {
+ // The error is thrown asynchronously after the Worker constructor.
+ notReached();
+ }
+ </script>
+</body>
diff --git a/cobalt/black_box_tests/tests/default_site_can_load.py b/cobalt/black_box_tests/tests/default_site_can_load.py
new file mode 100644
index 0000000..591bd2b
--- /dev/null
+++ b/cobalt/black_box_tests/tests/default_site_can_load.py
@@ -0,0 +1,62 @@
+# Copyright 2022 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests that Cobalt can load the default site."""
+
+import logging
+import traceback
+import time
+
+from cobalt.black_box_tests import black_box_tests
+from cobalt.black_box_tests.threaded_web_server import ThreadedWebServer
+
+
+class DefaultSiteCanLoadTest(black_box_tests.BlackBoxTestCase):
+ """Let Cobalt load the default site and perform some basic sanity checks."""
+
+ def test_default_site_can_load(self):
+ try:
+ with ThreadedWebServer(binding_address=self.GetBindingAddress()):
+ # Note: This test will fail when the Internet can not be reached from
+ # the device running Cobalt.
+
+ with self.CreateCobaltRunner() as runner:
+ # Allow for app startup time.
+ logging.info('Wait to allow app startup.')
+ time.sleep(10)
+ logging.info('Wait for html element.')
+ # Wait until there is at least the root element.
+ runner.PollUntilFound('html')
+ # Check that certain elements that should always be there are there,
+ # and remain there for a few seconds.
+ for i in range(10):
+ time.sleep(1)
+ logging.info(
+ ('Checking for consistent html, head, body, script, and div'
+ ' elements: %d'), i)
+ # Expect at least one <html> (root) element in the page
+ self.assertTrue(runner.FindElements('html'))
+ # Expect at least one <head> element in the page
+ self.assertTrue(runner.FindElements('head'))
+ # Expect at least one <body> element in the page
+ self.assertTrue(runner.FindElements('body'))
+ # Expect at least one <script> element in the page
+ self.assertTrue(runner.FindElements('script'))
+ # Expect at least three <div> elements in the page
+ self.assertTrue(len(runner.FindElements('div')) > 2)
+ except: # pylint: disable=bare-except
+ traceback.print_exc()
+ # Consider an exception being thrown as a test failure.
+ self.fail('Test failure')
+ finally:
+ logging.info('Cleaning up.')
diff --git a/cobalt/black_box_tests/tests/h5vcc_storage_write_verify_test.py b/cobalt/black_box_tests/tests/h5vcc_storage_write_verify_test.py
new file mode 100644
index 0000000..4727fde
--- /dev/null
+++ b/cobalt/black_box_tests/tests/h5vcc_storage_write_verify_test.py
@@ -0,0 +1,32 @@
+# Copyright 2023 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License
+"""Test that H5vccStorage::WriteTest and VerifyTest work on all platforms"""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+from cobalt.black_box_tests import black_box_tests
+from cobalt.black_box_tests.threaded_web_server import ThreadedWebServer
+
+
+class H5vccStorageWriteVerifyTest(black_box_tests.BlackBoxTestCase):
+
+ def test_h5vcc_storage_write_verify_test(self):
+ with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
+ url = server.GetURL(
+ file_name='testdata/h5vcc_storage_write_verify_test.html')
+ with self.CreateCobaltRunner(url=url) as runner:
+ runner.WaitForActiveElement()
+ self.assertTrue(runner.JSTestsSucceeded())
diff --git a/cobalt/black_box_tests/tests/service_worker_cache_keys_test.py b/cobalt/black_box_tests/tests/service_worker_cache_keys_test.py
new file mode 100644
index 0000000..996e3ad
--- /dev/null
+++ b/cobalt/black_box_tests/tests/service_worker_cache_keys_test.py
@@ -0,0 +1,28 @@
+# Copyright 2022 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests Service Worker getRegistrations() functionality."""
+
+from cobalt.black_box_tests import black_box_tests
+from cobalt.black_box_tests.threaded_web_server import ThreadedWebServer
+
+
+class ServiceWorkerCacheKeysTest(black_box_tests.BlackBoxTestCase):
+
+ def test_service_worker_cache_keys(self):
+ with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
+ url = server.GetURL(
+ file_name='testdata/service_worker_cache_keys_test.html')
+ with self.CreateCobaltRunner(url=url) as runner:
+ runner.WaitForJSTestsSetup()
+ self.assertTrue(runner.JSTestsSucceeded())
diff --git a/cobalt/black_box_tests/tests/service_worker_fetch_test.py b/cobalt/black_box_tests/tests/service_worker_fetch_test.py
new file mode 100644
index 0000000..27dac0e
--- /dev/null
+++ b/cobalt/black_box_tests/tests/service_worker_fetch_test.py
@@ -0,0 +1,27 @@
+# Copyright 2022 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests Service Worker Fetch functionality."""
+
+from cobalt.black_box_tests import black_box_tests
+from cobalt.black_box_tests.threaded_web_server import ThreadedWebServer
+
+
+class ServiceWorkerFetchTest(black_box_tests.BlackBoxTestCase):
+
+ def test_service_worker_fetch(self):
+ with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
+ url = server.GetURL(file_name='testdata/service_worker_fetch_test.html')
+ with self.CreateCobaltRunner(url=url) as runner:
+ runner.WaitForJSTestsSetup()
+ self.assertTrue(runner.JSTestsSucceeded())
diff --git a/cobalt/black_box_tests/tests/service_worker_get_registrations_test.py b/cobalt/black_box_tests/tests/service_worker_get_registrations_test.py
new file mode 100644
index 0000000..ebe059e
--- /dev/null
+++ b/cobalt/black_box_tests/tests/service_worker_get_registrations_test.py
@@ -0,0 +1,28 @@
+# Copyright 2022 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests Service Worker getRegistrations() functionality."""
+
+from cobalt.black_box_tests import black_box_tests
+from cobalt.black_box_tests.threaded_web_server import ThreadedWebServer
+
+
+class ServiceWorkerGetRegistrationsTest(black_box_tests.BlackBoxTestCase):
+
+ def test_service_worker_get_registrations(self):
+ with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
+ url = server.GetURL(
+ file_name='testdata/service_worker_get_registrations_test.html')
+ with self.CreateCobaltRunner(url=url) as runner:
+ runner.WaitForJSTestsSetup()
+ self.assertTrue(runner.JSTestsSucceeded())
diff --git a/cobalt/black_box_tests/tests/service_worker_persist_test.py b/cobalt/black_box_tests/tests/service_worker_persist_test.py
new file mode 100644
index 0000000..6505b0c
--- /dev/null
+++ b/cobalt/black_box_tests/tests/service_worker_persist_test.py
@@ -0,0 +1,48 @@
+# Copyright 2022 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests Service Worker Persistence functionality."""
+
+from cobalt.black_box_tests import black_box_tests
+from cobalt.black_box_tests.threaded_web_server import ThreadedWebServer
+from cobalt.tools.automated_testing import webdriver_utils
+from cobalt.black_box_tests.tests.service_worker_test import ServiceWorkerRequestDetector
+
+keys = webdriver_utils.import_selenium_module('webdriver.common.keys')
+
+
+class ServiceWorkerPersistTest(black_box_tests.BlackBoxTestCase):
+ """Test basic Service Worker functionality."""
+
+ def test_service_worker_persist(self):
+
+ with ThreadedWebServer(
+ ServiceWorkerRequestDetector,
+ binding_address=self.GetBindingAddress()) as server:
+ url = server.GetURL(file_name='testdata/service_worker_persist_test.html')
+
+ # NUMPAD0 calls test_successful_registration()
+ with self.CreateCobaltRunner(url=url) as runner:
+ runner.WaitForActiveElement()
+ runner.SendKeys(keys.Keys.NUMPAD0)
+ self.assertTrue(runner.JSTestsSucceeded())
+ # NUMPAD1 calls test_persistent_registration()
+ with self.CreateCobaltRunner(url=url) as runner:
+ runner.WaitForActiveElement()
+ runner.SendKeys(keys.Keys.NUMPAD1)
+ self.assertTrue(runner.JSTestsSucceeded())
+ # NUMPAD2 calls test_persistent_registration_does_not_exist()
+ with self.CreateCobaltRunner(url=url) as runner:
+ runner.WaitForActiveElement()
+ runner.SendKeys(keys.Keys.NUMPAD2)
+ self.assertTrue(runner.JSTestsSucceeded())
diff --git a/cobalt/black_box_tests/tests/worker_csp_test.py b/cobalt/black_box_tests/tests/worker_csp_test.py
index 18b6511..9c988e1 100644
--- a/cobalt/black_box_tests/tests/worker_csp_test.py
+++ b/cobalt/black_box_tests/tests/worker_csp_test.py
@@ -19,9 +19,13 @@
from cobalt.black_box_tests.threaded_web_server import ThreadedWebServer, MakeCustomHeaderRequestHandlerClass
paths_to_headers = {
+ 'worker_load_csp_test.html': {
+ 'Content-Security-Policy':
+ "script-src 'unsafe-inline' 'self'; worker-src 'self'"
+ },
'worker_csp_test.html': {
'Content-Security-Policy':
- "script-src 'unsafe-inline' 'self'; connect-src 'self';"
+ "script-src 'unsafe-inline' 'self'; connect-src 'self'"
},
'worker_csp_test.js': {
'Content-Security-Policy': "connect-src 'self';"
@@ -32,7 +36,7 @@
class WorkerCspTest(black_box_tests.BlackBoxTestCase):
"""Verify correct correct CSP behavior."""
- def test_worker_csp(self):
+ def test_1_worker_csp(self):
path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
with ThreadedWebServer(
binding_address=self.GetBindingAddress(),
@@ -42,3 +46,25 @@
with self.CreateCobaltRunner(url=url) as runner:
self.assertTrue(runner.JSTestsSucceeded())
+
+ def test_2_worker_load_csp(self):
+ path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
+ with ThreadedWebServer(
+ binding_address=self.GetBindingAddress(),
+ handler=MakeCustomHeaderRequestHandlerClass(
+ path, paths_to_headers)) as server:
+ url = server.GetURL(file_name='testdata/worker_load_csp_test.html')
+
+ with self.CreateCobaltRunner(url=url) as runner:
+ self.assertTrue(runner.JSTestsSucceeded())
+
+ def test_3_service_worker_csp(self):
+ path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
+ with ThreadedWebServer(
+ binding_address=self.GetBindingAddress(),
+ handler=MakeCustomHeaderRequestHandlerClass(
+ path, paths_to_headers)) as server:
+ url = server.GetURL(file_name='testdata/service_worker_csp_test.html')
+
+ with self.CreateCobaltRunner(url=url) as runner:
+ self.assertTrue(runner.JSTestsSucceeded())
diff --git a/cobalt/black_box_tests/tests/worker_load_test.py b/cobalt/black_box_tests/tests/worker_load_test.py
new file mode 100644
index 0000000..3a337b6
--- /dev/null
+++ b/cobalt/black_box_tests/tests/worker_load_test.py
@@ -0,0 +1,28 @@
+# Copyright 2023 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests worker loading violations."""
+
+from cobalt.black_box_tests import black_box_tests
+from cobalt.black_box_tests.threaded_web_server import ThreadedWebServer
+
+
+class WorkerLoadTest(black_box_tests.BlackBoxTestCase):
+ """Verify correct correct worker loading behavior."""
+
+ def test_worker_load(self):
+ with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
+ url = server.GetURL(file_name='testdata/worker_load_test.html')
+
+ with self.CreateCobaltRunner(url=url) as runner:
+ self.assertTrue(runner.JSTestsSucceeded())
diff --git a/cobalt/black_box_tests/threaded_web_server.py b/cobalt/black_box_tests/threaded_web_server.py
index 1e173e3..d305ea3 100644
--- a/cobalt/black_box_tests/threaded_web_server.py
+++ b/cobalt/black_box_tests/threaded_web_server.py
@@ -93,7 +93,8 @@
def do_GET(self): # pylint: disable=invalid-name
content = self.get_content()
- self.wfile.write(content)
+ if content:
+ self.wfile.write(content)
def do_HEAD(self): # pylint: disable=invalid-name
# Run get_content to send the headers, but ignore the returned results
@@ -172,8 +173,7 @@
Returns:
A string containing a HTTP URI.
"""
- return 'http://{}:{}/{}'.format(self._bound_host, self._bound_port,
- file_name)
+ return f'http://{self._bound_host}:{self._bound_port}/{file_name}'
def __enter__(self):
self._server_thread = threading.Thread(target=self._server.serve_forever)
diff --git a/cobalt/browser/BUILD.gn b/cobalt/browser/BUILD.gn
index 4c30f02..e8cf8d3 100644
--- a/cobalt/browser/BUILD.gn
+++ b/cobalt/browser/BUILD.gn
@@ -179,12 +179,12 @@
"//cobalt/script",
"//cobalt/script:engine",
"//cobalt/speech",
- "//cobalt/sso",
"//cobalt/storage",
"//cobalt/subtlecrypto",
"//cobalt/system_window",
"//cobalt/trace_event",
"//cobalt/ui_navigation",
+ "//cobalt/ui_navigation/scroll_engine",
"//cobalt/watchdog",
"//cobalt/web",
"//cobalt/webdriver",
diff --git a/cobalt/browser/application.cc b/cobalt/browser/application.cc
index 5cc3d2d..07af384 100644
--- a/cobalt/browser/application.cc
+++ b/cobalt/browser/application.cc
@@ -100,6 +100,15 @@
const char kPersistentSettingsJson[] = "settings.json";
+// The watchdog client name used to represent Stats.
+const char kWatchdogName[] = "stats";
+// The watchdog time interval in microseconds allowed between pings before
+// triggering violations.
+const int64_t kWatchdogTimeInterval = 10000000;
+// The watchdog time wait in microseconds to initially wait before triggering
+// violations.
+const int64_t kWatchdogTimeWait = 2000000;
+
bool IsStringNone(const std::string& str) {
return !base::strcasecmp(str.c_str(), "none");
}
@@ -563,6 +572,7 @@
user_agent.c_str())) {
result = false;
}
+ // TODO(b/265339522): move crashpad prod and ver setter to starboard.
if (!crash_handler_extension->SetString("prod", product.c_str())) {
result = false;
}
@@ -669,6 +679,12 @@
watchdog::Watchdog::CreateInstance(persistent_settings_.get());
DCHECK(watchdog);
+ // Registers Stats as a watchdog client.
+ if (watchdog)
+ watchdog->Register(kWatchdogName, kWatchdogName,
+ base::kApplicationStateStarted, kWatchdogTimeInterval,
+ kWatchdogTimeWait, watchdog::NONE);
+
cobalt::cache::Cache::GetInstance()->set_persistent_settings(
persistent_settings_.get());
@@ -1450,6 +1466,16 @@
TRACE_EVENT0("cobalt::browser", "Application::UpdatePeriodicStats()");
c_val_stats_.app_lifetime = base::StartupTimer::TimeElapsed();
+ // Pings watchdog.
+ watchdog::Watchdog* watchdog = watchdog::Watchdog::GetInstance();
+ if (watchdog) {
+#if defined(_DEBUG)
+ // Injects delay for watchdog debugging.
+ watchdog->MaybeInjectDebugDelay(kWatchdogName);
+#endif // defined(_DEBUG)
+ watchdog->Ping(kWatchdogName);
+ }
+
int64_t used_cpu_memory = SbSystemGetUsedCPUMemory();
base::Optional<int64_t> used_gpu_memory;
if (SbSystemHasCapability(kSbSystemCapabilityCanQueryGPUMemoryStats)) {
diff --git a/cobalt/browser/browser_module.cc b/cobalt/browser/browser_module.cc
index 0873200..c82aa97 100644
--- a/cobalt/browser/browser_module.cc
+++ b/cobalt/browser/browser_module.cc
@@ -53,6 +53,7 @@
#include "cobalt/math/matrix3_f.h"
#include "cobalt/overlay_info/overlay_info_registry.h"
#include "cobalt/persistent_storage/persistent_settings.h"
+#include "cobalt/ui_navigation/scroll_engine/scroll_engine.h"
#include "cobalt/web/csp_delegate_factory.h"
#include "cobalt/web/navigator_ua_data.h"
#include "nb/memory_scope.h"
@@ -227,6 +228,7 @@
event_dispatcher_(event_dispatcher),
account_manager_(account_manager),
is_rendered_(false),
+ is_web_module_rendered_(false),
can_play_type_handler_(media::MediaModule::CreateCanPlayTypeHandler()),
network_module_(network_module),
#if SB_IS(EVERGREEN)
@@ -283,14 +285,15 @@
main_web_module_generation_(0),
next_timeline_id_(1),
current_splash_screen_timeline_id_(-1),
- current_main_web_module_timeline_id_(-1),
- service_worker_registry_(network_module) {
+ current_main_web_module_timeline_id_(-1) {
TRACE_EVENT0("cobalt::browser", "BrowserModule::BrowserModule()");
// Apply platform memory setting adjustments and defaults.
ApplyAutoMemSettings();
platform_info_.reset(new browser::UserAgentPlatformInfo());
+ service_worker_registry_.reset(new ServiceWorkerRegistry(
+ &web_settings_, network_module, platform_info_.get()));
#if SB_HAS(CORE_DUMP_HANDLER_SUPPORT)
SbCoreDumpRegisterHandler(BrowserModule::CoreDumpHandler, this);
@@ -400,7 +403,7 @@
platform_info_.get(), application_state_,
base::Bind(&BrowserModule::QueueOnDebugConsoleRenderTreeProduced,
base::Unretained(this)),
- network_module_, GetViewportSize(), GetResourceProvider(),
+ &web_settings_, network_module_, GetViewportSize(), GetResourceProvider(),
kLayoutMaxRefreshFrequencyInHz,
base::Bind(&BrowserModule::CreateDebugClient, base::Unretained(this)),
base::Bind(&BrowserModule::OnMaybeFreeze, base::Unretained(this))));
@@ -545,7 +548,7 @@
platform_info_.get(), application_state_,
base::Bind(&BrowserModule::QueueOnSplashScreenRenderTreeProduced,
base::Unretained(this)),
- network_module_, viewport_size, GetResourceProvider(),
+ &web_settings_, network_module_, viewport_size, GetResourceProvider(),
kLayoutMaxRefreshFrequencyInHz, fallback_splash_screen_url_,
splash_screen_cache_.get(),
base::Bind(&BrowserModule::DestroySplashScreen, weak_this_),
@@ -554,6 +557,9 @@
}
}
+ scroll_engine_.reset(new ui_navigation::scroll_engine::ScrollEngine());
+ scroll_engine_->thread()->Start();
+
// Create new WebModule.
#if !defined(COBALT_FORCE_CSP)
options_.web_module_options.csp_insecure_allowed_token =
@@ -605,13 +611,14 @@
options.maybe_freeze_callback =
base::Bind(&BrowserModule::OnMaybeFreeze, base::Unretained(this));
+ options.web_options.web_settings = &web_settings_;
options.web_options.network_module = network_module_;
options.web_options.service_worker_jobs =
- service_worker_registry_.service_worker_jobs();
+ service_worker_registry_->service_worker_jobs();
options.web_options.platform_info = platform_info_.get();
web_module_.reset(new WebModule("MainWebModule"));
web_module_->Run(
- url, application_state_,
+ url, application_state_, scroll_engine_.get(),
base::Bind(&BrowserModule::QueueOnRenderTreeProduced,
base::Unretained(this), main_web_module_generation_),
base::Bind(&BrowserModule::OnError, base::Unretained(this)),
@@ -792,8 +799,9 @@
// explicitly.
renderer_submission.timeline_info.id = current_main_web_module_timeline_id_;
- renderer_submission.on_rasterized_callbacks.push_back(base::Bind(
- &BrowserModule::OnRendererSubmissionRasterized, base::Unretained(this)));
+ renderer_submission.on_rasterized_callbacks.push_back(
+ base::Bind(&BrowserModule::OnWebModuleRendererSubmissionRasterized,
+ base::Unretained(this)));
if (!splash_screen_) {
render_tree_combiner_.SetTimelineLayer(main_web_module_layer_.get());
@@ -1104,6 +1112,12 @@
}
#endif // defined(ENABLE_DEBUGGER)
+ scroll_engine_->thread()->message_loop()->task_runner()->PostTask(
+ FROM_HERE,
+ base::Bind(
+ &ui_navigation::scroll_engine::ScrollEngine::HandlePointerEvent,
+ base::Unretained(scroll_engine_.get()), type, event));
+
DCHECK(web_module_);
web_module_->InjectPointerEvent(type, event);
}
@@ -1505,6 +1519,23 @@
}
}
+void BrowserModule::OnWebModuleRendererSubmissionRasterized() {
+ TRACE_EVENT0("cobalt::browser",
+ "BrowserModule::OnFirstWebModuleSubmissionRasterized()");
+ OnRendererSubmissionRasterized();
+ if (!is_web_module_rendered_) {
+ is_web_module_rendered_ = true;
+ const CobaltExtensionGraphicsApi* graphics_extension =
+ static_cast<const CobaltExtensionGraphicsApi*>(
+ SbSystemGetExtension(kCobaltExtensionGraphicsName));
+ if (graphics_extension &&
+ strcmp(graphics_extension->name, kCobaltExtensionGraphicsName) == 0 &&
+ graphics_extension->version >= 6) {
+ graphics_extension->ReportFullyDrawn();
+ }
+ }
+}
+
#if defined(COBALT_CHECK_RENDER_TIMEOUT)
void BrowserModule::OnPollForRenderTimeout(const GURL& url) {
SbTime last_render_timestamp = static_cast<SbTime>(SbAtomicAcquire_Load64(
@@ -2037,8 +2068,9 @@
// The web_module_ member can not be safely used in this function.
h5vcc::H5vcc::Settings h5vcc_settings;
- h5vcc_settings.set_media_source_setting_func = base::Bind(
- &WebModule::SetMediaSourceSetting, base::Unretained(web_module));
+ h5vcc_settings.set_web_setting_func =
+ base::Bind(&web::WebSettingsImpl::Set, base::Unretained(&web_settings_));
+ h5vcc_settings.media_module = media_module_.get();
h5vcc_settings.network_module = network_module_;
#if SB_IS(EVERGREEN)
h5vcc_settings.updater_module = updater_module_;
diff --git a/cobalt/browser/browser_module.h b/cobalt/browser/browser_module.h
index 646b750..05aece7 100644
--- a/cobalt/browser/browser_module.h
+++ b/cobalt/browser/browser_module.h
@@ -68,6 +68,8 @@
#include "cobalt/script/array_buffer.h"
#include "cobalt/storage/storage_manager.h"
#include "cobalt/system_window/system_window.h"
+#include "cobalt/ui_navigation/scroll_engine/scroll_engine.h"
+#include "cobalt/web/web_settings.h"
#include "cobalt/webdriver/session_driver.h"
#include "starboard/configuration.h"
#include "starboard/time.h"
@@ -381,6 +383,10 @@
// system splash screen after the first render has completed.
void OnRendererSubmissionRasterized();
+ // Called when a renderer submission of the (main) web module has been
+ // rasterized. This should also call OnRendererSubmissionRasterized().
+ void OnWebModuleRendererSubmissionRasterized();
+
// Process all messages queued into the |render_tree_submission_queue_|.
void ProcessRenderTreeSubmissionQueue();
@@ -490,6 +496,11 @@
// |weak_ptr_factory_.GetWeakPtr() which is not).
base::WeakPtr<BrowserModule> weak_this_;
+ // Holds browser wide web settings accessible from both the main web thread
+ // and all Workers. It can only be set on the main web module via h5vcc but
+ // any setting changes affect all web modules.
+ web::WebSettingsImpl web_settings_;
+
// Memory configuration tool.
memory_settings::AutoMem auto_mem_;
@@ -514,6 +525,9 @@
// render, we hide the system splash screen.
bool is_rendered_;
+ // Whether the (main) web module has yet rendered anything.
+ bool is_web_module_rendered_;
+
// The main system window for our application. This routes input event
// callbacks, and provides a native window handle on desktop systems.
std::unique_ptr<system_window::SystemWindow> system_window_;
@@ -569,6 +583,9 @@
std::unique_ptr<dom::OnScreenKeyboardBridge> on_screen_keyboard_bridge_;
bool on_screen_keyboard_show_called_ = false;
+ // Handles pointer events for scroll containers.
+ std::unique_ptr<ui_navigation::scroll_engine::ScrollEngine> scroll_engine_;
+
// Sets up everything to do with web page management, from loading and
// parsing the web page and all referenced files to laying it out. The
// web module will ultimately produce a render tree that can be passed
@@ -717,7 +734,7 @@
std::unique_ptr<browser::UserAgentPlatformInfo> platform_info_;
// Manages the Service Workers.
- ServiceWorkerRegistry service_worker_registry_;
+ std::unique_ptr<ServiceWorkerRegistry> service_worker_registry_;
};
} // namespace browser
diff --git a/cobalt/browser/debug_console.cc b/cobalt/browser/debug_console.cc
index 1f0bba5..197233b 100644
--- a/cobalt/browser/debug_console.cc
+++ b/cobalt/browser/debug_console.cc
@@ -109,7 +109,7 @@
base::ApplicationState initial_application_state,
const WebModule::OnRenderTreeProducedCallback&
render_tree_produced_callback,
- network::NetworkModule* network_module,
+ web::WebSettings* web_settings, network::NetworkModule* network_module,
const cssom::ViewportSize& window_dimensions,
render_tree::ResourceProvider* resource_provider, float layout_refresh_rate,
const debug::CreateDebugClientCallback& create_debug_client_callback,
@@ -140,12 +140,13 @@
// Pass down this callback from Browser module to Web module eventually.
web_module_options.maybe_freeze_callback = maybe_freeze_callback;
+ web_module_options.web_options.web_settings = web_settings;
web_module_options.web_options.network_module = network_module;
web_module_options.web_options.platform_info = platform_info;
web_module_.reset(new WebModule("DebugConsoleWebModule"));
web_module_->Run(GURL(kInitialDebugConsoleUrl), initial_application_state,
- render_tree_produced_callback,
+ nullptr /* scroll_engine */, render_tree_produced_callback,
base::Bind(&DebugConsole::OnError, base::Unretained(this)),
WebModule::CloseCallback(), /* window_close_callback */
base::Closure(), /* window_minimize_callback */
diff --git a/cobalt/browser/debug_console.h b/cobalt/browser/debug_console.h
index 0ed23a9..c77e1ec 100644
--- a/cobalt/browser/debug_console.h
+++ b/cobalt/browser/debug_console.h
@@ -35,6 +35,7 @@
#include "cobalt/dom/wheel_event_init.h"
#include "cobalt/dom/window.h"
#include "cobalt/web/user_agent_platform_info.h"
+#include "cobalt/web/web_settings.h"
#include "url/gurl.h"
namespace cobalt {
@@ -49,7 +50,7 @@
base::ApplicationState initial_application_state,
const WebModule::OnRenderTreeProducedCallback&
render_tree_produced_callback,
- network::NetworkModule* network_module,
+ web::WebSettings* web_settings, network::NetworkModule* network_module,
const cssom::ViewportSize& window_dimensions,
render_tree::ResourceProvider* resource_provider,
float layout_refresh_rate,
diff --git a/cobalt/browser/idl_files.gni b/cobalt/browser/idl_files.gni
index 37256c3..b64086d 100644
--- a/cobalt/browser/idl_files.gni
+++ b/cobalt/browser/idl_files.gni
@@ -166,7 +166,6 @@
"//cobalt/h5vcc/h5vcc_runtime.idl",
"//cobalt/h5vcc/h5vcc_runtime_event_target.idl",
"//cobalt/h5vcc/h5vcc_settings.idl",
- "//cobalt/h5vcc/h5vcc_sso.idl",
"//cobalt/h5vcc/h5vcc_storage.idl",
"//cobalt/h5vcc/h5vcc_screen.idl",
"//cobalt/h5vcc/h5vcc_system.idl",
@@ -239,6 +238,7 @@
"//cobalt/worker/dedicated_worker_global_scope.idl",
"//cobalt/worker/extendable_event.idl",
"//cobalt/worker/extendable_message_event.idl",
+ "//cobalt/worker/fetch_event.idl",
"//cobalt/worker/navigation_preload_manager.idl",
"//cobalt/worker/service_worker.idl",
"//cobalt/worker/service_worker_container.idl",
@@ -355,6 +355,7 @@
"//cobalt/worker/client_query_options.idl",
"//cobalt/worker/extendable_event_init.idl",
"//cobalt/worker/extendable_message_event_init.idl",
+ "//cobalt/worker/fetch_event_init.idl",
"//cobalt/worker/frame_type.idl",
"//cobalt/worker/navigation_preload_state.idl",
"//cobalt/worker/registration_options.idl",
diff --git a/cobalt/browser/service_worker_registry.cc b/cobalt/browser/service_worker_registry.cc
index c0a4e00..2fdd068 100644
--- a/cobalt/browser/service_worker_registry.cc
+++ b/cobalt/browser/service_worker_registry.cc
@@ -33,19 +33,21 @@
} // namespace
void ServiceWorkerRegistry::WillDestroyCurrentMessageLoop() {
- // Clear all member variables allocated form the thread.
+ // Clear all member variables allocated from the thread.
service_worker_jobs_.reset();
}
ServiceWorkerRegistry::ServiceWorkerRegistry(
- network::NetworkModule* network_module)
+ web::WebSettings* web_settings, network::NetworkModule* network_module,
+ web::UserAgentPlatformInfo* platform_info)
: thread_("ServiceWorkerRegistry") {
if (!thread_.Start()) return;
DCHECK(message_loop());
message_loop()->task_runner()->PostTask(
- FROM_HERE, base::Bind(&ServiceWorkerRegistry::Initialize,
- base::Unretained(this), network_module));
+ FROM_HERE,
+ base::Bind(&ServiceWorkerRegistry::Initialize, base::Unretained(this),
+ web_settings, network_module, platform_info));
// Register as a destruction observer to shut down the Web Agent once all
// pending tasks have been executed and the message loop is about to be
@@ -71,6 +73,7 @@
// Ensure that the destruction observer got added before stopping the thread.
// Stop the thread. This will cause the destruction observer to be notified.
destruction_observer_added_.Wait();
+ service_worker_jobs_->Stop();
thread_.Stop();
}
@@ -80,11 +83,13 @@
return service_worker_jobs_.get();
}
-void ServiceWorkerRegistry::Initialize(network::NetworkModule* network_module) {
+void ServiceWorkerRegistry::Initialize(
+ web::WebSettings* web_settings, network::NetworkModule* network_module,
+ web::UserAgentPlatformInfo* platform_info) {
TRACE_EVENT0("cobalt::browser", "ServiceWorkerRegistry::Initialize()");
DCHECK_EQ(base::MessageLoop::current(), message_loop());
- service_worker_jobs_.reset(
- new worker::ServiceWorkerJobs(network_module, message_loop()));
+ service_worker_jobs_.reset(new worker::ServiceWorkerJobs(
+ web_settings, network_module, platform_info, message_loop()));
}
} // namespace browser
diff --git a/cobalt/browser/service_worker_registry.h b/cobalt/browser/service_worker_registry.h
index 4b4239b..14abedd 100644
--- a/cobalt/browser/service_worker_registry.h
+++ b/cobalt/browser/service_worker_registry.h
@@ -21,6 +21,7 @@
#include "base/synchronization/waitable_event.h"
#include "base/threading/thread.h"
#include "cobalt/network/network_module.h"
+#include "cobalt/web/web_settings.h"
#include "cobalt/worker/service_worker_jobs.h"
namespace cobalt {
@@ -31,7 +32,9 @@
// metadata are stored persistently on disk.
class ServiceWorkerRegistry : public base::MessageLoop::DestructionObserver {
public:
- explicit ServiceWorkerRegistry(network::NetworkModule* network_module);
+ ServiceWorkerRegistry(web::WebSettings* web_settings,
+ network::NetworkModule* network_module,
+ web::UserAgentPlatformInfo* platform_info);
~ServiceWorkerRegistry();
// The message loop this object is running on.
@@ -45,7 +48,9 @@
private:
// Called by the constructor to perform any other initialization required on
// the dedicated thread.
- void Initialize(network::NetworkModule* network_module);
+ void Initialize(web::WebSettings* web_settings,
+ network::NetworkModule* network_module,
+ web::UserAgentPlatformInfo* platform_info);
// The thread created and owned by the Service Worker Registry.
// All registry mutations occur on this thread. The thread has to outlive all
diff --git a/cobalt/browser/splash_screen.cc b/cobalt/browser/splash_screen.cc
index 952f790..ea764a6 100644
--- a/cobalt/browser/splash_screen.cc
+++ b/cobalt/browser/splash_screen.cc
@@ -55,7 +55,7 @@
base::ApplicationState initial_application_state,
const WebModule::OnRenderTreeProducedCallback&
render_tree_produced_callback,
- network::NetworkModule* network_module,
+ web::WebSettings* web_settings, network::NetworkModule* network_module,
const cssom::ViewportSize& window_dimensions,
render_tree::ResourceProvider* resource_provider, float layout_refresh_rate,
const base::Optional<GURL>& fallback_splash_screen_url,
@@ -102,14 +102,15 @@
// Pass down this callback from Browser module to Web module eventually.
web_module_options.maybe_freeze_callback = maybe_freeze_callback;
+ web_module_options.web_options.web_settings = web_settings;
web_module_options.web_options.network_module = network_module;
web_module_options.web_options.platform_info = platform_info;
DCHECK(url_to_pass);
web_module_.reset(new WebModule("SplashScreenWebModule"));
web_module_->Run(*url_to_pass, initial_application_state,
- render_tree_produced_callback_, base::Bind(&OnError),
- on_window_close,
+ nullptr /* scroll_engine */, render_tree_produced_callback_,
+ base::Bind(&OnError), on_window_close,
base::Closure(), // window_minimize_callback
NULL /* can_play_type_handler */, NULL /* media_module */,
window_dimensions, resource_provider, layout_refresh_rate,
diff --git a/cobalt/browser/splash_screen.h b/cobalt/browser/splash_screen.h
index 6453862..f50c2e4 100644
--- a/cobalt/browser/splash_screen.h
+++ b/cobalt/browser/splash_screen.h
@@ -26,6 +26,7 @@
#include "cobalt/cssom/viewport_size.h"
#include "cobalt/dom/window.h"
#include "cobalt/web/user_agent_platform_info.h"
+#include "cobalt/web/web_settings.h"
#include "url/gurl.h"
namespace cobalt {
@@ -39,6 +40,7 @@
base::ApplicationState initial_application_state,
const WebModule::OnRenderTreeProducedCallback&
render_tree_produced_callback,
+ web::WebSettings* web_settings,
network::NetworkModule* network_module,
const cssom::ViewportSize& window_dimensions,
render_tree::ResourceProvider* resource_provider,
diff --git a/cobalt/browser/web_module.cc b/cobalt/browser/web_module.cc
index 6cb0ea4..dc7ae32 100644
--- a/cobalt/browser/web_module.cc
+++ b/cobalt/browser/web_module.cc
@@ -48,7 +48,6 @@
#include "cobalt/dom/keyboard_event.h"
#include "cobalt/dom/keyboard_event_init.h"
#include "cobalt/dom/local_storage_database.h"
-#include "cobalt/dom/media_source_settings.h"
#include "cobalt/dom/mutation_observer_task_manager.h"
#include "cobalt/dom/navigation_type.h"
#include "cobalt/dom/navigator.h"
@@ -67,6 +66,7 @@
#include "cobalt/media/media_module.h"
#include "cobalt/media_session/media_session_client.h"
#include "cobalt/storage/storage_manager.h"
+#include "cobalt/ui_navigation/scroll_engine/scroll_engine.h"
#include "cobalt/web/blob.h"
#include "cobalt/web/context.h"
#include "cobalt/web/csp_delegate_factory.h"
@@ -216,8 +216,6 @@
void SetSize(cssom::ViewportSize viewport_size);
void UpdateCamera3D(const scoped_refptr<input::Camera3D>& camera_3d);
void SetMediaModule(media::MediaModule* media_module);
- void SetMediaSourceSetting(const std::string& name, int value,
- bool* succeeded);
void SetImageCacheCapacity(int64_t bytes);
void SetRemoteTypefaceCacheCapacity(int64_t bytes);
@@ -343,6 +341,8 @@
web::Context* web_context_;
+ ui_navigation::scroll_engine::ScrollEngine* scroll_engine_;
+
// Simple flag used for basic error checking.
bool is_running_;
@@ -399,9 +399,6 @@
// Object to register and retrieve MediaSource object with a string key.
std::unique_ptr<dom::MediaSource::Registry> media_source_registry_;
- // Object to hold WebModule wide settings for MediaSource related objects.
- dom::MediaSourceSettingsImpl media_source_settings_;
-
// The Window object wraps all DOM-related components.
scoped_refptr<dom::Window> window_;
@@ -488,6 +485,7 @@
WebModule::Impl::Impl(web::Context* web_context, const ConstructionData& data)
: web_context_(web_context),
+ scroll_engine_(data.scroll_engine),
is_running_(false),
is_render_tree_rasterization_pending_(
base::StringPrintf("%s.IsRenderTreeRasterizationPending",
@@ -505,7 +503,6 @@
base::Bind(&WebModule::Impl::LogScriptError, base::Unretained(this));
web_context_->javascript_engine()->RegisterErrorHandler(error_handler);
#endif
-
css_parser::Parser::SupportsMapToMeshFlag supports_map_to_mesh =
data.options.enable_map_to_mesh
? css_parser::Parser::kSupportsMapToMesh
@@ -586,8 +583,8 @@
web_context_->setup_environment_settings(new dom::DOMSettings(
debugger_hooks_, kDOMMaxElementDepth, media_source_registry_.get(),
- &media_source_settings_, data.can_play_type_handler, memory_info,
- &mutation_observer_task_manager_, data.options.dom_settings_options));
+ data.can_play_type_handler, memory_info, &mutation_observer_task_manager_,
+ data.options.dom_settings_options));
DCHECK(web_context_->environment_settings());
// From algorithm to setup up a window environment settings object:
// https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#set-up-a-window-environment-settings-object
@@ -1103,12 +1100,6 @@
window_->set_web_media_player_factory(media_module);
}
-void WebModule::Impl::SetMediaSourceSetting(const std::string& name, int value,
- bool* succeeded) {
- DCHECK(succeeded);
- *succeeded = media_source_settings_.Set(name, value);
-}
-
void WebModule::Impl::SetApplicationState(base::ApplicationState state,
SbTimeMonotonic timestamp) {
window_->SetApplicationState(state, timestamp);
@@ -1316,7 +1307,8 @@
base::polymorphic_downcast<const dom::UIEvent* const>(event.get())
->view());
if (!topmost_event_target_) {
- topmost_event_target_.reset(new layout::TopmostEventTarget());
+ topmost_event_target_.reset(
+ new layout::TopmostEventTarget(scroll_engine_));
}
topmost_event_target_->MaybeSendPointerEvents(event);
}
@@ -1350,6 +1342,7 @@
void WebModule::Run(
const GURL& initial_url, base::ApplicationState initial_application_state,
+ ui_navigation::scroll_engine::ScrollEngine* scroll_engine,
const OnRenderTreeProducedCallback& render_tree_produced_callback,
OnErrorCallback error_callback, const CloseCallback& window_close_callback,
const base::Closure& window_minimize_callback,
@@ -1358,10 +1351,11 @@
render_tree::ResourceProvider* resource_provider, float layout_refresh_rate,
const Options& options) {
ConstructionData construction_data(
- initial_url, initial_application_state, render_tree_produced_callback,
- error_callback, window_close_callback, window_minimize_callback,
- can_play_type_handler, media_module, window_dimensions, resource_provider,
- kDOMMaxElementDepth, layout_refresh_rate, ui_nav_root_,
+ initial_url, initial_application_state, scroll_engine,
+ render_tree_produced_callback, error_callback, window_close_callback,
+ window_minimize_callback, can_play_type_handler, media_module,
+ window_dimensions, resource_provider, kDOMMaxElementDepth,
+ layout_refresh_rate, ui_nav_root_,
#if defined(ENABLE_DEBUGGER)
&waiting_for_web_debugger_,
#endif // defined(ENABLE_DEBUGGER)
@@ -1605,12 +1599,6 @@
impl_->SetMediaModule(media_module);
}
-bool WebModule::SetMediaSourceSetting(const std::string& name, int value) {
- bool succeeded = false;
- SetMediaSourceSettingInternal(name, value, &succeeded);
- return succeeded;
-}
-
void WebModule::SetImageCacheCapacity(int64_t bytes) {
POST_TO_ENSURE_IMPL_ON_THREAD(SetImageCacheCapacity, bytes);
impl_->SetImageCacheCapacity(bytes);
@@ -1744,12 +1732,5 @@
impl_->SetUnloadEventTimingInfo(start_time, end_time);
}
-void WebModule::SetMediaSourceSettingInternal(const std::string& name,
- int value, bool* succeeded) {
- POST_AND_BLOCK_TO_ENSURE_IMPL_ON_THREAD(SetMediaSourceSettingInternal, name,
- value, succeeded);
- impl_->SetMediaSourceSetting(name, value, succeeded);
-}
-
} // namespace browser
} // namespace cobalt
diff --git a/cobalt/browser/web_module.h b/cobalt/browser/web_module.h
index a29c1bd..d3fa41c 100644
--- a/cobalt/browser/web_module.h
+++ b/cobalt/browser/web_module.h
@@ -51,6 +51,7 @@
#include "cobalt/render_tree/node.h"
#include "cobalt/render_tree/resource_provider.h"
#include "cobalt/ui_navigation/nav_item.h"
+#include "cobalt/ui_navigation/scroll_engine/scroll_engine.h"
#include "cobalt/web/agent.h"
#include "cobalt/web/blob.h"
#include "cobalt/web/context.h"
@@ -264,6 +265,7 @@
~WebModule();
void Run(const GURL& initial_url,
base::ApplicationState initial_application_state,
+ ui_navigation::scroll_engine::ScrollEngine* scroll_engine,
const OnRenderTreeProducedCallback& render_tree_produced_callback,
OnErrorCallback error_callback,
const CloseCallback& window_close_callback,
@@ -351,7 +353,6 @@
void UpdateCamera3D(const scoped_refptr<input::Camera3D>& camera_3d);
void SetMediaModule(media::MediaModule* media_module);
- bool SetMediaSourceSetting(const std::string& name, int value);
void SetImageCacheCapacity(int64_t bytes);
void SetRemoteTypefaceCacheCapacity(int64_t bytes);
@@ -408,6 +409,7 @@
struct ConstructionData {
ConstructionData(const GURL& initial_url,
base::ApplicationState initial_application_state,
+ ui_navigation::scroll_engine::ScrollEngine* scroll_engine,
OnRenderTreeProducedCallback render_tree_produced_callback,
const OnErrorCallback& error_callback,
CloseCallback window_close_callback,
@@ -425,6 +427,7 @@
const Options& options)
: initial_url(initial_url),
initial_application_state(initial_application_state),
+ scroll_engine(scroll_engine),
render_tree_produced_callback(render_tree_produced_callback),
error_callback(error_callback),
window_close_callback(window_close_callback),
@@ -445,6 +448,7 @@
GURL initial_url;
base::ApplicationState initial_application_state;
+ ui_navigation::scroll_engine::ScrollEngine* scroll_engine;
OnRenderTreeProducedCallback render_tree_produced_callback;
OnErrorCallback error_callback;
CloseCallback window_close_callback;
@@ -475,9 +479,6 @@
void GetIsReadyToFreeze(volatile bool* is_ready_to_freeze);
- void SetMediaSourceSettingInternal(const std::string& name, int value,
- bool* succeeded);
-
// The message loop this object is running on.
base::MessageLoop* message_loop() const {
DCHECK(web_agent_);
diff --git a/cobalt/build/build.id b/cobalt/build/build.id
index e77222b..7dca786 100644
--- a/cobalt/build/build.id
+++ b/cobalt/build/build.id
@@ -1 +1 @@
-309559
+310436
\ No newline at end of file
diff --git a/cobalt/build/cobalt_configuration.py b/cobalt/build/cobalt_configuration.py
index c70dfdf..a8c09b1 100644
--- a/cobalt/build/cobalt_configuration.py
+++ b/cobalt/build/cobalt_configuration.py
@@ -74,6 +74,10 @@
# the proxy has problems sending and terminating a single complete
# response. It may end up sending multiple empty responses.
filters = [
+ # CORS - 304 checks
+ # Disabled because of: Flaky on buildbot, proxy unreliability
+ 'cors/WebPlatformTest.Run/cors_304_htm',
+
# Late listeners: Preflight.
# Disabled because of: Flaky. Buildbot only failure.
'cors/WebPlatformTest.Run/cors_late_upload_events_htm',
@@ -167,6 +171,7 @@
'nb_test',
'net_unittests',
'network_test',
+ 'persistent_settings_test',
'poem_unittests',
'render_tree_test',
'renderer_test',
diff --git a/cobalt/build/gn.py b/cobalt/build/gn.py
index 88fa813..973ec4c 100755
--- a/cobalt/build/gn.py
+++ b/cobalt/build/gn.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
# Copyright 2021 The Cobalt Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,6 +18,7 @@
import os
import shutil
import subprocess
+import sys
from pathlib import Path
from typing import List
@@ -42,7 +43,10 @@
print(f'{dst_args_gn_file} already exists.' +
' Running ninja will regenerate build files automatically.')
- gn_command = ['gn', 'gen', out_directory] + gn_gen_args
+ gn_command = [
+ 'gn', '--script-executable={}'.format(sys.executable), 'gen',
+ out_directory
+ ] + gn_gen_args
print(' '.join(gn_command))
subprocess.check_call(gn_command)
diff --git a/cobalt/cache/cache.cc b/cobalt/cache/cache.cc
index aeb1132..2dc360a 100644
--- a/cobalt/cache/cache.cc
+++ b/cobalt/cache/cache.cc
@@ -38,6 +38,8 @@
switch (resource_type) {
case disk_cache::ResourceType::kCompiledScript:
return 4096u;
+ case disk_cache::ResourceType::kServiceWorkerScript:
+ return 1u;
default:
return base::nullopt;
}
@@ -50,6 +52,8 @@
return "cache_api";
case disk_cache::ResourceType::kCompiledScript:
return "compiled_js";
+ case disk_cache::ResourceType::kServiceWorkerScript:
+ return "service_worker_js";
default:
return base::nullopt;
}
@@ -113,6 +117,7 @@
}
void Cache::DeleteAll() {
+ Delete(disk_cache::ResourceType::kServiceWorkerScript);
Delete(disk_cache::ResourceType::kCompiledScript);
Delete(disk_cache::ResourceType::kCacheApi);
}
@@ -271,6 +276,7 @@
switch (resource_type) {
case disk_cache::ResourceType::kCacheApi:
case disk_cache::ResourceType::kCompiledScript:
+ case disk_cache::ResourceType::kServiceWorkerScript:
return disk_cache::kTypeMetadata[resource_type].max_size_bytes;
default:
return base::nullopt;
diff --git a/cobalt/cache/cache.h b/cobalt/cache/cache.h
index 5b57be6..fc9339b 100644
--- a/cobalt/cache/cache.h
+++ b/cobalt/cache/cache.h
@@ -92,7 +92,7 @@
std::map<disk_cache::ResourceType,
std::map<uint32_t, std::vector<base::WaitableEvent*>>>
pending_;
- bool enabled_;
+ bool enabled_ = true;
persistent_storage::PersistentSettings* persistent_settings_ = nullptr;
diff --git a/cobalt/cache/memory_capped_directory.cc b/cobalt/cache/memory_capped_directory.cc
index de70439..b44b3c6 100644
--- a/cobalt/cache/memory_capped_directory.cc
+++ b/cobalt/cache/memory_capped_directory.cc
@@ -97,7 +97,7 @@
base::DeleteFile(metadata_path, false);
}
file_sizes_.erase(file_path);
- file_keys_with_metadata_.erase(file_path);
+ file_keys_with_metadata_.erase(metadata_path);
auto* heap = &file_info_heap_;
for (auto it = heap->begin(); it != heap->end(); ++it) {
if (it->file_path_ == file_path) {
diff --git a/cobalt/content/fonts/config/android/fonts.xml b/cobalt/content/fonts/config/android/fonts.xml
index 578c124..9ced68c 100644
--- a/cobalt/content/fonts/config/android/fonts.xml
+++ b/cobalt/content/fonts/config/android/fonts.xml
@@ -113,6 +113,18 @@
<font weight="700" style="italic" font_name="Roboto Bold Italic" postscript_name="Roboto-BoldItalic">Roboto-BoldItalic.ttf</font>
<font weight="900" style="normal" font_name="Roboto Black" postscript_name="Roboto-Black">Roboto-Black.ttf</font>
<font weight="900" style="italic" font_name="Roboto Black Italic" postscript_name="Roboto-Black-Italic">Roboto-BlackItalic.ttf</font>
+ <!-- Android 12+ uses font variations -->
+ <font weight="100" style="normal" tag_wght="100" tag_ital="0" font_name="Roboto Thin" postscript_name="Roboto-Thin">Roboto-Regular.ttf</font>
+ <font weight="100" style="italic" tag_wght="100" tag_ital="1" font_name="Roboto Thin Italic" postscript_name="Roboto-Thin-Italic">Roboto-Regular.ttf</font>
+ <font weight="300" style="normal" tag_wght="300" tag_ital="0" font_name="Roboto Light" postscript_name="Roboto-Light">Roboto-Regular.ttf</font>
+ <font weight="300" style="italic" tag_wght="300" tag_ital="1" font_name="Roboto Light Italic" postscript_name="Roboto-Light-Italic">Roboto-Regular.ttf</font>
+ <font weight="400" style="italic" tag_wght="400" tag_ital="1" font_name="Roboto Regular Italic" postscript_name="Roboto-Italic">Roboto-Regular.ttf</font>
+ <font weight="500" style="normal" tag_wght="500" tag_ital="0" font_name="Roboto Medium" postscript_name="Roboto-Medium">Roboto-Regular.ttf</font>
+ <font weight="500" style="italic" tag_wght="500" tag_ital="1" font_name="Roboto Medium Italic" postscript_name="Roboto-Medium-Italic">Roboto-Regular.ttf</font>
+ <font weight="700" style="normal" tag_wght="700" tag_ital="0" font_name="Roboto Bold" postscript_name="Roboto-Bold">Roboto-Regular.ttf</font>
+ <font weight="700" style="italic" tag_wght="700" tag_ital="1" font_name="Roboto Bold Italic" postscript_name="Roboto-BoldItalic">Roboto-Regular.ttf</font>
+ <font weight="900" style="normal" tag_wght="900" tag_ital="0" font_name="Roboto Black" postscript_name="Roboto-Black">Roboto-Regular.ttf</font>
+ <font weight="900" style="italic" tag_wght="900" tag_ital="1" font_name="Roboto Black Italic" postscript_name="Roboto-Black-Italic">Roboto-Regular.ttf</font>
</family>
<!-- Note that aliases must come after the fonts they reference. -->
<alias name="roboto" to="sans-serif" />
diff --git a/cobalt/content/licenses/platform/default/licenses_cobalt.txt b/cobalt/content/licenses/platform/default/licenses_cobalt.txt
index fd93a89..3b7afae 100644
--- a/cobalt/content/licenses/platform/default/licenses_cobalt.txt
+++ b/cobalt/content/licenses/platform/default/licenses_cobalt.txt
@@ -5549,3 +5549,96 @@
liability, whether in an action of contract, tort or otherwise, arising from,
out of or in connection with the Software or the use or other dealings in the
Software.
+
+
+
+ Fraunhofer FDK AAC Codec Library
+
+ Software License for The Fraunhofer FDK AAC Codec Library for Android
+ © Copyright 1995 - 2013 Fraunhofer-Gesellschaft zur Förderung der angewandten Forschung e.V.
+ All rights reserved.
+ 1. INTRODUCTION
+ The Fraunhofer FDK AAC Codec Library for Android ("FDK AAC Codec") is software that implements
+ the MPEG Advanced Audio Coding ("AAC") encoding and decoding scheme for digital audio.
+ This FDK AAC Codec software is intended to be used on a wide variety of Android devices.
+ AAC's HE-AAC and HE-AAC v2 versions are regarded as today's most efficient general perceptual
+ audio codecs. AAC-ELD is considered the best-performing full-bandwidth communications codec by
+ independent studies and is widely deployed. AAC has been standardized by ISO and IEC as part
+ of the MPEG specifications.
+ Patent licenses for necessary patent claims for the FDK AAC Codec (including those of Fraunhofer)
+ may be obtained through Via Licensing (www.vialicensing.com) or through the respective patent owners
+ individually for the purpose of encoding or decoding bit streams in products that are compliant with
+ the ISO/IEC MPEG audio standards. Please note that most manufacturers of Android devices already license
+ these patent claims through Via Licensing or directly from the patent owners, and therefore FDK AAC Codec
+ software may already be covered under those patent licenses when it is used for those licensed purposes only.
+ Commercially-licensed AAC software libraries, including floating-point versions with enhanced sound quality,
+ are also available from Fraunhofer. Users are encouraged to check the Fraunhofer website for additional
+ applications information and documentation.
+ 2. COPYRIGHT LICENSE
+ Redistribution and use in source and binary forms, with or without modification, are permitted without
+ payment of copyright license fees provided that you satisfy the following conditions:
+ You must retain the complete text of this software license in redistributions of the FDK AAC Codec or
+ your modifications thereto in source code form.
+ You must retain the complete text of this software license in the documentation and/or other materials
+ provided with redistributions of the FDK AAC Codec or your modifications thereto in binary form.
+ You must make available free of charge copies of the complete source code of the FDK AAC Codec and your
+ modifications thereto to recipients of copies in binary form.
+ The name of Fraunhofer may not be used to endorse or promote products derived from this library without
+ prior written permission.
+ You may not charge copyright license fees for anyone to use, copy or distribute the FDK AAC Codec
+ software or your modifications thereto.
+ Your modified versions of the FDK AAC Codec must carry prominent notices stating that you changed the software
+ and the date of any change. For modified versions of the FDK AAC Codec, the term
+ "Fraunhofer FDK AAC Codec Library for Android" must be replaced by the term
+ "Third-Party Modified Version of the Fraunhofer FDK AAC Codec Library for Android."
+ 3. NO PATENT LICENSE
+ NO EXPRESS OR IMPLIED LICENSES TO ANY PATENT CLAIMS, including without limitation the patents of Fraunhofer,
+ ARE GRANTED BY THIS SOFTWARE LICENSE. Fraunhofer provides no warranty of patent non-infringement with
+ respect to this software.
+ You may use this FDK AAC Codec software or modifications thereto only for purposes that are authorized
+ by appropriate patent licenses.
+ 4. DISCLAIMER
+ This FDK AAC Codec software is provided by Fraunhofer on behalf of the copyright holders and contributors
+ "AS IS" and WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, including but not limited to the implied warranties
+ of merchantability and fitness for a particular purpose. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+ CONTRIBUTORS BE LIABLE for any direct, indirect, incidental, special, exemplary, or consequential damages,
+ including but not limited to procurement of substitute goods or services; loss of use, data, or profits,
+ or business interruption, however caused and on any theory of liability, whether in contract, strict
+ liability, or tort (including negligence), arising in any way out of the use of this software, even if
+ advised of the possibility of such damage.
+ 5. CONTACT INFORMATION
+ Fraunhofer Institute for Integrated Circuits IIS
+ Attention: Audio and Multimedia Departments - FDK AAC LL
+ Am Wolfsmantel 33
+ 91058 Erlangen, Germany
+ www.iis.fraunhofer.de/amm
+ amm-info@iis.fraunhofer.de
+
+
+
+ openh264
+
+
+ Copyright (c) 2013, Cisco Systems
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without modification,
+ are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright notice, this
+ list of conditions and the following disclaimer in the documentation and/or
+ other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/cobalt/content/licenses/platform/evergreen/licenses_cobalt.txt b/cobalt/content/licenses/platform/evergreen/licenses_cobalt.txt
index 8063535..2244b3d 100644
--- a/cobalt/content/licenses/platform/evergreen/licenses_cobalt.txt
+++ b/cobalt/content/licenses/platform/evergreen/licenses_cobalt.txt
@@ -5370,3 +5370,96 @@
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+
+ Fraunhofer FDK AAC Codec Library
+
+ Software License for The Fraunhofer FDK AAC Codec Library for Android
+ © Copyright 1995 - 2013 Fraunhofer-Gesellschaft zur Förderung der angewandten Forschung e.V.
+ All rights reserved.
+ 1. INTRODUCTION
+ The Fraunhofer FDK AAC Codec Library for Android ("FDK AAC Codec") is software that implements
+ the MPEG Advanced Audio Coding ("AAC") encoding and decoding scheme for digital audio.
+ This FDK AAC Codec software is intended to be used on a wide variety of Android devices.
+ AAC's HE-AAC and HE-AAC v2 versions are regarded as today's most efficient general perceptual
+ audio codecs. AAC-ELD is considered the best-performing full-bandwidth communications codec by
+ independent studies and is widely deployed. AAC has been standardized by ISO and IEC as part
+ of the MPEG specifications.
+ Patent licenses for necessary patent claims for the FDK AAC Codec (including those of Fraunhofer)
+ may be obtained through Via Licensing (www.vialicensing.com) or through the respective patent owners
+ individually for the purpose of encoding or decoding bit streams in products that are compliant with
+ the ISO/IEC MPEG audio standards. Please note that most manufacturers of Android devices already license
+ these patent claims through Via Licensing or directly from the patent owners, and therefore FDK AAC Codec
+ software may already be covered under those patent licenses when it is used for those licensed purposes only.
+ Commercially-licensed AAC software libraries, including floating-point versions with enhanced sound quality,
+ are also available from Fraunhofer. Users are encouraged to check the Fraunhofer website for additional
+ applications information and documentation.
+ 2. COPYRIGHT LICENSE
+ Redistribution and use in source and binary forms, with or without modification, are permitted without
+ payment of copyright license fees provided that you satisfy the following conditions:
+ You must retain the complete text of this software license in redistributions of the FDK AAC Codec or
+ your modifications thereto in source code form.
+ You must retain the complete text of this software license in the documentation and/or other materials
+ provided with redistributions of the FDK AAC Codec or your modifications thereto in binary form.
+ You must make available free of charge copies of the complete source code of the FDK AAC Codec and your
+ modifications thereto to recipients of copies in binary form.
+ The name of Fraunhofer may not be used to endorse or promote products derived from this library without
+ prior written permission.
+ You may not charge copyright license fees for anyone to use, copy or distribute the FDK AAC Codec
+ software or your modifications thereto.
+ Your modified versions of the FDK AAC Codec must carry prominent notices stating that you changed the software
+ and the date of any change. For modified versions of the FDK AAC Codec, the term
+ "Fraunhofer FDK AAC Codec Library for Android" must be replaced by the term
+ "Third-Party Modified Version of the Fraunhofer FDK AAC Codec Library for Android."
+ 3. NO PATENT LICENSE
+ NO EXPRESS OR IMPLIED LICENSES TO ANY PATENT CLAIMS, including without limitation the patents of Fraunhofer,
+ ARE GRANTED BY THIS SOFTWARE LICENSE. Fraunhofer provides no warranty of patent non-infringement with
+ respect to this software.
+ You may use this FDK AAC Codec software or modifications thereto only for purposes that are authorized
+ by appropriate patent licenses.
+ 4. DISCLAIMER
+ This FDK AAC Codec software is provided by Fraunhofer on behalf of the copyright holders and contributors
+ "AS IS" and WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, including but not limited to the implied warranties
+ of merchantability and fitness for a particular purpose. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+ CONTRIBUTORS BE LIABLE for any direct, indirect, incidental, special, exemplary, or consequential damages,
+ including but not limited to procurement of substitute goods or services; loss of use, data, or profits,
+ or business interruption, however caused and on any theory of liability, whether in contract, strict
+ liability, or tort (including negligence), arising in any way out of the use of this software, even if
+ advised of the possibility of such damage.
+ 5. CONTACT INFORMATION
+ Fraunhofer Institute for Integrated Circuits IIS
+ Attention: Audio and Multimedia Departments - FDK AAC LL
+ Am Wolfsmantel 33
+ 91058 Erlangen, Germany
+ www.iis.fraunhofer.de/amm
+ amm-info@iis.fraunhofer.de
+
+
+
+ openh264
+
+
+ Copyright (c) 2013, Cisco Systems
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without modification,
+ are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright notice, this
+ list of conditions and the following disclaimer in the documentation and/or
+ other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/cobalt/content/ssl/certs/03179a64.0 b/cobalt/content/ssl/certs/03179a64.0
deleted file mode 100644
index 315f665..0000000
--- a/cobalt/content/ssl/certs/03179a64.0
+++ /dev/null
@@ -1,32 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIFcDCCA1igAwIBAgIEAJiWjTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJO
-TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSkwJwYDVQQDDCBTdGFh
-dCBkZXIgTmVkZXJsYW5kZW4gRVYgUm9vdCBDQTAeFw0xMDEyMDgxMTE5MjlaFw0y
-MjEyMDgxMTEwMjhaMFgxCzAJBgNVBAYTAk5MMR4wHAYDVQQKDBVTdGFhdCBkZXIg
-TmVkZXJsYW5kZW4xKTAnBgNVBAMMIFN0YWF0IGRlciBOZWRlcmxhbmRlbiBFViBS
-b290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA48d+ifkkSzrS
-M4M1LGns3Amk41GoJSt5uAg94JG6hIXGhaTK5skuU6TJJB79VWZxXSzFYGgEt9nC
-UiY4iKTWO0Cmws0/zZiTs1QUWJZV1VD+hq2kY39ch/aO5ieSZxeSAgMs3NZmdO3d
-Z//BYY1jTw+bbRcwJu+r0h8QoPnFfxZpgQNH7R5ojXKhTbImxrpsX23Wr9GxE46p
-rfNeaXUmGD5BKyF/7otdBwadQ8QpCiv8Kj6GyzyDOvnJDdrFmeK8eEEzduG/L13l
-pJhQDBXd4Pqcfzho0LKmeqfRMb1+ilgnQ7O6M5HTp5gVXJrm0w912fxBmJc+qiXb
-j5IusHsMX/FjqTf5m3VpTCgmJdrV8hJwRVXj33NeN/UhbJCONVrJ0yPr08C+eKxC
-KFhmpUZtcALXEPlLVPxdhkqHz3/KRawRWrUgUY0viEeXOcDPusBCAUCZSCELa6fS
-/ZbV0b5GnUngC6agIk440ME8MLxwjyx1zNDFjFE7PZQIZCZhfbnDZY8UnCHQqv0X
-cgOPvZuM5l5Tnrmd74K74bzickFbIZTTRTeU0d8JOV3nI6qaHcptqAqGhYqCvkIH
-1vI4gnPah1vlPNOePqc7nvQDs/nxfRN0Av+7oeX6AHkcpmZBiFxgV6YuCcS6/ZrP
-px9Aw7vMWgpVSzs4dlG4Y4uElBbmVvMCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB
-/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP6rAJCYniT8qcwaivsnuL8wbqg7
-MA0GCSqGSIb3DQEBCwUAA4ICAQDPdyxuVr5Os7aEAJSrR8kN0nbHhp8dB9O2tLsI
-eK9p0gtJ3jPFrK3CiAJ9Brc1AsFgyb/E6JTe1NOpEyVa/m6irn0F3H3zbPB+po3u
-2dfOWBfoqSmuc0iH55vKbimhZF8ZE/euBhD/UcabTVUlT5OZEAFTdfETzsemQUHS
-v4ilf0X8rLiltTMMgsT7B/Zq5SWEXwbKwYY5EdtYzXc7LMJMD16a4/CrPmEbUCTC
-wPTxGfARKbalGAKb12NMcIxHowNDXLldRqANb/9Zjr7dn3LDWyvfjFvO5QxGbJKy
-CqNMVEIYFRIYvdr8unRu/8G2oGTYqV9Vrp9canaW2HNnh/tNf1zuacpzEPuKqf2e
-vTY4SUmH9A4U8OmHuD+nT3pajnnUk+S7aFKErGzp85hwVXIy+TSrK0m1zSBi5Dp6
-Z2Orltxtrpfs/J92VoguZs9btsmksNcFuuEnL5O7Jiqik7Ab846+HUCjuTaPPoIa
-Gl6I6lD4WeKDRikL40Rc4ZW2aZCaFG+XroHPaO+Zmr615+F/+PoTRxZMzG0IQOeL
-eG9QgkRQP2YGiqtDhFZKDyAthg710tvSeopLzaXoTvFeJiUBWSOgftL2fiFX1ye8
-FVdMpEbB4IMeDExNH08GGeL5qPQ6gqGyeUN51q1veieQA6TqJIc/2b3Z6fJfUEkc
-7uzXLg==
------END CERTIFICATE-----
diff --git a/cobalt/csp/content_security_policy.cc b/cobalt/csp/content_security_policy.cc
index e5d502c..8d53488 100644
--- a/cobalt/csp/content_security_policy.cc
+++ b/cobalt/csp/content_security_policy.cc
@@ -136,6 +136,9 @@
const char ContentSecurityPolicy::kReflectedXSS[] = "reflected-xss";
const char ContentSecurityPolicy::kReferrer[] = "referrer";
+// CSP Level 3 Directives
+const char ContentSecurityPolicy::kWorkerSrc[] = "worker-src";
+
// Custom Cobalt directive to enforce navigation restrictions.
const char ContentSecurityPolicy::kLocationSrc[] = "h5vcc-location-src";
@@ -159,19 +162,20 @@
// clang-format off
bool ContentSecurityPolicy::IsDirectiveName(const std::string& name) {
std::string lower_name = base::ToLowerASCII(name);
- return (lower_name == kConnectSrc ||
+ return (
+ // CSP Level 1 Directives
+ lower_name == kConnectSrc ||
lower_name == kDefaultSrc ||
lower_name == kFontSrc ||
lower_name == kFrameSrc ||
lower_name == kImgSrc ||
- lower_name == kLocationSrc ||
lower_name == kMediaSrc ||
lower_name == kObjectSrc ||
lower_name == kReportURI ||
lower_name == kSandbox ||
- lower_name == kSuborigin ||
lower_name == kScriptSrc ||
lower_name == kStyleSrc ||
+ // CSP Level 2 Directives
lower_name == kBaseURI ||
lower_name == kChildSrc ||
lower_name == kFormAction ||
@@ -179,9 +183,15 @@
lower_name == kPluginTypes ||
lower_name == kReflectedXSS ||
lower_name == kReferrer ||
+ // CSP Level 3 Directives
lower_name == kManifestSrc ||
+ lower_name == kWorkerSrc ||
+ // Directives Defined in Other Documents.
lower_name == kBlockAllMixedContent ||
- lower_name == kUpgradeInsecureRequests);
+ lower_name == kUpgradeInsecureRequests ||
+ lower_name == kSuborigin ||
+ // Custom CSP directive for Cobalt
+ lower_name == kLocationSrc);
}
// clang-format on
@@ -434,18 +444,13 @@
<< " directive is not supported inside a <meta> element.";
}
-bool ContentSecurityPolicy::AllowJavaScriptURLs(const std::string& context_url,
- int context_line,
- ReportingStatus status) const {
- FOR_ALL_POLICIES_3(AllowJavaScriptURLs, context_url, context_line, status);
-}
-
bool ContentSecurityPolicy::AllowInlineEventHandlers(
const std::string& context_url, int context_line,
ReportingStatus status) const {
FOR_ALL_POLICIES_3(AllowInlineEventHandlers, context_url, context_line,
status);
}
+
bool ContentSecurityPolicy::AllowInlineScript(const std::string& context_url,
int context_line,
const std::string& script_content,
@@ -453,6 +458,15 @@
FOR_ALL_POLICIES_4(AllowInlineScript, context_url, context_line, status,
script_content);
}
+
+bool ContentSecurityPolicy::AllowInlineWorker(const std::string& context_url,
+ int context_line,
+ const std::string& script_content,
+ ReportingStatus status) const {
+ FOR_ALL_POLICIES_4(AllowInlineWorker, context_url, context_line, status,
+ script_content);
+}
+
bool ContentSecurityPolicy::AllowInlineStyle(const std::string& context_url,
int context_line,
const std::string& style_content,
@@ -466,29 +480,36 @@
}
bool ContentSecurityPolicy::AllowScriptFromSource(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
+ const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
FOR_ALL_POLICIES_3(AllowScriptFromSource, url, redirect_status,
reporting_status);
}
+bool ContentSecurityPolicy::AllowWorkerFromSource(
+ const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
+ FOR_ALL_POLICIES_3(AllowWorkerFromSource, url, redirect_status,
+ reporting_status);
+}
+
bool ContentSecurityPolicy::AllowObjectFromSource(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
+ const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
FOR_ALL_POLICIES_3(AllowObjectFromSource, url, redirect_status,
reporting_status);
}
bool ContentSecurityPolicy::AllowImageFromSource(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
+ const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
FOR_ALL_POLICIES_3(AllowImageFromSource, url, redirect_status,
reporting_status);
}
bool ContentSecurityPolicy::AllowNavigateToSource(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
+ const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
// Note that this is a Cobalt-specific policy to prevent navigation
// to any unexpected URLs.
FOR_ALL_POLICIES_3(AllowNavigateToSource, url, redirect_status,
@@ -496,48 +517,48 @@
}
bool ContentSecurityPolicy::AllowStyleFromSource(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
+ const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
FOR_ALL_POLICIES_3(AllowStyleFromSource, url, redirect_status,
reporting_status);
}
bool ContentSecurityPolicy::AllowFontFromSource(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
+ const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
FOR_ALL_POLICIES_3(AllowFontFromSource, url, redirect_status,
reporting_status);
}
bool ContentSecurityPolicy::AllowMediaFromSource(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
+ const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
FOR_ALL_POLICIES_3(AllowMediaFromSource, url, redirect_status,
reporting_status);
}
bool ContentSecurityPolicy::AllowConnectToSource(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
+ const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
FOR_ALL_POLICIES_3(AllowConnectToSource, url, redirect_status,
reporting_status);
}
bool ContentSecurityPolicy::AllowFormAction(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
+ const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
FOR_ALL_POLICIES_3(AllowFormAction, url, redirect_status, reporting_status);
}
bool ContentSecurityPolicy::AllowBaseURI(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
+ const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
FOR_ALL_POLICIES_3(AllowBaseURI, url, redirect_status, reporting_status);
}
bool ContentSecurityPolicy::AllowManifestFromSource(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
+ const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
FOR_ALL_POLICIES_3(AllowManifestFromSource, url, redirect_status,
reporting_status);
}
@@ -546,6 +567,11 @@
FOR_ALL_POLICIES_1(AllowScriptNonce, nonce);
}
+bool ContentSecurityPolicy::AllowWorkerWithNonce(
+ const std::string& nonce) const {
+ FOR_ALL_POLICIES_1(AllowWorkerNonce, nonce);
+}
+
bool ContentSecurityPolicy::AllowStyleWithNonce(
const std::string& nonce) const {
FOR_ALL_POLICIES_1(AllowStyleNonce, nonce);
@@ -557,6 +583,12 @@
source, script_hash_algorithms_used_, policies_);
}
+bool ContentSecurityPolicy::AllowWorkerWithHash(
+ const std::string& source) const {
+ return CheckDigest<&DirectiveList::AllowWorkerHash>(
+ source, script_hash_algorithms_used_, policies_);
+}
+
bool ContentSecurityPolicy::AllowStyleWithHash(
const std::string& source) const {
return CheckDigest<&DirectiveList::AllowStyleHash>(
diff --git a/cobalt/csp/content_security_policy.h b/cobalt/csp/content_security_policy.h
index d8622b7..a5cf380 100644
--- a/cobalt/csp/content_security_policy.h
+++ b/cobalt/csp/content_security_policy.h
@@ -21,6 +21,7 @@
#include "base/callback.h"
#include "base/containers/hash_tables.h"
+#include "cobalt/csp/directive_list.h"
#include "cobalt/csp/parsers.h"
#include "net/http/http_response_headers.h"
#include "url/gurl.h"
@@ -28,7 +29,6 @@
namespace cobalt {
namespace csp {
-class DirectiveList;
class Source;
// Wrap up information about a CSP violation, for passing to the Delegate.
@@ -79,6 +79,7 @@
typedef std::vector<std::unique_ptr<DirectiveList>> PolicyList;
// CSP Level 1 Directives
+ // https://www.w3.org/TR/2012/CR-CSP-20121115/
static const char kConnectSrc[];
static const char kDefaultSrc[];
static const char kFontSrc[];
@@ -92,6 +93,7 @@
static const char kStyleSrc[];
// CSP Level 2 Directives
+ // https://www.w3.org/TR/2016/REC-CSP2-20161215/
static const char kBaseURI[];
static const char kChildSrc[];
static const char kFormAction[];
@@ -100,35 +102,30 @@
static const char kReflectedXSS[];
static const char kReferrer[];
- // Custom CSP directive for Cobalt
- static const char kLocationSrc[];
+ // CSP Level 3 Directives
+ // https://www.w3.org/TR/2022/WD-CSP3-20221014/#directive-manifest-src
- // Manifest Directives (to be merged into CSP Level 2)
- // https://w3c.github.io/manifest/#content-security-policy
static const char kManifestSrc[];
+ // https://www.w3.org/TR/2022/WD-CSP3-20221014/#directive-worker-src
+ static const char kWorkerSrc[];
- // Mixed Content Directive
- // https://w3c.github.io/webappsec/specs/mixedcontent/#strict-mode
+ // Directives Defined in Other Documents.
+ // https://www.w3.org/TR/2022/WD-CSP3-20221014/#directives-elsewhere
+
+ // Mixed Content Directive. Note: Deprecated in the current spec.
+ // https://www.w3.org/TR/2021/CRD-mixed-content-20211004/#strict-checking
static const char kBlockAllMixedContent[];
- // https://w3c.github.io/webappsec/specs/upgrade/
+ // The upgrade-insecure-requests Directive.
+ // https://w3c.github.io/webappsec-upgrade-insecure-requests/#delivery
static const char kUpgradeInsecureRequests[];
// Suborigin Directive
- // https://metromoxie.github.io/webappsec/specs/suborigins/index.html
+ // https://metromoxie.github.io/webappsec/specs/suborigins/index.html#the-suborigin-directive
static const char kSuborigin[];
- enum ReportingStatus {
- kSendReport,
- kSuppressReport,
- };
-
- // When a resource is loaded after a redirect, source paths are
- // ignored in the matching algorithm.
- enum RedirectStatus {
- kDidRedirect,
- kDidNotRedirect,
- };
+ // Custom CSP directive h5vcc-location-src for Cobalt
+ static const char kLocationSrc[];
static bool IsDirectiveName(const std::string& name);
@@ -174,14 +171,15 @@
void ReportDirectiveNotSupportedInsideMeta(const std::string& name);
// https://www.w3.org/TR/2015/CR-CSP2-20150721/#directives
- bool AllowJavaScriptURLs(const std::string& context_url, int context_line,
- ReportingStatus status = kSendReport) const;
bool AllowInlineEventHandlers(const std::string& context_url,
int context_line,
ReportingStatus status = kSendReport) const;
bool AllowInlineScript(const std::string& context_url, int context_line,
const std::string& script_content,
ReportingStatus status = kSendReport) const;
+ bool AllowInlineWorker(const std::string& context_url, int context_line,
+ const std::string& script_content,
+ ReportingStatus status = kSendReport) const;
bool AllowInlineStyle(const std::string& context_url, int context_line,
const std::string& style_content,
ReportingStatus status = kSendReport) const;
@@ -189,6 +187,9 @@
bool AllowScriptFromSource(const GURL& url,
RedirectStatus redirect = kDidNotRedirect,
ReportingStatus report = kSendReport) const;
+ bool AllowWorkerFromSource(const GURL& url,
+ RedirectStatus redirect = kDidNotRedirect,
+ ReportingStatus report = kSendReport) const;
bool AllowObjectFromSource(const GURL& url,
RedirectStatus redirect = kDidNotRedirect,
ReportingStatus report = kSendReport) const;
@@ -227,8 +228,10 @@
// If these return true, callers can then process the content or
// issue a load and be safe disabling any further CSP checks.
bool AllowScriptWithNonce(const std::string& nonce) const;
+ bool AllowWorkerWithNonce(const std::string& nonce) const;
bool AllowStyleWithNonce(const std::string& nonce) const;
bool AllowScriptWithHash(const std::string& source) const;
+ bool AllowWorkerWithHash(const std::string& source) const;
bool AllowStyleWithHash(const std::string& source) const;
void set_uses_script_hash_algorithms(uint8 algorithm) {
@@ -243,6 +246,16 @@
void NotifyUrlChanged(const GURL& url);
bool DidSetReferrerPolicy() const;
+ const PolicyList& policies() const { return policies_; }
+ void append_policy(const DirectiveList& directive_list) {
+ policies_.emplace_back(new DirectiveList(this, directive_list));
+ }
+
+ const ReferrerPolicy& referrer_policy() const { return referrer_policy_; }
+ void set_referrer_policy(const ReferrerPolicy& referrer_policy) {
+ referrer_policy_ = referrer_policy;
+ }
+
const GURL& url() const { return url_; }
void set_enforce_strict_mixed_content_checking() {
enforce_strict_mixed_content_checking_ = true;
@@ -258,6 +271,8 @@
void AddPolicyFromHeaderValue(const std::string& header, HeaderType type,
HeaderSource source);
+ // List of CSP Policies.
+ // https://www.w3.org/TR/2022/WD-CSP3-20221014/#csp-list
PolicyList policies_;
std::unique_ptr<Source> self_source_;
std::string self_scheme_;
diff --git a/cobalt/csp/content_security_policy_test.cc b/cobalt/csp/content_security_policy_test.cc
index b70f14c..8ef00fd 100644
--- a/cobalt/csp/content_security_policy_test.cc
+++ b/cobalt/csp/content_security_policy_test.cc
@@ -29,18 +29,54 @@
std::unique_ptr<ContentSecurityPolicy> csp_;
};
+TEST_F(CspTest, UrlMatchesSelf) {
+ // Test whether the URLs match the source expression.
+ // https://www.w3.org/TR/2015/CR-CSP2-20150721/#match-source-expression
+ // Tested in more detail in SourceTest.
+ csp_.reset(new ContentSecurityPolicy(GURL("https://www.example.com/foo/bar"),
+ callback_));
+ EXPECT_TRUE(csp_->UrlMatchesSelf(GURL("https://www.example.com")));
+ EXPECT_TRUE(csp_->UrlMatchesSelf(GURL("HTTPS://www.example.com")));
+ EXPECT_TRUE(csp_->UrlMatchesSelf(GURL("https://www.example.com/foo/bar")));
+ EXPECT_TRUE(csp_->UrlMatchesSelf(GURL("https://www.example.com/bar/foo")));
+ EXPECT_FALSE(csp_->UrlMatchesSelf(GURL("http://www.example.com/foo/bar")));
+ EXPECT_FALSE(csp_->UrlMatchesSelf(GURL("https://www.example.com:8000")));
+ EXPECT_FALSE(csp_->UrlMatchesSelf(GURL("https://example.com")));
+}
+
TEST_F(CspTest, SecureSchemeMatchesSelf) {
csp_.reset(
new ContentSecurityPolicy(GURL("https://www.example.com"), callback_));
EXPECT_TRUE(csp_->SchemeMatchesSelf(GURL("https://example.com")));
+ EXPECT_TRUE(csp_->SchemeMatchesSelf(GURL("HTTPS://example.com")));
EXPECT_FALSE(csp_->SchemeMatchesSelf(GURL("http://example.com")));
+
+ csp_.reset(
+ new ContentSecurityPolicy(GURL("HTTPS://www.example.com"), callback_));
+ EXPECT_TRUE(csp_->SchemeMatchesSelf(GURL("https://example.com")));
+ EXPECT_FALSE(csp_->SchemeMatchesSelf(GURL("http://example.com")));
+}
+
+TEST_F(CspTest, FileSchemeDoesNotMatchSelf) {
+ csp_.reset(
+ new ContentSecurityPolicy(GURL("https://www.example.com"), callback_));
+ EXPECT_FALSE(csp_->SchemeMatchesSelf(GURL("file://example.com")));
}
TEST_F(CspTest, InsecureSchemeMatchesSelf) {
csp_.reset(
new ContentSecurityPolicy(GURL("http://www.example.com"), callback_));
EXPECT_TRUE(csp_->SchemeMatchesSelf(GURL("https://example.com")));
+ EXPECT_TRUE(csp_->SchemeMatchesSelf(GURL("HTTPS://example.com")));
EXPECT_TRUE(csp_->SchemeMatchesSelf(GURL("http://example.com")));
+ EXPECT_TRUE(csp_->SchemeMatchesSelf(GURL("HTTP://example.com")));
+
+ csp_.reset(
+ new ContentSecurityPolicy(GURL("HTTP://www.example.com"), callback_));
+ EXPECT_TRUE(csp_->SchemeMatchesSelf(GURL("https://example.com")));
+ EXPECT_TRUE(csp_->SchemeMatchesSelf(GURL("HTTPS://example.com")));
+ EXPECT_TRUE(csp_->SchemeMatchesSelf(GURL("http://example.com")));
+ EXPECT_TRUE(csp_->SchemeMatchesSelf(GURL("HTTP://example.com")));
}
} // namespace
diff --git a/cobalt/csp/directive.cc b/cobalt/csp/directive.cc
index f386b5d..dd14639 100644
--- a/cobalt/csp/directive.cc
+++ b/cobalt/csp/directive.cc
@@ -23,5 +23,8 @@
ContentSecurityPolicy* policy)
: text_(name + " " + value), policy_(policy) {}
+Directive::Directive(ContentSecurityPolicy* policy, const Directive& other)
+ : text_(other.text_), policy_(policy) {}
+
} // namespace csp
} // namespace cobalt
diff --git a/cobalt/csp/directive.h b/cobalt/csp/directive.h
index 36fa60e..164948b 100644
--- a/cobalt/csp/directive.h
+++ b/cobalt/csp/directive.h
@@ -24,10 +24,23 @@
class ContentSecurityPolicy;
+enum ReportingStatus {
+ kSendReport,
+ kSuppressReport,
+};
+
+// When a resource is loaded after a redirect, source paths are
+// ignored in the matching algorithm.
+enum RedirectStatus {
+ kDidRedirect,
+ kDidNotRedirect,
+};
+
class Directive {
public:
Directive(const std::string& name, const std::string& value,
ContentSecurityPolicy* policy);
+ Directive(ContentSecurityPolicy* policy, const Directive& other);
const std::string& text() const { return text_; }
diff --git a/cobalt/csp/directive_list.cc b/cobalt/csp/directive_list.cc
index d62749a..77fd7a5 100644
--- a/cobalt/csp/directive_list.cc
+++ b/cobalt/csp/directive_list.cc
@@ -88,6 +88,42 @@
}
}
+DirectiveList::DirectiveList(ContentSecurityPolicy* policy,
+ const DirectiveList& other)
+ : policy_(policy),
+ header_(other.header_),
+ header_type_(other.header_type_),
+ header_source_(other.header_source_),
+ report_only_(other.report_only_),
+ has_sandbox_policy_(other.has_sandbox_policy_),
+ has_suborigin_policy_(other.has_suborigin_policy_),
+ reflected_xss_disposition_(other.reflected_xss_disposition_),
+ did_set_referrer_policy_(other.did_set_referrer_policy_),
+ referrer_policy_(other.referrer_policy_),
+ strict_mixed_content_checking_enforced_(
+ other.strict_mixed_content_checking_enforced_),
+ upgrade_insecure_requests_(other.upgrade_insecure_requests_),
+ plugin_types_(new MediaListDirective(policy, *(other.plugin_types_))),
+ base_uri_(new SourceListDirective(policy, *(other.base_uri_))),
+ child_src_(new SourceListDirective(policy, *(other.child_src_))),
+ connect_src_(new SourceListDirective(policy, *(other.connect_src_))),
+ default_src_(new SourceListDirective(policy, *(other.default_src_))),
+ font_src_(new SourceListDirective(policy, *(other.font_src_))),
+ form_action_(new SourceListDirective(policy, *(other.form_action_))),
+ frame_ancestors_(
+ new SourceListDirective(policy, *(other.frame_ancestors_))),
+ frame_src_(new SourceListDirective(policy, *(other.frame_src_))),
+ img_src_(new SourceListDirective(policy, *(other.img_src_))),
+ location_src_(new SourceListDirective(policy, *(other.location_src_))),
+ media_src_(new SourceListDirective(policy, *(other.media_src_))),
+ manifest_src_(new SourceListDirective(policy, *(other.manifest_src_))),
+ object_src_(new SourceListDirective(policy, *(other.object_src_))),
+ script_src_(new SourceListDirective(policy, *(other.script_src_))),
+ style_src_(new SourceListDirective(policy, *(other.style_src_))),
+ worker_src_(new SourceListDirective(policy, *(other.worker_src_))),
+ report_endpoints_(other.report_endpoints_),
+ eval_disabled_error_message_(other.eval_disabled_error_message_) {}
+
DirectiveList::~DirectiveList() {}
void DirectiveList::ReportViolation(const std::string& directive_text,
@@ -130,9 +166,8 @@
return !directive || directive->AllowHash(hashValue);
}
-bool DirectiveList::CheckSource(
- SourceListDirective* directive, const GURL& url,
- ContentSecurityPolicy::RedirectStatus redirect_status) const {
+bool DirectiveList::CheckSource(SourceListDirective* directive, const GURL& url,
+ RedirectStatus redirect_status) const {
return !directive || directive->Allows(url, redirect_status);
}
@@ -210,8 +245,8 @@
bool DirectiveList::CheckInlineAndReportViolation(
SourceListDirective* directive, const std::string& console_message,
- const std::string& context_url, int context_line, bool is_script,
- const std::string& hash_value) const {
+ const std::string& context_url, int context_line,
+ const char* directive_name, const std::string& hash_value) const {
if (CheckInline(directive)) {
return true;
}
@@ -240,20 +275,18 @@
" Either the 'unsafe-inline' keyword, a hash (" + hash_value +
"), or a nonce ('nonce-...') is required to enable inline execution.";
if (directive == default_src_.get())
- suffix = suffix + " Note also that '" +
- std::string(is_script ? "script" : "style") +
- "-src' was not explicitly set, so 'default-src' is used as a "
+ suffix = suffix + " Note also that '" + std::string(directive_name) +
+ "' was not explicitly set, so 'default-src' is used as a "
"fallback.";
}
ReportViolationWithLocation(
- directive->text(), is_script ? ContentSecurityPolicy::kScriptSrc
- : ContentSecurityPolicy::kStyleSrc,
+ directive->text(), directive_name,
console_message + "\"" + directive->text() + "\"." + suffix + "\n",
GURL(), context_url, context_line);
if (!report_only_) {
- if (is_script) {
+ if (directive_name == ContentSecurityPolicy::kScriptSrc) {
// policy_->ReportBlockedScriptExecutionToInspector(directive->text());
}
return false;
@@ -264,7 +297,7 @@
bool DirectiveList::CheckSourceAndReportViolation(
SourceListDirective* directive, const GURL& url,
const std::string& effective_directive,
- ContentSecurityPolicy::RedirectStatus redirect_status) const {
+ RedirectStatus redirect_status) const {
if (CheckSource(directive, url, redirect_status)) {
return true;
}
@@ -296,6 +329,8 @@
prefix = "Refused to load the script '";
} else if (ContentSecurityPolicy::kStyleSrc == effective_directive) {
prefix = "Refused to load the stylesheet '";
+ } else if (ContentSecurityPolicy::kWorkerSrc == effective_directive) {
+ prefix = "Refused to load the worker '";
}
std::string suffix = std::string();
@@ -313,63 +348,69 @@
return deny_if_enforcing_policy();
}
-bool DirectiveList::AllowJavaScriptURLs(
- const std::string& context_url, int context_line,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
- if (reporting_status == ContentSecurityPolicy::kSendReport) {
- return CheckInlineAndReportViolation(
- OperativeDirective(script_src_.get()),
- "Refused to execute JavaScript URL because it violates the following "
- "Content Security Policy directive: ",
- context_url, context_line, true, "sha256-...");
- }
- return CheckInline(OperativeDirective(script_src_.get()));
-}
-
bool DirectiveList::AllowInlineEventHandlers(
const std::string& context_url, int context_line,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
- if (reporting_status == ContentSecurityPolicy::kSendReport) {
+ ReportingStatus reporting_status) const {
+ if (reporting_status == kSendReport) {
return CheckInlineAndReportViolation(
OperativeDirective(script_src_.get()),
"Refused to execute inline event handler because it violates the "
"following Content Security Policy directive: ",
- context_url, context_line, true, "sha256-...");
+ context_url, context_line, ContentSecurityPolicy::kScriptSrc,
+ "sha256-...");
}
return CheckInline(OperativeDirective(script_src_.get()));
}
-bool DirectiveList::AllowInlineScript(
- const std::string& context_url, int context_line,
- ContentSecurityPolicy::ReportingStatus reporting_status,
- const std::string& content) const {
- if (reporting_status == ContentSecurityPolicy::kSendReport) {
+bool DirectiveList::AllowInlineScript(const std::string& context_url,
+ int context_line,
+ ReportingStatus reporting_status,
+ const std::string& content) const {
+ if (reporting_status == kSendReport) {
return CheckInlineAndReportViolation(
OperativeDirective(script_src_.get()),
"Refused to execute inline script because it violates the following "
"Content Security Policy directive: ",
- context_url, context_line, true, GetSha256String(content));
+ context_url, context_line, ContentSecurityPolicy::kScriptSrc,
+ GetSha256String(content));
}
return CheckInline(OperativeDirective(script_src_.get()));
}
-bool DirectiveList::AllowInlineStyle(
- const std::string& context_url, int context_line,
- ContentSecurityPolicy::ReportingStatus reporting_status,
- const std::string& content) const {
- if (reporting_status == ContentSecurityPolicy::kSendReport) {
+bool DirectiveList::AllowInlineWorker(const std::string& context_url,
+ int context_line,
+ ReportingStatus reporting_status,
+ const std::string& content) const {
+ if (reporting_status == kSendReport) {
+ return CheckInlineAndReportViolation(
+ OperativeDirective(worker_src_.get(),
+ OperativeDirective(script_src_.get())),
+ "Refused to execute inline script because it violates the following "
+ "Content Security Policy directive: ",
+ context_url, context_line, ContentSecurityPolicy::kWorkerSrc,
+ GetSha256String(content));
+ }
+ return CheckInline(OperativeDirective(worker_src_.get(),
+ OperativeDirective(script_src_.get())));
+}
+
+bool DirectiveList::AllowInlineStyle(const std::string& context_url,
+ int context_line,
+ ReportingStatus reporting_status,
+ const std::string& content) const {
+ if (reporting_status == kSendReport) {
return CheckInlineAndReportViolation(
OperativeDirective(style_src_.get()),
"Refused to apply inline style because it violates the following "
"Content Security Policy directive: ",
- context_url, context_line, false, GetSha256String(content));
+ context_url, context_line, ContentSecurityPolicy::kStyleSrc,
+ GetSha256String(content));
}
return CheckInline(OperativeDirective(style_src_.get()));
}
-bool DirectiveList::AllowEval(
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
- if (reporting_status == ContentSecurityPolicy::kSendReport) {
+bool DirectiveList::AllowEval(ReportingStatus reporting_status) const {
+ if (reporting_status == kSendReport) {
return CheckEvalAndReportViolation(
OperativeDirective(script_src_.get()),
"Refused to evaluate a string as JavaScript because 'unsafe-eval' is "
@@ -380,9 +421,9 @@
}
bool DirectiveList::AllowScriptFromSource(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
- return reporting_status == ContentSecurityPolicy::kSendReport
+ const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
+ return reporting_status == kSendReport
? CheckSourceAndReportViolation(
OperativeDirective(script_src_.get()), url,
ContentSecurityPolicy::kScriptSrc, redirect_status)
@@ -390,10 +431,24 @@
redirect_status);
}
+bool DirectiveList::AllowWorkerFromSource(
+ const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
+ return reporting_status == kSendReport
+ ? CheckSourceAndReportViolation(
+ OperativeDirective(worker_src_.get(),
+ OperativeDirective(script_src_.get())),
+ url, ContentSecurityPolicy::kWorkerSrc, redirect_status)
+ : CheckSource(
+ OperativeDirective(worker_src_.get(),
+ OperativeDirective(script_src_.get())),
+ url, redirect_status);
+}
+
bool DirectiveList::AllowObjectFromSource(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
- return reporting_status == ContentSecurityPolicy::kSendReport
+ const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
+ return reporting_status == kSendReport
? CheckSourceAndReportViolation(
OperativeDirective(object_src_.get()), url,
ContentSecurityPolicy::kObjectSrc, redirect_status)
@@ -402,9 +457,9 @@
}
bool DirectiveList::AllowImageFromSource(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
- return reporting_status == ContentSecurityPolicy::kSendReport
+ const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
+ return reporting_status == kSendReport
? CheckSourceAndReportViolation(
OperativeDirective(img_src_.get()), url,
ContentSecurityPolicy::kImgSrc, redirect_status)
@@ -413,9 +468,9 @@
}
bool DirectiveList::AllowStyleFromSource(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
- return reporting_status == ContentSecurityPolicy::kSendReport
+ const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
+ return reporting_status == kSendReport
? CheckSourceAndReportViolation(
OperativeDirective(style_src_.get()), url,
ContentSecurityPolicy::kStyleSrc, redirect_status)
@@ -424,9 +479,9 @@
}
bool DirectiveList::AllowFontFromSource(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
- return reporting_status == ContentSecurityPolicy::kSendReport
+ const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
+ return reporting_status == kSendReport
? CheckSourceAndReportViolation(
OperativeDirective(font_src_.get()), url,
ContentSecurityPolicy::kFontSrc, redirect_status)
@@ -435,9 +490,9 @@
}
bool DirectiveList::AllowMediaFromSource(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
- return reporting_status == ContentSecurityPolicy::kSendReport
+ const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
+ return reporting_status == kSendReport
? CheckSourceAndReportViolation(
OperativeDirective(media_src_.get()), url,
ContentSecurityPolicy::kMediaSrc, redirect_status)
@@ -446,9 +501,9 @@
}
bool DirectiveList::AllowManifestFromSource(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
- return reporting_status == ContentSecurityPolicy::kSendReport
+ const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
+ return reporting_status == kSendReport
? CheckSourceAndReportViolation(
OperativeDirective(manifest_src_.get()), url,
ContentSecurityPolicy::kManifestSrc, redirect_status)
@@ -457,9 +512,9 @@
}
bool DirectiveList::AllowConnectToSource(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
- return reporting_status == ContentSecurityPolicy::kSendReport
+ const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
+ return reporting_status == kSendReport
? CheckSourceAndReportViolation(
OperativeDirective(connect_src_.get()), url,
ContentSecurityPolicy::kConnectSrc, redirect_status)
@@ -468,31 +523,31 @@
}
bool DirectiveList::AllowNavigateToSource(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
+ const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
// No fallback to default for h5vcc-location-src policy, so we don't use
// OperativeDirective() in this case.
- return reporting_status == ContentSecurityPolicy::kSendReport
+ return reporting_status == kSendReport
? CheckSourceAndReportViolation(
location_src_.get(), url,
ContentSecurityPolicy::kLocationSrc, redirect_status)
: CheckSource(location_src_.get(), url, redirect_status);
}
-bool DirectiveList::AllowFormAction(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
- return reporting_status == ContentSecurityPolicy::kSendReport
+bool DirectiveList::AllowFormAction(const GURL& url,
+ RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
+ return reporting_status == kSendReport
? CheckSourceAndReportViolation(form_action_.get(), url,
ContentSecurityPolicy::kFormAction,
redirect_status)
: CheckSource(form_action_.get(), url, redirect_status);
}
-bool DirectiveList::AllowBaseURI(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const {
- return reporting_status == ContentSecurityPolicy::kSendReport
+bool DirectiveList::AllowBaseURI(const GURL& url,
+ RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const {
+ return reporting_status == kSendReport
? CheckSourceAndReportViolation(base_uri_.get(), url,
ContentSecurityPolicy::kBaseURI,
redirect_status)
@@ -503,6 +558,12 @@
return CheckNonce(OperativeDirective(script_src_.get()), nonce);
}
+bool DirectiveList::AllowWorkerNonce(const std::string& nonce) const {
+ return CheckNonce(OperativeDirective(worker_src_.get(),
+ OperativeDirective(script_src_.get())),
+ nonce);
+}
+
bool DirectiveList::AllowStyleNonce(const std::string& nonce) const {
return CheckNonce(OperativeDirective(style_src_.get()), nonce);
}
@@ -511,6 +572,12 @@
return CheckHash(OperativeDirective(script_src_.get()), hash_value);
}
+bool DirectiveList::AllowWorkerHash(const HashValue& hash_value) const {
+ return CheckHash(OperativeDirective(worker_src_.get(),
+ OperativeDirective(script_src_.get())),
+ hash_value);
+}
+
bool DirectiveList::AllowStyleHash(const HashValue& hash_value) const {
return CheckHash(OperativeDirective(style_src_.get()), hash_value);
}
@@ -612,7 +679,7 @@
if (header_source_ == kHeaderSourceMeta) {
// The report-uri, frame-ancestors, and sandbox directives are not supported
// inside a meta element.
- // https://w3c.github.io/webappsec-csp/#meta-element
+ // https://www.w3.org/TR/2022/WD-CSP3-20221014/#meta-element
policy_->ReportDirectiveNotSupportedInsideMeta(name);
return;
}
@@ -844,6 +911,10 @@
SetCSPDirective(name, value, &script_src_);
policy_->set_uses_script_hash_algorithms(
script_src_->hash_algorithms_used());
+ } else if (lower_name == ContentSecurityPolicy::kWorkerSrc) {
+ SetCSPDirective(name, value, &worker_src_);
+ policy_->set_uses_script_hash_algorithms(
+ worker_src_->hash_algorithms_used());
} else if (lower_name == ContentSecurityPolicy::kObjectSrc) {
SetCSPDirective(name, value, &object_src_);
} else if (lower_name == ContentSecurityPolicy::kFrameAncestors) {
diff --git a/cobalt/csp/directive_list.h b/cobalt/csp/directive_list.h
index 46597b3..38765c2 100644
--- a/cobalt/csp/directive_list.h
+++ b/cobalt/csp/directive_list.h
@@ -20,12 +20,14 @@
#include <vector>
#include "base/basictypes.h"
-#include "cobalt/csp/content_security_policy.h"
+#include "cobalt/csp/directive.h"
+#include "cobalt/csp/parsers.h"
#include "url/gurl.h"
namespace cobalt {
namespace csp {
+class ContentSecurityPolicy;
class MediaListDirective;
class SourceListDirective;
@@ -33,6 +35,7 @@
public:
DirectiveList(ContentSecurityPolicy* policy, const base::StringPiece& text,
HeaderType header_type, HeaderSource header_source);
+ DirectiveList(ContentSecurityPolicy* policy, const DirectiveList& other);
~DirectiveList();
void Parse(const base::StringPiece& text);
@@ -40,62 +43,53 @@
HeaderType header_type() const { return header_type_; }
HeaderSource header_source() const { return header_source_; }
- bool AllowJavaScriptURLs(
- const std::string& context_url, int context_line,
- ContentSecurityPolicy::ReportingStatus reporting_status) const;
- bool AllowInlineEventHandlers(
- const std::string& context_url, int context_line,
- ContentSecurityPolicy::ReportingStatus reporting_status) const;
- bool AllowInlineScript(
- const std::string& context_url, int context_line,
- ContentSecurityPolicy::ReportingStatus reporting_status,
- const std::string& script_content) const;
+ bool AllowInlineEventHandlers(const std::string& context_url,
+ int context_line,
+ ReportingStatus reporting_status) const;
+ bool AllowInlineScript(const std::string& context_url, int context_line,
+ ReportingStatus reporting_status,
+ const std::string& script_content) const;
+ bool AllowInlineWorker(const std::string& context_url, int context_line,
+ ReportingStatus reporting_status,
+ const std::string& worker_content) const;
bool AllowInlineStyle(const std::string& context_url, int context_line,
- ContentSecurityPolicy::ReportingStatus reporting_status,
+ ReportingStatus reporting_status,
const std::string& style_content) const;
- bool AllowEval(ContentSecurityPolicy::ReportingStatus reporting_status) const;
- bool AllowPluginType(
- const std::string& type, const std::string& type_attribute,
- const GURL& url,
- ContentSecurityPolicy::ReportingStatus reporting_status) const;
+ bool AllowEval(ReportingStatus reporting_status) const;
+ bool AllowPluginType(const std::string& type,
+ const std::string& type_attribute, const GURL& url,
+ ReportingStatus reporting_status) const;
- bool AllowScriptFromSource(
- const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const;
- bool AllowObjectFromSource(
- const GURL&, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const;
- bool AllowImageFromSource(
- const GURL&, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const;
- bool AllowStyleFromSource(
- const GURL&, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const;
- bool AllowFontFromSource(
- const GURL&, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const;
- bool AllowMediaFromSource(
- const GURL&, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const;
- bool AllowManifestFromSource(
- const GURL&, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const;
- bool AllowConnectToSource(
- const GURL&, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const;
- bool AllowNavigateToSource(
- const GURL&, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const;
- bool AllowFormAction(
- const GURL&, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const;
- bool AllowBaseURI(
- const GURL&, ContentSecurityPolicy::RedirectStatus redirect_status,
- ContentSecurityPolicy::ReportingStatus reporting_status) const;
+ bool AllowScriptFromSource(const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const;
+ bool AllowWorkerFromSource(const GURL& url, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const;
+ bool AllowObjectFromSource(const GURL&, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const;
+ bool AllowImageFromSource(const GURL&, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const;
+ bool AllowStyleFromSource(const GURL&, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const;
+ bool AllowFontFromSource(const GURL&, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const;
+ bool AllowMediaFromSource(const GURL&, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const;
+ bool AllowManifestFromSource(const GURL&, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const;
+ bool AllowConnectToSource(const GURL&, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const;
+ bool AllowNavigateToSource(const GURL&, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const;
+ bool AllowFormAction(const GURL&, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const;
+ bool AllowBaseURI(const GURL&, RedirectStatus redirect_status,
+ ReportingStatus reporting_status) const;
bool AllowScriptNonce(const std::string& script) const;
+ bool AllowWorkerNonce(const std::string& worker) const;
bool AllowStyleNonce(const std::string& style) const;
bool AllowScriptHash(const HashValue& script) const;
- bool AllowStyleHash(const HashValue& script) const;
+ bool AllowWorkerHash(const HashValue& worker) const;
+ bool AllowStyleHash(const HashValue& style) const;
const std::string& eval_disabled_error_message() const {
return eval_disabled_error_message_;
@@ -160,7 +154,7 @@
bool CheckHash(SourceListDirective* directive,
const HashValue& hash_value) const;
bool CheckSource(SourceListDirective* directive, const GURL& url,
- ContentSecurityPolicy::RedirectStatus redirect_status) const;
+ RedirectStatus redirect_status) const;
bool CheckMediaType(MediaListDirective* directive, const std::string& type,
const std::string& type_attribute) const;
@@ -173,13 +167,14 @@
bool CheckInlineAndReportViolation(SourceListDirective* directive,
const std::string& console_message,
const std::string& context_url,
- int context_line, bool is_script,
+ int context_line,
+ const char* directive_name,
const std::string& hash_value) const;
- bool CheckSourceAndReportViolation(
- SourceListDirective* directive, const GURL& url,
- const std::string& effective_directive,
- ContentSecurityPolicy::RedirectStatus redirect_status) const;
+ bool CheckSourceAndReportViolation(SourceListDirective* directive,
+ const GURL& url,
+ const std::string& effective_directive,
+ RedirectStatus redirect_status) const;
bool CheckMediaTypeAndReportViolation(
MediaListDirective* directive, const std::string& type,
const std::string& type_attribute,
@@ -221,6 +216,7 @@
std::unique_ptr<SourceListDirective> object_src_;
std::unique_ptr<SourceListDirective> script_src_;
std::unique_ptr<SourceListDirective> style_src_;
+ std::unique_ptr<SourceListDirective> worker_src_;
std::vector<std::string> report_endpoints_;
diff --git a/cobalt/csp/media_list_directive.cc b/cobalt/csp/media_list_directive.cc
index cf7a5b0..c7cee75 100644
--- a/cobalt/csp/media_list_directive.cc
+++ b/cobalt/csp/media_list_directive.cc
@@ -23,6 +23,10 @@
Parse(base::StringPiece(value));
}
+MediaListDirective::MediaListDirective(ContentSecurityPolicy* policy,
+ const MediaListDirective& other)
+ : Directive(policy, other), plugin_types_(other.plugin_types_) {}
+
bool MediaListDirective::Allows(const std::string& type) const {
return plugin_types_.find(type) != plugin_types_.end();
}
diff --git a/cobalt/csp/media_list_directive.h b/cobalt/csp/media_list_directive.h
index 1934cc9..5aa0e60 100644
--- a/cobalt/csp/media_list_directive.h
+++ b/cobalt/csp/media_list_directive.h
@@ -28,6 +28,8 @@
public:
MediaListDirective(const std::string& name, const std::string& value,
ContentSecurityPolicy* policy);
+ MediaListDirective(ContentSecurityPolicy* policy,
+ const MediaListDirective& other);
bool Allows(const std::string& type) const;
diff --git a/cobalt/csp/source.cc b/cobalt/csp/source.cc
index 3e5bb5b..932c824 100644
--- a/cobalt/csp/source.cc
+++ b/cobalt/csp/source.cc
@@ -33,10 +33,11 @@
config_.port_wildcard = config.port_wildcard;
}
+Source::Source(ContentSecurityPolicy* policy, const Source& other)
+ : policy_(policy), config_(other.config_) {}
+
// https://www.w3.org/TR/2015/CR-CSP2-20150721/#match-source-expression
-bool Source::Matches(
- const GURL& url,
- ContentSecurityPolicy::RedirectStatus redirect_status) const {
+bool Source::Matches(const GURL& url, RedirectStatus redirect_status) const {
if (!SchemeMatches(url)) {
return false;
}
@@ -50,8 +51,7 @@
return false;
}
- bool paths_match = (redirect_status == ContentSecurityPolicy::kDidRedirect) ||
- PathMatches(url);
+ bool paths_match = (redirect_status == kDidRedirect) || PathMatches(url);
return paths_match;
}
diff --git a/cobalt/csp/source.h b/cobalt/csp/source.h
index 1420953..40d0ae6 100644
--- a/cobalt/csp/source.h
+++ b/cobalt/csp/source.h
@@ -46,9 +46,9 @@
class Source {
public:
Source(ContentSecurityPolicy* policy, const SourceConfig& config);
+ Source(ContentSecurityPolicy* policy, const Source& other);
bool Matches(const GURL& url,
- ContentSecurityPolicy::RedirectStatus redirect_status =
- ContentSecurityPolicy::kDidNotRedirect) const;
+ RedirectStatus redirect_status = kDidNotRedirect) const;
private:
bool SchemeMatches(const GURL& url) const;
diff --git a/cobalt/csp/source_list.cc b/cobalt/csp/source_list.cc
index 17345f4..902ba39 100644
--- a/cobalt/csp/source_list.cc
+++ b/cobalt/csp/source_list.cc
@@ -84,9 +84,29 @@
hash_algorithms_used_(0),
local_network_checker_(checker) {}
-bool SourceList::Matches(
- const GURL& url,
- ContentSecurityPolicy::RedirectStatus redirect_status) const {
+SourceList::SourceList(const LocalNetworkCheckerInterface* checker,
+ ContentSecurityPolicy* policy, const SourceList& other)
+ : policy_(policy),
+ directive_name_(other.directive_name_),
+ allow_self_(other.allow_self_),
+ allow_star_(other.allow_star_),
+ allow_inline_(other.allow_inline_),
+ allow_eval_(other.allow_eval_),
+ allow_insecure_connections_to_local_network_(
+ other.allow_insecure_connections_to_local_network_),
+ allow_insecure_connections_to_localhost_(
+ other.allow_insecure_connections_to_localhost_),
+ allow_insecure_connections_to_private_range_(
+ other.allow_insecure_connections_to_private_range_),
+ hash_algorithms_used_(0),
+ local_network_checker_(checker) {
+ for (Source source : other.list_) {
+ list_.push_back(Source(policy, source));
+ }
+}
+
+bool SourceList::Matches(const GURL& url,
+ RedirectStatus redirect_status) const {
if (allow_star_ && SchemeCanMatchStar(url)) {
return true;
}
diff --git a/cobalt/csp/source_list.h b/cobalt/csp/source_list.h
index 7e67bfa..b9fffa7 100644
--- a/cobalt/csp/source_list.h
+++ b/cobalt/csp/source_list.h
@@ -43,11 +43,11 @@
SourceList(const LocalNetworkCheckerInterface* checker,
ContentSecurityPolicy* policy, const std::string& directive_name);
+ SourceList(const LocalNetworkCheckerInterface* checker,
+ ContentSecurityPolicy* policy, const SourceList& other);
void Parse(const base::StringPiece& begin);
- bool Matches(const GURL& url,
- ContentSecurityPolicy::RedirectStatus =
- ContentSecurityPolicy::kDidNotRedirect) const;
+ bool Matches(const GURL& url, RedirectStatus = kDidNotRedirect) const;
bool AllowInline() const;
bool AllowEval() const;
bool AllowNonce(const std::string& nonce) const;
diff --git a/cobalt/csp/source_list_directive.cc b/cobalt/csp/source_list_directive.cc
index fe4ed2c..4981852 100644
--- a/cobalt/csp/source_list_directive.cc
+++ b/cobalt/csp/source_list_directive.cc
@@ -35,9 +35,13 @@
source_list_.Parse(base::StringPiece(value));
}
-bool SourceListDirective::Allows(
- const GURL& url,
- ContentSecurityPolicy::RedirectStatus redirectStatus) const {
+SourceListDirective::SourceListDirective(ContentSecurityPolicy* policy,
+ const SourceListDirective& other)
+ : Directive(policy, other),
+ source_list_(&local_network_checker_, policy, other.source_list_) {}
+
+bool SourceListDirective::Allows(const GURL& url,
+ RedirectStatus redirectStatus) const {
return source_list_.Matches(url.is_empty() ? policy()->url() : url,
redirectStatus);
}
diff --git a/cobalt/csp/source_list_directive.h b/cobalt/csp/source_list_directive.h
index 7a68f4c..0d17ff2 100644
--- a/cobalt/csp/source_list_directive.h
+++ b/cobalt/csp/source_list_directive.h
@@ -34,9 +34,10 @@
SourceListDirective(const std::string& name, const std::string& value,
ContentSecurityPolicy* policy);
+ SourceListDirective(ContentSecurityPolicy* policy,
+ const SourceListDirective& other);
- bool Allows(const GURL& url,
- ContentSecurityPolicy::RedirectStatus redirect_status) const;
+ bool Allows(const GURL& url, RedirectStatus redirect_status) const;
bool AllowInline() const;
bool AllowEval() const;
bool AllowNonce(const std::string& nonce) const;
diff --git a/cobalt/csp/source_list_test.cc b/cobalt/csp/source_list_test.cc
index 6798ac3..574a2a9 100644
--- a/cobalt/csp/source_list_test.cc
+++ b/cobalt/csp/source_list_test.cc
@@ -161,21 +161,21 @@
SourceList source_list(&checker_, csp_.get(), "script-src");
ParseSourceList(&source_list, sources);
- EXPECT_TRUE(source_list.Matches(GURL("http://example1.com/foo/"),
- ContentSecurityPolicy::kDidRedirect));
- EXPECT_TRUE(source_list.Matches(GURL("http://example1.com/bar/"),
- ContentSecurityPolicy::kDidRedirect));
- EXPECT_TRUE(source_list.Matches(GURL("http://example2.com/bar/"),
- ContentSecurityPolicy::kDidRedirect));
- EXPECT_TRUE(source_list.Matches(GURL("http://example2.com/foo/"),
- ContentSecurityPolicy::kDidRedirect));
- EXPECT_TRUE(source_list.Matches(GURL("https://example1.com/foo/"),
- ContentSecurityPolicy::kDidRedirect));
- EXPECT_TRUE(source_list.Matches(GURL("https://example1.com/bar/"),
- ContentSecurityPolicy::kDidRedirect));
+ EXPECT_TRUE(
+ source_list.Matches(GURL("http://example1.com/foo/"), kDidRedirect));
+ EXPECT_TRUE(
+ source_list.Matches(GURL("http://example1.com/bar/"), kDidRedirect));
+ EXPECT_TRUE(
+ source_list.Matches(GURL("http://example2.com/bar/"), kDidRedirect));
+ EXPECT_TRUE(
+ source_list.Matches(GURL("http://example2.com/foo/"), kDidRedirect));
+ EXPECT_TRUE(
+ source_list.Matches(GURL("https://example1.com/foo/"), kDidRedirect));
+ EXPECT_TRUE(
+ source_list.Matches(GURL("https://example1.com/bar/"), kDidRedirect));
- EXPECT_FALSE(source_list.Matches(GURL("http://example3.com/foo/"),
- ContentSecurityPolicy::kDidRedirect));
+ EXPECT_FALSE(
+ source_list.Matches(GURL("http://example3.com/foo/"), kDidRedirect));
}
TEST_F(SourceListTest, TestInsecureLocalhostDefaultInsecureV4) {
diff --git a/cobalt/csp/source_test.cc b/cobalt/csp/source_test.cc
index ba7338f..7c0ef63 100644
--- a/cobalt/csp/source_test.cc
+++ b/cobalt/csp/source_test.cc
@@ -104,15 +104,14 @@
Source source(csp_.get(), config);
- EXPECT_TRUE(source.Matches(GURL("http://example.com:8000/"),
- ContentSecurityPolicy::kDidRedirect));
- EXPECT_TRUE(source.Matches(GURL("http://example.com:8000/foo"),
- ContentSecurityPolicy::kDidRedirect));
- EXPECT_TRUE(source.Matches(GURL("https://example.com:8000/foo"),
- ContentSecurityPolicy::kDidRedirect));
+ EXPECT_TRUE(source.Matches(GURL("http://example.com:8000/"), kDidRedirect));
+ EXPECT_TRUE(
+ source.Matches(GURL("http://example.com:8000/foo"), kDidRedirect));
+ EXPECT_TRUE(
+ source.Matches(GURL("https://example.com:8000/foo"), kDidRedirect));
- EXPECT_FALSE(source.Matches(GURL("http://not-example.com:8000/foo"),
- ContentSecurityPolicy::kDidRedirect));
+ EXPECT_FALSE(
+ source.Matches(GURL("http://not-example.com:8000/foo"), kDidRedirect));
EXPECT_FALSE(source.Matches(GURL("http://example.com:9000/foo/")));
}
diff --git a/cobalt/demos/content/partial-audio-frame/fmp4-aac-44100-tiny.mp4 b/cobalt/demos/content/partial-audio-frame/fmp4-aac-44100-tiny.mp4
new file mode 100644
index 0000000..1c15d74
--- /dev/null
+++ b/cobalt/demos/content/partial-audio-frame/fmp4-aac-44100-tiny.mp4
Binary files differ
diff --git a/cobalt/demos/content/partial-audio-frame/partial-audio-frame.html b/cobalt/demos/content/partial-audio-frame/partial-audio-frame.html
new file mode 100644
index 0000000..5ab57d7
--- /dev/null
+++ b/cobalt/demos/content/partial-audio-frame/partial-audio-frame.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Partial Audio</title>
+ <style>
+ body {
+ background-color: white;
+ }
+ video {
+ height: 240px;
+ width: 426px;
+ }
+ </style>
+</head>
+<body>
+ <script type="text/javascript" src="partial-audio-frame.js"></script>
+ <div id="status"></div>
+ <video id="video"></video>
+</body>
+</html>
diff --git a/cobalt/demos/content/partial-audio-frame/partial-audio-frame.js b/cobalt/demos/content/partial-audio-frame/partial-audio-frame.js
new file mode 100644
index 0000000..67f659b
--- /dev/null
+++ b/cobalt/demos/content/partial-audio-frame/partial-audio-frame.js
@@ -0,0 +1,133 @@
+// Copyright 2023 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+"use strict";
+
+function getFmp4AacData(onDataReady) {
+ const FILE_NAME = 'fmp4-aac-44100-tiny.mp4';
+
+ var xhr = new XMLHttpRequest;
+ xhr.responseType = 'arraybuffer';
+ xhr.addEventListener('readystatechange', function onreadystatechange() {
+ if (xhr.readyState == XMLHttpRequest.DONE) {
+ xhr.removeEventListener('readystatechange', onreadystatechange);
+
+ console.log('Media segment downloaded.');
+ onDataReady(xhr.response);
+ }
+ });
+ xhr.open('GET', FILE_NAME, true);
+ console.log('Sending request for media segment ...');
+ xhr.send();
+}
+
+function createAndAttachMediaSource(onSourceOpen) {
+ var video = document.getElementById('video');
+
+ var mediaSource = new MediaSource;
+ mediaSource.addEventListener('sourceopen', onSourceOpen);
+
+ console.log('Attaching MediaSource to video element ...');
+ video.src = window.URL.createObjectURL(mediaSource);
+}
+
+// Audio access units often contain more than one frame (in this case 1024
+// frames).
+// The function creates an audio stream with AUs that not all frames should be
+// played, like:
+// ... [*AA*] [*BB*] [*CC*] [*DD*] [*EE*] ...
+// 1. Each character represent ~256 frames.
+// 2. The letter represents the AU index, i.e. C is the next AU of B.
+// 3. '*' means the 256 frames are excluded from playback.
+function appendMediaSegment(mediaSource, sourceBuffer, mediaSegment,
+ currentOffset) {
+ // The input data is 44100hz, and each AU (access unit) contains 1024 frames.
+ var HALF_AU_DURATION_IN_SECONDS = 1024.0 / 44100 / 2;
+ var MAX_DURATION_IN_SECONDS = 5.;
+
+ if (!currentOffset) {
+ currentOffset = 0.0;
+ }
+
+ sourceBuffer.appendWindowEnd = MAX_DURATION_IN_SECONDS;
+
+ sourceBuffer.addEventListener('updateend', function onupdateend() {
+ sourceBuffer.removeEventListener('updateend', onupdateend);
+ sourceBuffer.abort();
+
+ currentOffset += HALF_AU_DURATION_IN_SECONDS;
+
+ if (currentOffset < MAX_DURATION_IN_SECONDS) {
+ appendMediaSegment(mediaSource, sourceBuffer, mediaSegment,
+ currentOffset);
+ } else {
+ mediaSource.endOfStream();
+ console.log('video.currentTime is ?');
+ var video = document.getElementById('video');
+ console.log('video.currentTime is ' + video.currentTime);
+ video.currentTime = HALF_AU_DURATION_IN_SECONDS / 2;
+ video.play();
+ }
+ });
+
+ console.log('Set timestampOffset to ' + currentOffset + ' before appending.');
+ // Assuming the buffered AUs are
+ // ... [*AAA] [BBBB] [CCCC] [DDDD] [EEEE] ...
+ //
+ // We setup the append by shifting `currentOffset` for 1/2 of an AU (so it
+ // points to the middle of the AU), and `appendWindowStart` for 1/4 of AU
+ // after `currentOffset`:
+ // currentOffset
+ // v
+ // ... [*A A A] [BBBB] [CCCC] [DDDD] [EEEE] ...
+ // ^
+ // appendWindowStart
+ //
+ // So the new append will start from `currentOffset`, but the first 256 frames
+ // will be masked due to `appendWindowStart`. The result will be:
+ // ... [*AA*] (all remaining AUs get replaced)
+ // ... [*XXX] [YYYY] [ZZZZ] ...
+ // i.e.
+ // ... [*AA*] [*XXX] [YYYY] [ZZZZ] ...
+ // This results an AU with first and last 256 frames (out of 1024 frames)
+ // excluded from playback. A non-conforming implementation will play the
+ // whole AU which takes twice of the time needed.
+ sourceBuffer.timestampOffset = currentOffset;
+ var appendWindowStart = currentOffset + HALF_AU_DURATION_IN_SECONDS / 2;
+ if (currentOffset > 0 && appendWindowStart < sourceBuffer.appendWindowEnd) {
+ sourceBuffer.appendWindowStart = appendWindowStart;
+ }
+ sourceBuffer.appendBuffer(mediaSegment);
+}
+
+function playPartialAudio() {
+ window.setInterval(function() {
+ document.getElementById('status').textContent =
+ 'currentTime ' + document.getElementById('video').currentTime;
+ }, 100);
+
+ getFmp4AacData(function(mediaSegment) {
+ createAndAttachMediaSource(function(event) {
+ var mediaSource = event.target;
+
+ console.log('Adding SourceBuffer ...');
+ var sourceBuffer =
+ mediaSource.addSourceBuffer('audio/mp4; codecs="mp4a.40.2"');
+
+ appendMediaSegment(mediaSource, sourceBuffer, mediaSegment);
+ });
+ });
+}
+
+addEventListener('load', playPartialAudio);
diff --git a/cobalt/dom/BUILD.gn b/cobalt/dom/BUILD.gn
index ff47acc..e25b7f2 100644
--- a/cobalt/dom/BUILD.gn
+++ b/cobalt/dom/BUILD.gn
@@ -14,6 +14,12 @@
import("//cobalt/build/contents_dir.gni")
+source_set("media_settings") {
+ has_pedantic_warnings = true
+ sources = [ "media_settings.h" ]
+ public_deps = [ "//cobalt/base" ]
+}
+
static_library("dom") {
has_pedantic_warnings = true
@@ -185,10 +191,9 @@
"lottie_player.h",
"media_query_list.cc",
"media_query_list.h",
+ "media_settings.cc",
"media_source.cc",
"media_source.h",
- "media_source_settings.cc",
- "media_source_settings.h",
"memory_info.cc",
"memory_info.h",
"mime_type_array.cc",
@@ -310,6 +315,7 @@
}
public_deps = [
+ ":media_settings",
"//cobalt/browser:generated_types",
"//cobalt/web",
"//cobalt/web:window_timers",
@@ -387,7 +393,7 @@
"local_storage_database_test.cc",
"location_test.cc",
"media_query_list_test.cc",
- "media_source_settings_test.cc",
+ "media_settings_test.cc",
"mutation_observer_test.cc",
"named_node_map_test.cc",
"navigator_licenses_test.cc",
diff --git a/cobalt/dom/dom_settings.cc b/cobalt/dom/dom_settings.cc
index 8390913..7ddf8cb 100644
--- a/cobalt/dom/dom_settings.cc
+++ b/cobalt/dom/dom_settings.cc
@@ -28,7 +28,6 @@
DOMSettings::DOMSettings(
const base::DebuggerHooks& debugger_hooks, const int max_dom_element_depth,
MediaSourceRegistry* media_source_registry,
- const MediaSourceSettings* media_source_settings,
media::CanPlayTypeHandler* can_play_type_handler,
const media::DecoderBufferMemoryInfo* decoder_buffer_memory_info,
MutationObserverTaskManager* mutation_observer_task_manager,
@@ -37,7 +36,6 @@
max_dom_element_depth_(max_dom_element_depth),
microphone_options_(options.microphone_options),
media_source_registry_(media_source_registry),
- media_source_settings_(media_source_settings),
can_play_type_handler_(can_play_type_handler),
decoder_buffer_memory_info_(decoder_buffer_memory_info),
mutation_observer_task_manager_(mutation_observer_task_manager) {}
diff --git a/cobalt/dom/dom_settings.h b/cobalt/dom/dom_settings.h
index 58e221f..d2be588 100644
--- a/cobalt/dom/dom_settings.h
+++ b/cobalt/dom/dom_settings.h
@@ -18,7 +18,6 @@
#include "base/logging.h"
#include "base/memory/ref_counted.h"
#include "cobalt/base/debugger_hooks.h"
-#include "cobalt/dom/media_source_settings.h"
#include "cobalt/dom/mutation_observer_task_manager.h"
#include "cobalt/media/can_play_type_handler.h"
#include "cobalt/media/decoder_buffer_memory_info.h"
@@ -57,7 +56,6 @@
DOMSettings(const base::DebuggerHooks& debugger_hooks,
const int max_dom_element_depth,
MediaSourceRegistry* media_source_registry,
- const MediaSourceSettings* media_source_settings,
media::CanPlayTypeHandler* can_play_type_handler,
const media::DecoderBufferMemoryInfo* decoder_buffer_memory_info,
MutationObserverTaskManager* mutation_observer_task_manager,
@@ -74,9 +72,6 @@
MediaSourceRegistry* media_source_registry() const {
return media_source_registry_;
}
- const MediaSourceSettings* media_source_settings() const {
- return media_source_settings_;
- }
void set_decoder_buffer_memory_info(
const media::DecoderBufferMemoryInfo* decoder_buffer_memory_info) {
decoder_buffer_memory_info_ = decoder_buffer_memory_info;
@@ -103,7 +98,6 @@
const int max_dom_element_depth_;
const speech::Microphone::Options microphone_options_;
MediaSourceRegistry* media_source_registry_;
- const MediaSourceSettings* media_source_settings_;
media::CanPlayTypeHandler* can_play_type_handler_;
const media::DecoderBufferMemoryInfo* decoder_buffer_memory_info_;
MutationObserverTaskManager* mutation_observer_task_manager_;
diff --git a/cobalt/dom/global_event_handlers.idl b/cobalt/dom/global_event_handlers.idl
index c877347..c98824e 100644
--- a/cobalt/dom/global_event_handlers.idl
+++ b/cobalt/dom/global_event_handlers.idl
@@ -51,6 +51,7 @@
// https://www.w3.org/TR/2015/REC-pointerevents-20150224/#extensions-to-the-globaleventhandlers-interface
attribute EventHandler ongotpointercapture;
attribute EventHandler onlostpointercapture;
+ attribute EventHandler onpointercancel;
attribute EventHandler onpointerdown;
attribute EventHandler onpointerenter;
attribute EventHandler onpointerleave;
diff --git a/cobalt/dom/html_element.cc b/cobalt/dom/html_element.cc
index 459be04..02dafb6 100644
--- a/cobalt/dom/html_element.cc
+++ b/cobalt/dom/html_element.cc
@@ -1121,7 +1121,7 @@
void HTMLElement::PurgeCachedBackgroundImages() {
ClearActiveBackgroundImages();
if (!cached_background_images_.empty()) {
- cached_background_images_.clear();
+ ClearCachedBackgroundImages();
computed_style_valid_ = false;
descendant_computed_styles_valid_ = false;
}
@@ -1474,6 +1474,21 @@
}
}
+void HTMLElement::SetUiNavItemBounds() {
+ if (!ui_nav_item_->IsContainer()) {
+ return;
+ }
+ float scrollable_width = scroll_width() - client_width();
+ float scroll_top_lower_bound = 0.0f;
+ float scroll_left_lower_bound =
+ GetUsedDirState() == DirState::kDirRightToLeft ? -scrollable_width : 0.0f;
+ float scroll_top_upper_bound = scroll_height() - client_height();
+ float scroll_left_upper_bound =
+ GetUsedDirState() == DirState::kDirRightToLeft ? 0.0f : scrollable_width;
+ ui_nav_item_->SetBounds(scroll_top_lower_bound, scroll_left_lower_bound,
+ scroll_top_upper_bound, scroll_left_upper_bound);
+}
+
void HTMLElement::SetDir(const std::string& value) {
// https://html.spec.whatwg.org/commit-snapshots/ebcac971c2add28a911283899da84ec509876c44/#the-dir-attribute
auto previous_dir = dir_;
@@ -2262,6 +2277,29 @@
active_background_images_.clear();
}
+void HTMLElement::ClearCachedBackgroundImages() {
+ // |cached_background_images_.clear()| cannot be used as it may lead to crash
+ // due to GetLoadTimingInfoAndCreateResourceTiming() being called indirectly
+ // from CachedImage dtor, and GetLoadTimingInfoAndCreateResourceTiming() loops
+ // on the |cached_background_images_| being cleared.
+ //
+ // To move and clear() like below is more straight-forward but leads to more
+ // in flight image loading performance timings get lost.
+ // auto images = std::move(cached_background_images_);
+ // DCHECK(cached_background_images_.empty());
+ // images.clear();
+ //
+ // So images are moved out and cleared one by one.
+ while (!cached_background_images_.empty()) {
+ auto image = std::move(cached_background_images_.back());
+ DCHECK(!cached_background_images_.back());
+ cached_background_images_.pop_back();
+ // TODO(b/265089478): This implementation will lose the performance timing
+ // of |image|, consider refining to record all performance timings.
+ image = nullptr;
+ }
+}
+
void HTMLElement::UpdateCachedBackgroundImagesFromComputedStyle() {
ClearActiveBackgroundImages();
@@ -2305,10 +2343,11 @@
cached_image, loaded_callback, base::Closure()));
}
+ ClearCachedBackgroundImages();
cached_background_images_ = std::move(cached_images);
} else {
// Clear the previous cached background image if the display is "none".
- cached_background_images_.clear();
+ ClearCachedBackgroundImages();
}
}
diff --git a/cobalt/dom/html_element.h b/cobalt/dom/html_element.h
index 0b59022..e2a6949 100644
--- a/cobalt/dom/html_element.h
+++ b/cobalt/dom/html_element.h
@@ -369,6 +369,9 @@
// for this HTML element (if any).
void UpdateUiNavigationFocus();
+ // Update boundaries for all UI navigation items.
+ void SetUiNavItemBounds();
+
// Returns true if the element is the root element as defined in
// https://www.w3.org/TR/html50/semantics.html#the-root-element.
bool IsRootElement();
@@ -393,7 +396,7 @@
const std::string& value) override;
void OnRemoveAttribute(const std::string& name) override;
- // Create Performance Resource Timing entry for background image.
+ // Create Performance Resource Timing entry for background image.
void GetLoadTimingInfoAndCreateResourceTiming();
// HTMLElement keeps a pointer to the dom stat tracker to ensure that it can
@@ -437,6 +440,9 @@
// Clear the list of active background images, and notify the animated image
// tracker to stop the animations.
void ClearActiveBackgroundImages();
+ // Carefully clear the list of cached background images to avoid crashing in
+ // the nested `HTMLElement::GetLoadTimingInfoAndCreateResourceTiming()` call.
+ void ClearCachedBackgroundImages();
void UpdateCachedBackgroundImagesFromComputedStyle();
diff --git a/cobalt/dom/html_media_element.cc b/cobalt/dom/html_media_element.cc
index e4fb384b..80a57d6 100644
--- a/cobalt/dom/html_media_element.cc
+++ b/cobalt/dom/html_media_element.cc
@@ -33,15 +33,18 @@
#include "cobalt/dom/document.h"
#include "cobalt/dom/html_element_context.h"
#include "cobalt/dom/html_video_element.h"
+#include "cobalt/dom/media_settings.h"
#include "cobalt/dom/media_source.h"
#include "cobalt/dom/media_source_ready_state.h"
#include "cobalt/loader/fetcher_factory.h"
-#include "cobalt/media/fetcher_buffered_data_source.h"
+#include "cobalt/media/url_fetcher_data_source.h"
#include "cobalt/media/web_media_player_factory.h"
#include "cobalt/script/script_value_factory.h"
+#include "cobalt/web/context.h"
#include "cobalt/web/csp_delegate.h"
#include "cobalt/web/dom_exception.h"
#include "cobalt/web/event.h"
+#include "cobalt/web/web_settings.h"
#include "cobalt/dom/eme/media_encrypted_event.h"
#include "cobalt/dom/eme/media_encrypted_event_init.h"
@@ -49,12 +52,9 @@
namespace cobalt {
namespace dom {
-using media::BufferedDataSource;
+using media::DataSource;
using media::WebMediaPlayer;
-const char HTMLMediaElement::kMediaSourceUrlProtocol[] = "blob";
-const double HTMLMediaElement::kMaxTimeupdateEventFrequency = 0.25;
-
namespace {
#define LOG_MEDIA_ELEMENT_ACTIVITIES 0
@@ -69,6 +69,9 @@
#endif // LOG_MEDIA_ELEMENT_ACTIVITIES
+constexpr char kMediaSourceUrlProtocol[] = "blob";
+constexpr int kTimeupdateEventIntervalInMilliseconds = 200;
+
DECLARE_INSTANCE_COUNTER(HTMLMediaElement);
loader::RequestMode GetRequestMode(
@@ -887,11 +890,10 @@
web::CspDelegate::kMedia);
request_mode_ = GetRequestMode(GetAttribute("crossOrigin"));
DCHECK(node_document()->location());
- std::unique_ptr<BufferedDataSource> data_source(
- new media::FetcherBufferedDataSource(
- base::MessageLoop::current()->task_runner(), url, csp_callback,
- html_element_context()->fetcher_factory()->network_module(),
- request_mode_, node_document()->location()->GetOriginAsObject()));
+ std::unique_ptr<DataSource> data_source(new media::URLFetcherDataSource(
+ base::MessageLoop::current()->task_runner(), url, csp_callback,
+ html_element_context()->fetcher_factory()->network_module(),
+ request_mode_, node_document()->location()->GetOriginAsObject()));
player_->LoadProgressive(url, std::move(data_source));
}
}
@@ -1025,10 +1027,19 @@
}
previous_progress_time_ = base::Time::Now().ToDoubleT();
+
+ DCHECK(environment_settings());
+ DCHECK(environment_settings()->context());
+ DCHECK(environment_settings()->context()->web_settings());
+
+ const auto& media_settings =
+ environment_settings()->context()->web_settings()->media_settings();
+ const int interval_in_milliseconds =
+ media_settings.GetMediaElementTimeupdateEventIntervalInMilliseconds()
+ .value_or(kTimeupdateEventIntervalInMilliseconds);
+
playback_progress_timer_.Start(
- FROM_HERE,
- base::TimeDelta::FromMilliseconds(
- static_cast<int64>(kMaxTimeupdateEventFrequency * 1000)),
+ FROM_HERE, base::TimeDelta::FromMilliseconds(interval_in_milliseconds),
this, &HTMLMediaElement::OnPlaybackProgressTimer);
}
diff --git a/cobalt/dom/html_media_element.h b/cobalt/dom/html_media_element.h
index 05e7a0e..4276fd8 100644
--- a/cobalt/dom/html_media_element.h
+++ b/cobalt/dom/html_media_element.h
@@ -161,9 +161,6 @@
const WebMediaPlayer* player() const { return player_.get(); }
private:
- static const char kMediaSourceUrlProtocol[];
- static const double kMaxTimeupdateEventFrequency;
-
// Loading
void CreateMediaPlayer();
void ScheduleLoad();
diff --git a/cobalt/dom/html_script_element.cc b/cobalt/dom/html_script_element.cc
index 1dda5a4..3634e62 100644
--- a/cobalt/dom/html_script_element.cc
+++ b/cobalt/dom/html_script_element.cc
@@ -344,7 +344,8 @@
csp_callback, request_mode_,
document_->location() ? document_->location()->GetOriginAsObject()
: loader::Origin(),
- disk_cache::kUncompiledScript, net::HttpRequestHeaders()),
+ disk_cache::kUncompiledScript, net::HttpRequestHeaders(),
+ /*skip_fetch_intercept=*/false),
base::Bind(&loader::TextDecoder::Create,
base::Bind(&HTMLScriptElement::OnSyncContentProduced,
base::Unretained(this)),
diff --git a/cobalt/dom/html_video_element.cc b/cobalt/dom/html_video_element.cc
index de2e639..a973f3e 100644
--- a/cobalt/dom/html_video_element.cc
+++ b/cobalt/dom/html_video_element.cc
@@ -25,7 +25,7 @@
namespace cobalt {
namespace dom {
-using media::VideoFrameProvider;
+using media::DecodeTargetProvider;
using media::WebMediaPlayer;
const char HTMLVideoElement::kTagName[] = "video";
@@ -98,9 +98,10 @@
}
}
-scoped_refptr<VideoFrameProvider> HTMLVideoElement::GetVideoFrameProvider() {
+scoped_refptr<DecodeTargetProvider>
+HTMLVideoElement::GetDecodeTargetProvider() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
- return player() ? player()->GetVideoFrameProvider() : NULL;
+ return player() ? player()->GetDecodeTargetProvider() : NULL;
}
WebMediaPlayer::SetBoundsCB HTMLVideoElement::GetSetBoundsCB() {
diff --git a/cobalt/dom/html_video_element.h b/cobalt/dom/html_video_element.h
index 1f5f64d..b98b6f6 100644
--- a/cobalt/dom/html_video_element.h
+++ b/cobalt/dom/html_video_element.h
@@ -30,7 +30,7 @@
// https://www.w3.org/TR/html50/embedded-content-0.html#the-video-element
class HTMLVideoElement : public HTMLMediaElement {
public:
- typedef media::VideoFrameProvider VideoFrameProvider;
+ typedef media::DecodeTargetProvider DecodeTargetProvider;
static const char kTagName[];
@@ -52,7 +52,7 @@
// From HTMLElement
scoped_refptr<HTMLVideoElement> AsHTMLVideoElement() override { return this; }
- scoped_refptr<VideoFrameProvider> GetVideoFrameProvider();
+ scoped_refptr<DecodeTargetProvider> GetDecodeTargetProvider();
WebMediaPlayer::SetBoundsCB GetSetBoundsCB();
diff --git a/cobalt/dom/html_video_element.idl b/cobalt/dom/html_video_element.idl
index d6decd5..c54a9f4 100644
--- a/cobalt/dom/html_video_element.idl
+++ b/cobalt/dom/html_video_element.idl
@@ -32,5 +32,5 @@
// INVALID_STATE_ERR exception is raised. The format of the string passed in
// is the same as the format for the string passed in to
// HTMLMediaElement.canPlayType().
- [Conditional=COBALT_ENABLE_SET_MAX_VIDEO_CAPABILITIES, RaisesException] void setMaxVideoCapabilities(DOMString max_video_capabilities);
+ [RaisesException] void setMaxVideoCapabilities(DOMString max_video_capabilities);
};
diff --git a/cobalt/dom/media_settings.cc b/cobalt/dom/media_settings.cc
new file mode 100644
index 0000000..411889d
--- /dev/null
+++ b/cobalt/dom/media_settings.cc
@@ -0,0 +1,83 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/dom/media_settings.h"
+
+#include <cstring>
+
+#include "base/logging.h"
+
+namespace cobalt {
+namespace dom {
+
+bool MediaSettingsImpl::Set(const std::string& name, int value) {
+ const char* kPrefixes[] = {"MediaElement.", "MediaSource."};
+ if (name.compare(0, strlen(kPrefixes[0]), kPrefixes[0]) != 0 &&
+ name.compare(0, strlen(kPrefixes[1]), kPrefixes[1]) != 0) {
+ return false;
+ }
+
+ base::AutoLock auto_lock(lock_);
+ if (name == "MediaSource.SourceBufferEvictExtraInBytes") {
+ if (value >= 0) {
+ source_buffer_evict_extra_in_bytes_ = value;
+ LOG(INFO) << name << ": set to " << value;
+ return true;
+ }
+ } else if (name == "MediaSource.MinimumProcessorCountToOffloadAlgorithm") {
+ if (value >= 0) {
+ minimum_processor_count_to_offload_algorithm_ = value;
+ LOG(INFO) << name << ": set to " << value;
+ return true;
+ }
+ } else if (name == "MediaSource.EnableAsynchronousReduction") {
+ if (value == 0 || value == 1) {
+ is_asynchronous_reduction_enabled_ = value != 0;
+ LOG(INFO) << name << ": set to " << value;
+ return true;
+ }
+ } else if (name == "MediaSource.EnableAvoidCopyingArrayBuffer") {
+ if (value == 0 || value == 1) {
+ is_avoid_copying_array_buffer_enabled_ = value != 0;
+ LOG(INFO) << name << ": set to " << value;
+ return true;
+ }
+ } else if (name == "MediaSource.MaxSizeForImmediateJob") {
+ if (value >= 0) {
+ max_size_for_immediate_job_ = value;
+ LOG(INFO) << name << ": set to " << value;
+ return true;
+ }
+ } else if (name == "MediaSource.MaxSourceBufferAppendSizeInBytes") {
+ if (value > 0) {
+ max_source_buffer_append_size_in_bytes_ = value;
+ LOG(INFO) << name << ": set to " << value;
+ return true;
+ }
+ } else if (name == "MediaElement.TimeupdateEventIntervalInMilliseconds") {
+ if (value > 0) {
+ media_element_timeupdate_event_interval_in_milliseconds_ = value;
+ LOG(INFO) << name << ": set to " << value;
+ return true;
+ }
+ } else {
+ LOG(WARNING) << "Ignore unknown setting with name \"" << name << "\"";
+ return false;
+ }
+ LOG(WARNING) << name << ": ignore invalid value " << value;
+ return false;
+}
+
+} // namespace dom
+} // namespace cobalt
diff --git a/cobalt/dom/media_settings.h b/cobalt/dom/media_settings.h
new file mode 100644
index 0000000..a6acb1c
--- /dev/null
+++ b/cobalt/dom/media_settings.h
@@ -0,0 +1,108 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_DOM_MEDIA_SETTINGS_H_
+#define COBALT_DOM_MEDIA_SETTINGS_H_
+
+#include <string>
+
+#include "base/optional.h"
+#include "base/synchronization/lock.h"
+
+namespace cobalt {
+namespace dom {
+
+// Holds browser wide settings for media related objects. Their default values
+// are set in related implementations, and the default values will be overridden
+// if the return values of the member functions are non-empty.
+// Please refer to where these functions are called for the particular
+// behaviors being controlled by them.
+class MediaSettings {
+ public:
+ virtual base::Optional<int> GetSourceBufferEvictExtraInBytes() const = 0;
+ virtual base::Optional<int> GetMinimumProcessorCountToOffloadAlgorithm()
+ const = 0;
+ virtual base::Optional<bool> IsAsynchronousReductionEnabled() const = 0;
+ virtual base::Optional<bool> IsAvoidCopyingArrayBufferEnabled() const = 0;
+ virtual base::Optional<int> GetMaxSizeForImmediateJob() const = 0;
+ virtual base::Optional<int> GetMaxSourceBufferAppendSizeInBytes() const = 0;
+
+ virtual base::Optional<int>
+ GetMediaElementTimeupdateEventIntervalInMilliseconds() const = 0;
+
+ protected:
+ MediaSettings() = default;
+ ~MediaSettings() = default;
+
+ MediaSettings(const MediaSettings&) = delete;
+ MediaSettings& operator=(const MediaSettings&) = delete;
+};
+
+// Allows setting the values of MediaSource settings via a name and an int
+// value.
+// This class is thread safe.
+class MediaSettingsImpl : public MediaSettings {
+ public:
+ base::Optional<int> GetSourceBufferEvictExtraInBytes() const override {
+ base::AutoLock auto_lock(lock_);
+ return source_buffer_evict_extra_in_bytes_;
+ }
+ base::Optional<int> GetMinimumProcessorCountToOffloadAlgorithm()
+ const override {
+ base::AutoLock auto_lock(lock_);
+ return minimum_processor_count_to_offload_algorithm_;
+ }
+ base::Optional<bool> IsAsynchronousReductionEnabled() const override {
+ base::AutoLock auto_lock(lock_);
+ return is_asynchronous_reduction_enabled_;
+ }
+ base::Optional<bool> IsAvoidCopyingArrayBufferEnabled() const override {
+ base::AutoLock auto_lock(lock_);
+ return is_avoid_copying_array_buffer_enabled_;
+ }
+ base::Optional<int> GetMaxSizeForImmediateJob() const override {
+ base::AutoLock auto_lock(lock_);
+ return max_size_for_immediate_job_;
+ }
+ base::Optional<int> GetMaxSourceBufferAppendSizeInBytes() const override {
+ base::AutoLock auto_lock(lock_);
+ return max_source_buffer_append_size_in_bytes_;
+ }
+
+ base::Optional<int> GetMediaElementTimeupdateEventIntervalInMilliseconds()
+ const override {
+ return media_element_timeupdate_event_interval_in_milliseconds_;
+ }
+
+ // Returns true when the setting associated with `name` is set to `value`.
+ // Returns false when `name` is not associated with any settings, or if
+ // `value` contains an invalid value.
+ bool Set(const std::string& name, int value);
+
+ private:
+ mutable base::Lock lock_;
+ base::Optional<int> source_buffer_evict_extra_in_bytes_;
+ base::Optional<int> minimum_processor_count_to_offload_algorithm_;
+ base::Optional<bool> is_asynchronous_reduction_enabled_;
+ base::Optional<bool> is_avoid_copying_array_buffer_enabled_;
+ base::Optional<int> max_size_for_immediate_job_;
+ base::Optional<int> max_source_buffer_append_size_in_bytes_;
+
+ base::Optional<int> media_element_timeupdate_event_interval_in_milliseconds_;
+};
+
+} // namespace dom
+} // namespace cobalt
+
+#endif // COBALT_DOM_MEDIA_SETTINGS_H_
diff --git a/cobalt/dom/media_settings_test.cc b/cobalt/dom/media_settings_test.cc
new file mode 100644
index 0000000..9b10847
--- /dev/null
+++ b/cobalt/dom/media_settings_test.cc
@@ -0,0 +1,144 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/dom/media_settings.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace cobalt {
+namespace dom {
+namespace {
+
+TEST(MediaSettingsImplTest, Empty) {
+ MediaSettingsImpl impl;
+
+ EXPECT_FALSE(impl.GetSourceBufferEvictExtraInBytes());
+ EXPECT_FALSE(impl.GetMinimumProcessorCountToOffloadAlgorithm());
+ EXPECT_FALSE(impl.IsAsynchronousReductionEnabled());
+ EXPECT_FALSE(impl.IsAvoidCopyingArrayBufferEnabled());
+ EXPECT_FALSE(impl.GetMaxSizeForImmediateJob());
+ EXPECT_FALSE(impl.GetMaxSourceBufferAppendSizeInBytes());
+ EXPECT_FALSE(impl.GetMediaElementTimeupdateEventIntervalInMilliseconds());
+}
+
+TEST(MediaSettingsImplTest, SunnyDay) {
+ MediaSettingsImpl impl;
+
+ ASSERT_TRUE(impl.Set("MediaSource.SourceBufferEvictExtraInBytes", 100));
+ ASSERT_TRUE(
+ impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", 101));
+ ASSERT_TRUE(impl.Set("MediaSource.EnableAsynchronousReduction", 1));
+ ASSERT_TRUE(impl.Set("MediaSource.EnableAvoidCopyingArrayBuffer", 1));
+ ASSERT_TRUE(impl.Set("MediaSource.MaxSizeForImmediateJob", 103));
+ ASSERT_TRUE(impl.Set("MediaSource.MaxSourceBufferAppendSizeInBytes", 100000));
+ ASSERT_TRUE(
+ impl.Set("MediaElement.TimeupdateEventIntervalInMilliseconds", 100001));
+
+ EXPECT_EQ(impl.GetSourceBufferEvictExtraInBytes().value(), 100);
+ EXPECT_EQ(impl.GetMinimumProcessorCountToOffloadAlgorithm().value(), 101);
+ EXPECT_TRUE(impl.IsAsynchronousReductionEnabled().value());
+ EXPECT_TRUE(impl.IsAvoidCopyingArrayBufferEnabled().value());
+ EXPECT_EQ(impl.GetMaxSizeForImmediateJob().value(), 103);
+ EXPECT_EQ(impl.GetMaxSourceBufferAppendSizeInBytes().value(), 100000);
+ EXPECT_EQ(impl.GetMediaElementTimeupdateEventIntervalInMilliseconds().value(),
+ 100001);
+}
+
+TEST(MediaSettingsImplTest, RainyDay) {
+ MediaSettingsImpl impl;
+
+ ASSERT_FALSE(impl.Set("MediaSource.SourceBufferEvictExtraInBytes", -100));
+ ASSERT_FALSE(
+ impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", -101));
+ ASSERT_FALSE(impl.Set("MediaSource.EnableAsynchronousReduction", 2));
+ ASSERT_FALSE(impl.Set("MediaSource.EnableAvoidCopyingArrayBuffer", 2));
+ ASSERT_FALSE(impl.Set("MediaSource.MaxSizeForImmediateJob", -103));
+ ASSERT_FALSE(impl.Set("MediaSource.MaxSourceBufferAppendSizeInBytes", 0));
+ ASSERT_FALSE(
+ impl.Set("MediaElement.TimeupdateEventIntervalInMilliseconds", 0));
+
+ EXPECT_FALSE(impl.GetSourceBufferEvictExtraInBytes());
+ EXPECT_FALSE(impl.GetMinimumProcessorCountToOffloadAlgorithm());
+ EXPECT_FALSE(impl.IsAsynchronousReductionEnabled());
+ EXPECT_FALSE(impl.IsAvoidCopyingArrayBufferEnabled());
+ EXPECT_FALSE(impl.GetMaxSizeForImmediateJob());
+ EXPECT_FALSE(impl.GetMaxSourceBufferAppendSizeInBytes());
+ EXPECT_FALSE(impl.GetMediaElementTimeupdateEventIntervalInMilliseconds());
+}
+
+TEST(MediaSettingsImplTest, ZeroValuesWork) {
+ MediaSettingsImpl impl;
+
+ ASSERT_TRUE(impl.Set("MediaSource.SourceBufferEvictExtraInBytes", 0));
+ ASSERT_TRUE(
+ impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", 0));
+ ASSERT_TRUE(impl.Set("MediaSource.EnableAsynchronousReduction", 0));
+ ASSERT_TRUE(impl.Set("MediaSource.EnableAvoidCopyingArrayBuffer", 0));
+ ASSERT_TRUE(impl.Set("MediaSource.MaxSizeForImmediateJob", 0));
+ // O is an invalid value for "MediaSource.MaxSourceBufferAppendSizeInBytes".
+ // O is an invalid value for
+ // "MediaElement.TimeupdateEventIntervalInMilliseconds".
+
+ EXPECT_EQ(impl.GetSourceBufferEvictExtraInBytes().value(), 0);
+ EXPECT_EQ(impl.GetMinimumProcessorCountToOffloadAlgorithm().value(), 0);
+ EXPECT_FALSE(impl.IsAsynchronousReductionEnabled().value());
+ EXPECT_FALSE(impl.IsAvoidCopyingArrayBufferEnabled().value());
+ EXPECT_EQ(impl.GetMaxSizeForImmediateJob().value(), 0);
+}
+
+TEST(MediaSettingsImplTest, Updatable) {
+ MediaSettingsImpl impl;
+
+ ASSERT_TRUE(impl.Set("MediaSource.SourceBufferEvictExtraInBytes", 0));
+ ASSERT_TRUE(
+ impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", 0));
+ ASSERT_TRUE(impl.Set("MediaSource.EnableAsynchronousReduction", 0));
+ ASSERT_TRUE(impl.Set("MediaSource.EnableAvoidCopyingArrayBuffer", 0));
+ ASSERT_TRUE(impl.Set("MediaSource.MaxSizeForImmediateJob", 0));
+ ASSERT_TRUE(impl.Set("MediaSource.MaxSourceBufferAppendSizeInBytes", 1));
+ ASSERT_TRUE(
+ impl.Set("MediaElement.TimeupdateEventIntervalInMilliseconds", 1));
+
+ ASSERT_TRUE(impl.Set("MediaSource.SourceBufferEvictExtraInBytes", 1));
+ ASSERT_TRUE(
+ impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", 1));
+ ASSERT_TRUE(impl.Set("MediaSource.EnableAsynchronousReduction", 1));
+ ASSERT_TRUE(impl.Set("MediaSource.EnableAvoidCopyingArrayBuffer", 1));
+ ASSERT_TRUE(impl.Set("MediaSource.MaxSizeForImmediateJob", 1));
+ ASSERT_TRUE(impl.Set("MediaSource.MaxSourceBufferAppendSizeInBytes", 2));
+ ASSERT_TRUE(
+ impl.Set("MediaElement.TimeupdateEventIntervalInMilliseconds", 2));
+
+ EXPECT_EQ(impl.GetSourceBufferEvictExtraInBytes().value(), 1);
+ EXPECT_EQ(impl.GetMinimumProcessorCountToOffloadAlgorithm().value(), 1);
+ EXPECT_TRUE(impl.IsAsynchronousReductionEnabled().value());
+ EXPECT_TRUE(impl.IsAvoidCopyingArrayBufferEnabled().value());
+ EXPECT_EQ(impl.GetMaxSizeForImmediateJob().value(), 1);
+ EXPECT_EQ(impl.GetMaxSourceBufferAppendSizeInBytes().value(), 2);
+ EXPECT_EQ(impl.GetMediaElementTimeupdateEventIntervalInMilliseconds().value(),
+ 2);
+}
+
+TEST(MediaSettingsImplTest, InvalidSettingNames) {
+ MediaSettingsImpl impl;
+
+ ASSERT_FALSE(impl.Set("MediaSource.Invalid", 0));
+ ASSERT_FALSE(impl.Set("MediaElement.Invalid", 1));
+ ASSERT_FALSE(impl.Set("Invalid.SourceBufferEvictExtraInBytes", 0));
+ ASSERT_FALSE(impl.Set("Invalid.TimeupdateEventIntervalInMilliseconds", 1));
+}
+
+} // namespace
+} // namespace dom
+} // namespace cobalt
diff --git a/cobalt/dom/media_source.cc b/cobalt/dom/media_source.cc
index a3571f1..e3e5223 100644
--- a/cobalt/dom/media_source.cc
+++ b/cobalt/dom/media_source.cc
@@ -57,6 +57,8 @@
#include "cobalt/base/polymorphic_downcast.h"
#include "cobalt/base/tokens.h"
#include "cobalt/dom/dom_settings.h"
+#include "cobalt/dom/media_settings.h"
+#include "cobalt/web/context.h"
#include "cobalt/web/dom_exception.h"
#include "cobalt/web/event.h"
#include "starboard/media.h"
@@ -72,12 +74,13 @@
using ::media::PIPELINE_OK;
using ::media::PipelineStatus;
-auto GetMediaSettings(script::EnvironmentSettings* settings) {
- DOMSettings* dom_settings =
- base::polymorphic_downcast<DOMSettings*>(settings);
- DCHECK(dom_settings);
- DCHECK(dom_settings->media_source_settings());
- return dom_settings->media_source_settings();
+const MediaSettings& GetMediaSettings(web::EnvironmentSettings* settings) {
+ DCHECK(settings);
+ DCHECK(settings->context());
+ DCHECK(settings->context()->web_settings());
+
+ const auto& web_settings = settings->context()->web_settings();
+ return web_settings->media_settings();
}
// If the system has more processors than the specified value, SourceBuffer
@@ -86,10 +89,10 @@
// The default value is 1024, which effectively disable offloading by default.
// Setting to a reasonably low value (say 0 or 2) will enable algorithm
// offloading.
-bool IsAlgorithmOffloadEnabled(script::EnvironmentSettings* settings) {
+bool IsAlgorithmOffloadEnabled(web::EnvironmentSettings* settings) {
int min_process_count_to_offload =
GetMediaSettings(settings)
- ->GetMinimumProcessorCountToOffloadAlgorithm()
+ .GetMinimumProcessorCountToOffloadAlgorithm()
.value_or(1024);
DCHECK_GE(min_process_count_to_offload, 0);
return SbSystemGetNumberOfProcessors() >= min_process_count_to_offload;
@@ -99,21 +102,20 @@
// behaviors. For example, queued events will be dispatached immediately when
// possible.
// The default value is false.
-bool IsAsynchronousReductionEnabled(script::EnvironmentSettings* settings) {
- return GetMediaSettings(settings)->IsAsynchronousReductionEnabled().value_or(
+bool IsAsynchronousReductionEnabled(web::EnvironmentSettings* settings) {
+ return GetMediaSettings(settings).IsAsynchronousReductionEnabled().value_or(
false);
}
// If the size of a job that is part of an algorithm is less than or equal to
// the return value of this function, the implementation will run the job
// immediately instead of scheduling it to run later to reduce latency.
-// NOTE: This only works when IsAsynchronousReductionEnabled() returns true,
-// and it is currently only enabled for buffer append.
-// The default value is 16 KB. Set to 0 will disable immediate job completely.
-int GetMaxSizeForImmediateJob(script::EnvironmentSettings* settings) {
- const int kDefaultMaxSize = 16 * 1024;
+// NOTE: This is currently only enabled for buffer append.
+// The default value is 0 KB, which disables immediate job completely.
+int GetMaxSizeForImmediateJob(web::EnvironmentSettings* settings) {
+ const int kDefaultMaxSize = 0;
auto max_size =
- GetMediaSettings(settings)->GetMaxSizeForImmediateJob().value_or(
+ GetMediaSettings(settings).GetMaxSizeForImmediateJob().value_or(
kDefaultMaxSize);
DCHECK_GE(max_size, 0);
return max_size;
@@ -123,9 +125,12 @@
MediaSource::MediaSource(script::EnvironmentSettings* settings)
: web::EventTarget(settings),
- algorithm_offload_enabled_(IsAlgorithmOffloadEnabled(settings)),
- asynchronous_reduction_enabled_(IsAsynchronousReductionEnabled(settings)),
- max_size_for_immediate_job_(GetMaxSizeForImmediateJob(settings)),
+ algorithm_offload_enabled_(
+ IsAlgorithmOffloadEnabled(environment_settings())),
+ asynchronous_reduction_enabled_(
+ IsAsynchronousReductionEnabled(environment_settings())),
+ max_size_for_immediate_job_(
+ GetMaxSizeForImmediateJob(environment_settings())),
default_algorithm_runner_(asynchronous_reduction_enabled_),
chunk_demuxer_(NULL),
ready_state_(kMediaSourceReadyStateClosed),
@@ -577,9 +582,20 @@
SerializedAlgorithmRunner<SourceBufferAlgorithm>*
MediaSource::GetAlgorithmRunner(int job_size) {
+ if (!asynchronous_reduction_enabled_ &&
+ job_size <= max_size_for_immediate_job_) {
+ // `default_algorithm_runner_` won't run jobs immediately when
+ // `asynchronous_reduction_enabled_` is false, so we use
+ // `immediate_job_algorithm_runner_` instead, which always has asynchronous
+ // reduction enabled.
+ return &immediate_job_algorithm_runner_;
+ }
if (!offload_algorithm_runner_) {
return &default_algorithm_runner_;
}
+ // The logic below is redundant as the code for immediate job can be
+ // consolidated with value of `asynchronous_reduction_enabled_` ignored. It's
+ // kept as is to leave existing behavior unchanged.
if (asynchronous_reduction_enabled_ &&
job_size <= max_size_for_immediate_job_) {
// Append without posting new tasks is only supported on the default runner.
@@ -599,8 +615,15 @@
}
void MediaSource::SetReadyState(MediaSourceReadyState ready_state) {
- if (ready_state == kMediaSourceReadyStateClosed) {
- chunk_demuxer_ = NULL;
+ if (!offload_algorithm_runner_) {
+ // Setting `chunk_demuxer_` to NULL when there is an active algorithm
+ // running may cause crash. So `chunk_demuxer_` is reset later in the
+ // function.
+ // When `offload_algorithm_runner_` is null, the logic is kept as is to
+ // ensure that the behavior stays the same when offload is not enabled.
+ if (ready_state == kMediaSourceReadyStateClosed) {
+ chunk_demuxer_ = NULL;
+ }
}
if (ready_state_ == ready_state) {
@@ -640,6 +663,7 @@
algorithm_process_thread_.reset();
}
offload_algorithm_runner_.reset();
+ chunk_demuxer_ = NULL;
}
bool MediaSource::IsUpdating() const {
diff --git a/cobalt/dom/media_source.h b/cobalt/dom/media_source.h
index 1c84e8b..0daa77a 100644
--- a/cobalt/dom/media_source.h
+++ b/cobalt/dom/media_source.h
@@ -151,6 +151,11 @@
// The default algorithm runner runs all steps on the web thread.
DefaultAlgorithmRunner<SourceBufferAlgorithm> default_algorithm_runner_;
+ // The immediate job algorithm runner runs job immediately on the web thread,
+ // it has asynchronous reduction always enabled to run immediate jobs even
+ // when asynchronous reduction is disabled on the `default_algorithm_runner_`.
+ DefaultAlgorithmRunner<SourceBufferAlgorithm> immediate_job_algorithm_runner_{
+ true};
// The offload algorithm runner offloads some steps to a non-web thread.
std::unique_ptr<OffloadAlgorithmRunner<SourceBufferAlgorithm>>
offload_algorithm_runner_;
diff --git a/cobalt/dom/media_source_settings.cc b/cobalt/dom/media_source_settings.cc
deleted file mode 100644
index d39602a..0000000
--- a/cobalt/dom/media_source_settings.cc
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright 2022 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "cobalt/dom/media_source_settings.h"
-
-#include <cstring>
-
-#include "base/logging.h"
-
-namespace cobalt {
-namespace dom {
-
-bool MediaSourceSettingsImpl::Set(const std::string& name, int value) {
- const char kPrefix[] = "MediaSource.";
- if (name.compare(0, strlen(kPrefix), kPrefix) != 0) {
- return false;
- }
-
- base::AutoLock auto_lock(lock_);
- if (name == "MediaSource.SourceBufferEvictExtraInBytes") {
- if (value >= 0) {
- source_buffer_evict_extra_in_bytes_ = value;
- LOG(INFO) << name << ": set to " << value;
- return true;
- }
- } else if (name == "MediaSource.MinimumProcessorCountToOffloadAlgorithm") {
- if (value >= 0) {
- minimum_processor_count_to_offload_algorithm_ = value;
- LOG(INFO) << name << ": set to " << value;
- return true;
- }
- } else if (name == "MediaSource.EnableAsynchronousReduction") {
- if (value == 0 || value == 1) {
- is_asynchronous_reduction_enabled_ = value != 0;
- LOG(INFO) << name << ": set to " << value;
- return true;
- }
- } else if (name == "MediaSource.MaxSizeForImmediateJob") {
- if (value >= 0) {
- max_size_for_immediate_job_ = value;
- LOG(INFO) << name << ": set to " << value;
- return true;
- }
- } else if (name == "MediaSource.MaxSourceBufferAppendSizeInBytes") {
- if (value > 0) {
- max_source_buffer_append_size_in_bytes_ = value;
- LOG(INFO) << name << ": set to " << value;
- return true;
- }
- } else {
- LOG(WARNING) << "Ignore unknown setting with name \"" << name << "\"";
- return false;
- }
- LOG(WARNING) << name << ": ignore invalid value " << value;
- return false;
-}
-
-} // namespace dom
-} // namespace cobalt
diff --git a/cobalt/dom/media_source_settings.h b/cobalt/dom/media_source_settings.h
deleted file mode 100644
index a122834..0000000
--- a/cobalt/dom/media_source_settings.h
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright 2022 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef COBALT_DOM_MEDIA_SOURCE_SETTINGS_H_
-#define COBALT_DOM_MEDIA_SOURCE_SETTINGS_H_
-
-#include <string>
-
-#include "base/optional.h"
-#include "base/synchronization/lock.h"
-
-namespace cobalt {
-namespace dom {
-
-// Holds WebModule wide settings for MediaSource related objects. Their default
-// values are set in MediaSource related implementations, and the default values
-// will be overridden if the return values of the member functions are
-// non-empty.
-// Please refer to where these functions are called for the particular
-// MediaSource behaviors being controlled by them.
-class MediaSourceSettings {
- public:
- virtual base::Optional<int> GetSourceBufferEvictExtraInBytes() const = 0;
- virtual base::Optional<int> GetMinimumProcessorCountToOffloadAlgorithm()
- const = 0;
- virtual base::Optional<bool> IsAsynchronousReductionEnabled() const = 0;
- virtual base::Optional<int> GetMaxSizeForImmediateJob() const = 0;
- virtual base::Optional<int> GetMaxSourceBufferAppendSizeInBytes() const = 0;
-
- protected:
- MediaSourceSettings() = default;
- ~MediaSourceSettings() = default;
-
- MediaSourceSettings(const MediaSourceSettings&) = delete;
- MediaSourceSettings& operator=(const MediaSourceSettings&) = delete;
-};
-
-// Allows setting the values of MediaSource settings via a name and an int
-// value.
-// This class is thread safe.
-class MediaSourceSettingsImpl : public MediaSourceSettings {
- public:
- base::Optional<int> GetSourceBufferEvictExtraInBytes() const override {
- base::AutoLock auto_lock(lock_);
- return source_buffer_evict_extra_in_bytes_;
- }
- base::Optional<int> GetMinimumProcessorCountToOffloadAlgorithm()
- const override {
- base::AutoLock auto_lock(lock_);
- return minimum_processor_count_to_offload_algorithm_;
- }
- base::Optional<bool> IsAsynchronousReductionEnabled() const override {
- base::AutoLock auto_lock(lock_);
- return is_asynchronous_reduction_enabled_;
- }
- base::Optional<int> GetMaxSizeForImmediateJob() const override {
- base::AutoLock auto_lock(lock_);
- return max_size_for_immediate_job_;
- }
- base::Optional<int> GetMaxSourceBufferAppendSizeInBytes() const override {
- base::AutoLock auto_lock(lock_);
- return max_source_buffer_append_size_in_bytes_;
- }
-
- // Returns true when the setting associated with `name` is set to `value`.
- // Returns false when `name` is not associated with any settings, or if
- // `value` contains an invalid value.
- bool Set(const std::string& name, int value);
-
- private:
- mutable base::Lock lock_;
- base::Optional<int> source_buffer_evict_extra_in_bytes_;
- base::Optional<int> minimum_processor_count_to_offload_algorithm_;
- base::Optional<bool> is_asynchronous_reduction_enabled_;
- base::Optional<int> max_size_for_immediate_job_;
- base::Optional<int> max_source_buffer_append_size_in_bytes_;
-};
-
-} // namespace dom
-} // namespace cobalt
-
-#endif // COBALT_DOM_MEDIA_SOURCE_SETTINGS_H_
diff --git a/cobalt/dom/media_source_settings_test.cc b/cobalt/dom/media_source_settings_test.cc
deleted file mode 100644
index d8a27ed..0000000
--- a/cobalt/dom/media_source_settings_test.cc
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright 2022 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "cobalt/dom/media_source_settings.h"
-
-#include "testing/gtest/include/gtest/gtest.h"
-
-namespace cobalt {
-namespace dom {
-namespace {
-
-TEST(MediaSourceSettingsImplTest, Empty) {
- MediaSourceSettingsImpl impl;
-
- EXPECT_FALSE(impl.GetSourceBufferEvictExtraInBytes());
- EXPECT_FALSE(impl.GetMinimumProcessorCountToOffloadAlgorithm());
- EXPECT_FALSE(impl.IsAsynchronousReductionEnabled());
- EXPECT_FALSE(impl.GetMaxSizeForImmediateJob());
- EXPECT_FALSE(impl.GetMaxSourceBufferAppendSizeInBytes());
-}
-
-TEST(MediaSourceSettingsImplTest, SunnyDay) {
- MediaSourceSettingsImpl impl;
-
- ASSERT_TRUE(impl.Set("MediaSource.SourceBufferEvictExtraInBytes", 100));
- ASSERT_TRUE(
- impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", 101));
- ASSERT_TRUE(impl.Set("MediaSource.EnableAsynchronousReduction", 1));
- ASSERT_TRUE(impl.Set("MediaSource.MaxSizeForImmediateJob", 103));
- ASSERT_TRUE(impl.Set("MediaSource.MaxSourceBufferAppendSizeInBytes", 100000));
-
- EXPECT_EQ(impl.GetSourceBufferEvictExtraInBytes().value(), 100);
- EXPECT_EQ(impl.GetMinimumProcessorCountToOffloadAlgorithm().value(), 101);
- EXPECT_TRUE(impl.IsAsynchronousReductionEnabled().value());
- EXPECT_EQ(impl.GetMaxSizeForImmediateJob().value(), 103);
- EXPECT_EQ(impl.GetMaxSourceBufferAppendSizeInBytes().value(), 100000);
-}
-
-TEST(MediaSourceSettingsImplTest, RainyDay) {
- MediaSourceSettingsImpl impl;
-
- ASSERT_FALSE(impl.Set("MediaSource.SourceBufferEvictExtraInBytes", -100));
- ASSERT_FALSE(
- impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", -101));
- ASSERT_FALSE(impl.Set("MediaSource.EnableAsynchronousReduction", 2));
- ASSERT_FALSE(impl.Set("MediaSource.MaxSizeForImmediateJob", -103));
- ASSERT_FALSE(impl.Set("MediaSource.MaxSourceBufferAppendSizeInBytes", 0));
-
- EXPECT_FALSE(impl.GetSourceBufferEvictExtraInBytes());
- EXPECT_FALSE(impl.GetMinimumProcessorCountToOffloadAlgorithm());
- EXPECT_FALSE(impl.IsAsynchronousReductionEnabled());
- EXPECT_FALSE(impl.GetMaxSizeForImmediateJob());
- EXPECT_FALSE(impl.GetMaxSourceBufferAppendSizeInBytes());
-}
-
-TEST(MediaSourceSettingsImplTest, ZeroValuesWork) {
- MediaSourceSettingsImpl impl;
-
- ASSERT_TRUE(impl.Set("MediaSource.SourceBufferEvictExtraInBytes", 0));
- ASSERT_TRUE(
- impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", 0));
- ASSERT_TRUE(impl.Set("MediaSource.EnableAsynchronousReduction", 0));
- ASSERT_TRUE(impl.Set("MediaSource.MaxSizeForImmediateJob", 0));
- // O is an invalid value for "MediaSource.MaxSourceBufferAppendSizeInBytes".
-
- EXPECT_EQ(impl.GetSourceBufferEvictExtraInBytes().value(), 0);
- EXPECT_EQ(impl.GetMinimumProcessorCountToOffloadAlgorithm().value(), 0);
- EXPECT_FALSE(impl.IsAsynchronousReductionEnabled().value());
- EXPECT_EQ(impl.GetMaxSizeForImmediateJob().value(), 0);
-}
-
-TEST(MediaSourceSettingsImplTest, Updatable) {
- MediaSourceSettingsImpl impl;
-
- ASSERT_TRUE(impl.Set("MediaSource.SourceBufferEvictExtraInBytes", 0));
- ASSERT_TRUE(
- impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", 0));
- ASSERT_TRUE(impl.Set("MediaSource.EnableAsynchronousReduction", 0));
- ASSERT_TRUE(impl.Set("MediaSource.MaxSizeForImmediateJob", 0));
- ASSERT_TRUE(impl.Set("MediaSource.MaxSourceBufferAppendSizeInBytes", 1));
-
- ASSERT_TRUE(impl.Set("MediaSource.SourceBufferEvictExtraInBytes", 1));
- ASSERT_TRUE(
- impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", 1));
- ASSERT_TRUE(impl.Set("MediaSource.EnableAsynchronousReduction", 1));
- ASSERT_TRUE(impl.Set("MediaSource.MaxSizeForImmediateJob", 1));
- ASSERT_TRUE(impl.Set("MediaSource.MaxSourceBufferAppendSizeInBytes", 2));
-
- EXPECT_EQ(impl.GetSourceBufferEvictExtraInBytes().value(), 1);
- EXPECT_EQ(impl.GetMinimumProcessorCountToOffloadAlgorithm().value(), 1);
- EXPECT_TRUE(impl.IsAsynchronousReductionEnabled().value());
- EXPECT_EQ(impl.GetMaxSizeForImmediateJob().value(), 1);
- EXPECT_EQ(impl.GetMaxSourceBufferAppendSizeInBytes().value(), 2);
-}
-
-TEST(MediaSourceSettingsImplTest, InvalidSettingNames) {
- MediaSourceSettingsImpl impl;
-
- ASSERT_FALSE(impl.Set("MediaSource.Invalid", 0));
- ASSERT_FALSE(impl.Set("Invalid.SourceBufferEvictExtraInBytes", 0));
-}
-
-} // namespace
-} // namespace dom
-} // namespace cobalt
diff --git a/cobalt/dom/performance_resource_timing.cc b/cobalt/dom/performance_resource_timing.cc
index f3bb1e8..09e72a3 100644
--- a/cobalt/dom/performance_resource_timing.cc
+++ b/cobalt/dom/performance_resource_timing.cc
@@ -128,6 +128,14 @@
return timing_info_.encoded_body_size;
}
+DOMHighResTimeStamp PerformanceResourceTiming::worker_start() const {
+ if (timing_info_.service_worker_start_time.is_null()) {
+ PerformanceEntry::start_time();
+ }
+ return Performance::MonotonicTimeToDOMHighResTimeStamp(
+ time_origin_, timing_info_.service_worker_start_time);
+}
+
void PerformanceResourceTiming::SetResourceTimingEntry(
const net::LoadTimingInfo& timing_info, const std::string& initiator_type,
const std::string& requested_url, const std::string& cache_mode) {
diff --git a/cobalt/dom/performance_resource_timing.h b/cobalt/dom/performance_resource_timing.h
index c80e45f..a7fa31d 100644
--- a/cobalt/dom/performance_resource_timing.h
+++ b/cobalt/dom/performance_resource_timing.h
@@ -57,6 +57,7 @@
DOMHighResTimeStamp response_end() const;
uint64_t transfer_size() const;
uint64_t encoded_body_size() const;
+ DOMHighResTimeStamp worker_start() const;
std::string entry_type() const override { return "resource"; }
PerformanceEntryType EntryTypeEnum() const override {
diff --git a/cobalt/dom/performance_resource_timing.idl b/cobalt/dom/performance_resource_timing.idl
index fe2875f..511c43d 100644
--- a/cobalt/dom/performance_resource_timing.idl
+++ b/cobalt/dom/performance_resource_timing.idl
@@ -28,4 +28,5 @@
readonly attribute DOMHighResTimeStamp responseEnd;
readonly attribute unsigned long long transferSize;
readonly attribute unsigned long long encodedBodySize;
+ readonly attribute DOMHighResTimeStamp workerStart;
};
diff --git a/cobalt/dom/pointer_state.cc b/cobalt/dom/pointer_state.cc
index 5dec62c..619fadf 100644
--- a/cobalt/dom/pointer_state.cc
+++ b/cobalt/dom/pointer_state.cc
@@ -261,6 +261,55 @@
pointers_with_active_buttons_.clear();
}
+void PointerState::SetClientCoordinates(int32_t pointer_id,
+ math::Vector2dF position) {
+ client_coordinates_[pointer_id] = position;
+}
+
+base::Optional<math::Vector2dF> PointerState::GetClientCoordinates(
+ int32_t pointer_id) {
+ auto client_coordinate = client_coordinates_.find(pointer_id);
+ if (client_coordinate != client_coordinates_.end()) {
+ return client_coordinate->second;
+ }
+ base::Optional<math::Vector2dF> ret;
+ return ret;
+}
+
+void PointerState::ClearClientCoordinates(int32_t pointer_id) {
+ client_coordinates_.erase(pointer_id);
+}
+
+void PointerState::SetClientTimeStamp(int32_t pointer_id, uint64 time_stamp) {
+ client_time_stamps_[pointer_id] = time_stamp;
+}
+
+base::Optional<uint64> PointerState::GetClientTimeStamp(int32_t pointer_id) {
+ auto time_stamp = client_time_stamps_.find(pointer_id);
+ if (time_stamp != client_time_stamps_.end()) {
+ return time_stamp->second;
+ }
+ base::Optional<uint64> ret;
+ return ret;
+}
+
+void PointerState::ClearTimeStamp(int32_t pointer_id) {
+ client_time_stamps_.erase(pointer_id);
+}
+
+void PointerState::SetWasCancelled(int32_t pointer_id) {
+ client_cancellations_.insert(pointer_id);
+}
+
+bool PointerState::GetWasCancelled(int32_t pointer_id) {
+ auto client_cancellation = client_cancellations_.find(pointer_id);
+ return client_cancellation != client_cancellations_.end();
+}
+
+void PointerState::ClearWasCancelled(int32_t pointer_id) {
+ client_cancellations_.erase(pointer_id);
+}
+
// static
bool PointerState::CanQueueEvent(const scoped_refptr<web::Event>& event) {
return event->GetWrappableType() == base::GetTypeId<PointerEvent>() ||
diff --git a/cobalt/dom/pointer_state.h b/cobalt/dom/pointer_state.h
index 15ed507..5552f9d 100644
--- a/cobalt/dom/pointer_state.h
+++ b/cobalt/dom/pointer_state.h
@@ -77,6 +77,21 @@
// shutdown.
void ClearForShutdown();
+ void SetClientCoordinates(int32_t pointer_id, math::Vector2dF position);
+ base::Optional<math::Vector2dF> GetClientCoordinates(int32_t pointer_id);
+ void ClearClientCoordinates(int32_t pointer_id);
+
+ void SetClientTimeStamp(int32_t pointer_id, uint64 time_stamp);
+ base::Optional<uint64> GetClientTimeStamp(int32_t pointer_id);
+ void ClearTimeStamp(int32_t pointer_id);
+
+ // Tracks whether a certain pointer was cancelled, i.e. if it panned the
+ // page viewport.
+ // https://www.w3.org/TR/pointerevents1/#the-pointercancel-event
+ void SetWasCancelled(int32_t pointer_id);
+ bool GetWasCancelled(int32_t pointer_id);
+ void ClearWasCancelled(int32_t pointer_id);
+
static bool CanQueueEvent(const scoped_refptr<web::Event>& event);
private:
@@ -95,6 +110,10 @@
// Store the set of pointers with active buttons.
// https://www.w3.org/TR/2015/REC-pointerevents-20150224/#dfn-active-buttons-state
std::set<int32_t> pointers_with_active_buttons_;
+
+ std::map<int32_t, math::Vector2dF> client_coordinates_;
+ std::map<int32_t, uint64> client_time_stamps_;
+ std::set<int32_t> client_cancellations_;
};
} // namespace dom
diff --git a/cobalt/dom/source_buffer.cc b/cobalt/dom/source_buffer.cc
index 449eaa1..267677b 100644
--- a/cobalt/dom/source_buffer.cc
+++ b/cobalt/dom/source_buffer.cc
@@ -56,8 +56,11 @@
#include "cobalt/base/polymorphic_downcast.h"
#include "cobalt/base/tokens.h"
#include "cobalt/dom/dom_settings.h"
+#include "cobalt/dom/media_settings.h"
#include "cobalt/dom/media_source.h"
+#include "cobalt/web/context.h"
#include "cobalt/web/dom_exception.h"
+#include "cobalt/web/web_settings.h"
#include "third_party/chromium/media/base/ranges.h"
#include "third_party/chromium/media/base/timestamp_constants.h"
@@ -87,35 +90,44 @@
return base::TimeDelta::FromSecondsD(time);
}
+const MediaSettings& GetMediaSettings(web::EnvironmentSettings* settings) {
+ DCHECK(settings);
+ DCHECK(settings->context());
+ DCHECK(settings->context()->web_settings());
+
+ const auto& web_settings = settings->context()->web_settings();
+ return web_settings->media_settings();
+}
+
// The return value will be used in `SourceBuffer::EvictCodedFrames()` to allow
// it to evict extra data from the SourceBuffer, so it can reduce the overall
// memory used by the underlying Demuxer implementation.
// The default value is 0, i.e. do not evict extra bytes.
-size_t GetEvictExtraInBytes(script::EnvironmentSettings* settings) {
- DOMSettings* dom_settings =
- base::polymorphic_downcast<DOMSettings*>(settings);
- DCHECK(dom_settings);
- DCHECK(dom_settings->media_source_settings());
- int bytes = dom_settings->media_source_settings()
- ->GetSourceBufferEvictExtraInBytes()
- .value_or(0);
+size_t GetEvictExtraInBytes(web::EnvironmentSettings* settings) {
+ const MediaSettings& media_settings = GetMediaSettings(settings);
+
+ int bytes = media_settings.GetSourceBufferEvictExtraInBytes().value_or(0);
DCHECK_GE(bytes, 0);
+
return std::max<int>(bytes, 0);
}
-size_t GetMaxAppendSizeInBytes(script::EnvironmentSettings* settings,
+size_t GetMaxAppendSizeInBytes(web::EnvironmentSettings* settings,
size_t default_value) {
- DOMSettings* dom_settings =
- base::polymorphic_downcast<DOMSettings*>(settings);
- DCHECK(dom_settings);
- DCHECK(dom_settings->media_source_settings());
- int bytes = dom_settings->media_source_settings()
- ->GetMaxSourceBufferAppendSizeInBytes()
- .value_or(default_value);
+ const MediaSettings& media_settings = GetMediaSettings(settings);
+
+ int bytes = media_settings.GetMaxSourceBufferAppendSizeInBytes().value_or(
+ default_value);
DCHECK_GT(bytes, 0);
+
return bytes;
}
+bool IsAvoidCopyingArrayBufferEnabled(web::EnvironmentSettings* settings) {
+ const MediaSettings& media_settings = GetMediaSettings(settings);
+ return media_settings.IsAvoidCopyingArrayBufferEnabled().value_or(false);
+}
+
} // namespace
SourceBuffer::OnInitSegmentReceivedHelper::OnInitSegmentReceivedHelper(
@@ -150,9 +162,9 @@
: web::EventTarget(settings),
on_init_segment_received_helper_(new OnInitSegmentReceivedHelper(this)),
id_(id),
- evict_extra_in_bytes_(GetEvictExtraInBytes(settings)),
- max_append_buffer_size_(
- GetMaxAppendSizeInBytes(settings, kDefaultMaxAppendBufferSize)),
+ evict_extra_in_bytes_(GetEvictExtraInBytes(environment_settings())),
+ max_append_buffer_size_(GetMaxAppendSizeInBytes(
+ environment_settings(), kDefaultMaxAppendBufferSize)),
media_source_(media_source),
chunk_demuxer_(chunk_demuxer),
event_queue_(event_queue),
@@ -307,19 +319,38 @@
append_window_end_ = end;
}
-void SourceBuffer::AppendBuffer(const script::Handle<script::ArrayBuffer>& data,
+void SourceBuffer::AppendBuffer(const script::Handle<ArrayBuffer>& data,
script::ExceptionState* exception_state) {
TRACE_EVENT1("cobalt::dom", "SourceBuffer::AppendBuffer()", "size",
data->ByteLength());
+
+ is_avoid_copying_array_buffer_enabled_ =
+ IsAvoidCopyingArrayBufferEnabled(environment_settings());
+
+ DCHECK(array_buffer_in_use_.IsEmpty());
+ DCHECK(array_buffer_view_in_use_.IsEmpty());
+ if (is_avoid_copying_array_buffer_enabled_) {
+ array_buffer_in_use_ = data;
+ }
+
AppendBufferInternal(static_cast<const unsigned char*>(data->Data()),
data->ByteLength(), exception_state);
}
-void SourceBuffer::AppendBuffer(
- const script::Handle<script::ArrayBufferView>& data,
- script::ExceptionState* exception_state) {
+void SourceBuffer::AppendBuffer(const script::Handle<ArrayBufferView>& data,
+ script::ExceptionState* exception_state) {
TRACE_EVENT1("cobalt::dom", "SourceBuffer::AppendBuffer()", "size",
data->ByteLength());
+
+ is_avoid_copying_array_buffer_enabled_ =
+ IsAvoidCopyingArrayBufferEnabled(environment_settings());
+
+ DCHECK(array_buffer_in_use_.IsEmpty());
+ DCHECK(array_buffer_view_in_use_.IsEmpty());
+ if (is_avoid_copying_array_buffer_enabled_) {
+ array_buffer_view_in_use_ = data;
+ }
+
AppendBufferInternal(static_cast<const unsigned char*>(data->RawData()),
data->ByteLength(), exception_state);
}
@@ -459,6 +490,8 @@
pending_append_data_.reset();
pending_append_data_capacity_ = 0;
+ array_buffer_in_use_ = script::Handle<ArrayBuffer>();
+ array_buffer_view_in_use_ = script::Handle<ArrayBufferView>();
}
double SourceBuffer::GetHighestPresentationTimestamp() const {
@@ -543,19 +576,34 @@
DCHECK(data || size == 0);
if (data) {
- if (pending_append_data_capacity_ < size) {
- pending_append_data_.reset();
- pending_append_data_.reset(new uint8_t[size]);
- pending_append_data_capacity_ = size;
+ if (is_avoid_copying_array_buffer_enabled_) {
+ // When |is_avoid_copying_array_buffer_enabled_| is true, we are holding
+ // reference to the underlying JS buffer object, and don't have to make a
+ // copy of the data.
+ if (array_buffer_view_in_use_.IsEmpty()) {
+ DCHECK(!array_buffer_in_use_.IsEmpty());
+ DCHECK_EQ(array_buffer_in_use_->Data(), data);
+ DCHECK_EQ(array_buffer_in_use_->ByteLength(), size);
+ } else {
+ DCHECK_EQ(array_buffer_view_in_use_->RawData(), data);
+ DCHECK_EQ(array_buffer_view_in_use_->ByteLength(), size);
+ }
+ } else {
+ if (pending_append_data_capacity_ < size) {
+ pending_append_data_.reset();
+ pending_append_data_.reset(new uint8_t[size]);
+ pending_append_data_capacity_ = size;
+ }
+ memcpy(pending_append_data_.get(), data, size);
+ data = pending_append_data_.get();
}
- memcpy(pending_append_data_.get(), data, size);
}
ScheduleEvent(base::Tokens::updatestart());
std::unique_ptr<SourceBufferAlgorithm> algorithm(
new SourceBufferAppendAlgorithm(
- media_source_, chunk_demuxer_, id_, pending_append_data_.get(), size,
+ media_source_, chunk_demuxer_, id_, data, size,
max_append_buffer_size_, DoubleToTimeDelta(append_window_start_),
DoubleToTimeDelta(append_window_end_),
DoubleToTimeDelta(timestamp_offset_),
@@ -574,6 +622,16 @@
void SourceBuffer::OnAlgorithmFinalized() {
DCHECK(active_algorithm_handle_);
active_algorithm_handle_ = nullptr;
+
+ if (is_avoid_copying_array_buffer_enabled_) {
+ // Allow them to be GCed.
+ array_buffer_in_use_ = script::Handle<ArrayBuffer>();
+ array_buffer_view_in_use_ = script::Handle<ArrayBufferView>();
+ is_avoid_copying_array_buffer_enabled_ = false;
+ } else {
+ DCHECK(array_buffer_in_use_.IsEmpty());
+ DCHECK(array_buffer_view_in_use_.IsEmpty());
+ }
}
void SourceBuffer::UpdateTimestampOffset(base::TimeDelta timestamp_offset) {
diff --git a/cobalt/dom/source_buffer.h b/cobalt/dom/source_buffer.h
index 25b15ac..c611177 100644
--- a/cobalt/dom/source_buffer.h
+++ b/cobalt/dom/source_buffer.h
@@ -142,6 +142,8 @@
private:
typedef ::media::MediaTracks MediaTracks;
+ typedef script::ArrayBuffer ArrayBuffer;
+ typedef script::ArrayBufferView ArrayBufferView;
// SourceBuffer is inherited from base::RefCounted<> and its ref count cannot
// be used on non-web threads. On the other hand the call to
@@ -210,6 +212,9 @@
double append_window_start_ = 0;
double append_window_end_ = std::numeric_limits<double>::infinity();
+ bool is_avoid_copying_array_buffer_enabled_ = false;
+ script::Handle<ArrayBuffer> array_buffer_in_use_;
+ script::Handle<ArrayBufferView> array_buffer_view_in_use_;
std::unique_ptr<uint8_t[]> pending_append_data_;
size_t pending_append_data_capacity_ = 0;
diff --git a/cobalt/dom/source_buffer_metrics.cc b/cobalt/dom/source_buffer_metrics.cc
index f2ae570..82aed8d 100644
--- a/cobalt/dom/source_buffer_metrics.cc
+++ b/cobalt/dom/source_buffer_metrics.cc
@@ -19,6 +19,7 @@
#include "base/logging.h"
#include "cobalt/base/statistics.h"
#include "starboard/common/string.h"
+#include "starboard/once.h"
#include "starboard/types.h"
namespace cobalt {
@@ -39,8 +40,15 @@
using BandwidthStatistics =
base::Statistics<int64_t, SbTimeMonotonic, 1024, GetBandwidthForStatistics>;
-BandwidthStatistics s_accumulated_wall_time_bandwidth_;
-BandwidthStatistics s_accumulated_thread_time_bandwidth_;
+class StatisticsWrapper {
+ public:
+ static StatisticsWrapper* GetInstance();
+ BandwidthStatistics accumulated_wall_time_bandwidth_{
+ "DOM.Performance.BandwidthStatsWallTime"};
+ BandwidthStatistics accumulated_thread_time_bandwidth_{
+ "DOM.Performance.BandwidthStatsThreadTime"};
+};
+
double GetWallToThreadTimeRatio(int64_t wall_time, int64_t thread_time) {
if (thread_time == 0) {
@@ -48,8 +56,8 @@
}
return static_cast<double>(wall_time) / thread_time;
}
-
} // namespace
+SB_ONCE_INITIALIZE_FUNCTION(StatisticsWrapper, StatisticsWrapper::GetInstance);
void SourceBufferMetrics::StartTracking() {
if (!is_primary_video_) {
@@ -99,9 +107,11 @@
return;
}
- s_accumulated_wall_time_bandwidth_.AddSample(total_size_, total_wall_time_);
- s_accumulated_thread_time_bandwidth_.AddSample(total_size_,
- total_thread_time_);
+ StatisticsWrapper::GetInstance()->accumulated_wall_time_bandwidth_.AddSample(
+ total_size_, total_wall_time_);
+ StatisticsWrapper::GetInstance()
+ ->accumulated_thread_time_bandwidth_.AddSample(total_size_,
+ total_thread_time_);
LOG_IF(INFO, total_thread_time_ > total_wall_time_)
<< "Total thread time " << total_thread_time_
@@ -140,16 +150,26 @@
" thread bandwidth statistics (B/s):\n"
" min %d, median %d, average %d, max %d",
GetWallToThreadTimeRatio(
- s_accumulated_wall_time_bandwidth_.accumulated_divisor(),
- s_accumulated_thread_time_bandwidth_.accumulated_divisor()),
- static_cast<int>(s_accumulated_wall_time_bandwidth_.min()),
- static_cast<int>(s_accumulated_wall_time_bandwidth_.GetMedian()),
- static_cast<int>(s_accumulated_wall_time_bandwidth_.average()),
- static_cast<int>(s_accumulated_wall_time_bandwidth_.max()),
- static_cast<int>(s_accumulated_thread_time_bandwidth_.min()),
- static_cast<int>(s_accumulated_thread_time_bandwidth_.GetMedian()),
- static_cast<int>(s_accumulated_thread_time_bandwidth_.average()),
- static_cast<int>(s_accumulated_thread_time_bandwidth_.max()));
+ StatisticsWrapper::GetInstance()
+ ->accumulated_wall_time_bandwidth_.accumulated_divisor(),
+ StatisticsWrapper::GetInstance()
+ ->accumulated_thread_time_bandwidth_.accumulated_divisor()),
+ static_cast<int>(StatisticsWrapper::GetInstance()
+ ->accumulated_wall_time_bandwidth_.min()),
+ static_cast<int>(StatisticsWrapper::GetInstance()
+ ->accumulated_wall_time_bandwidth_.GetMedian()),
+ static_cast<int>(StatisticsWrapper::GetInstance()
+ ->accumulated_wall_time_bandwidth_.average()),
+ static_cast<int>(StatisticsWrapper::GetInstance()
+ ->accumulated_wall_time_bandwidth_.max()),
+ static_cast<int>(StatisticsWrapper::GetInstance()
+ ->accumulated_thread_time_bandwidth_.min()),
+ static_cast<int>(StatisticsWrapper::GetInstance()
+ ->accumulated_thread_time_bandwidth_.GetMedian()),
+ static_cast<int>(StatisticsWrapper::GetInstance()
+ ->accumulated_thread_time_bandwidth_.average()),
+ static_cast<int>(StatisticsWrapper::GetInstance()
+ ->accumulated_thread_time_bandwidth_.max()));
}
#endif // !defined(COBALT_BUILD_TYPE_GOLD)
diff --git a/cobalt/dom/testing/stub_environment_settings.h b/cobalt/dom/testing/stub_environment_settings.h
index a70eef3..01d01f3 100644
--- a/cobalt/dom/testing/stub_environment_settings.h
+++ b/cobalt/dom/testing/stub_environment_settings.h
@@ -26,7 +26,7 @@
public:
explicit StubEnvironmentSettings(const Options& options = Options())
: DOMSettings(null_debugger_hooks_, 0, nullptr, nullptr, nullptr, nullptr,
- nullptr, options) {}
+ options) {}
~StubEnvironmentSettings() override {}
private:
diff --git a/cobalt/extension/extension_test.cc b/cobalt/extension/extension_test.cc
index 58f051e..b6cf3bc 100644
--- a/cobalt/extension/extension_test.cc
+++ b/cobalt/extension/extension_test.cc
@@ -69,7 +69,7 @@
EXPECT_STREQ(extension_api->name, kExtensionName);
EXPECT_GE(extension_api->version, 1u);
- EXPECT_LE(extension_api->version, 5u);
+ EXPECT_LE(extension_api->version, 6u);
EXPECT_NE(extension_api->GetMaximumFrameIntervalInMilliseconds, nullptr);
float maximum_frame_interval =
@@ -108,6 +108,10 @@
EXPECT_NE(extension_api->GetRenderRootTransform, nullptr);
}
+ if (extension_api->version >= 6) {
+ EXPECT_NE(extension_api->ReportFullyDrawn, nullptr);
+ }
+
const ExtensionApi* second_extension_api =
static_cast<const ExtensionApi*>(SbSystemGetExtension(kExtensionName));
EXPECT_EQ(second_extension_api, extension_api)
diff --git a/cobalt/extension/graphics.h b/cobalt/extension/graphics.h
index ed55a2e..15b1db1 100644
--- a/cobalt/extension/graphics.h
+++ b/cobalt/extension/graphics.h
@@ -107,6 +107,12 @@
bool (*GetRenderRootTransform)(float* m00, float* m01, float* m02, float* m10,
float* m11, float* m12, float* m20, float* m21,
float* m22);
+
+ // The fields below this point were added in version 6 or later.
+
+ // This function is called when the web app reports that it should be drawn.
+ void (*ReportFullyDrawn)();
+
} CobaltExtensionGraphicsApi;
#ifdef __cplusplus
diff --git a/cobalt/fetch/embedded_scripts/fetch.js b/cobalt/fetch/embedded_scripts/fetch.js
index ec89191..87435e2 100644
--- a/cobalt/fetch/embedded_scripts/fetch.js
+++ b/cobalt/fetch/embedded_scripts/fetch.js
@@ -1,22 +1,22 @@
-'use strict';(function(h){function J(a){"string"!==typeof a&&(a=String(a));if(/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(a))throw new e("Invalid character in header field name");return a.toLowerCase()}function X(a){"string"!==typeof a&&(a=String(a));var b;var c=0;for(b=a.length;c<b;c++){var d=a.charCodeAt(c);if(9!==d&&10!==d&&13!==d&&32!==d)break}for(b=a.length-1;b>c&&(d=a.charCodeAt(b),9===d||10===d||13===d||32===d);b--);a=a.substring(c,b+1);c=0;for(b=a.length;c<b;c++)if(d=a.charCodeAt(c),256<=d||0===d||
-10===d||13===d)throw new e("Invalid character in header field value");return a}function fa(a,b){throw new e("Immutable header cannot be modified");}function ha(a,b){return!1}function ia(a,b){a=a.toLowerCase();return-1<ja.indexOf(a)||a.startsWith("proxy-")||a.startsWith("sec-")?!0:!1}function ka(a,b){a=a.toLowerCase();return-1<la.indexOf(a)||"content-type"===a&&(b=b.split(";")[0].toLowerCase(),-1<ma.indexOf(b))?!1:!0}function S(a,b){return-1<na.indexOf(a.toLowerCase())?!0:!1}function n(a){this[p]=
-new P;void 0===this[A]&&(this[A]=ha);if(void 0!==a){if(null===a||"object"!==typeof a)throw new e("Constructing Headers with invalid parameters");a instanceof n?a.forEach(function(b,c){this.append(c,b)},this):K.isArray(a)?a.forEach(function(b){if(2!==b.length)throw new e("Constructing Headers with invalid parameters");this.append(b[0],b[1])},this):Object.getOwnPropertyNames(a).forEach(function(b){this.append(b,a[b])},this)}}function L(a,b){var c=oa(n.prototype);c[A]=b;n.call(c,a);return c}function T(a){if(a.bodyUsed)return z.reject(new e("Body was already read"));
-if(null===a.body)return z.resolve(new v(0));if(pa(a.body))return z.reject(new e("ReadableStream was already locked"));var b=a.body.getReader(),c=[],d=0;return b.read().then(function q(f){if(f.done){if(0===c.length)f=new v(0);else if(1===c.length)f=new v(c[0].buffer);else{f=new v(d);for(var g=0,w=c.length,M=0;g<w;g++)f.set(c[g],M),M+=c[g].length}return f}return f.value instanceof v?(d+=f.value.length,c.push(f.value),b.read().then(q)):z.reject(new e("Invalid stream data type"))})}function Y(){this._initBody=
-function(a){this[U]=!1;this[r]=null===a||void 0===a?null:a instanceof V?a:new V({start:function(b){if(a)if("string"===typeof a)b.enqueue(FetchInternal.encodeToUTF8(a));else if(Z.prototype.isPrototypeOf(a))b.enqueue(new v(a.slice(0)));else if(qa(a)){var c=new v(a.buffer);c=v.from(c.slice(a.byteOffset,a.byteLength+1));b.enqueue(c)}else if(a instanceof Blob)b.enqueue(new v(FetchInternal.blobToArrayBuffer(a)));else throw new e("Unsupported BodyInit type");b.close()}});this[x].get("content-type")||("string"===
-typeof a?this[x].set("content-type","text/plain;charset=UTF-8"):a instanceof Blob&&""!==a.type&&this[x].set("content-type",a.type))};W(this,{body:{get:function(){return this[r]}},bodyUsed:{get:function(){return this[U]?!0:this[r]?!!ra(this[r]):!1}}});this.arrayBuffer=function(){return this[E]?z.reject(new DOMException("Aborted","AbortError")):T(this).then(function(a){return a.buffer})};this.text=function(){return this[E]?z.reject(new DOMException("Aborted","AbortError")):T(this).then(function(a){return FetchInternal.decodeFromUTF8(a)})};
+'use strict';(function(h){function J(a){"string"!==typeof a&&(a=String(a));if(/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(a))throw new e("Invalid character in header field name");return a.toLowerCase()}function Y(a){"string"!==typeof a&&(a=String(a));var b;var c=0;for(b=a.length;c<b;c++){var d=a.charCodeAt(c);if(9!==d&&10!==d&&13!==d&&32!==d)break}for(b=a.length-1;b>c&&(d=a.charCodeAt(b),9===d||10===d||13===d||32===d);b--);a=a.substring(c,b+1);c=0;for(b=a.length;c<b;c++)if(d=a.charCodeAt(c),256<=d||0===d||
+10===d||13===d)throw new e("Invalid character in header field value");return a}function ia(a,b){throw new e("Immutable header cannot be modified");}function ja(a,b){return!1}function ka(a,b){a=a.toLowerCase();return-1<la.indexOf(a)||a.startsWith("proxy-")||a.startsWith("sec-")?!0:!1}function ma(a,b){a=a.toLowerCase();return-1<na.indexOf(a)||"content-type"===a&&(b=b.split(";")[0].toLowerCase(),-1<oa.indexOf(b))?!1:!0}function S(a,b){return-1<pa.indexOf(a.toLowerCase())?!0:!1}function n(a){this[p]=
+new P;void 0===this[A]&&(this[A]=ja);if(void 0!==a){if(null===a||"object"!==typeof a)throw new e("Constructing Headers with invalid parameters");a instanceof n?a.forEach(function(b,c){this.append(c,b)},this):K.isArray(a)?a.forEach(function(b){if(2!==b.length)throw new e("Constructing Headers with invalid parameters");this.append(b[0],b[1])},this):Object.getOwnPropertyNames(a).forEach(function(b){this.append(b,a[b])},this)}}function L(a,b){var c=qa(n.prototype);c[A]=b;n.call(c,a);return c}function T(a){if(a.bodyUsed)return z.reject(new e("Body was already read"));
+if(null===a.body)return z.resolve(new v(0));if(ra(a.body))return z.reject(new e("ReadableStream was already locked"));var b=a.body.getReader(),c=[],d=0;return b.read().then(function q(f){if(f.done){if(0===c.length)f=new v(0);else if(1===c.length)f=new v(c[0].buffer);else{f=new v(d);for(var g=0,w=c.length,M=0;g<w;g++)f.set(c[g],M),M+=c[g].length}return f}return f.value instanceof v?(d+=f.value.length,c.push(f.value),b.read().then(q)):z.reject(new e("Invalid stream data type"))})}function Z(){this._initBody=
+function(a){this[U]=!1;this[r]=null===a||void 0===a?null:a instanceof V?a:new V({start(b){if(a)if("string"===typeof a)b.enqueue(FetchInternal.encodeToUTF8(a));else if(aa.prototype.isPrototypeOf(a))b.enqueue(new v(a.slice(0)));else if(sa(a)){var c=new v(a.buffer);c=v.from(c.slice(a.byteOffset,a.byteLength+1));b.enqueue(c)}else if(a instanceof Blob)b.enqueue(new v(FetchInternal.blobToArrayBuffer(a)));else throw new e("Unsupported BodyInit type");b.close()}});this[x].get("content-type")||("string"===
+typeof a?this[x].set("content-type","text/plain;charset=UTF-8"):a instanceof Blob&&""!==a.type&&this[x].set("content-type",a.type))};W(this,{body:{get:function(){return this[r]}},bodyUsed:{get:function(){return this[U]?!0:this[r]?!!ta(this[r]):!1}}});this.arrayBuffer=function(){return this[E]?z.reject(new DOMException("Aborted","AbortError")):T(this).then(function(a){return a.buffer})};this.text=function(){return this[E]?z.reject(new DOMException("Aborted","AbortError")):T(this).then(function(a){return FetchInternal.decodeFromUTF8(a)})};
this.json=function(){return this[E]?z.reject(new DOMException("Aborted","AbortError")):this.text().then(JSON.parse)};return this}function B(a,b){var c=void 0!==b&&null!==b&&void 0===b.cloneBody;b=b||{};var d=b.body||b.cloneBody;""===b.body&&(d="");var l=b.headers,f=new AbortController;this[F]=f.signal;f=null;if(a instanceof B)this[C]=a.url,this[G]=a.cache,this[H]=a.credentials,void 0===l&&(l=a.headers),this[I]=a.integrity,this[D]=a.method,this[t]=a.mode,c&&"navigate"===this[t]&&(this[t]="same-origin"),
-this[N]=a.redirect,d||null===a.body||(d=a.body,a[U]=!0),f=a[F];else{this[C]=String(a);if(!FetchInternal.isUrlValid(this[C],!1))throw new e("Invalid request URL");this[t]="cors";this[H]="same-origin"}if(void 0!==b.window&&null!==b.window)throw new e("Invalid request window");this[G]=b.cache||this[G]||"default";if(-1===sa.indexOf(this[G]))throw new e("Invalid request cache mode");this[H]=b.credentials||this[H]||"same-origin";if(-1===ta.indexOf(this[H]))throw new e("Invalid request credentials");void 0!==
-b.integrity?this[I]=b.integrity:void 0===this[I]&&(this[I]="");a=(b.method||this[D]||"GET").toUpperCase();if(-1===ua.indexOf(a))throw new e("Invalid request method");this[D]=a;if(b.mode&&-1===va.indexOf(b.mode))throw new e("Invalid request mode");this[t]=b.mode||this[t]||"no-cors";if("no-cors"===this[t]){if(-1===wa.indexOf(this[D]))throw new e("Invalid request method for no-cors");if(""!==this[I])throw new e("Request integrity data is not allowed with no-cors");}if("same-origin"!==this[t]&&"only-if-cached"===
-this[G])throw new e("Request mode must be same-origin for only-if-cached");this[N]=b.redirect||this[N]||"follow";if(-1===xa.indexOf(this[N]))throw new e("Invalid request redirect mode");this[x]="no-cors"===this[t]?L(l,ka):L(l,ia);if(("GET"===this[D]||"HEAD"===this[D])&&d)throw new e("Request body is not allowed for GET or HEAD");"signal"in b&&(f=b.signal);f&&this[F].follow(f);this._initBody(d)}function ya(a,b){var c=L(void 0,b);a.replace(/\r?\n[\t ]+/g," ").split(/\r?\n/).forEach(function(d){var l=
-d.split(":");if(d=l.shift().trim())l=l.join(":").trim(),c.append(d,l)});return c}function u(a,b){b||(b={});this[Q]="default";this[y]="status"in b?b.status:200;if(200>this[y]||599<this[y])throw new aa("Invalid response status");this[ba]=200<=this[y]&&300>this[y];if("statusText"in b){var c=b.statusText;for(var d=0,l=c.length,f;d<l;d++)if(f=c.charCodeAt(d),9!==f&&(32>f||255<f||127===f))throw e("Invalid response status text");}else c="OK";this[R]=c;this[x]=L(b.headers,S);this[C]=b.url||"";if(a&&-1<za.indexOf(this[y]))throw new e("Response body is not allowed with a null body status");
-this[E]=b.is_aborted||!1;this._initBody(a)}if(!h.fetch){var K=h.Array,Z=h.ArrayBuffer,oa=h.Object.create,W=h.Object.defineProperties,k=h.Symbol,Aa=k.iterator,P=h.Map,aa=h.RangeError,e=h.TypeError,v=h.Uint8Array,z=h.Promise,V=h.ReadableStream,ca=h.ReadableStreamTee,ra=h.IsReadableStreamDisturbed,pa=h.IsReadableStreamLocked,r=k("body"),U=k("bodyUsed"),G=k("cache"),H=k("credentials"),A=k("guardCallback"),x=k("headers"),I=k("integrity"),p=k("map"),D=k("method"),t=k("mode"),ba=k("ok"),N=k("redirect"),
-y=k("status"),R=k("statusText"),Q=k("type"),C=k("url"),E=k("is_aborted"),F=k("signal"),ja="accept-charset accept-encoding access-control-request-headers access-control-request-method connection content-length cookie cookie2 date dnt expect host keep-alive origin referer te trailer transfer-encoding upgrade via".split(" "),na=["set-cookie","set-cookie2"],la=["accept","accept-language","content-language"],ma=["application/x-www-form-urlencoded","multipart/form-data","text/plain"],sa="default no-store reload no-cache force-cache only-if-cached".split(" "),
-ta=["omit","same-origin","include"],ua="DELETE GET HEAD OPTIONS POST PUT".split(" "),wa=["GET","HEAD","POST"],va=["same-origin","no-cors","cors"],xa=["follow","error","manual"],za=[101,204,205,304],Ba=[301,302,303,307,308],Ca="[object Int8Array];[object Uint8Array];[object Uint8ClampedArray];[object Int16Array];[object Uint16Array];[object Int32Array];[object Uint32Array];[object Float32Array];[object Float64Array]".split(";"),qa=Z.isView||function(a){return a&&-1<Ca.indexOf(Object.prototype.toString.call(a))};
-n.prototype.append=function(a,b){if(2!==arguments.length)throw e("Invalid parameters to append");a=J(a);b=X(b);this[A](a,b)||(this[p].has(a)?this[p].set(a,this[p].get(a)+", "+b):this[p].set(a,b))};n.prototype["delete"]=function(a){if(1!==arguments.length)throw e("Invalid parameters to delete");this[A](a,"invalid")||this[p].delete(J(a))};n.prototype.get=function(a){if(1!==arguments.length)throw e("Invalid parameters to get");a=J(a);var b=this[p].get(a);return void 0!==b?b:null};n.prototype.has=function(a){if(1!==
-arguments.length)throw e("Invalid parameters to has");return this[p].has(J(a))};n.prototype.set=function(a,b){if(2!==arguments.length)throw e("Invalid parameters to set");a=J(a);b=X(b);this[A](a,b)||this[p].set(a,b)};n.prototype.forEach=function(a,b){var c=this;K.from(this[p].entries()).sort().forEach(function(d){a.call(b,d[1],d[0],c)})};n.prototype.keys=function(){return(new P(K.from(this[p].entries()).sort())).keys()};n.prototype.values=function(){return(new P(K.from(this[p].entries()).sort())).values()};
-n.prototype.entries=function(){return(new P(K.from(this[p].entries()).sort())).entries()};n.prototype[Aa]=n.prototype.entries;B.prototype.clone=function(){var a=null;null!==this[r]&&(a=ca(this[r],!0),this[r]=a[0],a=a[1]);return new B(this,{cloneBody:a,signal:this[F]})};W(B.prototype,{cache:{get:function(){return this[G]}},credentials:{get:function(){return this[H]}},headers:{get:function(){return this[x]}},integrity:{get:function(){return this[I]}},method:{get:function(){return this[D]}},mode:{get:function(){return this[t]}},
-redirect:{get:function(){return this[N]}},url:{get:function(){return this[C]}},signal:{get:function(){return this[F]}}});Y.call(B.prototype);Y.call(u.prototype);u.prototype.clone=function(){var a=null;null!==this[r]&&(a=ca(this[r],!0),this[r]=a[0],a=a[1]);return new u(a,{status:this[y],statusText:this[R],headers:L(this[x],S),url:this[C],is_aborted:this[E]})};W(u.prototype,{headers:{get:function(){return this[x]}},ok:{get:function(){return this[ba]}},status:{get:function(){return this[y]}},statusText:{get:function(){return this[R]}},
-type:{get:function(){return this[Q]}},url:{get:function(){return this[C]}}});u.error=function(){var a=new u(null);a[x][A]=fa;a[Q]="error";a[y]=0;a[R]="";return a};u.redirect=function(a,b){if(!FetchInternal.isUrlValid(a,!0))throw new e("Invalid URL for response redirect");void 0===b&&(b=302);if(-1===Ba.indexOf(b))throw new aa("Invalid status code for response redirect");return new u(null,{status:b,headers:{location:a}})};h.Headers=n;h.Request=B;h.Response=u;h.fetch=function(a,b){return new z(function(c,
-d){var l=!1,f=!1,q=new B(a,b),g=new XMLHttpRequest,w=null;if(q.signal.aborted)return d(new DOMException("Aborted","AbortError"));var M=new V({start:function(m){w=m},cancel:function(m){l=!0;g.abort()}}),Da=function(){if(!l){l=!0;M.cancel();if(w)try{ReadableStreamDefaultControllerError(w,new DOMException("Aborted","AbortError"))}catch(m){}setTimeout(function(){try{g.abort()}catch(m){}},0)}};g.onload=function(){w.close()};g.onreadystatechange=function(){if(g.readyState===g.HEADERS_RECEIVED){var m={status:g.status,
-statusText:g.statusText,headers:ya(g.getAllResponseHeaders()||"",S)};m.url="responseURL"in g?g.responseURL:m.headers.get("X-Request-URL");try{var O=new u(M,m);q[F].addEventListener("abort",function(){O[E]=!0;Da();d(new DOMException("Aborted","AbortError"))});O[Q]=f?"cors":"basic";c(O)}catch(Ea){d(Ea)}}};g.onerror=function(){w.error(new e("Network request failed"));d(new e("Network request failed"))};g.ontimeout=function(){w.error(new e("Network request failed"));d(new e("Network request failed"))};
-g.open(q.method,q.url,!0);"include"===q.credentials&&(g.withCredentials=!0);q.headers.forEach(function(m,O){g.setRequestHeader(O,m)});var da=function(m){l||w.enqueue(m)},ea=function(m){f=m};null===q.body?g.fetch(da,ea,null):T(q).then(function(m){g.fetch(da,ea,m)})})};h.fetch.polyfill=!0}})(this);
\ No newline at end of file
+this[N]=a.redirect,d||null===a.body||(d=a.body,a[U]=!0),f=a[F];else{this[C]=String(a);if(!FetchInternal.isUrlValid(this[C],!1))throw new e("Invalid request URL");this[t]="cors";this[H]="same-origin"}if(void 0!==b.window&&null!==b.window)throw new e("Invalid request window");this[G]=b.cache||this[G]||"default";if(-1===ua.indexOf(this[G]))throw new e("Invalid request cache mode");this[H]=b.credentials||this[H]||"same-origin";if(-1===va.indexOf(this[H]))throw new e("Invalid request credentials");void 0!==
+b.integrity?this[I]=b.integrity:void 0===this[I]&&(this[I]="");a=(b.method||this[D]||"GET").toUpperCase();if(-1===wa.indexOf(a))throw new e("Invalid request method");this[D]=a;if(b.mode&&-1===xa.indexOf(b.mode))throw new e("Invalid request mode");this[t]=b.mode||this[t]||"no-cors";if("no-cors"===this[t]){if(-1===ya.indexOf(this[D]))throw new e("Invalid request method for no-cors");if(""!==this[I])throw new e("Request integrity data is not allowed with no-cors");}if("same-origin"!==this[t]&&"only-if-cached"===
+this[G])throw new e("Request mode must be same-origin for only-if-cached");this[N]=b.redirect||this[N]||"follow";if(-1===za.indexOf(this[N]))throw new e("Invalid request redirect mode");this[x]="no-cors"===this[t]?L(l,ma):L(l,ka);if(("GET"===this[D]||"HEAD"===this[D])&&d)throw new e("Request body is not allowed for GET or HEAD");"signal"in b&&(f=b.signal);f&&this[F].follow(f);this._initBody(d)}function Aa(a,b){var c=L(void 0,b);a.replace(/\r?\n[\t ]+/g," ").split(/\r?\n/).forEach(function(d){var l=
+d.split(":");if(d=l.shift().trim())l=l.join(":").trim(),c.append(d,l)});return c}function u(a,b){b||={};this[Q]="default";this[y]="status"in b?b.status:200;if(200>this[y]||599<this[y])throw new ba("Invalid response status");this[ca]=200<=this[y]&&300>this[y];if("statusText"in b){var c=b.statusText;for(var d=0,l=c.length,f;d<l;d++)if(f=c.charCodeAt(d),9!==f&&(32>f||255<f||127===f))throw e("Invalid response status text");}else c="OK";this[R]=c;this[x]=L(b.headers,S);this[C]=b.url||"";if(a&&-1<da.indexOf(this[y]))throw new e("Response body is not allowed with a null body status");
+this[E]=b.is_aborted||!1;this._initBody(a)}if(!h.fetch){var K=h.Array,aa=h.ArrayBuffer,qa=h.Object.create,W=h.Object.defineProperties,k=h.Symbol,Ba=k.iterator,P=h.Map,ba=h.RangeError,e=h.TypeError,v=h.Uint8Array,z=h.Promise,V=h.ReadableStream,ea=h.ReadableStreamTee,ta=h.IsReadableStreamDisturbed,ra=h.IsReadableStreamLocked,r=k("body"),U=k("bodyUsed"),G=k("cache"),H=k("credentials"),A=k("guardCallback"),x=k("headers"),I=k("integrity"),p=k("map"),D=k("method"),t=k("mode"),ca=k("ok"),N=k("redirect"),
+y=k("status"),R=k("statusText"),Q=k("type"),C=k("url"),E=k("is_aborted"),F=k("signal"),la="accept-charset accept-encoding access-control-request-headers access-control-request-method connection content-length cookie cookie2 date dnt expect host keep-alive origin referer te trailer transfer-encoding upgrade via".split(" "),pa=["set-cookie","set-cookie2"],na=["accept","accept-language","content-language"],oa=["application/x-www-form-urlencoded","multipart/form-data","text/plain"],ua="default no-store reload no-cache force-cache only-if-cached".split(" "),
+va=["omit","same-origin","include"],wa="DELETE GET HEAD OPTIONS POST PUT".split(" "),ya=["GET","HEAD","POST"],xa=["same-origin","no-cors","cors"],za=["follow","error","manual"],da=[101,204,205,304],Ca=[301,302,303,307,308],Da="[object Int8Array];[object Uint8Array];[object Uint8ClampedArray];[object Int16Array];[object Uint16Array];[object Int32Array];[object Uint32Array];[object Float32Array];[object Float64Array]".split(";"),sa=aa.isView||function(a){return a&&-1<Da.indexOf(Object.prototype.toString.call(a))};
+n.prototype.append=function(a,b){if(2!==arguments.length)throw e("Invalid parameters to append");a=J(a);b=Y(b);this[A](a,b)||(this[p].has(a)?this[p].set(a,this[p].get(a)+", "+b):this[p].set(a,b))};n.prototype["delete"]=function(a){if(1!==arguments.length)throw e("Invalid parameters to delete");this[A](a,"invalid")||this[p].delete(J(a))};n.prototype.get=function(a){if(1!==arguments.length)throw e("Invalid parameters to get");a=J(a);var b=this[p].get(a);return void 0!==b?b:null};n.prototype.has=function(a){if(1!==
+arguments.length)throw e("Invalid parameters to has");return this[p].has(J(a))};n.prototype.set=function(a,b){if(2!==arguments.length)throw e("Invalid parameters to set");a=J(a);b=Y(b);this[A](a,b)||this[p].set(a,b)};n.prototype.forEach=function(a,b){var c=this;K.from(this[p].entries()).sort().forEach(function(d){a.call(b,d[1],d[0],c)})};n.prototype.keys=function(){return(new P(K.from(this[p].entries()).sort())).keys()};n.prototype.values=function(){return(new P(K.from(this[p].entries()).sort())).values()};
+n.prototype.entries=function(){return(new P(K.from(this[p].entries()).sort())).entries()};n.prototype[Ba]=n.prototype.entries;B.prototype.clone=function(){var a=null;null!==this[r]&&(a=ea(this[r],!0),this[r]=a[0],a=a[1]);return new B(this,{cloneBody:a,signal:this[F]})};W(B.prototype,{cache:{get:function(){return this[G]}},credentials:{get:function(){return this[H]}},headers:{get:function(){return this[x]}},integrity:{get:function(){return this[I]}},method:{get:function(){return this[D]}},mode:{get:function(){return this[t]}},
+redirect:{get:function(){return this[N]}},url:{get:function(){return this[C]}},signal:{get:function(){return this[F]}}});Z.call(B.prototype);Z.call(u.prototype);u.prototype.clone=function(){var a=null;null!==this[r]&&(a=ea(this[r],!0),this[r]=a[0],a=a[1]);return new u(a,{status:this[y],statusText:this[R],headers:L(this[x],S),url:this[C],is_aborted:this[E]})};W(u.prototype,{headers:{get:function(){return this[x]}},ok:{get:function(){return this[ca]}},status:{get:function(){return this[y]}},statusText:{get:function(){return this[R]}},
+type:{get:function(){return this[Q]}},url:{get:function(){return this[C]}}});u.error=function(){var a=new u(null);a[x][A]=ia;a[Q]="error";a[y]=0;a[R]="";return a};u.redirect=function(a,b){if(!FetchInternal.isUrlValid(a,!0))throw new e("Invalid URL for response redirect");void 0===b&&(b=302);if(-1===Ca.indexOf(b))throw new ba("Invalid status code for response redirect");return new u(null,{status:b,headers:{location:a}})};h.Headers=n;h.Request=B;h.Response=u;h.fetch=function(a,b){return new z(function(c,
+d){var l=!1,f=!1,q=new B(a,b),g=new XMLHttpRequest,w=null;if(q.signal.aborted)return d(new DOMException("Aborted","AbortError"));var M=new V({start(m){w=m},cancel(m){l=!0;g.abort()}}),Ea=function(){if(!l){l=!0;M.cancel();if(w)try{ReadableStreamDefaultControllerError(w,new DOMException("Aborted","AbortError"))}catch(m){}setTimeout(function(){try{g.abort()}catch(m){}},0)}};g.onload=function(){w.close()};g.onreadystatechange=function(){if(g.readyState===g.HEADERS_RECEIVED){var m={status:g.status,statusText:g.statusText,
+headers:Aa(g.getAllResponseHeaders()||"",S)};m.url="responseURL"in g?g.responseURL:m.headers.get("X-Request-URL");try{let X=-1==da.indexOf(g.status);var O=new u(X?M:null,m);q[F].addEventListener("abort",()=>{O[E]=!0;Ea();d(new DOMException("Aborted","AbortError"))});O[Q]=f?"cors":"basic";c(O)}catch(X){d(X)}}};g.onerror=function(){w.error(new e("Network request failed"));d(new e("Network request failed"))};g.ontimeout=function(){w.error(new e("Network request failed"));d(new e("Network request failed"))};
+g.open(q.method,q.url,!0);"include"===q.credentials&&(g.withCredentials=!0);q.headers.forEach(function(m,O){g.setRequestHeader(O,m)});var fa=function(m){l||w.enqueue(m)},ha=function(m){f=m};null===q.body?g.fetch(fa,ha,null):T(q).then(function(m){g.fetch(fa,ha,m)})})};h.fetch.polyfill=!0}})(this);
diff --git a/cobalt/fetch/fetch.js b/cobalt/fetch/fetch.js
index 8c8162b..cae8152 100644
--- a/cobalt/fetch/fetch.js
+++ b/cobalt/fetch/fetch.js
@@ -854,7 +854,8 @@
xhr.responseURL : init.headers.get('X-Request-URL')
try {
// 6. Let responseObject be a new Response object
- var response = new Response(responseStream, init)
+ let body_allowed = NULL_BODY_STATUSES.indexOf(xhr.status) == -1
+ var response = new Response(body_allowed ? responseStream : null, init)
// 7. Let locallyAborted be false - done in response constructor
request[SIGNAL_SLOT].addEventListener('abort',() => {
diff --git a/cobalt/h5vcc/BUILD.gn b/cobalt/h5vcc/BUILD.gn
index 188c6ed..c31a775 100644
--- a/cobalt/h5vcc/BUILD.gn
+++ b/cobalt/h5vcc/BUILD.gn
@@ -22,9 +22,6 @@
if (enable_account_manager) {
defines += [ "COBALT_ENABLE_ACCOUNT_MANAGER" ]
}
- if (enable_sso) {
- defines += [ "COBALT_ENABLE_SSO" ]
- }
}
static_library("h5vcc") {
@@ -79,11 +76,11 @@
"//cobalt/cache",
"//cobalt/configuration",
"//cobalt/dom",
+ "//cobalt/media",
"//cobalt/network",
"//cobalt/persistent_storage:persistent_settings",
"//cobalt/script",
"//cobalt/speech",
- "//cobalt/sso",
"//cobalt/storage",
"//cobalt/trace_event",
"//cobalt/watchdog",
@@ -101,13 +98,6 @@
]
}
- if (enable_sso) {
- sources += [
- "h5vcc_sso.cc",
- "h5vcc_sso.h",
- ]
- }
-
if (sb_is_evergreen) {
sources += [
"h5vcc_updater.cc",
diff --git a/cobalt/h5vcc/h5vcc.cc b/cobalt/h5vcc/h5vcc.cc
index e88b901..6f2b436 100644
--- a/cobalt/h5vcc/h5vcc.cc
+++ b/cobalt/h5vcc/h5vcc.cc
@@ -20,7 +20,6 @@
#endif
#include "cobalt/persistent_storage/persistent_settings.h"
-#include "cobalt/sso/sso_interface.h"
namespace cobalt {
namespace h5vcc {
@@ -32,15 +31,13 @@
c_val_ = new dom::CValView();
crash_log_ = new H5vccCrashLog();
runtime_ = new H5vccRuntime(settings.event_dispatcher);
- settings_ = new H5vccSettings(
- settings.set_media_source_setting_func, settings.network_module,
+ settings_ =
+ new H5vccSettings(settings.set_web_setting_func, settings.media_module,
+ settings.network_module,
#if SB_IS(EVERGREEN)
- settings.updater_module,
+ settings.updater_module,
#endif
- settings.user_agent_data, settings.global_environment);
-#if defined(COBALT_ENABLE_SSO)
- sso_ = new H5vccSso();
-#endif
+ settings.user_agent_data, settings.global_environment);
storage_ =
new H5vccStorage(settings.network_module, settings.persistent_settings);
trace_event_ = new H5vccTraceEvent();
@@ -77,7 +74,6 @@
tracer->Trace(crash_log_);
tracer->Trace(runtime_);
tracer->Trace(settings_);
- tracer->Trace(sso_);
tracer->Trace(storage_);
tracer->Trace(system_);
tracer->Trace(trace_event_);
diff --git a/cobalt/h5vcc/h5vcc.h b/cobalt/h5vcc/h5vcc.h
index 0dce20d..138a67b 100644
--- a/cobalt/h5vcc/h5vcc.h
+++ b/cobalt/h5vcc/h5vcc.h
@@ -27,7 +27,6 @@
#include "cobalt/h5vcc/h5vcc_crash_log.h"
#include "cobalt/h5vcc/h5vcc_runtime.h"
#include "cobalt/h5vcc/h5vcc_settings.h"
-#include "cobalt/h5vcc/h5vcc_sso.h"
#include "cobalt/h5vcc/h5vcc_storage.h"
#include "cobalt/h5vcc/h5vcc_system.h"
#include "cobalt/h5vcc/h5vcc_trace_event.h"
@@ -46,7 +45,8 @@
public:
struct Settings {
Settings()
- : network_module(NULL),
+ : media_module(NULL),
+ network_module(NULL),
#if SB_IS(EVERGREEN)
updater_module(NULL),
#endif
@@ -55,7 +55,8 @@
user_agent_data(NULL),
global_environment(NULL) {
}
- H5vccSettings::SetMediaSourceSettingFunc set_media_source_setting_func;
+ H5vccSettings::SetSettingFunc set_web_setting_func;
+ media::MediaModule* media_module;
network::NetworkModule* network_module;
#if SB_IS(EVERGREEN)
updater::UpdaterModule* updater_module;
@@ -81,9 +82,6 @@
const scoped_refptr<H5vccCrashLog>& crash_log() const { return crash_log_; }
const scoped_refptr<H5vccRuntime>& runtime() const { return runtime_; }
const scoped_refptr<H5vccSettings>& settings() const { return settings_; }
-#if defined(COBALT_ENABLE_SSO)
- const scoped_refptr<H5vccSso>& sso() const { return sso_; }
-#endif
const scoped_refptr<H5vccStorage>& storage() const { return storage_; }
const scoped_refptr<H5vccSystem>& system() const { return system_; }
const scoped_refptr<H5vccTraceEvent>& trace_event() const {
@@ -104,7 +102,6 @@
scoped_refptr<H5vccCrashLog> crash_log_;
scoped_refptr<H5vccRuntime> runtime_;
scoped_refptr<H5vccSettings> settings_;
- scoped_refptr<H5vccSso> sso_;
scoped_refptr<H5vccStorage> storage_;
scoped_refptr<H5vccSystem> system_;
scoped_refptr<H5vccTraceEvent> trace_event_;
diff --git a/cobalt/h5vcc/h5vcc.idl b/cobalt/h5vcc/h5vcc.idl
index 4428dda..9201a36 100644
--- a/cobalt/h5vcc/h5vcc.idl
+++ b/cobalt/h5vcc/h5vcc.idl
@@ -38,8 +38,6 @@
readonly attribute CValView cVal;
readonly attribute H5vccRuntime runtime;
readonly attribute H5vccSettings settings;
- [Conditional=COBALT_ENABLE_SSO]
- readonly attribute H5vccSso sso;
readonly attribute H5vccStorage storage;
readonly attribute H5vccSystem system;
readonly attribute H5vccTraceEvent traceEvent;
diff --git a/cobalt/h5vcc/h5vcc_settings.cc b/cobalt/h5vcc/h5vcc_settings.cc
index 1735270..1eef9e6 100644
--- a/cobalt/h5vcc/h5vcc_settings.cc
+++ b/cobalt/h5vcc/h5vcc_settings.cc
@@ -19,15 +19,16 @@
namespace cobalt {
namespace h5vcc {
-H5vccSettings::H5vccSettings(
- const SetMediaSourceSettingFunc& set_media_source_setting_func,
- cobalt::network::NetworkModule* network_module,
+H5vccSettings::H5vccSettings(const SetSettingFunc& set_web_setting_func,
+ cobalt::media::MediaModule* media_module,
+ cobalt::network::NetworkModule* network_module,
#if SB_IS(EVERGREEN)
- cobalt::updater::UpdaterModule* updater_module,
+ cobalt::updater::UpdaterModule* updater_module,
#endif
- web::NavigatorUAData* user_agent_data,
- script::GlobalEnvironment* global_environment)
- : set_media_source_setting_func_(set_media_source_setting_func),
+ web::NavigatorUAData* user_agent_data,
+ script::GlobalEnvironment* global_environment)
+ : set_web_setting_func_(set_web_setting_func),
+ media_module_(media_module),
network_module_(network_module),
#if SB_IS(EVERGREEN)
updater_module_(updater_module),
@@ -37,6 +38,7 @@
}
bool H5vccSettings::Set(const std::string& name, int32 value) const {
+ const char kMediaPrefix[] = "Media.";
const char kNavigatorUAData[] = "NavigatorUAData";
const char kQUIC[] = "QUIC";
@@ -44,11 +46,16 @@
const char kUpdaterMinFreeSpaceBytes[] = "Updater.MinFreeSpaceBytes";
#endif
- if (set_media_source_setting_func_ &&
- set_media_source_setting_func_.Run(name, value)) {
+ if (set_web_setting_func_ && set_web_setting_func_.Run(name, value)) {
return true;
}
+ if (name.rfind(kMediaPrefix, 0) == 0) {
+ return media_module_ ? media_module_->SetConfiguration(
+ name.substr(strlen(kMediaPrefix)), value)
+ : false;
+ }
+
if (name.compare(kNavigatorUAData) == 0 && value == 1) {
global_environment_->BindTo("userAgentData", user_agent_data_, "navigator");
return true;
diff --git a/cobalt/h5vcc/h5vcc_settings.h b/cobalt/h5vcc/h5vcc_settings.h
index a73e59f..83a3adb 100644
--- a/cobalt/h5vcc/h5vcc_settings.h
+++ b/cobalt/h5vcc/h5vcc_settings.h
@@ -17,6 +17,7 @@
#include <string>
+#include "cobalt/media/media_module.h"
#include "cobalt/network/network_module.h"
#include "cobalt/script/global_environment.h"
#include "cobalt/script/wrappable.h"
@@ -35,9 +36,10 @@
class H5vccSettings : public script::Wrappable {
public:
typedef base::Callback<bool(const std::string& name, int value)>
- SetMediaSourceSettingFunc;
+ SetSettingFunc;
- H5vccSettings(const SetMediaSourceSettingFunc& set_media_source_setting_func,
+ H5vccSettings(const SetSettingFunc& set_web_setting_func,
+ cobalt::media::MediaModule* media_module,
cobalt::network::NetworkModule* network_module,
#if SB_IS(EVERGREEN)
cobalt::updater::UpdaterModule* updater_module,
@@ -53,7 +55,8 @@
DEFINE_WRAPPABLE_TYPE(H5vccSettings);
private:
- const SetMediaSourceSettingFunc set_media_source_setting_func_;
+ const SetSettingFunc set_web_setting_func_;
+ cobalt::media::MediaModule* media_module_ = nullptr;
cobalt::network::NetworkModule* network_module_ = nullptr;
#if SB_IS(EVERGREEN)
cobalt::updater::UpdaterModule* updater_module_ = nullptr;
diff --git a/cobalt/h5vcc/h5vcc_sso.cc b/cobalt/h5vcc/h5vcc_sso.cc
deleted file mode 100644
index bb94e73..0000000
--- a/cobalt/h5vcc/h5vcc_sso.cc
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright 2017 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "cobalt/h5vcc/h5vcc_sso.h"
-
-namespace cobalt {
-namespace h5vcc {
-
-H5vccSso::H5vccSso() : sso_(std::move(sso::CreateSSO())) {}
-
-std::string H5vccSso::GetApiKey() {
- DCHECK(sso_);
- return sso_->getApiKey();
-}
-
-std::string H5vccSso::GetOauthClientId() {
- DCHECK(sso_);
- return sso_->getOauthClientId();
-}
-
-std::string H5vccSso::GetOauthClientSecret() {
- DCHECK(sso_);
- return sso_->getOauthClientSecret();
-}
-
-} // namespace h5vcc
-} // namespace cobalt
diff --git a/cobalt/h5vcc/h5vcc_sso.h b/cobalt/h5vcc/h5vcc_sso.h
deleted file mode 100644
index 4755f7f..0000000
--- a/cobalt/h5vcc/h5vcc_sso.h
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright 2017 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef COBALT_H5VCC_H5VCC_SSO_H_
-#define COBALT_H5VCC_H5VCC_SSO_H_
-
-#include <memory>
-#include <string>
-
-#include "cobalt/script/wrappable.h"
-#include "cobalt/sso/sso_interface.h"
-
-namespace cobalt {
-namespace h5vcc {
-
-class H5vccSso : public script::Wrappable {
- public:
- H5vccSso();
-
- std::string GetApiKey();
- std::string GetOauthClientId();
- std::string GetOauthClientSecret();
-
- DEFINE_WRAPPABLE_TYPE(H5vccSso);
-
- private:
- std::unique_ptr<sso::SsoInterface> sso_;
-
- DISALLOW_COPY_AND_ASSIGN(H5vccSso);
-};
-
-} // namespace h5vcc
-} // namespace cobalt
-
-#endif // COBALT_H5VCC_H5VCC_SSO_H_
diff --git a/cobalt/h5vcc/h5vcc_sso.idl b/cobalt/h5vcc/h5vcc_sso.idl
deleted file mode 100644
index 6ac9757..0000000
--- a/cobalt/h5vcc/h5vcc_sso.idl
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright 2017 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-[
- Conditional=COBALT_ENABLE_SSO,
-] interface H5vccSso {
- DOMString getApiKey();
- DOMString getOauthClientId();
- DOMString getOauthClientSecret();
-};
diff --git a/cobalt/h5vcc/h5vcc_storage.cc b/cobalt/h5vcc/h5vcc_storage.cc
index cecdd0c..775026b 100644
--- a/cobalt/h5vcc/h5vcc_storage.cc
+++ b/cobalt/h5vcc/h5vcc_storage.cc
@@ -40,9 +40,7 @@
const char kTestFileName[] = "cache_test_file.json";
-const uint32 kWriteBufferSize = 1024 * 1024;
-
-const uint32 kReadBufferSize = 1024 * 1024;
+const uint32 kBufferSize = 16384; // 16 KB
H5vccStorageWriteTestResponse WriteTestResponse(std::string error = "",
uint32 bytes_written = 0) {
@@ -157,13 +155,13 @@
write_buf.append(test_string.substr(0, test_size % test_string.length()));
// Incremental Writes of test_data, copies SbWriteAll, using a maximum
- // kWriteBufferSize per write.
+ // kBufferSize per write.
uint32 total_bytes_written = 0;
do {
- auto bytes_written = test_file.Write(
- write_buf.data() + total_bytes_written,
- std::min(kWriteBufferSize, test_size - total_bytes_written));
+ auto bytes_written =
+ test_file.Write(write_buf.data() + total_bytes_written,
+ std::min(kBufferSize, test_size - total_bytes_written));
if (bytes_written <= 0) {
SbFileDelete(test_file_path.c_str());
return WriteTestResponse("SbWrite -1 return value error");
@@ -196,21 +194,21 @@
}
// Incremental Reads of test_data, copies SbReadAll, using a maximum
- // kReadBufferSize per write.
+ // kBufferSize per write.
uint32 total_bytes_read = 0;
do {
- char read_buf[kReadBufferSize];
+ auto read_buffer = std::make_unique<char[]>(kBufferSize);
auto bytes_read = test_file.Read(
- read_buf, std::min(kReadBufferSize, test_size - total_bytes_read));
+ read_buffer.get(), std::min(kBufferSize, test_size - total_bytes_read));
if (bytes_read <= 0) {
SbFileDelete(test_file_path.c_str());
return VerifyTestResponse("SbRead -1 return value error");
}
- // Verify read_buf equivalent to a repeated test_string.
+ // Verify read_buffer equivalent to a repeated test_string.
for (auto i = 0; i < bytes_read; ++i) {
- if (read_buf[i] !=
+ if (read_buffer.get()[i] !=
test_string[(total_bytes_read + i) % test_string.size()]) {
return VerifyTestResponse(
"File test data does not match with test data string");
@@ -235,7 +233,7 @@
if (!quota.has_other() || !quota.has_html() || !quota.has_css() ||
!quota.has_image() || !quota.has_font() || !quota.has_splash() ||
!quota.has_uncompiled_js() || !quota.has_compiled_js() ||
- !quota.has_cache_api()) {
+ !quota.has_cache_api() || !quota.has_service_worker_js()) {
return SetQuotaResponse(
"H5vccStorageResourceTypeQuotaBytesDictionary input parameter missing "
"required fields.");
@@ -244,7 +242,7 @@
if (quota.other() < 0 || quota.html() < 0 || quota.css() < 0 ||
quota.image() < 0 || quota.font() < 0 || quota.splash() < 0 ||
quota.uncompiled_js() < 0 || quota.compiled_js() < 0 ||
- quota.cache_api() < 0) {
+ quota.cache_api() < 0 || quota.service_worker_js() < 0) {
return SetQuotaResponse(
"H5vccStorageResourceTypeQuotaBytesDictionary input parameter fields "
"cannot have a negative value.");
@@ -253,7 +251,7 @@
auto quota_total = quota.other() + quota.html() + quota.css() +
quota.image() + quota.font() + quota.splash() +
quota.uncompiled_js() + quota.compiled_js() +
- quota.cache_api();
+ quota.cache_api() + quota.service_worker_js();
uint32_t max_quota_size = 24 * 1024 * 1024;
#if SB_API_VERSION >= 14
@@ -291,6 +289,8 @@
static_cast<uint32_t>(quota.compiled_js()));
SetAndSaveQuotaForBackend(disk_cache::kCacheApi,
static_cast<uint32_t>(quota.cache_api()));
+ SetAndSaveQuotaForBackend(disk_cache::kServiceWorkerScript,
+ static_cast<uint32_t>(quota.service_worker_js()));
return SetQuotaResponse("", true);
}
@@ -328,6 +328,8 @@
->GetMaxCacheStorageInBytes(disk_cache::kCompiledScript)
.value());
quota.set_cache_api(cache_backend_->GetQuota(disk_cache::kCacheApi));
+ quota.set_service_worker_js(
+ cache_backend_->GetQuota(disk_cache::kServiceWorkerScript));
uint32_t max_quota_size = 24 * 1024 * 1024;
#if SB_API_VERSION >= 14
diff --git a/cobalt/h5vcc/h5vcc_storage_resource_type_quota_bytes_dictionary.idl b/cobalt/h5vcc/h5vcc_storage_resource_type_quota_bytes_dictionary.idl
index 322f82d..c23a1b8 100644
--- a/cobalt/h5vcc/h5vcc_storage_resource_type_quota_bytes_dictionary.idl
+++ b/cobalt/h5vcc/h5vcc_storage_resource_type_quota_bytes_dictionary.idl
@@ -22,5 +22,6 @@
unsigned long uncompiled_js;
unsigned long compiled_js;
unsigned long cache_api;
+ unsigned long service_worker_js;
unsigned long total;
};
diff --git a/cobalt/layout/BUILD.gn b/cobalt/layout/BUILD.gn
index 5070a65..b95057e 100644
--- a/cobalt/layout/BUILD.gn
+++ b/cobalt/layout/BUILD.gn
@@ -117,6 +117,7 @@
"//cobalt/render_tree",
"//cobalt/render_tree:animations",
"//cobalt/ui_navigation",
+ "//cobalt/ui_navigation/scroll_engine",
"//cobalt/web_animations",
"//third_party/icu:icuuc",
]
diff --git a/cobalt/layout/box.cc b/cobalt/layout/box.cc
index a42730d..f7dc24c 100644
--- a/cobalt/layout/box.cc
+++ b/cobalt/layout/box.cc
@@ -60,8 +60,8 @@
using cobalt::render_tree::RoundedCorner;
using cobalt::render_tree::RoundedCorners;
using cobalt::render_tree::ViewportFilter;
-using cobalt::render_tree::animations::Animation;
using cobalt::render_tree::animations::AnimateNode;
+using cobalt::render_tree::animations::Animation;
namespace cobalt {
namespace layout {
@@ -258,11 +258,13 @@
}
RectLayoutUnit Box::GetTransformedBoxFromRootWithScroll(
- const RectLayoutUnit& box_from_margin_box) const {
+ const RectLayoutUnit& box_from_margin_box,
+ bool transform_forms_root) const {
// Get the transformed box from root while factoring in scrollLeft and
// scrollTop of intermediate containers.
return GetTransformedBox(
- GetMarginBoxTransformFromContainingBlockWithScroll(nullptr),
+ GetMarginBoxTransformFromContainingBlockWithScroll(
+ nullptr, transform_forms_root /* transform_forms_root */),
box_from_margin_box);
}
@@ -383,38 +385,51 @@
}
math::Matrix3F Box::GetMarginBoxTransformFromContainingBlockInternal(
- const ContainerBox* containing_block, bool include_scroll) const {
+ const ContainerBox* containing_block, bool transform_forms_root,
+ bool include_scroll) const {
math::Matrix3F transform = math::Matrix3F::Identity();
if (this == containing_block) {
return transform;
}
+ bool only_apply_scroll_transform = false;
+
// Walk up the containing block tree to build the transform matrix.
// The logic is similar to using ApplyTransformActionToCoordinate with exit
// transform but a matrix is calculated instead; logic analogous to
// GetMarginBoxOffsetFromRoot is also factored in.
for (const Box* box = this;;) {
// Factor in the margin box offset.
- transform =
- math::TranslateMatrix(
- box->margin_box_offset_from_containing_block().x().toFloat(),
- box->margin_box_offset_from_containing_block().y().toFloat()) *
- transform;
+ if (!only_apply_scroll_transform) {
+ transform =
+ math::TranslateMatrix(
+ box->margin_box_offset_from_containing_block().x().toFloat(),
+ box->margin_box_offset_from_containing_block().y().toFloat()) *
+ transform;
+ }
// Factor in the box's transform.
if (box->IsTransformed()) {
- Vector2dLayoutUnit transform_rect_offset =
- box->margin_box_offset_from_containing_block() +
- box->GetBorderBoxOffsetFromMarginBox();
- transform =
- GetCSSTransform(box->computed_style()->transform().get(),
- box->computed_style()->transform_origin().get(),
- math::RectF(transform_rect_offset.x().toFloat(),
- transform_rect_offset.y().toFloat(),
- box->GetBorderBoxWidth().toFloat(),
- box->GetBorderBoxHeight().toFloat()),
- box->ComputeUiNavFocusForTransform()) *
- transform;
+ if (transform_forms_root) {
+ if (!include_scroll) {
+ break;
+ }
+ only_apply_scroll_transform = true;
+ }
+ if (!only_apply_scroll_transform) {
+ Vector2dLayoutUnit transform_rect_offset =
+ box->margin_box_offset_from_containing_block() +
+ box->GetBorderBoxOffsetFromMarginBox();
+ transform =
+ GetCSSTransform(box->computed_style()->transform().get(),
+ box->computed_style()->transform_origin().get(),
+ math::RectF(transform_rect_offset.x().toFloat(),
+ transform_rect_offset.y().toFloat(),
+ box->GetBorderBoxWidth().toFloat(),
+ box->GetBorderBoxHeight().toFloat()),
+ box->ComputeUiNavFocusForTransform()) *
+ transform;
+ }
}
const ContainerBox* container = box->GetContainingBlock();
@@ -422,16 +437,18 @@
break;
}
- // Convert the transform into the container's coordinate space.
- Vector2dLayoutUnit containing_block_offset =
- box->GetContainingBlockOffsetFromItsContentBox(container) +
- container->GetContentBoxOffsetFromMarginBox();
- transform = math::TranslateMatrix(containing_block_offset.x().toFloat(),
- containing_block_offset.y().toFloat()) *
- transform;
+ if (!only_apply_scroll_transform) {
+ // Convert the transform into the container's coordinate space.
+ Vector2dLayoutUnit containing_block_offset =
+ box->GetContainingBlockOffsetFromItsContentBox(container) +
+ container->GetContentBoxOffsetFromMarginBox();
+ transform = math::TranslateMatrix(containing_block_offset.x().toFloat(),
+ containing_block_offset.y().toFloat()) *
+ transform;
+ }
// Factor in the container's scrollLeft / scrollTop as needed.
- if (include_scroll && container->ui_nav_item_ &&
+ if (container && include_scroll && container->ui_nav_item_ &&
container->ui_nav_item_->IsContainer()) {
float left, top;
container->ui_nav_item_->GetContentOffset(&left, &top);
@@ -447,13 +464,14 @@
math::Matrix3F Box::GetMarginBoxTransformFromContainingBlock(
const ContainerBox* containing_block) const {
return GetMarginBoxTransformFromContainingBlockInternal(
- containing_block, false /* include_scroll */);
+ containing_block, false /* transform_forms_root */,
+ false /* include_scroll */);
}
math::Matrix3F Box::GetMarginBoxTransformFromContainingBlockWithScroll(
- const ContainerBox* containing_block) const {
+ const ContainerBox* containing_block, bool transform_forms_root) const {
return GetMarginBoxTransformFromContainingBlockInternal(
- containing_block, true /* include_scroll */);
+ containing_block, transform_forms_root, true /* include_scroll */);
}
Vector2dLayoutUnit Box::GetMarginBoxOffsetFromRoot(
@@ -1211,7 +1229,9 @@
}
bool Box::IsUnderCoordinate(const Vector2dLayoutUnit& coordinate) const {
- RectLayoutUnit rect = GetBorderBoxFromRoot(true /*transform_forms_root*/);
+ RectLayoutUnit rect = GetTransformedBoxFromRootWithScroll(
+ GetBorderBoxFromMarginBox(), true /* transform_forms_root */);
+
bool res =
coordinate.x() >= rect.x() && coordinate.x() <= rect.x() + rect.width() &&
coordinate.y() >= rect.y() && coordinate.y() <= rect.y() + rect.height();
diff --git a/cobalt/layout/box.h b/cobalt/layout/box.h
index a36935a..83b4a27 100644
--- a/cobalt/layout/box.h
+++ b/cobalt/layout/box.h
@@ -281,7 +281,8 @@
RectLayoutUnit GetTransformedBoxFromRoot(
const RectLayoutUnit& box_from_margin_box) const;
RectLayoutUnit GetTransformedBoxFromRootWithScroll(
- const RectLayoutUnit& box_from_margin_box) const;
+ const RectLayoutUnit& box_from_margin_box,
+ bool transform_forms_root = false) const;
RectLayoutUnit GetTransformedBoxFromContainingBlock(
const ContainerBox* containing_block,
const RectLayoutUnit& box_from_margin_box) const;
@@ -372,7 +373,7 @@
math::Matrix3F GetMarginBoxTransformFromContainingBlock(
const ContainerBox* containing_block) const;
math::Matrix3F GetMarginBoxTransformFromContainingBlockWithScroll(
- const ContainerBox* containing_block) const;
+ const ContainerBox* containing_block, bool transform_forms_root) const;
Vector2dLayoutUnit GetMarginBoxOffsetFromRoot(
bool transform_forms_root) const;
@@ -861,7 +862,8 @@
// Get the transform for this box from the specified containing block (which
// may be null to indicate root).
math::Matrix3F GetMarginBoxTransformFromContainingBlockInternal(
- const ContainerBox* containing_block, bool include_scroll) const;
+ const ContainerBox* containing_block, bool transform_forms_root,
+ bool include_scroll) const;
// Some custom CSS transform functions require a UI navigation focus item as
// input. This computes the appropriate UI navigation item for this box's
diff --git a/cobalt/layout/box_generator.cc b/cobalt/layout/box_generator.cc
index a0e0438..6eee421 100644
--- a/cobalt/layout/box_generator.cc
+++ b/cobalt/layout/box_generator.cc
@@ -44,7 +44,7 @@
#include "cobalt/layout/used_style.h"
#include "cobalt/layout/white_space_processing.h"
#include "cobalt/loader/image/lottie_animation.h"
-#include "cobalt/media/base/video_frame_provider.h"
+#include "cobalt/media/base/decode_target_provider.h"
#include "cobalt/render_tree/image.h"
#include "cobalt/web_animations/keyframe_effect_read_only.h"
#include "starboard/decode_target.h"
@@ -52,19 +52,20 @@
namespace cobalt {
namespace layout {
-using media::VideoFrameProvider;
+using media::DecodeTargetProvider;
namespace {
scoped_refptr<render_tree::Image> GetVideoFrame(
- const scoped_refptr<VideoFrameProvider>& frame_provider,
+ const scoped_refptr<DecodeTargetProvider>& decode_target_provider,
render_tree::ResourceProvider* resource_provider) {
TRACE_EVENT0("cobalt::layout", "GetVideoFrame()");
- SbDecodeTarget decode_target = frame_provider->GetCurrentSbDecodeTarget();
+ SbDecodeTarget decode_target =
+ decode_target_provider->GetCurrentSbDecodeTarget();
if (SbDecodeTargetIsValid(decode_target)) {
return resource_provider->CreateImageFromSbDecodeTarget(decode_target);
} else {
- DCHECK(frame_provider);
+ DCHECK(decode_target_provider);
return NULL;
}
}
@@ -338,12 +339,12 @@
// If the optional is disengaged, then we don't know if punch out is enabled
// or not.
base::Optional<ReplacedBox::ReplacedBoxMode> replaced_box_mode;
- if (video_element->GetVideoFrameProvider()) {
- VideoFrameProvider::OutputMode output_mode =
- video_element->GetVideoFrameProvider()->GetOutputMode();
- if (output_mode != VideoFrameProvider::kOutputModeInvalid) {
+ if (video_element->GetDecodeTargetProvider()) {
+ DecodeTargetProvider::OutputMode output_mode =
+ video_element->GetDecodeTargetProvider()->GetOutputMode();
+ if (output_mode != DecodeTargetProvider::kOutputModeInvalid) {
replaced_box_mode =
- (output_mode == VideoFrameProvider::kOutputModePunchOut)
+ (output_mode == DecodeTargetProvider::kOutputModePunchOut)
? ReplacedBox::ReplacedBoxMode::kPunchOutVideo
: ReplacedBox::ReplacedBoxMode::kVideo;
}
@@ -351,8 +352,8 @@
ReplacedBoxGenerator replaced_box_generator(
video_element->css_computed_style_declaration(),
- video_element->GetVideoFrameProvider()
- ? base::Bind(GetVideoFrame, video_element->GetVideoFrameProvider(),
+ video_element->GetDecodeTargetProvider()
+ ? base::Bind(GetVideoFrame, video_element->GetDecodeTargetProvider(),
resource_provider)
: ReplacedBox::ReplaceImageCB(),
video_element->GetSetBoundsCB(), *paragraph_, text_position,
diff --git a/cobalt/layout/layout.cc b/cobalt/layout/layout.cc
index e446bf9..bd9ee75 100644
--- a/cobalt/layout/layout.cc
+++ b/cobalt/layout/layout.cc
@@ -174,6 +174,14 @@
}
}
+void UpdateUiNavItemBoundaries(const scoped_refptr<dom::Document>& document) {
+ TRACE_EVENT0("cobalt::layout", "UpdateUiNavItemBoundaries()");
+ const auto& ui_nav_elements = document->ui_navigation_elements();
+ for (dom::HTMLElement* html_element : ui_nav_elements) {
+ html_element->SetUiNavItemBounds();
+ }
+}
+
scoped_refptr<render_tree::Node> GenerateRenderTreeFromBoxTree(
UsedStyleProvider* used_style_provider,
LayoutStatTracker* layout_stat_tracker,
diff --git a/cobalt/layout/layout.h b/cobalt/layout/layout.h
index ce4a2ca..f4174d1 100644
--- a/cobalt/layout/layout.h
+++ b/cobalt/layout/layout.h
@@ -49,6 +49,9 @@
scoped_refptr<BlockLevelBlockContainerBox>* initial_containing_block,
bool clear_window_with_background_color);
+// Update all UI navigation elements with their scroll boundaries.
+void UpdateUiNavItemBoundaries(const scoped_refptr<dom::Document>& document);
+
// Generates the render tree (along with corresponding animations) of the box
// tree contained within the provided containing block.
scoped_refptr<render_tree::Node> GenerateRenderTreeFromBoxTree(
diff --git a/cobalt/layout/layout_manager.cc b/cobalt/layout/layout_manager.cc
index 2f82d74..6592f0e 100644
--- a/cobalt/layout/layout_manager.cc
+++ b/cobalt/layout/layout_manager.cc
@@ -284,6 +284,7 @@
line_break_iterator_.get(), character_break_iterator_.get(),
&initial_containing_block_, clear_window_with_background_color_);
are_computed_styles_and_box_tree_dirty_ = false;
+ layout::UpdateUiNavItemBoundaries(window_->document());
}
}
diff --git a/cobalt/layout/topmost_event_target.cc b/cobalt/layout/topmost_event_target.cc
index c4a1b72..e5b92ad 100644
--- a/cobalt/layout/topmost_event_target.cc
+++ b/cobalt/layout/topmost_event_target.cc
@@ -14,6 +14,8 @@
#include "cobalt/layout/topmost_event_target.h"
+#include <vector>
+
#include "base/optional.h"
#include "base/trace_event/trace_event.h"
#include "cobalt/base/token.h"
@@ -77,6 +79,53 @@
return NULL;
}
+scoped_refptr<dom::HTMLElement> FindFirstElementWithScrollType(
+ scoped_refptr<dom::HTMLElement> target_element,
+ ui_navigation::scroll_engine::ScrollType major_scroll_axis,
+ bool scrolling_right, bool scrolling_down) {
+ auto current_element = target_element;
+ bool scrolling_left = !scrolling_right;
+ bool scrolling_up = !scrolling_down;
+ bool horizontal_scroll_axis =
+ major_scroll_axis == ui_navigation::scroll_engine::ScrollType::Horizontal;
+ bool vertical_scroll_axis =
+ major_scroll_axis == ui_navigation::scroll_engine::ScrollType::Vertical;
+
+ while (true) {
+ float scroll_top_lower_bound;
+ float scroll_left_lower_bound;
+ float scroll_top_upper_bound;
+ float scroll_left_upper_bound;
+ float offset_x;
+ float offset_y;
+
+ if (!current_element->parent_element()) {
+ break;
+ }
+ current_element = current_element->parent_element()->AsHTMLElement();
+ auto current_ui_nav_item = current_element->GetUiNavItem();
+ if (!current_ui_nav_item) continue;
+
+ current_ui_nav_item->GetBounds(
+ &scroll_top_lower_bound, &scroll_left_lower_bound,
+ &scroll_top_upper_bound, &scroll_left_upper_bound);
+ current_ui_nav_item->GetContentOffset(&offset_x, &offset_y);
+
+ bool can_scroll_left = scroll_left_lower_bound < offset_x;
+ bool can_scroll_right = scroll_left_upper_bound > offset_x;
+ bool can_scroll_up = scroll_top_lower_bound < offset_y;
+ bool can_scroll_down = scroll_top_upper_bound > offset_y;
+
+ if ((scrolling_left && can_scroll_left && horizontal_scroll_axis) ||
+ (scrolling_right && can_scroll_right && horizontal_scroll_axis) ||
+ (scrolling_up && can_scroll_up && vertical_scroll_axis) ||
+ (scrolling_down && can_scroll_down && vertical_scroll_axis)) {
+ return current_element;
+ }
+ }
+ return nullptr;
+}
+
} // namespace
void TopmostEventTarget::ConsiderElement(dom::Element* element,
const math::Vector2dF& coordinate) {
@@ -129,6 +178,112 @@
}
}
+void TopmostEventTarget::CancelScrollsInParentNavItems(
+ scoped_refptr<dom::HTMLElement> target_element) {
+ // Cancel any scrolls in the tree.
+ std::vector<scoped_refptr<ui_navigation::NavItem>> scrolls_to_cancel;
+ auto current_element = target_element;
+ while (true) {
+ if (!current_element->parent_element()) {
+ break;
+ }
+ current_element = current_element->parent_element()->AsHTMLElement();
+ auto current_ui_nav_item = current_element->GetUiNavItem();
+ if (current_ui_nav_item) {
+ scrolls_to_cancel.push_back(current_ui_nav_item);
+ }
+ }
+
+ scroll_engine_->thread()->message_loop()->task_runner()->PostTask(
+ FROM_HERE,
+ base::Bind(&ui_navigation::scroll_engine::ScrollEngine::
+ CancelActiveScrollsForNavItems,
+ base::Unretained(scroll_engine_), scrolls_to_cancel));
+}
+
+void TopmostEventTarget::HandleScrollState(
+ scoped_refptr<dom::HTMLElement> target_element,
+ const dom::PointerEvent* pointer_event, dom::PointerState* pointer_state,
+ dom::PointerEventInit* event_init) {
+ // On pointer down, cancel any scrolls happening for UI nav items above
+ // that element. Additionally, save the pointer coordinates.
+ //
+ // On pointer move, check if we've reached the threshold to start
+ // scrolling. If we have, find the first scroll container we can scroll.
+ // Then send that scroll container, initial pointer event coords, current
+ // pointer event coords, scroll direction.
+ bool pointer_type_is_accepted = pointer_event->pointer_type() == "mouse" ||
+ pointer_event->pointer_type() == "pen" ||
+ pointer_event->pointer_type() == "touch";
+ if (!scroll_engine_ || !pointer_event || !pointer_type_is_accepted) {
+ return;
+ }
+
+ bool should_clear_pointer_state =
+ pointer_event->type() == base::Tokens::pointerup();
+
+ auto pointer_id = pointer_event->pointer_id();
+ auto pointer_coordinates =
+ math::Vector2dF(pointer_event->client_x(), pointer_event->client_y());
+
+ if (pointer_event->type() == base::Tokens::pointerdown()) {
+ CancelScrollsInParentNavItems(target_element);
+ pointer_state->SetClientCoordinates(pointer_id, pointer_coordinates);
+ pointer_state->SetClientTimeStamp(pointer_id, pointer_event->time_stamp());
+ return;
+ }
+
+ auto initial_coordinates = pointer_state->GetClientCoordinates(pointer_id);
+ auto initial_time_stamp = pointer_state->GetClientTimeStamp(pointer_id);
+ if (pointer_event->type() == base::Tokens::pointermove() &&
+ initial_coordinates.has_value() && initial_time_stamp.has_value()) {
+ cobalt::math::Vector2dF drag_vector =
+ initial_coordinates.value() - pointer_coordinates;
+ float x = drag_vector.x();
+ float y = drag_vector.y();
+
+ if (drag_vector.Length() >=
+ ui_navigation::scroll_engine::kDragDistanceThreshold) {
+ // Get major scroll direction.
+ ui_navigation::scroll_engine::ScrollType scroll_type =
+ std::abs(x) > std::abs(y)
+ ? ui_navigation::scroll_engine::ScrollType::Horizontal
+ : ui_navigation::scroll_engine::ScrollType::Vertical;
+ auto element_to_scroll = FindFirstElementWithScrollType(
+ target_element, scroll_type, x > 0, y > 0);
+ if (!element_to_scroll) {
+ return;
+ }
+
+ const scoped_refptr<dom::Window>& view = event_init->view();
+ element_to_scroll->DispatchEvent(new dom::PointerEvent(
+ base::Tokens::pointercancel(), web::Event::kBubbles,
+ web::Event::kNotCancelable, view, *event_init));
+ element_to_scroll->DispatchEvent(
+ new dom::PointerEvent(base::Tokens::pointerout(), view, *event_init));
+ element_to_scroll->DispatchEvent(new dom::PointerEvent(
+ base::Tokens::pointerleave(), web::Event::kNotBubbles,
+ web::Event::kNotCancelable, view, *event_init));
+ pointer_state->SetWasCancelled(pointer_id);
+
+ should_clear_pointer_state = true;
+ scroll_engine_->thread()->message_loop()->task_runner()->PostTask(
+ FROM_HERE,
+ base::Bind(
+ &ui_navigation::scroll_engine::ScrollEngine::HandleScrollStart,
+ base::Unretained(scroll_engine_),
+ element_to_scroll->GetUiNavItem(), scroll_type, pointer_id,
+ initial_coordinates.value(), initial_time_stamp.value(),
+ pointer_coordinates, pointer_event->time_stamp()));
+ }
+ }
+
+ if (should_clear_pointer_state) {
+ pointer_state->ClearClientCoordinates(pointer_id);
+ pointer_state->ClearTimeStamp(pointer_id);
+ }
+}
+
namespace {
// Return the nearest common ancestor of previous_element and target_element
scoped_refptr<dom::Element> GetNearestCommonAncestor(
@@ -342,6 +497,10 @@
}
} // namespace
+TopmostEventTarget::TopmostEventTarget(
+ ui_navigation::scroll_engine::ScrollEngine* scroll_engine)
+ : scroll_engine_(scroll_engine) {}
+
void TopmostEventTarget::MaybeSendPointerEvents(
const scoped_refptr<web::Event>& event) {
TRACE_EVENT0("cobalt::layout",
@@ -405,6 +564,11 @@
target_element = FindTopmostEventTarget(view->document(), coordinate);
}
+ if (target_element && pointer_event) {
+ HandleScrollState(target_element, pointer_event, pointer_state,
+ &event_init);
+ }
+
scoped_refptr<dom::HTMLElement> previous_html_element(
previous_html_element_weak_);
@@ -417,8 +581,16 @@
target_element, nearest_common_ancestor,
&event_init);
+ bool event_was_cancelled = pointer_event && pointer_state->GetWasCancelled(
+ pointer_event->pointer_id());
+ if (pointer_event && pointer_event->type() == base::Tokens::pointerup()) {
+ pointer_state->ClearWasCancelled(pointer_event->pointer_id());
+ }
+
if (target_element) {
- target_element->DispatchEvent(event);
+ if (!event_was_cancelled) {
+ target_element->DispatchEvent(event);
+ }
}
if (pointer_event) {
@@ -432,7 +604,7 @@
pointer_state->ClearPendingPointerCaptureTargetOverride(
pointer_event->pointer_id());
}
- if (target_element && !is_touchpad_event) {
+ if (target_element && !is_touchpad_event && !event_was_cancelled) {
SendCompatibilityMappingMouseEvent(target_element, event, pointer_event,
event_init,
&mouse_event_prevent_flags_);
diff --git a/cobalt/layout/topmost_event_target.h b/cobalt/layout/topmost_event_target.h
index 078e179..1d1dd9b 100644
--- a/cobalt/layout/topmost_event_target.h
+++ b/cobalt/layout/topmost_event_target.h
@@ -21,10 +21,12 @@
#include "base/memory/weak_ptr.h"
#include "cobalt/dom/document.h"
#include "cobalt/dom/html_element.h"
+#include "cobalt/dom/pointer_event.h"
#include "cobalt/layout/box.h"
#include "cobalt/layout/layout_boxes.h"
#include "cobalt/math/vector2d.h"
#include "cobalt/math/vector2d_f.h"
+#include "cobalt/ui_navigation/scroll_engine/scroll_engine.h"
#include "cobalt/web/event.h"
namespace cobalt {
@@ -32,7 +34,8 @@
class TopmostEventTarget {
public:
- TopmostEventTarget() {}
+ explicit TopmostEventTarget(
+ ui_navigation::scroll_engine::ScrollEngine* scroll_engine);
void MaybeSendPointerEvents(const scoped_refptr<web::Event>& event);
@@ -41,6 +44,13 @@
const scoped_refptr<dom::Document>& document,
const math::Vector2dF& coordinate);
+ void HandleScrollState(scoped_refptr<dom::HTMLElement> target_element,
+ const dom::PointerEvent* pointer_event,
+ dom::PointerState* pointer_state,
+ dom::PointerEventInit* event_init);
+ void CancelScrollsInParentNavItems(
+ scoped_refptr<dom::HTMLElement> target_element);
+
void ConsiderElement(dom::Element* element,
const math::Vector2dF& coordinate);
void ConsiderBoxes(const scoped_refptr<dom::HTMLElement>& html_element,
@@ -52,6 +62,8 @@
scoped_refptr<Box> box_;
Box::RenderSequence render_sequence_;
+ ui_navigation::scroll_engine::ScrollEngine* scroll_engine_;
+
// This map stores the pointer types for which the 'prevent mouse event' flag
// has been set as part of the compatibility mapping steps defined at
// https://www.w3.org/TR/pointerevents/#compatibility-mapping-with-mouse-events.
diff --git a/cobalt/layout_tests/layout_snapshot.cc b/cobalt/layout_tests/layout_snapshot.cc
index 88d07f3..e137530 100644
--- a/cobalt/layout_tests/layout_snapshot.cc
+++ b/cobalt/layout_tests/layout_snapshot.cc
@@ -27,6 +27,7 @@
#include "cobalt/dom/window.h"
#include "cobalt/network/network_module.h"
#include "cobalt/render_tree/resource_provider.h"
+#include "cobalt/web/web_settings.h"
#include "starboard/window.h"
using cobalt::cssom::ViewportSize;
@@ -73,6 +74,7 @@
// Some layout tests test Content Security Policy; allow HTTP so we
// don't interfere.
net_options.https_requirement = network::kHTTPSOptional;
+ web::WebSettingsImpl web_settings;
network::NetworkModule network_module(
browser::CreateUserAgentString(
browser::GetUserAgentPlatformInfoFromSystem()),
@@ -91,6 +93,7 @@
// we take advantage of the convenience of inline script tags.
web_module_options.enable_inline_script_warnings = false;
+ web_module_options.web_options.web_settings = &web_settings;
web_module_options.web_options.network_module = &network_module;
// Prepare a slot for our results to be placed when ready.
@@ -99,7 +102,7 @@
// Create the WebModule and wait for a layout to occur.
browser::WebModule web_module("SnapshotURL");
web_module.Run(
- url, base::kApplicationStateStarted,
+ url, base::kApplicationStateStarted, nullptr /* scroll_engine */,
base::Bind(&WebModuleOnRenderTreeProducedCallback, &results, &run_loop,
base::MessageLoop::current()),
base::Bind(&WebModuleErrorCallback, &run_loop,
diff --git a/cobalt/layout_tests/testdata/BUILD.gn b/cobalt/layout_tests/testdata/BUILD.gn
index 5741156..494c692 100644
--- a/cobalt/layout_tests/testdata/BUILD.gn
+++ b/cobalt/layout_tests/testdata/BUILD.gn
@@ -1399,8 +1399,10 @@
"css3-fonts/5-2-use-numerical-font-weights-in-family-face-matching.html",
"css3-fonts/5-2-use-specified-font-family-if-available-expected.png",
"css3-fonts/5-2-use-specified-font-family-if-available.html",
- "css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-expected.png",
- "css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found.html",
+ "css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-emoji-expected.png",
+ "css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-emoji.html",
+ "css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-non-emoji-expected.png",
+ "css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-non-emoji.html",
"css3-fonts/color-emojis-should-render-properly-expected.png",
"css3-fonts/color-emojis-should-render-properly.html",
"css3-fonts/layout_tests.txt",
diff --git a/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-emoji-expected.png b/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-emoji-expected.png
new file mode 100644
index 0000000..3b06f5b
--- /dev/null
+++ b/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-emoji-expected.png
Binary files differ
diff --git a/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-emoji.html b/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-emoji.html
new file mode 100644
index 0000000..a640c4e
--- /dev/null
+++ b/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-emoji.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<!--
+ | If no font family is specified that supports the characters, determine the
+ | font to use via system font fallback.
+ | https://www.w3.org/TR/css3-fonts/#system-font-fallback
+ -->
+<html>
+<head>
+ <meta charset="utf-8">
+ <style>
+ body {
+ margin: 0;
+ font-family: Roboto;
+ font-size: 30px;
+ font-weight: normal;
+ color: #fff;
+ }
+ .containing-block {
+ background-color: #03a9f4;
+ width: 500px;
+ }
+ </style>
+</head>
+<body>
+ <div class="containing-block">
+ <span>🍔🌞😜</span>
+ </div>
+</body>
+</html>
diff --git a/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-expected.png b/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-expected.png
deleted file mode 100644
index 8560d78..0000000
--- a/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-expected.png
+++ /dev/null
Binary files differ
diff --git a/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-non-emoji-expected.png b/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-non-emoji-expected.png
new file mode 100644
index 0000000..49fa021
--- /dev/null
+++ b/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-non-emoji-expected.png
Binary files differ
diff --git a/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-non-emoji.html b/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-non-emoji.html
new file mode 100644
index 0000000..68f6fa8
--- /dev/null
+++ b/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found-non-emoji.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<!--
+ | If no font family is specified that supports the characters, determine the
+ | font to use via system font fallback.
+ | https://www.w3.org/TR/css3-fonts/#system-font-fallback
+ -->
+<html>
+<head>
+ <meta charset="utf-8">
+ <style>
+ body {
+ margin: 0;
+ font-family: Roboto;
+ font-size: 30px;
+ font-weight: normal;
+ color: #fff;
+ }
+ .containing-block {
+ background-color: #03a9f4;
+ width: 500px;
+ }
+ </style>
+</head>
+<body>
+ <div class="containing-block">
+ <span>בְּרֵאשִׁ֖ית The Hegemony Consul sat on the balcony of his ebony spaceship. δῖος δῖοσ金魚 中國哲學書電子化計劃計劃 وَأَنْ يَعْمَلَ عَلَى مَا يَجْلِبُ السَّعَادَةَ لِلنَّاسِ . ولَ ☃ ᠦᡈᠣᡄ➤</span>
+ </div>
+</body>
+</html>
diff --git a/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found.html b/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found.html
deleted file mode 100644
index 899ee15..0000000
--- a/cobalt/layout_tests/testdata/css3-fonts/5-2-use-system-fallback-if-no-matching-family-is-found.html
+++ /dev/null
@@ -1,29 +0,0 @@
-<!DOCTYPE html>
-<!--
- | If no font family is specified that supports the characters, determine the
- | font to use via system font fallback.
- | https://www.w3.org/TR/css3-fonts/#system-font-fallback
- -->
-<html>
-<head>
- <meta charset="utf-8">
- <style>
- body {
- margin: 0;
- font-family: Roboto;
- font-size: 30px;
- font-weight: normal;
- color: #fff;
- }
- .containing-block {
- background-color: #03a9f4;
- width: 500px;
- }
- </style>
-</head>
-<body>
- <div class="containing-block">
- <span>🍔🌞 בְּרֵאשִׁ֖ית The Hegemony Consul sat on the balcony of his ebony spaceship. δῖος δῖοσ金魚 😜 中國哲學書電子化計劃計劃 وَأَنْ يَعْمَلَ عَلَى مَا يَجْلِبُ السَّعَادَةَ لِلنَّاسِ . ولَ ☃ ᠦᡈᠣᡄ➤</span>
- </div>
-</body>
-</html>
diff --git a/cobalt/layout_tests/testdata/css3-fonts/layout_tests.txt b/cobalt/layout_tests/testdata/css3-fonts/layout_tests.txt
index 8c922d4..554c89d 100644
--- a/cobalt/layout_tests/testdata/css3-fonts/layout_tests.txt
+++ b/cobalt/layout_tests/testdata/css3-fonts/layout_tests.txt
@@ -10,7 +10,8 @@
5-2-use-first-available-listed-font-family
5-2-use-numerical-font-weights-in-family-face-matching
5-2-use-specified-font-family-if-available
-5-2-use-system-fallback-if-no-matching-family-is-found
+5-2-use-system-fallback-if-no-matching-family-is-found-emoji
+5-2-use-system-fallback-if-no-matching-family-is-found-non-emoji
color-emojis-should-render-properly
synthetic-bolding-should-not-occur-on-bold-font
synthetic-bolding-should-occur-on-non-bold-font
diff --git a/cobalt/layout_tests/testdata/web-platform-tests/service-workers/web_platform_tests.txt b/cobalt/layout_tests/testdata/web-platform-tests/service-workers/web_platform_tests.txt
index ade7e63..c353c7e 100644
--- a/cobalt/layout_tests/testdata/web-platform-tests/service-workers/web_platform_tests.txt
+++ b/cobalt/layout_tests/testdata/web-platform-tests/service-workers/web_platform_tests.txt
@@ -1,21 +1,231 @@
# Service Worker API tests
-cache-storage/common.https.html, PASS
-cache-storage/serviceworker/cache-add.https.html, DISABLE
-cache-storage/serviceworker/cache-delete.https.html, DISABLE
-cache-storage/serviceworker/cache-match.https.html, DISABLE
-cache-storage/serviceworker/cache-put.https.html, DISABLE
-cache-storage/serviceworker/cache-storage-match.https.html, DISABLE
-cache-storage/serviceworker/cache-storage.https.html, DISABLE
-cache-storage/worker/cache-add.https.html, PASS
-cache-storage/worker/cache-delete.https.html, PASS
-cache-storage/worker/cache-match.https.html, PASS
-cache-storage/worker/cache-put.https.html, PASS
-cache-storage/worker/cache-storage-match.https.html, PASS
-cache-storage/worker/cache-storage.https.html, PASS
-cache-storage/window/cache-add.https.html, PASS
-cache-storage/window/cache-delete.https.html, PASS
-cache-storage/window/cache-match.https.html, PASS
-cache-storage/window/cache-put.https.html, PASS
-cache-storage/window/cache-storage-match.https.html, PASS
-cache-storage/window/cache-storage.https.html, PASS
+service-worker/about-blank-replacement.https.html, DISABLE
+service-worker/activate-event-after-install-state-change.https.html, DISABLE
+service-worker/activation-after-registration.https.html, DISABLE
+service-worker/activation.https.html, DISABLE
+service-worker/active.https.html, DISABLE
+service-worker/claim-affect-other-registration.https.html, DISABLE
+service-worker/claim-fetch.https.html, DISABLE
+service-worker/claim-not-using-registration.https.html, DISABLE
+service-worker/claim-shared-worker-fetch.https.html, DISABLE
+service-worker/claim-using-registration.https.html, DISABLE
+service-worker/claim-with-redirect.https.html, DISABLE
+service-worker/claim-worker-fetch.https.html, DISABLE
+service-worker/client-id.https.html, DISABLE
+service-worker/client-navigate.https.html, DISABLE
+service-worker/clients-get-client-types.https.html, DISABLE
+service-worker/clients-get-cross-origin.https.html, DISABLE
+service-worker/clients-get.https.html, DISABLE
+service-worker/clients-get-resultingClientId.https.html, DISABLE
+service-worker/clients-matchall-blob-url-worker.https.html, DISABLE
+service-worker/clients-matchall-client-types.https.html, DISABLE
+service-worker/clients-matchall-exact-controller.https.html, DISABLE
+service-worker/clients-matchall-frozen.https.html, DISABLE
+service-worker/clients-matchall.https.html, DISABLE
+service-worker/clients-matchall-include-uncontrolled.https.html, DISABLE
+service-worker/clients-matchall-on-evaluation.https.html, DISABLE
+service-worker/clients-matchall-order.https.html, DISABLE
+service-worker/client-url-of-blob-url-worker.https.html, DISABLE
+service-worker/controller-on-disconnect.https.html, DISABLE
+service-worker/controller-on-load.https.html, DISABLE
+service-worker/controller-on-reload.https.html, DISABLE
+service-worker/controller-with-no-fetch-event-handler.https.html, DISABLE
+service-worker/credentials.https.html, DISABLE
+service-worker/data-iframe.html, DISABLE
+service-worker/data-transfer-files.https.html, DISABLE
+service-worker/dedicated-worker-service-worker-interception.https.html, DISABLE
+service-worker/detached-context.https.html, DISABLE
+service-worker/embed-and-object-are-not-intercepted.https.html, DISABLE
+service-worker/extendable-event-async-waituntil.https.html, DISABLE
+service-worker/extendable-event-waituntil.https.html, DISABLE
+service-worker/fetch-audio-tainting.https.html, DISABLE
+service-worker/fetch-canvas-tainting-double-write.https.html, DISABLE
+service-worker/fetch-canvas-tainting-image-cache.https.html, DISABLE
+service-worker/fetch-canvas-tainting-image.https.html, DISABLE
+service-worker/fetch-canvas-tainting-video-cache.https.html, DISABLE
+service-worker/fetch-canvas-tainting-video.https.html, DISABLE
+service-worker/fetch-canvas-tainting-video-with-range-request.https.html, DISABLE
+service-worker/fetch-cors-exposed-header-names.https.html, DISABLE
+service-worker/fetch-cors-xhr.https.html, DISABLE
+service-worker/fetch-csp.https.html, DISABLE
+service-worker/fetch-error.https.html, DISABLE
+service-worker/fetch-event-add-async.https.html, DISABLE
+service-worker/fetch-event-after-navigation-within-page.https.html, DISABLE
+service-worker/fetch-event-async-respond-with.https.html, DISABLE
+service-worker/fetch-event-handled.https.html, DISABLE
+service-worker/fetch-event.https.h2.html, DISABLE
+service-worker/fetch-event.https.html, DISABLE
+service-worker/fetch-event-is-history-backward-navigation-manual.https.html, DISABLE
+service-worker/fetch-event-is-history-forward-navigation-manual.https.html, DISABLE
+service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html, DISABLE
+service-worker/fetch-event-is-reload-navigation-manual.https.html, DISABLE
+service-worker/fetch-event-network-error.https.html, DISABLE
+service-worker/fetch-event-redirect.https.html, DISABLE
+service-worker/fetch-event-referrer-policy.https.html, DISABLE
+service-worker/fetch-event-respond-with-argument.https.html, DISABLE
+service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html, DISABLE
+service-worker/fetch-event-respond-with-custom-response.https.html, DISABLE
+service-worker/fetch-event-respond-with-partial-stream.https.html, DISABLE
+service-worker/fetch-event-respond-with-readable-stream-chunk.https.html, DISABLE
+service-worker/fetch-event-respond-with-readable-stream.https.html, DISABLE
+service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html, DISABLE
+service-worker/fetch-event-respond-with-stops-propagation.https.html, DISABLE
+service-worker/fetch-event-throws-after-respond-with.https.html, DISABLE
+service-worker/fetch-event-within-sw.https.html, DISABLE
+service-worker/fetch-event-within-sw-manual.https.html, DISABLE
+service-worker/fetch-frame-resource.https.html, DISABLE
+service-worker/fetch-header-visibility.https.html, DISABLE
+service-worker/fetch-mixed-content-to-inscope.https.html, DISABLE
+service-worker/fetch-mixed-content-to-outscope.https.html, DISABLE
+service-worker/fetch-request-css-base-url.https.html, DISABLE
+service-worker/fetch-request-css-cross-origin.https.html, DISABLE
+service-worker/fetch-request-css-images.https.html, DISABLE
+service-worker/fetch-request-fallback.https.html, DISABLE
+service-worker/fetch-request-no-freshness-headers.https.html, DISABLE
+service-worker/fetch-request-redirect.https.html, DISABLE
+service-worker/fetch-request-resources.https.html, DISABLE
+service-worker/fetch-request-xhr.https.html, DISABLE
+service-worker/fetch-request-xhr-sync-error.https.window.js, DISABLE
+service-worker/fetch-request-xhr-sync.https.html, DISABLE
+service-worker/fetch-request-xhr-sync-on-worker.https.html, DISABLE
+service-worker/fetch-response-taint.https.html, DISABLE
+service-worker/fetch-response-xhr.https.html, DISABLE
+service-worker/fetch-waits-for-activate.https.html, DISABLE
+service-worker/getregistration.https.html, DISABLE
+service-worker/getregistrations.https.html, DISABLE
+service-worker/global-serviceworker.https.any.js, DISABLE
+service-worker/historical.https.any.js, DISABLE
+service-worker/http-to-https-redirect-and-register.https.html, DISABLE
+service-worker/immutable-prototype-serviceworker.https.html, DISABLE
+service-worker/import-scripts-cross-origin.https.html, DISABLE
+service-worker/import-scripts-mime-types.https.html, DISABLE
+service-worker/import-scripts-redirect.https.html, DISABLE
+service-worker/import-scripts-resource-map.https.html, DISABLE
+service-worker/import-scripts-updated-flag.https.html, DISABLE
+service-worker/indexeddb.https.html, DISABLE
+service-worker/install-event-type.https.html, DISABLE
+service-worker/installing.https.html, DISABLE
+service-worker/interface-requirements-sw.https.html, DISABLE
+service-worker/invalid-blobtype.https.html, DISABLE
+service-worker/invalid-header.https.html, DISABLE
+service-worker/iso-latin1-header.https.html, DISABLE
+service-worker/local-url-inherit-controller.https.html, DISABLE
+service-worker/mime-sniffing.https.html, DISABLE
+service-worker/multi-globals, DISABLE
+service-worker/multipart-image.https.html, DISABLE
+service-worker/multiple-register.https.html, DISABLE
+service-worker/multiple-update.https.html, DISABLE
+service-worker/navigate-window.https.html, DISABLE
+service-worker/navigation-headers.https.html, DISABLE
+service-worker/navigation-preload, DISABLE
+service-worker/navigation-redirect-body.https.html, DISABLE
+service-worker/navigation-redirect.https.html, DISABLE
+service-worker/navigation-redirect-resolution.https.html, DISABLE
+service-worker/navigation-redirect-to-http.https.html, DISABLE
+service-worker/navigation-sets-cookie.https.html, DISABLE
+service-worker/navigation-timing-extended.https.html, DISABLE
+service-worker/navigation-timing.https.html, DISABLE
+service-worker/nested-blob-url-workers.https.html, DISABLE
+service-worker/next-hop-protocol.https.html, DISABLE
+service-worker/no-dynamic-import.any.js, DISABLE
+service-worker/no-dynamic-import-in-module.any.js, DISABLE
+service-worker/onactivate-script-error.https.html, DISABLE
+service-worker/oninstall-script-error.https.html, DISABLE
+service-worker/opaque-response-preloaded.https.html, DISABLE
+service-worker/opaque-script.https.html, DISABLE
+service-worker/partitioned-claim.tentative.https.html, DISABLE
+service-worker/partitioned-getRegistrations.tentative.https.html, DISABLE
+service-worker/partitioned-matchAll.tentative.https.html, DISABLE
+service-worker/partitioned.tentative.https.html, DISABLE
+service-worker/performance-timeline.https.html, DISABLE
+service-worker/postmessage-blob-url.https.html, DISABLE
+service-worker/postmessage-from-waiting-serviceworker.https.html, DISABLE
+service-worker/postmessage.https.html, DISABLE
+service-worker/postmessage-msgport-to-client.https.html, DISABLE
+service-worker/postmessage-to-client.https.html, DISABLE
+service-worker/postmessage-to-client-message-queue.https.html, DISABLE
+service-worker/ready.https.window.js, DISABLE
+service-worker/redirected-response.https.html, DISABLE
+service-worker/referer.https.html, DISABLE
+service-worker/referrer-policy-header.https.html, DISABLE
+service-worker/referrer-toplevel-script-fetch.https.html, DISABLE
+service-worker/register-closed-window.https.html, DISABLE
+service-worker/register-default-scope.https.html, PASS
+service-worker/register-same-scope-different-script-url.https.html, DISABLE
+service-worker/register-wait-forever-in-install-worker.https.html, DISABLE
+service-worker/registration-basic.https.html, DISABLE
+service-worker/registration-end-to-end.https.html, DISABLE
+service-worker/registration-events.https.html, DISABLE
+service-worker/registration-iframe.https.html, DISABLE
+service-worker/registration-mime-types.https.html, DISABLE
+service-worker/registration-schedule-job.https.html, DISABLE
+service-worker/registration-scope.https.html, DISABLE
+service-worker/registration-scope-module-static-import.https.html, DISABLE
+service-worker/registration-script.https.html, DISABLE
+service-worker/registration-script-module.https.html, DISABLE
+service-worker/registration-script-url.https.html, DISABLE
+service-worker/registration-security-error.https.html, DISABLE
+service-worker/registration-service-worker-attributes.https.html, DISABLE
+service-worker/registration-updateviacache.https.html, DISABLE
+service-worker/rejections.https.html, PASS
+service-worker/request-end-to-end.https.html, DISABLE
+service-worker/resource-timing-bodySize.https.html, DISABLE
+service-worker/resource-timing-cross-origin.https.html, DISABLE
+service-worker/resource-timing-fetch-variants.https.html, DISABLE
+service-worker/resource-timing.sub.https.html, DISABLE
+service-worker/respond-with-body-accessed-response.https.html, DISABLE
+service-worker/same-site-cookies.https.html, DISABLE
+service-worker/sandboxed-iframe-fetch-event.https.html, DISABLE
+service-worker/sandboxed-iframe-navigator-serviceworker.https.html, DISABLE
+service-worker/secure-context.https.html, DISABLE
+service-worker/Service-Worker-Allowed-header.https.html, DISABLE
+service-worker/service-worker-csp-connect.https.html, DISABLE
+service-worker/service-worker-csp-default.https.html, DISABLE
+service-worker/service-worker-csp-script.https.html, DISABLE
+service-worker/service-worker-header.https.html, DISABLE
+service-worker/serviceworker-message-event-historical.https.html, DISABLE
+service-worker/serviceworkerobject-scripturl.https.html, PASS
+service-worker/skip-waiting.https.html, DISABLE
+service-worker/skip-waiting-installed.https.html, DISABLE
+service-worker/skip-waiting-using-registration.https.html, DISABLE
+service-worker/skip-waiting-without-client.https.html, DISABLE
+service-worker/skip-waiting-without-using-registration.https.html, DISABLE
+service-worker/state.https.html, DISABLE
+service-worker/svg-target-reftest.https.html, DISABLE
+service-worker/synced-state.https.html, DISABLE
+service-worker/uncontrolled-page.https.html, DISABLE
+service-worker/unregister-controller.https.html, DISABLE
+service-worker/unregister.https.html, PASS
+service-worker/unregister-immediately-before-installed.https.html, DISABLE
+service-worker/unregister-immediately-during-extendable-events.https.html, DISABLE
+service-worker/unregister-immediately.https.html, DISABLE
+service-worker/unregister-then-register.https.html, DISABLE
+service-worker/unregister-then-register-new-script.https.html, DISABLE
+service-worker/update-after-navigation-fetch-event.https.html, DISABLE
+service-worker/update-after-navigation-redirect.https.html, DISABLE
+service-worker/update-after-oneday.https.html, DISABLE
+service-worker/update-bytecheck-cors-import.https.html, DISABLE
+service-worker/update-bytecheck.https.html, DISABLE
+service-worker/update.https.html, DISABLE
+service-worker/update-import-scripts.https.html, DISABLE
+service-worker/update-missing-import-scripts.https.html, DISABLE
+service-worker/update-module-request-mode.https.html, DISABLE
+service-worker/update-no-cache-request-headers.https.html, DISABLE
+service-worker/update-not-allowed.https.html, DISABLE
+service-worker/pdate-on-navigation.https.html, DISABLE
+service-worker/update-recovery.https.html, DISABLE
+service-worker/update-registration-with-type.https.html, DISABLE
+service-worker/update-result.https.html, DISABLE
+service-worker/waiting.https.html, DISABLE
+service-worker/websocket.https.html, DISABLE
+service-worker/websocket-in-service-worker.https.html, DISABLE
+service-worker/webvtt-cross-origin.https.html, DISABLE
+service-worker/windowclient-navigate.https.html, DISABLE
+service-worker/worker-client-id.https.html, DISABLE
+service-worker/worker-in-sandboxed-iframe-by-csp-fetch-event.https.html, DISABLE
+service-worker/worker-interception.https.html, DISABLE
+service-worker/worker-interception-redirect.https.html, DISABLE
+service-worker/xhr-content-length.https.window.js, DISABLE
+service-worker/xhr-response-url.https.html, DISABLE
+service-worker/xsl-base-url.https.html, DISABLE
diff --git a/cobalt/layout_tests/web_platform_tests.cc b/cobalt/layout_tests/web_platform_tests.cc
index fda4f74..860755a 100644
--- a/cobalt/layout_tests/web_platform_tests.cc
+++ b/cobalt/layout_tests/web_platform_tests.cc
@@ -25,6 +25,8 @@
#include "base/strings/string_util.h"
#include "base/test/scoped_task_environment.h"
#include "base/values.h"
+#include "cobalt/browser/service_worker_registry.h"
+#include "cobalt/browser/user_agent_platform_info.h"
#include "cobalt/browser/web_module.h"
#include "cobalt/cssom/viewport_size.h"
#include "cobalt/layout_tests/test_utils.h"
@@ -34,6 +36,7 @@
#include "cobalt/network/network_module.h"
#include "cobalt/render_tree/resource_provider_stub.h"
#include "cobalt/web/csp_delegate_factory.h"
+#include "cobalt/web/web_settings.h"
#include "starboard/window.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
@@ -200,6 +203,7 @@
// Media module
FakeResourceProviderStub resource_provider;
+ web::WebSettingsImpl web_settings;
std::unique_ptr<media::MediaModule> media_module(
new media::MediaModule(NULL, &resource_provider));
std::unique_ptr<media::CanPlayTypeHandler> can_play_type_handler(
@@ -215,6 +219,7 @@
// we take advantage of the convenience of inline script tags.
web_module_options.enable_inline_script_warnings = false;
+ web_module_options.web_options.web_settings = &web_settings;
web_module_options.web_options.network_module = &network_module;
// Prepare a slot for our results to be placed when ready.
@@ -223,8 +228,16 @@
// Create the WebModule and wait for a layout to occur.
browser::WebModule web_module("RunWebPlatformTest");
+
+ // Create Service Worker Registry
+ browser::ServiceWorkerRegistry* service_worker_registry =
+ new browser::ServiceWorkerRegistry(&web_settings, &network_module,
+ new browser::UserAgentPlatformInfo());
+ web_module_options.web_options.service_worker_jobs =
+ service_worker_registry->service_worker_jobs();
+
web_module.Run(
- url, base::kApplicationStateStarted,
+ url, base::kApplicationStateStarted, nullptr /* scroll_engine */,
base::Bind(&WebModuleOnRenderTreeProducedCallback, &results),
base::Bind(&WebModuleErrorCallback, &run_loop,
base::MessageLoop::current()),
diff --git a/cobalt/loader/BUILD.gn b/cobalt/loader/BUILD.gn
index 3b8f6e0..8d50f70 100644
--- a/cobalt/loader/BUILD.gn
+++ b/cobalt/loader/BUILD.gn
@@ -29,6 +29,8 @@
"embedded_fetcher.h",
"error_fetcher.cc",
"error_fetcher.h",
+ "fetch_interceptor_coordinator.cc",
+ "fetch_interceptor_coordinator.h",
"fetcher.cc",
"fetcher.h",
"fetcher_cache.cc",
diff --git a/cobalt/loader/fetch_interceptor_coordinator.cc b/cobalt/loader/fetch_interceptor_coordinator.cc
new file mode 100644
index 0000000..fc2e8e8
--- /dev/null
+++ b/cobalt/loader/fetch_interceptor_coordinator.cc
@@ -0,0 +1,66 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/loader/fetch_interceptor_coordinator.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/memory/singleton.h"
+#include "url/gurl.h"
+
+namespace cobalt {
+namespace loader {
+
+// static
+FetchInterceptorCoordinator* FetchInterceptorCoordinator::GetInstance() {
+ return base::Singleton<
+ FetchInterceptorCoordinator,
+ base::LeakySingletonTraits<FetchInterceptorCoordinator>>::get();
+}
+
+FetchInterceptorCoordinator::FetchInterceptorCoordinator()
+ : fetch_interceptor_(nullptr) {}
+
+
+void FetchInterceptorCoordinator::TryIntercept(
+ const GURL& url,
+ std::unique_ptr<base::OnceCallback<void(std::unique_ptr<std::string>)>>
+ callback,
+ std::unique_ptr<base::OnceCallback<void(const net::LoadTimingInfo&)>>
+ report_load_timing_info,
+ std::unique_ptr<base::OnceClosure> fallback) {
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#handle-fetch
+ // Steps 17 to 23
+ // TODO: add should skip event handling
+ // (https://w3c.github.io/ServiceWorker/#should-skip-event).
+ // TODO: determine |shouldSoftUpdate| and if true run soft update in parallel
+ // (https://w3c.github.io/ServiceWorker/#soft-update).
+ if (!fetch_interceptor_) {
+ std::move(*fallback).Run();
+ return;
+ }
+ // TODO: test interception once registered service workers are persisted.
+ // Consider moving the interception out of the ServiceWorkerGlobalScope
+ // to avoid a race condition. Fetches should be able to be intercepted
+ // by an active, registered, service worker.
+ fetch_interceptor_->StartFetch(url, std::move(callback),
+ std::move(report_load_timing_info),
+ std::move(fallback));
+}
+
+} // namespace loader
+} // namespace cobalt
diff --git a/cobalt/loader/fetch_interceptor_coordinator.h b/cobalt/loader/fetch_interceptor_coordinator.h
new file mode 100644
index 0000000..13eb373
--- /dev/null
+++ b/cobalt/loader/fetch_interceptor_coordinator.h
@@ -0,0 +1,75 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_LOADER_FETCH_INTERCEPTOR_COORDINATOR_H_
+#define COBALT_LOADER_FETCH_INTERCEPTOR_COORDINATOR_H_
+
+#include <memory>
+#include <string>
+
+#include "base/bind.h"
+#include "net/base/load_timing_info.h"
+#include "url/gurl.h"
+
+namespace base {
+template <typename T>
+struct DefaultSingletonTraits;
+}
+
+namespace cobalt {
+namespace loader {
+
+class FetchInterceptor {
+ public:
+ virtual void StartFetch(
+ const GURL& url,
+ std::unique_ptr<base::OnceCallback<void(std::unique_ptr<std::string>)>>
+ callback,
+ std::unique_ptr<base::OnceCallback<void(const net::LoadTimingInfo&)>>
+ report_load_timing_info,
+ std::unique_ptr<base::OnceClosure> fallback) = 0;
+};
+
+// NetFetcher is for fetching data from the network.
+class FetchInterceptorCoordinator {
+ public:
+ static FetchInterceptorCoordinator* GetInstance();
+
+ void Add(FetchInterceptor* fetch_interceptor) {
+ fetch_interceptor_ = fetch_interceptor;
+ }
+
+ void Clear() { fetch_interceptor_ = nullptr; }
+
+ void TryIntercept(
+ const GURL& url,
+ std::unique_ptr<base::OnceCallback<void(std::unique_ptr<std::string>)>>
+ callback,
+ std::unique_ptr<base::OnceCallback<void(const net::LoadTimingInfo&)>>
+ report_load_timing_info,
+ std::unique_ptr<base::OnceClosure> fallback);
+
+ private:
+ friend struct base::DefaultSingletonTraits<FetchInterceptorCoordinator>;
+ FetchInterceptorCoordinator();
+
+ FetchInterceptor* fetch_interceptor_;
+
+ DISALLOW_COPY_AND_ASSIGN(FetchInterceptorCoordinator);
+};
+
+} // namespace loader
+} // namespace cobalt
+
+#endif // COBALT_LOADER_FETCH_INTERCEPTOR_COORDINATOR_H_
diff --git a/cobalt/loader/fetcher_factory.cc b/cobalt/loader/fetcher_factory.cc
index ac5fca0..d938046 100644
--- a/cobalt/loader/fetcher_factory.cc
+++ b/cobalt/loader/fetcher_factory.cc
@@ -88,14 +88,14 @@
Fetcher::Handler* handler) {
return CreateSecureFetcher(url, csp::SecurityCallback(), kNoCORSMode,
Origin(), type, net::HttpRequestHeaders(),
- handler);
+ /*skip_fetch_intercept=*/false, handler);
}
std::unique_ptr<Fetcher> FetcherFactory::CreateSecureFetcher(
const GURL& url, const csp::SecurityCallback& url_security_callback,
RequestMode request_mode, const Origin& origin,
const disk_cache::ResourceType type, net::HttpRequestHeaders headers,
- Fetcher::Handler* handler) {
+ bool skip_fetch_intercept, Fetcher::Handler* handler) {
LOG(INFO) << "Fetching: " << ClipUrl(url, 200);
if (!url.is_valid()) {
@@ -110,6 +110,7 @@
NetFetcher::Options options;
options.resource_type = type;
options.headers = std::move(headers);
+ options.skip_fetch_intercept = skip_fetch_intercept;
return std::unique_ptr<Fetcher>(
new NetFetcher(url, url_security_callback, handler, network_module_,
options, request_mode, origin));
diff --git a/cobalt/loader/fetcher_factory.h b/cobalt/loader/fetcher_factory.h
index abfa85e..cb46a6a 100644
--- a/cobalt/loader/fetcher_factory.h
+++ b/cobalt/loader/fetcher_factory.h
@@ -58,7 +58,7 @@
const GURL& url, const csp::SecurityCallback& url_security_callback,
RequestMode request_mode, const Origin& origin,
const disk_cache::ResourceType type, net::HttpRequestHeaders headers,
- Fetcher::Handler* handler);
+ bool skip_fetch_intercept, Fetcher::Handler* handler);
network::NetworkModule* network_module() const { return network_module_; }
diff --git a/cobalt/loader/net_fetcher.cc b/cobalt/loader/net_fetcher.cc
index c83c5b4..5d96aee 100644
--- a/cobalt/loader/net_fetcher.cc
+++ b/cobalt/loader/net_fetcher.cc
@@ -18,10 +18,12 @@
#include <string>
#include <utility>
+#include "base/memory/ptr_util.h"
#include "base/strings/stringprintf.h"
#include "base/trace_event/trace_event.h"
#include "cobalt/base/polymorphic_downcast.h"
#include "cobalt/loader/cors_preflight.h"
+#include "cobalt/loader/fetch_interceptor_coordinator.h"
#include "cobalt/loader/url_fetcher_string_writer.h"
#include "cobalt/network/network_module.h"
#include "net/base/mime_util.h"
@@ -101,7 +103,9 @@
base::Bind(&NetFetcher::Start, base::Unretained(this)))),
request_cross_origin_(false),
origin_(origin),
- request_script_(options.resource_type == disk_cache::kUncompiledScript) {
+ request_script_(options.resource_type == disk_cache::kUncompiledScript),
+ task_runner_(base::MessageLoop::current()->task_runner()),
+ skip_fetch_intercept_(options.skip_fetch_intercept) {
url_fetcher_ = net::URLFetcher::Create(url, options.request_method, this);
if (!options.headers.IsEmpty()) {
url_fetcher_->SetExtraRequestHeaders(options.headers.ToString());
@@ -143,7 +147,22 @@
original_url.spec());
if (security_callback_.is_null() ||
security_callback_.Run(original_url, false /* did not redirect */)) {
- url_fetcher_->Start();
+ if (skip_fetch_intercept_) {
+ url_fetcher_->Start();
+ return;
+ }
+ FetchInterceptorCoordinator::GetInstance()->TryIntercept(
+ original_url,
+ std::make_unique<
+ base::OnceCallback<void(std::unique_ptr<std::string>)>>(
+ base::BindOnce(&NetFetcher::OnFetchIntercepted,
+ base::Unretained(this))),
+ std::make_unique<base::OnceCallback<void(const net::LoadTimingInfo&)>>(
+ base::BindOnce(&NetFetcher::ReportLoadTimingInfo,
+ base::Unretained(this))),
+ std::make_unique<base::OnceClosure>(base::BindOnce(
+ &net::URLFetcher::Start, base::Unretained(url_fetcher_.get()))));
+
} else {
std::string msg(base::StringPrintf("URL %s rejected by security policy.",
original_url.spec().c_str()));
@@ -151,6 +170,22 @@
}
}
+void NetFetcher::OnFetchIntercepted(std::unique_ptr<std::string> body) {
+ if (task_runner_ != base::MessageLoop::current()->task_runner()) {
+ task_runner_->PostTask(
+ FROM_HERE, base::BindOnce(&NetFetcher::OnFetchIntercepted,
+ base::Unretained(this), std::move(body)));
+ return;
+ }
+ DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+ if (!body || body->empty()) {
+ url_fetcher_->Start();
+ return;
+ }
+ handler()->OnReceivedPassed(this, std::make_unique<std::string>(*body));
+ handler()->OnDone(this);
+}
+
void NetFetcher::OnURLFetchResponseStarted(const net::URLFetcher* source) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
TRACE_EVENT1("cobalt::loader", "NetFetcher::OnURLFetchResponseStarted", "url",
@@ -170,9 +205,9 @@
if ((handler()->OnResponseStarted(this, source->GetResponseHeaders()) ==
kLoadResponseAbort) ||
(!IsResponseCodeSuccess(source->GetResponseCode()))) {
- std::string msg(
- base::StringPrintf("Handler::OnResponseStarted aborted URL %s",
- source->GetURL().spec().c_str()));
+ std::string msg(base::StringPrintf("URL %s aborted or failed with code %d",
+ source->GetURL().spec().c_str(),
+ source->GetResponseCode()));
return HandleError(msg).InvalidateThis();
}
diff --git a/cobalt/loader/net_fetcher.h b/cobalt/loader/net_fetcher.h
index 4c4ec9b..3a6d3d8 100644
--- a/cobalt/loader/net_fetcher.h
+++ b/cobalt/loader/net_fetcher.h
@@ -42,10 +42,12 @@
public:
Options()
: request_method(net::URLFetcher::GET),
- resource_type(disk_cache::kOther) {}
+ resource_type(disk_cache::kOther),
+ skip_fetch_intercept(false) {}
net::URLFetcher::RequestType request_method;
disk_cache::ResourceType resource_type;
net::HttpRequestHeaders headers;
+ bool skip_fetch_intercept;
};
NetFetcher(const GURL& url, const csp::SecurityCallback& security_callback,
@@ -62,6 +64,8 @@
int64_t current_network_bytes) override;
void ReportLoadTimingInfo(const net::LoadTimingInfo& timing_info) override;
+ void OnFetchIntercepted(std::unique_ptr<std::string> body);
+
net::URLFetcher* url_fetcher() const {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
return url_fetcher_.get();
@@ -116,6 +120,9 @@
// True when the requested resource type is script.
bool request_script_;
+ scoped_refptr<base::SingleThreadTaskRunner> const task_runner_;
+ bool skip_fetch_intercept_;
+
DISALLOW_COPY_AND_ASSIGN(NetFetcher);
};
diff --git a/cobalt/loader/script_loader_factory.cc b/cobalt/loader/script_loader_factory.cc
index d70f846..97d096d 100644
--- a/cobalt/loader/script_loader_factory.cc
+++ b/cobalt/loader/script_loader_factory.cc
@@ -47,24 +47,14 @@
const GURL& url, const Origin& origin,
const csp::SecurityCallback& url_security_callback,
const TextDecoder::TextAvailableCallback& script_available_callback,
- const Loader::OnCompleteFunction& load_complete_callback) {
- return CreateScriptLoader(
- url, origin, url_security_callback, script_available_callback,
- TextDecoder::ResponseStartedCallback(), load_complete_callback);
-}
-
-std::unique_ptr<Loader> ScriptLoaderFactory::CreateScriptLoader(
- const GURL& url, const Origin& origin,
- const csp::SecurityCallback& url_security_callback,
- const TextDecoder::TextAvailableCallback& script_available_callback,
const TextDecoder::ResponseStartedCallback& response_started_callback,
const Loader::OnCompleteFunction& load_complete_callback,
- net::HttpRequestHeaders headers) {
+ net::HttpRequestHeaders headers, bool skip_fetch_intercept) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
- Loader::FetcherCreator fetcher_creator =
- MakeFetcherCreator(url, url_security_callback, kNoCORSMode, origin,
- disk_cache::kUncompiledScript, std::move(headers));
+ Loader::FetcherCreator fetcher_creator = MakeFetcherCreator(
+ url, url_security_callback, kNoCORSMode, origin,
+ disk_cache::kUncompiledScript, std::move(headers), skip_fetch_intercept);
std::unique_ptr<Loader> loader(
new Loader(fetcher_creator,
@@ -82,13 +72,14 @@
Loader::FetcherCreator ScriptLoaderFactory::MakeFetcherCreator(
const GURL& url, const csp::SecurityCallback& url_security_callback,
RequestMode request_mode, const Origin& origin,
- disk_cache::ResourceType type, net::HttpRequestHeaders headers) {
+ disk_cache::ResourceType type, net::HttpRequestHeaders headers,
+ bool skip_fetch_intercept) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
return base::Bind(&FetcherFactory::CreateSecureFetcher,
base::Unretained(fetcher_factory_), url,
url_security_callback, request_mode, origin, type,
- std::move(headers));
+ std::move(headers), skip_fetch_intercept);
}
void ScriptLoaderFactory::OnLoaderCreated(Loader* loader) {
diff --git a/cobalt/loader/script_loader_factory.h b/cobalt/loader/script_loader_factory.h
index 18e0da4..6ff287e 100644
--- a/cobalt/loader/script_loader_factory.h
+++ b/cobalt/loader/script_loader_factory.h
@@ -47,7 +47,13 @@
const GURL& url, const Origin& origin,
const csp::SecurityCallback& url_security_callback,
const TextDecoder::TextAvailableCallback& script_available_callback,
- const Loader::OnCompleteFunction& load_complete_callback);
+ const Loader::OnCompleteFunction& load_complete_callback,
+ bool skip_fetch_intercept = false) {
+ return CreateScriptLoader(
+ url, origin, url_security_callback, script_available_callback,
+ TextDecoder::ResponseStartedCallback(), load_complete_callback,
+ net::HttpRequestHeaders(), skip_fetch_intercept);
+ }
std::unique_ptr<Loader> CreateScriptLoader(
const GURL& url, const Origin& origin,
@@ -55,7 +61,8 @@
const TextDecoder::TextAvailableCallback& script_available_callback,
const TextDecoder::ResponseStartedCallback& response_started_callback,
const Loader::OnCompleteFunction& load_complete_callback,
- net::HttpRequestHeaders headers = net::HttpRequestHeaders());
+ net::HttpRequestHeaders headers = net::HttpRequestHeaders(),
+ bool skip_fetch_intercept = false);
protected:
void OnLoaderCreated(Loader* loader);
@@ -65,7 +72,8 @@
const GURL& url, const csp::SecurityCallback& url_security_callback,
RequestMode request_mode, const Origin& origin,
disk_cache::ResourceType type = disk_cache::kOther,
- net::HttpRequestHeaders headers = net::HttpRequestHeaders());
+ net::HttpRequestHeaders headers = net::HttpRequestHeaders(),
+ bool skip_fetch_intercept = false);
// Ensures that the LoaderFactory methods are only called from the same
// thread.
diff --git a/cobalt/media/BUILD.gn b/cobalt/media/BUILD.gn
index e7e4b0c..1b27130 100644
--- a/cobalt/media/BUILD.gn
+++ b/cobalt/media/BUILD.gn
@@ -30,6 +30,7 @@
"base/audio_bus.h",
"base/data_source.cc",
"base/data_source.h",
+ "base/decode_target_provider.h",
"base/decoder_buffer_cache.cc",
"base/decoder_buffer_cache.h",
"base/drm_system.cc",
@@ -40,20 +41,20 @@
"base/interleaved_sinc_resampler.h",
"base/playback_statistics.cc",
"base/playback_statistics.h",
+ "base/sbplayer_bridge.cc",
+ "base/sbplayer_bridge.h",
+ "base/sbplayer_interface.cc",
+ "base/sbplayer_interface.h",
"base/sbplayer_pipeline.cc",
"base/sbplayer_set_bounds_helper.cc",
"base/sbplayer_set_bounds_helper.h",
- "base/starboard_player.cc",
- "base/starboard_player.h",
- "base/video_frame_provider.h",
"decoder_buffer_allocator.cc",
"decoder_buffer_allocator.h",
"decoder_buffer_memory_info.h",
- "fetcher_buffered_data_source.cc",
- "fetcher_buffered_data_source.h",
+ "file_data_source.cc",
+ "file_data_source.h",
"media_module.cc",
"media_module.h",
- "player/buffered_data_source.h",
"player/web_media_player_impl.cc",
"player/web_media_player_impl.h",
"player/web_media_player_proxy.cc",
@@ -76,6 +77,8 @@
"progressive/progressive_parser.h",
"progressive/rbsp_stream.cc",
"progressive/rbsp_stream.h",
+ "url_fetcher_data_source.cc",
+ "url_fetcher_data_source.h",
]
configs -= [ "//starboard/build/config:size" ]
@@ -108,6 +111,7 @@
testonly = true
sources = [
+ "file_data_source_test.cc",
"progressive/demuxer_extension_wrapper_test.cc",
"progressive/mock_data_source_reader.h",
"progressive/mp4_map_unittest.cc",
@@ -119,10 +123,13 @@
deps = [
":media",
+ "//base/test:test_support",
"//cobalt/base",
"//cobalt/test:run_all_unittests",
"//testing/gmock",
"//testing/gtest",
"//third_party/chromium/media:media",
]
+
+ data_deps = [ "//cobalt/media/testing:cobalt_media_download_test_data" ]
}
diff --git a/cobalt/media/base/data_source.h b/cobalt/media/base/data_source.h
index c415f30..0af37b9 100644
--- a/cobalt/media/base/data_source.h
+++ b/cobalt/media/base/data_source.h
@@ -18,6 +18,7 @@
public:
typedef base::Callback<void(int64_t, int64_t)> StatusCallback;
typedef base::Callback<void(int)> ReadCB;
+ typedef base::Callback<void(bool)> DownloadingStatusCB;
enum { kReadError = -1, kAborted = -2 };
@@ -41,13 +42,9 @@
// retrieved.
virtual bool GetSize(int64_t* size_out) = 0;
- // Returns true if we are performing streaming. In this case seeking is
- // not possible.
- virtual bool IsStreaming() = 0;
-
- // Notify the DataSource of the bitrate of the media.
- // Values of |bitrate| <= 0 are invalid and should be ignored.
- virtual void SetBitrate(int bitrate) = 0;
+ // Sets a callback to receive downloading status.
+ virtual void SetDownloadingStatusCB(
+ const DownloadingStatusCB& downloading_status_cb) = 0;
private:
DISALLOW_COPY_AND_ASSIGN(DataSource);
diff --git a/cobalt/media/base/decode_target_provider.h b/cobalt/media/base/decode_target_provider.h
new file mode 100644
index 0000000..bf7ed26
--- /dev/null
+++ b/cobalt/media/base/decode_target_provider.h
@@ -0,0 +1,93 @@
+// Copyright 2018 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_MEDIA_BASE_DECODE_TARGET_PROVIDER_H_
+#define COBALT_MEDIA_BASE_DECODE_TARGET_PROVIDER_H_
+
+#include "base/callback.h"
+#include "base/memory/ref_counted.h"
+#include "base/synchronization/lock.h"
+#include "base/time/time.h"
+#include "starboard/decode_target.h"
+
+namespace cobalt {
+namespace media {
+
+// The DecodeTargetProvider manages the backlog for video frames. It has the
+// following functionalities:
+// 1. It caches the video frames ready to be displayed.
+// 2. It decides which frame to be displayed at the current time.
+// 3. It removes frames that will no longer be displayed.
+class DecodeTargetProvider
+ : public base::RefCountedThreadSafe<DecodeTargetProvider> {
+ public:
+ enum OutputMode {
+ kOutputModePunchOut,
+ kOutputModeDecodeToTexture,
+ kOutputModeInvalid,
+ };
+
+ DecodeTargetProvider() : output_mode_(kOutputModeInvalid) {}
+
+ typedef base::Callback<SbDecodeTarget()> GetCurrentSbDecodeTargetFunction;
+
+ void SetOutputMode(OutputMode output_mode) {
+ base::AutoLock auto_lock(lock_);
+ output_mode_ = output_mode;
+ }
+
+ DecodeTargetProvider::OutputMode GetOutputMode() const {
+ base::AutoLock auto_lock(lock_);
+ return output_mode_;
+ }
+
+ // For Starboard platforms that have a decode-to-texture player, we enable
+ // this DecodeTargetProvider to act as a bridge for Cobalt code to query
+ // for the current SbDecodeTarget. In effect, we bypass all of
+ // DecodeTargetProvider's logic in this case, instead relying on the
+ // Starboard implementation to provide us with the current video frame when
+ // needed.
+ void SetGetCurrentSbDecodeTargetFunction(
+ GetCurrentSbDecodeTargetFunction function) {
+ base::AutoLock auto_lock(lock_);
+ get_current_sb_decode_target_function_ = function;
+ }
+
+ void ResetGetCurrentSbDecodeTargetFunction() {
+ base::AutoLock auto_lock(lock_);
+ get_current_sb_decode_target_function_.Reset();
+ }
+
+ SbDecodeTarget GetCurrentSbDecodeTarget() const {
+ base::AutoLock auto_lock(lock_);
+ if (get_current_sb_decode_target_function_.is_null()) {
+ return kSbDecodeTargetInvalid;
+ } else {
+ return get_current_sb_decode_target_function_.Run();
+ }
+ }
+
+ private:
+ mutable base::Lock lock_;
+
+ OutputMode output_mode_;
+ GetCurrentSbDecodeTargetFunction get_current_sb_decode_target_function_;
+
+ DISALLOW_COPY_AND_ASSIGN(DecodeTargetProvider);
+};
+
+} // namespace media
+} // namespace cobalt
+
+#endif // COBALT_MEDIA_BASE_DECODE_TARGET_PROVIDER_H_
diff --git a/cobalt/media/base/pipeline.h b/cobalt/media/base/pipeline.h
index 4490669..ccb3a1b 100644
--- a/cobalt/media/base/pipeline.h
+++ b/cobalt/media/base/pipeline.h
@@ -21,8 +21,9 @@
#include "base/callback.h"
#include "base/memory/ref_counted.h"
#include "base/message_loop/message_loop.h"
+#include "cobalt/media/base/decode_target_provider.h"
#include "cobalt/media/base/media_export.h"
-#include "cobalt/media/base/video_frame_provider.h"
+#include "cobalt/media/base/sbplayer_interface.h"
#include "starboard/drm.h"
#include "starboard/window.h"
#include "third_party/chromium/media/base/demuxer.h"
@@ -92,12 +93,12 @@
#endif // SB_HAS(PLAYER_WITH_URL)
static scoped_refptr<Pipeline> Create(
- PipelineWindow window,
+ SbPlayerInterface* interface, PipelineWindow window,
const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
const GetDecodeTargetGraphicsContextProviderFunc&
get_decode_target_graphics_context_provider_func,
- bool allow_resume_after_suspend, MediaLog* media_log,
- VideoFrameProvider* video_frame_provider);
+ bool allow_resume_after_suspend, bool allow_batched_sample_write,
+ MediaLog* media_log, DecodeTargetProvider* decode_target_provider);
virtual ~Pipeline() {}
diff --git a/cobalt/media/base/sbplayer_bridge.cc b/cobalt/media/base/sbplayer_bridge.cc
new file mode 100644
index 0000000..6598dc8
--- /dev/null
+++ b/cobalt/media/base/sbplayer_bridge.cc
@@ -0,0 +1,1124 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/media/base/sbplayer_bridge.h"
+
+#include <algorithm>
+#include <iomanip>
+#include <string>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/compiler_specific.h"
+#include "base/location.h"
+#include "base/logging.h"
+#include "base/trace_event/trace_event.h"
+#include "cobalt/base/statistics.h"
+#include "cobalt/media/base/format_support_query_metrics.h"
+#include "starboard/common/media.h"
+#include "starboard/common/string.h"
+#include "starboard/configuration.h"
+#include "starboard/memory.h"
+#include "starboard/once.h"
+#include "third_party/chromium/media/base/starboard_utils.h"
+
+namespace cobalt {
+namespace media {
+
+namespace {
+
+class StatisticsWrapper {
+ public:
+ static StatisticsWrapper* GetInstance();
+ base::Statistics<SbTime, int, 1024> startup_latency{
+ "Media.PlaybackStartupLatency"};
+};
+} // namespace
+SB_ONCE_INITIALIZE_FUNCTION(StatisticsWrapper, StatisticsWrapper::GetInstance);
+
+SbPlayerBridge::CallbackHelper::CallbackHelper(SbPlayerBridge* player_bridge)
+ : player_bridge_(player_bridge) {}
+
+void SbPlayerBridge::CallbackHelper::ClearDecoderBufferCache() {
+ base::AutoLock auto_lock(lock_);
+ if (player_bridge_) {
+ player_bridge_->ClearDecoderBufferCache();
+ }
+}
+
+void SbPlayerBridge::CallbackHelper::OnDecoderStatus(SbPlayer player,
+ SbMediaType type,
+ SbPlayerDecoderState state,
+ int ticket) {
+ base::AutoLock auto_lock(lock_);
+ if (player_bridge_) {
+ player_bridge_->OnDecoderStatus(player, type, state, ticket);
+ }
+}
+
+void SbPlayerBridge::CallbackHelper::OnPlayerStatus(SbPlayer player,
+ SbPlayerState state,
+ int ticket) {
+ base::AutoLock auto_lock(lock_);
+ if (player_bridge_) {
+ player_bridge_->OnPlayerStatus(player, state, ticket);
+ }
+}
+
+void SbPlayerBridge::CallbackHelper::OnPlayerError(SbPlayer player,
+ SbPlayerError error,
+ const std::string& message) {
+ base::AutoLock auto_lock(lock_);
+ if (player_bridge_) {
+ player_bridge_->OnPlayerError(player, error, message);
+ }
+}
+
+void SbPlayerBridge::CallbackHelper::OnDeallocateSample(
+ const void* sample_buffer) {
+ base::AutoLock auto_lock(lock_);
+ if (player_bridge_) {
+ player_bridge_->OnDeallocateSample(sample_buffer);
+ }
+}
+
+void SbPlayerBridge::CallbackHelper::ResetPlayer() {
+ base::AutoLock auto_lock(lock_);
+ player_bridge_ = NULL;
+}
+
+#if SB_HAS(PLAYER_WITH_URL)
+SbPlayerBridge::SbPlayerBridge(
+ SbPlayerInterface* interface,
+ const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
+ const std::string& url, SbWindow window, Host* host,
+ SbPlayerSetBoundsHelper* set_bounds_helper, bool allow_resume_after_suspend,
+ bool prefer_decode_to_texture,
+ const OnEncryptedMediaInitDataEncounteredCB&
+ on_encrypted_media_init_data_encountered_cb,
+ DecodeTargetProvider* const decode_target_provider)
+ : url_(url),
+ sbplayer_interface_(interface),
+ task_runner_(task_runner),
+ callback_helper_(
+ new CallbackHelper(ALLOW_THIS_IN_INITIALIZER_LIST(this))),
+ window_(window),
+ host_(host),
+ set_bounds_helper_(set_bounds_helper),
+ allow_resume_after_suspend_(allow_resume_after_suspend),
+ on_encrypted_media_init_data_encountered_cb_(
+ on_encrypted_media_init_data_encountered_cb),
+ decode_target_provider_(decode_target_provider),
+ is_url_based_(true) {
+ DCHECK(host_);
+ DCHECK(set_bounds_helper_);
+
+ output_mode_ = ComputeSbUrlPlayerOutputMode(prefer_decode_to_texture);
+
+ CreateUrlPlayer(url_);
+
+ task_runner->PostTask(
+ FROM_HERE,
+ base::Bind(&SbPlayerBridge::CallbackHelper::ClearDecoderBufferCache,
+ callback_helper_));
+}
+#endif // SB_HAS(PLAYER_WITH_URL)
+
+SbPlayerBridge::SbPlayerBridge(
+ SbPlayerInterface* interface,
+ const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
+ const GetDecodeTargetGraphicsContextProviderFunc&
+ get_decode_target_graphics_context_provider_func,
+ const AudioDecoderConfig& audio_config, const std::string& audio_mime_type,
+ const VideoDecoderConfig& video_config, const std::string& video_mime_type,
+ SbWindow window, SbDrmSystem drm_system, Host* host,
+ SbPlayerSetBoundsHelper* set_bounds_helper, bool allow_resume_after_suspend,
+ bool prefer_decode_to_texture,
+ DecodeTargetProvider* const decode_target_provider,
+ const std::string& max_video_capabilities)
+ : sbplayer_interface_(interface),
+ task_runner_(task_runner),
+ get_decode_target_graphics_context_provider_func_(
+ get_decode_target_graphics_context_provider_func),
+ callback_helper_(
+ new CallbackHelper(ALLOW_THIS_IN_INITIALIZER_LIST(this))),
+ window_(window),
+ drm_system_(drm_system),
+ host_(host),
+ set_bounds_helper_(set_bounds_helper),
+ allow_resume_after_suspend_(allow_resume_after_suspend),
+ audio_config_(audio_config),
+ video_config_(video_config),
+ decode_target_provider_(decode_target_provider),
+ max_video_capabilities_(max_video_capabilities)
+#if SB_HAS(PLAYER_WITH_URL)
+ ,
+ is_url_based_(false)
+#endif // SB_HAS(PLAYER_WITH_URL
+{
+ DCHECK(!get_decode_target_graphics_context_provider_func_.is_null());
+ DCHECK(audio_config.IsValidConfig() || video_config.IsValidConfig());
+ DCHECK(host_);
+ DCHECK(set_bounds_helper_);
+ DCHECK(decode_target_provider_);
+
+ audio_sample_info_.codec = kSbMediaAudioCodecNone;
+ video_sample_info_.codec = kSbMediaVideoCodecNone;
+
+ if (audio_config.IsValidConfig()) {
+ UpdateAudioConfig(audio_config, audio_mime_type);
+ }
+ if (video_config.IsValidConfig()) {
+ UpdateVideoConfig(video_config, video_mime_type);
+ }
+
+ output_mode_ = ComputeSbPlayerOutputMode(prefer_decode_to_texture);
+
+ CreatePlayer();
+
+ if (SbPlayerIsValid(player_)) {
+ task_runner->PostTask(
+ FROM_HERE,
+ base::Bind(&SbPlayerBridge::CallbackHelper::ClearDecoderBufferCache,
+ callback_helper_));
+ }
+}
+
+SbPlayerBridge::~SbPlayerBridge() {
+ DCHECK(task_runner_->BelongsToCurrentThread());
+
+ callback_helper_->ResetPlayer();
+ set_bounds_helper_->SetPlayerBridge(NULL);
+
+ decode_target_provider_->SetOutputMode(
+ DecodeTargetProvider::kOutputModeInvalid);
+ decode_target_provider_->ResetGetCurrentSbDecodeTargetFunction();
+
+ if (SbPlayerIsValid(player_)) {
+ sbplayer_interface_->Destroy(player_);
+ }
+}
+
+void SbPlayerBridge::UpdateAudioConfig(const AudioDecoderConfig& audio_config,
+ const std::string& mime_type) {
+ DCHECK(task_runner_->BelongsToCurrentThread());
+ DCHECK(audio_config.IsValidConfig());
+
+ LOG(INFO) << "Updated AudioDecoderConfig -- "
+ << audio_config.AsHumanReadableString();
+
+ audio_config_ = audio_config;
+ audio_mime_type_ = mime_type;
+ audio_sample_info_ = MediaAudioConfigToSbMediaAudioSampleInfo(
+ audio_config_, audio_mime_type_.c_str());
+ LOG(INFO) << "Converted to SbMediaAudioSampleInfo -- " << audio_sample_info_;
+}
+
+void SbPlayerBridge::UpdateVideoConfig(const VideoDecoderConfig& video_config,
+ const std::string& mime_type) {
+ DCHECK(task_runner_->BelongsToCurrentThread());
+ DCHECK(video_config.IsValidConfig());
+
+ LOG(INFO) << "Updated VideoDecoderConfig -- "
+ << video_config.AsHumanReadableString();
+
+ video_config_ = video_config;
+ video_sample_info_.frame_width =
+ static_cast<int>(video_config_.natural_size().width());
+ video_sample_info_.frame_height =
+ static_cast<int>(video_config_.natural_size().height());
+ video_sample_info_.codec =
+ MediaVideoCodecToSbMediaVideoCodec(video_config_.codec());
+ video_sample_info_.color_metadata =
+ MediaToSbMediaColorMetadata(video_config_.color_space_info(),
+ video_config_.hdr_metadata(), mime_type);
+ video_mime_type_ = mime_type;
+ video_sample_info_.mime = video_mime_type_.c_str();
+ video_sample_info_.max_video_capabilities = max_video_capabilities_.c_str();
+ LOG(INFO) << "Converted to SbMediaVideoSampleInfo -- " << video_sample_info_;
+}
+
+void SbPlayerBridge::WriteBuffers(
+ DemuxerStream::Type type,
+ const std::vector<scoped_refptr<DecoderBuffer>>& buffers) {
+ DCHECK(task_runner_->BelongsToCurrentThread());
+#if SB_HAS(PLAYER_WITH_URL)
+ DCHECK(!is_url_based_);
+#endif // SB_HAS(PLAYER_WITH_URL)
+
+ if (allow_resume_after_suspend_) {
+ for (const auto& buffer : buffers) {
+ DCHECK(buffer);
+ decoder_buffer_cache_.AddBuffer(type, buffer);
+ }
+ if (state_ != kSuspended) {
+ WriteNextBuffersFromCache(type, buffers.size());
+ }
+ return;
+ }
+
+ WriteBuffersInternal(type, buffers);
+}
+
+void SbPlayerBridge::SetBounds(int z_index, const gfx::Rect& rect) {
+ base::AutoLock auto_lock(lock_);
+
+ set_bounds_z_index_ = z_index;
+ set_bounds_rect_ = rect;
+
+ if (state_ == kSuspended) {
+ return;
+ }
+
+ UpdateBounds_Locked();
+}
+
+void SbPlayerBridge::PrepareForSeek() {
+ DCHECK(task_runner_->BelongsToCurrentThread());
+
+ seek_pending_ = true;
+
+ if (state_ == kSuspended) {
+ return;
+ }
+
+ ++ticket_;
+ sbplayer_interface_->SetPlaybackRate(player_, 0.f);
+}
+
+void SbPlayerBridge::Seek(base::TimeDelta time) {
+ DCHECK(task_runner_->BelongsToCurrentThread());
+
+ decoder_buffer_cache_.ClearAll();
+ seek_pending_ = false;
+
+ pending_audio_eos_buffer_ = false;
+ pending_video_eos_buffer_ = false;
+
+ if (state_ == kSuspended) {
+ preroll_timestamp_ = time;
+ return;
+ }
+
+ // If a seek happens during resuming, the pipeline will write samples from the
+ // seek target time again so resuming can be aborted.
+ if (state_ == kResuming) {
+ state_ = kPlaying;
+ }
+
+ DCHECK(SbPlayerIsValid(player_));
+
+ ++ticket_;
+ sbplayer_interface_->Seek(player_, time.InMicroseconds(), ticket_);
+
+ sbplayer_interface_->SetPlaybackRate(player_, playback_rate_);
+}
+
+void SbPlayerBridge::SetVolume(float volume) {
+ DCHECK(task_runner_->BelongsToCurrentThread());
+
+ volume_ = volume;
+
+ if (state_ == kSuspended) {
+ return;
+ }
+
+ DCHECK(SbPlayerIsValid(player_));
+ sbplayer_interface_->SetVolume(player_, volume);
+}
+
+void SbPlayerBridge::SetPlaybackRate(double playback_rate) {
+ DCHECK(task_runner_->BelongsToCurrentThread());
+
+ playback_rate_ = playback_rate;
+
+ if (state_ == kSuspended) {
+ return;
+ }
+
+ if (seek_pending_) {
+ return;
+ }
+
+ sbplayer_interface_->SetPlaybackRate(player_, playback_rate);
+}
+
+void SbPlayerBridge::GetInfo(uint32* video_frames_decoded,
+ uint32* video_frames_dropped,
+ base::TimeDelta* media_time) {
+ DCHECK(video_frames_decoded || video_frames_dropped || media_time);
+
+ base::AutoLock auto_lock(lock_);
+ GetInfo_Locked(video_frames_decoded, video_frames_dropped, media_time);
+}
+
+#if SB_HAS(PLAYER_WITH_URL)
+void SbPlayerBridge::GetUrlPlayerBufferedTimeRanges(
+ base::TimeDelta* buffer_start_time, base::TimeDelta* buffer_length_time) {
+ DCHECK(buffer_start_time || buffer_length_time);
+ DCHECK(is_url_based_);
+
+ if (state_ == kSuspended) {
+ *buffer_start_time = base::TimeDelta();
+ *buffer_length_time = base::TimeDelta();
+ return;
+ }
+
+ DCHECK(SbPlayerIsValid(player_));
+
+ SbUrlPlayerExtraInfo url_player_info;
+ sbplayer_interface_->GetUrlPlayerExtraInfo(player_, &url_player_info);
+
+ if (buffer_start_time) {
+ *buffer_start_time = base::TimeDelta::FromMicroseconds(
+ url_player_info.buffer_start_timestamp);
+ }
+ if (buffer_length_time) {
+ *buffer_length_time =
+ base::TimeDelta::FromMicroseconds(url_player_info.buffer_duration);
+ }
+}
+
+void SbPlayerBridge::GetVideoResolution(int* frame_width, int* frame_height) {
+ DCHECK(frame_width);
+ DCHECK(frame_height);
+ DCHECK(is_url_based_);
+
+ if (state_ == kSuspended) {
+ *frame_width = video_sample_info_.frame_width;
+ *frame_height = video_sample_info_.frame_height;
+ return;
+ }
+
+ DCHECK(SbPlayerIsValid(player_));
+
+ SbPlayerInfo2 out_player_info;
+ sbplayer_interface_->GetInfo(player_, &out_player_info);
+
+ video_sample_info_.frame_width = out_player_info.frame_width;
+ video_sample_info_.frame_height = out_player_info.frame_height;
+
+ *frame_width = video_sample_info_.frame_width;
+ *frame_height = video_sample_info_.frame_height;
+}
+
+base::TimeDelta SbPlayerBridge::GetDuration() {
+ DCHECK(is_url_based_);
+
+ if (state_ == kSuspended) {
+ return base::TimeDelta();
+ }
+
+ DCHECK(SbPlayerIsValid(player_));
+
+ SbPlayerInfo2 info;
+ sbplayer_interface_->GetInfo(player_, &info);
+ if (info.duration == SB_PLAYER_NO_DURATION) {
+ // URL-based player may not have loaded asset yet, so map no duration to 0.
+ return base::TimeDelta();
+ }
+ return base::TimeDelta::FromMicroseconds(info.duration);
+}
+
+base::TimeDelta SbPlayerBridge::GetStartDate() {
+ DCHECK(is_url_based_);
+
+ if (state_ == kSuspended) {
+ return base::TimeDelta();
+ }
+
+ DCHECK(SbPlayerIsValid(player_));
+
+ SbPlayerInfo2 info;
+ sbplayer_interface_->GetInfo(player_, &info);
+ return base::TimeDelta::FromMicroseconds(info.start_date);
+}
+
+void SbPlayerBridge::SetDrmSystem(SbDrmSystem drm_system) {
+ DCHECK(is_url_based_);
+
+ drm_system_ = drm_system;
+ sbplayer_interface_->SetUrlPlayerDrmSystem(player_, drm_system);
+}
+#endif // SB_HAS(PLAYER_WITH_URL)
+
+void SbPlayerBridge::Suspend() {
+ DCHECK(task_runner_->BelongsToCurrentThread());
+
+ // Check if the player is already suspended.
+ if (state_ == kSuspended) {
+ return;
+ }
+
+ DCHECK(SbPlayerIsValid(player_));
+
+ sbplayer_interface_->SetPlaybackRate(player_, 0.0);
+
+ set_bounds_helper_->SetPlayerBridge(NULL);
+
+ base::AutoLock auto_lock(lock_);
+ GetInfo_Locked(&cached_video_frames_decoded_, &cached_video_frames_dropped_,
+ &preroll_timestamp_);
+
+ state_ = kSuspended;
+
+ decode_target_provider_->SetOutputMode(
+ DecodeTargetProvider::kOutputModeInvalid);
+ decode_target_provider_->ResetGetCurrentSbDecodeTargetFunction();
+
+ sbplayer_interface_->Destroy(player_);
+
+ player_ = kSbPlayerInvalid;
+}
+
+void SbPlayerBridge::Resume(SbWindow window) {
+ DCHECK(task_runner_->BelongsToCurrentThread());
+
+ window_ = window;
+
+ // Check if the player is already resumed.
+ if (state_ != kSuspended) {
+ DCHECK(SbPlayerIsValid(player_));
+ return;
+ }
+
+ decoder_buffer_cache_.StartResuming();
+
+#if SB_HAS(PLAYER_WITH_URL)
+ if (is_url_based_) {
+ CreateUrlPlayer(url_);
+ if (SbDrmSystemIsValid(drm_system_)) {
+ sbplayer_interface_->SetUrlPlayerDrmSystem(player_, drm_system_);
+ }
+ } else {
+ CreatePlayer();
+ }
+#else // SB_HAS(PLAYER_WITH_URL)
+ CreatePlayer();
+#endif // SB_HAS(PLAYER_WITH_URL)
+
+ if (SbPlayerIsValid(player_)) {
+ base::AutoLock auto_lock(lock_);
+ state_ = kResuming;
+ UpdateBounds_Locked();
+ }
+}
+
+namespace {
+DecodeTargetProvider::OutputMode ToVideoFrameProviderOutputMode(
+ SbPlayerOutputMode output_mode) {
+ switch (output_mode) {
+ case kSbPlayerOutputModeDecodeToTexture:
+ return DecodeTargetProvider::kOutputModeDecodeToTexture;
+ case kSbPlayerOutputModePunchOut:
+ return DecodeTargetProvider::kOutputModePunchOut;
+ case kSbPlayerOutputModeInvalid:
+ return DecodeTargetProvider::kOutputModeInvalid;
+ }
+
+ NOTREACHED();
+ return DecodeTargetProvider::kOutputModeInvalid;
+}
+
+} // namespace
+
+#if SB_HAS(PLAYER_WITH_URL)
+// static
+void SbPlayerBridge::EncryptedMediaInitDataEncounteredCB(
+ SbPlayer player, void* context, const char* init_data_type,
+ const unsigned char* init_data, unsigned int init_data_length) {
+ SbPlayerBridge* helper = static_cast<SbPlayerBridge*>(context);
+ DCHECK(!helper->on_encrypted_media_init_data_encountered_cb_.is_null());
+ // TODO: Use callback_helper here.
+ helper->on_encrypted_media_init_data_encountered_cb_.Run(
+ init_data_type, init_data, init_data_length);
+}
+
+void SbPlayerBridge::CreateUrlPlayer(const std::string& url) {
+ TRACE_EVENT0("cobalt::media", "SbPlayerBridge::CreateUrlPlayer");
+ DCHECK(task_runner_->BelongsToCurrentThread());
+
+ DCHECK(!on_encrypted_media_init_data_encountered_cb_.is_null());
+ LOG(INFO) << "CreateUrlPlayer passed url " << url;
+
+ if (max_video_capabilities_.empty()) {
+ FormatSupportQueryMetrics::PrintAndResetMetrics();
+ }
+
+ player_creation_time_ = SbTimeGetMonotonicNow();
+
+ player_ = sbplayer_interface_->CreateUrlPlayer(
+ url.c_str(), window_, &SbPlayerBridge::PlayerStatusCB,
+ &SbPlayerBridge::EncryptedMediaInitDataEncounteredCB,
+ &SbPlayerBridge::PlayerErrorCB, this);
+ DCHECK(SbPlayerIsValid(player_));
+
+ if (output_mode_ == kSbPlayerOutputModeDecodeToTexture) {
+ // If the player is setup to decode to texture, then provide Cobalt with
+ // a method of querying that texture.
+ decode_target_provider_->SetGetCurrentSbDecodeTargetFunction(base::Bind(
+ &SbPlayerBridge::GetCurrentSbDecodeTarget, base::Unretained(this)));
+ }
+ decode_target_provider_->SetOutputMode(
+ ToVideoFrameProviderOutputMode(output_mode_));
+
+ set_bounds_helper_->SetPlayerBridge(this);
+
+ base::AutoLock auto_lock(lock_);
+ UpdateBounds_Locked();
+}
+#endif // SB_HAS(PLAYER_WITH_URL)
+void SbPlayerBridge::CreatePlayer() {
+ TRACE_EVENT0("cobalt::media", "SbPlayerBridge::CreatePlayer");
+ DCHECK(task_runner_->BelongsToCurrentThread());
+
+ bool is_visible = SbWindowIsValid(window_);
+ SbMediaAudioCodec audio_codec = audio_sample_info_.codec;
+
+ bool has_audio = audio_codec != kSbMediaAudioCodecNone;
+
+ is_creating_player_ = true;
+
+ if (max_video_capabilities_.empty()) {
+ FormatSupportQueryMetrics::PrintAndResetMetrics();
+ }
+
+ player_creation_time_ = SbTimeGetMonotonicNow();
+
+ SbPlayerCreationParam creation_param = {};
+ creation_param.drm_system = drm_system_;
+ creation_param.audio_sample_info = audio_sample_info_;
+ creation_param.video_sample_info = video_sample_info_;
+ // TODO: This is temporary for supporting background media playback.
+ // Need to be removed with media refactor.
+ if (!is_visible) {
+ creation_param.video_sample_info.codec = kSbMediaVideoCodecNone;
+ }
+ creation_param.output_mode = output_mode_;
+ DCHECK_EQ(sbplayer_interface_->GetPreferredOutputMode(&creation_param),
+ output_mode_);
+ player_ = sbplayer_interface_->Create(
+ window_, &creation_param, &SbPlayerBridge::DeallocateSampleCB,
+ &SbPlayerBridge::DecoderStatusCB, &SbPlayerBridge::PlayerStatusCB,
+ &SbPlayerBridge::PlayerErrorCB, this,
+ get_decode_target_graphics_context_provider_func_.Run());
+
+ is_creating_player_ = false;
+
+ if (!SbPlayerIsValid(player_)) {
+ return;
+ }
+
+ if (output_mode_ == kSbPlayerOutputModeDecodeToTexture) {
+ // If the player is setup to decode to texture, then provide Cobalt with
+ // a method of querying that texture.
+ decode_target_provider_->SetGetCurrentSbDecodeTargetFunction(base::Bind(
+ &SbPlayerBridge::GetCurrentSbDecodeTarget, base::Unretained(this)));
+ }
+ decode_target_provider_->SetOutputMode(
+ ToVideoFrameProviderOutputMode(output_mode_));
+ set_bounds_helper_->SetPlayerBridge(this);
+
+ base::AutoLock auto_lock(lock_);
+ UpdateBounds_Locked();
+}
+
+void SbPlayerBridge::WriteNextBuffersFromCache(DemuxerStream::Type type,
+ int max_buffers_per_write) {
+ DCHECK(state_ != kSuspended);
+#if SB_HAS(PLAYER_WITH_URL)
+ DCHECK(!is_url_based_);
+#endif // SB_HAS(PLAYER_WITH_URL)
+
+ DCHECK(SbPlayerIsValid(player_));
+
+ std::vector<scoped_refptr<DecoderBuffer>> buffers;
+ buffers.reserve(max_buffers_per_write);
+
+ // TODO: DecoderBufferCache doesn't respect config change during resume
+ // b/243308409
+ for (int i = 0; i < max_buffers_per_write; i++) {
+ const scoped_refptr<DecoderBuffer>& buffer =
+ decoder_buffer_cache_.GetBuffer(type);
+ if (!buffer) {
+ break;
+ }
+ decoder_buffer_cache_.AdvanceToNextBuffer(type);
+ buffers.push_back(buffer);
+ }
+
+ WriteBuffersInternal(type, buffers);
+}
+
+void SbPlayerBridge::WriteBuffersInternal(
+ DemuxerStream::Type type,
+ const std::vector<scoped_refptr<DecoderBuffer>>& buffers) {
+#if SB_HAS(PLAYER_WITH_URL)
+ DCHECK(!is_url_based_);
+#endif // SB_HAS(PLAYER_WITH_URL)
+
+ auto sample_type = DemuxerStreamTypeToSbMediaType(type);
+
+ std::vector<SbPlayerSampleInfo> gathered_sbplayer_sample_infos;
+ std::vector<SbDrmSampleInfo> gathered_sbplayer_sample_infos_drm_info;
+ std::vector<SbDrmSubSampleMapping>
+ gathered_sbplayer_sample_infos_subsample_mapping;
+ std::vector<SbPlayerSampleSideData> gathered_sbplayer_sample_infos_side_data;
+
+ gathered_sbplayer_sample_infos.reserve(buffers.size());
+ gathered_sbplayer_sample_infos_drm_info.reserve(buffers.size());
+ gathered_sbplayer_sample_infos_subsample_mapping.reserve(buffers.size());
+ gathered_sbplayer_sample_infos_side_data.reserve(buffers.size());
+
+ for (int i = 0; i < buffers.size(); i++) {
+ const auto& buffer = buffers[i];
+ if (buffer->end_of_stream()) {
+ DCHECK_EQ(i, buffers.size() - 1);
+ if (buffers.size() > 1) {
+ if (type == DemuxerStream::AUDIO) {
+ pending_audio_eos_buffer_ = true;
+ } else {
+ pending_video_eos_buffer_ = true;
+ }
+
+ DCHECK(!gathered_sbplayer_sample_infos.empty());
+ sbplayer_interface_->WriteSample(player_, sample_type,
+ gathered_sbplayer_sample_infos.data(),
+ gathered_sbplayer_sample_infos.size());
+ } else {
+ sbplayer_interface_->WriteEndOfStream(
+ player_, DemuxerStreamTypeToSbMediaType(type));
+ }
+ return;
+ }
+
+ DecodingBuffers::iterator iter = decoding_buffers_.find(buffer->data());
+ if (iter == decoding_buffers_.end()) {
+ decoding_buffers_[buffer->data()] = std::make_pair(buffer, 1);
+ } else {
+ ++iter->second.second;
+ }
+
+ if (sample_type == kSbMediaTypeAudio && first_audio_sample_time_ == 0) {
+ first_audio_sample_time_ = SbTimeGetMonotonicNow();
+ } else if (sample_type == kSbMediaTypeVideo &&
+ first_video_sample_time_ == 0) {
+ first_video_sample_time_ = SbTimeGetMonotonicNow();
+ }
+
+ gathered_sbplayer_sample_infos_drm_info.push_back(SbDrmSampleInfo());
+ SbDrmSampleInfo* drm_info = &gathered_sbplayer_sample_infos_drm_info[i];
+
+ gathered_sbplayer_sample_infos_subsample_mapping.push_back(
+ SbDrmSubSampleMapping());
+ SbDrmSubSampleMapping* subsample_mapping =
+ &gathered_sbplayer_sample_infos_subsample_mapping[i];
+
+ drm_info->subsample_count = 0;
+ if (buffer->decrypt_config()) {
+ FillDrmSampleInfo(buffer, drm_info, subsample_mapping);
+ }
+
+ gathered_sbplayer_sample_infos_side_data.push_back(
+ SbPlayerSampleSideData());
+ SbPlayerSampleSideData* side_data =
+ &gathered_sbplayer_sample_infos_side_data[i];
+
+ SbPlayerSampleInfo sample_info = {};
+ sample_info.type = sample_type;
+ sample_info.buffer = buffer->data();
+ sample_info.buffer_size = buffer->data_size();
+ sample_info.timestamp = buffer->timestamp().InMicroseconds();
+
+ if (buffer->side_data_size() > 0) {
+ // We only support at most one side data currently.
+ side_data->data = buffer->side_data();
+ side_data->size = buffer->side_data_size();
+ sample_info.side_data = side_data;
+ sample_info.side_data_count = 1;
+ }
+
+ if (sample_type == kSbMediaTypeAudio) {
+ sample_info.audio_sample_info = audio_sample_info_;
+ } else {
+ DCHECK(sample_type == kSbMediaTypeVideo);
+ sample_info.video_sample_info = video_sample_info_;
+ sample_info.video_sample_info.is_key_frame = buffer->is_key_frame();
+ }
+ if (drm_info->subsample_count > 0) {
+ sample_info.drm_info = drm_info;
+ } else {
+ sample_info.drm_info = NULL;
+ }
+ gathered_sbplayer_sample_infos.push_back(sample_info);
+ }
+
+ if (!gathered_sbplayer_sample_infos.empty()) {
+ sbplayer_interface_->WriteSample(player_, sample_type,
+ gathered_sbplayer_sample_infos.data(),
+ gathered_sbplayer_sample_infos.size());
+ }
+}
+
+SbDecodeTarget SbPlayerBridge::GetCurrentSbDecodeTarget() {
+ return sbplayer_interface_->GetCurrentFrame(player_);
+}
+
+SbPlayerOutputMode SbPlayerBridge::GetSbPlayerOutputMode() {
+ return output_mode_;
+}
+
+void SbPlayerBridge::GetInfo_Locked(uint32* video_frames_decoded,
+ uint32* video_frames_dropped,
+ base::TimeDelta* media_time) {
+ lock_.AssertAcquired();
+ if (state_ == kSuspended) {
+ if (video_frames_decoded) {
+ *video_frames_decoded = cached_video_frames_decoded_;
+ }
+ if (video_frames_dropped) {
+ *video_frames_dropped = cached_video_frames_dropped_;
+ }
+ if (media_time) {
+ *media_time = preroll_timestamp_;
+ }
+ return;
+ }
+
+ DCHECK(SbPlayerIsValid(player_));
+
+ SbPlayerInfo2 info;
+ sbplayer_interface_->GetInfo(player_, &info);
+
+ if (media_time) {
+ *media_time =
+ base::TimeDelta::FromMicroseconds(info.current_media_timestamp);
+ }
+ if (video_frames_decoded) {
+ *video_frames_decoded = info.total_video_frames;
+ }
+ if (video_frames_dropped) {
+ *video_frames_dropped = info.dropped_video_frames;
+ }
+}
+
+void SbPlayerBridge::UpdateBounds_Locked() {
+ lock_.AssertAcquired();
+ DCHECK(SbPlayerIsValid(player_));
+
+ if (!set_bounds_z_index_ || !set_bounds_rect_) {
+ return;
+ }
+
+ auto& rect = *set_bounds_rect_;
+ sbplayer_interface_->SetBounds(player_, *set_bounds_z_index_, rect.x(),
+ rect.y(), rect.width(), rect.height());
+}
+
+void SbPlayerBridge::ClearDecoderBufferCache() {
+ DCHECK(task_runner_->BelongsToCurrentThread());
+
+ if (state_ != kResuming) {
+ base::TimeDelta media_time;
+ GetInfo(NULL, NULL, &media_time);
+ decoder_buffer_cache_.ClearSegmentsBeforeMediaTime(media_time);
+ }
+
+ task_runner_->PostDelayedTask(
+ FROM_HERE,
+ base::Bind(&SbPlayerBridge::CallbackHelper::ClearDecoderBufferCache,
+ callback_helper_),
+ base::TimeDelta::FromMilliseconds(
+ kClearDecoderCacheIntervalInMilliseconds));
+}
+
+void SbPlayerBridge::OnDecoderStatus(SbPlayer player, SbMediaType type,
+ SbPlayerDecoderState state, int ticket) {
+#if SB_HAS(PLAYER_WITH_URL)
+ DCHECK(!is_url_based_);
+#endif // SB_HAS(PLAYER_WITH_URL)
+ DCHECK(task_runner_->BelongsToCurrentThread());
+
+ if (player_ != player || ticket != ticket_) {
+ return;
+ }
+
+ DCHECK_NE(state_, kSuspended);
+
+ switch (state) {
+ case kSbPlayerDecoderStateNeedsData:
+ break;
+ }
+
+ DemuxerStream::Type stream_type =
+ ::media::SbMediaTypeToDemuxerStreamType(type);
+
+ if (stream_type == DemuxerStream::AUDIO && pending_audio_eos_buffer_) {
+ SbPlayerWriteEndOfStream(player_, type);
+ pending_audio_eos_buffer_ = false;
+ return;
+ } else if (stream_type == DemuxerStream::VIDEO && pending_video_eos_buffer_) {
+ SbPlayerWriteEndOfStream(player_, type);
+ pending_video_eos_buffer_ = false;
+ return;
+ }
+
+ auto max_number_of_samples_to_write =
+ SbPlayerGetMaximumNumberOfSamplesPerWrite(player_, type);
+ if (state_ == kResuming) {
+ if (decoder_buffer_cache_.GetBuffer(stream_type)) {
+ WriteNextBuffersFromCache(stream_type, max_number_of_samples_to_write);
+ return;
+ }
+ if (!decoder_buffer_cache_.GetBuffer(DemuxerStream::AUDIO) &&
+ !decoder_buffer_cache_.GetBuffer(DemuxerStream::VIDEO)) {
+ state_ = kPlaying;
+ }
+ }
+
+ host_->OnNeedData(stream_type, max_number_of_samples_to_write);
+}
+
+void SbPlayerBridge::OnPlayerStatus(SbPlayer player, SbPlayerState state,
+ int ticket) {
+ TRACE_EVENT1("cobalt::media", "SbPlayerBridge::OnPlayerStatus", "state",
+ state);
+ DCHECK(task_runner_->BelongsToCurrentThread());
+
+ if (player_ != player) {
+ return;
+ }
+
+ DCHECK_NE(state_, kSuspended);
+
+ if (ticket != SB_PLAYER_INITIAL_TICKET && ticket != ticket_) {
+ return;
+ }
+
+ if (state == kSbPlayerStateInitialized) {
+ if (ticket_ == SB_PLAYER_INITIAL_TICKET) {
+ ++ticket_;
+ }
+ if (sb_player_state_initialized_time_ == 0) {
+ sb_player_state_initialized_time_ = SbTimeGetMonotonicNow();
+ }
+ sbplayer_interface_->Seek(player_, preroll_timestamp_.InMicroseconds(),
+ ticket_);
+ SetVolume(volume_);
+ sbplayer_interface_->SetPlaybackRate(player_, playback_rate_);
+ return;
+ }
+ if (state == kSbPlayerStatePrerolling &&
+ sb_player_state_prerolling_time_ == 0) {
+ sb_player_state_prerolling_time_ = SbTimeGetMonotonicNow();
+ } else if (state == kSbPlayerStatePresenting &&
+ sb_player_state_presenting_time_ == 0) {
+ sb_player_state_presenting_time_ = SbTimeGetMonotonicNow();
+#if !defined(COBALT_BUILD_TYPE_GOLD)
+ LogStartupLatency();
+#endif // !defined(COBALT_BUILD_TYPE_GOLD)
+ }
+ host_->OnPlayerStatus(state);
+}
+
+void SbPlayerBridge::OnPlayerError(SbPlayer player, SbPlayerError error,
+ const std::string& message) {
+ DCHECK(task_runner_->BelongsToCurrentThread());
+
+ if (player_ != player) {
+ return;
+ }
+ host_->OnPlayerError(error, message);
+}
+
+void SbPlayerBridge::OnDeallocateSample(const void* sample_buffer) {
+#if SB_HAS(PLAYER_WITH_URL)
+ DCHECK(!is_url_based_);
+#endif // SB_HAS(PLAYER_WITH_URL)
+ DCHECK(task_runner_->BelongsToCurrentThread());
+
+ DecodingBuffers::iterator iter = decoding_buffers_.find(sample_buffer);
+ DCHECK(iter != decoding_buffers_.end());
+ if (iter == decoding_buffers_.end()) {
+ LOG(ERROR) << "SbPlayerBridge::OnDeallocateSample encounters unknown "
+ << "sample_buffer " << sample_buffer;
+ return;
+ }
+ --iter->second.second;
+ if (iter->second.second == 0) {
+ decoding_buffers_.erase(iter);
+ }
+}
+
+bool SbPlayerBridge::TryToSetPlayerCreationErrorMessage(
+ const std::string& message) {
+ DCHECK(task_runner_->BelongsToCurrentThread());
+ if (is_creating_player_) {
+ player_creation_error_message_ = message;
+ return true;
+ }
+ LOG(INFO) << "TryToSetPlayerCreationErrorMessage() "
+ "is called when |is_creating_player_| "
+ "is false. Error message is ignored.";
+ return false;
+}
+
+// static
+void SbPlayerBridge::DecoderStatusCB(SbPlayer player, void* context,
+ SbMediaType type,
+ SbPlayerDecoderState state, int ticket) {
+ SbPlayerBridge* helper = static_cast<SbPlayerBridge*>(context);
+ helper->task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&SbPlayerBridge::CallbackHelper::OnDecoderStatus,
+ helper->callback_helper_, player, type, state, ticket));
+}
+
+// static
+void SbPlayerBridge::PlayerStatusCB(SbPlayer player, void* context,
+ SbPlayerState state, int ticket) {
+ SbPlayerBridge* helper = static_cast<SbPlayerBridge*>(context);
+ helper->task_runner_->PostTask(
+ FROM_HERE, base::Bind(&SbPlayerBridge::CallbackHelper::OnPlayerStatus,
+ helper->callback_helper_, player, state, ticket));
+}
+
+// static
+void SbPlayerBridge::PlayerErrorCB(SbPlayer player, void* context,
+ SbPlayerError error, const char* message) {
+ SbPlayerBridge* helper = static_cast<SbPlayerBridge*>(context);
+ if (player == kSbPlayerInvalid) {
+ // TODO: Simplify by combining the functionality of
+ // TryToSetPlayerCreationErrorMessage() with OnPlayerError().
+ if (helper->TryToSetPlayerCreationErrorMessage(message)) {
+ return;
+ }
+ }
+ helper->task_runner_->PostTask(
+ FROM_HERE, base::Bind(&SbPlayerBridge::CallbackHelper::OnPlayerError,
+ helper->callback_helper_, player, error,
+ message ? std::string(message) : ""));
+}
+
+// static
+void SbPlayerBridge::DeallocateSampleCB(SbPlayer player, void* context,
+ const void* sample_buffer) {
+ SbPlayerBridge* helper = static_cast<SbPlayerBridge*>(context);
+ helper->task_runner_->PostTask(
+ FROM_HERE, base::Bind(&SbPlayerBridge::CallbackHelper::OnDeallocateSample,
+ helper->callback_helper_, sample_buffer));
+}
+
+#if SB_HAS(PLAYER_WITH_URL)
+SbPlayerOutputMode SbPlayerBridge::ComputeSbUrlPlayerOutputMode(
+ bool prefer_decode_to_texture) {
+ // Try to choose the output mode according to the passed in value of
+ // |prefer_decode_to_texture|. If the preferred output mode is unavailable
+ // though, fallback to an output mode that is available.
+ SbPlayerOutputMode output_mode = kSbPlayerOutputModeInvalid;
+ if (sbplayer_interface_->GetUrlPlayerOutputModeSupported(
+ kSbPlayerOutputModePunchOut)) {
+ output_mode = kSbPlayerOutputModePunchOut;
+ }
+ if ((prefer_decode_to_texture || output_mode == kSbPlayerOutputModeInvalid) &&
+ sbplayer_interface_->GetUrlPlayerOutputModeSupported(
+ kSbPlayerOutputModeDecodeToTexture)) {
+ output_mode = kSbPlayerOutputModeDecodeToTexture;
+ }
+ CHECK_NE(kSbPlayerOutputModeInvalid, output_mode);
+
+ return output_mode;
+}
+#endif // SB_HAS(PLAYER_WITH_URL)
+
+SbPlayerOutputMode SbPlayerBridge::ComputeSbPlayerOutputMode(
+ bool prefer_decode_to_texture) const {
+ SbPlayerCreationParam creation_param = {};
+ creation_param.drm_system = drm_system_;
+ creation_param.audio_sample_info = audio_sample_info_;
+ creation_param.video_sample_info = video_sample_info_;
+
+ // Try to choose |kSbPlayerOutputModeDecodeToTexture| when
+ // |prefer_decode_to_texture| is true.
+ if (prefer_decode_to_texture) {
+ creation_param.output_mode = kSbPlayerOutputModeDecodeToTexture;
+ } else {
+ creation_param.output_mode = kSbPlayerOutputModePunchOut;
+ }
+ auto output_mode =
+ sbplayer_interface_->GetPreferredOutputMode(&creation_param);
+ CHECK_NE(kSbPlayerOutputModeInvalid, output_mode);
+ return output_mode;
+}
+
+void SbPlayerBridge::LogStartupLatency() const {
+ std::string first_events_str;
+ if (set_drm_system_ready_cb_time_ == -1) {
+ first_events_str =
+ starboard::FormatString("%-40s0 us", "SbPlayerCreate() called");
+ } else if (set_drm_system_ready_cb_time_ < player_creation_time_) {
+ first_events_str = starboard::FormatString(
+ "%-40s0 us\n%-40s%" PRId64 " us", "set_drm_system_ready_cb called",
+ "SbPlayerCreate() called",
+ player_creation_time_ - set_drm_system_ready_cb_time_);
+ } else {
+ first_events_str = starboard::FormatString(
+ "%-40s0 us\n%-40s%" PRId64 " us", "SbPlayerCreate() called",
+ "set_drm_system_ready_cb called",
+ set_drm_system_ready_cb_time_ - player_creation_time_);
+ }
+
+ SbTime first_event_time =
+ std::max(player_creation_time_, set_drm_system_ready_cb_time_);
+ SbTime player_initialization_time_delta =
+ sb_player_state_initialized_time_ - first_event_time;
+ SbTime player_preroll_time_delta =
+ sb_player_state_prerolling_time_ - sb_player_state_initialized_time_;
+ SbTime first_audio_sample_time_delta = std::max(
+ first_audio_sample_time_ - sb_player_state_prerolling_time_, SbTime(0));
+ SbTime first_video_sample_time_delta = std::max(
+ first_video_sample_time_ - sb_player_state_prerolling_time_, SbTime(0));
+ SbTime player_presenting_time_delta =
+ sb_player_state_presenting_time_ -
+ std::max(first_audio_sample_time_, first_video_sample_time_);
+ SbTime startup_latency = sb_player_state_presenting_time_ - first_event_time;
+
+ StatisticsWrapper::GetInstance()->startup_latency.AddSample(startup_latency,
+ 1);
+
+ // clang-format off
+ LOG(INFO) << starboard::FormatString(
+ "\nSbPlayer startup latencies: %" PRId64 " us\n"
+ " Event name time since last event\n"
+ " %s\n" // |first_events_str| populated above
+ " kSbPlayerStateInitialized received %" PRId64 " us\n"
+ " kSbPlayerStatePrerolling received %" PRId64 " us\n"
+ " First media sample(s) written [a/v] %" PRId64 "/%" PRId64 " us\n"
+ " kSbPlayerStatePresenting received %" PRId64 " us\n"
+ " Startup latency statistics (us):"
+ " min: %" PRId64 ", median: %" PRId64 ", average: %" PRId64
+ ", max: %" PRId64,
+ startup_latency,
+ first_events_str.c_str(), player_initialization_time_delta,
+ player_preroll_time_delta, first_audio_sample_time_delta,
+ first_video_sample_time_delta, player_presenting_time_delta,
+ StatisticsWrapper::GetInstance()->startup_latency.min(),
+ StatisticsWrapper::GetInstance()->startup_latency.GetMedian(),
+ StatisticsWrapper::GetInstance()->startup_latency.average(),
+ StatisticsWrapper::GetInstance()->startup_latency.max());
+ // clang-format on
+}
+
+} // namespace media
+} // namespace cobalt
diff --git a/cobalt/media/base/sbplayer_bridge.h b/cobalt/media/base/sbplayer_bridge.h
new file mode 100644
index 0000000..1ea4cf6
--- /dev/null
+++ b/cobalt/media/base/sbplayer_bridge.h
@@ -0,0 +1,315 @@
+// Copyright 2016 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_MEDIA_BASE_SBPLAYER_BRIDGE_H_
+#define COBALT_MEDIA_BASE_SBPLAYER_BRIDGE_H_
+
+#include <map>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "base/memory/ref_counted.h"
+#include "base/message_loop/message_loop.h"
+#include "base/synchronization/lock.h"
+#include "base/time/time.h"
+#include "cobalt/media/base/decode_target_provider.h"
+#include "cobalt/media/base/decoder_buffer_cache.h"
+#include "cobalt/media/base/sbplayer_interface.h"
+#include "cobalt/media/base/sbplayer_set_bounds_helper.h"
+#include "starboard/media.h"
+#include "starboard/player.h"
+#include "third_party/chromium/media/base/audio_decoder_config.h"
+#include "third_party/chromium/media/base/decoder_buffer.h"
+#include "third_party/chromium/media/base/demuxer_stream.h"
+#include "third_party/chromium/media/base/video_decoder_config.h"
+#include "third_party/chromium/media/cobalt/ui/gfx/geometry/rect.h"
+
+namespace cobalt {
+namespace media {
+
+// TODO: Add switch to disable caching
+class SbPlayerBridge {
+ typedef ::media::AudioDecoderConfig AudioDecoderConfig;
+ typedef ::media::DecoderBuffer DecoderBuffer;
+ typedef ::media::DemuxerStream DemuxerStream;
+ typedef ::media::VideoDecoderConfig VideoDecoderConfig;
+
+ public:
+ class Host {
+ public:
+ virtual void OnNeedData(DemuxerStream::Type type,
+ int max_number_of_buffers_to_write) = 0;
+ virtual void OnPlayerStatus(SbPlayerState state) = 0;
+ virtual void OnPlayerError(SbPlayerError error,
+ const std::string& message) = 0;
+
+ protected:
+ ~Host() {}
+ };
+
+ // Call to get the SbDecodeTargetGraphicsContextProvider for SbPlayerCreate().
+ typedef base::Callback<SbDecodeTargetGraphicsContextProvider*()>
+ GetDecodeTargetGraphicsContextProviderFunc;
+
+#if SB_HAS(PLAYER_WITH_URL)
+ typedef base::Callback<void(const char*, const unsigned char*, unsigned)>
+ OnEncryptedMediaInitDataEncounteredCB;
+ // Create an SbPlayerBridge with url-based player.
+ SbPlayerBridge(SbPlayerInterface* interface,
+ const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
+ const std::string& url, SbWindow window, Host* host,
+ SbPlayerSetBoundsHelper* set_bounds_helper,
+ bool allow_resume_after_suspend, bool prefer_decode_to_texture,
+ const OnEncryptedMediaInitDataEncounteredCB&
+ encrypted_media_init_data_encountered_cb,
+ DecodeTargetProvider* const decode_target_provider);
+#endif // SB_HAS(PLAYER_WITH_URL)
+ // Create a SbPlayerBridge with normal player
+ SbPlayerBridge(SbPlayerInterface* interface,
+ const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
+ const GetDecodeTargetGraphicsContextProviderFunc&
+ get_decode_target_graphics_context_provider_func,
+ const AudioDecoderConfig& audio_config,
+ const std::string& audio_mime_type,
+ const VideoDecoderConfig& video_config,
+ const std::string& video_mime_type, SbWindow window,
+ SbDrmSystem drm_system, Host* host,
+ SbPlayerSetBoundsHelper* set_bounds_helper,
+ bool allow_resume_after_suspend, bool prefer_decode_to_texture,
+ DecodeTargetProvider* const decode_target_provider,
+ const std::string& max_video_capabilities);
+
+ ~SbPlayerBridge();
+
+ bool IsValid() const { return SbPlayerIsValid(player_); }
+
+ void UpdateAudioConfig(const AudioDecoderConfig& audio_config,
+ const std::string& mime_type);
+ void UpdateVideoConfig(const VideoDecoderConfig& video_config,
+ const std::string& mime_type);
+
+ void WriteBuffers(DemuxerStream::Type type,
+ const std::vector<scoped_refptr<DecoderBuffer>>& buffers);
+
+ void SetBounds(int z_index, const gfx::Rect& rect);
+
+ void PrepareForSeek();
+ void Seek(base::TimeDelta time);
+
+ void SetVolume(float volume);
+ void SetPlaybackRate(double playback_rate);
+ void GetInfo(uint32* video_frames_decoded, uint32* video_frames_dropped,
+ base::TimeDelta* media_time);
+
+#if SB_HAS(PLAYER_WITH_URL)
+ void GetUrlPlayerBufferedTimeRanges(base::TimeDelta* buffer_start_time,
+ base::TimeDelta* buffer_length_time);
+ void GetVideoResolution(int* frame_width, int* frame_height);
+ base::TimeDelta GetDuration();
+ base::TimeDelta GetStartDate();
+ void SetDrmSystem(SbDrmSystem drm_system);
+#endif // SB_HAS(PLAYER_WITH_URL)
+
+ void Suspend();
+ // TODO: This is temporary for supporting background media playback.
+ // Need to be removed with media refactor.
+ void Resume(SbWindow window);
+
+ // These functions help the pipeline report an error message on a player
+ // creation error. TryToSetPlayerCreationErrorMessage() will set
+ // |player_creation_error_message_| and return true when called while
+ // |is_creating_player_| is true, else it will do nothing and return false.
+ bool TryToSetPlayerCreationErrorMessage(const std::string& message);
+ std::string GetPlayerCreationErrorMessage() const {
+ return player_creation_error_message_;
+ }
+
+ SbDecodeTarget GetCurrentSbDecodeTarget();
+ SbPlayerOutputMode GetSbPlayerOutputMode();
+
+ void RecordSetDrmSystemReadyTime(SbTimeMonotonic timestamp) {
+ set_drm_system_ready_cb_time_ = timestamp;
+ }
+
+ private:
+ enum State {
+ kPlaying,
+ kSuspended,
+ kResuming,
+ };
+
+ // This class ensures that the callbacks posted to |task_runner_| are ignored
+ // automatically once SbPlayerBridge is destroyed.
+ class CallbackHelper : public base::RefCountedThreadSafe<CallbackHelper> {
+ public:
+ explicit CallbackHelper(SbPlayerBridge* player_bridge);
+
+ void ClearDecoderBufferCache();
+
+ void OnDecoderStatus(SbPlayer player, SbMediaType type,
+ SbPlayerDecoderState state, int ticket);
+ void OnPlayerStatus(SbPlayer player, SbPlayerState state, int ticket);
+ void OnPlayerError(SbPlayer player, SbPlayerError error,
+ const std::string& message);
+ void OnDeallocateSample(const void* sample_buffer);
+
+ void ResetPlayer();
+
+ private:
+ base::Lock lock_;
+ SbPlayerBridge* player_bridge_;
+ };
+
+ static const int64 kClearDecoderCacheIntervalInMilliseconds = 1000;
+
+ // A map from raw data pointer returned by DecoderBuffer::GetData() to the
+ // DecoderBuffer and a reference count. The reference count indicates how
+ // many instances of the DecoderBuffer is currently being decoded in the
+ // pipeline.
+ typedef std::map<const void*, std::pair<scoped_refptr<DecoderBuffer>, int>>
+ DecodingBuffers;
+
+#if SB_HAS(PLAYER_WITH_URL)
+ OnEncryptedMediaInitDataEncounteredCB
+ on_encrypted_media_init_data_encountered_cb_;
+
+ static void EncryptedMediaInitDataEncounteredCB(
+ SbPlayer player, void* context, const char* init_data_type,
+ const unsigned char* init_data, unsigned int init_data_length);
+
+ void CreateUrlPlayer(const std::string& url);
+#endif // SB_HAS(PLAYER_WITH_URL)
+ void CreatePlayer();
+
+ void WriteNextBuffersFromCache(DemuxerStream::Type type,
+ int max_buffers_per_write);
+ void WriteBuffersInternal(
+ DemuxerStream::Type type,
+ const std::vector<scoped_refptr<DecoderBuffer>>& buffers);
+
+ void GetInfo_Locked(uint32* video_frames_decoded,
+ uint32* video_frames_dropped,
+ base::TimeDelta* media_time);
+ void UpdateBounds_Locked();
+
+ void ClearDecoderBufferCache();
+
+ void OnDecoderStatus(SbPlayer player, SbMediaType type,
+ SbPlayerDecoderState state, int ticket);
+ void OnPlayerStatus(SbPlayer player, SbPlayerState state, int ticket);
+ void OnPlayerError(SbPlayer player, SbPlayerError error,
+ const std::string& message);
+ void OnDeallocateSample(const void* sample_buffer);
+
+ static void DecoderStatusCB(SbPlayer player, void* context, SbMediaType type,
+ SbPlayerDecoderState state, int ticket);
+ static void PlayerStatusCB(SbPlayer player, void* context,
+ SbPlayerState state, int ticket);
+ static void PlayerErrorCB(SbPlayer player, void* context, SbPlayerError error,
+ const char* message);
+ static void DeallocateSampleCB(SbPlayer player, void* context,
+ const void* sample_buffer);
+
+#if SB_HAS(PLAYER_WITH_URL)
+ SbPlayerOutputMode ComputeSbUrlPlayerOutputMode(
+ bool prefer_decode_to_texture);
+#endif // SB_HAS(PLAYER_WITH_URL)
+ // Returns the output mode that should be used for a video with the given
+ // specifications.
+ SbPlayerOutputMode ComputeSbPlayerOutputMode(
+ bool prefer_decode_to_texture) const;
+
+ void LogStartupLatency() const;
+
+// The following variables are initialized in the ctor and never changed.
+#if SB_HAS(PLAYER_WITH_URL)
+ std::string url_;
+#endif // SB_HAS(PLAYER_WITH_URL)
+ SbPlayerInterface* sbplayer_interface_;
+ const scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
+ const GetDecodeTargetGraphicsContextProviderFunc
+ get_decode_target_graphics_context_provider_func_;
+ scoped_refptr<CallbackHelper> callback_helper_;
+ SbWindow window_;
+ SbDrmSystem drm_system_ = kSbDrmSystemInvalid;
+ Host* const host_;
+ // Consider merge |SbPlayerSetBoundsHelper| into CallbackHelper.
+ SbPlayerSetBoundsHelper* const set_bounds_helper_;
+ const bool allow_resume_after_suspend_;
+
+ // The following variables are only changed or accessed from the
+ // |task_runner_|.
+ AudioDecoderConfig audio_config_;
+ VideoDecoderConfig video_config_;
+ SbMediaAudioSampleInfo audio_sample_info_ = {};
+ SbMediaVideoSampleInfo video_sample_info_ = {};
+ DecodingBuffers decoding_buffers_;
+ int ticket_ = SB_PLAYER_INITIAL_TICKET;
+ float volume_ = 1.0f;
+ double playback_rate_ = 0.0;
+ bool seek_pending_ = false;
+ DecoderBufferCache decoder_buffer_cache_;
+
+ // The following variables can be accessed from GetInfo(), which can be called
+ // from any threads. So some of their usages have to be guarded by |lock_|.
+ base::Lock lock_;
+
+ // Stores the |z_index| and |rect| parameters of the latest SetBounds() call.
+ base::Optional<int> set_bounds_z_index_;
+ base::Optional<gfx::Rect> set_bounds_rect_;
+ State state_ = kPlaying;
+ SbPlayer player_;
+ uint32 cached_video_frames_decoded_;
+ uint32 cached_video_frames_dropped_;
+ base::TimeDelta preroll_timestamp_;
+
+ // Keep track of the output mode we are supposed to output to.
+ SbPlayerOutputMode output_mode_;
+
+ DecodeTargetProvider* const decode_target_provider_;
+
+ // Keep copies of the mime type strings instead of using the ones in the
+ // DemuxerStreams to ensure that the strings are always valid.
+ std::string audio_mime_type_;
+ std::string video_mime_type_;
+ // A string of video maximum capabilities.
+ std::string max_video_capabilities_;
+
+ // Keep track of errors during player creation.
+ bool is_creating_player_ = false;
+ std::string player_creation_error_message_;
+
+ // Variables related to tracking player startup latencies.
+ SbTimeMonotonic set_drm_system_ready_cb_time_ = -1;
+ SbTimeMonotonic player_creation_time_ = 0;
+ SbTimeMonotonic sb_player_state_initialized_time_ = 0;
+ SbTimeMonotonic sb_player_state_prerolling_time_ = 0;
+ SbTimeMonotonic first_audio_sample_time_ = 0;
+ SbTimeMonotonic first_video_sample_time_ = 0;
+ SbTimeMonotonic sb_player_state_presenting_time_ = 0;
+
+#if SB_HAS(PLAYER_WITH_URL)
+ const bool is_url_based_;
+#endif // SB_HAS(PLAYER_WITH_URL)
+
+ // Used for Gathered Sample Write.
+ bool pending_audio_eos_buffer_ = false;
+ bool pending_video_eos_buffer_ = false;
+};
+
+} // namespace media
+} // namespace cobalt
+
+#endif // COBALT_MEDIA_BASE_SBPLAYER_BRIDGE_H_
diff --git a/cobalt/media/base/sbplayer_interface.cc b/cobalt/media/base/sbplayer_interface.cc
new file mode 100644
index 0000000..7c44dae
--- /dev/null
+++ b/cobalt/media/base/sbplayer_interface.cc
@@ -0,0 +1,113 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/media/base/sbplayer_interface.h"
+
+namespace cobalt {
+namespace media {
+
+SbPlayer DefaultSbPlayerInterface::Create(
+ SbWindow window, const SbPlayerCreationParam* creation_param,
+ SbPlayerDeallocateSampleFunc sample_deallocate_func,
+ SbPlayerDecoderStatusFunc decoder_status_func,
+ SbPlayerStatusFunc player_status_func, SbPlayerErrorFunc player_error_func,
+ void* context, SbDecodeTargetGraphicsContextProvider* context_provider) {
+ return SbPlayerCreate(window, creation_param, sample_deallocate_func,
+ decoder_status_func, player_status_func,
+ player_error_func, context, context_provider);
+}
+
+SbPlayerOutputMode DefaultSbPlayerInterface::GetPreferredOutputMode(
+ const SbPlayerCreationParam* creation_param) {
+ return SbPlayerGetPreferredOutputMode(creation_param);
+}
+
+void DefaultSbPlayerInterface::Destroy(SbPlayer player) {
+ SbPlayerDestroy(player);
+}
+
+void DefaultSbPlayerInterface::Seek(SbPlayer player, SbTime seek_to_timestamp,
+ int ticket) {
+ SbPlayerSeek2(player, seek_to_timestamp, ticket);
+}
+
+void DefaultSbPlayerInterface::WriteSample(
+ SbPlayer player, SbMediaType sample_type,
+ const SbPlayerSampleInfo* sample_infos, int number_of_sample_infos) {
+ SbPlayerWriteSample2(player, sample_type, sample_infos,
+ number_of_sample_infos);
+}
+
+int DefaultSbPlayerInterface::GetMaximumNumberOfSamplesPerWrite(
+ SbPlayer player, SbMediaType sample_type) {
+ return SbPlayerGetMaximumNumberOfSamplesPerWrite(player, sample_type);
+}
+
+void DefaultSbPlayerInterface::WriteEndOfStream(SbPlayer player,
+ SbMediaType stream_type) {
+ SbPlayerWriteEndOfStream(player, stream_type);
+}
+
+void DefaultSbPlayerInterface::SetBounds(SbPlayer player, int z_index, int x,
+ int y, int width, int height) {
+ SbPlayerSetBounds(player, z_index, x, y, width, height);
+}
+
+bool DefaultSbPlayerInterface::SetPlaybackRate(SbPlayer player,
+ double playback_rate) {
+ return SbPlayerSetPlaybackRate(player, playback_rate);
+}
+
+void DefaultSbPlayerInterface::SetVolume(SbPlayer player, double volume) {
+ SbPlayerSetVolume(player, volume);
+}
+
+void DefaultSbPlayerInterface::GetInfo(SbPlayer player,
+ SbPlayerInfo2* out_player_info2) {
+ SbPlayerGetInfo2(player, out_player_info2);
+}
+
+SbDecodeTarget DefaultSbPlayerInterface::GetCurrentFrame(SbPlayer player) {
+ return SbPlayerGetCurrentFrame(player);
+}
+
+#if SB_HAS(PLAYER_WITH_URL)
+SbPlayer DefaultSbPlayerInterface::CreateUrlPlayer(
+ const char* url, SbWindow window, SbPlayerStatusFunc player_status_func,
+ SbPlayerEncryptedMediaInitDataEncounteredCB
+ encrypted_media_init_data_encountered_cb,
+ SbPlayerErrorFunc player_error_func, void* context) {
+ return SbUrlPlayerCreate(url, window, player_status_func,
+ encrypted_media_init_data_encountered_cb,
+ player_error_func, context);
+}
+
+void DefaultSbPlayerInterface::SetUrlPlayerDrmSystem(SbPlayer player,
+ SbDrmSystem drm_system) {
+ SbUrlPlayerSetDrmSystem(player, drm_system);
+}
+
+bool DefaultSbPlayerInterface::GetUrlPlayerOutputModeSupported(
+ SbPlayerOutputMode output_mode) {
+ return SbUrlPlayerOutputModeSupported(output_mode);
+}
+
+void DefaultSbPlayerInterface::GetUrlPlayerExtraInfo(
+ SbPlayer player, SbUrlPlayerExtraInfo* out_url_player_info) {
+ SbUrlPlayerGetExtraInfo(player, out_url_player_info);
+}
+#endif // SB_HAS(PLAYER_WITH_URL)
+
+} // namespace media
+} // namespace cobalt
diff --git a/cobalt/media/base/sbplayer_interface.h b/cobalt/media/base/sbplayer_interface.h
new file mode 100644
index 0000000..d240e0b
--- /dev/null
+++ b/cobalt/media/base/sbplayer_interface.h
@@ -0,0 +1,115 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_MEDIA_BASE_SBPLAYER_INTERFACE_H_
+#define COBALT_MEDIA_BASE_SBPLAYER_INTERFACE_H_
+
+#include "starboard/player.h"
+
+#if SB_HAS(PLAYER_WITH_URL)
+#include SB_URL_PLAYER_INCLUDE_PATH
+#endif // SB_HAS(PLAYER_WITH_URL)
+
+namespace cobalt {
+namespace media {
+
+class SbPlayerInterface {
+ public:
+ virtual ~SbPlayerInterface() {}
+
+ virtual SbPlayer Create(
+ SbWindow window, const SbPlayerCreationParam* creation_param,
+ SbPlayerDeallocateSampleFunc sample_deallocate_func,
+ SbPlayerDecoderStatusFunc decoder_status_func,
+ SbPlayerStatusFunc player_status_func,
+ SbPlayerErrorFunc player_error_func, void* context,
+ SbDecodeTargetGraphicsContextProvider* context_provider) = 0;
+ virtual SbPlayerOutputMode GetPreferredOutputMode(
+ const SbPlayerCreationParam* creation_param) = 0;
+ virtual void Destroy(SbPlayer player) = 0;
+ virtual void Seek(SbPlayer player, SbTime seek_to_timestamp, int ticket) = 0;
+ virtual void WriteSample(SbPlayer player, SbMediaType sample_type,
+ const SbPlayerSampleInfo* sample_infos,
+ int number_of_sample_infos) = 0;
+ virtual int GetMaximumNumberOfSamplesPerWrite(SbPlayer player,
+ SbMediaType sample_type) = 0;
+ virtual void WriteEndOfStream(SbPlayer player, SbMediaType stream_type) = 0;
+ virtual void SetBounds(SbPlayer player, int z_index, int x, int y, int width,
+ int height) = 0;
+ virtual bool SetPlaybackRate(SbPlayer player, double playback_rate) = 0;
+ virtual void SetVolume(SbPlayer player, double volume) = 0;
+ virtual void GetInfo(SbPlayer player, SbPlayerInfo2* out_player_info2) = 0;
+ virtual SbDecodeTarget GetCurrentFrame(SbPlayer player) = 0;
+
+#if SB_HAS(PLAYER_WITH_URL)
+ virtual SbPlayer CreateUrlPlayer(const char* url, SbWindow window,
+ SbPlayerStatusFunc player_status_func,
+ SbPlayerEncryptedMediaInitDataEncounteredCB
+ encrypted_media_init_data_encountered_cb,
+ SbPlayerErrorFunc player_error_func,
+ void* context) = 0;
+ virtual void SetUrlPlayerDrmSystem(SbPlayer player,
+ SbDrmSystem drm_system) = 0;
+ virtual bool GetUrlPlayerOutputModeSupported(
+ SbPlayerOutputMode output_mode) = 0;
+ virtual void GetUrlPlayerExtraInfo(
+ SbPlayer player, SbUrlPlayerExtraInfo* out_url_player_info) = 0;
+#endif // SB_HAS(PLAYER_WITH_URL)
+};
+
+class DefaultSbPlayerInterface final : public SbPlayerInterface {
+ public:
+ SbPlayer Create(
+ SbWindow window, const SbPlayerCreationParam* creation_param,
+ SbPlayerDeallocateSampleFunc sample_deallocate_func,
+ SbPlayerDecoderStatusFunc decoder_status_func,
+ SbPlayerStatusFunc player_status_func,
+ SbPlayerErrorFunc player_error_func, void* context,
+ SbDecodeTargetGraphicsContextProvider* context_provider) override;
+ SbPlayerOutputMode GetPreferredOutputMode(
+ const SbPlayerCreationParam* creation_param) override;
+ void Destroy(SbPlayer player) override;
+ void Seek(SbPlayer player, SbTime seek_to_timestamp, int ticket) override;
+ void WriteSample(SbPlayer player, SbMediaType sample_type,
+ const SbPlayerSampleInfo* sample_infos,
+ int number_of_sample_infos) override;
+ int GetMaximumNumberOfSamplesPerWrite(SbPlayer player,
+ SbMediaType sample_type) override;
+ void WriteEndOfStream(SbPlayer player, SbMediaType stream_type) override;
+ void SetBounds(SbPlayer player, int z_index, int x, int y, int width,
+ int height) override;
+ bool SetPlaybackRate(SbPlayer player, double playback_rate) override;
+ void SetVolume(SbPlayer player, double volume) override;
+ void GetInfo(SbPlayer player, SbPlayerInfo2* out_player_info2) override;
+ SbDecodeTarget GetCurrentFrame(SbPlayer player) override;
+
+#if SB_HAS(PLAYER_WITH_URL)
+ SbPlayer CreateUrlPlayer(const char* url, SbWindow window,
+ SbPlayerStatusFunc player_status_func,
+ SbPlayerEncryptedMediaInitDataEncounteredCB
+ encrypted_media_init_data_encountered_cb,
+ SbPlayerErrorFunc player_error_func,
+ void* context) override;
+ void SetUrlPlayerDrmSystem(SbPlayer player, SbDrmSystem drm_system) override;
+ bool GetUrlPlayerOutputModeSupported(SbPlayerOutputMode output_mode) override;
+ void GetUrlPlayerExtraInfo(
+ SbPlayer player, SbUrlPlayerExtraInfo* out_url_player_info) override;
+#endif // SB_HAS(PLAYER_WITH_URL)
+};
+
+
+} // namespace media
+} // namespace cobalt
+
+#endif // COBALT_MEDIA_BASE_SBPLAYER_INTERFACE_H_
diff --git a/cobalt/media/base/sbplayer_pipeline.cc b/cobalt/media/base/sbplayer_pipeline.cc
index be1ea63..0d69a83 100644
--- a/cobalt/media/base/sbplayer_pipeline.cc
+++ b/cobalt/media/base/sbplayer_pipeline.cc
@@ -34,8 +34,8 @@
#include "cobalt/media/base/media_export.h"
#include "cobalt/media/base/pipeline.h"
#include "cobalt/media/base/playback_statistics.h"
+#include "cobalt/media/base/sbplayer_bridge.h"
#include "cobalt/media/base/sbplayer_set_bounds_helper.h"
-#include "cobalt/media/base/starboard_player.h"
#include "starboard/common/string.h"
#include "starboard/configuration_constants.h"
#include "starboard/time.h"
@@ -94,16 +94,16 @@
// interface internally.
class MEDIA_EXPORT SbPlayerPipeline : public Pipeline,
public DemuxerHost,
- public StarboardPlayer::Host {
+ public SbPlayerBridge::Host {
public:
// Constructs a media pipeline that will execute on |task_runner|.
SbPlayerPipeline(
- PipelineWindow window,
+ SbPlayerInterface* interface, PipelineWindow window,
const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
const GetDecodeTargetGraphicsContextProviderFunc&
get_decode_target_graphics_context_provider_func,
- bool allow_resume_after_suspend, MediaLog* media_log,
- VideoFrameProvider* video_frame_provider);
+ bool allow_resume_after_suspend, bool allow_batched_sample_write,
+ MediaLog* media_log, DecodeTargetProvider* decode_target_provider);
~SbPlayerPipeline() override;
void Suspend() override;
@@ -175,18 +175,20 @@
void OnDemuxerInitialized(PipelineStatus status);
void OnDemuxerSeeked(PipelineStatus status);
void OnDemuxerStopped();
- void OnDemuxerStreamRead(DemuxerStream::Type type,
- DemuxerStream::Status status,
- scoped_refptr<DecoderBuffer> buffer);
- // StarboardPlayer::Host implementation.
- void OnNeedData(DemuxerStream::Type type) override;
+ void OnDemuxerStreamRead(
+ DemuxerStream::Type type, int max_number_buffers_to_read,
+ DemuxerStream::Status status,
+ const std::vector<scoped_refptr<DecoderBuffer>>& buffers);
+ // SbPlayerBridge::Host implementation.
+ void OnNeedData(DemuxerStream::Type type,
+ int max_number_of_buffers_to_write) override;
void OnPlayerStatus(SbPlayerState state) override;
void OnPlayerError(SbPlayerError error, const std::string& message) override;
// Used to make a delayed call to OnNeedData() if |audio_read_delayed_| is
// true. If |audio_read_delayed_| is false, that means the delayed call has
// been cancelled due to a seek.
- void DelayedNeedData();
+ void DelayedNeedData(int max_number_of_buffers_to_write);
void UpdateDecoderConfig(DemuxerStream* stream);
void CallSeekCB(PipelineStatus status, const std::string& error_message);
@@ -208,16 +210,26 @@
void RunSetDrmSystemReadyCB(DrmSystemReadyCB drm_system_ready_cb);
+ void SetReadInProgress(DemuxerStream::Type type, bool in_progress);
+ bool GetReadInProgress(DemuxerStream::Type type) const;
+
// An identifier string for the pipeline, used in CVal to identify multiple
// pipelines.
const std::string pipeline_identifier_;
+ // A wrapped interface of starboard player functions, which will be used in
+ // underlying SbPlayerBridge.
+ SbPlayerInterface* sbplayer_interface_;
+
// Message loop used to execute pipeline tasks. It is thread-safe.
scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
// Whether we should save DecoderBuffers for resume after suspend.
const bool allow_resume_after_suspend_;
+ // Whether we enable batched sample write functionality.
+ const bool allow_batched_sample_write_;
+
// The window this player associates with. It should only be assigned in the
// dtor and accessed once by SbPlayerCreate().
PipelineWindow window_;
@@ -273,7 +285,7 @@
base::Closure content_size_change_cb_;
base::Optional<bool> decode_to_texture_output_mode_;
#if SB_HAS(PLAYER_WITH_URL)
- StarboardPlayer::OnEncryptedMediaInitDataEncounteredCB
+ SbPlayerBridge::OnEncryptedMediaInitDataEncounteredCB
on_encrypted_media_init_data_encountered_cb_;
#endif // SB_HAS(PLAYER_WITH_URL)
@@ -300,7 +312,7 @@
// Temporary callback used for Start() and Seek().
SeekCB seek_cb_;
TimeDelta seek_time_;
- std::unique_ptr<StarboardPlayer> player_;
+ std::unique_ptr<SbPlayerBridge> player_bridge_;
bool is_initial_preroll_ = true;
base::CVal<bool> started_;
base::CVal<bool> suspended_;
@@ -308,7 +320,7 @@
base::CVal<bool> ended_;
base::CVal<SbPlayerState> player_state_;
- VideoFrameProvider* video_frame_provider_;
+ DecodeTargetProvider* decode_target_provider_;
// Read audio from the stream if |timestamp_of_last_written_audio_| is less
// than |seek_time_| + |kAudioPrerollLimit|, this effectively allows 10
@@ -340,16 +352,18 @@
};
SbPlayerPipeline::SbPlayerPipeline(
- PipelineWindow window,
+ SbPlayerInterface* interface, PipelineWindow window,
const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
const GetDecodeTargetGraphicsContextProviderFunc&
get_decode_target_graphics_context_provider_func,
- bool allow_resume_after_suspend, MediaLog* media_log,
- VideoFrameProvider* video_frame_provider)
+ bool allow_resume_after_suspend, bool allow_batched_sample_write,
+ MediaLog* media_log, DecodeTargetProvider* decode_target_provider)
: pipeline_identifier_(
base::StringPrintf("%X", g_pipeline_identifier_counter++)),
+ sbplayer_interface_(interface),
task_runner_(task_runner),
allow_resume_after_suspend_(allow_resume_after_suspend),
+ allow_batched_sample_write_(allow_batched_sample_write),
window_(window),
get_decode_target_graphics_context_provider_func_(
get_decode_target_graphics_context_provider_func),
@@ -383,7 +397,7 @@
pipeline_identifier_.c_str()),
kSbPlayerStateInitialized,
"The underlying SbPlayer state of the media pipeline."),
- video_frame_provider_(video_frame_provider),
+ decode_target_provider_(decode_target_provider),
last_media_time_(base::StringPrintf("Media.Pipeline.%s.LastMediaTime",
pipeline_identifier_.c_str()),
0, "Last media time reported by the underlying player."),
@@ -395,7 +409,7 @@
SbMediaSetAudioWriteDuration(kAudioLimit);
}
-SbPlayerPipeline::~SbPlayerPipeline() { DCHECK(!player_); }
+SbPlayerPipeline::~SbPlayerPipeline() { DCHECK(!player_bridge_); }
void SbPlayerPipeline::Suspend() {
DCHECK(!task_runner_->BelongsToCurrentThread());
@@ -540,15 +554,15 @@
stopped_ = true;
- if (player_) {
- std::unique_ptr<StarboardPlayer> player;
+ if (player_bridge_) {
+ std::unique_ptr<SbPlayerBridge> player_bridge;
{
base::AutoLock auto_lock(lock_);
- player = std::move(player_);
+ player_bridge = std::move(player_bridge_);
}
LOG(INFO) << "Destroying SbPlayer.";
- player.reset();
+ player_bridge.reset();
LOG(INFO) << "SbPlayer destroyed.";
}
@@ -572,14 +586,14 @@
playback_statistics_.OnSeek(time);
- if (!player_) {
+ if (!player_bridge_) {
seek_cb.Run(::media::PIPELINE_ERROR_INVALID_STATE, false,
AppendStatisticsString("SbPlayerPipeline::Seek failed: "
- "player_ is nullptr."));
+ "player_bridge_ is nullptr."));
return;
}
- player_->PrepareForSeek();
+ player_bridge_->PrepareForSeek();
ended_ = false;
DCHECK(seek_cb_.is_null());
@@ -607,7 +621,7 @@
#if SB_HAS(PLAYER_WITH_URL)
if (is_url_based_) {
- player_->Seek(seek_time_);
+ player_bridge_->Seek(seek_time_);
return;
}
#endif // SB_HAS(PLAYER_WITH_URL)
@@ -664,7 +678,7 @@
StoreMediaTime(seek_time_);
return seek_time_;
}
- if (!player_) {
+ if (!player_bridge_) {
StoreMediaTime(TimeDelta());
return TimeDelta();
}
@@ -677,7 +691,7 @@
if (is_url_based_) {
int frame_width;
int frame_height;
- player_->GetVideoResolution(&frame_width, &frame_height);
+ player_bridge_->GetVideoResolution(&frame_width, &frame_height);
if (frame_width != natural_size_.width() ||
frame_height != natural_size_.height()) {
natural_size_ = gfx::Size(frame_width, frame_height);
@@ -685,8 +699,8 @@
}
}
#endif // SB_HAS(PLAYER_WITH_URL)
- player_->GetInfo(&statistics_.video_frames_decoded,
- &statistics_.video_frames_dropped, &media_time);
+ player_bridge_->GetInfo(&statistics_.video_frames_decoded,
+ &statistics_.video_frames_dropped, &media_time);
// Guarantee that we report monotonically increasing media time
if (media_time.ToSbTime() < last_media_time_) {
@@ -705,7 +719,7 @@
#if SB_HAS(PLAYER_WITH_URL)
::media::Ranges<TimeDelta> time_ranges;
- if (!player_) {
+ if (!player_bridge_) {
return time_ranges;
}
@@ -713,10 +727,10 @@
base::TimeDelta media_time;
base::TimeDelta buffer_start_time;
base::TimeDelta buffer_length_time;
- player_->GetInfo(&statistics_.video_frames_decoded,
- &statistics_.video_frames_dropped, &media_time);
- player_->GetUrlPlayerBufferedTimeRanges(&buffer_start_time,
- &buffer_length_time);
+ player_bridge_->GetInfo(&statistics_.video_frames_decoded,
+ &statistics_.video_frames_dropped, &media_time);
+ player_bridge_->GetUrlPlayerBufferedTimeRanges(&buffer_start_time,
+ &buffer_length_time);
if (buffer_length_time.InSeconds() == 0) {
buffered_time_ranges_ = time_ranges;
@@ -796,7 +810,7 @@
// The player can't be created yet, if it is, then we're updating the output
// mode too late.
- DCHECK(!player_);
+ DCHECK(!player_bridge_);
decode_to_texture_output_mode_ = enabled;
}
@@ -838,16 +852,16 @@
void SbPlayerPipeline::SetVolumeTask(float volume) {
DCHECK(task_runner_->BelongsToCurrentThread());
- if (player_) {
- player_->SetVolume(volume_);
+ if (player_bridge_) {
+ player_bridge_->SetVolume(volume_);
}
}
void SbPlayerPipeline::SetPlaybackRateTask(float volume) {
DCHECK(task_runner_->BelongsToCurrentThread());
- if (player_) {
- player_->SetPlaybackRate(playback_rate_);
+ if (player_bridge_) {
+ player_bridge_->SetPlaybackRate(playback_rate_);
}
}
@@ -908,22 +922,23 @@
std::string error_message;
{
base::AutoLock auto_lock(lock_);
- LOG(INFO) << "Creating StarboardPlayer with url: " << source_url;
- player_.reset(new StarboardPlayer(
- task_runner_, source_url, window_, this, set_bounds_helper_.get(),
- allow_resume_after_suspend_, *decode_to_texture_output_mode_,
- on_encrypted_media_init_data_encountered_cb_, video_frame_provider_));
- if (player_->IsValid()) {
+ LOG(INFO) << "Creating SbPlayerBridge with url: " << source_url;
+ player_bridge_.reset(new SbPlayerBridge(
+ sbplayer_interface_, task_runner_, source_url, window_, this,
+ set_bounds_helper_.get(), allow_resume_after_suspend_,
+ *decode_to_texture_output_mode_,
+ on_encrypted_media_init_data_encountered_cb_, decode_target_provider_));
+ if (player_bridge_->IsValid()) {
SetPlaybackRateTask(playback_rate_);
SetVolumeTask(volume_);
} else {
- error_message = player_->GetPlayerCreationErrorMessage();
- player_.reset();
- LOG(INFO) << "Failed to create a valid StarboardPlayer.";
+ error_message = player_bridge_->GetPlayerCreationErrorMessage();
+ player_bridge_.reset();
+ LOG(INFO) << "Failed to create a valid SbPlayerBridge.";
}
}
- if (player_ && player_->IsValid()) {
+ if (player_bridge_ && player_bridge_->IsValid()) {
base::Closure output_mode_change_cb;
{
base::AutoLock auto_lock(lock_);
@@ -936,12 +951,12 @@
std::string time_information = GetTimeInformation();
LOG(INFO) << "SbPlayerPipeline::CreateUrlPlayer failed to create a valid "
- "StarboardPlayer - "
+ "SbPlayerBridge - "
<< time_information << " \'" << error_message << "\'";
CallSeekCB(::media::DECODER_ERROR_NOT_SUPPORTED,
"SbPlayerPipeline::CreateUrlPlayer failed to create a valid "
- "StarboardPlayer - " +
+ "SbPlayerBridge - " +
time_information + " \'" + error_message + "\'");
}
@@ -949,14 +964,14 @@
TRACE_EVENT0("cobalt::media", "SbPlayerPipeline::SetDrmSystem");
base::AutoLock auto_lock(lock_);
- if (!player_) {
+ if (!player_bridge_) {
LOG(INFO) << "Player not set before calling SbPlayerPipeline::SetDrmSystem";
return;
}
- if (player_->IsValid()) {
- player_->RecordSetDrmSystemReadyTime(set_drm_system_ready_cb_time_);
- player_->SetDrmSystem(drm_system);
+ if (player_bridge_->IsValid()) {
+ player_bridge_->RecordSetDrmSystemReadyTime(set_drm_system_ready_cb_time_);
+ player_bridge_->SetDrmSystem(drm_system);
}
}
#endif // SB_HAS(PLAYER_WITH_URL)
@@ -1005,30 +1020,31 @@
std::string error_message;
{
base::AutoLock auto_lock(lock_);
- SB_DCHECK(!player_);
- // In the extreme case that CreatePlayer() is called when a |player_| is
- // available, reset the existing player first to reduce the number of active
- // players.
- player_.reset();
- LOG(INFO) << "Creating StarboardPlayer.";
- player_.reset(new StarboardPlayer(
- task_runner_, get_decode_target_graphics_context_provider_func_,
- audio_config, audio_mime_type, video_config, video_mime_type, window_,
- drm_system, this, set_bounds_helper_.get(), allow_resume_after_suspend_,
- *decode_to_texture_output_mode_, video_frame_provider_,
+ SB_DCHECK(!player_bridge_);
+ // In the extreme case that CreatePlayer() is called when a |player_bridge_|
+ // is available, reset the existing player first to reduce the number of
+ // active players.
+ player_bridge_.reset();
+ LOG(INFO) << "Creating SbPlayerBridge.";
+ player_bridge_.reset(new SbPlayerBridge(
+ sbplayer_interface_, task_runner_,
+ get_decode_target_graphics_context_provider_func_, audio_config,
+ audio_mime_type, video_config, video_mime_type, window_, drm_system,
+ this, set_bounds_helper_.get(), allow_resume_after_suspend_,
+ *decode_to_texture_output_mode_, decode_target_provider_,
max_video_capabilities_));
- if (player_->IsValid()) {
+ if (player_bridge_->IsValid()) {
SetPlaybackRateTask(playback_rate_);
SetVolumeTask(volume_);
} else {
- error_message = player_->GetPlayerCreationErrorMessage();
- player_.reset();
- LOG(INFO) << "Failed to create a valid StarboardPlayer.";
+ error_message = player_bridge_->GetPlayerCreationErrorMessage();
+ player_bridge_.reset();
+ LOG(INFO) << "Failed to create a valid SbPlayerBridge.";
}
}
- if (player_ && player_->IsValid()) {
- player_->RecordSetDrmSystemReadyTime(set_drm_system_ready_cb_time_);
+ if (player_bridge_ && player_bridge_->IsValid()) {
+ player_bridge_->RecordSetDrmSystemReadyTime(set_drm_system_ready_cb_time_);
base::Closure output_mode_change_cb;
{
base::AutoLock auto_lock(lock_);
@@ -1048,12 +1064,12 @@
std::string time_information = GetTimeInformation();
LOG(INFO) << "SbPlayerPipeline::CreatePlayer failed to create a valid "
- "StarboardPlayer - "
+ "SbPlayerBridge - "
<< time_information << " \'" << error_message << "\'";
CallSeekCB(::media::DECODER_ERROR_NOT_SUPPORTED,
"SbPlayerPipeline::CreatePlayer failed to create a valid "
- "StarboardPlayer - " +
+ "SbPlayerBridge - " +
time_information + " \'" + error_message + "\'");
}
@@ -1142,8 +1158,8 @@
void SbPlayerPipeline::OnDemuxerSeeked(PipelineStatus status) {
DCHECK(task_runner_->BelongsToCurrentThread());
- if (status == ::media::PIPELINE_OK && player_) {
- player_->Seek(seek_time_);
+ if (status == ::media::PIPELINE_OK && player_bridge_) {
+ player_bridge_->Seek(seek_time_);
}
}
@@ -1160,8 +1176,9 @@
}
void SbPlayerPipeline::OnDemuxerStreamRead(
- DemuxerStream::Type type, DemuxerStream::Status status,
- scoped_refptr<DecoderBuffer> buffer) {
+ DemuxerStream::Type type, int max_number_buffers_to_read,
+ DemuxerStream::Status status,
+ const std::vector<scoped_refptr<DecoderBuffer>>& buffers) {
#if SB_HAS(PLAYER_WITH_URL)
DCHECK(!is_url_based_);
#endif // SB_HAS(PLAYER_WITH_URL)
@@ -1170,8 +1187,9 @@
if (!task_runner_->BelongsToCurrentThread()) {
task_runner_->PostTask(
- FROM_HERE, base::BindOnce(&SbPlayerPipeline::OnDemuxerStreamRead, this,
- type, status, buffer));
+ FROM_HERE,
+ base::BindOnce(&SbPlayerPipeline::OnDemuxerStreamRead, this, type,
+ max_number_buffers_to_read, status, buffers));
return;
}
@@ -1180,18 +1198,14 @@
DCHECK(stream);
// In case if Stop() has been called.
- if (!player_) {
+ if (!player_bridge_) {
return;
}
if (status == DemuxerStream::kAborted) {
- if (type == DemuxerStream::AUDIO) {
- DCHECK(audio_read_in_progress_);
- audio_read_in_progress_ = false;
- } else {
- DCHECK(video_read_in_progress_);
- video_read_in_progress_ = false;
- }
+ DCHECK(GetReadInProgress(type));
+ SetReadInProgress(type, false);
+
if (!seek_cb_.is_null()) {
CallSeekCB(::media::PIPELINE_OK, "");
}
@@ -1200,45 +1214,53 @@
if (status == DemuxerStream::kConfigChanged) {
UpdateDecoderConfig(stream);
- stream->Read(
- base::BindOnce(&SbPlayerPipeline::OnDemuxerStreamRead, this, type));
+ stream->Read(max_number_buffers_to_read,
+ base::BindOnce(&SbPlayerPipeline::OnDemuxerStreamRead, this,
+ type, max_number_buffers_to_read));
return;
}
if (type == DemuxerStream::AUDIO) {
- audio_read_in_progress_ = false;
- playback_statistics_.OnAudioAU(buffer);
- if (!buffer->end_of_stream()) {
- timestamp_of_last_written_audio_ = buffer->timestamp().ToSbTime();
+ for (const auto& buffer : buffers) {
+ playback_statistics_.OnAudioAU(buffer);
+ if (!buffer->end_of_stream()) {
+ timestamp_of_last_written_audio_ = buffer->timestamp().ToSbTime();
+ }
}
} else {
- video_read_in_progress_ = false;
- playback_statistics_.OnVideoAU(buffer);
+ for (const auto& buffer : buffers) {
+ playback_statistics_.OnVideoAU(buffer);
+ }
}
+ SetReadInProgress(type, false);
- player_->WriteBuffer(type, buffer);
+ player_bridge_->WriteBuffers(type, buffers);
}
-void SbPlayerPipeline::OnNeedData(DemuxerStream::Type type) {
+void SbPlayerPipeline::OnNeedData(DemuxerStream::Type type,
+ int max_number_of_buffers_to_write) {
#if SB_HAS(PLAYER_WITH_URL)
DCHECK(!is_url_based_);
#endif // SB_HAS(PLAYER_WITH_URL)
DCHECK(task_runner_->BelongsToCurrentThread());
// In case if Stop() has been called.
- if (!player_) {
+ if (!player_bridge_) {
return;
}
+ int max_buffers =
+ allow_batched_sample_write_ ? max_number_of_buffers_to_write : 1;
+
+ if (GetReadInProgress(type)) return;
+
if (type == DemuxerStream::AUDIO) {
if (!audio_stream_) {
LOG(WARNING)
<< "Calling OnNeedData() for audio data during audioless playback";
return;
}
- if (audio_read_in_progress_) {
- return;
- }
+
// If we haven't checked the media time recently, update it now.
if (SbTimeGetNow() - last_time_media_time_retrieved_ >
kMediaTimeCheckInterval) {
@@ -1257,7 +1279,8 @@
timestamp_of_last_written_audio_ - last_media_time_;
if (time_ahead_of_playback > (kAudioLimit + kMediaTimeCheckInterval)) {
task_runner_->PostDelayedTask(
- FROM_HERE, base::Bind(&SbPlayerPipeline::DelayedNeedData, this),
+ FROM_HERE,
+ base::Bind(&SbPlayerPipeline::DelayedNeedData, this, max_buffers),
base::TimeDelta::FromMicroseconds(kMediaTimeCheckInterval));
audio_read_delayed_ = true;
return;
@@ -1268,23 +1291,22 @@
audio_read_in_progress_ = true;
} else {
DCHECK_EQ(type, DemuxerStream::VIDEO);
- if (video_read_in_progress_) {
- return;
- }
video_read_in_progress_ = true;
}
DemuxerStream* stream =
type == DemuxerStream::AUDIO ? audio_stream_ : video_stream_;
DCHECK(stream);
- stream->Read(
- base::BindOnce(&SbPlayerPipeline::OnDemuxerStreamRead, this, type));
+
+ stream->Read(max_buffers,
+ base::BindOnce(&SbPlayerPipeline::OnDemuxerStreamRead, this,
+ type, max_buffers));
}
void SbPlayerPipeline::OnPlayerStatus(SbPlayerState state) {
DCHECK(task_runner_->BelongsToCurrentThread());
// In case if Stop() has been called.
- if (!player_) {
+ if (!player_bridge_) {
return;
}
player_state_ = state;
@@ -1303,12 +1325,12 @@
case kSbPlayerStatePresenting: {
#if SB_HAS(PLAYER_WITH_URL)
if (is_url_based_) {
- duration_ = player_->GetDuration();
- start_date_ = player_->GetStartDate();
+ duration_ = player_bridge_->GetDuration();
+ start_date_ = player_bridge_->GetStartDate();
buffering_state_cb_.Run(kHaveMetadata);
int frame_width;
int frame_height;
- player_->GetVideoResolution(&frame_width, &frame_height);
+ player_bridge_->GetVideoResolution(&frame_width, &frame_height);
bool natural_size_changed = (frame_width != natural_size_.width() ||
frame_height != natural_size_.height());
natural_size_ = gfx::Size(frame_width, frame_height);
@@ -1341,7 +1363,7 @@
DCHECK(task_runner_->BelongsToCurrentThread());
// In case if Stop() has been called.
- if (!player_) {
+ if (!player_bridge_) {
return;
}
@@ -1373,10 +1395,10 @@
}
}
-void SbPlayerPipeline::DelayedNeedData() {
+void SbPlayerPipeline::DelayedNeedData(int max_number_of_buffers_to_write) {
DCHECK(task_runner_->BelongsToCurrentThread());
if (audio_read_delayed_) {
- OnNeedData(DemuxerStream::AUDIO);
+ OnNeedData(DemuxerStream::AUDIO, max_number_of_buffers_to_write);
}
}
@@ -1385,7 +1407,7 @@
if (stream->type() == DemuxerStream::AUDIO) {
const AudioDecoderConfig& decoder_config = stream->audio_decoder_config();
- player_->UpdateAudioConfig(decoder_config, stream->mime_type());
+ player_bridge_->UpdateAudioConfig(decoder_config, stream->mime_type());
} else {
DCHECK_EQ(stream->type(), DemuxerStream::VIDEO);
const VideoDecoderConfig& decoder_config = stream->video_decoder_config();
@@ -1394,7 +1416,7 @@
(decoder_config.natural_size().width() != natural_size_.width() ||
decoder_config.natural_size().height() != natural_size_.height());
natural_size_ = decoder_config.natural_size();
- player_->UpdateVideoConfig(decoder_config, stream->mime_type());
+ player_bridge_->UpdateVideoConfig(decoder_config, stream->mime_type());
if (natural_size_changed) {
content_size_change_cb_.Run();
}
@@ -1444,11 +1466,11 @@
return;
}
- if (player_) {
- // Cancel pending delayed calls to OnNeedData. After player_->Resume(),
- // |player_| will call OnNeedData again.
+ if (player_bridge_) {
+ // Cancel pending delayed calls to OnNeedData. After
+ // player_bridge_->Resume(), |player_bridge_| will call OnNeedData again.
audio_read_delayed_ = false;
- player_->Suspend();
+ player_bridge_->Suspend();
}
suspended_ = true;
@@ -1470,22 +1492,22 @@
window_ = window;
- if (player_) {
- player_->Resume(window);
- if (!player_->IsValid()) {
+ if (player_bridge_) {
+ player_bridge_->Resume(window);
+ if (!player_bridge_->IsValid()) {
std::string error_message;
{
base::AutoLock auto_lock(lock_);
- error_message = player_->GetPlayerCreationErrorMessage();
- player_.reset();
+ error_message = player_bridge_->GetPlayerCreationErrorMessage();
+ player_bridge_.reset();
}
std::string time_information = GetTimeInformation();
LOG(INFO) << "SbPlayerPipeline::ResumeTask failed to create a valid "
- "StarboardPlayer - "
+ "SbPlayerBridge - "
<< time_information << " \'" << error_message << "\'";
CallErrorCB(::media::DECODER_ERROR_NOT_SUPPORTED,
"SbPlayerPipeline::ResumeTask failed to create a valid "
- "StarboardPlayer - " +
+ "SbPlayerBridge - " +
time_information + " \'" + error_message + "\'");
done_event->Signal();
return;
@@ -1545,19 +1567,34 @@
set_drm_system_ready_cb_.Run(drm_system_ready_cb);
}
+void SbPlayerPipeline::SetReadInProgress(DemuxerStream::Type type,
+ bool in_progress) {
+ if (type == DemuxerStream::AUDIO)
+ audio_read_in_progress_ = in_progress;
+ else
+ video_read_in_progress_ = in_progress;
+}
+
+bool SbPlayerPipeline::GetReadInProgress(DemuxerStream::Type type) const {
+ if (type == DemuxerStream::AUDIO) return audio_read_in_progress_;
+ return video_read_in_progress_;
+}
+
} // namespace
// static
scoped_refptr<Pipeline> Pipeline::Create(
- PipelineWindow window,
+ SbPlayerInterface* interface, PipelineWindow window,
const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
const GetDecodeTargetGraphicsContextProviderFunc&
get_decode_target_graphics_context_provider_func,
- bool allow_resume_after_suspend, MediaLog* media_log,
- VideoFrameProvider* video_frame_provider) {
- return new SbPlayerPipeline(
- window, task_runner, get_decode_target_graphics_context_provider_func,
- allow_resume_after_suspend, media_log, video_frame_provider);
+ bool allow_resume_after_suspend, bool allow_batched_sample_write,
+ MediaLog* media_log, DecodeTargetProvider* decode_target_provider) {
+ return new SbPlayerPipeline(interface, window, task_runner,
+ get_decode_target_graphics_context_provider_func,
+ allow_resume_after_suspend,
+ allow_batched_sample_write, media_log,
+ decode_target_provider);
}
} // namespace media
diff --git a/cobalt/media/base/sbplayer_set_bounds_helper.cc b/cobalt/media/base/sbplayer_set_bounds_helper.cc
index d29e042..9b02952 100644
--- a/cobalt/media/base/sbplayer_set_bounds_helper.cc
+++ b/cobalt/media/base/sbplayer_set_bounds_helper.cc
@@ -15,7 +15,7 @@
#include "cobalt/media/base/sbplayer_set_bounds_helper.h"
#include "base/atomic_sequence_num.h"
-#include "cobalt/media/base/starboard_player.h"
+#include "cobalt/media/base/sbplayer_bridge.h"
namespace cobalt {
namespace media {
@@ -28,22 +28,22 @@
base::AtomicSequenceNumber s_z_index;
} // namespace
-void SbPlayerSetBoundsHelper::SetPlayer(StarboardPlayer* player) {
+void SbPlayerSetBoundsHelper::SetPlayerBridge(SbPlayerBridge* player_bridge) {
base::AutoLock auto_lock(lock_);
- player_ = player;
- if (player_ && rect_.has_value()) {
- player_->SetBounds(s_z_index.GetNext(), rect_.value());
+ player_bridge_ = player_bridge;
+ if (player_bridge_ && rect_.has_value()) {
+ player_bridge_->SetBounds(s_z_index.GetNext(), rect_.value());
}
}
bool SbPlayerSetBoundsHelper::SetBounds(int x, int y, int width, int height) {
base::AutoLock auto_lock(lock_);
rect_ = gfx::Rect(x, y, width, height);
- if (player_) {
- player_->SetBounds(s_z_index.GetNext(), rect_.value());
+ if (player_bridge_) {
+ player_bridge_->SetBounds(s_z_index.GetNext(), rect_.value());
}
- return player_ != nullptr;
+ return player_bridge_ != nullptr;
}
} // namespace media
diff --git a/cobalt/media/base/sbplayer_set_bounds_helper.h b/cobalt/media/base/sbplayer_set_bounds_helper.h
index 4c09e3c..c5f4b95 100644
--- a/cobalt/media/base/sbplayer_set_bounds_helper.h
+++ b/cobalt/media/base/sbplayer_set_bounds_helper.h
@@ -23,19 +23,19 @@
namespace cobalt {
namespace media {
-class StarboardPlayer;
+class SbPlayerBridge;
class SbPlayerSetBoundsHelper
: public base::RefCountedThreadSafe<SbPlayerSetBoundsHelper> {
public:
SbPlayerSetBoundsHelper() {}
- void SetPlayer(StarboardPlayer* player);
+ void SetPlayerBridge(SbPlayerBridge* player_bridge);
bool SetBounds(int x, int y, int width, int height);
private:
base::Lock lock_;
- StarboardPlayer* player_ = nullptr;
+ SbPlayerBridge* player_bridge_ = nullptr;
base::Optional<gfx::Rect> rect_;
DISALLOW_COPY_AND_ASSIGN(SbPlayerSetBoundsHelper);
diff --git a/cobalt/media/base/starboard_player.cc b/cobalt/media/base/starboard_player.cc
deleted file mode 100644
index 8c97ffb..0000000
--- a/cobalt/media/base/starboard_player.cc
+++ /dev/null
@@ -1,1032 +0,0 @@
-// Copyright 2016 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "cobalt/media/base/starboard_player.h"
-
-#include <algorithm>
-#include <iomanip>
-#include <string>
-#include <vector>
-
-#include "base/bind.h"
-#include "base/compiler_specific.h"
-#include "base/location.h"
-#include "base/logging.h"
-#include "base/trace_event/trace_event.h"
-#include "cobalt/base/statistics.h"
-#include "cobalt/media/base/format_support_query_metrics.h"
-#include "starboard/common/media.h"
-#include "starboard/common/string.h"
-#include "starboard/configuration.h"
-#include "starboard/memory.h"
-#include "third_party/chromium/media/base/starboard_utils.h"
-
-namespace cobalt {
-namespace media {
-
-namespace {
-
-base::Statistics<SbTime, int, 1024> s_startup_latency;
-
-} // namespace
-
-StarboardPlayer::CallbackHelper::CallbackHelper(StarboardPlayer* player)
- : player_(player) {}
-
-void StarboardPlayer::CallbackHelper::ClearDecoderBufferCache() {
- base::AutoLock auto_lock(lock_);
- if (player_) {
- player_->ClearDecoderBufferCache();
- }
-}
-
-void StarboardPlayer::CallbackHelper::OnDecoderStatus(
- SbPlayer player, SbMediaType type, SbPlayerDecoderState state, int ticket) {
- base::AutoLock auto_lock(lock_);
- if (player_) {
- player_->OnDecoderStatus(player, type, state, ticket);
- }
-}
-
-void StarboardPlayer::CallbackHelper::OnPlayerStatus(SbPlayer player,
- SbPlayerState state,
- int ticket) {
- base::AutoLock auto_lock(lock_);
- if (player_) {
- player_->OnPlayerStatus(player, state, ticket);
- }
-}
-
-void StarboardPlayer::CallbackHelper::OnPlayerError(
- SbPlayer player, SbPlayerError error, const std::string& message) {
- base::AutoLock auto_lock(lock_);
- if (player_) {
- player_->OnPlayerError(player, error, message);
- }
-}
-
-void StarboardPlayer::CallbackHelper::OnDeallocateSample(
- const void* sample_buffer) {
- base::AutoLock auto_lock(lock_);
- if (player_) {
- player_->OnDeallocateSample(sample_buffer);
- }
-}
-
-void StarboardPlayer::CallbackHelper::ResetPlayer() {
- base::AutoLock auto_lock(lock_);
- player_ = NULL;
-}
-
-#if SB_HAS(PLAYER_WITH_URL)
-StarboardPlayer::StarboardPlayer(
- const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
- const std::string& url, SbWindow window, Host* host,
- SbPlayerSetBoundsHelper* set_bounds_helper, bool allow_resume_after_suspend,
- bool prefer_decode_to_texture,
- const OnEncryptedMediaInitDataEncounteredCB&
- on_encrypted_media_init_data_encountered_cb,
- VideoFrameProvider* const video_frame_provider)
- : url_(url),
- task_runner_(task_runner),
- callback_helper_(
- new CallbackHelper(ALLOW_THIS_IN_INITIALIZER_LIST(this))),
- window_(window),
- host_(host),
- set_bounds_helper_(set_bounds_helper),
- allow_resume_after_suspend_(allow_resume_after_suspend),
- on_encrypted_media_init_data_encountered_cb_(
- on_encrypted_media_init_data_encountered_cb),
- video_frame_provider_(video_frame_provider),
- is_url_based_(true) {
- DCHECK(host_);
- DCHECK(set_bounds_helper_);
-
- output_mode_ = ComputeSbUrlPlayerOutputMode(prefer_decode_to_texture);
-
- CreateUrlPlayer(url_);
-
- task_runner->PostTask(
- FROM_HERE,
- base::Bind(&StarboardPlayer::CallbackHelper::ClearDecoderBufferCache,
- callback_helper_));
-}
-#endif // SB_HAS(PLAYER_WITH_URL)
-
-StarboardPlayer::StarboardPlayer(
- const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
- const GetDecodeTargetGraphicsContextProviderFunc&
- get_decode_target_graphics_context_provider_func,
- const AudioDecoderConfig& audio_config, const std::string& audio_mime_type,
- const VideoDecoderConfig& video_config, const std::string& video_mime_type,
- SbWindow window, SbDrmSystem drm_system, Host* host,
- SbPlayerSetBoundsHelper* set_bounds_helper, bool allow_resume_after_suspend,
- bool prefer_decode_to_texture,
- VideoFrameProvider* const video_frame_provider,
- const std::string& max_video_capabilities)
- : task_runner_(task_runner),
- get_decode_target_graphics_context_provider_func_(
- get_decode_target_graphics_context_provider_func),
- callback_helper_(
- new CallbackHelper(ALLOW_THIS_IN_INITIALIZER_LIST(this))),
- window_(window),
- drm_system_(drm_system),
- host_(host),
- set_bounds_helper_(set_bounds_helper),
- allow_resume_after_suspend_(allow_resume_after_suspend),
- audio_config_(audio_config),
- video_config_(video_config),
- video_frame_provider_(video_frame_provider),
- max_video_capabilities_(max_video_capabilities)
-#if SB_HAS(PLAYER_WITH_URL)
- ,
- is_url_based_(false)
-#endif // SB_HAS(PLAYER_WITH_URL
-{
- DCHECK(!get_decode_target_graphics_context_provider_func_.is_null());
- DCHECK(audio_config.IsValidConfig() || video_config.IsValidConfig());
- DCHECK(host_);
- DCHECK(set_bounds_helper_);
- DCHECK(video_frame_provider_);
-
- audio_sample_info_.codec = kSbMediaAudioCodecNone;
- video_sample_info_.codec = kSbMediaVideoCodecNone;
-
- if (audio_config.IsValidConfig()) {
- UpdateAudioConfig(audio_config, audio_mime_type);
- }
- if (video_config.IsValidConfig()) {
- UpdateVideoConfig(video_config, video_mime_type);
- }
-
- output_mode_ = ComputeSbPlayerOutputMode(prefer_decode_to_texture);
-
- CreatePlayer();
-
- if (SbPlayerIsValid(player_)) {
- task_runner->PostTask(
- FROM_HERE,
- base::Bind(&StarboardPlayer::CallbackHelper::ClearDecoderBufferCache,
- callback_helper_));
- }
-}
-
-StarboardPlayer::~StarboardPlayer() {
- DCHECK(task_runner_->BelongsToCurrentThread());
-
- callback_helper_->ResetPlayer();
- set_bounds_helper_->SetPlayer(NULL);
-
- video_frame_provider_->SetOutputMode(VideoFrameProvider::kOutputModeInvalid);
- video_frame_provider_->ResetGetCurrentSbDecodeTargetFunction();
-
- if (SbPlayerIsValid(player_)) {
- SbPlayerDestroy(player_);
- }
-}
-
-void StarboardPlayer::UpdateAudioConfig(const AudioDecoderConfig& audio_config,
- const std::string& mime_type) {
- DCHECK(task_runner_->BelongsToCurrentThread());
- DCHECK(audio_config.IsValidConfig());
-
- LOG(INFO) << "Updated AudioDecoderConfig -- "
- << audio_config.AsHumanReadableString();
-
- audio_config_ = audio_config;
- audio_mime_type_ = mime_type;
- audio_sample_info_ = MediaAudioConfigToSbMediaAudioSampleInfo(
- audio_config_, audio_mime_type_.c_str());
- LOG(INFO) << "Converted to SbMediaAudioSampleInfo -- " << audio_sample_info_;
-}
-
-void StarboardPlayer::UpdateVideoConfig(const VideoDecoderConfig& video_config,
- const std::string& mime_type) {
- DCHECK(task_runner_->BelongsToCurrentThread());
- DCHECK(video_config.IsValidConfig());
-
- LOG(INFO) << "Updated VideoDecoderConfig -- "
- << video_config.AsHumanReadableString();
-
- video_config_ = video_config;
- video_sample_info_.frame_width =
- static_cast<int>(video_config_.natural_size().width());
- video_sample_info_.frame_height =
- static_cast<int>(video_config_.natural_size().height());
- video_sample_info_.codec =
- MediaVideoCodecToSbMediaVideoCodec(video_config_.codec());
- video_sample_info_.color_metadata =
- MediaToSbMediaColorMetadata(video_config_.color_space_info(),
- video_config_.hdr_metadata(), mime_type);
- video_mime_type_ = mime_type;
- video_sample_info_.mime = video_mime_type_.c_str();
- video_sample_info_.max_video_capabilities = max_video_capabilities_.c_str();
- LOG(INFO) << "Converted to SbMediaVideoSampleInfo -- " << video_sample_info_;
-}
-
-void StarboardPlayer::WriteBuffer(DemuxerStream::Type type,
- const scoped_refptr<DecoderBuffer>& buffer) {
- DCHECK(task_runner_->BelongsToCurrentThread());
- DCHECK(buffer);
-#if SB_HAS(PLAYER_WITH_URL)
- DCHECK(!is_url_based_);
-#endif // SB_HAS(PLAYER_WITH_URL)
-
- if (allow_resume_after_suspend_) {
- decoder_buffer_cache_.AddBuffer(type, buffer);
-
- if (state_ != kSuspended) {
- WriteNextBufferFromCache(type);
- }
-
- return;
- }
- WriteBufferInternal(type, buffer);
-}
-
-void StarboardPlayer::SetBounds(int z_index, const gfx::Rect& rect) {
- base::AutoLock auto_lock(lock_);
-
- set_bounds_z_index_ = z_index;
- set_bounds_rect_ = rect;
-
- if (state_ == kSuspended) {
- return;
- }
-
- UpdateBounds_Locked();
-}
-
-void StarboardPlayer::PrepareForSeek() {
- DCHECK(task_runner_->BelongsToCurrentThread());
-
- seek_pending_ = true;
-
- if (state_ == kSuspended) {
- return;
- }
-
- ++ticket_;
- SbPlayerSetPlaybackRate(player_, 0.f);
-}
-
-void StarboardPlayer::Seek(base::TimeDelta time) {
- DCHECK(task_runner_->BelongsToCurrentThread());
-
- decoder_buffer_cache_.ClearAll();
- seek_pending_ = false;
-
- if (state_ == kSuspended) {
- preroll_timestamp_ = time;
- return;
- }
-
- // If a seek happens during resuming, the pipeline will write samples from the
- // seek target time again so resuming can be aborted.
- if (state_ == kResuming) {
- state_ = kPlaying;
- }
-
- DCHECK(SbPlayerIsValid(player_));
-
- ++ticket_;
- SbPlayerSeek2(player_, time.InMicroseconds(), ticket_);
-
- SbPlayerSetPlaybackRate(player_, playback_rate_);
-}
-
-void StarboardPlayer::SetVolume(float volume) {
- DCHECK(task_runner_->BelongsToCurrentThread());
-
- volume_ = volume;
-
- if (state_ == kSuspended) {
- return;
- }
-
- DCHECK(SbPlayerIsValid(player_));
- SbPlayerSetVolume(player_, volume);
-}
-
-void StarboardPlayer::SetPlaybackRate(double playback_rate) {
- DCHECK(task_runner_->BelongsToCurrentThread());
-
- playback_rate_ = playback_rate;
-
- if (state_ == kSuspended) {
- return;
- }
-
- if (seek_pending_) {
- return;
- }
-
- SbPlayerSetPlaybackRate(player_, playback_rate);
-}
-
-void StarboardPlayer::GetInfo(uint32* video_frames_decoded,
- uint32* video_frames_dropped,
- base::TimeDelta* media_time) {
- DCHECK(video_frames_decoded || video_frames_dropped || media_time);
-
- base::AutoLock auto_lock(lock_);
- GetInfo_Locked(video_frames_decoded, video_frames_dropped, media_time);
-}
-
-#if SB_HAS(PLAYER_WITH_URL)
-void StarboardPlayer::GetUrlPlayerBufferedTimeRanges(
- base::TimeDelta* buffer_start_time, base::TimeDelta* buffer_length_time) {
- DCHECK(buffer_start_time || buffer_length_time);
- DCHECK(is_url_based_);
-
- if (state_ == kSuspended) {
- *buffer_start_time = base::TimeDelta();
- *buffer_length_time = base::TimeDelta();
- return;
- }
-
- DCHECK(SbPlayerIsValid(player_));
-
- SbUrlPlayerExtraInfo url_player_info;
- SbUrlPlayerGetExtraInfo(player_, &url_player_info);
-
- if (buffer_start_time) {
- *buffer_start_time = base::TimeDelta::FromMicroseconds(
- url_player_info.buffer_start_timestamp);
- }
- if (buffer_length_time) {
- *buffer_length_time =
- base::TimeDelta::FromMicroseconds(url_player_info.buffer_duration);
- }
-}
-
-void StarboardPlayer::GetVideoResolution(int* frame_width, int* frame_height) {
- DCHECK(frame_width);
- DCHECK(frame_height);
- DCHECK(is_url_based_);
-
- if (state_ == kSuspended) {
- *frame_width = video_sample_info_.frame_width;
- *frame_height = video_sample_info_.frame_height;
- return;
- }
-
- DCHECK(SbPlayerIsValid(player_));
-
- SbPlayerInfo2 out_player_info;
- SbPlayerGetInfo2(player_, &out_player_info);
-
- video_sample_info_.frame_width = out_player_info.frame_width;
- video_sample_info_.frame_height = out_player_info.frame_height;
-
- *frame_width = video_sample_info_.frame_width;
- *frame_height = video_sample_info_.frame_height;
-}
-
-base::TimeDelta StarboardPlayer::GetDuration() {
- DCHECK(is_url_based_);
-
- if (state_ == kSuspended) {
- return base::TimeDelta();
- }
-
- DCHECK(SbPlayerIsValid(player_));
-
- SbPlayerInfo2 info;
- SbPlayerGetInfo2(player_, &info);
- if (info.duration == SB_PLAYER_NO_DURATION) {
- // URL-based player may not have loaded asset yet, so map no duration to 0.
- return base::TimeDelta();
- }
- return base::TimeDelta::FromMicroseconds(info.duration);
-}
-
-base::TimeDelta StarboardPlayer::GetStartDate() {
- DCHECK(is_url_based_);
-
- if (state_ == kSuspended) {
- return base::TimeDelta();
- }
-
- DCHECK(SbPlayerIsValid(player_));
-
- SbPlayerInfo2 info;
- SbPlayerGetInfo2(player_, &info);
- return base::TimeDelta::FromMicroseconds(info.start_date);
-}
-
-void StarboardPlayer::SetDrmSystem(SbDrmSystem drm_system) {
- DCHECK(is_url_based_);
-
- drm_system_ = drm_system;
- SbUrlPlayerSetDrmSystem(player_, drm_system);
-}
-#endif // SB_HAS(PLAYER_WITH_URL)
-
-void StarboardPlayer::Suspend() {
- DCHECK(task_runner_->BelongsToCurrentThread());
-
- // Check if the player is already suspended.
- if (state_ == kSuspended) {
- return;
- }
-
- DCHECK(SbPlayerIsValid(player_));
-
- SbPlayerSetPlaybackRate(player_, 0.0);
-
- set_bounds_helper_->SetPlayer(NULL);
-
- base::AutoLock auto_lock(lock_);
- GetInfo_Locked(&cached_video_frames_decoded_, &cached_video_frames_dropped_,
- &preroll_timestamp_);
-
- state_ = kSuspended;
-
- video_frame_provider_->SetOutputMode(VideoFrameProvider::kOutputModeInvalid);
- video_frame_provider_->ResetGetCurrentSbDecodeTargetFunction();
-
- SbPlayerDestroy(player_);
-
- player_ = kSbPlayerInvalid;
-}
-
-void StarboardPlayer::Resume(SbWindow window) {
- DCHECK(task_runner_->BelongsToCurrentThread());
-
- window_ = window;
-
- // Check if the player is already resumed.
- if (state_ != kSuspended) {
- DCHECK(SbPlayerIsValid(player_));
- return;
- }
-
- decoder_buffer_cache_.StartResuming();
-
-#if SB_HAS(PLAYER_WITH_URL)
- if (is_url_based_) {
- CreateUrlPlayer(url_);
- if (SbDrmSystemIsValid(drm_system_)) {
- SbUrlPlayerSetDrmSystem(player_, drm_system_);
- }
- } else {
- CreatePlayer();
- }
-#else // SB_HAS(PLAYER_WITH_URL)
- CreatePlayer();
-#endif // SB_HAS(PLAYER_WITH_URL)
-
- if (SbPlayerIsValid(player_)) {
- base::AutoLock auto_lock(lock_);
- state_ = kResuming;
- UpdateBounds_Locked();
- }
-}
-
-namespace {
-VideoFrameProvider::OutputMode ToVideoFrameProviderOutputMode(
- SbPlayerOutputMode output_mode) {
- switch (output_mode) {
- case kSbPlayerOutputModeDecodeToTexture:
- return VideoFrameProvider::kOutputModeDecodeToTexture;
- case kSbPlayerOutputModePunchOut:
- return VideoFrameProvider::kOutputModePunchOut;
- case kSbPlayerOutputModeInvalid:
- return VideoFrameProvider::kOutputModeInvalid;
- }
-
- NOTREACHED();
- return VideoFrameProvider::kOutputModeInvalid;
-}
-
-} // namespace
-
-#if SB_HAS(PLAYER_WITH_URL)
-// static
-void StarboardPlayer::EncryptedMediaInitDataEncounteredCB(
- SbPlayer player, void* context, const char* init_data_type,
- const unsigned char* init_data, unsigned int init_data_length) {
- StarboardPlayer* helper = static_cast<StarboardPlayer*>(context);
- DCHECK(!helper->on_encrypted_media_init_data_encountered_cb_.is_null());
- // TODO: Use callback_helper here.
- helper->on_encrypted_media_init_data_encountered_cb_.Run(
- init_data_type, init_data, init_data_length);
-}
-
-void StarboardPlayer::CreateUrlPlayer(const std::string& url) {
- TRACE_EVENT0("cobalt::media", "StarboardPlayer::CreateUrlPlayer");
- DCHECK(task_runner_->BelongsToCurrentThread());
-
- DCHECK(!on_encrypted_media_init_data_encountered_cb_.is_null());
- LOG(INFO) << "CreateUrlPlayer passed url " << url;
-
- if (max_video_capabilities_.empty()) {
- FormatSupportQueryMetrics::PrintAndResetMetrics();
- }
-
- player_creation_time_ = SbTimeGetMonotonicNow();
-
- player_ =
- SbUrlPlayerCreate(url.c_str(), window_, &StarboardPlayer::PlayerStatusCB,
- &StarboardPlayer::EncryptedMediaInitDataEncounteredCB,
- &StarboardPlayer::PlayerErrorCB, this);
- DCHECK(SbPlayerIsValid(player_));
-
- if (output_mode_ == kSbPlayerOutputModeDecodeToTexture) {
- // If the player is setup to decode to texture, then provide Cobalt with
- // a method of querying that texture.
- video_frame_provider_->SetGetCurrentSbDecodeTargetFunction(base::Bind(
- &StarboardPlayer::GetCurrentSbDecodeTarget, base::Unretained(this)));
- }
- video_frame_provider_->SetOutputMode(
- ToVideoFrameProviderOutputMode(output_mode_));
-
- set_bounds_helper_->SetPlayer(this);
-
- base::AutoLock auto_lock(lock_);
- UpdateBounds_Locked();
-}
-#endif // SB_HAS(PLAYER_WITH_URL)
-void StarboardPlayer::CreatePlayer() {
- TRACE_EVENT0("cobalt::media", "StarboardPlayer::CreatePlayer");
- DCHECK(task_runner_->BelongsToCurrentThread());
-
- bool is_visible = SbWindowIsValid(window_);
- SbMediaAudioCodec audio_codec = audio_sample_info_.codec;
-
- bool has_audio = audio_codec != kSbMediaAudioCodecNone;
-
- is_creating_player_ = true;
-
- if (max_video_capabilities_.empty()) {
- FormatSupportQueryMetrics::PrintAndResetMetrics();
- }
-
- player_creation_time_ = SbTimeGetMonotonicNow();
-
- SbPlayerCreationParam creation_param = {};
- creation_param.drm_system = drm_system_;
- creation_param.audio_sample_info = audio_sample_info_;
- creation_param.video_sample_info = video_sample_info_;
- // TODO: This is temporary for supporting background media playback.
- // Need to be removed with media refactor.
- if (!is_visible) {
- creation_param.video_sample_info.codec = kSbMediaVideoCodecNone;
- }
- creation_param.output_mode = output_mode_;
- DCHECK_EQ(SbPlayerGetPreferredOutputMode(&creation_param), output_mode_);
- player_ = SbPlayerCreate(
- window_, &creation_param, &StarboardPlayer::DeallocateSampleCB,
- &StarboardPlayer::DecoderStatusCB, &StarboardPlayer::PlayerStatusCB,
- &StarboardPlayer::PlayerErrorCB, this,
- get_decode_target_graphics_context_provider_func_.Run());
-
- is_creating_player_ = false;
-
- if (!SbPlayerIsValid(player_)) {
- return;
- }
-
- if (output_mode_ == kSbPlayerOutputModeDecodeToTexture) {
- // If the player is setup to decode to texture, then provide Cobalt with
- // a method of querying that texture.
- video_frame_provider_->SetGetCurrentSbDecodeTargetFunction(base::Bind(
- &StarboardPlayer::GetCurrentSbDecodeTarget, base::Unretained(this)));
- }
- video_frame_provider_->SetOutputMode(
- ToVideoFrameProviderOutputMode(output_mode_));
- set_bounds_helper_->SetPlayer(this);
-
- base::AutoLock auto_lock(lock_);
- UpdateBounds_Locked();
-}
-
-void StarboardPlayer::WriteNextBufferFromCache(DemuxerStream::Type type) {
- DCHECK(state_ != kSuspended);
-#if SB_HAS(PLAYER_WITH_URL)
- DCHECK(!is_url_based_);
-#endif // SB_HAS(PLAYER_WITH_URL)
-
- const scoped_refptr<DecoderBuffer>& buffer =
- decoder_buffer_cache_.GetBuffer(type);
- DCHECK(buffer);
- decoder_buffer_cache_.AdvanceToNextBuffer(type);
-
- DCHECK(SbPlayerIsValid(player_));
-
- WriteBufferInternal(type, buffer);
-}
-
-void StarboardPlayer::WriteBufferInternal(
- DemuxerStream::Type type, const scoped_refptr<DecoderBuffer>& buffer) {
-#if SB_HAS(PLAYER_WITH_URL)
- DCHECK(!is_url_based_);
-#endif // SB_HAS(PLAYER_WITH_URL)
-
- if (buffer->end_of_stream()) {
- SbPlayerWriteEndOfStream(player_, DemuxerStreamTypeToSbMediaType(type));
- return;
- }
-
- DecodingBuffers::iterator iter = decoding_buffers_.find(buffer->data());
- if (iter == decoding_buffers_.end()) {
- decoding_buffers_[buffer->data()] = std::make_pair(buffer, 1);
- } else {
- ++iter->second.second;
- }
-
- auto sample_type = DemuxerStreamTypeToSbMediaType(type);
-
- if (sample_type == kSbMediaTypeAudio && first_audio_sample_time_ == 0) {
- first_audio_sample_time_ = SbTimeGetMonotonicNow();
- } else if (sample_type == kSbMediaTypeVideo &&
- first_video_sample_time_ == 0) {
- first_video_sample_time_ = SbTimeGetMonotonicNow();
- }
-
- SbDrmSampleInfo drm_info;
- SbDrmSubSampleMapping subsample_mapping;
- drm_info.subsample_count = 0;
- if (buffer->decrypt_config()) {
- FillDrmSampleInfo(buffer, &drm_info, &subsample_mapping);
- }
-
- DCHECK_GT(SbPlayerGetMaximumNumberOfSamplesPerWrite(player_, sample_type), 0);
-
- SbPlayerSampleSideData side_data = {};
- SbPlayerSampleInfo sample_info = {};
- sample_info.type = sample_type;
- sample_info.buffer = buffer->data();
- sample_info.buffer_size = buffer->data_size();
- sample_info.timestamp = buffer->timestamp().InMicroseconds();
-
- if (buffer->side_data_size() > 0) {
- // We only support at most one side data currently.
- side_data.data = buffer->side_data();
- side_data.size = buffer->side_data_size();
- sample_info.side_data = &side_data;
- sample_info.side_data_count = 1;
- }
-
- if (sample_type == kSbMediaTypeAudio) {
- sample_info.audio_sample_info = audio_sample_info_;
- } else {
- DCHECK(sample_type == kSbMediaTypeVideo);
- sample_info.video_sample_info = video_sample_info_;
- sample_info.video_sample_info.is_key_frame = buffer->is_key_frame();
- }
- if (drm_info.subsample_count > 0) {
- sample_info.drm_info = &drm_info;
- } else {
- sample_info.drm_info = NULL;
- }
- SbPlayerWriteSample2(player_, sample_type, &sample_info, 1);
-}
-
-SbDecodeTarget StarboardPlayer::GetCurrentSbDecodeTarget() {
- return SbPlayerGetCurrentFrame(player_);
-}
-
-SbPlayerOutputMode StarboardPlayer::GetSbPlayerOutputMode() {
- return output_mode_;
-}
-
-void StarboardPlayer::GetInfo_Locked(uint32* video_frames_decoded,
- uint32* video_frames_dropped,
- base::TimeDelta* media_time) {
- lock_.AssertAcquired();
- if (state_ == kSuspended) {
- if (video_frames_decoded) {
- *video_frames_decoded = cached_video_frames_decoded_;
- }
- if (video_frames_dropped) {
- *video_frames_dropped = cached_video_frames_dropped_;
- }
- if (media_time) {
- *media_time = preroll_timestamp_;
- }
- return;
- }
-
- DCHECK(SbPlayerIsValid(player_));
-
- SbPlayerInfo2 info;
- SbPlayerGetInfo2(player_, &info);
-
- if (media_time) {
- *media_time =
- base::TimeDelta::FromMicroseconds(info.current_media_timestamp);
- }
- if (video_frames_decoded) {
- *video_frames_decoded = info.total_video_frames;
- }
- if (video_frames_dropped) {
- *video_frames_dropped = info.dropped_video_frames;
- }
-}
-
-void StarboardPlayer::UpdateBounds_Locked() {
- lock_.AssertAcquired();
- DCHECK(SbPlayerIsValid(player_));
-
- if (!set_bounds_z_index_ || !set_bounds_rect_) {
- return;
- }
-
- auto& rect = *set_bounds_rect_;
- SbPlayerSetBounds(player_, *set_bounds_z_index_, rect.x(), rect.y(),
- rect.width(), rect.height());
-}
-
-void StarboardPlayer::ClearDecoderBufferCache() {
- DCHECK(task_runner_->BelongsToCurrentThread());
-
- if (state_ != kResuming) {
- base::TimeDelta media_time;
- GetInfo(NULL, NULL, &media_time);
- decoder_buffer_cache_.ClearSegmentsBeforeMediaTime(media_time);
- }
-
- task_runner_->PostDelayedTask(
- FROM_HERE,
- base::Bind(&StarboardPlayer::CallbackHelper::ClearDecoderBufferCache,
- callback_helper_),
- base::TimeDelta::FromMilliseconds(
- kClearDecoderCacheIntervalInMilliseconds));
-}
-
-void StarboardPlayer::OnDecoderStatus(SbPlayer player, SbMediaType type,
- SbPlayerDecoderState state, int ticket) {
-#if SB_HAS(PLAYER_WITH_URL)
- DCHECK(!is_url_based_);
-#endif // SB_HAS(PLAYER_WITH_URL)
- DCHECK(task_runner_->BelongsToCurrentThread());
-
- if (player_ != player || ticket != ticket_) {
- return;
- }
-
- DCHECK_NE(state_, kSuspended);
-
- switch (state) {
- case kSbPlayerDecoderStateNeedsData:
- break;
- }
-
- if (state_ == kResuming) {
- DemuxerStream::Type stream_type =
- ::media::SbMediaTypeToDemuxerStreamType(type);
- if (decoder_buffer_cache_.GetBuffer(stream_type)) {
- WriteNextBufferFromCache(stream_type);
- return;
- }
- if (!decoder_buffer_cache_.GetBuffer(DemuxerStream::AUDIO) &&
- !decoder_buffer_cache_.GetBuffer(DemuxerStream::VIDEO)) {
- state_ = kPlaying;
- }
- }
-
- host_->OnNeedData(::media::SbMediaTypeToDemuxerStreamType(type));
-}
-
-void StarboardPlayer::OnPlayerStatus(SbPlayer player, SbPlayerState state,
- int ticket) {
- TRACE_EVENT1("cobalt::media", "StarboardPlayer::OnPlayerStatus", "state",
- state);
- DCHECK(task_runner_->BelongsToCurrentThread());
-
- if (player_ != player) {
- return;
- }
-
- DCHECK_NE(state_, kSuspended);
-
- if (ticket != SB_PLAYER_INITIAL_TICKET && ticket != ticket_) {
- return;
- }
-
- if (state == kSbPlayerStateInitialized) {
- if (ticket_ == SB_PLAYER_INITIAL_TICKET) {
- ++ticket_;
- }
- if (sb_player_state_initialized_time_ == 0) {
- sb_player_state_initialized_time_ = SbTimeGetMonotonicNow();
- }
- SbPlayerSeek2(player_, preroll_timestamp_.InMicroseconds(), ticket_);
- SetVolume(volume_);
- SbPlayerSetPlaybackRate(player_, playback_rate_);
- return;
- }
- if (state == kSbPlayerStatePrerolling &&
- sb_player_state_prerolling_time_ == 0) {
- sb_player_state_prerolling_time_ = SbTimeGetMonotonicNow();
- } else if (state == kSbPlayerStatePresenting &&
- sb_player_state_presenting_time_ == 0) {
- sb_player_state_presenting_time_ = SbTimeGetMonotonicNow();
-#if !defined(COBALT_BUILD_TYPE_GOLD)
- LogStartupLatency();
-#endif // !defined(COBALT_BUILD_TYPE_GOLD)
- }
- host_->OnPlayerStatus(state);
-}
-
-void StarboardPlayer::OnPlayerError(SbPlayer player, SbPlayerError error,
- const std::string& message) {
- DCHECK(task_runner_->BelongsToCurrentThread());
-
- if (player_ != player) {
- return;
- }
- host_->OnPlayerError(error, message);
-}
-
-void StarboardPlayer::OnDeallocateSample(const void* sample_buffer) {
-#if SB_HAS(PLAYER_WITH_URL)
- DCHECK(!is_url_based_);
-#endif // SB_HAS(PLAYER_WITH_URL)
- DCHECK(task_runner_->BelongsToCurrentThread());
-
- DecodingBuffers::iterator iter = decoding_buffers_.find(sample_buffer);
- DCHECK(iter != decoding_buffers_.end());
- if (iter == decoding_buffers_.end()) {
- LOG(ERROR) << "StarboardPlayer::OnDeallocateSample encounters unknown "
- << "sample_buffer " << sample_buffer;
- return;
- }
- --iter->second.second;
- if (iter->second.second == 0) {
- decoding_buffers_.erase(iter);
- }
-}
-
-bool StarboardPlayer::TryToSetPlayerCreationErrorMessage(
- const std::string& message) {
- DCHECK(task_runner_->BelongsToCurrentThread());
- if (is_creating_player_) {
- player_creation_error_message_ = message;
- return true;
- }
- LOG(INFO) << "TryToSetPlayerCreationErrorMessage() "
- "is called when |is_creating_player_| "
- "is false. Error message is ignored.";
- return false;
-}
-
-// static
-void StarboardPlayer::DecoderStatusCB(SbPlayer player, void* context,
- SbMediaType type,
- SbPlayerDecoderState state, int ticket) {
- StarboardPlayer* helper = static_cast<StarboardPlayer*>(context);
- helper->task_runner_->PostTask(
- FROM_HERE,
- base::Bind(&StarboardPlayer::CallbackHelper::OnDecoderStatus,
- helper->callback_helper_, player, type, state, ticket));
-}
-
-// static
-void StarboardPlayer::PlayerStatusCB(SbPlayer player, void* context,
- SbPlayerState state, int ticket) {
- StarboardPlayer* helper = static_cast<StarboardPlayer*>(context);
- helper->task_runner_->PostTask(
- FROM_HERE, base::Bind(&StarboardPlayer::CallbackHelper::OnPlayerStatus,
- helper->callback_helper_, player, state, ticket));
-}
-
-// static
-void StarboardPlayer::PlayerErrorCB(SbPlayer player, void* context,
- SbPlayerError error, const char* message) {
- StarboardPlayer* helper = static_cast<StarboardPlayer*>(context);
- if (player == kSbPlayerInvalid) {
- // TODO: Simplify by combining the functionality of
- // TryToSetPlayerCreationErrorMessage() with OnPlayerError().
- if (helper->TryToSetPlayerCreationErrorMessage(message)) {
- return;
- }
- }
- helper->task_runner_->PostTask(
- FROM_HERE, base::Bind(&StarboardPlayer::CallbackHelper::OnPlayerError,
- helper->callback_helper_, player, error,
- message ? std::string(message) : ""));
-}
-
-// static
-void StarboardPlayer::DeallocateSampleCB(SbPlayer player, void* context,
- const void* sample_buffer) {
- StarboardPlayer* helper = static_cast<StarboardPlayer*>(context);
- helper->task_runner_->PostTask(
- FROM_HERE,
- base::Bind(&StarboardPlayer::CallbackHelper::OnDeallocateSample,
- helper->callback_helper_, sample_buffer));
-}
-
-#if SB_HAS(PLAYER_WITH_URL)
-// static
-SbPlayerOutputMode StarboardPlayer::ComputeSbUrlPlayerOutputMode(
- bool prefer_decode_to_texture) {
- // Try to choose the output mode according to the passed in value of
- // |prefer_decode_to_texture|. If the preferred output mode is unavailable
- // though, fallback to an output mode that is available.
- SbPlayerOutputMode output_mode = kSbPlayerOutputModeInvalid;
- if (SbUrlPlayerOutputModeSupported(kSbPlayerOutputModePunchOut)) {
- output_mode = kSbPlayerOutputModePunchOut;
- }
- if ((prefer_decode_to_texture || output_mode == kSbPlayerOutputModeInvalid) &&
- SbUrlPlayerOutputModeSupported(kSbPlayerOutputModeDecodeToTexture)) {
- output_mode = kSbPlayerOutputModeDecodeToTexture;
- }
- CHECK_NE(kSbPlayerOutputModeInvalid, output_mode);
-
- return output_mode;
-}
-#endif // SB_HAS(PLAYER_WITH_URL)
-
-// static
-SbPlayerOutputMode StarboardPlayer::ComputeSbPlayerOutputMode(
- bool prefer_decode_to_texture) const {
- SbPlayerCreationParam creation_param = {};
- creation_param.drm_system = drm_system_;
- creation_param.audio_sample_info = audio_sample_info_;
- creation_param.video_sample_info = video_sample_info_;
-
- // Try to choose |kSbPlayerOutputModeDecodeToTexture| when
- // |prefer_decode_to_texture| is true.
- if (prefer_decode_to_texture) {
- creation_param.output_mode = kSbPlayerOutputModeDecodeToTexture;
- } else {
- creation_param.output_mode = kSbPlayerOutputModePunchOut;
- }
- auto output_mode = SbPlayerGetPreferredOutputMode(&creation_param);
- CHECK_NE(kSbPlayerOutputModeInvalid, output_mode);
- return output_mode;
-}
-
-void StarboardPlayer::LogStartupLatency() const {
- std::string first_events_str;
- if (set_drm_system_ready_cb_time_ == -1) {
- first_events_str =
- starboard::FormatString("%-40s0 us", "SbPlayerCreate() called");
- } else if (set_drm_system_ready_cb_time_ < player_creation_time_) {
- first_events_str = starboard::FormatString(
- "%-40s0 us\n%-40s%" PRId64 " us", "set_drm_system_ready_cb called",
- "SbPlayerCreate() called",
- player_creation_time_ - set_drm_system_ready_cb_time_);
- } else {
- first_events_str = starboard::FormatString(
- "%-40s0 us\n%-40s%" PRId64 " us", "SbPlayerCreate() called",
- "set_drm_system_ready_cb called",
- set_drm_system_ready_cb_time_ - player_creation_time_);
- }
-
- SbTime first_event_time =
- std::max(player_creation_time_, set_drm_system_ready_cb_time_);
- SbTime player_initialization_time_delta =
- sb_player_state_initialized_time_ - first_event_time;
- SbTime player_preroll_time_delta =
- sb_player_state_prerolling_time_ - sb_player_state_initialized_time_;
- SbTime first_audio_sample_time_delta = std::max(
- first_audio_sample_time_ - sb_player_state_prerolling_time_, SbTime(0));
- SbTime first_video_sample_time_delta = std::max(
- first_video_sample_time_ - sb_player_state_prerolling_time_, SbTime(0));
- SbTime player_presenting_time_delta =
- sb_player_state_presenting_time_ -
- std::max(first_audio_sample_time_, first_video_sample_time_);
- SbTime startup_latency = sb_player_state_presenting_time_ - first_event_time;
-
- s_startup_latency.AddSample(startup_latency, 1);
-
- // clang-format off
- LOG(INFO) << starboard::FormatString(
- "\nSbPlayer startup latencies: %" PRId64 " us\n"
- " Event name time since last event\n"
- " %s\n" // |first_events_str| populated above
- " kSbPlayerStateInitialized received %" PRId64 " us\n"
- " kSbPlayerStatePrerolling received %" PRId64 " us\n"
- " First media sample(s) written [a/v] %" PRId64 "/%" PRId64 " us\n"
- " kSbPlayerStatePresenting received %" PRId64 " us\n"
- " Startup latency statistics (us):\n"
- " min: %" PRId64 ", median: %" PRId64 ", average: %" PRId64
- ", max: %" PRId64,
- startup_latency,
- first_events_str.c_str(), player_initialization_time_delta,
- player_preroll_time_delta, first_audio_sample_time_delta,
- first_video_sample_time_delta, player_presenting_time_delta,
- s_startup_latency.min(),
- s_startup_latency.GetMedian(),
- s_startup_latency.average(), s_startup_latency.max());
- // clang-format on
-}
-
-} // namespace media
-} // namespace cobalt
diff --git a/cobalt/media/base/starboard_player.h b/cobalt/media/base/starboard_player.h
deleted file mode 100644
index a71ec3b..0000000
--- a/cobalt/media/base/starboard_player.h
+++ /dev/null
@@ -1,309 +0,0 @@
-// Copyright 2016 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef COBALT_MEDIA_BASE_STARBOARD_PLAYER_H_
-#define COBALT_MEDIA_BASE_STARBOARD_PLAYER_H_
-
-#include <map>
-#include <string>
-#include <utility>
-
-#include "base/memory/ref_counted.h"
-#include "base/message_loop/message_loop.h"
-#include "base/synchronization/lock.h"
-#include "base/time/time.h"
-#include "cobalt/media/base/decoder_buffer_cache.h"
-#include "cobalt/media/base/sbplayer_set_bounds_helper.h"
-#include "cobalt/media/base/video_frame_provider.h"
-#include "starboard/media.h"
-#include "starboard/player.h"
-#include "third_party/chromium/media/base/audio_decoder_config.h"
-#include "third_party/chromium/media/base/decoder_buffer.h"
-#include "third_party/chromium/media/base/demuxer_stream.h"
-#include "third_party/chromium/media/base/video_decoder_config.h"
-#include "third_party/chromium/media/cobalt/ui/gfx/geometry/rect.h"
-
-#if SB_HAS(PLAYER_WITH_URL)
-#include SB_URL_PLAYER_INCLUDE_PATH
-#endif // SB_HAS(PLAYER_WITH_URL)
-
-namespace cobalt {
-namespace media {
-
-// TODO: Add switch to disable caching
-class StarboardPlayer {
- typedef ::media::AudioDecoderConfig AudioDecoderConfig;
- typedef ::media::DecoderBuffer DecoderBuffer;
- typedef ::media::DemuxerStream DemuxerStream;
- typedef ::media::VideoDecoderConfig VideoDecoderConfig;
-
- public:
- class Host {
- public:
- virtual void OnNeedData(DemuxerStream::Type type) = 0;
- virtual void OnPlayerStatus(SbPlayerState state) = 0;
- virtual void OnPlayerError(SbPlayerError error,
- const std::string& message) = 0;
-
- protected:
- ~Host() {}
- };
-
- // Call to get the SbDecodeTargetGraphicsContextProvider for SbPlayerCreate().
- typedef base::Callback<SbDecodeTargetGraphicsContextProvider*()>
- GetDecodeTargetGraphicsContextProviderFunc;
-
-#if SB_HAS(PLAYER_WITH_URL)
- typedef base::Callback<void(const char*, const unsigned char*, unsigned)>
- OnEncryptedMediaInitDataEncounteredCB;
- // Create a StarboardPlayer with url-based player.
- StarboardPlayer(
- const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
- const std::string& url, SbWindow window, Host* host,
- SbPlayerSetBoundsHelper* set_bounds_helper,
- bool allow_resume_after_suspend, bool prefer_decode_to_texture,
- const OnEncryptedMediaInitDataEncounteredCB&
- encrypted_media_init_data_encountered_cb,
- VideoFrameProvider* const video_frame_provider);
-#endif // SB_HAS(PLAYER_WITH_URL)
- // Create a StarboardPlayer with normal player
- StarboardPlayer(
- const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
- const GetDecodeTargetGraphicsContextProviderFunc&
- get_decode_target_graphics_context_provider_func,
- const AudioDecoderConfig& audio_config,
- const std::string& audio_mime_type,
- const VideoDecoderConfig& video_config,
- const std::string& video_mime_type, SbWindow window,
- SbDrmSystem drm_system, Host* host,
- SbPlayerSetBoundsHelper* set_bounds_helper,
- bool allow_resume_after_suspend, bool prefer_decode_to_texture,
- VideoFrameProvider* const video_frame_provider,
- const std::string& max_video_capabilities);
-
- ~StarboardPlayer();
-
- bool IsValid() const { return SbPlayerIsValid(player_); }
-
- void UpdateAudioConfig(const AudioDecoderConfig& audio_config,
- const std::string& mime_type);
- void UpdateVideoConfig(const VideoDecoderConfig& video_config,
- const std::string& mime_type);
-
- void WriteBuffer(DemuxerStream::Type type,
- const scoped_refptr<DecoderBuffer>& buffer);
-
- void SetBounds(int z_index, const gfx::Rect& rect);
-
- void PrepareForSeek();
- void Seek(base::TimeDelta time);
-
- void SetVolume(float volume);
- void SetPlaybackRate(double playback_rate);
- void GetInfo(uint32* video_frames_decoded, uint32* video_frames_dropped,
- base::TimeDelta* media_time);
-
-#if SB_HAS(PLAYER_WITH_URL)
- void GetUrlPlayerBufferedTimeRanges(base::TimeDelta* buffer_start_time,
- base::TimeDelta* buffer_length_time);
- void GetVideoResolution(int* frame_width, int* frame_height);
- base::TimeDelta GetDuration();
- base::TimeDelta GetStartDate();
- void SetDrmSystem(SbDrmSystem drm_system);
-#endif // SB_HAS(PLAYER_WITH_URL)
-
- void Suspend();
- // TODO: This is temporary for supporting background media playback.
- // Need to be removed with media refactor.
- void Resume(SbWindow window);
-
- // These functions help the pipeline report an error message on a player
- // creation error. TryToSetPlayerCreationErrorMessage() will set
- // |player_creation_error_message_| and return true when called while
- // |is_creating_player_| is true, else it will do nothing and return false.
- bool TryToSetPlayerCreationErrorMessage(const std::string& message);
- std::string GetPlayerCreationErrorMessage() const {
- return player_creation_error_message_;
- }
-
- SbDecodeTarget GetCurrentSbDecodeTarget();
- SbPlayerOutputMode GetSbPlayerOutputMode();
-
- void RecordSetDrmSystemReadyTime(SbTimeMonotonic timestamp) {
- set_drm_system_ready_cb_time_ = timestamp;
- }
-
- private:
- enum State {
- kPlaying,
- kSuspended,
- kResuming,
- };
-
- // This class ensures that the callbacks posted to |task_runner_| are ignored
- // automatically once StarboardPlayer is destroyed.
- class CallbackHelper : public base::RefCountedThreadSafe<CallbackHelper> {
- public:
- explicit CallbackHelper(StarboardPlayer* player);
-
- void ClearDecoderBufferCache();
-
- void OnDecoderStatus(SbPlayer player, SbMediaType type,
- SbPlayerDecoderState state, int ticket);
- void OnPlayerStatus(SbPlayer player, SbPlayerState state, int ticket);
- void OnPlayerError(SbPlayer player, SbPlayerError error,
- const std::string& message);
- void OnDeallocateSample(const void* sample_buffer);
-
- void ResetPlayer();
-
- private:
- base::Lock lock_;
- StarboardPlayer* player_;
- };
-
- static const int64 kClearDecoderCacheIntervalInMilliseconds = 1000;
-
- // A map from raw data pointer returned by DecoderBuffer::GetData() to the
- // DecoderBuffer and a reference count. The reference count indicates how
- // many instances of the DecoderBuffer is currently being decoded in the
- // pipeline.
- typedef std::map<const void*, std::pair<scoped_refptr<DecoderBuffer>, int>>
- DecodingBuffers;
-
-#if SB_HAS(PLAYER_WITH_URL)
- OnEncryptedMediaInitDataEncounteredCB
- on_encrypted_media_init_data_encountered_cb_;
-
- static void EncryptedMediaInitDataEncounteredCB(
- SbPlayer player, void* context, const char* init_data_type,
- const unsigned char* init_data, unsigned int init_data_length);
-
- void CreateUrlPlayer(const std::string& url);
-#endif // SB_HAS(PLAYER_WITH_URL)
- void CreatePlayer();
-
- void WriteNextBufferFromCache(DemuxerStream::Type type);
- void WriteBufferInternal(DemuxerStream::Type type,
- const scoped_refptr<DecoderBuffer>& buffer);
-
- void GetInfo_Locked(uint32* video_frames_decoded,
- uint32* video_frames_dropped,
- base::TimeDelta* media_time);
- void UpdateBounds_Locked();
-
- void ClearDecoderBufferCache();
-
- void OnDecoderStatus(SbPlayer player, SbMediaType type,
- SbPlayerDecoderState state, int ticket);
- void OnPlayerStatus(SbPlayer player, SbPlayerState state, int ticket);
- void OnPlayerError(SbPlayer player, SbPlayerError error,
- const std::string& message);
- void OnDeallocateSample(const void* sample_buffer);
-
- static void DecoderStatusCB(SbPlayer player, void* context, SbMediaType type,
- SbPlayerDecoderState state, int ticket);
- static void PlayerStatusCB(SbPlayer player, void* context,
- SbPlayerState state, int ticket);
- static void PlayerErrorCB(SbPlayer player, void* context, SbPlayerError error,
- const char* message);
- static void DeallocateSampleCB(SbPlayer player, void* context,
- const void* sample_buffer);
-
-#if SB_HAS(PLAYER_WITH_URL)
- static SbPlayerOutputMode ComputeSbUrlPlayerOutputMode(
- bool prefer_decode_to_texture);
-#endif // SB_HAS(PLAYER_WITH_URL)
- // Returns the output mode that should be used for a video with the given
- // specifications.
- SbPlayerOutputMode ComputeSbPlayerOutputMode(
- bool prefer_decode_to_texture) const;
-
- void LogStartupLatency() const;
-
-// The following variables are initialized in the ctor and never changed.
-#if SB_HAS(PLAYER_WITH_URL)
- std::string url_;
-#endif // SB_HAS(PLAYER_WITH_URL)
- const scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
- const GetDecodeTargetGraphicsContextProviderFunc
- get_decode_target_graphics_context_provider_func_;
- scoped_refptr<CallbackHelper> callback_helper_;
- SbWindow window_;
- SbDrmSystem drm_system_ = kSbDrmSystemInvalid;
- Host* const host_;
- // Consider merge |SbPlayerSetBoundsHelper| into CallbackHelper.
- SbPlayerSetBoundsHelper* const set_bounds_helper_;
- const bool allow_resume_after_suspend_;
-
- // The following variables are only changed or accessed from the
- // |task_runner_|.
- AudioDecoderConfig audio_config_;
- VideoDecoderConfig video_config_;
- SbMediaAudioSampleInfo audio_sample_info_ = {};
- SbMediaVideoSampleInfo video_sample_info_ = {};
- DecodingBuffers decoding_buffers_;
- int ticket_ = SB_PLAYER_INITIAL_TICKET;
- float volume_ = 1.0f;
- double playback_rate_ = 0.0;
- bool seek_pending_ = false;
- DecoderBufferCache decoder_buffer_cache_;
-
- // The following variables can be accessed from GetInfo(), which can be called
- // from any threads. So some of their usages have to be guarded by |lock_|.
- base::Lock lock_;
-
- // Stores the |z_index| and |rect| parameters of the latest SetBounds() call.
- base::Optional<int> set_bounds_z_index_;
- base::Optional<gfx::Rect> set_bounds_rect_;
- State state_ = kPlaying;
- SbPlayer player_;
- uint32 cached_video_frames_decoded_;
- uint32 cached_video_frames_dropped_;
- base::TimeDelta preroll_timestamp_;
-
- // Keep track of the output mode we are supposed to output to.
- SbPlayerOutputMode output_mode_;
-
- VideoFrameProvider* const video_frame_provider_;
-
- // Keep copies of the mime type strings instead of using the ones in the
- // DemuxerStreams to ensure that the strings are always valid.
- std::string audio_mime_type_;
- std::string video_mime_type_;
- // A string of video maximum capabilities.
- std::string max_video_capabilities_;
-
- // Keep track of errors during player creation.
- bool is_creating_player_ = false;
- std::string player_creation_error_message_;
-
- // Variables related to tracking player startup latencies.
- SbTimeMonotonic set_drm_system_ready_cb_time_ = -1;
- SbTimeMonotonic player_creation_time_ = 0;
- SbTimeMonotonic sb_player_state_initialized_time_ = 0;
- SbTimeMonotonic sb_player_state_prerolling_time_ = 0;
- SbTimeMonotonic first_audio_sample_time_ = 0;
- SbTimeMonotonic first_video_sample_time_ = 0;
- SbTimeMonotonic sb_player_state_presenting_time_ = 0;
-
-#if SB_HAS(PLAYER_WITH_URL)
- const bool is_url_based_;
-#endif // SB_HAS(PLAYER_WITH_URL)
-};
-
-} // namespace media
-} // namespace cobalt
-
-#endif // COBALT_MEDIA_BASE_STARBOARD_PLAYER_H_
diff --git a/cobalt/media/base/video_frame_provider.h b/cobalt/media/base/video_frame_provider.h
deleted file mode 100644
index 0a9dfe9..0000000
--- a/cobalt/media/base/video_frame_provider.h
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright 2018 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef COBALT_MEDIA_BASE_VIDEO_FRAME_PROVIDER_H_
-#define COBALT_MEDIA_BASE_VIDEO_FRAME_PROVIDER_H_
-
-#include "base/callback.h"
-#include "base/memory/ref_counted.h"
-#include "base/synchronization/lock.h"
-#include "base/time/time.h"
-#include "starboard/decode_target.h"
-
-namespace cobalt {
-namespace media {
-
-// The VideoFrameProvider manages the backlog for video frames. It has the
-// following functionalities:
-// 1. It caches the video frames ready to be displayed.
-// 2. It decides which frame to be displayed at the current time.
-// 3. It removes frames that will no longer be displayed.
-class VideoFrameProvider
- : public base::RefCountedThreadSafe<VideoFrameProvider> {
- public:
- enum OutputMode {
- kOutputModePunchOut,
- kOutputModeDecodeToTexture,
- kOutputModeInvalid,
- };
-
- VideoFrameProvider() : output_mode_(kOutputModeInvalid) {}
-
- typedef base::Callback<SbDecodeTarget()> GetCurrentSbDecodeTargetFunction;
-
- void SetOutputMode(OutputMode output_mode) {
- base::AutoLock auto_lock(lock_);
- output_mode_ = output_mode;
- }
-
- VideoFrameProvider::OutputMode GetOutputMode() const {
- base::AutoLock auto_lock(lock_);
- return output_mode_;
- }
-
- // For Starboard platforms that have a decode-to-texture player, we enable
- // this VideoFrameProvider to act as a bridge for Cobalt code to query
- // for the current SbDecodeTarget. In effect, we bypass all of
- // VideoFrameProvider's logic in this case, instead relying on the
- // Starboard implementation to provide us with the current video frame when
- // needed.
- void SetGetCurrentSbDecodeTargetFunction(
- GetCurrentSbDecodeTargetFunction function) {
- base::AutoLock auto_lock(lock_);
- get_current_sb_decode_target_function_ = function;
- }
-
- void ResetGetCurrentSbDecodeTargetFunction() {
- base::AutoLock auto_lock(lock_);
- get_current_sb_decode_target_function_.Reset();
- }
-
- SbDecodeTarget GetCurrentSbDecodeTarget() const {
- base::AutoLock auto_lock(lock_);
- if (get_current_sb_decode_target_function_.is_null()) {
- return kSbDecodeTargetInvalid;
- } else {
- return get_current_sb_decode_target_function_.Run();
- }
- }
-
- private:
- mutable base::Lock lock_;
-
- OutputMode output_mode_;
- GetCurrentSbDecodeTargetFunction get_current_sb_decode_target_function_;
-
- DISALLOW_COPY_AND_ASSIGN(VideoFrameProvider);
-};
-
-} // namespace media
-} // namespace cobalt
-
-#endif // COBALT_MEDIA_BASE_VIDEO_FRAME_PROVIDER_H_
diff --git a/cobalt/media/decoder_buffer_allocator.cc b/cobalt/media/decoder_buffer_allocator.cc
index 57b9974..db38a23 100644
--- a/cobalt/media/decoder_buffer_allocator.cc
+++ b/cobalt/media/decoder_buffer_allocator.cc
@@ -162,6 +162,48 @@
}
}
+int DecoderBufferAllocator::GetAudioBufferBudget() const {
+ return SbMediaGetAudioBufferBudget();
+}
+
+int DecoderBufferAllocator::GetBufferAlignment() const {
+#if SB_API_VERSION >= 14
+ return SbMediaGetBufferAlignment();
+#else // SB_API_VERSION >= 14
+ return std::max(SbMediaGetBufferAlignment(kSbMediaTypeAudio),
+ SbMediaGetBufferAlignment(kSbMediaTypeVideo));
+#endif // SB_API_VERSION >= 14
+}
+
+int DecoderBufferAllocator::GetBufferPadding() const {
+#if SB_API_VERSION >= 14
+ return SbMediaGetBufferPadding();
+#else // SB_API_VERSION >= 14
+ return std::max(SbMediaGetBufferPadding(kSbMediaTypeAudio),
+ SbMediaGetBufferPadding(kSbMediaTypeVideo));
+#endif // SB_API_VERSION >= 14
+}
+
+SbTime DecoderBufferAllocator::GetBufferGarbageCollectionDurationThreshold()
+ const {
+ return SbMediaGetBufferGarbageCollectionDurationThreshold();
+}
+
+int DecoderBufferAllocator::GetProgressiveBufferBudget(
+ SbMediaVideoCodec codec, int resolution_width, int resolution_height,
+ int bits_per_pixel) const {
+ return SbMediaGetProgressiveBufferBudget(codec, resolution_width,
+ resolution_height, bits_per_pixel);
+}
+
+int DecoderBufferAllocator::GetVideoBufferBudget(SbMediaVideoCodec codec,
+ int resolution_width,
+ int resolution_height,
+ int bits_per_pixel) const {
+ return SbMediaGetVideoBufferBudget(codec, resolution_width, resolution_height,
+ bits_per_pixel);
+}
+
size_t DecoderBufferAllocator::GetAllocatedMemory() const {
if (!using_memory_pool_) {
return sbmemory_bytes_used_.load();
diff --git a/cobalt/media/decoder_buffer_allocator.h b/cobalt/media/decoder_buffer_allocator.h
index 31c3df4..e001547 100644
--- a/cobalt/media/decoder_buffer_allocator.h
+++ b/cobalt/media/decoder_buffer_allocator.h
@@ -43,6 +43,17 @@
void* Allocate(size_t size, size_t alignment) override;
void Free(void* p, size_t size) override;
+ int GetAudioBufferBudget() const override;
+ int GetBufferAlignment() const override;
+ int GetBufferPadding() const override;
+ SbTime GetBufferGarbageCollectionDurationThreshold() const override;
+ int GetProgressiveBufferBudget(SbMediaVideoCodec codec, int resolution_width,
+ int resolution_height,
+ int bits_per_pixel) const override;
+ int GetVideoBufferBudget(SbMediaVideoCodec codec, int resolution_width,
+ int resolution_height,
+ int bits_per_pixel) const override;
+
// DecoderBufferMemoryInfo methods.
size_t GetAllocatedMemory() const override;
size_t GetCurrentMemoryCapacity() const override;
diff --git a/cobalt/media/fetcher_buffered_data_source.cc b/cobalt/media/fetcher_buffered_data_source.cc
deleted file mode 100644
index c8a9595..0000000
--- a/cobalt/media/fetcher_buffered_data_source.cc
+++ /dev/null
@@ -1,520 +0,0 @@
-// Copyright 2015 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "cobalt/media/fetcher_buffered_data_source.h"
-
-#include <algorithm>
-#include <memory>
-#include <string>
-#include <utility>
-
-#include "base/bind.h"
-#include "base/callback_helpers.h"
-#include "base/strings/string_number_conversions.h"
-#include "cobalt/base/polymorphic_downcast.h"
-#include "cobalt/loader/cors_preflight.h"
-#include "cobalt/loader/url_fetcher_string_writer.h"
-#include "net/http/http_response_headers.h"
-#include "net/http/http_status_code.h"
-
-namespace cobalt {
-namespace media {
-
-namespace {
-
-const uint32 kBackwardBytes = 256 * 1024;
-const uint32 kInitialForwardBytes = 3 * 256 * 1024;
-const uint32 kInitialBufferCapacity = kBackwardBytes + kInitialForwardBytes;
-
-} // namespace
-
-using base::CircularBufferShell;
-
-FetcherBufferedDataSource::FetcherBufferedDataSource(
- const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
- const GURL& url, const csp::SecurityCallback& security_callback,
- network::NetworkModule* network_module, loader::RequestMode request_mode,
- loader::Origin origin)
- : task_runner_(task_runner),
- url_(url),
- network_module_(network_module),
- is_downloading_(false),
- buffer_(kInitialBufferCapacity, CircularBufferShell::kReserve),
- buffer_offset_(0),
- error_occured_(false),
- last_request_offset_(0),
- last_request_size_(0),
- last_read_position_(0),
- pending_read_position_(0),
- pending_read_size_(0),
- pending_read_data_(NULL),
- security_callback_(security_callback),
- request_mode_(request_mode),
- document_origin_(origin),
- is_origin_safe_(false) {
- DCHECK(task_runner_);
- DCHECK(network_module);
-}
-
-FetcherBufferedDataSource::~FetcherBufferedDataSource() {
- if (cancelable_create_fetcher_closure_) {
- cancelable_create_fetcher_closure_->Cancel();
- }
-}
-
-void FetcherBufferedDataSource::Read(int64 position, int size, uint8* data,
- const ReadCB& read_cb) {
- DCHECK_GE(position, 0);
- DCHECK_GE(size, 0);
-
- if (position < 0 || size < 0) {
- read_cb.Run(kInvalidSize);
- return;
- }
-
- base::AutoLock auto_lock(lock_);
- Read_Locked(static_cast<uint64>(position), static_cast<size_t>(size), data,
- read_cb);
-}
-
-void FetcherBufferedDataSource::Stop() {
- {
- base::AutoLock auto_lock(lock_);
-
- if (!pending_read_cb_.is_null()) {
- base::ResetAndReturn(&pending_read_cb_).Run(0);
- }
- // From this moment on, any call to Read() should be treated as an error.
- // Note that we cannot reset |fetcher_| here because of:
- // 1. Fetcher has to be destroyed on the thread that it is created, however
- // Stop() is usually called from the pipeline thread where |fetcher_| is
- // created on the web thread.
- // 2. We cannot post a task to the web thread to destroy |fetcher_| as the
- // web thread is blocked by WMPI::Destroy().
- // Once error_occured_ is set to true, the fetcher callbacks return
- // immediately so it is safe to destroy |fetcher_| inside the dtor.
- error_occured_ = true;
- }
-}
-
-bool FetcherBufferedDataSource::GetSize(int64* size_out) {
- base::AutoLock auto_lock(lock_);
-
- if (total_size_of_resource_) {
- *size_out = static_cast<int64>(total_size_of_resource_.value());
- DCHECK_GE(*size_out, 0);
- } else {
- *size_out = kInvalidSize;
- }
- return *size_out != kInvalidSize;
-}
-
-void FetcherBufferedDataSource::SetDownloadingStatusCB(
- const DownloadingStatusCB& downloading_status_cb) {
- DCHECK(task_runner_->BelongsToCurrentThread());
-
- DCHECK(!downloading_status_cb.is_null());
- DCHECK(downloading_status_cb_.is_null());
- downloading_status_cb_ = downloading_status_cb;
-}
-
-void FetcherBufferedDataSource::OnURLFetchResponseStarted(
- const net::URLFetcher* source) {
- DCHECK(task_runner_->BelongsToCurrentThread());
-
- base::AutoLock auto_lock(lock_);
-
- if (fetcher_.get() != source || error_occured_) {
- return;
- }
-
- if (!source->GetStatus().is_success()) {
- // The error will be handled on OnURLFetchComplete()
- error_occured_ = true;
- return;
- } else if (source->GetResponseCode() == -1) {
- // Could be a file URL, so we won't expect headers.
- return;
- }
-
- // In the event of a redirect, re-check the security policy.
- if (source->GetURL() != source->GetOriginalURL()) {
- if (!security_callback_.is_null() &&
- !security_callback_.Run(source->GetURL(), true /*did redirect*/)) {
- error_occured_ = true;
- if (!pending_read_cb_.is_null()) {
- base::ResetAndReturn(&pending_read_cb_).Run(-1);
- }
- return;
- }
- }
-
- scoped_refptr<net::HttpResponseHeaders> headers =
- source->GetResponseHeaders();
- DCHECK(headers);
-
- if (!is_origin_safe_) {
- if (loader::CORSPreflight::CORSCheck(
- *headers, document_origin_.SerializedOrigin(),
- request_mode_ == loader::kCORSModeIncludeCredentials)) {
- is_origin_safe_ = true;
- } else {
- error_occured_ = true;
- if (!pending_read_cb_.is_null()) {
- base::ResetAndReturn(&pending_read_cb_).Run(-1);
- }
- return;
- }
- }
-
- uint64 first_byte_offset = 0;
-
- if (headers->response_code() == net::HTTP_PARTIAL_CONTENT) {
- int64 first_byte_position = -1;
- int64 last_byte_position = -1;
- int64 instance_length = -1;
- bool is_range_valid = headers && headers->GetContentRangeFor206(
- &first_byte_position,
- &last_byte_position, &instance_length);
- if (is_range_valid) {
- if (first_byte_position >= 0) {
- first_byte_offset = static_cast<uint64>(first_byte_position);
- }
- if (!total_size_of_resource_ && instance_length > 0) {
- total_size_of_resource_ = static_cast<uint64>(instance_length);
- }
- }
- }
-
- DCHECK_LE(first_byte_offset, last_request_offset_);
-
- if (first_byte_offset < last_request_offset_) {
- last_request_size_ += last_request_offset_ - first_byte_offset;
- last_request_offset_ = first_byte_offset;
- }
-}
-
-void FetcherBufferedDataSource::OnURLFetchDownloadProgress(
- const net::URLFetcher* source, int64_t current, int64_t total,
- int64_t current_network_bytes) {
- DCHECK(task_runner_->BelongsToCurrentThread());
- auto* download_data_writer =
- base::polymorphic_downcast<loader::URLFetcherStringWriter*>(
- source->GetResponseWriter());
- std::string downloaded_data;
- download_data_writer->GetAndResetData(&downloaded_data);
- size_t size = downloaded_data.size();
- if (size == 0) {
- return;
- }
- const uint8* data = reinterpret_cast<const uint8*>(downloaded_data.data());
- base::AutoLock auto_lock(lock_);
-
- if (fetcher_.get() != source || error_occured_) {
- return;
- }
-
- size = static_cast<size_t>(std::min<uint64>(size, last_request_size_));
-
- if (size == 0 || size > buffer_.GetMaxCapacity()) {
- // The server side doesn't support range request. Delete the fetcher to
- // stop the current request.
- LOG(ERROR)
- << "FetcherBufferedDataSource::OnURLFetchDownloadProgress: server "
- << "doesn't support range requests (e.g. Python SimpleHTTPServer). "
- << "Please use a server that supports range requests (e.g. Flask).";
- error_occured_ = true;
- fetcher_.reset();
- ProcessPendingRead_Locked();
- UpdateDownloadingStatus(/* is_downloading = */ false);
- return;
- }
-
- // Because we can only append data into the buffer_. We just check if the
- // position of the first byte of the newly received data is overlapped with
- // the range of the buffer_. If not, we can discard all data in the buffer_
- // as there is no way to represent a gap or to prepend data.
- if (last_request_offset_ < buffer_offset_ ||
- last_request_offset_ > buffer_offset_ + buffer_.GetLength()) {
- buffer_.Clear();
- buffer_offset_ = last_request_offset_;
- }
-
- // If there is any overlapping, modify data/size accordingly.
- if (buffer_offset_ + buffer_.GetLength() > last_request_offset_) {
- uint64 difference =
- buffer_offset_ + buffer_.GetLength() - last_request_offset_;
- difference = std::min<uint64>(difference, size);
- data += difference;
- size -= difference;
- last_request_offset_ += difference;
- }
-
- // If we are overflow, remove some data from the front of the buffer_.
- if (buffer_.GetLength() + size > buffer_.GetMaxCapacity()) {
- size_t bytes_skipped;
- buffer_.Skip(buffer_.GetLength() + size - buffer_.GetMaxCapacity(),
- &bytes_skipped);
- // "+ 0" converts buffer_.GetMaxCapacity() into a r-value to avoid link
- // error.
- DCHECK_EQ(buffer_.GetLength() + size, buffer_.GetMaxCapacity() + 0);
- buffer_offset_ += bytes_skipped;
- }
-
- size_t bytes_written;
- bool result = buffer_.Write(data, size, &bytes_written);
- DCHECK(result);
- DCHECK_EQ(size, bytes_written);
-
- last_request_offset_ += bytes_written;
- last_request_size_ -= bytes_written;
-
- ProcessPendingRead_Locked();
-}
-
-void FetcherBufferedDataSource::OnURLFetchComplete(
- const net::URLFetcher* source) {
- DCHECK(task_runner_->BelongsToCurrentThread());
-
- base::AutoLock auto_lock(lock_);
-
- if (fetcher_.get() != source || error_occured_) {
- return;
- }
-
- const net::URLRequestStatus& status = source->GetStatus();
- if (status.is_success()) {
- if (!total_size_of_resource_ && last_request_size_ != 0) {
- total_size_of_resource_ = buffer_offset_ + buffer_.GetLength();
- }
- } else {
- LOG(ERROR)
- << "FetcherBufferedDataSource::OnURLFetchComplete called with error "
- << status.error();
- error_occured_ = true;
- buffer_.Clear();
- }
-
- fetcher_.reset();
-
- ProcessPendingRead_Locked();
- UpdateDownloadingStatus(/* is_downloading = */ false);
-}
-
-void FetcherBufferedDataSource::CreateNewFetcher() {
- DCHECK(task_runner_->BelongsToCurrentThread());
-
- base::AutoLock auto_lock(lock_);
-
- DCHECK(!fetcher_);
- fetcher_to_be_destroyed_.reset();
-
- DCHECK_GE(static_cast<int64>(last_request_offset_), 0);
- DCHECK_GE(static_cast<int64>(last_request_size_), 0);
-
- // Check if there was an error or if the request is blocked by csp.
- if (error_occured_ ||
- (!security_callback_.is_null() && !security_callback_.Run(url_, false))) {
- error_occured_ = true;
- if (!pending_read_cb_.is_null()) {
- base::ResetAndReturn(&pending_read_cb_).Run(-1);
- }
- UpdateDownloadingStatus(/* is_downloading = */ false);
- return;
- }
-
- fetcher_ =
- std::move(net::URLFetcher::Create(url_, net::URLFetcher::GET, this));
- fetcher_->SetRequestContext(
- network_module_->url_request_context_getter().get());
- std::unique_ptr<loader::URLFetcherStringWriter> download_data_writer(
- new loader::URLFetcherStringWriter());
- fetcher_->SaveResponseWithWriter(std::move(download_data_writer));
-
- std::string range_request =
- "Range: bytes=" + base::NumberToString(last_request_offset_) + "-" +
- base::NumberToString(last_request_offset_ + last_request_size_ - 1);
- fetcher_->AddExtraRequestHeader(range_request);
- if (!is_origin_safe_) {
- if (request_mode_ != loader::kNoCORSMode &&
- document_origin_ != loader::Origin(url_) && !url_.SchemeIs("data")) {
- fetcher_->AddExtraRequestHeader("Origin:" +
- document_origin_.SerializedOrigin());
- } else {
- is_origin_safe_ = true;
- }
- }
- fetcher_->Start();
- UpdateDownloadingStatus(/* is_downloading = */ true);
-}
-
-void FetcherBufferedDataSource::UpdateDownloadingStatus(bool is_downloading) {
- DCHECK(task_runner_->BelongsToCurrentThread());
-
- if (is_downloading_ == is_downloading) {
- return;
- }
-
- is_downloading_ = is_downloading;
- if (!downloading_status_cb_.is_null()) {
- downloading_status_cb_.Run(is_downloading_);
- }
-}
-
-void FetcherBufferedDataSource::Read_Locked(uint64 position, size_t size,
- uint8* data,
- const ReadCB& read_cb) {
- lock_.AssertAcquired();
-
- DCHECK(data);
- DCHECK(!read_cb.is_null());
- DCHECK(pending_read_cb_.is_null()); // One read operation at the same time.
-
- if (error_occured_) {
- read_cb.Run(-1);
- return;
- }
-
- // Clamp the request to valid range of the resource if its size is known.
- if (total_size_of_resource_) {
- position = std::min(position, total_size_of_resource_.value());
- if (size + position > total_size_of_resource_.value()) {
- size = static_cast<size_t>(total_size_of_resource_.value() - position);
- }
- }
-
- last_read_position_ = position;
-
- if (size == 0) {
- read_cb.Run(0);
- return;
- }
-
- // Fulfill the read request now if we have the data.
- if (position >= buffer_offset_ &&
- position + size <= buffer_offset_ + buffer_.GetLength()) {
- // All data is available
- size_t bytes_peeked;
- buffer_.Peek(data, size, static_cast<size_t>(position - buffer_offset_),
- &bytes_peeked);
- DCHECK_EQ(bytes_peeked, size);
- DCHECK_GE(static_cast<int>(bytes_peeked), 0);
- read_cb.Run(static_cast<int>(bytes_peeked));
- // If we have a large buffer size, it could be ideal if we can keep sending
- // small requests when the read offset is far from the beginning of the
- // buffer. However as the ProgressiveDemuxer will cache many frames and the
- // buffer we are using is usually small, we will just avoid sending requests
- // here to make code simple.
- return;
- }
-
- // Save the read request as we are unable to fulfill it now.
- pending_read_cb_ = read_cb;
- pending_read_position_ = position;
- pending_read_size_ = size;
- pending_read_data_ = data;
-
- // Combine the range of the buffer and any ongoing fetch to see if the read is
- // overlapped with it.
- if (fetcher_) {
- uint64 begin = last_request_offset_;
- uint64 end = last_request_offset_ + last_request_size_;
- if (last_request_offset_ >= buffer_offset_ &&
- last_request_offset_ <= buffer_offset_ + buffer_.GetLength()) {
- begin = buffer_offset_;
- }
- if (position >= begin && position < end) {
- // The read is overlapped with existing request, just wait.
- return;
- }
- }
-
- // Now we have to issue a new fetch and we no longer care about the range of
- // the current fetch in progress if there is any. Ideally the request range
- // starts at |last_read_position_ - kBackwardBytes| with length of
- // buffer_.GetMaxCapacity().
- if (last_read_position_ > kBackwardBytes) {
- last_request_offset_ = last_read_position_ - kBackwardBytes;
- } else {
- last_request_offset_ = 0;
- }
-
- size_t required_size = static_cast<size_t>(
- last_read_position_ - last_request_offset_ + pending_read_size_);
- if (required_size > buffer_.GetMaxCapacity()) {
- // The capacity of the current buffer is not large enough to hold the
- // pending read.
- size_t new_capacity =
- std::max<size_t>(buffer_.GetMaxCapacity() * 2, required_size);
- buffer_.IncreaseMaxCapacityTo(new_capacity);
- }
-
- last_request_size_ = buffer_.GetMaxCapacity();
-
- if (last_request_offset_ >= buffer_offset_ &&
- last_request_offset_ <= buffer_offset_ + buffer_.GetLength()) {
- // Part of the Read() can be fulfilled by the current buffer and current
- // request but cannot be fulfilled by the current request but we have to
- // send another request to retrieve the rest.
- last_request_size_ -=
- buffer_offset_ + buffer_.GetLength() - last_request_offset_;
- last_request_offset_ = buffer_offset_ + buffer_.GetLength();
- }
-
- if (cancelable_create_fetcher_closure_) {
- cancelable_create_fetcher_closure_->Cancel();
- }
- base::Closure create_fetcher_closure = base::Bind(
- &FetcherBufferedDataSource::CreateNewFetcher, base::Unretained(this));
- cancelable_create_fetcher_closure_ =
- new CancelableClosure(create_fetcher_closure);
- fetcher_to_be_destroyed_.reset(fetcher_.release());
- task_runner_->PostTask(FROM_HERE,
- cancelable_create_fetcher_closure_->AsClosure());
-}
-
-void FetcherBufferedDataSource::ProcessPendingRead_Locked() {
- lock_.AssertAcquired();
- if (!pending_read_cb_.is_null()) {
- Read_Locked(pending_read_position_, pending_read_size_, pending_read_data_,
- base::ResetAndReturn(&pending_read_cb_));
- }
-}
-
-FetcherBufferedDataSource::CancelableClosure::CancelableClosure(
- const base::Closure& closure)
- : closure_(closure) {
- DCHECK(!closure.is_null());
-}
-
-void FetcherBufferedDataSource::CancelableClosure::Cancel() {
- base::AutoLock auto_lock(lock_);
- closure_.Reset();
-}
-
-base::Closure FetcherBufferedDataSource::CancelableClosure::AsClosure() {
- return base::Bind(&CancelableClosure::Call, this);
-}
-
-void FetcherBufferedDataSource::CancelableClosure::Call() {
- base::AutoLock auto_lock(lock_);
- // closure_.Run() has to be called when the lock is acquired to avoid race
- // condition.
- if (!closure_.is_null()) {
- base::ResetAndReturn(&closure_).Run();
- }
-}
-
-} // namespace media
-} // namespace cobalt
diff --git a/cobalt/media/fetcher_buffered_data_source.h b/cobalt/media/fetcher_buffered_data_source.h
deleted file mode 100644
index fb589a1..0000000
--- a/cobalt/media/fetcher_buffered_data_source.h
+++ /dev/null
@@ -1,163 +0,0 @@
-// Copyright 2015 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef COBALT_MEDIA_FETCHER_BUFFERED_DATA_SOURCE_H_
-#define COBALT_MEDIA_FETCHER_BUFFERED_DATA_SOURCE_H_
-
-#include <memory>
-#include <string>
-
-#include "base/callback.h"
-#include "base/compiler_specific.h"
-#include "base/memory/ref_counted.h"
-#include "base/message_loop/message_loop.h"
-#include "base/optional.h"
-#include "base/synchronization/lock.h"
-#include "cobalt/base/circular_buffer_shell.h"
-#include "cobalt/csp/content_security_policy.h"
-#include "cobalt/loader/fetcher.h"
-#include "cobalt/loader/origin.h"
-#include "cobalt/loader/url_fetcher_string_writer.h"
-#include "cobalt/media/player/buffered_data_source.h"
-#include "cobalt/network/network_module.h"
-#include "net/url_request/url_fetcher.h"
-#include "net/url_request/url_fetcher_delegate.h"
-#include "url/gurl.h"
-
-namespace cobalt {
-namespace media {
-
-// TODO: This class requires a large block of memory.
-
-// A BufferedDataSource based on net::URLFetcher that can be used to retrieve
-// progressive videos from both local and network sources.
-// It uses a fixed size circular buffer so we may not be able to store all data
-// into this buffer. It is based on the following assumptions/strategies:
-// 1. It assumes that the buffer is large enough to fulfill one Read() request.
-// So any outstanding request only requires at most one request.
-// 2. It will do one initial request to retrieve the target resource. If the
-// whole resource can be fit into the buffer, no further request will be
-// fired.
-// 3. If the resource doesn't fit into the buffer. The class will store
-// kBackwardBytes bytes before the last read offset(LRO) and kForwardBytes
-// after LRO. Note that if LRO is less than kBackwardBytes, then data starts
-// from offset 0 will be cached.
-// 4. It assumes that the server supports range request.
-// 5. All data stored are continuous.
-class FetcherBufferedDataSource : public BufferedDataSource,
- private net::URLFetcherDelegate {
- public:
- static const int64 kInvalidSize = -1;
-
- // Because the Fetchers have to be created and destroyed on the same thread,
- // we use the task_runner passed in to create and destroy Fetchers.
- FetcherBufferedDataSource(
- const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
- const GURL& url, const csp::SecurityCallback& security_callback,
- network::NetworkModule* network_module, loader::RequestMode request_mode,
- loader::Origin origin);
- ~FetcherBufferedDataSource() override;
-
- // DataSource methods.
- void Read(int64 position, int size, uint8* data,
- const ReadCB& read_cb) override;
- void Stop() override;
- bool GetSize(int64* size_out) override;
- bool IsStreaming() override { return false; }
- void SetBitrate(int bitrate) override {}
-
- // BufferedDataSource methods.
- void SetDownloadingStatusCB(
- const DownloadingStatusCB& downloading_status_cb) override;
-
- private:
- class CancelableClosure
- : public base::RefCountedThreadSafe<CancelableClosure> {
- public:
- explicit CancelableClosure(const base::Closure& closure);
-
- void Cancel();
- base::Closure AsClosure();
-
- private:
- void Call();
-
- base::Lock lock_;
- base::Closure closure_;
- };
-
- // net::URLFetcherDelegate methods
- void OnURLFetchResponseStarted(const net::URLFetcher* source) override;
- void OnURLFetchDownloadProgress(const net::URLFetcher* source,
- int64_t current, int64_t total,
- int64_t current_network_bytes) override;
- void OnURLFetchComplete(const net::URLFetcher* source) override;
-
- void CreateNewFetcher();
- void UpdateDownloadingStatus(bool is_downloading);
- void Read_Locked(uint64 position, size_t size, uint8* data,
- const ReadCB& read_cb);
- void ProcessPendingRead_Locked();
- void TryToSendRequest_Locked();
-
- base::Lock lock_;
- scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
- GURL url_;
- network::NetworkModule* network_module_;
- std::unique_ptr<net::URLFetcher> fetcher_;
-
- bool is_downloading_;
- DownloadingStatusCB downloading_status_cb_;
-
- // |fetcher_| has to be destroyed on the thread it's created. So it cannot be
- // safely destroyed inside Read_Locked(). Save |fetcher_| into
- // |fetcher_to_be_destroyed_| to ensure that it is properly destroyed either
- // inside CreateNewFetcher() or in the dtor while still allow |fetcher_| to be
- // set to NULL to invalidate outstanding read.
- std::unique_ptr<net::URLFetcher> fetcher_to_be_destroyed_;
-
- // |buffer_| stores a continuous block of data of target resource starts from
- // |buffer_offset_|. When the target resource can be fit into |buffer_|,
- // |buffer_offset_| will always be 0.
- base::CircularBufferShell buffer_;
- uint64 buffer_offset_;
-
- base::Optional<uint64> total_size_of_resource_;
- bool error_occured_;
-
- uint64 last_request_offset_;
- uint64 last_request_size_;
-
- // This is usually the same as pending_read_position_. Represent it
- // explicitly using a separate variable.
- uint64 last_read_position_;
-
- ReadCB pending_read_cb_;
- uint64 pending_read_position_;
- size_t pending_read_size_;
- uint8* pending_read_data_;
-
- csp::SecurityCallback security_callback_;
- scoped_refptr<CancelableClosure> cancelable_create_fetcher_closure_;
-
- loader::RequestMode request_mode_;
- loader::Origin document_origin_;
- // True if the origin is allowed to fetch resource data.
- bool is_origin_safe_;
-};
-
-} // namespace media
-} // namespace cobalt
-
-#endif // COBALT_MEDIA_FETCHER_BUFFERED_DATA_SOURCE_H_
diff --git a/cobalt/media/file_data_source.cc b/cobalt/media/file_data_source.cc
new file mode 100644
index 0000000..81eb1ae
--- /dev/null
+++ b/cobalt/media/file_data_source.cc
@@ -0,0 +1,89 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/media/file_data_source.h"
+
+#include <string>
+#include <utility>
+
+#include "base/base_paths.h"
+#include "base/files/file_path.h"
+#include "base/path_service.h"
+#include "cobalt/base/cobalt_paths.h"
+
+namespace cobalt {
+namespace media {
+namespace {
+
+using base::File;
+using base::FilePath;
+using base::PathService;
+
+File OpenFile(const FilePath& file_path) {
+ int path_keys[] = {paths::DIR_COBALT_WEB_ROOT, base::DIR_TEST_DATA};
+
+ for (auto path_key : path_keys) {
+ FilePath root_path;
+ bool result = PathService::Get(path_key, &root_path);
+ SB_DCHECK(result);
+
+ File file(root_path.Append(file_path), File::FLAG_OPEN | File::FLAG_READ);
+ if (file.IsValid()) {
+ return std::move(file);
+ }
+ }
+
+ return File();
+}
+
+} // namespace
+
+FileDataSource::FileDataSource(const GURL& file_url) {
+ DCHECK(file_url.is_valid());
+ DCHECK(file_url.SchemeIsFile());
+
+ std::string path = file_url.path();
+ if (path.empty() || path[0] != '/') {
+ return;
+ }
+
+ file_ = std::move(OpenFile(base::FilePath(path.substr(1))));
+}
+
+void FileDataSource::Read(int64 position, int size, uint8* data,
+ const ReadCB& read_cb) {
+ DCHECK_GE(position, 0);
+ DCHECK_GE(size, 0);
+
+ if (!file_.IsValid()) {
+ read_cb.Run(kReadError);
+ return;
+ }
+
+ auto bytes_read = file_.Read(position, reinterpret_cast<char*>(data), size);
+ if (bytes_read == size) {
+ read_cb.Run(bytes_read);
+ } else {
+ read_cb.Run(kReadError);
+ }
+}
+
+bool FileDataSource::GetSize(int64* size_out) {
+ *size_out = file_.IsValid() ? file_.GetLength() : kInvalidSize;
+
+ return *size_out >= 0;
+}
+
+} // namespace media
+} // namespace cobalt
diff --git a/cobalt/media/file_data_source.h b/cobalt/media/file_data_source.h
new file mode 100644
index 0000000..dab3bcd
--- /dev/null
+++ b/cobalt/media/file_data_source.h
@@ -0,0 +1,53 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_MEDIA_FILE_DATA_SOURCE_H_
+#define COBALT_MEDIA_FILE_DATA_SOURCE_H_
+
+#include "base/files/file.h"
+#include "cobalt/media/base/data_source.h"
+#include "url/gurl.h"
+
+namespace cobalt {
+namespace media {
+
+// A file based DataSource used to retrieve progressive videos from local files.
+// The class is for testing purposes only, and shouldn't be used in production
+// environment.
+// Its member functions can be called from multiple threads. However, this
+// class doesn't synchronize the calls. It's the responsibility of its user to
+// ensure that such calls are synchronized.
+class FileDataSource : public DataSource {
+ public:
+ static constexpr int64 kInvalidSize = -1;
+
+ explicit FileDataSource(const GURL& file_url);
+
+ // DataSource methods.
+ void Read(int64 position, int size, uint8* data,
+ const ReadCB& read_cb) override;
+ void Stop() override {}
+ void Abort() override {}
+ bool GetSize(int64* size_out) override;
+ void SetDownloadingStatusCB(
+ const DownloadingStatusCB& downloading_status_cb) override {}
+
+ private:
+ base::File file_;
+};
+
+} // namespace media
+} // namespace cobalt
+
+#endif // COBALT_MEDIA_FILE_DATA_SOURCE_H_
diff --git a/cobalt/media/file_data_source_test.cc b/cobalt/media/file_data_source_test.cc
new file mode 100644
index 0000000..4fe7fe7
--- /dev/null
+++ b/cobalt/media/file_data_source_test.cc
@@ -0,0 +1,82 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/media/file_data_source.h"
+
+#include <string.h>
+
+#include "base/bind.h"
+#include "base/files/file_path.h"
+#include "base/test/scoped_task_environment.h"
+#include "starboard/common/log.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace cobalt {
+namespace media {
+
+void OnReadFinished(int* bytes_read_out, int bytes_read_in) {
+ SB_CHECK(bytes_read_out);
+ *bytes_read_out = bytes_read_in;
+}
+
+TEST(FileDataSourceTest, SunnyDay) {
+ base::test::ScopedTaskEnvironment scoped_task_environment_;
+ FileDataSource data_source(
+ GURL("file:///cobalt/media/testing/data/"
+ "progressive_aac_44100_stereo_h264_1280_720.mp4"));
+
+ int64 file_size = -1;
+ ASSERT_TRUE(data_source.GetSize(&file_size));
+ ASSERT_GT(file_size, 0);
+
+ uint8 buffer[1024];
+ int bytes_read = 0;
+
+ // Read from the beginning
+ data_source.Read(0, 1024, buffer, base::Bind(OnReadFinished, &bytes_read));
+ scoped_task_environment_.RunUntilIdle();
+ EXPECT_EQ(bytes_read, 1024);
+ EXPECT_EQ(memcmp(buffer + 4, "ftyp", 4), 0);
+
+ // Read from the end
+ data_source.Read(file_size - 1024, 1024, buffer,
+ base::Bind(OnReadFinished, &bytes_read));
+ scoped_task_environment_.RunUntilIdle();
+ EXPECT_EQ(bytes_read, 1024);
+
+ // Read beyond the end
+ data_source.Read(file_size - 512, 1024, buffer,
+ base::Bind(OnReadFinished, &bytes_read));
+ scoped_task_environment_.RunUntilIdle();
+ EXPECT_EQ(bytes_read, FileDataSource::kReadError);
+}
+
+TEST(FileDataSourceTest, RainyDay) {
+ base::test::ScopedTaskEnvironment scoped_task_environment_;
+ FileDataSource data_source(
+ GURL("file:///cobalt/media/testing/data/do_not_exist.invalid"));
+
+ int64 size = -1;
+ ASSERT_FALSE(data_source.GetSize(&size));
+
+ uint8 buffer[1024];
+ int bytes_read = 0;
+ data_source.Read(0, 1024, buffer, base::Bind(OnReadFinished, &bytes_read));
+
+ scoped_task_environment_.RunUntilIdle();
+ EXPECT_EQ(bytes_read, FileDataSource::kReadError);
+}
+
+} // namespace media
+} // namespace cobalt
diff --git a/cobalt/media/media_module.cc b/cobalt/media/media_module.cc
index 11bcbd7..6159298 100644
--- a/cobalt/media/media_module.cc
+++ b/cobalt/media/media_module.cc
@@ -183,6 +183,16 @@
} // namespace
+bool MediaModule::SetConfiguration(const std::string& name, int32 value) {
+ if (name == "EnableBatchedSampleWrite") {
+ allow_batched_sample_write_ = value;
+ LOG(INFO) << (allow_batched_sample_write_ ? "Enabling" : "Disabling")
+ << " batched sample write.";
+ return true;
+ }
+ return false;
+}
+
std::unique_ptr<WebMediaPlayer> MediaModule::CreateWebMediaPlayer(
WebMediaPlayerClient* client) {
TRACK_MEMORY_SCOPE("Media");
@@ -192,10 +202,11 @@
}
return std::unique_ptr<WebMediaPlayer>(new media::WebMediaPlayerImpl(
- window,
+ sbplayer_interface_.get(), window,
base::Bind(&MediaModule::GetSbDecodeTargetGraphicsContextProvider,
base::Unretained(this)),
- client, this, options_.allow_resume_after_suspend, &media_log_));
+ client, this, options_.allow_resume_after_suspend,
+ allow_batched_sample_write_, &media_log_));
}
void MediaModule::Suspend() {
diff --git a/cobalt/media/media_module.h b/cobalt/media/media_module.h
index e79db38..6ca47b8 100644
--- a/cobalt/media/media_module.h
+++ b/cobalt/media/media_module.h
@@ -25,6 +25,7 @@
#include "base/memory/ref_counted.h"
#include "base/optional.h"
#include "cobalt/math/size.h"
+#include "cobalt/media/base/sbplayer_interface.h"
#include "cobalt/media/can_play_type_handler.h"
#include "cobalt/media/decoder_buffer_allocator.h"
#include "cobalt/media/player/web_media_player_delegate.h"
@@ -57,10 +58,16 @@
MediaModule(system_window::SystemWindow* system_window,
render_tree::ResourceProvider* resource_provider,
const Options& options = Options())
- : options_(options),
+ : sbplayer_interface_(new DefaultSbPlayerInterface),
+ options_(options),
system_window_(system_window),
resource_provider_(resource_provider) {}
+ // Returns true when the setting is set successfully or if the setting has
+ // already been set to the expected value. Returns false when the setting is
+ // invalid or not set to the expected value.
+ bool SetConfiguration(const std::string& name, int32 value);
+
const DecoderBufferAllocator* GetDecoderBufferAllocator() const {
return &decoder_buffer_allocator_;
}
@@ -101,6 +108,7 @@
// paused by us.
typedef std::map<WebMediaPlayer*, bool> Players;
+ std::unique_ptr<SbPlayerInterface> sbplayer_interface_;
const Options options_;
system_window::SystemWindow* system_window_;
cobalt::render_tree::ResourceProvider* resource_provider_;
@@ -113,6 +121,8 @@
Players players_;
bool suspended_ = false;
+ bool allow_batched_sample_write_ = false;
+
DecoderBufferAllocator decoder_buffer_allocator_;
};
diff --git a/cobalt/media/player/buffered_data_source.h b/cobalt/media/player/buffered_data_source.h
deleted file mode 100644
index 03267e2..0000000
--- a/cobalt/media/player/buffered_data_source.h
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright 2015 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef COBALT_MEDIA_PLAYER_BUFFERED_DATA_SOURCE_H_
-#define COBALT_MEDIA_PLAYER_BUFFERED_DATA_SOURCE_H_
-
-#include "base/basictypes.h"
-#include "base/callback.h"
-#include "base/message_loop/message_loop.h"
-#include "cobalt/media/base/data_source.h"
-#include "starboard/types.h"
-#include "url/gurl.h"
-
-namespace cobalt {
-namespace media {
-
-enum Preload {
- kPreloadNone,
- kPreloadMetaData,
- kPreloadAuto,
-};
-
-// TODO: Investigate if we still need BufferedDataSource.
-class BufferedDataSource : public DataSource {
- public:
- typedef base::Callback<void(bool)> DownloadingStatusCB;
-
- virtual void SetDownloadingStatusCB(
- const DownloadingStatusCB& downloading_status_cb) {
- }
- virtual void SetPreload(Preload preload) {
- }
- virtual bool HasSingleOrigin() { return true; }
- virtual bool DidPassCORSAccessCheck() const { return true; }
- virtual void Abort() {}
-};
-
-} // namespace media
-} // namespace cobalt
-
-#endif // COBALT_MEDIA_PLAYER_BUFFERED_DATA_SOURCE_H_
diff --git a/cobalt/media/player/web_media_player.h b/cobalt/media/player/web_media_player.h
index d3b7abd..1322473 100644
--- a/cobalt/media/player/web_media_player.h
+++ b/cobalt/media/player/web_media_player.h
@@ -19,8 +19,9 @@
#include "base/memory/ref_counted.h"
#include "base/memory/weak_ptr.h"
#include "base/time/time.h"
-#include "cobalt/media/base/video_frame_provider.h"
-#include "cobalt/media/player/buffered_data_source.h"
+#include "cobalt/media/base/data_source.h"
+#include "cobalt/media/base/decode_target_provider.h"
+#include "starboard/window.h"
#include "url/gurl.h"
namespace media {
@@ -100,8 +101,8 @@
virtual void LoadUrl(const GURL& url) = 0;
#endif // SB_HAS(PLAYER_WITH_URL)
virtual void LoadMediaSource() = 0;
- virtual void LoadProgressive(
- const GURL& url, std::unique_ptr<BufferedDataSource> data_source) = 0;
+ virtual void LoadProgressive(const GURL& url,
+ std::unique_ptr<DataSource> data_source) = 0;
virtual void CancelLoad() = 0;
@@ -148,14 +149,11 @@
virtual bool DidLoadingProgress() const = 0;
- virtual bool HasSingleSecurityOrigin() const = 0;
- virtual bool DidPassCORSAccessCheck() const = 0;
-
virtual float MediaTimeForTimeValue(float timeValue) const = 0;
virtual PlayerStatistics GetStatistics() const = 0;
- virtual scoped_refptr<VideoFrameProvider> GetVideoFrameProvider() {
+ virtual scoped_refptr<DecodeTargetProvider> GetDecodeTargetProvider() {
return NULL;
}
diff --git a/cobalt/media/player/web_media_player_impl.cc b/cobalt/media/player/web_media_player_impl.cc
index 197a9a1..1844a6a 100644
--- a/cobalt/media/player/web_media_player_impl.cc
+++ b/cobalt/media/player/web_media_player_impl.cc
@@ -112,11 +112,12 @@
OnNeedKeyCB;
WebMediaPlayerImpl::WebMediaPlayerImpl(
- PipelineWindow window,
+ SbPlayerInterface* interface, PipelineWindow window,
const Pipeline::GetDecodeTargetGraphicsContextProviderFunc&
get_decode_target_graphics_context_provider_func,
WebMediaPlayerClient* client, WebMediaPlayerDelegate* delegate,
- bool allow_resume_after_suspend, ::media::MediaLog* const media_log)
+ bool allow_resume_after_suspend, bool allow_batched_sample_write,
+ ::media::MediaLog* const media_log)
: pipeline_thread_("media_pipeline"),
network_state_(WebMediaPlayer::kNetworkStateEmpty),
ready_state_(WebMediaPlayer::kReadyStateHaveNothing),
@@ -124,6 +125,7 @@
client_(client),
delegate_(delegate),
allow_resume_after_suspend_(allow_resume_after_suspend),
+ allow_batched_sample_write_(allow_batched_sample_write),
proxy_(new WebMediaPlayerProxy(main_loop_->task_runner(), this)),
media_log_(media_log),
is_local_source_(false),
@@ -134,15 +136,16 @@
ON_INSTANCE_CREATED(WebMediaPlayerImpl);
- video_frame_provider_ = new VideoFrameProvider();
+ decode_target_provider_ = new DecodeTargetProvider();
media_log_->AddEvent<::media::MediaLogEvent::kWebMediaPlayerCreated>();
pipeline_thread_.Start();
- pipeline_ = Pipeline::Create(window, pipeline_thread_.task_runner(),
- get_decode_target_graphics_context_provider_func,
- allow_resume_after_suspend_, media_log_,
- video_frame_provider_.get());
+ pipeline_ =
+ Pipeline::Create(interface, window, pipeline_thread_.task_runner(),
+ get_decode_target_graphics_context_provider_func,
+ allow_resume_after_suspend_, allow_batched_sample_write_,
+ media_log_, decode_target_provider_.get());
// Also we want to be notified of |main_loop_| destruction.
main_loop_->AddDestructionObserver(this);
@@ -259,7 +262,7 @@
}
void WebMediaPlayerImpl::LoadProgressive(
- const GURL& url, std::unique_ptr<BufferedDataSource> data_source) {
+ const GURL& url, std::unique_ptr<DataSource> data_source) {
TRACE_EVENT0("cobalt::media", "WebMediaPlayerImpl::LoadProgressive");
DCHECK_EQ(main_loop_, base::MessageLoop::current());
@@ -513,9 +516,6 @@
float WebMediaPlayerImpl::GetMaxTimeSeekable() const {
DCHECK_EQ(main_loop_, base::MessageLoop::current());
- // We don't support seeking in streaming media.
- if (proxy_ && proxy_->data_source() && proxy_->data_source()->IsStreaming())
- return 0.0f;
return static_cast<float>(pipeline_->GetMediaDuration().InSecondsF());
}
@@ -534,15 +534,6 @@
return pipeline_->DidLoadingProgress();
}
-bool WebMediaPlayerImpl::HasSingleSecurityOrigin() const {
- if (proxy_) return proxy_->HasSingleOrigin();
- return true;
-}
-
-bool WebMediaPlayerImpl::DidPassCORSAccessCheck() const {
- return proxy_ && proxy_->DidPassCORSAccessCheck();
-}
-
float WebMediaPlayerImpl::MediaTimeForTimeValue(float timeValue) const {
return ConvertSecondsToTimestamp(timeValue).InSecondsF();
}
@@ -559,8 +550,9 @@
return statistics;
}
-scoped_refptr<VideoFrameProvider> WebMediaPlayerImpl::GetVideoFrameProvider() {
- return video_frame_provider_;
+scoped_refptr<DecodeTargetProvider>
+WebMediaPlayerImpl::GetDecodeTargetProvider() {
+ return decode_target_provider_;
}
WebMediaPlayerImpl::SetBoundsCB WebMediaPlayerImpl::GetSetBoundsCB() {
diff --git a/cobalt/media/player/web_media_player_impl.h b/cobalt/media/player/web_media_player_impl.h
index c53b434..b64f318 100644
--- a/cobalt/media/player/web_media_player_impl.h
+++ b/cobalt/media/player/web_media_player_impl.h
@@ -59,8 +59,9 @@
#include "base/threading/thread.h"
#include "base/time/time.h"
#include "cobalt/math/size.h"
+#include "cobalt/media/base/decode_target_provider.h"
#include "cobalt/media/base/pipeline.h"
-#include "cobalt/media/base/video_frame_provider.h"
+#include "cobalt/media/base/sbplayer_interface.h"
#include "cobalt/media/player/web_media_player.h"
#include "cobalt/media/player/web_media_player_delegate.h"
#include "third_party/chromium/media/base/demuxer.h"
@@ -102,12 +103,13 @@
// When calling this, the |audio_source_provider| and
// |audio_renderer_sink| arguments should be the same object.
- WebMediaPlayerImpl(PipelineWindow window,
+ WebMediaPlayerImpl(SbPlayerInterface* interface, PipelineWindow window,
const Pipeline::GetDecodeTargetGraphicsContextProviderFunc&
get_decode_target_graphics_context_provider_func,
WebMediaPlayerClient* client,
WebMediaPlayerDelegate* delegate,
bool allow_resume_after_suspend,
+ bool allow_batched_sample_write,
::media::MediaLog* const media_log);
~WebMediaPlayerImpl() override;
@@ -115,9 +117,8 @@
void LoadUrl(const GURL& url) override;
#endif // SB_HAS(PLAYER_WITH_URL)
void LoadMediaSource() override;
- void LoadProgressive(
- const GURL& url,
- std::unique_ptr<BufferedDataSource> data_source) override;
+ void LoadProgressive(const GURL& url,
+ std::unique_ptr<DataSource> data_source) override;
void CancelLoad() override;
@@ -166,14 +167,11 @@
bool DidLoadingProgress() const override;
- bool HasSingleSecurityOrigin() const override;
- bool DidPassCORSAccessCheck() const override;
-
float MediaTimeForTimeValue(float timeValue) const override;
PlayerStatistics GetStatistics() const override;
- scoped_refptr<VideoFrameProvider> GetVideoFrameProvider() override;
+ scoped_refptr<DecodeTargetProvider> GetDecodeTargetProvider() override;
SetBoundsCB GetSetBoundsCB() override;
@@ -292,7 +290,8 @@
WebMediaPlayerClient* const client_;
WebMediaPlayerDelegate* const delegate_;
const bool allow_resume_after_suspend_;
- scoped_refptr<VideoFrameProvider> video_frame_provider_;
+ const bool allow_batched_sample_write_;
+ scoped_refptr<DecodeTargetProvider> decode_target_provider_;
scoped_refptr<WebMediaPlayerProxy> proxy_;
diff --git a/cobalt/media/player/web_media_player_proxy.cc b/cobalt/media/player/web_media_player_proxy.cc
index 350d71a..57a6742 100644
--- a/cobalt/media/player/web_media_player_proxy.cc
+++ b/cobalt/media/player/web_media_player_proxy.cc
@@ -21,16 +21,10 @@
WebMediaPlayerProxy::~WebMediaPlayerProxy() { Detach(); }
-bool WebMediaPlayerProxy::HasSingleOrigin() {
+void WebMediaPlayerProxy::Detach() {
DCHECK(render_loop_->BelongsToCurrentThread());
- if (data_source_) return data_source_->HasSingleOrigin();
- return true;
-}
-
-bool WebMediaPlayerProxy::DidPassCORSAccessCheck() const {
- DCHECK(render_loop_->BelongsToCurrentThread());
- if (data_source_) return data_source_->DidPassCORSAccessCheck();
- return false;
+ webmediaplayer_ = NULL;
+ data_source_.reset();
}
void WebMediaPlayerProxy::AbortDataSource() {
@@ -38,11 +32,5 @@
if (data_source_) data_source_->Abort();
}
-void WebMediaPlayerProxy::Detach() {
- DCHECK(render_loop_->BelongsToCurrentThread());
- webmediaplayer_ = NULL;
- data_source_.reset();
-}
-
} // namespace media
} // namespace cobalt
diff --git a/cobalt/media/player/web_media_player_proxy.h b/cobalt/media/player/web_media_player_proxy.h
index bfcffdd..7436f0d 100644
--- a/cobalt/media/player/web_media_player_proxy.h
+++ b/cobalt/media/player/web_media_player_proxy.h
@@ -6,10 +6,11 @@
#define COBALT_MEDIA_PLAYER_WEB_MEDIA_PLAYER_PROXY_H_
#include <memory>
+#include <utility>
#include "base/memory/ref_counted.h"
#include "base/message_loop/message_loop.h"
-#include "cobalt/media/player/buffered_data_source.h"
+#include "cobalt/media/base/data_source.h"
namespace cobalt {
namespace media {
@@ -24,15 +25,12 @@
WebMediaPlayerProxy(
const scoped_refptr<base::SingleThreadTaskRunner>& render_loop,
WebMediaPlayerImpl* webmediaplayer);
- BufferedDataSource* data_source() { return data_source_.get(); }
- void set_data_source(std::unique_ptr<BufferedDataSource> data_source) {
+ DataSource* data_source() { return data_source_.get(); }
+ void set_data_source(std::unique_ptr<DataSource> data_source) {
data_source_ = std::move(data_source);
}
void Detach();
- bool HasSingleOrigin();
- bool DidPassCORSAccessCheck() const;
-
void AbortDataSource();
private:
@@ -43,7 +41,7 @@
scoped_refptr<base::SingleThreadTaskRunner> render_loop_;
WebMediaPlayerImpl* webmediaplayer_;
- std::unique_ptr<BufferedDataSource> data_source_;
+ std::unique_ptr<DataSource> data_source_;
DISALLOW_IMPLICIT_CONSTRUCTORS(WebMediaPlayerProxy);
};
diff --git a/cobalt/media/progressive/demuxer_extension_wrapper.cc b/cobalt/media/progressive/demuxer_extension_wrapper.cc
index f4c8139..a743b32 100644
--- a/cobalt/media/progressive/demuxer_extension_wrapper.cc
+++ b/cobalt/media/progressive/demuxer_extension_wrapper.cc
@@ -189,14 +189,15 @@
<< "Audio config is not valid!";
}
-void DemuxerExtensionStream::Read(ReadCB read_cb) {
+void DemuxerExtensionStream::Read(int max_number_of_buffers_to_read,
+ ReadCB read_cb) {
DCHECK(!read_cb.is_null());
base::AutoLock auto_lock(lock_);
if (stopped_) {
LOG(INFO) << "Already stopped.";
- std::move(read_cb).Run(
- DemuxerStream::kOk,
- scoped_refptr<DecoderBuffer>(DecoderBuffer::CreateEOSBuffer()));
+ std::vector<scoped_refptr<DecoderBuffer>> buffers;
+ buffers.push_back(std::move(DecoderBuffer::CreateEOSBuffer()));
+ std::move(read_cb).Run(DemuxerStream::kOk, buffers);
return;
}
@@ -216,7 +217,9 @@
buffer_queue_.pop_front();
}
- std::move(read_cb).Run(DemuxerStream::kOk, buffer);
+ std::vector<scoped_refptr<DecoderBuffer>> buffers;
+ buffers.push_back(std::move(buffer));
+ std::move(read_cb).Run(DemuxerStream::kOk, buffers);
}
AudioDecoderConfig DemuxerExtensionStream::audio_decoder_config() {
@@ -277,7 +280,9 @@
CHECK_EQ(buffer_queue_.size(), 0);
ReadCB read_cb(std::move(read_queue_.front()));
read_queue_.pop_front();
- std::move(read_cb).Run(DemuxerStream::kOk, std::move(buffer));
+ std::vector<scoped_refptr<DecoderBuffer>> buffers;
+ buffers.push_back(std::move(buffer));
+ std::move(read_cb).Run(DemuxerStream::kOk, buffers);
}
void DemuxerExtensionStream::FlushBuffers() {
@@ -296,9 +301,9 @@
last_buffer_timestamp_ = ::media::kNoTimestamp;
// Fulfill any pending callbacks with EOS buffers set to end timestamp.
for (auto& read_cb : read_queue_) {
- std::move(read_cb).Run(
- DemuxerStream::kOk,
- scoped_refptr<DecoderBuffer>(DecoderBuffer::CreateEOSBuffer()));
+ std::vector<scoped_refptr<DecoderBuffer>> buffers;
+ buffers.push_back(std::move(DecoderBuffer::CreateEOSBuffer()));
+ std::move(read_cb).Run(DemuxerStream::kOk, buffers);
}
read_queue_.clear();
stopped_ = true;
diff --git a/cobalt/media/progressive/demuxer_extension_wrapper.h b/cobalt/media/progressive/demuxer_extension_wrapper.h
index 138b922..cce7aa4 100644
--- a/cobalt/media/progressive/demuxer_extension_wrapper.h
+++ b/cobalt/media/progressive/demuxer_extension_wrapper.h
@@ -69,7 +69,7 @@
size_t GetTotalBufferSize() const;
// DemuxerStream implementation:
- void Read(ReadCB read_cb) override;
+ void Read(int max_number_of_buffers_to_read, ReadCB read_cb) override;
::media::AudioDecoderConfig audio_decoder_config() override;
::media::VideoDecoderConfig video_decoder_config() override;
Type type() const override;
diff --git a/cobalt/media/progressive/demuxer_extension_wrapper_test.cc b/cobalt/media/progressive/demuxer_extension_wrapper_test.cc
index 3aae16a..48250b6 100644
--- a/cobalt/media/progressive/demuxer_extension_wrapper_test.cc
+++ b/cobalt/media/progressive/demuxer_extension_wrapper_test.cc
@@ -38,6 +38,7 @@
using ::testing::_;
using ::testing::AtMost;
using ::testing::ElementsAreArray;
+using ::testing::ElementsAre;
using ::testing::ExplainMatchResult;
using ::testing::Invoke;
using ::testing::InvokeWithoutArgs;
@@ -97,8 +98,8 @@
MOCK_METHOD0(Stop, void());
MOCK_METHOD0(Abort, void());
MOCK_METHOD1(GetSize, bool(int64_t* size_out));
- MOCK_METHOD0(IsStreaming, bool());
- MOCK_METHOD1(SetBitrate, void(int bitrate));
+ MOCK_METHOD1(SetDownloadingStatusCB,
+ void(const DownloadingStatusCB& downloading_status_cb));
};
// Mock class for receiving calls to the Cobalt Extension demuxer. Based on the
@@ -548,14 +549,17 @@
: streams[1];
base::MockCallback<base::OnceCallback<void(
- ::media::DemuxerStream::Status, scoped_refptr<::media::DecoderBuffer>)>>
+ ::media::DemuxerStream::Status,
+ const std::vector<scoped_refptr<::media::DecoderBuffer>>&)>>
read_cb;
base::WaitableEvent read_done;
+ std::vector<std::vector<uint8_t>> buffers;
+ buffers.push_back(buffer_data);
EXPECT_CALL(read_cb, Run(::media::DemuxerStream::kOk,
- Pointee(BufferHasData(buffer_data))))
+ ElementsAre(Pointee(BufferHasData(buffer_data)))))
.WillOnce(InvokeWithoutArgs([&read_done]() { read_done.Signal(); }));
- audio_stream->Read(read_cb.Get());
+ audio_stream->Read(1, read_cb.Get());
EXPECT_TRUE(WaitForEvent(read_done));
}
@@ -685,14 +689,17 @@
: streams[1];
base::MockCallback<base::OnceCallback<void(
- ::media::DemuxerStream::Status, scoped_refptr<::media::DecoderBuffer>)>>
+ ::media::DemuxerStream::Status,
+ const std::vector<scoped_refptr<::media::DecoderBuffer>>&)>>
read_cb;
base::WaitableEvent read_done;
+ std::vector<std::vector<uint8_t>> buffers;
+ buffers.push_back(buffer_data);
EXPECT_CALL(read_cb, Run(::media::DemuxerStream::kOk,
- Pointee(BufferHasData(buffer_data))))
+ ElementsAre(Pointee(BufferHasData(buffer_data)))))
.WillOnce(InvokeWithoutArgs([&read_done]() { read_done.Signal(); }));
- video_stream->Read(read_cb.Get());
+ video_stream->Read(1, read_cb.Get());
EXPECT_TRUE(WaitForEvent(read_done));
}
diff --git a/cobalt/media/progressive/progressive_demuxer.cc b/cobalt/media/progressive/progressive_demuxer.cc
index 8599a31..356d397 100644
--- a/cobalt/media/progressive/progressive_demuxer.cc
+++ b/cobalt/media/progressive/progressive_demuxer.cc
@@ -50,7 +50,8 @@
DCHECK(demuxer_);
}
-void ProgressiveDemuxerStream::Read(ReadCB read_cb) {
+void ProgressiveDemuxerStream::Read(int max_number_of_buffers_to_read,
+ ReadCB read_cb) {
TRACE_EVENT0("media_stack", "ProgressiveDemuxerStream::Read()");
DCHECK(!read_cb.is_null());
@@ -61,7 +62,7 @@
if (stopped_) {
TRACE_EVENT0("media_stack", "ProgressiveDemuxerStream::Read() EOS sent.");
std::move(read_cb).Run(DemuxerStream::kOk,
- DecoderBuffer::CreateEOSBuffer());
+ {DecoderBuffer::CreateEOSBuffer()});
return;
}
@@ -79,7 +80,7 @@
--total_buffer_count_;
buffer_queue_.pop_front();
}
- std::move(read_cb).Run(DemuxerStream::kOk, buffer);
+ std::move(read_cb).Run(DemuxerStream::kOk, {buffer});
} else {
TRACE_EVENT0("media_stack",
"ProgressiveDemuxerStream::Read() request queued.");
@@ -139,7 +140,7 @@
DCHECK_EQ(buffer_queue_.size(), 0);
ReadCB read_cb = std::move(read_queue_.front());
read_queue_.pop_front();
- std::move(read_cb).Run(DemuxerStream::kOk, buffer);
+ std::move(read_cb).Run(DemuxerStream::kOk, {buffer});
} else {
// save the buffer for next read request
buffer_queue_.push_back(buffer);
@@ -188,9 +189,7 @@
for (ReadQueue::iterator it = read_queue_.begin(); it != read_queue_.end();
++it) {
TRACE_EVENT0("media_stack", "ProgressiveDemuxerStream::Stop() EOS sent.");
- std::move(*it).Run(
- DemuxerStream::kOk,
- scoped_refptr<DecoderBuffer>(DecoderBuffer::CreateEOSBuffer()));
+ std::move(*it).Run(DemuxerStream::kOk, {DecoderBuffer::CreateEOSBuffer()});
}
read_queue_.clear();
stopped_ = true;
@@ -289,11 +288,6 @@
// IsConfigComplete() should guarantee we know the duration
DCHECK(parser_->Duration() != ::media::kInfiniteDuration);
host_->SetDuration(parser_->Duration());
- // Bitrate may not be known, however
- uint32 bitrate = parser_->BitsPerSecond();
- if (bitrate > 0) {
- data_source_->SetBitrate(bitrate);
- }
// successful parse of config data, inform the nonblocking demuxer thread
DCHECK_EQ(status, ::media::PIPELINE_OK);
diff --git a/cobalt/media/progressive/progressive_demuxer.h b/cobalt/media/progressive/progressive_demuxer.h
index f4ef49a..5ba9992 100644
--- a/cobalt/media/progressive/progressive_demuxer.h
+++ b/cobalt/media/progressive/progressive_demuxer.h
@@ -47,8 +47,12 @@
ProgressiveDemuxerStream(ProgressiveDemuxer* demuxer, Type type);
- // DemuxerStream implementation
+// DemuxerStream implementation
+#if defined(STARBOARD)
+ void Read(int max_number_of_buffers_to_read, ReadCB read_cb) override;
+#else // defined(STARBOARD)
void Read(ReadCB read_cb) override;
+#endif // defined(STARBOARD)
AudioDecoderConfig audio_decoder_config() override;
VideoDecoderConfig video_decoder_config() override;
Type type() const override;
diff --git a/cobalt/media/progressive/progressive_parser.cc b/cobalt/media/progressive/progressive_parser.cc
index fe810b1..771e386 100644
--- a/cobalt/media/progressive/progressive_parser.cc
+++ b/cobalt/media/progressive/progressive_parser.cc
@@ -48,9 +48,7 @@
}
ProgressiveParser::ProgressiveParser(scoped_refptr<DataSourceReader> reader)
- : reader_(reader),
- duration_(::media::kInfiniteDuration),
- bits_per_second_(0) {}
+ : reader_(reader), duration_(::media::kInfiniteDuration) {}
ProgressiveParser::~ProgressiveParser() {}
diff --git a/cobalt/media/progressive/progressive_parser.h b/cobalt/media/progressive/progressive_parser.h
index f3d3e82..cd3fce7 100644
--- a/cobalt/media/progressive/progressive_parser.h
+++ b/cobalt/media/progressive/progressive_parser.h
@@ -69,8 +69,6 @@
virtual bool IsConfigComplete();
// time-duration of file, may return kInfiniteDuration() if unknown
virtual base::TimeDelta Duration() { return duration_; }
- // bits per second of media, if known, otherwise 0
- virtual uint32 BitsPerSecond() { return bits_per_second_; }
virtual const AudioDecoderConfig& AudioConfig() { return audio_config_; }
virtual const VideoDecoderConfig& VideoConfig() { return video_config_; }
@@ -82,7 +80,6 @@
AudioDecoderConfig audio_config_;
VideoDecoderConfig video_config_;
base::TimeDelta duration_;
- uint32 bits_per_second_;
};
} // namespace media
diff --git a/cobalt/media/sandbox/format_guesstimator.cc b/cobalt/media/sandbox/format_guesstimator.cc
index abac9f6..1225b5a 100644
--- a/cobalt/media/sandbox/format_guesstimator.cc
+++ b/cobalt/media/sandbox/format_guesstimator.cc
@@ -144,6 +144,15 @@
return;
}
InitializeAsAdaptive(path, media_module);
+
+ if (!is_valid() && IsFormat(path_or_url, ".mp4")) {
+ // It's an mp4 file but not in DASH, let's try progressive again.
+ bool is_from_root = !path_or_url.empty() && path_or_url[0] == '/';
+ auto path_from_root = (is_from_root ? "" : "/") + path_or_url;
+ progressive_url_ = GURL("file://" + path_from_root);
+ SB_LOG(INFO) << progressive_url_.spec();
+ mime_type_ = "video/mp4; codecs=\"avc1.640028, mp4a.40.2\"";
+ }
}
void FormatGuesstimator::InitializeAsProgressive(const GURL& url) {
@@ -202,7 +211,6 @@
// true format.
continue;
}
-
// Succeeding |AppendData()| may be a false positive (i.e. the expected
// configuration does not match with the configuration determined by the
// ChunkDemuxer). To confirm, we check the decoder configuration determined
diff --git a/cobalt/media/sandbox/media2_sandbox.cc b/cobalt/media/sandbox/media2_sandbox.cc
index cb89b23..a81ed91 100644
--- a/cobalt/media/sandbox/media2_sandbox.cc
+++ b/cobalt/media/sandbox/media2_sandbox.cc
@@ -75,12 +75,15 @@
void ReadDemuxerStream(DemuxerStream* demuxer_stream);
-void OnDemuxerStreamRead(DemuxerStream* demuxer_stream,
- DemuxerStream::Status status,
- scoped_refptr<::media::DecoderBuffer> decoder_buffer) {
- if (!decoder_buffer->end_of_stream()) {
+void OnDemuxerStreamRead(
+ DemuxerStream* demuxer_stream, DemuxerStream::Status status,
+ const std::vector<scoped_refptr<::media::DecoderBuffer>>& decoder_buffers) {
+ if (status != DemuxerStream::kConfigChanged) {
+ DCHECK(decoder_buffers.size() > 0);
+ }
+ if (!decoder_buffers[0]->end_of_stream()) {
LOG(INFO) << "Reading " << GetDemuxerStreamType(demuxer_stream)
- << " buffer at " << decoder_buffer->timestamp();
+ << " buffer at " << decoder_buffers[0]->timestamp();
ReadDemuxerStream(demuxer_stream);
} else {
LOG(INFO) << "Received " << GetDemuxerStreamType(demuxer_stream) << " EOS";
@@ -90,7 +93,7 @@
void ReadDemuxerStream(DemuxerStream* demuxer_stream) {
DCHECK(demuxer_stream);
demuxer_stream->Read(
- base::BindOnce(OnDemuxerStreamRead, base::Unretained(demuxer_stream)));
+ 1, base::BindOnce(OnDemuxerStreamRead, base::Unretained(demuxer_stream)));
}
} // namespace
diff --git a/cobalt/media/sandbox/web_media_player_helper.cc b/cobalt/media/sandbox/web_media_player_helper.cc
index 9de31c7..ab5c080 100644
--- a/cobalt/media/sandbox/web_media_player_helper.cc
+++ b/cobalt/media/sandbox/web_media_player_helper.cc
@@ -12,12 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+#include "cobalt/media/sandbox/web_media_player_helper.h"
+
#include <memory>
#include <utility>
-#include "cobalt/media/sandbox/web_media_player_helper.h"
-
-#include "cobalt/media/fetcher_buffered_data_source.h"
+#include "cobalt/media/file_data_source.h"
+#include "cobalt/media/url_fetcher_data_source.h"
#include "third_party/chromium/media/cobalt/ui/gfx/geometry/rect.h"
namespace cobalt {
@@ -80,11 +81,19 @@
: client_(new WebMediaPlayerClientStub),
player_(media_module->CreateWebMediaPlayer(client_)) {
player_->SetRate(1.0);
- std::unique_ptr<BufferedDataSource> data_source(new FetcherBufferedDataSource(
- base::MessageLoop::current()->task_runner(), video_url,
- csp::SecurityCallback(), fetcher_factory->network_module(),
- loader::kNoCORSMode, loader::Origin()));
- player_->LoadProgressive(video_url, std::move(data_source));
+
+ if (video_url.SchemeIsFile()) {
+ std::unique_ptr<DataSource> data_source(
+ new media::FileDataSource(video_url));
+ player_->LoadProgressive(video_url, std::move(data_source));
+ } else {
+ std::unique_ptr<DataSource> data_source(new URLFetcherDataSource(
+ base::MessageLoop::current()->task_runner(), video_url,
+ csp::SecurityCallback(), fetcher_factory->network_module(),
+ loader::kNoCORSMode, loader::Origin()));
+ player_->LoadProgressive(video_url, std::move(data_source));
+ }
+
player_->Play();
auto set_bounds_cb = player_->GetSetBoundsCB();
@@ -99,7 +108,7 @@
}
SbDecodeTarget WebMediaPlayerHelper::GetCurrentDecodeTarget() const {
- return player_->GetVideoFrameProvider()->GetCurrentSbDecodeTarget();
+ return player_->GetDecodeTargetProvider()->GetCurrentSbDecodeTarget();
}
bool WebMediaPlayerHelper::IsPlaybackFinished() const {
diff --git a/cobalt/media/sandbox/web_media_player_helper.h b/cobalt/media/sandbox/web_media_player_helper.h
index 939f0ae..21a5877 100644
--- a/cobalt/media/sandbox/web_media_player_helper.h
+++ b/cobalt/media/sandbox/web_media_player_helper.h
@@ -20,7 +20,7 @@
#include "base/callback.h"
#include "cobalt/loader/fetcher_factory.h"
-#include "cobalt/media/base/video_frame_provider.h"
+#include "cobalt/media/base/decode_target_provider.h"
#include "cobalt/media/media_module.h"
#include "cobalt/media/player/web_media_player.h"
#include "third_party/chromium/media/cobalt/ui/gfx/geometry/size.h"
diff --git a/cobalt/media/testing/BUILD.gn b/cobalt/media/testing/BUILD.gn
new file mode 100644
index 0000000..43f1bbd
--- /dev/null
+++ b/cobalt/media/testing/BUILD.gn
@@ -0,0 +1,55 @@
+# Copyright 2022 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("//cobalt/media/testing/data/sha_files.gni")
+
+action("cobalt_media_download_test_data") {
+ install_content = true
+
+ script = "//tools/download_from_gcs.py"
+
+ sha_sources = []
+ foreach(sha_file, sha1_files) {
+ sha_sources += [ string_join("/",
+ [
+ "data",
+ sha_file,
+ ]) ]
+ }
+
+ sha_outputs = []
+ subdir = "cobalt/media/testing"
+ outdir = "$sb_static_contents_output_data_dir/test/$subdir"
+ foreach(sha_source, sha_sources) {
+ sha_outputs += [ string_join("/",
+ [
+ outdir,
+ string_replace(sha_source, ".sha1", ""),
+ ]) ]
+ }
+
+ sources = sha_sources
+ outputs = sha_outputs
+
+ sha1_dir = rebase_path("data", root_build_dir)
+
+ args = [
+ "--bucket",
+ "cobalt-static-storage",
+ "--sha1",
+ sha1_dir,
+ "--output",
+ rebase_path("$outdir/data", root_build_dir),
+ ]
+}
diff --git a/cobalt/media/testing/data/progressive_aac_44100_stereo_h264_1280_720.mp4.sha1 b/cobalt/media/testing/data/progressive_aac_44100_stereo_h264_1280_720.mp4.sha1
new file mode 100644
index 0000000..1b30031
--- /dev/null
+++ b/cobalt/media/testing/data/progressive_aac_44100_stereo_h264_1280_720.mp4.sha1
@@ -0,0 +1 @@
+92fb29d98bded5091e93db4db9c610441d643ce8
diff --git a/cobalt/media/testing/data/sha_files.gni b/cobalt/media/testing/data/sha_files.gni
new file mode 100644
index 0000000..d42094a
--- /dev/null
+++ b/cobalt/media/testing/data/sha_files.gni
@@ -0,0 +1,15 @@
+# Copyright 2022 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+sha1_files = [ "progressive_aac_44100_stereo_h264_1280_720.mp4.sha1" ]
diff --git a/cobalt/media/url_fetcher_data_source.cc b/cobalt/media/url_fetcher_data_source.cc
new file mode 100644
index 0000000..4f25520
--- /dev/null
+++ b/cobalt/media/url_fetcher_data_source.cc
@@ -0,0 +1,521 @@
+// Copyright 2015 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/media/url_fetcher_data_source.h"
+
+#include <algorithm>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/strings/string_number_conversions.h"
+#include "cobalt/base/polymorphic_downcast.h"
+#include "cobalt/loader/cors_preflight.h"
+#include "cobalt/loader/url_fetcher_string_writer.h"
+#include "net/http/http_response_headers.h"
+#include "net/http/http_status_code.h"
+
+namespace cobalt {
+namespace media {
+
+namespace {
+
+const uint32 kBackwardBytes = 256 * 1024;
+const uint32 kInitialForwardBytes = 3 * 256 * 1024;
+const uint32 kInitialBufferCapacity = kBackwardBytes + kInitialForwardBytes;
+
+} // namespace
+
+using base::CircularBufferShell;
+
+URLFetcherDataSource::URLFetcherDataSource(
+ const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
+ const GURL& url, const csp::SecurityCallback& security_callback,
+ network::NetworkModule* network_module, loader::RequestMode request_mode,
+ loader::Origin origin)
+ : task_runner_(task_runner),
+ url_(url),
+ network_module_(network_module),
+ is_downloading_(false),
+ buffer_(kInitialBufferCapacity, CircularBufferShell::kReserve),
+ buffer_offset_(0),
+ error_occured_(false),
+ last_request_offset_(0),
+ last_request_size_(0),
+ last_read_position_(0),
+ pending_read_position_(0),
+ pending_read_size_(0),
+ pending_read_data_(NULL),
+ security_callback_(security_callback),
+ request_mode_(request_mode),
+ document_origin_(origin),
+ is_origin_safe_(false) {
+ DCHECK(task_runner_->BelongsToCurrentThread());
+
+ DCHECK(task_runner_);
+ DCHECK(network_module);
+}
+
+URLFetcherDataSource::~URLFetcherDataSource() {
+ DCHECK(task_runner_->BelongsToCurrentThread());
+
+ if (cancelable_create_fetcher_closure_) {
+ cancelable_create_fetcher_closure_->Cancel();
+ }
+}
+
+void URLFetcherDataSource::Read(int64 position, int size, uint8* data,
+ const ReadCB& read_cb) {
+ DCHECK_GE(position, 0);
+ DCHECK_GE(size, 0);
+
+ if (position < 0 || size < 0) {
+ read_cb.Run(kInvalidSize);
+ return;
+ }
+
+ base::AutoLock auto_lock(lock_);
+ Read_Locked(static_cast<uint64>(position), static_cast<size_t>(size), data,
+ read_cb);
+}
+
+void URLFetcherDataSource::Stop() {
+ {
+ base::AutoLock auto_lock(lock_);
+
+ if (!pending_read_cb_.is_null()) {
+ base::ResetAndReturn(&pending_read_cb_).Run(0);
+ }
+ // From this moment on, any call to Read() should be treated as an error.
+ // Note that we cannot reset |fetcher_| here because of:
+ // 1. URLFetcher has to be destroyed on the thread that it is created,
+ // however Stop() is usually called from the pipeline thread where
+ // |fetcher_| is created on the web thread.
+ // 2. We cannot post a task to the web thread to destroy |fetcher_| as the
+ // web thread is blocked by WebMediaPlayerImpl::Destroy().
+ // Once |error_occured_| is set to true, the URLFetcher callbacks return
+ // immediately and it is safe to destroy |fetcher_| inside the dtor.
+ error_occured_ = true;
+ }
+}
+
+bool URLFetcherDataSource::GetSize(int64* size_out) {
+ base::AutoLock auto_lock(lock_);
+
+ if (total_size_of_resource_) {
+ *size_out = static_cast<int64>(total_size_of_resource_.value());
+ DCHECK_GE(*size_out, 0);
+ } else {
+ *size_out = kInvalidSize;
+ }
+ return *size_out != kInvalidSize;
+}
+
+void URLFetcherDataSource::SetDownloadingStatusCB(
+ const DownloadingStatusCB& downloading_status_cb) {
+ DCHECK(task_runner_->BelongsToCurrentThread());
+
+ DCHECK(!downloading_status_cb.is_null());
+ DCHECK(downloading_status_cb_.is_null());
+ downloading_status_cb_ = downloading_status_cb;
+}
+
+void URLFetcherDataSource::OnURLFetchResponseStarted(
+ const net::URLFetcher* source) {
+ DCHECK(task_runner_->BelongsToCurrentThread());
+
+ base::AutoLock auto_lock(lock_);
+
+ if (fetcher_.get() != source || error_occured_) {
+ return;
+ }
+
+ if (!source->GetStatus().is_success()) {
+ // The error will be handled on OnURLFetchComplete()
+ error_occured_ = true;
+ return;
+ } else if (source->GetResponseCode() == -1) {
+ // Could be a file URL, so we won't expect headers.
+ return;
+ }
+
+ // In the event of a redirect, re-check the security policy.
+ if (source->GetURL() != source->GetOriginalURL()) {
+ if (!security_callback_.is_null() &&
+ !security_callback_.Run(source->GetURL(), true /*did redirect*/)) {
+ error_occured_ = true;
+ if (!pending_read_cb_.is_null()) {
+ base::ResetAndReturn(&pending_read_cb_).Run(-1);
+ }
+ return;
+ }
+ }
+
+ scoped_refptr<net::HttpResponseHeaders> headers =
+ source->GetResponseHeaders();
+ DCHECK(headers);
+
+ if (!is_origin_safe_) {
+ if (loader::CORSPreflight::CORSCheck(
+ *headers, document_origin_.SerializedOrigin(),
+ request_mode_ == loader::kCORSModeIncludeCredentials)) {
+ is_origin_safe_ = true;
+ } else {
+ error_occured_ = true;
+ if (!pending_read_cb_.is_null()) {
+ base::ResetAndReturn(&pending_read_cb_).Run(-1);
+ }
+ return;
+ }
+ }
+
+ uint64 first_byte_offset = 0;
+
+ if (headers->response_code() == net::HTTP_PARTIAL_CONTENT) {
+ int64 first_byte_position = -1;
+ int64 last_byte_position = -1;
+ int64 instance_length = -1;
+ bool is_range_valid = headers && headers->GetContentRangeFor206(
+ &first_byte_position,
+ &last_byte_position, &instance_length);
+ if (is_range_valid) {
+ if (first_byte_position >= 0) {
+ first_byte_offset = static_cast<uint64>(first_byte_position);
+ }
+ if (!total_size_of_resource_ && instance_length > 0) {
+ total_size_of_resource_ = static_cast<uint64>(instance_length);
+ }
+ }
+ }
+
+ DCHECK_LE(first_byte_offset, last_request_offset_);
+
+ if (first_byte_offset < last_request_offset_) {
+ last_request_size_ += last_request_offset_ - first_byte_offset;
+ last_request_offset_ = first_byte_offset;
+ }
+}
+
+void URLFetcherDataSource::OnURLFetchDownloadProgress(
+ const net::URLFetcher* source, int64_t /*current*/, int64_t /*total*/,
+ int64_t /*current_network_bytes*/) {
+ DCHECK(task_runner_->BelongsToCurrentThread());
+ auto* download_data_writer =
+ base::polymorphic_downcast<loader::URLFetcherStringWriter*>(
+ source->GetResponseWriter());
+ std::string downloaded_data;
+ download_data_writer->GetAndResetData(&downloaded_data);
+ size_t size = downloaded_data.size();
+ if (size == 0) {
+ return;
+ }
+ const uint8* data = reinterpret_cast<const uint8*>(downloaded_data.data());
+ base::AutoLock auto_lock(lock_);
+
+ if (fetcher_.get() != source || error_occured_) {
+ return;
+ }
+
+ size = static_cast<size_t>(std::min<uint64>(size, last_request_size_));
+
+ if (size == 0 || size > buffer_.GetMaxCapacity()) {
+ // The server side doesn't support range request. Delete |fetcher_| to
+ // stop the current request.
+ LOG(ERROR)
+ << "URLFetcherDataSource::OnURLFetchDownloadProgress: server "
+ << "doesn't support range requests (e.g. Python SimpleHTTPServer). "
+ << "Please use a server that supports range requests (e.g. Flask).";
+ error_occured_ = true;
+ fetcher_.reset();
+ ProcessPendingRead_Locked();
+ UpdateDownloadingStatus(/* is_downloading = */ false);
+ return;
+ }
+
+ // Because we can only append data into the buffer_. We just check if the
+ // position of the first byte of the newly received data is overlapped with
+ // the range of the buffer_. If not, we can discard all data in the buffer_
+ // as there is no way to represent a gap or to prepend data.
+ if (last_request_offset_ < buffer_offset_ ||
+ last_request_offset_ > buffer_offset_ + buffer_.GetLength()) {
+ buffer_.Clear();
+ buffer_offset_ = last_request_offset_;
+ }
+
+ // If there is any overlapping, modify data/size accordingly.
+ if (buffer_offset_ + buffer_.GetLength() > last_request_offset_) {
+ uint64 difference =
+ buffer_offset_ + buffer_.GetLength() - last_request_offset_;
+ difference = std::min<uint64>(difference, size);
+ data += difference;
+ size -= difference;
+ last_request_offset_ += difference;
+ }
+
+ // If we are overflow, remove some data from the front of the buffer_.
+ if (buffer_.GetLength() + size > buffer_.GetMaxCapacity()) {
+ size_t bytes_skipped;
+ buffer_.Skip(buffer_.GetLength() + size - buffer_.GetMaxCapacity(),
+ &bytes_skipped);
+ // "+ 0" converts buffer_.GetMaxCapacity() into a r-value to avoid link
+ // error.
+ DCHECK_EQ(buffer_.GetLength() + size, buffer_.GetMaxCapacity() + 0);
+ buffer_offset_ += bytes_skipped;
+ }
+
+ size_t bytes_written;
+ bool result = buffer_.Write(data, size, &bytes_written);
+ DCHECK(result);
+ DCHECK_EQ(size, bytes_written);
+
+ last_request_offset_ += bytes_written;
+ last_request_size_ -= bytes_written;
+
+ ProcessPendingRead_Locked();
+}
+
+void URLFetcherDataSource::OnURLFetchComplete(const net::URLFetcher* source) {
+ DCHECK(task_runner_->BelongsToCurrentThread());
+
+ base::AutoLock auto_lock(lock_);
+
+ if (fetcher_.get() != source || error_occured_) {
+ return;
+ }
+
+ const net::URLRequestStatus& status = source->GetStatus();
+ if (status.is_success()) {
+ if (!total_size_of_resource_ && last_request_size_ != 0) {
+ total_size_of_resource_ = buffer_offset_ + buffer_.GetLength();
+ }
+ } else {
+ LOG(ERROR) << "URLFetcherDataSource::OnURLFetchComplete called with error "
+ << status.error();
+ error_occured_ = true;
+ buffer_.Clear();
+ }
+
+ fetcher_.reset();
+
+ ProcessPendingRead_Locked();
+ UpdateDownloadingStatus(/* is_downloading = */ false);
+}
+
+void URLFetcherDataSource::CreateNewFetcher() {
+ DCHECK(task_runner_->BelongsToCurrentThread());
+
+ base::AutoLock auto_lock(lock_);
+
+ DCHECK(!fetcher_);
+ fetcher_to_be_destroyed_.reset();
+
+ DCHECK_GE(static_cast<int64>(last_request_offset_), 0);
+ DCHECK_GE(static_cast<int64>(last_request_size_), 0);
+
+ // Check if there was an error or if the request is blocked by csp.
+ if (error_occured_ ||
+ (!security_callback_.is_null() && !security_callback_.Run(url_, false))) {
+ error_occured_ = true;
+ if (!pending_read_cb_.is_null()) {
+ base::ResetAndReturn(&pending_read_cb_).Run(-1);
+ }
+ UpdateDownloadingStatus(/* is_downloading = */ false);
+ return;
+ }
+
+ fetcher_ =
+ std::move(net::URLFetcher::Create(url_, net::URLFetcher::GET, this));
+ fetcher_->SetRequestContext(
+ network_module_->url_request_context_getter().get());
+ std::unique_ptr<loader::URLFetcherStringWriter> download_data_writer(
+ new loader::URLFetcherStringWriter());
+ fetcher_->SaveResponseWithWriter(std::move(download_data_writer));
+
+ std::string range_request =
+ "Range: bytes=" + base::NumberToString(last_request_offset_) + "-" +
+ base::NumberToString(last_request_offset_ + last_request_size_ - 1);
+ fetcher_->AddExtraRequestHeader(range_request);
+ if (!is_origin_safe_) {
+ if (request_mode_ != loader::kNoCORSMode &&
+ document_origin_ != loader::Origin(url_) && !url_.SchemeIs("data")) {
+ fetcher_->AddExtraRequestHeader("Origin:" +
+ document_origin_.SerializedOrigin());
+ } else {
+ is_origin_safe_ = true;
+ }
+ }
+ fetcher_->Start();
+ UpdateDownloadingStatus(/* is_downloading = */ true);
+}
+
+void URLFetcherDataSource::UpdateDownloadingStatus(bool is_downloading) {
+ DCHECK(task_runner_->BelongsToCurrentThread());
+
+ if (is_downloading_ == is_downloading) {
+ return;
+ }
+
+ is_downloading_ = is_downloading;
+ if (!downloading_status_cb_.is_null()) {
+ downloading_status_cb_.Run(is_downloading_);
+ }
+}
+
+void URLFetcherDataSource::Read_Locked(uint64 position, size_t size,
+ uint8* data, const ReadCB& read_cb) {
+ lock_.AssertAcquired();
+
+ DCHECK(data);
+ DCHECK(!read_cb.is_null());
+ DCHECK(pending_read_cb_.is_null()); // One read operation at the same time.
+
+ if (error_occured_) {
+ read_cb.Run(-1);
+ return;
+ }
+
+ // Clamp the request to valid range of the resource if its size is known.
+ if (total_size_of_resource_) {
+ position = std::min(position, total_size_of_resource_.value());
+ if (size + position > total_size_of_resource_.value()) {
+ size = static_cast<size_t>(total_size_of_resource_.value() - position);
+ }
+ }
+
+ last_read_position_ = position;
+
+ if (size == 0) {
+ read_cb.Run(0);
+ return;
+ }
+
+ // Fulfill the read request now if we have the data.
+ if (position >= buffer_offset_ &&
+ position + size <= buffer_offset_ + buffer_.GetLength()) {
+ // All data is available
+ size_t bytes_peeked;
+ buffer_.Peek(data, size, static_cast<size_t>(position - buffer_offset_),
+ &bytes_peeked);
+ DCHECK_EQ(bytes_peeked, size);
+ DCHECK_GE(static_cast<int>(bytes_peeked), 0);
+ read_cb.Run(static_cast<int>(bytes_peeked));
+ // If we have a large buffer size, it could be ideal if we can keep sending
+ // small requests when the read offset is far from the beginning of the
+ // buffer. However as the ProgressiveDemuxer will cache many frames and the
+ // buffer we are using is usually small, we will just avoid sending requests
+ // here to make code simple.
+ return;
+ }
+
+ // Save the read request as we are unable to fulfill it now.
+ pending_read_cb_ = read_cb;
+ pending_read_position_ = position;
+ pending_read_size_ = size;
+ pending_read_data_ = data;
+
+ // Combine the range of the buffer and any ongoing fetch to see if the read is
+ // overlapped with it.
+ if (fetcher_) {
+ uint64 begin = last_request_offset_;
+ uint64 end = last_request_offset_ + last_request_size_;
+ if (last_request_offset_ >= buffer_offset_ &&
+ last_request_offset_ <= buffer_offset_ + buffer_.GetLength()) {
+ begin = buffer_offset_;
+ }
+ if (position >= begin && position < end) {
+ // The read is overlapped with existing request, just wait.
+ return;
+ }
+ }
+
+ // Now we have to issue a new fetch and we no longer care about the range of
+ // the current fetch in progress if there is any. Ideally the request range
+ // starts at |last_read_position_ - kBackwardBytes| with length of
+ // buffer_.GetMaxCapacity().
+ if (last_read_position_ > kBackwardBytes) {
+ last_request_offset_ = last_read_position_ - kBackwardBytes;
+ } else {
+ last_request_offset_ = 0;
+ }
+
+ size_t required_size = static_cast<size_t>(
+ last_read_position_ - last_request_offset_ + pending_read_size_);
+ if (required_size > buffer_.GetMaxCapacity()) {
+ // The capacity of the current buffer is not large enough to hold the
+ // pending read.
+ size_t new_capacity =
+ std::max<size_t>(buffer_.GetMaxCapacity() * 2, required_size);
+ buffer_.IncreaseMaxCapacityTo(new_capacity);
+ }
+
+ last_request_size_ = buffer_.GetMaxCapacity();
+
+ if (last_request_offset_ >= buffer_offset_ &&
+ last_request_offset_ <= buffer_offset_ + buffer_.GetLength()) {
+ // Part of the Read() can be fulfilled by the current buffer and current
+ // request but cannot be fulfilled by the current request but we have to
+ // send another request to retrieve the rest.
+ last_request_size_ -=
+ buffer_offset_ + buffer_.GetLength() - last_request_offset_;
+ last_request_offset_ = buffer_offset_ + buffer_.GetLength();
+ }
+
+ if (cancelable_create_fetcher_closure_) {
+ cancelable_create_fetcher_closure_->Cancel();
+ }
+ base::Closure create_fetcher_closure = base::Bind(
+ &URLFetcherDataSource::CreateNewFetcher, base::Unretained(this));
+ cancelable_create_fetcher_closure_ =
+ new CancelableClosure(create_fetcher_closure);
+ fetcher_to_be_destroyed_.reset(fetcher_.release());
+ task_runner_->PostTask(FROM_HERE,
+ cancelable_create_fetcher_closure_->AsClosure());
+}
+
+void URLFetcherDataSource::ProcessPendingRead_Locked() {
+ lock_.AssertAcquired();
+ if (!pending_read_cb_.is_null()) {
+ Read_Locked(pending_read_position_, pending_read_size_, pending_read_data_,
+ base::ResetAndReturn(&pending_read_cb_));
+ }
+}
+
+URLFetcherDataSource::CancelableClosure::CancelableClosure(
+ const base::Closure& closure)
+ : closure_(closure) {
+ DCHECK(!closure.is_null());
+}
+
+void URLFetcherDataSource::CancelableClosure::Cancel() {
+ base::AutoLock auto_lock(lock_);
+ closure_.Reset();
+}
+
+base::Closure URLFetcherDataSource::CancelableClosure::AsClosure() {
+ return base::Bind(&CancelableClosure::Call, this);
+}
+
+void URLFetcherDataSource::CancelableClosure::Call() {
+ base::AutoLock auto_lock(lock_);
+ // closure_.Run() has to be called when the lock is acquired to avoid race
+ // condition.
+ if (!closure_.is_null()) {
+ base::ResetAndReturn(&closure_).Run();
+ }
+}
+
+} // namespace media
+} // namespace cobalt
diff --git a/cobalt/media/url_fetcher_data_source.h b/cobalt/media/url_fetcher_data_source.h
new file mode 100644
index 0000000..8cea653
--- /dev/null
+++ b/cobalt/media/url_fetcher_data_source.h
@@ -0,0 +1,162 @@
+// Copyright 2015 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_MEDIA_URL_FETCHER_DATA_SOURCE_H_
+#define COBALT_MEDIA_URL_FETCHER_DATA_SOURCE_H_
+
+#include <memory>
+#include <string>
+
+#include "base/callback.h"
+#include "base/compiler_specific.h"
+#include "base/memory/ref_counted.h"
+#include "base/message_loop/message_loop.h"
+#include "base/optional.h"
+#include "base/synchronization/lock.h"
+#include "cobalt/base/circular_buffer_shell.h"
+#include "cobalt/csp/content_security_policy.h"
+#include "cobalt/loader/fetcher.h"
+#include "cobalt/loader/origin.h"
+#include "cobalt/loader/url_fetcher_string_writer.h"
+#include "cobalt/media/base/data_source.h"
+#include "cobalt/network/network_module.h"
+#include "net/url_request/url_fetcher.h"
+#include "net/url_request/url_fetcher_delegate.h"
+#include "url/gurl.h"
+
+namespace cobalt {
+namespace media {
+
+// TODO: This class requires a large block of memory.
+
+// A DataSource based on net::URLFetcher that can be used to retrieve
+// progressive videos from both local and network sources.
+// It uses a fixed size circular buffer so we may not be able to store all data
+// into this buffer. It is based on the following assumptions/strategies:
+// 1. It assumes that the buffer is large enough to fulfill one Read() request.
+// So any outstanding request only requires at most one request.
+// 2. It will do one initial request to retrieve the target resource. If the
+// whole resource can be fit into the buffer, no further request will be
+// fired.
+// 3. If the resource doesn't fit into the buffer. The class will store
+// kBackwardBytes bytes before the last read offset(LRO) and kForwardBytes
+// after LRO. Note that if LRO is less than kBackwardBytes, then data starts
+// from offset 0 will be cached.
+// 4. It assumes that the server supports range request.
+// 5. All data stored are continuous.
+class URLFetcherDataSource : public DataSource,
+ private net::URLFetcherDelegate {
+ public:
+ static const int64 kInvalidSize = -1;
+
+ // Because the URLFetchers have to be created and destroyed on the same
+ // thread, we use the |task_runner| passed in to ensure that. Note that the
+ // ctor and dtor are also called from the |task_runner|, which is checked in
+ // the ctor and dtor.
+ URLFetcherDataSource(
+ const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
+ const GURL& url, const csp::SecurityCallback& security_callback,
+ network::NetworkModule* network_module, loader::RequestMode request_mode,
+ loader::Origin origin);
+ ~URLFetcherDataSource() override;
+
+ // DataSource methods.
+ void Read(int64 position, int size, uint8* data,
+ const ReadCB& read_cb) override;
+ void Stop() override;
+ void Abort() override {}
+ bool GetSize(int64* size_out) override;
+ void SetDownloadingStatusCB(
+ const DownloadingStatusCB& downloading_status_cb) override;
+
+ private:
+ class CancelableClosure
+ : public base::RefCountedThreadSafe<CancelableClosure> {
+ public:
+ explicit CancelableClosure(const base::Closure& closure);
+
+ void Cancel();
+ base::Closure AsClosure();
+
+ private:
+ void Call();
+
+ base::Lock lock_;
+ base::Closure closure_;
+ };
+
+ // net::URLFetcherDelegate methods
+ void OnURLFetchResponseStarted(const net::URLFetcher* source) override;
+ void OnURLFetchDownloadProgress(const net::URLFetcher* source,
+ int64_t current, int64_t total,
+ int64_t current_network_bytes) override;
+ void OnURLFetchComplete(const net::URLFetcher* source) override;
+
+ void CreateNewFetcher();
+ void UpdateDownloadingStatus(bool is_downloading);
+ void Read_Locked(uint64 position, size_t size, uint8* data,
+ const ReadCB& read_cb);
+ void ProcessPendingRead_Locked();
+ void TryToSendRequest_Locked();
+
+ base::Lock lock_;
+ scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
+ GURL url_;
+ network::NetworkModule* network_module_;
+ std::unique_ptr<net::URLFetcher> fetcher_;
+
+ bool is_downloading_;
+ DownloadingStatusCB downloading_status_cb_;
+
+ // |fetcher_| has to be destroyed on the thread it's created. So it cannot be
+ // safely destroyed inside Read_Locked(). Save |fetcher_| into
+ // |fetcher_to_be_destroyed_| to ensure that it is properly destroyed either
+ // inside CreateNewFetcher() or in the dtor while still allow |fetcher_| to be
+ // set to NULL to invalidate outstanding read.
+ std::unique_ptr<net::URLFetcher> fetcher_to_be_destroyed_;
+
+ // |buffer_| stores a continuous block of data of target resource starts from
+ // |buffer_offset_|. When the target resource can be fit into |buffer_|,
+ // |buffer_offset_| will always be 0.
+ base::CircularBufferShell buffer_;
+ uint64 buffer_offset_;
+
+ base::Optional<uint64> total_size_of_resource_;
+ bool error_occured_;
+
+ uint64 last_request_offset_;
+ uint64 last_request_size_;
+
+ // This is usually the same as pending_read_position_. Represent it
+ // explicitly using a separate variable.
+ uint64 last_read_position_;
+
+ ReadCB pending_read_cb_;
+ uint64 pending_read_position_;
+ size_t pending_read_size_;
+ uint8* pending_read_data_;
+
+ csp::SecurityCallback security_callback_;
+ scoped_refptr<CancelableClosure> cancelable_create_fetcher_closure_;
+
+ loader::RequestMode request_mode_;
+ loader::Origin document_origin_;
+ // True if the origin is allowed to fetch resource data.
+ bool is_origin_safe_;
+};
+
+} // namespace media
+} // namespace cobalt
+
+#endif // COBALT_MEDIA_URL_FETCHER_DATA_SOURCE_H_
diff --git a/cobalt/media_session/media_session.cc b/cobalt/media_session/media_session.cc
index 2b8dd33..d0d412c 100644
--- a/cobalt/media_session/media_session.cc
+++ b/cobalt/media_session/media_session.cc
@@ -26,6 +26,10 @@
last_position_updated_time_(0) {}
MediaSession::~MediaSession() {
+ // Stop the media session client first. This avoids possible access to media
+ // session during destruction.
+ media_session_client_.reset();
+
ActionMap::iterator it;
for (it = action_map_.begin(); it != action_map_.end(); ++it) {
delete it->second;
@@ -98,9 +102,7 @@
}
is_change_task_queued_ = true;
task_runner_->PostDelayedTask(
- FROM_HERE,
- base::Bind(&MediaSession::OnChanged, this),
- delay);
+ FROM_HERE, base::Bind(&MediaSession::OnChanged, this), delay);
}
void MediaSession::OnChanged() {
diff --git a/cobalt/media_session/media_session.h b/cobalt/media_session/media_session.h
index c83b1df..4814c9e 100644
--- a/cobalt/media_session/media_session.h
+++ b/cobalt/media_session/media_session.h
@@ -16,6 +16,7 @@
#define COBALT_MEDIA_SESSION_MEDIA_SESSION_H_
#include <map>
+#include <memory>
#include "base/containers/small_map.h"
#include "base/location.h"
diff --git a/cobalt/media_session/media_session_client.cc b/cobalt/media_session/media_session_client.cc
index c8290cf..ab740de 100644
--- a/cobalt/media_session/media_session_client.cc
+++ b/cobalt/media_session/media_session_client.cc
@@ -91,6 +91,9 @@
} else if (extension_->RegisterMediaSessionCallbacks != nullptr) {
extension_->RegisterMediaSessionCallbacks(
this, &InvokeActionCallback, &UpdatePlatformPlaybackStateCallback);
+ DCHECK(extension_->DestroyMediaSessionClientCallback)
+ << "Possible heap use-after-free if platform does not handle media "
+ << "session DestroyMediaSessionClientCallback()";
}
}
}
@@ -186,8 +189,8 @@
void MediaSessionClient::PostDelayedTaskForMaybeFreezeCallback() {
media_session_->task_runner_->PostDelayedTask(
FROM_HERE,
- base::Bind(&MediaSessionClient::RunMaybeFreezeCallback,
- base::Unretained(this), ++sequence_number_),
+ base::Bind(&MediaSessionClient::RunMaybeFreezeCallback, AsWeakPtr(),
+ ++sequence_number_),
kMaybeFreezeDelay);
}
@@ -197,7 +200,7 @@
if (!media_session_->task_runner_->BelongsToCurrentThread()) {
media_session_->task_runner_->PostTask(
FROM_HERE, base::Bind(&MediaSessionClient::UpdatePlatformPlaybackState,
- base::Unretained(this), state));
+ AsWeakPtr(), state));
return;
}
@@ -235,7 +238,7 @@
if (!media_session_->task_runner_->BelongsToCurrentThread()) {
media_session_->task_runner_->PostTask(
FROM_HERE, base::Bind(&MediaSessionClient::InvokeActionInternal,
- base::Unretained(this), base::Passed(&details)));
+ AsWeakPtr(), base::Passed(&details)));
return;
}
diff --git a/cobalt/media_session/media_session_client.h b/cobalt/media_session/media_session_client.h
index 35a9a31..245a635 100644
--- a/cobalt/media_session/media_session_client.h
+++ b/cobalt/media_session/media_session_client.h
@@ -18,6 +18,7 @@
#include <bitset>
#include <memory>
+#include "base/memory/weak_ptr.h"
#include "base/threading/thread_checker.h"
#include "cobalt/extension/media_session.h"
#include "cobalt/media/web_media_player_factory.h"
@@ -31,7 +32,7 @@
// Base class for a platform-level implementation of MediaSession.
// Platforms should subclass this to connect MediaSession to their platform.
-class MediaSessionClient {
+class MediaSessionClient : public base::SupportsWeakPtr<MediaSessionClient> {
friend class MediaSession;
public:
diff --git a/cobalt/network/BUILD.gn b/cobalt/network/BUILD.gn
index 53821e9..4af6239 100644
--- a/cobalt/network/BUILD.gn
+++ b/cobalt/network/BUILD.gn
@@ -86,7 +86,6 @@
sources = [
"//cobalt/content/ssl/certs/002c0b4f.0",
"//cobalt/content/ssl/certs/02265526.0",
- "//cobalt/content/ssl/certs/03179a64.0",
"//cobalt/content/ssl/certs/062cdee6.0",
"//cobalt/content/ssl/certs/064e0aa9.0",
"//cobalt/content/ssl/certs/06dc52d5.0",
diff --git a/cobalt/persistent_storage/persistent_settings.cc b/cobalt/persistent_storage/persistent_settings.cc
index 822a5d3..c5d92e4 100644
--- a/cobalt/persistent_storage/persistent_settings.cc
+++ b/cobalt/persistent_storage/persistent_settings.cc
@@ -49,7 +49,7 @@
DCHECK(message_loop());
std::vector<char> storage_dir(kSbFileMaxPath + 1, 0);
- SbSystemGetPath(kSbSystemPathStorageDirectory, storage_dir.data(),
+ SbSystemGetPath(kSbSystemPathCacheDirectory, storage_dir.data(),
kSbFileMaxPath);
persistent_settings_file_ =
diff --git a/cobalt/persistent_storage/persistent_settings.h b/cobalt/persistent_storage/persistent_settings.h
index 47cd446..0cf789f 100644
--- a/cobalt/persistent_storage/persistent_settings.h
+++ b/cobalt/persistent_storage/persistent_settings.h
@@ -34,7 +34,8 @@
namespace persistent_storage {
// PersistentSettings manages Cobalt settings that persist from one application
-// lifecycle to another as a JSON file in kSbSystemPathStorageDirectory.
+// lifecycle to another as a JSON file in kSbSystemPathCacheDirectory. The
+// persistent settings will persist until the cache is cleared.
// PersistentSettings uses JsonPrefStore for most of its functionality.
// JsonPrefStore maintains thread safety by requiring that all access occurs on
// the Sequence that created it.
diff --git a/cobalt/persistent_storage/persistent_settings_test.cc b/cobalt/persistent_storage/persistent_settings_test.cc
index 4a3f3a7..3f88397 100644
--- a/cobalt/persistent_storage/persistent_settings_test.cc
+++ b/cobalt/persistent_storage/persistent_settings_test.cc
@@ -44,7 +44,7 @@
base::test::ScopedTaskEnvironment::MainThreadType::DEFAULT,
base::test::ScopedTaskEnvironment::ExecutionMode::ASYNC) {
std::vector<char> storage_dir(kSbFileMaxPath + 1, 0);
- SbSystemGetPath(kSbSystemPathStorageDirectory, storage_dir.data(),
+ SbSystemGetPath(kSbSystemPathCacheDirectory, storage_dir.data(),
kSbFileMaxPath);
persistent_settings_file_ = std::string(storage_dir.data()) +
@@ -69,7 +69,8 @@
};
TEST_F(PersistentSettingTest, GetDefaultBool) {
- auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
+ auto persistent_settings =
+ std::make_unique<PersistentSettings>(kPersistentSettingsJson);
persistent_settings->ValidatePersistentSettings();
// does not exist
@@ -84,15 +85,15 @@
persistent_settings->GetPersistentSettingAsInt("key", true));
test_done->Signal();
},
- persistent_settings, &test_done_);
+ persistent_settings.get(), &test_done_);
persistent_settings->SetPersistentSetting(
"key", std::make_unique<base::Value>(4.2), std::move(closure));
test_done_.Wait();
- delete persistent_settings;
}
TEST_F(PersistentSettingTest, GetSetBool) {
- auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
+ auto persistent_settings =
+ std::make_unique<PersistentSettings>(kPersistentSettingsJson);
persistent_settings->ValidatePersistentSettings();
base::OnceClosure closure = base::BindOnce(
@@ -104,7 +105,7 @@
persistent_settings->GetPersistentSettingAsBool("key", false));
test_done->Signal();
},
- persistent_settings, &test_done_);
+ persistent_settings.get(), &test_done_);
persistent_settings->SetPersistentSetting(
"key", std::make_unique<base::Value>(true), std::move(closure));
@@ -120,17 +121,17 @@
persistent_settings->GetPersistentSettingAsBool("key", false));
test_done->Signal();
},
- persistent_settings, &test_done_);
+ persistent_settings.get(), &test_done_);
persistent_settings->SetPersistentSetting(
"key", std::make_unique<base::Value>(false), std::move(closure));
test_done_.Wait();
- delete persistent_settings;
}
TEST_F(PersistentSettingTest, GetDefaultInt) {
- auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
+ auto persistent_settings =
+ std::make_unique<PersistentSettings>(kPersistentSettingsJson);
persistent_settings->ValidatePersistentSettings();
// does not exist
@@ -145,7 +146,7 @@
ASSERT_EQ(8, persistent_settings->GetPersistentSettingAsInt("key", 8));
test_done->Signal();
},
- persistent_settings, &test_done_);
+ persistent_settings.get(), &test_done_);
persistent_settings->SetPersistentSetting(
"key", std::make_unique<base::Value>(4.2), std::move(closure));
@@ -153,7 +154,8 @@
}
TEST_F(PersistentSettingTest, GetSetInt) {
- auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
+ auto persistent_settings =
+ std::make_unique<PersistentSettings>(kPersistentSettingsJson);
persistent_settings->ValidatePersistentSettings();
base::OnceClosure closure = base::BindOnce(
@@ -162,7 +164,7 @@
ASSERT_EQ(-1, persistent_settings->GetPersistentSettingAsInt("key", 8));
test_done->Signal();
},
- persistent_settings, &test_done_);
+ persistent_settings.get(), &test_done_);
persistent_settings->SetPersistentSetting(
"key", std::make_unique<base::Value>(-1), std::move(closure));
test_done_.Wait();
@@ -174,7 +176,7 @@
ASSERT_EQ(0, persistent_settings->GetPersistentSettingAsInt("key", 8));
test_done->Signal();
},
- persistent_settings, &test_done_);
+ persistent_settings.get(), &test_done_);
persistent_settings->SetPersistentSetting(
"key", std::make_unique<base::Value>(0), std::move(closure));
test_done_.Wait();
@@ -186,14 +188,15 @@
ASSERT_EQ(42, persistent_settings->GetPersistentSettingAsInt("key", 8));
test_done->Signal();
},
- persistent_settings, &test_done_);
+ persistent_settings.get(), &test_done_);
persistent_settings->SetPersistentSetting(
"key", std::make_unique<base::Value>(42), std::move(closure));
test_done_.Wait();
}
TEST_F(PersistentSettingTest, GetDefaultString) {
- auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
+ auto persistent_settings =
+ std::make_unique<PersistentSettings>(kPersistentSettingsJson);
persistent_settings->ValidatePersistentSettings();
// does not exist
@@ -215,15 +218,15 @@
"key", "hello"));
test_done->Signal();
},
- persistent_settings, &test_done_);
+ persistent_settings.get(), &test_done_);
persistent_settings->SetPersistentSetting(
"key", std::make_unique<base::Value>(4.2), std::move(closure));
test_done_.Wait();
- delete persistent_settings;
}
TEST_F(PersistentSettingTest, GetSetString) {
- auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
+ auto persistent_settings =
+ std::make_unique<PersistentSettings>(kPersistentSettingsJson);
persistent_settings->ValidatePersistentSettings();
base::OnceClosure closure = base::BindOnce(
@@ -233,7 +236,7 @@
"key", "hello"));
test_done->Signal();
},
- persistent_settings, &test_done_);
+ persistent_settings.get(), &test_done_);
persistent_settings->SetPersistentSetting(
"key", std::make_unique<base::Value>(""), std::move(closure));
@@ -248,7 +251,7 @@
persistent_settings->GetPersistentSettingAsString("key", "hello"));
test_done->Signal();
},
- persistent_settings, &test_done_);
+ persistent_settings.get(), &test_done_);
persistent_settings->SetPersistentSetting(
"key", std::make_unique<base::Value>("hello there"), std::move(closure));
@@ -262,7 +265,7 @@
"key", "hello"));
test_done->Signal();
},
- persistent_settings, &test_done_);
+ persistent_settings.get(), &test_done_);
persistent_settings->SetPersistentSetting(
"key", std::make_unique<base::Value>("42"), std::move(closure));
test_done_.Wait();
@@ -275,7 +278,7 @@
"key", "hello"));
test_done->Signal();
},
- persistent_settings, &test_done_);
+ persistent_settings.get(), &test_done_);
persistent_settings->SetPersistentSetting(
"key", std::make_unique<base::Value>("\n"), std::move(closure));
test_done_.Wait();
@@ -288,15 +291,178 @@
"key", "hello"));
test_done->Signal();
},
- persistent_settings, &test_done_);
+ persistent_settings.get(), &test_done_);
persistent_settings->SetPersistentSetting(
"key", std::make_unique<base::Value>("\\n"), std::move(closure));
test_done_.Wait();
test_done_.Reset();
}
+TEST_F(PersistentSettingTest, GetSetList) {
+ auto persistent_settings =
+ std::make_unique<PersistentSettings>(kPersistentSettingsJson);
+ persistent_settings->ValidatePersistentSettings();
+
+ base::OnceClosure closure = base::BindOnce(
+ [](PersistentSettings* persistent_settings,
+ base::WaitableEvent* test_done) {
+ auto test_list = persistent_settings->GetPersistentSettingAsList("key");
+ ASSERT_FALSE(test_list.empty());
+ ASSERT_EQ(1, test_list.size());
+ ASSERT_TRUE(test_list[0].is_string());
+ ASSERT_EQ("hello", test_list[0].GetString());
+ test_done->Signal();
+ },
+ persistent_settings.get(), &test_done_);
+
+ std::vector<base::Value> list;
+ list.emplace_back("hello");
+ persistent_settings->SetPersistentSetting(
+ "key", std::make_unique<base::Value>(list), std::move(closure));
+ test_done_.Wait();
+ test_done_.Reset();
+
+ closure = base::BindOnce(
+ [](PersistentSettings* persistent_settings,
+ base::WaitableEvent* test_done) {
+ auto test_list = persistent_settings->GetPersistentSettingAsList("key");
+ ASSERT_FALSE(test_list.empty());
+ ASSERT_EQ(2, test_list.size());
+ ASSERT_TRUE(test_list[0].is_string());
+ ASSERT_EQ("hello", test_list[0].GetString());
+ ASSERT_TRUE(test_list[1].is_string());
+ ASSERT_EQ("there", test_list[1].GetString());
+ test_done->Signal();
+ },
+ persistent_settings.get(), &test_done_);
+
+ list.emplace_back("there");
+ persistent_settings->SetPersistentSetting(
+ "key", std::make_unique<base::Value>(list), std::move(closure));
+ test_done_.Wait();
+ test_done_.Reset();
+
+ closure = base::BindOnce(
+ [](PersistentSettings* persistent_settings,
+ base::WaitableEvent* test_done) {
+ auto test_list = persistent_settings->GetPersistentSettingAsList("key");
+ ASSERT_FALSE(test_list.empty());
+ ASSERT_EQ(3, test_list.size());
+ ASSERT_TRUE(test_list[0].is_string());
+ ASSERT_EQ("hello", test_list[0].GetString());
+ ASSERT_TRUE(test_list[1].is_string());
+ ASSERT_EQ("there", test_list[1].GetString());
+ ASSERT_TRUE(test_list[2].is_int());
+ ASSERT_EQ(42, test_list[2].GetInt());
+ test_done->Signal();
+ },
+ persistent_settings.get(), &test_done_);
+
+ list.emplace_back(42);
+ persistent_settings->SetPersistentSetting(
+ "key", std::make_unique<base::Value>(list), std::move(closure));
+ test_done_.Wait();
+ test_done_.Reset();
+}
+
+TEST_F(PersistentSettingTest, GetSetDictionary) {
+ auto persistent_settings =
+ std::make_unique<PersistentSettings>(kPersistentSettingsJson);
+ persistent_settings->ValidatePersistentSettings();
+
+ base::OnceClosure closure = base::BindOnce(
+ [](PersistentSettings* persistent_settings,
+ base::WaitableEvent* test_done) {
+ auto test_dict =
+ persistent_settings->GetPersistentSettingAsDictionary("key");
+ ASSERT_FALSE(test_dict.empty());
+ ASSERT_EQ(1, test_dict.size());
+ ASSERT_TRUE(test_dict["key_string"]->is_string());
+ ASSERT_EQ("hello", test_dict["key_string"]->GetString());
+ test_done->Signal();
+ },
+ persistent_settings.get(), &test_done_);
+
+ base::flat_map<std::string, std::unique_ptr<base::Value>> dict;
+ dict.try_emplace("key_string", std::make_unique<base::Value>("hello"));
+ persistent_settings->SetPersistentSetting(
+ "key", std::make_unique<base::Value>(dict), std::move(closure));
+ test_done_.Wait();
+ test_done_.Reset();
+
+ closure = base::BindOnce(
+ [](PersistentSettings* persistent_settings,
+ base::WaitableEvent* test_done) {
+ auto test_dict =
+ persistent_settings->GetPersistentSettingAsDictionary("key");
+ ASSERT_FALSE(test_dict.empty());
+ ASSERT_EQ(2, test_dict.size());
+ ASSERT_TRUE(test_dict["key_string"]->is_string());
+ ASSERT_EQ("hello", test_dict["key_string"]->GetString());
+ ASSERT_TRUE(test_dict["key_int"]->is_int());
+ ASSERT_EQ(42, test_dict["key_int"]->GetInt());
+ test_done->Signal();
+ },
+ persistent_settings.get(), &test_done_);
+
+ dict.try_emplace("key_int", std::make_unique<base::Value>(42));
+ persistent_settings->SetPersistentSetting(
+ "key", std::make_unique<base::Value>(dict), std::move(closure));
+ test_done_.Wait();
+ test_done_.Reset();
+}
+
+TEST_F(PersistentSettingTest, URLAsKey) {
+ // Tests that json_pref_store has the correct SetValue and
+ // RemoveValue changes for using a URL as a PersistentSettings
+ // Key.
+ auto persistent_settings =
+ std::make_unique<PersistentSettings>(kPersistentSettingsJson);
+ persistent_settings->ValidatePersistentSettings();
+
+ base::OnceClosure closure = base::BindOnce(
+ [](PersistentSettings* persistent_settings,
+ base::WaitableEvent* test_done) {
+ auto test_dict = persistent_settings->GetPersistentSettingAsDictionary(
+ "http://127.0.0.1:45019/");
+ ASSERT_FALSE(test_dict.empty());
+ ASSERT_EQ(1, test_dict.size());
+ ASSERT_TRUE(test_dict["http://127.0.0.1:45019/"]->is_string());
+ ASSERT_EQ("Dictionary URL Key Works!",
+ test_dict["http://127.0.0.1:45019/"]->GetString());
+ test_done->Signal();
+ },
+ persistent_settings.get(), &test_done_);
+
+ // Test that json_pref_store uses SetKey instead of Set, making URL
+ // keys viable.
+ base::flat_map<std::string, std::unique_ptr<base::Value>> dict;
+ dict.try_emplace("http://127.0.0.1:45019/",
+ std::make_unique<base::Value>("Dictionary URL Key Works!"));
+ persistent_settings->SetPersistentSetting("http://127.0.0.1:45019/",
+ std::make_unique<base::Value>(dict),
+ std::move(closure));
+ test_done_.Wait();
+ test_done_.Reset();
+
+ closure = base::BindOnce(
+ [](PersistentSettings* persistent_settings,
+ base::WaitableEvent* test_done) {
+ auto test_dict = persistent_settings->GetPersistentSettingAsDictionary(
+ "http://127.0.0.1:45019/");
+ ASSERT_TRUE(test_dict.empty());
+ test_done->Signal();
+ },
+ persistent_settings.get(), &test_done_);
+ persistent_settings->RemovePersistentSetting("http://127.0.0.1:45019/",
+ std::move(closure));
+ test_done_.Wait();
+ test_done_.Reset();
+}
+
TEST_F(PersistentSettingTest, RemoveSetting) {
- auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
+ auto persistent_settings =
+ std::make_unique<PersistentSettings>(kPersistentSettingsJson);
persistent_settings->ValidatePersistentSettings();
ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
@@ -311,7 +477,7 @@
persistent_settings->GetPersistentSettingAsBool("key", false));
test_done->Signal();
},
- persistent_settings, &test_done_);
+ persistent_settings.get(), &test_done_);
persistent_settings->SetPersistentSetting(
"key", std::make_unique<base::Value>(true), std::move(closure));
test_done_.Wait();
@@ -326,16 +492,15 @@
persistent_settings->GetPersistentSettingAsBool("key", false));
test_done->Signal();
},
- persistent_settings, &test_done_);
+ persistent_settings.get(), &test_done_);
persistent_settings->RemovePersistentSetting("key", std::move(closure));
test_done_.Wait();
test_done_.Reset();
-
- delete persistent_settings;
}
TEST_F(PersistentSettingTest, DeleteSettings) {
- auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
+ auto persistent_settings =
+ std::make_unique<PersistentSettings>(kPersistentSettingsJson);
persistent_settings->ValidatePersistentSettings();
ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
@@ -350,7 +515,7 @@
persistent_settings->GetPersistentSettingAsBool("key", false));
test_done->Signal();
},
- persistent_settings, &test_done_);
+ persistent_settings.get(), &test_done_);
persistent_settings->SetPersistentSetting(
"key", std::make_unique<base::Value>(true), std::move(closure));
test_done_.Wait();
@@ -365,16 +530,15 @@
persistent_settings->GetPersistentSettingAsBool("key", false));
test_done->Signal();
},
- persistent_settings, &test_done_);
+ persistent_settings.get(), &test_done_);
persistent_settings->DeletePersistentSettings(std::move(closure));
test_done_.Wait();
test_done_.Reset();
-
- delete persistent_settings;
}
TEST_F(PersistentSettingTest, InvalidSettings) {
- auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
+ auto persistent_settings =
+ std::make_unique<PersistentSettings>(kPersistentSettingsJson);
persistent_settings->ValidatePersistentSettings();
ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
@@ -389,18 +553,18 @@
persistent_settings->GetPersistentSettingAsBool("key", false));
test_done->Signal();
},
- persistent_settings, &test_done_);
+ persistent_settings.get(), &test_done_);
persistent_settings->SetPersistentSetting(
"key", std::make_unique<base::Value>(true), std::move(closure));
test_done_.Wait();
test_done_.Reset();
- delete persistent_settings;
// Sleep for one second to allow for the previous persistent_setting's
// JsonPrefStore instance time to write to disk before creating a new
// persistent_settings and JsonPrefStore instance.
base::PlatformThread::Sleep(base::TimeDelta::FromSeconds(1));
- persistent_settings = new PersistentSettings(kPersistentSettingsJson);
+ persistent_settings =
+ std::make_unique<PersistentSettings>(kPersistentSettingsJson);
ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", false));
@@ -413,20 +577,18 @@
persistent_settings->GetPersistentSettingAsBool("key", false));
test_done->Signal();
},
- persistent_settings, &test_done_);
+ persistent_settings.get(), &test_done_);
persistent_settings->SetPersistentSetting(
"key", std::make_unique<base::Value>(false), std::move(closure));
test_done_.Wait();
test_done_.Reset();
- delete persistent_settings;
- persistent_settings = new PersistentSettings(kPersistentSettingsJson);
+ persistent_settings =
+ std::make_unique<PersistentSettings>(kPersistentSettingsJson);
persistent_settings->ValidatePersistentSettings();
ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
ASSERT_FALSE(persistent_settings->GetPersistentSettingAsBool("key", false));
-
- delete persistent_settings;
}
} // namespace persistent_storage
diff --git a/cobalt/renderer/glimp_shaders/glsl/fragment_skia_color_texture_14.glsl b/cobalt/renderer/glimp_shaders/glsl/fragment_skia_color_texture_14.glsl
new file mode 100644
index 0000000..936209f
--- /dev/null
+++ b/cobalt/renderer/glimp_shaders/glsl/fragment_skia_color_texture_14.glsl
@@ -0,0 +1,29 @@
+#version 100
+
+precision mediump float;
+precision mediump sampler2D;
+uniform mediump vec4 uColor_Stage0;
+uniform mediump vec4 ucolor_Stage1;
+uniform sampler2D uTextureSampler_0_Stage0;
+varying highp vec2 vTextureCoords_Stage0;
+varying highp float vTexIndex_Stage0;
+void main() {
+ mediump vec4 outputColor_Stage0;
+ {
+ outputColor_Stage0 = uColor_Stage0;
+ mediump vec4 texColor;
+ {
+ texColor = texture2D(uTextureSampler_0_Stage0, vTextureCoords_Stage0);
+ }
+ outputColor_Stage0 = outputColor_Stage0 * texColor;
+ }
+ mediump vec4 output_Stage1;
+ {
+ {
+ output_Stage1 = outputColor_Stage0 * ucolor_Stage1;
+ }
+ }
+ {
+ gl_FragColor = output_Stage1;
+ }
+}
diff --git a/cobalt/renderer/rasterizer/egl/textured_mesh_renderer.cc b/cobalt/renderer/rasterizer/egl/textured_mesh_renderer.cc
index 0393771..40bfa2f 100644
--- a/cobalt/renderer/rasterizer/egl/textured_mesh_renderer.cc
+++ b/cobalt/renderer/rasterizer/egl/textured_mesh_renderer.cc
@@ -164,6 +164,13 @@
GL_TEXTURE_WRAP_S, GL_REPEAT));
}
+ if (image.type == Image::YUV_3PLANE_10BIT_COMPACT_BT2020) {
+ GL_CALL(
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST));
+ GL_CALL(
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST));
+ }
+
GL_CALL(glUniform1i(blit_program.texture_uniforms[i], i));
if (blit_program.texture_size_uniforms[i] != -1) {
@@ -190,16 +197,26 @@
SB_DCHECK(image.num_textures() >= 1);
math::Size texture_size = image.textures[0].texture->GetSize();
- GL_CALL(glUniform2f(
- blit_program.viewport_to_texture_size_ratio_uniform,
- viewport_size.width() / static_cast<float>(texture_size.width()),
- viewport_size.height() / static_cast<float>(texture_size.height())));
-
// The pixel shader requires the actual frame size of the first plane
// for calculations.
size_t subtexture_width = (texture_size.width() + 2) / 3;
GL_CALL(glUniform2f(blit_program.subtexture_size_uniform, subtexture_width,
texture_size.height()));
+
+ const int kCompactTextureFilteringThreshold = 1920;
+ int enable_filtering = (abs(image.textures[0].content_region.width()) <
+ kCompactTextureFilteringThreshold)
+ ? 1
+ : 0;
+ GL_CALL(
+ glUniform1i(blit_program.enable_filtering_uniform, enable_filtering));
+
+ GL_CALL(glUniform2f(blit_program.content_region_size_uniform,
+ abs(image.textures[0].content_region.width()),
+ abs(image.textures[0].content_region.height())));
+
+ GL_CALL(glUniform2f(blit_program.texture_size_as_unpacked_uniform,
+ texture_size.width(), texture_size.height()));
}
GL_CALL(glDrawArrays(mode, 0, num_vertices));
@@ -514,7 +531,9 @@
"uniform sampler2D texture_u;"
"uniform sampler2D texture_v;"
"uniform vec2 viewport_size;"
- "uniform vec2 viewport_to_texture_ratio;"
+ "uniform vec2 texture_size_as_unpacked;"
+ "uniform vec2 content_region_size;"
+ "uniform int enable_filtering;"
"uniform vec2 texture_size;"
"uniform mat4 to_rgb_color_matrix;"
// In a compacted YUV image each channel, Y, U and V, is stored as a
@@ -526,37 +545,95 @@
// decompacted_position = compacted_position / 3;
// pixel = CompactedTexture.Sample(Sampler, decompacted_position)[index];
"void main() {"
- // Take into account display size to texture size ratio for Y component.
- "vec2 DTid = vec2(floor(v_tex_coord_y * viewport_size));"
- "vec2 compact_pos_Y = vec2((DTid + 0.5) / viewport_to_texture_ratio);"
- // Calculate the position of 10-bit pixel for Y.
- "vec2 decompact_pos_Y;"
- "decompact_pos_Y.x = (floor(compact_pos_Y.x / 3.0) + 0.5) / "
- "texture_size.x;"
- "decompact_pos_Y.y = compact_pos_Y.y / texture_size.y;"
- // Calculate the index of 10-bit pixel for Y.
- "int index_Y = int(mod(floor(compact_pos_Y.x), 3.0));"
- // Extract Y component.
- "float Y_component = texture2D(texture_y, decompact_pos_Y)[index_Y];"
- // For yuv420 U and V textures have dimensions twice less than Y.
- "DTid = vec2(DTid / 2.0);"
- "vec2 texture_size_UV = vec2(texture_size / 2.0);"
- "vec2 compact_pos_UV = vec2((DTid + 0.5) / viewport_to_texture_ratio);"
- // Calculate the position of 10-bit pixels for U and V.
- "vec2 decompact_pos_UV;"
- "decompact_pos_UV.x = (floor(compact_pos_UV.x / 3.0) + 0.5) / "
- "texture_size_UV.x;"
- "decompact_pos_UV.y = (floor(compact_pos_UV.y) + 0.5) / "
- "texture_size_UV.y;"
- // Calculate the index of 10-bit pixels for U and V.
- "int index_UV = int(mod(floor(compact_pos_UV), 3.0));"
- // Extract U and V components.
- "float U_component = texture2D(texture_u, decompact_pos_UV)[index_UV];"
- "float V_component = texture2D(texture_v, decompact_pos_UV)[index_UV];"
- // Perform the YUV->RGB transform and output the color value.
- "vec4 untransformed_color = vec4(Y_component, U_component, V_component, "
- "1.0);"
- "gl_FragColor = untransformed_color * to_rgb_color_matrix;"
+ // texture size as unpacked, f.i. 2562x1440
+ " float frame_w = texture_size_as_unpacked.x; \n"
+ " float frame_h = texture_size_as_unpacked.y; \n"
+ // texture size in textels, f.i. 854x1440
+ " float ptex_w = texture_size.x;\n"
+ " float ptex_h = texture_size.y;\n"
+ " float y_step = 1.0 / ptex_h;\n"
+ " float w_mult = 1.0 / ptex_w;\n"
+ " float y = (enable_filtering != 0) ?\n"
+ " floor(v_tex_coord_y.y * frame_h) * y_step :\n"
+ " v_tex_coord_y.y;\n"
+ " float x = (frame_w - 1.0) * v_tex_coord_y.x;\n"
+ " float xi = floor(x);\n"
+ " float xcoord = floor(xi * (1.0 / 3.0));\n"
+ " int xcoord_fr = int(floor(xi - xcoord * 3.0));\n"
+ " xcoord *= w_mult;\n"
+ " float Y_component = texture2D(texture_y, vec2(xcoord, "
+ "y))[xcoord_fr];\n"
+ " if (enable_filtering != 0)\n"
+ " {\n"
+ " float cx1 = x - xi;\n"
+ " float cy1 = (v_tex_coord_y.y - y) * frame_h;\n"
+ " float cx0 = 1.0 - cx1;\n"
+ " float cy0 = 1.0 - cy1;\n"
+ " Y_component *= cx0 * cy0;\n"
+ " float y2 = min(y + y_step, y_step * (content_region_size.y - "
+ "1.0));\n"
+ " Y_component += cx0 * cy1 * texture2D(texture_y, "
+ "vec2(xcoord, y2))[xcoord_fr];\n"
+ " xi = min(xi + 1.0, float(content_region_size.x - 1.0));\n"
+ " xcoord = floor(xi * (1.0 / 3.0));\n"
+ " xcoord_fr = int(floor(xi - xcoord * 3.0));\n"
+ " xcoord *= w_mult;\n"
+ " Y_component += cx1 * cy0 * texture2D(texture_y, vec2(xcoord, "
+ "y))[xcoord_fr];\n"
+ " Y_component += cx1 * cy1 * texture2D(texture_y, vec2(xcoord, "
+ "y2))[xcoord_fr];\n"
+ " }\n"
+ " //chroma:\n"
+ " float f_w = frame_w / 2.0;\n"
+ " float f_h = frame_h / 2.0;\n"
+ " y_step = 1.0 / (ptex_h / 2.0);\n"
+ " w_mult = 1.0 / (ptex_w / 2.0);\n"
+ " y = (enable_filtering != 0) ?\n"
+ " floor(v_tex_coord_y.y * f_h) * y_step :\n"
+ " v_tex_coord_y.y;\n"
+ " x = (f_w - 1.0) * v_tex_coord_y.x;\n"
+ " xi = floor(x);\n"
+ " xcoord = floor(xi * (1.0 / 3.0));\n"
+ " xcoord_fr = int(floor(xi - xcoord * 3.0));\n"
+ " xcoord *= w_mult;\n"
+ " float U_component = texture2D(texture_u, vec2(xcoord, "
+ "y))[xcoord_fr];\n"
+ " float V_component = texture2D(texture_v, vec2(xcoord, "
+ "y))[xcoord_fr];\n"
+ " if (enable_filtering != 0)\n"
+ " {\n"
+ " float cx1 = x - xi;\n"
+ " float cy1 = (v_tex_coord_y.y - y) * f_h;\n"
+ " float coef0 = (1.0 - cx1) * (1.0 - cy1);\n"
+ " float coef2 = cx1 * (1.0 - cy1);\n"
+ " float coef1 = (1.0 - cx1) * cy1;\n"
+ " float coef3 = cx1 * cy1;\n"
+ " U_component *= coef0;\n"
+ " V_component *= coef0;\n"
+ " float y2 = min(y + y_step, y_step * (content_region_size.y / "
+ "2.0 - "
+ "1.0));\n"
+ " U_component += coef1 * texture2D(texture_u, vec2(xcoord, "
+ "y2))[xcoord_fr];\n"
+ " V_component += coef1 * texture2D(texture_v, vec2(xcoord, "
+ "y2))[xcoord_fr];\n"
+ " xi = min(xi + 1.0, float(content_region_size.x / 2.0 - "
+ "1.0));\n"
+ " xcoord = floor(xi * (1.0 / 3.0));\n"
+ " xcoord_fr = int(floor(xi - xcoord * 3.0));\n"
+ " xcoord *= w_mult;\n"
+ " U_component += coef2 * texture2D(texture_u, vec2(xcoord, "
+ "y))[xcoord_fr];\n"
+ " V_component += coef2 * texture2D(texture_v, vec2(xcoord, "
+ "y))[xcoord_fr];\n"
+ " U_component += coef3 * texture2D(texture_u, vec2(xcoord, "
+ "y2))[xcoord_fr];\n"
+ " V_component += coef3 * texture2D(texture_v, vec2(xcoord, "
+ "y2))[xcoord_fr];\n"
+ " }\n"
+ " vec4 untransformed_color = vec4(Y_component, U_component, "
+ "V_component, 1.0);\n"
+ " gl_FragColor = untransformed_color * to_rgb_color_matrix;\n"
"}";
return CompileShader(blit_fragment_shader_source);
@@ -624,13 +701,19 @@
result.viewport_size_uniform = GL_CALL_SIMPLE(
glGetUniformLocation(result.gl_program_id, "viewport_size"));
DCHECK_EQ(GL_NO_ERROR, GL_CALL_SIMPLE(glGetError()));
- result.viewport_to_texture_size_ratio_uniform =
- GL_CALL_SIMPLE(glGetUniformLocation(result.gl_program_id,
- "viewport_to_texture_ratio"));
- DCHECK_EQ(GL_NO_ERROR, GL_CALL_SIMPLE(glGetError()));
result.subtexture_size_uniform = GL_CALL_SIMPLE(
glGetUniformLocation(result.gl_program_id, "texture_size"));
DCHECK_EQ(GL_NO_ERROR, GL_CALL_SIMPLE(glGetError()));
+ result.texture_size_as_unpacked_uniform =
+ GL_CALL_SIMPLE(glGetUniformLocation(result.gl_program_id,
+ "texture_size_as_unpacked"));
+ DCHECK_EQ(GL_NO_ERROR, GL_CALL_SIMPLE(glGetError()));
+ result.enable_filtering_uniform = GL_CALL_SIMPLE(
+ glGetUniformLocation(result.gl_program_id, "enable_filtering"));
+ DCHECK_EQ(GL_NO_ERROR, GL_CALL_SIMPLE(glGetError()));
+ result.content_region_size_uniform = GL_CALL_SIMPLE(
+ glGetUniformLocation(result.gl_program_id, "content_region_size"));
+ DCHECK_EQ(GL_NO_ERROR, GL_CALL_SIMPLE(glGetError()));
}
}
diff --git a/cobalt/renderer/rasterizer/egl/textured_mesh_renderer.h b/cobalt/renderer/rasterizer/egl/textured_mesh_renderer.h
index 2faac78..8d70738 100644
--- a/cobalt/renderer/rasterizer/egl/textured_mesh_renderer.h
+++ b/cobalt/renderer/rasterizer/egl/textured_mesh_renderer.h
@@ -127,7 +127,9 @@
int32 texture_size_uniforms[3];
uint32 gl_program_id;
int32 viewport_size_uniform;
- int32 viewport_to_texture_size_ratio_uniform;
+ int32 enable_filtering_uniform;
+ int32 content_region_size_uniform;
+ int32 texture_size_as_unpacked_uniform;
int32 subtexture_size_uniform;
};
// We key each program off of their GL texture type and image type.
diff --git a/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontConfigParser_cobalt.cc b/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontConfigParser_cobalt.cc
index 67fe50a..e37e258 100644
--- a/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontConfigParser_cobalt.cc
+++ b/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontConfigParser_cobalt.cc
@@ -14,6 +14,8 @@
#include "cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontConfigParser_cobalt.h"
+#include <ft2build.h>
+#include FT_TYPES_H
#include <libxml/parser.h>
#include <limits>
#include <memory>
@@ -339,8 +341,12 @@
DCHECK(file != NULL);
// A <font> may have following attributes:
- // weight (non-negative integer), style (normal, italic), font_name (string),
- // postscript_name (string), and index (non-negative integer)
+ // - weight (non-negative integer)
+ // - style (normal, italic)
+ // - font_name (string),
+ // - postscript_name (string)
+ // - index (non-negative integer)
+ // - tags (non-negative integer) [if using font variations]
// The element should contain a filename.
for (size_t i = 0; attributes[i] != NULL && attributes[i + 1] != NULL;
@@ -378,6 +384,23 @@
continue;
}
break;
+ case 8:
+ if (strncmp("tag_", name, 4) == 0) {
+ // The remaining 4 characters define the tag.
+ FontFileInfo::VariationCoordinate coord;
+ coord.tag = FT_MAKE_TAG(name[4], name[5], name[6], name[7]);
+ int axis_value;
+ if (ParseNonNegativeInteger(value, &axis_value)) {
+ // Convert axis_value to Fixed16.
+ coord.value = axis_value << 16;
+ file->variation_position.push_back(coord);
+ } else {
+ LOG(ERROR) << "---- Invalid tag (" << name << ") value [" << value
+ << "]";
+ NOTREACHED();
+ }
+ }
+ break;
case 9:
if (strncmp("font_name", name, 9) == 0) {
SkAutoAsciiToLC to_lowercase(value);
diff --git a/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontMgr_cobalt.cc b/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontMgr_cobalt.cc
index 7d694af..ee32ab1 100644
--- a/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontMgr_cobalt.cc
+++ b/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontMgr_cobalt.cc
@@ -282,7 +282,7 @@
// To pre-fetch glyphs for remote fonts, we could pass character_map here.
if (!sk_freetype_cobalt::ScanFont(stream.get(), face_index, &name, &style,
- &is_fixed_pitch, nullptr)) {
+ &is_fixed_pitch, nullptr, nullptr)) {
return NULL;
}
scoped_refptr<font_character_map::CharacterMap> character_map =
diff --git a/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontStyleSet_cobalt.cc b/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontStyleSet_cobalt.cc
index 00ffc9c..136b2e3 100644
--- a/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontStyleSet_cobalt.cc
+++ b/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontStyleSet_cobalt.cc
@@ -70,6 +70,49 @@
return score;
}
+
+// Calculate the variation design coordinates for use with
+// FT_Set_Var_Design_Coordinates or passed to SkFontData.
+bool ComputeVariationPosition(
+ const sk_freetype_cobalt::AxisDefinitions& axis_definitions,
+ const FontFileInfo::VariationPosition& variation_position,
+ SkFontStyleSet_Cobalt::ComputedVariationPosition* out_position) {
+ if (variation_position.count() > axis_definitions.count()) {
+ // More variation axes were specified than actually supported.
+ return false;
+ }
+
+ int positions_used = 0;
+ out_position->resize(axis_definitions.count());
+ for (int axis = 0; axis < axis_definitions.count(); ++axis) {
+ // See if this axis has a specified position. If not, then use the default
+ // value from the axis definitions.
+ Fixed16 value = axis_definitions[axis].def;
+ for (int pos = 0; pos < variation_position.count(); ++pos) {
+ if (variation_position[pos].tag == axis_definitions[axis].tag) {
+ value = variation_position[pos].value;
+ DCHECK(value >= axis_definitions[axis].minimum);
+ DCHECK(value <= axis_definitions[axis].maximum);
+ if (value < axis_definitions[axis].minimum ||
+ value > axis_definitions[axis].maximum) {
+ return false;
+ }
+ ++positions_used;
+ break;
+ }
+ }
+ (*out_position)[axis] = value;
+ }
+
+ if (positions_used != variation_position.count()) {
+ // Some axes were specified which aren't supported.
+ LOG(ERROR) << "Mismatched font variation tags specified";
+ return false;
+ }
+
+ return true;
+}
+
} // namespace
SkFontStyleSet_Cobalt::SkFontStyleSet_Cobalt(
@@ -94,16 +137,91 @@
family_name_ = family_info.names[0];
SkTHashMap<SkString, int> styles_index_map;
+ // These structures support validation of entries for font variations.
+ SkTHashMap<SkString, sk_freetype_cobalt::AxisDefinitions>
+ variation_definitions;
+ ComputedVariationPosition computed_variation_position;
+ sk_freetype_cobalt::AxisDefinitions axis_definitions;
+
+ // Avoid expensive alloc / dealloc calls with SkTArray by reserving size and
+ // using resize(0) instead of reset(). Adobe multiple master fonts are limited
+ // to 4 axes, and although OpenType font variations may have more, they tend
+ // not to -- it's usually just weight, width, and slant. But just in case,
+ // use a relatively high reservation.
+ computed_variation_position.reserve(16);
+ axis_definitions.reserve(16);
+
for (int i = 0; i < family_info.fonts.count(); ++i) {
const FontFileInfo& font_file = family_info.fonts[i];
SkString file_path(SkOSPath::Join(base_path, font_file.file_name.c_str()));
- // Validate that the file exists at this location. If it does not, then skip
- // over it; it isn't being added to the set.
- if (!sk_exists(file_path.c_str(), kRead_SkFILE_Flag)) {
- DLOG(INFO) << "Failed to find font file: " << file_path.c_str();
- continue;
+ // Validate the font file entry.
+ if (font_file.variation_position.empty()) {
+ // Static font files only need to check for file existence.
+ if (!sk_exists(file_path.c_str(), kRead_SkFILE_Flag)) {
+ DLOG(INFO) << "Failed to find static font file: " << file_path.c_str();
+ continue;
+ }
+ computed_variation_position.resize(0);
+ } else {
+ // Need to scan the font file to verify variation parameters. To improve
+ // performance, cache the scan results.
+
+ // In case axis definition changes based on font index, include font
+ // index in the key.
+ SkString cache_key(font_file.file_name);
+ char cache_key_suffix[3] = {':', static_cast<char>(font_file.index + 'A'),
+ 0};
+ cache_key.append(cache_key_suffix);
+
+ sk_freetype_cobalt::AxisDefinitions* cached_axis_definitions =
+ variation_definitions.find(cache_key);
+
+ if (cached_axis_definitions == nullptr) {
+ // Scan the font file to get the variation information (if any).
+ if (!sk_exists(file_path.c_str(), kRead_SkFILE_Flag)) {
+ // Add an empty axis definition list as placeholder. This will
+ // be detected as an incompatible font file.
+ axis_definitions.resize(0);
+ } else {
+ DLOG(INFO) << "Scanning variable font file: " << file_path.c_str();
+
+ // Create a stream to scan the font. Since these fonts may not ever
+ // be used at runtime, purge stream's chunks after scanning to avoid
+ // memory bloat.
+ SkFileMemoryChunkStreamProvider* stream_provider =
+ local_typeface_stream_manager_->GetStreamProvider(
+ file_path.c_str());
+ std::unique_ptr<const SkFileMemoryChunks> memory_chunks_snapshot(
+ stream_provider->CreateMemoryChunksSnapshot());
+ std::unique_ptr<SkFileMemoryChunkStream> stream(
+ stream_provider->OpenStream());
+
+ SkString unused_face_name;
+ SkFontStyle unused_font_style;
+ bool unused_is_fixed_pitch;
+ if (!sk_freetype_cobalt::ScanFont(
+ stream.get(), font_file.index, &unused_face_name,
+ &unused_font_style, &unused_is_fixed_pitch, &axis_definitions,
+ nullptr)) {
+ axis_definitions.resize(0);
+ }
+
+ stream.reset(nullptr);
+ stream_provider->PurgeUnusedMemoryChunks();
+ }
+
+ cached_axis_definitions =
+ variation_definitions.set(cache_key, axis_definitions);
+ }
+
+ if (!ComputeVariationPosition(*cached_axis_definitions,
+ font_file.variation_position,
+ &computed_variation_position)) {
+ DLOG(INFO) << "Incompatible variable font file: " << file_path.c_str();
+ continue;
+ }
}
auto file_name = font_file.file_name.c_str();
@@ -149,10 +267,10 @@
font_name = SkString(full_font_name.c_str());
}
auto font = new SkFontStyleSetEntry_Cobalt(
- file_path, font_file.index, style, full_font_name, postscript_name,
- font_file.disable_synthetic_bolding);
+ file_path, font_file.index, computed_variation_position, style,
+ full_font_name, postscript_name, font_file.disable_synthetic_bolding);
int* index = styles_index_map.find(font_name);
- if (index != NULL) {
+ if (index != nullptr) {
// If style with name already exists in family, replace it.
if (font_format_setting == kTtfPreferred &&
SbStringCompareNoCase("ttf", extension) == 0) {
@@ -357,12 +475,19 @@
character_map = character_maps_[style_index].get();
}
+ SkFontStyle old_style = style->font_style;
if (!sk_freetype_cobalt::ScanFont(
stream, style->face_index, &style->face_name, &style->font_style,
- &style->face_is_fixed_pitch, character_map)) {
+ &style->face_is_fixed_pitch, nullptr, character_map)) {
return false;
}
+ if (style->computed_variation_position.count() > 0) {
+ // For font variations, use the font style parsed from fonts.xml rather than
+ // the style returned by ScanFont since that's just the default.
+ style->font_style = old_style;
+ }
+
style->is_face_info_generated = true;
return true;
}
@@ -372,6 +497,10 @@
int max_score = std::numeric_limits<int>::min();
for (int i = 0; i < styles_.count(); ++i) {
int score = MatchScore(pattern, styles_[i]->font_style);
+ if (styles_[i]->computed_variation_position.count() > 0) {
+ // Slightly prefer static fonts over font variations to maintain old look.
+ score -= 1;
+ }
if (score > max_score) {
closest_index = i;
max_score = score;
@@ -403,7 +532,8 @@
style_entry->typeface.reset(new SkTypeface_CobaltStreamProvider(
stream_provider, style_entry->face_index, style_entry->font_style,
style_entry->face_is_fixed_pitch, family_name_,
- style_entry->disable_synthetic_bolding, map));
+ style_entry->disable_synthetic_bolding,
+ style_entry->computed_variation_position, map));
} else {
LOG(ERROR) << "Failed to scan font: "
<< style_entry->font_file_path.c_str();
diff --git a/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontStyleSet_cobalt.h b/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontStyleSet_cobalt.h
index fb5d6d0..11b0241 100644
--- a/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontStyleSet_cobalt.h
+++ b/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontStyleSet_cobalt.h
@@ -40,19 +40,23 @@
// entry are lazily loaded the first time that they are needed.
class SkFontStyleSet_Cobalt : public SkFontStyleSet {
public:
+ typedef SkTArray<Fixed16, true> ComputedVariationPosition;
+
struct SkFontStyleSetEntry_Cobalt : public SkRefCnt {
// NOTE: SkFontStyleSetEntry_Cobalt objects are not guaranteed to last for
// the lifetime of SkFontMgr_Cobalt and can be removed by their owning
// SkFontStyleSet_Cobalt if their typeface fails to load properly. As a
// result, it is not safe to store their pointers outside of
// SkFontStyleSet_Cobalt.
- SkFontStyleSetEntry_Cobalt(const SkString& file_path, const int face_index,
- const SkFontStyle& style,
- const std::string& full_name,
- const std::string& postscript_name,
- const bool disable_synthetic_bolding)
+ SkFontStyleSetEntry_Cobalt(
+ const SkString& file_path, const int face_index,
+ const ComputedVariationPosition& computed_variation_position,
+ const SkFontStyle& style, const std::string& full_name,
+ const std::string& postscript_name,
+ const bool disable_synthetic_bolding)
: font_file_path(file_path),
face_index(face_index),
+ computed_variation_position(computed_variation_position),
font_style(style),
full_font_name(full_name),
font_postscript_name(postscript_name),
@@ -62,6 +66,7 @@
const SkString font_file_path;
const int face_index;
+ const ComputedVariationPosition computed_variation_position;
SkFontStyle font_style;
const std::string full_font_name;
const std::string font_postscript_name;
diff --git a/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontUtil_cobalt.h b/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontUtil_cobalt.h
index 6314e5a..95b7d56 100644
--- a/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontUtil_cobalt.h
+++ b/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFontUtil_cobalt.h
@@ -25,6 +25,9 @@
#include "base/basictypes.h"
#include "base/memory/ref_counted.h"
+// This mimics FT_Fixed which is a 16.16 fixed point format.
+typedef int32_t Fixed16;
+
// The font_character_map namespace contains all of the constants, typedefs, and
// utility functions used with determining whether or not a font supports a
// character.
@@ -133,6 +136,12 @@
};
typedef uint32_t FontStyle;
+ struct VariationCoordinate {
+ uint32_t tag;
+ Fixed16 value;
+ };
+ typedef SkTArray<VariationCoordinate, true> VariationPosition;
+
FontFileInfo()
: index(0),
weight(400),
@@ -144,6 +153,7 @@
int weight;
FontStyle style;
bool disable_synthetic_bolding;
+ VariationPosition variation_position;
SkString full_font_name;
SkString postscript_name;
diff --git a/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFreeType_cobalt.cc b/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFreeType_cobalt.cc
index cbcc362..75cd24c 100644
--- a/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFreeType_cobalt.cc
+++ b/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFreeType_cobalt.cc
@@ -16,6 +16,7 @@
#include <ft2build.h>
#include FT_FREETYPE_H
+#include FT_MULTIPLE_MASTERS_H
#include FT_TRUETYPE_TABLES_H
#include FT_TYPE1_TABLES_H
@@ -118,10 +119,11 @@
// These functions are used by FreeType during FT_Open_Face.
extern "C" {
-static unsigned long sk_freetype_cobalt_stream_io(FT_Stream ftStream,
- unsigned long offset,
- unsigned char* buffer,
- unsigned long count) {
+static unsigned long sk_freetype_cobalt_stream_io( // NOLINT(runtime/int)
+ FT_Stream ftStream,
+ unsigned long offset, // NOLINT(runtime/int)
+ unsigned char* buffer, // NOLINT(runtime/int)
+ unsigned long count) { // NOLINT(runtime/int)
SkStreamAsset* stream =
static_cast<SkStreamAsset*>(ftStream->descriptor.pointer);
stream->seek(offset);
@@ -134,7 +136,7 @@
namespace sk_freetype_cobalt {
bool ScanFont(SkStreamAsset* stream, int face_index, SkString* name,
- SkFontStyle* style, bool* is_fixed_pitch,
+ SkFontStyle* style, bool* is_fixed_pitch, AxisDefinitions* axes,
font_character_map::CharacterMap* maybe_character_map /*=NULL*/) {
TRACE_EVENT0("cobalt::renderer", "SkFreeTypeUtil::ScanFont()");
@@ -166,6 +168,27 @@
*style = GenerateSkFontStyleFromFace(face);
*is_fixed_pitch = FT_IS_FIXED_WIDTH(face);
+ if (axes && FT_HAS_MULTIPLE_MASTERS(face)) {
+ FT_MM_Var* variations = nullptr;
+ err = FT_Get_MM_Var(face, &variations);
+ if (err) {
+ LOG(INFO) << "Unable to get variations for " << name;
+ } else {
+ axes->resize(variations->num_axis);
+ for (int i = 0; i < variations->num_axis; ++i) {
+ const FT_Var_Axis& ft_axis = variations->axis[i];
+ AxisDefinition& cobalt_axis = (*axes)[i];
+ cobalt_axis.tag = ft_axis.tag;
+ // The following values are already in 16.16 format, so no need to
+ // convert to Fixed16.
+ cobalt_axis.minimum = ft_axis.minimum;
+ cobalt_axis.def = ft_axis.def;
+ cobalt_axis.maximum = ft_axis.maximum;
+ }
+ FT_Done_MM_Var(freetype_lib, variations);
+ }
+ }
+
if (maybe_character_map) {
GenerateCharacterMapFromFace(face, maybe_character_map);
}
diff --git a/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFreeType_cobalt.h b/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFreeType_cobalt.h
index 0f4c3a5..8eb43c7 100644
--- a/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFreeType_cobalt.h
+++ b/cobalt/renderer/rasterizer/skia/skia/src/ports/SkFreeType_cobalt.h
@@ -21,11 +21,20 @@
namespace sk_freetype_cobalt {
+// This contains information from FT_Var_Axis.
+struct AxisDefinition {
+ uint32_t tag;
+ Fixed16 minimum;
+ Fixed16 def;
+ Fixed16 maximum;
+};
+typedef SkTArray<AxisDefinition, true> AxisDefinitions;
+
// Scans the font stream using FreeType, setting its name, style and whether or
// not it has a fixed pitch. It also generates the font's character map if that
// optional parameter is provided.
bool ScanFont(SkStreamAsset* stream, int face_index, SkString* name,
- SkFontStyle* style, bool* is_fixed_pitch,
+ SkFontStyle* style, bool* is_fixed_pitch, AxisDefinitions* axes,
font_character_map::CharacterMap* maybe_character_map = NULL);
} // namespace sk_freetype_cobalt
diff --git a/cobalt/renderer/rasterizer/skia/skia/src/ports/SkTypeface_cobalt.cc b/cobalt/renderer/rasterizer/skia/skia/src/ports/SkTypeface_cobalt.cc
index 837ce75..eb6f55e 100644
--- a/cobalt/renderer/rasterizer/skia/skia/src/ports/SkTypeface_cobalt.cc
+++ b/cobalt/renderer/rasterizer/skia/skia/src/ports/SkTypeface_cobalt.cc
@@ -64,6 +64,17 @@
*family_name = family_name_;
}
+std::unique_ptr<SkFontData> SkTypeface_Cobalt::onMakeFontData() const {
+ int index;
+ std::unique_ptr<SkStreamAsset> stream(this->onOpenStream(&index));
+ if (!stream) {
+ return nullptr;
+ }
+ return std::make_unique<SkFontData>(std::move(stream), index,
+ computed_variation_position_.data(),
+ computed_variation_position_.count());
+}
+
SkTypeface_CobaltStream::SkTypeface_CobaltStream(
std::unique_ptr<SkStreamAsset> stream, int face_index, SkFontStyle style,
bool is_fixed_pitch, const SkString& family_name,
@@ -93,31 +104,23 @@
return stream_->getLength();
}
-#ifdef USE_SKIA_NEXT
-std::unique_ptr<SkFontData> SkTypeface_CobaltStream::onMakeFontData() const {
- int index;
- std::unique_ptr<SkStreamAsset> stream(this->onOpenStream(&index));
- if (!stream) {
- return nullptr;
- }
- return std::make_unique<SkFontData>(std::move(stream), index, nullptr, 0);
-}
-#endif
-
SkTypeface_CobaltStreamProvider::SkTypeface_CobaltStreamProvider(
SkFileMemoryChunkStreamProvider* stream_provider, int face_index,
SkFontStyle style, bool is_fixed_pitch, const SkString& family_name,
bool disable_synthetic_bolding,
+ const ComputedVariationPosition& computed_variation_position,
scoped_refptr<font_character_map::CharacterMap> character_map)
: INHERITED(face_index, style, is_fixed_pitch, family_name, character_map),
stream_provider_(stream_provider) {
if (disable_synthetic_bolding) {
synthesizes_bold_ = false;
}
- LOG(INFO) << "Created SkTypeface_CobaltStreamProvider: "
- << family_name.c_str() << "(" << style.weight() << ", "
- << style.width() << ", " << style.slant() << "); File: \""
- << stream_provider->file_path() << "\"";
+ computed_variation_position_ = computed_variation_position;
+ LOG(INFO) << "Created " << font_type_string()
+ << " SkTypeface_CobaltStreamProvider: " << family_name.c_str()
+ << "(" << style.weight() << ", " << style.width() << ", "
+ << style.slant() << "); File: \"" << stream_provider->file_path()
+ << "\"";
}
void SkTypeface_CobaltStreamProvider::onGetFontDescriptor(
@@ -143,15 +146,3 @@
stream_provider_->OpenStream());
return stream->getLength();
}
-
-#ifdef USE_SKIA_NEXT
-std::unique_ptr<SkFontData> SkTypeface_CobaltStreamProvider::onMakeFontData()
- const {
- int index;
- std::unique_ptr<SkStreamAsset> stream(this->onOpenStream(&index));
- if (!stream) {
- return nullptr;
- }
- return std::make_unique<SkFontData>(std::move(stream), index, nullptr, 0);
-}
-#endif
diff --git a/cobalt/renderer/rasterizer/skia/skia/src/ports/SkTypeface_cobalt.h b/cobalt/renderer/rasterizer/skia/skia/src/ports/SkTypeface_cobalt.h
index f5ede9c..3b1cfcd 100644
--- a/cobalt/renderer/rasterizer/skia/skia/src/ports/SkTypeface_cobalt.h
+++ b/cobalt/renderer/rasterizer/skia/skia/src/ports/SkTypeface_cobalt.h
@@ -28,6 +28,9 @@
class SkTypeface_Cobalt : public SkTypeface_FreeType {
public:
+ typedef SkFontStyleSet_Cobalt::ComputedVariationPosition
+ ComputedVariationPosition;
+
SkTypeface_Cobalt(
int face_index, SkFontStyle style, bool is_fixed_pitch,
const SkString& family_name,
@@ -45,10 +48,20 @@
void onGetFamilyName(SkString* family_name) const override;
+ std::unique_ptr<SkFontData> onMakeFontData() const override;
+
+ const char* font_type_string() const {
+ return computed_variation_position_.empty() ? "static" : "variable";
+ }
+
int face_index_;
SkString family_name_;
bool synthesizes_bold_;
+ // Support font variations. Axis data should already be sorted to match
+ // the order defined in the font file.
+ ComputedVariationPosition computed_variation_position_;
+
private:
typedef SkTypeface_FreeType INHERITED;
SkGlyphID characterMapGetGlyphIdForCharacter(SkUnichar character) const;
@@ -66,9 +79,6 @@
bool* serialize) const override;
std::unique_ptr<SkStreamAsset> onOpenStream(int* face_index) const override;
-#ifdef USE_SKIA_NEXT
- std::unique_ptr<SkFontData> onMakeFontData() const override;
-#endif
size_t GetStreamLength() const override;
@@ -84,15 +94,13 @@
SkFileMemoryChunkStreamProvider* stream_provider, int face_index,
SkFontStyle style, bool is_fixed_pitch, const SkString& family_name,
bool disable_synthetic_bolding,
+ const ComputedVariationPosition& computed_variation_position,
scoped_refptr<font_character_map::CharacterMap> character_map);
void onGetFontDescriptor(SkFontDescriptor* descriptor,
bool* serialize) const override;
std::unique_ptr<SkStreamAsset> onOpenStream(int* face_index) const override;
-#ifdef USE_SKIA_NEXT
- std::unique_ptr<SkFontData> onMakeFontData() const override;
-#endif
size_t GetStreamLength() const override;
diff --git a/cobalt/script/array_buffer.h b/cobalt/script/array_buffer.h
index 1bbaf7a..2618084 100644
--- a/cobalt/script/array_buffer.h
+++ b/cobalt/script/array_buffer.h
@@ -16,7 +16,7 @@
#define COBALT_SCRIPT_ARRAY_BUFFER_H_
#include <algorithm>
-#include <memory>
+#include <utility>
#include "base/logging.h"
#include "base/memory/ref_counted.h"
@@ -49,9 +49,8 @@
// Create a new |ArrayBuffer| from existing block of memory. See
// |PreallocatedArrayBufferData| for details.
- static Handle<ArrayBuffer> New(
- GlobalEnvironment* global_environment,
- std::unique_ptr<PreallocatedArrayBufferData> data);
+ static Handle<ArrayBuffer> New(GlobalEnvironment* global_environment,
+ PreallocatedArrayBufferData&& data);
virtual ~ArrayBuffer() {}
@@ -72,10 +71,10 @@
// PreallocatedArrayBufferData data(16);
//
// // Manipulate the data however you want.
-// static_cast<uint8_t*>(data.data())[0] = 0xFF;
+// data.data()[0] = 0xFF;
//
// // Create a new |ArrayBuffer| using |data|.
-// auto array_buffer = ArrayBuffer::New(env, &data);
+// auto array_buffer = ArrayBuffer::New(env, std::move(data));
//
// // |PreallocatedData| now no longer holds anything, data should now be
// // accessed from the ArrayBuffer itself.
@@ -86,12 +85,14 @@
explicit PreallocatedArrayBufferData(size_t byte_length);
~PreallocatedArrayBufferData();
- PreallocatedArrayBufferData(PreallocatedArrayBufferData&& other) = default;
+ PreallocatedArrayBufferData(PreallocatedArrayBufferData&& other) {
+ other.Detach(&data_, &byte_length_);
+ }
PreallocatedArrayBufferData& operator=(PreallocatedArrayBufferData&& other) =
- default;
+ delete;
- void* data() { return data_; }
- const void* data() const { return data_; }
+ uint8_t* data() { return data_; }
+ const uint8_t* data() const { return data_; }
size_t byte_length() const { return byte_length_; }
void Swap(PreallocatedArrayBufferData* that) {
@@ -106,7 +107,7 @@
PreallocatedArrayBufferData(const PreallocatedArrayBufferData&) = delete;
void operator=(const PreallocatedArrayBufferData&) = delete;
- void Detach(void** data, size_t* byte_length) {
+ void Detach(uint8_t** data, size_t* byte_length) {
DCHECK(data);
DCHECK(byte_length);
@@ -117,7 +118,7 @@
byte_length_ = 0u;
}
- void* data_ = nullptr;
+ uint8_t* data_ = nullptr;
size_t byte_length_ = 0u;
friend ArrayBuffer;
diff --git a/cobalt/script/v8c/v8c_array_buffer.cc b/cobalt/script/v8c/v8c_array_buffer.cc
index 835c320..939ea56 100644
--- a/cobalt/script/v8c/v8c_array_buffer.cc
+++ b/cobalt/script/v8c/v8c_array_buffer.cc
@@ -23,7 +23,7 @@
namespace script {
PreallocatedArrayBufferData::PreallocatedArrayBufferData(size_t byte_length) {
- data_ = SbMemoryAllocate(byte_length);
+ data_ = static_cast<uint8_t*>(SbMemoryAllocate(byte_length));
byte_length_ = byte_length;
}
@@ -36,7 +36,10 @@
}
void PreallocatedArrayBufferData::Resize(size_t new_byte_length) {
- data_ = SbMemoryReallocate(data_, new_byte_length);
+ if (byte_length_ == new_byte_length) {
+ return;
+ }
+ data_ = static_cast<uint8_t*>(SbMemoryReallocate(data_, new_byte_length));
byte_length_ = new_byte_length;
}
@@ -56,19 +59,18 @@
}
// static
-Handle<ArrayBuffer> ArrayBuffer::New(
- GlobalEnvironment* global_environment,
- std::unique_ptr<PreallocatedArrayBufferData> data) {
+Handle<ArrayBuffer> ArrayBuffer::New(GlobalEnvironment* global_environment,
+ PreallocatedArrayBufferData&& data) {
auto* v8c_global_environment =
base::polymorphic_downcast<v8c::V8cGlobalEnvironment*>(
global_environment);
v8::Isolate* isolate = v8c_global_environment->isolate();
v8c::EntryScope entry_scope(isolate);
- void* buffer;
+ uint8_t* buffer;
size_t byte_length;
- data->Detach(&buffer, &byte_length);
+ data.Detach(&buffer, &byte_length);
v8::Local<v8::ArrayBuffer> array_buffer = v8::ArrayBuffer::New(
isolate, buffer, byte_length, v8::ArrayBufferCreationMode::kInternalized);
diff --git a/cobalt/site/docs/reference/starboard/gn-configuration.md b/cobalt/site/docs/reference/starboard/gn-configuration.md
index 4c72557..2fff0ac 100644
--- a/cobalt/site/docs/reference/starboard/gn-configuration.md
+++ b/cobalt/site/docs/reference/starboard/gn-configuration.md
@@ -13,8 +13,6 @@
| **`default_renderer_options_dependency`**<br><br> Override this value to adjust the default rasterizer setting for your platform.<br><br>The default value is `"//cobalt/renderer:default_options"`. |
| **`enable_account_manager`**<br><br> Set to true to enable H5vccAccountManager.<br><br>The default value is `false`. |
| **`enable_in_app_dial`**<br><br> Enables or disables the DIAL server that runs inside Cobalt. Note: Only enable if there's no system-wide DIAL support.<br><br>The default value is `false`. |
-| **`enable_sso`**<br><br> Set to true to enable H5vccSSO (Single Sign On).<br><br>The default value is `false`. |
-| **`enable_xhr_header_filtering`**<br><br> Set to true to enable filtering of HTTP headers before sending.<br><br>The default value is `false`. |
| **`executable_configs`**<br><br> Target-specific configurations for executable targets.<br><br>The default value is `[]`. |
| **`final_executable_type`**<br><br> The target type for executable targets. Allows changing the target type on platforms where the native code may require an additional packaging step (ex. Android).<br><br>The default value is `"executable"`. |
| **`gl_type`**<br><br> The source of EGL and GLES headers and libraries. Valid values (case and everything sensitive!):<ul><li><code>none</code> - No EGL + GLES implementation is available on this platform.<li><code>system_gles2</code> - Use the system implementation of EGL + GLES2. The headers and libraries must be on the system include and link paths.<li><code>glimp</code> - Cobalt's own EGL + GLES2 implementation. This requires a valid Glimp implementation for the platform.<li><code>angle</code> - A DirectX-to-OpenGL adaptation layer. This requires a valid ANGLE implementation for the platform.<br><br>The default value is `"system_gles2"`. |
@@ -31,6 +29,7 @@
| **`sb_enable_lib`**<br><br> Enables embedding Cobalt as a shared library within another app. This requires a 'lib' starboard implementation for the corresponding platform.<br><br>The default value is `false`. |
| **`sb_enable_opus_sse`**<br><br> Enables optimizations on SSE compatible platforms.<br><br>The default value is `true`. |
| **`sb_evergreen_compatible_enable_lite`**<br><br> Whether to adopt Evergreen Lite on the Evergreen compatible platform.<br><br>The default value is `false`. |
+| **`sb_evergreen_compatible_package`**<br><br> Whether to generate the whole package containing both Loader app and Cobalt core on the Evergreen compatible platform.<br><br>The default value is `false`. |
| **`sb_evergreen_compatible_use_libunwind`**<br><br> Whether to use the libunwind library on Evergreen compatible platform.<br><br>The default value is `false`. |
| **`sb_filter_based_player`**<br><br> Used to indicate that the player is filter based.<br><br>The default value is `true`. |
| **`sb_is_evergreen`**<br><br> Whether this is an Evergreen build.<br><br>The default value is `false`. |
diff --git a/cobalt/speech/sandbox/speech_sandbox.cc b/cobalt/speech/sandbox/speech_sandbox.cc
index 78e3353..56f46bd 100644
--- a/cobalt/speech/sandbox/speech_sandbox.cc
+++ b/cobalt/speech/sandbox/speech_sandbox.cc
@@ -77,7 +77,7 @@
const dom::DOMSettings::Options& dom_settings_options) {
std::unique_ptr<script::EnvironmentSettings> environment_settings(
new dom::DOMSettings(null_debugger_hooks_, kDOMMaxElementDepth, NULL,
- NULL, NULL, NULL, NULL, dom_settings_options));
+ NULL, NULL, NULL, dom_settings_options));
DCHECK(environment_settings);
speech_recognition_ = new SpeechRecognition(environment_settings.get());
diff --git a/cobalt/sso/BUILD.gn b/cobalt/sso/BUILD.gn
deleted file mode 100644
index b4efae6..0000000
--- a/cobalt/sso/BUILD.gn
+++ /dev/null
@@ -1,21 +0,0 @@
-# Copyright 2021 The Cobalt Authors. All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-source_set("sso") {
- has_pedantic_warnings = true
-
- sources = [ "sso_interface.h" ]
-
- deps = [ "//cobalt/base" ]
-}
diff --git a/cobalt/sso/sso_interface.h b/cobalt/sso/sso_interface.h
deleted file mode 100644
index 20f2b9c..0000000
--- a/cobalt/sso/sso_interface.h
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2017 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef COBALT_SSO_SSO_INTERFACE_H_
-#define COBALT_SSO_SSO_INTERFACE_H_
-
-#include <memory>
-#include <string>
-
-
-namespace cobalt {
-namespace sso {
-
-// Porters should inherit from this class, and implement the pure
-// virtual functions.
-class SsoInterface {
- public:
- virtual ~SsoInterface() {}
- virtual std::string getApiKey() const = 0;
- virtual std::string getOauthClientId() const = 0;
- virtual std::string getOauthClientSecret() const = 0;
-};
-
-// Porters should implement this function in the |starboard|
-// directory, and link the definition inside cobalt.
-std::unique_ptr<SsoInterface> CreateSSO();
-
-} // namespace sso
-} // namespace cobalt
-
-#endif // COBALT_SSO_SSO_INTERFACE_H_
diff --git a/cobalt/tools/automated_testing/cobalt_runner.py b/cobalt/tools/automated_testing/cobalt_runner.py
index efdcc06..b0de327 100644
--- a/cobalt/tools/automated_testing/cobalt_runner.py
+++ b/cobalt/tools/automated_testing/cobalt_runner.py
@@ -128,18 +128,20 @@
self.log_handler = log_handler
if log_file:
- self.log_file = open(log_file) # pylint: disable=consider-using-with
+ self.log_file = open(log_file, encoding='utf-8') # pylint: disable=consider-using-with
logging.basicConfig(stream=self.log_file, level=logging.INFO)
else:
self.log_file = sys.stdout
- self.url = url
+ if url:
+ self.url = url
self.target_params = target_params
self.success_message = success_message
- url_string = '--url=' + self.url
- if not self.target_params:
- self.target_params = [url_string]
- else:
- self.target_params.append(url_string)
+ if hasattr(self, 'url'):
+ url_string = '--url=' + self.url
+ if not self.target_params:
+ self.target_params = [url_string]
+ else:
+ self.target_params.append(url_string)
if self.launcher_params.target_params:
self.target_params.extend(self.launcher_params.target_params)
@@ -272,12 +274,12 @@
self.launcher_is_running = True
try:
self.WaitForStart()
- except KeyboardInterrupt:
+ except KeyboardInterrupt as e:
# potentially from _thread.interrupt_main(). We will treat as
# a timeout regardless.
self.Exit(should_fail=True)
- raise TimeoutException
+ raise TimeoutException from e
def __exit__(self, exc_type, exc_value, exc_traceback):
# The unittest module terminates with a SystemExit
@@ -329,7 +331,7 @@
def _StartWebdriver(self, port):
host, webdriver_port = self.launcher.GetHostAndPortGivenPort(port)
- url = 'http://{}:{}/'.format(host, webdriver_port)
+ url = f'http://{host}:{webdriver_port}/'
self.webdriver = self.selenium_webdriver_module.Remote(
url, COBALT_WEBDRIVER_CAPABILITIES)
self.webdriver.command_executor.set_timeout(WEBDRIVER_HTTP_TIMEOUT_SECONDS)
@@ -350,11 +352,14 @@
def _RunLauncher(self):
"""Thread run routine."""
try:
+ # Force a newline because unittest with verbosity=2 doesn't start on a new
+ # line.
+ sys.stderr.write('\n')
logging.info('Running launcher')
self.launcher.Run()
logging.info('Cobalt terminated.')
if not self.failed and self.success_message:
- print('{}\n'.format(self.success_message))
+ print(f'{self.success_message}\n')
logging.info('%s\n', self.success_message)
# pylint: disable=broad-except
except Exception as ex:
@@ -375,8 +380,7 @@
while True:
if retry_count >= EXECUTE_JAVASCRIPT_RETRY_LIMIT:
raise TimeoutException(
- 'Selenium element or window not found in {} tries'.format(
- EXECUTE_JAVASCRIPT_RETRY_LIMIT))
+ f'Selenium element or window not found in {retry_count} tries')
retry_count += 1
try:
result = self.webdriver.execute_script(js_code)
@@ -396,7 +400,7 @@
Returns:
Python object represented by the cval string
"""
- javascript_code = 'return h5vcc.cVal.getValue(\'{}\')'.format(cval_name)
+ javascript_code = f'return h5vcc.cVal.getValue(\'{cval_name}\')'
cval_string = self.ExecuteJavaScript(javascript_code)
if cval_string:
try:
@@ -421,7 +425,7 @@
Python dictionary of values indexed by the cval names provided.
"""
javascript_code_list = [
- 'h5vcc.cVal.getValue(\'{}\')'.format(name) for name in cval_name_list
+ f'h5vcc.cVal.getValue(\'{name}\')' for name in cval_name_list
]
javascript_code = 'return [' + ','.join(javascript_code_list) + ']'
json_results = self.ExecuteJavaScript(javascript_code)
@@ -478,7 +482,7 @@
# probably does.
if not self.UniqueFind(css_selector):
raise CobaltRunner.AssertException(
- 'Did not find selector: {}'.format(css_selector))
+ f'Did not find selector: {css_selector}')
def FindElements(self, css_selector, expected_num=None):
"""Finds elements based on a selector.
@@ -500,8 +504,7 @@
while True:
if retry_count >= FIND_ELEMENT_RETRY_LIMIT:
raise TimeoutException(
- 'Selenium element or window not found in {} tries'.format(
- retry_count))
+ f'Selenium element or window not found in {retry_count} tries')
retry_count += 1
try:
elements = self.webdriver.find_elements_by_css_selector(css_selector)
@@ -512,8 +515,8 @@
break
if expected_num and len(elements) != expected_num:
raise CobaltRunner.AssertException(
- 'Expected number of element {} is: {}, got {}'.format(
- css_selector, expected_num, len(elements)))
+ f'Expected number of element {css_selector} '
+ f'is: {expected_num}, got {len(elements)}')
return elements
def WaitForActiveElement(self):
@@ -522,7 +525,7 @@
while True:
if retry_count >= FIND_ELEMENT_RETRY_LIMIT:
raise TimeoutException(
- 'Selenium active element not found in {} tries'.format(retry_count))
+ f'Selenium active element not found in {retry_count} tries')
retry_count += 1
try:
element = self.webdriver.switch_to.active_element
diff --git a/cobalt/ui_navigation/interface.cc b/cobalt/ui_navigation/interface.cc
index d833d7d..da1f51f 100644
--- a/cobalt/ui_navigation/interface.cc
+++ b/cobalt/ui_navigation/interface.cc
@@ -22,25 +22,68 @@
namespace {
struct ItemImpl {
- explicit ItemImpl(NativeItemType type) : type(type) {}
+ explicit ItemImpl(NativeItemType type, const NativeCallbacks* callbacks,
+ void* callback_context)
+ : type(type), callbacks(callbacks), callback_context(callback_context) {}
starboard::SpinLock lock;
const NativeItemType type;
+ const NativeCallbacks* callbacks;
+ void* callback_context;
+ bool is_enabled = true;
+
float content_offset_x = 0.0f;
float content_offset_y = 0.0f;
+
+ float scroll_top_lower_bound = 0.0f;
+ float scroll_left_lower_bound = 0.0f;
+ float scroll_top_upper_bound = 0.0f;
+ float scroll_left_upper_bound = 0.0f;
};
NativeItem CreateItem(NativeItemType type, const NativeCallbacks* callbacks,
void* callback_context) {
- return reinterpret_cast<NativeItem>(new ItemImpl(type));
+ return reinterpret_cast<NativeItem>(
+ new ItemImpl(type, callbacks, callback_context));
}
void DestroyItem(NativeItem item) { delete reinterpret_cast<ItemImpl*>(item); }
+void SetItemBounds(NativeItem item, float scroll_top_lower_bound,
+ float scroll_left_lower_bound, float scroll_top_upper_bound,
+ float scroll_left_upper_bound) {
+ ItemImpl* stub_item = reinterpret_cast<ItemImpl*>(item);
+ if (stub_item->type == kNativeItemTypeContainer) {
+ starboard::ScopedSpinLock scoped_lock(stub_item->lock);
+ stub_item->scroll_top_lower_bound = scroll_top_lower_bound;
+ stub_item->scroll_left_lower_bound = scroll_left_lower_bound;
+ stub_item->scroll_top_upper_bound = scroll_top_upper_bound;
+ stub_item->scroll_left_upper_bound = scroll_left_upper_bound;
+ }
+}
+
+void GetItemBounds(NativeItem item, float* out_scroll_top_lower_bound,
+ float* out_scroll_left_lower_bound,
+ float* out_scroll_top_upper_bound,
+ float* out_scroll_left_upper_bound) {
+ ItemImpl* stub_item = reinterpret_cast<ItemImpl*>(item);
+ if (stub_item->type == kNativeItemTypeContainer) {
+ starboard::ScopedSpinLock scoped_lock(stub_item->lock);
+ *out_scroll_top_lower_bound = stub_item->scroll_top_lower_bound;
+ *out_scroll_left_lower_bound = stub_item->scroll_left_lower_bound;
+ *out_scroll_top_upper_bound = stub_item->scroll_top_upper_bound;
+ *out_scroll_left_upper_bound = stub_item->scroll_left_upper_bound;
+ }
+}
+
void SetFocus(NativeItem item) {}
-void SetItemEnabled(NativeItem item, bool enabled) {}
+void SetItemEnabled(NativeItem item, bool enabled) {
+ ItemImpl* stub_item = reinterpret_cast<ItemImpl*>(item);
+ starboard::ScopedSpinLock scoped_lock(stub_item->lock);
+ stub_item->is_enabled = enabled;
+}
void SetItemDir(NativeItem item, NativeItemDir dir) {}
@@ -65,10 +108,19 @@
void SetItemContentOffset(NativeItem item, float content_offset_x,
float content_offset_y) {
ItemImpl* stub_item = reinterpret_cast<ItemImpl*>(item);
- if (stub_item->type == kNativeItemTypeContainer) {
- starboard::ScopedSpinLock scoped_lock(stub_item->lock);
- stub_item->content_offset_x = content_offset_x;
- stub_item->content_offset_y = content_offset_y;
+ if (stub_item->type != kNativeItemTypeContainer) {
+ return;
+ }
+ starboard::ScopedSpinLock scoped_lock(stub_item->lock);
+ const bool scroll_changed = stub_item->content_offset_x != content_offset_x ||
+ stub_item->content_offset_y != content_offset_y;
+ if (!scroll_changed) {
+ return;
+ }
+ stub_item->content_offset_x = content_offset_x;
+ stub_item->content_offset_y = content_offset_y;
+ if (stub_item->is_enabled) {
+ stub_item->callbacks->onscroll(item, stub_item->callback_context);
}
}
@@ -90,6 +142,8 @@
if (SbUiNavGetInterface(&sb_ui_interface)) {
interface.create_item = sb_ui_interface.create_item;
interface.destroy_item = sb_ui_interface.destroy_item;
+ interface.set_item_bounds = nullptr;
+ interface.get_item_bounds = nullptr;
interface.set_focus = sb_ui_interface.set_focus;
interface.set_item_enabled = sb_ui_interface.set_item_enabled;
interface.set_item_dir = sb_ui_interface.set_item_dir;
@@ -115,6 +169,8 @@
interface.create_item = &CreateItem;
interface.destroy_item = &DestroyItem;
+ interface.set_item_bounds = &SetItemBounds;
+ interface.get_item_bounds = &GetItemBounds;
interface.set_focus = &SetFocus;
interface.set_item_enabled = &SetItemEnabled;
interface.set_item_dir = &SetItemDir;
diff --git a/cobalt/ui_navigation/interface.h b/cobalt/ui_navigation/interface.h
index ec8b71b..2d42301 100644
--- a/cobalt/ui_navigation/interface.h
+++ b/cobalt/ui_navigation/interface.h
@@ -37,6 +37,14 @@
const NativeCallbacks* callbacks,
void* callback_context);
void (*destroy_item)(NativeItem item);
+ void (*set_item_bounds)(NativeItem item, float scroll_top_lower_bound,
+ float scroll_left_lower_bound,
+ float scroll_top_upper_bound,
+ float scroll_left_upper_bound);
+ void (*get_item_bounds)(NativeItem item, float* out_scroll_top_lower_bound,
+ float* out_scroll_left_lower_bound,
+ float* out_scroll_top_upper_bound,
+ float* out_scroll_left_upper_bound);
void (*set_focus)(NativeItem item);
void (*set_item_enabled)(NativeItem item, bool enabled);
void (*set_item_dir)(NativeItem item, NativeItemDir dir);
diff --git a/cobalt/ui_navigation/nav_item.cc b/cobalt/ui_navigation/nav_item.cc
index ff5eb6c..8795d59 100644
--- a/cobalt/ui_navigation/nav_item.cc
+++ b/cobalt/ui_navigation/nav_item.cc
@@ -146,6 +146,29 @@
GetInterface().do_batch_update(&ProcessPendingChanges, &updates_snapshot);
}
+void NavItem::SetBounds(float scroll_top_lower_bound,
+ float scroll_left_lower_bound,
+ float scroll_top_upper_bound,
+ float scroll_left_upper_bound) {
+ starboard::ScopedSpinLock lock(&g_pending_updates_lock);
+ if (GetInterface().set_item_bounds) {
+ GetInterface().set_item_bounds(
+ nav_item_, scroll_top_lower_bound, scroll_left_lower_bound,
+ scroll_top_upper_bound, scroll_left_upper_bound);
+ }
+}
+
+void NavItem::GetBounds(float* out_scroll_top_lower_bound,
+ float* out_scroll_left_lower_bound,
+ float* out_scroll_top_upper_bound,
+ float* out_scroll_left_upper_bound) {
+ if (GetInterface().get_item_bounds) {
+ GetInterface().get_item_bounds(
+ nav_item_, out_scroll_top_lower_bound, out_scroll_left_lower_bound,
+ out_scroll_top_upper_bound, out_scroll_left_upper_bound);
+ }
+}
+
void NavItem::Focus() {
starboard::ScopedSpinLock lock(&g_pending_updates_lock);
if (state_ == kStateEnabled) {
diff --git a/cobalt/ui_navigation/nav_item.h b/cobalt/ui_navigation/nav_item.h
index fbba34f..eebbfa4 100644
--- a/cobalt/ui_navigation/nav_item.h
+++ b/cobalt/ui_navigation/nav_item.h
@@ -41,6 +41,13 @@
// periodically to ensure all UI updates are executed.
void PerformQueuedUpdates();
+ void SetBounds(float scroll_top_lower_bound, float scroll_left_lower_bound,
+ float scroll_top_upper_bound, float scroll_left_upper_bound);
+ void GetBounds(float* out_scroll_top_lower_bound,
+ float* out_scroll_left_lower_bound,
+ float* out_scroll_top_upper_bound,
+ float* out_scroll_left_upper_bound);
+
void Focus();
void UnfocusAll();
void SetEnabled(bool enabled);
diff --git a/cobalt/ui_navigation/scroll_engine/BUILD.gn b/cobalt/ui_navigation/scroll_engine/BUILD.gn
new file mode 100644
index 0000000..485dbea
--- /dev/null
+++ b/cobalt/ui_navigation/scroll_engine/BUILD.gn
@@ -0,0 +1,28 @@
+# Copyright 2022 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+static_library("scroll_engine") {
+ has_pedantic_warnings = true
+ sources = [
+ "scroll_engine.cc",
+ "scroll_engine.h",
+ ]
+ deps = [
+ "//cobalt/base",
+ "//cobalt/cssom",
+ "//cobalt/dom",
+ "//cobalt/math",
+ "//cobalt/ui_navigation",
+ ]
+}
diff --git a/cobalt/ui_navigation/scroll_engine/scroll_engine.cc b/cobalt/ui_navigation/scroll_engine/scroll_engine.cc
new file mode 100644
index 0000000..bd5495c
--- /dev/null
+++ b/cobalt/ui_navigation/scroll_engine/scroll_engine.cc
@@ -0,0 +1,308 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/ui_navigation/scroll_engine/scroll_engine.h"
+
+#include <algorithm>
+
+#include "cobalt/dom/pointer_event.h"
+
+namespace cobalt {
+namespace ui_navigation {
+namespace scroll_engine {
+
+namespace {
+
+const base::TimeDelta kFreeScrollDuration =
+ base::TimeDelta::FromMilliseconds(700);
+
+void BoundValuesByNavItemBounds(scoped_refptr<ui_navigation::NavItem> nav_item,
+ float* x, float* y) {
+ float scroll_top_lower_bound;
+ float scroll_left_lower_bound;
+ float scroll_top_upper_bound;
+ float scroll_left_upper_bound;
+ nav_item->GetBounds(&scroll_top_lower_bound, &scroll_left_lower_bound,
+ &scroll_top_upper_bound, &scroll_left_upper_bound);
+
+ *x = std::min(scroll_left_upper_bound, *x);
+ *x = std::max(scroll_left_lower_bound, *x);
+ *y = std::min(scroll_top_upper_bound, *y);
+ *y = std::max(scroll_top_lower_bound, *y);
+}
+
+math::Vector2dF BoundValuesByNavItemBounds(
+ scoped_refptr<ui_navigation::NavItem> nav_item, math::Vector2dF vector) {
+ float x = vector.x();
+ float y = vector.y();
+ BoundValuesByNavItemBounds(nav_item, &x, &y);
+ vector.set_x(x);
+ vector.set_y(y);
+ return vector;
+}
+
+bool ShouldFreeScroll(scoped_refptr<ui_navigation::NavItem> scroll_container,
+ math::Vector2dF drag_vector) {
+ float x = drag_vector.x();
+ float y = drag_vector.y();
+ bool scrolling_right = x > 0;
+ bool scrolling_down = y > 0;
+ bool scrolling_left = !scrolling_right;
+ bool scrolling_up = !scrolling_down;
+
+ float scroll_top_lower_bound, scroll_left_lower_bound, scroll_top_upper_bound,
+ scroll_left_upper_bound;
+ float offset_x, offset_y;
+ scroll_container->GetBounds(&scroll_top_lower_bound, &scroll_left_lower_bound,
+ &scroll_top_upper_bound,
+ &scroll_left_upper_bound);
+ scroll_container->GetContentOffset(&offset_x, &offset_y);
+
+ bool can_scroll_left = scroll_left_lower_bound < offset_x;
+ bool can_scroll_right = scroll_left_upper_bound > offset_x;
+ bool can_scroll_up = scroll_top_lower_bound < offset_y;
+ bool can_scroll_down = scroll_top_upper_bound > offset_y;
+ return (
+ ((can_scroll_left && scrolling_left) ||
+ (can_scroll_right && scrolling_right)) &&
+ ((can_scroll_down && scrolling_down) || (can_scroll_up && scrolling_up)));
+}
+
+void ScrollNavItemWithVector(scoped_refptr<NavItem> nav_item,
+ math::Vector2dF vector) {
+ float offset_x;
+ float offset_y;
+ nav_item->GetContentOffset(&offset_x, &offset_y);
+ offset_x += vector.x();
+ offset_y += vector.y();
+ BoundValuesByNavItemBounds(nav_item, &offset_x, &offset_y);
+
+ nav_item->SetContentOffset(offset_x, offset_y);
+}
+
+} // namespace
+
+ScrollEngine::ScrollEngine()
+ : timing_function_(cssom::TimingFunction::GetEaseInOut()) {}
+ScrollEngine::~ScrollEngine() { free_scroll_timer_.Stop(); }
+
+void ScrollEngine::MaybeFreeScrollActiveNavItem() {
+ DCHECK(base::MessageLoop::current() == scroll_engine_.message_loop());
+
+ DCHECK(previous_events_.size() == 2);
+ if (previous_events_.size() != 2) {
+ return;
+ }
+
+ auto previous_event = previous_events_.back();
+ auto current_event = previous_events_.front();
+ math::Vector2dF distance_delta =
+ previous_event.position - current_event.position;
+ // TODO(andrewsavage): See if we need this
+ // if (distance_delta.Length() < kFreeScrollThreshold) {
+ // return;
+ // }
+
+ // Get the average velocity for the entire run
+ math::Vector2dF average_velocity = distance_delta;
+ average_velocity.Scale(1.0f / static_cast<float>(current_event.time_stamp -
+ previous_event.time_stamp));
+ average_velocity.Scale(0.5);
+
+ // Get the distance
+ average_velocity.Scale(kFreeScrollDuration.ToSbTime());
+
+ float initial_offset_x;
+ float initial_offset_y;
+ active_item_->GetContentOffset(&initial_offset_x, &initial_offset_y);
+ math::Vector2dF initial_offset(initial_offset_x, initial_offset_y);
+
+ math::Vector2dF target_offset = initial_offset + average_velocity;
+ target_offset = BoundValuesByNavItemBounds(active_item_, target_offset);
+
+ nav_items_with_decaying_scroll_.push_back(
+ FreeScrollingNavItem(active_item_, initial_offset, target_offset));
+ if (!free_scroll_timer_.IsRunning()) {
+ free_scroll_timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds(5),
+ this,
+ &ScrollEngine::ScrollNavItemsWithDecayingScroll);
+ }
+
+ while (!previous_events_.empty()) {
+ previous_events_.pop();
+ }
+}
+
+void ScrollEngine::HandlePointerEventForActiveItem(
+ scoped_refptr<dom::PointerEvent> pointer_event) {
+ DCHECK(base::MessageLoop::current() == scroll_engine_.message_loop());
+
+ if (pointer_event->type() == base::Tokens::pointerup()) {
+ MaybeFreeScrollActiveNavItem();
+ active_item_ = nullptr;
+ active_velocity_ = math::Vector2dF(0.0f, 0.0f);
+ return;
+ }
+
+ if (pointer_event->type() != base::Tokens::pointermove()) {
+ return;
+ }
+
+ auto current_coordinates =
+ math::Vector2dF(pointer_event->x(), pointer_event->y());
+ if (previous_events_.size() != 2) {
+ // This is an error.
+ previous_events_.push(EventPositionWithTimeStamp(
+ current_coordinates, pointer_event->time_stamp()));
+ return;
+ }
+
+ auto drag_vector = previous_events_.front().position - current_coordinates;
+ if (active_scroll_type_ == ScrollType::Horizontal) {
+ drag_vector.set_y(0.0f);
+ } else if (active_scroll_type_ == ScrollType::Vertical) {
+ drag_vector.set_x(0.0f);
+ }
+
+ previous_events_.push(EventPositionWithTimeStamp(
+ current_coordinates, pointer_event->time_stamp()));
+ previous_events_.pop();
+
+ active_velocity_ = drag_vector;
+
+ ScrollNavItemWithVector(active_item_, drag_vector);
+}
+
+void ScrollEngine::HandlePointerEvent(base::Token type,
+ const dom::PointerEventInit& event) {
+ DCHECK(base::MessageLoop::current() == scroll_engine_.message_loop());
+
+ scoped_refptr<dom::PointerEvent> pointer_event(
+ new dom::PointerEvent(type, nullptr, event));
+ uint32_t pointer_id = pointer_event->pointer_id();
+ if (pointer_event->type() == base::Tokens::pointerdown()) {
+ events_to_handle_[pointer_id] = pointer_event;
+ return;
+ }
+ if (active_item_) {
+ HandlePointerEventForActiveItem(pointer_event);
+ return;
+ }
+
+ auto last_event_to_handle = events_to_handle_.find(pointer_id);
+ if (last_event_to_handle == events_to_handle_.end()) {
+ // Pointer events have not come in the appropriate order.
+ return;
+ }
+
+ if (pointer_event->type() == base::Tokens::pointermove()) {
+ if (last_event_to_handle->second->type() == base::Tokens::pointermove() ||
+ (math::Vector2dF(last_event_to_handle->second->x(),
+ last_event_to_handle->second->y()) -
+ math::Vector2dF(pointer_event->x(), pointer_event->y()))
+ .Length() > kDragDistanceThreshold) {
+ events_to_handle_[pointer_id] = pointer_event;
+ }
+ } else if (pointer_event->type() == base::Tokens::pointerup()) {
+ if (last_event_to_handle->second->type() == base::Tokens::pointermove()) {
+ events_to_handle_[pointer_id] = pointer_event;
+ } else {
+ events_to_handle_.erase(pointer_id);
+ }
+ }
+}
+
+void ScrollEngine::HandleScrollStart(
+ scoped_refptr<ui_navigation::NavItem> scroll_container,
+ ScrollType scroll_type, int32_t pointer_id,
+ math::Vector2dF initial_coordinates, uint64 initial_time_stamp,
+ math::Vector2dF current_coordinates, uint64 current_time_stamp) {
+ DCHECK(base::MessageLoop::current() == scroll_engine_.message_loop());
+
+ auto drag_vector = initial_coordinates - current_coordinates;
+ if (ShouldFreeScroll(scroll_container, drag_vector)) {
+ scroll_type = ScrollType::Free;
+ }
+ active_item_ = scroll_container;
+ active_scroll_type_ = scroll_type;
+
+ if (active_scroll_type_ == ScrollType::Horizontal) {
+ drag_vector.set_y(0.0f);
+ } else if (active_scroll_type_ == ScrollType::Vertical) {
+ drag_vector.set_x(0.0f);
+ }
+
+ previous_events_.push(
+ EventPositionWithTimeStamp(initial_coordinates, initial_time_stamp));
+ previous_events_.push(
+ EventPositionWithTimeStamp(current_coordinates, current_time_stamp));
+
+ active_velocity_ = drag_vector;
+ ScrollNavItemWithVector(active_item_, drag_vector);
+
+ auto event_to_handle = events_to_handle_.find(pointer_id);
+ if (event_to_handle != events_to_handle_.end()) {
+ HandlePointerEventForActiveItem(event_to_handle->second);
+ }
+}
+
+void ScrollEngine::CancelActiveScrollsForNavItems(
+ std::vector<scoped_refptr<ui_navigation::NavItem>> scrolls_to_cancel) {
+ DCHECK(base::MessageLoop::current() == scroll_engine_.message_loop());
+
+ for (auto scroll_to_cancel : scrolls_to_cancel) {
+ for (std::vector<FreeScrollingNavItem>::iterator it =
+ nav_items_with_decaying_scroll_.begin();
+ it != nav_items_with_decaying_scroll_.end();) {
+ if (it->nav_item.get() == scroll_to_cancel.get()) {
+ it = nav_items_with_decaying_scroll_.erase(it);
+ } else {
+ it++;
+ }
+ }
+ }
+}
+
+void ScrollEngine::ScrollNavItemsWithDecayingScroll() {
+ DCHECK(base::MessageLoop::current() == scroll_engine_.message_loop());
+
+ if (nav_items_with_decaying_scroll_.size() == 0) {
+ free_scroll_timer_.Stop();
+ return;
+ }
+ for (std::vector<FreeScrollingNavItem>::iterator it =
+ nav_items_with_decaying_scroll_.begin();
+ it != nav_items_with_decaying_scroll_.end();) {
+ auto now = base::Time::Now();
+ auto update_delta = now - it->last_change;
+ float fraction_of_time =
+ std::max<float>(update_delta / kFreeScrollDuration, 1.0);
+ float progress = timing_function_->Evaluate(fraction_of_time);
+ math::Vector2dF current_offset = it->target_offset - it->initial_offset;
+ current_offset.Scale(progress);
+ current_offset += it->initial_offset;
+ it->nav_item->SetContentOffset(current_offset.x(), current_offset.y());
+ it->last_change = now;
+
+ if (fraction_of_time == 1.0) {
+ it = nav_items_with_decaying_scroll_.erase(it);
+ } else {
+ it++;
+ }
+ }
+}
+
+} // namespace scroll_engine
+} // namespace ui_navigation
+} // namespace cobalt
diff --git a/cobalt/ui_navigation/scroll_engine/scroll_engine.h b/cobalt/ui_navigation/scroll_engine/scroll_engine.h
new file mode 100644
index 0000000..2399457
--- /dev/null
+++ b/cobalt/ui_navigation/scroll_engine/scroll_engine.h
@@ -0,0 +1,107 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_UI_NAVIGATION_SCROLL_ENGINE_SCROLL_ENGINE_H_
+#define COBALT_UI_NAVIGATION_SCROLL_ENGINE_SCROLL_ENGINE_H_
+
+#include <map>
+#include <queue>
+#include <vector>
+
+#include "base/threading/thread.h"
+#include "base/time/time.h"
+#include "base/timer/timer.h"
+#include "cobalt/base/token.h"
+#include "cobalt/dom/pointer_event.h"
+#include "cobalt/dom/pointer_event_init.h"
+#include "cobalt/math/vector2d_f.h"
+#include "cobalt/ui_navigation/nav_item.h"
+
+namespace cobalt {
+namespace ui_navigation {
+namespace scroll_engine {
+
+// kDragDistanceThreshold is measured in viewport coordinate distance.
+const float kDragDistanceThreshold = 20.0f;
+
+typedef enum ScrollType {
+ Unknown,
+ Horizontal,
+ Vertical,
+ Free,
+} ScrollType;
+
+class ScrollEngine {
+ public:
+ ScrollEngine();
+ ~ScrollEngine();
+
+ void HandlePointerEvent(base::Token type, const dom::PointerEventInit& event);
+ void HandleScrollStart(scoped_refptr<ui_navigation::NavItem> scroll_container,
+ ScrollType scroll_type, int32_t pointer_id,
+ math::Vector2dF initial_coordinates,
+ uint64 initial_time_stamp,
+ math::Vector2dF current_coordinates,
+ uint64 current_time_stamp);
+ void CancelActiveScrollsForNavItems(
+ std::vector<scoped_refptr<ui_navigation::NavItem>> scrolls_to_cancel);
+
+ void HandlePointerEventForActiveItem(
+ scoped_refptr<dom::PointerEvent> pointer_event);
+ void ScrollNavItemsWithDecayingScroll();
+ void MaybeFreeScrollActiveNavItem();
+
+ base::Thread* thread() { return &scroll_engine_; }
+
+ private:
+ base::Thread scroll_engine_{"ScrollEngineThread"};
+ base::RepeatingTimer free_scroll_timer_;
+
+ struct EventPositionWithTimeStamp {
+ EventPositionWithTimeStamp(math::Vector2dF position, uint64 time_stamp)
+ : position(position), time_stamp(time_stamp) {}
+ math::Vector2dF position;
+ uint64 time_stamp;
+ };
+
+ struct FreeScrollingNavItem {
+ FreeScrollingNavItem(scoped_refptr<NavItem> nav_item,
+ math::Vector2dF initial_offset,
+ math::Vector2dF target_offset)
+ : nav_item(nav_item),
+ initial_offset(initial_offset),
+ target_offset(target_offset),
+ last_change(base::Time::Now()) {}
+ scoped_refptr<NavItem> nav_item;
+ math::Vector2dF initial_offset;
+ math::Vector2dF target_offset;
+ base::Time last_change;
+ };
+
+ std::queue<EventPositionWithTimeStamp> previous_events_;
+ const scoped_refptr<cssom::TimingFunction>& timing_function_;
+
+ scoped_refptr<NavItem> active_item_;
+ math::Vector2dF active_velocity_;
+ ScrollType active_scroll_type_ = ScrollType::Unknown;
+ std::map<uint32_t, scoped_refptr<dom::PointerEvent>> events_to_handle_;
+ std::vector<FreeScrollingNavItem> nav_items_with_decaying_scroll_;
+};
+
+
+} // namespace scroll_engine
+} // namespace ui_navigation
+} // namespace cobalt
+
+#endif // COBALT_UI_NAVIGATION_SCROLL_ENGINE_SCROLL_ENGINE_H_
diff --git a/cobalt/updater/updater_module.cc b/cobalt/updater/updater_module.cc
index 00460fd..a31f3d1 100644
--- a/cobalt/updater/updater_module.cc
+++ b/cobalt/updater/updater_module.cc
@@ -95,7 +95,7 @@
namespace updater {
// The delay in seconds before the first update check.
-const uint64_t kDefaultUpdateCheckDelaySeconds = 15;
+const uint64_t kDefaultUpdateCheckDelaySeconds = 30;
void Observer::OnEvent(Events event, const std::string& id) {
LOG(INFO) << "Observer::OnEvent id=" << id;
diff --git a/cobalt/version.h b/cobalt/version.h
index 7f8ca72..bc6859b 100644
--- a/cobalt/version.h
+++ b/cobalt/version.h
@@ -35,6 +35,6 @@
// release is cut.
//.
-#define COBALT_VERSION "23.lts.2"
+#define COBALT_VERSION "23.lts.3"
#endif // COBALT_VERSION_H_
diff --git a/cobalt/watchdog/watchdog.cc b/cobalt/watchdog/watchdog.cc
index 8ad8474..61cc134 100644
--- a/cobalt/watchdog/watchdog.cc
+++ b/cobalt/watchdog/watchdog.cc
@@ -37,7 +37,7 @@
// The Watchdog violations json filename.
const char kWatchdogViolationsJson[] = "watchdog.json";
// The frequency in microseconds of monitor loops.
-const int64_t kWatchdogMonitorFrequency = 1000000;
+const int64_t kWatchdogMonitorFrequency = 500000;
// The maximum number of Watchdog violations.
const int kWatchdogMaxViolations = 200;
// The minimum number of microseconds between writes.
@@ -79,7 +79,6 @@
watchdog_file_name_ = watchdog_file_name;
watchdog_monitor_frequency_ = watchdog_monitor_frequency;
- SB_CHECK(SbMutexCreate(&mutex_));
pending_write_ = false;
write_wait_time_microseconds_ = kWatchdogWriteWaitTime;
@@ -109,7 +108,7 @@
#endif // defined(_DEBUG)
// Starts monitor thread.
- is_monitoring_ = true;
+ is_monitoring_.store(true);
SB_DCHECK(!SbThreadIsValid(watchdog_thread_));
watchdog_thread_ = SbThreadCreate(0, kSbThreadNoPriority, kSbThreadNoAffinity,
true, "Watchdog", &Watchdog::Monitor, this);
@@ -125,10 +124,11 @@
void Watchdog::Uninitialize() {
if (is_disabled_) return;
- SB_CHECK(SbMutexAcquire(&mutex_) == kSbMutexAcquired);
+ mutex_.Acquire();
if (pending_write_) WriteWatchdogViolations();
- is_monitoring_ = false;
- SB_CHECK(SbMutexRelease(&mutex_));
+ is_monitoring_.store(false);
+ monitor_wait_.Signal();
+ mutex_.Release();
SbThreadJoin(watchdog_thread_, nullptr);
}
@@ -162,22 +162,13 @@
void Watchdog::UpdateState(base::ApplicationState state) {
if (is_disabled_) return;
- SB_CHECK(SbMutexAcquire(&mutex_) == kSbMutexAcquired);
+ starboard::ScopedLock scoped_lock(mutex_);
state_ = state;
- SB_CHECK(SbMutexRelease(&mutex_));
}
void* Watchdog::Monitor(void* context) {
+ starboard::ScopedLock scoped_lock(static_cast<Watchdog*>(context)->mutex_);
while (1) {
- SB_CHECK(SbMutexAcquire(&(static_cast<Watchdog*>(context))->mutex_) ==
- kSbMutexAcquired);
-
- // Shutdown
- if (!((static_cast<Watchdog*>(context))->is_monitoring_)) {
- SB_CHECK(SbMutexRelease(&(static_cast<Watchdog*>(context))->mutex_));
- break;
- }
-
SbTimeMonotonic current_monotonic_time = SbTimeGetMonotonicNow();
// Iterates through client map to monitor all registered clients.
@@ -216,8 +207,12 @@
MaybeWriteWatchdogViolations(context);
if (watchdog_violation) MaybeTriggerCrash(context);
- SB_CHECK(SbMutexRelease(&(static_cast<Watchdog*>(context))->mutex_));
- SbThreadSleep(static_cast<Watchdog*>(context)->watchdog_monitor_frequency_);
+ // Wait
+ static_cast<Watchdog*>(context)->monitor_wait_.WaitTimed(
+ static_cast<Watchdog*>(context)->watchdog_monitor_frequency_);
+
+ // Shutdown
+ if (!(static_cast<Watchdog*>(context)->is_monitoring_.load())) break;
}
return nullptr;
}
@@ -416,7 +411,7 @@
return false;
}
- SB_CHECK(SbMutexAcquire(&mutex_) == kSbMutexAcquired);
+ starboard::ScopedLock scoped_lock(mutex_);
int64_t current_time = SbTimeToPosix(SbTimeGetNow());
SbTimeMonotonic current_monotonic_time = SbTimeGetMonotonicNow();
@@ -431,7 +426,6 @@
it->second->time_last_pinged_microseconds = current_time;
it->second->time_last_updated_monotonic_microseconds =
current_monotonic_time;
- SB_CHECK(SbMutexRelease(&mutex_));
return true;
}
if (replace == ALL) Unregister(name, false);
@@ -454,8 +448,6 @@
// Registers.
auto result = client_map_.emplace(name, std::move(client));
- SB_CHECK(SbMutexRelease(&mutex_));
-
if (result.second) {
SB_DLOG(INFO) << "[Watchdog] Registered: " << name;
} else {
@@ -468,9 +460,9 @@
if (is_disabled_) return true;
// Unregisters.
- if (lock) SB_CHECK(SbMutexAcquire(&mutex_) == kSbMutexAcquired);
+ if (lock) mutex_.Acquire();
auto result = client_map_.erase(name);
- if (lock) SB_CHECK(SbMutexRelease(&mutex_));
+ if (lock) mutex_.Release();
if (result) {
SB_DLOG(INFO) << "[Watchdog] Unregistered: " << name;
@@ -493,7 +485,7 @@
return false;
}
- SB_CHECK(SbMutexAcquire(&mutex_) == kSbMutexAcquired);
+ starboard::ScopedLock scoped_lock(mutex_);
auto it = client_map_.find(name);
bool client_exists = it != client_map_.end();
@@ -522,7 +514,6 @@
} else {
SB_DLOG(ERROR) << "[Watchdog] Unable to Ping: " << name;
}
- SB_CHECK(SbMutexRelease(&mutex_));
return client_exists;
}
@@ -534,7 +525,7 @@
std::string watchdog_json = "";
- SB_CHECK(SbMutexAcquire(&mutex_) == kSbMutexAcquired);
+ starboard::ScopedLock scoped_lock(mutex_);
if (pending_write_) WriteWatchdogViolations();
@@ -558,7 +549,6 @@
} else {
SB_LOG(INFO) << "[Watchdog] No violations.";
}
- SB_CHECK(SbMutexRelease(&mutex_));
return watchdog_json;
}
@@ -605,7 +595,7 @@
void Watchdog::MaybeInjectDebugDelay(const std::string& name) {
if (is_disabled_) return;
- starboard::ScopedLock scoped_lock(delay_lock_);
+ starboard::ScopedLock scoped_lock(delay_mutex_);
if (name != delay_name_) return;
diff --git a/cobalt/watchdog/watchdog.h b/cobalt/watchdog/watchdog.h
index 04ba81d..ff2e486 100644
--- a/cobalt/watchdog/watchdog.h
+++ b/cobalt/watchdog/watchdog.h
@@ -23,8 +23,9 @@
#include "cobalt/base/application_state.h"
#include "cobalt/persistent_storage/persistent_settings.h"
#include "cobalt/watchdog/singleton.h"
+#include "starboard/atomic.h"
+#include "starboard/common/condition_variable.h"
#include "starboard/common/mutex.h"
-#include "starboard/mutex.h"
#include "starboard/thread.h"
#include "starboard/time.h"
@@ -124,7 +125,7 @@
// only occur in between loops of monitor. API functions like Register(),
// Unregister(), Ping(), and GetWatchdogViolations() will be called by
// various threads and interact with these class variables.
- SbMutex mutex_;
+ starboard::Mutex mutex_;
// Tracks application state.
base::ApplicationState state_ = base::kApplicationStateStarted;
// Flag to trigger Watchdog violations writes to persistent storage.
@@ -143,12 +144,15 @@
// Monitor thread.
SbThread watchdog_thread_;
// Flag to stop monitor thread.
- bool is_monitoring_;
+ starboard::atomic_bool is_monitoring_;
+ // Conditional Variable to wait and shutdown monitor thread.
+ starboard::ConditionVariable monitor_wait_ =
+ starboard::ConditionVariable(mutex_);
// The frequency in microseconds of monitor loops.
int64_t watchdog_monitor_frequency_;
#if defined(_DEBUG)
- starboard::Mutex delay_lock_;
+ starboard::Mutex delay_mutex_;
// Name of the client to inject a delay for.
std::string delay_name_ = "";
// Monotonically increasing timestamp when a delay was last injected. 0
diff --git a/cobalt/web/BUILD.gn b/cobalt/web/BUILD.gn
index cda07f8..c722217 100644
--- a/cobalt/web/BUILD.gn
+++ b/cobalt/web/BUILD.gn
@@ -97,6 +97,7 @@
"url_utils.cc",
"url_utils.h",
"user_agent_platform_info.h",
+ "web_settings.h",
"window_or_worker_global_scope.cc",
"window_or_worker_global_scope.h",
]
@@ -107,11 +108,13 @@
"//cobalt/browser:generated_bindings",
"//cobalt/cache",
"//cobalt/csp",
+ "//cobalt/dom:media_settings",
"//cobalt/network",
"//cobalt/network_bridge",
"//cobalt/script",
"//cobalt/script:engine",
"//cobalt/script/v8c:engine",
+ "//cobalt/xhr:xhr_settings",
"//net",
"//url",
]
diff --git a/cobalt/web/agent.cc b/cobalt/web/agent.cc
index f44a345..7edf26e 100644
--- a/cobalt/web/agent.cc
+++ b/cobalt/web/agent.cc
@@ -84,6 +84,7 @@
return script_runner_.get();
}
Blob::Registry* blob_registry() const final { return blob_registry_.get(); }
+ web::WebSettings* web_settings() const final { return web_settings_; }
network::NetworkModule* network_module() const final {
DCHECK(fetcher_factory_);
return fetcher_factory_->network_module();
@@ -110,14 +111,14 @@
scoped_refptr<worker::ServiceWorkerRegistration>
LookupServiceWorkerRegistration(
worker::ServiceWorkerRegistrationObject* registration) final;
- // https://w3c.github.io/ServiceWorker/#service-worker-registration-creation
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#service-worker-registration-creation
scoped_refptr<worker::ServiceWorkerRegistration> GetServiceWorkerRegistration(
worker::ServiceWorkerRegistrationObject* registration) final;
void RemoveServiceWorker(worker::ServiceWorkerObject* worker) final;
scoped_refptr<worker::ServiceWorker> LookupServiceWorker(
worker::ServiceWorkerObject* worker) final;
- // https://w3c.github.io/ServiceWorker/#get-the-service-worker-object
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#get-the-service-worker-object
scoped_refptr<worker::ServiceWorker> GetServiceWorker(
worker::ServiceWorkerObject* worker) final;
@@ -133,7 +134,7 @@
return network_module()->preferred_language();
}
- // https://w3c.github.io/ServiceWorker/#dfn-control
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-control
bool is_controlled_by(worker::ServiceWorkerObject* worker) const final {
// When a service worker client has a non-null active service worker, it is
// said to be controlled by that active service worker.
@@ -171,6 +172,9 @@
// Name of the web instance.
std::string name_;
+
+ web::WebSettings* const web_settings_;
+
// FetcherFactory that is used to create a fetcher according to URL.
std::unique_ptr<loader::FetcherFactory> fetcher_factory_;
@@ -199,13 +203,13 @@
std::unique_ptr<EnvironmentSettings> environment_settings_;
// The service worker registration object map.
- // https://w3c.github.io/ServiceWorker/#environment-settings-object-service-worker-registration-object-map
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#environment-settings-object-service-worker-registration-object-map
std::map<worker::ServiceWorkerRegistrationObject*,
scoped_refptr<worker::ServiceWorkerRegistration>>
service_worker_registration_object_map_;
// The service worker object map.
- // https://w3c.github.io/ServiceWorker/#environment-settings-object-service-worker-object-map
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#environment-settings-object-service-worker-object-map
std::map<worker::ServiceWorkerObject*, scoped_refptr<worker::ServiceWorker>>
service_worker_object_map_;
@@ -224,7 +228,7 @@
};
Impl::Impl(const std::string& name, const Agent::Options& options)
- : name_(name) {
+ : name_(name), web_settings_(options.web_settings) {
TRACE_EVENT0("cobalt::web", "Agent::Impl::Impl()");
service_worker_jobs_ = options.service_worker_jobs;
platform_info_ = options.platform_info;
@@ -287,7 +291,13 @@
blob_registry_.reset();
script_runner_.reset();
execution_state_.reset();
- global_environment_ = NULL;
+
+ // Ensure that global_environment_ is null before it's destroyed.
+ scoped_refptr<script::GlobalEnvironment> global_environment(
+ std::move(global_environment_));
+ DCHECK(!global_environment_);
+ global_environment = nullptr;
+
javascript_engine_.reset();
fetcher_factory_.reset();
script_loader_factory_.reset();
@@ -337,7 +347,7 @@
Impl::GetServiceWorkerRegistration(
worker::ServiceWorkerRegistrationObject* registration) {
// Algorithm for 'get the service worker registration object':
- // https://w3c.github.io/ServiceWorker/#get-the-service-worker-registration-object
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#get-the-service-worker-registration-object
scoped_refptr<worker::ServiceWorkerRegistration> worker_registration;
if (!registration) {
// Return undefined when registration is null.
@@ -404,7 +414,7 @@
scoped_refptr<worker::ServiceWorker> Impl::LookupServiceWorker(
worker::ServiceWorkerObject* worker) {
// Algorithm for 'get the service worker object':
- // https://w3c.github.io/ServiceWorker/#get-the-service-worker-object
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#get-the-service-worker-object
scoped_refptr<worker::ServiceWorker> service_worker;
if (!worker) {
@@ -424,7 +434,7 @@
scoped_refptr<worker::ServiceWorker> Impl::GetServiceWorker(
worker::ServiceWorkerObject* worker) {
// Algorithm for 'get the service worker object':
- // https://w3c.github.io/ServiceWorker/#get-the-service-worker-object
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#get-the-service-worker-object
scoped_refptr<worker::ServiceWorker> service_worker;
if (!worker) {
@@ -453,7 +463,7 @@
WindowOrWorkerGlobalScope* Impl::GetWindowOrWorkerGlobalScope() {
script::Wrappable* global_wrappable =
- global_environment()->global_wrappable();
+ global_environment_ ? global_environment_->global_wrappable() : nullptr;
if (!global_wrappable) {
return nullptr;
}
diff --git a/cobalt/web/agent.h b/cobalt/web/agent.h
index aa48bb1..d57d526 100644
--- a/cobalt/web/agent.h
+++ b/cobalt/web/agent.h
@@ -30,6 +30,7 @@
#include "cobalt/web/context.h"
#include "cobalt/web/environment_settings.h"
#include "cobalt/web/user_agent_platform_info.h"
+#include "cobalt/web/web_settings.h"
namespace cobalt {
namespace worker {
@@ -58,6 +59,7 @@
script::JavaScriptEngine::Options javascript_engine_options;
+ web::WebSettings* web_settings = nullptr;
network::NetworkModule* network_module = nullptr;
// Optional directory to add to the search path for web files (file://).
diff --git a/cobalt/web/cache.cc b/cobalt/web/cache.cc
index bbef89f..f9143a9 100644
--- a/cobalt/web/cache.cc
+++ b/cobalt/web/cache.cc
@@ -432,10 +432,8 @@
get_global_environment(environment_settings);
auto* isolate = global_environment->isolate();
script::v8c::EntryScope entry_scope(isolate);
- auto keys =
- cache::Cache::GetInstance()->KeysWithMetadata(kResourceType);
std::vector<v8::Local<v8::Value>> requests;
- for (uint8_t key :
+ for (uint32_t key :
cache::Cache::GetInstance()->KeysWithMetadata(kResourceType)) {
std::unique_ptr<base::Value> url =
cache::Cache::GetInstance()->Metadata(kResourceType, key);
diff --git a/cobalt/web/cache_storage.cc b/cobalt/web/cache_storage.cc
index db96ded..222b617 100644
--- a/cobalt/web/cache_storage.cc
+++ b/cobalt/web/cache_storage.cc
@@ -22,6 +22,7 @@
#include "cobalt/base/source_location.h"
#include "cobalt/cache/cache.h"
#include "cobalt/script/source_code.h"
+#include "cobalt/script/v8c/v8c_value_handle.h"
#include "cobalt/web/context.h"
#include "cobalt/web/environment_settings_helper.h"
@@ -83,6 +84,28 @@
return promise;
}
+script::HandlePromiseBool CacheStorage::Has(
+ script::EnvironmentSettings* environment_settings,
+ const std::string& cache_name) {
+ script::HandlePromiseBool promise =
+ get_script_value_factory(environment_settings)
+ ->CreateBasicPromise<bool>();
+ promise->Resolve(true);
+ return promise;
+}
+
+script::Handle<script::Promise<script::Handle<script::ValueHandle>>>
+CacheStorage::Keys(script::EnvironmentSettings* environment_settings) {
+ script::HandlePromiseAny promise =
+ get_script_value_factory(environment_settings)
+ ->CreateBasicPromise<script::Any>();
+ auto global_environment = get_global_environment(environment_settings);
+ auto* isolate = global_environment->isolate();
+ promise->Resolve(script::Any(
+ new script::v8c::V8cValueHandleHolder(isolate, v8::Array::New(isolate))));
+ return promise;
+}
+
scoped_refptr<Cache> CacheStorage::GetOrCreateCache() {
if (!cache_) {
cache_ = new Cache();
diff --git a/cobalt/web/cache_storage.h b/cobalt/web/cache_storage.h
index ec25b1b..e3a4cfb 100644
--- a/cobalt/web/cache_storage.h
+++ b/cobalt/web/cache_storage.h
@@ -49,6 +49,11 @@
script::HandlePromiseBool Delete(
script::EnvironmentSettings* environment_settings,
const std::string& cache_name);
+ script::HandlePromiseBool Has(
+ script::EnvironmentSettings* environment_settings,
+ const std::string& cache_name);
+ script::Handle<script::Promise<script::Handle<script::ValueHandle>>> Keys(
+ script::EnvironmentSettings* environment_settings);
DEFINE_WRAPPABLE_TYPE(CacheStorage);
diff --git a/cobalt/web/cache_storage.idl b/cobalt/web/cache_storage.idl
index a9744a6..9d4d586 100644
--- a/cobalt/web/cache_storage.idl
+++ b/cobalt/web/cache_storage.idl
@@ -22,4 +22,6 @@
[CallWith=EnvironmentSettings, NewObject] Promise<Cache> open(DOMString cacheName);
// Ignores |cacheName| and deletes all Cache API data.
[CallWith=EnvironmentSettings, NewObject] Promise<boolean> delete(DOMString cacheName);
+ [CallWith=EnvironmentSettings, NewObject] Promise<boolean> has(DOMString cacheName);
+ [CallWith=EnvironmentSettings, NewObject] Promise<any> keys();
};
diff --git a/cobalt/web/cache_utils.cc b/cobalt/web/cache_utils.cc
index 3026127..92fc5a2 100644
--- a/cobalt/web/cache_utils.cc
+++ b/cobalt/web/cache_utils.cc
@@ -52,6 +52,20 @@
return !result.IsNothing();
}
+v8::MaybeLocal<v8::Value> TryCall(v8::Local<v8::Context> context,
+ v8::Local<v8::Value> object,
+ const std::string& key, int argc,
+ v8::Local<v8::Value> argv[]) {
+ v8::Local<v8::Value> function;
+ if (!cache_utils::TryGet(context, object, key).ToLocal(&function) ||
+ function.IsEmpty() || !function->IsFunction()) {
+ return v8::MaybeLocal<v8::Value>();
+ }
+ auto object_context =
+ object.As<v8::Object>()->GetIsolate()->GetCurrentContext();
+ return function.As<v8::Function>()->Call(object_context, object, argc, argv);
+}
+
script::Any GetUndefined(script::EnvironmentSettings* environment_settings) {
auto* global_environment = get_global_environment(environment_settings);
auto* isolate = global_environment->isolate();
@@ -106,7 +120,7 @@
auto* global_environment = get_global_environment(environment_settings);
auto* isolate = global_environment->isolate();
v8::Local<v8::Value> argv[] = {V8String(isolate, url)};
- return CreateInstance(environment_settings, "Response", /*argc=*/1, argv);
+ return CreateInstance(environment_settings, "Request", /*argc=*/1, argv);
}
base::Optional<script::Any> CreateResponse(
diff --git a/cobalt/web/cache_utils.h b/cobalt/web/cache_utils.h
index 983895f..0ed4c1b 100644
--- a/cobalt/web/cache_utils.h
+++ b/cobalt/web/cache_utils.h
@@ -37,9 +37,45 @@
v8::Local<v8::Value> Get(v8::Local<v8::Context> context,
v8::Local<v8::Value> object, const std::string& key);
+template <typename T>
+inline T* GetExternal(v8::Local<v8::Context> context,
+ v8::Local<v8::Value> object, const std::string& key) {
+ return static_cast<T*>(
+ web::cache_utils::Get(context, object, key).As<v8::External>()->Value());
+}
+
+template <typename T>
+inline std::unique_ptr<T> GetOwnedExternal(v8::Local<v8::Context> context,
+ v8::Local<v8::Value> object,
+ const std::string& key) {
+ return std::unique_ptr<T>(
+ web::cache_utils::GetExternal<T>(context, object, key));
+}
+
bool Set(v8::Local<v8::Context> context, v8::Local<v8::Value> object,
const std::string& key, v8::Local<v8::Value> value);
+
+template <typename T>
+inline bool SetExternal(v8::Local<v8::Context> context,
+ v8::Local<v8::Value> object, const std::string& key,
+ T* value) {
+ auto* isolate = context->GetIsolate();
+ return Set(context, object, key, v8::External::New(isolate, value));
+}
+
+template <typename T>
+inline bool SetOwnedExternal(v8::Local<v8::Context> context,
+ v8::Local<v8::Value> object,
+ const std::string& key, std::unique_ptr<T> value) {
+ return SetExternal<T>(context, object, key, value.release());
+}
+
+v8::MaybeLocal<v8::Value> TryCall(v8::Local<v8::Context> context,
+ v8::Local<v8::Value> object,
+ const std::string& key, int argc = 0,
+ v8::Local<v8::Value> argv[] = nullptr);
+
script::Any GetUndefined(script::EnvironmentSettings* environment_settings);
script::Any EvaluateString(script::EnvironmentSettings* environment_settings,
diff --git a/cobalt/web/context.h b/cobalt/web/context.h
index 360dbe2..66825db 100644
--- a/cobalt/web/context.h
+++ b/cobalt/web/context.h
@@ -29,6 +29,7 @@
#include "cobalt/web/blob.h"
#include "cobalt/web/environment_settings.h"
#include "cobalt/web/user_agent_platform_info.h"
+#include "cobalt/web/web_settings.h"
namespace cobalt {
namespace worker {
@@ -62,6 +63,7 @@
virtual script::ExecutionState* execution_state() const = 0;
virtual script::ScriptRunner* script_runner() const = 0;
virtual Blob::Registry* blob_registry() const = 0;
+ virtual web::WebSettings* web_settings() const = 0;
virtual network::NetworkModule* network_module() const = 0;
virtual worker::ServiceWorkerJobs* service_worker_jobs() const = 0;
@@ -72,7 +74,7 @@
virtual scoped_refptr<worker::ServiceWorkerRegistration>
LookupServiceWorkerRegistration(
worker::ServiceWorkerRegistrationObject* registration) = 0;
- // https://w3c.github.io/ServiceWorker/#get-the-service-worker-registration-object
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#get-the-service-worker-registration-object
virtual scoped_refptr<worker::ServiceWorkerRegistration>
GetServiceWorkerRegistration(
worker::ServiceWorkerRegistrationObject* registration) = 0;
@@ -80,7 +82,7 @@
virtual void RemoveServiceWorker(worker::ServiceWorkerObject* worker) = 0;
virtual scoped_refptr<worker::ServiceWorker> LookupServiceWorker(
worker::ServiceWorkerObject* worker) = 0;
- // https://w3c.github.io/ServiceWorker/#get-the-service-worker-object
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#get-the-service-worker-object
virtual scoped_refptr<worker::ServiceWorker> GetServiceWorker(
worker::ServiceWorkerObject* worker) = 0;
@@ -96,7 +98,7 @@
virtual void RemoveEnvironmentSettingsChangeObserver(
EnvironmentSettingsChangeObserver* observer) = 0;
- // https://w3c.github.io/ServiceWorker/#dfn-control
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-control
virtual bool is_controlled_by(worker::ServiceWorkerObject* worker) const = 0;
// https://html.spec.whatwg.org/multipage/webappapis.html#concept-environment-active-service-worker
diff --git a/cobalt/web/csp_delegate.cc b/cobalt/web/csp_delegate.cc
index 8b1cbb9..2a54b9c 100644
--- a/cobalt/web/csp_delegate.cc
+++ b/cobalt/web/csp_delegate.cc
@@ -45,11 +45,29 @@
CspDelegateSecure::~CspDelegateSecure() {}
+void CspDelegateSecure::ClonePolicyContainer(
+ const csp::ContentSecurityPolicy& other) {
+ // https://html.spec.whatwg.org/commit-snapshots/814668ef2d1919a2a9387a0b29ebc6df7748fa80/#clone-a-policy-container
+ // To clone a policy container given a policy container policyContainer:
+ // 1. Let clone be a new policy container.
+ // We already have csp_ initialized in the constructor.
+ // 2. For each policy in policyContainer's CSP list, append a copy of policy
+ // into clone's CSP list.
+ for (const auto& directive_list : other.policies()) {
+ DCHECK(directive_list);
+ csp_->append_policy(*directive_list);
+ }
+ // 3. Set clone's embedder policy to a copy of policyContainer's embedder
+ // policy.
+ // Cobalt doesn't currently store embedder policy.
+ // 4. Set clone's referrer policy to policyContainer's referrer policy.
+ csp_->set_referrer_policy(csp_->referrer_policy());
+}
+
bool CspDelegateSecure::CanLoad(ResourceType type, const GURL& url,
bool did_redirect) const {
- const csp::ContentSecurityPolicy::RedirectStatus redirect_status =
- did_redirect ? csp::ContentSecurityPolicy::kDidRedirect
- : csp::ContentSecurityPolicy::kDidNotRedirect;
+ const csp::RedirectStatus redirect_status =
+ did_redirect ? csp::kDidRedirect : csp::kDidNotRedirect;
// Special case for "offline" mode- in the absence of any server policy,
// we check our default navigation policy, to permit navigation to
@@ -87,6 +105,9 @@
case kStyle:
can_load = csp_->AllowStyleFromSource(url, redirect_status);
break;
+ case kWorker:
+ can_load = csp_->AllowWorkerFromSource(url, redirect_status);
+ break;
case kXhr:
can_load = csp_->AllowConnectToSource(url, redirect_status);
break;
@@ -100,12 +121,18 @@
bool CspDelegateSecure::IsValidNonce(ResourceType type,
const std::string& nonce) const {
bool is_valid = false;
- if (type == kScript) {
- is_valid = csp_->AllowScriptWithNonce(nonce);
- } else if (type == kStyle) {
- is_valid = csp_->AllowStyleWithNonce(nonce);
- } else {
- NOTREACHED() << "Invalid resource type " << type;
+ switch (type) {
+ case kScript:
+ is_valid = csp_->AllowScriptWithNonce(nonce);
+ break;
+ case kStyle:
+ is_valid = csp_->AllowStyleWithNonce(nonce);
+ break;
+ case kWorker:
+ is_valid = csp_->AllowWorkerWithNonce(nonce);
+ break;
+ default:
+ NOTREACHED() << "Invalid resource type " << type;
}
return is_valid;
}
@@ -118,14 +145,21 @@
return true;
}
bool can_load = false;
- if (type == kScript) {
- can_load = csp_->AllowInlineScript(location.file_path, location.line_number,
- content);
- } else if (type == kStyle) {
- can_load = csp_->AllowInlineStyle(location.file_path, location.line_number,
- content);
- } else {
- NOTREACHED() << "Invalid resource type" << type;
+ switch (type) {
+ case kScript:
+ can_load = csp_->AllowInlineScript(location.file_path,
+ location.line_number, content);
+ break;
+ case kStyle:
+ can_load = csp_->AllowInlineStyle(location.file_path,
+ location.line_number, content);
+ break;
+ case kWorker:
+ can_load = csp_->AllowInlineWorker(location.file_path,
+ location.line_number, content);
+ break;
+ default:
+ NOTREACHED() << "Invalid resource type" << type;
}
return can_load;
}
@@ -133,8 +167,7 @@
bool CspDelegateSecure::AllowEval(std::string* eval_disabled_message) const {
bool allow_eval =
// If CSP is not provided, allow eval() function.
- !was_header_received_ ||
- csp_->AllowEval(csp::ContentSecurityPolicy::kSuppressReport);
+ !was_header_received_ || csp_->AllowEval(csp::kSuppressReport);
if (!allow_eval && eval_disabled_message) {
*eval_disabled_message = csp_->disable_eval_error_message();
}
@@ -142,7 +175,7 @@
}
void CspDelegateSecure::ReportEval() const {
- csp_->AllowEval(csp::ContentSecurityPolicy::kSendReport);
+ csp_->AllowEval(csp::kSendReport);
}
bool CspDelegateSecure::OnReceiveHeaders(const csp::ResponseHeaders& headers) {
diff --git a/cobalt/web/csp_delegate.h b/cobalt/web/csp_delegate.h
index 0a74804..4fa1cfb 100644
--- a/cobalt/web/csp_delegate.h
+++ b/cobalt/web/csp_delegate.h
@@ -38,6 +38,7 @@
kMedia,
kScript,
kStyle,
+ kWorker,
kXhr,
kWebSocket,
};
@@ -45,6 +46,10 @@
CspDelegate();
virtual ~CspDelegate();
+ virtual const csp::ContentSecurityPolicy* GetPolicyContainer() = 0;
+ virtual void ClonePolicyContainer(
+ const csp::ContentSecurityPolicy& other) = 0;
+
// Return |true| if the given resource type can be loaded from |url|.
// Set |did_redirect| if url was the result of a redirect.
virtual bool CanLoad(ResourceType type, const GURL& url,
@@ -83,6 +88,13 @@
class CspDelegateInsecure : public CspDelegate {
public:
CspDelegateInsecure() {}
+ const csp::ContentSecurityPolicy* GetPolicyContainer() override {
+ return nullptr;
+ }
+ void ClonePolicyContainer(const csp::ContentSecurityPolicy& other) override {
+ // No policy to clone.
+ return;
+ }
bool CanLoad(ResourceType, const GURL&, bool) const override { return true; }
bool IsValidNonce(ResourceType, const std::string&) const override {
return true;
@@ -109,6 +121,11 @@
const base::Closure& policy_changed_callback);
~CspDelegateSecure();
+ const csp::ContentSecurityPolicy* GetPolicyContainer() override {
+ return csp_.get();
+ }
+ void ClonePolicyContainer(const csp::ContentSecurityPolicy& other) override;
+
// Return |true| if the given resource type can be loaded from |url|.
// Set |did_redirect| if url was the result of a redirect.
bool CanLoad(ResourceType type, const GURL& url,
diff --git a/cobalt/web/csp_delegate_test.cc b/cobalt/web/csp_delegate_test.cc
index b10dbaf..96e5ccc 100644
--- a/cobalt/web/csp_delegate_test.cc
+++ b/cobalt/web/csp_delegate_test.cc
@@ -34,8 +34,12 @@
namespace {
struct ResourcePair {
+ // Resource type queried for the test.
CspDelegate::ResourceType type;
+ // Directive to allow 'self' for.
const char* directive;
+ // Effective directive reported in the violation.
+ const char* effective_directive;
};
std::ostream& operator<<(std::ostream& out, const ResourcePair& obj) {
@@ -43,21 +47,35 @@
}
const ResourcePair s_params[] = {
- {CspDelegate::kFont, "font-src"},
- {CspDelegate::kImage, "img-src"},
- {CspDelegate::kLocation, "h5vcc-location-src"},
- {CspDelegate::kMedia, "media-src"},
- {CspDelegate::kScript, "script-src"},
- {CspDelegate::kStyle, "style-src"},
- {CspDelegate::kXhr, "connect-src"},
- {CspDelegate::kWebSocket, "connect-src"},
+ {CspDelegate::kFont, "font-src", "font-src"},
+ {CspDelegate::kFont, "default-src", "font-src"},
+ {CspDelegate::kImage, "img-src", "img-src"},
+ {CspDelegate::kImage, "default-src", "img-src"},
+ {CspDelegate::kLocation, "h5vcc-location-src", "h5vcc-location-src"},
+ {CspDelegate::kMedia, "media-src", "media-src"},
+ {CspDelegate::kMedia, "default-src", "media-src"},
+ {CspDelegate::kScript, "script-src", "script-src"},
+ {CspDelegate::kScript, "default-src", "script-src"},
+ {CspDelegate::kStyle, "style-src", "style-src"},
+ {CspDelegate::kStyle, "default-src", "style-src"},
+ {CspDelegate::kWorker, "worker-src", "worker-src"},
+ {CspDelegate::kWorker, "script-src", "worker-src"},
+ {CspDelegate::kWorker, "default-src", "worker-src"},
+ {CspDelegate::kXhr, "connect-src", "connect-src"},
+ {CspDelegate::kXhr, "default-src", "connect-src"},
+ {CspDelegate::kWebSocket, "connect-src", "connect-src"},
+ {CspDelegate::kWebSocket, "default-src", "connect-src"},
};
std::string ResourcePairName(::testing::TestParamInfo<ResourcePair> info) {
- std::string name(info.param.directive);
- std::replace(name.begin(), name.end(), '-', '_');
- base::StringAppendF(&name, "_type_%d", info.param.type);
- return name;
+ std::string directive(info.param.directive);
+ std::replace(directive.begin(), directive.end(), '-', '_');
+ std::string effective_directive(info.param.effective_directive);
+ std::replace(effective_directive.begin(), effective_directive.end(), '-',
+ '_');
+ return base::StringPrintf("type_%d_directive_%s_effective_%s",
+ info.param.type, directive.c_str(),
+ effective_directive.c_str());
}
class MockViolationReporter : public CspViolationReporter {
@@ -115,8 +133,13 @@
csp_delegate_.reset(new CspDelegateSecure(
std::move(reporter), origin, csp::kCSPRequired, base::Closure()));
- std::string policy =
- base::StringPrintf("default-src none; %s 'self'", GetParam().directive);
+ std::string policy;
+ if (!strcmp(GetParam().directive, "default-src")) {
+ policy = base::StringPrintf("%s 'self'", GetParam().directive);
+ } else {
+ policy =
+ base::StringPrintf("default-src none; %s 'self'", GetParam().directive);
+ }
csp_delegate_->OnReceiveHeader(policy, csp::kHeaderTypeEnforce,
csp::kHeaderSourceMeta);
}
@@ -129,7 +152,7 @@
TEST_P(CspDelegateTest, LoadNotOk) {
CspDelegate::ResourceType param = GetParam().type;
- std::string effective_directive = GetParam().directive;
+ std::string effective_directive = GetParam().effective_directive;
GURL test_url("http://www.evil.com");
csp::ViolationInfo info;
diff --git a/cobalt/web/environment_settings.h b/cobalt/web/environment_settings.h
index c4a0c18..6fa5d89 100644
--- a/cobalt/web/environment_settings.h
+++ b/cobalt/web/environment_settings.h
@@ -46,14 +46,6 @@
// https://storage.spec.whatwg.org/#obtain-a-storage-key
url::Origin ObtainStorageKey() { return url::Origin::Create(creation_url()); }
- static Context* context(script::EnvironmentSettings* environment_settings);
- static script::GlobalEnvironment* global_environment(
- script::EnvironmentSettings* environment_settings);
- static script::Wrappable* global_wrappable(
- script::EnvironmentSettings* environment_settings);
- static script::ScriptValueFactory* script_value_factory(
- script::EnvironmentSettings* environment_settings);
-
protected:
friend std::unique_ptr<EnvironmentSettings>::deleter_type;
diff --git a/cobalt/web/event_target.h b/cobalt/web/event_target.h
index 7484d98..134fe79 100644
--- a/cobalt/web/event_target.h
+++ b/cobalt/web/event_target.h
@@ -303,6 +303,13 @@
SetAttributeEventListener(base::Tokens::scroll(), event_listener);
}
+ const EventListenerScriptValue* onpointercancel() {
+ return GetAttributeEventListener(base::Tokens::pointercancel());
+ }
+ void set_onpointercancel(const EventListenerScriptValue& event_listener) {
+ SetAttributeEventListener(base::Tokens::pointercancel(), event_listener);
+ }
+
const EventListenerScriptValue* ongotpointercapture() {
return GetAttributeEventListener(base::Tokens::gotpointercapture());
}
diff --git a/cobalt/web/on_error_event_listener.h b/cobalt/web/on_error_event_listener.h
index 18754ca..d7455c3 100644
--- a/cobalt/web/on_error_event_listener.h
+++ b/cobalt/web/on_error_event_listener.h
@@ -45,7 +45,7 @@
protected:
virtual base::Optional<bool> HandleEvent(
const scoped_refptr<script::Wrappable>& callback_this,
- EventOrMessage message, const std::string& filename, uint32 lineno,
+ const EventOrMessage& message, const std::string& filename, uint32 lineno,
uint32 colno, const script::ValueHandleHolder* error,
bool* had_exception) const = 0;
};
diff --git a/cobalt/web/testing/BUILD.gn b/cobalt/web/testing/BUILD.gn
index 17f0326..8c67e01 100644
--- a/cobalt/web/testing/BUILD.gn
+++ b/cobalt/web/testing/BUILD.gn
@@ -28,6 +28,7 @@
"//base",
"//base/test:test_support",
"//cobalt/base",
+ "//cobalt/browser",
"//cobalt/loader",
"//cobalt/network",
"//cobalt/script",
diff --git a/cobalt/web/testing/stub_web_context.h b/cobalt/web/testing/stub_web_context.h
index dd97ea3..311c255 100644
--- a/cobalt/web/testing/stub_web_context.h
+++ b/cobalt/web/testing/stub_web_context.h
@@ -32,6 +32,7 @@
#include "cobalt/web/testing/stub_environment_settings.h"
#include "cobalt/web/url.h"
#include "cobalt/web/user_agent_platform_info.h"
+#include "cobalt/web/web_settings.h"
#include "cobalt/worker/service_worker.h"
#include "cobalt/worker/service_worker_object.h"
#include "cobalt/worker/service_worker_registration.h"
@@ -51,6 +52,7 @@
javascript_engine_ = script::JavaScriptEngine::CreateEngine();
global_environment_ = javascript_engine_->CreateGlobalEnvironment();
blob_registry_.reset(new Blob::Registry);
+ web_settings_.reset(new WebSettingsImpl());
network_module_.reset(new network::NetworkModule());
fetcher_factory_.reset(new loader::FetcherFactory(
network_module_.get(),
@@ -99,6 +101,10 @@
DCHECK(blob_registry_);
return blob_registry_.get();
}
+ web::WebSettings* web_settings() const final {
+ DCHECK(web_settings_);
+ return web_settings_.get();
+ }
network::NetworkModule* network_module() const final {
DCHECK(network_module_);
return network_module_.get();
@@ -207,6 +213,7 @@
std::unique_ptr<script::JavaScriptEngine> javascript_engine_;
scoped_refptr<script::GlobalEnvironment> global_environment_;
+ std::unique_ptr<WebSettingsImpl> web_settings_;
std::unique_ptr<network::NetworkModule> network_module_;
// Environment Settings object
std::unique_ptr<EnvironmentSettings> environment_settings_;
diff --git a/cobalt/web/web_settings.h b/cobalt/web/web_settings.h
new file mode 100644
index 0000000..3079c31
--- /dev/null
+++ b/cobalt/web/web_settings.h
@@ -0,0 +1,91 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_WEB_WEB_SETTINGS_H_
+#define COBALT_WEB_WEB_SETTINGS_H_
+
+#include <string>
+
+#include "cobalt/dom/media_settings.h"
+#include "cobalt/xhr/xhr_settings.h"
+#include "starboard/types.h"
+
+namespace cobalt {
+namespace web {
+
+// The `WebSettings` interface and its concrete implementation
+// `WebSettingsImpl` provide a convenient way to share browser wide settings
+// that can be read by various modules of Cobalt across the main web module and
+// all Workers.
+//
+// These settings can be updated by the web app via `h5vcc.settings` or other
+// web interfaces.
+//
+// The following should be introduced when a group of new settings are needed;
+// 1. A new interface close to where the code using these settings to allow C++
+// code read the settings, like the `XhrSettings` used below.
+// A pure virtual function should be added to `WebSettings` to return a
+// pointer to this interface.
+// 2. A concrete class inherited from the new interface to allow the settings to
+// be updated.
+// A member variable as an instance of this class should be added to
+// `WebSettingsImpl` and hooked up in `WebSettingsImpl::Set()`.
+
+// A centralized interface to hold all setting interfaces to allow them to be
+// accessed. Any modifications to the settings should be done through the
+// concrete class below.
+class WebSettings {
+ public:
+ virtual const dom::MediaSettings& media_settings() const = 0;
+ virtual const xhr::XhrSettings& xhr_settings() const = 0;
+
+ protected:
+ ~WebSettings() {}
+};
+
+// A centralized class to hold all concrete setting classes to allow
+// modifications. It exposes the Set() function to allow updating settings via
+// a name.
+class WebSettingsImpl final : public web::WebSettings {
+ public:
+ const dom::MediaSettings& media_settings() const override {
+ return media_settings_impl_;
+ }
+
+ const xhr::XhrSettingsImpl& xhr_settings() const override {
+ return xhr_settings_impl_;
+ }
+
+ bool Set(const std::string& name, int32_t value) {
+ if (media_settings_impl_.Set(name, value)) {
+ return true;
+ }
+
+ if (xhr_settings_impl_.Set(name, value)) {
+ return true;
+ }
+
+ LOG(WARNING) << "Failed to set " << name << " to " << value;
+ return false;
+ }
+
+ private:
+ dom::MediaSettingsImpl media_settings_impl_;
+ xhr::XhrSettingsImpl xhr_settings_impl_;
+};
+
+} // namespace web
+} // namespace cobalt
+
+#endif // COBALT_WEB_WEB_SETTINGS_H_
diff --git a/cobalt/web/window_or_worker_global_scope.h b/cobalt/web/window_or_worker_global_scope.h
index cbb74dc..d2580ac 100644
--- a/cobalt/web/window_or_worker_global_scope.h
+++ b/cobalt/web/window_or_worker_global_scope.h
@@ -99,6 +99,8 @@
return nullptr;
}
+ // The CspDelegate gives access to the CSP list of the policy container
+ // https://html.spec.whatwg.org/commit-snapshots/ae3c91103aada3d6d346a6dd3c5356773195fc79/#policy-container
web::CspDelegate* csp_delegate() const {
DCHECK(csp_delegate_);
return csp_delegate_.get();
@@ -134,7 +136,7 @@
void DestroyTimers();
// Web API: GlobalCacheStorage (implements)
- // https://w3c.github.io/ServiceWorker/#cachestorage
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#cachestorage
scoped_refptr<CacheStorage> caches() const;
// Web API: GlobalCrypto (implements)
diff --git a/cobalt/worker/BUILD.gn b/cobalt/worker/BUILD.gn
index cf05426..f56409c 100644
--- a/cobalt/worker/BUILD.gn
+++ b/cobalt/worker/BUILD.gn
@@ -30,6 +30,8 @@
"extendable_event.h",
"extendable_message_event.cc",
"extendable_message_event.h",
+ "fetch_event.cc",
+ "fetch_event.h",
"navigation_preload_manager.cc",
"navigation_preload_manager.h",
"service_worker.cc",
@@ -42,6 +44,8 @@
"service_worker_jobs.h",
"service_worker_object.cc",
"service_worker_object.h",
+ "service_worker_persistent_settings.cc",
+ "service_worker_persistent_settings.h",
"service_worker_registration.cc",
"service_worker_registration.h",
"service_worker_registration_map.cc",
@@ -63,6 +67,7 @@
deps = [
"//cobalt/browser:generated_bindings",
+ "//cobalt/loader",
"//cobalt/web",
"//url",
]
diff --git a/cobalt/worker/client.cc b/cobalt/worker/client.cc
index 20349f9..7ca45be 100644
--- a/cobalt/worker/client.cc
+++ b/cobalt/worker/client.cc
@@ -29,7 +29,7 @@
->service_worker()) {
DCHECK(client);
// Algorithm for Create Client:
- // https://w3c.github.io/ServiceWorker/#create-client
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#create-client
// 1. Let clientObject be a new Client object.
// 2. Set clientObject’s service worker client to client.
service_worker_client_ = client;
@@ -39,7 +39,7 @@
ClientType Client::type() {
// Algorithm for the type getter:
- // https://w3c.github.io/ServiceWorker/#client-type
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#client-type
// 1. Let client be this's service worker client.
web::WindowOrWorkerGlobalScope* client_global_scope =
diff --git a/cobalt/worker/client.h b/cobalt/worker/client.h
index 53a5f8f..70179aa 100644
--- a/cobalt/worker/client.h
+++ b/cobalt/worker/client.h
@@ -28,22 +28,22 @@
class Client : public web::MessagePort {
public:
- // https://w3c.github.io/ServiceWorker/#create-client-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#create-client-algorithm
static scoped_refptr<Client> Create(web::EnvironmentSettings* client) {
return new Client(client);
}
~Client() { service_worker_client_ = nullptr; }
std::string url() {
- // https://w3c.github.io/ServiceWorker/#client-url
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#client-url
return service_worker_client_->creation_url().spec();
}
- // https://w3c.github.io/ServiceWorker/#client-frametype
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#client-frametype
void set_frame_type(FrameType frame_type) { frame_type_ = frame_type; }
FrameType frame_type() { return frame_type_; }
- // https://w3c.github.io/ServiceWorker/#dom-client-id
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dom-client-id
std::string id() { return service_worker_client_->id(); }
ClientType type();
diff --git a/cobalt/worker/clients.cc b/cobalt/worker/clients.cc
index b1ce469..0a81e08 100644
--- a/cobalt/worker/clients.cc
+++ b/cobalt/worker/clients.cc
@@ -59,7 +59,7 @@
TRACE_EVENT0("cobalt::worker", "Clients::Get()");
DCHECK_EQ(base::MessageLoop::current(), settings_->context()->message_loop());
// Algorithm for get(id):
- // https://w3c.github.io/ServiceWorker/#clients-get
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#clients-get
// 1. Let promise be a new promise.
script::HandlePromiseWrappable promise =
settings_->context()
@@ -89,7 +89,7 @@
TRACE_EVENT0("cobalt::worker", "Clients::MatchAll()");
DCHECK_EQ(base::MessageLoop::current(), settings_->context()->message_loop());
// Algorithm for matchAll():
- // https://w3c.github.io/ServiceWorker/#clients-matchall
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#clients-matchall
// 1. Let promise be a new promise.
auto promise = settings_->context()
->global_environment()
@@ -117,7 +117,7 @@
TRACE_EVENT0("cobalt::worker", "Clients::Claim()");
DCHECK_EQ(base::MessageLoop::current(), settings_->context()->message_loop());
// Algorithm for claim():
- // https://w3c.github.io/ServiceWorker/#clients-claim
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#clients-claim
// 2. Let promise be a new promise.
// Note: Done first because it's needed for rejecting in step 1.
auto promise = settings_->context()
diff --git a/cobalt/worker/dedicated_worker.cc b/cobalt/worker/dedicated_worker.cc
index 1daf030..39251a7 100644
--- a/cobalt/worker/dedicated_worker.cc
+++ b/cobalt/worker/dedicated_worker.cc
@@ -33,41 +33,48 @@
} // namespace
DedicatedWorker::DedicatedWorker(script::EnvironmentSettings* settings,
- const std::string& scriptURL)
+ const std::string& scriptURL,
+ script::ExceptionState* exception_state)
: web::EventTarget(settings), script_url_(scriptURL) {
- Initialize();
+ Initialize(exception_state);
}
DedicatedWorker::DedicatedWorker(script::EnvironmentSettings* settings,
const std::string& scriptURL,
- const WorkerOptions& worker_options)
+ const WorkerOptions& worker_options,
+ script::ExceptionState* exception_state)
: web::EventTarget(settings),
script_url_(scriptURL),
worker_options_(worker_options) {
- Initialize();
+ Initialize(exception_state);
}
-void DedicatedWorker::Initialize() {
+void DedicatedWorker::Initialize(script::ExceptionState* exception_state) {
// Algorithm for the Worker constructor.
// https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#dom-worker
// 1. The user agent may throw a "SecurityError" web::DOMException if the
- // request
- // violates a policy decision (e.g. if the user agent is configured to
- // not
- // allow the page to start dedicated workers).
+ // request violates a policy decision (e.g. if the user agent is configured
+ // to not allow the page to start dedicated workers).
// 2. Let outside settings be the current settings object.
// 3. Parse the scriptURL argument relative to outside settings.
Worker::Options options;
const GURL& base_url = environment_settings()->base_url();
options.url = base_url.Resolve(script_url_);
- LOG_IF(WARNING, !options.url.is_valid())
- << script_url_ << " cannot be resolved based on " << base_url << ".";
-
// 4. If this fails, throw a "SyntaxError" web::DOMException.
+ if (!options.url.is_valid()) {
+ web::DOMException::Raise(
+ web::DOMException::kSyntaxErr,
+ script_url_ + " cannot be resolved based on " + base_url.spec() + ".",
+ exception_state);
+ return;
+ }
+
// 5. Let worker URL be the resulting URL record.
options.web_options.stack_size = cobalt::browser::kWorkerStackSize;
+ options.web_options.web_settings =
+ environment_settings()->context()->web_settings();
options.web_options.network_module =
environment_settings()->context()->network_module();
// 6. Let worker be a new Worker object.
@@ -77,12 +84,23 @@
// 9. Run this step in parallel:
// 1. Run a worker given worker, worker URL, outside settings, outside
// port, and options.
- options.outside_settings = environment_settings();
+ options.outside_context = environment_settings()->context();
options.outside_port = outside_port_.get();
options.options = worker_options_;
options.web_options.service_worker_jobs =
- options.outside_settings->context()->service_worker_jobs();
-
+ options.outside_context->service_worker_jobs();
+ // Store the current source location as the construction location, to be used
+ // in the error event if worker loading of initialization fails.
+ auto stack_trace =
+ options.outside_context->global_environment()->GetStackTrace(0);
+ if (stack_trace.size() > 0) {
+ options.construction_location = base::SourceLocation(
+ stack_trace[0].source_url, stack_trace[0].line_number,
+ stack_trace[0].column_number);
+ } else {
+ options.construction_location.file_path =
+ environment_settings()->creation_url().spec();
+ }
worker_.reset(new Worker(kDedicatedWorkerName, options));
// 10. Return worker.
}
diff --git a/cobalt/worker/dedicated_worker.h b/cobalt/worker/dedicated_worker.h
index b1264d8..9a88582 100644
--- a/cobalt/worker/dedicated_worker.h
+++ b/cobalt/worker/dedicated_worker.h
@@ -35,9 +35,11 @@
class DedicatedWorker : public AbstractWorker, public web::EventTarget {
public:
DedicatedWorker(script::EnvironmentSettings* settings,
- const std::string& scriptURL);
+ const std::string& scriptURL,
+ script::ExceptionState* exception_state);
DedicatedWorker(script::EnvironmentSettings* settings,
- const std::string& scriptURL, const WorkerOptions& options);
+ const std::string& scriptURL, const WorkerOptions& options,
+ script::ExceptionState* exception_state);
DedicatedWorker(const DedicatedWorker&) = delete;
DedicatedWorker& operator=(const DedicatedWorker&) = delete;
@@ -78,7 +80,7 @@
private:
~DedicatedWorker() override;
- void Initialize();
+ void Initialize(script::ExceptionState* exception_state);
const std::string script_url_;
const WorkerOptions worker_options_;
diff --git a/cobalt/worker/dedicated_worker_global_scope.cc b/cobalt/worker/dedicated_worker_global_scope.cc
index a03e8a0..7043148 100644
--- a/cobalt/worker/dedicated_worker_global_scope.cc
+++ b/cobalt/worker/dedicated_worker_global_scope.cc
@@ -30,9 +30,9 @@
: WorkerGlobalScope(settings), cross_origin_isolated_capability_(false) {
// Algorithm for 'run a worker'
// https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#run-a-worker
- // 14.9. If is shared is false and owner's cross-origin isolated
- // capability is false, then set worker global scope's cross-origin
- // isolated capability to false.
+ // 14.9. If is shared is false and owner's cross-origin isolated
+ // capability is false, then set worker global scope's cross-origin
+ // isolated capability to false.
if (!parent_cross_origin_isolated_capability) {
cross_origin_isolated_capability_ = false;
}
@@ -41,32 +41,40 @@
void DedicatedWorkerGlobalScope::Initialize() {
// Algorithm for 'run a worker'
// https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#run-a-worker
- // 14.4. Initialize worker global scope's policy container given worker
- // global scope, response, and inside settings.
- // 14.5. If the Run CSP initialization for a global object algorithm
- // returns
- // "Blocked" when executed upon worker global scope, set response to a
- // network error. [CSP]
+ // 14.4. Initialize worker global scope's policy container given worker
+ // global scope, response, and inside settings.
+ InitializePolicyContainer();
+ // 14.5. If the Run CSP initialization for a global object algorithm
+ // returns "Blocked" when executed upon worker global scope, set
+ // response to a network error. [CSP]
- // 14.6. If worker global scope's embedder policy's value is compatible
- // with
- // cross-origin isolation and is shared is true, then set agent's agent
- // cluster's cross-origin isolation mode to "logical" or "concrete".
- // The one chosen is implementation-defined.
- // No need for dedicated worker.
+ // Note: This is a no-op here:
+ // The "Run CSP initialization for a global object algorithm"
+ // https://www.w3.org/TR/2023/WD-CSP3-20230110/#run-global-object-csp-initialization
+ // Only returns "Blocked" if a "directive’s initialization algorithm on
+ // global" returns "Blocked".
+ // And "directive’s initialization algorithm on global" is this:
+ // https://www.w3.org/TR/2023/WD-CSP3-20230110/#directive-initialization
+ // Unless otherwise specified, it has no effect and it returns "Allowed".
- // 14.7. If the result of checking a global object's embedder policy with
- // worker global scope, outside settings, and response is false, then
- // set response to a network error.
- // 14.8. Set worker global scope's cross-origin isolated capability to
- // true
- // if agent's agent cluster's cross-origin isolation mode is
- // "concrete".
+ // 14.6. If worker global scope's embedder policy's value is compatible with
+ // cross-origin isolation and is shared is true, then set agent's agent
+ // cluster's cross-origin isolation mode to "logical" or "concrete".
+ // The one chosen is implementation-defined.
+ // No need for dedicated worker.
- // 14.10. If is shared is false and response's url's scheme is "data",
- // then
- // set worker global scope's cross-origin isolated capability to
- // false.
+ // 14.7. If the result of checking a global object's embedder policy with
+ // worker global scope, outside settings, and response is false, then
+ // set response to a network error.
+ // Cobalt does not implement embedder policy.
+ // 14.8. Set worker global scope's cross-origin isolated capability to true
+ // if agent's agent cluster's cross-origin isolation mode is
+ // "concrete".
+ // Cobalt does not implement "concrete" cross-origin isolation mode.
+
+ // 14.10. If is shared is false and response's url's scheme is "data", then
+ // set worker global scope's cross-origin isolated capability to
+ // false.
if (!Url().SchemeIs("data")) {
cross_origin_isolated_capability_ = false;
}
@@ -79,6 +87,27 @@
->PostMessage(message);
}
+void DedicatedWorkerGlobalScope::InitializePolicyContainer() {
+ // Algorithm for Initialize a worker global scope's policy container
+ // https://html.spec.whatwg.org/commit-snapshots/814668ef2d1919a2a9387a0b29ebc6df7748fa80/#initialize-worker-policy-container
+ // 1. If workerGlobalScope's url is local but its scheme is not "blob":
+ // URL: https://fetch.spec.whatwg.org/#local-scheme: A local scheme is
+ // "about", "blob", or "data".
+ if (Url().SchemeIs(url::kAboutScheme) || Url().SchemeIs(url::kDataScheme)) {
+ // 1.1. Assert: workerGlobalScope's owner set's size is 1.
+ DCHECK(owner_set()->size() == 1);
+ // 1.2. Set workerGlobalScope's policy container to a clone of
+ // workerGlobalScope's owner set[0]'s relevant settings object's
+ // policy container.
+ auto* owner = *owner_set()->begin();
+ DCHECK(owner->csp_delegate()->GetPolicyContainer());
+ csp_delegate()->ClonePolicyContainer(
+ *owner->csp_delegate()->GetPolicyContainer());
+ }
+ // 2. Otherwise, set workerGlobalScope's policy container to the result
+ // of creating a policy container from a fetch response given response
+ // and environment.
+}
} // namespace worker
} // namespace cobalt
diff --git a/cobalt/worker/dedicated_worker_global_scope.h b/cobalt/worker/dedicated_worker_global_scope.h
index 3793608..4252b4f 100644
--- a/cobalt/worker/dedicated_worker_global_scope.h
+++ b/cobalt/worker/dedicated_worker_global_scope.h
@@ -79,6 +79,8 @@
~DedicatedWorkerGlobalScope() override {}
private:
+ void InitializePolicyContainer();
+
bool cross_origin_isolated_capability_;
std::string name_;
diff --git a/cobalt/worker/extendable_event.cc b/cobalt/worker/extendable_event.cc
index 6b49b47..5aaa923 100644
--- a/cobalt/worker/extendable_event.cc
+++ b/cobalt/worker/extendable_event.cc
@@ -22,7 +22,7 @@
std::unique_ptr<script::Promise<script::ValueHandle*>>& promise,
script::ExceptionState* exception_state) {
// Algorithm for waitUntil(), to add lifetime promise to event.
- // https://w3c.github.io/ServiceWorker/#dom-extendableevent-waituntil
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dom-extendableevent-waituntil
// 1. If event’s isTrusted attribute is false, throw an "InvalidStateError"
// DOMException.
@@ -50,7 +50,7 @@
const script::Promise<script::ValueHandle*>* promise) {
// Implement the microtask called upon fulfillment or rejection of a
// promise, as part of the algorithm for waitUntil().
- // https://w3c.github.io/ServiceWorker/#dom-extendableevent-waituntil
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dom-extendableevent-waituntil
DCHECK(promise);
has_rejected_promise_ |= promise->State() == script::PromiseState::kRejected;
// 5.1. Decrement event’s pending promises count by one.
diff --git a/cobalt/worker/extendable_event.h b/cobalt/worker/extendable_event.h
index 949c7b1..f915189 100644
--- a/cobalt/worker/extendable_event.h
+++ b/cobalt/worker/extendable_event.h
@@ -65,7 +65,7 @@
// An ExtendableEvent object is said to be active when its timed out flag
// is unset and either its pending promises count is greater than zero or
// its dispatch flag is set.
- // https://w3c.github.io/ServiceWorker/#extendableevent-active
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#extendableevent-active
return !timed_out_flag_ &&
((pending_promise_count_ > 0) || IsBeingDispatched());
}
@@ -78,11 +78,11 @@
~ExtendableEvent() override {}
private:
- // https://w3c.github.io/ServiceWorker/#extendableevent-extend-lifetime-promises
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#extendableevent-extend-lifetime-promises
// std::list<script::Promise<script::ValueHandle*>> extend_lifetime_promises_;
int pending_promise_count_ = 0;
bool has_rejected_promise_ = false;
- // https://w3c.github.io/ServiceWorker/#extendableevent-timed-out-flag
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#extendableevent-timed-out-flag
bool timed_out_flag_ = false;
base::OnceCallback<void(bool)> done_callback_;
diff --git a/cobalt/worker/fetch_event.cc b/cobalt/worker/fetch_event.cc
new file mode 100644
index 0000000..4480643
--- /dev/null
+++ b/cobalt/worker/fetch_event.cc
@@ -0,0 +1,169 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/worker/fetch_event.h"
+
+#include <memory>
+#include <utility>
+
+#include "cobalt/script/v8c/conversion_helpers.h"
+#include "cobalt/script/v8c/v8c_value_handle.h"
+#include "cobalt/web/cache_utils.h"
+#include "cobalt/web/environment_settings_helper.h"
+
+namespace cobalt {
+namespace worker {
+
+FetchEvent::FetchEvent(script::EnvironmentSettings* environment_settings,
+ const std::string& type,
+ const FetchEventInit& event_init_dict)
+ : FetchEvent(
+ environment_settings, base::Token(type), event_init_dict,
+ /*respond_with_callback=*/std::make_unique<RespondWithCallback>(),
+ /*report_load_timing_info=*/
+ std::make_unique<ReportLoadTimingInfo>()) {}
+
+FetchEvent::FetchEvent(
+ script::EnvironmentSettings* environment_settings, base::Token type,
+ const FetchEventInit& event_init_dict,
+ std::unique_ptr<RespondWithCallback> respond_with_callback,
+ std::unique_ptr<ReportLoadTimingInfo> report_load_timing_info)
+ : ExtendableEvent(type, event_init_dict),
+ respond_with_callback_(std::move(respond_with_callback)),
+ report_load_timing_info_(std::move(report_load_timing_info)) {
+ auto script_value_factory =
+ web::get_script_value_factory(environment_settings);
+ handled_property_ = std::make_unique<script::ValuePromiseVoid::Reference>(
+ this, script_value_factory->CreateBasicPromise<void>());
+ request_ = std::make_unique<script::ValueHandleHolder::Reference>(
+ this, event_init_dict.request());
+
+ load_timing_info_.request_start = base::TimeTicks::Now();
+ load_timing_info_.request_start_time = base::Time::Now();
+ load_timing_info_.send_start = base::TimeTicks::Now();
+ load_timing_info_.send_end = base::TimeTicks::Now();
+ load_timing_info_.service_worker_start_time = base::TimeTicks::Now();
+}
+
+void FetchEvent::RespondWith(
+ script::EnvironmentSettings* environment_settings,
+ std::unique_ptr<script::Promise<script::ValueHandle*>>& response) {
+ respond_with_called_ = true;
+
+ // TODO: call |WaitUntil()|.
+ v8::Local<v8::Promise> v8_response = response->promise();
+ auto* global_environment = web::get_global_environment(environment_settings);
+ auto* isolate = global_environment->isolate();
+ auto context = isolate->GetCurrentContext();
+ auto data = v8::Object::New(isolate);
+ web::cache_utils::SetExternal(context, data, "environment_settings",
+ environment_settings);
+ web::cache_utils::SetOwnedExternal(context, data, "respond_with_callback",
+ std::move(respond_with_callback_));
+ web::cache_utils::SetOwnedExternal(context, data, "report_load_timing_info",
+ std::move(report_load_timing_info_));
+ web::cache_utils::SetExternal(context, data, "load_timing_info",
+ &load_timing_info_);
+ web::cache_utils::SetExternal(context, data, "handled",
+ handled_property_.get());
+ auto result = v8_response->Then(
+ context,
+ v8::Function::New(
+ context,
+ [](const v8::FunctionCallbackInfo<v8::Value>& info) {
+ auto* isolate = info.GetIsolate();
+ auto context = info.GetIsolate()->GetCurrentContext();
+ v8::Local<v8::Value> text_result;
+ if (!web::cache_utils::TryCall(context, /*object=*/info[0], "text")
+ .ToLocal(&text_result)) {
+ auto respond_with_callback =
+ web::cache_utils::GetOwnedExternal<RespondWithCallback>(
+ context, info.Data(), "respond_with_callback");
+ std::move(*respond_with_callback)
+ .Run(std::make_unique<std::string>());
+ return;
+ }
+ auto* load_timing_info =
+ web::cache_utils::GetExternal<net::LoadTimingInfo>(
+ context, info.Data(), "load_timing_info");
+ load_timing_info->receive_headers_end = base::TimeTicks::Now();
+ auto result = text_result.As<v8::Promise>()->Then(
+ context,
+ v8::Function::New(
+ context,
+ [](const v8::FunctionCallbackInfo<v8::Value>& info) {
+ auto* isolate = info.GetIsolate();
+ auto context = info.GetIsolate()->GetCurrentContext();
+ auto* handled = web::cache_utils::GetExternal<
+ script::ValuePromiseVoid::Reference>(
+ context, info.Data(), "handled");
+ handled->value().Resolve();
+ auto respond_with_callback =
+ web::cache_utils::GetOwnedExternal<
+ RespondWithCallback>(context, info.Data(),
+ "respond_with_callback");
+ auto body = std::make_unique<std::string>();
+ FromJSValue(isolate, info[0],
+ script::v8c::kNoConversionFlags, nullptr,
+ body.get());
+ auto* load_timing_info =
+ web::cache_utils::GetExternal<net::LoadTimingInfo>(
+ context, info.Data(), "load_timing_info");
+ auto report_load_timing_info =
+ web::cache_utils::GetOwnedExternal<
+ ReportLoadTimingInfo>(context, info.Data(),
+ "report_load_timing_info");
+ std::move(*report_load_timing_info)
+ .Run(*load_timing_info);
+ auto* environment_settings =
+ web::cache_utils::GetExternal<
+ script::EnvironmentSettings>(
+ context, info.Data(), "environment_settings");
+ web::get_context(environment_settings)
+ ->network_module()
+ ->task_runner()
+ ->PostTask(
+ FROM_HERE,
+ base::BindOnce(
+ [](std::unique_ptr<base::OnceCallback<void(
+ std::unique_ptr<std::string>)>>
+ respond_with_callback,
+ std::unique_ptr<std::string> body) {
+ std::move(*respond_with_callback)
+ .Run(std::move(body));
+ },
+ std::move(respond_with_callback),
+ std::move(body)));
+ },
+ info.Data())
+ .ToLocalChecked());
+ if (result.IsEmpty()) {
+ LOG(WARNING) << "Failure during FetchEvent respondWith handling. "
+ "Retrieving Response text failed.";
+ }
+ },
+ data)
+ .ToLocalChecked());
+ if (result.IsEmpty()) {
+ LOG(WARNING) << "Failure during FetchEvent respondWith handling.";
+ }
+}
+
+script::HandlePromiseVoid FetchEvent::handled(
+ script::EnvironmentSettings* environment_settings) {
+ return script::HandlePromiseVoid(handled_property_->referenced_value());
+}
+
+} // namespace worker
+} // namespace cobalt
diff --git a/cobalt/worker/fetch_event.h b/cobalt/worker/fetch_event.h
new file mode 100644
index 0000000..ac3ff69
--- /dev/null
+++ b/cobalt/worker/fetch_event.h
@@ -0,0 +1,73 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_WORKER_FETCH_EVENT_H_
+#define COBALT_WORKER_FETCH_EVENT_H_
+
+#include <memory>
+#include <string>
+
+#include "base/bind.h"
+#include "cobalt/base/token.h"
+#include "cobalt/script/environment_settings.h"
+#include "cobalt/script/exception_state.h"
+#include "cobalt/script/value_handle.h"
+#include "cobalt/worker/extendable_event.h"
+#include "cobalt/worker/fetch_event_init.h"
+#include "net/base/load_timing_info.h"
+
+namespace cobalt {
+namespace worker {
+
+class FetchEvent : public ::cobalt::worker::ExtendableEvent {
+ public:
+ using RespondWithCallback =
+ base::OnceCallback<void(std::unique_ptr<std::string>)>;
+ using ReportLoadTimingInfo =
+ base::OnceCallback<void(const net::LoadTimingInfo&)>;
+
+ FetchEvent(script::EnvironmentSettings*, const std::string& type,
+ const FetchEventInit& event_init_dict);
+ FetchEvent(script::EnvironmentSettings*, base::Token type,
+ const FetchEventInit& event_init_dict,
+ std::unique_ptr<RespondWithCallback> respond_with_callback,
+ std::unique_ptr<ReportLoadTimingInfo> report_load_timing_info);
+ ~FetchEvent() override = default;
+
+ void RespondWith(
+ script::EnvironmentSettings*,
+ std::unique_ptr<script::Promise<script::ValueHandle*>>& response);
+ script::HandlePromiseVoid handled(script::EnvironmentSettings*);
+
+ const script::ValueHandleHolder* request() {
+ return &(request_->referenced_value());
+ }
+
+ bool respond_with_called() const { return respond_with_called_; }
+
+ DEFINE_WRAPPABLE_TYPE(FetchEvent);
+
+ private:
+ std::unique_ptr<RespondWithCallback> respond_with_callback_;
+ std::unique_ptr<ReportLoadTimingInfo> report_load_timing_info_;
+ std::unique_ptr<script::ValueHandleHolder::Reference> request_;
+ std::unique_ptr<script::ValuePromiseVoid::Reference> handled_property_;
+ bool respond_with_called_ = false;
+ net::LoadTimingInfo load_timing_info_;
+};
+
+} // namespace worker
+} // namespace cobalt
+
+#endif // COBALT_WORKER_FETCH_EVENT_H_
diff --git a/cobalt/worker/fetch_event.idl b/cobalt/worker/fetch_event.idl
new file mode 100644
index 0000000..04253c3
--- /dev/null
+++ b/cobalt/worker/fetch_event.idl
@@ -0,0 +1,29 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// https://w3c.github.io/ServiceWorker/#fetchevent-interface
+
+[
+ Exposed=ServiceWorker,
+ ConstructorCallWith=EnvironmentSettings,
+ Constructor(DOMString type, FetchEventInit eventInitDict)
+] interface FetchEvent : ExtendableEvent {
+ // Returns |any| type because |Request| is polyfilled.
+ [SameObject] readonly attribute any request;
+
+
+ // Takes a |Promise<any>| because |Response| is polyfilled.
+ [CallWith=EnvironmentSettings] void respondWith(Promise<any> response);
+ [CallWith=EnvironmentSettings] readonly attribute Promise<void> handled;
+};
diff --git a/cobalt/worker/fetch_event_init.idl b/cobalt/worker/fetch_event_init.idl
new file mode 100644
index 0000000..ace8fb1
--- /dev/null
+++ b/cobalt/worker/fetch_event_init.idl
@@ -0,0 +1,23 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// https://w3c.github.io/ServiceWorker/#dictdef-fetcheventinit
+
+dictionary FetchEventInit : ExtendableEventInit {
+ // |any| type because |Request| is polyfilled.
+ any request = null;
+ DOMString clientId = "";
+ DOMString resultingClientId = "";
+ boolean isReload = false;
+};
diff --git a/cobalt/worker/service_worker.cc b/cobalt/worker/service_worker.cc
index 31480b2..de9d193 100644
--- a/cobalt/worker/service_worker.cc
+++ b/cobalt/worker/service_worker.cc
@@ -39,7 +39,7 @@
void ServiceWorker::PostMessage(const script::ValueHandleHolder& message) {
// Algorithm for ServiceWorker.postMessage():
- // https://w3c.github.io/ServiceWorker/#service-worker-postmessage
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#service-worker-postmessage
// 1. Let serviceWorker be the service worker represented by this.
ServiceWorkerObject* service_worker = service_worker_object();
@@ -55,8 +55,7 @@
return;
}
// 5. If the result of running the Should Skip Event algorithm with
- // "message"
- // and serviceWorker is true, then return.
+ // "message" and serviceWorker is true, then return.
if (service_worker->ShouldSkipEvent(base::Tokens::message())) return;
// 6. Run these substeps in parallel:
ServiceWorkerJobs* jobs =
diff --git a/cobalt/worker/service_worker.h b/cobalt/worker/service_worker.h
index e00424f..07d4df5 100644
--- a/cobalt/worker/service_worker.h
+++ b/cobalt/worker/service_worker.h
@@ -34,7 +34,7 @@
// The ServiceWorker interface represents a service worker within a service
// worker client realm.
-// https://w3c.github.io/ServiceWorker/#serviceworker-interface
+// https://www.w3.org/TR/2022/CRD-service-workers-20220712/#serviceworker-interface
class ServiceWorker : public AbstractWorker, public web::EventTarget {
public:
ServiceWorker(script::EnvironmentSettings* settings,
@@ -50,7 +50,7 @@
// service worker's serialized script url.
std::string script_url() const { return worker_->script_url().spec(); }
- // https://w3c.github.io/ServiceWorker/#dom-serviceworker-state
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dom-serviceworker-state
void set_state(ServiceWorkerState state) { state_ = state; }
ServiceWorkerState state() const { return state_; }
diff --git a/cobalt/worker/service_worker_container.cc b/cobalt/worker/service_worker_container.cc
index 43fb13a..97677b2 100644
--- a/cobalt/worker/service_worker_container.cc
+++ b/cobalt/worker/service_worker_container.cc
@@ -42,7 +42,7 @@
scoped_refptr<ServiceWorker> ServiceWorkerContainer::controller() {
// Algorithm for controller:
- // https://w3c.github.io/ServiceWorker/#navigator-service-worker-controller
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#navigator-service-worker-controller
// 1. Let client be this's service worker client.
web::EnvironmentSettings* client = environment_settings();
@@ -59,7 +59,7 @@
script::HandlePromiseWrappable ServiceWorkerContainer::ready() {
// Algorithm for ready attribute:
- // https://w3c.github.io/ServiceWorker/#navigator-service-worker-ready
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#navigator-service-worker-ready
// 1. If this's ready promise is null, then set this's ready promise to a new
// promise.
if (!promise_reference_) {
@@ -92,8 +92,8 @@
ServiceWorkerRegistrationObject* registration) {
// This implements resolving of the ready promise for the Activate algorithm
// (steps 7.1-7.3) as well as for the ready attribute (step 3.3).
- // https://w3c.github.io/ServiceWorker/#activation-algorithm
- // https://w3c.github.io/ServiceWorker/#navigator-service-worker-ready
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#activation-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#navigator-service-worker-ready
TRACE_EVENT0("cobalt::worker",
"ServiceWorkerContainer::MaybeResolveReadyPromise()");
DCHECK_EQ(base::MessageLoop::current(),
@@ -123,7 +123,7 @@
TRACE_EVENT0("cobalt::worker", "ServiceWorkerContainer::Register()");
DCHECK_EQ(base::MessageLoop::current(),
environment_settings()->context()->message_loop());
- // https://w3c.github.io/ServiceWorker/#navigator-service-worker-register
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#navigator-service-worker-register
// 1. Let p be a promise.
script::HandlePromiseWrappable promise =
environment_settings()
@@ -165,7 +165,7 @@
DCHECK_EQ(base::MessageLoop::current(),
environment_settings()->context()->message_loop());
// Algorithm for 'ServiceWorkerContainer.getRegistration()':
- // https://w3c.github.io/ServiceWorker/#navigator-service-worker-getRegistration
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#navigator-service-worker-getRegistration
// Let promise be a new promise.
// Perform the rest of the steps in a task, because the promise has to be
// returned before we can safely reject or resolve it.
@@ -195,7 +195,7 @@
DCHECK_EQ(base::MessageLoop::current(),
environment_settings()->context()->message_loop());
// Algorithm for 'ServiceWorkerContainer.getRegistration()':
- // https://w3c.github.io/ServiceWorker/#navigator-service-worker-getRegistration
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#navigator-service-worker-getRegistration
// 1. Let client be this's service worker client.
web::EnvironmentSettings* client = environment_settings();
@@ -242,36 +242,35 @@
script::HandlePromiseSequenceWrappable
ServiceWorkerContainer::GetRegistrations() {
- // https://w3c.github.io/ServiceWorker/#navigator-service-worker-getRegistrations
- // 1. Let client be this's service worker client.
- // 2. Let promise be a new promise.
- // 3. Run the following steps in parallel:
- // 1. Let registrations be a new list.
- // 2. For each scope → registration of scope to registration map:
- // 1. If the origin of the result of parsing scope is the same as
- // client’s origin, then append registration to registrations.
- // 3. Queue a task on promise’s relevant settings object's responsible
- // event loop, using the DOM manipulation task source, to run the
- // following steps:
- // 1. Let registrationObjects be a new list.
- // 2. For each registration of registrations:
- // 1. Let registrationObj be the result of getting the service worker
- // registration object that represents registration in promise’s
- // relevant settings object.
- // 2. Append registrationObj to registrationObjects.
- // 3. Resolve promise with a new frozen array of registrationObjects in
- // promise’s relevant Realm.
- // 4. Return promise.
- // TODO(b/235531652): Implement getRegistrations().
auto promise = environment_settings()
->context()
->global_environment()
->script_value_factory()
->CreateBasicPromise<script::SequenceWrappable>();
-
+ std::unique_ptr<script::ValuePromiseSequenceWrappable::Reference>
+ promise_reference(
+ new script::ValuePromiseSequenceWrappable::Reference(this, promise));
+ base::MessageLoop::current()->task_runner()->PostTask(
+ FROM_HERE,
+ base::BindOnce(&ServiceWorkerContainer::GetRegistrationsTask,
+ base::Unretained(this), std::move(promise_reference)));
return promise;
}
+void ServiceWorkerContainer::GetRegistrationsTask(
+ std::unique_ptr<script::ValuePromiseSequenceWrappable::Reference>
+ promise_reference) {
+ auto* client = environment_settings();
+ // https://w3c.github.io/ServiceWorker/#navigator-service-worker-getRegistrations
+ worker::ServiceWorkerJobs* jobs =
+ environment_settings()->context()->service_worker_jobs();
+ url::Origin storage_key = environment_settings()->ObtainStorageKey();
+ jobs->message_loop()->task_runner()->PostTask(
+ FROM_HERE, base::BindOnce(&ServiceWorkerJobs::GetRegistrationsSubSteps,
+ base::Unretained(jobs), storage_key, client,
+ std::move(promise_reference)));
+}
+
void ServiceWorkerContainer::StartMessages() {}
} // namespace worker
diff --git a/cobalt/worker/service_worker_container.h b/cobalt/worker/service_worker_container.h
index c28718f..aca45b6 100644
--- a/cobalt/worker/service_worker_container.h
+++ b/cobalt/worker/service_worker_container.h
@@ -34,12 +34,12 @@
// The ServiceWorkerContainer interface represents the interface to register and
// access service workers from a service worker client realm.
-// https://w3c.github.io/ServiceWorker/#serviceworkercontainer-interface
+// https://www.w3.org/TR/2022/CRD-service-workers-20220712/#serviceworkercontainer-interface
class ServiceWorkerContainer : public web::EventTarget {
public:
explicit ServiceWorkerContainer(script::EnvironmentSettings* settings);
- // https://w3c.github.io/ServiceWorker/#navigator-service-worker-controller
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#navigator-service-worker-controller
scoped_refptr<ServiceWorker> controller();
script::HandlePromiseWrappable ready();
@@ -72,6 +72,10 @@
std::unique_ptr<script::ValuePromiseWrappable::Reference>
promise_reference);
+ void GetRegistrationsTask(
+ std::unique_ptr<script::ValuePromiseSequenceWrappable::Reference>
+ promise_reference);
+
scoped_refptr<ServiceWorker> ready_;
script::HandlePromiseWrappable ready_promise_;
diff --git a/cobalt/worker/service_worker_global_scope.cc b/cobalt/worker/service_worker_global_scope.cc
index cf7f3b6..fe6552f 100644
--- a/cobalt/worker/service_worker_global_scope.cc
+++ b/cobalt/worker/service_worker_global_scope.cc
@@ -24,7 +24,11 @@
#include "base/trace_event/trace_event.h"
#include "cobalt/script/environment_settings.h"
#include "cobalt/script/exception_state.h"
+#include "cobalt/script/v8c/entry_scope.h"
+#include "cobalt/web/environment_settings_helper.h"
#include "cobalt/worker/clients.h"
+#include "cobalt/worker/fetch_event.h"
+#include "cobalt/worker/fetch_event_init.h"
#include "cobalt/worker/service_worker_jobs.h"
#include "cobalt/worker/worker_settings.h"
@@ -34,7 +38,13 @@
script::EnvironmentSettings* settings, ServiceWorkerObject* service_worker)
: WorkerGlobalScope(settings),
clients_(new Clients(settings)),
- service_worker_object_(base::AsWeakPtr(service_worker)) {}
+ service_worker_object_(base::AsWeakPtr(service_worker)) {
+ loader::FetchInterceptorCoordinator::GetInstance()->Add(this);
+}
+
+ServiceWorkerGlobalScope::~ServiceWorkerGlobalScope() {
+ loader::FetchInterceptorCoordinator::GetInstance()->Clear();
+}
void ServiceWorkerGlobalScope::Initialize() {}
@@ -42,7 +52,7 @@
const std::vector<std::string>& urls,
script::ExceptionState* exception_state) {
// Algorithm for importScripts():
- // https://w3c.github.io/ServiceWorker/#importscripts
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#importscripts
// When the importScripts(urls) method is called, the user agent must import
// scripts into worker global scope, with the following steps to perform the
@@ -60,26 +70,29 @@
DCHECK(service_worker);
// 3. Let url be request’s url.
DCHECK(url.is_valid());
- std::string* resource = service_worker->LookupScriptResource(url);
+ const ScriptResource* script_resource =
+ service_worker->LookupScriptResource(url);
+ std::string* resource_content =
+ script_resource ? script_resource->content.get() : nullptr;
// 4. If serviceWorker’s state is not "parsed" or "installing":
if (service_worker->state() != kServiceWorkerStateParsed &&
service_worker->state() != kServiceWorkerStateInstalling) {
// 4.1. Return map[url] if it exists and a network error
// otherwise.
- if (!resource) {
+ if (!resource_content) {
web::DOMException::Raise(web::DOMException::kNetworkErr,
exception_state);
}
- return resource;
+ return resource_content;
}
// 5. If map[url] exists:
- if (resource) {
+ if (resource_content) {
// 5.1. Append url to serviceWorker’s set of used scripts.
service_worker->AppendToSetOfUsedScripts(url);
// 5.2. Return map[url].
- return resource;
+ return resource_content;
}
// 6. Let registration be serviceWorker’s containing service worker
@@ -96,7 +109,7 @@
// a separate callback that gets passed to the ScriptLoader for the
// FetcherFactory.
- return resource;
+ return resource_content;
},
service_worker_object_),
base::Bind(
@@ -122,7 +135,7 @@
scoped_refptr<ServiceWorkerRegistration>
ServiceWorkerGlobalScope::registration() const {
// Algorithm for registration():
- // https://w3c.github.io/ServiceWorker/#service-worker-global-scope-registration
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#service-worker-global-scope-registration
// The registration getter steps are to return the result of getting the
// service worker registration object representing this's service worker's
@@ -136,7 +149,7 @@
scoped_refptr<ServiceWorker> ServiceWorkerGlobalScope::service_worker() const {
// Algorithm for service_worker():
- // https://w3c.github.io/ServiceWorker/#service-worker-global-scope-serviceworker
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#service-worker-global-scope-serviceworker
// The serviceWorker getter steps are to return the result of getting the
// service worker object that represents this's service worker in this's
@@ -152,7 +165,7 @@
DCHECK_EQ(base::MessageLoop::current(),
environment_settings()->context()->message_loop());
// Algorithm for skipWaiting():
- // https://w3c.github.io/ServiceWorker/#dom-serviceworkerglobalscope-skipwaiting
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dom-serviceworkerglobalscope-skipwaiting
// 1. Let promise be a new promise.
auto promise = environment_settings()
->context()
@@ -170,10 +183,61 @@
base::BindOnce(&ServiceWorkerJobs::SkipWaitingSubSteps,
base::Unretained(jobs),
base::Unretained(environment_settings()->context()),
- service_worker_object_, std::move(promise_reference)));
+ base::Unretained(service_worker_object_.get()),
+ std::move(promise_reference)));
// 3. Return promise.
return promise;
}
+void ServiceWorkerGlobalScope::StartFetch(
+ const GURL& url,
+ std::unique_ptr<base::OnceCallback<void(std::unique_ptr<std::string>)>>
+ callback,
+ std::unique_ptr<base::OnceCallback<void(const net::LoadTimingInfo&)>>
+ report_load_timing_info,
+ std::unique_ptr<base::OnceClosure> fallback) {
+ if (base::MessageLoop::current() !=
+ environment_settings()->context()->message_loop()) {
+ environment_settings()->context()->message_loop()->task_runner()->PostTask(
+ FROM_HERE,
+ base::BindOnce(&ServiceWorkerGlobalScope::StartFetch,
+ base::Unretained(this), url, std::move(callback),
+ std::move(report_load_timing_info),
+ std::move(fallback)));
+ return;
+ }
+ if (!service_worker()) {
+ std::move(*fallback).Run();
+ return;
+ }
+ // TODO: handle the following steps in
+ // https://w3c.github.io/ServiceWorker/#handle-fetch.
+ // 22. If activeWorker’s state is "activating", wait for activeWorker’s state
+ // to become "activated".
+ // 23. If the result of running the Run Service Worker algorithm with
+ // activeWorker is failure, then set handleFetchFailed to true.
+
+ auto* global_environment = get_global_environment(environment_settings());
+ auto* isolate = global_environment->isolate();
+ script::v8c::EntryScope entry_scope(isolate);
+ auto request =
+ web::cache_utils::CreateRequest(environment_settings(), url.spec());
+ if (!request) {
+ std::move(*fallback).Run();
+ return;
+ }
+ FetchEventInit event_init;
+ event_init.set_request(request.value().GetScriptValue());
+ scoped_refptr<FetchEvent> fetch_event =
+ new FetchEvent(environment_settings(), base::Tokens::fetch(), event_init,
+ std::move(callback), std::move(report_load_timing_info));
+ // 24. Create and dispatch event.
+ DispatchEvent(fetch_event);
+ // TODO: implement steps 25 and 26.
+ if (!fetch_event->respond_with_called()) {
+ std::move(*fallback).Run();
+ }
+}
+
} // namespace worker
} // namespace cobalt
diff --git a/cobalt/worker/service_worker_global_scope.h b/cobalt/worker/service_worker_global_scope.h
index a908d86..f89606c 100644
--- a/cobalt/worker/service_worker_global_scope.h
+++ b/cobalt/worker/service_worker_global_scope.h
@@ -15,6 +15,7 @@
#ifndef COBALT_WORKER_SERVICE_WORKER_GLOBAL_SCOPE_H_
#define COBALT_WORKER_SERVICE_WORKER_GLOBAL_SCOPE_H_
+#include <memory>
#include <string>
#include <vector>
@@ -22,11 +23,13 @@
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "cobalt/base/tokens.h"
+#include "cobalt/loader/fetch_interceptor_coordinator.h"
#include "cobalt/script/environment_settings.h"
#include "cobalt/script/promise.h"
#include "cobalt/script/script_value_factory.h"
#include "cobalt/script/value_handle.h"
#include "cobalt/script/wrappable.h"
+#include "cobalt/web/cache_utils.h"
#include "cobalt/web/event_target.h"
#include "cobalt/web/event_target_listener_info.h"
#include "cobalt/worker/clients.h"
@@ -34,14 +37,16 @@
#include "cobalt/worker/service_worker_object.h"
#include "cobalt/worker/service_worker_registration.h"
#include "cobalt/worker/worker_global_scope.h"
+#include "net/base/load_timing_info.h"
namespace cobalt {
namespace worker {
// Implementation of Service Worker Global Scope.
-// https://w3c.github.io/ServiceWorker/#serviceworkerglobalscope-interface
+// https://www.w3.org/TR/2022/CRD-service-workers-20220712/#serviceworkerglobalscope-interface
-class ServiceWorkerGlobalScope : public WorkerGlobalScope {
+class ServiceWorkerGlobalScope : public WorkerGlobalScope,
+ public loader::FetchInterceptor {
public:
explicit ServiceWorkerGlobalScope(script::EnvironmentSettings* settings,
ServiceWorkerObject* service_worker);
@@ -66,6 +71,14 @@
scoped_refptr<ServiceWorker> service_worker() const;
script::HandlePromiseVoid SkipWaiting();
+ void StartFetch(
+ const GURL& url,
+ std::unique_ptr<base::OnceCallback<void(std::unique_ptr<std::string>)>>
+ callback,
+ std::unique_ptr<base::OnceCallback<void(const net::LoadTimingInfo&)>>
+ report_load_timing_info,
+ std::unique_ptr<base::OnceClosure> fallback) override;
+
const web::EventTargetListenerInfo::EventListenerScriptValue* oninstall() {
return GetAttributeEventListener(base::Tokens::install());
}
@@ -119,7 +132,7 @@
DEFINE_WRAPPABLE_TYPE(ServiceWorkerGlobalScope);
protected:
- ~ServiceWorkerGlobalScope() override {}
+ ~ServiceWorkerGlobalScope() override;
private:
scoped_refptr<Clients> clients_;
diff --git a/cobalt/worker/service_worker_jobs.cc b/cobalt/worker/service_worker_jobs.cc
index 41181a2..6f4e1ad 100644
--- a/cobalt/worker/service_worker_jobs.cc
+++ b/cobalt/worker/service_worker_jobs.cc
@@ -64,6 +64,7 @@
#include "url/gurl.h"
#include "url/origin.h"
+
namespace cobalt {
namespace worker {
@@ -112,17 +113,24 @@
// 8. If origin has been configured as a trustworthy origin, return
// "Potentially Trustworthy".
+ if (origin.host() == "web-platform.test") {
+ return true;
+ }
// 9. Return "Not Trustworthy".
return false;
}
-bool PermitAnyURL(const GURL&, bool) { return true; }
+bool PermitAnyNonRedirectedURL(const GURL&, bool did_redirect) {
+ return !did_redirect;
+}
} // namespace
-ServiceWorkerJobs::ServiceWorkerJobs(network::NetworkModule* network_module,
+ServiceWorkerJobs::ServiceWorkerJobs(web::WebSettings* web_settings,
+ network::NetworkModule* network_module,
+ web::UserAgentPlatformInfo* platform_info,
base::MessageLoop* message_loop)
- : network_module_(network_module), message_loop_(message_loop) {
+ : message_loop_(message_loop) {
DCHECK_EQ(message_loop_, base::MessageLoop::current());
fetcher_factory_.reset(new loader::FetcherFactory(network_module));
DCHECK(fetcher_factory_);
@@ -130,6 +138,11 @@
script_loader_factory_.reset(new loader::ScriptLoaderFactory(
"ServiceWorkerJobs", fetcher_factory_.get()));
DCHECK(script_loader_factory_);
+
+ ServiceWorkerPersistentSettings::Options options(web_settings, network_module,
+ platform_info, this);
+ scope_to_registration_map_.reset(new ServiceWorkerRegistrationMap(options));
+ DCHECK(scope_to_registration_map_);
}
ServiceWorkerJobs::~ServiceWorkerJobs() {
@@ -138,7 +151,14 @@
// Wait for web context registrations to be cleared.
web_context_registrations_cleared_.Wait();
}
- scope_to_registration_map_.HandleUserAgentShutdown(this);
+ scope_to_registration_map_->HandleUserAgentShutdown(this);
+ scope_to_registration_map_->AbortAllActive();
+}
+
+void ServiceWorkerJobs::Stop() {
+ if (!done_event_.IsSignaled()) {
+ done_event_.Signal();
+ }
}
void ServiceWorkerJobs::StartRegister(
@@ -153,9 +173,9 @@
DCHECK_NE(message_loop(), base::MessageLoop::current());
DCHECK_EQ(client->context()->message_loop(), base::MessageLoop::current());
// Algorithm for Start Register:
- // https://w3c.github.io/ServiceWorker/#start-register-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#start-register-algorithm
// 1. If scriptURL is failure, reject promise with a TypeError and abort these
- // steps.
+ // steps.
if (script_url_with_fragment.is_empty()) {
promise_reference->value().Reject(script::kTypeError);
return;
@@ -169,26 +189,42 @@
DCHECK(!script_url.is_empty());
// 3. If scriptURL’s scheme is not one of "http" and "https", reject promise
- // with a TypeError and abort these steps.
+ // with a TypeError and abort these steps.
if (!script_url.SchemeIsHTTPOrHTTPS()) {
promise_reference->value().Reject(script::kTypeError);
return;
}
// 4. If any of the strings in scriptURL’s path contains either ASCII
- // case-insensitive "%2f" or ASCII case-insensitive "%5c", reject promise with
- // a TypeError and abort these steps.
+ // case-insensitive "%2f" or ASCII case-insensitive "%5c", reject promise
+ // with a TypeError and abort these steps.
if (PathContainsEscapedSlash(script_url)) {
promise_reference->value().Reject(script::kTypeError);
return;
}
+ DCHECK(client);
+ web::WindowOrWorkerGlobalScope* window_or_worker_global_scope =
+ client->context()->GetWindowOrWorkerGlobalScope();
+ DCHECK(window_or_worker_global_scope);
+ web::CspDelegate* csp_delegate =
+ window_or_worker_global_scope->csp_delegate();
+ DCHECK(csp_delegate);
+ if (!csp_delegate->CanLoad(web::CspDelegate::kWorker, script_url,
+ /* did_redirect*/ false)) {
+ promise_reference->value().Reject(new web::DOMException(
+ web::DOMException::kSecurityErr,
+ "Failed to register a ServiceWorker: The provided scriptURL ('" +
+ script_url.spec() + "') violates the Content Security Policy."));
+ return;
+ }
+
// 5. If scopeURL is null, set scopeURL to the result of parsing the string
- // "./" with scriptURL.
+ // "./" with scriptURL.
GURL scope_url = maybe_scope_url.value_or(script_url.Resolve("./"));
// 6. If scopeURL is failure, reject promise with a TypeError and abort these
- // steps.
+ // steps.
if (scope_url.is_empty()) {
promise_reference->value().Reject(script::kTypeError);
return;
@@ -200,26 +236,26 @@
DCHECK(!scope_url.is_empty());
// 8. If scopeURL’s scheme is not one of "http" and "https", reject promise
- // with a TypeError and abort these steps.
+ // with a TypeError and abort these steps.
if (!scope_url.SchemeIsHTTPOrHTTPS()) {
promise_reference->value().Reject(script::kTypeError);
return;
}
// 9. If any of the strings in scopeURL’s path contains either ASCII
- // case-insensitive "%2f" or ASCII case-insensitive "%5c", reject promise with
- // a TypeError and abort these steps.
+ // case-insensitive "%2f" or ASCII case-insensitive "%5c", reject promise
+ // with a TypeError and abort these steps.
if (PathContainsEscapedSlash(scope_url)) {
promise_reference->value().Reject(script::kTypeError);
return;
}
// 10. Let storage key be the result of running obtain a storage key given
- // client.
+ // client.
url::Origin storage_key = client->ObtainStorageKey();
// 11. Let job be the result of running Create Job with register, storage key,
- // scopeURL, scriptURL, promise, and client.
+ // scopeURL, scriptURL, promise, and client.
std::unique_ptr<Job> job =
CreateJob(kRegister, storage_key, scope_url, script_url,
JobPromiseType::Create(std::move(promise_reference)), client);
@@ -258,7 +294,7 @@
TRACE_EVENT2("cobalt::worker", "ServiceWorkerJobs::CreateJob()", "type", type,
"script_url", script_url.spec());
// Algorithm for Create Job:
- // https://w3c.github.io/ServiceWorker/#create-job
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#create-job
// 1. Let job be a new job.
// 2. Set job’s job type to jobType.
// 3. Set job’s storage key to storage key.
@@ -281,7 +317,7 @@
DCHECK_EQ(message_loop(), base::MessageLoop::current());
DCHECK(job);
// Algorithm for Schedule Job:
- // https://w3c.github.io/ServiceWorker/#schedule-job
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#schedule-job
// 1. Let jobQueue be null.
// 2. Let jobScope be job’s scope url, serialized.
@@ -336,7 +372,7 @@
bool ServiceWorkerJobs::EquivalentJobs(Job* one, Job* two) {
// Algorithm for Two jobs are equivalent:
- // https://w3c.github.io/ServiceWorker/#dfn-job-equivalent
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-job-equivalent
DCHECK(one);
DCHECK(two);
if (!one || !two) {
@@ -365,7 +401,7 @@
TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::RunJob()");
DCHECK_EQ(message_loop(), base::MessageLoop::current());
// Algorithm for Run Job:
- // https://w3c.github.io/ServiceWorker/#run-job-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#run-job-algorithm
// 1. Assert: jobQueue is not empty.
DCHECK(job_queue && !job_queue->empty());
@@ -383,7 +419,7 @@
TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::RunJobTask()");
DCHECK_EQ(message_loop(), base::MessageLoop::current());
// Task for "Run Job" to run in the service worker thread.
- // https://w3c.github.io/ServiceWorker/#run-job-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#run-job-algorithm
DCHECK(job_queue);
if (!job_queue) return;
DCHECK(!job_queue->empty());
@@ -418,7 +454,7 @@
DCHECK_EQ(message_loop(), base::MessageLoop::current());
DCHECK(job);
// Algorithm for Register:
- // https://w3c.github.io/ServiceWorker/#register-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#register-algorithm
// 1. If the result of running potentially trustworthy origin with the origin
// of job’s script url as the argument is Not Trusted, then:
@@ -468,8 +504,8 @@
// 4. Let registration be the result of running Get Registration given job’s
// storage key and job’s scope url.
scoped_refptr<ServiceWorkerRegistrationObject> registration =
- scope_to_registration_map_.GetRegistration(job->storage_key,
- job->scope_url);
+ scope_to_registration_map_->GetRegistration(job->storage_key,
+ job->scope_url);
// 5. If registration is not null, then:
if (registration) {
@@ -494,7 +530,7 @@
// 6.1 Invoke Set Registration algorithm with job’s storage key, job’s scope
// url, and job’s update via cache mode.
- registration = scope_to_registration_map_.SetRegistration(
+ registration = scope_to_registration_map_->SetRegistration(
job->storage_key, job->scope_url, job->update_via_cache);
}
@@ -507,13 +543,13 @@
DCHECK_EQ(message_loop(), base::MessageLoop::current());
DCHECK(job);
// Algorithm for Update:
- // https://w3c.github.io/ServiceWorker/#update-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#update-algorithm
// 1. Let registration be the result of running Get Registration given job’s
// storage key and job’s scope url.
scoped_refptr<ServiceWorkerRegistrationObject> registration =
- scope_to_registration_map_.GetRegistration(job->storage_key,
- job->scope_url);
+ scope_to_registration_map_->GetRegistration(job->storage_key,
+ job->scope_url);
// 2. If registration is null, then:
if (!registration) {
@@ -573,10 +609,11 @@
// 8.4. If the is top-level flag is unset, then return the result of
// fetching request.
// 8.5. Set request’s redirect mode to "error".
+ csp::SecurityCallback csp_callback = base::Bind(&PermitAnyNonRedirectedURL);
// 8.6. Fetch request, and asynchronously wait to run the remaining steps
// as part of fetch’s process response for the response response.
- // TODO(b/225037465): Implement CSP check.
- csp::SecurityCallback csp_callback = base::Bind(&PermitAnyURL);
+ // Note: The CSP check for the script_url is done in StartRegister, where
+ // the client's CSP list can still be referred to.
loader::Origin origin = loader::Origin(job->script_url.GetOrigin());
job->loader = script_loader_factory_->CreateScriptLoader(
job->script_url, origin, csp_callback,
@@ -586,7 +623,8 @@
base::Unretained(this), state),
base::Bind(&ServiceWorkerJobs::UpdateOnLoadingComplete,
base::Unretained(this), state),
- std::move(headers));
+ std::move(headers),
+ /*skip_fetch_intercept=*/true);
}
namespace {
@@ -643,6 +681,7 @@
"Service-Worker-Allowed", &service_worker_allowed);
// 8.9. Set policyContainer to the result of creating a policy container
// from a fetch response given response.
+ state->script_headers = headers;
// 8.10. If serviceWorkerAllowed is failure, then:
// 8.10.1 Asynchronously complete these steps with a network error.
// 8.11. Let scopeURL be registration’s scope url.
@@ -694,14 +733,19 @@
std::unique_ptr<std::string> content) {
TRACE_EVENT0("cobalt::worker",
"ServiceWorkerJobs::UpdateOnContentProduced()");
+ DCHECK(content);
DCHECK_EQ(message_loop(), base::MessageLoop::current());
// Note: There seems to be missing handling of network errors here.
// 8.17. Let url be request’s url.
// 8.18. Set updatedResourceMap[url] to response.
- auto result = state->updated_resource_map.insert(
- std::make_pair(state->job->script_url, std::move(content)));
+ auto result = state->updated_resource_map.emplace(std::make_pair(
+ state->job->script_url,
+ ScriptResource(std::move(content), state->script_headers)));
// Assert that the insert was successful.
DCHECK(result.second);
+ std::string* updated_script_content =
+ result.second ? result.first->second.content.get() : nullptr;
+ DCHECK(updated_script_content);
// 8.19. If response’s cache state is not "local", set registration’s last
// update check time to the current time.
// 8.20. Set hasUpdatedResources to true if any of the following are true:
@@ -717,9 +761,14 @@
if (state->newest_worker->script_url() != state->job->script_url) {
state->has_updated_resources = true;
} else {
- std::string* script_resource =
+ const ScriptResource* newest_worker_script_resource =
state->newest_worker->LookupScriptResource(state->job->script_url);
- if (script_resource && content && (*script_resource != *content)) {
+ std::string* newest_worker_script_content =
+ newest_worker_script_resource
+ ? newest_worker_script_resource->content.get()
+ : nullptr;
+ if (!newest_worker_script_content || !updated_script_content ||
+ (*newest_worker_script_content != *updated_script_content)) {
state->has_updated_resources = true;
}
}
@@ -744,8 +793,8 @@
state->job,
PromiseErrorData(web::DOMException::kNetworkErr, error.value()));
if (state->newest_worker == nullptr) {
- scope_to_registration_map_.RemoveRegistration(state->job->storage_key,
- state->job->scope_url);
+ scope_to_registration_map_->RemoveRegistration(state->job->storage_key,
+ state->job->scope_url);
}
FinishJob(state->job);
return;
@@ -770,7 +819,7 @@
// steps, with script being the asynchronous completion value.
auto entry = state->updated_resource_map.find(state->job->script_url);
auto* script = entry != state->updated_resource_map.end()
- ? entry->second.get()
+ ? entry->second.content.get()
: nullptr;
// 9. If script is null or Is Async Module with script’s record, script’s
// base URL, and {} it true, then:
@@ -781,8 +830,8 @@
// 9.2. If newestWorker is null, then remove registration
// map[(registration’s storage key, serialized scopeURL)].
if (state->newest_worker == nullptr) {
- scope_to_registration_map_.RemoveRegistration(state->job->storage_key,
- state->job->scope_url);
+ scope_to_registration_map_->RemoveRegistration(state->job->storage_key,
+ state->job->scope_url);
}
// 9.3. Invoke Finish Job with job and abort these steps.
FinishJob(state->job);
@@ -806,8 +855,8 @@
// 11. Let worker be a new service worker.
ServiceWorkerObject::Options options(
- "ServiceWorker", state->job->client->context()->network_module(),
- state->registration);
+ "ServiceWorker", state->job->client->context()->web_settings(),
+ state->job->client->context()->network_module(), state->registration);
options.web_options.platform_info =
state->job->client->context()->platform_info();
options.web_options.service_worker_jobs =
@@ -822,6 +871,7 @@
// 13. Append url to worker’s set of used scripts.
worker->AppendToSetOfUsedScripts(state->job->script_url);
// 14. Set worker’s script resource’s policy container to policyContainer.
+ DCHECK(state->script_headers);
// 15. Let forceBypassCache be true if job’s force bypass cache flag is
// set, and false otherwise.
bool force_bypass_cache = state->job->force_bypass_cache_flag;
@@ -848,8 +898,8 @@
// 17.2. If newestWorker is null, then remove registration
// map[(registration’s storage key, serialized scopeURL)].
if (state->newest_worker == nullptr) {
- scope_to_registration_map_.RemoveRegistration(state->job->storage_key,
- state->job->scope_url);
+ scope_to_registration_map_->RemoveRegistration(state->job->storage_key,
+ state->job->scope_url);
}
// 17.3. Invoke Finish Job with job.
FinishJob(state->job);
@@ -867,7 +917,7 @@
DCHECK_EQ(message_loop(), base::MessageLoop::current());
DCHECK(worker);
// Algorithm for "Run Service Worker"
- // https://w3c.github.io/ServiceWorker/#run-service-worker-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#run-service-worker-algorithm
// 1. Let unsafeCreationTime be the unsafe shared current time.
auto unsafe_creation_time = base::TimeTicks::Now();
@@ -904,7 +954,7 @@
TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::Install()");
DCHECK_EQ(message_loop(), base::MessageLoop::current());
// Algorithm for Install:
- // https://w3c.github.io/ServiceWorker/#installation-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#installation-algorithm
// 1. Let installFailed be false.
// Using a shared pointer because this flag is explicitly defined in the spec
@@ -990,11 +1040,8 @@
} else {
// 11.3.1. Queue a task task on installingWorker’s event loop using the
// DOM manipulation task source to run the following steps:
- // Using a shared pointer to ensure that it still exists when it is
- // signaled from the callback after the timeout.
- std::shared_ptr<base::WaitableEvent> done_event(new base::WaitableEvent(
- base::WaitableEvent::ResetPolicy::MANUAL,
- base::WaitableEvent::InitialState::NOT_SIGNALED));
+ DCHECK(done_event_.IsSignaled());
+ done_event_.Reset();
installing_worker->web_agent()
->context()
->message_loop()
@@ -1003,7 +1050,7 @@
FROM_HERE,
base::Bind(
[](ServiceWorkerObject* installing_worker,
- std::shared_ptr<base::WaitableEvent> done_event,
+ base::WaitableEvent* done_event,
std::shared_ptr<starboard::atomic_bool> install_failed) {
// 11.3.1.1. Let e be the result of creating an event with
// ExtendableEvent.
@@ -1020,7 +1067,7 @@
// true.
// If task is discarded, set installFailed to true.
auto done_callback = base::BindOnce(
- [](std::shared_ptr<base::WaitableEvent> done_event,
+ [](base::WaitableEvent* done_event,
std::shared_ptr<starboard::atomic_bool>
install_failed,
bool was_rejected) {
@@ -1039,7 +1086,7 @@
done_event->Signal();
}
},
- base::Unretained(installing_worker), done_event,
+ base::Unretained(installing_worker), &done_event_,
install_failed));
// 11.3.2. Wait for task to have executed or been discarded.
// This waiting is done inside PostBlockingTask above.
@@ -1048,18 +1095,20 @@
// TODO(b/240164388): Investigate a better approach for combining waiting
// for the ExtendableEvent while also allowing use of algorithms that run
// on the same thread from the event handler.
- while (!done_event->TimedWait(base::TimeDelta::FromMilliseconds(100))) {
+ while (!done_event_.TimedWait(base::TimeDelta::FromMilliseconds(100))) {
base::MessageLoopCurrent::ScopedNestableTaskAllower allow;
base::RunLoop().RunUntilIdle();
}
}
}
// 12. If installFailed is true, then:
- if (install_failed->load()) {
+ if (install_failed->load() || !registration->installing_worker()) {
// 12.1. Run the Update Worker State algorithm passing registration’s
// installing worker and "redundant" as the arguments.
- UpdateWorkerState(registration->installing_worker(),
- kServiceWorkerStateRedundant);
+ if (registration->installing_worker()) {
+ UpdateWorkerState(registration->installing_worker(),
+ kServiceWorkerStateRedundant);
+ }
// 12.2. Run the Update Registration State algorithm passing registration,
// "installing" and null as the arguments.
UpdateRegistrationState(registration, kInstalling, nullptr);
@@ -1067,8 +1116,8 @@
// map[(registration’s storage key, serialized registration’s
// scope url)].
if (newest_worker == nullptr) {
- scope_to_registration_map_.RemoveRegistration(registration->storage_key(),
- registration->scope_url());
+ scope_to_registration_map_->RemoveRegistration(
+ registration->storage_key(), registration->scope_url());
}
// 12.4. Invoke Finish Job with job and abort these steps.
FinishJob(job);
@@ -1107,6 +1156,10 @@
// TODO(b/234788479): Wait for tasks.
// 22. Invoke Try Activate with registration.
TryActivate(registration);
+
+ // Persist registration since the waiting_worker has been updated.
+ scope_to_registration_map_->PersistRegistration(registration->storage_key(),
+ registration->scope_url());
}
bool ServiceWorkerJobs::IsAnyClientUsingRegistration(
@@ -1116,7 +1169,7 @@
// When a service worker client is controlled by a service worker, it is
// said that the service worker client is using the service worker’s
// containing service worker registration.
- // https://w3c.github.io/ServiceWorker/#dfn-control
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-control
if (context->is_controlled_by(registration->active_worker())) {
any_client_is_using = true;
break;
@@ -1130,7 +1183,7 @@
TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::TryActivate()");
DCHECK_EQ(message_loop(), base::MessageLoop::current());
// Algorithm for Try Activate:
- // https://w3c.github.io/ServiceWorker/#try-activate-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#try-activate-algorithm
// 1. If registration’s waiting worker is null, return.
if (!registration) return;
@@ -1169,7 +1222,7 @@
TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::Activate()");
DCHECK_EQ(message_loop(), base::MessageLoop::current());
// Algorithm for Activate:
- // https://w3c.github.io/ServiceWorker/#activation-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#activation-algorithm
// 1. If registration’s waiting worker is null, abort these steps.
if (registration->waiting_worker() == nullptr) return;
@@ -1200,7 +1253,7 @@
url::Origin context_storage_key =
url::Origin::Create(context->environment_settings()->creation_url());
scoped_refptr<ServiceWorkerRegistrationObject> matched_registration =
- scope_to_registration_map_.MatchServiceWorkerRegistration(
+ scope_to_registration_map_->MatchServiceWorkerRegistration(
context_storage_key, registration->scope_url());
if (matched_registration == registration) {
matched_clients.push_back(context);
@@ -1241,7 +1294,7 @@
// When a service worker client is controlled by a service worker, it is
// said that the service worker client is using the service worker’s
// containing service worker registration.
- // https://w3c.github.io/ServiceWorker/#dfn-control
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-control
if (context->is_controlled_by(registration->active_worker())) {
// 9.1. Set client’s active worker to registration’s active worker.
client->context()->set_active_service_worker(
@@ -1253,8 +1306,10 @@
}
// 10. Let activeWorker be registration’s active worker.
ServiceWorkerObject* active_worker = registration->active_worker();
+ bool activated = true;
// 11. If the result of running the Should Skip Event algorithm with
// activeWorker and "activate" is false, then:
+ DCHECK(active_worker);
if (!active_worker->ShouldSkipEvent(base::Tokens::activate())) {
// 11.1. If the result of running the Run Service Worker algorithm with
// activeWorker is not failure, then:
@@ -1266,9 +1321,8 @@
active_worker->worker_global_scope()
->environment_settings()
->context());
- std::shared_ptr<base::WaitableEvent> done_event(new base::WaitableEvent(
- base::WaitableEvent::ResetPolicy::MANUAL,
- base::WaitableEvent::InitialState::NOT_SIGNALED));
+ DCHECK(done_event_.IsSignaled());
+ done_event_.Reset();
active_worker->web_agent()
->context()
->message_loop()
@@ -1277,11 +1331,11 @@
FROM_HERE,
base::Bind(
[](ServiceWorkerObject* active_worker,
- std::shared_ptr<base::WaitableEvent> done_event) {
- auto done_callback = base::BindOnce(
- [](std::shared_ptr<base::WaitableEvent> done_event,
- bool) { done_event->Signal(); },
- done_event);
+ base::WaitableEvent* done_event) {
+ auto done_callback =
+ base::BindOnce([](base::WaitableEvent* done_event,
+ bool) { done_event->Signal(); },
+ done_event);
scoped_refptr<ExtendableEvent> event(new ExtendableEvent(
base::Tokens::activate(), std::move(done_callback)));
// 11.1.1.1. Let e be the result of creating an event with
@@ -1298,7 +1352,7 @@
done_event->Signal();
}
},
- base::Unretained(active_worker), done_event));
+ base::Unretained(active_worker), &done_event_));
// 11.1.2. Wait for task to have executed or been discarded.
// This waiting is done inside PostBlockingTask above.
// 11.1.3. Wait for the step labeled WaitForAsynchronousExtensions to
@@ -1306,7 +1360,7 @@
// TODO(b/240164388): Investigate a better approach for combining waiting
// for the ExtendableEvent while also allowing use of algorithms that run
// on the same thread from the event handler.
- while (!done_event->TimedWait(base::TimeDelta::FromMilliseconds(100))) {
+ while (!done_event_.TimedWait(base::TimeDelta::FromMilliseconds(100))) {
base::MessageLoopCurrent::ScopedNestableTaskAllower allow;
base::RunLoop().RunUntilIdle();
}
@@ -1314,14 +1368,21 @@
}
// 12. Run the Update Worker State algorithm passing registration’s active
// worker and "activated" as the arguments.
- UpdateWorkerState(registration->active_worker(),
- kServiceWorkerStateActivated);
+ if (activated && registration->active_worker()) {
+ UpdateWorkerState(registration->active_worker(),
+ kServiceWorkerStateActivated);
+
+ // Persist registration since the waiting_worker has been updated to nullptr
+ // and the active_worker has been updated to the previous waiting_worker.
+ scope_to_registration_map_->PersistRegistration(registration->storage_key(),
+ registration->scope_url());
+ }
}
void ServiceWorkerJobs::NotifyControllerChange(
web::EnvironmentSettings* client) {
// Algorithm for Notify Controller Change:
- // https://w3c.github.io/ServiceWorker/#notify-controller-change-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#notify-controller-change-algorithm
// 1. Assert: client is not null.
DCHECK(client);
@@ -1344,7 +1405,7 @@
bool ServiceWorkerJobs::ServiceWorkerHasNoPendingEvents(
ServiceWorkerObject* worker) {
// Algorithm for Service Worker Has No Pending Events
- // https://w3c.github.io/ServiceWorker/#service-worker-has-no-pending-events
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#service-worker-has-no-pending-events
// TODO(b/240174245): Implement this using the 'set of extended events'.
NOTIMPLEMENTED();
@@ -1358,7 +1419,7 @@
scoped_refptr<ServiceWorkerRegistrationObject> registration) {
TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::ClearRegistration()");
// Algorithm for Clear Registration:
- // https://w3c.github.io/ServiceWorker/#clear-registration-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#clear-registration-algorithm
// 1. Run the following steps atomically.
DCHECK_EQ(message_loop(), base::MessageLoop::current());
@@ -1388,18 +1449,24 @@
UpdateRegistrationState(registration, kWaiting, nullptr);
}
- // 3. If registration’s active worker is not null, then:
+ // 4. If registration’s active worker is not null, then:
ServiceWorkerObject* active_worker = registration->active_worker();
if (active_worker) {
- // 3.1. Terminate registration’s active worker.
+ // 4.1. Terminate registration’s active worker.
TerminateServiceWorker(active_worker);
- // 3.2. Run the Update Worker State algorithm passing registration’s
+ // 4.2. Run the Update Worker State algorithm passing registration’s
// active worker and "redundant" as the arguments.
UpdateWorkerState(active_worker, kServiceWorkerStateRedundant);
- // 3.3. Run the Update Registration State algorithm passing registration,
+ // 4.3. Run the Update Registration State algorithm passing registration,
// "active" and null as the arguments.
UpdateRegistrationState(registration, kActive, nullptr);
}
+
+ // Persist registration since the waiting_worker and active_worker have
+ // been updated to nullptr. This will remove any persisted registration
+ // if one exists.
+ scope_to_registration_map_->PersistRegistration(registration->storage_key(),
+ registration->scope_url());
}
void ServiceWorkerJobs::TryClearRegistration(
@@ -1407,7 +1474,7 @@
TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::TryClearRegistration()");
DCHECK_EQ(message_loop(), base::MessageLoop::current());
// Algorithm for Try Clear Registration:
- // https://w3c.github.io/ServiceWorker/#try-clear-registration-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#try-clear-registration-algorithm
// 1. Invoke Clear Registration with registration if no service worker client
// is using registration and all of the following conditions are true:
@@ -1445,7 +1512,7 @@
DCHECK_EQ(message_loop(), base::MessageLoop::current());
DCHECK(registration);
// Algorithm for Update Registration State:
- // https://w3c.github.io/ServiceWorker/#update-registration-state-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#update-registration-state-algorithm
// 1. Let registrationObjects be an array containing all the
// ServiceWorkerRegistration objects associated with registration.
@@ -1557,7 +1624,7 @@
return;
}
// Algorithm for Update Worker State:
- // https://w3c.github.io/ServiceWorker/#update-state-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#update-state-algorithm
// 1. Assert: state is not "parsed".
DCHECK_NE(kServiceWorkerStateParsed, state);
// 2. Set worker's state to state.
@@ -1608,7 +1675,7 @@
TRACE_EVENT0("cobalt::worker",
"ServiceWorkerJobs::HandleServiceWorkerClientUnload()");
// Algorithm for Handle Servicer Worker Client Unload:
- // https://w3c.github.io/ServiceWorker/#on-user-agent-shutdown-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#on-client-unload-algorithm
DCHECK(client);
// 1. Run the following steps atomically.
DCHECK_EQ(message_loop(), base::MessageLoop::current());
@@ -1631,7 +1698,7 @@
// 5. If registration is unregistered, invoke Try Clear Registration with
// registration.
- if (scope_to_registration_map_.IsUnregistered(registration)) {
+ if (scope_to_registration_map_->IsUnregistered(registration)) {
TryClearRegistration(registration);
}
@@ -1643,7 +1710,7 @@
TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::TerminateServiceWorker()");
DCHECK_EQ(message_loop(), base::MessageLoop::current());
// Algorithm for Terminate Service Worker:
- // https://w3c.github.io/ServiceWorker/#terminate-service-worker
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#terminate-service-worker
// 1. Run the following steps in parallel with serviceWorker’s main loop:
// This runs in the ServiceWorkerRegistry thread.
DCHECK_EQ(message_loop(), base::MessageLoop::current());
@@ -1653,7 +1720,8 @@
worker->worker_global_scope();
// 1.2. Set serviceWorkerGlobalScope’s closing flag to true.
- service_worker_global_scope->set_closing_flag(true);
+ if (service_worker_global_scope != nullptr)
+ service_worker_global_scope->set_closing_flag(true);
// 1.3. Remove all the items from serviceWorker’s set of extended events.
// TODO(b/240174245): Implement 'set of extended events'.
@@ -1686,8 +1754,7 @@
}
// 1.5. Abort the script currently running in serviceWorker.
- DCHECK(worker->is_running());
- worker->Abort();
+ if (worker->is_running()) worker->Abort();
// 1.6. Set serviceWorker’s start status to null.
worker->set_start_status(nullptr);
@@ -1697,7 +1764,7 @@
TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::Unregister()");
DCHECK_EQ(message_loop(), base::MessageLoop::current());
// Algorithm for Unregister:
- // https://w3c.github.io/ServiceWorker/#unregister-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#unregister-algorithm
// 1. If the origin of job’s scope url is not job’s client's origin, then:
if (!url::Origin::Create(GURL(job->client->GetOrigin().SerializedOrigin()))
.IsSameOriginWith(url::Origin::Create(job->scope_url))) {
@@ -1716,8 +1783,8 @@
// 2. Let registration be the result of running Get Registration given job’s
// storage key and job’s scope url.
scoped_refptr<ServiceWorkerRegistrationObject> registration =
- scope_to_registration_map_.GetRegistration(job->storage_key,
- job->scope_url);
+ scope_to_registration_map_->GetRegistration(job->storage_key,
+ job->scope_url);
// 3. If registration is null, then:
if (!registration) {
@@ -1731,8 +1798,8 @@
// 4. Remove registration map[(registration’s storage key, job’s scope url)].
// Keep the registration until this algorithm finishes.
- scope_to_registration_map_.RemoveRegistration(registration->storage_key(),
- job->scope_url);
+ scope_to_registration_map_->RemoveRegistration(registration->storage_key(),
+ job->scope_url);
// 5. Invoke Resolve Job Promise with job and true.
ResolveJobPromise(job, true);
@@ -1749,7 +1816,7 @@
TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::RejectJobPromise()");
DCHECK_EQ(message_loop(), base::MessageLoop::current());
// Algorithm for Reject Job Promise:
- // https://w3c.github.io/ServiceWorker/#reject-job-promise
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#reject-job-promise
// 1. If job’s client is not null, queue a task, on job’s client's responsible
// event loop using the DOM manipulation task source, to reject job’s job
// promise with a new exception with errorData and a user agent-defined
@@ -1801,7 +1868,7 @@
DCHECK_EQ(message_loop(), base::MessageLoop::current());
DCHECK(job);
// Algorithm for Resolve Job Promise:
- // https://w3c.github.io/ServiceWorker/#resolve-job-promise-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#resolve-job-promise-algorithm
// 1. If job’s client is not null, queue a task, on job’s client's responsible
// event loop using the DOM manipulation task source, to run the following
@@ -1869,7 +1936,7 @@
job->equivalent_jobs.clear();
}
-// https://w3c.github.io/ServiceWorker/#finish-job-algorithm
+// https://www.w3.org/TR/2022/CRD-service-workers-20220712/#finish-job-algorithm
void ServiceWorkerJobs::FinishJob(Job* job) {
TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::FinishJob()");
DCHECK_EQ(message_loop(), base::MessageLoop::current());
@@ -1892,7 +1959,7 @@
web::EnvironmentSettings* client) {
DCHECK_EQ(message_loop(), base::MessageLoop::current());
// Algorithm for Sub steps of ServiceWorkerContainer.ready():
- // https://w3c.github.io/ServiceWorker/#navigator-service-worker-ready
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#navigator-service-worker-ready
// 3.1. Let client by this's service worker client.
// 3.2. Let storage key be the result of running obtain a storage
@@ -1906,8 +1973,8 @@
const GURL& base_url = client->creation_url();
GURL client_url = base_url.Resolve("");
scoped_refptr<ServiceWorkerRegistrationObject> registration =
- scope_to_registration_map_.MatchServiceWorkerRegistration(storage_key,
- client_url);
+ scope_to_registration_map_->MatchServiceWorkerRegistration(storage_key,
+ client_url);
// 3.3. If registration is not null, and registration’s active
// worker is not null, queue a task on readyPromise’s
// relevant settings object's responsible event loop, using
@@ -1937,13 +2004,13 @@
"ServiceWorkerJobs::GetRegistrationSubSteps()");
DCHECK_EQ(message_loop(), base::MessageLoop::current());
// Algorithm for Sub steps of ServiceWorkerContainer.getRegistration():
- // https://w3c.github.io/ServiceWorker/#navigator-service-worker-getRegistration
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#navigator-service-worker-getRegistration
// 8.1. Let registration be the result of running Match Service Worker
// Registration algorithm with clientURL as its argument.
scoped_refptr<ServiceWorkerRegistrationObject> registration =
- scope_to_registration_map_.MatchServiceWorkerRegistration(storage_key,
- client_url);
+ scope_to_registration_map_->MatchServiceWorkerRegistration(storage_key,
+ client_url);
// 8.2. If registration is null, resolve promise with undefined and abort
// these steps.
// 8.3. Resolve promise with the result of getting the service worker
@@ -1965,9 +2032,40 @@
client, std::move(promise_reference), registration));
}
+void ServiceWorkerJobs::GetRegistrationsSubSteps(
+ const url::Origin& storage_key, web::EnvironmentSettings* client,
+ std::unique_ptr<script::ValuePromiseSequenceWrappable::Reference>
+ promise_reference) {
+ DCHECK_EQ(message_loop(), base::MessageLoop::current());
+ std::vector<scoped_refptr<ServiceWorkerRegistrationObject>>
+ registration_objects =
+ scope_to_registration_map_->GetRegistrations(storage_key);
+ client->context()->message_loop()->task_runner()->PostTask(
+ FROM_HERE,
+ base::BindOnce(
+ [](web::EnvironmentSettings* settings,
+ std::unique_ptr<script::ValuePromiseSequenceWrappable::Reference>
+ promise,
+ std::vector<scoped_refptr<ServiceWorkerRegistrationObject>>
+ registration_objects) {
+ TRACE_EVENT0(
+ "cobalt::worker",
+ "ServiceWorkerJobs::GetRegistrationSubSteps() Resolve");
+ script::Sequence<scoped_refptr<script::Wrappable>> registrations;
+ for (auto registration_object : registration_objects) {
+ registrations.push_back(scoped_refptr<script::Wrappable>(
+ settings->context()
+ ->GetServiceWorkerRegistration(registration_object)
+ .get()));
+ }
+ promise->value().Resolve(std::move(registrations));
+ },
+ client, std::move(promise_reference),
+ std::move(registration_objects)));
+}
+
void ServiceWorkerJobs::SkipWaitingSubSteps(
- web::Context* client_context,
- const base::WeakPtr<ServiceWorkerObject>& service_worker,
+ web::Context* client_context, ServiceWorkerObject* service_worker,
std::unique_ptr<script::ValuePromiseVoid::Reference> promise_reference) {
TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::SkipWaitingSubSteps()");
DCHECK_EQ(message_loop(), base::MessageLoop::current());
@@ -1979,7 +2077,7 @@
}
// Algorithm for Sub steps of ServiceWorkerGlobalScope.skipWaiting():
- // https://w3c.github.io/ServiceWorker/#dom-serviceworkerglobalscope-skipwaiting
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dom-serviceworkerglobalscope-skipwaiting
// 2.1. Set service worker's skip waiting flag.
service_worker->set_skip_waiting();
@@ -2001,12 +2099,12 @@
void ServiceWorkerJobs::WaitUntilSubSteps(
ServiceWorkerRegistrationObject* registration) {
TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::WaitUntilSubSteps()");
- DCHECK_EQ(message_loop_, base::MessageLoop::current());
+ DCHECK_EQ(message_loop(), base::MessageLoop::current());
// Sub steps for WaitUntil.
- // https://w3c.github.io/ServiceWorker/#dom-extendableevent-waituntil
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dom-extendableevent-waituntil
// 5.2.2. If registration is unregistered, invoke Try Clear Registration
// with registration.
- if (scope_to_registration_map_.IsUnregistered(registration)) {
+ if (scope_to_registration_map_->IsUnregistered(registration)) {
TryClearRegistration(registration);
}
// 5.2.3. If registration is not null, invoke Try Activate with
@@ -2022,7 +2120,7 @@
std::unique_ptr<script::ValuePromiseWrappable::Reference> promise_reference,
const std::string& id) {
TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::ClientsGetSubSteps()");
- DCHECK_EQ(message_loop_, base::MessageLoop::current());
+ DCHECK_EQ(message_loop(), base::MessageLoop::current());
// Check if the client web context is still active. This may trigger if
// Clients.get() was called and service worker installation fails.
if (!IsWebContextRegistered(promise_context)) {
@@ -2030,7 +2128,7 @@
return;
}
// Parallel sub steps (2) for algorithm for Clients.get(id):
- // https://w3c.github.io/ServiceWorker/#clients-get
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#clients-get
// 2.1. For each service worker client client where the result of running
// obtain a storage key given client equals the associated service
// worker's containing service worker registration's storage key:
@@ -2076,7 +2174,7 @@
TRACE_EVENT0("cobalt::worker",
"ServiceWorkerJobs::ResolveGetClientPromise()");
// Algorithm for Resolve Get Client Promise:
- // https://w3c.github.io/ServiceWorker/#resolve-get-client-promise
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#resolve-get-client-promise
// 1. If client is an environment settings object, then:
// 1.1. If client is not a secure context, queue a task to reject promise with
@@ -2187,7 +2285,7 @@
bool include_uncontrolled, ClientType type) {
TRACE_EVENT0("cobalt::worker",
"ServiceWorkerJobs::ClientsMatchAllSubSteps()");
- DCHECK_EQ(message_loop_, base::MessageLoop::current());
+ DCHECK_EQ(message_loop(), base::MessageLoop::current());
// Check if the client web context is still active. This may trigger if
// Clients.matchAll() was called and service worker installation fails.
if (!IsWebContextRegistered(client_context)) {
@@ -2196,7 +2294,7 @@
}
// Parallel sub steps (2) for algorithm for Clients.matchAll():
- // https://w3c.github.io/ServiceWorker/#clients-matchall
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#clients-matchall
// 2.1. Let targetClients be a new list.
std::list<web::EnvironmentSettings*> target_clients;
@@ -2402,7 +2500,7 @@
ServiceWorkerObject* associated_service_worker,
std::unique_ptr<script::ValuePromiseVoid::Reference> promise_reference) {
TRACE_EVENT0("cobalt::worker", "ServiceWorkerJobs::ClaimSubSteps()");
- DCHECK_EQ(message_loop_, base::MessageLoop::current());
+ DCHECK_EQ(message_loop(), base::MessageLoop::current());
// Check if the client web context is still active. This may trigger if
// Clients.claim() was called and service worker installation fails.
@@ -2412,7 +2510,7 @@
}
// Parallel sub steps (3) for algorithm for Clients.claim():
- // https://w3c.github.io/ServiceWorker/#dom-clients-claim
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dom-clients-claim
std::list<web::EnvironmentSettings*> target_clients;
// 3.1. For each service worker client client where the result of running
@@ -2445,7 +2543,7 @@
const GURL& base_url = client->creation_url();
GURL client_url = base_url.Resolve("");
scoped_refptr<ServiceWorkerRegistrationObject> registration =
- scope_to_registration_map_.MatchServiceWorkerRegistration(
+ scope_to_registration_map_->MatchServiceWorkerRegistration(
client_storage_key, client_url);
// 3.1.5. If registration is not the service worker's containing service
@@ -2487,12 +2585,14 @@
web::EnvironmentSettings* incumbent_settings,
std::unique_ptr<script::DataBuffer> serialize_result) {
// Parallel sub steps (6) for algorithm for ServiceWorker.postMessage():
- // https://w3c.github.io/ServiceWorker/#service-worker-postmessage-options
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#service-worker-postmessage-options
// 3. Let incumbentGlobal be incumbentSettings’s global object.
+ // Note: The 'incumbent' is the sender of the message.
// 6.1 If the result of running the Run Service Worker algorithm with
// serviceWorker is failure, then return.
auto* run_result = RunServiceWorker(service_worker);
if (!run_result) return;
+ if (!serialize_result) return;
// 6.2 Queue a task on the DOM manipulation task source to run the following
// steps:
@@ -2502,64 +2602,78 @@
[](ServiceWorkerObject* service_worker,
web::EnvironmentSettings* incumbent_settings,
std::unique_ptr<script::DataBuffer> serialize_result) {
- web::WindowOrWorkerGlobalScope* incumbent_global =
- incumbent_settings->context()->GetWindowOrWorkerGlobalScope();
+ if (!serialize_result) return;
web::EventTarget* event_target =
service_worker->worker_global_scope();
if (!event_target) return;
- ExtendableMessageEventInit init_dict;
- if (incumbent_global->GetWrappableType() ==
- base::GetTypeId<ServiceWorkerGlobalScope>()) {
- // 6.2.1. Let source be determined by switching on the
- // type of incumbentGlobal:
- // . ServiceWorkerGlobalScope
- // The result of getting the service worker
- // object that represents incumbentGlobal’s
- // service worker in the relevant settings
- // object of serviceWorker’s global object.
- init_dict.set_source(ExtendableMessageEvent::SourceType(
- event_target->environment_settings()
- ->context()
- ->GetServiceWorker(incumbent_global->AsServiceWorker()
- ->service_worker_object())));
- } else if (incumbent_global->GetWrappableType() ==
- base::GetTypeId<dom::Window>()) {
- // . Window
- // a new WindowClient object that represents
- // incumbentGlobal’s relevant settings object.
- init_dict.set_source(
- ExtendableMessageEvent::SourceType(WindowClient::Create(
- WindowData(incumbent_global->environment_settings()))));
- } else {
- // . Otherwise
- // a new Client object that represents
- // incumbentGlobal’s associated worker
- init_dict.set_source(ExtendableMessageEvent::SourceType(
- Client::Create(incumbent_global->environment_settings())));
- }
-
+ web::WindowOrWorkerGlobalScope* incumbent_global =
+ incumbent_settings->context()->GetWindowOrWorkerGlobalScope();
+ DCHECK_EQ(incumbent_settings,
+ incumbent_global->environment_settings());
+ base::TypeId incumbent_type = incumbent_global->GetWrappableType();
+ ServiceWorkerObject* incumbent_worker =
+ incumbent_global->IsServiceWorker()
+ ? incumbent_global->AsServiceWorker()
+ ->service_worker_object()
+ : nullptr;
base::MessageLoop* message_loop =
event_target->environment_settings()->context()->message_loop();
if (!message_loop) {
return;
}
- if (!serialize_result) {
- return;
- }
message_loop->task_runner()->PostTask(
FROM_HERE,
base::BindOnce(
- [](const ExtendableMessageEventInit& init_dict,
+ [](const base::TypeId& incumbent_type,
+ ServiceWorkerObject* incumbent_worker,
+ web::EnvironmentSettings* incumbent_settings,
web::EventTarget* event_target,
std::unique_ptr<script::DataBuffer> serialize_result) {
+ ExtendableMessageEventInit init_dict;
+ if (incumbent_type ==
+ base::GetTypeId<ServiceWorkerGlobalScope>()) {
+ // 6.2.1. Let source be determined by switching on the
+ // type of incumbentGlobal:
+ // . ServiceWorkerGlobalScope
+ // The result of getting the service worker
+ // object that represents incumbentGlobal’s
+ // service worker in the relevant settings
+ // object of serviceWorker’s global object.
+ init_dict.set_source(ExtendableMessageEvent::SourceType(
+ event_target->environment_settings()
+ ->context()
+ ->GetServiceWorker(incumbent_worker)));
+ } else if (incumbent_type ==
+ base::GetTypeId<dom::Window>()) {
+ // . Window
+ // a new WindowClient object that represents
+ // incumbentGlobal’s relevant settings object.
+ init_dict.set_source(ExtendableMessageEvent::SourceType(
+ WindowClient::Create(
+ WindowData(incumbent_settings))));
+ } else {
+ // . Otherwise
+ // a new Client object that represents
+ // incumbentGlobal’s associated worker
+ init_dict.set_source(ExtendableMessageEvent::SourceType(
+ Client::Create(incumbent_settings)));
+ }
+
event_target->DispatchEvent(
new worker::ExtendableMessageEvent(
base::Tokens::message(), init_dict,
std::move(serialize_result)));
},
- init_dict, base::Unretained(event_target),
+ incumbent_type, base::Unretained(incumbent_worker),
+ // Note: These should probably be weak pointers for when
+ // the message sender disappears before the recipient
+ // processes the event, but since base::WeakPtr
+ // dereferencing isn't thread-safe, that can't actually be
+ // used here.
+ base::Unretained(incumbent_settings),
+ base::Unretained(event_target),
std::move(serialize_result)));
},
base::Unretained(service_worker),
diff --git a/cobalt/worker/service_worker_jobs.h b/cobalt/worker/service_worker_jobs.h
index c4fa601..9245421 100644
--- a/cobalt/worker/service_worker_jobs.h
+++ b/cobalt/worker/service_worker_jobs.h
@@ -39,6 +39,7 @@
#include "cobalt/script/script_value_factory.h"
#include "cobalt/web/dom_exception.h"
#include "cobalt/web/environment_settings.h"
+#include "cobalt/web/web_settings.h"
#include "cobalt/worker/client_query_options.h"
#include "cobalt/worker/frame_type.h"
#include "cobalt/worker/service_worker.h"
@@ -56,10 +57,10 @@
namespace worker {
// Algorithms for Service Worker Jobs.
-// https://w3c.github.io/ServiceWorker/#algorithms
+// https://www.w3.org/TR/2022/CRD-service-workers-20220712/#algorithms
class ServiceWorkerJobs {
public:
- // https://w3c.github.io/ServiceWorker/#dfn-job-type
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-job-type
enum JobType { kRegister, kUpdate, kUnregister };
class JobQueue;
@@ -98,7 +99,7 @@
promise_wrappable_reference_;
};
- // https://w3c.github.io/ServiceWorker/#dfn-job
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-job
struct Job {
Job(JobType type, const url::Origin& storage_key, const GURL& scope_url,
const GURL& script_url, web::EnvironmentSettings* client,
@@ -107,6 +108,8 @@
storage_key(storage_key),
scope_url(scope_url),
script_url(script_url),
+ update_via_cache(
+ ServiceWorkerUpdateViaCache::kServiceWorkerUpdateViaCacheImports),
client(client),
promise(std::move(promise)) {}
~Job() {
@@ -139,7 +142,7 @@
std::unique_ptr<loader::Loader> loader;
};
- // https://w3c.github.io/ServiceWorker/#dfn-job-queue
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-job-queue
class JobQueue {
public:
bool empty() {
@@ -176,14 +179,17 @@
std::queue<std::unique_ptr<Job>> jobs_;
};
- ServiceWorkerJobs(network::NetworkModule* network_module,
+ ServiceWorkerJobs(web::WebSettings* web_settings,
+ network::NetworkModule* network_module,
+ web::UserAgentPlatformInfo* platform_info,
base::MessageLoop* message_loop);
~ServiceWorkerJobs();
- base::MessageLoop* message_loop() { return message_loop_; }
- network::NetworkModule* network_module() { return network_module_; }
+ void Stop();
- // https://w3c.github.io/ServiceWorker/#start-register-algorithm
+ base::MessageLoop* message_loop() { return message_loop_; }
+
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#start-register-algorithm
void StartRegister(const base::Optional<GURL>& scope_url,
const GURL& script_url,
std::unique_ptr<script::ValuePromiseWrappable::Reference>
@@ -194,26 +200,30 @@
void MaybeResolveReadyPromiseSubSteps(web::EnvironmentSettings* client);
// Sub steps (8) of ServiceWorkerContainer.getRegistration().
- // https://w3c.github.io/ServiceWorker/#navigator-service-worker-getRegistration
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#navigator-service-worker-getRegistration
void GetRegistrationSubSteps(
const url::Origin& storage_key, const GURL& client_url,
web::EnvironmentSettings* client,
std::unique_ptr<script::ValuePromiseWrappable::Reference>
promise_reference);
+ void GetRegistrationsSubSteps(
+ const url::Origin& storage_key, web::EnvironmentSettings* client,
+ std::unique_ptr<script::ValuePromiseSequenceWrappable::Reference>
+ promise_reference);
+
// Sub steps (2) of ServiceWorkerGlobalScope.skipWaiting().
- // https://w3c.github.io/ServiceWorker/#dom-serviceworkerglobalscope-skipwaiting
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dom-serviceworkerglobalscope-skipwaiting
void SkipWaitingSubSteps(
- web::Context* client_context,
- const base::WeakPtr<ServiceWorkerObject>& service_worker,
+ web::Context* client_context, ServiceWorkerObject* service_worker,
std::unique_ptr<script::ValuePromiseVoid::Reference> promise_reference);
// Sub steps for ExtendableEvent.WaitUntil().
- // https://w3c.github.io/ServiceWorker/#dom-extendableevent-waituntil
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dom-extendableevent-waituntil
void WaitUntilSubSteps(ServiceWorkerRegistrationObject* registration);
// Parallel sub steps (2) for algorithm for Clients.get(id):
- // https://w3c.github.io/ServiceWorker/#clients-get
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#clients-get
void ClientsGetSubSteps(
web::Context* client_context,
ServiceWorkerObject* associated_service_worker,
@@ -222,14 +232,14 @@
const std::string& id);
// Algorithm for Resolve Get Client Promise:
- // https://w3c.github.io/ServiceWorker/#resolve-get-client-promise
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#resolve-get-client-promise
void ResolveGetClientPromise(
web::EnvironmentSettings* client, web::Context* promise_context,
std::unique_ptr<script::ValuePromiseWrappable::Reference>
promise_reference);
// Parallel sub steps (2) for algorithm for Clients.matchAll():
- // https://w3c.github.io/ServiceWorker/#clients-matchall
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#clients-matchall
void ClientsMatchAllSubSteps(
web::Context* client_context,
ServiceWorkerObject* associated_service_worker,
@@ -238,14 +248,14 @@
bool include_uncontrolled, ClientType type);
// Parallel sub steps (3) for algorithm for Clients.claim():
- // https://w3c.github.io/ServiceWorker/#dom-clients-claim
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dom-clients-claim
void ClaimSubSteps(
web::Context* client_context,
ServiceWorkerObject* associated_service_worker,
std::unique_ptr<script::ValuePromiseVoid::Reference> promise_reference);
// Parallel sub steps (6) for algorithm for ServiceWorker.postMessage():
- // https://w3c.github.io/ServiceWorker/#service-worker-postmessage-options
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#service-worker-postmessage-options
void ServiceWorkerPostMessageSubSteps(
ServiceWorkerObject* service_worker,
web::EnvironmentSettings* incumbent_settings,
@@ -259,7 +269,7 @@
web_context_registrations_.find(context);
}
- // https://w3c.github.io/ServiceWorker/#create-job
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#create-job
std::unique_ptr<Job> CreateJob(
JobType type, const url::Origin& storage_key, const GURL& scope_url,
const GURL& script_url,
@@ -281,13 +291,13 @@
std::unique_ptr<JobPromiseType> promise,
web::EnvironmentSettings* client);
- // https://w3c.github.io/ServiceWorker/#schedule-job
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#schedule-job
void ScheduleJob(std::unique_ptr<Job> job);
- // https://w3c.github.io/ServiceWorker/#activation-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#activation-algorithm
void Activate(scoped_refptr<ServiceWorkerRegistrationObject> registration);
- // https://w3c.github.io/ServiceWorker/#clear-registration-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#clear-registration-algorithm
void ClearRegistration(
scoped_refptr<ServiceWorkerRegistrationObject> registration);
@@ -302,6 +312,9 @@
scoped_refptr<ServiceWorkerRegistrationObject> registration;
ServiceWorkerObject* newest_worker;
+ // Headers received with the main service worker script load.
+ scoped_refptr<net::HttpResponseHeaders> script_headers;
+
// map of content or resources for the worker.
ScriptResourceMap updated_resource_map;
@@ -310,7 +323,7 @@
bool has_updated_resources = false;
};
- // https://w3c.github.io/ServiceWorker/#dfn-scope-to-job-queue-map
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-scope-to-job-queue-map
using JobQueueMap = std::map<std::string, std::unique_ptr<JobQueue>>;
// Type to hold the errorData for rejection of promises.
@@ -337,19 +350,19 @@
enum RegistrationState { kInstalling, kWaiting, kActive };
- // https://w3c.github.io/ServiceWorker/#dfn-job-equivalent
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-job-equivalent
bool EquivalentJobs(Job* one, Job* two);
- // https://w3c.github.io/ServiceWorker/#run-job-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#run-job-algorithm
void RunJob(JobQueue* job_queue);
// Task for "Run Job" to run in the service worker thread.
void RunJobTask(JobQueue* job_queue);
- // https://w3c.github.io/ServiceWorker/#register-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#register-algorithm
void Register(Job* job);
- // https://w3c.github.io/ServiceWorker/#update-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#update-algorithm
void Update(Job* job);
void UpdateOnContentProduced(scoped_refptr<UpdateJobState> state,
@@ -366,13 +379,13 @@
bool run_result);
- // https://w3c.github.io/ServiceWorker/#unregister-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#unregister-algorithm
void Unregister(Job* job);
- // https://w3c.github.io/ServiceWorker/#reject-job-promise
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#reject-job-promise
void RejectJobPromise(Job* job, const PromiseErrorData& error_data);
- // https://w3c.github.io/ServiceWorker/#resolve-job-promise-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#resolve-job-promise-algorithm
void ResolveJobPromise(Job* job,
scoped_refptr<ServiceWorkerRegistrationObject> value) {
ResolveJobPromise(job, false, value);
@@ -381,14 +394,14 @@
Job* job, bool value,
scoped_refptr<ServiceWorkerRegistrationObject> registration = nullptr);
- // https://w3c.github.io/ServiceWorker/#finish-job-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#finish-job-algorithm
void FinishJob(Job* job);
- // https://w3c.github.io/ServiceWorker/#get-newest-worker
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#get-newest-worker
ServiceWorker* GetNewestWorker(
scoped_refptr<ServiceWorkerRegistrationObject> registration);
- // https://w3c.github.io/ServiceWorker/#run-service-worker-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#run-service-worker-algorithm
// The return value is a 'Completion or failure'.
// A failure is signaled by returning nullptr. Otherwise, the returned string
// points to the value of the Completion returned by the script runner
@@ -396,34 +409,34 @@
std::string* RunServiceWorker(ServiceWorkerObject* worker,
bool force_bypass_cache = false);
- // https://w3c.github.io/ServiceWorker/#installation-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#installation-algorithm
void Install(Job* job, scoped_refptr<ServiceWorkerObject> worker,
scoped_refptr<ServiceWorkerRegistrationObject> registration);
- // https://w3c.github.io/ServiceWorker/#try-activate-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#try-activate-algorithm
void TryActivate(scoped_refptr<ServiceWorkerRegistrationObject> registration);
- // https://w3c.github.io/ServiceWorker/#service-worker-has-no-pending-events
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#service-worker-has-no-pending-events
bool ServiceWorkerHasNoPendingEvents(ServiceWorkerObject* worker);
- // https://w3c.github.io/ServiceWorker/#update-registration-state-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#update-registration-state-algorithm
void UpdateRegistrationState(
scoped_refptr<ServiceWorkerRegistrationObject> registration,
RegistrationState target, scoped_refptr<ServiceWorkerObject> source);
- // https://w3c.github.io/ServiceWorker/#update-state-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#update-state-algorithm
void UpdateWorkerState(ServiceWorkerObject* worker, ServiceWorkerState state);
- // https://w3c.github.io/ServiceWorker/#on-client-unload-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#on-client-unload-algorithm
void HandleServiceWorkerClientUnload(web::EnvironmentSettings* client);
- // https://w3c.github.io/ServiceWorker/#terminate-service-worker
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#terminate-service-worker
void TerminateServiceWorker(ServiceWorkerObject* worker);
- // https://w3c.github.io/ServiceWorker/#notify-controller-change-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#notify-controller-change-algorithm
void NotifyControllerChange(web::EnvironmentSettings* client);
- // https://w3c.github.io/ServiceWorker/#try-clear-registration-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#try-clear-registration-algorithm
void TryClearRegistration(
scoped_refptr<ServiceWorkerRegistrationObject> registration);
@@ -434,17 +447,20 @@
std::unique_ptr<loader::FetcherFactory> fetcher_factory_;
// LoaderFactory that is used to acquire references to resources from a URL.
std::unique_ptr<loader::ScriptLoaderFactory> script_loader_factory_;
- network::NetworkModule* network_module_;
base::MessageLoop* message_loop_;
JobQueueMap job_queue_map_;
- ServiceWorkerRegistrationMap scope_to_registration_map_;
+ std::unique_ptr<ServiceWorkerRegistrationMap> scope_to_registration_map_;
std::set<web::Context*> web_context_registrations_;
base::WaitableEvent web_context_registrations_cleared_ = {
base::WaitableEvent::ResetPolicy::MANUAL,
base::WaitableEvent::InitialState::NOT_SIGNALED};
+
+ base::WaitableEvent done_event_ = {
+ base::WaitableEvent::ResetPolicy::MANUAL,
+ base::WaitableEvent::InitialState::SIGNALED};
};
} // namespace worker
diff --git a/cobalt/worker/service_worker_object.cc b/cobalt/worker/service_worker_object.cc
index 34c2005..7981e68 100644
--- a/cobalt/worker/service_worker_object.cc
+++ b/cobalt/worker/service_worker_object.cc
@@ -46,9 +46,6 @@
ServiceWorkerObject::~ServiceWorkerObject() {
TRACE_EVENT0("cobalt::worker", "ServiceWorkerObject::~ServiceWorkerObject()");
- // Check that the object isn't destroyed without first calling Abort().
- DCHECK(!web_agent_);
- DCHECK(!web_context_);
Abort();
}
@@ -71,15 +68,19 @@
// storing in the map.
auto entry = script_resource_map_.find(url);
if (entry != script_resource_map_.end()) {
- if (entry->second.get() != resource) {
+ if (entry->second.content.get() != resource) {
// The map has an entry, but it's different than the given one, make a
// copy and replace.
- entry->second.reset(new std::string(*resource));
+ entry->second.content.reset(new std::string(*resource));
}
return;
}
- script_resource_map_[url].reset(new std::string(*resource));
+ auto result = script_resource_map_.emplace(std::make_pair(
+ url,
+ ScriptResource(std::make_unique<std::string>(std::string(*resource)))));
+ // Assert that the insert was successful.
+ DCHECK(result.second);
}
bool ServiceWorkerObject::HasScriptResource() const {
@@ -87,14 +88,15 @@
script_resource_map_.end() != script_resource_map_.find(script_url_);
}
-std::string* ServiceWorkerObject::LookupScriptResource(const GURL& url) const {
+const ScriptResource* ServiceWorkerObject::LookupScriptResource(
+ const GURL& url) const {
auto entry = script_resource_map_.find(url);
- return entry != script_resource_map_.end() ? entry->second.get() : nullptr;
+ return entry != script_resource_map_.end() ? &entry->second : nullptr;
}
void ServiceWorkerObject::PurgeScriptResourceMap() {
// Steps 13-15 of Algorithm for Install:
- // https://w3c.github.io/ServiceWorker/#installation-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#installation-algorithm
// 13. Let map be registration’s installing worker's script resource map.
// 14. Let usedSet be registration’s installing worker's set of used scripts.
// 15. For each url of map:
@@ -117,7 +119,6 @@
#if defined(ENABLE_DEBUGGER)
debug_module_.reset();
#endif // ENABLE_DEBUGGER
-
worker_global_scope_ = nullptr;
}
@@ -134,7 +135,7 @@
bool ServiceWorkerObject::ShouldSkipEvent(base::Token event_name) {
// Algorithm for Should Skip Event:
- // https://w3c.github.io/ServiceWorker/#should-skip-event-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#should-skip-event-algorithm
// TODO(b/229622132): Implementing this algorithm will improve performance.
NOTIMPLEMENTED();
return false;
@@ -143,7 +144,7 @@
void ServiceWorkerObject::Initialize(web::Context* context) {
TRACE_EVENT0("cobalt::worker", "ServiceWorkerObject::Initialize()");
// Algorithm for "Run Service Worker"
- // https://w3c.github.io/ServiceWorker/#run-service-worker-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#run-service-worker-algorithm
// 8.1. Let realmExecutionContext be the result of creating a new JavaScript
// realm given agent and the following customizations:
@@ -211,7 +212,17 @@
// 8.10. If the run CSP initialization for a global object algorithm returns
// "Blocked" when executed upon workerGlobalScope, set startFailed to
// true and abort these steps.
- // TODO(b/225037465): Implement CSP check.
+ const ScriptResource* script_resource = LookupScriptResource(script_url_);
+ DCHECK(script_resource);
+ csp::ResponseHeaders csp_headers(script_resource->headers);
+ DCHECK(service_worker_global_scope);
+ DCHECK(service_worker_global_scope->csp_delegate());
+ if (!service_worker_global_scope->csp_delegate()->OnReceiveHeaders(
+ csp_headers)) {
+ // https://www.w3.org/TR/service-workers/#content-security-policy
+ DLOG(WARNING) << "Warning: No Content Security Header received for the "
+ "service worker.";
+ }
// 8.11. If serviceWorker is an active worker, and there are any tasks queued
// in serviceWorker’s containing service worker registration’s task
// queues, queue them to serviceWorker’s event loop’s task queues in the
@@ -224,11 +235,11 @@
bool mute_errors = false;
bool succeeded = false;
- std::string* content = LookupScriptResource(script_url_);
- DCHECK(content);
+ DCHECK(script_resource->content.get());
base::SourceLocation script_location(script_url().spec(), 1, 1);
std::string retval = web_context_->script_runner()->Execute(
- *content, script_location, mute_errors, &succeeded);
+ *script_resource->content.get(), script_location, mute_errors,
+ &succeeded);
// 8.13.2. If evaluationStatus.[[Value]] is empty, this means the script was
// not evaluated. Set startFailed to true and abort these steps.
// We don't actually have access to an 'evaluationStatus' from ScriptRunner,
diff --git a/cobalt/worker/service_worker_object.h b/cobalt/worker/service_worker_object.h
index c9b0c1a..dfa4117 100644
--- a/cobalt/worker/service_worker_object.h
+++ b/cobalt/worker/service_worker_object.h
@@ -27,6 +27,7 @@
#include "base/message_loop/message_loop_current.h"
#include "cobalt/web/agent.h"
#include "cobalt/web/context.h"
+#include "cobalt/web/web_settings.h"
#include "cobalt/worker/service_worker_state.h"
#include "cobalt/worker/worker_global_scope.h"
#include "starboard/atomic.h"
@@ -41,14 +42,14 @@
class ServiceWorkerRegistrationObject;
// This class represents the 'service worker'.
-// https://w3c.github.io/ServiceWorker/#dfn-service-worker
+// https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-service-worker
// Not to be confused with the ServiceWorker JavaScript object, this represents
// the service worker in the browser, independent from the JavaScript realm.
// The lifetime of a service worker is tied to the execution lifetime of events
// and not references held by service worker clients to the ServiceWorker
// object. A user agent may terminate service workers at any time it has no
// event to handle, or detects abnormal operation.
-// https://w3c.github.io/ServiceWorker/#service-worker-lifetime
+// https://www.w3.org/TR/2022/CRD-service-workers-20220712/#service-worker-lifetime
class ServiceWorkerObject
: public base::RefCountedThreadSafe<ServiceWorkerObject>,
public base::SupportsWeakPtr<ServiceWorkerObject>,
@@ -57,11 +58,13 @@
// Worker Options needed at thread run time.
struct Options {
Options(
- const std::string& name, network::NetworkModule* network_module,
+ const std::string& name, web::WebSettings* web_settings,
+ network::NetworkModule* network_module,
ServiceWorkerRegistrationObject* containing_service_worker_registration)
: name(name),
containing_service_worker_registration(
containing_service_worker_registration) {
+ web_options.web_settings = web_settings;
web_options.network_module = network_module;
}
@@ -75,18 +78,18 @@
ServiceWorkerObject(const ServiceWorkerObject&) = delete;
ServiceWorkerObject& operator=(const ServiceWorkerObject&) = delete;
- // https://w3c.github.io/ServiceWorker/#dfn-state
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-state
void set_state(ServiceWorkerState state) { state_ = state; }
ServiceWorkerState state() const { return state_; }
- // https://w3c.github.io/ServiceWorker/#dfn-script-url
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-script-url
void set_script_url(const GURL& script_url) { script_url_ = script_url; }
const GURL& script_url() const { return script_url_; }
- // https://w3c.github.io/ServiceWorker/#dfn-containing-service-worker-registration
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-containing-service-worker-registration
ServiceWorkerRegistrationObject* containing_service_worker_registration() {
return options_.containing_service_worker_registration;
}
- // https://w3c.github.io/ServiceWorker/#dfn-skip-waiting-flag
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-skip-waiting-flag
void set_skip_waiting() { skip_waiting_ = true; }
bool skip_waiting() const { return skip_waiting_; }
@@ -94,27 +97,28 @@
void set_classic_scripts_imported() { classic_scripts_imported_ = true; }
bool classic_scripts_imported() { return classic_scripts_imported_; }
- // https://w3c.github.io/ServiceWorker/#dfn-set-of-used-scripts
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-set-of-used-scripts
void AppendToSetOfUsedScripts(const GURL& url) {
set_of_used_scripts_.insert(url);
}
+ std::set<GURL> set_of_used_scripts() { return set_of_used_scripts_; }
- // https://w3c.github.io/ServiceWorker/#dfn-script-resource-map
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-script-resource-map
void set_script_resource_map(ScriptResourceMap&& resource_map) {
script_resource_map_ = std::move(resource_map);
}
void SetScriptResource(const GURL& url, std::string* resource);
bool HasScriptResource() const;
- std::string* LookupScriptResource(const GURL& url) const;
+ const ScriptResource* LookupScriptResource(const GURL& url) const;
// Steps 13-15 of Algorithm for Install.
- // https://w3c.github.io/ServiceWorker/#installation-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#installation-algorithm
void PurgeScriptResourceMap();
const ScriptResourceMap& script_resource_map() {
return script_resource_map_;
}
- // https://w3c.github.io/ServiceWorker/#service-worker-start-status
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#service-worker-start-status
void set_start_status(std::string* start_status) {
start_status_.reset(start_status);
}
@@ -137,13 +141,15 @@
void ObtainWebAgentAndWaitUntilDone();
// Algorithm for Should Skip Event:
- // https://w3c.github.io/ServiceWorker/#should-skip-event-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#should-skip-event-algorithm
bool ShouldSkipEvent(base::Token event_name);
+ std::string options_name() { return options_.name; }
+
private:
// Called by ObtainWebAgentAndWaitUntilDone to perform initialization required
// on the dedicated thread.
- // https://w3c.github.io/ServiceWorker/#run-service-worker-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#run-service-worker-algorithm
void Initialize(web::Context* context);
// The message loop this object is running on.
@@ -163,25 +169,25 @@
Options options_;
- // https://w3c.github.io/ServiceWorker/#dfn-state
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-state
ServiceWorkerState state_;
- // https://w3c.github.io/ServiceWorker/#dfn-script-url
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-script-url
GURL script_url_;
- // https://w3c.github.io/ServiceWorker/#dfn-script-resource-map
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-script-resource-map
ScriptResourceMap script_resource_map_;
- // https://w3c.github.io/ServiceWorker/#dfn-set-of-used-scripts
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-set-of-used-scripts
std::set<GURL> set_of_used_scripts_;
- // https://w3c.github.io/ServiceWorker/#dfn-skip-waiting-flag
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-skip-waiting-flag
bool skip_waiting_ = false;
- // https://w3c.github.io/ServiceWorker/#dfn-classic-scripts-imported-flag
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-classic-scripts-imported-flag
bool classic_scripts_imported_ = false;
- // https://w3c.github.io/ServiceWorker/#service-worker-start-status
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#service-worker-start-status
std::unique_ptr<std::string> start_status_;
starboard::atomic_bool start_failed_;
diff --git a/cobalt/worker/service_worker_persistent_settings.cc b/cobalt/worker/service_worker_persistent_settings.cc
new file mode 100644
index 0000000..96be7fa
--- /dev/null
+++ b/cobalt/worker/service_worker_persistent_settings.cc
@@ -0,0 +1,425 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/worker/service_worker_persistent_settings.h"
+
+#include <list>
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/containers/flat_map.h"
+#include "base/logging.h"
+#include "base/memory/ref_counted.h"
+#include "base/synchronization/lock.h"
+#include "base/trace_event/trace_event.h"
+#include "cobalt/persistent_storage/persistent_settings.h"
+#include "cobalt/script/exception_message.h"
+#include "cobalt/script/promise.h"
+#include "cobalt/script/script_value.h"
+#include "cobalt/web/context.h"
+#include "cobalt/web/environment_settings.h"
+#include "cobalt/worker/service_worker_jobs.h"
+#include "cobalt/worker/service_worker_registration_object.h"
+#include "cobalt/worker/service_worker_update_via_cache.h"
+#include "cobalt/worker/worker_global_scope.h"
+#include "net/base/completion_once_callback.h"
+#include "net/disk_cache/cobalt/resource_type.h"
+#include "url/gurl.h"
+#include "url/origin.h"
+
+namespace cobalt {
+namespace worker {
+
+namespace {
+// ServiceWorkerRegistrationMap persistent settings keys.
+const char kSettingsJson[] = "service_worker_settings.json";
+const char kSettingsKeyList[] = "key_list";
+
+// ServiceWorkerRegistrationObject persistent settings keys.
+const char kSettingsStorageKeyKey[] = "storage_key";
+const char kSettingsScopeStringKey[] = "scope_string";
+const char kSettingsScopeUrlKey[] = "scope_url";
+const char kSettingsUpdateViaCacheModeKey[] = "update_via_cache_mode";
+const char kSettingsWaitingWorkerKey[] = "waiting_worker";
+const char kSettingsActiveWorkerKey[] = "active_worker";
+
+// ServicerWorkerObject persistent settings keys.
+const char kSettingsOptionsNameKey[] = "options_name";
+const char kSettingsServiceWorkerStateKey[] = "service_worker_state";
+const char kSettingsScriptUrlKey[] = "script_url";
+const char kSettingsScriptResourceMapScriptUrlsKey[] =
+ "script_resource_map_script_urls";
+const char kSettingsSetOfUsedScriptsKey[] = "set_of_used_scripts";
+const char kSettingsSkipWaitingKey[] = "skip_waiting";
+const char kSettingsClassicScriptsImportedKey[] = "classic_scripts_imported";
+
+bool CheckPersistentValue(
+ std::string key_string, std::string settings_key,
+ base::flat_map<std::string, std::unique_ptr<base::Value>>& dict,
+ base::Value::Type type) {
+ if (!dict.contains(settings_key)) {
+ DLOG(INFO) << "Key: " << key_string << " does not contain " << settings_key;
+ return false;
+ } else if (!(dict[settings_key]->type() == type)) {
+ DLOG(INFO) << "Key: " << key_string << " " << settings_key
+ << " is of type: " << dict[settings_key]->type()
+ << ", but expected type is: " << type;
+ return false;
+ }
+ return true;
+}
+} // namespace
+
+ServiceWorkerPersistentSettings::ServiceWorkerPersistentSettings(
+ const Options& options)
+ : options_(options) {
+ persistent_settings_.reset(
+ new cobalt::persistent_storage::PersistentSettings(kSettingsJson));
+ persistent_settings_->ValidatePersistentSettings();
+ DCHECK(persistent_settings_);
+
+ cache_.reset(cobalt::cache::Cache::GetInstance());
+ DCHECK(cache_);
+}
+
+void ServiceWorkerPersistentSettings::ReadServiceWorkerRegistrationMapSettings(
+ std::map<RegistrationMapKey,
+ scoped_refptr<ServiceWorkerRegistrationObject>>&
+ registration_map) {
+ std::vector<base::Value> key_list =
+ persistent_settings_->GetPersistentSettingAsList(kSettingsKeyList);
+ std::set<std::string> unverified_key_set;
+ for (auto& key : key_list) {
+ if (key.is_string()) {
+ unverified_key_set.insert(key.GetString());
+ }
+ }
+ for (auto& key_string : unverified_key_set) {
+ auto dict =
+ persistent_settings_->GetPersistentSettingAsDictionary(key_string);
+ if (dict.empty()) {
+ DLOG(INFO) << "Key: " << key_string << " does not exist in "
+ << kSettingsJson;
+ continue;
+ }
+ if (!CheckPersistentValue(key_string, kSettingsStorageKeyKey, dict,
+ base::Value::Type::STRING))
+ continue;
+ url::Origin storage_key =
+ url::Origin::Create(GURL(dict[kSettingsStorageKeyKey]->GetString()));
+
+ if (!CheckPersistentValue(key_string, kSettingsScopeUrlKey, dict,
+ base::Value::Type::STRING))
+ continue;
+ GURL scope(dict[kSettingsScopeUrlKey]->GetString());
+
+ if (!CheckPersistentValue(key_string, kSettingsUpdateViaCacheModeKey, dict,
+ base::Value::Type::INTEGER))
+ continue;
+ ServiceWorkerUpdateViaCache update_via_cache =
+ static_cast<ServiceWorkerUpdateViaCache>(
+ dict[kSettingsUpdateViaCacheModeKey]->GetInt());
+
+ if (!CheckPersistentValue(key_string, kSettingsScopeStringKey, dict,
+ base::Value::Type::STRING))
+ continue;
+ std::string scope_string(dict[kSettingsScopeStringKey]->GetString());
+
+ RegistrationMapKey key(storage_key, scope_string);
+ scoped_refptr<ServiceWorkerRegistrationObject> registration(
+ new ServiceWorkerRegistrationObject(storage_key, scope,
+ update_via_cache));
+
+ auto worker_key = kSettingsWaitingWorkerKey;
+ if (!CheckPersistentValue(key_string, worker_key, dict,
+ base::Value::Type::DICTIONARY)) {
+ worker_key = kSettingsActiveWorkerKey;
+ if (!CheckPersistentValue(key_string, worker_key, dict,
+ base::Value::Type::DICTIONARY)) {
+ // Neither the waiting_worker or active_worker were correctly read
+ // from persistent settings.
+ continue;
+ }
+ }
+ if (!ReadServiceWorkerObjectSettings(registration, key_string,
+ std::move(dict[worker_key]),
+ worker_key)) {
+ continue;
+ }
+
+ key_set_.insert(key_string);
+ registration_map.insert(std::make_pair(key, registration));
+ }
+}
+
+bool ServiceWorkerPersistentSettings::ReadServiceWorkerObjectSettings(
+ scoped_refptr<ServiceWorkerRegistrationObject> registration,
+ std::string key_string, std::unique_ptr<base::Value> value_dict,
+ std::string worker_key_string) {
+ base::Value* options_name_value = value_dict->FindKeyOfType(
+ kSettingsOptionsNameKey, base::Value::Type::STRING);
+ if (options_name_value == nullptr) return false;
+ ServiceWorkerObject::Options options(options_name_value->GetString(),
+ options_.web_settings,
+ options_.network_module, registration);
+ options.web_options.platform_info = options_.platform_info;
+ options.web_options.service_worker_jobs = options_.service_worker_jobs;
+ scoped_refptr<ServiceWorkerObject> worker(new ServiceWorkerObject(options));
+
+ base::Value* script_url_value = value_dict->FindKeyOfType(
+ kSettingsScriptUrlKey, base::Value::Type::STRING);
+ if (script_url_value == nullptr) return false;
+ worker->set_script_url(GURL(script_url_value->GetString()));
+
+ base::Value* state_value = value_dict->FindKeyOfType(
+ kSettingsServiceWorkerStateKey, base::Value::Type::INTEGER);
+ if (state_value == nullptr) return false;
+ worker->set_state(static_cast<ServiceWorkerState>(state_value->GetInt()));
+
+ base::Value* skip_waiting_value = value_dict->FindKeyOfType(
+ kSettingsSkipWaitingKey, base::Value::Type::BOOLEAN);
+ if (skip_waiting_value == nullptr) return false;
+ if (skip_waiting_value->GetBool()) worker->set_skip_waiting();
+
+ base::Value* classic_scripts_imported_value = value_dict->FindKeyOfType(
+ kSettingsClassicScriptsImportedKey, base::Value::Type::BOOLEAN);
+ if (classic_scripts_imported_value == nullptr) return false;
+ if (classic_scripts_imported_value->GetBool())
+ worker->set_classic_scripts_imported();
+
+ worker->set_start_status(nullptr);
+
+ base::Value* used_scripts_value = value_dict->FindKeyOfType(
+ kSettingsSetOfUsedScriptsKey, base::Value::Type::LIST);
+ if (used_scripts_value == nullptr) return false;
+ std::vector<base::Value> used_scripts_list = used_scripts_value->TakeList();
+ for (int i = 0; i < used_scripts_list.size(); i++) {
+ auto script_value = std::move(used_scripts_list[i]);
+ if (script_value.is_string()) {
+ worker->AppendToSetOfUsedScripts(GURL(script_value.GetString()));
+ }
+ }
+ base::Value* script_urls_value = value_dict->FindKeyOfType(
+ kSettingsScriptResourceMapScriptUrlsKey, base::Value::Type::LIST);
+ if (script_urls_value == nullptr) return false;
+ std::vector<base::Value> script_urls_list = script_urls_value->TakeList();
+ ScriptResourceMap script_resource_map;
+ for (int i = 0; i < script_urls_list.size(); i++) {
+ auto script_url_value = std::move(script_urls_list[i]);
+ if (script_url_value.is_string()) {
+ auto script_url_string = script_url_value.GetString();
+ auto script_url = GURL(script_url_string);
+ std::unique_ptr<std::vector<uint8_t>> data =
+ cache_->Retrieve(disk_cache::ResourceType::kServiceWorkerScript,
+ cache_->CreateKey(key_string + script_url_string));
+ if (data == nullptr) {
+ return false;
+ }
+ std::string script_string(data->begin(), data->end());
+ auto result = script_resource_map.insert(std::make_pair(
+ script_url,
+ ScriptResource(std::make_unique<std::string>(script_string))));
+ DCHECK(result.second);
+ }
+ }
+ if (script_resource_map.size() == 0) {
+ return false;
+ }
+ worker->set_script_resource_map(std::move(script_resource_map));
+
+ if (worker_key_string == kSettingsWaitingWorkerKey) {
+ registration->set_waiting_worker(worker);
+ } else {
+ registration->set_active_worker(worker);
+ }
+ return true;
+}
+
+void ServiceWorkerPersistentSettings::
+ WriteServiceWorkerRegistrationObjectSettings(
+ RegistrationMapKey key,
+ scoped_refptr<ServiceWorkerRegistrationObject> registration) {
+ auto key_string = key.first.GetURL().spec() + key.second;
+ base::flat_map<std::string, std::unique_ptr<base::Value>> dict;
+
+ // https://w3c.github.io/ServiceWorker/#user-agent-shutdown
+ // An installing worker does not persist, but is discarded.
+ auto waiting_worker = registration->waiting_worker();
+ auto active_worker = registration->active_worker();
+
+ if (waiting_worker == nullptr && active_worker == nullptr) {
+ // If the installing worker was the only service worker for the service
+ // worker registration, the service worker registration is discarded.
+ RemoveServiceWorkerRegistrationObjectSettings(key);
+ return;
+ }
+
+ if (waiting_worker) {
+ // A waiting worker promotes to an active worker. This will be handled
+ // upon restart.
+ dict.try_emplace(
+ kSettingsWaitingWorkerKey,
+ WriteServiceWorkerObjectSettings(key_string, waiting_worker));
+ } else {
+ dict.try_emplace(kSettingsActiveWorkerKey, WriteServiceWorkerObjectSettings(
+ key_string, active_worker));
+ }
+
+ // Add key_string to the registered keys and write to persistent settings.
+ key_set_.insert(key_string);
+ std::vector<base::Value> key_list;
+ for (auto& key : key_set_) {
+ key_list.emplace_back(key);
+ }
+ persistent_settings_->SetPersistentSetting(
+ kSettingsKeyList, std::make_unique<base::Value>(std::move(key_list)));
+
+ // Persist ServiceWorkerRegistrationObject's fields.
+ dict.try_emplace(kSettingsStorageKeyKey,
+ std::make_unique<base::Value>(
+ registration->storage_key().GetURL().spec()));
+
+ dict.try_emplace(kSettingsScopeStringKey,
+ std::make_unique<base::Value>(key.second));
+
+ dict.try_emplace(kSettingsScopeUrlKey, std::make_unique<base::Value>(
+ registration->scope_url().spec()));
+
+ dict.try_emplace(
+ kSettingsUpdateViaCacheModeKey,
+ std::make_unique<base::Value>(registration->update_via_cache_mode()));
+
+ persistent_settings_->SetPersistentSetting(
+ key_string, std::make_unique<base::Value>(dict));
+}
+
+std::unique_ptr<base::Value>
+ServiceWorkerPersistentSettings::WriteServiceWorkerObjectSettings(
+ std::string registration_key_string,
+ const scoped_refptr<ServiceWorkerObject>& service_worker_object) {
+ base::flat_map<std::string, std::unique_ptr<base::Value>> dict;
+ DCHECK(service_worker_object);
+ dict.try_emplace(
+ kSettingsOptionsNameKey,
+ std::make_unique<base::Value>(service_worker_object->options_name()));
+
+ dict.try_emplace(
+ kSettingsServiceWorkerStateKey,
+ std::make_unique<base::Value>(service_worker_object->state()));
+
+ dict.try_emplace(kSettingsScriptUrlKey,
+ std::make_unique<base::Value>(
+ service_worker_object->script_url().spec()));
+
+ dict.try_emplace(
+ kSettingsSkipWaitingKey,
+ std::make_unique<base::Value>(service_worker_object->skip_waiting()));
+
+ dict.try_emplace(kSettingsClassicScriptsImportedKey,
+ std::make_unique<base::Value>(
+ service_worker_object->classic_scripts_imported()));
+
+ // Persist set_of_used_scripts as a List.
+ base::Value set_of_used_scripts_value(base::Value::Type::LIST);
+ for (auto script_url : service_worker_object->set_of_used_scripts()) {
+ set_of_used_scripts_value.GetList().push_back(
+ base::Value(script_url.spec()));
+ }
+ dict.try_emplace(
+ kSettingsSetOfUsedScriptsKey,
+ std::make_unique<base::Value>(std::move(set_of_used_scripts_value)));
+
+ // Persist the script_resource_map script urls as a List.
+ base::Value script_urls_value(base::Value::Type::LIST);
+ for (auto const& script_resource :
+ service_worker_object->script_resource_map()) {
+ std::string script_url_string = script_resource.first.spec();
+ script_urls_value.GetList().push_back(base::Value(script_url_string));
+ // Use Cache::Store to persist the script resource.
+ std::string resource = *(script_resource.second.content.get());
+ std::vector<uint8_t> data(resource.begin(), resource.end());
+ cache_->Store(
+ disk_cache::ResourceType::kServiceWorkerScript,
+ cache_->CreateKey(registration_key_string + script_url_string), data,
+ /* metadata */ base::nullopt);
+ }
+ dict.try_emplace(kSettingsScriptResourceMapScriptUrlsKey,
+ std::make_unique<base::Value>(std::move(script_urls_value)));
+
+ return std::move(std::make_unique<base::Value>(dict));
+}
+
+void ServiceWorkerPersistentSettings::
+ RemoveServiceWorkerRegistrationObjectSettings(RegistrationMapKey key) {
+ auto key_string = key.first.GetURL().spec() + key.second;
+
+ if (key_set_.find(key_string) == key_set_.end()) {
+ // The key does not exist in PersistentSettings.
+ return;
+ }
+
+ // Remove the worker script_resource_map from the Cache.
+ RemoveServiceWorkerObjectSettings(key_string);
+
+ // Remove registration key string.
+ key_set_.erase(key_string);
+ std::vector<base::Value> key_list;
+ for (auto& key : key_set_) {
+ key_list.emplace_back(key);
+ }
+ persistent_settings_->SetPersistentSetting(
+ kSettingsKeyList, std::make_unique<base::Value>(std::move(key_list)));
+
+ // Remove the registration dictionary.
+ persistent_settings_->RemovePersistentSetting(key_string);
+}
+
+void ServiceWorkerPersistentSettings::RemoveServiceWorkerObjectSettings(
+ std::string key_string) {
+ auto dict =
+ persistent_settings_->GetPersistentSettingAsDictionary(key_string);
+ if (dict.empty()) return;
+ std::vector<std::string> worker_keys{kSettingsWaitingWorkerKey,
+ kSettingsActiveWorkerKey};
+ for (std::string worker_key : worker_keys) {
+ if (!CheckPersistentValue(key_string, worker_key, dict,
+ base::Value::Type::DICTIONARY))
+ continue;
+ auto worker_dict = std::move(dict[worker_key]);
+ base::Value* script_urls_value = worker_dict->FindKeyOfType(
+ kSettingsScriptResourceMapScriptUrlsKey, base::Value::Type::LIST);
+ if (script_urls_value == nullptr) return;
+ std::vector<base::Value> script_urls_list = script_urls_value->TakeList();
+
+ for (int i = 0; i < script_urls_list.size(); i++) {
+ auto script_url_value = std::move(script_urls_list[i]);
+ if (script_url_value.is_string()) {
+ auto script_url_string = script_url_value.GetString();
+ cache_->Delete(disk_cache::ResourceType::kServiceWorkerScript,
+ cache_->CreateKey(key_string + script_url_string));
+ }
+ }
+ }
+}
+
+void ServiceWorkerPersistentSettings::RemoveAll() {
+ for (auto& key : key_set_) {
+ persistent_settings_->RemovePersistentSetting(key);
+ }
+}
+
+} // namespace worker
+} // namespace cobalt
diff --git a/cobalt/worker/service_worker_persistent_settings.h b/cobalt/worker/service_worker_persistent_settings.h
new file mode 100644
index 0000000..b260d76
--- /dev/null
+++ b/cobalt/worker/service_worker_persistent_settings.h
@@ -0,0 +1,103 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_WORKER_SERVICE_WORKER_PERSISTENT_SETTINGS_H_
+#define COBALT_WORKER_SERVICE_WORKER_PERSISTENT_SETTINGS_H_
+
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "base/containers/flat_map.h"
+#include "base/memory/ref_counted.h"
+#include "base/synchronization/lock.h"
+#include "base/threading/thread_checker.h"
+#include "cobalt/cache/cache.h"
+#include "cobalt/network/network_module.h"
+#include "cobalt/persistent_storage/persistent_settings.h"
+#include "cobalt/script/exception_message.h"
+#include "cobalt/script/promise.h"
+#include "cobalt/script/script_value.h"
+#include "cobalt/script/script_value_factory.h"
+#include "cobalt/web/environment_settings.h"
+#include "cobalt/web/web_settings.h"
+#include "cobalt/worker/service_worker_registration_object.h"
+#include "cobalt/worker/service_worker_update_via_cache.h"
+#include "url/gurl.h"
+#include "url/origin.h"
+
+namespace cobalt {
+namespace worker {
+
+class ServiceWorkerPersistentSettings {
+ public:
+ struct Options {
+ Options(web::WebSettings* web_settings,
+ network::NetworkModule* network_module,
+ web::UserAgentPlatformInfo* platform_info,
+ ServiceWorkerJobs* service_worker_jobs)
+ : web_settings(web_settings),
+ network_module(network_module),
+ platform_info(platform_info),
+ service_worker_jobs(service_worker_jobs) {}
+ web::WebSettings* web_settings;
+ network::NetworkModule* network_module;
+ web::UserAgentPlatformInfo* platform_info;
+ ServiceWorkerJobs* service_worker_jobs;
+ };
+
+ explicit ServiceWorkerPersistentSettings(const Options& options);
+
+ void ReadServiceWorkerRegistrationMapSettings(
+ std::map<RegistrationMapKey,
+ scoped_refptr<ServiceWorkerRegistrationObject>>&
+ registration_map);
+
+ bool ReadServiceWorkerObjectSettings(
+ scoped_refptr<ServiceWorkerRegistrationObject> registration,
+ std::string key_string, std::unique_ptr<base::Value> value_dict,
+ std::string worker_type_string);
+
+ void WriteServiceWorkerRegistrationObjectSettings(
+ RegistrationMapKey key,
+ scoped_refptr<ServiceWorkerRegistrationObject> registration);
+
+ std::unique_ptr<base::Value> WriteServiceWorkerObjectSettings(
+ std::string registration_key_string,
+ const scoped_refptr<ServiceWorkerObject>& service_worker_object);
+
+ void RemoveServiceWorkerRegistrationObjectSettings(RegistrationMapKey key);
+
+ void RemoveServiceWorkerObjectSettings(std::string key_string);
+
+ void RemoveAll();
+
+ private:
+ Options options_;
+
+ std::unique_ptr<cobalt::persistent_storage::PersistentSettings>
+ persistent_settings_;
+
+ std::set<std::string> key_set_;
+
+ std::unique_ptr<cobalt::cache::Cache> cache_;
+};
+
+} // namespace worker
+} // namespace cobalt
+
+#endif // COBALT_WORKER_SERVICE_WORKER_PERSISTENT_SETTINGS_H_
diff --git a/cobalt/worker/service_worker_registration.cc b/cobalt/worker/service_worker_registration.cc
index 0728683..f5c4ee6 100644
--- a/cobalt/worker/service_worker_registration.cc
+++ b/cobalt/worker/service_worker_registration.cc
@@ -41,7 +41,7 @@
script::HandlePromiseWrappable ServiceWorkerRegistration::Update() {
TRACE_EVENT0("cobalt::worker", "ServiceWorkerRegistration::Update()");
// Algorithm for update():
- // https://w3c.github.io/ServiceWorker/#service-worker-registration-update
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#service-worker-registration-update
script::HandlePromiseWrappable promise =
environment_settings()
@@ -69,7 +69,7 @@
DCHECK_EQ(base::MessageLoop::current(),
environment_settings()->context()->message_loop());
// Algorithm for update():
- // https://w3c.github.io/ServiceWorker/#service-worker-registration-update
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#service-worker-registration-update
// 1. Let registration be the service worker registration.
// 2. Let newestWorker be the result of running Get Newest Worker algorithm
@@ -127,7 +127,7 @@
script::HandlePromiseBool ServiceWorkerRegistration::Unregister() {
// Algorithm for unregister():
- // https://w3c.github.io/ServiceWorker/#navigator-service-worker-unregister
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#navigator-service-worker-unregister
// 1. Let registration be the service worker registration.
// 2. Let promise be a new promise.
script::HandlePromiseBool promise = environment_settings()
@@ -151,7 +151,7 @@
void ServiceWorkerRegistration::UnregisterTask(
std::unique_ptr<script::ValuePromiseBool::Reference> promise_reference) {
// Algorithm for unregister():
- // https://w3c.github.io/ServiceWorker/#navigator-service-worker-unregister
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#navigator-service-worker-unregister
// 3. Let job be the result of running Create Job with unregister,
// registration’s storage key, registration’s scope url, null, promise, and
// this's relevant settings object.
@@ -171,7 +171,7 @@
}
std::string ServiceWorkerRegistration::scope() const {
- return registration_->scope_url().GetContent();
+ return registration_->scope_url().spec();
}
ServiceWorkerUpdateViaCache ServiceWorkerRegistration::update_via_cache()
diff --git a/cobalt/worker/service_worker_registration.h b/cobalt/worker/service_worker_registration.h
index 58a2e3d..202647c 100644
--- a/cobalt/worker/service_worker_registration.h
+++ b/cobalt/worker/service_worker_registration.h
@@ -37,7 +37,7 @@
// The ServiceWorkerRegistration interface represents a service worker
// registration within a service worker client realm.
-// https://w3c.github.io/ServiceWorker/#serviceworker-interface
+// https://www.w3.org/TR/2022/CRD-service-workers-20220712/#serviceworker-interface
class ServiceWorkerRegistration : public web::EventTarget {
public:
ServiceWorkerRegistration(
diff --git a/cobalt/worker/service_worker_registration_map.cc b/cobalt/worker/service_worker_registration_map.cc
index 5e8231e..d495b0f 100644
--- a/cobalt/worker/service_worker_registration_map.cc
+++ b/cobalt/worker/service_worker_registration_map.cc
@@ -19,6 +19,7 @@
#include <memory>
#include <string>
#include <utility>
+#include <vector>
#include "base/logging.h"
#include "base/memory/ref_counted.h"
@@ -40,6 +41,7 @@
namespace worker {
namespace {
+
// Returns the serialized URL excluding the fragment.
std::string SerializeExcludingFragment(const GURL& url) {
url::Replacements<char> replacements;
@@ -49,8 +51,18 @@
DCHECK(!no_fragment_url.is_empty());
return no_fragment_url.spec();
}
+
} // namespace
+ServiceWorkerRegistrationMap::ServiceWorkerRegistrationMap(
+ const ServiceWorkerPersistentSettings::Options& options) {
+ service_worker_persistent_settings_.reset(
+ new ServiceWorkerPersistentSettings(options));
+ DCHECK(service_worker_persistent_settings_);
+ service_worker_persistent_settings_->ReadServiceWorkerRegistrationMapSettings(
+ registration_map_);
+}
+
scoped_refptr<ServiceWorkerRegistrationObject>
ServiceWorkerRegistrationMap::MatchServiceWorkerRegistration(
const url::Origin& storage_key, const GURL& client_url) {
@@ -59,7 +71,7 @@
"ServiceWorkerRegistrationMap::MatchServiceWorkerRegistration()");
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
// Algorithm for Match Service Worker Registration:
- // https://w3c.github.io/ServiceWorker/#scope-match-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#scope-match-algorithm
GURL matching_scope;
// 1. Run the following steps atomically.
@@ -110,7 +122,6 @@
url::Origin::Create(client_url));
}
}
-
// 9. Return the result of running Get Registration given storage key and
// matchingScope.
return GetRegistration(storage_key, matching_scope);
@@ -123,7 +134,7 @@
"ServiceWorkerRegistrationMap::GetRegistration()");
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
// Algorithm for Get Registration:
- // https://w3c.github.io/ServiceWorker/#get-registration-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#get-registration-algorithm
// 1. Run the following steps atomically.
base::AutoLock lock(mutex_);
@@ -137,7 +148,7 @@
scope_string = SerializeExcludingFragment(scope);
}
- Key registration_key(storage_key, scope_string);
+ RegistrationMapKey registration_key(storage_key, scope_string);
// 4. For each (entry storage key, entry scope) → registration of registration
// map:
for (const auto& entry : registration_map_) {
@@ -152,6 +163,21 @@
return nullptr;
}
+std::vector<scoped_refptr<ServiceWorkerRegistrationObject>>
+ServiceWorkerRegistrationMap::GetRegistrations(const url::Origin& storage_key) {
+ TRACE_EVENT0("cobalt::worker",
+ "ServiceWorkerRegistrationMap::GetRegistrations()");
+ DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+ base::AutoLock lock(mutex_);
+ std::vector<scoped_refptr<ServiceWorkerRegistrationObject>> result;
+ for (const auto& entry : registration_map_) {
+ if (entry.first.first == storage_key) {
+ result.push_back(std::move(entry.second));
+ }
+ }
+ return result;
+}
+
scoped_refptr<ServiceWorkerRegistrationObject>
ServiceWorkerRegistrationMap::SetRegistration(
const url::Origin& storage_key, const GURL& scope,
@@ -160,7 +186,7 @@
"ServiceWorkerRegistrationMap::SetRegistration()");
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
// Algorithm for Set Registration:
- // https://w3c.github.io/ServiceWorker/#set-registration-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#set-registration-algorithm
// 1. Run the following steps atomically.
base::AutoLock lock(mutex_);
@@ -176,7 +202,7 @@
update_via_cache));
// 4. Set registration map[(storage key, scopeString)] to registration.
- Key registration_key(storage_key, scope_string);
+ RegistrationMapKey registration_key(storage_key, scope_string);
registration_map_.insert(std::make_pair(
registration_key,
scoped_refptr<ServiceWorkerRegistrationObject>(registration)));
@@ -189,11 +215,13 @@
const url::Origin& storage_key, const GURL& scope) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
std::string scope_string = SerializeExcludingFragment(scope);
- Key registration_key(storage_key, scope_string);
+ RegistrationMapKey registration_key(storage_key, scope_string);
auto entry = registration_map_.find(registration_key);
DCHECK(entry != registration_map_.end());
if (entry != registration_map_.end()) {
registration_map_.erase(entry);
+ service_worker_persistent_settings_
+ ->RemoveServiceWorkerRegistrationObjectSettings(registration_key);
}
}
@@ -202,10 +230,12 @@
// A service worker registration is said to be unregistered if registration
// map[this service worker registration's (storage key, serialized scope url)]
// is not this service worker registration.
- // https://w3c.github.io/ServiceWorker/#dfn-service-worker-registration-unregistered
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-service-worker-registration-unregistered
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
- Key registration_key(registration->storage_key(),
- registration->scope_url().spec());
+ std::string scope_string =
+ SerializeExcludingFragment(registration->scope_url());
+ RegistrationMapKey registration_key(registration->storage_key(),
+ scope_string);
auto entry = registration_map_.find(registration_key);
if (entry == registration_map_.end()) return true;
@@ -218,7 +248,7 @@
"ServiceWorkerRegistrationMap::HandleUserAgentShutdown()");
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
// Algorithm for Handle User Agent Shutdown:
- // https://w3c.github.io/ServiceWorker/#on-user-agent-shutdown-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#on-user-agent-shutdown-algorithm
// 1. For each (storage key, scope) -> registration of registration map:
for (auto& entry : registration_map_) {
@@ -249,5 +279,31 @@
}
}
+void ServiceWorkerRegistrationMap::AbortAllActive() {
+ for (auto& entry : registration_map_) {
+ const scoped_refptr<ServiceWorkerRegistrationObject>& registration =
+ entry.second;
+ if (registration->active_worker()) {
+ registration->active_worker()->Abort();
+ }
+ }
+}
+
+void ServiceWorkerRegistrationMap::PersistRegistration(
+ const url::Origin& storage_key, const GURL& scope) {
+ DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+ std::string scope_string = SerializeExcludingFragment(scope);
+ RegistrationMapKey registration_key(storage_key, scope_string);
+ auto entry = registration_map_.find(registration_key);
+ if (entry != registration_map_.end()) {
+ service_worker_persistent_settings_
+ ->WriteServiceWorkerRegistrationObjectSettings(registration_key,
+ entry->second);
+ } else {
+ service_worker_persistent_settings_
+ ->RemoveServiceWorkerRegistrationObjectSettings(registration_key);
+ }
+}
+
} // namespace worker
} // namespace cobalt
diff --git a/cobalt/worker/service_worker_registration_map.h b/cobalt/worker/service_worker_registration_map.h
index 5f104b0..850c5c0 100644
--- a/cobalt/worker/service_worker_registration_map.h
+++ b/cobalt/worker/service_worker_registration_map.h
@@ -19,15 +19,18 @@
#include <memory>
#include <string>
#include <utility>
+#include <vector>
#include "base/memory/ref_counted.h"
#include "base/synchronization/lock.h"
#include "base/threading/thread_checker.h"
+#include "cobalt/network/network_module.h"
#include "cobalt/script/exception_message.h"
#include "cobalt/script/promise.h"
#include "cobalt/script/script_value.h"
#include "cobalt/script/script_value_factory.h"
#include "cobalt/web/environment_settings.h"
+#include "cobalt/worker/service_worker_persistent_settings.h"
#include "cobalt/worker/service_worker_registration_object.h"
#include "cobalt/worker/service_worker_update_via_cache.h"
#include "url/gurl.h"
@@ -38,44 +41,58 @@
class ServiceWorkerJobs;
// Algorithms for the service worker scope to registration map.
-// https://w3c.github.io/ServiceWorker/#dfn-scope-to-registration-map
+// https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-scope-to-registration-map
class ServiceWorkerRegistrationMap {
public:
- using Key = std::pair<url::Origin, std::string>;
+ explicit ServiceWorkerRegistrationMap(
+ const ServiceWorkerPersistentSettings::Options& options);
- // https://w3c.github.io/ServiceWorker/#get-registration-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#get-registration-algorithm
scoped_refptr<ServiceWorkerRegistrationObject> GetRegistration(
const url::Origin& storage_key, const GURL& scope);
- // https://w3c.github.io/ServiceWorker/#set-registration-algorithm
+ std::vector<scoped_refptr<ServiceWorkerRegistrationObject>> GetRegistrations(
+ const url::Origin& storage_key);
+
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#set-registration-algorithm
scoped_refptr<ServiceWorkerRegistrationObject> SetRegistration(
const url::Origin& storage_key, const GURL& scope,
const ServiceWorkerUpdateViaCache& update_via_cache);
- // https://w3c.github.io/ServiceWorker/#scope-match-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#scope-match-algorithm
scoped_refptr<ServiceWorkerRegistrationObject> MatchServiceWorkerRegistration(
const url::Origin& storage_key, const GURL& client_url);
void RemoveRegistration(const url::Origin& storage_key, const GURL& scope);
- // https://w3c.github.io/ServiceWorker/#dfn-service-worker-registration-unregistered
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-service-worker-registration-unregistered
bool IsUnregistered(ServiceWorkerRegistrationObject* registration);
- // https://w3c.github.io/ServiceWorker/#on-user-agent-shutdown-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#on-user-agent-shutdown-algorithm
void HandleUserAgentShutdown(ServiceWorkerJobs* jobs);
+ void AbortAllActive();
+
+ // Called from the end of ServiceWorkerJobs Install, Activate, and Clear
+ // Registration since these are the cases in which a service worker
+ // registration's active_worker or waiting_worker are updated.
+ void PersistRegistration(const url::Origin& storage_key, const GURL& scope);
+
private:
// ThreadChecker for use by the methods operating on the registration map.
THREAD_CHECKER(thread_checker_);
// A registration map is an ordered map where the keys are (storage key,
// serialized scope urls) and the values are service worker registrations.
- // https://w3c.github.io/ServiceWorker/#dfn-scope-to-registration-map
- std::map<Key, scoped_refptr<ServiceWorkerRegistrationObject>>
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-scope-to-registration-map
+ std::map<RegistrationMapKey, scoped_refptr<ServiceWorkerRegistrationObject>>
registration_map_;
// This lock is to allow atomic operations on the registration map.
base::Lock mutex_;
+
+ std::unique_ptr<ServiceWorkerPersistentSettings>
+ service_worker_persistent_settings_;
};
} // namespace worker
diff --git a/cobalt/worker/service_worker_registration_object.cc b/cobalt/worker/service_worker_registration_object.cc
index 921eb8f..ab444e9 100644
--- a/cobalt/worker/service_worker_registration_object.cc
+++ b/cobalt/worker/service_worker_registration_object.cc
@@ -32,7 +32,7 @@
ServiceWorkerObject* ServiceWorkerRegistrationObject::GetNewestWorker() {
// Algorithm for Get Newest Worker:
- // https://w3c.github.io/ServiceWorker/#get-newest-worker
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#get-newest-worker
// 1. Run the following steps atomically.
base::AutoLock lock(mutex_);
diff --git a/cobalt/worker/service_worker_registration_object.h b/cobalt/worker/service_worker_registration_object.h
index ef1f83b..0a399e0 100644
--- a/cobalt/worker/service_worker_registration_object.h
+++ b/cobalt/worker/service_worker_registration_object.h
@@ -29,13 +29,13 @@
namespace worker {
// This class represents the 'service worker registration'.
-// https://w3c.github.io/ServiceWorker/#dfn-service-worker-registration
+// https://www.w3.org/TR/2022/CRD-service-workers-20220712/#dfn-service-worker-registration
// Not to be confused with the ServiceWorkerRegistration JavaScript object,
// this represents the registration of the service worker in the browser,
// independent from the JavaScript realm. The lifetime of this object is beyond
// that of the ServiceWorkerRegistration JavaScript object(s) that represent
// this object in their service worker clients.
-// https://w3c.github.io/ServiceWorker/#service-worker-registration-lifetime
+// https://www.w3.org/TR/2022/CRD-service-workers-20220712/#service-worker-registration-lifetime
class ServiceWorkerRegistrationObject
: public base::RefCountedThreadSafe<ServiceWorkerRegistrationObject> {
public:
@@ -73,7 +73,7 @@
return active_worker_;
}
- // https://w3c.github.io/ServiceWorker/#get-newest-worker
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#get-newest-worker
ServiceWorkerObject* GetNewestWorker();
private:
diff --git a/cobalt/worker/testing/test_with_javascript.h b/cobalt/worker/testing/test_with_javascript.h
index 72dda4c..e40483d 100644
--- a/cobalt/worker/testing/test_with_javascript.h
+++ b/cobalt/worker/testing/test_with_javascript.h
@@ -62,7 +62,8 @@
kServiceWorkerUpdateViaCacheNone);
service_worker_object_ =
new ServiceWorkerObject(ServiceWorkerObject::Options(
- "TestServiceWorkerObject", web_context_->network_module(),
+ "TestServiceWorkerObject", web_context_->web_settings(),
+ web_context_->network_module(),
containing_service_worker_registration_));
service_worker_global_scope_ = new ServiceWorkerGlobalScope(
web_context_->environment_settings(), service_worker_object_);
diff --git a/cobalt/worker/window_client.cc b/cobalt/worker/window_client.cc
index a0dc031..aa5e26a 100644
--- a/cobalt/worker/window_client.cc
+++ b/cobalt/worker/window_client.cc
@@ -26,7 +26,7 @@
WindowClient::WindowClient(const WindowData& window_data)
: Client(window_data.client) {
// Algorithm for Create Window Client:
- // https://w3c.github.io/ServiceWorker/#create-window-client
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#create-window-client
// 1. Let windowClient be a new WindowClient object.
// 2. Set windowClient’s service worker client to client.
diff --git a/cobalt/worker/window_client.h b/cobalt/worker/window_client.h
index fe1eacc..a802684 100644
--- a/cobalt/worker/window_client.h
+++ b/cobalt/worker/window_client.h
@@ -35,7 +35,7 @@
class WindowClient : public Client {
public:
- // https://w3c.github.io/ServiceWorker/#create-window-client
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#create-window-client
static scoped_refptr<Client> Create(const WindowData& window_data) {
return new WindowClient(window_data);
}
diff --git a/cobalt/worker/worker.cc b/cobalt/worker/worker.cc
index 870783d..a27476f 100644
--- a/cobalt/worker/worker.cc
+++ b/cobalt/worker/worker.cc
@@ -49,7 +49,7 @@
// 1. Let is shared be true if worker is a SharedWorker object, and false
// otherwise.
is_shared_ = options.is_shared;
- // 2. Let owner be the relevant owner to add given outside settings.
+ // 2. Moved to below.
// 3. Let parent worker global scope be null.
// 4. If owner is a WorkerGlobalScope object (i.e., we are creating a nested
// dedicated worker), then set parent worker global scope to owner.
@@ -97,7 +97,8 @@
// The origin return a unique opaque origin if worker global scope's url's
// scheme is "data", and inherited origin otherwise.
// https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#set-up-a-worker-environment-settings-object
- worker_settings->set_origin(options_.outside_settings->GetOrigin());
+ worker_settings->set_origin(
+ options_.outside_context->environment_settings()->GetOrigin());
web_context_->setup_environment_settings(worker_settings);
// From algorithm for to setup up a worker environment settings object:
// https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#set-up-a-worker-environment-settings-object
@@ -133,9 +134,18 @@
// 10. Set worker global scope's name to the value of options's name member.
dedicated_worker_global_scope->set_name(options_.options.name());
+ // (Moved) 2. Let owner be the relevant owner to add given outside settings.
+ web::WindowOrWorkerGlobalScope* owner =
+ options_.outside_context->GetWindowOrWorkerGlobalScope();
+ if (!owner) {
+ // There is not a running owner.
+ return;
+ }
// 11. Append owner to worker global scope's owner set.
+ dedicated_worker_global_scope->owner_set()->insert(owner);
// 12. If is shared is true, then:
- // 1. Set worker global scope's constructor origin to outside settings's
+ // 1. Set worker global scope's constructor origin to outside
+ // settings's
// origin.
// 2. Set worker global scope's constructor url to url.
// 3. Set worker global scope's type to the value of options's type
@@ -170,8 +180,11 @@
const GURL& url = web_context_->environment_settings()->creation_url();
loader::Origin origin = loader::Origin(url.GetOrigin());
- // Todo: implement csp check (b/225037465)
- csp::SecurityCallback csp_callback = base::Bind(&PermitAnyURL);
+ csp::SecurityCallback csp_callback = base::Bind(
+ &web::CspDelegate::CanLoad,
+ base::Unretained(options_.outside_context->GetWindowOrWorkerGlobalScope()
+ ->csp_delegate()),
+ web::CspDelegate::kWorker);
loader_ = web_context_->script_loader_factory()->CreateScriptLoader(
url, origin, csp_callback,
@@ -205,16 +218,22 @@
// 1. Queue a global task on the DOM manipulation task source given
// worker's relevant global object to fire an event named error at
// worker.
- options_.outside_settings->context()
- ->message_loop()
- ->task_runner()
- ->PostTask(FROM_HERE,
- base::BindOnce(
- [](web::WindowOrWorkerGlobalScope* global_scope) {
- global_scope->DispatchEvent(new web::ErrorEvent());
- },
- base::Unretained(options_.outside_settings->context()
- ->GetWindowOrWorkerGlobalScope())));
+ options_.outside_context->message_loop()->task_runner()->PostTask(
+ FROM_HERE,
+ base::BindOnce(
+ [](web::WindowOrWorkerGlobalScope* global_scope,
+ const base::Optional<std::string>& message,
+ const base::SourceLocation& location) {
+ web::ErrorEventInit error;
+ error.set_message(message.value_or("No content for worker."));
+ error.set_filename(location.file_path);
+ error.set_lineno(location.line_number);
+ error.set_colno(location.column_number);
+ global_scope->DispatchEvent(new web::ErrorEvent(error));
+ },
+ base::Unretained(
+ options_.outside_context->GetWindowOrWorkerGlobalScope()),
+ error, options_.construction_location));
if (error_) {
LOG(WARNING) << "Script loading failed : " << *error;
} else {
@@ -267,18 +286,19 @@
std::string retval = web_context_->script_runner()->Execute(
content, script_location, mute_errors, &succeeded);
if (!succeeded) {
- options_.outside_settings->context()
- ->message_loop()
- ->task_runner()
- ->PostTask(FROM_HERE,
- base::BindOnce(
- [](web::Context* context, const std::string& message) {
- web::ErrorEventInit error;
- error.set_message(message);
- context->GetWindowOrWorkerGlobalScope()->DispatchEvent(
- new web::ErrorEvent(error));
- },
- options_.outside_settings->context(), retval));
+ options_.outside_context->message_loop()->task_runner()->PostTask(
+ FROM_HERE,
+ base::BindOnce(
+ [](web::Context* context, const std::string& message,
+ const std::string& filename) {
+ web::ErrorEventInit error;
+ error.set_message(message);
+ error.set_filename(filename);
+ context->GetWindowOrWorkerGlobalScope()->DispatchEvent(
+ new web::ErrorEvent(error));
+ },
+ options_.outside_context, retval,
+ web_context_->environment_settings()->creation_url().spec()));
}
// 24. Enable outside port's port message queue.
@@ -303,8 +323,7 @@
// Algorithm for 'run a worker'
// https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#run-a-worker
// 29. Clear the worker global scope's map of active timers.
- if (worker_global_scope_) {
- DCHECK(message_loop());
+ if (worker_global_scope_ && message_loop()) {
message_loop()->task_runner()->PostBlockingTask(
FROM_HERE, base::Bind(
[](WorkerGlobalScope* worker_global_scope) {
@@ -314,6 +333,9 @@
}
// 30. Disentangle all the ports in the list of the worker's ports.
// 31. Empty worker global scope's owner set.
+ if (worker_global_scope_) {
+ worker_global_scope_->owner_set()->clear();
+ }
if (web_agent_) {
DCHECK(message_loop());
web_agent_->WaitUntilDone();
diff --git a/cobalt/worker/worker.h b/cobalt/worker/worker.h
index feea239..9d7043c 100644
--- a/cobalt/worker/worker.h
+++ b/cobalt/worker/worker.h
@@ -25,6 +25,7 @@
#include "base/message_loop/message_loop_current.h"
#include "base/synchronization/waitable_event.h"
#include "base/threading/thread.h"
+#include "cobalt/base/source_location.h"
#include "cobalt/csp/content_security_policy.h"
#include "cobalt/loader/script_loader_factory.h"
#include "cobalt/script/environment_settings.h"
@@ -57,13 +58,16 @@
struct Options {
web::Agent::Options web_options;
+ // Holds the source location where the worker was constructed.
+ base::SourceLocation construction_location;
+
// True if worker is a SharedWorker object, and false otherwise.
bool is_shared;
// Parameters from 'Run a worker' step 9.1 in the spec.
// https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#dom-worker
GURL url;
- web::EnvironmentSettings* outside_settings = nullptr;
+ web::Context* outside_context = nullptr;
web::MessagePort* outside_port = nullptr;
WorkerOptions options;
};
diff --git a/cobalt/worker/worker.idl b/cobalt/worker/worker.idl
index e05669f..db7918a 100644
--- a/cobalt/worker/worker.idl
+++ b/cobalt/worker/worker.idl
@@ -15,12 +15,11 @@
// https://html.spec.whatwg.org/multipage/workers.html#dedicated-workers-and-the-worker-interface
// https://html.spec.whatwg.org/dev/workers.html#workers
-[Exposed=Window,
- ImplementedAs=DedicatedWorker,
- ConstructorCallWith=EnvironmentSettings,
- Constructor(USVString scriptURL, optional WorkerOptions options)
-]
-interface Worker : EventTarget {
+[
+ Exposed = Window, ImplementedAs = DedicatedWorker,
+ ConstructorCallWith = EnvironmentSettings, RaisesException = Constructor,
+ Constructor(USVString scriptURL, optional WorkerOptions options)
+] interface Worker : EventTarget {
void terminate();
void postMessage(any message);
diff --git a/cobalt/worker/worker_global_scope.cc b/cobalt/worker/worker_global_scope.cc
index 4c68bbe..d9025b9 100644
--- a/cobalt/worker/worker_global_scope.cc
+++ b/cobalt/worker/worker_global_scope.cc
@@ -120,6 +120,8 @@
// Todo: implement csp check (b/225037465)
csp::SecurityCallback csp_callback = base::Bind(&PermitAnyURL);
+ bool skip_fetch_intercept =
+ context_->GetWindowOrWorkerGlobalScope()->IsServiceWorker();
// If there is a request callback, call it to possibly retrieve previously
// requested content.
*loader = script_loader_factory_->CreateScriptLoader(
@@ -132,7 +134,8 @@
},
content),
base::Bind(&ScriptLoader::LoadingCompleteCallback,
- base::Unretained(this), loader, error));
+ base::Unretained(this), loader, error),
+ skip_fetch_intercept);
}
void LoadingCompleteCallback(std::unique_ptr<loader::Loader>* loader,
@@ -154,7 +157,7 @@
}
}
- const std::unique_ptr<std::string>& GetContents(int index) {
+ std::unique_ptr<std::string>& GetContents(int index) {
return contents_[index];
}
@@ -233,11 +236,14 @@
// Only NetFetchers are expected to call this, since only they have the
// response headers.
loader::NetFetcher* net_fetcher =
- base::polymorphic_downcast<loader::NetFetcher*>(fetcher);
- net::URLFetcher* url_fetcher = net_fetcher->url_fetcher();
- LOG(INFO) << "Failure receiving Content Security Policy headers "
- "for URL: "
- << url_fetcher->GetURL() << ".";
+ fetcher ? base::polymorphic_downcast<loader::NetFetcher*>(fetcher)
+ : nullptr;
+ net::URLFetcher* url_fetcher =
+ net_fetcher ? net_fetcher->url_fetcher() : nullptr;
+ const GURL& url = url_fetcher ? url_fetcher->GetURL()
+ : environment_settings()->creation_url();
+ LOG(INFO) << "Failure receiving Content Security Policy headers for URL: "
+ << url << ".";
// Return true regardless of CSP headers being received to continue loading
// the response.
return true;
@@ -254,7 +260,7 @@
ScriptResourceMap* new_resource_map) {
bool has_updated_resources = false;
// Steps from Algorithm for Update:
- // https://w3c.github.io/ServiceWorker/#update-algorithm
+ // https://www.w3.org/TR/2022/CRD-service-workers-20220712/#update-algorithm
// 8.21.1. For each importUrl -> storedResponse of newestWorker’s script
// resource map:
std::vector<GURL> request_urls;
@@ -280,6 +286,7 @@
web::EnvironmentSettings* settings = environment_settings();
const GURL& base_url = settings->base_url();
loader::Origin origin = loader::Origin(base_url.GetOrigin());
+ // TODO(b/241801523): Apply CSP.
ScriptLoader script_loader(settings->context());
script_loader.Load(origin, request_urls);
@@ -287,11 +294,13 @@
const auto& error = script_loader.GetError(index);
if (error) continue;
const GURL& url = request_urls[index];
- const std::unique_ptr<std::string>& script =
- script_loader.GetContents(index);
// 8.21.1.5. Set updatedResourceMap[importRequest’s url] to
// fetchedResponse.
- (*new_resource_map)[url].reset(new std::string(*script.get()));
+ // Note: The headers of imported scripts aren't used anywhere.
+ auto result = new_resource_map->insert(std::make_pair(
+ url, ScriptResource(std::move(script_loader.GetContents(index)))));
+ // Assert that the insert was successful.
+ DCHECK(result.second);
// 8.21.1.6. Set fetchedResponse to fetchedResponse’s unsafe response.
// 8.21.1.7. If fetchedResponse’s cache state is not
// "local", set registration’s last update check time to the
@@ -301,7 +310,8 @@
// storedResponse’s unsafe response's body, set
// hasUpdatedResources to true.
DCHECK(previous_resource_map.find(url) != previous_resource_map.end());
- if (*script != *(previous_resource_map.find(url)->second)) {
+ if (*result.first->second.content !=
+ *(previous_resource_map.find(url)->second.content)) {
has_updated_resources = true;
}
}
@@ -368,6 +378,7 @@
// object, passing along any custom perform the fetch steps provided.
// If this succeeds, let script be the result. Otherwise, rethrow the
// exception.
+ // TODO(b/241801523): Apply CSP.
ScriptLoader script_loader(settings->context());
script_loader.Load(origin, request_urls);
diff --git a/cobalt/worker/worker_global_scope.h b/cobalt/worker/worker_global_scope.h
index 345220b..e387991 100644
--- a/cobalt/worker/worker_global_scope.h
+++ b/cobalt/worker/worker_global_scope.h
@@ -17,10 +17,13 @@
#include <map>
#include <memory>
+#include <set>
#include <string>
+#include <utility>
#include <vector>
#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_refptr.h"
#include "cobalt/base/tokens.h"
#include "cobalt/script/environment_settings.h"
#include "cobalt/script/exception_state.h"
@@ -34,15 +37,30 @@
#include "cobalt/web/window_timers.h"
#include "cobalt/worker/worker_location.h"
#include "cobalt/worker/worker_navigator.h"
+#include "net/http/http_response_headers.h"
#include "net/url_request/url_request.h"
#include "url/gurl.h"
+#include "url/origin.h"
namespace cobalt {
namespace worker {
// Implementation of the WorkerGlobalScope common interface.
// https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#the-workerglobalscope-common-interface
-using ScriptResourceMap = std::map<GURL, std::unique_ptr<std::string>>;
+struct ScriptResource {
+ explicit ScriptResource(std::unique_ptr<std::string> content)
+ : content(std::move(content)) {}
+ ScriptResource(std::unique_ptr<std::string> content,
+ const scoped_refptr<net::HttpResponseHeaders>& headers)
+ : content(std::move(content)), headers(headers) {}
+ std::unique_ptr<std::string> content;
+ scoped_refptr<net::HttpResponseHeaders> headers;
+};
+
+using ScriptResourceMap = std::map<GURL, ScriptResource>;
+// A registration map is an ordered map where the keys are (storage key,
+// serialized scope urls) and the values are service worker registrations.
+using RegistrationMapKey = std::pair<url::Origin, std::string>;
class WorkerGlobalScope : public web::WindowOrWorkerGlobalScope {
public:
@@ -79,6 +97,8 @@
virtual void ImportScripts(const std::vector<std::string>& urls,
script::ExceptionState* exception_state);
+ std::set<WindowOrWorkerGlobalScope*>* owner_set() { return &owner_set_; }
+
void set_url(const GURL& url) { url_ = url; }
const GURL& Url() const { return url_; }
@@ -134,6 +154,13 @@
ResponseCallback response_callback);
private:
+ // WorkerGlobalScope Infrastructure
+ // https://html.spec.whatwg.org/multipage/workers.html#workerglobalscope
+ // owner_set_ would typically have a union of Document and WorkerGlobalScope
+ // objects in this set, but we use WindowOrWorkerGlobalScope here since we
+ // have easy access from a Window to its associated Document.
+ std::set<WindowOrWorkerGlobalScope*> owner_set_;
+
// WorkerGlobalScope url
// https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#concept-workerglobalscope-url
GURL url_;
diff --git a/cobalt/worker/worker_settings.cc b/cobalt/worker/worker_settings.cc
index df42733..96d52ae 100644
--- a/cobalt/worker/worker_settings.cc
+++ b/cobalt/worker/worker_settings.cc
@@ -27,6 +27,7 @@
namespace cobalt {
namespace worker {
+
WorkerSettings::WorkerSettings() : web::EnvironmentSettings() {}
WorkerSettings::WorkerSettings(web::MessagePort* message_port)
diff --git a/cobalt/xhr/BUILD.gn b/cobalt/xhr/BUILD.gn
index 75a2c80..17ad212 100644
--- a/cobalt/xhr/BUILD.gn
+++ b/cobalt/xhr/BUILD.gn
@@ -12,12 +12,20 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+source_set("xhr_settings") {
+ has_pedantic_warnings = true
+ sources = [ "xhr_settings.h" ]
+ public_deps = [ "//cobalt/base" ]
+}
+
static_library("xhr") {
# Creates cycle with //cobalt/dom through xml_document.h
check_includes = false
has_pedantic_warnings = true
sources = [
+ "fetch_buffer_pool.cc",
+ "fetch_buffer_pool.h",
"url_fetcher_buffer_writer.cc",
"url_fetcher_buffer_writer.h",
"xml_http_request.cc",
@@ -26,6 +34,8 @@
"xml_http_request_event_target.h",
]
+ public_deps = [ ":xhr_settings" ]
+
deps = [
":global_stats",
"//cobalt/base",
@@ -39,12 +49,6 @@
"//third_party/protobuf:protobuf_lite",
"//url",
]
-
- if (enable_xhr_header_filtering && !sb_is_evergreen) {
- sources = [ "xhr_modify_headers.h" ]
- defines = [ "COBALT_ENABLE_XHR_HEADER_FILTERING" ]
- deps += cobalt_platform_dependencies
- }
}
static_library("global_stats") {
@@ -60,13 +64,18 @@
target(gtest_target_type, "xhr_test") {
testonly = true
has_pedantic_warnings = true
- sources = [ "xml_http_request_test.cc" ]
+ sources = [
+ "fetch_buffer_pool_test.cc",
+ "xhr_settings_test.cc",
+ "xml_http_request_test.cc",
+ ]
deps = [
":xhr",
"//cobalt/base",
"//cobalt/browser",
"//cobalt/debug",
"//cobalt/dom/testing:dom_testing",
+ "//cobalt/script",
"//cobalt/test:run_all_unittests",
"//cobalt/web:dom_exception",
"//cobalt/web/testing:web_testing",
diff --git a/cobalt/xhr/fetch_buffer_pool.cc b/cobalt/xhr/fetch_buffer_pool.cc
new file mode 100644
index 0000000..b67a270
--- /dev/null
+++ b/cobalt/xhr/fetch_buffer_pool.cc
@@ -0,0 +1,125 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/xhr/fetch_buffer_pool.h"
+
+#include <algorithm>
+#include <utility>
+
+namespace cobalt {
+namespace xhr {
+
+int FetchBufferPool::GetSize() const {
+ int size = 0;
+ for (const auto& buffer : buffers_) {
+ size += buffer.byte_length();
+ }
+
+ // The last buffer may not be fully populated.
+ if (current_buffer_) {
+ DCHECK_EQ(current_buffer_, &buffers_.back());
+ DCHECK_LE(current_buffer_offset_, current_buffer_->byte_length());
+
+ auto remaining = current_buffer_->byte_length() - current_buffer_offset_;
+ size -= remaining;
+ }
+
+ DCHECK_GE(size, 0);
+ return size;
+}
+
+void FetchBufferPool::Clear() {
+ buffers_.clear();
+ current_buffer_ = nullptr;
+ current_buffer_offset_ = 0;
+}
+
+int FetchBufferPool::Write(const void* data, int num_bytes) {
+ DCHECK(num_bytes >= 0);
+
+ if (num_bytes > 0) {
+ DCHECK(data);
+ }
+
+ auto source_bytes_remaining = num_bytes;
+ while (source_bytes_remaining > 0) {
+ EnsureCurrentBufferAllocated(source_bytes_remaining);
+
+ int destination_bytes_remaining =
+ current_buffer_->byte_length() - current_buffer_offset_;
+ int bytes_to_copy =
+ std::min(destination_bytes_remaining, source_bytes_remaining);
+
+ memcpy(current_buffer_->data() + current_buffer_offset_, data,
+ bytes_to_copy);
+
+ data = static_cast<const uint8_t*>(data) + bytes_to_copy;
+ current_buffer_offset_ += bytes_to_copy;
+ source_bytes_remaining -= bytes_to_copy;
+
+ if (current_buffer_offset_ == current_buffer_->byte_length()) {
+ current_buffer_ = nullptr;
+ current_buffer_offset_ = 0;
+ }
+ }
+
+ return num_bytes;
+}
+
+void FetchBufferPool::ResetAndReturnAsArrayBuffers(
+ bool return_all,
+ std::vector<script::PreallocatedArrayBufferData>* buffers) {
+ DCHECK(buffers);
+
+ buffers->clear();
+
+ if (!return_all && buffers_.size() > 1 && current_buffer_) {
+ // Don't return the last buffer when:
+ // 1. `return_all` is set to false
+ // 2. the last buffer is not full (current_buffer_ is not null)
+ // 3. There are more than one buffer cached so the caller will receive
+ // something.
+ auto saved_last_buffer = std::move(buffers_.back());
+ buffers_.pop_back();
+ buffers->swap(buffers_);
+ buffers_.push_back(std::move(saved_last_buffer));
+ current_buffer_ = &buffers_.back();
+ return;
+ }
+
+ if (current_buffer_) {
+ DCHECK(return_all || buffers_.size() == 1);
+ current_buffer_->Resize(current_buffer_offset_);
+ }
+
+ buffers->swap(buffers_);
+
+ Clear();
+}
+
+void FetchBufferPool::EnsureCurrentBufferAllocated(int expected_buffer_size) {
+ if (current_buffer_ != nullptr) {
+ DCHECK_EQ(current_buffer_, &buffers_.back());
+ DCHECK_LE(current_buffer_offset_, current_buffer_->byte_length());
+ return;
+ }
+
+ buffers_.emplace_back(std::max(default_buffer_size_, expected_buffer_size));
+
+ current_buffer_ = &buffers_.back();
+ current_buffer_offset_ = 0;
+}
+
+} // namespace xhr
+} // namespace cobalt
diff --git a/cobalt/xhr/fetch_buffer_pool.h b/cobalt/xhr/fetch_buffer_pool.h
new file mode 100644
index 0000000..13158a3
--- /dev/null
+++ b/cobalt/xhr/fetch_buffer_pool.h
@@ -0,0 +1,80 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_XHR_FETCH_BUFFER_POOL_H_
+#define COBALT_XHR_FETCH_BUFFER_POOL_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "base/logging.h"
+#include "base/optional.h"
+#include "cobalt/script/array_buffer.h"
+
+namespace cobalt {
+namespace xhr {
+
+// Manages buffer used by the Fetch api.
+// The Fetch api is implemented as a polyfill on top of XMLHttpRequest, where
+// the fetched data is sent to the web app in chunks represented as Uint8Array
+// objects. This class allows to minimizing copying by appending the incoming
+// network data to PreallocatedArrayBufferData.
+class FetchBufferPool {
+ public:
+ static constexpr int kDefaultBufferSize = 1024 * 1024;
+
+ explicit FetchBufferPool(
+ const base::Optional<int>& default_buffer_size = base::Optional<int>())
+ // Use "kDefaultBufferSize + 0" to force using kDefaultBufferSize as
+ // r-value to avoid link error.
+ : default_buffer_size_(
+ default_buffer_size.value_or(kDefaultBufferSize + 0)) {
+ DCHECK_GT(default_buffer_size_, 0);
+ }
+ ~FetchBufferPool() = default;
+
+ int GetSize() const;
+ void Clear();
+
+ int Write(const void* data, int num_bytes);
+
+ // When `return_all` is set to true, the function should return all data
+ // buffered, otherwise the function may decide not to return some buffers so
+ // it can return larger buffers in future.
+ // The user of this class should call the function with `return_all` set to
+ // true at least for the last call to this function during a request.
+ void ResetAndReturnAsArrayBuffers(
+ bool return_all,
+ std::vector<script::PreallocatedArrayBufferData>* buffers);
+
+ private:
+ FetchBufferPool(const FetchBufferPool&) = delete;
+ FetchBufferPool& operator=(const FetchBufferPool&) = delete;
+
+ void EnsureCurrentBufferAllocated(int expected_buffer_size);
+
+ const int default_buffer_size_ = 0;
+ std::vector<script::PreallocatedArrayBufferData> buffers_;
+
+ // Either nullptr or points to the last element of |buffers_|, using a member
+ // variable explicitly to keep it in sync with |current_buffer_offset_|.
+ script::PreallocatedArrayBufferData* current_buffer_ = nullptr;
+ size_t current_buffer_offset_ = 0;
+};
+
+} // namespace xhr
+} // namespace cobalt
+
+#endif // COBALT_XHR_FETCH_BUFFER_POOL_H_
diff --git a/cobalt/xhr/fetch_buffer_pool_test.cc b/cobalt/xhr/fetch_buffer_pool_test.cc
new file mode 100644
index 0000000..9a7b12e
--- /dev/null
+++ b/cobalt/xhr/fetch_buffer_pool_test.cc
@@ -0,0 +1,230 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/xhr/fetch_buffer_pool.h"
+
+#include <vector>
+
+#include "cobalt/script/array_buffer.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace cobalt {
+namespace xhr {
+namespace {
+
+void FillTestData(std::vector<uint8_t>* buffer) {
+ for (size_t i = 0; i < buffer->size(); ++i) {
+ buffer->at(i) = static_cast<char>(i);
+ }
+}
+
+void AppendBuffersTo(
+ const std::vector<script::PreallocatedArrayBufferData>& sources,
+ std::vector<uint8_t>* destination) {
+ ASSERT_NE(destination, nullptr);
+
+ for (const auto& buffer : sources) {
+ destination->insert(destination->end(), buffer.data(),
+ buffer.data() + buffer.byte_length());
+ }
+}
+
+TEST(FetchBufferPoolTest, Empty) {
+ FetchBufferPool empty_buffer_pool;
+ EXPECT_EQ(empty_buffer_pool.GetSize(), 0);
+
+ std::vector<script::PreallocatedArrayBufferData> buffers;
+
+ empty_buffer_pool.ResetAndReturnAsArrayBuffers(false, &buffers);
+ EXPECT_TRUE(buffers.empty());
+
+ empty_buffer_pool.ResetAndReturnAsArrayBuffers(true, &buffers);
+ EXPECT_TRUE(buffers.empty());
+
+ empty_buffer_pool.Write(nullptr, 0); // still empty
+ EXPECT_EQ(empty_buffer_pool.GetSize(), 0);
+
+ empty_buffer_pool.ResetAndReturnAsArrayBuffers(false, &buffers);
+ EXPECT_TRUE(buffers.empty());
+
+ empty_buffer_pool.ResetAndReturnAsArrayBuffers(true, &buffers);
+ EXPECT_TRUE(buffers.empty());
+}
+
+TEST(FetchBufferPoolTest, SunnyDay) {
+ FetchBufferPool fetch_buffer_pool;
+
+ std::vector<uint8_t> source_buffer(1024 * 1024 + 1023);
+ FillTestData(&source_buffer);
+
+ std::vector<uint8_t> reference_buffer;
+ for (int i = 0; i < 10; ++i) {
+ fetch_buffer_pool.Write(source_buffer.data(),
+ static_cast<int>(source_buffer.size()));
+ reference_buffer.insert(reference_buffer.end(), source_buffer.begin(),
+ source_buffer.end());
+ }
+
+ std::vector<script::PreallocatedArrayBufferData> buffers;
+ fetch_buffer_pool.ResetAndReturnAsArrayBuffers(true, &buffers);
+ ASSERT_FALSE(buffers.empty());
+ ASSERT_EQ(fetch_buffer_pool.GetSize(), 0);
+
+ std::vector<uint8_t> collected_buffer;
+ AppendBuffersTo(buffers, &collected_buffer);
+ ASSERT_EQ(collected_buffer, reference_buffer);
+}
+
+TEST(FetchBufferPoolTest, Clear) {
+ FetchBufferPool fetch_buffer_pool;
+
+ std::vector<uint8_t> source_buffer(1024);
+ FillTestData(&source_buffer);
+
+ fetch_buffer_pool.Write(source_buffer.data(),
+ static_cast<int>(source_buffer.size()));
+ EXPECT_EQ(fetch_buffer_pool.GetSize(), source_buffer.size());
+
+ fetch_buffer_pool.Clear();
+
+ EXPECT_EQ(fetch_buffer_pool.GetSize(), 0);
+
+ std::vector<script::PreallocatedArrayBufferData> buffers;
+ fetch_buffer_pool.ResetAndReturnAsArrayBuffers(false, &buffers);
+ ASSERT_TRUE(buffers.empty());
+ fetch_buffer_pool.ResetAndReturnAsArrayBuffers(true, &buffers);
+ ASSERT_TRUE(buffers.empty());
+}
+
+TEST(FetchBufferPoolTest, IncrementalWriteAndRead) {
+ FetchBufferPool fetch_buffer_pool;
+
+ std::vector<uint8_t> source_buffer(4097);
+ FillTestData(&source_buffer);
+
+ std::vector<uint8_t> reference_buffer;
+ std::vector<uint8_t> collected_buffer;
+ while (reference_buffer.size() < 1024 * 1024 * 10) {
+ int bytes_to_write =
+ static_cast<int>((reference_buffer.size() + 1) % source_buffer.size());
+ fetch_buffer_pool.Write(source_buffer.data(), bytes_to_write);
+ reference_buffer.insert(reference_buffer.end(), source_buffer.begin(),
+ source_buffer.begin() + bytes_to_write);
+
+ std::vector<script::PreallocatedArrayBufferData> buffers;
+ fetch_buffer_pool.ResetAndReturnAsArrayBuffers(false, &buffers);
+ ASSERT_FALSE(buffers.empty());
+ AppendBuffersTo(buffers, &collected_buffer);
+ }
+
+ std::vector<script::PreallocatedArrayBufferData> buffers;
+ fetch_buffer_pool.ResetAndReturnAsArrayBuffers(true, &buffers);
+ ASSERT_EQ(fetch_buffer_pool.GetSize(), 0);
+ AppendBuffersTo(buffers, &collected_buffer);
+
+ ASSERT_EQ(collected_buffer, reference_buffer);
+}
+
+TEST(FetchBufferPoolTest, SingleLargeWrite) {
+ FetchBufferPool fetch_buffer_pool;
+
+ std::vector<uint8_t> large_source_buffer(1024 * 1024 * 16 + 1023);
+ FillTestData(&large_source_buffer);
+
+ {
+ std::vector<uint8_t> reference_buffer;
+ std::vector<uint8_t> collected_buffer;
+
+ fetch_buffer_pool.Write(large_source_buffer.data(),
+ static_cast<int>(large_source_buffer.size()));
+ reference_buffer.insert(reference_buffer.end(), large_source_buffer.begin(),
+ large_source_buffer.end());
+
+ std::vector<script::PreallocatedArrayBufferData> buffers;
+ fetch_buffer_pool.ResetAndReturnAsArrayBuffers(true, &buffers);
+ ASSERT_FALSE(buffers.empty());
+ ASSERT_EQ(fetch_buffer_pool.GetSize(), 0);
+
+ AppendBuffersTo(buffers, &collected_buffer);
+ ASSERT_EQ(collected_buffer, reference_buffer);
+ }
+}
+
+TEST(FetchBufferPoolTest, MixedSizeWrites) {
+ FetchBufferPool fetch_buffer_pool;
+
+ std::vector<uint8_t> small_source_buffer(1023);
+ std::vector<uint8_t> large_source_buffer(1024 * 1024 * 16 + 1023);
+
+ FillTestData(&small_source_buffer);
+ FillTestData(&large_source_buffer);
+
+ {
+ // Small then large
+ std::vector<uint8_t> reference_buffer;
+ std::vector<uint8_t> collected_buffer;
+
+ fetch_buffer_pool.Write(small_source_buffer.data(),
+ static_cast<int>(small_source_buffer.size()));
+ reference_buffer.insert(reference_buffer.end(), small_source_buffer.begin(),
+ small_source_buffer.end());
+
+ fetch_buffer_pool.Write(large_source_buffer.data(),
+ static_cast<int>(large_source_buffer.size()));
+ reference_buffer.insert(reference_buffer.end(), large_source_buffer.begin(),
+ large_source_buffer.end());
+
+ std::vector<script::PreallocatedArrayBufferData> buffers;
+ fetch_buffer_pool.ResetAndReturnAsArrayBuffers(false, &buffers);
+ ASSERT_FALSE(buffers.empty());
+ AppendBuffersTo(buffers, &collected_buffer);
+
+ fetch_buffer_pool.ResetAndReturnAsArrayBuffers(true, &buffers);
+ ASSERT_EQ(fetch_buffer_pool.GetSize(), 0);
+ AppendBuffersTo(buffers, &collected_buffer);
+
+ ASSERT_EQ(collected_buffer, reference_buffer);
+ }
+
+ {
+ // Large then small
+ std::vector<uint8_t> reference_buffer;
+ std::vector<uint8_t> collected_buffer;
+
+ fetch_buffer_pool.Write(large_source_buffer.data(),
+ static_cast<int>(large_source_buffer.size()));
+ reference_buffer.insert(reference_buffer.end(), large_source_buffer.begin(),
+ large_source_buffer.end());
+
+ fetch_buffer_pool.Write(small_source_buffer.data(),
+ static_cast<int>(small_source_buffer.size()));
+ reference_buffer.insert(reference_buffer.end(), small_source_buffer.begin(),
+ small_source_buffer.end());
+
+ std::vector<script::PreallocatedArrayBufferData> buffers;
+ fetch_buffer_pool.ResetAndReturnAsArrayBuffers(false, &buffers);
+ ASSERT_FALSE(buffers.empty());
+ AppendBuffersTo(buffers, &collected_buffer);
+
+ fetch_buffer_pool.ResetAndReturnAsArrayBuffers(true, &buffers);
+ ASSERT_EQ(fetch_buffer_pool.GetSize(), 0);
+ AppendBuffersTo(buffers, &collected_buffer);
+
+ ASSERT_EQ(collected_buffer, reference_buffer);
+ }
+}
+
+} // namespace
+} // namespace xhr
+} // namespace cobalt
diff --git a/cobalt/xhr/url_fetcher_buffer_writer.cc b/cobalt/xhr/url_fetcher_buffer_writer.cc
index fd31307..3876437 100644
--- a/cobalt/xhr/url_fetcher_buffer_writer.cc
+++ b/cobalt/xhr/url_fetcher_buffer_writer.cc
@@ -1,4 +1,4 @@
-// Copyright 2019 Google Inc. All Rights Reserved.
+// Copyright 2019 The Cobalt Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,6 +14,8 @@
#include "cobalt/xhr/url_fetcher_buffer_writer.h"
+#include <algorithm>
+
#include "base/logging.h"
#include "net/base/net_errors.h"
#include "starboard/memory.h"
@@ -44,7 +46,23 @@
} // namespace
-URLFetcherResponseWriter::Buffer::Buffer(Type type) : type_(type) {}
+URLFetcherResponseWriter::Buffer::Buffer(
+ Type type, const base::Optional<bool>& enable_try_lock_for_progress_check)
+ : type_(type),
+ enable_try_lock_for_progress_check_(
+ enable_try_lock_for_progress_check.value_or(false)) {
+ DCHECK_NE(type_, kBufferPool);
+}
+
+URLFetcherResponseWriter::Buffer::Buffer(
+ Type type, const base::Optional<bool>& enable_try_lock_for_progress_check,
+ const base::Optional<int>& fetch_buffer_size)
+ : type_(type),
+ enable_try_lock_for_progress_check_(
+ enable_try_lock_for_progress_check.value_or(true)),
+ data_as_buffer_pool_(fetch_buffer_size) {
+ DCHECK_EQ(type_, kBufferPool);
+}
void URLFetcherResponseWriter::Buffer::DisablePreallocate() {
base::AutoLock auto_lock(lock_);
@@ -70,9 +88,28 @@
return static_cast<int64_t>(download_progress_);
}
-bool URLFetcherResponseWriter::Buffer::HasProgressSinceLastGetAndReset() const {
- base::AutoLock auto_lock(lock_);
- return GetSize_Locked() > download_progress_;
+bool URLFetcherResponseWriter::Buffer::HasProgressSinceLastGetAndReset(
+ bool request_done) const {
+ if (!enable_try_lock_for_progress_check_) {
+ base::AutoLock auto_lock(lock_);
+ return GetSize_Locked() > download_progress_;
+ }
+
+ // The |lock_| might be held on the network thread to copy data, which can be
+ // slow sometimes. Waiting for |lock_| may block the JS thread for extended
+ // amount of time, so we only wait for it when the |request_done| is true,
+ // where we absolutely have to know the correct progress.
+ if (request_done) {
+ base::AutoLock auto_lock(lock_);
+ return GetSize_Locked() > download_progress_;
+ }
+ if (!lock_.Try()) {
+ // Failed to acquire |lock_|, assume there is no progress.
+ return false;
+ }
+ bool has_progress = GetSize_Locked() > download_progress_;
+ lock_.Release();
+ return has_progress;
}
const std::string&
@@ -81,26 +118,31 @@
UpdateType_Locked(kString);
allow_write_ = false;
-
return data_as_string_;
}
const std::string&
URLFetcherResponseWriter::Buffer::GetTemporaryReferenceOfString() {
+ DCHECK_NE(type_, kBufferPool);
+
base::AutoLock auto_lock(lock_);
// This function can be further optimized by always return reference of
// |data_as_string_|, and only make a copy when |data_as_string_| is extended.
- // It is not done as GetTemporaryReferenceOfString() is currently not
+ // It is not done as GetTemporaryReferenceOfString() is currently not
// triggered. It will only be called when JS app is retrieving responseText
// while the request is still in progress.
-
- if (type_ == kString) {
- copy_of_data_as_string_ = data_as_string_;
- } else {
- DCHECK_EQ(type_, kArrayBuffer);
- const char* begin = static_cast<const char*>(data_as_array_buffer_.data());
- copy_of_data_as_string_.assign(begin, begin + data_as_array_buffer_size_);
+ switch (type_) {
+ case kString:
+ copy_of_data_as_string_ = data_as_string_;
+ break;
+ case kArrayBuffer: {
+ auto data = data_as_array_buffer_.data();
+ copy_of_data_as_string_.assign(data, data + data_as_array_buffer_size_);
+ } break;
+ case kBufferPool:
+ NOTREACHED();
+ break;
}
return copy_of_data_as_string_;
@@ -131,6 +173,25 @@
download_progress_ = 0;
}
+void URLFetcherResponseWriter::Buffer::GetAndResetDataAndDownloadProgress(
+ bool request_done,
+ std::vector<script::PreallocatedArrayBufferData>* buffers) {
+ DCHECK(buffers);
+
+ buffers->clear();
+
+ base::AutoLock auto_lock(lock_);
+
+ DCHECK_EQ(type_, kBufferPool);
+ data_as_buffer_pool_.ResetAndReturnAsArrayBuffers(request_done, buffers);
+
+ // It is important to reset the |download_progress_| and return the data in
+ // one function to avoid potential race condition that may prevent the last
+ // bit of data of Fetcher from being downloaded, because the data download is
+ // guarded by HasProgressSinceLastGetAndReset().
+ download_progress_ = 0;
+}
+
void URLFetcherResponseWriter::Buffer::GetAndResetData(
PreallocatedArrayBufferData* data) {
DCHECK(data);
@@ -185,6 +246,9 @@
DCHECK_EQ(data_as_array_buffer_size_, 0u);
data_as_array_buffer_.Resize(capacity);
return;
+ case kBufferPool:
+ // FetchBufferPool don't need preallocate.
+ return;
}
NOTREACHED();
}
@@ -197,6 +261,8 @@
return;
}
+ const char* data = static_cast<const char*>(buffer);
+
base::AutoLock auto_lock(lock_);
DCHECK(allow_write_);
@@ -211,33 +277,39 @@
SB_LOG(WARNING) << "Data written is larger than the preset capacity "
<< data_as_string_.capacity();
}
- data_as_string_.append(static_cast<const char*>(buffer), num_bytes);
+ data_as_string_.append(data, num_bytes);
return;
}
- DCHECK_EQ(type_, kArrayBuffer);
- if (data_as_array_buffer_size_ + num_bytes >
- data_as_array_buffer_.byte_length()) {
- if (capacity_known_) {
- SB_LOG(WARNING) << "Data written is larger than the preset capacity "
- << data_as_array_buffer_.byte_length();
+ if (type_ == kArrayBuffer) {
+ if (data_as_array_buffer_size_ + num_bytes >
+ data_as_array_buffer_.byte_length()) {
+ if (capacity_known_) {
+ SB_LOG(WARNING) << "Data written is larger than the preset capacity "
+ << data_as_array_buffer_.byte_length();
+ }
+ size_t new_size = std::max(
+ std::min(data_as_array_buffer_.byte_length() * kResizingMultiplier,
+ desired_capacity_),
+ data_as_array_buffer_size_ + num_bytes);
+ if (new_size > desired_capacity_) {
+ // Content-length is wrong, response size is completely unknown.
+ // Double the capacity to avoid frequent resizing.
+ new_size *= kResizingMultiplier;
+ }
+ data_as_array_buffer_.Resize(new_size);
}
- size_t new_size = std::max(
- std::min(data_as_array_buffer_.byte_length() * kResizingMultiplier,
- desired_capacity_),
- data_as_array_buffer_size_ + num_bytes);
- if (new_size > desired_capacity_) {
- // Content-length is wrong, response size is completely unknown.
- // Double the capacity to avoid frequent resizing.
- new_size *= kResizingMultiplier;
- }
- data_as_array_buffer_.Resize(new_size);
+
+ auto destination = static_cast<uint8_t*>(data_as_array_buffer_.data()) +
+ data_as_array_buffer_size_;
+ memcpy(destination, data, num_bytes);
+ data_as_array_buffer_size_ += num_bytes;
+
+ return;
}
- auto destination = static_cast<uint8_t*>(data_as_array_buffer_.data()) +
- data_as_array_buffer_size_;
- memcpy(destination, buffer, num_bytes);
- data_as_array_buffer_size_ += num_bytes;
+ DCHECK_EQ(type_, kBufferPool);
+ data_as_buffer_pool_.Write(data, num_bytes);
}
size_t URLFetcherResponseWriter::Buffer::GetSize_Locked() const {
@@ -248,6 +320,8 @@
return data_as_string_.size();
case kArrayBuffer:
return data_as_array_buffer_size_;
+ case kBufferPool:
+ return data_as_buffer_pool_.GetSize();
}
NOTREACHED();
return 0;
@@ -260,6 +334,10 @@
return;
}
+ // kBufferPool cannot be converted to or from other types.
+ DCHECK_NE(type_, kBufferPool);
+ DCHECK_NE(type, kBufferPool);
+
DCHECK(allow_write_);
DLOG_IF(WARNING, GetSize_Locked() > 0)
@@ -282,7 +360,7 @@
data_as_array_buffer_.Resize(data_as_string_.capacity());
data_as_array_buffer_size_ = data_as_string_.size();
memcpy(data_as_array_buffer_.data(), data_as_string_.data(),
- data_as_array_buffer_size_);
+ data_as_array_buffer_size_);
ReleaseMemory(&data_as_string_);
ReleaseMemory(©_of_data_as_string_);
@@ -290,8 +368,9 @@
}
data_as_string_.reserve(data_as_array_buffer_.byte_length());
- data_as_string_.append(static_cast<const char*>(data_as_array_buffer_.data()),
- data_as_array_buffer_size_);
+ data_as_string_.append(
+ reinterpret_cast<const char*>(data_as_array_buffer_.data()),
+ data_as_array_buffer_size_);
ReleaseMemory(&data_as_array_buffer_);
data_as_array_buffer_size_ = 0;
diff --git a/cobalt/xhr/url_fetcher_buffer_writer.h b/cobalt/xhr/url_fetcher_buffer_writer.h
index 35058ab..245709c 100644
--- a/cobalt/xhr/url_fetcher_buffer_writer.h
+++ b/cobalt/xhr/url_fetcher_buffer_writer.h
@@ -1,4 +1,4 @@
-// Copyright 2019 Google Inc. All Rights Reserved.
+// Copyright 2019 The Cobalt Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -17,12 +17,15 @@
#include <memory>
#include <string>
+#include <vector>
#include "base/callback.h"
#include "base/memory/ref_counted.h"
+#include "base/optional.h"
#include "base/synchronization/lock.h"
#include "base/task_runner.h"
#include "cobalt/script/array_buffer.h"
+#include "cobalt/xhr/fetch_buffer_pool.h"
#include "net/base/io_buffer.h"
#include "net/url_request/url_fetcher_response_writer.h"
@@ -38,15 +41,25 @@
enum Type {
kString,
kArrayBuffer,
+ kBufferPool,
};
- explicit Buffer(Type type);
+ // This ctor should be used when |type| isn't |kBufferPool|, it's checked in
+ // the implementation.
+ Buffer(Type type,
+ const base::Optional<bool>& enable_try_lock_for_progress_check);
+ // This ctor should be used when |type| is |kBufferPool|, it's checked in
+ // the implementation.
+ Buffer(Type type,
+ const base::Optional<bool>& enable_try_lock_for_progress_check,
+ const base::Optional<int>& fetch_buffer_size);
void DisablePreallocate();
void Clear();
+ Type type() const { return type_; }
int64_t GetAndResetDownloadProgress();
- bool HasProgressSinceLastGetAndReset() const;
+ bool HasProgressSinceLastGetAndReset(bool request_done) const;
// When the following function is called, Write() can no longer be called to
// append more data. It is the responsibility of the user of this class to
@@ -58,6 +71,9 @@
const std::string& GetTemporaryReferenceOfString();
void GetAndResetDataAndDownloadProgress(std::string* str);
+ void GetAndResetDataAndDownloadProgress(
+ bool request_done,
+ std::vector<script::PreallocatedArrayBufferData>* buffers);
void GetAndResetData(PreallocatedArrayBufferData* data);
void MaybePreallocate(int64_t capacity);
@@ -72,6 +88,7 @@
void UpdateType_Locked(Type type);
Type type_;
+ const bool enable_try_lock_for_progress_check_;
bool allow_preallocate_ = true;
bool capacity_known_ = false;
size_t desired_capacity_ = 0;
@@ -84,11 +101,17 @@
// Data is stored in one of the following buffers, depends on the value of
// |type_|.
+ // When |type_| is kString:
std::string data_as_string_;
// For use in GetReferenceOfString() so it can return a reference.
std::string copy_of_data_as_string_;
+
+ // When |type_| is kArrayBuffer:
PreallocatedArrayBufferData data_as_array_buffer_;
size_t data_as_array_buffer_size_ = 0;
+
+ // When |type_| is kBufferPool:
+ FetchBufferPool data_as_buffer_pool_;
};
explicit URLFetcherResponseWriter(const scoped_refptr<Buffer>& buffer);
diff --git a/cobalt/xhr/xhr_modify_headers.h b/cobalt/xhr/xhr_modify_headers.h
deleted file mode 100644
index 69c4f69..0000000
--- a/cobalt/xhr/xhr_modify_headers.h
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright 2017 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef COBALT_XHR_XHR_MODIFY_HEADERS_H_
-#define COBALT_XHR_XHR_MODIFY_HEADERS_H_
-
-#include "net/http/http_request_headers.h"
-#include "url/gurl.h"
-
-namespace cobalt {
-namespace xhr {
-
-void CobaltXhrModifyHeader(const GURL& request_url,
- net::HttpRequestHeaders* headers);
-
-} // namespace xhr
-} // namespace cobalt
-
-#endif // COBALT_XHR_XHR_MODIFY_HEADERS_H_
diff --git a/cobalt/xhr/xhr_settings.h b/cobalt/xhr/xhr_settings.h
new file mode 100644
index 0000000..be07a62
--- /dev/null
+++ b/cobalt/xhr/xhr_settings.h
@@ -0,0 +1,103 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_XHR_XHR_SETTINGS_H_
+#define COBALT_XHR_XHR_SETTINGS_H_
+
+#include <string>
+
+#include "base/optional.h"
+#include "base/synchronization/lock.h"
+
+namespace cobalt {
+namespace xhr {
+
+// Holds browser wide settings for XMLHttpRequest. Their default values are set
+// in XMLHttpRequest related classes, and the default values will be overridden
+// if the return values of the member functions are non-empty.
+// Please refer to where these functions are called for the particular
+// XMLHttpRequest behaviors being controlled by them.
+class XhrSettings {
+ public:
+ virtual base::Optional<bool> IsFetchBufferPoolEnabled() const = 0;
+ virtual base::Optional<int> GetDefaultFetchBufferSize() const = 0;
+ virtual base::Optional<bool> IsTryLockForProgressCheckEnabled() const = 0;
+
+ protected:
+ XhrSettings() = default;
+ ~XhrSettings() = default;
+
+ XhrSettings(const XhrSettings&) = delete;
+ XhrSettings& operator=(const XhrSettings&) = delete;
+};
+
+// Allows setting the values of XMLHttpRequest settings via a name and an int
+// value.
+// This class is thread safe.
+class XhrSettingsImpl : public XhrSettings {
+ public:
+ base::Optional<bool> IsFetchBufferPoolEnabled() const override {
+ base::AutoLock auto_lock(lock_);
+ return is_fetch_buffer_pool_enabled_;
+ }
+ base::Optional<int> GetDefaultFetchBufferSize() const override {
+ base::AutoLock auto_lock(lock_);
+ return default_fetch_buffer_size_;
+ }
+ base::Optional<bool> IsTryLockForProgressCheckEnabled() const override {
+ base::AutoLock auto_lock(lock_);
+ return is_try_lock_for_progress_check_enabled_;
+ }
+
+ bool Set(const std::string& name, int32_t value) {
+ if (name == "XHR.EnableFetchBufferPool") {
+ if (value == 0 || value == 1) {
+ LOG(INFO) << name << ": set to " << value;
+
+ base::AutoLock auto_lock(lock_);
+ is_fetch_buffer_pool_enabled_ = value != 0;
+ return true;
+ }
+ } else if (name == "XHR.DefaultFetchBufferSize") {
+ if (value > 0) {
+ LOG(INFO) << name << ": set to " << value;
+
+ base::AutoLock auto_lock(lock_);
+ default_fetch_buffer_size_ = value;
+ return true;
+ }
+ } else if (name == "XHR.EnableTryLockForProgressCheck") {
+ if (value == 0 || value == 1) {
+ LOG(INFO) << name << ": set to " << value;
+
+ base::AutoLock auto_lock(lock_);
+ is_try_lock_for_progress_check_enabled_ = value != 0;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private:
+ mutable base::Lock lock_;
+ base::Optional<bool> is_fetch_buffer_pool_enabled_;
+ base::Optional<int> default_fetch_buffer_size_;
+ base::Optional<bool> is_try_lock_for_progress_check_enabled_;
+};
+
+} // namespace xhr
+} // namespace cobalt
+
+#endif // COBALT_XHR_XHR_SETTINGS_H_
diff --git a/cobalt/xhr/xhr_settings_test.cc b/cobalt/xhr/xhr_settings_test.cc
new file mode 100644
index 0000000..9a2ceda
--- /dev/null
+++ b/cobalt/xhr/xhr_settings_test.cc
@@ -0,0 +1,91 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/xhr/xhr_settings.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace cobalt {
+namespace xhr {
+namespace {
+
+TEST(XhrSettingsImplTest, Empty) {
+ XhrSettingsImpl impl;
+
+ EXPECT_FALSE(impl.IsFetchBufferPoolEnabled());
+ EXPECT_FALSE(impl.GetDefaultFetchBufferSize());
+ EXPECT_FALSE(impl.IsTryLockForProgressCheckEnabled());
+}
+
+TEST(XhrSettingsImplTest, SunnyDay) {
+ XhrSettingsImpl impl;
+
+ ASSERT_TRUE(impl.Set("XHR.EnableFetchBufferPool", 1));
+ ASSERT_TRUE(impl.Set("XHR.DefaultFetchBufferSize", 1024 * 1024));
+ ASSERT_TRUE(impl.Set("XHR.EnableTryLockForProgressCheck", 1));
+
+ EXPECT_TRUE(impl.IsFetchBufferPoolEnabled().value());
+ EXPECT_EQ(impl.GetDefaultFetchBufferSize().value(), 1024 * 1024);
+ EXPECT_TRUE(impl.IsTryLockForProgressCheckEnabled());
+}
+
+TEST(XhrSettingsImplTest, RainyDay) {
+ XhrSettingsImpl impl;
+
+ ASSERT_FALSE(impl.Set("XHR.EnableFetchBufferPool", 2));
+ ASSERT_FALSE(impl.Set("XHR.DefaultFetchBufferSize", -100));
+ ASSERT_FALSE(impl.Set("XHR.EnableTryLockForProgressCheck", 3));
+
+ EXPECT_FALSE(impl.IsFetchBufferPoolEnabled());
+ EXPECT_FALSE(impl.GetDefaultFetchBufferSize());
+ EXPECT_FALSE(impl.IsTryLockForProgressCheckEnabled());
+}
+
+TEST(XhrSettingsImplTest, ZeroValuesWork) {
+ XhrSettingsImpl impl;
+
+ ASSERT_TRUE(impl.Set("XHR.EnableFetchBufferPool", 0));
+ // O is an invalid value for "XHR.DefaultFetchBufferSize".
+ ASSERT_TRUE(impl.Set("XHR.EnableTryLockForProgressCheck", 0));
+
+ EXPECT_FALSE(impl.IsFetchBufferPoolEnabled().value());
+ EXPECT_FALSE(impl.IsTryLockForProgressCheckEnabled().value());
+}
+
+TEST(XhrSettingsImplTest, Updatable) {
+ XhrSettingsImpl impl;
+
+ ASSERT_TRUE(impl.Set("XHR.EnableFetchBufferPool", 0));
+ ASSERT_TRUE(impl.Set("XHR.DefaultFetchBufferSize", 1024));
+ ASSERT_TRUE(impl.Set("XHR.EnableTryLockForProgressCheck", 0));
+
+ ASSERT_TRUE(impl.Set("XHR.EnableFetchBufferPool", 1));
+ ASSERT_TRUE(impl.Set("XHR.DefaultFetchBufferSize", 1024 * 2));
+ ASSERT_TRUE(impl.Set("XHR.EnableTryLockForProgressCheck", 1));
+
+ EXPECT_TRUE(impl.IsFetchBufferPoolEnabled().value());
+ EXPECT_EQ(impl.GetDefaultFetchBufferSize().value(), 1024 * 2);
+ EXPECT_TRUE(impl.IsTryLockForProgressCheckEnabled().value());
+}
+
+TEST(XhrSettingsImplTest, InvalidSettingNames) {
+ XhrSettingsImpl impl;
+
+ ASSERT_FALSE(impl.Set("XHR.Invalid", 0));
+ ASSERT_FALSE(impl.Set("Invalid.EnableFetchBufferPool", 0));
+}
+
+} // namespace
+} // namespace xhr
+} // namespace cobalt
diff --git a/cobalt/xhr/xml_http_request.cc b/cobalt/xhr/xml_http_request.cc
index 551bd8c8..0a9d4ed 100644
--- a/cobalt/xhr/xml_http_request.cc
+++ b/cobalt/xhr/xml_http_request.cc
@@ -33,6 +33,7 @@
#include "cobalt/dom/xml_document.h"
#include "cobalt/dom_parser/xml_decoder.h"
#include "cobalt/loader/cors_preflight.h"
+#include "cobalt/loader/fetch_interceptor_coordinator.h"
#include "cobalt/loader/fetcher_factory.h"
#include "cobalt/loader/url_fetcher_string_writer.h"
#include "cobalt/script/global_environment.h"
@@ -41,7 +42,6 @@
#include "cobalt/web/csp_delegate.h"
#include "cobalt/web/environment_settings.h"
#include "cobalt/xhr/global_stats.h"
-#include "cobalt/xhr/xhr_modify_headers.h"
#include "nb/memory_scope.h"
#include "net/http/http_util.h"
@@ -305,7 +305,8 @@
int64_t current, int64_t total,
int64_t current_network_bytes) {
xhr_impl_->OnURLFetchDownloadProgress(source, current, total,
- current_network_bytes);
+ current_network_bytes,
+ /* request_done = */ false);
}
void XMLHttpRequest::OnURLFetchComplete(const net::URLFetcher* source) {
xhr_impl_->OnURLFetchComplete(source);
@@ -345,7 +346,12 @@
is_redirect_(false),
method_(net::URLFetcher::GET),
response_body_(new URLFetcherResponseWriter::Buffer(
- URLFetcherResponseWriter::Buffer::kString)),
+ URLFetcherResponseWriter::Buffer::kString,
+ xhr->environment_settings()
+ ->context()
+ ->web_settings()
+ ->xhr_settings()
+ .IsTryLockForProgressCheckEnabled())),
response_type_(XMLHttpRequest::kDefault),
state_(XMLHttpRequest::kUnsent),
upload_listener_(false),
@@ -357,6 +363,7 @@
sent_(false),
settings_(xhr->environment_settings()),
stop_timeout_(false),
+ task_runner_(base::MessageLoop::current()->task_runner()),
timeout_ms_(0),
upload_complete_(false) {
DCHECK(environment_settings());
@@ -502,6 +509,103 @@
void XMLHttpRequestImpl::Send(
const base::Optional<XMLHttpRequest::RequestBodyType>& request_body,
script::ExceptionState* exception_state) {
+ error_ = false;
+ bool in_service_worker = environment_settings()
+ ->context()
+ ->GetWindowOrWorkerGlobalScope()
+ ->IsServiceWorker();
+ if (!in_service_worker && method_ == net::URLFetcher::GET) {
+ loader::FetchInterceptorCoordinator::GetInstance()->TryIntercept(
+ request_url_,
+ std::make_unique<
+ base::OnceCallback<void(std::unique_ptr<std::string>)>>(
+ base::BindOnce(&XMLHttpRequestImpl::SendIntercepted,
+ base::Unretained(this))),
+ std::make_unique<base::OnceCallback<void(const net::LoadTimingInfo&)>>(
+ base::BindOnce(&XMLHttpRequestImpl::ReportLoadTimingInfo,
+ base::Unretained(this))),
+ std::make_unique<base::OnceClosure>(base::BindOnce(
+ &XMLHttpRequestImpl::SendFallback, base::Unretained(this),
+ request_body, exception_state)));
+ return;
+ }
+ SendFallback(request_body, exception_state);
+}
+
+void XMLHttpRequestImpl::SendIntercepted(
+ std::unique_ptr<std::string> response) {
+ if (task_runner_ != base::MessageLoop::current()->task_runner()) {
+ task_runner_->PostTask(
+ FROM_HERE, base::BindOnce(&XMLHttpRequestImpl::SendIntercepted,
+ base::Unretained(this), std::move(response)));
+ return;
+ }
+ sent_ = true;
+ // Now that a send is happening, prevent this object
+ // from being collected until it's complete or aborted
+ // if no currently active request has called it before.
+ // TODO: consider deduplicating code from |Send()|,
+ // |OnURLFetchDownloadProgress()|, and |OnURLFetchComplete()|.
+
+ // Send().
+ IncrementActiveRequests();
+ FireProgressEvent(xhr_, base::Tokens::loadstart());
+ if (!upload_complete_) {
+ FireProgressEvent(upload_, base::Tokens::loadstart());
+ }
+
+ // OnURLFetchResponseStarted().
+ http_status_ = 200;
+ http_response_headers_ = new net::HttpResponseHeaders("");
+ ChangeState(XMLHttpRequest::kHeadersReceived);
+
+ // OnURLFetchDownloadProgress().
+ ChangeState(XMLHttpRequest::kLoading);
+ const auto& xhr_settings =
+ environment_settings()->context()->web_settings()->xhr_settings();
+ response_body_ = new URLFetcherResponseWriter::Buffer(
+ URLFetcherResponseWriter::Buffer::kString,
+ xhr_settings.IsTryLockForProgressCheckEnabled());
+ response_body_->Write(response->data(), response->size());
+ if (fetch_callback_) {
+ script::Handle<script::Uint8Array> data =
+ script::Uint8Array::New(settings_->context()->global_environment(),
+ response->data(), response->size());
+ fetch_callback_->value().Run(data);
+ }
+
+ // OnURLFetchComplete().
+ if (!upload_complete_ && upload_listener_) {
+ upload_complete_ = true;
+ FireProgressEvent(upload_, base::Tokens::progress());
+ FireProgressEvent(upload_, base::Tokens::load());
+ FireProgressEvent(upload_, base::Tokens::loadend());
+ }
+ ChangeState(XMLHttpRequest::kDone);
+ size_t received_length = response->size();
+ FireProgressEvent(xhr_, base::Tokens::load(), received_length,
+ received_length,
+ /*length_computable=*/true);
+ FireProgressEvent(xhr_, base::Tokens::loadend(), received_length,
+ received_length,
+ /*length_computable=*/true);
+ // Undo the ref we added in Send()
+ DecrementActiveRequests();
+
+ fetch_callback_.reset();
+ fetch_mode_callback_.reset();
+}
+
+void XMLHttpRequestImpl::SendFallback(
+ const base::Optional<XMLHttpRequest::RequestBodyType>& request_body,
+ script::ExceptionState* exception_state) {
+ if (task_runner_ != base::MessageLoop::current()->task_runner()) {
+ task_runner_->PostTask(
+ FROM_HERE,
+ base::BindOnce(&XMLHttpRequestImpl::SendFallback,
+ base::Unretained(this), request_body, exception_state));
+ return;
+ }
TRACK_MEMORY_SCOPE("XHR");
// https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-send()-method
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
@@ -522,10 +626,6 @@
error_ = false;
upload_complete_ = false;
-#if defined(COBALT_ENABLE_XHR_HEADER_FILTERING)
- CobaltXhrModifyHeader(request_url_, &request_headers_);
-#endif
-
// Add request body, if appropriate.
if ((method_ == net::URLFetcher::POST || method_ == net::URLFetcher::PUT) &&
request_body) {
@@ -539,17 +639,17 @@
"text/plain;charset=UTF-8");
}
} else if (request_body
- ->IsType<script::Handle<script::ArrayBufferView> >()) {
+ ->IsType<script::Handle<script::ArrayBufferView>>()) {
script::Handle<script::ArrayBufferView> view =
- request_body->AsType<script::Handle<script::ArrayBufferView> >();
+ request_body->AsType<script::Handle<script::ArrayBufferView>>();
if (view->ByteLength()) {
const char* start = reinterpret_cast<const char*>(view->RawData());
request_body_text_.assign(start + view->ByteOffset(),
view->ByteLength());
}
- } else if (request_body->IsType<script::Handle<script::ArrayBuffer> >()) {
+ } else if (request_body->IsType<script::Handle<script::ArrayBuffer>>()) {
script::Handle<script::ArrayBuffer> array_buffer =
- request_body->AsType<script::Handle<script::ArrayBuffer> >();
+ request_body->AsType<script::Handle<script::ArrayBuffer>>();
if (array_buffer->ByteLength()) {
const char* start = reinterpret_cast<const char*>(array_buffer->Data());
request_body_text_.assign(start, array_buffer->ByteLength());
@@ -841,7 +941,7 @@
if (is_cross_origin_) {
size_t iter = 0;
std::string name, value;
- std::vector<std::pair<std::string, std::string> > header_names_to_discard;
+ std::vector<std::pair<std::string, std::string>> header_names_to_discard;
std::vector<std::string> expose_headers;
loader::CORSPreflight::GetServerAllowedHeaders(*http_response_headers_,
&expose_headers);
@@ -859,7 +959,7 @@
if (is_data_url_) {
size_t iter = 0;
std::string name, value;
- std::vector<std::pair<std::string, std::string> > header_names_to_discard;
+ std::vector<std::pair<std::string, std::string>> header_names_to_discard;
while (http_response_headers_->EnumerateHeaderLines(&iter, &name, &value)) {
if (name != net::HttpRequestHeaders::kContentType) {
header_names_to_discard.push_back(std::make_pair(name, value));
@@ -876,13 +976,13 @@
}
void XMLHttpRequestImpl::OnURLFetchDownloadProgress(
- const net::URLFetcher* source, int64_t current, int64_t total,
- int64_t current_network_bytes) {
+ const net::URLFetcher* source, int64_t /*current*/, int64_t /*total*/,
+ int64_t /*current_network_bytes*/, bool request_done) {
TRACK_MEMORY_SCOPE("XHR");
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DCHECK_NE(state_, XMLHttpRequest::kDone);
- if (response_body_->HasProgressSinceLastGetAndReset() == 0) {
+ if (response_body_->HasProgressSinceLastGetAndReset(request_done) == 0) {
return;
}
@@ -890,12 +990,29 @@
ChangeState(XMLHttpRequest::kLoading);
if (fetch_callback_) {
- std::string downloaded_data;
- response_body_->GetAndResetDataAndDownloadProgress(&downloaded_data);
- script::Handle<script::Uint8Array> data = script::Uint8Array::New(
- environment_settings()->context()->global_environment(),
- downloaded_data.data(), downloaded_data.size());
- fetch_callback_->value().Run(data);
+ if (response_body_->type() == URLFetcherResponseWriter::Buffer::kString) {
+ std::string downloaded_data;
+ response_body_->GetAndResetDataAndDownloadProgress(&downloaded_data);
+ script::Handle<script::Uint8Array> data = script::Uint8Array::New(
+ environment_settings()->context()->global_environment(),
+ downloaded_data.data(), downloaded_data.size());
+ fetch_callback_->value().Run(data);
+ } else {
+ DCHECK_EQ(response_body_->type(),
+ URLFetcherResponseWriter::Buffer::kBufferPool);
+ std::vector<script::PreallocatedArrayBufferData> downloaded_buffers;
+ response_body_->GetAndResetDataAndDownloadProgress(request_done,
+ &downloaded_buffers);
+ for (auto& downloaded_buffer : downloaded_buffers) {
+ auto array_buffer =
+ script::ArrayBuffer::New(settings_->context()->global_environment(),
+ std::move(downloaded_buffer));
+ script::Handle<script::Uint8Array> data = script::Uint8Array::New(
+ settings_->context()->global_environment(), array_buffer, 0,
+ array_buffer->ByteLength());
+ fetch_callback_->value().Run(data);
+ }
+ }
}
// Send a progress notification if at least 50ms have elapsed.
@@ -942,7 +1059,7 @@
// Ensure all fetched data is read and transferred to this XHR. This should
// only be done for successful and error-free fetches.
- OnURLFetchDownloadProgress(source, 0, 0, 0);
+ OnURLFetchDownloadProgress(source, 0, 0, 0, /* request_done = */ true);
// The request may have completed too quickly, before URLFetcher's upload
// progress timer had a chance to inform us upload is finished.
@@ -1199,9 +1316,8 @@
// The request is done so it is safe to only keep the ArrayBuffer and clear
// |response_body_|. As |response_body_| will not be used unless the
// request is re-opened.
- std::unique_ptr<script::PreallocatedArrayBufferData> downloaded_data(
- new script::PreallocatedArrayBufferData());
- response_body_->GetAndResetData(downloaded_data.get());
+ script::PreallocatedArrayBufferData downloaded_data;
+ response_body_->GetAndResetData(&downloaded_data);
auto array_buffer = script::ArrayBuffer::New(
environment_settings()->context()->global_environment(),
std::move(downloaded_data));
@@ -1242,7 +1358,18 @@
void XMLHttpRequestImpl::StartRequest(const std::string& request_body) {
TRACK_MEMORY_SCOPE("XHR");
- LOG(INFO) << "Fetching: " << ClipUrl(request_url_, 200);
+
+ const auto& xhr_settings =
+ environment_settings()->context()->web_settings()->xhr_settings();
+ const bool fetch_buffer_pool_enabled =
+ xhr_settings.IsFetchBufferPoolEnabled().value_or(false);
+
+ if (fetch_callback_ && fetch_buffer_pool_enabled) {
+ LOG(INFO) << "Fetching (backed by BufferPool): "
+ << ClipUrl(request_url_, 200);
+ } else {
+ LOG(INFO) << "Fetching: " << ClipUrl(request_url_, 200);
+ }
response_array_buffer_reference_.reset();
@@ -1251,15 +1378,30 @@
url_fetcher_ = net::URLFetcher::Create(request_url_, method_, xhr_);
++url_fetcher_generation_;
url_fetcher_->SetRequestContext(network_module->url_request_context_getter());
+
if (fetch_callback_) {
- response_body_ = new URLFetcherResponseWriter::Buffer(
- URLFetcherResponseWriter::Buffer::kString);
- response_body_->DisablePreallocate();
+ // FetchBufferPool is by default disabled, but can be explicitly overridden.
+ if (fetch_buffer_pool_enabled) {
+ response_body_ = new URLFetcherResponseWriter::Buffer(
+ URLFetcherResponseWriter::Buffer::kBufferPool,
+ xhr_settings.IsTryLockForProgressCheckEnabled(),
+ xhr_settings.GetDefaultFetchBufferSize());
+ } else {
+ response_body_ = new URLFetcherResponseWriter::Buffer(
+ URLFetcherResponseWriter::Buffer::kString,
+ xhr_settings.IsTryLockForProgressCheckEnabled());
+ response_body_->DisablePreallocate();
+ }
} else {
- response_body_ = new URLFetcherResponseWriter::Buffer(
- response_type_ == XMLHttpRequest::kArrayBuffer
- ? URLFetcherResponseWriter::Buffer::kArrayBuffer
- : URLFetcherResponseWriter::Buffer::kString);
+ if (response_type_ == XMLHttpRequest::kArrayBuffer) {
+ response_body_ = new URLFetcherResponseWriter::Buffer(
+ URLFetcherResponseWriter::Buffer::kArrayBuffer,
+ xhr_settings.IsTryLockForProgressCheckEnabled());
+ } else {
+ response_body_ = new URLFetcherResponseWriter::Buffer(
+ URLFetcherResponseWriter::Buffer::kString,
+ xhr_settings.IsTryLockForProgressCheckEnabled());
+ }
}
std::unique_ptr<net::URLFetcherResponseWriter> download_data_writer(
new URLFetcherResponseWriter(response_body_));
diff --git a/cobalt/xhr/xml_http_request.h b/cobalt/xhr/xml_http_request.h
index bbf3591..30c606d 100644
--- a/cobalt/xhr/xml_http_request.h
+++ b/cobalt/xhr/xml_http_request.h
@@ -290,7 +290,8 @@
void OnURLFetchResponseStarted(const net::URLFetcher* source);
void OnURLFetchDownloadProgress(const net::URLFetcher* source,
int64_t current, int64_t total,
- int64_t current_network_bytes);
+ int64_t current_network_bytes,
+ bool request_done);
void OnURLFetchComplete(const net::URLFetcher* source);
void OnURLFetchUploadProgress(const net::URLFetcher* source, int64 current,
@@ -377,6 +378,11 @@
virtual void StartRequest(const std::string& request_body);
+ void SendFallback(
+ const base::Optional<XMLHttpRequest::RequestBodyType>& request_body,
+ script::ExceptionState* exception_state);
+ void SendIntercepted(std::unique_ptr<std::string> response);
+
// The following two methods are used to determine if garbage collection is
// needed. It is legal to reuse XHR and send a new request in last request's
// onload event listener. We should not allow garbage collection until
@@ -425,6 +431,7 @@
bool sent_;
web::EnvironmentSettings* const settings_;
bool stop_timeout_;
+ scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
uint32 timeout_ms_;
bool upload_complete_;
diff --git a/components/prefs/json_pref_store.cc b/components/prefs/json_pref_store.cc
index ef86d93..7d8d189 100644
--- a/components/prefs/json_pref_store.cc
+++ b/components/prefs/json_pref_store.cc
@@ -206,7 +206,9 @@
base::Value* old_value = nullptr;
prefs_->Get(key, &old_value);
if (!old_value || !value->Equals(old_value)) {
- prefs_->Set(key, std::move(value));
+ // Value::DictionaryValue::Set creates a nested dictionary treating a URL
+ // key as a path, SetKey avoids this.
+ prefs_->SetKey(key, std::move(*value.get()));
ReportValueChanged(key, flags);
}
}
@@ -220,7 +222,7 @@
base::Value* old_value = nullptr;
prefs_->Get(key, &old_value);
if (!old_value || !value->Equals(old_value)) {
- prefs_->Set(key, std::move(value));
+ prefs_->SetPath({key}, base::Value::FromUniquePtrValue(std::move(value)));
ScheduleWrite(flags);
}
}
@@ -228,15 +230,16 @@
void JsonPrefStore::RemoveValue(const std::string& key, uint32_t flags) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
- if (prefs_->RemovePath(key, nullptr))
+ if (prefs_->RemovePath({key})) {
ReportValueChanged(key, flags);
+ }
}
void JsonPrefStore::RemoveValueSilently(const std::string& key,
uint32_t flags) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
- prefs_->RemovePath(key, nullptr);
+ prefs_->RemovePath({key});
ScheduleWrite(flags);
}
diff --git a/docker-compose-windows.yml b/docker-compose-windows.yml
index da83efe..e577ee4 100644
--- a/docker-compose-windows.yml
+++ b/docker-compose-windows.yml
@@ -29,6 +29,14 @@
cpus: ${DOCKER_CPUS:-12}
mem_limit: ${DOCKER_MEMLIMIT:-100000M}
+x-shared-build-env: &shared-build-env
+ IS_CI: ${IS_CI:-0}
+ IS_DOCKER: 1
+ PYTHONPATH: c:/code/
+ CONFIG: ${CONFIG:-devel}
+ TARGET: ${TARGET:-cobalt_install}
+ NINJA_FLAGS: ${NINJA_FLAGS}
+
services:
visual-studio-base:
build:
@@ -78,11 +86,8 @@
depends_on:
- cobalt-build-win32-base
environment:
- - PLATFORM=win-win32
- - CONFIG=${CONFIG:-devel}
- - TARGET=${TARGET:-cobalt_install}
- - NINJA_FLAGS
- - IS_DOCKER=1
+ <<: *shared-build-env
+ PLATFORM: win-win32
image: cobalt-build-win32
build-win-win32:
diff --git a/docker/linux/base/Dockerfile b/docker/linux/base/Dockerfile
index 50dd9e0..849e656 100644
--- a/docker/linux/base/Dockerfile
+++ b/docker/linux/base/Dockerfile
@@ -32,6 +32,4 @@
curl xz-utils \
&& /opt/clean-after-apt.sh
-RUN python3 -m pip install --upgrade pip
-
CMD ["/usr/bin/python","--version"]
diff --git a/docker/linux/base/build/Dockerfile b/docker/linux/base/build/Dockerfile
index aadc720..a3f0a3b 100644
--- a/docker/linux/base/build/Dockerfile
+++ b/docker/linux/base/build/Dockerfile
@@ -15,8 +15,6 @@
ARG FROM_IMAGE
FROM ${FROM_IMAGE:-cobalt-base}
-ARG gn_hash=vC0rxqiqGTD3ls9KJHrgJoWP2OBiPk_QEO_xbDItKYoC
-
ARG HOME=/root
# === Install common build tools, required by all platforms
@@ -53,8 +51,12 @@
ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH
# === Get GN via CIPD
+ARG GN_SHA256SUM="af7b2dcb3905bca56655e12131b365f1cba8e159db80d2022330c4f522fab2ef /tmp/gn.zip"
+ARG GN_HASH=r3styzkFvKVmVeEhMbNl8cuo4VnbgNICIzDE9SL6su8C
+
RUN curl --location --silent --output /tmp/gn.zip \
- "https://chrome-infra-packages.appspot.com/dl/gn/gn/linux-amd64/+/${gn_hash}" \
+ "https://chrome-infra-packages.appspot.com/dl/gn/gn/linux-amd64/+/${GN_HASH}" \
+ && echo ${GN_SHA256SUM} | sha256sum --check \
&& unzip /tmp/gn.zip -d /usr/local/bin \
&& rm /tmp/gn.zip
diff --git a/docker/linux/linux-x64x11/Dockerfile b/docker/linux/linux-x64x11/Dockerfile
index f77957f..e28bacb 100644
--- a/docker/linux/linux-x64x11/Dockerfile
+++ b/docker/linux/linux-x64x11/Dockerfile
@@ -27,7 +27,8 @@
libxi-dev \
&& /opt/clean-after-apt.sh
-RUN python3 -m pip install "selenium==3.141.0" "Brotli==1.0.9"
+COPY ./linux-x64x11/requirements.txt /opt/requirements.txt
+RUN python3 -m pip install --require-hashes --no-deps -r /opt/requirements.txt
CMD gn gen ${OUTDIR}/${PLATFORM}_${CONFIG} --args="target_platform=\"${PLATFORM}\" build_type=\"${CONFIG}\" sb_api_version=${SB_API_VERSION:-14}" && \
ninja -v -j ${NINJA_PARALLEL} -C ${OUTDIR}/${PLATFORM}_${CONFIG} ${TARGET:-cobalt_install}
diff --git a/docker/linux/linux-x64x11/requirements.in b/docker/linux/linux-x64x11/requirements.in
new file mode 100644
index 0000000..5d8c046
--- /dev/null
+++ b/docker/linux/linux-x64x11/requirements.in
@@ -0,0 +1,2 @@
+selenium==3.141.0
+Brotli==1.0.9
diff --git a/docker/linux/linux-x64x11/requirements.txt b/docker/linux/linux-x64x11/requirements.txt
new file mode 100644
index 0000000..ef2d62e
--- /dev/null
+++ b/docker/linux/linux-x64x11/requirements.txt
@@ -0,0 +1,78 @@
+#
+# This file is autogenerated by pip-compile with python 3.10
+# To update, run:
+#
+# pip-compile --generate-hashes requirements.in
+#
+brotli==1.0.9 \
+ --hash=sha256:12effe280b8ebfd389022aa65114e30407540ccb89b177d3fbc9a4f177c4bd5d \
+ --hash=sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8 \
+ --hash=sha256:16d528a45c2e1909c2798f27f7bf0a3feec1dc9e50948e738b961618e38b6a7b \
+ --hash=sha256:19598ecddd8a212aedb1ffa15763dd52a388518c4550e615aed88dc3753c0f0c \
+ --hash=sha256:1c48472a6ba3b113452355b9af0a60da5c2ae60477f8feda8346f8fd48e3e87c \
+ --hash=sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70 \
+ --hash=sha256:269a5743a393c65db46a7bb982644c67ecba4b8d91b392403ad8a861ba6f495f \
+ --hash=sha256:26d168aac4aaec9a4394221240e8a5436b5634adc3cd1cdf637f6645cecbf181 \
+ --hash=sha256:29d1d350178e5225397e28ea1b7aca3648fcbab546d20e7475805437bfb0a130 \
+ --hash=sha256:2aad0e0baa04517741c9bb5b07586c642302e5fb3e75319cb62087bd0995ab19 \
+ --hash=sha256:3496fc835370da351d37cada4cf744039616a6db7d13c430035e901443a34daa \
+ --hash=sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429 \
+ --hash=sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126 \
+ --hash=sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4 \
+ --hash=sha256:44bb8ff420c1d19d91d79d8c3574b8954288bdff0273bf788954064d260d7ab0 \
+ --hash=sha256:4688c1e42968ba52e57d8670ad2306fe92e0169c6f3af0089be75bbac0c64a3b \
+ --hash=sha256:495ba7e49c2db22b046a53b469bbecea802efce200dffb69b93dd47397edc9b6 \
+ --hash=sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438 \
+ --hash=sha256:503fa6af7da9f4b5780bb7e4cbe0c639b010f12be85d02c99452825dd0feef3f \
+ --hash=sha256:56d027eace784738457437df7331965473f2c0da2c70e1a1f6fdbae5402e0389 \
+ --hash=sha256:5913a1177fc36e30fcf6dc868ce23b0453952c78c04c266d3149b3d39e1410d6 \
+ --hash=sha256:5b6ef7d9f9c38292df3690fe3e302b5b530999fa90014853dcd0d6902fb59f26 \
+ --hash=sha256:5cb1e18167792d7d21e21365d7650b72d5081ed476123ff7b8cac7f45189c0c7 \
+ --hash=sha256:61a7ee1f13ab913897dac7da44a73c6d44d48a4adff42a5701e3239791c96e14 \
+ --hash=sha256:622a231b08899c864eb87e85f81c75e7b9ce05b001e59bbfbf43d4a71f5f32b2 \
+ --hash=sha256:68715970f16b6e92c574c30747c95cf8cf62804569647386ff032195dc89a430 \
+ --hash=sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296 \
+ --hash=sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12 \
+ --hash=sha256:6d847b14f7ea89f6ad3c9e3901d1bc4835f6b390a9c71df999b0162d9bb1e20f \
+ --hash=sha256:76ffebb907bec09ff511bb3acc077695e2c32bc2142819491579a695f77ffd4d \
+ --hash=sha256:7bbff90b63328013e1e8cb50650ae0b9bac54ffb4be6104378490193cd60f85a \
+ --hash=sha256:7cb81373984cc0e4682f31bc3d6be9026006d96eecd07ea49aafb06897746452 \
+ --hash=sha256:7ee83d3e3a024a9618e5be64648d6d11c37047ac48adff25f12fa4226cf23d1c \
+ --hash=sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761 \
+ --hash=sha256:85f7912459c67eaab2fb854ed2bc1cc25772b300545fe7ed2dc03954da638649 \
+ --hash=sha256:87fdccbb6bb589095f413b1e05734ba492c962b4a45a13ff3408fa44ffe6479b \
+ --hash=sha256:88c63a1b55f352b02c6ffd24b15ead9fc0e8bf781dbe070213039324922a2eea \
+ --hash=sha256:8a674ac10e0a87b683f4fa2b6fa41090edfd686a6524bd8dedbd6138b309175c \
+ --hash=sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a \
+ --hash=sha256:9744a863b489c79a73aba014df554b0e7a0fc44ef3f8a0ef2a52919c7d155031 \
+ --hash=sha256:9749a124280a0ada4187a6cfd1ffd35c350fb3af79c706589d98e088c5044267 \
+ --hash=sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5 \
+ --hash=sha256:9bf919756d25e4114ace16a8ce91eb340eb57a08e2c6950c3cebcbe3dff2a5e7 \
+ --hash=sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d \
+ --hash=sha256:9ed4c92a0665002ff8ea852353aeb60d9141eb04109e88928026d3c8a9e5433c \
+ --hash=sha256:a72661af47119a80d82fa583b554095308d6a4c356b2a554fdc2799bc19f2a43 \
+ --hash=sha256:afde17ae04d90fbe53afb628f7f2d4ca022797aa093e809de5c3cf276f61bbfa \
+ --hash=sha256:b336c5e9cf03c7be40c47b5fd694c43c9f1358a80ba384a21969e0b4e66a9b17 \
+ --hash=sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb \
+ --hash=sha256:b83bb06a0192cccf1eb8d0a28672a1b79c74c3a8a5f2619625aeb6f28b3a82bb \
+ --hash=sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b \
+ --hash=sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4 \
+ --hash=sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3 \
+ --hash=sha256:db844eb158a87ccab83e868a762ea8024ae27337fc7ddcbfcddd157f841fdfe7 \
+ --hash=sha256:defed7ea5f218a9f2336301e6fd379f55c655bea65ba2476346340a0ce6f74a1 \
+ --hash=sha256:e16eb9541f3dd1a3e92b89005e37b1257b157b7256df0e36bd7b33b50be73bcb \
+ --hash=sha256:e23281b9a08ec338469268f98f194658abfb13658ee98e2b7f85ee9dd06caa91 \
+ --hash=sha256:e2d9e1cbc1b25e22000328702b014227737756f4b5bf5c485ac1d8091ada078b \
+ --hash=sha256:e48f4234f2469ed012a98f4b7874e7f7e173c167bed4934912a29e03167cf6b1 \
+ --hash=sha256:e4c4e92c14a57c9bd4cb4be678c25369bf7a092d55fd0866f759e425b9660806 \
+ --hash=sha256:ec1947eabbaf8e0531e8e899fc1d9876c179fc518989461f5d24e2223395a9e3 \
+ --hash=sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1
+ # via -r requirements.in
+selenium==3.141.0 \
+ --hash=sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c \
+ --hash=sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d
+ # via -r requirements.in
+urllib3==1.26.12 \
+ --hash=sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e \
+ --hash=sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997
+ # via selenium
diff --git a/docker/precommit_hooks/Dockerfile b/docker/precommit_hooks/Dockerfile
index 19630fb..f263390 100644
--- a/docker/precommit_hooks/Dockerfile
+++ b/docker/precommit_hooks/Dockerfile
@@ -24,17 +24,18 @@
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
&& rm -rf /var/lib/{apt,dpkg,cache,log}
-RUN pip3 install "pre-commit<3" "cpplint<2" "yapf<1" "pylint<3"
+COPY requirements.txt /opt/requirements.txt
+RUN pip3 install --require-hashes --no-deps -r /opt/requirements.txt
# === Get GN via CIPD
-ARG GN_SHA256SUM="1291d4cf9729b6615c621139be4e9c8bb49b5cc80330e7a9e3e83c583d683f71 /usr/local/bin/gn"
-ARG GN_HASH=vC0rxqiqGTD3ls9KJHrgJoWP2OBiPk_QEO_xbDItKYoC
+ARG GN_SHA256SUM="af7b2dcb3905bca56655e12131b365f1cba8e159db80d2022330c4f522fab2ef /tmp/gn.zip"
+ARG GN_HASH=r3styzkFvKVmVeEhMbNl8cuo4VnbgNICIzDE9SL6su8C
RUN curl --location --silent --output /tmp/gn.zip \
"https://chrome-infra-packages.appspot.com/dl/gn/gn/linux-amd64/+/${GN_HASH}" \
+ && echo ${GN_SHA256SUM} | sha256sum --check \
&& unzip /tmp/gn.zip -d /usr/local/bin \
- && rm /tmp/gn.zip \
- && echo ${GN_SHA256SUM} | sha256sum --check
+ && rm /tmp/gn.zip
WORKDIR /code
diff --git a/docker/precommit_hooks/requirements.in b/docker/precommit_hooks/requirements.in
new file mode 100644
index 0000000..daa21ff
--- /dev/null
+++ b/docker/precommit_hooks/requirements.in
@@ -0,0 +1,4 @@
+pre-commit<3
+cpplint<2
+yapf<1
+pylint<3
diff --git a/docker/precommit_hooks/requirements.txt b/docker/precommit_hooks/requirements.txt
new file mode 100644
index 0000000..cc066e8
--- /dev/null
+++ b/docker/precommit_hooks/requirements.txt
@@ -0,0 +1,228 @@
+#
+# This file is autogenerated by pip-compile with python 3.10
+# To update, run:
+#
+# pip-compile --allow-unsafe --generate-hashes requirements.in
+#
+astroid==2.11.7 \
+ --hash=sha256:86b0a340a512c65abf4368b80252754cda17c02cdbbd3f587dddf98112233e7b \
+ --hash=sha256:bb24615c77f4837c707669d16907331374ae8a964650a66999da3f5ca68dc946
+ # via pylint
+cfgv==3.3.1 \
+ --hash=sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426 \
+ --hash=sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736
+ # via pre-commit
+cpplint==1.6.1 \
+ --hash=sha256:00ddc86d6e4de2a9dcfa272402dcbe21593363a93b7c475bc391e335062f34b1 \
+ --hash=sha256:d430ce8f67afc1839340e60daa89e90de08b874bc27149833077bba726dfc13a
+ # via -r requirements.in
+dill==0.3.5.1 \
+ --hash=sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302 \
+ --hash=sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86
+ # via pylint
+distlib==0.3.5 \
+ --hash=sha256:a7f75737c70be3b25e2bee06288cec4e4c221de18455b2dd037fe2a795cab2fe \
+ --hash=sha256:b710088c59f06338ca514800ad795a132da19fda270e3ce4affc74abf955a26c
+ # via virtualenv
+filelock==3.8.0 \
+ --hash=sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc \
+ --hash=sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4
+ # via virtualenv
+identify==2.5.3 \
+ --hash=sha256:25851c8c1370effb22aaa3c987b30449e9ff0cece408f810ae6ce408fdd20893 \
+ --hash=sha256:887e7b91a1be152b0d46bbf072130235a8117392b9f1828446079a816a05ef44
+ # via pre-commit
+isort==5.10.1 \
+ --hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \
+ --hash=sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951
+ # via pylint
+lazy-object-proxy==1.7.1 \
+ --hash=sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7 \
+ --hash=sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a \
+ --hash=sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c \
+ --hash=sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc \
+ --hash=sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f \
+ --hash=sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09 \
+ --hash=sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442 \
+ --hash=sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e \
+ --hash=sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029 \
+ --hash=sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61 \
+ --hash=sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb \
+ --hash=sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0 \
+ --hash=sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35 \
+ --hash=sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42 \
+ --hash=sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1 \
+ --hash=sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad \
+ --hash=sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443 \
+ --hash=sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd \
+ --hash=sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9 \
+ --hash=sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148 \
+ --hash=sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38 \
+ --hash=sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55 \
+ --hash=sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36 \
+ --hash=sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a \
+ --hash=sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b \
+ --hash=sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44 \
+ --hash=sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6 \
+ --hash=sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69 \
+ --hash=sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4 \
+ --hash=sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84 \
+ --hash=sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de \
+ --hash=sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28 \
+ --hash=sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c \
+ --hash=sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1 \
+ --hash=sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8 \
+ --hash=sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b \
+ --hash=sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb
+ # via astroid
+mccabe==0.7.0 \
+ --hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \
+ --hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e
+ # via pylint
+nodeenv==1.7.0 \
+ --hash=sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e \
+ --hash=sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b
+ # via pre-commit
+platformdirs==2.5.2 \
+ --hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \
+ --hash=sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19
+ # via
+ # pylint
+ # virtualenv
+pre-commit==2.20.0 \
+ --hash=sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7 \
+ --hash=sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959
+ # via -r requirements.in
+pylint==2.14.5 \
+ --hash=sha256:487ce2192eee48211269a0e976421f334cf94de1806ca9d0a99449adcdf0285e \
+ --hash=sha256:fabe30000de7d07636d2e82c9a518ad5ad7908590fe135ace169b44839c15f90
+ # via -r requirements.in
+pyyaml==6.0 \
+ --hash=sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293 \
+ --hash=sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b \
+ --hash=sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57 \
+ --hash=sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b \
+ --hash=sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4 \
+ --hash=sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07 \
+ --hash=sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba \
+ --hash=sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9 \
+ --hash=sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287 \
+ --hash=sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513 \
+ --hash=sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0 \
+ --hash=sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0 \
+ --hash=sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92 \
+ --hash=sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f \
+ --hash=sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 \
+ --hash=sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc \
+ --hash=sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c \
+ --hash=sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86 \
+ --hash=sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4 \
+ --hash=sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c \
+ --hash=sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34 \
+ --hash=sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b \
+ --hash=sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c \
+ --hash=sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb \
+ --hash=sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737 \
+ --hash=sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3 \
+ --hash=sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d \
+ --hash=sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53 \
+ --hash=sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78 \
+ --hash=sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803 \
+ --hash=sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a \
+ --hash=sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174 \
+ --hash=sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5
+ # via pre-commit
+toml==0.10.2 \
+ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \
+ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f
+ # via pre-commit
+tomli==2.0.1 \
+ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
+ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
+ # via pylint
+tomlkit==0.11.4 \
+ --hash=sha256:25d4e2e446c453be6360c67ddfb88838cfc42026322770ba13d1fbd403a93a5c \
+ --hash=sha256:3235a9010fae54323e727c3ac06fb720752fe6635b3426e379daec60fbd44a83
+ # via pylint
+virtualenv==20.16.3 \
+ --hash=sha256:4193b7bc8a6cd23e4eb251ac64f29b4398ab2c233531e66e40b19a6b7b0d30c1 \
+ --hash=sha256:d86ea0bb50e06252d79e6c241507cb904fcd66090c3271381372d6221a3970f9
+ # via pre-commit
+wrapt==1.14.1 \
+ --hash=sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3 \
+ --hash=sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b \
+ --hash=sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4 \
+ --hash=sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2 \
+ --hash=sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656 \
+ --hash=sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3 \
+ --hash=sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff \
+ --hash=sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310 \
+ --hash=sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a \
+ --hash=sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57 \
+ --hash=sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069 \
+ --hash=sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383 \
+ --hash=sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe \
+ --hash=sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87 \
+ --hash=sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d \
+ --hash=sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b \
+ --hash=sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907 \
+ --hash=sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f \
+ --hash=sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0 \
+ --hash=sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28 \
+ --hash=sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1 \
+ --hash=sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853 \
+ --hash=sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc \
+ --hash=sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3 \
+ --hash=sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3 \
+ --hash=sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164 \
+ --hash=sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1 \
+ --hash=sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c \
+ --hash=sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1 \
+ --hash=sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7 \
+ --hash=sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1 \
+ --hash=sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320 \
+ --hash=sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed \
+ --hash=sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1 \
+ --hash=sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248 \
+ --hash=sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c \
+ --hash=sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456 \
+ --hash=sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77 \
+ --hash=sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef \
+ --hash=sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1 \
+ --hash=sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7 \
+ --hash=sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86 \
+ --hash=sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4 \
+ --hash=sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d \
+ --hash=sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d \
+ --hash=sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8 \
+ --hash=sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5 \
+ --hash=sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471 \
+ --hash=sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00 \
+ --hash=sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68 \
+ --hash=sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3 \
+ --hash=sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d \
+ --hash=sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735 \
+ --hash=sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d \
+ --hash=sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569 \
+ --hash=sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7 \
+ --hash=sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59 \
+ --hash=sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5 \
+ --hash=sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb \
+ --hash=sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b \
+ --hash=sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f \
+ --hash=sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462 \
+ --hash=sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015 \
+ --hash=sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af
+ # via astroid
+yapf==0.32.0 \
+ --hash=sha256:8fea849025584e486fd06d6ba2bed717f396080fd3cc236ba10cb97c4c51cf32 \
+ --hash=sha256:a3f5085d37ef7e3e004c4ba9f9b3e40c54ff1901cd111f05145ae313a7c67d1b
+ # via -r requirements.in
+
+# The following packages are considered to be unsafe in a requirements file:
+setuptools==65.3.0 \
+ --hash=sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82 \
+ --hash=sha256:7732871f4f7fa58fb6bdcaeadb0161b2bd046c85905dbaa066bdcbcc81953b57
+ # via
+ # astroid
+ # nodeenv
diff --git a/docker/pytest/Dockerfile b/docker/pytest/Dockerfile
index 962201e..d78f0c1 100644
--- a/docker/pytest/Dockerfile
+++ b/docker/pytest/Dockerfile
@@ -15,7 +15,7 @@
FROM cobalt-base
COPY requirements.txt /opt/requirements.txt
-RUN pip3 install -r /opt/requirements.txt
+RUN pip3 install --require-hashes --no-deps -r /opt/requirements.txt
WORKDIR /code
diff --git a/docker/pytest/requirements.txt b/docker/pytest/requirements.txt
index a93c321..bdb88a3 100644
--- a/docker/pytest/requirements.txt
+++ b/docker/pytest/requirements.txt
@@ -1,40 +1,130 @@
#
-# This file is autogenerated by pip-compile
+# This file is autogenerated by pip-compile with python 3.10
# To update, run:
#
-# pip-compile --output-file=docker/pytest/requirements.txt docker/pytest/requirements.in
+# pip-compile --generate-hashes requirements.in
#
-attrs==21.4.0
+attrs==21.4.0 \
+ --hash=sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4 \
+ --hash=sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd
# via
# jsonschema
# pytest
-certifi==2021.10.8
+certifi==2021.10.8 \
+ --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 \
+ --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569
# via requests
-chardet==4.0.0
+chardet==4.0.0 \
+ --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa \
+ --hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5
# via requests
-coverage==6.3.2
- # via -r docker/pytest/requirements.in
-idna==2.10
+coverage==6.3.2 \
+ --hash=sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9 \
+ --hash=sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d \
+ --hash=sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf \
+ --hash=sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7 \
+ --hash=sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6 \
+ --hash=sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4 \
+ --hash=sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059 \
+ --hash=sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39 \
+ --hash=sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536 \
+ --hash=sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac \
+ --hash=sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c \
+ --hash=sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903 \
+ --hash=sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d \
+ --hash=sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05 \
+ --hash=sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684 \
+ --hash=sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1 \
+ --hash=sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f \
+ --hash=sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7 \
+ --hash=sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca \
+ --hash=sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad \
+ --hash=sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca \
+ --hash=sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d \
+ --hash=sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92 \
+ --hash=sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4 \
+ --hash=sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf \
+ --hash=sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6 \
+ --hash=sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1 \
+ --hash=sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4 \
+ --hash=sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359 \
+ --hash=sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3 \
+ --hash=sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620 \
+ --hash=sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512 \
+ --hash=sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69 \
+ --hash=sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2 \
+ --hash=sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518 \
+ --hash=sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0 \
+ --hash=sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa \
+ --hash=sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4 \
+ --hash=sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e \
+ --hash=sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1 \
+ --hash=sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2
+ # via -r requirements.in
+idna==2.10 \
+ --hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6 \
+ --hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0
# via requests
-iniconfig==1.1.1
+iniconfig==1.1.1 \
+ --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \
+ --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32
# via pytest
-jsonschema==4.4.0
- # via -r docker/pytest/requirements.in
-packaging==21.3
+jsonschema==4.4.0 \
+ --hash=sha256:636694eb41b3535ed608fe04129f26542b59ed99808b4f688aa32dcf55317a83 \
+ --hash=sha256:77281a1f71684953ee8b3d488371b162419767973789272434bbc3f29d9c8823
+ # via -r requirements.in
+packaging==21.3 \
+ --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \
+ --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522
# via pytest
-pluggy==1.0.0
+pluggy==1.0.0 \
+ --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \
+ --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3
# via pytest
-py==1.11.0
+py==1.11.0 \
+ --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \
+ --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378
# via pytest
-pyparsing==3.0.7
+pyparsing==3.0.7 \
+ --hash=sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea \
+ --hash=sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484
# via packaging
-pyrsistent==0.18.1
+pyrsistent==0.18.1 \
+ --hash=sha256:0e3e1fcc45199df76053026a51cc59ab2ea3fc7c094c6627e93b7b44cdae2c8c \
+ --hash=sha256:1b34eedd6812bf4d33814fca1b66005805d3640ce53140ab8bbb1e2651b0d9bc \
+ --hash=sha256:4ed6784ceac462a7d6fcb7e9b663e93b9a6fb373b7f43594f9ff68875788e01e \
+ --hash=sha256:5d45866ececf4a5fff8742c25722da6d4c9e180daa7b405dc0a2a2790d668c26 \
+ --hash=sha256:636ce2dc235046ccd3d8c56a7ad54e99d5c1cd0ef07d9ae847306c91d11b5fec \
+ --hash=sha256:6455fc599df93d1f60e1c5c4fe471499f08d190d57eca040c0ea182301321286 \
+ --hash=sha256:6bc66318fb7ee012071b2792024564973ecc80e9522842eb4e17743604b5e045 \
+ --hash=sha256:7bfe2388663fd18bd8ce7db2c91c7400bf3e1a9e8bd7d63bf7e77d39051b85ec \
+ --hash=sha256:7ec335fc998faa4febe75cc5268a9eac0478b3f681602c1f27befaf2a1abe1d8 \
+ --hash=sha256:914474c9f1d93080338ace89cb2acee74f4f666fb0424896fcfb8d86058bf17c \
+ --hash=sha256:b568f35ad53a7b07ed9b1b2bae09eb15cdd671a5ba5d2c66caee40dbf91c68ca \
+ --hash=sha256:cdfd2c361b8a8e5d9499b9082b501c452ade8bbf42aef97ea04854f4a3f43b22 \
+ --hash=sha256:d1b96547410f76078eaf66d282ddca2e4baae8964364abb4f4dcdde855cd123a \
+ --hash=sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96 \
+ --hash=sha256:d7a096646eab884bf8bed965bad63ea327e0d0c38989fc83c5ea7b8a87037bfc \
+ --hash=sha256:df46c854f490f81210870e509818b729db4488e1f30f2a1ce1698b2295a878d1 \
+ --hash=sha256:e24a828f57e0c337c8d8bb9f6b12f09dfdf0273da25fda9e314f0b684b415a07 \
+ --hash=sha256:e4f3149fd5eb9b285d6bfb54d2e5173f6a116fe19172686797c056672689daf6 \
+ --hash=sha256:e92a52c166426efbe0d1ec1332ee9119b6d32fc1f0bbfd55d5c1088070e7fc1b \
+ --hash=sha256:f87cc2863ef33c709e237d4b5f4502a62a00fab450c9e020892e8e2ede5847f5 \
+ --hash=sha256:fd8da6d0124efa2f67d86fa70c851022f87c98e205f0594e1fae044e7119a5a6
# via jsonschema
-pytest==7.1.1
- # via -r docker/pytest/requirements.in
-requests==2.25.1
- # via -r docker/pytest/requirements.in
-tomli==2.0.1
+pytest==7.1.1 \
+ --hash=sha256:841132caef6b1ad17a9afde46dc4f6cfa59a05f9555aae5151f73bdf2820ca63 \
+ --hash=sha256:92f723789a8fdd7180b6b06483874feca4c48a5c76968e03bb3e7f806a1869ea
+ # via -r requirements.in
+requests==2.25.1 \
+ --hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804 \
+ --hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e
+ # via -r requirements.in
+tomli==2.0.1 \
+ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
+ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
# via pytest
-urllib3==1.26.9
+urllib3==1.26.9 \
+ --hash=sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14 \
+ --hash=sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e
# via requests
diff --git a/docker/windows/base/build/Dockerfile b/docker/windows/base/build/Dockerfile
index ec4ebaa..19aa9b6 100644
--- a/docker/windows/base/build/Dockerfile
+++ b/docker/windows/base/build/Dockerfile
@@ -45,8 +45,9 @@
Copy-Item C:\Python3\python.exe C:\Python3\python3.exe
# Install python3 packages via PIP and set various configurations.
+COPY ./requirements.txt /requirements.txt
RUN mkdir C:\pip-cache;`
- python3 -m pip install six --cache-dir C:\pip-cache;`
+ python3 -m pip install --require-hashes --no-deps -r /requirements.txt --cache-dir C:\pip-cache;`
C:\fast-win-rmdir.cmd C:\pip-cache;`
# Configure git global settings.
git config --global core.autocrlf false;`
diff --git a/docker/windows/base/build/requirements.in b/docker/windows/base/build/requirements.in
new file mode 100644
index 0000000..ffe2fce
--- /dev/null
+++ b/docker/windows/base/build/requirements.in
@@ -0,0 +1 @@
+six
diff --git a/docker/windows/base/build/requirements.txt b/docker/windows/base/build/requirements.txt
new file mode 100644
index 0000000..1fc47bb
--- /dev/null
+++ b/docker/windows/base/build/requirements.txt
@@ -0,0 +1,10 @@
+#
+# This file is autogenerated by pip-compile with python 3.10
+# To update, run:
+#
+# pip-compile --generate-hashes requirements.in
+#
+six==1.16.0 \
+ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
+ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
+ # via -r requirements.in
diff --git a/glimp/gles/context.cc b/glimp/gles/context.cc
index 5a5eb59..ac0b53b 100644
--- a/glimp/gles/context.cc
+++ b/glimp/gles/context.cc
@@ -1,5 +1,5 @@
/*
- * Copyright 2015 Google Inc. All Rights Reserved.
+ * Copyright 2015 The Cobalt Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -37,6 +37,7 @@
namespace {
+std::atomic_int s_context_id_counter_(0);
SbOnceControl s_tls_current_context_key_once_control = SB_ONCE_INITIALIZER;
SbThreadLocalKey s_tls_current_context_key = kSbThreadLocalKeyInvalid;
@@ -54,6 +55,7 @@
Context::Context(nb::scoped_ptr<ContextImpl> context_impl,
Context* share_context)
: impl_(context_impl.Pass()),
+ context_id_(s_context_id_counter_++),
current_thread_(kSbThreadInvalid),
has_been_current_(false),
active_texture_(GL_TEXTURE0),
@@ -974,7 +976,7 @@
SB_DCHECK(access & GL_MAP_INVALIDATE_BUFFER_BIT)
<< "glimp requires the GL_MAP_INVALIDATE_BUFFER_BIT flag to be set.";
SB_DCHECK(access & GL_MAP_UNSYNCHRONIZED_BIT)
- << "glimp requres the GL_MAP_UNSYNCHRONIZED_BIT flag to be set.";
+ << "glimp requires the GL_MAP_UNSYNCHRONIZED_BIT flag to be set.";
SB_DCHECK(!(access & GL_MAP_FLUSH_EXPLICIT_BIT))
<< "glimp does not support the GL_MAP_FLUSH_EXPLICIT_BIT flag.";
SB_DCHECK(length == bound_buffer->size_in_bytes())
diff --git a/glimp/gles/context.h b/glimp/gles/context.h
index 5444618..66e9ed7 100644
--- a/glimp/gles/context.h
+++ b/glimp/gles/context.h
@@ -1,5 +1,5 @@
/*
- * Copyright 2015 Google Inc. All Rights Reserved.
+ * Copyright 2015 The Cobalt Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
#include <GLES3/gl3.h>
+#include <atomic>
#include <map>
#include <set>
#include <string>
@@ -58,6 +59,9 @@
// Releases the current thread's current context.
static void ReleaseTLSCurrentContext();
+ // Returns the unique id of the context instance.
+ int context_id() const { return context_id_; }
+
egl::Surface* draw_surface() {
return default_draw_framebuffer_->color_attachment_surface();
}
@@ -302,6 +306,10 @@
// A reference to the platform-specific implementation aspects of the context.
nb::scoped_ptr<ContextImpl> impl_;
+ // The unique id of context instance. It might be queried from different
+ // threads.
+ std::atomic_int context_id_;
+
// The thread that currently holds this context as its current context.
SbThread current_thread_;
diff --git a/net/base/load_timing_info.h b/net/base/load_timing_info.h
index aacaac0..bdfd91f 100644
--- a/net/base/load_timing_info.h
+++ b/net/base/load_timing_info.h
@@ -163,6 +163,7 @@
#if defined(STARBOARD)
uint64_t encoded_body_size;
+ base::TimeTicks service_worker_start_time;
#endif // defined(STARBOARD)
};
diff --git a/net/disk_cache/cobalt/resource_type.h b/net/disk_cache/cobalt/resource_type.h
index 6397a9b..f291033 100644
--- a/net/disk_cache/cobalt/resource_type.h
+++ b/net/disk_cache/cobalt/resource_type.h
@@ -30,7 +30,8 @@
kUncompiledScript = 6,
kCompiledScript = 7,
kCacheApi = 8,
- kTypeCount = 9
+ kServiceWorkerScript = 9,
+ kTypeCount = 10,
};
struct ResourceTypeMetadata {
@@ -43,10 +44,10 @@
// persisted values saved in settings.json.
static ResourceTypeMetadata kTypeMetadata[] = {
{"other", kInitialBytes}, {"html", 2 * 1024 * 1024},
- {"css", 1 * 1024 * 1024}, {"image", kInitialBytes},
+ {"css", 1 * 1024 * 1024}, {"image", 0},
{"font", kInitialBytes}, {"splash", 2 * 1024 * 1024},
{"uncompiled_js", kInitialBytes}, {"compiled_js", kInitialBytes},
- {"cache_api", kInitialBytes},
+ {"cache_api", kInitialBytes}, {"service_worker_js", kInitialBytes},
};
} // namespace disk_cache
diff --git a/net/spdy/spdy_network_transaction_unittest.cc b/net/spdy/spdy_network_transaction_unittest.cc
index 6353a46..e58d681 100644
--- a/net/spdy/spdy_network_transaction_unittest.cc
+++ b/net/spdy/spdy_network_transaction_unittest.cc
@@ -4676,7 +4676,7 @@
}
// Verify the case where we buffer data and cancel the transaction.
-TEST_F(SpdyNetworkTransactionTest, BufferedCancelled) {
+TEST_F(SpdyNetworkTransactionTest, DISABLED_BufferedCancelled) {
spdy::SpdySerializedFrame req(
spdy_util_.ConstructSpdyGet(nullptr, 0, 1, LOWEST));
spdy::SpdySerializedFrame rst(
diff --git a/precommit_hooks/check_bug_in_description_wrapper.py b/precommit_hooks/check_bug_in_description_wrapper.py
index 706a127..c37ea47 100644
--- a/precommit_hooks/check_bug_in_description_wrapper.py
+++ b/precommit_hooks/check_bug_in_description_wrapper.py
@@ -25,4 +25,8 @@
if __name__ == '__main__':
from_ref = os.environ.get('PRE_COMMIT_FROM_REF')
to_ref = os.environ.get('PRE_COMMIT_TO_REF')
+ if not from_ref or not to_ref:
+ print('Invalid from ref or to ref, exiting...')
+ sys.exit(0)
+
sys.exit(CheckForBugInCommitDescription(from_ref, to_ref))
diff --git a/precommit_hooks/check_copyright_year.py b/precommit_hooks/check_copyright_year.py
old mode 100755
new mode 100644
index e682ac4..e6d90ea
--- a/precommit_hooks/check_copyright_year.py
+++ b/precommit_hooks/check_copyright_year.py
@@ -35,8 +35,10 @@
def GetCreatedFiles() -> List[str]:
- from_ref = os.environ['PRE_COMMIT_FROM_REF']
- to_ref = os.environ['PRE_COMMIT_TO_REF']
+ from_ref = os.environ.get('PRE_COMMIT_FROM_REF')
+ to_ref = os.environ.get('PRE_COMMIT_TO_REF')
+ if not from_ref or not to_ref:
+ return []
new_files = subprocess.check_output(
['git', 'diff', from_ref, to_ref, '--name-only',
diff --git a/precommit_hooks/clang_format_wrapper.py b/precommit_hooks/clang_format_wrapper.py
old mode 100755
new mode 100644
diff --git a/precommit_hooks/gcheckstyle_wrapper.py b/precommit_hooks/gcheckstyle_wrapper.py
old mode 100755
new mode 100644
diff --git a/precommit_hooks/google_java_format_wrapper.py b/precommit_hooks/google_java_format_wrapper.py
old mode 100755
new mode 100644
diff --git a/precommit_hooks/sync_keyboxes_wrapper.py b/precommit_hooks/sync_keyboxes_wrapper.py
old mode 100755
new mode 100644
index 002c012..dc4890f
--- a/precommit_hooks/sync_keyboxes_wrapper.py
+++ b/precommit_hooks/sync_keyboxes_wrapper.py
@@ -18,7 +18,7 @@
import sys
try:
- import internal.sync_keyboxes as sync_keyboxes
+ from internal import sync_keyboxes
except ImportError:
print('sync_keyboxes.py not found, skipping.')
sys.exit(0)
diff --git a/starboard/CHANGELOG.md b/starboard/CHANGELOG.md
index fddc2c3..69ca601 100644
--- a/starboard/CHANGELOG.md
+++ b/starboard/CHANGELOG.md
@@ -15,9 +15,6 @@
[configuration.h](configuration.h).
## Version 14
-### Require kSbSystemPathStorageDirectory on all platforms.
-Path to directory for permanent persistent storage.
-
### Add MP3, FLAC, and PCM values to SbMediaAudioCodec.
This makes it possible to support these codecs in the future.
diff --git a/starboard/android/apk/app/build.gradle b/starboard/android/apk/app/build.gradle
index 5622927..c88e2d3 100644
--- a/starboard/android/apk/app/build.gradle
+++ b/starboard/android/apk/app/build.gradle
@@ -46,6 +46,11 @@
buildToolsVersion '31.0.0'
ndkVersion NDK_VERSION
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
aaptOptions {
// The following pattern matches the default aapt pattern with the
// exception that it is missing the blanket ignore wildcard (!.*) that
@@ -175,7 +180,14 @@
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
+ // GameActivity dependency. Follow the steps in
+ // //starboard/android/apk/app/src/main/java/dev/cobalt/libraries/game_activity/README.md
+ // if you want to update to a new version.
+ implementation fileTree(dir: 'src/main/java/dev/cobalt/libraries/game_activity',
+ include: ['games-activity*.aar'])
implementation 'androidx.annotation:annotation:1.1.0'
+ implementation 'androidx.appcompat:appcompat:1.4.2'
+ implementation 'androidx.core:core:1.8.0'
implementation 'androidx.leanback:leanback:1.0.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0'
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/AdvertisingId.java b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/AdvertisingId.java
index 3ca25e4..bde7982 100644
--- a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/AdvertisingId.java
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/AdvertisingId.java
@@ -46,18 +46,15 @@
return;
}
singleThreadExecutor.execute(
- new Runnable() {
- @Override
- public void run() {
- try {
- advertisingIdInfo = AdvertisingIdClient.getAdvertisingIdInfo(context);
- lastRefreshed = System.currentTimeMillis();
- Log.i(TAG, "Successfully retrieved Advertising ID (IfA).");
- } catch (IOException
- | GooglePlayServicesNotAvailableException
- | GooglePlayServicesRepairableException e) {
- Log.e(TAG, "Failed to retrieve Advertising ID (IfA).");
- }
+ () -> {
+ try {
+ advertisingIdInfo = AdvertisingIdClient.getAdvertisingIdInfo(context);
+ lastRefreshed = System.currentTimeMillis();
+ Log.i(TAG, "Successfully retrieved Advertising ID (IfA).");
+ } catch (IOException
+ | GooglePlayServicesNotAvailableException
+ | GooglePlayServicesRepairableException e) {
+ Log.e(TAG, "Failed to retrieve Advertising ID (IfA).");
}
});
}
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java
index 4b05f41..eae0f49 100644
--- a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java
@@ -16,7 +16,7 @@
import static dev.cobalt.util.Log.TAG;
-import android.app.NativeActivity;
+import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
@@ -29,6 +29,8 @@
import android.view.ViewGroup.LayoutParams;
import android.view.ViewParent;
import android.widget.FrameLayout;
+import androidx.annotation.CallSuper;
+import com.google.androidgamesdk.GameActivity;
import dev.cobalt.media.AudioOutputManager;
import dev.cobalt.media.MediaCodecUtil;
import dev.cobalt.media.VideoSurfaceView;
@@ -41,7 +43,7 @@
import java.util.Locale;
/** Native activity that has the required JNI methods called by the Starboard implementation. */
-public abstract class CobaltActivity extends NativeActivity {
+public abstract class CobaltActivity extends GameActivity {
// A place to put args while debugging so they're used even when starting from the launcher.
// This should always be empty in submitted code.
@@ -72,6 +74,8 @@
private static native void nativeLowMemoryEvent();
+ protected View mContentView = null;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
// Record the application start timestamp.
@@ -110,6 +114,21 @@
}
/**
+ * Creates an empty View for the launching activity, and prevent GameActivity from creating the
+ * default SurfaceView.
+ */
+ @Override
+ protected void onCreateSurfaceView() {
+ mSurfaceView = null;
+
+ getWindow().takeSurface(this);
+
+ mContentView = new View(this);
+ setContentView(mContentView);
+ mContentView.requestFocus();
+ }
+
+ /**
* Instantiates the StarboardBridge. Apps not supporting sign-in should inject an instance of
* NoopUserAuthorizer. Apps may subclass StarboardBridge if they need to override anything.
*/
@@ -271,8 +290,10 @@
return StarboardBridge.isReleaseBuild();
}
+ @CallSuper
@Override
protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
getStarboardBridge().handleDeepLink(getIntentUrlAsString(intent));
}
@@ -290,6 +311,7 @@
getStarboardBridge().onActivityResult(requestCode, resultCode, data);
}
+ @SuppressLint("MissingSuperCall")
@Override
public void onRequestPermissionsResult(
int requestCode, String[] permissions, int[] grantResults) {
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java
index f956eff..a950322 100644
--- a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java
@@ -808,4 +808,13 @@
}
return 0;
}
+
+ @SuppressWarnings("unused")
+ @UsedByNative
+ void reportFullyDrawn() {
+ Activity activity = activityHolder.get();
+ if (activity != null) {
+ activity.reportFullyDrawn();
+ }
+ }
}
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/libraries/game_activity/README.md b/starboard/android/apk/app/src/main/java/dev/cobalt/libraries/game_activity/README.md
new file mode 100644
index 0000000..99d8e12
--- /dev/null
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/libraries/game_activity/README.md
@@ -0,0 +1,25 @@
+# Android GameActivity
+
+The library in this directory is copied from the AndroidX GameActivity
+release package.
+
+To learn more about GameActivity, refer to [the official GameActivity
+documentation](https://d.android.com/games/agdk/game-activity).
+
+The library in this directory is used by the Android in
+//starboard/android/apk/app/build.gradle and also the native files are
+automatically extracted and used in //starboard/android/shared/BUILD.gn.
+
+## Updating instructions
+
+To update GameActivity to the latest version, do the following:
+
+1. Download the .aar file from Google Maven at
+ https://maven.google.com/web/index.html#androidx.games:games-activity
+ into this directory.
+
+1. Update `game_activity_aar_file` in //starboard/android/shared/BUILD.gn
+ to reflect the new .aar filename.
+
+1. Delete the old .aar file -- there should only be a single .aar in this
+ directory.
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/libraries/game_activity/games-activity-1.2.2.aar b/starboard/android/apk/app/src/main/java/dev/cobalt/libraries/game_activity/games-activity-1.2.2.aar
new file mode 100644
index 0000000..3b4cb1f
--- /dev/null
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/libraries/game_activity/games-activity-1.2.2.aar
Binary files differ
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java
index 5050078..2b63cd8 100644
--- a/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioTrackBridge.java
@@ -157,8 +157,11 @@
}
// AudioTrack ctor can fail in multiple, platform specific ways, so do a thorough check
// before proceed.
- if (audioTrack != null && audioTrack.getState() == AudioTrack.STATE_INITIALIZED) {
- break;
+ if (audioTrack != null) {
+ if (audioTrack.getState() == AudioTrack.STATE_INITIALIZED) {
+ break;
+ }
+ audioTrack = null;
}
audioTrackBufferSize /= 2;
}
@@ -194,6 +197,7 @@
return audioTrack.setVolume(gain);
}
+ // TODO (b/262608024): Have this method return a boolean and return false on failure.
@SuppressWarnings("unused")
@UsedByNative
private void play() {
@@ -201,9 +205,14 @@
Log.e(TAG, "Unable to play with NULL audio track.");
return;
}
- audioTrack.play();
+ try {
+ audioTrack.play();
+ } catch (IllegalStateException e) {
+ Log.e(TAG, String.format("Unable to play audio track, error: %s", e.toString()));
+ }
}
+ // TODO (b/262608024): Have this method return a boolean and return false on failure.
@SuppressWarnings("unused")
@UsedByNative
private void pause() {
@@ -211,9 +220,14 @@
Log.e(TAG, "Unable to pause with NULL audio track.");
return;
}
- audioTrack.pause();
+ try {
+ audioTrack.pause();
+ } catch (IllegalStateException e) {
+ Log.e(TAG, String.format("Unable to pause audio track, error: %s", e.toString()));
+ }
}
+ // TODO (b/262608024): Have this method return a boolean and return false on failure.
@SuppressWarnings("unused")
@UsedByNative
private void stop() {
@@ -221,7 +235,11 @@
Log.e(TAG, "Unable to stop with NULL audio track.");
return;
}
- audioTrack.stop();
+ try {
+ audioTrack.stop();
+ } catch (IllegalStateException e) {
+ Log.e(TAG, String.format("Unable to stop audio track, error: %s", e.toString()));
+ }
}
@SuppressWarnings("unused")
diff --git a/starboard/android/apk/app/src/main/res/values/styles.xml b/starboard/android/apk/app/src/main/res/values/styles.xml
index ef05d47..0f64c0a 100644
--- a/starboard/android/apk/app/src/main/res/values/styles.xml
+++ b/starboard/android/apk/app/src/main/res/values/styles.xml
@@ -16,7 +16,7 @@
-->
<resources>
- <style name="CobaltTheme" parent="@style/android:Theme.Material.NoActionBar">
+ <style name="CobaltTheme" parent="Theme.AppCompat.NoActionBar">
<!-- Color of the transition animation when launching the app. -->
<item name="android:colorPrimary">@color/primary</item>
diff --git a/starboard/android/arm/platform_configuration/configuration.gni b/starboard/android/arm/platform_configuration/configuration.gni
index a4cf9ff..a77aa89 100644
--- a/starboard/android/arm/platform_configuration/configuration.gni
+++ b/starboard/android/arm/platform_configuration/configuration.gni
@@ -16,4 +16,8 @@
android_abi = "armeabi-v7a"
arm_version = 7
+arm_float_abi = "softfp"
+
+sb_evergreen_compatible_package = true
+
sabi_path = "//starboard/sabi/arm/softfp/sabi-v$sb_api_version.json"
diff --git a/starboard/android/shared/BUILD.gn b/starboard/android/shared/BUILD.gn
index 5efe78e..806a67b 100644
--- a/starboard/android/shared/BUILD.gn
+++ b/starboard/android/shared/BUILD.gn
@@ -17,6 +17,39 @@
import("//starboard/shared/starboard/player/buildfiles.gni")
import("//starboard/shared/starboard/player/player_tests.gni")
+##########################################################
+# Configuration to extract GameActivity native files.
+##########################################################
+
+game_activity_aar_file = "//starboard/android/apk/app/src/main/java/dev/cobalt/libraries/game_activity/games-activity-1.2.2.aar"
+
+game_activity_source_files = [
+ "$target_gen_dir/game_activity/prefab/modules/game-activity/include/game-activity/GameActivity.cpp",
+ "$target_gen_dir/game_activity/prefab/modules/game-activity/include/game-activity/GameActivity.h",
+ "$target_gen_dir/game_activity/prefab/modules/game-activity/include/game-text-input/gamecommon.h",
+ "$target_gen_dir/game_activity/prefab/modules/game-activity/include/game-text-input/gametextinput.cpp",
+ "$target_gen_dir/game_activity/prefab/modules/game-activity/include/game-text-input/gametextinput.h",
+]
+
+game_activity_include_dirs =
+ [ "$target_gen_dir/game_activity/prefab/modules/game-activity/include" ]
+
+action("game_activity_sources") {
+ script = "//starboard/tools/unzip_file.py"
+ sources = [ game_activity_aar_file ]
+ outputs = game_activity_source_files
+ args = [
+ "--zip_file",
+ rebase_path(game_activity_aar_file, root_build_dir),
+ "--output_dir",
+ rebase_path(target_gen_dir, root_build_dir) + "/game_activity",
+ ]
+}
+
+##########################################################
+# Configuration for overall Android Starboard Platform.
+##########################################################
+
static_library("starboard_platform") {
sources = [
"$target_gen_dir/ndk-sources/cpu-features.c",
@@ -409,6 +442,7 @@
"window_update_on_screen_keyboard_suggestions.cc",
]
+ sources += game_activity_source_files
sources += common_player_sources
sources -= [
@@ -419,9 +453,12 @@
"//starboard/shared/starboard/player/player_set_playback_rate.cc",
]
+ include_dirs = game_activity_include_dirs
+
configs += [ "//starboard/build/config:starboard_implementation" ]
public_deps = [
+ ":game_activity_sources",
":ndk_sources",
":starboard_base_symbolize",
"//starboard/common",
diff --git a/starboard/android/shared/android_main.cc b/starboard/android/shared/android_main.cc
index 0c04c79..755210d 100644
--- a/starboard/android/shared/android_main.cc
+++ b/starboard/android/shared/android_main.cc
@@ -12,12 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+#include "game-activity/GameActivity.h"
#include "starboard/android/shared/application_android.h"
#include "starboard/android/shared/jni_env_ext.h"
#include "starboard/android/shared/jni_utils.h"
#include "starboard/android/shared/log_internal.h"
#include "starboard/common/semaphore.h"
#include "starboard/common/string.h"
+#include "starboard/log.h"
#include "starboard/shared/starboard/command_line.h"
#include "starboard/thread.h"
@@ -84,7 +86,7 @@
// allow sending the first AndroidCommand after onCreate() returns.
g_app_running = true;
- // Signal ANativeActivity_onCreate() that it may proceed.
+ // Signal GameActivity_onCreate() that it may proceed.
app_created_semaphore->Put();
// Enter the Starboard run loop until stopped.
@@ -107,13 +109,13 @@
return NULL;
}
-void OnStart(ANativeActivity* activity) {
+void OnStart(GameActivity* activity) {
if (g_app_running) {
ApplicationAndroid::Get()->SendAndroidCommand(AndroidCommand::kStart);
}
}
-void OnResume(ANativeActivity* activity) {
+void OnResume(GameActivity* activity) {
if (g_app_running) {
// Stop the MediaPlaybackService if activity state transits from background
// to foreground. Note that the MediaPlaybackService may already have
@@ -124,7 +126,7 @@
}
}
-void OnPause(ANativeActivity* activity) {
+void OnPause(GameActivity* activity) {
if (g_app_running) {
// Start the MediaPlaybackService before activity state transits from
// foreground to background.
@@ -133,13 +135,28 @@
}
}
-void OnStop(ANativeActivity* activity) {
+void OnStop(GameActivity* activity) {
if (g_app_running) {
ApplicationAndroid::Get()->SendAndroidCommand(AndroidCommand::kStop);
}
}
-void OnWindowFocusChanged(ANativeActivity* activity, int focused) {
+bool OnTouchEvent(GameActivity* activity,
+ const GameActivityMotionEvent* event) {
+ if (g_app_running) {
+ return ApplicationAndroid::Get()->SendAndroidMotionEvent(event);
+ }
+ return false;
+}
+
+bool OnKey(GameActivity* activity, const GameActivityKeyEvent* event) {
+ if (g_app_running) {
+ return ApplicationAndroid::Get()->SendAndroidKeyEvent(event);
+ }
+ return false;
+}
+
+void OnWindowFocusChanged(GameActivity* activity, bool focused) {
if (g_app_running) {
ApplicationAndroid::Get()->SendAndroidCommand(
focused ? AndroidCommand::kWindowFocusGained
@@ -147,36 +164,22 @@
}
}
-void OnNativeWindowCreated(ANativeActivity* activity, ANativeWindow* window) {
+void OnNativeWindowCreated(GameActivity* activity, ANativeWindow* window) {
if (g_app_running) {
ApplicationAndroid::Get()->SendAndroidCommand(
AndroidCommand::kNativeWindowCreated, window);
}
}
-void OnNativeWindowDestroyed(ANativeActivity* activity, ANativeWindow* window) {
+void OnNativeWindowDestroyed(GameActivity* activity, ANativeWindow* window) {
if (g_app_running) {
ApplicationAndroid::Get()->SendAndroidCommand(
AndroidCommand::kNativeWindowDestroyed);
}
}
-void OnInputQueueCreated(ANativeActivity* activity, AInputQueue* queue) {
- if (g_app_running) {
- ApplicationAndroid::Get()->SendAndroidCommand(
- AndroidCommand::kInputQueueChanged, queue);
- }
-}
-
-void OnInputQueueDestroyed(ANativeActivity* activity, AInputQueue* queue) {
- if (g_app_running) {
- ApplicationAndroid::Get()->SendAndroidCommand(
- AndroidCommand::kInputQueueChanged, NULL);
- }
-}
-
-extern "C" SB_EXPORT_PLATFORM void ANativeActivity_onCreate(
- ANativeActivity* activity,
+extern "C" SB_EXPORT_PLATFORM void GameActivity_onCreate(
+ GameActivity* activity,
void* savedState,
size_t savedStateSize) {
// Start the Starboard thread the first time an Activity is created.
@@ -195,11 +198,13 @@
activity->callbacks->onResume = OnResume;
activity->callbacks->onPause = OnPause;
activity->callbacks->onStop = OnStop;
+ activity->callbacks->onTouchEvent = OnTouchEvent;
+ activity->callbacks->onKeyDown = OnKey;
+ activity->callbacks->onKeyUp = OnKey;
activity->callbacks->onWindowFocusChanged = OnWindowFocusChanged;
activity->callbacks->onNativeWindowCreated = OnNativeWindowCreated;
activity->callbacks->onNativeWindowDestroyed = OnNativeWindowDestroyed;
- activity->callbacks->onInputQueueCreated = OnInputQueueCreated;
- activity->callbacks->onInputQueueDestroyed = OnInputQueueDestroyed;
+
activity->instance = ApplicationAndroid::Get();
}
diff --git a/starboard/android/shared/android_media_session_client.cc b/starboard/android/shared/android_media_session_client.cc
index 3ddd6f6..c0986b8 100644
--- a/starboard/android/shared/android_media_session_client.cc
+++ b/starboard/android/shared/android_media_session_client.cc
@@ -127,9 +127,9 @@
// In practice, only one MediaSessionClient will become active at a time.
// Protected by "mutex"
CobaltExtensionMediaSessionUpdatePlatformPlaybackStateCallback
- g_update_platform_playback_state_callback;
-CobaltExtensionMediaSessionInvokeActionCallback g_invoke_action_callback;
-void* g_callback_context;
+ g_update_platform_playback_state_callback = NULL;
+CobaltExtensionMediaSessionInvokeActionCallback g_invoke_action_callback = NULL;
+void* g_callback_context = NULL;
void OnceInit() {
SbMutexCreate(&mutex);
@@ -263,6 +263,17 @@
SbMutexRelease(&mutex);
}
+void DestroyMediaSessionClientCallback() {
+ SbOnce(&once_flag, OnceInit);
+ SbMutexAcquire(&mutex);
+
+ g_callback_context = NULL;
+ g_invoke_action_callback = NULL;
+ g_update_platform_playback_state_callback = NULL;
+
+ SbMutexRelease(&mutex);
+}
+
} // namespace
const CobaltExtensionMediaSessionApi kMediaSessionApi = {
@@ -270,7 +281,7 @@
1,
&OnMediaSessionStateChanged,
&RegisterMediaSessionCallbacks,
- NULL,
+ &DestroyMediaSessionClientCallback,
&UpdateActiveSessionPlatformPlaybackState};
const void* GetMediaSessionApi() {
diff --git a/starboard/android/shared/application_android.cc b/starboard/android/shared/application_android.cc
index fc79766..cc8344b 100644
--- a/starboard/android/shared/application_android.cc
+++ b/starboard/android/shared/application_android.cc
@@ -59,8 +59,6 @@
return "Pause";
case ApplicationAndroid::AndroidCommand::kStop:
return "Stop";
- case ApplicationAndroid::AndroidCommand::kInputQueueChanged:
- return "InputQueueChanged";
case ApplicationAndroid::AndroidCommand::kNativeWindowCreated:
return "NativeWindowCreated";
case ApplicationAndroid::AndroidCommand::kNativeWindowDestroyed:
@@ -83,7 +81,6 @@
ApplicationAndroid::ApplicationAndroid(ALooper* looper)
: looper_(looper),
native_window_(NULL),
- input_queue_(NULL),
android_command_readfd_(-1),
android_command_writefd_(-1),
keyboard_inject_readfd_(-1),
@@ -100,6 +97,17 @@
// from the assets. The use ICU is used in our logging.
SbFileAndroidInitialize();
+ // Enable axes used by Cobalt.
+ static const unsigned int required_axes[] = {
+ AMOTION_EVENT_AXIS_Z, AMOTION_EVENT_AXIS_RZ,
+ AMOTION_EVENT_AXIS_HAT_X, AMOTION_EVENT_AXIS_HAT_Y,
+ AMOTION_EVENT_AXIS_HSCROLL, AMOTION_EVENT_AXIS_VSCROLL,
+ AMOTION_EVENT_AXIS_WHEEL,
+ };
+ for (auto axis : required_axes) {
+ GameActivityPointerAxes_enableAxis(axis);
+ }
+
int pipefd[2];
int err;
@@ -152,6 +160,7 @@
if (SbWindowIsValid(window_)) {
return kSbWindowInvalid;
}
+ ScopedLock lock(input_mutex_);
window_ = new SbWindowPrivate;
window_->native_window = native_window_;
input_events_generator_.reset(new InputEventsGenerator(window_));
@@ -163,6 +172,7 @@
return false;
}
+ ScopedLock lock(input_mutex_);
input_events_generator_.reset();
SB_DCHECK(window == window_);
@@ -182,9 +192,6 @@
case kLooperIdAndroidCommand:
ProcessAndroidCommand();
break;
- case kLooperIdAndroidInput:
- ProcessAndroidInput();
- break;
case kLooperIdKeyboardInject:
ProcessKeyboardInject();
break;
@@ -232,23 +239,6 @@
switch (cmd.type) {
case AndroidCommand::kUndefined:
break;
-
- case AndroidCommand::kInputQueueChanged: {
- ScopedLock lock(android_command_mutex_);
- if (input_queue_) {
- AInputQueue_detachLooper(input_queue_);
- }
- input_queue_ = static_cast<AInputQueue*>(cmd.data);
- if (input_queue_) {
- AInputQueue_attachLooper(input_queue_, looper_, kLooperIdAndroidInput,
- NULL, NULL);
- }
- // Now that we've swapped our use of the input queue, signal that the
- // Android UI thread can continue.
- android_command_condition_.Signal();
- break;
- }
-
// Starboard resume/suspend is tied to the UI window being created/destroyed
// (rather than to the Activity lifecycle) since Cobalt can't do anything at
// all if it doesn't have a window surface to draw on.
@@ -327,10 +317,12 @@
}
// Remember the Android activity state to sync to when we have a window.
+ case AndroidCommand::kStop:
+ SbAtomicNoBarrier_Increment(&android_stop_count_, -1);
+ // Intentional fall-through.
case AndroidCommand::kStart:
case AndroidCommand::kResume:
case AndroidCommand::kPause:
- case AndroidCommand::kStop:
sync_state = activity_state_ = cmd.type;
break;
case AndroidCommand::kDeepLink: {
@@ -356,6 +348,12 @@
}
}
+ // If there's an outstanding "stop" command, then don't update the app state
+ // since it'll be overridden by the upcoming "stop" state.
+ if (SbAtomicNoBarrier_Load(&android_stop_count_) > 0) {
+ return;
+ }
+
// If there's a window, sync the app state to the Activity lifecycle.
if (native_window_) {
switch (sync_state) {
@@ -385,43 +383,65 @@
write(android_command_writefd_, &cmd, sizeof(cmd));
// Synchronization only necessary when managing resources.
switch (type) {
- case AndroidCommand::kInputQueueChanged:
- while (input_queue_ != data) {
- android_command_condition_.Wait();
- }
- break;
case AndroidCommand::kNativeWindowCreated:
case AndroidCommand::kNativeWindowDestroyed:
while (native_window_ != data) {
android_command_condition_.Wait();
}
break;
+ case AndroidCommand::kStop:
+ SbAtomicNoBarrier_Increment(&android_stop_count_, 1);
+ break;
default:
break;
}
}
-void ApplicationAndroid::ProcessAndroidInput() {
- AInputEvent* android_event = NULL;
- while (AInputQueue_getEvent(input_queue_, &android_event) >= 0) {
- SB_LOG(INFO) << "Android input: type="
- << AInputEvent_getType(android_event);
- if (AInputQueue_preDispatchEvent(input_queue_, android_event)) {
- continue;
- }
- if (!input_events_generator_) {
- SB_DLOG(WARNING) << "Android input event ignored without an SbWindow.";
- AInputQueue_finishEvent(input_queue_, android_event, false);
- continue;
- }
- InputEventsGenerator::Events app_events;
- bool handled = input_events_generator_->CreateInputEventsFromAndroidEvent(
- android_event, &app_events);
- for (int i = 0; i < app_events.size(); ++i) {
- Inject(app_events[i].release());
- }
- AInputQueue_finishEvent(input_queue_, android_event, handled);
+bool ApplicationAndroid::SendAndroidMotionEvent(
+ const GameActivityMotionEvent* event) {
+ bool result = false;
+
+ ScopedLock lock(input_mutex_);
+ if (!input_events_generator_) {
+ return false;
}
+
+ // add motion event into the queue.
+ InputEventsGenerator::Events app_events;
+ result = input_events_generator_->CreateInputEventsFromGameActivityEvent(
+ const_cast<GameActivityMotionEvent*>(event), &app_events);
+
+ for (int i = 0; i < app_events.size(); ++i) {
+ Inject(app_events[i].release());
+ }
+
+ return result;
+}
+
+bool ApplicationAndroid::SendAndroidKeyEvent(
+ const GameActivityKeyEvent* event) {
+ bool result = false;
+
+#ifdef STARBOARD_INPUT_EVENTS_FILTER
+ if (!input_events_filter_.ShouldProcessKeyEvent(event)) {
+ return result;
+ }
+#endif
+
+ ScopedLock lock(input_mutex_);
+ if (!input_events_generator_) {
+ return false;
+ }
+
+ // Add key event to the application queue.
+ InputEventsGenerator::Events app_events;
+ result = input_events_generator_->CreateInputEventsFromGameActivityEvent(
+ const_cast<GameActivityKeyEvent*>(event), &app_events);
+ for (int i = 0; i < app_events.size(); i++) {
+ Inject(app_events[i].release());
+ }
+
+ return result;
}
void ApplicationAndroid::ProcessKeyboardInject() {
@@ -429,6 +449,7 @@
int err = read(keyboard_inject_readfd_, &key, sizeof(key));
SB_DCHECK(err >= 0) << "Keyboard inject read failed: errno=" << errno;
SB_LOG(INFO) << "Keyboard inject: " << key;
+ ScopedLock lock(input_mutex_);
if (!input_events_generator_) {
SB_DLOG(WARNING) << "Injected input event ignored without an SbWindow.";
return;
diff --git a/starboard/android/shared/application_android.h b/starboard/android/shared/application_android.h
index 2d147eb..6ae871e 100644
--- a/starboard/android/shared/application_android.h
+++ b/starboard/android/shared/application_android.h
@@ -21,8 +21,13 @@
#include <unordered_map>
#include <vector>
+#include "game-activity/GameActivity.h"
#include "starboard/android/shared/input_events_generator.h"
+#ifdef STARBOARD_INPUT_EVENTS_FILTER
+#include "starboard/android/shared/internal/input_events_filter.h"
+#endif
#include "starboard/android/shared/jni_env_ext.h"
+#include "starboard/atomic.h"
#include "starboard/common/condition_variable.h"
#include "starboard/common/mutex.h"
#include "starboard/common/scoped_ptr.h"
@@ -47,7 +52,6 @@
kResume,
kPause,
kStop,
- kInputQueueChanged,
kNativeWindowCreated,
kNativeWindowDestroyed,
kWindowFocusGained,
@@ -80,6 +84,9 @@
void SendAndroidCommand(AndroidCommand::CommandType type) {
SendAndroidCommand(type, NULL);
}
+ bool SendAndroidMotionEvent(const GameActivityMotionEvent* event);
+ bool SendAndroidKeyEvent(const GameActivityKeyEvent* event);
+
void SendKeyboardInject(SbKey key);
void SbWindowShowOnScreenKeyboard(SbWindow window,
@@ -125,7 +132,6 @@
private:
ALooper* looper_;
ANativeWindow* native_window_;
- AInputQueue* input_queue_;
// Pipes attached to the looper.
int android_command_readfd_;
@@ -134,18 +140,29 @@
int keyboard_inject_writefd_;
// Synchronization for commands that change availability of Android resources
- // such as the input_queue_ and/or native_window_.
+ // such as the input and/or native_window_.
Mutex android_command_mutex_;
ConditionVariable android_command_condition_;
+ // Track queued "stop" commands to avoid starting the app when Android has
+ // already requested it be stopped.
+ SbAtomic32 android_stop_count_ = 0;
+
// The last Activity lifecycle state command received.
AndroidCommand::CommandType activity_state_;
// The single open window, if any.
SbWindow window_;
+ // |input_events_generator_| is accessed from multiple threads, so use a mutex
+ // to safely access it.
+ Mutex input_mutex_;
scoped_ptr<InputEventsGenerator> input_events_generator_;
+#ifdef STARBOARD_INPUT_EVENTS_FILTER
+ internal::InputEventsFilter input_events_filter_;
+#endif
+
bool last_is_accessibility_high_contrast_text_enabled_;
jobject resource_overlay_;
@@ -157,7 +174,6 @@
// Methods to process pipes attached to the Looper.
void ProcessAndroidCommand();
- void ProcessAndroidInput();
void ProcessKeyboardInject();
};
diff --git a/starboard/android/shared/audio_decoder.cc b/starboard/android/shared/audio_decoder.cc
index 2ad0bff..fb81100 100644
--- a/starboard/android/shared/audio_decoder.cc
+++ b/starboard/android/shared/audio_decoder.cc
@@ -24,7 +24,8 @@
// Can be locally set to |1| for verbose audio decoding. Verbose audio
// decoding will log the following transitions that take place for each audio
// unit:
-// T1: Our client passes an |InputBuffer| of audio data into us.
+// T1: Our client passes multiple packets (i.e. InputBuffers) of audio data
+// into us.
// T2: We receive a corresponding media codec output buffer back from our
// |MediaCodecBridge|.
// T3: Our client reads a corresponding |DecodedAudio| out of us.
@@ -96,16 +97,18 @@
media_decoder_->Initialize(error_cb_);
}
-void AudioDecoder::Decode(const scoped_refptr<InputBuffer>& input_buffer,
+void AudioDecoder::Decode(const InputBuffers& input_buffers,
const ConsumedCB& consumed_cb) {
SB_DCHECK(BelongsToCurrentThread());
- SB_DCHECK(input_buffer);
+ SB_DCHECK(!input_buffers.empty());
SB_DCHECK(output_cb_);
SB_DCHECK(media_decoder_);
- VERBOSE_MEDIA_LOG() << "T1: timestamp " << input_buffer->timestamp();
+ for (const auto& input_buffer : input_buffers) {
+ VERBOSE_MEDIA_LOG() << "T1: timestamp " << input_buffer->timestamp();
+ }
- media_decoder_->WriteInputBuffer(input_buffer);
+ media_decoder_->WriteInputBuffers(input_buffers);
ScopedLock lock(decoded_audios_mutex_);
if (media_decoder_->GetNumberOfPendingTasks() + decoded_audios_.size() <=
diff --git a/starboard/android/shared/audio_decoder.h b/starboard/android/shared/audio_decoder.h
index 8a909fe..f9968b4 100644
--- a/starboard/android/shared/audio_decoder.h
+++ b/starboard/android/shared/audio_decoder.h
@@ -48,7 +48,7 @@
~AudioDecoder() override;
void Initialize(const OutputCB& output_cb, const ErrorCB& error_cb) override;
- void Decode(const scoped_refptr<InputBuffer>& input_buffer,
+ void Decode(const InputBuffers& input_buffers,
const ConsumedCB& consumed_cb) override;
void WriteEndOfStream() override;
scoped_refptr<DecodedAudio> Read(int* samples_per_second) override;
diff --git a/starboard/android/shared/audio_decoder_passthrough.h b/starboard/android/shared/audio_decoder_passthrough.h
index 59b2473..4b5ca80 100644
--- a/starboard/android/shared/audio_decoder_passthrough.h
+++ b/starboard/android/shared/audio_decoder_passthrough.h
@@ -51,10 +51,10 @@
output_cb_ = output_cb;
}
- void Decode(const scoped_refptr<InputBuffer>& input_buffer,
+ void Decode(const InputBuffers& input_buffers,
const ConsumedCB& consumed_cb) override {
SB_DCHECK(thread_checker_.CalledOnValidThread());
- SB_DCHECK(input_buffer);
+ SB_DCHECK(!input_buffers.empty());
SB_DCHECK(consumed_cb);
SB_DCHECK(output_cb_);
@@ -66,15 +66,18 @@
// We should revisit this once |DecodedAudio| is used by passthrough
// mode on more platforms.
const int kChannels = 1;
- scoped_refptr<DecodedAudio> decoded_audio =
- new DecodedAudio(kChannels, kSbMediaAudioSampleTypeInt16Deprecated,
- kSbMediaAudioFrameStorageTypePlanar,
- input_buffer->timestamp(), input_buffer->size());
- memcpy(decoded_audio->buffer(), input_buffer->data(), input_buffer->size());
- decoded_audios_.push(decoded_audio);
+ for (const auto& input_buffer : input_buffers) {
+ scoped_refptr<DecodedAudio> decoded_audio =
+ new DecodedAudio(kChannels, kSbMediaAudioSampleTypeInt16Deprecated,
+ kSbMediaAudioFrameStorageTypePlanar,
+ input_buffer->timestamp(), input_buffer->size());
+ memcpy(decoded_audio->buffer(), input_buffer->data(),
+ input_buffer->size());
+ decoded_audios_.push(decoded_audio);
+ output_cb_();
+ }
consumed_cb();
- output_cb_();
}
void WriteEndOfStream() override {
diff --git a/starboard/android/shared/audio_renderer_passthrough.cc b/starboard/android/shared/audio_renderer_passthrough.cc
index b65ca06..34f5bed 100644
--- a/starboard/android/shared/audio_renderer_passthrough.cc
+++ b/starboard/android/shared/audio_renderer_passthrough.cc
@@ -123,10 +123,9 @@
std::bind(&AudioRendererPassthrough::OnDecoderOutput, this), error_cb);
}
-void AudioRendererPassthrough::WriteSample(
- const scoped_refptr<InputBuffer>& input_buffer) {
+void AudioRendererPassthrough::WriteSamples(const InputBuffers& input_buffers) {
SB_DCHECK(BelongsToCurrentThread());
- SB_DCHECK(input_buffer);
+ SB_DCHECK(!input_buffers.empty());
SB_DCHECK(can_accept_more_data_.load());
if (!audio_track_thread_) {
@@ -138,14 +137,14 @@
if (frames_per_input_buffer_ == 0) {
frames_per_input_buffer_ = ParseAc3SyncframeAudioSampleCount(
- input_buffer->data(), input_buffer->size());
+ input_buffers.front()->data(), input_buffers.front()->size());
SB_LOG(INFO) << "Got frames per input buffer " << frames_per_input_buffer_;
}
can_accept_more_data_.store(false);
decoder_->Decode(
- input_buffer,
+ input_buffers,
std::bind(&AudioRendererPassthrough::OnDecoderConsumed, this));
}
@@ -535,14 +534,18 @@
"has likely changed. Restarting playback.";
error_cb_(kSbPlayerErrorCapabilityChanged,
"Audio device capability changed");
- audio_track_bridge_->PauseAndFlush();
- return;
+ } else {
+ // `kSbPlayerErrorDecode` is used for general SbPlayer error, there is
+ // no error code corresponding to audio sink.
+ error_cb_(
+ kSbPlayerErrorDecode,
+ FormatString("Error while writing frames: %d", samples_written));
+ SB_LOG(INFO) << "Encountered kSbPlayerErrorDecode while writing "
+ "frames, error: "
+ << samples_written;
}
- // `kSbPlayerErrorDecode` is used for general SbPlayer error, there is
- // no error code corresponding to audio sink.
- error_cb_(
- kSbPlayerErrorDecode,
- FormatString("Error while writing frames: %d", samples_written));
+ audio_track_bridge_->PauseAndFlush();
+ return;
}
decoded_audio_writing_offset_ += samples_written;
diff --git a/starboard/android/shared/audio_renderer_passthrough.h b/starboard/android/shared/audio_renderer_passthrough.h
index d1d2d05..9e421e0 100644
--- a/starboard/android/shared/audio_renderer_passthrough.h
+++ b/starboard/android/shared/audio_renderer_passthrough.h
@@ -60,7 +60,7 @@
void Initialize(const ErrorCB& error_cb,
const PrerolledCB& prerolled_cb,
const EndedCB& ended_cb) override;
- void WriteSample(const scoped_refptr<InputBuffer>& input_buffer) override;
+ void WriteSamples(const InputBuffers& input_buffers) override;
void WriteEndOfStream() override;
void SetVolume(double volume) override;
diff --git a/starboard/android/shared/audio_sink_min_required_frames_tester.cc b/starboard/android/shared/audio_sink_min_required_frames_tester.cc
index 92ca9e4..b8139a2 100644
--- a/starboard/android/shared/audio_sink_min_required_frames_tester.cc
+++ b/starboard/android/shared/audio_sink_min_required_frames_tester.cc
@@ -132,7 +132,9 @@
}
// Get start threshold before release the audio sink.
- int start_threshold = audio_sink_->GetStartThresholdInFrames();
+ int start_threshold = audio_sink_->IsAudioTrackValid()
+ ? audio_sink_->GetStartThresholdInFrames()
+ : 0;
// |min_required_frames_| is shared between two threads. Release audio sink
// to end audio sink thread before access |min_required_frames_| on this
diff --git a/starboard/android/shared/cobalt/configuration.py b/starboard/android/shared/cobalt/configuration.py
index c606d82..4e57e41 100644
--- a/starboard/android/shared/cobalt/configuration.py
+++ b/starboard/android/shared/cobalt/configuration.py
@@ -60,7 +60,12 @@
'layout_tests': [
# Android relies of system fonts and some older Android builds do not
# have the update (Emoji 11.0) NotoColorEmoji.ttf installed.
- 'CSS3FontsLayoutTests/Layout.Test/color_emojis_should_render_properly'
+ ('CSS3FontsLayoutTests/Layout.Test/'
+ 'color_emojis_should_render_properly'),
+
+ # Android 12 changed the look of emojis.
+ ('CSS3FontsLayoutTests/Layout.Test/'
+ '5_2_use_system_fallback_if_no_matching_family_is_found_emoji'),
],
'crypto_unittests': ['P224.*'],
'renderer_test': [
@@ -69,6 +74,10 @@
'PixelTest.SimpleTextInRed40PtChineseFont',
'PixelTest.SimpleTextInRed40PtThaiFont',
+ # The Roboto variable font looks slightly different than the static
+ # font version. Android 12 and up use font variations for Roboto.
+ 'PixelTest.ScalingUpAnOpacityFilterTextDoesNotPixellate',
+
# Instead of returning an error when allocating too much texture
# memory, Android instead just terminates the process. Since this
# test explicitly tries to allocate too much texture memory, we cannot
diff --git a/starboard/android/shared/file_open.cc b/starboard/android/shared/file_open.cc
index 3e0caa6..8d82ab5 100644
--- a/starboard/android/shared/file_open.cc
+++ b/starboard/android/shared/file_open.cc
@@ -47,13 +47,7 @@
// system font file of the same name.
const std::string fonts_xml("fonts.xml");
const std::string system_fonts_dir("/system/fonts/");
-
-#if SB_IS(EVERGREEN_COMPATIBLE)
- const std::string cobalt_fonts_dir(
- "/cobalt/assets/app/cobalt/content/fonts/");
-#else
const std::string cobalt_fonts_dir("/cobalt/assets/fonts/");
-#endif
// Fonts fallback to the system fonts.
if (path.compare(0, cobalt_fonts_dir.length(), cobalt_fonts_dir) == 0) {
diff --git a/starboard/android/shared/graphics.cc b/starboard/android/shared/graphics.cc
index 5ceed59..85091f8 100644
--- a/starboard/android/shared/graphics.cc
+++ b/starboard/android/shared/graphics.cc
@@ -18,6 +18,7 @@
#include "cobalt/extension/graphics.h"
#include "starboard/android/shared/application_android.h"
+#include "starboard/android/shared/jni_env_ext.h"
namespace starboard {
namespace android {
@@ -40,12 +41,48 @@
return supports_spherical_videos;
}
+bool DefaultShouldClearFrameOnShutdown(float* clear_color_red,
+ float* clear_color_green,
+ float* clear_color_blue,
+ float* clear_color_alpha) {
+ *clear_color_red = 0.0f;
+ *clear_color_green = 0.0f;
+ *clear_color_blue = 0.0f;
+ *clear_color_alpha = 1.0f;
+ return true;
+}
+
+bool DefaultGetMapToMeshColorAdjustments(
+ CobaltExtensionGraphicsMapToMeshColorAdjustment* color_adjustment) {
+ return false;
+}
+
+bool DefaultGetRenderRootTransform(float* m00,
+ float* m01,
+ float* m02,
+ float* m10,
+ float* m11,
+ float* m12,
+ float* m20,
+ float* m21,
+ float* m22) {
+ return false;
+}
+
+void ReportFullyDrawn() {
+ JniEnvExt::Get()->CallStarboardVoidMethodOrAbort("reportFullyDrawn", "()V");
+}
+
const CobaltExtensionGraphicsApi kGraphicsApi = {
kCobaltExtensionGraphicsName,
- 3,
+ 6,
&GetMaximumFrameIntervalInMilliseconds,
&GetMinimumFrameIntervalInMilliseconds,
&IsMapToMeshEnabled,
+ &DefaultShouldClearFrameOnShutdown,
+ &DefaultGetMapToMeshColorAdjustments,
+ &DefaultGetRenderRootTransform,
+ &ReportFullyDrawn,
};
} // namespace
diff --git a/starboard/android/shared/input_events_generator.cc b/starboard/android/shared/input_events_generator.cc
index f749a96..17805e9 100644
--- a/starboard/android/shared/input_events_generator.cc
+++ b/starboard/android/shared/input_events_generator.cc
@@ -14,7 +14,6 @@
#include "starboard/android/shared/input_events_generator.h"
-#include <android/input.h>
#include <android/keycodes.h>
#include <jni.h>
#include <math.h>
@@ -35,8 +34,8 @@
namespace {
-SbKeyLocation AInputEventToSbKeyLocation(AInputEvent* event) {
- int32_t keycode = AKeyEvent_getKeyCode(event);
+SbKeyLocation GameActivityKeyToSbKeyLocation(GameActivityKeyEvent* event) {
+ int32_t keycode = event->keyCode;
switch (keycode) {
case AKEYCODE_ALT_LEFT:
case AKEYCODE_CTRL_LEFT:
@@ -52,8 +51,7 @@
return kSbKeyLocationUnspecified;
}
-unsigned int AInputEventToSbModifiers(AInputEvent* event) {
- int32_t meta = AKeyEvent_getMetaState(event);
+unsigned int GameActivityInputEventMetaStateToSbModifiers(unsigned int meta) {
unsigned int modifiers = kSbKeyModifiersNone;
if (meta & AMETA_ALT_ON) {
modifiers |= kSbKeyModifiersAlt;
@@ -114,8 +112,8 @@
key == kSbKeyGamepadDPadLeft || key == kSbKeyGamepadDPadRight;
}
-SbKey AInputEventToSbKey(AInputEvent* event) {
- int32_t keycode = AKeyEvent_getKeyCode(event);
+SbKey AInputEventToSbKey(GameActivityKeyEvent* event) {
+ auto keycode = event->keyCode;
switch (keycode) {
// Modifiers
case AKEYCODE_ALT_LEFT:
@@ -411,17 +409,19 @@
//
// On game pads with two analog joysticks, AMOTION_EVENT_AXIS_RZ is often
// reinterpreted to report the absolute Y position of the second joystick.
-void InputEventsGenerator::ProcessJoyStickEvent(FlatAxis axis,
- int32_t motion_axis,
- AInputEvent* android_event,
- Events* events) {
- SB_DCHECK(AMotionEvent_getPointerCount(android_event) > 0);
+void InputEventsGenerator::ProcessJoyStickEvent(
+ FlatAxis axis,
+ int32_t motion_axis,
+ GameActivityMotionEvent* android_motion_event,
+ Events* events) {
+ SB_DCHECK(android_motion_event->pointerCount > 0);
- int32_t device_id = AInputEvent_getDeviceId(android_event);
+ int32_t device_id = android_motion_event->deviceId;
SB_DCHECK(device_flat_.find(device_id) != device_flat_.end());
float flat = device_flat_[device_id][axis];
- float offset = AMotionEvent_getAxisValue(android_event, motion_axis, 0);
+ float offset = GameActivityPointerAxes_getAxisValue(
+ &android_motion_event->pointers[0], motion_axis);
int sign = offset < 0.0f ? -1 : 1;
if (fabs(offset) < flat) {
@@ -474,13 +474,13 @@
namespace {
-// Generate a Starboard event from an Android event, with the SbKey and
+// Generate a Starboard event from an Android motion event, with the SbKey and
// SbInputEventType pre-specified (so that it can be used by event
// synthesization as well.)
void PushKeyEvent(SbKey key,
SbInputEventType type,
SbWindow window,
- AInputEvent* android_event,
+ GameActivityMotionEvent* android_event,
Events* events) {
if (key == kSbKeyUnknown) {
SB_NOTREACHED();
@@ -497,12 +497,49 @@
// device
// TODO: differentiate gamepad, remote, etc.
data->device_type = kSbInputDeviceTypeKeyboard;
- data->device_id = AInputEvent_getDeviceId(android_event);
+ data->device_id = android_event->deviceId;
+
+ data->key = key;
+ data->key_location = kSbKeyLocationUnspecified;
+ data->key_modifiers =
+ GameActivityInputEventMetaStateToSbModifiers(android_event->metaState);
+
+ std::unique_ptr<Event> event(
+ new Event(kSbEventTypeInput, data.release(),
+ &Application::DeleteDestructor<SbInputData>));
+ events->push_back(std::move(event));
+}
+
+// Generate a Starboard event from an Android key event, with the SbKey and
+// SbInputEventType pre-specified (so that it can be used by event
+// synthesization as well.)
+void PushKeyEvent(SbKey key,
+ SbInputEventType type,
+ SbWindow window,
+ GameActivityKeyEvent* android_event,
+ Events* events) {
+ if (key == kSbKeyUnknown) {
+ SB_NOTREACHED();
+ return;
+ }
+
+ std::unique_ptr<SbInputData> data(new SbInputData());
+ memset(data.get(), 0, sizeof(*data));
+
+ // window
+ data->window = window;
+ data->type = type;
+
+ // device
+ // TODO: differentiate gamepad, remote, etc.
+ data->device_type = kSbInputDeviceTypeKeyboard;
+ data->device_id = android_event->deviceId;
// key
data->key = key;
- data->key_location = AInputEventToSbKeyLocation(android_event);
- data->key_modifiers = AInputEventToSbModifiers(android_event);
+ data->key_location = GameActivityKeyToSbKeyLocation(android_event);
+ data->key_modifiers =
+ GameActivityInputEventMetaStateToSbModifiers(android_event->metaState);
std::unique_ptr<Event> event(
new Event(kSbEventTypeInput, data.release(),
@@ -588,7 +625,7 @@
float old_value,
float new_value,
SbWindow window,
- AInputEvent* android_event,
+ GameActivityMotionEvent* android_event,
Events* events) {
if (old_value == new_value) {
// No events to generate if the hat motion value did not change.
@@ -607,16 +644,11 @@
} // namespace
-bool InputEventsGenerator::ProcessKeyEvent(AInputEvent* android_event,
- Events* events) {
-#ifdef STARBOARD_INPUT_EVENTS_FILTER
- if (!input_events_filter_.ShouldProcessKeyEvent(android_event)) {
- return false;
- }
-#endif
-
+bool InputEventsGenerator::CreateInputEventsFromGameActivityEvent(
+ GameActivityKeyEvent* android_event,
+ Events* events) {
SbInputEventType type;
- switch (AKeyEvent_getAction(android_event)) {
+ switch (android_event->action) {
case AKEY_EVENT_ACTION_DOWN:
type = kSbInputEventTypePress;
break;
@@ -633,8 +665,7 @@
return false;
}
- if (AKeyEvent_getFlags(android_event) & AKEY_EVENT_FLAG_FALLBACK &&
- IsDPadKey(key)) {
+ if (android_event->flags & AKEY_EVENT_FLAG_FALLBACK && IsDPadKey(key)) {
// For fallback DPad keys, we flow into special processing to manage the
// differentiation between the actual DPad and the left thumbstick, since
// Android conflates the key down/up events for these inputs.
@@ -642,6 +673,7 @@
} else {
PushKeyEvent(key, type, window_, android_event, events);
}
+
return true;
}
@@ -685,12 +717,13 @@
} // namespace
-bool InputEventsGenerator::ProcessPointerEvent(AInputEvent* android_event,
- Events* events) {
- float offset_x =
- AMotionEvent_getAxisValue(android_event, AMOTION_EVENT_AXIS_X, 0);
- float offset_y =
- AMotionEvent_getAxisValue(android_event, AMOTION_EVENT_AXIS_Y, 0);
+bool InputEventsGenerator::ProcessPointerEvent(
+ GameActivityMotionEvent* android_event,
+ Events* events) {
+ float offset_x = GameActivityPointerAxes_getAxisValue(
+ &android_event->pointers[0], AMOTION_EVENT_AXIS_X);
+ float offset_y = GameActivityPointerAxes_getAxisValue(
+ &android_event->pointers[0], AMOTION_EVENT_AXIS_Y);
std::unique_ptr<SbInputData> data(new SbInputData());
memset(data.get(), 0, sizeof(*data));
@@ -700,27 +733,28 @@
data->pressure = NAN;
data->size = {NAN, NAN};
data->tilt = {NAN, NAN};
- unsigned int button_state = AMotionEvent_getButtonState(android_event);
+ unsigned int button_state = android_event->buttonState;
unsigned int button_modifiers = ButtonStateToSbModifiers(button_state);
// Default to reporting pointer events as mouse events.
data->device_type = kSbInputDeviceTypeMouse;
// Report both stylus and touchscreen events as touchscreen device events.
- int32_t event_source = AInputEvent_getSource(android_event);
+ int32_t event_source = android_event->source;
if (((event_source & AINPUT_SOURCE_TOUCHSCREEN) != 0) ||
((event_source & AINPUT_SOURCE_STYLUS) != 0)) {
data->device_type = kSbInputDeviceTypeTouchScreen;
}
- data->device_id = AInputEvent_getDeviceId(android_event);
+ data->device_id = android_event->deviceId;
data->key_modifiers =
- button_modifiers | AInputEventToSbModifiers(android_event);
+ button_modifiers |
+ GameActivityInputEventMetaStateToSbModifiers(android_event->metaState);
data->position.x = offset_x;
data->position.y = offset_y;
data->key = ButtonStateToSbKey(button_state);
- switch (AKeyEvent_getAction(android_event) & AMOTION_EVENT_ACTION_MASK) {
+ switch (android_event->action & AMOTION_EVENT_ACTION_MASK) {
case AMOTION_EVENT_ACTION_UP:
data->type = kSbInputEventTypeUnpress;
break;
@@ -732,12 +766,15 @@
data->type = kSbInputEventTypeMove;
break;
case AMOTION_EVENT_ACTION_SCROLL: {
- float hscroll = AMotionEvent_getAxisValue(
- android_event, AMOTION_EVENT_AXIS_HSCROLL, 0); // left is -1
- float vscroll = AMotionEvent_getAxisValue(
- android_event, AMOTION_EVENT_AXIS_VSCROLL, 0); // down is -1
- float wheel =
- AMotionEvent_getAxisValue(android_event, AMOTION_EVENT_AXIS_WHEEL, 0);
+ float hscroll = GameActivityPointerAxes_getAxisValue(
+ &android_event->pointers[0],
+ AMOTION_EVENT_AXIS_HSCROLL); // left is -1
+ float vscroll = GameActivityPointerAxes_getAxisValue(
+ &android_event->pointers[0],
+ AMOTION_EVENT_AXIS_VSCROLL); // down is -1
+ float wheel = GameActivityPointerAxes_getAxisValue(
+ &android_event->pointers[0],
+ AMOTION_EVENT_AXIS_WHEEL); // this is not used
data->type = kSbInputEventTypeWheel;
data->key = kSbKeyUnknown;
data->delta.y = -vscroll;
@@ -754,13 +791,14 @@
return true;
}
-bool InputEventsGenerator::ProcessMotionEvent(AInputEvent* android_event,
- Events* events) {
- int32_t event_source = AInputEvent_getSource(android_event);
- if ((event_source & AINPUT_SOURCE_CLASS_POINTER) != 0) {
+bool InputEventsGenerator::CreateInputEventsFromGameActivityEvent(
+ GameActivityMotionEvent* android_event,
+ Events* events) {
+ if ((android_event->source & AINPUT_SOURCE_CLASS_POINTER) != 0) {
return ProcessPointerEvent(android_event, events);
}
- if ((event_source & AINPUT_SOURCE_JOYSTICK) == 0) {
+
+ if ((android_event->source & AINPUT_SOURCE_JOYSTICK) == 0) {
// Only handles joystick events in the code below.
return false;
}
@@ -784,11 +822,12 @@
// Special processing to disambiguate between DPad events and left-thumbstick
// direction key events.
-void InputEventsGenerator::ProcessFallbackDPadEvent(SbInputEventType type,
- SbKey key,
- AInputEvent* android_event,
- Events* events) {
- SB_DCHECK(AKeyEvent_getFlags(android_event) & AKEY_EVENT_FLAG_FALLBACK);
+void InputEventsGenerator::ProcessFallbackDPadEvent(
+ SbInputEventType type,
+ SbKey key,
+ GameActivityKeyEvent* android_event,
+ Events* events) {
+ SB_DCHECK(android_event->flags & AKEY_EVENT_FLAG_FALLBACK);
SB_DCHECK(IsDPadKey(key));
HatAxis hat_axis = HatValueForDPadKey(key).axis;
@@ -797,8 +836,8 @@
// Direction pad events are all assumed to be coming from the hat controls
// if motion events for that hat DPAD is active, but we do still handle
// repeat keys here.
- if (AKeyEvent_getRepeatCount(android_event) > 0) {
- SB_LOG(INFO) << AKeyEvent_getRepeatCount(android_event);
+ if ((android_event->repeatCount) > 0) {
+ SB_LOG(INFO) << android_event->repeatCount;
PushKeyEvent(key, kSbInputEventTypePress, window_, android_event, events);
}
return;
@@ -833,24 +872,24 @@
// event's data. Possibly generate DPad events based on any changes in value
// here.
void InputEventsGenerator::UpdateHatValuesAndPossiblySynthesizeKeyEvents(
- AInputEvent* android_event,
+ GameActivityMotionEvent* android_motion_event,
Events* events) {
- float new_hat_x =
- AMotionEvent_getAxisValue(android_event, AMOTION_EVENT_AXIS_HAT_X, 0);
+ float new_hat_x = GameActivityPointerAxes_getAxisValue(
+ &android_motion_event->pointers[0], AMOTION_EVENT_AXIS_HAT_X);
PossiblySynthesizeHatKeyEvents(kHatX, hat_value_[kHatX], new_hat_x, window_,
- android_event, events);
+ android_motion_event, events);
hat_value_[kHatX] = new_hat_x;
- float new_hat_y =
- AMotionEvent_getAxisValue(android_event, AMOTION_EVENT_AXIS_HAT_Y, 0);
+ float new_hat_y = GameActivityPointerAxes_getAxisValue(
+ &android_motion_event->pointers[0], AMOTION_EVENT_AXIS_HAT_Y);
PossiblySynthesizeHatKeyEvents(kHatY, hat_value_[kHatY], new_hat_y, window_,
- android_event, events);
+ android_motion_event, events);
hat_value_[kHatY] = new_hat_y;
}
void InputEventsGenerator::UpdateDeviceFlatMapIfNecessary(
- AInputEvent* android_event) {
- int32_t device_id = AInputEvent_getDeviceId(android_event);
+ GameActivityMotionEvent* android_motion_event) {
+ int32_t device_id = android_motion_event->deviceId;
if (device_flat_.find(device_id) != device_flat_.end()) {
// |device_flat_| is already contains the device flat information.
return;
@@ -867,28 +906,6 @@
device_flat_[device_id] = std::vector<float>(flats, flats + kNumAxes);
}
-bool InputEventsGenerator::CreateInputEventsFromAndroidEvent(
- AInputEvent* android_event,
- Events* events) {
- if (android_event == NULL ||
- (AInputEvent_getType(android_event) != AINPUT_EVENT_TYPE_KEY &&
- AInputEvent_getType(android_event) != AINPUT_EVENT_TYPE_MOTION)) {
- return false;
- }
-
- switch (AInputEvent_getType(android_event)) {
- case AINPUT_EVENT_TYPE_KEY:
- return ProcessKeyEvent(android_event, events);
- case AINPUT_EVENT_TYPE_MOTION: {
- return ProcessMotionEvent(android_event, events);
- }
- default:
- SB_NOTREACHED();
- }
-
- return false;
-}
-
void InputEventsGenerator::CreateInputEventsFromSbKey(SbKey key,
Events* events) {
events->clear();
diff --git a/starboard/android/shared/input_events_generator.h b/starboard/android/shared/input_events_generator.h
index c192079..ec2ff9c 100644
--- a/starboard/android/shared/input_events_generator.h
+++ b/starboard/android/shared/input_events_generator.h
@@ -15,14 +15,11 @@
#ifndef STARBOARD_ANDROID_SHARED_INPUT_EVENTS_GENERATOR_H_
#define STARBOARD_ANDROID_SHARED_INPUT_EVENTS_GENERATOR_H_
-#include <android/input.h>
#include <map>
#include <memory>
#include <vector>
-#ifdef STARBOARD_INPUT_EVENTS_FILTER
-#include "starboard/android/shared/internal/input_events_filter.h"
-#endif
+#include "game-activity/GameActivity.h"
#include "starboard/input.h"
#include "starboard/shared/starboard/application.h"
@@ -43,8 +40,12 @@
// Translates an Android input event into a series of Starboard application
// events. The caller owns the new events and must delete them when done with
// them.
- bool CreateInputEventsFromAndroidEvent(AInputEvent* android_event,
- Events* events);
+ bool CreateInputEventsFromGameActivityEvent(
+ GameActivityMotionEvent* android_event,
+ Events* events);
+ bool CreateInputEventsFromGameActivityEvent(
+ GameActivityKeyEvent* android_event,
+ Events* events);
// Create press/unpress events from SbKey
// (for use with CobaltA11yHelper injection)
@@ -59,28 +60,25 @@
kNumAxes,
};
- bool ProcessKeyEvent(AInputEvent* android_event, Events* events);
- bool ProcessPointerEvent(AInputEvent* android_event, Events* events);
- bool ProcessMotionEvent(AInputEvent* android_event, Events* events);
+ bool ProcessPointerEvent(GameActivityMotionEvent* android_event,
+ Events* events);
void ProcessJoyStickEvent(FlatAxis axis,
int32_t motion_axis,
- AInputEvent* android_event,
+ GameActivityMotionEvent* android_event,
Events* events);
- void UpdateDeviceFlatMapIfNecessary(AInputEvent* android_event);
+ void UpdateDeviceFlatMapIfNecessary(GameActivityMotionEvent* android_event);
+ void UpdateDeviceFlatMapIfNecessary(GameActivityKeyEvent* android_event);
void ProcessFallbackDPadEvent(SbInputEventType type,
SbKey key,
- AInputEvent* android_event,
+ GameActivityKeyEvent* android_event,
Events* events);
- void UpdateHatValuesAndPossiblySynthesizeKeyEvents(AInputEvent* android_event,
- Events* events);
+ void UpdateHatValuesAndPossiblySynthesizeKeyEvents(
+ GameActivityMotionEvent* android_event,
+ Events* events);
SbWindow window_;
-#ifdef STARBOARD_INPUT_EVENTS_FILTER
- internal::InputEventsFilter input_events_filter_;
-#endif
-
// Map the device id with joystick flat position.
// Cache the flat area of joystick to avoid calling jni functions frequently.
std::map<int32_t, std::vector<float> > device_flat_;
diff --git a/starboard/android/shared/media_decoder.cc b/starboard/android/shared/media_decoder.cc
index 66bd437..90beda3 100644
--- a/starboard/android/shared/media_decoder.cc
+++ b/starboard/android/shared/media_decoder.cc
@@ -178,11 +178,9 @@
}
}
-void MediaDecoder::WriteInputBuffer(
- const scoped_refptr<InputBuffer>& input_buffer) {
+void MediaDecoder::WriteInputBuffers(const InputBuffers& input_buffers) {
SB_DCHECK(thread_checker_.CalledOnValidThread());
- SB_DCHECK(input_buffer);
-
+ SB_DCHECK(!input_buffers.empty());
if (stream_ended_.load()) {
SB_LOG(ERROR) << "Decode() is called after WriteEndOfStream() is called.";
return;
@@ -199,9 +197,12 @@
}
ScopedLock scoped_lock(mutex_);
- pending_tasks_.push_back(Event(input_buffer));
- number_of_pending_tasks_.increment();
- if (pending_tasks_.size() == 1) {
+ bool need_signal = pending_tasks_.empty();
+ for (const auto& input_buffer : input_buffers) {
+ pending_tasks_.push_back(Event(input_buffer));
+ number_of_pending_tasks_.increment();
+ }
+ if (need_signal) {
condition_variable_.Signal();
}
}
diff --git a/starboard/android/shared/media_decoder.h b/starboard/android/shared/media_decoder.h
index dbe73fc..913aff6 100644
--- a/starboard/android/shared/media_decoder.h
+++ b/starboard/android/shared/media_decoder.h
@@ -47,6 +47,7 @@
public:
typedef ::starboard::shared::starboard::player::filter::ErrorCB ErrorCB;
typedef ::starboard::shared::starboard::player::InputBuffer InputBuffer;
+ typedef ::starboard::shared::starboard::player::InputBuffers InputBuffers;
typedef std::function<void(SbTime)> FrameRenderedCB;
// This class should be implemented by the users of MediaDecoder to receive
@@ -94,7 +95,7 @@
~MediaDecoder();
void Initialize(const ErrorCB& error_cb);
- void WriteInputBuffer(const scoped_refptr<InputBuffer>& input_buffer);
+ void WriteInputBuffers(const InputBuffers& input_buffers);
void WriteEndOfStream();
void SetPlaybackRate(double playback_rate);
diff --git a/starboard/android/shared/system_get_path.cc b/starboard/android/shared/system_get_path.cc
index 5e4610a..1bd30ae 100644
--- a/starboard/android/shared/system_get_path.cc
+++ b/starboard/android/shared/system_get_path.cc
@@ -77,6 +77,21 @@
break;
}
+ case kSbSystemPathFontConfigurationDirectory:
+ case kSbSystemPathFontDirectory:
+#if SB_IS(EVERGREEN_COMPATIBLE)
+ if (starboard::strlcat(path, g_app_assets_dir, kPathSize) >= kPathSize) {
+ return false;
+ }
+ if (starboard::strlcat(path, "/fonts", kPathSize) >= kPathSize) {
+ return false;
+ }
+ break;
+#else
+ SB_NOTIMPLEMENTED();
+ return false;
+#endif
+
case kSbSystemPathStorageDirectory: {
if (starboard::strlcpy(path, g_app_files_dir, kPathSize) >= kPathSize) {
return false;
diff --git a/starboard/android/shared/test_filters.py b/starboard/android/shared/test_filters.py
index 8d5d0c0..e68339f 100644
--- a/starboard/android/shared/test_filters.py
+++ b/starboard/android/shared/test_filters.py
@@ -19,6 +19,10 @@
# pylint: disable=line-too-long
_FILTERED_TESTS = {
'player_filter_tests': [
+ # Invalid input may lead to unexpected behaviors.
+ 'AudioDecoderTests/AudioDecoderTest.MultipleInvalidInput/*',
+ 'AudioDecoderTests/AudioDecoderTest.MultipleValidInputsAfterInvalidInput*',
+
# GetMaxNumberOfCachedFrames() on Android is device dependent,
# and Android doesn't provide an API to get it. So, this function
# doesn't make sense on Android. But HoldFramesUntilFull tests depend
diff --git a/starboard/android/shared/video_decoder.cc b/starboard/android/shared/video_decoder.cc
index 5078989..e84cf2e 100644
--- a/starboard/android/shared/video_decoder.cc
+++ b/starboard/android/shared/video_decoder.cc
@@ -337,20 +337,20 @@
return kInitialPrerollTimeout;
}
-void VideoDecoder::WriteInputBuffer(
- const scoped_refptr<InputBuffer>& input_buffer) {
+void VideoDecoder::WriteInputBuffers(const InputBuffers& input_buffers) {
SB_DCHECK(BelongsToCurrentThread());
- SB_DCHECK(input_buffer);
- SB_DCHECK(input_buffer->sample_type() == kSbMediaTypeVideo);
+ SB_DCHECK(!input_buffers.empty());
+ SB_DCHECK(input_buffers.front()->sample_type() == kSbMediaTypeVideo);
SB_DCHECK(decoder_status_cb_);
if (input_buffer_written_ == 0) {
SB_DCHECK(video_fps_ == 0);
- first_buffer_timestamp_ = input_buffer->timestamp();
+ first_buffer_timestamp_ = input_buffers.front()->timestamp();
// If color metadata is present and is not an identity mapping, then
// teardown the codec so it can be reinitalized with the new metadata.
- auto& color_metadata = input_buffer->video_sample_info().color_metadata;
+ const auto& color_metadata =
+ input_buffers.front()->video_sample_info().color_metadata;
if (!IsIdentity(color_metadata)) {
SB_DCHECK(!color_metadata_) << "Unexpected residual color metadata.";
SB_LOG(INFO) << "Reinitializing codec with HDR color metadata.";
@@ -378,14 +378,15 @@
}
}
- ++input_buffer_written_;
+ input_buffer_written_ += input_buffers.size();
if (video_codec_ == kSbMediaVideoCodecAv1 && video_fps_ == 0) {
SB_DCHECK(!media_decoder_);
+ pending_input_buffers_.insert(pending_input_buffers_.end(),
+ input_buffers.begin(), input_buffers.end());
if (pending_input_buffers_.size() <
kFpsGuesstimateRequiredInputBufferCount) {
- pending_input_buffers_.push_back(input_buffer);
decoder_status_cb_(kNeedMoreInput, NULL);
return;
}
@@ -398,9 +399,10 @@
ReportError(kSbPlayerErrorDecode, error_message);
return;
}
+ return;
}
- WriteInputBufferInternal(input_buffer);
+ WriteInputBuffersInternal(input_buffers);
}
void VideoDecoder::WriteEndOfStream() {
@@ -581,9 +583,9 @@
} else {
SB_DCHECK(pending_input_buffers_.empty());
}
- while (!pending_input_buffers_.empty()) {
- WriteInputBufferInternal(pending_input_buffers_[0]);
- pending_input_buffers_.pop_front();
+ if (!pending_input_buffers_.empty()) {
+ WriteInputBuffersInternal(pending_input_buffers_);
+ pending_input_buffers_.clear();
}
return true;
}
@@ -646,8 +648,10 @@
sink_->Render();
}
-void VideoDecoder::WriteInputBufferInternal(
- const scoped_refptr<InputBuffer>& input_buffer) {
+void VideoDecoder::WriteInputBuffersInternal(
+ const InputBuffers& input_buffers) {
+ SB_DCHECK(!input_buffers.empty());
+
// There's a race condition when suspending the app. If surface view is
// destroyed before video decoder stopped, |media_decoder_| could be null
// here. And error_cb_() could be handled asynchronously. It's possible
@@ -657,7 +661,7 @@
SB_LOG(INFO) << "Trying to write input buffer when media_decoder_ is null.";
return;
}
- media_decoder_->WriteInputBuffer(input_buffer);
+ media_decoder_->WriteInputBuffers(input_buffers);
if (media_decoder_->GetNumberOfPendingTasks() < kMaxPendingWorkSize) {
decoder_status_cb_(kNeedMoreInput, NULL);
} else if (tunnel_mode_audio_session_id_ != -1) {
@@ -669,7 +673,11 @@
}
if (tunnel_mode_audio_session_id_ != -1) {
- video_frame_tracker_->OnInputBuffer(input_buffer->timestamp());
+ SbTime max_timestamp = input_buffers[0]->timestamp();
+ for (const auto& input_buffer : input_buffers) {
+ video_frame_tracker_->OnInputBuffer(input_buffer->timestamp());
+ max_timestamp = std::max(max_timestamp, input_buffer->timestamp());
+ }
if (tunnel_mode_prerolling_.load()) {
// TODO: Refine preroll logic in tunnel mode.
@@ -677,17 +685,17 @@
if (first_buffer_timestamp_ == 0) {
// Initial playback.
enough_buffers_written_to_media_codec =
- (input_buffer_written_ - pending_input_buffers_.size() -
+ (input_buffer_written_ -
media_decoder_->GetNumberOfPendingTasks()) >
kInitialPrerollFrameCount;
} else {
// Seeking. Note that this branch can be eliminated once seeking in
// tunnel mode is always aligned to the next video key frame.
enough_buffers_written_to_media_codec =
- (input_buffer_written_ - pending_input_buffers_.size() -
+ (input_buffer_written_ -
media_decoder_->GetNumberOfPendingTasks()) >
kSeekingPrerollPendingWorkSizeInTunnelMode &&
- input_buffer->timestamp() >= video_frame_tracker_->seek_to_time();
+ max_timestamp >= video_frame_tracker_->seek_to_time();
}
bool cache_full =
@@ -698,7 +706,7 @@
if (prerolled && tunnel_mode_prerolling_.exchange(false)) {
SB_LOG(INFO)
<< "Tunnel mode preroll finished on enqueuing input buffer "
- << input_buffer->timestamp() << ", for seek time "
+ << max_timestamp << ", for seek time "
<< video_frame_tracker_->seek_to_time();
decoder_status_cb_(
kNeedMoreInput,
diff --git a/starboard/android/shared/video_decoder.h b/starboard/android/shared/video_decoder.h
index 7859aa2..19a8b82 100644
--- a/starboard/android/shared/video_decoder.h
+++ b/starboard/android/shared/video_decoder.h
@@ -16,8 +16,8 @@
#define STARBOARD_ANDROID_SHARED_VIDEO_DECODER_H_
#include <atomic>
-#include <deque>
#include <string>
+#include <vector>
#include "starboard/android/shared/drm_system.h"
#include "starboard/android/shared/media_codec_bridge.h"
@@ -87,8 +87,7 @@
// buffer.
size_t GetMaxNumberOfCachedFrames() const override { return 12; }
- void WriteInputBuffer(
- const scoped_refptr<InputBuffer>& input_buffer) override;
+ void WriteInputBuffers(const InputBuffers& input_buffers) override;
void WriteEndOfStream() override;
void Reset() override;
SbDecodeTarget GetCurrentDecodeTarget() override;
@@ -105,7 +104,7 @@
bool InitializeCodec(std::string* error_message);
void TeardownCodec();
- void WriteInputBufferInternal(const scoped_refptr<InputBuffer>& input_buffer);
+ void WriteInputBuffersInternal(const InputBuffers& input_buffers);
void ProcessOutputBuffer(MediaCodecBridge* media_codec_bridge,
const DequeueOutputResult& output) override;
void OnEndOfStreamWritten(MediaCodecBridge* media_codec_bridge);
@@ -196,7 +195,7 @@
Mutex surface_destroy_mutex_;
ConditionVariable surface_condition_variable_;
- std::deque<const scoped_refptr<InputBuffer>> pending_input_buffers_;
+ std::vector<scoped_refptr<InputBuffer>> pending_input_buffers_;
int video_fps_ = 0;
};
diff --git a/starboard/build/config/base_configuration.gni b/starboard/build/config/base_configuration.gni
index 5f62aaa..bcd37d2 100644
--- a/starboard/build/config/base_configuration.gni
+++ b/starboard/build/config/base_configuration.gni
@@ -51,6 +51,10 @@
# Whether to adopt Evergreen Lite on the Evergreen compatible platform.
sb_evergreen_compatible_enable_lite = false
+ # Whether to generate the whole package containing both Loader app and Cobalt
+ # core on the Evergreen compatible platform.
+ sb_evergreen_compatible_package = false
+
# The target type for test targets. Allows changing the target type
# on platforms where the native code may require an additional packaging step
# (ex. Android).
@@ -135,12 +139,6 @@
# Set to true to enable H5vccAccountManager.
enable_account_manager = false
- # Set to true to enable H5vccSSO (Single Sign On).
- enable_sso = false
-
- # Set to true to enable filtering of HTTP headers before sending.
- enable_xhr_header_filtering = false
-
# TODO(b/173248397): Migrate to CobaltExtensions or PlatformServices.
# List of platform-specific targets that get compiled into cobalt.
cobalt_platform_dependencies = []
diff --git a/starboard/build/config/bundle_content.gni b/starboard/build/config/bundle_content.gni
index 2008c95..f7f2a5d 100644
--- a/starboard/build/config/bundle_content.gni
+++ b/starboard/build/config/bundle_content.gni
@@ -16,7 +16,7 @@
bundle_name = invoker.bundle_name
bundle_deps = invoker.bundle_deps
- bundle_content_list_file = "$root_gen_dir/${target_name}_files.txt"
+ bundle_content_rsp_file = "$target_gen_dir/${target_name}_list.rsp"
file_format = "list lines"
# If the platform bundles content the list of content files can be collected
@@ -37,7 +37,7 @@
}
output_conversion = file_format
- outputs = [ bundle_content_list_file ]
+ outputs = [ bundle_content_rsp_file ]
}
action(target_name) {
@@ -45,16 +45,14 @@
deps = [ ":list_$target_name" ]
- inputs = [ bundle_content_list_file ]
+ sources = [ bundle_content_rsp_file ]
bundle_content_dir =
"$sb_install_output_dir/$bundle_name/$sb_install_content_subdir"
- # TODO(b/220024845): We don't have the list of output files. The files
- # are listed in `bundle_content_list_file` but can't be accessed in GN
- # as `read_file` can't reliably access files generated by
- # `generated_file` targets.
- outputs = [ bundle_content_dir ]
+ outputs = [ "$target_gen_dir/${target_name}.stamp" ]
+
+ depfile = "$target_out_dir/$target_name.d"
script = "//starboard/build/copy_install_content.py"
args = [
@@ -63,7 +61,11 @@
"--base_dir",
rebase_path(sb_static_contents_output_data_dir, root_build_dir),
"--files_list",
- rebase_path(bundle_content_list_file, root_build_dir),
+ rebase_path(bundle_content_rsp_file, root_build_dir),
+ "--output",
+ rebase_path(outputs[0], root_build_dir),
+ "--depfile",
+ rebase_path(depfile, root_build_dir),
]
}
}
diff --git a/starboard/build/copy_install_content.py b/starboard/build/copy_install_content.py
index 1e29cbf..b92ff67 100644
--- a/starboard/build/copy_install_content.py
+++ b/starboard/build/copy_install_content.py
@@ -19,17 +19,31 @@
The folder structure of the input files is maintained in the output relative
to the 'base_dir' parameter.
+
+If the parameters `output` and `depfile` are supplied the list of copied files
+will be written to `depfile` and an `output` dummy file will be created.
"""
import argparse
import os
+import pathlib
import shutil
+import sys
class InvalidArgumentException(Exception):
pass
+def validate_args(options):
+ if not os.path.exists(options.files_list):
+ raise InvalidArgumentException(f'{options.files_list} doesn\'t exist')
+
+ # If either `depfile` and `output` is present the other one must also be.
+ if bool(options.depfile) != bool(options.output):
+ raise InvalidArgumentException('output and depfile must both be supplied')
+
+
def copy_files(files_to_copy, base_dir, output_dir):
if not os.path.exists(output_dir):
os.makedirs(output_dir)
@@ -66,7 +80,14 @@
shutil.copytree(filename, output_filename)
-if __name__ == '__main__':
+def write_outputs(output, depfile, files):
+ with open(depfile, 'w') as f:
+ f.write('{}: \\\n {}\n'.format(output, ' \\\n '.join(sorted(files))))
+ # Touch the output file to tell ninja that the script ran successfully.
+ pathlib.Path(output).touch()
+
+
+def main():
parser = argparse.ArgumentParser()
parser.add_argument(
'--output_dir', dest='output_dir', required=True, help='output directory')
@@ -80,11 +101,26 @@
dest='files_list',
required=True,
help='path to file containing list of input files')
+ parser.add_argument('--output', dest='output', help='dummy output file')
+ parser.add_argument(
+ '--depfile',
+ dest='depfile',
+ help='depfile to write the list of touched files to')
options = parser.parse_args()
+ validate_args(options)
+
# Load file names from the file containing the list of file names.
# The file name list must be passed in a file to due to command line limits.
with open(options.files_list) as input_file:
file_names = [line.strip() for line in input_file]
copy_files(file_names, options.base_dir, options.output_dir)
+
+ if options.output and options.depfile:
+ # If depfile and output are present write the list of files to the depfile.
+ write_outputs(options.output, options.depfile, file_names)
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/starboard/common/ref_counted.h b/starboard/common/ref_counted.h
index cd94aa1..4c3c8c3 100644
--- a/starboard/common/ref_counted.h
+++ b/starboard/common/ref_counted.h
@@ -6,6 +6,7 @@
#define STARBOARD_COMMON_REF_COUNTED_H_
#include <algorithm>
+#include <utility>
#include "starboard/atomic.h"
#include "starboard/common/log.h"
@@ -148,7 +149,8 @@
: public starboard::RefCountedThreadSafe<starboard::RefCountedData<T> > {
public:
RefCountedData() : data() {}
- RefCountedData(const T& in_value) : data(in_value) {}
+ RefCountedData(const T& in_value) : data(in_value) {} // NOLINT
+ RefCountedData(T&& in_value) : data(std::move(in_value)) {} // NOLINT
T data;
@@ -212,7 +214,7 @@
scoped_refptr() : ptr_(NULL) {}
- scoped_refptr(T* p) : ptr_(p) {
+ scoped_refptr(T* p) : ptr_(p) { // NOLINT
if (ptr_)
ptr_->AddRef();
}
@@ -222,6 +224,20 @@
ptr_->AddRef();
}
+ // Move constructor. This is required in addition to the move conversion
+ // constructor below.
+ scoped_refptr(scoped_refptr<T>&& r) noexcept : ptr_(r.ptr_) {
+ r.ptr_ = nullptr;
+ }
+
+ // Move conversion constructor.
+ template <typename U,
+ typename = typename std::enable_if<
+ std::is_convertible<U*, T*>::value>::type>
+ scoped_refptr(scoped_refptr<U>&& r) noexcept : ptr_(r.get()) {
+ r.ptr_ = nullptr;
+ }
+
template <typename U>
scoped_refptr(const scoped_refptr<U>& r) : ptr_(r.get()) {
if (ptr_)
@@ -275,6 +291,11 @@
protected:
T* ptr_;
+
+ private:
+ // Friend required for move constructors that set r.ptr_ to null.
+ template <typename U>
+ friend class scoped_refptr;
};
// Handy utility for creating a scoped_refptr<T> out of a T* explicitly without
diff --git a/starboard/elf_loader/exported_symbols.h b/starboard/elf_loader/exported_symbols.h
index d385e1d..d2ee152 100644
--- a/starboard/elf_loader/exported_symbols.h
+++ b/starboard/elf_loader/exported_symbols.h
@@ -18,10 +18,6 @@
#include <map>
#include <string>
-#include "starboard/elf_loader/elf_hash_table.h"
-#include "starboard/elf_loader/gnu_hash_table.h"
-#include "starboard/file.h"
-
namespace starboard {
namespace elf_loader {
diff --git a/starboard/evergreen/shared/platform_configuration/BUILD.gn b/starboard/evergreen/shared/platform_configuration/BUILD.gn
index 43d8a69..db425fc 100644
--- a/starboard/evergreen/shared/platform_configuration/BUILD.gn
+++ b/starboard/evergreen/shared/platform_configuration/BUILD.gn
@@ -167,6 +167,9 @@
config("size") {
cflags = [ "-Os" ]
+ if (is_qa || is_gold) {
+ ldflags = [ "--icf=safe" ]
+ }
}
config("pedantic_warnings") {
diff --git a/starboard/evergreen/testing/README.md b/starboard/evergreen/testing/README.md
index e401325..bf64968 100644
--- a/starboard/evergreen/testing/README.md
+++ b/starboard/evergreen/testing/README.md
@@ -75,6 +75,7 @@
* `tests/update_fails_verification_test.sh`
* `tests/update_works_for_only_one_app_test.sh`
* `tests/valid_slot_overwritten_test.sh`
+* `tests/verify_qa_channel_compressed_update_test.sh`
* `tests/verify_qa_channel_update_test.sh`
How To Run
diff --git a/starboard/evergreen/testing/run_all_tests.sh b/starboard/evergreen/testing/run_all_tests.sh
index 91c53fb..1339884 100755
--- a/starboard/evergreen/testing/run_all_tests.sh
+++ b/starboard/evergreen/testing/run_all_tests.sh
@@ -48,9 +48,10 @@
if [[ "${USE_COMPRESSED_SYSTEM_IMAGE}" == "true" ]]; then
# It would be valid to run all test cases using a compressed system image but
- # is probably excessive. Instead, just the Evergreen Lite case is run to test
- # that the compressed system image can be successfully loaded.
- TESTS=($(eval "find ${DIR}/tests -maxdepth 1 -name 'evergreen_lite_test.sh'"))
+ # is probably excessive. Instead, just two cases are run: one to test that a
+ # compressed system image can be loaded and one to test that a compressed
+ # update can be upgraded to.
+ TESTS=($(eval "find ${DIR}/tests -maxdepth 1 \( -name 'evergreen_lite_test.sh' -o -name 'verify_qa_channel_compressed_update_test.sh' \)"))
else
TESTS=($(eval "find ${DIR}/tests -maxdepth 1 -name '*_test.sh'"))
fi
diff --git a/starboard/evergreen/testing/tests/verify_qa_channel_compressed_update_test.sh b/starboard/evergreen/testing/tests/verify_qa_channel_compressed_update_test.sh
new file mode 100755
index 0000000..61d8602
--- /dev/null
+++ b/starboard/evergreen/testing/tests/verify_qa_channel_compressed_update_test.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+#
+# Copyright 2022 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Unset the previous test's name and runner function.
+unset TEST_NAME
+unset TEST_FILE
+unset -f run_test
+
+TEST_NAME="VerifyQaChannelCompressedUpdate"
+TEST_FILE="test.html"
+
+function run_test() {
+ clear_storage
+
+ cycle_cobalt "file:///tests/${TEST_FILE}?channel=test" "${TEST_NAME}.0.log" "update from test channel was installed" "--use_compressed_updates"
+
+ if [[ $? -ne 0 ]]; then
+ log "error" "Failed to download and install the test package"
+ return 1
+ fi
+
+ # It's not necessary to set --use_compressed_updates in this run, since that
+ # doesn't impact whether an update is available, but it doesn't hurt and is
+ # more realistic.
+ cycle_cobalt "file:///tests/${TEST_FILE}?channel=test" "${TEST_NAME}.1.log" "App is up to date" "--use_compressed_updates"
+
+ if [[ $? -ne 0 ]]; then
+ log "error" "Failed to run the downloaded installation"
+ return 1
+ fi
+
+ if ! grep -Eq "Evergreen-Compressed" "${LOG_PATH}/${TEST_NAME}.1.log"; then
+ log "error" "According to the user-agent string, the installation run was not compressed"
+ return 1
+ fi
+
+ return 0
+}
diff --git a/starboard/linux/shared/BUILD.gn b/starboard/linux/shared/BUILD.gn
index 8e4a107..ec8ce53 100644
--- a/starboard/linux/shared/BUILD.gn
+++ b/starboard/linux/shared/BUILD.gn
@@ -27,6 +27,7 @@
deps = [
"//third_party/libevent",
+ "//third_party/modp_b64",
"//third_party/opus",
]
@@ -83,6 +84,7 @@
"//starboard/shared/alsa/alsa_util.cc",
"//starboard/shared/alsa/alsa_util.h",
"//starboard/shared/deviceauth/deviceauth_internal.cc",
+ "//starboard/shared/deviceauth/deviceauth_internal.h",
"//starboard/shared/egl/system_egl.cc",
"//starboard/shared/gcc/atomic_gcc_public.h",
"//starboard/shared/gles/system_gles2.cc",
@@ -137,6 +139,10 @@
"//starboard/shared/libevent/socket_waiter_wait.cc",
"//starboard/shared/libevent/socket_waiter_wait_timed.cc",
"//starboard/shared/libevent/socket_waiter_wake_up.cc",
+ "//starboard/shared/libfdkaac/fdk_aac_audio_decoder.cc",
+ "//starboard/shared/libfdkaac/fdk_aac_audio_decoder.h",
+ "//starboard/shared/libfdkaac/libfdkaac_library_loader.cc",
+ "//starboard/shared/libfdkaac/libfdkaac_library_loader.h",
"//starboard/shared/libvpx/vpx_video_decoder.cc",
"//starboard/shared/libvpx/vpx_video_decoder.h",
"//starboard/shared/linux/byte_swap.cc",
@@ -160,6 +166,10 @@
"//starboard/shared/nouser/user_get_property.cc",
"//starboard/shared/nouser/user_get_signed_in.cc",
"//starboard/shared/nouser/user_internal.cc",
+ "//starboard/shared/openh264/openh264_library_loader.cc",
+ "//starboard/shared/openh264/openh264_library_loader.h",
+ "//starboard/shared/openh264/openh264_video_decoder.cc",
+ "//starboard/shared/openh264/openh264_video_decoder.h",
"//starboard/shared/opus/opus_audio_decoder.cc",
"//starboard/shared/opus/opus_audio_decoder.h",
"//starboard/shared/posix/directory_create.cc",
diff --git a/starboard/linux/shared/player_components_factory.cc b/starboard/linux/shared/player_components_factory.cc
index c142f8f..7440b40 100644
--- a/starboard/linux/shared/player_components_factory.cc
+++ b/starboard/linux/shared/player_components_factory.cc
@@ -22,7 +22,11 @@
#include "starboard/shared/ffmpeg/ffmpeg_video_decoder.h"
#include "starboard/shared/libdav1d/dav1d_video_decoder.h"
#include "starboard/shared/libde265/de265_video_decoder.h"
+#include "starboard/shared/libfdkaac/fdk_aac_audio_decoder.h"
+#include "starboard/shared/libfdkaac/libfdkaac_library_loader.h"
#include "starboard/shared/libvpx/vpx_video_decoder.h"
+#include "starboard/shared/openh264/openh264_library_loader.h"
+#include "starboard/shared/openh264/openh264_video_decoder.h"
#include "starboard/shared/opus/opus_audio_decoder.h"
#include "starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.h"
#include "starboard/shared/starboard/player/filter/audio_decoder_internal.h"
@@ -43,6 +47,8 @@
namespace {
+using ::starboard::shared::openh264::is_openh264_supported;
+
class PlayerComponentsFactory : public PlayerComponents::Factory {
public:
bool CreateSubComponents(
@@ -61,6 +67,8 @@
typedef ::starboard::shared::ffmpeg::AudioDecoder FfmpegAudioDecoder;
typedef ::starboard::shared::opus::OpusAudioDecoder OpusAudioDecoder;
+ typedef ::starboard::shared::libfdkaac::FdkAacAudioDecoder
+ FdkAacAudioDecoder;
auto decoder_creator = [](const SbMediaAudioSampleInfo& audio_sample_info,
SbDrmSystem drm_system) {
@@ -70,11 +78,18 @@
if (audio_decoder_impl->is_valid()) {
return audio_decoder_impl.PassAs<AudioDecoder>();
}
+ } else if (audio_sample_info.codec == kSbMediaAudioCodecAac &&
+ audio_sample_info.number_of_channels <=
+ FdkAacAudioDecoder::kMaxChannels &&
+ libfdkaac::LibfdkaacHandle::GetHandle()->IsLoaded()) {
+ SB_LOG(INFO) << "Playing audio using FdkAacAudioDecoder.";
+ return scoped_ptr<AudioDecoder>(new FdkAacAudioDecoder());
} else {
scoped_ptr<FfmpegAudioDecoder> audio_decoder_impl(
FfmpegAudioDecoder::Create(audio_sample_info.codec,
audio_sample_info));
if (audio_decoder_impl && audio_decoder_impl->is_valid()) {
+ SB_LOG(INFO) << "Playing audio using FfmpegAudioDecoder";
return audio_decoder_impl.PassAs<AudioDecoder>();
}
}
@@ -92,6 +107,8 @@
typedef ::starboard::shared::de265::VideoDecoder H265VideoDecoderImpl;
typedef ::starboard::shared::ffmpeg::VideoDecoder FfmpegVideoDecoderImpl;
typedef ::starboard::shared::vpx::VideoDecoder VpxVideoDecoderImpl;
+ typedef ::starboard::shared::openh264::VideoDecoder
+ Openh264VideoDecoderImpl;
const SbTime kVideoSinkRenderInterval = 10 * kSbTimeMillisecond;
@@ -118,6 +135,14 @@
creation_parameters.video_codec(),
creation_parameters.output_mode(),
creation_parameters.decode_target_graphics_context_provider()));
+ } else if ((creation_parameters.video_codec() ==
+ kSbMediaVideoCodecH264) &&
+ is_openh264_supported()) {
+ SB_LOG(INFO) << "Playing video using openh264::VideoDecoder.";
+ video_decoder->reset(new Openh264VideoDecoderImpl(
+ creation_parameters.video_codec(),
+ creation_parameters.output_mode(),
+ creation_parameters.decode_target_graphics_context_provider()));
} else {
scoped_ptr<FfmpegVideoDecoderImpl> ffmpeg_video_decoder(
FfmpegVideoDecoderImpl::Create(
@@ -125,6 +150,7 @@
creation_parameters.output_mode(),
creation_parameters.decode_target_graphics_context_provider()));
if (ffmpeg_video_decoder && ffmpeg_video_decoder->is_valid()) {
+ SB_LOG(INFO) << "Playing video using ffmpeg::VideoDecoder.";
video_decoder->reset(ffmpeg_video_decoder.release());
} else {
SB_LOG(ERROR) << "Failed to create video decoder for codec "
diff --git a/starboard/loader_app/BUILD.gn b/starboard/loader_app/BUILD.gn
index 3ef98fd..71c898c 100644
--- a/starboard/loader_app/BUILD.gn
+++ b/starboard/loader_app/BUILD.gn
@@ -38,22 +38,69 @@
}
}
+if (sb_is_evergreen_compatible && sb_evergreen_compatible_package) {
+ copy("copy_loader_app_content") {
+ install_content = true
+ if (target_cpu == "arm" && arm_float_abi == "softfp") {
+ sources = [ "$root_out_dir/../evergreen-$target_cpu-${arm_float_abi}_$build_type/content" ]
+ } else if (target_cpu == "arm64") {
+ sources = [ "$root_out_dir/../evergreen-$target_cpu_$build_type/content" ]
+ } else {
+ sources = []
+ }
+ outputs = [ "$sb_static_contents_output_data_dir/app/cobalt/content" ]
+ }
+ copy("copy_loader_app_lib") {
+ install_content = true
+ if (target_cpu == "arm" && arm_float_abi == "softfp") {
+ sources = [ "$root_out_dir/../evergreen-$target_cpu-${arm_float_abi}_$build_type/libcobalt.so" ]
+ } else if (target_cpu == "arm64") {
+ sources =
+ [ "$root_out_dir/../evergreen-$target_cpu_$build_type/libcobalt.so" ]
+ } else {
+ sources = []
+ }
+ outputs =
+ [ "$sb_static_contents_output_data_dir/app/cobalt/lib/libcobalt.so" ]
+ }
+}
+
target(final_executable_type, "loader_app") {
if (target_cpu == "x86" || target_cpu == "x64" || target_cpu == "arm" ||
target_cpu == "arm64") {
+ data_deps = [ "//third_party/icu:icudata" ]
+ if (cobalt_font_package == "empty") {
+ data_deps += [ "//cobalt/content/fonts:copy_font_data" ]
+ } else {
+ data_deps += [
+ "//cobalt/content/fonts:copy_fonts",
+ "//cobalt/content/fonts:fonts_xml",
+ ]
+ }
sources = _common_loader_app_sources
deps = [
":common_loader_app_dependencies",
"//cobalt/content/fonts:copy_font_data",
"//starboard/elf_loader",
]
+ if (sb_is_evergreen_compatible && sb_evergreen_compatible_package) {
+ data_deps += [
+ ":copy_loader_app_content",
+ ":copy_loader_app_lib",
+ ]
+ deps += [
+ ":copy_loader_app_content",
+ ":copy_loader_app_lib",
+ ]
+ }
}
}
if (sb_is_evergreen_compatible) {
+ # TODO: b/261635039 enable this target on Android
target(final_executable_type, "loader_app_sys") {
- if (target_cpu == "x86" || target_cpu == "x64" || target_cpu == "arm" ||
- target_cpu == "arm64") {
+ if ((target_cpu == "x86" || target_cpu == "x64" || target_cpu == "arm" ||
+ target_cpu == "arm64") && target_os != "android") {
sources = _common_loader_app_sources
starboard_syms_path =
diff --git a/starboard/nplb/media_can_play_mime_and_key_system_test.cc b/starboard/nplb/media_can_play_mime_and_key_system_test.cc
index 16f8c28..7cb583e 100644
--- a/starboard/nplb/media_can_play_mime_and_key_system_test.cc
+++ b/starboard/nplb/media_can_play_mime_and_key_system_test.cc
@@ -784,13 +784,13 @@
test_sequential_function_calls(
kWarmupQueryParams, SB_ARRAY_SIZE_INT(kWarmupQueryParams),
5 * kSbTimeMillisecond /* 9 calls */, "Warmup queries");
- // First round of the queires.
+ // First round of the queries.
test_sequential_function_calls(
kSdrQueryParams, SB_ARRAY_SIZE_INT(kSdrQueryParams),
10 * kSbTimeMillisecond /* 38 calls */, "SDR queries");
test_sequential_function_calls(
kHdrQueryParams, SB_ARRAY_SIZE_INT(kHdrQueryParams),
- 10 * kSbTimeMillisecond /* 82 calls */, "HDR queries");
+ 15 * kSbTimeMillisecond /* 82 calls */, "HDR queries");
test_sequential_function_calls(
kDrmQueryParams, SB_ARRAY_SIZE_INT(kDrmQueryParams),
10 * kSbTimeMillisecond /* 81 calls */, "DRM queries");
diff --git a/starboard/nplb/player_write_sample_test.cc b/starboard/nplb/player_write_sample_test.cc
index 1eca87f..65b3b19 100644
--- a/starboard/nplb/player_write_sample_test.cc
+++ b/starboard/nplb/player_write_sample_test.cc
@@ -113,9 +113,10 @@
const SbTime timeout = kDefaultWaitForPlayerStateTimeout);
// Player and Decoder methods for driving input and output.
- void WriteSingleInput(size_t index);
+ void WriteSingleBatch(int start_index, int samples_to_write);
void WriteEndOfStream();
- void WriteMultipleInputs(size_t start_index, size_t num_inputs_to_write);
+ void WriteMultipleBatches(size_t start_index,
+ size_t number_of_write_sample_calls);
void DrainOutputs();
int GetNumBuffers() const;
@@ -389,12 +390,26 @@
<< "Did not received expected state.";
}
-void SbPlayerWriteSampleTest::WriteSingleInput(size_t index) {
+void SbPlayerWriteSampleTest::WriteSingleBatch(int start_index,
+ int samples_to_write) {
+ SB_DCHECK(samples_to_write > 0);
+ SB_DCHECK(start_index >= 0);
ASSERT_FALSE(destroy_player_called_);
- ASSERT_LT(index, GetNumBuffers());
- SbPlayerSampleInfo sample_info =
- dmp_reader_->GetPlayerSampleInfo(test_media_type_, index);
- SbPlayerWriteSample2(player_, test_media_type_, &sample_info, 1);
+
+ int max_batch_size =
+ SbPlayerGetMaximumNumberOfSamplesPerWrite(player_, test_media_type_);
+ SB_DCHECK(max_batch_size > 0);
+ samples_to_write = std::min(samples_to_write, max_batch_size);
+
+ SB_DCHECK(start_index + samples_to_write <= GetNumBuffers());
+ // Prepare a batch writing.
+ std::vector<SbPlayerSampleInfo> sample_infos;
+ for (int i = 0; i < samples_to_write; ++i) {
+ sample_infos.push_back(
+ dmp_reader_->GetPlayerSampleInfo(test_media_type_, start_index++));
+ }
+ SbPlayerWriteSample2(player_, test_media_type_, sample_infos.data(),
+ samples_to_write);
}
void SbPlayerWriteSampleTest::WriteEndOfStream() {
@@ -404,20 +419,29 @@
SbPlayerWriteEndOfStream(player_, test_media_type_);
}
-void SbPlayerWriteSampleTest::WriteMultipleInputs(size_t start_index,
- size_t num_inputs_to_write) {
- SB_DCHECK(num_inputs_to_write > 0);
+void SbPlayerWriteSampleTest::WriteMultipleBatches(
+ size_t start_index,
+ size_t number_of_write_sample_calls) {
+ SB_DCHECK(number_of_write_sample_calls > 0);
SB_DCHECK(start_index < GetNumBuffers());
+ ASSERT_FALSE(destroy_player_called_);
- ASSERT_NO_FATAL_FAILURE(WriteSingleInput(start_index));
- ++start_index;
- --num_inputs_to_write;
+ int max_batch_size =
+ SbPlayerGetMaximumNumberOfSamplesPerWrite(player_, test_media_type_);
+ SB_DCHECK(max_batch_size > 0);
- while (num_inputs_to_write > 0 && start_index < GetNumBuffers()) {
+ int num_inputs_to_write = max_batch_size;
+ int sample_index = start_index;
+ for (int i = 0; i < number_of_write_sample_calls; ++i) {
+ if (sample_index + num_inputs_to_write > GetNumBuffers()) {
+ break;
+ }
+ ASSERT_NO_FATAL_FAILURE(
+ WriteSingleBatch(sample_index, num_inputs_to_write));
ASSERT_NO_FATAL_FAILURE(WaitForDecoderStateNeedsData());
- ASSERT_NO_FATAL_FAILURE(WriteSingleInput(start_index));
- ++start_index;
- --num_inputs_to_write;
+ sample_index = sample_index + num_inputs_to_write;
+ num_inputs_to_write =
+ (num_inputs_to_write == 1) ? max_batch_size : num_inputs_to_write - 1;
}
}
@@ -504,17 +528,19 @@
DrainOutputs();
}
-TEST_P(SbPlayerWriteSampleTest, SingleInput) {
- ASSERT_NO_FATAL_FAILURE(WriteSingleInput(0));
+// A single call to write a batch consists of multiple samples.
+TEST_P(SbPlayerWriteSampleTest, WriteSingleBatch) {
+ int max_batch_size =
+ SbPlayerGetMaximumNumberOfSamplesPerWrite(player_, test_media_type_);
+ ASSERT_NO_FATAL_FAILURE(
+ WriteSingleBatch(0, std::min<size_t>(max_batch_size, GetNumBuffers())));
ASSERT_NO_FATAL_FAILURE(WaitForDecoderStateNeedsData());
WriteEndOfStream();
DrainOutputs();
}
-TEST_P(SbPlayerWriteSampleTest, MultipleInputs) {
- ASSERT_NO_FATAL_FAILURE(
- WriteMultipleInputs(0, std::min<size_t>(10, GetNumBuffers())));
- ASSERT_NO_FATAL_FAILURE(WaitForDecoderStateNeedsData());
+TEST_P(SbPlayerWriteSampleTest, WriteMultipleBatches) {
+ ASSERT_NO_FATAL_FAILURE(WriteMultipleBatches(0, 8));
WriteEndOfStream();
DrainOutputs();
}
diff --git a/starboard/nplb/system_get_path_test.cc b/starboard/nplb/system_get_path_test.cc
index 44f4e00..e320737 100644
--- a/starboard/nplb/system_get_path_test.cc
+++ b/starboard/nplb/system_get_path_test.cc
@@ -69,9 +69,6 @@
TEST(SbSystemGetPathTest, ReturnsRequiredPaths) {
BasicTest(kSbSystemPathContentDirectory, true, true, __LINE__);
BasicTest(kSbSystemPathCacheDirectory, true, true, __LINE__);
-#if SB_API_VERSION >= 14
- BasicTest(kSbSystemPathStorageDirectory, true, true, __LINE__);
-#endif // SB_API_VERSION >= 14
}
TEST(SbSystemGetPathTest, FailsGracefullyZeroBufferLength) {
diff --git a/starboard/queue.h b/starboard/queue.h
deleted file mode 100644
index ca22cee..0000000
--- a/starboard/queue.h
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright 2019 The Cobalt Authors. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef STARBOARD_QUEUE_H_
-#define STARBOARD_QUEUE_H_
-
-#error "File moved to //starboard/common/queue.h."
-
-#endif // STARBOARD_QUEUE_H_
diff --git a/starboard/raspi/shared/launcher.py b/starboard/raspi/shared/launcher.py
index 99fa3d2..3f6a2ba 100644
--- a/starboard/raspi/shared/launcher.py
+++ b/starboard/raspi/shared/launcher.py
@@ -104,6 +104,8 @@
signal.signal(signal.SIGINT, functools.partial(_SigIntOrSigTermHandler))
signal.signal(signal.SIGTERM, functools.partial(_SigIntOrSigTermHandler))
+ self.last_run_pexpect_cmd = ''
+
def _InitPexpectCommands(self):
"""Initializes all of the pexpect commands needed for running the test."""
@@ -180,15 +182,15 @@
try:
i = self.pexpect_process.expect(expected_prompts)
if i == 0:
- self.pexpect_process.sendline('yes')
+ self._PexpectSendLine('yes')
elif i == 1:
- self.pexpect_process.sendline(Launcher._RASPI_PASSWORD)
+ self._PexpectSendLine(Launcher._RASPI_PASSWORD)
break
else:
# If any other input comes in, maybe we've logged in with rsa key or
# raspi does not have password. Check if we've logged in by echoing
# a special sentence and expect it back.
- self.pexpect_process.sendline('echo ' + Launcher._SSH_LOGIN_SIGNAL)
+ self._PexpectSendLine('echo ' + Launcher._SSH_LOGIN_SIGNAL)
i = self.pexpect_process.expect([Launcher._SSH_LOGIN_SIGNAL])
break
except pexpect.TIMEOUT:
@@ -200,6 +202,11 @@
if retry_count > Launcher._PEXPECT_PASSWORD_TIMEOUT_MAX_RETRIES:
raise
+ def _PexpectSendLine(self, cmd):
+ """Send lines to Pexpect and record the last command for logging purposes"""
+ self.last_run_pexpect_cmd = cmd
+ self.pexpect_process.sendline(cmd)
+
def _PexpectReadLines(self):
"""Reads all lines from the pexpect process."""
@@ -231,8 +238,8 @@
raise
def _Sleep(self, val):
- self.pexpect_process.sendline('sleep {};echo {}'.format(
- val, Launcher._SSH_SLEEP_SIGNAL))
+ self._PexpectSendLine('sleep {};echo {}'.format(val,
+ Launcher._SSH_SLEEP_SIGNAL))
self.pexpect_process.expect([Launcher._SSH_SLEEP_SIGNAL])
def _CleanupPexpectProcess(self):
@@ -242,16 +249,18 @@
# Check if kernel logged OOM kill or any other system failure message
if self.return_value:
logging.info('Sending dmesg')
- self.pexpect_process.sendline('dmesg -P --color=never | tail -n 100')
+ self._PexpectSendLine('dmesg -P --color=never | tail -n 100')
time.sleep(3)
try:
self.pexpect_process.readlines()
except pexpect.TIMEOUT:
+ logging.info('Timeout exception during cleanup command: %s',
+ self.last_run_pexpect_cmd)
pass
logging.info('Done sending dmesg')
# Send ctrl-c to the raspi and close the process.
- self.pexpect_process.sendline(chr(3))
+ self._PexpectSendLine(chr(3))
time.sleep(1) # Allow a second for normal shutdown
self.pexpect_process.close()
@@ -263,12 +272,14 @@
self.pexpect_process.expect(self._RASPI_PROMPT)
break
except pexpect.TIMEOUT:
+ logging.info('Timeout exception during WaitForPrompt command: %s',
+ self.last_run_pexpect_cmd)
if self.shutdown_initiated.is_set():
return
retry_count -= 1
if not retry_count:
raise
- self.pexpect_process.sendline('echo ' + Launcher._SSH_SLEEP_SIGNAL)
+ self._PexpectSendLine('echo ' + Launcher._SSH_SLEEP_SIGNAL)
time.sleep(self._INTER_COMMAND_DELAY_SECONDS)
def _KillExistingCobaltProcesses(self):
@@ -279,11 +290,11 @@
cause other problems.
"""
logging.info('Killing existing processes')
- self.pexpect_process.sendline(
+ self._PexpectSendLine(
'pkill -9 -ef "(cobalt)|(crashpad_handler)|(elf_loader)"')
self._WaitForPrompt()
# Print the return code of pkill. 0 if a process was halted
- self.pexpect_process.sendline('echo PROCKILL:${?}')
+ self._PexpectSendLine('echo PROCKILL:${?}')
i = self.pexpect_process.expect([r'PROCKILL:0', r'PROCKILL:(\d+)'])
if i == 0:
logging.warning('Forced to pkill existing instance(s) of cobalt. '
@@ -321,10 +332,14 @@
self._PexpectSpawnAndConnect(self.ssh_command)
self._Sleep(self._INTER_COMMAND_DELAY_SECONDS)
# Execute debugging commands on the first run
+ first_run_commands = []
+ if self.test_result_xml_path:
+ first_run_commands.append('touch {}'.format(self.test_result_xml_path))
+ first_run_commands.extend(['free -mh', 'ps -ux', 'df -h'])
if FirstRun():
- for cmd in ['free -mh', 'ps -ux', 'df -h']:
+ for cmd in first_run_commands:
if not self.shutdown_initiated.is_set():
- self.pexpect_process.sendline(cmd)
+ self._PexpectSendLine(cmd)
line = self.pexpect_process.readline()
self.output_file.write(line)
self._WaitForPrompt()
@@ -334,15 +349,18 @@
self._Sleep(self._INTER_COMMAND_DELAY_SECONDS)
if not self.shutdown_initiated.is_set():
- self.pexpect_process.sendline(self.test_command)
+ self._PexpectSendLine(self.test_command)
self._PexpectReadLines()
except pexpect.EOF:
- logging.exception('pexpect encountered EOF while reading line.')
+ logging.exception('pexpect encountered EOF while reading line. (cmd: %s)',
+ self.last_run_pexpect_cmd)
except pexpect.TIMEOUT:
- logging.exception('pexpect timed out while reading line.')
+ logging.exception('pexpect timed out while reading line. (cmd: %s)',
+ self.last_run_pexpect_cmd)
except Exception: # pylint: disable=broad-except
- logging.exception('Error occurred while running test.')
+ logging.exception('Error occurred while running test. (cmd: %s)',
+ self.last_run_pexpect_cmd)
finally:
self._CleanupPexpectProcess()
diff --git a/starboard/raspi/shared/open_max/video_decoder.cc b/starboard/raspi/shared/open_max/video_decoder.cc
index d9ad900..e2afc38 100644
--- a/starboard/raspi/shared/open_max/video_decoder.cc
+++ b/starboard/raspi/shared/open_max/video_decoder.cc
@@ -69,18 +69,19 @@
SB_DCHECK(SbThreadIsValid(thread_));
}
-void VideoDecoder::WriteInputBuffer(
- const scoped_refptr<InputBuffer>& input_buffer) {
- SB_DCHECK(input_buffer);
+void VideoDecoder::WriteInputBuffers(const InputBuffers& input_buffers) {
+ SB_DCHECK(input_buffers.size() == 1);
+ SB_DCHECK(input_buffers[0]);
SB_DCHECK(decoder_status_cb_);
SB_DCHECK(!eos_written_);
first_input_written_ = true;
+ const auto& input_buffer = input_buffers[0];
queue_.Put(new Event(input_buffer));
if (!TryToDeliverOneFrame()) {
SbThreadSleep(kSbTimeMillisecond);
- // Call the callback with NULL frame to ensure that the host know that more
- // data is expected.
+ // Call the callback with NULL frame to ensure that the host knows that
+ // more data is expected.
decoder_status_cb_(kNeedMoreInput, NULL);
}
}
diff --git a/starboard/raspi/shared/open_max/video_decoder.h b/starboard/raspi/shared/open_max/video_decoder.h
index 7dc899e..9965a59 100644
--- a/starboard/raspi/shared/open_max/video_decoder.h
+++ b/starboard/raspi/shared/open_max/video_decoder.h
@@ -50,8 +50,7 @@
size_t GetPrerollFrameCount() const override { return 1; }
SbTime GetPrerollTimeout() const override { return kSbTimeMax; }
size_t GetMaxNumberOfCachedFrames() const override { return 12; }
- void WriteInputBuffer(const scoped_refptr<InputBuffer>& input_buffer)
- override;
+ void WriteInputBuffers(const InputBuffers& input_buffers) override;
void WriteEndOfStream() override;
void Reset() override;
SbDecodeTarget GetCurrentDecodeTarget() override {
diff --git a/starboard/shared/ffmpeg/ffmpeg_audio_decoder_impl.cc b/starboard/shared/ffmpeg/ffmpeg_audio_decoder_impl.cc
index 50cb9bd..4b21032 100644
--- a/starboard/shared/ffmpeg/ffmpeg_audio_decoder_impl.cc
+++ b/starboard/shared/ffmpeg/ffmpeg_audio_decoder_impl.cc
@@ -106,14 +106,16 @@
error_cb_ = error_cb;
}
-void AudioDecoderImpl<FFMPEG>::Decode(
- const scoped_refptr<InputBuffer>& input_buffer,
- const ConsumedCB& consumed_cb) {
+void AudioDecoderImpl<FFMPEG>::Decode(const InputBuffers& input_buffers,
+ const ConsumedCB& consumed_cb) {
SB_DCHECK(BelongsToCurrentThread());
- SB_DCHECK(input_buffer);
+ SB_DCHECK(input_buffers.size() == 1);
+ SB_DCHECK(input_buffers[0]);
SB_DCHECK(output_cb_);
SB_CHECK(codec_context_ != NULL);
+ const auto& input_buffer = input_buffers[0];
+
Schedule(consumed_cb);
if (stream_ended_) {
diff --git a/starboard/shared/ffmpeg/ffmpeg_audio_decoder_impl.h b/starboard/shared/ffmpeg/ffmpeg_audio_decoder_impl.h
index 2378319..9c4cc69 100644
--- a/starboard/shared/ffmpeg/ffmpeg_audio_decoder_impl.h
+++ b/starboard/shared/ffmpeg/ffmpeg_audio_decoder_impl.h
@@ -53,7 +53,7 @@
// From: starboard::player::filter::AudioDecoder
void Initialize(const OutputCB& output_cb, const ErrorCB& error_cb) override;
- void Decode(const scoped_refptr<InputBuffer>& input_buffer,
+ void Decode(const InputBuffers& input_buffers,
const ConsumedCB& consumed_cb) override;
void WriteEndOfStream() override;
scoped_refptr<DecodedAudio> Read(int* samples_per_second) override;
diff --git a/starboard/shared/ffmpeg/ffmpeg_demuxer_impl.cc b/starboard/shared/ffmpeg/ffmpeg_demuxer_impl.cc
index 0e6cf9a..ef15ed2 100644
--- a/starboard/shared/ffmpeg/ffmpeg_demuxer_impl.cc
+++ b/starboard/shared/ffmpeg/ffmpeg_demuxer_impl.cc
@@ -247,6 +247,65 @@
}
}
+// Attempts to parse a codec profile from |extradata|. Upon success,
+// |out_profile| is populated with the profile and true is returned. Upon
+// failure, false is returned and |out_profile| is not modified.
+bool TryParseH264Profile(const uint8_t* extradata,
+ size_t extradata_size,
+ CobaltExtensionDemuxerVideoCodecProfile& out_profile) {
+ if (extradata_size < 2) {
+ return false;
+ }
+ const uint8_t version = extradata[0];
+ if (version != 1) {
+ return false;
+ }
+ const int profile = extradata[1];
+ switch (profile) {
+ case FF_PROFILE_H264_BASELINE:
+ out_profile = kCobaltExtensionDemuxerH264ProfileBaseline;
+ return true;
+ case FF_PROFILE_H264_MAIN:
+ out_profile = kCobaltExtensionDemuxerH264ProfileMain;
+ return true;
+ case FF_PROFILE_H264_EXTENDED:
+ out_profile = kCobaltExtensionDemuxerH264ProfileExtended;
+ return true;
+ case FF_PROFILE_H264_HIGH:
+ out_profile = kCobaltExtensionDemuxerH264ProfileHigh;
+ return true;
+ case FF_PROFILE_H264_HIGH_10:
+ out_profile = kCobaltExtensionDemuxerH264ProfileHigh10Profile;
+ return true;
+ case FF_PROFILE_H264_HIGH_422:
+ out_profile = kCobaltExtensionDemuxerH264ProfileHigh422Profile;
+ return true;
+ case FF_PROFILE_H264_HIGH_444_PREDICTIVE:
+ out_profile = kCobaltExtensionDemuxerH264ProfileHigh444PredictiveProfile;
+ return true;
+ default:
+ SB_LOG(ERROR) << "Unknown H264 profile: " << profile;
+ return false;
+ }
+}
+
+// Attempts to parse a codec profile from |extradata|. Upon success,
+// |out_profile| is populated with the profile and true is returned. Upon
+// failure, false is returned and |out_profile| is not modified.
+bool TryParseH265Profile(const uint8_t* extradata,
+ size_t extradata_size,
+ int& out_profile) {
+ if (extradata_size < 2) {
+ return false;
+ }
+ const uint8_t version = extradata[0];
+ if (version != 1) {
+ return false;
+ }
+ out_profile = extradata[1] & 0x1F;
+ return true;
+}
+
int AVIOReadOperation(void* opaque, uint8_t* buf, int buf_size) {
auto* data_source = static_cast<CobaltExtensionDemuxerDataSource*>(opaque);
const int bytes_read =
@@ -898,8 +957,15 @@
config->profile = ProfileIDToVideoCodecProfile(codec_context->profile);
if (config->profile == kCobaltExtensionDemuxerVideoCodecProfileUnknown &&
codec_context->extradata && codec_context->extradata_size) {
- // TODO(b/231631898): handle the extra data here, if necessary.
- SB_LOG(ERROR) << "Extra data is not currently handled.";
+ CobaltExtensionDemuxerVideoCodecProfile profile =
+ kCobaltExtensionDemuxerVideoCodecProfileUnknown;
+ // Attempt to populate profile based on extradata.
+ if (TryParseH264Profile(codec_context->extradata,
+ codec_context->extradata_size, profile)) {
+ config->profile = profile;
+ } else {
+ SB_LOG(ERROR) << "Could not parse H264 profile from extradata.";
+ }
}
break;
}
@@ -915,8 +981,11 @@
#endif // FF_PROFILE_HEVC_REXT
) &&
codec_context->extradata && codec_context->extradata_size) {
- // TODO(b/231631898): handle the extra data here, if necessary.
- SB_LOG(ERROR) << "Extra data is not currently handled.";
+ // Attempt to populate hevc_profile based on extradata.
+ if (!TryParseH265Profile(codec_context->extradata,
+ codec_context->extradata_size, hevc_profile)) {
+ SB_LOG(ERROR) << "Could not parse H265 profile from extradata.";
+ }
} else {
hevc_profile = codec_context->profile;
}
diff --git a/starboard/shared/ffmpeg/ffmpeg_video_decoder_impl.cc b/starboard/shared/ffmpeg/ffmpeg_video_decoder_impl.cc
index d8605a6..3b6a9bb 100644
--- a/starboard/shared/ffmpeg/ffmpeg_video_decoder_impl.cc
+++ b/starboard/shared/ffmpeg/ffmpeg_video_decoder_impl.cc
@@ -148,12 +148,15 @@
error_cb_ = error_cb;
}
-void VideoDecoderImpl<FFMPEG>::WriteInputBuffer(
- const scoped_refptr<InputBuffer>& input_buffer) {
- SB_DCHECK(input_buffer);
+void VideoDecoderImpl<FFMPEG>::WriteInputBuffers(
+ const InputBuffers& input_buffers) {
+ SB_DCHECK(input_buffers.size() == 1);
+ SB_DCHECK(input_buffers[0]);
SB_DCHECK(queue_.Poll().type == kInvalid);
SB_DCHECK(decoder_status_cb_);
+ const auto& input_buffer = input_buffers[0];
+
if (stream_ended_) {
SB_LOG(ERROR) << "WriteInputFrame() was called after WriteEndOfStream().";
return;
@@ -165,7 +168,6 @@
&VideoDecoderImpl<FFMPEG>::ThreadEntryPoint, this);
SB_DCHECK(SbThreadIsValid(decoder_thread_));
}
-
queue_.Put(Event(input_buffer));
}
@@ -177,7 +179,7 @@
stream_ended_ = true;
if (!SbThreadIsValid(decoder_thread_)) {
- // In case there is no WriteInputBuffer() call before WriteEndOfStream(),
+ // In case there is no WriteInputBuffers() call before WriteEndOfStream(),
// don't create the decoder thread and send the EOS frame directly.
decoder_status_cb_(kBufferFull, VideoFrame::CreateEOSFrame());
return;
diff --git a/starboard/shared/ffmpeg/ffmpeg_video_decoder_impl.h b/starboard/shared/ffmpeg/ffmpeg_video_decoder_impl.h
index 4e3cca0..aa9faab 100644
--- a/starboard/shared/ffmpeg/ffmpeg_video_decoder_impl.h
+++ b/starboard/shared/ffmpeg/ffmpeg_video_decoder_impl.h
@@ -63,8 +63,7 @@
SbTime GetPrerollTimeout() const override { return kSbTimeMax; }
size_t GetMaxNumberOfCachedFrames() const override { return 12; }
- void WriteInputBuffer(
- const scoped_refptr<InputBuffer>& input_buffer) override;
+ void WriteInputBuffers(const InputBuffers& input_buffers) override;
void WriteEndOfStream() override;
void Reset() override;
diff --git a/starboard/shared/libaom/aom_video_decoder.cc b/starboard/shared/libaom/aom_video_decoder.cc
index 605ed98..2170661 100644
--- a/starboard/shared/libaom/aom_video_decoder.cc
+++ b/starboard/shared/libaom/aom_video_decoder.cc
@@ -58,10 +58,10 @@
error_cb_ = error_cb;
}
-void VideoDecoder::WriteInputBuffer(
- const scoped_refptr<InputBuffer>& input_buffer) {
+void VideoDecoder::WriteInputBuffers(const InputBuffers& input_buffers) {
SB_DCHECK(BelongsToCurrentThread());
- SB_DCHECK(input_buffer);
+ SB_DCHECK(input_buffers.size() == 1);
+ SB_DCHECK(input_buffers[0]);
SB_DCHECK(decoder_status_cb_);
if (stream_ended_) {
@@ -74,6 +74,7 @@
SB_DCHECK(decoder_thread_);
}
+ auto input_buffer = input_buffers[0];
decoder_thread_->job_queue()->Schedule(
std::bind(&VideoDecoder::DecodeOneBuffer, this, input_buffer));
}
diff --git a/starboard/shared/libaom/aom_video_decoder.h b/starboard/shared/libaom/aom_video_decoder.h
index 5301023..d551872 100644
--- a/starboard/shared/libaom/aom_video_decoder.h
+++ b/starboard/shared/libaom/aom_video_decoder.h
@@ -48,8 +48,7 @@
SbTime GetPrerollTimeout() const override { return kSbTimeMax; }
size_t GetMaxNumberOfCachedFrames() const override { return 12; }
- void WriteInputBuffer(
- const scoped_refptr<InputBuffer>& input_buffer) override;
+ void WriteInputBuffers(const InputBuffers& input_buffers) override;
void WriteEndOfStream() override;
void Reset() override;
diff --git a/starboard/shared/libdav1d/dav1d_video_decoder.cc b/starboard/shared/libdav1d/dav1d_video_decoder.cc
index dc0f8e7..95a1c08 100644
--- a/starboard/shared/libdav1d/dav1d_video_decoder.cc
+++ b/starboard/shared/libdav1d/dav1d_video_decoder.cc
@@ -15,6 +15,7 @@
#include "starboard/shared/libdav1d/dav1d_video_decoder.h"
#include <string>
+#include <utility>
#include "starboard/common/log.h"
#include "starboard/common/string.h"
@@ -81,14 +82,14 @@
error_cb_ = error_cb;
}
-void VideoDecoder::WriteInputBuffer(
- const scoped_refptr<InputBuffer>& input_buffer) {
+void VideoDecoder::WriteInputBuffers(const InputBuffers& input_buffers) {
SB_DCHECK(BelongsToCurrentThread());
- SB_DCHECK(input_buffer);
+ SB_DCHECK(input_buffers.size() == 1);
+ SB_DCHECK(input_buffers[0]);
SB_DCHECK(decoder_status_cb_);
if (stream_ended_) {
- ReportError("WriteInputBuffer() was called after WriteEndOfStream().");
+ ReportError("WriteInputBuffers() was called after WriteEndOfStream().");
return;
}
@@ -97,6 +98,7 @@
SB_DCHECK(decoder_thread_);
}
+ const auto& input_buffer = input_buffers[0];
decoder_thread_->job_queue()->Schedule(
std::bind(&VideoDecoder::DecodeOneBuffer, this, input_buffer));
}
@@ -110,7 +112,7 @@
stream_ended_ = true;
if (!decoder_thread_) {
- // In case there is no WriteInputBuffer() call before WriteEndOfStream(),
+ // In case there is no WriteInputBuffers() call before WriteEndOfStream(),
// don't create the decoder thread and send the EOS frame directly.
decoder_status_cb_(kBufferFull, VideoFrame::CreateEOSFrame());
return;
@@ -131,7 +133,6 @@
decoder_thread_.reset();
}
- error_occurred_ = false;
stream_ended_ = false;
CancelPendingJobs();
@@ -155,10 +156,13 @@
}
void VideoDecoder::ReportError(const std::string& error_message) {
- SB_DCHECK(decoder_thread_->job_queue()->BelongsToCurrentThread());
+ SB_DCHECK(error_cb_);
- error_occurred_ = true;
- Schedule(std::bind(error_cb_, kSbPlayerErrorDecode, error_message));
+ if (!BelongsToCurrentThread()) {
+ Schedule(std::bind(error_cb_, kSbPlayerErrorDecode, error_message));
+ return;
+ }
+ error_cb_(kSbPlayerErrorDecode, error_message);
}
void VideoDecoder::InitializeCodec() {
diff --git a/starboard/shared/libdav1d/dav1d_video_decoder.h b/starboard/shared/libdav1d/dav1d_video_decoder.h
index 49d590a..fb39ba1 100644
--- a/starboard/shared/libdav1d/dav1d_video_decoder.h
+++ b/starboard/shared/libdav1d/dav1d_video_decoder.h
@@ -49,8 +49,7 @@
SbTime GetPrerollTimeout() const override { return kSbTimeMax; }
size_t GetMaxNumberOfCachedFrames() const override { return 12; }
- void WriteInputBuffer(
- const scoped_refptr<InputBuffer>& input_buffer) override;
+ void WriteInputBuffers(const InputBuffers& input_buffers) override;
void WriteEndOfStream() override;
void Reset() override;
@@ -91,7 +90,6 @@
Dav1dContext* dav1d_context_ = NULL;
bool stream_ended_ = false;
- bool error_occurred_ = false;
// Working thread to avoid lengthy decoding work block the player thread.
scoped_ptr<starboard::player::JobThread> decoder_thread_;
diff --git a/starboard/shared/libde265/de265_video_decoder.cc b/starboard/shared/libde265/de265_video_decoder.cc
index 48a9882..ee8e149 100644
--- a/starboard/shared/libde265/de265_video_decoder.cc
+++ b/starboard/shared/libde265/de265_video_decoder.cc
@@ -54,10 +54,10 @@
error_cb_ = error_cb;
}
-void VideoDecoder::WriteInputBuffer(
- const scoped_refptr<InputBuffer>& input_buffer) {
+void VideoDecoder::WriteInputBuffers(const InputBuffers& input_buffers) {
SB_DCHECK(BelongsToCurrentThread());
- SB_DCHECK(input_buffer);
+ SB_DCHECK(input_buffers.size() == 1);
+ SB_DCHECK(input_buffers[0]);
SB_DCHECK(decoder_status_cb_);
if (stream_ended_) {
@@ -70,6 +70,7 @@
SB_DCHECK(decoder_thread_);
}
+ const auto& input_buffer = input_buffers[0];
decoder_thread_->job_queue()->Schedule(
std::bind(&VideoDecoder::DecodeOneBuffer, this, input_buffer));
}
@@ -83,7 +84,7 @@
stream_ended_ = true;
if (!decoder_thread_) {
- // In case there is no WriteInputBuffer() call before WriteEndOfStream(),
+ // In case there is no WriteInputBuffers() call before WriteEndOfStream(),
// don't create the decoder thread and send the EOS frame directly.
decoder_status_cb_(kBufferFull, VideoFrame::CreateEOSFrame());
return;
diff --git a/starboard/shared/libde265/de265_video_decoder.h b/starboard/shared/libde265/de265_video_decoder.h
index 614b4ab..878b49d 100644
--- a/starboard/shared/libde265/de265_video_decoder.h
+++ b/starboard/shared/libde265/de265_video_decoder.h
@@ -50,8 +50,7 @@
SbTime GetPrerollTimeout() const override { return kSbTimeMax; }
size_t GetMaxNumberOfCachedFrames() const override { return 12; }
- void WriteInputBuffer(
- const scoped_refptr<InputBuffer>& input_buffer) override;
+ void WriteInputBuffers(const InputBuffers& input_buffers) override;
void WriteEndOfStream() override;
void Reset() override;
diff --git a/starboard/shared/libfdkaac/fdk_aac_audio_decoder.cc b/starboard/shared/libfdkaac/fdk_aac_audio_decoder.cc
new file mode 100644
index 0000000..59ffd16
--- /dev/null
+++ b/starboard/shared/libfdkaac/fdk_aac_audio_decoder.cc
@@ -0,0 +1,250 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/shared/libfdkaac/fdk_aac_audio_decoder.h"
+
+#include "starboard/common/log.h"
+#include "starboard/common/string.h"
+#include "starboard/shared/libfdkaac/libfdkaac_library_loader.h"
+
+namespace starboard {
+namespace shared {
+namespace libfdkaac {
+
+FdkAacAudioDecoder::FdkAacAudioDecoder() {
+ static_assert(sizeof(INT_PCM) == sizeof(int16_t),
+ "sizeof(INT_PCM) has to be the same as sizeof(int16_t).");
+ InitializeCodec();
+}
+
+FdkAacAudioDecoder::~FdkAacAudioDecoder() {
+ TeardownCodec();
+}
+
+void FdkAacAudioDecoder::Initialize(const OutputCB& output_cb,
+ const ErrorCB& error_cb) {
+ SB_DCHECK(BelongsToCurrentThread());
+ SB_DCHECK(output_cb);
+ SB_DCHECK(!output_cb_);
+ SB_DCHECK(error_cb);
+ SB_DCHECK(!error_cb_);
+
+ output_cb_ = output_cb;
+ error_cb_ = error_cb;
+}
+
+void FdkAacAudioDecoder::Decode(const InputBuffers& input_buffers,
+ const ConsumedCB& consumed_cb) {
+ SB_DCHECK(BelongsToCurrentThread());
+ SB_DCHECK(input_buffers.size() == 1);
+ SB_DCHECK(input_buffers[0]);
+ SB_DCHECK(output_cb_);
+ SB_DCHECK(decoder_ != NULL);
+
+ if (stream_ended_) {
+ SB_LOG(ERROR) << "Decode() is called after WriteEndOfStream() is called.";
+ return;
+ }
+ const auto& input_buffer = input_buffers[0];
+ if (!WriteToFdkDecoder(input_buffer)) {
+ return;
+ }
+
+ timestamp_queue_.push(input_buffer->timestamp());
+ Schedule(consumed_cb);
+ ReadFromFdkDecoder(kDecodeModeDoNotFlush);
+}
+
+scoped_refptr<FdkAacAudioDecoder::DecodedAudio> FdkAacAudioDecoder::Read(
+ int* samples_per_second) {
+ SB_DCHECK(BelongsToCurrentThread());
+ SB_DCHECK(output_cb_);
+ SB_DCHECK(!decoded_audios_.empty());
+
+ scoped_refptr<DecodedAudio> result;
+ if (!decoded_audios_.empty()) {
+ result = decoded_audios_.front();
+ decoded_audios_.pop();
+ }
+ *samples_per_second = samples_per_second_;
+ return result;
+}
+
+void FdkAacAudioDecoder::Reset() {
+ SB_DCHECK(decoder_ != NULL);
+ SB_DCHECK(BelongsToCurrentThread());
+
+ TeardownCodec();
+ InitializeCodec();
+
+ stream_ended_ = false;
+ decoded_audios_ = std::queue<scoped_refptr<DecodedAudio>>(); // clear
+ partially_decoded_audio_ = nullptr;
+ partially_decoded_audio_data_in_bytes_ = 0;
+ timestamp_queue_ = std::queue<SbTime>(); // clear
+ // Clean up stream information and deduced results.
+ has_stream_info_ = false;
+ num_channels_ = 0;
+ samples_per_second_ = 0;
+ decoded_audio_size_in_bytes_ = 0;
+ audio_data_to_discard_in_bytes_ = 0;
+ CancelPendingJobs();
+}
+
+void FdkAacAudioDecoder::WriteEndOfStream() {
+ SB_DCHECK(decoder_ != NULL);
+ SB_DCHECK(BelongsToCurrentThread());
+ SB_DCHECK(output_cb_);
+
+ while (!timestamp_queue_.empty()) {
+ if (!ReadFromFdkDecoder(kDecodeModeFlush)) {
+ return;
+ }
+ }
+ stream_ended_ = true;
+ // Put EOS into the queue.
+ decoded_audios_.push(new DecodedAudio);
+ Schedule(output_cb_);
+}
+
+void FdkAacAudioDecoder::InitializeCodec() {
+ SB_DCHECK(decoder_ == NULL);
+ decoder_ = aacDecoder_Open(TT_MP4_ADTS, 1);
+ SB_DCHECK(decoder_ != NULL);
+
+ // Set AAC_PCM_MAX_OUTPUT_CHANNELS to 0 to disable downmixing feature.
+ // It makes the decoder output contain the same number of channels as the
+ // encoded bitstream.
+ AAC_DECODER_ERROR error =
+ aacDecoder_SetParam(decoder_, AAC_PCM_MAX_OUTPUT_CHANNELS, 0);
+ SB_DCHECK(error == AAC_DEC_OK);
+}
+
+void FdkAacAudioDecoder::TeardownCodec() {
+ if (decoder_) {
+ aacDecoder_Close(decoder_);
+ decoder_ = nullptr;
+ }
+}
+
+bool FdkAacAudioDecoder::WriteToFdkDecoder(
+ const scoped_refptr<InputBuffer>& input_buffer) {
+ SB_DCHECK(BelongsToCurrentThread());
+ SB_DCHECK(input_buffer);
+
+ unsigned char* data = const_cast<unsigned char*>(input_buffer->data());
+ const unsigned int data_size_in_bytes = input_buffer->size();
+ unsigned int left_to_decode_in_bytes = input_buffer->size();
+ AAC_DECODER_ERROR error = aacDecoder_Fill(
+ decoder_, &data, &data_size_in_bytes, &left_to_decode_in_bytes);
+ if (error != AAC_DEC_OK) {
+ SB_LOG(ERROR) << "aacDecoder_Fill() failed with result : " << error;
+ error_cb_(kSbPlayerErrorDecode,
+ FormatString("aacDecoder_Fill() failed with result %d.", error));
+ return false;
+ }
+
+ // Returned |left_to_decode_in_bytes| should always be 0 as DecodeFrame() will
+ // be called immediately on the same thread.
+ SB_DCHECK(left_to_decode_in_bytes == 0);
+ return true;
+}
+
+bool FdkAacAudioDecoder::ReadFromFdkDecoder(DecodeMode mode) {
+ SB_DCHECK(mode == kDecodeModeFlush || mode == kDecodeModeDoNotFlush);
+ int flags = mode == kDecodeModeFlush ? AACDEC_FLUSH : 0;
+
+ AAC_DECODER_ERROR error = aacDecoder_DecodeFrame(
+ decoder_, reinterpret_cast<INT_PCM*>(output_buffer_),
+ kMaxOutputBufferSizeInBytes / sizeof(INT_PCM), flags);
+ if (error != AAC_DEC_OK) {
+ error_cb_(
+ kSbPlayerErrorDecode,
+ FormatString("aacDecoder_DecodeFrame() failed with result %d.", error));
+ return false;
+ }
+
+ TryToUpdateStreamInfo();
+ SB_DCHECK(has_stream_info_);
+ if (audio_data_to_discard_in_bytes_ >= decoded_audio_size_in_bytes_) {
+ // Discard all decoded data in |output_buffer_|.
+ audio_data_to_discard_in_bytes_ -= decoded_audio_size_in_bytes_;
+ return true;
+ }
+
+ // Discard the initial |audio_data_to_discard_in_bytes_| in |output_buffer_|.
+ int offset_in_bytes = audio_data_to_discard_in_bytes_;
+ audio_data_to_discard_in_bytes_ = 0;
+ TryToOutputDecodedAudio(output_buffer_ + offset_in_bytes,
+ decoded_audio_size_in_bytes_ - offset_in_bytes);
+ return true;
+}
+
+void FdkAacAudioDecoder::TryToUpdateStreamInfo() {
+ if (has_stream_info_) {
+ return;
+ }
+ CStreamInfo* stream_info = aacDecoder_GetStreamInfo(decoder_);
+ SB_DCHECK(stream_info);
+
+ num_channels_ = stream_info->numChannels;
+ samples_per_second_ = stream_info->sampleRate;
+ decoded_audio_size_in_bytes_ =
+ sizeof(int16_t) * stream_info->frameSize * num_channels_;
+ audio_data_to_discard_in_bytes_ =
+ sizeof(int16_t) * stream_info->outputDelay * num_channels_;
+ has_stream_info_ = true;
+}
+
+void FdkAacAudioDecoder::TryToOutputDecodedAudio(const uint8_t* data,
+ int size_in_bytes) {
+ SB_DCHECK(BelongsToCurrentThread());
+ SB_DCHECK(has_stream_info_);
+
+ while (size_in_bytes > 0 && !timestamp_queue_.empty()) {
+ if (!partially_decoded_audio_) {
+ SB_DCHECK(partially_decoded_audio_data_in_bytes_ == 0);
+ partially_decoded_audio_ = new DecodedAudio(
+ num_channels_, kSbMediaAudioSampleTypeInt16Deprecated,
+ kSbMediaAudioFrameStorageTypeInterleaved, timestamp_queue_.front(),
+ decoded_audio_size_in_bytes_);
+ }
+ int freespace = static_cast<int>(partially_decoded_audio_->size()) -
+ partially_decoded_audio_data_in_bytes_;
+ if (size_in_bytes >= freespace) {
+ memcpy(partially_decoded_audio_->buffer() +
+ partially_decoded_audio_data_in_bytes_,
+ data, freespace);
+ data += freespace;
+ size_in_bytes -= freespace;
+ SB_DCHECK(timestamp_queue_.front() ==
+ partially_decoded_audio_->timestamp());
+ timestamp_queue_.pop();
+ decoded_audios_.push(partially_decoded_audio_);
+ Schedule(output_cb_);
+ partially_decoded_audio_ = nullptr;
+ partially_decoded_audio_data_in_bytes_ = 0;
+ continue;
+ }
+ memcpy(partially_decoded_audio_->buffer() +
+ partially_decoded_audio_data_in_bytes_,
+ data, size_in_bytes);
+ partially_decoded_audio_data_in_bytes_ += size_in_bytes;
+ return;
+ }
+}
+
+} // namespace libfdkaac
+} // namespace shared
+} // namespace starboard
diff --git a/starboard/shared/libfdkaac/fdk_aac_audio_decoder.h b/starboard/shared/libfdkaac/fdk_aac_audio_decoder.h
new file mode 100644
index 0000000..98a90c2
--- /dev/null
+++ b/starboard/shared/libfdkaac/fdk_aac_audio_decoder.h
@@ -0,0 +1,101 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_SHARED_LIBFDKAAC_FDK_AAC_AUDIO_DECODER_H_
+#define STARBOARD_SHARED_LIBFDKAAC_FDK_AAC_AUDIO_DECODER_H_
+
+#include <queue>
+
+#include "starboard/common/ref_counted.h"
+#include "starboard/media.h"
+#include "starboard/shared/internal_only.h"
+#include "starboard/shared/starboard/player/decoded_audio_internal.h"
+#include "starboard/shared/starboard/player/filter/audio_decoder_internal.h"
+#include "starboard/shared/starboard/player/job_queue.h"
+#include "third_party/libfdkaac/include/aacdecoder_lib.h"
+
+namespace starboard {
+namespace shared {
+namespace libfdkaac {
+
+class FdkAacAudioDecoder : public starboard::player::filter::AudioDecoder,
+ private starboard::player::JobQueue::JobOwner {
+ public:
+ // The max supportable channels to be decoded for fdk aac is 8.
+ static constexpr int kMaxChannels = 8;
+
+ FdkAacAudioDecoder();
+ ~FdkAacAudioDecoder() override;
+
+ // Overriding functions from starboard::player::filter::AudioDecoder.
+ void Initialize(const OutputCB& output_cb, const ErrorCB& error_cb) override;
+ void Decode(const InputBuffers& input_buffers,
+ const ConsumedCB& consumed_cb) override;
+ scoped_refptr<DecodedAudio> Read(int* samples_per_second) override;
+ void Reset() override;
+ void WriteEndOfStream() override;
+
+ private:
+ // An AAC access unit can contain at most 2048 PCM samples (when it's HE-AAC).
+ static constexpr int kMaxSamplesPerAccessUnit = 2048;
+ // The max bytes required to store a decoded access unit.
+ static constexpr int kMaxOutputBufferSizeInBytes =
+ sizeof(int16_t) * kMaxSamplesPerAccessUnit * kMaxChannels;
+
+ enum DecodeMode {
+ kDecodeModeFlush,
+ kDecodeModeDoNotFlush,
+ };
+
+ void InitializeCodec();
+ void TeardownCodec();
+ bool WriteToFdkDecoder(const scoped_refptr<InputBuffer>& input_buffer);
+ bool ReadFromFdkDecoder(DecodeMode mode);
+ void TryToUpdateStreamInfo();
+ void TryToOutputDecodedAudio(const uint8_t* audio_data, int size_in_bytes);
+
+ OutputCB output_cb_;
+ ErrorCB error_cb_;
+
+ bool stream_ended_ = false;
+ uint8_t output_buffer_[kMaxOutputBufferSizeInBytes];
+
+ std::queue<scoped_refptr<DecodedAudio>> decoded_audios_;
+ // The DecodedAudio being filled up, will be appended to |decoded_audios_|
+ // once it's fully filled (and |output_cb_| will be called).
+ scoped_refptr<DecodedAudio> partially_decoded_audio_;
+ int partially_decoded_audio_data_in_bytes_ = 0;
+ // Keep timestamps for inputs, which will be used to create DecodedAudio.
+ std::queue<SbTime> timestamp_queue_;
+ // libfdkaac related parameters are listed below.
+ HANDLE_AACDECODER decoder_ = nullptr;
+ // There are two quirks of the fdk aac decoder:
+ // 1. Its output parameters (contained in CStreamInfo) are only filled after
+ // the first aacDecoder_DecodeFrame() call.
+ // 2. When filled with N aac access units (i.e. InputBuffer), it will produce
+ // `stream_info->outputDelay + stream_info->frameSize * N` output samples.
+ // The first `outputDelay` samples should be discarded and the remaining
+ // samples contain valid output.
+ bool has_stream_info_ = false;
+ int num_channels_ = 0;
+ int samples_per_second_ = 0;
+ size_t decoded_audio_size_in_bytes_ = 0;
+ // How many bytes of audio output left to be discarded.
+ size_t audio_data_to_discard_in_bytes_ = 0;
+};
+
+} // namespace libfdkaac
+} // namespace shared
+} // namespace starboard
+#endif // STARBOARD_SHARED_LIBFDKAAC_FDK_AAC_AUDIO_DECODER_H_
diff --git a/starboard/shared/libfdkaac/libfdkaac_library_loader.cc b/starboard/shared/libfdkaac/libfdkaac_library_loader.cc
new file mode 100644
index 0000000..dac61d5
--- /dev/null
+++ b/starboard/shared/libfdkaac/libfdkaac_library_loader.cc
@@ -0,0 +1,105 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <dlfcn.h>
+
+#include "starboard/common/log.h"
+#include "starboard/once.h"
+#include "starboard/shared/libfdkaac/libfdkaac_library_loader.h"
+
+namespace starboard {
+namespace shared {
+namespace libfdkaac {
+
+namespace {
+const char kLibfdkaacLibraryName[] = "libfdk-aac.so";
+}
+
+SB_ONCE_INITIALIZE_FUNCTION(LibfdkaacHandle, LibfdkaacHandle::GetHandle);
+
+LibfdkaacHandle::LibfdkaacHandle() {
+ LoadLibrary();
+}
+
+LibfdkaacHandle::~LibfdkaacHandle() {
+ if (handle_) {
+ dlclose(handle_);
+ }
+}
+
+bool LibfdkaacHandle::IsLoaded() const {
+ return handle_;
+}
+
+void LibfdkaacHandle::ReportSymbolError() {
+ SB_LOG(ERROR) << "libfdkaac load error: " << dlerror();
+ dlclose(handle_);
+ handle_ = NULL;
+}
+
+void LibfdkaacHandle::LoadLibrary() {
+ SB_DCHECK(!handle_);
+ handle_ = dlopen(kLibfdkaacLibraryName, RTLD_LAZY);
+ if (!handle_) {
+ return;
+ }
+
+#define INITSYMBOL(symbol) \
+ symbol = reinterpret_cast<decltype(symbol)>(dlsym(handle_, #symbol)); \
+ if (!symbol) { \
+ ReportSymbolError(); \
+ return; \
+ }
+
+ INITSYMBOL(aacDecoder_GetStreamInfo);
+ INITSYMBOL(aacDecoder_Close);
+ INITSYMBOL(aacDecoder_Open);
+ INITSYMBOL(aacDecoder_ConfigRaw);
+ INITSYMBOL(aacDecoder_SetParam);
+ INITSYMBOL(aacDecoder_AncDataInit);
+ INITSYMBOL(aacDecoder_Fill);
+ INITSYMBOL(aacDecoder_DecodeFrame);
+}
+
+CStreamInfo* (*aacDecoder_GetStreamInfo)(HANDLE_AACDECODER self);
+
+void (*aacDecoder_Close)(HANDLE_AACDECODER self);
+
+HANDLE_AACDECODER(*aacDecoder_Open)
+(TRANSPORT_TYPE transportFmt, UINT nrOfLayers);
+
+AAC_DECODER_ERROR(*aacDecoder_ConfigRaw)
+(HANDLE_AACDECODER self, UCHAR* conf[], const UINT length[]);
+
+AAC_DECODER_ERROR(*aacDecoder_SetParam)
+(const HANDLE_AACDECODER self, const AACDEC_PARAM param, const INT value);
+
+AAC_DECODER_ERROR(*aacDecoder_AncDataInit)
+(HANDLE_AACDECODER self, UCHAR* buffer, int size);
+
+AAC_DECODER_ERROR(*aacDecoder_Fill)
+(HANDLE_AACDECODER self,
+ UCHAR* pBuffer[],
+ const UINT bufferSize[],
+ UINT* bytesValid);
+
+AAC_DECODER_ERROR(*aacDecoder_DecodeFrame)
+(HANDLE_AACDECODER self,
+ INT_PCM* pTimeData,
+ const INT timeDataSize,
+ const UINT flags);
+
+} // namespace libfdkaac
+} // namespace shared
+} // namespace starboard
diff --git a/starboard/shared/libfdkaac/libfdkaac_library_loader.h b/starboard/shared/libfdkaac/libfdkaac_library_loader.h
new file mode 100644
index 0000000..43d642e
--- /dev/null
+++ b/starboard/shared/libfdkaac/libfdkaac_library_loader.h
@@ -0,0 +1,76 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_SHARED_LIBFDKAAC_LIBFDKAAC_LIBRARY_LOADER_H_
+#define STARBOARD_SHARED_LIBFDKAAC_LIBFDKAAC_LIBRARY_LOADER_H_
+
+#include "starboard/shared/internal_only.h"
+#include "third_party/libfdkaac/include/aacdecoder_lib.h"
+
+namespace starboard {
+namespace shared {
+namespace libfdkaac {
+
+class LibfdkaacHandle {
+ public:
+ LibfdkaacHandle();
+
+ ~LibfdkaacHandle();
+
+ static LibfdkaacHandle* GetHandle();
+
+ bool IsLoaded() const;
+
+ private:
+ void ReportSymbolError();
+
+ void LoadLibrary();
+
+ void* handle_ = NULL;
+};
+
+extern CStreamInfo* (*aacDecoder_GetStreamInfo)(HANDLE_AACDECODER self);
+
+extern void (*aacDecoder_Close)(HANDLE_AACDECODER self);
+
+extern HANDLE_AACDECODER (*aacDecoder_Open)(TRANSPORT_TYPE transportFmt,
+ UINT nrOfLayers);
+
+extern AAC_DECODER_ERROR (*aacDecoder_ConfigRaw)(HANDLE_AACDECODER self,
+ UCHAR* conf[],
+ const UINT length[]);
+
+extern AAC_DECODER_ERROR (*aacDecoder_SetParam)(const HANDLE_AACDECODER self,
+ const AACDEC_PARAM param,
+ const INT value);
+
+extern AAC_DECODER_ERROR (*aacDecoder_AncDataInit)(HANDLE_AACDECODER self,
+ UCHAR* buffer,
+ int size);
+
+extern AAC_DECODER_ERROR (*aacDecoder_Fill)(HANDLE_AACDECODER self,
+ UCHAR* pBuffer[],
+ const UINT bufferSize[],
+ UINT* bytesValid);
+
+extern AAC_DECODER_ERROR (*aacDecoder_DecodeFrame)(HANDLE_AACDECODER self,
+ INT_PCM* pTimeData,
+ const INT timeDataSize,
+ const UINT flags);
+
+} // namespace libfdkaac
+} // namespace shared
+} // namespace starboard
+
+#endif // STARBOARD_SHARED_LIBFDKAAC_LIBFDKAAC_LIBRARY_LOADER_H_
diff --git a/starboard/shared/libvpx/vpx_video_decoder.cc b/starboard/shared/libvpx/vpx_video_decoder.cc
index 90e0878..d88a762 100644
--- a/starboard/shared/libvpx/vpx_video_decoder.cc
+++ b/starboard/shared/libvpx/vpx_video_decoder.cc
@@ -56,10 +56,10 @@
error_cb_ = error_cb;
}
-void VideoDecoder::WriteInputBuffer(
- const scoped_refptr<InputBuffer>& input_buffer) {
+void VideoDecoder::WriteInputBuffers(const InputBuffers& input_buffers) {
SB_DCHECK(BelongsToCurrentThread());
- SB_DCHECK(input_buffer);
+ SB_DCHECK(input_buffers.size() == 1);
+ SB_DCHECK(input_buffers[0]);
SB_DCHECK(decoder_status_cb_);
if (stream_ended_) {
@@ -72,6 +72,7 @@
SB_DCHECK(decoder_thread_);
}
+ const auto& input_buffer = input_buffers[0];
decoder_thread_->job_queue()->Schedule(
std::bind(&VideoDecoder::DecodeOneBuffer, this, input_buffer));
}
@@ -85,7 +86,7 @@
stream_ended_ = true;
if (!decoder_thread_) {
- // In case there is no WriteInputBuffer() call before WriteEndOfStream(),
+ // In case there is no WriteInputBuffers() call before WriteEndOfStream(),
// don't create the decoder thread and send the EOS frame directly.
decoder_status_cb_(kBufferFull, VideoFrame::CreateEOSFrame());
return;
diff --git a/starboard/shared/libvpx/vpx_video_decoder.h b/starboard/shared/libvpx/vpx_video_decoder.h
index b414374..6fe86b8 100644
--- a/starboard/shared/libvpx/vpx_video_decoder.h
+++ b/starboard/shared/libvpx/vpx_video_decoder.h
@@ -50,8 +50,7 @@
SbTime GetPrerollTimeout() const override { return kSbTimeMax; }
size_t GetMaxNumberOfCachedFrames() const override { return 12; }
- void WriteInputBuffer(
- const scoped_refptr<InputBuffer>& input_buffer) override;
+ void WriteInputBuffers(const InputBuffers& input_buffers) override;
void WriteEndOfStream() override;
void Reset() override;
diff --git a/starboard/shared/openh264/openh264_library_loader.cc b/starboard/shared/openh264/openh264_library_loader.cc
new file mode 100644
index 0000000..e8ccc7c
--- /dev/null
+++ b/starboard/shared/openh264/openh264_library_loader.cc
@@ -0,0 +1,81 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <dlfcn.h>
+
+#include "starboard/common/log.h"
+#include "starboard/shared/openh264/openh264_library_loader.h"
+
+namespace starboard {
+namespace shared {
+namespace openh264 {
+
+namespace {
+
+const char kOpenh264LibraryName[] = "libopenh264.so";
+
+class LibOpenh264Handle {
+ public:
+ LibOpenh264Handle() { LoadLibrary(); }
+ ~LibOpenh264Handle() {
+ if (handle_) {
+ dlclose(handle_);
+ }
+ }
+
+ bool IsLoaded() const { return handle_; }
+
+ private:
+ void ReportSymbolError() {
+ SB_LOG(ERROR) << "Openh264 load error: " << dlerror();
+ dlclose(handle_);
+ handle_ = NULL;
+ }
+
+ void LoadLibrary() {
+ SB_DCHECK(!handle_);
+
+ handle_ = dlopen(kOpenh264LibraryName, RTLD_LAZY);
+ if (!handle_) {
+ return;
+ }
+#define INITSYMBOL(symbol) \
+ symbol = reinterpret_cast<decltype(symbol)>(dlsym(handle_, #symbol)); \
+ if (!symbol) { \
+ ReportSymbolError(); \
+ return; \
+ }
+ INITSYMBOL(WelsCreateDecoder);
+ INITSYMBOL(WelsDestroyDecoder);
+ }
+ void* handle_ = NULL;
+};
+
+LibOpenh264Handle* GetHandle() {
+ static LibOpenh264Handle instance;
+ return &instance;
+}
+} // namespace
+
+int (*WelsCreateDecoder)(ISVCDecoder** ppDecoder);
+
+void (*WelsDestroyDecoder)(ISVCDecoder* pDecoder);
+
+bool is_openh264_supported() {
+ return GetHandle()->IsLoaded();
+}
+
+} // namespace openh264
+} // namespace shared
+} // namespace starboard
diff --git a/starboard/shared/openh264/openh264_library_loader.h b/starboard/shared/openh264/openh264_library_loader.h
new file mode 100644
index 0000000..fee4559
--- /dev/null
+++ b/starboard/shared/openh264/openh264_library_loader.h
@@ -0,0 +1,37 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_SHARED_OPENH264_OPENH264_LIBRARY_LOADER_H_
+#define STARBOARD_SHARED_OPENH264_OPENH264_LIBRARY_LOADER_H_
+
+#include "starboard/shared/internal_only.h"
+#include "third_party/openh264/include/codec_api.h"
+#include "third_party/openh264/include/codec_app_def.h"
+#include "third_party/openh264/include/codec_def.h"
+
+namespace starboard {
+namespace shared {
+namespace openh264 {
+
+bool is_openh264_supported();
+
+extern int (*WelsCreateDecoder)(ISVCDecoder** ppDecoder);
+
+extern void (*WelsDestroyDecoder)(ISVCDecoder* pDecoder);
+
+} // namespace openh264
+} // namespace shared
+} // namespace starboard
+
+#endif // STARBOARD_SHARED_OPENH264_OPENH264_LIBRARY_LOADER_H_
diff --git a/starboard/shared/openh264/openh264_video_decoder.cc b/starboard/shared/openh264/openh264_video_decoder.cc
new file mode 100644
index 0000000..7538611
--- /dev/null
+++ b/starboard/shared/openh264/openh264_video_decoder.cc
@@ -0,0 +1,338 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "starboard/shared/openh264/openh264_video_decoder.h"
+
+#include "starboard/common/log.h"
+#include "starboard/common/string.h"
+#include "starboard/linux/shared/decode_target_internal.h"
+#include "starboard/memory.h"
+#include "starboard/shared/openh264/openh264_library_loader.h"
+#include "starboard/shared/starboard/player/filter/cpu_video_frame.h"
+#include "starboard/shared/starboard/player/job_queue.h"
+
+namespace starboard {
+namespace shared {
+namespace openh264 {
+
+namespace {
+
+using shared::starboard::media::VideoConfig;
+using starboard::player::InputBuffer;
+using starboard::player::JobThread;
+using starboard::player::filter::CpuVideoFrame;
+
+} // namespace
+
+VideoDecoder::VideoDecoder(SbMediaVideoCodec video_codec,
+ SbPlayerOutputMode output_mode,
+ SbDecodeTargetGraphicsContextProvider*
+ decode_target_graphics_context_provider)
+ : output_mode_(output_mode),
+ decode_target_graphics_context_provider_(
+ decode_target_graphics_context_provider) {
+ SB_DCHECK(video_codec == kSbMediaVideoCodecH264);
+}
+
+VideoDecoder::~VideoDecoder() {
+ SB_DCHECK(BelongsToCurrentThread());
+ Reset();
+}
+
+void VideoDecoder::Initialize(const DecoderStatusCB& decoder_status_cb,
+ const ErrorCB& error_cb) {
+ SB_DCHECK(BelongsToCurrentThread());
+ SB_DCHECK(decoder_status_cb);
+ SB_DCHECK(!decoder_status_cb_);
+ SB_DCHECK(error_cb);
+ SB_DCHECK(!error_cb_);
+
+ decoder_status_cb_ = decoder_status_cb;
+ error_cb_ = error_cb;
+}
+
+void VideoDecoder::Reset() {
+ SB_DCHECK(BelongsToCurrentThread());
+
+ if (decoder_thread_) {
+ decoder_thread_->job_queue()->Schedule(
+ std::bind(&VideoDecoder::TeardownCodec, this));
+ // Join the thread to ensure that all callbacks in process are finished.
+ decoder_thread_.reset();
+ }
+
+ video_config_ = nullopt;
+ stream_ended_ = false;
+
+ CancelPendingJobs();
+ frames_being_decoded_ = 0;
+ time_sequential_queue_ = TimeSequentialQueue();
+
+ ScopedLock lock(decode_target_mutex_);
+ frames_ = std::queue<scoped_refptr<CpuVideoFrame>>();
+}
+
+void VideoDecoder::InitializeCodec() {
+ SB_DCHECK(decoder_thread_->job_queue()->BelongsToCurrentThread());
+ SB_DCHECK(!decoder_);
+
+ int result = WelsCreateDecoder(&decoder_);
+ if (result != 0) {
+ decoder_ = nullptr;
+ ReportError(
+ FormatString("WelsCreateDecoder() failed with status %d.", result));
+ return;
+ }
+ SDecodingParam sDecParam;
+ sDecParam.sVideoProperty.eVideoBsType = VIDEO_BITSTREAM_AVC;
+ sDecParam.bParseOnly = false;
+ result = decoder_->Initialize(&sDecParam);
+ if (result != 0) {
+ WelsDestroyDecoder(decoder_);
+ decoder_ = nullptr;
+ ReportError(
+ FormatString("Decoder Initialize() failed with status %d.", result));
+ return;
+ }
+}
+
+void VideoDecoder::TeardownCodec() {
+ SB_DCHECK(decoder_thread_->job_queue()->BelongsToCurrentThread());
+ if (decoder_) {
+ decoder_->Uninitialize();
+ WelsDestroyDecoder(decoder_);
+ decoder_ = nullptr;
+ }
+ if (output_mode_ == kSbPlayerOutputModeDecodeToTexture) {
+ SbDecodeTarget decode_target_to_release;
+ {
+ ScopedLock lock(decode_target_mutex_);
+ decode_target_to_release = decode_target_;
+ decode_target_ = kSbDecodeTargetInvalid;
+ }
+
+ if (SbDecodeTargetIsValid(decode_target_to_release)) {
+ DecodeTargetRelease(decode_target_graphics_context_provider_,
+ decode_target_to_release);
+ }
+ }
+}
+
+void VideoDecoder::WriteInputBuffers(const InputBuffers& input_buffers) {
+ SB_DCHECK(BelongsToCurrentThread());
+ SB_DCHECK(input_buffers.size() == 1);
+ SB_DCHECK(input_buffers[0]);
+ SB_DCHECK(decoder_status_cb_);
+
+ if (stream_ended_) {
+ ReportError("WriteInputBuffer() was called after WriteEndOfStream().");
+ return;
+ }
+ if (!decoder_thread_) {
+ decoder_thread_.reset(new JobThread("openh264_video_decoder"));
+ SB_DCHECK(decoder_thread_);
+ }
+ const auto& input_buffer = input_buffers[0];
+ decoder_thread_->job_queue()->Schedule(
+ std::bind(&VideoDecoder::DecodeOneBuffer, this, input_buffer));
+}
+
+void VideoDecoder::DecodeOneBuffer(
+ const scoped_refptr<InputBuffer>& input_buffer) {
+ SB_DCHECK(decoder_thread_->job_queue()->BelongsToCurrentThread());
+ SB_DCHECK(input_buffer);
+
+ const SbMediaVideoSampleInfo& sample_info = input_buffer->video_sample_info();
+ if (sample_info.is_key_frame) {
+ VideoConfig new_config(sample_info, input_buffer->data(),
+ input_buffer->size());
+ if (!video_config_ || video_config_.value() != new_config) {
+ video_config_ = new_config;
+ if (decoder_) {
+ FlushFrames();
+ }
+ TeardownCodec();
+ InitializeCodec();
+ }
+ }
+ SB_DCHECK(decoder_);
+ unsigned char* decoded_frame[3];
+ SBufferInfo buffer_info;
+ buffer_info.uiInBsTimeStamp = input_buffer->timestamp();
+ DECODING_STATE status = decoder_->DecodeFrameNoDelay(
+ input_buffer->data(), input_buffer->size(), decoded_frame, &buffer_info);
+ if (status != dsErrorFree) {
+ ReportError(
+ FormatString("DecodeFrameNoDelay() failed with status %d.", status));
+ return;
+ }
+ ++frames_being_decoded_;
+
+ if (buffer_info.iBufferStatus == 1) {
+ ProcessDecodedImage(decoded_frame, buffer_info, false);
+ } else {
+ Schedule(std::bind(decoder_status_cb_, kNeedMoreInput, nullptr));
+ }
+}
+
+void VideoDecoder::FlushFrames() {
+ SB_DCHECK(decoder_thread_->job_queue()->BelongsToCurrentThread());
+ SB_DCHECK(decoder_);
+
+ int num_of_frames_in_buffer = 0;
+ unsigned char* decoded_frame[3];
+ SBufferInfo buffer_info;
+ decoder_->GetOption(DECODER_OPTION_NUM_OF_FRAMES_REMAINING_IN_BUFFER,
+ &num_of_frames_in_buffer);
+ for (int i = 0; i < num_of_frames_in_buffer; ++i) {
+ decoder_->FlushFrame(decoded_frame, &buffer_info);
+ if (buffer_info.iBufferStatus == 1) {
+ ProcessDecodedImage(decoded_frame, buffer_info, true);
+ } else {
+ SB_LOG(WARNING) << "Cannot get decoded frame by calling FlushFrame()!";
+ }
+ }
+ if (frames_being_decoded_ != 0) {
+ SB_LOG(WARNING) << "Inconsistency in the number of input/output frames";
+ }
+
+ while (!time_sequential_queue_.empty()) {
+ auto output_frame = time_sequential_queue_.top();
+ if (output_mode_ == kSbPlayerOutputModeDecodeToTexture) {
+ ScopedLock lock(decode_target_mutex_);
+ frames_.push(output_frame);
+ }
+ Schedule(std::bind(decoder_status_cb_, kBufferFull, output_frame));
+ time_sequential_queue_.pop();
+ }
+}
+
+void VideoDecoder::ProcessDecodedImage(unsigned char* decoded_frame[],
+ const SBufferInfo& buffer_info,
+ bool flushing) {
+ SB_DCHECK(decoder_thread_->job_queue()->BelongsToCurrentThread());
+ if (buffer_info.UsrData.sSystemBuffer.iFormat != videoFormatI420) {
+ ReportError(FormatString("Invalid video format %d.",
+ buffer_info.UsrData.sSystemBuffer.iFormat));
+ return;
+ }
+ for (int i = 0; i < 3; i++) {
+ SB_DCHECK(decoded_frame[i]);
+ }
+
+ --frames_being_decoded_;
+ scoped_refptr<CpuVideoFrame> frame = CpuVideoFrame::CreateYV12Frame(
+ kDefaultOpenH264BitsDepth, buffer_info.UsrData.sSystemBuffer.iWidth,
+ buffer_info.UsrData.sSystemBuffer.iHeight,
+ buffer_info.UsrData.sSystemBuffer.iStride[0],
+ buffer_info.UsrData.sSystemBuffer.iStride[1],
+ buffer_info.uiOutYuvTimeStamp, decoded_frame[0], decoded_frame[1],
+ decoded_frame[2]);
+
+ bool has_new_output = false;
+ while (!time_sequential_queue_.empty() &&
+ time_sequential_queue_.top()->timestamp() < frame->timestamp()) {
+ has_new_output = true;
+ auto output_frame = time_sequential_queue_.top();
+ if (output_mode_ == kSbPlayerOutputModeDecodeToTexture) {
+ ScopedLock lock(decode_target_mutex_);
+ frames_.push(output_frame);
+ }
+ if (flushing) {
+ Schedule(std::bind(decoder_status_cb_, kBufferFull, output_frame));
+ } else {
+ Schedule(std::bind(decoder_status_cb_, kNeedMoreInput, output_frame));
+ }
+ time_sequential_queue_.pop();
+ }
+ time_sequential_queue_.push(frame);
+
+ if (!has_new_output) {
+ Schedule(std::bind(decoder_status_cb_, kNeedMoreInput, nullptr));
+ }
+}
+
+void VideoDecoder::WriteEndOfStream() {
+ SB_DCHECK(BelongsToCurrentThread());
+ SB_DCHECK(decoder_status_cb_);
+
+ // We have to flush the decoder to decode the rest frames and to ensure that
+ // Decode() is not called when the stream is ended.
+ stream_ended_ = true;
+
+ if (!decoder_thread_) {
+ // In case there is no WriteInputBuffer() call before WriteEndOfStream(),
+ // don't create the decoder thread and send the EOS frame directly.
+ decoder_status_cb_(kBufferFull, VideoFrame::CreateEOSFrame());
+ return;
+ }
+ decoder_thread_->job_queue()->Schedule(
+ std::bind(&VideoDecoder::DecodeEndOfStream, this));
+}
+
+void VideoDecoder::DecodeEndOfStream() {
+ SB_DCHECK(decoder_thread_->job_queue()->BelongsToCurrentThread());
+ FlushFrames();
+ Schedule(
+ std::bind(decoder_status_cb_, kBufferFull, VideoFrame::CreateEOSFrame()));
+}
+
+void VideoDecoder::UpdateDecodeTarget_Locked(
+ const scoped_refptr<CpuVideoFrame>& frame) {
+ SbDecodeTarget decode_target = DecodeTargetCreate(
+ decode_target_graphics_context_provider_, frame, decode_target_);
+
+ // Lock only after the post to the renderer thread, to prevent deadlock.
+ decode_target_ = decode_target;
+
+ if (!SbDecodeTargetIsValid(decode_target)) {
+ SB_LOG(ERROR) << "Could not acquire a decode target from provider.";
+ }
+}
+
+// When in decode-to-texture mode, this returns the current decoded video frame.
+SbDecodeTarget VideoDecoder::GetCurrentDecodeTarget() {
+ SB_DCHECK(output_mode_ == kSbPlayerOutputModeDecodeToTexture);
+
+ // We must take a lock here since this function can be called from a
+ // separate thread.
+ ScopedLock lock(decode_target_mutex_);
+ while (frames_.size() > 1 && frames_.front()->HasOneRef()) {
+ frames_.pop();
+ }
+ if (!frames_.empty()) {
+ UpdateDecodeTarget_Locked(frames_.front());
+ }
+ if (SbDecodeTargetIsValid(decode_target_)) {
+ // Make a disposable copy, since the state is internally reused by this
+ // class (to avoid recreating GL objects).
+ return DecodeTargetCopy(decode_target_);
+ } else {
+ return kSbDecodeTargetInvalid;
+ }
+}
+
+void VideoDecoder::ReportError(const std::string& error_message) {
+ SB_DCHECK(error_cb_);
+
+ if (!BelongsToCurrentThread()) {
+ Schedule(std::bind(error_cb_, kSbPlayerErrorDecode, error_message));
+ return;
+ }
+ error_cb_(kSbPlayerErrorDecode, error_message);
+}
+
+} // namespace openh264
+} // namespace shared
+} // namespace starboard
diff --git a/starboard/shared/openh264/openh264_video_decoder.h b/starboard/shared/openh264/openh264_video_decoder.h
new file mode 100644
index 0000000..b547b1f
--- /dev/null
+++ b/starboard/shared/openh264/openh264_video_decoder.h
@@ -0,0 +1,137 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef STARBOARD_SHARED_OPENH264_OPENH264_VIDEO_DECODER_H_
+#define STARBOARD_SHARED_OPENH264_OPENH264_VIDEO_DECODER_H_
+
+#include <queue>
+#include <string>
+#include <vector>
+
+#include "starboard/common/optional.h"
+#include "starboard/common/ref_counted.h"
+#include "starboard/decode_target.h"
+#include "starboard/shared/internal_only.h"
+#include "starboard/shared/starboard/media/codec_util.h"
+#include "starboard/shared/starboard/player/filter/cpu_video_frame.h"
+#include "starboard/shared/starboard/player/filter/video_decoder_internal.h"
+#include "starboard/shared/starboard/player/input_buffer_internal.h"
+#include "starboard/shared/starboard/player/job_thread.h"
+#include "third_party/openh264/include/codec_api.h"
+#include "third_party/openh264/include/codec_app_def.h"
+#include "third_party/openh264/include/codec_def.h"
+
+namespace starboard {
+namespace shared {
+namespace openh264 {
+
+class VideoDecoder : public starboard::player::filter::VideoDecoder,
+ private starboard::player::JobQueue::JobOwner {
+ public:
+ VideoDecoder(SbMediaVideoCodec video_codec,
+ SbPlayerOutputMode output_mode,
+ SbDecodeTargetGraphicsContextProvider*
+ decode_target_graphics_context_provider);
+ ~VideoDecoder() override;
+
+ void Initialize(const DecoderStatusCB& decoder_status_cb,
+ const ErrorCB& error_cb) override;
+
+ // TODO: Verify if these values are correct.
+ size_t GetPrerollFrameCount() const override { return 8; }
+ SbTime GetPrerollTimeout() const override { return kSbTimeMax; }
+ size_t GetMaxNumberOfCachedFrames() const override { return 12; }
+
+ void WriteInputBuffers(const InputBuffers& input_buffers) override;
+ void WriteEndOfStream() override;
+ void Reset() override;
+
+ SbDecodeTarget GetCurrentDecodeTarget() override;
+
+ private:
+ static const int kDefaultOpenH264BitsDepth = 8;
+ typedef ::starboard::shared::starboard::player::filter::CpuVideoFrame
+ CpuVideoFrame;
+ // Operator to compare CpuVideoFrame by timestamp.
+ struct VideoFrameTimeStampGreater {
+ bool operator()(const scoped_refptr<CpuVideoFrame>& left,
+ const scoped_refptr<CpuVideoFrame>& right) const {
+ // In chronological order.
+ return left->timestamp() > right->timestamp();
+ }
+ };
+ typedef std::priority_queue<scoped_refptr<CpuVideoFrame>,
+ std::vector<scoped_refptr<CpuVideoFrame>>,
+ VideoFrameTimeStampGreater>
+ TimeSequentialQueue;
+
+ void UpdateDecodeTarget_Locked(const scoped_refptr<CpuVideoFrame>& frame);
+
+ // The following functions are only called on the decoder thread.
+ void InitializeCodec();
+ void TeardownCodec();
+ void DecodeOneBuffer(const scoped_refptr<InputBuffer>& input_buffer);
+ void DecodeEndOfStream();
+ void ProcessDecodedImage(unsigned char* decoded_frame[],
+ const SBufferInfo& buffer_info,
+ bool flushing);
+ void FlushFrames();
+ void ReportError(const std::string& error_message);
+
+ SbDecodeTargetGraphicsContextProvider*
+ decode_target_graphics_context_provider_;
+ SbPlayerOutputMode output_mode_;
+
+ // The following callbacks will be initialized in Initialize() and won't be
+ // changed during the life time of this class.
+ DecoderStatusCB decoder_status_cb_;
+ ErrorCB error_cb_;
+
+ // Openh264 does NOT always output video frames in chronological order.
+ // |time_sequential_queue_| is used to reorder |CpuVideoFrame|
+ // chronologically.
+ TimeSequentialQueue time_sequential_queue_;
+
+ std::queue<scoped_refptr<CpuVideoFrame>> frames_;
+
+ bool stream_ended_ = false;
+
+ // If decode-to-texture is enabled, then we store the decode target texture
+ // inside of this |decode_target_| member.
+ SbDecodeTarget decode_target_ = kSbDecodeTargetInvalid;
+
+ // GetCurrentDecodeTarget() needs to be called from an arbitrary thread
+ // to obtain the current decode target (which ultimately ends up being a
+ // copy of |decode_target_|), we need to safe-guard access to |decode_target_|
+ // and we do so through this mutex.
+ Mutex decode_target_mutex_;
+
+ // Working thread to avoid lengthy decoding work block the player thread.
+ scoped_ptr<starboard::player::JobThread> decoder_thread_;
+
+ // Openh264 decode handler.
+ ISVCDecoder* decoder_ = nullptr;
+
+ // The number of frames which have been sent to decoder but not received yet.
+ int frames_being_decoded_ = 0;
+
+ // Store current avc level profile and resolution.
+ optional<shared::starboard::media::VideoConfig> video_config_;
+};
+
+} // namespace openh264
+} // namespace shared
+} // namespace starboard
+
+#endif // STARBOARD_SHARED_OPENH264_OPENH264_VIDEO_DECODER_H_
diff --git a/starboard/shared/opus/opus_audio_decoder.cc b/starboard/shared/opus/opus_audio_decoder.cc
index 0ac7a17..4d6867f 100644
--- a/starboard/shared/opus/opus_audio_decoder.cc
+++ b/starboard/shared/opus/opus_audio_decoder.cc
@@ -87,10 +87,10 @@
error_cb_ = error_cb;
}
-void OpusAudioDecoder::Decode(const scoped_refptr<InputBuffer>& input_buffer,
+void OpusAudioDecoder::Decode(const InputBuffers& input_buffers,
const ConsumedCB& consumed_cb) {
SB_DCHECK(BelongsToCurrentThread());
- SB_DCHECK(input_buffer);
+ SB_DCHECK(!input_buffers.empty());
SB_DCHECK(output_cb_);
if (stream_ended_) {
@@ -98,6 +98,21 @@
return;
}
+ for (const auto& input_buffer : input_buffers) {
+ if (!DecodeInternal(input_buffer)) {
+ return;
+ }
+ }
+ Schedule(consumed_cb);
+}
+
+bool OpusAudioDecoder::DecodeInternal(
+ const scoped_refptr<InputBuffer>& input_buffer) {
+ SB_DCHECK(BelongsToCurrentThread());
+ SB_DCHECK(input_buffer);
+ SB_DCHECK(output_cb_);
+ SB_DCHECK(!stream_ended_);
+
scoped_refptr<DecodedAudio> decoded_audio = new DecodedAudio(
audio_sample_info_.number_of_channels, GetSampleType(),
kSbMediaAudioFrameStorageTypeInterleaved, input_buffer->timestamp(),
@@ -122,8 +137,7 @@
frames_per_au_ < kMaxOpusFramesPerAU) {
frames_per_au_ = kMaxOpusFramesPerAU;
// Send to decode again with the new |frames_per_au_|.
- Decode(input_buffer, consumed_cb);
- return;
+ return DecodeInternal(input_buffer);
}
if (decoded_frames <= 0) {
// When the following check fails, it indicates that |frames_per_au_| is
@@ -137,7 +151,7 @@
error_cb_(kSbPlayerErrorDecode,
FormatString("%s() failed with error code: %d",
kDecodeFunctionName, decoded_frames));
- return;
+ return false;
}
frames_per_au_ = decoded_frames;
@@ -146,8 +160,8 @@
starboard::media::GetBytesPerSample(GetSampleType()));
decoded_audios_.push(decoded_audio);
- Schedule(consumed_cb);
- Schedule(output_cb_);
+ output_cb_();
+ return true;
}
void OpusAudioDecoder::WriteEndOfStream() {
diff --git a/starboard/shared/opus/opus_audio_decoder.h b/starboard/shared/opus/opus_audio_decoder.h
index c878412..09687f3 100644
--- a/starboard/shared/opus/opus_audio_decoder.h
+++ b/starboard/shared/opus/opus_audio_decoder.h
@@ -44,13 +44,14 @@
// AudioDecoder functions
void Initialize(const OutputCB& output_cb, const ErrorCB& error_cb) override;
- void Decode(const scoped_refptr<InputBuffer>& input_buffer,
+ void Decode(const InputBuffers& input_buffers,
const ConsumedCB& consumed_cb) override;
void WriteEndOfStream() override;
scoped_refptr<DecodedAudio> Read(int* samples_per_second) override;
void Reset() override;
private:
+ bool DecodeInternal(const scoped_refptr<InputBuffer>& input_buffer);
static const int kMaxOpusFramesPerAU = 9600;
SbMediaAudioSampleType GetSampleType() const;
diff --git a/starboard/shared/starboard/media/mime_util.cc b/starboard/shared/starboard/media/mime_util.cc
index 6c53aa7..c8656b1 100644
--- a/starboard/shared/starboard/media/mime_util.cc
+++ b/starboard/shared/starboard/media/mime_util.cc
@@ -156,7 +156,8 @@
std::string cryptoblockformat =
mime_type.GetParamStringValue("cryptoblockformat", "");
if (!cryptoblockformat.empty()) {
- if (mime_type.subtype() != "webm" || cryptoblockformat != "subsample") {
+ if ((mime_type.subtype() != "mp4" && mime_type.subtype() != "webm") ||
+ cryptoblockformat != "subsample") {
return false;
}
}
diff --git a/starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.cc b/starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.cc
index 644ae2f..664544c 100644
--- a/starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.cc
+++ b/starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.cc
@@ -14,6 +14,8 @@
#include "starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.h"
+#include <utility>
+
#include "starboard/audio_sink.h"
#include "starboard/common/log.h"
#include "starboard/common/reset_and_return.h"
@@ -59,35 +61,47 @@
error_cb_ = error_cb;
}
-void AdaptiveAudioDecoder::Decode(
- const scoped_refptr<InputBuffer>& input_buffer,
- const ConsumedCB& consumed_cb) {
+void AdaptiveAudioDecoder::Decode(const InputBuffers& input_buffers,
+ const ConsumedCB& consumed_cb) {
SB_DCHECK(BelongsToCurrentThread());
SB_DCHECK(!stream_ended_);
SB_DCHECK(output_cb_);
SB_DCHECK(error_cb_);
SB_DCHECK(!flushing_);
- SB_DCHECK(!pending_input_buffer_);
+ SB_DCHECK(pending_input_buffers_.empty());
SB_DCHECK(!pending_consumed_cb_);
- SB_DCHECK(input_buffer->sample_type() == kSbMediaTypeAudio);
- SB_DCHECK(input_buffer->audio_sample_info().codec != kSbMediaAudioCodecNone);
+ SB_DCHECK(!input_buffers.empty());
+ SB_DCHECK(input_buffers.front()->sample_type() == kSbMediaTypeAudio);
+ SB_DCHECK(input_buffers.front()->audio_sample_info().codec !=
+ kSbMediaAudioCodecNone);
if (!audio_decoder_) {
- InitializeAudioDecoder(input_buffer->audio_sample_info());
+ InitializeAudioDecoder(input_buffers.front()->audio_sample_info());
if (audio_decoder_) {
- audio_decoder_->Decode(input_buffer, consumed_cb);
+ audio_decoder_->Decode(input_buffers, consumed_cb);
}
return;
}
if (starboard::media::IsAudioSampleInfoSubstantiallyDifferent(
- input_audio_sample_info_, input_buffer->audio_sample_info())) {
+ input_audio_sample_info_,
+ input_buffers.front()->audio_sample_info())) {
flushing_ = true;
- pending_input_buffer_ = input_buffer;
+ pending_input_buffers_ = input_buffers;
pending_consumed_cb_ = consumed_cb;
audio_decoder_->WriteEndOfStream();
- } else {
- audio_decoder_->Decode(input_buffer, consumed_cb);
+ return;
}
+#if !defined(COBALT_BUILD_TYPE_GOLD)
+ for (int i = 1; i < input_buffers.size(); i++) {
+ if (starboard::media::IsAudioSampleInfoSubstantiallyDifferent(
+ input_audio_sample_info_, input_buffers[i]->audio_sample_info())) {
+ error_cb_(kSbPlayerErrorDecode,
+ "Configuration switches should NOT happen within a batch.");
+ return;
+ }
+ }
+#endif
+ audio_decoder_->Decode(input_buffers, consumed_cb);
}
void AdaptiveAudioDecoder::WriteEndOfStream() {
@@ -95,7 +109,7 @@
SB_DCHECK(!stream_ended_);
SB_DCHECK(output_cb_);
SB_DCHECK(error_cb_);
- SB_DCHECK(!pending_input_buffer_);
+ SB_DCHECK(pending_input_buffers_.empty());
SB_DCHECK(!pending_consumed_cb_);
stream_ended_ = true;
@@ -139,7 +153,7 @@
while (!decoded_audios_.empty()) {
decoded_audios_.pop();
}
- pending_input_buffer_ = nullptr;
+ pending_input_buffers_.clear();
pending_consumed_cb_ = nullptr;
flushing_ = false;
stream_ended_ = false;
@@ -213,8 +227,8 @@
SB_DCHECK(audio_decoder_);
TeardownAudioDecoder();
flushing_ = false;
- Decode(ResetAndReturn(&pending_input_buffer_),
- ResetAndReturn(&pending_consumed_cb_));
+ InputBuffers input_buffers = std::move(pending_input_buffers_);
+ Decode(input_buffers, ResetAndReturn(&pending_consumed_cb_));
} else {
SB_DCHECK(stream_ended_);
decoded_audios_.push(decoded_audio);
diff --git a/starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.h b/starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.h
index 8a997a8..0932965 100644
--- a/starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.h
+++ b/starboard/shared/starboard/player/filter/adaptive_audio_decoder_internal.h
@@ -55,7 +55,7 @@
~AdaptiveAudioDecoder() override;
void Initialize(const OutputCB& output_cb, const ErrorCB& error_cb) override;
- void Decode(const scoped_refptr<InputBuffer>& input_buffer,
+ void Decode(const InputBuffers& input_buffers,
const ConsumedCB& consumed_cb) override;
void WriteEndOfStream() override;
scoped_refptr<DecodedAudio> Read(int* samples_per_second) override;
@@ -82,7 +82,7 @@
scoped_ptr<filter::AudioDecoder> audio_decoder_;
scoped_ptr<filter::AudioResampler> resampler_;
scoped_ptr<filter::AudioChannelLayoutMixer> channel_mixer_;
- scoped_refptr<InputBuffer> pending_input_buffer_;
+ InputBuffers pending_input_buffers_;
ConsumedCB pending_consumed_cb_;
std::queue<scoped_refptr<DecodedAudio>> decoded_audios_;
bool flushing_ = false;
diff --git a/starboard/shared/starboard/player/filter/audio_decoder_internal.h b/starboard/shared/starboard/player/filter/audio_decoder_internal.h
index 6d4fca9..4e59f55 100644
--- a/starboard/shared/starboard/player/filter/audio_decoder_internal.h
+++ b/starboard/shared/starboard/player/filter/audio_decoder_internal.h
@@ -40,6 +40,7 @@
typedef ::starboard::shared::starboard::player::DecodedAudio DecodedAudio;
typedef ::starboard::shared::starboard::player::InputBuffer InputBuffer;
+ typedef ::starboard::shared::starboard::player::InputBuffers InputBuffers;
virtual ~AudioDecoder() {}
@@ -52,12 +53,12 @@
virtual void Initialize(const OutputCB& output_cb,
const ErrorCB& error_cb) = 0;
- // Decode the encoded audio data stored in |input_buffer|. Whenever the input
+ // Decode the encoded audio data stored in |input_buffers|. Whenever the input
// is consumed and the decoder is ready to accept a new input, it calls
// |consumed_cb|.
// Note that |consumed_cb| is always called asynchronously on the calling job
// queue.
- virtual void Decode(const scoped_refptr<InputBuffer>& input_buffer,
+ virtual void Decode(const InputBuffers& input_buffer,
const ConsumedCB& consumed_cb) = 0;
// Notice the object that there is no more input data unless Reset() is
diff --git a/starboard/shared/starboard/player/filter/audio_renderer_internal.h b/starboard/shared/starboard/player/filter/audio_renderer_internal.h
index 4368a45..ab00627 100644
--- a/starboard/shared/starboard/player/filter/audio_renderer_internal.h
+++ b/starboard/shared/starboard/player/filter/audio_renderer_internal.h
@@ -31,13 +31,14 @@
typedef ::starboard::shared::starboard::player::filter::PrerolledCB
PrerolledCB;
typedef ::starboard::shared::starboard::player::InputBuffer InputBuffer;
+ typedef ::starboard::shared::starboard::player::InputBuffers InputBuffers;
virtual ~AudioRenderer() {}
virtual void Initialize(const ErrorCB& error_cb,
const PrerolledCB& prerolled_cb,
const EndedCB& ended_cb) = 0;
- virtual void WriteSample(const scoped_refptr<InputBuffer>& input_buffer) = 0;
+ virtual void WriteSamples(const InputBuffers& input_buffers) = 0;
virtual void WriteEndOfStream() = 0;
virtual void SetVolume(double volume) = 0;
diff --git a/starboard/shared/starboard/player/filter/audio_renderer_internal_pcm.cc b/starboard/shared/starboard/player/filter/audio_renderer_internal_pcm.cc
index 709cd8b..3e09554 100644
--- a/starboard/shared/starboard/player/filter/audio_renderer_internal_pcm.cc
+++ b/starboard/shared/starboard/player/filter/audio_renderer_internal_pcm.cc
@@ -124,21 +124,20 @@
error_cb);
}
-void AudioRendererPcm::WriteSample(
- const scoped_refptr<InputBuffer>& input_buffer) {
+void AudioRendererPcm::WriteSamples(const InputBuffers& input_buffers) {
SB_DCHECK(BelongsToCurrentThread());
- SB_DCHECK(input_buffer);
+ SB_DCHECK(!input_buffers.empty());
SB_DCHECK(can_accept_more_data_);
if (eos_state_ >= kEOSWrittenToDecoder) {
- SB_LOG(ERROR) << "Appending audio sample at " << input_buffer->timestamp()
- << " after EOS reached.";
+ SB_LOG(ERROR) << "Appending audio samples from "
+ << input_buffers.front()->timestamp() << " to "
+ << input_buffers.back()->timestamp() << " after EOS reached.";
return;
}
can_accept_more_data_ = false;
-
- decoder_->Decode(input_buffer,
+ decoder_->Decode(input_buffers,
std::bind(&AudioRendererPcm::OnDecoderConsumed, this));
first_input_written_ = true;
}
diff --git a/starboard/shared/starboard/player/filter/audio_renderer_internal_pcm.h b/starboard/shared/starboard/player/filter/audio_renderer_internal_pcm.h
index 7c37d7b..ed3b734 100644
--- a/starboard/shared/starboard/player/filter/audio_renderer_internal_pcm.h
+++ b/starboard/shared/starboard/player/filter/audio_renderer_internal_pcm.h
@@ -76,7 +76,7 @@
void Initialize(const ErrorCB& error_cb,
const PrerolledCB& prerolled_cb,
const EndedCB& ended_cb) override;
- void WriteSample(const scoped_refptr<InputBuffer>& input_buffer) override;
+ void WriteSamples(const InputBuffers& input_buffers) override;
void WriteEndOfStream() override;
void SetVolume(double volume) override;
diff --git a/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.cc b/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.cc
index b5d1d59..a632764 100644
--- a/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.cc
+++ b/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.cc
@@ -14,6 +14,8 @@
#include "starboard/shared/starboard/player/filter/filter_based_player_worker_handler.h"
+#include <utility>
+
#include "starboard/audio_sink.h"
#include "starboard/common/log.h"
#include "starboard/common/murmurhash2.h"
@@ -207,81 +209,90 @@
return true;
}
-bool FilterBasedPlayerWorkerHandler::WriteSample(
- const scoped_refptr<InputBuffer>& input_buffer,
- bool* written) {
- SB_DCHECK(input_buffer);
+bool FilterBasedPlayerWorkerHandler::WriteSamples(
+ const InputBuffers& input_buffers,
+ int* samples_written) {
+ SB_DCHECK(!input_buffers.empty());
SB_DCHECK(BelongsToCurrentThread());
- SB_DCHECK(written != NULL);
+ SB_DCHECK(samples_written != NULL);
+ for (const auto& input_buffer : input_buffers) {
+ SB_DCHECK(input_buffer);
+ }
- if (input_buffer->sample_type() == kSbMediaTypeAudio) {
+ *samples_written = 0;
+ if (input_buffers.front()->sample_type() == kSbMediaTypeAudio) {
if (!audio_renderer_) {
return false;
}
- *written = true;
-
if (audio_renderer_->IsEndOfStreamWritten()) {
SB_LOG(WARNING) << "Try to write audio sample after EOS is reached";
} else {
if (!audio_renderer_->CanAcceptMoreData()) {
- *written = false;
return true;
}
-
- if (input_buffer->drm_info()) {
- if (!SbDrmSystemIsValid(drm_system_)) {
- return false;
+ for (const auto& input_buffer : input_buffers) {
+ if (input_buffer->drm_info()) {
+ if (!SbDrmSystemIsValid(drm_system_)) {
+ return false;
+ }
+ DumpInputHash(input_buffer);
+ SbDrmSystemPrivate::DecryptStatus decrypt_status =
+ drm_system_->Decrypt(input_buffer);
+ if (decrypt_status == SbDrmSystemPrivate::kRetry) {
+ if (*samples_written > 0) {
+ audio_renderer_->WriteSamples(
+ InputBuffers(input_buffers.begin(),
+ input_buffers.begin() + *samples_written));
+ }
+ return true;
+ }
+ if (decrypt_status == SbDrmSystemPrivate::kFailure) {
+ return false;
+ }
}
DumpInputHash(input_buffer);
- SbDrmSystemPrivate::DecryptStatus decrypt_status =
- drm_system_->Decrypt(input_buffer);
- if (decrypt_status == SbDrmSystemPrivate::kRetry) {
- *written = false;
- return true;
- }
- if (decrypt_status == SbDrmSystemPrivate::kFailure) {
- *written = false;
- return false;
- }
+ ++*samples_written;
}
- DumpInputHash(input_buffer);
- audio_renderer_->WriteSample(input_buffer);
+ audio_renderer_->WriteSamples(input_buffers);
}
} else {
- SB_DCHECK(input_buffer->sample_type() == kSbMediaTypeVideo);
+ SB_DCHECK(input_buffers.front()->sample_type() == kSbMediaTypeVideo);
if (!video_renderer_) {
return false;
}
- *written = true;
-
if (video_renderer_->IsEndOfStreamWritten()) {
SB_LOG(WARNING) << "Try to write video sample after EOS is reached";
} else {
if (!video_renderer_->CanAcceptMoreData()) {
- *written = false;
return true;
}
- if (input_buffer->drm_info()) {
- if (!SbDrmSystemIsValid(drm_system_)) {
- return false;
+ for (const auto& input_buffer : input_buffers) {
+ if (input_buffer->drm_info()) {
+ if (!SbDrmSystemIsValid(drm_system_)) {
+ return false;
+ }
+ DumpInputHash(input_buffer);
+ SbDrmSystemPrivate::DecryptStatus decrypt_status =
+ drm_system_->Decrypt(input_buffer);
+ if (decrypt_status == SbDrmSystemPrivate::kRetry) {
+ if (*samples_written > 0) {
+ video_renderer_->WriteSamples(
+ InputBuffers(input_buffers.begin(),
+ input_buffers.begin() + *samples_written));
+ }
+ return true;
+ }
+ if (decrypt_status == SbDrmSystemPrivate::kFailure) {
+ return false;
+ }
}
DumpInputHash(input_buffer);
- SbDrmSystemPrivate::DecryptStatus decrypt_status =
- drm_system_->Decrypt(input_buffer);
- if (decrypt_status == SbDrmSystemPrivate::kRetry) {
- *written = false;
- return true;
- }
- if (decrypt_status == SbDrmSystemPrivate::kFailure) {
- *written = false;
- return false;
- }
+ ++*samples_written;
}
- DumpInputHash(input_buffer);
- video_renderer_->WriteSample(input_buffer);
+ video_renderer_->WriteSamples(input_buffers);
}
}
diff --git a/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.h b/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.h
index d6c56b3..17df66d 100644
--- a/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.h
+++ b/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.h
@@ -54,8 +54,8 @@
UpdatePlayerStateCB update_player_state_cb,
UpdatePlayerErrorCB update_player_error_cb) override;
bool Seek(SbTime seek_to_time, int ticket) override;
- bool WriteSample(const scoped_refptr<InputBuffer>& input_buffer,
- bool* written) override;
+ bool WriteSamples(const InputBuffers& input_buffers,
+ int* samples_written) override;
bool WriteEndOfStream(SbMediaType sample_type) override;
bool SetPause(bool pause) override;
bool SetPlaybackRate(double playback_rate) override;
diff --git a/starboard/shared/starboard/player/filter/mock_audio_decoder.h b/starboard/shared/starboard/player/filter/mock_audio_decoder.h
index c8b120c..5af346e 100644
--- a/starboard/shared/starboard/player/filter/mock_audio_decoder.h
+++ b/starboard/shared/starboard/player/filter/mock_audio_decoder.h
@@ -40,8 +40,7 @@
int samples_per_second) {}
MOCK_METHOD2(Initialize, void(const OutputCB&, const ErrorCB&));
- MOCK_METHOD2(Decode,
- void(const scoped_refptr<InputBuffer>&, const ConsumedCB&));
+ MOCK_METHOD2(Decode, void(const InputBuffers&, const ConsumedCB&));
MOCK_METHOD0(WriteEndOfStream, void());
MOCK_METHOD1(Read, scoped_refptr<DecodedAudio>(int*));
MOCK_METHOD0(Reset, void());
diff --git a/starboard/shared/starboard/player/filter/stub_audio_decoder.cc b/starboard/shared/starboard/player/filter/stub_audio_decoder.cc
index 3bad2d9..b406e40 100644
--- a/starboard/shared/starboard/player/filter/stub_audio_decoder.cc
+++ b/starboard/shared/starboard/player/filter/stub_audio_decoder.cc
@@ -51,16 +51,19 @@
error_cb_ = error_cb;
}
-void StubAudioDecoder::Decode(const scoped_refptr<InputBuffer>& input_buffer,
+void StubAudioDecoder::Decode(const InputBuffers& input_buffers,
const ConsumedCB& consumed_cb) {
SB_DCHECK(BelongsToCurrentThread());
- SB_DCHECK(input_buffer);
+ SB_DCHECK(!input_buffers.empty());
+ for (const auto& input_buffer : input_buffers) {
+ SB_DCHECK(input_buffer);
+ }
if (!decoder_thread_) {
decoder_thread_.reset(new JobThread("stub_audio_decoder"));
}
decoder_thread_->job_queue()->Schedule(std::bind(
- &StubAudioDecoder::DecodeOneBuffer, this, input_buffer, consumed_cb));
+ &StubAudioDecoder::DecodeBuffers, this, input_buffers, consumed_cb));
}
void StubAudioDecoder::WriteEndOfStream() {
@@ -99,11 +102,17 @@
CancelPendingJobs();
}
-void StubAudioDecoder::DecodeOneBuffer(
- const scoped_refptr<InputBuffer>& input_buffer,
- const ConsumedCB& consumed_cb) {
+void StubAudioDecoder::DecodeBuffers(const InputBuffers& input_buffers,
+ const ConsumedCB& consumed_cb) {
SB_DCHECK(decoder_thread_->job_queue()->BelongsToCurrentThread());
+ for (const auto& input_buffer : input_buffers) {
+ DecodeOneBuffer(input_buffer);
+ }
+ decoder_thread_->job_queue()->Schedule(consumed_cb);
+}
+void StubAudioDecoder::DecodeOneBuffer(
+ const scoped_refptr<InputBuffer>& input_buffer) {
// Values to represent what kind of dummy audio to fill the decoded audio
// we produce with.
enum FillType {
@@ -177,7 +186,6 @@
decoder_thread_->job_queue()->Schedule(output_cb_);
}
}
- decoder_thread_->job_queue()->Schedule(consumed_cb);
last_input_buffer_ = input_buffer;
total_input_count_++;
}
diff --git a/starboard/shared/starboard/player/filter/stub_audio_decoder.h b/starboard/shared/starboard/player/filter/stub_audio_decoder.h
index f59c14c..887a67e 100644
--- a/starboard/shared/starboard/player/filter/stub_audio_decoder.h
+++ b/starboard/shared/starboard/player/filter/stub_audio_decoder.h
@@ -38,15 +38,16 @@
void Initialize(const OutputCB& output_cb, const ErrorCB& error_cb) override;
- void Decode(const scoped_refptr<InputBuffer>& input_buffer,
+ void Decode(const InputBuffers& input_buffer,
const ConsumedCB& consumed_cb) override;
void WriteEndOfStream() override;
scoped_refptr<DecodedAudio> Read(int* samples_per_second) override;
void Reset() override;
private:
- void DecodeOneBuffer(const scoped_refptr<InputBuffer>& input_buffer,
- const ConsumedCB& consumed_cb);
+ void DecodeBuffers(const InputBuffers& input_buffers,
+ const ConsumedCB& consumed_cb);
+ void DecodeOneBuffer(const scoped_refptr<InputBuffer>& input_buffer);
void DecodeEndOfStream();
OutputCB output_cb_;
diff --git a/starboard/shared/starboard/player/filter/stub_video_decoder.cc b/starboard/shared/starboard/player/filter/stub_video_decoder.cc
index 359b48c..7e3fc13 100644
--- a/starboard/shared/starboard/player/filter/stub_video_decoder.cc
+++ b/starboard/shared/starboard/player/filter/stub_video_decoder.cc
@@ -45,16 +45,15 @@
return 12;
}
-void StubVideoDecoder::WriteInputBuffer(
- const scoped_refptr<InputBuffer>& input_buffer) {
+void StubVideoDecoder::WriteInputBuffers(const InputBuffers& input_buffers) {
SB_DCHECK(BelongsToCurrentThread());
- SB_DCHECK(input_buffer);
+ SB_DCHECK(!input_buffers.empty());
if (!decoder_thread_) {
decoder_thread_.reset(new JobThread("stub_video_decoder"));
}
decoder_thread_->job_queue()->Schedule(
- std::bind(&StubVideoDecoder::DecodeOneBuffer, this, input_buffer));
+ std::bind(&StubVideoDecoder::DecodeBuffers, this, input_buffers));
}
void StubVideoDecoder::WriteEndOfStream() {
@@ -82,41 +81,42 @@
return kSbDecodeTargetInvalid;
}
-void StubVideoDecoder::DecodeOneBuffer(
- const scoped_refptr<InputBuffer>& input_buffer) {
+void StubVideoDecoder::DecodeBuffers(const InputBuffers& input_buffers) {
SB_DCHECK(decoder_thread_->job_queue()->BelongsToCurrentThread());
- auto& video_sample_info = input_buffer->video_sample_info();
- if (video_sample_info.is_key_frame) {
- if (video_sample_info_ != video_sample_info) {
- SB_LOG(INFO) << "New video sample info: " << video_sample_info;
- video_sample_info_ = video_sample_info;
+ for (const auto& input_buffer : input_buffers) {
+ auto& video_sample_info = input_buffer->video_sample_info();
+ if (video_sample_info.is_key_frame) {
+ if (video_sample_info_ != video_sample_info) {
+ SB_LOG(INFO) << "New video sample info: " << video_sample_info;
+ video_sample_info_ = video_sample_info;
+ }
}
- }
- // Defer sending frames out until we've accumulated a reasonable number.
- // This allows for input buffers to be out of order, and we expect that
- // after buffering 8 (arbitrarily chosen) that the first timestamp in the
- // sorted buffer will be the "correct" timestamp to send out.
- const int kMaxFramesToDelay = 8;
- // Send kBufferFull on every 5th input buffer received, starting with the
- // first.
- const int kMaxInputBeforeBufferFull = 5;
- scoped_refptr<VideoFrame> output_frame = NULL;
+ // Defer sending frames out until we've accumulated a reasonable number.
+ // This allows for input buffers to be out of order, and we expect that
+ // after buffering 8 (arbitrarily chosen) that the first timestamp in the
+ // sorted buffer will be the "correct" timestamp to send out.
+ const int kMaxFramesToDelay = 8;
+ // Send kBufferFull on every 5th input buffer received, starting with the
+ // first.
+ const int kMaxInputBeforeBufferFull = 5;
+ scoped_refptr<VideoFrame> output_frame = NULL;
- output_frame_timestamps_.insert(input_buffer->timestamp());
- if (output_frame_timestamps_.size() > kMaxFramesToDelay) {
- output_frame = CreateOutputFrame(*output_frame_timestamps_.begin());
- output_frame_timestamps_.erase(output_frame_timestamps_.begin());
- }
+ output_frame_timestamps_.insert(input_buffer->timestamp());
+ if (output_frame_timestamps_.size() > kMaxFramesToDelay) {
+ output_frame = CreateOutputFrame(*output_frame_timestamps_.begin());
+ output_frame_timestamps_.erase(output_frame_timestamps_.begin());
+ }
- if (total_input_count_ % kMaxInputBeforeBufferFull == 0) {
+ if (total_input_count_ % kMaxInputBeforeBufferFull == 0) {
+ total_input_count_++;
+ decoder_status_cb_(kBufferFull, output_frame);
+ decoder_status_cb_(kNeedMoreInput, nullptr);
+ continue;
+ }
total_input_count_++;
- decoder_status_cb_(kBufferFull, output_frame);
- decoder_status_cb_(kNeedMoreInput, nullptr);
- return;
+ decoder_status_cb_(kNeedMoreInput, output_frame);
}
- total_input_count_++;
- decoder_status_cb_(kNeedMoreInput, output_frame);
}
void StubVideoDecoder::DecodeEndOfStream() {
diff --git a/starboard/shared/starboard/player/filter/stub_video_decoder.h b/starboard/shared/starboard/player/filter/stub_video_decoder.h
index 0199274..fc66d7f 100644
--- a/starboard/shared/starboard/player/filter/stub_video_decoder.h
+++ b/starboard/shared/starboard/player/filter/stub_video_decoder.h
@@ -41,15 +41,14 @@
SbTime GetPrerollTimeout() const override;
size_t GetMaxNumberOfCachedFrames() const override;
- void WriteInputBuffer(
- const scoped_refptr<InputBuffer>& input_buffer) override;
+ void WriteInputBuffers(const InputBuffers& input_buffers) override;
void WriteEndOfStream() override;
void Reset() override;
SbDecodeTarget GetCurrentDecodeTarget() override;
private:
- void DecodeOneBuffer(const scoped_refptr<InputBuffer>& input_buffer);
+ void DecodeBuffers(const InputBuffers& input_buffers);
void DecodeEndOfStream();
scoped_refptr<VideoFrame> CreateOutputFrame(SbTime timestamp) const;
diff --git a/starboard/shared/starboard/player/filter/testing/adaptive_audio_decoder_test.cc b/starboard/shared/starboard/player/filter/testing/adaptive_audio_decoder_test.cc
index 4c77870..9dc1e6a 100644
--- a/starboard/shared/starboard/player/filter/testing/adaptive_audio_decoder_test.cc
+++ b/starboard/shared/starboard/player/filter/testing/adaptive_audio_decoder_test.cc
@@ -134,7 +134,7 @@
can_accept_more_input_ = false;
audio_decoder_->Decode(
- GetAudioInputBuffer(dmp_reader, buffer_index),
+ {GetAudioInputBuffer(dmp_reader, buffer_index)},
std::bind(&AdaptiveAudioDecoderTest::OnConsumed, this));
}
diff --git a/starboard/shared/starboard/player/filter/testing/audio_decoder_benchmark.cc b/starboard/shared/starboard/player/filter/testing/audio_decoder_benchmark.cc
index e92e02d..ab08076 100644
--- a/starboard/shared/starboard/player/filter/testing/audio_decoder_benchmark.cc
+++ b/starboard/shared/starboard/player/filter/testing/audio_decoder_benchmark.cc
@@ -92,7 +92,7 @@
return;
}
if (current_input_buffer_index_ < number_of_inputs_) {
- audio_decoder_->Decode(GetAudioInputBuffer(current_input_buffer_index_),
+ audio_decoder_->Decode({GetAudioInputBuffer(current_input_buffer_index_)},
std::bind(&AudioDecoderHelper::OnConsumed, this));
++current_input_buffer_index_;
} else {
diff --git a/starboard/shared/starboard/player/filter/testing/audio_decoder_test.cc b/starboard/shared/starboard/player/filter/testing/audio_decoder_test.cc
index eee3730..f778eba 100644
--- a/starboard/shared/starboard/player/filter/testing/audio_decoder_test.cc
+++ b/starboard/shared/starboard/player/filter/testing/audio_decoder_test.cc
@@ -17,6 +17,8 @@
#include <deque>
#include <functional>
#include <map>
+#include <utility>
+#include <vector>
#include "starboard/common/condition_variable.h"
#include "starboard/common/media.h"
@@ -136,8 +138,7 @@
can_accept_more_input_ = false;
last_input_buffer_ = GetAudioInputBuffer(index);
-
- audio_decoder_->Decode(last_input_buffer_, consumed_cb());
+ audio_decoder_->Decode({last_input_buffer_}, consumed_cb());
}
// This has to be called when OnOutput() is called.
@@ -306,8 +307,7 @@
if (iter != invalid_inputs_.end()) {
std::vector<uint8_t> content(input_buffer->size(), iter->second);
// Replace the content with invalid data.
- input_buffer->SetDecryptedContent(content.data(),
- static_cast<int>(content.size()));
+ input_buffer->SetDecryptedContent(std::move(content));
}
return input_buffer;
}
diff --git a/starboard/shared/starboard/player/filter/testing/audio_renderer_internal_test.cc b/starboard/shared/starboard/player/filter/testing/audio_renderer_internal_test.cc
index ae7e57c..bfeb7a7 100644
--- a/starboard/shared/starboard/player/filter/testing/audio_renderer_internal_test.cc
+++ b/starboard/shared/starboard/player/filter/testing/audio_renderer_internal_test.cc
@@ -155,9 +155,11 @@
ASSERT_FALSE(consumed_cb_);
buffers_in_decoder_.insert(input_buffer->data());
- EXPECT_CALL(*audio_decoder_, Decode(input_buffer, _))
+ InputBuffers input_buffers;
+ input_buffers.push_back(input_buffer);
+ EXPECT_CALL(*audio_decoder_, Decode(input_buffers, _))
.WillOnce(SaveArg<1>(&consumed_cb_));
- audio_renderer_->WriteSample(input_buffer);
+ audio_renderer_->WriteSamples(input_buffers);
job_queue_.RunUntilIdle();
ASSERT_TRUE(consumed_cb_);
diff --git a/starboard/shared/starboard/player/filter/testing/player_components_test.cc b/starboard/shared/starboard/player/filter/testing/player_components_test.cc
index 11f880a..13e4858 100644
--- a/starboard/shared/starboard/player/filter/testing/player_components_test.cc
+++ b/starboard/shared/starboard/player/filter/testing/player_components_test.cc
@@ -259,6 +259,11 @@
return std::min(next_timestamps[0], next_timestamps[1]);
}
+ SbTime GetMaxWrittenBufferTimestamp() const {
+ return std::max(GetCurrentVideoBufferTimestamp(),
+ GetCurrentAudioBufferTimestamp());
+ }
+
void WriteDataUntilPrerolled(SbTime timeout = kDefaultPrerollTimeOut) {
SbTimeMonotonic start_time = SbTimeGetMonotonicNow();
SbTime max_timestamp = GetMediaTime() + kMaxWriteAheadDuration;
@@ -373,7 +378,11 @@
}
current_time = GetMediaTime();
// TODO: investigate and reduce the tolerance.
- ASSERT_LE(std::abs(current_time - duration), 500 * kSbTimeMillisecond);
+ ASSERT_LE(std::abs(current_time - duration), 500 * kSbTimeMillisecond)
+ << "Media time difference is too large, buffered audio("
+ << GetCurrentAudioBufferTimestamp() << "), buffered video ("
+ << GetCurrentVideoBufferTimestamp() << "), current media time is "
+ << GetMediaTime() << ".";
}
// This function needs to be called periodically to keep player components
@@ -395,8 +404,8 @@
private:
// We won't write audio data more than 1s ahead of current media time in
// cobalt. So, to test with the same condition, we limit max inputs ahead to
- // 1s in the tests.
- const SbTime kMaxWriteAheadDuration = kSbTimeSecond;
+ // 1.5s in the tests.
+ const SbTime kMaxWriteAheadDuration = kSbTimeMillisecond * 1500;
void OnError(SbPlayerError error, const std::string& error_message) {
has_error_ = true;
@@ -447,13 +456,13 @@
if (GetAudioRenderer() && GetAudioRenderer()->CanAcceptMoreData() &&
audio_index_ < audio_reader_->number_of_audio_buffers() &&
GetCurrentAudioBufferTimestamp() < max_timestamp) {
- GetAudioRenderer()->WriteSample(GetAudioInputBuffer(audio_index_++));
+ GetAudioRenderer()->WriteSamples({GetAudioInputBuffer(audio_index_++)});
input_buffer_written = true;
}
if (GetVideoRenderer() && GetVideoRenderer()->CanAcceptMoreData() &&
video_index_ < video_reader_->number_of_video_buffers() &&
GetCurrentVideoBufferTimestamp() < max_timestamp) {
- GetVideoRenderer()->WriteSample(GetVideoInputBuffer(video_index_++));
+ GetVideoRenderer()->WriteSamples({GetVideoInputBuffer(video_index_++)});
input_buffer_written = true;
}
if (input_buffer_written) {
@@ -493,11 +502,10 @@
SbTimeMonotonic play_requested_at = SbTimeGetMonotonicNow();
Play();
- SbTime media_duration = std::max(GetCurrentVideoBufferTimestamp(),
- GetCurrentAudioBufferTimestamp());
- media_duration = std::max(kSbTimeSecond, media_duration);
+ SbTime eos_timestamp =
+ std::max(kSbTimeSecond, GetMaxWrittenBufferTimestamp());
- ASSERT_NO_FATAL_FAILURE(WriteDataAndEOS(media_duration));
+ ASSERT_NO_FATAL_FAILURE(WriteDataAndEOS(eos_timestamp));
ASSERT_NO_FATAL_FAILURE(WaitUntilPlaybackEnded());
// TODO: investigate and reduce the tolerance.
@@ -544,9 +552,7 @@
ASSERT_EQ(media_time, GetMediaTime());
Play();
- SbTime duration = std::max(GetCurrentAudioBufferTimestamp(),
- GetCurrentVideoBufferTimestamp());
- ASSERT_NO_FATAL_FAILURE(WriteDataAndEOS(duration));
+ ASSERT_NO_FATAL_FAILURE(WriteDataAndEOS(GetMaxWrittenBufferTimestamp()));
ASSERT_NO_FATAL_FAILURE(WaitUntilPlaybackEnded());
}
@@ -625,7 +631,9 @@
ASSERT_FALSE(IsPlaying());
Play();
- ASSERT_NO_FATAL_FAILURE(WriteDataAndEOS(seek_to_time + kSbTimeSecond));
+ SbTime eos_timestamp =
+ std::max(GetMaxWrittenBufferTimestamp(), seek_to_time + kSbTimeSecond);
+ ASSERT_NO_FATAL_FAILURE(WriteDataAndEOS(eos_timestamp));
ASSERT_NO_FATAL_FAILURE(WaitUntilPlaybackEnded());
}
@@ -647,7 +655,9 @@
ASSERT_FALSE(IsPlaying());
Play();
- ASSERT_NO_FATAL_FAILURE(WriteDataAndEOS(seek_to_time + kSbTimeSecond));
+ SbTime eos_timestamp =
+ std::max(GetMaxWrittenBufferTimestamp(), seek_to_time + kSbTimeSecond);
+ ASSERT_NO_FATAL_FAILURE(WriteDataAndEOS(eos_timestamp));
ASSERT_NO_FATAL_FAILURE(WaitUntilPlaybackEnded());
}
diff --git a/starboard/shared/starboard/player/filter/testing/video_decoder_test_fixture.cc b/starboard/shared/starboard/player/filter/testing/video_decoder_test_fixture.cc
index f274a3e..618359c 100644
--- a/starboard/shared/starboard/player/filter/testing/video_decoder_test_fixture.cc
+++ b/starboard/shared/starboard/player/filter/testing/video_decoder_test_fixture.cc
@@ -20,6 +20,7 @@
#include <map>
#include <set>
#include <string>
+#include <utility>
#include <vector>
#include "starboard/common/condition_variable.h"
@@ -232,8 +233,7 @@
need_more_input_ = false;
outstanding_inputs_.insert(input_buffer->timestamp());
}
-
- video_decoder_->WriteInputBuffer(input_buffer);
+ video_decoder_->WriteInputBuffers({input_buffer});
}
void VideoDecoderTestFixture::WriteEndOfStream() {
@@ -369,8 +369,7 @@
if (iter != invalid_inputs_.end()) {
std::vector<uint8_t> content(input_buffer->size(), iter->second);
// Replace the content with invalid data.
- input_buffer->SetDecryptedContent(content.data(),
- static_cast<int>(content.size()));
+ input_buffer->SetDecryptedContent(std::move(content));
}
return input_buffer;
}
diff --git a/starboard/shared/starboard/player/filter/tools/audio_dmp_player.cc b/starboard/shared/starboard/player/filter/tools/audio_dmp_player.cc
index f1f0288..4aa8c06 100644
--- a/starboard/shared/starboard/player/filter/tools/audio_dmp_player.cc
+++ b/starboard/shared/starboard/player/filter/tools/audio_dmp_player.cc
@@ -33,6 +33,7 @@
using starboard::shared::starboard::player::filter::AudioRenderer;
using starboard::shared::starboard::player::filter::PlayerComponents;
using starboard::shared::starboard::player::InputBuffer;
+using starboard::shared::starboard::player::InputBuffers;
using starboard::shared::starboard::player::JobThread;
using starboard::scoped_ptr;
@@ -50,11 +51,11 @@
std::vector<char> content_path(kPathSize);
SB_CHECK(SbSystemGetPath(kSbSystemPathContentDirectory, content_path.data(),
kPathSize));
- std::string directory_path =
- std::string(content_path.data()) + kSbFileSepChar + "test" +
- kSbFileSepChar + "starboard" + kSbFileSepChar + "shared" +
- kSbFileSepChar + "starboard" + kSbFileSepChar + "player" +
- kSbFileSepChar + "testdata";
+ std::string directory_path = std::string(content_path.data()) +
+ kSbFileSepChar + "test" + kSbFileSepChar +
+ "starboard" + kSbFileSepChar + "shared" +
+ kSbFileSepChar + "starboard" + kSbFileSepChar +
+ "player" + kSbFileSepChar + "testdata";
SB_CHECK(SbDirectoryCanOpen(directory_path.c_str()))
<< "Cannot open directory " << directory_path;
@@ -73,8 +74,7 @@
static void DeallocateSampleFunc(SbPlayer player,
void* context,
- const void* sample_buffer) {
-}
+ const void* sample_buffer) {}
starboard::scoped_refptr<InputBuffer> GetAudioInputBuffer(size_t index) {
auto player_sample_info =
@@ -93,9 +93,12 @@
s_player_components->GetAudioRenderer()->WriteEndOfStream();
return;
} else {
+ InputBuffers input_buffers;
auto input_buffer = GetAudioInputBuffer(s_audio_sample_index);
+
s_duration = input_buffer->timestamp();
- s_player_components->GetAudioRenderer()->WriteSample(input_buffer);
+ input_buffers.push_back(std::move(input_buffer));
+ s_player_components->GetAudioRenderer()->WriteSamples(input_buffers);
++s_audio_sample_index;
}
diff --git a/starboard/shared/starboard/player/filter/video_decoder_internal.h b/starboard/shared/starboard/player/filter/video_decoder_internal.h
index 0d0db76..f30a843 100644
--- a/starboard/shared/starboard/player/filter/video_decoder_internal.h
+++ b/starboard/shared/starboard/player/filter/video_decoder_internal.h
@@ -35,6 +35,7 @@
class VideoDecoder {
public:
typedef ::starboard::shared::starboard::player::InputBuffer InputBuffer;
+ typedef ::starboard::shared::starboard::player::InputBuffers InputBuffers;
typedef ::starboard::shared::starboard::player::filter::VideoFrame VideoFrame;
enum Status {
@@ -86,9 +87,8 @@
// acceptable playback performance. A number greater than 6 is recommended.
virtual size_t GetMaxNumberOfCachedFrames() const = 0;
- // Send encoded video frame stored in |input_buffer| to decode.
- virtual void WriteInputBuffer(
- const scoped_refptr<InputBuffer>& input_buffer) = 0;
+ // Send encoded video frames stored in |input_buffers| to decode.
+ virtual void WriteInputBuffers(const InputBuffers& input_buffers) = 0;
// Note that there won't be more input data unless Reset() is called.
// |decoder_status_cb| can still be called multiple times afterwards to
diff --git a/starboard/shared/starboard/player/filter/video_renderer_internal.h b/starboard/shared/starboard/player/filter/video_renderer_internal.h
index 471dfc7..12546ee 100644
--- a/starboard/shared/starboard/player/filter/video_renderer_internal.h
+++ b/starboard/shared/starboard/player/filter/video_renderer_internal.h
@@ -39,7 +39,8 @@
const EndedCB& ended_cb) = 0;
virtual int GetDroppedFrames() const = 0;
- virtual void WriteSample(const scoped_refptr<InputBuffer>& input_buffer) = 0;
+ virtual void WriteSamples(const InputBuffers& input_buffers) = 0;
+
virtual void WriteEndOfStream() = 0;
virtual void Seek(SbTime seek_to_time) = 0;
diff --git a/starboard/shared/starboard/player/filter/video_renderer_internal_impl.cc b/starboard/shared/starboard/player/filter/video_renderer_internal_impl.cc
index b07a4fe..25c12a6 100644
--- a/starboard/shared/starboard/player/filter/video_renderer_internal_impl.cc
+++ b/starboard/shared/starboard/player/filter/video_renderer_internal_impl.cc
@@ -101,10 +101,12 @@
}
}
-void VideoRendererImpl::WriteSample(
- const scoped_refptr<InputBuffer>& input_buffer) {
+void VideoRendererImpl::WriteSamples(const InputBuffers& input_buffers) {
SB_DCHECK(BelongsToCurrentThread());
- SB_DCHECK(input_buffer);
+ SB_DCHECK(!input_buffers.empty());
+ for (const auto& input_buffer : input_buffers) {
+ SB_DCHECK(input_buffer);
+ }
#if SB_PLAYER_FILTER_ENABLE_STATE_CHECK
buffering_state_ = kWaitForConsumption;
@@ -112,8 +114,9 @@
#endif // SB_PLAYER_FILTER_ENABLE_STATE_CHECK
if (end_of_stream_written_.load()) {
- SB_LOG(ERROR) << "Appending video sample at " << input_buffer->timestamp()
- << " after EOS reached.";
+ SB_LOG(ERROR) << "Appending video samples from "
+ << input_buffers.front()->timestamp() << " to "
+ << input_buffers.back()->timestamp() << " after EOS reached.";
return;
}
@@ -128,7 +131,7 @@
SB_DCHECK(need_more_input_.load());
need_more_input_.store(false);
- decoder_->WriteInputBuffer(input_buffer);
+ decoder_->WriteInputBuffers(input_buffers);
}
void VideoRendererImpl::WriteEndOfStream() {
diff --git a/starboard/shared/starboard/player/filter/video_renderer_internal_impl.h b/starboard/shared/starboard/player/filter/video_renderer_internal_impl.h
index ee8bad0..a46edae 100644
--- a/starboard/shared/starboard/player/filter/video_renderer_internal_impl.h
+++ b/starboard/shared/starboard/player/filter/video_renderer_internal_impl.h
@@ -61,7 +61,8 @@
return algorithm_->GetDroppedFrames();
}
- void WriteSample(const scoped_refptr<InputBuffer>& input_buffer) override;
+ void WriteSamples(const InputBuffers& input_buffers) override;
+
void WriteEndOfStream() override;
void Seek(SbTime seek_to_time) override;
diff --git a/starboard/shared/starboard/player/input_buffer_internal.cc b/starboard/shared/starboard/player/input_buffer_internal.cc
index b9b22c2..f3ec61e 100644
--- a/starboard/shared/starboard/player/input_buffer_internal.cc
+++ b/starboard/shared/starboard/player/input_buffer_internal.cc
@@ -18,6 +18,7 @@
#include <cstring>
#include <numeric>
#include <sstream>
+#include <utility>
#include "starboard/common/log.h"
#include "starboard/common/string.h"
@@ -65,18 +66,17 @@
DeallocateSampleBuffer(data_);
}
-void InputBuffer::SetDecryptedContent(const void* buffer, int size) {
- SB_DCHECK(size == size_);
+void InputBuffer::SetDecryptedContent(std::vector<uint8_t> decrypted_content) {
+ SB_DCHECK(decrypted_content.size() == size_);
DeallocateSampleBuffer(data_);
- if (size > 0) {
- flattened_data_.clear();
- flattened_data_.assign(static_cast<const uint8_t*>(buffer),
- static_cast<const uint8_t*>(buffer) + size);
- data_ = flattened_data_.data();
- } else {
+ if (decrypted_content.empty()) {
data_ = NULL;
+ } else {
+ flattened_data_ = std::move(decrypted_content);
+ data_ = flattened_data_.data();
}
+
has_drm_info_ = false;
}
diff --git a/starboard/shared/starboard/player/input_buffer_internal.h b/starboard/shared/starboard/player/input_buffer_internal.h
index 976dd9f..3da8d19 100644
--- a/starboard/shared/starboard/player/input_buffer_internal.h
+++ b/starboard/shared/starboard/player/input_buffer_internal.h
@@ -58,7 +58,7 @@
const SbDrmSampleInfo* drm_info() const {
return has_drm_info_ ? &drm_info_ : NULL;
}
- void SetDecryptedContent(const void* buffer, int size);
+ void SetDecryptedContent(std::vector<uint8_t> decrypted_content);
std::string ToString() const;
@@ -88,6 +88,8 @@
void operator=(const InputBuffer&) = delete;
};
+typedef std::vector<scoped_refptr<InputBuffer>> InputBuffers;
+
} // namespace player
} // namespace starboard
} // namespace shared
diff --git a/starboard/shared/starboard/player/player_internal.cc b/starboard/shared/starboard/player/player_internal.cc
index 45333ad..a25f74d 100644
--- a/starboard/shared/starboard/player/player_internal.cc
+++ b/starboard/shared/starboard/player/player_internal.cc
@@ -15,6 +15,7 @@
#include "starboard/shared/starboard/player/player_internal.h"
#include <functional>
+#include <utility>
#include "starboard/common/log.h"
#if SB_PLAYER_ENABLE_VIDEO_DUMPER
@@ -24,6 +25,7 @@
namespace {
using starboard::shared::starboard::player::InputBuffer;
+using starboard::shared::starboard::player::InputBuffers;
using std::placeholders::_1;
using std::placeholders::_2;
using std::placeholders::_3;
@@ -100,19 +102,29 @@
worker_->Seek(seek_to_time, ticket);
}
-void SbPlayerPrivate::WriteSample(const SbPlayerSampleInfo& sample_info) {
- if (sample_info.type == kSbMediaTypeVideo) {
- ++total_video_frames_;
- frame_width_ = sample_info.video_sample_info.frame_width;
- frame_height_ = sample_info.video_sample_info.frame_height;
+void SbPlayerPrivate::WriteSamples(const SbPlayerSampleInfo* sample_infos,
+ int number_of_sample_infos) {
+ SB_DCHECK(sample_infos);
+ SB_DCHECK(number_of_sample_infos > 0);
+
+ if (sample_infos[0].type == kSbMediaTypeVideo) {
+ const auto& last_sample_info = sample_infos[number_of_sample_infos - 1];
+ total_video_frames_ += number_of_sample_infos;
+ frame_width_ = last_sample_info.video_sample_info.frame_width;
+ frame_height_ = last_sample_info.video_sample_info.frame_height;
}
- starboard::scoped_refptr<InputBuffer> input_buffer =
- new InputBuffer(sample_deallocate_func_, this, context_, sample_info);
+
+ InputBuffers input_buffers;
+ input_buffers.reserve(number_of_sample_infos);
+ for (int i = 0; i < number_of_sample_infos; i++) {
+ input_buffers.push_back(new InputBuffer(sample_deallocate_func_, this,
+ context_, sample_infos[i]));
#if SB_PLAYER_ENABLE_VIDEO_DUMPER
- using ::starboard::shared::starboard::player::video_dmp::VideoDmpWriter;
- VideoDmpWriter::OnPlayerWriteSample(this, input_buffer);
+ using ::starboard::shared::starboard::player::video_dmp::VideoDmpWriter;
+ VideoDmpWriter::OnPlayerWriteSample(this, input_buffers.back());
#endif // SB_PLAYER_ENABLE_VIDEO_DUMPER
- worker_->WriteSample(input_buffer);
+ }
+ worker_->WriteSamples(std::move(input_buffers));
}
void SbPlayerPrivate::WriteEndOfStream(SbMediaType stream_type) {
diff --git a/starboard/shared/starboard/player/player_internal.h b/starboard/shared/starboard/player/player_internal.h
index 7033737..a6ffecf 100644
--- a/starboard/shared/starboard/player/player_internal.h
+++ b/starboard/shared/starboard/player/player_internal.h
@@ -42,7 +42,8 @@
starboard::scoped_ptr<PlayerWorker::Handler> player_worker_handler);
void Seek(SbTime seek_to_time, int ticket);
- void WriteSample(const SbPlayerSampleInfo& sample_info);
+ void WriteSamples(const SbPlayerSampleInfo* sample_infos,
+ int number_of_sample_infos);
void WriteEndOfStream(SbMediaType stream_type);
void SetBounds(int z_index, int x, int y, int width, int height);
diff --git a/starboard/shared/starboard/player/player_worker.cc b/starboard/shared/starboard/player/player_worker.cc
index 2cec1a6..5ee9bc0 100644
--- a/starboard/shared/starboard/player/player_worker.cc
+++ b/starboard/shared/starboard/player/player_worker.cc
@@ -228,8 +228,9 @@
job_queue_->RemoveJobByToken(write_pending_sample_job_token_);
write_pending_sample_job_token_.ResetToInvalid();
}
- pending_audio_buffer_ = NULL;
- pending_video_buffer_ = NULL;
+
+ pending_audio_buffers_.clear();
+ pending_video_buffers_.clear();
if (!handler_->Seek(seek_to_time, ticket)) {
UpdatePlayerError(kSbPlayerErrorDecode, "Failed seek.");
@@ -247,10 +248,9 @@
}
}
-void PlayerWorker::DoWriteSample(
- const scoped_refptr<InputBuffer>& input_buffer) {
+void PlayerWorker::DoWriteSamples(InputBuffers input_buffers) {
SB_DCHECK(job_queue_->BelongsToCurrentThread());
- SB_DCHECK(input_buffer);
+ SB_DCHECK(!input_buffers.empty());
if (player_state_ == kSbPlayerStateInitialized ||
player_state_ == kSbPlayerStateEndOfStream ||
@@ -264,27 +264,34 @@
return;
}
- if (input_buffer->sample_type() == kSbMediaTypeAudio) {
+ SbMediaType media_type = input_buffers.front()->sample_type();
+ if (media_type == kSbMediaTypeAudio) {
SB_DCHECK(audio_codec_ != kSbMediaAudioCodecNone);
- SB_DCHECK(!pending_audio_buffer_);
+ SB_DCHECK(pending_audio_buffers_.empty());
} else {
SB_DCHECK(video_codec_ != kSbMediaVideoCodecNone);
- SB_DCHECK(!pending_video_buffer_);
+ SB_DCHECK(pending_video_buffers_.empty());
}
- bool written;
- bool result = handler_->WriteSample(input_buffer, &written);
+ int samples_written;
+ bool result = handler_->WriteSamples(input_buffers, &samples_written);
if (!result) {
UpdatePlayerError(kSbPlayerErrorDecode, "Failed to write sample.");
return;
}
- if (written) {
- UpdateDecoderState(input_buffer->sample_type(),
- kSbPlayerDecoderStateNeedsData);
+ if (samples_written == input_buffers.size()) {
+ UpdateDecoderState(media_type, kSbPlayerDecoderStateNeedsData);
} else {
- if (input_buffer->sample_type() == kSbMediaTypeAudio) {
- pending_audio_buffer_ = input_buffer;
+ SB_DCHECK(samples_written >= 0 && samples_written <= input_buffers.size());
+
+ size_t num_of_pending_buffers = input_buffers.size() - samples_written;
+ input_buffers.erase(input_buffers.begin(),
+ input_buffers.begin() + samples_written);
+ if (media_type == kSbMediaTypeAudio) {
+ pending_audio_buffers_ = std::move(input_buffers);
+ SB_DCHECK(pending_audio_buffers_.size() == num_of_pending_buffers);
} else {
- pending_video_buffer_ = input_buffer;
+ pending_video_buffers_ = std::move(input_buffers);
+ SB_DCHECK(pending_video_buffers_.size() == num_of_pending_buffers);
}
if (!write_pending_sample_job_token_.is_valid()) {
write_pending_sample_job_token_ = job_queue_->Schedule(
@@ -299,13 +306,14 @@
SB_DCHECK(write_pending_sample_job_token_.is_valid());
write_pending_sample_job_token_.ResetToInvalid();
- if (pending_audio_buffer_) {
+ if (!pending_audio_buffers_.empty()) {
SB_DCHECK(audio_codec_ != kSbMediaAudioCodecNone);
- DoWriteSample(common::ResetAndReturn(&pending_audio_buffer_));
+ DoWriteSamples(std::move(pending_audio_buffers_));
}
- if (pending_video_buffer_) {
+ if (!pending_video_buffers_.empty()) {
SB_DCHECK(video_codec_ != kSbMediaVideoCodecNone);
- DoWriteSample(common::ResetAndReturn(&pending_video_buffer_));
+ InputBuffers input_buffers = std::move(pending_video_buffers_);
+ DoWriteSamples(input_buffers);
}
}
@@ -327,10 +335,10 @@
if (sample_type == kSbMediaTypeAudio) {
SB_DCHECK(audio_codec_ != kSbMediaAudioCodecNone);
- SB_DCHECK(!pending_audio_buffer_);
+ SB_DCHECK(pending_audio_buffers_.empty());
} else {
SB_DCHECK(video_codec_ != kSbMediaVideoCodecNone);
- SB_DCHECK(!pending_video_buffer_);
+ SB_DCHECK(pending_video_buffers_.empty());
}
if (!handler_->WriteEndOfStream(sample_type)) {
diff --git a/starboard/shared/starboard/player/player_worker.h b/starboard/shared/starboard/player/player_worker.h
index dac9f8f..cf27fb3 100644
--- a/starboard/shared/starboard/player/player_worker.h
+++ b/starboard/shared/starboard/player/player_worker.h
@@ -17,7 +17,9 @@
#include <atomic>
#include <functional>
+#include <memory>
#include <string>
+#include <utility>
#include "starboard/common/log.h"
#include "starboard/common/ref_counted.h"
@@ -63,6 +65,7 @@
public:
typedef PlayerWorker::Bounds Bounds;
typedef ::starboard::shared::starboard::player::InputBuffer InputBuffer;
+ typedef ::starboard::shared::starboard::player::InputBuffers InputBuffers;
typedef std::function<
void(SbTime media_time, int dropped_video_frames, bool is_progressing)>
@@ -84,8 +87,8 @@
UpdatePlayerStateCB update_player_state_cb,
UpdatePlayerErrorCB update_player_error_cb) = 0;
virtual bool Seek(SbTime seek_to_time, int ticket) = 0;
- virtual bool WriteSample(const scoped_refptr<InputBuffer>& input_buffer,
- bool* written) = 0;
+ virtual bool WriteSamples(const InputBuffers& input_buffers,
+ int* samples_written) = 0;
virtual bool WriteEndOfStream(SbMediaType sample_type) = 0;
virtual bool SetPause(bool pause) = 0;
virtual bool SetPlaybackRate(double playback_rate) = 0;
@@ -123,9 +126,9 @@
std::bind(&PlayerWorker::DoSeek, this, seek_to_time, ticket));
}
- void WriteSample(const scoped_refptr<InputBuffer>& input_buffer) {
- job_queue_->Schedule(
- std::bind(&PlayerWorker::DoWriteSample, this, input_buffer));
+ void WriteSamples(InputBuffers input_buffers) {
+ job_queue_->Schedule(std::bind(&PlayerWorker::DoWriteSamples, this,
+ std::move(input_buffers)));
}
void WriteEndOfStream(SbMediaType sample_type) {
@@ -190,7 +193,7 @@
void RunLoop();
void DoInit();
void DoSeek(SbTime seek_to_time, int ticket);
- void DoWriteSample(const scoped_refptr<InputBuffer>& input_buffer);
+ void DoWriteSamples(InputBuffers input_buffers);
void DoWritePendingSamples();
void DoWriteEndOfStream(SbMediaType sample_type);
void DoSetBounds(Bounds bounds);
@@ -218,8 +221,8 @@
int ticket_;
SbPlayerState player_state_;
- scoped_refptr<InputBuffer> pending_audio_buffer_;
- scoped_refptr<InputBuffer> pending_video_buffer_;
+ InputBuffers pending_audio_buffers_;
+ InputBuffers pending_video_buffers_;
JobQueue::JobToken write_pending_sample_job_token_;
};
diff --git a/starboard/shared/starboard/player/player_write_sample2.cc b/starboard/shared/starboard/player/player_write_sample2.cc
index 7a3a018..d839aff 100644
--- a/starboard/shared/starboard/player/player_write_sample2.cc
+++ b/starboard/shared/starboard/player/player_write_sample2.cc
@@ -21,12 +21,26 @@
SbMediaType sample_type,
const SbPlayerSampleInfo* sample_infos,
int number_of_sample_infos) {
- SB_DCHECK(number_of_sample_infos == 1);
-
if (!SbPlayerIsValid(player)) {
- SB_DLOG(WARNING) << "player is invalid.";
+ SB_LOG(WARNING) << "player is invalid.";
return;
}
+ if (!sample_infos) {
+ SB_LOG(WARNING) << "sample_infos is null.";
+ return;
+ }
+ if (number_of_sample_infos < 1) {
+ SB_LOG(WARNING) << "number_of_sample_infos is " << number_of_sample_infos
+ << ", which should be greater than or equal to 1";
+ return;
+ }
+ auto max_samples_per_write =
+ SbPlayerGetMaximumNumberOfSamplesPerWrite(player, sample_type);
+ if (number_of_sample_infos > max_samples_per_write) {
+ SB_LOG(WARNING) << "number_of_sample_infos is " << number_of_sample_infos
+ << ", which should be less than or equal to "
+ << max_samples_per_write;
+ }
- player->WriteSample(*sample_infos);
+ player->WriteSamples(sample_infos, number_of_sample_infos);
}
diff --git a/starboard/shared/starboard/player/testdata/beneath_the_canopy_137_avc.dmp.sha1 b/starboard/shared/starboard/player/testdata/beneath_the_canopy_137_avc.dmp.sha1
index 58c5cd1..8f85266 100644
--- a/starboard/shared/starboard/player/testdata/beneath_the_canopy_137_avc.dmp.sha1
+++ b/starboard/shared/starboard/player/testdata/beneath_the_canopy_137_avc.dmp.sha1
@@ -1 +1 @@
-f3c82f30f61d744f35a3d37e9de060a13cd1bd90
\ No newline at end of file
+cf63bb5cf3d72b8a524b505fb8ea761a9fdcc362
diff --git a/starboard/shared/widevine/drm_system_widevine.cc b/starboard/shared/widevine/drm_system_widevine.cc
index 59292ef..cd32817 100644
--- a/starboard/shared/widevine/drm_system_widevine.cc
+++ b/starboard/shared/widevine/drm_system_widevine.cc
@@ -15,6 +15,7 @@
#include "starboard/shared/widevine/drm_system_widevine.h"
#include <algorithm>
+#include <utility>
#include <vector>
#include "starboard/character.h"
@@ -440,6 +441,7 @@
input.pattern.clear_blocks = drm_info->encryption_pattern.skip_byte_block;
std::vector<uint8_t> output_data(buffer->size());
+
wv3cdm::OutputBuffer output;
output.data = output_data.data();
output.data_length = output_data.size();
@@ -540,7 +542,7 @@
}
}
- buffer->SetDecryptedContent(output_data.data(), output_data.size());
+ buffer->SetDecryptedContent(std::move(output_data));
return kSuccess;
}
diff --git a/starboard/shared/win32/audio_decoder.cc b/starboard/shared/win32/audio_decoder.cc
index 0b54739..fbcfe44 100644
--- a/starboard/shared/win32/audio_decoder.cc
+++ b/starboard/shared/win32/audio_decoder.cc
@@ -98,14 +98,15 @@
callback_scheduler_.reset(new CallbackScheduler());
}
-void AudioDecoder::Decode(const scoped_refptr<InputBuffer>& input_buffer,
+void AudioDecoder::Decode(const InputBuffers& input_buffers,
const ConsumedCB& consumed_cb) {
SB_DCHECK(thread_checker_.CalledOnValidThread());
- SB_DCHECK(input_buffer);
+ SB_DCHECK(input_buffers.size() == 1);
+ SB_DCHECK(input_buffers[0]);
callback_scheduler_->SetCallbackOnce(consumed_cb);
callback_scheduler_->OnCallbackSignaled();
- const bool can_take_more_data = decoder_thread_->QueueInput(input_buffer);
+ const bool can_take_more_data = decoder_thread_->QueueInput(input_buffers[0]);
if (can_take_more_data) {
callback_scheduler_->ScheduleCallbackIfNecessary();
}
diff --git a/starboard/shared/win32/audio_decoder.h b/starboard/shared/win32/audio_decoder.h
index 18631f0..0467b47 100644
--- a/starboard/shared/win32/audio_decoder.h
+++ b/starboard/shared/win32/audio_decoder.h
@@ -45,7 +45,7 @@
~AudioDecoder() override;
void Initialize(const OutputCB& output_cb, const ErrorCB& error_cb) override;
- void Decode(const scoped_refptr<InputBuffer>& input_buffer,
+ void Decode(const InputBuffers& input_buffers,
const ConsumedCB& consumed_cb) override;
void WriteEndOfStream() override;
scoped_refptr<DecodedAudio> Read(int* samples_per_second) override;
diff --git a/starboard/shared/win32/hardware_decode_target_internal.cc b/starboard/shared/win32/hardware_decode_target_internal.cc
index 80859fc..1685aa0 100644
--- a/starboard/shared/win32/hardware_decode_target_internal.cc
+++ b/starboard/shared/win32/hardware_decode_target_internal.cc
@@ -37,7 +37,8 @@
ComPtr<ID3D11Texture2D> AllocateTexture(const ComPtr<ID3D11Device>& d3d_device,
SbDecodeTargetFormat format,
int width,
- int height) {
+ int height,
+ HRESULT* h_result) {
ComPtr<ID3D11Texture2D> texture;
D3D11_TEXTURE2D_DESC texture_desc = {};
texture_desc.Width = width;
@@ -60,8 +61,8 @@
texture_desc.Usage = D3D11_USAGE_DEFAULT;
texture_desc.BindFlags =
D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
- CheckResult(d3d_device->CreateTexture2D(&texture_desc, nullptr,
- texture.GetAddressOf()));
+ *h_result = d3d_device->CreateTexture2D(&texture_desc, nullptr,
+ texture.GetAddressOf());
return texture;
}
@@ -137,17 +138,19 @@
info.format = kSbDecodeTargetFormat2PlaneYUVNV12;
}
- d3d_texture =
- AllocateTexture(d3d_device, info.format, info.width, info.height);
- if (video_sample) {
- UpdateTexture(d3d_texture, video_device, video_context, video_enumerator,
- video_processor, video_sample, video_area);
- }
+ d3d_texture = AllocateTexture(d3d_device, info.format, info.width,
+ info.height, &create_texture_2d_h_result);
+ if (d3d_texture) {
+ if (video_sample) {
+ UpdateTexture(d3d_texture, video_device, video_context, video_enumerator,
+ video_processor, video_sample, video_area);
+ }
- if (texture_RGBA_) {
- InitTextureRGBA();
- } else {
- InitTextureYUV();
+ if (texture_RGBA_) {
+ InitTextureRGBA();
+ } else {
+ InitTextureYUV();
+ }
}
}
diff --git a/starboard/shared/win32/hardware_decode_target_internal.h b/starboard/shared/win32/hardware_decode_target_internal.h
index 8fccdbe..a73b298 100644
--- a/starboard/shared/win32/hardware_decode_target_internal.h
+++ b/starboard/shared/win32/hardware_decode_target_internal.h
@@ -27,6 +27,10 @@
template <typename T>
using ComPtr = ::Microsoft::WRL::ComPtr<T>;
+ // Return value of CreateTexture2D. Stored here to report exact error codes to
+ // the video decoder when the call fails (b/257541360).
+ HRESULT create_texture_2d_h_result;
+
ComPtr<ID3D11Texture2D> d3d_texture;
bool texture_RGBA_;
diff --git a/starboard/shared/win32/media_common.cc b/starboard/shared/win32/media_common.cc
index a09843c..1c4a100 100644
--- a/starboard/shared/win32/media_common.cc
+++ b/starboard/shared/win32/media_common.cc
@@ -98,12 +98,59 @@
return output;
}
+HRESULT CreateAV1Decoder(const IID& iid, void** object) {
+ MFT_REGISTER_TYPE_INFO type_info = {MFMediaType_Video, MFVideoFormat_AV1};
+ MFT_REGISTER_TYPE_INFO output_info = {MFMediaType_Video, MFVideoFormat_NV12};
+
+ IMFActivate** acts;
+ UINT32 acts_num = 0;
+ HRESULT hr = ::MFTEnumEx(MFT_CATEGORY_VIDEO_DECODER,
+ MFT_ENUM_FLAG_SYNCMFT | MFT_ENUM_FLAG_LOCALMFT |
+ MFT_ENUM_FLAG_UNTRUSTED_STOREMFT,
+ &type_info, &output_info, &acts, &acts_num);
+ if (FAILED(hr))
+ return hr;
+
+ if (acts_num < 1)
+ return E_FAIL;
+
+ hr = acts[0]->ActivateObject(iid, object);
+ for (UINT32 i = 0; i < acts_num; ++i)
+ acts[i]->Release();
+ CoTaskMemFree(acts);
+ return hr;
+}
+
HRESULT CreateDecoderTransform(const GUID& decoder_guid,
ComPtr<IMFTransform>* transform) {
+ if (decoder_guid == MFVideoFormat_AV1) {
+ return CreateAV1Decoder(IID_PPV_ARGS(transform->GetAddressOf()));
+ }
return CoCreateInstance(decoder_guid, NULL, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(transform->GetAddressOf()));
}
+bool IsHardwareAv1DecoderSupported() {
+ static bool av1_decoder_supported = false;
+ static bool av1_support_checked = false;
+ if (!av1_support_checked) {
+ MFT_REGISTER_TYPE_INFO type_info = {MFMediaType_Video, MFVideoFormat_AV1};
+ MFT_REGISTER_TYPE_INFO output_info = {MFMediaType_Video,
+ MFVideoFormat_NV12};
+
+ IMFActivate** acts;
+ UINT32 acts_num = 0;
+ HRESULT hr = ::MFTEnumEx(MFT_CATEGORY_VIDEO_DECODER,
+ MFT_ENUM_FLAG_SYNCMFT | MFT_ENUM_FLAG_LOCALMFT |
+ MFT_ENUM_FLAG_UNTRUSTED_STOREMFT,
+ &type_info, &output_info, &acts, &acts_num);
+ for (UINT32 i = 0; i < acts_num; ++i)
+ acts[i]->Release();
+ av1_decoder_supported = SUCCEEDED(hr) && acts_num >= 1;
+ av1_support_checked = true;
+ }
+ return av1_decoder_supported;
+}
} // namespace win32
} // namespace shared
} // namespace starboard
diff --git a/starboard/shared/win32/media_common.h b/starboard/shared/win32/media_common.h
index eacedfb..21d1323 100644
--- a/starboard/shared/win32/media_common.h
+++ b/starboard/shared/win32/media_common.h
@@ -69,6 +69,8 @@
HRESULT CreateDecoderTransform(const GUID& decoder_guid,
ComPtr<IMFTransform>* transform);
+bool IsHardwareAv1DecoderSupported();
+
} // namespace win32
} // namespace shared
} // namespace starboard
diff --git a/starboard/shared/win32/video_decoder.cc b/starboard/shared/win32/video_decoder.cc
index 44f30c4..fe58e7b 100644
--- a/starboard/shared/win32/video_decoder.cc
+++ b/starboard/shared/win32/video_decoder.cc
@@ -24,8 +24,10 @@
// Include this after all other headers to avoid introducing redundant
// definitions from other header files.
-#include <codecapi.h> // For CODECAPI_*
-#include <initguid.h> // For DXVA_ModeVP9_VLD_Profile0
+// For CODECAPI_*
+#include <codecapi.h> // NOLINT(build/include_order)
+// For DXVA_ModeVP9_VLD_Profile0
+#include <initguid.h> // NOLINT(build/include_order)
namespace starboard {
namespace shared {
@@ -148,7 +150,7 @@
CheckResult(input_type->SetGUID(MF_MT_SUBTYPE, input_guid));
CheckResult(input_type->SetUINT32(MF_MT_INTERLACE_MODE,
MFVideoInterlace_Progressive));
- if (input_guid == MFVideoFormat_VP90) {
+ if (input_guid == MFVideoFormat_VP90 || input_guid == MFVideoFormat_AV1) {
// Set the expected video resolution. Setting the proper resolution can
// mitigate a format change, but the decoder will adjust to the real
// resolution regardless.
@@ -276,13 +278,14 @@
}
}
-void VideoDecoder::WriteInputBuffer(
- const scoped_refptr<InputBuffer>& input_buffer) {
+void VideoDecoder::WriteInputBuffers(const InputBuffers& input_buffers) {
SB_DCHECK(thread_checker_.CalledOnValidThread());
- SB_DCHECK(input_buffer);
+ SB_DCHECK(input_buffers.size() == 1);
+ SB_DCHECK(input_buffers[0]);
SB_DCHECK(decoder_status_cb_);
EnsureDecoderThreadRunning();
+ const auto& input_buffer = input_buffers[0];
if (TryUpdateOutputForHdrVideo(input_buffer->video_sample_info())) {
ScopedLock lock(thread_lock_);
thread_events_.emplace_back(
@@ -391,6 +394,15 @@
decode_target = new HardwareDecodeTargetPrivate(
d3d_device_, video_device_, video_context_, video_enumerator_,
video_processor_, video_sample, video_area, is_hdr_supported_);
+ auto hardware_decode_target =
+ reinterpret_cast<HardwareDecodeTargetPrivate*>(decode_target);
+ if (!hardware_decode_target->d3d_texture) {
+ error_cb_(kSbPlayerErrorDecode,
+ "Failed to allocate texture with error code: " +
+ ::starboard::shared::win32::HResultToString(
+ hardware_decode_target->create_texture_2d_h_result));
+ decode_target = kSbDecodeTargetInvalid;
+ }
}
// Release the video_sample before releasing the reset lock.
@@ -429,6 +441,17 @@
}
break;
}
+ case kSbMediaVideoCodecAv1: {
+ media_transform =
+ CreateVideoTransform(MFVideoFormat_AV1, MFVideoFormat_AV1,
+ MFVideoFormat_NV12, device_manager_.Get());
+ priming_output_count_ = 0;
+ if (!media_transform) {
+ SB_LOG(WARNING) << "AV1 hardware decoder creation failed.";
+ return;
+ }
+ break;
+ }
default: { SB_NOTREACHED(); }
}
@@ -524,7 +547,7 @@
// NOTE: The video decoder thread will exit after processing the
// kWriteEndOfStream event. In this case, Reset must be called (which will
- // then StopDecoderThread) before WriteInputBuffer or WriteEndOfStream again.
+ // then StopDecoderThread) before WriteInputBuffers or WriteEndOfStream again.
SB_DCHECK(!decoder_thread_stopped_);
if (!SbThreadIsValid(decoder_thread_)) {
diff --git a/starboard/shared/win32/video_decoder.h b/starboard/shared/win32/video_decoder.h
index 2186963..c8a38e0 100644
--- a/starboard/shared/win32/video_decoder.h
+++ b/starboard/shared/win32/video_decoder.h
@@ -59,8 +59,7 @@
SbTime GetPrerollTimeout() const override { return kSbTimeMax; }
size_t GetMaxNumberOfCachedFrames() const override;
- void WriteInputBuffer(
- const scoped_refptr<InputBuffer>& input_buffer) override;
+ void WriteInputBuffers(const InputBuffers& input_buffers) override;
void WriteEndOfStream() override;
void Reset() override;
SbDecodeTarget GetCurrentDecodeTarget() override;
diff --git a/starboard/system.h b/starboard/system.h
index 0263241..233cb4d 100644
--- a/starboard/system.h
+++ b/starboard/system.h
@@ -66,9 +66,10 @@
// Full path to the executable file.
kSbSystemPathExecutableFile,
- // Path to a directory for permanent file storage. Both read and write
- // access is required. This is where an app may store its persistent settings.
- // The location should be user agnostic if possible.
+ // Path to the directory dedicated for Evergreen Full permanent file storage.
+ // Both read and write access is required.
+ // The directory should be used only for storing the updates.
+ // See starboard/doc/evergreen/cobalt_evergreen_overview.md
kSbSystemPathStorageDirectory,
} SbSystemPathId;
diff --git a/starboard/tools/abstract_launcher.py b/starboard/tools/abstract_launcher.py
index 3d7bd76..01bb410 100644
--- a/starboard/tools/abstract_launcher.py
+++ b/starboard/tools/abstract_launcher.py
@@ -52,6 +52,7 @@
output_file=None,
out_directory=None,
env_variables=None,
+ test_result_xml_path=None,
**kwargs):
"""Creates the proper launcher based upon command line args.
@@ -90,6 +91,7 @@
output_file=output_file,
out_directory=out_directory,
env_variables=env_variables,
+ test_result_xml_path=test_result_xml_path,
**kwargs)
@@ -137,6 +139,8 @@
# this variable during initialization.
self.startup_timeout_seconds = 2 * 60
+ self.test_result_xml_path = kwargs.get("test_result_xml_path", None)
+
@abc.abstractmethod
def Run(self):
"""Runs the launcher's executable.
diff --git a/starboard/tools/testing/test_runner.py b/starboard/tools/testing/test_runner.py
index 2fc8f24..312d634 100755
--- a/starboard/tools/testing/test_runner.py
+++ b/starboard/tools/testing/test_runner.py
@@ -36,6 +36,8 @@
from starboard.tools.testing.test_sharding import ShardingTestConfig
from starboard.tools.util import SetupDefaultLoggingConfig
+# pylint: disable=consider-using-f-string
+
_FLAKY_RETRY_LIMIT = 4
_TOTAL_TESTS_REGEX = re.compile(r"^\[==========\] (.*) tests? from .*"
r"test cases? ran. \(.* ms total\)")
@@ -432,7 +434,12 @@
test_params.append("--gtest_total_shards={}".format(shard_count))
test_params.append("--gtest_shard_index={}".format(shard_index))
+ # Path to where the test results XML will be created (if applicable).
+ # For on-device testing, this is w.r.t on device storage.
+ test_result_xml_path = None
+
def MakeLauncher():
+ logging.info("MakeLauncher(): %s", test_result_xml_path)
return abstract_launcher.LauncherFactory(
self.platform,
target_name,
@@ -443,21 +450,26 @@
out_directory=self.out_directory,
coverage_directory=self.coverage_directory,
env_variables=env,
+ test_result_xml_path=test_result_xml_path,
loader_platform=self.loader_platform,
loader_config=self.loader_config,
loader_out_directory=self.loader_out_directory,
launcher_args=self.launcher_args)
+ logging.info(
+ "XML test result logging: %s",
+ ("enabled" if
+ (self.log_xml_results or self.xml_output_dir) else "disabled"))
if self.log_xml_results:
out_path = MakeLauncher().GetDeviceOutputPath()
xml_filename = "{}_testoutput.xml".format(target_name)
if out_path:
- xml_path = os.path.join(out_path, xml_filename)
+ test_result_xml_path = os.path.join(out_path, xml_filename)
else:
- xml_path = xml_filename
- test_params.append("--gtest_output=xml:{}".format(xml_path))
+ test_result_xml_path = xml_filename
+ test_params.append("--gtest_output=xml:{}".format(test_result_xml_path))
logging.info(("Xml results for this test will "
- "be logged to '%s'."), xml_path)
+ "be logged to '%s'."), test_result_xml_path)
elif self.xml_output_dir:
# Have gtest create and save a test result xml
xml_output_subdir = os.path.join(self.xml_output_dir, target_name)
@@ -465,10 +477,11 @@
os.makedirs(xml_output_subdir)
except OSError:
pass
- xml_output_path = os.path.join(xml_output_subdir, "sponge_log.xml")
+ test_result_xml_path = os.path.join(xml_output_subdir, "sponge_log.xml")
logging.info("Xml output for this test will be saved to: %s",
- xml_output_path)
- test_params.append("--gtest_output=xml:%s" % (xml_output_path))
+ test_result_xml_path)
+ test_params.append("--gtest_output=xml:%s" % (test_result_xml_path))
+ logging.info("XML test result path: %s", test_result_xml_path)
# Turn off color codes from output to make it easy to parse
test_params.append("--gtest_color=no")
@@ -628,6 +641,7 @@
actual_failed_count = len(actual_failed_tests)
flaky_failed_count = len(flaky_failed_tests)
+ initial_flaky_failed_count = flaky_failed_count
filtered_count = len(filtered_tests)
# If our math does not agree with gtest...
@@ -637,9 +651,9 @@
# Retry the flaky test cases that failed, and mark them as passed if they
# succeed within the retry limit.
+ flaky_passed_tests = []
if flaky_failed_count > 0:
logging.info("RE-RUNNING FLAKY TESTS.\n")
- flaky_passed_tests = []
for test_case in flaky_failed_tests:
for retry in range(_FLAKY_RETRY_LIMIT):
# Sometimes the returned test "name" includes information about the
@@ -662,9 +676,13 @@
test_status = "SUCCEEDED"
+ all_flaky_tests_succeeded = initial_flaky_failed_count == len(
+ flaky_passed_tests)
+
# Always mark as FAILED if we have a non-zero return code, or failing
# test.
- if return_code != 0 or actual_failed_count > 0 or flaky_failed_count > 0:
+ if ((return_code != 0 and not all_flaky_tests_succeeded) or
+ actual_failed_count > 0 or flaky_failed_count > 0):
error = True
test_status = "FAILED"
failed_test_groups.append(target_name)
diff --git a/starboard/tools/unzip_file.py b/starboard/tools/unzip_file.py
new file mode 100644
index 0000000..55196d7
--- /dev/null
+++ b/starboard/tools/unzip_file.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+# Copyright 2022 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Simple helper to unzip a given archive to a specified directory."""
+
+import argparse
+import sys
+import zipfile
+
+
+def main():
+ argument_parser = argparse.ArgumentParser()
+ argument_parser.add_argument('--zip_file', required=True)
+ argument_parser.add_argument('--output_dir', required=True)
+ args = argument_parser.parse_args()
+
+ with zipfile.ZipFile(args.zip_file, 'r') as zip_ref:
+ zip_ref.extractall(args.output_dir)
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/testing/gtest/src/gtest.cc b/testing/gtest/src/gtest.cc
index 09bca96..71fbd6f 100644
--- a/testing/gtest/src/gtest.cc
+++ b/testing/gtest/src/gtest.cc
@@ -3237,38 +3237,35 @@
}
}
+#if !GTEST_OS_STARBOARD
void PrettyUnitTestResultPrinter::OnTestIterationEnd(const UnitTest& unit_test,
int /*iteration*/) {
ColoredPrintf(COLOR_GREEN, "[==========] ");
- posix::PrintF(
- "%s from %s ran.",
- FormatTestCount(unit_test.test_to_run_count()).c_str(),
- FormatTestCaseCount(unit_test.test_case_to_run_count()).c_str());
+ printf("%s from %s ran.",
+ FormatTestCount(unit_test.test_to_run_count()).c_str(),
+ FormatTestCaseCount(unit_test.test_case_to_run_count()).c_str());
if (GTEST_FLAG(print_time)) {
- posix::PrintF(
- " (%s ms total)",
- internal::StreamableToString(unit_test.elapsed_time()).c_str());
+ printf(" (%s ms total)",
+ internal::StreamableToString(unit_test.elapsed_time()).c_str());
}
- posix::PrintF("\n");
+ printf("\n");
ColoredPrintf(COLOR_GREEN, "[ PASSED ] ");
- posix::PrintF("%s.\n",
- FormatTestCount(unit_test.successful_test_count()).c_str());
+ printf("%s.\n", FormatTestCount(unit_test.successful_test_count()).c_str());
int num_failures = unit_test.failed_test_count();
if (!unit_test.Passed()) {
const int failed_test_count = unit_test.failed_test_count();
ColoredPrintf(COLOR_RED, "[ FAILED ] ");
- posix::PrintF("%s, listed below:\n",
- FormatTestCount(failed_test_count).c_str());
+ printf("%s, listed below:\n", FormatTestCount(failed_test_count).c_str());
PrintFailedTests(unit_test);
- posix::PrintF("\n%2d FAILED %s\n", num_failures,
- num_failures == 1 ? "TEST" : "TESTS");
+ printf("\n%2d FAILED %s\n", num_failures,
+ num_failures == 1 ? "TEST" : "TESTS");
}
int num_disabled = unit_test.reportable_disabled_test_count();
if (num_disabled && !GTEST_FLAG(also_run_disabled_tests)) {
if (!num_failures) {
- posix::PrintF("\n"); // Add a spacer if no FAILURE banner is displayed.
+ printf("\n"); // Add a spacer if no FAILURE banner is displayed.
}
ColoredPrintf(COLOR_YELLOW,
" YOU HAVE %d DISABLED %s\n\n",
@@ -3276,8 +3273,109 @@
num_disabled == 1 ? "TEST" : "TESTS");
}
// Ensure that Google Test output is printed before, e.g., heapchecker output.
+ fflush(stdout);
+}
+#else // !GTEST_OS_STARBOARD
+void PrettyUnitTestResultPrinter::OnTestIterationEnd(const UnitTest& unit_test,
+ int /*iteration*/) {
+ // Due to the test processes relying on regex-parsing of GTEST test result
+ // summary output, we modify this step to atomically output the logs to stdout
+ // to avoid other external SbLogRaw calls from being inserted between calls to
+ // posix:PrintF().
+ // This batching guarantees that the output is well-formatted as the test
+ // runner scripts are setup to expect.
+ // See b/251827741
+ std::stringstream out_stream;
+ out_stream << "[==========] ";
+
+ out_stream << FormatTestCount(unit_test.test_to_run_count())
+ << " from "
+ << FormatTestCaseCount(unit_test.test_case_to_run_count())
+ << " ran.";
+
+ if (GTEST_FLAG(print_time)) {
+ out_stream << " ("
+ << internal::StreamableToString(unit_test.elapsed_time())
+ << " ms total)";
+ }
+ out_stream << std::endl;
+
+ out_stream << "[ PASSED ] "
+ << FormatTestCount(unit_test.successful_test_count())
+ << "."
+ << std::endl;
+
+ int num_failures = unit_test.failed_test_count();
+ if (!unit_test.Passed()) {
+ const int failed_test_count = unit_test.failed_test_count();
+ out_stream << "[ FAILED ] "
+ << FormatTestCount(failed_test_count)
+ << ", listed below:"
+ << std::endl;
+
+ // Print each failed test case within the unit test.
+ for (int i = 0; i < unit_test.total_test_case_count(); ++i) {
+ const TestCase& test_case = *unit_test.GetTestCase(i);
+ if (!test_case.should_run() || (test_case.failed_test_count() == 0)) {
+ continue;
+ }
+ for (int j = 0; j < test_case.total_test_count(); ++j) {
+ const TestInfo& test_info = *test_case.GetTestInfo(j);
+ if (!test_info.should_run() || test_info.result()->Passed()) {
+ continue;
+ }
+ out_stream << "[ FAILED ] "
+ << test_case.name()
+ << "."
+ << test_info.name();
+
+ // Output the full test comment if present.
+ const char* const type_param = test_info.type_param();
+ const char* const value_param = test_info.value_param();
+ if (type_param != NULL || value_param != NULL) {
+ out_stream << ", where ";
+ if (type_param != NULL) {
+ out_stream << kTypeParamLabel
+ << " = "
+ << type_param;
+ if (value_param != NULL) {
+ out_stream << " and ";
+ }
+ }
+ if (value_param != NULL) {
+ out_stream << kValueParamLabel
+ << " = "
+ << value_param;
+ }
+ }
+ out_stream << std::endl;
+ }
+ }
+
+ out_stream << num_failures
+ << " FAILED "
+ << (num_failures == 1 ? "TEST" : "TESTS")
+ << std::endl;
+ }
+
+ int num_disabled = unit_test.reportable_disabled_test_count();
+ if (num_disabled && !GTEST_FLAG(also_run_disabled_tests)) {
+ if (!num_failures) {
+ // Add a spacer if no FAILURE banner is displayed.
+ out_stream << std::endl;
+ }
+ out_stream << " YOU HAVE "
+ << num_disabled
+ << " DISABLED "
+ << (num_disabled == 1 ? "TEST" : "TESTS")
+ << std::endl
+ << std::endl;
+ }
+ // Ensure that Google Test output is printed before, e.g., heapchecker output.
+ posix::PrintF("%s", out_stream.str().c_str());
posix::Flush();
}
+#endif // !GTEST_OS_STARBOARD
// End PrettyUnitTestResultPrinter
diff --git a/third_party/angle/src/libANGLE/renderer/d3d/DynamicHLSL.cpp b/third_party/angle/src/libANGLE/renderer/d3d/DynamicHLSL.cpp
index d50e5f3..18d2410 100644
--- a/third_party/angle/src/libANGLE/renderer/d3d/DynamicHLSL.cpp
+++ b/third_party/angle/src/libANGLE/renderer/d3d/DynamicHLSL.cpp
@@ -75,7 +75,7 @@
" float3 j = pow(SRGB_OETF(i), gamma);\n"
" float3 adj = (minLNits + (maxLNits - minLNits) * j) / kRefWhiteLevelSRGB;\n"
" float3 cri_float = (float3)cri;\n"
- " float3 ret = lerp(adj, L, cri_float);\n"
+ " float3 ret = lerp(L, adj, cri_float);\n"
" return ret;\n"
"}\n"
"//input: normalized L in units of RefWhite (1.0=100nits), output: normalized E\n"
@@ -97,7 +97,7 @@
"{\n"
" PS_OUTPUT output;\n"
" \n"
- " float3 input_colors = gl_Color[0].rgb;\n"
+ " float3 input_colors = max(gl_Color[0].rgb, 0);\n"
" float3 lin_osd_graphics = SRGB_EOTF(input_colors);\n"
" lin_osd_graphics = mul(BT709_TO_BT2020, lin_osd_graphics);\n"
" lin_osd_graphics *= kMaxNits/kRefWhiteLevelSRGB;\n"
diff --git a/third_party/angle/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp b/third_party/angle/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp
index 8268fba..5ff2eaa 100644
--- a/third_party/angle/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp
+++ b/third_party/angle/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp
@@ -428,12 +428,9 @@
#if defined(STARBOARD)
- // Only allow feature level 10 on starboard by default.
-#if defined(ENABLE_D3D11_FEATURE_LEVEL_11)
+ // D3D11CreateDevice will choose proper feature level from this list.
mAvailableFeatureLevels.push_back(D3D_FEATURE_LEVEL_11_0);
-#else
mAvailableFeatureLevels.push_back(D3D_FEATURE_LEVEL_10_0);
-#endif // defined(ENABLE_D3D11_FEATURE_LEVEL_11)
#else
if (requestedMajorVersion == EGL_DONT_CARE || requestedMajorVersion >= 11)
{
diff --git a/third_party/boringssl/src/crypto/x509/x509_test.cc b/third_party/boringssl/src/crypto/x509/x509_test.cc
index 551bd8ca..52e6c95 100644
--- a/third_party/boringssl/src/crypto/x509/x509_test.cc
+++ b/third_party/boringssl/src/crypto/x509/x509_test.cc
@@ -1504,3 +1504,182 @@
EXPECT_EQ(X509_V_ERR_INVALID_CA,
Verify(leaf.get(), {root.get()}, {intermediate.get()}, {}, 0));
}
+
+TEST(X509Test, GeneralName) {
+ const std::vector<uint8_t> kNames[] = {
+ // [0] {
+ // OBJECT_IDENTIFIER { 1.2.840.113554.4.1.72585.2.1 }
+ // [0] {
+ // SEQUENCE {}
+ // }
+ // }
+ {0xa0, 0x13, 0x06, 0x0d, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x12, 0x04,
+ 0x01, 0x84, 0xb7, 0x09, 0x02, 0x01, 0xa0, 0x02, 0x30, 0x00},
+ // [0] {
+ // OBJECT_IDENTIFIER { 1.2.840.113554.4.1.72585.2.1 }
+ // [0] {
+ // [APPLICATION 0] {}
+ // }
+ // }
+ {0xa0, 0x13, 0x06, 0x0d, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x12, 0x04,
+ 0x01, 0x84, 0xb7, 0x09, 0x02, 0x01, 0xa0, 0x02, 0x60, 0x00},
+ // [0] {
+ // OBJECT_IDENTIFIER { 1.2.840.113554.4.1.72585.2.1 }
+ // [0] {
+ // UTF8String { "a" }
+ // }
+ // }
+ {0xa0, 0x14, 0x06, 0x0d, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x12, 0x04,
+ 0x01, 0x84, 0xb7, 0x09, 0x02, 0x01, 0xa0, 0x03, 0x0c, 0x01, 0x61},
+ // [0] {
+ // OBJECT_IDENTIFIER { 1.2.840.113554.4.1.72585.2.2 }
+ // [0] {
+ // UTF8String { "a" }
+ // }
+ // }
+ {0xa0, 0x14, 0x06, 0x0d, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x12, 0x04,
+ 0x01, 0x84, 0xb7, 0x09, 0x02, 0x02, 0xa0, 0x03, 0x0c, 0x01, 0x61},
+ // [0] {
+ // OBJECT_IDENTIFIER { 1.2.840.113554.4.1.72585.2.1 }
+ // [0] {
+ // UTF8String { "b" }
+ // }
+ // }
+ {0xa0, 0x14, 0x06, 0x0d, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x12, 0x04,
+ 0x01, 0x84, 0xb7, 0x09, 0x02, 0x01, 0xa0, 0x03, 0x0c, 0x01, 0x62},
+ // [0] {
+ // OBJECT_IDENTIFIER { 1.2.840.113554.4.1.72585.2.1 }
+ // [0] {
+ // BOOLEAN { TRUE }
+ // }
+ // }
+ {0xa0, 0x14, 0x06, 0x0d, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x12, 0x04,
+ 0x01, 0x84, 0xb7, 0x09, 0x02, 0x01, 0xa0, 0x03, 0x01, 0x01, 0xff},
+ // [0] {
+ // OBJECT_IDENTIFIER { 1.2.840.113554.4.1.72585.2.1 }
+ // [0] {
+ // BOOLEAN { FALSE }
+ // }
+ // }
+ {0xa0, 0x14, 0x06, 0x0d, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x12, 0x04,
+ 0x01, 0x84, 0xb7, 0x09, 0x02, 0x01, 0xa0, 0x03, 0x01, 0x01, 0x00},
+ // [1 PRIMITIVE] { "a" }
+ {0x81, 0x01, 0x61},
+ // [1 PRIMITIVE] { "b" }
+ {0x81, 0x01, 0x62},
+ // [2 PRIMITIVE] { "a" }
+ {0x82, 0x01, 0x61},
+ // [2 PRIMITIVE] { "b" }
+ {0x82, 0x01, 0x62},
+ // [4] {
+ // SEQUENCE {
+ // SET {
+ // SEQUENCE {
+ // # commonName
+ // OBJECT_IDENTIFIER { 2.5.4.3 }
+ // UTF8String { "a" }
+ // }
+ // }
+ // }
+ // }
+ {0xa4, 0x0e, 0x30, 0x0c, 0x31, 0x0a, 0x30, 0x08, 0x06, 0x03, 0x55, 0x04,
+ 0x03, 0x0c, 0x01, 0x61},
+ // [4] {
+ // SEQUENCE {
+ // SET {
+ // SEQUENCE {
+ // # commonName
+ // OBJECT_IDENTIFIER { 2.5.4.3 }
+ // UTF8String { "b" }
+ // }
+ // }
+ // }
+ // }
+ {0xa4, 0x0e, 0x30, 0x0c, 0x31, 0x0a, 0x30, 0x08, 0x06, 0x03, 0x55, 0x04,
+ 0x03, 0x0c, 0x01, 0x62},
+ // [5] {
+ // [1] {
+ // UTF8String { "a" }
+ // }
+ // }
+ {0xa5, 0x05, 0xa1, 0x03, 0x0c, 0x01, 0x61},
+ // [5] {
+ // [1] {
+ // UTF8String { "b" }
+ // }
+ // }
+ {0xa5, 0x05, 0xa1, 0x03, 0x0c, 0x01, 0x62},
+ // [5] {
+ // [0] {
+ // UTF8String {}
+ // }
+ // [1] {
+ // UTF8String { "a" }
+ // }
+ // }
+ {0xa5, 0x09, 0xa0, 0x02, 0x0c, 0x00, 0xa1, 0x03, 0x0c, 0x01, 0x61},
+ // [5] {
+ // [0] {
+ // UTF8String { "a" }
+ // }
+ // [1] {
+ // UTF8String { "a" }
+ // }
+ // }
+ {0xa5, 0x0a, 0xa0, 0x03, 0x0c, 0x01, 0x61, 0xa1, 0x03, 0x0c, 0x01, 0x61},
+ // [5] {
+ // [0] {
+ // UTF8String { "b" }
+ // }
+ // [1] {
+ // UTF8String { "a" }
+ // }
+ // }
+ {0xa5, 0x0a, 0xa0, 0x03, 0x0c, 0x01, 0x62, 0xa1, 0x03, 0x0c, 0x01, 0x61},
+ // [6 PRIMITIVE] { "a" }
+ {0x86, 0x01, 0x61},
+ // [6 PRIMITIVE] { "b" }
+ {0x86, 0x01, 0x62},
+ // [7 PRIMITIVE] { `11111111` }
+ {0x87, 0x04, 0x11, 0x11, 0x11, 0x11},
+ // [7 PRIMITIVE] { `22222222`}
+ {0x87, 0x04, 0x22, 0x22, 0x22, 0x22},
+ // [7 PRIMITIVE] { `11111111111111111111111111111111` }
+ {0x87, 0x10, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
+ 0x11, 0x11, 0x11, 0x11, 0x11, 0x11},
+ // [7 PRIMITIVE] { `22222222222222222222222222222222` }
+ {0x87, 0x10, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
+ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22},
+ // [8 PRIMITIVE] { 1.2.840.113554.4.1.72585.2.1 }
+ {0x88, 0x0d, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x12, 0x04, 0x01, 0x84, 0xb7,
+ 0x09, 0x02, 0x01},
+ // [8 PRIMITIVE] { 1.2.840.113554.4.1.72585.2.2 }
+ {0x88, 0x0d, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x12, 0x04, 0x01, 0x84, 0xb7,
+ 0x09, 0x02, 0x02},
+ };
+
+ // Every name should be equal to itself and not equal to any others.
+ for (size_t i = 0; i < OPENSSL_ARRAY_SIZE(kNames); i++) {
+ SCOPED_TRACE(Bytes(kNames[i]));
+
+ const uint8_t *ptr = kNames[i].data();
+ bssl::UniquePtr<GENERAL_NAME> a(
+ d2i_GENERAL_NAME(nullptr, &ptr, kNames[i].size()));
+ ASSERT_TRUE(a);
+
+ for (size_t j = 0; j < OPENSSL_ARRAY_SIZE(kNames); j++) {
+ SCOPED_TRACE(Bytes(kNames[j]));
+
+ ptr = kNames[j].data();
+ bssl::UniquePtr<GENERAL_NAME> b(
+ d2i_GENERAL_NAME(nullptr, &ptr, kNames[j].size()));
+ ASSERT_TRUE(b);
+
+ if (i == j) {
+ EXPECT_EQ(GENERAL_NAME_cmp(a.get(), b.get()), 0);
+ } else {
+ EXPECT_NE(GENERAL_NAME_cmp(a.get(), b.get()), 0);
+ }
+ }
+ }
+}
diff --git a/third_party/boringssl/src/crypto/x509v3/v3_genn.c b/third_party/boringssl/src/crypto/x509v3/v3_genn.c
index 8c92687..0c7900c 100644
--- a/third_party/boringssl/src/crypto/x509v3/v3_genn.c
+++ b/third_party/boringssl/src/crypto/x509v3/v3_genn.c
@@ -72,8 +72,9 @@
IMPLEMENT_ASN1_FUNCTIONS(OTHERNAME)
ASN1_SEQUENCE(EDIPARTYNAME) = {
- ASN1_IMP_OPT(EDIPARTYNAME, nameAssigner, DIRECTORYSTRING, 0),
- ASN1_IMP_OPT(EDIPARTYNAME, partyName, DIRECTORYSTRING, 1)
+ /* DirectoryString is a CHOICE type, so use explicit tagging. */
+ ASN1_EXP_OPT(EDIPARTYNAME, nameAssigner, DIRECTORYSTRING, 0),
+ ASN1_EXP(EDIPARTYNAME, partyName, DIRECTORYSTRING, 1)
} ASN1_SEQUENCE_END(EDIPARTYNAME)
IMPLEMENT_ASN1_FUNCTIONS(EDIPARTYNAME)
@@ -107,6 +108,24 @@
(char *)a);
}
+static int edipartyname_cmp(const EDIPARTYNAME *a, const EDIPARTYNAME *b)
+{
+ /* nameAssigner is optional and may be NULL. */
+ if (a->nameAssigner == NULL) {
+ if (b->nameAssigner != NULL) {
+ return -1;
+ }
+ } else {
+ if (b->nameAssigner == NULL ||
+ ASN1_STRING_cmp(a->nameAssigner, b->nameAssigner) != 0) {
+ return -1;
+ }
+ }
+
+ /* partyName may not be NULL. */
+ return ASN1_STRING_cmp(a->partyName, b->partyName);
+}
+
/* Returns 0 if they are equal, != 0 otherwise. */
int GENERAL_NAME_cmp(GENERAL_NAME *a, GENERAL_NAME *b)
{
@@ -116,8 +135,11 @@
return -1;
switch (a->type) {
case GEN_X400:
+ result = ASN1_TYPE_cmp(a->d.x400Address, b->d.x400Address);
+ break;
+
case GEN_EDIPARTY:
- result = ASN1_TYPE_cmp(a->d.other, b->d.other);
+ result = edipartyname_cmp(a->d.ediPartyName, b->d.ediPartyName);
break;
case GEN_OTHERNAME:
@@ -164,8 +186,11 @@
{
switch (type) {
case GEN_X400:
+ a->d.x400Address = value;
+ break;
+
case GEN_EDIPARTY:
- a->d.other = value;
+ a->d.ediPartyName = value;
break;
case GEN_OTHERNAME:
@@ -199,8 +224,10 @@
*ptype = a->type;
switch (a->type) {
case GEN_X400:
+ return a->d.x400Address;
+
case GEN_EDIPARTY:
- return a->d.other;
+ return a->d.ediPartyName;
case GEN_OTHERNAME:
return a->d.otherName;
diff --git a/third_party/boringssl/src/include/openssl/x509v3.h b/third_party/boringssl/src/include/openssl/x509v3.h
index 1af439d..e376050 100644
--- a/third_party/boringssl/src/include/openssl/x509v3.h
+++ b/third_party/boringssl/src/include/openssl/x509v3.h
@@ -534,6 +534,12 @@
DECLARE_ASN1_FUNCTIONS(GENERAL_NAME)
OPENSSL_EXPORT GENERAL_NAME *GENERAL_NAME_dup(GENERAL_NAME *a);
+
+// GENERAL_NAME_cmp returns zero if |a| and |b| are equal and a non-zero
+// value otherwise. Note this function does not provide a comparison suitable
+// for sorting.
+//
+// TODO(davidben): Const-correct this function.
OPENSSL_EXPORT int GENERAL_NAME_cmp(GENERAL_NAME *a, GENERAL_NAME *b);
diff --git a/third_party/chromium/media/base/decoder_buffer.cc b/third_party/chromium/media/base/decoder_buffer.cc
index 6060c9b..d242506 100644
--- a/third_party/chromium/media/base/decoder_buffer.cc
+++ b/third_party/chromium/media/base/decoder_buffer.cc
@@ -7,9 +7,6 @@
#include <sstream>
#include "base/debug/alias.h"
-#if defined(STARBOARD)
-#include "starboard/media.h"
-#endif // defined(STARBOARD)
namespace media {
@@ -20,6 +17,12 @@
} // namespace
// static
+DecoderBuffer::Allocator* DecoderBuffer::Allocator::GetInstance() {
+ DCHECK(s_allocator);
+ return s_allocator;
+}
+
+// static
void DecoderBuffer::Allocator::Set(Allocator* allocator) {
s_allocator = allocator;
}
@@ -73,8 +76,7 @@
is_key_frame_(false) {}
DecoderBuffer::DecoderBuffer(
- std::unique_ptr<ReadOnlyUnalignedMapping> shared_mem_mapping,
- size_t size)
+ std::unique_ptr<ReadOnlyUnalignedMapping> shared_mem_mapping, size_t size)
: size_(size),
side_data_size_(0),
shared_mem_mapping_(std::move(shared_mem_mapping)),
@@ -96,15 +98,8 @@
DCHECK(s_allocator);
DCHECK(!data_);
-#if SB_API_VERSION >= 14
- int alignment = SbMediaGetBufferAlignment();
- int padding = SbMediaGetBufferPadding();
-#else // SB_API_VERSION >= 14
- int alignment = std::max(SbMediaGetBufferAlignment(kSbMediaTypeAudio),
- SbMediaGetBufferAlignment(kSbMediaTypeVideo));
- int padding = std::max(SbMediaGetBufferPadding(kSbMediaTypeAudio),
- SbMediaGetBufferPadding(kSbMediaTypeVideo));
-#endif // SB_API_VERSION >= 14
+ int alignment = s_allocator->GetBufferAlignment();
+ int padding = s_allocator->GetBufferPadding();
allocated_size_ = size_ + padding;
data_ = static_cast<uint8_t*>(s_allocator->Allocate(allocated_size_,
alignment));
diff --git a/third_party/chromium/media/base/decoder_buffer.h b/third_party/chromium/media/base/decoder_buffer.h
index ed7c01e..5892e26 100644
--- a/third_party/chromium/media/base/decoder_buffer.h
+++ b/third_party/chromium/media/base/decoder_buffer.h
@@ -21,9 +21,11 @@
#include "media/base/decrypt_config.h"
#include "media/base/media_export.h"
#include "media/base/timestamp_constants.h"
-#if !defined(STARBOARD)
+#if defined(STARBOARD)
+#include "starboard/media.h"
+#else // defined(STARBOARD)
#include "media/base/unaligned_shared_memory.h"
-#endif // !defined(STARBOARD)
+#endif // defined(STARBOARD)
namespace media {
@@ -46,16 +48,31 @@
#if defined(STARBOARD)
class Allocator {
- public:
- // The function should never return nullptr. It may terminate the app on
- // allocation failure.
- virtual void* Allocate(size_t size, size_t alignment) = 0;
- virtual void Free(void* p, size_t size) = 0;
+ public:
+ static Allocator* GetInstance();
- protected:
- ~Allocator() {}
+ // The function should never return nullptr. It may terminate the app on
+ // allocation failure.
+ virtual void* Allocate(size_t size, size_t alignment) = 0;
+ virtual void Free(void* p, size_t size) = 0;
- static void Set(Allocator* allocator);
+ virtual int GetAudioBufferBudget() const = 0;
+ virtual int GetBufferAlignment() const = 0;
+ virtual int GetBufferPadding() const = 0;
+ virtual SbTime GetBufferGarbageCollectionDurationThreshold() const = 0;
+ virtual int GetProgressiveBufferBudget(SbMediaVideoCodec codec,
+ int resolution_width,
+ int resolution_height,
+ int bits_per_pixel) const = 0;
+ virtual int GetVideoBufferBudget(SbMediaVideoCodec codec,
+ int resolution_width,
+ int resolution_height,
+ int bits_per_pixel) const = 0;
+
+ protected:
+ ~Allocator() {}
+
+ static void Set(Allocator* allocator);
};
#endif // defined(STARBOARD)
diff --git a/third_party/chromium/media/base/demuxer_memory_limit_starboard.cc b/third_party/chromium/media/base/demuxer_memory_limit_starboard.cc
index 19fca6b..d8305f0 100644
--- a/third_party/chromium/media/base/demuxer_memory_limit_starboard.cc
+++ b/third_party/chromium/media/base/demuxer_memory_limit_starboard.cc
@@ -14,16 +14,16 @@
#include "media/base/demuxer_memory_limit.h"
+#include "media/base/decoder_buffer.h"
#include "media/base/starboard_utils.h"
#include "media/base/video_codecs.h"
#include "base/logging.h"
-#include "starboard/media.h"
namespace media {
size_t GetDemuxerStreamAudioMemoryLimit(
const AudioDecoderConfig* /*audio_config*/) {
- return SbMediaGetAudioBufferBudget();
+ return DecoderBuffer::Allocator::GetInstance()->GetAudioBufferBudget();
}
size_t GetDemuxerStreamVideoMemoryLimit(
diff --git a/third_party/chromium/media/base/demuxer_stream.h b/third_party/chromium/media/base/demuxer_stream.h
index 9a31c24..67769b3 100644
--- a/third_party/chromium/media/base/demuxer_stream.h
+++ b/third_party/chromium/media/base/demuxer_stream.h
@@ -5,6 +5,10 @@
#ifndef MEDIA_BASE_DEMUXER_STREAM_H_
#define MEDIA_BASE_DEMUXER_STREAM_H_
+#if defined(STARBOARD)
+#include <vector>
+#endif // defined(STARBOARD)
+
#include "base/callback.h"
#include "base/memory/ref_counted.h"
#include "media/base/media_export.h"
@@ -73,8 +77,13 @@
// The first parameter indicates the status of the read.
// The second parameter is non-NULL and contains media data
// or the end of the stream if the first parameter is kOk. NULL otherwise.
+#if defined(STARBOARD)
+ typedef base::OnceCallback<void(Status, const std::vector<scoped_refptr<DecoderBuffer>>&)> ReadCB;
+ virtual void Read(int max_number_of_buffers_to_read, ReadCB read_cb) = 0;
+#else // defined (STARBOARD)
typedef base::OnceCallback<void(Status, scoped_refptr<DecoderBuffer>)> ReadCB;
virtual void Read(ReadCB read_cb) = 0;
+#endif // defined (STARBOARD)
// Returns the audio/video decoder configuration. It is an error to call the
// audio method on a video stream and vice versa. After |kConfigChanged| is
diff --git a/third_party/chromium/media/base/starboard_utils.cc b/third_party/chromium/media/base/starboard_utils.cc
index 687e5af..96fb524 100644
--- a/third_party/chromium/media/base/starboard_utils.cc
+++ b/third_party/chromium/media/base/starboard_utils.cc
@@ -19,6 +19,7 @@
#include "base/logging.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
+#include "media/base/decoder_buffer.h"
#include "media/base/decrypt_config.h"
#include "starboard/common/media.h"
#include "starboard/configuration.h"
@@ -382,18 +383,19 @@
return sb_media_color_metadata;
}
-
int GetSbMediaVideoBufferBudget(const VideoDecoderConfig* video_config,
const std::string& mime_type) {
if (!video_config) {
- return SbMediaGetVideoBufferBudget(kSbMediaVideoCodecH264, 1920, 1080, 8);
+ return DecoderBuffer::Allocator::GetInstance()->GetVideoBufferBudget(
+ kSbMediaVideoCodecH264, 1920, 1080, 8);
}
auto width = video_config->visible_rect().size().width();
auto height = video_config->visible_rect().size().height();
auto bits_per_pixel = GetBitsPerPixel(mime_type);
auto codec = MediaVideoCodecToSbMediaVideoCodec(video_config->codec());
- return SbMediaGetVideoBufferBudget(codec, width, height, bits_per_pixel);
+ return DecoderBuffer::Allocator::GetInstance()->GetVideoBufferBudget(
+ codec, width, height, bits_per_pixel);
}
std::string ExtractCodecs(const std::string& mime_type) {
diff --git a/third_party/chromium/media/filters/chunk_demuxer.cc b/third_party/chromium/media/filters/chunk_demuxer.cc
index e738be9..1d6e475 100644
--- a/third_party/chromium/media/filters/chunk_demuxer.cc
+++ b/third_party/chromium/media/filters/chunk_demuxer.cc
@@ -127,8 +127,15 @@
DVLOG(1) << "ChunkDemuxerStream::AbortReads()";
base::AutoLock auto_lock(lock_);
ChangeState_Locked(RETURNING_ABORT_FOR_READS);
- if (read_cb_)
+ pending_config_change_ = false;
+
+ if (read_cb_) {
+#if defined(STARBOARD)
+ std::move(read_cb_).Run(kAborted, {});
+#else // defined (STARBOARD)
std::move(read_cb_).Run(kAborted, nullptr);
+#endif // defined (STARBOARD)
+ }
}
void ChunkDemuxerStream::CompletePendingReadIfPossible() {
@@ -147,8 +154,12 @@
// Pass an end of stream buffer to the pending callback to signal that no more
// data will be sent.
if (read_cb_) {
+#if defined(STARBOARD)
+ std::move(read_cb_).Run(DemuxerStream::kOk, {StreamParserBuffer::CreateEOSBuffer()});
+#else // defined (STARBOARD)
std::move(read_cb_).Run(DemuxerStream::kOk,
StreamParserBuffer::CreateEOSBuffer());
+#endif // defined (STARBOARD)
}
}
@@ -348,18 +359,33 @@
}
// DemuxerStream methods.
+#if defined(STARBOARD)
+void ChunkDemuxerStream::Read(int max_number_of_buffers_to_read, ReadCB read_cb) {
+#else // defined(STARBOARD)
void ChunkDemuxerStream::Read(ReadCB read_cb) {
+#endif // defined(STARBOARD)
base::AutoLock auto_lock(lock_);
DCHECK_NE(state_, UNINITIALIZED);
DCHECK(!read_cb_);
read_cb_ = BindToCurrentLoop(std::move(read_cb));
+#if defined(STARBOARD)
+ max_number_of_buffers_to_read_ = max_number_of_buffers_to_read;
+
+ if (!is_enabled_) {
+ DVLOG(1) << "Read from disabled stream, returning EOS";
+ std::move(read_cb_).Run(kOk, {StreamParserBuffer::CreateEOSBuffer()});
+ return;
+ }
+#else // defined(STARBOARD)
+
if (!is_enabled_) {
DVLOG(1) << "Read from disabled stream, returning EOS";
std::move(read_cb_).Run(kOk, StreamParserBuffer::CreateEOSBuffer());
return;
}
+#endif // defined(STARBOARD)
CompletePendingReadIfPossible_Locked();
}
@@ -406,7 +432,11 @@
stream_->Seek(timestamp);
} else if (read_cb_) {
DVLOG(1) << "Read from disabled stream, returning EOS";
+#if defined(STARBOARD)
+ std::move(read_cb_).Run(kOk, {StreamParserBuffer::CreateEOSBuffer()});
+#else // defined(STARBOARD)
std::move(read_cb_).Run(kOk, StreamParserBuffer::CreateEOSBuffer());
+#endif // defined(STARBOARD)
}
}
@@ -436,6 +466,72 @@
ChunkDemuxerStream::~ChunkDemuxerStream() = default;
+#if defined(STARBOARD)
+void ChunkDemuxerStream::CompletePendingReadIfPossible_Locked() {
+ lock_.AssertAcquired();
+ DCHECK(read_cb_);
+
+ DemuxerStream::Status status = DemuxerStream::kAborted;
+ std::vector<scoped_refptr<DecoderBuffer>> buffers;
+
+ if (pending_config_change_) {
+ status = kConfigChanged;
+ std::move(read_cb_).Run(status, buffers);
+ pending_config_change_ = false;
+ return;
+ }
+
+ if (state_ == RETURNING_DATA_FOR_READS) {
+ for (int i = 0; i < max_number_of_buffers_to_read_; i++) {
+ scoped_refptr<StreamParserBuffer> buffer;
+ SourceBufferStreamStatus stream_status = stream_->GetNextBuffer(&buffer);
+ if (stream_status == SourceBufferStreamStatus::kSuccess) {
+ DVLOG(2) << __func__ << ": found kOk, type " << type_ << ", dts "
+ << buffer->GetDecodeTimestamp().InSecondsF() << ", pts "
+ << buffer->timestamp().InSecondsF() << ", dur "
+ << buffer->duration().InSecondsF() << ", key "
+ << buffer->is_key_frame();
+ buffers.push_back(std::move(buffer));
+ status = DemuxerStream::kOk;
+ } else if (stream_status == SourceBufferStreamStatus::kEndOfStream) {
+ buffer = StreamParserBuffer::CreateEOSBuffer();
+ DVLOG(2) << __func__ << ": found kOk with EOS buffer, type "
+ << type_;
+ buffers.push_back(std::move(buffer));
+ status = DemuxerStream::kOk;
+ break;
+ } else if (stream_status == SourceBufferStreamStatus::kConfigChange) {
+ DVLOG(2) << __func__ << ": returning kConfigChange, type " << type_;
+ status = kConfigChanged;
+ break;
+ } else if (stream_status == SourceBufferStreamStatus::kNeedBuffer) {
+ if (buffers.empty())
+ return;
+ else
+ break;
+ }
+ }
+
+ if (status == kConfigChanged && !buffers.empty()) {
+ pending_config_change_ = true;
+ status = kOk;
+ }
+
+ } else if (state_ == RETURNING_ABORT_FOR_READS) {
+ status = DemuxerStream::kAborted;
+ DVLOG(2) << __func__ << ": returning kAborted, type " << type_;
+ } else if (state_ == SHUTDOWN) {
+ status = DemuxerStream::kOk;
+ buffers.push_back(std::move(StreamParserBuffer::CreateEOSBuffer()));
+ DVLOG(2) << __func__ << ": returning kOk with EOS buffer, type " << type_;
+ } else if (state_ == UNINITIALIZED) {
+ NOTREACHED();
+ return;
+ }
+
+ std::move(read_cb_).Run(status, buffers);
+}
+#else // defined(STARBOARD)
void ChunkDemuxerStream::CompletePendingReadIfPossible_Locked() {
lock_.AssertAcquired();
DCHECK(read_cb_);
@@ -492,6 +588,7 @@
std::move(read_cb_).Run(status, buffer);
}
+#endif // defined(STARBOARD)
ChunkDemuxer::ChunkDemuxer(
base::OnceClosure open_cb,
diff --git a/third_party/chromium/media/filters/chunk_demuxer.h b/third_party/chromium/media/filters/chunk_demuxer.h
index e59fcfb..fd67e6f 100644
--- a/third_party/chromium/media/filters/chunk_demuxer.h
+++ b/third_party/chromium/media/filters/chunk_demuxer.h
@@ -130,7 +130,12 @@
std::string mime_type() const override { return mime_type_; }
#endif // defined (STARBOARD)
+#if defined(STARBOARD)
+ void Read(int max_number_of_buffers_to_read, ReadCB read_cb) override;
+#else // defined (STARBOARD)
void Read(ReadCB read_cb) override;
+#endif // defined (STARBOARD)
+
Type type() const override;
Liveness liveness() const override;
AudioDecoderConfig audio_decoder_config() override;
@@ -180,6 +185,8 @@
#if defined(STARBOARD)
const std::string mime_type_;
+ int max_number_of_buffers_to_read_{1};
+ bool pending_config_change_ {false};
#endif // defined (STARBOARD)
// Specifies the type of the stream.
diff --git a/third_party/chromium/media/filters/source_buffer_stream.cc b/third_party/chromium/media/filters/source_buffer_stream.cc
index b6d4bbc..a1e9e2e 100644
--- a/third_party/chromium/media/filters/source_buffer_stream.cc
+++ b/third_party/chromium/media/filters/source_buffer_stream.cc
@@ -14,12 +14,12 @@
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/trace_event/trace_event.h"
+#if defined(STARBOARD)
+#include "media/base/decoder_buffer.h"
+#endif // defined(STARBOARD)
#include "media/base/demuxer_memory_limit.h"
#include "media/base/media_switches.h"
#include "media/base/timestamp_constants.h"
-#if defined(STARBOARD)
-#include "starboard/media.h"
-#endif // defined(STARBOARD)
namespace media {
@@ -849,7 +849,8 @@
// Address duration based GC.
base::TimeDelta duration = GetBufferedDurationForGarbageCollection();
const SbTime duration_gc_threadold =
- SbMediaGetBufferGarbageCollectionDurationThreshold();
+ DecoderBuffer::Allocator::GetInstance()
+ ->GetBufferGarbageCollectionDurationThreshold();
if (duration.ToSbTime() > duration_gc_threadold) {
effective_memory_limit = ranges_size * duration_gc_threadold /
duration.ToSbTime();
diff --git a/third_party/crashpad/handler/BUILD.gn b/third_party/crashpad/handler/BUILD.gn
index 10582a8..c91280d 100644
--- a/third_party/crashpad/handler/BUILD.gn
+++ b/third_party/crashpad/handler/BUILD.gn
@@ -153,7 +153,8 @@
}
}
-if (!crashpad_is_ios) {
+# TODO: b/256660693 Disabled crashpad_handler executables due to build issues on Android
+if (!crashpad_is_ios && !crashpad_is_android) {
if (crashpad_is_in_starboard) {
config("crashpad_handler_starboard_config") {
cflags = [
@@ -207,27 +208,28 @@
# It is normal to package native code as a loadable module but Android's APK
# installer will ignore files not named like a shared object, so give the
# handler executable an acceptable name.
-if (crashpad_is_android) {
- copy("crashpad_handler_named_as_so") {
- deps = [ ":crashpad_handler" ]
-
- sources = [ "$root_out_dir/crashpad_handler" ]
-
- outputs = [ "$root_out_dir/libcrashpad_handler.so" ]
- }
-
- crashpad_executable("crashpad_handler_trampoline") {
- output_name = "libcrashpad_handler_trampoline.so"
-
- sources = [ "linux/handler_trampoline.cc" ]
-
- ldflags = [ "-llog" ]
-
- if (crashpad_is_in_chromium) {
- no_default_deps = true
- }
- }
-}
+# TODO: b/256660693 Disabled crashpad_handler executables due to build issues on Android
+# if (crashpad_is_android) {
+# copy("crashpad_handler_named_as_so") {
+# deps = [ ":crashpad_handler" ]
+#
+# sources = [ "$root_out_dir/crashpad_handler" ]
+#
+# outputs = [ "$root_out_dir/libcrashpad_handler.so" ]
+# }
+#
+# crashpad_executable("crashpad_handler_trampoline") {
+# output_name = "libcrashpad_handler_trampoline.so"
+#
+# sources = [ "linux/handler_trampoline.cc" ]
+#
+# ldflags = [ "-llog" ]
+#
+# if (crashpad_is_in_chromium) {
+# no_default_deps = true
+# }
+# }
+# }
if (!crashpad_is_ios && !crashpad_is_in_starboard) {
crashpad_executable("crashpad_handler_test_extended_handler") {
diff --git a/third_party/crashpad/test/gtest_main.cc b/third_party/crashpad/test/gtest_main.cc
index ad3a095..23b416d 100644
--- a/third_party/crashpad/test/gtest_main.cc
+++ b/third_party/crashpad/test/gtest_main.cc
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+#include "base/base_wrapper.h"
#include "build/build_config.h"
#include "gtest/gtest.h"
#include "test/main_arguments.h"
diff --git a/third_party/jinja2/filters.py b/third_party/jinja2/filters.py
index fd0db04..8dbad84 100644
--- a/third_party/jinja2/filters.py
+++ b/third_party/jinja2/filters.py
@@ -21,7 +21,7 @@
from jinja2._compat import next, imap, string_types, text_type, iteritems
-_word_re = re.compile(r'\w+(?u)')
+_word_re = re.compile(r'\w+')
def contextfilter(f):
@@ -183,7 +183,7 @@
uppercase letters, all remaining characters are lowercase.
"""
rv = []
- for item in re.compile(r'([-\s]+)(?u)').split(s):
+ for item in re.compile(r'([-\s]+)').split(s):
if not item:
continue
rv.append(item[0].upper() + item[1:].lower())
diff --git a/third_party/libfdkaac/LICENSE b/third_party/libfdkaac/LICENSE
new file mode 100644
index 0000000..796c2a2
--- /dev/null
+++ b/third_party/libfdkaac/LICENSE
@@ -0,0 +1,61 @@
+/* -----------------------------------------------------------------------------------------------------------
+Software License for The Fraunhofer FDK AAC Codec Library for Android
+© Copyright 1995 - 2013 Fraunhofer-Gesellschaft zur Förderung der angewandten Forschung e.V.
+ All rights reserved.
+ 1. INTRODUCTION
+The Fraunhofer FDK AAC Codec Library for Android ("FDK AAC Codec") is software that implements
+the MPEG Advanced Audio Coding ("AAC") encoding and decoding scheme for digital audio.
+This FDK AAC Codec software is intended to be used on a wide variety of Android devices.
+AAC's HE-AAC and HE-AAC v2 versions are regarded as today's most efficient general perceptual
+audio codecs. AAC-ELD is considered the best-performing full-bandwidth communications codec by
+independent studies and is widely deployed. AAC has been standardized by ISO and IEC as part
+of the MPEG specifications.
+Patent licenses for necessary patent claims for the FDK AAC Codec (including those of Fraunhofer)
+may be obtained through Via Licensing (www.vialicensing.com) or through the respective patent owners
+individually for the purpose of encoding or decoding bit streams in products that are compliant with
+the ISO/IEC MPEG audio standards. Please note that most manufacturers of Android devices already license
+these patent claims through Via Licensing or directly from the patent owners, and therefore FDK AAC Codec
+software may already be covered under those patent licenses when it is used for those licensed purposes only.
+Commercially-licensed AAC software libraries, including floating-point versions with enhanced sound quality,
+are also available from Fraunhofer. Users are encouraged to check the Fraunhofer website for additional
+applications information and documentation.
+2. COPYRIGHT LICENSE
+Redistribution and use in source and binary forms, with or without modification, are permitted without
+payment of copyright license fees provided that you satisfy the following conditions:
+You must retain the complete text of this software license in redistributions of the FDK AAC Codec or
+your modifications thereto in source code form.
+You must retain the complete text of this software license in the documentation and/or other materials
+provided with redistributions of the FDK AAC Codec or your modifications thereto in binary form.
+You must make available free of charge copies of the complete source code of the FDK AAC Codec and your
+modifications thereto to recipients of copies in binary form.
+The name of Fraunhofer may not be used to endorse or promote products derived from this library without
+prior written permission.
+You may not charge copyright license fees for anyone to use, copy or distribute the FDK AAC Codec
+software or your modifications thereto.
+Your modified versions of the FDK AAC Codec must carry prominent notices stating that you changed the software
+and the date of any change. For modified versions of the FDK AAC Codec, the term
+"Fraunhofer FDK AAC Codec Library for Android" must be replaced by the term
+"Third-Party Modified Version of the Fraunhofer FDK AAC Codec Library for Android."
+3. NO PATENT LICENSE
+NO EXPRESS OR IMPLIED LICENSES TO ANY PATENT CLAIMS, including without limitation the patents of Fraunhofer,
+ARE GRANTED BY THIS SOFTWARE LICENSE. Fraunhofer provides no warranty of patent non-infringement with
+respect to this software.
+You may use this FDK AAC Codec software or modifications thereto only for purposes that are authorized
+by appropriate patent licenses.
+4. DISCLAIMER
+This FDK AAC Codec software is provided by Fraunhofer on behalf of the copyright holders and contributors
+"AS IS" and WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, including but not limited to the implied warranties
+of merchantability and fitness for a particular purpose. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+CONTRIBUTORS BE LIABLE for any direct, indirect, incidental, special, exemplary, or consequential damages,
+including but not limited to procurement of substitute goods or services; loss of use, data, or profits,
+or business interruption, however caused and on any theory of liability, whether in contract, strict
+liability, or tort (including negligence), arising in any way out of the use of this software, even if
+advised of the possibility of such damage.
+5. CONTACT INFORMATION
+Fraunhofer Institute for Integrated Circuits IIS
+Attention: Audio and Multimedia Departments - FDK AAC LL
+Am Wolfsmantel 33
+91058 Erlangen, Germany
+www.iis.fraunhofer.de/amm
+amm-info@iis.fraunhofer.de
+----------------------------------------------------------------------------------------------------------- */
diff --git a/third_party/libfdkaac/METADATA b/third_party/libfdkaac/METADATA
new file mode 100644
index 0000000..527f8da
--- /dev/null
+++ b/third_party/libfdkaac/METADATA
@@ -0,0 +1,14 @@
+# Format: google3/devtools/metadata/metadata.proto (go/google3metadata)
+name: "fdk-aac"
+description:
+ "A standalone library of the Fraunhofer FDK AAC code from Android."
+third_party {
+ url {
+ type: GIT
+ value: "https://github.com/mstorsjo/fdk-aac"
+ }
+ version: "2.0.2"
+ last_upgrade_date { year: 2022 month: 10 day: 17 }
+ license_note: "Software License for The Fraunhofer FDK AAC Codec Library for Android"
+ license_type: BY_EXCEPTION_ONLY
+}
diff --git a/third_party/libfdkaac/include/FDK_audio.h b/third_party/libfdkaac/include/FDK_audio.h
new file mode 100644
index 0000000..0e440c9
--- /dev/null
+++ b/third_party/libfdkaac/include/FDK_audio.h
@@ -0,0 +1,813 @@
+/* -----------------------------------------------------------------------------
+Software License for The Fraunhofer FDK AAC Codec Library for Android
+
+© Copyright 1995 - 2018 Fraunhofer-Gesellschaft zur Förderung der angewandten
+Forschung e.V. All rights reserved.
+
+ 1. INTRODUCTION
+The Fraunhofer FDK AAC Codec Library for Android ("FDK AAC Codec") is software
+that implements the MPEG Advanced Audio Coding ("AAC") encoding and decoding
+scheme for digital audio. This FDK AAC Codec software is intended to be used on
+a wide variety of Android devices.
+
+AAC's HE-AAC and HE-AAC v2 versions are regarded as today's most efficient
+general perceptual audio codecs. AAC-ELD is considered the best-performing
+full-bandwidth communications codec by independent studies and is widely
+deployed. AAC has been standardized by ISO and IEC as part of the MPEG
+specifications.
+
+Patent licenses for necessary patent claims for the FDK AAC Codec (including
+those of Fraunhofer) may be obtained through Via Licensing
+(www.vialicensing.com) or through the respective patent owners individually for
+the purpose of encoding or decoding bit streams in products that are compliant
+with the ISO/IEC MPEG audio standards. Please note that most manufacturers of
+Android devices already license these patent claims through Via Licensing or
+directly from the patent owners, and therefore FDK AAC Codec software may
+already be covered under those patent licenses when it is used for those
+licensed purposes only.
+
+Commercially-licensed AAC software libraries, including floating-point versions
+with enhanced sound quality, are also available from Fraunhofer. Users are
+encouraged to check the Fraunhofer website for additional applications
+information and documentation.
+
+2. COPYRIGHT LICENSE
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted without payment of copyright license fees provided that you
+satisfy the following conditions:
+
+You must retain the complete text of this software license in redistributions of
+the FDK AAC Codec or your modifications thereto in source code form.
+
+You must retain the complete text of this software license in the documentation
+and/or other materials provided with redistributions of the FDK AAC Codec or
+your modifications thereto in binary form. You must make available free of
+charge copies of the complete source code of the FDK AAC Codec and your
+modifications thereto to recipients of copies in binary form.
+
+The name of Fraunhofer may not be used to endorse or promote products derived
+from this library without prior written permission.
+
+You may not charge copyright license fees for anyone to use, copy or distribute
+the FDK AAC Codec software or your modifications thereto.
+
+Your modified versions of the FDK AAC Codec must carry prominent notices stating
+that you changed the software and the date of any change. For modified versions
+of the FDK AAC Codec, the term "Fraunhofer FDK AAC Codec Library for Android"
+must be replaced by the term "Third-Party Modified Version of the Fraunhofer FDK
+AAC Codec Library for Android."
+
+3. NO PATENT LICENSE
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PATENT CLAIMS, including without
+limitation the patents of Fraunhofer, ARE GRANTED BY THIS SOFTWARE LICENSE.
+Fraunhofer provides no warranty of patent non-infringement with respect to this
+software.
+
+You may use this FDK AAC Codec software or modifications thereto only for
+purposes that are authorized by appropriate patent licenses.
+
+4. DISCLAIMER
+
+This FDK AAC Codec software is provided by Fraunhofer on behalf of the copyright
+holders and contributors "AS IS" and WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES,
+including but not limited to the implied warranties of merchantability and
+fitness for a particular purpose. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+CONTRIBUTORS BE LIABLE for any direct, indirect, incidental, special, exemplary,
+or consequential damages, including but not limited to procurement of substitute
+goods or services; loss of use, data, or profits, or business interruption,
+however caused and on any theory of liability, whether in contract, strict
+liability, or tort (including negligence), arising in any way out of the use of
+this software, even if advised of the possibility of such damage.
+
+5. CONTACT INFORMATION
+
+Fraunhofer Institute for Integrated Circuits IIS
+Attention: Audio and Multimedia Departments - FDK AAC LL
+Am Wolfsmantel 33
+91058 Erlangen, Germany
+
+www.iis.fraunhofer.de/amm
+amm-info@iis.fraunhofer.de
+----------------------------------------------------------------------------- */
+
+/************************* System integration library **************************
+
+ Author(s): Manuel Jander
+
+ Description:
+
+*******************************************************************************/
+
+/** \file FDK_audio.h
+ * \brief Global audio struct and constant definitions.
+ */
+
+#ifndef FDK_AUDIO_H
+#define FDK_AUDIO_H
+
+#include "machine_type.h"
+#include "genericStds.h"
+#include "syslib_channelMapDescr.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * File format identifiers.
+ */
+typedef enum {
+ FF_UNKNOWN = -1, /**< Unknown format. */
+ FF_RAW = 0, /**< No container, bit stream data conveyed "as is". */
+
+ FF_MP4_3GPP = 3, /**< 3GPP file format. */
+ FF_MP4_MP4F = 4, /**< MPEG-4 File format. */
+
+ FF_RAWPACKETS = 5 /**< Proprietary raw packet file. */
+
+} FILE_FORMAT;
+
+/**
+ * Transport type identifiers.
+ */
+typedef enum {
+ TT_UNKNOWN = -1, /**< Unknown format. */
+ TT_MP4_RAW = 0, /**< "as is" access units (packet based since there is
+ obviously no sync layer) */
+ TT_MP4_ADIF = 1, /**< ADIF bitstream format. */
+ TT_MP4_ADTS = 2, /**< ADTS bitstream format. */
+
+ TT_MP4_LATM_MCP1 = 6, /**< Audio Mux Elements with muxConfigPresent = 1 */
+ TT_MP4_LATM_MCP0 = 7, /**< Audio Mux Elements with muxConfigPresent = 0, out
+ of band StreamMuxConfig */
+
+ TT_MP4_LOAS = 10, /**< Audio Sync Stream. */
+
+ TT_DRM = 12 /**< Digital Radio Mondial (DRM30/DRM+) bitstream format. */
+
+} TRANSPORT_TYPE;
+
+#define TT_IS_PACKET(x) \
+ (((x) == TT_MP4_RAW) || ((x) == TT_DRM) || ((x) == TT_MP4_LATM_MCP0) || \
+ ((x) == TT_MP4_LATM_MCP1))
+
+/**
+ * Audio Object Type definitions.
+ */
+typedef enum {
+ AOT_NONE = -1,
+ AOT_NULL_OBJECT = 0,
+ AOT_AAC_MAIN = 1, /**< Main profile */
+ AOT_AAC_LC = 2, /**< Low Complexity object */
+ AOT_AAC_SSR = 3,
+ AOT_AAC_LTP = 4,
+ AOT_SBR = 5,
+ AOT_AAC_SCAL = 6,
+ AOT_TWIN_VQ = 7,
+ AOT_CELP = 8,
+ AOT_HVXC = 9,
+ AOT_RSVD_10 = 10, /**< (reserved) */
+ AOT_RSVD_11 = 11, /**< (reserved) */
+ AOT_TTSI = 12, /**< TTSI Object */
+ AOT_MAIN_SYNTH = 13, /**< Main Synthetic object */
+ AOT_WAV_TAB_SYNTH = 14, /**< Wavetable Synthesis object */
+ AOT_GEN_MIDI = 15, /**< General MIDI object */
+ AOT_ALG_SYNTH_AUD_FX = 16, /**< Algorithmic Synthesis and Audio FX object */
+ AOT_ER_AAC_LC = 17, /**< Error Resilient(ER) AAC Low Complexity */
+ AOT_RSVD_18 = 18, /**< (reserved) */
+ AOT_ER_AAC_LTP = 19, /**< Error Resilient(ER) AAC LTP object */
+ AOT_ER_AAC_SCAL = 20, /**< Error Resilient(ER) AAC Scalable object */
+ AOT_ER_TWIN_VQ = 21, /**< Error Resilient(ER) TwinVQ object */
+ AOT_ER_BSAC = 22, /**< Error Resilient(ER) BSAC object */
+ AOT_ER_AAC_LD = 23, /**< Error Resilient(ER) AAC LowDelay object */
+ AOT_ER_CELP = 24, /**< Error Resilient(ER) CELP object */
+ AOT_ER_HVXC = 25, /**< Error Resilient(ER) HVXC object */
+ AOT_ER_HILN = 26, /**< Error Resilient(ER) HILN object */
+ AOT_ER_PARA = 27, /**< Error Resilient(ER) Parametric object */
+ AOT_RSVD_28 = 28, /**< might become SSC */
+ AOT_PS = 29, /**< PS, Parametric Stereo (includes SBR) */
+ AOT_MPEGS = 30, /**< MPEG Surround */
+
+ AOT_ESCAPE = 31, /**< Signal AOT uses more than 5 bits */
+
+ AOT_MP3ONMP4_L1 = 32, /**< MPEG-Layer1 in mp4 */
+ AOT_MP3ONMP4_L2 = 33, /**< MPEG-Layer2 in mp4 */
+ AOT_MP3ONMP4_L3 = 34, /**< MPEG-Layer3 in mp4 */
+ AOT_RSVD_35 = 35, /**< might become DST */
+ AOT_RSVD_36 = 36, /**< might become ALS */
+ AOT_AAC_SLS = 37, /**< AAC + SLS */
+ AOT_SLS = 38, /**< SLS */
+ AOT_ER_AAC_ELD = 39, /**< AAC Enhanced Low Delay */
+
+ AOT_USAC = 42, /**< USAC */
+ AOT_SAOC = 43, /**< SAOC */
+ AOT_LD_MPEGS = 44, /**< Low Delay MPEG Surround */
+
+ /* Pseudo AOTs */
+ AOT_MP2_AAC_LC = 129, /**< Virtual AOT MP2 Low Complexity profile */
+ AOT_MP2_SBR = 132, /**< Virtual AOT MP2 Low Complexity Profile with SBR */
+
+ AOT_DRM_AAC = 143, /**< Virtual AOT for DRM (ER-AAC-SCAL without SBR) */
+ AOT_DRM_SBR = 144, /**< Virtual AOT for DRM (ER-AAC-SCAL with SBR) */
+ AOT_DRM_MPEG_PS =
+ 145, /**< Virtual AOT for DRM (ER-AAC-SCAL with SBR and MPEG-PS) */
+ AOT_DRM_SURROUND =
+ 146, /**< Virtual AOT for DRM Surround (ER-AAC-SCAL (+SBR) +MPS) */
+ AOT_DRM_USAC = 147 /**< Virtual AOT for DRM with USAC */
+
+} AUDIO_OBJECT_TYPE;
+
+#define CAN_DO_PS(aot) \
+ ((aot) == AOT_AAC_LC || (aot) == AOT_SBR || (aot) == AOT_PS || \
+ (aot) == AOT_ER_BSAC || (aot) == AOT_DRM_AAC)
+
+#define IS_USAC(aot) ((aot) == AOT_USAC)
+
+#define IS_LOWDELAY(aot) ((aot) == AOT_ER_AAC_LD || (aot) == AOT_ER_AAC_ELD)
+
+/** Channel Mode ( 1-7 equals MPEG channel configurations, others are
+ * arbitrary). */
+typedef enum {
+ MODE_INVALID = -1,
+ MODE_UNKNOWN = 0,
+ MODE_1 = 1, /**< C */
+ MODE_2 = 2, /**< L+R */
+ MODE_1_2 = 3, /**< C, L+R */
+ MODE_1_2_1 = 4, /**< C, L+R, Rear */
+ MODE_1_2_2 = 5, /**< C, L+R, LS+RS */
+ MODE_1_2_2_1 = 6, /**< C, L+R, LS+RS, LFE */
+ MODE_1_2_2_2_1 = 7, /**< C, LC+RC, L+R, LS+RS, LFE */
+
+ MODE_6_1 = 11, /**< C, L+R, LS+RS, Crear, LFE */
+ MODE_7_1_BACK = 12, /**< C, L+R, LS+RS, Lrear+Rrear, LFE */
+ MODE_7_1_TOP_FRONT = 14, /**< C, L+R, LS+RS, LFE, Ltop+Rtop */
+
+ MODE_7_1_REAR_SURROUND = 33, /**< C, L+R, LS+RS, Lrear+Rrear, LFE */
+ MODE_7_1_FRONT_CENTER = 34, /**< C, LC+RC, L+R, LS+RS, LFE */
+
+ MODE_212 = 128 /**< 212 configuration, used in ELDv2 */
+
+} CHANNEL_MODE;
+
+/**
+ * Speaker description tags.
+ * Do not change the enumeration values unless it keeps the following
+ * segmentation:
+ * - Bit 0-3: Horizontal postion (0: none, 1: front, 2: side, 3: back, 4: lfe)
+ * - Bit 4-7: Vertical position (0: normal, 1: top, 2: bottom)
+ */
+typedef enum {
+ ACT_NONE = 0x00,
+ ACT_FRONT = 0x01, /*!< Front speaker position (at normal height) */
+ ACT_SIDE = 0x02, /*!< Side speaker position (at normal height) */
+ ACT_BACK = 0x03, /*!< Back speaker position (at normal height) */
+ ACT_LFE = 0x04, /*!< Low frequency effect speaker postion (front) */
+
+ ACT_TOP =
+ 0x10, /*!< Top speaker area (for combination with speaker positions) */
+ ACT_FRONT_TOP = 0x11, /*!< Top front speaker = (ACT_FRONT|ACT_TOP) */
+ ACT_SIDE_TOP = 0x12, /*!< Top side speaker = (ACT_SIDE |ACT_TOP) */
+ ACT_BACK_TOP = 0x13, /*!< Top back speaker = (ACT_BACK |ACT_TOP) */
+
+ ACT_BOTTOM =
+ 0x20, /*!< Bottom speaker area (for combination with speaker positions) */
+ ACT_FRONT_BOTTOM = 0x21, /*!< Bottom front speaker = (ACT_FRONT|ACT_BOTTOM) */
+ ACT_SIDE_BOTTOM = 0x22, /*!< Bottom side speaker = (ACT_SIDE |ACT_BOTTOM) */
+ ACT_BACK_BOTTOM = 0x23 /*!< Bottom back speaker = (ACT_BACK |ACT_BOTTOM) */
+
+} AUDIO_CHANNEL_TYPE;
+
+typedef enum {
+ SIG_UNKNOWN = -1,
+ SIG_IMPLICIT = 0,
+ SIG_EXPLICIT_BW_COMPATIBLE = 1,
+ SIG_EXPLICIT_HIERARCHICAL = 2
+
+} SBR_PS_SIGNALING;
+
+/**
+ * Audio Codec flags.
+ */
+#define AC_ER_VCB11 \
+ 0x000001 /*!< aacSectionDataResilienceFlag flag (from ASC): 1 means use \
+ virtual codebooks */
+#define AC_ER_RVLC \
+ 0x000002 /*!< aacSpectralDataResilienceFlag flag (from ASC): 1 means use \
+ huffman codeword reordering */
+#define AC_ER_HCR \
+ 0x000004 /*!< aacSectionDataResilienceFlag flag (from ASC): 1 means use \
+ virtual codebooks */
+#define AC_SCALABLE 0x000008 /*!< AAC Scalable*/
+#define AC_ELD 0x000010 /*!< AAC-ELD */
+#define AC_LD 0x000020 /*!< AAC-LD */
+#define AC_ER 0x000040 /*!< ER syntax */
+#define AC_BSAC 0x000080 /*!< BSAC */
+#define AC_USAC 0x000100 /*!< USAC */
+#define AC_RSV603DA 0x000200 /*!< RSVD60 3D audio */
+#define AC_HDAAC 0x000400 /*!< HD-AAC */
+#define AC_RSVD50 0x004000 /*!< Rsvd50 */
+#define AC_SBR_PRESENT 0x008000 /*!< SBR present flag (from ASC) */
+#define AC_SBRCRC \
+ 0x010000 /*!< SBR CRC present flag. Only relevant for AAC-ELD for now. */
+#define AC_PS_PRESENT 0x020000 /*!< PS present flag (from ASC or implicit) */
+#define AC_MPS_PRESENT \
+ 0x040000 /*!< MPS present flag (from ASC or implicit) \
+ */
+#define AC_DRM 0x080000 /*!< DRM bit stream syntax */
+#define AC_INDEP 0x100000 /*!< Independency flag */
+#define AC_MPEGD_RES 0x200000 /*!< MPEG-D residual individual channel data. */
+#define AC_SAOC_PRESENT 0x400000 /*!< SAOC Present Flag */
+#define AC_DAB 0x800000 /*!< DAB bit stream syntax */
+#define AC_ELD_DOWNSCALE 0x1000000 /*!< ELD Downscaled playout */
+#define AC_LD_MPS 0x2000000 /*!< Low Delay MPS. */
+#define AC_DRC_PRESENT \
+ 0x4000000 /*!< Dynamic Range Control (DRC) data found. \
+ */
+#define AC_USAC_SCFGI3 \
+ 0x8000000 /*!< USAC flag: If stereoConfigIndex is 3 the flag is set. */
+/**
+ * Audio Codec flags (reconfiguration).
+ */
+#define AC_CM_DET_CFG_CHANGE \
+ 0x000001 /*!< Config mode signalizes the callback to work in config change \
+ detection mode */
+#define AC_CM_ALLOC_MEM \
+ 0x000002 /*!< Config mode signalizes the callback to work in memory \
+ allocation mode */
+
+/**
+ * Audio Codec flags (element specific).
+ */
+#define AC_EL_USAC_TW 0x000001 /*!< USAC time warped filter bank is active */
+#define AC_EL_USAC_NOISE 0x000002 /*!< USAC noise filling is active */
+#define AC_EL_USAC_ITES 0x000004 /*!< USAC SBR inter-TES tool is active */
+#define AC_EL_USAC_PVC \
+ 0x000008 /*!< USAC SBR predictive vector coding tool is active */
+#define AC_EL_USAC_MPS212 0x000010 /*!< USAC MPS212 tool is active */
+#define AC_EL_USAC_LFE 0x000020 /*!< USAC element is LFE */
+#define AC_EL_USAC_CP_POSSIBLE \
+ 0x000040 /*!< USAC may use Complex Stereo Prediction in this channel element \
+ */
+#define AC_EL_ENHANCED_NOISE 0x000080 /*!< Enhanced noise filling*/
+#define AC_EL_IGF_AFTER_TNS 0x000100 /*!< IGF after TNS */
+#define AC_EL_IGF_INDEP_TILING 0x000200 /*!< IGF independent tiling */
+#define AC_EL_IGF_USE_ENF 0x000400 /*!< IGF use enhanced noise filling */
+#define AC_EL_FULLBANDLPD 0x000800 /*!< enable fullband LPD tools */
+#define AC_EL_LPDSTEREOIDX 0x001000 /*!< LPD-stereo-tool stereo index */
+#define AC_EL_LFE 0x002000 /*!< The element is of type LFE. */
+
+/* CODER_CONFIG::flags */
+#define CC_MPEG_ID 0x00100000
+#define CC_IS_BASELAYER 0x00200000
+#define CC_PROTECTION 0x00400000
+#define CC_SBR 0x00800000
+#define CC_SBRCRC 0x00010000
+#define CC_SAC 0x00020000
+#define CC_RVLC 0x01000000
+#define CC_VCB11 0x02000000
+#define CC_HCR 0x04000000
+#define CC_PSEUDO_SURROUND 0x08000000
+#define CC_USAC_NOISE 0x10000000
+#define CC_USAC_TW 0x20000000
+#define CC_USAC_HBE 0x40000000
+
+/** Generic audio coder configuration structure. */
+typedef struct {
+ AUDIO_OBJECT_TYPE aot; /**< Audio Object Type (AOT). */
+ AUDIO_OBJECT_TYPE extAOT; /**< Extension Audio Object Type (SBR). */
+ CHANNEL_MODE channelMode; /**< Channel mode. */
+ UCHAR channelConfigZero; /**< Use channel config zero + pce although a
+ standard channel config could be signaled. */
+ INT samplingRate; /**< Sampling rate. */
+ INT extSamplingRate; /**< Extended samplerate (SBR). */
+ INT downscaleSamplingRate; /**< Downscale sampling rate (ELD downscaled mode)
+ */
+ INT bitRate; /**< Average bitrate. */
+ int samplesPerFrame; /**< Number of PCM samples per codec frame and audio
+ channel. */
+ int noChannels; /**< Number of audio channels. */
+ int bitsFrame;
+ int nSubFrames; /**< Amount of encoder subframes. 1 means no subframing. */
+ int BSACnumOfSubFrame; /**< The number of the sub-frames which are grouped and
+ transmitted in a super-frame (BSAC). */
+ int BSAClayerLength; /**< The average length of the large-step layers in bytes
+ (BSAC). */
+ UINT flags; /**< flags */
+ UCHAR matrixMixdownA; /**< Matrix mixdown index to put into PCE. Default value
+ 0 means no mixdown coefficient, valid values are 1-4
+ which correspond to matrix_mixdown_idx 0-3. */
+ UCHAR headerPeriod; /**< Frame period for sending in band configuration
+ buffers in the transport layer. */
+
+ UCHAR stereoConfigIndex; /**< USAC MPS stereo mode */
+ UCHAR sbrMode; /**< USAC SBR mode */
+ SBR_PS_SIGNALING sbrSignaling; /**< 0: implicit signaling, 1: backwards
+ compatible explicit signaling, 2:
+ hierarcical explicit signaling */
+
+ UCHAR rawConfig[64]; /**< raw codec specific config as bit stream */
+ int rawConfigBits; /**< Size of rawConfig in bits */
+
+ UCHAR sbrPresent;
+ UCHAR psPresent;
+} CODER_CONFIG;
+
+#define USAC_ID_BIT 16 /** USAC element IDs start at USAC_ID_BIT */
+
+/** MP4 Element IDs. */
+typedef enum {
+ /* mp4 element IDs */
+ ID_NONE = -1, /**< Invalid Element helper ID. */
+ ID_SCE = 0, /**< Single Channel Element. */
+ ID_CPE = 1, /**< Channel Pair Element. */
+ ID_CCE = 2, /**< Coupling Channel Element. */
+ ID_LFE = 3, /**< LFE Channel Element. */
+ ID_DSE = 4, /**< Currently one Data Stream Element for ancillary data is
+ supported. */
+ ID_PCE = 5, /**< Program Config Element. */
+ ID_FIL = 6, /**< Fill Element. */
+ ID_END = 7, /**< Arnie (End Element = Terminator). */
+ ID_EXT = 8, /**< Extension Payload (ER only). */
+ ID_SCAL = 9, /**< AAC scalable element (ER only). */
+ /* USAC element IDs */
+ ID_USAC_SCE = 0 + USAC_ID_BIT, /**< Single Channel Element. */
+ ID_USAC_CPE = 1 + USAC_ID_BIT, /**< Channel Pair Element. */
+ ID_USAC_LFE = 2 + USAC_ID_BIT, /**< LFE Channel Element. */
+ ID_USAC_EXT = 3 + USAC_ID_BIT, /**< Extension Element. */
+ ID_USAC_END = 4 + USAC_ID_BIT, /**< Arnie (End Element = Terminator). */
+ ID_LAST
+} MP4_ELEMENT_ID;
+
+/* usacConfigExtType q.v. ISO/IEC DIS 23008-3 Table 52 and ISO/IEC FDIS
+ * 23003-3:2011(E) Table 74*/
+typedef enum {
+ /* USAC and RSVD60 3DA */
+ ID_CONFIG_EXT_FILL = 0,
+ /* RSVD60 3DA */
+ ID_CONFIG_EXT_DOWNMIX = 1,
+ ID_CONFIG_EXT_LOUDNESS_INFO = 2,
+ ID_CONFIG_EXT_AUDIOSCENE_INFO = 3,
+ ID_CONFIG_EXT_HOA_MATRIX = 4,
+ ID_CONFIG_EXT_SIG_GROUP_INFO = 6
+ /* 5-127 => reserved for ISO use */
+ /* > 128 => reserved for use outside of ISO scope */
+} CONFIG_EXT_ID;
+
+#define IS_CHANNEL_ELEMENT(elementId) \
+ ((elementId) == ID_SCE || (elementId) == ID_CPE || (elementId) == ID_LFE || \
+ (elementId) == ID_USAC_SCE || (elementId) == ID_USAC_CPE || \
+ (elementId) == ID_USAC_LFE)
+
+#define IS_MP4_CHANNEL_ELEMENT(elementId) \
+ ((elementId) == ID_SCE || (elementId) == ID_CPE || (elementId) == ID_LFE)
+
+#define EXT_ID_BITS 4 /**< Size in bits of extension payload type tags. */
+
+/** Extension payload types. */
+typedef enum {
+ EXT_FIL = 0x00,
+ EXT_FILL_DATA = 0x01,
+ EXT_DATA_ELEMENT = 0x02,
+ EXT_DATA_LENGTH = 0x03,
+ EXT_UNI_DRC = 0x04,
+ EXT_LDSAC_DATA = 0x09,
+ EXT_SAOC_DATA = 0x0a,
+ EXT_DYNAMIC_RANGE = 0x0b,
+ EXT_SAC_DATA = 0x0c,
+ EXT_SBR_DATA = 0x0d,
+ EXT_SBR_DATA_CRC = 0x0e
+} EXT_PAYLOAD_TYPE;
+
+#define IS_USAC_CHANNEL_ELEMENT(elementId) \
+ ((elementId) == ID_USAC_SCE || (elementId) == ID_USAC_CPE || \
+ (elementId) == ID_USAC_LFE)
+
+/** MPEG-D USAC & RSVD60 3D audio Extension Element Types. */
+typedef enum {
+ /* usac */
+ ID_EXT_ELE_FILL = 0x00,
+ ID_EXT_ELE_MPEGS = 0x01,
+ ID_EXT_ELE_SAOC = 0x02,
+ ID_EXT_ELE_AUDIOPREROLL = 0x03,
+ ID_EXT_ELE_UNI_DRC = 0x04,
+ /* rsv603da */
+ ID_EXT_ELE_OBJ_METADATA = 0x05,
+ ID_EXT_ELE_SAOC_3D = 0x06,
+ ID_EXT_ELE_HOA = 0x07,
+ ID_EXT_ELE_FMT_CNVRTR = 0x08,
+ ID_EXT_ELE_MCT = 0x09,
+ ID_EXT_ELE_ENHANCED_OBJ_METADATA = 0x0d,
+ /* reserved for use outside of ISO scope */
+ ID_EXT_ELE_VR_METADATA = 0x81,
+ ID_EXT_ELE_UNKNOWN = 0xFF
+} USAC_EXT_ELEMENT_TYPE;
+
+/**
+ * Proprietary raw packet file configuration data type identifier.
+ */
+typedef enum {
+ TC_NOTHING = 0, /* No configuration available -> in-band configuration. */
+ TC_RAW_ADTS = 2, /* Transfer type is ADTS. */
+ TC_RAW_LATM_MCP1 = 6, /* Transfer type is LATM with SMC present. */
+ TC_RAW_SDC = 21 /* Configuration data field is Drm SDC. */
+
+} TP_CONFIG_TYPE;
+
+/*
+ * ##############################################################################################
+ * Library identification and error handling
+ * ##############################################################################################
+ */
+/* \cond */
+
+typedef enum {
+ FDK_NONE = 0,
+ FDK_TOOLS = 1,
+ FDK_SYSLIB = 2,
+ FDK_AACDEC = 3,
+ FDK_AACENC = 4,
+ FDK_SBRDEC = 5,
+ FDK_SBRENC = 6,
+ FDK_TPDEC = 7,
+ FDK_TPENC = 8,
+ FDK_MPSDEC = 9,
+ FDK_MPEGFILEREAD = 10,
+ FDK_MPEGFILEWRITE = 11,
+ FDK_PCMDMX = 31,
+ FDK_MPSENC = 34,
+ FDK_TDLIMIT = 35,
+ FDK_UNIDRCDEC = 38,
+
+ FDK_MODULE_LAST
+
+} FDK_MODULE_ID;
+
+/* AAC capability flags */
+#define CAPF_AAC_LC 0x00000001 /**< Support flag for AAC Low Complexity. */
+#define CAPF_ER_AAC_LD \
+ 0x00000002 /**< Support flag for AAC Low Delay with Error Resilience tools. \
+ */
+#define CAPF_ER_AAC_SCAL 0x00000004 /**< Support flag for AAC Scalable. */
+#define CAPF_ER_AAC_LC \
+ 0x00000008 /**< Support flag for AAC Low Complexity with Error Resilience \
+ tools. */
+#define CAPF_AAC_480 \
+ 0x00000010 /**< Support flag for AAC with 480 framelength. */
+#define CAPF_AAC_512 \
+ 0x00000020 /**< Support flag for AAC with 512 framelength. */
+#define CAPF_AAC_960 \
+ 0x00000040 /**< Support flag for AAC with 960 framelength. */
+#define CAPF_AAC_1024 \
+ 0x00000080 /**< Support flag for AAC with 1024 framelength. */
+#define CAPF_AAC_HCR \
+ 0x00000100 /**< Support flag for AAC with Huffman Codeword Reordering. */
+#define CAPF_AAC_VCB11 \
+ 0x00000200 /**< Support flag for AAC Virtual Codebook 11. */
+#define CAPF_AAC_RVLC \
+ 0x00000400 /**< Support flag for AAC Reversible Variable Length Coding. */
+#define CAPF_AAC_MPEG4 0x00000800 /**< Support flag for MPEG file format. */
+#define CAPF_AAC_DRC \
+ 0x00001000 /**< Support flag for AAC Dynamic Range Control. */
+#define CAPF_AAC_CONCEALMENT \
+ 0x00002000 /**< Support flag for AAC concealment. */
+#define CAPF_AAC_DRM_BSFORMAT \
+ 0x00004000 /**< Support flag for AAC DRM bistream format. */
+#define CAPF_ER_AAC_ELD \
+ 0x00008000 /**< Support flag for AAC Enhanced Low Delay with Error \
+ Resilience tools. */
+#define CAPF_ER_AAC_BSAC \
+ 0x00010000 /**< Support flag for AAC BSAC. */
+#define CAPF_AAC_ELD_DOWNSCALE \
+ 0x00040000 /**< Support flag for AAC-ELD Downscaling */
+#define CAPF_AAC_USAC_LP \
+ 0x00100000 /**< Support flag for USAC low power mode. */
+#define CAPF_AAC_USAC \
+ 0x00200000 /**< Support flag for Unified Speech and Audio Coding (USAC). */
+#define CAPF_ER_AAC_ELDV2 \
+ 0x00800000 /**< Support flag for AAC Enhanced Low Delay with MPS 212. */
+#define CAPF_AAC_UNIDRC \
+ 0x01000000 /**< Support flag for MPEG-D Dynamic Range Control (uniDrc). */
+
+/* Transport capability flags */
+#define CAPF_ADTS \
+ 0x00000001 /**< Support flag for ADTS transport format. */
+#define CAPF_ADIF \
+ 0x00000002 /**< Support flag for ADIF transport format. */
+#define CAPF_LATM \
+ 0x00000004 /**< Support flag for LATM transport format. */
+#define CAPF_LOAS \
+ 0x00000008 /**< Support flag for LOAS transport format. */
+#define CAPF_RAWPACKETS \
+ 0x00000010 /**< Support flag for RAW PACKETS transport format. */
+#define CAPF_DRM \
+ 0x00000020 /**< Support flag for DRM/DRM+ transport format. */
+#define CAPF_RSVD50 \
+ 0x00000040 /**< Support flag for RSVD50 transport format */
+
+/* SBR capability flags */
+#define CAPF_SBR_LP \
+ 0x00000001 /**< Support flag for SBR Low Power mode. */
+#define CAPF_SBR_HQ \
+ 0x00000002 /**< Support flag for SBR High Quality mode. */
+#define CAPF_SBR_DRM_BS \
+ 0x00000004 /**< Support flag for */
+#define CAPF_SBR_CONCEALMENT \
+ 0x00000008 /**< Support flag for SBR concealment. */
+#define CAPF_SBR_DRC \
+ 0x00000010 /**< Support flag for SBR Dynamic Range Control. */
+#define CAPF_SBR_PS_MPEG \
+ 0x00000020 /**< Support flag for MPEG Parametric Stereo. */
+#define CAPF_SBR_PS_DRM \
+ 0x00000040 /**< Support flag for DRM Parametric Stereo. */
+#define CAPF_SBR_ELD_DOWNSCALE \
+ 0x00000080 /**< Support flag for ELD reduced delay mode */
+#define CAPF_SBR_HBEHQ \
+ 0x00000100 /**< Support flag for HQ HBE */
+
+/* PCM utils capability flags */
+#define CAPF_DMX_BLIND \
+ 0x00000001 /**< Support flag for blind downmixing. */
+#define CAPF_DMX_PCE \
+ 0x00000002 /**< Support flag for guided downmix with data from MPEG-2/4 \
+ Program Config Elements (PCE). */
+#define CAPF_DMX_ARIB \
+ 0x00000004 /**< Support flag for PCE guided downmix with slightly different \
+ equations and levels to fulfill ARIB standard. */
+#define CAPF_DMX_DVB \
+ 0x00000008 /**< Support flag for guided downmix with data from DVB ancillary \
+ data fields. */
+#define CAPF_DMX_CH_EXP \
+ 0x00000010 /**< Support flag for simple upmixing by dublicating channels or \
+ adding zero channels. */
+#define CAPF_DMX_6_CH \
+ 0x00000020 /**< Support flag for 5.1 channel configuration (input and \
+ output). */
+#define CAPF_DMX_8_CH \
+ 0x00000040 /**< Support flag for 6 and 7.1 channel configurations (input and \
+ output). */
+#define CAPF_DMX_24_CH \
+ 0x00000080 /**< Support flag for 22.2 channel configuration (input and \
+ output). */
+#define CAPF_LIMITER \
+ 0x00002000 /**< Support flag for signal level limiting. \
+ */
+
+/* MPEG Surround capability flags */
+#define CAPF_MPS_STD \
+ 0x00000001 /**< Support flag for MPEG Surround. */
+#define CAPF_MPS_LD \
+ 0x00000002 /**< Support flag for Low Delay MPEG Surround. \
+ */
+#define CAPF_MPS_USAC \
+ 0x00000004 /**< Support flag for USAC MPEG Surround. */
+#define CAPF_MPS_HQ \
+ 0x00000010 /**< Support flag indicating if high quality processing is \
+ supported */
+#define CAPF_MPS_LP \
+ 0x00000020 /**< Support flag indicating if partially complex (low power) \
+ processing is supported */
+#define CAPF_MPS_BLIND \
+ 0x00000040 /**< Support flag indicating if blind processing is supported */
+#define CAPF_MPS_BINAURAL \
+ 0x00000080 /**< Support flag indicating if binaural output is possible */
+#define CAPF_MPS_2CH_OUT \
+ 0x00000100 /**< Support flag indicating if 2ch output is possible */
+#define CAPF_MPS_6CH_OUT \
+ 0x00000200 /**< Support flag indicating if 6ch output is possible */
+#define CAPF_MPS_8CH_OUT \
+ 0x00000400 /**< Support flag indicating if 8ch output is possible */
+#define CAPF_MPS_1CH_IN \
+ 0x00001000 /**< Support flag indicating if 1ch dmx input is possible */
+#define CAPF_MPS_2CH_IN \
+ 0x00002000 /**< Support flag indicating if 2ch dmx input is possible */
+#define CAPF_MPS_6CH_IN \
+ 0x00004000 /**< Support flag indicating if 5ch dmx input is possible */
+
+/* \endcond */
+
+/*
+ * ##############################################################################################
+ * Library versioning
+ * ##############################################################################################
+ */
+
+/**
+ * Convert each member of version numbers to one single numeric version
+ * representation.
+ * \param lev0 1st level of version number.
+ * \param lev1 2nd level of version number.
+ * \param lev2 3rd level of version number.
+ */
+#define LIB_VERSION(lev0, lev1, lev2) \
+ ((lev0 << 24 & 0xff000000) | (lev1 << 16 & 0x00ff0000) | \
+ (lev2 << 8 & 0x0000ff00))
+
+/**
+ * Build text string of version.
+ */
+#define LIB_VERSION_STRING(info) \
+ FDKsprintf((info)->versionStr, "%d.%d.%d", (((info)->version >> 24) & 0xff), \
+ (((info)->version >> 16) & 0xff), \
+ (((info)->version >> 8) & 0xff))
+
+/**
+ * Library information.
+ */
+typedef struct LIB_INFO {
+ const char* title;
+ const char* build_date;
+ const char* build_time;
+ FDK_MODULE_ID module_id;
+ INT version;
+ UINT flags;
+ char versionStr[32];
+} LIB_INFO;
+
+#ifdef __cplusplus
+#define FDK_AUDIO_INLINE inline
+#else
+#define FDK_AUDIO_INLINE
+#endif
+
+/** Initialize library info. */
+static FDK_AUDIO_INLINE void FDKinitLibInfo(LIB_INFO* info) {
+ int i;
+
+ for (i = 0; i < FDK_MODULE_LAST; i++) {
+ info[i].module_id = FDK_NONE;
+ }
+}
+
+/** Aquire supported features of library. */
+static FDK_AUDIO_INLINE UINT
+FDKlibInfo_getCapabilities(const LIB_INFO* info, FDK_MODULE_ID module_id) {
+ int i;
+
+ for (i = 0; i < FDK_MODULE_LAST; i++) {
+ if (info[i].module_id == module_id) {
+ return info[i].flags;
+ }
+ }
+ return 0;
+}
+
+/** Search for next free tab. */
+static FDK_AUDIO_INLINE INT FDKlibInfo_lookup(const LIB_INFO* info,
+ FDK_MODULE_ID module_id) {
+ int i = -1;
+
+ for (i = 0; i < FDK_MODULE_LAST; i++) {
+ if (info[i].module_id == module_id) return -1;
+ if (info[i].module_id == FDK_NONE) break;
+ }
+ if (i == FDK_MODULE_LAST) return -1;
+
+ return i;
+}
+
+/*
+ * ##############################################################################################
+ * Buffer description
+ * ##############################################################################################
+ */
+
+/**
+ * I/O buffer descriptor.
+ */
+typedef struct FDK_bufDescr {
+ void** ppBase; /*!< Pointer to an array containing buffer base addresses.
+ Set to NULL for buffer requirement info. */
+ UINT* pBufSize; /*!< Pointer to an array containing the number of elements
+ that can be placed in the specific buffer. */
+ UINT* pEleSize; /*!< Pointer to an array containing the element size for each
+ buffer in bytes. That is mostly the number returned by the
+ sizeof() operator for the data type used for the specific
+ buffer. */
+ UINT*
+ pBufType; /*!< Pointer to an array of bit fields containing a description
+ for each buffer. See XXX below for more details. */
+ UINT numBufs; /*!< Total number of buffers. */
+
+} FDK_bufDescr;
+
+/**
+ * Buffer type description field.
+ */
+#define FDK_BUF_TYPE_MASK_IO ((UINT)0x03 << 30)
+#define FDK_BUF_TYPE_MASK_DESCR ((UINT)0x3F << 16)
+#define FDK_BUF_TYPE_MASK_ID ((UINT)0xFF)
+
+#define FDK_BUF_TYPE_INPUT ((UINT)0x1 << 30)
+#define FDK_BUF_TYPE_OUTPUT ((UINT)0x2 << 30)
+
+#define FDK_BUF_TYPE_PCM_DATA ((UINT)0x1 << 16)
+#define FDK_BUF_TYPE_ANC_DATA ((UINT)0x2 << 16)
+#define FDK_BUF_TYPE_BS_DATA ((UINT)0x4 << 16)
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* FDK_AUDIO_H */
diff --git a/third_party/libfdkaac/include/aacdecoder_lib.h b/third_party/libfdkaac/include/aacdecoder_lib.h
new file mode 100644
index 0000000..e64ae70
--- /dev/null
+++ b/third_party/libfdkaac/include/aacdecoder_lib.h
@@ -0,0 +1,1083 @@
+/* -----------------------------------------------------------------------------
+Software License for The Fraunhofer FDK AAC Codec Library for Android
+
+© Copyright 1995 - 2019 Fraunhofer-Gesellschaft zur Förderung der angewandten
+Forschung e.V. All rights reserved.
+
+ 1. INTRODUCTION
+The Fraunhofer FDK AAC Codec Library for Android ("FDK AAC Codec") is software
+that implements the MPEG Advanced Audio Coding ("AAC") encoding and decoding
+scheme for digital audio. This FDK AAC Codec software is intended to be used on
+a wide variety of Android devices.
+
+AAC's HE-AAC and HE-AAC v2 versions are regarded as today's most efficient
+general perceptual audio codecs. AAC-ELD is considered the best-performing
+full-bandwidth communications codec by independent studies and is widely
+deployed. AAC has been standardized by ISO and IEC as part of the MPEG
+specifications.
+
+Patent licenses for necessary patent claims for the FDK AAC Codec (including
+those of Fraunhofer) may be obtained through Via Licensing
+(www.vialicensing.com) or through the respective patent owners individually for
+the purpose of encoding or decoding bit streams in products that are compliant
+with the ISO/IEC MPEG audio standards. Please note that most manufacturers of
+Android devices already license these patent claims through Via Licensing or
+directly from the patent owners, and therefore FDK AAC Codec software may
+already be covered under those patent licenses when it is used for those
+licensed purposes only.
+
+Commercially-licensed AAC software libraries, including floating-point versions
+with enhanced sound quality, are also available from Fraunhofer. Users are
+encouraged to check the Fraunhofer website for additional applications
+information and documentation.
+
+2. COPYRIGHT LICENSE
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted without payment of copyright license fees provided that you
+satisfy the following conditions:
+
+You must retain the complete text of this software license in redistributions of
+the FDK AAC Codec or your modifications thereto in source code form.
+
+You must retain the complete text of this software license in the documentation
+and/or other materials provided with redistributions of the FDK AAC Codec or
+your modifications thereto in binary form. You must make available free of
+charge copies of the complete source code of the FDK AAC Codec and your
+modifications thereto to recipients of copies in binary form.
+
+The name of Fraunhofer may not be used to endorse or promote products derived
+from this library without prior written permission.
+
+You may not charge copyright license fees for anyone to use, copy or distribute
+the FDK AAC Codec software or your modifications thereto.
+
+Your modified versions of the FDK AAC Codec must carry prominent notices stating
+that you changed the software and the date of any change. For modified versions
+of the FDK AAC Codec, the term "Fraunhofer FDK AAC Codec Library for Android"
+must be replaced by the term "Third-Party Modified Version of the Fraunhofer FDK
+AAC Codec Library for Android."
+
+3. NO PATENT LICENSE
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PATENT CLAIMS, including without
+limitation the patents of Fraunhofer, ARE GRANTED BY THIS SOFTWARE LICENSE.
+Fraunhofer provides no warranty of patent non-infringement with respect to this
+software.
+
+You may use this FDK AAC Codec software or modifications thereto only for
+purposes that are authorized by appropriate patent licenses.
+
+4. DISCLAIMER
+
+This FDK AAC Codec software is provided by Fraunhofer on behalf of the copyright
+holders and contributors "AS IS" and WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES,
+including but not limited to the implied warranties of merchantability and
+fitness for a particular purpose. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+CONTRIBUTORS BE LIABLE for any direct, indirect, incidental, special, exemplary,
+or consequential damages, including but not limited to procurement of substitute
+goods or services; loss of use, data, or profits, or business interruption,
+however caused and on any theory of liability, whether in contract, strict
+liability, or tort (including negligence), arising in any way out of the use of
+this software, even if advised of the possibility of such damage.
+
+5. CONTACT INFORMATION
+
+Fraunhofer Institute for Integrated Circuits IIS
+Attention: Audio and Multimedia Departments - FDK AAC LL
+Am Wolfsmantel 33
+91058 Erlangen, Germany
+
+www.iis.fraunhofer.de/amm
+amm-info@iis.fraunhofer.de
+----------------------------------------------------------------------------- */
+
+/**************************** AAC decoder library ******************************
+
+ Author(s): Manuel Jander
+
+ Description:
+
+*******************************************************************************/
+
+#ifndef AACDECODER_LIB_H
+#define AACDECODER_LIB_H
+
+/**
+ * \file aacdecoder_lib.h
+ * \brief FDK AAC decoder library interface header file.
+ *
+
+\page INTRO Introduction
+
+
+\section SCOPE Scope
+
+This document describes the high-level application interface and usage of the
+ISO/MPEG-2/4 AAC Decoder library developed by the Fraunhofer Institute for
+Integrated Circuits (IIS). Depending on the library configuration, decoding of
+AAC-LC (Low-Complexity), HE-AAC (High-Efficiency AAC v1 and v2), AAC-LD
+(Low-Delay) and AAC-ELD (Enhanced Low-Delay) is implemented.
+
+All references to SBR (Spectral Band Replication) are only applicable to HE-AAC
+and AAC-ELD configurations of the FDK library. All references to PS (Parametric
+Stereo) are only applicable to HE-AAC v2 decoder configuration of the library.
+
+\section DecoderBasics Decoder Basics
+
+This document can only give a rough overview about the ISO/MPEG-2, ISO/MPEG-4
+AAC audio and MPEG-D USAC coding standards. To understand all details referenced
+in this document, you are encouraged to read the following documents.
+
+- ISO/IEC 13818-7 (MPEG-2 AAC) Standard, defines the syntax of MPEG-2 AAC audio
+bitstreams.
+- ISO/IEC 14496-3 (MPEG-4 AAC, subpart 1 and 4) Standard, defines the syntax of
+MPEG-4 AAC audio bitstreams.
+- ISO/IEC 23003-3 (MPEG-D USAC), defines MPEG-D USAC unified speech and audio
+codec.
+- Lutzky, Schuller, Gayer, Krämer, Wabnik, "A guideline to audio codec
+delay", 116th AES Convention, May 8, 2004
+
+In short, MPEG Advanced Audio Coding is based on a time-to-frequency mapping of
+the signal. The signal is partitioned into overlapping time portions and
+transformed into frequency domain. The spectral components are then quantized
+and coded using a highly efficient coding scheme.\n Encoded MPEG-2 and MPEG-4
+AAC audio bitstreams are composed of frames. Contrary to MPEG-1/2 Layer-3 (mp3),
+the length of individual frames is not restricted to a fixed number of bytes,
+but can take any length between 1 and 768 bytes.
+
+In addition to the above mentioned frequency domain coding mode, MPEG-D USAC
+also employs a time domain Algebraic Code-Excited Linear Prediction (ACELP)
+speech coder core. This operating mode is selected by the encoder in order to
+achieve the optimum audio quality for different content type. Several
+enhancements allow achieving higher quality at lower bit rates compared to
+MPEG-4 HE-AAC.
+
+
+\page LIBUSE Library Usage
+
+
+\section InterfaceDescritpion API Description
+
+All API header files are located in the folder /include of the release package.
+The contents of each file is described in detail in this document. All header
+files are provided for usage in specific C/C++ programs. The main AAC decoder
+library API functions are located in aacdecoder_lib.h header file.
+
+
+\section Calling_Sequence Calling Sequence
+
+The following sequence is necessary for proper decoding of ISO/MPEG-2/4 AAC,
+HE-AAC v2, or MPEG-D USAC bitstreams. In the following description, input stream
+read and output write function details are left out, since they may be
+implemented in a variety of configurations depending on the user's specific
+requirements.
+
+
+-# Call aacDecoder_Open() to open and retrieve a handle to a new AAC decoder
+instance. \code aacDecoderInfo = aacDecoder_Open(transportType, nrOfLayers);
+\endcode
+-# If out-of-band config data (Audio Specific Config (ASC) or Stream Mux Config
+(SMC)) is available, call aacDecoder_ConfigRaw() to pass this data to the
+decoder before beginning the decoding process. If this data is not available in
+advance, the decoder will configure itself while decoding, during the
+aacDecoder_DecodeFrame() function call.
+-# Begin decoding loop.
+\code
+do {
+\endcode
+-# Read data from bitstream file or stream buffer in to the driver program
+working memory (a client-supplied input buffer "inBuffer" in framework). This
+buffer will be used to load AAC bitstream data to the decoder. Only when all
+data in this buffer has been processed will the decoder signal an empty buffer.
+-# Call aacDecoder_Fill() to fill the decoder's internal bitstream input buffer
+with the client-supplied bitstream input buffer. Note, if the data loaded in to
+the internal buffer is not sufficient to decode a frame,
+aacDecoder_DecodeFrame() will return ::AAC_DEC_NOT_ENOUGH_BITS until a
+sufficient amount of data is loaded in to the internal buffer. For streaming
+formats (ADTS, LOAS), it is acceptable to load more than one frame to the
+decoder. However, for packed based formats, only one frame may be loaded to the
+decoder per aacDecoder_DecodeFrame() call. For least amount of communication
+delay, fill and decode should be performed on a frame by frame basis. \code
+ ErrorStatus = aacDecoder_Fill(aacDecoderInfo, inBuffer, bytesRead,
+bytesValid); \endcode
+-# Call aacDecoder_DecodeFrame(). This function decodes one frame and writes
+decoded PCM audio data to a client-supplied buffer. It is the client's
+responsibility to allocate a buffer which is large enough to hold the decoded
+output data. \code ErrorStatus = aacDecoder_DecodeFrame(aacDecoderInfo,
+TimeData, OUT_BUF_SIZE, flags); \endcode If the bitstream configuration (number
+of channels, sample rate, frame size) is not known a priori, you may call
+aacDecoder_GetStreamInfo() to retrieve a structure that contains this
+information. You may use this data to initialize an audio output device. \code
+ p_si = aacDecoder_GetStreamInfo(aacDecoderInfo);
+\endcode
+-# Repeat steps 5 to 7 until no data is available to decode any more, or in case
+of error. \code } while (bytesRead[0] > 0 || doFlush || doBsFlush ||
+forceContinue); \endcode
+-# Call aacDecoder_Close() to de-allocate all AAC decoder and transport layer
+structures. \code aacDecoder_Close(aacDecoderInfo); \endcode
+
+\image latex decode.png "Decode calling sequence" width=11cm
+
+\image latex change_source.png "Change data source sequence" width=5cm
+
+\image latex conceal.png "Error concealment sequence" width=14cm
+
+\subsection Error_Concealment_Sequence Error Concealment Sequence
+
+There are different strategies to handle bit stream errors. Depending on the
+system properties the product designer might choose to take different actions in
+case a bit error occurs. In many cases the decoder might be able to do
+reasonable error concealment without the need of any additional actions from the
+system. But in some cases its not even possible to know how many decoded PCM
+output samples are required to fill the gap due to the data error, then the
+software surrounding the decoder must deal with the situation. The most simple
+way would be to just stop audio playback and resume once enough bit stream data
+and/or buffered output samples are available. More sophisticated designs might
+also be able to deal with sender/receiver clock drifts or data drop outs by
+using a closed loop control of FIFO fulness levels. The chosen strategy depends
+on the final product requirements.
+
+The error concealment sequence diagram illustrates the general execution paths
+for error handling.
+
+The macro IS_OUTPUT_VALID(err) can be used to identify if the audio output
+buffer contains valid audio either from error free bit stream data or successful
+error concealment. In case the result is false, the decoder output buffer does
+not contain meaningful audio samples and should not be passed to any output as
+it is. Most likely in case that a continuous audio output PCM stream is
+required, the output buffer must be filled with audio data from the calling
+framework. This might be e.g. an appropriate number of samples all zero.
+
+If error code ::AAC_DEC_TRANSPORT_SYNC_ERROR is returned by the decoder, under
+some particular conditions it is possible to estimate lost frames due to the bit
+stream error. In that case the bit stream is required to have a constant
+bitrate, and compatible transport type. Audio samples for the lost frames can be
+obtained by calling aacDecoder_DecodeFrame() with flag ::AACDEC_CONCEAL set
+n-times where n is the count of lost frames. Please note that the decoder has to
+have encountered valid configuration data at least once to be able to generate
+concealed data, because at the minimum the sampling rate, frame size and amount
+of audio channels needs to be known.
+
+If it is not possible to get an estimation of lost frames then a constant
+fullness of the audio output buffer can be achieved by implementing different
+FIFO control techniques e.g. just stop taking of samples from the buffer to
+avoid underflow or stop filling new data to the buffer to avoid overflow. But
+this techniques are out of scope of this document.
+
+For a detailed description of a specific error code please refer also to
+::AAC_DECODER_ERROR.
+
+\section BufferSystem Buffer System
+
+There are three main buffers in an AAC decoder application. One external input
+buffer to hold bitstream data from file I/O or elsewhere, one decoder-internal
+input buffer, and one to hold the decoded output PCM sample data. In resource
+limited applications, the output buffer may be reused as an external input
+buffer prior to the subsequence aacDecoder_Fill() function call.
+
+To feed the data to the decoder-internal input buffer, use the
+function aacDecoder_Fill(). This function returns important information
+regarding the number of bytes in the external input buffer that have not yet
+been copied into the internal input buffer (variable bytesValid). Once the
+external buffer has been fully copied, it can be completely re-filled again. In
+case you wish to refill the buffer while there are unprocessed bytes (bytesValid
+is unequal 0), you should preserve the unconsumed data. However, we recommend to
+refill the buffer only when bytesValid returns 0.
+
+The bytesValid parameter is an input and output parameter to the FDK decoder. As
+an input, it signals how many valid bytes are available in the external buffer.
+After consumption of the external buffer using aacDecoder_Fill() function, the
+bytesValid parameter indicates if any of the bytes in the external buffer were
+not consumed.
+
+\image latex dec_buffer.png "Life cycle of the external input buffer" width=9cm
+
+\page OutputFormat Decoder audio output
+
+\section OutputFormatObtaining Obtaining channel mapping information
+
+The decoded audio output format is indicated by a set of variables of the
+CStreamInfo structure. While the struct members sampleRate, frameSize and
+numChannels might be self explanatory, pChannelType and pChannelIndices require
+some further explanation.
+
+These two arrays indicate the configuration of channel data within the output
+buffer. Both arrays have CStreamInfo::numChannels number of cells. Each cell of
+pChannelType indicates the channel type, which is described in the enum
+::AUDIO_CHANNEL_TYPE (defined in FDK_audio.h). The cells of pChannelIndices
+indicate the sub index among the channels starting with 0 among channels of the
+same audio channel type.
+
+The indexing scheme is structured as defined in MPEG-2/4 Standards. Indices
+start from the front direction (a center channel if available, will always be
+index 0) and increment, starting with the left side, pairwise (e.g. L, R) and
+from front to back (Front L, Front R, Surround L, Surround R). For detailed
+explanation, please refer to ISO/IEC 13818-7:2005(E), chapter 8.5.3.2.
+
+In case a Program Config is included in the audio configuration, the channel
+mapping described within it will be adopted.
+
+The examples below explain these aspects in detail.
+
+\section OutputFormatChange Changing the audio output format
+
+For MPEG-4 audio the channel order can be changed at runtime through the
+parameter
+::AAC_PCM_OUTPUT_CHANNEL_MAPPING. See the description of those
+parameters and the decoder library function aacDecoder_SetParam() for more
+detail.
+
+\section OutputFormatExample Channel mapping examples
+
+The following examples illustrate the location of individual audio samples in
+the audio buffer that is passed to aacDecoder_DecodeFrame() and the expected
+data in the CStreamInfo structure which can be obtained by calling
+aacDecoder_GetStreamInfo().
+
+\subsection ExamplesStereo Stereo
+
+In case of ::AAC_PCM_OUTPUT_CHANNEL_MAPPING set to 1,
+a AAC-LC bit stream which has channelConfiguration = 2 in its audio specific
+config would lead to the following values in CStreamInfo:
+
+CStreamInfo::numChannels = 2
+
+CStreamInfo::pChannelType = { ::ACT_FRONT, ::ACT_FRONT }
+
+CStreamInfo::pChannelIndices = { 0, 1 }
+
+The output buffer will be formatted as follows:
+
+\verbatim
+ <left sample 0> <left sample 1> <left sample 2> ... <left sample N>
+ <right sample 0> <right sample 1> <right sample 2> ... <right sample N>
+\endverbatim
+
+Where N equals to CStreamInfo::frameSize .
+
+\subsection ExamplesSurround Surround 5.1
+
+In case of ::AAC_PCM_OUTPUT_CHANNEL_MAPPING set to 1,
+a AAC-LC bit stream which has channelConfiguration = 6 in its audio specific
+config, would lead to the following values in CStreamInfo:
+
+CStreamInfo::numChannels = 6
+
+CStreamInfo::pChannelType = { ::ACT_FRONT, ::ACT_FRONT, ::ACT_FRONT, ::ACT_LFE,
+::ACT_BACK, ::ACT_BACK }
+
+CStreamInfo::pChannelIndices = { 1, 2, 0, 0, 0, 1 }
+
+Since ::AAC_PCM_OUTPUT_CHANNEL_MAPPING is 1, WAV file channel ordering will be
+used. For a 5.1 channel scheme, thus the channels would be: front left, front
+right, center, LFE, surround left, surround right. Thus the third channel is the
+center channel, receiving the index 0. The other front channels are front left,
+front right being placed as first and second channels with indices 1 and 2
+correspondingly. There is only one LFE, placed as the fourth channel and index
+0. Finally both surround channels get the type definition ACT_BACK, and the
+indices 0 and 1.
+
+The output buffer will be formatted as follows:
+
+\verbatim
+<front left sample 0> <front right sample 0>
+<center sample 0> <LFE sample 0>
+<surround left sample 0> <surround right sample 0>
+
+<front left sample 1> <front right sample 1>
+<center sample 1> <LFE sample 1>
+<surround left sample 1> <surround right sample 1>
+
+...
+
+<front left sample N> <front right sample N>
+<center sample N> <LFE sample N>
+<surround left sample N> <surround right sample N>
+\endverbatim
+
+Where N equals to CStreamInfo::frameSize .
+
+\subsection ExamplesArib ARIB coding mode 2/1
+
+In case of ::AAC_PCM_OUTPUT_CHANNEL_MAPPING set to 1,
+in case of a ARIB bit stream using coding mode 2/1 as described in ARIB STD-B32
+Part 2 Version 2.1-E1, page 61, would lead to the following values in
+CStreamInfo:
+
+CStreamInfo::numChannels = 3
+
+CStreamInfo::pChannelType = { ::ACT_FRONT, ::ACT_FRONT, ::ACT_BACK }
+
+CStreamInfo::pChannelIndices = { 0, 1, 0 }
+
+The audio channels will be placed as follows in the audio output buffer:
+
+\verbatim
+<front left sample 0> <front right sample 0> <mid surround sample 0>
+
+<front left sample 1> <front right sample 1> <mid surround sample 1>
+
+...
+
+<front left sample N> <front right sample N> <mid surround sample N>
+
+Where N equals to CStreamInfo::frameSize .
+
+\endverbatim
+
+*/
+
+#include "machine_type.h"
+#include "FDK_audio.h"
+
+#include "genericStds.h"
+
+#define AACDECODER_LIB_VL0 3
+#define AACDECODER_LIB_VL1 2
+#define AACDECODER_LIB_VL2 0
+
+/**
+ * \brief AAC decoder error codes.
+ */
+typedef enum {
+ AAC_DEC_OK =
+ 0x0000, /*!< No error occurred. Output buffer is valid and error free. */
+ AAC_DEC_OUT_OF_MEMORY =
+ 0x0002, /*!< Heap returned NULL pointer. Output buffer is invalid. */
+ AAC_DEC_UNKNOWN =
+ 0x0005, /*!< Error condition is of unknown reason, or from a another
+ module. Output buffer is invalid. */
+
+ /* Synchronization errors. Output buffer is invalid. */
+ aac_dec_sync_error_start = 0x1000,
+ AAC_DEC_TRANSPORT_SYNC_ERROR = 0x1001, /*!< The transport decoder had
+ synchronization problems. Do not
+ exit decoding. Just feed new
+ bitstream data. */
+ AAC_DEC_NOT_ENOUGH_BITS = 0x1002, /*!< The input buffer ran out of bits. */
+ aac_dec_sync_error_end = 0x1FFF,
+
+ /* Initialization errors. Output buffer is invalid. */
+ aac_dec_init_error_start = 0x2000,
+ AAC_DEC_INVALID_HANDLE =
+ 0x2001, /*!< The handle passed to the function call was invalid (NULL). */
+ AAC_DEC_UNSUPPORTED_AOT =
+ 0x2002, /*!< The AOT found in the configuration is not supported. */
+ AAC_DEC_UNSUPPORTED_FORMAT =
+ 0x2003, /*!< The bitstream format is not supported. */
+ AAC_DEC_UNSUPPORTED_ER_FORMAT =
+ 0x2004, /*!< The error resilience tool format is not supported. */
+ AAC_DEC_UNSUPPORTED_EPCONFIG =
+ 0x2005, /*!< The error protection format is not supported. */
+ AAC_DEC_UNSUPPORTED_MULTILAYER =
+ 0x2006, /*!< More than one layer for AAC scalable is not supported. */
+ AAC_DEC_UNSUPPORTED_CHANNELCONFIG =
+ 0x2007, /*!< The channel configuration (either number or arrangement) is
+ not supported. */
+ AAC_DEC_UNSUPPORTED_SAMPLINGRATE = 0x2008, /*!< The sample rate specified in
+ the configuration is not
+ supported. */
+ AAC_DEC_INVALID_SBR_CONFIG =
+ 0x2009, /*!< The SBR configuration is not supported. */
+ AAC_DEC_SET_PARAM_FAIL = 0x200A, /*!< The parameter could not be set. Either
+ the value was out of range or the
+ parameter does not exist. */
+ AAC_DEC_NEED_TO_RESTART = 0x200B, /*!< The decoder needs to be restarted,
+ since the required configuration change
+ cannot be performed. */
+ AAC_DEC_OUTPUT_BUFFER_TOO_SMALL =
+ 0x200C, /*!< The provided output buffer is too small. */
+ aac_dec_init_error_end = 0x2FFF,
+
+ /* Decode errors. Output buffer is valid but concealed. */
+ aac_dec_decode_error_start = 0x4000,
+ AAC_DEC_TRANSPORT_ERROR =
+ 0x4001, /*!< The transport decoder encountered an unexpected error. */
+ AAC_DEC_PARSE_ERROR = 0x4002, /*!< Error while parsing the bitstream. Most
+ probably it is corrupted, or the system
+ crashed. */
+ AAC_DEC_UNSUPPORTED_EXTENSION_PAYLOAD =
+ 0x4003, /*!< Error while parsing the extension payload of the bitstream.
+ The extension payload type found is not supported. */
+ AAC_DEC_DECODE_FRAME_ERROR = 0x4004, /*!< The parsed bitstream value is out of
+ range. Most probably the bitstream is
+ corrupt, or the system crashed. */
+ AAC_DEC_CRC_ERROR = 0x4005, /*!< The embedded CRC did not match. */
+ AAC_DEC_INVALID_CODE_BOOK = 0x4006, /*!< An invalid codebook was signaled.
+ Most probably the bitstream is corrupt,
+ or the system crashed. */
+ AAC_DEC_UNSUPPORTED_PREDICTION =
+ 0x4007, /*!< Predictor found, but not supported in the AAC Low Complexity
+ profile. Most probably the bitstream is corrupt, or has a wrong
+ format. */
+ AAC_DEC_UNSUPPORTED_CCE = 0x4008, /*!< A CCE element was found which is not
+ supported. Most probably the bitstream is
+ corrupt, or has a wrong format. */
+ AAC_DEC_UNSUPPORTED_LFE = 0x4009, /*!< A LFE element was found which is not
+ supported. Most probably the bitstream is
+ corrupt, or has a wrong format. */
+ AAC_DEC_UNSUPPORTED_GAIN_CONTROL_DATA =
+ 0x400A, /*!< Gain control data found but not supported. Most probably the
+ bitstream is corrupt, or has a wrong format. */
+ AAC_DEC_UNSUPPORTED_SBA =
+ 0x400B, /*!< SBA found, but currently not supported in the BSAC profile.
+ */
+ AAC_DEC_TNS_READ_ERROR = 0x400C, /*!< Error while reading TNS data. Most
+ probably the bitstream is corrupt or the
+ system crashed. */
+ AAC_DEC_RVLC_ERROR =
+ 0x400D, /*!< Error while decoding error resilient data. */
+ aac_dec_decode_error_end = 0x4FFF,
+ /* Ancillary data errors. Output buffer is valid. */
+ aac_dec_anc_data_error_start = 0x8000,
+ AAC_DEC_ANC_DATA_ERROR =
+ 0x8001, /*!< Non severe error concerning the ancillary data handling. */
+ AAC_DEC_TOO_SMALL_ANC_BUFFER = 0x8002, /*!< The registered ancillary data
+ buffer is too small to receive the
+ parsed data. */
+ AAC_DEC_TOO_MANY_ANC_ELEMENTS = 0x8003, /*!< More than the allowed number of
+ ancillary data elements should be
+ written to buffer. */
+ aac_dec_anc_data_error_end = 0x8FFF
+
+} AAC_DECODER_ERROR;
+
+/** Macro to identify initialization errors. Output buffer is invalid. */
+#define IS_INIT_ERROR(err) \
+ ((((err) >= aac_dec_init_error_start) && ((err) <= aac_dec_init_error_end)) \
+ ? 1 \
+ : 0)
+/** Macro to identify decode errors. Output buffer is valid but concealed. */
+#define IS_DECODE_ERROR(err) \
+ ((((err) >= aac_dec_decode_error_start) && \
+ ((err) <= aac_dec_decode_error_end)) \
+ ? 1 \
+ : 0)
+/**
+ * Macro to identify if the audio output buffer contains valid samples after
+ * calling aacDecoder_DecodeFrame(). Output buffer is valid but can be
+ * concealed.
+ */
+#define IS_OUTPUT_VALID(err) (((err) == AAC_DEC_OK) || IS_DECODE_ERROR(err))
+
+/*! \enum AAC_MD_PROFILE
+ * \brief The available metadata profiles which are mostly related to downmixing. The values define the arguments
+ * for the use with parameter ::AAC_METADATA_PROFILE.
+ */
+typedef enum {
+ AAC_MD_PROFILE_MPEG_STANDARD =
+ 0, /*!< The standard profile creates a mixdown signal based on the
+ advanced downmix metadata (from a DSE). The equations and default
+ values are defined in ISO/IEC 14496:3 Ammendment 4. Any other
+ (legacy) downmix metadata will be ignored. No other parameter will
+ be modified. */
+ AAC_MD_PROFILE_MPEG_LEGACY =
+ 1, /*!< This profile behaves identical to the standard profile if advanced
+ downmix metadata (from a DSE) is available. If not, the
+ matrix_mixdown information embedded in the program configuration
+ element (PCE) will be applied. If neither is the case, the module
+ creates a mixdown using the default coefficients as defined in
+ ISO/IEC 14496:3 AMD 4. The profile can be used to support legacy
+ digital TV (e.g. DVB) streams. */
+ AAC_MD_PROFILE_MPEG_LEGACY_PRIO =
+ 2, /*!< Similar to the ::AAC_MD_PROFILE_MPEG_LEGACY profile but if both
+ the advanced (ISO/IEC 14496:3 AMD 4) and the legacy (PCE) MPEG
+ downmix metadata are available the latter will be applied.
+ */
+ AAC_MD_PROFILE_ARIB_JAPAN =
+ 3 /*!< Downmix creation as described in ABNT NBR 15602-2. But if advanced
+ downmix metadata (ISO/IEC 14496:3 AMD 4) is available it will be
+ preferred because of the higher resolutions. In addition the
+ metadata expiry time will be set to the value defined in the ARIB
+ standard (see ::AAC_METADATA_EXPIRY_TIME).
+ */
+} AAC_MD_PROFILE;
+
+/*! \enum AAC_DRC_DEFAULT_PRESENTATION_MODE_OPTIONS
+ * \brief Options for handling of DRC parameters, if presentation mode is not indicated in bitstream
+ */
+typedef enum {
+ AAC_DRC_PARAMETER_HANDLING_DISABLED = -1, /*!< DRC parameter handling
+ disabled, all parameters are
+ applied as requested. */
+ AAC_DRC_PARAMETER_HANDLING_ENABLED =
+ 0, /*!< Apply changes to requested DRC parameters to prevent clipping. */
+ AAC_DRC_PRESENTATION_MODE_1_DEFAULT =
+ 1, /*!< Use DRC presentation mode 1 as default (e.g. for Nordig) */
+ AAC_DRC_PRESENTATION_MODE_2_DEFAULT =
+ 2 /*!< Use DRC presentation mode 2 as default (e.g. for DTG DBook) */
+} AAC_DRC_DEFAULT_PRESENTATION_MODE_OPTIONS;
+
+/**
+ * \brief AAC decoder setting parameters
+ */
+typedef enum {
+ AAC_PCM_DUAL_CHANNEL_OUTPUT_MODE =
+ 0x0002, /*!< Defines how the decoder processes two channel signals: \n
+ 0: Leave both signals as they are (default). \n
+ 1: Create a dual mono output signal from channel 1. \n
+ 2: Create a dual mono output signal from channel 2. \n
+ 3: Create a dual mono output signal by mixing both channels
+ (L' = R' = 0.5*Ch1 + 0.5*Ch2). */
+ AAC_PCM_OUTPUT_CHANNEL_MAPPING =
+ 0x0003, /*!< Output buffer channel ordering. 0: MPEG PCE style order, 1:
+ WAV file channel order (default). */
+ AAC_PCM_LIMITER_ENABLE =
+ 0x0004, /*!< Enable signal level limiting. \n
+ -1: Auto-config. Enable limiter for all
+ non-lowdelay configurations by default. \n
+ 0: Disable limiter in general. \n
+ 1: Enable limiter always.
+ It is recommended to call the decoder
+ with a AACDEC_CLRHIST flag to reset all
+ states when the limiter switch is changed
+ explicitly. */
+ AAC_PCM_LIMITER_ATTACK_TIME = 0x0005, /*!< Signal level limiting attack time
+ in ms. Default configuration is 15
+ ms. Adjustable range from 1 ms to 15
+ ms. */
+ AAC_PCM_LIMITER_RELEAS_TIME = 0x0006, /*!< Signal level limiting release time
+ in ms. Default configuration is 50
+ ms. Adjustable time must be larger
+ than 0 ms. */
+ AAC_PCM_MIN_OUTPUT_CHANNELS =
+ 0x0011, /*!< Minimum number of PCM output channels. If higher than the
+ number of encoded audio channels, a simple channel extension is
+ applied (see note 4 for exceptions). \n -1, 0: Disable channel
+ extension feature. The decoder output contains the same number
+ of channels as the encoded bitstream. \n 1: This value is
+ currently needed only together with the mix-down feature. See
+ ::AAC_PCM_MAX_OUTPUT_CHANNELS and note 2 below. \n
+ 2: Encoded mono signals will be duplicated to achieve a
+ 2/0/0.0 channel output configuration. \n 6: The decoder
+ tries to reorder encoded signals with less than six channels to
+ achieve a 3/0/2.1 channel output signal. Missing channels will
+ be filled with a zero signal. If reordering is not possible the
+ empty channels will simply be appended. Only available if
+ instance is configured to support multichannel output. \n 8:
+ The decoder tries to reorder encoded signals with less than
+ eight channels to achieve a 3/0/4.1 channel output signal.
+ Missing channels will be filled with a zero signal. If
+ reordering is not possible the empty channels will simply be
+ appended. Only available if instance is configured to
+ support multichannel output.\n NOTE: \n
+ 1. The channel signaling (CStreamInfo::pChannelType and
+ CStreamInfo::pChannelIndices) will not be modified. Added empty
+ channels will be signaled with channel type
+ AUDIO_CHANNEL_TYPE::ACT_NONE. \n
+ 2. If the parameter value is greater than that of
+ ::AAC_PCM_MAX_OUTPUT_CHANNELS both will be set to the same
+ value. \n
+ 3. This parameter will be ignored if the number of encoded
+ audio channels is greater than 8. */
+ AAC_PCM_MAX_OUTPUT_CHANNELS =
+ 0x0012, /*!< Maximum number of PCM output channels. If lower than the
+ number of encoded audio channels, downmixing is applied
+ accordingly (see note 5 for exceptions). If dedicated metadata
+ is available in the stream it will be used to achieve better
+ mixing results. \n -1, 0: Disable downmixing feature. The
+ decoder output contains the same number of channels as the
+ encoded bitstream. \n 1: All encoded audio configurations
+ with more than one channel will be mixed down to one mono
+ output signal. \n 2: The decoder performs a stereo mix-down
+ if the number encoded audio channels is greater than two. \n 6:
+ If the number of encoded audio channels is greater than six the
+ decoder performs a mix-down to meet the target output
+ configuration of 3/0/2.1 channels. Only available if instance
+ is configured to support multichannel output. \n 8: This
+ value is currently needed only together with the channel
+ extension feature. See ::AAC_PCM_MIN_OUTPUT_CHANNELS and note 2
+ below. Only available if instance is configured to support
+ multichannel output. \n NOTE: \n
+ 1. Down-mixing of any seven or eight channel configuration
+ not defined in ISO/IEC 14496-3 PDAM 4 is not supported by this
+ software version. \n
+ 2. If the parameter value is greater than zero but smaller
+ than ::AAC_PCM_MIN_OUTPUT_CHANNELS both will be set to same
+ value. \n
+ 3. This parameter will be ignored if the number of encoded
+ audio channels is greater than 8. */
+ AAC_METADATA_PROFILE =
+ 0x0020, /*!< See ::AAC_MD_PROFILE for all available values. */
+ AAC_METADATA_EXPIRY_TIME = 0x0021, /*!< Defines the time in ms after which all
+ the bitstream associated meta-data (DRC,
+ downmix coefficients, ...) will be reset
+ to default if no update has been
+ received. Negative values disable the
+ feature. */
+
+ AAC_CONCEAL_METHOD = 0x0100, /*!< Error concealment: Processing method. \n
+ 0: Spectral muting. \n
+ 1: Noise substitution (see ::CONCEAL_NOISE).
+ \n 2: Energy interpolation (adds additional
+ signal delay of one frame, see
+ ::CONCEAL_INTER. only some AOTs are
+ supported). \n */
+ AAC_DRC_BOOST_FACTOR =
+ 0x0200, /*!< MPEG-4 / MPEG-D Dynamic Range Control (DRC): Scaling factor
+ for boosting gain values. Defines how the boosting DRC factors
+ (conveyed in the bitstream) will be applied to the decoded
+ signal. The valid values range from 0 (don't apply boost
+ factors) to 127 (fully apply boost factors). Default value is 0
+ for MPEG-4 DRC and 127 for MPEG-D DRC. */
+ AAC_DRC_ATTENUATION_FACTOR = 0x0201, /*!< MPEG-4 / MPEG-D DRC: Scaling factor
+ for attenuating gain values. Same as
+ ::AAC_DRC_BOOST_FACTOR but for
+ attenuating DRC factors. */
+ AAC_DRC_REFERENCE_LEVEL =
+ 0x0202, /*!< MPEG-4 / MPEG-D DRC: Target reference level / decoder target
+ loudness.\n Defines the level below full-scale (quantized in
+ steps of 0.25dB) to which the output audio signal will be
+ normalized to by the DRC module.\n The parameter controls
+ loudness normalization for both MPEG-4 DRC and MPEG-D DRC. The
+ valid values range from 40 (-10 dBFS) to 127 (-31.75 dBFS).\n
+ Example values:\n
+ 124 (-31 dBFS) for audio/video receivers (AVR) or other
+ devices allowing audio playback with high dynamic range,\n 96
+ (-24 dBFS) for TV sets or equivalent devices (default),\n 64
+ (-16 dBFS) for mobile devices where the dynamic range of audio
+ playback is restricted.\n Any value smaller than 0 switches off
+ loudness normalization and MPEG-4 DRC. */
+ AAC_DRC_HEAVY_COMPRESSION =
+ 0x0203, /*!< MPEG-4 DRC: En-/Disable DVB specific heavy compression (aka
+ RF mode). If set to 1, the decoder will apply the compression
+ values from the DVB specific ancillary data field. At the same
+ time the MPEG-4 Dynamic Range Control tool will be disabled. By
+ default, heavy compression is disabled. */
+ AAC_DRC_DEFAULT_PRESENTATION_MODE =
+ 0x0204, /*!< MPEG-4 DRC: Default presentation mode (DRC parameter
+ handling). \n Defines the handling of the DRC parameters boost
+ factor, attenuation factor and heavy compression, if no
+ presentation mode is indicated in the bitstream.\n For options,
+ see ::AAC_DRC_DEFAULT_PRESENTATION_MODE_OPTIONS.\n Default:
+ ::AAC_DRC_PARAMETER_HANDLING_DISABLED */
+ AAC_DRC_ENC_TARGET_LEVEL =
+ 0x0205, /*!< MPEG-4 DRC: Encoder target level for light (i.e. not heavy)
+ compression.\n If known, this declares the target reference
+ level that was assumed at the encoder for calculation of
+ limiting gains. The valid values range from 0 (full-scale) to
+ 127 (31.75 dB below full-scale). This parameter is used only
+ with ::AAC_DRC_PARAMETER_HANDLING_ENABLED and ignored
+ otherwise.\n Default: 127 (worst-case assumption).\n */
+ AAC_UNIDRC_SET_EFFECT = 0x0206, /*!< MPEG-D DRC: Request a DRC effect type for
+ selection of a DRC set.\n Supported indices
+ are:\n -1: DRC off. Completely disables
+ MPEG-D DRC.\n 0: None (default). Disables
+ MPEG-D DRC, but automatically enables DRC
+ if necessary to prevent clipping.\n 1: Late
+ night\n 2: Noisy environment\n 3: Limited
+ playback range\n 4: Low playback level\n 5:
+ Dialog enhancement\n 6: General
+ compression. Used for generally enabling
+ MPEG-D DRC without particular request.\n */
+ AAC_UNIDRC_ALBUM_MODE =
+ 0x0207, /*!< MPEG-D DRC: Enable album mode. 0: Disabled (default), 1:
+ Enabled.\n Disabled album mode leads to application of gain
+ sequences for fading in and out, if provided in the
+ bitstream.\n Enabled album mode makes use of dedicated album
+ loudness information, if provided in the bitstream.\n */
+ AAC_QMF_LOWPOWER =
+ 0x0300, /*!< Quadrature Mirror Filter (QMF) Bank processing mode. \n
+ -1: Use internal default. \n
+ 0: Use complex QMF data mode. \n
+ 1: Use real (low power) QMF data mode. \n */
+ AAC_TPDEC_CLEAR_BUFFER =
+ 0x0603 /*!< Clear internal bit stream buffer of transport layers. The
+ decoder will start decoding at new data passed after this event
+ and any previous data is discarded. */
+
+} AACDEC_PARAM;
+
+/**
+ * \brief This structure gives information about the currently decoded audio
+ * data. All fields are read-only.
+ */
+typedef struct {
+ /* These five members are the only really relevant ones for the user. */
+ INT sampleRate; /*!< The sample rate in Hz of the decoded PCM audio signal. */
+ INT frameSize; /*!< The frame size of the decoded PCM audio signal. \n
+ Typically this is: \n
+ 1024 or 960 for AAC-LC \n
+ 2048 or 1920 for HE-AAC (v2) \n
+ 512 or 480 for AAC-LD and AAC-ELD \n
+ 768, 1024, 2048 or 4096 for USAC */
+ INT numChannels; /*!< The number of output audio channels before the rendering
+ module, i.e. the original channel configuration. */
+ AUDIO_CHANNEL_TYPE
+ *pChannelType; /*!< Audio channel type of each output audio channel. */
+ UCHAR *pChannelIndices; /*!< Audio channel index for each output audio
+ channel. See ISO/IEC 13818-7:2005(E), 8.5.3.2
+ Explicit channel mapping using a
+ program_config_element() */
+ /* Decoder internal members. */
+ INT aacSampleRate; /*!< Sampling rate in Hz without SBR (from configuration
+ info) divided by a (ELD) downscale factor if present. */
+ INT profile; /*!< MPEG-2 profile (from file header) (-1: not applicable (e. g.
+ MPEG-4)). */
+ AUDIO_OBJECT_TYPE
+ aot; /*!< Audio Object Type (from ASC): is set to the appropriate value
+ for MPEG-2 bitstreams (e. g. 2 for AAC-LC). */
+ INT channelConfig; /*!< Channel configuration (0: PCE defined, 1: mono, 2:
+ stereo, ... */
+ INT bitRate; /*!< Instantaneous bit rate. */
+ INT aacSamplesPerFrame; /*!< Samples per frame for the AAC core (from ASC)
+ divided by a (ELD) downscale factor if present. \n
+ Typically this is (with a downscale factor of 1):
+ \n 1024 or 960 for AAC-LC \n 512 or 480 for
+ AAC-LD and AAC-ELD */
+ INT aacNumChannels; /*!< The number of audio channels after AAC core
+ processing (before PS or MPS processing). CAUTION: This
+ are not the final number of output channels! */
+ AUDIO_OBJECT_TYPE extAot; /*!< Extension Audio Object Type (from ASC) */
+ INT extSamplingRate; /*!< Extension sampling rate in Hz (from ASC) divided by
+ a (ELD) downscale factor if present. */
+
+ UINT outputDelay; /*!< The number of samples the output is additionally
+ delayed by.the decoder. */
+ UINT flags; /*!< Copy of internal flags. Only to be written by the decoder,
+ and only to be read externally. */
+
+ SCHAR epConfig; /*!< epConfig level (from ASC): only level 0 supported, -1
+ means no ER (e. g. AOT=2, MPEG-2 AAC, etc.) */
+ /* Statistics */
+ INT numLostAccessUnits; /*!< This integer will reflect the estimated amount of
+ lost access units in case aacDecoder_DecodeFrame()
+ returns AAC_DEC_TRANSPORT_SYNC_ERROR. It will be
+ < 0 if the estimation failed. */
+
+ INT64 numTotalBytes; /*!< This is the number of total bytes that have passed
+ through the decoder. */
+ INT64
+ numBadBytes; /*!< This is the number of total bytes that were considered
+ with errors from numTotalBytes. */
+ INT64
+ numTotalAccessUnits; /*!< This is the number of total access units that
+ have passed through the decoder. */
+ INT64 numBadAccessUnits; /*!< This is the number of total access units that
+ were considered with errors from numTotalBytes. */
+
+ /* Metadata */
+ SCHAR drcProgRefLev; /*!< DRC program reference level. Defines the reference
+ level below full-scale. It is quantized in steps of
+ 0.25dB. The valid values range from 0 (0 dBFS) to 127
+ (-31.75 dBFS). It is used to reflect the average
+ loudness of the audio in LKFS according to ITU-R BS
+ 1770. If no level has been found in the bitstream the
+ value is -1. */
+ SCHAR
+ drcPresMode; /*!< DRC presentation mode. According to ETSI TS 101 154,
+ this field indicates whether light (MPEG-4 Dynamic Range
+ Control tool) or heavy compression (DVB heavy
+ compression) dynamic range control shall take priority
+ on the outputs. For details, see ETSI TS 101 154, table
+ C.33. Possible values are: \n -1: No corresponding
+ metadata found in the bitstream \n 0: DRC presentation
+ mode not indicated \n 1: DRC presentation mode 1 \n 2:
+ DRC presentation mode 2 \n 3: Reserved */
+ INT outputLoudness; /*!< Audio output loudness in steps of -0.25 dB. Range: 0
+ (0 dBFS) to 231 (-57.75 dBFS).\n A value of -1
+ indicates that no loudness metadata is present.\n If
+ loudness normalization is active, the value corresponds
+ to the target loudness value set with
+ ::AAC_DRC_REFERENCE_LEVEL.\n If loudness normalization
+ is not active, the output loudness value corresponds to
+ the loudness metadata given in the bitstream.\n
+ Loudness metadata can originate from MPEG-4 DRC or
+ MPEG-D DRC. */
+
+} CStreamInfo;
+
+typedef struct AAC_DECODER_INSTANCE
+ *HANDLE_AACDECODER; /*!< Pointer to a AAC decoder instance. */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \brief Initialize ancillary data buffer.
+ *
+ * \param self AAC decoder handle.
+ * \param buffer Pointer to (external) ancillary data buffer.
+ * \param size Size of the buffer pointed to by buffer.
+ * \return Error code.
+ */
+LINKSPEC_H AAC_DECODER_ERROR aacDecoder_AncDataInit(HANDLE_AACDECODER self,
+ UCHAR *buffer, int size);
+
+/**
+ * \brief Get one ancillary data element.
+ *
+ * \param self AAC decoder handle.
+ * \param index Index of the ancillary data element to get.
+ * \param ptr Pointer to a buffer receiving a pointer to the requested
+ * ancillary data element.
+ * \param size Pointer to a buffer receiving the length of the requested
+ * ancillary data element.
+ * \return Error code.
+ */
+LINKSPEC_H AAC_DECODER_ERROR aacDecoder_AncDataGet(HANDLE_AACDECODER self,
+ int index, UCHAR **ptr,
+ int *size);
+
+/**
+ * \brief Set one single decoder parameter.
+ *
+ * \param self AAC decoder handle.
+ * \param param Parameter to be set.
+ * \param value Parameter value.
+ * \return Error code.
+ */
+LINKSPEC_H AAC_DECODER_ERROR aacDecoder_SetParam(const HANDLE_AACDECODER self,
+ const AACDEC_PARAM param,
+ const INT value);
+
+/**
+ * \brief Get free bytes inside decoder internal buffer.
+ * \param self Handle of AAC decoder instance.
+ * \param pFreeBytes Pointer to variable receiving amount of free bytes inside
+ * decoder internal buffer.
+ * \return Error code.
+ */
+LINKSPEC_H AAC_DECODER_ERROR
+aacDecoder_GetFreeBytes(const HANDLE_AACDECODER self, UINT *pFreeBytes);
+
+/**
+ * \brief Open an AAC decoder instance.
+ * \param transportFmt The transport type to be used.
+ * \param nrOfLayers Number of transport layers.
+ * \return AAC decoder handle.
+ */
+LINKSPEC_H HANDLE_AACDECODER aacDecoder_Open(TRANSPORT_TYPE transportFmt,
+ UINT nrOfLayers);
+
+/**
+ * \brief Explicitly configure the decoder by passing a raw AudioSpecificConfig
+ * (ASC) or a StreamMuxConfig (SMC), contained in a binary buffer. This is
+ * required for MPEG-4 and Raw Packets file format bitstreams as well as for
+ * LATM bitstreams with no in-band SMC. If the transport format is LATM with or
+ * without LOAS, configuration is assumed to be an SMC, for all other file
+ * formats an ASC.
+ *
+ * \param self AAC decoder handle.
+ * \param conf Pointer to an unsigned char buffer containing the binary
+ * configuration buffer (either ASC or SMC).
+ * \param length Length of the configuration buffer in bytes.
+ * \return Error code.
+ */
+LINKSPEC_H AAC_DECODER_ERROR aacDecoder_ConfigRaw(HANDLE_AACDECODER self,
+ UCHAR *conf[],
+ const UINT length[]);
+
+/**
+ * \brief Submit raw ISO base media file format boxes to decoder for parsing
+ * (only some box types are recognized).
+ *
+ * \param self AAC decoder handle.
+ * \param buffer Pointer to an unsigned char buffer containing the binary box
+ * data (including size and type, can be a sequence of multiple boxes).
+ * \param length Length of the data in bytes.
+ * \return Error code.
+ */
+LINKSPEC_H AAC_DECODER_ERROR aacDecoder_RawISOBMFFData(HANDLE_AACDECODER self,
+ UCHAR *buffer,
+ UINT length);
+
+/**
+ * \brief Fill AAC decoder's internal input buffer with bitstream data from the
+ * external input buffer. The function only copies such data as long as the
+ * decoder-internal input buffer is not full. So it grabs whatever it can from
+ * pBuffer and returns information (bytesValid) so that at a subsequent call of
+ * %aacDecoder_Fill(), the right position in pBuffer can be determined to grab
+ * the next data.
+ *
+ * \param self AAC decoder handle.
+ * \param pBuffer Pointer to external input buffer.
+ * \param bufferSize Size of external input buffer. This argument is required
+ * because decoder-internally we need the information to calculate the offset to
+ * pBuffer, where the next available data is, which is then
+ * fed into the decoder-internal buffer (as much as
+ * possible). Our example framework implementation fills the
+ * buffer at pBuffer again, once it contains no available valid bytes anymore
+ * (meaning bytesValid equal 0).
+ * \param bytesValid Number of bitstream bytes in the external bitstream buffer
+ * that have not yet been copied into the decoder's internal bitstream buffer by
+ * calling this function. The value is updated according to
+ * the amount of newly copied bytes.
+ * \return Error code.
+ */
+LINKSPEC_H AAC_DECODER_ERROR aacDecoder_Fill(HANDLE_AACDECODER self,
+ UCHAR *pBuffer[],
+ const UINT bufferSize[],
+ UINT *bytesValid);
+
+/** Flag for aacDecoder_DecodeFrame(): Trigger the built-in error concealment
+ * module to generate a substitute signal for one lost frame. New input data
+ * will not be considered.
+ */
+#define AACDEC_CONCEAL 1
+/** Flag for aacDecoder_DecodeFrame(): Flush all filterbanks to get all delayed
+ * audio without having new input data. Thus new input data will not be
+ * considered.
+ */
+#define AACDEC_FLUSH 2
+/** Flag for aacDecoder_DecodeFrame(): Signal an input bit stream data
+ * discontinuity. Resync any internals as necessary.
+ */
+#define AACDEC_INTR 4
+/** Flag for aacDecoder_DecodeFrame(): Clear all signal delay lines and history
+ * buffers. CAUTION: This can cause discontinuities in the output signal.
+ */
+#define AACDEC_CLRHIST 8
+
+/**
+ * \brief Decode one audio frame
+ *
+ * \param self AAC decoder handle.
+ * \param pTimeData Pointer to external output buffer where the decoded PCM
+ * samples will be stored into.
+ * \param timeDataSize Size of external output buffer in PCM samples.
+ * \param flags Bit field with flags for the decoder: \n
+ * (flags & AACDEC_CONCEAL) == 1: Do concealment. \n
+ * (flags & AACDEC_FLUSH) == 2: Discard input data. Flush
+ * filter banks (output delayed audio). \n (flags & AACDEC_INTR) == 4: Input
+ * data is discontinuous. Resynchronize any internals as
+ * necessary. \n (flags & AACDEC_CLRHIST) == 8: Clear all signal delay lines and
+ * history buffers.
+ * \return Error code.
+ */
+LINKSPEC_H AAC_DECODER_ERROR aacDecoder_DecodeFrame(HANDLE_AACDECODER self,
+ INT_PCM *pTimeData,
+ const INT timeDataSize,
+ const UINT flags);
+
+/**
+ * \brief De-allocate all resources of an AAC decoder instance.
+ *
+ * \param self AAC decoder handle.
+ * \return void.
+ */
+LINKSPEC_H void aacDecoder_Close(HANDLE_AACDECODER self);
+
+/**
+ * \brief Get CStreamInfo handle from decoder.
+ *
+ * \param self AAC decoder handle.
+ * \return Reference to requested CStreamInfo.
+ */
+LINKSPEC_H CStreamInfo *aacDecoder_GetStreamInfo(HANDLE_AACDECODER self);
+
+/**
+ * \brief Get decoder library info.
+ *
+ * \param info Pointer to an allocated LIB_INFO structure.
+ * \return 0 on success.
+ */
+LINKSPEC_H INT aacDecoder_GetLibInfo(LIB_INFO *info);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* AACDECODER_LIB_H */
diff --git a/third_party/libfdkaac/include/genericStds.h b/third_party/libfdkaac/include/genericStds.h
new file mode 100644
index 0000000..8828ba7
--- /dev/null
+++ b/third_party/libfdkaac/include/genericStds.h
@@ -0,0 +1,584 @@
+/* -----------------------------------------------------------------------------
+Software License for The Fraunhofer FDK AAC Codec Library for Android
+
+© Copyright 1995 - 2018 Fraunhofer-Gesellschaft zur Förderung der angewandten
+Forschung e.V. All rights reserved.
+
+ 1. INTRODUCTION
+The Fraunhofer FDK AAC Codec Library for Android ("FDK AAC Codec") is software
+that implements the MPEG Advanced Audio Coding ("AAC") encoding and decoding
+scheme for digital audio. This FDK AAC Codec software is intended to be used on
+a wide variety of Android devices.
+
+AAC's HE-AAC and HE-AAC v2 versions are regarded as today's most efficient
+general perceptual audio codecs. AAC-ELD is considered the best-performing
+full-bandwidth communications codec by independent studies and is widely
+deployed. AAC has been standardized by ISO and IEC as part of the MPEG
+specifications.
+
+Patent licenses for necessary patent claims for the FDK AAC Codec (including
+those of Fraunhofer) may be obtained through Via Licensing
+(www.vialicensing.com) or through the respective patent owners individually for
+the purpose of encoding or decoding bit streams in products that are compliant
+with the ISO/IEC MPEG audio standards. Please note that most manufacturers of
+Android devices already license these patent claims through Via Licensing or
+directly from the patent owners, and therefore FDK AAC Codec software may
+already be covered under those patent licenses when it is used for those
+licensed purposes only.
+
+Commercially-licensed AAC software libraries, including floating-point versions
+with enhanced sound quality, are also available from Fraunhofer. Users are
+encouraged to check the Fraunhofer website for additional applications
+information and documentation.
+
+2. COPYRIGHT LICENSE
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted without payment of copyright license fees provided that you
+satisfy the following conditions:
+
+You must retain the complete text of this software license in redistributions of
+the FDK AAC Codec or your modifications thereto in source code form.
+
+You must retain the complete text of this software license in the documentation
+and/or other materials provided with redistributions of the FDK AAC Codec or
+your modifications thereto in binary form. You must make available free of
+charge copies of the complete source code of the FDK AAC Codec and your
+modifications thereto to recipients of copies in binary form.
+
+The name of Fraunhofer may not be used to endorse or promote products derived
+from this library without prior written permission.
+
+You may not charge copyright license fees for anyone to use, copy or distribute
+the FDK AAC Codec software or your modifications thereto.
+
+Your modified versions of the FDK AAC Codec must carry prominent notices stating
+that you changed the software and the date of any change. For modified versions
+of the FDK AAC Codec, the term "Fraunhofer FDK AAC Codec Library for Android"
+must be replaced by the term "Third-Party Modified Version of the Fraunhofer FDK
+AAC Codec Library for Android."
+
+3. NO PATENT LICENSE
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PATENT CLAIMS, including without
+limitation the patents of Fraunhofer, ARE GRANTED BY THIS SOFTWARE LICENSE.
+Fraunhofer provides no warranty of patent non-infringement with respect to this
+software.
+
+You may use this FDK AAC Codec software or modifications thereto only for
+purposes that are authorized by appropriate patent licenses.
+
+4. DISCLAIMER
+
+This FDK AAC Codec software is provided by Fraunhofer on behalf of the copyright
+holders and contributors "AS IS" and WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES,
+including but not limited to the implied warranties of merchantability and
+fitness for a particular purpose. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+CONTRIBUTORS BE LIABLE for any direct, indirect, incidental, special, exemplary,
+or consequential damages, including but not limited to procurement of substitute
+goods or services; loss of use, data, or profits, or business interruption,
+however caused and on any theory of liability, whether in contract, strict
+liability, or tort (including negligence), arising in any way out of the use of
+this software, even if advised of the possibility of such damage.
+
+5. CONTACT INFORMATION
+
+Fraunhofer Institute for Integrated Circuits IIS
+Attention: Audio and Multimedia Departments - FDK AAC LL
+Am Wolfsmantel 33
+91058 Erlangen, Germany
+
+www.iis.fraunhofer.de/amm
+amm-info@iis.fraunhofer.de
+----------------------------------------------------------------------------- */
+
+/************************* System integration library **************************
+
+ Author(s):
+
+ Description:
+
+*******************************************************************************/
+
+/** \file genericStds.h
+ \brief Generic Run-Time Support function wrappers and heap allocation
+ monitoring.
+ */
+
+#if !defined(GENERICSTDS_H)
+#define GENERICSTDS_H
+
+#include "machine_type.h"
+
+#ifndef M_PI
+#define M_PI 3.14159265358979323846 /*!< Pi. Only used in example projects. */
+#endif
+
+/**
+ * Identifiers for various memory locations. They are used along with memory
+ * allocation functions like FDKcalloc_L() to specify the requested memory's
+ * location.
+ */
+typedef enum {
+ /* Internal */
+ SECT_DATA_L1 = 0x2000,
+ SECT_DATA_L2,
+ SECT_DATA_L1_A,
+ SECT_DATA_L1_B,
+ SECT_CONSTDATA_L1,
+
+ /* External */
+ SECT_DATA_EXTERN = 0x4000,
+ SECT_CONSTDATA_EXTERN
+
+} MEMORY_SECTION;
+
+/*! \addtogroup SYSLIB_MEMORY_MACROS FDK memory macros
+ *
+ * The \c H_ prefix indicates that the macro is to be used in a header file, the
+ * \c C_ prefix indicates that the macro is to be used in a source file.
+ *
+ * Declaring memory areas requires to specify a unique name and a data type.
+ *
+ * For defining a memory area you require additionally one or two sizes,
+ * depending if the memory should be organized into one or two dimensions.
+ *
+ * The macros containing the keyword \c AALLOC instead of \c ALLOC additionally
+ * take care of returning aligned memory addresses (beyond the natural alignment
+ * of its type). The preprocesor macro
+ * ::ALIGNMENT_DEFAULT indicates the aligment to be used (this is hardware
+ * specific).
+ *
+ * The \c _L suffix indicates that the memory will be located in a specific
+ * section. This is useful to allocate critical memory section into fast
+ * internal SRAM for example.
+ *
+ * @{
+ */
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define H_ALLOC_MEM(name, type) \
+ type *Get##name(int n = 0); \
+ void Free##name(type **p); \
+ UINT GetRequiredMem##name(void);
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define H_ALLOC_MEM_OVERLAY(name, type) \
+ type *Get##name(int n = 0); \
+ void Free##name(type **p); \
+ UINT GetRequiredMem##name(void);
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_ALLOC_MEM(name, type, num) \
+ type *Get##name(int n) { \
+ FDK_ASSERT((n) == 0); \
+ return ((type *)FDKcalloc(num, sizeof(type))); \
+ } \
+ void Free##name(type **p) { \
+ if (p != NULL) { \
+ FDKfree(*p); \
+ *p = NULL; \
+ } \
+ } \
+ UINT GetRequiredMem##name(void) { \
+ return ALGN_SIZE_EXTRES((num) * sizeof(type)); \
+ }
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_ALLOC_MEM2(name, type, n1, n2) \
+ type *Get##name(int n) { \
+ FDK_ASSERT((n) < (n2)); \
+ return ((type *)FDKcalloc(n1, sizeof(type))); \
+ } \
+ void Free##name(type **p) { \
+ if (p != NULL) { \
+ FDKfree(*p); \
+ *p = NULL; \
+ } \
+ } \
+ UINT GetRequiredMem##name(void) { \
+ return ALGN_SIZE_EXTRES((n1) * sizeof(type)) * (n2); \
+ }
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_AALLOC_MEM(name, type, num) \
+ type *Get##name(int n) { \
+ type *ap; \
+ FDK_ASSERT((n) == 0); \
+ ap = ((type *)FDKaalloc((num) * sizeof(type), ALIGNMENT_DEFAULT)); \
+ return ap; \
+ } \
+ void Free##name(type **p) { \
+ if (p != NULL) { \
+ FDKafree(*p); \
+ *p = NULL; \
+ } \
+ } \
+ UINT GetRequiredMem##name(void) { \
+ return ALGN_SIZE_EXTRES((num) * sizeof(type) + ALIGNMENT_DEFAULT + \
+ sizeof(void *)); \
+ }
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_AALLOC_MEM2(name, type, n1, n2) \
+ type *Get##name(int n) { \
+ type *ap; \
+ FDK_ASSERT((n) < (n2)); \
+ ap = ((type *)FDKaalloc((n1) * sizeof(type), ALIGNMENT_DEFAULT)); \
+ return ap; \
+ } \
+ void Free##name(type **p) { \
+ if (p != NULL) { \
+ FDKafree(*p); \
+ *p = NULL; \
+ } \
+ } \
+ UINT GetRequiredMem##name(void) { \
+ return ALGN_SIZE_EXTRES((n1) * sizeof(type) + ALIGNMENT_DEFAULT + \
+ sizeof(void *)) * \
+ (n2); \
+ }
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_ALLOC_MEM_L(name, type, num, s) \
+ type *Get##name(int n) { \
+ FDK_ASSERT((n) == 0); \
+ return ((type *)FDKcalloc_L(num, sizeof(type), s)); \
+ } \
+ void Free##name(type **p) { \
+ if (p != NULL) { \
+ FDKfree_L(*p); \
+ *p = NULL; \
+ } \
+ } \
+ UINT GetRequiredMem##name(void) { \
+ return ALGN_SIZE_EXTRES((num) * sizeof(type)); \
+ }
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_ALLOC_MEM2_L(name, type, n1, n2, s) \
+ type *Get##name(int n) { \
+ FDK_ASSERT((n) < (n2)); \
+ return (type *)FDKcalloc_L(n1, sizeof(type), s); \
+ } \
+ void Free##name(type **p) { \
+ if (p != NULL) { \
+ FDKfree_L(*p); \
+ *p = NULL; \
+ } \
+ } \
+ UINT GetRequiredMem##name(void) { \
+ return ALGN_SIZE_EXTRES((n1) * sizeof(type)) * (n2); \
+ }
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_AALLOC_MEM_L(name, type, num, s) \
+ type *Get##name(int n) { \
+ type *ap; \
+ FDK_ASSERT((n) == 0); \
+ ap = ((type *)FDKaalloc_L((num) * sizeof(type), ALIGNMENT_DEFAULT, s)); \
+ return ap; \
+ } \
+ void Free##name(type **p) { \
+ if (p != NULL) { \
+ FDKafree_L(*p); \
+ *p = NULL; \
+ } \
+ } \
+ UINT GetRequiredMem##name(void) { \
+ return ALGN_SIZE_EXTRES((num) * sizeof(type) + ALIGNMENT_DEFAULT + \
+ sizeof(void *)); \
+ }
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_AALLOC_MEM2_L(name, type, n1, n2, s) \
+ type *Get##name(int n) { \
+ type *ap; \
+ FDK_ASSERT((n) < (n2)); \
+ ap = ((type *)FDKaalloc_L((n1) * sizeof(type), ALIGNMENT_DEFAULT, s)); \
+ return ap; \
+ } \
+ void Free##name(type **p) { \
+ if (p != NULL) { \
+ FDKafree_L(*p); \
+ *p = NULL; \
+ } \
+ } \
+ UINT GetRequiredMem##name(void) { \
+ return ALGN_SIZE_EXTRES((n1) * sizeof(type) + ALIGNMENT_DEFAULT + \
+ sizeof(void *)) * \
+ (n2); \
+ }
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_ALLOC_MEM_OVERLAY(name, type, num, sect, tag) \
+ C_AALLOC_MEM_L(name, type, num, sect)
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_AALLOC_SCRATCH_START(name, type, n) \
+ type _##name[(n) + (ALIGNMENT_DEFAULT + sizeof(type) - 1)]; \
+ type *name = (type *)ALIGN_PTR(_##name); \
+ C_ALLOC_ALIGNED_REGISTER(name, (n) * sizeof(type));
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_ALLOC_SCRATCH_START(name, type, n) type name[n];
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_AALLOC_SCRATCH_END(name, type, n) C_ALLOC_ALIGNED_UNREGISTER(name);
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_ALLOC_SCRATCH_END(name, type, n)
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_AALLOC_STACK_START(name, type, n) \
+ type _##name[(n) + (ALIGNMENT_DEFAULT + sizeof(type) - 1)]; \
+ type *name = (type *)ALIGN_PTR(_##name); \
+ C_ALLOC_ALIGNED_REGISTER(name, (n) * sizeof(type));
+
+/** See \ref SYSLIB_MEMORY_MACROS for description. */
+#define C_AALLOC_STACK_END(name, type, n) C_ALLOC_ALIGNED_UNREGISTER(name);
+
+/*! @} */
+
+#define C_ALLOC_ALIGNED_REGISTER(x, size)
+#define C_ALLOC_ALIGNED_UNREGISTER(x)
+#define C_ALLOC_ALIGNED_CHECK(x)
+#define C_ALLOC_ALIGNED_CHECK2(x, y)
+#define FDK_showBacktrace(a, b)
+
+/*! \addtogroup SYSLIB_EXITCODES Unified exit codes
+ * Exit codes to be used as return values of FDK software test and
+ * demonstration applications. Not as return values of product modules and/or
+ * libraries.
+ * @{
+ */
+#define FDK_EXITCODE_OK 0 /*!< Successful termination. No errors. */
+#define FDK_EXITCODE_USAGE \
+ 64 /*!< The command/application was used incorrectly, e.g. with the wrong \
+ number of arguments, a bad flag, a bad syntax in a parameter, or \
+ whatever. */
+#define FDK_EXITCODE_DATAERROR \
+ 65 /*!< The input data was incorrect in some way. This should only be used \
+ for user data and not system files. */
+#define FDK_EXITCODE_NOINPUT \
+ 66 /*!< An input file (not a system file) did not exist or was not readable. \
+ */
+#define FDK_EXITCODE_UNAVAILABLE \
+ 69 /*!< A service is unavailable. This can occur if a support program or \
+ file does not exist. This can also be used as a catchall message when \
+ something you wanted to do doesn't work, but you don't know why. */
+#define FDK_EXITCODE_SOFTWARE \
+ 70 /*!< An internal software error has been detected. This should be limited \
+ to non- operating system related errors as possible. */
+#define FDK_EXITCODE_CANTCREATE \
+ 73 /*!< A (user specified) output file cannot be created. */
+#define FDK_EXITCODE_IOERROR \
+ 74 /*!< An error occurred while doing I/O on some file. */
+/*! @} */
+
+/*--------------------------------------------
+ * Runtime support declarations
+ *---------------------------------------------*/
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void FDKprintf(const char *szFmt, ...);
+
+void FDKprintfErr(const char *szFmt, ...);
+
+/** Wrapper for <stdio.h>'s getchar(). */
+int FDKgetchar(void);
+
+INT FDKfprintf(void *stream, const char *format, ...);
+INT FDKsprintf(char *str, const char *format, ...);
+
+char *FDKstrchr(char *s, INT c);
+const char *FDKstrstr(const char *haystack, const char *needle);
+char *FDKstrcpy(char *dest, const char *src);
+char *FDKstrncpy(char *dest, const char *src, const UINT n);
+
+#define FDK_MAX_OVERLAYS 8 /**< Maximum number of memory overlays. */
+
+void *FDKcalloc(const UINT n, const UINT size);
+void *FDKmalloc(const UINT size);
+void FDKfree(void *ptr);
+
+/**
+ * Allocate and clear an aligned memory area. Use FDKafree() instead of
+ * FDKfree() for these memory areas.
+ *
+ * \param size Size of requested memory in bytes.
+ * \param alignment Alignment of requested memory in bytes.
+ * \return Pointer to allocated memory.
+ */
+void *FDKaalloc(const UINT size, const UINT alignment);
+
+/**
+ * Free an aligned memory area.
+ *
+ * \param ptr Pointer to be freed.
+ */
+void FDKafree(void *ptr);
+
+/**
+ * Allocate memory in a specific memory section.
+ * Requests can be made for internal or external memory. If internal memory is
+ * requested, FDKcalloc_L() first tries to use L1 memory, which sizes are
+ * defined by ::DATA_L1_A_SIZE and ::DATA_L1_B_SIZE. If no L1 memory is
+ * available, then FDKcalloc_L() tries to use L2 memory. If that fails as well,
+ * the requested memory is allocated at an extern location using the fallback
+ * FDKcalloc().
+ *
+ * \param n See MSDN documentation on calloc().
+ * \param size See MSDN documentation on calloc().
+ * \param s Memory section.
+ * \return See MSDN documentation on calloc().
+ */
+void *FDKcalloc_L(const UINT n, const UINT size, MEMORY_SECTION s);
+
+/**
+ * Allocate aligned memory in a specific memory section.
+ * See FDKcalloc_L() description for details - same applies here.
+ */
+void *FDKaalloc_L(const UINT size, const UINT alignment, MEMORY_SECTION s);
+
+/**
+ * Free memory that was allocated in a specific memory section.
+ */
+void FDKfree_L(void *ptr);
+
+/**
+ * Free aligned memory that was allocated in a specific memory section.
+ */
+void FDKafree_L(void *ptr);
+
+/**
+ * Copy memory. Source and destination memory must not overlap.
+ * Either use implementation from a Standard Library, or, if no Standard Library
+ * is available, a generic implementation.
+ * The define ::USE_BUILTIN_MEM_FUNCTIONS in genericStds.cpp controls what to
+ * use. The function arguments correspond to the standard memcpy(). Please see
+ * MSDN documentation for details on how to use it.
+ */
+void FDKmemcpy(void *dst, const void *src, const UINT size);
+
+/**
+ * Copy memory. Source and destination memory are allowed to overlap.
+ * Either use implementation from a Standard Library, or, if no Standard Library
+ * is available, a generic implementation.
+ * The define ::USE_BUILTIN_MEM_FUNCTIONS in genericStds.cpp controls what to
+ * use. The function arguments correspond to the standard memmove(). Please see
+ * MSDN documentation for details on how to use it.
+ */
+void FDKmemmove(void *dst, const void *src, const UINT size);
+
+/**
+ * Clear memory.
+ * Either use implementation from a Standard Library, or, if no Standard Library
+ * is available, a generic implementation.
+ * The define ::USE_BUILTIN_MEM_FUNCTIONS in genericStds.cpp controls what to
+ * use. The function arguments correspond to the standard memclear(). Please see
+ * MSDN documentation for details on how to use it.
+ */
+void FDKmemclear(void *memPtr, const UINT size);
+
+/**
+ * Fill memory with values.
+ * The function arguments correspond to the standard memset(). Please see MSDN
+ * documentation for details on how to use it.
+ */
+void FDKmemset(void *memPtr, const INT value, const UINT size);
+
+/* Compare function wrappers */
+INT FDKmemcmp(const void *s1, const void *s2, const UINT size);
+INT FDKstrcmp(const char *s1, const char *s2);
+INT FDKstrncmp(const char *s1, const char *s2, const UINT size);
+
+UINT FDKstrlen(const char *s);
+
+#define FDKmax(a, b) ((a) > (b) ? (a) : (b))
+#define FDKmin(a, b) ((a) < (b) ? (a) : (b))
+
+#define FDK_INT_MAX ((INT)0x7FFFFFFF)
+#define FDK_INT_MIN ((INT)0x80000000)
+
+/* FILE I/O */
+
+/*!
+ * Check platform for endianess.
+ *
+ * \return 1 if platform is little endian, non-1 if platform is big endian.
+ */
+int IS_LITTLE_ENDIAN(void);
+
+/*!
+ * Convert input value to little endian format.
+ *
+ * \param val Value to be converted. It may be in both big or little endian.
+ * \return Value in little endian format.
+ */
+UINT TO_LITTLE_ENDIAN(UINT val);
+
+/*!
+ * \fn FDKFILE *FDKfopen(const char *filename, const char *mode);
+ * Standard fopen() wrapper.
+ * \fn INT FDKfclose(FDKFILE *FP);
+ * Standard fclose() wrapper.
+ * \fn INT FDKfseek(FDKFILE *FP, LONG OFFSET, int WHENCE);
+ * Standard fseek() wrapper.
+ * \fn INT FDKftell(FDKFILE *FP);
+ * Standard ftell() wrapper.
+ * \fn INT FDKfflush(FDKFILE *fp);
+ * Standard fflush() wrapper.
+ * \fn UINT FDKfwrite(const void *ptrf, INT size, UINT nmemb, FDKFILE *fp);
+ * Standard fwrite() wrapper.
+ * \fn UINT FDKfread(void *dst, INT size, UINT nmemb, FDKFILE *fp);
+ * Standard fread() wrapper.
+ */
+typedef void FDKFILE;
+extern const INT FDKSEEK_SET, FDKSEEK_CUR, FDKSEEK_END;
+
+FDKFILE *FDKfopen(const char *filename, const char *mode);
+INT FDKfclose(FDKFILE *FP);
+INT FDKfseek(FDKFILE *FP, LONG OFFSET, int WHENCE);
+INT FDKftell(FDKFILE *FP);
+INT FDKfflush(FDKFILE *fp);
+UINT FDKfwrite(const void *ptrf, INT size, UINT nmemb, FDKFILE *fp);
+UINT FDKfread(void *dst, INT size, UINT nmemb, FDKFILE *fp);
+char *FDKfgets(void *dst, INT size, FDKFILE *fp);
+void FDKrewind(FDKFILE *fp);
+INT FDKfeof(FDKFILE *fp);
+
+/**
+ * \brief Write each member in little endian order. Convert automatically
+ * to host endianess.
+ * \param ptrf Pointer to memory where to read data from.
+ * \param size Size of each item to be written.
+ * \param nmemb Number of items to be written.
+ * \param fp File pointer of type FDKFILE.
+ * \return Number of items read on success and fread() error on failure.
+ */
+UINT FDKfwrite_EL(const void *ptrf, INT size, UINT nmemb, FDKFILE *fp);
+
+/**
+ * \brief Read variable of size "size" as little endian. Convert
+ * automatically to host endianess. 4-byte alignment is enforced for 24 bit
+ * data, at 32 bit full scale.
+ * \param dst Pointer to memory where to store data into.
+ * \param size Size of each item to be read.
+ * \param nmemb Number of items to be read.
+ * \param fp File pointer of type FDKFILE.
+ * \return Number of items read on success and fread() error on failure.
+ */
+UINT FDKfread_EL(void *dst, INT size, UINT nmemb, FDKFILE *fp);
+
+/**
+ * \brief Print FDK software disclaimer.
+ */
+void FDKprintDisclaimer(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* GENERICSTDS_H */
diff --git a/third_party/libfdkaac/include/machine_type.h b/third_party/libfdkaac/include/machine_type.h
new file mode 100644
index 0000000..bd97669
--- /dev/null
+++ b/third_party/libfdkaac/include/machine_type.h
@@ -0,0 +1,411 @@
+/* -----------------------------------------------------------------------------
+Software License for The Fraunhofer FDK AAC Codec Library for Android
+
+© Copyright 1995 - 2018 Fraunhofer-Gesellschaft zur Förderung der angewandten
+Forschung e.V. All rights reserved.
+
+ 1. INTRODUCTION
+The Fraunhofer FDK AAC Codec Library for Android ("FDK AAC Codec") is software
+that implements the MPEG Advanced Audio Coding ("AAC") encoding and decoding
+scheme for digital audio. This FDK AAC Codec software is intended to be used on
+a wide variety of Android devices.
+
+AAC's HE-AAC and HE-AAC v2 versions are regarded as today's most efficient
+general perceptual audio codecs. AAC-ELD is considered the best-performing
+full-bandwidth communications codec by independent studies and is widely
+deployed. AAC has been standardized by ISO and IEC as part of the MPEG
+specifications.
+
+Patent licenses for necessary patent claims for the FDK AAC Codec (including
+those of Fraunhofer) may be obtained through Via Licensing
+(www.vialicensing.com) or through the respective patent owners individually for
+the purpose of encoding or decoding bit streams in products that are compliant
+with the ISO/IEC MPEG audio standards. Please note that most manufacturers of
+Android devices already license these patent claims through Via Licensing or
+directly from the patent owners, and therefore FDK AAC Codec software may
+already be covered under those patent licenses when it is used for those
+licensed purposes only.
+
+Commercially-licensed AAC software libraries, including floating-point versions
+with enhanced sound quality, are also available from Fraunhofer. Users are
+encouraged to check the Fraunhofer website for additional applications
+information and documentation.
+
+2. COPYRIGHT LICENSE
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted without payment of copyright license fees provided that you
+satisfy the following conditions:
+
+You must retain the complete text of this software license in redistributions of
+the FDK AAC Codec or your modifications thereto in source code form.
+
+You must retain the complete text of this software license in the documentation
+and/or other materials provided with redistributions of the FDK AAC Codec or
+your modifications thereto in binary form. You must make available free of
+charge copies of the complete source code of the FDK AAC Codec and your
+modifications thereto to recipients of copies in binary form.
+
+The name of Fraunhofer may not be used to endorse or promote products derived
+from this library without prior written permission.
+
+You may not charge copyright license fees for anyone to use, copy or distribute
+the FDK AAC Codec software or your modifications thereto.
+
+Your modified versions of the FDK AAC Codec must carry prominent notices stating
+that you changed the software and the date of any change. For modified versions
+of the FDK AAC Codec, the term "Fraunhofer FDK AAC Codec Library for Android"
+must be replaced by the term "Third-Party Modified Version of the Fraunhofer FDK
+AAC Codec Library for Android."
+
+3. NO PATENT LICENSE
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PATENT CLAIMS, including without
+limitation the patents of Fraunhofer, ARE GRANTED BY THIS SOFTWARE LICENSE.
+Fraunhofer provides no warranty of patent non-infringement with respect to this
+software.
+
+You may use this FDK AAC Codec software or modifications thereto only for
+purposes that are authorized by appropriate patent licenses.
+
+4. DISCLAIMER
+
+This FDK AAC Codec software is provided by Fraunhofer on behalf of the copyright
+holders and contributors "AS IS" and WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES,
+including but not limited to the implied warranties of merchantability and
+fitness for a particular purpose. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+CONTRIBUTORS BE LIABLE for any direct, indirect, incidental, special, exemplary,
+or consequential damages, including but not limited to procurement of substitute
+goods or services; loss of use, data, or profits, or business interruption,
+however caused and on any theory of liability, whether in contract, strict
+liability, or tort (including negligence), arising in any way out of the use of
+this software, even if advised of the possibility of such damage.
+
+5. CONTACT INFORMATION
+
+Fraunhofer Institute for Integrated Circuits IIS
+Attention: Audio and Multimedia Departments - FDK AAC LL
+Am Wolfsmantel 33
+91058 Erlangen, Germany
+
+www.iis.fraunhofer.de/amm
+amm-info@iis.fraunhofer.de
+----------------------------------------------------------------------------- */
+
+/************************* System integration library **************************
+
+ Author(s):
+
+ Description:
+
+*******************************************************************************/
+
+/** \file machine_type.h
+ * \brief Type defines for various processors and compiler tools.
+ */
+
+#if !defined(MACHINE_TYPE_H)
+#define MACHINE_TYPE_H
+
+#include <stddef.h> /* Needed to define size_t */
+
+#if defined(__ANDROID__) && (__GNUC__ == 4) && (__GNUC_MINOR__ == 4) && \
+ (__GNUC_GNU_INLINE__ == 1)
+typedef unsigned long long uint64_t;
+#include <sys/types.h>
+#endif
+
+/* Library calling convention spec. __cdecl and friends might be added here as
+ * required. */
+#define LINKSPEC_H
+#define LINKSPEC_CPP
+
+/* for doxygen the following docu parts must be separated */
+/** \var SCHAR
+ * Data type representing at least 1 byte signed integer on all supported
+ * platforms.
+ */
+/** \var UCHAR
+ * Data type representing at least 1 byte unsigned integer on all
+ * supported platforms.
+ */
+/** \var INT
+ * Data type representing at least 4 byte signed integer on all supported
+ * platforms.
+ */
+/** \var UINT
+ * Data type representing at least 4 byte unsigned integer on all
+ * supported platforms.
+ */
+/** \var LONG
+ * Data type representing 4 byte signed integer on all supported
+ * platforms.
+ */
+/** \var ULONG
+ * Data type representing 4 byte unsigned integer on all supported
+ * platforms.
+ */
+/** \var SHORT
+ * Data type representing 2 byte signed integer on all supported
+ * platforms.
+ */
+/** \var USHORT
+ * Data type representing 2 byte unsigned integer on all supported
+ * platforms.
+ */
+/** \var INT64
+ * Data type representing 8 byte signed integer on all supported
+ * platforms.
+ */
+/** \var UINT64
+ * Data type representing 8 byte unsigned integer on all supported
+ * platforms.
+ */
+/** \def SHORT_BITS
+ * Number of bits the data type short represents. sizeof() is not suited
+ * to get this info, because a byte is not always defined as 8 bits.
+ */
+/** \def CHAR_BITS
+ * Number of bits the data type char represents. sizeof() is not suited
+ * to get this info, because a byte is not always defined as 8 bits.
+ */
+/** \var INT_PCM
+ * Data type representing the width of input and output PCM samples.
+ */
+
+typedef signed int INT;
+typedef unsigned int UINT;
+#ifdef __LP64__
+/* force FDK long-datatypes to 4 byte */
+/* Use defines to avoid type alias problems on 64 bit machines. */
+#define LONG INT
+#define ULONG UINT
+#else /* __LP64__ */
+typedef signed long LONG;
+typedef unsigned long ULONG;
+#endif /* __LP64__ */
+typedef signed short SHORT;
+typedef unsigned short USHORT;
+typedef signed char SCHAR;
+typedef unsigned char UCHAR;
+
+#define SHORT_BITS 16
+#define CHAR_BITS 8
+
+/* Define 64 bit base integer type. */
+#ifdef _MSC_VER
+typedef __int64 INT64;
+typedef unsigned __int64 UINT64;
+#else
+typedef long long INT64;
+typedef unsigned long long UINT64;
+#endif
+
+#ifndef NULL
+#ifdef __cplusplus
+#define NULL 0
+#else
+#define NULL ((void *)0)
+#endif
+#endif
+
+#if ((defined(__i686__) || defined(__i586__) || defined(__i386__) || \
+ defined(__x86_64__)) || \
+ (defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_X64)))) && \
+ !defined(FDK_ASSERT_ENABLE)
+#define FDK_ASSERT_ENABLE
+#endif
+
+#if defined(FDK_ASSERT_ENABLE)
+#include <assert.h>
+#define FDK_ASSERT(x) assert(x)
+#else
+#define FDK_ASSERT(ignore)
+#endif
+
+typedef SHORT INT_PCM;
+#define MAXVAL_PCM MAXVAL_SGL
+#define MINVAL_PCM MINVAL_SGL
+#define WAV_BITS 16
+#define SAMPLE_BITS 16
+#define SAMPLE_MAX ((INT_PCM)(((ULONG)1 << (SAMPLE_BITS - 1)) - 1))
+#define SAMPLE_MIN (~SAMPLE_MAX)
+
+/*!
+* \def RAM_ALIGN
+* Used to align memory as prefix before memory declaration. For example:
+ \code
+ RAM_ALIGN
+ int myArray[16];
+ \endcode
+
+ Note, that not all platforms support this mechanism. For example with TI
+compilers a preprocessor pragma is used, but to do something like
+
+ \code
+ #define RAM_ALIGN #pragma DATA_ALIGN(x)
+ \endcode
+
+ would require the preprocessor to process this line twice to fully resolve
+it. Hence, a fully platform-independant way to use alignment is not supported.
+
+* \def ALIGNMENT_DEFAULT
+* Default alignment in bytes.
+*/
+
+#define ALIGNMENT_DEFAULT 8
+
+/* RAM_ALIGN keyword causes memory alignment of global variables. */
+#if defined(_MSC_VER)
+#define RAM_ALIGN __declspec(align(ALIGNMENT_DEFAULT))
+#elif defined(__GNUC__)
+#define RAM_ALIGN __attribute__((aligned(ALIGNMENT_DEFAULT)))
+#else
+#define RAM_ALIGN
+#endif
+
+/*!
+ * \def RESTRICT
+ * The restrict keyword is supported by some platforms and RESTRICT maps
+ * to either the corresponding keyword on each platform or to void if the
+ * compiler does not provide such feature. It tells the compiler that a
+ * pointer points to memory that does not overlap with other memories pointed to
+ * by other pointers. If this keyword is used and the assumption of no
+ * overlap is not true the resulting code might crash.
+ *
+ * \def WORD_ALIGNED(x)
+ * Tells the compiler that pointer x is 16 bit aligned. It does not cause
+ * the address itself to be aligned, but serves as a hint to the optimizer. The
+ * alignment of the pointer must be guarranteed, if not the code might
+ * crash.
+ *
+ * \def DWORD_ALIGNED(x)
+ * Tells the compiler that pointer x is 32 bit aligned. It does not cause
+ * the address itself to be aligned, but serves as a hint to the optimizer. The
+ * alignment of the pointer must be guarranteed, if not the code might
+ * crash.
+ *
+ */
+#define RESTRICT
+#define WORD_ALIGNED(x) C_ALLOC_ALIGNED_CHECK2((const void *)(x), 2);
+#define DWORD_ALIGNED(x) C_ALLOC_ALIGNED_CHECK2((const void *)(x), 4);
+
+/*-----------------------------------------------------------------------------------
+ * ALIGN_SIZE
+ *-----------------------------------------------------------------------------------*/
+/*!
+ * \brief This macro aligns a given value depending on ::ALIGNMENT_DEFAULT.
+ *
+ * For example if #ALIGNMENT_DEFAULT equals 8, then:
+ * - ALIGN_SIZE(3) returns 8
+ * - ALIGN_SIZE(8) returns 8
+ * - ALIGN_SIZE(9) returns 16
+ */
+#define ALIGN_SIZE(a) \
+ ((a) + (((INT)ALIGNMENT_DEFAULT - ((size_t)(a) & (ALIGNMENT_DEFAULT - 1))) & \
+ (ALIGNMENT_DEFAULT - 1)))
+
+/*!
+ * \brief This macro aligns a given address depending on ::ALIGNMENT_DEFAULT.
+ */
+#define ALIGN_PTR(a) \
+ ((void *)((unsigned char *)(a) + \
+ ((((INT)ALIGNMENT_DEFAULT - \
+ ((size_t)(a) & (ALIGNMENT_DEFAULT - 1))) & \
+ (ALIGNMENT_DEFAULT - 1)))))
+
+/* Alignment macro for libSYS heap implementation */
+#define ALIGNMENT_EXTRES (ALIGNMENT_DEFAULT)
+#define ALGN_SIZE_EXTRES(a) \
+ ((a) + (((INT)ALIGNMENT_EXTRES - ((INT)(a) & (ALIGNMENT_EXTRES - 1))) & \
+ (ALIGNMENT_EXTRES - 1)))
+
+/*!
+ * \def FDK_FORCEINLINE
+ * Sometimes compiler do not do what they are told to do, and in case of
+ * inlining some additional command might be necessary depending on the
+ * platform.
+ *
+ * \def FDK_INLINE
+ * Defines how the compiler is told to inline stuff.
+ */
+#ifndef FDK_FORCEINLINE
+#if defined(__GNUC__) && !defined(__SDE_MIPS__)
+#define FDK_FORCEINLINE inline __attribute((always_inline))
+#else
+#define FDK_FORCEINLINE inline
+#endif
+#endif
+
+#define FDK_INLINE static inline
+
+/*!
+ * \def LNK_SECTION_DATA_L1
+ * The LNK_SECTION_* defines allow memory to be drawn from specific memory
+ * sections. Used as prefix before variable declaration.
+ *
+ * \def LNK_SECTION_DATA_L2
+ * See ::LNK_SECTION_DATA_L1
+ * \def LNK_SECTION_L1_DATA_A
+ * See ::LNK_SECTION_DATA_L1
+ * \def LNK_SECTION_L1_DATA_B
+ * See ::LNK_SECTION_DATA_L1
+ * \def LNK_SECTION_CONSTDATA_L1
+ * See ::LNK_SECTION_DATA_L1
+ * \def LNK_SECTION_CONSTDATA
+ * See ::LNK_SECTION_DATA_L1
+ * \def LNK_SECTION_CODE_L1
+ * See ::LNK_SECTION_DATA_L1
+ * \def LNK_SECTION_CODE_L2
+ * See ::LNK_SECTION_DATA_L1
+ * \def LNK_SECTION_INITCODE
+ * See ::LNK_SECTION_DATA_L1
+ */
+/**************************************************
+ * Code Section macros
+ **************************************************/
+#define LNK_SECTION_CODE_L1
+#define LNK_SECTION_CODE_L2
+#define LNK_SECTION_INITCODE
+
+/* Memory section macros. */
+
+/* default fall back */
+#define LNK_SECTION_DATA_L1
+#define LNK_SECTION_DATA_L2
+#define LNK_SECTION_CONSTDATA
+#define LNK_SECTION_CONSTDATA_L1
+
+#define LNK_SECTION_L1_DATA_A
+#define LNK_SECTION_L1_DATA_B
+
+/**************************************************
+ * Macros regarding static code analysis
+ **************************************************/
+#ifdef __cplusplus
+#if !defined(__has_cpp_attribute)
+#define __has_cpp_attribute(x) 0
+#endif
+#if defined(__clang__) && __has_cpp_attribute(clang::fallthrough)
+#define FDK_FALLTHROUGH [[clang::fallthrough]]
+#endif
+#endif
+
+#ifndef FDK_FALLTHROUGH
+#if defined(__GNUC__) && (__GNUC__ >= 7)
+#define FDK_FALLTHROUGH __attribute__((fallthrough))
+#else
+#define FDK_FALLTHROUGH
+#endif
+#endif
+
+#ifdef _MSC_VER
+/*
+ * Sometimes certain features are excluded from compilation and therefore the
+ * warning 4065 may occur: "switch statement contains 'default' but no 'case'
+ * labels" We consider this warning irrelevant and disable it.
+ */
+#pragma warning(disable : 4065)
+#endif
+
+#endif /* MACHINE_TYPE_H */
diff --git a/third_party/libfdkaac/include/syslib_channelMapDescr.h b/third_party/libfdkaac/include/syslib_channelMapDescr.h
new file mode 100644
index 0000000..375a24d
--- /dev/null
+++ b/third_party/libfdkaac/include/syslib_channelMapDescr.h
@@ -0,0 +1,202 @@
+/* -----------------------------------------------------------------------------
+Software License for The Fraunhofer FDK AAC Codec Library for Android
+
+© Copyright 1995 - 2018 Fraunhofer-Gesellschaft zur Förderung der angewandten
+Forschung e.V. All rights reserved.
+
+ 1. INTRODUCTION
+The Fraunhofer FDK AAC Codec Library for Android ("FDK AAC Codec") is software
+that implements the MPEG Advanced Audio Coding ("AAC") encoding and decoding
+scheme for digital audio. This FDK AAC Codec software is intended to be used on
+a wide variety of Android devices.
+
+AAC's HE-AAC and HE-AAC v2 versions are regarded as today's most efficient
+general perceptual audio codecs. AAC-ELD is considered the best-performing
+full-bandwidth communications codec by independent studies and is widely
+deployed. AAC has been standardized by ISO and IEC as part of the MPEG
+specifications.
+
+Patent licenses for necessary patent claims for the FDK AAC Codec (including
+those of Fraunhofer) may be obtained through Via Licensing
+(www.vialicensing.com) or through the respective patent owners individually for
+the purpose of encoding or decoding bit streams in products that are compliant
+with the ISO/IEC MPEG audio standards. Please note that most manufacturers of
+Android devices already license these patent claims through Via Licensing or
+directly from the patent owners, and therefore FDK AAC Codec software may
+already be covered under those patent licenses when it is used for those
+licensed purposes only.
+
+Commercially-licensed AAC software libraries, including floating-point versions
+with enhanced sound quality, are also available from Fraunhofer. Users are
+encouraged to check the Fraunhofer website for additional applications
+information and documentation.
+
+2. COPYRIGHT LICENSE
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted without payment of copyright license fees provided that you
+satisfy the following conditions:
+
+You must retain the complete text of this software license in redistributions of
+the FDK AAC Codec or your modifications thereto in source code form.
+
+You must retain the complete text of this software license in the documentation
+and/or other materials provided with redistributions of the FDK AAC Codec or
+your modifications thereto in binary form. You must make available free of
+charge copies of the complete source code of the FDK AAC Codec and your
+modifications thereto to recipients of copies in binary form.
+
+The name of Fraunhofer may not be used to endorse or promote products derived
+from this library without prior written permission.
+
+You may not charge copyright license fees for anyone to use, copy or distribute
+the FDK AAC Codec software or your modifications thereto.
+
+Your modified versions of the FDK AAC Codec must carry prominent notices stating
+that you changed the software and the date of any change. For modified versions
+of the FDK AAC Codec, the term "Fraunhofer FDK AAC Codec Library for Android"
+must be replaced by the term "Third-Party Modified Version of the Fraunhofer FDK
+AAC Codec Library for Android."
+
+3. NO PATENT LICENSE
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PATENT CLAIMS, including without
+limitation the patents of Fraunhofer, ARE GRANTED BY THIS SOFTWARE LICENSE.
+Fraunhofer provides no warranty of patent non-infringement with respect to this
+software.
+
+You may use this FDK AAC Codec software or modifications thereto only for
+purposes that are authorized by appropriate patent licenses.
+
+4. DISCLAIMER
+
+This FDK AAC Codec software is provided by Fraunhofer on behalf of the copyright
+holders and contributors "AS IS" and WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES,
+including but not limited to the implied warranties of merchantability and
+fitness for a particular purpose. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+CONTRIBUTORS BE LIABLE for any direct, indirect, incidental, special, exemplary,
+or consequential damages, including but not limited to procurement of substitute
+goods or services; loss of use, data, or profits, or business interruption,
+however caused and on any theory of liability, whether in contract, strict
+liability, or tort (including negligence), arising in any way out of the use of
+this software, even if advised of the possibility of such damage.
+
+5. CONTACT INFORMATION
+
+Fraunhofer Institute for Integrated Circuits IIS
+Attention: Audio and Multimedia Departments - FDK AAC LL
+Am Wolfsmantel 33
+91058 Erlangen, Germany
+
+www.iis.fraunhofer.de/amm
+amm-info@iis.fraunhofer.de
+----------------------------------------------------------------------------- */
+
+/************************* System integration library **************************
+
+ Author(s): Thomas Dietzen
+
+ Description:
+
+*******************************************************************************/
+
+/** \file syslib_channelMapDescr.h
+ * \brief Function and structure declarations for the channel map descriptor implementation.
+ */
+
+#ifndef SYSLIB_CHANNELMAPDESCR_H
+#define SYSLIB_CHANNELMAPDESCR_H
+
+#include "machine_type.h"
+
+/**
+ * \brief Contains information needed for a single channel map.
+ */
+typedef struct {
+ const UCHAR*
+ pChannelMap; /*!< Actual channel mapping for one single configuration. */
+ UCHAR numChannels; /*!< The number of channels for the channel map which is
+ the maximum used channel index+1. */
+} CHANNEL_MAP_INFO;
+
+/**
+ * \brief This is the main data struct. It contains the mapping for all
+ * channel configurations such as administration information.
+ *
+ * CAUTION: Do not access this structure directly from a algorithm specific
+ * library. Always use one of the API access functions below!
+ */
+typedef struct {
+ const CHANNEL_MAP_INFO* pMapInfoTab; /*!< Table of channel maps. */
+ UINT mapInfoTabLen; /*!< Length of the channel map table array. */
+ UINT fPassThrough; /*!< Flag that defines whether the specified mapping shall
+ be applied (value: 0) or the input just gets passed
+ through (MPEG mapping). */
+} FDK_channelMapDescr;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \brief Initialize a given channel map descriptor.
+ *
+ * \param pMapDescr Pointer to a channel map descriptor to be initialized.
+ * \param pMapInfoTab Table of channel maps to initizalize the descriptor
+ with.
+ * If a NULL pointer is given a default table for
+ WAV-like mapping will be used.
+ * \param mapInfoTabLen Length of the channel map table array (pMapInfoTab).
+ If a zero length is given a default table for WAV-like mapping will be used.
+ * \param fPassThrough If the flag is set the reordering (given by
+ pMapInfoTab) will be bypassed.
+ */
+void FDK_chMapDescr_init(FDK_channelMapDescr* const pMapDescr,
+ const CHANNEL_MAP_INFO* const pMapInfoTab,
+ const UINT mapInfoTabLen, const UINT fPassThrough);
+
+/**
+ * \brief Change the channel reordering state of a given channel map
+ * descriptor.
+ *
+ * \param pMapDescr Pointer to a (initialized) channel map descriptor.
+ * \param fPassThrough If the flag is set the reordering (given by
+ * pMapInfoTab) will be bypassed.
+ * \return Value unequal to zero if set operation was not
+ * successful. And zero on success.
+ */
+int FDK_chMapDescr_setPassThrough(FDK_channelMapDescr* const pMapDescr,
+ UINT fPassThrough);
+
+/**
+ * \brief Get the mapping value for a specific channel and map index.
+ *
+ * \param pMapDescr Pointer to channel map descriptor.
+ * \param chIdx Channel index.
+ * \param mapIdx Mapping index (corresponding to the channel configuration
+ * index).
+ * \return Mapping value.
+ */
+UCHAR FDK_chMapDescr_getMapValue(const FDK_channelMapDescr* const pMapDescr,
+ const UCHAR chIdx, const UINT mapIdx);
+
+/**
+ * \brief Evaluate whether channel map descriptor is reasonable or not.
+ *
+ * \param pMapDescr Pointer to channel map descriptor.
+ * \return Value unequal to zero if descriptor is valid, otherwise
+ * zero.
+ */
+int FDK_chMapDescr_isValid(const FDK_channelMapDescr* const pMapDescr);
+
+/**
+ * Extra variables for setting up Wg4 channel mapping.
+ */
+extern const CHANNEL_MAP_INFO FDK_mapInfoTabWg4[];
+extern const UINT FDK_mapInfoTabLenWg4;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* !defined(SYSLIB_CHANNELMAPDESCR_H) */
diff --git a/third_party/mini_chromium/base/base_wrapper.h b/third_party/mini_chromium/base/base_wrapper.h
index ae57c30..491f74f 100644
--- a/third_party/mini_chromium/base/base_wrapper.h
+++ b/third_party/mini_chromium/base/base_wrapper.h
@@ -16,14 +16,35 @@
#define MINI_CHROMIUM_BASE_BASE_WRAPPER_H_
// Change the symbol name to avoid collisions with //base
+#define Alias MAlias
+
+#define AssertAcquired MAssertAcquired
#define FilePath MFilePath
+#define CheckHeldAndUnmark MCheckHeldAndUnmark
+#define CheckUnheldAndMark MCheckUnheldAndMark
+#define ConditionVariable MConditionVariable
#define GetLogMessageHandler MGetLogMessageHandler
+#define Lock MLock
+#define LockImpl MLockImpl
#define LogMessage MLogMessage
+#define PlatformThreadLocalStorage MPlatformThreadLocalStorage
+#define RandBytes MRandBytes
+#define RandBytesAsString MRandBytesAsString
+#define RandDouble MRandDouble
+#define RandGenerator MRandGenerator
+#define RandInt MRandInt
+#define RandUint64 MRandUint64
#define ReadUnicodeCharacter MReadUnicodeCharacter
#define SetLogMessageHandler MSetLogMessageHandler
+#define StringAppendV MStringAppendV
+#define StringPrintf MStringPrintf
+#define ThreadLocalStorage MThreadLocalStorage
#define UTF16ToUTF8 MUTF16ToUTF8
#define UmaHistogramSparse MUmaHistogramSparse
+#define UncheckedMalloc MUncheckedMalloc
#define WriteUnicodeCharacter MWriteUnicodeCharacter
#define c16len mc16len
+#define utf8_nextCharSafeBody mutf8_nextCharSafeBody
+
#endif // MINI_CHROMIUM_BASE_BASE_WRAPPER_H_
diff --git a/third_party/mini_chromium/base/debug/alias.h b/third_party/mini_chromium/base/debug/alias.h
index 08d833a..3d764ce 100644
--- a/third_party/mini_chromium/base/debug/alias.h
+++ b/third_party/mini_chromium/base/debug/alias.h
@@ -5,6 +5,8 @@
#ifndef MINI_CHROMIUM_BASE_DEBUG_ALIAS_H_
#define MINI_CHROMIUM_BASE_DEBUG_ALIAS_H_
+#include "base/base_wrapper.h"
+
namespace base {
namespace debug {
diff --git a/third_party/mini_chromium/base/process/memory.h b/third_party/mini_chromium/base/process/memory.h
index fca8745..5205e8f 100644
--- a/third_party/mini_chromium/base/process/memory.h
+++ b/third_party/mini_chromium/base/process/memory.h
@@ -7,6 +7,7 @@
#include <stddef.h>
+#include "base/base_wrapper.h"
#include "base/compiler_specific.h"
namespace base {
diff --git a/third_party/mini_chromium/base/rand_util.h b/third_party/mini_chromium/base/rand_util.h
index 21ece1b..c0906bc 100644
--- a/third_party/mini_chromium/base/rand_util.h
+++ b/third_party/mini_chromium/base/rand_util.h
@@ -9,6 +9,8 @@
#include <string>
+#include "base/base_wrapper.h"
+
namespace base {
uint64_t RandUint64();
diff --git a/third_party/mini_chromium/base/strings/stringprintf.h b/third_party/mini_chromium/base/strings/stringprintf.h
index 1b1e60b..d2d794c 100644
--- a/third_party/mini_chromium/base/strings/stringprintf.h
+++ b/third_party/mini_chromium/base/strings/stringprintf.h
@@ -9,6 +9,7 @@
#include <string>
+#include "base/base_wrapper.h"
#include "base/compiler_specific.h"
namespace base {
diff --git a/third_party/mini_chromium/base/synchronization/condition_variable_posix.cc b/third_party/mini_chromium/base/synchronization/condition_variable_posix.cc
index 20af747..3c83e80 100644
--- a/third_party/mini_chromium/base/synchronization/condition_variable_posix.cc
+++ b/third_party/mini_chromium/base/synchronization/condition_variable_posix.cc
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#include "base/base_wrapper.h"
#include "base/synchronization/condition_variable.h"
#include "base/logging.h"
diff --git a/third_party/mini_chromium/base/synchronization/lock.cc b/third_party/mini_chromium/base/synchronization/lock.cc
index e94e637..dedcc82 100644
--- a/third_party/mini_chromium/base/synchronization/lock.cc
+++ b/third_party/mini_chromium/base/synchronization/lock.cc
@@ -6,6 +6,7 @@
// is functionally a wrapper around the LockImpl class, so the only
// real intelligence in the class is in the debugging logic.
+#include "base/base_wrapper.h"
#include "base/synchronization/lock.h"
#include "base/logging.h"
diff --git a/third_party/mini_chromium/base/synchronization/lock_impl_posix.cc b/third_party/mini_chromium/base/synchronization/lock_impl_posix.cc
index 27120df..8ae216c 100644
--- a/third_party/mini_chromium/base/synchronization/lock_impl_posix.cc
+++ b/third_party/mini_chromium/base/synchronization/lock_impl_posix.cc
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#include "base/base_wrapper.h"
#include "base/synchronization/lock_impl.h"
#include <errno.h>
diff --git a/third_party/mini_chromium/base/third_party/icu/icu_utf.h b/third_party/mini_chromium/base/third_party/icu/icu_utf.h
index df3fb5f..ebae921 100644
--- a/third_party/mini_chromium/base/third_party/icu/icu_utf.h
+++ b/third_party/mini_chromium/base/third_party/icu/icu_utf.h
@@ -14,6 +14,8 @@
#include <stdint.h>
+#include "base/base_wrapper.h"
+
namespace base_icu {
// source/common/unicode/umachine.h
diff --git a/third_party/mini_chromium/base/threading/thread_local_storage.h b/third_party/mini_chromium/base/threading/thread_local_storage.h
index fa79f00..211ef2c 100644
--- a/third_party/mini_chromium/base/threading/thread_local_storage.h
+++ b/third_party/mini_chromium/base/threading/thread_local_storage.h
@@ -5,6 +5,7 @@
#ifndef MINI_CHROMIUM_BASE_THREADING_THREAD_LOCAL_STORAGE_H_
#define MINI_CHROMIUM_BASE_THREADING_THREAD_LOCAL_STORAGE_H_
+#include "base/base_wrapper.h"
#include "base/macros.h"
#include "build/build_config.h"
diff --git a/third_party/openh264/METADATA b/third_party/openh264/METADATA
new file mode 100644
index 0000000..50a92fb
--- /dev/null
+++ b/third_party/openh264/METADATA
@@ -0,0 +1,18 @@
+# Format: google3/devtools/metadata/metadata.proto (go/google3metadata)
+
+name: "openh264"
+description:
+ "H.264/MPEG-4 (AVC) baseline profile video codec in C/C++ open-sourced by "
+ "Cisco."
+
+third_party {
+ url {
+ type: GIT
+ value: "https://github.com/cisco/openh264.git"
+ }
+ version: "v2.3.0"
+ last_upgrade_date { year: 2022 month: 7 day: 31 }
+ security {
+ tag: "NVD-CPE2.3:cpe:/a:cisco:openh264:2.3.0"
+ }
+}
diff --git a/third_party/openh264/README.md b/third_party/openh264/README.md
new file mode 100644
index 0000000..f973e25
--- /dev/null
+++ b/third_party/openh264/README.md
@@ -0,0 +1,7 @@
+# OpenH264 Version Compatibility
+The header files in this folder is for Release Version 2.3.0.
+It is also compatible to all openh264 library after Release Version 2.0.0.
+
+## Libraries
+All release versions can be accessed via the link below.
+https://github.com/cisco/openh264/releases
diff --git a/third_party/openh264/include/codec_api.h b/third_party/openh264/include/codec_api.h
new file mode 100644
index 0000000..a1326c8
--- /dev/null
+++ b/third_party/openh264/include/codec_api.h
@@ -0,0 +1,592 @@
+/*!
+ *@page License
+ *
+ * \copy
+ * Copyright (c) 2013, Cisco Systems
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+#ifndef WELS_VIDEO_CODEC_SVC_API_H__
+#define WELS_VIDEO_CODEC_SVC_API_H__
+
+#ifndef __cplusplus
+#if defined(_MSC_VER) && (_MSC_VER < 1800)
+typedef unsigned char bool;
+#else
+#include <stdbool.h>
+#endif
+#endif
+
+#include "codec_app_def.h"
+#include "codec_def.h"
+
+#if defined(_WIN32) || defined(__cdecl)
+#define EXTAPI __cdecl
+#else
+#define EXTAPI
+#endif
+
+/**
+ * @file codec_api.h
+*/
+
+/**
+ * @page Overview
+ * * This page is for openh264 codec API usage.
+ * * For how to use the encoder,please refer to page UsageExampleForEncoder
+ * * For how to use the decoder,please refer to page UsageExampleForDecoder
+ * * For more detail about ISVEncoder,please refer to page ISVCEncoder
+ * * For more detail about ISVDecoder,please refer to page ISVCDecoder
+*/
+
+/**
+ * @page DecoderUsageExample
+ *
+ * @brief
+ * * An example for using the decoder for Decoding only or Parsing only
+ *
+ * Step 1:decoder declaration
+ * @code
+ *
+ * //decoder declaration
+ * ISVCDecoder *pSvcDecoder;
+ * //input: encoded bitstream start position; should include start code prefix
+ * unsigned char *pBuf =...;
+ * //input: encoded bit stream length; should include the size of start code prefix
+ * int iSize =...;
+ * //output: [0~2] for Y,U,V buffer for Decoding only
+ * unsigned char *pData[3] =...;
+ * //in-out: for Decoding only: declare and initialize the output buffer info, this should never co-exist with Parsing only
+ * SBufferInfo sDstBufInfo;
+ * memset(&sDstBufInfo, 0, sizeof(SBufferInfo));
+ * //in-out: for Parsing only: declare and initialize the output bitstream buffer info for parse only, this should never co-exist with Decoding only
+ * SParserBsInfo sDstParseInfo;
+ * memset(&sDstParseInfo, 0, sizeof(SParserBsInfo));
+ * sDstParseInfo.pDstBuff = new unsigned char[PARSE_SIZE]; //In Parsing only, allocate enough buffer to save transcoded bitstream for a frame
+ *
+ * @endcode
+ *
+ * Step 2:decoder creation
+ * @code
+ * WelsCreateDecoder(&pSvcDecoder);
+ * @endcode
+ *
+ * Step 3:declare required parameter, used to differentiate Decoding only and Parsing only
+ * @code
+ * SDecodingParam sDecParam = {0};
+ * sDecParam.sVideoProperty.eVideoBsType = VIDEO_BITSTREAM_AVC;
+ * //for Parsing only, the assignment is mandatory
+ * sDecParam.bParseOnly = true;
+ * @endcode
+ *
+ * Step 4:initialize the parameter and decoder context, allocate memory
+ * @code
+ * pSvcDecoder->Initialize(&sDecParam);
+ * @endcode
+ *
+ * Step 5:do actual decoding process in slice level;
+ * this can be done in a loop until data ends
+ * @code
+ * //for Decoding only
+ * iRet = pSvcDecoder->DecodeFrameNoDelay(pBuf, iSize, pData, &sDstBufInfo);
+ * //or
+ * iRet = pSvcDecoder->DecodeFrame2(pBuf, iSize, pData, &sDstBufInfo);
+ * //for Parsing only
+ * iRet = pSvcDecoder->DecodeParser(pBuf, iSize, &sDstParseInfo);
+ * //decode failed
+ * If (iRet != 0){
+ * //error handling (RequestIDR or something like that)
+ * }
+ * //for Decoding only, pData can be used for render.
+ * if (sDstBufInfo.iBufferStatus==1){
+ * //output handling (pData[0], pData[1], pData[2])
+ * }
+ * //for Parsing only, sDstParseInfo can be used for, e.g., HW decoding
+ * if (sDstBufInfo.iNalNum > 0){
+ * //Hardware decoding sDstParseInfo;
+ * }
+ * //no-delay decoding can be realized by directly calling DecodeFrameNoDelay(), which is the recommended usage.
+ * //no-delay decoding can also be realized by directly calling DecodeFrame2() again with NULL input, as in the following. In this case, decoder would immediately reconstruct the input data. This can also be used similarly for Parsing only. Consequent decoding error and output indication should also be considered as above.
+ * iRet = pSvcDecoder->DecodeFrame2(NULL, 0, pData, &sDstBufInfo);
+ * //judge iRet, sDstBufInfo.iBufferStatus ...
+ * @endcode
+ *
+ * Step 6:uninitialize the decoder and memory free
+ * @code
+ * pSvcDecoder->Uninitialize();
+ * @endcode
+ *
+ * Step 7:destroy the decoder
+ * @code
+ * DestroyDecoder(pSvcDecoder);
+ * @endcode
+ *
+*/
+
+/**
+ * @page EncoderUsageExample1
+ *
+ * @brief
+ * * An example for using encoder with basic parameter
+ *
+ * Step1:setup encoder
+ * @code
+ * ISVCEncoder* encoder_;
+ * int rv = WelsCreateSVCEncoder (&encoder_);
+ * assert (rv == 0);
+ * assert (encoder_ != NULL);
+ * @endcode
+ *
+ * Step2:initilize with basic parameter
+ * @code
+ * SEncParamBase param;
+ * memset (¶m, 0, sizeof (SEncParamBase));
+ * param.iUsageType = usageType; //from EUsageType enum
+ * param.fMaxFrameRate = frameRate;
+ * param.iPicWidth = width;
+ * param.iPicHeight = height;
+ * param.iTargetBitrate = 5000000;
+ * encoder_->Initialize (¶m);
+ * @endcode
+ *
+ * Step3:set option, set option during encoding process
+ * @code
+ * encoder_->SetOption (ENCODER_OPTION_TRACE_LEVEL, &g_LevelSetting);
+ * int videoFormat = videoFormatI420;
+ * encoder_->SetOption (ENCODER_OPTION_DATAFORMAT, &videoFormat);
+ * @endcode
+ *
+ * Step4: encode and store ouput bistream
+ * @code
+ * int frameSize = width * height * 3 / 2;
+ * BufferedData buf;
+ * buf.SetLength (frameSize);
+ * assert (buf.Length() == (size_t)frameSize);
+ * SFrameBSInfo info;
+ * memset (&info, 0, sizeof (SFrameBSInfo));
+ * SSourcePicture pic;
+ * memset (&pic, 0, sizeof (SsourcePicture));
+ * pic.iPicWidth = width;
+ * pic.iPicHeight = height;
+ * pic.iColorFormat = videoFormatI420;
+ * pic.iStride[0] = pic.iPicWidth;
+ * pic.iStride[1] = pic.iStride[2] = pic.iPicWidth >> 1;
+ * pic.pData[0] = buf.data();
+ * pic.pData[1] = pic.pData[0] + width * height;
+ * pic.pData[2] = pic.pData[1] + (width * height >> 2);
+ * for(int num = 0;num<total_num;num++) {
+ * //prepare input data
+ * rv = encoder_->EncodeFrame (&pic, &info);
+ * assert (rv == cmResultSuccess);
+ * if (info.eFrameType != videoFrameTypeSkip) {
+ * //output bitstream handling
+ * }
+ * }
+ * @endcode
+ *
+ * Step5:teardown encoder
+ * @code
+ * if (encoder_) {
+ * encoder_->Uninitialize();
+ * WelsDestroySVCEncoder (encoder_);
+ * }
+ * @endcode
+ *
+ */
+
+/**
+ * @page EncoderUsageExample2
+ *
+ * @brief
+ * * An example for using the encoder with extension parameter.
+ * * The same operation on Step 1,3,4,5 with Example-1
+ *
+ * Step 2:initialize with extension parameter
+ * @code
+ * SEncParamExt param;
+ * encoder_->GetDefaultParams (¶m);
+ * param.iUsageType = usageType;
+ * param.fMaxFrameRate = frameRate;
+ * param.iPicWidth = width;
+ * param.iPicHeight = height;
+ * param.iTargetBitrate = 5000000;
+ * param.bEnableDenoise = denoise;
+ * param.iSpatialLayerNum = layers;
+ * //SM_DYN_SLICE don't support multi-thread now
+ * if (sliceMode != SM_SINGLE_SLICE && sliceMode != SM_DYN_SLICE)
+ * param.iMultipleThreadIdc = 2;
+ *
+ * for (int i = 0; i < param.iSpatialLayerNum; i++) {
+ * param.sSpatialLayers[i].iVideoWidth = width >> (param.iSpatialLayerNum - 1 - i);
+ * param.sSpatialLayers[i].iVideoHeight = height >> (param.iSpatialLayerNum - 1 - i);
+ * param.sSpatialLayers[i].fFrameRate = frameRate;
+ * param.sSpatialLayers[i].iSpatialBitrate = param.iTargetBitrate;
+ *
+ * param.sSpatialLayers[i].sSliceCfg.uiSliceMode = sliceMode;
+ * if (sliceMode == SM_DYN_SLICE) {
+ * param.sSpatialLayers[i].sSliceCfg.sSliceArgument.uiSliceSizeConstraint = 600;
+ * param.uiMaxNalSize = 1500;
+ * }
+ * }
+ * param.iTargetBitrate *= param.iSpatialLayerNum;
+ * encoder_->InitializeExt (¶m);
+ * int videoFormat = videoFormatI420;
+ * encoder_->SetOption (ENCODER_OPTION_DATAFORMAT, &videoFormat);
+ *
+ * @endcode
+ */
+
+
+
+
+#ifdef __cplusplus
+/**
+* @brief Endocder definition
+*/
+class ISVCEncoder {
+ public:
+ /**
+ * @brief Initialize the encoder
+ * @param pParam basic encoder parameter
+ * @return CM_RETURN: 0 - success; otherwise - failed;
+ */
+ virtual int EXTAPI Initialize (const SEncParamBase* pParam) = 0;
+
+ /**
+ * @brief Initilaize encoder by using extension parameters.
+ * @param pParam extension parameter for encoder
+ * @return CM_RETURN: 0 - success; otherwise - failed;
+ */
+ virtual int EXTAPI InitializeExt (const SEncParamExt* pParam) = 0;
+
+ /**
+ * @brief Get the default extension parameters.
+ * If you want to change some parameters of encoder, firstly you need to get the default encoding parameters,
+ * after that you can change part of parameters you want to.
+ * @param pParam extension parameter for encoder
+ * @return CM_RETURN: 0 - success; otherwise - failed;
+ * */
+ virtual int EXTAPI GetDefaultParams (SEncParamExt* pParam) = 0;
+ /// uninitialize the encoder
+ virtual int EXTAPI Uninitialize() = 0;
+
+ /**
+ * @brief Encode one frame
+ * @param kpSrcPic the pointer to the source luminance plane
+ * chrominance data:
+ * CbData = kpSrc + m_iMaxPicWidth * m_iMaxPicHeight;
+ * CrData = CbData + (m_iMaxPicWidth * m_iMaxPicHeight)/4;
+ * the application calling this interface needs to ensure the data validation between the location
+ * @param pBsInfo output bit stream
+ * @return 0 - success; otherwise -failed;
+ */
+ virtual int EXTAPI EncodeFrame (const SSourcePicture* kpSrcPic, SFrameBSInfo* pBsInfo) = 0;
+
+ /**
+ * @brief Encode the parameters from output bit stream
+ * @param pBsInfo output bit stream
+ * @return 0 - success; otherwise - failed;
+ */
+ virtual int EXTAPI EncodeParameterSets (SFrameBSInfo* pBsInfo) = 0;
+
+ /**
+ * @brief Force encoder to encoder frame as IDR if bIDR set as true
+ * @param bIDR true: force encoder to encode frame as IDR frame;false, return 1 and nothing to do
+ * @return 0 - success; otherwise - failed;
+ */
+ virtual int EXTAPI ForceIntraFrame (bool bIDR, int iLayerId = -1) = 0;
+
+ /**
+ * @brief Set option for encoder, detail option type, please refer to enumurate ENCODER_OPTION.
+ * @param pOption option for encoder such as InDataFormat, IDRInterval, SVC Encode Param, Frame Rate, Bitrate,...
+ * @return CM_RETURN: 0 - success; otherwise - failed;
+ */
+ virtual int EXTAPI SetOption (ENCODER_OPTION eOptionId, void* pOption) = 0;
+
+ /**
+ * @brief Get option for encoder, detail option type, please refer to enumurate ENCODER_OPTION.
+ * @param pOption option for encoder such as InDataFormat, IDRInterval, SVC Encode Param, Frame Rate, Bitrate,...
+ * @return CM_RETURN: 0 - success; otherwise - failed;
+ */
+ virtual int EXTAPI GetOption (ENCODER_OPTION eOptionId, void* pOption) = 0;
+ virtual ~ISVCEncoder() {}
+};
+
+
+
+/**
+* @brief Decoder definition
+*/
+class ISVCDecoder {
+ public:
+
+ /**
+ * @brief Initilaize decoder
+ * @param pParam parameter for decoder
+ * @return 0 - success; otherwise - failed;
+ */
+ virtual long EXTAPI Initialize (const SDecodingParam* pParam) = 0;
+
+ /// Uninitialize the decoder
+ virtual long EXTAPI Uninitialize() = 0;
+
+ /**
+ * @brief Decode one frame
+ * @param pSrc the h264 stream to be decoded
+ * @param iSrcLen the length of h264 stream
+ * @param ppDst buffer pointer of decoded data (YUV)
+ * @param pStride output stride
+ * @param iWidth output width
+ * @param iHeight output height
+ * @return 0 - success; otherwise -failed;
+ */
+ virtual DECODING_STATE EXTAPI DecodeFrame (const unsigned char* pSrc,
+ const int iSrcLen,
+ unsigned char** ppDst,
+ int* pStride,
+ int& iWidth,
+ int& iHeight) = 0;
+
+ /**
+ * @brief For slice level DecodeFrameNoDelay() (4 parameters input),
+ * whatever the function return value is, the output data
+ * of I420 format will only be available when pDstInfo->iBufferStatus == 1,.
+ * This function will parse and reconstruct the input frame immediately if it is complete
+ * It is recommended as the main decoding function for H.264/AVC format input
+ * @param pSrc the h264 stream to be decoded
+ * @param iSrcLen the length of h264 stream
+ * @param ppDst buffer pointer of decoded data (YUV)
+ * @param pDstInfo information provided to API(width, height, etc.)
+ * @return 0 - success; otherwise -failed;
+ */
+ virtual DECODING_STATE EXTAPI DecodeFrameNoDelay (const unsigned char* pSrc,
+ const int iSrcLen,
+ unsigned char** ppDst,
+ SBufferInfo* pDstInfo) = 0;
+
+ /**
+ * @brief For slice level DecodeFrame2() (4 parameters input),
+ * whatever the function return value is, the output data
+ * of I420 format will only be available when pDstInfo->iBufferStatus == 1,.
+ * (e.g., in multi-slice cases, only when the whole picture
+ * is completely reconstructed, this variable would be set equal to 1.)
+ * @param pSrc the h264 stream to be decoded
+ * @param iSrcLen the length of h264 stream
+ * @param ppDst buffer pointer of decoded data (YUV)
+ * @param pDstInfo information provided to API(width, height, etc.)
+ * @return 0 - success; otherwise -failed;
+ */
+ virtual DECODING_STATE EXTAPI DecodeFrame2 (const unsigned char* pSrc,
+ const int iSrcLen,
+ unsigned char** ppDst,
+ SBufferInfo* pDstInfo) = 0;
+
+
+ /**
+ * @brief This function gets a decoded ready frame remaining in buffers after the last frame has been decoded.
+ * Use GetOption with option DECODER_OPTION_NUM_OF_FRAMES_REMAINING_IN_BUFFER to get the number of frames remaining in buffers.
+ * Note that it is only applicable for profile_idc != 66
+ * @param ppDst buffer pointer of decoded data (YUV)
+ * @param pDstInfo information provided to API(width, height, etc.)
+ * @return 0 - success; otherwise -failed;
+ */
+ virtual DECODING_STATE EXTAPI FlushFrame (unsigned char** ppDst,
+ SBufferInfo* pDstInfo) = 0;
+
+ /**
+ * @brief This function parse input bitstream only, and rewrite possible SVC syntax to AVC syntax
+ * @param pSrc the h264 stream to be decoded
+ * @param iSrcLen the length of h264 stream
+ * @param pDstInfo bit stream info
+ * @return 0 - success; otherwise -failed;
+ */
+ virtual DECODING_STATE EXTAPI DecodeParser (const unsigned char* pSrc,
+ const int iSrcLen,
+ SParserBsInfo* pDstInfo) = 0;
+
+ /**
+ * @brief This API does not work for now!! This is for future use to support non-I420 color format output.
+ * @param pSrc the h264 stream to be decoded
+ * @param iSrcLen the length of h264 stream
+ * @param pDst buffer pointer of decoded data (YUV)
+ * @param iDstStride output stride
+ * @param iDstLen bit stream info
+ * @param iWidth output width
+ * @param iHeight output height
+ * @param iColorFormat output color format
+ * @return to do ...
+ */
+ virtual DECODING_STATE EXTAPI DecodeFrameEx (const unsigned char* pSrc,
+ const int iSrcLen,
+ unsigned char* pDst,
+ int iDstStride,
+ int& iDstLen,
+ int& iWidth,
+ int& iHeight,
+ int& iColorFormat) = 0;
+
+ /**
+ * @brief Set option for decoder, detail option type, please refer to enumurate DECODER_OPTION.
+ * @param pOption option for decoder such as OutDataFormat, Eos Flag, EC method, ...
+ * @return CM_RETURN: 0 - success; otherwise - failed;
+ */
+ virtual long EXTAPI SetOption (DECODER_OPTION eOptionId, void* pOption) = 0;
+
+ /**
+ * @brief Get option for decoder, detail option type, please refer to enumurate DECODER_OPTION.
+ * @param pOption option for decoder such as OutDataFormat, Eos Flag, EC method, ...
+ * @return CM_RETURN: 0 - success; otherwise - failed;
+ */
+ virtual long EXTAPI GetOption (DECODER_OPTION eOptionId, void* pOption) = 0;
+ virtual ~ISVCDecoder() {}
+};
+
+
+extern "C"
+{
+#else
+
+typedef struct ISVCEncoderVtbl ISVCEncoderVtbl;
+typedef const ISVCEncoderVtbl* ISVCEncoder;
+struct ISVCEncoderVtbl {
+
+int (*Initialize) (ISVCEncoder*, const SEncParamBase* pParam);
+int (*InitializeExt) (ISVCEncoder*, const SEncParamExt* pParam);
+
+int (*GetDefaultParams) (ISVCEncoder*, SEncParamExt* pParam);
+
+int (*Uninitialize) (ISVCEncoder*);
+
+int (*EncodeFrame) (ISVCEncoder*, const SSourcePicture* kpSrcPic, SFrameBSInfo* pBsInfo);
+int (*EncodeParameterSets) (ISVCEncoder*, SFrameBSInfo* pBsInfo);
+
+int (*ForceIntraFrame) (ISVCEncoder*, bool bIDR);
+
+int (*SetOption) (ISVCEncoder*, ENCODER_OPTION eOptionId, void* pOption);
+int (*GetOption) (ISVCEncoder*, ENCODER_OPTION eOptionId, void* pOption);
+};
+
+typedef struct ISVCDecoderVtbl ISVCDecoderVtbl;
+typedef const ISVCDecoderVtbl* ISVCDecoder;
+struct ISVCDecoderVtbl {
+long (*Initialize) (ISVCDecoder*, const SDecodingParam* pParam);
+long (*Uninitialize) (ISVCDecoder*);
+
+DECODING_STATE (*DecodeFrame) (ISVCDecoder*, const unsigned char* pSrc,
+ const int iSrcLen,
+ unsigned char** ppDst,
+ int* pStride,
+ int* iWidth,
+ int* iHeight);
+
+DECODING_STATE (*DecodeFrameNoDelay) (ISVCDecoder*, const unsigned char* pSrc,
+ const int iSrcLen,
+ unsigned char** ppDst,
+ SBufferInfo* pDstInfo);
+
+DECODING_STATE (*DecodeFrame2) (ISVCDecoder*, const unsigned char* pSrc,
+ const int iSrcLen,
+ unsigned char** ppDst,
+ SBufferInfo* pDstInfo);
+
+DECODING_STATE (*FlushFrame) (ISVCDecoder*, unsigned char** ppDst,
+ SBufferInfo* pDstInfo);
+
+DECODING_STATE (*DecodeParser) (ISVCDecoder*, const unsigned char* pSrc,
+ const int iSrcLen,
+ SParserBsInfo* pDstInfo);
+
+DECODING_STATE (*DecodeFrameEx) (ISVCDecoder*, const unsigned char* pSrc,
+ const int iSrcLen,
+ unsigned char* pDst,
+ int iDstStride,
+ int* iDstLen,
+ int* iWidth,
+ int* iHeight,
+ int* iColorFormat);
+
+long (*SetOption) (ISVCDecoder*, DECODER_OPTION eOptionId, void* pOption);
+long (*GetOption) (ISVCDecoder*, DECODER_OPTION eOptionId, void* pOption);
+};
+#endif
+
+typedef void (*WelsTraceCallback) (void* ctx, int level, const char* string);
+
+/** @brief Create encoder
+ * @param ppEncoder encoder
+ * @return 0 - success; otherwise - failed;
+*/
+int WelsCreateSVCEncoder (ISVCEncoder** ppEncoder);
+
+
+/** @brief Destroy encoder
+* @param pEncoder encoder
+ * @return void
+*/
+void WelsDestroySVCEncoder (ISVCEncoder* pEncoder);
+
+
+/** @brief Get the capability of decoder
+ * @param pDecCapability decoder capability
+ * @return 0 - success; otherwise - failed;
+*/
+int WelsGetDecoderCapability (SDecoderCapability* pDecCapability);
+
+
+/** @brief Create decoder
+ * @param ppDecoder decoder
+ * @return 0 - success; otherwise - failed;
+*/
+long WelsCreateDecoder (ISVCDecoder** ppDecoder);
+
+
+/** @brief Destroy decoder
+ * @param pDecoder decoder
+ * @return void
+*/
+void WelsDestroyDecoder (ISVCDecoder* pDecoder);
+
+/** @brief Get codec version
+ * Note, old versions of Mingw (GCC < 4.7) are buggy and use an
+ * incorrect/different ABI for calling this function, making it
+ * incompatible with MSVC builds.
+ * @return The linked codec version
+*/
+OpenH264Version WelsGetCodecVersion (void);
+
+/** @brief Get codec version
+ * @param pVersion struct to fill in with the version
+*/
+void WelsGetCodecVersionEx (OpenH264Version* pVersion);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif//WELS_VIDEO_CODEC_SVC_API_H__
diff --git a/third_party/openh264/include/codec_app_def.h b/third_party/openh264/include/codec_app_def.h
new file mode 100644
index 0000000..9b13c33
--- /dev/null
+++ b/third_party/openh264/include/codec_app_def.h
@@ -0,0 +1,812 @@
+/*!
+ * \copy
+ * Copyright (c) 2013, Cisco Systems
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+
+
+#ifndef WELS_VIDEO_CODEC_APPLICATION_DEFINITION_H__
+#define WELS_VIDEO_CODEC_APPLICATION_DEFINITION_H__
+/**
+ * @file codec_app_def.h
+ * @brief Data and /or structures introduced in Cisco OpenH264 application
+*/
+
+#include "codec_def.h"
+/* Constants */
+#define MAX_TEMPORAL_LAYER_NUM 4
+#define MAX_SPATIAL_LAYER_NUM 4
+#define MAX_QUALITY_LAYER_NUM 4
+
+#define MAX_LAYER_NUM_OF_FRAME 128
+#define MAX_NAL_UNITS_IN_LAYER 128 ///< predetermined here, adjust it later if need
+
+#define MAX_RTP_PAYLOAD_LEN 1000
+#define AVERAGE_RTP_PAYLOAD_LEN 800
+
+
+#define SAVED_NALUNIT_NUM_TMP ( (MAX_SPATIAL_LAYER_NUM*MAX_QUALITY_LAYER_NUM) + 1 + MAX_SPATIAL_LAYER_NUM ) ///< SPS/PPS + SEI/SSEI + PADDING_NAL
+#define MAX_SLICES_NUM_TMP ( ( MAX_NAL_UNITS_IN_LAYER - SAVED_NALUNIT_NUM_TMP ) / 3 )
+
+
+#define AUTO_REF_PIC_COUNT -1 ///< encoder selects the number of reference frame automatically
+#define UNSPECIFIED_BIT_RATE 0 ///< to do: add detail comment
+
+/**
+ * @brief Struct of OpenH264 version
+ */
+///
+/// E.g. SDK version is 1.2.0.0, major version number is 1, minor version number is 2, and revision number is 0.
+typedef struct _tagVersion {
+ unsigned int uMajor; ///< The major version number
+ unsigned int uMinor; ///< The minor version number
+ unsigned int uRevision; ///< The revision number
+ unsigned int uReserved; ///< The reserved number, it should be 0.
+} OpenH264Version;
+
+/**
+* @brief Decoding status
+*/
+typedef enum {
+ /**
+ * Errors derived from bitstream parsing
+ */
+ dsErrorFree = 0x00, ///< bit stream error-free
+ dsFramePending = 0x01, ///< need more throughput to generate a frame output,
+ dsRefLost = 0x02, ///< layer lost at reference frame with temporal id 0
+ dsBitstreamError = 0x04, ///< error bitstreams(maybe broken internal frame) the decoder cared
+ dsDepLayerLost = 0x08, ///< dependented layer is ever lost
+ dsNoParamSets = 0x10, ///< no parameter set NALs involved
+ dsDataErrorConcealed = 0x20, ///< current data error concealed specified
+ dsRefListNullPtrs = 0x40, ///<ref picure list contains null ptrs within uiRefCount range
+
+ /**
+ * Errors derived from logic level
+ */
+ dsInvalidArgument = 0x1000, ///< invalid argument specified
+ dsInitialOptExpected = 0x2000, ///< initializing operation is expected
+ dsOutOfMemory = 0x4000, ///< out of memory due to new request
+ /**
+ * ANY OTHERS?
+ */
+ dsDstBufNeedExpan = 0x8000 ///< actual picture size exceeds size of dst pBuffer feed in decoder, so need expand its size
+
+} DECODING_STATE;
+
+/**
+* @brief Option types introduced in SVC encoder application
+*/
+typedef enum {
+ ENCODER_OPTION_DATAFORMAT = 0,
+ ENCODER_OPTION_IDR_INTERVAL, ///< IDR period,0/-1 means no Intra period (only the first frame); lager than 0 means the desired IDR period, must be multiple of (2^temporal_layer)
+ ENCODER_OPTION_SVC_ENCODE_PARAM_BASE, ///< structure of Base Param
+ ENCODER_OPTION_SVC_ENCODE_PARAM_EXT, ///< structure of Extension Param
+ ENCODER_OPTION_FRAME_RATE, ///< maximal input frame rate, current supported range: MAX_FRAME_RATE = 30,MIN_FRAME_RATE = 1
+ ENCODER_OPTION_BITRATE,
+ ENCODER_OPTION_MAX_BITRATE,
+ ENCODER_OPTION_INTER_SPATIAL_PRED,
+ ENCODER_OPTION_RC_MODE,
+ ENCODER_OPTION_RC_FRAME_SKIP,
+ ENCODER_PADDING_PADDING, ///< 0:disable padding;1:padding
+
+ ENCODER_OPTION_PROFILE, ///< assgin the profile for each layer
+ ENCODER_OPTION_LEVEL, ///< assgin the level for each layer
+ ENCODER_OPTION_NUMBER_REF, ///< the number of refererence frame
+ ENCODER_OPTION_DELIVERY_STATUS, ///< the delivery info which is a feedback from app level
+
+ ENCODER_LTR_RECOVERY_REQUEST,
+ ENCODER_LTR_MARKING_FEEDBACK,
+ ENCODER_LTR_MARKING_PERIOD,
+ ENCODER_OPTION_LTR, ///< 0:disable LTR;larger than 0 enable LTR; LTR number is fixed to be 2 in current encoder
+ ENCODER_OPTION_COMPLEXITY,
+
+ ENCODER_OPTION_ENABLE_SSEI, ///< enable SSEI: true--enable ssei; false--disable ssei
+ ENCODER_OPTION_ENABLE_PREFIX_NAL_ADDING, ///< enable prefix: true--enable prefix; false--disable prefix
+ ENCODER_OPTION_SPS_PPS_ID_STRATEGY, ///< different stategy in adjust ID in SPS/PPS: 0- constant ID, 1-additional ID, 6-mapping and additional
+
+ ENCODER_OPTION_CURRENT_PATH,
+ ENCODER_OPTION_DUMP_FILE, ///< dump layer reconstruct frame to a specified file
+ ENCODER_OPTION_TRACE_LEVEL, ///< trace info based on the trace level
+ ENCODER_OPTION_TRACE_CALLBACK, ///< a void (*)(void* context, int level, const char* message) function which receives log messages
+ ENCODER_OPTION_TRACE_CALLBACK_CONTEXT, ///< context info of trace callback
+
+ ENCODER_OPTION_GET_STATISTICS, ///< read only
+ ENCODER_OPTION_STATISTICS_LOG_INTERVAL, ///< log interval in millisecond
+
+ ENCODER_OPTION_IS_LOSSLESS_LINK, ///< advanced algorithmetic settings
+
+ ENCODER_OPTION_BITS_VARY_PERCENTAGE ///< bit vary percentage
+} ENCODER_OPTION;
+
+/**
+* @brief Option types introduced in decoder application
+*/
+typedef enum {
+ DECODER_OPTION_END_OF_STREAM = 1, ///< end of stream flag
+ DECODER_OPTION_VCL_NAL, ///< feedback whether or not have VCL NAL in current AU for application layer
+ DECODER_OPTION_TEMPORAL_ID, ///< feedback temporal id for application layer
+ DECODER_OPTION_FRAME_NUM, ///< feedback current decoded frame number
+ DECODER_OPTION_IDR_PIC_ID, ///< feedback current frame belong to which IDR period
+ DECODER_OPTION_LTR_MARKING_FLAG, ///< feedback wether current frame mark a LTR
+ DECODER_OPTION_LTR_MARKED_FRAME_NUM, ///< feedback frame num marked by current Frame
+ DECODER_OPTION_ERROR_CON_IDC, ///< indicate decoder error concealment method
+ DECODER_OPTION_TRACE_LEVEL,
+ DECODER_OPTION_TRACE_CALLBACK, ///< a void (*)(void* context, int level, const char* message) function which receives log messages
+ DECODER_OPTION_TRACE_CALLBACK_CONTEXT,///< context info of trace callbac
+
+ DECODER_OPTION_GET_STATISTICS, ///< feedback decoder statistics
+ DECODER_OPTION_GET_SAR_INFO, ///< feedback decoder Sample Aspect Ratio info in Vui
+ DECODER_OPTION_PROFILE, ///< get current AU profile info, only is used in GetOption
+ DECODER_OPTION_LEVEL, ///< get current AU level info,only is used in GetOption
+ DECODER_OPTION_STATISTICS_LOG_INTERVAL,///< set log output interval
+ DECODER_OPTION_IS_REF_PIC, ///< feedback current frame is ref pic or not
+ DECODER_OPTION_NUM_OF_FRAMES_REMAINING_IN_BUFFER, ///< number of frames remaining in decoder buffer when pictures are required to re-ordered into display-order.
+ DECODER_OPTION_NUM_OF_THREADS, ///< number of decoding threads. The maximum thread count is equal or less than lesser of (cpu core counts and 16).
+} DECODER_OPTION;
+
+/**
+* @brief Enumerate the type of error concealment methods
+*/
+typedef enum {
+ ERROR_CON_DISABLE = 0,
+ ERROR_CON_FRAME_COPY,
+ ERROR_CON_SLICE_COPY,
+ ERROR_CON_FRAME_COPY_CROSS_IDR,
+ ERROR_CON_SLICE_COPY_CROSS_IDR,
+ ERROR_CON_SLICE_COPY_CROSS_IDR_FREEZE_RES_CHANGE,
+ ERROR_CON_SLICE_MV_COPY_CROSS_IDR,
+ ERROR_CON_SLICE_MV_COPY_CROSS_IDR_FREEZE_RES_CHANGE
+} ERROR_CON_IDC;
+/**
+* @brief Feedback that whether or not have VCL NAL in current AU
+*/
+typedef enum {
+ FEEDBACK_NON_VCL_NAL = 0,
+ FEEDBACK_VCL_NAL,
+ FEEDBACK_UNKNOWN_NAL
+} FEEDBACK_VCL_NAL_IN_AU;
+
+/**
+* @brief Type of layer being encoded
+*/
+typedef enum {
+ NON_VIDEO_CODING_LAYER = 0,
+ VIDEO_CODING_LAYER = 1
+} LAYER_TYPE;
+
+/**
+* @brief Spatial layer num
+*/
+typedef enum {
+ SPATIAL_LAYER_0 = 0,
+ SPATIAL_LAYER_1 = 1,
+ SPATIAL_LAYER_2 = 2,
+ SPATIAL_LAYER_3 = 3,
+ SPATIAL_LAYER_ALL = 4
+} LAYER_NUM;
+
+/**
+* @brief Enumerate the type of video bitstream which is provided to decoder
+*/
+typedef enum {
+ VIDEO_BITSTREAM_AVC = 0,
+ VIDEO_BITSTREAM_SVC = 1,
+ VIDEO_BITSTREAM_DEFAULT = VIDEO_BITSTREAM_SVC
+} VIDEO_BITSTREAM_TYPE;
+
+/**
+* @brief Enumerate the type of key frame request
+*/
+typedef enum {
+ NO_RECOVERY_REQUSET = 0,
+ LTR_RECOVERY_REQUEST = 1,
+ IDR_RECOVERY_REQUEST = 2,
+ NO_LTR_MARKING_FEEDBACK = 3,
+ LTR_MARKING_SUCCESS = 4,
+ LTR_MARKING_FAILED = 5
+} KEY_FRAME_REQUEST_TYPE;
+
+/**
+* @brief Structure for LTR recover request
+*/
+typedef struct {
+ unsigned int uiFeedbackType; ///< IDR request or LTR recovery request
+ unsigned int uiIDRPicId; ///< distinguish request from different IDR
+ int iLastCorrectFrameNum;
+ int iCurrentFrameNum; ///< specify current decoder frame_num.
+ int iLayerId; //specify the layer for recovery request
+} SLTRRecoverRequest;
+
+/**
+* @brief Structure for LTR marking feedback
+*/
+typedef struct {
+ unsigned int uiFeedbackType; ///< mark failed or successful
+ unsigned int uiIDRPicId; ///< distinguish request from different IDR
+ int iLTRFrameNum; ///< specify current decoder frame_num
+ int iLayerId; //specify the layer for LTR marking feedback
+} SLTRMarkingFeedback;
+
+/**
+* @brief Structure for LTR configuration
+*/
+typedef struct {
+ bool bEnableLongTermReference; ///< 1: on, 0: off
+ int iLTRRefNum; ///< TODO: not supported to set it arbitrary yet
+} SLTRConfig;
+
+/**
+* @brief Enumerate the type of rate control mode
+*/
+typedef enum {
+ RC_QUALITY_MODE = 0, ///< quality mode
+ RC_BITRATE_MODE = 1, ///< bitrate mode
+ RC_BUFFERBASED_MODE = 2, ///< no bitrate control,only using buffer status,adjust the video quality
+ RC_TIMESTAMP_MODE = 3, //rate control based timestamp
+ RC_BITRATE_MODE_POST_SKIP = 4, ///< this is in-building RC MODE, WILL BE DELETED after algorithm tuning!
+ RC_OFF_MODE = -1, ///< rate control off mode
+} RC_MODES;
+
+/**
+* @brief Enumerate the type of profile id
+*/
+typedef enum {
+ PRO_UNKNOWN = 0,
+ PRO_BASELINE = 66,
+ PRO_MAIN = 77,
+ PRO_EXTENDED = 88,
+ PRO_HIGH = 100,
+ PRO_HIGH10 = 110,
+ PRO_HIGH422 = 122,
+ PRO_HIGH444 = 144,
+ PRO_CAVLC444 = 244,
+
+ PRO_SCALABLE_BASELINE = 83,
+ PRO_SCALABLE_HIGH = 86
+} EProfileIdc;
+
+/**
+* @brief Enumerate the type of level id
+*/
+typedef enum {
+ LEVEL_UNKNOWN = 0,
+ LEVEL_1_0 = 10,
+ LEVEL_1_B = 9,
+ LEVEL_1_1 = 11,
+ LEVEL_1_2 = 12,
+ LEVEL_1_3 = 13,
+ LEVEL_2_0 = 20,
+ LEVEL_2_1 = 21,
+ LEVEL_2_2 = 22,
+ LEVEL_3_0 = 30,
+ LEVEL_3_1 = 31,
+ LEVEL_3_2 = 32,
+ LEVEL_4_0 = 40,
+ LEVEL_4_1 = 41,
+ LEVEL_4_2 = 42,
+ LEVEL_5_0 = 50,
+ LEVEL_5_1 = 51,
+ LEVEL_5_2 = 52
+} ELevelIdc;
+
+/**
+* @brief Enumerate the type of wels log
+*/
+enum {
+ WELS_LOG_QUIET = 0x00, ///< quiet mode
+ WELS_LOG_ERROR = 1 << 0, ///< error log iLevel
+ WELS_LOG_WARNING = 1 << 1, ///< Warning log iLevel
+ WELS_LOG_INFO = 1 << 2, ///< information log iLevel
+ WELS_LOG_DEBUG = 1 << 3, ///< debug log, critical algo log
+ WELS_LOG_DETAIL = 1 << 4, ///< per packet/frame log
+ WELS_LOG_RESV = 1 << 5, ///< resversed log iLevel
+ WELS_LOG_LEVEL_COUNT = 6,
+ WELS_LOG_DEFAULT = WELS_LOG_WARNING ///< default log iLevel in Wels codec
+};
+
+/**
+ * @brief Enumerate the type of slice mode
+ */
+typedef enum {
+ SM_SINGLE_SLICE = 0, ///< | SliceNum==1
+ SM_FIXEDSLCNUM_SLICE = 1, ///< | according to SliceNum | enabled dynamic slicing for multi-thread
+ SM_RASTER_SLICE = 2, ///< | according to SlicesAssign | need input of MB numbers each slice. In addition, if other constraint in SSliceArgument is presented, need to follow the constraints. Typically if MB num and slice size are both constrained, re-encoding may be involved.
+ SM_SIZELIMITED_SLICE = 3, ///< | according to SliceSize | slicing according to size, the slicing will be dynamic(have no idea about slice_nums until encoding current frame)
+ SM_RESERVED = 4
+} SliceModeEnum;
+
+/**
+ * @brief Structure for slice argument
+ */
+typedef struct {
+ SliceModeEnum uiSliceMode; ///< by default, uiSliceMode will be SM_SINGLE_SLICE
+ unsigned int
+ uiSliceNum; ///< only used when uiSliceMode=1, when uiSliceNum=0 means auto design it with cpu core number
+ unsigned int
+ uiSliceMbNum[MAX_SLICES_NUM_TMP]; ///< only used when uiSliceMode=2; when =0 means setting one MB row a slice
+ unsigned int uiSliceSizeConstraint; ///< now only used when uiSliceMode=4
+} SSliceArgument;
+
+/**
+* @brief Enumerate the type of video format
+*/
+typedef enum {
+ VF_COMPONENT,
+ VF_PAL,
+ VF_NTSC,
+ VF_SECAM,
+ VF_MAC,
+ VF_UNDEF,
+ VF_NUM_ENUM
+} EVideoFormatSPS; // EVideoFormat is already defined/used elsewhere!
+
+/**
+* @brief Enumerate the type of color primaries
+*/
+typedef enum {
+ CP_RESERVED0,
+ CP_BT709,
+ CP_UNDEF,
+ CP_RESERVED3,
+ CP_BT470M,
+ CP_BT470BG,
+ CP_SMPTE170M,
+ CP_SMPTE240M,
+ CP_FILM,
+ CP_BT2020,
+ CP_NUM_ENUM
+} EColorPrimaries;
+
+/**
+* @brief Enumerate the type of transfer characteristics
+*/
+typedef enum {
+ TRC_RESERVED0,
+ TRC_BT709,
+ TRC_UNDEF,
+ TRC_RESERVED3,
+ TRC_BT470M,
+ TRC_BT470BG,
+ TRC_SMPTE170M,
+ TRC_SMPTE240M,
+ TRC_LINEAR,
+ TRC_LOG100,
+ TRC_LOG316,
+ TRC_IEC61966_2_4,
+ TRC_BT1361E,
+ TRC_IEC61966_2_1,
+ TRC_BT2020_10,
+ TRC_BT2020_12,
+ TRC_NUM_ENUM
+} ETransferCharacteristics;
+
+/**
+* @brief Enumerate the type of color matrix
+*/
+typedef enum {
+ CM_GBR,
+ CM_BT709,
+ CM_UNDEF,
+ CM_RESERVED3,
+ CM_FCC,
+ CM_BT470BG,
+ CM_SMPTE170M,
+ CM_SMPTE240M,
+ CM_YCGCO,
+ CM_BT2020NC,
+ CM_BT2020C,
+ CM_NUM_ENUM
+} EColorMatrix;
+
+
+/**
+* @brief Enumerate the type of sample aspect ratio
+*/
+typedef enum {
+ ASP_UNSPECIFIED = 0,
+ ASP_1x1 = 1,
+ ASP_12x11 = 2,
+ ASP_10x11 = 3,
+ ASP_16x11 = 4,
+ ASP_40x33 = 5,
+ ASP_24x11 = 6,
+ ASP_20x11 = 7,
+ ASP_32x11 = 8,
+ ASP_80x33 = 9,
+ ASP_18x11 = 10,
+ ASP_15x11 = 11,
+ ASP_64x33 = 12,
+ ASP_160x99 = 13,
+
+ ASP_EXT_SAR = 255
+} ESampleAspectRatio;
+
+
+/**
+* @brief Structure for spatial layer configuration
+*/
+typedef struct {
+ int iVideoWidth; ///< width of picture in luminance samples of a layer
+ int iVideoHeight; ///< height of picture in luminance samples of a layer
+ float fFrameRate; ///< frame rate specified for a layer
+ int iSpatialBitrate; ///< target bitrate for a spatial layer, in unit of bps
+ int iMaxSpatialBitrate; ///< maximum bitrate for a spatial layer, in unit of bps
+ EProfileIdc uiProfileIdc; ///< value of profile IDC (PRO_UNKNOWN for auto-detection)
+ ELevelIdc uiLevelIdc; ///< value of profile IDC (0 for auto-detection)
+ int iDLayerQp; ///< value of level IDC (0 for auto-detection)
+
+ SSliceArgument sSliceArgument;
+
+ // Note: members bVideoSignalTypePresent through uiColorMatrix below are also defined in SWelsSPS in parameter_sets.h.
+ bool bVideoSignalTypePresent; // false => do not write any of the following information to the header
+ unsigned char
+ uiVideoFormat; // EVideoFormatSPS; 3 bits in header; 0-5 => component, kpal, ntsc, secam, mac, undef
+ bool bFullRange; // false => analog video data range [16, 235]; true => full data range [0,255]
+ bool bColorDescriptionPresent; // false => do not write any of the following three items to the header
+ unsigned char
+ uiColorPrimaries; // EColorPrimaries; 8 bits in header; 0 - 9 => ???, bt709, undef, ???, bt470m, bt470bg,
+ // smpte170m, smpte240m, film, bt2020
+ unsigned char
+ uiTransferCharacteristics; // ETransferCharacteristics; 8 bits in header; 0 - 15 => ???, bt709, undef, ???, bt470m, bt470bg, smpte170m,
+ // smpte240m, linear, log100, log316, iec61966-2-4, bt1361e, iec61966-2-1, bt2020-10, bt2020-12
+ unsigned char
+ uiColorMatrix; // EColorMatrix; 8 bits in header (corresponds to FFmpeg "colorspace"); 0 - 10 => GBR, bt709,
+ // undef, ???, fcc, bt470bg, smpte170m, smpte240m, YCgCo, bt2020nc, bt2020c
+
+ bool bAspectRatioPresent; ///< aspect ratio present in VUI
+ ESampleAspectRatio eAspectRatio; ///< aspect ratio idc
+ unsigned short sAspectRatioExtWidth; ///< use if aspect ratio idc == 255
+ unsigned short sAspectRatioExtHeight; ///< use if aspect ratio idc == 255
+
+} SSpatialLayerConfig;
+
+/**
+* @brief Encoder usage type
+*/
+typedef enum {
+ CAMERA_VIDEO_REAL_TIME, ///< camera video for real-time communication
+ SCREEN_CONTENT_REAL_TIME, ///< screen content signal
+ CAMERA_VIDEO_NON_REAL_TIME,
+ SCREEN_CONTENT_NON_REAL_TIME,
+ INPUT_CONTENT_TYPE_ALL,
+} EUsageType;
+
+/**
+* @brief Enumulate the complexity mode
+*/
+typedef enum {
+ LOW_COMPLEXITY = 0, ///< the lowest compleixty,the fastest speed,
+ MEDIUM_COMPLEXITY, ///< medium complexity, medium speed,medium quality
+ HIGH_COMPLEXITY ///< high complexity, lowest speed, high quality
+} ECOMPLEXITY_MODE;
+
+/**
+ * @brief Enumulate for the stategy of SPS/PPS strategy
+ */
+typedef enum {
+ CONSTANT_ID = 0, ///< constant id in SPS/PPS
+ INCREASING_ID = 0x01, ///< SPS/PPS id increases at each IDR
+ SPS_LISTING = 0x02, ///< using SPS in the existing list if possible
+ SPS_LISTING_AND_PPS_INCREASING = 0x03,
+ SPS_PPS_LISTING = 0x06,
+} EParameterSetStrategy;
+
+// TODO: Refine the parameters definition.
+/**
+* @brief SVC Encoding Parameters
+*/
+typedef struct TagEncParamBase {
+ EUsageType
+ iUsageType; ///< application type; please refer to the definition of EUsageType
+
+ int iPicWidth; ///< width of picture in luminance samples (the maximum of all layers if multiple spatial layers presents)
+ int iPicHeight; ///< height of picture in luminance samples((the maximum of all layers if multiple spatial layers presents)
+ int iTargetBitrate; ///< target bitrate desired, in unit of bps
+ RC_MODES iRCMode; ///< rate control mode
+ float fMaxFrameRate; ///< maximal input frame rate
+
+} SEncParamBase, *PEncParamBase;
+
+/**
+* @brief SVC Encoding Parameters extention
+*/
+typedef struct TagEncParamExt {
+ EUsageType
+ iUsageType; ///< same as in TagEncParamBase
+
+ int iPicWidth; ///< same as in TagEncParamBase
+ int iPicHeight; ///< same as in TagEncParamBase
+ int iTargetBitrate; ///< same as in TagEncParamBase
+ RC_MODES iRCMode; ///< same as in TagEncParamBase
+ float fMaxFrameRate; ///< same as in TagEncParamBase
+
+ int iTemporalLayerNum; ///< temporal layer number, max temporal layer = 4
+ int iSpatialLayerNum; ///< spatial layer number,1<= iSpatialLayerNum <= MAX_SPATIAL_LAYER_NUM, MAX_SPATIAL_LAYER_NUM = 4
+ SSpatialLayerConfig sSpatialLayers[MAX_SPATIAL_LAYER_NUM];
+
+ ECOMPLEXITY_MODE iComplexityMode;
+ unsigned int uiIntraPeriod; ///< period of Intra frame
+ int iNumRefFrame; ///< number of reference frame used
+ EParameterSetStrategy
+ eSpsPpsIdStrategy; ///< different stategy in adjust ID in SPS/PPS: 0- constant ID, 1-additional ID, 6-mapping and additional
+ bool bPrefixNalAddingCtrl; ///< false:not use Prefix NAL; true: use Prefix NAL
+ bool bEnableSSEI; ///< false:not use SSEI; true: use SSEI -- TODO: planning to remove the interface of SSEI
+ bool bSimulcastAVC; ///< (when encoding more than 1 spatial layer) false: use SVC syntax for higher layers; true: use Simulcast AVC
+ int iPaddingFlag; ///< 0:disable padding;1:padding
+ int iEntropyCodingModeFlag; ///< 0:CAVLC 1:CABAC.
+
+ /* rc control */
+ bool bEnableFrameSkip; ///< False: don't skip frame even if VBV buffer overflow.True: allow skipping frames to keep the bitrate within limits
+ int iMaxBitrate; ///< the maximum bitrate, in unit of bps, set it to UNSPECIFIED_BIT_RATE if not needed
+ int iMaxQp; ///< the maximum QP encoder supports
+ int iMinQp; ///< the minmum QP encoder supports
+ unsigned int uiMaxNalSize; ///< the maximum NAL size. This value should be not 0 for dynamic slice mode
+
+ /*LTR settings*/
+ bool bEnableLongTermReference; ///< 1: on, 0: off
+ int iLTRRefNum; ///< the number of LTR(long term reference),TODO: not supported to set it arbitrary yet
+ unsigned int iLtrMarkPeriod; ///< the LTR marked period that is used in feedback.
+ /* multi-thread settings*/
+ unsigned short
+ iMultipleThreadIdc; ///< 1 # 0: auto(dynamic imp. internal encoder); 1: multiple threads imp. disabled; lager than 1: count number of threads;
+ bool bUseLoadBalancing; ///< only used when uiSliceMode=1 or 3, will change slicing of a picture during the run-time of multi-thread encoding, so the result of each run may be different
+
+ /* Deblocking loop filter */
+ int iLoopFilterDisableIdc; ///< 0: on, 1: off, 2: on except for slice boundaries
+ int iLoopFilterAlphaC0Offset; ///< AlphaOffset: valid range [-6, 6], default 0
+ int iLoopFilterBetaOffset; ///< BetaOffset: valid range [-6, 6], default 0
+ /*pre-processing feature*/
+ bool bEnableDenoise; ///< denoise control
+ bool bEnableBackgroundDetection; ///< background detection control //VAA_BACKGROUND_DETECTION //BGD cmd
+ bool bEnableAdaptiveQuant; ///< adaptive quantization control
+ bool bEnableFrameCroppingFlag; ///< enable frame cropping flag: TRUE always in application
+ bool bEnableSceneChangeDetect;
+
+ bool bIsLosslessLink; ///< LTR advanced setting
+ bool bFixRCOverShoot; ///< fix rate control overshooting
+ int iIdrBitrateRatio; ///< the target bits of IDR is (idr_bitrate_ratio/100) * average target bit per frame.
+} SEncParamExt;
+
+/**
+* @brief Define a new struct to show the property of video bitstream.
+*/
+typedef struct {
+ unsigned int size; ///< size of the struct
+ VIDEO_BITSTREAM_TYPE eVideoBsType; ///< video stream type (AVC/SVC)
+} SVideoProperty;
+
+/**
+* @brief SVC Decoding Parameters, reserved here and potential applicable in the future
+*/
+typedef struct TagSVCDecodingParam {
+ char* pFileNameRestructed; ///< file name of reconstructed frame used for PSNR calculation based debug
+
+ unsigned int uiCpuLoad; ///< CPU load
+ unsigned char uiTargetDqLayer; ///< setting target dq layer id
+
+ ERROR_CON_IDC eEcActiveIdc; ///< whether active error concealment feature in decoder
+ bool bParseOnly; ///< decoder for parse only, no reconstruction. When it is true, SPS/PPS size should not exceed SPS_PPS_BS_SIZE (128). Otherwise, it will return error info
+
+ SVideoProperty sVideoProperty; ///< video stream property
+} SDecodingParam, *PDecodingParam;
+
+/**
+* @brief Bitstream inforamtion of a layer being encoded
+*/
+typedef struct {
+ unsigned char uiTemporalId;
+ unsigned char uiSpatialId;
+ unsigned char uiQualityId;
+ EVideoFrameType eFrameType;
+ unsigned char uiLayerType;
+
+ /**
+ * The sub sequence layers are ordered hierarchically based on their dependency on each other so that any picture in a layer shall not be
+ * predicted from any picture on any higher layer.
+ */
+ int iSubSeqId; ///< refer to D.2.11 Sub-sequence information SEI message semantics
+ int iNalCount; ///< count number of NAL coded already
+ int* pNalLengthInByte; ///< length of NAL size in byte from 0 to iNalCount-1
+ unsigned char* pBsBuf; ///< buffer of bitstream contained
+} SLayerBSInfo, *PLayerBSInfo;
+
+/**
+* @brief Frame bit stream info
+*/
+typedef struct {
+ int iLayerNum;
+ SLayerBSInfo sLayerInfo[MAX_LAYER_NUM_OF_FRAME];
+
+ EVideoFrameType eFrameType;
+ int iFrameSizeInBytes;
+ long long uiTimeStamp;
+} SFrameBSInfo, *PFrameBSInfo;
+
+/**
+* @brief Structure for source picture
+*/
+typedef struct Source_Picture_s {
+ int iColorFormat; ///< color space type
+ int iStride[4]; ///< stride for each plane pData
+ unsigned char* pData[4]; ///< plane pData
+ int iPicWidth; ///< luma picture width in x coordinate
+ int iPicHeight; ///< luma picture height in y coordinate
+ long long uiTimeStamp; ///< timestamp of the source picture, unit: millisecond
+} SSourcePicture;
+/**
+* @brief Structure for bit rate info
+*/
+typedef struct TagBitrateInfo {
+ LAYER_NUM iLayer;
+ int iBitrate; ///< the maximum bitrate
+} SBitrateInfo;
+
+/**
+* @brief Structure for dump layer info
+*/
+typedef struct TagDumpLayer {
+ int iLayer;
+ char* pFileName;
+} SDumpLayer;
+
+/**
+* @brief Structure for profile info in layer
+*
+*/
+typedef struct TagProfileInfo {
+ int iLayer;
+ EProfileIdc uiProfileIdc; ///< the profile info
+} SProfileInfo;
+
+/**
+* @brief Structure for level info in layer
+*
+*/
+typedef struct TagLevelInfo {
+ int iLayer;
+ ELevelIdc uiLevelIdc; ///< the level info
+} SLevelInfo;
+/**
+* @brief Structure for dilivery status
+*
+*/
+typedef struct TagDeliveryStatus {
+ bool bDeliveryFlag; ///< 0: the previous frame isn't delivered,1: the previous frame is delivered
+ int iDropFrameType; ///< the frame type that is dropped; reserved
+ int iDropFrameSize; ///< the frame size that is dropped; reserved
+} SDeliveryStatus;
+
+/**
+* @brief The capability of decoder, for SDP negotiation
+*/
+typedef struct TagDecoderCapability {
+ int iProfileIdc; ///< profile_idc
+ int iProfileIop; ///< profile-iop
+ int iLevelIdc; ///< level_idc
+ int iMaxMbps; ///< max-mbps
+ int iMaxFs; ///< max-fs
+ int iMaxCpb; ///< max-cpb
+ int iMaxDpb; ///< max-dpb
+ int iMaxBr; ///< max-br
+ bool bRedPicCap; ///< redundant-pic-cap
+} SDecoderCapability;
+
+/**
+* @brief Structure for parse only output
+*/
+typedef struct TagParserBsInfo {
+ int iNalNum; ///< total NAL number in current AU
+ int* pNalLenInByte; ///< each nal length
+ unsigned char* pDstBuff; ///< outputted dst buffer for parsed bitstream
+ int iSpsWidthInPixel; ///< required SPS width info
+ int iSpsHeightInPixel; ///< required SPS height info
+ unsigned long long uiInBsTimeStamp; ///< input BS timestamp
+ unsigned long long uiOutBsTimeStamp; ///< output BS timestamp
+} SParserBsInfo, *PParserBsInfo;
+
+/**
+* @brief Structure for encoder statistics
+*/
+typedef struct TagVideoEncoderStatistics {
+ unsigned int uiWidth; ///< the width of encoded frame
+ unsigned int uiHeight; ///< the height of encoded frame
+ //following standard, will be 16x aligned, if there are multiple spatial, this is of the highest
+ float fAverageFrameSpeedInMs; ///< average_Encoding_Time
+
+ // rate control related
+ float fAverageFrameRate; ///< the average frame rate in, calculate since encoding starts, supposed that the input timestamp is in unit of ms
+ float fLatestFrameRate; ///< the frame rate in, in the last second, supposed that the input timestamp is in unit of ms (? useful for checking BR, but is it easy to calculate?
+ unsigned int uiBitRate; ///< sendrate in Bits per second, calculated within the set time-window
+ unsigned int uiAverageFrameQP; ///< the average QP of last encoded frame
+
+ unsigned int uiInputFrameCount; ///< number of frames
+ unsigned int uiSkippedFrameCount; ///< number of frames
+
+ unsigned int uiResolutionChangeTimes; ///< uiResolutionChangeTimes
+ unsigned int uiIDRReqNum; ///< number of IDR requests
+ unsigned int uiIDRSentNum; ///< number of actual IDRs sent
+ unsigned int uiLTRSentNum; ///< number of LTR sent/marked
+
+ long long iStatisticsTs; ///< Timestamp of updating the statistics
+
+ unsigned long iTotalEncodedBytes;
+ unsigned long iLastStatisticsBytes;
+ unsigned long iLastStatisticsFrameCount;
+} SEncoderStatistics;
+
+/**
+* @brief Structure for decoder statistics
+*/
+typedef struct TagVideoDecoderStatistics {
+ unsigned int uiWidth; ///< the width of encode/decode frame
+ unsigned int uiHeight; ///< the height of encode/decode frame
+ float fAverageFrameSpeedInMs; ///< average_Decoding_Time
+ float fActualAverageFrameSpeedInMs; ///< actual average_Decoding_Time, including freezing pictures
+ unsigned int uiDecodedFrameCount; ///< number of frames
+ unsigned int uiResolutionChangeTimes; ///< uiResolutionChangeTimes
+ unsigned int uiIDRCorrectNum; ///< number of correct IDR received
+ //EC on related
+ unsigned int
+ uiAvgEcRatio; ///< when EC is on, the average ratio of total EC areas, can be an indicator of reconstruction quality
+ unsigned int
+ uiAvgEcPropRatio; ///< when EC is on, the rough average ratio of propogate EC areas, can be an indicator of reconstruction quality
+ unsigned int uiEcIDRNum; ///< number of actual unintegrity IDR or not received but eced
+ unsigned int uiEcFrameNum; ///<
+ unsigned int uiIDRLostNum; ///< number of whole lost IDR
+ unsigned int
+ uiFreezingIDRNum; ///< number of freezing IDR with error (partly received), under resolution change
+ unsigned int uiFreezingNonIDRNum; ///< number of freezing non-IDR with error
+ int iAvgLumaQp; ///< average luma QP. default: -1, no correct frame outputted
+ int iSpsReportErrorNum; ///< number of Sps Invalid report
+ int iSubSpsReportErrorNum; ///< number of SubSps Invalid report
+ int iPpsReportErrorNum; ///< number of Pps Invalid report
+ int iSpsNoExistNalNum; ///< number of Sps NoExist Nal
+ int iSubSpsNoExistNalNum; ///< number of SubSps NoExist Nal
+ int iPpsNoExistNalNum; ///< number of Pps NoExist Nal
+
+ unsigned int uiProfile; ///< Profile idc in syntax
+ unsigned int uiLevel; ///< level idc according to Annex A-1
+
+ int iCurrentActiveSpsId; ///< current active SPS id
+ int iCurrentActivePpsId; ///< current active PPS id
+
+ unsigned int iStatisticsLogInterval; ///< frame interval of statistics log
+} SDecoderStatistics; // in building, coming soon
+
+/**
+* @brief Structure for sample aspect ratio (SAR) info in VUI
+*/
+typedef struct TagVuiSarInfo {
+ unsigned int uiSarWidth; ///< SAR width
+ unsigned int uiSarHeight; ///< SAR height
+ bool bOverscanAppropriateFlag; ///< SAR overscan flag
+} SVuiSarInfo, *PVuiSarInfo;
+
+#endif//WELS_VIDEO_CODEC_APPLICATION_DEFINITION_H__
diff --git a/third_party/openh264/include/codec_def.h b/third_party/openh264/include/codec_def.h
new file mode 100644
index 0000000..edde5f4
--- /dev/null
+++ b/third_party/openh264/include/codec_def.h
@@ -0,0 +1,216 @@
+/*!
+ * \copy
+ * Copyright (c) 2013, Cisco Systems
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+#ifndef WELS_VIDEO_CODEC_DEFINITION_H__
+#define WELS_VIDEO_CODEC_DEFINITION_H__
+
+/**
+ * @file codec_def.h
+*/
+
+/**
+* @brief Enumerate the type of video format
+*/
+typedef enum {
+ videoFormatRGB = 1, ///< rgb color formats
+ videoFormatRGBA = 2,
+ videoFormatRGB555 = 3,
+ videoFormatRGB565 = 4,
+ videoFormatBGR = 5,
+ videoFormatBGRA = 6,
+ videoFormatABGR = 7,
+ videoFormatARGB = 8,
+
+ videoFormatYUY2 = 20, ///< yuv color formats
+ videoFormatYVYU = 21,
+ videoFormatUYVY = 22,
+ videoFormatI420 = 23, ///< the same as IYUV
+ videoFormatYV12 = 24,
+ videoFormatInternal = 25, ///< only used in SVC decoder testbed
+
+ videoFormatNV12 = 26, ///< new format for output by DXVA decoding
+
+ videoFormatVFlip = 0x80000000
+} EVideoFormatType;
+
+/**
+* @brief Enumerate video frame type
+*/
+typedef enum {
+ videoFrameTypeInvalid, ///< encoder not ready or parameters are invalidate
+ videoFrameTypeIDR, ///< IDR frame in H.264
+ videoFrameTypeI, ///< I frame type
+ videoFrameTypeP, ///< P frame type
+ videoFrameTypeSkip, ///< skip the frame based encoder kernel
+ videoFrameTypeIPMixed ///< a frame where I and P slices are mixing, not supported yet
+} EVideoFrameType;
+
+/**
+* @brief Enumerate return type
+*/
+typedef enum {
+ cmResultSuccess, ///< successful
+ cmInitParaError, ///< parameters are invalid
+ cmUnknownReason,
+ cmMallocMemeError, ///< malloc a memory error
+ cmInitExpected, ///< initial action is expected
+ cmUnsupportedData
+} CM_RETURN;
+
+/**
+* @brief Enumulate the nal unit type
+*/
+enum ENalUnitType {
+ NAL_UNKNOWN = 0,
+ NAL_SLICE = 1,
+ NAL_SLICE_DPA = 2,
+ NAL_SLICE_DPB = 3,
+ NAL_SLICE_DPC = 4,
+ NAL_SLICE_IDR = 5, ///< ref_idc != 0
+ NAL_SEI = 6, ///< ref_idc == 0
+ NAL_SPS = 7,
+ NAL_PPS = 8
+ ///< ref_idc == 0 for 6,9,10,11,12
+};
+
+/**
+* @brief NRI: eNalRefIdc
+*/
+enum ENalPriority {
+ NAL_PRIORITY_DISPOSABLE = 0,
+ NAL_PRIORITY_LOW = 1,
+ NAL_PRIORITY_HIGH = 2,
+ NAL_PRIORITY_HIGHEST = 3
+};
+
+#define IS_PARAMETER_SET_NAL(eNalRefIdc, eNalType) \
+( (eNalRefIdc == NAL_PRIORITY_HIGHEST) && (eNalType == (NAL_SPS|NAL_PPS) || eNalType == NAL_SPS) )
+
+#define IS_IDR_NAL(eNalRefIdc, eNalType) \
+( (eNalRefIdc == NAL_PRIORITY_HIGHEST) && (eNalType == NAL_SLICE_IDR) )
+
+#define FRAME_NUM_PARAM_SET (-1)
+#define FRAME_NUM_IDR 0
+
+/**
+ * @brief eDeblockingIdc
+ */
+enum {
+ DEBLOCKING_IDC_0 = 0,
+ DEBLOCKING_IDC_1 = 1,
+ DEBLOCKING_IDC_2 = 2
+};
+#define DEBLOCKING_OFFSET (6)
+#define DEBLOCKING_OFFSET_MINUS (-6)
+
+/* Error Tools definition */
+typedef unsigned short ERR_TOOL;
+
+/**
+ @brief to do
+*/
+enum {
+ ET_NONE = 0x00, ///< NONE Error Tools
+ ET_IP_SCALE = 0x01, ///< IP Scalable
+ ET_FMO = 0x02, ///< Flexible Macroblock Ordering
+ ET_IR_R1 = 0x04, ///< Intra Refresh in predifined 2% MB
+ ET_IR_R2 = 0x08, ///< Intra Refresh in predifined 5% MB
+ ET_IR_R3 = 0x10, ///< Intra Refresh in predifined 10% MB
+ ET_FEC_HALF = 0x20, ///< Forward Error Correction in 50% redundency mode
+ ET_FEC_FULL = 0x40, ///< Forward Error Correction in 100% redundency mode
+ ET_RFS = 0x80 ///< Reference Frame Selection
+};
+
+/**
+* @brief Information of coded Slice(=NAL)(s)
+*/
+typedef struct SliceInformation {
+ unsigned char* pBufferOfSlices; ///< base buffer of coded slice(s)
+ int iCodedSliceCount; ///< number of coded slices
+ unsigned int* pLengthOfSlices; ///< array of slices length accordingly by number of slice
+ int iFecType; ///< FEC type[0, 50%FEC, 100%FEC]
+ unsigned char uiSliceIdx; ///< index of slice in frame [FMO: 0,..,uiSliceCount-1; No FMO: 0]
+ unsigned char uiSliceCount; ///< count number of slice in frame [FMO: 2-8; No FMO: 1]
+ char iFrameIndex; ///< index of frame[-1, .., idr_interval-1]
+ unsigned char uiNalRefIdc; ///< NRI, priority level of slice(NAL)
+ unsigned char uiNalType; ///< NAL type
+ unsigned char
+ uiContainingFinalNal; ///< whether final NAL is involved in buffer of coded slices, flag used in Pause feature in T27
+} SliceInfo, *PSliceInfo;
+
+/**
+* @brief thresholds of the initial, maximal and minimal rate
+*/
+typedef struct {
+ int iWidth; ///< frame width
+ int iHeight; ///< frame height
+ int iThresholdOfInitRate; ///< threshold of initial rate
+ int iThresholdOfMaxRate; ///< threshold of maximal rate
+ int iThresholdOfMinRate; ///< threshold of minimal rate
+ int iMinThresholdFrameRate; ///< min frame rate min
+ int iSkipFrameRate; ///< skip to frame rate min
+ int iSkipFrameStep; ///< how many frames to skip
+} SRateThresholds, *PRateThresholds;
+
+/**
+* @brief Structure for decoder memery
+*/
+typedef struct TagSysMemBuffer {
+ int iWidth; ///< width of decoded pic for display
+ int iHeight; ///< height of decoded pic for display
+ int iFormat; ///< type is "EVideoFormatType"
+ int iStride[2]; ///< stride of 2 component
+} SSysMEMBuffer;
+
+/**
+* @brief Buffer info
+*/
+typedef struct TagBufferInfo {
+ int iBufferStatus; ///< 0: one frame data is not ready; 1: one frame data is ready
+ unsigned long long uiInBsTimeStamp; ///< input BS timestamp
+ unsigned long long uiOutYuvTimeStamp; ///< output YUV timestamp, when bufferstatus is 1
+ union {
+ SSysMEMBuffer sSystemBuffer; ///< memory info for one picture
+ } UsrData; ///< output buffer info
+ unsigned char* pDst[3]; //point to picture YUV data
+} SBufferInfo;
+
+
+/**
+* @brief In a GOP, multiple of the key frame number, derived from
+* the number of layers(index or array below)
+*/
+static const char kiKeyNumMultiple[] = {
+ 1, 1, 2, 4, 8, 16,
+};
+
+#endif//WELS_VIDEO_CODEC_DEFINITION_H__
diff --git a/third_party/openh264/include/codec_ver.h b/third_party/openh264/include/codec_ver.h
new file mode 100644
index 0000000..0944109
--- /dev/null
+++ b/third_party/openh264/include/codec_ver.h
@@ -0,0 +1,15 @@
+//The current file is auto-generated by script: generate_codec_ver.sh
+#ifndef CODEC_VER_H
+#define CODEC_VER_H
+
+#include "codec_app_def.h"
+
+static const OpenH264Version g_stCodecVersion = {2, 3, 0, 2206};
+static const char* const g_strCodecVer = "OpenH264 version:2.3.0.2206";
+
+#define OPENH264_MAJOR (2)
+#define OPENH264_MINOR (3)
+#define OPENH264_REVISION (0)
+#define OPENH264_RESERVED (2206)
+
+#endif // CODEC_VER_H
diff --git a/third_party/web_platform_tests/resources/testharness.js b/third_party/web_platform_tests/resources/testharness.js
index 83bb012..fe2eea3 100644
--- a/third_party/web_platform_tests/resources/testharness.js
+++ b/third_party/web_platform_tests/resources/testharness.js
@@ -197,6 +197,13 @@
add_result_callback(function (test) {
this_obj.output_handler.show_status();
});
+ // Only show test summary when browsing tests in Chrome.
+ // Cobalt doesn't support html elements at this moment.
+ if (navigator.userAgent.includes("Chrome")) {
+ add_completion_callback(function (tests, harness_status) {
+ this_obj.output_handler.show_results(tests, harness_status);
+ });
+ }
this.setup_messages(settings.message_events);
};
diff --git a/third_party/web_platform_tests/service-workers/META.yml b/third_party/web_platform_tests/service-workers/META.yml
new file mode 100644
index 0000000..03a0dd0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/META.yml
@@ -0,0 +1,6 @@
+spec: https://w3c.github.io/ServiceWorker/
+suggested_reviewers:
+ - asutherland
+ - mkruisselbrink
+ - mattto
+ - wanderview
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/META.yml b/third_party/web_platform_tests/service-workers/cache-storage/META.yml
new file mode 100644
index 0000000..bf34474
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/META.yml
@@ -0,0 +1,3 @@
+suggested_reviewers:
+ - inexorabletash
+ - wanderview
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-abort.https.any.js b/third_party/web_platform_tests/service-workers/cache-storage/cache-abort.https.any.js
new file mode 100644
index 0000000..960d1bb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-abort.https.any.js
@@ -0,0 +1,81 @@
+// META: title=Cache Storage: Abort
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: script=/common/utils.js
+// META: timeout=long
+
+// We perform the same tests on put, add, addAll. Parameterise the tests to
+// reduce repetition.
+const methodsToTest = {
+ put: async (cache, request) => {
+ const response = await fetch(request);
+ return cache.put(request, response);
+ },
+ add: async (cache, request) => cache.add(request),
+ addAll: async (cache, request) => cache.addAll([request]),
+};
+
+for (const method in methodsToTest) {
+ const perform = methodsToTest[method];
+
+ cache_test(async (cache, test) => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+ const request = new Request('../resources/simple.txt', { signal });
+ return promise_rejects_dom(test, 'AbortError', perform(cache, request),
+ `${method} should reject`);
+ }, `${method}() on an already-aborted request should reject with AbortError`);
+
+ cache_test(async (cache, test) => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request('../resources/simple.txt', { signal });
+ const promise = perform(cache, request);
+ controller.abort();
+ return promise_rejects_dom(test, 'AbortError', promise,
+ `${method} should reject`);
+ }, `${method}() synchronously followed by abort should reject with ` +
+ `AbortError`);
+
+ cache_test(async (cache, test) => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ const request = new Request(
+ `../../../fetch/api/resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`,
+ { signal });
+
+ const promise = perform(cache, request);
+
+ // Wait for the server to start sending the response body.
+ let opened = false;
+ do {
+ // Normally only one fetch to 'stash-take' is needed, but the fetches
+ // will be served in reverse order sometimes
+ // (i.e., 'stash-take' gets served before 'infinite-slow-response').
+
+ const response =
+ await fetch(`../../../fetch/api/resources/stash-take.py?key=${stateKey}`);
+ const body = await response.json();
+ if (body === 'open') opened = true;
+ } while (!opened);
+
+ // Sadly the above loop cannot guarantee that the browser has started
+ // processing the response body. This delay is needed to make the test
+ // failures non-flaky in Chrome version 66. My deepest apologies.
+ await new Promise(resolve => setTimeout(resolve, 250));
+
+ controller.abort();
+
+ await promise_rejects_dom(test, 'AbortError', promise,
+ `${method} should reject`);
+
+ // infinite-slow-response.py doesn't know when to stop.
+ return fetch(`../../../fetch/api/resources/stash-put.py?key=${abortKey}`);
+ }, `${method}() followed by abort after headers received should reject ` +
+ `with AbortError`);
+}
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-add.https.any.js b/third_party/web_platform_tests/service-workers/cache-storage/cache-add.https.any.js
new file mode 100644
index 0000000..eca516a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-add.https.any.js
@@ -0,0 +1,368 @@
+// META: title=Cache.add and Cache.addAll
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+const { REMOTE_HOST } = get_host_info();
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add(),
+ 'Cache.add should throw a TypeError when no arguments are given.');
+ }, 'Cache.add called with no arguments');
+
+cache_test(function(cache) {
+ return cache.add('./resources/simple.txt')
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.add should resolve with undefined on success.');
+ return cache.match('./resources/simple.txt');
+ })
+ .then(function(response) {
+ assert_class_string(response, 'Response',
+ 'Cache.add should put a resource in the cache.');
+ return response.text();
+ })
+ .then(function(body) {
+ assert_equals(body, 'a simple text file\n',
+ 'Cache.add should retrieve the correct body.');
+ });
+ }, 'Cache.add called with relative URL specified as a string');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add('javascript://this-is-not-http-mmkay'),
+ 'Cache.add should throw a TypeError for non-HTTP/HTTPS URLs.');
+ }, 'Cache.add called with non-HTTP/HTTPS URL');
+
+cache_test(function(cache) {
+ var request = new Request('./resources/simple.txt');
+ return cache.add(request)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.add should resolve with undefined on success.');
+ });
+ }, 'Cache.add called with Request object');
+
+cache_test(function(cache, test) {
+ var request = new Request('./resources/simple.txt',
+ {method: 'POST', body: 'This is a body.'});
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add(request),
+ 'Cache.add should throw a TypeError for non-GET requests.');
+ }, 'Cache.add called with POST request');
+
+cache_test(function(cache) {
+ var request = new Request('./resources/simple.txt');
+ return cache.add(request)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.add should resolve with undefined on success.');
+ })
+ .then(function() {
+ return cache.add(request);
+ })
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.add should resolve with undefined on success.');
+ });
+ }, 'Cache.add called twice with the same Request object');
+
+cache_test(function(cache) {
+ var request = new Request('./resources/simple.txt');
+ return request.text()
+ .then(function() {
+ assert_false(request.bodyUsed);
+ })
+ .then(function() {
+ return cache.add(request);
+ });
+ }, 'Cache.add with request with null body (not consumed)');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add('./resources/fetch-status.py?status=206'),
+ 'Cache.add should reject on partial response');
+ }, 'Cache.add with 206 response');
+
+cache_test(function(cache, test) {
+ var urls = ['./resources/fetch-status.py?status=206',
+ './resources/fetch-status.py?status=200'];
+ var requests = urls.map(function(url) {
+ return new Request(url);
+ });
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.addAll(requests),
+ 'Cache.addAll should reject with TypeError if any request fails');
+ }, 'Cache.addAll with 206 response');
+
+cache_test(function(cache, test) {
+ var urls = ['./resources/fetch-status.py?status=206',
+ './resources/fetch-status.py?status=200'];
+ var requests = urls.map(function(url) {
+ var cross_origin_url = new URL(url, location.href);
+ cross_origin_url.hostname = REMOTE_HOST;
+ return new Request(cross_origin_url.href, { mode: 'no-cors' });
+ });
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.addAll(requests),
+ 'Cache.addAll should reject with TypeError if any request fails');
+ }, 'Cache.addAll with opaque-filtered 206 response');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add('this-does-not-exist-please-dont-create-it'),
+ 'Cache.add should reject if response is !ok');
+ }, 'Cache.add with request that results in a status of 404');
+
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.add('./resources/fetch-status.py?status=500'),
+ 'Cache.add should reject if response is !ok');
+ }, 'Cache.add with request that results in a status of 500');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.addAll(),
+ 'Cache.addAll with no arguments should throw TypeError.');
+ }, 'Cache.addAll with no arguments');
+
+cache_test(function(cache, test) {
+ // Assumes the existence of ../resources/simple.txt and ../resources/blank.html
+ var urls = ['./resources/simple.txt', undefined, './resources/blank.html'];
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.addAll(urls),
+ 'Cache.addAll should throw TypeError for an undefined argument.');
+ }, 'Cache.addAll with a mix of valid and undefined arguments');
+
+cache_test(function(cache) {
+ return cache.addAll([])
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.addAll should resolve with undefined on ' +
+ 'success.');
+ return cache.keys();
+ })
+ .then(function(result) {
+ assert_equals(result.length, 0,
+ 'There should be no entry in the cache.');
+ });
+ }, 'Cache.addAll with an empty array');
+
+cache_test(function(cache) {
+ // Assumes the existence of ../resources/simple.txt and
+ // ../resources/blank.html
+ var urls = ['./resources/simple.txt',
+ self.location.href,
+ './resources/blank.html'];
+ return cache.addAll(urls)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.addAll should resolve with undefined on ' +
+ 'success.');
+ return Promise.all(
+ urls.map(function(url) { return cache.match(url); }));
+ })
+ .then(function(responses) {
+ assert_class_string(
+ responses[0], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ assert_class_string(
+ responses[1], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ assert_class_string(
+ responses[2], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ return Promise.all(
+ responses.map(function(response) { return response.text(); }));
+ })
+ .then(function(bodies) {
+ assert_equals(
+ bodies[0], 'a simple text file\n',
+ 'Cache.add should retrieve the correct body.');
+ assert_equals(
+ bodies[2], '<!DOCTYPE html>\n<title>Empty doc</title>\n',
+ 'Cache.add should retrieve the correct body.');
+ });
+ }, 'Cache.addAll with string URL arguments');
+
+cache_test(function(cache) {
+ // Assumes the existence of ../resources/simple.txt and
+ // ../resources/blank.html
+ var urls = ['./resources/simple.txt',
+ self.location.href,
+ './resources/blank.html'];
+ var requests = urls.map(function(url) {
+ return new Request(url);
+ });
+ return cache.addAll(requests)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.addAll should resolve with undefined on ' +
+ 'success.');
+ return Promise.all(
+ urls.map(function(url) { return cache.match(url); }));
+ })
+ .then(function(responses) {
+ assert_class_string(
+ responses[0], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ assert_class_string(
+ responses[1], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ assert_class_string(
+ responses[2], 'Response',
+ 'Cache.addAll should put a resource in the cache.');
+ return Promise.all(
+ responses.map(function(response) { return response.text(); }));
+ })
+ .then(function(bodies) {
+ assert_equals(
+ bodies[0], 'a simple text file\n',
+ 'Cache.add should retrieve the correct body.');
+ assert_equals(
+ bodies[2], '<!DOCTYPE html>\n<title>Empty doc</title>\n',
+ 'Cache.add should retrieve the correct body.');
+ });
+ }, 'Cache.addAll with Request arguments');
+
+cache_test(function(cache, test) {
+ // Assumes that ../resources/simple.txt and ../resources/blank.html exist.
+ // The second resource does not.
+ var urls = ['./resources/simple.txt',
+ 'this-resource-should-not-exist',
+ './resources/blank.html'];
+ var requests = urls.map(function(url) {
+ return new Request(url);
+ });
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.addAll(requests),
+ 'Cache.addAll should reject with TypeError if any request fails')
+ .then(function() {
+ return Promise.all(urls.map(function(url) {
+ return cache.match(url);
+ }));
+ })
+ .then(function(matches) {
+ assert_array_equals(
+ matches,
+ [undefined, undefined, undefined],
+ 'If any response fails, no response should be added to cache');
+ });
+ }, 'Cache.addAll with a mix of succeeding and failing requests');
+
+cache_test(function(cache, test) {
+ var request = new Request('../resources/simple.txt');
+ return promise_rejects_dom(
+ test,
+ 'InvalidStateError',
+ cache.addAll([request, request]),
+ 'Cache.addAll should throw InvalidStateError if the same request is added ' +
+ 'twice.');
+ }, 'Cache.addAll called with the same Request object specified twice');
+
+cache_test(async function(cache, test) {
+ const url = './resources/vary.py?vary=x-shape';
+ let requests = [
+ new Request(url, { headers: { 'x-shape': 'circle' }}),
+ new Request(url, { headers: { 'x-shape': 'square' }}),
+ ];
+ let result = await cache.addAll(requests);
+ assert_equals(result, undefined, 'Cache.addAll() should succeed');
+ }, 'Cache.addAll should succeed when entries differ by vary header');
+
+cache_test(async function(cache, test) {
+ const url = './resources/vary.py?vary=x-shape';
+ let requests = [
+ new Request(url, { headers: { 'x-shape': 'circle' }}),
+ new Request(url, { headers: { 'x-shape': 'circle' }}),
+ ];
+ await promise_rejects_dom(
+ test,
+ 'InvalidStateError',
+ cache.addAll(requests),
+ 'Cache.addAll() should reject when entries are duplicate by vary header');
+ }, 'Cache.addAll should reject when entries are duplicate by vary header');
+
+// VARY header matching is asymmetric. Determining if two entries are duplicate
+// depends on which entry's response is used in the comparison. The target
+// response's VARY header determines what request headers are examined. This
+// test verifies that Cache.addAll() duplicate checking handles this asymmetric
+// behavior correctly.
+cache_test(async function(cache, test) {
+ const base_url = './resources/vary.py';
+
+ // Define a request URL that sets a VARY header in the
+ // query string to be echoed back by the server.
+ const url = base_url + '?vary=x-size';
+
+ // Set a cookie to override the VARY header of the response
+ // when the request is made with credentials. This will
+ // take precedence over the query string vary param. This
+ // is a bit confusing, but it's necessary to construct a test
+ // where the URL is the same, but the VARY headers differ.
+ //
+ // Note, the test could also pass this information in additional
+ // request headers. If the cookie approach becomes too unwieldy
+ // this test could be rewritten to use that technique.
+ await fetch(base_url + '?set-vary-value-override-cookie=x-shape');
+ test.add_cleanup(_ => fetch(base_url + '?clear-vary-value-override-cookie'));
+
+ let requests = [
+ // This request will result in a Response with a "Vary: x-shape"
+ // header. This *will not* result in a duplicate match with the
+ // other entry.
+ new Request(url, { headers: { 'x-shape': 'circle',
+ 'x-size': 'big' },
+ credentials: 'same-origin' }),
+
+ // This request will result in a Response with a "Vary: x-size"
+ // header. This *will* result in a duplicate match with the other
+ // entry.
+ new Request(url, { headers: { 'x-shape': 'square',
+ 'x-size': 'big' },
+ credentials: 'omit' }),
+ ];
+ await promise_rejects_dom(
+ test,
+ 'InvalidStateError',
+ cache.addAll(requests),
+ 'Cache.addAll() should reject when one entry has a vary header ' +
+ 'matching an earlier entry.');
+
+ // Test the reverse order now.
+ await promise_rejects_dom(
+ test,
+ 'InvalidStateError',
+ cache.addAll(requests.reverse()),
+ 'Cache.addAll() should reject when one entry has a vary header ' +
+ 'matching a later entry.');
+
+ }, 'Cache.addAll should reject when one entry has a vary header ' +
+ 'matching another entry');
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-delete.https.any.js b/third_party/web_platform_tests/service-workers/cache-storage/cache-delete.https.any.js
new file mode 100644
index 0000000..3eae2b6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-delete.https.any.js
@@ -0,0 +1,164 @@
+// META: title=Cache.delete
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+var test_url = 'https://example.com/foo';
+
+// Construct a generic Request object. The URL is |test_url|. All other fields
+// are defaults.
+function new_test_request() {
+ return new Request(test_url);
+}
+
+// Construct a generic Response object.
+function new_test_response() {
+ return new Response('Hello world!', { status: 200 });
+}
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.delete(),
+ 'Cache.delete should reject with a TypeError when called with no ' +
+ 'arguments.');
+ }, 'Cache.delete with no arguments');
+
+cache_test(function(cache) {
+ return cache.put(new_test_request(), new_test_response())
+ .then(function() {
+ return cache.delete(test_url);
+ })
+ .then(function(result) {
+ assert_true(result,
+ 'Cache.delete should resolve with "true" if an entry ' +
+ 'was successfully deleted.');
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.delete should remove matching entries from cache.');
+ });
+ }, 'Cache.delete called with a string URL');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ return cache.put(request, new_test_response())
+ .then(function() {
+ return cache.delete(request);
+ })
+ .then(function(result) {
+ assert_true(result,
+ 'Cache.delete should resolve with "true" if an entry ' +
+ 'was successfully deleted.');
+ });
+ }, 'Cache.delete called with a Request object');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new_test_response();
+ return cache.put(request, response)
+ .then(function() {
+ return cache.delete(new Request(test_url, {method: 'HEAD'}));
+ })
+ .then(function(result) {
+ assert_false(result,
+ 'Cache.delete should not match a non-GET request ' +
+ 'unless ignoreMethod option is set.');
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, response,
+ 'Cache.delete should leave non-matching response in the cache.');
+ return cache.delete(new Request(test_url, {method: 'HEAD'}),
+ {ignoreMethod: true});
+ })
+ .then(function(result) {
+ assert_true(result,
+ 'Cache.delete should match a non-GET request ' +
+ ' if ignoreMethod is true.');
+ });
+ }, 'Cache.delete called with a HEAD request');
+
+cache_test(function(cache) {
+ var vary_request = new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}});
+ var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+ var mismatched_vary_request = new Request('http://example.com/c');
+
+ return cache.put(vary_request.clone(), vary_response.clone())
+ .then(function() {
+ return cache.delete(mismatched_vary_request.clone());
+ })
+ .then(function(result) {
+ assert_false(result,
+ 'Cache.delete should not delete if vary does not ' +
+ 'match unless ignoreVary is true');
+ return cache.delete(mismatched_vary_request.clone(),
+ {ignoreVary: true});
+ })
+ .then(function(result) {
+ assert_true(result,
+ 'Cache.delete should ignore vary if ignoreVary is true');
+ });
+ }, 'Cache.delete supports ignoreVary');
+
+cache_test(function(cache) {
+ return cache.delete(test_url)
+ .then(function(result) {
+ assert_false(result,
+ 'Cache.delete should resolve with "false" if there ' +
+ 'are no matching entries.');
+ });
+ }, 'Cache.delete with a non-existent entry');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a_with_query.request,
+ { ignoreSearch: true })
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ]);
+ return cache.delete(entries.a_with_query.request,
+ { ignoreSearch: true });
+ })
+ .then(function(result) {
+ return cache.matchAll(entries.a_with_query.request,
+ { ignoreSearch: true });
+ })
+ .then(function(result) {
+ assert_response_array_equals(result, []);
+ });
+ },
+ 'Cache.delete with ignoreSearch option (request with search parameters)');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a_with_query.request,
+ { ignoreSearch: true })
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ]);
+ // cache.delete()'s behavior should be the same if ignoreSearch is
+ // not provided or if ignoreSearch is false.
+ return cache.delete(entries.a_with_query.request,
+ { ignoreSearch: false });
+ })
+ .then(function(result) {
+ return cache.matchAll(entries.a_with_query.request,
+ { ignoreSearch: true });
+ })
+ .then(function(result) {
+ assert_response_array_equals(result, [ entries.a.response ]);
+ });
+ },
+ 'Cache.delete with ignoreSearch option (when it is specified as false)');
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html b/third_party/web_platform_tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html
new file mode 100644
index 0000000..3c96348
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<title>Cache.keys (checking request attributes that can be set only on service workers)</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-keys">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./../service-worker/resources/test-helpers.sub.js"></script>
+<script>
+const worker = './resources/cache-keys-attributes-for-service-worker.js';
+
+function wait(ms) {
+ return new Promise(resolve => step_timeout(resolve, ms));
+}
+
+promise_test(async (t) => {
+ const scope = './resources/blank.html?name=isReloadNavigation';
+ let frame;
+ let reg;
+
+ try {
+ reg = await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, reg.installing, 'activated');
+ frame = await with_iframe(scope);
+ assert_equals(frame.contentDocument.body.textContent,
+ 'original: false, stored: false');
+ await new Promise((resolve) => {
+ frame.onload = resolve;
+ frame.contentWindow.location.reload();
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'original: true, stored: true');
+ } finally {
+ if (frame) {
+ frame.remove();
+ }
+ if (reg) {
+ await reg.unregister();
+ }
+ }
+}, 'Request.IsReloadNavigation should persist.');
+
+promise_test(async (t) => {
+ const scope = './resources/blank.html?name=isHistoryNavigation';
+ let frame;
+ let reg;
+
+ try {
+ reg = await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, reg.installing, 'activated');
+ frame = await with_iframe(scope);
+ assert_equals(frame.contentDocument.body.textContent,
+ 'original: false, stored: false');
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.onload = resolve;
+ frame.src = '../resources/blank.html?ignore';
+ });
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.onload = resolve;
+ frame.contentWindow.history.go(-1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'original: true, stored: true');
+ } finally {
+ if (frame) {
+ frame.remove();
+ }
+ if (reg) {
+ await reg.unregister();
+ }
+ }
+}, 'Request.IsHistoryNavigation should persist.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-keys.https.any.js b/third_party/web_platform_tests/service-workers/cache-storage/cache-keys.https.any.js
new file mode 100644
index 0000000..232fb76
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-keys.https.any.js
@@ -0,0 +1,212 @@
+// META: title=Cache.keys
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+cache_test(cache => {
+ return cache.keys()
+ .then(requests => {
+ assert_equals(
+ requests.length, 0,
+ 'Cache.keys should resolve to an empty array for an empty cache');
+ });
+ }, 'Cache.keys() called on an empty cache');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys('not-present-in-the-cache')
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [],
+ 'Cache.keys should resolve with an empty array on failure.');
+ });
+ }, 'Cache.keys with no matching entries');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(entries.a.request.url)
+ .then(function(result) {
+ assert_request_array_equals(result, [entries.a.request],
+ 'Cache.keys should match by URL.');
+ });
+ }, 'Cache.keys with URL');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(entries.a.request)
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [entries.a.request],
+ 'Cache.keys should match by Request.');
+ });
+ }, 'Cache.keys with Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(new Request(entries.a.request.url))
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [entries.a.request],
+ 'Cache.keys should match by Request.');
+ });
+ }, 'Cache.keys with new Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(entries.a.request, {ignoreSearch: true})
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ entries.a.request,
+ entries.a_with_query.request
+ ],
+ 'Cache.keys with ignoreSearch should ignore the ' +
+ 'search parameters of cached request.');
+ });
+ },
+ 'Cache.keys with ignoreSearch option (request with no search ' +
+ 'parameters)');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(entries.a_with_query.request, {ignoreSearch: true})
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ entries.a.request,
+ entries.a_with_query.request
+ ],
+ 'Cache.keys with ignoreSearch should ignore the ' +
+ 'search parameters of request.');
+ });
+ },
+ 'Cache.keys with ignoreSearch option (request with search parameters)');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com/');
+ var head_request = new Request('http://example.com/', {method: 'HEAD'});
+ var response = new Response('foo');
+ return cache.put(request.clone(), response.clone())
+ .then(function() {
+ return cache.keys(head_request.clone());
+ })
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [],
+ 'Cache.keys should resolve with an empty array with a ' +
+ 'mismatched method.');
+ return cache.keys(head_request.clone(),
+ {ignoreMethod: true});
+ })
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ request,
+ ],
+ 'Cache.keys with ignoreMethod should ignore the ' +
+ 'method of request.');
+ });
+ }, 'Cache.keys supports ignoreMethod');
+
+cache_test(function(cache) {
+ var vary_request = new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}});
+ var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+ var mismatched_vary_request = new Request('http://example.com/c');
+
+ return cache.put(vary_request.clone(), vary_response.clone())
+ .then(function() {
+ return cache.keys(mismatched_vary_request.clone());
+ })
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [],
+ 'Cache.keys should resolve with an empty array with a ' +
+ 'mismatched vary.');
+ return cache.keys(mismatched_vary_request.clone(),
+ {ignoreVary: true});
+ })
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ vary_request,
+ ],
+ 'Cache.keys with ignoreVary should ignore the ' +
+ 'vary of request.');
+ });
+ }, 'Cache.keys supports ignoreVary');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(entries.cat.request.url + '#mouse')
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ entries.cat.request,
+ ],
+ 'Cache.keys should ignore URL fragment.');
+ });
+ }, 'Cache.keys with URL containing fragment');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys('http')
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [],
+ 'Cache.keys should treat query as a URL and not ' +
+ 'just a string fragment.');
+ });
+ }, 'Cache.keys with string fragment "http" as query');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys()
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ simple_entries.map(entry => entry.request),
+ 'Cache.keys without parameters should match all entries.');
+ });
+ }, 'Cache.keys without parameters');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(undefined)
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ simple_entries.map(entry => entry.request),
+ 'Cache.keys with undefined request should match all entries.');
+ });
+ }, 'Cache.keys with explicitly undefined request');
+
+cache_test(cache => {
+ return cache.keys(undefined, {})
+ .then(requests => {
+ assert_equals(
+ requests.length, 0,
+ 'Cache.keys should resolve to an empty array for an empty cache');
+ });
+ }, 'Cache.keys with explicitly undefined request and empty options');
+
+prepopulated_cache_test(vary_entries, function(cache, entries) {
+ return cache.keys()
+ .then(function(result) {
+ assert_request_array_equals(
+ result,
+ [
+ entries.vary_cookie_is_cookie.request,
+ entries.vary_cookie_is_good.request,
+ entries.vary_cookie_absent.request,
+ ],
+ 'Cache.keys without parameters should match all entries.');
+ });
+ }, 'Cache.keys without parameters and VARY entries');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.keys(new Request(entries.cat.request.url, {method: 'HEAD'}))
+ .then(function(result) {
+ assert_request_array_equals(
+ result, [],
+ 'Cache.keys should not match HEAD request unless ignoreMethod ' +
+ 'option is set.');
+ });
+ }, 'Cache.keys with a HEAD Request');
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-match.https.any.js b/third_party/web_platform_tests/service-workers/cache-storage/cache-match.https.any.js
new file mode 100644
index 0000000..9ca4590
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-match.https.any.js
@@ -0,0 +1,437 @@
+// META: title=Cache.match
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: script=/common/get-host-info.sub.js
+// META: timeout=long
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match('not-present-in-the-cache')
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.match failures should resolve with undefined.');
+ });
+ }, 'Cache.match with no matching entries');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(entries.a.request.url)
+ .then(function(result) {
+ assert_response_equals(result, entries.a.response,
+ 'Cache.match should match by URL.');
+ });
+ }, 'Cache.match with URL');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(entries.a.request)
+ .then(function(result) {
+ assert_response_equals(result, entries.a.response,
+ 'Cache.match should match by Request.');
+ });
+ }, 'Cache.match with Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ var alt_response = new Response('', {status: 201});
+
+ return self.caches.open('second_matching_cache')
+ .then(function(cache) {
+ return cache.put(entries.a.request, alt_response.clone());
+ })
+ .then(function() {
+ return cache.match(entries.a.request);
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, entries.a.response,
+ 'Cache.match should match the first cache.');
+ });
+ }, 'Cache.match with multiple cache hits');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(new Request(entries.a.request.url))
+ .then(function(result) {
+ assert_response_equals(result, entries.a.response,
+ 'Cache.match should match by Request.');
+ });
+ }, 'Cache.match with new Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(new Request(entries.a.request.url, {method: 'HEAD'}))
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.match should not match HEAD Request.');
+ });
+ }, 'Cache.match with HEAD');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(entries.a.request,
+ {ignoreSearch: true})
+ .then(function(result) {
+ assert_response_in_array(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ],
+ 'Cache.match with ignoreSearch should ignore the ' +
+ 'search parameters of cached request.');
+ });
+ },
+ 'Cache.match with ignoreSearch option (request with no search ' +
+ 'parameters)');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(entries.a_with_query.request,
+ {ignoreSearch: true})
+ .then(function(result) {
+ assert_response_in_array(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ],
+ 'Cache.match with ignoreSearch should ignore the ' +
+ 'search parameters of request.');
+ });
+ },
+ 'Cache.match with ignoreSearch option (request with search parameter)');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com/');
+ var head_request = new Request('http://example.com/', {method: 'HEAD'});
+ var response = new Response('foo');
+ return cache.put(request.clone(), response.clone())
+ .then(function() {
+ return cache.match(head_request.clone());
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'Cache.match should resolve as undefined with a ' +
+ 'mismatched method.');
+ return cache.match(head_request.clone(),
+ {ignoreMethod: true});
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, response,
+ 'Cache.match with ignoreMethod should ignore the ' +
+ 'method of request.');
+ });
+ }, 'Cache.match supports ignoreMethod');
+
+cache_test(function(cache) {
+ var vary_request = new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}});
+ var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+ var mismatched_vary_request = new Request('http://example.com/c');
+
+ return cache.put(vary_request.clone(), vary_response.clone())
+ .then(function() {
+ return cache.match(mismatched_vary_request.clone());
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'Cache.match should resolve as undefined with a ' +
+ 'mismatched vary.');
+ return cache.match(mismatched_vary_request.clone(),
+ {ignoreVary: true});
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, vary_response,
+ 'Cache.match with ignoreVary should ignore the ' +
+ 'vary of request.');
+ });
+ }, 'Cache.match supports ignoreVary');
+
+cache_test(function(cache) {
+ let has_cache_name = false;
+ const opts = {
+ get cacheName() {
+ has_cache_name = true;
+ return undefined;
+ }
+ };
+ return self.caches.open('foo')
+ .then(function() {
+ return cache.match('bar', opts);
+ })
+ .then(function() {
+ assert_false(has_cache_name,
+ 'Cache.match does not support cacheName option ' +
+ 'which was removed in CacheQueryOptions.');
+ });
+ }, 'Cache.match does not support cacheName option');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match(entries.cat.request.url + '#mouse')
+ .then(function(result) {
+ assert_response_equals(result, entries.cat.response,
+ 'Cache.match should ignore URL fragment.');
+ });
+ }, 'Cache.match with URL containing fragment');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.match('http')
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'Cache.match should treat query as a URL and not ' +
+ 'just a string fragment.');
+ });
+ }, 'Cache.match with string fragment "http" as query');
+
+prepopulated_cache_test(vary_entries, function(cache, entries) {
+ return cache.match('http://example.com/c')
+ .then(function(result) {
+ assert_response_in_array(
+ result,
+ [
+ entries.vary_cookie_absent.response
+ ],
+ 'Cache.match should honor "Vary" header.');
+ });
+ }, 'Cache.match with responses containing "Vary" header');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com');
+ var response;
+ var request_url = new URL('./resources/simple.txt', location.href).href;
+ return fetch(request_url)
+ .then(function(fetch_result) {
+ response = fetch_result;
+ assert_equals(
+ response.url, request_url,
+ '[https://fetch.spec.whatwg.org/#dom-response-url] ' +
+ 'Reponse.url should return the URL of the response.');
+ return cache.put(request, response.clone());
+ })
+ .then(function() {
+ return cache.match(request.url);
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, response,
+ 'Cache.match should return a Response object that has the same ' +
+ 'properties as the stored response.');
+ return cache.match(response.url);
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'Cache.match should not match cache entry based on response URL.');
+ });
+ }, 'Cache.match with Request and Response objects with different URLs');
+
+cache_test(function(cache) {
+ var request_url = new URL('./resources/simple.txt', location.href).href;
+ return fetch(request_url)
+ .then(function(fetch_result) {
+ return cache.put(new Request(request_url), fetch_result);
+ })
+ .then(function() {
+ return cache.match(request_url);
+ })
+ .then(function(result) {
+ return result.text();
+ })
+ .then(function(body_text) {
+ assert_equals(body_text, 'a simple text file\n',
+ 'Cache.match should return a Response object with a ' +
+ 'valid body.');
+ })
+ .then(function() {
+ return cache.match(request_url);
+ })
+ .then(function(result) {
+ return result.text();
+ })
+ .then(function(body_text) {
+ assert_equals(body_text, 'a simple text file\n',
+ 'Cache.match should return a Response object with a ' +
+ 'valid body each time it is called.');
+ });
+ }, 'Cache.match invoked multiple times for the same Request/Response');
+
+cache_test(function(cache) {
+ var request_url = new URL('./resources/simple.txt', location.href).href;
+ return fetch(request_url)
+ .then(function(fetch_result) {
+ return cache.put(new Request(request_url), fetch_result);
+ })
+ .then(function() {
+ return cache.match(request_url);
+ })
+ .then(function(result) {
+ return result.blob();
+ })
+ .then(function(blob) {
+ var sliced = blob.slice(2,8);
+
+ return new Promise(function (resolve, reject) {
+ var reader = new FileReader();
+ reader.onloadend = function(event) {
+ resolve(event.target.result);
+ };
+ reader.readAsText(sliced);
+ });
+ })
+ .then(function(text) {
+ assert_equals(text, 'simple',
+ 'A Response blob returned by Cache.match should be ' +
+ 'sliceable.' );
+ });
+ }, 'Cache.match blob should be sliceable');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ var request = new Request(entries.a.request.clone(), {method: 'POST'});
+ return cache.match(request)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.match should not find a match');
+ });
+ }, 'Cache.match with POST Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ var response = entries.non_2xx_response.response;
+ return cache.match(entries.non_2xx_response.request.url)
+ .then(function(result) {
+ assert_response_equals(
+ result, entries.non_2xx_response.response,
+ 'Cache.match should return a Response object that has the ' +
+ 'same properties as a stored non-2xx response.');
+ });
+ }, 'Cache.match with a non-2xx Response');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ var response = entries.error_response.response;
+ return cache.match(entries.error_response.request.url)
+ .then(function(result) {
+ assert_response_equals(
+ result, entries.error_response.response,
+ 'Cache.match should return a Response object that has the ' +
+ 'same properties as a stored network error response.');
+ });
+ }, 'Cache.match with a network error Response');
+
+cache_test(function(cache) {
+ // This test validates that we can get a Response from the Cache API,
+ // clone it, and read just one side of the clone. This was previously
+ // bugged in FF for Responses with large bodies.
+ var data = [];
+ data.length = 80 * 1024;
+ data.fill('F');
+ var response;
+ return cache.put('/', new Response(data.toString()))
+ .then(function(result) {
+ return cache.match('/');
+ })
+ .then(function(r) {
+ // Make sure the original response is not GC'd.
+ response = r;
+ // Return only the clone. We purposefully test that the other
+ // half of the clone does not need to be read here.
+ return response.clone().text();
+ })
+ .then(function(text) {
+ assert_equals(text, data.toString(), 'cloned body text can be read correctly');
+ });
+ }, 'Cache produces large Responses that can be cloned and read correctly.');
+
+cache_test(async (cache) => {
+ const url = get_host_info().HTTPS_REMOTE_ORIGIN +
+ '/service-workers/cache-storage/resources/simple.txt?pipe=' +
+ 'header(access-control-allow-origin,*)|' +
+ 'header(access-control-expose-headers,*)|' +
+ 'header(foo,bar)|' +
+ 'header(set-cookie,X)';
+
+ const response = await fetch(url);
+ await cache.put(new Request(url), response);
+ const cached_response = await cache.match(url);
+
+ const headers = cached_response.headers;
+ assert_equals(headers.get('access-control-expose-headers'), '*');
+ assert_equals(headers.get('foo'), 'bar');
+ assert_equals(headers.get('set-cookie'), null);
+ }, 'cors-exposed header should be stored correctly.');
+
+cache_test(async (cache) => {
+ // A URL that should load a resource with a known mime type.
+ const url = '/service-workers/cache-storage/resources/blank.html';
+ const expected_mime_type = 'text/html';
+
+ // Verify we get the expected mime type from the network. Note,
+ // we cannot use an exact match here since some browsers append
+ // character encoding information to the blob.type value.
+ const net_response = await fetch(url);
+ const net_mime_type = (await net_response.blob()).type;
+ assert_true(net_mime_type.includes(expected_mime_type),
+ 'network response should include the expected mime type');
+
+ // Verify we get the exact same mime type when reading the same
+ // URL resource back out of the cache.
+ await cache.add(url);
+ const cache_response = await cache.match(url);
+ const cache_mime_type = (await cache_response.blob()).type;
+ assert_equals(cache_mime_type, net_mime_type,
+ 'network and cache response mime types should match');
+ }, 'MIME type should be set from content-header correctly.');
+
+cache_test(async (cache) => {
+ const url = '/dummy';
+ const original_type = 'text/html';
+ const override_type = 'text/plain';
+ const init_with_headers = {
+ headers: {
+ 'content-type': original_type
+ }
+ }
+
+ // Verify constructing a synthetic response with a content-type header
+ // gets the correct mime type.
+ const response = new Response('hello world', init_with_headers);
+ const original_response_type = (await response.blob()).type;
+ assert_true(original_response_type.includes(original_type),
+ 'original response should include the expected mime type');
+
+ // Verify overwriting the content-type header changes the mime type.
+ const overwritten_response = new Response('hello world', init_with_headers);
+ overwritten_response.headers.set('content-type', override_type);
+ const overwritten_response_type = (await overwritten_response.blob()).type;
+ assert_equals(overwritten_response_type, override_type,
+ 'mime type can be overridden');
+
+ // Verify the Response read from Cache uses the original mime type
+ // computed when it was first constructed.
+ const tmp = new Response('hello world', init_with_headers);
+ tmp.headers.set('content-type', override_type);
+ await cache.put(url, tmp);
+ const cache_response = await cache.match(url);
+ const cache_mime_type = (await cache_response.blob()).type;
+ assert_equals(cache_mime_type, override_type,
+ 'overwritten and cached response mime types should match');
+ }, 'MIME type should reflect Content-Type headers of response.');
+
+cache_test(async (cache) => {
+ const url = new URL('./resources/vary.py?vary=foo',
+ get_host_info().HTTPS_REMOTE_ORIGIN + self.location.pathname);
+ const original_request = new Request(url, { mode: 'no-cors',
+ headers: { 'foo': 'bar' } });
+ const fetch_response = await fetch(original_request);
+ assert_equals(fetch_response.type, 'opaque');
+
+ await cache.put(original_request, fetch_response);
+
+ const match_response_1 = await cache.match(original_request);
+ assert_not_equals(match_response_1, undefined);
+
+ // Verify that cache.match() finds the entry even if queried with a varied
+ // header that does not match the cache key. Vary headers should be ignored
+ // for opaque responses.
+ const different_request = new Request(url, { headers: { 'foo': 'CHANGED' } });
+ const match_response_2 = await cache.match(different_request);
+ assert_not_equals(match_response_2, undefined);
+}, 'Cache.match ignores vary headers on opaque response.');
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-matchAll.https.any.js b/third_party/web_platform_tests/service-workers/cache-storage/cache-matchAll.https.any.js
new file mode 100644
index 0000000..93c5517
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-matchAll.https.any.js
@@ -0,0 +1,244 @@
+// META: title=Cache.matchAll
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll('not-present-in-the-cache')
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [],
+ 'Cache.matchAll should resolve with an empty array on failure.');
+ });
+ }, 'Cache.matchAll with no matching entries');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a.request.url)
+ .then(function(result) {
+ assert_response_array_equals(result, [entries.a.response],
+ 'Cache.matchAll should match by URL.');
+ });
+ }, 'Cache.matchAll with URL');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a.request)
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [entries.a.response],
+ 'Cache.matchAll should match by Request.');
+ });
+ }, 'Cache.matchAll with Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(new Request(entries.a.request.url))
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [entries.a.response],
+ 'Cache.matchAll should match by Request.');
+ });
+ }, 'Cache.matchAll with new Request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(new Request(entries.a.request.url, {method: 'HEAD'}),
+ {ignoreSearch: true})
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [],
+ 'Cache.matchAll should not match HEAD Request.');
+ });
+ }, 'Cache.matchAll with HEAD');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a.request,
+ {ignoreSearch: true})
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ],
+ 'Cache.matchAll with ignoreSearch should ignore the ' +
+ 'search parameters of cached request.');
+ });
+ },
+ 'Cache.matchAll with ignoreSearch option (request with no search ' +
+ 'parameters)');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.a_with_query.request,
+ {ignoreSearch: true})
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.a.response,
+ entries.a_with_query.response
+ ],
+ 'Cache.matchAll with ignoreSearch should ignore the ' +
+ 'search parameters of request.');
+ });
+ },
+ 'Cache.matchAll with ignoreSearch option (request with search parameters)');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com/');
+ var head_request = new Request('http://example.com/', {method: 'HEAD'});
+ var response = new Response('foo');
+ return cache.put(request.clone(), response.clone())
+ .then(function() {
+ return cache.matchAll(head_request.clone());
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [],
+ 'Cache.matchAll should resolve with empty array for a ' +
+ 'mismatched method.');
+ return cache.matchAll(head_request.clone(),
+ {ignoreMethod: true});
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [response],
+ 'Cache.matchAll with ignoreMethod should ignore the ' +
+ 'method of request.');
+ });
+ }, 'Cache.matchAll supports ignoreMethod');
+
+cache_test(function(cache) {
+ var vary_request = new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}});
+ var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+ var mismatched_vary_request = new Request('http://example.com/c');
+
+ return cache.put(vary_request.clone(), vary_response.clone())
+ .then(function() {
+ return cache.matchAll(mismatched_vary_request.clone());
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [],
+ 'Cache.matchAll should resolve as undefined with a ' +
+ 'mismatched vary.');
+ return cache.matchAll(mismatched_vary_request.clone(),
+ {ignoreVary: true});
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [vary_response],
+ 'Cache.matchAll with ignoreVary should ignore the ' +
+ 'vary of request.');
+ });
+ }, 'Cache.matchAll supports ignoreVary');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(entries.cat.request.url + '#mouse')
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.cat.response,
+ ],
+ 'Cache.matchAll should ignore URL fragment.');
+ });
+ }, 'Cache.matchAll with URL containing fragment');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll('http')
+ .then(function(result) {
+ assert_response_array_equals(
+ result, [],
+ 'Cache.matchAll should treat query as a URL and not ' +
+ 'just a string fragment.');
+ });
+ }, 'Cache.matchAll with string fragment "http" as query');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll()
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ simple_entries.map(entry => entry.response),
+ 'Cache.matchAll without parameters should match all entries.');
+ });
+ }, 'Cache.matchAll without parameters');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(undefined)
+ .then(result => {
+ assert_response_array_equals(
+ result,
+ simple_entries.map(entry => entry.response),
+ 'Cache.matchAll with undefined request should match all entries.');
+ });
+ }, 'Cache.matchAll with explicitly undefined request');
+
+prepopulated_cache_test(simple_entries, function(cache, entries) {
+ return cache.matchAll(undefined, {})
+ .then(result => {
+ assert_response_array_equals(
+ result,
+ simple_entries.map(entry => entry.response),
+ 'Cache.matchAll with undefined request should match all entries.');
+ });
+ }, 'Cache.matchAll with explicitly undefined request and empty options');
+
+prepopulated_cache_test(vary_entries, function(cache, entries) {
+ return cache.matchAll('http://example.com/c')
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.vary_cookie_absent.response
+ ],
+ 'Cache.matchAll should exclude matches if a vary header is ' +
+ 'missing in the query request, but is present in the cached ' +
+ 'request.');
+ })
+
+ .then(function() {
+ return cache.matchAll(
+ new Request('http://example.com/c',
+ {headers: {'Cookies': 'none-of-the-above'}}));
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ ],
+ 'Cache.matchAll should exclude matches if a vary header is ' +
+ 'missing in the cached request, but is present in the query ' +
+ 'request.');
+ })
+
+ .then(function() {
+ return cache.matchAll(
+ new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}}));
+ })
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [entries.vary_cookie_is_cookie.response],
+ 'Cache.matchAll should match the entire header if a vary header ' +
+ 'is present in both the query and cached requests.');
+ });
+ }, 'Cache.matchAll with responses containing "Vary" header');
+
+prepopulated_cache_test(vary_entries, function(cache, entries) {
+ return cache.matchAll('http://example.com/c',
+ {ignoreVary: true})
+ .then(function(result) {
+ assert_response_array_equals(
+ result,
+ [
+ entries.vary_cookie_is_cookie.response,
+ entries.vary_cookie_is_good.response,
+ entries.vary_cookie_absent.response
+ ],
+ 'Cache.matchAll should support multiple vary request/response ' +
+ 'pairs.');
+ });
+ }, 'Cache.matchAll with multiple vary pairs');
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-put.https.any.js b/third_party/web_platform_tests/service-workers/cache-storage/cache-put.https.any.js
new file mode 100644
index 0000000..dbf2650
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-put.https.any.js
@@ -0,0 +1,411 @@
+// META: title=Cache.put
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+var test_url = 'https://example.com/foo';
+var test_body = 'Hello world!';
+const { REMOTE_HOST } = get_host_info();
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new Response(test_body);
+ return cache.put(request, response)
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Cache.put should resolve with undefined on success.');
+ });
+ }, 'Cache.put called with simple Request and Response');
+
+cache_test(function(cache) {
+ var test_url = new URL('./resources/simple.txt', location.href).href;
+ var request = new Request(test_url);
+ var response;
+ return fetch(test_url)
+ .then(function(fetch_result) {
+ response = fetch_result.clone();
+ return cache.put(request, fetch_result);
+ })
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, response,
+ 'Cache.put should update the cache with ' +
+ 'new request and response.');
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, 'a simple text file\n',
+ 'Cache.put should store response body.');
+ });
+ }, 'Cache.put called with Request and Response from fetch()');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new Response(test_body);
+ assert_false(request.bodyUsed,
+ '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' +
+ 'Request.bodyUsed should be initially false.');
+ return cache.put(request, response)
+ .then(function() {
+ assert_false(request.bodyUsed,
+ 'Cache.put should not mark empty request\'s body used');
+ });
+ }, 'Cache.put with Request without a body');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new Response();
+ assert_false(response.bodyUsed,
+ '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' +
+ 'Response.bodyUsed should be initially false.');
+ return cache.put(request, response)
+ .then(function() {
+ assert_false(response.bodyUsed,
+ 'Cache.put should not mark empty response\'s body used');
+ });
+ }, 'Cache.put with Response without a body');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new Response(test_body);
+ return cache.put(request, response.clone())
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, response,
+ 'Cache.put should update the cache with ' +
+ 'new Request and Response.');
+ });
+ }, 'Cache.put with a Response containing an empty URL');
+
+cache_test(function(cache) {
+ var request = new Request(test_url);
+ var response = new Response('', {
+ status: 200,
+ headers: [['Content-Type', 'text/plain']]
+ });
+ return cache.put(request, response)
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_equals(result.status, 200, 'Cache.put should store status.');
+ assert_equals(result.headers.get('Content-Type'), 'text/plain',
+ 'Cache.put should store headers.');
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, '',
+ 'Cache.put should store response body.');
+ });
+ }, 'Cache.put with an empty response body');
+
+cache_test(function(cache, test) {
+ var request = new Request(test_url);
+ var response = new Response('', {
+ status: 206,
+ headers: [['Content-Type', 'text/plain']]
+ });
+
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(request, response),
+ 'Cache.put should reject 206 Responses with a TypeError.');
+ }, 'Cache.put with synthetic 206 response');
+
+cache_test(function(cache, test) {
+ var test_url = new URL('./resources/fetch-status.py?status=206', location.href).href;
+ var request = new Request(test_url);
+ var response;
+ return fetch(test_url)
+ .then(function(fetch_result) {
+ assert_equals(fetch_result.status, 206,
+ 'Test framework error: The status code should be 206.');
+ response = fetch_result.clone();
+ return promise_rejects_js(test, TypeError, cache.put(request, fetch_result));
+ });
+ }, 'Cache.put with HTTP 206 response');
+
+cache_test(function(cache, test) {
+ // We need to jump through some hoops to allow the test to perform opaque
+ // response filtering, but bypass the ORB safelist check. This is
+ // done, by forcing the MIME type retrieval to fail and the
+ // validation of partial first response to succeed.
+ var pipe = "status(206)|header(Content-Type,)|header(Content-Range, bytes 0-1/41)|slice(null, 1)";
+ var test_url = new URL(`./resources/blank.html?pipe=${pipe}`, location.href);
+ test_url.hostname = REMOTE_HOST;
+ var request = new Request(test_url.href, { mode: 'no-cors' });
+ var response;
+ return fetch(request)
+ .then(function(fetch_result) {
+ assert_equals(fetch_result.type, 'opaque',
+ 'Test framework error: The response type should be opaque.');
+ assert_equals(fetch_result.status, 0,
+ 'Test framework error: The status code should be 0 for an ' +
+ ' opaque-filtered response. This is actually HTTP 206.');
+ response = fetch_result.clone();
+ return cache.put(request, fetch_result);
+ })
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_not_equals(result, undefined,
+ 'Cache.put should store an entry for the opaque response');
+ });
+ }, 'Cache.put with opaque-filtered HTTP 206 response');
+
+cache_test(function(cache) {
+ var test_url = new URL('./resources/fetch-status.py?status=500', location.href).href;
+ var request = new Request(test_url);
+ var response;
+ return fetch(test_url)
+ .then(function(fetch_result) {
+ assert_equals(fetch_result.status, 500,
+ 'Test framework error: The status code should be 500.');
+ response = fetch_result.clone();
+ return cache.put(request, fetch_result);
+ })
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, response,
+ 'Cache.put should update the cache with ' +
+ 'new request and response.');
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, '',
+ 'Cache.put should store response body.');
+ });
+ }, 'Cache.put with HTTP 500 response');
+
+cache_test(function(cache) {
+ var alternate_response_body = 'New body';
+ var alternate_response = new Response(alternate_response_body,
+ { statusText: 'New status' });
+ return cache.put(new Request(test_url),
+ new Response('Old body', { statusText: 'Old status' }))
+ .then(function() {
+ return cache.put(new Request(test_url), alternate_response.clone());
+ })
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, alternate_response,
+ 'Cache.put should replace existing ' +
+ 'response with new response.');
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, alternate_response_body,
+ 'Cache put should store new response body.');
+ });
+ }, 'Cache.put called twice with matching Requests and different Responses');
+
+cache_test(function(cache) {
+ var first_url = test_url;
+ var second_url = first_url + '#(O_o)';
+ var third_url = first_url + '#fragment';
+ var alternate_response_body = 'New body';
+ var alternate_response = new Response(alternate_response_body,
+ { statusText: 'New status' });
+ return cache.put(new Request(first_url),
+ new Response('Old body', { statusText: 'Old status' }))
+ .then(function() {
+ return cache.put(new Request(second_url), alternate_response.clone());
+ })
+ .then(function() {
+ return cache.match(test_url);
+ })
+ .then(function(result) {
+ assert_response_equals(result, alternate_response,
+ 'Cache.put should replace existing ' +
+ 'response with new response.');
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, alternate_response_body,
+ 'Cache put should store new response body.');
+ })
+ .then(function() {
+ return cache.put(new Request(third_url), alternate_response.clone());
+ })
+ .then(function() {
+ return cache.keys();
+ })
+ .then(function(results) {
+ // Should match urls (without fragments or with different ones) to the
+ // same cache key. However, result.url should be the latest url used.
+ assert_equals(results[0].url, third_url);
+ return;
+ });
+}, 'Cache.put called multiple times with request URLs that differ only by a fragment');
+
+cache_test(function(cache) {
+ var url = 'http://example.com/foo';
+ return cache.put(url, new Response('some body'))
+ .then(function() { return cache.match(url); })
+ .then(function(response) { return response.text(); })
+ .then(function(body) {
+ assert_equals(body, 'some body',
+ 'Cache.put should accept a string as request.');
+ });
+ }, 'Cache.put with a string request');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(new Request(test_url), 'Hello world!'),
+ 'Cache.put should only accept a Response object as the response.');
+ }, 'Cache.put with an invalid response');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(new Request('file:///etc/passwd'),
+ new Response(test_body)),
+ 'Cache.put should reject non-HTTP/HTTPS requests with a TypeError.');
+ }, 'Cache.put with a non-HTTP/HTTPS request');
+
+cache_test(function(cache) {
+ var response = new Response(test_body);
+ return cache.put(new Request('relative-url'), response.clone())
+ .then(function() {
+ return cache.match(new URL('relative-url', location.href).href);
+ })
+ .then(function(result) {
+ assert_response_equals(result, response,
+ 'Cache.put should accept a relative URL ' +
+ 'as the request.');
+ });
+ }, 'Cache.put with a relative URL');
+
+cache_test(function(cache, test) {
+ var request = new Request('http://example.com/foo', { method: 'HEAD' });
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(request, new Response(test_body)),
+ 'Cache.put should throw a TypeError for non-GET requests.');
+ }, 'Cache.put with a non-GET request');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(new Request(test_url), null),
+ 'Cache.put should throw a TypeError for a null response.');
+ }, 'Cache.put with a null response');
+
+cache_test(function(cache, test) {
+ var request = new Request(test_url, {method: 'POST', body: test_body});
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(request, new Response(test_body)),
+ 'Cache.put should throw a TypeError for a POST request.');
+ }, 'Cache.put with a POST request');
+
+cache_test(function(cache) {
+ var response = new Response(test_body);
+ assert_false(response.bodyUsed,
+ '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' +
+ 'Response.bodyUsed should be initially false.');
+ return response.text().then(function() {
+ assert_true(
+ response.bodyUsed,
+ '[https://fetch.spec.whatwg.org/#concept-body-consume-body] ' +
+ 'The text() method should make the body disturbed.');
+ var request = new Request(test_url);
+ return cache.put(request, response).then(() => {
+ assert_unreached('cache.put should be rejected');
+ }, () => {});
+ });
+ }, 'Cache.put with a used response body');
+
+cache_test(function(cache) {
+ var response = new Response(test_body);
+ return cache.put(new Request(test_url), response)
+ .then(function() {
+ assert_throws_js(TypeError, () => response.body.getReader());
+ });
+ }, 'getReader() after Cache.put');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(new Request(test_url),
+ new Response(test_body, { headers: { VARY: '*' }})),
+ 'Cache.put should reject VARY:* Responses with a TypeError.');
+ }, 'Cache.put with a VARY:* Response');
+
+cache_test(function(cache, test) {
+ return promise_rejects_js(
+ test,
+ TypeError,
+ cache.put(new Request(test_url),
+ new Response(test_body,
+ { headers: { VARY: 'Accept-Language,*' }})),
+ 'Cache.put should reject Responses with an embedded VARY:* with a ' +
+ 'TypeError.');
+ }, 'Cache.put with an embedded VARY:* Response');
+
+cache_test(async function(cache, test) {
+ const url = new URL('./resources/vary.py?vary=*',
+ get_host_info().HTTPS_REMOTE_ORIGIN + self.location.pathname);
+ const request = new Request(url, { mode: 'no-cors' });
+ const response = await fetch(request);
+ assert_equals(response.type, 'opaque');
+ await cache.put(request, response);
+ }, 'Cache.put with a VARY:* opaque response should not reject');
+
+cache_test(function(cache) {
+ var url = 'foo.html';
+ var redirectURL = 'http://example.com/foo-bar.html';
+ var redirectResponse = Response.redirect(redirectURL);
+ assert_equals(redirectResponse.headers.get('Location'), redirectURL,
+ 'Response.redirect() should set Location header.');
+ return cache.put(url, redirectResponse.clone())
+ .then(function() {
+ return cache.match(url);
+ })
+ .then(function(response) {
+ assert_response_equals(response, redirectResponse,
+ 'Redirect response is reproduced by the Cache API');
+ assert_equals(response.headers.get('Location'), redirectURL,
+ 'Location header is preserved by Cache API.');
+ });
+ }, 'Cache.put should store Response.redirect() correctly');
+
+cache_test(async (cache) => {
+ var request = new Request(test_url);
+ var response = new Response(new Blob([test_body]));
+ await cache.put(request, response);
+ var cachedResponse = await cache.match(request);
+ assert_equals(await cachedResponse.text(), test_body);
+ }, 'Cache.put called with simple Request and blob Response');
+
+cache_test(async (cache) => {
+ var formData = new FormData();
+ formData.append("name", "value");
+
+ var request = new Request(test_url);
+ var response = new Response(formData);
+ await cache.put(request, response);
+ var cachedResponse = await cache.match(request);
+ var cachedResponseText = await cachedResponse.text();
+ assert_true(cachedResponseText.indexOf("name=\"name\"\r\n\r\nvalue") !== -1);
+}, 'Cache.put called with simple Request and form data Response');
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-storage-buckets.https.any.js b/third_party/web_platform_tests/service-workers/cache-storage/cache-storage-buckets.https.any.js
new file mode 100644
index 0000000..0b5ef7b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-storage-buckets.https.any.js
@@ -0,0 +1,71 @@
+// META: title=Cache.put
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+var test_url = 'https://example.com/foo';
+var test_body = 'Hello world!';
+const { REMOTE_HOST } = get_host_info();
+
+promise_test(async function(test) {
+ var inboxBucket = await navigator.storageBuckets.open('inbox');
+ var draftsBucket = await navigator.storageBuckets.open('drafts');
+
+ test.add_cleanup(async function() {
+ await navigator.storageBuckets.delete('inbox');
+ await navigator.storageBuckets.delete('drafts');
+ });
+
+ const cacheName = 'attachments';
+ const cacheKey = 'receipt1.txt';
+
+ var inboxCache = await inboxBucket.caches.open(cacheName);
+ var draftsCache = await draftsBucket.caches.open(cacheName);
+
+ await inboxCache.put(cacheKey, new Response('bread x 2'))
+ await draftsCache.put(cacheKey, new Response('eggs x 1'));
+
+ return inboxCache.match(cacheKey)
+ .then(function(result) {
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, 'bread x 2', 'Wrong cache contents');
+ return draftsCache.match(cacheKey);
+ })
+ .then(function(result) {
+ return result.text();
+ })
+ .then(function(body) {
+ assert_equals(body, 'eggs x 1', 'Wrong cache contents');
+ });
+}, 'caches from different buckets have different contents');
+
+promise_test(async function(test) {
+ var inboxBucket = await navigator.storageBuckets.open('inbox');
+ var draftBucket = await navigator.storageBuckets.open('drafts');
+
+ test.add_cleanup(async function() {
+ await navigator.storageBuckets.delete('inbox');
+ await navigator.storageBuckets.delete('drafts');
+ });
+
+ var caches = inboxBucket.caches;
+ var attachments = await caches.open('attachments');
+ await attachments.put('receipt1.txt', new Response('bread x 2'));
+ var result = await attachments.match('receipt1.txt');
+ assert_equals(await result.text(), 'bread x 2');
+
+ await navigator.storageBuckets.delete('inbox');
+
+ await promise_rejects_dom(
+ test, 'UnknownError', caches.open('attachments'));
+
+ // Also test when `caches` is first accessed after the deletion.
+ await navigator.storageBuckets.delete('drafts');
+ return promise_rejects_dom(
+ test, 'UnknownError', draftBucket.caches.open('attachments'));
+}, 'cache.open promise is rejected when bucket is gone');
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-storage-keys.https.any.js b/third_party/web_platform_tests/service-workers/cache-storage/cache-storage-keys.https.any.js
new file mode 100644
index 0000000..f19522b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-storage-keys.https.any.js
@@ -0,0 +1,35 @@
+// META: title=CacheStorage.keys
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+var test_cache_list =
+ ['', 'example', 'Another cache name', 'A', 'a', 'ex ample'];
+
+promise_test(function(test) {
+ return self.caches.keys()
+ .then(function(keys) {
+ assert_true(Array.isArray(keys),
+ 'CacheStorage.keys should return an Array.');
+ return Promise.all(keys.map(function(key) {
+ return self.caches.delete(key);
+ }));
+ })
+ .then(function() {
+ return Promise.all(test_cache_list.map(function(key) {
+ return self.caches.open(key);
+ }));
+ })
+
+ .then(function() { return self.caches.keys(); })
+ .then(function(keys) {
+ assert_true(Array.isArray(keys),
+ 'CacheStorage.keys should return an Array.');
+ assert_array_equals(keys,
+ test_cache_list,
+ 'CacheStorage.keys should only return ' +
+ 'existing caches.');
+ });
+ }, 'CacheStorage keys');
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-storage-match.https.any.js b/third_party/web_platform_tests/service-workers/cache-storage/cache-storage-match.https.any.js
new file mode 100644
index 0000000..0c31b72
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-storage-match.https.any.js
@@ -0,0 +1,245 @@
+// META: title=CacheStorage.match
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+(function() {
+ var next_index = 1;
+
+ // Returns a transaction (request, response, and url) for a unique URL.
+ function create_unique_transaction(test) {
+ var uniquifier = String(next_index++);
+ var url = 'http://example.com/' + uniquifier;
+
+ return {
+ request: new Request(url),
+ response: new Response('hello'),
+ url: url
+ };
+ }
+
+ self.create_unique_transaction = create_unique_transaction;
+})();
+
+cache_test(function(cache) {
+ var transaction = create_unique_transaction();
+
+ return cache.put(transaction.request.clone(), transaction.response.clone())
+ .then(function() {
+ return self.caches.match(transaction.request);
+ })
+ .then(function(response) {
+ assert_response_equals(response, transaction.response,
+ 'The response should not have changed.');
+ });
+}, 'CacheStorageMatch with no cache name provided');
+
+cache_test(function(cache) {
+ var transaction = create_unique_transaction();
+
+ var test_cache_list = ['a', 'b', 'c'];
+ return cache.put(transaction.request.clone(), transaction.response.clone())
+ .then(function() {
+ return Promise.all(test_cache_list.map(function(key) {
+ return self.caches.open(key);
+ }));
+ })
+ .then(function() {
+ return self.caches.match(transaction.request);
+ })
+ .then(function(response) {
+ assert_response_equals(response, transaction.response,
+ 'The response should not have changed.');
+ });
+}, 'CacheStorageMatch from one of many caches');
+
+promise_test(function(test) {
+ var transaction = create_unique_transaction();
+
+ var test_cache_list = ['x', 'y', 'z'];
+ return Promise.all(test_cache_list.map(function(key) {
+ return self.caches.open(key);
+ }))
+ .then(function() { return self.caches.open('x'); })
+ .then(function(cache) {
+ return cache.put(transaction.request.clone(),
+ transaction.response.clone());
+ })
+ .then(function() {
+ return self.caches.match(transaction.request, {cacheName: 'x'});
+ })
+ .then(function(response) {
+ assert_response_equals(response, transaction.response,
+ 'The response should not have changed.');
+ })
+ .then(function() {
+ return self.caches.match(transaction.request, {cacheName: 'y'});
+ })
+ .then(function(response) {
+ assert_equals(response, undefined,
+ 'Cache y should not have a response for the request.');
+ });
+}, 'CacheStorageMatch from one of many caches by name');
+
+cache_test(function(cache) {
+ var transaction = create_unique_transaction();
+ return cache.put(transaction.url, transaction.response.clone())
+ .then(function() {
+ return self.caches.match(transaction.request);
+ })
+ .then(function(response) {
+ assert_response_equals(response, transaction.response,
+ 'The response should not have changed.');
+ });
+}, 'CacheStorageMatch a string request');
+
+cache_test(function(cache) {
+ var transaction = create_unique_transaction();
+ return cache.put(transaction.request.clone(), transaction.response.clone())
+ .then(function() {
+ return self.caches.match(new Request(transaction.request.url,
+ {method: 'HEAD'}));
+ })
+ .then(function(response) {
+ assert_equals(response, undefined,
+ 'A HEAD request should not be matched');
+ });
+}, 'CacheStorageMatch a HEAD request');
+
+promise_test(function(test) {
+ var transaction = create_unique_transaction();
+ return self.caches.match(transaction.request)
+ .then(function(response) {
+ assert_equals(response, undefined,
+ 'The response should not be found.');
+ });
+}, 'CacheStorageMatch with no cached entry');
+
+promise_test(function(test) {
+ var transaction = create_unique_transaction();
+ return self.caches.delete('foo')
+ .then(function() {
+ return self.caches.has('foo');
+ })
+ .then(function(has_foo) {
+ assert_false(has_foo, "The cache should not exist.");
+ return self.caches.match(transaction.request, {cacheName: 'foo'});
+ })
+ .then(function(response) {
+ assert_equals(response, undefined,
+ 'The match with bad cache name should resolve to ' +
+ 'undefined.');
+ return self.caches.has('foo');
+ })
+ .then(function(has_foo) {
+ assert_false(has_foo, "The cache should still not exist.");
+ });
+}, 'CacheStorageMatch with no caches available but name provided');
+
+cache_test(function(cache) {
+ var transaction = create_unique_transaction();
+
+ return self.caches.delete('')
+ .then(function() {
+ return self.caches.has('');
+ })
+ .then(function(has_cache) {
+ assert_false(has_cache, "The cache should not exist.");
+ return cache.put(transaction.request, transaction.response.clone());
+ })
+ .then(function() {
+ return self.caches.match(transaction.request, {cacheName: ''});
+ })
+ .then(function(response) {
+ assert_equals(response, undefined,
+ 'The response should not be found.');
+ return self.caches.open('');
+ })
+ .then(function(cache) {
+ return cache.put(transaction.request, transaction.response);
+ })
+ .then(function() {
+ return self.caches.match(transaction.request, {cacheName: ''});
+ })
+ .then(function(response) {
+ assert_response_equals(response, transaction.response,
+ 'The response should be matched.');
+ return self.caches.delete('');
+ });
+}, 'CacheStorageMatch with empty cache name provided');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com/?foo');
+ var no_query_request = new Request('http://example.com/');
+ var response = new Response('foo');
+ return cache.put(request.clone(), response.clone())
+ .then(function() {
+ return self.caches.match(no_query_request.clone());
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'CacheStorageMatch should resolve as undefined with a ' +
+ 'mismatched query.');
+ return self.caches.match(no_query_request.clone(),
+ {ignoreSearch: true});
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, response,
+ 'CacheStorageMatch with ignoreSearch should ignore the ' +
+ 'query of the request.');
+ });
+ }, 'CacheStorageMatch supports ignoreSearch');
+
+cache_test(function(cache) {
+ var request = new Request('http://example.com/');
+ var head_request = new Request('http://example.com/', {method: 'HEAD'});
+ var response = new Response('foo');
+ return cache.put(request.clone(), response.clone())
+ .then(function() {
+ return self.caches.match(head_request.clone());
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'CacheStorageMatch should resolve as undefined with a ' +
+ 'mismatched method.');
+ return self.caches.match(head_request.clone(),
+ {ignoreMethod: true});
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, response,
+ 'CacheStorageMatch with ignoreMethod should ignore the ' +
+ 'method of request.');
+ });
+ }, 'Cache.match supports ignoreMethod');
+
+cache_test(function(cache) {
+ var vary_request = new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}});
+ var vary_response = new Response('', {headers: {'Vary': 'Cookies'}});
+ var mismatched_vary_request = new Request('http://example.com/c');
+
+ return cache.put(vary_request.clone(), vary_response.clone())
+ .then(function() {
+ return self.caches.match(mismatched_vary_request.clone());
+ })
+ .then(function(result) {
+ assert_equals(
+ result, undefined,
+ 'CacheStorageMatch should resolve as undefined with a ' +
+ ' mismatched vary.');
+ return self.caches.match(mismatched_vary_request.clone(),
+ {ignoreVary: true});
+ })
+ .then(function(result) {
+ assert_response_equals(
+ result, vary_response,
+ 'CacheStorageMatch with ignoreVary should ignore the ' +
+ 'vary of request.');
+ });
+ }, 'CacheStorageMatch supports ignoreVary');
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cache-storage.https.any.js b/third_party/web_platform_tests/service-workers/cache-storage/cache-storage.https.any.js
new file mode 100644
index 0000000..b7d5af7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cache-storage.https.any.js
@@ -0,0 +1,239 @@
+// META: title=CacheStorage
+// META: global=window,worker
+// META: script=./resources/test-helpers.js
+// META: timeout=long
+
+promise_test(function(t) {
+ var cache_name = 'cache-storage/foo';
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function(cache) {
+ assert_true(cache instanceof Cache,
+ 'CacheStorage.open should return a Cache.');
+ });
+ }, 'CacheStorage.open');
+
+promise_test(function(t) {
+ var cache_name = 'cache-storage/bar';
+ var first_cache = null;
+ var second_cache = null;
+ return self.caches.open(cache_name)
+ .then(function(cache) {
+ first_cache = cache;
+ return self.caches.delete(cache_name);
+ })
+ .then(function() {
+ return first_cache.add('./resources/simple.txt');
+ })
+ .then(function() {
+ return self.caches.keys();
+ })
+ .then(function(cache_names) {
+ assert_equals(cache_names.indexOf(cache_name), -1);
+ return self.caches.open(cache_name);
+ })
+ .then(function(cache) {
+ second_cache = cache;
+ return second_cache.keys();
+ })
+ .then(function(keys) {
+ assert_equals(keys.length, 0);
+ return first_cache.keys();
+ })
+ .then(function(keys) {
+ assert_equals(keys.length, 1);
+ // Clean up
+ return self.caches.delete(cache_name);
+ });
+ }, 'CacheStorage.delete dooms, but does not delete immediately');
+
+promise_test(function(t) {
+ // Note that this test may collide with other tests running in the same
+ // origin that also uses an empty cache name.
+ var cache_name = '';
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function(cache) {
+ assert_true(cache instanceof Cache,
+ 'CacheStorage.open should accept an empty name.');
+ });
+ }, 'CacheStorage.open with an empty name');
+
+promise_test(function(t) {
+ return promise_rejects_js(
+ t,
+ TypeError,
+ self.caches.open(),
+ 'CacheStorage.open should throw TypeError if called with no arguments.');
+ }, 'CacheStorage.open with no arguments');
+
+promise_test(function(t) {
+ var test_cases = [
+ {
+ name: 'cache-storage/lowercase',
+ should_not_match:
+ [
+ 'cache-storage/Lowercase',
+ ' cache-storage/lowercase',
+ 'cache-storage/lowercase '
+ ]
+ },
+ {
+ name: 'cache-storage/has a space',
+ should_not_match:
+ [
+ 'cache-storage/has'
+ ]
+ },
+ {
+ name: 'cache-storage/has\000_in_the_name',
+ should_not_match:
+ [
+ 'cache-storage/has',
+ 'cache-storage/has_in_the_name'
+ ]
+ }
+ ];
+ return Promise.all(test_cases.map(function(testcase) {
+ var cache_name = testcase.name;
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function() {
+ return self.caches.has(cache_name);
+ })
+ .then(function(result) {
+ assert_true(result,
+ 'CacheStorage.has should return true for existing ' +
+ 'cache.');
+ })
+ .then(function() {
+ return Promise.all(
+ testcase.should_not_match.map(function(cache_name) {
+ return self.caches.has(cache_name)
+ .then(function(result) {
+ assert_false(result,
+ 'CacheStorage.has should only perform ' +
+ 'exact matches on cache names.');
+ });
+ }));
+ })
+ .then(function() {
+ return self.caches.delete(cache_name);
+ });
+ }));
+ }, 'CacheStorage.has with existing cache');
+
+promise_test(function(t) {
+ return self.caches.has('cheezburger')
+ .then(function(result) {
+ assert_false(result,
+ 'CacheStorage.has should return false for ' +
+ 'nonexistent cache.');
+ });
+ }, 'CacheStorage.has with nonexistent cache');
+
+promise_test(function(t) {
+ var cache_name = 'cache-storage/open';
+ var cache;
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function(result) {
+ cache = result;
+ })
+ .then(function() {
+ return cache.add('./resources/simple.txt');
+ })
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function(result) {
+ assert_true(result instanceof Cache,
+ 'CacheStorage.open should return a Cache object');
+ assert_not_equals(result, cache,
+ 'CacheStorage.open should return a new Cache ' +
+ 'object each time its called.');
+ return Promise.all([cache.keys(), result.keys()]);
+ })
+ .then(function(results) {
+ var expected_urls = results[0].map(function(r) { return r.url });
+ var actual_urls = results[1].map(function(r) { return r.url });
+ assert_array_equals(actual_urls, expected_urls,
+ 'CacheStorage.open should return a new Cache ' +
+ 'object for the same backing store.');
+ });
+ }, 'CacheStorage.open with existing cache');
+
+promise_test(function(t) {
+ var cache_name = 'cache-storage/delete';
+
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function() { return self.caches.delete(cache_name); })
+ .then(function(result) {
+ assert_true(result,
+ 'CacheStorage.delete should return true after ' +
+ 'deleting an existing cache.');
+ })
+
+ .then(function() { return self.caches.has(cache_name); })
+ .then(function(cache_exists) {
+ assert_false(cache_exists,
+ 'CacheStorage.has should return false after ' +
+ 'fulfillment of CacheStorage.delete promise.');
+ });
+ }, 'CacheStorage.delete with existing cache');
+
+promise_test(function(t) {
+ return self.caches.delete('cheezburger')
+ .then(function(result) {
+ assert_false(result,
+ 'CacheStorage.delete should return false for a ' +
+ 'nonexistent cache.');
+ });
+ }, 'CacheStorage.delete with nonexistent cache');
+
+promise_test(function(t) {
+ var unpaired_name = 'unpaired\uD800';
+ var converted_name = 'unpaired\uFFFD';
+
+ // The test assumes that a cache with converted_name does not
+ // exist, but if the implementation fails the test then such
+ // a cache will be created. Start off in a fresh state by
+ // deleting all caches.
+ return delete_all_caches()
+ .then(function() {
+ return self.caches.has(converted_name);
+ })
+ .then(function(cache_exists) {
+ assert_false(cache_exists,
+ 'Test setup failure: cache should not exist');
+ })
+ .then(function() { return self.caches.open(unpaired_name); })
+ .then(function() { return self.caches.keys(); })
+ .then(function(keys) {
+ assert_true(keys.indexOf(unpaired_name) !== -1,
+ 'keys should include cache with bad name');
+ })
+ .then(function() { return self.caches.has(unpaired_name); })
+ .then(function(cache_exists) {
+ assert_true(cache_exists,
+ 'CacheStorage names should be not be converted.');
+ })
+ .then(function() { return self.caches.has(converted_name); })
+ .then(function(cache_exists) {
+ assert_false(cache_exists,
+ 'CacheStorage names should be not be converted.');
+ });
+ }, 'CacheStorage names are DOMStrings not USVStrings');
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/common.https.html b/third_party/web_platform_tests/service-workers/cache-storage/common.https.html
deleted file mode 100644
index d5f7d24..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/common.https.html
+++ /dev/null
@@ -1,52 +0,0 @@
-<!DOCTYPE html>
-<title>Cache Storage: Verify that Window and Workers see same storage</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../resources/testharness-helpers.js"></script>
-<script>
-
-function wait_for_message(worker) {
- return new Promise(function(resolve) {
- worker.addEventListener('message', function listener(e) {
- resolve(e.data);
- worker.removeEventListener('message', listener);
- });
- });
-}
-
-promise_test(function(t) {
- var cache_name = 'common-test';
- return self.caches.delete(cache_name)
- .then(function() {
- var worker = new Worker('resources/common-worker.js');
- worker.postMessage({name: cache_name});
- return wait_for_message(worker);
- })
- .then(function(message) {
- return self.caches.open(cache_name);
- })
- .then(function(cache) {
- return Promise.all([
- cache.match('https://example.com/a'),
- cache.match('https://example.com/b'),
- cache.match('https://example.com/c')
- ]);
- })
- .then(function(responses) {
- return Promise.all(responses.map(
- function(response) { return response.text(); }
- ));
- })
- .then(function(bodies) {
- assert_equals(bodies[0], 'a',
- 'Body should match response put by worker');
- assert_equals(bodies[1], 'b',
- 'Body should match response put by worker');
- assert_equals(bodies[2], 'c',
- 'Body should match response put by worker');
- });
-}, 'Window sees cache puts by Worker');
-
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/common.https.window.js b/third_party/web_platform_tests/service-workers/cache-storage/common.https.window.js
new file mode 100644
index 0000000..eba312c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/common.https.window.js
@@ -0,0 +1,44 @@
+// META: title=Cache Storage: Verify that Window and Workers see same storage
+// META: timeout=long
+
+function wait_for_message(worker) {
+ return new Promise(function(resolve) {
+ worker.addEventListener('message', function listener(e) {
+ resolve(e.data);
+ worker.removeEventListener('message', listener);
+ });
+ });
+}
+
+promise_test(function(t) {
+ var cache_name = 'common-test';
+ return self.caches.delete(cache_name)
+ .then(function() {
+ var worker = new Worker('resources/common-worker.js');
+ worker.postMessage({name: cache_name});
+ return wait_for_message(worker);
+ })
+ .then(function(message) {
+ return self.caches.open(cache_name);
+ })
+ .then(function(cache) {
+ return Promise.all([
+ cache.match('https://example.com/a'),
+ cache.match('https://example.com/b'),
+ cache.match('https://example.com/c')
+ ]);
+ })
+ .then(function(responses) {
+ return Promise.all(responses.map(
+ function(response) { return response.text(); }
+ ));
+ })
+ .then(function(bodies) {
+ assert_equals(bodies[0], 'a',
+ 'Body should match response put by worker');
+ assert_equals(bodies[1], 'b',
+ 'Body should match response put by worker');
+ assert_equals(bodies[2], 'c',
+ 'Body should match response put by worker');
+ });
+}, 'Window sees cache puts by Worker');
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html b/third_party/web_platform_tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html
new file mode 100644
index 0000000..ec930a8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html class="test-wait">
+<meta charset="utf-8">
+<script type="module">
+ const cache = await window.caches.open('cache_name_0')
+ await cache.add("")
+ const resp1 = await cache.match("")
+ const readStream = resp1.body
+ // Cloning will open the stream via NS_AsyncCopy in Gecko
+ resp1.clone()
+ // Give a little bit of time
+ await new Promise(setTimeout)
+ // At this point the previous open operation is about to finish but not yet.
+ // It will finish after the second open operation is made, potentially causing incorrect state.
+ await readStream.getReader().read();
+ document.documentElement.classList.remove('test-wait')
+</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/credentials.https.html b/third_party/web_platform_tests/service-workers/cache-storage/credentials.https.html
new file mode 100644
index 0000000..0fe4a0a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/credentials.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Cache Storage: Verify credentials are respected by Cache operations</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-storage">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./../service-worker/resources/test-helpers.sub.js"></script>
+<style>iframe { display: none; }</style>
+<script>
+
+var worker = "./resources/credentials-worker.js";
+var scope = "./resources/credentials-iframe.html";
+promise_test(function(t) {
+ return self.caches.delete('credentials')
+ .then(function() {
+ return service_worker_unregister_and_register(t, worker, scope)
+ })
+ .then(function(reg) {
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ frame.contentWindow.postMessage([
+ {name: 'file.txt', username: 'aa', password: 'bb'},
+ {name: 'file.txt', username: 'cc', password: 'dd'},
+ {name: 'file.txt'}
+ ], '*');
+ return new Promise(function(resolve, reject) {
+ window.onmessage = t.step_func(function(e) {
+ resolve(e.data);
+ });
+ });
+ })
+ .then(function(data) {
+ assert_equals(data.length, 3, 'three entries should be present');
+ assert_equals(data.filter(function(url) { return /@/.test(url); }).length, 2,
+ 'two entries should contain credentials');
+ assert_true(data.some(function(url) { return /aa:bb@/.test(url); }),
+ 'entry with credentials aa:bb should be present');
+ assert_true(data.some(function(url) { return /cc:dd@/.test(url); }),
+ 'entry with credentials cc:dd should be present');
+ });
+}, "Cache API matching includes credentials");
+</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/cross-partition.https.tentative.html b/third_party/web_platform_tests/service-workers/cache-storage/cross-partition.https.tentative.html
new file mode 100644
index 0000000..1cfc256
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/cross-partition.https.tentative.html
@@ -0,0 +1,269 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<!-- Pull in executor_path needed by newPopup / newIframe -->
+<script src="/html/cross-origin-embedder-policy/credentialless/resources/common.js"></script>
+<!-- Pull in importScript / newPopup / newIframe -->
+<script src="/html/anonymous-iframe/resources/common.js"></script>
+<body>
+<script>
+
+const cache_exists_js = (cache_name, response_queue_name) => `
+ try {
+ const exists = await self.caches.has("${cache_name}");
+ if (exists) {
+ await send("${response_queue_name}", "true");
+ } else {
+ await send("${response_queue_name}", "false");
+ }
+ } catch {
+ await send("${response_queue_name}", "exception");
+ }
+`;
+
+const add_iframe_js = (iframe_origin, response_queue_uuid) => `
+ const importScript = ${importScript};
+ await importScript("/html/cross-origin-embedder-policy/credentialless" +
+ "/resources/common.js");
+ await importScript("/html/anonymous-iframe/resources/common.js");
+ await importScript("/common/utils.js");
+ await send("${response_queue_uuid}", newIframe("${iframe_origin}"));
+`;
+
+const same_site_origin = get_host_info().HTTPS_ORIGIN;
+const cross_site_origin = get_host_info().HTTPS_NOTSAMESITE_ORIGIN;
+
+async function create_test_iframes(t, response_queue_uuid) {
+
+ // Create a same-origin iframe in a cross-site popup.
+ const not_same_site_popup_uuid = newPopup(t, cross_site_origin);
+ await send(not_same_site_popup_uuid,
+ add_iframe_js(same_site_origin, response_queue_uuid));
+ const iframe_1_uuid = await receive(response_queue_uuid);
+
+ // Create a same-origin iframe in a same-site popup.
+ const same_origin_popup_uuid = newPopup(t, same_site_origin);
+ await send(same_origin_popup_uuid,
+ add_iframe_js(same_site_origin, response_queue_uuid));
+ const iframe_2_uuid = await receive(response_queue_uuid);
+
+ return [iframe_1_uuid, iframe_2_uuid];
+}
+
+promise_test(t => {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const response_queue_uuid = token();
+
+ const [iframe_1_uuid, iframe_2_uuid] =
+ await create_test_iframes(t, response_queue_uuid);
+
+ const cache_name = token();
+ await self.caches.open(cache_name);
+ t.add_cleanup(() => self.caches.delete(cache_name));
+
+ await send(iframe_2_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "true") {
+ reject("Cache not visible in same-top-level-site iframe");
+ }
+
+ await send(iframe_1_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "false") {
+ reject("Cache visible in not-same-top-level-site iframe");
+ }
+
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+}, "CacheStorage caches shouldn't be shared with a cross-partition iframe");
+
+const newWorker = (origin) => {
+ const worker_token = token();
+ const worker_url = origin + executor_worker_path + `&uuid=${worker_token}`;
+ const worker = new Worker(worker_url);
+ return worker_token;
+}
+
+promise_test(t => {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const response_queue_uuid = token();
+
+ const create_worker_js = (origin) => `
+ const importScript = ${importScript};
+ await importScript("/html/cross-origin-embedder-policy/credentialless" +
+ "/resources/common.js");
+ await importScript("/html/anonymous-iframe/resources/common.js");
+ await importScript("/common/utils.js");
+ const newWorker = ${newWorker};
+ await send("${response_queue_uuid}", newWorker("${origin}"));
+ `;
+
+ const [iframe_1_uuid, iframe_2_uuid] =
+ await create_test_iframes(t, response_queue_uuid);
+
+ // Create a dedicated worker in the cross-top-level-site iframe.
+ await send(iframe_1_uuid, create_worker_js(same_site_origin));
+ const worker_1_uuid = await receive(response_queue_uuid);
+
+ // Create a dedicated worker in the same-top-level-site iframe.
+ await send(iframe_2_uuid, create_worker_js(same_site_origin));
+ const worker_2_uuid = await receive(response_queue_uuid);
+
+ const cache_name = token();
+ await self.caches.open(cache_name);
+ t.add_cleanup(() => self.caches.delete(cache_name));
+
+ await send(worker_2_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "true") {
+ reject("Cache not visible in same-top-level-site worker");
+ }
+
+ await send(worker_1_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "false") {
+ reject("Cache visible in not-same-top-level-site worker");
+ }
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+}, "CacheStorage caches shouldn't be shared with a cross-partition dedicated worker");
+
+const newSharedWorker = (origin) => {
+ const worker_token = token();
+ const worker_url = origin + executor_worker_path + `&uuid=${worker_token}`;
+ const worker = new SharedWorker(worker_url, worker_token);
+ return worker_token;
+}
+
+promise_test(t => {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const response_queue_uuid = token();
+
+ const create_worker_js = (origin) => `
+ const importScript = ${importScript};
+ await importScript("/html/cross-origin-embedder-policy/credentialless" +
+ "/resources/common.js");
+ await importScript("/html/anonymous-iframe/resources/common.js");
+ await importScript("/common/utils.js");
+ const newSharedWorker = ${newSharedWorker};
+ await send("${response_queue_uuid}", newSharedWorker("${origin}"));
+ `;
+
+ const [iframe_1_uuid, iframe_2_uuid] =
+ await create_test_iframes(t, response_queue_uuid);
+
+ // Create a shared worker in the cross-top-level-site iframe.
+ await send(iframe_1_uuid, create_worker_js(same_site_origin));
+ const worker_1_uuid = await receive(response_queue_uuid);
+
+ // Create a shared worker in the same-top-level-site iframe.
+ await send(iframe_2_uuid, create_worker_js(same_site_origin));
+ const worker_2_uuid = await receive(response_queue_uuid);
+
+ const cache_name = token();
+ await self.caches.open(cache_name);
+ t.add_cleanup(() => self.caches.delete(cache_name));
+
+ await send(worker_2_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "true") {
+ reject("Cache not visible in same-top-level-site worker");
+ }
+
+ await send(worker_1_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "false") {
+ reject("Cache visible in not-same-top-level-site worker");
+ }
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+}, "CacheStorage caches shouldn't be shared with a cross-partition shared worker");
+
+const newServiceWorker = async (origin) => {
+ const worker_token = token();
+ const worker_url = origin + executor_service_worker_path +
+ `&uuid=${worker_token}`;
+ const worker_url_path = executor_service_worker_path.substring(0,
+ executor_service_worker_path.lastIndexOf('/'));
+ const scope = worker_url_path + "/not-used/";
+ const reg = await navigator.serviceWorker.register(worker_url,
+ {'scope': scope});
+ return worker_token;
+}
+
+promise_test(t => {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const response_queue_uuid = token();
+
+ const create_worker_js = (origin) => `
+ const importScript = ${importScript};
+ await importScript("/html/cross-origin-embedder-policy/credentialless" +
+ "/resources/common.js");
+ await importScript("/html/anonymous-iframe/resources/common.js");
+ await importScript("/common/utils.js");
+ const newServiceWorker = ${newServiceWorker};
+ await send("${response_queue_uuid}", await newServiceWorker("${origin}"));
+ `;
+
+ const [iframe_1_uuid, iframe_2_uuid] =
+ await create_test_iframes(t, response_queue_uuid);
+
+ // Create a service worker in the same-top-level-site iframe.
+ await send(iframe_2_uuid, create_worker_js(same_site_origin));
+ const worker_2_uuid = await receive(response_queue_uuid);
+
+ t.add_cleanup(() =>
+ send(worker_2_uuid, "self.registration.unregister();"));
+
+ const cache_name = token();
+ await self.caches.open(cache_name);
+ t.add_cleanup(() => self.caches.delete(cache_name));
+
+ await send(worker_2_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "true") {
+ reject("Cache not visible in same-top-level-site worker");
+ }
+
+ // Create a service worker in the cross-top-level-site iframe. Note that
+ // if service workers are unpartitioned then this new service worker would
+ // replace the one created above. This is why we wait to create the second
+ // service worker until after we are done with the first one.
+ await send(iframe_1_uuid, create_worker_js(same_site_origin));
+ const worker_1_uuid = await receive(response_queue_uuid);
+
+ t.add_cleanup(() =>
+ send(worker_1_uuid, "self.registration.unregister();"));
+
+ await send(worker_1_uuid,
+ cache_exists_js(cache_name, response_queue_uuid));
+ if (await receive(response_queue_uuid) !== "false") {
+ reject("Cache visible in not-same-top-level-site worker");
+ }
+
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+}, "CacheStorage caches shouldn't be shared with a cross-partition service worker");
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js b/third_party/web_platform_tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js
new file mode 100644
index 0000000..ee574d2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js
@@ -0,0 +1,22 @@
+self.addEventListener('fetch', (event) => {
+ const params = new URL(event.request.url).searchParams;
+ if (params.has('ignore')) {
+ return;
+ }
+ if (!params.has('name')) {
+ event.respondWith(Promise.reject(TypeError('No name is provided.')));
+ return;
+ }
+
+ event.respondWith(Promise.resolve().then(async () => {
+ const name = params.get('name');
+ await caches.delete('foo');
+ const cache = await caches.open('foo');
+ await cache.put(event.request, new Response('hello'));
+ const keys = await cache.keys();
+
+ const original = event.request[name];
+ const stored = keys[0][name];
+ return new Response(`original: ${original}, stored: ${stored}`);
+ }));
+ });
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/resources/credentials-iframe.html b/third_party/web_platform_tests/service-workers/cache-storage/resources/credentials-iframe.html
new file mode 100644
index 0000000..00702df
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/resources/credentials-iframe.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Controlled frame for Cache API test with credentials</title>
+<script>
+
+function xhr(url, username, password) {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest(), async = true;
+ xhr.open('GET', url, async, username, password);
+ xhr.send();
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState !== XMLHttpRequest.DONE)
+ return;
+ if (xhr.status === 200) {
+ resolve(xhr.responseText);
+ } else {
+ reject(new Error(xhr.statusText));
+ }
+ };
+ });
+}
+
+window.onmessage = function(e) {
+ Promise.all(e.data.map(function(item) {
+ return xhr(item.name, item.username, item.password);
+ }))
+ .then(function() {
+ navigator.serviceWorker.controller.postMessage('keys');
+ navigator.serviceWorker.onmessage = function(e) {
+ window.parent.postMessage(e.data, '*');
+ };
+ });
+};
+
+</script>
+<body>
+Hello? Yes, this is iframe.
+</body>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/resources/credentials-worker.js b/third_party/web_platform_tests/service-workers/cache-storage/resources/credentials-worker.js
new file mode 100644
index 0000000..43965b5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/resources/credentials-worker.js
@@ -0,0 +1,59 @@
+var cache_name = 'credentials';
+
+function assert_equals(actual, expected, message) {
+ if (!Object.is(actual, expected))
+ throw Error(message + ': expected: ' + expected + ', actual: ' + actual);
+}
+
+self.onfetch = function(e) {
+ if (!/\.txt$/.test(e.request.url)) return;
+ var content = e.request.url;
+ var cache;
+ e.respondWith(
+ self.caches.open(cache_name)
+ .then(function(result) {
+ cache = result;
+ return cache.put(e.request, new Response(content));
+ })
+
+ .then(function() { return cache.match(e.request); })
+ .then(function(result) { return result.text(); })
+ .then(function(text) {
+ assert_equals(text, content, 'Cache.match() body should match');
+ })
+
+ .then(function() { return cache.matchAll(e.request); })
+ .then(function(results) {
+ assert_equals(results.length, 1, 'Should have one response');
+ return results[0].text();
+ })
+ .then(function(text) {
+ assert_equals(text, content, 'Cache.matchAll() body should match');
+ })
+
+ .then(function() { return self.caches.match(e.request); })
+ .then(function(result) { return result.text(); })
+ .then(function(text) {
+ assert_equals(text, content, 'CacheStorage.match() body should match');
+ })
+
+ .then(function() {
+ return new Response('dummy');
+ })
+ );
+};
+
+self.onmessage = function(e) {
+ if (e.data === 'keys') {
+ self.caches.open(cache_name)
+ .then(function(cache) { return cache.keys(); })
+ .then(function(requests) {
+ var urls = requests.map(function(request) { return request.url; });
+ self.clients.matchAll().then(function(clients) {
+ clients.forEach(function(client) {
+ client.postMessage(urls);
+ });
+ });
+ });
+ }
+};
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/resources/fetch-status.py b/third_party/web_platform_tests/service-workers/cache-storage/resources/fetch-status.py
index 71f13eb..b7109f4 100644
--- a/third_party/web_platform_tests/service-workers/cache-storage/resources/fetch-status.py
+++ b/third_party/web_platform_tests/service-workers/cache-storage/resources/fetch-status.py
@@ -1,2 +1,2 @@
def main(request, response):
- return int(request.GET["status"]), [], ""
+ return int(request.GET[b"status"]), [], b""
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/resources/test-helpers.js b/third_party/web_platform_tests/service-workers/cache-storage/resources/test-helpers.js
index 9111095..050ac0b 100644
--- a/third_party/web_platform_tests/service-workers/cache-storage/resources/test-helpers.js
+++ b/third_party/web_platform_tests/service-workers/cache-storage/resources/test-helpers.js
@@ -32,6 +32,241 @@
function cache_test(test_function, description) {
promise_test(function(test) {
return create_temporary_cache(test)
- .then(test_function);
+ .then(function(cache) { return test_function(cache, test); });
}, description);
}
+
+// A set of Request/Response pairs to be used with prepopulated_cache_test().
+var simple_entries = [
+ {
+ name: 'a',
+ request: new Request('http://example.com/a'),
+ response: new Response('')
+ },
+
+ {
+ name: 'b',
+ request: new Request('http://example.com/b'),
+ response: new Response('')
+ },
+
+ {
+ name: 'a_with_query',
+ request: new Request('http://example.com/a?q=r'),
+ response: new Response('')
+ },
+
+ {
+ name: 'A',
+ request: new Request('http://example.com/A'),
+ response: new Response('')
+ },
+
+ {
+ name: 'a_https',
+ request: new Request('https://example.com/a'),
+ response: new Response('')
+ },
+
+ {
+ name: 'a_org',
+ request: new Request('http://example.org/a'),
+ response: new Response('')
+ },
+
+ {
+ name: 'cat',
+ request: new Request('http://example.com/cat'),
+ response: new Response('')
+ },
+
+ {
+ name: 'catmandu',
+ request: new Request('http://example.com/catmandu'),
+ response: new Response('')
+ },
+
+ {
+ name: 'cat_num_lives',
+ request: new Request('http://example.com/cat?lives=9'),
+ response: new Response('')
+ },
+
+ {
+ name: 'cat_in_the_hat',
+ request: new Request('http://example.com/cat/in/the/hat'),
+ response: new Response('')
+ },
+
+ {
+ name: 'non_2xx_response',
+ request: new Request('http://example.com/non2xx'),
+ response: new Response('', {status: 404, statusText: 'nope'})
+ },
+
+ {
+ name: 'error_response',
+ request: new Request('http://example.com/error'),
+ response: Response.error()
+ },
+];
+
+// A set of Request/Response pairs to be used with prepopulated_cache_test().
+// These contain a mix of test cases that use Vary headers.
+var vary_entries = [
+ {
+ name: 'vary_cookie_is_cookie',
+ request: new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-for-cookie'}}),
+ response: new Response('',
+ {headers: {'Vary': 'Cookies'}})
+ },
+
+ {
+ name: 'vary_cookie_is_good',
+ request: new Request('http://example.com/c',
+ {headers: {'Cookies': 'is-good-enough-for-me'}}),
+ response: new Response('',
+ {headers: {'Vary': 'Cookies'}})
+ },
+
+ {
+ name: 'vary_cookie_absent',
+ request: new Request('http://example.com/c'),
+ response: new Response('',
+ {headers: {'Vary': 'Cookies'}})
+ }
+];
+
+// Run |test_function| with a Cache object and a map of entries. Prior to the
+// call, the Cache is populated by cache entries from |entries|. The latter is
+// expected to be an Object mapping arbitrary keys to objects of the form
+// {request: <Request object>, response: <Response object>}. Entries are
+// serially added to the cache in the order specified.
+//
+// |test_function| should return a Promise that can be used with promise_test.
+function prepopulated_cache_test(entries, test_function, description) {
+ cache_test(function(cache) {
+ var p = Promise.resolve();
+ var hash = {};
+ entries.forEach(function(entry) {
+ hash[entry.name] = entry;
+ p = p.then(function() {
+ return cache.put(entry.request.clone(), entry.response.clone())
+ .catch(function(e) {
+ assert_unreached(
+ 'Test setup failed for entry ' + entry.name + ': ' + e
+ );
+ });
+ });
+ });
+ return p
+ .then(function() {
+ assert_equals(Object.keys(hash).length, entries.length);
+ })
+ .then(function() {
+ return test_function(cache, hash);
+ });
+ }, description);
+}
+
+// Helper for testing with Headers objects. Compares Headers instances
+// by serializing |expected| and |actual| to arrays and comparing.
+function assert_header_equals(actual, expected, description) {
+ assert_class_string(actual, "Headers", description);
+ var header;
+ var actual_headers = [];
+ var expected_headers = [];
+ for (header of actual)
+ actual_headers.push(header[0] + ": " + header[1]);
+ for (header of expected)
+ expected_headers.push(header[0] + ": " + header[1]);
+ assert_array_equals(actual_headers, expected_headers,
+ description + " Headers differ.");
+}
+
+// Helper for testing with Response objects. Compares simple
+// attributes defined on the interfaces, as well as the headers. It
+// does not compare the response bodies.
+function assert_response_equals(actual, expected, description) {
+ assert_class_string(actual, "Response", description);
+ ["type", "url", "status", "ok", "statusText"].forEach(function(attribute) {
+ assert_equals(actual[attribute], expected[attribute],
+ description + " Attributes differ: " + attribute + ".");
+ });
+ assert_header_equals(actual.headers, expected.headers, description);
+}
+
+// Assert that the two arrays |actual| and |expected| contain the same
+// set of Responses as determined by assert_response_equals. The order
+// is not significant.
+//
+// |expected| is assumed to not contain any duplicates.
+function assert_response_array_equivalent(actual, expected, description) {
+ assert_true(Array.isArray(actual), description);
+ assert_equals(actual.length, expected.length, description);
+ expected.forEach(function(expected_element) {
+ // assert_response_in_array treats the first argument as being
+ // 'actual', and the second as being 'expected array'. We are
+ // switching them around because we want to be resilient
+ // against the |actual| array containing duplicates.
+ assert_response_in_array(expected_element, actual, description);
+ });
+}
+
+// Asserts that two arrays |actual| and |expected| contain the same
+// set of Responses as determined by assert_response_equals(). The
+// corresponding elements must occupy corresponding indices in their
+// respective arrays.
+function assert_response_array_equals(actual, expected, description) {
+ assert_true(Array.isArray(actual), description);
+ assert_equals(actual.length, expected.length, description);
+ actual.forEach(function(value, index) {
+ assert_response_equals(value, expected[index],
+ description + " : object[" + index + "]");
+ });
+}
+
+// Equivalent to assert_in_array, but uses assert_response_equals.
+function assert_response_in_array(actual, expected_array, description) {
+ assert_true(expected_array.some(function(element) {
+ try {
+ assert_response_equals(actual, element);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }), description);
+}
+
+// Helper for testing with Request objects. Compares simple
+// attributes defined on the interfaces, as well as the headers.
+function assert_request_equals(actual, expected, description) {
+ assert_class_string(actual, "Request", description);
+ ["url"].forEach(function(attribute) {
+ assert_equals(actual[attribute], expected[attribute],
+ description + " Attributes differ: " + attribute + ".");
+ });
+ assert_header_equals(actual.headers, expected.headers, description);
+}
+
+// Asserts that two arrays |actual| and |expected| contain the same
+// set of Requests as determined by assert_request_equals(). The
+// corresponding elements must occupy corresponding indices in their
+// respective arrays.
+function assert_request_array_equals(actual, expected, description) {
+ assert_true(Array.isArray(actual), description);
+ assert_equals(actual.length, expected.length, description);
+ actual.forEach(function(value, index) {
+ assert_request_equals(value, expected[index],
+ description + " : object[" + index + "]");
+ });
+}
+
+// Deletes all caches, returning a promise indicating success.
+function delete_all_caches() {
+ return self.caches.keys()
+ .then(function(keys) {
+ return Promise.all(keys.map(self.caches.delete.bind(self.caches)));
+ });
+}
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/resources/testharness-helpers.js b/third_party/web_platform_tests/service-workers/cache-storage/resources/testharness-helpers.js
deleted file mode 100644
index b4a0c27..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/resources/testharness-helpers.js
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * testharness-helpers contains various useful extensions to testharness.js to
- * allow them to be used across multiple tests before they have been
- * upstreamed. This file is intended to be usable from both document and worker
- * environments, so code should for example not rely on the DOM.
- */
-
-// Returns a promise that fulfills after the provided |promise| is fulfilled.
-// The |test| succeeds only if |promise| rejects with an exception matching
-// |code|. Accepted values for |code| follow those accepted for assert_throws().
-// The optional |description| describes the test being performed.
-//
-// E.g.:
-// assert_promise_rejects(
-// new Promise(...), // something that should throw an exception.
-// 'NotFoundError',
-// 'Should throw NotFoundError.');
-//
-// assert_promise_rejects(
-// new Promise(...),
-// new TypeError(),
-// 'Should throw TypeError');
-function assert_promise_rejects(promise, code, description) {
- return promise.then(
- function() {
- throw 'assert_promise_rejects: ' + description + ' Promise did not reject.';
- },
- function(e) {
- if (code !== undefined) {
- assert_throws(code, function() { throw e; }, description);
- }
- });
-}
-
-// Helper for testing with Headers objects. Compares Headers instances
-// by serializing |expected| and |actual| to arrays and comparing.
-function assert_header_equals(actual, expected, description) {
- assert_class_string(actual, "Headers", description);
- var header, actual_headers = [], expected_headers = [];
- for (header of actual)
- actual_headers.push(header[0] + ": " + header[1]);
- for (header of expected)
- expected_headers.push(header[0] + ": " + header[1]);
- assert_array_equals(actual_headers, expected_headers,
- description + " Headers differ.");
-}
-
-// Helper for testing with Response objects. Compares simple
-// attributes defined on the interfaces, as well as the headers. It
-// does not compare the response bodies.
-function assert_response_equals(actual, expected, description) {
- return Promise.all([actual.clone().text(), expected.clone().text()]).then(bodies => {
- assert_equals(bodies[0], bodies[1]);
- });
- // assert_class_string(actual, "Response", description);
- // ["type", "url", "status", "ok", "statusText"].forEach(function(attribute) {
- // assert_equals(actual[attribute], expected[attribute],
- // description + " Attributes differ: " + attribute + ".");
- // });
- // assert_header_equals(actual.headers, expected.headers, description);
-}
-
-// Assert that the two arrays |actual| and |expected| contain the same
-// set of Responses as determined by assert_response_equals. The order
-// is not significant.
-//
-// |expected| is assumed to not contain any duplicates.
-function assert_response_array_equivalent(actual, expected, description) {
- assert_true(Array.isArray(actual), description);
- assert_equals(actual.length, expected.length, description);
- expected.forEach(function(expected_element) {
- // assert_response_in_array treats the first argument as being
- // 'actual', and the second as being 'expected array'. We are
- // switching them around because we want to be resilient
- // against the |actual| array containing duplicates.
- assert_response_in_array(expected_element, actual, description);
- });
-}
-
-// Asserts that two arrays |actual| and |expected| contain the same
-// set of Responses as determined by assert_response_equals(). The
-// corresponding elements must occupy corresponding indices in their
-// respective arrays.
-function assert_response_array_equals(actual, expected, description) {
- assert_true(Array.isArray(actual), description);
- assert_equals(actual.length, expected.length, description);
- actual.forEach(function(value, index) {
- assert_response_equals(value, expected[index],
- description + " : object[" + index + "]");
- });
-}
-
-// Equivalent to assert_in_array, but uses assert_response_equals.
-function assert_response_in_array(actual, expected_array, description) {
- assert_true(expected_array.some(function(element) {
- try {
- assert_response_equals(actual, element);
- return true;
- } catch (e) {
- return false;
- }
- }), description);
-}
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/resources/vary.py b/third_party/web_platform_tests/service-workers/cache-storage/resources/vary.py
new file mode 100644
index 0000000..7fde1b1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/resources/vary.py
@@ -0,0 +1,25 @@
+def main(request, response):
+ if b"clear-vary-value-override-cookie" in request.GET:
+ response.unset_cookie(b"vary-value-override")
+ return b"vary cookie cleared"
+
+ set_cookie_vary = request.GET.first(b"set-vary-value-override-cookie",
+ default=b"")
+ if set_cookie_vary:
+ response.set_cookie(b"vary-value-override", set_cookie_vary)
+ return b"vary cookie set"
+
+ # If there is a vary-value-override cookie set, then use its value
+ # for the VARY header no matter what the query string is set to. This
+ # override is necessary to test the case when two URLs are identical
+ # (including query), but differ by VARY header.
+ cookie_vary = request.cookies.get(b"vary-value-override")
+ if cookie_vary:
+ response.headers.set(b"vary", str(cookie_vary))
+ else:
+ # If there is no cookie, then use the query string value, if present.
+ query_vary = request.GET.first(b"vary", default=b"")
+ if query_vary:
+ response.headers.set(b"vary", query_vary)
+
+ return b"vary response"
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/sandboxed-iframes.https.html b/third_party/web_platform_tests/service-workers/cache-storage/sandboxed-iframes.https.html
new file mode 100644
index 0000000..098fa89
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/cache-storage/sandboxed-iframes.https.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<title>Cache Storage: Verify access in sandboxed iframes</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-storage">
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+function load_iframe(src, sandbox) {
+ return new Promise(function(resolve, reject) {
+ var iframe = document.createElement('iframe');
+ iframe.onload = function() { resolve(iframe); };
+
+ iframe.sandbox = sandbox;
+ iframe.src = src;
+
+ document.documentElement.appendChild(iframe);
+ });
+}
+
+function wait_for_message(id) {
+ return new Promise(function(resolve) {
+ self.addEventListener('message', function listener(e) {
+ if (e.data.id === id) {
+ resolve(e.data);
+ self.removeEventListener('message', listener);
+ }
+ });
+ });
+}
+
+var counter = 0;
+
+promise_test(function(t) {
+ return load_iframe('./resources/iframe.html',
+ 'allow-scripts allow-same-origin')
+ .then(function(iframe) {
+ var id = ++counter;
+ iframe.contentWindow.postMessage({id: id}, '*');
+ return wait_for_message(id);
+ })
+ .then(function(message) {
+ assert_equals(
+ message.result, 'allowed',
+ 'Access should be allowed if sandbox has allow-same-origin');
+ });
+}, 'Sandboxed iframe with allow-same-origin is allowed access');
+
+promise_test(function(t) {
+ return load_iframe('./resources/iframe.html',
+ 'allow-scripts')
+ .then(function(iframe) {
+ var id = ++counter;
+ iframe.contentWindow.postMessage({id: id}, '*');
+ return wait_for_message(id);
+ })
+ .then(function(message) {
+ assert_equals(
+ message.result, 'denied',
+ 'Access should be denied if sandbox lacks allow-same-origin');
+ assert_equals(message.name, 'SecurityError',
+ 'Failure should be a SecurityError');
+ });
+}, 'Sandboxed iframe without allow-same-origin is denied access');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-add.js b/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-add.js
deleted file mode 100644
index 69ca447..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-add.js
+++ /dev/null
@@ -1,138 +0,0 @@
-if (self.importScripts) {
- importScripts('/resources/testharness.js');
- importScripts('../resources/testharness-helpers.js');
- importScripts('../resources/test-helpers.js');
-}
-
-// TODO(b/250611661): implement complete Cache API and adhere to web spec. Once
-// complete, enable the tests commented out.
-
-// cache_test(function(cache) {
-// return assert_promise_rejects(
-// cache.add(),
-// new TypeError(),
-// 'Cache.add should throw a TypeError when no arguments are given.');
-// }, 'Cache.add called with no arguments');
-
-cache_test(function(cache) {
- return cache.add('../resources/simple.txt')
- .then(function(result) {
- assert_equals(result, undefined,
- 'Cache.add should resolve with undefined on success.');
- });
- }, 'Cache.add called with relative URL specified as a string');
-
-// cache_test(function(cache) {
-// return assert_promise_rejects(
-// cache.add('javascript://this-is-not-http-mmkay'),
-// new TypeError(),
-// 'Cache.add should throw a TypeError for non-HTTP/HTTPS URLs.');
-// }, 'Cache.add called with non-HTTP/HTTPS URL');
-
-cache_test(function(cache) {
- var request = new Request('../resources/simple.txt');
- return cache.add(request)
- .then(function(result) {
- assert_equals(result, undefined,
- 'Cache.add should resolve with undefined on success.');
- });
- }, 'Cache.add called with Request object');
-
-cache_test(function(cache) {
- var request = new Request('../resources/simple.txt');
- return cache.add(request)
- .then(function(result) {
- assert_equals(result, undefined,
- 'Cache.add should resolve with undefined on success.');
- })
- .then(function() {
- return cache.add(request);
- })
- .then(function(result) {
- assert_equals(result, undefined,
- 'Cache.add should resolve with undefined on success.');
- });
- }, 'Cache.add called twice with the same Request object');
-
-cache_test(function(cache) {
- return cache.add('this-does-not-exist-please-dont-create-it')
- .then(function(result) {
- assert_equals(result, undefined,
- 'Cache.add should resolve with undefined on success.');
- });
- }, 'Cache.add with request that results in a status of 404');
-
-// cache_test(function(cache) {
-// return cache.add('../resources/fetch-status.py?status=500')
-// .then(function(result) {
-// assert_equals(result, undefined,
-// 'Cache.add should resolve with undefined on success.');
-// });
-// }, 'Cache.add with request that results in a status of 500');
-
-// cache_test(function(cache) {
-// return assert_promise_rejects(
-// cache.addAll(),
-// new TypeError(),
-// 'Cache.addAll with no arguments should throw TypeError.');
-// }, 'Cache.addAll with no arguments');
-
-// cache_test(function(cache) {
-// // Assumes the existence of ../resources/simple.txt and ../resources/blank.html
-// var urls = ['../resources/simple.txt', undefined, '../resources/blank.html'];
-// return assert_promise_rejects(
-// cache.addAll(),
-// new TypeError(),
-// 'Cache.addAll should throw TypeError for an undefined argument.');
-// }, 'Cache.addAll with a mix of valid and undefined arguments');
-
-// cache_test(function(cache) {
-// // Assumes the existence of ../resources/simple.txt and ../resources/blank.html
-// var urls = ['../resources/simple.txt', self.location.href, '../resources/blank.html'];
-// return cache.addAll(urls)
-// .then(function(result) {
-// assert_equals(result, undefined,
-// 'Cache.addAll should resolve with undefined on ' +
-// 'success.');
-// });
-// }, 'Cache.addAll with string URL arguments');
-
-// cache_test(function(cache) {
-// // Assumes the existence of ../resources/simple.txt and ../resources/blank.html
-// var urls = ['../resources/simple.txt', self.location.href, '../resources/blank.html'];
-// var requests = urls.map(function(url) {
-// return new Request(url);
-// });
-// return cache.addAll(requests)
-// .then(function(result) {
-// assert_equals(result, undefined,
-// 'Cache.addAll should resolve with undefined on ' +
-// 'success.');
-// });
-// }, 'Cache.addAll with Request arguments');
-
-// cache_test(function(cache) {
-// // Assumes that ../resources/simple.txt and ../resources/blank.html exist. The second
-// // resource does not.
-// var urls = ['../resources/simple.txt', 'this-resource-should-not-exist', '../resources/blank.html'];
-// var requests = urls.map(function(url) {
-// return new Request(url);
-// });
-// return cache.addAll(requests)
-// .then(function(result) {
-// assert_equals(result, undefined,
-// 'Cache.addAll should resolve with undefined on ' +
-// 'success.');
-// });
-// }, 'Cache.addAll with a mix of succeeding and failing requests');
-
-// cache_test(function(cache) {
-// var request = new Request('../resources/simple.txt');
-// return assert_promise_rejects(
-// cache.addAll([request, request]),
-// 'InvalidStateError',
-// 'Cache.addAll should throw InvalidStateError if the same request is added ' +
-// 'twice.');
-// }, 'Cache.addAll called with the same Request object specified twice');
-
-done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-delete.js b/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-delete.js
deleted file mode 100644
index badce08..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-delete.js
+++ /dev/null
@@ -1,99 +0,0 @@
-if (self.importScripts) {
- importScripts('/resources/testharness.js');
- importScripts('../resources/testharness-helpers.js');
- importScripts('../resources/test-helpers.js');
-}
-
-// TODO(b/250611661): implement complete Cache API and adhere to web spec. Once
-// complete, enable the tests commented out.
-
-var test_url = 'https://example.com/foo';
-
-// Construct a generic Request object. The URL is |test_url|. All other fields
-// are defaults.
-function new_test_request() {
- return new Request(test_url);
-}
-
-// Construct a generic Response object.
-function new_test_response() {
- return new Response('Hello world!', { status: 200 });
-}
-
-// cache_test(function(cache) {
-// return assert_promise_rejects(
-// cache.delete(),
-// new TypeError(),
-// 'Cache.delete should reject with a TypeError when called with no ' +
-// 'arguments.');
-// }, 'Cache.delete with no arguments');
-
-cache_test(function(cache) {
- return cache.put(new_test_request(), new_test_response())
- .then(function() {
- return cache.delete(test_url);
- })
- .then(function(result) {
- assert_true(result,
- 'Cache.delete should resolve with "true" if an entry ' +
- 'was successfully deleted.');
- return cache.match(test_url);
- })
- .then(function(result) {
- assert_equals(result, undefined,
- 'Cache.delete should remove matching entries from cache.');
- });
- }, 'Cache.delete called with a string URL');
-
-cache_test(function(cache) {
- var request = new Request(test_url);
- return cache.put(request, new_test_response())
- .then(function() {
- return cache.delete(request);
- })
- .then(function(result) {
- assert_true(result,
- 'Cache.delete should resolve with "true" if an entry ' +
- 'was successfully deleted.');
- });
- }, 'Cache.delete called with a Request object');
-
-cache_test(function(cache) {
- return cache.delete(test_url)
- .then(function(result) {
- assert_false(result,
- 'Cache.delete should resolve with "false" if there ' +
- 'are no matching entries.');
- });
- }, 'Cache.delete with a non-existent entry');
-
-var cache_entries = {
- a: {
- request: new Request('http://example.com/abc'),
- response: new Response('')
- },
-
- b: {
- request: new Request('http://example.com/b'),
- response: new Response('')
- },
-
- a_with_query: {
- request: new Request('http://example.com/abc?q=r'),
- response: new Response('')
- }
-};
-
-function prepopulated_cache_test(test_function, description) {
- cache_test(function(cache) {
- return Promise.all(Object.keys(cache_entries).map(function(k) {
- return cache.put(cache_entries[k].request.clone(),
- cache_entries[k].response.clone());
- }))
- .then(function() {
- return test_function(cache);
- });
- }, description);
-}
-
-done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-match.js b/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-match.js
deleted file mode 100644
index c0ccb16..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-match.js
+++ /dev/null
@@ -1,446 +0,0 @@
-if (self.importScripts) {
- importScripts('/resources/testharness.js');
- importScripts('../resources/testharness-helpers.js');
- importScripts('../resources/test-helpers.js');
-}
-
-// TODO(b/250611661): implement complete Cache API and adhere to web spec. Once
-// complete, enable the tests commented out. Consider caching a response with an
-// empty body.
-
-// A set of Request/Response pairs to be used with prepopulated_cache_test().
-var simple_entries = [
- {
- name: 'a',
- request: new Request('http://example.com/a'),
- response: new Response('a')
- },
-
- {
- name: 'b',
- request: new Request('http://example.com/b'),
- response: new Response('b')
- },
-
- {
- name: 'a_with_query',
- request: new Request('http://example.com/a?q=r'),
- response: new Response('a?q=r')
- },
-
- {
- name: 'A',
- request: new Request('http://example.com/A'),
- response: new Response('A')
- },
-
- {
- name: 'a_https',
- request: new Request('https://example.com/a'),
- response: new Response('a')
- },
-
- {
- name: 'a_org',
- request: new Request('http://example.org/a'),
- response: new Response('a')
- },
-
- {
- name: 'cat',
- request: new Request('http://example.com/cat'),
- response: new Response('cat')
- },
-
- {
- name: 'catmandu',
- request: new Request('http://example.com/catmandu'),
- response: new Response('catmandu')
- },
-
- {
- name: 'cat_num_lives',
- request: new Request('http://example.com/cat?lives=9'),
- response: new Response('cat?lives=9')
- },
-
- {
- name: 'cat_in_the_hat',
- request: new Request('http://example.com/cat/in/the/hat'),
- response: new Response('cat/in/the/hat')
- }
-];
-
-// A set of Request/Response pairs to be used with prepopulated_cache_test().
-// These contain a mix of test cases that use Vary headers.
-var vary_entries = [
- {
- name: 'vary_cookie_is_cookie',
- request: new Request('http://example.com/c',
- {headers: {'Cookies': 'is-for-cookie'}}),
- response: new Response('c',
- {headers: {'Vary': 'Cookies'}})
- },
-
- {
- name: 'vary_cookie_is_good',
- request: new Request('http://example.com/c',
- {headers: {'Cookies': 'is-good-enough-for-me'}}),
- response: new Response('c',
- {headers: {'Vary': 'Cookies'}})
- },
-
- {
- name: 'vary_cookie_absent',
- request: new Request('http://example.com/c'),
- response: new Response('c',
- {headers: {'Vary': 'Cookies'}})
- }
-];
-
-// prepopulated_cache_test(simple_entries, function(cache, entries) {
-// return cache.matchAll('not-present-in-the-cache')
-// .then(function(result) {
-// assert_response_array_equivalent(
-// result, [],
-// 'Cache.matchAll should resolve with an empty array on failure.');
-// });
-// }, 'Cache.matchAll with no matching entries');
-
-prepopulated_cache_test(simple_entries, function(cache, entries) {
- return cache.match('not-present-in-the-cache')
- .then(function(result) {
- assert_equals(result, undefined,
- 'Cache.match failures should resolve with undefined.');
- });
- }, 'Cache.match with no matching entries');
-
-// prepopulated_cache_test(simple_entries, function(cache, entries) {
-// return cache.matchAll(entries.a.request.url)
-// .then(function(result) {
-// assert_response_array_equals(result, [entries.a.response],
-// 'Cache.matchAll should match by URL.');
-// });
-// }, 'Cache.matchAll with URL');
-
-prepopulated_cache_test(simple_entries, function(cache, entries) {
- return cache.match(entries.a.request.url)
- .then(function(result) {
- assert_response_equals(result, entries.a.response,
- 'Cache.match should match by URL.');
- });
- }, 'Cache.match with URL');
-
-// prepopulated_cache_test(simple_entries, function(cache, entries) {
-// return cache.matchAll(entries.a.request)
-// .then(function(result) {
-// assert_response_array_equals(
-// result, [entries.a.response],
-// 'Cache.matchAll should match by Request.');
-// });
-// }, 'Cache.matchAll with Request');
-
-prepopulated_cache_test(simple_entries, function(cache, entries) {
- return cache.match(entries.a.request)
- .then(function(result) {
- assert_response_equals(result, entries.a.response,
- 'Cache.match should match by Request.');
- });
- }, 'Cache.match with Request');
-
-// prepopulated_cache_test(simple_entries, function(cache, entries) {
-// return cache.matchAll(new Request(entries.a.request.url))
-// .then(function(result) {
-// assert_response_array_equals(
-// result, [entries.a.response],
-// 'Cache.matchAll should match by Request.');
-// });
-// }, 'Cache.matchAll with new Request');
-
-prepopulated_cache_test(simple_entries, function(cache, entries) {
- return cache.match(new Request(entries.a.request.url))
- .then(function(result) {
- assert_response_equals(result, entries.a.response,
- 'Cache.match should match by Request.');
- });
- }, 'Cache.match with new Request');
-
-// prepopulated_cache_test(simple_entries, function(cache, entries) {
-// return cache.matchAll(entries.a.request,
-// {ignoreSearch: true})
-// .then(function(result) {
-// assert_response_array_equivalent(
-// result,
-// [
-// entries.a.response,
-// entries.a_with_query.response
-// ],
-// 'Cache.matchAll with ignoreSearch should ignore the ' +
-// 'search parameters of cached request.');
-// });
-// },
-// 'Cache.matchAll with ignoreSearch option (request with no search ' +
-// 'parameters)');
-
-prepopulated_cache_test(simple_entries, function(cache, entries) {
- return cache.match(entries.a.request,
- {ignoreSearch: true})
- .then(function(result) {
- assert_response_in_array(
- result,
- [
- entries.a.response,
- entries.a_with_query.response
- ],
- 'Cache.match with ignoreSearch should ignore the ' +
- 'search parameters of cached request.');
- });
- },
- 'Cache.match with ignoreSearch option (request with no search ' +
- 'parameters)');
-
-// prepopulated_cache_test(simple_entries, function(cache, entries) {
-// return cache.matchAll(entries.a_with_query.request,
-// {ignoreSearch: true})
-// .then(function(result) {
-// assert_response_array_equivalent(
-// result,
-// [
-// entries.a.response,
-// entries.a_with_query.response
-// ],
-// 'Cache.matchAll with ignoreSearch should ignore the ' +
-// 'search parameters of request.');
-// });
-// },
-// 'Cache.matchAll with ignoreSearch option (request with search parameter)');
-
-prepopulated_cache_test(simple_entries, function(cache, entries) {
- return cache.match(entries.a_with_query.request,
- {ignoreSearch: true})
- .then(function(result) {
- assert_response_in_array(
- result,
- [
- entries.a.response,
- entries.a_with_query.response
- ],
- 'Cache.match with ignoreSearch should ignore the ' +
- 'search parameters of request.');
- });
- },
- 'Cache.match with ignoreSearch option (request with search parameter)');
-
-// prepopulated_cache_test(simple_entries, function(cache, entries) {
-// return cache.matchAll(entries.cat.request.url + '#mouse')
-// .then(function(result) {
-// assert_response_array_equivalent(
-// result,
-// [
-// entries.cat.response,
-// ],
-// 'Cache.matchAll should ignore URL fragment.');
-// });
-// }, 'Cache.matchAll with URL containing fragment');
-
-prepopulated_cache_test(simple_entries, function(cache, entries) {
- return cache.match(entries.cat.request.url + '#mouse')
- .then(function(result) {
- assert_response_equals(result, entries.cat.response,
- 'Cache.match should ignore URL fragment.');
- });
- }, 'Cache.match with URL containing fragment');
-
-// prepopulated_cache_test(simple_entries, function(cache, entries) {
-// return cache.matchAll('http')
-// .then(function(result) {
-// assert_response_array_equivalent(
-// result, [],
-// 'Cache.matchAll should treat query as a URL and not ' +
-// 'just a string fragment.');
-// });
-// }, 'Cache.matchAll with string fragment "http" as query');
-
-prepopulated_cache_test(simple_entries, function(cache, entries) {
- return cache.match('http')
- .then(function(result) {
- assert_equals(
- result, undefined,
- 'Cache.match should treat query as a URL and not ' +
- 'just a string fragment.');
- });
- }, 'Cache.match with string fragment "http" as query');
-
-// prepopulated_cache_test(vary_entries, function(cache, entries) {
-// return cache.matchAll('http://example.com/c')
-// .then(function(result) {
-// assert_response_array_equivalent(
-// result,
-// [
-// entries.vary_cookie_absent.response
-// ],
-// 'Cache.matchAll should exclude matches if a vary header is ' +
-// 'missing in the query request, but is present in the cached ' +
-// 'request.');
-// })
-
-// .then(function() {
-// return cache.matchAll(
-// new Request('http://example.com/c',
-// {headers: {'Cookies': 'none-of-the-above'}}));
-// })
-// .then(function(result) {
-// assert_response_array_equivalent(
-// result,
-// [
-// ],
-// 'Cache.matchAll should exclude matches if a vary header is ' +
-// 'missing in the cached request, but is present in the query ' +
-// 'request.');
-// })
-
-// .then(function() {
-// return cache.matchAll(
-// new Request('http://example.com/c',
-// {headers: {'Cookies': 'is-for-cookie'}}));
-// })
-// .then(function(result) {
-// assert_response_array_equivalent(
-// result,
-// [entries.vary_cookie_is_cookie.response],
-// 'Cache.matchAll should match the entire header if a vary header ' +
-// 'is present in both the query and cached requests.');
-// });
-// }, 'Cache.matchAll with responses containing "Vary" header');
-
-prepopulated_cache_test(vary_entries, function(cache, entries) {
- return cache.match('http://example.com/c')
- .then(function(result) {
- assert_response_in_array(
- result,
- [
- entries.vary_cookie_absent.response
- ],
- 'Cache.match should honor "Vary" header.');
- });
- }, 'Cache.match with responses containing "Vary" header');
-
-// prepopulated_cache_test(vary_entries, function(cache, entries) {
-// return cache.matchAll('http://example.com/c',
-// {ignoreVary: true})
-// .then(function(result) {
-// assert_response_array_equivalent(
-// result,
-// [
-// entries.vary_cookie_is_cookie.response,
-// entries.vary_cookie_is_good.response,
-// entries.vary_cookie_absent.response,
-// ],
-// 'Cache.matchAll should honor "ignoreVary" parameter.');
-// });
-// }, 'Cache.matchAll with "ignoreVary" parameter');
-
-// cache_test(function(cache) {
-// var request = new Request('http://example.com');
-// var response;
-// var request_url = new URL('../resources/simple.txt', location.href).href;
-// return fetch(request_url)
-// .then(function(fetch_result) {
-// response = fetch_result;
-// assert_equals(
-// response.url, request_url,
-// '[https://fetch.spec.whatwg.org/#dom-response-url] ' +
-// 'Reponse.url should return the URL of the response.');
-// return cache.put(request, response.clone());
-// })
-// .then(function() {
-// return cache.match(request.url);
-// })
-// .then(function(result) {
-// assert_response_equals(
-// result, response,
-// 'Cache.match should return a Response object that has the same ' +
-// 'properties as the stored response.');
-// return cache.match(response.url);
-// })
-// .then(function(result) {
-// assert_equals(
-// result, undefined,
-// 'Cache.match should not match cache entry based on response URL.');
-// });
-// }, 'Cache.match with Request and Response objects with different URLs');
-
-cache_test(function(cache) {
- var request_url = new URL('../resources/simple.txt', location.href).href;
- return fetch(request_url)
- .then(function(fetch_result) {
- return cache.put(new Request(request_url), fetch_result);
- })
- .then(function() {
- return cache.match(request_url);
- })
- .then(function(result) {
- return result.text();
- })
- .then(function(body_text) {
- assert_equals(body_text, 'a simple text file\n',
- 'Cache.match should return a Response object with a ' +
- 'valid body.');
- })
- .then(function() {
- return cache.match(request_url);
- })
- .then(function(result) {
- return result.text();
- })
- .then(function(body_text) {
- assert_equals(body_text, 'a simple text file\n',
- 'Cache.match should return a Response object with a ' +
- 'valid body each time it is called.');
- });
- }, 'Cache.match invoked multiple times for the same Request/Response');
-
-// prepopulated_cache_test(simple_entries, function(cache, entries) {
-// var request = new Request(entries.a.request, { method: 'POST' });
-// return cache.match(request)
-// .then(function(result) {
-// assert_equals(result, undefined,
-// 'Cache.match should not find a match');
-// });
-// }, 'Cache.match with POST Request');
-
-// Helpers ---
-
-// Run |test_function| with a Cache object as its only parameter. Prior to the
-// call, the Cache is populated by cache entries from |entries|. The latter is
-// expected to be an Object mapping arbitrary keys to objects of the form
-// {request: <Request object>, response: <Response object>}. There's no
-// guarantee on the order in which entries will be added to the cache.
-//
-// |test_function| should return a Promise that can be used with promise_test.
-function prepopulated_cache_test(entries, test_function, description) {
- cache_test(function(cache) {
- var p = Promise.resolve();
- var hash = {};
- entries.forEach(function(entry) {
- p = p.then(function() {
- return cache.put(entry.request.clone(),
- entry.response.clone())
- .catch(function(e) {
- assert_unreached('Test setup failed for entry ' +
- entry.name + ': ' + e);
- });
- });
- hash[entry.name] = entry;
- });
- p = p.then(function() {
- assert_equals(Object.keys(hash).length, entries.length);
- });
-
- return p.then(function() {
- return test_function(cache, hash);
- });
- }, description);
-}
-
-done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-put.js b/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-put.js
deleted file mode 100644
index 755b8c2..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-put.js
+++ /dev/null
@@ -1,284 +0,0 @@
-if (self.importScripts) {
- importScripts('/resources/testharness.js');
- importScripts('../resources/testharness-helpers.js');
- importScripts('../resources/test-helpers.js');
-}
-
-// TODO(b/250611661): implement complete Cache API and adhere to web spec. Once
-// complete, enable the tests commented out.
-
-var test_url = 'https://example.com/foo';
-var test_body = 'Hello world!';
-
-cache_test(function(cache) {
- var request = new Request(test_url);
- var response = new Response(test_body);
- return cache.put(request, response)
- .then(function(result) {
- assert_equals(result, undefined,
- 'Cache.put should resolve with undefined on success.');
- });
- }, 'Cache.put called with simple Request and Response');
-
-// cache_test(function(cache) {
-// var test_url = new URL('../resources/simple.txt', location.href).href;
-// var request = new Request(test_url);
-// var response;
-// return fetch(test_url)
-// .then(function(fetch_result) {
-// response = fetch_result.clone();
-// return cache.put(request, fetch_result);
-// })
-// .then(function() {
-// return cache.match(test_url);
-// })
-// .then(function(result) {
-// assert_response_equals(result, response,
-// 'Cache.put should update the cache with ' +
-// 'new request and response.');
-// return result.text();
-// })
-// .then(function(body) {
-// assert_equals(body, 'a simple text file\n',
-// 'Cache.put should store response body.');
-// });
-// }, 'Cache.put called with Request and Response from fetch()');
-
-cache_test(function(cache) {
- var request = new Request(test_url);
- var response = new Response(test_body);
- assert_false(request.bodyUsed,
- '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' +
- 'Request.bodyUsed should be initially false.');
- return cache.put(request, response)
- .then(function() {
- assert_false(request.bodyUsed,
- 'Cache.put should not mark empty request\'s body used');
- });
- }, 'Cache.put with Request without a body');
-
-cache_test(function(cache) {
- var request = new Request(test_url);
- var response = new Response();
- assert_false(response.bodyUsed,
- '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' +
- 'Response.bodyUsed should be initially false.');
- return cache.put(request, response)
- .then(function() {
- assert_false(response.bodyUsed,
- 'Cache.put should not mark empty response\'s body used');
- });
- }, 'Cache.put with Response without a body');
-
-cache_test(function(cache) {
- var request = new Request(test_url);
- var response = new Response(test_body);
- return cache.put(request, response.clone())
- .then(function() {
- return cache.match(test_url);
- })
- .then(function(result) {
- assert_response_equals(result, response,
- 'Cache.put should update the cache with ' +
- 'new Request and Response.');
- });
- }, 'Cache.put with a Response containing an empty URL');
-
-// cache_test(function(cache) {
-// var request = new Request(test_url);
-// var response = new Response('', {
-// status: 200,
-// headers: [['Content-Type', 'text/plain']]
-// });
-// return cache.put(request, response)
-// .then(function() {
-// return cache.match(test_url);
-// })
-// .then(function(result) {
-// assert_equals(result.status, 200, 'Cache.put should store status.');
-// assert_equals(result.headers.get('Content-Type'), 'text/plain',
-// 'Cache.put should store headers.');
-// return result.text();
-// })
-// .then(function(body) {
-// assert_equals(body, '',
-// 'Cache.put should store response body.');
-// });
-// }, 'Cache.put with an empty response body');
-
-// cache_test(function(cache) {
-// var test_url = new URL('../resources/fetch-status.py?status=500', location.href).href;
-// var request = new Request(test_url);
-// var response;
-// return fetch(test_url)
-// .then(function(fetch_result) {
-// assert_equals(fetch_result.status, 500,
-// 'Test framework error: The status code should be 500.');
-// response = fetch_result.clone();
-// return cache.put(request, fetch_result);
-// })
-// .then(function() {
-// return cache.match(test_url);
-// })
-// .then(function(result) {
-// assert_response_equals(result, response,
-// 'Cache.put should update the cache with ' +
-// 'new request and response.');
-// return result.text();
-// })
-// .then(function(body) {
-// assert_equals(body, '',
-// 'Cache.put should store response body.');
-// });
-// }, 'Cache.put with HTTP 500 response');
-
-// cache_test(function(cache) {
-// var alternate_response_body = 'New body';
-// var alternate_response = new Response(alternate_response_body,
-// { statusText: 'New status' });
-// return cache.put(new Request(test_url),
-// new Response('Old body', { statusText: 'Old status' }))
-// .then(function() {
-// return cache.put(new Request(test_url), alternate_response.clone());
-// })
-// .then(function() {
-// return cache.match(test_url);
-// })
-// .then(function(result) {
-// assert_response_equals(result, alternate_response,
-// 'Cache.put should replace existing ' +
-// 'response with new response.');
-// return result.text();
-// })
-// .then(function(body) {
-// assert_equals(body, alternate_response_body,
-// 'Cache put should store new response body.');
-// });
-// }, 'Cache.put called twice with matching Requests and different Responses');
-
-cache_test(function(cache) {
- var first_url = test_url;
- var second_url = first_url + '#(O_o)';
- var alternate_response_body = 'New body';
- var alternate_response = new Response(alternate_response_body,
- { statusText: 'New status' });
- return cache.put(new Request(first_url),
- new Response('Old body', { statusText: 'Old status' }))
- .then(function() {
- return cache.put(new Request(second_url), alternate_response.clone());
- })
- .then(function() {
- return cache.match(test_url);
- })
- .then(function(result) {
- assert_response_equals(result, alternate_response,
- 'Cache.put should replace existing ' +
- 'response with new response.');
- return result.text();
- })
- .then(function(body) {
- assert_equals(body, alternate_response_body,
- 'Cache put should store new response body.');
- });
- }, 'Cache.put called twice with request URLs that differ only by a fragment');
-
-cache_test(function(cache) {
- var url = 'http://example.com/foo';
- return cache.put(url, new Response('some body'))
- .then(function() { return cache.match(url); })
- .then(function(response) { return response.text(); })
- .then(function(body) {
- assert_equals(body, 'some body',
- 'Cache.put should accept a string as request.');
- });
- }, 'Cache.put with a string request');
-
-// cache_test(function(cache) {
-// return assert_promise_rejects(
-// cache.put(new Request(test_url), 'Hello world!'),
-// new TypeError(),
-// 'Cache.put should only accept a Response object as the response.');
-// }, 'Cache.put with an invalid response');
-
-// cache_test(function(cache) {
-// return assert_promise_rejects(
-// cache.put(new Request('file:///etc/passwd'),
-// new Response(test_body)),
-// new TypeError(),
-// 'Cache.put should reject non-HTTP/HTTPS requests with a TypeError.');
-// }, 'Cache.put with a non-HTTP/HTTPS request');
-
-cache_test(function(cache) {
- var response = new Response(test_body);
- return cache.put(new Request('relative-url'), response.clone())
- .then(function() {
- return cache.match(new URL('relative-url', location.href).href);
- })
- .then(function(result) {
- assert_response_equals(result, response,
- 'Cache.put should accept a relative URL ' +
- 'as the request.');
- });
- }, 'Cache.put with a relative URL');
-
-// cache_test(function(cache) {
-// var request = new Request('http://example.com/foo', { method: 'HEAD' });
-// return assert_promise_rejects(
-// cache.put(request, new Response(test_body)),
-// new TypeError(),
-// 'Cache.put should throw a TypeError for non-GET requests.');
-// }, 'Cache.put with a non-GET request');
-
-// cache_test(function(cache) {
-// return assert_promise_rejects(
-// cache.put(new Request(test_url), null),
-// new TypeError(),
-// 'Cache.put should throw a TypeError for a null response.');
-// }, 'Cache.put with a null response');
-
-// cache_test(function(cache) {
-// var request = new Request(test_url, {method: 'POST', body: test_body});
-// return assert_promise_rejects(
-// cache.put(request, new Response(test_body)),
-// new TypeError(),
-// 'Cache.put should throw a TypeError for a POST request.');
-// }, 'Cache.put with a POST request');
-
-cache_test(function(cache) {
- var response = new Response(test_body);
- assert_false(response.bodyUsed,
- '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' +
- 'Response.bodyUsed should be initially false.');
- return response.text().then(function() {
- assert_true(
- response.bodyUsed,
- '[https://fetch.spec.whatwg.org/#concept-body-consume-body] ' +
- 'The text() method should set "body used" flag.');
- return assert_promise_rejects(
- cache.put(new Request(test_url), response),
- new TypeError,
- '[https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#cache-put] ' +
- 'Cache put should reject with TypeError when Response ' +
- 'body is already used.');
- });
- }, 'Cache.put with a used response body');
-
-// cache_test(function(cache) {
-// return assert_promise_rejects(
-// cache.put(new Request(test_url),
-// new Response(test_body, { headers: { VARY: '*' }})),
-// new TypeError(),
-// 'Cache.put should reject VARY:* Responses with a TypeError.');
-// }, 'Cache.put with a VARY:* Response');
-
-// cache_test(function(cache) {
-// return assert_promise_rejects(
-// cache.put(new Request(test_url),
-// new Response(test_body,
-// { headers: { VARY: 'Accept-Language,*' }})),
-// new TypeError(),
-// 'Cache.put should reject Responses with an embedded VARY:* with a ' +
-// 'TypeError.');
-// }, 'Cache.put with an embedded VARY:* Response');
-
-done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-storage-keys.js b/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-storage-keys.js
deleted file mode 100644
index 4d7bc62..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-storage-keys.js
+++ /dev/null
@@ -1,36 +0,0 @@
-if (self.importScripts) {
- importScripts('/resources/testharness.js');
- importScripts('../resources/testharness-helpers.js');
- importScripts('../resources/test-helpers.js');
-}
-
-var test_cache_list =
- ['', 'example', 'Another cache name', 'A', 'a', 'ex ample'];
-
-promise_test(function(test) {
- return self.caches.keys()
- .then(function(keys) {
- assert_true(Array.isArray(keys),
- 'CacheStorage.keys should return an Array.');
- return Promise.all(keys.map(function(key) {
- return self.caches.delete(key);
- }));
- })
- .then(function() {
- return Promise.all(test_cache_list.map(function(key) {
- return self.caches.open(key);
- }));
- })
-
- .then(function() { return self.caches.keys(); })
- .then(function(keys) {
- assert_true(Array.isArray(keys),
- 'CacheStorage.keys should return an Array.');
- assert_array_equals(keys,
- test_cache_list,
- 'CacheStorage.keys should only return ' +
- 'existing caches.');
- });
- }, 'CacheStorage keys');
-
-done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-storage-match.js b/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-storage-match.js
deleted file mode 100644
index b5122ac..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-storage-match.js
+++ /dev/null
@@ -1,129 +0,0 @@
-if (self.importScripts) {
- importScripts('/resources/testharness.js');
- importScripts('../resources/testharness-helpers.js');
- importScripts('../resources/test-helpers.js');
-}
-
-// TODO(b/250611661): implement complete Cache API and adhere to web spec. Once
-// complete, enable the tests commented out.
-
-(function() {
- var next_index = 1;
-
- // Returns a transaction (request, response, and url) for a unique URL.
- function create_unique_transaction(test) {
- var uniquifier = String(next_index++);
- var url = 'http://example.com/' + uniquifier;
-
- return {
- request: new Request(url),
- response: new Response('hello'),
- url: url
- };
- }
-
- self.create_unique_transaction = create_unique_transaction;
-})();
-
-cache_test(function(cache) {
- var transaction = create_unique_transaction();
-
- return cache.put(transaction.request.clone(), transaction.response.clone())
- .then(function() {
- return self.caches.match(transaction.request);
- })
- .then(function(response) {
- assert_response_equals(response, transaction.response,
- 'The response should not have changed.');
- });
-}, 'CacheStorageMatch with no cache name provided');
-
-// cache_test(function(cache) {
-// var transaction = create_unique_transaction();
-
-// var test_cache_list = ['a', 'b', 'c'];
-// return cache.put(transaction.request.clone(), transaction.response.clone())
-// .then(function() {
-// return Promise.all(test_cache_list.map(function(key) {
-// return self.caches.open(key);
-// }));
-// })
-// .then(function() {
-// return self.caches.match(transaction.request);
-// })
-// .then(function(response) {
-// assert_response_equals(response, transaction.response,
-// 'The response should not have changed.');
-// });
-// }, 'CacheStorageMatch from one of many caches');
-
-// promise_test(function(test) {
-// var transaction = create_unique_transaction();
-
-// var test_cache_list = ['x', 'y', 'z'];
-// return Promise.all(test_cache_list.map(function(key) {
-// return self.caches.open(key);
-// }))
-// .then(function() { return caches.open('x'); })
-// .then(function(cache) {
-// return cache.put(transaction.request.clone(),
-// transaction.response.clone());
-// })
-// .then(function() {
-// return self.caches.match(transaction.request, {cacheName: 'x'});
-// })
-// .then(function(response) {
-// assert_response_equals(response, transaction.response,
-// 'The response should not have changed.');
-// })
-// .then(function() {
-// return self.caches.match(transaction.request, {cacheName: 'y'});
-// })
-// .then(function(response) {
-// assert_equals(response, undefined,
-// 'Cache y should not have a response for the request.');
-// });
-// }, 'CacheStorageMatch from one of many caches by name');
-
-// cache_test(function(cache) {
-// var transaction = create_unique_transaction();
-// return cache.put(transaction.url, transaction.response.clone())
-// .then(function() {
-// return self.caches.match(transaction.request);
-// })
-// .then(function(response) {
-// assert_response_equals(response, transaction.response,
-// 'The response should not have changed.');
-// });
-// }, 'CacheStorageMatch a string request');
-
-// promise_test(function(test) {
-// var transaction = create_unique_transaction();
-// return self.caches.match(transaction.request)
-// .then(function(response) {
-// assert_equals(response, undefined,
-// 'The response should not be found.');
-// })
-// }, 'CacheStorageMatch with no cached entry');
-
-// promise_test(function(test) {
-// var transaction = create_unique_transaction();
-// return self.caches.has('foo')
-// .then(function(has_foo) {
-// assert_false(has_foo, "The cache should not exist.");
-// return self.caches.match(transaction.request, {cacheName: 'foo'});
-// })
-// .then(function(response) {
-// assert_unreached('The match with bad cache name should reject.');
-// })
-// .catch(function(err) {
-// assert_equals(err.name, 'NotFoundError',
-// 'The match should reject with NotFoundError.');
-// return self.caches.has('foo');
-// })
-// .then(function(has_foo) {
-// assert_false(has_foo, "The cache should still not exist.");
-// })
-// }, 'CacheStorageMatch with no caches available but name provided');
-
-done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-storage.js b/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-storage.js
deleted file mode 100644
index 905ab5d..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/script-tests/cache-storage.js
+++ /dev/null
@@ -1,201 +0,0 @@
-if (self.importScripts) {
- importScripts('/resources/testharness.js');
- importScripts('../resources/testharness-helpers.js');
- importScripts('../resources/test-helpers.js');
-}
-
-// TODO(b/250611661): implement complete Cache API and adhere to web spec. Once
-// complete, enable the tests commented out.
-
-promise_test(function(t) {
- var cache_name = 'cache-storage/foo';
- return self.caches.delete(cache_name)
- .then(function() {
- return self.caches.open(cache_name);
- })
- .then(function(cache) {
- assert_true(cache instanceof Cache,
- 'CacheStorage.open should return a Cache.');
- });
- }, 'CacheStorage.open');
-
-promise_test(function(t) {
- // Note that this test may collide with other tests running in the same
- // origin that also uses an empty cache name.
- var cache_name = '';
- return self.caches.delete(cache_name)
- .then(function() {
- return self.caches.open(cache_name);
- })
- .then(function(cache) {
- assert_true(cache instanceof Cache,
- 'CacheStorage.open should accept an empty name.');
- });
- }, 'CacheStorage.open with an empty name');
-
-// promise_test(function(t) {
-// return assert_promise_rejects(
-// self.caches.open(),
-// new TypeError(),
-// 'CacheStorage.open should throw TypeError if called with no arguments.');
-// }, 'CacheStorage.open with no arguments');
-
-// promise_test(function(t) {
-// var test_cases = [
-// {
-// name: 'cache-storage/lowercase',
-// should_not_match:
-// [
-// 'cache-storage/Lowercase',
-// ' cache-storage/lowercase',
-// 'cache-storage/lowercase '
-// ]
-// },
-// {
-// name: 'cache-storage/has a space',
-// should_not_match:
-// [
-// 'cache-storage/has'
-// ]
-// },
-// {
-// name: 'cache-storage/has\000_in_the_name',
-// should_not_match:
-// [
-// 'cache-storage/has',
-// 'cache-storage/has_in_the_name'
-// ]
-// }
-// ];
-// return Promise.all(test_cases.map(function(testcase) {
-// var cache_name = testcase.name;
-// return self.caches.delete(cache_name)
-// .then(function() {
-// return self.caches.open(cache_name);
-// })
-// .then(function() {
-// return self.caches.has(cache_name);
-// })
-// .then(function(result) {
-// assert_true(result,
-// 'CacheStorage.has should return true for existing ' +
-// 'cache.');
-// })
-// .then(function() {
-// return Promise.all(
-// testcase.should_not_match.map(function(cache_name) {
-// return self.caches.has(cache_name)
-// .then(function(result) {
-// assert_false(result,
-// 'CacheStorage.has should only perform ' +
-// 'exact matches on cache names.');
-// });
-// }));
-// })
-// .then(function() {
-// return self.caches.delete(cache_name);
-// });
-// }));
-// }, 'CacheStorage.has with existing cache');
-
-// promise_test(function(t) {
-// return self.caches.has('cheezburger')
-// .then(function(result) {
-// assert_false(result,
-// 'CacheStorage.has should return false for ' +
-// 'nonexistent cache.');
-// });
-// }, 'CacheStorage.has with nonexistent cache');
-
-promise_test(function(t) {
- var cache_name = 'cache-storage/open';
- var url = '../resources/simple.txt';
- var cache;
- return self.caches.delete(cache_name)
- .then(function() {
- return self.caches.open(cache_name);
- })
- .then(function(result) {
- cache = result;
- })
- .then(function() {
- return cache.add('../resources/simple.txt');
- })
- .then(function() {
- return self.caches.open(cache_name);
- })
- .then(function(result) {
- assert_true(result instanceof Cache,
- 'CacheStorage.open should return a Cache object');
- // assert_not_equals(result, cache,
- // 'CacheStorage.open should return a new Cache ' +
- // 'object each time its called.');
- return Promise.all([cache.keys(), result.keys()]);
- })
- .then(function(results) {
- var expected_urls = results[0].map(function(r) { return r.url });
- var actual_urls = results[1].map(function(r) { return r.url });
- assert_array_equals(actual_urls, expected_urls,
- 'CacheStorage.open should return a new Cache ' +
- 'object for the same backing store.');
- })
- }, 'CacheStorage.open with existing cache');
-
-promise_test(function(t) {
- var cache_name = 'cache-storage/delete';
-
- return self.caches.delete(cache_name)
- .then(function() {
- return self.caches.open(cache_name);
- })
- .then(function() { return self.caches.delete(cache_name); })
- .then(function(result) {
- assert_true(result,
- 'CacheStorage.delete should return true after ' +
- 'deleting an existing cache.');
- })
-
- // .then(function() { return self.caches.has(cache_name); })
- // .then(function(cache_exists) {
- // assert_false(cache_exists,
- // 'CacheStorage.has should return false after ' +
- // 'fulfillment of CacheStorage.delete promise.');
- // });
- }, 'CacheStorage.delete with existing cache');
-
-// promise_test(function(t) {
-// return self.caches.delete('cheezburger')
-// .then(function(result) {
-// assert_false(result,
-// 'CacheStorage.delete should return false for a ' +
-// 'nonexistent cache.');
-// });
-// }, 'CacheStorage.delete with nonexistent cache');
-
-// promise_test(function(t) {
-// var bad_name = 'unpaired\uD800';
-// var converted_name = 'unpaired\uFFFD'; // Don't create cache with this name.
-// return self.caches.has(converted_name)
-// .then(function(cache_exists) {
-// assert_false(cache_exists,
-// 'Test setup failure: cache should not exist');
-// })
-// .then(function() { return self.caches.open(bad_name); })
-// .then(function() { return self.caches.keys(); })
-// .then(function(keys) {
-// assert_true(keys.indexOf(bad_name) !== -1,
-// 'keys should include cache with bad name');
-// })
-// .then(function() { return self.caches.has(bad_name); })
-// .then(function(cache_exists) {
-// assert_true(cache_exists,
-// 'CacheStorage names should be not be converted.');
-// })
-// .then(function() { return self.caches.has(converted_name); })
-// .then(function(cache_exists) {
-// assert_false(cache_exists,
-// 'CacheStorage names should be not be converted.');
-// });
-// }, 'CacheStorage names are DOMStrings not USVStrings');
-
-done();
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-add.https.html b/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-add.https.html
deleted file mode 100644
index 57e74b7..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-add.https.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<!DOCTYPE html>
-<title>Cache.add and Cache.addAll</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-add">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../../service-workers/resources/test-helpers.js"></script>
-<script>
-service_worker_test('../script-tests/cache-add.js');
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-delete.https.html b/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-delete.https.html
deleted file mode 100644
index 7a5a43f..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-delete.https.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<!DOCTYPE html>
-<title>Cache.delete</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-delete">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../../service-workers/resources/test-helpers.js"></script>
-<script>
-service_worker_test('../script-tests/cache-delete.js');
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-match.https.html b/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-match.https.html
deleted file mode 100644
index 859b1cd..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-match.https.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<!DOCTYPE html>
-<title>Cache.match and Cache.matchAll</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-match">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../../service-workers/resources/test-helpers.js"></script>
-<script>
-service_worker_test('../script-tests/cache-match.js');
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-put.https.html b/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-put.https.html
deleted file mode 100644
index d67f939..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-put.https.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<!DOCTYPE html>
-<title>Cache.put</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-put">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../../service-workers/resources/test-helpers.js"></script>
-<script>
-service_worker_test('../script-tests/cache-put.js');
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-storage-keys.https.html b/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-storage-keys.https.html
deleted file mode 100644
index ec7e14b..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-storage-keys.https.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<!DOCTYPE html>
-<title>CacheStorage.keys</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../../service-workers/resources/test-helpers.js"></script>
-<script>
-service_worker_test('../script-tests/cache-storage-keys.js');
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-storage-match.https.html b/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-storage-match.https.html
deleted file mode 100644
index 937f143..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-storage-match.https.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<!DOCTYPE html>
-<title>CacheStorage.match</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage-match">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../../service-workers/resources/test-helpers.js"></script>
-<script>
-service_worker_test('../script-tests/cache-storage-match.js');
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-storage.https.html b/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-storage.https.html
deleted file mode 100644
index 62c6b63..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/serviceworker/cache-storage.https.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<!DOCTYPE html>
-<title>CacheStorage</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../../service-workers/resources/test-helpers.js"></script>
-<script>
-service_worker_test('../script-tests/cache-storage.js');
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-add.https.html b/third_party/web_platform_tests/service-workers/cache-storage/window/cache-add.https.html
deleted file mode 100644
index 42e4b50..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-add.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>Cache Storage: Cache.add and Cache.addAll</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-add">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../resources/testharness-helpers.js"></script>
-<script src="../resources/test-helpers.js"></script>
-<script src="../script-tests/cache-add.js"></script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-delete.https.html b/third_party/web_platform_tests/service-workers/cache-storage/window/cache-delete.https.html
deleted file mode 100644
index 754f785..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-delete.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>Cache Storage: Cache.delete</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-delete">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../resources/testharness-helpers.js"></script>
-<script src="../resources/test-helpers.js"></script>
-<script src="../script-tests/cache-delete.js"></script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-match.https.html b/third_party/web_platform_tests/service-workers/cache-storage/window/cache-match.https.html
deleted file mode 100644
index 093df8d..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-match.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>Cache Storage: Cache.match and Cache.matchAll</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-match">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../resources/testharness-helpers.js"></script>
-<script src="../resources/test-helpers.js"></script>
-<script src="../script-tests/cache-match.js"></script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-put.https.html b/third_party/web_platform_tests/service-workers/cache-storage/window/cache-put.https.html
deleted file mode 100644
index b32cfa2..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-put.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>Cache Storage: Cache.put</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-put">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../resources/testharness-helpers.js"></script>
-<script src="../resources/test-helpers.js"></script>
-<script src="../script-tests/cache-put.js"></script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-storage-keys.https.html b/third_party/web_platform_tests/service-workers/cache-storage/window/cache-storage-keys.https.html
deleted file mode 100644
index acde773..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-storage-keys.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>Cache Storage: CacheStorage.keys</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../resources/testharness-helpers.js"></script>
-<script src="../resources/test-helpers.js"></script>
-<script src="../script-tests/cache-storage-keys.js"></script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-storage-match.https.html b/third_party/web_platform_tests/service-workers/cache-storage/window/cache-storage-match.https.html
deleted file mode 100644
index 3c69d0f..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-storage-match.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>Cache Storage: CacheStorage.match</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage-match">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../resources/testharness-helpers.js"></script>
-<script src="../resources/test-helpers.js"></script>
-<script src="../script-tests/cache-storage-match.js"></script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-storage.https.html b/third_party/web_platform_tests/service-workers/cache-storage/window/cache-storage.https.html
deleted file mode 100644
index 7d015e3..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/window/cache-storage.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>Cache Storage: CacheStorage</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../resources/testharness-helpers.js"></script>
-<script src="../resources/test-helpers.js"></script>
-<script src="../script-tests/cache-storage.js"></script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/window/sandboxed-iframes.https.html b/third_party/web_platform_tests/service-workers/cache-storage/window/sandboxed-iframes.https.html
deleted file mode 100644
index 648bd59..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/window/sandboxed-iframes.https.html
+++ /dev/null
@@ -1,67 +0,0 @@
-<!DOCTYPE html>
-<title>Cache Storage: Verify access in sandboxed iframes</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="../resources/testharness-helpers.js"></script>
-<script>
-
-function load_iframe(src, sandbox) {
- return new Promise(function(resolve, reject) {
- var iframe = document.createElement('iframe');
- iframe.onload = function() { resolve(iframe); };
-
- iframe.sandbox = sandbox;
- iframe.src = src;
-
- document.documentElement.appendChild(iframe);
- });
-}
-
-function wait_for_message(id) {
- return new Promise(function(resolve) {
- self.addEventListener('message', function listener(e) {
- if (e.data.id === id) {
- resolve(e.data);
- self.removeEventListener('message', listener);
- }
- });
- });
-}
-
-var counter = 0;
-
-promise_test(function(t) {
- return load_iframe('../resources/iframe.html',
- 'allow-scripts allow-same-origin')
- .then(function(iframe) {
- var id = ++counter;
- iframe.contentWindow.postMessage({id: id}, '*');
- return wait_for_message(id);
- })
- .then(function(message) {
- assert_equals(
- message.result, 'allowed',
- 'Access should be allowed if sandbox has allow-same-origin');
- });
-}, 'Sandboxed iframe with allow-same-origin is allowed access');
-
-promise_test(function(t) {
- return load_iframe('../resources/iframe.html',
- 'allow-scripts')
- .then(function(iframe) {
- var id = ++counter;
- iframe.contentWindow.postMessage({id: id}, '*');
- return wait_for_message(id);
- })
- .then(function(message) {
- assert_equals(
- message.result, 'denied',
- 'Access should be denied if sandbox lacks allow-same-origin');
- assert_equals(message.name, 'SecurityError',
- 'Failure should be a SecurityError');
- });
-}, 'Sandboxed iframe without allow-same-origin is denied access');
-
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-add.https.html b/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-add.https.html
deleted file mode 100644
index 8e6deeb..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-add.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>Cache.add and Cache.addAll</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-add">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script>
-fetch_tests_from_worker(new Worker('../script-tests/cache-add.js'));
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-delete.https.html b/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-delete.https.html
deleted file mode 100644
index 2dd06f3..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-delete.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>Cache.delete</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-delete">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script>
-fetch_tests_from_worker(new Worker('../script-tests/cache-delete.js'));
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-match.https.html b/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-match.https.html
deleted file mode 100644
index b0926fc..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-match.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>Cache.match and Cache.matchAll</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-match">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script>
-fetch_tests_from_worker(new Worker('../script-tests/cache-match.js'));
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-put.https.html b/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-put.https.html
deleted file mode 100644
index 8876930..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-put.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>Cache.put</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-put">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script>
-fetch_tests_from_worker(new Worker('../script-tests/cache-put.js'));
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-storage-keys.https.html b/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-storage-keys.https.html
deleted file mode 100644
index 5c75ef8..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-storage-keys.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>CacheStorage.keys</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script>
-fetch_tests_from_worker(new Worker('../script-tests/cache-storage-keys.js'));
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-storage-match.https.html b/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-storage-match.https.html
deleted file mode 100644
index 4d48683..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-storage-match.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>CacheStorage.match</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage-match">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script>
-fetch_tests_from_worker(new Worker('../script-tests/cache-storage-match.js'));
-</script>
diff --git a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-storage.https.html b/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-storage.https.html
deleted file mode 100644
index e10f5c5..0000000
--- a/third_party/web_platform_tests/service-workers/cache-storage/worker/cache-storage.https.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<title>CacheStorage</title>
-<link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage">
-<meta name="timeout" content="long">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script>
-fetch_tests_from_worker(new Worker('../script-tests/cache-storage.js'));
-</script>
diff --git a/third_party/web_platform_tests/service-workers/idlharness.https.any.js b/third_party/web_platform_tests/service-workers/idlharness.https.any.js
new file mode 100644
index 0000000..8db5d4d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/idlharness.https.any.js
@@ -0,0 +1,53 @@
+// META: global=window,worker
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+// META: script=cache-storage/resources/test-helpers.js
+// META: script=service-worker/resources/test-helpers.sub.js
+// META: timeout=long
+
+// https://w3c.github.io/ServiceWorker
+
+idl_test(
+ ['service-workers'],
+ ['dom', 'html'],
+ async (idl_array, t) => {
+ self.cacheInstance = await create_temporary_cache(t);
+
+ idl_array.add_objects({
+ CacheStorage: ['caches'],
+ Cache: ['self.cacheInstance'],
+ ServiceWorkerContainer: ['navigator.serviceWorker']
+ });
+
+ // TODO: Add ServiceWorker and ServiceWorkerRegistration instances for the
+ // other worker scopes.
+ if (self.GLOBAL.isWindow()) {
+ idl_array.add_objects({
+ ServiceWorkerRegistration: ['registrationInstance'],
+ ServiceWorker: ['registrationInstance.installing']
+ });
+
+ const scope = 'service-worker/resources/scope/idlharness';
+ const registration = await service_worker_unregister_and_register(
+ t, 'service-worker/resources/empty-worker.js', scope);
+ t.add_cleanup(() => registration.unregister());
+
+ self.registrationInstance = registration;
+ } else if (self.ServiceWorkerGlobalScope) {
+ // self.ServiceWorkerGlobalScope should only be defined for the
+ // ServiceWorker scope, which allows us to detect and test the interfaces
+ // exposed only for ServiceWorker.
+ idl_array.add_objects({
+ Clients: ['clients'],
+ ExtendableEvent: ['new ExtendableEvent("type")'],
+ FetchEvent: ['new FetchEvent("type", { request: new Request("") })'],
+ ServiceWorkerGlobalScope: ['self'],
+ ServiceWorkerRegistration: ['registration'],
+ ServiceWorker: ['serviceWorker'],
+ // TODO: Test instances of Client and WindowClient, e.g.
+ // Client: ['self.clientInstance'],
+ // WindowClient: ['self.windowClientInstance']
+ });
+ }
+ }
+);
diff --git a/third_party/web_platform_tests/service-workers/service-worker/Service-Worker-Allowed-header.https.html b/third_party/web_platform_tests/service-workers/service-worker/Service-Worker-Allowed-header.https.html
new file mode 100644
index 0000000..6f44bb1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/Service-Worker-Allowed-header.https.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<title>Service Worker: Service-Worker-Allowed header</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+const host_info = get_host_info();
+
+// Returns a URL for a service worker script whose Service-Worker-Allowed
+// header value is set to |allowed_path|. If |origin| is specified, that origin
+// is used.
+function build_script_url(allowed_path, origin) {
+ const script = 'resources/empty-worker.js';
+ const url = origin ? `${origin}${base_path()}${script}` : script;
+ return `${url}?pipe=header(Service-Worker-Allowed,${allowed_path})`;
+}
+
+// register_test is a promise_test that registers a service worker.
+function register_test(script, scope, description) {
+ promise_test(async t => {
+ t.add_cleanup(() => {
+ return service_worker_unregister(t, scope);
+ });
+
+ const registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ assert_true(registration instanceof ServiceWorkerRegistration, 'registered');
+ assert_equals(registration.scope, normalizeURL(scope));
+ }, description);
+}
+
+// register_fail_test is like register_test but expects a SecurityError.
+function register_fail_test(script, scope, description) {
+ promise_test(async t => {
+ t.add_cleanup(() => {
+ return service_worker_unregister(t, scope);
+ });
+
+ await service_worker_unregister(t, scope);
+ await promise_rejects_dom(t,
+ 'SecurityError',
+ navigator.serviceWorker.register(script, {scope}));
+ }, description);
+}
+
+register_test(
+ build_script_url('/allowed-path'),
+ '/allowed-path',
+ 'Registering within Service-Worker-Allowed path');
+
+register_test(
+ build_script_url(new URL('/allowed-path', document.location)),
+ '/allowed-path',
+ 'Registering within Service-Worker-Allowed path (absolute URL)');
+
+register_test(
+ build_script_url('../allowed-path-with-parent'),
+ 'allowed-path-with-parent',
+ 'Registering within Service-Worker-Allowed path with parent reference');
+
+register_fail_test(
+ build_script_url('../allowed-path'),
+ '/disallowed-path',
+ 'Registering outside Service-Worker-Allowed path'),
+
+register_fail_test(
+ build_script_url('../allowed-path-with-parent'),
+ '/allowed-path-with-parent',
+ 'Registering outside Service-Worker-Allowed path with parent reference');
+
+register_fail_test(
+ build_script_url(host_info.HTTPS_REMOTE_ORIGIN + '/'),
+ 'resources/this-scope-is-normally-allowed',
+ 'Service-Worker-Allowed is cross-origin to script, registering on a normally allowed scope');
+
+register_fail_test(
+ build_script_url(host_info.HTTPS_REMOTE_ORIGIN + '/'),
+ '/this-scope-is-normally-disallowed',
+ 'Service-Worker-Allowed is cross-origin to script, registering on a normally disallowed scope');
+
+register_fail_test(
+ build_script_url(host_info.HTTPS_REMOTE_ORIGIN + '/cross-origin/',
+ host_info.HTTPS_REMOTE_ORIGIN),
+ '/cross-origin/',
+ 'Service-Worker-Allowed is cross-origin to page, same-origin to script');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/close.https.html b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/close.https.html
new file mode 100644
index 0000000..3e3cc8b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/close.https.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: close operation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+
+service_worker_test(
+ 'resources/close-worker.js', 'ServiceWorkerGlobalScope: close operation');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event-constructor.https.html b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event-constructor.https.html
new file mode 100644
index 0000000..525245f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event-constructor.https.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: ExtendableMessageEvent Constructor</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+service_worker_test(
+ 'resources/extendable-message-event-constructor-worker.js', document.title
+ );
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event.https.html b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event.https.html
new file mode 100644
index 0000000..89efd7a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event.https.html
@@ -0,0 +1,226 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: ExtendableMessageEvent</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script src='./resources/extendable-message-event-utils.js'></script>
+<script>
+promise_test(function(t) {
+ var script = 'resources/extendable-message-event-worker.js';
+ var scope = 'resources/scope/extendable-message-event-from-toplevel';
+ var registration;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(r) {
+ registration = r;
+ add_completion_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var saw_message = new Promise(function(resolve) {
+ navigator.serviceWorker.onmessage =
+ function(event) { resolve(event.data); }
+ });
+ var channel = new MessageChannel;
+ registration.active.postMessage('', [channel.port1]);
+ return saw_message;
+ })
+ .then(function(results) {
+ var expected = {
+ constructor: { name: 'ExtendableMessageEvent' },
+ origin: location.origin,
+ lastEventId: '',
+ source: {
+ constructor: { name: 'WindowClient' },
+ frameType: 'top-level',
+ url: location.href,
+ visibilityState: 'visible',
+ focused: true
+ },
+ ports: [ { constructor: { name: 'MessagePort' } } ]
+ };
+ ExtendableMessageEventUtils.assert_equals(results, expected);
+ });
+ }, 'Post an extendable message from a top-level client');
+
+promise_test(function(t) {
+ var script = 'resources/extendable-message-event-worker.js';
+ var scope = 'resources/scope/extendable-message-event-from-nested';
+ var frame;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ add_completion_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ frame = f;
+ add_completion_callback(function() { frame.remove(); });
+ var saw_message = new Promise(function(resolve) {
+ frame.contentWindow.navigator.serviceWorker.onmessage =
+ function(event) { resolve(event.data); }
+ });
+ f.contentWindow.navigator.serviceWorker.controller.postMessage('');
+ return saw_message;
+ })
+ .then(function(results) {
+ var expected = {
+ constructor: { name: 'ExtendableMessageEvent' },
+ origin: location.origin,
+ lastEventId: '',
+ source: {
+ constructor: { name: 'WindowClient' },
+ url: frame.contentWindow.location.href,
+ frameType: 'nested',
+ visibilityState: 'visible',
+ focused: false
+ },
+ ports: []
+ };
+ ExtendableMessageEventUtils.assert_equals(results, expected);
+ });
+ }, 'Post an extendable message from a nested client');
+
+promise_test(function(t) {
+ var script = 'resources/extendable-message-event-loopback-worker.js';
+ var scope = 'resources/scope/extendable-message-event-loopback';
+ var registration;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(r) {
+ registration = r;
+ add_completion_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var results = [];
+ var saw_message = new Promise(function(resolve) {
+ navigator.serviceWorker.onmessage = function(event) {
+ switch (event.data.type) {
+ case 'record':
+ results.push(event.data.results);
+ break;
+ case 'finish':
+ resolve(results);
+ break;
+ }
+ };
+ });
+ registration.active.postMessage({type: 'start'});
+ return saw_message;
+ })
+ .then(function(results) {
+ assert_equals(results.length, 2);
+
+ var expected_trial_1 = {
+ constructor: { name: 'ExtendableMessageEvent' },
+ origin: location.origin,
+ lastEventId: '',
+ source: {
+ constructor: { name: 'ServiceWorker' },
+ scriptURL: normalizeURL(script),
+ state: 'activated'
+ },
+ ports: []
+ };
+ assert_equals(results[0].trial, 1);
+ ExtendableMessageEventUtils.assert_equals(
+ results[0].event, expected_trial_1
+ );
+
+ var expected_trial_2 = {
+ constructor: { name: 'ExtendableMessageEvent' },
+ origin: location.origin,
+ lastEventId: '',
+ source: {
+ constructor: { name: 'ServiceWorker' },
+ scriptURL: normalizeURL(script),
+ state: 'activated'
+ },
+ ports: [],
+ };
+ assert_equals(results[1].trial, 2);
+ ExtendableMessageEventUtils.assert_equals(
+ results[1].event, expected_trial_2
+ );
+ });
+ }, 'Post loopback extendable messages');
+
+promise_test(function(t) {
+ var script1 = 'resources/extendable-message-event-ping-worker.js';
+ var script2 = 'resources/extendable-message-event-pong-worker.js';
+ var scope = 'resources/scope/extendable-message-event-pingpong';
+ var registration;
+
+ return service_worker_unregister_and_register(t, script1, scope)
+ .then(function(r) {
+ registration = r;
+ add_completion_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ // A controlled frame is necessary for keeping a waiting worker.
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ add_completion_callback(function() { frame.remove(); });
+ return navigator.serviceWorker.register(script2, {scope: scope});
+ })
+ .then(function(r) {
+ return wait_for_state(t, r.installing, 'installed');
+ })
+ .then(function() {
+ var results = [];
+ var saw_message = new Promise(function(resolve) {
+ navigator.serviceWorker.onmessage = function(event) {
+ switch (event.data.type) {
+ case 'record':
+ results.push(event.data.results);
+ break;
+ case 'finish':
+ resolve(results);
+ break;
+ }
+ };
+ });
+ registration.active.postMessage({type: 'start'});
+ return saw_message;
+ })
+ .then(function(results) {
+ assert_equals(results.length, 2);
+
+ var expected_ping = {
+ constructor: { name: 'ExtendableMessageEvent' },
+ origin: location.origin,
+ lastEventId: '',
+ source: {
+ constructor: { name: 'ServiceWorker' },
+ scriptURL: normalizeURL(script1),
+ state: 'activated'
+ },
+ ports: []
+ };
+ assert_equals(results[0].pingOrPong, 'ping');
+ ExtendableMessageEventUtils.assert_equals(
+ results[0].event, expected_ping
+ );
+
+ var expected_pong = {
+ constructor: { name: 'ExtendableMessageEvent' },
+ origin: location.origin,
+ lastEventId: '',
+ source: {
+ constructor: { name: 'ServiceWorker' },
+ scriptURL: normalizeURL(script2),
+ state: 'installed'
+ },
+ ports: []
+ };
+ assert_equals(results[1].pingOrPong, 'pong');
+ ExtendableMessageEventUtils.assert_equals(
+ results[1].event, expected_pong
+ );
+ });
+ }, 'Post extendable messages among service workers');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/fetch-on-the-right-interface.https.any.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/fetch-on-the-right-interface.https.any.js
new file mode 100644
index 0000000..5ca5f65
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/fetch-on-the-right-interface.https.any.js
@@ -0,0 +1,14 @@
+// META: title=fetch method on the right interface
+// META: global=serviceworker
+
+test(function() {
+ assert_false(self.hasOwnProperty('fetch'), 'ServiceWorkerGlobalScope ' +
+ 'instance should not have "fetch" method as its property.');
+ assert_inherits(self, 'fetch', 'ServiceWorkerGlobalScope should ' +
+ 'inherit "fetch" method.');
+ assert_own_property(Object.getPrototypeOf(Object.getPrototypeOf(self)), 'fetch',
+ 'WorkerGlobalScope should have "fetch" propery in its prototype.');
+ assert_equals(self.fetch, Object.getPrototypeOf(Object.getPrototypeOf(self)).fetch,
+ 'ServiceWorkerGlobalScope.fetch should be the same as ' +
+ 'WorkerGlobalScope.fetch.');
+}, 'Fetch method on the right interface');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.https.html b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.https.html
new file mode 100644
index 0000000..399820d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.https.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Service Worker: isSecureContext</title>
+</head>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(async (t) => {
+ var url = 'isSecureContext.serviceworker.js';
+ var scope = 'empty.html';
+ var frame_sw, sw_registration;
+
+ await service_worker_unregister(t, scope);
+ var f = await with_iframe(scope);
+ t.add_cleanup(function() {
+ f.remove();
+ });
+ frame_sw = f.contentWindow.navigator.serviceWorker;
+ var registration = await navigator.serviceWorker.register(url, {scope: scope});
+ sw_registration = registration;
+ await wait_for_state(t, registration.installing, 'activated');
+ fetch_tests_from_worker(sw_registration.active);
+}, 'Setting up tests');
+
+</script>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.serviceworker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.serviceworker.js
new file mode 100644
index 0000000..5033594
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.serviceworker.js
@@ -0,0 +1,5 @@
+importScripts("/resources/testharness.js");
+
+test(() => {
+ assert_true(self.isSecureContext, true);
+}, "isSecureContext");
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/postmessage.https.html b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/postmessage.https.html
new file mode 100644
index 0000000..99dedeb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/postmessage.https.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: postMessage</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+
+promise_test(function(t) {
+ var script = 'resources/postmessage-loopback-worker.js';
+ var scope = 'resources/scope/postmessage-loopback';
+ var registration;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = function(event) {
+ resolve(event.data);
+ };
+ });
+ registration.active.postMessage({port: channel.port2},
+ [channel.port2]);
+ return saw_message;
+ })
+ .then(function(result) {
+ assert_equals(result, 'OK');
+ });
+ }, 'Post loopback messages');
+
+promise_test(function(t) {
+ var script1 = 'resources/postmessage-ping-worker.js';
+ var script2 = 'resources/postmessage-pong-worker.js';
+ var scope = 'resources/scope/postmessage-pingpong';
+ var registration;
+ var frame;
+
+ return service_worker_unregister_and_register(t, script1, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ // A controlled frame is necessary for keeping a waiting worker.
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ return navigator.serviceWorker.register(script2, {scope: scope});
+ })
+ .then(function(r) {
+ return wait_for_state(t, r.installing, 'installed');
+ })
+ .then(function() {
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = function(event) {
+ resolve(event.data);
+ };
+ });
+ registration.active.postMessage({port: channel.port2},
+ [channel.port2]);
+ return saw_message;
+ })
+ .then(function(result) {
+ assert_equals(result, 'OK');
+ frame.remove();
+ });
+ }, 'Post messages among service workers');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/registration-attribute.https.html b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/registration-attribute.https.html
new file mode 100644
index 0000000..aa3c74a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/registration-attribute.https.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: registration</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+
+promise_test(function(t) {
+ var script = 'resources/registration-attribute-worker.js';
+ var scope = 'resources/scope/registration-attribute';
+ var registration;
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(reg) {
+ registration = reg;
+ add_result_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(frame) {
+ var expected_events_seen = [
+ 'updatefound',
+ 'install',
+ 'statechange(installed)',
+ 'statechange(activating)',
+ 'activate',
+ 'statechange(activated)',
+ 'fetch',
+ ];
+
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ expected_events_seen.toString(),
+ 'Service Worker should respond to fetch');
+ frame.remove();
+ return registration.unregister();
+ });
+ }, 'Verify registration attributes on ServiceWorkerGlobalScope');
+
+promise_test(function(t) {
+ var script = 'resources/registration-attribute-worker.js';
+ var newer_script = 'resources/registration-attribute-newer-worker.js';
+ var scope = 'resources/scope/registration-attribute';
+ var newer_worker;
+ var registration;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(reg) {
+ registration = reg;
+ add_result_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return navigator.serviceWorker.register(newer_script, {scope: scope});
+ })
+ .then(function(reg) {
+ assert_equals(reg, registration);
+ newer_worker = registration.installing;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var channel = new MessageChannel;
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = function(e) { resolve(e.data); };
+ });
+ newer_worker.postMessage({port: channel.port2}, [channel.port2]);
+ return saw_message;
+ })
+ .then(function(results) {
+ var script_url = normalizeURL(script);
+ var newer_script_url = normalizeURL(newer_script);
+ var expectations = [
+ 'evaluate',
+ ' installing: empty',
+ ' waiting: empty',
+ ' active: ' + script_url,
+ 'updatefound',
+ ' installing: ' + newer_script_url,
+ ' waiting: empty',
+ ' active: ' + script_url,
+ 'install',
+ ' installing: ' + newer_script_url,
+ ' waiting: empty',
+ ' active: ' + script_url,
+ 'statechange(installed)',
+ ' installing: empty',
+ ' waiting: ' + newer_script_url,
+ ' active: ' + script_url,
+ 'statechange(activating)',
+ ' installing: empty',
+ ' waiting: empty',
+ ' active: ' + newer_script_url,
+ 'activate',
+ ' installing: empty',
+ ' waiting: empty',
+ ' active: ' + newer_script_url,
+ 'statechange(activated)',
+ ' installing: empty',
+ ' waiting: empty',
+ ' active: ' + newer_script_url,
+ ];
+ assert_array_equals(results, expectations);
+ return registration.unregister();
+ });
+ }, 'Verify registration attributes on ServiceWorkerGlobalScope of the ' +
+ 'newer worker');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/close-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/close-worker.js
new file mode 100644
index 0000000..41a8bc0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/close-worker.js
@@ -0,0 +1,5 @@
+importScripts('../../resources/worker-testharness.js');
+
+test(function() {
+ assert_false('close' in self);
+}, 'ServiceWorkerGlobalScope close operation');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/error-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/error-worker.js
new file mode 100644
index 0000000..f6838ff
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/error-worker.js
@@ -0,0 +1,12 @@
+var source;
+
+self.addEventListener('message', function(e) {
+ source = e.source;
+ throw 'testError';
+});
+
+self.addEventListener('error', function(e) {
+ source.postMessage({
+ error: e.error, filename: e.filename, message: e.message, lineno: e.lineno,
+ colno: e.colno});
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-constructor-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-constructor-worker.js
new file mode 100644
index 0000000..42da582
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-constructor-worker.js
@@ -0,0 +1,197 @@
+importScripts('/resources/testharness.js');
+
+const TEST_OBJECT = { wanwan: 123 };
+const CHANNEL1 = new MessageChannel();
+const CHANNEL2 = new MessageChannel();
+const PORTS = [CHANNEL1.port1, CHANNEL1.port2, CHANNEL2.port1];
+function createEvent(initializer) {
+ if (initializer === undefined)
+ return new ExtendableMessageEvent('type');
+ return new ExtendableMessageEvent('type', initializer);
+}
+
+// These test cases are mostly copied from the following file in the Chromium
+// project (as of commit 848ad70823991e0f12b437d789943a4ab24d65bb):
+// third_party/WebKit/LayoutTests/fast/events/constructors/message-event-constructor.html
+
+test(function() {
+ assert_false(createEvent().bubbles);
+ assert_false(createEvent().cancelable);
+ assert_equals(createEvent().data, null);
+ assert_equals(createEvent().origin, '');
+ assert_equals(createEvent().lastEventId, '');
+ assert_equals(createEvent().source, null);
+ assert_array_equals(createEvent().ports, []);
+}, 'no initializer specified');
+
+test(function() {
+ assert_false(createEvent({ bubbles: false }).bubbles);
+ assert_true(createEvent({ bubbles: true }).bubbles);
+}, '`bubbles` is specified');
+
+test(function() {
+ assert_false(createEvent({ cancelable: false }).cancelable);
+ assert_true(createEvent({ cancelable: true }).cancelable);
+}, '`cancelable` is specified');
+
+test(function() {
+ assert_equals(createEvent({ data: TEST_OBJECT }).data, TEST_OBJECT);
+ assert_equals(createEvent({ data: undefined }).data, null);
+ assert_equals(createEvent({ data: null }).data, null);
+ assert_equals(createEvent({ data: false }).data, false);
+ assert_equals(createEvent({ data: true }).data, true);
+ assert_equals(createEvent({ data: '' }).data, '');
+ assert_equals(createEvent({ data: 'chocolate' }).data, 'chocolate');
+ assert_equals(createEvent({ data: 12345 }).data, 12345);
+ assert_equals(createEvent({ data: 18446744073709551615 }).data,
+ 18446744073709552000);
+ assert_equals(createEvent({ data: NaN }).data, NaN);
+ // Note that valueOf() is not called, when the left hand side is
+ // evaluated.
+ assert_false(
+ createEvent({ data: {
+ valueOf: function() { return TEST_OBJECT; } } }).data ==
+ TEST_OBJECT);
+ assert_equals(createEvent({ get data(){ return 123; } }).data, 123);
+ let thrown = { name: 'Error' };
+ assert_throws_exactly(thrown, function() {
+ createEvent({ get data() { throw thrown; } }); });
+}, '`data` is specified');
+
+test(function() {
+ assert_equals(createEvent({ origin: 'melancholy' }).origin, 'melancholy');
+ assert_equals(createEvent({ origin: '' }).origin, '');
+ assert_equals(createEvent({ origin: null }).origin, 'null');
+ assert_equals(createEvent({ origin: false }).origin, 'false');
+ assert_equals(createEvent({ origin: true }).origin, 'true');
+ assert_equals(createEvent({ origin: 12345 }).origin, '12345');
+ assert_equals(
+ createEvent({ origin: 18446744073709551615 }).origin,
+ '18446744073709552000');
+ assert_equals(createEvent({ origin: NaN }).origin, 'NaN');
+ assert_equals(createEvent({ origin: [] }).origin, '');
+ assert_equals(createEvent({ origin: [1, 2, 3] }).origin, '1,2,3');
+ assert_equals(
+ createEvent({ origin: { melancholy: 12345 } }).origin,
+ '[object Object]');
+ // Note that valueOf() is not called, when the left hand side is
+ // evaluated.
+ assert_equals(
+ createEvent({ origin: {
+ valueOf: function() { return 'melancholy'; } } }).origin,
+ '[object Object]');
+ assert_equals(
+ createEvent({ get origin() { return 123; } }).origin, '123');
+ let thrown = { name: 'Error' };
+ assert_throws_exactly(thrown, function() {
+ createEvent({ get origin() { throw thrown; } }); });
+}, '`origin` is specified');
+
+test(function() {
+ assert_equals(
+ createEvent({ lastEventId: 'melancholy' }).lastEventId, 'melancholy');
+ assert_equals(createEvent({ lastEventId: '' }).lastEventId, '');
+ assert_equals(createEvent({ lastEventId: null }).lastEventId, 'null');
+ assert_equals(createEvent({ lastEventId: false }).lastEventId, 'false');
+ assert_equals(createEvent({ lastEventId: true }).lastEventId, 'true');
+ assert_equals(createEvent({ lastEventId: 12345 }).lastEventId, '12345');
+ assert_equals(
+ createEvent({ lastEventId: 18446744073709551615 }).lastEventId,
+ '18446744073709552000');
+ assert_equals(createEvent({ lastEventId: NaN }).lastEventId, 'NaN');
+ assert_equals(createEvent({ lastEventId: [] }).lastEventId, '');
+ assert_equals(
+ createEvent({ lastEventId: [1, 2, 3] }).lastEventId, '1,2,3');
+ assert_equals(
+ createEvent({ lastEventId: { melancholy: 12345 } }).lastEventId,
+ '[object Object]');
+ // Note that valueOf() is not called, when the left hand side is
+ // evaluated.
+ assert_equals(
+ createEvent({ lastEventId: {
+ valueOf: function() { return 'melancholy'; } } }).lastEventId,
+ '[object Object]');
+ assert_equals(
+ createEvent({ get lastEventId() { return 123; } }).lastEventId,
+ '123');
+ let thrown = { name: 'Error' };
+ assert_throws_exactly(thrown, function() {
+ createEvent({ get lastEventId() { throw thrown; } }); });
+}, '`lastEventId` is specified');
+
+test(function() {
+ assert_equals(createEvent({ source: CHANNEL1.port1 }).source, CHANNEL1.port1);
+ assert_equals(
+ createEvent({ source: self.registration.active }).source,
+ self.registration.active);
+ assert_equals(
+ createEvent({ source: CHANNEL1.port1 }).source, CHANNEL1.port1);
+ assert_throws_js(
+ TypeError, function() { createEvent({ source: this }); },
+ 'source should be Client or ServiceWorker or MessagePort');
+}, '`source` is specified');
+
+test(function() {
+ // Valid message ports.
+ var passed_ports = createEvent({ ports: PORTS}).ports;
+ assert_equals(passed_ports[0], CHANNEL1.port1);
+ assert_equals(passed_ports[1], CHANNEL1.port2);
+ assert_equals(passed_ports[2], CHANNEL2.port1);
+ assert_array_equals(createEvent({ ports: [] }).ports, []);
+ assert_array_equals(createEvent({ ports: undefined }).ports, []);
+
+ // Invalid message ports.
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: [1, 2, 3] }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: TEST_OBJECT }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: null }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: this }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: false }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: true }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: '' }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: 'chocolate' }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: 12345 }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: 18446744073709551615 }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ ports: NaN }); });
+ assert_throws_js(TypeError,
+ function() { createEvent({ get ports() { return 123; } }); });
+ let thrown = { name: 'Error' };
+ assert_throws_exactly(thrown, function() {
+ createEvent({ get ports() { throw thrown; } }); });
+ // Note that valueOf() is not called, when the left hand side is
+ // evaluated.
+ var valueOf = function() { return PORTS; };
+ assert_throws_js(TypeError, function() {
+ createEvent({ ports: { valueOf: valueOf } }); });
+}, '`ports` is specified');
+
+test(function() {
+ var initializers = {
+ bubbles: true,
+ cancelable: true,
+ data: TEST_OBJECT,
+ origin: 'wonderful',
+ lastEventId: 'excellent',
+ source: CHANNEL1.port1,
+ ports: PORTS
+ };
+ assert_equals(createEvent(initializers).bubbles, true);
+ assert_equals(createEvent(initializers).cancelable, true);
+ assert_equals(createEvent(initializers).data, TEST_OBJECT);
+ assert_equals(createEvent(initializers).origin, 'wonderful');
+ assert_equals(createEvent(initializers).lastEventId, 'excellent');
+ assert_equals(createEvent(initializers).source, CHANNEL1.port1);
+ assert_equals(createEvent(initializers).ports[0], PORTS[0]);
+ assert_equals(createEvent(initializers).ports[1], PORTS[1]);
+ assert_equals(createEvent(initializers).ports[2], PORTS[2]);
+}, 'all initial values are specified');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-loopback-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-loopback-worker.js
new file mode 100644
index 0000000..13cae88
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-loopback-worker.js
@@ -0,0 +1,36 @@
+importScripts('./extendable-message-event-utils.js');
+
+self.addEventListener('message', function(event) {
+ switch (event.data.type) {
+ case 'start':
+ self.registration.active.postMessage(
+ {type: '1st', client_id: event.source.id});
+ break;
+ case '1st':
+ // 1st loopback message via ServiceWorkerRegistration.active.
+ var results = {
+ trial: 1,
+ event: ExtendableMessageEventUtils.serialize(event)
+ };
+ var client_id = event.data.client_id;
+ event.source.postMessage({type: '2nd', client_id: client_id});
+ event.waitUntil(clients.get(client_id)
+ .then(function(client) {
+ client.postMessage({type: 'record', results: results});
+ }));
+ break;
+ case '2nd':
+ // 2nd loopback message via ExtendableMessageEvent.source.
+ var results = {
+ trial: 2,
+ event: ExtendableMessageEventUtils.serialize(event)
+ };
+ var client_id = event.data.client_id;
+ event.waitUntil(clients.get(client_id)
+ .then(function(client) {
+ client.postMessage({type: 'record', results: results});
+ client.postMessage({type: 'finish'});
+ }));
+ break;
+ }
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-ping-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-ping-worker.js
new file mode 100644
index 0000000..d07b229
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-ping-worker.js
@@ -0,0 +1,23 @@
+importScripts('./extendable-message-event-utils.js');
+
+self.addEventListener('message', function(event) {
+ switch (event.data.type) {
+ case 'start':
+ // Send a ping message to another service worker.
+ self.registration.waiting.postMessage(
+ {type: 'ping', client_id: event.source.id});
+ break;
+ case 'pong':
+ var results = {
+ pingOrPong: 'pong',
+ event: ExtendableMessageEventUtils.serialize(event)
+ };
+ var client_id = event.data.client_id;
+ event.waitUntil(clients.get(client_id)
+ .then(function(client) {
+ client.postMessage({type: 'record', results: results});
+ client.postMessage({type: 'finish'});
+ }));
+ break;
+ }
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-pong-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-pong-worker.js
new file mode 100644
index 0000000..5e9669e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-pong-worker.js
@@ -0,0 +1,18 @@
+importScripts('./extendable-message-event-utils.js');
+
+self.addEventListener('message', function(event) {
+ switch (event.data.type) {
+ case 'ping':
+ var results = {
+ pingOrPong: 'ping',
+ event: ExtendableMessageEventUtils.serialize(event)
+ };
+ var client_id = event.data.client_id;
+ event.waitUntil(clients.get(client_id)
+ .then(function(client) {
+ client.postMessage({type: 'record', results: results});
+ event.source.postMessage({type: 'pong', client_id: client_id});
+ }));
+ break;
+ }
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-utils.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-utils.js
new file mode 100644
index 0000000..d6a3b48
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-utils.js
@@ -0,0 +1,78 @@
+var ExtendableMessageEventUtils = {};
+
+// Create a representation of a given ExtendableMessageEvent that is suitable
+// for transmission via the `postMessage` API.
+ExtendableMessageEventUtils.serialize = function(event) {
+ var ports = event.ports.map(function(port) {
+ return { constructor: { name: port.constructor.name } };
+ });
+ return {
+ constructor: {
+ name: event.constructor.name
+ },
+ origin: event.origin,
+ lastEventId: event.lastEventId,
+ source: {
+ constructor: {
+ name: event.source.constructor.name
+ },
+ url: event.source.url,
+ frameType: event.source.frameType,
+ visibilityState: event.source.visibilityState,
+ focused: event.source.focused
+ },
+ ports: ports
+ };
+};
+
+// Compare the actual and expected values of an ExtendableMessageEvent that has
+// been transformed using the `serialize` function defined in this file.
+ExtendableMessageEventUtils.assert_equals = function(actual, expected) {
+ assert_equals(
+ actual.constructor.name, expected.constructor.name, 'event constructor'
+ );
+ assert_equals(actual.origin, expected.origin, 'event `origin` property');
+ assert_equals(
+ actual.lastEventId,
+ expected.lastEventId,
+ 'event `lastEventId` property'
+ );
+
+ assert_equals(
+ actual.source.constructor.name,
+ expected.source.constructor.name,
+ 'event `source` property constructor'
+ );
+ assert_equals(
+ actual.source.url, expected.source.url, 'event `source` property `url`'
+ );
+ assert_equals(
+ actual.source.frameType,
+ expected.source.frameType,
+ 'event `source` property `frameType`'
+ );
+ assert_equals(
+ actual.source.visibilityState,
+ expected.source.visibilityState,
+ 'event `source` property `visibilityState`'
+ );
+ assert_equals(
+ actual.source.focused,
+ expected.source.focused,
+ 'event `source` property `focused`'
+ );
+
+ assert_equals(
+ actual.ports.length,
+ expected.ports.length,
+ 'event `ports` property length'
+ );
+
+ for (var idx = 0; idx < expected.ports.length; ++idx) {
+ assert_equals(
+ actual.ports[idx].constructor.name,
+ expected.ports[idx].constructor.name,
+ 'MessagePort #' + idx + ' constructor'
+ );
+ }
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-worker.js
new file mode 100644
index 0000000..f5e7647
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-worker.js
@@ -0,0 +1,5 @@
+importScripts('./extendable-message-event-utils.js');
+
+self.addEventListener('message', function(event) {
+ event.source.postMessage(ExtendableMessageEventUtils.serialize(event));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-loopback-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-loopback-worker.js
new file mode 100644
index 0000000..083e9aa
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-loopback-worker.js
@@ -0,0 +1,15 @@
+self.addEventListener('message', function(event) {
+ if ('port' in event.data) {
+ var port = event.data.port;
+
+ var channel = new MessageChannel();
+ channel.port1.onmessage = function(event) {
+ if ('pong' in event.data)
+ port.postMessage(event.data.pong);
+ };
+ self.registration.active.postMessage({ping: channel.port2},
+ [channel.port2]);
+ } else if ('ping' in event.data) {
+ event.data.ping.postMessage({pong: 'OK'});
+ }
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-ping-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-ping-worker.js
new file mode 100644
index 0000000..ebb1ecc
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-ping-worker.js
@@ -0,0 +1,15 @@
+self.addEventListener('message', function(event) {
+ if ('port' in event.data) {
+ var port = event.data.port;
+
+ var channel = new MessageChannel();
+ channel.port1.onmessage = function(event) {
+ if ('pong' in event.data)
+ port.postMessage(event.data.pong);
+ };
+
+ // Send a ping message to another service worker.
+ self.registration.waiting.postMessage({ping: channel.port2},
+ [channel.port2]);
+ }
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-pong-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-pong-worker.js
new file mode 100644
index 0000000..4a0d90b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-pong-worker.js
@@ -0,0 +1,4 @@
+self.addEventListener('message', function(event) {
+ if ('ping' in event.data)
+ event.data.ping.postMessage({pong: 'OK'});
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-newer-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-newer-worker.js
new file mode 100644
index 0000000..44f3e2e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-newer-worker.js
@@ -0,0 +1,33 @@
+// TODO(nhiroki): stop using global states because service workers can be killed
+// at any point. Instead, we could post a message to the page on each event via
+// Client object (http://crbug.com/558244).
+var results = [];
+
+function stringify(worker) {
+ return worker ? worker.scriptURL : 'empty';
+}
+
+function record(event_name) {
+ results.push(event_name);
+ results.push(' installing: ' + stringify(self.registration.installing));
+ results.push(' waiting: ' + stringify(self.registration.waiting));
+ results.push(' active: ' + stringify(self.registration.active));
+}
+
+record('evaluate');
+
+self.registration.addEventListener('updatefound', function() {
+ record('updatefound');
+ var worker = self.registration.installing;
+ self.registration.installing.addEventListener('statechange', function() {
+ record('statechange(' + worker.state + ')');
+ });
+ });
+
+self.addEventListener('install', function(e) { record('install'); });
+
+self.addEventListener('activate', function(e) { record('activate'); });
+
+self.addEventListener('message', function(e) {
+ e.data.port.postMessage(results);
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-worker.js
new file mode 100644
index 0000000..315f437
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-worker.js
@@ -0,0 +1,139 @@
+importScripts('../../resources/test-helpers.sub.js');
+importScripts('../../resources/worker-testharness.js');
+
+// TODO(nhiroki): stop using global states because service workers can be killed
+// at any point. Instead, we could post a message to the page on each event via
+// Client object (http://crbug.com/558244).
+var events_seen = [];
+
+// TODO(nhiroki): Move these assertions to registration-attribute.html because
+// an assertion failure on the worker is not shown on the result page and
+// handled as timeout. See registration-attribute-newer-worker.js for example.
+
+assert_equals(
+ self.registration.scope,
+ normalizeURL('scope/registration-attribute'),
+ 'On worker script evaluation, registration attribute should be set');
+assert_equals(
+ self.registration.installing,
+ null,
+ 'On worker script evaluation, installing worker should be null');
+assert_equals(
+ self.registration.waiting,
+ null,
+ 'On worker script evaluation, waiting worker should be null');
+assert_equals(
+ self.registration.active,
+ null,
+ 'On worker script evaluation, active worker should be null');
+
+self.registration.addEventListener('updatefound', function() {
+ events_seen.push('updatefound');
+
+ assert_equals(
+ self.registration.scope,
+ normalizeURL('scope/registration-attribute'),
+ 'On updatefound event, registration attribute should be set');
+ assert_equals(
+ self.registration.installing.scriptURL,
+ normalizeURL('registration-attribute-worker.js'),
+ 'On updatefound event, installing worker should be set');
+ assert_equals(
+ self.registration.waiting,
+ null,
+ 'On updatefound event, waiting worker should be null');
+ assert_equals(
+ self.registration.active,
+ null,
+ 'On updatefound event, active worker should be null');
+
+ assert_equals(
+ self.registration.installing.state,
+ 'installing',
+ 'On updatefound event, worker should be in the installing state');
+
+ var worker = self.registration.installing;
+ self.registration.installing.addEventListener('statechange', function() {
+ events_seen.push('statechange(' + worker.state + ')');
+ });
+ });
+
+self.addEventListener('install', function(e) {
+ events_seen.push('install');
+
+ assert_equals(
+ self.registration.scope,
+ normalizeURL('scope/registration-attribute'),
+ 'On install event, registration attribute should be set');
+ assert_equals(
+ self.registration.installing.scriptURL,
+ normalizeURL('registration-attribute-worker.js'),
+ 'On install event, installing worker should be set');
+ assert_equals(
+ self.registration.waiting,
+ null,
+ 'On install event, waiting worker should be null');
+ assert_equals(
+ self.registration.active,
+ null,
+ 'On install event, active worker should be null');
+
+ assert_equals(
+ self.registration.installing.state,
+ 'installing',
+ 'On install event, worker should be in the installing state');
+ });
+
+self.addEventListener('activate', function(e) {
+ events_seen.push('activate');
+
+ assert_equals(
+ self.registration.scope,
+ normalizeURL('scope/registration-attribute'),
+ 'On activate event, registration attribute should be set');
+ assert_equals(
+ self.registration.installing,
+ null,
+ 'On activate event, installing worker should be null');
+ assert_equals(
+ self.registration.waiting,
+ null,
+ 'On activate event, waiting worker should be null');
+ assert_equals(
+ self.registration.active.scriptURL,
+ normalizeURL('registration-attribute-worker.js'),
+ 'On activate event, active worker should be set');
+
+ assert_equals(
+ self.registration.active.state,
+ 'activating',
+ 'On activate event, worker should be in the activating state');
+ });
+
+self.addEventListener('fetch', function(e) {
+ events_seen.push('fetch');
+
+ assert_equals(
+ self.registration.scope,
+ normalizeURL('scope/registration-attribute'),
+ 'On fetch event, registration attribute should be set');
+ assert_equals(
+ self.registration.installing,
+ null,
+ 'On fetch event, installing worker should be null');
+ assert_equals(
+ self.registration.waiting,
+ null,
+ 'On fetch event, waiting worker should be null');
+ assert_equals(
+ self.registration.active.scriptURL,
+ normalizeURL('registration-attribute-worker.js'),
+ 'On fetch event, active worker should be set');
+
+ assert_equals(
+ self.registration.active.state,
+ 'activated',
+ 'On fetch event, worker should be in the activated state');
+
+ e.respondWith(new Response(events_seen));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-controlling-worker.html b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-controlling-worker.html
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-controlling-worker.html
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-worker.js
new file mode 100644
index 0000000..6da397d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-worker.js
@@ -0,0 +1,25 @@
+function matchQuery(query) {
+ return self.location.href.indexOf(query) != -1;
+}
+
+if (matchQuery('?evaluation'))
+ self.registration.unregister();
+
+self.addEventListener('install', function(e) {
+ if (matchQuery('?install')) {
+ // Don't do waitUntil(unregister()) as that would deadlock as specified.
+ self.registration.unregister();
+ }
+ });
+
+self.addEventListener('activate', function(e) {
+ if (matchQuery('?activate'))
+ e.waitUntil(self.registration.unregister());
+ });
+
+self.addEventListener('message', function(e) {
+ e.waitUntil(self.registration.unregister()
+ .then(function(result) {
+ e.data.port.postMessage({result: result});
+ }));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.js b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.js
new file mode 100644
index 0000000..8be8a1f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.js
@@ -0,0 +1,22 @@
+var events_seen = [];
+
+self.registration.addEventListener('updatefound', function() {
+ events_seen.push('updatefound');
+ });
+
+self.addEventListener('activate', function(e) {
+ events_seen.push('activate');
+ });
+
+self.addEventListener('fetch', function(e) {
+ events_seen.push('fetch');
+ e.respondWith(new Response(events_seen));
+ });
+
+self.addEventListener('message', function(e) {
+ events_seen.push('message');
+ self.registration.update();
+ });
+
+// update() during the script evaluation should be ignored.
+self.registration.update();
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py
new file mode 100644
index 0000000..8a87e1b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py
@@ -0,0 +1,16 @@
+import os
+import time
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ # update() does not bypass cache so set the max-age to 0 such that update()
+ # can find a new version in the network.
+ headers = [(b'Cache-Control', b'max-age: 0'),
+ (b'Content-Type', b'application/javascript')]
+ with open(os.path.join(os.path.dirname(isomorphic_decode(__file__)),
+ u'update-worker.js'), u'r') as file:
+ script = file.read()
+ # Return a different script for each access.
+ return headers, u'// %s\n%s' % (time.time(), script)
+
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/service-worker-error-event.https.html b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/service-worker-error-event.https.html
new file mode 100644
index 0000000..988f546
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/service-worker-error-event.https.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: Error event error message</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+promise_test(t => {
+ var script = 'resources/error-worker.js';
+ var scope = 'resources/scope/service-worker-error-event';
+ var error_name = 'testError'
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ var worker = registration.installing;
+ add_completion_callback(() => { registration.unregister(); });
+ return new Promise(function(resolve) {
+ navigator.serviceWorker.onmessage = resolve;
+ worker.postMessage('');
+ });
+ })
+ .then(e => {
+ assert_equals(e.data.error, error_name, 'error type');
+ assert_greater_than(
+ e.data.message.indexOf(error_name), -1, 'error message');
+ assert_greater_than(
+ e.data.filename.indexOf(script), -1, 'filename');
+ assert_equals(e.data.lineno, 5, 'error line number');
+ assert_equals(e.data.colno, 3, 'error column number');
+ });
+ }, 'Error handlers inside serviceworker should see the attributes of ' +
+ 'ErrorEvent');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/unregister.https.html b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/unregister.https.html
new file mode 100644
index 0000000..1a124d7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/unregister.https.html
@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: unregister</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+
+promise_test(function(t) {
+ var script = 'resources/unregister-worker.js?evaluation';
+ var scope = 'resources/scope/unregister-on-script-evaluation';
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'redundant');
+ })
+ .then(function() {
+ return navigator.serviceWorker.getRegistration(scope);
+ })
+ .then(function(result) {
+ assert_equals(
+ result,
+ undefined,
+ 'After unregister(), the registration should not found');
+ });
+ }, 'Unregister on script evaluation');
+
+promise_test(function(t) {
+ var script = 'resources/unregister-worker.js?install';
+ var scope = 'resources/scope/unregister-on-install-event';
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'redundant');
+ })
+ .then(function() {
+ return navigator.serviceWorker.getRegistration(scope);
+ })
+ .then(function(result) {
+ assert_equals(
+ result,
+ undefined,
+ 'After unregister(), the registration should not found');
+ });
+ }, 'Unregister on installing event');
+
+promise_test(function(t) {
+ var script = 'resources/unregister-worker.js?activate';
+ var scope = 'resources/scope/unregister-on-activate-event';
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'redundant');
+ })
+ .then(function() {
+ return navigator.serviceWorker.getRegistration(scope);
+ })
+ .then(function(result) {
+ assert_equals(
+ result,
+ undefined,
+ 'After unregister(), the registration should not found');
+ });
+ }, 'Unregister on activate event');
+
+promise_test(function(t) {
+ var script = 'resources/unregister-worker.js';
+ var scope = 'resources/unregister-controlling-worker.html';
+
+ var controller;
+ var frame;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ frame = f;
+ controller = frame.contentWindow.navigator.serviceWorker.controller;
+
+ assert_equals(
+ controller.scriptURL,
+ normalizeURL(script),
+ 'Service worker should control a new document')
+
+ // Wait for the completion of unregister() on the worker.
+ var channel = new MessageChannel();
+ var promise = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_true(e.data.result,
+ 'unregister() should successfully finish');
+ resolve();
+ });
+ });
+ controller.postMessage({port: channel.port2}, [channel.port2]);
+ return promise;
+ })
+ .then(function() {
+ return navigator.serviceWorker.getRegistration(scope);
+ })
+ .then(function(result) {
+ assert_equals(
+ result,
+ undefined,
+ 'After unregister(), the registration should not found');
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller,
+ controller,
+ 'After unregister(), the worker should still control the document');
+ return with_iframe(scope);
+ })
+ .then(function(new_frame) {
+ assert_equals(
+ new_frame.contentWindow.navigator.serviceWorker.controller,
+ null,
+ 'After unregister(), the worker should not control a new document');
+
+ frame.remove();
+ new_frame.remove();
+ })
+ }, 'Unregister controlling service worker');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/update.https.html b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/update.https.html
new file mode 100644
index 0000000..a7dde22
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ServiceWorkerGlobalScope/update.https.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<title>ServiceWorkerGlobalScope: update</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../resources/test-helpers.sub.js'></script>
+<script>
+
+promise_test(function(t) {
+ var script = 'resources/update-worker.py';
+ var scope = 'resources/scope/update';
+ var registration;
+ var frame1;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ frame1 = f;
+ registration.active.postMessage('update');
+ return wait_for_update(t, registration);
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(frame2) {
+ var expected_events_seen = [
+ 'updatefound', // by register().
+ 'activate',
+ 'fetch',
+ 'message',
+ 'updatefound', // by update() in the message handler.
+ 'fetch',
+ ];
+ assert_equals(
+ frame2.contentDocument.body.textContent,
+ expected_events_seen.toString(),
+ 'events seen by the worker');
+ frame1.remove();
+ frame2.remove();
+ });
+ }, 'Update a registration on ServiceWorkerGlobalScope');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/about-blank-replacement.https.html b/third_party/web_platform_tests/service-workers/service-worker/about-blank-replacement.https.html
new file mode 100644
index 0000000..b6efe3e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/about-blank-replacement.https.html
@@ -0,0 +1,181 @@
+<!DOCTYPE html>
+<title>Service Worker: about:blank replacement handling</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// This test attempts to verify various initial about:blank document
+// creation is accurately reflected via the Clients API. The goal is
+// for Clients API to reflect what the browser actually does and not
+// to make special cases for the API.
+//
+// If your browser does not create an about:blank document in certain
+// cases then please just mark the test expected fail for now. The
+// reuse of globals from about:blank documents to the final load document
+// has particularly bad interop at the moment. Hopefully we can evolve
+// tests like this to eventually align browsers.
+
+const worker = 'resources/about-blank-replacement-worker.js';
+
+// Helper routine that creates an iframe that internally has some kind
+// of nested window. The nested window could be another iframe or
+// it could be a popup window.
+function createFrameWithNestedWindow(url) {
+ return new Promise((resolve, reject) => {
+ let frame = document.createElement('iframe');
+ frame.src = url;
+ document.body.appendChild(frame);
+
+ window.addEventListener('message', function onMsg(evt) {
+ if (evt.data.type !== 'NESTED_LOADED') {
+ return;
+ }
+ window.removeEventListener('message', onMsg);
+ if (evt.data.result && evt.data.result.startsWith('failure:')) {
+ reject(evt.data.result);
+ return;
+ }
+ resolve(frame);
+ });
+ });
+}
+
+// Helper routine to request the given worker find the client with
+// the specified URL using the clients.matchAll() API.
+function getClientIdByURL(worker, url) {
+ return new Promise(resolve => {
+ navigator.serviceWorker.addEventListener('message', function onMsg(evt) {
+ if (evt.data.type !== 'GET_CLIENT_ID') {
+ return;
+ }
+ navigator.serviceWorker.removeEventListener('message', onMsg);
+ resolve(evt.data.result);
+ });
+ worker.postMessage({ type: 'GET_CLIENT_ID', url: url.toString() });
+ });
+}
+
+async function doAsyncTest(t, scope) {
+ let reg = await service_worker_unregister_and_register(t, worker, scope);
+
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Load the scope as a frame. We expect this in turn to have a nested
+ // iframe. The service worker will intercept the load of the nested
+ // iframe and populate its body with the client ID of the initial
+ // about:blank document it sees via clients.matchAll().
+ let frame = await createFrameWithNestedWindow(scope);
+ let initialResult = frame.contentWindow.nested().document.body.textContent;
+ assert_false(initialResult.startsWith('failure:'), `result: ${initialResult}`);
+
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ frame.contentWindow.nested().navigator.serviceWorker.controller.scriptURL,
+ 'nested about:blank should have same controlling service worker');
+
+ // Next, ask the service worker to find the final client ID for the fully
+ // loaded nested frame.
+ let nestedURL = new URL(frame.contentWindow.nested().location);
+ let finalResult = await getClientIdByURL(reg.active, nestedURL);
+ assert_false(finalResult.startsWith('failure:'), `result: ${finalResult}`);
+
+ // If the nested frame doesn't have a URL to load, then there is no fetch
+ // event and the body should be empty. We can't verify the final client ID
+ // against anything.
+ if (nestedURL.href === 'about:blank' ||
+ nestedURL.href === 'about:srcdoc') {
+ assert_equals('', initialResult, 'about:blank text content should be blank');
+ }
+
+ // If the nested URL is not about:blank, though, then the fetch event handler
+ // should have populated the body with the client id of the initial about:blank.
+ // Verify the final client id matches.
+ else {
+ assert_equals(initialResult, finalResult, 'client ID values should match');
+ }
+
+ frame.remove();
+}
+
+promise_test(async function(t) {
+ // Execute a test where the nested frame is simply loaded normally.
+ await doAsyncTest(t, 'resources/about-blank-replacement-frame.py');
+}, 'Initial about:blank is controlled, exposed to clients.matchAll(), and ' +
+ 'matches final Client.');
+
+promise_test(async function(t) {
+ // Execute a test where the nested frame is modified immediately by
+ // its parent. In this case we add a message listener so the service
+ // worker can ping the client to verify its existence. This ping-pong
+ // check is performed during the initial load and when verifying the
+ // final loaded client.
+ await doAsyncTest(t, 'resources/about-blank-replacement-ping-frame.py');
+}, 'Initial about:blank modified by parent is controlled, exposed to ' +
+ 'clients.matchAll(), and matches final Client.');
+
+promise_test(async function(t) {
+ // Execute a test where the nested window is a popup window instead of
+ // an iframe. This should behave the same as the simple iframe case.
+ await doAsyncTest(t, 'resources/about-blank-replacement-popup-frame.py');
+}, 'Popup initial about:blank is controlled, exposed to clients.matchAll(), and ' +
+ 'matches final Client.');
+
+promise_test(async function(t) {
+ const scope = 'resources/about-blank-replacement-uncontrolled-nested-frame.html';
+
+ let reg = await service_worker_unregister_and_register(t, worker, scope);
+
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Load the scope as a frame. We expect this in turn to have a nested
+ // iframe. Unlike the other tests in this file the nested iframe URL
+ // is not covered by a service worker scope. It should end up as
+ // uncontrolled even though its initial about:blank is controlled.
+ let frame = await createFrameWithNestedWindow(scope);
+ let nested = frame.contentWindow.nested();
+ let initialResult = nested.document.body.textContent;
+
+ // The nested iframe should not have been intercepted by the service
+ // worker. The empty.html nested frame has "hello world" for its body.
+ assert_equals(initialResult.trim(), 'hello world', `result: ${initialResult}`);
+
+ assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller, null,
+ 'outer frame should be controlled');
+
+ assert_equals(nested.navigator.serviceWorker.controller, null,
+ 'nested frame should not be controlled');
+
+ frame.remove();
+}, 'Initial about:blank is controlled, exposed to clients.matchAll(), and ' +
+ 'final Client is not controlled by a service worker.');
+
+promise_test(async function(t) {
+ // Execute a test where the nested frame is an iframe without a src
+ // attribute. This simple nested about:blank should still inherit the
+ // controller and be visible to clients.matchAll().
+ await doAsyncTest(t, 'resources/about-blank-replacement-blank-nested-frame.html');
+}, 'Simple about:blank is controlled and is exposed to clients.matchAll().');
+
+promise_test(async function(t) {
+ // Execute a test where the nested frame is an iframe using a non-empty
+ // srcdoc containing only a tag pair so its textContent is still empty.
+ // This nested iframe should still inherit the controller and be visible
+ // to clients.matchAll().
+ await doAsyncTest(t, 'resources/about-blank-replacement-srcdoc-nested-frame.html');
+}, 'Nested about:srcdoc is controlled and is exposed to clients.matchAll().');
+
+promise_test(async function(t) {
+ // Execute a test where the nested frame is dynamically added without a src
+ // attribute. This simple nested about:blank should still inherit the
+ // controller and be visible to clients.matchAll().
+ await doAsyncTest(t, 'resources/about-blank-replacement-blank-dynamic-nested-frame.html');
+}, 'Dynamic about:blank is controlled and is exposed to clients.matchAll().');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/activate-event-after-install-state-change.https.html b/third_party/web_platform_tests/service-workers/service-worker/activate-event-after-install-state-change.https.html
new file mode 100644
index 0000000..016a52c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/activate-event-after-install-state-change.https.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<title>Service Worker: registration events</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var scope = 'resources/blank.html';
+ var registration;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ var sw = registration.installing;
+
+ return new Promise(t.step_func(function(resolve) {
+ sw.onstatechange = t.step_func(function() {
+ if (sw.state === 'installed') {
+ assert_equals(registration.active, null,
+ 'installed event should be fired before activating service worker');
+ resolve();
+ }
+ });
+ }));
+ })
+ .catch(unreached_rejection(t));
+ }, 'installed event should be fired before activating service worker');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/activation-after-registration.https.html b/third_party/web_platform_tests/service-workers/service-worker/activation-after-registration.https.html
new file mode 100644
index 0000000..29f97e3
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/activation-after-registration.https.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<title>Service Worker: Activation occurs after registration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(function(t) {
+ var scope = 'resources/blank.html';
+ var registration;
+
+ return service_worker_unregister_and_register(
+ t, 'resources/empty-worker.js', scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ assert_equals(
+ r.installing.state,
+ 'installing',
+ 'worker should be in the "installing" state upon registration');
+ return wait_for_state(t, r.installing, 'activated');
+ });
+}, 'activation occurs after registration');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/activation.https.html b/third_party/web_platform_tests/service-workers/service-worker/activation.https.html
new file mode 100644
index 0000000..278454d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/activation.https.html
@@ -0,0 +1,168 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<title>service worker: activation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// Returns {registration, iframe}, where |registration| has an active and
+// waiting worker. The active worker controls |iframe| and has an inflight
+// message event that can be finished by calling
+// |registration.active.postMessage('go')|.
+function setup_activation_test(t, scope, worker_url) {
+ var registration;
+ var iframe;
+ return navigator.serviceWorker.getRegistration(scope)
+ .then(r => {
+ if (r)
+ return r.unregister();
+ })
+ .then(() => {
+ // Create an in-scope iframe. Do this prior to registration to avoid
+ // racing between an update triggered by navigation and the update()
+ // call below.
+ return with_iframe(scope);
+ })
+ .then(f => {
+ iframe = f;
+ // Create an active worker.
+ return navigator.serviceWorker.register(worker_url, { scope: scope });
+ })
+ .then(r => {
+ registration = r;
+ add_result_callback(() => registration.unregister());
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(() => {
+ // Check that the frame was claimed.
+ assert_not_equals(
+ iframe.contentWindow.navigator.serviceWorker.controller, null);
+ // Create an in-flight request.
+ registration.active.postMessage('wait');
+ // Now there is both a controllee and an in-flight request.
+ // Initiate an update.
+ return registration.update();
+ })
+ .then(() => {
+ // Wait for a waiting worker.
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(() => {
+ return wait_for_activation_on_sample_scope(t, self);
+ })
+ .then(() => {
+ assert_not_equals(registration.waiting, null);
+ assert_not_equals(registration.active, null);
+ return Promise.resolve({registration: registration, iframe: iframe});
+ });
+}
+promise_test(t => {
+ var scope = 'resources/no-controllee';
+ var worker_url = 'resources/mint-new-worker.py';
+ var registration;
+ var iframe;
+ var new_worker;
+ return setup_activation_test(t, scope, worker_url)
+ .then(result => {
+ registration = result.registration;
+ iframe = result.iframe;
+ // Finish the in-flight request.
+ registration.active.postMessage('go');
+ return wait_for_activation_on_sample_scope(t, self);
+ })
+ .then(() => {
+ // The new worker is still waiting. Remove the frame and it should
+ // activate.
+ new_worker = registration.waiting;
+ assert_equals(new_worker.state, 'installed');
+ var reached_active = wait_for_state(t, new_worker, 'activating');
+ iframe.remove();
+ return reached_active;
+ })
+ .then(() => {
+ assert_equals(new_worker, registration.active);
+ });
+ }, 'loss of controllees triggers activation');
+promise_test(t => {
+ var scope = 'resources/no-request';
+ var worker_url = 'resources/mint-new-worker.py';
+ var registration;
+ var iframe;
+ var new_worker;
+ return setup_activation_test(t, scope, worker_url)
+ .then(result => {
+ registration = result.registration;
+ iframe = result.iframe;
+ // Remove the iframe.
+ iframe.remove();
+ return new Promise(resolve => setTimeout(resolve, 0));
+ })
+ .then(() => {
+ // Finish the request.
+ new_worker = registration.waiting;
+ var reached_active = wait_for_state(t, new_worker, 'activating');
+ registration.active.postMessage('go');
+ return reached_active;
+ })
+ .then(() => {
+ assert_equals(registration.active, new_worker);
+ });
+ }, 'finishing a request triggers activation');
+promise_test(t => {
+ var scope = 'resources/skip-waiting';
+ var worker_url = 'resources/mint-new-worker.py?skip-waiting';
+ var registration;
+ var iframe;
+ var new_worker;
+ return setup_activation_test(t, scope, worker_url)
+ .then(result => {
+ registration = result.registration;
+ iframe = result.iframe;
+ // Finish the request. The iframe does not need to be removed because
+ // skipWaiting() was called.
+ new_worker = registration.waiting;
+ var reached_active = wait_for_state(t, new_worker, 'activating');
+ registration.active.postMessage('go');
+ return reached_active;
+ })
+ .then(() => {
+ assert_equals(registration.active, new_worker);
+ // Remove the iframe.
+ iframe.remove();
+ });
+ }, 'skipWaiting bypasses no controllee requirement');
+
+// This test is not really about activation, but otherwise is very
+// similar to the other tests here.
+promise_test(t => {
+ var scope = 'resources/unregister';
+ var worker_url = 'resources/mint-new-worker.py';
+ var registration;
+ var iframe;
+ var new_worker;
+ return setup_activation_test(t, scope, worker_url)
+ .then(result => {
+ registration = result.registration;
+ iframe = result.iframe;
+ // Remove the iframe.
+ iframe.remove();
+ return registration.unregister();
+ })
+ .then(() => {
+ // The unregister operation should wait for the active worker to
+ // finish processing its events before clearing the registration.
+ new_worker = registration.waiting;
+ var reached_redundant = wait_for_state(t, new_worker, 'redundant');
+ registration.active.postMessage('go');
+ return reached_redundant;
+ })
+ .then(() => {
+ assert_equals(registration.installing, null);
+ assert_equals(registration.waiting, null);
+ assert_equals(registration.active, null);
+ });
+ }, 'finishing a request triggers unregister');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/active.https.html b/third_party/web_platform_tests/service-workers/service-worker/active.https.html
new file mode 100644
index 0000000..350a34b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/active.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<title>ServiceWorker: navigator.serviceWorker.active</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const SCRIPT = 'resources/empty-worker.js';
+const SCOPE = 'resources/blank.html';
+
+// "active" is set
+promise_test(async t => {
+
+ t.add_cleanup(async() => {
+ if (frame)
+ frame.remove();
+ if (registration)
+ await registration.unregister();
+ });
+
+ await service_worker_unregister(t, SCOPE);
+ const frame = await with_iframe(SCOPE);
+ const registration =
+ await navigator.serviceWorker.register(SCRIPT, {scope: SCOPE});
+ await wait_for_state(t, registration.installing, 'activating');
+ const container = frame.contentWindow.navigator.serviceWorker;
+ assert_equals(container.controller, null, 'controller');
+ assert_equals(registration.active.state, 'activating',
+ 'registration.active');
+ assert_equals(registration.waiting, null, 'registration.waiting');
+ assert_equals(registration.installing, null, 'registration.installing');
+
+ // FIXME: Add a test for a frame created after installation.
+ // Should the existing frame ("frame") block activation?
+}, 'active is set');
+
+// Tests that the ServiceWorker objects returned from active attribute getter
+// that represent the same service worker are the same objects.
+promise_test(async t => {
+ const registration1 =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ const registration2 = await navigator.serviceWorker.getRegistration(SCOPE);
+ assert_equals(registration1.active, registration2.active,
+ 'ServiceWorkerRegistration.active should return the same ' +
+ 'object');
+ await registration1.unregister();
+}, 'The ServiceWorker objects returned from active attribute getter that ' +
+ 'represent the same service worker are the same objects');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/claim-affect-other-registration.https.html b/third_party/web_platform_tests/service-workers/service-worker/claim-affect-other-registration.https.html
new file mode 100644
index 0000000..52555ac
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/claim-affect-other-registration.https.html
@@ -0,0 +1,136 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Service Worker: claim() should affect other registration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+ var frame;
+
+ var init_scope = 'resources/blank.html?affect-other';
+ var init_worker_url = 'resources/empty.js';
+ var init_registration;
+ var init_workers = [undefined, undefined];
+
+ var claim_scope = 'resources/blank.html?affect-other-registration';
+ var claim_worker_url = 'resources/claim-worker.js';
+ var claim_registration;
+ var claim_worker;
+
+ return Promise.resolve()
+ // Register the first service worker to init_scope.
+ .then(() => navigator.serviceWorker.register(init_worker_url + '?v1',
+ {scope: init_scope}))
+ .then(r => {
+ init_registration = r;
+ init_workers[0] = r.installing;
+ return Promise.resolve()
+ .then(() => wait_for_state(t, init_workers[0], 'activated'))
+ .then(() => assert_array_equals([init_registration.active,
+ init_registration.waiting,
+ init_registration.installing],
+ [init_workers[0],
+ null,
+ null],
+ 'Wrong workers.'));
+ })
+
+ // Create an iframe as the client of the first service worker of init_scope.
+ .then(() => with_iframe(claim_scope))
+ .then(f => frame = f)
+
+ // Check the controller.
+ .then(() => frame.contentWindow.navigator.serviceWorker.getRegistration(
+ normalizeURL(init_scope)))
+ .then(r => assert_equals(frame.contentWindow.navigator.serviceWorker.controller,
+ r.active,
+ '.controller should belong to init_scope.'))
+
+ // Register the second service worker to init_scope.
+ .then(() => navigator.serviceWorker.register(init_worker_url + '?v2',
+ {scope: init_scope}))
+ .then(r => {
+ assert_equals(r, init_registration, 'Should be the same registration');
+ init_workers[1] = r.installing;
+ return Promise.resolve()
+ .then(() => wait_for_state(t, init_workers[1], 'installed'))
+ .then(() => assert_array_equals([init_registration.active,
+ init_registration.waiting,
+ init_registration.installing],
+ [init_workers[0],
+ init_workers[1],
+ null],
+ 'Wrong workers.'));
+ })
+
+ // Register a service worker to claim_scope.
+ .then(() => navigator.serviceWorker.register(claim_worker_url,
+ {scope: claim_scope}))
+ .then(r => {
+ claim_registration = r;
+ claim_worker = r.installing;
+ return wait_for_state(t, claim_worker, 'activated')
+ })
+
+ // Let claim_worker claim the created iframe.
+ .then(function() {
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS',
+ 'Worker call to claim() should fulfill.');
+ resolve();
+ });
+ });
+
+ claim_worker.postMessage({port: channel.port2}, [channel.port2]);
+ return saw_message;
+ })
+
+ // Check the controller.
+ .then(() => frame.contentWindow.navigator.serviceWorker.getRegistration(
+ normalizeURL(claim_scope)))
+ .then(r => assert_equals(frame.contentWindow.navigator.serviceWorker.controller,
+ r.active,
+ '.controller should belong to claim_scope.'))
+
+ // Check the status of created registrations and service workers.
+ .then(() => wait_for_state(t, init_workers[1], 'activated'))
+ .then(() => {
+ assert_array_equals([claim_registration.active,
+ claim_registration.waiting,
+ claim_registration.installing],
+ [claim_worker,
+ null,
+ null],
+ 'claim_worker should be the only worker.')
+
+ assert_array_equals([init_registration.active,
+ init_registration.waiting,
+ init_registration.installing],
+ [init_workers[1],
+ null,
+ null],
+ 'The waiting sw should become the active worker.')
+
+ assert_array_equals([init_workers[0].state,
+ init_workers[1].state,
+ claim_worker.state],
+ ['redundant',
+ 'activated',
+ 'activated'],
+ 'Wrong worker states.');
+ })
+
+ // Cleanup and finish testing.
+ .then(() => frame.remove())
+ .then(() => Promise.all([
+ init_registration.unregister(),
+ claim_registration.unregister()
+ ]))
+ .then(() => t.done());
+}, 'claim() should affect the originally controlling registration.');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/claim-fetch.https.html b/third_party/web_platform_tests/service-workers/service-worker/claim-fetch.https.html
new file mode 100644
index 0000000..ae0082d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/claim-fetch.https.html
@@ -0,0 +1,90 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+async function tryFetch(fetchFunc, path) {
+ let response;
+ try {
+ response = await fetchFunc(path);
+ } catch (err) {
+ throw (`fetch() threw: ${err}`);
+ }
+
+ let responseText;
+ try {
+ responseText = await response.text();
+ } catch (err) {
+ throw (`text() threw: ${err}`);
+ }
+
+ return responseText;
+}
+
+promise_test(async function(t) {
+ const scope = 'resources/';
+ const script = 'resources/claim-worker.js';
+ const resource = 'simple.txt';
+
+ // Create the test frame.
+ const frame = await with_iframe('resources/blank.html');
+ t.add_cleanup(() => frame.remove());
+
+ // Check the controller and test with fetch.
+ assert_equals(frame.contentWindow.navigator.controller, undefined,
+ 'Should have no controller.');
+ let response;
+ try {
+ response = await tryFetch(frame.contentWindow.fetch, resource);
+ } catch (err) {
+ assert_unreached(`uncontrolled fetch failed: ${err}`);
+ }
+ assert_equals(response, 'a simple text file\n',
+ 'fetch() should not be intercepted.');
+
+ // Register a service worker.
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+
+ // Register a controllerchange event to wait until the controller is updated
+ // and check if the frame is controlled by a service worker.
+ const controllerChanged = new Promise((resolve) => {
+ frame.contentWindow.navigator.serviceWorker.oncontrollerchange = () => {
+ resolve(frame.contentWindow.navigator.serviceWorker.controller);
+ };
+ });
+
+ // Tell the service worker to claim the iframe.
+ const sawMessage = new Promise((resolve) => {
+ const channel = new MessageChannel();
+ channel.port1.onmessage = t.step_func((event) => {
+ resolve(event.data);
+ });
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ });
+ const data = await sawMessage;
+ assert_equals(data, 'PASS', 'Worker call to claim() should fulfill.');
+
+ // Check if the controller is updated after claim() and test with fetch.
+ const controller = await controllerChanged;
+ assert_true(controller instanceof frame.contentWindow.ServiceWorker,
+ 'iframe should be controlled.');
+ try {
+ response = await tryFetch(frame.contentWindow.fetch, resource);
+ } catch (err) {
+ assert_unreached(`controlled fetch failed: ${err}`);
+ }
+ assert_equals(response, 'Intercepted!',
+ 'fetch() should be intercepted.');
+}, 'fetch() should be intercepted after the client is claimed.');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/claim-not-using-registration.https.html b/third_party/web_platform_tests/service-workers/service-worker/claim-not-using-registration.https.html
new file mode 100644
index 0000000..fd61d05
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/claim-not-using-registration.https.html
@@ -0,0 +1,131 @@
+<!DOCTYPE html>
+<title>Service Worker: claim client not using registration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+promise_test(function(t) {
+ var init_scope = 'resources/blank.html?not-using-init';
+ var claim_scope = 'resources/blank.html?not-using';
+ var init_worker_url = 'resources/empty.js';
+ var claim_worker_url = 'resources/claim-worker.js';
+ var claim_worker, claim_registration, frame1, frame2;
+ return service_worker_unregister_and_register(
+ t, init_worker_url, init_scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, init_scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return Promise.all(
+ [with_iframe(init_scope), with_iframe(claim_scope)]);
+ })
+ .then(function(frames) {
+ frame1 = frames[0];
+ frame2 = frames[1];
+ assert_equals(
+ frame1.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ normalizeURL(init_worker_url),
+ 'Frame1 controller should not be null');
+ assert_equals(
+ frame2.contentWindow.navigator.serviceWorker.controller, null,
+ 'Frame2 controller should be null');
+ return navigator.serviceWorker.register(claim_worker_url,
+ {scope: claim_scope});
+ })
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, claim_scope);
+ });
+
+ claim_worker = registration.installing;
+ claim_registration = registration;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var saw_controllerchanged = new Promise(function(resolve) {
+ frame2.contentWindow.navigator.serviceWorker.oncontrollerchange =
+ function() { resolve(); }
+ });
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS',
+ 'Worker call to claim() should fulfill.');
+ resolve();
+ });
+ });
+ claim_worker.postMessage({port: channel.port2}, [channel.port2]);
+ return Promise.all([saw_controllerchanged, saw_message]);
+ })
+ .then(function() {
+ assert_equals(
+ frame1.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ normalizeURL(init_worker_url),
+ 'Frame1 should not be influenced');
+ assert_equals(
+ frame2.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ normalizeURL(claim_worker_url),
+ 'Frame2 should be controlled by the new registration');
+ frame1.remove();
+ frame2.remove();
+ return claim_registration.unregister();
+ });
+ }, 'Test claim client which is not using registration');
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html?longer-matched';
+ var claim_scope = 'resources/blank.html?longer';
+ var claim_worker_url = 'resources/claim-worker.js';
+ var installing_worker_url = 'resources/empty-worker.js';
+ var frame, claim_worker;
+ return with_iframe(scope)
+ .then(function(f) {
+ frame = f;
+ return navigator.serviceWorker.register(
+ claim_worker_url, {scope: claim_scope});
+ })
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, claim_scope);
+ });
+
+ claim_worker = registration.installing;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return navigator.serviceWorker.register(
+ installing_worker_url, {scope: scope});
+ })
+ .then(function() {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS',
+ 'Worker call to claim() should fulfill.');
+ resolve();
+ });
+ });
+ claim_worker.postMessage({port: channel.port2}, [channel.port2]);
+ return saw_message;
+ })
+ .then(function() {
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller, null,
+ 'Frame should not be claimed when a longer-matched ' +
+ 'registration exists');
+ frame.remove();
+ });
+ }, 'Test claim client when there\'s a longer-matched registration not ' +
+ 'already used by the page');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/claim-shared-worker-fetch.https.html b/third_party/web_platform_tests/service-workers/service-worker/claim-shared-worker-fetch.https.html
new file mode 100644
index 0000000..f5f4488
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/claim-shared-worker-fetch.https.html
@@ -0,0 +1,71 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+promise_test(function(t) {
+ var frame;
+ var resource = 'simple.txt';
+
+ var worker;
+ var scope = 'resources/';
+ var script = 'resources/claim-worker.js';
+
+ return Promise.resolve()
+ // Create the test iframe with a shared worker.
+ .then(() => with_iframe('resources/claim-shared-worker-fetch-iframe.html'))
+ .then(f => frame = f)
+
+ // Check the controller and test with fetch in the shared worker.
+ .then(() => assert_equals(frame.contentWindow.navigator.controller,
+ undefined,
+ 'Should have no controller.'))
+ .then(() => frame.contentWindow.fetch_in_shared_worker(resource))
+ .then(response_text => assert_equals(response_text,
+ 'a simple text file\n',
+ 'fetch() should not be intercepted.'))
+ // Register a service worker.
+ .then(() => service_worker_unregister_and_register(t, script, scope))
+ .then(r => {
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ worker = r.installing;
+
+ return wait_for_state(t, worker, 'activated')
+ })
+ // Let the service worker claim the iframe and the shared worker.
+ .then(() => {
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS',
+ 'Worker call to claim() should fulfill.');
+ resolve();
+ });
+ });
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ return saw_message;
+ })
+
+ // Check the controller and test with fetch in the shared worker.
+ .then(() => frame.contentWindow.navigator.serviceWorker.getRegistration(scope))
+ .then(r => assert_equals(frame.contentWindow.navigator.serviceWorker.controller,
+ r.active,
+ 'Test iframe should be claimed.'))
+ // TODO(horo): Check the SharedWorker's navigator.seviceWorker.controller.
+ .then(() => frame.contentWindow.fetch_in_shared_worker(resource))
+ .then(response_text =>
+ assert_equals(response_text,
+ 'Intercepted!',
+ 'fetch() in the shared worker should be intercepted.'))
+
+ // Cleanup this testcase.
+ .then(() => frame.remove());
+}, 'fetch() in SharedWorker should be intercepted after the client is claimed.')
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/claim-using-registration.https.html b/third_party/web_platform_tests/service-workers/service-worker/claim-using-registration.https.html
new file mode 100644
index 0000000..8a2a6ff
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/claim-using-registration.https.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<title>Service Worker: claim client using registration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+ var scope = 'resources/';
+ var frame_url = 'resources/blank.html?using-different-registration';
+ var url1 = 'resources/empty.js';
+ var url2 = 'resources/claim-worker.js';
+ var worker, sw_registration, frame;
+ return service_worker_unregister_and_register(t, url1, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(frame_url);
+ })
+ .then(function(f) {
+ frame = f;
+ return navigator.serviceWorker.register(url2, {scope: frame_url});
+ })
+ .then(function(registration) {
+ worker = registration.installing;
+ sw_registration = registration;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var saw_controllerchanged = new Promise(function(resolve) {
+ frame.contentWindow.navigator.serviceWorker.oncontrollerchange =
+ function() { resolve(); }
+ });
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS',
+ 'Worker call to claim() should fulfill.');
+ resolve();
+ });
+ });
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ return Promise.all([saw_controllerchanged, saw_message]);
+ })
+ .then(function() {
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ normalizeURL(url2),
+ 'Frame1 controller scriptURL should be changed to url2');
+ frame.remove();
+ return sw_registration.unregister();
+ });
+ }, 'Test worker claims client which is using another registration');
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html?using-same-registration';
+ var url1 = 'resources/empty.js';
+ var url2 = 'resources/claim-worker.js';
+ var frame, worker;
+ return service_worker_unregister_and_register(t, url1, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ return navigator.serviceWorker.register(url2, {scope: scope});
+ })
+ .then(function(registration) {
+ worker = registration.installing;
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'FAIL: exception: InvalidStateError',
+ 'Worker call to claim() should reject with ' +
+ 'InvalidStateError');
+ resolve();
+ });
+ });
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ return saw_message;
+ })
+ .then(function() {
+ frame.remove();
+ });
+ }, 'Test for the waiting worker claims a client which is using the the ' +
+ 'same registration');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/claim-with-redirect.https.html b/third_party/web_platform_tests/service-workers/service-worker/claim-with-redirect.https.html
new file mode 100644
index 0000000..fd89cb9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/claim-with-redirect.https.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<title>Service Worker: Claim() when update happens after redirect</title>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var host_info = get_host_info();
+var BASE_URL = host_info['HTTPS_ORIGIN'] + base_path();
+var OTHER_BASE_URL = host_info['HTTPS_REMOTE_ORIGIN'] + base_path();
+
+var WORKER_URL = OTHER_BASE_URL + 'resources/update-claim-worker.py'
+var SCOPE_URL = OTHER_BASE_URL + 'resources/redirect.py'
+var OTHER_IFRAME_URL = OTHER_BASE_URL +
+ 'resources/claim-with-redirect-iframe.html';
+
+// Different origin from the registration
+var REDIRECT_TO_URL = BASE_URL +
+ 'resources/claim-with-redirect-iframe.html?redirected';
+
+var REGISTER_IFRAME_URL = OTHER_IFRAME_URL + '?register=' +
+ encodeURIComponent(WORKER_URL) + '&' +
+ 'scope=' + encodeURIComponent(SCOPE_URL);
+var REDIRECT_IFRAME_URL = SCOPE_URL + '?Redirect=' +
+ encodeURIComponent(REDIRECT_TO_URL);
+var UPDATE_IFRAME_URL = OTHER_IFRAME_URL + '?update=' +
+ encodeURIComponent(SCOPE_URL);
+var UNREGISTER_IFRAME_URL = OTHER_IFRAME_URL + '?unregister=' +
+ encodeURIComponent(SCOPE_URL);
+
+var waiting_resolver = undefined;
+
+addEventListener('message', e => {
+ if (waiting_resolver !== undefined) {
+ waiting_resolver(e.data);
+ }
+ });
+
+function assert_with_iframe(url, expected_message) {
+ return new Promise(resolve => {
+ waiting_resolver = resolve;
+ with_iframe(url);
+ })
+ .then(data => assert_equals(data.message, expected_message));
+}
+
+// This test checks behavior when browser got a redirect header from in-scope
+// page and navigated to out-of-scope page which has a different origin from any
+// registrations.
+promise_test(t => {
+ return assert_with_iframe(REGISTER_IFRAME_URL, 'registered')
+ .then(() => assert_with_iframe(REDIRECT_IFRAME_URL, 'redirected'))
+ .then(() => assert_with_iframe(UPDATE_IFRAME_URL, 'updated'))
+ .then(() => assert_with_iframe(UNREGISTER_IFRAME_URL, 'unregistered'));
+ }, 'Claim works after redirection to another origin');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/claim-worker-fetch.https.html b/third_party/web_platform_tests/service-workers/service-worker/claim-worker-fetch.https.html
new file mode 100644
index 0000000..7cb26c7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/claim-worker-fetch.https.html
@@ -0,0 +1,83 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+promise_test((t) => {
+ return runTest(t, 'resources/claim-worker-fetch-iframe.html');
+}, 'fetch() in Worker should be intercepted after the client is claimed.');
+
+promise_test((t) => {
+ return runTest(t, 'resources/claim-nested-worker-fetch-iframe.html');
+}, 'fetch() in nested Worker should be intercepted after the client is claimed.');
+
+promise_test((t) => {
+ return runTest(t, 'resources/claim-blob-url-worker-fetch-iframe.html');
+}, 'fetch() in blob URL Worker should be intercepted after the client is claimed.');
+
+promise_test((t) => {
+ return runTest(t, 'resources/nested-blob-url-workers.html');
+}, 'fetch() in nested blob URL Worker created from a blob URL Worker should be intercepted after the client is claimed.');
+
+promise_test((t) => {
+ return runTest(t, 'resources/nested-worker-created-from-blob-url-worker.html');
+}, 'fetch() in nested Worker created from a blob URL Worker should be intercepted after the client is claimed.');
+
+promise_test((t) => {
+ return runTest(t, 'resources/nested-blob-url-worker-created-from-worker.html');
+}, 'fetch() in nested blob URL Worker created from a Worker should be intercepted after the client is claimed.');
+
+async function runTest(t, iframe_url) {
+ const resource = 'simple.txt';
+ const scope = 'resources/';
+ const script = 'resources/claim-worker.js';
+
+ // Create the test iframe with a dedicated worker.
+ const frame = await with_iframe(iframe_url);
+ t.add_cleanup(_ => frame.remove());
+
+ // Check the controller and test with fetch in the worker.
+ assert_equals(frame.contentWindow.navigator.controller,
+ undefined, 'Should have no controller.');
+ {
+ const response_text = await frame.contentWindow.fetch_in_worker(resource);
+ assert_equals(response_text, 'a simple text file\n',
+ 'fetch() should not be intercepted.');
+ }
+
+ // Register a service worker.
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(_ => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Let the service worker claim the iframe and the worker.
+ const channel = new MessageChannel();
+ const saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = t.step_func(function(e) {
+ assert_equals(e.data, 'PASS', 'Worker call to claim() should fulfill.');
+ resolve();
+ });
+ });
+ reg.active.postMessage({port: channel.port2}, [channel.port2]);
+ await saw_message;
+
+ // Check the controller and test with fetch in the worker.
+ const reg2 =
+ await frame.contentWindow.navigator.serviceWorker.getRegistration(scope);
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller,
+ reg2.active, 'Test iframe should be claimed.');
+
+ {
+ // TODO(horo): Check the worker's navigator.seviceWorker.controller.
+ const response_text = await frame.contentWindow.fetch_in_worker(resource);
+ assert_equals(response_text, 'Intercepted!',
+ 'fetch() in the worker should be intercepted.');
+ }
+}
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/client-id.https.html b/third_party/web_platform_tests/service-workers/service-worker/client-id.https.html
new file mode 100644
index 0000000..b93b341
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/client-id.https.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<title>Service Worker: Client.id</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var scope = 'resources/blank.html?client-id';
+var frame1, frame2;
+
+promise_test(function(t) {
+ return service_worker_unregister_and_register(
+ t, 'resources/client-id-worker.js', scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope + '#1'); })
+ .then(function(f) {
+ frame1 = f;
+ // To be sure Clients.matchAll() iterates in the same order.
+ f.focus();
+ return with_iframe(scope + '#2');
+ })
+ .then(function(f) {
+ frame2 = f;
+ var channel = new MessageChannel();
+
+ return new Promise(function(resolve, reject) {
+ channel.port1.onmessage = resolve;
+ channel.port1.onmessageerror = reject;
+ f.contentWindow.navigator.serviceWorker.controller.postMessage(
+ {port:channel.port2}, [channel.port2]);
+ });
+ })
+ .then(on_message);
+ }, 'Client.id returns the client\'s ID.');
+
+function on_message(e) {
+ // The result of two sequential clients.matchAll() calls in the SW.
+ // 1st matchAll() results in e.data[0], e.data[1].
+ // 2nd matchAll() results in e.data[2], e.data[3].
+ assert_equals(e.data.length, 4);
+ // All should be string values.
+ assert_equals(typeof e.data[0], 'string');
+ assert_equals(typeof e.data[1], 'string');
+ assert_equals(typeof e.data[2], 'string');
+ assert_equals(typeof e.data[3], 'string');
+ // Different clients should have different ids.
+ assert_not_equals(e.data[0], e.data[1]);
+ assert_not_equals(e.data[2], e.data[3]);
+ // Same clients should have an identical id.
+ assert_equals(e.data[0], e.data[2]);
+ assert_equals(e.data[1], e.data[3]);
+ frame1.remove();
+ frame2.remove();
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/client-navigate.https.html b/third_party/web_platform_tests/service-workers/service-worker/client-navigate.https.html
new file mode 100644
index 0000000..f40a086
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/client-navigate.https.html
@@ -0,0 +1,107 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>Service Worker: WindowClient.navigate</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+ function wait_for_message(msg) {
+ return new Promise(function(resolve, reject) {
+ var get_message_data = function get_message_data(e) {
+ window.removeEventListener("message", get_message_data);
+ resolve(e.data);
+ }
+ window.addEventListener("message", get_message_data, false);
+ });
+ }
+
+ function run_test(controller, clientId, test) {
+ return new Promise(function(resolve, reject) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = function(e) {
+ resolve(e.data);
+ };
+ var message = {
+ port: channel.port2,
+ test: test,
+ clientId: clientId,
+ };
+ controller.postMessage(
+ message, [channel.port2]);
+ });
+ }
+
+ async function with_controlled_iframe_and_url(t, name, f) {
+ const SCRIPT = "resources/client-navigate-worker.js";
+ const SCOPE = "resources/client-navigate-frame.html";
+
+ // Register service worker and wait for it to become activated
+ const registration = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+
+ // Create child iframe and make sure we register a listener for the message
+ // it sends before it's created
+ const client_id_promise = wait_for_message();
+ const iframe = await with_iframe(SCOPE);
+ t.add_cleanup(() => iframe.remove());
+ const { id } = await client_id_promise;
+
+ // Run the test in the service worker and fetch it
+ const { result, url } = await run_test(worker, id, name);
+ fetch_tests_from_worker(worker);
+ assert_equals(result, name);
+
+ // Hand over the iframe and URL from the service worker to the callback
+ await f(iframe, url);
+ }
+
+ promise_test(function(t) {
+ return with_controlled_iframe_and_url(t, 'test_client_navigate_success', async (iframe, url) => {
+ assert_equals(
+ url, new URL("resources/client-navigated-frame.html",
+ location).toString());
+ assert_equals(
+ iframe.contentWindow.location.href,
+ new URL("resources/client-navigated-frame.html",
+ location).toString());
+ });
+ }, "Frame location should update on successful navigation");
+
+ promise_test(function(t) {
+ return with_controlled_iframe_and_url(t, 'test_client_navigate_redirect', async (iframe, url) => {
+ assert_equals(url, "");
+ assert_throws_dom("SecurityError", function() { return iframe.contentWindow.location.href });
+ });
+ }, "Frame location should not be accessible after redirect");
+
+ promise_test(function(t) {
+ return with_controlled_iframe_and_url(t, 'test_client_navigate_cross_origin', async (iframe, url) => {
+ assert_equals(url, "");
+ assert_throws_dom("SecurityError", function() { return iframe.contentWindow.location.href });
+ });
+ }, "Frame location should not be accessible after cross-origin navigation");
+
+ promise_test(function(t) {
+ return with_controlled_iframe_and_url(t, 'test_client_navigate_about_blank', async (iframe, url) => {
+ assert_equals(
+ iframe.contentWindow.location.href,
+ new URL("resources/client-navigate-frame.html",
+ location).toString());
+ iframe.contentWindow.document.body.style = "background-color: green"
+ });
+ }, "Frame location should not update on failed about:blank navigation");
+
+ promise_test(function(t) {
+ return with_controlled_iframe_and_url(t, 'test_client_navigate_mixed_content', async (iframe, url) => {
+ assert_equals(
+ iframe.contentWindow.location.href,
+ new URL("resources/client-navigate-frame.html",
+ location).toString());
+ iframe.contentWindow.document.body.style = "background-color: green"
+ });
+ }, "Frame location should not update on failed mixed-content navigation");
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/client-url-of-blob-url-worker.https.html b/third_party/web_platform_tests/service-workers/service-worker/client-url-of-blob-url-worker.https.html
new file mode 100644
index 0000000..97a2fcf
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/client-url-of-blob-url-worker.https.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<title>Service Worker: client.url of a worker created from a blob URL</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+const SCRIPT = 'resources/client-url-of-blob-url-worker.js';
+const SCOPE = 'resources/client-url-of-blob-url-worker.html';
+
+promise_test(async (t) => {
+ const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ t.add_cleanup(_ => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ const frame = await with_iframe(SCOPE);
+ t.add_cleanup(_ => frame.remove());
+ assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller,
+ null, 'frame should be controlled');
+
+ const response = await frame.contentWindow.createAndFetchFromBlobWorker();
+
+ assert_not_equals(response.result, 'one worker client should exist',
+ 'worker client should exist');
+ assert_equals(response.result, response.expected,
+ 'client.url and worker location href should be the same');
+
+}, 'Client.url of a blob URL worker should be a blob URL.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-get-client-types.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-get-client-types.https.html
new file mode 100644
index 0000000..63e3e51
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-get-client-types.https.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.get with window and worker clients</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var scope = 'resources/clients-get-client-types';
+var frame_url = scope + '-frame.html';
+var shared_worker_url = scope + '-shared-worker.js';
+var worker_url = scope + '-worker.js';
+var client_ids = [];
+var registration;
+var frame;
+promise_test(function(t) {
+ return service_worker_unregister_and_register(
+ t, 'resources/clients-get-worker.js', scope)
+ .then(function(r) {
+ registration = r;
+ add_completion_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(frame_url);
+ })
+ .then(function(f) {
+ frame = f;
+ add_completion_callback(function() { frame.remove(); });
+ frame.focus();
+ return wait_for_clientId();
+ })
+ .then(function(client_id) {
+ client_ids.push(client_id);
+ return new Promise(function(resolve) {
+ var w = new SharedWorker(shared_worker_url);
+ w.port.onmessage = function(e) {
+ resolve(e.data.clientId);
+ };
+ });
+ })
+ .then(function(client_id) {
+ client_ids.push(client_id);
+ var channel = new MessageChannel();
+ var w = new Worker(worker_url);
+ w.postMessage({cmd:'GetClientId', port:channel.port2},
+ [channel.port2]);
+ return new Promise(function(resolve) {
+ channel.port1.onmessage = function(e) {
+ resolve(e.data.clientId);
+ };
+ });
+ })
+ .then(function(client_id) {
+ client_ids.push(client_id);
+ var channel = new MessageChannel();
+ frame.contentWindow.postMessage('StartWorker', '*', [channel.port2]);
+ return new Promise(function(resolve) {
+ channel.port1.onmessage = function(e) {
+ resolve(e.data.clientId);
+ };
+ });
+ })
+ .then(function(client_id) {
+ client_ids.push(client_id);
+ var saw_message = new Promise(function(resolve) {
+ navigator.serviceWorker.onmessage = resolve;
+ });
+ registration.active.postMessage({clientIds: client_ids});
+ return saw_message;
+ })
+ .then(function(e) {
+ assert_equals(e.data.length, expected.length);
+ // We use these assert_not_equals because assert_array_equals doesn't
+ // print the error description when passed an undefined value.
+ assert_not_equals(e.data[0], undefined,
+ 'Window client should not be undefined');
+ assert_array_equals(e.data[0], expected[0], 'Window client');
+ assert_not_equals(e.data[1], undefined,
+ 'Shared worker client should not be undefined');
+ assert_array_equals(e.data[1], expected[1], 'Shared worker client');
+ assert_not_equals(e.data[2], undefined,
+ 'Worker(Started by main frame) client should not be undefined');
+ assert_array_equals(e.data[2], expected[2],
+ 'Worker(Started by main frame) client');
+ assert_not_equals(e.data[3], undefined,
+ 'Worker(Started by sub frame) client should not be undefined');
+ assert_array_equals(e.data[3], expected[3],
+ 'Worker(Started by sub frame) client');
+ });
+ }, 'Test Clients.get() with window and worker clients');
+
+function wait_for_clientId() {
+ return new Promise(function(resolve) {
+ function get_client_id(e) {
+ window.removeEventListener('message', get_client_id);
+ resolve(e.data.clientId);
+ }
+ window.addEventListener('message', get_client_id, false);
+ });
+}
+
+var expected = [
+ // visibilityState, focused, url, type, frameType
+ ['visible', true, normalizeURL(scope) + '-frame.html', 'window', 'nested'],
+ [undefined, undefined, normalizeURL(scope) + '-shared-worker.js', 'sharedworker', 'none'],
+ [undefined, undefined, normalizeURL(scope) + '-worker.js', 'worker', 'none'],
+ [undefined, undefined, normalizeURL(scope) + '-frame-worker.js', 'worker', 'none']
+];
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-get-cross-origin.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-get-cross-origin.https.html
new file mode 100644
index 0000000..1e4acfb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-get-cross-origin.https.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.get across origins</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var host_info = get_host_info();
+
+var scope = 'resources/clients-get-frame.html';
+var other_origin_iframe = host_info['HTTPS_REMOTE_ORIGIN'] + base_path() +
+ 'resources/clients-get-cross-origin-frame.html';
+// The ID of a client from the same origin as us.
+var my_origin_client_id;
+// This test asserts the behavior of the Client API in cases where the client
+// belongs to a foreign origin. It does this by creating an iframe with a
+// foreign origin which connects to a server worker in the current origin.
+promise_test(function(t) {
+ return service_worker_unregister_and_register(
+ t, 'resources/clients-get-worker.js', scope)
+ .then(function(registration) {
+ add_completion_callback(function() { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ // Create a same-origin client and use it to populate |my_origin_client_id|.
+ .then(function(frame1) {
+ add_completion_callback(function() { frame1.remove(); });
+ return new Promise(function(resolve, reject) {
+ function get_client_id(e) {
+ window.removeEventListener('message', get_client_id);
+ resolve(e.data.clientId);
+ }
+ window.addEventListener('message', get_client_id, false);
+ });
+ })
+ // Create a cross-origin client. We'll communicate with this client to
+ // test the cross-origin service worker's behavior.
+ .then(function(client_id) {
+ my_origin_client_id = client_id;
+ return with_iframe(other_origin_iframe);
+ })
+ // Post the 'getClientId' message to the cross-origin client. The client
+ // will then ask its service worker to look up |my_origin_client_id| via
+ // Clients#get. Since this client ID is for a different origin, we expect
+ // the client to not be found.
+ .then(function(frame2) {
+ add_completion_callback(function() { frame2.remove(); });
+
+ frame2.contentWindow.postMessage(
+ {clientId: my_origin_client_id, type: 'getClientId'},
+ host_info['HTTPS_REMOTE_ORIGIN']
+ );
+
+ return new Promise(function(resolve) {
+ window.addEventListener('message', function(e) {
+ if (e.data && e.data.type === 'clientId') {
+ resolve(e.data.value);
+ }
+ });
+ });
+ })
+ .then(function(client_id) {
+ assert_equals(client_id, undefined, 'iframe client ID');
+ });
+ }, 'Test Clients.get() cross origin');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-get-resultingClientId.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-get-resultingClientId.https.html
new file mode 100644
index 0000000..3419cf1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-get-resultingClientId.https.html
@@ -0,0 +1,177 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test clients.get(resultingClientId)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const scope = "resources/";
+let worker;
+
+// Setup. Keep this as the first promise_test.
+promise_test(async (t) => {
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/get-resultingClientId-worker.js',
+ scope);
+ worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+}, 'global setup');
+
+// Sends |command| to the worker and returns a promise that resolves to its
+// response. There should only be one inflight command at a time.
+async function sendCommand(command) {
+ const saw_message = new Promise((resolve) => {
+ navigator.serviceWorker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ });
+ worker.postMessage(command);
+ return saw_message;
+}
+
+// Wrapper for 'startTest' command. Tells the worker a test is starting,
+// so it resets state and keeps itself alive until 'finishTest'.
+async function startTest(t) {
+ const result = await sendCommand({command: 'startTest'});
+ assert_equals(result, 'ok', 'startTest');
+
+ t.add_cleanup(async () => {
+ return finishTest();
+ });
+}
+
+// Wrapper for 'finishTest' command.
+async function finishTest() {
+ const result = await sendCommand({command: 'finishTest'});
+ assert_equals(result, 'ok', 'finishTest');
+}
+
+// Wrapper for 'getResultingClient' command. Tells the worker to return
+// clients.get(event.resultingClientId) for the navigation that occurs
+// during this test.
+//
+// The return value describes how clients.get() settled. It also includes
+// |queriedId| which is the id passed to clients.get() (the resultingClientId
+// in this case).
+//
+// Example value:
+// {
+// queriedId: 'abc',
+// promiseState: fulfilled,
+// promiseValue: client,
+// client: {
+// id: 'abc',
+// url: '//example.com/client'
+// }
+// }
+async function getResultingClient() {
+ return sendCommand({command: 'getResultingClient'});
+}
+
+// Wrapper for 'getClient' command. Tells the worker to return
+// clients.get(|id|). The return value is as in the getResultingClient()
+// documentation.
+async function getClient(id) {
+ return sendCommand({command: 'getClient', id: id});
+}
+
+// Navigates to |url|. Returns the result of clients.get() on the
+// resultingClientId.
+async function navigateAndGetResultingClient(t, url) {
+ const resultPromise = getResultingClient();
+ const frame = await with_iframe(url);
+ t.add_cleanup(() => {
+ frame.remove();
+ });
+ const result = await resultPromise;
+ const resultingClientId = result.queriedId;
+
+ // First test clients.get(event.resultingClientId) inside the fetch event. The
+ // behavior of this is subtle due to the use of iframes and about:blank
+ // replacement. The spec probably requires that it resolve to the original
+ // about:blank client, and that later that client should be discarded after
+ // load if the load was to another origin. Implementations might differ. For
+ // now, this test just asserts that the promise resolves. See
+ // https://github.com/w3c/ServiceWorker/issues/1385.
+ assert_equals(result.promiseState, 'fulfilled',
+ 'get(event.resultingClientId) in the fetch event should fulfill');
+
+ // Test clients.get() on the previous resultingClientId again. By this
+ // time the load finished, so it's more straightforward how this promise
+ // should settle. Return the result of this promise.
+ return await getClient(resultingClientId);
+}
+
+// Test get(resultingClientId) in the basic same-origin case.
+promise_test(async (t) => {
+ await startTest(t);
+
+ const url = new URL('resources/empty.html', window.location);
+ const result = await navigateAndGetResultingClient(t, url);
+ assert_equals(result.promiseState, 'fulfilled', 'promiseState');
+ assert_equals(result.promiseValue, 'client', 'promiseValue');
+ assert_equals(result.client.url, url.href, 'client.url',);
+ assert_equals(result.client.id, result.queriedId, 'client.id');
+}, 'get(resultingClientId) for same-origin document');
+
+// Test get(resultingClientId) when the response redirects to another origin.
+promise_test(async (t) => {
+ await startTest(t);
+
+ // Navigate to a URL that redirects to another origin.
+ const base_url = new URL('.', window.location);
+ const host_info = get_host_info();
+ const other_origin_url = new URL(base_url.pathname + 'resources/empty.html',
+ host_info['HTTPS_REMOTE_ORIGIN']);
+ const url = new URL('resources/empty.html', window.location);
+ const pipe = `status(302)|header(Location, ${other_origin_url})`;
+ url.searchParams.set('pipe', pipe);
+
+ // The original reserved client should have been discarded on cross-origin
+ // redirect.
+ const result = await navigateAndGetResultingClient(t, url);
+ assert_equals(result.promiseState, 'fulfilled', 'promiseState');
+ assert_equals(result.promiseValue, 'undefinedValue', 'promiseValue');
+}, 'get(resultingClientId) on cross-origin redirect');
+
+// Test get(resultingClientId) when the document is sandboxed to a unique
+// origin using a CSP HTTP response header.
+promise_test(async (t) => {
+ await startTest(t);
+
+ // Navigate to a URL that has CSP sandboxing set in the HTTP response header.
+ const url = new URL('resources/empty.html', window.location);
+ const pipe = 'header(Content-Security-Policy, sandbox)';
+ url.searchParams.set('pipe', pipe);
+
+ // The original reserved client should have been discarded upon loading
+ // the sandboxed document.
+ const result = await navigateAndGetResultingClient(t, url);
+ assert_equals(result.promiseState, 'fulfilled', 'promiseState');
+ assert_equals(result.promiseValue, 'undefinedValue', 'promiseValue');
+}, 'get(resultingClientId) for document sandboxed by CSP header');
+
+// Test get(resultingClientId) when the document is sandboxed with
+// allow-same-origin.
+promise_test(async (t) => {
+ await startTest(t);
+
+ // Navigate to a URL that has CSP sandboxing set in the HTTP response header.
+ const url = new URL('resources/empty.html', window.location);
+ const pipe = 'header(Content-Security-Policy, sandbox allow-same-origin)';
+ url.searchParams.set('pipe', pipe);
+
+ // The client should be the original reserved client, as it's same-origin.
+ const result = await navigateAndGetResultingClient(t, url);
+ assert_equals(result.promiseState, 'fulfilled', 'promiseState');
+ assert_equals(result.promiseValue, 'client', 'promiseValue');
+ assert_equals(result.client.url, url.href, 'client.url',);
+ assert_equals(result.client.id, result.queriedId, 'client.id');
+}, 'get(resultingClientId) for document sandboxed by CSP header with allow-same-origin');
+
+// Cleanup. Keep this as the last promise_test.
+promise_test(async (t) => {
+ return service_worker_unregister(t, scope);
+}, 'global cleanup');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-get.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-get.https.html
new file mode 100644
index 0000000..4cfbf59
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-get.https.html
@@ -0,0 +1,154 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.get</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function wait_for_clientId() {
+ return new Promise(function(resolve, reject) {
+ window.onmessage = e => {
+ resolve(e.data.clientId);
+ };
+ });
+}
+
+promise_test(async t => {
+ // Register service worker.
+ const scope = 'resources/clients-get-frame.html';
+ const client_ids = [];
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/clients-get-worker.js', scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Prepare for test cases.
+ // Case 1: frame1 which is focused.
+ const frame1 = await with_iframe(scope + '#1');
+ t.add_cleanup(() => frame1.remove());
+ frame1.focus();
+ client_ids.push(await wait_for_clientId());
+ // Case 2: frame2 which is not focused.
+ const frame2 = await with_iframe(scope + '#2');
+ t.add_cleanup(() => frame2.remove());
+ client_ids.push(await wait_for_clientId());
+ // Case 3: invalid id.
+ client_ids.push('invalid-id');
+
+ // Call clients.get() for each id on the service worker.
+ const message_event = await new Promise(resolve => {
+ navigator.serviceWorker.onmessage = resolve;
+ registration.active.postMessage({clientIds: client_ids});
+ });
+
+ const expected = [
+ // visibilityState, focused, url, type, frameType
+ ['visible', true, normalizeURL(scope) + '#1', 'window', 'nested'],
+ ['visible', false, normalizeURL(scope) + '#2', 'window', 'nested'],
+ undefined
+ ];
+ assert_equals(message_event.data.length, 3);
+ assert_array_equals(message_event.data[0], expected[0]);
+ assert_array_equals(message_event.data[1], expected[1]);
+ assert_equals(message_event.data[2], expected[2]);
+}, 'Test Clients.get()');
+
+promise_test(async t => {
+ // Register service worker.
+ const scope = 'resources/simple.html';
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/clients-get-resultingClientId-worker.js', scope)
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const worker = registration.active;
+
+ // Load frame within the scope.
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+ frame.focus();
+
+ // Get resulting client id.
+ const resultingClientId = await new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => {
+ if (e.data.msg == 'getResultingClientId') {
+ resolve(e.data.resultingClientId);
+ }
+ };
+ worker.postMessage({msg: 'getResultingClientId'});
+ });
+
+ // Query service worker for clients.get(resultingClientId).
+ const isResultingClientUndefined = await new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => {
+ if (e.data.msg == 'getIsResultingClientUndefined') {
+ resolve(e.data.isResultingClientUndefined);
+ }
+ };
+ worker.postMessage({msg: 'getIsResultingClientUndefined',
+ resultingClientId});
+ });
+
+ assert_false(
+ isResultingClientUndefined,
+ 'Clients.get(FetchEvent.resultingClientId) resolved with a Client');
+}, 'Test successful Clients.get(FetchEvent.resultingClientId)');
+
+promise_test(async t => {
+ // Register service worker.
+ const scope = 'resources/simple.html?fail';
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/clients-get-resultingClientId-worker.js', scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Load frame, and destroy it while loading.
+ const worker = registration.active;
+ let frame = document.createElement('iframe');
+ frame.src = scope;
+ t.add_cleanup(() => {
+ if (frame) {
+ frame.remove();
+ }
+ });
+
+ await new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => {
+ // The service worker posts a message to remove the iframe during fetch
+ // event.
+ if (e.data.msg == 'destroyResultingClient') {
+ frame.remove();
+ frame = null;
+ worker.postMessage({msg: 'resultingClientDestroyed'});
+ resolve();
+ }
+ };
+ document.body.appendChild(frame);
+ });
+
+ resultingDestroyedClientId = await new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => {
+ // The worker sends a message back when it receives the message
+ // 'resultingClientDestroyed' with the resultingClientId.
+ if (e.data.msg == 'resultingClientDestroyedAck') {
+ assert_equals(frame, null, 'Frame should be destroyed at this point.');
+ resolve(e.data.resultingDestroyedClientId);
+ }
+ };
+ });
+
+ // Query service worker for clients.get(resultingDestroyedClientId).
+ const isResultingClientUndefined = await new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => {
+ if (e.data.msg == 'getIsResultingClientUndefined') {
+ resolve(e.data.isResultingClientUndefined);
+ }
+ };
+ worker.postMessage({msg: 'getIsResultingClientUndefined',
+ resultingClientId: resultingDestroyedClientId });
+ });
+
+ assert_true(
+ isResultingClientUndefined,
+ 'Clients.get(FetchEvent.resultingClientId) resolved with `undefined`');
+}, 'Test unsuccessful Clients.get(FetchEvent.resultingClientId)');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-blob-url-worker.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-blob-url-worker.https.html
new file mode 100644
index 0000000..c29bac8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-blob-url-worker.https.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll with a blob URL worker client</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const SCRIPT = 'resources/clients-matchall-worker.js';
+
+promise_test(async (t) => {
+ const scope = 'resources/clients-matchall-blob-url-worker.html';
+
+ const reg = await service_worker_unregister_and_register(t, SCRIPT, scope);
+ t.add_cleanup(_ => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ const frame = await with_iframe(scope);
+ t.add_cleanup(_ => frame.remove());
+
+ {
+ const message = await frame.contentWindow.waitForWorker();
+ assert_equals(message.data, 'Worker is ready.',
+ 'Worker should reply to the message.');
+ }
+
+ const channel = new MessageChannel();
+ const message = await new Promise(resolve => {
+ channel.port1.onmessage = resolve;
+ frame.contentWindow.navigator.serviceWorker.controller.postMessage(
+ {port: channel.port2, options: {type: 'worker'}}, [channel.port2]);
+ });
+
+ checkMessageEvent(message);
+
+}, 'Test Clients.matchAll() with a blob URL worker client.');
+
+promise_test(async (t) => {
+ const scope = 'resources/blank.html';
+
+ const reg = await service_worker_unregister_and_register(t, SCRIPT, scope);
+ t.add_cleanup(_ => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ const workerScript = `
+ self.onmessage = (e) => {
+ self.postMessage("Worker is ready.");
+ };
+ `;
+ const blob = new Blob([workerScript], { type: 'text/javascript' });
+ const blobUrl = URL.createObjectURL(blob);
+ const worker = new Worker(blobUrl);
+
+ {
+ const message = await new Promise(resolve => {
+ worker.onmessage = resolve;
+ worker.postMessage("Ping to worker.");
+ });
+ assert_equals(message.data, 'Worker is ready.',
+ 'Worker should reply to the message.');
+ }
+
+ const channel = new MessageChannel();
+ const message = await new Promise(resolve => {
+ channel.port1.onmessage = resolve;
+ reg.active.postMessage(
+ {port: channel.port2,
+ options: {includeUncontrolled: true, type: 'worker'}},
+ [channel.port2]
+ );
+ });
+
+ checkMessageEvent(message);
+
+}, 'Test Clients.matchAll() with an uncontrolled blob URL worker client.');
+
+function checkMessageEvent(e) {
+ assert_equals(e.data.length, 1);
+
+ const workerClient = e.data[0];
+ assert_equals(workerClient[0], undefined); // visibilityState
+ assert_equals(workerClient[1], undefined); // focused
+ assert_true(workerClient[2].includes('blob:')); // url
+ assert_equals(workerClient[3], 'worker'); // type
+ assert_equals(workerClient[4], 'none'); // frameType
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-client-types.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-client-types.https.html
new file mode 100644
index 0000000..54f182b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-client-types.https.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll with various clientTypes</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const scope = 'resources/clients-matchall-client-types';
+const iframe_url = scope + '-iframe.html';
+const shared_worker_url = scope + '-shared-worker.js';
+const dedicated_worker_url = scope + '-dedicated-worker.js';
+
+/* visibilityState, focused, url, type, frameType */
+const expected_only_window = [
+ ['visible', true, new URL(iframe_url, location).href, 'window', 'nested']
+];
+const expected_only_shared_worker = [
+ [undefined, undefined, new URL(shared_worker_url, location).href, 'sharedworker', 'none']
+];
+const expected_only_dedicated_worker = [
+ [undefined, undefined, new URL(dedicated_worker_url, location).href, 'worker', 'none']
+];
+
+// These are explicitly sorted by URL in the service worker script.
+const expected_all_clients = [
+ expected_only_dedicated_worker[0],
+ expected_only_window[0],
+ expected_only_shared_worker[0],
+];
+
+async function test_matchall(frame, expected, query_options) {
+ // Make sure the frame gets focus.
+ frame.focus();
+ const data = await new Promise(resolve => {
+ const channel = new MessageChannel();
+ channel.port1.onmessage = e => resolve(e.data);
+ frame.contentWindow.navigator.serviceWorker.controller.postMessage(
+ {port:channel.port2, options:query_options},
+ [channel.port2]);
+ });
+
+ if (typeof data === 'string') {
+ throw new Error(data);
+ }
+
+ assert_equals(data.length, expected.length, 'result count');
+
+ for (let i = 0; i < data.length; ++i) {
+ assert_array_equals(data[i], expected[i]);
+ }
+}
+
+promise_test(async t => {
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/clients-matchall-worker.js', scope);
+ t.add_cleanup(_ => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(iframe_url);
+ t.add_cleanup(_ => frame.remove());
+ await test_matchall(frame, expected_only_window, {});
+ await test_matchall(frame, expected_only_window, {type:'window'});
+}, 'Verify matchAll() with window client type');
+
+promise_test(async t => {
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/clients-matchall-worker.js', scope);
+ t.add_cleanup(_ => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(iframe_url);
+ t.add_cleanup(_ => frame.remove());
+
+ // Set up worker clients.
+ const shared_worker = await new Promise((resolve, reject) => {
+ const w = new SharedWorker(shared_worker_url);
+ w.onerror = e => reject(e.message);
+ w.port.onmessage = _ => resolve(w);
+ });
+ const dedicated_worker = await new Promise((resolve, reject) => {
+ const w = new Worker(dedicated_worker_url);
+ w.onerror = e => reject(e.message);
+ w.onmessage = _ => resolve(w);
+ w.postMessage('Start');
+ });
+
+ await test_matchall(frame, expected_only_window, {});
+ await test_matchall(frame, expected_only_window, {type:'window'});
+ await test_matchall(frame, expected_only_shared_worker,
+ {type:'sharedworker'});
+ await test_matchall(frame, expected_only_dedicated_worker, {type:'worker'});
+ await test_matchall(frame, expected_all_clients, {type:'all'});
+}, 'Verify matchAll() with {window, sharedworker, worker} client types');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-exact-controller.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-exact-controller.https.html
new file mode 100644
index 0000000..a61c8af
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-exact-controller.https.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll with exact controller</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const scope = 'resources/blank.html?clients-matchAll';
+let frames = [];
+
+function checkWorkerClients(worker, expected) {
+ return new Promise((resolve, reject) => {
+ let channel = new MessageChannel();
+ channel.port1.onmessage = evt => {
+ try {
+ assert_equals(evt.data.length, expected.length);
+ for (let i = 0; i < expected.length; ++i) {
+ assert_array_equals(evt.data[i], expected[i]);
+ }
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ };
+
+ worker.postMessage({port:channel.port2}, [channel.port2]);
+ });
+}
+
+let expected = [
+ // visibilityState, focused, url, type, frameType
+ ['visible', true, new URL(scope + '#1', location).toString(), 'window', 'nested'],
+ ['visible', false, new URL(scope + '#2', location).toString(), 'window', 'nested']
+];
+
+promise_test(t => {
+ let script = 'resources/clients-matchall-worker.js';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(_ => with_iframe(scope + '#1') )
+ .then(frame1 => {
+ frames.push(frame1);
+ frame1.focus();
+ return with_iframe(scope + '#2');
+ })
+ .then(frame2 => {
+ frames.push(frame2);
+ return navigator.serviceWorker.register(script + '?updated', { scope: scope });
+ })
+ .then(registration => {
+ return wait_for_state(t, registration.installing, 'installed')
+ .then(_ => registration);
+ })
+ .then(registration => {
+ return Promise.all([
+ checkWorkerClients(registration.waiting, []),
+ checkWorkerClients(registration.active, expected),
+ ]);
+ })
+ .then(_ => {
+ frames.forEach(f => f.remove() );
+ });
+}, 'Test Clients.matchAll() with exact controller');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-frozen.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-frozen.https.html
new file mode 100644
index 0000000..479c28a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-frozen.https.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var scope = 'resources/clients-frame-freeze.html';
+var windows = [];
+var expected_window_1 =
+ {visibilityState: 'visible', focused: false, lifecycleState: "frozen", url: new URL(scope + '#1', location).toString(), type: 'window', frameType: 'top-level'};
+var expected_window_2 =
+ {visibilityState: 'visible', focused: false, lifecycleState: "active", url: new URL(scope + '#2', location).toString(), type: 'window', frameType: 'top-level'};
+function with_window(url, name) {
+ return new Promise(function(resolve) {
+ var child = window.open(url, name);
+ window.onmessage = () => {resolve(child)};
+ });
+}
+
+promise_test(function(t) {
+ return service_worker_unregister_and_register(
+ t, 'resources/clients-matchall-worker.js', scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_window(scope + '#1', 'Child 1'); })
+ .then(function(window1) {
+ windows.push(window1);
+ return with_window(scope + '#2', 'Child 2');
+ })
+ .then(function(window2) {
+ windows.push(window2);
+ return new Promise(function(resolve) {
+ window.onmessage = resolve;
+ windows[0].postMessage('freeze');
+ });
+ })
+ .then(function() {
+ var channel = new MessageChannel();
+
+ return new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ windows[1].navigator.serviceWorker.controller.postMessage(
+ {port:channel.port2, includeLifecycleState: true}, [channel.port2]);
+ });
+ })
+ .then(function(e) {
+ assert_equals(e.data.length, 2);
+ // No specific order is required, so support inversion.
+ if (e.data[0][3] == new URL(scope + '#2', location)) {
+ assert_object_equals(e.data[0], expected_window_2);
+ assert_object_equals(e.data[1], expected_window_1);
+ } else {
+ assert_object_equals(e.data[0], expected_window_1);
+ assert_object_equals(e.data[1], expected_window_2);
+ }
+ });
+}, 'Test Clients.matchAll()');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-include-uncontrolled.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-include-uncontrolled.https.html
new file mode 100644
index 0000000..9f34e57
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-include-uncontrolled.https.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll with includeUncontrolled</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function test_matchall(service_worker, expected, query_options) {
+ expected.sort((a, b) => a[2] > b[2] ? 1 : -1);
+ return new Promise((resolve, reject) => {
+ const channel = new MessageChannel();
+ channel.port1.onmessage = e => {
+ const data = e.data.filter(info => {
+ return info[2].indexOf('clients-matchall') > -1;
+ });
+ data.sort((a, b) => a[2] > b[2] ? 1 : -1);
+ assert_equals(data.length, expected.length);
+ for (let i = 0; i < data.length; i++)
+ assert_array_equals(data[i], expected[i]);
+ resolve();
+ };
+ service_worker.postMessage({port:channel.port2, options:query_options},
+ [channel.port2]);
+ });
+}
+
+// Run clients.matchAll without and with includeUncontrolled=true.
+// (We want to run the two tests sequentially in the same promise_test
+// so that we can use the same set of iframes without intefering each other.
+promise_test(async t => {
+ // |base_url| is out-of-scope.
+ const base_url = 'resources/blank.html?clients-matchall';
+ const scope = base_url + '-includeUncontrolled';
+
+ const registration =
+ await service_worker_unregister_and_register(
+ t, 'resources/clients-matchall-worker.js', scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+ const service_worker = registration.installing;
+ await wait_for_state(t, service_worker, 'activated');
+
+ // Creates 3 iframes, 2 for in-scope and 1 for out-of-scope.
+ let frames = [];
+ frames.push(await with_iframe(base_url));
+ frames.push(await with_iframe(scope + '#1'));
+ frames.push(await with_iframe(scope + '#2'));
+
+ // Make sure we have focus for '#2' frame and its parent window.
+ frames[2].focus();
+ frames[2].contentWindow.focus();
+
+ const expected_without_include_uncontrolled = [
+ // visibilityState, focused, url, type, frameType
+ ['visible', false, new URL(scope + '#1', location).toString(), 'window', 'nested'],
+ ['visible', true, new URL(scope + '#2', location).toString(), 'window', 'nested']
+ ];
+ const expected_with_include_uncontrolled = [
+ // visibilityState, focused, url, type, frameType
+ ['visible', true, location.href, 'window', 'top-level'],
+ ['visible', false, new URL(scope + '#1', location).toString(), 'window', 'nested'],
+ ['visible', true, new URL(scope + '#2', location).toString(), 'window', 'nested'],
+ ['visible', false, new URL(base_url, location).toString(), 'window', 'nested']
+ ];
+
+ await test_matchall(service_worker, expected_without_include_uncontrolled);
+ await test_matchall(service_worker, expected_with_include_uncontrolled,
+ { includeUncontrolled: true });
+}, 'Verify matchAll() with windows respect includeUncontrolled');
+
+// TODO: Add tests for clients.matchAll for dedicated workers.
+
+async function create_shared_worker(script_url) {
+ const shared_worker = new SharedWorker(script_url);
+ const msgEvent = await new Promise(r => shared_worker.port.onmessage = r);
+ assert_equals(msgEvent.data, 'started');
+ return shared_worker;
+}
+
+// Run clients.matchAll for shared workers without and with
+// includeUncontrolled=true.
+promise_test(async t => {
+ const script_url = 'resources/clients-matchall-client-types-shared-worker.js';
+ const uncontrolled_script_url =
+ new URL(script_url + '?uncontrolled', location).toString();
+ const controlled_script_url =
+ new URL(script_url + '?controlled', location).toString();
+
+ // Start a shared worker that is not controlled by a service worker.
+ const uncontrolled_shared_worker =
+ await create_shared_worker(uncontrolled_script_url);
+
+ // Register a service worker.
+ const registration =
+ await service_worker_unregister_and_register(
+ t, 'resources/clients-matchall-worker.js', script_url);
+ t.add_cleanup(() => service_worker_unregister(t, script_url));
+ const service_worker = registration.installing;
+ await wait_for_state(t, service_worker, 'activated');
+
+ // Start another shared worker controlled by the service worker.
+ await create_shared_worker(controlled_script_url);
+
+ const expected_without_include_uncontrolled = [
+ // visibilityState, focused, url, type, frameType
+ [undefined, undefined, controlled_script_url, 'sharedworker', 'none'],
+ ];
+ const expected_with_include_uncontrolled = [
+ // visibilityState, focused, url, type, frameType
+ [undefined, undefined, controlled_script_url, 'sharedworker', 'none'],
+ [undefined, undefined, uncontrolled_script_url, 'sharedworker', 'none'],
+ ];
+
+ await test_matchall(service_worker, expected_without_include_uncontrolled,
+ { type: 'sharedworker' });
+ await test_matchall(service_worker, expected_with_include_uncontrolled,
+ { includeUncontrolled: true, type: 'sharedworker' });
+}, 'Verify matchAll() with shared workers respect includeUncontrolled');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-on-evaluation.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-on-evaluation.https.html
new file mode 100644
index 0000000..8705f85
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-on-evaluation.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll on script evaluation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var script = 'resources/clients-matchall-on-evaluation-worker.js';
+ var scope = 'resources/blank.html?clients-matchAll-on-evaluation';
+
+ var saw_message = new Promise(function(resolve) {
+ navigator.serviceWorker.onmessage = function(e) {
+ assert_equals(e.data, 'matched');
+ resolve();
+ };
+ });
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ add_completion_callback(function() { registration.unregister(); });
+ return saw_message;
+ });
+ }, 'Test Clients.matchAll() on script evaluation');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-order.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-order.https.html
new file mode 100644
index 0000000..ec650f2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall-order.https.html
@@ -0,0 +1,427 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll ordering</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+// Utility function for URLs this test will open.
+function makeURL(name, num, type) {
+ let u = new URL('resources/empty.html', location);
+ u.searchParams.set('name', name);
+ if (num !== undefined) {
+ u.searchParams.set('q', num);
+ }
+ if (type === 'nested') {
+ u.searchParams.set('nested', true);
+ }
+ return u.href;
+}
+
+// Non-test URLs that will be open during each test. The harness URLs
+// are from the WPT harness. The "extra" URL is a final window opened
+// by the test.
+const EXTRA_URL = makeURL('extra');
+const TEST_HARNESS_URL = location.href;
+const TOP_HARNESS_URL = new URL('/testharness_runner.html', location).href;
+
+// Utility function to open an iframe in the target parent window. We
+// can't just use with_iframe() because it does not support a configurable
+// parent window.
+function openFrame(parentWindow, url) {
+ return new Promise(resolve => {
+ let frame = parentWindow.document.createElement('iframe');
+ frame.src = url;
+ parentWindow.document.body.appendChild(frame);
+
+ frame.contentWindow.addEventListener('load', evt => {
+ resolve(frame);
+ }, { once: true });
+ });
+}
+
+// Utility function to open a window and wait for it to load. The
+// window may optionally have a nested iframe as well. Returns
+// a result like `{ top: <frame ref> nested: <nested frame ref if present> }`.
+function openFrameConfig(opts) {
+ let url = new URL(opts.url, location.href);
+ return openFrame(window, url.href).then(top => {
+ if (!opts.withNested) {
+ return { top: top };
+ }
+
+ url.searchParams.set('nested', true);
+ return openFrame(top.contentWindow, url.href).then(nested => {
+ return { top: top, nested: nested };
+ });
+ });
+}
+
+// Utility function that takes a list of configurations and opens the
+// corresponding windows in sequence. An array of results is returned.
+function openFrameConfigList(optList) {
+ let resultList = [];
+ function openNextWindow(optList, nextWindow) {
+ if (nextWindow >= optList.length) {
+ return resultList;
+ }
+ return openFrameConfig(optList[nextWindow]).then(result => {
+ resultList.push(result);
+ return openNextWindow(optList, nextWindow + 1);
+ });
+ }
+ return openNextWindow(optList, 0);
+}
+
+// Utility function that focuses the given entry in window result list.
+function executeFocus(frameResultList, opts) {
+ return new Promise(resolve => {
+ let w = frameResultList[opts.index][opts.type];
+ let target = w.contentWindow ? w.contentWindow : w;
+ target.addEventListener('focus', evt => {
+ resolve();
+ }, { once: true });
+ target.focus();
+ });
+}
+
+// Utility function that performs a list of focus commands in sequence
+// based on the window result list.
+function executeFocusList(frameResultList, optList) {
+ function executeNextCommand(frameResultList, optList, nextCommand) {
+ if (nextCommand >= optList.length) {
+ return;
+ }
+ return executeFocus(frameResultList, optList[nextCommand]).then(_ => {
+ return executeNextCommand(frameResultList, optList, nextCommand + 1);
+ });
+ }
+ return executeNextCommand(frameResultList, optList, 0);
+}
+
+// Perform a `clients.matchAll()` in the service worker with the given
+// options dictionary.
+function doMatchAll(worker, options) {
+ return new Promise(resolve => {
+ let channel = new MessageChannel();
+ channel.port1.onmessage = evt => {
+ resolve(evt.data);
+ };
+ worker.postMessage({ port: channel.port2, options: options, disableSort: true },
+ [channel.port2]);
+ });
+}
+
+// Function that performs a single test case. It takes a configuration object
+// describing the windows to open, how to focus them, the matchAll options,
+// and the resulting expectations. See the test cases for examples of how to
+// use this.
+function matchAllOrderTest(t, opts) {
+ let script = 'resources/clients-matchall-worker.js';
+ let worker;
+ let frameResultList;
+ let extraWindowResult;
+ return service_worker_unregister_and_register(t, script, opts.scope).then(swr => {
+ t.add_cleanup(() => service_worker_unregister(t, opts.scope));
+
+ worker = swr.installing;
+ return wait_for_state(t, worker, 'activated');
+ }).then(_ => {
+ return openFrameConfigList(opts.frameConfigList);
+ }).then(results => {
+ frameResultList = results;
+ return openFrameConfig({ url: EXTRA_URL });
+ }).then(result => {
+ extraWindowResult = result;
+ return executeFocusList(frameResultList, opts.focusConfigList);
+ }).then(_ => {
+ return doMatchAll(worker, opts.matchAllOptions);
+ }).then(data => {
+ assert_equals(data.length, opts.expected.length);
+ for (let i = 0; i < data.length; ++i) {
+ assert_equals(data[i][2], opts.expected[i], 'expected URL index ' + i);
+ }
+ }).then(_ => {
+ frameResultList.forEach(result => result.top.remove());
+ extraWindowResult.top.remove();
+ }).catch(e => {
+ if (frameResultList) {
+ frameResultList.forEach(result => result.top.remove());
+ }
+ if (extraWindowResult) {
+ extraWindowResult.top.remove();
+ }
+ throw(e);
+ });
+}
+
+// ----------
+// Test cases
+// ----------
+
+promise_test(t => {
+ let name = 'no-focus-controlled-windows';
+ let opts = {
+ scope: makeURL(name),
+
+ frameConfigList: [
+ { url: makeURL(name, 0), withNested: false },
+ { url: makeURL(name, 1), withNested: false },
+ { url: makeURL(name, 2), withNested: false },
+ ],
+
+ focusConfigList: [
+ // no focus commands
+ ],
+
+ matchAllOptions: {
+ includeUncontrolled: false
+ },
+
+ expected: [
+ makeURL(name, 0),
+ makeURL(name, 1),
+ makeURL(name, 2),
+ ],
+ };
+
+ return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns non-focused controlled windows in creation order.');
+
+promise_test(t => {
+ let name = 'focus-controlled-windows-1';
+ let opts = {
+ scope: makeURL(name),
+
+ frameConfigList: [
+ { url: makeURL(name, 0), withNested: false },
+ { url: makeURL(name, 1), withNested: false },
+ { url: makeURL(name, 2), withNested: false },
+ ],
+
+ focusConfigList: [
+ { index: 0, type: 'top' },
+ { index: 1, type: 'top' },
+ { index: 2, type: 'top' },
+ ],
+
+ matchAllOptions: {
+ includeUncontrolled: false
+ },
+
+ expected: [
+ makeURL(name, 2),
+ makeURL(name, 1),
+ makeURL(name, 0),
+ ],
+ };
+
+ return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns controlled windows in focus order. Case 1.');
+
+promise_test(t => {
+ let name = 'focus-controlled-windows-2';
+ let opts = {
+ scope: makeURL(name),
+
+ frameConfigList: [
+ { url: makeURL(name, 0), withNested: false },
+ { url: makeURL(name, 1), withNested: false },
+ { url: makeURL(name, 2), withNested: false },
+ ],
+
+ focusConfigList: [
+ { index: 2, type: 'top' },
+ { index: 1, type: 'top' },
+ { index: 0, type: 'top' },
+ ],
+
+ matchAllOptions: {
+ includeUncontrolled: false
+ },
+
+ expected: [
+ makeURL(name, 0),
+ makeURL(name, 1),
+ makeURL(name, 2),
+ ],
+ };
+
+ return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns controlled windows in focus order. Case 2.');
+
+promise_test(t => {
+ let name = 'no-focus-uncontrolled-windows';
+ let opts = {
+ scope: makeURL(name + '-outofscope'),
+
+ frameConfigList: [
+ { url: makeURL(name, 0), withNested: false },
+ { url: makeURL(name, 1), withNested: false },
+ { url: makeURL(name, 2), withNested: false },
+ ],
+
+ focusConfigList: [
+ // no focus commands
+ ],
+
+ matchAllOptions: {
+ includeUncontrolled: true
+ },
+
+ expected: [
+ // The harness windows have been focused, so appear first
+ TEST_HARNESS_URL,
+ TOP_HARNESS_URL,
+
+ // Test frames have not been focused, so appear in creation order
+ makeURL(name, 0),
+ makeURL(name, 1),
+ makeURL(name, 2),
+ EXTRA_URL,
+ ],
+ };
+
+ return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns non-focused uncontrolled windows in creation order.');
+
+promise_test(t => {
+ let name = 'focus-uncontrolled-windows-1';
+ let opts = {
+ scope: makeURL(name + '-outofscope'),
+
+ frameConfigList: [
+ { url: makeURL(name, 0), withNested: false },
+ { url: makeURL(name, 1), withNested: false },
+ { url: makeURL(name, 2), withNested: false },
+ ],
+
+ focusConfigList: [
+ { index: 0, type: 'top' },
+ { index: 1, type: 'top' },
+ { index: 2, type: 'top' },
+ ],
+
+ matchAllOptions: {
+ includeUncontrolled: true
+ },
+
+ expected: [
+ // The test harness window is a parent of all test frames. It will
+ // always have the same focus time or later as its frames. So it
+ // appears first.
+ TEST_HARNESS_URL,
+
+ makeURL(name, 2),
+ makeURL(name, 1),
+ makeURL(name, 0),
+
+ // The overall harness has been focused
+ TOP_HARNESS_URL,
+
+ // The extra frame was never focused
+ EXTRA_URL,
+ ],
+ };
+
+ return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns uncontrolled windows in focus order. Case 1.');
+
+promise_test(t => {
+ let name = 'focus-uncontrolled-windows-2';
+ let opts = {
+ scope: makeURL(name + '-outofscope'),
+
+ frameConfigList: [
+ { url: makeURL(name, 0), withNested: false },
+ { url: makeURL(name, 1), withNested: false },
+ { url: makeURL(name, 2), withNested: false },
+ ],
+
+ focusConfigList: [
+ { index: 2, type: 'top' },
+ { index: 1, type: 'top' },
+ { index: 0, type: 'top' },
+ ],
+
+ matchAllOptions: {
+ includeUncontrolled: true
+ },
+
+ expected: [
+ // The test harness window is a parent of all test frames. It will
+ // always have the same focus time or later as its frames. So it
+ // appears first.
+ TEST_HARNESS_URL,
+
+ makeURL(name, 0),
+ makeURL(name, 1),
+ makeURL(name, 2),
+
+ // The overall harness has been focused
+ TOP_HARNESS_URL,
+
+ // The extra frame was never focused
+ EXTRA_URL,
+ ],
+ };
+
+ return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns uncontrolled windows in focus order. Case 2.');
+
+promise_test(t => {
+ let name = 'focus-controlled-nested-windows';
+ let opts = {
+ scope: makeURL(name),
+
+ frameConfigList: [
+ { url: makeURL(name, 0), withNested: true },
+ { url: makeURL(name, 1), withNested: true },
+ { url: makeURL(name, 2), withNested: true },
+ ],
+
+ focusConfigList: [
+ { index: 0, type: 'top' },
+
+ // Note, some browsers don't let programmatic focus of a frame unless
+ // an ancestor window is already focused. So focus the window and
+ // then the frame.
+ { index: 1, type: 'top' },
+ { index: 1, type: 'nested' },
+
+ { index: 2, type: 'top' },
+ ],
+
+ matchAllOptions: {
+ includeUncontrolled: false
+ },
+
+ expected: [
+ // Focus order for window 2, but not its frame. We only focused
+ // the window.
+ makeURL(name, 2),
+
+ // Window 1 is next via focus order, but the window is always
+ // shown first here. The window gets its last focus time updated
+ // when the frame is focused. Since the times match between the
+ // two it falls back to creation order. The window was created
+ // before the frame. This behavior is being discussed in:
+ // https://github.com/w3c/ServiceWorker/issues/1080
+ makeURL(name, 1),
+ makeURL(name, 1, 'nested'),
+
+ // Focus order for window 0, but not its frame. We only focused
+ // the window.
+ makeURL(name, 0),
+
+ // Creation order of the frames since they are not focused by
+ // default when they are created.
+ makeURL(name, 0, 'nested'),
+ makeURL(name, 2, 'nested'),
+ ],
+ };
+
+ return matchAllOrderTest(t, opts);
+}, 'Clients.matchAll() returns controlled windows and frames in focus order.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/clients-matchall.https.html b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall.https.html
new file mode 100644
index 0000000..ce44f19
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/clients-matchall.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<title>Service Worker: Clients.matchAll</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var scope = 'resources/blank.html?clients-matchAll';
+var frames = [];
+promise_test(function(t) {
+ return service_worker_unregister_and_register(
+ t, 'resources/clients-matchall-worker.js', scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope + '#1'); })
+ .then(function(frame1) {
+ frames.push(frame1);
+ frame1.focus();
+ return with_iframe(scope + '#2');
+ })
+ .then(function(frame2) {
+ frames.push(frame2);
+ var channel = new MessageChannel();
+
+ return new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ frame2.contentWindow.navigator.serviceWorker.controller.postMessage(
+ {port:channel.port2}, [channel.port2]);
+ });
+ })
+ .then(onMessage);
+}, 'Test Clients.matchAll()');
+
+var expected = [
+ // visibilityState, focused, url, type, frameType
+ ['visible', true, new URL(scope + '#1', location).toString(), 'window', 'nested'],
+ ['visible', false, new URL(scope + '#2', location).toString(), 'window', 'nested']
+];
+
+function onMessage(e) {
+ assert_equals(e.data.length, 2);
+ assert_array_equals(e.data[0], expected[0]);
+ assert_array_equals(e.data[1], expected[1]);
+ frames.forEach(function(f) { f.remove(); });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/controller-on-disconnect.https.html b/third_party/web_platform_tests/service-workers/service-worker/controller-on-disconnect.https.html
new file mode 100644
index 0000000..f23dfe7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/controller-on-disconnect.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: Controller on load</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(function(t) {
+ var url = 'resources/empty-worker.js';
+ var scope = 'resources/blank.html';
+ var registration;
+ var controller;
+ var frame;
+ return service_worker_unregister_and_register(t, url, scope)
+ .then(function(swr) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = swr;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope)
+ })
+ .then(function(f) {
+ frame = f;
+ var w = frame.contentWindow;
+ var swc = w.navigator.serviceWorker;
+ assert_true(swc.controller instanceof w.ServiceWorker,
+ 'controller should be a ServiceWorker object');
+
+ frame.remove();
+
+ assert_equals(swc.controller, null,
+ 'disconnected frame should not be controlled');
+ });
+}, 'controller is cleared on disconnected window');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/controller-on-load.https.html b/third_party/web_platform_tests/service-workers/service-worker/controller-on-load.https.html
new file mode 100644
index 0000000..e4c5e5f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/controller-on-load.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<title>Service Worker: Controller on load</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(function(t) {
+ var url = 'resources/empty-worker.js';
+ var scope = 'resources/blank.html';
+ var registration;
+ var controller;
+ var frame;
+ return service_worker_unregister_and_register(t, url, scope)
+ .then(function(swr) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = swr;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ var w = frame.contentWindow;
+ controller = w.navigator.serviceWorker.controller;
+ assert_true(controller instanceof w.ServiceWorker,
+ 'controller should be a ServiceWorker object');
+ assert_equals(controller.scriptURL, normalizeURL(url));
+
+ // objects from different windows should not be equal
+ assert_not_equals(controller, registration.active);
+
+ return w.navigator.serviceWorker.getRegistration();
+ })
+ .then(function(frameRegistration) {
+ // SW objects from same window should be equal
+ assert_equals(frameRegistration.active, controller);
+ frame.remove();
+ });
+}, 'controller is set for a controlled document');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/controller-on-reload.https.html b/third_party/web_platform_tests/service-workers/service-worker/controller-on-reload.https.html
new file mode 100644
index 0000000..2e966d4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/controller-on-reload.https.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<title>Service Worker: Controller on reload</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(function(t) {
+ const iframe_scope = 'blank.html';
+ const scope = 'resources/' + iframe_scope;
+ var frame;
+ var registration;
+ var controller;
+ return service_worker_unregister(t, scope)
+ .then(function() {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ return frame.contentWindow.navigator.serviceWorker.register(
+ 'empty-worker.js', {scope: iframe_scope});
+ })
+ .then(function(swr) {
+ registration = swr;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var w = frame.contentWindow;
+ assert_equals(w.navigator.serviceWorker.controller, null,
+ 'controller should be null until the document is ' +
+ 'reloaded');
+ return new Promise(function(resolve) {
+ frame.onload = function() { resolve(); }
+ w.location.reload();
+ });
+ })
+ .then(function() {
+ var w = frame.contentWindow;
+ controller = w.navigator.serviceWorker.controller;
+ assert_true(controller instanceof w.ServiceWorker,
+ 'controller should be a ServiceWorker object upon reload');
+
+ // objects from separate windows should not be equal
+ assert_not_equals(controller, registration.active);
+
+ return w.navigator.serviceWorker.getRegistration(iframe_scope);
+ })
+ .then(function(frameRegistration) {
+ assert_equals(frameRegistration.active, controller);
+ frame.remove();
+ });
+ }, 'controller is set upon reload after registration');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/controller-with-no-fetch-event-handler.https.html b/third_party/web_platform_tests/service-workers/service-worker/controller-with-no-fetch-event-handler.https.html
new file mode 100644
index 0000000..d947139
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/controller-with-no-fetch-event-handler.https.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: controller without a fetch event handler</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body>
+<script>
+let registration;
+let frame;
+const host_info = get_host_info();
+const remote_base_url =
+ new URL(`${host_info.HTTPS_REMOTE_ORIGIN}${base_path()}resources/`);
+
+promise_test(async t => {
+ const script = 'resources/empty.js'
+ const scope = 'resources/';
+
+ promise_test(async t => {
+ if (frame)
+ frame.remove();
+
+ if (registration)
+ await registration.unregister();
+ }, 'cleanup global state');
+
+ registration = await
+ service_worker_unregister_and_register(t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+ frame = await with_iframe(scope + 'blank.html');
+}, 'global setup');
+
+promise_test(async t => {
+ const url = new URL('cors-approved.txt', remote_base_url);
+ const response = await frame.contentWindow.fetch(url, {mode:'no-cors'});
+ const text = await response.text();
+ assert_equals(text, '');
+}, 'cross-origin request, no-cors mode');
+
+
+promise_test(async t => {
+ const url = new URL('cors-denied.txt', remote_base_url);
+ const response = frame.contentWindow.fetch(url);
+ await promise_rejects_js(t, frame.contentWindow.TypeError, response);
+}, 'cross-origin request, cors denied');
+
+promise_test(async t => {
+ const url = new URL('cors-approved.txt', remote_base_url);
+ response = await frame.contentWindow.fetch(url);
+ let text = await response.text();
+ text = text.trim();
+ assert_equals(text, 'plaintext');
+}, 'cross-origin request, cors approved');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/credentials.https.html b/third_party/web_platform_tests/service-workers/service-worker/credentials.https.html
new file mode 100644
index 0000000..0a90dc2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/credentials.https.html
@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Credentials for service worker scripts</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/cookies/resources/cookie-helper.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// Check if the service worker's script has appropriate credentials for a new
+// worker and byte-for-byte checking.
+
+const SCOPE = 'resources/in-scope';
+const COOKIE_NAME = `service-worker-credentials-${Math.random()}`;
+
+promise_test(async t => {
+ // Set-Cookies for path=/.
+ await fetch(
+ `/cookies/resources/set-cookie.py?name=${COOKIE_NAME}&path=%2F`);
+}, 'Set cookies as initialization');
+
+async function get_cookies(worker) {
+ worker.postMessage('get cookie');
+ const message = await new Promise(resolve =>
+ navigator.serviceWorker.addEventListener('message', resolve));
+ return message.data;
+}
+
+promise_test(async t => {
+ const key = token();
+ const registration = await service_worker_unregister_and_register(
+ t, `resources/echo-cookie-worker.py?key=${key}`, SCOPE);
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+
+ const cookies = await get_cookies(worker);
+ assert_equals(cookies[COOKIE_NAME], '1', 'new worker has credentials');
+
+ await registration.update();
+ const updated_worker = registration.installing;
+ const updated_cookies = await get_cookies(updated_worker);
+ assert_equals(updated_cookies[COOKIE_NAME], '1',
+ 'updated worker has credentials');
+}, 'Main script should have credentials');
+
+promise_test(async t => {
+ const key = token();
+ const registration = await service_worker_unregister_and_register(
+ t, `resources/import-echo-cookie-worker.js?key=${key}`, SCOPE);
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+
+ const cookies = await get_cookies(worker);
+ assert_equals(cookies[COOKIE_NAME], '1', 'new worker has credentials');
+
+ await registration.update();
+ const updated_worker = registration.installing;
+ const updated_cookies = await get_cookies(updated_worker);
+ assert_equals(updated_cookies[COOKIE_NAME], '1',
+ 'updated worker has credentials');
+}, 'Imported script should have credentials');
+
+promise_test(async t => {
+ const key = token();
+ const registration = await service_worker_unregister_and_register(
+ t, `resources/import-echo-cookie-worker-module.py?key=${key}`, SCOPE, {type: 'module'});
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+
+ const cookies = await get_cookies(worker);
+ assert_equals(cookies[COOKIE_NAME], undefined, 'new module worker should not have credentials');
+
+ await registration.update();
+ const updated_worker = registration.installing;
+ const updated_cookies = await get_cookies(updated_worker);
+ assert_equals(updated_cookies[COOKIE_NAME], undefined,
+ 'updated worker should not have credentials');
+}, 'Module with an imported statement should not have credentials');
+
+promise_test(async t => {
+ const key = token();
+ const registration = await service_worker_unregister_and_register(
+t, `resources/echo-cookie-worker.py?key=${key}`, SCOPE, {type: 'module'});
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+
+ const cookies = await get_cookies(worker);
+ assert_equals(cookies[COOKIE_NAME], undefined, 'new module worker should not have credentials');
+
+ await registration.update();
+ const updated_worker = registration.installing;
+ const updated_cookies = await get_cookies(updated_worker);
+ assert_equals(updated_cookies[COOKIE_NAME], undefined,
+ 'updated worker should not have credentials');
+}, 'Script with service worker served as modules should not have credentials');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/data-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/data-iframe.html
new file mode 100644
index 0000000..d767d57
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/data-iframe.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>Service Workers in data iframes</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body></body>
+<script>
+'use strict';
+
+promise_test(t => {
+ const url = encodeURI(`data:text/html,<!DOCTYPE html>
+ <script>
+ parent.postMessage({ isDefined: 'serviceWorker' in navigator }, '*');
+ </` + `script>`);
+ var p = new Promise((resolve, reject) => {
+ window.addEventListener('message', event => {
+ resolve(event.data.isDefined);
+ });
+ });
+ with_iframe(url);
+ return p.then(isDefined => {
+ assert_false(isDefined, 'navigator.serviceWorker should not be defined in iframe');
+ });
+}, 'navigator.serviceWorker is not available in a data: iframe');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/data-transfer-files.https.html b/third_party/web_platform_tests/service-workers/service-worker/data-transfer-files.https.html
new file mode 100644
index 0000000..c503a28
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/data-transfer-files.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Post a file in a navigation controlled by a service worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<iframe id=testframe name=testframe></iframe>
+<form id=testform method=post action="/html/semantics/forms/form-submission-0/resources/file-submission.py" target=testframe enctype="multipart/form-data">
+<input name=testinput id=testinput type=file>
+</form>
+<script>
+// Test that DataTransfer with a File entry works when posted to a
+// service worker that falls back to network. Regression test for
+// https://crbug.com/944145.
+promise_test(async (t) => {
+ const scope = '/html/semantics/forms/form-submission-0/resources/';
+ const header = `pipe=header(Service-Worker-Allowed,${scope})`;
+ const script = `resources/fetch-event-network-fallback-worker.js?${header}`;
+
+ const registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const dataTransfer = new DataTransfer();
+ dataTransfer.items.add(new File(['foobar'], 'name'));
+ assert_equals(1, dataTransfer.files.length);
+
+ testinput.files = dataTransfer.files;
+ testform.submit();
+
+ const data = await new Promise(resolve => {
+ onmessage = e => {
+ if (e.source !== testframe) return;
+ resolve(e.data);
+ };
+ });
+ assert_equals(data, "foobar");
+}, 'Posting a File in a navigation handled by a service worker');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/dedicated-worker-service-worker-interception.https.html b/third_party/web_platform_tests/service-workers/service-worker/dedicated-worker-service-worker-interception.https.html
new file mode 100644
index 0000000..2144f48
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/dedicated-worker-service-worker-interception.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>DedicatedWorker: ServiceWorker interception</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+// Note that Chrome cannot pass these tests because of https://crbug.com/731599.
+
+function service_worker_interception_test(url, description) {
+ promise_test(async t => {
+ // Register a service worker whose scope includes |url|.
+ const kServiceWorkerScriptURL =
+ 'resources/service-worker-interception-service-worker.js';
+ const registration = await service_worker_unregister_and_register(
+ t, kServiceWorkerScriptURL, url);
+ add_result_callback(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Start a dedicated worker for |url|. The top-level script request and any
+ // module imports should be intercepted by the service worker.
+ const worker = new Worker(url, { type: 'module' });
+ const msg_event = await new Promise(resolve => worker.onmessage = resolve);
+ assert_equals(msg_event.data, 'LOADED_FROM_SERVICE_WORKER');
+ }, description);
+}
+
+service_worker_interception_test(
+ 'resources/service-worker-interception-network-worker.js',
+ 'Top-level module loading should be intercepted by a service worker.');
+
+service_worker_interception_test(
+ 'resources/service-worker-interception-static-import-worker.js',
+ 'Static import should be intercepted by a service worker.');
+
+service_worker_interception_test(
+ 'resources/service-worker-interception-dynamic-import-worker.js',
+ 'Dynamic import should be intercepted by a service worker.');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/detached-context.https.html b/third_party/web_platform_tests/service-workers/service-worker/detached-context.https.html
new file mode 100644
index 0000000..747a953
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/detached-context.https.html
@@ -0,0 +1,141 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service WorkerRegistration from a removed iframe</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+</body>
+<script>
+// NOTE: This file tests corner case behavior that might not be defined in the
+// spec. See https://github.com/w3c/ServiceWorker/issues/1221
+
+promise_test(t => {
+ const url = 'resources/blank.html';
+ const scope_for_iframe = 'removed-registration'
+ const scope_for_main = 'resources/' + scope_for_iframe;
+ const script = 'resources/empty-worker.js';
+ let frame;
+ let resolvedCount = 0;
+
+ return service_worker_unregister(t, scope_for_main)
+ .then(() => {
+ return with_iframe(url);
+ })
+ .then(f => {
+ frame = f;
+ return navigator.serviceWorker.register(script,
+ {scope: scope_for_main});
+ })
+ .then(r => {
+ add_completion_callback(() => { r.unregister(); });
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(() => {
+ return frame.contentWindow.navigator.serviceWorker.getRegistration(
+ scope_for_iframe);
+ })
+ .then(r => {
+ frame.remove();
+ assert_equals(r.installing, null);
+ assert_equals(r.waiting, null);
+ assert_equals(r.active.state, 'activated');
+ assert_equals(r.scope, normalizeURL(scope_for_main));
+ r.onupdatefound = () => { /* empty */ };
+
+ // We want to verify that unregister() and update() do not
+ // resolve on a detached registration. We can't check for
+ // an explicit rejection, though, because not all browsers
+ // fire rejection callbacks on detached promises. Instead
+ // we wait for a sample scope to install, activate, and
+ // unregister before declaring that the promises did not
+ // resolve.
+ r.unregister().then(() => resolvedCount += 1,
+ () => {});
+ r.update().then(() => resolvedCount += 1,
+ () => {});
+ return wait_for_activation_on_sample_scope(t, window);
+ })
+ .then(() => {
+ assert_equals(resolvedCount, 0,
+ 'methods called on a detached registration should not resolve');
+ frame.remove();
+ })
+ }, 'accessing a ServiceWorkerRegistration from a removed iframe');
+
+promise_test(t => {
+ const script = 'resources/empty-worker.js';
+ const scope = 'resources/scope/serviceworker-from-detached';
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ add_completion_callback(() => { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => { return with_iframe(scope); })
+ .then(frame => {
+ const worker = frame.contentWindow.navigator.serviceWorker.controller;
+ const ctor = frame.contentWindow.DOMException;
+ frame.remove();
+ assert_equals(worker.scriptURL, normalizeURL(script));
+ assert_equals(worker.state, 'activated');
+ worker.onstatechange = () => { /* empty */ };
+ assert_throws_dom(
+ 'InvalidStateError',
+ ctor,
+ () => { worker.postMessage(''); },
+ 'postMessage on a detached client should throw an exception.');
+ });
+ }, 'accessing a ServiceWorker object from a removed iframe');
+
+promise_test(t => {
+ const iframe = document.createElement('iframe');
+ iframe.src = 'resources/blank.html';
+ document.body.appendChild(iframe);
+ const f = iframe.contentWindow.Function;
+ function get_navigator() {
+ return f('return navigator')();
+ }
+ return new Promise(resolve => {
+ assert_equals(iframe.contentWindow.navigator, get_navigator());
+ iframe.src = 'resources/blank.html?navigate-to-new-url';
+ iframe.onload = resolve;
+ }).then(function() {
+ assert_not_equals(get_navigator().serviceWorker, null);
+ assert_equals(
+ get_navigator().serviceWorker,
+ iframe.contentWindow.navigator.serviceWorker);
+ iframe.remove();
+ });
+ }, 'accessing navigator.serviceWorker on a detached iframe');
+
+test(t => {
+ const iframe = document.createElement('iframe');
+ iframe.src = 'resources/blank.html';
+ document.body.appendChild(iframe);
+ const f = iframe.contentWindow.Function;
+ function get_navigator() {
+ return f('return navigator')();
+ }
+ assert_not_equals(get_navigator().serviceWorker, null);
+ iframe.remove();
+ assert_throws_js(TypeError, () => get_navigator());
+ }, 'accessing navigator on a removed frame');
+
+// It seems weird that about:blank and blank.html (the test above) have
+// different behavior. These expectations are based on Chromium behavior, which
+// might not be right.
+test(t => {
+ const iframe = document.createElement('iframe');
+ iframe.src = 'about:blank';
+ document.body.appendChild(iframe);
+ const f = iframe.contentWindow.Function;
+ function get_navigator() {
+ return f('return navigator')();
+ }
+ assert_not_equals(get_navigator().serviceWorker, null);
+ iframe.remove();
+ assert_equals(get_navigator().serviceWorker, null);
+ }, 'accessing navigator.serviceWorker on a removed about:blank frame');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/embed-and-object-are-not-intercepted.https.html b/third_party/web_platform_tests/service-workers/service-worker/embed-and-object-are-not-intercepted.https.html
new file mode 100644
index 0000000..581dbec
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/embed-and-object-are-not-intercepted.https.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>embed and object are not intercepted</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body>
+<script>
+let registration;
+
+const kScript = 'resources/embed-and-object-are-not-intercepted-worker.js';
+const kScope = 'resources/';
+
+promise_test(t => {
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ promise_test(() => {
+ return registration.unregister();
+ }, 'restore global state');
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ }, 'initialize global state');
+
+promise_test(t => {
+ let frame;
+ return with_iframe('resources/embed-is-not-intercepted-iframe.html')
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.test_promise;
+ })
+ .then(result => {
+ assert_equals(result, 'request for embedded content was not intercepted');
+ });
+ }, 'requests for EMBED elements of embedded HTML content should not be intercepted by service workers');
+
+promise_test(t => {
+ let frame;
+ return with_iframe('resources/object-is-not-intercepted-iframe.html')
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.test_promise;
+ })
+ .then(result => {
+ assert_equals(result, 'request for embedded content was not intercepted');
+ });
+ }, 'requests for OBJECT elements of embedded HTML content should not be intercepted by service workers');
+
+promise_test(t => {
+ let frame;
+ return with_iframe('resources/embed-image-is-not-intercepted-iframe.html')
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.test_promise;
+ })
+ .then(result => {
+ assert_equals(result, 'request was not intercepted');
+ });
+ }, 'requests for EMBED elements of an image should not be intercepted by service workers');
+
+promise_test(t => {
+ let frame;
+ return with_iframe('resources/object-image-is-not-intercepted-iframe.html')
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.test_promise;
+ })
+ .then(result => {
+ assert_equals(result, 'request was not intercepted');
+ });
+ }, 'requests for OBJECT elements of an image should not be intercepted by service workers');
+
+promise_test(t => {
+ let frame;
+ return with_iframe('resources/object-navigation-is-not-intercepted-iframe.html')
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.test_promise;
+ })
+ .then(result => {
+ assert_equals(result, 'request for embedded content was not intercepted');
+ });
+ }, 'post-load navigation of OBJECT elements should not be intercepted by service workers');
+
+
+promise_test(t => {
+ let frame;
+ return with_iframe('resources/embed-navigation-is-not-intercepted-iframe.html')
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.test_promise;
+ })
+ .then(result => {
+ assert_equals(result, 'request for embedded content was not intercepted');
+ });
+ }, 'post-load navigation of EMBED elements should not be intercepted by service workers');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/extendable-event-async-waituntil.https.html b/third_party/web_platform_tests/service-workers/service-worker/extendable-event-async-waituntil.https.html
new file mode 100644
index 0000000..04e9826
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/extendable-event-async-waituntil.https.html
@@ -0,0 +1,120 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+function sync_message(worker, message, transfer) {
+ let wait = new Promise((res, rej) => {
+ navigator.serviceWorker.addEventListener('message', function(e) {
+ if (e.data === 'ACK') {
+ res();
+ } else {
+ rej();
+ }
+ });
+ });
+ worker.postMessage(message, transfer);
+ return wait;
+}
+
+function runTest(test, step, testBody) {
+ var scope = './resources/' + step;
+ var script = 'resources/extendable-event-async-waituntil.js?' + scope;
+ return service_worker_unregister_and_register(test, script, scope)
+ .then(function(registration) {
+ test.add_cleanup(function() {
+ return service_worker_unregister(test, scope);
+ });
+
+ let worker = registration.installing;
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = function(e) { resolve(e.data); }
+ });
+
+ return wait_for_state(test, worker, 'activated')
+ .then(function() {
+ return sync_message(worker, { step: 'init', port: channel.port2 },
+ [channel.port2]);
+ })
+ .then(function() { return testBody(worker); })
+ .then(function() { return saw_message; })
+ .then(function(output) {
+ assert_equals(output.result, output.expected);
+ })
+ .then(function() { return sync_message(worker, { step: 'done' }); });
+ });
+}
+
+function msg_event_test(scope, test) {
+ var testBody = function(worker) {
+ return sync_message(worker, { step: scope });
+ };
+ return runTest(test, scope, testBody);
+}
+
+promise_test(msg_event_test.bind(this, 'no-current-extension-different-task'),
+ 'Test calling waitUntil in a task at the end of the event handler without an existing extension throws');
+
+promise_test(msg_event_test.bind(this, 'no-current-extension-different-microtask'),
+ 'Test calling waitUntil in a microtask at the end of the event handler without an existing extension suceeds');
+
+promise_test(msg_event_test.bind(this, 'current-extension-different-task'),
+ 'Test calling waitUntil in a different task an existing extension succeeds');
+
+promise_test(msg_event_test.bind(this, 'during-event-dispatch-current-extension-expired-same-microtask-turn'),
+ 'Test calling waitUntil at the end of an existing extension promise handler succeeds (event is still being dispatched)');
+
+promise_test(msg_event_test.bind(this, 'during-event-dispatch-current-extension-expired-same-microtask-turn-extra'),
+ 'Test calling waitUntil in a microtask at the end of an existing extension promise handler succeeds (event is still being dispatched)');
+
+promise_test(msg_event_test.bind(this, 'after-event-dispatch-current-extension-expired-same-microtask-turn'),
+ 'Test calling waitUntil in an existing extension promise handler succeeds (event is not being dispatched)');
+
+promise_test(msg_event_test.bind(this, 'after-event-dispatch-current-extension-expired-same-microtask-turn-extra'),
+ 'Test calling waitUntil in a microtask at the end of an existing extension promise handler throws (event is not being dispatched)');
+
+promise_test(msg_event_test.bind(this, 'current-extension-expired-different-task'),
+ 'Test calling waitUntil after the current extension expired in a different task fails');
+
+promise_test(msg_event_test.bind(this, 'script-extendable-event'),
+ 'Test calling waitUntil on a script constructed ExtendableEvent throws exception');
+
+promise_test(function(t) {
+ var testBody = function(worker) {
+ return with_iframe('./resources/pending-respondwith-async-waituntil');
+ }
+ return runTest(t, 'pending-respondwith-async-waituntil', testBody);
+ }, 'Test calling waitUntil asynchronously with pending respondWith promise.');
+
+promise_test(function(t) {
+ var testBody = function(worker) {
+ return with_iframe('./resources/during-event-dispatch-respondwith-microtask-sync-waituntil');
+ }
+ return runTest(t, 'during-event-dispatch-respondwith-microtask-sync-waituntil', testBody);
+ }, 'Test calling waitUntil synchronously inside microtask of respondWith promise (event is being dispatched).');
+
+promise_test(function(t) {
+ var testBody = function(worker) {
+ return with_iframe('./resources/during-event-dispatch-respondwith-microtask-async-waituntil');
+ }
+ return runTest(t, 'during-event-dispatch-respondwith-microtask-async-waituntil', testBody);
+ }, 'Test calling waitUntil asynchronously inside microtask of respondWith promise (event is being dispatched).');
+
+promise_test(function(t) {
+ var testBody = function(worker) {
+ return with_iframe('./resources/after-event-dispatch-respondwith-microtask-sync-waituntil');
+ }
+ return runTest(t, 'after-event-dispatch-respondwith-microtask-sync-waituntil', testBody);
+ }, 'Test calling waitUntil synchronously inside microtask of respondWith promise (event is not being dispatched).');
+
+promise_test(function(t) {
+ var testBody = function(worker) {
+ return with_iframe('./resources/after-event-dispatch-respondwith-microtask-async-waituntil');
+ }
+ return runTest(t, 'after-event-dispatch-respondwith-microtask-async-waituntil', testBody);
+ }, 'Test calling waitUntil asynchronously inside microtask of respondWith promise (event is not being dispatched).');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/extendable-event-waituntil.https.html b/third_party/web_platform_tests/service-workers/service-worker/extendable-event-waituntil.https.html
new file mode 100644
index 0000000..33b4eac
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/extendable-event-waituntil.https.html
@@ -0,0 +1,140 @@
+<!DOCTYPE html>
+<title>ExtendableEvent: waitUntil</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function runTest(test, scope, onRegister) {
+ var script = 'resources/extendable-event-waituntil.js?' + scope;
+ return service_worker_unregister_and_register(test, script, scope)
+ .then(function(registration) {
+ test.add_cleanup(function() {
+ return service_worker_unregister(test, scope);
+ });
+
+ return onRegister(registration.installing);
+ });
+}
+
+// Sends a SYN to the worker and asynchronously listens for an ACK; sets
+// |obj.synced| to true once ack'd.
+function syncWorker(worker, obj) {
+ var channel = new MessageChannel();
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ return new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ }).then(function(e) {
+ var message = e.data;
+ assert_equals(message, 'SYNC',
+ 'Should receive sync message from worker.');
+ obj.synced = true;
+ channel.port1.postMessage('ACK');
+ });
+}
+
+promise_test(function(t) {
+ // Passing scope as the test switch for worker script.
+ var scope = 'resources/install-fulfilled';
+ var onRegister = function(worker) {
+ var obj = {};
+
+ return Promise.all([
+ syncWorker(worker, obj),
+ wait_for_state(t, worker, 'installed')
+ ]).then(function() {
+ assert_true(
+ obj.synced,
+ 'state should be "installed" after the waitUntil promise ' +
+ 'for "oninstall" is fulfilled.');
+ service_worker_unregister_and_done(t, scope);
+ });
+ };
+ return runTest(t, scope, onRegister);
+ }, 'Test install event waitUntil fulfilled');
+
+promise_test(function(t) {
+ var scope = 'resources/install-multiple-fulfilled';
+ var onRegister = function(worker) {
+ var obj1 = {};
+ var obj2 = {};
+
+ return Promise.all([
+ syncWorker(worker, obj1),
+ syncWorker(worker, obj2),
+ wait_for_state(t, worker, 'installed')
+ ]).then(function() {
+ assert_true(
+ obj1.synced && obj2.synced,
+ 'state should be "installed" after all waitUntil promises ' +
+ 'for "oninstall" are fulfilled.');
+ });
+ };
+ return runTest(t, scope, onRegister);
+ }, 'Test ExtendableEvent multiple waitUntil fulfilled.');
+
+promise_test(function(t) {
+ var scope = 'resources/install-reject-precedence';
+ var onRegister = function(worker) {
+ var obj1 = {};
+ var obj2 = {};
+
+ return Promise.all([
+ syncWorker(worker, obj1)
+ .then(function() {
+ syncWorker(worker, obj2);
+ }),
+ wait_for_state(t, worker, 'redundant')
+ ]).then(function() {
+ assert_true(
+ obj1.synced,
+ 'The "redundant" state was entered after the first "extend ' +
+ 'lifetime promise" resolved.'
+ );
+ assert_true(
+ obj2.synced,
+ 'The "redundant" state was entered after the third "extend ' +
+ 'lifetime promise" resolved.'
+ );
+ });
+ };
+ return runTest(t, scope, onRegister);
+ }, 'Test ExtendableEvent waitUntil reject precedence.');
+
+promise_test(function(t) {
+ var scope = 'resources/activate-fulfilled';
+ var onRegister = function(worker) {
+ var obj = {};
+ return wait_for_state(t, worker, 'activating')
+ .then(function() {
+ return Promise.all([
+ syncWorker(worker, obj),
+ wait_for_state(t, worker, 'activated')
+ ]);
+ })
+ .then(function() {
+ assert_true(
+ obj.synced,
+ 'state should be "activated" after the waitUntil promise ' +
+ 'for "onactivate" is fulfilled.');
+ });
+ };
+ return runTest(t, scope, onRegister);
+ }, 'Test activate event waitUntil fulfilled');
+
+promise_test(function(t) {
+ var scope = 'resources/install-rejected';
+ var onRegister = function(worker) {
+ return wait_for_state(t, worker, 'redundant');
+ };
+ return runTest(t, scope, onRegister);
+ }, 'Test install event waitUntil rejected');
+
+promise_test(function(t) {
+ var scope = 'resources/activate-rejected';
+ var onRegister = function(worker) {
+ return wait_for_state(t, worker, 'activated');
+ };
+ return runTest(t, scope, onRegister);
+ }, 'Test activate event waitUntil rejected.');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-audio-tainting.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-audio-tainting.https.html
new file mode 100644
index 0000000..9821759
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-audio-tainting.https.html
@@ -0,0 +1,47 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+promise_test(async (t) => {
+ const SCOPE = 'resources/empty.html';
+ const SCRIPT = 'resources/fetch-rewrite-worker.js';
+ const host_info = get_host_info();
+ const REMOTE_ORIGIN = host_info.HTTPS_REMOTE_ORIGIN;
+
+ const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ await wait_for_state(t, reg.installing, 'activated');
+ const frame = await with_iframe(SCOPE);
+
+ const doc = frame.contentDocument;
+ const win = frame.contentWindow;
+
+ const context = new win.AudioContext();
+ try {
+ context.suspend();
+ const audio = doc.createElement('audio');
+ audio.autoplay = true;
+ const source = context.createMediaElementSource(audio);
+ const spn = context.createScriptProcessor(16384, 1, 1);
+ source.connect(spn).connect(context.destination);
+ const url = `${REMOTE_ORIGIN}/webaudio/resources/sin_440Hz_-6dBFS_1s.wav`;
+ audio.src = '/test?url=' + encodeURIComponent(url);
+ doc.body.appendChild(audio);
+
+ await new Promise((resolve) => {
+ audio.addEventListener('playing', resolve);
+ });
+ await context.resume();
+ const event = await new Promise((resolve) => {
+ spn.addEventListener('audioprocess', resolve);
+ });
+ const data = event.inputBuffer.getChannelData(0);
+ for (const e of data) {
+ assert_equals(e, 0);
+ }
+ } finally {
+ context.close();
+ }
+ }, 'Verify CORS XHR of fetch() in a Service Worker');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-double-write.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-double-write.https.html
new file mode 100644
index 0000000..dab2153
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-double-write.https.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<meta charset="utf-8">
+<title>canvas tainting when written twice</title>
+<script>
+function loadImage(doc, url) {
+ return new Promise((resolve, reject) => {
+ const image = doc.createElement('img');
+ image.onload = () => { resolve(image); }
+ image.onerror = () => { reject('failed to load: ' + url); };
+ image.src = url;
+ });
+}
+
+// Tests that a canvas is tainted after it's written to with both a clear image
+// and opaque image from the same URL. A bad implementation might cache the
+// info of the clear image and assume the opaque image is also clear because
+// it's from the same URL. See https://crbug.com/907047 for details.
+promise_test(async (t) => {
+ // Set up a service worker and a controlled iframe.
+ const script = 'resources/fetch-canvas-tainting-double-write-worker.js';
+ const scope = 'resources/fetch-canvas-tainting-double-write-iframe.html';
+ const registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const iframe = await with_iframe(scope);
+ t.add_cleanup(() => iframe.remove());
+
+ // Load the same cross-origin image URL through the controlled iframe and
+ // this uncontrolled frame. The service worker responds with a same-origin
+ // image for the controlled iframe, so it is cleartext.
+ const imagePath = base_path() + 'resources/fetch-access-control.py?PNGIMAGE';
+ const imageUrl = get_host_info()['HTTPS_REMOTE_ORIGIN'] + imagePath;
+ const clearImage = await loadImage(iframe.contentDocument, imageUrl);
+ const opaqueImage = await loadImage(document, imageUrl);
+
+ // Set up a canvas for testing tainting.
+ const canvas = document.createElement('canvas');
+ const context = canvas.getContext('2d');
+ canvas.width = clearImage.width;
+ canvas.height = clearImage.height;
+
+ // The clear image and the opaque image have the same src URL. But...
+
+ // ... the clear image doesn't taint the canvas.
+ context.drawImage(clearImage, 0, 0);
+ assert_true(canvas.toDataURL().length > 0);
+
+ // ... the opaque image taints the canvas.
+ context.drawImage(opaqueImage, 0, 0);
+ assert_throws_dom('SecurityError', () => { canvas.toDataURL(); });
+}, 'canvas is tainted after writing both a non-opaque image and an opaque image from the same URL');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-image-cache.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-image-cache.https.html
new file mode 100644
index 0000000..2132381
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-image-cache.https.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: canvas tainting of the fetched image using cached responses</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script src="resources/fetch-canvas-tainting-tests.js"></script>
+<body>
+<script>
+do_canvas_tainting_tests({
+ resource_path: base_path() + 'resources/fetch-access-control.py?PNGIMAGE',
+ cache: true
+});
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-image.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-image.https.html
new file mode 100644
index 0000000..57dc7d9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-image.https.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: canvas tainting of the fetched image</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script src="resources/fetch-canvas-tainting-tests.js"></script>
+<body>
+<script>
+do_canvas_tainting_tests({
+ resource_path: base_path() + 'resources/fetch-access-control.py?PNGIMAGE',
+ cache: false
+});
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-video-cache.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-video-cache.https.html
new file mode 100644
index 0000000..c37e8e5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-video-cache.https.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: canvas tainting of the fetched video using cache responses</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script src="resources/fetch-canvas-tainting-tests.js"></script>
+<body>
+<script>
+do_canvas_tainting_tests({
+ resource_path: base_path() + 'resources/fetch-access-control.py?VIDEO',
+ cache: true
+});
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-video-with-range-request.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-video-with-range-request.https.html
new file mode 100644
index 0000000..28c3071
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-video-with-range-request.https.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Canvas tainting due to video whose responses are fetched via a service worker including range requests</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<body>
+<script>
+// These tests try to test canvas tainting due to a <video> element. The video
+// src URL is same-origin as the page, but the response is fetched via a service
+// worker that does tricky things like returning opaque responses from another
+// origin. Furthermore, this tests range requests so there are multiple
+// responses.
+//
+// We test range requests by having the server return 206 Partial Content to the
+// first request (which doesn't necessarily have a "Range" header or one with a
+// byte range). Then the <video> element automatically makes ranged requests
+// (the "Range" HTTP request header specifies a byte range). The server responds
+// to these with 206 Partial Content for the given range.
+function range_request_test(script, expected, description) {
+ promise_test(t => {
+ let frame;
+ let registration;
+ add_result_callback(() => {
+ if (frame) frame.remove();
+ if (registration) registration.unregister();
+ });
+
+ const scope = 'resources/fetch-canvas-tainting-iframe.html';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(scope);
+ })
+ .then(f => {
+ frame = f;
+ // Add "?VIDEO&PartialContent" to get a video resource from the
+ // server using range requests.
+ const video_url = 'fetch-access-control.py?VIDEO&PartialContent';
+ return frame.contentWindow.create_test_case_promise(video_url);
+ })
+ .then(result => {
+ assert_equals(result, expected);
+ });
+ }, description);
+}
+
+// We want to consider a number of scenarios:
+// (1) Range responses come from a single origin, the same-origin as the page.
+// The canvas should not be tainted.
+range_request_test(
+ 'resources/fetch-event-network-fallback-worker.js',
+ 'NOT_TAINTED',
+ 'range responses from single origin (same-origin)');
+
+// (2) Range responses come from a single origin, cross-origin from the page
+// (and without CORS sharing). This is not possible to test, since service
+// worker can't make a request with a "Range" HTTP header in no-cors mode.
+
+// (3) Range responses come from multiple origins. The first response comes from
+// cross-origin (and without CORS sharing, so is opaque). Subsequent
+// responses come from same-origin. This should result in a load error, as regardless of canvas
+// loading range requests from multiple opaque origins can reveal information across those origins.
+range_request_test(
+ 'resources/range-request-to-different-origins-worker.js',
+ 'LOAD_ERROR',
+ 'range responses from multiple origins (cross-origin first)');
+
+// (4) Range responses come from multiple origins. The first response comes from
+// same-origin. Subsequent responses come from cross-origin (and without
+// CORS sharing). Like (2) this is not possible since the service worker
+// cannot make range requests cross-origin.
+
+// (5) Range responses come from a single origin, with a mix of opaque and
+// non-opaque responses. The first request uses 'no-cors' mode to
+// receive an opaque response, and subsequent range requests use 'cors'
+// to receive non-opaque responses. The canvas should be tainted.
+range_request_test(
+ 'resources/range-request-with-different-cors-modes-worker.js',
+ 'TAINTED',
+ 'range responses from single origin with both opaque and non-opaque responses');
+
+// (6) Range responses come from a single origin, with a mix of opaque and
+// non-opaque responses. The first request uses 'cors' mode to
+// receive an non-opaque response, and subsequent range requests use
+// 'no-cors' to receive non-opaque responses. Like (2) this is not possible.
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-video.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-video.https.html
new file mode 100644
index 0000000..e8c23a2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-canvas-tainting-video.https.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: canvas tainting of the fetched video</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script src="resources/fetch-canvas-tainting-tests.js"></script>
+<body>
+<script>
+do_canvas_tainting_tests({
+ resource_path: base_path() + 'resources/fetch-access-control.py?VIDEO',
+ cache: false
+});
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-cors-exposed-header-names.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-cors-exposed-header-names.https.html
new file mode 100644
index 0000000..317b021
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-cors-exposed-header-names.https.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<title>Service Worker: CORS-exposed header names should be transferred correctly</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(async function(t) {
+ const SCOPE = 'resources/simple.html';
+ const SCRIPT = 'resources/fetch-cors-exposed-header-names-worker.js';
+ const host_info = get_host_info();
+
+ const URL = get_host_info().HTTPS_REMOTE_ORIGIN +
+ '/service-workers/service-worker/resources/simple.txt?pipe=' +
+ 'header(access-control-allow-origin,*)|' +
+ 'header(access-control-expose-headers,*)|' +
+ 'header(foo,bar)|' +
+ 'header(set-cookie,X)';
+
+ const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ await wait_for_state(t, reg.installing, 'activated');
+ const frame = await with_iframe(SCOPE);
+
+ const response = await frame.contentWindow.fetch(URL);
+ const headers = response.headers;
+ assert_equals(headers.get('foo'), 'bar');
+ assert_equals(headers.get('set-cookie'), null);
+ assert_equals(headers.get('access-control-expose-headers'), '*');
+ }, 'CORS-exposed header names for a response from sw');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-cors-xhr.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-cors-xhr.https.html
new file mode 100644
index 0000000..f8ff445
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-cors-xhr.https.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<title>Service Worker: CORS XHR of fetch()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body>
+<script>
+promise_test(function(t) {
+ var SCOPE = 'resources/fetch-cors-xhr-iframe.html';
+ var SCRIPT = 'resources/fetch-rewrite-worker.js';
+ var host_info = get_host_info();
+
+ return login_https(t)
+ .then(function() {
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ })
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+
+ return new Promise(function(resolve, reject) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = (event) => {
+ if (event.data === 'done') {
+ resolve();
+ return;
+ }
+ test(() => {
+ assert_true(event.data.result);
+ }, event.data.testName);
+ };
+ frame.contentWindow.postMessage({},
+ host_info['HTTPS_ORIGIN'],
+ [channel.port2]);
+ });
+ });
+ }, 'Verify CORS XHR of fetch() in a Service Worker');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-csp.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-csp.https.html
new file mode 100644
index 0000000..9e7b242
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-csp.https.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<title>Service Worker: CSP control of fetch()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+
+function assert_resolves(promise, description) {
+ return promise.catch(function(reason) {
+ throw new Error(description + ' - ' + reason.message);
+ });
+}
+
+function assert_rejects(promise, description) {
+ return promise.then(
+ function() { throw new Error(description); },
+ function() {});
+}
+
+promise_test(function(t) {
+ var SCOPE = 'resources/fetch-csp-iframe.html';
+ var SCRIPT = 'resources/fetch-rewrite-worker.js';
+ var host_info = get_host_info();
+ var IMAGE_PATH =
+ base_path() + 'resources/fetch-access-control.py?PNGIMAGE';
+ var IMAGE_URL = host_info['HTTPS_ORIGIN'] + IMAGE_PATH;
+ var REMOTE_IMAGE_URL = host_info['HTTPS_REMOTE_ORIGIN'] + IMAGE_PATH;
+ var REDIRECT_URL =
+ host_info['HTTPS_ORIGIN'] + base_path() + 'resources/redirect.py';
+ var frame;
+
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(
+ SCOPE + '?' +
+ encodeURIComponent('img-src ' + host_info['HTTPS_ORIGIN'] +
+ '; script-src \'unsafe-inline\''));
+ })
+ .then(function(f) {
+ frame = f;
+ return assert_resolves(
+ frame.contentWindow.load_image(IMAGE_URL),
+ 'Allowed scope image resource should be loaded.');
+ })
+ .then(function() {
+ return assert_rejects(
+ frame.contentWindow.load_image(REMOTE_IMAGE_URL),
+ 'Disallowed scope image resource should not be loaded.');
+ })
+ .then(function() {
+ return assert_resolves(
+ frame.contentWindow.load_image(
+ // The request for IMAGE_URL will be fetched in SW.
+ './sample?url=' + encodeURIComponent(IMAGE_URL)),
+ 'Allowed scope image resource which was fetched via SW should ' +
+ 'be loaded.');
+ })
+ .then(function() {
+ return assert_rejects(
+ frame.contentWindow.load_image(
+ // The request for REMOTE_IMAGE_URL will be fetched in SW.
+ './sample?mode=no-cors&url=' +
+ encodeURIComponent(REMOTE_IMAGE_URL)),
+ 'Disallowed scope image resource which was fetched via SW ' +
+ 'should not be loaded.');
+ })
+ .then(function() {
+ frame.remove();
+ return with_iframe(
+ SCOPE + '?' +
+ encodeURIComponent(
+ 'img-src ' + REDIRECT_URL +
+ '; script-src \'unsafe-inline\''));
+ })
+ .then(function(f) {
+ frame = f;
+ return assert_resolves(
+ frame.contentWindow.load_image(
+ // Set 'ignore' not to call respondWith() in the SW.
+ REDIRECT_URL + '?ignore&Redirect=' +
+ encodeURIComponent(IMAGE_URL)),
+ 'When the request was redirected, CSP match algorithm should ' +
+ 'ignore the path component of the URL.');
+ })
+ .then(function() {
+ return assert_resolves(
+ frame.contentWindow.load_image(
+ // This request will be fetched via SW and redirected by
+ // redirect.php.
+ REDIRECT_URL + '?Redirect=' + encodeURIComponent(IMAGE_URL)),
+ 'When the request was redirected via SW, CSP match algorithm ' +
+ 'should ignore the path component of the URL.');
+ })
+ .then(function() {
+ return assert_resolves(
+ frame.contentWindow.load_image(
+ // The request for IMAGE_URL will be fetched in SW.
+ REDIRECT_URL + '?url=' + encodeURIComponent(IMAGE_URL)),
+ 'When the request was fetched via SW, CSP match algorithm ' +
+ 'should ignore the path component of the URL.');
+ })
+ .then(function() {
+ return assert_resolves(
+ frame.contentWindow.fetch(IMAGE_URL + "&fetch1", { mode: 'no-cors'}),
+ 'Allowed scope fetch resource should be loaded.');
+ })
+ .then(function() {
+ return assert_resolves(
+ frame.contentWindow.fetch(
+ // The request for IMAGE_URL will be fetched in SW.
+ './sample?url=' + encodeURIComponent(IMAGE_URL + '&fetch2'), { mode: 'no-cors'}),
+ 'Allowed scope fetch resource which was fetched via SW should be loaded.');
+ })
+ .then(function() {
+ return assert_rejects(
+ frame.contentWindow.fetch(REMOTE_IMAGE_URL + "&fetch3", { mode: 'no-cors'}),
+ 'Disallowed scope fetch resource should not be loaded.');
+ })
+ .then(function() {
+ return assert_rejects(
+ frame.contentWindow.fetch(
+ // The request for REMOTE_IMAGE_URL will be fetched in SW.
+ './sample?url=' + encodeURIComponent(REMOTE_IMAGE_URL + '&fetch4'), { mode: 'no-cors'}),
+ 'Disallowed scope fetch resource which was fetched via SW should not be loaded.');
+ })
+ .then(function() {
+ frame.remove();
+ });
+ }, 'Verify CSP control of fetch() in a Service Worker');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-error.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-error.https.html
new file mode 100644
index 0000000..ca2f884
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-error.https.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+</head>
+<body>
+<script>
+const scope = "./resources/in-scope";
+
+promise_test(async (test) => {
+ const registration = await service_worker_unregister_and_register(
+ test, "./resources/fetch-error-worker.js", scope);
+ promise_test(async () => registration.unregister(),
+ "Unregister service worker");
+ await wait_for_state(test, registration.installing, 'activated');
+}, "Setup service worker");
+
+promise_test(async (test) => {
+ const iframe = await with_iframe(scope);
+ test.add_cleanup(() => iframe.remove());
+ const response = await iframe.contentWindow.fetch("fetch-error-test");
+ try {
+ await response.text();
+ assert_unreached();
+ } catch (error) {
+ assert_true(error.message.includes("Sorry"));
+ }
+}, "Make sure a load that makes progress does not time out");
+</script>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-add-async.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-add-async.https.html
new file mode 100644
index 0000000..ac13e4f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-add-async.https.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<title>Service Worker: Fetch event added asynchronously doesn't throw</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+service_worker_test(
+ 'resources/fetch-event-add-async-worker.js');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-after-navigation-within-page.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-after-navigation-within-page.https.html
new file mode 100644
index 0000000..4812d8a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-after-navigation-within-page.https.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<title>ServiceWorker: navigator.serviceWorker.waiting</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+promise_test(function(t) {
+ var scope =
+ 'resources/fetch-event-after-navigation-within-page-iframe.html' +
+ '?hashchange';
+ var worker = 'resources/simple-intercept-worker.js';
+ var frame;
+
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ frame = f;
+ return frame.contentWindow.fetch_url('simple.txt');
+ })
+ .then(function(response) {
+ assert_equals(response, 'intercepted by service worker');
+ frame.contentWindow.location.hash = 'foo';
+ return frame.contentWindow.fetch_url('simple.txt');
+ })
+ .then(function(response) {
+ assert_equals(response, 'intercepted by service worker');
+ frame.remove();
+ })
+ }, 'Service Worker should respond to fetch event after the hash changes');
+
+promise_test(function(t) {
+ var scope =
+ 'resources/fetch-event-after-navigation-within-page-iframe.html' +
+ '?pushState';
+ var worker = 'resources/simple-intercept-worker.js';
+ var frame;
+
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ frame = f;
+ return frame.contentWindow.fetch_url('simple.txt');
+ })
+ .then(function(response) {
+ assert_equals(response, 'intercepted by service worker');
+ frame.contentWindow.history.pushState('', '', 'bar');
+ return frame.contentWindow.fetch_url('simple.txt');
+ })
+ .then(function(response) {
+ assert_equals(response, 'intercepted by service worker');
+ frame.remove();
+ })
+ }, 'Service Worker should respond to fetch event after the pushState');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-async-respond-with.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-async-respond-with.https.html
new file mode 100644
index 0000000..d9147f8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-async-respond-with.https.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<html>
+<title>respondWith cannot be called asynchronously</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// This file has tests that call respondWith() asynchronously.
+
+let frame;
+let worker;
+const script = 'resources/fetch-event-async-respond-with-worker.js';
+const scope = 'resources/simple.html';
+
+// Global setup: this must be the first promise_test.
+promise_test(async (t) => {
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+ frame = await with_iframe(scope);
+}, 'global setup');
+
+// Waits for a single message from the service worker and then removes the
+// message handler. Not safe for concurrent use.
+function wait_for_message() {
+ return new Promise((resolve) => {
+ const handler = (event) => {
+ navigator.serviceWorker.removeEventListener('message', handler);
+ resolve(event.data);
+ };
+ navigator.serviceWorker.addEventListener('message', handler);
+ });
+}
+
+// Does one test case. It fetches |url|. The service worker gets a fetch event
+// for |url| and attempts to call respondWith() asynchronously. It reports back
+// to the test whether an exception was thrown.
+async function do_test(url) {
+ // Send a message to tell the worker a new test case is starting.
+ const message = wait_for_message();
+ worker.postMessage('initializeMessageHandler');
+ const response = await message;
+ assert_equals(response, 'messageHandlerInitialized');
+
+ // Start a fetch.
+ const fetchPromise = frame.contentWindow.fetch(url);
+
+ // Receive the test result from the service worker.
+ const result = wait_for_message();
+ await fetchPromise.then(()=> {}, () => {});
+ return result;
+};
+
+promise_test(async (t) => {
+ const result = await do_test('respondWith-in-task');
+ assert_true(result.didThrow, 'should throw');
+ assert_equals(result.error, 'InvalidStateError');
+}, 'respondWith in a task throws InvalidStateError');
+
+promise_test(async (t) => {
+ const result = await do_test('respondWith-in-microtask');
+ assert_equals(result.didThrow, false, 'should not throw');
+}, 'respondWith in a microtask does not throw');
+
+// Global cleanup: the final promise_test.
+promise_test(async (t) => {
+ if (frame)
+ frame.remove();
+ await service_worker_unregister(t, scope);
+}, 'global cleanup');
+</script>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-handled.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-handled.https.html
new file mode 100644
index 0000000..08b88ce
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-handled.https.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<html>
+<title>Service Worker: FetchEvent.handled</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+let frame = null;
+let worker = null;
+const script = 'resources/fetch-event-handled-worker.js';
+const scope = 'resources/simple.html';
+const channel = new MessageChannel();
+
+// Wait for a message from the service worker and removes the message handler.
+function wait_for_message_from_worker() {
+ return new Promise((resolve) => channel.port2.onmessage = (event) => resolve(event.data));
+}
+
+// Global setup: this must be the first promise_test.
+promise_test(async (t) => {
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ worker = registration.installing;
+ if (!worker)
+ worker = registration.active;
+ worker.postMessage({port:channel.port1}, [channel.port1]);
+ await wait_for_state(t, worker, 'activated');
+}, 'global setup');
+
+promise_test(async (t) => {
+ const promise = with_iframe(scope);
+ const message = await wait_for_message_from_worker();
+ frame = await promise;
+ assert_equals(message, 'RESOLVED');
+}, 'FetchEvent.handled should resolve when respondWith() is not called for a' +
+ ' navigation request');
+
+promise_test(async (t) => {
+ frame.contentWindow.fetch('sample.txt?respondWith-not-called');
+ const message = await wait_for_message_from_worker();
+ assert_equals(message, 'RESOLVED');
+}, 'FetchEvent.handled should resolve when respondWith() is not called for a' +
+ ' sub-resource request');
+
+promise_test(async (t) => {
+ frame.contentWindow.fetch(
+ 'sample.txt?respondWith-not-called-and-event-canceled').catch((e) => {});
+ const message = await wait_for_message_from_worker();
+ assert_equals(message, 'REJECTED');
+}, 'FetchEvent.handled should reject when respondWith() is not called and the' +
+ ' event is canceled');
+
+promise_test(async (t) => {
+ frame.contentWindow.fetch(
+ 'sample.txt?respondWith-called-and-promise-resolved');
+ const message = await wait_for_message_from_worker();
+ assert_equals(message, 'RESOLVED');
+}, 'FetchEvent.handled should resolve when the promise provided' +
+ ' to respondWith() is resolved');
+
+promise_test(async (t) => {
+ frame.contentWindow.fetch(
+ 'sample.txt?respondWith-called-and-promise-resolved-to-invalid-response')
+ .catch((e) => {});
+ const message = await wait_for_message_from_worker();
+ assert_equals(message, 'REJECTED');
+}, 'FetchEvent.handled should reject when the promise provided' +
+ ' to respondWith() is resolved to an invalid response');
+
+promise_test(async (t) => {
+ frame.contentWindow.fetch(
+ 'sample.txt?respondWith-called-and-promise-rejected').catch((e) => {});
+ const message = await wait_for_message_from_worker();
+ assert_equals(message, 'REJECTED');
+}, 'FetchEvent.handled should reject when the promise provided to' +
+ ' respondWith() is rejected');
+
+// Global cleanup: the final promise_test.
+promise_test(async (t) => {
+ if (frame)
+ frame.remove();
+ await service_worker_unregister(t, scope);
+}, 'global cleanup');
+</script>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-history-backward-navigation-manual.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-history-backward-navigation-manual.https.html
new file mode 100644
index 0000000..3cf5922
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-history-backward-navigation-manual.https.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<body>
+<p>Click <a href="resources/install-worker.html?isHistoryNavigation&script=fetch-event-test-worker.js">this link</a>.
+ Once you see "method = GET,..." in the page, go to another page, and then go back to the page using the Backward button.
+ You should see "method = GET, isHistoryNavigation = true".
+</p>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-history-forward-navigation-manual.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-history-forward-navigation-manual.https.html
new file mode 100644
index 0000000..401939b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-history-forward-navigation-manual.https.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<body>
+<p>Click <a href="resources/install-worker.html?isHistoryNavigation&script=fetch-event-test-worker.js">this link</a>.
+ Once you see "method = GET,..." in the page, go back to this page using the Backward button, and then go to the second page using the Forward button.
+ You should see "method = GET, isHistoryNavigation = true".
+</p>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html
new file mode 100644
index 0000000..cf1fecc
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const worker = 'resources/fetch-event-test-worker.js';
+
+promise_test(async (t) => {
+ const scope = 'resources/simple.html?isReloadNavigation';
+
+ const reg = await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, reg.installing, 'activated');
+ const frame = await with_iframe(scope);
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentDocument.body.innerText =
+ 'Reload this frame manually!';
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = true');
+ frame.remove();
+ await reg.unregister();
+}, 'FetchEvent#request.isReloadNavigation is true for manual reload.');
+
+</script>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html
new file mode 100644
index 0000000..a349f07
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<body>
+<p>Click <a href="resources/install-worker.html?isReloadNavigation&script=fetch-event-test-worker.js">this link</a>.
+ Once you see "method = GET,..." in the page, reload the page.
+ You will see "method = GET, isReloadNavigation = true".
+</p>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-network-error.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-network-error.https.html
new file mode 100644
index 0000000..fea2ad1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-network-error.https.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<title>Service Worker: Fetch event network error</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var resolve_test_done;
+
+var test_done_promise = new Promise(function(resolve) {
+ resolve_test_done = resolve;
+ });
+
+// Called by the child frame.
+function notify_test_done(result) {
+ resolve_test_done(result);
+}
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-event-network-error-controllee-iframe.html';
+ var script = 'resources/fetch-event-network-error-worker.js';
+ var frame;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ return test_done_promise;
+ })
+ .then(function(result) {
+ frame.remove();
+ assert_equals(result, 'PASS');
+ });
+ }, 'Rejecting the fetch event or using preventDefault() causes a network ' +
+ 'error');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-redirect.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-redirect.https.html
new file mode 100644
index 0000000..5229284
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-redirect.https.html
@@ -0,0 +1,1038 @@
+<!DOCTYPE html>
+<title>Service Worker: Fetch Event Redirect Handling</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+// ------------------------
+// Utilities for testing non-navigation requests that are intercepted with
+// a redirect.
+
+const host_info = get_host_info();
+const kScript = 'resources/fetch-rewrite-worker.js';
+const kScope = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'resources/blank.html?fetch-event-redirect';
+let frame;
+
+function redirect_fetch_test(t, test) {
+ const hostKeySuffix = test['url_credentials'] ? '_WITH_CREDS' : '';
+ const successPath = base_path() + 'resources/success.py';
+
+ let acaOrigin = '';
+ let host = host_info['HTTPS_ORIGIN' + hostKeySuffix];
+ if (test['redirect_dest'] === 'no-cors') {
+ host = host_info['HTTPS_REMOTE_ORIGIN' + hostKeySuffix]
+ } else if (test['redirect_dest'] === 'cors') {
+ acaOrigin = '?ACAOrigin=' + encodeURIComponent(host_info['HTTPS_ORIGIN']);
+ host = host_info['HTTPS_REMOTE_ORIGIN' + hostKeySuffix]
+ }
+
+ const dest = '?Redirect=' + encodeURIComponent(host + successPath + acaOrigin);
+ const expectedTypeParam =
+ test['expected_type']
+ ? '&expected_type=' + test['expected_type']
+ : '';
+ const expectedRedirectedParam =
+ test['expected_redirected']
+ ? '&expected_redirected=' + test['expected_redirected']
+ : '';
+ const url = '/' + test.name +
+ '?url=' + encodeURIComponent('redirect.py' + dest) +
+ expectedTypeParam + expectedRedirectedParam
+ const request = new Request(url, test.request_init);
+
+ if (test.should_reject) {
+ return promise_rejects_js(
+ t,
+ frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(request),
+ 'Must fail to fetch: url=' + url);
+ }
+ return frame.contentWindow.fetch(request).then((response) => {
+ assert_equals(response.type, test.expected_type,
+ 'response.type');
+ assert_equals(response.redirected, test.expected_redirected,
+ 'response.redirected');
+ if (response.type === 'opaque' || response.type === 'opaqueredirect') {
+ return;
+ }
+ return response.json().then((json) => {
+ assert_equals(json.result, 'success', 'JSON result must be "success".');
+ });
+ });
+}
+
+// Set up the service worker and the frame.
+promise_test(t => {
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ promise_test(() => {
+ return registration.unregister();
+ }, 'restore global state');
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(kScope);
+ })
+ .then(f => {
+ frame = f;
+ add_completion_callback(() => { f.remove(); });
+ });
+ }, 'initialize global state');
+
+// ------------------------
+// Test every combination of:
+// - RequestMode (same-origin, cors, no-cors)
+// - RequestRedirect (manual, follow, error)
+// - redirect destination origin (same-origin, cors, no-cors)
+// - redirect destination credentials (no user/pass, user/pass)
+//
+// TODO: add navigation requests
+// TODO: add redirects to data URI and verify same-origin data-URL flag behavior
+// TODO: add test where original redirect URI is cross-origin
+// TODO: verify final method is correct for 301, 302, and 303
+// TODO: verify CORS redirect results in all further redirects being
+// considered cross origin
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-cors-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+ 'same-origin without credentials should succeed opaqueredirect ' +
+ 'interception and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-cors-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+ 'no-cors without credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-cors-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+ 'cors without credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-sameorigin-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+ 'same-origin without credentials should succeed opaqueredirect ' +
+ 'interception and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-sameorigin-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+ 'no-cors without credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-sameorigin-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+ 'cors without credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-nocors-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+ 'same-origin without credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-nocors-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'no-cors'
+ },
+ // This should succeed because its redirecting from same-origin to
+ // cross-origin. Since the same-origin URL provides the location
+ // header the manual redirect mode should result in an opaqueredirect
+ // response.
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+ 'no-cors without credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-nocors-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'no-cors'
+ },
+ // This should succeed because its redirecting from same-origin to
+ // cross-origin. Since the same-origin URL provides the location
+ // header the manual redirect mode should result in an opaqueredirect
+ // response.
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+ 'cors without credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-cors-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+ 'same-origin with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-cors-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+ 'no-cors with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-cors-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, cors mode Request redirected to ' +
+ 'cors with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-sameorigin-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+ 'same-origin with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-sameorigin-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+ 'no-cors with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-sameorigin-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, same-origin mode Request redirected to ' +
+ 'cors with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-nocors-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+ 'same-origin with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-nocors-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'no-cors'
+ },
+ // This should succeed because its redirecting from same-origin to
+ // cross-origin. Since the same-origin URL provides the location
+ // header the manual redirect mode should result in an opaqueredirect
+ // response.
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+ 'no-cors with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-manual-nocors-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'manual',
+ mode: 'no-cors'
+ },
+ // This should succeed because its redirecting from same-origin to
+ // cross-origin. Since the same-origin URL provides the location
+ // header the manual redirect mode should result in an opaqueredirect
+ // response.
+ should_reject: false
+ });
+}, 'Non-navigation, manual redirect, no-cors mode Request redirected to ' +
+ 'cors with credentials should succeed opaqueredirect interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-cors-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ expected_type: 'basic',
+ expected_redirected: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+ 'same-origin without credentials should succeed interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-cors-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'follow',
+ mode: 'cors'
+ },
+ // should reject because CORS requests require CORS headers on cross-origin
+ // resources
+ should_reject: true
+ });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+ 'no-cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-cors-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ expected_type: 'cors',
+ expected_redirected: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+ 'cors without credentials should succeed interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-sameorigin-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ expected_type: 'basic',
+ expected_redirected: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+ 'same-origin without credentials should succeed interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-sameorigin-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'follow',
+ mode: 'same-origin'
+ },
+ // should reject because same-origin requests cannot load cross-origin
+ // resources
+ should_reject: true
+ });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+ 'no-cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-sameorigin-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'follow',
+ mode: 'same-origin'
+ },
+ // should reject because same-origin requests cannot load cross-origin
+ // resources
+ should_reject: true
+ });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+ 'cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-nocors-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ expected_type: 'basic',
+ expected_redirected: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+ 'same-origin without credentials should succeed interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-nocors-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ expected_type: 'opaque',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'follow',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+ 'no-cors without credentials should succeed interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-nocors-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ expected_type: 'opaque',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'follow',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+ 'cors without credentials should succeed interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-cors-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ expected_type: 'basic',
+ expected_redirected: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+ 'same-origin with credentials should succeed interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-cors-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'cors'
+ },
+ // should reject because CORS requests require CORS headers on cross-origin
+ // resources
+ should_reject: true
+ });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+ 'no-cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-cors-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'cors'
+ },
+ // should reject because CORS requests do not allow user/pass entries in
+ // cross-origin URLs
+ // NOTE: https://github.com/whatwg/fetch/issues/112
+ should_reject: true
+ });
+}, 'Non-navigation, follow redirect, cors mode Request redirected to ' +
+ 'cors with credentials should fail interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-sameorigin-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ expected_type: 'basic',
+ expected_redirected: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'same-origin'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+ 'same-origin with credentials should succeed interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-sameorigin-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'same-origin'
+ },
+ // should reject because same-origin requests cannot load cross-origin
+ // resources
+ should_reject: true
+ });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+ 'no-cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-sameorigin-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'same-origin'
+ },
+ // should reject because same-origin requests cannot load cross-origin
+ // resources
+ should_reject: true
+ });
+}, 'Non-navigation, follow redirect, same-origin mode Request redirected to ' +
+ 'cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-nocors-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ expected_type: 'basic',
+ expected_redirected: true,
+ request_init: {
+ redirect: 'follow',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+ 'same-origin with credentials should succeed interception ' +
+ 'and response should be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-nocors-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ expected_type: 'opaque',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'follow',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+ 'no-cors with credentials should succeed interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-follow-nocors-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ expected_type: 'opaque',
+ expected_redirected: false,
+ request_init: {
+ redirect: 'follow',
+ mode: 'no-cors'
+ },
+ should_reject: false
+ });
+}, 'Non-navigation, follow redirect, no-cors mode Request redirected to ' +
+ 'cors with credentials should succeed interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-cors-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+ 'same-origin without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-cors-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+ 'no-cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-cors-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+ 'cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-sameorigin-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'same-origin'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+ 'same-origin without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-sameorigin-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'same-origin'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+ 'no-cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-sameorigin-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'same-origin'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+ 'cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-nocors-redirects-to-sameorigin-nocreds',
+ redirect_dest: 'same-origin',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'no-cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+ 'same-origin without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-nocors-redirects-to-nocors-nocreds',
+ redirect_dest: 'no-cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'no-cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+ 'no-cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-nocors-redirects-to-cors-nocreds',
+ redirect_dest: 'cors',
+ url_credentials: false,
+ request_init: {
+ redirect: 'error',
+ mode: 'no-cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+ 'cors without credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-cors-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+ 'same-origin with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-cors-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+ 'no-cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-cors-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, cors mode Request redirected to ' +
+ 'cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-sameorigin-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'same-origin'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+ 'same-origin with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-sameorigin-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'same-origin'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+ 'no-cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-sameorigin-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'same-origin'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, same-origin mode Request redirected to ' +
+ 'cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-nocors-redirects-to-sameorigin-creds',
+ redirect_dest: 'same-origin',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'no-cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+ 'same-origin with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-nocors-redirects-to-nocors-creds',
+ redirect_dest: 'no-cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'no-cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+ 'no-cors with credentials should fail interception ' +
+ 'and response should not be redirected');
+
+promise_test(function(t) {
+ return redirect_fetch_test(t, {
+ name: 'nonav-error-nocors-redirects-to-cors-creds',
+ redirect_dest: 'cors',
+ url_credentials: true,
+ request_init: {
+ redirect: 'error',
+ mode: 'no-cors'
+ },
+ // should reject because requests with 'error' RequestRedirect cannot be
+ // redirected.
+ should_reject: true
+ });
+}, 'Non-navigation, error redirect, no-cors mode Request redirected to ' +
+ 'cors with credentials should fail interception and response should not ' +
+ 'be redirected');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-referrer-policy.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-referrer-policy.https.html
new file mode 100644
index 0000000..af4b20a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-referrer-policy.https.html
@@ -0,0 +1,274 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var worker = 'resources/fetch-event-test-worker.js';
+
+function do_test(referrer, value, expected, name)
+{
+ test(() => {
+ assert_equals(value, expected);
+ }, name + (referrer ? " - Custom Referrer" : " - Default Referrer"));
+}
+
+function run_referrer_policy_tests(frame, referrer, href, origin) {
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {method: "GET", referrer: referrer})
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + href + '\n' +
+ 'ReferrerPolicy: strict-origin-when-cross-origin',
+ 'Service Worker should respond to fetch with the referrer URL when a member of RequestInit is present');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {method: "GET", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: \n' +
+ 'ReferrerPolicy: strict-origin-when-cross-origin',
+ 'Service Worker should respond to fetch with no referrer when a member of RequestInit is present with an HTTP request');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + href + '\n' +
+ 'ReferrerPolicy: strict-origin-when-cross-origin',
+ 'Service Worker should respond to fetch with the referrer with ""');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: \n' +
+ 'ReferrerPolicy: strict-origin-when-cross-origin',
+ 'Service Worker should respond to fetch with no referrer with ""');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + origin + '/' + '\n' +
+ 'ReferrerPolicy: origin',
+ 'Service Worker should respond to fetch with the referrer origin with "origin" and a same origin request');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + origin + '/' + '\n' +
+ 'ReferrerPolicy: origin',
+ 'Service Worker should respond to fetch with the referrer origin with "origin" and a cross origin request');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "origin-when-cross-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + href + '\n' +
+ 'ReferrerPolicy: origin-when-cross-origin',
+ 'Service Worker should respond to fetch with the referrer URL with "origin-when-cross-origin" and a same origin request');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "origin-when-cross-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + origin + '/' + '\n' +
+ 'ReferrerPolicy: origin-when-cross-origin',
+ 'Service Worker should respond to fetch with the referrer origin with "origin-when-cross-origin" and a cross origin request');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "no-referrer-when-downgrade", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + href + '\n' +
+ 'ReferrerPolicy: no-referrer-when-downgrade',
+ 'Service Worker should respond to fetch with no referrer with "no-referrer-when-downgrade" and a same origin request');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "no-referrer-when-downgrade", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: \n' +
+ 'ReferrerPolicy: no-referrer-when-downgrade',
+ 'Service Worker should respond to fetch with no referrer with "no-referrer-when-downgrade" and an HTTP request');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url, {referrerPolicy: "unsafe-url", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + href + '\n' +
+ 'ReferrerPolicy: unsafe-url',
+ 'Service Worker should respond to fetch with no referrer with "unsafe-url"');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "no-referrer", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: \n' +
+ 'ReferrerPolicy: no-referrer',
+ 'Service Worker should respond to fetch with no referrer URL with "no-referrer"');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "same-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + href + '\n' +
+ 'ReferrerPolicy: same-origin',
+ 'Service Worker should respond to fetch with referrer URL with "same-origin" and a same origin request');
+ var http_url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "same-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: \n' +
+ 'ReferrerPolicy: same-origin',
+ 'Service Worker should respond to fetch with no referrer with "same-origin" and cross origin request');
+ var http_url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "strict-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + origin + '/' + '\n' +
+ 'ReferrerPolicy: strict-origin',
+ 'Service Worker should respond to fetch with the referrer origin with "strict-origin" and a HTTPS cross origin request');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "strict-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + origin + '/' + '\n' +
+ 'ReferrerPolicy: strict-origin',
+ 'Service Worker should respond to fetch with the referrer origin with "strict-origin" and a same origin request');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "strict-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: \n' +
+ 'ReferrerPolicy: strict-origin',
+ 'Service Worker should respond to fetch with no referrer with "strict-origin" and a HTTP request');
+ return frame.contentWindow.fetch('resources/simple.html?referrerFull',
+ {referrerPolicy: "strict-origin-when-cross-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + href + '\n' +
+ 'ReferrerPolicy: strict-origin-when-cross-origin',
+ 'Service Worker should respond to fetch with the referrer URL with "strict-origin-when-cross-origin" and a same origin request');
+ var http_url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "strict-origin-when-cross-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: ' + origin + '/' + '\n' +
+ 'ReferrerPolicy: strict-origin-when-cross-origin',
+ 'Service Worker should respond to fetch with the referrer origin with "strict-origin-when-cross-origin" and a HTTPS cross origin request');
+ var http_url = get_host_info()['HTTP_ORIGIN'] + base_path() +
+ '/resources/simple.html?referrerFull';
+ return frame.contentWindow.fetch(http_url,
+ {referrerPolicy: "strict-origin-when-cross-origin", referrer: referrer});
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ do_test(referrer,
+ response_text,
+ 'Referrer: \n' +
+ 'ReferrerPolicy: strict-origin-when-cross-origin',
+ 'Service Worker should respond to fetch with no referrer with "strict-origin-when-cross-origin" and a HTTP request');
+ });
+}
+
+promise_test(function(t) {
+ var scope = 'resources/simple.html?referrerPolicy';
+ var frame;
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ frame = f;
+ test(() => {
+ assert_equals(frame.contentDocument.body.textContent, 'ReferrerPolicy: strict-origin-when-cross-origin');
+ }, 'Service Worker should respond to fetch with the default referrer policy');
+ // First, run the referrer policy tests without passing a referrer in RequestInit.
+ return run_referrer_policy_tests(frame, undefined, frame.contentDocument.location.href,
+ frame.contentDocument.location.origin);
+ })
+ .then(function() {
+ // Now, run the referrer policy tests while passing a referrer in RequestInit.
+ var referrer = get_host_info()['HTTPS_ORIGIN'] + base_path() + 'resources/fake-referrer';
+ return run_referrer_policy_tests(frame, 'fake-referrer', referrer,
+ frame.contentDocument.location.origin);
+ })
+ .then(function() {
+ frame.remove();
+ });
+ }, 'Service Worker responds to fetch event with the referrer policy');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-argument.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-argument.https.html
new file mode 100644
index 0000000..05e2210
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-argument.https.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<title>Service Worker: FetchEvent.respondWith() argument type test.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var resolve_test_done;
+
+var test_done_promise = new Promise(function(resolve) {
+ resolve_test_done = resolve;
+ });
+
+// Called by the child frame.
+function notify_test_done(result) {
+ resolve_test_done(result);
+}
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-event-respond-with-argument-iframe.html';
+ var script = 'resources/fetch-event-respond-with-argument-worker.js';
+ var frame;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ return test_done_promise;
+ })
+ .then(function(result) {
+ frame.remove();
+ assert_equals(result, 'PASS');
+ });
+ }, 'respondWith() takes either a Response or a promise that resolves ' +
+ 'with a Response. Other values should raise a network error.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html
new file mode 100644
index 0000000..932f903
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith with a response whose body is being loaded from the network by chunks</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const WORKER = 'resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js';
+const SCOPE = 'resources/fetch-event-respond-with-body-loaded-in-chunk-iframe.html';
+
+promise_test(async t => {
+ var reg = await service_worker_unregister_and_register(t, WORKER, SCOPE);
+ add_completion_callback(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+ let iframe = await with_iframe(SCOPE);
+ t.add_cleanup(() => iframe.remove());
+
+ let response = await iframe.contentWindow.fetch('body-in-chunk');
+ assert_equals(await response.text(), 'TEST_TRICKLE\nTEST_TRICKLE\nTEST_TRICKLE\nTEST_TRICKLE\n');
+}, 'Respond by chunks with a Response being loaded');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-custom-response.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-custom-response.https.html
new file mode 100644
index 0000000..645a29c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-custom-response.https.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith with a new Response</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const WORKER =
+ 'resources/fetch-event-respond-with-custom-response-worker.js';
+const SCOPE =
+ 'resources/blank.html';
+
+// Register a service worker, then create an iframe at url.
+function iframeTest(url, callback, name) {
+ return promise_test(async t => {
+ const reg = await service_worker_unregister_and_register(t, WORKER, SCOPE);
+ add_completion_callback(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+ const iframe = await with_iframe(url);
+ const iwin = iframe.contentWindow;
+ t.add_cleanup(() => iframe.remove());
+ await callback(t, iwin);
+ }, name);
+}
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?type=string');
+ assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a string');
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?type=blob');
+ assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a blob');
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?type=buffer');
+ assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a buffer');
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?type=buffer-view');
+ assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a buffer-view');
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?type=form-data');
+ const data = await response.formData();
+ assert_equals(data.get('result'), 'PASS');
+}, 'Subresource built from form-data');
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?type=search-params');
+ assert_equals(await response.text(), 'result=PASS');
+}, 'Subresource built from search-params');
+
+// As above, but navigations
+
+iframeTest(SCOPE + '?type=string', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Navigation resource built from a string');
+
+iframeTest(SCOPE + '?type=blob', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Navigation resource built from a blob');
+
+iframeTest(SCOPE + '?type=buffer', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Navigation resource built from a buffer');
+
+iframeTest(SCOPE + '?type=buffer-view', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Navigation resource built from a buffer-view');
+
+// Note: not testing form data for a navigation as the boundary header is lost.
+
+iframeTest(SCOPE + '?type=search-params', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'result=PASS');
+}, 'Navigation resource built from search-params');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-partial-stream.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-partial-stream.https.html
new file mode 100644
index 0000000..505cef2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-partial-stream.https.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith streams data to an intercepted fetch()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const WORKER =
+ 'resources/fetch-event-respond-with-partial-stream-worker.js';
+const SCOPE =
+ 'resources/fetch-event-respond-with-partial-stream-iframe.html';
+
+promise_test(async t => {
+ let reg = await service_worker_unregister_and_register(t, WORKER, SCOPE)
+ add_completion_callback(() => reg.unregister());
+
+ await wait_for_state(t, reg.installing, 'activated');
+
+ let frame = await with_iframe(SCOPE);
+ t.add_cleanup(_ => frame.remove());
+
+ let response = await frame.contentWindow.fetch('partial-stream.txt');
+
+ let reader = response.body.getReader();
+
+ let encoder = new TextEncoder();
+ let decoder = new TextDecoder();
+
+ let expected = 'partial-stream-content';
+ let encodedExpected = encoder.encode(expected);
+ let received = '';
+ let encodedReceivedLength = 0;
+
+ // Accumulate response data from the service worker. We do this as a loop
+ // to allow the browser the flexibility of rebuffering if it chooses. We
+ // do expect to get the partial data within the test timeout period, though.
+ // The spec is a bit vague at the moment about this, but it seems reasonable
+ // that the browser should not stall the response stream when the service
+ // worker has only written a partial result, but not closed the stream.
+ while (encodedReceivedLength < encodedExpected.length) {
+ let chunk = await reader.read();
+ assert_false(chunk.done, 'partial body stream should not be closed yet');
+
+ encodedReceivedLength += chunk.value.length;
+ received += decoder.decode(chunk.value);
+ }
+
+ // Note, the spec may allow some re-buffering between the service worker
+ // and the outer intercepted fetch. We could relax this exact chunk value
+ // match if necessary. The goal, though, is to ensure the outer fetch is
+ // not completely blocked until the service worker body is closed.
+ assert_equals(received, expected,
+ 'should receive partial content through service worker interception');
+
+ reg.active.postMessage('done');
+
+ await reader.closed;
+
+ }, 'respondWith() streams data to an intercepted fetch()');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-readable-stream-chunk.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-readable-stream-chunk.https.html
new file mode 100644
index 0000000..4544a9e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-readable-stream-chunk.https.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith with a response built from a ReadableStream</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const WORKER = 'resources/fetch-event-respond-with-readable-stream-chunk-worker.js';
+const SCOPE = 'resources/fetch-event-respond-with-readable-stream-chunk-iframe.html';
+
+promise_test(async t => {
+ var reg = await service_worker_unregister_and_register(t, WORKER, SCOPE);
+ add_completion_callback(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+ let iframe = await with_iframe(SCOPE);
+ t.add_cleanup(() => iframe.remove());
+
+ let response = await iframe.contentWindow.fetch('body-stream');
+ assert_equals(await response.text(), 'chunk #1 chunk #2 chunk #3 chunk #4');
+}, 'Respond by chunks with a Response built from a ReadableStream');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-readable-stream.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-readable-stream.https.html
new file mode 100644
index 0000000..439e547
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-readable-stream.https.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith with a response built from a ReadableStream</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script>
+'use strict';
+
+const WORKER =
+ 'resources/fetch-event-respond-with-readable-stream-worker.js';
+const SCOPE =
+ 'resources/blank.html';
+
+// Register a service worker, then create an iframe at url.
+function iframeTest(url, callback, name) {
+ return promise_test(async t => {
+ const reg = await service_worker_unregister_and_register(t, WORKER, SCOPE);
+ add_completion_callback(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+ const iframe = await with_iframe(url);
+ const iwin = iframe.contentWindow;
+ t.add_cleanup(() => iframe.remove());
+ await callback(t, iwin);
+ }, name);
+}
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?stream');
+ assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a ReadableStream');
+
+iframeTest(SCOPE + '?stream', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Main resource built from a ReadableStream');
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?stream&delay');
+ assert_equals(await response.text(), 'PASS');
+}, 'Subresource built from a ReadableStream - delayed');
+
+iframeTest(SCOPE + '?stream&delay', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'PASS');
+}, 'Main resource built from a ReadableStream - delayed');
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const response = await iwin.fetch('?stream&use-fetch-stream');
+ assert_equals(await response.text(), 'PASS\n');
+}, 'Subresource built from a ReadableStream - fetch stream');
+
+iframeTest(SCOPE + '?stream&use-fetch-stream', (t, iwin) => {
+ assert_equals(iwin.document.body.textContent, 'PASS\n');
+}, 'Main resource built from a ReadableStream - fetch stream');
+
+iframeTest(SCOPE, async (t, iwin) => {
+ const id = token();
+ let response = await iwin.fetch('?stream&observe-cancel&id=${id}');
+ response.body.cancel();
+
+ // Wait for a while to avoid a race between the cancel handling and the
+ // second fetch request.
+ await new Promise(r => step_timeout(r, 10));
+
+ response = await iwin.fetch('?stream&query-cancel&id=${id}');
+ assert_equals(await response.text(), 'cancelled');
+}, 'Cancellation in the page should be observable in the service worker');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html
new file mode 100644
index 0000000..2a44811
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respondWith with response body having invalid chunks</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const WORKER =
+ 'resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js';
+const SCOPE =
+ 'resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html';
+
+// Called by the iframe when it has the reader promise we should watch.
+var set_reader_promise;
+let reader_promise = new Promise(resolve => set_reader_promise = resolve);
+
+var set_fetch_promise;
+let fetch_promise = new Promise(resolve => set_fetch_promise = resolve);
+
+// This test creates an controlled iframe that makes a fetch request. The
+// service worker returns a response with a body stream containing an invalid
+// chunk.
+promise_test(async t => {
+ // Start off the process.
+ let errorConstructor;
+ await service_worker_unregister_and_register(t, WORKER, SCOPE)
+ .then(reg => {
+ add_completion_callback(() => reg.unregister());
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(() => with_iframe(SCOPE))
+ .then(frame => {
+ t.add_cleanup(() => frame.remove())
+ errorConstructor = frame.contentWindow.TypeError;
+ });
+
+ await promise_rejects_js(t, errorConstructor, reader_promise,
+ "read() should be rejected");
+ // Fetch should complete properly, because the reader error is caught in
+ // the subframe. That is, there should be no errors _other_ than the
+ // reader!
+ return fetch_promise;
+ }, 'Response with a ReadableStream having non-Uint8Array chunks should be transferred as errored');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-stops-propagation.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-stops-propagation.https.html
new file mode 100644
index 0000000..31fd616
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-respond-with-stops-propagation.https.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var script =
+ 'resources/fetch-event-respond-with-stops-propagation-worker.js';
+ var scope = 'resources/simple.html';
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ t.add_cleanup(function() { frame.remove(); });
+ var channel = new MessageChannel();
+ var saw_message = new Promise(function(resolve) {
+ channel.port1.onmessage = function(e) { resolve(e.data); }
+ });
+ var worker = frame.contentWindow.navigator.serviceWorker.controller;
+
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ return saw_message;
+ })
+ .then(function(message) {
+ assert_equals(message, 'PASS');
+ })
+ }, 'respondWith() invokes stopImmediatePropagation()');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-throws-after-respond-with.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-throws-after-respond-with.https.html
new file mode 100644
index 0000000..d98fb22
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-throws-after-respond-with.https.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title></title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-event-throws-after-respond-with-iframe.html';
+ var workerscript = 'resources/respond-then-throw-worker.js';
+ var iframe;
+ return service_worker_unregister_and_register(t, workerscript, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated')
+ .then(() => reg.active);
+ })
+ .then(function(worker) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = function(e) {
+ assert_equals(e.data, 'SYNC', ' Should receive sync message.');
+ channel.port1.postMessage('ACK');
+ }
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ // The iframe will only be loaded after the sync is completed.
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ assert_true(frame.contentDocument.body.innerHTML.includes("intercepted"));
+ })
+ }, 'Fetch event handler throws after a successful respondWith()');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-within-sw-manual.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-within-sw-manual.https.html
new file mode 100644
index 0000000..15a2e95
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-within-sw-manual.https.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const worker = 'resources/fetch-event-within-sw-worker.js';
+
+function wait(ms) {
+ return new Promise(r => setTimeout(r, ms));
+}
+
+function reset() {
+ for (const iframe of [...document.querySelectorAll('.test-iframe')]) {
+ iframe.remove();
+ }
+ return navigator.serviceWorker.getRegistrations().then(registrations => {
+ return Promise.all(registrations.map(r => r.unregister()));
+ }).then(() => caches.keys()).then(cacheKeys => {
+ return Promise.all(cacheKeys.map(c => caches.delete(c)));
+ });
+}
+
+add_completion_callback(reset);
+
+function regReady(reg) {
+ return new Promise((resolve, reject) => {
+ if (reg.active) {
+ resolve();
+ return;
+ }
+ const nextWorker = reg.waiting || reg.installing;
+
+ nextWorker.addEventListener('statechange', () => {
+ if (nextWorker.state == 'redundant') {
+ reject(Error(`Service worker failed to install`));
+ return;
+ }
+ if (nextWorker.state == 'activated') {
+ resolve();
+ }
+ });
+ });
+}
+
+function getCookies() {
+ return new Map(
+ document.cookie
+ .split(/;/g)
+ .map(c => c.trim().split('=').map(s => s.trim()))
+ );
+}
+
+function registerSwAndOpenFrame() {
+ return reset().then(() => navigator.serviceWorker.register(worker, {scope: 'resources/'}))
+ .then(reg => regReady(reg))
+ .then(() => with_iframe('resources/simple.html'));
+}
+
+function raceBroadcastAndCookie(channel, cookie) {
+ const initialCookie = getCookies().get(cookie);
+ let done = false;
+
+ return Promise.race([
+ new Promise(resolve => {
+ const bc = new BroadcastChannel(channel);
+ bc.onmessage = () => {
+ bc.close();
+ resolve('broadcast');
+ };
+ }),
+ (function checkCookie() {
+ // Stop polling if the broadcast channel won
+ if (done == true) return;
+ if (getCookies().get(cookie) != initialCookie) return 'cookie';
+
+ return wait(200).then(checkCookie);
+ }())
+ ]).then(val => {
+ done = true;
+ return val;
+ });
+}
+
+promise_test(() => {
+ return Notification.requestPermission().then(permission => {
+ if (permission != "granted") {
+ throw Error('You must allow notifications for this origin before running this test.');
+ }
+ return registerSwAndOpenFrame();
+ }).then(iframe => {
+ return Promise.resolve().then(() => {
+ // In this test, the service worker will ping the 'icon-request' channel
+ // if it intercepts a request for 'notification_icon.py'. If the request
+ // reaches the server it sets the 'notification' cookie to the value given
+ // in the URL. "raceBroadcastAndCookie" monitors both and returns which
+ // happens first.
+ const race = raceBroadcastAndCookie('icon-request', 'notification');
+ const notification = new iframe.contentWindow.Notification('test', {
+ icon: `notification_icon.py?set-cookie-notification=${Math.random()}`
+ });
+ notification.close();
+
+ return race.then(winner => {
+ assert_equals(winner, 'broadcast', 'The service worker intercepted the from-window notification icon request');
+ });
+ }).then(() => {
+ // Similar race to above, but this time the service worker requests the
+ // notification.
+ const race = raceBroadcastAndCookie('icon-request', 'notification');
+ iframe.contentWindow.fetch(`show-notification?set-cookie-notification=${Math.random()}`);
+
+ return race.then(winner => {
+ assert_equals(winner, 'broadcast', 'The service worker intercepted the from-service-worker notification icon request');
+ });
+ })
+ });
+}, `Notification requests intercepted both from window and SW`);
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event-within-sw.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-within-sw.https.html
new file mode 100644
index 0000000..0b52b18
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event-within-sw.https.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+
+<script>
+const worker = 'resources/fetch-event-within-sw-worker.js';
+
+async function registerSwAndOpenFrame(t) {
+ const registration = await navigator.serviceWorker.register(
+ worker, { scope: 'resources/' });
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const frame = await with_iframe('resources/simple.html');
+ t.add_cleanup(() => frame.remove());
+ return frame;
+}
+
+async function deleteCaches() {
+ const cacheKeys = await caches.keys();
+ await Promise.all(cacheKeys.map(c => caches.delete(c)));
+}
+
+promise_test(async t => {
+ t.add_cleanup(deleteCaches);
+
+ const iframe = await registerSwAndOpenFrame(t);
+ const fetchText =
+ await iframe.contentWindow.fetch('sample.txt').then(r => r.text());
+
+ const cache = await iframe.contentWindow.caches.open('test');
+ await cache.add('sample.txt');
+
+ const response = await cache.match('sample.txt');
+ const cacheText = await (response ? response.text() : 'cache match failed');
+ assert_equals(fetchText, 'intercepted', 'fetch intercepted');
+ assert_equals(cacheText, 'intercepted', 'cache.add intercepted');
+}, 'Service worker intercepts requests from window');
+
+promise_test(async t => {
+ const iframe = await registerSwAndOpenFrame(t);
+ const [fetchText, cacheText] = await Promise.all([
+ iframe.contentWindow.fetch('sample.txt-inner-fetch').then(r => r.text()),
+ iframe.contentWindow.fetch('sample.txt-inner-cache').then(r => r.text())
+ ]);
+ assert_equals(fetchText, 'Hello world\n', 'fetch within SW not intercepted');
+ assert_equals(cacheText, 'Hello world\n',
+ 'cache.add within SW not intercepted');
+}, 'Service worker does not intercept fetch/cache requests within service ' +
+ 'worker');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event.https.h2.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event.https.h2.html
new file mode 100644
index 0000000..5cd381e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event.https.h2.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const worker = 'resources/fetch-event-test-worker.js';
+
+const method = 'POST';
+const duplex = 'half';
+
+function createBody(t) {
+ const rs = new ReadableStream({start(c) {
+ c.enqueue('i a');
+ c.enqueue('m the request');
+ step_timeout(t.step_func(() => {
+ c.enqueue(' body');
+ c.close();
+ }, 10));
+ }});
+ return rs.pipeThrough(new TextEncoderStream());
+}
+
+promise_test(async t => {
+ const scope = 'resources/';
+ const registration =
+ await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // This will happen after all other tests
+ promise_test(t => {
+ return registration.unregister();
+ }, 'restore global state');
+}, 'global setup');
+
+// Test that the service worker can read FetchEvent#body when it is made from
+// a ReadableStream. It responds with request body it read.
+promise_test(async t => {
+ const body = createBody(t);
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = `resources/simple.html?ignore&id=${token()}`;
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ const response = await frame.contentWindow.fetch('simple.html?request-body', {
+ method, body, duplex});
+ assert_equals(response.status, 200, 'status');
+ const text = await response.text();
+ assert_equals(text, 'i am the request body', 'body');
+}, 'The streaming request body is readable in the service worker.');
+
+// Network fallback
+promise_test(async t => {
+ const body = createBody(t);
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = `resources/simple.html?ignore&id=${token()}`;
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ // Add "?ignore" so that the service worker falls back to
+ // echo-content.h2.py.
+ const echo_url = '/fetch/api/resources/echo-content.h2.py?ignore';
+ const response =
+ await frame.contentWindow.fetch(echo_url, { method, body, duplex});
+ assert_equals(response.status, 200, 'status');
+ const text = await response.text();
+ assert_equals(text, 'i am the request body', 'body');
+}, 'Network fallback for streaming upload.');
+
+// When the streaming body is used in the service worker, network fallback
+// fails.
+promise_test(async t => {
+ const body = createBody(t);
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = `resources/simple.html?ignore&id=${token()}`;
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ const echo_url = '/fetch/api/resources/echo-content.h2.py?use-and-ignore';
+ const w = frame.contentWindow;
+ await promise_rejects_js(t, w.TypeError, w.fetch(echo_url, {
+ method, body, duplex}));
+}, 'When the streaming request body is used, network fallback fails.');
+
+// When the streaming body is used by clone() in the service worker, network
+// fallback succeeds.
+promise_test(async t => {
+ const body = createBody(t);
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = `resources/simple.html?ignore&id=${token()}`;
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ // Add "?clone-and-ignore" so that the service worker falls back to
+ // echo-content.h2.py.
+ const echo_url = '/fetch/api/resources/echo-content.h2.py?clone-and-ignore';
+ const response = await frame.contentWindow.fetch(echo_url, {
+ method, body, duplex});
+ assert_equals(response.status, 200, 'status');
+ const text = await response.text();
+ assert_equals(text, 'i am the request body', 'body');
+}, 'Running clone() in the service worker does not prevent network fallback.');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-event.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-event.https.html
new file mode 100644
index 0000000..ce53f3c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-event.https.html
@@ -0,0 +1,1000 @@
+<!DOCTYPE html>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var worker = 'resources/fetch-event-test-worker.js';
+function wait(ms) {
+ return new Promise(resolve => step_timeout(resolve, ms));
+}
+
+promise_test(async t => {
+ const scope = 'resources/';
+ const registration =
+ await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // This will happen after all other tests
+ promise_test(t => {
+ return registration.unregister();
+ }, 'restore global state');
+ }, 'global setup');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?headers';
+ return with_iframe(page_url)
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ const headers = JSON.parse(frame.contentDocument.body.textContent);
+ const header_names = {};
+ for (const [name, value] of headers) {
+ header_names[name] = true;
+ }
+
+ assert_true(
+ header_names.hasOwnProperty('accept'),
+ 'request includes "Accept" header as inserted by Fetch'
+ );
+ });
+ }, 'Service Worker headers in the request of a fetch event');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?string';
+ return with_iframe(page_url)
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'Test string',
+ 'Service Worker should respond to fetch with a test string');
+ assert_equals(
+ frame.contentDocument.contentType,
+ 'text/plain',
+ 'The content type of the response created with a string should be text/plain');
+ assert_equals(
+ frame.contentDocument.characterSet,
+ 'UTF-8',
+ 'The character set of the response created with a string should be UTF-8');
+ });
+ }, 'Service Worker responds to fetch event with string');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?string';
+ var frame;
+ return with_iframe(page_url)
+ .then(function(f) {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.fetch(page_url + "#foo")
+ })
+ .then(function(response) { return response.text() })
+ .then(function(text) {
+ assert_equals(
+ text,
+ 'Test string',
+ 'Service Worker should respond to fetch with a test string');
+ });
+ }, 'Service Worker responds to fetch event using request fragment with string');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?blob';
+ return with_iframe(page_url)
+ .then(frame => {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'Test blob',
+ 'Service Worker should respond to fetch with a test string');
+ });
+ }, 'Service Worker responds to fetch event with blob body');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?referrer';
+ return with_iframe(page_url)
+ .then(frame => {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'Referrer: ' + document.location.href,
+ 'Service Worker should respond to fetch with the referrer URL');
+ });
+ }, 'Service Worker responds to fetch event with the referrer URL');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?clientId';
+ var frame;
+ return with_iframe(page_url)
+ .then(function(f) {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'Client ID Not Found',
+ 'Service Worker should respond to fetch with a client id');
+ return frame.contentWindow.fetch('resources/other.html?clientId');
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ assert_equals(
+ response_text.substr(0, 15),
+ 'Client ID Found',
+ 'Service Worker should respond to fetch with an existing client id');
+ });
+ }, 'Service Worker responds to fetch event with an existing client id');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?resultingClientId';
+ const expected_found = 'Resulting Client ID Found';
+ const expected_not_found = 'Resulting Client ID Not Found';
+ return with_iframe(page_url)
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(
+ frame.contentDocument.body.textContent.substr(0, expected_found.length),
+ expected_found,
+ 'Service Worker should respond with an existing resulting client id for non-subresource requests');
+ return frame.contentWindow.fetch('resources/other.html?resultingClientId');
+ })
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ assert_equals(
+ response_text.substr(0),
+ expected_not_found,
+ 'Service Worker should respond with an empty resulting client id for subresource requests');
+ });
+ }, 'Service Worker responds to fetch event with the correct resulting client id');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?ignore';
+ return with_iframe(page_url)
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'Here\'s a simple html file.\n',
+ 'Response should come from fallback to native fetch');
+ });
+ }, 'Service Worker does not respond to fetch event');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?null';
+ return with_iframe(page_url)
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ '',
+ 'Response should be the empty string');
+ });
+ }, 'Service Worker responds to fetch event with null response body');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?fetch';
+ return with_iframe(page_url)
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'Here\'s an other html file.\n',
+ 'Response should come from fetched other file');
+ });
+ }, 'Service Worker fetches other file in fetch event');
+
+// Creates a form and an iframe and does a form submission that navigates the
+// frame to |action_url|. Returns the frame after navigation.
+function submit_form(action_url) {
+ return new Promise(resolve => {
+ const frame = document.createElement('iframe');
+ frame.name = 'post-frame';
+ document.body.appendChild(frame);
+ const form = document.createElement('form');
+ form.target = frame.name;
+ form.action = action_url;
+ form.method = 'post';
+ const input1 = document.createElement('input');
+ input1.type = 'text';
+ input1.value = 'testValue1';
+ input1.name = 'testName1'
+ form.appendChild(input1);
+ const input2 = document.createElement('input');
+ input2.type = 'text';
+ input2.value = 'testValue2';
+ input2.name = 'testName2'
+ form.appendChild(input2);
+ document.body.appendChild(form);
+ frame.onload = function() {
+ form.remove();
+ resolve(frame);
+ };
+ form.submit();
+ });
+}
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?form-post';
+ return submit_form(page_url)
+ .then(frame => {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'POST:application/x-www-form-urlencoded:' +
+ 'testName1=testValue1&testName2=testValue2');
+ });
+ }, 'Service Worker responds to fetch event with POST form');
+
+promise_test(t => {
+ // Add '?ignore' so the service worker falls back to network.
+ const page_url = 'resources/echo-content.py?ignore';
+ return submit_form(page_url)
+ .then(frame => {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'testName1=testValue1&testName2=testValue2');
+ });
+ }, 'Service Worker falls back to network in fetch event with POST form');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?multiple-respond-with';
+ return with_iframe(page_url)
+ .then(frame => {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ '(0)(1)[InvalidStateError](2)[InvalidStateError]',
+ 'Multiple calls of respondWith must throw InvalidStateErrors.');
+ });
+ }, 'Multiple calls of respondWith must throw InvalidStateErrors');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?used-check';
+ var first_frame;
+ return with_iframe(page_url)
+ .then(function(frame) {
+ assert_equals(frame.contentDocument.body.textContent,
+ 'Here\'s an other html file.\n',
+ 'Response should come from fetched other file');
+ first_frame = frame;
+ t.add_cleanup(() => { first_frame.remove(); });
+ return with_iframe(page_url);
+ })
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ // When we access to the page_url in the second time, the content of the
+ // response is generated inside the ServiceWorker. The body contains
+ // the value of bodyUsed of the first response which is already
+ // consumed by FetchEvent.respondWith method.
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'bodyUsed: true',
+ 'event.respondWith must set the used flag.');
+ });
+ }, 'Service Worker event.respondWith must set the used flag');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?fragment-check';
+ var fragment = '#/some/fragment';
+ var first_frame;
+ return with_iframe(page_url + fragment)
+ .then(function(frame) {
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'Fragment Found :' + fragment,
+ 'Service worker should expose URL fragments in request.');
+ });
+ }, 'Service Worker should expose FetchEvent URL fragments.');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?cache';
+ var frame;
+ var cacheTypes = [
+ undefined, 'default', 'no-store', 'reload', 'no-cache', 'force-cache', 'only-if-cached'
+ ];
+ return with_iframe(page_url)
+ .then(function(f) {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentWindow.document.body.textContent, 'default');
+ var tests = cacheTypes.map(function(type) {
+ return new Promise(function(resolve, reject) {
+ var init = {cache: type};
+ if (type === 'only-if-cached') {
+ // For privacy reasons, for the time being, only-if-cached
+ // requires the mode to be same-origin.
+ init.mode = 'same-origin';
+ }
+ return frame.contentWindow.fetch(page_url + '=' + type, init)
+ .then(function(response) { return response.text(); })
+ .then(function(response_text) {
+ var expected = (type === undefined) ? 'default' : type;
+ assert_equals(response_text, expected,
+ 'Service Worker should respond to fetch with the correct type');
+ })
+ .then(resolve)
+ .catch(reject);
+ });
+ });
+ return Promise.all(tests);
+ })
+ .then(function() {
+ return new Promise(function(resolve, reject) {
+ frame.addEventListener('load', function onLoad() {
+ frame.removeEventListener('load', onLoad);
+ try {
+ assert_equals(frame.contentWindow.document.body.textContent,
+ 'no-cache');
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ frame.contentWindow.location.reload();
+ });
+ });
+ }, 'Service Worker responds to fetch event with the correct cache types');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?eventsource';
+ var frame;
+
+ function test_eventsource(opts) {
+ return new Promise(function(resolve, reject) {
+ var eventSource = new frame.contentWindow.EventSource(page_url, opts);
+ eventSource.addEventListener('message', function(msg) {
+ eventSource.close();
+ try {
+ var data = JSON.parse(msg.data);
+ assert_equals(data.mode, 'cors',
+ 'EventSource should make CORS requests.');
+ assert_equals(data.cache, 'no-store',
+ 'EventSource should bypass the http cache.');
+ var expectedCredentials = opts.withCredentials ? 'include'
+ : 'same-origin';
+ assert_equals(data.credentials, expectedCredentials,
+ 'EventSource should pass correct credentials mode.');
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ eventSource.addEventListener('error', function(e) {
+ eventSource.close();
+ reject('The EventSource fired an error event.');
+ });
+ });
+ }
+
+ return with_iframe(page_url)
+ .then(function(f) {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return test_eventsource({ withCredentials: false });
+ })
+ .then(function() {
+ return test_eventsource({ withCredentials: true });
+ });
+ }, 'Service Worker should intercept EventSource');
+
+promise_test(t => {
+ const page_url = 'resources/simple.html?integrity';
+ var frame;
+ var integrity_metadata = 'gs0nqru8KbsrIt5YToQqS9fYao4GQJXtcId610g7cCU=';
+
+ return with_iframe(page_url)
+ .then(function(f) {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ // A request has associated integrity metadata (a string).
+ // Unless stated otherwise, it is the empty string.
+ assert_equals(
+ frame.contentDocument.body.textContent, '');
+
+ return frame.contentWindow.fetch(page_url, {'integrity': integrity_metadata});
+ })
+ .then(response => {
+ return response.text();
+ })
+ .then(response_text => {
+ assert_equals(response_text, integrity_metadata, 'integrity');
+ });
+ }, 'Service Worker responds to fetch event with the correct integrity_metadata');
+
+// Test that the service worker can read FetchEvent#body when it is a string.
+// It responds with request body it read.
+promise_test(t => {
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/simple.html?ignore-for-request-body-string';
+ let frame;
+
+ return with_iframe(page_url)
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ return frame.contentWindow.fetch('simple.html?request-body', {
+ method: 'POST',
+ body: 'i am the request body'
+ });
+ })
+ .then(response => {
+ return response.text();
+ })
+ .then(response_text => {
+ assert_equals(response_text, 'i am the request body');
+ });
+ }, 'FetchEvent#body is a string');
+
+// Test that the service worker can read FetchEvent#body when it is made from
+// a ReadableStream. It responds with request body it read.
+promise_test(async t => {
+ const rs = new ReadableStream({start(c) {
+ c.enqueue('i a');
+ c.enqueue('m the request');
+ step_timeout(t.step_func(() => {
+ c.enqueue(' body');
+ c.close();
+ }, 10));
+ }});
+
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = `resources/simple.html?ignore&id=${token()}`;
+
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ const res = await frame.contentWindow.fetch('simple.html?request-body', {
+ method: 'POST',
+ body: rs.pipeThrough(new TextEncoderStream()),
+ duplex: 'half',
+ });
+ assert_equals(await res.text(), 'i am the request body');
+ }, 'FetchEvent#body is a ReadableStream');
+
+// Test that the request body is sent to network upon network fallback,
+// for a string body.
+promise_test(t => {
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/?ignore-for-request-body-fallback-string';
+ let frame;
+
+ return with_iframe(page_url)
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ // Add "?ignore" so the service worker falls back to echo-content.py.
+ const echo_url = '/fetch/api/resources/echo-content.py?ignore';
+ return frame.contentWindow.fetch(echo_url, {
+ method: 'POST',
+ body: 'i am the request body'
+ });
+ })
+ .then(response => {
+ return response.text();
+ })
+ .then(response_text => {
+ assert_equals(
+ response_text,
+ 'i am the request body',
+ 'the network fallback request should include the request body');
+ });
+ }, 'FetchEvent#body is a string and is passed to network fallback');
+
+// Test that the request body is sent to network upon network fallback,
+// for a ReadableStream body.
+promise_test(async t => {
+ const rs = new ReadableStream({start(c) {
+ c.enqueue('i a');
+ c.enqueue('m the request');
+ t.step_timeout(t.step_func(() => {
+ c.enqueue(' body');
+ c.close();
+ }, 10));
+ }});
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/?ignore-for-request-body-fallback-string';
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ // Add "?ignore" so the service worker falls back to echo-content.py.
+ const echo_url = '/fetch/api/resources/echo-content.py?ignore';
+ const w = frame.contentWindow;
+ await promise_rejects_js(t, w.TypeError, w.fetch(echo_url, {
+ method: 'POST',
+ body: rs
+ }));
+ }, 'FetchEvent#body is a none Uint8Array ReadableStream and is passed to a service worker');
+
+// Test that the request body is sent to network upon network fallback even when
+// the request body is used in the service worker, for a string body.
+promise_test(async t => {
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/?ignore-for-request-body-fallback-string';
+
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ // Add "?use-and-ignore" so the service worker falls back to echo-content.py.
+ const echo_url = '/fetch/api/resources/echo-content.py?use-and-ignore';
+ const response = await frame.contentWindow.fetch(echo_url, {
+ method: 'POST',
+ body: 'i am the request body'
+ });
+ const text = await response.text();
+ assert_equals(
+ text,
+ 'i am the request body',
+ 'the network fallback request should include the request body');
+ }, 'FetchEvent#body is a string, used and passed to network fallback');
+
+// Test that the request body is sent to network upon network fallback even when
+// the request body is used by clone() in the service worker, for a string body.
+promise_test(async t => {
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/?ignore-for-request-body-fallback-string';
+
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ // Add "?clone-and-ignore" so the service worker falls back to
+ // echo-content.py.
+ const echo_url = '/fetch/api/resources/echo-content.py?clone-and-ignore';
+ const response = await frame.contentWindow.fetch(echo_url, {
+ method: 'POST',
+ body: 'i am the request body'
+ });
+ const text = await response.text();
+ assert_equals(
+ text,
+ 'i am the request body',
+ 'the network fallback request should include the request body');
+ }, 'FetchEvent#body is a string, cloned and passed to network fallback');
+
+// Test that the service worker can read FetchEvent#body when it is a blob.
+// It responds with request body it read.
+promise_test(t => {
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/simple.html?ignore-for-request-body-blob';
+ let frame;
+
+ return with_iframe(page_url)
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ const blob = new Blob(['it\'s me the blob', ' ', 'and more blob!']);
+ return frame.contentWindow.fetch('simple.html?request-body', {
+ method: 'POST',
+ body: blob
+ });
+ })
+ .then(response => {
+ return response.text();
+ })
+ .then(response_text => {
+ assert_equals(response_text, 'it\'s me the blob and more blob!');
+ });
+ }, 'FetchEvent#body is a blob');
+
+// Test that the request body is sent to network upon network fallback,
+// for a blob body.
+promise_test(t => {
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/simple.html?ignore-for-request-body-fallback-blob';
+ let frame;
+
+ return with_iframe(page_url)
+ .then(f => {
+ frame = f;
+ t.add_cleanup(() => { frame.remove(); });
+ const blob = new Blob(['it\'s me the blob', ' ', 'and more blob!']);
+ // Add "?ignore" so the service worker falls back to echo-content.py.
+ const echo_url = '/fetch/api/resources/echo-content.py?ignore';
+ return frame.contentWindow.fetch(echo_url, {
+ method: 'POST',
+ body: blob
+ });
+ })
+ .then(response => {
+ return response.text();
+ })
+ .then(response_text => {
+ assert_equals(
+ response_text,
+ 'it\'s me the blob and more blob!',
+ 'the network fallback request should include the request body');
+ });
+ }, 'FetchEvent#body is a blob and is passed to network fallback');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?keepalive';
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent, 'false');
+ const response = await frame.contentWindow.fetch(page_url, {keepalive: true});
+ const text = await response.text();
+ assert_equals(text, 'true');
+ }, 'Service Worker responds to fetch event with the correct keepalive value');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isReloadNavigation';
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.location.reload();
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = true');
+ }, 'FetchEvent#request.isReloadNavigation is true (location.reload())');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isReloadNavigation';
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(0);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = true');
+ }, 'FetchEvent#request.isReloadNavigation is true (history.go(0))');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isReloadNavigation';
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ const form = frame.contentDocument.createElement('form');
+ form.method = 'POST';
+ form.name = 'form';
+ form.action = new Request(page_url).url;
+ frame.contentDocument.body.appendChild(form);
+ form.submit();
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = POST, isReloadNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.location.reload();
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = POST, isReloadNavigation = true');
+ }, 'FetchEvent#request.isReloadNavigation is true (POST + location.reload())');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isReloadNavigation';
+ const anotherUrl = new Request('resources/simple.html').url;
+ let frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = false');
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = anotherUrl;
+ });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(0);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isReloadNavigation = true');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(1);
+ });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ }, 'FetchEvent#request.isReloadNavigation is true (with history traversal)');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isHistoryNavigation';
+ const anotherUrl = new Request('resources/simple.html?ignore').url;
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = anotherUrl;
+ });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = true');
+ }, 'FetchEvent#request.isHistoryNavigation is true (with history.go(-1))');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isHistoryNavigation';
+ const anotherUrl = new Request('resources/simple.html?ignore').url;
+ const frame = await with_iframe(anotherUrl);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = page_url;
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-1);
+ });
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = true');
+ }, 'FetchEvent#request.isHistoryNavigation is true (with history.go(1))');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isHistoryNavigation';
+ const anotherUrl = new Request('resources/simple.html?ignore').url;
+ const frame = await with_iframe(anotherUrl);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = page_url;
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-1);
+ });
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = true');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(0);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ }, 'FetchEvent#request.isHistoryNavigation is false (with history.go(0))');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isHistoryNavigation';
+ const anotherUrl = new Request('resources/simple.html?ignore').url;
+ const frame = await with_iframe(anotherUrl);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = page_url;
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-1);
+ });
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = true');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.location.reload();
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ }, 'FetchEvent#request.isHistoryNavigation is false (with location.reload)');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isHistoryNavigation';
+ const anotherUrl = new Request('resources/simple.html?ignore').url;
+ const oneAnotherUrl = new Request('resources/simple.html?ignore2').url;
+
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = anotherUrl;
+ });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = oneAnotherUrl;
+ });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-2);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = true');
+ }, 'FetchEvent#request.isHistoryNavigation is true (with history.go(-2))');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isHistoryNavigation';
+ const anotherUrl = new Request('resources/simple.html?ignore').url;
+ const oneAnotherUrl = new Request('resources/simple.html?ignore2').url;
+ const frame = await with_iframe(anotherUrl);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = oneAnotherUrl;
+ });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = page_url;
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-2);
+ });
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(2);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = true');
+ }, 'FetchEvent#request.isHistoryNavigation is true (with history.go(2))');
+
+promise_test(async (t) => {
+ const page_url = 'resources/simple.html?isHistoryNavigation';
+ const anotherUrl = new Request('resources/simple.html?ignore').url;
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = GET, isHistoryNavigation = false');
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ const form = frame.contentDocument.createElement('form');
+ form.method = 'POST';
+ form.name = 'form';
+ form.action = new Request(page_url).url;
+ frame.contentDocument.body.appendChild(form);
+ form.submit();
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = POST, isHistoryNavigation = false');
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.src = anotherUrl;
+ });
+ assert_equals(frame.contentDocument.body.textContent, "Here's a simple html file.\n");
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.addEventListener('load', resolve);
+ frame.contentWindow.history.go(-1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'method = POST, isHistoryNavigation = true');
+ }, 'FetchEvent#request.isHistoryNavigation is true (POST + history.go(-1))');
+
+// When service worker responds with a Response, no XHR upload progress
+// events are delivered.
+promise_test(async t => {
+ const page_url = 'resources/simple.html?ignore-for-request-body-string';
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+
+ const xhr = new frame.contentWindow.XMLHttpRequest();
+ xhr.open('POST', 'simple.html?request-body');
+ xhr.upload.addEventListener('progress', t.unreached_func('progress'));
+ xhr.upload.addEventListener('error', t.unreached_func('error'));
+ xhr.upload.addEventListener('abort', t.unreached_func('abort'));
+ xhr.upload.addEventListener('timeout', t.unreached_func('timeout'));
+ xhr.upload.addEventListener('load', t.unreached_func('load'));
+ xhr.upload.addEventListener('loadend', t.unreached_func('loadend'));
+ xhr.send('i am the request body');
+
+ await new Promise((resolve) => xhr.addEventListener('load', resolve));
+ }, 'XHR upload progress events for response coming from SW');
+
+// Upload progress events should be delivered for the network fallback case.
+promise_test(async t => {
+ const page_url = 'resources/simple.html?ignore-for-request-body-string';
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+
+ let progress = false;
+ let load = false;
+ let loadend = false;
+
+ const xhr = new frame.contentWindow.XMLHttpRequest();
+ xhr.open('POST', '/fetch/api/resources/echo-content.py?ignore');
+ xhr.upload.addEventListener('progress', () => progress = true);
+ xhr.upload.addEventListener('error', t.unreached_func('error'));
+ xhr.upload.addEventListener('abort', t.unreached_func('abort'));
+ xhr.upload.addEventListener('timeout', t.unreached_func('timeout'));
+ xhr.upload.addEventListener('load', () => load = true);
+ xhr.upload.addEventListener('loadend', () => loadend = true);
+ xhr.send('i am the request body');
+
+ await new Promise((resolve) => xhr.addEventListener('load', resolve));
+ assert_true(progress, 'progress');
+ assert_true(load, 'load');
+ assert_true(loadend, 'loadend');
+ }, 'XHR upload progress events for network fallback');
+
+promise_test(async t => {
+ // Set page_url to "?ignore" so the service worker falls back to network
+ // for the main resource request, and add a suffix to avoid colliding
+ // with other tests.
+ const page_url = 'resources/?ignore-for-request-body-fallback-string';
+
+ const frame = await with_iframe(page_url);
+ t.add_cleanup(() => { frame.remove(); });
+ // Add "?clone-and-ignore" so the service worker falls back to
+ // echo-content.py.
+ const echo_url = '/fetch/api/resources/echo-content.py?status=421';
+ const response = await frame.contentWindow.fetch(echo_url, {
+ method: 'POST',
+ body: 'text body'
+ });
+ assert_equals(response.status, 421);
+ const text = await response.text();
+ assert_equals(
+ text,
+ 'text body. Request was sent 1 times.',
+ 'the network fallback request should include the request body');
+ }, 'Fetch with POST with text on sw 421 response should not be retried.');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-frame-resource.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-frame-resource.https.html
new file mode 100644
index 0000000..a33309f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-frame-resource.https.html
@@ -0,0 +1,236 @@
+<!DOCTYPE html>
+<title>Service Worker: Fetch for the frame loading.</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var worker = 'resources/fetch-rewrite-worker.js';
+var path = base_path() + 'resources/fetch-access-control.py';
+var host_info = get_host_info();
+
+function getLoadedObject(win, contentFunc, closeFunc) {
+ return new Promise(function(resolve) {
+ function done(contentString) {
+ var result = null;
+ // fetch-access-control.py returns a string like "report( <json> )".
+ // Eval the returned string with a report functionto get the json
+ // object.
+ try {
+ function report(obj) { result = obj };
+ eval(contentString);
+ } catch(e) {
+ // just resolve null if we get unexpected page content
+ }
+ closeFunc(win);
+ resolve(result);
+ }
+
+ // We can't catch the network error on window. So we use the timer.
+ var timeout = setTimeout(function() {
+ // Failure pages are considered cross-origin in some browsers. This
+ // means you cannot even .resolve() the window because the check for
+ // the .then property will throw. Instead, treat cross-origin
+ // failure pages as the empty string which will fail to parse as the
+ // expected json result.
+ var content = '';
+ try {
+ content = contentFunc(win);
+ } catch(e) {
+ // use default empty string for cross-domain window
+ }
+ done(content);
+ }, 10000);
+
+ win.onload = function() {
+ clearTimeout(timeout);
+ let content = '';
+ try {
+ content = contentFunc(win);
+ } catch(e) {
+ // use default empty string for cross-domain window (see above)
+ }
+ done(content);
+ };
+ });
+}
+
+function getLoadedFrameAsObject(frame) {
+ return getLoadedObject(frame, function(f) {
+ return f.contentDocument.body.textContent;
+ }, function(f) {
+ f.parentNode.removeChild(f);
+ });
+}
+
+function getLoadedWindowAsObject(win) {
+ return getLoadedObject(win, function(w) {
+ return w.document.body.textContent
+ }, function(w) {
+ w.close();
+ });
+}
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-frame-resource/frame-basic';
+ var frame;
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ frame = document.createElement('iframe');
+ frame.src =
+ scope + '?url=' +
+ encodeURIComponent(host_info['HTTPS_ORIGIN'] + path);
+ document.body.appendChild(frame);
+ return getLoadedFrameAsObject(frame);
+ })
+ .then(function(result) {
+ assert_equals(
+ result.jsonpResult,
+ 'success',
+ 'Basic type response could be loaded in the iframe.');
+ frame.remove();
+ });
+ }, 'Basic type response could be loaded in the iframe.');
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-frame-resource/frame-cors';
+ var frame;
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ frame = document.createElement('iframe');
+ frame.src =
+ scope + '?mode=cors&url=' +
+ encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + path +
+ '?ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+ '&ACACredentials=true');
+ document.body.appendChild(frame);
+ return getLoadedFrameAsObject(frame);
+ })
+ .then(function(result) {
+ assert_equals(
+ result.jsonpResult,
+ 'success',
+ 'CORS type response could be loaded in the iframe.');
+ frame.remove();
+ });
+ }, 'CORS type response could be loaded in the iframe.');
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-frame-resource/frame-opaque';
+ var frame;
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ frame = document.createElement('iframe');
+ frame.src =
+ scope + '?mode=no-cors&url=' +
+ encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + path);
+ document.body.appendChild(frame);
+ return getLoadedFrameAsObject(frame);
+ })
+ .then(function(result) {
+ assert_equals(
+ result,
+ null,
+ 'Opaque type response could not be loaded in the iframe.');
+ frame.remove();
+ });
+ }, 'Opaque type response could not be loaded in the iframe.');
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-frame-resource/window-basic';
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ var win = window.open(
+ scope + '?url=' +
+ encodeURIComponent(host_info['HTTPS_ORIGIN'] + path));
+ return getLoadedWindowAsObject(win);
+ })
+ .then(function(result) {
+ assert_equals(
+ result.jsonpResult,
+ 'success',
+ 'Basic type response could be loaded in the new window.');
+ });
+ }, 'Basic type response could be loaded in the new window.');
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-frame-resource/window-cors';
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ var win = window.open(
+ scope + '?mode=cors&url=' +
+ encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + path +
+ '?ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+ '&ACACredentials=true'));
+ return getLoadedWindowAsObject(win);
+ })
+ .then(function(result) {
+ assert_equals(
+ result.jsonpResult,
+ 'success',
+ 'CORS type response could be loaded in the new window.');
+ });
+ }, 'CORS type response could be loaded in the new window.');
+
+promise_test(function(t) {
+ var scope = 'resources/fetch-frame-resource/window-opaque';
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ var win = window.open(
+ scope + '?mode=no-cors&url=' +
+ encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + path));
+ return getLoadedWindowAsObject(win);
+ })
+ .then(function(result) {
+ assert_equals(
+ result,
+ null,
+ 'Opaque type response could not be loaded in the new window.');
+ });
+ }, 'Opaque type response could not be loaded in the new window.');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-header-visibility.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-header-visibility.https.html
new file mode 100644
index 0000000..1f4813c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-header-visibility.https.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<title>Service Worker: Visibility of headers during fetch.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+ var worker = 'resources/fetch-rewrite-worker.js';
+ var path = base_path() + 'resources/fetch-access-control.py';
+ var host_info = get_host_info();
+ var frame;
+
+ promise_test(function(t) {
+ var scope = 'resources/fetch-header-visibility-iframe.html';
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ frame = document.createElement('iframe');
+ frame.src = scope;
+ document.body.appendChild(frame);
+
+ // Resolve a promise when we recieve 2 success messages
+ return new Promise(function(resolve, reject) {
+ var remaining = 4;
+ function onMessage(e) {
+ if (e.data == 'PASS') {
+ remaining--;
+ if (remaining == 0) {
+ resolve();
+ } else {
+ return;
+ }
+ } else {
+ reject(e.data);
+ }
+
+ window.removeEventListener('message', onMessage);
+ }
+ window.addEventListener('message', onMessage);
+ });
+ })
+ .then(function(result) {
+ frame.remove();
+ });
+ }, 'Visibility of defaulted headers during interception');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-mixed-content-to-inscope.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-mixed-content-to-inscope.https.html
new file mode 100644
index 0000000..0e8fa93
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-mixed-content-to-inscope.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<title>Service Worker: Mixed content of fetch()</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body></body>
+<script>
+async_test(function(t) {
+ var host_info = get_host_info();
+ window.addEventListener('message', t.step_func(on_message), false);
+ with_iframe(
+ host_info['HTTPS_ORIGIN'] + base_path() +
+ 'resources/fetch-mixed-content-iframe.html?target=inscope');
+ function on_message(e) {
+ assert_equals(e.data.results, 'finish');
+ t.done();
+ }
+ }, 'Verify Mixed content of fetch() in a Service Worker');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-mixed-content-to-outscope.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-mixed-content-to-outscope.https.html
new file mode 100644
index 0000000..391dc5d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-mixed-content-to-outscope.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<title>Service Worker: Mixed content of fetch()</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body></body>
+<script>
+async_test(function(t) {
+ var host_info = get_host_info();
+ window.addEventListener('message', t.step_func(on_message), false);
+ with_iframe(
+ host_info['HTTPS_ORIGIN'] + base_path() +
+ 'resources/fetch-mixed-content-iframe.html?target=outscope');
+ function on_message(e) {
+ assert_equals(e.data.results, 'finish');
+ t.done();
+ }
+ }, 'Verify Mixed content of fetch() in a Service Worker');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-request-css-base-url.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-css-base-url.https.html
new file mode 100644
index 0000000..467a66c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-css-base-url.https.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<title>Service Worker: CSS's base URL must be the response URL</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+const SCOPE = 'resources/fetch-request-css-base-url-iframe.html';
+const SCRIPT = 'resources/fetch-request-css-base-url-worker.js';
+let worker;
+
+var signalMessage;
+function getNextMessage() {
+ return new Promise(resolve => { signalMessage = resolve; });
+}
+
+promise_test(async (t) => {
+ const registration = await service_worker_unregister_and_register(
+ t, SCRIPT, SCOPE);
+ worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+}, 'global setup');
+
+// Creates a test concerning the base URL of a stylesheet. It loads a
+// stylesheet from a controlled page. The stylesheet makes a subresource
+// request for an image. The service worker messages back the details of the
+// image request in order to test the base URL.
+//
+// The request URL for the stylesheet is under "resources/request-url-path/".
+// The service worker may respond in a way such that the response URL is
+// different to the request URL.
+function base_url_test(params) {
+ promise_test(async (t) => {
+ let frame;
+ t.add_cleanup(() => {
+ if (frame)
+ frame.remove();
+ });
+
+ // Ask the service worker to message this page once it gets the request
+ // for the image.
+ let channel = new MessageChannel();
+ const sawPong = getNextMessage();
+ channel.port1.onmessage = (event) => {
+ signalMessage(event.data);
+ };
+ worker.postMessage({port:channel.port2},[channel.port2]);
+
+ // It sends a pong back immediately. This ping/pong protocol helps deflake
+ // the test for browsers where message/fetch ordering isn't guaranteed.
+ assert_equals('pong', await sawPong);
+
+ // Load the frame which will load the stylesheet that makes the image
+ // request.
+ const sawResult = getNextMessage();
+ frame = await with_iframe(params.framePath);
+ const result = await sawResult;
+
+ // Test the image request.
+ const base = new URL('.', document.location).href;
+ assert_equals(result.url,
+ base + params.expectImageRequestPath,
+ 'request');
+ assert_equals(result.referrer,
+ base + params.expectImageRequestReferrer,
+ 'referrer');
+ }, params.description);
+}
+
+const cssFile = 'fetch-request-css-base-url-style.css';
+
+base_url_test({
+ framePath: SCOPE + '?fetch',
+ expectImageRequestPath: 'resources/sample.png',
+ expectImageRequestReferrer: `resources/${cssFile}?fetch`,
+ description: 'base URL when service worker does respondWith(fetch(responseUrl)).'});
+
+base_url_test({
+ framePath: SCOPE + '?newResponse',
+ expectImageRequestPath: 'resources/request-url-path/sample.png',
+ expectImageRequestReferrer: `resources/request-url-path/${cssFile}?newResponse`,
+ description: 'base URL when service worker does respondWith(new Response()).'});
+
+// Cleanup step: this must be the last promise_test.
+promise_test(async (t) => {
+ return service_worker_unregister(t, SCOPE);
+}, 'cleanup global state');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-request-css-cross-origin.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-css-cross-origin.https.html
new file mode 100644
index 0000000..d9c1c7f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-css-cross-origin.https.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<title>Service Worker: Cross-origin CSS files fetched via SW.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+function getElementColorInFrame(frame, id) {
+ var element = frame.contentDocument.getElementById(id);
+ var style = frame.contentWindow.getComputedStyle(element, '');
+ return style['color'];
+}
+
+promise_test(async t => {
+ var SCOPE =
+ 'resources/fetch-request-css-cross-origin';
+ var SCRIPT =
+ 'resources/fetch-request-css-cross-origin-worker.js';
+ let registration = await service_worker_unregister_and_register(
+ t, SCRIPT, SCOPE);
+ promise_test(async t => {
+ await registration.unregister();
+ }, 'cleanup global state');
+
+ await wait_for_state(t, registration.installing, 'activated');
+}, 'setup global state');
+
+promise_test(async t => {
+ const EXPECTED_COLOR = 'rgb(0, 0, 255)';
+ const PAGE =
+ 'resources/fetch-request-css-cross-origin-mime-check-iframe.html';
+
+ const f = await with_iframe(PAGE);
+ t.add_cleanup(() => {f.remove(); });
+ assert_equals(
+ getElementColorInFrame(f, 'crossOriginCss'),
+ EXPECTED_COLOR,
+ 'The color must be overridden by cross origin CSS.');
+ assert_equals(
+ getElementColorInFrame(f, 'crossOriginHtml'),
+ EXPECTED_COLOR,
+ 'The color must not be overridden by cross origin non CSS file.');
+ assert_equals(
+ getElementColorInFrame(f, 'sameOriginCss'),
+ EXPECTED_COLOR,
+ 'The color must be overridden by same origin CSS.');
+ assert_equals(
+ getElementColorInFrame(f, 'sameOriginHtml'),
+ EXPECTED_COLOR,
+ 'The color must be overridden by same origin non CSS file.');
+ assert_equals(
+ getElementColorInFrame(f, 'synthetic'),
+ EXPECTED_COLOR,
+ 'The color must be overridden by synthetic CSS.');
+}, 'MIME checking of CSS resources fetched via service worker when Content-Type is not set.');
+
+promise_test(async t => {
+ const PAGE =
+ 'resources/fetch-request-css-cross-origin-read-contents.html';
+
+ const f = await with_iframe(PAGE);
+ t.add_cleanup(() => {f.remove(); });
+ assert_throws_dom('SecurityError', f.contentWindow.DOMException, () => {
+ f.contentDocument.styleSheets[0].cssRules[0].cssText;
+ });
+ assert_equals(
+ f.contentDocument.styleSheets[1].cssRules[0].cssText,
+ '#crossOriginCss { color: blue; }',
+ 'cross-origin CORS approved response');
+ assert_equals(
+ f.contentDocument.styleSheets[2].cssRules[0].cssText,
+ '#sameOriginCss { color: blue; }',
+ 'same-origin response');
+ assert_equals(
+ f.contentDocument.styleSheets[3].cssRules[0].cssText,
+ '#synthetic { color: blue; }',
+ 'service worker generated response');
+ }, 'Same-origin policy for access to CSS resources fetched via service worker');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-request-css-images.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-css-images.https.html
new file mode 100644
index 0000000..586dea2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-css-images.https.html
@@ -0,0 +1,214 @@
+<!DOCTYPE html>
+<title>Service Worker: FetchEvent for css image</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+var SCOPE = 'resources/fetch-request-resources-iframe.https.html';
+var SCRIPT = 'resources/fetch-request-resources-worker.js';
+var host_info = get_host_info();
+var LOCAL_URL =
+ host_info['HTTPS_ORIGIN'] + base_path() + 'resources/sample?test';
+var REMOTE_URL =
+ host_info['HTTPS_REMOTE_ORIGIN'] + base_path() + 'resources/sample?test';
+
+function css_image_test(expected_results, frame, url, type,
+ expected_mode, expected_credentials) {
+ expected_results[url] = {
+ url: url,
+ mode: expected_mode,
+ credentials: expected_credentials,
+ message: 'CSSImage load (url:' + url + ' type:' + type + ')'
+ };
+ return frame.contentWindow.load_css_image(url, type);
+}
+
+function css_image_set_test(expected_results, frame, url, type,
+ expected_mode, expected_credentials) {
+ expected_results[url] = {
+ url: url,
+ mode: expected_mode,
+ credentials: expected_credentials,
+ message: 'CSSImageSet load (url:' + url + ' type:' + type + ')'
+ };
+ return frame.contentWindow.load_css_image_set(url, type);
+}
+
+function waitForWorker(worker) {
+ return new Promise(function(resolve) {
+ var channel = new MessageChannel();
+ channel.port1.addEventListener('message', function(msg) {
+ if (msg.data.ready) {
+ resolve(channel);
+ }
+ });
+ channel.port1.start();
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ });
+}
+
+function create_message_promise(channel, expected_results, worker, scope) {
+ return new Promise(function(resolve) {
+ channel.port1.addEventListener('message', function(msg) {
+ var result = msg.data;
+ if (!expected_results[result.url]) {
+ return;
+ }
+ resolve(result);
+ });
+ }).then(function(result) {
+ var expected = expected_results[result.url];
+ assert_equals(
+ result.mode, expected.mode,
+ 'mode of ' + expected.message + ' must be ' +
+ expected.mode + '.');
+ assert_equals(
+ result.credentials, expected.credentials,
+ 'credentials of ' + expected.message + ' must be ' +
+ expected.credentials + '.');
+ delete expected_results[result.url];
+ });
+}
+
+promise_test(function(t) {
+ var scope = SCOPE + "?img=backgroundImage";
+ var expected_results = {};
+ var worker;
+ var frame;
+ return service_worker_unregister_and_register(t, SCRIPT, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ t.add_cleanup(function() {
+ f.remove();
+ });
+ frame = f;
+ return waitForWorker(worker);
+ })
+ .then(function(channel) {
+ css_image_test(expected_results, frame, LOCAL_URL + Date.now(),
+ 'backgroundImage', 'no-cors', 'include');
+ css_image_test(expected_results, frame, REMOTE_URL + Date.now(),
+ 'backgroundImage', 'no-cors', 'include');
+
+ return Promise.all([
+ create_message_promise(channel, expected_results, worker, scope),
+ create_message_promise(channel, expected_results, worker, scope)
+ ]);
+ });
+ }, 'Verify FetchEvent for css image (backgroundImage).');
+
+promise_test(function(t) {
+ var scope = SCOPE + "?img=shapeOutside";
+ var expected_results = {};
+ var worker;
+ var frame;
+ return service_worker_unregister_and_register(t, SCRIPT, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ t.add_cleanup(function() {
+ f.remove();
+ });
+ frame = f;
+ return waitForWorker(worker);
+ })
+ .then(function(channel) {
+ css_image_test(expected_results, frame, LOCAL_URL + Date.now(),
+ 'shapeOutside', 'cors', 'same-origin');
+ css_image_test(expected_results, frame, REMOTE_URL + Date.now(),
+ 'shapeOutside', 'cors', 'same-origin');
+
+ return Promise.all([
+ create_message_promise(channel, expected_results, worker, scope),
+ create_message_promise(channel, expected_results, worker, scope)
+ ]);
+ });
+ }, 'Verify FetchEvent for css image (shapeOutside).');
+
+promise_test(function(t) {
+ var scope = SCOPE + "?img_set=backgroundImage";
+ var expected_results = {};
+ var worker;
+ var frame;
+ return service_worker_unregister_and_register(t, SCRIPT, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ t.add_cleanup(function() {
+ f.remove();;
+ });
+ frame = f;
+ return waitForWorker(worker);
+ })
+ .then(function(channel) {
+ css_image_set_test(expected_results, frame, LOCAL_URL + Date.now(),
+ 'backgroundImage', 'no-cors', 'include');
+ css_image_set_test(expected_results, frame, REMOTE_URL + Date.now(),
+ 'backgroundImage', 'no-cors', 'include');
+
+ return Promise.all([
+ create_message_promise(channel, expected_results, worker, scope),
+ create_message_promise(channel, expected_results, worker, scope)
+ ]);
+ });
+ }, 'Verify FetchEvent for css image-set (backgroundImage).');
+
+promise_test(function(t) {
+ var scope = SCOPE + "?img_set=shapeOutside";
+ var expected_results = {};
+ var worker;
+ var frame;
+ return service_worker_unregister_and_register(t, SCRIPT, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ t.add_cleanup(function() {
+ f.remove();
+ });
+ frame = f;
+ return waitForWorker(worker);
+ })
+ .then(function(channel) {
+ css_image_set_test(expected_results, frame, LOCAL_URL + Date.now(),
+ 'shapeOutside', 'cors', 'same-origin');
+ css_image_set_test(expected_results, frame, REMOTE_URL + Date.now(),
+ 'shapeOutside', 'cors', 'same-origin');
+
+ return Promise.all([
+ create_message_promise(channel, expected_results, worker, scope),
+ create_message_promise(channel, expected_results, worker, scope)
+ ]);
+ });
+ }, 'Verify FetchEvent for css image-set (shapeOutside).');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-request-fallback.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-fallback.https.html
new file mode 100644
index 0000000..a29f31d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-fallback.https.html
@@ -0,0 +1,282 @@
+<!DOCTYPE html>
+<title>Service Worker: the fallback behavior of FetchEvent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function get_fetched_urls(worker) {
+ return new Promise(function(resolve) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = function(msg) { resolve(msg); };
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ });
+}
+
+function check_urls(worker, expected_requests) {
+ return get_fetched_urls(worker)
+ .then(function(msg) {
+ var requests = msg.data.requests;
+ assert_object_equals(requests, expected_requests);
+ });
+}
+
+var path = new URL(".", window.location).pathname;
+var SCOPE = 'resources/fetch-request-fallback-iframe.html';
+var SCRIPT = 'resources/fetch-request-fallback-worker.js';
+var host_info = get_host_info();
+var BASE_URL = host_info['HTTPS_ORIGIN'] +
+ path + 'resources/fetch-access-control.py?';
+var BASE_PNG_URL = BASE_URL + 'PNGIMAGE&';
+var OTHER_BASE_URL = host_info['HTTPS_REMOTE_ORIGIN'] +
+ path + 'resources/fetch-access-control.py?';
+var OTHER_BASE_PNG_URL = OTHER_BASE_URL + 'PNGIMAGE&';
+var REDIRECT_URL = host_info['HTTPS_ORIGIN'] +
+ path + 'resources/redirect.py?Redirect=';
+var register;
+
+promise_test(function(t) {
+ var registration;
+ var worker;
+
+ register = service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(r) {
+ registration = r;
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ // This test should not be considered complete until after the service
+ // worker has been unregistered. Currently, `testharness.js` does not
+ // support asynchronous global "tear down" logic, so this must be
+ // expressed using a dedicated `promise_test`. Because the other
+ // sub-tests in this file are declared synchronously, this test will be
+ // the final test executed.
+ promise_test(function(t) {
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+ return registration.unregister();
+ }, 'restore global state');
+
+ return {frame: frame, worker: worker};
+ });
+
+ return register;
+ }, 'initialize global state');
+
+function promise_frame_test(body, desc) {
+ promise_test(function(test) {
+ return register.then(function(result) {
+ return body(test, result.frame, result.worker);
+ });
+ }, desc);
+}
+
+promise_frame_test(function(t, frame, worker) {
+ return check_urls(
+ worker,
+ [{
+ url: host_info['HTTPS_ORIGIN'] + path + SCOPE,
+ mode: 'navigate'
+ }]);
+ }, 'The SW must intercept the request for a main resource.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.xhr(BASE_URL)
+ .then(function() {
+ return check_urls(
+ worker,
+ [{ url: BASE_URL, mode: 'cors' }]);
+ });
+ }, 'The SW must intercept the request of same origin XHR.');
+
+promise_frame_test(function(t, frame, worker) {
+ return promise_rejects_js(
+ t,
+ frame.contentWindow.Error,
+ frame.contentWindow.xhr(OTHER_BASE_URL),
+ 'SW fallbacked CORS-unsupported other origin XHR should fail.')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{ url: OTHER_BASE_URL, mode: 'cors' }]);
+ });
+ }, 'The SW must intercept the request of CORS-unsupported other origin XHR.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.xhr(OTHER_BASE_URL + 'ACAOrigin=*')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{ url: OTHER_BASE_URL + 'ACAOrigin=*', mode: 'cors' }]);
+ })
+ }, 'The SW must intercept the request of CORS-supported other origin XHR.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.xhr(
+ REDIRECT_URL + encodeURIComponent(BASE_URL))
+ .then(function() {
+ return check_urls(
+ worker,
+ [{
+ url: REDIRECT_URL + encodeURIComponent(BASE_URL),
+ mode: 'cors'
+ }]);
+ });
+ }, 'The SW must intercept only the first request of redirected XHR.');
+
+promise_frame_test(function(t, frame, worker) {
+ return promise_rejects_js(
+ t,
+ frame.contentWindow.Error,
+ frame.contentWindow.xhr(
+ REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL)),
+ 'SW fallbacked XHR which is redirected to CORS-unsupported ' +
+ 'other origin should fail.')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{
+ url: REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL),
+ mode: 'cors'
+ }]);
+ });
+ }, 'The SW must intercept only the first request for XHR which is' +
+ ' redirected to CORS-unsupported other origin.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.xhr(
+ REDIRECT_URL +
+ encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'))
+ .then(function() {
+ return check_urls(
+ worker,
+ [{
+ url: REDIRECT_URL +
+ encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'),
+ mode: 'cors'
+ }]);
+ });
+ }, 'The SW must intercept only the first request for XHR which is ' +
+ 'redirected to CORS-supported other origin.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.load_image(BASE_PNG_URL, '')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{ url: BASE_PNG_URL, mode: 'no-cors' }]);
+ });
+ }, 'The SW must intercept the request for image.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.load_image(OTHER_BASE_PNG_URL, '')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{ url: OTHER_BASE_PNG_URL, mode: 'no-cors' }]);
+ });
+ }, 'The SW must intercept the request for other origin image.');
+
+promise_frame_test(function(t, frame, worker) {
+ return promise_rejects_js(
+ t,
+ frame.contentWindow.Error,
+ frame.contentWindow.load_image(OTHER_BASE_PNG_URL, 'anonymous'),
+ 'SW fallbacked CORS-unsupported other origin image request ' +
+ 'should fail.')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{ url: OTHER_BASE_PNG_URL, mode: 'cors' }]);
+ })
+ }, 'The SW must intercept the request for CORS-unsupported other ' +
+ 'origin image.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.load_image(
+ OTHER_BASE_PNG_URL + 'ACAOrigin=*', 'anonymous')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{ url: OTHER_BASE_PNG_URL + 'ACAOrigin=*', mode: 'cors' }]);
+ });
+ }, 'The SW must intercept the request for CORS-supported other ' +
+ 'origin image.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.load_image(
+ REDIRECT_URL + encodeURIComponent(BASE_PNG_URL), '')
+ .catch(function() {
+ assert_unreached(
+ 'SW fallbacked redirected image request should succeed.');
+ })
+ .then(function() {
+ return check_urls(
+ worker,
+ [{
+ url: REDIRECT_URL + encodeURIComponent(BASE_PNG_URL),
+ mode: 'no-cors'
+ }]);
+ });
+ }, 'The SW must intercept only the first request for redirected ' +
+ 'image resource.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.load_image(
+ REDIRECT_URL + encodeURIComponent(OTHER_BASE_PNG_URL), '')
+ .catch(function() {
+ assert_unreached(
+ 'SW fallbacked image request which is redirected to ' +
+ 'other origin should succeed.');
+ })
+ .then(function() {
+ return check_urls(
+ worker,
+ [{
+ url: REDIRECT_URL + encodeURIComponent(OTHER_BASE_PNG_URL),
+ mode: 'no-cors'
+ }]);
+ })
+ }, 'The SW must intercept only the first request for image ' +
+ 'resource which is redirected to other origin.');
+
+promise_frame_test(function(t, frame, worker) {
+ return promise_rejects_js(
+ t,
+ frame.contentWindow.Error,
+ frame.contentWindow.load_image(
+ REDIRECT_URL + encodeURIComponent(OTHER_BASE_PNG_URL),
+ 'anonymous'),
+ 'SW fallbacked image request which is redirected to ' +
+ 'CORS-unsupported other origin should fail.')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{
+ url: REDIRECT_URL + encodeURIComponent(OTHER_BASE_PNG_URL),
+ mode: 'cors'
+ }]);
+ });
+ }, 'The SW must intercept only the first request for image ' +
+ 'resource which is redirected to CORS-unsupported other origin.');
+
+promise_frame_test(function(t, frame, worker) {
+ return frame.contentWindow.load_image(
+ REDIRECT_URL +
+ encodeURIComponent(OTHER_BASE_PNG_URL + 'ACAOrigin=*'),
+ 'anonymous')
+ .then(function() {
+ return check_urls(
+ worker,
+ [{
+ url: REDIRECT_URL +
+ encodeURIComponent(OTHER_BASE_PNG_URL + 'ACAOrigin=*'),
+ mode: 'cors'
+ }]);
+ });
+ }, 'The SW must intercept only the first request for image ' +
+ 'resource which is redirected to CORS-supported other origin.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-request-no-freshness-headers.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-no-freshness-headers.https.html
new file mode 100644
index 0000000..03b7d35
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-no-freshness-headers.https.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<title>Service Worker: the headers of FetchEvent shouldn't contain freshness headers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+promise_test(function(t) {
+ var SCOPE = 'resources/fetch-request-no-freshness-headers-iframe.html';
+ var SCRIPT = 'resources/fetch-request-no-freshness-headers-worker.js';
+ var worker;
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ return new Promise(function(resolve) {
+ frame.onload = function() {
+ resolve(frame);
+ };
+ frame.contentWindow.location.reload();
+ });
+ })
+ .then(function(frame) {
+ return new Promise(function(resolve) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = t.step_func(function(msg) {
+ frame.remove();
+ resolve(msg);
+ });
+ worker.postMessage(
+ {port: channel.port2}, [channel.port2]);
+ });
+ })
+ .then(function(msg) {
+ var freshness_headers = {
+ 'if-none-match': true,
+ 'if-modified-since': true
+ };
+ msg.data.requests.forEach(function(request) {
+ request.headers.forEach(function(header) {
+ assert_false(
+ !!freshness_headers[header[0]],
+ header[0] + ' header must not be set in the ' +
+ 'FetchEvent\'s request. (url = ' + request.url + ')');
+ });
+ })
+ });
+ }, 'The headers of FetchEvent shouldn\'t contain freshness headers.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-request-redirect.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-redirect.https.html
new file mode 100644
index 0000000..5ce015b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-redirect.https.html
@@ -0,0 +1,385 @@
+<!DOCTYPE html>
+<title>Service Worker: FetchEvent for resources</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/media.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var test_scope = ""
+function assert_resolves(promise, description) {
+ return promise.then(
+ () => test(() => {}, description + " - " + test_scope),
+ (e) => test(() => { throw e; }, description + " - " + test_scope)
+ );
+}
+
+function assert_rejects(promise, description) {
+ return promise.then(
+ () => test(() => { assert_unreached(); }, description + " - " + test_scope),
+ () => test(() => {}, description + " - " + test_scope)
+ );
+}
+
+function iframe_test(url, timeout_enabled) {
+ return new Promise(function(resolve, reject) {
+ var frame = document.createElement('iframe');
+ frame.src = url;
+ if (timeout_enabled) {
+ // We can't catch the network error on iframe. So we use the timer for
+ // failure detection.
+ var timer = setTimeout(function() {
+ reject(new Error('iframe load timeout'));
+ frame.remove();
+ }, 10000);
+ }
+ frame.onload = function() {
+ if (timeout_enabled)
+ clearTimeout(timer);
+ try {
+ if (frame.contentDocument.body.textContent == 'Hello world\n')
+ resolve();
+ else
+ reject(new Error('content mismatch'));
+ } catch (e) {
+ // Chrome treats iframes that failed to load due to a network error as
+ // having a different origin, so accessing contentDocument throws an
+ // error. Other browsers might have different behavior.
+ reject(new Error(e));
+ }
+ frame.remove();
+ };
+ document.body.appendChild(frame);
+ });
+}
+
+promise_test(function(t) {
+ test_scope = "default";
+
+ var SCOPE = 'resources/fetch-request-redirect-iframe.html';
+ var SCRIPT = 'resources/fetch-rewrite-worker.js';
+ var REDIRECT_URL = base_path() + 'resources/redirect.py?Redirect=';
+ var IMAGE_URL = base_path() + 'resources/square.png';
+ var AUDIO_URL = getAudioURI("/media/sound_5");
+ var XHR_URL = base_path() + 'resources/simple.txt';
+ var HTML_URL = base_path() + 'resources/sample.html';
+
+ var REDIRECT_TO_IMAGE_URL = REDIRECT_URL + encodeURIComponent(IMAGE_URL);
+ var REDIRECT_TO_AUDIO_URL = REDIRECT_URL + encodeURIComponent(AUDIO_URL);
+ var REDIRECT_TO_XHR_URL = REDIRECT_URL + encodeURIComponent(XHR_URL);
+ var REDIRECT_TO_HTML_URL = REDIRECT_URL + encodeURIComponent(HTML_URL);
+
+ var worker;
+ var frame;
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(() => service_worker_unregister(t, SCOPE));
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(async function(f) {
+ frame = f;
+ // XMLHttpRequest tests.
+ await assert_resolves(frame.contentWindow.xhr(XHR_URL),
+ 'Normal XHR should succeed.');
+ await assert_resolves(frame.contentWindow.xhr(REDIRECT_TO_XHR_URL),
+ 'Redirected XHR should succeed.');
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&redirect-mode=follow'),
+ 'Redirected XHR with Request.redirect=follow should succeed.');
+ await assert_rejects(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&redirect-mode=error'),
+ 'Redirected XHR with Request.redirect=error should fail.');
+ await assert_rejects(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&redirect-mode=manual'),
+ 'Redirected XHR with Request.redirect=manual should fail.');
+
+ // Image loading tests.
+ await assert_resolves(frame.contentWindow.load_image(IMAGE_URL),
+ 'Normal image resource should be loaded.');
+ await assert_resolves(
+ frame.contentWindow.load_image(REDIRECT_TO_IMAGE_URL),
+ 'Redirected image resource should be loaded.');
+ await assert_resolves(
+ frame.contentWindow.load_image(
+ './?url=' + encodeURIComponent(REDIRECT_TO_IMAGE_URL) +
+ '&redirect-mode=follow'),
+ 'Loading redirected image with Request.redirect=follow should' +
+ ' succeed.');
+ await assert_rejects(
+ frame.contentWindow.load_image(
+ './?url=' + encodeURIComponent(REDIRECT_TO_IMAGE_URL) +
+ '&redirect-mode=error'),
+ 'Loading redirected image with Request.redirect=error should ' +
+ 'fail.');
+ await assert_rejects(
+ frame.contentWindow.load_image(
+ './?url=' + encodeURIComponent(REDIRECT_TO_IMAGE_URL) +
+ '&redirect-mode=manual'),
+ 'Loading redirected image with Request.redirect=manual should' +
+ ' fail.');
+
+ // Audio loading tests.
+ await assert_resolves(frame.contentWindow.load_audio(AUDIO_URL),
+ 'Normal audio resource should be loaded.');
+ await assert_resolves(
+ frame.contentWindow.load_audio(REDIRECT_TO_AUDIO_URL),
+ 'Redirected audio resource should be loaded.');
+ await assert_resolves(
+ frame.contentWindow.load_audio(
+ './?url=' + encodeURIComponent(REDIRECT_TO_AUDIO_URL) +
+ '&redirect-mode=follow'),
+ 'Loading redirected audio with Request.redirect=follow should' +
+ ' succeed.');
+ await assert_rejects(
+ frame.contentWindow.load_audio(
+ './?url=' + encodeURIComponent(REDIRECT_TO_AUDIO_URL) +
+ '&redirect-mode=error'),
+ 'Loading redirected audio with Request.redirect=error should ' +
+ 'fail.');
+ await assert_rejects(
+ frame.contentWindow.load_audio(
+ './?url=' + encodeURIComponent(REDIRECT_TO_AUDIO_URL) +
+ '&redirect-mode=manual'),
+ 'Loading redirected audio with Request.redirect=manual should' +
+ ' fail.');
+
+ // Iframe tests.
+ await assert_resolves(iframe_test(HTML_URL),
+ 'Normal iframe loading should succeed.');
+ await assert_resolves(
+ iframe_test(REDIRECT_TO_HTML_URL),
+ 'Normal redirected iframe loading should succeed.');
+ await assert_rejects(
+ iframe_test(SCOPE + '?url=' +
+ encodeURIComponent(REDIRECT_TO_HTML_URL) +
+ '&redirect-mode=follow',
+ true /* timeout_enabled */),
+ 'Redirected iframe loading with Request.redirect=follow should'+
+ ' fail.');
+ await assert_rejects(
+ iframe_test(SCOPE + '?url=' +
+ encodeURIComponent(REDIRECT_TO_HTML_URL) +
+ '&redirect-mode=error',
+ true /* timeout_enabled */),
+ 'Redirected iframe loading with Request.redirect=error should '+
+ 'fail.');
+ await assert_resolves(
+ iframe_test(SCOPE + '?url=' +
+ encodeURIComponent(REDIRECT_TO_HTML_URL) +
+ '&redirect-mode=manual',
+ true /* timeout_enabled */),
+ 'Redirected iframe loading with Request.redirect=manual should'+
+ ' succeed.');
+ })
+ .then(function() {
+ frame.remove();
+ });
+ }, 'Verify redirect mode of Fetch API and ServiceWorker FetchEvent.');
+
+// test for reponse.redirected
+promise_test(function(t) {
+ test_scope = "redirected";
+
+ var SCOPE = 'resources/fetch-request-redirect-iframe.html';
+ var SCRIPT = 'resources/fetch-rewrite-worker.js';
+ var REDIRECT_URL = base_path() + 'resources/redirect.py?Redirect=';
+ var XHR_URL = base_path() + 'resources/simple.txt';
+ var IMAGE_URL = base_path() + 'resources/square.png';
+
+ var REDIRECT_TO_XHR_URL = REDIRECT_URL + encodeURIComponent(XHR_URL);
+
+ var host_info = get_host_info();
+
+ var CROSS_ORIGIN_URL = host_info['HTTPS_REMOTE_ORIGIN'] + IMAGE_URL;
+
+ var REDIRECT_TO_CROSS_ORIGIN = REDIRECT_URL +
+ encodeURIComponent(CROSS_ORIGIN_URL + '?ACAOrigin=*');
+
+ var worker;
+ var frame;
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(() => service_worker_unregister(t, SCOPE));
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(async function(f) {
+ frame = f;
+ // XMLHttpRequest tests.
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(XHR_URL) +
+ '&expected_redirected=false' +
+ '&expected_resolves=true'),
+ 'Normal XHR should be resolved and response should not be ' +
+ 'redirected.');
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&expected_redirected=true' +
+ '&expected_resolves=true'),
+ 'Redirected XHR should be resolved and response should be ' +
+ 'redirected.');
+
+ // tests for request's mode = cors
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(XHR_URL) +
+ '&mode=cors' +
+ '&expected_redirected=false' +
+ '&expected_resolves=true'),
+ 'Normal XHR should be resolved and response should not be ' +
+ 'redirected even with CORS mode.');
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&mode=cors' +
+ '&redirect-mode=follow' +
+ '&expected_redirected=true' +
+ '&expected_resolves=true'),
+ 'Redirected XHR should be resolved and response.redirected ' +
+ 'should be redirected with CORS mode.');
+
+ // tests for request's mode = no-cors
+ // The response.redirect should be false since we will not add
+ // redirected url list when redirect-mode is not follow.
+ await assert_rejects(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&mode=no-cors' +
+ '&redirect-mode=manual' +
+ '&expected_redirected=false' +
+ '&expected_resolves=false'),
+ 'Redirected XHR should be reject and response should be ' +
+ 'redirected with NO-CORS mode and redirect-mode=manual.');
+
+ // tests for redirecting to a cors
+ await assert_resolves(
+ frame.contentWindow.load_image(
+ './?url=' + encodeURIComponent(REDIRECT_TO_CROSS_ORIGIN) +
+ '&mode=no-cors' +
+ '&redirect-mode=follow' +
+ '&expected_redirected=false' +
+ '&expected_resolves=true'),
+ 'Redirected CORS image should be reject and response should ' +
+ 'not be redirected with NO-CORS mode.');
+ })
+ .then(function() {
+ frame.remove();
+ });
+ }, 'Verify redirected of Response(Fetch API) and ServiceWorker FetchEvent.');
+
+// test for reponse.redirected after cached
+promise_test(function(t) {
+ test_scope = "cache";
+
+ var SCOPE = 'resources/fetch-request-redirect-iframe.html';
+ var SCRIPT = 'resources/fetch-rewrite-worker.js';
+ var REDIRECT_URL = base_path() + 'resources/redirect.py?Redirect=';
+ var XHR_URL = base_path() + 'resources/simple.txt';
+ var IMAGE_URL = base_path() + 'resources/square.png';
+
+ var REDIRECT_TO_XHR_URL = REDIRECT_URL + encodeURIComponent(XHR_URL);
+
+ var host_info = get_host_info();
+
+ var CROSS_ORIGIN_URL = host_info['HTTPS_REMOTE_ORIGIN'] + IMAGE_URL;
+
+ var REDIRECT_TO_CROSS_ORIGIN = REDIRECT_URL +
+ encodeURIComponent(CROSS_ORIGIN_URL + '?ACAOrigin=*');
+
+ var worker;
+ var frame;
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(() => service_worker_unregister(t, SCOPE));
+
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(async function(f) {
+ frame = f;
+ // XMLHttpRequest tests.
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(XHR_URL) +
+ '&expected_redirected=false' +
+ '&expected_resolves=true' +
+ '&cache'),
+ 'Normal XHR should be resolved and response should not be ' +
+ 'redirected.');
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&expected_redirected=true' +
+ '&expected_resolves=true' +
+ '&cache'),
+ 'Redirected XHR should be resolved and response should be ' +
+ 'redirected.');
+
+ // tests for request's mode = cors
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(XHR_URL) +
+ '&mode=cors' +
+ '&expected_redirected=false' +
+ '&expected_resolves=true' +
+ '&cache'),
+ 'Normal XHR should be resolved and response should not be ' +
+ 'redirected even with CORS mode.');
+ await assert_resolves(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&mode=cors' +
+ '&redirect-mode=follow' +
+ '&expected_redirected=true' +
+ '&expected_resolves=true' +
+ '&cache'),
+ 'Redirected XHR should be resolved and response.redirected ' +
+ 'should be redirected with CORS mode.');
+
+ // tests for request's mode = no-cors
+ // The response.redirect should be false since we will not add
+ // redirected url list when redirect-mode is not follow.
+ await assert_rejects(
+ frame.contentWindow.xhr(
+ './?url=' + encodeURIComponent(REDIRECT_TO_XHR_URL) +
+ '&mode=no-cors' +
+ '&redirect-mode=manual' +
+ '&expected_redirected=false' +
+ '&expected_resolves=false' +
+ '&cache'),
+ 'Redirected XHR should be reject and response should be ' +
+ 'redirected with NO-CORS mode and redirect-mode=manual.');
+
+ // tests for redirecting to a cors
+ await assert_resolves(
+ frame.contentWindow.load_image(
+ './?url=' + encodeURIComponent(REDIRECT_TO_CROSS_ORIGIN) +
+ '&mode=no-cors' +
+ '&redirect-mode=follow' +
+ '&expected_redirected=false' +
+ '&expected_resolves=true' +
+ '&cache'),
+ 'Redirected CORS image should be reject and response should ' +
+ 'not be redirected with NO-CORS mode.');
+ })
+ .then(function() {
+ frame.remove();
+ });
+ }, 'Verify redirected of Response(Fetch API), Cache API and ServiceWorker ' +
+ 'FetchEvent.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-request-resources.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-resources.https.html
new file mode 100644
index 0000000..b4680c3
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-resources.https.html
@@ -0,0 +1,302 @@
+<!DOCTYPE html>
+<title>Service Worker: FetchEvent for resources</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+let url_count = 0;
+const expected_results = {};
+
+function add_promise_to_test(url)
+{
+ const expected = expected_results[url];
+ return new Promise((resolve) => {
+ expected.resolve = resolve;
+ });
+}
+
+function image_test(frame, url, cross_origin, expected_mode,
+ expected_credentials) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ cross_origin: cross_origin,
+ mode: expected_mode,
+ credentials: expected_credentials,
+ redirect: 'follow',
+ integrity: '',
+ destination: 'image',
+ message: `Image load (url:${actual_url} cross_origin:${cross_origin})`
+ };
+ frame.contentWindow.load_image(actual_url, cross_origin);
+ return add_promise_to_test(actual_url);
+}
+
+function script_test(frame, url, cross_origin, expected_mode,
+ expected_credentials) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ cross_origin: cross_origin,
+ mode: expected_mode,
+ credentials: expected_credentials,
+ redirect: 'follow',
+ integrity: '',
+ destination: 'script',
+ message: `Script load (url:${actual_url} cross_origin:${cross_origin})`
+ };
+ frame.contentWindow.load_script(actual_url, cross_origin);
+ return add_promise_to_test(actual_url);
+}
+
+function css_test(frame, url, cross_origin, expected_mode,
+ expected_credentials) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ cross_origin: cross_origin,
+ mode: expected_mode,
+ credentials: expected_credentials,
+ redirect: 'follow',
+ integrity: '',
+ destination: 'style',
+ message: `CSS load (url:${actual_url} cross_origin:${cross_origin})`
+ };
+ frame.contentWindow.load_css(actual_url, cross_origin);
+ return add_promise_to_test(actual_url);
+}
+
+function font_face_test(frame, url, expected_mode, expected_credentials) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ url: actual_url,
+ mode: expected_mode,
+ credentials: expected_credentials,
+ redirect: 'follow',
+ integrity: '',
+ destination: 'font',
+ message: `FontFace load (url: ${actual_url})`
+ };
+ frame.contentWindow.load_font(actual_url);
+ return add_promise_to_test(actual_url);
+}
+
+function script_integrity_test(frame, url, integrity, expected_integrity) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ url: actual_url,
+ mode: 'no-cors',
+ credentials: 'include',
+ redirect: 'follow',
+ integrity: expected_integrity,
+ destination: 'script',
+ message: `Script load (url:${actual_url})`
+ };
+ frame.contentWindow.load_script_with_integrity(actual_url, integrity);
+ return add_promise_to_test(actual_url);
+}
+
+function css_integrity_test(frame, url, integrity, expected_integrity) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ url: actual_url,
+ mode: 'no-cors',
+ credentials: 'include',
+ redirect: 'follow',
+ integrity: expected_integrity,
+ destination: 'style',
+ message: `CSS load (url:${actual_url})`
+ };
+ frame.contentWindow.load_css_with_integrity(actual_url, integrity);
+ return add_promise_to_test(actual_url);
+}
+
+function fetch_test(frame, url, mode, credentials,
+ expected_mode, expected_credentials) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ mode: expected_mode,
+ credentials: expected_credentials,
+ redirect: 'follow',
+ integrity: '',
+ destination: '',
+ message: `fetch (url:${actual_url} mode:${mode} ` +
+ `credentials:${credentials})`
+ };
+ frame.contentWindow.fetch(
+ new Request(actual_url, {mode: mode, credentials: credentials}));
+ return add_promise_to_test(actual_url);
+}
+
+function audio_test(frame, url, cross_origin,
+ expected_mode, expected_credentials) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ mode: expected_mode,
+ credentials: expected_credentials,
+ redirect: 'follow',
+ integrity: '',
+ destination: 'audio',
+ message: `Audio load (url:${actual_url} cross_origin:${cross_origin})`
+ };
+ frame.contentWindow.load_audio(actual_url, cross_origin);
+ return add_promise_to_test(actual_url);
+}
+
+
+function video_test(frame, url, cross_origin,
+ expected_mode, expected_credentials) {
+ const actual_url = url + (++url_count);
+ expected_results[actual_url] = {
+ mode: expected_mode,
+ credentials: expected_credentials,
+ redirect: 'follow',
+ integrity: '',
+ destination: 'video',
+ message: `Video load (url:${actual_url} cross_origin:${cross_origin})`
+ };
+ frame.contentWindow.load_video(actual_url, cross_origin);
+ return add_promise_to_test(actual_url);
+}
+
+promise_test(async t => {
+ const SCOPE = 'resources/fetch-request-resources-iframe.https.html';
+ const SCRIPT = 'resources/fetch-request-resources-worker.js';
+ const host_info = get_host_info();
+ const LOCAL_URL =
+ host_info['HTTPS_ORIGIN'] + base_path() + 'resources/sample?test';
+ const REMOTE_URL =
+ host_info['HTTPS_REMOTE_ORIGIN'] + base_path() + 'resources/sample?test';
+
+ const registration =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+
+ await new Promise((resolve, reject) => {
+ const channel = new MessageChannel();
+ channel.port1.onmessage = t.step_func(msg => {
+ if (msg.data.ready) {
+ resolve();
+ return;
+ }
+ const result = msg.data;
+ const expected = expected_results[result.url];
+ if (!expected) {
+ return;
+ }
+ test(() => {
+ assert_equals(
+ result.mode, expected.mode,
+ `mode of must be ${expected.mode}.`);
+ assert_equals(
+ result.credentials, expected.credentials,
+ `credentials of ${expected.message} must be ` +
+ `${expected.credentials}.`);
+ assert_equals(
+ result.redirect, expected.redirect,
+ `redirect mode of ${expected.message} must be ` +
+ `${expected.redirect}.`);
+ assert_equals(
+ result.integrity, expected.integrity,
+ `integrity of ${expected.message} must be ` +
+ `${expected.integrity}.`);
+ assert_equals(
+ result.destination, expected.destination,
+ `destination of ${expected.message} must be ` +
+ `${expected.destination}.`);
+ }, expected.message);
+ expected.resolve();
+ delete expected_results[result.url];
+ });
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ });
+
+ const f = await with_iframe(SCOPE);
+ t.add_cleanup(() => f.remove());
+
+ await image_test(f, LOCAL_URL, '', 'no-cors', 'include');
+ await image_test(f, REMOTE_URL, '', 'no-cors', 'include');
+ await css_test(f, LOCAL_URL, '', 'no-cors', 'include');
+ await css_test(f, REMOTE_URL, '', 'no-cors', 'include');
+
+ await image_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin');
+ await image_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include');
+ await image_test(f, REMOTE_URL, '', 'no-cors', 'include');
+ await image_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin');
+ await image_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include');
+
+ await script_test(f, LOCAL_URL, '', 'no-cors', 'include');
+ await script_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin');
+ await script_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include');
+ await script_test(f, REMOTE_URL, '', 'no-cors', 'include');
+ await script_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin');
+ await script_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include');
+
+ await css_test(f, LOCAL_URL, '', 'no-cors', 'include');
+ await css_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin');
+ await css_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include');
+ await css_test(f, REMOTE_URL, '', 'no-cors', 'include');
+ await css_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin');
+ await css_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include');
+
+ await font_face_test(f, LOCAL_URL, 'cors', 'same-origin');
+ await font_face_test(f, REMOTE_URL, 'cors', 'same-origin');
+
+ await script_integrity_test(f, LOCAL_URL, ' ', ' ');
+ await script_integrity_test(
+ f, LOCAL_URL,
+ 'This is not a valid integrity because it has no dashes',
+ 'This is not a valid integrity because it has no dashes');
+ await script_integrity_test(f, LOCAL_URL, 'sha256-', 'sha256-');
+ await script_integrity_test(f, LOCAL_URL, 'sha256-foo?123', 'sha256-foo?123');
+ await script_integrity_test(f, LOCAL_URL, 'sha256-foo sha384-abc ',
+ 'sha256-foo sha384-abc ');
+ await script_integrity_test(f, LOCAL_URL, 'sha256-foo sha256-abc',
+ 'sha256-foo sha256-abc');
+
+ await css_integrity_test(f, LOCAL_URL, ' ', ' ');
+ await css_integrity_test(
+ f, LOCAL_URL,
+ 'This is not a valid integrity because it has no dashes',
+ 'This is not a valid integrity because it has no dashes');
+ await css_integrity_test(f, LOCAL_URL, 'sha256-', 'sha256-');
+ await css_integrity_test(f, LOCAL_URL, 'sha256-foo?123', 'sha256-foo?123');
+ await css_integrity_test(f, LOCAL_URL, 'sha256-foo sha384-abc ',
+ 'sha256-foo sha384-abc ');
+ await css_integrity_test(f, LOCAL_URL, 'sha256-foo sha256-abc',
+ 'sha256-foo sha256-abc');
+
+ await fetch_test(f, LOCAL_URL, 'same-origin', 'omit', 'same-origin', 'omit');
+ await fetch_test(f, LOCAL_URL, 'same-origin', 'same-origin',
+ 'same-origin', 'same-origin');
+ await fetch_test(f, LOCAL_URL, 'same-origin', 'include',
+ 'same-origin', 'include');
+ await fetch_test(f, LOCAL_URL, 'no-cors', 'omit', 'no-cors', 'omit');
+ await fetch_test(f, LOCAL_URL, 'no-cors', 'same-origin',
+ 'no-cors', 'same-origin');
+ await fetch_test(f, LOCAL_URL, 'no-cors', 'include', 'no-cors', 'include');
+ await fetch_test(f, LOCAL_URL, 'cors', 'omit', 'cors', 'omit');
+ await fetch_test(f, LOCAL_URL, 'cors', 'same-origin', 'cors', 'same-origin');
+ await fetch_test(f, LOCAL_URL, 'cors', 'include', 'cors', 'include');
+ await fetch_test(f, REMOTE_URL, 'no-cors', 'omit', 'no-cors', 'omit');
+ await fetch_test(f, REMOTE_URL, 'no-cors', 'same-origin', 'no-cors', 'same-origin');
+ await fetch_test(f, REMOTE_URL, 'no-cors', 'include', 'no-cors', 'include');
+ await fetch_test(f, REMOTE_URL, 'cors', 'omit', 'cors', 'omit');
+ await fetch_test(f, REMOTE_URL, 'cors', 'same-origin', 'cors', 'same-origin');
+ await fetch_test(f, REMOTE_URL, 'cors', 'include', 'cors', 'include');
+
+ await audio_test(f, LOCAL_URL, '', 'no-cors', 'include');
+ await audio_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin');
+ await audio_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include');
+ await audio_test(f, REMOTE_URL, '', 'no-cors', 'include');
+ await audio_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin');
+ await audio_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include');
+
+ await video_test(f, LOCAL_URL, '', 'no-cors', 'include');
+ await video_test(f, LOCAL_URL, 'anonymous', 'cors', 'same-origin');
+ await video_test(f, LOCAL_URL, 'use-credentials', 'cors', 'include');
+ await video_test(f, REMOTE_URL, '', 'no-cors', 'include');
+ await video_test(f, REMOTE_URL, 'anonymous', 'cors', 'same-origin');
+ await video_test(f, REMOTE_URL, 'use-credentials', 'cors', 'include');
+}, 'Verify FetchEvent for resources.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr-sync-error.https.window.js b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr-sync-error.https.window.js
new file mode 100644
index 0000000..e6c0213
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr-sync-error.https.window.js
@@ -0,0 +1,19 @@
+// META: script=resources/test-helpers.sub.js
+
+"use strict";
+
+promise_test(async t => {
+ const url = "resources/fetch-request-xhr-sync-error-worker.js";
+ const scope = "resources/fetch-request-xhr-sync-iframe.html";
+
+ const registration = await service_worker_unregister_and_register(t, url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ assert_throws_dom("NetworkError", frame.contentWindow.DOMException, () => frame.contentWindow.performSyncXHR("non-existent-stream-1.txt"));
+ assert_throws_dom("NetworkError", frame.contentWindow.DOMException, () => frame.contentWindow.performSyncXHR("non-existent-stream-2.txt"));
+ assert_throws_dom("NetworkError", frame.contentWindow.DOMException, () => frame.contentWindow.performSyncXHR("non-existent-stream-3.txt"));
+}, "Verify synchronous XMLHttpRequest always throws a NetworkError for ReadableStream errors");
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr-sync-on-worker.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr-sync-on-worker.https.html
new file mode 100644
index 0000000..9f18096
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr-sync-on-worker.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<title>Service Worker: Synchronous XHR on Worker is intercepted</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test((t) => {
+ const url = 'resources/fetch-request-xhr-sync-on-worker-worker.js';
+ const scope = 'resources/fetch-request-xhr-sync-on-worker-scope/';
+ const non_existent_file = 'non-existent-file.txt';
+
+ // In Chromium, the service worker scope matching for workers is based on
+ // the URL of the parent HTML. So this test creates an iframe which is
+ // controlled by the service worker first, and creates a worker from the
+ // iframe.
+ return service_worker_unregister_and_register(t, url, scope)
+ .then((registration) => {
+ t.add_cleanup(() => registration.unregister());
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => { return with_iframe(scope + 'iframe_page'); })
+ .then((frame) => {
+ t.add_cleanup(() => frame.remove());
+ return frame.contentWindow.performSyncXHROnWorker(non_existent_file);
+ })
+ .then((result) => {
+ assert_equals(
+ result.status,
+ 200,
+ 'HTTP response status code for intercepted request'
+ );
+ assert_equals(
+ result.responseText,
+ 'Response from service worker',
+ 'HTTP response text for intercepted request'
+ );
+ });
+ }, 'Verify SyncXHR on Worker is intercepted');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr-sync.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr-sync.https.html
new file mode 100644
index 0000000..ec27fb8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr-sync.https.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<title>Service Worker: Synchronous XHR is intercepted</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var url = 'resources/fetch-request-xhr-sync-worker.js';
+ var scope = 'resources/fetch-request-xhr-sync-iframe.html';
+ var non_existent_file = 'non-existent-file.txt';
+
+ return service_worker_unregister_and_register(t, url, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return registration.unregister();
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(frame) {
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+
+ return new Promise(function(resolve, reject) {
+ t.step_timeout(function() {
+ var xhr;
+ try {
+ xhr = frame.contentWindow.performSyncXHR(non_existent_file);
+ resolve(xhr);
+ } catch (err) {
+ reject(err);
+ }
+ }, 0);
+ })
+ })
+ .then(function(xhr) {
+ assert_equals(
+ xhr.status,
+ 200,
+ 'HTTP response status code for intercepted request'
+ );
+ assert_equals(
+ xhr.responseText,
+ 'Response from service worker',
+ 'HTTP response text for intercepted request'
+ );
+ });
+ }, 'Verify SyncXHR is intercepted');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr.https.html
new file mode 100644
index 0000000..37a4573
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-request-xhr.https.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<title>Service Worker: the body of FetchEvent using XMLHttpRequest</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe-sub"></script>
+<script>
+let frame;
+
+// Set up the service worker and the frame.
+promise_test(t => {
+ const kScope = 'resources/fetch-request-xhr-iframe.https.html';
+ const kScript = 'resources/fetch-request-xhr-worker.js';
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ promise_test(() => {
+ return registration.unregister();
+ }, 'restore global state');
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(kScope);
+ })
+ .then(f => {
+ frame = f;
+ add_completion_callback(() => { f.remove(); });
+ });
+ }, 'initialize global state');
+
+// Run the tests.
+promise_test(t => {
+ return frame.contentWindow.get_header_test();
+ }, 'event.request has the expected headers for same-origin GET.');
+
+promise_test(t => {
+ return frame.contentWindow.post_header_test();
+ }, 'event.request has the expected headers for same-origin POST.');
+
+promise_test(t => {
+ return frame.contentWindow.cross_origin_get_header_test();
+ }, 'event.request has the expected headers for cross-origin GET.');
+
+promise_test(t => {
+ return frame.contentWindow.cross_origin_post_header_test();
+ }, 'event.request has the expected headers for cross-origin POST.');
+
+promise_test(t => {
+ return frame.contentWindow.string_test();
+ }, 'FetchEvent#request.body contains XHR request data (string)');
+
+promise_test(t => {
+ return frame.contentWindow.blob_test();
+ }, 'FetchEvent#request.body contains XHR request data (blob)');
+
+promise_test(t => {
+ return frame.contentWindow.custom_method_test();
+ }, 'FetchEvent#request.method is set to XHR method');
+
+promise_test(t => {
+ return frame.contentWindow.options_method_test();
+ }, 'XHR using OPTIONS method');
+
+promise_test(t => {
+ return frame.contentWindow.form_data_test();
+ }, 'XHR with form data');
+
+promise_test(t => {
+ return frame.contentWindow.mode_credentials_test();
+ }, 'XHR with mode/credentials set');
+
+promise_test(t => {
+ return frame.contentWindow.data_url_test();
+ }, 'XHR to data URL');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-response-taint.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-response-taint.https.html
new file mode 100644
index 0000000..8e190f4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-response-taint.https.html
@@ -0,0 +1,223 @@
+<!DOCTYPE html>
+<title>Service Worker: Tainting of responses fetched via SW.</title>
+<!-- This test makes a large number of requests sequentially. -->
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var host_info = get_host_info();
+var BASE_ORIGIN = host_info.HTTPS_ORIGIN;
+var OTHER_ORIGIN = host_info.HTTPS_REMOTE_ORIGIN;
+var BASE_URL = BASE_ORIGIN + base_path() +
+ 'resources/fetch-access-control.py?';
+var OTHER_BASE_URL = OTHER_ORIGIN + base_path() +
+ 'resources/fetch-access-control.py?';
+
+function frame_fetch(frame, url, mode, credentials) {
+ var foreignPromise = frame.contentWindow.fetch(
+ new Request(url, {mode: mode, credentials: credentials}))
+
+ // Event loops should be shared between contexts of similar origin, not all
+ // browsers adhere to this expectation at the time of this writing. Incorrect
+ // behavior in this regard can interfere with test execution when the
+ // provided iframe is removed from the document.
+ //
+ // WPT maintains a test dedicated the expected treatment of event loops, so
+ // the following workaround is acceptable in this context.
+ return Promise.resolve(foreignPromise);
+}
+
+var login_and_register;
+promise_test(function(t) {
+ var SCOPE = 'resources/fetch-response-taint-iframe.html';
+ var SCRIPT = 'resources/fetch-rewrite-worker.js';
+ var registration;
+
+ login_and_register = login_https(t, host_info.HTTPS_ORIGIN, host_info.HTTPS_REMOTE_ORIGIN)
+ .then(function() {
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ })
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(f) {
+ // This test should not be considered complete until after the
+ // service worker has been unregistered. Currently, `testharness.js`
+ // does not support asynchronous global "tear down" logic, so this
+ // must be expressed using a dedicated `promise_test`. Because the
+ // other sub-tests in this file are declared synchronously, this
+ // test will be the final test executed.
+ promise_test(function(t) {
+ f.remove();
+ return registration.unregister();
+ }, 'restore global state');
+
+ return f;
+ });
+ return login_and_register;
+ }, 'initialize global state');
+
+function ng_test(url, mode, credentials) {
+ promise_test(function(t) {
+ return login_and_register
+ .then(function(frame) {
+ var fetchRequest = frame_fetch(frame, url, mode, credentials);
+ return promise_rejects_js(t, frame.contentWindow.TypeError, fetchRequest);
+ });
+ }, 'url:\"' + url + '\" mode:\"' + mode +
+ '\" credentials:\"' + credentials + '\" should fail.');
+}
+
+function ok_test(url, mode, credentials, expected_type, expected_username) {
+ promise_test(function() {
+ return login_and_register.then(function(frame) {
+ return frame_fetch(frame, url, mode, credentials)
+ })
+ .then(function(res) {
+ assert_equals(res.type, expected_type, 'response type');
+ return res.text();
+ })
+ .then(function(text) {
+ if (expected_type == 'opaque') {
+ assert_equals(text, '');
+ } else {
+ return new Promise(function(resolve) {
+ var report = resolve;
+ // text must contain report() call.
+ eval(text);
+ })
+ .then(function(result) {
+ assert_equals(result.username, expected_username);
+ });
+ }
+ });
+ }, 'fetching url:\"' + url + '\" mode:\"' + mode +
+ '\" credentials:\"' + credentials + '\" should ' +
+ 'succeed.');
+}
+
+function build_rewrite_url(origin, url, mode, credentials) {
+ return origin + '/?url=' + encodeURIComponent(url) + '&mode=' + mode +
+ '&credentials=' + credentials + '&';
+}
+
+function for_each_origin_mode_credentials(callback) {
+ [BASE_ORIGIN, OTHER_ORIGIN].forEach(function(origin) {
+ ['same-origin', 'no-cors', 'cors'].forEach(function(mode) {
+ ['omit', 'same-origin', 'include'].forEach(function(credentials) {
+ callback(origin, mode, credentials);
+ });
+ });
+ });
+}
+
+ok_test(BASE_URL, 'same-origin', 'omit', 'basic', 'undefined');
+ok_test(BASE_URL, 'same-origin', 'same-origin', 'basic', 'username2s');
+ok_test(BASE_URL, 'same-origin', 'include', 'basic', 'username2s');
+ok_test(BASE_URL, 'no-cors', 'omit', 'basic', 'undefined');
+ok_test(BASE_URL, 'no-cors', 'same-origin', 'basic', 'username2s');
+ok_test(BASE_URL, 'no-cors', 'include', 'basic', 'username2s');
+ok_test(BASE_URL, 'cors', 'omit', 'basic', 'undefined');
+ok_test(BASE_URL, 'cors', 'same-origin', 'basic', 'username2s');
+ok_test(BASE_URL, 'cors', 'include', 'basic', 'username2s');
+ng_test(OTHER_BASE_URL, 'same-origin', 'omit');
+ng_test(OTHER_BASE_URL, 'same-origin', 'same-origin');
+ng_test(OTHER_BASE_URL, 'same-origin', 'include');
+ok_test(OTHER_BASE_URL, 'no-cors', 'omit', 'opaque');
+ok_test(OTHER_BASE_URL, 'no-cors', 'same-origin', 'opaque');
+ok_test(OTHER_BASE_URL, 'no-cors', 'include', 'opaque');
+ng_test(OTHER_BASE_URL, 'cors', 'omit');
+ng_test(OTHER_BASE_URL, 'cors', 'same-origin');
+ng_test(OTHER_BASE_URL, 'cors', 'include');
+ok_test(OTHER_BASE_URL + 'ACAOrigin=*', 'cors', 'omit', 'cors', 'undefined');
+ok_test(OTHER_BASE_URL + 'ACAOrigin=*', 'cors', 'same-origin', 'cors',
+ 'undefined');
+ng_test(OTHER_BASE_URL + 'ACAOrigin=*', 'cors', 'include');
+ok_test(OTHER_BASE_URL + 'ACAOrigin=' + BASE_ORIGIN + '&ACACredentials=true',
+ 'cors', 'include', 'cors', 'username1s')
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+ var url = build_rewrite_url(
+ origin, BASE_URL, 'same-origin', 'omit');
+ // Fetch to the other origin with same-origin mode should fail.
+ if (origin == OTHER_ORIGIN && mode == 'same-origin') {
+ ng_test(url, mode, credentials);
+ } else {
+ // The response type from the SW should be basic
+ ok_test(url, mode, credentials, 'basic', 'undefined');
+ }
+});
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+ var url = build_rewrite_url(
+ origin, BASE_URL, 'same-origin', 'same-origin');
+
+ // Fetch to the other origin with same-origin mode should fail.
+ if (origin == OTHER_ORIGIN && mode == 'same-origin') {
+ ng_test(url, mode, credentials);
+ } else {
+ // The response type from the SW should be basic.
+ ok_test(url, mode, credentials, 'basic', 'username2s');
+ }
+});
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+ var url = build_rewrite_url(
+ origin, OTHER_BASE_URL, 'same-origin', 'omit');
+ // The response from the SW should be an error.
+ ng_test(url, mode, credentials);
+});
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+ var url = build_rewrite_url(
+ origin, OTHER_BASE_URL, 'no-cors', 'omit');
+
+ // SW can respond only to no-cors requests.
+ if (mode != 'no-cors') {
+ ng_test(url, mode, credentials);
+ } else {
+ // The response type from the SW should be opaque.
+ ok_test(url, mode, credentials, 'opaque');
+ }
+});
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+ var url = build_rewrite_url(
+ origin, OTHER_BASE_URL + 'ACAOrigin=*', 'cors', 'omit');
+
+ // Fetch to the other origin with same-origin mode should fail.
+ if (origin == OTHER_ORIGIN && mode == 'same-origin') {
+ ng_test(url, mode, credentials);
+ } else if (origin == BASE_ORIGIN && mode == 'same-origin') {
+ // Cors type response to a same-origin mode request should fail
+ ng_test(url, mode, credentials);
+ } else {
+ // The response from the SW should be cors.
+ ok_test(url, mode, credentials, 'cors', 'undefined');
+ }
+});
+
+for_each_origin_mode_credentials(function(origin, mode, credentials) {
+ var url = build_rewrite_url(
+ origin,
+ OTHER_BASE_URL + 'ACAOrigin=' + BASE_ORIGIN +
+ '&ACACredentials=true',
+ 'cors', 'include');
+ // Fetch to the other origin with same-origin mode should fail.
+ if (origin == OTHER_ORIGIN && mode == 'same-origin') {
+ ng_test(url, mode, credentials);
+ } else if (origin == BASE_ORIGIN && mode == 'same-origin') {
+ // Cors type response to a same-origin mode request should fail
+ ng_test(url, mode, credentials);
+ } else {
+ // The response from the SW should be cors.
+ ok_test(url, mode, credentials, 'cors', 'username1s');
+ }
+});
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-response-xhr.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-response-xhr.https.html
new file mode 100644
index 0000000..891eb02
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-response-xhr.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<title>Service Worker: the response of FetchEvent using XMLHttpRequest</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var SCOPE = 'resources/fetch-response-xhr-iframe.https.html';
+ var SCRIPT = 'resources/fetch-response-xhr-worker.js';
+ var host_info = get_host_info();
+
+ window.addEventListener('message', t.step_func(on_message), false);
+ function on_message(e) {
+ assert_equals(e.data.results, 'foo, bar');
+ e.source.postMessage('ACK', host_info['HTTPS_ORIGIN']);
+ }
+
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ var channel;
+
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+
+ channel = new MessageChannel();
+ var onPortMsg = new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ });
+
+ frame.contentWindow.postMessage('START',
+ host_info['HTTPS_ORIGIN'],
+ [channel.port2]);
+
+ return onPortMsg;
+ })
+ .then(function(e) {
+ assert_equals(e.data.results, 'finish');
+ });
+ }, 'Verify the response of FetchEvent using XMLHttpRequest');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/fetch-waits-for-activate.https.html b/third_party/web_platform_tests/service-workers/service-worker/fetch-waits-for-activate.https.html
new file mode 100644
index 0000000..7c88845
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/fetch-waits-for-activate.https.html
@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+<title>Service Worker: Fetch Event Waits for Activate Event</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const worker_url = 'resources/fetch-waits-for-activate-worker.js';
+const normalized_worker_url = normalizeURL(worker_url);
+const worker_scope = 'resources/fetch-waits-for-activate/';
+
+// Resolves with the Service Worker's registration once it's reached the
+// "activating" state. (The Service Worker should remain "activating" until
+// explicitly told advance to the "activated" state).
+async function registerAndWaitForActivating(t) {
+ const registration = await service_worker_unregister_and_register(
+ t, worker_url, worker_scope);
+ t.add_cleanup(() => service_worker_unregister(t, worker_scope));
+
+ await wait_for_state(t, registration.installing, 'activating');
+
+ return registration;
+}
+
+// Attempts to ensure that the "Handle Fetch" algorithm has reached the step
+//
+// "If activeWorker’s state is "activating", wait for activeWorker’s state to
+// become "activated"."
+//
+// by waiting for some time to pass.
+//
+// WARNING: whether the algorithm has reached that step isn't directly
+// observable, so this is best effort and can race. Note that this can only
+// result in false positives (where the algorithm hasn't reached that step yet
+// and any functional events haven't actually been handled by the Service
+// Worker).
+async function ensureFunctionalEventsAreWaiting(registration) {
+ await (new Promise(resolve => { setTimeout(resolve, 1000); }));
+
+ assert_equals(registration.active.scriptURL, normalized_worker_url,
+ 'active worker should be present');
+ assert_equals(registration.active.state, 'activating',
+ 'active worker should be in activating state');
+}
+
+promise_test(async t => {
+ const registration = await registerAndWaitForActivating(t);
+
+ let frame = null;
+ t.add_cleanup(() => {
+ if (frame) {
+ frame.remove();
+ }
+ });
+
+ // This should block until we message the worker to tell it to complete
+ // the activate event.
+ const frameLoadPromise = with_iframe(worker_scope).then(function(f) {
+ frame = f;
+ });
+
+ await ensureFunctionalEventsAreWaiting(registration);
+ assert_equals(frame, null, 'frame should not be loaded');
+
+ registration.active.postMessage('ACTIVATE');
+
+ await frameLoadPromise;
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ normalized_worker_url,
+ 'frame should now be loaded and controlled');
+ assert_equals(registration.active.state, 'activated',
+ 'active worker should be in activated state');
+}, 'Navigation fetch events should wait for the activate event to complete.');
+
+promise_test(async t => {
+ const frame = await with_iframe(worker_scope);
+ t.add_cleanup(() => { frame.remove(); });
+
+ const registration = await registerAndWaitForActivating(t);
+
+ // Make the Service Worker control the frame so the frame can perform an
+ // intercepted fetch.
+ await (new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => {
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ normalized_worker_url, 'frame should be controlled');
+ resolve();
+ };
+
+ registration.active.postMessage('CLAIM');
+ }));
+
+ const fetch_url = `${worker_scope}non/existent/path`;
+ const expected_fetch_result = 'Hello world';
+ let fetch_promise_settled = false;
+
+ // This should block until we message the worker to tell it to complete
+ // the activate event.
+ const fetchPromise = frame.contentWindow.fetch(fetch_url, {
+ method: 'POST',
+ body: expected_fetch_result,
+ }).then(response => {
+ fetch_promise_settled = true;
+ return response;
+ });
+
+ await ensureFunctionalEventsAreWaiting(registration);
+ assert_false(fetch_promise_settled,
+ "fetch()-ing a Service Worker-controlled scope shouldn't have " +
+ "settled yet");
+
+ registration.active.postMessage('ACTIVATE');
+
+ const response = await fetchPromise;
+ assert_equals(await response.text(), expected_fetch_result,
+ "Service Worker should have responded to request to" +
+ fetch_url)
+ assert_equals(registration.active.state, 'activated',
+ 'active worker should be in activated state');
+}, 'Subresource fetch events should wait for the activate event to complete.');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/getregistration.https.html b/third_party/web_platform_tests/service-workers/service-worker/getregistration.https.html
new file mode 100644
index 0000000..634c2ef
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/getregistration.https.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+async_test(function(t) {
+ var documentURL = 'no-such-worker';
+ navigator.serviceWorker.getRegistration(documentURL)
+ .then(function(value) {
+ assert_equals(value, undefined,
+ 'getRegistration should resolve with undefined');
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'getRegistration');
+
+promise_test(function(t) {
+ var scope = 'resources/scope/getregistration/normal';
+ var registration;
+ return service_worker_unregister_and_register(t, 'resources/empty-worker.js',
+ scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return navigator.serviceWorker.getRegistration(scope);
+ })
+ .then(function(value) {
+ assert_equals(
+ value, registration,
+ 'getRegistration should resolve to the same registration object');
+ });
+ }, 'Register then getRegistration');
+
+promise_test(function(t) {
+ var scope = 'resources/scope/getregistration/url-with-fragment';
+ var documentURL = scope + '#ref';
+ var registration;
+ return service_worker_unregister_and_register(t, 'resources/empty-worker.js',
+ scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return navigator.serviceWorker.getRegistration(documentURL);
+ })
+ .then(function(value) {
+ assert_equals(
+ value, registration,
+ 'getRegistration should resolve to the same registration object');
+ });
+ }, 'Register then getRegistration with a URL having a fragment');
+
+async_test(function(t) {
+ var documentURL = 'http://example.com/';
+ navigator.serviceWorker.getRegistration(documentURL)
+ .then(function() {
+ assert_unreached(
+ 'getRegistration with an out of origin URL should fail');
+ }, function(reason) {
+ assert_equals(
+ reason.name, 'SecurityError',
+ 'getRegistration with an out of origin URL should fail');
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'getRegistration with a cross origin URL');
+
+async_test(function(t) {
+ var scope = 'resources/scope/getregistration/register-unregister';
+ service_worker_unregister_and_register(t, 'resources/empty-worker.js',
+ scope)
+ .then(function(registration) {
+ return registration.unregister();
+ })
+ .then(function() {
+ return navigator.serviceWorker.getRegistration(scope);
+ })
+ .then(function(value) {
+ assert_equals(value, undefined,
+ 'getRegistration should resolve with undefined');
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Register then Unregister then getRegistration');
+
+
+promise_test(async function(t) {
+ const scope = 'resources/scope/getregistration/register-unregister';
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/empty-worker.js', scope
+ );
+
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ const frameNav = frame.contentWindow.navigator;
+ await registration.unregister();
+ const value = await frameNav.serviceWorker.getRegistration(scope);
+
+ assert_equals(value, undefined, 'getRegistration should resolve with undefined');
+}, 'Register then Unregister then getRegistration in controlled iframe');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/getregistrations.https.html b/third_party/web_platform_tests/service-workers/service-worker/getregistrations.https.html
new file mode 100644
index 0000000..3a9b9a2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/getregistrations.https.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<title>Service Worker: getRegistrations()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+// Purge the existing registrations for the origin.
+// getRegistrations() is used in order to avoid adding additional complexity
+// e.g. adding an internal function.
+promise_test(async () => {
+ const registrations = await navigator.serviceWorker.getRegistrations();
+ await Promise.all(registrations.map(r => r.unregister()));
+ const value = await navigator.serviceWorker.getRegistrations();
+ assert_array_equals(
+ value, [],
+ 'getRegistrations should resolve with an empty array.');
+}, 'registrations are not returned following unregister');
+
+promise_test(async t => {
+ const scope = 'resources/scope/getregistrations/normal';
+ const script = 'resources/empty-worker.js';
+ const registrations = [
+ await service_worker_unregister_and_register(t, script, scope)];
+ t.add_cleanup(() => registrations[0].unregister());
+ const value = await navigator.serviceWorker.getRegistrations();
+ assert_array_equals(value, registrations,
+ 'getRegistrations should resolve with an array of registrations');
+}, 'Register then getRegistrations');
+
+promise_test(async t => {
+ const scope1 = 'resources/scope/getregistrations/scope1';
+ const scope2 = 'resources/scope/getregistrations/scope2';
+ const scope3 = 'resources/scope/getregistrations/scope12';
+
+ const script = 'resources/empty-worker.js';
+ t.add_cleanup(() => service_worker_unregister(t, scope1));
+ t.add_cleanup(() => service_worker_unregister(t, scope2));
+ t.add_cleanup(() => service_worker_unregister(t, scope3));
+
+ const registrations = [
+ await service_worker_unregister_and_register(t, script, scope1),
+ await service_worker_unregister_and_register(t, script, scope2),
+ await service_worker_unregister_and_register(t, script, scope3),
+ ];
+
+ const value = await navigator.serviceWorker.getRegistrations();
+ assert_array_equals(value, registrations);
+}, 'Register multiple times then getRegistrations');
+
+promise_test(async t => {
+ const scope = 'resources/scope/getregistrations/register-unregister';
+ const script = 'resources/empty-worker.js';
+ const registration = await service_worker_unregister_and_register(t, script, scope);
+ await registration.unregister();
+ const value = await navigator.serviceWorker.getRegistrations();
+ assert_array_equals(
+ value, [], 'getRegistrations should resolve with an empty array.');
+}, 'Register then Unregister then getRegistrations');
+
+promise_test(async t => {
+ const scope = 'resources/scope/getregistrations/register-unregister-controlled';
+ const script = 'resources/empty-worker.js';
+ const registration = await service_worker_unregister_and_register(t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Create a frame controlled by the service worker and unregister the
+ // worker.
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+ await registration.unregister();
+
+ const value = await navigator.serviceWorker.getRegistrations();
+ assert_array_equals(
+ value, [],
+ 'getRegistrations should resolve with an empty array.');
+ assert_equals(registration.installing, null);
+ assert_equals(registration.waiting, null);
+ assert_equals(registration.active.state, 'activated');
+}, 'Register then Unregister with controlled frame then getRegistrations');
+
+promise_test(async t => {
+ const host_info = get_host_info();
+ // Rewrite the url to point to remote origin.
+ const frame_same_origin_url = new URL("resources/frame-for-getregistrations.html", window.location);
+ const frame_url = host_info['HTTPS_REMOTE_ORIGIN'] + frame_same_origin_url.pathname;
+ const scope = 'resources/scope-for-getregistrations';
+ const script = 'resources/empty-worker.js';
+
+ // Loads an iframe and waits for 'ready' message from it to resolve promise.
+ // Caller is responsible for removing frame.
+ function with_iframe_ready(url) {
+ return new Promise(resolve => {
+ const frame = document.createElement('iframe');
+ frame.src = url;
+ window.addEventListener('message', function onMessage(e) {
+ window.removeEventListener('message', onMessage);
+ if (e.data == 'ready') {
+ resolve(frame);
+ }
+ });
+ document.body.appendChild(frame);
+ });
+ }
+
+ // We need this special frame loading function because the frame is going
+ // to register it's own service worker and there is the possibility that that
+ // register() finishes after the register() for the same domain later in the
+ // test. So we have to wait until the cross origin register() is done, and not
+ // just until the frame loads.
+ const frame = await with_iframe_ready(frame_url);
+ t.add_cleanup(async () => {
+ // Wait until the cross-origin worker is unregistered.
+ let resolve;
+ const channel = new MessageChannel();
+ channel.port1.onmessage = e => {
+ if (e.data == 'unregistered')
+ resolve();
+ };
+ frame.contentWindow.postMessage('unregister', '*', [channel.port2]);
+ await new Promise(r => { resolve = r; });
+
+ frame.remove();
+ });
+
+ const registrations = [
+ await service_worker_unregister_and_register(t, script, scope)];
+ t.add_cleanup(() => registrations[0].unregister());
+ const value = await navigator.serviceWorker.getRegistrations();
+ assert_array_equals(
+ value, registrations,
+ 'getRegistrations should only return same origin registrations.');
+}, 'getRegistrations promise resolves only with same origin registrations.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/global-serviceworker.https.any.js b/third_party/web_platform_tests/service-workers/service-worker/global-serviceworker.https.any.js
new file mode 100644
index 0000000..19d7784
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/global-serviceworker.https.any.js
@@ -0,0 +1,53 @@
+// META: title=serviceWorker on service worker global
+// META: global=serviceworker
+
+test(() => {
+ assert_equals(registration.installing, null, 'registration.installing');
+ assert_equals(registration.waiting, null, 'registration.waiting');
+ assert_equals(registration.active, null, 'registration.active');
+ assert_true('serviceWorker' in self, 'self.serviceWorker exists');
+ assert_equals(serviceWorker.state, 'parsed', 'serviceWorker.state');
+ assert_readonly(self, 'serviceWorker', `self.serviceWorker is read only`);
+}, 'First run');
+
+// Cache this for later tests.
+const initialServiceWorker = self.serviceWorker;
+
+async_test((t) => {
+ assert_true('serviceWorker' in self, 'self.serviceWorker exists');
+ serviceWorker.postMessage({ messageTest: true });
+
+ // The rest of the test runs once this receives the above message.
+ addEventListener('message', t.step_func((event) => {
+ // Ignore unrelated messages.
+ if (!event.data.messageTest) return;
+ assert_equals(event.source, serviceWorker, 'event.source');
+ t.done();
+ }));
+}, 'Can post message to self during startup');
+
+// The test is registered now so there isn't a race condition when collecting tests, but the asserts
+// don't happen until the 'install' event fires.
+async_test((t) => {
+ addEventListener('install', t.step_func_done(() => {
+ assert_true('serviceWorker' in self, 'self.serviceWorker exists');
+ assert_equals(serviceWorker, initialServiceWorker, `self.serviceWorker hasn't changed`);
+ assert_equals(registration.installing, serviceWorker, 'registration.installing');
+ assert_equals(registration.waiting, null, 'registration.waiting');
+ assert_equals(registration.active, null, 'registration.active');
+ assert_equals(serviceWorker.state, 'installing', 'serviceWorker.state');
+ }));
+}, 'During install');
+
+// The test is registered now so there isn't a race condition when collecting tests, but the asserts
+// don't happen until the 'activate' event fires.
+async_test((t) => {
+ addEventListener('activate', t.step_func_done(() => {
+ assert_true('serviceWorker' in self, 'self.serviceWorker exists');
+ assert_equals(serviceWorker, initialServiceWorker, `self.serviceWorker hasn't changed`);
+ assert_equals(registration.installing, null, 'registration.installing');
+ assert_equals(registration.waiting, null, 'registration.waiting');
+ assert_equals(registration.active, serviceWorker, 'registration.active');
+ assert_equals(serviceWorker.state, 'activating', 'serviceWorker.state');
+ }));
+}, 'During activate');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/historical.https.any.js b/third_party/web_platform_tests/service-workers/service-worker/historical.https.any.js
new file mode 100644
index 0000000..20b3ddf
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/historical.https.any.js
@@ -0,0 +1,5 @@
+// META: global=serviceworker
+
+test((t) => {
+ assert_false('targetClientId' in FetchEvent.prototype)
+}, 'targetClientId should not be on FetchEvent');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/http-to-https-redirect-and-register.https.html b/third_party/web_platform_tests/service-workers/service-worker/http-to-https-redirect-and-register.https.html
new file mode 100644
index 0000000..5626237
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/http-to-https-redirect-and-register.https.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<title>register on a secure page after redirect from an non-secure url</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+'use strict';
+
+var host_info = get_host_info();
+
+// Loads a non-secure url in a new window, which redirects to |target_url|.
+// That page then registers a service worker, and messages back with the result.
+// Returns a promise that resolves with the result.
+function redirect_and_register(target_url) {
+ var redirect_url = host_info.HTTP_REMOTE_ORIGIN + base_path() +
+ 'resources/redirect.py?Redirect=';
+ var child = window.open(redirect_url + encodeURIComponent(target_url));
+ return new Promise(resolve => {
+ window.addEventListener('message', e => resolve(e.data));
+ })
+ .then(function(result) {
+ child.close();
+ return result;
+ });
+}
+
+promise_test(function(t) {
+ var target_url = window.location.origin + base_path() +
+ 'resources/http-to-https-redirect-and-register-iframe.html';
+
+ return redirect_and_register(target_url)
+ .then(result => {
+ assert_equals(result, 'OK');
+ });
+ }, 'register on a secure page after redirect from an non-secure url');
+
+promise_test(function(t) {
+ var target_url = host_info.HTTP_REMOTE_ORIGIN + base_path() +
+ 'resources/http-to-https-redirect-and-register-iframe.html';
+
+ return redirect_and_register(target_url)
+ .then(result => {
+ assert_equals(result, 'FAIL: navigator.serviceWorker is undefined');
+ });
+ }, 'register on a non-secure page after redirect from an non-secure url');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/immutable-prototype-serviceworker.https.html b/third_party/web_platform_tests/service-workers/service-worker/immutable-prototype-serviceworker.https.html
new file mode 100644
index 0000000..e63f6b3
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/immutable-prototype-serviceworker.https.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+'use strict';
+
+let expected = ['immutable', 'immutable', 'immutable', 'immutable', 'immutable'];
+
+promise_test(t =>
+ navigator.serviceWorker.register('resources/immutable-prototype-serviceworker.js', {scope: './resources/'})
+ .then(registration => {
+ let worker = registration.installing || registration.waiting || registration.active;
+ let channel = new MessageChannel()
+ worker.postMessage(channel.port2, [channel.port2]);
+ let resolve;
+ let promise = new Promise(r => resolve = r);
+ channel.port1.onmessage = resolve;
+ return promise.then(result => assert_array_equals(expected, result.data));
+ }),
+'worker prototype chain should be immutable');
+</script>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/import-scripts-cross-origin.https.html b/third_party/web_platform_tests/service-workers/service-worker/import-scripts-cross-origin.https.html
new file mode 100644
index 0000000..773708a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/import-scripts-cross-origin.https.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for importScripts: cross-origin</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(async t => {
+ const scope = 'resources/import-scripts-cross-origin';
+ await service_worker_unregister(t, scope);
+ let reg = await navigator.serviceWorker.register(
+ 'resources/import-scripts-cross-origin-worker.sub.js', { scope: scope });
+ t.add_cleanup(_ => reg.unregister());
+ assert_not_equals(reg.installing, null, 'worker is installing');
+ }, 'importScripts() supports cross-origin requests');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/import-scripts-mime-types.https.html b/third_party/web_platform_tests/service-workers/service-worker/import-scripts-mime-types.https.html
new file mode 100644
index 0000000..1679831
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/import-scripts-mime-types.https.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for importScripts: MIME types</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+/**
+ * Test that a Service Worker's importScript() only accepts valid MIME types.
+ */
+let serviceWorker = null;
+
+promise_test(async t => {
+ const scope = 'resources/import-scripts-mime-types';
+ const registration = await service_worker_unregister_and_register(t,
+ 'resources/import-scripts-mime-types-worker.js', scope);
+
+ add_completion_callback(() => { registration.unregister(); });
+
+ await wait_for_state(t, registration.installing, 'activated');
+
+ serviceWorker = registration.active;
+}, 'Global setup');
+
+promise_test(async t => {
+ await fetch_tests_from_worker(serviceWorker);
+}, 'Fetch importScripts tests from service worker')
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/import-scripts-redirect.https.html b/third_party/web_platform_tests/service-workers/service-worker/import-scripts-redirect.https.html
new file mode 100644
index 0000000..07ea494
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/import-scripts-redirect.https.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for importScripts: redirect</title>
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(async t => {
+ const scope = 'resources/import-scripts-redirect';
+ await service_worker_unregister(t, scope);
+ let reg = await navigator.serviceWorker.register(
+ 'resources/import-scripts-redirect-worker.js', { scope: scope });
+ assert_not_equals(reg.installing, null, 'worker is installing');
+ await reg.unregister();
+ }, 'importScripts() supports redirects');
+
+promise_test(async t => {
+ const scope = 'resources/import-scripts-redirect';
+ await service_worker_unregister(t, scope);
+ let reg = await navigator.serviceWorker.register(
+ 'resources/import-scripts-redirect-worker.js', { scope: scope });
+ assert_not_equals(reg.installing, null, 'before update');
+ await wait_for_state(t, reg.installing, 'activated');
+ await Promise.all([
+ wait_for_update(t, reg),
+ reg.update()
+ ]);
+ assert_not_equals(reg.installing, null, 'after update');
+ await reg.unregister();
+ },
+ "an imported script redirects, and the body changes during the update check");
+
+promise_test(async t => {
+ const key = token();
+ const scope = 'resources/import-scripts-redirect';
+ await service_worker_unregister(t, scope);
+ let reg = await navigator.serviceWorker.register(
+ `resources/import-scripts-redirect-on-second-time-worker.js?Key=${key}`,
+ { scope });
+ t.add_cleanup(() => reg.unregister());
+
+ assert_not_equals(reg.installing, null, 'before update');
+ await wait_for_state(t, reg.installing, 'activated');
+ await Promise.all([
+ wait_for_update(t, reg),
+ reg.update()
+ ]);
+ assert_not_equals(reg.installing, null, 'after update');
+ },
+ "an imported script doesn't redirect initially, then redirects during " +
+ "the update check and the body changes");
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/import-scripts-resource-map.https.html b/third_party/web_platform_tests/service-workers/service-worker/import-scripts-resource-map.https.html
new file mode 100644
index 0000000..4742bd0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/import-scripts-resource-map.https.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Tests for importScripts: script resource map</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+ <script>
+ // This test registers a worker that imports a script multiple times. The
+ // script should be stored on the first import and thereafter that stored
+ // script should be loaded. The worker asserts that the stored script was
+ // loaded; if the assert fails then registration fails.
+
+ promise_test(async t => {
+ const SCOPE = "resources/import-scripts-resource-map";
+ const SCRIPT = "resources/import-scripts-resource-map-worker.js";
+ await service_worker_unregister(t, SCOPE);
+ const registration = await navigator.serviceWorker.register(SCRIPT, {
+ scope: SCOPE
+ });
+ await registration.unregister();
+ }, "import the same script URL multiple times");
+
+ promise_test(async t => {
+ const SCOPE = "resources/import-scripts-diff-resource-map";
+ const SCRIPT = "resources/import-scripts-diff-resource-map-worker.js";
+ await service_worker_unregister(t, SCOPE);
+ const registration = await navigator.serviceWorker.register(SCRIPT, {
+ scope: SCOPE
+ });
+ await registration.unregister();
+ }, "call importScripts() with multiple arguments");
+ </script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/import-scripts-updated-flag.https.html b/third_party/web_platform_tests/service-workers/service-worker/import-scripts-updated-flag.https.html
new file mode 100644
index 0000000..09b4496
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/import-scripts-updated-flag.https.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for importScripts: import scripts updated flag</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// This test registers a worker that calls importScripts at various stages of
+// service worker lifetime. The sub-tests trigger subsequent `importScript`
+// invocations via the `message` event.
+
+var register;
+
+function post_and_wait_for_reply(worker, message) {
+ return new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => { resolve(e.data); };
+ worker.postMessage(message);
+ });
+}
+
+promise_test(function(t) {
+ const scope = 'resources/import-scripts-updated-flag';
+ let registration;
+
+ register = service_worker_unregister_and_register(
+ t, 'resources/import-scripts-updated-flag-worker.js', scope)
+ .then(r => {
+ registration = r;
+ add_completion_callback(() => { registration.unregister(); });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ // This test should not be considered complete until after the
+ // service worker has been unregistered. Currently, `testharness.js`
+ // does not support asynchronous global "tear down" logic, so this
+ // must be expressed using a dedicated `promise_test`. Because the
+ // other sub-tests in this file are declared synchronously, this test
+ // will be the final test executed.
+ promise_test(function(t) {
+ return registration.unregister();
+ });
+
+ return registration.active;
+ });
+
+ return register;
+ }, 'initialize global state');
+
+promise_test(t => {
+ return register
+ .then(function(worker) {
+ return post_and_wait_for_reply(worker, 'root-and-message');
+ })
+ .then(result => {
+ assert_equals(result.error, null);
+ assert_equals(result.value, 'root-and-message');
+ });
+ }, 'import script previously imported at worker evaluation time');
+
+promise_test(t => {
+ return register
+ .then(function(worker) {
+ return post_and_wait_for_reply(worker, 'install-and-message');
+ })
+ .then(result => {
+ assert_equals(result.error, null);
+ assert_equals(result.value, 'install-and-message');
+ });
+ }, 'import script previously imported at worker install time');
+
+promise_test(t => {
+ return register
+ .then(function(worker) {
+ return post_and_wait_for_reply(worker, 'message');
+ })
+ .then(result => {
+ assert_equals(result.error, 'NetworkError');
+ assert_equals(result.value, null);
+ });
+ }, 'import script not previously imported');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/indexeddb.https.html b/third_party/web_platform_tests/service-workers/service-worker/indexeddb.https.html
new file mode 100644
index 0000000..be9be49
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/indexeddb.https.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<title>Service Worker: Indexed DB</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function readDB() {
+ return new Promise(function(resolve, reject) {
+ var openRequest = indexedDB.open('db');
+
+ openRequest.onerror = reject;
+ openRequest.onsuccess = function() {
+ var db = openRequest.result;
+ var tx = db.transaction('store');
+ var store = tx.objectStore('store');
+ var getRequest = store.get('key');
+
+ getRequest.onerror = function() {
+ db.close();
+ reject(getRequest.error);
+ };
+ getRequest.onsuccess = function() {
+ db.close();
+ resolve(getRequest.result);
+ };
+ };
+ });
+}
+
+function send(worker, action) {
+ return new Promise(function(resolve, reject) {
+ var messageChannel = new MessageChannel();
+ messageChannel.port1.onmessage = function(event) {
+ if (event.data.type === 'error') {
+ reject(event.data.reason);
+ }
+
+ resolve();
+ };
+
+ worker.postMessage(
+ {action: action, port: messageChannel.port2},
+ [messageChannel.port2]);
+ });
+}
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html';
+
+ return service_worker_unregister_and_register(
+ t, 'resources/indexeddb-worker.js', scope)
+ .then(function(registration) {
+ var worker = registration.installing;
+
+ promise_test(function() {
+ return registration.unregister();
+ }, 'clean up: registration');
+
+ return send(worker, 'create')
+ .then(function() {
+ promise_test(function() {
+ return new Promise(function(resolve, reject) {
+ var delete_request = indexedDB.deleteDatabase('db');
+
+ delete_request.onsuccess = resolve;
+ delete_request.onerror = reject;
+ });
+ }, 'clean up: database');
+ })
+ .then(readDB)
+ .then(function(value) {
+ assert_equals(
+ value, 'value',
+ 'The get() result should match what the worker put().');
+ });
+ });
+ }, 'Verify Indexed DB operation in a Service Worker');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/install-event-type.https.html b/third_party/web_platform_tests/service-workers/service-worker/install-event-type.https.html
new file mode 100644
index 0000000..7e74af8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/install-event-type.https.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function wait_for_install_event(worker) {
+ return new Promise(function(resolve) {
+ worker.addEventListener('statechange', function(event) {
+ if (worker.state == 'installed')
+ resolve(true);
+ else if (worker.state == 'redundant')
+ resolve(false);
+ });
+ });
+}
+
+promise_test(function(t) {
+ var script = 'resources/install-event-type-worker.js';
+ var scope = 'resources/install-event-type';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ return wait_for_install_event(registration.installing);
+ })
+ .then(function(did_install) {
+ assert_true(did_install, 'The worker was installed');
+ })
+ }, 'install event type');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/installing.https.html b/third_party/web_platform_tests/service-workers/service-worker/installing.https.html
new file mode 100644
index 0000000..0f257b6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/installing.https.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<title>ServiceWorker: navigator.serviceWorker.installing</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const SCRIPT = 'resources/empty-worker.js';
+const SCOPE = 'resources/blank.html';
+
+// "installing" is set
+promise_test(async t => {
+
+ t.add_cleanup(async() => {
+ if (frame)
+ frame.remove();
+ if (registration)
+ await registration.unregister();
+ });
+
+ await service_worker_unregister(t, SCOPE);
+ const frame = await with_iframe(SCOPE);
+ const registration =
+ await navigator.serviceWorker.register(SCRIPT, {scope: SCOPE});
+ const container = frame.contentWindow.navigator.serviceWorker;
+ assert_equals(container.controller, null, 'controller');
+ assert_equals(registration.active, null, 'registration.active');
+ assert_equals(registration.waiting, null, 'registration.waiting');
+ assert_equals(registration.installing.scriptURL, normalizeURL(SCRIPT),
+ 'registration.installing.scriptURL');
+ // FIXME: Add a test for a frame created after installation.
+ // Should the existing frame ("frame") block activation?
+}, 'installing is set');
+
+// Tests that The ServiceWorker objects returned from installing attribute getter
+// that represent the same service worker are the same objects.
+promise_test(async t => {
+ const registration1 =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ const registration2 = await navigator.serviceWorker.getRegistration(SCOPE);
+ assert_equals(registration1.installing, registration2.installing,
+ 'ServiceWorkerRegistration.installing should return the ' +
+ 'same object');
+ await registration1.unregister();
+}, 'The ServiceWorker objects returned from installing attribute getter that ' +
+ 'represent the same service worker are the same objects');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/interface-requirements-sw.https.html b/third_party/web_platform_tests/service-workers/service-worker/interface-requirements-sw.https.html
new file mode 100644
index 0000000..eef868c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/interface-requirements-sw.https.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<title>Service Worker Global Scope Interfaces</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+// interface-requirements-worker.sub.js checks additional interface
+// requirements, on top of the basic IDL that is validated in
+// service-workers/idlharness.any.js
+service_worker_test(
+ 'resources/interface-requirements-worker.sub.js',
+ 'Interfaces and attributes in ServiceWorkerGlobalScope');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/invalid-blobtype.https.html b/third_party/web_platform_tests/service-workers/service-worker/invalid-blobtype.https.html
new file mode 100644
index 0000000..1c5920f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/invalid-blobtype.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: respondWith with header value containing a null byte</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var SCOPE = 'resources/invalid-blobtype-iframe.https.html';
+ var SCRIPT = 'resources/invalid-blobtype-worker.js';
+ var host_info = get_host_info();
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+
+ var channel = new MessageChannel();
+ var onMsg = new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ });
+
+ frame.contentWindow.postMessage({},
+ host_info['HTTPS_ORIGIN'],
+ [channel.port2]);
+ return onMsg;
+ })
+ .then(function(e) {
+ assert_equals(e.data.results, 'finish');
+ });
+ }, 'Verify the response of FetchEvent using XMLHttpRequest');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/invalid-header.https.html b/third_party/web_platform_tests/service-workers/service-worker/invalid-header.https.html
new file mode 100644
index 0000000..1bc9769
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/invalid-header.https.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<title>Service Worker: respondWith with header value containing a null byte</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var SCOPE = 'resources/invalid-header-iframe.https.html';
+ var SCRIPT = 'resources/invalid-header-worker.js';
+ var host_info = get_host_info();
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+
+ var channel = new MessageChannel();
+ var onMsg = new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ });
+ frame.contentWindow.postMessage({},
+ host_info['HTTPS_ORIGIN'],
+ [channel.port2]);
+ return onMsg;
+ })
+ .then(function(e) {
+ assert_equals(e.data.results, 'finish');
+ });
+ }, 'Verify the response of FetchEvent using XMLHttpRequest');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/iso-latin1-header.https.html b/third_party/web_platform_tests/service-workers/service-worker/iso-latin1-header.https.html
new file mode 100644
index 0000000..c27a5f4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/iso-latin1-header.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: respondWith with header value containing an ISO Latin 1 (ISO-8859-1 Character Set) string</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var SCOPE = 'resources/iso-latin1-header-iframe.html';
+ var SCRIPT = 'resources/iso-latin1-header-worker.js';
+ var host_info = get_host_info();
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ var channel = new MessageChannel();
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+
+ var onMsg = new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ });
+
+ frame.contentWindow.postMessage({},
+ host_info['HTTPS_ORIGIN'],
+ [channel.port2]);
+ return onMsg;
+ })
+ .then(function(e) {
+ assert_equals(e.data.results, 'finish');
+ });
+ }, 'Verify the response of FetchEvent using XMLHttpRequest');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/local-url-inherit-controller.https.html b/third_party/web_platform_tests/service-workers/service-worker/local-url-inherit-controller.https.html
new file mode 100644
index 0000000..6702abc
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/local-url-inherit-controller.https.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<title>Service Worker: local URL windows and workers inherit controller</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const SCRIPT = 'resources/local-url-inherit-controller-worker.js';
+const SCOPE = 'resources/local-url-inherit-controller-frame.html';
+
+async function doAsyncTest(t, opts) {
+ let name = `${opts.scheme}-${opts.child}-${opts.check}`;
+ let scope = SCOPE + '?name=' + name;
+ let reg = await service_worker_unregister_and_register(t, SCRIPT, scope);
+ add_completion_callback(_ => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ let frame = await with_iframe(scope);
+ add_completion_callback(_ => frame.remove());
+ assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller, null,
+ 'frame should be controlled');
+
+ let result = await frame.contentWindow.checkChildController(opts);
+ result = result.data;
+
+ let expect = 'unexpected';
+ if (opts.check === 'controller') {
+ expect = opts.expect === 'inherit'
+ ? frame.contentWindow.navigator.serviceWorker.controller.scriptURL
+ : null;
+ } else if (opts.check === 'fetch') {
+ // The service worker FetchEvent handler will provide an "intercepted"
+ // body. If the local URL ends up with an opaque origin and is not
+ // intercepted then it will get an opaque Response. In that case it
+ // should see an empty string body.
+ expect = opts.expect === 'intercept' ? 'intercepted' : '';
+ }
+
+ assert_equals(result, expect,
+ `${opts.scheme} URL ${opts.child} should ${opts.expect} ${opts.check}`);
+}
+
+promise_test(function(t) {
+ return doAsyncTest(t, {
+ scheme: 'blob',
+ child: 'iframe',
+ check: 'controller',
+ expect: 'inherit',
+ });
+}, 'Same-origin blob URL iframe should inherit service worker controller.');
+
+promise_test(function(t) {
+ return doAsyncTest(t, {
+ scheme: 'blob',
+ child: 'iframe',
+ check: 'fetch',
+ expect: 'intercept',
+ });
+}, 'Same-origin blob URL iframe should intercept fetch().');
+
+promise_test(function(t) {
+ return doAsyncTest(t, {
+ scheme: 'blob',
+ child: 'worker',
+ check: 'controller',
+ expect: 'inherit',
+ });
+}, 'Same-origin blob URL worker should inherit service worker controller.');
+
+promise_test(function(t) {
+ return doAsyncTest(t, {
+ scheme: 'blob',
+ child: 'worker',
+ check: 'fetch',
+ expect: 'intercept',
+ });
+}, 'Same-origin blob URL worker should intercept fetch().');
+
+promise_test(function(t) {
+ return doAsyncTest(t, {
+ scheme: 'data',
+ child: 'iframe',
+ check: 'fetch',
+ expect: 'not intercept',
+ });
+}, 'Data URL iframe should not intercept fetch().');
+
+promise_test(function(t) {
+ // Data URLs should result in an opaque origin and should probably not
+ // have access to a cross-origin service worker. See:
+ //
+ // https://github.com/w3c/ServiceWorker/issues/1262
+ //
+ return doAsyncTest(t, {
+ scheme: 'data',
+ child: 'worker',
+ check: 'controller',
+ expect: 'not inherit',
+ });
+}, 'Data URL worker should not inherit service worker controller.');
+
+promise_test(function(t) {
+ return doAsyncTest(t, {
+ scheme: 'data',
+ child: 'worker',
+ check: 'fetch',
+ expect: 'not intercept',
+ });
+}, 'Data URL worker should not intercept fetch().');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/mime-sniffing.https.html b/third_party/web_platform_tests/service-workers/service-worker/mime-sniffing.https.html
new file mode 100644
index 0000000..8175bcd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/mime-sniffing.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<title>Service Worker: MIME sniffing</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ const SCOPE = 'resources/blank.html?mime-sniffing';
+ const SCRIPT = 'resources/mime-sniffing-worker.js';
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(registration => {
+ add_completion_callback(() => registration.unregister());
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(_ => with_iframe(SCOPE))
+ .then(frame => {
+ add_completion_callback(() => frame.remove());
+ assert_equals(frame.contentWindow.document.body.innerText, 'test');
+ const h1 = frame.contentWindow.document.getElementById('testid');
+ assert_equals(h1.innerText,'test');
+ });
+ }, 'The response from service worker should be correctly MIME siniffed.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/multi-globals/current/current.https.html b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/current/current.https.html
new file mode 100644
index 0000000..82a48d4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/current/current.https.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Current page used as a test helper</title>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/multi-globals/current/test-sw.js b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/current/test-sw.js
new file mode 100644
index 0000000..e673292
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/current/test-sw.js
@@ -0,0 +1 @@
+// Service worker for current/
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/multi-globals/incumbent/incumbent.https.html b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/incumbent/incumbent.https.html
new file mode 100644
index 0000000..4585f15
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/incumbent/incumbent.https.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<title>Incumbent page used as a test helper</title>
+
+<iframe src="../current/current.https.html" id="c"></iframe>
+<iframe src="../relevant/relevant.https.html" id="r"></iframe>
+
+<script>
+'use strict';
+
+const current = document.querySelector('#c').contentWindow;
+const relevant = document.querySelector('#r').contentWindow;
+
+window.testRegister = options => {
+ return current.navigator.serviceWorker.register.call(relevant.navigator.serviceWorker, 'test-sw.js', options);
+};
+
+window.testGetRegistration = () => {
+ return current.navigator.serviceWorker.getRegistration.call(relevant.navigator.serviceWorker, 'test-sw.js');
+};
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/multi-globals/incumbent/test-sw.js b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/incumbent/test-sw.js
new file mode 100644
index 0000000..e2a0e93
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/incumbent/test-sw.js
@@ -0,0 +1 @@
+// Service worker for incumbent/
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/multi-globals/relevant/relevant.https.html b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/relevant/relevant.https.html
new file mode 100644
index 0000000..44f42ed
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/relevant/relevant.https.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Relevant page used as a test helper</title>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/multi-globals/relevant/test-sw.js b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/relevant/test-sw.js
new file mode 100644
index 0000000..ff44cdf
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/relevant/test-sw.js
@@ -0,0 +1 @@
+// Service worker for relevant/
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/multi-globals/test-sw.js b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/test-sw.js
new file mode 100644
index 0000000..ce3c940
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/test-sw.js
@@ -0,0 +1 @@
+// Service worker for /
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/multi-globals/url-parsing.https.html b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/url-parsing.https.html
new file mode 100644
index 0000000..b9dfe36
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/multi-globals/url-parsing.https.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<title>register()/getRegistration() URL parsing, with multiple globals in play</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/spec/service_worker/index.html#service-worker-container-register-method">
+<link rel="help" href="https://w3c.github.io/ServiceWorker/spec/service_worker/index.html#service-worker-container-getregistration-method">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<script src="/resources/testharness.js"></script>
+<script src="../resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+
+<!-- This is the entry global -->
+
+<iframe src="incumbent/incumbent.https.html"></iframe>
+
+<script>
+'use strict';
+
+const loadPromise = new Promise(resolve => {
+ window.addEventListener('load', () => resolve());
+});
+
+promise_test(t => {
+ let registration;
+
+ return loadPromise.then(() => {
+ return frames[0].testRegister();
+ }).then(r => {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ }).then(_ => {
+ assert_equals(registration.active.scriptURL, normalizeURL('relevant/test-sw.js'), 'the script URL should be parsed against the relevant global');
+ assert_equals(registration.scope, normalizeURL('relevant/'), 'the default scope URL should be parsed against the parsed script URL');
+
+ return registration.unregister();
+ });
+}, 'register should use the relevant global of the object it was called on to resolve the script URL and the default scope URL');
+
+promise_test(t => {
+ let registration;
+
+ return loadPromise.then(() => {
+ return frames[0].testRegister({ scope: 'scope' });
+ }).then(r => {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ }).then(_ => {
+ assert_equals(registration.active.scriptURL, normalizeURL('relevant/test-sw.js'), 'the script URL should be parsed against the relevant global');
+ assert_equals(registration.scope, normalizeURL('relevant/scope'), 'the given scope URL should be parsed against the relevant global');
+
+ return registration.unregister();
+ });
+}, 'register should use the relevant global of the object it was called on to resolve the script URL and the given scope URL');
+
+promise_test(t => {
+ let registration;
+
+ return loadPromise.then(() => {
+ return navigator.serviceWorker.register(normalizeURL('relevant/test-sw.js'));
+ }).then(r => {
+ registration = r;
+ return frames[0].testGetRegistration();
+ })
+ .then(gottenRegistration => {
+ assert_not_equals(registration, null, 'the registration should not be null');
+ assert_not_equals(gottenRegistration, null, 'the registration from the other frame should not be null');
+ assert_equals(gottenRegistration.scope, registration.scope,
+ 'the retrieved registration\'s scope should be equal to the original\'s scope');
+
+ return registration.unregister();
+ });
+}, 'getRegistration should use the relevant global of the object it was called on to resolve the script URL');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/multipart-image.https.html b/third_party/web_platform_tests/service-workers/service-worker/multipart-image.https.html
new file mode 100644
index 0000000..00c20d2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/multipart-image.https.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<title>Tests for cross-origin multipart image returned by service worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+
+<script>
+// This tests loading a multipart image via service worker. The service worker responds with
+// an opaque or a non-opaque response. The content of opaque response should not be readable.
+
+const script = 'resources/multipart-image-worker.js';
+const scope = 'resources/multipart-image-iframe.html';
+let frame;
+
+function check_image_data(data) {
+ assert_equals(data[0], 255);
+ assert_equals(data[1], 0);
+ assert_equals(data[2], 0);
+ assert_equals(data[3], 255);
+}
+
+promise_test(t => {
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ promise_test(() => {
+ if (frame) {
+ frame.remove();
+ }
+ return registration.unregister();
+ }, 'restore global state');
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => with_iframe(scope))
+ .then(f => {
+ frame = f;
+ });
+ }, 'initialize global state');
+
+promise_test(t => {
+ return frame.contentWindow.load_multipart_image('same-origin-multipart-image')
+ .then(img => frame.contentWindow.get_image_data(img))
+ .then(img_data => {
+ check_image_data(img_data.data);
+ });
+ }, 'same-origin multipart image via SW should be readable');
+
+promise_test(t => {
+ return frame.contentWindow.load_multipart_image('cross-origin-multipart-image-with-cors-approved')
+ .then(img => frame.contentWindow.get_image_data(img))
+ .then(img_data => {
+ check_image_data(img_data.data);
+ });
+ }, 'cross-origin multipart image via SW with approved CORS should be readable');
+
+promise_test(t => {
+ return frame.contentWindow.load_multipart_image('cross-origin-multipart-image-with-no-cors')
+ .then(img => {
+ assert_throws_dom('SecurityError', frame.contentWindow.DOMException,
+ () => frame.contentWindow.get_image_data(img));
+ });
+ }, 'cross-origin multipart image with no-cors via SW should not be readable');
+
+promise_test(t => {
+ const promise = frame.contentWindow.load_multipart_image('cross-origin-multipart-image-with-cors-rejected');
+ return promise_rejects_dom(t, 'NetworkError', frame.contentWindow.DOMException, promise);
+ }, 'cross-origin multipart image via SW with rejected CORS should fail to load');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/multiple-register.https.html b/third_party/web_platform_tests/service-workers/service-worker/multiple-register.https.html
new file mode 100644
index 0000000..752e132
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/multiple-register.https.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var worker_url = 'resources/empty-worker.js';
+
+async_test(function(t) {
+ var scope = 'resources/scope/subsequent-register-from-same-window';
+ var registration;
+
+ service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ return navigator.serviceWorker.register(worker_url, { scope: scope });
+ })
+ .then(function(new_registration) {
+ assert_equals(new_registration, registration,
+ 'register should resolve to the same registration');
+ assert_equals(new_registration.active, registration.active,
+ 'register should resolve to the same worker');
+ assert_equals(new_registration.active.state, 'activated',
+ 'the worker should be in state "activated"');
+ return registration.unregister();
+ })
+ .then(function() { t.done(); })
+ .catch(unreached_rejection(t));
+}, 'Subsequent registrations resolve to the same registration object');
+
+async_test(function(t) {
+ var scope = 'resources/scope/subsequent-register-from-different-iframe';
+ var frame;
+ var registration;
+
+ service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() { return with_iframe('resources/404.py'); })
+ .then(function(f) {
+ frame = f;
+ return frame.contentWindow.navigator.serviceWorker.register(
+ 'empty-worker.js',
+ { scope: 'scope/subsequent-register-from-different-iframe' });
+ })
+ .then(function(new_registration) {
+ assert_not_equals(
+ registration, new_registration,
+ 'register should resolve to a different registration');
+ assert_equals(
+ registration.scope, new_registration.scope,
+ 'registrations should have the same scope');
+
+ assert_equals(
+ registration.installing, null,
+ 'installing worker should be null');
+ assert_equals(
+ new_registration.installing, null,
+ 'installing worker should be null');
+ assert_equals(
+ registration.waiting, null,
+ 'waiting worker should be null')
+ assert_equals(
+ new_registration.waiting, null,
+ 'waiting worker should be null')
+
+ assert_not_equals(
+ registration.active, new_registration.active,
+ 'registration should have a different active worker');
+ assert_equals(
+ registration.active.scriptURL,
+ new_registration.active.scriptURL,
+ 'active workers should have the same script URL');
+ assert_equals(
+ registration.active.state,
+ new_registration.active.state,
+ 'active workers should be in the same state');
+
+ frame.remove();
+ return registration.unregister();
+ })
+ .then(function() { t.done(); })
+ .catch(unreached_rejection(t));
+}, 'Subsequent registrations from a different iframe resolve to the ' +
+ 'different registration object but they refer to the same ' +
+ 'registration and workers');
+
+async_test(function(t) {
+ var scope = 'resources/scope/concurrent-register';
+
+ service_worker_unregister(t, scope)
+ .then(function() {
+ var promises = [];
+ for (var i = 0; i < 10; ++i) {
+ promises.push(navigator.serviceWorker.register(worker_url,
+ { scope: scope }));
+ }
+ return Promise.all(promises);
+ })
+ .then(function(registrations) {
+ registrations.forEach(function(registration) {
+ assert_equals(registration, registrations[0],
+ 'register should resolve to the same registration');
+ });
+ return registrations[0].unregister();
+ })
+ .then(function() { t.done(); })
+ .catch(unreached_rejection(t));
+}, 'Concurrent registrations resolve to the same registration object');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/multiple-update.https.html b/third_party/web_platform_tests/service-workers/service-worker/multiple-update.https.html
new file mode 100644
index 0000000..6a83f73
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/multiple-update.https.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<!-- In Bug 1217367, we will try to merge update events for same registration
+ if possible. This testcase is used to make sure the optimization algorithm
+ doesn't go wrong. -->
+<title>Service Worker: Trigger multiple updates</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var script = 'resources/update-nocookie-worker.py';
+ var scope = 'resources/scope/update';
+ var expected_url = normalizeURL(script);
+ var registration;
+
+ return service_worker_unregister_and_register(t, expected_url, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ // Test single update works before triggering multiple update events
+ return Promise.all([registration.update(),
+ wait_for_update(t, registration)]);
+ })
+ .then(function() {
+ assert_equals(registration.installing.scriptURL, expected_url,
+ 'new installing should be set after update resolves.');
+ assert_equals(registration.waiting, null,
+ 'waiting should still be null after update resolves.');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'active should still exist after update found.');
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'installing should be null after installing.');
+ if (registration.waiting) {
+ assert_equals(registration.waiting.scriptURL, expected_url,
+ 'waiting should be set after installing.');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'active should still exist after installing.');
+ return wait_for_state(t, registration.waiting, 'activated');
+ }
+ })
+ .then(function() {
+ // Test triggering multiple update events at the same time.
+ var promiseList = [];
+ const burstUpdateCount = 10;
+ for (var i = 0; i < burstUpdateCount; i++) {
+ promiseList.push(registration.update());
+ }
+ promiseList.push(wait_for_update(t, registration));
+ return Promise.all(promiseList);
+ })
+ .then(function() {
+ assert_equals(registration.installing.scriptURL, expected_url,
+ 'new installing should be set after update resolves.');
+ assert_equals(registration.waiting, null,
+ 'waiting should still be null after update resolves.');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'active should still exist after update found.');
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'installing should be null after installing.');
+ if (registration.waiting) {
+ assert_equals(registration.waiting.scriptURL, expected_url,
+ 'waiting should be set after installing.');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'active should still exist after installing.');
+ return wait_for_state(t, registration.waiting, 'activated');
+ }
+ })
+ .then(function() {
+ // Test update still works after handling update event burst.
+ return Promise.all([registration.update(),
+ wait_for_update(t, registration)]);
+ })
+ .then(function() {
+ assert_equals(registration.installing.scriptURL, expected_url,
+ 'new installing should be set after update resolves.');
+ assert_equals(registration.waiting, null,
+ 'waiting should be null after activated.');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'active should still exist after update found.');
+ });
+ }, 'Trigger multiple updates.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigate-window.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigate-window.https.html
new file mode 100644
index 0000000..46d32a4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigate-window.https.html
@@ -0,0 +1,151 @@
+<!DOCTYPE html>
+<title>Service Worker: Navigate a Window</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var host_info = get_host_info();
+var BASE_URL = host_info['HTTPS_ORIGIN'] + base_path();
+
+function wait_for_message(msg) {
+ return new Promise(function(resolve, reject) {
+ window.addEventListener('message', function onMsg(evt) {
+ if (evt.data.type === msg) {
+ resolve();
+ }
+ });
+ });
+}
+
+function with_window(url) {
+ var win = window.open(url);
+ return wait_for_message('LOADED').then(_ => win);
+}
+
+function navigate_window(win, url) {
+ win.location = url;
+ return wait_for_message('LOADED').then(_ => win);
+}
+
+function reload_window(win) {
+ win.location.reload();
+ return wait_for_message('LOADED').then(_ => win);
+}
+
+function go_back(win) {
+ win.history.back();
+ return wait_for_message('PAGESHOW').then(_ => win);
+}
+
+function go_forward(win) {
+ win.history.forward();
+ return wait_for_message('PAGESHOW').then(_ => win);
+}
+
+function get_clients(win, sw, opts) {
+ return new Promise((resolve, reject) => {
+ win.navigator.serviceWorker.addEventListener('message', function onMsg(evt) {
+ win.navigator.serviceWorker.removeEventListener('message', onMsg);
+ if (evt.data.type === 'success') {
+ resolve(evt.data.detail);
+ } else {
+ reject(evt.data.detail);
+ }
+ });
+ sw.postMessage({ type: 'GET_CLIENTS', opts: (opts || {}) });
+ });
+}
+
+function compare_urls(a, b) {
+ return a.url < b.url ? -1 : b.url < a.url ? 1 : 0;
+}
+
+function validate_window(win, url, opts) {
+ return win.navigator.serviceWorker.getRegistration(url)
+ .then(reg => {
+ // In order to compare service worker instances we need to
+ // make sure the DOM object is owned by the same global; the
+ // opened window in this case.
+ assert_equals(win.navigator.serviceWorker.controller, reg.active,
+ 'window should be controlled by service worker');
+ return get_clients(win, reg.active, opts);
+ })
+ .then(resultList => {
+ // We should always see our controlled window.
+ var expected = [
+ { url: url, frameType: 'auxiliary' }
+ ];
+ // If we are including uncontrolled windows, then we might see the
+ // test window itself and the test harness.
+ if (opts.includeUncontrolled) {
+ expected.push({ url: BASE_URL + 'navigate-window.https.html',
+ frameType: 'auxiliary' });
+ expected.push({
+ url: host_info['HTTPS_ORIGIN'] + '/testharness_runner.html',
+ frameType: 'top-level' });
+ }
+
+ assert_equals(resultList.length, expected.length,
+ 'expected number of clients');
+
+ expected.sort(compare_urls);
+ resultList.sort(compare_urls);
+
+ for (var i = 0; i < resultList.length; ++i) {
+ assert_equals(resultList[i].url, expected[i].url,
+ 'client should have expected url');
+ assert_equals(resultList[i].frameType, expected[i].frameType,
+ 'client should have expected frame type');
+ }
+ return win;
+ })
+}
+
+promise_test(function(t) {
+ var worker = BASE_URL + 'resources/navigate-window-worker.js';
+ var scope = BASE_URL + 'resources/loaded.html?navigate-window-controlled';
+ var url1 = scope + '&q=1';
+ var url2 = scope + '&q=2';
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(reg => wait_for_state(t, reg.installing, 'activated') )
+ .then(___ => with_window(url1))
+ .then(win => validate_window(win, url1, { includeUncontrolled: false }))
+ .then(win => navigate_window(win, url2))
+ .then(win => validate_window(win, url2, { includeUncontrolled: false }))
+ .then(win => go_back(win))
+ .then(win => validate_window(win, url1, { includeUncontrolled: false }))
+ .then(win => go_forward(win))
+ .then(win => validate_window(win, url2, { includeUncontrolled: false }))
+ .then(win => reload_window(win))
+ .then(win => validate_window(win, url2, { includeUncontrolled: false }))
+ .then(win => win.close())
+ .catch(unreached_rejection(t))
+ .then(___ => service_worker_unregister(t, scope))
+ }, 'Clients.matchAll() should not show an old window as controlled after ' +
+ 'it navigates.');
+
+promise_test(function(t) {
+ var worker = BASE_URL + 'resources/navigate-window-worker.js';
+ var scope = BASE_URL + 'resources/loaded.html?navigate-window-uncontrolled';
+ var url1 = scope + '&q=1';
+ var url2 = scope + '&q=2';
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(reg => wait_for_state(t, reg.installing, 'activated') )
+ .then(___ => with_window(url1))
+ .then(win => validate_window(win, url1, { includeUncontrolled: true }))
+ .then(win => navigate_window(win, url2))
+ .then(win => validate_window(win, url2, { includeUncontrolled: true }))
+ .then(win => go_back(win))
+ .then(win => validate_window(win, url1, { includeUncontrolled: true }))
+ .then(win => go_forward(win))
+ .then(win => validate_window(win, url2, { includeUncontrolled: true }))
+ .then(win => reload_window(win))
+ .then(win => validate_window(win, url2, { includeUncontrolled: true }))
+ .then(win => win.close())
+ .catch(unreached_rejection(t))
+ .then(___ => service_worker_unregister(t, scope))
+ }, 'Clients.matchAll() should not show an old window after it navigates.');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-headers.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-headers.https.html
new file mode 100644
index 0000000..a4b5203
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-headers.https.html
@@ -0,0 +1,819 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="timeout" content="long">
+<title>Service Worker: Navigation Post Request Origin Header</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<body>
+<script>
+'use strict';
+
+const script = new URL('./resources/fetch-rewrite-worker.js', self.location);
+const base = './resources/navigation-headers-server.py';
+const scope = base + '?with-sw';
+let registration;
+
+async function post_and_get_headers(t, form_host, method, swaction,
+ redirect_hosts=[]) {
+ if (swaction === 'navpreload') {
+ assert_true('navigationPreload' in registration,
+ 'navigation preload must be supported');
+ }
+ let target_string;
+ if (swaction === 'no-sw') {
+ target_string = base + '?no-sw';
+ } else if (swaction === 'fallback') {
+ target_string = `${scope}&ignore`;
+ } else {
+ target_string = `${scope}&${swaction}`;
+ }
+ let target = new URL(target_string, self.location);
+
+ for (let i = redirect_hosts.length - 1; i >= 0; --i) {
+ const redirect_url = new URL('./resources/redirect.py', self.location);
+ redirect_url.hostname = redirect_hosts[i];
+ redirect_url.search = `?Status=307&Redirect=${encodeURIComponent(target)}`;
+ target = redirect_url;
+ }
+
+ let popup_url_path;
+ if (method === 'GET') {
+ popup_url_path = './resources/location-setter.html';
+ } else if (method === 'POST') {
+ popup_url_path = './resources/form-poster.html';
+ }
+
+ const popup_url = new URL(popup_url_path, self.location);
+ popup_url.hostname = form_host;
+ popup_url.search = `?target=${encodeURIComponent(target.href)}`;
+
+ const message_promise = new Promise(resolve => {
+ self.addEventListener('message', evt => {
+ resolve(evt.data);
+ });
+ });
+
+ const frame = await with_iframe(popup_url);
+ t.add_cleanup(() => frame.remove());
+
+ return await message_promise;
+}
+
+const SAME_ORIGIN = new URL(self.location.origin);
+const SAME_SITE = new URL(get_host_info().HTTPS_REMOTE_ORIGIN);
+const CROSS_SITE = new URL(get_host_info().HTTPS_NOTSAMESITE_ORIGIN);
+
+promise_test(async t => {
+ registration = await service_worker_unregister_and_register(t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+ if (registration.navigationPreload)
+ await registration.navigationPreload.enable();
+}, 'Setup service worker');
+
+//
+// Origin and referer headers
+//
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'no-sw');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'GET Navigation, same-origin with no service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'no-sw');
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with no service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'passthrough');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'GET Navigation, same-origin with passthrough service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'passthrough');
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with passthrough service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'fallback');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'GET Navigation, same-origin with fallback service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'fallback');
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with fallback service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'navpreload');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'GET Navigation, same-origin with navpreload service worker sets correct ' +
+ 'origin and referer headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'change-request');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'GET Navigation, same-origin with service worker that changes the ' +
+ 'request sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'change-request');
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, same-origin with service worker that changes the ' +
+ 'request sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'no-sw');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'GET Navigation, same-site with no service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'no-sw');
+ assert_equals(result.origin, SAME_SITE.origin, 'origin header');
+ assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'POST Navigation, same-site with no service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'passthrough');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'GET Navigation, same-site with passthrough service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'passthrough');
+ assert_equals(result.origin, SAME_SITE.origin, 'origin header');
+ assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'POST Navigation, same-site with passthrough service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'fallback');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'GET Navigation, same-site with fallback service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'fallback');
+ assert_equals(result.origin, SAME_SITE.origin, 'origin header');
+ assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'POST Navigation, same-site with fallback service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'navpreload');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, SAME_SITE.href, 'referer header');
+}, 'GET Navigation, same-site with navpreload service worker sets correct ' +
+ 'origin and referer headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'change-request');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'GET Navigation, same-site with service worker that changes the ' +
+ 'request sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'change-request');
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, same-site with service worker that changes the ' +
+ 'request sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'no-sw');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'GET Navigation, cross-site with no service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'no-sw');
+ assert_equals(result.origin, CROSS_SITE.origin, 'origin header');
+ assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'POST Navigation, cross-site with no service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'passthrough');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'GET Navigation, cross-site with passthrough service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'passthrough');
+ assert_equals(result.origin, CROSS_SITE.origin, 'origin header');
+ assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'POST Navigation, cross-site with passthrough service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'fallback');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'GET Navigation, cross-site with fallback service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'fallback');
+ assert_equals(result.origin, CROSS_SITE.origin, 'origin header');
+ assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'POST Navigation, cross-site with fallback service worker sets correct ' +
+ 'origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'navpreload');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, CROSS_SITE.href, 'referer header');
+}, 'GET Navigation, cross-site with navpreload service worker sets correct ' +
+ 'origin and referer headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'change-request');
+ assert_equals(result.origin, 'not set', 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'GET Navigation, cross-site with service worker that changes the ' +
+ 'request sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'change-request');
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, cross-site with service worker that changes the ' +
+ 'request sets correct origin and referer headers.');
+
+//
+// Origin and referer header tests using redirects
+//
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'no-sw', [SAME_SITE.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with same-site redirect and no service worker ' +
+ 'sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'passthrough', [SAME_SITE.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with same-site redirect and passthrough service ' +
+ 'worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'fallback', [SAME_SITE.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with same-site redirect and fallback service ' +
+ 'worker sets correct origin and referer headers.');
+
+// There is no navpreload case because it does not work with POST requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'change-request', [SAME_SITE.hostname]);
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, same-origin with same-site redirect and change-request service ' +
+ 'worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'no-sw', [CROSS_SITE.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect and no service worker ' +
+ 'sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'passthrough', [CROSS_SITE.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect and passthrough service ' +
+ 'worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'fallback', [CROSS_SITE.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect and fallback service ' +
+ 'worker sets correct origin and referer headers.');
+
+// There is no navpreload case because it does not work with POST requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'change-request', [CROSS_SITE.hostname]);
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect and change-request service ' +
+ 'worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'no-sw', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and no service worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'passthrough', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and passthrough service worker sets correct origin and referer headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'fallback', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result.origin, 'null', 'origin header');
+ assert_equals(result.referer, SAME_ORIGIN.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and fallback service worker sets correct origin and referer headers.');
+
+// There is no navpreload case because it does not work with POST requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'change-request', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result.origin, SAME_ORIGIN.origin, 'origin header');
+ assert_equals(result.referer, script.href, 'referer header');
+}, 'POST Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and change-request service worker sets correct origin and referer headers.');
+
+//
+// Sec-Fetch-* Headers (separated since not all browsers implement them)
+//
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'no-sw');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with no service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'no-sw');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, same-origin with no service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'passthrough');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with passthrough service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'passthrough');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, same-origin with passthrough service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'fallback');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with fallback service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'fallback');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, same-origin with fallback service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'navpreload');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with navpreload service worker sets correct ' +
+ 'sec-fetch headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'change-request');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with service worker that changes the ' +
+ 'request sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'POST',
+ 'change-request');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, same-origin with service worker that changes the ' +
+ 'request sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'no-sw');
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-site with no service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'no-sw');
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, same-site with no service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'passthrough');
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-site with passthrough service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'passthrough');
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, same-site with passthrough service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'fallback');
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-site with fallback service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'fallback');
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, same-site with fallback service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'navpreload');
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-site with navpreload service worker sets correct ' +
+ 'sec-fetch headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'GET',
+ 'change-request');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-site with service worker that changes the ' +
+ 'request sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_SITE.hostname, 'POST',
+ 'change-request');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, same-site with service worker that changes the ' +
+ 'request sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'no-sw');
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, cross-site with no service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'no-sw');
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, cross-site with no service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'passthrough');
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, cross-site with passthrough service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'passthrough');
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, cross-site with passthrough service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'fallback');
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, cross-site with fallback service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'fallback');
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'POST Navigation, cross-site with fallback service worker sets correct ' +
+ 'sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'navpreload');
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, cross-site with navpreload service worker sets correct ' +
+ 'sec-fetch headers.');
+
+// There is no POST test for navpreload since the feature only supports GET
+// requests.
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'GET',
+ 'change-request');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, cross-site with service worker that changes the ' +
+ 'request sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, CROSS_SITE.hostname, 'POST',
+ 'change-request');
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'POST Navigation, cross-site with service worker that changes the ' +
+ 'request sets correct sec-fetch headers.');
+
+//
+// Sec-Fetch-* header tests using redirects
+//
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'no-sw', [SAME_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with same-site redirect and no service worker ' +
+ 'sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'passthrough', [SAME_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with same-site redirect and passthrough service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'fallback', [SAME_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with same-site redirect and fallback service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'navpreload', [SAME_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'same-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with same-site redirect and navpreload service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'change-request', [SAME_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with same-site redirect and change-request service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'no-sw', [CROSS_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect and no service worker ' +
+ 'sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'passthrough', [CROSS_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect and passthrough service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'fallback', [CROSS_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect and fallback service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'navpreload', [CROSS_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect and navpreload service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'change-request', [CROSS_SITE.hostname]);
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect and change-request service ' +
+ 'worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'no-sw', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and no service worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'passthrough', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and passthrough service worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'fallback', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and fallback service worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'navpreload', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result['sec-fetch-site'], 'cross-site', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'navigate', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'iframe', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and navpreload service worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ const result = await post_and_get_headers(t, SAME_ORIGIN.hostname, 'GET',
+ 'change-request', [CROSS_SITE.hostname,
+ SAME_ORIGIN.hostname]);
+ assert_equals(result['sec-fetch-site'], 'same-origin', 'sec-fetch-site header');
+ assert_equals(result['sec-fetch-mode'], 'same-origin', 'sec-fetch-mode header');
+ assert_equals(result['sec-fetch-dest'], 'empty', 'sec-fetch-dest header');
+}, 'GET Navigation, same-origin with cross-site redirect, same-origin redirect, ' +
+ 'and change-request service worker sets correct sec-fetch headers.');
+
+promise_test(async t => {
+ await registration.unregister();
+}, 'Cleanup service worker');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/broken-chunked-encoding.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/broken-chunked-encoding.https.html
new file mode 100644
index 0000000..ec74282
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/broken-chunked-encoding.https.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload with chunked encoding</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ var script = 'resources/broken-chunked-encoding-worker.js';
+ var scope = 'resources/broken-chunked-encoding-scope.asis';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ add_completion_callback(_ => registration.unregister());
+ var worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(_ => with_iframe(scope))
+ .then(frame => {
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'PASS: preloadResponse resolved');
+ });
+ }, 'FetchEvent#preloadResponse resolves even if the body is sent with broken chunked encoding.');
+
+promise_test(t => {
+ var script = 'resources/broken-chunked-encoding-worker.js';
+ var scope = 'resources/chunked-encoding-scope.py?use_broken_body';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ add_completion_callback(_ => registration.unregister());
+ var worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(_ => with_iframe(scope))
+ .then(frame => {
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ 'PASS: preloadResponse resolved');
+ });
+ }, 'FetchEvent#preloadResponse resolves even if the body is sent with broken chunked encoding with some delays');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/chunked-encoding.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/chunked-encoding.https.html
new file mode 100644
index 0000000..830ce32
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/chunked-encoding.https.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload with chunked encoding</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ var script = 'resources/chunked-encoding-worker.js';
+ var scope = 'resources/chunked-encoding-scope.py';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ add_completion_callback(_ => registration.unregister());
+ var worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(_ => with_iframe(scope))
+ .then(frame => {
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ '0123456789');
+ });
+ }, 'Navigation Preload must work with chunked encoding.');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/empty-preload-response-body.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/empty-preload-response-body.https.html
new file mode 100644
index 0000000..7e8aacd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/empty-preload-response-body.https.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload empty response body</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ var script = 'resources/empty-preload-response-body-worker.js';
+ var scope = 'resources/empty-preload-response-body-scope.html';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ add_completion_callback(_ => registration.unregister());
+ var worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(_ => with_iframe(scope))
+ .then(frame => {
+ assert_equals(
+ frame.contentDocument.body.textContent,
+ '[]');
+ });
+ }, 'Navigation Preload empty response body.');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/get-state.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/get-state.https.html
new file mode 100644
index 0000000..08e2f49
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/get-state.https.html
@@ -0,0 +1,217 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>NavigationPreloadManager.getState</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script src="resources/helpers.js"></script>
+<body>
+<script>
+function post_and_wait_for_reply(worker, message) {
+ return new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => { resolve(e.data); };
+ worker.postMessage(message);
+ });
+}
+
+promise_test(t => {
+ const scope = '../resources/get-state';
+ const script = '../resources/empty-worker.js';
+ var np;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ np = r.navigationPreload;
+ add_completion_callback(() => r.unregister());
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(() => np.getState())
+ .then(state => {
+ expect_navigation_preload_state(state, false, 'true', 'default state');
+ return np.enable();
+ })
+ .then(result => {
+ assert_equals(result, undefined,
+ 'enable() should resolve to undefined');
+ return np.getState();
+ })
+ .then(state => {
+ expect_navigation_preload_state(state, true, 'true',
+ 'state after enable()');
+ return np.disable();
+ })
+ .then(result => {
+ assert_equals(result, undefined,
+ 'disable() should resolve to undefined');
+ return np.getState();
+ })
+ .then(state => {
+ expect_navigation_preload_state(state, false, 'true',
+ 'state after disable()');
+ return np.setHeaderValue('dreams that cannot be');
+ })
+ .then(result => {
+ assert_equals(result, undefined,
+ 'setHeaderValue() should resolve to undefined');
+ return np.getState();
+ })
+ .then(state => {
+ expect_navigation_preload_state(state, false, 'dreams that cannot be',
+ 'state after setHeaderValue()');
+ return np.setHeaderValue('').then(() => np.getState());
+ })
+ .then(state => {
+ expect_navigation_preload_state(state, false, '',
+ 'after setHeaderValue to empty string');
+ return np.setHeaderValue(null).then(() => np.getState());
+ })
+ .then(state => {
+ expect_navigation_preload_state(state, false, 'null',
+ 'after setHeaderValue to null');
+ return promise_rejects_js(t,
+ TypeError,
+ np.setHeaderValue('what\uDC00\uD800this'),
+ 'setHeaderValue() should throw if passed surrogates');
+ })
+ .then(() => {
+ return promise_rejects_js(t,
+ TypeError,
+ np.setHeaderValue('zer\0o'),
+ 'setHeaderValue() should throw if passed \\0');
+ })
+ .then(() => {
+ return promise_rejects_js(t,
+ TypeError,
+ np.setHeaderValue('\rcarriage'),
+ 'setHeaderValue() should throw if passed \\r');
+ })
+ .then(() => {
+ return promise_rejects_js(t,
+ TypeError,
+ np.setHeaderValue('newline\n'),
+ 'setHeaderValue() should throw if passed \\n');
+ })
+ .then(() => {
+ return promise_rejects_js(t,
+ TypeError,
+ np.setHeaderValue(),
+ 'setHeaderValue() should throw if passed undefined');
+ })
+ .then(() => np.enable().then(() => np.getState()))
+ .then(state => {
+ expect_navigation_preload_state(state, true, 'null',
+ 'enable() should not change header');
+ });
+ }, 'getState');
+
+// This test sends commands to a worker to call enable()/disable()/getState().
+// It checks the results from the worker and verifies that they match the
+// navigation preload state accessible from the page.
+promise_test(t => {
+ const scope = 'resources/get-state-worker';
+ const script = 'resources/get-state-worker.js';
+ var worker;
+ var registration;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ registration = r;
+ add_completion_callback(() => registration.unregister());
+ worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(() => {
+ // Call getState().
+ return post_and_wait_for_reply(worker, 'getState');
+ })
+ .then(data => {
+ return Promise.all([data, registration.navigationPreload.getState()]);
+ })
+ .then(states => {
+ expect_navigation_preload_state(states[0], false, 'true',
+ 'default state (from worker)');
+ expect_navigation_preload_state(states[1], false, 'true',
+ 'default state (from page)');
+ // Call enable() and then getState().
+ return post_and_wait_for_reply(worker, 'enable');
+ })
+ .then(data => {
+ assert_equals(data, undefined, 'enable() should resolve to undefined');
+ return Promise.all([
+ post_and_wait_for_reply(worker, 'getState'),
+ registration.navigationPreload.getState()
+ ]);
+ })
+ .then(states => {
+ expect_navigation_preload_state(states[0], true, 'true',
+ 'state after enable() (from worker)');
+ expect_navigation_preload_state(states[1], true, 'true',
+ 'state after enable() (from page)');
+ // Call disable() and then getState().
+ return post_and_wait_for_reply(worker, 'disable');
+ })
+ .then(data => {
+ assert_equals(data, undefined,
+ '.disable() should resolve to undefined');
+ return Promise.all([
+ post_and_wait_for_reply(worker, 'getState'),
+ registration.navigationPreload.getState()
+ ]);
+ })
+ .then(states => {
+ expect_navigation_preload_state(states[0], false, 'true',
+ 'state after disable() (from worker)');
+ expect_navigation_preload_state(states[1], false, 'true',
+ 'state after disable() (from page)');
+ return post_and_wait_for_reply(worker, 'setHeaderValue');
+ })
+ .then(data => {
+ assert_equals(data, undefined,
+ '.setHeaderValue() should resolve to undefined');
+ return Promise.all([
+ post_and_wait_for_reply(worker, 'getState'),
+ registration.navigationPreload.getState()]);
+ })
+ .then(states => {
+ expect_navigation_preload_state(
+ states[0], false, 'insightful',
+ 'state after setHeaderValue() (from worker)');
+ expect_navigation_preload_state(
+ states[1], false, 'insightful',
+ 'state after setHeaderValue() (from page)');
+ });
+ }, 'getState from a worker');
+
+// This tests navigation preload API when there is no active worker. It calls
+// the API from the main page and then from the worker itself.
+promise_test(t => {
+ const scope = 'resources/wait-for-activate-worker';
+ const script = 'resources/wait-for-activate-worker.js';
+ var registration;
+ var np;
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ registration = r;
+ np = registration.navigationPreload;
+ add_completion_callback(() => registration.unregister());
+ return Promise.all([
+ promise_rejects_dom(
+ t, 'InvalidStateError', np.enable(),
+ 'enable should reject if there is no active worker'),
+ promise_rejects_dom(
+ t, 'InvalidStateError', np.disable(),
+ 'disable should reject if there is no active worker'),
+ promise_rejects_dom(
+ t, 'InvalidStateError', np.setHeaderValue('umm'),
+ 'setHeaderValue should reject if there is no active worker')]);
+ })
+ .then(() => np.getState())
+ .then(state => {
+ expect_navigation_preload_state(state, false, 'true',
+ 'state before activation');
+ return post_and_wait_for_reply(registration.installing, 'ping');
+ })
+ .then(result => assert_equals(result, 'PASS'));
+ }, 'no active worker');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/navigationPreload.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/navigationPreload.https.html
new file mode 100644
index 0000000..392e5c1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/navigationPreload.https.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>ServiceWorker: navigator.serviceWorker.navigationPreload</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script src="resources/helpers.js"></script>
+<script>
+promise_test(async t => {
+ const SCRIPT = '../resources/empty-worker.js';
+ const SCOPE = '../resources/navigationpreload';
+ const registration =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ const navigationPreload = registration.navigationPreload;
+ assert_true(navigationPreload instanceof NavigationPreloadManager,
+ 'ServiceWorkerRegistration.navigationPreload');
+ await registration.unregister();
+}, "The navigationPreload attribute must return service worker " +
+ "registration's NavigationPreloadManager object.");
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/redirect.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/redirect.https.html
new file mode 100644
index 0000000..5970f05
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/redirect.https.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload redirect response</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+
+function check_opaqueredirect(response_info, scope) {
+ assert_equals(response_info.type, 'opaqueredirect');
+ assert_equals(response_info.url, '' + new URL(scope, location));
+ assert_equals(response_info.status, 0);
+ assert_equals(response_info.ok, false);
+ assert_equals(response_info.statusText, '');
+ assert_equals(response_info.headers.length, 0);
+}
+
+function redirect_response_test(t, scope, expected_body, expected_urls) {
+ var script = 'resources/redirect-worker.js';
+ var registration;
+ var message_resolvers = [];
+ function wait_for_message(count) {
+ var promises = [];
+ message_resolvers = [];
+ for (var i = 0; i < count; ++i) {
+ promises.push(new Promise(resolve => message_resolvers.push(resolve)));
+ }
+ return promises;
+ }
+ function on_message(e) {
+ var resolve = message_resolvers.shift();
+ if (resolve)
+ resolve(e.data);
+ }
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(reg => {
+ registration = reg;
+ add_completion_callback(_ => registration.unregister());
+ var worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(_ => with_iframe(scope + '&base'))
+ .then(frame => {
+ assert_equals(frame.contentDocument.body.textContent, 'OK');
+ frame.contentWindow.navigator.serviceWorker.onmessage = on_message;
+ return Promise.all(wait_for_message(expected_urls.length)
+ .concat(with_iframe(scope)));
+ })
+ .then(results => {
+ var frame = results[expected_urls.length];
+ assert_equals(frame.contentDocument.body.textContent, expected_body);
+ for (var i = 0; i < expected_urls.length; ++i) {
+ check_opaqueredirect(results[i], expected_urls[i]);
+ }
+ frame.remove();
+ return registration.unregister();
+ });
+}
+
+promise_test(t => {
+ return redirect_response_test(
+ t,
+ 'resources/redirect-scope.py?type=normal',
+ 'redirected\n',
+ ['resources/redirect-scope.py?type=normal']);
+ }, 'Navigation Preload redirect response.');
+
+promise_test(t => {
+ return redirect_response_test(
+ t,
+ 'resources/redirect-scope.py?type=no-location',
+ '',
+ ['resources/redirect-scope.py?type=no-location']);
+ }, 'Navigation Preload no-location redirect response.');
+
+promise_test(t => {
+ return redirect_response_test(
+ t,
+ 'resources/redirect-scope.py?type=no-location-with-body',
+ 'BODY',
+ ['resources/redirect-scope.py?type=no-location-with-body']);
+ }, 'Navigation Preload no-location redirect response with body.');
+
+promise_test(t => {
+ return redirect_response_test(
+ t,
+ 'resources/redirect-scope.py?type=redirect-to-scope',
+ 'redirected\n',
+ ['resources/redirect-scope.py?type=redirect-to-scope',
+ 'resources/redirect-scope.py?type=redirect-to-scope2',
+ 'resources/redirect-scope.py?type=redirect-to-scope3',]);
+ }, 'Navigation Preload redirect to the same scope.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/request-headers.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/request-headers.https.html
new file mode 100644
index 0000000..0964201
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/request-headers.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload request headers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ var script = 'resources/request-headers-worker.js';
+ var scope = 'resources/request-headers-scope.py';
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ add_completion_callback(_ => registration.unregister());
+ var worker = registration.installing;
+ return wait_for_state(t, worker, 'activated');
+ })
+ .then(_ => with_iframe(scope))
+ .then(frame => {
+ var headers = JSON.parse(frame.contentDocument.body.textContent);
+ assert_true(
+ 'SERVICE-WORKER-NAVIGATION-PRELOAD' in headers,
+ 'The Navigation Preload request must specify a ' +
+ '"Service-Worker-Navigation-Preload" header.');
+ assert_array_equals(
+ headers['SERVICE-WORKER-NAVIGATION-PRELOAD'],
+ ['hello'],
+ 'The Navigation Preload request must specify the correct value ' +
+ 'for the "Service-Worker-Navigation-Preload" header.');
+ assert_true(
+ 'UPGRADE-INSECURE-REQUESTS' in headers,
+ 'The Navigation Preload request must specify an ' +
+ '"Upgrade-Insecure-Requests" header.');
+ assert_array_equals(
+ headers['UPGRADE-INSECURE-REQUESTS'],
+ ['1'],
+ 'The Navigation Preload request must specify the correct value ' +
+ 'for the "Upgrade-Insecure-Requests" header.');
+ });
+ }, 'Navigation Preload request headers.');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resource-timing.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resource-timing.https.html
new file mode 100644
index 0000000..b4756d0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resource-timing.https.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload Resource Timing</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<script>
+
+function check_timing_entry(entry, url, decodedBodySize, encodedBodySize) {
+ assert_equals(entry.name, url, 'The entry name of '+ url);
+
+ assert_equals(
+ entry.entryType, 'resource',
+ 'The entryType of preload response timing entry must be "resource' +
+ '" :' + url);
+ assert_equals(
+ entry.initiatorType, 'navigation',
+ 'The initiatorType of preload response timing entry must be ' +
+ '"navigation":' + url);
+
+ // If the server returns the redirect response, |decodedBodySize| is null and
+ // |entry.decodedBodySize| shuld be 0. Otherwise |entry.decodedBodySize| must
+ // same as |decodedBodySize|
+ assert_equals(
+ entry.decodedBodySize, Number(decodedBodySize),
+ 'decodedBodySize must same as the decoded size in the server:' + url);
+
+ // If the server returns the redirect response, |encodedBodySize| is null and
+ // |entry.encodedBodySize| shuld be 0. Otherwise |entry.encodedBodySize| must
+ // same as |encodedBodySize|
+ assert_equals(
+ entry.encodedBodySize, Number(encodedBodySize),
+ 'encodedBodySize must same as the encoded size in the server:' + url);
+
+ assert_greater_than(
+ entry.transferSize, entry.decodedBodySize,
+ 'transferSize must greater then encodedBodySize.');
+
+ assert_greater_than(entry.startTime, 0, 'startTime of ' + url);
+ assert_greater_than_equal(entry.fetchStart, entry.startTime,
+ 'fetchStart >= startTime of ' + url);
+ assert_greater_than_equal(entry.domainLookupStart, entry.fetchStart,
+ 'domainLookupStart >= fetchStart of ' + url);
+ assert_greater_than_equal(entry.domainLookupEnd, entry.domainLookupStart,
+ 'domainLookupEnd >= domainLookupStart of ' + url);
+ assert_greater_than_equal(entry.connectStart, entry.domainLookupEnd,
+ 'connectStart >= domainLookupEnd of ' + url);
+ assert_greater_than_equal(entry.connectEnd, entry.connectStart,
+ 'connectEnd >= connectStart of ' + url);
+ assert_greater_than_equal(entry.requestStart, entry.connectEnd,
+ 'requestStart >= connectEnd of ' + url);
+ assert_greater_than_equal(entry.responseStart, entry.requestStart,
+ 'domainLookupStart >= requestStart of ' + url);
+ assert_greater_than_equal(entry.responseEnd, entry.responseStart,
+ 'responseEnd >= responseStart of ' + url);
+ assert_greater_than(entry.duration, 0, 'duration of ' + url);
+}
+
+promise_test(t => {
+ var script = 'resources/resource-timing-worker.js';
+ var scope = 'resources/resource-timing-scope.py';
+ var registration;
+ var frames = [];
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(reg => {
+ registration = reg;
+ add_completion_callback(_ => registration.unregister());
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(_ => with_iframe(scope + '?type=normal'))
+ .then(frame => {
+ frames.push(frame);
+ return with_iframe(scope + '?type=redirect');
+ })
+ .then(frame => {
+ frames.push(frame);
+ frames.forEach(frame => {
+ var result = JSON.parse(frame.contentDocument.body.textContent);
+ assert_equals(
+ result.timingEntries.length, 1,
+ 'performance.getEntriesByName() must returns one ' +
+ 'PerformanceResourceTiming entry for the navigation preload.');
+ var entry = result.timingEntries[0];
+ check_timing_entry(entry, frame.src, result.decodedBodySize,
+ result.encodedBodySize);
+ frame.remove();
+ });
+ return registration.unregister();
+ });
+ }, 'Navigation Preload Resource Timing.');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-scope.asis b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-scope.asis
new file mode 100644
index 0000000..2a71953
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-scope.asis
@@ -0,0 +1,6 @@
+HTTP/1.1 200 OK
+Content-type: text/html; charset=UTF-8
+Transfer-encoding: chunked
+
+hello
+world
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-worker.js b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-worker.js
new file mode 100644
index 0000000..7a453e4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-worker.js
@@ -0,0 +1,11 @@
+self.addEventListener('activate', event => {
+ event.waitUntil(
+ self.registration.navigationPreload.enable());
+ });
+
+self.addEventListener('fetch', event => {
+ event.respondWith(event.preloadResponse
+ .then(
+ _ => new Response('PASS: preloadResponse resolved'),
+ _ => new Response('FAIL: preloadResponse rejected')));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-scope.py b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-scope.py
new file mode 100644
index 0000000..659c4d8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-scope.py
@@ -0,0 +1,19 @@
+import time
+
+def main(request, response):
+ use_broken_body = b'use_broken_body' in request.GET
+
+ response.add_required_headers = False
+ response.writer.write_status(200)
+ response.writer.write_header(b"Content-type", b"text/html; charset=UTF-8")
+ response.writer.write_header(b"Transfer-encoding", b"chunked")
+ response.writer.end_headers()
+
+ for idx in range(10):
+ if use_broken_body:
+ response.writer.write(u"%s\n%s\n" % (len(str(idx)), idx))
+ else:
+ response.writer.write(u"%s\r\n%s\r\n" % (len(str(idx)), idx))
+ time.sleep(0.001)
+
+ response.writer.write(u"0\r\n\r\n")
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-worker.js b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-worker.js
new file mode 100644
index 0000000..f30e5ed
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-worker.js
@@ -0,0 +1,8 @@
+self.addEventListener('activate', event => {
+ event.waitUntil(
+ self.registration.navigationPreload.enable());
+ });
+
+self.addEventListener('fetch', event => {
+ event.respondWith(event.preloadResponse);
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/cookie.py b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/cookie.py
new file mode 100644
index 0000000..30a1dd4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/cookie.py
@@ -0,0 +1,20 @@
+def main(request, response):
+ """
+ Returns a response with a Set-Cookie header based on the query params.
+ The body will be "1" if the cookie is present in the request and `drop` parameter is "0",
+ otherwise the body will be "0".
+ """
+ same_site = request.GET.first(b"same-site")
+ cookie_name = request.GET.first(b"cookie-name")
+ drop = request.GET.first(b"drop")
+ cookie_in_request = b"0"
+ cookie = b"%s=1; Secure; SameSite=%s" % (cookie_name, same_site)
+
+ if drop == b"1":
+ cookie += b"; Max-Age=0"
+
+ if request.cookies.get(cookie_name):
+ cookie_in_request = request.cookies[cookie_name].value
+
+ headers = [(b'Content-Type', b'text/html'), (b'Set-Cookie', cookie)]
+ return (200, headers, cookie_in_request)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-scope.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-scope.html
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-scope.html
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-worker.js b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-worker.js
new file mode 100644
index 0000000..48c14b7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-worker.js
@@ -0,0 +1,15 @@
+self.addEventListener('activate', event => {
+ event.waitUntil(
+ self.registration.navigationPreload.enable());
+ });
+
+self.addEventListener('fetch', event => {
+ event.respondWith(
+ event.preloadResponse
+ .then(res => res.text())
+ .then(text => {
+ return new Response(
+ '<body>[' + text + ']</body>',
+ {headers: [['content-type', 'text/html']]});
+ }));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/get-state-worker.js b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/get-state-worker.js
new file mode 100644
index 0000000..a14ffb4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/get-state-worker.js
@@ -0,0 +1,21 @@
+// This worker listens for commands from the page and messages back
+// the result.
+
+function handle(message) {
+ const np = self.registration.navigationPreload;
+ switch (message) {
+ case 'getState':
+ return np.getState();
+ case 'enable':
+ return np.enable();
+ case 'disable':
+ return np.disable();
+ case 'setHeaderValue':
+ return np.setHeaderValue('insightful');
+ }
+ return Promise.reject('bad message');
+}
+
+self.addEventListener('message', e => {
+ e.waitUntil(handle(e.data).then(result => e.source.postMessage(result)));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/helpers.js b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/helpers.js
new file mode 100644
index 0000000..86f0c09
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/helpers.js
@@ -0,0 +1,5 @@
+function expect_navigation_preload_state(state, enabled, header, desc) {
+ assert_equals(Object.keys(state).length, 2, desc + ': # of keys');
+ assert_equals(state.enabled, enabled, desc + ': enabled');
+ assert_equals(state.headerValue, header, desc + ': header');
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/navigation-preload-worker.js b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/navigation-preload-worker.js
new file mode 100644
index 0000000..6e1ab23
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/navigation-preload-worker.js
@@ -0,0 +1,3 @@
+self.addEventListener('fetch', event => {
+ event.respondWith(event.preloadResponse);
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/redirect-redirected.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/redirect-redirected.html
new file mode 100644
index 0000000..f9bfce5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/redirect-redirected.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<body>redirected</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/redirect-scope.py b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/redirect-scope.py
new file mode 100644
index 0000000..84a97e5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/redirect-scope.py
@@ -0,0 +1,38 @@
+def main(request, response):
+ if b"base" in request.GET:
+ return [(b"Content-Type", b"text/html")], b"OK"
+ type = request.GET.first(b"type")
+
+ if type == b"normal":
+ response.status = 302
+ response.headers.append(b"Location", b"redirect-redirected.html")
+ response.headers.append(b"Custom-Header", b"hello")
+ return b""
+
+ if type == b"no-location":
+ response.status = 302
+ response.headers.append(b"Content-Type", b"text/html")
+ response.headers.append(b"Custom-Header", b"hello")
+ return b""
+
+ if type == b"no-location-with-body":
+ response.status = 302
+ response.headers.append(b"Content-Type", b"text/html")
+ response.headers.append(b"Custom-Header", b"hello")
+ return b"<body>BODY</body>"
+
+ if type == b"redirect-to-scope":
+ response.status = 302
+ response.headers.append(b"Location",
+ b"redirect-scope.py?type=redirect-to-scope2")
+ return b""
+ if type == b"redirect-to-scope2":
+ response.status = 302
+ response.headers.append(b"Location",
+ b"redirect-scope.py?type=redirect-to-scope3")
+ return b""
+ if type == b"redirect-to-scope3":
+ response.status = 302
+ response.headers.append(b"Location", b"redirect-redirected.html")
+ response.headers.append(b"Custom-Header", b"hello")
+ return b""
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/redirect-worker.js b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/redirect-worker.js
new file mode 100644
index 0000000..1b55f2e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/redirect-worker.js
@@ -0,0 +1,35 @@
+self.addEventListener('activate', event => {
+ event.waitUntil(
+ self.registration.navigationPreload.enable());
+ });
+
+function get_response_info(r) {
+ var info = {
+ type: r.type,
+ url: r.url,
+ status: r.status,
+ ok: r.ok,
+ statusText: r.statusText,
+ headers: []
+ };
+ r.headers.forEach((value, name) => { info.headers.push([value, name]); });
+ return info;
+}
+
+function post_to_page(data) {
+ return self.clients.matchAll()
+ .then(clients => clients.forEach(client => client.postMessage(data)));
+}
+
+self.addEventListener('fetch', event => {
+ event.respondWith(
+ event.preloadResponse
+ .then(
+ res => {
+ if (res.url.includes("base")) {
+ return res;
+ }
+ return post_to_page(get_response_info(res)).then(_ => res);
+ },
+ err => new Response(err.toString())));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/request-headers-scope.py b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/request-headers-scope.py
new file mode 100644
index 0000000..5bab5b0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/request-headers-scope.py
@@ -0,0 +1,14 @@
+import json
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ normalized = dict()
+
+ for key, values in dict(request.headers).items():
+ values = [isomorphic_decode(value) for value in values]
+ normalized[isomorphic_decode(key.upper())] = values
+
+ response.headers.append(b"Content-Type", b"text/html")
+
+ return json.dumps(normalized)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/request-headers-worker.js b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/request-headers-worker.js
new file mode 100644
index 0000000..1006cf2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/request-headers-worker.js
@@ -0,0 +1,10 @@
+self.addEventListener('activate', event => {
+ event.waitUntil(
+ Promise.all[
+ self.registration.navigationPreload.enable(),
+ self.registration.navigationPreload.setHeaderValue('hello')]);
+ });
+
+self.addEventListener('fetch', event => {
+ event.respondWith(event.preloadResponse);
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/resource-timing-scope.py b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/resource-timing-scope.py
new file mode 100644
index 0000000..856f9db
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/resource-timing-scope.py
@@ -0,0 +1,19 @@
+import zlib
+
+def main(request, response):
+ type = request.GET.first(b"type")
+
+ if type == "normal":
+ content = b"This is Navigation Preload Resource Timing test."
+ output = zlib.compress(content, 9)
+ headers = [(b"Content-type", b"text/plain"),
+ (b"Content-Encoding", b"deflate"),
+ (b"X-Decoded-Body-Size", len(content)),
+ (b"X-Encoded-Body-Size", len(output)),
+ (b"Content-Length", len(output))]
+ return headers, output
+
+ if type == b"redirect":
+ response.status = 302
+ response.headers.append(b"Location", b"redirect-redirected.html")
+ return b""
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/resource-timing-worker.js b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/resource-timing-worker.js
new file mode 100644
index 0000000..fac0d8d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/resource-timing-worker.js
@@ -0,0 +1,37 @@
+async function wait_for_performance_entries(url) {
+ let entries = performance.getEntriesByName(url);
+ if (entries.length > 0) {
+ return entries;
+ }
+ return new Promise((resolve) => {
+ new PerformanceObserver((list) => {
+ const entries = list.getEntriesByName(url);
+ if (entries.length > 0) {
+ resolve(entries);
+ }
+ }).observe({ entryTypes: ['resource'] });
+ });
+}
+
+self.addEventListener('activate', event => {
+ event.waitUntil(self.registration.navigationPreload.enable());
+ });
+
+self.addEventListener('fetch', event => {
+ let headers;
+ event.respondWith(
+ event.preloadResponse
+ .then(response => {
+ headers = response.headers;
+ return response.text()
+ })
+ .then(_ => wait_for_performance_entries(event.request.url))
+ .then(entries =>
+ new Response(
+ JSON.stringify({
+ decodedBodySize: headers.get('X-Decoded-Body-Size'),
+ encodedBodySize: headers.get('X-Encoded-Body-Size'),
+ timingEntries: entries
+ }),
+ {headers: {'Content-Type': 'text/html'}})));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/samesite-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/samesite-iframe.html
new file mode 100644
index 0000000..a28b612
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/samesite-iframe.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<body>samesite</body>
+<script>
+onmessage = (e) => {
+ if (e.data === "GetBody") {
+ parent.postMessage("samesite", '*');
+ }
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/samesite-sw-helper.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/samesite-sw-helper.html
new file mode 100644
index 0000000..51fdc9e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/samesite-sw-helper.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload Same Site SW registrator</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/test-helpers.sub.js"></script>
+<script>
+
+/**
+ * This is a helper file to register/unregister service worker in a same-site
+ * iframe.
+ **/
+
+async function messageToParent(msg) {
+ parent.postMessage(msg, '*');
+}
+
+onmessage = async (e) => {
+ // t is a , but the helper function needs a test object.
+ let t = {
+ step_func: (func) => func,
+ };
+ if (e.data === "Register") {
+ let reg = await service_worker_unregister_and_register(t, "samesite-worker.js", ".");
+ let worker = reg.installing;
+ await wait_for_state(t, worker, 'activated');
+ await messageToParent("SW Registered");
+ } else if (e.data == "Unregister") {
+ await service_worker_unregister(t, ".");
+ await messageToParent("SW Unregistered");
+ }
+}
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/samesite-worker.js b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/samesite-worker.js
new file mode 100644
index 0000000..f30e5ed
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/samesite-worker.js
@@ -0,0 +1,8 @@
+self.addEventListener('activate', event => {
+ event.waitUntil(
+ self.registration.navigationPreload.enable());
+ });
+
+self.addEventListener('fetch', event => {
+ event.respondWith(event.preloadResponse);
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/wait-for-activate-worker.js b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/wait-for-activate-worker.js
new file mode 100644
index 0000000..87791d2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/resources/wait-for-activate-worker.js
@@ -0,0 +1,40 @@
+// This worker remains in the installing phase so that the
+// navigation preload API can be tested when there is no
+// active worker.
+importScripts('/resources/testharness.js');
+importScripts('helpers.js');
+
+function expect_rejection(promise) {
+ return promise.then(
+ () => { return Promise.reject('unexpected fulfillment'); },
+ err => { assert_equals('InvalidStateError', err.name); });
+}
+
+function test_before_activation() {
+ const np = self.registration.navigationPreload;
+ return expect_rejection(np.enable())
+ .then(() => expect_rejection(np.disable()))
+ .then(() => expect_rejection(np.setHeaderValue('hi')))
+ .then(() => np.getState())
+ .then(state => expect_navigation_preload_state(
+ state, false, 'true', 'state should be the default'))
+ .then(() => 'PASS')
+ .catch(err => 'FAIL: ' + err);
+}
+
+var resolve_done_promise;
+var done_promise = new Promise(resolve => { resolve_done_promise = resolve; });
+
+// Run the test once the page messages this worker.
+self.addEventListener('message', e => {
+ e.waitUntil(test_before_activation()
+ .then(result => {
+ e.source.postMessage(result);
+ resolve_done_promise();
+ }));
+ });
+
+// Don't become the active worker until the test is done.
+self.addEventListener('install', e => {
+ e.waitUntil(done_promise);
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/samesite-cookies.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/samesite-cookies.https.html
new file mode 100644
index 0000000..a860d95
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/samesite-cookies.https.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<title>Navigation Preload: SameSite cookies</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const scope = 'resources/cookie.py';
+const script = 'resources/navigation-preload-worker.js';
+
+async function drop_cookie(t, same_site, cookie) {
+ const frame = await with_iframe(scope + '?same-site=' + same_site + '&cookie-name=' + cookie + '&drop=1');
+ t.add_cleanup(() => frame.remove());
+}
+
+async function same_site_cookies_test(t, same_site, cookie) {
+ // Remove the cookie before the first visit.
+ await drop_cookie(t, same_site, cookie);
+
+ {
+ const frame = await with_iframe(scope + '?same-site=' + same_site + '&cookie-name=' + cookie + '&drop=0');
+ t.add_cleanup(() => frame.remove());
+ // The body will be 0 because this is the first visit.
+ assert_equals(frame.contentDocument.body.textContent, '0', 'first visit');
+ }
+
+ {
+ const frame = await with_iframe(scope + '?same-site=' + same_site + '&cookie-name=' + cookie + '&drop=0');
+ t.add_cleanup(() => frame.remove());
+ // The body will be 1 because this is the second visit.
+ assert_equals(frame.contentDocument.body.textContent, '1', 'second visit');
+ }
+
+ // Remove the cookie after the test.
+ t.add_cleanup(() => drop_cookie(t, same_site, cookie));
+}
+
+promise_test(async t => {
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ promise_test(t => registration.unregister(), 'Unregister a service worker.');
+
+ await wait_for_state(t, registration.installing, 'activated');
+ await registration.navigationPreload.enable();
+}, 'Set up a service worker for navigation preload tests.');
+
+promise_test(async t => {
+ await same_site_cookies_test(t, 'None', 'cookie-key-none');
+}, 'Navigation Preload for same site cookies (None).');
+
+promise_test(async t => {
+ await same_site_cookies_test(t, 'Strict', 'cookie-key-strict');
+}, 'Navigation Preload for same site cookies (Strict).');
+
+promise_test(async t => {
+ await same_site_cookies_test(t, 'Lax', 'cookie-key-lax');
+}, 'Navigation Preload for same site cookies (Lax).');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/samesite-iframe.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/samesite-iframe.https.html
new file mode 100644
index 0000000..633da99
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-preload/samesite-iframe.https.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Navigation Preload for same site iframe</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="../resources/test-helpers.sub.js"></script>
+<body></body>
+<script>
+
+const SAME_SITE = get_host_info().HTTPS_REMOTE_ORIGIN;
+const RESOURCES_DIR = "/service-workers/service-worker/navigation-preload/resources/";
+
+/**
+ * This test is used for testing the NavigationPreload works in a same site iframe.
+ * The test scenario is
+ * 1. Create a same site iframe to register service worker and wait for it be activated
+ * 2. Create a same site iframe which be intercepted by the service worker.
+ * 3. Once the iframe is loaded, service worker should set the page through the preload response.
+ * And checking if the iframe's body content is expected.
+ * 4. Unregister the service worker.
+ * 5. remove created iframes.
+ */
+
+promise_test(async (t) => {
+ let resolver;
+ let checkValue = false;
+ window.onmessage = (e) => {
+ if (checkValue) {
+ assert_equals(e.data, "samesite");
+ checkValue = false;
+ }
+ resolver();
+ };
+
+ let helperIframe = document.createElement("iframe");
+ helperIframe.src = SAME_SITE + RESOURCES_DIR + "samesite-sw-helper.html";
+ document.body.appendChild(helperIframe);
+
+ await new Promise(resolve => {
+ resolver = resolve;
+ helperIframe.onload = async () => {
+ helperIframe.contentWindow.postMessage("Register", '*');
+ }
+ });
+
+ let sameSiteIframe = document.createElement("iframe");
+ sameSiteIframe.src = SAME_SITE + RESOURCES_DIR + "samesite-iframe.html";
+ document.body.appendChild(sameSiteIframe);
+ await new Promise(resolve => {
+ resolver = resolve;
+ sameSiteIframe.onload = async() => {
+ checkValue = true;
+ sameSiteIframe.contentWindow.postMessage("GetBody", '*')
+ }
+ });
+
+ await new Promise(resolve => {
+ resolver = resolve;
+ helperIframe.contentWindow.postMessage("Unregister", '*')
+ });
+
+ helperIframe.remove();
+ sameSiteIframe.remove();
+ });
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect-body.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect-body.https.html
new file mode 100644
index 0000000..0441c61
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect-body.https.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<title>Service Worker: Navigation redirection must clear body</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<meta charset="utf-8">
+<body>
+<form id="test-form" method="POST" style="display: none;">
+ <input type="submit" id="submit-button" />
+</form>
+<script>
+promise_test(function(t) {
+ var scope = 'resources/navigation-redirect-body.py';
+ var script = 'resources/navigation-redirect-body-worker.js';
+ var registration;
+ var frame = document.createElement('frame');
+ var form = document.getElementById('test-form');
+ var submit_button = document.getElementById('submit-button');
+
+ frame.src = 'about:blank';
+ frame.name = 'target_frame';
+ frame.id = 'frame';
+ document.body.appendChild(frame);
+ t.add_cleanup(function() { document.body.removeChild(frame); });
+
+ form.action = scope;
+ form.target = 'target_frame';
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ var frame_load_promise = new Promise(function(resolve) {
+ frame.addEventListener('load', function() {
+ resolve(frame.contentWindow.document.body.innerText);
+ }, false);
+ });
+ submit_button.click();
+ return frame_load_promise;
+ })
+ .then(function(text) {
+ var request_uri = decodeURIComponent(text);
+ assert_equals(
+ request_uri,
+ '/service-workers/service-worker/resources/navigation-redirect-body.py?redirect');
+ return registration.unregister();
+ });
+ }, 'Navigation redirection must clear body');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect-resolution.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect-resolution.https.html
new file mode 100644
index 0000000..59e1caf
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect-resolution.https.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<title>Service Worker: Navigation Redirect Resolution</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+function make_absolute(url) {
+ return new URL(url, location).toString();
+}
+
+const script = 'resources/fetch-rewrite-worker.js';
+
+function redirect_result_test(scope, expected_url, description) {
+ promise_test(async t => {
+ const registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ t.add_cleanup(() => {
+ return service_worker_unregister(t, scope);
+ })
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // The navigation to |scope| will be resolved by a fetch to |redirect_url|
+ // which returns a relative Location header. If it is resolved relative to
+ // |scope|, the result will be navigate-redirect-resolution/blank.html. If
+ // relative to |redirect_url|, it will be resources/blank.html. The latter
+ // is correct.
+ const iframe = await with_iframe(scope);
+ t.add_cleanup(() => { iframe.remove(); });
+ assert_equals(iframe.contentWindow.location.href,
+ make_absolute(expected_url));
+ }, description);
+}
+
+// |redirect_url| serves a relative redirect to resources/blank.html.
+const redirect_url = 'resources/redirect.py?Redirect=blank.html';
+
+// |scope_base| does not exist but will be replaced with a fetch of
+// |redirect_url| by fetch-rewrite-worker.js.
+const scope_base = 'resources/subdir/navigation-redirect-resolution?' +
+ 'redirect-mode=manual&url=' +
+ encodeURIComponent(make_absolute(redirect_url));
+
+// When the Service Worker forwards the result of |redirect_url| as an
+// opaqueredirect response, the redirect uses the response's URL list as the
+// base URL, not the request.
+redirect_result_test(scope_base, 'resources/blank.html',
+ 'test relative opaqueredirect');
+
+// The response's base URL should be preserved across CacheStorage and clone.
+redirect_result_test(scope_base + '&cache=1', 'resources/blank.html',
+ 'test relative opaqueredirect with CacheStorage');
+redirect_result_test(scope_base + '&clone=1', 'resources/blank.html',
+ 'test relative opaqueredirect with clone');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect-to-http.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect-to-http.https.html
new file mode 100644
index 0000000..d4d2788
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect-to-http.https.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>Service Worker: Service Worker can receive HTTP opaqueredirect response.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<meta charset="utf-8">
+<body></body>
+<script>
+async_test(function(t) {
+ var frame_src = get_host_info()['HTTPS_ORIGIN'] + base_path() +
+ 'resources/navigation-redirect-to-http-iframe.html';
+ function on_message(e) {
+ assert_equals(e.data.results, 'OK');
+ t.done();
+ }
+
+ window.addEventListener('message', t.step_func(on_message), false);
+
+ with_iframe(frame_src)
+ .then(function(frame) {
+ t.add_cleanup(function() { frame.remove(); });
+ });
+ }, 'Verify Service Worker can receive HTTP opaqueredirect response.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect.https.html
new file mode 100644
index 0000000..d7d3d52
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-redirect.https.html
@@ -0,0 +1,846 @@
+<!DOCTYPE html>
+<title>Service Worker: Navigation redirection</title>
+<meta name="timeout" content="long">
+<!-- empty variant tests document.location and intercepted URLs -->
+<meta name="variant" content="">
+<!-- client variant tests the Clients API (resultingClientId and Client.url) -->
+<meta name="variant" content="?client">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const host_info = get_host_info();
+
+// This test registers three Service Workers at SCOPE1, SCOPE2 and
+// OTHER_ORIGIN_SCOPE. And checks the redirected page's URL and the requests
+// which are intercepted by Service Worker while loading redirect page.
+const BASE_URL = host_info['HTTPS_ORIGIN'] + base_path();
+const OTHER_BASE_URL = host_info['HTTPS_REMOTE_ORIGIN'] + base_path();
+
+const SCOPE1 = BASE_URL + 'resources/navigation-redirect-scope1.py?';
+const SCOPE2 = BASE_URL + 'resources/navigation-redirect-scope2.py?';
+const OUT_SCOPE = BASE_URL + 'resources/navigation-redirect-out-scope.py?';
+const SCRIPT = 'resources/redirect-worker.js';
+
+const OTHER_ORIGIN_IFRAME_URL =
+ OTHER_BASE_URL + 'resources/navigation-redirect-other-origin.html';
+const OTHER_ORIGIN_SCOPE =
+ OTHER_BASE_URL + 'resources/navigation-redirect-scope1.py?';
+const OTHER_ORIGIN_OUT_SCOPE =
+ OTHER_BASE_URL + 'resources/navigation-redirect-out-scope.py?';
+
+let registrations;
+let workers;
+let other_origin_frame;
+let message_resolvers = {};
+let next_message_id = 0;
+
+promise_test(async t => {
+ // In this frame we register a service worker at OTHER_ORIGIN_SCOPE.
+ // And will use this frame to communicate with the worker.
+ other_origin_frame = await with_iframe(OTHER_ORIGIN_IFRAME_URL);
+
+ // Register same-origin service workers.
+ registrations = await Promise.all([
+ service_worker_unregister_and_register(t, SCRIPT, SCOPE1),
+ service_worker_unregister_and_register(t, SCRIPT, SCOPE2)]);
+
+ // Wait for all workers to activate.
+ workers = registrations.map(get_effective_worker);
+ return Promise.all([
+ wait_for_state(t, workers[0], 'activated'),
+ wait_for_state(t, workers[1], 'activated'),
+ // This promise will resolve when |wait_for_worker_promise|
+ // in OTHER_ORIGIN_IFRAME_URL resolves.
+ send_to_iframe(other_origin_frame, {command: 'wait_for_worker'})]);
+}, 'initialize global state');
+
+function get_effective_worker(registration) {
+ if (registration.active)
+ return registration.active;
+ if (registration.waiting)
+ return registration.waiting;
+ if (registration.installing)
+ return registration.installing;
+}
+
+async function check_all_intercepted_urls(expected_urls) {
+ const urls = [];
+ urls.push(await get_intercepted_urls(workers[0]));
+ urls.push(await get_intercepted_urls(workers[1]));
+ // Gets the request URLs which are intercepted by OTHER_ORIGIN_SCOPE's
+ // SW. This promise will resolve when get_request_infos() in
+ // OTHER_ORIGIN_IFRAME_URL resolves.
+ const request_infos = await send_to_iframe(other_origin_frame,
+ {command: 'get_request_infos'});
+ urls.push(request_infos.map(info => { return info.url; }));
+
+ assert_object_equals(urls, expected_urls, 'Intercepted URLs should match.');
+}
+
+// Checks |clients| returned from a worker. Only the client matching
+// |expected_final_client_tag| should be found. Returns true if a client was
+// found. Note that the final client is not necessarily found by this worker,
+// if the client is cross-origin.
+//
+// |clients| is an object like:
+// {x: {found: true, id: id1, url: url1}, b: {found: false}}
+function check_clients(clients,
+ expected_id,
+ expected_url,
+ expected_final_client_tag,
+ worker_name) {
+ let found = false;
+ Object.keys(clients).forEach(key => {
+ const info = clients[key];
+ if (info.found) {
+ assert_true(!!expected_final_client_tag,
+ `${worker_name} client tag exists`);
+ assert_equals(key, expected_final_client_tag,
+ `${worker_name} client tag matches`);
+ assert_equals(info.id, expected_id, `${worker_name} client id`);
+ assert_equals(info.url, expected_url, `${worker_name} client url`);
+ found = true;
+ }
+ });
+ return found;
+}
+
+function check_resulting_client_ids(infos, expected_infos, actual_ids, worker) {
+ assert_equals(infos.length, expected_infos.length,
+ `request length for ${worker}`);
+ for (var i = 0; i < infos.length; i++) {
+ const tag = expected_infos[i].resultingClientIdTag;
+ const url = expected_infos[i].url;
+ const actual_id = infos[i].resultingClientId;
+ const expected_id = actual_ids[tag];
+ assert_equals(typeof(actual_id), 'string',
+ `resultingClientId for ${url} request to ${worker}`);
+ if (expected_id) {
+ assert_equals(actual_id, expected_id,
+ `resultingClientId for ${url} request to ${worker}`);
+ } else {
+ actual_ids[tag] = actual_id;
+ }
+ }
+}
+
+// Creates an iframe and navigates to |url|, which is expected to start a chain
+// of redirects.
+// - |expected_last_url| is the expected window.location after the
+// navigation.
+//
+// - |expected_request_infos| is the expected requests that the service workers
+// were dispatched fetch events for. The format is:
+// [
+// [
+// // Requests received by workers[0].
+// {url: url1, resultingClientIdTag: 'a'},
+// {url: url2, resultingClientIdTag: 'a'}
+// ],
+// [
+// // Requests received by workers[1].
+// {url: url3, resultingClientIdTag: 'a'}
+// ],
+// [
+// // Requests received by the cross-origin worker.
+// {url: url4, resultingClientIdTag: 'x'}
+// {url: url5, resultingClientIdTag: 'x'}
+// ]
+// ]
+// Here, |url| is |event.request.url| and |resultingClientIdTag| represents
+// |event.resultingClientId|. Since the actual client ids are not known
+// beforehand, the expectation isn't the literal expected value, but all equal
+// tags must map to the same actual id.
+//
+// - |expected_final_client_tag| is the resultingClientIdTag that is
+// expected to map to the created client's id. This is null if there
+// is no such tag, which can happen when the final request was a cross-origin
+// redirect to out-scope, so no worker received a fetch event whose
+// resultingClientId is the id of the resulting client.
+//
+// In the example above:
+// - workers[0] receives two requests with the same resultingClientId.
+// - workers[1] receives one request also with that resultingClientId.
+// - The cross-origin worker receives two requests with the same
+// resultingClientId which differs from the previous one.
+// - Assuming |expected_final_client_tag| is 'x', then the created
+// client has the id seen by the cross-origin worker above.
+function redirect_test(url,
+ expected_last_url,
+ expected_request_infos,
+ expected_final_client_tag,
+ test_name) {
+ promise_test(async t => {
+ const frame = await with_iframe(url);
+ t.add_cleanup(() => { frame.remove(); });
+
+ // Switch on variant.
+ if (document.location.search == '?client') {
+ return client_variant_test(url, expected_last_url, expected_request_infos,
+ expected_final_client_tag, test_name);
+ }
+
+ return default_variant_test(url, expected_last_url, expected_request_infos,
+ frame, test_name);
+ }, test_name);
+}
+
+// The default variant tests the request interception chain and
+// resulting document.location.
+async function default_variant_test(url,
+ expected_last_url,
+ expected_request_infos,
+ frame,
+ test_name) {
+ const expected_intercepted_urls = expected_request_infos.map(
+ requests_for_worker => {
+ return requests_for_worker.map(info => {
+ return info.url;
+ });
+ });
+ await check_all_intercepted_urls(expected_intercepted_urls);
+ const last_url = await send_to_iframe(frame, 'getLocation');
+ assert_equals(last_url, expected_last_url, 'Last URL should match.');
+}
+
+// The "client" variant tests the Clients API using resultingClientId.
+async function client_variant_test(url,
+ expected_last_url,
+ expected_request_infos,
+ expected_final_client_tag,
+ test_name) {
+ // Request infos is an array like:
+ // [
+ // [{url: url1, resultingClientIdTag: tag1}],
+ // [{url: url2, resultingClientIdTag: tag2}],
+ // [{url: url3: resultingClientIdTag: tag3}]
+ // ]
+ const requestInfos = await get_all_request_infos();
+
+ // We check the actual infos against the expected ones, and learn the
+ // actual ids as we go.
+ const actual_ids = {};
+ check_resulting_client_ids(requestInfos[0],
+ expected_request_infos[0],
+ actual_ids,
+ 'worker0');
+ check_resulting_client_ids(requestInfos[1],
+ expected_request_infos[1],
+ actual_ids,
+ 'worker1');
+ check_resulting_client_ids(requestInfos[2],
+ expected_request_infos[2],
+ actual_ids,
+ 'crossOriginWorker');
+
+ // Now |actual_ids| maps tag to actual id:
+ // {x: id1, b: id2, c: id3}
+ // Ask each worker to try to resolve the actual ids to clients.
+ // Only |expected_final_client_tag| should resolve to a client.
+ const client_infos = await get_all_clients(actual_ids);
+
+ // Client infos is an object like:
+ // {
+ // worker0: {x: {found: true, id: id1, url: url1}, b: {found: false}},
+ // worker1: {x: {found: true, id: id1, url: url1}},
+ // crossOriginWorker: {x: {found: false}}, {b: {found: false}}
+ // }
+ //
+ // Now check each client info. check_clients() verifies each info: only
+ // |expected_final_client_tag| should ever be found and the found client
+ // should have the expected url and id. A wrinkle is that not all workers
+ // will find the client, if they are cross-origin to the client. This
+ // means check_clients() trivially passes if no clients are found. So
+ // additionally check that at least one worker found the client (|found|),
+ // if that was expected (|expect_found|).
+ let found = false;
+ const expect_found = !!expected_final_client_tag;
+ const expected_id = actual_ids[expected_final_client_tag];
+ found = check_clients(client_infos.worker0,
+ expected_id,
+ expected_last_url,
+ expected_final_client_tag,
+ 'worker0');
+ found = check_clients(client_infos.worker1,
+ expected_id,
+ expected_last_url,
+ expected_final_client_tag,
+ 'worker1') || found;
+ found = check_clients(client_infos.crossOriginWorker,
+ expected_id,
+ expected_last_url,
+ expected_final_client_tag,
+ 'crossOriginWorker') || found;
+ assert_equals(found, expect_found, 'client found');
+
+ if (!expect_found) {
+ // TODO(falken): Ask the other origin frame if it has a client of the
+ // expected URL.
+ }
+}
+
+window.addEventListener('message', on_message, false);
+
+function on_message(e) {
+ if (e.origin != host_info['HTTPS_REMOTE_ORIGIN'] &&
+ e.origin != host_info['HTTPS_ORIGIN'] ) {
+ console.error('invalid origin: ' + e.origin);
+ return;
+ }
+ var resolve = message_resolvers[e.data.id];
+ delete message_resolvers[e.data.id];
+ resolve(e.data.result);
+}
+
+function send_to_iframe(frame, message) {
+ var message_id = next_message_id++;
+ return new Promise(resolve => {
+ message_resolvers[message_id] = resolve;
+ frame.contentWindow.postMessage(
+ {id: message_id, message},
+ '*');
+ });
+}
+
+async function get_all_clients(actual_ids) {
+ const client_infos = {};
+ client_infos['worker0'] = await get_clients(workers[0], actual_ids);
+ client_infos['worker1'] = await get_clients(workers[1], actual_ids);
+ client_infos['crossOriginWorker'] =
+ await send_to_iframe(other_origin_frame,
+ {command: 'get_clients', actual_ids});
+ return client_infos;
+}
+
+function get_clients(worker, actual_ids) {
+ return new Promise(resolve => {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = (msg) => {
+ resolve(msg.data.clients);
+ };
+ worker.postMessage({command: 'getClients', actual_ids, port: channel.port2},
+ [channel.port2]);
+ });
+}
+
+// Returns an array of the URLs that |worker| received fetch events for:
+// [url1, url2]
+async function get_intercepted_urls(worker) {
+ const infos = await get_request_infos(worker);
+ return infos.map(info => { return info.url; });
+}
+
+// Returns the requests that |worker| received fetch events for. The return
+// value is an array of format:
+// [
+// {url: url1, resultingClientId: id},
+// {url: url2, resultingClientId: id}
+// ]
+function get_request_infos(worker) {
+ return new Promise(resolve => {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = (msg) => {
+ resolve(msg.data.requestInfos);
+ };
+ worker.postMessage({command: 'getRequestInfos', port: channel.port2},
+ [channel.port2]);
+ });
+}
+
+// Returns an array of the requests the workers received fetch events for:
+// [
+// // Requests from workers[0].
+// [
+// {url: url1, resultingClientIdTag: tag1},
+// {url: url2, resultingClientIdTag: tag1}
+// ],
+//
+// // Requests from workers[1].
+// [{url: url3, resultingClientIdTag: tag2}],
+//
+// // Requests from the cross-origin worker.
+// []
+// ]
+async function get_all_request_infos() {
+ const request_infos = [];
+ request_infos.push(await get_request_infos(workers[0]));
+ request_infos.push(await get_request_infos(workers[1]));
+ request_infos.push(await send_to_iframe(other_origin_frame,
+ {command: 'get_request_infos'}));
+ return request_infos;
+}
+
+let url;
+let url1;
+let url2;
+
+// Normal redirect (from out-scope to in-scope).
+url = SCOPE1;
+redirect_test(
+ OUT_SCOPE + 'url=' + encodeURIComponent(url),
+ url,
+ [[{url, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'Normal redirect to same-origin scope.');
+
+
+url = SCOPE1 + '#ref';
+redirect_test(
+ OUT_SCOPE + 'url=' + encodeURIComponent(SCOPE1) + '#ref',
+ url,
+ [[{url, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'Normal redirect to same-origin scope with a hash fragment.');
+
+url = SCOPE1 + '#ref2';
+redirect_test(
+ OUT_SCOPE + 'url=' + encodeURIComponent(url) + '#ref',
+ url,
+ [[{url, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'Normal redirect to same-origin scope with different hash fragments.');
+
+url = OTHER_ORIGIN_SCOPE;
+redirect_test(
+ OUT_SCOPE + 'url=' + encodeURIComponent(url),
+ url,
+ [[], [], [{url, resultingClientIdTag: 'x'}]],
+ 'x',
+ 'Normal redirect to other-origin scope.');
+
+// SW fallbacked redirect. SW doesn't handle the fetch request.
+url = SCOPE1 + 'url=' + encodeURIComponent(OUT_SCOPE);
+redirect_test(
+ url,
+ OUT_SCOPE,
+ [[{url, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'SW-fallbacked redirect to same-origin out-scope.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE1);
+url2 = SCOPE1;
+redirect_test(
+ url1,
+ url2,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'},
+ {url: url2, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'SW-fallbacked redirect to same-origin same-scope.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE1) + '#ref';
+url2 = SCOPE1 + '#ref';
+redirect_test(
+ url1,
+ url2,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'},
+ {url: url2, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'SW-fallbacked redirect to same-origin same-scope with a hash fragment.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE1 + '#ref2') + '#ref';
+url2 = SCOPE1 + '#ref2';
+redirect_test(
+ url1,
+ url2,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'},
+ {url: url2, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'SW-fallbacked redirect to same-origin same-scope with different hash ' +
+ 'fragments.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE2);
+url2 = SCOPE2;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'x'}],
+ [{url: url2, resultingClientIdTag: 'x'}],
+ []
+ ],
+ 'x',
+ 'SW-fallbacked redirect to same-origin other-scope.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
+url2 = OTHER_ORIGIN_OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'a'}], [], []],
+ null,
+ 'SW-fallbacked redirect to other-origin out-scope.');
+
+url1 = SCOPE1 + 'url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE);
+url2 = OTHER_ORIGIN_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'a'}],
+ [],
+ [{url: url2, resultingClientIdTag: 'x'}]
+ ],
+ 'x',
+ 'SW-fallbacked redirect to other-origin in-scope.');
+
+
+url3 = SCOPE1;
+url2 = OTHER_ORIGIN_SCOPE + 'url=' + encodeURIComponent(url3);
+url1 = SCOPE1 + 'url=' + encodeURIComponent(url2);
+redirect_test(
+ url1,
+ url3,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'a'},
+ {url: url3, resultingClientIdTag: 'x'}
+ ],
+ [],
+ [{url: url2, resultingClientIdTag: 'b'}]
+ ],
+ 'x',
+ 'SW-fallbacked redirect to other-origin and back to same-origin.');
+
+// SW generated redirect.
+// SW: event.respondWith(Response.redirect(params['url']));
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OUT_SCOPE);
+url2 = OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'SW-generated redirect to same-origin out-scope.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OUT_SCOPE) + '#ref';
+url2 = OUT_SCOPE + '#ref';
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'SW-generated redirect to same-origin out-scope with a hash fragment.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OUT_SCOPE + '#ref2') + '#ref';
+url2 = OUT_SCOPE + '#ref2';
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'SW-generated redirect to same-origin out-scope with different hash ' +
+ 'fragments.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(SCOPE1);
+url2 = SCOPE1;
+redirect_test(
+ url1,
+ url2,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'},
+ {url: url2, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'SW-generated redirect to same-origin same-scope.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(SCOPE2);
+url2 = SCOPE2;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'x'}],
+ [{url: url2, resultingClientIdTag: 'x'}],
+ []
+ ],
+ 'x',
+ 'SW-generated redirect to same-origin other-scope.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
+url2 = OTHER_ORIGIN_OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'a'}], [], []],
+ null,
+ 'SW-generated redirect to other-origin out-scope.');
+
+url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE);
+url2 = OTHER_ORIGIN_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'a'}],
+ [],
+ [{url: url2, resultingClientIdTag: 'x'}]
+ ],
+ 'x',
+ 'SW-generated redirect to other-origin in-scope.');
+
+
+// SW fetched redirect.
+// SW: event.respondWith(fetch(event.request));
+url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(OUT_SCOPE)
+url2 = OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'SW-fetched redirect to same-origin out-scope.');
+
+url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(SCOPE1);
+url2 = SCOPE1;
+redirect_test(
+ url1,
+ url2,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'},
+ {url: url2, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'SW-fetched redirect to same-origin same-scope.');
+
+url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(SCOPE2);
+url2 = SCOPE2;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'x'}],
+ [{url: url2, resultingClientIdTag: 'x'}],
+ []
+ ],
+ 'x',
+ 'SW-fetched redirect to same-origin other-scope.');
+
+url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
+url2 = OTHER_ORIGIN_OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'a'}], [], []],
+ null,
+ 'SW-fetched redirect to other-origin out-scope.');
+
+url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE);
+url2 = OTHER_ORIGIN_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'a'}],
+ [],
+ [{url: url2, resultingClientIdTag: 'x'}]
+ ],
+ 'x',
+ 'SW-fetched redirect to other-origin in-scope.');
+
+
+// SW responds with a fetch from a different url.
+// SW: event.respondWith(fetch(params['url']));
+url2 = SCOPE1;
+url1 = SCOPE1 + 'sw=fetch-url&url=' + encodeURIComponent(url2);
+redirect_test(
+ url1,
+ url1,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'SW-fetched response from different URL, same-origin same-scope.');
+
+
+// Opaque redirect.
+// SW: event.respondWith(fetch(
+// new Request(event.request.url, {redirect: 'manual'})));
+url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(OUT_SCOPE);
+url2 = OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'Redirect to same-origin out-scope with opaque redirect response.');
+
+url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(SCOPE1);
+url2 = SCOPE1;
+redirect_test(
+ url1,
+ url2,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'},
+ {url: url2, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'Redirect to same-origin same-scope with opaque redirect response.');
+
+url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(SCOPE2);
+url2 = SCOPE2;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'x'}],
+ [{url: url2, resultingClientIdTag: 'x'}],
+ []
+ ],
+ 'x',
+ 'Redirect to same-origin other-scope with opaque redirect response.');
+
+url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
+url2 = OTHER_ORIGIN_OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'a'}], [], []],
+ null,
+ 'Redirect to other-origin out-scope with opaque redirect response.');
+
+url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE);
+url2 = OTHER_ORIGIN_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'a'}],
+ [],
+ [{url: url2, resultingClientIdTag: 'x'}]
+ ],
+ 'x',
+ 'Redirect to other-origin in-scope with opaque redirect response.');
+
+url= SCOPE1 + 'sw=manual&noLocationRedirect';
+redirect_test(
+ url, url, [[{url, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'No location redirect response.');
+
+
+// Opaque redirect passed through Cache.
+// SW responds with an opaque redirectresponse from the Cache API.
+url1 = SCOPE1 + 'sw=manualThroughCache&url=' + encodeURIComponent(OUT_SCOPE);
+url2 = OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'Redirect to same-origin out-scope with opaque redirect response which ' +
+ 'is passed through Cache.');
+
+url1 = SCOPE1 + 'sw=manualThroughCache&url=' + encodeURIComponent(SCOPE1);
+url2 = SCOPE1;
+redirect_test(
+ url1,
+ url2,
+ [
+ [
+ {url: url1, resultingClientIdTag: 'x'},
+ {url: url2, resultingClientIdTag: 'x'}
+ ],
+ [],
+ []
+ ],
+ 'x',
+ 'Redirect to same-origin same-scope with opaque redirect response which ' +
+ 'is passed through Cache.');
+
+url1 = SCOPE1 + 'sw=manualThroughCache&url=' + encodeURIComponent(SCOPE2);
+url2 = SCOPE2;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'x'}],
+ [{url: url2, resultingClientIdTag: 'x'}],
+ []
+ ],
+ 'x',
+ 'Redirect to same-origin other-scope with opaque redirect response which ' +
+ 'is passed through Cache.');
+
+url1 = SCOPE1 + 'sw=manualThroughCache&url=' +
+ encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
+url2 = OTHER_ORIGIN_OUT_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [[{url: url1, resultingClientIdTag: 'a'}], [], []],
+ null,
+ 'Redirect to other-origin out-scope with opaque redirect response which ' +
+ 'is passed through Cache.');
+
+url1 = SCOPE1 + 'sw=manualThroughCache&url=' +
+ encodeURIComponent(OTHER_ORIGIN_SCOPE);
+url2 = OTHER_ORIGIN_SCOPE;
+redirect_test(
+ url1,
+ url2,
+ [
+ [{url: url1, resultingClientIdTag: 'a'}],
+ [],
+ [{url: url2, resultingClientIdTag: 'x'}],
+ ],
+ 'x',
+ 'Redirect to other-origin in-scope with opaque redirect response which ' +
+ 'is passed through Cache.');
+
+url = SCOPE1 + 'sw=manualThroughCache&noLocationRedirect';
+redirect_test(
+ url,
+ url,
+ [[{url, resultingClientIdTag: 'x'}], [], []],
+ 'x',
+ 'No location redirect response via Cache.');
+
+// Clean up the test environment. This promise_test() needs to be the last one.
+promise_test(async t => {
+ registrations.forEach(async registration => {
+ if (registration)
+ await registration.unregister();
+ });
+ await send_to_iframe(other_origin_frame, {command: 'unregister'});
+ other_origin_frame.remove();
+}, 'clean up global state');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-sets-cookie.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-sets-cookie.https.html
new file mode 100644
index 0000000..7f6c756
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-sets-cookie.https.html
@@ -0,0 +1,133 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="timeout" content="long">
+<title>Service Worker: Navigation setting cookies</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="/cookies/resources/cookie-helper.sub.js"></script>
+<body>
+<script>
+'use strict';
+
+const scopepath = '/cookies/resources/setSameSite.py?with-sw';
+
+async function unregister_service_worker(origin) {
+ let target_url = origin +
+ '/service-workers/service-worker/resources/unregister-rewrite-worker.html' +
+ '?scopepath=' + encodeURIComponent(scopepath);
+ const w = window.open(target_url);
+ try {
+ await wait_for_message('SW-UNREGISTERED');
+ } finally {
+ w.close();
+ }
+}
+
+async function register_service_worker(origin) {
+ let target_url = origin +
+ '/service-workers/service-worker/resources/register-rewrite-worker.html' +
+ '?scopepath=' + encodeURIComponent(scopepath);
+ const w = window.open(target_url);
+ try {
+ await wait_for_message('SW-REGISTERED');
+ } finally {
+ w.close();
+ }
+}
+
+async function clear_cookies(origin) {
+ let target_url = origin + '/cookies/samesite/resources/puppet.html';
+ const w = window.open(target_url);
+ try {
+ await wait_for_message('READY');
+ w.postMessage({ type: 'drop' }, '*');
+ await wait_for_message('drop-complete');
+ } finally {
+ w.close();
+ }
+}
+
+// The following tests are adapted from /cookies/samesite/setcookie-navigation.https.html
+
+// Asserts that cookies are present or not present (according to `expectation`)
+// in the cookie string `cookies` with the correct names and value.
+function assert_cookies_present(cookies, value, expected_cookie_names, expectation) {
+ for (name of expected_cookie_names) {
+ let re = new RegExp("(?:^|; )" + name + "=" + value + "(?:$|;)");
+ let assertion = expectation ? assert_true : assert_false;
+ assertion(re.test(cookies), "`" + name + "=" + value + "` in cookies");
+ }
+}
+
+// Navigate from ORIGIN to |origin_to|, expecting the navigation to set SameSite
+// cookies on |origin_to|.
+function navigate_test(method, origin_to, query, title) {
+ promise_test(async function(t) {
+ // The cookies don't need to be cleared on each run because |value| is
+ // a new random value on each run, so on each run we are overwriting and
+ // checking for a cookie with a different random value.
+ let value = query + "&" + Math.random();
+ let url_from = SECURE_ORIGIN + "/cookies/samesite/resources/navigate.html"
+ let url_to = origin_to + "/cookies/resources/setSameSite.py?" + value;
+ var w = window.open(url_from);
+ await wait_for_message('READY', SECURE_ORIGIN);
+ assert_equals(SECURE_ORIGIN, window.origin);
+ assert_equals(SECURE_ORIGIN, w.origin);
+ let command = (method === "POST") ? "post-form" : "navigate";
+ w.postMessage({ type: command, url: url_to }, "*");
+ let message = await wait_for_message('COOKIES_SET', origin_to);
+ let samesite_cookie_names = ['samesite_strict', 'samesite_lax', 'samesite_none', 'samesite_unspecified'];
+ assert_cookies_present(message.data.cookies, value, samesite_cookie_names, true);
+ w.close();
+ }, title);
+}
+
+promise_test(async t => {
+ await register_service_worker(SECURE_ORIGIN);
+ await register_service_worker(SECURE_CROSS_SITE_ORIGIN);
+}, 'Setup service workers');
+
+navigate_test("GET", SECURE_ORIGIN, "with-sw&ignore",
+ "Same-site top-level navigation with fallback service worker should be able to set SameSite=* cookies.");
+navigate_test("GET", SECURE_CROSS_SITE_ORIGIN, "with-sw&ignore",
+ "Cross-site top-level navigation with fallback service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_ORIGIN, "with-sw&ignore",
+ "Same-site top-level POST with fallback service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_CROSS_SITE_ORIGIN, "with-sw&ignore",
+ "Cross-site top-level with fallback service worker POST should be able to set SameSite=* cookies.");
+
+navigate_test("GET", SECURE_ORIGIN, "with-sw",
+ "Same-site top-level navigation with passthrough service worker should be able to set SameSite=* cookies.");
+navigate_test("GET", SECURE_CROSS_SITE_ORIGIN, "with-sw",
+ "Cross-site top-level navigation with passthrough service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_ORIGIN, "with-sw",
+ "Same-site top-level POST with passthrough service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_CROSS_SITE_ORIGIN, "with-sw",
+ "Cross-site top-level with passthrough service worker POST should be able to set SameSite=* cookies.");
+
+navigate_test("GET", SECURE_ORIGIN, "with-sw&navpreload",
+ "Same-site top-level navigation with navpreload service worker should be able to set SameSite=* cookies.");
+navigate_test("GET", SECURE_CROSS_SITE_ORIGIN, "with-sw&navpreload",
+ "Cross-site top-level navigation with navpreload service worker should be able to set SameSite=* cookies.");
+// navpreload not supported with POST method
+
+navigate_test("GET", SECURE_ORIGIN, "with-sw&change-request",
+ "Same-site top-level navigation with change-request service worker should be able to set SameSite=* cookies.");
+navigate_test("GET", SECURE_CROSS_SITE_ORIGIN, "with-sw&change-request",
+ "Cross-site top-level navigation with change-request service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_ORIGIN, "with-sw&change-request",
+ "Same-site top-level POST with change-request service worker should be able to set SameSite=* cookies.");
+navigate_test("POST", SECURE_CROSS_SITE_ORIGIN, "with-sw&change-request",
+ "Cross-site top-level with change-request service worker POST should be able to set SameSite=* cookies.");
+
+promise_test(async t => {
+ await unregister_service_worker(SECURE_ORIGIN);
+ await unregister_service_worker(SECURE_CROSS_SITE_ORIGIN);
+ await clear_cookies(SECURE_ORIGIN);
+ await clear_cookies(SECURE_CROSS_SITE_ORIGIN);
+}, 'Cleanup service workers');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-timing-extended.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-timing-extended.https.html
new file mode 100644
index 0000000..acb02c6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-timing-extended.https.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+
+<script>
+const timingEventOrder = [
+ 'startTime',
+ 'workerStart',
+ 'fetchStart',
+ 'requestStart',
+ 'responseStart',
+ 'responseEnd',
+];
+
+function navigate_in_frame(frame, url) {
+ frame.contentWindow.location = url;
+ return new Promise((resolve) => {
+ frame.addEventListener('load', () => {
+ const timing = frame.contentWindow.performance.getEntriesByType('navigation')[0];
+ const {timeOrigin} = frame.contentWindow.performance;
+ resolve({
+ workerStart: timing.workerStart + timeOrigin,
+ fetchStart: timing.fetchStart + timeOrigin
+ })
+ });
+ });
+}
+
+const worker_url = 'resources/navigation-timing-worker-extended.js';
+
+promise_test(async (t) => {
+ const scope = 'resources/timings/dummy.html';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activating');
+ const frame = await with_iframe('resources/empty.html');
+ t.add_cleanup(() => frame.remove());
+
+ const [timingFromEntry, timingFromWorker] = await Promise.all([
+ navigate_in_frame(frame, scope),
+ new Promise(resolve => {
+ window.addEventListener('message', m => {
+ resolve(m.data)
+ })
+ })])
+
+ assert_greater_than(timingFromWorker.activateWorkerEnd, timingFromEntry.workerStart,
+ 'workerStart marking should not wait for worker activation to finish');
+ assert_greater_than(timingFromEntry.fetchStart, timingFromWorker.activateWorkerEnd,
+ 'fetchStart should be marked once the worker is activated');
+ assert_greater_than(timingFromWorker.handleFetchEvent, timingFromEntry.fetchStart,
+ 'fetchStart should be marked before the Fetch event handler is called');
+}, 'Service worker controlled navigation timing');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/navigation-timing.https.html b/third_party/web_platform_tests/service-workers/service-worker/navigation-timing.https.html
new file mode 100644
index 0000000..6b51a5c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/navigation-timing.https.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+
+<script>
+const timingEventOrder = [
+ 'startTime',
+ 'workerStart',
+ 'fetchStart',
+ 'requestStart',
+ 'responseStart',
+ 'responseEnd',
+];
+
+function verify(timing) {
+ for (let i = 0; i < timingEventOrder.length - 1; i++) {
+ assert_true(timing[timingEventOrder[i]] <= timing[timingEventOrder[i + 1]],
+ `Expected ${timingEventOrder[i]} <= ${timingEventOrder[i + 1]}`);
+ }
+}
+
+function navigate_in_frame(frame, url) {
+ frame.contentWindow.location = url;
+ return new Promise((resolve) => {
+ frame.addEventListener('load', () => {
+ const timing = frame.contentWindow.performance.getEntriesByType('navigation')[0];
+ resolve(timing);
+ });
+ });
+}
+
+const worker_url = 'resources/navigation-timing-worker.js';
+
+promise_test(async (t) => {
+ const scope = 'resources/empty.html';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ const timing = await navigate_in_frame(frame, scope);
+ assert_greater_than(timing.workerStart, 0);
+ verify(timing);
+}, 'Service worker controlled navigation timing');
+
+promise_test(async (t) => {
+ const scope = 'resources/empty.html?network-fallback';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ const timing = await navigate_in_frame(frame, scope);
+ verify(timing);
+}, 'Service worker controlled navigation timing network fallback');
+
+promise_test(async (t) => {
+ const scope = 'resources/redirect.py?Redirect=empty.html';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ const timing = await navigate_in_frame(frame, scope);
+ verify(timing);
+ // Additional checks for redirected navigation.
+ assert_true(timing.redirectStart <= timing.redirectEnd,
+ 'Expected redirectStart <= redirectEnd');
+ assert_true(timing.redirectEnd <= timing.fetchStart,
+ 'Expected redirectEnd <= fetchStart');
+}, 'Service worker controlled navigation timing redirect');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/nested-blob-url-workers.https.html b/third_party/web_platform_tests/service-workers/service-worker/nested-blob-url-workers.https.html
new file mode 100644
index 0000000..7269cbb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/nested-blob-url-workers.https.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Service Worker: nested blob URL worker clients</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const SCRIPT = 'resources/simple-intercept-worker.js';
+const SCOPE = 'resources/';
+const RESOURCE = 'resources/simple.txt';
+
+promise_test((t) => {
+ return runTest(t, 'resources/nested-blob-url-workers.html');
+}, 'Nested blob URL workers should be intercepted by a service worker.');
+
+promise_test((t) => {
+ return runTest(t, 'resources/nested-worker-created-from-blob-url-worker.html');
+}, 'Nested worker created from a blob URL worker should be intercepted by a service worker.');
+
+promise_test((t) => {
+ return runTest(t, 'resources/nested-blob-url-worker-created-from-worker.html');
+}, 'Nested blob URL worker created from a worker should be intercepted by a service worker.');
+
+async function runTest(t, iframe_url) {
+ const reg = await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ t.add_cleanup(_ => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ const frame = await with_iframe(iframe_url);
+ t.add_cleanup(_ => frame.remove());
+ assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller,
+ null, 'frame should be controlled');
+
+ const response_text = await frame.contentWindow.fetch_in_worker(RESOURCE);
+ assert_equals(response_text, 'intercepted by service worker',
+ 'fetch() should be intercepted.');
+}
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/next-hop-protocol.https.html b/third_party/web_platform_tests/service-workers/service-worker/next-hop-protocol.https.html
new file mode 100644
index 0000000..7a90743
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/next-hop-protocol.https.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: Verify nextHopProtocol is set correctly</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+async function getNextHopProtocol(frame, url) {
+ let final_url = new URL(url, self.location).href;
+ await frame.contentWindow.fetch(final_url).then(r => r.text());
+ let entryList = frame.contentWindow.performance.getEntriesByName(final_url);
+ let entry = entryList[entryList.length - 1];
+ return entry.nextHopProtocol;
+}
+
+async function runTest(t, base_url, expected_protocol) {
+ const scope = 'resources/empty.html?next-hop-protocol';
+ const script = 'resources/fetch-rewrite-worker.js';
+ let frame;
+
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(async _ => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ frame = await with_iframe(scope);
+ t.add_cleanup(_ => frame.remove());
+
+ assert_equals(await getNextHopProtocol(frame, `${base_url}?generate-png`),
+ '', 'nextHopProtocol is not set on synthetic response');
+ assert_equals(await getNextHopProtocol(frame, `${base_url}?ignore`),
+ expected_protocol, 'nextHopProtocol is set on fallback');
+ assert_equals(await getNextHopProtocol(frame, `${base_url}`),
+ expected_protocol, 'nextHopProtocol is set on pass-through');
+ assert_equals(await getNextHopProtocol(frame, `${base_url}?cache`),
+ expected_protocol, 'nextHopProtocol is set on cached response');
+}
+
+promise_test(async (t) => {
+ return runTest(t, 'resources/empty.js', 'http/1.1');
+}, 'nextHopProtocol reports H1 correctly when routed via a service worker.');
+
+// This may be expected to fail if the WPT infrastructure does not fully
+// support H2 protocol testing yet.
+promise_test(async (t) => {
+ return runTest(t, 'resources/empty.h2.js', 'h2');
+}, 'nextHopProtocol reports H2 correctly when routed via a service worker.');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/no-dynamic-import-in-module.any.js b/third_party/web_platform_tests/service-workers/service-worker/no-dynamic-import-in-module.any.js
new file mode 100644
index 0000000..f7c2ef3
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/no-dynamic-import-in-module.any.js
@@ -0,0 +1,7 @@
+// META: global=serviceworker-module
+
+// This is imported to ensure import('./basic-module-2.js') fails even if
+// it has been previously statically imported.
+import './resources/basic-module-2.js';
+
+import './resources/no-dynamic-import.js';
diff --git a/third_party/web_platform_tests/service-workers/service-worker/no-dynamic-import.any.js b/third_party/web_platform_tests/service-workers/service-worker/no-dynamic-import.any.js
new file mode 100644
index 0000000..25b370b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/no-dynamic-import.any.js
@@ -0,0 +1,3 @@
+// META: global=serviceworker
+
+importScripts('resources/no-dynamic-import.js');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/onactivate-script-error.https.html b/third_party/web_platform_tests/service-workers/service-worker/onactivate-script-error.https.html
new file mode 100644
index 0000000..f5e80bb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/onactivate-script-error.https.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function wait_for_install(worker) {
+ return new Promise(function(resolve, reject) {
+ worker.addEventListener('statechange', function(event) {
+ if (worker.state == 'installed')
+ resolve();
+ else if (worker.state == 'redundant')
+ reject();
+ });
+ });
+}
+
+function wait_for_activate(worker) {
+ return new Promise(function(resolve, reject) {
+ worker.addEventListener('statechange', function(event) {
+ if (worker.state == 'activated')
+ resolve();
+ else if (worker.state == 'redundant')
+ reject();
+ });
+ });
+}
+
+function make_test(name, script) {
+ promise_test(function(t) {
+ var scope = script;
+ var registration;
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(r) {
+ registration = r;
+
+ t.add_cleanup(function() {
+ return r.unregister();
+ });
+
+ return wait_for_install(registration.installing);
+ })
+ .then(function() {
+ // Activate should succeed regardless of script errors.
+ return wait_for_activate(registration.waiting);
+ });
+ }, name);
+}
+
+[
+ {
+ name: 'activate handler throws an error',
+ script: 'resources/onactivate-throw-error-worker.js',
+ },
+ {
+ name: 'activate handler throws an error, error handler does not cancel',
+ script: 'resources/onactivate-throw-error-with-empty-onerror-worker.js',
+ },
+ {
+ name: 'activate handler dispatches an event that throws an error',
+ script: 'resources/onactivate-throw-error-from-nested-event-worker.js',
+ },
+ {
+ name: 'activate handler throws an error that is cancelled',
+ script: 'resources/onactivate-throw-error-then-cancel-worker.js',
+ },
+ {
+ name: 'activate handler throws an error and prevents default',
+ script: 'resources/onactivate-throw-error-then-prevent-default-worker.js',
+ }
+].forEach(function(test_case) {
+ make_test(test_case.name, test_case.script);
+ });
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/oninstall-script-error.https.html b/third_party/web_platform_tests/service-workers/service-worker/oninstall-script-error.https.html
new file mode 100644
index 0000000..fe7f6e9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/oninstall-script-error.https.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function wait_for_install_event(worker) {
+ return new Promise(function(resolve) {
+ worker.addEventListener('statechange', function(event) {
+ if (worker.state == 'installed')
+ resolve(true);
+ else if (worker.state == 'redundant')
+ resolve(false);
+ });
+ });
+}
+
+function make_test(name, script, expect_install) {
+ promise_test(function(t) {
+ var scope = script;
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(registration) {
+ return wait_for_install_event(registration.installing);
+ })
+ .then(function(did_install) {
+ assert_equals(did_install, expect_install,
+ 'The worker was installed');
+ })
+ }, name);
+}
+
+[
+ {
+ name: 'install handler throws an error',
+ script: 'resources/oninstall-throw-error-worker.js',
+ expect_install: true
+ },
+ {
+ name: 'install handler throws an error, error handler does not cancel',
+ script: 'resources/oninstall-throw-error-with-empty-onerror-worker.js',
+ expect_install: true
+ },
+ {
+ name: 'install handler dispatches an event that throws an error',
+ script: 'resources/oninstall-throw-error-from-nested-event-worker.js',
+ expect_install: true
+ },
+ {
+ name: 'install handler throws an error in the waitUntil',
+ script: 'resources/oninstall-waituntil-throw-error-worker.js',
+ expect_install: false
+ },
+
+ // The following two cases test what happens when the ServiceWorkerGlobalScope
+ // 'error' event handler cancels the resulting error event. Since the
+ // original 'install' event handler through, the installation should still
+ // be stopped in this case. See:
+ // https://github.com/slightlyoff/ServiceWorker/issues/778
+ {
+ name: 'install handler throws an error that is cancelled',
+ script: 'resources/oninstall-throw-error-then-cancel-worker.js',
+ expect_install: true
+ },
+ {
+ name: 'install handler throws an error and prevents default',
+ script: 'resources/oninstall-throw-error-then-prevent-default-worker.js',
+ expect_install: true
+ }
+].forEach(function(test_case) {
+ make_test(test_case.name, test_case.script, test_case.expect_install);
+ });
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/opaque-response-preloaded.https.html b/third_party/web_platform_tests/service-workers/service-worker/opaque-response-preloaded.https.html
new file mode 100644
index 0000000..417aa4e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/opaque-response-preloaded.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Opaque responses should not be reused for XHRs</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const WORKER =
+ 'resources/opaque-response-preloaded-worker.js';
+
+var done;
+
+// These test that the browser does not inappropriately use a cached opaque
+// response for a request that is not no-cors. The test opens a controlled
+// iframe that uses link rel=preload to issue a same-origin no-cors request.
+// The service worker responds to the request with an opaque response. Then the
+// iframe does an XHR (not no-cors) to that URL again. The request should fail.
+promise_test(t => {
+ const SCOPE =
+ 'resources/opaque-response-being-preloaded-xhr.html';
+ const promise = new Promise(resolve => done = resolve);
+
+ return service_worker_unregister_and_register(t, WORKER, SCOPE)
+ .then(reg => {
+ add_completion_callback(() => reg.unregister());
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(() => with_iframe(SCOPE))
+ .then(frame => t.add_cleanup(() => frame.remove() ))
+ .then(() => promise)
+ .then(result => assert_equals(result, 'PASS'));
+ }, 'Opaque responses should not be reused for XHRs, loading case');
+
+promise_test(t => {
+ const SCOPE =
+ 'resources/opaque-response-preloaded-xhr.html';
+ const promise = new Promise(resolve => done = resolve);
+
+ return service_worker_unregister_and_register(t, WORKER, SCOPE)
+ .then(reg => {
+ add_completion_callback(() => reg.unregister());
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(() => with_iframe(SCOPE))
+ .then(frame => t.add_cleanup(() => frame.remove() ))
+ .then(() => promise)
+ .then(result => assert_equals(result, 'PASS'));
+ }, 'Opaque responses should not be reused for XHRs, done case');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/opaque-script.https.html b/third_party/web_platform_tests/service-workers/service-worker/opaque-script.https.html
new file mode 100644
index 0000000..7d21218
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/opaque-script.https.html
@@ -0,0 +1,71 @@
+<!doctype html>
+<title>Cache Storage: verify scripts loaded from cache_storage are marked opaque</title>
+<link rel="help" href="https://w3c.github.io/ServiceWorker/#cache-interface">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+'use strict';
+
+const SW_URL = 'resources/opaque-script-sw.js';
+const BASE_SCOPE = './resources/opaque-script-frame.html';
+const SAME_ORIGIN_BASE = new URL('./resources/', self.location.href).href;
+const CROSS_ORIGIN_BASE = new URL('./resources/',
+ get_host_info().HTTPS_REMOTE_ORIGIN + base_path()).href;
+
+function wait_for_error() {
+ return new Promise(resolve => {
+ self.addEventListener('message', function messageHandler(evt) {
+ if (evt.data.type !== 'ErrorEvent')
+ return;
+ self.removeEventListener('message', messageHandler);
+ resolve(evt.data.msg);
+ });
+ });
+}
+
+// Load an iframe that dynamically adds a script tag that is
+// same/cross origin and large/small. It then calls a function
+// defined in that loaded script that throws an unhandled error.
+// The resulting message exposed in the global onerror handler
+// is reported back from this function. Opaque cross origin
+// scripts should not expose the details of the uncaught exception.
+async function get_error_message(t, mode, size) {
+ const script_base = mode === 'same-origin' ? SAME_ORIGIN_BASE
+ : CROSS_ORIGIN_BASE;
+ const script = script_base + `opaque-script-${size}.js`;
+ const scope = BASE_SCOPE + `?script=${script}`;
+ const reg = await service_worker_unregister_and_register(t, SW_URL, scope);
+ t.add_cleanup(_ => reg.unregister());
+ assert_true(!!reg.installing);
+ await wait_for_state(t, reg.installing, 'activated');
+ const error_promise = wait_for_error();
+ const f = await with_iframe(scope);
+ t.add_cleanup(_ => f.remove());
+ const error = await error_promise;
+ return error;
+}
+
+promise_test(async t => {
+ const error = await get_error_message(t, 'same-origin', 'small');
+ assert_true(error.includes('Intentional error'));
+}, 'Verify small same-origin cache_storage scripts are not opaque.');
+
+promise_test(async t => {
+ const error = await get_error_message(t, 'same-origin', 'large');
+ assert_true(error.includes('Intentional error'));
+}, 'Verify large same-origin cache_storage scripts are not opaque.');
+
+promise_test(async t => {
+ const error = await get_error_message(t, 'cross-origin', 'small');
+ assert_false(error.includes('Intentional error'));
+}, 'Verify small cross-origin cache_storage scripts are opaque.');
+
+promise_test(async t => {
+ const error = await get_error_message(t, 'cross-origin', 'large');
+ assert_false(error.includes('Intentional error'));
+}, 'Verify large cross-origin cache_storage scripts are opaque.');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/partitioned-claim.tentative.https.html b/third_party/web_platform_tests/service-workers/service-worker/partitioned-claim.tentative.https.html
new file mode 100644
index 0000000..1f42c52
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/partitioned-claim.tentative.https.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<title>Service Worker: Partitioned Service Workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/partitioned-utils.js"></script>
+
+<body>
+This test creates a iframe in a first-party context and then registers a
+service worker (such that the iframe client is unclaimed).
+A third-party iframe is then created which has its SW call clients.claim()
+and then the test checks that the 1p iframe was not claimed int he process.
+Finally the test has its SW call clients.claim() and confirms the 1p iframe is
+claimed.
+
+<script>
+promise_test(async t => {
+ const script = './resources/partitioned-storage-sw.js';
+ const scope = './resources/partitioned-';
+
+ // Add a 1p iframe.
+ const wait_frame_url = new URL(
+ './resources/partitioned-service-worker-iframe-claim.html?1p-mode',
+ self.location);
+
+ const frame = await with_iframe(wait_frame_url, false);
+ t.add_cleanup(async () => {
+ frame.remove();
+ });
+
+ // Add service worker to this 1P context.
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Register the message listener.
+ self.addEventListener('message', messageEventHandler);
+
+ // Now we need to create a third-party iframe whose SW will claim it and then
+ // the iframe will postMessage that its serviceWorker.controller state has
+ // changed.
+ const third_party_iframe_url = new URL(
+ './resources/partitioned-service-worker-iframe-claim.html?3p-mode',
+ get_host_info().HTTPS_ORIGIN + self.location.pathname);
+
+ // Create the 3p window (which will in turn create the iframe with the SW)
+ // and await on its data.
+ const frame_3p_data = await loadAndReturnSwData(t, third_party_iframe_url,
+ 'window');
+ assert_equals(frame_3p_data.status, "success",
+ "3p iframe was successfully claimed");
+
+ // Confirm that the 1p iframe wasn't claimed at the same time.
+ const controller_1p_iframe = makeMessagePromise();
+ frame.contentWindow.postMessage({type: "get-controller"});
+ const controller_1p_iframe_data = await controller_1p_iframe;
+ assert_equals(controller_1p_iframe_data.controller, null,
+ "Test iframe client isn't claimed yet.");
+
+
+ // Tell the SW to claim.
+ const claimed_1p_iframe = makeMessagePromise();
+ reg.active.postMessage({type: "claim"});
+ const claimed_1p_iframe_data = await claimed_1p_iframe;
+
+ assert_equals(claimed_1p_iframe_data.status, "success",
+ "iframe client was successfully claimed.");
+
+}, "ServiceWorker's clients.claim() is partitioned");
+</script>
+
+</body>
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/partitioned-getRegistrations.tentative.https.html b/third_party/web_platform_tests/service-workers/service-worker/partitioned-getRegistrations.tentative.https.html
new file mode 100644
index 0000000..7c4d4f1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/partitioned-getRegistrations.tentative.https.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<title>Service Worker: Partitioned Service Workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/partitioned-utils.js"></script>
+
+<body>
+This test loads a SW in a first-party context and gets the SW's (randomly)
+generated ID. It does the same thing for the SW but in a third-party context
+and then confirms that the IDs are different.
+
+<script>
+promise_test(async t => {
+ const script = './resources/partitioned-storage-sw.js'
+ const scope = './resources/partitioned-'
+ const absoluteScope = new URL(scope, window.location).href;
+
+ // Add service worker to this 1P context.
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Register the message listener.
+ self.addEventListener('message', messageEventHandler);
+
+ // Open an iframe that will create a promise within the SW.
+ // The query param is there to track which request the service worker is
+ // handling.
+ //
+ // This promise is necessary to prevent the service worker from being
+ // shutdown during the test which would cause a new ID to be generated
+ // and thus invalidate the test.
+ const wait_frame_url = new URL(
+ './resources/partitioned-waitUntilResolved.fakehtml?From1pFrame',
+ self.location);
+
+ // We don't really need the data the SW sent us from this request
+ // but we can use the ID to confirm the SW wasn't shut down during the
+ // test.
+ const wait_frame_1p_data = await loadAndReturnSwData(t, wait_frame_url,
+ 'iframe');
+
+ // Now we need to create a third-party iframe that will send us its SW's
+ // ID.
+ const third_party_iframe_url = new URL(
+ './resources/partitioned-service-worker-third-party-iframe-getRegistrations.html',
+ get_host_info().HTTPS_ORIGIN + self.location.pathname);
+
+ // Create the 3p window (which will in turn create the iframe with the SW)
+ // and await on its data.
+ const frame_3p_ID = await loadAndReturnSwData(t, third_party_iframe_url,
+ 'window');
+
+ // Now get this frame's SW's ID.
+ const frame_1p_ID_promise = makeMessagePromise();
+
+ const retrieved_registrations =
+ await navigator.serviceWorker.getRegistrations();
+ // It's possible that other tests have left behind other service workers.
+ // This steps filters those other SWs out.
+ const filtered_registrations =
+ retrieved_registrations.filter(reg => reg.scope == absoluteScope);
+
+ // Register a listener on the service worker container and then forward to
+ // the self event listener so we can reuse the existing message promise
+ // function.
+ navigator.serviceWorker.addEventListener('message', evt => {
+ self.postMessage(evt.data, '*');
+ });
+
+ filtered_registrations[0].active.postMessage({type: "get-id"});
+
+ const frame_1p_ID = await frame_1p_ID_promise;
+
+ // First check that the SW didn't shutdown during the run of the test.
+ // (Note: We're not using assert_equals because random values make it
+ // difficult to use a test expectations file.)
+ assert_true(wait_frame_1p_data.ID === frame_1p_ID.ID,
+ "1p SW didn't shutdown");
+ // Now check that the 1p and 3p IDs differ.
+ assert_false(frame_1p_ID.ID === frame_3p_ID.ID,
+ "1p SW ID matches 3p SW ID");
+
+ // Finally, for clean up, resolve the SW's promise so it stops waiting.
+ const resolve_frame_url = new URL(
+ './resources/partitioned-resolve.fakehtml?From1pFrame', self.location);
+
+ // We don't care about the data.
+ await loadAndReturnSwData(t, resolve_frame_url, 'iframe');
+
+}, "ServiceWorker's getRegistrations() is partitioned");
+
+
+</script>
+
+</body>
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/partitioned-matchAll.tentative.https.html b/third_party/web_platform_tests/service-workers/service-worker/partitioned-matchAll.tentative.https.html
new file mode 100644
index 0000000..46beec8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/partitioned-matchAll.tentative.https.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<title>Service Worker: Partitioned Service Workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/partitioned-utils.js"></script>
+
+<body>
+This test loads a SW in a first-party context and gets has the SW send
+its list of clients from client.matchAll(). It does the same thing for the
+SW in a third-party context as well and confirms that each SW see's the correct
+clients and that they don't see eachother's clients.
+
+<script>
+promise_test(async t => {
+
+ const script = './resources/partitioned-storage-sw.js'
+ const scope = './resources/partitioned-'
+
+ // Add service worker to this 1P context.
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Register the message listener.
+ self.addEventListener('message', messageEventHandler);
+
+ // Create a third-party iframe that will send us its SW's clients.
+ const third_party_iframe_url = new URL(
+ './resources/partitioned-service-worker-third-party-iframe-matchAll.html',
+ get_host_info().HTTPS_ORIGIN + self.location.pathname);
+
+ const {urls_list: frame_3p_urls_list} = await loadAndReturnSwData(t,
+ third_party_iframe_url, 'window');
+
+ // Register a listener on the service worker container and then forward to
+ // the self event listener so we can reuse the existing message promise
+ // function.
+ navigator.serviceWorker.addEventListener('message', evt => {
+ self.postMessage(evt.data, '*');
+ });
+
+ const frame_1p_data_promise = makeMessagePromise();
+
+ reg.active.postMessage({type: "get-match-all"});
+
+ const {urls_list: frame_1p_urls_list} = await frame_1p_data_promise;
+
+ // If partitioning is working, the 1p and 3p SWs should only see a single
+ // client.
+ assert_equals(frame_3p_urls_list.length, 1);
+ assert_equals(frame_1p_urls_list.length, 1);
+ // Confirm that the expected URL was seen by each.
+ assert_equals(frame_3p_urls_list[0], third_party_iframe_url.toString(),
+ "3p SW has the correct client url.");
+ assert_equals(frame_1p_urls_list[0], window.location.href,
+ "1P SW has the correct client url.");
+}, "ServiceWorker's matchAll() is partitioned");
+
+
+</script>
+
+</body>
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/partitioned.tentative.https.html b/third_party/web_platform_tests/service-workers/service-worker/partitioned.tentative.https.html
new file mode 100644
index 0000000..17a375f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/partitioned.tentative.https.html
@@ -0,0 +1,188 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<title>Service Worker: Partitioned Service Workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/partitioned-utils.js"></script>
+
+<body>
+ <!-- Debugging text for both test cases -->
+ The 3p iframe's postMessage:
+ <p id="iframe_response">No message received</p>
+
+ The nested iframe's postMessage:
+ <p id="nested_iframe_response">No message received</p>
+
+<script>
+promise_test(async t => {
+ const script = './resources/partitioned-storage-sw.js'
+ const scope = './resources/partitioned-'
+
+ // Add service worker to this 1P context. wait_for_state() and
+ // service_worker_unregister_and_register() are helper functions
+ // for creating test ServiceWorkers defined in:
+ // service-workers/service-worker/resources/test-helpers.sub.js
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Registers the message listener with messageEventHandler(), defined in:
+ // service-workers/service-worker/resources/partitioned-utils.js
+ self.addEventListener('message', messageEventHandler);
+
+ // Open an iframe that will create a promise within the SW.
+ // Defined in service-workers/service-worker/resources/partitioned-storage-sw.js:
+ // `waitUntilResolved.fakehtml`: URL scope that creates the promise.
+ // `?From1pFrame`: query param that tracks which request the service worker is
+ // handling.
+ const wait_frame_url = new URL(
+ './resources/partitioned-waitUntilResolved.fakehtml?From1pFrame',
+ self.location);
+
+ // Loads a child iframe with wait_frame_url as the content and returns
+ // a promise for the data messaged from the loaded iframe.
+ // loadAndReturnSwData() defined in:
+ // service-workers/service-worker/resources/partitioned-utils.js:
+ const wait_frame_1p_data = await loadAndReturnSwData(t, wait_frame_url,
+ 'iframe');
+ assert_equals(wait_frame_1p_data.source, 'From1pFrame',
+ 'The data for the 1p frame came from the wrong source');
+
+ // Now create a 3p iframe that will try to resolve the SW in a 3p context.
+ const third_party_iframe_url = new URL(
+ './resources/partitioned-service-worker-third-party-iframe.html',
+ get_host_info().HTTPS_ORIGIN + self.location.pathname);
+
+ // loadAndReturnSwData() creates a HTTPS_NOTSAMESITE_ORIGIN or 3p `window`
+ // element which embeds an iframe with the ServiceWorker and returns
+ // a promise of the data messaged from that frame.
+ const frame_3p_data = await loadAndReturnSwData(t, third_party_iframe_url, 'window');
+ assert_equals(frame_3p_data.source, 'From3pFrame',
+ 'The data for the 3p frame came from the wrong source');
+
+ // Print some debug info to the main frame.
+ document.getElementById("iframe_response").innerHTML =
+ "3p iframe's has_pending: " + frame_3p_data.has_pending + " source: " +
+ frame_3p_data.source + ". ";
+
+ // Now do the same for the 1p iframe.
+ // Defined in service-workers/service-worker/resources/partitioned-storage-sw.js:
+ // `resolve.fakehtml`: URL scope that resolves the promise.
+ const resolve_frame_url = new URL(
+ './resources/partitioned-resolve.fakehtml?From1pFrame', self.location);
+
+ const frame_1p_data = await loadAndReturnSwData(t, resolve_frame_url,
+ 'iframe');
+ assert_equals(frame_1p_data.source, 'From1pFrame',
+ 'The data for the 1p frame came from the wrong source');
+ // Both the 1p frames should have been serviced by the same service worker ID.
+ // If this isn't the case then that means the SW could have been deactivated
+ // which invalidates the test.
+ assert_equals(frame_1p_data.ID, wait_frame_1p_data.ID,
+ 'The 1p frames were serviced by different service workers.');
+
+ document.getElementById("iframe_response").innerHTML +=
+ "1p iframe's has_pending: " + frame_1p_data.has_pending + " source: " +
+ frame_1p_data.source;
+
+ // If partitioning is working correctly then only the 1p iframe should see
+ // (and resolve) its SW's promise. Additionally the two frames should see
+ // different IDs.
+ assert_true(frame_1p_data.has_pending,
+ 'The 1p iframe saw a pending promise in the service worker.');
+ assert_false(frame_3p_data.has_pending,
+ 'The 3p iframe saw a pending promise in the service worker.');
+ assert_not_equals(frame_1p_data.ID, frame_3p_data.ID,
+ 'The frames were serviced by the same service worker thread.');
+}, 'Services workers under different top-level sites are partitioned.');
+
+// Optional Test: Checking for partitioned ServiceWorkers in an A->B->A
+// (nested-iframes with cross-site ancestor) scenario.
+promise_test(async t => {
+ const script = './resources/partitioned-storage-sw.js'
+ const scope = './resources/partitioned-'
+
+ // Add service worker to this 1P context. wait_for_state() and
+ // service_worker_unregister_and_register() are helper functions
+ // for creating test ServiceWorkers defined in:
+ // service-workers/service-worker/resources/test-helpers.sub.js
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Registers the message listener with messageEventHandler(), defined in:
+ // service-workers/service-worker/resources/partitioned-utils.js
+ self.addEventListener('message', messageEventHandler);
+
+ // Open an iframe that will create a promise within the SW.
+ // Defined in service-workers/service-worker/resources/partitioned-storage-sw.js:
+ // `waitUntilResolved.fakehtml`: URL scope that creates the promise.
+ // `?From1pFrame`: query param that tracks which request the service worker is
+ // handling.
+ const wait_frame_url = new URL(
+ './resources/partitioned-waitUntilResolved.fakehtml?From1pFrame',
+ self.location);
+
+ // Load a child iframe with wait_frame_url as the content.
+ // loadAndReturnSwData() defined in:
+ // service-workers/service-worker/resources/partitioned-utils.js:
+ const wait_frame_1p_data = await loadAndReturnSwData(t, wait_frame_url,
+ 'iframe');
+ assert_equals(wait_frame_1p_data.source, 'From1pFrame',
+ 'The data for the 1p frame came from the wrong source');
+
+ // Now create a set of nested iframes in the configuration A1->B->A2
+ // where B is cross-site and A2 is same-site to this top-level
+ // site (A1). The innermost iframe of the nested iframes (A2) will
+ // create an additional iframe to finally resolve the ServiceWorker.
+ const nested_iframe_url = new URL(
+ './resources/partitioned-service-worker-nested-iframe-parent.html',
+ get_host_info().HTTPS_NOTSAMESITE_ORIGIN + self.location.pathname);
+
+ // Create the nested iframes (which will in turn create the iframe
+ // with the ServiceWorker) and await on receiving its data.
+ const nested_iframe_data = await loadAndReturnSwData(t, nested_iframe_url, 'iframe');
+ assert_equals(nested_iframe_data.source, 'FromNestedFrame',
+ 'The data for the nested iframe frame came from the wrong source');
+
+ // Print some debug info to the main frame.
+ document.getElementById("nested_iframe_response").innerHTML =
+ "Nested iframe's has_pending: " + nested_iframe_data.has_pending + " source: " +
+ nested_iframe_data.source + ". ";
+
+ // Now do the same for the 1p iframe.
+ // Defined in service-workers/service-worker/resources/partitioned-storage-sw.js:
+ // `resolve.fakehtml`: URL scope that resolves the promise.
+ const resolve_frame_url = new URL(
+ './resources/partitioned-resolve.fakehtml?From1pFrame', self.location);
+
+ const frame_1p_data = await loadAndReturnSwData(t, resolve_frame_url,
+ 'iframe');
+ assert_equals(frame_1p_data.source, 'From1pFrame',
+ 'The data for the 1p frame came from the wrong source');
+ // Both the 1p frames should have been serviced by the same service worker ID.
+ // If this isn't the case then that means the SW could have been deactivated
+ // which invalidates the test.
+ assert_equals(frame_1p_data.ID, wait_frame_1p_data.ID,
+ 'The 1p frames were serviced by different service workers.');
+
+ document.getElementById("nested_iframe_response").innerHTML +=
+ "1p iframe's has_pending: " + frame_1p_data.has_pending + " source: " +
+ frame_1p_data.source;
+
+ // If partitioning is working correctly then only the 1p iframe should see
+ // (and resolve) its SW's promise. Additionally, the innermost iframe of
+ // the nested iframes (A2 in the configuration A1->B->A2) should have a
+ // different service worker ID than the 1p (A1) frame.
+ assert_true(frame_1p_data.has_pending,
+ 'The 1p iframe saw a pending promise in the service worker.');
+ assert_false(nested_iframe_data.has_pending,
+ 'The 3p iframe saw a pending promise in the service worker.');
+ assert_not_equals(frame_1p_data.ID, nested_iframe_data.ID,
+ 'The frames were serviced by the same service worker thread.');
+}, 'Services workers with cross-site ancestors are partitioned.');
+
+</script>
+</body>
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/performance-timeline.https.html b/third_party/web_platform_tests/service-workers/service-worker/performance-timeline.https.html
new file mode 100644
index 0000000..e56e6fe
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/performance-timeline.https.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+service_worker_test(
+ 'resources/performance-timeline-worker.js',
+ 'Test Performance Timeline API in Service Worker');
+
+// The purpose of this test is to verify that service worker overhead
+// is included in the Performance API's timing information.
+promise_test(t => {
+ let script = 'resources/empty-but-slow-worker.js';
+ let scope = 'resources/sample.txt?slow-sw-timing';
+ let url = new URL(scope, window.location).href;
+ let slowURL = url + '&slow';
+ let frame;
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(reg => {
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(_ => with_iframe(scope))
+ .then(f => {
+ frame = f;
+ return frame.contentWindow.fetch(url).then(r => r && r.text());
+ })
+ .then(_ => {
+ return frame.contentWindow.fetch(slowURL).then(r => r && r.text());
+ })
+ .then(_ => {
+ function elapsed(u) {
+ let entry = frame.contentWindow.performance.getEntriesByName(u);
+ return entry[0] ? entry[0].duration : undefined;
+ }
+ let urlTime = elapsed(url);
+ let slowURLTime = elapsed(slowURL);
+ // Verify the request slowed by the service worker is indeed measured
+ // to be slower. Note, we compare to smaller delay instead of the exact
+ // delay amount to avoid making the test racy under automation.
+ assert_greater_than(slowURLTime, urlTime + 1000,
+ 'Slow service worker request should measure increased delay.');
+ frame.remove();
+ })
+}, 'empty service worker fetch event included in performance timings');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/postmessage-blob-url.https.html b/third_party/web_platform_tests/service-workers/service-worker/postmessage-blob-url.https.html
new file mode 100644
index 0000000..16fddd5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/postmessage-blob-url.https.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage Blob URL</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ let script = 'resources/postmessage-blob-url.js';
+ let scope = 'resources/blank.html';
+ let registration;
+ let blobText = 'Blob text';
+ let blob;
+ let blobUrl;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ add_completion_callback(() => r.unregister());
+ registration = r;
+ let worker = registration.installing;
+ blob = new Blob([blobText]);
+ blobUrl = URL.createObjectURL(blob);
+ return new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => { resolve(e.data); }
+ worker.postMessage(blobUrl);
+ });
+ })
+ .then(response => {
+ assert_equals(response, 'Worker reply:' + blobText);
+ URL.revokeObjectURL(blobUrl);
+ return registration.unregister();
+ });
+ }, 'postMessage Blob URL to a ServiceWorker');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/postmessage-from-waiting-serviceworker.https.html b/third_party/web_platform_tests/service-workers/service-worker/postmessage-from-waiting-serviceworker.https.html
new file mode 100644
index 0000000..117def9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/postmessage-from-waiting-serviceworker.https.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage from waiting serviceworker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+function echo(worker, data) {
+ return new Promise(resolve => {
+ navigator.serviceWorker.addEventListener('message', function onMsg(evt) {
+ navigator.serviceWorker.removeEventListener('message', onMsg);
+ resolve(evt);
+ });
+ worker.postMessage(data);
+ });
+}
+
+promise_test(t => {
+ let script = 'resources/echo-message-to-source-worker.js';
+ let scope = 'resources/client-postmessage-from-wait-serviceworker';
+ let registration;
+ let frame;
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(swr => {
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ registration = swr;
+ return wait_for_state(t, registration.installing, 'activated');
+ }).then(_ => {
+ return with_iframe(scope);
+ }).then(f => {
+ frame = f;
+ return navigator.serviceWorker.register(script + '?update', { scope: scope })
+ }).then(swr => {
+ assert_equals(swr, registration, 'should be same registration');
+ return wait_for_state(t, registration.installing, 'installed');
+ }).then(_ => {
+ return echo(registration.waiting, 'waiting');
+ }).then(evt => {
+ assert_equals(evt.source, registration.waiting,
+ 'message event source should be correct');
+ return echo(registration.active, 'active');
+ }).then(evt => {
+ assert_equals(evt.source, registration.active,
+ 'message event source should be correct');
+ frame.remove();
+ });
+}, 'Client.postMessage() from waiting serviceworker.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/postmessage-msgport-to-client.https.html b/third_party/web_platform_tests/service-workers/service-worker/postmessage-msgport-to-client.https.html
new file mode 100644
index 0000000..29c0560
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/postmessage-msgport-to-client.https.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage via MessagePort to Client</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ var script = 'resources/postmessage-msgport-to-client-worker.js';
+ var scope = 'resources/blank.html';
+ var port;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(registration => {
+ add_completion_callback(() => registration.unregister());
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => with_iframe(scope))
+ .then(frame => {
+ t.add_cleanup(() => frame.remove());
+ return new Promise(resolve => {
+ var w = frame.contentWindow;
+ w.navigator.serviceWorker.onmessage = resolve;
+ w.navigator.serviceWorker.controller.postMessage('ping');
+ });
+ })
+ .then(e => {
+ port = e.ports[0];
+ port.postMessage({value: 1});
+ port.postMessage({value: 2});
+ port.postMessage({done: true});
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => {
+ assert_equals(e.data.ack, 'Acking value: 1');
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => {
+ assert_equals(e.data.ack, 'Acking value: 2');
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => { assert_true(e.data.done, 'done'); });
+ }, 'postMessage MessagePorts from ServiceWorker to Client');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/postmessage-to-client-message-queue.https.html b/third_party/web_platform_tests/service-workers/service-worker/postmessage-to-client-message-queue.https.html
new file mode 100644
index 0000000..83e5f45
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/postmessage-to-client-message-queue.https.html
@@ -0,0 +1,212 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage to Client (message queue)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// This function creates a message listener that captures all messages
+// sent to this window and matches them with corresponding requests.
+// This frees test code from having to use clunky constructs just to
+// avoid race conditions, since the relative order of message and
+// request arrival doesn't matter.
+function create_message_listener(t) {
+ const listener = {
+ messages: new Set(),
+ requests: new Set(),
+ waitFor: function(predicate) {
+ for (const event of this.messages) {
+ // If a message satisfying the predicate has already
+ // arrived, it gets matched to this request.
+ if (predicate(event)) {
+ this.messages.delete(event);
+ return Promise.resolve(event);
+ }
+ }
+
+ // If no match was found, the request is stored and a
+ // promise is returned.
+ const request = { predicate };
+ const promise = new Promise(resolve => request.resolve = resolve);
+ this.requests.add(request);
+ return promise;
+ }
+ };
+ window.onmessage = t.step_func(event => {
+ for (const request of listener.requests) {
+ // If the new message matches a stored request's
+ // predicate, the request's promise is resolved with this
+ // message.
+ if (request.predicate(event)) {
+ listener.requests.delete(request);
+ request.resolve(event);
+ return;
+ }
+ };
+
+ // No outstanding request for this message, store it in case
+ // it's requested later.
+ listener.messages.add(event);
+ });
+ return listener;
+}
+
+async function service_worker_register_and_activate(t, script, scope) {
+ const registration = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => registration.unregister());
+ const worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+ return worker;
+}
+
+// Add an iframe (parent) whose document contains a nested iframe
+// (child), then set the child's src attribute to child_url and return
+// its Window (without waiting for it to finish loading).
+async function with_nested_iframes(t, child_url) {
+ const parent = await with_iframe('resources/nested-iframe-parent.html?role=parent');
+ t.add_cleanup(() => parent.remove());
+ const child = parent.contentWindow.document.getElementById('child');
+ child.setAttribute('src', child_url);
+ return child.contentWindow;
+}
+
+// Returns a predicate matching a fetch message with the specified
+// key.
+function fetch_message(key) {
+ return event => event.data.type === 'fetch' && event.data.key === key;
+}
+
+// Returns a predicate matching a ping message with the specified
+// payload.
+function ping_message(data) {
+ return event => event.data.type === 'ping' && event.data.data === data;
+}
+
+// A client message queue test is a testharness.js test with some
+// additional setup:
+// 1. A listener (see create_message_listener)
+// 2. An active service worker
+// 3. Two nested iframes
+// 4. A state transition function that controls the order of events
+// during the test
+function client_message_queue_test(url, test_function, description) {
+ promise_test(async t => {
+ t.listener = create_message_listener(t);
+
+ const script = 'resources/stalling-service-worker.js';
+ const scope = 'resources/';
+ t.service_worker = await service_worker_register_and_activate(t, script, scope);
+
+ // We create two nested iframes such that both are controlled by
+ // the newly installed service worker.
+ const child_url = url + '?role=child';
+ t.frame = await with_nested_iframes(t, child_url);
+
+ t.state_transition = async function(from, to, scripts) {
+ // A state transition begins with the child's parser
+ // fetching a script due to a <script> tag. The request
+ // arrives at the service worker, which notifies the
+ // parent, which in turn notifies the test. Note that the
+ // event loop keeps spinning while the parser is waiting.
+ const request = await this.listener.waitFor(fetch_message(to));
+
+ // The test instructs the service worker to send two ping
+ // messages through the Client interface: first to the
+ // child, then to the parent.
+ this.service_worker.postMessage(from);
+
+ // When the parent receives the ping message, it forwards
+ // it to the test. Assuming that messages to both child
+ // and parent are mapped to the same task queue (this is
+ // not [yet] required by the spec), receiving this message
+ // guarantees that the child has already dispatched its
+ // message if it was allowed to do so.
+ await this.listener.waitFor(ping_message(from));
+
+ // Finally, reply to the service worker's fetch
+ // notification with the script it should use as the fetch
+ // request's response. This is a defensive mechanism that
+ // ensures the child's parser really is blocked until the
+ // test is ready to continue.
+ request.ports[0].postMessage([`state = '${to}';`].concat(scripts));
+ };
+
+ await test_function(t);
+ }, description);
+}
+
+function client_message_queue_enable_test(
+ install_script,
+ start_script,
+ earliest_dispatch,
+ description)
+{
+ function assert_state_less_than_equal(state1, state2, explanation) {
+ const states = ['init', 'install', 'start', 'finish', 'loaded'];
+ const index1 = states.indexOf(state1);
+ const index2 = states.indexOf(state2);
+ if (index1 > index2)
+ assert_unreached(explanation);
+ }
+
+ client_message_queue_test('enable-client-message-queue.html', async t => {
+ // While parsing the child's document, the child transitions
+ // from the 'init' state all the way to the 'finish' state.
+ // Once parsing is finished it would enter the final 'loaded'
+ // state. All but the last transition require assitance from
+ // the test.
+ await t.state_transition('init', 'install', [install_script]);
+ await t.state_transition('install', 'start', [start_script]);
+ await t.state_transition('start', 'finish', []);
+
+ // Wait for all messages to get dispatched on the child's
+ // ServiceWorkerContainer and then verify that each message
+ // was dispatched after |earliest_dispatch|.
+ const report = await t.frame.report;
+ ['init', 'install', 'start'].forEach(state => {
+ const explanation = `Message sent in state '${state}' was dispatched in '${report[state]}', should be dispatched no earlier than '${earliest_dispatch}'`;
+ assert_state_less_than_equal(earliest_dispatch,
+ report[state],
+ explanation);
+ });
+ }, description);
+}
+
+const empty_script = ``;
+
+const add_event_listener =
+ `navigator.serviceWorker.addEventListener('message', handle_message);`;
+
+const set_onmessage = `navigator.serviceWorker.onmessage = handle_message;`;
+
+const start_messages = `navigator.serviceWorker.startMessages();`;
+
+client_message_queue_enable_test(add_event_listener, empty_script, 'loaded',
+ 'Messages from ServiceWorker to Client only received after DOMContentLoaded event.');
+
+client_message_queue_enable_test(add_event_listener, start_messages, 'start',
+ 'Messages from ServiceWorker to Client only received after calling startMessages().');
+
+client_message_queue_enable_test(set_onmessage, empty_script, 'install',
+ 'Messages from ServiceWorker to Client only received after setting onmessage.');
+
+const resolve_manual_promise = `resolve_manual_promise();`
+
+async function test_microtasks_when_client_message_queue_enabled(t, scripts) {
+ await t.state_transition('init', 'start', scripts.concat([resolve_manual_promise]));
+ let result = await t.frame.result;
+ assert_equals(result[0], 'microtask', 'The microtask was executed first.');
+ assert_equals(result[1], 'message', 'The message was dispatched.');
+}
+
+client_message_queue_test('message-vs-microtask.html', t => {
+ return test_microtasks_when_client_message_queue_enabled(t, [
+ add_event_listener,
+ start_messages,
+ ]);
+}, 'Microtasks run before dispatching messages after calling startMessages().');
+
+client_message_queue_test('message-vs-microtask.html', t => {
+ return test_microtasks_when_client_message_queue_enabled(t, [set_onmessage]);
+}, 'Microtasks run before dispatching messages after setting onmessage.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/postmessage-to-client.https.html b/third_party/web_platform_tests/service-workers/service-worker/postmessage-to-client.https.html
new file mode 100644
index 0000000..f834a4b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/postmessage-to-client.https.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage to Client</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(async t => {
+ const script = 'resources/postmessage-to-client-worker.js';
+ const scope = 'resources/blank.html';
+
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+ const w = frame.contentWindow;
+
+ w.navigator.serviceWorker.controller.postMessage('ping');
+ let e = await new Promise(r => w.navigator.serviceWorker.onmessage = r);
+
+ assert_equals(e.constructor, w.MessageEvent,
+ 'message events should use MessageEvent interface.');
+ assert_equals(e.type, 'message', 'type should be "message".');
+ assert_false(e.bubbles, 'message events should not bubble.');
+ assert_false(e.cancelable, 'message events should not be cancelable.');
+ assert_equals(e.origin, location.origin,
+ 'origin of message should be origin of Service Worker.');
+ assert_equals(e.lastEventId, '',
+ 'lastEventId should be an empty string.');
+ assert_equals(e.source.constructor, w.ServiceWorker,
+ 'source should use ServiceWorker interface.');
+ assert_equals(e.source, w.navigator.serviceWorker.controller,
+ 'source should be the service worker that sent the message.');
+ assert_equals(e.ports.length, 0, 'ports should be an empty array.');
+ assert_equals(e.data, 'Sending message via clients');
+
+ e = await new Promise(r => w.navigator.serviceWorker.onmessage = r);
+ assert_equals(e.data, 'quit');
+}, 'postMessage from ServiceWorker to Client.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/postmessage.https.html b/third_party/web_platform_tests/service-workers/service-worker/postmessage.https.html
new file mode 100644
index 0000000..7abb302
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/postmessage.https.html
@@ -0,0 +1,202 @@
+<!DOCTYPE html>
+<title>Service Worker: postMessage</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ var script = 'resources/postmessage-worker.js';
+ var scope = 'resources/blank.html';
+ var registration;
+ var worker;
+ var port;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ t.add_cleanup(() => r.unregister());
+ registration = r;
+ worker = registration.installing;
+
+ var messageChannel = new MessageChannel();
+ port = messageChannel.port1;
+ return new Promise(resolve => {
+ port.onmessage = resolve;
+ worker.postMessage({port: messageChannel.port2},
+ [messageChannel.port2]);
+ worker.postMessage({value: 1});
+ worker.postMessage({value: 2});
+ worker.postMessage({done: true});
+ });
+ })
+ .then(e => {
+ assert_equals(e.data, 'Acking value: 1');
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => {
+ assert_equals(e.data, 'Acking value: 2');
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => {
+ assert_equals(e.data, 'quit');
+ return registration.unregister(scope);
+ });
+ }, 'postMessage to a ServiceWorker (and back via MessagePort)');
+
+promise_test(t => {
+ var script = 'resources/postmessage-transferables-worker.js';
+ var scope = 'resources/blank.html';
+ var sw = navigator.serviceWorker;
+
+ var message = 'Hello, world!';
+ var text_encoder = new TextEncoder;
+ var text_decoder = new TextDecoder;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ t.add_cleanup(() => r.unregister());
+
+ var ab = text_encoder.encode(message);
+ assert_equals(ab.byteLength, message.length);
+ r.installing.postMessage(ab, [ab.buffer]);
+ assert_equals(text_decoder.decode(ab), '');
+ assert_equals(ab.byteLength, 0);
+
+ return new Promise(resolve => { sw.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify the integrity of the transferred array buffer.
+ assert_equals(e.data.content, message);
+ assert_equals(e.data.byteLength, message.length);
+ return new Promise(resolve => { sw.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify the integrity of the array buffer sent back from
+ // ServiceWorker via Client.postMessage.
+ assert_equals(text_decoder.decode(e.data), message);
+ assert_equals(e.data.byteLength, message.length);
+ return new Promise(resolve => { sw.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify that the array buffer on ServiceWorker is neutered.
+ assert_equals(e.data.content, '');
+ assert_equals(e.data.byteLength, 0);
+ });
+ }, 'postMessage a transferable ArrayBuffer between ServiceWorker and Client');
+
+promise_test(t => {
+ var script = 'resources/postmessage-transferables-worker.js';
+ var scope = 'resources/blank.html';
+ var message = 'Hello, world!';
+ var text_encoder = new TextEncoder;
+ var text_decoder = new TextDecoder;
+ var port;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ t.add_cleanup(() => r.unregister());
+
+ var channel = new MessageChannel;
+ port = channel.port1;
+ r.installing.postMessage(undefined, [channel.port2]);
+
+ var ab = text_encoder.encode(message);
+ assert_equals(ab.byteLength, message.length);
+ port.postMessage(ab, [ab.buffer]);
+ assert_equals(text_decoder.decode(ab), '');
+ assert_equals(ab.byteLength, 0);
+
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify the integrity of the transferred array buffer.
+ assert_equals(e.data.content, message);
+ assert_equals(e.data.byteLength, message.length);
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify the integrity of the array buffer sent back from
+ // ServiceWorker via Client.postMessage.
+ assert_equals(text_decoder.decode(e.data), message);
+ assert_equals(e.data.byteLength, message.length);
+ return new Promise(resolve => { port.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify that the array buffer on ServiceWorker is neutered.
+ assert_equals(e.data.content, '');
+ assert_equals(e.data.byteLength, 0);
+ });
+ }, 'postMessage a transferable ArrayBuffer between ServiceWorker and Client' +
+ ' over MessagePort');
+
+ promise_test(t => {
+ var script = 'resources/postmessage-dictionary-transferables-worker.js';
+ var scope = 'resources/blank.html';
+ var sw = navigator.serviceWorker;
+
+ var message = 'Hello, world!';
+ var text_encoder = new TextEncoder;
+ var text_decoder = new TextDecoder;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(r => {
+ t.add_cleanup(() => r.unregister());
+
+ var ab = text_encoder.encode(message);
+ assert_equals(ab.byteLength, message.length);
+ r.installing.postMessage(ab, {transfer: [ab.buffer]});
+ assert_equals(text_decoder.decode(ab), '');
+ assert_equals(ab.byteLength, 0);
+
+ return new Promise(resolve => { sw.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify the integrity of the transferred array buffer.
+ assert_equals(e.data.content, message);
+ assert_equals(e.data.byteLength, message.length);
+ return new Promise(resolve => { sw.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify the integrity of the array buffer sent back from
+ // ServiceWorker via Client.postMessage.
+ assert_equals(text_decoder.decode(e.data), message);
+ assert_equals(e.data.byteLength, message.length);
+ return new Promise(resolve => { sw.onmessage = resolve; });
+ })
+ .then(e => {
+ // Verify that the array buffer on ServiceWorker is neutered.
+ assert_equals(e.data.content, '');
+ assert_equals(e.data.byteLength, 0);
+ });
+ }, 'postMessage with dictionary a transferable ArrayBuffer between ServiceWorker and Client');
+
+ promise_test(async t => {
+ const firstScript = 'resources/postmessage-echo-worker.js?one';
+ const secondScript = 'resources/postmessage-echo-worker.js?two';
+ const scope = 'resources/';
+
+ const registration = await service_worker_unregister_and_register(t, firstScript, scope);
+ t.add_cleanup(() => registration.unregister());
+ const firstWorker = registration.installing;
+
+ const messagePromise = new Promise(resolve => {
+ navigator.serviceWorker.addEventListener('message', (event) => {
+ resolve(event.data);
+ }, {once: true});
+ });
+
+ await wait_for_state(t, firstWorker, 'activated');
+ await navigator.serviceWorker.register(secondScript, {scope});
+ const secondWorker = registration.installing;
+ await wait_for_state(t, firstWorker, 'redundant');
+
+ // postMessage() to a redundant worker should be dropped silently.
+ // Historically, this threw an exception.
+ firstWorker.postMessage('firstWorker');
+
+ // To test somewhat that it was not received, send a message to another
+ // worker and check that we get a reply for that one.
+ secondWorker.postMessage('secondWorker');
+ const data = await messagePromise;
+ assert_equals(data, 'secondWorker');
+ }, 'postMessage to a redundant worker');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/ready.https.window.js b/third_party/web_platform_tests/service-workers/service-worker/ready.https.window.js
new file mode 100644
index 0000000..6c4e270
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/ready.https.window.js
@@ -0,0 +1,223 @@
+// META: title=Service Worker: navigator.serviceWorker.ready
+// META: script=resources/test-helpers.sub.js
+
+test(() => {
+ assert_equals(
+ navigator.serviceWorker.ready,
+ navigator.serviceWorker.ready,
+ 'repeated access to ready without intervening registrations should return the same Promise object'
+ );
+}, 'ready returns the same Promise object');
+
+promise_test(async t => {
+ const frame = await with_iframe('resources/blank.html?uncontrolled');
+ t.add_cleanup(() => frame.remove());
+
+ const promise = frame.contentWindow.navigator.serviceWorker.ready;
+
+ assert_equals(
+ Object.getPrototypeOf(promise),
+ frame.contentWindow.Promise.prototype,
+ 'the Promise should be in the context of the related document'
+ );
+}, 'ready returns a Promise object in the context of the related document');
+
+promise_test(async t => {
+ const url = 'resources/empty-worker.js';
+ const scope = 'resources/blank.html?ready-controlled';
+ const expectedURL = normalizeURL(url);
+ const registration = await service_worker_unregister_and_register(t, url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ const readyReg = await frame.contentWindow.navigator.serviceWorker.ready;
+
+ assert_equals(readyReg.installing, null, 'installing should be null');
+ assert_equals(readyReg.waiting, null, 'waiting should be null');
+ assert_equals(readyReg.active.scriptURL, expectedURL, 'active after ready should not be null');
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller,
+ readyReg.active,
+ 'the controller should be the active worker'
+ );
+ assert_in_array(
+ readyReg.active.state,
+ ['activating', 'activated'],
+ '.ready should be resolved when the registration has an active worker'
+ );
+}, 'ready on a controlled document');
+
+promise_test(async t => {
+ const url = 'resources/empty-worker.js';
+ const scope = 'resources/blank.html?ready-potential-controlled';
+ const expected_url = normalizeURL(url);
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ const registration = await navigator.serviceWorker.register(url, { scope });
+ t.add_cleanup(() => registration.unregister());
+
+ const readyReg = await frame.contentWindow.navigator.serviceWorker.ready;
+
+ assert_equals(readyReg.installing, null, 'installing should be null');
+ assert_equals(readyReg.waiting, null, 'waiting should be null.')
+ assert_equals(readyReg.active.scriptURL, expected_url, 'active after ready should not be null');
+ assert_in_array(
+ readyReg.active.state,
+ ['activating', 'activated'],
+ '.ready should be resolved when the registration has an active worker'
+ );
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller,
+ null,
+ 'uncontrolled document should not have a controller'
+ );
+}, 'ready on a potential controlled document');
+
+promise_test(async t => {
+ const url = 'resources/empty-worker.js';
+ const scope = 'resources/blank.html?ready-installing';
+
+ await service_worker_unregister(t, scope);
+
+ const frame = await with_iframe(scope);
+ const promise = frame.contentWindow.navigator.serviceWorker.ready;
+ navigator.serviceWorker.register(url, { scope });
+ const registration = await promise;
+
+ t.add_cleanup(async () => {
+ await registration.unregister();
+ frame.remove();
+ });
+
+ assert_equals(registration.installing, null, 'installing should be null');
+ assert_equals(registration.waiting, null, 'waiting should be null');
+ assert_not_equals(registration.active, null, 'active after ready should not be null');
+ assert_in_array(
+ registration.active.state,
+ ['activating', 'activated'],
+ '.ready should be resolved when the registration has an active worker'
+ );
+}, 'ready on an iframe whose parent registers a new service worker');
+
+promise_test(async t => {
+ const scope = 'resources/register-iframe.html';
+ const frame = await with_iframe(scope);
+
+ const registration = await frame.contentWindow.navigator.serviceWorker.ready;
+
+ t.add_cleanup(async () => {
+ await registration.unregister();
+ frame.remove();
+ });
+
+ assert_equals(registration.installing, null, 'installing should be null');
+ assert_equals(registration.waiting, null, 'waiting should be null');
+ assert_not_equals(registration.active, null, 'active after ready should not be null');
+ assert_in_array(
+ registration.active.state,
+ ['activating', 'activated'],
+ '.ready should be resolved with "active worker"'
+ );
+ }, 'ready on an iframe that installs a new service worker');
+
+promise_test(async t => {
+ const url = 'resources/empty-worker.js';
+ const matchedScope = 'resources/blank.html?ready-after-match';
+ const longerMatchedScope = 'resources/blank.html?ready-after-match-longer';
+
+ await service_worker_unregister(t, matchedScope);
+ await service_worker_unregister(t, longerMatchedScope);
+
+ const frame = await with_iframe(longerMatchedScope);
+ const registration = await navigator.serviceWorker.register(url, { scope: matchedScope });
+
+ t.add_cleanup(async () => {
+ await registration.unregister();
+ frame.remove();
+ });
+
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const longerRegistration = await navigator.serviceWorker.register(url, { scope: longerMatchedScope });
+
+ t.add_cleanup(() => longerRegistration.unregister());
+
+ const readyReg = await frame.contentWindow.navigator.serviceWorker.ready;
+
+ assert_equals(
+ readyReg.scope,
+ normalizeURL(longerMatchedScope),
+ 'longer matched registration should be returned'
+ );
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller,
+ null,
+ 'controller should be null'
+ );
+}, 'ready after a longer matched registration registered');
+
+promise_test(async t => {
+ const url = 'resources/empty-worker.js';
+ const matchedScope = 'resources/blank.html?ready-after-resolve';
+ const longerMatchedScope = 'resources/blank.html?ready-after-resolve-longer';
+ const registration = await service_worker_unregister_and_register(t, url, matchedScope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const frame = await with_iframe(longerMatchedScope);
+ t.add_cleanup(() => frame.remove());
+
+ const readyReg1 = await frame.contentWindow.navigator.serviceWorker.ready;
+
+ assert_equals(
+ readyReg1.scope,
+ normalizeURL(matchedScope),
+ 'matched registration should be returned'
+ );
+
+ const longerReg = await navigator.serviceWorker.register(url, { scope: longerMatchedScope });
+ t.add_cleanup(() => longerReg.unregister());
+
+ const readyReg2 = await frame.contentWindow.navigator.serviceWorker.ready;
+
+ assert_equals(
+ readyReg2.scope,
+ normalizeURL(matchedScope),
+ 'ready should only be resolved once'
+ );
+}, 'access ready after it has been resolved');
+
+promise_test(async t => {
+ const url1 = 'resources/empty-worker.js';
+ const url2 = url1 + '?2';
+ const matchedScope = 'resources/blank.html?ready-after-unregister';
+ const reg1 = await service_worker_unregister_and_register(t, url1, matchedScope);
+ t.add_cleanup(() => reg1.unregister());
+
+ await wait_for_state(t, reg1.installing, 'activating');
+
+ const frame = await with_iframe(matchedScope);
+ t.add_cleanup(() => frame.remove());
+
+ await reg1.unregister();
+
+ // Ready promise should be pending, waiting for a new registration to arrive
+ const readyPromise = frame.contentWindow.navigator.serviceWorker.ready;
+
+ const reg2 = await navigator.serviceWorker.register(url2, { scope: matchedScope });
+ t.add_cleanup(() => reg2.unregister());
+
+ const readyReg = await readyPromise;
+
+ // Wait for registration update, since it comes from another global, the states are racy.
+ await wait_for_state(t, reg2.installing || reg2.waiting || reg2.active, 'activated');
+
+ assert_equals(readyReg.active.scriptURL, reg2.active.scriptURL, 'Resolves with the second registration');
+ assert_not_equals(reg1, reg2, 'Registrations should be different');
+}, 'resolve ready after unregistering');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/redirected-response.https.html b/third_party/web_platform_tests/service-workers/service-worker/redirected-response.https.html
new file mode 100644
index 0000000..71b35d0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/redirected-response.https.html
@@ -0,0 +1,471 @@
+<!DOCTYPE html>
+<title>Service Worker: Redirected response</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// Tests redirect behavior. It calls fetch_method(url, fetch_option) and tests
+// the resulting response against the expected values. It also adds the
+// response to |cache| and checks the cached response matches the expected
+// values.
+//
+// |options|: a dictionary of parameters for the test
+// |options.url|: the URL to fetch
+// |options.fetch_option|: the options passed to |fetch_method|
+// |options.fetch_method|: the method used to fetch. Useful for testing an
+// iframe's fetch() vs. this page's fetch().
+// |options.expected_type|: The value of response.type
+// |options.expected_redirected|: The value of response.redirected
+// |options.expected_intercepted_urls|: The list of intercepted request URLs.
+function redirected_test(options) {
+ return options.fetch_method.call(null, options.url, options.fetch_option).then(response => {
+ let cloned_response = response.clone();
+ assert_equals(
+ response.type, options.expected_type,
+ 'The response type of response must match. URL: ' + options.url);
+ assert_equals(
+ cloned_response.type, options.expected_type,
+ 'The response type of cloned response must match. URL: ' + options.url);
+ assert_equals(
+ response.redirected, options.expected_redirected,
+ 'The redirected flag of response must match. URL: ' + options.url);
+ assert_equals(
+ cloned_response.redirected, options.expected_redirected,
+ 'The redirected flag of cloned response must match. URL: ' + options.url);
+ if (options.expected_response_url) {
+ assert_equals(
+ cloned_response.url, options.expected_response_url,
+ 'The URL does not meet expectation. URL: ' + options.url);
+ }
+ return cache.put(options.url, response);
+ })
+ .then(_ => cache.match(options.url))
+ .then(response => {
+ assert_equals(
+ response.type, options.expected_type,
+ 'The response type of response in CacheStorage must match. ' +
+ 'URL: ' + options.url);
+ assert_equals(
+ response.redirected, options.expected_redirected,
+ 'The redirected flag of response in CacheStorage must match. ' +
+ 'URL: ' + options.url);
+ return check_intercepted_urls(options.expected_intercepted_urls);
+ });
+}
+
+async function take_intercepted_urls() {
+ const message = new Promise((resolve) => {
+ let channel = new MessageChannel();
+ channel.port1.onmessage = msg => { resolve(msg.data.requestInfos); };
+ worker.postMessage({command: 'getRequestInfos', port: channel.port2},
+ [channel.port2]);
+ });
+ const request_infos = await message;
+ return request_infos.map(info => { return info.url; });
+}
+
+function check_intercepted_urls(expected_urls) {
+ return take_intercepted_urls().then((urls) => {
+ assert_object_equals(urls, expected_urls, 'Intercepted URLs matching.');
+ });
+}
+
+function setup_and_clean() {
+ // To prevent interference from previous tests, take the intercepted URLs from
+ // the service worker.
+ return setup.then(() => take_intercepted_urls());
+}
+
+
+let host_info = get_host_info();
+const REDIRECT_URL = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'resources/redirect.py?Redirect=';
+const TARGET_URL = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'resources/simple.txt?';
+const REDIRECT_TO_TARGET_URL = REDIRECT_URL + encodeURIComponent(TARGET_URL);
+let frame;
+let cache;
+let setup;
+let worker;
+
+promise_test(t => {
+ const SCOPE = 'resources/blank.html?redirected-response';
+ const SCRIPT = 'resources/redirect-worker.js';
+ const CACHE_NAME = 'service-workers/service-worker/redirected-response';
+ setup = service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(registration => {
+ promise_test(
+ () => registration.unregister(),
+ 'restore global state (service worker registration)');
+ worker = registration.installing;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(_ => self.caches.open(CACHE_NAME))
+ .then(c => {
+ cache = c;
+ promise_test(
+ () => self.caches.delete(CACHE_NAME),
+ 'restore global state (caches)');
+ return with_iframe(SCOPE);
+ })
+ .then(f => {
+ frame = f;
+ add_completion_callback(() => f.remove());
+ return check_intercepted_urls(
+ [host_info['HTTPS_ORIGIN'] + base_path() + SCOPE]);
+ });
+ return setup;
+ }, 'initialize global state (service worker registration and caches)');
+
+// ===============================================================
+// Tests for requests that are out-of-scope of the service worker.
+// ===============================================================
+promise_test(t => setup_and_clean()
+ .then(() => redirected_test({url: TARGET_URL,
+ fetch_option: {},
+ fetch_method: self.fetch,
+ expected_type: 'basic',
+ expected_redirected: false,
+ expected_intercepted_urls: []})),
+ 'mode: "follow", non-intercepted request, no server redirect');
+
+promise_test(t => setup_and_clean()
+ .then(() => redirected_test({url: REDIRECT_TO_TARGET_URL,
+ fetch_option: {},
+ fetch_method: self.fetch,
+ expected_type: 'basic',
+ expected_redirected: true,
+ expected_intercepted_urls: []})),
+ 'mode: "follow", non-intercepted request');
+
+promise_test(t => setup_and_clean()
+ .then(() => redirected_test({url: REDIRECT_TO_TARGET_URL + '&manual',
+ fetch_option: {redirect: 'manual'},
+ fetch_method: self.fetch,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ expected_intercepted_urls: []})),
+ 'mode: "manual", non-intercepted request');
+
+promise_test(t => setup_and_clean()
+ .then(() => promise_rejects_js(
+ t, TypeError,
+ self.fetch(REDIRECT_TO_TARGET_URL + '&error',
+ {redirect:'error'}),
+ 'The redirect response from the server should be treated as' +
+ ' an error when the redirect flag of request was \'error\'.'))
+ .then(() => check_intercepted_urls([])),
+ 'mode: "error", non-intercepted request');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = TARGET_URL + '&sw=fetch';
+ return redirected_test({url: url,
+ fetch_option: {},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'basic',
+ expected_redirected: false,
+ expected_intercepted_urls: [url]})
+ }),
+ 'mode: "follow", no mode change, no server redirect');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns a redirected response.
+// =======================================================
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = REDIRECT_TO_TARGET_URL +
+ '&original-redirect-mode=follow&sw=fetch';
+ return redirected_test({url: url,
+ fetch_option: {redirect: 'follow'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'basic',
+ expected_redirected: true,
+ expected_intercepted_urls: [url]})
+ }),
+ 'mode: "follow", no mode change');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = REDIRECT_TO_TARGET_URL +
+ '&original-redirect-mode=error&sw=follow';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'error'}),
+ 'The redirected response from the service worker should be ' +
+ 'treated as an error when the redirect flag of request was ' +
+ '\'error\'.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "error", mode change: "follow"');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = REDIRECT_TO_TARGET_URL +
+ '&original-redirect-mode=manual&sw=follow';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'manual'}),
+ 'The redirected response from the service worker should be ' +
+ 'treated as an error when the redirect flag of request was ' +
+ '\'manual\'.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "manual", mode change: "follow"');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns an opaqueredirect response.
+// =======================================================
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = REDIRECT_TO_TARGET_URL +
+ '&original-redirect-mode=follow&sw=manual';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'follow'}),
+ 'The opaqueredirect response from the service worker should ' +
+ 'be treated as an error when the redirect flag of request was' +
+ ' \'follow\'.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "follow", mode change: "manual"');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = REDIRECT_TO_TARGET_URL +
+ '&original-redirect-mode=error&sw=manual';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'error'}),
+ 'The opaqueredirect response from the service worker should ' +
+ 'be treated as an error when the redirect flag of request was' +
+ ' \'error\'.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "error", mode change: "manual"');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = REDIRECT_TO_TARGET_URL +
+ '&original-redirect-mode=manual&sw=manual';
+ return redirected_test({url: url,
+ fetch_option: {redirect: 'manual'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ expected_intercepted_urls: [url]});
+ }),
+ 'mode: "manual", no mode change');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns a generated redirect response.
+// =======================================================
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=' + encodeURIComponent(TARGET_URL) +
+ '&original-redirect-mode=follow&sw=gen';
+ return redirected_test({url: url,
+ fetch_option: {redirect: 'follow'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'basic',
+ expected_redirected: true,
+ expected_intercepted_urls: [url, TARGET_URL]})
+ }),
+ 'mode: "follow", generated redirect response');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=' + encodeURIComponent(TARGET_URL) +
+ '&original-redirect-mode=error&sw=gen';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'error'}),
+ 'The generated redirect response from the service worker should ' +
+ 'be treated as an error when the redirect flag of request was' +
+ ' \'error\'.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "error", generated redirect response');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=' + encodeURIComponent(TARGET_URL) +
+ '&original-redirect-mode=manual&sw=gen';
+ return redirected_test({url: url,
+ fetch_option: {redirect: 'manual'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ expected_intercepted_urls: [url]})
+ }),
+ 'mode: "manual", generated redirect response');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns a generated redirect response manually with the Response
+// constructor.
+// =======================================================
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=' + encodeURIComponent(TARGET_URL) +
+ '&original-redirect-mode=follow&sw=gen-manual';
+ return redirected_test({url: url,
+ fetch_option: {redirect: 'follow'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'basic',
+ expected_redirected: true,
+ expected_intercepted_urls: [url, TARGET_URL]})
+ }),
+ 'mode: "follow", manually-generated redirect response');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=' + encodeURIComponent(TARGET_URL) +
+ '&original-redirect-mode=error&sw=gen-manual';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'error'}),
+ 'The generated redirect response from the service worker should ' +
+ 'be treated as an error when the redirect flag of request was' +
+ ' \'error\'.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "error", manually-generated redirect response');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=' + encodeURIComponent(TARGET_URL) +
+ '&original-redirect-mode=manual&sw=gen-manual';
+ return redirected_test({url: url,
+ fetch_option: {redirect: 'manual'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ expected_intercepted_urls: [url]})
+ }),
+ 'mode: "manual", manually-generated redirect response');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns a generated redirect response with a relative location header.
+// Generated responses do not have URLs, so this should fail to resolve.
+// =======================================================
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=blank.html' +
+ '&original-redirect-mode=follow&sw=gen-manual';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'follow'}),
+ 'Following the generated redirect response from the service worker '+
+ 'should result fail.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "follow", generated relative redirect response');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=blank.html' +
+ '&original-redirect-mode=error&sw=gen-manual';
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(url, {redirect: 'error'}),
+ 'The generated redirect response from the service worker should ' +
+ 'be treated as an error when the redirect flag of request was' +
+ ' \'error\'.')
+ .then(() => check_intercepted_urls([url]));
+ }),
+ 'mode: "error", generated relative redirect response');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'sample?url=blank.html' +
+ '&original-redirect-mode=manual&sw=gen-manual';
+ return redirected_test({url: url,
+ fetch_option: {redirect: 'manual'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'opaqueredirect',
+ expected_redirected: false,
+ expected_intercepted_urls: [url]})
+ }),
+ 'mode: "manual", generated relative redirect response');
+
+// =======================================================
+// Tests for requests that are in-scope of the service worker. The service
+// worker returns a generated redirect response. And the fetch follows the
+// redirection multiple times.
+// =======================================================
+promise_test(t => setup_and_clean()
+ .then(() => {
+ // The Fetch spec says: "If request’s redirect count is twenty, return a
+ // network error." https://fetch.spec.whatwg.org/#http-redirect-fetch
+ // So fetch can follow the redirect response 20 times.
+ let urls = [TARGET_URL];
+ for (let i = 0; i < 20; ++i) {
+ urls.unshift(host_info['HTTPS_ORIGIN'] + '/sample?sw=gen&url=' +
+ encodeURIComponent(urls[0]));
+
+ }
+ return redirected_test({url: urls[0],
+ fetch_option: {redirect: 'follow'},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'basic',
+ expected_redirected: true,
+ expected_intercepted_urls: urls})
+ }),
+ 'Fetch should follow the redirect response 20 times');
+
+promise_test(t => setup_and_clean()
+ .then(() => {
+ let urls = [TARGET_URL];
+ // The Fetch spec says: "If request’s redirect count is twenty, return a
+ // network error." https://fetch.spec.whatwg.org/#http-redirect-fetch
+ // So fetch can't follow the redirect response 21 times.
+ for (let i = 0; i < 21; ++i) {
+ urls.unshift(host_info['HTTPS_ORIGIN'] + '/sample?sw=gen&url=' +
+ encodeURIComponent(urls[0]));
+
+ }
+ return promise_rejects_js(
+ t, frame.contentWindow.TypeError,
+ frame.contentWindow.fetch(urls[0], {redirect: 'follow'}),
+ 'Fetch should not follow the redirect response 21 times.')
+ .then(() => {
+ urls.pop();
+ return check_intercepted_urls(urls)
+ });
+ }),
+ 'Fetch should not follow the redirect response 21 times.');
+
+// =======================================================
+// A test for verifying the url of a service-worker-redirected request is
+// propagated to the outer response.
+// =======================================================
+promise_test(t => setup_and_clean()
+ .then(() => {
+ const url = host_info['HTTPS_ORIGIN'] + base_path() + 'sample?url=' +
+ encodeURIComponent(TARGET_URL) +'&sw=fetch-url';
+ return redirected_test({url: url,
+ fetch_option: {},
+ fetch_method: frame.contentWindow.fetch,
+ expected_type: 'basic',
+ expected_redirected: false,
+ expected_intercepted_urls: [url],
+ expected_response_url: TARGET_URL});
+ }),
+ 'The URL for the service worker redirected request should be propagated to ' +
+ 'response.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/referer.https.html b/third_party/web_platform_tests/service-workers/service-worker/referer.https.html
new file mode 100644
index 0000000..0957e4c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/referer.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: check referer of fetch()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+promise_test(function(t) {
+ var SCOPE = 'resources/referer-iframe.html';
+ var SCRIPT = 'resources/fetch-rewrite-worker.js';
+ var host_info = get_host_info();
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, SCOPE);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(function(frame) {
+ var channel = new MessageChannel();
+ t.add_cleanup(function() {
+ frame.remove();
+ });
+
+ var onMsg = new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ });
+
+ frame.contentWindow.postMessage({},
+ host_info['HTTPS_ORIGIN'],
+ [channel.port2]);
+ return onMsg;
+ })
+ .then(function(e) {
+ assert_equals(e.data.results, 'finish');
+ });
+ }, 'Verify the referer');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/referrer-policy-header.https.html b/third_party/web_platform_tests/service-workers/service-worker/referrer-policy-header.https.html
new file mode 100644
index 0000000..784343e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/referrer-policy-header.https.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<title>Service Worker: check referer of fetch() with Referrer Policy</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+const SCOPE = 'resources/referrer-policy-iframe.html';
+const SCRIPT = 'resources/fetch-rewrite-worker-referrer-policy.js';
+
+promise_test(async t => {
+ const registration =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ await wait_for_state(t, registration.installing, 'activated');
+ t.add_cleanup(() => registration.unregister(),
+ 'Remove registration as a cleanup');
+
+ const full_scope_url = new URL(SCOPE, location.href);
+ const redirect_to = `${full_scope_url.href}?ignore=true`;
+ const frame = await with_iframe(
+ `${SCOPE}?pipe=status(302)|header(Location,${redirect_to})|` +
+ 'header(Referrer-Policy,origin)');
+ assert_equals(frame.contentDocument.referrer,
+ full_scope_url.origin + '/');
+ t.add_cleanup(() => frame.remove());
+}, 'Referrer for a main resource redirected with referrer-policy (origin) ' +
+ 'should only have origin.');
+
+promise_test(async t => {
+ const registration =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE, `{type: 'module'}`);
+ await wait_for_state(t, registration.installing, 'activated');
+ t.add_cleanup(() => registration.unregister(),
+ 'Remove registration as a cleanup');
+
+ const full_scope_url = new URL(SCOPE, location.href);
+ const redirect_to = `${full_scope_url.href}?ignore=true`;
+ const frame = await with_iframe(
+ `${SCOPE}?pipe=status(302)|header(Location,${redirect_to})|` +
+ 'header(Referrer-Policy,origin)');
+ assert_equals(frame.contentDocument.referrer,
+ full_scope_url.origin + '/');
+ t.add_cleanup(() => frame.remove());
+}, 'Referrer for a main resource redirected with a module script with referrer-policy (origin) ' +
+ 'should only have origin.');
+
+promise_test(async t => {
+ const registration =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ await wait_for_state(t, registration.installing, 'activated');
+ t.add_cleanup(() => registration.unregister(),
+ 'Remove registration as a cleanup');
+
+ const host_info = get_host_info();
+ const frame = await with_iframe(SCOPE);
+ const channel = new MessageChannel();
+ t.add_cleanup(() => frame.remove());
+ const e = await new Promise(resolve => {
+ channel.port1.onmessage = resolve;
+ frame.contentWindow.postMessage(
+ {}, host_info['HTTPS_ORIGIN'], [channel.port2]);
+ });
+ assert_equals(e.data.results, 'finish');
+}, 'Referrer for fetch requests initiated from a service worker with ' +
+ 'referrer-policy (origin) should only have origin.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/referrer-toplevel-script-fetch.https.html b/third_party/web_platform_tests/service-workers/service-worker/referrer-toplevel-script-fetch.https.html
new file mode 100644
index 0000000..65c60a1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/referrer-toplevel-script-fetch.https.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<title>Service Worker: check referrer of top-level script fetch</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+
+async function get_toplevel_script_headers(worker) {
+ worker.postMessage("getHeaders");
+ return new Promise((resolve) => {
+ navigator.serviceWorker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ });
+}
+
+promise_test(async (t) => {
+ const script = "resources/test-request-headers-worker.py";
+ const scope = "resources/blank.html";
+ const host_info = get_host_info();
+
+ const registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+ await wait_for_state(t, registration.installing, "activated");
+
+ const expected_referrer = host_info["HTTPS_ORIGIN"] + location.pathname;
+
+ // Check referrer for register().
+ const register_headers = await get_toplevel_script_headers(registration.active);
+ assert_equals(register_headers["referer"], expected_referrer, "referrer of register()");
+
+ // Check referrer for update().
+ await registration.update();
+ await wait_for_state(t, registration.installing, "installed");
+ const update_headers = await get_toplevel_script_headers(registration.waiting);
+ assert_equals(update_headers["referer"], expected_referrer, "referrer of update()");
+}, "Referrer of the top-level script fetch should be the document URL");
+
+promise_test(async (t) => {
+ const script = "resources/test-request-headers-worker.py";
+ const scope = "resources/blank.html";
+ const host_info = get_host_info();
+
+ const registration = await service_worker_unregister_and_register(
+ t, script, scope, {type: 'module'});
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+ await wait_for_state(t, registration.installing, "activated");
+
+ const expected_referrer = host_info["HTTPS_ORIGIN"] + location.pathname;
+
+ // Check referrer for register().
+ const register_headers = await get_toplevel_script_headers(registration.active);
+ assert_equals(register_headers["referer"], expected_referrer, "referrer of register()");
+
+ // Check referrer for update().
+ await registration.update();
+ await wait_for_state(t, registration.installing, "installed");
+ const update_headers = await get_toplevel_script_headers(registration.waiting);
+ assert_equals(update_headers["referer"], expected_referrer, "referrer of update()");
+}, "Referrer of the module script fetch should be the document URL");
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/register-closed-window.https.html b/third_party/web_platform_tests/service-workers/service-worker/register-closed-window.https.html
new file mode 100644
index 0000000..9c1b639
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/register-closed-window.https.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<title>Service Worker: Register() on Closed Window</title>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+var host_info = get_host_info();
+var frameURL = host_info['HTTPS_ORIGIN'] + base_path() +
+ 'resources/register-closed-window-iframe.html';
+
+async_test(function(t) {
+ var frame;
+ with_iframe(frameURL).then(function(f) {
+ frame = f;
+ return new Promise(function(resolve) {
+ window.addEventListener('message', function messageHandler(evt) {
+ window.removeEventListener('message', messageHandler);
+ resolve(evt.data);
+ });
+ frame.contentWindow.postMessage('START', '*');
+ });
+ }).then(function(result) {
+ assert_equals(result, 'OK', 'frame should complete without crashing');
+ frame.remove();
+ t.done();
+ }).catch(unreached_rejection(t));
+}, 'Call register() on ServiceWorkerContainer owned by closed window.');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/register-default-scope.https.html b/third_party/web_platform_tests/service-workers/service-worker/register-default-scope.https.html
new file mode 100644
index 0000000..1d86548
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/register-default-scope.https.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<title>register() and scope</title>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var script_url = new URL(script, location.href);
+ var expected_scope = new URL('./', script_url).href;
+ return service_worker_unregister(t, expected_scope)
+ .then(function() {
+ return navigator.serviceWorker.register('resources/empty-worker.js');
+ }).then(function(registration) {
+ assert_equals(registration.scope, expected_scope,
+ 'The default scope should be URL("./", script_url)');
+ return registration.unregister();
+ }).then(function() {
+ t.done();
+ });
+ }, 'default scope');
+
+promise_test(function(t) {
+ // This script must be different than the 'default scope' test, or else
+ // the scopes will collide.
+ var script = 'resources/empty.js';
+ var script_url = new URL(script, location.href);
+ var expected_scope = new URL('./', script_url).href;
+ return service_worker_unregister(t, expected_scope)
+ .then(function() {
+ return navigator.serviceWorker.register('resources/empty.js',
+ { scope: undefined });
+ }).then(function(registration) {
+ assert_equals(registration.scope, expected_scope,
+ 'The default scope should be URL("./", script_url)');
+ return registration.unregister();
+ }).then(function() {
+ t.done();
+ });
+ }, 'undefined scope');
+
+promise_test(function(t) {
+ var script = 'resources/simple-fetch-worker.js';
+ var script_url = new URL(script, location.href);
+ var expected_scope = new URL('./', script_url).href;
+ return service_worker_unregister(t, expected_scope)
+ .then(function() {
+ return navigator.serviceWorker.register('resources/empty.js',
+ { scope: null });
+ })
+ .then(
+ function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, registration.scope);
+ });
+
+ assert_unreached('register should fail');
+ },
+ function(error) {
+ assert_equals(error.name, 'SecurityError',
+ 'passing a null scope should be interpreted as ' +
+ 'scope="null" which violates the path restriction');
+ t.done();
+ });
+ }, 'null scope');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/register-same-scope-different-script-url.https.html b/third_party/web_platform_tests/service-workers/service-worker/register-same-scope-different-script-url.https.html
new file mode 100644
index 0000000..6eb00f3
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/register-same-scope-different-script-url.https.html
@@ -0,0 +1,233 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var script1 = normalizeURL('resources/empty-worker.js');
+var script2 = normalizeURL('resources/empty-worker.js?new');
+
+async_test(function(t) {
+ var scope = 'resources/scope/register-new-script-concurrently';
+ var register_promise1;
+ var register_promise2;
+
+ service_worker_unregister(t, scope)
+ .then(function() {
+ register_promise1 = navigator.serviceWorker.register(script1,
+ {scope: scope});
+ register_promise2 = navigator.serviceWorker.register(script2,
+ {scope: scope});
+ return register_promise1;
+ })
+ .then(function(registration) {
+ assert_equals(registration.installing.scriptURL, script1,
+ 'on first register, first script should be installing');
+ assert_equals(registration.waiting, null,
+ 'on first register, waiting should be null');
+ assert_equals(registration.active, null,
+ 'on first register, active should be null');
+ return register_promise2;
+ })
+ .then(function(registration) {
+ assert_equals(
+ registration.installing.scriptURL, script2,
+ 'on second register, second script should be installing');
+ // Spec allows racing: the first register may have finished
+ // or the second one could have terminated the installing worker.
+ assert_true(registration.waiting == null ||
+ registration.waiting.scriptURL == script1,
+ 'on second register, .waiting should be null or the ' +
+ 'first script');
+ assert_true(registration.active == null ||
+ (registration.waiting == null &&
+ registration.active.scriptURL == script1),
+ 'on second register, .active should be null or the ' +
+ 'first script');
+ return registration.unregister();
+ })
+ .then(function() {
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Register different scripts concurrently');
+
+async_test(function(t) {
+ var scope = 'resources/scope/register-then-register-new-script';
+ var registration;
+
+ service_worker_unregister_and_register(t, script1, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'on activated, installing should be null');
+ assert_equals(registration.waiting, null,
+ 'on activated, waiting should be null');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on activated, the first script should be active');
+ return navigator.serviceWorker.register(script2, {scope:scope});
+ })
+ .then(function(r) {
+ registration = r;
+ assert_equals(registration.installing.scriptURL, script2,
+ 'on second register, the second script should be ' +
+ 'installing');
+ assert_equals(registration.waiting, null,
+ 'on second register, waiting should be null');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on second register, the first script should be ' +
+ 'active');
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'on installed, installing should be null');
+ assert_equals(registration.waiting.scriptURL, script2,
+ 'on installed, the second script should be waiting');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on installed, the first script should be active');
+ return registration.unregister();
+ })
+ .then(function() {
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Register then register new script URL');
+
+async_test(function(t) {
+ var scope = 'resources/scope/register-then-register-new-script-404';
+ var registration;
+
+ service_worker_unregister_and_register(t, script1, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'on activated, installing should be null');
+ assert_equals(registration.waiting, null,
+ 'on activated, waiting should be null');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on activated, the first script should be active');
+ return navigator.serviceWorker.register('this-will-404.js',
+ {scope:scope});
+ })
+ .then(
+ function() { assert_unreached('register should reject'); },
+ function(error) {
+ assert_equals(registration.installing, null,
+ 'on rejected, installing should be null');
+ assert_equals(registration.waiting, null,
+ 'on rejected, waiting should be null');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on rejected, the first script should be active');
+ return registration.unregister();
+ })
+ .then(function() {
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Register then register new script URL that 404s');
+
+async_test(function(t) {
+ var scope = 'resources/scope/register-then-register-new-script-reject-install';
+ var reject_script = normalizeURL('resources/reject-install-worker.js');
+ var registration;
+
+ service_worker_unregister_and_register(t, script1, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'on activated, installing should be null');
+ assert_equals(registration.waiting, null,
+ 'on activated, waiting should be null');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on activated, the first script should be active');
+ return navigator.serviceWorker.register(reject_script, {scope:scope});
+ })
+ .then(function(r) {
+ registration = r;
+ assert_equals(registration.installing.scriptURL, reject_script,
+ 'on update, the second script should be installing');
+ assert_equals(registration.waiting, null,
+ 'on update, waiting should be null');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on update, the first script should be active');
+ return wait_for_state(t, registration.installing, 'redundant');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'on redundant, installing should be null');
+ assert_equals(registration.waiting, null,
+ 'on redundant, waiting should be null');
+ assert_equals(registration.active.scriptURL, script1,
+ 'on redundant, the first script should be active');
+ return registration.unregister();
+ })
+ .then(function() {
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Register then register new script that does not install');
+
+async_test(function(t) {
+ var scope = 'resources/scope/register-new-script-controller';
+ var iframe;
+ var registration;
+
+ service_worker_unregister_and_register(t, script1, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ iframe = frame;
+ return navigator.serviceWorker.register(script2, { scope: scope })
+ })
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ var sw_container = iframe.contentWindow.navigator.serviceWorker;
+ assert_equals(sw_container.controller.scriptURL, script1,
+ 'the old version should control the old doc');
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ var sw_container = frame.contentWindow.navigator.serviceWorker;
+ assert_equals(sw_container.controller.scriptURL, script1,
+ 'the old version should control a new doc');
+ var onactivated_promise = wait_for_state(t,
+ registration.waiting,
+ 'activated');
+ frame.remove();
+ iframe.remove();
+ return onactivated_promise;
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ var sw_container = frame.contentWindow.navigator.serviceWorker;
+ assert_equals(sw_container.controller.scriptURL, script2,
+ 'the new version should control a new doc');
+ frame.remove();
+ return registration.unregister();
+ })
+ .then(function() {
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Register same-scope new script url effect on controller');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/register-wait-forever-in-install-worker.https.html b/third_party/web_platform_tests/service-workers/service-worker/register-wait-forever-in-install-worker.https.html
new file mode 100644
index 0000000..0920b5c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/register-wait-forever-in-install-worker.https.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<title>Service Worker: Register wait-forever-in-install-worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+ var bad_script = 'resources/wait-forever-in-install-worker.js';
+ var good_script = 'resources/empty-worker.js';
+ var scope = 'resources/wait-forever-in-install-worker';
+ var other_scope = 'resources/wait-forever-in-install-worker-other';
+ var registration;
+ var registerPromise;
+
+ return navigator.serviceWorker.register(bad_script, {scope: scope})
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ assert_equals(registration.installing.scriptURL,
+ normalizeURL(bad_script));
+
+ // This register job should not start until the first
+ // register for the same scope completes.
+ registerPromise =
+ navigator.serviceWorker.register(good_script, {scope: scope});
+
+ // In order to test that the above register does not complete
+ // we will perform a register() on a different scope. The
+ // assumption here is that the previous register call would
+ // have completed in the same timeframe if it was able to do
+ // so.
+ return navigator.serviceWorker.register(good_script,
+ {scope: other_scope});
+ })
+ .then(function(swr) {
+ return swr.unregister();
+ })
+ .then(function() {
+ assert_equals(registration.installing.scriptURL,
+ normalizeURL(bad_script));
+ registration.installing.postMessage('STOP_WAITING');
+ return registerPromise;
+ })
+ .then(function(swr) {
+ assert_equals(registration.installing.scriptURL,
+ normalizeURL(good_script));
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ }, 'register worker that calls waitUntil with a promise that never ' +
+ 'resolves in oninstall');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-basic.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-basic.https.html
new file mode 100644
index 0000000..759b424
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-basic.https.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (basic)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const script = 'resources/registration-worker.js';
+
+promise_test(async (t) => {
+ const scope = 'resources/registration/normal';
+ const registration = await navigator.serviceWorker.register(script, {scope});
+ t.add_cleanup(() => registration.unregister());
+ assert_true(
+ registration instanceof ServiceWorkerRegistration,
+ 'Successfully registered.');
+}, 'Registering normal scope');
+
+promise_test(async (t) => {
+ const scope = 'resources/registration/scope-with-fragment#ref';
+ const registration = await navigator.serviceWorker.register(script, {scope});
+ t.add_cleanup(() => registration.unregister());
+ assert_true(
+ registration instanceof ServiceWorkerRegistration,
+ 'Successfully registered.');
+ assert_equals(
+ registration.scope,
+ normalizeURL('resources/registration/scope-with-fragment'),
+ 'A fragment should be removed from scope');
+}, 'Registering scope with fragment');
+
+promise_test(async (t) => {
+ const scope = 'resources/';
+ const registration = await navigator.serviceWorker.register(script, {scope})
+ t.add_cleanup(() => registration.unregister());
+ assert_true(
+ registration instanceof ServiceWorkerRegistration,
+ 'Successfully registered.');
+}, 'Registering same scope as the script directory');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-end-to-end.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-end-to-end.https.html
new file mode 100644
index 0000000..1af4582
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-end-to-end.https.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<title>Service Worker: registration end-to-end</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var t = async_test('Registration: end-to-end');
+t.step(function() {
+
+ var scope = 'resources/in-scope/';
+ var serviceWorkerStates = [];
+ var lastServiceWorkerState = '';
+ var receivedMessageFromPort = '';
+
+ assert_true(navigator.serviceWorker instanceof ServiceWorkerContainer);
+ assert_equals(typeof navigator.serviceWorker.register, 'function');
+ assert_equals(typeof navigator.serviceWorker.getRegistration, 'function');
+
+ service_worker_unregister_and_register(
+ t, 'resources/end-to-end-worker.js', scope)
+ .then(onRegister)
+ .catch(unreached_rejection(t));
+
+ function sendMessagePort(worker, from) {
+ var messageChannel = new MessageChannel();
+ worker.postMessage({from:from, port:messageChannel.port2}, [messageChannel.port2]);
+ return messageChannel.port1;
+ }
+
+ function onRegister(registration) {
+ var sw = registration.installing;
+ serviceWorkerStates.push(sw.state);
+ lastServiceWorkerState = sw.state;
+
+ var sawMessage = new Promise(t.step_func(function(resolve) {
+ sendMessagePort(sw, 'registering doc').onmessage = t.step_func(function (e) {
+ receivedMessageFromPort = e.data;
+ resolve();
+ });
+ }));
+
+ var sawActive = new Promise(t.step_func(function(resolve) {
+ sw.onstatechange = t.step_func(function() {
+ serviceWorkerStates.push(sw.state);
+
+ switch (sw.state) {
+ case 'installed':
+ assert_equals(lastServiceWorkerState, 'installing');
+ break;
+ case 'activating':
+ assert_equals(lastServiceWorkerState, 'installed');
+ break;
+ case 'activated':
+ assert_equals(lastServiceWorkerState, 'activating');
+ break;
+ default:
+ // We won't see 'redundant' because onstatechange is
+ // overwritten before calling unregister.
+ assert_unreached('Unexpected state: ' + sw.state);
+ }
+
+ lastServiceWorkerState = sw.state;
+ if (sw.state === 'activated')
+ resolve();
+ });
+ }));
+
+ Promise.all([sawMessage, sawActive]).then(t.step_func(function() {
+ assert_array_equals(serviceWorkerStates,
+ ['installing', 'installed', 'activating', 'activated'],
+ 'Service worker should pass through all states');
+
+ assert_equals(receivedMessageFromPort, 'Ack for: registering doc');
+
+ var sawRedundant = new Promise(t.step_func(function(resolve) {
+ sw.onstatechange = t.step_func(function() {
+ assert_equals(sw.state, 'redundant');
+ resolve();
+ });
+ }));
+ registration.unregister();
+ sawRedundant.then(t.step_func(function() {
+ t.done();
+ }));
+ }));
+ }
+});
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-events.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-events.https.html
new file mode 100644
index 0000000..5bcfd66
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-events.https.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<title>Service Worker: registration events</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var scope = 'resources/in-scope/';
+ return service_worker_unregister_and_register(
+ t, 'resources/events-worker.js', scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return onRegister(registration.installing);
+ });
+
+ function sendMessagePort(worker, from) {
+ var messageChannel = new MessageChannel();
+ worker.postMessage({from:from, port:messageChannel.port2}, [messageChannel.port2]);
+ return messageChannel.port1;
+ }
+
+ function onRegister(sw) {
+ return new Promise(function(resolve) {
+ sw.onstatechange = function() {
+ if (sw.state === 'activated')
+ resolve();
+ };
+ }).then(function() {
+ return new Promise(function(resolve) {
+ sendMessagePort(sw, 'registering doc').onmessage = resolve;
+ });
+ }).then(function(e) {
+ assert_array_equals(e.data.events,
+ ['install', 'activate'],
+ 'Worker should see install then activate events');
+ });
+ }
+}, 'Registration: events');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-iframe.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-iframe.https.html
new file mode 100644
index 0000000..ae39ddf
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-iframe.https.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: Registration for iframe</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+// Set script url and scope url relative to the iframe's document's url. Assert
+// the implementation parses the urls against the iframe's document's url.
+async_test(function(t) {
+ const url = 'resources/blank.html';
+ const iframe_scope = 'registration-with-valid-scope';
+ const scope = normalizeURL('resources/' + iframe_scope);
+ const iframe_script = 'empty-worker.js';
+ const script = normalizeURL('resources/' + iframe_script);
+ var frame;
+ var registration;
+
+ service_worker_unregister(t, scope)
+ .then(function() { return with_iframe(url); })
+ .then(function(f) {
+ frame = f;
+ return frame.contentWindow.navigator.serviceWorker.register(
+ iframe_script,
+ { scope: iframe_scope });
+ })
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ assert_equals(registration.scope, scope,
+ 'registration\'s scope must be parsed against the ' +
+ '"relevant global object"');
+ assert_equals(registration.active.scriptURL, script,
+ 'worker\'s scriptURL must be parsed against the ' +
+ '"relevant global object"');
+ return registration.unregister();
+ })
+ .then(function() {
+ frame.remove();
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'register method should use the "relevant global object" to parse its ' +
+ 'scriptURL and scope - normal case');
+
+// Set script url and scope url relative to the parent frame's document's url.
+// Assert the implementation throws a TypeError exception.
+async_test(function(t) {
+ const url = 'resources/blank.html';
+ const iframe_scope = 'resources/registration-with-scope-to-non-existing-url';
+ const scope = normalizeURL('resources/' + iframe_scope);
+ const script = 'resources/empty-worker.js';
+ var frame;
+ var registration;
+
+ service_worker_unregister(t, scope)
+ .then(function() { return with_iframe(url); })
+ .then(function(f) {
+ frame = f;
+ return frame.contentWindow.navigator.serviceWorker.register(
+ script,
+ { scope: iframe_scope });
+ })
+ .then(
+ function() {
+ assert_unreached('register() should reject');
+ },
+ function(e) {
+ assert_equals(e.name, 'TypeError',
+ 'register method with scriptURL and scope parsed to ' +
+ 'nonexistent location should reject with TypeError');
+ frame.remove();
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'register method should use the "relevant global object" to parse its ' +
+ 'scriptURL and scope - error case');
+
+// Set the scope url to a non-subdirectory of the script url. Assert the
+// implementation throws a SecurityError exception.
+async_test(function(t) {
+ const url = 'resources/blank.html';
+ const scope = 'registration-with-disallowed-scope';
+ const iframe_scope = '../' + scope;
+ const script = 'empty-worker.js';
+ var frame;
+ var registration;
+
+ service_worker_unregister(t, scope)
+ .then(function() { return with_iframe(url); })
+ .then(function(f) {
+ frame = f;
+ return frame.contentWindow.navigator.serviceWorker.register(
+ script,
+ { scope: iframe_scope });
+ })
+ .then(
+ function() {
+ assert_unreached('register() should reject');
+ },
+ function(e) {
+ assert_equals(e.name, 'SecurityError',
+ 'The scope set to a non-subdirectory of the scriptURL ' +
+ 'should reject with SecurityError');
+ frame.remove();
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'A scope url should start with the given script url');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-mime-types.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-mime-types.https.html
new file mode 100644
index 0000000..3a21aac
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-mime-types.https.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (MIME types)</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-mime-types.js"></script>
+<script>
+registration_tests_mime_types((script, options) => navigator.serviceWorker.register(script, options));
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-schedule-job.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-schedule-job.https.html
new file mode 100644
index 0000000..25d758e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-schedule-job.https.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name=timeout content=long>
+<title>Service Worker: Schedule Job algorithm</title>
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// Tests for https://w3c.github.io/ServiceWorker/#schedule-job-algorithm
+// Non-equivalent register jobs should not be coalesced.
+const scope = 'resources/';
+const script1 = 'resources/empty.js';
+const script2 = 'resources/empty.js?change';
+
+async function cleanup() {
+ const registration = await navigator.serviceWorker.getRegistration(scope);
+ if (registration)
+ await registration.unregister();
+}
+
+function absolute_url(url) {
+ return new URL(url, self.location).toString();
+}
+
+// Test that a change to `script` starts a new register job.
+promise_test(async t => {
+ await cleanup();
+ t.add_cleanup(cleanup);
+
+ // Make a registration.
+ const registration = await
+ navigator.serviceWorker.register(script1, {scope});
+
+ // Schedule two more register jobs.
+ navigator.serviceWorker.register(script1, {scope});
+ await navigator.serviceWorker.register(script2, {scope});
+
+ // The jobs should not have been coalesced.
+ const worker = get_newest_worker(registration);
+ assert_equals(worker.scriptURL, absolute_url(script2));
+}, 'different scriptURL');
+
+// Test that a change to `updateViaCache` starts a new register job.
+promise_test(async t => {
+ await cleanup();
+ t.add_cleanup(cleanup);
+
+ // Check defaults.
+ const registration = await
+ navigator.serviceWorker.register(script1, {scope});
+ assert_equals(registration.updateViaCache, 'imports');
+
+ // Schedule two more register jobs.
+ navigator.serviceWorker.register(script1, {scope});
+ await navigator.serviceWorker.register(script1, {scope,
+ updateViaCache: 'none'});
+
+ // The jobs should not have been coalesced.
+ assert_equals(registration.updateViaCache, 'none');
+}, 'different updateViaCache');
+
+// Test that a change to `type` starts a new register job.
+promise_test(async t => {
+ await cleanup();
+ t.add_cleanup(cleanup);
+
+ const scriptForTypeCheck = 'resources/type-check-worker.js';
+ // Check defaults.
+ const registration = await
+ navigator.serviceWorker.register(scriptForTypeCheck, {scope});
+
+ let worker_type = await new Promise((resolve) => {
+ navigator.serviceWorker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ // The jobs should not have been coalesced. get_newest_worker() helps the
+ // test fail with stable output on browers that incorrectly coalesce
+ // register jobs, since then sometimes registration is not a new worker as
+ // expected.
+ const worker = get_newest_worker(registration);
+ // The argument of postMessage doesn't matter for this case.
+ worker.postMessage('');
+ });
+
+ assert_equals(worker_type, 'classic');
+
+ // Schedule two more register jobs.
+ navigator.serviceWorker.register(scriptForTypeCheck, {scope});
+ await navigator.serviceWorker.register(scriptForTypeCheck, {scope, type: 'module'});
+
+ worker_type = await new Promise((resolve) => {
+ navigator.serviceWorker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ // The jobs should not have been coalesced. get_newest_worker() helps the
+ // test fail with stable output on browers that incorrectly coalesce
+ // register jobs, since then sometimes registration is not a new worker as
+ // expected.
+ const worker = get_newest_worker(registration);
+ // The argument of postMessage doesn't matter for this case.
+ worker.postMessage('');
+ });
+
+ assert_equals(worker_type, 'module');
+}, 'different type');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-scope-module-static-import.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-scope-module-static-import.https.html
new file mode 100644
index 0000000..5c75295
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-scope-module-static-import.https.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<title>Service Worker: Static imports from module top-level scripts shouldn't be affected by the service worker script path restriction</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// https://w3c.github.io/ServiceWorker/#path-restriction
+// is applied to top-level scripts in
+// https://w3c.github.io/ServiceWorker/#update-algorithm
+// but not to submodules imported from top-level scripts.
+async function runTest(t, script, scope) {
+ const script_url = new URL(script, location.href);
+ await service_worker_unregister(t, scope);
+ const registration = await
+ navigator.serviceWorker.register(script, {type: 'module'});
+ t.add_cleanup(_ => registration.unregister());
+ const msg = await new Promise(resolve => {
+ registration.installing.postMessage('ping');
+ navigator.serviceWorker.onmessage = resolve;
+ });
+ assert_equals(msg.data, 'pong');
+}
+
+promise_test(async t => {
+ await runTest(t,
+ 'resources/scope2/imported-module-script.js',
+ 'resources/scope2/');
+ }, 'imported-module-script.js works when used as top-level');
+
+promise_test(async t => {
+ await runTest(t,
+ 'resources/scope1/module-worker-importing-scope2.js',
+ 'resources/scope1/');
+ }, 'static imports to outside path restriction should be allowed');
+
+promise_test(async t => {
+ await runTest(t,
+ 'resources/scope1/module-worker-importing-redirect-to-scope2.js',
+ 'resources/scope1/');
+ }, 'static imports redirecting to outside path restriction should be allowed');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-scope.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-scope.https.html
new file mode 100644
index 0000000..141875f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-scope.https.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (scope)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-scope.js"></script>
+<script>
+registration_tests_scope((script, options) => navigator.serviceWorker.register(script, options));
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-script-module.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-script-module.https.html
new file mode 100644
index 0000000..9e39a1f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-script-module.https.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (module script)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-script.js"></script>
+<script>
+registration_tests_script(
+ (script, options) => navigator.serviceWorker.register(
+ script,
+ Object.assign({type: 'module'}, options)),
+ 'module');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-script-url.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-script-url.https.html
new file mode 100644
index 0000000..bda61ad
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-script-url.https.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (scriptURL)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-script-url.js"></script>
+<script>
+registration_tests_script_url((script, options) => navigator.serviceWorker.register(script, options));
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-script.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-script.https.html
new file mode 100644
index 0000000..f1e51fd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-script.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (script)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-script.js"></script>
+<script>
+registration_tests_script(
+ (script, options) => navigator.serviceWorker.register(script, options),
+ 'classic'
+);
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-security-error.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-security-error.https.html
new file mode 100644
index 0000000..860c2d2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-security-error.https.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration (SecurityError)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/registration-tests-security-error.js"></script>
+<script>
+registration_tests_security_error((script, options) => navigator.serviceWorker.register(script, options));
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-service-worker-attributes.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-service-worker-attributes.https.html
new file mode 100644
index 0000000..f7b52d5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-service-worker-attributes.https.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+promise_test(function(t) {
+ var scope = 'resources/scope/installing-waiting-active-after-registration';
+ var worker_url = 'resources/empty-worker.js';
+ var expected_url = normalizeURL(worker_url);
+ var newest_worker;
+ var registration;
+
+ return service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return r.unregister();
+ });
+ registration = r;
+ newest_worker = registration.installing;
+ assert_equals(registration.installing.scriptURL, expected_url,
+ 'installing before updatefound');
+ assert_equals(registration.waiting, null,
+ 'waiting before updatefound');
+ assert_equals(registration.active, null,
+ 'active before updatefound');
+ return wait_for_update(t, registration);
+ })
+ .then(function() {
+ assert_equals(registration.installing, newest_worker,
+ 'installing after updatefound');
+ assert_equals(registration.waiting, null,
+ 'waiting after updatefound');
+ assert_equals(registration.active, null,
+ 'active after updatefound');
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'installing after installed');
+ assert_equals(registration.waiting, newest_worker,
+ 'waiting after installed');
+ assert_equals(registration.active, null,
+ 'active after installed');
+ return wait_for_state(t, registration.waiting, 'activated');
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'installing after activated');
+ assert_equals(registration.waiting, null,
+ 'waiting after activated');
+ assert_equals(registration.active, newest_worker,
+ 'active after activated');
+ return Promise.all([
+ wait_for_state(t, registration.active, 'redundant'),
+ registration.unregister()
+ ]);
+ })
+ .then(function() {
+ assert_equals(registration.installing, null,
+ 'installing after redundant');
+ assert_equals(registration.waiting, null,
+ 'waiting after redundant');
+ // According to spec, Clear Registration runs Update State which is
+ // immediately followed by setting active to null, which means by the
+ // time the event loop turns and the Promise for statechange is
+ // resolved, this will be gone.
+ assert_equals(registration.active, null,
+ 'active should be null after redundant');
+ });
+ }, 'installing/waiting/active after registration');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/registration-updateviacache.https.html b/third_party/web_platform_tests/service-workers/service-worker/registration-updateviacache.https.html
new file mode 100644
index 0000000..b2f6bbc
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/registration-updateviacache.https.html
@@ -0,0 +1,204 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration-updateViaCache</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+ const UPDATE_VIA_CACHE_VALUES = [undefined, 'imports', 'all', 'none'];
+ const SCRIPT_URL = 'resources/update-max-aged-worker.py';
+ const SCOPE = 'resources/blank.html';
+
+ async function cleanup() {
+ const reg = await navigator.serviceWorker.getRegistration(SCOPE);
+ if (!reg) return;
+ if (reg.scope == new URL(SCOPE, location).href) {
+ return reg.unregister();
+ };
+ }
+
+ function getScriptTimes(sw, testName) {
+ return new Promise(resolve => {
+ navigator.serviceWorker.addEventListener('message', function listener(event) {
+ if (event.data.test !== testName) return;
+ navigator.serviceWorker.removeEventListener('message', listener);
+ resolve({
+ mainTime: event.data.mainTime,
+ importTime: event.data.importTime
+ });
+ });
+
+ sw.postMessage('');
+ });
+ }
+
+ // Test creating registrations & triggering an update.
+ for (const updateViaCache of UPDATE_VIA_CACHE_VALUES) {
+ const testName = `register-with-updateViaCache-${updateViaCache}`;
+
+ promise_test(async t => {
+ await cleanup();
+
+ const opts = {scope: SCOPE};
+
+ if (updateViaCache) opts.updateViaCache = updateViaCache;
+
+ const reg = await navigator.serviceWorker.register(
+ `${SCRIPT_URL}?test=${testName}`,
+ opts
+ );
+
+ assert_equals(reg.updateViaCache, updateViaCache || 'imports', "reg.updateViaCache");
+
+ const sw = reg.installing || reg.waiting || reg.active;
+ await wait_for_state(t, sw, 'activated');
+ const values = await getScriptTimes(sw, testName);
+ await reg.update();
+
+ if (updateViaCache == 'all') {
+ assert_equals(reg.installing, null, "No new service worker");
+ }
+ else {
+ const newWorker = reg.installing;
+ assert_true(!!newWorker, "New worker installing");
+ const newValues = await getScriptTimes(newWorker, testName);
+
+ if (!updateViaCache || updateViaCache == 'imports') {
+ assert_not_equals(values.mainTime, newValues.mainTime, "Main script should have updated");
+ assert_equals(values.importTime, newValues.importTime, "Imported script should be the same");
+ }
+ else if (updateViaCache == 'none') {
+ assert_not_equals(values.mainTime, newValues.mainTime, "Main script should have updated");
+ assert_not_equals(values.importTime, newValues.importTime, "Imported script should have updated");
+ }
+ else {
+ // We should have handled all of the possible values for updateViaCache.
+ // If this runs, something's gone very wrong.
+ throw Error(`Unexpected updateViaCache value: ${updateViaCache}`);
+ }
+ }
+
+ await cleanup();
+ }, testName);
+ }
+
+ // Test changing the updateViaCache value of an existing registration.
+ for (const updateViaCache1 of UPDATE_VIA_CACHE_VALUES) {
+ for (const updateViaCache2 of UPDATE_VIA_CACHE_VALUES) {
+ const testName = `register-with-updateViaCache-${updateViaCache1}-then-${updateViaCache2}`;
+
+ promise_test(async t => {
+ await cleanup();
+
+ const fullScriptUrl = `${SCRIPT_URL}?test=${testName}`;
+ let opts = {scope: SCOPE};
+ if (updateViaCache1) opts.updateViaCache = updateViaCache1;
+
+ const reg = await navigator.serviceWorker.register(fullScriptUrl, opts);
+
+ const sw = reg.installing;
+ await wait_for_state(t, sw, 'activated');
+ const values = await getScriptTimes(sw, testName);
+
+ const frame = await with_iframe(SCOPE);
+ const reg_in_frame = await frame.contentWindow.navigator.serviceWorker.getRegistration(normalizeURL(SCOPE));
+ assert_equals(reg_in_frame.updateViaCache, updateViaCache1 || 'imports', "reg_in_frame.updateViaCache");
+
+ opts = {scope: SCOPE};
+ if (updateViaCache2) opts.updateViaCache = updateViaCache2;
+
+ await navigator.serviceWorker.register(fullScriptUrl, opts);
+
+ const expected_updateViaCache = updateViaCache2 || 'imports';
+
+ assert_equals(reg.updateViaCache, expected_updateViaCache, "reg.updateViaCache updated");
+ // If the update happens via the cache, the scripts will come back byte-identical.
+ // We bypass the byte-identical check if the script URL has changed, but not if
+ // only the updateViaCache value has changed.
+ if (updateViaCache2 == 'all') {
+ assert_equals(reg.installing, null, "No new service worker");
+ }
+ // If there's no change to the updateViaCache value, register should be a no-op.
+ // The default value should behave as 'imports'.
+ else if ((updateViaCache1 || 'imports') == (updateViaCache2 || 'imports')) {
+ assert_equals(reg.installing, null, "No new service worker");
+ }
+ else {
+ const newWorker = reg.installing;
+ assert_true(!!newWorker, "New worker installing");
+ const newValues = await getScriptTimes(newWorker, testName);
+
+ if (!updateViaCache2 || updateViaCache2 == 'imports') {
+ assert_not_equals(values.mainTime, newValues.mainTime, "Main script should have updated");
+ assert_equals(values.importTime, newValues.importTime, "Imported script should be the same");
+ }
+ else if (updateViaCache2 == 'none') {
+ assert_not_equals(values.mainTime, newValues.mainTime, "Main script should have updated");
+ assert_not_equals(values.importTime, newValues.importTime, "Imported script should have updated");
+ }
+ else {
+ // We should have handled all of the possible values for updateViaCache2.
+ // If this runs, something's gone very wrong.
+ throw Error(`Unexpected updateViaCache value: ${updateViaCache}`);
+ }
+ }
+
+ // Wait for all registration related tasks on |frame| to complete.
+ await wait_for_activation_on_sample_scope(t, frame.contentWindow);
+ // The updateViaCache change should have been propagated to all
+ // corresponding JS registration objects.
+ assert_equals(reg_in_frame.updateViaCache, expected_updateViaCache, "reg_in_frame.updateViaCache updated");
+ frame.remove();
+
+ await cleanup();
+ }, testName);
+ }
+ }
+
+ // Test accessing updateViaCache of an unregistered registration.
+ for (const updateViaCache of UPDATE_VIA_CACHE_VALUES) {
+ const testName = `access-updateViaCache-after-unregister-${updateViaCache}`;
+
+ promise_test(async t => {
+ await cleanup();
+
+ const opts = {scope: SCOPE};
+
+ if (updateViaCache) opts.updateViaCache = updateViaCache;
+
+ const reg = await navigator.serviceWorker.register(
+ `${SCRIPT_URL}?test=${testName}`,
+ opts
+ );
+
+ const expected_updateViaCache = updateViaCache || 'imports';
+ assert_equals(reg.updateViaCache, expected_updateViaCache, "reg.updateViaCache");
+
+ await reg.unregister();
+
+ // Keep the original value.
+ assert_equals(reg.updateViaCache, expected_updateViaCache, "reg.updateViaCache");
+
+ await cleanup();
+ }, testName);
+ }
+
+ promise_test(async t => {
+ await cleanup();
+ t.add_cleanup(cleanup);
+
+ const registration = await navigator.serviceWorker.register(
+ 'resources/empty.js',
+ {scope: SCOPE});
+ assert_equals(registration.updateViaCache, 'imports',
+ 'before update attempt');
+
+ const fail = navigator.serviceWorker.register(
+ 'resources/malformed-worker.py?parse-error',
+ {scope: SCOPE, updateViaCache: 'none'});
+ await promise_rejects_js(t, TypeError, fail);
+ assert_equals(registration.updateViaCache, 'imports',
+ 'after update attempt');
+ }, 'updateViaCache is not updated if register() rejects');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/rejections.https.html b/third_party/web_platform_tests/service-workers/service-worker/rejections.https.html
new file mode 100644
index 0000000..8002ad9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/rejections.https.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<title>Service Worker: Rejection Types</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+(function() {
+ var t = async_test('Rejections are DOMExceptions');
+ t.step(function() {
+
+ navigator.serviceWorker.register('http://example.com').then(
+ t.step_func(function() { assert_unreached('Registration should fail'); }),
+ t.step_func(function(reason) {
+ assert_true(reason instanceof DOMException);
+ assert_true(reason instanceof Error);
+ t.done();
+ }));
+ });
+}());
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/request-end-to-end.https.html b/third_party/web_platform_tests/service-workers/service-worker/request-end-to-end.https.html
new file mode 100644
index 0000000..a39cead
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/request-end-to-end.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: FetchEvent.request passed to onfetch</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(t => {
+ var url = 'resources/request-end-to-end-worker.js';
+ var scope = 'resources/blank.html';
+ return service_worker_unregister_and_register(t, url, scope)
+ .then(r => {
+ add_completion_callback(() => { r.unregister(); });
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(() => { return with_iframe(scope); })
+ .then(frame => {
+ add_completion_callback(() => { frame.remove(); });
+
+ var result = JSON.parse(frame.contentDocument.body.textContent);
+ assert_equals(result.url, frame.src, 'request.url');
+ assert_equals(result.method, 'GET', 'request.method');
+ assert_equals(result.referrer, location.href, 'request.referrer');
+ assert_equals(result.mode, 'navigate', 'request.mode');
+ assert_equals(result.request_construct_error, '',
+ 'Constructing a Request with a Request whose mode ' +
+ 'is navigate and non-empty RequestInit must not throw a ' +
+ 'TypeError.')
+ assert_equals(result.credentials, 'include', 'request.credentials');
+ assert_equals(result.redirect, 'manual', 'request.redirect');
+ assert_equals(result.headers['user-agent'], undefined,
+ 'Default User-Agent header should not be passed to ' +
+ 'onfetch event.')
+ assert_equals(result.append_header_error, 'TypeError',
+ 'Appending a new header to the request must throw a ' +
+ 'TypeError.')
+ });
+ }, 'Test FetchEvent.request passed to onfetch');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resource-timing-bodySize.https.html b/third_party/web_platform_tests/service-workers/service-worker/resource-timing-bodySize.https.html
new file mode 100644
index 0000000..5c2b1eb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resource-timing-bodySize.https.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const {REMOTE_ORIGIN} = get_host_info();
+
+/*
+ This test does the following:
+ - Loads a service worker
+ - Loads an iframe in the service worker's scope
+ - The service worker tries to fetch a resource which is either:
+ - constructed inside the service worker
+ - fetched from a different URL ny the service worker
+ - Streamed from a differend URL by the service worker
+ - Passes through
+ - By default the RT entry should have encoded/decoded body size. except for
+ the case where the response is an opaque pass-through.
+*/
+function test_scenario({tao, mode, name}) {
+ promise_test(async (t) => {
+ const uid = token();
+ const worker_url = `resources/fetch-response.js?uid=${uid}`;
+ const scope = `resources/fetch-response.html?uid=${uid}`;
+ const iframe = document.createElement('iframe');
+ const path = name === "passthrough" ? `element-timing/resources/TAOImage.py?origin=*&tao=${
+ tao === "pass" ? "wildcard" : "none"})}` : name;
+
+ iframe.src = `${scope}&path=${encodeURIComponent(
+ `${mode === "same-origin" ? "" : REMOTE_ORIGIN}/${path}`)}&mode=${mode}`;
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+ t.add_cleanup(() => iframe.remove());
+ await wait_for_state(t, registration.installing, 'activated');
+ const waitForMessage = new Promise(resolve =>
+ window.addEventListener('message', ({data}) => resolve(data)));
+ document.body.appendChild(iframe);
+ const {buffer, entry} = await waitForMessage;
+ const expectPass = name !== "passthrough" || mode !== "no-cors";
+ assert_equals(buffer.byteLength, expectPass ? entry.decodedBodySize : 0);
+ assert_equals(buffer.byteLength, expectPass ? entry.encodedBodySize : 0);
+ }, `Response body size: ${name}, ${mode}, TAO ${tao}`);
+}
+for (const mode of ["cors", "no-cors", "same-origin"]) {
+ for (const tao of ["pass", "fail"])
+ for (const name of ['constructed', 'forward', 'stream', 'passthrough']) {
+ test_scenario({tao, mode, name});
+ }
+}
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resource-timing-cross-origin.https.html b/third_party/web_platform_tests/service-workers/service-worker/resource-timing-cross-origin.https.html
new file mode 100644
index 0000000..2155d7f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resource-timing-cross-origin.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8" />
+<title>This test validates Resource Timing for cross origin content fetched by Service Worker from an originally same-origin URL.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+</head>
+
+<body>
+<script>
+function test_sw_resource_timing({ mode }) {
+ promise_test(async t => {
+ const worker_url = `resources/worker-fetching-cross-origin.js?mode=${mode}`;
+ const scope = 'resources/iframe-with-image.html';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ const frame_performance = frame.contentWindow.performance;
+ // Check that there is one entry for which the timing allow check algorithm failed.
+ const entries = frame_performance.getEntriesByType('resource');
+ assert_equals(entries.length, 1);
+ const entry = entries[0];
+ assert_equals(entry.redirectStart, 0, 'redirectStart should be 0 in cross-origin request.');
+ assert_equals(entry.redirectEnd, 0, 'redirectEnd should be 0 in cross-origin request.');
+ assert_equals(entry.domainLookupStart, entry.fetchStart, 'domainLookupStart should be 0 in cross-origin request.');
+ assert_equals(entry.domainLookupEnd, entry.fetchStart, 'domainLookupEnd should be 0 in cross-origin request.');
+ assert_equals(entry.connectStart, entry.fetchStart, 'connectStart should be 0 in cross-origin request.');
+ assert_equals(entry.connectEnd, entry.fetchStart, 'connectEnd should be 0 in cross-origin request.');
+ assert_greater_than(entry.responseStart, entry.fetchStart, 'responseStart should be 0 in cross-origin request.');
+ assert_equals(entry.secureConnectionStart, entry.fetchStart, 'secureConnectionStart should be 0 in cross-origin request.');
+ assert_equals(entry.transferSize, 0, 'decodedBodySize should be 0 in cross-origin request.');
+ frame.remove();
+ await registration.unregister();
+ }, `Test that timing allow check fails when service worker changes origin from same to cross origin (${mode}).`);
+}
+
+test_sw_resource_timing({ mode: "cors" });
+test_sw_resource_timing({ mode: "no-cors" });
+
+
+</script>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resource-timing-fetch-variants.https.html b/third_party/web_platform_tests/service-workers/service-worker/resource-timing-fetch-variants.https.html
new file mode 100644
index 0000000..8d4f0be
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resource-timing-fetch-variants.https.html
@@ -0,0 +1,121 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test various interactions between fetch, service-workers and resource timing</title>
+<meta charset="utf-8" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<link rel="help" href="https://w3c.github.io/resource-timing/" >
+<!--
+ This test checks that the different properties in a PerformanceResourceTimingEntry
+ measure what they are supposed to measure according to spec.
+
+ It is achieved by generating programmatic delays and redirects inside a service worker,
+ and checking how the different metrics respond to the delays and redirects.
+
+ The deltas are not measured precisely, but rather relatively to the delay.
+ The delay needs to be long enough so that it's clear that what's measured is the test's
+ programmatic delay and not arbitrary system delays.
+-->
+</head>
+
+<body>
+<script>
+
+const delay = 200;
+const absolutePath = `${base_path()}/simple.txt`
+function toSequence({before, after, entry}) {
+ /*
+ The order of keys is the same as in this chart:
+ https://w3c.github.io/resource-timing/#attribute-descriptions
+ */
+ const keys = [
+ 'startTime',
+ 'redirectStart',
+ 'redirectEnd',
+ 'workerStart',
+ 'fetchStart',
+ 'connectStart',
+ 'requestStart',
+ 'responseStart',
+ 'responseEnd'
+ ];
+
+ let cursor = before;
+ const step = value => {
+ // A zero/null value, reflect that in the sequence
+ if (!value)
+ return value;
+
+ // Value is the same as before
+ if (value === cursor)
+ return "same";
+
+ // Oops, value is in the wrong place
+ if (value < cursor)
+ return "back";
+
+ // Delta is greater than programmatic delay, this is where the delay is measured.
+ if ((value - cursor) >= delay)
+ return "delay";
+
+ // Some small delta, probably measuring an actual networking stack delay
+ return "tick";
+ }
+
+ const res = keys.map(key => {
+ const value = step(entry[key]);
+ if (entry[key])
+ cursor = entry[key];
+ return [key, value];
+ });
+
+ return Object.fromEntries([...res, ['after', step(after)]]);
+}
+async function testVariant(t, variant) {
+ const worker_url = 'resources/fetch-variants-worker.js';
+ const url = encodeURIComponent(`simple.txt?delay=${delay}&variant=${variant}`);
+ const scope = `resources/iframe-with-fetch-variants.html?url=${url}`;
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+ const result = await new Promise(resolve => window.addEventListener('message', message => {
+ resolve(message.data);
+ }))
+
+ return toSequence(result);
+}
+
+promise_test(async t => {
+ const result = await testVariant(t, 'redirect');
+ assert_equals(result.redirectStart, 0);
+}, 'Redirects done from within a service-worker should not be exposed to client ResourceTiming');
+
+promise_test(async t => {
+ const result = await testVariant(t, 'forward');
+ assert_equals(result.connectStart, 'same');
+}, 'Connection info from within a service-worker should not be exposed to client ResourceTiming');
+
+promise_test(async t => {
+ const result = await testVariant(t, 'forward');
+ assert_not_equals(result.requestStart, 'back');
+}, 'requestStart should never be before fetchStart');
+
+promise_test(async t => {
+ const result = await testVariant(t, 'delay-after-fetch');
+ const whereIsDelayMeasured = Object.entries(result).find(r => r[1] === 'delay')[0];
+ assert_equals(whereIsDelayMeasured, 'responseStart');
+}, 'Delay from within service-worker (after internal fetching) should be accessible through `responseStart`');
+
+promise_test(async t => {
+ const result = await testVariant(t, 'delay-before-fetch');
+ const whereIsDelayMeasured = Object.entries(result).find(r => r[1] === 'delay')[0];
+ assert_equals(whereIsDelayMeasured, 'responseStart');
+}, 'Delay from within service-worker (before internal fetching) should be measured before responseStart in the client ResourceTiming entry');
+</script>
+
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resource-timing.sub.https.html b/third_party/web_platform_tests/service-workers/service-worker/resource-timing.sub.https.html
new file mode 100644
index 0000000..9808ae5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resource-timing.sub.https.html
@@ -0,0 +1,150 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function resourceUrl(path) {
+ return "https://{{host}}:{{ports[https][0]}}" + base_path() + path;
+}
+
+function crossOriginUrl(path) {
+ return "https://{{hosts[alt][]}}:{{ports[https][0]}}" + base_path() + path;
+}
+
+// Verify existance of a PerformanceEntry and the order between the timings.
+//
+// |options| has these properties:
+// performance: Performance interface to verify existance of the entry.
+// resource: the path to the resource.
+// mode: 'cross-origin' to load the resource from third-party origin.
+// description: the description passed to each assertion.
+// should_no_performance_entry: no entry is expected to be recorded when it's
+// true.
+function verify(options) {
+ const url = options.mode === 'cross-origin' ? crossOriginUrl(options.resource)
+ : resourceUrl(options.resource);
+ const entryList = options.performance.getEntriesByName(url, 'resource');
+ if (options.should_no_performance_entry) {
+ // The performance timeline may not have an entry for a resource
+ // which failed to load.
+ assert_equals(entryList.length, 0, options.description);
+ return;
+ }
+
+ assert_equals(entryList.length, 1, options.description);
+ const entry = entryList[0];
+ assert_equals(entry.entryType, 'resource', options.description);
+
+ // workerStart is recorded between startTime and fetchStart.
+ assert_greater_than(entry.workerStart, 0, options.description);
+ assert_greater_than_equal(entry.workerStart, entry.startTime, options.description);
+ assert_less_than_equal(entry.workerStart, entry.fetchStart, options.description);
+
+ if (options.mode === 'cross-origin') {
+ assert_equals(entry.responseStart, 0, options.description);
+ assert_greater_than_equal(entry.responseEnd, entry.fetchStart, options.description);
+ } else {
+ assert_greater_than_equal(entry.responseStart, entry.fetchStart, options.description);
+ assert_greater_than_equal(entry.responseEnd, entry.responseStart, options.description);
+ }
+
+ // responseEnd follows fetchStart.
+ assert_greater_than(entry.responseEnd, entry.fetchStart, options.description);
+ // duration always has some value.
+ assert_greater_than(entry.duration, 0, options.description);
+
+ if (options.resource.indexOf('redirect.py') != -1) {
+ assert_less_than_equal(entry.workerStart, entry.redirectStart,
+ options.description);
+ } else {
+ assert_equals(entry.redirectStart, 0, options.description);
+ }
+}
+
+promise_test(async (t) => {
+ const worker_url = 'resources/resource-timing-worker.js';
+ const scope = 'resources/resource-timing-iframe.sub.html';
+
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ const performance = frame.contentWindow.performance;
+ verify({
+ performance: performance,
+ resource: 'resources/sample.js',
+ mode: 'same-origin',
+ description: 'Generated response',
+ });
+ verify({
+ performance: performance,
+ resource: 'resources/empty.js',
+ mode: 'same-origin',
+ description: 'Network fallback',
+ });
+ verify({
+ performance: performance,
+ resource: 'resources/redirect.py?Redirect=empty.js',
+ mode: 'same-origin',
+ description: 'Redirect',
+ });
+ verify({
+ performance: performance,
+ resource: 'resources/square.png',
+ mode: 'same-origin',
+ description: 'Network fallback image',
+ });
+ // Test that worker start is available on cross-origin no-cors
+ // subresources.
+ verify({
+ performance: performance,
+ resource: 'resources/square.png',
+ mode: 'cross-origin',
+ description: 'Network fallback cross-origin image',
+ });
+
+ // Tests for resouces which failed to load.
+ verify({
+ performance: performance,
+ resource: 'resources/missing.jpg',
+ mode: 'same-origin',
+ description: 'Network fallback load failure',
+ });
+ verify({
+ performance: performance,
+ resource: 'resources/missing.jpg',
+ mode: 'cross-origin',
+ description: 'Network fallback cross-origin load failure',
+ });
+ // Tests for respondWith(fetch()).
+ verify({
+ performance: performance,
+ resource: 'resources/missing.jpg?SWRespondsWithFetch',
+ mode: 'same-origin',
+ description: 'Resource in iframe, nonexistent but responded with fetch to another.',
+ });
+ verify({
+ performance: performance,
+ resource: 'resources/sample.txt?SWFetched',
+ mode: 'same-origin',
+ description: 'Resource fetched as response from missing.jpg?SWRespondsWithFetch.',
+ should_no_performance_entry: true,
+ });
+ // Test for a normal resource that is unaffected by the Service Worker.
+ verify({
+ performance: performance,
+ resource: 'resources/empty-worker.js',
+ mode: 'same-origin',
+ description: 'Resource untouched by the Service Worker.',
+ });
+}, 'Controlled resource loads');
+
+test(() => {
+ const url = resourceUrl('resources/test-helpers.sub.js');
+ const entry = window.performance.getEntriesByName(url, 'resource')[0];
+ assert_equals(entry.workerStart, 0, 'Non-controlled');
+}, 'Non-controlled resource loads');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/404.py b/third_party/web_platform_tests/service-workers/service-worker/resources/404.py
new file mode 100644
index 0000000..1ee4af1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/404.py
@@ -0,0 +1,5 @@
+# iframe does not fire onload event if the response's content-type is not
+# text/plain or text/html so this script exists if you want to test a 404 load
+# in an iframe.
+def main(req, res):
+ return 404, [(b'Content-Type', b'text/plain')], b"Page not found"
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-blank-dynamic-nested-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-blank-dynamic-nested-frame.html
new file mode 100644
index 0000000..1e0c620
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-blank-dynamic-nested-frame.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+<body>
+<script>
+function nestedLoaded() {
+ parent.postMessage({ type: 'NESTED_LOADED' }, '*');
+}
+
+// dynamically add an about:blank iframe
+var f = document.createElement('iframe');
+f.onload = nestedLoaded;
+document.body.appendChild(f);
+
+// Helper routine to make it slightly easier for our parent to find
+// the nested frame.
+function nested() {
+ return f.contentWindow;
+}
+</script>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-blank-nested-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-blank-nested-frame.html
new file mode 100644
index 0000000..16f7e7c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-blank-nested-frame.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+<body>
+<script>
+function nestedLoaded() {
+ parent.postMessage({ type: 'NESTED_LOADED' }, '*');
+}
+
+// Helper routine to make it slightly easier for our parent to find
+// the nested frame.
+function nested() {
+ return document.getElementById('nested').contentWindow;
+}
+
+// NOTE: Make sure not to touch the iframe directly here. We want to
+// test the case where the initial about:blank document is not
+// directly accessed before load.
+</script>
+<iframe id="nested" onload="nestedLoaded()"></iframe>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-frame.py b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-frame.py
new file mode 100644
index 0000000..a29ff9d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-frame.py
@@ -0,0 +1,31 @@
+def main(request, response):
+ if b'nested' in request.GET:
+ return (
+ [(b'Content-Type', b'text/html')],
+ b'failed: nested frame was not intercepted by the service worker'
+ )
+
+ return ([(b'Content-Type', b'text/html')], b"""
+<!doctype html>
+<html>
+<body>
+<script>
+function nestedLoaded() {
+ parent.postMessage({ type: 'NESTED_LOADED' }, '*');
+}
+</script>
+<iframe src="?nested=true" id="nested" onload="nestedLoaded()"></iframe>
+<script>
+// Helper routine to make it slightly easier for our parent to find
+// the nested frame.
+function nested() {
+ return document.getElementById('nested').contentWindow;
+}
+
+// NOTE: Make sure not to touch the iframe directly here. We want to
+// test the case where the initial about:blank document is not
+// directly accessed before load.
+</script>
+</body>
+</html>
+""")
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-ping-frame.py b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-ping-frame.py
new file mode 100644
index 0000000..30fbbbb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-ping-frame.py
@@ -0,0 +1,49 @@
+def main(request, response):
+ if b'nested' in request.GET:
+ return (
+ [(b'Content-Type', b'text/html')],
+ b'failed: nested frame was not intercepted by the service worker'
+ )
+
+ return ([(b'Content-Type', b'text/html')], b"""
+<!doctype html>
+<html>
+<body>
+<script>
+function nestedLoaded() {
+ parent.postMessage({ type: 'NESTED_LOADED' }, '*');
+}
+</script>
+<iframe src="?nested=true&ping=true" id="nested" onload="nestedLoaded()"></iframe>
+<script>
+// Helper routine to make it slightly easier for our parent to find
+// the nested frame.
+function nested() {
+ return document.getElementById('nested').contentWindow;
+}
+
+// This modifies the nested iframe immediately and does not wait for it to
+// load. This effectively modifies the global for the initial about:blank
+// document. Any modifications made here should be preserved after the
+// frame loads because the global should be re-used.
+let win = nested();
+if (win.location.href !== 'about:blank') {
+ parent.postMessage({
+ type: 'NESTED_LOADED',
+ result: 'failed: nested iframe does not have an initial about:blank URL'
+ }, '*');
+} else {
+ win.navigator.serviceWorker.addEventListener('message', evt => {
+ if (evt.data.type === 'PING') {
+ evt.source.postMessage({
+ type: 'PONG',
+ location: win.location.toString()
+ });
+ }
+ });
+ win.navigator.serviceWorker.startMessages();
+}
+</script>
+</body>
+</html>
+""")
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-popup-frame.py b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-popup-frame.py
new file mode 100644
index 0000000..04c12a6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-popup-frame.py
@@ -0,0 +1,32 @@
+def main(request, response):
+ if b'nested' in request.GET:
+ return (
+ [(b'Content-Type', b'text/html')],
+ b'failed: nested frame was not intercepted by the service worker'
+ )
+
+ return ([(b'Content-Type', b'text/html')], b"""
+<!doctype html>
+<html>
+<body>
+<script>
+function nestedLoaded() {
+ parent.postMessage({ type: 'NESTED_LOADED' }, '*');
+}
+
+let popup = window.open('?nested=true');
+popup.onload = nestedLoaded;
+
+addEventListener('unload', evt => {
+ popup.close();
+}, { once: true });
+
+// Helper routine to make it slightly easier for our parent to find
+// the nested popup window.
+function nested() {
+ return popup;
+}
+</script>
+</body>
+</html>
+""")
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-srcdoc-nested-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-srcdoc-nested-frame.html
new file mode 100644
index 0000000..0122a00
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-srcdoc-nested-frame.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<html>
+<body>
+<script>
+function nestedLoaded() {
+ parent.postMessage({ type: 'NESTED_LOADED' }, '*');
+}
+</script>
+<iframe id="nested" srcdoc="<div></div>" onload="nestedLoaded()"></iframe>
+<script>
+// Helper routine to make it slightly easier for our parent to find
+// the nested frame.
+function nested() {
+ return document.getElementById('nested').contentWindow;
+}
+
+// NOTE: Make sure not to touch the iframe directly here. We want to
+// test the case where the initial about:blank document is not
+// directly accessed before load.
+</script>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-uncontrolled-nested-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-uncontrolled-nested-frame.html
new file mode 100644
index 0000000..8950915
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-uncontrolled-nested-frame.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<html>
+<body>
+<script>
+function nestedLoaded() {
+ parent.postMessage({ type: 'NESTED_LOADED' }, '*');
+}
+</script>
+<iframe src="empty.html?nested=true" id="nested" onload="nestedLoaded()"></iframe>
+<script>
+// Helper routine to make it slightly easier for our parent to find
+// the nested frame.
+function nested() {
+ return document.getElementById('nested').contentWindow;
+}
+
+// NOTE: Make sure not to touch the iframe directly here. We want to
+// test the case where the initial about:blank document is not
+// directly accessed before load.
+</script>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-worker.js
new file mode 100644
index 0000000..f43598e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/about-blank-replacement-worker.js
@@ -0,0 +1,95 @@
+// Helper routine to find a client that matches a particular URL. Note, we
+// require that Client to be controlled to avoid false matches with other
+// about:blank windows the browser might have. The initial about:blank should
+// inherit the controller from its parent.
+async function getClientByURL(url) {
+ let list = await clients.matchAll();
+ return list.find(client => client.url === url);
+}
+
+// Helper routine to perform a ping-pong with the given target client. We
+// expect the Client to respond with its location URL.
+async function pingPong(target) {
+ function waitForPong() {
+ return new Promise(resolve => {
+ self.addEventListener('message', function onMessage(evt) {
+ if (evt.data.type === 'PONG') {
+ resolve(evt.data.location);
+ }
+ });
+ });
+ }
+
+ target.postMessage({ type: 'PING' })
+ return await waitForPong(target);
+}
+
+addEventListener('fetch', async evt => {
+ let url = new URL(evt.request.url);
+ if (!url.searchParams.get('nested')) {
+ return;
+ }
+
+ evt.respondWith(async function() {
+ // Find the initial about:blank document.
+ const client = await getClientByURL('about:blank');
+ if (!client) {
+ return new Response('failure: could not find about:blank client');
+ }
+
+ // If the nested frame is configured to support a ping-pong, then
+ // ping it now to verify its message listener exists. We also
+ // verify the Client's idea of its own location URL while we are doing
+ // this.
+ if (url.searchParams.get('ping')) {
+ const loc = await pingPong(client);
+ if (loc !== 'about:blank') {
+ return new Response(`failure: got location {$loc}, expected about:blank`);
+ }
+ }
+
+ // Finally, allow the nested frame to complete loading. We place the
+ // Client ID we found for the initial about:blank in the body.
+ return new Response(client.id);
+ }());
+});
+
+addEventListener('message', evt => {
+ if (evt.data.type !== 'GET_CLIENT_ID') {
+ return;
+ }
+
+ evt.waitUntil(async function() {
+ let url = new URL(evt.data.url);
+
+ // Find the given Client by its URL.
+ let client = await getClientByURL(evt.data.url);
+ if (!client) {
+ evt.source.postMessage({
+ type: 'GET_CLIENT_ID',
+ result: `failure: could not find ${evt.data.url} client`
+ });
+ return;
+ }
+
+ // If the Client supports a ping-pong, then do it now to verify
+ // the message listener exists and its location matches the
+ // Client object.
+ if (url.searchParams.get('ping')) {
+ let loc = await pingPong(client);
+ if (loc !== evt.data.url) {
+ evt.source.postMessage({
+ type: 'GET_CLIENT_ID',
+ result: `failure: got location ${loc}, expected ${evt.data.url}`
+ });
+ return;
+ }
+ }
+
+ // Finally, send the client ID back.
+ evt.source.postMessage({
+ type: 'GET_CLIENT_ID',
+ result: client.id
+ });
+ }());
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/basic-module-2.js b/third_party/web_platform_tests/service-workers/service-worker/resources/basic-module-2.js
new file mode 100644
index 0000000..189b1c8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/basic-module-2.js
@@ -0,0 +1 @@
+export default 'hello again!';
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/basic-module.js b/third_party/web_platform_tests/service-workers/service-worker/resources/basic-module.js
new file mode 100644
index 0000000..789a89b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/basic-module.js
@@ -0,0 +1 @@
+export default 'hello!';
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/blank.html b/third_party/web_platform_tests/service-workers/service-worker/resources/blank.html
new file mode 100644
index 0000000..a3c3a46
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/blank.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Empty doc</title>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/bytecheck-worker-imported-script.py b/third_party/web_platform_tests/service-workers/service-worker/resources/bytecheck-worker-imported-script.py
new file mode 100644
index 0000000..1931c77
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/bytecheck-worker-imported-script.py
@@ -0,0 +1,20 @@
+import time
+
+def main(request, response):
+ headers = [(b'Content-Type', b'application/javascript'),
+ (b'Cache-Control', b'max-age=0'),
+ (b'Access-Control-Allow-Origin', b'*')]
+
+ imported_content_type = b''
+ if b'imported' in request.GET:
+ imported_content_type = request.GET[b'imported']
+
+ imported_content = b'default'
+ if imported_content_type == b'time':
+ imported_content = b'%f' % time.time()
+
+ body = b'''
+ // %s
+ ''' % (imported_content)
+
+ return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/bytecheck-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/bytecheck-worker.py
new file mode 100644
index 0000000..10f3bce
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/bytecheck-worker.py
@@ -0,0 +1,38 @@
+import time
+
+def main(request, response):
+ headers = [(b'Content-Type', b'application/javascript'),
+ (b'Cache-Control', b'max-age=0')]
+
+ main_content_type = b''
+ if b'main' in request.GET:
+ main_content_type = request.GET[b'main']
+
+ main_content = b'default'
+ if main_content_type == b'time':
+ main_content = b'%f' % time.time()
+
+ imported_request_path = b''
+ if b'path' in request.GET:
+ imported_request_path = request.GET[b'path']
+
+ imported_request_type = b''
+ if b'imported' in request.GET:
+ imported_request_type = request.GET[b'imported']
+
+ imported_request = b''
+ if imported_request_type == b'time':
+ imported_request = b'?imported=time'
+
+ if b'type' in request.GET and request.GET[b'type'] == b'module':
+ body = b'''
+ // %s
+ import '%sbytecheck-worker-imported-script.py%s';
+ ''' % (main_content, imported_request_path, imported_request)
+ else:
+ body = b'''
+ // %s
+ importScripts('%sbytecheck-worker-imported-script.py%s');
+ ''' % (main_content, imported_request_path, imported_request)
+
+ return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/claim-blob-url-worker-fetch-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-blob-url-worker-fetch-iframe.html
new file mode 100644
index 0000000..12ae1a8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-blob-url-worker-fetch-iframe.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<script>
+const baseLocation = window.location;
+const workerScript =
+ `self.onmessage = async (e) => {
+ const url = new URL(e.data, '${baseLocation}').href;
+ const response = await fetch(url);
+ const text = await response.text();
+ self.postMessage(text);
+ };`;
+const blob = new Blob([workerScript], { type: 'text/javascript' });
+const blobUrl = URL.createObjectURL(blob);
+const worker = new Worker(blobUrl);
+
+function fetch_in_worker(url) {
+ return new Promise((resolve) => {
+ worker.onmessage = (e) => resolve(e.data);
+ worker.postMessage(url);
+ });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/claim-nested-worker-fetch-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-nested-worker-fetch-iframe.html
new file mode 100644
index 0000000..2fa15db
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-nested-worker-fetch-iframe.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<script>
+// An iframe that starts a nested worker. Our parent frame (the test page) calls
+// fetch_in_worker() to ask the nested worker to perform a fetch to see whether
+// it's controlled by a service worker.
+var worker = new Worker('./claim-nested-worker-fetch-parent-worker.js');
+
+function fetch_in_worker(url) {
+ return new Promise((resolve) => {
+ worker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ worker.postMessage(url);
+ });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/claim-nested-worker-fetch-parent-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-nested-worker-fetch-parent-worker.js
new file mode 100644
index 0000000..f5ff7c2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-nested-worker-fetch-parent-worker.js
@@ -0,0 +1,12 @@
+try {
+ var worker = new Worker('./claim-worker-fetch-worker.js');
+
+ self.onmessage = (event) => {
+ worker.postMessage(event.data);
+ }
+ worker.onmessage = (event) => {
+ self.postMessage(event.data);
+ };
+} catch (e) {
+ self.postMessage("Fail: " + e.data);
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/claim-shared-worker-fetch-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-shared-worker-fetch-iframe.html
new file mode 100644
index 0000000..ad865b8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-shared-worker-fetch-iframe.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<script>
+var worker = new SharedWorker('./claim-shared-worker-fetch-worker.js');
+
+function fetch_in_shared_worker(url) {
+ return new Promise((resolve) => {
+ worker.port.onmessage = (event) => {
+ resolve(event.data);
+ };
+ worker.port.postMessage(url);
+ });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/claim-shared-worker-fetch-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-shared-worker-fetch-worker.js
new file mode 100644
index 0000000..ddc8bea
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-shared-worker-fetch-worker.js
@@ -0,0 +1,8 @@
+self.onconnect = (event) => {
+ var port = event.ports[0];
+ event.ports[0].onmessage = (evt) => {
+ fetch(evt.data)
+ .then(response => response.text())
+ .then(text => port.postMessage(text));
+ };
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/claim-with-redirect-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-with-redirect-iframe.html
new file mode 100644
index 0000000..4150d7e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-with-redirect-iframe.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<body>
+<script>
+var host_info = get_host_info();
+
+function send_result(result) {
+ window.parent.postMessage({message: result},
+ host_info['HTTPS_ORIGIN']);
+}
+
+function executeTask(params) {
+ // Execute task for each parameter
+ if (params.has('register')) {
+ var worker_url = decodeURIComponent(params.get('register'));
+ var scope = decodeURIComponent(params.get('scope'));
+ navigator.serviceWorker.register(worker_url, {scope: scope})
+ .then(r => send_result('registered'));
+ } else if (params.has('redirected')) {
+ send_result('redirected');
+ } else if (params.has('update')) {
+ var scope = decodeURIComponent(params.get('update'));
+ navigator.serviceWorker.getRegistration(scope)
+ .then(r => r.update())
+ .then(() => send_result('updated'));
+ } else if (params.has('unregister')) {
+ var scope = decodeURIComponent(params.get('unregister'));
+ navigator.serviceWorker.getRegistration(scope)
+ .then(r => r.unregister())
+ .then(succeeded => {
+ if (succeeded) {
+ send_result('unregistered');
+ } else {
+ send_result('failure: unregister');
+ }
+ });
+ } else {
+ send_result('unknown parameter: ' + params.toString());
+ }
+}
+
+var params = new URLSearchParams(location.search.slice(1));
+executeTask(params);
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/claim-worker-fetch-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-worker-fetch-iframe.html
new file mode 100644
index 0000000..92c5d15
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-worker-fetch-iframe.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<script>
+var worker = new Worker('./claim-worker-fetch-worker.js');
+
+function fetch_in_worker(url) {
+ return new Promise((resolve) => {
+ worker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ worker.postMessage(url);
+ });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/claim-worker-fetch-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-worker-fetch-worker.js
new file mode 100644
index 0000000..7080181
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-worker-fetch-worker.js
@@ -0,0 +1,5 @@
+self.onmessage = (event) => {
+ fetch(event.data)
+ .then(response => response.text())
+ .then(text => self.postMessage(text));
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/claim-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-worker.js
new file mode 100644
index 0000000..1800407
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/claim-worker.js
@@ -0,0 +1,19 @@
+self.addEventListener('message', function(event) {
+ self.clients.claim()
+ .then(function(result) {
+ if (result !== undefined) {
+ event.data.port.postMessage(
+ 'FAIL: claim() should be resolved with undefined');
+ return;
+ }
+ event.data.port.postMessage('PASS');
+ })
+ .catch(function(error) {
+ event.data.port.postMessage('FAIL: exception: ' + error.name);
+ });
+ });
+
+self.addEventListener('fetch', function(event) {
+ if (!/404/.test(event.request.url))
+ event.respondWith(new Response('Intercepted!'));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/classic-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/classic-worker.js
new file mode 100644
index 0000000..36a32b1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/classic-worker.js
@@ -0,0 +1 @@
+importScripts('./imported-classic-script.js');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/client-id-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/client-id-worker.js
new file mode 100644
index 0000000..ec71b34
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/client-id-worker.js
@@ -0,0 +1,27 @@
+self.onmessage = function(e) {
+ var port = e.data.port;
+ var message = [];
+
+ var promise = Promise.resolve()
+ .then(function() {
+ // 1st matchAll()
+ return self.clients.matchAll().then(function(clients) {
+ clients.forEach(function(client) {
+ message.push(client.id);
+ });
+ });
+ })
+ .then(function() {
+ // 2nd matchAll()
+ return self.clients.matchAll().then(function(clients) {
+ clients.forEach(function(client) {
+ message.push(client.id);
+ });
+ });
+ })
+ .then(function() {
+ // Send an array containing ids of clients from 1st and 2nd matchAll()
+ port.postMessage(message);
+ });
+ e.waitUntil(promise);
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/client-navigate-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/client-navigate-frame.html
new file mode 100644
index 0000000..7e186f8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/client-navigate-frame.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<script>
+ fetch("clientId")
+ .then(function(response) {
+ return response.text();
+ })
+ .then(function(text) {
+ parent.postMessage({id: text}, "*");
+ });
+</script>
+<body style="background-color: red;"></body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/client-navigate-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/client-navigate-worker.js
new file mode 100644
index 0000000..6101d5d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/client-navigate-worker.js
@@ -0,0 +1,92 @@
+importScripts("worker-testharness.js");
+importScripts("test-helpers.sub.js");
+importScripts("/common/get-host-info.sub.js")
+importScripts("testharness-helpers.js")
+
+setup({ explicit_done: true });
+
+self.onfetch = function(e) {
+ if (e.request.url.indexOf("client-navigate-frame.html") >= 0) {
+ return;
+ }
+ e.respondWith(new Response(e.clientId));
+};
+
+function pass(test, url) {
+ return { result: test,
+ url: url };
+}
+
+function fail(test, reason) {
+ return { result: "FAILED " + test + " " + reason }
+}
+
+self.onmessage = function(e) {
+ var port = e.data.port;
+ var test = e.data.test;
+ var clientId = e.data.clientId;
+ var clientUrl = "";
+ if (test === "test_client_navigate_success") {
+ promise_test(function(t) {
+ this.add_cleanup(() => port.postMessage(pass(test, clientUrl)));
+ return self.clients.get(clientId)
+ .then(client => client.navigate("client-navigated-frame.html"))
+ .then(client => {
+ clientUrl = client.url;
+ assert_true(client instanceof WindowClient);
+ })
+ .catch(unreached_rejection(t));
+ }, "Return value should be instance of WindowClient");
+ done();
+ } else if (test === "test_client_navigate_cross_origin") {
+ promise_test(function(t) {
+ this.add_cleanup(() => port.postMessage(pass(test, clientUrl)));
+ var path = new URL('client-navigated-frame.html', self.location.href).pathname;
+ var url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + path;
+ return self.clients.get(clientId)
+ .then(client => client.navigate(url))
+ .then(client => {
+ clientUrl = (client && client.url) || "";
+ assert_equals(client, null,
+ 'cross-origin navigate resolves with null');
+ })
+ .catch(unreached_rejection(t));
+ }, "Navigating to different origin should resolve with null");
+ done();
+ } else if (test === "test_client_navigate_about_blank") {
+ promise_test(function(t) {
+ this.add_cleanup(function() { port.postMessage(pass(test, "")); });
+ return self.clients.get(clientId)
+ .then(client => promise_rejects_js(t, TypeError, client.navigate("about:blank")))
+ .catch(unreached_rejection(t));
+ }, "Navigating to about:blank should reject with TypeError");
+ done();
+ } else if (test === "test_client_navigate_mixed_content") {
+ promise_test(function(t) {
+ this.add_cleanup(function() { port.postMessage(pass(test, "")); });
+ var path = new URL('client-navigated-frame.html', self.location.href).pathname;
+ // Insecure URL should fail since the frame is owned by a secure parent
+ // and navigating to http:// would create a mixed-content violation.
+ var url = get_host_info()['HTTP_REMOTE_ORIGIN'] + path;
+ return self.clients.get(clientId)
+ .then(client => promise_rejects_js(t, TypeError, client.navigate(url)))
+ .catch(unreached_rejection(t));
+ }, "Navigating to mixed-content iframe should reject with TypeError");
+ done();
+ } else if (test === "test_client_navigate_redirect") {
+ var host_info = get_host_info();
+ var url = new URL(host_info['HTTPS_REMOTE_ORIGIN']).toString() +
+ new URL("client-navigated-frame.html", location).pathname.substring(1);
+ promise_test(function(t) {
+ this.add_cleanup(() => port.postMessage(pass(test, clientUrl)));
+ return self.clients.get(clientId)
+ .then(client => client.navigate("redirect.py?Redirect=" + url))
+ .then(client => {
+ clientUrl = (client && client.url) || ""
+ assert_equals(client, null);
+ })
+ .catch(unreached_rejection(t));
+ }, "Redirecting to another origin should resolve with null");
+ done();
+ }
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/client-navigated-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/client-navigated-frame.html
new file mode 100644
index 0000000..307f7f9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/client-navigated-frame.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<body style="background-color: green;"></body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.html b/third_party/web_platform_tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.html
new file mode 100644
index 0000000..00f6ace
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<script>
+
+// Return a URL of a client when it's successful.
+function createAndFetchFromBlobWorker() {
+ const fetchURL = new URL('get-worker-client-url.txt', window.location).href;
+ const workerScript =
+ `self.onmessage = async (e) => {
+ const response = await fetch(e.data.url);
+ const text = await response.text();
+ self.postMessage({"result": text, "expected": self.location.href});
+ };`;
+ const blob = new Blob([workerScript], { type: 'text/javascript' });
+ const blobUrl = URL.createObjectURL(blob);
+
+ const worker = new Worker(blobUrl);
+ return new Promise((resolve, reject) => {
+ worker.onmessage = e => resolve(e.data);
+ worker.onerror = e => reject(e.message);
+ worker.postMessage({"url": fetchURL});
+ });
+}
+
+</script>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.js
new file mode 100644
index 0000000..fd754f8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.js
@@ -0,0 +1,10 @@
+addEventListener('fetch', e => {
+ if (e.request.url.includes('get-worker-client-url')) {
+ e.respondWith((async () => {
+ const clients = await self.clients.matchAll({type: 'worker'});
+ if (clients.length != 1)
+ return new Response('one worker client should exist');
+ return new Response(clients[0].url);
+ })());
+ }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-frame-freeze.html b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-frame-freeze.html
new file mode 100644
index 0000000..7468a66
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-frame-freeze.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+ document.addEventListener('freeze', () => {
+ opener.postMessage('frozen', "*");
+ });
+
+ window.onmessage = (e) => {
+ if (e.data == 'freeze') {
+ test_driver.freeze();
+ }
+ };
+ opener.postMessage('loaded', '*');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-frame-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-frame-worker.js
new file mode 100644
index 0000000..0a1461b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-frame-worker.js
@@ -0,0 +1,11 @@
+onmessage = function(e) {
+ if (e.data.cmd == 'GetClientId') {
+ fetch('clientId')
+ .then(function(response) {
+ return response.text();
+ })
+ .then(function(text) {
+ e.data.port.postMessage({clientId: text});
+ });
+ }
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-frame.html
new file mode 100644
index 0000000..4324e6d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-frame.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<script>
+fetch('clientId')
+ .then(function(response) {
+ return response.text();
+ })
+ .then(function(text) {
+ parent.postMessage({clientId: text}, '*');
+ });
+
+onmessage = function(e) {
+ if (e.data == 'StartWorker') {
+ var w = new Worker('clients-get-client-types-frame-worker.js');
+ w.postMessage({cmd:'GetClientId', port:e.ports[0]}, [e.ports[0]]);
+ }
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-shared-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-shared-worker.js
new file mode 100644
index 0000000..fadef97
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-shared-worker.js
@@ -0,0 +1,10 @@
+onconnect = function(e) {
+ var port = e.ports[0];
+ fetch('clientId')
+ .then(function(response) {
+ return response.text();
+ })
+ .then(function(text) {
+ port.postMessage({clientId: text});
+ });
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-worker.js
new file mode 100644
index 0000000..0a1461b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-client-types-worker.js
@@ -0,0 +1,11 @@
+onmessage = function(e) {
+ if (e.data.cmd == 'GetClientId') {
+ fetch('clientId')
+ .then(function(response) {
+ return response.text();
+ })
+ .then(function(text) {
+ e.data.port.postMessage({clientId: text});
+ });
+ }
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-cross-origin-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-cross-origin-frame.html
new file mode 100644
index 0000000..e16bb11
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-cross-origin-frame.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js"></script>
+<script>
+var host_info = get_host_info();
+var scope = 'blank.html?clients-get';
+var script = 'clients-get-worker.js';
+
+var registration;
+var worker;
+var wait_for_worker_promise = navigator.serviceWorker.getRegistration(scope)
+ .then(function(reg) {
+ if (reg)
+ return reg.unregister();
+ })
+ .then(function() {
+ return navigator.serviceWorker.register(script, {scope: scope});
+ })
+ .then(function(reg) {
+ registration = reg;
+ worker = reg.installing;
+ return new Promise(function(resolve) {
+ worker.addEventListener('statechange', function() {
+ if (worker.state == 'activated')
+ resolve();
+ });
+ });
+ });
+
+window.addEventListener('message', function(e) {
+ var cross_origin_client_ids = [];
+ cross_origin_client_ids.push(e.data.clientId);
+ wait_for_worker_promise
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(iframe) {
+ add_completion_callback(function() { iframe.remove(); });
+ navigator.serviceWorker.onmessage = function(e) {
+ registration.unregister();
+ window.parent.postMessage(
+ { type: 'clientId', value: e.data }, host_info['HTTPS_ORIGIN']
+ );
+ };
+ registration.active.postMessage({clientIds: cross_origin_client_ids});
+ });
+});
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-frame.html
new file mode 100644
index 0000000..27143d4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-frame.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<script>
+
+ fetch("clientId")
+ .then(function(response) {
+ return response.text();
+ })
+ .then(function(text) {
+ parent.postMessage({clientId: text}, "*");
+ });
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-other-origin.html b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-other-origin.html
new file mode 100644
index 0000000..6342fe0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-other-origin.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js"></script>
+<script>
+var host_info = get_host_info();
+var SCOPE = 'blank.html?clients-get';
+var SCRIPT = 'clients-get-worker.js';
+
+var registration;
+var worker;
+var wait_for_worker_promise = navigator.serviceWorker.getRegistration(SCOPE)
+ .then(function(reg) {
+ if (reg)
+ return reg.unregister();
+ })
+ .then(function() {
+ return navigator.serviceWorker.register(SCRIPT, {scope: SCOPE});
+ })
+ .then(function(reg) {
+ registration = reg;
+ worker = reg.installing;
+ return new Promise(function(resolve) {
+ worker.addEventListener('statechange', function() {
+ if (worker.state == 'activated')
+ resolve();
+ });
+ });
+ });
+
+function send_result(result) {
+ window.parent.postMessage(
+ {result: result},
+ host_info['HTTPS_ORIGIN']);
+}
+
+window.addEventListener("message", on_message, false);
+
+function on_message(e) {
+ if (e.origin != host_info['HTTPS_ORIGIN']) {
+ console.error('invalid origin: ' + e.origin);
+ return;
+ }
+ if (e.data.message == 'get_client_id') {
+ var otherOriginClientId = e.data.clientId;
+ wait_for_worker_promise
+ .then(function() {
+ return with_iframe(SCOPE);
+ })
+ .then(function(iframe) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = function(e) {
+ navigator.serviceWorker.getRegistration(SCOPE)
+ .then(function(reg) {
+ reg.unregister();
+ send_result(e.data);
+ });
+ };
+ iframe.contentWindow.navigator.serviceWorker.controller.postMessage(
+ {port:channel.port2, clientId: otherOriginClientId,
+ message: 'get_other_client_id'}, [channel.port2]);
+ })
+ }
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js
new file mode 100644
index 0000000..5a46ff9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js
@@ -0,0 +1,60 @@
+let savedPort = null;
+let savedResultingClientId = null;
+
+async function getTestingPage() {
+ const clientList = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
+ for (let c of clientList) {
+ if (c.url.endsWith('clients-get.https.html')) {
+ c.focus();
+ return c;
+ }
+ }
+ return null;
+}
+
+async function destroyResultingClient(testingPage) {
+ const destroyedPromise = new Promise(resolve => {
+ self.addEventListener('message', e => {
+ if (e.data.msg == 'resultingClientDestroyed') {
+ resolve();
+ }
+ }, {once: true});
+ });
+ testingPage.postMessage({ msg: 'destroyResultingClient' });
+ return destroyedPromise;
+}
+
+self.addEventListener('fetch', async (e) => {
+ let { resultingClientId } = e;
+ savedResultingClientId = resultingClientId;
+
+ if (e.request.url.endsWith('simple.html?fail')) {
+ e.waitUntil((async () => {
+ const testingPage = await getTestingPage();
+ await destroyResultingClient(testingPage);
+ testingPage.postMessage({ msg: 'resultingClientDestroyedAck',
+ resultingDestroyedClientId: savedResultingClientId });
+ })());
+ return;
+ }
+
+ e.respondWith(fetch(e.request));
+});
+
+self.addEventListener('message', (e) => {
+ let { msg, resultingClientId } = e.data;
+ e.waitUntil((async () => {
+ if (msg == 'getIsResultingClientUndefined') {
+ const client = await self.clients.get(resultingClientId);
+ let isUndefined = typeof client == 'undefined';
+ e.source.postMessage({ msg: 'getIsResultingClientUndefined',
+ isResultingClientUndefined: isUndefined });
+ return;
+ }
+ if (msg == 'getResultingClientId') {
+ e.source.postMessage({ msg: 'getResultingClientId',
+ resultingClientId: savedResultingClientId });
+ return;
+ }
+ })());
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-worker.js
new file mode 100644
index 0000000..8effa56
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-get-worker.js
@@ -0,0 +1,41 @@
+// This worker is designed to expose information about clients that is only available from Service Worker contexts.
+//
+// In the case of the `onfetch` handler, it provides the `clientId` property of
+// the `event` object. In the case of the `onmessage` handler, it provides the
+// Client instance attributes of the requested clients.
+self.onfetch = function(e) {
+ if (/\/clientId$/.test(e.request.url)) {
+ e.respondWith(new Response(e.clientId));
+ return;
+ }
+};
+
+self.onmessage = function(e) {
+ var client_ids = e.data.clientIds;
+ var message = [];
+
+ e.waitUntil(Promise.all(
+ client_ids.map(function(client_id) {
+ return self.clients.get(client_id);
+ }))
+ .then(function(clients) {
+ // No matching client for a given id or a matched client is off-origin
+ // from the service worker.
+ if (clients.length == 1 && clients[0] == undefined) {
+ e.source.postMessage(clients[0]);
+ } else {
+ clients.forEach(function(client) {
+ if (client instanceof Client) {
+ message.push([client.visibilityState,
+ client.focused,
+ client.url,
+ client.type,
+ client.frameType]);
+ } else {
+ message.push(client);
+ }
+ });
+ e.source.postMessage(message);
+ }
+ }));
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-blob-url-worker.html b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-blob-url-worker.html
new file mode 100644
index 0000000..ee89a0d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-blob-url-worker.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<script>
+const workerScript = `
+ self.onmessage = (e) => {
+ self.postMessage("Worker is ready.");
+ };
+`;
+const blob = new Blob([workerScript], { type: 'text/javascript' });
+const blobUrl = URL.createObjectURL(blob);
+const worker = new Worker(blobUrl);
+
+function waitForWorker() {
+ return new Promise(resolve => {
+ worker.onmessage = resolve;
+ worker.postMessage("Ping to worker.");
+ });
+}
+</script>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-client-types-dedicated-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-client-types-dedicated-worker.js
new file mode 100644
index 0000000..5a3f04d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-client-types-dedicated-worker.js
@@ -0,0 +1,3 @@
+onmessage = function(e) {
+ postMessage(e.data);
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-client-types-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-client-types-iframe.html
new file mode 100644
index 0000000..7607b03
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-client-types-iframe.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<title>Empty doc</title>
+<!--
+ Change the page URL using the History API to ensure that ServiceWorkerClient
+ uses the creation URL.
+-->
+<body onload="history.pushState({}, 'title', 'url-modified-via-pushstate.html')">
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-client-types-shared-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-client-types-shared-worker.js
new file mode 100644
index 0000000..1ae72fb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-client-types-shared-worker.js
@@ -0,0 +1,4 @@
+onconnect = function(e) {
+ var port = e.ports[0];
+ port.postMessage('started');
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-on-evaluation-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-on-evaluation-worker.js
new file mode 100644
index 0000000..f1559ac
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-on-evaluation-worker.js
@@ -0,0 +1,11 @@
+importScripts('test-helpers.sub.js');
+
+var page_url = normalizeURL('../clients-matchall-on-evaluation.https.html');
+
+self.clients.matchAll({includeUncontrolled: true})
+ .then(function(clients) {
+ clients.forEach(function(client) {
+ if (client.url == page_url)
+ client.postMessage('matched');
+ });
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-worker.js
new file mode 100644
index 0000000..13e111a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/clients-matchall-worker.js
@@ -0,0 +1,40 @@
+self.onmessage = function(e) {
+ var port = e.data.port;
+ var options = e.data.options;
+
+ e.waitUntil(self.clients.matchAll(options)
+ .then(function(clients) {
+ var message = [];
+ clients.forEach(function(client) {
+ var frame_type = client.frameType;
+ if (client.url.indexOf('clients-matchall-include-uncontrolled.https.html') > -1 &&
+ client.frameType == 'auxiliary') {
+ // The test tab might be opened using window.open() by the test framework.
+ // In that case, just pretend it's top-level!
+ frame_type = 'top-level';
+ }
+ if (e.data.includeLifecycleState) {
+ message.push({visibilityState: client.visibilityState,
+ focused: client.focused,
+ url: client.url,
+ lifecycleState: client.lifecycleState,
+ type: client.type,
+ frameType: frame_type});
+ } else {
+ message.push([client.visibilityState,
+ client.focused,
+ client.url,
+ client.type,
+ frame_type]);
+ }
+ });
+ // Sort by url
+ if (!e.data.disableSort) {
+ message.sort(function(a, b) { return a[2] > b[2] ? 1 : -1; });
+ }
+ port.postMessage(message);
+ })
+ .catch(e => {
+ port.postMessage('clients.matchAll() rejected: ' + e);
+ }));
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/cors-approved.txt b/third_party/web_platform_tests/service-workers/service-worker/resources/cors-approved.txt
new file mode 100644
index 0000000..1cd89bb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/cors-approved.txt
@@ -0,0 +1 @@
+plaintext
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/cors-approved.txt.headers b/third_party/web_platform_tests/service-workers/service-worker/resources/cors-approved.txt.headers
new file mode 100644
index 0000000..f7985fd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/cors-approved.txt.headers
@@ -0,0 +1,3 @@
+Content-Type: text/plain
+Access-Control-Allow-Origin: *
+
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/cors-denied.txt b/third_party/web_platform_tests/service-workers/service-worker/resources/cors-denied.txt
new file mode 100644
index 0000000..ff333bd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/cors-denied.txt
@@ -0,0 +1,2 @@
+this file is served without Access-Control-Allow-Origin headers so it should not
+be readable from cross-origin.
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/create-blob-url-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/create-blob-url-worker.js
new file mode 100644
index 0000000..57e4882
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/create-blob-url-worker.js
@@ -0,0 +1,22 @@
+const childWorkerScript = `
+ self.onmessage = async (e) => {
+ const response = await fetch(e.data);
+ const text = await response.text();
+ self.postMessage(text);
+ };
+`;
+const blob = new Blob([childWorkerScript], { type: 'text/javascript' });
+const blobUrl = URL.createObjectURL(blob);
+const childWorker = new Worker(blobUrl);
+
+// When a message comes from the parent frame, sends a resource url to the child
+// worker.
+self.onmessage = (e) => {
+ childWorker.postMessage(e.data);
+};
+
+// When a message comes from the child worker, sends a content of fetch() to the
+// parent frame.
+childWorker.onmessage = (e) => {
+ self.postMessage(e.data);
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/create-out-of-scope-worker.html b/third_party/web_platform_tests/service-workers/service-worker/resources/create-out-of-scope-worker.html
new file mode 100644
index 0000000..b51c451
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/create-out-of-scope-worker.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<script>
+const workerUrl = '../out-of-scope/sample-synthesized-worker.js?dedicated';
+const worker = new Worker(workerUrl);
+const workerPromise = new Promise(resolve => {
+ worker.onmessage = e => {
+ // `e.data` is 'worker loading intercepted by service worker' when a worker
+ // is intercepted by a service worker.
+ resolve(e.data);
+ }
+ worker.onerror = _ => {
+ resolve('worker loading was not intercepted by service worker');
+ }
+});
+
+function getWorkerPromise() {
+ return workerPromise;
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/echo-content.py b/third_party/web_platform_tests/service-workers/service-worker/resources/echo-content.py
new file mode 100644
index 0000000..70ae4b6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/echo-content.py
@@ -0,0 +1,16 @@
+# This is a copy of fetch/api/resources/echo-content.py since it's more
+# convenient in this directory due to service worker's path restriction.
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+
+ headers = [(b"X-Request-Method", isomorphic_encode(request.method)),
+ (b"X-Request-Content-Length", request.headers.get(b"Content-Length", b"NO")),
+ (b"X-Request-Content-Type", request.headers.get(b"Content-Type", b"NO")),
+
+ # Avoid any kind of content sniffing on the response.
+ (b"Content-Type", b"text/plain")]
+
+ content = request.body
+
+ return headers, content
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/echo-cookie-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/echo-cookie-worker.py
new file mode 100644
index 0000000..561f64a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/echo-cookie-worker.py
@@ -0,0 +1,24 @@
+def main(request, response):
+ headers = [(b"Content-Type", b"text/javascript")]
+
+ values = []
+ for key in request.cookies:
+ for cookie in request.cookies.get_list(key):
+ values.append(b'"%s": "%s"' % (key, cookie.value))
+
+ # Update the counter to change the script body for every request to trigger
+ # update of the service worker.
+ key = request.GET[b'key']
+ counter = request.server.stash.take(key)
+ if counter is None:
+ counter = 0
+ counter += 1
+ request.server.stash.put(key, counter)
+
+ body = b"""
+// %d
+self.addEventListener('message', e => {
+ e.source.postMessage({%s})
+});""" % (counter, b','.join(values))
+
+ return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/echo-message-to-source-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/echo-message-to-source-worker.js
new file mode 100644
index 0000000..bbbd35f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/echo-message-to-source-worker.js
@@ -0,0 +1,3 @@
+addEventListener('message', evt => {
+ evt.source.postMessage(evt.data);
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/embed-and-object-are-not-intercepted-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/embed-and-object-are-not-intercepted-worker.js
new file mode 100644
index 0000000..ffcdb75
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/embed-and-object-are-not-intercepted-worker.js
@@ -0,0 +1,14 @@
+// This worker intercepts a request for EMBED/OBJECT and responds with a
+// response that indicates that interception occurred. The tests expect
+// that interception does not occur.
+self.addEventListener('fetch', e => {
+ if (e.request.url.indexOf('embedded-content-from-server.html') != -1) {
+ e.respondWith(fetch('embedded-content-from-service-worker.html'));
+ return;
+ }
+
+ if (e.request.url.indexOf('green.png') != -1) {
+ e.respondWith(Promise.reject('network error to show interception occurred'));
+ return;
+ }
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/embed-image-is-not-intercepted-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/embed-image-is-not-intercepted-iframe.html
new file mode 100644
index 0000000..7b8b257
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/embed-image-is-not-intercepted-iframe.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe for embed-and-object-are-not-intercepted test</title>
+<body>
+<embed type="image/png" src="/images/green.png"></embed>
+<script>
+// Our parent (the root frame of the test) will examine this to get the result.
+var test_promise = new Promise(resolve => {
+ if (!navigator.serviceWorker.controller)
+ resolve('FAIL: this iframe is not controlled');
+
+ const elem = document.querySelector('embed');
+ elem.addEventListener('load', e => {
+ resolve('request was not intercepted');
+ });
+ elem.addEventListener('error', e => {
+ resolve('FAIL: request was intercepted');
+ });
+ });
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html
new file mode 100644
index 0000000..3914991
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe for embed-and-object-are-not-intercepted test</title>
+<body>
+<script>
+// The EMBED element will call this with the result about whether the EMBED
+// request was intercepted by the service worker.
+var report_result;
+
+// Our parent (the root frame of the test) will examine this to get the result.
+var test_promise = new Promise(resolve => {
+ report_result = resolve;
+ });
+</script>
+
+<embed src="embedded-content-from-server.html"></embed>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/embed-navigation-is-not-intercepted-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/embed-navigation-is-not-intercepted-iframe.html
new file mode 100644
index 0000000..5e86f67
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/embed-navigation-is-not-intercepted-iframe.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe for embed-and-object-are-not-intercepted test</title>
+<body>
+<script>
+// The EMBED element will call this with the result about whether the EMBED
+// request was intercepted by the service worker.
+var report_result;
+
+// Our parent (the root frame of the test) will examine this to get the result.
+var test_promise = new Promise(resolve => {
+ report_result = resolve;
+ });
+
+let el = document.createElement('embed');
+el.src = "/common/blank.html";
+el.addEventListener('load', _ => {
+ window[0].location = "/service-workers/service-worker/resources/embedded-content-from-server.html";
+}, { once: true });
+document.body.appendChild(el);
+</script>
+
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/embedded-content-from-server.html b/third_party/web_platform_tests/service-workers/service-worker/resources/embedded-content-from-server.html
new file mode 100644
index 0000000..ff50a9c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/embedded-content-from-server.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>embed for embed-and-object-are-not-intercepted test</title>
+<script>
+window.parent.report_result('request for embedded content was not intercepted');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/embedded-content-from-service-worker.html b/third_party/web_platform_tests/service-workers/service-worker/resources/embedded-content-from-service-worker.html
new file mode 100644
index 0000000..2e2b923
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/embedded-content-from-service-worker.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>embed for embed-and-object-are-not-intercepted test</title>
+<script>
+window.parent.report_result('request for embedded content was intercepted by service worker');
+</script>
+
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/empty-but-slow-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/empty-but-slow-worker.js
new file mode 100644
index 0000000..92abac7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/empty-but-slow-worker.js
@@ -0,0 +1,8 @@
+addEventListener('fetch', evt => {
+ if (evt.request.url.endsWith('slow')) {
+ // Performance.now() might be a bit better here, but Date.now() has
+ // better compat in workers right now.
+ let start = Date.now();
+ while(Date.now() - start < 2000);
+ }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/empty-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/empty-worker.js
new file mode 100644
index 0000000..49ceb26
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/empty-worker.js
@@ -0,0 +1 @@
+// Do nothing.
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/empty.h2.js b/third_party/web_platform_tests/service-workers/service-worker/resources/empty.h2.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/empty.h2.js
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/empty.html b/third_party/web_platform_tests/service-workers/service-worker/resources/empty.html
new file mode 100644
index 0000000..6feb119
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/empty.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<html>
+<body>
+hello world
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/empty.js b/third_party/web_platform_tests/service-workers/service-worker/resources/empty.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/empty.js
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/enable-client-message-queue.html b/third_party/web_platform_tests/service-workers/service-worker/resources/enable-client-message-queue.html
new file mode 100644
index 0000000..512bd14
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/enable-client-message-queue.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<script>
+ // The state variable is used by handle_message to record the time
+ // at which a message was handled. It's updated by the scripts
+ // loaded by the <script> tags at the bottom of the file as well as
+ // by the event listener added here.
+ var state = 'init';
+ addEventListener('DOMContentLoaded', () => state = 'loaded');
+
+ // We expect to get three ping messages from the service worker.
+ const expected = ['init', 'install', 'start'];
+ let promises = {};
+ let resolvers = {};
+ expected.forEach(name => {
+ promises[name] = new Promise(resolve => resolvers[name] = resolve);
+ });
+
+ // Once all messages have been dispatched, the state in which each
+ // of them was dispatched is recorded in the draft. At that point
+ // the draft becomes the final report.
+ var draft = {};
+ var report = Promise.all(Object.values(promises)).then(() => window.draft);
+
+ // This message handler is installed by the 'install' script.
+ function handle_message(event) {
+ const data = event.data.data;
+ draft[data] = state;
+ resolvers[data]();
+ }
+</script>
+
+<!--
+ The controlling service worker will delay the response to these
+ fetch requests until the test instructs it how to reply. Note that
+ the event loop keeps spinning while the parser is blocked.
+-->
+<script src="empty.js?key=install"></script>
+<script src="empty.js?key=start"></script>
+<script src="empty.js?key=finish"></script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/end-to-end-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/end-to-end-worker.js
new file mode 100644
index 0000000..d45a505
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/end-to-end-worker.js
@@ -0,0 +1,7 @@
+onmessage = function(e) {
+ var message = e.data;
+ if (typeof message === 'object' && 'port' in message) {
+ var response = 'Ack for: ' + message.from;
+ message.port.postMessage(response);
+ }
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/events-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/events-worker.js
new file mode 100644
index 0000000..80a2188
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/events-worker.js
@@ -0,0 +1,12 @@
+var eventsSeen = [];
+
+function handler(event) { eventsSeen.push(event.type); }
+
+['activate', 'install'].forEach(function(type) {
+ self.addEventListener(type, handler);
+ });
+
+onmessage = function(e) {
+ var message = e.data;
+ message.port.postMessage({events: eventsSeen});
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/extendable-event-async-waituntil.js b/third_party/web_platform_tests/service-workers/service-worker/resources/extendable-event-async-waituntil.js
new file mode 100644
index 0000000..8a975b0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/extendable-event-async-waituntil.js
@@ -0,0 +1,210 @@
+// This worker calls waitUntil() and respondWith() asynchronously and
+// reports back to the test whether they threw.
+//
+// These test cases are confusing. Bear in mind that the event is active
+// (calling waitUntil() is allowed) if:
+// * The pending promise count is not 0, or
+// * The event dispatch flag is set.
+
+// Controlled by 'init'/'done' messages.
+var resolveLockPromise;
+var port;
+
+self.addEventListener('message', function(event) {
+ var waitPromise;
+ var resolveTestPromise;
+
+ switch (event.data.step) {
+ case 'init':
+ event.waitUntil(new Promise((res) => { resolveLockPromise = res; }));
+ port = event.data.port;
+ break;
+ case 'done':
+ resolveLockPromise();
+ break;
+
+ // Throws because waitUntil() is called in a task after event dispatch
+ // finishes.
+ case 'no-current-extension-different-task':
+ async_task_waituntil(event).then(reportResultExpecting('InvalidStateError'));
+ break;
+
+ // OK because waitUntil() is called in a microtask that runs after the
+ // event handler runs, while the event dispatch flag is still set.
+ case 'no-current-extension-different-microtask':
+ async_microtask_waituntil(event).then(reportResultExpecting('OK'));
+ break;
+
+ // OK because the second waitUntil() is called while the first waitUntil()
+ // promise is still pending.
+ case 'current-extension-different-task':
+ event.waitUntil(new Promise((res) => { resolveTestPromise = res; }));
+ async_task_waituntil(event).then(reportResultExpecting('OK')).then(resolveTestPromise);
+ break;
+
+ // OK because all promises involved resolve "immediately", so the second
+ // waitUntil() is called during the microtask checkpoint at the end of
+ // event dispatching, when the event dispatch flag is still set.
+ case 'during-event-dispatch-current-extension-expired-same-microtask-turn':
+ waitPromise = Promise.resolve();
+ event.waitUntil(waitPromise);
+ waitPromise.then(() => { return sync_waituntil(event); })
+ .then(reportResultExpecting('OK'))
+ break;
+
+ // OK for the same reason as above.
+ case 'during-event-dispatch-current-extension-expired-same-microtask-turn-extra':
+ waitPromise = Promise.resolve();
+ event.waitUntil(waitPromise);
+ waitPromise.then(() => { return async_microtask_waituntil(event); })
+ .then(reportResultExpecting('OK'))
+ break;
+
+
+ // OK because the pending promise count is decremented in a microtask
+ // queued upon fulfillment of the first waitUntil() promise, so the second
+ // waitUntil() is called while the pending promise count is still
+ // positive.
+ case 'after-event-dispatch-current-extension-expired-same-microtask-turn':
+ waitPromise = makeNewTaskPromise();
+ event.waitUntil(waitPromise);
+ waitPromise.then(() => { return sync_waituntil(event); })
+ .then(reportResultExpecting('OK'))
+ break;
+
+ // Throws because the second waitUntil() is called after the pending
+ // promise count was decremented to 0.
+ case 'after-event-dispatch-current-extension-expired-same-microtask-turn-extra':
+ waitPromise = makeNewTaskPromise();
+ event.waitUntil(waitPromise);
+ waitPromise.then(() => { return async_microtask_waituntil(event); })
+ .then(reportResultExpecting('InvalidStateError'))
+ break;
+
+ // Throws because the second waitUntil() is called in a new task, after
+ // first waitUntil() promise settled and the event dispatch flag is unset.
+ case 'current-extension-expired-different-task':
+ event.waitUntil(Promise.resolve());
+ async_task_waituntil(event).then(reportResultExpecting('InvalidStateError'));
+ break;
+
+ case 'script-extendable-event':
+ self.dispatchEvent(new ExtendableEvent('nontrustedevent'));
+ break;
+ }
+
+ event.source.postMessage('ACK');
+ });
+
+self.addEventListener('fetch', function(event) {
+ const path = new URL(event.request.url).pathname;
+ const step = path.substring(path.lastIndexOf('/') + 1);
+ let response;
+ switch (step) {
+ // OK because waitUntil() is called while the respondWith() promise is still
+ // unsettled, so the pending promise count is positive.
+ case 'pending-respondwith-async-waituntil':
+ var resolveFetch;
+ response = new Promise((res) => { resolveFetch = res; });
+ event.respondWith(response);
+ async_task_waituntil(event)
+ .then(reportResultExpecting('OK'))
+ .then(() => { resolveFetch(new Response('OK')); });
+ break;
+
+ // OK because all promises involved resolve "immediately", so waitUntil() is
+ // called during the microtask checkpoint at the end of event dispatching,
+ // when the event dispatch flag is still set.
+ case 'during-event-dispatch-respondwith-microtask-sync-waituntil':
+ response = Promise.resolve(new Response('RESP'));
+ event.respondWith(response);
+ response.then(() => { return sync_waituntil(event); })
+ .then(reportResultExpecting('OK'));
+ break;
+
+ // OK because all promises involved resolve "immediately", so waitUntil() is
+ // called during the microtask checkpoint at the end of event dispatching,
+ // when the event dispatch flag is still set.
+ case 'during-event-dispatch-respondwith-microtask-async-waituntil':
+ response = Promise.resolve(new Response('RESP'));
+ event.respondWith(response);
+ response.then(() => { return async_microtask_waituntil(event); })
+ .then(reportResultExpecting('OK'));
+ break;
+
+ // OK because the pending promise count is decremented in a microtask queued
+ // upon fulfillment of the respondWith() promise, so waitUntil() is called
+ // while the pending promise count is still positive.
+ case 'after-event-dispatch-respondwith-microtask-sync-waituntil':
+ response = makeNewTaskPromise().then(() => {return new Response('RESP');});
+ event.respondWith(response);
+ response.then(() => { return sync_waituntil(event); })
+ .then(reportResultExpecting('OK'));
+ break;
+
+
+ // Throws because waitUntil() is called after the pending promise count was
+ // decremented to 0.
+ case 'after-event-dispatch-respondwith-microtask-async-waituntil':
+ response = makeNewTaskPromise().then(() => {return new Response('RESP');});
+ event.respondWith(response);
+ response.then(() => { return async_microtask_waituntil(event); })
+ .then(reportResultExpecting('InvalidStateError'))
+ break;
+ }
+});
+
+self.addEventListener('nontrustedevent', function(event) {
+ sync_waituntil(event).then(reportResultExpecting('InvalidStateError'));
+ });
+
+function reportResultExpecting(expectedResult) {
+ return function (result) {
+ port.postMessage({result : result, expected: expectedResult});
+ return result;
+ };
+}
+
+function sync_waituntil(event) {
+ return new Promise((res, rej) => {
+ try {
+ event.waitUntil(Promise.resolve());
+ res('OK');
+ } catch (error) {
+ res(error.name);
+ }
+ });
+}
+
+function async_microtask_waituntil(event) {
+ return new Promise((res, rej) => {
+ Promise.resolve().then(() => {
+ try {
+ event.waitUntil(Promise.resolve());
+ res('OK');
+ } catch (error) {
+ res(error.name);
+ }
+ });
+ });
+}
+
+function async_task_waituntil(event) {
+ return new Promise((res, rej) => {
+ setTimeout(() => {
+ try {
+ event.waitUntil(Promise.resolve());
+ res('OK');
+ } catch (error) {
+ res(error.name);
+ }
+ }, 0);
+ });
+}
+
+// Returns a promise that settles in a separate task.
+function makeNewTaskPromise() {
+ return new Promise(resolve => {
+ setTimeout(resolve, 0);
+ });
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/extendable-event-waituntil.js b/third_party/web_platform_tests/service-workers/service-worker/resources/extendable-event-waituntil.js
new file mode 100644
index 0000000..20a9eb0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/extendable-event-waituntil.js
@@ -0,0 +1,87 @@
+var pendingPorts = [];
+var portResolves = [];
+
+onmessage = function(e) {
+ var message = e.data;
+ if ('port' in message) {
+ var resolve = self.portResolves.shift();
+ if (resolve)
+ resolve(message.port);
+ else
+ self.pendingPorts.push(message.port);
+ }
+};
+
+function fulfillPromise() {
+ return new Promise(function(resolve) {
+ // Make sure the oninstall/onactivate callback returns first.
+ Promise.resolve().then(function() {
+ var port = self.pendingPorts.shift();
+ if (port)
+ resolve(port);
+ else
+ self.portResolves.push(resolve);
+ });
+ }).then(function(port) {
+ port.postMessage('SYNC');
+ return new Promise(function(resolve) {
+ port.onmessage = function(e) {
+ if (e.data == 'ACK')
+ resolve();
+ };
+ });
+ });
+}
+
+function rejectPromise() {
+ return new Promise(function(resolve, reject) {
+ // Make sure the oninstall/onactivate callback returns first.
+ Promise.resolve().then(reject);
+ });
+}
+
+function stripScopeName(url) {
+ return url.split('/').slice(-1)[0];
+}
+
+oninstall = function(e) {
+ switch (stripScopeName(self.location.href)) {
+ case 'install-fulfilled':
+ e.waitUntil(fulfillPromise());
+ break;
+ case 'install-rejected':
+ e.waitUntil(rejectPromise());
+ break;
+ case 'install-multiple-fulfilled':
+ e.waitUntil(fulfillPromise());
+ e.waitUntil(fulfillPromise());
+ break;
+ case 'install-reject-precedence':
+ // Three "extend lifetime promises" are needed to verify that the user
+ // agent waits for all promises to settle even in the event of rejection.
+ // The first promise is fulfilled on demand by the client, the second is
+ // immediately scheduled for rejection, and the third is fulfilled on
+ // demand by the client (but only after the first promise has been
+ // fulfilled).
+ //
+ // User agents which simply expose `Promise.all` semantics in this case
+ // (by entering the "redundant state" following the rejection of the
+ // second promise but prior to the fulfillment of the third) can be
+ // identified from the client context.
+ e.waitUntil(fulfillPromise());
+ e.waitUntil(rejectPromise());
+ e.waitUntil(fulfillPromise());
+ break;
+ }
+};
+
+onactivate = function(e) {
+ switch (stripScopeName(self.location.href)) {
+ case 'activate-fulfilled':
+ e.waitUntil(fulfillPromise());
+ break;
+ case 'activate-rejected':
+ e.waitUntil(rejectPromise());
+ break;
+ }
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fail-on-fetch-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fail-on-fetch-worker.js
new file mode 100644
index 0000000..517f289
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fail-on-fetch-worker.js
@@ -0,0 +1,5 @@
+importScripts('worker-testharness.js');
+
+this.addEventListener('fetch', function(event) {
+ event.respondWith(new Response('ERROR'));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-access-control-login.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-access-control-login.html
new file mode 100644
index 0000000..ee29680
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-access-control-login.html
@@ -0,0 +1,16 @@
+<script>
+// Set authentication info
+window.addEventListener("message", function(evt) {
+ var port = evt.ports[0];
+ document.cookie = 'cookie=' + evt.data.cookie;
+ var xhr = new XMLHttpRequest();
+ xhr.addEventListener('load', function() {
+ port.postMessage({msg: 'LOGIN FINISHED'});
+ }, false);
+ xhr.open('GET',
+ './fetch-access-control.py?Auth',
+ true,
+ evt.data.username, evt.data.password);
+ xhr.send();
+ }, false);
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-access-control.py b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-access-control.py
new file mode 100644
index 0000000..446af87
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-access-control.py
@@ -0,0 +1,109 @@
+import json
+import os
+from base64 import decodebytes
+
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+def main(request, response):
+ headers = []
+ headers.append((b'X-ServiceWorker-ServerHeader', b'SetInTheServer'))
+
+ if b"ACAOrigin" in request.GET:
+ for item in request.GET[b"ACAOrigin"].split(b","):
+ headers.append((b"Access-Control-Allow-Origin", item))
+
+ for suffix in [b"Headers", b"Methods", b"Credentials"]:
+ query = b"ACA%s" % suffix
+ header = b"Access-Control-Allow-%s" % suffix
+ if query in request.GET:
+ headers.append((header, request.GET[query]))
+
+ if b"ACEHeaders" in request.GET:
+ headers.append((b"Access-Control-Expose-Headers", request.GET[b"ACEHeaders"]))
+
+ if (b"Auth" in request.GET and not request.auth.username) or b"AuthFail" in request.GET:
+ status = 401
+ headers.append((b'WWW-Authenticate', b'Basic realm="Restricted"'))
+ body = b'Authentication canceled'
+ return status, headers, body
+
+ if b"PNGIMAGE" in request.GET:
+ headers.append((b"Content-Type", b"image/png"))
+ body = decodebytes(b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1B"
+ b"AACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAhSURBVDhPY3wro/KfgQLABKXJBqMG"
+ b"jBoAAqMGDLwBDAwAEsoCTFWunmQAAAAASUVORK5CYII=")
+ return headers, body
+
+ if b"VIDEO" in request.GET:
+ headers.append((b"Content-Type", b"video/ogg"))
+ body = open(os.path.join(request.doc_root, u"media", u"movie_5.ogv"), "rb").read()
+ length = len(body)
+ # If "PartialContent" is specified, the requestor wants to test range
+ # requests. For the initial request, respond with "206 Partial Content"
+ # and don't send the entire content. Then expect subsequent requests to
+ # have a "Range" header with a byte range. Respond with that range.
+ if b"PartialContent" in request.GET:
+ if length < 1:
+ return 500, headers, b"file is too small for range requests"
+ start = 0
+ end = length - 1
+ if b"Range" in request.headers:
+ range_header = request.headers[b"Range"]
+ prefix = b"bytes="
+ split_header = range_header[len(prefix):].split(b"-")
+ # The first request might be "bytes=0-". We want to force a range
+ # request, so just return the first byte.
+ if split_header[0] == b"0" and split_header[1] == b"":
+ end = start
+ # Otherwise, it is a range request. Respect the values sent.
+ if split_header[0] != b"":
+ start = int(split_header[0])
+ if split_header[1] != b"":
+ end = int(split_header[1])
+ else:
+ # The request doesn't have a range. Force a range request by
+ # returning the first byte.
+ end = start
+
+ headers.append((b"Accept-Ranges", b"bytes"))
+ headers.append((b"Content-Length", isomorphic_encode(str(end -start + 1))))
+ headers.append((b"Content-Range", b"bytes %d-%d/%d" % (start, end, length)))
+ chunk = body[start:(end + 1)]
+ return 206, headers, chunk
+ return headers, body
+
+ username = request.auth.username if request.auth.username else b"undefined"
+ password = request.auth.password if request.auth.username else b"undefined"
+ cookie = request.cookies[b'cookie'].value if b'cookie' in request.cookies else b"undefined"
+
+ files = []
+ for key, values in request.POST.items():
+ assert len(values) == 1
+ value = values[0]
+ if not hasattr(value, u"file"):
+ continue
+ data = value.file.read()
+ files.append({u"key": isomorphic_decode(key),
+ u"name": value.file.name,
+ u"type": value.type,
+ u"error": 0, #TODO,
+ u"size": len(data),
+ u"content": data})
+
+ get_data = {isomorphic_decode(key):isomorphic_decode(request.GET[key]) for key, value in request.GET.items()}
+ post_data = {isomorphic_decode(key):isomorphic_decode(request.POST[key]) for key, value in request.POST.items()
+ if not hasattr(request.POST[key], u"file")}
+ headers_data = {isomorphic_decode(key):isomorphic_decode(request.headers[key]) for key, value in request.headers.items()}
+
+ data = {u"jsonpResult": u"success",
+ u"method": request.method,
+ u"headers": headers_data,
+ u"body": isomorphic_decode(request.body),
+ u"files": files,
+ u"GET": get_data,
+ u"POST": post_data,
+ u"username": isomorphic_decode(username),
+ u"password": isomorphic_decode(password),
+ u"cookie": isomorphic_decode(cookie)}
+
+ return headers, u"report( %s )" % json.dumps(data)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-canvas-tainting-double-write-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-canvas-tainting-double-write-worker.js
new file mode 100644
index 0000000..17723dc
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-canvas-tainting-double-write-worker.js
@@ -0,0 +1,7 @@
+self.addEventListener('fetch', (event) => {
+ url = new URL(event.request.url);
+ if (url.search == '?PNGIMAGE') {
+ localUrl = new URL(url.pathname + url.search, self.location);
+ event.respondWith(fetch(localUrl));
+ }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html
new file mode 100644
index 0000000..75d766c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html
@@ -0,0 +1,70 @@
+<html>
+<title>iframe for fetch canvas tainting test</title>
+<script>
+const NOT_TAINTED = 'NOT_TAINTED';
+const TAINTED = 'TAINTED';
+const LOAD_ERROR = 'LOAD_ERROR';
+
+// Creates an image/video element with src=|url| and an optional |cross_origin|
+// attibute. Tries to read from the image/video using a canvas element. Returns
+// NOT_TAINTED if it could be read, TAINTED if it could not be read, and
+// LOAD_ERROR if loading the image/video failed.
+function create_test_case_promise(url, cross_origin) {
+ return new Promise(resolve => {
+ if (url.indexOf('PNGIMAGE') != -1) {
+ const img = document.createElement('img');
+ if (cross_origin != '') {
+ img.crossOrigin = cross_origin;
+ }
+ img.onload = function() {
+ try {
+ const canvas = document.createElement('canvas');
+ canvas.width = 100;
+ canvas.height = 100;
+ const context = canvas.getContext('2d');
+ context.drawImage(img, 0, 0);
+ context.getImageData(0, 0, 100, 100);
+ resolve(NOT_TAINTED);
+ } catch (e) {
+ resolve(TAINTED);
+ }
+ };
+ img.onerror = function() {
+ resolve(LOAD_ERROR);
+ }
+ img.src = url;
+ return;
+ }
+
+ if (url.indexOf('VIDEO') != -1) {
+ const video = document.createElement('video');
+ video.autoplay = true;
+ video.muted = true;
+ if (cross_origin != '') {
+ video.crossOrigin = cross_origin;
+ }
+ video.onplay = function() {
+ try {
+ const canvas = document.createElement('canvas');
+ canvas.width = 100;
+ canvas.height = 100;
+ const context = canvas.getContext('2d');
+ context.drawImage(video, 0, 0);
+ context.getImageData(0, 0, 100, 100);
+ resolve(NOT_TAINTED);
+ } catch (e) {
+ resolve(TAINTED);
+ }
+ };
+ video.onerror = function() {
+ resolve(LOAD_ERROR);
+ }
+ video.src = url;
+ return;
+ }
+
+ resolve('unknown resource type');
+ });
+}
+</script>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-canvas-tainting-tests.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-canvas-tainting-tests.js
new file mode 100644
index 0000000..2aada36
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-canvas-tainting-tests.js
@@ -0,0 +1,241 @@
+// This is the main driver of the canvas tainting tests.
+const NOT_TAINTED = 'NOT_TAINTED';
+const TAINTED = 'TAINTED';
+const LOAD_ERROR = 'LOAD_ERROR';
+
+let frame;
+
+// Creates a single promise_test.
+function canvas_taint_test(url, cross_origin, expected_result) {
+ promise_test(t => {
+ return frame.contentWindow.create_test_case_promise(url, cross_origin)
+ .then(result => {
+ assert_equals(result, expected_result);
+ });
+ }, 'url "' + url + '" with crossOrigin "' + cross_origin + '" should be ' +
+ expected_result);
+}
+
+
+// Runs all the tests. The given |params| has these properties:
+// * |resource_path|: the relative path to the (image/video) resource to test.
+// * |cache|: when true, the service worker bounces responses into
+// Cache Storage and back out before responding with them.
+function do_canvas_tainting_tests(params) {
+ const host_info = get_host_info();
+ let resource_path = params.resource_path;
+ if (params.cache)
+ resource_path += "&cache=true";
+ const resource_url = host_info['HTTPS_ORIGIN'] + resource_path;
+ const remote_resource_url = host_info['HTTPS_REMOTE_ORIGIN'] + resource_path;
+
+ // Set up the service worker and the frame.
+ promise_test(function(t) {
+ const SCOPE = 'resources/fetch-canvas-tainting-iframe.html';
+ const SCRIPT = 'resources/fetch-rewrite-worker.js';
+ const host_info = get_host_info();
+
+ // login_https() is needed because some test cases use credentials.
+ return login_https(t)
+ .then(function() {
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ })
+ .then(function(registration) {
+ promise_test(() => {
+ if (frame)
+ frame.remove();
+ return registration.unregister();
+ }, 'restore global state');
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(SCOPE); })
+ .then(f => {
+ frame = f;
+ });
+ }, 'initialize global state');
+
+ // Reject tests. Add '&reject' so the service worker responds with a rejected promise.
+ // A load error is expected.
+ canvas_taint_test(resource_url + '&reject', '', LOAD_ERROR);
+ canvas_taint_test(resource_url + '&reject', 'anonymous', LOAD_ERROR);
+ canvas_taint_test(resource_url + '&reject', 'use-credentials', LOAD_ERROR);
+
+ // Fallback tests. Add '&ignore' so the service worker does not respond to the fetch
+ // request, and we fall back to network.
+ canvas_taint_test(resource_url + '&ignore', '', NOT_TAINTED);
+ canvas_taint_test(remote_resource_url + '&ignore', '', TAINTED);
+ canvas_taint_test(remote_resource_url + '&ignore', 'anonymous', LOAD_ERROR);
+ canvas_taint_test(
+ remote_resource_url + '&ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+ '&ignore',
+ 'anonymous',
+ NOT_TAINTED);
+ canvas_taint_test(remote_resource_url + '&ignore', 'use-credentials', LOAD_ERROR);
+ canvas_taint_test(
+ remote_resource_url + '&ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+ '&ignore',
+ 'use-credentials',
+ LOAD_ERROR);
+ canvas_taint_test(
+ remote_resource_url + '&ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+ '&ACACredentials=true&ignore',
+ 'use-credentials',
+ NOT_TAINTED);
+
+ // Credential tests (with fallback). Add '&Auth' so the server requires authentication.
+ // Furthermore, add '&ignore' so the service worker falls back to network.
+ canvas_taint_test(resource_url + '&Auth&ignore', '', NOT_TAINTED);
+ canvas_taint_test(remote_resource_url + '&Auth&ignore', '', TAINTED);
+ canvas_taint_test(
+ remote_resource_url + '&Auth&ignore', 'anonymous', LOAD_ERROR);
+ canvas_taint_test(
+ remote_resource_url + '&Auth&ignore',
+ 'use-credentials',
+ LOAD_ERROR);
+ canvas_taint_test(
+ remote_resource_url + '&Auth&ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+ '&ignore',
+ 'use-credentials',
+ LOAD_ERROR);
+ canvas_taint_test(
+ remote_resource_url + '&Auth&ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+ '&ACACredentials=true&ignore',
+ 'use-credentials',
+ NOT_TAINTED);
+
+ // In the following tests, the service worker provides a response.
+ // Add '&url' so the service worker responds with fetch(url).
+ // Add '&mode' to configure the fetch request options.
+
+ // Basic response tests. Set &url to the original url.
+ canvas_taint_test(
+ resource_url + '&mode=same-origin&url=' + encodeURIComponent(resource_url),
+ '',
+ NOT_TAINTED);
+ canvas_taint_test(
+ resource_url + '&mode=same-origin&url=' + encodeURIComponent(resource_url),
+ 'anonymous',
+ NOT_TAINTED);
+ canvas_taint_test(
+ resource_url + '&mode=same-origin&url=' + encodeURIComponent(resource_url),
+ 'use-credentials',
+ NOT_TAINTED);
+ canvas_taint_test(
+ remote_resource_url + '&mode=same-origin&url=' +
+ encodeURIComponent(resource_url),
+ '',
+ NOT_TAINTED);
+ canvas_taint_test(
+ remote_resource_url + '&mode=same-origin&url=' +
+ encodeURIComponent(resource_url),
+ 'anonymous',
+ NOT_TAINTED);
+ canvas_taint_test(
+ remote_resource_url + '&mode=same-origin&url=' +
+ encodeURIComponent(resource_url),
+ 'use-credentials',
+ NOT_TAINTED);
+
+ // Opaque response tests. Set &url to the cross-origin URL, and &mode to
+ // 'no-cors' so we expect an opaque response.
+ canvas_taint_test(
+ resource_url +
+ '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url),
+ '',
+ TAINTED);
+ canvas_taint_test(
+ resource_url +
+ '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url),
+ 'anonymous',
+ LOAD_ERROR);
+ canvas_taint_test(
+ resource_url +
+ '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url),
+ 'use-credentials',
+ LOAD_ERROR);
+ canvas_taint_test(
+ remote_resource_url +
+ '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url),
+ '',
+ TAINTED);
+ canvas_taint_test(
+ remote_resource_url +
+ '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url),
+ 'anonymous',
+ LOAD_ERROR);
+ canvas_taint_test(
+ remote_resource_url +
+ '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url),
+ 'use-credentials',
+ LOAD_ERROR);
+
+ // CORS response tests. Set &url to the cross-origin URL, and &mode
+ // to 'cors' to attempt a CORS request.
+ canvas_taint_test(
+ resource_url + '&mode=cors&url=' +
+ encodeURIComponent(remote_resource_url +
+ '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+ '',
+ LOAD_ERROR); // We expect LOAD_ERROR since the server doesn't respond
+ // with an Access-Control-Allow-Credentials header.
+ canvas_taint_test(
+ resource_url + '&mode=cors&credentials=same-origin&url=' +
+ encodeURIComponent(remote_resource_url +
+ '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+ '',
+ NOT_TAINTED);
+ canvas_taint_test(
+ resource_url + '&mode=cors&url=' +
+ encodeURIComponent(remote_resource_url +
+ '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+ 'anonymous',
+ NOT_TAINTED);
+ canvas_taint_test(
+ resource_url + '&mode=cors&url=' +
+ encodeURIComponent(remote_resource_url +
+ '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+ 'use-credentials',
+ LOAD_ERROR); // We expect LOAD_ERROR since the server doesn't respond
+ // with an Access-Control-Allow-Credentials header.
+ canvas_taint_test(
+ resource_url + '&mode=cors&url=' +
+ encodeURIComponent(
+ remote_resource_url +
+ '&ACACredentials=true&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+ 'use-credentials',
+ NOT_TAINTED);
+ canvas_taint_test(
+ remote_resource_url + '&mode=cors&url=' +
+ encodeURIComponent(remote_resource_url +
+ '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+ '',
+ LOAD_ERROR); // We expect LOAD_ERROR since the server doesn't respond
+ // with an Access-Control-Allow-Credentials header.
+ canvas_taint_test(
+ remote_resource_url + '&mode=cors&credentials=same-origin&url=' +
+ encodeURIComponent(remote_resource_url +
+ '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+ '',
+ NOT_TAINTED);
+ canvas_taint_test(
+ remote_resource_url + '&mode=cors&url=' +
+ encodeURIComponent(remote_resource_url +
+ '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+ 'anonymous',
+ NOT_TAINTED);
+ canvas_taint_test(
+ remote_resource_url + '&mode=cors&url=' +
+ encodeURIComponent(remote_resource_url +
+ '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+ 'use-credentials',
+ LOAD_ERROR); // We expect LOAD_ERROR since the server doesn't respond
+ // with an Access-Control-Allow-Credentials header.
+ canvas_taint_test(
+ remote_resource_url + '&mode=cors&url=' +
+ encodeURIComponent(
+ remote_resource_url +
+ '&ACACredentials=true&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
+ 'use-credentials',
+ NOT_TAINTED);
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-cors-exposed-header-names-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-cors-exposed-header-names-worker.js
new file mode 100644
index 0000000..145952a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-cors-exposed-header-names-worker.js
@@ -0,0 +1,3 @@
+self.addEventListener('fetch', (e) => {
+ e.respondWith(fetch(e.request));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-cors-xhr-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-cors-xhr-iframe.html
new file mode 100644
index 0000000..d88c510
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-cors-xhr-iframe.html
@@ -0,0 +1,170 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js?pipe=sub"></script>
+<script>
+var path = base_path() + 'fetch-access-control.py';
+var host_info = get_host_info();
+var SUCCESS = 'SUCCESS';
+var FAIL = 'FAIL';
+
+function create_test_case_promise(url, with_credentials) {
+ return new Promise(function(resolve) {
+ var xhr = new XMLHttpRequest();
+ xhr.onload = function() {
+ if (xhr.status == 200) {
+ resolve(SUCCESS);
+ } else {
+ resolve("STATUS" + xhr.status);
+ }
+ }
+ xhr.onerror = function() {
+ resolve(FAIL);
+ }
+ xhr.responseType = 'text';
+ xhr.withCredentials = with_credentials;
+ xhr.open('GET', url, true);
+ xhr.send();
+ });
+}
+
+window.addEventListener('message', async (evt) => {
+ var port = evt.ports[0];
+ var url = host_info['HTTPS_ORIGIN'] + path;
+ var remote_url = host_info['HTTPS_REMOTE_ORIGIN'] + path;
+ var TEST_CASES = [
+ // Reject tests
+ [url + '?reject', false, FAIL],
+ [url + '?reject', true, FAIL],
+ [remote_url + '?reject', false, FAIL],
+ [remote_url + '?reject', true, FAIL],
+ // Event handler exception tests
+ [url + '?throw', false, SUCCESS],
+ [url + '?throw', true, SUCCESS],
+ [remote_url + '?throw', false, FAIL],
+ [remote_url + '?throw', true, FAIL],
+ // Reject(resolve-null) tests
+ [url + '?resolve-null', false, FAIL],
+ [url + '?resolve-null', true, FAIL],
+ [remote_url + '?resolve-null', false, FAIL],
+ [remote_url + '?resolve-null', true, FAIL],
+ // Fallback tests
+ [url + '?ignore', false, SUCCESS],
+ [url + '?ignore', true, SUCCESS],
+ [remote_url + '?ignore', false, FAIL, true], // Executed in serial.
+ [remote_url + '?ignore', true, FAIL, true], // Executed in serial.
+ [
+ remote_url + '?ACAOrigin=' + host_info['HTTPS_ORIGIN'] + '&ignore',
+ false, SUCCESS
+ ],
+ [
+ remote_url + '?ACAOrigin=' + host_info['HTTPS_ORIGIN'] + '&ignore',
+ true, FAIL, true // Executed in serial.
+ ],
+ [
+ remote_url + '?ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+ '&ACACredentials=true&ignore',
+ true, SUCCESS
+ ],
+ // Credential test (fallback)
+ [url + '?Auth&ignore', false, SUCCESS],
+ [url + '?Auth&ignore', true, SUCCESS],
+ [remote_url + '?Auth&ignore', false, FAIL],
+ [remote_url + '?Auth&ignore', true, FAIL],
+ [
+ remote_url + '?Auth&ACAOrigin=' + host_info['HTTPS_ORIGIN'] + '&ignore',
+ false, 'STATUS401'
+ ],
+ [
+ remote_url + '?Auth&ACAOrigin=' + host_info['HTTPS_ORIGIN'] + '&ignore',
+ true, FAIL, true // Executed in serial.
+ ],
+ [
+ remote_url + '?Auth&ACAOrigin=' + host_info['HTTPS_ORIGIN'] +
+ '&ACACredentials=true&ignore',
+ true, SUCCESS
+ ],
+ // Basic response
+ [
+ url + '?mode=same-origin&url=' + encodeURIComponent(url),
+ false, SUCCESS
+ ],
+ [
+ url + '?mode=same-origin&url=' + encodeURIComponent(url),
+ false, SUCCESS
+ ],
+ [
+ remote_url + '?mode=same-origin&url=' + encodeURIComponent(url),
+ false, SUCCESS
+ ],
+ [
+ remote_url + '?mode=same-origin&url=' + encodeURIComponent(url),
+ false, SUCCESS
+ ],
+ // Opaque response
+ [
+ url + '?mode=no-cors&url=' + encodeURIComponent(remote_url),
+ false, FAIL
+ ],
+ [
+ url + '?mode=no-cors&url=' + encodeURIComponent(remote_url),
+ false, FAIL
+ ],
+ [
+ remote_url + '?mode=no-cors&url=' + encodeURIComponent(remote_url),
+ false, FAIL
+ ],
+ [
+ remote_url + '?mode=no-cors&url=' + encodeURIComponent(remote_url),
+ false, FAIL
+ ],
+ // CORS response
+ [
+ url + '?mode=cors&url=' +
+ encodeURIComponent(remote_url + '?ACAOrigin=' +
+ host_info['HTTPS_ORIGIN']),
+ false, SUCCESS
+ ],
+ [
+ url + '?mode=cors&url=' +
+ encodeURIComponent(remote_url + '?ACAOrigin=' +
+ host_info['HTTPS_ORIGIN']),
+ true, FAIL
+ ],
+ [
+ url + '?mode=cors&url=' +
+ encodeURIComponent(remote_url + '?ACAOrigin=' +
+ host_info['HTTPS_ORIGIN'] +
+ '&ACACredentials=true'),
+ true, SUCCESS
+ ],
+ [
+ remote_url + '?mode=cors&url=' +
+ encodeURIComponent(remote_url + '?ACAOrigin=' +
+ host_info['HTTPS_ORIGIN']),
+ false, SUCCESS
+ ],
+ [
+ remote_url +
+ '?mode=cors&url=' +
+ encodeURIComponent(remote_url + '?ACAOrigin=' +
+ host_info['HTTPS_ORIGIN']),
+ true, FAIL
+ ],
+ [
+ remote_url +
+ '?mode=cors&url=' +
+ encodeURIComponent(remote_url + '?ACAOrigin=' +
+ host_info['HTTPS_ORIGIN'] +
+ '&ACACredentials=true'),
+ true, SUCCESS
+ ]
+ ];
+
+ let counter = 0;
+ for (let test of TEST_CASES) {
+ let result = await create_test_case_promise(test[0], test[1]);
+ let testName = 'test ' + (++counter) + ': ' + test[0] + ' with credentials ' + test[1] + ' must be ' + test[2];
+ port.postMessage({testName: testName, result: result === test[2]});
+ }
+ port.postMessage('done');
+ }, false);
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-csp-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-csp-iframe.html
new file mode 100644
index 0000000..33bf041
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-csp-iframe.html
@@ -0,0 +1,16 @@
+<script>
+var meta = document.createElement('meta');
+meta.setAttribute('http-equiv', 'Content-Security-Policy');
+meta.setAttribute('content', decodeURIComponent(location.search.substring(1)));
+document.head.appendChild(meta);
+
+function load_image(url) {
+ return new Promise(function(resolve, reject) {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = resolve;
+ img.onerror = reject;
+ img.src = url;
+ });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-csp-iframe.html.sub.headers b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-csp-iframe.html.sub.headers
new file mode 100644
index 0000000..5a1c7b9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-csp-iframe.html.sub.headers
@@ -0,0 +1 @@
+Content-Security-Policy: img-src https://{{host}}:{{ports[https][0]}}; connect-src 'unsafe-inline' 'self'
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-error-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-error-worker.js
new file mode 100644
index 0000000..788252c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-error-worker.js
@@ -0,0 +1,22 @@
+importScripts("/resources/testharness.js");
+
+function doTest(event)
+{
+ if (!event.request.url.includes("fetch-error-test"))
+ return;
+
+ let counter = 0;
+ const stream = new ReadableStream({ pull: controller => {
+ switch (++counter) {
+ case 1:
+ controller.enqueue(new Uint8Array([1]));
+ return;
+ default:
+ // We asynchronously error the stream so that there is ample time to resolve the fetch promise and call text() on the response.
+ step_timeout(() => controller.error("Sorry"), 50);
+ }
+ }});
+ event.respondWith(new Response(stream));
+}
+
+self.addEventListener("fetch", doTest);
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-add-async-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-add-async-worker.js
new file mode 100644
index 0000000..a5a44a5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-add-async-worker.js
@@ -0,0 +1,6 @@
+importScripts('/resources/testharness.js');
+
+promise_test(async () => {
+ await new Promise(handler => { step_timeout(handler, 0); });
+ self.addEventListener('fetch', () => {});
+}, 'fetch event added asynchronously does not throw');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-after-navigation-within-page-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-after-navigation-within-page-iframe.html
new file mode 100644
index 0000000..bf8a6d5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-after-navigation-within-page-iframe.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<script>
+function fetch_url(url) {
+ return new Promise(function(resolve, reject) {
+ var request = new XMLHttpRequest();
+ request.addEventListener('load', function(event) {
+ if (request.status == 200)
+ resolve(request.response);
+ else
+ reject(new Error('fetch_url: ' + request.statusText + " : " + url));
+ });
+ request.addEventListener('error', function(event) {
+ reject(new Error('fetch_url encountered an error: ' + url));
+ });
+ request.addEventListener('abort', function(event) {
+ reject(new Error('fetch_url was aborted: ' + url));
+ });
+ request.open('GET', url);
+ request.send();
+ });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js
new file mode 100644
index 0000000..dc3f1a1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js
@@ -0,0 +1,66 @@
+// This worker attempts to call respondWith() asynchronously after the
+// fetch event handler finished. It reports back to the test whether
+// an exception was thrown.
+
+// These get reset at the start of a test case.
+let reportResult;
+
+// The test page sends a message to tell us that a new test case is starting.
+// We expect a fetch event after this.
+self.addEventListener('message', (event) => {
+ // Ensure tests run mutually exclusive.
+ if (reportResult) {
+ event.source.postMessage('testAlreadyRunning');
+ return;
+ }
+
+ const resultPromise = new Promise((resolve) => {
+ reportResult = resolve;
+ // Tell the client that everything is initialized and that it's safe to
+ // proceed with the test without relying on the order of events (which some
+ // browsers like Chrome may not guarantee).
+ event.source.postMessage('messageHandlerInitialized');
+ });
+
+ // Keep the worker alive until the test case finishes, and report
+ // back the result to the test page.
+ event.waitUntil(resultPromise.then(result => {
+ reportResult = null;
+ event.source.postMessage(result);
+ }));
+});
+
+// Calls respondWith() and reports back whether an exception occurred.
+function tryRespondWith(event) {
+ try {
+ event.respondWith(new Response());
+ reportResult({didThrow: false});
+ } catch (error) {
+ reportResult({didThrow: true, error: error.name});
+ }
+}
+
+function respondWithInTask(event) {
+ setTimeout(() => {
+ tryRespondWith(event);
+ }, 0);
+}
+
+function respondWithInMicrotask(event) {
+ Promise.resolve().then(() => {
+ tryRespondWith(event);
+ });
+}
+
+self.addEventListener('fetch', function(event) {
+ const path = new URL(event.request.url).pathname;
+ const test = path.substring(path.lastIndexOf('/') + 1);
+
+ // If this is a test case, try respondWith() and report back to the test page
+ // the result.
+ if (test == 'respondWith-in-task') {
+ respondWithInTask(event);
+ } else if (test == 'respondWith-in-microtask') {
+ respondWithInMicrotask(event);
+ }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-handled-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-handled-worker.js
new file mode 100644
index 0000000..53ee149
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-handled-worker.js
@@ -0,0 +1,37 @@
+// This worker reports back the final state of FetchEvent.handled (RESOLVED or
+// REJECTED) to the test.
+
+self.addEventListener('message', function(event) {
+ self.port = event.data.port;
+});
+
+self.addEventListener('fetch', function(event) {
+ try {
+ event.handled.then(() => {
+ self.port.postMessage('RESOLVED');
+ }, () => {
+ self.port.postMessage('REJECTED');
+ });
+ } catch (e) {
+ self.port.postMessage('FAILED');
+ return;
+ }
+
+ const search = new URL(event.request.url).search;
+ switch (search) {
+ case '?respondWith-not-called':
+ break;
+ case '?respondWith-not-called-and-event-canceled':
+ event.preventDefault();
+ break;
+ case '?respondWith-called-and-promise-resolved':
+ event.respondWith(Promise.resolve(new Response('body')));
+ break;
+ case '?respondWith-called-and-promise-resolved-to-invalid-response':
+ event.respondWith(Promise.resolve('invalid response'));
+ break;
+ case '?respondWith-called-and-promise-rejected':
+ event.respondWith(Promise.reject(new Error('respondWith rejected')));
+ break;
+ }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-network-error-controllee-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-network-error-controllee-iframe.html
new file mode 100644
index 0000000..f6c1919
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-network-error-controllee-iframe.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<script>
+function fetch_url(url) {
+ return new Promise(function(resolve, reject) {
+ var request = new XMLHttpRequest();
+ request.addEventListener('load', function(event) {
+ resolve();
+ });
+ request.addEventListener('error', function(event) {
+ reject();
+ });
+ request.open('GET', url);
+ request.send();
+ });
+}
+
+function make_test(testcase) {
+ var name = testcase.name;
+ return fetch_url(window.location.href + '?' + name)
+ .then(
+ function() {
+ if (testcase.expect_load)
+ return Promise.resolve();
+ return Promise.reject(new Error(
+ name + ': expected network error but loaded'));
+ },
+ function() {
+ if (!testcase.expect_load)
+ return Promise.resolve();
+ return Promise.reject(new Error(
+ name + ': expected to load but got network error'));
+ });
+}
+
+function run_tests() {
+ var tests = [
+ { name: 'prevent-default-and-respond-with', expect_load: true },
+ { name: 'prevent-default', expect_load: false },
+ { name: 'reject', expect_load: false },
+ { name: 'unused-body', expect_load: true },
+ { name: 'used-body', expect_load: false },
+ { name: 'unused-fetched-body', expect_load: true },
+ { name: 'used-fetched-body', expect_load: false },
+ { name: 'throw-exception', expect_load: true },
+ ].map(make_test);
+
+ Promise.all(tests)
+ .then(function() {
+ window.parent.notify_test_done('PASS');
+ })
+ .catch(function(error) {
+ window.parent.notify_test_done('FAIL: ' + error.message);
+ });
+}
+
+if (!navigator.serviceWorker.controller)
+ window.parent.notify_test_done('FAIL: no controller');
+else
+ run_tests();
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-network-error-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-network-error-worker.js
new file mode 100644
index 0000000..5bfe3a0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-network-error-worker.js
@@ -0,0 +1,49 @@
+// Test that multiple fetch handlers do not confuse the implementation.
+self.addEventListener('fetch', function(event) {});
+
+self.addEventListener('fetch', function(event) {
+ var testcase = new URL(event.request.url).search;
+ switch (testcase) {
+ case '?reject':
+ event.respondWith(Promise.reject());
+ break;
+ case '?prevent-default':
+ event.preventDefault();
+ break;
+ case '?prevent-default-and-respond-with':
+ event.preventDefault();
+ break;
+ case '?unused-body':
+ event.respondWith(new Response('body'));
+ break;
+ case '?used-body':
+ var res = new Response('body');
+ res.text();
+ event.respondWith(res);
+ break;
+ case '?unused-fetched-body':
+ event.respondWith(fetch('other.html').then(function(res){
+ return res;
+ }));
+ break;
+ case '?used-fetched-body':
+ event.respondWith(fetch('other.html').then(function(res){
+ res.text();
+ return res;
+ }));
+ break;
+ case '?throw-exception':
+ throw('boom');
+ break;
+ }
+ });
+
+self.addEventListener('fetch', function(event) {});
+
+self.addEventListener('fetch', function(event) {
+ var testcase = new URL(event.request.url).search;
+ if (testcase == '?prevent-default-and-respond-with')
+ event.respondWith(new Response('responding!'));
+ });
+
+self.addEventListener('fetch', function(event) {});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-network-fallback-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-network-fallback-worker.js
new file mode 100644
index 0000000..376bdbe
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-network-fallback-worker.js
@@ -0,0 +1,3 @@
+self.addEventListener('fetch', () => {
+ // Do nothing.
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-iframe.html
new file mode 100644
index 0000000..0ebd1ca
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-iframe.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<script>
+function fetch_url(url) {
+ return new Promise(function(resolve, reject) {
+ var request = new XMLHttpRequest();
+ request.addEventListener('load', function(event) {
+ resolve();
+ });
+ request.addEventListener('error', function(event) {
+ reject();
+ });
+ request.open('GET', url);
+ request.send();
+ });
+}
+
+function make_test(testcase) {
+ var name = testcase.name;
+ return fetch_url(window.location.href + '?' + name)
+ .then(
+ function() {
+ if (testcase.expect_load)
+ return Promise.resolve();
+ return Promise.reject(new Error(
+ name + ': expected network error but loaded'));
+ },
+ function() {
+ if (!testcase.expect_load)
+ return Promise.resolve();
+ return Promise.reject(new Error(
+ name + ': expected to load but got network error'));
+ });
+}
+
+function run_tests() {
+ var tests = [
+ { name: 'response-object', expect_load: true },
+ { name: 'response-promise-object', expect_load: true },
+ { name: 'other-value', expect_load: false },
+ ].map(make_test);
+
+ Promise.all(tests)
+ .then(function() {
+ window.parent.notify_test_done('PASS');
+ })
+ .catch(function(error) {
+ window.parent.notify_test_done('FAIL: ' + error.message);
+ });
+}
+
+if (!navigator.serviceWorker.controller)
+ window.parent.notify_test_done('FAIL: no controller');
+else
+ run_tests();
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-worker.js
new file mode 100644
index 0000000..712c4b7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-worker.js
@@ -0,0 +1,14 @@
+self.addEventListener('fetch', function(event) {
+ var testcase = new URL(event.request.url).search;
+ switch (testcase) {
+ case '?response-object':
+ event.respondWith(new Response('body'));
+ break;
+ case '?response-promise-object':
+ event.respondWith(Promise.resolve(new Response('body')));
+ break;
+ case '?other-value':
+ event.respondWith(new Object());
+ break;
+ }
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js
new file mode 100644
index 0000000..d3ba8a8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js
@@ -0,0 +1,7 @@
+'use strict';
+
+self.addEventListener('fetch', event => {
+ if (!event.request.url.match(/body-in-chunk$/))
+ return;
+ event.respondWith(fetch("../../../fetch/api/resources/trickle.py?count=4&delay=50"));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-custom-response-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-custom-response-worker.js
new file mode 100644
index 0000000..ff24aed
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-custom-response-worker.js
@@ -0,0 +1,45 @@
+'use strict';
+
+addEventListener('fetch', event => {
+ const url = new URL(event.request.url);
+ const type = url.searchParams.get('type');
+
+ if (!type) return;
+
+ if (type === 'string') {
+ event.respondWith(new Response('PASS'));
+ }
+ else if (type === 'blob') {
+ event.respondWith(
+ new Response(new Blob(['PASS']))
+ );
+ }
+ else if (type === 'buffer-view') {
+ const encoder = new TextEncoder();
+ event.respondWith(
+ new Response(encoder.encode('PASS'))
+ );
+ }
+ else if (type === 'buffer') {
+ const encoder = new TextEncoder();
+ event.respondWith(
+ new Response(encoder.encode('PASS').buffer)
+ );
+ }
+ else if (type === 'form-data') {
+ const body = new FormData();
+ body.set('result', 'PASS');
+ event.respondWith(
+ new Response(body)
+ );
+ }
+ else if (type === 'search-params') {
+ const body = new URLSearchParams();
+ body.set('result', 'PASS');
+ event.respondWith(
+ new Response(body, {
+ headers: { 'Content-Type': 'text/plain' }
+ })
+ );
+ }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-partial-stream-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-partial-stream-worker.js
new file mode 100644
index 0000000..b7307f2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-partial-stream-worker.js
@@ -0,0 +1,28 @@
+let waitUntilResolve;
+
+let bodyController;
+
+self.addEventListener('message', evt => {
+ if (evt.data === 'done') {
+ bodyController.close();
+ waitUntilResolve();
+ }
+});
+
+self.addEventListener('fetch', evt => {
+ if (!evt.request.url.includes('partial-stream.txt')) {
+ return;
+ }
+
+ evt.waitUntil(new Promise(resolve => waitUntilResolve = resolve));
+
+ let body = new ReadableStream({
+ start: controller => {
+ let encoder = new TextEncoder();
+ controller.enqueue(encoder.encode('partial-stream-content'));
+ bodyController = controller;
+ },
+ });
+
+ evt.respondWith(new Response(body));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-chunk-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-chunk-worker.js
new file mode 100644
index 0000000..f954e3a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-chunk-worker.js
@@ -0,0 +1,40 @@
+'use strict';
+
+self.addEventListener('fetch', event => {
+ if (!event.request.url.match(/body-stream$/))
+ return;
+
+ var counter = 0;
+ const encoder = new TextEncoder();
+ const stream = new ReadableStream({ pull: controller => {
+ switch (++counter) {
+ case 1:
+ controller.enqueue(encoder.encode(''));
+ return;
+ case 2:
+ controller.enqueue(encoder.encode('chunk #1'));
+ return;
+ case 3:
+ controller.enqueue(encoder.encode(' '));
+ return;
+ case 4:
+ controller.enqueue(encoder.encode('chunk #2'));
+ return;
+ case 5:
+ controller.enqueue(encoder.encode(' '));
+ return;
+ case 6:
+ controller.enqueue(encoder.encode('chunk #3'));
+ return;
+ case 7:
+ controller.enqueue(encoder.encode(' '));
+ return;
+ case 8:
+ controller.enqueue(encoder.encode('chunk #4'));
+ return;
+ default:
+ controller.close();
+ }
+ }});
+ event.respondWith(new Response(stream));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-worker.js
new file mode 100644
index 0000000..e54cb6d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-worker.js
@@ -0,0 +1,75 @@
+'use strict';
+importScripts("/resources/testharness.js");
+
+const map = new Map();
+
+self.addEventListener('fetch', event => {
+ const url = new URL(event.request.url);
+ if (!url.searchParams.has('stream')) return;
+
+ if (url.searchParams.has('observe-cancel')) {
+ const id = url.searchParams.get('id');
+ if (id === undefined) {
+ event.respondWith(new Error('error'));
+ return;
+ }
+ event.waitUntil(new Promise(resolve => {
+ map.set(id, {label: 'pending', resolve});
+ }));
+
+ const stream = new ReadableStream({
+ cancel() {
+ map.get(id).label = 'cancelled';
+ }
+ });
+ event.respondWith(new Response(stream));
+ return;
+ }
+
+ if (url.searchParams.has('query-cancel')) {
+ const id = url.searchParams.get('id');
+ if (id === undefined) {
+ event.respondWith(new Error('error'));
+ return;
+ }
+ const entry = map.get(id);
+ if (entry === undefined) {
+ event.respondWith(new Error('not found'));
+ return;
+ }
+ map.delete(id);
+ entry.resolve();
+ event.respondWith(new Response(entry.label));
+ return;
+ }
+
+ if (url.searchParams.has('use-fetch-stream')) {
+ event.respondWith(async function() {
+ const response = await fetch('pass.txt');
+ return new Response(response.body);
+ }());
+ return;
+ }
+
+ const delayEnqueue = url.searchParams.has('delay');
+
+ const stream = new ReadableStream({
+ start(controller) {
+ const encoder = new TextEncoder();
+
+ const populate = () => {
+ controller.enqueue(encoder.encode('PASS'));
+ controller.close();
+ }
+
+ if (delayEnqueue) {
+ step_timeout(populate, 16);
+ }
+ else {
+ populate();
+ }
+ }
+ });
+
+ event.respondWith(new Response(stream));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html
new file mode 100644
index 0000000..d15454d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>respond-with-response-body-with-invalid-chunk</title>
+<body></body>
+<script>
+'use strict';
+
+parent.set_fetch_promise(fetch('body-stream-with-invalid-chunk').then(resp => {
+ const reader = resp.body.getReader();
+ const reader_promise = reader.read();
+ parent.set_reader_promise(reader_promise);
+ // Suppress our expected error.
+ return reader_promise.catch(() => {});
+ }));
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js
new file mode 100644
index 0000000..0254e24
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js
@@ -0,0 +1,12 @@
+'use strict';
+
+self.addEventListener('fetch', event => {
+ if (!event.request.url.match(/body-stream-with-invalid-chunk$/))
+ return;
+ const stream = new ReadableStream({start: controller => {
+ // The argument is intentionally a string, not a Uint8Array.
+ controller.enqueue('hello');
+ }});
+ const headers = { 'x-content-type-options': 'nosniff' };
+ event.respondWith(new Response(stream, { headers }));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-stops-propagation-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-stops-propagation-worker.js
new file mode 100644
index 0000000..18da049
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-respond-with-stops-propagation-worker.js
@@ -0,0 +1,15 @@
+var result = null;
+
+self.addEventListener('message', function(event) {
+ event.data.port.postMessage(result);
+ });
+
+self.addEventListener('fetch', function(event) {
+ if (!result)
+ result = 'PASS';
+ event.respondWith(new Response());
+ });
+
+self.addEventListener('fetch', function(event) {
+ result = 'FAIL: fetch event propagated';
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-test-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-test-worker.js
new file mode 100644
index 0000000..813f79d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-test-worker.js
@@ -0,0 +1,224 @@
+function handleHeaders(event) {
+ const headers = Array.from(event.request.headers);
+ event.respondWith(new Response(JSON.stringify(headers)));
+}
+
+function handleString(event) {
+ event.respondWith(new Response('Test string'));
+}
+
+function handleBlob(event) {
+ event.respondWith(new Response(new Blob(['Test blob'])));
+}
+
+function handleReferrer(event) {
+ event.respondWith(new Response(new Blob(
+ ['Referrer: ' + event.request.referrer])));
+}
+
+function handleReferrerPolicy(event) {
+ event.respondWith(new Response(new Blob(
+ ['ReferrerPolicy: ' + event.request.referrerPolicy])));
+}
+
+function handleReferrerFull(event) {
+ event.respondWith(new Response(new Blob(
+ ['Referrer: ' + event.request.referrer + '\n' +
+ 'ReferrerPolicy: ' + event.request.referrerPolicy])));
+}
+
+function handleClientId(event) {
+ var body;
+ if (event.clientId !== "") {
+ body = 'Client ID Found: ' + event.clientId;
+ } else {
+ body = 'Client ID Not Found';
+ }
+ event.respondWith(new Response(body));
+}
+
+function handleResultingClientId(event) {
+ var body;
+ if (event.resultingClientId !== "") {
+ body = 'Resulting Client ID Found: ' + event.resultingClientId;
+ } else {
+ body = 'Resulting Client ID Not Found';
+ }
+ event.respondWith(new Response(body));
+}
+
+function handleNullBody(event) {
+ event.respondWith(new Response());
+}
+
+function handleFetch(event) {
+ event.respondWith(fetch('other.html'));
+}
+
+function handleFormPost(event) {
+ event.respondWith(new Promise(function(resolve) {
+ event.request.text()
+ .then(function(result) {
+ resolve(new Response(event.request.method + ':' +
+ event.request.headers.get('Content-Type') + ':' +
+ result));
+ });
+ }));
+}
+
+function handleMultipleRespondWith(event) {
+ var logForMultipleRespondWith = '';
+ for (var i = 0; i < 3; ++i) {
+ logForMultipleRespondWith += '(' + i + ')';
+ try {
+ event.respondWith(new Promise(function(resolve) {
+ setTimeout(function() {
+ resolve(new Response(logForMultipleRespondWith));
+ }, 0);
+ }));
+ } catch (e) {
+ logForMultipleRespondWith += '[' + e.name + ']';
+ }
+ }
+}
+
+var lastResponseForUsedCheck = undefined;
+
+function handleUsedCheck(event) {
+ if (!lastResponseForUsedCheck) {
+ event.respondWith(fetch('other.html').then(function(response) {
+ lastResponseForUsedCheck = response;
+ return response;
+ }));
+ } else {
+ event.respondWith(new Response(
+ 'bodyUsed: ' + lastResponseForUsedCheck.bodyUsed));
+ }
+}
+function handleFragmentCheck(event) {
+ var body;
+ if (event.request.url.indexOf('#') === -1) {
+ body = 'Fragment Not Found';
+ } else {
+ body = 'Fragment Found :' +
+ event.request.url.substring(event.request.url.indexOf('#'));
+ }
+ event.respondWith(new Response(body));
+}
+function handleCache(event) {
+ event.respondWith(new Response(event.request.cache));
+}
+function handleEventSource(event) {
+ if (event.request.mode === 'navigate') {
+ return;
+ }
+ var data = {
+ mode: event.request.mode,
+ cache: event.request.cache,
+ credentials: event.request.credentials
+ };
+ var body = 'data:' + JSON.stringify(data) + '\n\n';
+ event.respondWith(new Response(body, {
+ headers: { 'Content-Type': 'text/event-stream' }
+ }
+ ));
+}
+
+function handleIntegrity(event) {
+ event.respondWith(new Response(event.request.integrity));
+}
+
+function handleRequestBody(event) {
+ event.respondWith(event.request.text().then(text => {
+ return new Response(text);
+ }));
+}
+
+function handleKeepalive(event) {
+ event.respondWith(new Response(event.request.keepalive));
+}
+
+function handleIsReloadNavigation(event) {
+ const request = event.request;
+ const body =
+ `method = ${request.method}, ` +
+ `isReloadNavigation = ${request.isReloadNavigation}`;
+ event.respondWith(new Response(body));
+}
+
+function handleIsHistoryNavigation(event) {
+ const request = event.request;
+ const body =
+ `method = ${request.method}, ` +
+ `isHistoryNavigation = ${request.isHistoryNavigation}`;
+ event.respondWith(new Response(body));
+}
+
+function handleUseAndIgnore(event) {
+ const request = event.request;
+ request.text();
+ return;
+}
+
+function handleCloneAndIgnore(event) {
+ const request = event.request;
+ request.clone().text();
+ return;
+}
+
+var handle_status_count = 0;
+function handleStatus(event) {
+ handle_status_count++;
+ event.respondWith(async function() {
+ const res = await fetch(event.request);
+ const text = await res.text();
+ return new Response(`${text}. Request was sent ${handle_status_count} times.`,
+ {"status": new URL(event.request.url).searchParams.get("status")});
+ }());
+}
+
+self.addEventListener('fetch', function(event) {
+ var url = event.request.url;
+ var handlers = [
+ { pattern: '?headers', fn: handleHeaders },
+ { pattern: '?string', fn: handleString },
+ { pattern: '?blob', fn: handleBlob },
+ { pattern: '?referrerFull', fn: handleReferrerFull },
+ { pattern: '?referrerPolicy', fn: handleReferrerPolicy },
+ { pattern: '?referrer', fn: handleReferrer },
+ { pattern: '?clientId', fn: handleClientId },
+ { pattern: '?resultingClientId', fn: handleResultingClientId },
+ { pattern: '?ignore', fn: function() {} },
+ { pattern: '?null', fn: handleNullBody },
+ { pattern: '?fetch', fn: handleFetch },
+ { pattern: '?form-post', fn: handleFormPost },
+ { pattern: '?multiple-respond-with', fn: handleMultipleRespondWith },
+ { pattern: '?used-check', fn: handleUsedCheck },
+ { pattern: '?fragment-check', fn: handleFragmentCheck },
+ { pattern: '?cache', fn: handleCache },
+ { pattern: '?eventsource', fn: handleEventSource },
+ { pattern: '?integrity', fn: handleIntegrity },
+ { pattern: '?request-body', fn: handleRequestBody },
+ { pattern: '?keepalive', fn: handleKeepalive },
+ { pattern: '?isReloadNavigation', fn: handleIsReloadNavigation },
+ { pattern: '?isHistoryNavigation', fn: handleIsHistoryNavigation },
+ { pattern: '?use-and-ignore', fn: handleUseAndIgnore },
+ { pattern: '?clone-and-ignore', fn: handleCloneAndIgnore },
+ { pattern: '?status', fn: handleStatus },
+ ];
+
+ var handler = null;
+ for (var i = 0; i < handlers.length; ++i) {
+ if (url.indexOf(handlers[i].pattern) != -1) {
+ handler = handlers[i];
+ break;
+ }
+ }
+
+ if (handler) {
+ handler.fn(event);
+ } else {
+ event.respondWith(new Response(new Blob(
+ ['Service Worker got an unexpected request: ' + url])));
+ }
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-within-sw-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-within-sw-worker.js
new file mode 100644
index 0000000..5903bab
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-event-within-sw-worker.js
@@ -0,0 +1,48 @@
+skipWaiting();
+
+addEventListener('fetch', event => {
+ const url = new URL(event.request.url);
+
+ if (url.origin != location.origin) return;
+
+ if (url.pathname.endsWith('/sample.txt')) {
+ event.respondWith(new Response('intercepted'));
+ return;
+ }
+
+ if (url.pathname.endsWith('/sample.txt-inner-fetch')) {
+ event.respondWith(fetch('sample.txt'));
+ return;
+ }
+
+ if (url.pathname.endsWith('/sample.txt-inner-cache')) {
+ event.respondWith(
+ caches.open('test-inner-cache').then(cache =>
+ cache.add('sample.txt').then(() => cache.match('sample.txt'))
+ )
+ );
+ return;
+ }
+
+ if (url.pathname.endsWith('/show-notification')) {
+ // Copy the currect search string onto the icon url
+ const iconURL = new URL('notification_icon.py', location);
+ iconURL.search = url.search;
+
+ event.respondWith(
+ registration.showNotification('test', {
+ icon: iconURL
+ }).then(() => registration.getNotifications()).then(notifications => {
+ for (const n of notifications) n.close();
+ return new Response('done');
+ })
+ );
+ return;
+ }
+
+ if (url.pathname.endsWith('/notification_icon.py')) {
+ new BroadcastChannel('icon-request').postMessage('yay');
+ event.respondWith(new Response('done'));
+ return;
+ }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-header-visibility-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-header-visibility-iframe.html
new file mode 100644
index 0000000..0d9ab6f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-header-visibility-iframe.html
@@ -0,0 +1,66 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js?pipe=sub"></script>
+<script>
+ var host_info = get_host_info();
+ var uri = document.location + '?check-ua-header';
+
+ var headers = new Headers();
+ headers.set('User-Agent', 'custom_ua');
+
+ // Check the custom UA case
+ fetch(uri, { headers: headers }).then(function(response) {
+ return response.text();
+ }).then(function(text) {
+ if (text == 'custom_ua') {
+ parent.postMessage('PASS', '*');
+ } else {
+ parent.postMessage('withUA FAIL - expected "custom_ua", got "' + text + '"', '*');
+ }
+ }).catch(function(err) {
+ parent.postMessage('withUA FAIL - unexpected error: ' + err, '*');
+ });
+
+ // Check the default UA case
+ fetch(uri, {}).then(function(response) {
+ return response.text();
+ }).then(function(text) {
+ if (text == 'NO_UA') {
+ parent.postMessage('PASS', '*');
+ } else {
+ parent.postMessage('noUA FAIL - expected "NO_UA", got "' + text + '"', '*');
+ }
+ }).catch(function(err) {
+ parent.postMessage('noUA FAIL - unexpected error: ' + err, '*');
+ });
+
+ var uri = document.location + '?check-accept-header';
+ var headers = new Headers();
+ headers.set('Accept', 'hmm');
+
+ // Check for custom accept header
+ fetch(uri, { headers: headers }).then(function(response) {
+ return response.text();
+ }).then(function(text) {
+ if (text === headers.get('Accept')) {
+ parent.postMessage('PASS', '*');
+ } else {
+ parent.postMessage('custom accept FAIL - expected ' + headers.get('Accept') +
+ ' got "' + text + '"', '*');
+ }
+ }).catch(function(err) {
+ parent.postMessage('custom accept FAIL - unexpected error: ' + err, '*');
+ });
+
+ // Check for default accept header
+ fetch(uri).then(function(response) {
+ return response.text();
+ }).then(function(text) {
+ if (text === '*/*') {
+ parent.postMessage('PASS', '*');
+ } else {
+ parent.postMessage('accept FAIL - expected */* got "' + text + '"', '*');
+ }
+ }).catch(function(err) {
+ parent.postMessage('accept FAIL - unexpected error: ' + err, '*');
+ });
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-inscope.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-inscope.html
new file mode 100644
index 0000000..64a634e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-inscope.html
@@ -0,0 +1,71 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js?pipe=sub"></script>
+<script>
+var image_path = base_path() + 'fetch-access-control.py?PNGIMAGE';
+var host_info = get_host_info();
+var results = '';
+
+function test1() {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = function() {
+ test2();
+ };
+ img.onerror = function() {
+ results += 'FAIL(1)';
+ test2();
+ };
+ img.src = './sample?url=' +
+ encodeURIComponent(host_info['HTTPS_ORIGIN'] + image_path);
+}
+
+function test2() {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = function() {
+ test3();
+ };
+ img.onerror = function() {
+ results += 'FAIL(2)';
+ test3();
+ };
+ img.src = './sample?mode=no-cors&url=' +
+ encodeURIComponent(host_info['HTTPS_REMOTE_ORIGIN'] + image_path);
+}
+
+function test3() {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = function() {
+ results += 'FAIL(3)';
+ test4();
+ };
+ img.onerror = function() {
+ test4();
+ };
+ img.src = './sample?mode=no-cors&url=' +
+ encodeURIComponent(host_info['HTTP_ORIGIN'] + image_path);
+}
+
+function test4() {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = function() {
+ results += 'FAIL(4)';
+ finish();
+ };
+ img.onerror = function() {
+ finish();
+ };
+ img.src = './sample?mode=no-cors&url=' +
+ encodeURIComponent(host_info['HTTP_REMOTE_ORIGIN'] + image_path);
+}
+
+function finish() {
+ results += 'finish';
+ window.parent.postMessage({results: results}, host_info['HTTPS_ORIGIN']);
+}
+</script>
+
+<body onload='test1();'>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-outscope.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-outscope.html
new file mode 100644
index 0000000..be0b5c8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-outscope.html
@@ -0,0 +1,80 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js?pipe=sub"></script>
+<script>
+var image_path = base_path() + 'fetch-access-control.py?PNGIMAGE';
+var host_info = get_host_info();
+var results = '';
+
+function test1() {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = function() {
+ test2();
+ };
+ img.onerror = function() {
+ results += 'FAIL(1)';
+ test2();
+ };
+ img.src = host_info['HTTPS_ORIGIN'] + image_path;
+}
+
+function test2() {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = function() {
+ test3();
+ };
+ img.onerror = function() {
+ results += 'FAIL(2)';
+ test3();
+ };
+ img.src = host_info['HTTPS_REMOTE_ORIGIN'] + image_path;
+}
+
+function test3() {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = function() {
+ results += 'FAIL(3)';
+ test4();
+ };
+ img.onerror = function() {
+ test4();
+ };
+ img.src = host_info['HTTP_ORIGIN'] + image_path;
+}
+
+function test4() {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = function() {
+ results += 'FAIL(4)';
+ test5();
+ };
+ img.onerror = function() {
+ test5();
+ };
+ img.src = host_info['HTTP_REMOTE_ORIGIN'] + image_path;
+}
+
+function test5() {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = function() {
+ finish();
+ };
+ img.onerror = function() {
+ results += 'FAIL(5)';
+ finish();
+ };
+ img.src = './sample?generate-png';
+}
+
+function finish() {
+ results += 'finish';
+ window.parent.postMessage({results: results}, host_info['HTTPS_ORIGIN']);
+}
+</script>
+
+<body onload='test1();'>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-mixed-content-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-mixed-content-iframe.html
new file mode 100644
index 0000000..2831c38
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-mixed-content-iframe.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js?pipe=sub"></script>
+<script>
+var params = get_query_params(location.href);
+var SCOPE = 'fetch-mixed-content-iframe-inscope-to-' + params['target'] + '.html';
+var URL = 'fetch-rewrite-worker.js';
+var host_info = get_host_info();
+
+window.addEventListener('message', on_message, false);
+
+navigator.serviceWorker.getRegistration(SCOPE)
+ .then(function(registration) {
+ if (registration)
+ return registration.unregister();
+ })
+ .then(function() {
+ return navigator.serviceWorker.register(URL, {scope: SCOPE});
+ })
+ .then(function(registration) {
+ return new Promise(function(resolve) {
+ registration.addEventListener('updatefound', function() {
+ resolve(registration.installing);
+ });
+ });
+ })
+ .then(function(worker) {
+ worker.addEventListener('statechange', on_state_change);
+ })
+ .catch(function(reason) {
+ window.parent.postMessage({results: 'FAILURE: ' + reason.message},
+ host_info['HTTPS_ORIGIN']);
+ });
+
+function on_state_change(event) {
+ if (event.target.state != 'activated')
+ return;
+ var frame = document.createElement('iframe');
+ frame.src = SCOPE;
+ document.body.appendChild(frame);
+}
+
+function on_message(e) {
+ navigator.serviceWorker.getRegistration(SCOPE)
+ .then(function(registration) {
+ if (registration)
+ return registration.unregister();
+ })
+ .then(function() {
+ window.parent.postMessage(e.data, host_info['HTTPS_ORIGIN']);
+ })
+ .catch(function(reason) {
+ window.parent.postMessage({results: 'FAILURE: ' + reason.message},
+ host_info['HTTPS_ORIGIN']);
+ });
+}
+
+function get_query_params(url) {
+ var search = (new URL(url)).search;
+ if (!search) {
+ return {};
+ }
+ var ret = {};
+ var params = search.substring(1).split('&');
+ params.forEach(function(param) {
+ var element = param.split('=');
+ ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]);
+ });
+ return ret;
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-base-url-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-base-url-iframe.html
new file mode 100644
index 0000000..504e104
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-base-url-iframe.html
@@ -0,0 +1,20 @@
+<html>
+<head>
+<title>iframe for css base url test</title>
+</head>
+<body>
+<script>
+// Load a stylesheet. Create it dynamically so we can construct the href URL
+// dynamically.
+const link = document.createElement('link');
+link.rel = 'stylesheet';
+link.type = 'text/css';
+// Add "request-url-path" to the path to help distinguish the request URL from
+// the response URL. Add |document.location.search| (chosen by the test main
+// page) to tell the service worker how to respond to the request.
+link.href = 'request-url-path/fetch-request-css-base-url-style.css' +
+ document.location.search;
+document.head.appendChild(link);
+</script>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-base-url-style.css b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-base-url-style.css
new file mode 100644
index 0000000..f14fcaa
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-base-url-style.css
@@ -0,0 +1 @@
+body { background-image: url("./sample.png");}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-base-url-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-base-url-worker.js
new file mode 100644
index 0000000..f3d6a73
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-base-url-worker.js
@@ -0,0 +1,45 @@
+let source;
+let resolveDone;
+let done = new Promise(resolve => resolveDone = resolve);
+
+// The page messages this worker to ask for the result. Keep the worker alive
+// via waitUntil() until the result is sent.
+self.addEventListener('message', event => {
+ source = event.data.port;
+ source.postMessage('pong');
+ event.waitUntil(done);
+});
+
+self.addEventListener('fetch', event => {
+ const url = new URL(event.request.url);
+
+ // For the CSS file, respond in a way that may change the response URL,
+ // depending on |url.search|.
+ const cssPath = 'request-url-path/fetch-request-css-base-url-style.css';
+ if (url.pathname.indexOf(cssPath) != -1) {
+ // Respond with a different URL, deleting "request-url-path/".
+ if (url.search == '?fetch') {
+ event.respondWith(fetch('fetch-request-css-base-url-style.css?fetch'));
+ }
+ // Respond with new Response().
+ else if (url.search == '?newResponse') {
+ const styleString = 'body { background-image: url("./sample.png");}';
+ const headers = {'content-type': 'text/css'};
+ event.respondWith(new Response(styleString, headers));
+ }
+ }
+
+ // The image request indicates what the base URL of the CSS was. Message the
+ // result back to the test page.
+ else if (url.pathname.indexOf('sample.png') != -1) {
+ // For some reason |source| is undefined here when running the test manually
+ // in Firefox. The test author experimented with both using Client
+ // (event.source) and MessagePort to try to get the test to pass, but
+ // failed.
+ source.postMessage({
+ url: event.request.url,
+ referrer: event.request.referrer
+ });
+ resolveDone();
+ }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.css b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.css
new file mode 100644
index 0000000..9a7545d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.css
@@ -0,0 +1 @@
+#crossOriginCss { color: blue; }
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.html
new file mode 100644
index 0000000..3211f78
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.html
@@ -0,0 +1 @@
+#crossOriginHtml { color: red; }
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-iframe.html
new file mode 100644
index 0000000..9a4aded
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-iframe.html
@@ -0,0 +1,17 @@
+<style type="text/css">
+#crossOriginCss { color: red; }
+#crossOriginHtml { color: blue; }
+#sameOriginCss { color: red; }
+#sameOriginHtml { color: red; }
+#synthetic { color: red; }
+</style>
+<link href="./cross-origin-css.css?mime=no" rel="stylesheet" type="text/css">
+<link href="./cross-origin-html.css?mime=no" rel="stylesheet" type="text/css">
+<link href="./fetch-request-css-cross-origin-mime-check-same.css" rel="stylesheet" type="text/css">
+<link href="./fetch-request-css-cross-origin-mime-check-same.html" rel="stylesheet" type="text/css">
+<link href="./synthetic.css?mime=no" rel="stylesheet" type="text/css">
+<h1 id=crossOriginCss>I should be blue</h1>
+<h1 id=crossOriginHtml>I should be blue</h1>
+<h1 id=sameOriginCss>I should be blue</h1>
+<h1 id=sameOriginHtml>I should be blue</h1>
+<h1 id=synthetic>I should be blue</h1>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.css b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.css
new file mode 100644
index 0000000..55455bd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.css
@@ -0,0 +1 @@
+#sameOriginCss { color: blue; }
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.html
new file mode 100644
index 0000000..6fad4b9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.html
@@ -0,0 +1 @@
+#sameOriginHtml { color: blue; }
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-read-contents.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-read-contents.html
new file mode 100644
index 0000000..c902366
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-read-contents.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe: cross-origin CSS via service worker</title>
+
+<!-- Service worker responds with a cross-origin opaque response. -->
+<link href="cross-origin-css.css" rel="stylesheet" type="text/css">
+
+<!-- Service worker responds with a cross-origin CORS approved response. -->
+<link href="cross-origin-css.css?cors" rel="stylesheet" type="text/css">
+
+<!-- Service worker falls back to network. This is a same-origin response. -->
+<link href="fetch-request-css-cross-origin-mime-check-same.css" rel="stylesheet" type="text/css">
+
+<!-- Service worker responds with a new Response() synthetic response. -->
+<link href="synthetic.css" rel="stylesheet" type="text/css">
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-worker.js
new file mode 100644
index 0000000..a71e912
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-worker.js
@@ -0,0 +1,65 @@
+importScripts('/common/get-host-info.sub.js');
+importScripts('test-helpers.sub.js');
+
+const HOST_INFO = get_host_info();
+const REMOTE_ORIGIN = HOST_INFO.HTTPS_REMOTE_ORIGIN;
+const BASE_PATH = base_path();
+const CSS_FILE = 'fetch-request-css-cross-origin-mime-check-cross.css';
+const HTML_FILE = 'fetch-request-css-cross-origin-mime-check-cross.html';
+
+function add_pipe_header(url_str, header) {
+ if (url_str.indexOf('?pipe=') == -1) {
+ url_str += '?pipe=';
+ } else {
+ url_str += '|';
+ }
+ url_str += `header${header}`;
+ return url_str;
+}
+
+self.addEventListener('fetch', function(event) {
+ const url = new URL(event.request.url);
+
+ const use_mime =
+ (url.searchParams.get('mime') != 'no');
+ const mime_header = '(Content-Type, text/css)';
+
+ const use_cors =
+ (url.searchParams.has('cors'));
+ const cors_header = '(Access-Control-Allow-Origin, *)';
+
+ const file = url.pathname.substring(url.pathname.lastIndexOf('/') + 1);
+
+ // Respond with a cross-origin CSS resource, using CORS if desired.
+ if (file == 'cross-origin-css.css') {
+ let fetch_url = REMOTE_ORIGIN + BASE_PATH + CSS_FILE;
+ if (use_mime)
+ fetch_url = add_pipe_header(fetch_url, mime_header);
+ if (use_cors)
+ fetch_url = add_pipe_header(fetch_url, cors_header);
+ const mode = use_cors ? 'cors' : 'no-cors';
+ event.respondWith(fetch(fetch_url, {'mode': mode}));
+ return;
+ }
+
+ // Respond with a cross-origin CSS resource with an HTML name. This is only
+ // used in the MIME sniffing test, so MIME is never added.
+ if (file == 'cross-origin-html.css') {
+ const fetch_url = REMOTE_ORIGIN + BASE_PATH + HTML_FILE;
+ event.respondWith(fetch(fetch_url, {mode: 'no-cors'}));
+ return;
+ }
+
+ // Respond with synthetic CSS.
+ if (file == 'synthetic.css') {
+ let headers = {};
+ if (use_mime) {
+ headers['Content-Type'] = 'text/css';
+ }
+
+ event.respondWith(new Response("#synthetic { color: blue; }", {headers}));
+ return;
+ }
+
+ // Otherwise, fallback to network.
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-fallback-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-fallback-iframe.html
new file mode 100644
index 0000000..d117d0f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-fallback-iframe.html
@@ -0,0 +1,32 @@
+<script>
+function xhr(url) {
+ return new Promise(function(resolve, reject) {
+ var request = new XMLHttpRequest();
+ request.addEventListener(
+ 'error',
+ function() { reject(new Error()); });
+ request.addEventListener(
+ 'load',
+ function(event) { resolve(request.response); });
+ request.open('GET', url);
+ request.send();
+ });
+}
+
+function load_image(url, cross_origin) {
+ return new Promise(function(resolve, reject) {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = function() {
+ resolve();
+ };
+ img.onerror = function() {
+ reject(new Error());
+ };
+ if (cross_origin != '') {
+ img.crossOrigin = cross_origin;
+ }
+ img.src = url;
+ });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-fallback-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-fallback-worker.js
new file mode 100644
index 0000000..3b028b2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-fallback-worker.js
@@ -0,0 +1,13 @@
+var requests = [];
+
+self.addEventListener('message', function(event) {
+ event.data.port.postMessage({requests: requests});
+ requests = [];
+ });
+
+self.addEventListener('fetch', function(event) {
+ requests.push({
+ url: event.request.url,
+ mode: event.request.mode
+ });
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-html-imports-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-html-imports-iframe.html
new file mode 100644
index 0000000..07a0842
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-html-imports-iframe.html
@@ -0,0 +1,13 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script type="text/javascript">
+ var hostInfo = get_host_info();
+ var makeLink = function(id, url) {
+ var link = document.createElement('link');
+ link.rel = 'import'
+ link.id = id;
+ link.href = url;
+ document.documentElement.appendChild(link);
+ };
+ makeLink('same', hostInfo.HTTPS_ORIGIN + '/sample-dir/same.html');
+ makeLink('other', hostInfo.HTTPS_REMOTE_ORIGIN + '/sample-dir/other.html');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-html-imports-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-html-imports-worker.js
new file mode 100644
index 0000000..110727b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-html-imports-worker.js
@@ -0,0 +1,30 @@
+importScripts('/common/get-host-info.sub.js');
+var host_info = get_host_info();
+
+self.addEventListener('fetch', function(event) {
+ var url = event.request.url;
+ if (url.indexOf('sample-dir') == -1) {
+ return;
+ }
+ var result = 'mode=' + event.request.mode +
+ ' credentials=' + event.request.credentials;
+ if (url == host_info.HTTPS_ORIGIN + '/sample-dir/same.html') {
+ event.respondWith(new Response(
+ result +
+ '<link id="same-same" rel="import" ' +
+ 'href="' + host_info.HTTPS_ORIGIN + '/sample-dir/same-same.html">' +
+ '<link id="same-other" rel="import" ' +
+ ' href="' + host_info.HTTPS_REMOTE_ORIGIN +
+ '/sample-dir/same-other.html">'));
+ } else if (url == host_info.HTTPS_REMOTE_ORIGIN + '/sample-dir/other.html') {
+ event.respondWith(new Response(
+ result +
+ '<link id="other-same" rel="import" ' +
+ ' href="' + host_info.HTTPS_ORIGIN + '/sample-dir/other-same.html">' +
+ '<link id="other-other" rel="import" ' +
+ ' href="' + host_info.HTTPS_REMOTE_ORIGIN +
+ '/sample-dir/other-other.html">'));
+ } else {
+ event.respondWith(new Response(result));
+ }
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-iframe.html
new file mode 100644
index 0000000..e6e9380
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-iframe.html
@@ -0,0 +1 @@
+<script src="./fetch-request-no-freshness-headers-script.py"></script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-script.py b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-script.py
new file mode 100644
index 0000000..bf8df15
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-script.py
@@ -0,0 +1,6 @@
+def main(request, response):
+ headers = []
+ # Sets an ETag header to check the cache revalidation behavior.
+ headers.append((b"ETag", b"abc123"))
+ headers.append((b"Content-Type", b"text/javascript"))
+ return headers, b"/* empty script */"
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-worker.js
new file mode 100644
index 0000000..2bd59d7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-worker.js
@@ -0,0 +1,18 @@
+var requests = [];
+
+self.addEventListener('message', function(event) {
+ event.data.port.postMessage({requests: requests});
+ });
+
+self.addEventListener('fetch', function(event) {
+ var url = event.request.url;
+ var headers = [];
+ for (var header of event.request.headers) {
+ headers.push(header);
+ }
+ requests.push({
+ url: url,
+ headers: headers
+ });
+ event.respondWith(fetch(event.request));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-redirect-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-redirect-iframe.html
new file mode 100644
index 0000000..ffd76bf
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-redirect-iframe.html
@@ -0,0 +1,35 @@
+<script>
+function xhr(url) {
+ return new Promise(function(resolve, reject) {
+ var request = new XMLHttpRequest();
+ request.addEventListener(
+ 'error',
+ function(event) { reject(event); });
+ request.addEventListener(
+ 'load',
+ function(event) { resolve(request.response); });
+ request.open('GET', url);
+ request.send();
+ });
+}
+
+function load_image(url) {
+ return new Promise(function(resolve, reject) {
+ var img = document.createElement('img');
+ document.body.appendChild(img);
+ img.onload = resolve;
+ img.onerror = reject;
+ img.src = url;
+ });
+}
+
+function load_audio(url) {
+ return new Promise(function(resolve, reject) {
+ var audio = document.createElement('audio');
+ document.body.appendChild(audio);
+ audio.oncanplay = resolve;
+ audio.onerror = reject;
+ audio.src = url;
+ });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html
new file mode 100644
index 0000000..86e9f4b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html
@@ -0,0 +1,87 @@
+<script src="test-helpers.sub.js?pipe=sub"></script>
+<body>
+<script>
+
+function load_image(url, cross_origin) {
+ const img = document.createElement('img');
+ if (cross_origin != '') {
+ img.crossOrigin = cross_origin;
+ }
+ img.src = url;
+}
+
+function load_script(url, cross_origin) {
+ const script = document.createElement('script');
+ script.src = url;
+ if (cross_origin != '') {
+ script.crossOrigin = cross_origin;
+ }
+ document.body.appendChild(script);
+}
+
+function load_css(url, cross_origin) {
+ const link = document.createElement('link');
+ link.rel = 'stylesheet'
+ link.href = url;
+ link.type = 'text/css';
+ if (cross_origin != '') {
+ link.crossOrigin = cross_origin;
+ }
+ document.body.appendChild(link);
+}
+
+function load_font(url) {
+ const fontFace = new FontFace('test', 'url(' + url + ')');
+ fontFace.load();
+}
+
+function load_css_image(url, type) {
+ const div = document.createElement('div');
+ document.body.appendChild(div);
+ div.style[type] = 'url(' + url + ')';
+}
+
+function load_css_image_set(url, type) {
+ const div = document.createElement('div');
+ document.body.appendChild(div);
+ div.style[type] = 'image-set(url(' + url + ') 1x)';
+ if (!div.style[type]) {
+ div.style[type] = '-webkit-image-set(url(' + url + ') 1x)';
+ }
+}
+
+function load_script_with_integrity(url, integrity) {
+ const script = document.createElement('script');
+ script.src = url;
+ script.integrity = integrity;
+ document.body.appendChild(script);
+}
+
+function load_css_with_integrity(url, integrity) {
+ const link = document.createElement('link');
+ link.rel = 'stylesheet'
+ link.href = url;
+ link.type = 'text/css';
+ link.integrity = integrity;
+ document.body.appendChild(link);
+}
+
+function load_audio(url, cross_origin) {
+ const audio = document.createElement('audio');
+ if (cross_origin != '') {
+ audio.crossOrigin = cross_origin;
+ }
+ audio.src = url;
+ document.body.appendChild(audio);
+}
+
+function load_video(url, cross_origin) {
+ const video = document.createElement('video');
+ if (cross_origin != '') {
+ video.crossOrigin = cross_origin;
+ }
+ video.src = url;
+ document.body.appendChild(video);
+}
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-resources-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-resources-worker.js
new file mode 100644
index 0000000..983cccb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-resources-worker.js
@@ -0,0 +1,26 @@
+const requests = [];
+let port = undefined;
+
+self.onmessage = e => {
+ const message = e.data;
+ if ('port' in message) {
+ port = message.port;
+ port.postMessage({ready: true});
+ }
+};
+
+self.addEventListener('fetch', e => {
+ const url = e.request.url;
+ if (!url.includes('sample?test')) {
+ return;
+ }
+ port.postMessage({
+ url: url,
+ mode: e.request.mode,
+ redirect: e.request.redirect,
+ credentials: e.request.credentials,
+ integrity: e.request.integrity,
+ destination: e.request.destination
+ });
+ e.respondWith(Promise.reject());
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-iframe.https.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-iframe.https.html
new file mode 100644
index 0000000..b3ddec1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-iframe.https.html
@@ -0,0 +1,208 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+var host_info = get_host_info();
+
+function get_boundary(headers) {
+ var reg = new RegExp('multipart\/form-data; boundary=(.*)');
+ for (var i = 0; i < headers.length; ++i) {
+ if (headers[i][0] != 'content-type') {
+ continue;
+ }
+ var regResult = reg.exec(headers[i][1]);
+ if (!regResult) {
+ continue;
+ }
+ return regResult[1];
+ }
+ return '';
+}
+
+function xhr_send(url_base, method, data, with_credentials) {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.onload = function() {
+ resolve(JSON.parse(xhr.response));
+ };
+ xhr.onerror = function() {
+ reject('XHR should succeed.');
+ };
+ xhr.responseType = 'text';
+ if (with_credentials) {
+ xhr.withCredentials = true;
+ }
+ xhr.open(method, url_base + '/sample?test', true);
+ xhr.send(data);
+ });
+}
+
+function get_sorted_header_name_list(headers) {
+ var header_names = [];
+ var idx, name;
+
+ for (idx = 0; idx < headers.length; ++idx) {
+ name = headers[idx][0];
+ // The `Accept-Language` header is optional; its presence should not
+ // influence test results.
+ //
+ // > 4. If request’s header list does not contain `Accept-Language`, user
+ // > agents should append `Accept-Language`/an appropriate value to
+ // > request's header list.
+ //
+ // https://fetch.spec.whatwg.org/#fetching
+ if (name === 'accept-language') {
+ continue;
+ }
+
+ header_names.push(name);
+ }
+ header_names.sort();
+ return header_names;
+}
+
+function get_header_test() {
+ return xhr_send(host_info['HTTPS_ORIGIN'], 'GET', '', false)
+ .then(function(response) {
+ assert_array_equals(
+ get_sorted_header_name_list(response.headers),
+ ["accept"],
+ 'event.request has the expected headers for same-origin GET.');
+ });
+}
+
+function post_header_test() {
+ return xhr_send(host_info['HTTPS_ORIGIN'], 'POST', '', false)
+ .then(function(response) {
+ assert_array_equals(
+ get_sorted_header_name_list(response.headers),
+ ["accept", "content-type"],
+ 'event.request has the expected headers for same-origin POST.');
+ });
+}
+
+function cross_origin_get_header_test() {
+ return xhr_send(host_info['HTTPS_REMOTE_ORIGIN'], 'GET', '', false)
+ .then(function(response) {
+ assert_array_equals(
+ get_sorted_header_name_list(response.headers),
+ ["accept"],
+ 'event.request has the expected headers for cross-origin GET.');
+ });
+}
+
+function cross_origin_post_header_test() {
+ return xhr_send(host_info['HTTPS_REMOTE_ORIGIN'], 'POST', '', false)
+ .then(function(response) {
+ assert_array_equals(
+ get_sorted_header_name_list(response.headers),
+ ["accept", "content-type"],
+ 'event.request has the expected headers for cross-origin POST.');
+ });
+}
+
+function string_test() {
+ return xhr_send(host_info['HTTPS_ORIGIN'], 'POST', 'test string', false)
+ .then(function(response) {
+ assert_equals(response.method, 'POST');
+ assert_equals(response.body, 'test string');
+ });
+}
+
+function blob_test() {
+ return xhr_send(host_info['HTTPS_ORIGIN'], 'POST', new Blob(['test blob']),
+ false)
+ .then(function(response) {
+ assert_equals(response.method, 'POST');
+ assert_equals(response.body, 'test blob');
+ });
+}
+
+function custom_method_test() {
+ return xhr_send(host_info['HTTPS_ORIGIN'], 'XXX', 'test string xxx', false)
+ .then(function(response) {
+ assert_equals(response.method, 'XXX');
+ assert_equals(response.body, 'test string xxx');
+ });
+}
+
+function options_method_test() {
+ return xhr_send(host_info['HTTPS_ORIGIN'], 'OPTIONS', 'test string xxx', false)
+ .then(function(response) {
+ assert_equals(response.method, 'OPTIONS');
+ assert_equals(response.body, 'test string xxx');
+ });
+}
+
+function form_data_test() {
+ var formData = new FormData();
+ formData.append('sample string', '1234567890');
+ formData.append('sample blob', new Blob(['blob content']));
+ formData.append('sample file', new File(['file content'], 'file.dat'));
+ return xhr_send(host_info['HTTPS_ORIGIN'], 'POST', formData, false)
+ .then(function(response) {
+ assert_equals(response.method, 'POST');
+ var boundary = get_boundary(response.headers);
+ var expected_body =
+ '--' + boundary + '\r\n' +
+ 'Content-Disposition: form-data; name="sample string"\r\n' +
+ '\r\n' +
+ '1234567890\r\n' +
+ '--' + boundary + '\r\n' +
+ 'Content-Disposition: form-data; name="sample blob"; ' +
+ 'filename="blob"\r\n' +
+ 'Content-Type: application/octet-stream\r\n' +
+ '\r\n' +
+ 'blob content\r\n' +
+ '--' + boundary + '\r\n' +
+ 'Content-Disposition: form-data; name="sample file"; ' +
+ 'filename="file.dat"\r\n' +
+ 'Content-Type: application/octet-stream\r\n' +
+ '\r\n' +
+ 'file content\r\n' +
+ '--' + boundary + '--\r\n';
+ assert_equals(response.body, expected_body, "form data response content is as expected");
+ });
+}
+
+function mode_credentials_test() {
+ return xhr_send(host_info['HTTPS_ORIGIN'], 'GET', '', false)
+ .then(function(response){
+ assert_equals(response.mode, 'cors');
+ assert_equals(response.credentials, 'same-origin');
+ return xhr_send(host_info['HTTPS_ORIGIN'], 'GET', '', true);
+ })
+ .then(function(response){
+ assert_equals(response.mode, 'cors');
+ assert_equals(response.credentials, 'include');
+ return xhr_send(host_info['HTTPS_REMOTE_ORIGIN'], 'GET', '', false);
+ })
+ .then(function(response){
+ assert_equals(response.mode, 'cors');
+ assert_equals(response.credentials, 'same-origin');
+ return xhr_send(host_info['HTTPS_REMOTE_ORIGIN'], 'GET', '', true);
+ })
+ .then(function(response){
+ assert_equals(response.mode, 'cors');
+ assert_equals(response.credentials, 'include');
+ });
+}
+
+function data_url_test() {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.onload = function() {
+ resolve(xhr.response);
+ };
+ xhr.onerror = function() {
+ reject('XHR should succeed.');
+ };
+ xhr.responseType = 'text';
+ xhr.open('GET', 'data:text/html,Foobar', true);
+ xhr.send();
+ })
+ .then(function(data) {
+ assert_equals(data, 'Foobar');
+ });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-error-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-error-worker.js
new file mode 100644
index 0000000..b8d3db9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-error-worker.js
@@ -0,0 +1,19 @@
+"use strict";
+
+self.onfetch = event => {
+ if (event.request.url.endsWith("non-existent-stream-1.txt")) {
+ const rs1 = new ReadableStream();
+ event.respondWith(new Response(rs1));
+ rs1.cancel(1);
+ } else if (event.request.url.endsWith("non-existent-stream-2.txt")) {
+ const rs2 = new ReadableStream({
+ start(controller) { controller.error(1) }
+ });
+ event.respondWith(new Response(rs2));
+ } else if (event.request.url.endsWith("non-existent-stream-3.txt")) {
+ const rs3 = new ReadableStream({
+ pull(controller) { controller.error(1) }
+ });
+ event.respondWith(new Response(rs3));
+ }
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-iframe.html
new file mode 100644
index 0000000..900762f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-iframe.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<title>Service Worker: Synchronous XHR is intercepted iframe</title>
+<script>
+'use strict';
+
+function performSyncXHR(url) {
+ var syncXhr = new XMLHttpRequest();
+ syncXhr.open('GET', url, false);
+ syncXhr.send();
+
+ return syncXhr;
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-on-worker-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-on-worker-worker.js
new file mode 100644
index 0000000..0d24ffc
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-on-worker-worker.js
@@ -0,0 +1,41 @@
+'use strict';
+
+self.onfetch = function(event) {
+ if (event.request.url.indexOf('non-existent-file.txt') !== -1) {
+ event.respondWith(new Response('Response from service worker'));
+ } else if (event.request.url.indexOf('/iframe_page') !== -1) {
+ event.respondWith(new Response(
+ '<!DOCTYPE html>\n' +
+ '<script>\n' +
+ 'function performSyncXHROnWorker(url) {\n' +
+ ' return new Promise((resolve) => {\n' +
+ ' var worker =\n' +
+ ' new Worker(\'./worker_script\');\n' +
+ ' worker.addEventListener(\'message\', (msg) => {\n' +
+ ' resolve(msg.data);\n' +
+ ' });\n' +
+ ' worker.postMessage({\n' +
+ ' url: url\n' +
+ ' });\n' +
+ ' });\n' +
+ '}\n' +
+ '</script>',
+ {
+ headers: [['content-type', 'text/html']]
+ }));
+ } else if (event.request.url.indexOf('/worker_script') !== -1) {
+ event.respondWith(new Response(
+ 'self.onmessage = (msg) => {' +
+ ' const syncXhr = new XMLHttpRequest();' +
+ ' syncXhr.open(\'GET\', msg.data.url, false);' +
+ ' syncXhr.send();' +
+ ' self.postMessage({' +
+ ' status: syncXhr.status,' +
+ ' responseText: syncXhr.responseText' +
+ ' });' +
+ '}',
+ {
+ headers: [['content-type', 'application/javascript']]
+ }));
+ }
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-worker.js
new file mode 100644
index 0000000..070e572
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-sync-worker.js
@@ -0,0 +1,7 @@
+'use strict';
+
+self.onfetch = function(event) {
+ if (event.request.url.indexOf('non-existent-file.txt') !== -1) {
+ event.respondWith(new Response('Response from service worker'));
+ }
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-worker.js
new file mode 100644
index 0000000..4e42837
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-request-xhr-worker.js
@@ -0,0 +1,22 @@
+self.addEventListener('fetch', function(event) {
+ var url = event.request.url;
+ if (url.indexOf('sample?test') == -1) {
+ return;
+ }
+ event.respondWith(new Promise(function(resolve) {
+ var headers = [];
+ for (var header of event.request.headers) {
+ headers.push(header);
+ }
+ event.request.text()
+ .then(function(result) {
+ resolve(new Response(JSON.stringify({
+ method: event.request.method,
+ mode: event.request.mode,
+ credentials: event.request.credentials,
+ headers: headers,
+ body: result
+ })));
+ });
+ }));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response-taint-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response-taint-iframe.html
new file mode 100644
index 0000000..5f09efe
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response-taint-iframe.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<body></body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response-xhr-iframe.https.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response-xhr-iframe.https.html
new file mode 100644
index 0000000..c26eebe
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response-xhr-iframe.https.html
@@ -0,0 +1,53 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js?pipe=sub"></script>
+<script>
+var host_info = get_host_info();
+
+function xhr_send(method, data) {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.onload = function() {
+ resolve(xhr);
+ };
+ xhr.onerror = function() {
+ reject('XHR should succeed.');
+ };
+ xhr.responseType = 'text';
+ xhr.open(method, './sample?test', true);
+ xhr.send(data);
+ });
+}
+
+function coalesce_headers_test() {
+ return xhr_send('POST', 'test string')
+ .then(function(xhr) {
+ window.parent.postMessage({results: xhr.getResponseHeader('foo')},
+ host_info['HTTPS_ORIGIN']);
+
+ return new Promise(function(resolve) {
+ window.addEventListener('message', function handle(evt) {
+ if (evt.data !== 'ACK') {
+ return;
+ }
+
+ window.removeEventListener('message', handle);
+ resolve();
+ });
+ });
+ });
+}
+
+window.addEventListener('message', function(evt) {
+ var port;
+
+ if (evt.data !== 'START') {
+ return;
+ }
+
+ port = evt.ports[0];
+
+ coalesce_headers_test()
+ .then(function() { port.postMessage({results: 'finish'}); })
+ .catch(function(e) { port.postMessage({results: 'failure:' + e}); });
+ });
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response-xhr-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response-xhr-worker.js
new file mode 100644
index 0000000..0301b12
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response-xhr-worker.js
@@ -0,0 +1,12 @@
+self.addEventListener('fetch', function(event) {
+ var url = event.request.url;
+ if (url.indexOf('sample?test') == -1) {
+ return;
+ }
+ event.respondWith(new Promise(function(resolve) {
+ var headers = new Headers;
+ headers.append('foo', 'foo');
+ headers.append('foo', 'bar');
+ resolve(new Response('hello world', {'headers': headers}));
+ }));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response.html b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response.html
new file mode 100644
index 0000000..6d27cf1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+
+<script>
+ const params =new URLSearchParams(location.search);
+ const mode = params.get("mode") || "cors";
+ const path = params.get('path');
+ const bufferPromise =
+ new Promise(resolve =>
+ fetch(path, {mode})
+ .then(response => resolve(response.arrayBuffer()))
+ .catch(() => resolve(new Uint8Array())));
+
+ const entryPromise = new Promise(resolve => {
+ new PerformanceObserver(entries => {
+ const byName = entries.getEntriesByType("resource").find(e => e.name.includes(path));
+ if (byName)
+ resolve(byName);
+ }).observe({entryTypes: ["resource"]});
+ });
+
+ Promise.all([bufferPromise, entryPromise]).then(([buffer, entry]) => {
+ parent.postMessage({
+ buffer,
+ entry: entry.toJSON(),
+ }, '*');
+ });
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response.js
new file mode 100644
index 0000000..775efc0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-response.js
@@ -0,0 +1,35 @@
+self.addEventListener('fetch', event => {
+ const path = event.request.url.match(/\/(?<name>[^\/]+)$/);
+ switch (path?.groups?.name) {
+ case 'constructed':
+ event.respondWith(new Response(new Uint8Array([1, 2, 3])));
+ break;
+ case 'forward':
+ event.respondWith(fetch('/common/text-plain.txt'));
+ break;
+ case 'stream':
+ event.respondWith((async() => {
+ const res = await fetch('/common/text-plain.txt');
+ const body = await res.body;
+ const reader = await body.getReader();
+ const stream = new ReadableStream({
+ async start(controller) {
+ while (true) {
+ const {done, value} = await reader.read();
+ if (done)
+ break;
+
+ controller.enqueue(value);
+ }
+ controller.close();
+ reader.releaseLock();
+ }
+ });
+ return new Response(stream);
+ })());
+ break;
+ default:
+ event.respondWith(fetch(event.request));
+ break;
+ }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js
new file mode 100644
index 0000000..64c99c9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js
@@ -0,0 +1,4 @@
+// This script is intended to be served with the `Referrer-Policy` header as
+// defined in the corresponding `.headers` file.
+
+importScripts('fetch-rewrite-worker.js');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js.headers b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js.headers
new file mode 100644
index 0000000..5ae4265
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js.headers
@@ -0,0 +1,2 @@
+Content-Type: application/javascript
+Referrer-Policy: origin
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker.js
new file mode 100644
index 0000000..20a8066
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker.js
@@ -0,0 +1,166 @@
+// By default, this worker responds to fetch events with
+// respondWith(fetch(request)). Additionally, if the request has a &url
+// parameter, it fetches the provided URL instead. Because it forwards fetch
+// events to this other URL, it is called the "fetch rewrite" worker.
+//
+// The worker also looks for other params on the request to do more custom
+// behavior, like falling back to network or throwing an error.
+
+function get_query_params(url) {
+ var search = (new URL(url)).search;
+ if (!search) {
+ return {};
+ }
+ var ret = {};
+ var params = search.substring(1).split('&');
+ params.forEach(function(param) {
+ var element = param.split('=');
+ ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]);
+ });
+ return ret;
+}
+
+function get_request_init(base, params) {
+ var init = {};
+ init['method'] = params['method'] || base['method'];
+ init['mode'] = params['mode'] || base['mode'];
+ if (init['mode'] == 'navigate') {
+ init['mode'] = 'same-origin';
+ }
+ init['credentials'] = params['credentials'] || base['credentials'];
+ init['redirect'] = params['redirect-mode'] || base['redirect'];
+ return init;
+}
+
+self.addEventListener('fetch', function(event) {
+ var params = get_query_params(event.request.url);
+ var init = get_request_init(event.request, params);
+ var url = params['url'];
+ if (params['ignore']) {
+ return;
+ }
+ if (params['throw']) {
+ throw new Error('boom');
+ }
+ if (params['reject']) {
+ event.respondWith(new Promise(function(resolve, reject) {
+ reject();
+ }));
+ return;
+ }
+ if (params['resolve-null']) {
+ event.respondWith(new Promise(function(resolve) {
+ resolve(null);
+ }));
+ return;
+ }
+ if (params['generate-png']) {
+ var binary = atob(
+ 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAA' +
+ 'RnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAhSURBVDhPY3wro/Kf' +
+ 'gQLABKXJBqMGjBoAAqMGDLwBDAwAEsoCTFWunmQAAAAASUVORK5CYII=');
+ var array = new Uint8Array(binary.length);
+ for(var i = 0; i < binary.length; i++) {
+ array[i] = binary.charCodeAt(i);
+ };
+ event.respondWith(new Response(new Blob([array], {type: 'image/png'})));
+ return;
+ }
+ if (params['check-ua-header']) {
+ var ua = event.request.headers.get('User-Agent');
+ if (ua) {
+ // We have a user agent!
+ event.respondWith(new Response(new Blob([ua])));
+ } else {
+ // We don't have a user-agent!
+ event.respondWith(new Response(new Blob(["NO_UA"])));
+ }
+ return;
+ }
+ if (params['check-accept-header']) {
+ var accept = event.request.headers.get('Accept');
+ if (accept) {
+ event.respondWith(new Response(accept));
+ } else {
+ event.respondWith(new Response('NO_ACCEPT'));
+ }
+ return;
+ }
+ event.respondWith(new Promise(function(resolve, reject) {
+ var request = event.request;
+ if (url) {
+ request = new Request(url, init);
+ } else if (params['change-request']) {
+ request = new Request(request, init);
+ }
+ const response_promise = params['navpreload'] ? event.preloadResponse
+ : fetch(request);
+ response_promise.then(function(response) {
+ var expectedType = params['expected_type'];
+ if (expectedType && response.type !== expectedType) {
+ // Resolve a JSON object with a failure instead of rejecting
+ // in order to distinguish this from a NetworkError, which
+ // may be expected even if the type is correct.
+ resolve(new Response(JSON.stringify({
+ result: 'failure',
+ detail: 'got ' + response.type + ' Response.type instead of ' +
+ expectedType
+ })));
+ }
+
+ var expectedRedirected = params['expected_redirected'];
+ if (typeof expectedRedirected !== 'undefined') {
+ var expected_redirected = (expectedRedirected === 'true');
+ if(response.redirected !== expected_redirected) {
+ // This is simply determining how to pass an error to the outer
+ // test case(fetch-request-redirect.https.html).
+ var execptedResolves = params['expected_resolves'];
+ if (execptedResolves === 'true') {
+ // Reject a JSON object with a failure since promise is expected
+ // to be resolved.
+ reject(new Response(JSON.stringify({
+ result: 'failure',
+ detail: 'got '+ response.redirected +
+ ' Response.redirected instead of ' +
+ expectedRedirected
+ })));
+ } else {
+ // Resolve a JSON object with a failure since promise is
+ // expected to be rejected.
+ resolve(new Response(JSON.stringify({
+ result: 'failure',
+ detail: 'got '+ response.redirected +
+ ' Response.redirected instead of ' +
+ expectedRedirected
+ })));
+ }
+ }
+ }
+
+ if (params['clone']) {
+ response = response.clone();
+ }
+
+ // |cache| means to bounce responses through Cache Storage and back.
+ if (params['cache']) {
+ var cacheName = "cached-fetches-" + performance.now() + "-" +
+ event.request.url;
+ var cache;
+ var cachedResponse;
+ return self.caches.open(cacheName).then(function(opened) {
+ cache = opened;
+ return cache.put(request, response);
+ }).then(function() {
+ return cache.match(request);
+ }).then(function(cached) {
+ cachedResponse = cached;
+ return self.caches.delete(cacheName);
+ }).then(function() {
+ resolve(cachedResponse);
+ });
+ } else {
+ resolve(response);
+ }
+ }, reject)
+ }));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker.js.headers b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker.js.headers
new file mode 100644
index 0000000..123053b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-rewrite-worker.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript
+Service-Worker-Allowed: /
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-variants-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-variants-worker.js
new file mode 100644
index 0000000..b950b9a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-variants-worker.js
@@ -0,0 +1,35 @@
+importScripts('/common/get-host-info.sub.js');
+importScripts('test-helpers.sub.js');
+importScripts('/resources/testharness.js');
+
+const storedResponse = new Response(new Blob(['a simple text file']))
+const absolultePath = `${base_path()}/simple.txt`
+
+self.addEventListener('fetch', event => {
+ const search = new URLSearchParams(new URL(event.request.url).search.substr(1))
+ const variant = search.get('variant')
+ const delay = search.get('delay')
+ if (!variant)
+ return
+
+ switch (variant) {
+ case 'forward':
+ event.respondWith(fetch(event.request.url))
+ break
+ case 'redirect':
+ event.respondWith(fetch(`/xhr/resources/redirect.py?location=${base_path()}/simple.txt`))
+ break
+ case 'delay-before-fetch':
+ event.respondWith(
+ new Promise(resolve => {
+ step_timeout(() => fetch(event.request.url).then(resolve), delay)
+ }))
+ break
+ case 'delay-after-fetch':
+ event.respondWith(new Promise(resolve => {
+ fetch(event.request.url)
+ .then(response => step_timeout(() => resolve(response), delay))
+ }))
+ break
+ }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js
new file mode 100644
index 0000000..92a96ff
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js
@@ -0,0 +1,31 @@
+var activatePromiseResolve;
+
+addEventListener('activate', function(evt) {
+ evt.waitUntil(new Promise(function(resolve) {
+ activatePromiseResolve = resolve;
+ }));
+});
+
+addEventListener('message', async function(evt) {
+ switch (evt.data) {
+ case 'CLAIM':
+ evt.waitUntil(new Promise(async resolve => {
+ await clients.claim();
+ evt.source.postMessage('CLAIMED');
+ resolve();
+ }));
+ break;
+ case 'ACTIVATE':
+ if (typeof activatePromiseResolve !== 'function') {
+ throw new Error('Not activating!');
+ }
+ activatePromiseResolve();
+ break;
+ default:
+ throw new Error('Unknown message!');
+ }
+});
+
+addEventListener('fetch', function(evt) {
+ evt.respondWith(new Response('Hello world'));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/form-poster.html b/third_party/web_platform_tests/service-workers/service-worker/resources/form-poster.html
new file mode 100644
index 0000000..cd11a30
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/form-poster.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="referrer" content="origin">
+<form method="POST" id="form"></form>
+<script>
+function onLoad() {
+ const params = new URLSearchParams(self.location.search);
+ const form = document.getElementById('form');
+ form.action = params.get('target');
+ form.submit();
+}
+self.addEventListener('load', onLoad);
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/frame-for-getregistrations.html b/third_party/web_platform_tests/service-workers/service-worker/resources/frame-for-getregistrations.html
new file mode 100644
index 0000000..7fc35f1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/frame-for-getregistrations.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<title>Service Worker: frame for getRegistrations()</title>
+<script>
+var scope = 'scope-for-getregistrations';
+var script = 'empty-worker.js';
+var registration;
+
+navigator.serviceWorker.register(script, { scope: scope })
+ .then(function(r) { registration = r; window.parent.postMessage('ready', '*'); })
+
+self.onmessage = function(e) {
+ if (e.data == 'unregister') {
+ registration.unregister()
+ .then(function() {
+ e.ports[0].postMessage('unregistered');
+ });
+ }
+};
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/get-resultingClientId-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/get-resultingClientId-worker.js
new file mode 100644
index 0000000..f0e6c7b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/get-resultingClientId-worker.js
@@ -0,0 +1,107 @@
+// This worker expects a fetch event for a navigation and messages back the
+// result of clients.get(event.resultingClientId).
+
+// Resolves when the test finishes.
+let testFinishPromise;
+let resolveTestFinishPromise;
+let rejectTestFinishPromise;
+
+// Resolves to clients.get(event.resultingClientId) from the fetch event.
+let getPromise;
+let resolveGetPromise;
+let rejectGetPromise;
+
+let resultingClientId;
+
+function startTest() {
+ testFinishPromise = new Promise((resolve, reject) => {
+ resolveTestFinishPromise = resolve;
+ rejectTestFinishPromise = reject;
+ });
+
+ getPromise = new Promise((resolve, reject) => {
+ resolveGetPromise = resolve;
+ rejectGetPromise = reject;
+ });
+}
+
+async function describeGetPromiseResult(promise) {
+ const result = {};
+
+ await promise.then(
+ (client) => {
+ result.promiseState = 'fulfilled';
+ if (client === undefined) {
+ result.promiseValue = 'undefinedValue';
+ } else if (client instanceof Client) {
+ result.promiseValue = 'client';
+ result.client = {
+ id: client.id,
+ url: client.url
+ };
+ } else {
+ result.promiseValue = 'unknown';
+ }
+ },
+ (error) => {
+ result.promiseState = 'rejected';
+ });
+
+ return result;
+}
+
+async function handleGetResultingClient(event) {
+ // Note that this message can arrive before |resultingClientId| is populated.
+ const result = await describeGetPromiseResult(getPromise);
+ // |resultingClientId| must be populated by now.
+ result.queriedId = resultingClientId;
+ event.source.postMessage(result);
+};
+
+async function handleGetClient(event) {
+ const id = event.data.id;
+ const result = await describeGetPromiseResult(self.clients.get(id));
+ result.queriedId = id;
+ event.source.postMessage(result);
+};
+
+self.addEventListener('message', (event) => {
+ if (event.data.command == 'startTest') {
+ startTest();
+ event.waitUntil(testFinishPromise);
+ event.source.postMessage('ok');
+ return;
+ }
+
+ if (event.data.command == 'finishTest') {
+ resolveTestFinishPromise();
+ event.source.postMessage('ok');
+ return;
+ }
+
+ if (event.data.command == 'getResultingClient') {
+ event.waitUntil(handleGetResultingClient(event));
+ return;
+ }
+
+ if (event.data.command == 'getClient') {
+ event.waitUntil(handleGetClient(event));
+ return;
+ }
+});
+
+async function handleFetch(event) {
+ try {
+ resultingClientId = event.resultingClientId;
+ const client = await self.clients.get(resultingClientId);
+ resolveGetPromise(client);
+ } catch (error) {
+ rejectGetPromise(error);
+ }
+}
+
+self.addEventListener('fetch', (event) => {
+ if (event.request.mode != 'navigate')
+ return;
+ event.waitUntil(handleFetch(event));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/http-to-https-redirect-and-register-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/http-to-https-redirect-and-register-iframe.html
new file mode 100644
index 0000000..bcab353
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/http-to-https-redirect-and-register-iframe.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<title>register, unregister, and report result to opener</title>
+<body>
+<script>
+'use strict';
+
+if (!navigator.serviceWorker) {
+ window.opener.postMessage('FAIL: navigator.serviceWorker is undefined', '*');
+} else {
+ navigator.serviceWorker.register('empty-worker.js', {scope: 'scope-register'})
+ .then(
+ registration => {
+ registration.unregister().then(() => {
+ window.opener.postMessage('OK', '*');
+ });
+ },
+ error => {
+ window.opener.postMessage('FAIL: ' + error.name, '*');
+ })
+ .catch(error => {
+ window.opener.postMessage('ERROR: ' + error.name, '*');
+ });
+}
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/iframe-with-fetch-variants.html b/third_party/web_platform_tests/service-workers/service-worker/resources/iframe-with-fetch-variants.html
new file mode 100644
index 0000000..3a61d7b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/iframe-with-fetch-variants.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<script>
+ const url = new URL(new URLSearchParams(location.search.substr(1)).get('url'), location.href);
+ const before = performance.now();
+ fetch(url)
+ .then(r => r.text())
+ .then(() =>
+ parent.postMessage({
+ before,
+ after: performance.now(),
+ entry: performance.getEntriesByName(url)[0].toJSON()
+ }));
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/iframe-with-image.html b/third_party/web_platform_tests/service-workers/service-worker/resources/iframe-with-image.html
new file mode 100644
index 0000000..ce78840
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/iframe-with-image.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<img src="square">
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/immutable-prototype-serviceworker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/immutable-prototype-serviceworker.js
new file mode 100644
index 0000000..d8a94ad
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/immutable-prototype-serviceworker.js
@@ -0,0 +1,19 @@
+function prototypeChain(global) {
+ let result = [];
+ while (global !== null) {
+ let thrown = false;
+ let next = Object.getPrototypeOf(global);
+ try {
+ Object.setPrototypeOf(global, {});
+ result.push('mutable');
+ } catch (e) {
+ result.push('immutable');
+ }
+ global = next;
+ }
+ return result;
+}
+
+self.onmessage = function(e) {
+ e.data.postMessage(prototypeChain(self));
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-echo-cookie-worker-module.py b/third_party/web_platform_tests/service-workers/service-worker/resources/import-echo-cookie-worker-module.py
new file mode 100644
index 0000000..8f0b68e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-echo-cookie-worker-module.py
@@ -0,0 +1,6 @@
+def main(request, response):
+ # This script generates a worker script for static imports from module
+ # service workers.
+ headers = [(b'Content-Type', b'text/javascript')]
+ body = b"import './echo-cookie-worker.py?key=%s'" % request.GET[b'key']
+ return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-echo-cookie-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-echo-cookie-worker.js
new file mode 100644
index 0000000..f5eac95
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-echo-cookie-worker.js
@@ -0,0 +1 @@
+importScripts(`echo-cookie-worker.py${location.search}`);
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-mime-type-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/import-mime-type-worker.py
new file mode 100644
index 0000000..b6e82f3
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-mime-type-worker.py
@@ -0,0 +1,10 @@
+def main(request, response):
+ if b'mime' in request.GET:
+ return (
+ [(b'Content-Type', b'application/javascript')],
+ b"importScripts('./mime-type-worker.py?mime=%s');" % request.GET[b'mime']
+ )
+ return (
+ [(b'Content-Type', b'application/javascript')],
+ b"importScripts('./mime-type-worker.py');"
+ )
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-relative.xsl b/third_party/web_platform_tests/service-workers/service-worker/resources/import-relative.xsl
new file mode 100644
index 0000000..063a62d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-relative.xsl
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+ <xsl:import href="xslt-pass.xsl"/>
+</xsl:stylesheet>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-404-after-update-plus-update-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-404-after-update-plus-update-worker.js
new file mode 100644
index 0000000..e9899d8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-404-after-update-plus-update-worker.js
@@ -0,0 +1,8 @@
+// This worker imports a script that returns 200 on the first request and 404
+// on the second request, and a script that is updated every time when
+// requesting it.
+const params = new URLSearchParams(location.search);
+const key = params.get('Key');
+const additional_key = params.get('AdditionalKey');
+importScripts(`update-worker.py?Key=${key}&Mode=not_found`,
+ `update-worker.py?Key=${additional_key}&Mode=normal`);
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-404-after-update.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-404-after-update.js
new file mode 100644
index 0000000..b569346
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-404-after-update.js
@@ -0,0 +1,6 @@
+// This worker imports a script that returns 200 on the first request and 404
+// on the second request. The resulting body also changes each time it is
+// requested.
+const params = new URLSearchParams(location.search);
+const key = params.get('Key');
+importScripts(`update-worker.py?Key=${key}&Mode=not_found`);
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-404.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-404.js
new file mode 100644
index 0000000..19c7a4b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-404.js
@@ -0,0 +1 @@
+importScripts('404.py');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-cross-origin-worker.sub.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-cross-origin-worker.sub.js
new file mode 100644
index 0000000..b432854
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-cross-origin-worker.sub.js
@@ -0,0 +1 @@
+importScripts('https://{{domains[www1]}}:{{ports[https][0]}}/service-workers/service-worker/resources/import-scripts-version.py');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-diff-resource-map-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-diff-resource-map-worker.js
new file mode 100644
index 0000000..0fdcb0f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-diff-resource-map-worker.js
@@ -0,0 +1,10 @@
+importScripts('/resources/testharness.js');
+
+let echo1 = null;
+let echo2 = null;
+let arg1 = 'import-scripts-get.py?output=echo1&msg=test1';
+let arg2 = 'import-scripts-get.py?output=echo2&msg=test2';
+
+importScripts(arg1, arg2);
+assert_equals(echo1, 'test1');
+assert_equals(echo2, 'test2');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-echo.py b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-echo.py
new file mode 100644
index 0000000..d38d660
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-echo.py
@@ -0,0 +1,6 @@
+def main(req, res):
+ return ([
+ (b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache'),
+ (b'Content-Type', b'application/javascript')],
+ b'echo_output = "%s";\n' % req.GET[b'msg'])
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-get.py b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-get.py
new file mode 100644
index 0000000..ab7b84e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-get.py
@@ -0,0 +1,6 @@
+def main(req, res):
+ return ([
+ (b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache'),
+ (b'Content-Type', b'application/javascript')],
+ b'%s = "%s";\n' % (req.GET[b'output'], req.GET[b'msg']))
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js
new file mode 100644
index 0000000..d4f1f3e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js
@@ -0,0 +1,49 @@
+const badMimeTypes = [
+ null, // no MIME type
+ 'text/plain',
+];
+
+const validMimeTypes = [
+ 'application/ecmascript',
+ 'application/javascript',
+ 'application/x-ecmascript',
+ 'application/x-javascript',
+ 'text/ecmascript',
+ 'text/javascript',
+ 'text/javascript1.0',
+ 'text/javascript1.1',
+ 'text/javascript1.2',
+ 'text/javascript1.3',
+ 'text/javascript1.4',
+ 'text/javascript1.5',
+ 'text/jscript',
+ 'text/livescript',
+ 'text/x-ecmascript',
+ 'text/x-javascript',
+];
+
+function importScriptsWithMimeType(mimeType) {
+ importScripts(`./mime-type-worker.py${mimeType ? '?mime=' + mimeType : ''}`);
+}
+
+importScripts('/resources/testharness.js');
+
+for (const mimeType of badMimeTypes) {
+ test(() => {
+ assert_throws_dom(
+ 'NetworkError',
+ () => { importScriptsWithMimeType(mimeType); },
+ `importScripts with ${mimeType ? 'bad' : 'no'} MIME type ${mimeType || ''} throws NetworkError`,
+ );
+ }, `Importing script with ${mimeType ? 'bad' : 'no'} MIME type ${mimeType || ''}`);
+}
+
+for (const mimeType of validMimeTypes) {
+ test(() => {
+ try {
+ importScriptsWithMimeType(mimeType);
+ } catch {
+ assert_unreached(`importScripts with MIME type ${mimeType} should not throw`);
+ }
+ }, `Importing script with valid JavaScript MIME type ${mimeType}`);
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-redirect-import.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-redirect-import.js
new file mode 100644
index 0000000..56c04f0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-redirect-import.js
@@ -0,0 +1 @@
+// empty script
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-redirect-on-second-time-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-redirect-on-second-time-worker.js
new file mode 100644
index 0000000..f612ab8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-redirect-on-second-time-worker.js
@@ -0,0 +1,7 @@
+// This worker imports a script that returns 200 on the first request and a
+// redirect on the second request. The resulting body also changes each time it
+// is requested.
+const params = new URLSearchParams(location.search);
+const key = params.get('Key');
+importScripts(`update-worker.py?Key=${key}&Mode=redirect&` +
+ `Redirect=update-worker.py?Key=${key}%26Mode=normal`);
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-redirect-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-redirect-worker.js
new file mode 100644
index 0000000..d02a453
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-redirect-worker.js
@@ -0,0 +1 @@
+importScripts('redirect.py?Redirect=import-scripts-version.py');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-resource-map-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-resource-map-worker.js
new file mode 100644
index 0000000..b3b9bc4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-resource-map-worker.js
@@ -0,0 +1,15 @@
+importScripts('/resources/testharness.js');
+
+let version = null;
+importScripts('import-scripts-version.py');
+// Once imported, the stored script should be loaded for subsequent importScripts.
+const expected_version = version;
+
+version = null;
+importScripts('import-scripts-version.py');
+assert_equals(expected_version, version, 'second import');
+
+version = null;
+importScripts('import-scripts-version.py', 'import-scripts-version.py',
+ 'import-scripts-version.py');
+assert_equals(expected_version, version, 'multiple imports');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-updated-flag-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-updated-flag-worker.js
new file mode 100644
index 0000000..e016646
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-updated-flag-worker.js
@@ -0,0 +1,31 @@
+importScripts('/resources/testharness.js');
+
+let echo_output = null;
+
+// Tests importing a script that sets |echo_output| to the query string.
+function test_import(str) {
+ echo_output = null;
+ importScripts('import-scripts-echo.py?msg=' + str);
+ assert_equals(echo_output, str);
+}
+
+test_import('root');
+test_import('root-and-message');
+
+self.addEventListener('install', () => {
+ test_import('install');
+ test_import('install-and-message');
+ });
+
+self.addEventListener('message', e => {
+ var error = null;
+ echo_output = null;
+
+ try {
+ importScripts('import-scripts-echo.py?msg=' + e.data);
+ } catch (e) {
+ error = e && e.name;
+ }
+
+ e.source.postMessage({ error: error, value: echo_output });
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-version.py b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-version.py
new file mode 100644
index 0000000..cde2854
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/import-scripts-version.py
@@ -0,0 +1,17 @@
+import datetime
+import time
+
+epoch = datetime.datetime(1970, 1, 1)
+
+def main(req, res):
+ # Artificially delay response time in order to ensure uniqueness of
+ # computed value
+ time.sleep(0.1)
+
+ now = (datetime.datetime.now() - epoch).total_seconds()
+
+ return ([
+ (b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache'),
+ (b'Content-Type', b'application/javascript')],
+ u'version = "%s";\n' % now)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/imported-classic-script.js b/third_party/web_platform_tests/service-workers/service-worker/resources/imported-classic-script.js
new file mode 100644
index 0000000..5fc5204
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/imported-classic-script.js
@@ -0,0 +1 @@
+const imported = 'A classic script.';
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/imported-module-script.js b/third_party/web_platform_tests/service-workers/service-worker/resources/imported-module-script.js
new file mode 100644
index 0000000..56d196d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/imported-module-script.js
@@ -0,0 +1 @@
+export const imported = 'A module script.';
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/indexeddb-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/indexeddb-worker.js
new file mode 100644
index 0000000..9add476
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/indexeddb-worker.js
@@ -0,0 +1,57 @@
+self.addEventListener('message', function(e) {
+ var message = e.data;
+ if (message.action === 'create') {
+ e.waitUntil(deleteDB()
+ .then(doIndexedDBTest)
+ .then(function() {
+ message.port.postMessage({ type: 'created' });
+ })
+ .catch(function(reason) {
+ message.port.postMessage({ type: 'error', value: reason });
+ }));
+ } else if (message.action === 'cleanup') {
+ e.waitUntil(deleteDB()
+ .then(function() {
+ message.port.postMessage({ type: 'done' });
+ })
+ .catch(function(reason) {
+ message.port.postMessage({ type: 'error', value: reason });
+ }));
+ }
+ });
+
+function deleteDB() {
+ return new Promise(function(resolve, reject) {
+ var delete_request = indexedDB.deleteDatabase('db');
+
+ delete_request.onsuccess = resolve;
+ delete_request.onerror = reject;
+ });
+}
+
+function doIndexedDBTest(port) {
+ return new Promise(function(resolve, reject) {
+ var open_request = indexedDB.open('db');
+
+ open_request.onerror = reject;
+ open_request.onupgradeneeded = function() {
+ var db = open_request.result;
+ db.createObjectStore('store');
+ };
+ open_request.onsuccess = function() {
+ var db = open_request.result;
+ var tx = db.transaction('store', 'readwrite');
+ var store = tx.objectStore('store');
+ store.put('value', 'key');
+
+ tx.onerror = function() {
+ db.close();
+ reject(tx.error);
+ };
+ tx.oncomplete = function() {
+ db.close();
+ resolve();
+ };
+ };
+ });
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/install-event-type-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/install-event-type-worker.js
new file mode 100644
index 0000000..1c94ae2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/install-event-type-worker.js
@@ -0,0 +1,9 @@
+importScripts('worker-testharness.js');
+
+self.oninstall = function(event) {
+ assert_true(event instanceof ExtendableEvent, 'instance of ExtendableEvent');
+ assert_true(event instanceof InstallEvent, 'instance of InstallEvent');
+ assert_equals(event.type, 'install', '`type` property value');
+ assert_false(event.cancelable, '`cancelable` property value');
+ assert_false(event.bubbles, '`bubbles` property value');
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/install-worker.html b/third_party/web_platform_tests/service-workers/service-worker/resources/install-worker.html
new file mode 100644
index 0000000..ed20cd4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/install-worker.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<body>
+<p>Loading...</p>
+<script>
+async function install() {
+ let script;
+ for (const q of location.search.slice(1).split('&')) {
+ if (q.split('=')[0] === 'script') {
+ script = q.split('=')[1];
+ }
+ }
+ const scope = location.href;
+ const reg = await navigator.serviceWorker.register(script, {scope});
+ await navigator.serviceWorker.ready;
+ location.reload();
+}
+
+install();
+</script>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/interface-requirements-worker.sub.js b/third_party/web_platform_tests/service-workers/service-worker/resources/interface-requirements-worker.sub.js
new file mode 100644
index 0000000..a3f239b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/interface-requirements-worker.sub.js
@@ -0,0 +1,59 @@
+'use strict';
+
+// This file checks additional interface requirements, on top of the basic IDL
+// that is validated in service-workers/idlharness.any.js
+
+importScripts('/resources/testharness.js');
+
+test(function() {
+ var req = new Request('http://{{host}}/',
+ {method: 'POST',
+ headers: [['Content-Type', 'Text/Html']]});
+ assert_equals(
+ new ExtendableEvent('ExtendableEvent').type,
+ 'ExtendableEvent', 'Type of ExtendableEvent should be ExtendableEvent');
+ assert_throws_js(TypeError, function() {
+ new FetchEvent('FetchEvent');
+ }, 'FetchEvent constructor with one argument throws');
+ assert_throws_js(TypeError, function() {
+ new FetchEvent('FetchEvent', {});
+ }, 'FetchEvent constructor with empty init dict throws');
+ assert_throws_js(TypeError, function() {
+ new FetchEvent('FetchEvent', {request: null});
+ }, 'FetchEvent constructor with null request member throws');
+ assert_equals(
+ new FetchEvent('FetchEvent', {request: req}).type,
+ 'FetchEvent', 'Type of FetchEvent should be FetchEvent');
+ assert_equals(
+ new FetchEvent('FetchEvent', {request: req}).cancelable,
+ false, 'Default FetchEvent.cancelable should be false');
+ assert_equals(
+ new FetchEvent('FetchEvent', {request: req}).bubbles,
+ false, 'Default FetchEvent.bubbles should be false');
+ assert_equals(
+ new FetchEvent('FetchEvent', {request: req}).clientId,
+ '', 'Default FetchEvent.clientId should be the empty string');
+ assert_equals(
+ new FetchEvent('FetchEvent', {request: req, cancelable: false}).cancelable,
+ false, 'FetchEvent.cancelable should be false');
+ assert_equals(
+ new FetchEvent('FetchEvent', {request: req, clientId : 'test-client-id'}).clientId, 'test-client-id',
+ 'FetchEvent.clientId with option {clientId : "test-client-id"} should be "test-client-id"');
+ assert_equals(
+ new FetchEvent('FetchEvent', {request : req}).request.url,
+ 'http://{{host}}/',
+ 'FetchEvent.request.url should return the value it was initialized to');
+ assert_equals(
+ new FetchEvent('FetchEvent', {request : req}).isReload,
+ undefined,
+ 'FetchEvent.isReload should not exist');
+
+ }, 'Event constructors');
+
+test(() => {
+ assert_false('XMLHttpRequest' in self);
+ }, 'xhr is not exposed');
+
+test(() => {
+ assert_false('createObjectURL' in self.URL);
+ }, 'URL.createObjectURL is not exposed')
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-blobtype-iframe.https.html b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-blobtype-iframe.https.html
new file mode 100644
index 0000000..04a9cb5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-blobtype-iframe.https.html
@@ -0,0 +1,28 @@
+<script src="test-helpers.sub.js"></script>
+<script>
+
+function xhr_send(method, data) {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.onload = function() {
+ if (xhr.getResponseHeader('Content-Type') !== null) {
+ reject('Content-Type must be null.');
+ }
+ resolve();
+ };
+ xhr.onerror = function() {
+ reject('XHR must succeed.');
+ };
+ xhr.responseType = 'text';
+ xhr.open(method, './sample?test', true);
+ xhr.send(data);
+ });
+}
+
+window.addEventListener('message', function(evt) {
+ var port = evt.ports[0];
+ xhr_send('POST', 'test string')
+ .then(function() { port.postMessage({results: 'finish'}); })
+ .catch(function(e) { port.postMessage({results: 'failure:' + e}); });
+ });
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-blobtype-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-blobtype-worker.js
new file mode 100644
index 0000000..865dc30
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-blobtype-worker.js
@@ -0,0 +1,10 @@
+self.addEventListener('fetch', function(event) {
+ var url = event.request.url;
+ if (url.indexOf('sample?test') == -1) {
+ return;
+ }
+ event.respondWith(new Promise(function(resolve) {
+ // null byte in blob type
+ resolve(new Response(new Blob([],{type: 'a\0b'})));
+ }));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-chunked-encoding-with-flush.py b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-chunked-encoding-with-flush.py
new file mode 100644
index 0000000..05977c6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-chunked-encoding-with-flush.py
@@ -0,0 +1,9 @@
+import time
+def main(request, response):
+ response.headers.set(b"Content-Type", b"application/javascript")
+ response.headers.set(b"Transfer-encoding", b"chunked")
+ response.write_status_headers()
+
+ time.sleep(1)
+
+ response.writer.write(b"XX\r\n\r\n")
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-chunked-encoding.py b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-chunked-encoding.py
new file mode 100644
index 0000000..a8edd06
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-chunked-encoding.py
@@ -0,0 +1,2 @@
+def main(request, response):
+ return [(b"Content-Type", b"application/javascript"), (b"Transfer-encoding", b"chunked")], b"XX\r\n\r\n"
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-header-iframe.https.html b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-header-iframe.https.html
new file mode 100644
index 0000000..8f0e6ba
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-header-iframe.https.html
@@ -0,0 +1,25 @@
+<script src="test-helpers.sub.js"></script>
+<script>
+
+function xhr_send(method, data) {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.onload = function() {
+ reject('XHR must fail.');
+ };
+ xhr.onerror = function() {
+ resolve();
+ };
+ xhr.responseType = 'text';
+ xhr.open(method, './sample?test', true);
+ xhr.send(data);
+ });
+}
+
+window.addEventListener('message', function(evt) {
+ var port = evt.ports[0];
+ xhr_send('POST', 'test string')
+ .then(function() { port.postMessage({results: 'finish'}); })
+ .catch(function(e) { port.postMessage({results: 'failure:' + e}); });
+ });
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-header-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-header-worker.js
new file mode 100644
index 0000000..850874b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/invalid-header-worker.js
@@ -0,0 +1,12 @@
+self.addEventListener('fetch', function(event) {
+ var url = event.request.url;
+ if (url.indexOf('sample?test') == -1) {
+ return;
+ }
+ event.respondWith(new Promise(function(resolve) {
+ var headers = new Headers;
+ headers.append('foo', 'foo');
+ headers.append('foo', 'b\0r'); // header value with a null byte
+ resolve(new Response('hello world', {'headers': headers}));
+ }));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/iso-latin1-header-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/iso-latin1-header-iframe.html
new file mode 100644
index 0000000..cf2fa8d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/iso-latin1-header-iframe.html
@@ -0,0 +1,23 @@
+<script>
+function xhr_send(method, data) {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.onload = function() {
+ resolve();
+ };
+ xhr.onerror = function() {
+ reject('XHR must succeed.');
+ };
+ xhr.responseType = 'text';
+ xhr.open(method, './sample?test', true);
+ xhr.send(data);
+ });
+}
+
+window.addEventListener('message', function(evt) {
+ var port = evt.ports[0];
+ xhr_send('POST', 'test string')
+ .then(function() { port.postMessage({results: 'finish'}); })
+ .catch(function(e) { port.postMessage({results: 'failure:' + e}); });
+ });
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/iso-latin1-header-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/iso-latin1-header-worker.js
new file mode 100644
index 0000000..d9ecca2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/iso-latin1-header-worker.js
@@ -0,0 +1,12 @@
+self.addEventListener('fetch', function(event) {
+ var url = event.request.url;
+ if (url.indexOf('sample?test') == -1) {
+ return;
+ }
+
+ event.respondWith(new Promise(function(resolve) {
+ var headers = new Headers;
+ headers.append('TEST', 'ßÀ¿'); // header value holds the Latin1 (ISO8859-1) string.
+ resolve(new Response('hello world', {'headers': headers}));
+ }));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/load_worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/load_worker.js
new file mode 100644
index 0000000..18c673b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/load_worker.js
@@ -0,0 +1,29 @@
+function run_test(data, sender) {
+ if (data === 'xhr') {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', 'synthesized-response.txt', true);
+ xhr.responseType = 'text';
+ xhr.send();
+ xhr.onload = evt => sender.postMessage(xhr.responseText);
+ xhr.onerror = () => sender.postMessage('XHR failed!');
+ } else if (data === 'fetch') {
+ fetch('synthesized-response.txt')
+ .then(response => response.text())
+ .then(data => sender.postMessage(data))
+ .catch(error => sender.postMessage('Fetch failed!'));
+ } else if (data === 'importScripts') {
+ importScripts('synthesized-response.js');
+ // |message| is provided by 'synthesized-response.js';
+ sender.postMessage(message);
+ } else {
+ sender.postMessage('Unexpected message! ' + data);
+ }
+}
+
+// Entry point for dedicated workers.
+self.onmessage = evt => run_test(evt.data, self);
+
+// Entry point for shared workers.
+self.onconnect = evt => {
+ evt.ports[0].onmessage = e => run_test(e.data, evt.ports[0]);
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/loaded.html b/third_party/web_platform_tests/service-workers/service-worker/resources/loaded.html
new file mode 100644
index 0000000..0cabce6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/loaded.html
@@ -0,0 +1,9 @@
+<script>
+addEventListener('load', function() {
+ opener.postMessage({ type: 'LOADED' }, '*');
+});
+
+addEventListener('pageshow', function() {
+ opener.postMessage({ type: 'PAGESHOW' }, '*');
+});
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/local-url-inherit-controller-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/local-url-inherit-controller-frame.html
new file mode 100644
index 0000000..5520c3a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/local-url-inherit-controller-frame.html
@@ -0,0 +1,130 @@
+<!DOCTYPE html>
+<html>
+<script>
+
+const fetchURL = new URL('sample.txt', window.location).href;
+
+const frameControllerText =
+`<script>
+ let t = null;
+ try {
+ if (navigator.serviceWorker.controller) {
+ t = navigator.serviceWorker.controller.scriptURL;
+ }
+ } catch (e) {
+ t = e.message;
+ } finally {
+ parent.postMessage({ data: t }, '*');
+ }
+</` + `script>`;
+
+const frameFetchText =
+`<script>
+ fetch('${fetchURL}', { mode: 'no-cors' }).then(response => {
+ return response.text();
+ }).then(text => {
+ parent.postMessage({ data: text }, '*');
+ }).catch(e => {
+ parent.postMessage({ data: e.message }, '*');
+ });
+</` + `script>`;
+
+const workerControllerText =
+`let t = navigator.serviceWorker.controller
+ ? navigator.serviceWorker.controller.scriptURL
+ : null;
+self.postMessage(t);`;
+
+const workerFetchText =
+`fetch('${fetchURL}', { mode: 'no-cors' }).then(response => {
+ return response.text();
+}).then(text => {
+ self.postMessage(text);
+}).catch(e => {
+ self.postMessage(e.message);
+});`
+
+function getChildText(opts) {
+ if (opts.child === 'iframe') {
+ if (opts.check === 'controller') {
+ return frameControllerText;
+ }
+
+ if (opts.check === 'fetch') {
+ return frameFetchText;
+ }
+
+ throw('unexpected feature to check: ' + opts.check);
+ }
+
+ if (opts.child === 'worker') {
+ if (opts.check === 'controller') {
+ return workerControllerText;
+ }
+
+ if (opts.check === 'fetch') {
+ return workerFetchText;
+ }
+
+ throw('unexpected feature to check: ' + opts.check);
+ }
+
+ throw('unexpected child type ' + opts.child);
+}
+
+function makeURL(opts) {
+ let mimetype = opts.child === 'iframe' ? 'text/html'
+ : 'text/javascript';
+
+ if (opts.scheme === 'blob') {
+ let blob = new Blob([getChildText(opts)], { type: mimetype });
+ return URL.createObjectURL(blob);
+ }
+
+ if (opts.scheme === 'data') {
+ return `data:${mimetype},${getChildText(opts)}`;
+ }
+
+ throw(`unexpected URL scheme ${opts.scheme}`);
+}
+
+function testWorkerChild(url) {
+ let w = new Worker(url);
+ return new Promise((resolve, reject) => {
+ w.onmessage = resolve;
+ w.onerror = evt => {
+ reject(evt.message);
+ }
+ });
+}
+
+function testIframeChild(url) {
+ let frame = document.createElement('iframe');
+ frame.src = url;
+ document.body.appendChild(frame);
+
+ return new Promise(resolve => {
+ addEventListener('message', evt => {
+ resolve(evt.data);
+ }, { once: true });
+ });
+}
+
+function testURL(opts, url) {
+ if (opts.child === 'worker') {
+ return testWorkerChild(url);
+ }
+
+ if (opts.child === 'iframe') {
+ return testIframeChild(url);
+ }
+
+ throw(`unexpected child type ${opts.child}`);
+}
+
+function checkChildController(opts) {
+ let url = makeURL(opts);
+ return testURL(opts, url);
+}
+</script>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/local-url-inherit-controller-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/local-url-inherit-controller-worker.js
new file mode 100644
index 0000000..4b7aad0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/local-url-inherit-controller-worker.js
@@ -0,0 +1,5 @@
+addEventListener('fetch', evt => {
+ if (evt.request.url.includes('sample')) {
+ evt.respondWith(new Response('intercepted'));
+ }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/location-setter.html b/third_party/web_platform_tests/service-workers/service-worker/resources/location-setter.html
new file mode 100644
index 0000000..f0ced06
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/location-setter.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="referrer" content="origin">
+<script>
+function onLoad() {
+ const params = new URLSearchParams(self.location.search);
+ self.location = params.get('target');
+}
+self.addEventListener('load', onLoad);
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/malformed-http-response.asis b/third_party/web_platform_tests/service-workers/service-worker/resources/malformed-http-response.asis
new file mode 100644
index 0000000..bc3c68d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/malformed-http-response.asis
@@ -0,0 +1 @@
+HAHAHA THIS IS NOT HTTP AND THE BROWSER SHOULD CONSIDER IT A NETWORK ERROR
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/malformed-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/malformed-worker.py
new file mode 100644
index 0000000..319b6e2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/malformed-worker.py
@@ -0,0 +1,14 @@
+def main(request, response):
+ headers = [(b"Content-Type", b"application/javascript")]
+
+ body = {u'parse-error': u'var foo = function() {;',
+ u'undefined-error': u'foo.bar = 42;',
+ u'uncaught-exception': u'throw new DOMException("AbortError");',
+ u'caught-exception': u'try { throw new Error; } catch(e) {}',
+ u'import-malformed-script': u'importScripts("malformed-worker.py?parse-error");',
+ u'import-no-such-script': u'importScripts("no-such-script.js");',
+ u'top-level-await': u'await Promise.resolve(1);',
+ u'instantiation-error': u'import nonexistent from "./imported-module-script.js";',
+ u'instantiation-error-and-top-level-await': u'import nonexistent from "./imported-module-script.js"; await Promise.resolve(1);'}[request.url_parts.query]
+
+ return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/message-vs-microtask.html b/third_party/web_platform_tests/service-workers/service-worker/resources/message-vs-microtask.html
new file mode 100644
index 0000000..2c45c59
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/message-vs-microtask.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<script>
+ let draft = [];
+ var resolve_manual_promise;
+ let manual_promise =
+ new Promise(resolve => resolve_manual_promise = resolve).then(() => draft.push('microtask'));
+
+ let resolve_message_promise;
+ let message_promise = new Promise(resolve => resolve_message_promise = resolve);
+ function handle_message(event) {
+ draft.push('message');
+ resolve_message_promise();
+ }
+
+ var result = Promise.all([manual_promise, message_promise]).then(() => draft);
+</script>
+
+<script src="empty.js?key=start"></script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/mime-sniffing-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/mime-sniffing-worker.js
new file mode 100644
index 0000000..5c34a7a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/mime-sniffing-worker.js
@@ -0,0 +1,9 @@
+self.addEventListener('fetch', function(event) {
+ // Use an empty content-type value to force mime-sniffing. Note, this
+ // must be passed to the constructor since the mime-type of the Response
+ // is fixed and cannot be later changed.
+ var res = new Response('<!DOCTYPE html>\n<h1 id=\'testid\'>test</h1>', {
+ headers: { 'content-type': '' }
+ });
+ event.respondWith(res);
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/mime-type-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/mime-type-worker.py
new file mode 100644
index 0000000..92a602e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/mime-type-worker.py
@@ -0,0 +1,4 @@
+def main(request, response):
+ if b'mime' in request.GET:
+ return [(b'Content-Type', request.GET[b'mime'])], b""
+ return [], b""
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/mint-new-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/mint-new-worker.py
new file mode 100644
index 0000000..ebee4ff
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/mint-new-worker.py
@@ -0,0 +1,27 @@
+import random
+
+import time
+
+body = u'''
+onactivate = (e) => e.waitUntil(clients.claim());
+var resolve_wait_until;
+var wait_until = new Promise(resolve => {
+ resolve_wait_until = resolve;
+ });
+onmessage = (e) => {
+ if (e.data == 'wait')
+ e.waitUntil(wait_until);
+ if (e.data == 'go')
+ resolve_wait_until();
+ };'''
+
+def main(request, response):
+ headers = [(b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache'),
+ (b'Content-Type', b'application/javascript')]
+
+ skipWaiting = u''
+ if b'skip-waiting' in request.GET:
+ skipWaiting = u'skipWaiting();'
+
+ return headers, u'/* %s %s */ %s %s' % (time.time(), random.random(), skipWaiting, body)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/module-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/module-worker.js
new file mode 100644
index 0000000..385fe71
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/module-worker.js
@@ -0,0 +1 @@
+import * as module from './imported-module-script.js';
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/multipart-image-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/multipart-image-iframe.html
new file mode 100644
index 0000000..c59b955
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/multipart-image-iframe.html
@@ -0,0 +1,19 @@
+<script>
+function load_multipart_image(src) {
+ return new Promise((resolve, reject) => {
+ const img = document.createElement('img');
+ img.addEventListener('load', () => resolve(img));
+ img.addEventListener('error', (e) => reject(new DOMException('load failed', 'NetworkError')));
+ img.src = src;
+ });
+}
+
+function get_image_data(img) {
+ const canvas = document.createElement('canvas');
+ const context = canvas.getContext('2d');
+ context.drawImage(img, 0, 0);
+ // When |img.src| is cross origin, this should throw a SecurityError.
+ const imageData = context.getImageData(0, 0, 1, 1);
+ return imageData;
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/multipart-image-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/multipart-image-worker.js
new file mode 100644
index 0000000..a38fe54
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/multipart-image-worker.js
@@ -0,0 +1,21 @@
+importScripts('/common/get-host-info.sub.js');
+importScripts('test-helpers.sub.js');
+
+const host_info = get_host_info();
+
+const multipart_image_path = base_path() + 'multipart-image.py';
+const sameorigin_url = host_info['HTTPS_ORIGIN'] + multipart_image_path;
+const cross_origin_url = host_info['HTTPS_REMOTE_ORIGIN'] + multipart_image_path;
+
+self.addEventListener('fetch', event => {
+ const url = event.request.url;
+ if (url.indexOf('cross-origin-multipart-image-with-no-cors') >= 0) {
+ event.respondWith(fetch(cross_origin_url, {mode: 'no-cors'}));
+ } else if (url.indexOf('cross-origin-multipart-image-with-cors-rejected') >= 0) {
+ event.respondWith(fetch(cross_origin_url, {mode: 'cors'}));
+ } else if (url.indexOf('cross-origin-multipart-image-with-cors-approved') >= 0) {
+ event.respondWith(fetch(cross_origin_url + '?approvecors', {mode: 'cors'}));
+ } else if (url.indexOf('same-origin-multipart-image') >= 0) {
+ event.respondWith(fetch(sameorigin_url));
+ }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/multipart-image.py b/third_party/web_platform_tests/service-workers/service-worker/resources/multipart-image.py
new file mode 100644
index 0000000..9a3c035
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/multipart-image.py
@@ -0,0 +1,23 @@
+# A request handler that serves a multipart image.
+
+import os
+
+
+BOUNDARY = b'cutHere'
+
+
+def create_part(path):
+ with open(path, u'rb') as f:
+ return b'Content-Type: image/png\r\n\r\n' + f.read() + b'--%s' % BOUNDARY
+
+
+def main(request, response):
+ content_type = b'multipart/x-mixed-replace; boundary=%s' % BOUNDARY
+ headers = [(b'Content-Type', content_type)]
+ if b'approvecors' in request.GET:
+ headers.append((b'Access-Control-Allow-Origin', b'*'))
+
+ image_path = os.path.join(request.doc_root, u'images')
+ body = create_part(os.path.join(image_path, u'red.png'))
+ body = body + create_part(os.path.join(image_path, u'red-16x16.png'))
+ return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigate-window-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/navigate-window-worker.js
new file mode 100644
index 0000000..f961743
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigate-window-worker.js
@@ -0,0 +1,21 @@
+addEventListener('message', function(evt) {
+ if (evt.data.type === 'GET_CLIENTS') {
+ clients.matchAll(evt.data.opts).then(function(clientList) {
+ var resultList = clientList.map(function(c) {
+ return { url: c.url, frameType: c.frameType, id: c.id };
+ });
+ evt.source.postMessage({ type: 'success', detail: resultList });
+ }).catch(function(err) {
+ evt.source.postMessage({
+ type: 'failure',
+ detail: 'matchAll() rejected with "' + err + '"'
+ });
+ });
+ return;
+ }
+
+ evt.source.postMessage({
+ type: 'failure',
+ detail: 'Unexpected message type "' + evt.data.type + '"'
+ });
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-headers-server.py b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-headers-server.py
new file mode 100644
index 0000000..5b2e044
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-headers-server.py
@@ -0,0 +1,19 @@
+def main(request, response):
+ response.status = (200, b"OK")
+ response.headers.set(b"Content-Type", b"text/html")
+ return b"""
+ <script>
+ self.addEventListener('load', evt => {
+ self.parent.postMessage({
+ origin: '%s',
+ referer: '%s',
+ 'sec-fetch-site': '%s',
+ 'sec-fetch-mode': '%s',
+ 'sec-fetch-dest': '%s',
+ });
+ });
+ </script>""" % (request.headers.get(
+ b"origin", b"not set"), request.headers.get(b"referer", b"not set"),
+ request.headers.get(b"sec-fetch-site", b"not set"),
+ request.headers.get(b"sec-fetch-mode", b"not set"),
+ request.headers.get(b"sec-fetch-dest", b"not set"))
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-body-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-body-worker.js
new file mode 100644
index 0000000..39f11ba
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-body-worker.js
@@ -0,0 +1,11 @@
+self.addEventListener('fetch', function(event) {
+ event.respondWith(
+ fetch(event.request)
+ .then(
+ function(response) {
+ return response;
+ },
+ function(error) {
+ return new Response('Error:' + error);
+ }));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-body.py b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-body.py
new file mode 100644
index 0000000..d10329e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-body.py
@@ -0,0 +1,11 @@
+import os
+
+from wptserve.utils import isomorphic_encode
+
+filename = os.path.basename(isomorphic_encode(__file__))
+
+def main(request, response):
+ if request.method == u'POST':
+ return 302, [(b'Location', b'./%s?redirect' % filename)], b''
+
+ return [(b'Content-Type', b'text/plain')], request.request_path
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-other-origin.html b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-other-origin.html
new file mode 100644
index 0000000..d82571d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-other-origin.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js"></script>
+<script>
+var host_info = get_host_info();
+var SCOPE = 'navigation-redirect-scope1.py';
+var SCRIPT = 'redirect-worker.js';
+
+var registration;
+var worker;
+var wait_for_worker_promise = navigator.serviceWorker.getRegistration(SCOPE)
+ .then(function(reg) {
+ if (reg)
+ return reg.unregister();
+ })
+ .then(function() {
+ return navigator.serviceWorker.register(SCRIPT, {scope: SCOPE});
+ })
+ .then(function(reg) {
+ registration = reg;
+ worker = reg.installing;
+ return new Promise(function(resolve) {
+ worker.addEventListener('statechange', function() {
+ if (worker.state == 'activated')
+ resolve();
+ });
+ });
+ });
+
+function send_result(message_id, result) {
+ window.parent.postMessage(
+ {id: message_id, result: result},
+ host_info['HTTPS_ORIGIN']);
+}
+
+function get_request_infos(worker) {
+ return new Promise(function(resolve) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = (msg) => {
+ resolve(msg.data.requestInfos);
+ };
+ worker.postMessage({command: 'getRequestInfos', port: channel.port2},
+ [channel.port2]);
+ });
+}
+
+function get_clients(worker, actual_ids) {
+ return new Promise(function(resolve) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = (msg) => {
+ resolve(msg.data.clients);
+ };
+ worker.postMessage({
+ command: 'getClients',
+ actual_ids,
+ port: channel.port2
+ }, [channel.port2]);
+ });
+}
+
+window.addEventListener('message', on_message, false);
+
+function on_message(e) {
+ if (e.origin != host_info['HTTPS_ORIGIN']) {
+ console.error('invalid origin: ' + e.origin);
+ return;
+ }
+ const command = e.data.message.command;
+ if (command == 'wait_for_worker') {
+ wait_for_worker_promise.then(function() { send_result(e.data.id, 'ok'); });
+ } else if (command == 'get_request_infos') {
+ get_request_infos(worker)
+ .then(function(data) {
+ send_result(e.data.id, data);
+ });
+ } else if (command == 'get_clients') {
+ get_clients(worker, e.data.message.actual_ids)
+ .then(function(data) {
+ send_result(e.data.id, data);
+ });
+ } else if (command == 'unregister') {
+ registration.unregister()
+ .then(function() {
+ send_result(e.data.id, 'ok');
+ });
+ }
+}
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-out-scope.py b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-out-scope.py
new file mode 100644
index 0000000..9b90b14
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-out-scope.py
@@ -0,0 +1,22 @@
+def main(request, response):
+ if b"url" in request.GET:
+ headers = [(b"Location", request.GET[b"url"])]
+ return 302, headers, b''
+
+ status = 200
+
+ if b"noLocationRedirect" in request.GET:
+ status = 302
+
+ return status, [(b"content-type", b"text/html")], b'''
+<!DOCTYPE html>
+<script>
+onmessage = event => {
+ window.parent.postMessage(
+ {
+ id: event.data.id,
+ result: location.href
+ }, '*');
+};
+</script>
+'''
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-scope1.py b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-scope1.py
new file mode 100644
index 0000000..9b90b14
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-scope1.py
@@ -0,0 +1,22 @@
+def main(request, response):
+ if b"url" in request.GET:
+ headers = [(b"Location", request.GET[b"url"])]
+ return 302, headers, b''
+
+ status = 200
+
+ if b"noLocationRedirect" in request.GET:
+ status = 302
+
+ return status, [(b"content-type", b"text/html")], b'''
+<!DOCTYPE html>
+<script>
+onmessage = event => {
+ window.parent.postMessage(
+ {
+ id: event.data.id,
+ result: location.href
+ }, '*');
+};
+</script>
+'''
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-scope2.py b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-scope2.py
new file mode 100644
index 0000000..9b90b14
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-scope2.py
@@ -0,0 +1,22 @@
+def main(request, response):
+ if b"url" in request.GET:
+ headers = [(b"Location", request.GET[b"url"])]
+ return 302, headers, b''
+
+ status = 200
+
+ if b"noLocationRedirect" in request.GET:
+ status = 302
+
+ return status, [(b"content-type", b"text/html")], b'''
+<!DOCTYPE html>
+<script>
+onmessage = event => {
+ window.parent.postMessage(
+ {
+ id: event.data.id,
+ result: location.href
+ }, '*');
+};
+</script>
+'''
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-to-http-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-to-http-iframe.html
new file mode 100644
index 0000000..40e27c6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-to-http-iframe.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js"></script>
+<script>
+var SCOPE = './redirect.py?Redirect=' + encodeURI('http://example.com');
+var SCRIPT = 'navigation-redirect-to-http-worker.js';
+var host_info = get_host_info();
+
+navigator.serviceWorker.getRegistration(SCOPE)
+ .then(function(registration) {
+ if (registration)
+ return registration.unregister();
+ })
+ .then(function() {
+ return navigator.serviceWorker.register(SCRIPT, {scope: SCOPE});
+ })
+ .then(function(registration) {
+ return new Promise(function(resolve) {
+ registration.addEventListener('updatefound', function() {
+ resolve(registration.installing);
+ });
+ });
+ })
+ .then(function(worker) {
+ worker.addEventListener('statechange', on_state_change);
+ })
+ .catch(function(reason) {
+ window.parent.postMessage({results: 'FAILURE: ' + reason.message},
+ host_info['HTTPS_ORIGIN']);
+ });
+
+function on_state_change(event) {
+ if (event.target.state != 'activated')
+ return;
+ with_iframe(SCOPE, {auto_remove: false})
+ .then(function(frame) {
+ window.parent.postMessage(
+ {results: frame.contentDocument.body.textContent},
+ host_info['HTTPS_ORIGIN']);
+ });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-to-http-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-to-http-worker.js
new file mode 100644
index 0000000..6f2a8ae
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-redirect-to-http-worker.js
@@ -0,0 +1,22 @@
+importScripts('/resources/testharness.js');
+
+self.addEventListener('fetch', function(event) {
+ event.respondWith(new Promise(function(resolve) {
+ Promise.resolve()
+ .then(function() {
+ assert_equals(
+ event.request.redirect, 'manual',
+ 'The redirect mode of navigation request must be manual.');
+ return fetch(event.request);
+ })
+ .then(function(response) {
+ assert_equals(
+ response.type, 'opaqueredirect',
+ 'The response type of 302 response must be opaqueredirect.');
+ resolve(new Response('OK'));
+ })
+ .catch(function(error) {
+ resolve(new Response('Failed in SW: ' + error));
+ });
+ }));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-timing-worker-extended.js b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-timing-worker-extended.js
new file mode 100644
index 0000000..79c5408
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-timing-worker-extended.js
@@ -0,0 +1,22 @@
+importScripts("/resources/testharness.js");
+const timings = {}
+
+const DELAY_ACTIVATION = 500
+
+self.addEventListener('activate', event => {
+ event.waitUntil(new Promise(resolve => {
+ timings.activateWorkerStart = performance.now() + performance.timeOrigin;
+
+ // This gives us enough time to ensure activation would delay fetch handling
+ step_timeout(resolve, DELAY_ACTIVATION);
+ }).then(() => timings.activateWorkerEnd = performance.now() + performance.timeOrigin));
+})
+
+self.addEventListener('fetch', event => {
+ timings.handleFetchEvent = performance.now() + performance.timeOrigin;
+ event.respondWith(Promise.resolve(new Response(new Blob([`
+ <script>
+ parent.postMessage(${JSON.stringify(timings)}, "*")
+ </script>
+ `]))));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-timing-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-timing-worker.js
new file mode 100644
index 0000000..8539b40
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/navigation-timing-worker.js
@@ -0,0 +1,15 @@
+self.addEventListener('fetch', (event) => {
+ const url = event.request.url;
+
+ // Network fallback.
+ if (url.indexOf('network-fallback') >= 0) {
+ return;
+ }
+
+ // Don't intercept redirect.
+ if (url.indexOf('redirect.py') >= 0) {
+ return;
+ }
+
+ event.respondWith(fetch(url));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/nested-blob-url-worker-created-from-worker.html b/third_party/web_platform_tests/service-workers/service-worker/resources/nested-blob-url-worker-created-from-worker.html
new file mode 100644
index 0000000..fc048e2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/nested-blob-url-worker-created-from-worker.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<script>
+const baseLocation = window.location;
+const workerUrl = new URL('create-blob-url-worker.js', baseLocation).href;
+const worker = new Worker(workerUrl);
+
+function fetch_in_worker(url) {
+ const resourceUrl = new URL(url, baseLocation).href;
+ return new Promise((resolve) => {
+ worker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ worker.postMessage(resourceUrl);
+ });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/nested-blob-url-workers.html b/third_party/web_platform_tests/service-workers/service-worker/resources/nested-blob-url-workers.html
new file mode 100644
index 0000000..f0eafcd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/nested-blob-url-workers.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<script>
+const baseLocation = window.location;
+const parentWorkerScript = `
+ const childWorkerScript = 'self.onmessage = async (e) => {' +
+ ' const response = await fetch(e.data);' +
+ ' const text = await response.text();' +
+ ' self.postMessage(text);' +
+ '};';
+ const blob = new Blob([childWorkerScript], { type: 'text/javascript' });
+ const blobUrl = URL.createObjectURL(blob);
+ const childWorker = new Worker(blobUrl);
+
+ // When a message comes from the parent frame, sends a resource url to the
+ // child worker.
+ self.onmessage = (e) => {
+ childWorker.postMessage(e.data);
+ };
+ // When a message comes from the child worker, sends a content of fetch() to
+ // the parent frame.
+ childWorker.onmessage = (e) => {
+ self.postMessage(e.data);
+ };
+`;
+const blob = new Blob([parentWorkerScript], { type: 'text/javascript' });
+const blobUrl = URL.createObjectURL(blob);
+const worker = new Worker(blobUrl);
+
+function fetch_in_worker(url) {
+ const resourceUrl = new URL(url, baseLocation).href;
+ return new Promise((resolve) => {
+ worker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ worker.postMessage(resourceUrl);
+ });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/nested-iframe-parent.html b/third_party/web_platform_tests/service-workers/service-worker/resources/nested-iframe-parent.html
new file mode 100644
index 0000000..115ab26
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/nested-iframe-parent.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<script>
+ navigator.serviceWorker.onmessage = event => parent.postMessage(event.data, '*', event.ports);
+</script>
+<iframe id='child'></iframe>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/nested-parent.html b/third_party/web_platform_tests/service-workers/service-worker/resources/nested-parent.html
new file mode 100644
index 0000000..b4832d4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/nested-parent.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="referrer" content="origin">
+<script>
+async function onLoad() {
+ self.addEventListener('message', evt => {
+ if (self.opener)
+ self.opener.postMessage(evt.data, '*');
+ else
+ self.top.postMessage(evt.data, '*');
+ }, { once: true });
+ const params = new URLSearchParams(self.location.search);
+ const frame = document.createElement('iframe');
+ frame.src = params.get('target');
+ document.body.appendChild(frame);
+}
+self.addEventListener('load', onLoad);
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/nested-worker-created-from-blob-url-worker.html b/third_party/web_platform_tests/service-workers/service-worker/resources/nested-worker-created-from-blob-url-worker.html
new file mode 100644
index 0000000..3fad2c9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/nested-worker-created-from-blob-url-worker.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<script>
+const baseLocation = window.location;
+const parentWorkerScript = `
+ const workerUrl =
+ new URL('postmessage-fetched-text.js', '${baseLocation}').href;
+ const childWorker = new Worker(workerUrl);
+
+ // When a message comes from the parent frame, sends a resource url to the
+ // child worker.
+ self.onmessage = (e) => {
+ childWorker.postMessage(e.data);
+ };
+ // When a message comes from the child worker, sends a content of fetch() to
+ // the parent frame.
+ childWorker.onmessage = (e) => {
+ self.postMessage(e.data);
+ };
+`;
+const blob = new Blob([parentWorkerScript], { type: 'text/javascript' });
+const blobUrl = URL.createObjectURL(blob);
+const worker = new Worker(blobUrl);
+
+function fetch_in_worker(url) {
+ const resourceUrl = new URL(url, baseLocation).href;
+ return new Promise((resolve) => {
+ worker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ worker.postMessage(resourceUrl);
+ });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/nested_load_worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/nested_load_worker.js
new file mode 100644
index 0000000..ef0ed8f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/nested_load_worker.js
@@ -0,0 +1,23 @@
+// Entry point for dedicated workers.
+self.onmessage = evt => {
+ try {
+ const worker = new Worker('load_worker.js');
+ worker.onmessage = evt => self.postMessage(evt.data);
+ worker.postMessage(evt.data);
+ } catch (err) {
+ self.postMessage('Unexpected error! ' + err.message);
+ }
+};
+
+// Entry point for shared workers.
+self.onconnect = evt => {
+ evt.ports[0].onmessage = e => {
+ try {
+ const worker = new Worker('load_worker.js');
+ worker.onmessage = e => evt.ports[0].postMessage(e.data);
+ worker.postMessage(evt.data);
+ } catch (err) {
+ evt.ports[0].postMessage('Unexpected error! ' + err.message);
+ }
+ };
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/no-dynamic-import.js b/third_party/web_platform_tests/service-workers/service-worker/resources/no-dynamic-import.js
new file mode 100644
index 0000000..ecedd6c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/no-dynamic-import.js
@@ -0,0 +1,18 @@
+/** @type {[name: string, url: string][]} */
+const importUrlTests = [
+ ["Module URL", "./basic-module.js"],
+ // In no-dynamic-import-in-module.any.js, this module is also statically imported
+ ["Another module URL", "./basic-module-2.js"],
+ [
+ "Module data: URL",
+ "data:text/javascript;charset=utf-8," +
+ encodeURIComponent(`export default 'hello!';`),
+ ],
+];
+
+for (const [name, url] of importUrlTests) {
+ promise_test(
+ (t) => promise_rejects_js(t, TypeError, import(url), "Import must reject"),
+ name
+ );
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/notification_icon.py b/third_party/web_platform_tests/service-workers/service-worker/resources/notification_icon.py
new file mode 100644
index 0000000..71f5a9d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/notification_icon.py
@@ -0,0 +1,11 @@
+from urllib.parse import parse_qs
+
+from wptserve.utils import isomorphic_encode
+
+def main(req, res):
+ qs_cookie_val = parse_qs(req.url_parts.query).get(u'set-cookie-notification')
+
+ if qs_cookie_val:
+ res.set_cookie(b'notification', isomorphic_encode(qs_cookie_val[0]))
+
+ return b'not really an icon'
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/object-image-is-not-intercepted-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/object-image-is-not-intercepted-iframe.html
new file mode 100644
index 0000000..5a20a58
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/object-image-is-not-intercepted-iframe.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe for embed-and-object-are-not-intercepted test</title>
+<body>
+<object type="image/png" data="/images/green.png"></embed>
+<script>
+// Our parent (the root frame of the test) will examine this to get the result.
+var test_promise = new Promise(resolve => {
+ if (!navigator.serviceWorker.controller)
+ resolve('FAIL: this iframe is not controlled');
+
+ const elem = document.querySelector('object');
+ elem.addEventListener('load', e => {
+ resolve('request was not intercepted');
+ });
+ elem.addEventListener('error', e => {
+ resolve('FAIL: request was intercepted');
+ });
+ });
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/object-is-not-intercepted-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/object-is-not-intercepted-iframe.html
new file mode 100644
index 0000000..0aeb819
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/object-is-not-intercepted-iframe.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe for embed-and-object-are-not-intercepted test</title>
+<body>
+<script>
+// The OBJECT element will call this with the result about whether the OBJECT
+// request was intercepted by the service worker.
+var report_result;
+
+// Our parent (the root frame of the test) will examine this to get the result.
+var test_promise = new Promise(resolve => {
+ report_result = resolve;
+ });
+</script>
+
+<object data="embedded-content-from-server.html"></object>
+</body>
+
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/object-navigation-is-not-intercepted-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/object-navigation-is-not-intercepted-iframe.html
new file mode 100644
index 0000000..5c8ab79
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/object-navigation-is-not-intercepted-iframe.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe for embed-and-object-are-not-intercepted test</title>
+<body>
+<script>
+// The OBJECT element will call this with the result about whether the OBJECT
+// request was intercepted by the service worker.
+var report_result;
+
+// Our parent (the root frame of the test) will examine this to get the result.
+var test_promise = new Promise(resolve => {
+ report_result = resolve;
+ });
+
+let el = document.createElement('object');
+el.data = "/common/blank.html";
+el.addEventListener('load', _ => {
+ window[0].location = "/service-workers/service-worker/resources/embedded-content-from-server.html";
+}, { once: true });
+document.body.appendChild(el);
+</script>
+
+</body>
+
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-from-nested-event-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-from-nested-event-worker.js
new file mode 100644
index 0000000..7c97014
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-from-nested-event-worker.js
@@ -0,0 +1,13 @@
+var max_nesting_level = 8;
+
+self.addEventListener('message', function(event) {
+ var level = event.data;
+ if (level < max_nesting_level)
+ dispatchEvent(new MessageEvent('message', { data: level + 1 }));
+ throw Error('error at level ' + level);
+ });
+
+self.addEventListener('activate', function(event) {
+ dispatchEvent(new MessageEvent('message', { data: 1 }));
+ });
+
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-then-cancel-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-then-cancel-worker.js
new file mode 100644
index 0000000..0bd9d31
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-then-cancel-worker.js
@@ -0,0 +1,3 @@
+self.onerror = function(event) { return true; };
+
+self.addEventListener('activate', function(event) { throw new Error(); });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-then-prevent-default-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-then-prevent-default-worker.js
new file mode 100644
index 0000000..d56c951
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-then-prevent-default-worker.js
@@ -0,0 +1,7 @@
+// Ensure we can handle multiple error handlers. One error handler
+// calling preventDefault should cause the event to be treated as
+// handled.
+self.addEventListener('error', function(event) {});
+self.addEventListener('error', function(event) { event.preventDefault(); });
+self.addEventListener('error', function(event) {});
+self.addEventListener('activate', function(event) { throw new Error(); });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-with-empty-onerror-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-with-empty-onerror-worker.js
new file mode 100644
index 0000000..eb12ae8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-with-empty-onerror-worker.js
@@ -0,0 +1,2 @@
+self.addEventListener('error', function(event) {});
+self.addEventListener('activate', function(event) { throw new Error(); });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-worker.js
new file mode 100644
index 0000000..1e88ac5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-throw-error-worker.js
@@ -0,0 +1,7 @@
+// Ensure we can handle multiple activate handlers. One handler throwing an
+// error should cause the event dispatch to be treated as having unhandled
+// errors.
+self.addEventListener('activate', function(event) {});
+self.addEventListener('activate', function(event) {});
+self.addEventListener('activate', function(event) { throw new Error(); });
+self.addEventListener('activate', function(event) {});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-waituntil-forever.js b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-waituntil-forever.js
new file mode 100644
index 0000000..65b02b1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/onactivate-waituntil-forever.js
@@ -0,0 +1,8 @@
+'use strict';
+
+self.addEventListener('activate', event => {
+ event.waitUntil(new Promise(() => {
+ // Use a promise that never resolves to prevent this service worker from
+ // advancing past the 'activating' state.
+ }));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/onfetch-waituntil-forever.js b/third_party/web_platform_tests/service-workers/service-worker/resources/onfetch-waituntil-forever.js
new file mode 100644
index 0000000..b905d55
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/onfetch-waituntil-forever.js
@@ -0,0 +1,10 @@
+'use strict';
+
+self.addEventListener('fetch', event => {
+ if (event.request.url.endsWith('waituntil-forever')) {
+ event.respondWith(new Promise(() => {
+ // Use a promise that never resolves to prevent this fetch from
+ // completing.
+ }));
+ }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-from-nested-event-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-from-nested-event-worker.js
new file mode 100644
index 0000000..6729ab6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-from-nested-event-worker.js
@@ -0,0 +1,12 @@
+var max_nesting_level = 8;
+
+self.addEventListener('message', function(event) {
+ var level = event.data;
+ if (level < max_nesting_level)
+ dispatchEvent(new MessageEvent('message', { data: level + 1 }));
+ throw Error('error at level ' + level);
+ });
+
+self.addEventListener('install', function(event) {
+ dispatchEvent(new MessageEvent('message', { data: 1 }));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-then-cancel-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-then-cancel-worker.js
new file mode 100644
index 0000000..c2c499a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-then-cancel-worker.js
@@ -0,0 +1,3 @@
+self.onerror = function(event) { return true; };
+
+self.addEventListener('install', function(event) { throw new Error(); });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-then-prevent-default-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-then-prevent-default-worker.js
new file mode 100644
index 0000000..7667c27
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-then-prevent-default-worker.js
@@ -0,0 +1,7 @@
+// Ensure we can handle multiple error handlers. One error handler
+// calling preventDefault should cause the event to be treated as
+// handled.
+self.addEventListener('error', function(event) {});
+self.addEventListener('error', function(event) { event.preventDefault(); });
+self.addEventListener('error', function(event) {});
+self.addEventListener('install', function(event) { throw new Error(); });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-with-empty-onerror-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-with-empty-onerror-worker.js
new file mode 100644
index 0000000..8f56d1b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-with-empty-onerror-worker.js
@@ -0,0 +1,2 @@
+self.addEventListener('error', function(event) {});
+self.addEventListener('install', function(event) { throw new Error(); });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-worker.js
new file mode 100644
index 0000000..cc2f6d7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-throw-error-worker.js
@@ -0,0 +1,7 @@
+// Ensure we can handle multiple install handlers. One handler throwing an
+// error should cause the event dispatch to be treated as having unhandled
+// errors.
+self.addEventListener('install', function(event) {});
+self.addEventListener('install', function(event) {});
+self.addEventListener('install', function(event) { throw new Error(); });
+self.addEventListener('install', function(event) {});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-waituntil-forever.js b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-waituntil-forever.js
new file mode 100644
index 0000000..964483f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-waituntil-forever.js
@@ -0,0 +1,8 @@
+'use strict';
+
+self.addEventListener('install', event => {
+ event.waitUntil(new Promise(() => {
+ // Use a promise that never resolves to prevent this service worker from
+ // advancing past the 'installing' state.
+ }));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-waituntil-throw-error-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-waituntil-throw-error-worker.js
new file mode 100644
index 0000000..6cb8f6e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/oninstall-waituntil-throw-error-worker.js
@@ -0,0 +1,5 @@
+self.addEventListener('install', function(event) {
+ event.waitUntil(new Promise(function(aRequest, aResponse) {
+ throw new Error();
+ }));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/onparse-infiniteloop-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/onparse-infiniteloop-worker.js
new file mode 100644
index 0000000..6f439ae
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/onparse-infiniteloop-worker.js
@@ -0,0 +1,8 @@
+'use strict';
+
+// Use an infinite loop to prevent this service worker from advancing past the
+// 'parsed' state.
+let i = 0;
+while (true) {
+ ++i;
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-response-being-preloaded-xhr.html b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-response-being-preloaded-xhr.html
new file mode 100644
index 0000000..9c6d8bd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-response-being-preloaded-xhr.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<body></body>
+<script>
+const URL = 'opaque-response?from=opaque-response-being-preloaded-xhr.html';
+function runTest() {
+ var l = document.createElement('link');
+ // Use link rel=preload to try to get the browser to cache the opaque
+ // response.
+ l.setAttribute('rel', 'preload');
+ l.setAttribute('href', URL);
+ l.setAttribute('as', 'fetch');
+ l.onerror = function() {
+ parent.done('FAIL: preload failed unexpectedly');
+ };
+ document.body.appendChild(l);
+ xhr = new XMLHttpRequest;
+ xhr.withCredentials = true;
+ xhr.open('GET', URL);
+ // opaque-response returns an opaque response from serviceworker and thus
+ // the XHR must fail because it is not no-cors request.
+ // Particularly, the XHR must not reuse the opaque response from the
+ // preload request.
+ xhr.onerror = function() {
+ parent.done('PASS');
+ };
+ xhr.onload = function() {
+ parent.done('FAIL: ' + xhr.responseText);
+ };
+ xhr.send();
+}
+</script>
+<body onload="setTimeout(runTest, 100)"></body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-response-preloaded-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-response-preloaded-worker.js
new file mode 100644
index 0000000..9859bad
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-response-preloaded-worker.js
@@ -0,0 +1,12 @@
+importScripts('/common/get-host-info.sub.js');
+
+var remoteUrl = get_host_info()['HTTPS_REMOTE_ORIGIN'] +
+ '/service-workers/service-worker/resources/simple.txt'
+
+self.addEventListener('fetch', event => {
+ if (!event.request.url.match(/opaque-response\?from=/)) {
+ return;
+ }
+
+ event.respondWith(fetch(remoteUrl, {mode: 'no-cors'}));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-response-preloaded-xhr.html b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-response-preloaded-xhr.html
new file mode 100644
index 0000000..f31ac9b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-response-preloaded-xhr.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<body></body>
+<script>
+const URL = 'opaque-response?from=opaque-response-preloaded-xhr.html';
+function runTest() {
+ var l = document.createElement('link');
+ // Use link rel=preload to try to get the browser to cache the opaque
+ // response.
+ l.setAttribute('rel', 'preload');
+ l.setAttribute('href', URL);
+ l.setAttribute('as', 'fetch');
+ l.onload = function() {
+ xhr = new XMLHttpRequest;
+ xhr.withCredentials = true;
+ xhr.open('GET', URL);
+ // opaque-response returns an opaque response from serviceworker and thus
+ // the XHR must fail because it is not no-cors request.
+ // Particularly, the XHR must not reuse the opaque response from the
+ // preload request.
+ xhr.onerror = function() {
+ parent.done('PASS');
+ };
+ xhr.onload = function() {
+ parent.done('FAIL: ' + xhr.responseText);
+ };
+ xhr.send();
+ };
+ l.onerror = function() {
+ parent.done('FAIL: preload failed unexpectedly');
+ };
+ document.body.appendChild(l);
+}
+</script>
+<body onload="setTimeout(runTest, 100)"></body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-frame.html
new file mode 100644
index 0000000..a57aace
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-frame.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+<script>
+self.addEventListener('error', evt => {
+ self.parent.postMessage({ type: 'ErrorEvent', msg: evt.message }, '*');
+});
+
+const el = document.createElement('script');
+const params = new URLSearchParams(self.location.search);
+el.src = params.get('script');
+el.addEventListener('load', evt => {
+ runScript();
+});
+document.body.appendChild(el);
+</script>
+</body>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-large.js b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-large.js
new file mode 100644
index 0000000..7e1c598
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-large.js
@@ -0,0 +1,41 @@
+function runScript() {
+ throw new Error("Intentional error.");
+}
+
+function unused() {
+ // The following string is intended to be relatively large since some
+ // browsers trigger different code paths based on script size.
+ return "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a " +
+ "tortor ut orci bibendum blandit non quis diam. Aenean sit amet " +
+ "urna sit amet neque malesuada ultricies at vel nisi. Nunc et lacus " +
+ "est. Nam posuere erat enim, ac fringilla purus pellentesque " +
+ "cursus. Proin sodales eleifend lorem, eu semper massa scelerisque " +
+ "ac. Maecenas pharetra leo malesuada vulputate vulputate. Sed at " +
+ "efficitur odio. In rhoncus neque varius nibh efficitur gravida. " +
+ "Curabitur vitae dolor enim. Mauris semper lobortis libero sed " +
+ "congue. Donec felis ante, fringilla eget urna ut, finibus " +
+ "hendrerit lacus. Donec at interdum diam. Proin a neque vitae diam " +
+ "egestas euismod. Mauris posuere elementum lorem, eget convallis " +
+ "nisl elementum et. In ut leo ac neque dapibus pharetra quis ac " +
+ "velit. Integer pretium lectus non urna vulputate, in interdum mi " +
+ "lobortis. Sed laoreet ex et metus pharetra blandit. Curabitur " +
+ "sollicitudin non neque eu varius. Phasellus posuere congue arcu, " +
+ "in aliquam nunc fringilla a. Morbi id facilisis libero. Phasellus " +
+ "metus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
+ "tortor ut orci bibendum blandit non quis diam. Aenean sit amet " +
+ "urna sit amet neque malesuada ultricies at vel nisi. Nunc et lacus " +
+ "est. Nam posuere erat enim, ac fringilla purus pellentesque " +
+ "cursus. Proin sodales eleifend lorem, eu semper massa scelerisque " +
+ "ac. Maecenas pharetra leo malesuada vulputate vulputate. Sed at " +
+ "efficitur odio. In rhoncus neque varius nibh efficitur gravida. " +
+ "Curabitur vitae dolor enim. Mauris semper lobortis libero sed " +
+ "congue. Donec felis ante, fringilla eget urna ut, finibus " +
+ "hendrerit lacus. Donec at interdum diam. Proin a neque vitae diam " +
+ "egestas euismod. Mauris posuere elementum lorem, eget convallis " +
+ "nisl elementum et. In ut leo ac neque dapibus pharetra quis ac " +
+ "velit. Integer pretium lectus non urna vulputate, in interdum mi " +
+ "lobortis. Sed laoreet ex et metus pharetra blandit. Curabitur " +
+ "sollicitudin non neque eu varius. Phasellus posuere congue arcu, " +
+ "in aliquam nunc fringilla a. Morbi id facilisis libero. Phasellus " +
+ "metus.";
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-small.js b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-small.js
new file mode 100644
index 0000000..8b89098
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-small.js
@@ -0,0 +1,3 @@
+function runScript() {
+ throw new Error("Intentional error.");
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-sw.js b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-sw.js
new file mode 100644
index 0000000..4d882c6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/opaque-script-sw.js
@@ -0,0 +1,37 @@
+importScripts('test-helpers.sub.js');
+importScripts('/common/get-host-info.sub.js');
+
+const NAME = 'foo';
+const SAME_ORIGIN_BASE = new URL('./', self.location.href).href;
+const CROSS_ORIGIN_BASE = new URL('./',
+ get_host_info().HTTPS_REMOTE_ORIGIN + base_path()).href;
+
+const urls = [
+ `${SAME_ORIGIN_BASE}opaque-script-small.js`,
+ `${SAME_ORIGIN_BASE}opaque-script-large.js`,
+ `${CROSS_ORIGIN_BASE}opaque-script-small.js`,
+ `${CROSS_ORIGIN_BASE}opaque-script-large.js`,
+];
+
+self.addEventListener('install', evt => {
+ evt.waitUntil(async function() {
+ const c = await caches.open(NAME);
+ const promises = urls.map(async function(u) {
+ const r = await fetch(u, { mode: 'no-cors' });
+ await c.put(u, r);
+ });
+ await Promise.all(promises);
+ }());
+});
+
+self.addEventListener('fetch', evt => {
+ const url = new URL(evt.request.url);
+ if (!url.pathname.includes('opaque-script-small.js') &&
+ !url.pathname.includes('opaque-script-large.js')) {
+ return;
+ }
+ evt.respondWith(async function() {
+ const c = await caches.open(NAME);
+ return c.match(evt.request);
+ }());
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/other.html b/third_party/web_platform_tests/service-workers/service-worker/resources/other.html
new file mode 100644
index 0000000..b9f3504
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/other.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<title>Other</title>
+Here's an other html file.
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/override_assert_object_equals.js b/third_party/web_platform_tests/service-workers/service-worker/resources/override_assert_object_equals.js
new file mode 100644
index 0000000..835046d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/override_assert_object_equals.js
@@ -0,0 +1,58 @@
+// .body attribute of Request and Response object are experimental feture. It is
+// enabled when --enable-experimental-web-platform-features flag is set.
+// Touching this attribute can change the behavior of the objects. To avoid
+// touching it while comparing the objects in LayoutTest, we overwrite
+// assert_object_equals method.
+
+(function() {
+ var original_assert_object_equals = self.assert_object_equals;
+ function _brand(object) {
+ return Object.prototype.toString.call(object).match(/^\[object (.*)\]$/)[1];
+ }
+ var assert_request_equals = function(actual, expected, prefix) {
+ if (typeof actual !== 'object') {
+ assert_equals(actual, expected, prefix);
+ return;
+ }
+ assert_true(actual instanceof Request, prefix);
+ assert_true(expected instanceof Request, prefix);
+ assert_equals(actual.bodyUsed, expected.bodyUsed, prefix + '.bodyUsed');
+ assert_equals(actual.method, expected.method, prefix + '.method');
+ assert_equals(actual.url, expected.url, prefix + '.url');
+ original_assert_object_equals(actual.headers, expected.headers,
+ prefix + '.headers');
+ assert_equals(actual.context, expected.context, prefix + '.context');
+ assert_equals(actual.referrer, expected.referrer, prefix + '.referrer');
+ assert_equals(actual.mode, expected.mode, prefix + '.mode');
+ assert_equals(actual.credentials, expected.credentials,
+ prefix + '.credentials');
+ assert_equals(actual.cache, expected.cache, prefix + '.cache');
+ };
+ var assert_response_equals = function(actual, expected, prefix) {
+ if (typeof actual !== 'object') {
+ assert_equals(actual, expected, prefix);
+ return;
+ }
+ assert_true(actual instanceof Response, prefix);
+ assert_true(expected instanceof Response, prefix);
+ assert_equals(actual.bodyUsed, expected.bodyUsed, prefix + '.bodyUsed');
+ assert_equals(actual.type, expected.type, prefix + '.type');
+ assert_equals(actual.url, expected.url, prefix + '.url');
+ assert_equals(actual.status, expected.status, prefix + '.status');
+ assert_equals(actual.statusText, expected.statusText,
+ prefix + '.statusText');
+ original_assert_object_equals(actual.headers, expected.headers,
+ prefix + '.headers');
+ };
+ var assert_object_equals = function(actual, expected, description) {
+ var prefix = (description ? description + ': ' : '') + _brand(expected);
+ if (expected instanceof Request) {
+ assert_request_equals(actual, expected, prefix);
+ } else if (expected instanceof Response) {
+ assert_response_equals(actual, expected, prefix);
+ } else {
+ original_assert_object_equals(actual, expected, description);
+ }
+ };
+ self.assert_object_equals = assert_object_equals;
+})();
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-iframe-claim.html b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-iframe-claim.html
new file mode 100644
index 0000000..12b048e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-iframe-claim.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<title>Service Worker: 3P iframe for partitioned service workers</title>
+<script src="./test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="./partitioned-utils.js"></script>
+
+<body>
+ <script>
+ // 1p mode will respond to requests for its current controller and
+ // postMessage when its controller changes.
+ async function onLoad1pMode(){
+ self.addEventListener('message', evt => {
+ if(!evt.data)
+ return;
+
+ if (evt.data.type === "get-controller") {
+ window.parent.postMessage({controller: navigator.serviceWorker.controller});
+ }
+ });
+
+ navigator.serviceWorker.addEventListener('controllerchange', evt => {
+ window.parent.postMessage({status: "success", context: "1p"}, '*');
+ });
+ }
+
+ // 3p mode will tell its SW to claim and then postMessage its results
+ // automatically.
+ async function onLoad3pMode() {
+ reg = await setupServiceWorker();
+
+ if(navigator.serviceWorker.controller != null){
+ //This iframe is already under control of a service worker, testing for
+ // a controller change will timeout. Return a failure.
+ window.parent.postMessage({status: "failure", context: "3p"}, '*');
+ return;
+ }
+
+ // Once this client is claimed, let the test know.
+ navigator.serviceWorker.addEventListener('controllerchange', evt => {
+ window.parent.postMessage({status: "success", context: "3p"}, '*');
+ });
+
+ // Trigger the SW to claim.
+ reg.active.postMessage({type: "claim"});
+
+ }
+
+ const request_url = new URL(window.location.href);
+ var url_search = request_url.search.substr(1);
+
+ if(url_search == "1p-mode") {
+ self.addEventListener('load', onLoad1pMode);
+ }
+ else if(url_search == "3p-mode") {
+ self.addEventListener('load', onLoad3pMode);
+ }
+ // Else do nothing.
+ </script>
+</body>
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-child.html b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-child.html
new file mode 100644
index 0000000..d05fef4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-child.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<title>Service Worker: Innermost nested iframe for partitioned service workers</title>
+<script src="./test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="./partitioned-utils.js"></script>
+
+<body>
+Innermost 1p iframe (A2) with 3p ancestor (A1-B-A2-A3): this iframe will
+register a service worker when it loads and then add its own iframe (A3) that
+will attempt to navigate to a url. ServiceWorker will intercept this navigation
+and resolve the ServiceWorker's internal Promise. When
+ThirdPartyStoragePartitioning is enabled, this iframe should be partitioned
+from the main frame and should not share a ServiceWorker.
+<script>
+
+async function onLoad() {
+ // Set-up the ServiceWorker for this iframe, defined in:
+ // service-workers/service-worker/resources/partitioned-utils.js
+ await setupServiceWorker();
+
+ // When the SW's iframe finishes it'll post a message. This forwards
+ // it up to the middle-iframe.
+ self.addEventListener('message', evt => {
+ window.parent.postMessage(evt.data, '*');
+ });
+
+ // Now that we have set up the ServiceWorker, we need it to
+ // intercept a navigation that will resolve its promise.
+ // To do this, we create an additional iframe to send that
+ // navigation request to resolve (`resolve.fakehtml`). If we're
+ // partitioned then there shouldn't be a promise to resolve. Defined
+ // in: service-workers/service-worker/resources/partitioned-storage-sw.js
+ const resolve_frame_url = new URL('./partitioned-resolve.fakehtml?FromNestedFrame', self.location);
+ const frame_resolve = await new Promise(resolve => {
+ var frame = document.createElement('iframe');
+ frame.src = resolve_frame_url;
+ frame.onload = function() { resolve(frame); };
+ document.body.appendChild(frame);
+ });
+}
+
+self.addEventListener('load', onLoad);
+</script>
+</body>
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-parent.html b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-parent.html
new file mode 100644
index 0000000..f748e2f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-parent.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<title>Service Worker: Middle nested iframe for partitioned service workers</title>
+<script src="./test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="./partitioned-utils.js"></script>
+
+<body>
+Middle of the nested iframes (3p ancestor or B in A1-B-A2).
+<script>
+
+async function onLoad() {
+ // The innermost iframe will recieve a message from the
+ // ServiceWorker and pass it to this iframe. We need to
+ // then pass that message to the main frame to complete
+ // the test.
+ self.addEventListener('message', evt => {
+ window.parent.postMessage(evt.data, '*');
+ });
+
+ // Embed the innermost iframe and set-up the service worker there.
+ const innermost_iframe_url = new URL('./partitioned-service-worker-nested-iframe-child.html',
+ get_host_info().HTTPS_ORIGIN + self.location.pathname);
+ var frame = document.createElement('iframe');
+ frame.src = innermost_iframe_url;
+ document.body.appendChild(frame);
+}
+
+self.addEventListener('load', onLoad);
+</script>
+</body>
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-getRegistrations.html b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-getRegistrations.html
new file mode 100644
index 0000000..747c058
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-getRegistrations.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>Service Worker: 3P iframe for partitioned service workers</title>
+<script src="./test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="./partitioned-utils.js"></script>
+
+<body>
+ This iframe will register a service worker when it loads and then will use
+ getRegistrations to get a handle to the SW. It will then postMessage to the
+ SW to retrieve the SW's ID. This iframe will then forward that message up,
+ eventually, to the test.
+ <script>
+
+ async function onLoad() {
+ const scope = './partitioned-'
+ const absoluteScope = new URL(scope, window.location).href;
+
+ await setupServiceWorker();
+
+ // Once the SW sends us its ID, forward it up to the window.
+ navigator.serviceWorker.addEventListener('message', evt => {
+ window.parent.postMessage(evt.data, '*');
+ });
+
+ // Now get the SW with getRegistrations.
+ const retrieved_registrations =
+ await navigator.serviceWorker.getRegistrations();
+
+ // It's possible that other tests have left behind other service workers.
+ // This steps filters those other SWs out.
+ const filtered_registrations =
+ retrieved_registrations.filter(reg => reg.scope == absoluteScope);
+
+ filtered_registrations[0].active.postMessage({type: "get-id"});
+
+ }
+
+ self.addEventListener('load', onLoad);
+ </script>
+</body>
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-matchAll.html b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-matchAll.html
new file mode 100644
index 0000000..7a2c366
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-matchAll.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<title>Service Worker: 3P iframe for partitioned service workers</title>
+<script src="./test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="./partitioned-utils.js"></script>
+
+<body>
+ This iframe will register a service worker when it loads and then will use
+ getRegistrations to get a handle to the SW. It will then postMessage to the
+ SW to get the SW's clients via matchAll(). This iframe will then forward the
+ SW's response up, eventually, to the test.
+ <script>
+ async function onLoad() {
+ reg = await setupServiceWorker();
+
+ // Once the SW sends us its ID, forward it up to the window.
+ navigator.serviceWorker.addEventListener('message', evt => {
+ window.parent.postMessage(evt.data, '*');
+ });
+
+ reg.active.postMessage({type: "get-match-all"});
+
+ }
+
+ self.addEventListener('load', onLoad);
+ </script>
+</body>
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html
new file mode 100644
index 0000000..1b7f671
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<title>Service Worker: 3P iframe for partitioned service workers</title>
+<script src="./test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="./partitioned-utils.js"></script>
+
+
+<body>
+This iframe will register a service worker when it loads and then add its own
+iframe that will attempt to navigate to a url that service worker will intercept
+and use to resolve the service worker's internal Promise.
+<script>
+
+async function onLoad() {
+ await setupServiceWorker();
+
+ // When the SW's iframe finishes it'll post a message. This forwards it up to
+ // the window.
+ self.addEventListener('message', evt => {
+ window.parent.postMessage(evt.data, '*');
+ });
+
+ // Now try to resolve the SW's promise. If we're partitioned then there
+ // shouldn't be a promise to resolve.
+ const resolve_frame_url = new URL('./partitioned-resolve.fakehtml?From3pFrame', self.location);
+ const frame_resolve = await new Promise(resolve => {
+ var frame = document.createElement('iframe');
+ frame.src = resolve_frame_url;
+ frame.onload = function() { resolve(frame); };
+ document.body.appendChild(frame);
+ });
+}
+
+self.addEventListener('load', onLoad);
+</script>
+</body>
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html
new file mode 100644
index 0000000..86384ce
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<title>Service Worker: 3P window for partitioned service workers</title>
+<script src="./test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+
+
+<body>
+This page should be opened as a third-party window. It then loads an iframe
+specified by the query parameter. Finally it forwards the postMessage from the
+iframe up to the opener (the test).
+
+<script>
+
+async function onLoad() {
+ const message_promise = new Promise(resolve => {
+ self.addEventListener('message', evt => {
+ resolve(evt.data);
+ });
+ });
+
+ const search_param = new URLSearchParams(window.location.search);
+ const iframe_url = search_param.get('target');
+
+ var frame = document.createElement('iframe');
+ frame.src = iframe_url;
+ frame.style.position = 'absolute';
+ document.body.appendChild(frame);
+
+
+ await message_promise.then(data => {
+ // We're done, forward the message and clean up.
+ window.opener.postMessage(data, '*');
+
+ frame.remove();
+ });
+}
+
+self.addEventListener('load', onLoad);
+
+</script>
+</body>
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-storage-sw.js b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-storage-sw.js
new file mode 100644
index 0000000..00f7979
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-storage-sw.js
@@ -0,0 +1,81 @@
+// Holds the promise that the "resolve.fakehtml" call attempts to resolve.
+// This is "the SW's promise" that other parts of the test refer to.
+var promise;
+// Stores the resolve funcution for the current promise.
+var pending_resolve_func = null;
+// Unique ID to determine which service worker is being used.
+const ID = Math.random();
+
+function callAndResetResolve() {
+ var local_resolve = pending_resolve_func;
+ pending_resolve_func = null;
+ local_resolve();
+}
+
+self.addEventListener('fetch', function(event) {
+ fetchEventHandler(event);
+})
+
+self.addEventListener('message', (event) => {
+ event.waitUntil(async function() {
+ if(!event.data)
+ return;
+
+ if (event.data.type === "get-id") {
+ event.source.postMessage({ID: ID});
+ }
+ else if(event.data.type === "get-match-all") {
+ clients.matchAll({includeUncontrolled: true}).then(clients_list => {
+ const url_list = clients_list.map(item => item.url);
+ event.source.postMessage({urls_list: url_list});
+ });
+ }
+ else if(event.data.type === "claim") {
+ await clients.claim();
+ }
+ }());
+});
+
+async function fetchEventHandler(event){
+ var request_url = new URL(event.request.url);
+ var url_search = request_url.search.substr(1);
+ request_url.search = "";
+ if ( request_url.href.endsWith('waitUntilResolved.fakehtml') ) {
+
+ if (pending_resolve_func != null) {
+ // Respond with an error if there is already a pending promise
+ event.respondWith(Response.error());
+ return;
+ }
+
+ // Create the new promise.
+ promise = new Promise(function(resolve) {
+ pending_resolve_func = resolve;
+ });
+ event.waitUntil(promise);
+
+ event.respondWith(new Response(`
+ <html>
+ Promise created by ${url_search}
+ <script>self.parent.postMessage({ ID:${ID}, source: "${url_search}"
+ }, '*');</script>
+ </html>
+ `, {headers: {'Content-Type': 'text/html'}}
+ ));
+
+ }
+ else if ( request_url.href.endsWith('resolve.fakehtml') ) {
+ var has_pending = !!pending_resolve_func;
+ event.respondWith(new Response(`
+ <html>
+ Promise settled for ${url_search}
+ <script>self.parent.postMessage({ ID:${ID}, has_pending: ${has_pending},
+ source: "${url_search}" }, '*');</script>
+ </html>
+ `, {headers: {'Content-Type': 'text/html'}}));
+
+ if (has_pending) {
+ callAndResetResolve();
+ }
+ }
+}
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-utils.js b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-utils.js
new file mode 100644
index 0000000..22e90be
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/partitioned-utils.js
@@ -0,0 +1,110 @@
+// The resolve function for the current pending event listener's promise.
+// It is nulled once the promise is resolved.
+var message_event_promise_resolve = null;
+
+function messageEventHandler(evt) {
+ if (message_event_promise_resolve) {
+ local_resolve = message_event_promise_resolve;
+ message_event_promise_resolve = null;
+ local_resolve(evt.data);
+ }
+}
+
+function makeMessagePromise() {
+ if (message_event_promise_resolve != null) {
+ // Do not create a new promise until the previous is settled.
+ return;
+ }
+
+ return new Promise(resolve => {
+ message_event_promise_resolve = resolve;
+ });
+}
+
+// Loads a url for the frame type and then returns a promise for
+// the data that was postMessage'd from the loaded frame.
+// If the frame type is 'window' then `url` is encoded into the search param
+// as the url the 3p window is meant to iframe.
+function loadAndReturnSwData(t, url, frame_type) {
+ if (frame_type !== 'iframe' && frame_type !== 'window') {
+ return;
+ }
+
+ const message_promise = makeMessagePromise();
+
+ // Create the iframe or window and then return the promise for data.
+ if ( frame_type === 'iframe' ) {
+ const frame = with_iframe(url, false);
+ t.add_cleanup(async () => {
+ const f = await frame;
+ f.remove();
+ });
+ }
+ else {
+ // 'window' case.
+ const search_param = new URLSearchParams();
+ search_param.append('target', url);
+
+ const third_party_window_url = new URL(
+ './resources/partitioned-service-worker-third-party-window.html' +
+ '?' + search_param,
+ get_host_info().HTTPS_NOTSAMESITE_ORIGIN + self.location.pathname);
+
+ const w = window.open(third_party_window_url);
+ t.add_cleanup(() => w.close());
+ }
+
+ return message_promise;
+}
+
+// Checks for an existing service worker registration. If not present,
+// registers and maintains a service worker. Used in windows or iframes
+// that will be partitioned from the main frame.
+async function setupServiceWorker() {
+
+ const script = './partitioned-storage-sw.js';
+ const scope = './partitioned-';
+
+ var reg = await navigator.serviceWorker.register(script, { scope: scope });
+
+ // We should keep track if we installed a worker or not. If we did then we
+ // need to uninstall it. Otherwise we let the top level test uninstall it
+ // (If partitioning is not working).
+ var installed_a_worker = true;
+ await new Promise(resolve => {
+ // Check if a worker is already activated.
+ var worker = reg.active;
+ // If so, just resolve.
+ if ( worker ) {
+ installed_a_worker = false;
+ resolve();
+ return;
+ }
+
+ //Otherwise check if one is waiting.
+ worker = reg.waiting;
+ // If not waiting, grab the installing worker.
+ if ( !worker ) {
+ worker = reg.installing;
+ }
+
+ // Resolve once it's activated.
+ worker.addEventListener('statechange', evt => {
+ if (worker.state === 'activated') {
+ resolve();
+ }
+ });
+ });
+
+ self.addEventListener('unload', async () => {
+ // If we didn't install a worker then that means the top level test did, and
+ // that test is therefore responsible for cleaning it up.
+ if ( !installed_a_worker ) {
+ return;
+ }
+
+ await reg.unregister();
+ });
+
+ return reg;
+}
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/pass-through-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/pass-through-worker.js
new file mode 100644
index 0000000..5eaf48d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/pass-through-worker.js
@@ -0,0 +1,3 @@
+addEventListener('fetch', evt => {
+ evt.respondWith(fetch(evt.request));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/pass.txt b/third_party/web_platform_tests/service-workers/service-worker/resources/pass.txt
new file mode 100644
index 0000000..7ef22e9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/pass.txt
@@ -0,0 +1 @@
+PASS
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/performance-timeline-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/performance-timeline-worker.js
new file mode 100644
index 0000000..6c6dfcb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/performance-timeline-worker.js
@@ -0,0 +1,62 @@
+importScripts('/resources/testharness.js');
+
+promise_test(function(test) {
+ var durationMsec = 100;
+ // There are limits to our accuracy here. Timers may fire up to a
+ // millisecond early due to platform-dependent rounding. In addition
+ // the performance API introduces some rounding as well to prevent
+ // timing attacks.
+ var accuracy = 1.5;
+ return new Promise(function(resolve) {
+ performance.mark('startMark');
+ setTimeout(resolve, durationMsec);
+ }).then(function() {
+ performance.mark('endMark');
+ performance.measure('measure', 'startMark', 'endMark');
+ var startMark = performance.getEntriesByName('startMark')[0];
+ var endMark = performance.getEntriesByName('endMark')[0];
+ var measure = performance.getEntriesByType('measure')[0];
+ assert_equals(measure.startTime, startMark.startTime);
+ assert_approx_equals(endMark.startTime - startMark.startTime,
+ measure.duration, 0.001);
+ assert_greater_than(measure.duration, durationMsec - accuracy);
+ assert_equals(performance.getEntriesByType('mark').length, 2);
+ assert_equals(performance.getEntriesByType('measure').length, 1);
+ performance.clearMarks('startMark');
+ performance.clearMeasures('measure');
+ assert_equals(performance.getEntriesByType('mark').length, 1);
+ assert_equals(performance.getEntriesByType('measure').length, 0);
+ });
+ }, 'User Timing');
+
+promise_test(function(test) {
+ return fetch('sample.txt')
+ .then(function(resp) {
+ return resp.text();
+ })
+ .then(function(text) {
+ var expectedResources = ['testharness.js', 'sample.txt'];
+ assert_equals(performance.getEntriesByType('resource').length, expectedResources.length);
+ for (var i = 0; i < expectedResources.length; i++) {
+ var entry = performance.getEntriesByType('resource')[i];
+ assert_true(entry.name.endsWith(expectedResources[i]));
+ assert_equals(entry.workerStart, 0);
+ assert_greater_than(entry.startTime, 0);
+ assert_greater_than(entry.responseEnd, entry.startTime);
+ }
+ return new Promise(function(resolve) {
+ performance.onresourcetimingbufferfull = _ => {
+ resolve('bufferfull');
+ }
+ performance.setResourceTimingBufferSize(expectedResources.length);
+ fetch('sample.txt');
+ });
+ })
+ .then(function(result) {
+ assert_equals(result, 'bufferfull');
+ performance.clearResourceTimings();
+ assert_equals(performance.getEntriesByType('resource').length, 0);
+ })
+ }, 'Resource Timing');
+
+done();
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-blob-url.js b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-blob-url.js
new file mode 100644
index 0000000..9095194
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-blob-url.js
@@ -0,0 +1,5 @@
+self.onmessage = e => {
+ fetch(e.data)
+ .then(response => response.text())
+ .then(text => e.source.postMessage('Worker reply:' + text));
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-dictionary-transferables-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-dictionary-transferables-worker.js
new file mode 100644
index 0000000..87a4500
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-dictionary-transferables-worker.js
@@ -0,0 +1,24 @@
+var messageHandler = function(port, e) {
+ var text_decoder = new TextDecoder;
+ port.postMessage({
+ content: text_decoder.decode(e.data),
+ byteLength: e.data.byteLength
+ });
+
+ // Send back the array buffer via Client.postMessage.
+ port.postMessage(e.data, {transfer: [e.data.buffer]});
+
+ port.postMessage({
+ content: text_decoder.decode(e.data),
+ byteLength: e.data.byteLength
+ });
+};
+
+self.addEventListener('message', e => {
+ if (e.ports[0]) {
+ // Wait for messages sent via MessagePort.
+ e.ports[0].onmessage = messageHandler.bind(null, e.ports[0]);
+ return;
+ }
+ messageHandler(e.source, e);
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-echo-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-echo-worker.js
new file mode 100644
index 0000000..f088ad1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-echo-worker.js
@@ -0,0 +1,3 @@
+self.addEventListener('message', event => {
+ event.source.postMessage(event.data);
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-fetched-text.js b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-fetched-text.js
new file mode 100644
index 0000000..9fc6717
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-fetched-text.js
@@ -0,0 +1,5 @@
+self.onmessage = async (e) => {
+ const response = await fetch(e.data);
+ const text = await response.text();
+ self.postMessage(text);
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-msgport-to-client-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-msgport-to-client-worker.js
new file mode 100644
index 0000000..7af935f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-msgport-to-client-worker.js
@@ -0,0 +1,19 @@
+self.onmessage = function(e) {
+ e.waitUntil(self.clients.matchAll().then(function(clients) {
+ clients.forEach(function(client) {
+ var messageChannel = new MessageChannel();
+ messageChannel.port1.onmessage =
+ onMessageViaMessagePort.bind(null, messageChannel.port1);
+ client.postMessage(undefined, [messageChannel.port2]);
+ });
+ }));
+};
+
+function onMessageViaMessagePort(port, e) {
+ var message = e.data;
+ if ('value' in message) {
+ port.postMessage({ack: 'Acking value: ' + message.value});
+ } else if ('done' in message) {
+ port.postMessage({done: true});
+ }
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-on-load-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-on-load-worker.js
new file mode 100644
index 0000000..c2b0bcb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-on-load-worker.js
@@ -0,0 +1,9 @@
+if ('DedicatedWorkerGlobalScope' in self &&
+ self instanceof DedicatedWorkerGlobalScope) {
+ postMessage('dedicated worker script loaded');
+} else if ('SharedWorkerGlobalScope' in self &&
+ self instanceof SharedWorkerGlobalScope) {
+ self.onconnect = evt => {
+ evt.ports[0].postMessage('shared worker script loaded');
+ };
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-to-client-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-to-client-worker.js
new file mode 100644
index 0000000..1791306
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-to-client-worker.js
@@ -0,0 +1,10 @@
+self.onmessage = function(e) {
+ e.waitUntil(self.clients.matchAll().then(function(clients) {
+ clients.forEach(function(client) {
+ client.postMessage('Sending message via clients');
+ if (!Array.isArray(clients))
+ client.postMessage('clients is not an array');
+ client.postMessage('quit');
+ });
+ }));
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-transferables-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-transferables-worker.js
new file mode 100644
index 0000000..d35c1c9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-transferables-worker.js
@@ -0,0 +1,24 @@
+var messageHandler = function(port, e) {
+ var text_decoder = new TextDecoder;
+ port.postMessage({
+ content: text_decoder.decode(e.data),
+ byteLength: e.data.byteLength
+ });
+
+ // Send back the array buffer via Client.postMessage.
+ port.postMessage(e.data, [e.data.buffer]);
+
+ port.postMessage({
+ content: text_decoder.decode(e.data),
+ byteLength: e.data.byteLength
+ });
+};
+
+self.addEventListener('message', e => {
+ if (e.ports[0]) {
+ // Wait for messages sent via MessagePort.
+ e.ports[0].onmessage = messageHandler.bind(null, e.ports[0]);
+ return;
+ }
+ messageHandler(e.source, e);
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-worker.js
new file mode 100644
index 0000000..858cf04
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/postmessage-worker.js
@@ -0,0 +1,19 @@
+var port;
+
+// Exercise the 'onmessage' handler:
+self.onmessage = function(e) {
+ var message = e.data;
+ if ('port' in message) {
+ port = message.port;
+ }
+};
+
+// And an event listener:
+self.addEventListener('message', function(e) {
+ var message = e.data;
+ if ('value' in message) {
+ port.postMessage('Acking value: ' + message.value);
+ } else if ('done' in message) {
+ port.postMessage('quit');
+ }
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/range-request-to-different-origins-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/range-request-to-different-origins-worker.js
new file mode 100644
index 0000000..cab6058
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/range-request-to-different-origins-worker.js
@@ -0,0 +1,40 @@
+// This worker is meant to test range requests where the responses come from
+// multiple origins. It forwards the first request to a cross-origin URL
+// (generating an opaque response). The server is expected to return a 206
+// Partial Content response. Then the worker lets subsequent range requests
+// fall back to network (generating same-origin responses). The intent is to try
+// to trick the browser into treating the resource as same-origin.
+//
+// It would also be interesting to do the reverse test where the first request
+// goes to the same-origin URL, and subsequent range requests go cross-origin in
+// 'no-cors' mode to receive opaque responses. But the service worker cannot do
+// this, because in 'no-cors' mode the 'range' HTTP header is disallowed.
+
+importScripts('/common/get-host-info.sub.js')
+
+let initial = true;
+function is_initial_request() {
+ const old = initial;
+ initial = false;
+ return old;
+}
+
+self.addEventListener('fetch', e => {
+ const url = new URL(e.request.url);
+ if (url.search.indexOf('VIDEO') == -1) {
+ // Fall back for non-video.
+ return;
+ }
+
+ // Make the first request go cross-origin.
+ if (is_initial_request()) {
+ const cross_origin_url = get_host_info().HTTPS_REMOTE_ORIGIN +
+ url.pathname + url.search;
+ const cross_origin_request = new Request(cross_origin_url,
+ {mode: 'no-cors', headers: e.request.headers});
+ e.respondWith(fetch(cross_origin_request));
+ return;
+ }
+
+ // Fall back to same origin for subsequent range requests.
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/range-request-with-different-cors-modes-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/range-request-with-different-cors-modes-worker.js
new file mode 100644
index 0000000..7580b0b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/range-request-with-different-cors-modes-worker.js
@@ -0,0 +1,60 @@
+// This worker is meant to test range requests where the responses are a mix of
+// opaque ones and non-opaque ones. It forwards the first request to a
+// cross-origin URL (generating an opaque response). The server is expected to
+// return a 206 Partial Content response. Then the worker forwards subsequent
+// range requests to that URL, with CORS sharing generating a non-opaque
+// responses. The intent is to try to trick the browser into treating the
+// resource as non-opaque.
+//
+// It would also be interesting to do the reverse test where the first request
+// uses 'cors', and subsequent range requests use 'no-cors' mode. But the
+// service worker cannot do this, because in 'no-cors' mode the 'range' HTTP
+// header is disallowed.
+
+importScripts('/common/get-host-info.sub.js')
+
+let initial = true;
+function is_initial_request() {
+ const old = initial;
+ initial = false;
+ return old;
+}
+
+self.addEventListener('fetch', e => {
+ const url = new URL(e.request.url);
+ if (url.search.indexOf('VIDEO') == -1) {
+ // Fall back for non-video.
+ return;
+ }
+
+ let cross_origin_url = get_host_info().HTTPS_REMOTE_ORIGIN +
+ url.pathname + url.search;
+
+ // The first request is no-cors.
+ if (is_initial_request()) {
+ const init = { mode: 'no-cors', headers: e.request.headers };
+ const cross_origin_request = new Request(cross_origin_url, init);
+ e.respondWith(fetch(cross_origin_request));
+ return;
+ }
+
+ // Subsequent range requests are cors.
+
+ // Copy headers needed for range requests.
+ let my_headers = new Headers;
+ if (e.request.headers.get('accept'))
+ my_headers.append('accept', e.request.headers.get('accept'));
+ if (e.request.headers.get('range'))
+ my_headers.append('range', e.request.headers.get('range'));
+
+ // Add &ACAOrigin to allow CORS.
+ cross_origin_url += '&ACAOrigin=' + get_host_info().HTTPS_ORIGIN;
+ // Add &ACAHeaders to allow range requests.
+ cross_origin_url += '&ACAHeaders=accept,range';
+
+ // Make the CORS request.
+ const init = { mode: 'cors', headers: my_headers };
+ const cross_origin_request = new Request(cross_origin_url, init);
+ e.respondWith(fetch(cross_origin_request));
+ });
+
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/redirect-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/redirect-worker.js
new file mode 100644
index 0000000..82e21fc
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/redirect-worker.js
@@ -0,0 +1,145 @@
+// We store an empty response for each fetch event request we see
+// in this Cache object so we can get the list of urls in the
+// message event.
+var cacheName = 'urls-' + self.registration.scope;
+
+var waitUntilPromiseList = [];
+
+// Sends the requests seen by this worker. The output is:
+// {
+// requestInfos: [
+// {url: url1, resultingClientId: id1},
+// {url: url2, resultingClientId: id2},
+// ]
+// }
+async function getRequestInfos(event) {
+ // Wait for fetch events to finish.
+ await Promise.all(waitUntilPromiseList);
+ waitUntilPromiseList = [];
+
+ // Generate the message.
+ const cache = await caches.open(cacheName);
+ const requestList = await cache.keys();
+ const requestInfos = [];
+ for (let i = 0; i < requestList.length; i++) {
+ const response = await cache.match(requestList[i]);
+ const body = await response.json();
+ requestInfos[i] = {
+ url: requestList[i].url,
+ resultingClientId: body.resultingClientId
+ };
+ }
+ await caches.delete(cacheName);
+
+ event.data.port.postMessage({requestInfos});
+}
+
+// Sends the results of clients.get(id) from this worker. The
+// input is:
+// {
+// actual_ids: {a: id1, b: id2, x: id3}
+// }
+//
+// The output is:
+// {
+// clients: {
+// a: {found: false},
+// b: {found: false},
+// x: {
+// id: id3,
+// url: url1,
+// found: true
+// }
+// }
+// }
+async function getClients(event) {
+ // |actual_ids| is like:
+ // {a: id1, b: id2, x: id3}
+ const actual_ids = event.data.actual_ids;
+ const result = {}
+ for (let key of Object.keys(actual_ids)) {
+ const id = actual_ids[key];
+ const client = await self.clients.get(id);
+ if (client === undefined)
+ result[key] = {found: false};
+ else
+ result[key] = {found: true, url: client.url, id: client.id};
+ }
+ event.data.port.postMessage({clients: result});
+}
+
+self.addEventListener('message', async function(event) {
+ if (event.data.command == 'getRequestInfos') {
+ event.waitUntil(getRequestInfos(event));
+ return;
+ }
+
+ if (event.data.command == 'getClients') {
+ event.waitUntil(getClients(event));
+ return;
+ }
+});
+
+function get_query_params(url) {
+ var search = (new URL(url)).search;
+ if (!search) {
+ return {};
+ }
+ var ret = {};
+ var params = search.substring(1).split('&');
+ params.forEach(function(param) {
+ var element = param.split('=');
+ ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]);
+ });
+ return ret;
+}
+
+self.addEventListener('fetch', function(event) {
+ var waitUntilPromise = caches.open(cacheName).then(function(cache) {
+ const responseBody = {};
+ responseBody['resultingClientId'] = event.resultingClientId;
+ const headers = new Headers({'Content-Type': 'application/json'});
+ const response = new Response(JSON.stringify(responseBody), {headers});
+ return cache.put(event.request, response);
+ });
+ event.waitUntil(waitUntilPromise);
+
+ var params = get_query_params(event.request.url);
+ if (!params['sw']) {
+ // To avoid races, add the waitUntil() promise to our global list.
+ // If we get a message event before we finish here, it will wait
+ // these promises to complete before proceeding to read from the
+ // cache.
+ waitUntilPromiseList.push(waitUntilPromise);
+ return;
+ }
+
+ event.respondWith(waitUntilPromise.then(async () => {
+ if (params['sw'] == 'gen') {
+ return Response.redirect(params['url']);
+ } else if (params['sw'] == 'gen-manual') {
+ // Note this differs from Response.redirect() in that relative URLs are
+ // preserved.
+ return new Response("", {
+ status: 301,
+ headers: {location: params['url']},
+ });
+ } else if (params['sw'] == 'fetch') {
+ return fetch(event.request);
+ } else if (params['sw'] == 'fetch-url') {
+ return fetch(params['url']);
+ } else if (params['sw'] == 'follow') {
+ return fetch(new Request(event.request.url, {redirect: 'follow'}));
+ } else if (params['sw'] == 'manual') {
+ return fetch(new Request(event.request.url, {redirect: 'manual'}));
+ } else if (params['sw'] == 'manualThroughCache') {
+ const url = event.request.url;
+ await caches.delete(url)
+ const cache = await self.caches.open(url);
+ const response = await fetch(new Request(url, {redirect: 'manual'}));
+ await cache.put(event.request, response);
+ return cache.match(url);
+ }
+ // unexpected... trigger an interception failure
+ }));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/redirect.py b/third_party/web_platform_tests/service-workers/service-worker/resources/redirect.py
new file mode 100644
index 0000000..bd559d5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/redirect.py
@@ -0,0 +1,27 @@
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ if b'Status' in request.GET:
+ status = int(request.GET[b"Status"])
+ else:
+ status = 302
+
+ headers = []
+
+ url = isomorphic_decode(request.GET[b'Redirect'])
+ headers.append((b"Location", url))
+
+ if b"ACAOrigin" in request.GET:
+ for item in request.GET[b"ACAOrigin"].split(b","):
+ headers.append((b"Access-Control-Allow-Origin", item))
+
+ for suffix in [b"Headers", b"Methods", b"Credentials"]:
+ query = b"ACA%s" % suffix
+ header = b"Access-Control-Allow-%s" % suffix
+ if query in request.GET:
+ headers.append((header, request.GET[query]))
+
+ if b"ACEHeaders" in request.GET:
+ headers.append((b"Access-Control-Expose-Headers", request.GET[b"ACEHeaders"]))
+
+ return status, headers, b""
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/referer-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/referer-iframe.html
new file mode 100644
index 0000000..295ff45
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/referer-iframe.html
@@ -0,0 +1,39 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js"></script>
+<script>
+function check_referer(url, expected_referer) {
+ return fetch(url)
+ .then(function(res) { return res.json(); })
+ .then(function(headers) {
+ if (headers['referer'] === expected_referer) {
+ return Promise.resolve();
+ } else {
+ return Promise.reject('Referer for ' + url + ' must be ' +
+ expected_referer + ' but got ' +
+ headers['referer']);
+ }
+ });
+}
+
+window.addEventListener('message', function(evt) {
+ var host_info = get_host_info();
+ var port = evt.ports[0];
+ check_referer('request-headers.py?ignore=true',
+ host_info['HTTPS_ORIGIN'] +
+ base_path() + 'referer-iframe.html')
+ .then(function() {
+ return check_referer(
+ 'request-headers.py',
+ host_info['HTTPS_ORIGIN'] +
+ base_path() + 'referer-iframe.html');
+ })
+ .then(function() {
+ return check_referer(
+ 'request-headers.py?url=request-headers.py',
+ host_info['HTTPS_ORIGIN'] +
+ base_path() + 'fetch-rewrite-worker.js');
+ })
+ .then(function() { port.postMessage({results: 'finish'}); })
+ .catch(function(e) { port.postMessage({results: 'failure:' + e}); });
+ });
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/referrer-policy-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/referrer-policy-iframe.html
new file mode 100644
index 0000000..9ef3cd1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/referrer-policy-iframe.html
@@ -0,0 +1,32 @@
+<script src="/common/get-host-info.sub.js"></script>
+<script src="test-helpers.sub.js"></script>
+<script>
+function check_referer(url, expected_referer) {
+ return fetch(url)
+ .then(function(res) { return res.json(); })
+ .then(function(headers) {
+ if (headers['referer'] === expected_referer) {
+ return Promise.resolve();
+ } else {
+ return Promise.reject('Referer for ' + url + ' must be ' +
+ expected_referer + ' but got ' +
+ headers['referer']);
+ }
+ });
+}
+
+window.addEventListener('message', function(evt) {
+ var host_info = get_host_info();
+ var port = evt.ports[0];
+ check_referer('request-headers.py?ignore=true',
+ host_info['HTTPS_ORIGIN'] +
+ base_path() + 'referrer-policy-iframe.html')
+ .then(function() {
+ return check_referer(
+ 'request-headers.py?url=request-headers.py',
+ host_info['HTTPS_ORIGIN'] + '/');
+ })
+ .then(function() { port.postMessage({results: 'finish'}); })
+ .catch(function(e) { port.postMessage({results: 'failure:' + e}); });
+ });
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/register-closed-window-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/register-closed-window-iframe.html
new file mode 100644
index 0000000..117f254
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/register-closed-window-iframe.html
@@ -0,0 +1,19 @@
+<html>
+<head>
+<script>
+window.addEventListener('message', async function(evt) {
+ if (evt.data === 'START') {
+ var w = window.open('./');
+ var sw = w.navigator.serviceWorker;
+ w.close();
+ w = null;
+ try {
+ await sw.register('doesntmatter.js');
+ } finally {
+ parent.postMessage('OK', '*');
+ }
+ }
+});
+</script>
+</head>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/register-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/register-iframe.html
new file mode 100644
index 0000000..f5a040e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/register-iframe.html
@@ -0,0 +1,4 @@
+<script type="text/javascript">
+navigator.serviceWorker.register('empty-worker.js',
+ {scope: 'register-iframe.html'});
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/register-rewrite-worker.html b/third_party/web_platform_tests/service-workers/service-worker/resources/register-rewrite-worker.html
new file mode 100644
index 0000000..bf06317
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/register-rewrite-worker.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<script>
+async function onLoad() {
+ const params = new URLSearchParams(self.location.search);
+ const scope = self.origin + params.get('scopepath');
+ const script = './fetch-rewrite-worker.js';
+ const reg = await navigator.serviceWorker.register(script, { scope: scope });
+ // In nested cases we may be impacted by partitioning or not depending on
+ // the browser. With partitioning we will be installing a new worker here,
+ // but without partitioning the worker will already exist. Handle both cases.
+ if (reg.installing) {
+ await new Promise(resolve => {
+ const worker = reg.installing;
+ worker.addEventListener('statechange', evt => {
+ if (worker.state === 'activated') {
+ resolve();
+ }
+ });
+ });
+ if (reg.navigationPreload) {
+ await reg.navigationPreload.enable();
+ }
+ }
+ if (window.opener) {
+ window.opener.postMessage({ type: 'SW-REGISTERED' }, '*');
+ } else {
+ window.top.postMessage({ type: 'SW-REGISTERED' }, '*');
+ }
+}
+self.addEventListener('load', onLoad);
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-mime-types.js b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-mime-types.js
new file mode 100644
index 0000000..037e6c0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-mime-types.js
@@ -0,0 +1,96 @@
+// Registration tests that mostly verify the MIME type.
+//
+// This file tests every MIME type so it necessarily starts many service
+// workers, so it may be slow.
+function registration_tests_mime_types(register_method) {
+ promise_test(function(t) {
+ var script = 'resources/mime-type-worker.py';
+ var scope = 'resources/scope/no-mime-type-worker/';
+ return promise_rejects_dom(t,
+ 'SecurityError',
+ register_method(script, {scope: scope}),
+ 'Registration of no MIME type script should fail.');
+ }, 'Registering script with no MIME type');
+
+ promise_test(function(t) {
+ var script = 'resources/mime-type-worker.py?mime=text/plain';
+ var scope = 'resources/scope/bad-mime-type-worker/';
+ return promise_rejects_dom(t,
+ 'SecurityError',
+ register_method(script, {scope: scope}),
+ 'Registration of plain text script should fail.');
+ }, 'Registering script with bad MIME type');
+
+ /**
+ * ServiceWorkerContainer.register() should throw a TypeError, according to
+ * step 17.1 of https://w3c.github.io/ServiceWorker/#importscripts
+ *
+ * "[17] If an uncaught runtime script error occurs during the above step, then:
+ * [17.1] Invoke Reject Job Promise with job and TypeError"
+ *
+ * (Where the "uncaught runtime script error" is thrown by an unsuccessful
+ * importScripts())
+ */
+ promise_test(function(t) {
+ var script = 'resources/import-mime-type-worker.py';
+ var scope = 'resources/scope/no-mime-type-worker/';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of no MIME type imported script should fail.');
+ }, 'Registering script that imports script with no MIME type');
+
+ promise_test(function(t) {
+ var script = 'resources/import-mime-type-worker.py?mime=text/plain';
+ var scope = 'resources/scope/bad-mime-type-worker/';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of plain text imported script should fail.');
+ }, 'Registering script that imports script with bad MIME type');
+
+ const validMimeTypes = [
+ 'application/ecmascript',
+ 'application/javascript',
+ 'application/x-ecmascript',
+ 'application/x-javascript',
+ 'text/ecmascript',
+ 'text/javascript',
+ 'text/javascript1.0',
+ 'text/javascript1.1',
+ 'text/javascript1.2',
+ 'text/javascript1.3',
+ 'text/javascript1.4',
+ 'text/javascript1.5',
+ 'text/jscript',
+ 'text/livescript',
+ 'text/x-ecmascript',
+ 'text/x-javascript'
+ ];
+
+ for (const validMimeType of validMimeTypes) {
+ promise_test(() => {
+ var script = `resources/mime-type-worker.py?mime=${validMimeType}`;
+ var scope = 'resources/scope/good-mime-type-worker/';
+
+ return register_method(script, {scope}).then(registration => {
+ assert_true(
+ registration instanceof ServiceWorkerRegistration,
+ 'Successfully registered.');
+ return registration.unregister();
+ });
+ }, `Registering script with good MIME type ${validMimeType}`);
+
+ promise_test(() => {
+ var script = `resources/import-mime-type-worker.py?mime=${validMimeType}`;
+ var scope = 'resources/scope/good-mime-type-worker/';
+
+ return register_method(script, { scope }).then(registration => {
+ assert_true(
+ registration instanceof ServiceWorkerRegistration,
+ 'Successfully registered.');
+ return registration.unregister();
+ });
+ }, `Registering script that imports script with good MIME type ${validMimeType}`);
+ }
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-scope.js b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-scope.js
new file mode 100644
index 0000000..30c424b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-scope.js
@@ -0,0 +1,120 @@
+// Registration tests that mostly exercise the scope option.
+function registration_tests_scope(register_method) {
+ promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var scope = 'resources/scope%2fencoded-slash-in-scope';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'URL-encoded slash in the scope should be rejected.');
+ }, 'Scope including URL-encoded slash');
+
+ promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var scope = 'resources/scope%5cencoded-slash-in-scope';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'URL-encoded backslash in the scope should be rejected.');
+ }, 'Scope including URL-encoded backslash');
+
+ promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var scope = 'data:text/html,';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'scope URL scheme is not "http" or "https"');
+ }, 'Scope URL scheme is a data: URL');
+
+ promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var scope = new URL('resources', location).href.replace('https:', 'ftp:');
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'scope URL scheme is not "http" or "https"');
+ }, 'Scope URL scheme is an ftp: URL');
+
+ promise_test(function(t) {
+ // URL-encoded full-width 'scope'.
+ var name = '%ef%bd%93%ef%bd%83%ef%bd%8f%ef%bd%90%ef%bd%85';
+ var script = 'resources/empty-worker.js';
+ var scope = 'resources/' + name + '/escaped-multibyte-character-scope';
+ return register_method(script, {scope: scope})
+ .then(function(registration) {
+ assert_equals(
+ registration.scope,
+ normalizeURL(scope),
+ 'URL-encoded multibyte characters should be available.');
+ return registration.unregister();
+ });
+ }, 'Scope including URL-encoded multibyte characters');
+
+ promise_test(function(t) {
+ // Non-URL-encoded full-width "scope".
+ var name = String.fromCodePoint(0xff53, 0xff43, 0xff4f, 0xff50, 0xff45);
+ var script = 'resources/empty-worker.js';
+ var scope = 'resources/' + name + '/non-escaped-multibyte-character-scope';
+ return register_method(script, {scope: scope})
+ .then(function(registration) {
+ assert_equals(
+ registration.scope,
+ normalizeURL(scope),
+ 'Non-URL-encoded multibyte characters should be available.');
+ return registration.unregister();
+ });
+ }, 'Scope including non-escaped multibyte characters');
+
+ promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var scope = 'resources/././scope/self-reference-in-scope';
+ return register_method(script, {scope: scope})
+ .then(function(registration) {
+ assert_equals(
+ registration.scope,
+ normalizeURL('resources/scope/self-reference-in-scope'),
+ 'Scope including self-reference should be normalized.');
+ return registration.unregister();
+ });
+ }, 'Scope including self-reference');
+
+ promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var scope = 'resources/../resources/scope/parent-reference-in-scope';
+ return register_method(script, {scope: scope})
+ .then(function(registration) {
+ assert_equals(
+ registration.scope,
+ normalizeURL('resources/scope/parent-reference-in-scope'),
+ 'Scope including parent-reference should be normalized.');
+ return registration.unregister();
+ });
+ }, 'Scope including parent-reference');
+
+ promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var scope = 'resources/scope////consecutive-slashes-in-scope';
+ return register_method(script, {scope: scope})
+ .then(function(registration) {
+ // Although consecutive slashes in the scope are not unified, the
+ // scope is under the script directory and registration should
+ // succeed.
+ assert_equals(
+ registration.scope,
+ normalizeURL(scope),
+ 'Should successfully be registered.');
+ return registration.unregister();
+ })
+ }, 'Scope including consecutive slashes');
+
+ promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var scope = 'filesystem:' + normalizeURL('resources/scope/filesystem-scope-url');
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registering with the scope that has same-origin filesystem: URL ' +
+ 'should fail with TypeError.');
+ }, 'Scope URL is same-origin filesystem: URL');
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-script-url.js b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-script-url.js
new file mode 100644
index 0000000..55cbe6f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-script-url.js
@@ -0,0 +1,82 @@
+// Registration tests that mostly exercise the scriptURL parameter.
+function registration_tests_script_url(register_method) {
+ promise_test(function(t) {
+ var script = 'resources%2fempty-worker.js';
+ var scope = 'resources/scope/encoded-slash-in-script-url';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'URL-encoded slash in the script URL should be rejected.');
+ }, 'Script URL including URL-encoded slash');
+
+ promise_test(function(t) {
+ var script = 'resources%2Fempty-worker.js';
+ var scope = 'resources/scope/encoded-slash-in-script-url';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'URL-encoded slash in the script URL should be rejected.');
+ }, 'Script URL including uppercase URL-encoded slash');
+
+ promise_test(function(t) {
+ var script = 'resources%5cempty-worker.js';
+ var scope = 'resources/scope/encoded-slash-in-script-url';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'URL-encoded backslash in the script URL should be rejected.');
+ }, 'Script URL including URL-encoded backslash');
+
+ promise_test(function(t) {
+ var script = 'resources%5Cempty-worker.js';
+ var scope = 'resources/scope/encoded-slash-in-script-url';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'URL-encoded backslash in the script URL should be rejected.');
+ }, 'Script URL including uppercase URL-encoded backslash');
+
+ promise_test(function(t) {
+ var script = 'data:application/javascript,';
+ var scope = 'resources/scope/data-url-in-script-url';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Data URLs should not be registered as service workers.');
+ }, 'Script URL is a data URL');
+
+ promise_test(function(t) {
+ var script = 'data:application/javascript,';
+ var scope = new URL('resources/scope/data-url-in-script-url', location);
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Data URLs should not be registered as service workers.');
+ }, 'Script URL is a data URL and scope URL is not relative');
+
+ promise_test(function(t) {
+ var script = 'resources/././empty-worker.js';
+ var scope = 'resources/scope/parent-reference-in-script-url';
+ return register_method(script, {scope: scope})
+ .then(function(registration) {
+ assert_equals(
+ get_newest_worker(registration).scriptURL,
+ normalizeURL('resources/empty-worker.js'),
+ 'Script URL including self-reference should be normalized.');
+ return registration.unregister();
+ });
+ }, 'Script URL including self-reference');
+
+ promise_test(function(t) {
+ var script = 'resources/../resources/empty-worker.js';
+ var scope = 'resources/scope/parent-reference-in-script-url';
+ return register_method(script, {scope: scope})
+ .then(function(registration) {
+ assert_equals(
+ get_newest_worker(registration).scriptURL,
+ normalizeURL('resources/empty-worker.js'),
+ 'Script URL including parent-reference should be normalized.');
+ return registration.unregister();
+ });
+ }, 'Script URL including parent-reference');
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-script.js b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-script.js
new file mode 100644
index 0000000..e5bdaf4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-script.js
@@ -0,0 +1,121 @@
+// Registration tests that mostly exercise the service worker script contents or
+// response.
+function registration_tests_script(register_method, type) {
+ promise_test(function(t) {
+ var script = 'resources/invalid-chunked-encoding.py';
+ var scope = 'resources/scope/invalid-chunked-encoding/';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of invalid chunked encoding script should fail.');
+ }, 'Registering invalid chunked encoding script');
+
+ promise_test(function(t) {
+ var script = 'resources/invalid-chunked-encoding-with-flush.py';
+ var scope = 'resources/scope/invalid-chunked-encoding-with-flush/';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of invalid chunked encoding script should fail.');
+ }, 'Registering invalid chunked encoding script with flush');
+
+ promise_test(function(t) {
+ var script = 'resources/malformed-worker.py?parse-error';
+ var scope = 'resources/scope/parse-error';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of script including parse error should fail.');
+ }, 'Registering script including parse error');
+
+ promise_test(function(t) {
+ var script = 'resources/malformed-worker.py?undefined-error';
+ var scope = 'resources/scope/undefined-error';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of script including undefined error should fail.');
+ }, 'Registering script including undefined error');
+
+ promise_test(function(t) {
+ var script = 'resources/malformed-worker.py?uncaught-exception';
+ var scope = 'resources/scope/uncaught-exception';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of script including uncaught exception should fail.');
+ }, 'Registering script including uncaught exception');
+
+ if (type === 'classic') {
+ promise_test(function(t) {
+ var script = 'resources/malformed-worker.py?import-malformed-script';
+ var scope = 'resources/scope/import-malformed-script';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of script importing malformed script should fail.');
+ }, 'Registering script importing malformed script');
+ }
+
+ if (type === 'module') {
+ promise_test(function(t) {
+ var script = 'resources/malformed-worker.py?top-level-await';
+ var scope = 'resources/scope/top-level-await';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of script with top-level await should fail.');
+ }, 'Registering script with top-level await');
+
+ promise_test(function(t) {
+ var script = 'resources/malformed-worker.py?instantiation-error';
+ var scope = 'resources/scope/instantiation-error';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of script with module instantiation error should fail.');
+ }, 'Registering script with module instantiation error');
+
+ promise_test(function(t) {
+ var script = 'resources/malformed-worker.py?instantiation-error-and-top-level-await';
+ var scope = 'resources/scope/instantiation-error-and-top-level-await';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of script with module instantiation error and top-level await should fail.');
+ }, 'Registering script with module instantiation error and top-level await');
+ }
+
+ promise_test(function(t) {
+ var script = 'resources/no-such-worker.js';
+ var scope = 'resources/scope/no-such-worker';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of non-existent script should fail.');
+ }, 'Registering non-existent script');
+
+ if (type === 'classic') {
+ promise_test(function(t) {
+ var script = 'resources/malformed-worker.py?import-no-such-script';
+ var scope = 'resources/scope/import-no-such-script';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registration of script importing non-existent script should fail.');
+ }, 'Registering script importing non-existent script');
+ }
+
+ promise_test(function(t) {
+ var script = 'resources/malformed-worker.py?caught-exception';
+ var scope = 'resources/scope/caught-exception';
+ return register_method(script, {scope: scope})
+ .then(function(registration) {
+ assert_true(
+ registration instanceof ServiceWorkerRegistration,
+ 'Successfully registered.');
+ return registration.unregister();
+ });
+ }, 'Registering script including caught exception');
+
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-security-error.js b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-security-error.js
new file mode 100644
index 0000000..c45fbd4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-tests-security-error.js
@@ -0,0 +1,78 @@
+// Registration tests that mostly exercise SecurityError cases.
+function registration_tests_security_error(register_method) {
+ promise_test(function(t) {
+ var script = 'resources/registration-worker.js';
+ var scope = 'resources';
+ return promise_rejects_dom(t,
+ 'SecurityError',
+ register_method(script, {scope: scope}),
+ 'Registering same scope as the script directory without the last ' +
+ 'slash should fail with SecurityError.');
+ }, 'Registering same scope as the script directory without the last slash');
+
+ promise_test(function(t) {
+ var script = 'resources/registration-worker.js';
+ var scope = 'different-directory/';
+ return promise_rejects_dom(t,
+ 'SecurityError',
+ register_method(script, {scope: scope}),
+ 'Registration scope outside the script directory should fail ' +
+ 'with SecurityError.');
+ }, 'Registration scope outside the script directory');
+
+ promise_test(function(t) {
+ var script = 'resources/registration-worker.js';
+ var scope = 'http://example.com/';
+ return promise_rejects_dom(t,
+ 'SecurityError',
+ register_method(script, {scope: scope}),
+ 'Registration scope outside domain should fail with SecurityError.');
+ }, 'Registering scope outside domain');
+
+ promise_test(function(t) {
+ var script = 'http://example.com/worker.js';
+ var scope = 'http://example.com/scope/';
+ return promise_rejects_dom(t,
+ 'SecurityError',
+ register_method(script, {scope: scope}),
+ 'Registration script outside domain should fail with SecurityError.');
+ }, 'Registering script outside domain');
+
+ promise_test(function(t) {
+ var script = 'resources/redirect.py?Redirect=' +
+ encodeURIComponent('/resources/registration-worker.js');
+ var scope = 'resources/scope/redirect/';
+ return promise_rejects_dom(t,
+ 'SecurityError',
+ register_method(script, {scope: scope}),
+ 'Registration of redirected script should fail.');
+ }, 'Registering redirected script');
+
+ promise_test(function(t) {
+ var script = 'resources/empty-worker.js';
+ var scope = 'resources/../scope/parent-reference-in-scope';
+ return promise_rejects_dom(t,
+ 'SecurityError',
+ register_method(script, {scope: scope}),
+ 'Scope not under the script directory should be rejected.');
+ }, 'Scope including parent-reference and not under the script directory');
+
+ promise_test(function(t) {
+ var script = 'resources////empty-worker.js';
+ var scope = 'resources/scope/consecutive-slashes-in-script-url';
+ return promise_rejects_dom(t,
+ 'SecurityError',
+ register_method(script, {scope: scope}),
+ 'Consecutive slashes in the script url should not be unified.');
+ }, 'Script URL including consecutive slashes');
+
+ promise_test(function(t) {
+ var script = 'filesystem:' + normalizeURL('resources/empty-worker.js');
+ var scope = 'resources/scope/filesystem-script-url';
+ return promise_rejects_js(t,
+ TypeError,
+ register_method(script, {scope: scope}),
+ 'Registering a script which has same-origin filesystem: URL should ' +
+ 'fail with TypeError.');
+ }, 'Script URL is same-origin filesystem: URL');
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/registration-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-worker.js
new file mode 100644
index 0000000..44d1d27
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/registration-worker.js
@@ -0,0 +1 @@
+// empty for now
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/reject-install-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/reject-install-worker.js
new file mode 100644
index 0000000..41f07fd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/reject-install-worker.js
@@ -0,0 +1,3 @@
+self.oninstall = function(event) {
+ event.waitUntil(Promise.reject());
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/reply-to-message.html b/third_party/web_platform_tests/service-workers/service-worker/resources/reply-to-message.html
new file mode 100644
index 0000000..8a70e2a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/reply-to-message.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<script>
+window.addEventListener('message', event => {
+ var port = event.ports[0];
+ port.postMessage(event.data);
+ });
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/request-end-to-end-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/request-end-to-end-worker.js
new file mode 100644
index 0000000..6bd2b72
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/request-end-to-end-worker.js
@@ -0,0 +1,34 @@
+'use strict';
+
+onfetch = function(e) {
+ var headers = {};
+ for (var header of e.request.headers) {
+ var key = header[0], value = header[1];
+ headers[key] = value;
+ }
+ var append_header_error = '';
+ try {
+ e.request.headers.append('Test-Header', 'TestValue');
+ } catch (error) {
+ append_header_error = error.name;
+ }
+
+ var request_construct_error = '';
+ try {
+ new Request(e.request, {method: 'GET'});
+ } catch (error) {
+ request_construct_error = error.name;
+ }
+
+ e.respondWith(new Response(JSON.stringify({
+ url: e.request.url,
+ method: e.request.method,
+ referrer: e.request.referrer,
+ headers: headers,
+ mode: e.request.mode,
+ credentials: e.request.credentials,
+ redirect: e.request.redirect,
+ append_header_error: append_header_error,
+ request_construct_error: request_construct_error
+ })));
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/request-headers.py b/third_party/web_platform_tests/service-workers/service-worker/resources/request-headers.py
new file mode 100644
index 0000000..6ab148e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/request-headers.py
@@ -0,0 +1,8 @@
+import json
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ data = {isomorphic_decode(key):isomorphic_decode(request.headers[key]) for key, value in request.headers.items()}
+
+ return [(b"Content-Type", b"application/json")], json.dumps(data)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html b/third_party/web_platform_tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html
new file mode 100644
index 0000000..384c29b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<script src="empty.js"></script>
+<script src="sample.js"></script>
+<script src="redirect.py?Redirect=empty.js"></script>
+<img src="square.png">
+<img src="https://{{hosts[alt][]}}:{{ports[https][0]}}/service-workers/service-worker/resources/square.png">
+<img src="missing.jpg">
+<img src="https://{{hosts[alt][]}}:{{ports[https][0]}}/service-workers/service-worker/resources/missing.jpg">
+<img src='missing.jpg?SWRespondsWithFetch'>
+<script src='empty-worker.js'></script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/resource-timing-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/resource-timing-worker.js
new file mode 100644
index 0000000..b74e8cd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/resource-timing-worker.js
@@ -0,0 +1,12 @@
+self.addEventListener('fetch', function(event) {
+ if (event.request.url.indexOf('sample.js') != -1) {
+ event.respondWith(new Promise(resolve => {
+ // Slightly delay the response so we ensure we get a non-zero
+ // duration.
+ setTimeout(_ => resolve(new Response('// Empty javascript')), 50);
+ }));
+ }
+ else if (event.request.url.indexOf('missing.jpg?SWRespondsWithFetch') != -1) {
+ event.respondWith(fetch('sample.txt?SWFetched'));
+ }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/respond-then-throw-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/respond-then-throw-worker.js
new file mode 100644
index 0000000..adb48de
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/respond-then-throw-worker.js
@@ -0,0 +1,40 @@
+var syncport = null;
+
+self.addEventListener('message', function(e) {
+ if ('port' in e.data) {
+ if (syncport) {
+ syncport(e.data.port);
+ } else {
+ syncport = e.data.port;
+ }
+ }
+});
+
+function sync() {
+ return new Promise(function(resolve) {
+ if (syncport) {
+ resolve(syncport);
+ } else {
+ syncport = resolve;
+ }
+ }).then(function(port) {
+ port.postMessage('SYNC');
+ return new Promise(function(resolve) {
+ port.onmessage = function(e) {
+ if (e.data === 'ACK') {
+ resolve();
+ }
+ }
+ });
+ });
+}
+
+
+self.addEventListener('fetch', function(event) {
+ // In Firefox the result would depend on a race between fetch handling
+ // and exception handling code. On the assumption that this might be a common
+ // design error, we explicitly allow the exception to be handled first.
+ event.respondWith(sync().then(() => new Response('intercepted')));
+
+ throw("error");
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/respond-with-body-accessed-response-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/respond-with-body-accessed-response-iframe.html
new file mode 100644
index 0000000..7be3148
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/respond-with-body-accessed-response-iframe.html
@@ -0,0 +1,20 @@
+<script>
+var callback;
+
+// Creates a <script> element with |url| source, and returns a promise for the
+// result of the executed script. Uses JSONP because some responses to |url|
+// are opaque so their body cannot be tested directly.
+function getJSONP(url) {
+ var sc = document.createElement('script');
+ sc.src = url;
+ var promise = new Promise(function(resolve, reject) {
+ // This callback function is called by appending a script element.
+ callback = resolve;
+ sc.addEventListener(
+ 'error',
+ function() { reject('Failed to load url:' + url); });
+ });
+ document.body.appendChild(sc);
+ return promise;
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/respond-with-body-accessed-response-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/respond-with-body-accessed-response-worker.js
new file mode 100644
index 0000000..c602109
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/respond-with-body-accessed-response-worker.js
@@ -0,0 +1,93 @@
+importScripts('/common/get-host-info.sub.js');
+importScripts('test-helpers.sub.js');
+
+function getQueryParams(url) {
+ var search = (new URL(url)).search;
+ if (!search) {
+ return {};
+ }
+ var ret = {};
+ var params = search.substring(1).split('&');
+ params.forEach(function(param) {
+ var element = param.split('=');
+ ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]);
+ });
+ return ret;
+}
+
+function createResponse(params) {
+ if (params['type'] == 'basic') {
+ return fetch('respond-with-body-accessed-response.jsonp');
+ }
+ if (params['type'] == 'opaque') {
+ return fetch(get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() +
+ 'respond-with-body-accessed-response.jsonp',
+ {mode: 'no-cors'});
+ }
+ if (params['type'] == 'default') {
+ return Promise.resolve(new Response('callback(\'OK\');'));
+ }
+
+ return Promise.reject(new Error('unexpected type :' + params['type']));
+}
+
+function cloneResponseIfNeeded(params, response) {
+ if (params['clone'] == '1') {
+ return response.clone();
+ } else if (params['clone'] == '2') {
+ response.clone();
+ return response;
+ }
+ return response;
+}
+
+function passThroughCacheIfNeeded(params, request, response) {
+ return new Promise(function(resolve) {
+ if (params['passThroughCache'] == 'true') {
+ var cache_name = request.url;
+ var cache;
+ self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ })
+ .then(function(c) {
+ cache = c;
+ return cache.put(request, response);
+ })
+ .then(function() {
+ return cache.match(request.url);
+ })
+ .then(function(res) {
+ // Touch .body here to test the behavior after touching it.
+ res.body;
+ resolve(res);
+ });
+ } else {
+ resolve(response);
+ }
+ })
+}
+
+self.addEventListener('fetch', function(event) {
+ if (event.request.url.indexOf('TestRequest') == -1) {
+ return;
+ }
+ var params = getQueryParams(event.request.url);
+ event.respondWith(
+ createResponse(params)
+ .then(function(response) {
+ // Touch .body here to test the behavior after touching it.
+ response.body;
+ return cloneResponseIfNeeded(params, response);
+ })
+ .then(function(response) {
+ // Touch .body here to test the behavior after touching it.
+ response.body;
+ return passThroughCacheIfNeeded(params, event.request, response);
+ })
+ .then(function(response) {
+ // Touch .body here to test the behavior after touching it.
+ response.body;
+ return response;
+ }));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/respond-with-body-accessed-response.jsonp b/third_party/web_platform_tests/service-workers/service-worker/resources/respond-with-body-accessed-response.jsonp
new file mode 100644
index 0000000..b9c28f5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/respond-with-body-accessed-response.jsonp
@@ -0,0 +1 @@
+callback('OK');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/sample-worker-interceptor.js b/third_party/web_platform_tests/service-workers/service-worker/resources/sample-worker-interceptor.js
new file mode 100644
index 0000000..c06f8dd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/sample-worker-interceptor.js
@@ -0,0 +1,62 @@
+importScripts('/common/get-host-info.sub.js');
+
+const text = 'worker loading intercepted by service worker';
+const dedicated_worker_script = `postMessage('${text}');`;
+const shared_worker_script =
+ `onconnect = evt => evt.ports[0].postMessage('${text}');`;
+
+let source;
+let resolveDone;
+let done = new Promise(resolve => resolveDone = resolve);
+
+// The page messages this worker to ask for the result. Keep the worker alive
+// via waitUntil() until the result is sent.
+self.addEventListener('message', event => {
+ source = event.data.port;
+ source.postMessage({id: event.source.id});
+ source.onmessage = resolveDone;
+ event.waitUntil(done);
+});
+
+self.onfetch = event => {
+ const url = event.request.url;
+ const destination = event.request.destination;
+
+ if (source)
+ source.postMessage({clientId:event.clientId, resultingClientId: event.resultingClientId});
+
+ // Request handler for a synthesized response.
+ if (url.indexOf('synthesized') != -1) {
+ let script_headers = new Headers({ "Content-Type": "text/javascript" });
+ if (destination === 'worker')
+ event.respondWith(new Response(dedicated_worker_script, { 'headers': script_headers }));
+ else if (destination === 'sharedworker')
+ event.respondWith(new Response(shared_worker_script, { 'headers': script_headers }));
+ else
+ event.respondWith(new Response('Unexpected request! ' + destination));
+ return;
+ }
+
+ // Request handler for a same-origin response.
+ if (url.indexOf('same-origin') != -1) {
+ event.respondWith(fetch('postmessage-on-load-worker.js'));
+ return;
+ }
+
+ // Request handler for a cross-origin response.
+ if (url.indexOf('cors') != -1) {
+ const filename = 'postmessage-on-load-worker.js';
+ const path = (new URL(filename, self.location)).pathname;
+ let new_url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + path;
+ let mode;
+ if (url.indexOf('no-cors') != -1) {
+ // Test no-cors mode.
+ mode = 'no-cors';
+ } else {
+ // Test cors mode.
+ new_url += '?pipe=header(Access-Control-Allow-Origin,*)';
+ mode = 'cors';
+ }
+ event.respondWith(fetch(new_url, { mode: mode }));
+ }
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/sample.html b/third_party/web_platform_tests/service-workers/service-worker/resources/sample.html
new file mode 100644
index 0000000..12a1799
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/sample.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<body>Hello world
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/sample.txt b/third_party/web_platform_tests/service-workers/service-worker/resources/sample.txt
new file mode 100644
index 0000000..802992c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/sample.txt
@@ -0,0 +1 @@
+Hello world
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.html
new file mode 100644
index 0000000..239fa73
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.html
@@ -0,0 +1,63 @@
+<script>
+function with_iframe(url) {
+ return new Promise(resolve => {
+ let frame = document.createElement('iframe');
+ frame.src = url;
+ frame.onload = () => { resolve(frame); };
+ document.body.appendChild(frame);
+ });
+}
+
+function with_sandboxed_iframe(url, sandbox) {
+ return new Promise(resolve => {
+ let frame = document.createElement('iframe');
+ frame.sandbox = sandbox;
+ frame.src = url;
+ frame.onload = () => { resolve(frame); };
+ document.body.appendChild(frame);
+ });
+}
+
+function fetch_from_worker(url) {
+ return new Promise(resolve => {
+ let blob = new Blob([
+ `fetch('${url}', {mode: 'no-cors'})` +
+ " .then(() => { self.postMessage('OK'); });"]);
+ let worker_url = URL.createObjectURL(blob);
+ let worker = new Worker(worker_url);
+ worker.onmessage = resolve;
+ });
+}
+
+function run_test(type) {
+ const base_path = location.href;
+ switch (type) {
+ case 'fetch':
+ return fetch(`${base_path}&test=fetch`, {mode: 'no-cors'});
+ case 'fetch-from-worker':
+ return fetch_from_worker(`${base_path}&test=fetch-from-worker`);
+ case 'iframe':
+ return with_iframe(`${base_path}&test=iframe`);
+ case 'sandboxed-iframe':
+ return with_sandboxed_iframe(`${base_path}&test=sandboxed-iframe`,
+ "allow-scripts");
+ case 'sandboxed-iframe-same-origin':
+ return with_sandboxed_iframe(
+ `${base_path}&test=sandboxed-iframe-same-origin`,
+ "allow-scripts allow-same-origin");
+ default:
+ return Promise.reject(`Unknown type: ${type}`);
+ }
+}
+
+window.onmessage = event => {
+ let id = event.data['id'];
+ run_test(event.data['type'])
+ .then(() => {
+ window.top.postMessage({id: id, result: 'done'}, '*');
+ })
+ .catch(e => {
+ window.top.postMessage({id: id, result: 'error: ' + e.toString()}, '*');
+ });
+};
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.py b/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.py
new file mode 100644
index 0000000..409a15b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.py
@@ -0,0 +1,18 @@
+import os.path
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ header = [(b'Content-Type', b'text/html')]
+ if b'test' in request.GET:
+ with open(os.path.join(os.path.dirname(isomorphic_decode(__file__)), u'blank.html'), u'r') as f:
+ body = f.read()
+ return (header, body)
+
+ if b'sandbox' in request.GET:
+ header.append((b'Content-Security-Policy',
+ b'sandbox %s' % request.GET[b'sandbox']))
+ with open(os.path.join(os.path.dirname(isomorphic_decode(__file__)),
+ u'sandboxed-iframe-fetch-event-iframe.html'), u'r') as f:
+ body = f.read()
+ return (header, body)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-worker.js
new file mode 100644
index 0000000..4035a8b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-worker.js
@@ -0,0 +1,20 @@
+var requests = [];
+
+self.addEventListener('message', function(event) {
+ event.waitUntil(self.clients.matchAll()
+ .then(function(clients) {
+ var client_urls = [];
+ for(var client of clients){
+ client_urls.push(client.url);
+ }
+ client_urls = client_urls.sort();
+ event.data.port.postMessage(
+ {clients: client_urls, requests: requests});
+ requests = [];
+ }));
+ });
+
+self.addEventListener('fetch', function(event) {
+ requests.push(event.request.url);
+ event.respondWith(fetch(event.request));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-navigator-serviceworker-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-navigator-serviceworker-iframe.html
new file mode 100644
index 0000000..1d682e4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/sandboxed-iframe-navigator-serviceworker-iframe.html
@@ -0,0 +1,25 @@
+<script>
+window.onmessage = function(e) {
+ const id = e.data['id'];
+ try {
+ var sw = window.navigator.serviceWorker;
+ } catch (e) {
+ window.top.postMessage({
+ id: id,
+ result: 'navigator.serviceWorker failed: ' + e.name
+ }, '*');
+ return;
+ }
+
+ window.navigator.serviceWorker.getRegistration()
+ .then(function() {
+ window.top.postMessage({id: id, result:'ok'}, '*');
+ })
+ .catch(function(e) {
+ window.top.postMessage({
+ id: id,
+ result: 'getRegistration() failed: ' + e.name
+ }, '*');
+ });
+};
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/scope1/module-worker-importing-redirect-to-scope2.js b/third_party/web_platform_tests/service-workers/service-worker/resources/scope1/module-worker-importing-redirect-to-scope2.js
new file mode 100644
index 0000000..ae681ba
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/scope1/module-worker-importing-redirect-to-scope2.js
@@ -0,0 +1 @@
+import * as module from './redirect.py?Redirect=/service-workers/service-worker/resources/scope2/imported-module-script.js';
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/scope1/module-worker-importing-scope2.js b/third_party/web_platform_tests/service-workers/service-worker/resources/scope1/module-worker-importing-scope2.js
new file mode 100644
index 0000000..e285052
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/scope1/module-worker-importing-scope2.js
@@ -0,0 +1 @@
+import * as module from '../scope2/imported-module-script.js';
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/scope1/redirect.py b/third_party/web_platform_tests/service-workers/service-worker/resources/scope1/redirect.py
new file mode 100644
index 0000000..bb4c874
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/scope1/redirect.py
@@ -0,0 +1,6 @@
+import os
+import imp
+# Use the file from the parent directory.
+mod = imp.load_source("_parent", os.path.join(os.path.dirname(os.path.dirname(__file__)),
+ os.path.basename(__file__)))
+main = mod.main
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/import-scripts-echo.py b/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/import-scripts-echo.py
new file mode 100644
index 0000000..5f785b5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/import-scripts-echo.py
@@ -0,0 +1,6 @@
+def main(req, res):
+ return ([
+ (b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache'),
+ (b'Content-Type', b'application/javascript')],
+ b'echo_output = "%s (scope2/)";\n' % req.GET[b'msg'])
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/imported-module-script.js b/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/imported-module-script.js
new file mode 100644
index 0000000..a18e704
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/imported-module-script.js
@@ -0,0 +1,4 @@
+export const imported = 'A module script.';
+onmessage = msg => {
+ msg.source.postMessage('pong');
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/simple.txt b/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/simple.txt
new file mode 100644
index 0000000..cd87667
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/simple.txt
@@ -0,0 +1 @@
+a simple text file (scope2/)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py
new file mode 100644
index 0000000..bb4c874
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py
@@ -0,0 +1,6 @@
+import os
+import imp
+# Use the file from the parent directory.
+mod = imp.load_source("_parent", os.path.join(os.path.dirname(os.path.dirname(__file__)),
+ os.path.basename(__file__)))
+main = mod.main
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/secure-context-service-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/secure-context-service-worker.js
new file mode 100644
index 0000000..5ba99f0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/secure-context-service-worker.js
@@ -0,0 +1,21 @@
+self.addEventListener('fetch', event => {
+ let url = new URL(event.request.url);
+ if (url.pathname.indexOf('sender.html') != -1) {
+ event.respondWith(new Response(
+ "<script>window.parent.postMessage('interception', '*');</script>",
+ { headers: { 'Content-Type': 'text/html'} }
+ ));
+ } else if (url.pathname.indexOf('report') != -1) {
+ self.clients.matchAll().then(clients => {
+ for (client of clients) {
+ client.postMessage(url.searchParams.get('result'));
+ }
+ });
+ event.respondWith(
+ new Response(
+ '<script>window.close()</script>',
+ { headers: { 'Content-Type': 'text/html'} }
+ )
+ );
+ }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/secure-context/sender.html b/third_party/web_platform_tests/service-workers/service-worker/resources/secure-context/sender.html
new file mode 100644
index 0000000..05e5882
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/secure-context/sender.html
@@ -0,0 +1 @@
+<script>window.parent.postMessage('network', '*');</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/secure-context/window.html b/third_party/web_platform_tests/service-workers/service-worker/resources/secure-context/window.html
new file mode 100644
index 0000000..071a507
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/secure-context/window.html
@@ -0,0 +1,15 @@
+<body>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="../test-helpers.sub.js"></script>
+<script>
+const HTTPS_PREFIX = get_host_info().HTTPS_ORIGIN + base_path();
+
+window.onmessage = event => {
+ window.location = HTTPS_PREFIX + 'report?result=' + event.data;
+};
+
+const frame = document.createElement('iframe');
+frame.src = HTTPS_PREFIX + 'sender.html';
+document.body.appendChild(frame);
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-csp-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-csp-worker.py
new file mode 100644
index 0000000..35a4696
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-csp-worker.py
@@ -0,0 +1,183 @@
+bodyDefault = b'''
+importScripts('worker-testharness.js');
+importScripts('test-helpers.sub.js');
+importScripts('/common/get-host-info.sub.js');
+
+var host_info = get_host_info();
+
+test(function() {
+ var import_script_failed = false;
+ try {
+ importScripts(host_info.HTTPS_REMOTE_ORIGIN +
+ base_path() + 'empty.js');
+ } catch(e) {
+ import_script_failed = true;
+ }
+ assert_true(import_script_failed,
+ 'Importing the other origins script should fail.');
+ }, 'importScripts test for default-src');
+
+test(function() {
+ assert_throws_js(EvalError,
+ function() { eval('1 + 1'); },
+ 'eval() should throw EvalError.')
+ assert_throws_js(EvalError,
+ function() { new Function('1 + 1'); },
+ 'new Function() should throw EvalError.')
+ }, 'eval test for default-src');
+
+async_test(function(t) {
+ fetch(host_info.HTTPS_REMOTE_ORIGIN +
+ base_path() + 'fetch-access-control.py?ACAOrigin=*',
+ {mode: 'cors'})
+ .then(function(response){
+ assert_unreached('fetch should fail.');
+ }, function(){
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Fetch test for default-src');
+
+async_test(function(t) {
+ var REDIRECT_URL = host_info.HTTPS_ORIGIN +
+ base_path() + 'redirect.py?Redirect=';
+ var OTHER_BASE_URL = host_info.HTTPS_REMOTE_ORIGIN +
+ base_path() + 'fetch-access-control.py?'
+ fetch(REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'),
+ {mode: 'cors'})
+ .then(function(response){
+ assert_unreached('Redirected fetch should fail.');
+ }, function(){
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Redirected fetch test for default-src');'''
+
+bodyScript = b'''
+importScripts('worker-testharness.js');
+importScripts('test-helpers.sub.js');
+importScripts('/common/get-host-info.sub.js');
+
+var host_info = get_host_info();
+
+test(function() {
+ var import_script_failed = false;
+ try {
+ importScripts(host_info.HTTPS_REMOTE_ORIGIN +
+ base_path() + 'empty.js');
+ } catch(e) {
+ import_script_failed = true;
+ }
+ assert_true(import_script_failed,
+ 'Importing the other origins script should fail.');
+ }, 'importScripts test for script-src');
+
+test(function() {
+ assert_throws_js(EvalError,
+ function() { eval('1 + 1'); },
+ 'eval() should throw EvalError.')
+ assert_throws_js(EvalError,
+ function() { new Function('1 + 1'); },
+ 'new Function() should throw EvalError.')
+ }, 'eval test for script-src');
+
+async_test(function(t) {
+ fetch(host_info.HTTPS_REMOTE_ORIGIN +
+ base_path() + 'fetch-access-control.py?ACAOrigin=*',
+ {mode: 'cors'})
+ .then(function(response){
+ t.done();
+ }, function(){
+ assert_unreached('fetch should not fail.');
+ })
+ .catch(unreached_rejection(t));
+ }, 'Fetch test for script-src');
+
+async_test(function(t) {
+ var REDIRECT_URL = host_info.HTTPS_ORIGIN +
+ base_path() + 'redirect.py?Redirect=';
+ var OTHER_BASE_URL = host_info.HTTPS_REMOTE_ORIGIN +
+ base_path() + 'fetch-access-control.py?'
+ fetch(REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'),
+ {mode: 'cors'})
+ .then(function(response){
+ t.done();
+ }, function(){
+ assert_unreached('Redirected fetch should not fail.');
+ })
+ .catch(unreached_rejection(t));
+ }, 'Redirected fetch test for script-src');'''
+
+bodyConnect = b'''
+importScripts('worker-testharness.js');
+importScripts('test-helpers.sub.js');
+importScripts('/common/get-host-info.sub.js');
+
+var host_info = get_host_info();
+
+test(function() {
+ var import_script_failed = false;
+ try {
+ importScripts(host_info.HTTPS_REMOTE_ORIGIN +
+ base_path() + 'empty.js');
+ } catch(e) {
+ import_script_failed = true;
+ }
+ assert_false(import_script_failed,
+ 'Importing the other origins script should not fail.');
+ }, 'importScripts test for connect-src');
+
+test(function() {
+ var eval_failed = false;
+ try {
+ eval('1 + 1');
+ new Function('1 + 1');
+ } catch(e) {
+ eval_failed = true;
+ }
+ assert_false(eval_failed,
+ 'connect-src without unsafe-eval should not block eval().');
+ }, 'eval test for connect-src');
+
+async_test(function(t) {
+ fetch(host_info.HTTPS_REMOTE_ORIGIN +
+ base_path() + 'fetch-access-control.py?ACAOrigin=*',
+ {mode: 'cors'})
+ .then(function(response){
+ assert_unreached('fetch should fail.');
+ }, function(){
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Fetch test for connect-src');
+
+async_test(function(t) {
+ var REDIRECT_URL = host_info.HTTPS_ORIGIN +
+ base_path() + 'redirect.py?Redirect=';
+ var OTHER_BASE_URL = host_info.HTTPS_REMOTE_ORIGIN +
+ base_path() + 'fetch-access-control.py?'
+ fetch(REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'),
+ {mode: 'cors'})
+ .then(function(response){
+ assert_unreached('Redirected fetch should fail.');
+ }, function(){
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Redirected fetch test for connect-src');'''
+
+def main(request, response):
+ headers = []
+ headers.append((b'Content-Type', b'application/javascript'))
+ directive = request.GET[b'directive']
+ body = b'ERROR: Unknown directive'
+ if directive == b'default':
+ headers.append((b'Content-Security-Policy', b"default-src 'self'"))
+ body = bodyDefault
+ elif directive == b'script':
+ headers.append((b'Content-Security-Policy', b"script-src 'self'"))
+ body = bodyScript
+ elif directive == b'connect':
+ headers.append((b'Content-Security-Policy', b"connect-src 'self'"))
+ body = bodyConnect
+ return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-header.py b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-header.py
new file mode 100644
index 0000000..d64a9d2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-header.py
@@ -0,0 +1,20 @@
+def main(request, response):
+ service_worker_header = request.headers.get(b'service-worker')
+
+ if b'header' in request.GET and service_worker_header != b'script':
+ return 400, [(b'Content-Type', b'text/plain')], b'Bad Request'
+
+ if b'no-header' in request.GET and service_worker_header == b'script':
+ return 400, [(b'Content-Type', b'text/plain')], b'Bad Request'
+
+ # no-cache itself to ensure the user agent finds a new version for each
+ # update.
+ headers = [(b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache'),
+ (b'Content-Type', b'application/javascript')]
+ body = b'/* This is a service worker script */\n'
+
+ if b'import' in request.GET:
+ body += b"importScripts('%s');" % request.GET[b'import']
+
+ return 200, headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-dynamic-import-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-dynamic-import-worker.js
new file mode 100644
index 0000000..680e07f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-dynamic-import-worker.js
@@ -0,0 +1 @@
+import('./service-worker-interception-network-worker.js');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-network-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-network-worker.js
new file mode 100644
index 0000000..5ff3900
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-network-worker.js
@@ -0,0 +1 @@
+postMessage('LOADED_FROM_NETWORK');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-service-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-service-worker.js
new file mode 100644
index 0000000..6b43a37
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-service-worker.js
@@ -0,0 +1,9 @@
+const kURL = '/service-worker-interception-network-worker.js';
+const kScript = 'postMessage("LOADED_FROM_SERVICE_WORKER")';
+const kHeaders = [['content-type', 'text/javascript']];
+
+self.addEventListener('fetch', e => {
+ // Serve a generated response for kURL.
+ if (e.request.url.indexOf(kURL) != -1)
+ e.respondWith(new Response(kScript, { headers: kHeaders }));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-static-import-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-static-import-worker.js
new file mode 100644
index 0000000..e570958
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/service-worker-interception-static-import-worker.js
@@ -0,0 +1 @@
+import './service-worker-interception-network-worker.js';
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/silence.oga b/third_party/web_platform_tests/service-workers/service-worker/resources/silence.oga
new file mode 100644
index 0000000..af59188
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/silence.oga
Binary files differ
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/simple-intercept-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/simple-intercept-worker.js
new file mode 100644
index 0000000..f8b5f8c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/simple-intercept-worker.js
@@ -0,0 +1,5 @@
+self.onfetch = function(event) {
+ if (event.request.url.indexOf('simple') != -1)
+ event.respondWith(
+ new Response(new Blob(['intercepted by service worker'])));
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/simple-intercept-worker.js.headers b/third_party/web_platform_tests/service-workers/service-worker/resources/simple-intercept-worker.js.headers
new file mode 100644
index 0000000..a17a9a3
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/simple-intercept-worker.js.headers
@@ -0,0 +1 @@
+Content-Type: application/javascript
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/simple.html b/third_party/web_platform_tests/service-workers/service-worker/resources/simple.html
new file mode 100644
index 0000000..0c3e3e7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/simple.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<title>Simple</title>
+Here's a simple html file.
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/simple.txt b/third_party/web_platform_tests/service-workers/service-worker/resources/simple.txt
new file mode 100644
index 0000000..9e3cb91
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/simple.txt
@@ -0,0 +1 @@
+a simple text file
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/skip-waiting-installed-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/skip-waiting-installed-worker.js
new file mode 100644
index 0000000..6f7008b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/skip-waiting-installed-worker.js
@@ -0,0 +1,33 @@
+var saw_activate_event = false
+
+self.addEventListener('activate', function() {
+ saw_activate_event = true;
+ });
+
+self.addEventListener('message', function(event) {
+ var port = event.data.port;
+ event.waitUntil(self.skipWaiting()
+ .then(function(result) {
+ if (result !== undefined) {
+ port.postMessage('FAIL: Promise should be resolved with undefined');
+ return;
+ }
+
+ if (!saw_activate_event) {
+ port.postMessage(
+ 'FAIL: Promise should be resolved after activate event is dispatched');
+ return;
+ }
+
+ if (self.registration.active.state !== 'activating') {
+ port.postMessage(
+ 'FAIL: Promise should be resolved before ServiceWorker#state is set to activated');
+ return;
+ }
+
+ port.postMessage('PASS');
+ })
+ .catch(function(e) {
+ port.postMessage('FAIL: unexpected exception: ' + e);
+ }));
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/skip-waiting-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/skip-waiting-worker.js
new file mode 100644
index 0000000..3fc1d1e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/skip-waiting-worker.js
@@ -0,0 +1,21 @@
+importScripts('worker-testharness.js');
+
+promise_test(function() {
+ return skipWaiting()
+ .then(function(result) {
+ assert_equals(result, undefined,
+ 'Promise should be resolved with undefined');
+ })
+ .then(function() {
+ var promises = [];
+ for (var i = 0; i < 8; ++i)
+ promises.push(self.skipWaiting());
+ return Promise.all(promises);
+ })
+ .then(function(results) {
+ results.forEach(function(r) {
+ assert_equals(r, undefined,
+ 'Promises should be resolved with undefined');
+ });
+ });
+ }, 'skipWaiting');
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/square.png b/third_party/web_platform_tests/service-workers/service-worker/resources/square.png
new file mode 100644
index 0000000..01c9666
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/square.png
Binary files differ
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/square.png.sub.headers b/third_party/web_platform_tests/service-workers/service-worker/resources/square.png.sub.headers
new file mode 100644
index 0000000..7341132
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/square.png.sub.headers
@@ -0,0 +1,2 @@
+Content-Type: image/png
+Access-Control-Allow-Origin: *
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/stalling-service-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/stalling-service-worker.js
new file mode 100644
index 0000000..fdf1e6c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/stalling-service-worker.js
@@ -0,0 +1,54 @@
+async function post_message_to_client(role, message, ports) {
+ (await clients.matchAll()).forEach(client => {
+ if (new URL(client.url).searchParams.get('role') === role) {
+ client.postMessage(message, ports);
+ }
+ });
+}
+
+async function post_message_to_child(message, ports) {
+ await post_message_to_client('child', message, ports);
+}
+
+function ping_message(data) {
+ return { type: 'ping', data };
+}
+
+self.onmessage = event => {
+ const message = ping_message(event.data);
+ post_message_to_child(message);
+ post_message_to_parent(message);
+}
+
+async function post_message_to_parent(message, ports) {
+ await post_message_to_client('parent', message, ports);
+}
+
+function fetch_message(key) {
+ return { type: 'fetch', key };
+}
+
+// Send a message to the parent along with a MessagePort to respond
+// with.
+function report_fetch_request(key) {
+ const channel = new MessageChannel();
+ const reply = new Promise(resolve => {
+ channel.port1.onmessage = resolve;
+ }).then(event => event.data);
+ return post_message_to_parent(fetch_message(key), [channel.port2]).then(() => reply);
+}
+
+function respond_with_script(script) {
+ return new Response(new Blob(script, { type: 'text/javascript' }));
+}
+
+// Whenever a controlled document requests a URL with a 'key' search
+// parameter we report the request to the parent frame and wait for
+// a response. The content of the response is then used to respond to
+// the fetch request.
+addEventListener('fetch', event => {
+ let key = new URL(event.request.url).searchParams.get('key');
+ if (key) {
+ event.respondWith(report_fetch_request(key).then(respond_with_script));
+ }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/blank.html b/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/blank.html
new file mode 100644
index 0000000..a3c3a46
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/blank.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Empty doc</title>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/import-scripts-echo.py b/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/import-scripts-echo.py
new file mode 100644
index 0000000..f745d7a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/import-scripts-echo.py
@@ -0,0 +1,6 @@
+def main(req, res):
+ return ([
+ (b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache'),
+ (b'Content-Type', b'application/javascript')],
+ b'echo_output = "%s (subdir/)";\n' % req.GET[b'msg'])
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/simple.txt b/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/simple.txt
new file mode 100644
index 0000000..86bcdd7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/simple.txt
@@ -0,0 +1 @@
+a simple text file (subdir/)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/worker_interception_redirect_webworker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/worker_interception_redirect_webworker.py
new file mode 100644
index 0000000..bb4c874
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/subdir/worker_interception_redirect_webworker.py
@@ -0,0 +1,6 @@
+import os
+import imp
+# Use the file from the parent directory.
+mod = imp.load_source("_parent", os.path.join(os.path.dirname(os.path.dirname(__file__)),
+ os.path.basename(__file__)))
+main = mod.main
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/success.py b/third_party/web_platform_tests/service-workers/service-worker/resources/success.py
new file mode 100644
index 0000000..a026991
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/success.py
@@ -0,0 +1,8 @@
+def main(request, response):
+ headers = []
+
+ if b"ACAOrigin" in request.GET:
+ for item in request.GET[b"ACAOrigin"].split(b","):
+ headers.append((b"Access-Control-Allow-Origin", item))
+
+ return headers, b"{ \"result\": \"success\" }"
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/svg-target-reftest-001-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/svg-target-reftest-001-frame.html
new file mode 100644
index 0000000..59fb524
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/svg-target-reftest-001-frame.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<img src="/images/green.svg">
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/svg-target-reftest-001.html b/third_party/web_platform_tests/service-workers/service-worker/resources/svg-target-reftest-001.html
new file mode 100644
index 0000000..9a93d3b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/svg-target-reftest-001.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Green svg box reference file</title>
+<p>Pass if you see a green box below.</p>
+<iframe src="svg-target-reftest-001-frame.html">
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/svg-target-reftest-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/svg-target-reftest-frame.html
new file mode 100644
index 0000000..d6fc820
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/svg-target-reftest-frame.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<img src="/images/colors.svg#green">
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/test-helpers.sub.js b/third_party/web_platform_tests/service-workers/service-worker/resources/test-helpers.sub.js
new file mode 100644
index 0000000..7430152
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/test-helpers.sub.js
@@ -0,0 +1,300 @@
+// Adapter for testharness.js-style tests with Service Workers
+
+/**
+ * @param options an object that represents RegistrationOptions except for scope.
+ * @param options.type a WorkerType.
+ * @param options.updateViaCache a ServiceWorkerUpdateViaCache.
+ * @see https://w3c.github.io/ServiceWorker/#dictdef-registrationoptions
+ */
+function service_worker_unregister_and_register(test, url, scope, options) {
+ if (!scope || scope.length == 0)
+ return Promise.reject(new Error('tests must define a scope'));
+
+ if (options && options.scope)
+ return Promise.reject(new Error('scope must not be passed in options'));
+
+ options = Object.assign({ scope: scope }, options);
+ return service_worker_unregister(test, scope)
+ .then(function() {
+ return navigator.serviceWorker.register(url, options);
+ })
+ .catch(unreached_rejection(test,
+ 'unregister and register should not fail'));
+}
+
+// This unregisters the registration that precisely matches scope. Use this
+// when unregistering by scope. If no registration is found, it just resolves.
+function service_worker_unregister(test, scope) {
+ var absoluteScope = (new URL(scope, window.location).href);
+ return navigator.serviceWorker.getRegistration(scope)
+ .then(function(registration) {
+ if (registration && registration.scope === absoluteScope)
+ return registration.unregister();
+ })
+ .catch(unreached_rejection(test, 'unregister should not fail'));
+}
+
+function service_worker_unregister_and_done(test, scope) {
+ return service_worker_unregister(test, scope)
+ .then(test.done.bind(test));
+}
+
+function unreached_fulfillment(test, prefix) {
+ return test.step_func(function(result) {
+ var error_prefix = prefix || 'unexpected fulfillment';
+ assert_unreached(error_prefix + ': ' + result);
+ });
+}
+
+// Rejection-specific helper that provides more details
+function unreached_rejection(test, prefix) {
+ return test.step_func(function(error) {
+ var reason = error.message || error.name || error;
+ var error_prefix = prefix || 'unexpected rejection';
+ assert_unreached(error_prefix + ': ' + reason);
+ });
+}
+
+/**
+ * Adds an iframe to the document and returns a promise that resolves to the
+ * iframe when it finishes loading. The caller is responsible for removing the
+ * iframe later if needed.
+ *
+ * @param {string} url
+ * @returns {HTMLIFrameElement}
+ */
+function with_iframe(url) {
+ return new Promise(function(resolve) {
+ var frame = document.createElement('iframe');
+ frame.className = 'test-iframe';
+ frame.src = url;
+ frame.onload = function() { resolve(frame); };
+ document.body.appendChild(frame);
+ });
+}
+
+function normalizeURL(url) {
+ return new URL(url, self.location).toString().replace(/#.*$/, '');
+}
+
+function wait_for_update(test, registration) {
+ if (!registration || registration.unregister == undefined) {
+ return Promise.reject(new Error(
+ 'wait_for_update must be passed a ServiceWorkerRegistration'));
+ }
+
+ return new Promise(test.step_func(function(resolve) {
+ var handler = test.step_func(function() {
+ registration.removeEventListener('updatefound', handler);
+ resolve(registration.installing);
+ });
+ registration.addEventListener('updatefound', handler);
+ }));
+}
+
+// Return true if |state_a| is more advanced than |state_b|.
+function is_state_advanced(state_a, state_b) {
+ if (state_b === 'installing') {
+ switch (state_a) {
+ case 'installed':
+ case 'activating':
+ case 'activated':
+ case 'redundant':
+ return true;
+ }
+ }
+
+ if (state_b === 'installed') {
+ switch (state_a) {
+ case 'activating':
+ case 'activated':
+ case 'redundant':
+ return true;
+ }
+ }
+
+ if (state_b === 'activating') {
+ switch (state_a) {
+ case 'activated':
+ case 'redundant':
+ return true;
+ }
+ }
+
+ if (state_b === 'activated') {
+ switch (state_a) {
+ case 'redundant':
+ return true;
+ }
+ }
+ return false;
+}
+
+function wait_for_state(test, worker, state) {
+ if (!worker || worker.state == undefined) {
+ return Promise.reject(new Error(
+ 'wait_for_state needs a ServiceWorker object to be passed.'));
+ }
+ if (worker.state === state)
+ return Promise.resolve(state);
+
+ if (is_state_advanced(worker.state, state)) {
+ return Promise.reject(new Error(
+ `Waiting for ${state} but the worker is already ${worker.state}.`));
+ }
+ return new Promise(test.step_func(function(resolve, reject) {
+ worker.addEventListener('statechange', test.step_func(function() {
+ if (worker.state === state)
+ resolve(state);
+
+ if (is_state_advanced(worker.state, state)) {
+ reject(new Error(
+ `The state of the worker becomes ${worker.state} while waiting` +
+ `for ${state}.`));
+ }
+ }));
+ }));
+}
+
+// Declare a test that runs entirely in the ServiceWorkerGlobalScope. The |url|
+// is the service worker script URL. This function:
+// - Instantiates a new test with the description specified in |description|.
+// The test will succeed if the specified service worker can be successfully
+// registered and installed.
+// - Creates a new ServiceWorker registration with a scope unique to the current
+// document URL. Note that this doesn't allow more than one
+// service_worker_test() to be run from the same document.
+// - Waits for the new worker to begin installing.
+// - Imports tests results from tests running inside the ServiceWorker.
+function service_worker_test(url, description) {
+ // If the document URL is https://example.com/document and the script URL is
+ // https://example.com/script/worker.js, then the scope would be
+ // https://example.com/script/scope/document.
+ var scope = new URL('scope' + window.location.pathname,
+ new URL(url, window.location)).toString();
+ promise_test(function(test) {
+ return service_worker_unregister_and_register(test, url, scope)
+ .then(function(registration) {
+ add_completion_callback(function() {
+ registration.unregister();
+ });
+ return wait_for_update(test, registration)
+ .then(function(worker) {
+ return fetch_tests_from_worker(worker);
+ });
+ });
+ }, description);
+}
+
+function base_path() {
+ return location.pathname.replace(/\/[^\/]*$/, '/');
+}
+
+function test_login(test, origin, username, password, cookie) {
+ return new Promise(function(resolve, reject) {
+ with_iframe(
+ origin + base_path() +
+ 'resources/fetch-access-control-login.html')
+ .then(test.step_func(function(frame) {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = test.step_func(function() {
+ frame.remove();
+ resolve();
+ });
+ frame.contentWindow.postMessage(
+ {username: username, password: password, cookie: cookie},
+ origin, [channel.port2]);
+ }));
+ });
+}
+
+function test_websocket(test, frame, url) {
+ return new Promise(function(resolve, reject) {
+ var ws = new frame.contentWindow.WebSocket(url, ['echo', 'chat']);
+ var openCalled = false;
+ ws.addEventListener('open', test.step_func(function(e) {
+ assert_equals(ws.readyState, 1, "The WebSocket should be open");
+ openCalled = true;
+ ws.close();
+ }), true);
+
+ ws.addEventListener('close', test.step_func(function(e) {
+ assert_true(openCalled, "The WebSocket should be closed after being opened");
+ resolve();
+ }), true);
+
+ ws.addEventListener('error', reject);
+ });
+}
+
+function login_https(test) {
+ var host_info = get_host_info();
+ return test_login(test, host_info.HTTPS_REMOTE_ORIGIN,
+ 'username1s', 'password1s', 'cookie1')
+ .then(function() {
+ return test_login(test, host_info.HTTPS_ORIGIN,
+ 'username2s', 'password2s', 'cookie2');
+ });
+}
+
+function websocket(test, frame) {
+ return test_websocket(test, frame, get_websocket_url());
+}
+
+function get_websocket_url() {
+ return 'wss://{{host}}:{{ports[wss][0]}}/echo';
+}
+
+// The navigator.serviceWorker.register() method guarantees that the newly
+// installing worker is available as registration.installing when its promise
+// resolves. However some tests test installation using a <link> element where
+// it is possible for the installing worker to have already become the waiting
+// or active worker. So this method is used to get the newest worker when these
+// tests need access to the ServiceWorker itself.
+function get_newest_worker(registration) {
+ if (registration.installing)
+ return registration.installing;
+ if (registration.waiting)
+ return registration.waiting;
+ if (registration.active)
+ return registration.active;
+}
+
+function register_using_link(script, options) {
+ var scope = options.scope;
+ var link = document.createElement('link');
+ link.setAttribute('rel', 'serviceworker');
+ link.setAttribute('href', script);
+ link.setAttribute('scope', scope);
+ document.getElementsByTagName('head')[0].appendChild(link);
+ return new Promise(function(resolve, reject) {
+ link.onload = resolve;
+ link.onerror = reject;
+ })
+ .then(() => navigator.serviceWorker.getRegistration(scope));
+}
+
+function with_sandboxed_iframe(url, sandbox) {
+ return new Promise(function(resolve) {
+ var frame = document.createElement('iframe');
+ frame.sandbox = sandbox;
+ frame.src = url;
+ frame.onload = function() { resolve(frame); };
+ document.body.appendChild(frame);
+ });
+}
+
+// Registers, waits for activation, then unregisters on a sample scope.
+//
+// This can be used to wait for a period of time needed to register,
+// activate, and then unregister a service worker. When checking that
+// certain behavior does *NOT* happen, this is preferable to using an
+// arbitrary delay.
+async function wait_for_activation_on_sample_scope(t, window_or_workerglobalscope) {
+ const script = '/service-workers/service-worker/resources/empty-worker.js';
+ const scope = 'resources/there/is/no/there/there?' + Date.now();
+ let registration = await window_or_workerglobalscope.navigator.serviceWorker.register(script, { scope });
+ await wait_for_state(t, registration.installing, 'activated');
+ await registration.unregister();
+}
+
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-headers-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-headers-worker.js
new file mode 100644
index 0000000..566e2e9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-headers-worker.js
@@ -0,0 +1,10 @@
+// Add a unique UUID per request to induce service worker script update.
+// Time stamp: %UUID%
+
+// The server injects the request headers here as a JSON string.
+const headersAsJson = `%HEADERS%`;
+const headers = JSON.parse(headersAsJson);
+
+self.addEventListener('message', async (e) => {
+ e.source.postMessage(headers);
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-headers-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-headers-worker.py
new file mode 100644
index 0000000..78a9335
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-headers-worker.py
@@ -0,0 +1,21 @@
+import json
+import os
+import uuid
+import sys
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ path = os.path.join(os.path.dirname(isomorphic_decode(__file__)),
+ u"test-request-headers-worker.js")
+ body = open(path, u"rb").read()
+
+ data = {isomorphic_decode(key):isomorphic_decode(request.headers[key]) for key, value in request.headers.items()}
+ body = body.replace(b"%HEADERS%", json.dumps(data).encode("utf-8"))
+ body = body.replace(b"%UUID%", str(uuid.uuid4()).encode("utf-8"))
+
+ headers = []
+ headers.append((b"ETag", b"etag"))
+ headers.append((b"Content-Type", b'text/javascript'))
+
+ return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-mode-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-mode-worker.js
new file mode 100644
index 0000000..566e2e9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-mode-worker.js
@@ -0,0 +1,10 @@
+// Add a unique UUID per request to induce service worker script update.
+// Time stamp: %UUID%
+
+// The server injects the request headers here as a JSON string.
+const headersAsJson = `%HEADERS%`;
+const headers = JSON.parse(headersAsJson);
+
+self.addEventListener('message', async (e) => {
+ e.source.postMessage(headers);
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-mode-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-mode-worker.py
new file mode 100644
index 0000000..8449841
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/test-request-mode-worker.py
@@ -0,0 +1,22 @@
+import json
+import os
+import uuid
+import sys
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ path = os.path.join(os.path.dirname(isomorphic_decode(__file__)),
+ u"test-request-mode-worker.js")
+ body = open(path, u"rb").read()
+
+ data = {isomorphic_decode(key):isomorphic_decode(request.headers[key]) for key, value in request.headers.items()}
+
+ body = body.replace(b"%HEADERS%", json.dumps(data).encode("utf-8"))
+ body = body.replace(b"%UUID%", str(uuid.uuid4()).encode("utf-8"))
+
+ headers = []
+ headers.append((b"ETag", b"etag"))
+ headers.append((b"Content-Type", b'text/javascript'))
+
+ return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/testharness-helpers.js b/third_party/web_platform_tests/service-workers/service-worker/resources/testharness-helpers.js
new file mode 100644
index 0000000..b1a5b96
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/testharness-helpers.js
@@ -0,0 +1,136 @@
+/*
+ * testharness-helpers contains various useful extensions to testharness.js to
+ * allow them to be used across multiple tests before they have been
+ * upstreamed. This file is intended to be usable from both document and worker
+ * environments, so code should for example not rely on the DOM.
+ */
+
+// Asserts that two objects |actual| and |expected| are weakly equal under the
+// following definition:
+//
+// |a| and |b| are weakly equal if any of the following are true:
+// 1. If |a| is not an 'object', and |a| === |b|.
+// 2. If |a| is an 'object', and all of the following are true:
+// 2.1 |a.p| is weakly equal to |b.p| for all own properties |p| of |a|.
+// 2.2 Every own property of |b| is an own property of |a|.
+//
+// This is a replacement for the the version of assert_object_equals() in
+// testharness.js. The latter doesn't handle own properties correctly. I.e. if
+// |a.p| is not an own property, it still requires that |b.p| be an own
+// property.
+//
+// Note that |actual| must not contain cyclic references.
+self.assert_object_equals = function(actual, expected, description) {
+ var object_stack = [];
+
+ function _is_equal(actual, expected, prefix) {
+ if (typeof actual !== 'object') {
+ assert_equals(actual, expected, prefix);
+ return;
+ }
+ assert_equals(typeof expected, 'object', prefix);
+ assert_equals(object_stack.indexOf(actual), -1,
+ prefix + ' must not contain cyclic references.');
+
+ object_stack.push(actual);
+
+ Object.getOwnPropertyNames(expected).forEach(function(property) {
+ assert_own_property(actual, property, prefix);
+ _is_equal(actual[property], expected[property],
+ prefix + '.' + property);
+ });
+ Object.getOwnPropertyNames(actual).forEach(function(property) {
+ assert_own_property(expected, property, prefix);
+ });
+
+ object_stack.pop();
+ }
+
+ function _brand(object) {
+ return Object.prototype.toString.call(object).match(/^\[object (.*)\]$/)[1];
+ }
+
+ _is_equal(actual, expected,
+ (description ? description + ': ' : '') + _brand(expected));
+};
+
+// Equivalent to assert_in_array, but uses a weaker equivalence relation
+// (assert_object_equals) than '==='.
+function assert_object_in_array(actual, expected_array, description) {
+ assert_true(expected_array.some(function(element) {
+ try {
+ assert_object_equals(actual, element);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }), description);
+}
+
+// Assert that the two arrays |actual| and |expected| contain the same set of
+// elements as determined by assert_object_equals. The order is not significant.
+//
+// |expected| is assumed to not contain any duplicates as determined by
+// assert_object_equals().
+function assert_array_equivalent(actual, expected, description) {
+ assert_true(Array.isArray(actual), description);
+ assert_equals(actual.length, expected.length, description);
+ expected.forEach(function(expected_element) {
+ // assert_in_array treats the first argument as being 'actual', and the
+ // second as being 'expected array'. We are switching them around because
+ // we want to be resilient against the |actual| array containing
+ // duplicates.
+ assert_object_in_array(expected_element, actual, description);
+ });
+}
+
+// Asserts that two arrays |actual| and |expected| contain the same set of
+// elements as determined by assert_object_equals(). The corresponding elements
+// must occupy corresponding indices in their respective arrays.
+function assert_array_objects_equals(actual, expected, description) {
+ assert_true(Array.isArray(actual), description);
+ assert_equals(actual.length, expected.length, description);
+ actual.forEach(function(value, index) {
+ assert_object_equals(value, expected[index],
+ description + ' : object[' + index + ']');
+ });
+}
+
+// Asserts that |object| that is an instance of some interface has the attribute
+// |attribute_name| following the conditions specified by WebIDL, but it's
+// acceptable that the attribute |attribute_name| is an own property of the
+// object because we're in the middle of moving the attribute to a prototype
+// chain. Once we complete the transition to prototype chains,
+// assert_will_be_idl_attribute must be replaced with assert_idl_attribute
+// defined in testharness.js.
+//
+// FIXME: Remove assert_will_be_idl_attribute once we complete the transition
+// of moving the DOM attributes to prototype chains. (http://crbug.com/43394)
+function assert_will_be_idl_attribute(object, attribute_name, description) {
+ assert_equals(typeof object, "object", description);
+
+ assert_true("hasOwnProperty" in object, description);
+
+ // Do not test if |attribute_name| is not an own property because
+ // |attribute_name| is in the middle of the transition to a prototype
+ // chain. (http://crbug.com/43394)
+
+ assert_true(attribute_name in object, description);
+}
+
+// Stringifies a DOM object. This function stringifies not only own properties
+// but also DOM attributes which are on a prototype chain. Note that
+// JSON.stringify only stringifies own properties.
+function stringifyDOMObject(object)
+{
+ function deepCopy(src) {
+ if (typeof src != "object")
+ return src;
+ var dst = Array.isArray(src) ? [] : {};
+ for (var property in src) {
+ dst[property] = deepCopy(src[property]);
+ }
+ return dst;
+ }
+ return JSON.stringify(deepCopy(object));
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/trickle.py b/third_party/web_platform_tests/service-workers/service-worker/resources/trickle.py
new file mode 100644
index 0000000..6423f7f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/trickle.py
@@ -0,0 +1,14 @@
+import time
+
+def main(request, response):
+ delay = float(request.GET.first(b"ms", 500)) / 1E3
+ count = int(request.GET.first(b"count", 50))
+ # Read request body
+ request.body
+ time.sleep(delay)
+ response.headers.set(b"Content-type", b"text/plain")
+ response.write_status_headers()
+ time.sleep(delay)
+ for i in range(count):
+ response.writer.write_content(b"TEST_TRICKLE\n")
+ time.sleep(delay)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/type-check-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/type-check-worker.js
new file mode 100644
index 0000000..1779e23
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/type-check-worker.js
@@ -0,0 +1,10 @@
+let type = '';
+try {
+ importScripts('empty.js');
+ type = 'classic';
+} catch (e) {
+ type = 'module';
+}
+onmessage = e => {
+ e.source.postMessage(type);
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/unregister-controller-page.html b/third_party/web_platform_tests/service-workers/service-worker/resources/unregister-controller-page.html
new file mode 100644
index 0000000..18a95ee
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/unregister-controller-page.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<script>
+function fetch_url(url) {
+ return new Promise(function(resolve, reject) {
+ var request = new XMLHttpRequest();
+ request.addEventListener('load', function(event) {
+ if (request.status == 200)
+ resolve(request.response);
+ else
+ reject(Error(request.statusText));
+ });
+ request.open('GET', url);
+ request.send();
+ });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/unregister-immediately-helpers.js b/third_party/web_platform_tests/service-workers/service-worker/resources/unregister-immediately-helpers.js
new file mode 100644
index 0000000..91a30de
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/unregister-immediately-helpers.js
@@ -0,0 +1,19 @@
+'use strict';
+
+// Returns a promise for a network response that contains the Clear-Site-Data:
+// "storage" header.
+function clear_site_data() {
+ return fetch('resources/blank.html?pipe=header(Clear-Site-Data,"storage")');
+}
+
+async function assert_no_registrations_exist() {
+ const registrations = await navigator.serviceWorker.getRegistrations();
+ assert_equals(registrations.length, 0);
+}
+
+async function add_controlled_iframe(test, url) {
+ const frame = await with_iframe(url);
+ test.add_cleanup(() => { frame.remove(); });
+ assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller, null);
+ return frame;
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/unregister-rewrite-worker.html b/third_party/web_platform_tests/service-workers/service-worker/resources/unregister-rewrite-worker.html
new file mode 100644
index 0000000..f5d0367
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/unregister-rewrite-worker.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<script>
+async function onLoad() {
+ const params = new URLSearchParams(self.location.search);
+ const scope = self.origin + params.get('scopepath');
+ const reg = await navigator.serviceWorker.getRegistration(scope);
+ if (reg) {
+ await reg.unregister();
+ }
+ if (window.opener) {
+ window.opener.postMessage({ type: 'SW-UNREGISTERED' }, '*');
+ } else {
+ window.top.postMessage({ type: 'SW-UNREGISTERED' }, '*');
+ }
+}
+self.addEventListener('load', onLoad);
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-claim-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-claim-worker.py
new file mode 100644
index 0000000..64914a9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-claim-worker.py
@@ -0,0 +1,24 @@
+import time
+
+script = u'''
+// Time stamp: %s
+// (This ensures the source text is *not* a byte-for-byte match with any
+// previously-fetched version of this script.)
+
+// This no-op fetch handler is necessary to bypass explicitly the no fetch
+// handler optimization by which this service worker script can be skipped.
+addEventListener('fetch', event => {
+ return;
+ });
+
+addEventListener('install', event => {
+ event.waitUntil(self.skipWaiting());
+ });
+
+addEventListener('activate', event => {
+ event.waitUntil(self.clients.claim());
+ });'''
+
+
+def main(request, response):
+ return [(b'Content-Type', b'application/javascript')], script % time.time()
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-during-installation-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/update-during-installation-worker.js
new file mode 100644
index 0000000..f1997bd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-during-installation-worker.js
@@ -0,0 +1,61 @@
+'use strict';
+
+const installEventFired = new Promise(resolve => {
+ self.fireInstallEvent = resolve;
+});
+
+const installFinished = new Promise(resolve => {
+ self.finishInstall = resolve;
+});
+
+addEventListener('install', event => {
+ fireInstallEvent();
+ event.waitUntil(installFinished);
+});
+
+addEventListener('message', event => {
+ let resolveWaitUntil;
+ event.waitUntil(new Promise(resolve => { resolveWaitUntil = resolve; }));
+
+ // Use a dedicated MessageChannel for every request so senders can wait for
+ // individual requests to finish, and concurrent requests (to different
+ // workers) don't cause race conditions.
+ const port = event.data;
+ port.onmessage = (event) => {
+ switch (event.data) {
+ case 'awaitInstallEvent':
+ installEventFired.then(() => {
+ port.postMessage('installEventFired');
+ }).finally(resolveWaitUntil);
+ break;
+
+ case 'finishInstall':
+ installFinished.then(() => {
+ port.postMessage('installFinished');
+ }).finally(resolveWaitUntil);
+ finishInstall();
+ break;
+
+ case 'callUpdate': {
+ const channel = new MessageChannel();
+ registration.update().then(() => {
+ channel.port2.postMessage({
+ success: true,
+ });
+ }).catch((exception) => {
+ channel.port2.postMessage({
+ success: false,
+ exception: exception.name,
+ });
+ }).finally(resolveWaitUntil);
+ port.postMessage(channel.port1, [channel.port1]);
+ break;
+ }
+
+ default:
+ port.postMessage('Unexpected command ' + event.data);
+ resolveWaitUntil();
+ break;
+ }
+ };
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-during-installation-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-during-installation-worker.py
new file mode 100644
index 0000000..3e15926
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-during-installation-worker.py
@@ -0,0 +1,11 @@
+import random
+
+def main(request, response):
+ headers = [(b'Content-Type', b'application/javascript'),
+ (b'Cache-Control', b'max-age=0')]
+ # Plug in random.random() to the worker so update() finds a new worker every time.
+ body = u'''
+// %s
+importScripts('update-during-installation-worker.js');
+ '''.strip() % (random.random())
+ return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-fetch-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-fetch-worker.py
new file mode 100644
index 0000000..02cbb42
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-fetch-worker.py
@@ -0,0 +1,18 @@
+import random
+import time
+
+def main(request, response):
+ # no-cache itself to ensure the user agent finds a new version for each update.
+ headers = [(b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache')]
+
+ content_type = b''
+ extra_body = u''
+
+ content_type = b'application/javascript'
+ headers.append((b'Content-Type', content_type))
+
+ extra_body = u"self.onfetch = (event) => { event.respondWith(fetch(event.request)); };"
+
+ # Return a different script for each access.
+ return headers, u'/* %s %s */ %s' % (time.time(), random.random(), extra_body)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-max-aged-worker-imported-script.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-max-aged-worker-imported-script.py
new file mode 100644
index 0000000..7cc5a65
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-max-aged-worker-imported-script.py
@@ -0,0 +1,14 @@
+import time
+
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ headers = [(b'Content-Type', b'application/javascript'),
+ (b'Cache-Control', b'max-age=86400'),
+ (b'Last-Modified', isomorphic_encode(time.strftime(u"%a, %d %b %Y %H:%M:%S GMT", time.gmtime())))]
+
+ body = u'''
+ const importTime = {time:8f};
+ '''.format(time=time.time())
+
+ return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-max-aged-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-max-aged-worker.py
new file mode 100644
index 0000000..4f87906
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-max-aged-worker.py
@@ -0,0 +1,30 @@
+import time
+import json
+
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+def main(request, response):
+ headers = [(b'Content-Type', b'application/javascript'),
+ (b'Cache-Control', b'max-age=86400'),
+ (b'Last-Modified', isomorphic_encode(time.strftime(u"%a, %d %b %Y %H:%M:%S GMT", time.gmtime())))]
+
+ test = request.GET[b'test']
+
+ body = u'''
+ const mainTime = {time:8f};
+ const testName = {test};
+ importScripts('update-max-aged-worker-imported-script.py');
+
+ addEventListener('message', event => {{
+ event.source.postMessage({{
+ mainTime,
+ importTime,
+ test: {test}
+ }});
+ }});
+ '''.format(
+ time=time.time(),
+ test=json.dumps(isomorphic_decode(test))
+ )
+
+ return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-missing-import-scripts-imported-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-missing-import-scripts-imported-worker.py
new file mode 100644
index 0000000..1547cb5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-missing-import-scripts-imported-worker.py
@@ -0,0 +1,9 @@
+def main(request, response):
+ key = request.GET[b'key']
+ already_requested = request.server.stash.take(key)
+
+ if already_requested is None:
+ request.server.stash.put(key, True)
+ return [(b'Content-Type', b'application/javascript')], b'// initial script'
+
+ response.status = (404, b'Not found: should not have been able to import this script twice!')
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-missing-import-scripts-main-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-missing-import-scripts-main-worker.py
new file mode 100644
index 0000000..1c447e1
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-missing-import-scripts-main-worker.py
@@ -0,0 +1,15 @@
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ key = request.GET[b'key']
+ already_requested = request.server.stash.take(key)
+
+ header = [(b'Content-Type', b'application/javascript')]
+ initial_script = u'importScripts("./update-missing-import-scripts-imported-worker.py?key={0}")'.format(isomorphic_decode(key))
+ updated_script = u'// removed importScripts()'
+
+ if already_requested is None:
+ request.server.stash.put(key, True)
+ return header, initial_script
+
+ return header, updated_script
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-nocookie-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-nocookie-worker.py
new file mode 100644
index 0000000..34eff02
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-nocookie-worker.py
@@ -0,0 +1,14 @@
+import random
+import time
+
+def main(request, response):
+ # no-cache itself to ensure the user agent finds a new version for each update.
+ headers = [(b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache')]
+
+ # Set a normal mimetype.
+ content_type = b'application/javascript'
+
+ headers.append((b'Content-Type', content_type))
+ # Return a different script for each access.
+ return headers, u'// %s %s' % (time.time(), random.random())
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-recovery-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-recovery-worker.py
new file mode 100644
index 0000000..9ac7ce7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-recovery-worker.py
@@ -0,0 +1,25 @@
+def main(request, response):
+ # Set mode to 'init' for initial fetch.
+ mode = b'init'
+ if b'update-recovery-mode' in request.cookies:
+ mode = request.cookies[b'update-recovery-mode'].value
+
+ # no-cache itself to ensure the user agent finds a new version for each update.
+ headers = [(b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache')]
+
+ extra_body = b''
+
+ if mode == b'init':
+ # Install a bad service worker that will break the controlled
+ # document navigation.
+ response.set_cookie(b'update-recovery-mode', b'bad')
+ extra_body = b"addEventListener('fetch', function(e) { e.respondWith(Promise.reject()); });"
+ elif mode == b'bad':
+ # When the update tries to pull the script again, update to
+ # a worker service worker that does not break document
+ # navigation. Serve the same script from then on.
+ response.delete_cookie(b'update-recovery-mode')
+
+ headers.append((b'Content-Type', b'application/javascript'))
+ return headers, b'%s' % (extra_body)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-registration-with-type.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-registration-with-type.py
new file mode 100644
index 0000000..3cabc0f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-registration-with-type.py
@@ -0,0 +1,33 @@
+def classic_script():
+ return b"""
+ importScripts('./imported-classic-script.js');
+ self.onmessage = e => {
+ e.source.postMessage(imported);
+ };
+ """
+
+def module_script():
+ return b"""
+ import * as module from './imported-module-script.js';
+ self.onmessage = e => {
+ e.source.postMessage(module.imported);
+ };
+ """
+
+# Returns the classic script for a first request and
+# returns the module script for second and subsequent requests.
+def main(request, response):
+ headers = [(b'Content-Type', b'application/javascript'),
+ (b'Pragma', b'no-store'),
+ (b'Cache-Control', b'no-store')]
+
+ classic_first = request.GET[b'classic_first']
+ key = request.GET[b'key']
+ requested_once = request.server.stash.take(key)
+ if requested_once is None:
+ request.server.stash.put(key, True)
+ body = classic_script() if classic_first == b'1' else module_script()
+ else:
+ body = module_script() if classic_first == b'1' else classic_script()
+
+ return 200, headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-smaller-body-after-update-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/update-smaller-body-after-update-worker.js
new file mode 100644
index 0000000..d43f6b2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-smaller-body-after-update-worker.js
@@ -0,0 +1 @@
+// Hello world!
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-smaller-body-before-update-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/update-smaller-body-before-update-worker.js
new file mode 100644
index 0000000..30c8783
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-smaller-body-before-update-worker.js
@@ -0,0 +1,2 @@
+// Hello world!
+// **with extra body**
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-worker-from-file.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-worker-from-file.py
new file mode 100644
index 0000000..ac0850f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-worker-from-file.py
@@ -0,0 +1,33 @@
+import os
+
+from wptserve.utils import isomorphic_encode
+
+def serve_js_from_file(request, response, filename):
+ body = b''
+ path = os.path.join(os.path.dirname(isomorphic_encode(__file__)), filename)
+ with open(path, 'rb') as f:
+ body = f.read()
+ return (
+ [
+ (b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache'),
+ (b'Content-Type', b'application/javascript')
+ ], body)
+
+def main(request, response):
+ key = request.GET[b"Key"]
+
+ visited_count = request.server.stash.take(key)
+ if visited_count is None:
+ visited_count = 0
+
+ # Keep how many times the test requested this resource.
+ visited_count += 1
+ request.server.stash.put(key, visited_count)
+
+ # Serve a file based on how many times it's requested.
+ if visited_count == 1:
+ return serve_js_from_file(request, response, request.GET[b"First"])
+ if visited_count == 2:
+ return serve_js_from_file(request, response, request.GET[b"Second"])
+ raise u"Unknown state"
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update-worker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update-worker.py
new file mode 100644
index 0000000..5638a88
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update-worker.py
@@ -0,0 +1,62 @@
+from urllib.parse import unquote
+
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+def redirect_response(request, response, visited_count):
+ # |visited_count| is used as a unique id to differentiate responses
+ # every time.
+ location = b'empty.js'
+ if b'Redirect' in request.GET:
+ location = isomorphic_encode(unquote(isomorphic_decode(request.GET[b'Redirect'])))
+ return (301,
+ [
+ (b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache'),
+ (b'Content-Type', b'application/javascript'),
+ (b'Location', location),
+ ],
+ u'/* %s */' % str(visited_count))
+
+def not_found_response():
+ return 404, [(b'Content-Type', b'text/plain')], u"Page not found"
+
+def ok_response(request, response, visited_count,
+ extra_body=u'', mime_type=b'application/javascript'):
+ # |visited_count| is used as a unique id to differentiate responses
+ # every time.
+ return (
+ [
+ (b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache'),
+ (b'Content-Type', mime_type)
+ ],
+ u'/* %s */ %s' % (str(visited_count), extra_body))
+
+def main(request, response):
+ key = request.GET[b"Key"]
+ mode = request.GET[b"Mode"]
+
+ visited_count = request.server.stash.take(key)
+ if visited_count is None:
+ visited_count = 0
+
+ # Keep how many times the test requested this resource.
+ visited_count += 1
+ request.server.stash.put(key, visited_count)
+
+ # Return a response based on |mode| only when it's the second time (== update).
+ if visited_count == 2:
+ if mode == b'normal':
+ return ok_response(request, response, visited_count)
+ if mode == b'bad_mime_type':
+ return ok_response(request, response, visited_count, mime_type=b'text/html')
+ if mode == b'not_found':
+ return not_found_response()
+ if mode == b'redirect':
+ return redirect_response(request, response, visited_count)
+ if mode == b'syntax_error':
+ return ok_response(request, response, visited_count, extra_body=u'badsyntax(isbad;')
+ if mode == b'throw_install':
+ return ok_response(request, response, visited_count, extra_body=u"addEventListener('install', function(e) { throw new Error('boom'); });")
+
+ return ok_response(request, response, visited_count)
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update/update-after-oneday.https.html b/third_party/web_platform_tests/service-workers/service-worker/resources/update/update-after-oneday.https.html
new file mode 100644
index 0000000..9d4c982
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update/update-after-oneday.https.html
@@ -0,0 +1,8 @@
+<body>
+<script>
+function load_image(url) {
+ var img = document.createElement('img');
+ img.src = url;
+}
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/update_shell.py b/third_party/web_platform_tests/service-workers/service-worker/resources/update_shell.py
new file mode 100644
index 0000000..2070509
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/update_shell.py
@@ -0,0 +1,32 @@
+# This serves a different response to each request, to test service worker
+# updates. If |filename| is provided, it writes that file into the body.
+#
+# Usage:
+# navigator.serviceWorker.register('update_shell.py?filename=worker.js')
+#
+# This registers worker.js as a service worker, and every update check
+# will return a new response.
+import os
+import random
+import time
+
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ # Set no-cache to ensure the user agent finds a new version for each update.
+ headers = [(b'Cache-Control', b'no-cache, must-revalidate'),
+ (b'Pragma', b'no-cache'),
+ (b'Content-Type', b'application/javascript')]
+
+ # Return a different script for each access.
+ timestamp = u'// %s %s' % (time.time(), random.random())
+ body = isomorphic_encode(timestamp) + b'\n'
+
+ # Inject the file into the response.
+ if b'filename' in request.GET:
+ path = os.path.join(os.path.dirname(isomorphic_encode(__file__)),
+ request.GET[b'filename'])
+ with open(path, 'rb') as f:
+ body += f.read()
+
+ return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/vtt-frame.html b/third_party/web_platform_tests/service-workers/service-worker/resources/vtt-frame.html
new file mode 100644
index 0000000..c3ac803
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/vtt-frame.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Page Title</title>
+<video>
+ <track>
+</video>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/wait-forever-in-install-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/wait-forever-in-install-worker.js
new file mode 100644
index 0000000..af85a73
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/wait-forever-in-install-worker.js
@@ -0,0 +1,12 @@
+var waitUntilResolve;
+self.addEventListener('install', function(event) {
+ event.waitUntil(new Promise(function(resolve) {
+ waitUntilResolve = resolve;
+ }));
+ });
+
+self.addEventListener('message', function(event) {
+ if (event.data === 'STOP_WAITING') {
+ waitUntilResolve();
+ }
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/websocket-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/websocket-worker.js
new file mode 100644
index 0000000..bb2dc81
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/websocket-worker.js
@@ -0,0 +1,35 @@
+let port;
+let received = false;
+
+function reportFailure(details) {
+ port.postMessage('FAIL: ' + details);
+}
+
+onmessage = event => {
+ port = event.source;
+
+ const ws = new WebSocket('wss://{{host}}:{{ports[wss][0]}}/echo');
+ ws.onopen = () => {
+ ws.send('Hello');
+ };
+ ws.onmessage = msg => {
+ if (msg.data !== 'Hello') {
+ reportFailure('Unexpected reply: ' + msg.data);
+ return;
+ }
+
+ received = true;
+ ws.close();
+ };
+ ws.onclose = (event) => {
+ if (!received) {
+ reportFailure('Closed before receiving reply: ' + event.code);
+ return;
+ }
+
+ port.postMessage('PASS');
+ };
+ ws.onerror = () => {
+ reportFailure('Got an error event');
+ };
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/websocket.js b/third_party/web_platform_tests/service-workers/service-worker/resources/websocket.js
new file mode 100644
index 0000000..fc6abd2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/websocket.js
@@ -0,0 +1,7 @@
+self.urls = [];
+self.addEventListener('fetch', function(event) {
+ self.urls.push(event.request.url);
+ });
+self.addEventListener('message', function(event) {
+ event.data.port.postMessage({urls: self.urls});
+ });
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/window-opener.html b/third_party/web_platform_tests/service-workers/service-worker/resources/window-opener.html
new file mode 100644
index 0000000..32d0744
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/window-opener.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="referrer" content="origin">
+<script>
+function onLoad() {
+ self.onmessage = evt => {
+ if (self.opener)
+ self.opener.postMessage(evt.data, '*');
+ else
+ self.top.postMessage(evt.data, '*');
+ }
+ const params = new URLSearchParams(self.location.search);
+ const w = window.open(params.get('target'));
+ self.addEventListener('unload', evt => w.close());
+}
+self.addEventListener('load', onLoad);
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/windowclient-navigate-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/windowclient-navigate-worker.js
new file mode 100644
index 0000000..383f666
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/windowclient-navigate-worker.js
@@ -0,0 +1,75 @@
+importScripts('/resources/testharness.js');
+
+function matchQuery(queryString) {
+ return self.location.search.substr(1) === queryString;
+}
+
+async function navigateTest(t, e) {
+ const port = e.data.port;
+ const url = e.data.url;
+ const expected = e.data.expected;
+
+ let p = clients.matchAll({ includeUncontrolled : true })
+ .then(function(clients) {
+ for (const client of clients) {
+ if (client.url === e.data.clientUrl) {
+ assert_equals(client.frameType, e.data.frameType);
+ return client.navigate(url);
+ }
+ }
+ throw 'Could not locate window client.';
+ }).then(function(newClient) {
+ // If we didn't reject, we better get resolved with the right thing.
+ if (newClient === null) {
+ assert_equals(newClient, expected);
+ } else {
+ assert_equals(newClient.url, expected);
+ }
+ });
+
+ if (typeof self[expected] === "function") {
+ // It's a JS error type name. We are expecting our promise to be rejected
+ // with that error.
+ p = promise_rejects_js(t, self[expected], p);
+ }
+
+ // Let our caller know we are done.
+ return p.finally(() => port.postMessage(null));
+}
+
+function getTestClient() {
+ return clients.matchAll({ includeUncontrolled: true })
+ .then(function(clients) {
+ for (const client of clients) {
+ if (client.url.includes('windowclient-navigate.https.html')) {
+ return client;
+ }
+ }
+
+ throw new Error('Service worker was unable to locate test client.');
+ });
+}
+
+function waitForMessage(client) {
+ const channel = new MessageChannel();
+ client.postMessage({ port: channel.port2 }, [channel.port2]);
+
+ return new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ });
+}
+
+// The worker must remain in the "installing" state for the duration of some
+// sub-tests. In order to achieve this coordination without relying on global
+// state, the worker must create a message channel with the client from within
+// the "install" event handler.
+if (matchQuery('installing')) {
+ self.addEventListener('install', function(e) {
+ e.waitUntil(getTestClient().then(waitForMessage));
+ });
+}
+
+self.addEventListener('message', function(e) {
+ e.waitUntil(promise_test(t => navigateTest(t, e),
+ e.data.description + " worker side"));
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/worker-client-id-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-client-id-worker.js
new file mode 100644
index 0000000..f592629
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-client-id-worker.js
@@ -0,0 +1,25 @@
+addEventListener('fetch', evt => {
+ if (evt.request.url.includes('worker-echo-client-id.js')) {
+ evt.respondWith(new Response(
+ 'fetch("fetch-echo-client-id").then(r => r.text()).then(t => self.postMessage(t));',
+ { headers: { 'Content-Type': 'application/javascript' }}));
+ return;
+ }
+
+ if (evt.request.url.includes('fetch-echo-client-id')) {
+ evt.respondWith(new Response(evt.clientId));
+ return;
+ }
+
+ if (evt.request.url.includes('frame.html')) {
+ evt.respondWith(new Response(''));
+ return;
+ }
+});
+
+addEventListener('message', evt => {
+ if (evt.data === 'echo-client-id') {
+ evt.ports[0].postMessage(evt.source.id);
+ return;
+ }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/worker-fetching-cross-origin.js b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-fetching-cross-origin.js
new file mode 100644
index 0000000..a81bb3d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-fetching-cross-origin.js
@@ -0,0 +1,12 @@
+importScripts('/common/get-host-info.sub.js');
+importScripts('test-helpers.sub.js');
+
+self.addEventListener('fetch', event => {
+ const host_info = get_host_info();
+ // The sneaky Service Worker changes the same-origin 'square' request for a cross-origin image.
+ if (event.request.url.indexOf('square') != -1) {
+ const searchParams = new URLSearchParams(location.search);
+ const mode = searchParams.get("mode") || "cors";
+ event.respondWith(fetch(`${host_info['HTTPS_REMOTE_ORIGIN']}${base_path()}square.png`, { mode }));
+ }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/worker-interception-redirect-serviceworker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-interception-redirect-serviceworker.js
new file mode 100644
index 0000000..d36b0b6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-interception-redirect-serviceworker.js
@@ -0,0 +1,53 @@
+let name;
+if (self.registration.scope.indexOf('scope1') != -1)
+ name = 'sw1';
+if (self.registration.scope.indexOf('scope2') != -1)
+ name = 'sw2';
+
+
+self.addEventListener('fetch', evt => {
+ // There are three types of requests this service worker handles.
+
+ // (1) The first request for the worker, which will redirect elsewhere.
+ // "redirect.py" means to test network redirect, so let network handle it.
+ if (evt.request.url.indexOf('redirect.py') != -1) {
+ return;
+ }
+ // "sw-redirect" means to test service worker redirect, so respond with a
+ // redirect.
+ if (evt.request.url.indexOf('sw-redirect') != -1) {
+ const url = new URL(evt.request.url);
+ const redirect_to = url.searchParams.get('Redirect');
+ evt.respondWith(Response.redirect(redirect_to));
+ return;
+ }
+
+ // (2) After redirect, the request is for a "webworker.py" URL.
+ // Add a search parameter to indicate this service worker handled the
+ // final request for the worker.
+ if (evt.request.url.indexOf('webworker.py') != -1) {
+ const greeting = encodeURIComponent(`${name} saw the request for the worker script`);
+ // Serve from `./subdir/`, not `./`,
+ // to conform that the base URL used in the worker is
+ // the response URL (`./subdir/`), not the current request URL (`./`).
+ evt.respondWith(fetch(`subdir/worker_interception_redirect_webworker.py?greeting=${greeting}`));
+ return;
+ }
+
+ const path = (new URL(evt.request.url)).pathname;
+
+ // (3) The worker does an importScripts() to import-scripts-echo.py. Indicate
+ // that this service worker handled the request.
+ if (evt.request.url.indexOf('import-scripts-echo.py') != -1) {
+ const msg = encodeURIComponent(`${name} saw importScripts from the worker: ${path}`);
+ evt.respondWith(fetch(`import-scripts-echo.py?msg=${msg}`));
+ return;
+ }
+
+ // (4) The worker does a fetch() to simple.txt. Indicate that this service
+ // worker handled the request.
+ if (evt.request.url.indexOf('simple.txt') != -1) {
+ evt.respondWith(new Response(`${name} saw the fetch from the worker: ${path}`));
+ return;
+ }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/worker-interception-redirect-webworker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-interception-redirect-webworker.js
new file mode 100644
index 0000000..b7e6d81
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-interception-redirect-webworker.js
@@ -0,0 +1,56 @@
+// This is the (shared or dedicated) worker file for the
+// worker-interception-redirect test. It should be served by the corresponding
+// .py file instead of being served directly.
+//
+// This file is served from both resources/*webworker.py,
+// resources/scope2/*webworker.py and resources/subdir/*webworker.py.
+// Relative paths are used in `fetch()` and `importScripts()` to confirm that
+// the correct base URLs are used.
+
+// This greeting text is meant to be injected by the Python script that serves
+// this file, to indicate how the script was served (from network or from
+// service worker).
+//
+// We can't just use a sub pipe and name this file .sub.js since we want
+// to serve the file from multiple URLs (see above).
+let greeting = '%GREETING_TEXT%';
+if (!greeting)
+ greeting = 'the worker script was served from network';
+
+// Call importScripts() which fills |echo_output| with a string indicating
+// whether a service worker intercepted the importScripts() request.
+let echo_output;
+const import_scripts_msg = encodeURIComponent(
+ 'importScripts: served from network');
+let import_scripts_greeting = 'not set';
+try {
+ importScripts(`import-scripts-echo.py?msg=${import_scripts_msg}`);
+ import_scripts_greeting = echo_output;
+} catch(e) {
+ import_scripts_greeting = 'importScripts failed';
+}
+
+async function runTest(port) {
+ port.postMessage(greeting);
+
+ port.postMessage(import_scripts_greeting);
+
+ const response = await fetch('simple.txt');
+ const text = await response.text();
+ port.postMessage('fetch(): ' + text);
+
+ port.postMessage(self.location.href);
+}
+
+if ('DedicatedWorkerGlobalScope' in self &&
+ self instanceof DedicatedWorkerGlobalScope) {
+ runTest(self);
+} else if (
+ 'SharedWorkerGlobalScope' in self &&
+ self instanceof SharedWorkerGlobalScope) {
+ self.onconnect = function(e) {
+ const port = e.ports[0];
+ port.start();
+ runTest(port);
+ };
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/worker-load-interceptor.js b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-load-interceptor.js
new file mode 100644
index 0000000..ebc0db6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-load-interceptor.js
@@ -0,0 +1,16 @@
+importScripts('/common/get-host-info.sub.js');
+
+const response_text = 'This load was successfully intercepted.';
+const response_script =
+ `const message = 'This load was successfully intercepted.';`;
+
+self.onfetch = event => {
+ const url = event.request.url;
+ if (url.indexOf('synthesized-response.txt') != -1) {
+ event.respondWith(new Response(response_text));
+ } else if (url.indexOf('synthesized-response.js') != -1) {
+ event.respondWith(new Response(
+ response_script,
+ {headers: {'Content-Type': 'application/javascript'}}));
+ }
+};
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/worker-testharness.js b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-testharness.js
new file mode 100644
index 0000000..73e97be
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/worker-testharness.js
@@ -0,0 +1,49 @@
+/*
+ * worker-test-harness should be considered a temporary polyfill around
+ * testharness.js for supporting Service Worker based tests. It should not be
+ * necessary once the test harness is able to drive worker based tests natively.
+ * See https://github.com/w3c/testharness.js/pull/82 for status of effort to
+ * update upstream testharness.js. Once the upstreaming is complete, tests that
+ * reference worker-test-harness should be updated to directly import
+ * testharness.js.
+ */
+
+importScripts('/resources/testharness.js');
+
+(function() {
+ var next_cache_index = 1;
+
+ // Returns a promise that resolves to a newly created Cache object. The
+ // returned Cache will be destroyed when |test| completes.
+ function create_temporary_cache(test) {
+ var uniquifier = String(++next_cache_index);
+ var cache_name = self.location.pathname + '/' + uniquifier;
+
+ test.add_cleanup(function() {
+ return self.caches.delete(cache_name);
+ });
+
+ return self.caches.delete(cache_name)
+ .then(function() {
+ return self.caches.open(cache_name);
+ });
+ }
+
+ self.create_temporary_cache = create_temporary_cache;
+})();
+
+// Runs |test_function| with a temporary unique Cache passed in as the only
+// argument. The function is run as a part of Promise chain owned by
+// promise_test(). As such, it is expected to behave in a manner identical (with
+// the exception of the argument) to a function passed into promise_test().
+//
+// E.g.:
+// cache_test(function(cache) {
+// // Do something with |cache|, which is a Cache object.
+// }, "Some Cache test");
+function cache_test(test_function, description) {
+ promise_test(function(test) {
+ return create_temporary_cache(test)
+ .then(test_function);
+ }, description);
+}
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/worker_interception_redirect_webworker.py b/third_party/web_platform_tests/service-workers/service-worker/resources/worker_interception_redirect_webworker.py
new file mode 100644
index 0000000..4ed5bee
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/worker_interception_redirect_webworker.py
@@ -0,0 +1,20 @@
+# This serves the worker JavaScript file. It takes a |greeting| request
+# parameter to inject into the JavaScript to indicate how the request
+# reached the server.
+import os
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ path = os.path.join(os.path.dirname(isomorphic_decode(__file__)),
+ u"worker-interception-redirect-webworker.js")
+ body = open(path, u"rb").read()
+ if b"greeting" in request.GET:
+ body = body.replace(b"%GREETING_TEXT%", request.GET[b"greeting"])
+ else:
+ body = body.replace(b"%GREETING_TEXT%", b"")
+
+ headers = []
+ headers.append((b"Content-Type", b"text/javascript"))
+
+ return headers, body
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/xhr-content-length-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/xhr-content-length-worker.js
new file mode 100644
index 0000000..604deec
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/xhr-content-length-worker.js
@@ -0,0 +1,22 @@
+// Service worker for the xhr-content-length test.
+
+self.addEventListener("fetch", event => {
+ const url = new URL(event.request.url);
+ const type = url.searchParams.get("type");
+
+ if (type === "no-content-length") {
+ event.respondWith(new Response("Hello!"));
+ }
+
+ if (type === "larger-content-length") {
+ event.respondWith(new Response("meeeeh", { headers: [["Content-Length", "10000"]] }));
+ }
+
+ if (type === "double-content-length") {
+ event.respondWith(new Response("meeeeh", { headers: [["Content-Length", "10000"], ["Content-Length", "10000"]] }));
+ }
+
+ if (type === "bogus-content-length") {
+ event.respondWith(new Response("meeeeh", { headers: [["Content-Length", "test"]] }));
+ }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/xhr-iframe.html b/third_party/web_platform_tests/service-workers/service-worker/resources/xhr-iframe.html
new file mode 100644
index 0000000..4c57bbb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/xhr-iframe.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe for xhr tests</title>
+<script>
+async function xhr(url, options) {
+ return new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ const opts = options ? options : {};
+ xhr.onload = () => {
+ resolve(xhr);
+ };
+ xhr.onerror = () => {
+ reject('xhr failed');
+ };
+
+ xhr.open('GET', url);
+ if (opts.responseType) {
+ xhr.responseType = opts.responseType;
+ }
+ xhr.send();
+ });
+}
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/xhr-response-url-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/xhr-response-url-worker.js
new file mode 100644
index 0000000..906ad50
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/xhr-response-url-worker.js
@@ -0,0 +1,32 @@
+// Service worker for the xhr-response-url test.
+
+self.addEventListener('fetch', event => {
+ const url = new URL(event.request.url);
+ const respondWith = url.searchParams.get('respondWith');
+ if (!respondWith)
+ return;
+
+ if (respondWith == 'fetch') {
+ const target = url.searchParams.get('url');
+ event.respondWith(fetch(target));
+ return;
+ }
+
+ if (respondWith == 'string') {
+ const headers = {'content-type': 'text/plain'};
+ event.respondWith(new Response('hello', {headers}));
+ return;
+ }
+
+ if (respondWith == 'document') {
+ const doc = `
+ <!DOCTYPE html>
+ <html>
+ <title>hi</title>
+ <body>hello</body>
+ </html>`;
+ const headers = {'content-type': 'text/html'};
+ event.respondWith(new Response(doc, {headers}));
+ return;
+ }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/xsl-base-url-iframe.xml b/third_party/web_platform_tests/service-workers/service-worker/resources/xsl-base-url-iframe.xml
new file mode 100644
index 0000000..065a07a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/xsl-base-url-iframe.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/xsl" href="resources/request-url-path/import-relative.xsl"?>
+<stylesheet-test>
+This tests a stylesheet which has a xsl:import with a relative URL.
+</stylesheet-test>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/xsl-base-url-worker.js b/third_party/web_platform_tests/service-workers/service-worker/resources/xsl-base-url-worker.js
new file mode 100644
index 0000000..50e2b18
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/xsl-base-url-worker.js
@@ -0,0 +1,12 @@
+self.addEventListener('fetch', event => {
+ const url = new URL(event.request.url);
+
+ // For the import-relative.xsl file, respond in a way that changes the
+ // response URL. This is expected to change the base URL and allow the import
+ // from the file to succeed.
+ const path = 'request-url-path/import-relative.xsl';
+ if (url.pathname.indexOf(path) != -1) {
+ // Respond with a different URL, deleting "request-url-path/".
+ event.respondWith(fetch('import-relative.xsl'));
+ }
+});
diff --git a/third_party/web_platform_tests/service-workers/service-worker/resources/xslt-pass.xsl b/third_party/web_platform_tests/service-workers/service-worker/resources/xslt-pass.xsl
new file mode 100644
index 0000000..2cd7f2f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/resources/xslt-pass.xsl
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+ <xsl:template match="/">
+ <html>
+ <body>
+ <p>PASS</p>
+ </body>
+ </html>
+ </xsl:template>
+</xsl:stylesheet>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/respond-with-body-accessed-response.https.html b/third_party/web_platform_tests/service-workers/service-worker/respond-with-body-accessed-response.https.html
new file mode 100644
index 0000000..f6713d8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/respond-with-body-accessed-response.https.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<title>Service Worker responds with .body accessed response.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+let frame;
+
+promise_test(t => {
+ const SCOPE = 'resources/respond-with-body-accessed-response-iframe.html';
+ const SCRIPT = 'resources/respond-with-body-accessed-response-worker.js';
+
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(reg => {
+ promise_test(t => {
+ if (frame)
+ frame.remove();
+ return reg.unregister();
+ }, 'restore global state');
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(() => { return with_iframe(SCOPE); })
+ .then(f => { frame = f; });
+ }, 'initialize global state');
+
+const TEST_CASES = [
+ "type=basic",
+ "type=opaque",
+ "type=default",
+ "type=basic&clone=1",
+ "type=opaque&clone=1",
+ "type=default&clone=1",
+ "type=basic&clone=2",
+ "type=opaque&clone=2",
+ "type=default&clone=2",
+ "type=basic&passThroughCache=true",
+ "type=opaque&passThroughCache=true",
+ "type=default&passThroughCache=true",
+ "type=basic&clone=1&passThroughCache=true",
+ "type=opaque&clone=1&passThroughCache=true",
+ "type=default&clone=1&passThroughCache=true",
+ "type=basic&clone=2&passThroughCache=true",
+ "type=opaque&clone=2&passThroughCache=true",
+ "type=default&clone=2&passThroughCache=true",
+];
+
+TEST_CASES.forEach(param => {
+ promise_test(t => {
+ const url = 'TestRequest?' + param;
+ return frame.contentWindow.getJSONP(url)
+ .then(result => { assert_equals(result, 'OK'); });
+ }, 'test: ' + param);
+ });
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/same-site-cookies.https.html b/third_party/web_platform_tests/service-workers/service-worker/same-site-cookies.https.html
new file mode 100644
index 0000000..1d9b60d
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/same-site-cookies.https.html
@@ -0,0 +1,496 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<meta name="timeout" content="long">
+<title>Service Worker: Same-site cookie behavior</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="/cookies/resources/cookie-helper.sub.js"></script>
+<body>
+<script>
+'use strict';
+
+const COOKIE_VALUE = 'COOKIE_VALUE';
+
+function make_nested_url(nested_origins, target_url) {
+ for (let i = nested_origins.length - 1; i >= 0; --i) {
+ target_url = new URL(
+ `./resources/nested-parent.html?target=${encodeURIComponent(target_url)}`,
+ nested_origins[i] + self.location.pathname);
+ }
+ return target_url;
+}
+
+const scopepath = '/cookies/resources/postToParent.py?with-sw';
+
+async function unregister_service_worker(origin, nested_origins=[]) {
+ let target_url = origin +
+ '/service-workers/service-worker/resources/unregister-rewrite-worker.html' +
+ '?scopepath=' + encodeURIComponent(scopepath);
+ target_url = make_nested_url(nested_origins, target_url);
+ const w = window.open(target_url);
+ try {
+ await wait_for_message('SW-UNREGISTERED');
+ } finally {
+ w.close();
+ }
+}
+
+async function register_service_worker(origin, nested_origins=[]) {
+ let target_url = origin +
+ '/service-workers/service-worker/resources/register-rewrite-worker.html' +
+ '?scopepath=' + encodeURIComponent(scopepath);
+ target_url = make_nested_url(nested_origins, target_url);
+ const w = window.open(target_url);
+ try {
+ await wait_for_message('SW-REGISTERED');
+ } finally {
+ w.close();
+ }
+}
+
+async function run_test(t, origin, navaction, swaction, expected,
+ redirect_origins=[], nested_origins=[]) {
+ if (swaction === 'navpreload') {
+ assert_true('navigationPreload' in ServiceWorkerRegistration.prototype,
+ 'navigation preload must be supported');
+ }
+ const sw_param = swaction === 'no-sw' ? 'no-sw' : 'with-sw';
+ let action_param = '';
+ if (swaction === 'fallback') {
+ action_param = '&ignore';
+ } else if (swaction !== 'no-sw') {
+ action_param = '&' + swaction;
+ }
+ const navpreload_param = swaction === 'navpreload' ? '&navpreload' : '';
+ const change_request_param = swaction === 'change-request' ? '&change-request' : '';
+ const target_string = origin + `/cookies/resources/postToParent.py?` +
+ `${sw_param}${action_param}`
+ let target_url = new URL(target_string);
+
+ for (let i = redirect_origins.length - 1; i >= 0; --i) {
+ const redirect_url = new URL(
+ `./resources/redirect.py?Status=307&Redirect=${encodeURIComponent(target_url)}`,
+ redirect_origins[i] + self.location.pathname);
+ target_url = redirect_url;
+ }
+
+ if (navaction === 'window.open') {
+ target_url = new URL(
+ `./resources/window-opener.html?target=${encodeURIComponent(target_url)}`,
+ self.origin + self.location.pathname);
+ } else if (navaction === 'form post') {
+ target_url = new URL(
+ `./resources/form-poster.html?target=${encodeURIComponent(target_url)}`,
+ self.origin + self.location.pathname);
+ } else if (navaction === 'set location') {
+ target_url = new URL(
+ `./resources/location-setter.html?target=${encodeURIComponent(target_url)}`,
+ self.origin + self.location.pathname);
+ }
+
+ const w = window.open(make_nested_url(nested_origins, target_url));
+ t.add_cleanup(() => w.close());
+
+ const result = await wait_for_message('COOKIES');
+ verifySameSiteCookieState(expected, COOKIE_VALUE, result.data);
+}
+
+promise_test(async t => {
+ await resetSameSiteCookies(self.origin, COOKIE_VALUE);
+ await register_service_worker(self.origin);
+
+ await resetSameSiteCookies(SECURE_SUBDOMAIN_ORIGIN, COOKIE_VALUE);
+ await register_service_worker(SECURE_SUBDOMAIN_ORIGIN);
+
+ await resetSameSiteCookies(SECURE_CROSS_SITE_ORIGIN, COOKIE_VALUE);
+ await register_service_worker(SECURE_CROSS_SITE_ORIGIN);
+
+ await register_service_worker(self.origin,
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'Setup service workers');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'no-sw',
+ SameSiteStatus.STRICT);
+}, 'same-origin, window.open with no service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'fallback',
+ SameSiteStatus.STRICT);
+}, 'same-origin, window.open with fallback');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'passthrough',
+ SameSiteStatus.STRICT);
+}, 'same-origin, window.open with passthrough');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'change-request',
+ SameSiteStatus.STRICT);
+}, 'same-origin, window.open with change-request');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'navpreload',
+ SameSiteStatus.STRICT);
+}, 'same-origin, window.open with navpreload');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'no-sw',
+ SameSiteStatus.STRICT);
+}, 'same-site, window.open with no service worker');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'fallback',
+ SameSiteStatus.STRICT);
+}, 'same-site, window.open with fallback');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'passthrough',
+ SameSiteStatus.STRICT);
+}, 'same-site, window.open with passthrough');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'change-request',
+ SameSiteStatus.STRICT);
+}, 'same-site, window.open with change-request');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'window.open', 'navpreload',
+ SameSiteStatus.STRICT);
+}, 'same-site, window.open with navpreload');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'no-sw',
+ SameSiteStatus.LAX);
+}, 'cross-site, window.open with no service worker');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'fallback',
+ SameSiteStatus.LAX);
+}, 'cross-site, window.open with fallback');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'passthrough',
+ SameSiteStatus.LAX);
+}, 'cross-site, window.open with passthrough');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'change-request',
+ SameSiteStatus.STRICT);
+}, 'cross-site, window.open with change-request');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'window.open', 'navpreload',
+ SameSiteStatus.LAX);
+}, 'cross-site, window.open with navpreload');
+
+//
+// window.open redirect tests
+//
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'no-sw',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, window.open with no service worker and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'fallback',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, window.open with fallback and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'passthrough',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, window.open with passthrough and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'change-request',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, window.open with change-request and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'navpreload',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, window.open with navpreload and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'no-sw',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, window.open with no service worker and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'fallback',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, window.open with fallback and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'passthrough',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, window.open with passthrough and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'change-request',
+ SameSiteStatus.STRICT, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, window.open with change-request and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'navpreload',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, window.open with navpreload and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'no-sw',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN, self.origin]);
+}, 'same-origin, window.open with no service worker, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'fallback',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN, self.origin]);
+}, 'same-origin, window.open with fallback, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'passthrough',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN, self.origin]);
+}, 'same-origin, window.open with passthrough, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'change-request',
+ SameSiteStatus.STRICT, [SECURE_CROSS_SITE_ORIGIN, self.origin]);
+}, 'same-origin, window.open with change-request, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'navpreload',
+ SameSiteStatus.LAX, [SECURE_CROSS_SITE_ORIGIN, self.origin]);
+}, 'same-origin, window.open with navpreload, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+//
+// Double-nested frame calling open.window() tests
+//
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'no-sw',
+ SameSiteStatus.STRICT, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested window.open with cross-site middle frame and ' +
+ 'no service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'fallback',
+ SameSiteStatus.STRICT, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested window.open with cross-site middle frame and ' +
+ 'fallback service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'passthrough',
+ SameSiteStatus.STRICT, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested window.open with cross-site middle frame and ' +
+ 'passthrough service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'change-request',
+ SameSiteStatus.STRICT, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested window.open with cross-site middle frame and ' +
+ 'change-request service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'window.open', 'navpreload',
+ SameSiteStatus.STRICT, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested window.open with cross-site middle frame and ' +
+ 'navpreload service worker');
+
+//
+// Double-nested frame setting location tests
+//
+promise_test(t => {
+ return run_test(t, self.origin, 'set location', 'no-sw',
+ SameSiteStatus.CROSS_SITE, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested set location with cross-site middle frame and ' +
+ 'no service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'set location', 'fallback',
+ SameSiteStatus.CROSS_SITE, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested set location with cross-site middle frame and ' +
+ 'fallback service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'set location', 'passthrough',
+ SameSiteStatus.CROSS_SITE, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested set location with cross-site middle frame and ' +
+ 'passthrough service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'set location', 'change-request',
+ SameSiteStatus.CROSS_SITE, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested set location with cross-site middle frame and ' +
+ 'change-request service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'set location', 'navpreload',
+ SameSiteStatus.CROSS_SITE, [],
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, nested set location with cross-site middle frame and ' +
+ 'navpreload service worker');
+
+//
+// Form POST tests
+//
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'no-sw', SameSiteStatus.STRICT);
+}, 'same-origin, form post with no service worker');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'fallback',
+ SameSiteStatus.STRICT);
+}, 'same-origin, form post with fallback');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'passthrough',
+ SameSiteStatus.STRICT);
+}, 'same-origin, form post with passthrough');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'change-request',
+ SameSiteStatus.STRICT);
+}, 'same-origin, form post with change-request');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'form post', 'no-sw',
+ SameSiteStatus.STRICT);
+}, 'same-site, form post with no service worker');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'form post', 'fallback',
+ SameSiteStatus.STRICT);
+}, 'same-site, form post with fallback');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'form post', 'passthrough',
+ SameSiteStatus.STRICT);
+}, 'same-site, form post with passthrough');
+
+promise_test(t => {
+ return run_test(t, SECURE_SUBDOMAIN_ORIGIN, 'form post', 'change-request',
+ SameSiteStatus.STRICT);
+}, 'same-site, form post with change-request');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'form post', 'no-sw',
+ SameSiteStatus.CROSS_SITE);
+}, 'cross-site, form post with no service worker');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'form post', 'fallback',
+ SameSiteStatus.CROSS_SITE);
+}, 'cross-site, form post with fallback');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'form post', 'passthrough',
+ SameSiteStatus.CROSS_SITE);
+}, 'cross-site, form post with passthrough');
+
+promise_test(t => {
+ return run_test(t, SECURE_CROSS_SITE_ORIGIN, 'form post', 'change-request',
+ SameSiteStatus.STRICT);
+}, 'cross-site, form post with change-request');
+
+//
+// Form POST redirect tests
+//
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'no-sw',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, form post with no service worker and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'fallback',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, form post with fallback and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'passthrough',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, form post with passthrough and same-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'change-request',
+ SameSiteStatus.STRICT, [SECURE_SUBDOMAIN_ORIGIN]);
+}, 'same-origin, form post with change-request and same-site redirect');
+
+// navpreload is not supported for POST requests
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'no-sw',
+ SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, form post with no service worker and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'fallback',
+ SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, form post with fallback and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'passthrough',
+ SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, form post with passthrough and cross-site redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'change-request',
+ SameSiteStatus.STRICT, [SECURE_CROSS_SITE_ORIGIN]);
+}, 'same-origin, form post with change-request and cross-site redirect');
+
+// navpreload is not supported for POST requests
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'no-sw',
+ SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN,
+ self.origin]);
+}, 'same-origin, form post with no service worker, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'fallback',
+ SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN,
+ self.origin]);
+}, 'same-origin, form post with fallback, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'passthrough',
+ SameSiteStatus.CROSS_SITE, [SECURE_CROSS_SITE_ORIGIN,
+ self.origin]);
+}, 'same-origin, form post with passthrough, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+promise_test(t => {
+ return run_test(t, self.origin, 'form post', 'change-request',
+ SameSiteStatus.STRICT, [SECURE_CROSS_SITE_ORIGIN,
+ self.origin]);
+}, 'same-origin, form post with change-request, cross-site redirect, and ' +
+ 'same-origin redirect');
+
+// navpreload is not supported for POST requests
+
+promise_test(async t => {
+ await unregister_service_worker(self.origin);
+ await unregister_service_worker(SECURE_SUBDOMAIN_ORIGIN);
+ await unregister_service_worker(SECURE_CROSS_SITE_ORIGIN);
+ await unregister_service_worker(self.origin,
+ [self.origin, SECURE_CROSS_SITE_ORIGIN]);
+}, 'Cleanup service workers');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/sandboxed-iframe-fetch-event.https.html b/third_party/web_platform_tests/service-workers/service-worker/sandboxed-iframe-fetch-event.https.html
new file mode 100644
index 0000000..ba34e79
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/sandboxed-iframe-fetch-event.https.html
@@ -0,0 +1,536 @@
+<!DOCTYPE html>
+<title>ServiceWorker FetchEvent for sandboxed iframe.</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var lastCallbackId = 0;
+var callbacks = {};
+function doTest(frame, type) {
+ return new Promise(function(resolve) {
+ var id = ++lastCallbackId;
+ callbacks[id] = resolve;
+ frame.contentWindow.postMessage({id: id, type: type}, '*');
+ });
+}
+
+// Asks the service worker for data about requests and clients seen. The
+// worker posts a message back with |data| where:
+// |data.requests|: the requests the worker received FetchEvents for
+// |data.clients|: the URLs of all the worker's clients
+// The worker clears its data after responding.
+function getResultsFromWorker(worker) {
+ return new Promise(resolve => {
+ let channel = new MessageChannel();
+ channel.port1.onmessage = msg => {
+ resolve(msg.data);
+ };
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ });
+}
+
+window.onmessage = function (e) {
+ message = e.data;
+ var id = message['id'];
+ var callback = callbacks[id];
+ delete callbacks[id];
+ callback(message['result']);
+};
+
+const SCOPE = 'resources/sandboxed-iframe-fetch-event-iframe.py';
+const SCRIPT = 'resources/sandboxed-iframe-fetch-event-worker.js';
+const expected_base_url = new URL(SCOPE, location.href);
+// Service worker controlling |SCOPE|.
+let worker;
+// A normal iframe.
+// This should be controlled by a service worker.
+let normal_frame;
+// An iframe created by <iframe sandbox='allow-scripts'>.
+// This should NOT be controlled by a service worker.
+let sandboxed_frame;
+// An iframe created by <iframe sandbox='allow-scripts allow-same-origin'>.
+// This should be controlled by a service worker.
+let sandboxed_same_origin_frame;
+// An iframe whose response header has
+// 'Content-Security-Policy: allow-scripts'.
+// This should NOT be controlled by a service worker.
+let sandboxed_frame_by_header;
+// An iframe whose response header has
+// 'Content-Security-Policy: allow-scripts allow-same-origin'.
+// This should be controlled by a service worker.
+let sandboxed_same_origin_frame_by_header;
+
+promise_test(t => {
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ add_completion_callback(() => registration.unregister());
+ worker = registration.installing;
+ return wait_for_state(t, registration.installing, 'activated');
+ });
+}, 'Prepare a service worker.');
+
+promise_test(t => {
+ return with_iframe(SCOPE + '?iframe')
+ .then(f => {
+ normal_frame = f;
+ add_completion_callback(() => f.remove());
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1);
+ assert_equals(requests[0], expected_base_url + '?iframe');
+ assert_true(data.clients.includes(expected_base_url + '?iframe'));
+ });
+}, 'Prepare a normal iframe.');
+
+promise_test(t => {
+ return with_sandboxed_iframe(SCOPE + '?sandboxed-iframe', 'allow-scripts')
+ .then(f => {
+ sandboxed_frame = f;
+ add_completion_callback(() => f.remove());
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0);
+ assert_false(data.clients.includes(expected_base_url +
+ '?sandboxed-iframe'));
+ });
+}, 'Prepare an iframe sandboxed by <iframe sandbox="allow-scripts">.');
+
+promise_test(t => {
+ return with_sandboxed_iframe(SCOPE + '?sandboxed-iframe-same-origin',
+ 'allow-scripts allow-same-origin')
+ .then(f => {
+ sandboxed_same_origin_frame = f;
+ add_completion_callback(() => f.remove());
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1);
+ assert_equals(requests[0],
+ expected_base_url + '?sandboxed-iframe-same-origin');
+ assert_true(data.clients.includes(
+ expected_base_url + '?sandboxed-iframe-same-origin'));
+ })
+}, 'Prepare an iframe sandboxed by ' +
+ '<iframe sandbox="allow-scripts allow-same-origin">.');
+
+promise_test(t => {
+ const iframe_full_url = expected_base_url + '?sandbox=allow-scripts&' +
+ 'sandboxed-frame-by-header';
+ return with_iframe(iframe_full_url)
+ .then(f => {
+ sandboxed_frame_by_header = f;
+ add_completion_callback(() => f.remove());
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'Service worker should provide the response');
+ assert_equals(requests[0], iframe_full_url);
+ assert_false(data.clients.includes(iframe_full_url),
+ 'Service worker should NOT control the sandboxed page');
+ });
+}, 'Prepare an iframe sandboxed by CSP HTTP header with allow-scripts.');
+
+promise_test(t => {
+ const iframe_full_url =
+ expected_base_url + '?sandbox=allow-scripts%20allow-same-origin&' +
+ 'sandboxed-iframe-same-origin-by-header';
+ return with_iframe(iframe_full_url)
+ .then(f => {
+ sandboxed_same_origin_frame_by_header = f;
+ add_completion_callback(() => f.remove());
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1);
+ assert_equals(requests[0], iframe_full_url);
+ assert_true(data.clients.includes(iframe_full_url));
+ })
+}, 'Prepare an iframe sandboxed by CSP HTTP header with allow-scripts and ' +
+ 'allow-same-origin.');
+
+promise_test(t => {
+ let frame = normal_frame;
+ return doTest(frame, 'fetch')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The fetch request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=fetch');
+ });
+}, 'Fetch request from a normal iframe');
+
+promise_test(t => {
+ let frame = normal_frame;
+ return doTest(frame, 'fetch-from-worker')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The fetch request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=fetch-from-worker');
+ });
+}, 'Fetch request from a worker in a normal iframe');
+
+promise_test(t => {
+ let frame = normal_frame;
+ return doTest(frame, 'iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=iframe');
+ assert_true(data.clients.includes(frame.src + '&test=iframe'));
+
+ });
+}, 'Request for an iframe in the normal iframe');
+
+promise_test(t => {
+ let frame = normal_frame;
+ return doTest(frame, 'sandboxed-iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts flag in the normal ' +
+ 'iframe');
+
+promise_test(t => {
+ let frame = normal_frame;
+ return doTest(frame, 'sandboxed-iframe-same-origin')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0],
+ frame.src + '&test=sandboxed-iframe-same-origin');
+ assert_true(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe-same-origin'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts and ' +
+ 'allow-same-origin flag in the normal iframe');
+
+promise_test(t => {
+ let frame = sandboxed_frame;
+ return doTest(frame, 'fetch')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The fetch request should NOT be handled by SW.');
+ });
+}, 'Fetch request from iframe sandboxed by an attribute with allow-scripts ' +
+ 'flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame;
+ return doTest(frame, 'fetch-from-worker')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The fetch request should NOT be handled by SW.');
+ });
+}, 'Fetch request from a worker in iframe sandboxed by an attribute with ' +
+ 'allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame;
+ return doTest(frame, 'iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(frame.src + '&test=iframe'));
+ });
+}, 'Request for an iframe in the iframe sandboxed by an attribute with ' +
+ 'allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame;
+ return doTest(frame, 'sandboxed-iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts flag in the iframe ' +
+ 'sandboxed by an attribute with allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame;
+ return doTest(frame, 'sandboxed-iframe-same-origin')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe-same-origin'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts and ' +
+ 'allow-same-origin flag in the iframe sandboxed by an attribute with ' +
+ 'allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame;
+ return doTest(frame, 'fetch')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The fetch request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=fetch');
+ });
+}, 'Fetch request from iframe sandboxed by an attribute with allow-scripts ' +
+ 'and allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame;
+ return doTest(frame, 'fetch-from-worker')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The fetch request should be handled by SW.');
+ assert_equals(requests[0],
+ frame.src + '&test=fetch-from-worker');
+ });
+}, 'Fetch request from a worker in iframe sandboxed by an attribute with ' +
+ 'allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame;
+ return doTest(frame, 'iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=iframe');
+ assert_true(data.clients.includes(frame.src + '&test=iframe'));
+ });
+}, 'Request for an iframe in the iframe sandboxed by an attribute with ' +
+ 'allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame;
+ return doTest(frame, 'sandboxed-iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts flag in the iframe ' +
+ 'sandboxed by attribute with allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame;
+ return doTest(frame, 'sandboxed-iframe-same-origin')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0],
+ frame.src + '&test=sandboxed-iframe-same-origin');
+ assert_true(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe-same-origin'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts and ' +
+ 'allow-same-origin flag in the iframe sandboxed by attribute with ' +
+ 'allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame_by_header;
+ return doTest(frame, 'fetch')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ });
+}, 'Fetch request from iframe sandboxed by CSP HTTP header with ' +
+ 'allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame_by_header;
+ return doTest(frame, 'iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(frame.src + '&test=iframe'));
+ });
+}, 'Request for an iframe in the iframe sandboxed by CSP HTTP header with ' +
+ 'allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame_by_header;
+ return doTest(frame, 'sandboxed-iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts flag in the iframe ' +
+ 'sandboxed by CSP HTTP header with allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_frame_by_header;
+ return doTest(frame, 'sandboxed-iframe-same-origin')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe-same-origin'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts and ' +
+ 'allow-same-origin flag in the iframe sandboxed by CSP HTTP header with ' +
+ 'allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame_by_header;
+ return doTest(frame, 'fetch')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=fetch');
+ });
+}, 'Fetch request from iframe sandboxed by CSP HTTP header with ' +
+ 'allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame_by_header;
+ return doTest(frame, 'iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=iframe');
+ assert_true(data.clients.includes(frame.src + '&test=iframe'));
+ });
+}, 'Request for an iframe in the iframe sandboxed by CSP HTTP header with ' +
+ 'allow-scripts and allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame_by_header;
+ return doTest(frame, 'sandboxed-iframe')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ assert_false(
+ data.clients.includes(frame.src + '&test=sandboxed-iframe'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts flag in the ' +
+ 'iframe sandboxed by CSP HTTP header with allow-scripts and ' +
+ 'allow-same-origin flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame_by_header;
+ return doTest(frame, 'sandboxed-iframe-same-origin')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0],
+ frame.src + '&test=sandboxed-iframe-same-origin');
+ assert_true(data.clients.includes(
+ frame.src + '&test=sandboxed-iframe-same-origin'));
+ });
+}, 'Request for an sandboxed iframe with allow-scripts and ' +
+ 'allow-same-origin flag in the iframe sandboxed by CSP HTTP header with ' +
+ 'allow-scripts and allow-same-origin flag');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/sandboxed-iframe-navigator-serviceworker.https.html b/third_party/web_platform_tests/service-workers/service-worker/sandboxed-iframe-navigator-serviceworker.https.html
new file mode 100644
index 0000000..70be6ef
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/sandboxed-iframe-navigator-serviceworker.https.html
@@ -0,0 +1,120 @@
+<!DOCTYPE html>
+<title>Accessing navigator.serviceWorker in sandboxed iframe.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+var lastCallbackId = 0;
+var callbacks = {};
+function postMessageAndWaitResult(frame) {
+ return new Promise(function(resolve, reject) {
+ var id = ++lastCallbackId;
+ callbacks[id] = resolve;
+ frame.contentWindow.postMessage({id:id}, '*');
+ const timeout = 1000;
+ step_timeout(() => reject("no msg back after " + timeout + "ms"), timeout);
+ });
+}
+
+window.onmessage = function(e) {
+ message = e.data;
+ var id = message['id'];
+ var callback = callbacks[id];
+ delete callbacks[id];
+ callback(message.result);
+};
+
+promise_test(function(t) {
+ var url = 'resources/sandboxed-iframe-navigator-serviceworker-iframe.html';
+ var frame;
+ return with_iframe(url)
+ .then(function(f) {
+ frame = f;
+ add_result_callback(() => { frame.remove(); });
+ return postMessageAndWaitResult(f);
+ })
+ .then(function(result) {
+ assert_equals(result, 'ok');
+ });
+ }, 'Accessing navigator.serviceWorker in normal iframe should not throw.');
+
+promise_test(function(t) {
+ var url = 'resources/sandboxed-iframe-navigator-serviceworker-iframe.html';
+ var frame;
+ return with_sandboxed_iframe(url, 'allow-scripts')
+ .then(function(f) {
+ frame = f;
+ add_result_callback(() => { frame.remove(); });
+ return postMessageAndWaitResult(f);
+ })
+ .then(function(result) {
+ assert_equals(
+ result,
+ 'navigator.serviceWorker failed: SecurityError');
+ });
+ }, 'Accessing navigator.serviceWorker in sandboxed iframe should throw.');
+
+promise_test(function(t) {
+ var url = 'resources/sandboxed-iframe-navigator-serviceworker-iframe.html';
+ var frame;
+ return with_sandboxed_iframe(url, 'allow-scripts allow-same-origin')
+ .then(function(f) {
+ frame = f;
+ add_result_callback(() => { frame.remove(); });
+ return postMessageAndWaitResult(f);
+ })
+ .then(function(result) {
+ assert_equals(result, 'ok');
+ });
+ },
+ 'Accessing navigator.serviceWorker in sandboxed iframe with ' +
+ 'allow-same-origin flag should not throw.');
+
+promise_test(function(t) {
+ var url = 'resources/sandboxed-iframe-navigator-serviceworker-iframe.html';
+ var frame;
+ return new Promise(function(resolve) {
+ frame = document.createElement('iframe');
+ add_result_callback(() => { frame.remove(); });
+ frame.sandbox = '';
+ frame.src = url;
+ frame.onload = resolve;
+ document.body.appendChild(frame);
+ // Switch the sandbox attribute while loading the iframe.
+ frame.sandbox = 'allow-scripts allow-same-origin';
+ })
+ .then(function() {
+ return postMessageAndWaitResult(frame)
+ })
+ .then(function(result) {
+ // The HTML spec seems to say that changing the sandbox attribute
+ // after the iframe is inserted into its parent document does not
+ // affect the sandboxing. If that's true, the frame should still
+ // act as if it still doesn't have
+ // 'allow-scripts allow-same-origin' set and throw a SecurityError.
+ //
+ // 1) From Section 4.8.5 "The iframe element":
+ // "When an iframe element is inserted into a document that has a
+ // browsing context, the user agent must create a new browsing
+ // context..."
+ // 2) "Create a new browsing context" expands to Section 7.1
+ // "Browsing contexts", which includes creating a Document and
+ // "Implement the sandboxing for document."
+ // 3) "Implement the sandboxing" expands to Section 7.6 "Sandboxing",
+ // which includes "populate document's active sandboxing flag set".
+ //
+ // It's not clear whether navigation subsequently creates a new
+ // Document, but I'm assuming it wouldn't.
+ // https://html.spec.whatwg.org/multipage/embedded-content.html#attr-iframe-sandbox
+ assert_true(
+ false,
+ 'should NOT get message back from a sandboxed frame where scripts are not allowed to execute');
+ })
+ .catch(msg => {
+ assert_true(msg.startsWith('no msg back'), 'expecting error message "no msg back"');
+ });
+ }, 'Switching iframe sandbox attribute while loading the iframe');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/secure-context.https.html b/third_party/web_platform_tests/service-workers/service-worker/secure-context.https.html
new file mode 100644
index 0000000..666a5d3
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/secure-context.https.html
@@ -0,0 +1,57 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Ensure service worker is bypassed in insecure contexts</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+// This test checks that an HTTPS iframe embedded in an HTTP document is not
+// loaded via a service worker, since it's not a secure context. To that end, we
+// first register a service worker, wait for its activation, and create an
+// iframe that is controlled by said service worker. We use the iframe as a
+// way to receive messages from the service worker.
+// The bulk of the test begins by opening an HTTP window with the noopener
+// option, installing a message event handler, and embedding an HTTPS iframe. If
+// the browser behaves correctly then the iframe will be loaded from the network
+// and will contain a script that posts a message to the parent window,
+// informing it that it was loaded from the network. If, however, the iframe is
+// intercepted, the service worker will return a page with a script that posts a
+// message to the parent window, informing it that it was intercepted.
+// Upon getting either result, the window will report the result to the service
+// worker by navigating to a reporting URL. The service worker will then inform
+// all clients about the result, including the controlled iframe from the
+// beginning of the test. The message event handler will verify that the result
+// is as expected, concluding the test.
+promise_test(t => {
+ const SCRIPT = "resources/secure-context-service-worker.js";
+ const SCOPE = "resources/";
+ const HTTP_IFRAME_URL = get_host_info().HTTP_ORIGIN + base_path() + SCOPE + "secure-context/window.html";
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(registration => {
+ t.add_cleanup(() => {
+ return registration.unregister();
+ });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(SCOPE + "blank.html");
+ })
+ .then(iframe => {
+ t.add_cleanup(() => {
+ iframe.remove();
+ });
+ return new Promise(resolve => {
+ iframe.contentWindow.navigator.serviceWorker.onmessage = t.step_func(event => {
+ assert_equals(event.data, 'network');
+ resolve();
+ });
+ window.open(HTTP_IFRAME_URL, 'MyWindow', 'noopener');
+ });
+ });
+})
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/service-worker-csp-connect.https.html b/third_party/web_platform_tests/service-workers/service-worker/service-worker-csp-connect.https.html
new file mode 100644
index 0000000..226f4a4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/service-worker-csp-connect.https.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>Service Worker: CSP connect directive for ServiceWorker script</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+service_worker_test(
+ 'resources/service-worker-csp-worker.py?directive=connect',
+ 'CSP test for connect-src in ServiceWorkerGlobalScope');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/service-worker-csp-default.https.html b/third_party/web_platform_tests/service-workers/service-worker/service-worker-csp-default.https.html
new file mode 100644
index 0000000..1d4e762
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/service-worker-csp-default.https.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>Service Worker: CSP default directive for ServiceWorker script</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+service_worker_test(
+ 'resources/service-worker-csp-worker.py?directive=default',
+ 'CSP test for default-src in ServiceWorkerGlobalScope');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/service-worker-csp-script.https.html b/third_party/web_platform_tests/service-workers/service-worker/service-worker-csp-script.https.html
new file mode 100644
index 0000000..14c2eb7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/service-worker-csp-script.https.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<title>Service Worker: CSP script directive for ServiceWorker script</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+service_worker_test(
+ 'resources/service-worker-csp-worker.py?directive=script',
+ 'CSP test for script-src in ServiceWorkerGlobalScope');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/service-worker-header.https.html b/third_party/web_platform_tests/service-workers/service-worker/service-worker-header.https.html
new file mode 100644
index 0000000..fb902cd
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/service-worker-header.https.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<title>Service Worker: Service-Worker header</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(async t => {
+ const script = 'resources/service-worker-header.py'
+ + '?header&import=service-worker-header.py?no-header';
+ const scope = 'resources/service-worker-header';
+ const expected_url = normalizeURL(script);
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => registration.unregister());
+ assert_true(registration instanceof ServiceWorkerRegistration);
+
+ await wait_for_state(t, registration.installing, 'activated');
+ await registration.update();
+}, 'A request to fetch service worker main script should have Service-Worker '
+ + 'header and imported scripts should not have one');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/serviceworker-message-event-historical.https.html b/third_party/web_platform_tests/service-workers/service-worker/serviceworker-message-event-historical.https.html
new file mode 100644
index 0000000..fac8f20
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/serviceworker-message-event-historical.https.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<title>Service Worker: ServiceWorkerMessageEvent</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html';
+ var url = 'resources/postmessage-to-client-worker.js';
+ return service_worker_unregister_and_register(t, url, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ var w = frame.contentWindow;
+ var worker = w.navigator.serviceWorker.controller;
+ assert_equals(
+ self.ServiceWorkerMessageEvent, undefined,
+ 'ServiceWorkerMessageEvent should not be defined.');
+ return new Promise(function(resolve) {
+ w.navigator.serviceWorker.onmessage = t.step_func(function(e) {
+ assert_true(
+ e instanceof w.MessageEvent,
+ 'message events should use MessageEvent interface.');
+ assert_true(e.source instanceof w.ServiceWorker);
+ assert_equals(e.type, 'message');
+ assert_equals(e.source, worker,
+ 'source should equal to the controller.');
+ assert_equals(e.ports.length, 0);
+ resolve();
+ });
+ worker.postMessage('PING');
+ });
+ });
+ }, 'Test MessageEvent supplants ServiceWorkerMessageEvent.');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/serviceworkerobject-scripturl.https.html b/third_party/web_platform_tests/service-workers/service-worker/serviceworkerobject-scripturl.https.html
new file mode 100644
index 0000000..6004985
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/serviceworkerobject-scripturl.https.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<title>ServiceWorker object: scriptURL property</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+function url_test(name, url) {
+ const scope = 'resources/scope/' + name;
+ const expectedURL = normalizeURL(url);
+
+ promise_test(async t => {
+ const registration =
+ await service_worker_unregister_and_register(t, url, scope);
+ const worker = registration.installing;
+ assert_equals(worker.scriptURL, expectedURL, 'scriptURL');
+ await registration.unregister();
+ }, 'Verify the scriptURL property: ' + name);
+}
+
+url_test('relative', 'resources/empty-worker.js');
+url_test('with-fragment', 'resources/empty-worker.js#ref');
+url_test('with-query', 'resources/empty-worker.js?ref');
+url_test('absolute', normalizeURL('./resources/empty-worker.js'));
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-installed.https.html b/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-installed.https.html
new file mode 100644
index 0000000..b604f65
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-installed.https.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<title>Service Worker: Skip waiting installed worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html?skip-waiting-installed';
+ var url1 = 'resources/empty.js';
+ var url2 = 'resources/skip-waiting-installed-worker.js';
+ var frame, frame_sw, service_worker, registration, onmessage, oncontrollerchanged;
+ var saw_message = new Promise(function(resolve) {
+ onmessage = function(e) {
+ resolve(e.data);
+ };
+ })
+ .then(function(message) {
+ assert_equals(
+ message, 'PASS',
+ 'skipWaiting promise should be resolved with undefined');
+ });
+ var saw_controllerchanged = new Promise(function(resolve) {
+ oncontrollerchanged = function() {
+ assert_equals(
+ frame_sw.controller.scriptURL, normalizeURL(url2),
+ 'Controller scriptURL should change to the second one');
+ assert_equals(registration.active.scriptURL, normalizeURL(url2),
+ 'Worker which calls skipWaiting should be active by controllerchange');
+ resolve();
+ };
+ });
+ return service_worker_unregister_and_register(t, url1, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ frame_sw = f.contentWindow.navigator.serviceWorker;
+ assert_equals(
+ frame_sw.controller.scriptURL, normalizeURL(url1),
+ 'Document controller scriptURL should equal to the first one');
+ frame_sw.oncontrollerchange = t.step_func(oncontrollerchanged);
+ return navigator.serviceWorker.register(url2, {scope: scope});
+ })
+ .then(function(r) {
+ registration = r;
+ service_worker = r.installing;
+ return wait_for_state(t, service_worker, 'installed');
+ })
+ .then(function() {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = t.step_func(onmessage);
+ service_worker.postMessage({port: channel.port2}, [channel.port2]);
+ return Promise.all([saw_message, saw_controllerchanged]);
+ })
+ .then(function() {
+ frame.remove();
+ });
+ }, 'Test skipWaiting when a installed worker is waiting');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-using-registration.https.html b/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-using-registration.https.html
new file mode 100644
index 0000000..412ee2a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-using-registration.https.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<title>Service Worker: Skip waiting using registration</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html?skip-waiting-using-registration';
+ var url1 = 'resources/empty.js';
+ var url2 = 'resources/skip-waiting-worker.js';
+ var frame, frame_sw, sw_registration, oncontrollerchanged;
+ var saw_controllerchanged = new Promise(function(resolve) {
+ oncontrollerchanged = function(e) {
+ resolve(e);
+ };
+ })
+ .then(function(e) {
+ assert_equals(e.type, 'controllerchange',
+ 'Event name should be "controllerchange"');
+ assert_true(
+ e.target instanceof frame.contentWindow.ServiceWorkerContainer,
+ 'Event target should be a ServiceWorkerContainer');
+ assert_equals(e.target.controller.state, 'activating',
+ 'Controller state should be activating');
+ assert_equals(
+ frame_sw.controller.scriptURL, normalizeURL(url2),
+ 'Controller scriptURL should change to the second one');
+ });
+
+ return service_worker_unregister_and_register(t, url1, scope)
+ .then(function(registration) {
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ t.add_cleanup(function() {
+ f.remove();
+ });
+ frame = f;
+ frame_sw = f.contentWindow.navigator.serviceWorker;
+ assert_equals(
+ frame_sw.controller.scriptURL, normalizeURL(url1),
+ 'Document controller scriptURL should equal to the first one');
+ frame_sw.oncontrollerchange = t.step_func(oncontrollerchanged);
+ return navigator.serviceWorker.register(url2, {scope: scope});
+ })
+ .then(function(registration) {
+ sw_registration = registration;
+ t.add_cleanup(function() {
+ return registration.unregister();
+ });
+ return saw_controllerchanged;
+ })
+ .then(function() {
+ assert_not_equals(sw_registration.active, null,
+ 'Registration active worker should not be null');
+ return fetch_tests_from_worker(sw_registration.active);
+ });
+ }, 'Test skipWaiting while a client is using the registration');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-without-client.https.html b/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-without-client.https.html
new file mode 100644
index 0000000..62060a8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-without-client.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<title>Service Worker: Skip waiting without client</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+service_worker_test(
+ 'resources/skip-waiting-worker.js',
+ 'Test single skipWaiting() when no client attached');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-without-using-registration.https.html b/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-without-using-registration.https.html
new file mode 100644
index 0000000..ced64e5
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/skip-waiting-without-using-registration.https.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<title>Service Worker: Skip waiting without using registration</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html?skip-waiting-without-using-registration';
+ var url = 'resources/skip-waiting-worker.js';
+ var frame_sw, sw_registration;
+
+ return service_worker_unregister(t, scope)
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ t.add_cleanup(function() {
+ f.remove();
+ });
+ frame_sw = f.contentWindow.navigator.serviceWorker;
+ assert_equals(frame_sw.controller, null,
+ 'Document controller should be null');
+ return navigator.serviceWorker.register(url, {scope: scope});
+ })
+ .then(function(registration) {
+ sw_registration = registration;
+ t.add_cleanup(function() {
+ return registration.unregister();
+ });
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ assert_equals(frame_sw.controller, null,
+ 'Document controller should still be null');
+ assert_not_equals(sw_registration.active, null,
+ 'Registration active worker should not be null');
+ return fetch_tests_from_worker(sw_registration.active);
+ });
+ }, 'Test skipWaiting while a client is not being controlled');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/skip-waiting.https.html b/third_party/web_platform_tests/service-workers/service-worker/skip-waiting.https.html
new file mode 100644
index 0000000..f8392fc
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/skip-waiting.https.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<title>Service Worker: Skip waiting</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+promise_test(function(t) {
+ var scope = 'resources/blank.html?skip-waiting';
+ var url1 = 'resources/empty.js';
+ var url2 = 'resources/empty-worker.js';
+ var url3 = 'resources/skip-waiting-worker.js';
+ var sw_registration, activated_worker, waiting_worker;
+ return service_worker_unregister_and_register(t, url1, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ sw_registration = registration;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ t.add_cleanup(function() {
+ f.remove();
+ });
+ return navigator.serviceWorker.register(url2, {scope: scope});
+ })
+ .then(function(registration) {
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ activated_worker = sw_registration.active;
+ waiting_worker = sw_registration.waiting;
+ assert_equals(activated_worker.scriptURL, normalizeURL(url1),
+ 'Worker with url1 should be activated');
+ assert_equals(waiting_worker.scriptURL, normalizeURL(url2),
+ 'Worker with url2 should be waiting');
+ return navigator.serviceWorker.register(url3, {scope: scope});
+ })
+ .then(function(registration) {
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ assert_equals(activated_worker.state, 'redundant',
+ 'Worker with url1 should be redundant');
+ assert_equals(waiting_worker.state, 'redundant',
+ 'Worker with url2 should be redundant');
+ assert_equals(sw_registration.active.scriptURL, normalizeURL(url3),
+ 'Worker with url3 should be activated');
+ });
+ }, 'Test skipWaiting with both active and waiting workers');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/state.https.html b/third_party/web_platform_tests/service-workers/service-worker/state.https.html
new file mode 100644
index 0000000..7358e58
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/state.https.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+promise_test(function (t) {
+ var currentState = 'test-is-starting';
+ var scope = 'resources/state/';
+
+ return service_worker_unregister_and_register(
+ t, 'resources/empty-worker.js', scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ var sw = registration.installing;
+
+ assert_equals(sw.state, 'installing',
+ 'the service worker should be in "installing" state.');
+ checkStateTransition(sw.state);
+ return onStateChange(sw);
+ });
+
+ function checkStateTransition(newState) {
+ switch (currentState) {
+ case 'test-is-starting':
+ break; // anything goes
+ case 'installing':
+ assert_in_array(newState, ['installed', 'redundant']);
+ break;
+ case 'installed':
+ assert_in_array(newState, ['activating', 'redundant']);
+ break;
+ case 'activating':
+ assert_in_array(newState, ['activated', 'redundant']);
+ break;
+ case 'activated':
+ assert_equals(newState, 'redundant');
+ break;
+ case 'redundant':
+ assert_unreached('a ServiceWorker should not transition out of ' +
+ 'the "redundant" state');
+ break;
+ default:
+ assert_unreached('should not transition into unknown state "' +
+ newState + '"');
+ break;
+ }
+ currentState = newState;
+ }
+
+ function onStateChange(expectedTarget) {
+ return new Promise(function(resolve) {
+ expectedTarget.addEventListener('statechange', resolve);
+ }).then(function(event) {
+ assert_true(event.target instanceof ServiceWorker,
+ 'the target of the statechange event should be a ' +
+ 'ServiceWorker.');
+ assert_equals(event.target, expectedTarget,
+ 'the target of the statechange event should be ' +
+ 'the installing ServiceWorker');
+ assert_equals(event.type, 'statechange',
+ 'the type of the event should be "statechange".');
+
+ checkStateTransition(event.target.state);
+
+ if (event.target.state != 'activated')
+ return onStateChange(expectedTarget);
+ });
+ }
+}, 'Service Worker state property and "statechange" event');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/svg-target-reftest.https.html b/third_party/web_platform_tests/service-workers/service-worker/svg-target-reftest.https.html
new file mode 100644
index 0000000..3710ee6
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/svg-target-reftest.https.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<meta charset="utf-8">
+<title>Service worker interception does not break SVG fragment targets</title>
+<meta name="assert" content="SVG with link fragment should render correctly when intercepted by a service worker.">
+<script src="resources/test-helpers.sub.js"></script>
+<link rel="match" href="resources/svg-target-reftest-001.html">
+<p>Pass if you see a green box below.</p>
+<script>
+// We want to use utility functions designed for testharness.js where
+// there is a test object. We don't have a test object in reftests
+// so fake one for now.
+const fake_test = { step_func: f => f };
+
+async function runTest() {
+ const script = './resources/pass-through-worker.js';
+ const scope = './resources/svg-target-reftest-frame.html';
+ let reg = await navigator.serviceWorker.register(script, { scope });
+ await wait_for_state(fake_test, reg.installing, 'activated');
+ let f = await with_iframe(scope);
+ document.documentElement.classList.remove('reftest-wait');
+ await reg.unregister();
+ // Note, we cannot remove the frame explicitly because we can't
+ // tell when the reftest completes.
+}
+runTest();
+</script>
+</html>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/synced-state.https.html b/third_party/web_platform_tests/service-workers/service-worker/synced-state.https.html
new file mode 100644
index 0000000..0e9f63a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/synced-state.https.html
@@ -0,0 +1,93 @@
+<!doctype html>
+<title>ServiceWorker: worker objects have synced state</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// Tests that ServiceWorker objects representing the same Service Worker
+// entity have the same state. JS-level equality is now required according to
+// the spec.
+'use strict';
+
+function nextChange(worker) {
+ return new Promise(function(resolve, reject) {
+ worker.addEventListener('statechange', function handler(event) {
+ try {
+ worker.removeEventListener('statechange', handler);
+ resolve(event.currentTarget.state);
+ } catch (err) {
+ reject(err);
+ }
+ });
+ });
+}
+
+promise_test(function(t) {
+ var scope = 'resources/synced-state';
+ var script = 'resources/empty-worker.js';
+ var registration, worker;
+
+ return service_worker_unregister_and_register(t, script, scope)
+ .then(function(r) {
+ registration = r;
+ worker = registration.installing;
+
+ t.add_cleanup(function() {
+ return r.unregister();
+ });
+
+ return nextChange(worker);
+ })
+ .then(function(state) {
+ assert_equals(state, 'installed',
+ 'original SW should be installed');
+ assert_equals(registration.installing, null,
+ 'in installed, .installing should be null');
+ assert_equals(registration.waiting, worker,
+ 'in installed, .waiting should be equal to the ' +
+ 'original worker');
+ assert_equals(registration.waiting.state, 'installed',
+ 'in installed, .waiting should be installed');
+ assert_equals(registration.active, null,
+ 'in installed, .active should be null');
+
+ return nextChange(worker);
+ })
+ .then(function(state) {
+ assert_equals(state, 'activating',
+ 'original SW should be activating');
+ assert_equals(registration.installing, null,
+ 'in activating, .installing should be null');
+ assert_equals(registration.waiting, null,
+ 'in activating, .waiting should be null');
+ assert_equals(registration.active, worker,
+ 'in activating, .active should be equal to the ' +
+ 'original worker');
+ assert_equals(
+ registration.active.state, 'activating',
+ 'in activating, .active should be activating');
+
+ return nextChange(worker);
+ })
+ .then(function(state) {
+ assert_equals(state, 'activated',
+ 'original SW should be activated');
+ assert_equals(registration.installing, null,
+ 'in activated, .installing should be null');
+ assert_equals(registration.waiting, null,
+ 'in activated, .waiting should be null');
+ assert_equals(registration.active, worker,
+ 'in activated, .active should be equal to the ' +
+ 'original worker');
+ assert_equals(registration.active.state, 'activated',
+ 'in activated .active should be activated');
+ })
+ .then(function() {
+ return navigator.serviceWorker.getRegistration(scope);
+ })
+ .then(function(r) {
+ assert_equals(r, registration, 'getRegistration should return the ' +
+ 'same object');
+ });
+ }, 'worker objects for the same entity have the same state');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/uncontrolled-page.https.html b/third_party/web_platform_tests/service-workers/service-worker/uncontrolled-page.https.html
new file mode 100644
index 0000000..e22ca8f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/uncontrolled-page.https.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+function fetch_url(url) {
+ return new Promise(function(resolve, reject) {
+ var request = new XMLHttpRequest();
+ request.addEventListener('load', function(event) {
+ if (request.status == 200)
+ resolve(request.response);
+ else
+ reject(Error(request.statusText));
+ });
+ request.open('GET', url);
+ request.send();
+ });
+}
+var worker = 'resources/fail-on-fetch-worker.js';
+
+promise_test(function(t) {
+ var scope = 'resources/scope/uncontrolled-page/';
+ return service_worker_unregister_and_register(t, worker, scope)
+ .then(function(reg) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, reg.installing, 'activated');
+ })
+ .then(function() {
+ return fetch_url('resources/simple.txt');
+ })
+ .then(function(text) {
+ assert_equals(text, 'a simple text file\n');
+ });
+ }, 'Fetch events should not go through uncontrolled page.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/unregister-controller.https.html b/third_party/web_platform_tests/service-workers/service-worker/unregister-controller.https.html
new file mode 100644
index 0000000..3bf4cff
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/unregister-controller.https.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var worker_url = 'resources/simple-intercept-worker.js';
+
+async_test(function(t) {
+ var scope =
+ 'resources/unregister-controller-page.html?load-before-unregister';
+ var frame_window;
+ var controller;
+ var registration;
+ var frame;
+
+ service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ frame_window = frame.contentWindow;
+ controller = frame_window.navigator.serviceWorker.controller;
+ assert_true(controller instanceof frame_window.ServiceWorker,
+ 'document should load with a controller');
+ return registration.unregister();
+ })
+ .then(function() {
+ assert_equals(frame_window.navigator.serviceWorker.controller,
+ controller,
+ 'unregistration should not modify controller');
+ return frame_window.fetch_url('simple.txt');
+ })
+ .then(function(response) {
+ assert_equals(response, 'intercepted by service worker',
+ 'controller should intercept requests');
+ frame.remove();
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Unregister does not affect existing controller');
+
+async_test(function(t) {
+ var scope =
+ 'resources/unregister-controller-page.html?load-after-unregister';
+ var registration;
+ var frame;
+
+ service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ return registration.unregister();
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(f) {
+ frame = f;
+ var frame_window = frame.contentWindow;
+ assert_equals(frame_window.navigator.serviceWorker.controller, null,
+ 'document should not have a controller');
+ return frame_window.fetch_url('simple.txt');
+ })
+ .then(function(response) {
+ assert_equals(response, 'a simple text file\n',
+ 'requests should not be intercepted');
+ frame.remove();
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Unregister prevents control of subsequent navigations');
+
+async_test(function(t) {
+ var scope =
+ 'resources/scope/no-new-controllee-even-if-registration-is-still-used';
+ var registration;
+
+ service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ return registration.unregister();
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller,
+ null,
+ 'document should not have a controller');
+ frame.remove();
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Unregister prevents new controllee even if registration is still in use');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/unregister-immediately-before-installed.https.html b/third_party/web_platform_tests/service-workers/service-worker/unregister-immediately-before-installed.https.html
new file mode 100644
index 0000000..79cdaf0
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/unregister-immediately-before-installed.https.html
@@ -0,0 +1,57 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Use Clear-Site-Data to immediately unregister service workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/unregister-immediately-helpers.js"></script>
+<body>
+<script>
+'use strict';
+
+// These tests use the Clear-Site-Data network response header to immediately
+// unregister a service worker registration with a worker whose state is
+// 'installing' or 'parsed'. Clear-Site-Data must delete the registration,
+// abort the installation and then clear the registration by setting the
+// worker's state to 'redundant'.
+
+promise_test(async test => {
+ // This test keeps the the service worker in the 'parsed' state by using a
+ // script with an infinite loop.
+ const script_url = 'resources/onparse-infiniteloop-worker.js';
+ const scope_url =
+ 'resources/scope-for-unregister-immediately-with-parsed-worker';
+
+ await service_worker_unregister(test, /*scope=*/script_url);
+
+ // Clear-Site-Data must cause register() to fail.
+ const register_promise = promise_rejects_dom(test, 'AbortError',
+ navigator.serviceWorker.register(script_url, { scope: scope_url}));;
+
+ await Promise.all([clear_site_data(), register_promise]);
+
+ await assert_no_registrations_exist();
+ }, 'Clear-Site-Data must abort service worker registration.');
+
+promise_test(async test => {
+ // This test keeps the the service worker in the 'installing' state by using a
+ // script with an install event waitUntil() promise that never resolves.
+ const script_url = 'resources/oninstall-waituntil-forever.js';
+ const scope_url =
+ 'resources/scope-for-unregister-immediately-with-installing-worker';
+
+ const registration = await service_worker_unregister_and_register(
+ test, script_url, scope_url);
+ const service_worker = registration.installing;
+
+ // Clear-Site-Data must cause install to fail.
+ await Promise.all([
+ clear_site_data(),
+ wait_for_state(test, service_worker, 'redundant')]);
+
+ await assert_no_registrations_exist();
+ }, 'Clear-Site-Data must unregister a registration with a worker '
+ + 'in the "installing" state.');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/unregister-immediately-during-extendable-events.https.html b/third_party/web_platform_tests/service-workers/service-worker/unregister-immediately-during-extendable-events.https.html
new file mode 100644
index 0000000..6ba87a7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/unregister-immediately-during-extendable-events.https.html
@@ -0,0 +1,50 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Use Clear-Site-Data to immediately unregister service workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/unregister-immediately-helpers.js"></script>
+<body>
+<script>
+'use strict';
+
+// These tests use the Clear-Site-Data network response header to immediately
+// unregister a service worker registration with a worker that has pending
+// extendable events. Clear-Site-Data must delete the registration,
+// abort all pending extendable events and then clear the registration by
+// setting the worker's state to 'redundant'
+
+promise_test(async test => {
+ // Use a service worker script that can produce fetch events with pending
+ // respondWith() promises that never resolve.
+ const script_url = 'resources/onfetch-waituntil-forever.js';
+ const scope_url =
+ 'resources/blank.html?unregister-immediately-with-fetch-event';
+
+ const registration = await service_worker_unregister_and_register(
+ test, script_url, scope_url);
+
+ await wait_for_state(test, registration.installing, 'activated');
+
+ const frame = await add_controlled_iframe(test, scope_url);
+
+ // Clear-Site-Data must cause the pending fetch promise to reject.
+ const fetch_promise = promise_rejects_js(
+ test, TypeError, frame.contentWindow.fetch('waituntil-forever'));
+
+ const event_watcher = new EventWatcher(
+ test, frame.contentWindow.navigator.serviceWorker, 'controllerchange');
+
+ await Promise.all([
+ clear_site_data(),
+ fetch_promise,
+ event_watcher.wait_for('controllerchange'),
+ wait_for_state(test, registration.active, 'redundant'),]);
+
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller, null);
+ await assert_no_registrations_exist();
+}, 'Clear-Site-Data must fail pending subresource fetch events.');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/unregister-immediately.https.html b/third_party/web_platform_tests/service-workers/service-worker/unregister-immediately.https.html
new file mode 100644
index 0000000..54be40a
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/unregister-immediately.https.html
@@ -0,0 +1,134 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Use Clear-Site-Data to immediately unregister service workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="resources/unregister-immediately-helpers.js"></script>
+<body>
+<script>
+'use strict';
+
+// These tests use the Clear-Site-Data network response header to immediately
+// unregister a service worker registration with a worker whose state is
+// 'installed', 'waiting', 'activating' or 'activated'. Immediately
+// unregistering runs the "Clear Registration" algorithm without waiting for the
+// active worker's controlled clients to unload.
+
+promise_test(async test => {
+ // This test keeps the the service worker in the 'activating' state by using a
+ // script with an activate event waitUntil() promise that never resolves.
+ const script_url = 'resources/onactivate-waituntil-forever.js';
+ const scope_url =
+ 'resources/scope-for-unregister-immediately-with-waiting-worker';
+
+ const registration = await service_worker_unregister_and_register(
+ test, script_url, scope_url);
+ const service_worker = registration.installing;
+
+ await wait_for_state(test, service_worker, 'activating');
+
+ // Clear-Site-Data must cause activation to fail.
+ await Promise.all([
+ clear_site_data(),
+ wait_for_state(test, service_worker, 'redundant')]);
+
+ await assert_no_registrations_exist();
+ }, 'Clear-Site-Data must unregister a registration with a worker '
+ + 'in the "activating" state.');
+
+promise_test(async test => {
+ // Create an registration with two service workers: one activated and one
+ // installed.
+ const script_url = 'resources/update_shell.py';
+ const scope_url =
+ 'resources/scope-for-unregister-immediately-with-with-update';
+
+ const registration = await service_worker_unregister_and_register(
+ test, script_url, scope_url);
+ const first_service_worker = registration.installing;
+
+ await wait_for_state(test, first_service_worker, 'activated');
+ registration.update();
+
+ const event_watcher = new EventWatcher(test, registration, 'updatefound');
+ await event_watcher.wait_for('updatefound');
+
+ const second_service_worker = registration.installing;
+ await wait_for_state(test, second_service_worker, 'installed');
+
+ // Clear-Site-Data must clear both workers from the registration.
+ await Promise.all([
+ clear_site_data(),
+ wait_for_state(test, first_service_worker, 'redundant'),
+ wait_for_state(test, second_service_worker, 'redundant')]);
+
+ await assert_no_registrations_exist();
+}, 'Clear-Site-Data must unregister an activated registration with '
+ + 'an update waiting.');
+
+promise_test(async test => {
+ const script_url = 'resources/empty.js';
+ const scope_url =
+ 'resources/blank.html?unregister-immediately-with-controlled-client';
+
+ const registration = await service_worker_unregister_and_register(
+ test, script_url, scope_url);
+ const service_worker = registration.installing;
+
+ await wait_for_state(test, service_worker, 'activated');
+ const frame = await add_controlled_iframe(test, scope_url);
+ const frame_registration =
+ await frame.contentWindow.navigator.serviceWorker.ready;
+
+ const event_watcher = new EventWatcher(
+ test, frame.contentWindow.navigator.serviceWorker, 'controllerchange');
+
+ // Clear-Site-Data must remove the iframe's controller.
+ await Promise.all([
+ clear_site_data(),
+ event_watcher.wait_for('controllerchange'),
+ wait_for_state(test, service_worker, 'redundant')]);
+
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller, null);
+ await assert_no_registrations_exist();
+
+ // The ready promise must continue to resolve with the unregistered
+ // registration.
+ assert_equals(frame_registration,
+ await frame.contentWindow.navigator.serviceWorker.ready);
+}, 'Clear-Site-Data must unregister an activated registration with controlled '
+ + 'clients.');
+
+promise_test(async test => {
+ const script_url = 'resources/empty.js';
+ const scope_url =
+ 'resources/blank.html?unregister-immediately-while-waiting-to-clear';
+
+ const registration = await service_worker_unregister_and_register(
+ test, script_url, scope_url);
+ const service_worker = registration.installing;
+
+ await wait_for_state(test, service_worker, 'activated');
+ const frame = await add_controlled_iframe(test, scope_url);
+
+ const event_watcher = new EventWatcher(
+ test, frame.contentWindow.navigator.serviceWorker, 'controllerchange');
+
+ // Unregister waits to clear the registration until no controlled clients
+ // exist.
+ await registration.unregister();
+
+ // Clear-Site-Data must clear the unregistered registration immediately.
+ await Promise.all([
+ clear_site_data(),
+ event_watcher.wait_for('controllerchange'),
+ wait_for_state(test, service_worker, 'redundant')]);
+
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller, null);
+ await assert_no_registrations_exist();
+}, 'Clear-Site-Data must clear an unregistered registration waiting for '
+ + ' controlled clients to unload.');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/unregister-then-register-new-script.https.html b/third_party/web_platform_tests/service-workers/service-worker/unregister-then-register-new-script.https.html
new file mode 100644
index 0000000..d046423
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/unregister-then-register-new-script.https.html
@@ -0,0 +1,136 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var worker_url = 'resources/empty-worker.js';
+
+promise_test(async function(t) {
+ const scope = 'resources/scope/unregister-then-register-new-script-that-exists';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ const newWorkerURL = worker_url + '?new';
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const iframe = await with_iframe(scope);
+ t.add_cleanup(() => iframe.remove());
+
+ await registration.unregister();
+
+ const newRegistration = await navigator.serviceWorker.register(newWorkerURL, { scope });
+ t.add_cleanup(() => newRegistration.unregister());
+
+ assert_equals(
+ registration.installing,
+ null,
+ 'before activated registration.installing'
+ );
+ assert_equals(
+ registration.waiting,
+ null,
+ 'before activated registration.waiting'
+ );
+ assert_equals(
+ registration.active.scriptURL,
+ normalizeURL(worker_url),
+ 'before activated registration.active'
+ );
+ assert_equals(
+ newRegistration.installing.scriptURL,
+ normalizeURL(newWorkerURL),
+ 'before activated newRegistration.installing'
+ );
+ assert_equals(
+ newRegistration.waiting,
+ null,
+ 'before activated newRegistration.waiting'
+ );
+ assert_equals(
+ newRegistration.active,
+ null,
+ 'before activated newRegistration.active'
+ );
+ iframe.remove();
+
+ await wait_for_state(t, newRegistration.installing, 'activated');
+
+ assert_equals(
+ newRegistration.installing,
+ null,
+ 'after activated newRegistration.installing'
+ );
+ assert_equals(
+ newRegistration.waiting,
+ null,
+ 'after activated newRegistration.waiting'
+ );
+ assert_equals(
+ newRegistration.active.scriptURL,
+ normalizeURL(newWorkerURL),
+ 'after activated newRegistration.active'
+ );
+
+ const newIframe = await with_iframe(scope);
+ t.add_cleanup(() => newIframe.remove());
+
+ assert_equals(
+ newIframe.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ normalizeURL(newWorkerURL),
+ 'the new worker should control a new document'
+ );
+}, 'Registering a new script URL while an unregistered registration is in use');
+
+promise_test(async function(t) {
+ const scope = 'resources/scope/unregister-then-register-new-script-that-404s';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const iframe = await with_iframe(scope);
+ t.add_cleanup(() => iframe.remove());
+
+ await registration.unregister();
+
+ await promise_rejects_js(
+ t, TypeError,
+ navigator.serviceWorker.register('this-will-404', { scope })
+ );
+
+ assert_equals(registration.installing, null, 'registration.installing');
+ assert_equals(registration.waiting, null, 'registration.waiting');
+ assert_equals(registration.active.scriptURL, normalizeURL(worker_url), 'registration.active');
+
+ const newIframe = await with_iframe(scope);
+ t.add_cleanup(() => newIframe.remove());
+
+ assert_equals(newIframe.contentWindow.navigator.serviceWorker.controller, null, 'Document should not be controlled');
+}, 'Registering a new script URL that 404s does not resurrect unregistered registration');
+
+promise_test(async function(t) {
+ const scope = 'resources/scope/unregister-then-register-reject-install-worker';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const iframe = await with_iframe(scope);
+ t.add_cleanup(() => iframe.remove());
+
+ await registration.unregister();
+
+ const newRegistration = await navigator.serviceWorker.register(
+ 'resources/reject-install-worker.js', { scope }
+ );
+ t.add_cleanup(() => newRegistration.unregister());
+
+ await wait_for_state(t, newRegistration.installing, 'redundant');
+
+ assert_equals(registration.installing, null, 'registration.installing');
+ assert_equals(registration.waiting, null, 'registration.waiting');
+ assert_equals(registration.active.scriptURL, normalizeURL(worker_url),
+ 'registration.active');
+ assert_not_equals(registration, newRegistration, 'New registration is different');
+}, 'Registering a new script URL that fails to install does not resurrect unregistered registration');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/unregister-then-register.https.html b/third_party/web_platform_tests/service-workers/service-worker/unregister-then-register.https.html
new file mode 100644
index 0000000..b61608c
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/unregister-then-register.https.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+var worker_url = 'resources/empty-worker.js';
+
+promise_test(async function(t) {
+ const scope = 'resources/scope/re-register-resolves-to-new-value';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activated');
+ await registration.unregister();
+ const newRegistration = await navigator.serviceWorker.register(worker_url, { scope });
+ t.add_cleanup(() => newRegistration.unregister());
+
+ assert_not_equals(
+ registration, newRegistration,
+ 'register should resolve to a new value'
+ );
+ }, 'Unregister then register resolves to a new value');
+
+promise_test(async function(t) {
+ const scope = 'resources/scope/re-register-while-old-registration-in-use';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activated');
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+
+ await registration.unregister();
+ const newRegistration = await navigator.serviceWorker.register(worker_url, { scope });
+ t.add_cleanup(() => newRegistration.unregister());
+
+ assert_not_equals(
+ registration, newRegistration,
+ 'Unregister and register should always create a new registration'
+ );
+}, 'Unregister then register does not resolve to the original value even if the registration is in use.');
+
+promise_test(function(t) {
+ var scope = 'resources/scope/re-register-does-not-affect-existing-controllee';
+ var iframe;
+ var registration;
+ var controller;
+
+ return service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ iframe = frame;
+ controller = iframe.contentWindow.navigator.serviceWorker.controller;
+ return registration.unregister();
+ })
+ .then(function() {
+ return navigator.serviceWorker.register(worker_url, { scope: scope });
+ })
+ .then(function(newRegistration) {
+ assert_equals(registration.installing, null,
+ 'installing version is null');
+ assert_equals(registration.waiting, null, 'waiting version is null');
+ assert_equals(
+ iframe.contentWindow.navigator.serviceWorker.controller,
+ controller,
+ 'the worker from the first registration is the controller');
+ iframe.remove();
+ });
+ }, 'Unregister then register does not affect existing controllee');
+
+promise_test(async function(t) {
+ const scope = 'resources/scope/resurrection';
+ const altWorkerURL = worker_url + '?alt';
+ const registration = await service_worker_unregister_and_register(t, worker_url, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ await wait_for_state(t, registration.installing, 'activating');
+ const iframe = await with_iframe(scope);
+ t.add_cleanup(() => iframe.remove());
+
+ await registration.unregister();
+ const newRegistration = await navigator.serviceWorker.register(altWorkerURL, { scope });
+ t.add_cleanup(() => newRegistration.unregister());
+
+ assert_equals(newRegistration.active, null, 'Registration is new');
+
+ await wait_for_state(t, newRegistration.installing, 'activating');
+
+ const newIframe = await with_iframe(scope);
+ t.add_cleanup(() => newIframe.remove());
+
+ const iframeController = iframe.contentWindow.navigator.serviceWorker.controller;
+ const newIframeController = newIframe.contentWindow.navigator.serviceWorker.controller;
+
+ assert_not_equals(iframeController, newIframeController, 'iframes have different controllers');
+}, 'Unregister then register does not resurrect the registration');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/unregister.https.html b/third_party/web_platform_tests/service-workers/service-worker/unregister.https.html
new file mode 100644
index 0000000..492aecb
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/unregister.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+async_test(function(t) {
+ var scope = 'resources/scope/unregister-twice';
+ var registration;
+ navigator.serviceWorker.register('resources/empty-worker.js',
+ {scope: scope})
+ .then(function(r) {
+ registration = r;
+ return registration.unregister();
+ })
+ .then(function() {
+ return registration.unregister();
+ })
+ .then(function(value) {
+ assert_equals(value, false,
+ 'unregistering twice should resolve with false');
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Unregister twice');
+
+async_test(function(t) {
+ var scope = 'resources/scope/successful-unregister/';
+ navigator.serviceWorker.register('resources/empty-worker.js',
+ {scope: scope})
+ .then(function(registration) {
+ return registration.unregister();
+ })
+ .then(function(value) {
+ assert_equals(value, true,
+ 'unregistration should resolve with true');
+ t.done();
+ })
+ .catch(unreached_rejection(t));
+ }, 'Register then unregister');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-after-navigation-fetch-event.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-after-navigation-fetch-event.https.html
new file mode 100644
index 0000000..ff51f7f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-after-navigation-fetch-event.https.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<meta name=timeout content=long>
+<title>Service Worker: Update should be triggered after a navigation</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+async function cleanup(frame, registration) {
+ if (frame)
+ frame.remove();
+ if (registration)
+ await registration.unregister();
+}
+
+promise_test(async t => {
+ const script = 'resources/update_shell.py?filename=empty.js';
+ const scope = 'resources/scope/update';
+ let registration;
+ let frame;
+
+ async function run() {
+ registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Navigation should trigger update.
+ frame = await with_iframe(scope);
+ await wait_for_update(t, registration);
+ }
+
+ try {
+ await run();
+ } finally {
+ await cleanup(frame, registration);
+ }
+}, 'Update should be triggered after a navigation (no fetch event worker).');
+
+promise_test(async t => {
+ const script = 'resources/update_shell.py?filename=simple-intercept-worker.js';
+ const scope = 'resources/scope/update';
+ let registration;
+ let frame;
+
+ async function run() {
+ registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Navigation should trigger update (network fallback).
+ frame = await with_iframe(scope + '?ignore');
+ await wait_for_update(t, registration);
+
+ // Navigation should trigger update (respondWith called).
+ frame.src = scope + '?string';
+ await wait_for_update(t, registration);
+ }
+
+ try {
+ await run();
+ } finally {
+ await cleanup(frame, registration);
+ }
+}, 'Update should be triggered after a navigation (fetch event worker).');
+
+promise_test(async t => {
+ const script = 'resources/update_shell.py?filename=empty.js';
+ const scope = 'resources/';
+ let registration;
+ let frame;
+
+ async function run() {
+ registration = await service_worker_unregister_and_register(
+ t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Navigation should trigger update. Don't use with_iframe as it waits for
+ // the onload event.
+ frame = document.createElement('iframe');
+ frame.src = 'resources/malformed-http-response.asis';
+ document.body.appendChild(frame);
+ await wait_for_update(t, registration);
+ }
+
+ try {
+ await run();
+ } finally {
+ await cleanup(frame, registration);
+ }
+}, 'Update should be triggered after a navigation (network error).');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-after-navigation-redirect.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-after-navigation-redirect.https.html
new file mode 100644
index 0000000..6e821fe
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-after-navigation-redirect.https.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: Update should be triggered after redirects during navigation</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(async t => {
+ // This test does a navigation that goes through a redirect chain. Each
+ // request in the chain has a service worker. Each service worker has no
+ // fetch event handler. The redirects are performed by redirect.py.
+ const script = 'resources/update-nocookie-worker.py';
+ const scope1 = 'resources/redirect.py?scope1';
+ const scope2 = 'resources/redirect.py?scope2';
+ const scope3 = 'resources/empty.html';
+ let registration1;
+ let registration2;
+ let registration3;
+ let frame;
+
+ async function cleanup() {
+ if (frame)
+ frame.remove();
+ if (registration1)
+ return registration1.unregister();
+ if (registration2)
+ return registration2.unregister();
+ if (registration3)
+ return registration3.unregister();
+ }
+
+ async function make_active_registration(scope) {
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+ return registration;
+ }
+
+ async function run() {
+ // Make the registrations.
+ registration1 = await make_active_registration(scope1);
+ registration2 = await make_active_registration(scope2);
+ registration3 = await make_active_registration(scope3);
+
+ // Make the promises that resolve on update.
+ const saw_update1 = wait_for_update(t, registration1);
+ const saw_update2 = wait_for_update(t, registration2);
+ const saw_update3 = wait_for_update(t, registration3);
+
+ // Create a URL for the redirect chain: scope1 -> scope2 -> scope3.
+ // Build the URL in reverse order.
+ let url = `${base_path()}${scope3}`;
+ url = `${base_path()}${scope2}&Redirect=${encodeURIComponent(url)}`
+ url = `${base_path()}${scope1}&Redirect=${encodeURIComponent(url)}`
+
+ // Navigate to the URL.
+ frame = await with_iframe(url);
+
+ // Each registration should update.
+ await saw_update1;
+ await saw_update2;
+ await saw_update3;
+ }
+
+ try {
+ await run();
+ } finally {
+ await cleanup();
+ }
+}, 'service workers are updated on redirects during navigation');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-after-oneday.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-after-oneday.https.html
new file mode 100644
index 0000000..e7a8aa4
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-after-oneday.https.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!-- This test requires browser to treat all registrations are older than 24 hours.
+ Preference 'dom.serviceWorkers.testUpdateOverOneDay' should be enabled during
+ the execution of the test -->
+<title>Service Worker: Functional events should trigger update if last update time is over 24 hours</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+
+promise_test(function(t) {
+ var script = 'resources/update-nocookie-worker.py';
+ var scope = 'resources/update/update-after-oneday.https.html';
+ var expected_url = normalizeURL(script);
+ var registration;
+ var frame;
+
+ return service_worker_unregister_and_register(t, expected_url, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(scope); })
+ .then(function(f) {
+ frame = f;
+ return wait_for_update(t, registration);
+ })
+ .then(function() {
+ assert_equals(registration.installing.scriptURL, expected_url,
+ 'new installing should be set after update resolves.');
+ assert_equals(registration.waiting, null,
+ 'waiting should still be null after update resolves.');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'active should still exist after update found.');
+ return wait_for_state(t, registration.installing, 'installed');
+ })
+ .then(function() {
+ // Trigger a non-navigation fetch event
+ frame.contentWindow.load_image(normalizeURL('resources/update/sample'));
+ return wait_for_update(t, registration);
+ })
+ .then(function() {
+ frame.remove();
+ })
+ }, 'Update should be triggered after a functional event when last update time is over 24 hours');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-bytecheck-cors-import.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-bytecheck-cors-import.https.html
new file mode 100644
index 0000000..121a737
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-bytecheck-cors-import.https.html
@@ -0,0 +1,92 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+// Tests of updating a service worker. This file contains cors cases only.
+
+/*
+ * @param string main
+ * Decide the content of the main script, where 'default' is for constant
+ * content while 'time' is for time-variant content.
+ * @param string imported
+ * Decide the content of the imported script, where 'default' is for constant
+ * content while 'time' is for time-variant content.
+ */
+const settings = [{main: 'default', imported: 'default'},
+ {main: 'default', imported: 'time' },
+ {main: 'time', imported: 'default'},
+ {main: 'time', imported: 'time' }];
+
+const host_info = get_host_info();
+settings.forEach(({main, imported}) => {
+ promise_test(async (t) => {
+ // Specify a cross origin path to load imported scripts from a cross origin.
+ const path = host_info.HTTPS_REMOTE_ORIGIN +
+ '/service-workers/service-worker/resources/';
+ const script = 'resources/bytecheck-worker.py' +
+ '?main=' + main +
+ '&imported=' + imported +
+ '&path=' + path +
+ '&type=classic';
+ const scope = 'resources/blank.html';
+
+ // Register a service worker.
+ const swr = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => swr.unregister());
+ const sw = await wait_for_update(t, swr);
+ await wait_for_state(t, sw, 'activated');
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+
+ // Update the service worker registration.
+ await swr.update();
+
+ // If there should be a new service worker.
+ if (main === 'time' || imported === 'time') {
+ return wait_for_update(t, swr);
+ }
+ // Otherwise, make sure there is no newly created service worker.
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+ }, `Test(main: ${main}, imported: ${imported})`);
+});
+
+settings.forEach(({main, imported}) => {
+ promise_test(async (t) => {
+ // Specify a cross origin path to load imported scripts from a cross origin.
+ const path = host_info.HTTPS_REMOTE_ORIGIN +
+ '/service-workers/service-worker/resources/';
+ const script = 'resources/bytecheck-worker.py' +
+ '?main=' + main +
+ '&imported=' + imported +
+ '&path=' + path +
+ '&type=module';
+ const scope = 'resources/blank.html';
+
+ // Register a service worker.
+ const swr = await service_worker_unregister_and_register(t, script, scope, {type: 'module'});
+ t.add_cleanup(() => swr.unregister());
+ const sw = await wait_for_update(t, swr);
+ await wait_for_state(t, sw, 'activated');
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+
+ // Update the service worker registration.
+ await swr.update();
+
+ // If there should be a new service worker.
+ if (main === 'time' || imported === 'time') {
+ return wait_for_update(t, swr);
+ }
+ // Otherwise, make sure there is no newly created service worker.
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+ }, `Test module script(main: ${main}, imported: ${imported})`);
+});
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-bytecheck.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-bytecheck.https.html
new file mode 100644
index 0000000..3e5a28b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-bytecheck.https.html
@@ -0,0 +1,92 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+// Tests of updating a service worker. This file contains non-cors cases only.
+
+/*
+ * @param string main
+ * Decide the content of the main script, where 'default' is for constant
+ * content while 'time' is for time-variant content.
+ * @param string imported
+ * Decide the content of the imported script, where 'default' is for constant
+ * content while 'time' is for time-variant content.
+ */
+const settings = [{main: 'default', imported: 'default'},
+ {main: 'default', imported: 'time' },
+ {main: 'time', imported: 'default'},
+ {main: 'time', imported: 'time' }];
+
+const host_info = get_host_info();
+settings.forEach(({main, imported}) => {
+ promise_test(async (t) => {
+ // Empty path results in the same origin imported scripts.
+ const path = '';
+ const script = 'resources/bytecheck-worker.py' +
+ '?main=' + main +
+ '&imported=' + imported +
+ '&path=' + path +
+ '&type=classic';
+ const scope = 'resources/blank.html';
+
+ // Register a service worker.
+ const swr = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => swr.unregister());
+ const sw = await wait_for_update(t, swr);
+ await wait_for_state(t, sw, 'activated');
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+
+ // Update the service worker registration.
+ await swr.update();
+
+ // If there should be a new service worker.
+ if (main === 'time' || imported === 'time') {
+ return wait_for_update(t, swr);
+ }
+ // Otherwise, make sure there is no newly created service worker.
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+ }, `Test(main: ${main}, imported: ${imported})`);
+});
+
+settings.forEach(({main, imported}) => {
+ promise_test(async (t) => {
+ // Empty path results in the same origin imported scripts.
+ const path = './';
+ const script = 'resources/bytecheck-worker.py' +
+ '?main=' + main +
+ '&imported=' + imported +
+ '&path=' + path +
+ '&type=module';
+ const scope = 'resources/blank.html';
+
+ // Register a module service worker.
+ const swr = await service_worker_unregister_and_register(t, script, scope,
+ {type: 'module'});
+
+ t.add_cleanup(() => swr.unregister());
+ const sw = await wait_for_update(t, swr);
+ await wait_for_state(t, sw, 'activated');
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+
+ // Update the service worker registration.
+ await swr.update();
+
+ // If there should be a new service worker.
+ if (main === 'time' || imported === 'time') {
+ return wait_for_update(t, swr);
+ }
+ // Otherwise, make sure there is no newly created service worker.
+ assert_array_equals([swr.active, swr.waiting, swr.installing],
+ [sw, null, null]);
+ }, `Test module script(main: ${main}, imported: ${imported})`);
+});
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-import-scripts.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-import-scripts.https.html
new file mode 100644
index 0000000..a2df529
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-import-scripts.https.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for importScripts: import scripts ignored error</title>
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// This file contains tests to check if imported scripts appropriately updated.
+
+const SCOPE = 'resources/simple.txt';
+
+// Create a service worker (update-worker-from-file.py), which is initially
+// |initial_worker| and |updated_worker| later.
+async function prepare_ready_update_worker_from_file(
+ t, initial_worker, updated_worker) {
+ const key = token();
+ const worker_url = `resources/update-worker-from-file.py?` +
+ `First=${initial_worker}&Second=${updated_worker}&Key=${key}`;
+ const expected_url = normalizeURL(worker_url);
+
+ const registration = await service_worker_unregister_and_register(
+ t, worker_url, SCOPE);
+ await wait_for_state(t, registration.installing, 'activated');
+ assert_equals(registration.installing, null,
+ 'prepare_ready: installing');
+ assert_equals(registration.waiting, null,
+ 'prepare_ready: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'prepare_ready: active');
+ return [registration, expected_url];
+}
+
+// Create a service worker using the script under resources/.
+async function prepare_ready_normal_worker(t, filename, additional_params='') {
+ const key = token();
+ const worker_url = `resources/${filename}?Key=${key}&${additional_params}`;
+ const expected_url = normalizeURL(worker_url);
+
+ const registration = await service_worker_unregister_and_register(
+ t, worker_url, SCOPE);
+ await wait_for_state(t, registration.installing, 'activated');
+ assert_equals(registration.installing, null,
+ 'prepare_ready: installing');
+ assert_equals(registration.waiting, null,
+ 'prepare_ready: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'prepare_ready: active');
+ return [registration, expected_url];
+}
+
+function assert_installing_and_active(registration, expected_url) {
+ assert_equals(registration.installing.scriptURL, expected_url,
+ 'assert_installing_and_active: installing');
+ assert_equals(registration.waiting, null,
+ 'assert_installing_and_active: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'assert_installing_and_active: active');
+}
+
+function assert_waiting_and_active(registration, expected_url) {
+ assert_equals(registration.installing, null,
+ 'assert_waiting_and_active: installing');
+ assert_equals(registration.waiting.scriptURL, expected_url,
+ 'assert_waiting_and_active: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'assert_waiting_and_active: active');
+}
+
+function assert_active_only(registration, expected_url) {
+ assert_equals(registration.installing, null,
+ 'assert_active_only: installing');
+ assert_equals(registration.waiting, null,
+ 'assert_active_only: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'assert_active_only: active');
+}
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_update_worker_from_file(
+ t, 'empty.js', 'import-scripts-404.js');
+ t.add_cleanup(() => registration.unregister());
+
+ await promise_rejects_js(t, TypeError, registration.update());
+ assert_active_only(registration, expected_url);
+}, 'update() should fail when a new worker imports an unavailable script.');
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_update_worker_from_file(
+ t, 'import-scripts-404-after-update.js', 'empty.js');
+ t.add_cleanup(() => registration.unregister());
+
+ await Promise.all([registration.update(), wait_for_update(t, registration)]);
+ assert_installing_and_active(registration, expected_url);
+
+ await wait_for_state(t, registration.installing, 'installed');
+ assert_waiting_and_active(registration, expected_url);
+
+ await wait_for_state(t, registration.waiting, 'activated');
+ assert_active_only(registration, expected_url);
+}, 'update() should succeed when the old imported script no longer exist but ' +
+ "the new worker doesn't import it.");
+
+promise_test(async t => {
+ const [registration, expected_url] = await prepare_ready_normal_worker(
+ t, 'import-scripts-404-after-update.js');
+ t.add_cleanup(() => registration.unregister());
+
+ await registration.update();
+ assert_active_only(registration, expected_url);
+}, 'update() should treat 404 on imported scripts as no change.');
+
+promise_test(async t => {
+ const [registration, expected_url] = await prepare_ready_normal_worker(
+ t, 'import-scripts-404-after-update-plus-update-worker.js',
+ `AdditionalKey=${token()}`);
+ t.add_cleanup(() => registration.unregister());
+
+ await promise_rejects_js(t, TypeError, registration.update());
+ assert_active_only(registration, expected_url);
+}, 'update() should find an update in an imported script but update() should ' +
+ 'result in failure due to missing the other imported script.');
+
+promise_test(async t => {
+ const [registration, expected_url] = await prepare_ready_normal_worker(
+ t, 'import-scripts-cross-origin-worker.sub.js');
+ t.add_cleanup(() => registration.unregister());
+ await registration.update();
+ assert_installing_and_active(registration, expected_url);
+}, 'update() should work with cross-origin importScripts.');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-missing-import-scripts.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-missing-import-scripts.https.html
new file mode 100644
index 0000000..66e8bfa
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-missing-import-scripts.https.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<title>Service Worker: update with missing importScripts</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/utils.js"></script>
+<body>
+<script>
+/**
+ * Test ServiceWorkerRegistration.update() when importScripts in a service worker
+ * script is no longer available (but was initially).
+ */
+let registration = null;
+
+promise_test(async (test) => {
+ const script = `resources/update-missing-import-scripts-main-worker.py?key=${token()}`;
+ const scope = 'resources/update-missing-import-scripts';
+
+ registration = await service_worker_unregister_and_register(test, script, scope);
+
+ add_completion_callback(() => { registration.unregister(); });
+
+ await wait_for_state(test, registration.installing, 'activated');
+}, 'Initialize global state');
+
+promise_test(test => {
+ return new Promise(resolve => {
+ registration.addEventListener('updatefound', resolve);
+ registration.update();
+ });
+}, 'Update service worker with new script that\'s missing importScripts()');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-module-request-mode.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-module-request-mode.https.html
new file mode 100644
index 0000000..b3875d2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-module-request-mode.https.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<title>Test that mode is set to same-origin for a main module</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// Tests a main module service worker script fetch during an update check.
+// The fetch should have the mode set to 'same-origin'.
+//
+// The test works by registering a main module service worker. It then does an
+// update. The test server responds with an updated worker script that remembers
+// the http request. The updated worker reports back this request to the test
+// page.
+promise_test(async (t) => {
+ const script = "resources/test-request-mode-worker.py";
+ const scope = "resources/";
+
+ // Register the service worker.
+ await service_worker_unregister(t, scope);
+ const registration = await navigator.serviceWorker.register(
+ script, {scope, type: 'module'});
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Do an update.
+ await registration.update();
+
+ // Ask the new worker what the request was.
+ const newWorker = registration.installing;
+ const sawMessage = new Promise((resolve) => {
+ navigator.serviceWorker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ });
+ newWorker.postMessage('getHeaders');
+ const result = await sawMessage;
+
+ // Test the result.
+ assert_equals(result['sec-fetch-mode'], 'same-origin');
+ assert_equals(result['origin'], undefined);
+
+}, 'headers of a main module script');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-no-cache-request-headers.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-no-cache-request-headers.https.html
new file mode 100644
index 0000000..6ebad4b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-no-cache-request-headers.https.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that cache is being bypassed/validated in no-cache mode on update</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// Tests a service worker script fetch during an update check which
+// bypasses/validates the browser cache. The fetch should have the
+// 'if-none-match' request header.
+//
+// This tests the Update step:
+// "Set request’s cache mode to "no-cache" if any of the following are true..."
+// https://w3c.github.io/ServiceWorker/#update-algorithm
+//
+// The test works by registering a service worker with |updateViaCache|
+// set to "none". It then does an update. The test server responds with
+// an updated worker script that remembers the http request headers.
+// The updated worker reports back these headers to the test page.
+promise_test(async (t) => {
+ const script = "resources/test-request-headers-worker.py";
+ const scope = "resources/";
+
+ // Register the service worker.
+ await service_worker_unregister(t, scope);
+ const registration = await navigator.serviceWorker.register(
+ script, {scope, updateViaCache: 'none'});
+ await wait_for_state(t, registration.installing, 'activated');
+
+ // Do an update.
+ await registration.update();
+
+ // Ask the new worker what the request headers were.
+ const newWorker = registration.installing;
+ const sawMessage = new Promise((resolve) => {
+ navigator.serviceWorker.onmessage = (event) => {
+ resolve(event.data);
+ };
+ });
+ newWorker.postMessage('getHeaders');
+ const result = await sawMessage;
+
+ // Test the result.
+ assert_equals(result['service-worker'], 'script');
+ assert_equals(result['if-none-match'], 'etag');
+}, 'headers in no-cache mode');
+
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-not-allowed.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-not-allowed.https.html
new file mode 100644
index 0000000..0a54aa9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-not-allowed.https.html
@@ -0,0 +1,140 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+function send_message_to_worker_and_wait_for_response(worker, message) {
+ return new Promise(resolve => {
+ // Use a dedicated channel for every request to avoid race conditions on
+ // concurrent requests.
+ const channel = new MessageChannel();
+ worker.postMessage(channel.port1, [channel.port1]);
+
+ let messageReceived = false;
+ channel.port2.onmessage = event => {
+ assert_false(messageReceived, 'Already received response for ' + message);
+ messageReceived = true;
+ resolve(event.data);
+ };
+ channel.port2.postMessage(message);
+ });
+}
+
+async function ensure_install_event_fired(worker) {
+ const response = await send_message_to_worker_and_wait_for_response(worker, 'awaitInstallEvent');
+ assert_equals('installEventFired', response);
+ assert_equals('installing', worker.state, 'Expected worker to be installing.');
+}
+
+async function finish_install(worker) {
+ await ensure_install_event_fired(worker);
+ const response = await send_message_to_worker_and_wait_for_response(worker, 'finishInstall');
+ assert_equals('installFinished', response);
+}
+
+async function activate_service_worker(t, worker) {
+ await finish_install(worker);
+ // By waiting for both states at the same time, the test fails
+ // quickly if the installation fails, avoiding a timeout.
+ await Promise.race([wait_for_state(t, worker, 'activated'),
+ wait_for_state(t, worker, 'redundant')]);
+ assert_equals('activated', worker.state, 'Service worker should be activated.');
+}
+
+async function update_within_service_worker(worker) {
+ // This function returns a Promise that resolves when update()
+ // has been called but is not necessarily finished yet.
+ // Call finish() on the returned object to wait for update() settle.
+ const port = await send_message_to_worker_and_wait_for_response(worker, 'callUpdate');
+ let messageReceived = false;
+ return {
+ finish: () => {
+ return new Promise(resolve => {
+ port.onmessage = event => {
+ assert_false(messageReceived, 'Update already finished.');
+ messageReceived = true;
+ resolve(event.data);
+ };
+ });
+ },
+ };
+}
+
+async function update_from_client_and_await_installing_version(test, registration) {
+ const updatefound = wait_for_update(test, registration);
+ registration.update();
+ await updatefound;
+ return registration.installing;
+}
+
+async function spin_up_service_worker(test) {
+ const script = 'resources/update-during-installation-worker.py';
+ const scope = 'resources/blank.html';
+
+ const registration = await service_worker_unregister_and_register(test, script, scope);
+ test.add_cleanup(async () => {
+ if (registration.installing) {
+ // If there is an installing worker, we need to finish installing it.
+ // Otherwise, the tests fails with an timeout because unregister() blocks
+ // until the install-event-handler finishes.
+ const worker = registration.installing;
+ await send_message_to_worker_and_wait_for_response(worker, 'awaitInstallEvent');
+ await send_message_to_worker_and_wait_for_response(worker, 'finishInstall');
+ }
+ return registration.unregister();
+ });
+
+ return registration;
+}
+
+promise_test(async t => {
+ const registration = await spin_up_service_worker(t);
+ const worker = registration.installing;
+ await ensure_install_event_fired(worker);
+
+ const result = registration.update();
+ await activate_service_worker(t, worker);
+ return result;
+}, 'ServiceWorkerRegistration.update() from client succeeds while installing service worker.');
+
+promise_test(async t => {
+ const registration = await spin_up_service_worker(t);
+ const worker = registration.installing;
+ await ensure_install_event_fired(worker);
+
+ // Add event listener to fail the test if update() succeeds.
+ const updatefound = t.step_func(async () => {
+ registration.removeEventListener('updatefound', updatefound);
+ // Activate new worker so non-compliant browsers don't fail with timeout.
+ await activate_service_worker(t, registration.installing);
+ assert_unreached("update() should have failed");
+ });
+ registration.addEventListener('updatefound', updatefound);
+
+ const update = await update_within_service_worker(worker);
+ // Activate worker to ensure update() finishes and the test doesn't timeout
+ // in non-compliant browsers.
+ await activate_service_worker(t, worker);
+
+ const response = await update.finish();
+ assert_false(response.success, 'update() should have failed.');
+ assert_equals('InvalidStateError', response.exception, 'update() should have thrown InvalidStateError.');
+}, 'ServiceWorkerRegistration.update() from installing service worker throws.');
+
+promise_test(async t => {
+ const registration = await spin_up_service_worker(t);
+ const worker1 = registration.installing;
+ await activate_service_worker(t, worker1);
+
+ const worker2 = await update_from_client_and_await_installing_version(t, registration);
+ await ensure_install_event_fired(worker2);
+
+ const update = await update_within_service_worker(worker1);
+ // Activate the new version so that update() finishes and the test doesn't timeout.
+ await activate_service_worker(t, worker2);
+ const response = await update.finish();
+ assert_true(response.success, 'update() from active service worker should have succeeded.');
+}, 'ServiceWorkerRegistration.update() from active service worker succeeds while installing service worker.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-on-navigation.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-on-navigation.https.html
new file mode 100644
index 0000000..5273420
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-on-navigation.https.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<title>Update on navigation</title>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='resources/test-helpers.sub.js'></script>
+<script>
+promise_test(async (t) => {
+ var script = 'resources/update-fetch-worker.py';
+ var scope = 'resources/trickle.py?ms=1000&count=1';
+
+ const registration = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => registration.unregister());
+
+ if (registration.installing)
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const frame = await with_iframe(scope);
+ t.add_cleanup(() => frame.remove());
+}, 'The active service worker in charge of a navigation load should not be terminated as part of updating the registration');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-recovery.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-recovery.https.html
new file mode 100644
index 0000000..17608d2
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-recovery.https.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<title>Service Worker: recovery by navigation update</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(function(t) {
+ var scope = 'resources/simple.txt';
+ var worker_url = 'resources/update-recovery-worker.py';
+ var expected_url = normalizeURL(worker_url);
+ var registration;
+
+ function with_bad_iframe(url) {
+ return new Promise(function(resolve, reject) {
+ var frame = document.createElement('iframe');
+
+ // There is no cross-browser event to listen for to detect an
+ // iframe that fails to load due to a bad interception. Unfortunately
+ // we have to use a timeout.
+ var timeout = setTimeout(function() {
+ frame.remove();
+ resolve();
+ }, 5000);
+
+ // If we do get a load event, though, we know something went wrong.
+ frame.addEventListener('load', function() {
+ clearTimeout(timeout);
+ frame.remove();
+ reject('expected bad iframe should not fire a load event!');
+ });
+
+ frame.src = url;
+ document.body.appendChild(frame);
+ });
+ }
+
+ function with_update(t) {
+ return new Promise(function(resolve, reject) {
+ registration.addEventListener('updatefound', function onUpdate() {
+ registration.removeEventListener('updatefound', onUpdate);
+ wait_for_state(t, registration.installing, 'activated').then(function() {
+ resolve();
+ });
+ });
+ });
+ }
+
+ return service_worker_unregister_and_register(t, worker_url, scope)
+ .then(function(r) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ registration = r;
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() {
+ return Promise.all([
+ with_update(t),
+ with_bad_iframe(scope)
+ ]);
+ })
+ .then(function() {
+ return with_iframe(scope);
+ })
+ .then(function(frame) {
+ assert_equals(frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ expected_url);
+ frame.remove();
+ });
+ }, 'Recover from a bad service worker by updating after a failed navigation.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-registration-with-type.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-registration-with-type.https.html
new file mode 100644
index 0000000..269e61b
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-registration-with-type.https.html
@@ -0,0 +1,208 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: Update the registration with a different script type.</title>
+<!-- common.js is for guid() -->
+<script src="/common/security-features/resources/common.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+// The following two tests check that a registration is updated correctly
+// with different script type. At first Service Worker is registered as
+// classic script type, then it is re-registered as module script type,
+// and vice versa. A main script is also updated at the same time.
+promise_test(async t => {
+ const key = guid();
+ const script = `resources/update-registration-with-type.py?classic_first=1&key=${key}`;
+ const scope = 'resources/update-registration-with-type';
+ await service_worker_unregister(t, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ // Register with classic script type.
+ const firstRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'classic'
+ });
+ const firstWorker = firstRegistration.installing;
+ await wait_for_state(t, firstWorker, 'activated');
+ firstWorker.postMessage(' ');
+ let msgEvent = await new Promise(r => navigator.serviceWorker.onmessage = r);
+ assert_equals(msgEvent.data, 'A classic script.');
+
+ // Re-register with module script type.
+ const secondRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ });
+ const secondWorker = secondRegistration.installing;
+ secondWorker.postMessage(' ');
+ msgEvent = await new Promise(r => navigator.serviceWorker.onmessage = r);
+ assert_equals(msgEvent.data, 'A module script.');
+
+ assert_not_equals(firstWorker, secondWorker);
+ assert_equals(firstRegistration, secondRegistration);
+}, 'Update the registration with a different script type (classic => module).');
+
+promise_test(async t => {
+ const key = guid();
+ const script = `resources/update-registration-with-type.py?classic_first=0&key=${key}`;
+ const scope = 'resources/update-registration-with-type';
+ await service_worker_unregister(t, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ // Register with module script type.
+ const firstRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ });
+ const firstWorker = firstRegistration.installing;
+ await wait_for_state(t, firstWorker, 'activated');
+ firstWorker.postMessage(' ');
+ let msgEvent = await new Promise(r => navigator.serviceWorker.onmessage = r);
+ assert_equals(msgEvent.data, 'A module script.');
+
+ // Re-register with classic script type.
+ const secondRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'classic'
+ });
+ const secondWorker = secondRegistration.installing;
+ secondWorker.postMessage(' ');
+ msgEvent = await new Promise(r => navigator.serviceWorker.onmessage = r);
+ assert_equals(msgEvent.data, 'A classic script.');
+
+ assert_not_equals(firstWorker, secondWorker);
+ assert_equals(firstRegistration, secondRegistration);
+}, 'Update the registration with a different script type (module => classic).');
+
+// The following two tests change the script type while keeping
+// the script identical.
+promise_test(async t => {
+ const script = 'resources/empty-worker.js';
+ const scope = 'resources/update-registration-with-type';
+ await service_worker_unregister(t, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ // Register with classic script type.
+ const firstRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'classic'
+ });
+ const firstWorker = firstRegistration.installing;
+ await wait_for_state(t, firstWorker, 'activated');
+
+ // Re-register with module script type.
+ const secondRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ });
+ const secondWorker = secondRegistration.installing;
+
+ assert_not_equals(firstWorker, secondWorker);
+ assert_equals(firstRegistration, secondRegistration);
+}, 'Update the registration with a different script type (classic => module) '
+ + 'and with a same main script.');
+
+promise_test(async t => {
+ const script = 'resources/empty-worker.js';
+ const scope = 'resources/update-registration-with-type';
+ await service_worker_unregister(t, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ // Register with module script type.
+ const firstRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ });
+ const firstWorker = firstRegistration.installing;
+ await wait_for_state(t, firstWorker, 'activated');
+
+ // Re-register with classic script type.
+ const secondRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'classic'
+ });
+ const secondWorker = secondRegistration.installing;
+
+ assert_not_equals(firstWorker, secondWorker);
+ assert_equals(firstRegistration, secondRegistration);
+}, 'Update the registration with a different script type (module => classic) '
+ + 'and with a same main script.');
+
+// This test checks that a registration is not updated with the same script
+// type and the same main script.
+promise_test(async t => {
+ const script = 'resources/empty-worker.js';
+ const scope = 'resources/update-registration-with-type';
+ await service_worker_unregister(t, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ // Register with module script type.
+ const firstRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ });
+ await wait_for_state(t, firstRegistration.installing, 'activated');
+
+ // Re-register with module script type.
+ const secondRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ });
+ assert_equals(secondRegistration.installing, null);
+
+ assert_equals(firstRegistration, secondRegistration);
+}, 'Does not update the registration with the same script type and '
+ + 'the same main script.');
+
+// In the case (classic => module), a worker script contains importScripts()
+// that is disallowed on module scripts, so the second registration is
+// expected to fail script evaluation.
+promise_test(async t => {
+ const script = 'resources/classic-worker.js';
+ const scope = 'resources/update-registration-with-type';
+ await service_worker_unregister(t, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ // Register with classic script type.
+ const firstRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'classic'
+ });
+ assert_not_equals(firstRegistration.installing, null);
+ await wait_for_state(t, firstRegistration.installing, 'activated');
+
+ // Re-register with module script type and expect TypeError.
+ return promise_rejects_js(t, TypeError, navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ }), 'Registering with invalid evaluation should be failed.');
+}, 'Update the registration with a different script type (classic => module) '
+ + 'and with a same main script. Expect evaluation failed.');
+
+// In the case (module => classic), a worker script contains static-import
+// that is disallowed on classic scripts, so the second registration is
+// expected to fail script evaluation.
+promise_test(async t => {
+ const script = 'resources/module-worker.js';
+ const scope = 'resources/update-registration-with-type';
+ await service_worker_unregister(t, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+
+ // Register with module script type.
+ const firstRegistration = await navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'module'
+ });
+ assert_not_equals(firstRegistration.installing, null);
+ await wait_for_state(t, firstRegistration.installing, 'activated');
+
+ // Re-register with classic script type and expect TypeError.
+ return promise_rejects_js(t, TypeError, navigator.serviceWorker.register(script, {
+ scope: scope,
+ type: 'classic'
+ }), 'Registering with invalid evaluation should be failed.');
+}, 'Update the registration with a different script type (module => classic) '
+ + 'and with a same main script. Expect evaluation failed.');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update-result.https.html b/third_party/web_platform_tests/service-workers/service-worker/update-result.https.html
new file mode 100644
index 0000000..d8ed94f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update-result.https.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<title>Service Worker: update() should resolve a ServiceWorkerRegistration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+promise_test(async function(t) {
+ const script = './resources/empty.js';
+ const scope = './resources/empty.html?update-result';
+
+ let reg = await navigator.serviceWorker.register(script, { scope });
+ t.add_cleanup(async _ => await reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ let result = await reg.update();
+ assert_true(result instanceof ServiceWorkerRegistration,
+ 'update() should resolve a ServiceWorkerRegistration');
+}, 'ServiceWorkerRegistration.update() should resolve a registration object');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/update.https.html b/third_party/web_platform_tests/service-workers/service-worker/update.https.html
new file mode 100644
index 0000000..f9fded3
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/update.https.html
@@ -0,0 +1,164 @@
+<!DOCTYPE html>
+<title>Service Worker: Registration update()</title>
+<meta name="timeout" content="long">
+<script src="/common/utils.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/testharness-helpers.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+'use strict';
+
+const SCOPE = 'resources/simple.txt';
+
+// Create a service worker (update-worker.py). The response to update() will be
+// different based on the mode.
+async function prepare_ready_registration_with_mode(t, mode) {
+ const key = token();
+ const worker_url = `resources/update-worker.py?Key=${key}&Mode=${mode}`;
+ const expected_url = normalizeURL(worker_url);
+ const registration = await service_worker_unregister_and_register(
+ t, worker_url, SCOPE);
+ await wait_for_state(t, registration.installing, 'activated');
+ assert_equals(registration.installing, null,
+ 'prepare_ready: installing');
+ assert_equals(registration.waiting, null,
+ 'prepare_ready: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'prepare_ready: active');
+ return [registration, expected_url];
+}
+
+// Create a service worker (update-worker-from-file.py), which is initially
+// |initial_worker| and |updated_worker| later.
+async function prepare_ready_registration_with_file(
+ t, initial_worker, updated_worker) {
+ const key = token();
+ const worker_url = `resources/update-worker-from-file.py?` +
+ `First=${initial_worker}&Second=${updated_worker}&Key=${key}`;
+ const expected_url = normalizeURL(worker_url);
+
+ const registration = await service_worker_unregister_and_register(
+ t, worker_url, SCOPE);
+ await wait_for_state(t, registration.installing, 'activated');
+ assert_equals(registration.installing, null,
+ 'prepare_ready: installing');
+ assert_equals(registration.waiting, null,
+ 'prepare_ready: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'prepare_ready: active');
+ return [registration, expected_url];
+}
+
+function assert_installing_and_active(registration, expected_url) {
+ assert_equals(registration.installing.scriptURL, expected_url,
+ 'assert_installing_and_active: installing');
+ assert_equals(registration.waiting, null,
+ 'assert_installing_and_active: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'assert_installing_and_active: active');
+}
+
+function assert_waiting_and_active(registration, expected_url) {
+ assert_equals(registration.installing, null,
+ 'assert_waiting_and_active: installing');
+ assert_equals(registration.waiting.scriptURL, expected_url,
+ 'assert_waiting_and_active: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'assert_waiting_and_active: active');
+}
+
+function assert_active_only(registration, expected_url) {
+ assert_equals(registration.installing, null,
+ 'assert_active_only: installing');
+ assert_equals(registration.waiting, null,
+ 'assert_active_only: waiting');
+ assert_equals(registration.active.scriptURL, expected_url,
+ 'assert_active_only: active');
+}
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_registration_with_mode(t, 'normal');
+ t.add_cleanup(() => registration.unregister());
+
+ await Promise.all([registration.update(), wait_for_update(t, registration)]);
+ assert_installing_and_active(registration, expected_url);
+
+ await wait_for_state(t, registration.installing, 'installed');
+ assert_waiting_and_active(registration, expected_url);
+
+ await wait_for_state(t, registration.waiting, 'activated');
+ assert_active_only(registration, expected_url);
+}, 'update() should succeed when new script is available.');
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_registration_with_mode(t, 'bad_mime_type');
+ t.add_cleanup(() => registration.unregister());
+
+ await promise_rejects_dom(t, 'SecurityError', registration.update());
+ assert_active_only(registration, expected_url);
+}, 'update() should fail when mime type is invalid.');
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_registration_with_mode(t, 'redirect');
+ t.add_cleanup(() => registration.unregister());
+
+ await promise_rejects_js(t, TypeError, registration.update());
+ assert_active_only(registration, expected_url);
+}, 'update() should fail when a response for the main script is redirect.');
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_registration_with_mode(t, 'syntax_error');
+ t.add_cleanup(() => registration.unregister());
+
+ await promise_rejects_js(t, TypeError, registration.update());
+ assert_active_only(registration, expected_url);
+}, 'update() should fail when a new script contains a syntax error.');
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_registration_with_mode(t, 'throw_install');
+ t.add_cleanup(() => registration.unregister());
+
+ await Promise.all([registration.update(), wait_for_update(t, registration)]);
+ assert_installing_and_active(registration, expected_url);
+}, 'update() should resolve when the install event throws.');
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_registration_with_mode(t, 'normal');
+ t.add_cleanup(() => registration.unregister());
+
+ // We need to hold a client alive so that unregister() below doesn't remove
+ // the registration before update() has had a chance to look at the pending
+ // uninstall flag.
+ const frame = await with_iframe(SCOPE);
+ t.add_cleanup(() => frame.remove());
+
+ await promise_rejects_js(
+ t, TypeError,
+ Promise.all([registration.unregister(), registration.update()]));
+}, 'update() should fail when the pending uninstall flag is set.');
+
+promise_test(async t => {
+ const [registration, expected_url] =
+ await prepare_ready_registration_with_file(
+ t,
+ 'update-smaller-body-before-update-worker.js',
+ 'update-smaller-body-after-update-worker.js');
+ t.add_cleanup(() => registration.unregister());
+
+ await Promise.all([registration.update(), wait_for_update(t, registration)]);
+ assert_installing_and_active(registration, expected_url);
+
+ await wait_for_state(t, registration.installing, 'installed');
+ assert_waiting_and_active(registration, expected_url);
+
+ await wait_for_state(t, registration.waiting, 'activated');
+ assert_active_only(registration, expected_url);
+}, 'update() should succeed when the script shrinks.');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/waiting.https.html b/third_party/web_platform_tests/service-workers/service-worker/waiting.https.html
new file mode 100644
index 0000000..499e581
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/waiting.https.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<title>ServiceWorker: navigator.serviceWorker.waiting</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+const SCRIPT = 'resources/empty-worker.js';
+const SCOPE = 'resources/blank.html';
+
+promise_test(async t => {
+
+ t.add_cleanup(async() => {
+ if (frame)
+ frame.remove();
+ if (registration)
+ await registration.unregister();
+ });
+
+ await service_worker_unregister(t, SCOPE);
+ const frame = await with_iframe(SCOPE);
+ const registration =
+ await navigator.serviceWorker.register(SCRIPT, {scope: SCOPE});
+ await wait_for_state(t, registration.installing, 'installed');
+ const controller = frame.contentWindow.navigator.serviceWorker.controller;
+ assert_equals(controller, null, 'controller');
+ assert_equals(registration.active, null, 'registration.active');
+ assert_equals(registration.waiting.state, 'installed',
+ 'registration.waiting');
+ assert_equals(registration.installing, null, 'registration.installing');
+}, 'waiting is set after installation');
+
+// Tests that the ServiceWorker objects returned from waiting attribute getter
+// that represent the same service worker are the same objects.
+promise_test(async t => {
+ const registration1 =
+ await service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+ const registration2 = await navigator.serviceWorker.getRegistration(SCOPE);
+ assert_equals(registration1.waiting, registration2.waiting,
+ 'ServiceWorkerRegistration.waiting should return the same ' +
+ 'object');
+ await registration1.unregister();
+}, 'The ServiceWorker objects returned from waiting attribute getter that ' +
+ 'represent the same service worker are the same objects');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/websocket-in-service-worker.https.html b/third_party/web_platform_tests/service-workers/service-worker/websocket-in-service-worker.https.html
new file mode 100644
index 0000000..cda9d6f
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/websocket-in-service-worker.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<title>Service Worker: WebSockets can be created in a Service Worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+promise_test(t => {
+ const SCRIPT = 'resources/websocket-worker.js?pipe=sub';
+ const SCOPE = 'resources/blank.html';
+ let registration;
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(r => {
+ add_completion_callback(() => { r.unregister(); });
+ registration = r;
+ return wait_for_state(t, r.installing, 'activated');
+ })
+ .then(() => {
+ return new Promise(resolve => {
+ navigator.serviceWorker.onmessage = t.step_func(msg => {
+ assert_equals(msg.data, 'PASS');
+ resolve();
+ });
+ registration.active.postMessage({});
+ });
+ });
+ }, 'Verify WebSockets can be created in a Service Worker');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/websocket.https.html b/third_party/web_platform_tests/service-workers/service-worker/websocket.https.html
new file mode 100644
index 0000000..cbfed45
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/websocket.https.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<title>Service Worker: WebSocket handshake channel is not intercepted</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+
+promise_test(function(t) {
+ var path = new URL(".", window.location).pathname
+ var url = 'resources/websocket.js';
+ var scope = 'resources/blank.html?websocket';
+ var host_info = get_host_info();
+ var frameURL = host_info['HTTPS_ORIGIN'] + path + scope;
+ var frame;
+
+ return service_worker_unregister_and_register(t, url, scope)
+ .then(function(registration) {
+ t.add_cleanup(function() {
+ return service_worker_unregister(t, scope);
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(function() { return with_iframe(frameURL); })
+ .then(function(f) {
+ frame = f;
+ return websocket(t, frame);
+ })
+ .then(function() {
+ var channel = new MessageChannel();
+ return new Promise(function(resolve) {
+ channel.port1.onmessage = resolve;
+ frame.contentWindow.navigator.serviceWorker.controller.postMessage({port: channel.port2}, [channel.port2]);
+ });
+ })
+ .then(function(e) {
+ for (var url in e.data.urls) {
+ assert_equals(url.indexOf(get_websocket_url()), -1,
+ "Observed an unexpected FetchEvent for the WebSocket handshake");
+ }
+ frame.remove();
+ });
+ }, 'Verify WebSocket handshake channel does not get intercepted');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/webvtt-cross-origin.https.html b/third_party/web_platform_tests/service-workers/service-worker/webvtt-cross-origin.https.html
new file mode 100644
index 0000000..9394ff7
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/webvtt-cross-origin.https.html
@@ -0,0 +1,175 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>cross-origin webvtt returned by service worker is detected</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body>
+<script>
+// This file tests responses for WebVTT text track from a service worker. It
+// creates an iframe with a <track> element, controlled by a service worker.
+// Each test tries to load a text track, the service worker intercepts the
+// requests and responds with opaque or non-opaque responses. As the
+// crossorigin attribute is not set, request's mode is always "same-origin",
+// and as specified in https://fetch.spec.whatwg.org/#http-fetch,
+// a response from a service worker whose type is neither "basic" nor
+// "default" is rejected.
+
+const host_info = get_host_info();
+const kScript = 'resources/fetch-rewrite-worker.js';
+// Add '?ignore' so the service worker falls back for the navigation.
+const kScope = 'resources/vtt-frame.html?ignore';
+let frame;
+
+function load_track(url) {
+ const track = frame.contentDocument.querySelector('track');
+ const result = new Promise((resolve, reject) => {
+ track.onload = (e => {
+ resolve('load event');
+ });
+ track.onerror = (e => {
+ resolve('error event');
+ });
+ });
+
+ track.src = url;
+ // Setting mode to hidden seems needed, or else the text track requests don't
+ // occur.
+ track.track.mode = 'hidden';
+ return result;
+}
+
+promise_test(t => {
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ promise_test(() => {
+ frame.remove();
+ return registration.unregister();
+ }, 'restore global state');
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(kScope);
+ })
+ .then(f => {
+ frame = f;
+ })
+ }, 'initialize global state');
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a same-origin URL.
+ url += '?url=' + host_info.HTTPS_ORIGIN + '/media/foo.vtt';
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'load event');
+ });
+ }, 'same-origin text track should load');
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a cross-origin URL.
+ url += '?url=' + get_host_info().HTTPS_REMOTE_ORIGIN + '/media/foo.vtt';
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'error event');
+ });
+ }, 'cross-origin text track with no-cors request should not load');
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a cross-origin URL that
+ // doesn't support CORS.
+ url += '?url=' + get_host_info().HTTPS_REMOTE_ORIGIN +
+ '/media/foo-no-cors.vtt';
+ // Add '&mode' to tell the service worker to do a CORS request.
+ url += '&mode=cors';
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'error event');
+ });
+ }, 'cross-origin text track with rejected cors request should not load');
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a cross-origin URL.
+ url += '?url=' + get_host_info().HTTPS_REMOTE_ORIGIN + '/media/foo.vtt';
+ // Add '&mode' to tell the service worker to do a CORS request.
+ url += '&mode=cors';
+ // Add '&credentials=same-origin' to allow Access-Control-Allow-Origin=* so
+ // that CORS will succeed if the service approves it.
+ url += '&credentials=same-origin';
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'error event');
+ });
+ }, 'cross-origin text track with approved cors request should not load');
+
+// Redirect tests.
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a same-origin URL that redirects...
+ redirector_url = host_info.HTTPS_ORIGIN + base_path() + 'resources/redirect.py?Redirect=';
+ // ... to a same-origin URL.
+ redirect_target = host_info.HTTPS_ORIGIN + '/media/foo.vtt';
+ url += '?url=' + encodeURIComponent(redirector_url + encodeURIComponent(redirect_target));
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'load event');
+ });
+ }, 'same-origin text track that redirects same-origin should load');
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a same-origin URL that redirects...
+ redirector_url = host_info.HTTPS_ORIGIN + base_path() + 'resources/redirect.py?Redirect=';
+ // ... to a cross-origin URL.
+ redirect_target = host_info.HTTPS_REMOTE_ORIGIN + '/media/foo.vtt';
+ url += '?url=' + encodeURIComponent(redirector_url + encodeURIComponent(redirect_target));
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'error event');
+ });
+ }, 'same-origin text track that redirects cross-origin should not load');
+
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a same-origin URL that redirects...
+ redirector_url = host_info.HTTPS_ORIGIN + base_path() + 'resources/redirect.py?Redirect=';
+ // ... to a cross-origin URL.
+ redirect_target = host_info.HTTPS_REMOTE_ORIGIN + '/media/foo-no-cors.vtt';
+ url += '?url=' + encodeURIComponent(redirector_url + encodeURIComponent(redirect_target));
+ // Add '&mode' to tell the service worker to do a CORS request.
+ url += '&mode=cors';
+ // Add '&credentials=same-origin' to allow Access-Control-Allow-Origin=* so
+ // that CORS will succeed if the server approves it.
+ url += '&credentials=same-origin';
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'error event');
+ });
+ }, 'same-origin text track that redirects to a cross-origin text track with rejected cors should not load');
+
+promise_test(t => {
+ let url = '/media/foo.vtt';
+ // Add '?url' and tell the service worker to fetch a same-origin URL that redirects...
+ redirector_url = host_info.HTTPS_ORIGIN + base_path() + 'resources/redirect.py?Redirect=';
+ // ... to a cross-origin URL.
+ redirect_target = host_info.HTTPS_REMOTE_ORIGIN + '/media/foo.vtt';
+ url += '?url=' + encodeURIComponent(redirector_url + encodeURIComponent(redirect_target));
+ // Add '&mode' to tell the service worker to do a CORS request.
+ url += '&mode=cors';
+ // Add '&credentials=same-origin' to allow Access-Control-Allow-Origin=* so
+ // that CORS will succeed if the server approves it.
+ url += '&credentials=same-origin';
+ return load_track(url)
+ .then(result => {
+ assert_equals(result, 'error event');
+ });
+ }, 'same-origin text track that redirects to a cross-origin text track with approved cors should not load');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/windowclient-navigate.https.html b/third_party/web_platform_tests/service-workers/service-worker/windowclient-navigate.https.html
new file mode 100644
index 0000000..ad60f78
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/windowclient-navigate.https.html
@@ -0,0 +1,190 @@
+<!DOCTYPE html>
+<title>Service Worker: WindowClient.navigate() tests</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+'use strict';
+
+const SCOPE = 'resources/blank.html';
+const SCRIPT_URL = 'resources/windowclient-navigate-worker.js';
+const CROSS_ORIGIN_URL =
+ get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() + 'resources/blank.html';
+
+navigateTest({
+ description: 'normal',
+ destUrl: 'blank.html?navigate',
+ expected: normalizeURL(SCOPE) + '?navigate',
+});
+
+navigateTest({
+ description: 'blank url',
+ destUrl: '',
+ expected: normalizeURL(SCRIPT_URL)
+});
+
+navigateTest({
+ description: 'in scope but not controlled test on installing worker',
+ destUrl: 'blank.html?navigate',
+ expected: 'TypeError',
+ waitState: 'installing',
+});
+
+navigateTest({
+ description: 'in scope but not controlled test on active worker',
+ destUrl: 'blank.html?navigate',
+ expected: 'TypeError',
+ controlled: false,
+});
+
+navigateTest({
+ description: 'out of scope',
+ srcUrl: '/common/blank.html',
+ destUrl: 'blank.html?navigate',
+ expected: 'TypeError',
+});
+
+navigateTest({
+ description: 'cross orgin url',
+ destUrl: CROSS_ORIGIN_URL,
+ expected: null
+});
+
+navigateTest({
+ description: 'invalid url (http://[example.com])',
+ destUrl: 'http://[example].com',
+ expected: 'TypeError'
+});
+
+navigateTest({
+ description: 'invalid url (view-source://example.com)',
+ destUrl: 'view-source://example.com',
+ expected: 'TypeError'
+});
+
+navigateTest({
+ description: 'invalid url (file:///)',
+ destUrl: 'file:///',
+ expected: 'TypeError'
+});
+
+navigateTest({
+ description: 'invalid url (about:blank)',
+ destUrl: 'about:blank',
+ expected: 'TypeError'
+});
+
+navigateTest({
+ description: 'navigate on a top-level window client',
+ destUrl: 'blank.html?navigate',
+ srcUrl: 'resources/loaded.html',
+ scope: 'resources/loaded.html',
+ expected: normalizeURL(SCOPE) + '?navigate',
+ frameType: 'top-level'
+});
+
+async function createFrame(t, parameters) {
+ if (parameters.frameType === 'top-level') {
+ // Wait for window.open is completed.
+ await new Promise(resolve => {
+ const win = window.open(parameters.srcUrl);
+ t.add_cleanup(() => win.close());
+ window.addEventListener('message', (e) => {
+ if (e.data.type === 'LOADED') {
+ resolve();
+ }
+ });
+ });
+ }
+
+ if (parameters.frameType === 'nested') {
+ const frame = await with_iframe(parameters.srcUrl);
+ t.add_cleanup(() => frame.remove());
+ }
+}
+
+function navigateTest(overrideParameters) {
+ // default parameters
+ const parameters = {
+ description: null,
+ srcUrl: SCOPE,
+ destUrl: null,
+ expected: null,
+ waitState: 'activated',
+ scope: SCOPE,
+ controlled: true,
+ // `frameType` can be 'nested' for an iframe WindowClient or 'top-level' for
+ // a main frame WindowClient.
+ frameType: 'nested'
+ };
+
+ for (const key in overrideParameters)
+ parameters[key] = overrideParameters[key];
+
+ promise_test(async function(t) {
+ let pausedLifecyclePort;
+ let scriptUrl = SCRIPT_URL;
+
+ // For in-scope-but-not-controlled test on installing worker,
+ // if the waitState is "installing", then append the query to scriptUrl.
+ if (parameters.waitState === 'installing') {
+ scriptUrl += '?' + parameters.waitState;
+
+ navigator.serviceWorker.addEventListener('message', (event) => {
+ if (event.data.port) {
+ pausedLifecyclePort = event.data.port;
+ }
+ });
+ }
+
+ t.add_cleanup(() => {
+ // Some tests require that the worker remain in a given lifecycle phase.
+ // "Clean up" logic for these tests requires signaling the worker to
+ // release the hold; this allows the worker to be properly discarded
+ // prior to the execution of additional tests.
+ if (pausedLifecyclePort) {
+ // The value of the posted message is inconsequential. A string is
+ // specified here solely to aid in test debugging.
+ pausedLifecyclePort.postMessage('continue lifecycle');
+ }
+ });
+
+ // Create a frame that is not controlled by a service worker.
+ if (!parameters.controlled) {
+ await createFrame(t, parameters);
+ }
+
+ const registration = await service_worker_unregister_and_register(
+ t, scriptUrl, parameters.scope);
+ const serviceWorker = registration.installing;
+ await wait_for_state(t, serviceWorker, parameters.waitState);
+ t.add_cleanup(() => registration.unregister());
+
+ // Create a frame after a service worker is registered so that the frmae is
+ // controlled by an active service worker.
+ if (parameters.controlled) {
+ await createFrame(t, parameters);
+ }
+
+ const response = await new Promise(resolve => {
+ const channel = new MessageChannel();
+ channel.port1.onmessage = t.step_func(resolve);
+ serviceWorker.postMessage({
+ port: channel.port2,
+ url: parameters.destUrl,
+ clientUrl: new URL(parameters.srcUrl, location).toString(),
+ frameType: parameters.frameType,
+ expected: parameters.expected,
+ description: parameters.description,
+ }, [channel.port2]);
+ });
+
+ assert_equals(response.data, null);
+ await fetch_tests_from_worker(serviceWorker);
+ }, parameters.description);
+}
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/worker-client-id.https.html b/third_party/web_platform_tests/service-workers/service-worker/worker-client-id.https.html
new file mode 100644
index 0000000..4e4d316
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/worker-client-id.https.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<title>Service Worker: Workers should have their own unique client Id</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+// Get the iframe client ID by calling postMessage() on its controlling
+// worker. This will cause the service worker to post back the
+// MessageEvent.source.id value.
+function getFrameClientId(frame) {
+ return new Promise(resolve => {
+ let mc = new MessageChannel();
+ frame.contentWindow.navigator.serviceWorker.controller.postMessage(
+ 'echo-client-id', [mc.port2]);
+ mc.port1.onmessage = evt => {
+ resolve(evt.data);
+ };
+ });
+}
+
+// Get the worker client ID by creating a worker that performs an intercepted
+// fetch(). The synthetic fetch() response will contain the FetchEvent.clientId
+// value. This is then posted back to here.
+function getWorkerClientId(frame) {
+ return new Promise(resolve => {
+ let w = new frame.contentWindow.Worker('worker-echo-client-id.js');
+ w.onmessage = evt => {
+ resolve(evt.data);
+ };
+ });
+}
+
+promise_test(async function(t) {
+ const script = './resources/worker-client-id-worker.js';
+ const scope = './resources/worker-client-id';
+ const frame = scope + '/frame.html';
+
+ let reg = await navigator.serviceWorker.register(script, { scope });
+ t.add_cleanup(async _ => await reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ let f = await with_iframe(frame);
+ t.add_cleanup(_ => f.remove());
+
+ let frameClientId = await getFrameClientId(f);
+ assert_not_equals(frameClientId, null, 'frame client id should exist');
+
+ let workerClientId = await getWorkerClientId(f);
+ assert_not_equals(workerClientId, null, 'worker client id should exist');
+
+ assert_not_equals(frameClientId, workerClientId,
+ 'frame and worker client ids should be different');
+}, 'Verify workers have a unique client id separate from their owning documents window');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/worker-in-sandboxed-iframe-by-csp-fetch-event.https.html b/third_party/web_platform_tests/service-workers/service-worker/worker-in-sandboxed-iframe-by-csp-fetch-event.https.html
new file mode 100644
index 0000000..c8480bf
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/worker-in-sandboxed-iframe-by-csp-fetch-event.https.html
@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<title>ServiceWorker FetchEvent issued from workers in an iframe sandboxed via CSP HTTP response header.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+let lastCallbackId = 0;
+let callbacks = {};
+function doTest(frame, type) {
+ return new Promise(function(resolve) {
+ var id = ++lastCallbackId;
+ callbacks[id] = resolve;
+ frame.contentWindow.postMessage({id: id, type: type}, '*');
+ });
+}
+
+// Asks the service worker for data about requests and clients seen. The
+// worker posts a message back with |data| where:
+// |data.requests|: the requests the worker received FetchEvents for
+// |data.clients|: the URLs of all the worker's clients
+// The worker clears its data after responding.
+function getResultsFromWorker(worker) {
+ return new Promise(resolve => {
+ let channel = new MessageChannel();
+ channel.port1.onmessage = msg => {
+ resolve(msg.data);
+ };
+ worker.postMessage({port: channel.port2}, [channel.port2]);
+ });
+}
+
+window.onmessage = function (e) {
+ message = e.data;
+ let id = message['id'];
+ let callback = callbacks[id];
+ delete callbacks[id];
+ callback(message['result']);
+};
+
+const SCOPE = 'resources/sandboxed-iframe-fetch-event-iframe.py';
+const SCRIPT = 'resources/sandboxed-iframe-fetch-event-worker.js';
+const expected_base_url = new URL(SCOPE, location.href);
+// A service worker controlling |SCOPE|.
+let worker;
+// An iframe whose response header has
+// 'Content-Security-Policy: allow-scripts'.
+// This should NOT be controlled by a service worker.
+let sandboxed_frame_by_header;
+// An iframe whose response header has
+// 'Content-Security-Policy: allow-scripts allow-same-origin'.
+// This should be controlled by a service worker.
+let sandboxed_same_origin_frame_by_header;
+
+promise_test(t => {
+ return service_worker_unregister_and_register(t, SCRIPT, SCOPE)
+ .then(function(registration) {
+ add_completion_callback(() => registration.unregister());
+ worker = registration.installing;
+ return wait_for_state(t, registration.installing, 'activated');
+ });
+}, 'Prepare a service worker.');
+
+promise_test(t => {
+ const iframe_full_url = expected_base_url + '?sandbox=allow-scripts&' +
+ 'sandboxed-frame-by-header';
+ return with_iframe(iframe_full_url)
+ .then(f => {
+ sandboxed_frame_by_header = f;
+ add_completion_callback(() => f.remove());
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'Service worker should provide the response');
+ assert_equals(requests[0], iframe_full_url);
+ assert_false(data.clients.includes(iframe_full_url),
+ 'Service worker should NOT control the sandboxed page');
+ });
+}, 'Prepare an iframe sandboxed by CSP HTTP header with allow-scripts.');
+
+promise_test(t => {
+ const iframe_full_url =
+ expected_base_url + '?sandbox=allow-scripts%20allow-same-origin&' +
+ 'sandboxed-iframe-same-origin-by-header';
+ return with_iframe(iframe_full_url)
+ .then(f => {
+ sandboxed_same_origin_frame_by_header = f;
+ add_completion_callback(() => f.remove());
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1);
+ assert_equals(requests[0], iframe_full_url);
+ assert_true(data.clients.includes(iframe_full_url));
+ })
+}, 'Prepare an iframe sandboxed by CSP HTTP header with allow-scripts and ' +
+ 'allow-same-origin.');
+
+promise_test(t => {
+ let frame = sandboxed_frame_by_header;
+ return doTest(frame, 'fetch-from-worker')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ assert_equals(data.requests.length, 0,
+ 'The request should NOT be handled by SW.');
+ });
+}, 'Fetch request from a worker in iframe sandboxed by CSP HTTP header ' +
+ 'allow-scripts flag');
+
+promise_test(t => {
+ let frame = sandboxed_same_origin_frame_by_header;
+ return doTest(frame, 'fetch-from-worker')
+ .then(result => {
+ assert_equals(result, 'done');
+ return getResultsFromWorker(worker);
+ })
+ .then(data => {
+ let requests = data.requests;
+ assert_equals(requests.length, 1,
+ 'The request should be handled by SW.');
+ assert_equals(requests[0], frame.src + '&test=fetch-from-worker');
+ });
+}, 'Fetch request from a worker in iframe sandboxed by CSP HTTP header ' +
+ 'with allow-scripts and allow-same-origin flag');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/worker-interception-redirect.https.html b/third_party/web_platform_tests/service-workers/service-worker/worker-interception-redirect.https.html
new file mode 100644
index 0000000..8d566b9
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/worker-interception-redirect.https.html
@@ -0,0 +1,212 @@
+<!DOCTYPE html>
+<title>Service Worker: controlling Worker/SharedWorker</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+// This tests service worker interception for worker clients, when the request
+// for the worker script goes through redirects. For example, a request can go
+// through a chain of URLs like A -> B -> C -> D and each URL might fall in the
+// scope of a different service worker, if any.
+// The two key questions are:
+// 1. Upon a redirect from A -> B, should a service worker for scope B
+// intercept the request?
+// 2. After the final response, which service worker controls the resulting
+// client?
+//
+// The standard prescribes the following:
+// 1. The service worker for scope B intercepts the redirect. *However*, once a
+// request falls back to network (i.e., a service worker did not call
+// respondWith()) and a redirect is then received from network, no service
+// worker should intercept that redirect or any subsequent redirects.
+// 2. The final service worker that got a fetch event (or would have, in the
+// case of a non-fetch-event worker) becomes the controller of the client.
+//
+// The standard may change later, see:
+// https://github.com/w3c/ServiceWorker/issues/1289
+//
+// The basic test setup is:
+// 1. Page registers service workers for scope1 and scope2.
+// 2. Page requests a worker from scope1.
+// 3. The request is redirected to scope2 or out-of-scope.
+// 4. The worker posts message to the page describing where the final response
+// was served from (service worker or network).
+// 5. The worker does an importScripts() and fetch(), and posts back the
+// responses, which describe where the responses where served from.
+
+// Globals for easier cleanup.
+const scope1 = 'resources/scope1';
+const scope2 = 'resources/scope2';
+let frame;
+
+function get_message_from_worker(port) {
+ return new Promise(resolve => {
+ port.onmessage = evt => {
+ resolve(evt.data);
+ }
+ });
+}
+
+async function cleanup() {
+ if (frame)
+ frame.remove();
+
+ const reg1 = await navigator.serviceWorker.getRegistration(scope1);
+ if (reg1)
+ await reg1.unregister();
+ const reg2 = await navigator.serviceWorker.getRegistration(scope2);
+ if (reg2)
+ await reg2.unregister();
+}
+
+// Builds the worker script URL, which encodes information about where
+// to redirect to. The URL falls in sw1's scope.
+//
+// - |redirector| is "network" or "serviceworker". If "serviceworker", sw1 will
+// respondWith() a redirect. Otherwise, it falls back to network and the server
+// responds with a redirect.
+// - |redirect_location| is "scope2" or "out-of-scope". If "scope2", the
+// redirect ends up in sw2's scope2. Otherwise it's out of scope.
+function build_worker_url(redirector, redirect_location) {
+ let redirect_path;
+ // Set path to redirect.py, a file on the server that serves
+ // a redirect. When sw1 sees this URL, it falls back to network.
+ if (redirector == 'network')
+ redirector_path = 'redirect.py';
+ // Set path to 'sw-redirect', to tell the service worker
+ // to respond with redirect.
+ else if (redirector == 'serviceworker')
+ redirector_path = 'sw-redirect';
+
+ let redirect_to = base_path() + 'resources/';
+ // Append "scope2/" to redirect_to, so the redirect falls in scope2.
+ // Otherwise no change is needed, as the parent "resources/" directory is
+ // used, and is out-of-scope.
+ if (redirect_location == 'scope2')
+ redirect_to += 'scope2/';
+ // Append the name of the file which serves the worker script.
+ redirect_to += 'worker_interception_redirect_webworker.py';
+
+ return `scope1/${redirector_path}?Redirect=${redirect_to}`
+}
+
+promise_test(async t => {
+ await cleanup();
+ const service_worker = 'resources/worker-interception-redirect-serviceworker.js';
+ const registration1 = await navigator.serviceWorker.register(service_worker, {scope: scope1});
+ await wait_for_state(t, registration1.installing, 'activated');
+ const registration2 = await navigator.serviceWorker.register(service_worker, {scope: scope2});
+ await wait_for_state(t, registration2.installing, 'activated');
+
+ promise_test(t => {
+ return cleanup();
+ }, 'cleanup global state');
+}, 'initialize global state');
+
+async function worker_redirect_test(worker_request_url,
+ worker_expected_url,
+ expected_main_resource_message,
+ expected_import_scripts_message,
+ expected_fetch_message,
+ description) {
+ for (const workerType of ['DedicatedWorker', 'SharedWorker']) {
+ for (const type of ['classic', 'module']) {
+ promise_test(async t => {
+ // Create a frame to load the worker from. This way we can remove the
+ // frame to destroy the worker client when the test is done.
+ frame = await with_iframe('resources/blank.html');
+ t.add_cleanup(() => { frame.remove(); });
+
+ // Start the worker.
+ let w;
+ let port;
+ if (workerType === 'DedicatedWorker') {
+ w = new frame.contentWindow.Worker(worker_request_url, {type});
+ port = w;
+ } else {
+ w = new frame.contentWindow.SharedWorker(worker_request_url, {type});
+ port = w.port;
+ w.port.start();
+ }
+ w.onerror = t.unreached_func('Worker error');
+
+ // Expect a message from the worker indicating which service worker
+ // provided the response for the worker script request, if any.
+ const data = await get_message_from_worker(port);
+
+ // The worker does an importScripts(). Expect a message from the worker
+ // indicating which service worker provided the response for the
+ // importScripts(), if any.
+ const import_scripts_message = await get_message_from_worker(port);
+ test(() => {
+ if (type === 'classic') {
+ assert_equals(import_scripts_message,
+ expected_import_scripts_message);
+ } else {
+ assert_equals(import_scripts_message, 'importScripts failed');
+ }
+ }, `${description} (${type} ${workerType}, importScripts())`);
+
+ // The worker does a fetch(). Expect a message from the worker
+ // indicating which service worker provided the response for the
+ // fetch(), if any.
+ const fetch_message = await get_message_from_worker(port);
+ test(() => {
+ assert_equals(fetch_message, expected_fetch_message);
+ }, `${description} (${type} ${workerType}, fetch())`);
+
+ // Expect a message from the worker indicating |self.location|.
+ const worker_actual_url = await get_message_from_worker(port);
+ test(() => {
+ assert_equals(
+ worker_actual_url,
+ (new URL(worker_expected_url, location.href)).toString(),
+ 'location.href');
+ }, `${description} (${type} ${workerType}, location.href)`);
+
+ assert_equals(data, expected_main_resource_message);
+
+ }, `${description} (${type} ${workerType})`);
+ }
+ }
+}
+
+// request to sw1 scope gets network redirect to sw2 scope
+worker_redirect_test(
+ build_worker_url('network', 'scope2'),
+ 'resources/scope2/worker_interception_redirect_webworker.py',
+ 'the worker script was served from network',
+ 'sw1 saw importScripts from the worker: /service-workers/service-worker/resources/scope2/import-scripts-echo.py',
+ 'fetch(): sw1 saw the fetch from the worker: /service-workers/service-worker/resources/scope2/simple.txt',
+ 'Case #1: network scope1->scope2');
+
+// request to sw1 scope gets network redirect to out-of-scope
+worker_redirect_test(
+ build_worker_url('network', 'out-scope'),
+ 'resources/worker_interception_redirect_webworker.py',
+ 'the worker script was served from network',
+ 'sw1 saw importScripts from the worker: /service-workers/service-worker/resources/import-scripts-echo.py',
+ 'fetch(): sw1 saw the fetch from the worker: /service-workers/service-worker/resources/simple.txt',
+ 'Case #2: network scope1->out-scope');
+
+// request to sw1 scope gets service-worker redirect to sw2 scope
+worker_redirect_test(
+ build_worker_url('serviceworker', 'scope2'),
+ 'resources/subdir/worker_interception_redirect_webworker.py?greeting=sw2%20saw%20the%20request%20for%20the%20worker%20script',
+ 'sw2 saw the request for the worker script',
+ 'sw2 saw importScripts from the worker: /service-workers/service-worker/resources/subdir/import-scripts-echo.py',
+ 'fetch(): sw2 saw the fetch from the worker: /service-workers/service-worker/resources/subdir/simple.txt',
+ 'Case #3: sw scope1->scope2');
+
+// request to sw1 scope gets service-worker redirect to out-of-scope
+worker_redirect_test(
+ build_worker_url('serviceworker', 'out-scope'),
+ 'resources/worker_interception_redirect_webworker.py',
+ 'the worker script was served from network',
+ 'sw1 saw importScripts from the worker: /service-workers/service-worker/resources/import-scripts-echo.py',
+ 'fetch(): sw1 saw the fetch from the worker: /service-workers/service-worker/resources/simple.txt',
+ 'Case #4: sw scope1->out-scope');
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/worker-interception.https.html b/third_party/web_platform_tests/service-workers/service-worker/worker-interception.https.html
new file mode 100644
index 0000000..27983d8
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/worker-interception.https.html
@@ -0,0 +1,244 @@
+<!DOCTYPE html>
+<title>Service Worker: intercepting Worker script loads</title>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<body>
+<script>
+
+// ========== Worker main resource interception tests ==========
+
+async function setup_service_worker(t, service_worker_url, scope) {
+ const r = await service_worker_unregister_and_register(
+ t, service_worker_url, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+ await wait_for_state(t, r.installing, 'activated');
+ return r.active;
+}
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-synthesized-worker.js?dedicated';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ const serviceWorker = await setup_service_worker(t, service_worker_url, scope);
+
+ const channels = new MessageChannel();
+ serviceWorker.postMessage({port: channels.port1}, [channels.port1]);
+
+ const clientId = await new Promise(resolve => channels.port2.onmessage = (e) => resolve(e.data.id));
+
+ const resultPromise = new Promise(resolve => channels.port2.onmessage = (e) => resolve(e.data));
+
+ const w = new Worker(worker_url);
+ const data = await new Promise((resolve, reject) => {
+ w.onmessage = e => resolve(e.data);
+ w.onerror = e => reject(e.message);
+ });
+ assert_equals(data, 'worker loading intercepted by service worker');
+
+ const results = await resultPromise;
+ assert_equals(results.clientId, clientId);
+ assert_true(!!results.resultingClientId.length);
+
+ channels.port2.postMessage("done");
+}, `Verify a dedicated worker script request gets correct client Ids`);
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-synthesized-worker.js?dedicated';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new Worker(worker_url);
+ const data = await new Promise((resolve, reject) => {
+ w.onmessage = e => resolve(e.data);
+ w.onerror = e => reject(e.message);
+ });
+ assert_equals(data, 'worker loading intercepted by service worker');
+}, `Verify a dedicated worker script request issued from a uncontrolled ` +
+ `document is intercepted by worker's own service worker.`);
+
+promise_test(async t => {
+ const frame_url = 'resources/create-out-of-scope-worker.html';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = frame_url;
+
+ const registration = await service_worker_unregister_and_register(
+ t, service_worker_url, scope);
+ t.add_cleanup(() => service_worker_unregister(t, scope));
+ await wait_for_state(t, registration.installing, 'activated');
+
+ const frame = await with_iframe(frame_url);
+ t.add_cleanup(_ => frame.remove());
+
+ assert_equals(
+ frame.contentWindow.navigator.serviceWorker.controller.scriptURL,
+ get_newest_worker(registration).scriptURL,
+ 'the frame should be controlled by a service worker'
+ );
+
+ const result = await frame.contentWindow.getWorkerPromise();
+
+ assert_equals(result,
+ 'worker loading was not intercepted by service worker');
+}, `Verify an out-of-scope dedicated worker script request issued from a ` +
+ `controlled document should not be intercepted by document's service ` +
+ `worker.`);
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-synthesized-worker.js?shared';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new SharedWorker(worker_url);
+ const data = await new Promise((resolve, reject) => {
+ w.port.onmessage = e => resolve(e.data);
+ w.onerror = e => reject(e.message);
+ });
+ assert_equals(data, 'worker loading intercepted by service worker');
+}, `Verify a shared worker script request issued from a uncontrolled ` +
+ `document is intercepted by worker's own service worker.`);
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-same-origin-worker.js?dedicated';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new Worker(worker_url);
+ const data = await new Promise((resolve, reject) => {
+ w.onmessage = e => resolve(e.data);
+ w.onerror = e => reject(e.message);
+ });
+ assert_equals(data, 'dedicated worker script loaded');
+}, 'Verify a same-origin worker script served by a service worker succeeds ' +
+ 'in starting a dedicated worker.');
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-same-origin-worker.js?shared';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new SharedWorker(worker_url);
+ const data = await new Promise((resolve, reject) => {
+ w.port.onmessage = e => resolve(e.data);
+ w.onerror = e => reject(e.message);
+ });
+ assert_equals(data, 'shared worker script loaded');
+}, 'Verify a same-origin worker script served by a service worker succeeds ' +
+ 'in starting a shared worker.');
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-cors-worker.js?dedicated';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new Worker(worker_url);
+ const watcher = new EventWatcher(t, w, ['message', 'error']);
+ await watcher.wait_for('error');
+}, 'Verify a cors worker script served by a service worker fails dedicated ' +
+ 'worker start.');
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-cors-worker.js?shared';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new SharedWorker(worker_url);
+ const watcher = new EventWatcher(t, w, ['message', 'error']);
+ await watcher.wait_for('error');
+}, 'Verify a cors worker script served by a service worker fails shared ' +
+ 'worker start.');
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-no-cors-worker.js?dedicated';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new Worker(worker_url);
+ const watcher = new EventWatcher(t, w, ['message', 'error']);
+ await watcher.wait_for('error');
+}, 'Verify a no-cors cross-origin worker script served by a service worker ' +
+ 'fails dedicated worker start.');
+
+promise_test(async t => {
+ const worker_url = 'resources/sample-no-cors-worker.js?shared';
+ const service_worker_url = 'resources/sample-worker-interceptor.js';
+ const scope = worker_url;
+
+ await setup_service_worker(t, service_worker_url, scope);
+ const w = new SharedWorker(worker_url);
+ const watcher = new EventWatcher(t, w, ['message', 'error']);
+ await watcher.wait_for('error');
+}, 'Verify a no-cors cross-origin worker script served by a service worker ' +
+ 'fails shared worker start.');
+
+// ========== Worker subresource interception tests ==========
+
+const scope_for_subresource_interception = 'resources/load_worker.js';
+
+promise_test(async t => {
+ const service_worker_url = 'resources/worker-load-interceptor.js';
+ const r = await service_worker_unregister_and_register(
+ t, service_worker_url, scope_for_subresource_interception);
+ await wait_for_state(t, r.installing, 'activated');
+}, 'Register a service worker for worker subresource interception tests.');
+
+// Do not call this function multiple times without waiting for the promise
+// resolution because this sets new event handlers on |worker|.
+// TODO(nhiroki): To isolate multiple function calls, use MessagePort instead of
+// worker's onmessage event handler.
+async function request_on_worker(worker, resource_type) {
+ const data = await new Promise((resolve, reject) => {
+ if (worker instanceof Worker) {
+ worker.onmessage = e => resolve(e.data);
+ worker.onerror = e => reject(e);
+ worker.postMessage(resource_type);
+ } else if (worker instanceof SharedWorker) {
+ worker.port.onmessage = e => resolve(e.data);
+ worker.onerror = e => reject(e);
+ worker.port.postMessage(resource_type);
+ } else {
+ reject('Unexpected worker type!');
+ }
+ });
+ assert_equals(data, 'This load was successfully intercepted.');
+}
+
+async function subresource_test(worker) {
+ await request_on_worker(worker, 'xhr');
+ await request_on_worker(worker, 'fetch');
+ await request_on_worker(worker, 'importScripts');
+}
+
+promise_test(async t => {
+ await subresource_test(new Worker('resources/load_worker.js'));
+}, 'Requests on a dedicated worker controlled by a service worker.');
+
+promise_test(async t => {
+ await subresource_test(new SharedWorker('resources/load_worker.js'));
+}, 'Requests on a shared worker controlled by a service worker.');
+
+promise_test(async t => {
+ await subresource_test(new Worker('resources/nested_load_worker.js'));
+}, 'Requests on a dedicated worker nested in a dedicated worker and ' +
+ 'controlled by a service worker');
+
+promise_test(async t => {
+ await subresource_test(new SharedWorker('resources/nested_load_worker.js'));
+}, 'Requests on a dedicated worker nested in a shared worker and controlled ' +
+ 'by a service worker');
+
+promise_test(async t => {
+ await service_worker_unregister(t, scope_for_subresource_interception);
+}, 'Unregister a service worker for subresource interception tests.');
+
+</script>
+</body>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/xhr-content-length.https.window.js b/third_party/web_platform_tests/service-workers/service-worker/xhr-content-length.https.window.js
new file mode 100644
index 0000000..1ae320e
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/xhr-content-length.https.window.js
@@ -0,0 +1,55 @@
+// META: script=resources/test-helpers.sub.js
+
+let frame;
+
+promise_test(async (t) => {
+ const scope = "resources/empty.html";
+ const script = "resources/xhr-content-length-worker.js";
+ const registration = await service_worker_unregister_and_register(t, script, scope);
+ await wait_for_state(t, registration.installing, "activated");
+ frame = await with_iframe(scope);
+}, "Setup");
+
+promise_test(async t => {
+ const xhr = new frame.contentWindow.XMLHttpRequest();
+ xhr.open("GET", "test?type=no-content-length");
+ xhr.send();
+ const event = await new Promise(resolve => xhr.onload = resolve);
+ assert_equals(xhr.getResponseHeader("content-length"), null);
+ assert_false(event.lengthComputable);
+ assert_equals(event.total, 0);
+ assert_equals(event.loaded, xhr.responseText.length);
+}, `Synthetic response without Content-Length header`);
+
+promise_test(async t => {
+ const xhr = new frame.contentWindow.XMLHttpRequest();
+ xhr.open("GET", "test?type=larger-content-length");
+ xhr.send();
+ const event = await new Promise(resolve => xhr.onload = resolve);
+ assert_equals(xhr.getResponseHeader("content-length"), "10000");
+ assert_true(event.lengthComputable);
+ assert_equals(event.total, 10000);
+ assert_equals(event.loaded, xhr.responseText.length);
+}, `Synthetic response with Content-Length header with value larger than response body length`);
+
+promise_test(async t => {
+ const xhr = new frame.contentWindow.XMLHttpRequest();
+ xhr.open("GET", "test?type=double-content-length");
+ xhr.send();
+ const event = await new Promise(resolve => xhr.onload = resolve);
+ assert_equals(xhr.getResponseHeader("content-length"), "10000, 10000");
+ assert_true(event.lengthComputable);
+ assert_equals(event.total, 10000);
+ assert_equals(event.loaded, xhr.responseText.length);
+}, `Synthetic response with two Content-Length headers value larger than response body length`);
+
+promise_test(async t => {
+ const xhr = new frame.contentWindow.XMLHttpRequest();
+ xhr.open("GET", "test?type=bogus-content-length");
+ xhr.send();
+ const event = await new Promise(resolve => xhr.onload = resolve);
+ assert_equals(xhr.getResponseHeader("content-length"), "test");
+ assert_false(event.lengthComputable);
+ assert_equals(event.total, 0);
+ assert_equals(event.loaded, xhr.responseText.length);
+}, `Synthetic response with bogus Content-Length header`);
diff --git a/third_party/web_platform_tests/service-workers/service-worker/xhr-response-url.https.html b/third_party/web_platform_tests/service-workers/service-worker/xhr-response-url.https.html
new file mode 100644
index 0000000..673ca52
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/xhr-response-url.https.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: XHR responseURL uses the response url</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script>
+const scope = 'resources/xhr-iframe.html';
+const script = 'resources/xhr-response-url-worker.js';
+let iframe;
+
+function build_url(options) {
+ const url = new URL('test', window.location);
+ const opts = options ? options : {};
+ if (opts.respondWith)
+ url.searchParams.set('respondWith', opts.respondWith);
+ if (opts.url)
+ url.searchParams.set('url', opts.url.href);
+ return url.href;
+}
+
+promise_test(async (t) => {
+ const registration =
+ await service_worker_unregister_and_register(t, script, scope);
+ await wait_for_state(t, registration.installing, 'activated');
+ iframe = await with_iframe(scope);
+}, 'global setup');
+
+// Test that XMLHttpRequest.responseURL uses the response URL from the service
+// worker.
+promise_test(async (t) => {
+ // Build a URL that tells the service worker to respondWith(fetch(|target|)).
+ const target = new URL('resources/sample.txt', window.location);
+ const url = build_url({
+ respondWith: 'fetch',
+ url: target
+ });
+
+ // Perform the XHR.
+ const xhr = await iframe.contentWindow.xhr(url);
+ assert_equals(xhr.responseURL, target.href, 'responseURL');
+}, 'XHR responseURL should be the response URL');
+
+// Same as above with a generated response.
+promise_test(async (t) => {
+ // Build a URL that tells the service worker to respondWith(new Response()).
+ const url = build_url({respondWith: 'string'});
+
+ // Perform the XHR.
+ const xhr = await iframe.contentWindow.xhr(url);
+ assert_equals(xhr.responseURL, url, 'responseURL');
+}, 'XHR responseURL should be the response URL (generated response)');
+
+// Test that XMLHttpRequest.responseXML is a Document whose URL is the
+// response URL from the service worker.
+promise_test(async (t) => {
+ // Build a URL that tells the service worker to respondWith(fetch(|target|)).
+ const target = new URL('resources/blank.html', window.location);
+ const url = build_url({
+ respondWith: 'fetch',
+ url: target
+ });
+
+ // Perform the XHR.
+ const xhr = await iframe.contentWindow.xhr(url, {responseType: 'document'});
+ assert_equals(xhr.responseURL, target.href, 'responseURL');
+
+ // The document's URL uses the response URL:
+ // "Set |document|’s URL to |response|’s url."
+ // https://xhr.spec.whatwg.org/#document-response
+ assert_equals(xhr.responseXML.URL, target.href, 'responseXML.URL');
+
+ // The document's base URL falls back to the document URL:
+ // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#document-base-url
+ assert_equals(xhr.responseXML.baseURI, target.href, 'responseXML.baseURI');
+}, 'XHR Document should use the response URL');
+
+// Same as above with a generated response from the service worker.
+promise_test(async (t) => {
+ // Build a URL that tells the service worker to
+ // respondWith(new Response()) with a document response.
+ const url = build_url({respondWith: 'document'});
+
+ // Perform the XHR.
+ const xhr = await iframe.contentWindow.xhr(url, {responseType: 'document'});
+ assert_equals(xhr.responseURL, url, 'responseURL');
+
+ // The document's URL uses the response URL, which is the request URL:
+ // "Set |document|’s URL to |response|’s url."
+ // https://xhr.spec.whatwg.org/#document-response
+ assert_equals(xhr.responseXML.URL, url, 'responseXML.URL');
+
+ // The document's base URL falls back to the document URL:
+ // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#document-base-url
+ assert_equals(xhr.responseXML.baseURI, url, 'responseXML.baseURI');
+}, 'XHR Document should use the response URL (generated response)');
+
+promise_test(async (t) => {
+ if (iframe)
+ iframe.remove();
+ await service_worker_unregister(t, scope);
+}, 'global cleanup');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-worker/xsl-base-url.https.html b/third_party/web_platform_tests/service-workers/service-worker/xsl-base-url.https.html
new file mode 100644
index 0000000..1d3c364
--- /dev/null
+++ b/third_party/web_platform_tests/service-workers/service-worker/xsl-base-url.https.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Service Worker: XSL's base URL must be the response URL</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<script>
+// This test loads an XML document which is controlled a service worker. The
+// document loads a stylesheet and a service worker responds with another URL.
+// The stylesheet imports a relative URL to test that the base URL is the
+// response URL from the service worker.
+promise_test(async (t) => {
+ const SCOPE = 'resources/xsl-base-url-iframe.xml';
+ const SCRIPT = 'resources/xsl-base-url-worker.js';
+ let worker;
+ let frame;
+
+ t.add_cleanup(() => {
+ if (frame)
+ frame.remove();
+ service_worker_unregister(t, SCOPE);
+ });
+
+ const registration = await service_worker_unregister_and_register(
+ t, SCRIPT, SCOPE);
+ worker = registration.installing;
+ await wait_for_state(t, worker, 'activated');
+
+ frame = await with_iframe(SCOPE);
+ assert_equals(frame.contentDocument.body.textContent, 'PASS');
+}, 'base URL when service worker does respondWith(fetch(responseUrl))');
+</script>
diff --git a/third_party/web_platform_tests/service-workers/service-workers/resources/test-helpers.js b/third_party/web_platform_tests/service-workers/service-workers/resources/test-helpers.js
deleted file mode 100644
index 147ea61..0000000
--- a/third_party/web_platform_tests/service-workers/service-workers/resources/test-helpers.js
+++ /dev/null
@@ -1,222 +0,0 @@
-// Adapter for testharness.js-style tests with Service Workers
-
-function service_worker_unregister_and_register(test, url, scope) {
- if (!scope || scope.length == 0)
- return Promise.reject(new Error('tests must define a scope'));
-
- var options = { scope: scope };
- return service_worker_unregister(test, scope)
- .then(function() {
- return navigator.serviceWorker.register(url, options);
- })
- .catch(unreached_rejection(test,
- 'unregister and register should not fail'));
-}
-
-function service_worker_unregister(test, documentUrl) {
- return navigator.serviceWorker.getRegistration(documentUrl)
- .then(function(registration) {
- if (registration)
- return registration.unregister();
- })
- .catch(unreached_rejection(test, 'unregister should not fail'));
-}
-
-function service_worker_unregister_and_done(test, scope) {
- return service_worker_unregister(test, scope)
- .then(test.done.bind(test));
-}
-
-function unreached_fulfillment(test, prefix) {
- return test.step_func(function(result) {
- var error_prefix = prefix || 'unexpected fulfillment';
- assert_unreached(error_prefix + ': ' + result);
- });
-}
-
-// Rejection-specific helper that provides more details
-function unreached_rejection(test, prefix) {
- return test.step_func(function(error) {
- var reason = error.message || error.name || error;
- var error_prefix = prefix || 'unexpected rejection';
- assert_unreached(error_prefix + ': ' + reason);
- });
-}
-
-// Adds an iframe to the document and returns a promise that resolves to the
-// iframe when it finishes loading. The caller is responsible for removing the
-// iframe later if needed.
-function with_iframe(url) {
- return new Promise(function(resolve) {
- var frame = document.createElement('iframe');
- frame.src = url;
- frame.onload = function() { resolve(frame); };
- document.body.appendChild(frame);
- });
-}
-
-function normalizeURL(url) {
- return new URL(url, self.location).toString().replace(/#.*$/, '');
-}
-
-function wait_for_update(test, registration) {
- if (!registration || registration.unregister == undefined) {
- return Promise.reject(new Error(
- 'wait_for_update must be passed a ServiceWorkerRegistration'));
- }
-
- return new Promise(test.step_func(function(resolve) {
- registration.addEventListener('updatefound', test.step_func(function() {
- resolve(registration.installing);
- }));
- }));
-}
-
-function wait_for_state(test, worker, state) {
- if (!worker || worker.state == undefined) {
- return Promise.reject(new Error(
- 'wait_for_state must be passed a ServiceWorker'));
- }
- if (worker.state === state)
- return Promise.resolve(state);
-
- if (state === 'installing') {
- switch (worker.state) {
- case 'installed':
- case 'activating':
- case 'activated':
- case 'redundant':
- return Promise.reject(new Error(
- 'worker is ' + worker.state + ' but waiting for ' + state));
- }
- }
-
- if (state === 'installed') {
- switch (worker.state) {
- case 'activating':
- case 'activated':
- case 'redundant':
- return Promise.reject(new Error(
- 'worker is ' + worker.state + ' but waiting for ' + state));
- }
- }
-
- if (state === 'activating') {
- switch (worker.state) {
- case 'activated':
- case 'redundant':
- return Promise.reject(new Error(
- 'worker is ' + worker.state + ' but waiting for ' + state));
- }
- }
-
- if (state === 'activated') {
- switch (worker.state) {
- case 'redundant':
- return Promise.reject(new Error(
- 'worker is ' + worker.state + ' but waiting for ' + state));
- }
- }
-
- return new Promise(test.step_func(function(resolve) {
- worker.addEventListener('statechange', test.step_func(function() {
- if (worker.state === state)
- resolve(state);
- }));
- }));
-}
-
-// Declare a test that runs entirely in the ServiceWorkerGlobalScope. The |url|
-// is the service worker script URL. This function:
-// - Instantiates a new test with the description specified in |description|.
-// The test will succeed if the specified service worker can be successfully
-// registered and installed.
-// - Creates a new ServiceWorker registration with a scope unique to the current
-// document URL. Note that this doesn't allow more than one
-// service_worker_test() to be run from the same document.
-// - Waits for the new worker to begin installing.
-// - Imports tests results from tests running inside the ServiceWorker.
-function service_worker_test(url, description) {
- // If the document URL is https://example.com/document and the script URL is
- // https://example.com/script/worker.js, then the scope would be
- // https://example.com/script/scope/document.
- var scope = new URL('scope' + window.location.pathname,
- new URL(url, window.location)).toString();
- promise_test(function(test) {
- return service_worker_unregister_and_register(test, url, scope)
- .then(function(registration) {
- add_completion_callback(function() {
- registration.unregister();
- });
- return wait_for_update(test, registration)
- .then(function(worker) {
- return fetch_tests_from_worker(worker);
- });
- });
- }, description);
-}
-
-function get_host_info() {
- var ORIGINAL_HOST = '127.0.0.1';
- var REMOTE_HOST = 'localhost';
- var UNAUTHENTICATED_HOST = 'example.test';
- var HTTP_PORT = 8000;
- var HTTPS_PORT = 8443;
- try {
- // In W3C test, we can get the hostname and port number in config.json
- // using wptserve's built-in pipe.
- // http://wptserve.readthedocs.org/en/latest/pipes.html#built-in-pipes
- HTTP_PORT = eval('{{ports[http][0]}}');
- HTTPS_PORT = eval('{{ports[https][0]}}');
- ORIGINAL_HOST = eval('\'{{host}}\'');
- REMOTE_HOST = 'www1.' + ORIGINAL_HOST;
- } catch (e) {
- }
- return {
- HTTP_ORIGIN: 'http://' + ORIGINAL_HOST + ':' + HTTP_PORT,
- HTTPS_ORIGIN: 'https://' + ORIGINAL_HOST + ':' + HTTPS_PORT,
- HTTP_REMOTE_ORIGIN: 'http://' + REMOTE_HOST + ':' + HTTP_PORT,
- HTTPS_REMOTE_ORIGIN: 'https://' + REMOTE_HOST + ':' + HTTPS_PORT,
- UNAUTHENTICATED_ORIGIN: 'http://' + UNAUTHENTICATED_HOST + ':' + HTTP_PORT
- };
-}
-
-function base_path() {
- return location.pathname.replace(/\/[^\/]*$/, '/');
-}
-
-function test_login(test, origin, username, password, cookie) {
- return new Promise(function(resolve, reject) {
- with_iframe(
- origin +
- '/serviceworker/resources/fetch-access-control-login.html')
- .then(test.step_func(function(frame) {
- var channel = new MessageChannel();
- channel.port1.onmessage = test.step_func(function() {
- frame.remove();
- resolve();
- });
- frame.contentWindow.postMessage(
- {username: username, password: password, cookie: cookie},
- origin, [channel.port2]);
- }));
- });
-}
-
-function login(test) {
- return test_login(test, 'http://127.0.0.1:8000',
- 'username1', 'password1', 'cookie1')
- .then(function() {
- return test_login(test, 'http://localhost:8000',
- 'username2', 'password2', 'cookie2');
- });
-}
-
-function login_https(test) {
- return test_login(test, 'https://127.0.0.1:8443',
- 'username1s', 'password1s', 'cookie1')
- .then(function() {
- return test_login(test, 'https://localhost:8443',
- 'username2s', 'password2s', 'cookie2');
- });
-}
diff --git a/third_party/web_platform_tests/service-workers/specgen.json b/third_party/web_platform_tests/service-workers/specgen.json
deleted file mode 100644
index 5d76da8..0000000
--- a/third_party/web_platform_tests/service-workers/specgen.json
+++ /dev/null
@@ -1,658 +0,0 @@
-{
- "sections": [
- {
- "href": "#introduction",
- "id": "introduction",
- "hash": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
- "secno": "1",
- "testable": false
- },
- {
- "href": "#about",
- "id": "about",
- "hash": "8d3cf149aa73cff52328509ebbaffd933e8fb6af",
- "secno": "1.1",
- "testable": false
- },
- {
- "href": "#dependencies",
- "id": "dependencies",
- "hash": "1355f2d7ec9bf4e617ee632c0db44f834c96435b",
- "secno": "1.2",
- "testable": false
- },
- {
- "href": "#motivations",
- "id": "motivations",
- "hash": "92d899bc1e63a170d2324638d16f580b97b4f4d6",
- "secno": "1.3",
- "testable": false
- },
- {
- "href": "#concepts",
- "id": "concepts",
- "hash": "589023372dc033b0a77be1cd01f54f5f8c3ebfa8",
- "secno": "2",
- "testable": false
- },
- {
- "href": "#document-context",
- "id": "document-context",
- "hash": "34feeb18dea978349a2f76e6b17c127123b3db74",
- "secno": "3",
- "testable": false
- },
- {
- "href": "#service-worker-obj",
- "id": "service-worker-obj",
- "hash": "6cbd0107199072ab86b36e72d08d5465b42e6da8",
- "secno": "3.1",
- "testPageHash": "8dbbc9aa4300f0203524f3e405dbf7ca462e7164",
- "testPagePath": "stub-3.1-service-worker-obj.html",
- "testable": true
- },
- {
- "href": "#service-worker-scope",
- "id": "service-worker-scope",
- "hash": "136f25ef227515a7be9b32c44967f68b34ad8924",
- "secno": "3.1.1",
- "testPageHash": "965a00b32d56192330aa9f6337072bb3633ad382",
- "testPagePath": "stub-3.1.1-service-worker-scope.html",
- "testable": true
- },
- {
- "href": "#service-worker-url",
- "id": "service-worker-url",
- "hash": "df66a1b4b3bfa3e7ab96fd491a6829fab1d18a88",
- "secno": "3.1.2",
- "testPageHash": "92f6aed1437bb39c5941b495ac6c5f342c025b38",
- "testPagePath": "stub-3.1.2-service-worker-url.html",
- "testable": true
- },
- {
- "href": "#service-worker-state",
- "id": "service-worker-state",
- "hash": "8f80f2b4cbb1c228867c9dd90c05cbecfc92dd77",
- "secno": "3.1.3",
- "testPageHash": "4aad1dc47572879fdc2c79a814ad21e1ef9a64ec",
- "testPagePath": "stub-3.1.3-service-worker-state.html",
- "testable": true
- },
- {
- "href": "#service-worker-on-state-change",
- "id": "service-worker-on-state-change",
- "hash": "0f8fd9d1431deacea72fe739f42992ab5a396bf2",
- "secno": "3.1.4",
- "testPageHash": "6bb309bccc1e7c74ade7fc4c6e400bafb60daceb",
- "testPagePath": "stub-3.1.4-service-worker-on-state-change.html",
- "testable": true
- },
- {
- "href": "#navigator-service-worker",
- "id": "navigator-service-worker",
- "hash": "22f1ebbafca6976d0f4814b0fbb8f173bf919f06",
- "secno": "3.2",
- "testPageHash": "6d597735816a09ec774150029ed5136198f52ab7",
- "testPagePath": "stub-3.2-navigator-service-worker.html",
- "testable": true
- },
- {
- "href": "#navigator-service-worker-installing",
- "id": "navigator-service-worker-installing",
- "hash": "9675c3cdf5ba4b4155284e06a19e4de631645509",
- "secno": "3.2.1",
- "testPageHash": "2c8e56e74c130104e395de46bad20fb5d3021d95",
- "testPagePath": "stub-3.2.1-navigator-service-worker-installing.html",
- "testable": true
- },
- {
- "href": "#navigator-service-worker-waiting",
- "id": "navigator-service-worker-waiting",
- "hash": "88b4db92cc49109e6a15ddebdd219690d9648e76",
- "secno": "3.2.2",
- "testPageHash": "1cf6ed58bf5ecf963fed8c3d9211b853dab564e2",
- "testPagePath": "stub-3.2.2-navigator-service-worker-waiting.html",
- "testable": true
- },
- {
- "href": "#navigator-service-worker-active",
- "id": "navigator-service-worker-active",
- "hash": "0da48e885c77da60d1837197780049904789e3cb",
- "secno": "3.2.3",
- "testPageHash": "f5dca8c6eb5f29a0f9a5f06e25861e7f3106cc67",
- "testPagePath": "stub-3.2.3-navigator-service-worker-active.html",
- "testable": true
- },
- {
- "href": "#navigator-service-worker-controller",
- "id": "navigator-service-worker-controller",
- "hash": "293433ccb7bb2a22d8d5a81e788892e071b25e65",
- "secno": "3.2.4",
- "testPageHash": "6452f431d0765d7aa3135d18fee43e6664dcbb12",
- "testPagePath": "stub-3.2.4-navigator-service-worker-controller.html",
- "testable": true
- },
- {
- "href": "#navigator-service-worker-ready",
- "id": "navigator-service-worker-ready",
- "hash": "6240fde8d7168beeb95f4f36aa9e143319b2061b",
- "secno": "3.2.5",
- "testPageHash": "ae4fd694c88bab72f338d97bf96b7d23e2e83e87",
- "testPagePath": "stub-3.2.5-navigator-service-worker-ready.html",
- "testable": true
- },
- {
- "href": "#navigator-service-worker-getAll",
- "id": "navigator-service-worker-getAll",
- "hash": "292ee3af2cc8fadc24302446809d04bf2e9811a5",
- "secno": "3.2.6",
- "testPageHash": "4096ae712cc3e753456fbe05bb4d0cfc4399d2c9",
- "testPagePath": "stub-3.2.6-navigator-service-worker-getAll.html",
- "testable": true
- },
- {
- "href": "#navigator-service-worker-register",
- "id": "navigator-service-worker-register",
- "hash": "c999dc5f67126c9f0f02b25fd943a34b48cff618",
- "secno": "3.2.7",
- "testPageHash": "bde900b97dbb08b053ff8115775ea3b79a124b6e",
- "testPagePath": "stub-3.2.7-navigator-service-worker-register.html",
- "testable": true
- },
- {
- "href": "#navigator-service-worker-unregister",
- "id": "navigator-service-worker-unregister",
- "hash": "fd196f926f181563855e4683cc995405c1e611d0",
- "secno": "3.2.8",
- "testPageHash": "dbd99a1dcbcb629431617790a305e840495049eb",
- "testPagePath": "stub-3.2.8-navigator-service-worker-unregister.html",
- "testable": true
- },
- {
- "href": "#navigator-service-worker-onupdatefound",
- "id": "navigator-service-worker-onupdatefound",
- "hash": "2bb5aabaca24a68f9e6b4c4443968178eb1ccfe8",
- "secno": "3.2.9",
- "testPageHash": "eef0c1c39577abefb3654a6e9917ff2da657871b",
- "testPagePath": "stub-3.2.9-navigator-service-worker-onupdatefound.html",
- "testable": true
- },
- {
- "href": "#navigator-service-worker-oncontrollerchange",
- "id": "navigator-service-worker-oncontrollerchange",
- "hash": "c89a4ffba10d9285e07c38c28718719d87053994",
- "secno": "3.2.10",
- "testPageHash": "35e0ce2b8f4527ebbd75d4dfa3436fd7f8c79792",
- "testPagePath": "stub-3.2.10-navigator-service-worker-oncontrollerchange.html",
- "testable": true
- },
- {
- "href": "#navigator-service-worker-onreloadpage",
- "id": "navigator-service-worker-onreloadpage",
- "hash": "424441910abf2e1bdc3db658fe46827f7abe60a4",
- "secno": "3.2.11",
- "testPageHash": "ae614de17e5f339b65f77cafa6e0f5625491abfb",
- "testPagePath": "stub-3.2.11-navigator-service-worker-onreloadpage.html",
- "testable": true
- },
- {
- "href": "#navigator-service-worker-onerror",
- "id": "navigator-service-worker-onerror",
- "hash": "710f7fcd2f5340147b9e030bc5932b8242cef828",
- "secno": "3.2.12",
- "testPageHash": "cd62779e27151d55f14ac6ab7aa41dcf723e0ac7",
- "testPagePath": "stub-3.2.12-navigator-service-worker-onerror.html",
- "testable": true
- },
- {
- "href": "#execution-context",
- "id": "execution-context",
- "hash": "ddf24f0adf58237e264c3c43cb7ab07af3013c9d",
- "secno": "4",
- "testable": false
- },
- {
- "href": "#service-worker-global-scope",
- "id": "service-worker-global-scope",
- "hash": "e6b8bb7f99c125f4226fc5b6c51cf03a7437f2ef",
- "secno": "4.1",
- "testPageHash": "2f596b6b07bcfb71c01d75f725eb52c84e9c69dd",
- "testPagePath": "stub-4.1-service-worker-global-scope.html",
- "testable": true
- },
- {
- "href": "#service-worker-global-scope-caches",
- "id": "service-worker-global-scope-caches",
- "hash": "43d3c9f441b3a7abd0d3a7f55d93faaceeb7d97d",
- "secno": "4.1.1",
- "testPageHash": "f19b91c887f6312688b66b1988147a599cd9470f",
- "testPagePath": "stub-4.1.1-service-worker-global-scope-caches.html",
- "testable": true
- },
- {
- "href": "#service-worker-global-scope-clients",
- "id": "service-worker-global-scope-clients",
- "hash": "cb83230107645229da9776ed0fc9f7bc6fcce747",
- "secno": "4.1.2",
- "testPageHash": "45b3aae572f7161748fa98e97b4f2b738c3dcfef",
- "testPagePath": "stub-4.1.2-service-worker-global-scope-clients.html",
- "testable": true
- },
- {
- "href": "#service-worker-global-scope-scope",
- "id": "service-worker-global-scope-scope",
- "hash": "08c808048b647aa9d4cc0b0a0f70b06ca89af4a3",
- "secno": "4.1.3",
- "testPageHash": "bfe7eaf8deb8de7d2ccfbba97640478b1c81d6c7",
- "testPagePath": "stub-4.1.3-service-worker-global-scope-scope.html",
- "testable": true
- },
- {
- "href": "#service-worker-global-scope-fetch",
- "id": "service-worker-global-scope-fetch",
- "hash": "b66133d8a2c67f9b10c274d5b05383ff76d2cd42",
- "secno": "4.1.4",
- "testPageHash": "2b1ffa915afddeb099dfff23f4ecf555b0710ed4",
- "testPagePath": "stub-4.1.4-service-worker-global-scope-fetch.html",
- "testable": true
- },
- {
- "href": "#service-worker-global-scope-update",
- "id": "service-worker-global-scope-update",
- "hash": "3ddf48cecb4d4a67a329248787dd220ce17f4eff",
- "secno": "4.1.5",
- "testPageHash": "15879bf45f460c0ab0c02793655096c1bca418a7",
- "testPagePath": "stub-4.1.5-service-worker-global-scope-update.html",
- "testable": true
- },
- {
- "href": "#service-worker-global-scope-unregister",
- "id": "service-worker-global-scope-unregister",
- "hash": "fff9ef2daa5689b38a17eeb9a6bd7071098ca778",
- "secno": "4.1.6",
- "testPageHash": "c4bf327228628b794db9c6f2eb17519e37cea6b9",
- "testPagePath": "stub-4.1.6-service-worker-global-scope-unregister.html",
- "testable": true
- },
- {
- "href": "#service-worker-global-scope-onmessage",
- "id": "service-worker-global-scope-onmessage",
- "hash": "bc8f6aed2d515dc7f6b0757afa02f37899082668",
- "secno": "4.1.7",
- "testPageHash": "9e6f2732d21871ec06e9541ea881baf962f7cdf4",
- "testPagePath": "stub-4.1.7-service-worker-global-scope-onmessage.html",
- "testable": true
- },
- {
- "href": "#client",
- "id": "client",
- "hash": "47a1c10cd9e4db9a5c86d9bcf80477f771ea954c",
- "secno": "4.2",
- "testPageHash": "21d74c1af0b3176b029c9b62b37fe73436e0f197",
- "testPagePath": "stub-4.2-client.html",
- "testable": true
- },
- {
- "href": "#service-worker-clients",
- "id": "service-worker-clients",
- "hash": "c2c6f4873f07b53705a46b2bd44ba10f84dd2b56",
- "secno": "4.3",
- "testPageHash": "9c0366e6cfd28caaeaf940bad2b3c7ace93037f6",
- "testPagePath": "stub-4.3-service-worker-clients.html",
- "testable": true
- },
- {
- "href": "#get-serviced-method",
- "id": "get-serviced-method",
- "hash": "299abaa21cf096e423edfa19755987986f742a1f",
- "secno": "4.3.1",
- "testPageHash": "efeb1c2dc8144c30e6628cb56b3e532531ee1e88",
- "testPagePath": "stub-4.3.1-get-serviced-method.html",
- "testable": true
- },
- {
- "href": "#reloadall-method",
- "id": "reloadall-method",
- "hash": "bb4d775d261e69cbeaf65c123e949c24cf542ae7",
- "secno": "4.3.2",
- "testPageHash": "d1a4dde873b77201b4de745d2083bf63549b0b8b",
- "testPagePath": "stub-4.3.2-reloadall-method.html",
- "testable": true
- },
- {
- "href": "#request-objects",
- "id": "request-objects",
- "hash": "65ae6c08f720a2eedb7b140f5635a5ac46ddadfc",
- "secno": "4.4",
- "testPageHash": "ec493c70e8a0d8d3eeb0ecaef59610aed97d298e",
- "testPagePath": "stub-4.4-request-objects.html",
- "testable": true
- },
- {
- "href": "#response-objects",
- "id": "response-objects",
- "hash": "2efbff63c70ab92f93e4acd021409b9df4776882",
- "secno": "4.5",
- "testPageHash": "8340b69d62f111f56095c5fe9047d9215fa7aefc",
- "testPagePath": "stub-4.5-response-objects.html",
- "testable": true
- },
- {
- "href": "#abstract-response",
- "id": "abstract-response",
- "hash": "bddc306a9892c0bca43e8b361c1ee22b87759e23",
- "secno": "4.5.1",
- "testable": false
- },
- {
- "href": "#response",
- "id": "response",
- "hash": "6471d25755bdab0d4f72413f9367b7bb36c53a6f",
- "secno": "4.5.2",
- "testPageHash": "346d63cc7eb8ee412f5f704ba241205c8d437540",
- "testPagePath": "stub-4.5.2-response.html",
- "testable": true
- },
- {
- "href": "#header",
- "id": "header",
- "hash": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
- "secno": "4.5.3",
- "testable": false
- },
- {
- "href": "#opaque-response",
- "id": "opaque-response",
- "hash": "df5431f4fbd26d81f2d4f567309c6a7a26dbfd4a",
- "secno": "4.5.4",
- "testPageHash": "85373f290cf594f0f09eb0a76bc6ef6299be595f",
- "testPagePath": "stub-4.5.4-opaque-response.html",
- "testable": true
- },
- {
- "href": "#cors-response",
- "id": "cors-response",
- "hash": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
- "secno": "4.5.5",
- "testable": false
- },
- {
- "href": "#cache-objects",
- "id": "cache-objects",
- "hash": "001d0dfb8fbcbcb6443d1be2b722c9a84d6fd95b",
- "secno": "4.6",
- "testPageHash": "c1ef341d15a8c76d015eef57842ed10e62c02927",
- "testPagePath": "stub-4.6-cache-objects.html",
- "testable": true
- },
- {
- "href": "#cache-lifetimes",
- "id": "cache-lifetimes",
- "hash": "7c73698ca9b686a0314ddf368bf8ad4ca6af392f",
- "secno": "4.6.1",
- "testPageHash": "f3524320a98f2fbdc5d711de82770957a7f5ec4b",
- "testPagePath": "stub-4.6.1-cache-lifetimes.html",
- "testable": true
- },
- {
- "href": "#cache",
- "id": "cache",
- "hash": "bf1fe844577ab57a60eb550be24335a3321ca2ee",
- "secno": "4.6.2",
- "testPageHash": "c55b7b05c8e2f4b65722e16cdbcd78ffdfe1e4bf",
- "testPagePath": "stub-4.6.2-cache.html",
- "testable": true
- },
- {
- "href": "#cache-storage",
- "id": "cache-storage",
- "hash": "9cdaac070f56e55d66a89cd4b6e669a04aa73b82",
- "secno": "4.6.3",
- "testPageHash": "ee6902f170d94cc1e3a4a00f4c90e7e19c4dff95",
- "testPagePath": "stub-4.6.3-cache-storage.html",
- "testable": true
- },
- {
- "href": "#events",
- "id": "events",
- "hash": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
- "secno": "4.7",
- "testable": false
- },
- {
- "href": "#install-phase-event",
- "id": "install-phase-event",
- "hash": "8495382b418adbbed436b2002ab0155a3a295ef2",
- "secno": "4.7.1",
- "testPageHash": "e48e98d51936bd57d21903615203f2b78d3f4b12",
- "testPagePath": "stub-4.7.1-install-phase-event.html",
- "testable": true
- },
- {
- "href": "#wait-until-method",
- "id": "wait-until-method",
- "hash": "295fb5d4932396fd13365ed2fe57aa672f1f2a56",
- "secno": "4.7.1.1",
- "testPageHash": "c3769e51852b8438a97c39c50fa62351a73c4ee6",
- "testPagePath": "stub-4.7.1.1-wait-until-method.html",
- "testable": true
- },
- {
- "href": "#install-event",
- "id": "install-event",
- "hash": "3a0f6da1771c22ab21ddc00729433a4d95ac6782",
- "secno": "4.7.2",
- "testPageHash": "9a103cc461eaca3da75db583ce08f13ecd2b1a98",
- "testPagePath": "stub-4.7.2-install-event.html",
- "testable": true
- },
- {
- "href": "#install-event-section",
- "id": "install-event-section",
- "hash": "4631577df2efc1a4350000461629bc1ca93dbd14",
- "secno": "4.7.2.1",
- "testPageHash": "32f54e74bef784d2f0ac772b44abeee06573062d",
- "testPagePath": "stub-4.7.2.1-install-event-section.html",
- "testable": true
- },
- {
- "href": "#replace-method",
- "id": "replace-method",
- "hash": "b9093b05204d09748311023b4c737ede02ff8115",
- "secno": "4.7.2.2",
- "testPageHash": "372bed923f8c35c4923634ae27fa121919ac0fec",
- "testPagePath": "stub-4.7.2.2-replace-method.html",
- "testable": true
- },
- {
- "href": "#activate-event",
- "id": "activate-event",
- "hash": "ac3d03aa0ed961fb1122850aeab92c302c55ecd0",
- "secno": "4.7.3",
- "testPageHash": "6241762ab1d6f430fa9b7cc8f02a00e6591c6bc6",
- "testPagePath": "stub-4.7.3-activate-event.html",
- "testable": true
- },
- {
- "href": "#fetch-event",
- "id": "fetch-event",
- "hash": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
- "secno": "4.7.4",
- "testable": false
- },
- {
- "href": "#fetch-event-section",
- "id": "fetch-event-section",
- "hash": "ae24fda9664a3bd7b7fe2a8712ac469c3ee7128e",
- "secno": "4.7.4.1",
- "testPageHash": "393fc7b65e9f5afd18da666b6b206ccd639397cd",
- "testPagePath": "stub-4.7.4.1-fetch-event-section.html",
- "testable": true
- },
- {
- "href": "#respond-with-method",
- "id": "respond-with-method",
- "hash": "7e4f010e2ec1ea0500b435cf599ba58942164457",
- "secno": "4.7.4.2",
- "testPageHash": "31e0acd058b9a5b722ae9f405b50bc94d31596b8",
- "testPagePath": "stub-4.7.4.2-respond-with-method.html",
- "testable": true
- },
- {
- "href": "#default-method",
- "id": "default-method",
- "hash": "4d6f8f93b2e10ab0e486dbf464ff107ec1a6aa4c",
- "secno": "4.7.4.3",
- "testPageHash": "34e015c973887e2b3bf8b6db62f75d5d417a43cc",
- "testPagePath": "stub-4.7.4.3-default-method.html",
- "testable": true
- },
- {
- "href": "#is-reload-attribute",
- "id": "is-reload-attribute",
- "hash": "6e1afd9e8940e9cd38aa7de1ed57e8c5b1a60e3d",
- "secno": "4.7.4.4",
- "testPageHash": "703a6469782d37be3c25e2214f897d1064acca47",
- "testPagePath": "stub-4.7.4.4-is-reload-attribute.html",
- "testable": true
- },
- {
- "href": "#security-considerations",
- "id": "security-considerations",
- "hash": "5b02b143172647dd7f74f0464dffa7ec7d0e8f94",
- "secno": "5",
- "testable": false
- },
- {
- "href": "#origin-relativity",
- "id": "origin-relativity",
- "hash": "72bbbd7d3d43a859af6ff9f19353210ddfcc26de",
- "secno": "5.1",
- "testPageHash": "1c92607dfac57b0f59654d059a4a67e0f984b84d",
- "testPagePath": "stub-5.1-origin-relativity.html",
- "testable": true
- },
- {
- "href": "#cross-origin-resources",
- "id": "cross-origin-resources",
- "hash": "6176879ecfb5ae769679ceef4ee1e8889be8df92",
- "secno": "5.2",
- "testPageHash": "bcf85ba278c70c086645c416cee729ce753bc528",
- "testPagePath": "stub-5.2-cross-origin-resources.html",
- "testable": true
- },
- {
- "href": "#storage-considerations",
- "id": "storage-considerations",
- "hash": "e101cee2062749b1a73086492377458251a5e875",
- "secno": "6",
- "testable": false
- },
- {
- "href": "#extensibility",
- "id": "extensibility",
- "hash": "ef1b382bb89c52e01edad421b02b237765a21ce7",
- "secno": "7",
- "testable": false
- },
- {
- "href": "#algorithms",
- "id": "algorithms",
- "hash": "d130247eab1d368efea646ff369e65f6c0c19481",
- "secno": "8",
- "testable": false
- },
- {
- "href": "#registration-algorithm",
- "id": "registration-algorithm",
- "hash": "b688d090671c08ca17ea7cadc561e6d471ee099e",
- "secno": "8.1",
- "testable": false
- },
- {
- "href": "#update-algorithm",
- "id": "update-algorithm",
- "hash": "679a19fef428affc83103c1eec0dbd3be40c4e2a",
- "secno": "8.2",
- "testable": false
- },
- {
- "href": "#soft-update-algorithm",
- "id": "soft-update-algorithm",
- "hash": "8eb103f5cd0e595ee5e25f075e8c6239211e482a",
- "secno": "8.3",
- "testable": false
- },
- {
- "href": "#installation-algorithm",
- "id": "installation-algorithm",
- "hash": "5874d9247d979009b67aedf964ae097837cfb3d9",
- "secno": "8.4",
- "testable": false
- },
- {
- "href": "#activation-algorithm",
- "id": "activation-algorithm",
- "hash": "648b34baf6e7c2096a842e6d367949117843108e",
- "secno": "8.5",
- "testable": false
- },
- {
- "href": "#on-fetch-request-algorithm",
- "id": "on-fetch-request-algorithm",
- "hash": "e1da43671071ec307f99cd781fc9b46353f3adfd",
- "secno": "8.6",
- "testable": false
- },
- {
- "href": "#on-document-unload-algorithm",
- "id": "on-document-unload-algorithm",
- "hash": "8a7196b5dd04ad4fb9b96e16a52f4f7ac1906763",
- "secno": "8.7",
- "testable": false
- },
- {
- "href": "#unregistration-algorithm",
- "id": "unregistration-algorithm",
- "hash": "0114db166d42211d0d7ab4b8e77de64a9fc97517",
- "secno": "8.8",
- "testable": false
- },
- {
- "href": "#update-state-algorithm",
- "id": "update-state-algorithm",
- "hash": "2ed8a1e7479f1a8ad038aa44ccdd5e4f6b65cf05",
- "secno": "8.9",
- "testable": false
- },
- {
- "href": "#scope-match-algorithm",
- "id": "scope-match-algorithm",
- "hash": "a2117fb34a8fa4ca3e832d9276477cfc1318dd1a",
- "secno": "8.10",
- "testable": false
- },
- {
- "href": "#get-registration-algorithm",
- "id": "get-registration-algorithm",
- "hash": "b20332db952ba8f4b7e5f65b740a18da4a199c2e",
- "secno": "8.11",
- "testable": false
- },
- {
- "href": "#get-newest-worker-algorithm",
- "id": "get-newest-worker-algorithm",
- "hash": "72dc1cbee8c98501931c411018fd1cad4376142b",
- "secno": "8.12",
- "testable": false
- },
- {
- "href": "#acknowledgements",
- "id": "acknowledgements",
- "hash": "6347067ca5a574f8cc80c76d95dee568042d059b",
- "secno": "9",
- "testable": false
- }
- ],
- "specUrl": "https://slightlyoff.github.io/ServiceWorker/spec/service_worker/"
-}
\ No newline at end of file
diff --git a/third_party/web_platform_tests/service-workers/stub-3.1-service-worker-obj.html b/third_party/web_platform_tests/service-workers/stub-3.1-service-worker-obj.html
deleted file mode 100644
index 588720e..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.1-service-worker-obj.html
+++ /dev/null
@@ -1,63 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: ServiceWorker</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-obj">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- <script src=/resources/WebIDLParser.js></script>
- <script src=/resources/idlharness.js></script>
-
- </head>
- <body>
-
-<script type=text/plain id="idl_0">
-[Constructor()] // no-op constructor
-interface ServiceWorker : Worker {
- readonly attribute DOMString scope;
- readonly attribute DOMString url;
- readonly attribute ServiceWorkerState state;
-
- // event
- attribute EventHandler onstatechange;
-};
-
-enum ServiceWorkerState {
- "installing",
- "installed",
- "activating",
- "activated",
- "redundant"
-};
-</pre>
-
-<!--
-The `ServiceWorker` interface represents the document-side view of a Service
-Worker. This object provides a no-op constructor. Callers should note that only
-`ServiceWorker` objects created by the user agent (see
-`navigator.serviceWorker.installing`, `navigator.serviceWorker.waiting`,
-`navigator.serviceWorker.active` and `navigator.serviceWorker.controller`) will
-provide meaningful functionality.
--->
-
-
- <script type=text/plain id="untested_idls">
- interface EventHandler {};
- interface Worker {};
- </pre>
-
- <script>
- var idl_array = new IdlArray();
- idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
- idl_array.add_idls(document.getElementById("idl_0").textContent);
- idl_array.add_objects({
- ServiceWorker: ["throw new Error ('No object defined for the ServiceWorker interface')"],
- ServiceWorkerState: ["throw new Error ('No object defined for the ServiceWorkerState enum')"]
- });
- idl_array.test();
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.1.1-service-worker-scope.html b/third_party/web_platform_tests/service-workers/stub-3.1.1-service-worker-scope.html
deleted file mode 100644
index 47b4935..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.1.1-service-worker-scope.html
+++ /dev/null
@@ -1,46 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: scope</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-scope">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-The `scope` of a `ServiceWorker` object reflects the [URL scope][1] of the
-associated Service Worker [registration][2]. The `scope` attribute must return
-the [serialization][3] of the URL representing the [URL scope][1] of the
-associated Service Worker [registration][2].
-
-For example, consider a document created by a navigation to
-`https://example.com/app.html` which [matches][4] via the following
-registration call which has been previously executed:
-// Script on the page https://example.com/app.html
-navigator.serviceWorker.register("/service_worker.js", { scope: "/*" });
-The value of `navigator.serviceWorker.controller.scope` will be
-`"https://example.com/*"`.
-
-
-
-[1]: #url-scope
-[2]: #registration
-[3]: http://url.spec.whatwg.org/#concept-url-serializer
-[4]: #on-fetch-request-algorithm
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section scope so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.1.2-service-worker-url.html b/third_party/web_platform_tests/service-workers/stub-3.1.2-service-worker-url.html
deleted file mode 100644
index be17bb8..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.1.2-service-worker-url.html
+++ /dev/null
@@ -1,43 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: url</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-url">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-The `url` attribute must return the [serialization][1] of the URL of the script
-of the Service Worker, identified by its [URL scope][2], that is associated
-with the [ServiceWorkerGlobalScope][3] object. The `url` attribute is always an
-[absolute URL][4] corresponding to the script file which the Service Worker
-evaluates.
-
-In the example in section 3.1.1, the value of
-`navigator.serviceWorker.controller.url` will be
-`"https://example.com/service_worker.js"`.
-
-
-
-[1]: http://url.spec.whatwg.org/#concept-url-serializer
-[2]: #url-scope
-[3]: #service-worker-global-scope-interface
-[4]: http://url.spec.whatwg.org/#concept-absolute-url
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section url so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.1.3-service-worker-state.html b/third_party/web_platform_tests/service-workers/stub-3.1.3-service-worker-state.html
deleted file mode 100644
index 40f4da4..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.1.3-service-worker-state.html
+++ /dev/null
@@ -1,76 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: state</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-state">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-The [ServiceWorker][1] object can be in several states. The `state` attribute
-must return the current state, which must be one of the following values
-defined in the [ServiceWorkerState][2] enumeration:
-
-`"installing"`:
- The Service Worker represented by the [ServiceWorker][1] object has entered
- and is running the steps in the [installation process][3]. During this
- state, `e.waitUntil(p)` can be called inside the `oninstall` event handler
- of the associcated [ServiceWorkerGloberScope][4] object to extend the life
- of the [installing worker][5] until the passed [Promise][6] resolves
- successfully. This is primarily used to ensure that the Service Worker is
- not active until all of the core caches are populated.
-`"installed"`:
- The Service Worker represented by the [ServiceWorker][1] object has
- completed the steps in the [installation process][3]. The Service Worker in
- this state is considered the [worker in waiting][7].
-`"activating"`:
- The Service Worker represented by the [ServiceWorker][1] object has entered
- and is running the steps in the [activation process][8]. During this state,
- `e.waitUntil(p)` can be called inside the `onactivate` event handler of the
- associated [ServiceWorkerGloberScope][9] object to extend the life of the
- activating [active worker][10] until the passed [Promise][6] resolves
- successfully. Note that no [functional events][11] are dispatched until the
- state becomes `"activated"`.
-`"activated"`:
- The Service Worker represented by the [ServiceWorker][1] object has
- completed the steps in the [activation process][8]. The Service Worker in
- this state is considered the [active worker][10] ready to [control][12] the
- documents in matching scope upon subsequence [navigation][13].
-`"redundant"`:
- A newly created Service Worker [registration][14] is replacing the current
- [registration][14] of the Service Worker.
-
-
-
-[1]: #service-worker-interface
-[2]: #service-worker-state-enum
-[3]: #installation-process
-[4]: #service-worker-glober-scope-interface
-[5]: #installing-worker
-[6]: http://goo.gl/3TobQS
-[7]: #worker-in-waiting
-[8]: #activation-process
-[9]: #service-worker-global-scope-interface
-[10]: #active-worker
-[11]: #functional-events
-[12]: #document-control
-[13]: http://www.whatwg.org/specs/web-apps/current-work/multipage/history.html#navigate
-[14]: #registration
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section state so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.1.4-service-worker-on-state-change.html b/third_party/web_platform_tests/service-workers/stub-3.1.4-service-worker-on-state-change.html
deleted file mode 100644
index 3613874..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.1.4-service-worker-on-state-change.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: onstatechange</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-on-state-change">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`onstatechange` is the [event handler][1] that must be supported as attribute
-by the `[ServiceWorker][2]` object. A `statechange` event using the
-`[Event][3]` interface is dispatched on `[ServiceWorker][2]` object when the
-`state` attribute of the `ServiceWorker` object is changed.
-
-[1]: http://goo.gl/rBfiz0
-[2]: #service-worker-interface
-[3]: http://goo.gl/Mzv7Dv
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section onstatechange so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2-navigator-service-worker.html b/third_party/web_platform_tests/service-workers/stub-3.2-navigator-service-worker.html
deleted file mode 100644
index 0855d6c..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2-navigator-service-worker.html
+++ /dev/null
@@ -1,84 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: navigator.serviceWorker</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- <script src=/resources/WebIDLParser.js></script>
- <script src=/resources/idlharness.js></script>
-
- </head>
- <body>
-
-<!--
-The `serviceWorker` attribute of the [Navigator][1] interface must return an
-instance of the `ServiceWorkerContainer` interface, which provides access to
-registration, removal, upgrade, and communication with Service Workers that are
-(or will become) active for the current document. Communication with these
-workers is provided via standard [HTML5 messaging APIs][2], and [messaging
-occurs as per usual with Web Workers][3].
--->
-<script type=text/plain id="idl_0">
-partial interface Navigator {
- readonly attribute ServiceWorkerContainer serviceWorker;
-};
-
-interface ServiceWorkerContainer : EventTarget {
- [Unforgeable] readonly attribute ServiceWorker? installing;
- [Unforgeable] readonly attribute ServiceWorker? waiting;
- [Unforgeable] readonly attribute ServiceWorker? active;
- [Unforgeable] readonly attribute ServiceWorker? controller;
- readonly attribute Promise<ServiceWorker> ready;
-
- Promise<sequence<ServiceWorker>?> getAll();
- Promise<ServiceWorker> register(DOMString url, optional RegistrationOptionList options);
- Promise<any> unregister(DOMString? scope);
-
- // events
- attribute EventHandler onupdatefound;
- attribute EventHandler oncontrollerchange;
- attribute EventHandler onreloadpage;
- attribute EventHandler onerror;
-};
-
-dictionary RegistrationOptionList {
- DOMString scope = "/*";
-};
-
-interface ReloadPageEvent : Event {
- void waitUntil(Promise<any> f);
-};
-</script>
-
-<!--
-[1]: http://goo.gl/I7WAhg
-[2]: http://www.whatwg.org/specs/web-apps/current-work/multipage/web-messaging.html
-[3]: http://www.w3.org/TR/workers/#dom-worker-postmessage
--->
-
-
- <script type=text/plain id="untested_idls">
- interface ServiceWorker {};
- interface EventHandler {};
- interface EventTarget {};
- interface Event {};
- </pre>
-
- <script>
- var idl_array = new IdlArray();
- idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
- idl_array.add_idls(document.getElementById("idl_0").textContent);
- idl_array.add_objects({
- Navigator: ["throw new Error ('No object defined for the Navigator interface')"],
- ServiceWorkerContainer: ["throw new Error ('No object defined for the ServiceWorkerContainer interface')"],
- RegistrationOptionList: ["throw new Error ('No object defined for the RegistrationOptionList dictionary')"],
- ReloadPageEvent: ["throw new Error ('No object defined for the ReloadPageEvent interface')"]
- });
- idl_array.test();
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.1-navigator-service-worker-installing.html b/third_party/web_platform_tests/service-workers/stub-3.2.1-navigator-service-worker-installing.html
deleted file mode 100644
index d73a35b..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.1-navigator-service-worker-installing.html
+++ /dev/null
@@ -1,43 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: installing</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-installing">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`navigator.serviceWorker.installing` must return a [ServiceWorker][1] object
-representing the [installing worker][2] that is currently undergoing the
-installation process (from step 1 to step 7 of the [_Installation
-algorithm][3]) for the given [URL scope][4] in which the document may be
-[controlled][5] when the Service Worker becomes the [active worker][6].
-`navigator.serviceWorker.installing` returns `null` if no Service Worker
-[registration][7] is in the [installation process][8].
-
-[1]: #service-worker-interface
-[2]: #installing-worker
-[3]: #installation-algorithm
-[4]: #url-scope
-[5]: #document-control
-[6]: #active-worker
-[7]: #service-worker-registration-internal-interface
-[8]: #installation-process
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section installing so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.10-navigator-service-worker-oncontrollerchange.html b/third_party/web_platform_tests/service-workers/stub-3.2.10-navigator-service-worker-oncontrollerchange.html
deleted file mode 100644
index 1e23e82..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.10-navigator-service-worker-oncontrollerchange.html
+++ /dev/null
@@ -1,45 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: oncontrollerchange</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-oncontrollerchange">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`navigator.serviceWorker.oncontrollerchange` is the [event handler][1] that
-must be supported as attribute by the `[ServiceWorkerContainer][2]` object. A
-`controllerchange` event using the `[Event][3]` interface is dispatched on
-`[ServiceWorkerContainer][2]` object (See step 7 of the [_Activation
-algorithm][4]) when the associated Service Worker [registration][5] for the
-document enters the [activation process][6]. When the [activation process][6]
-is triggered by `replace()` method call within the event handler of the
-`install` event, `navigator.serviceWorker.controller` immediately reflects the
-[active worker][7] as the Service Worker that [controls][8] the document.
-
-[1]: http://goo.gl/rBfiz0
-[2]: #service-worker-container-interface
-[3]: http://goo.gl/Mzv7Dv
-[4]: #activation-algorithm
-[5]: #registration
-[6]: #activation-process
-[7]: #active-worker
-[8]: #document-control
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section oncontrollerchange so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.11-navigator-service-worker-onreloadpage.html b/third_party/web_platform_tests/service-workers/stub-3.2.11-navigator-service-worker-onreloadpage.html
deleted file mode 100644
index ef3fd10..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.11-navigator-service-worker-onreloadpage.html
+++ /dev/null
@@ -1,41 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: onreloadpage</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-onreloadpage">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`navigator.serviceWorker.onreloadpage` is the [event handler][1] that must be
-supported as attribute by the `[ServiceWorkerContainer][2]` object. An event
-named `reloadpage` using the `[ReloadPageEvent][3]` interface is dispatched on
-`[ServiceWorkerContainer][2]` object when the page reload is triggered by the
-`[self.clients.reloadAll()][4]` method call from the [active worker][5],
-represented by its associated [ServiceWorkerGlobalScope][6] object, for the
-document.
-
-[1]: http://goo.gl/rBfiz0
-[2]: #service-worker-container-interface
-[3]: #reload-page-event-interface
-[4]: #reloadall-method
-[5]: #active-worker
-[6]: #service-worker-global-scope-interface
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section onreloadpage so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.12-navigator-service-worker-onerror.html b/third_party/web_platform_tests/service-workers/stub-3.2.12-navigator-service-worker-onerror.html
deleted file mode 100644
index e254256..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.12-navigator-service-worker-onerror.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: onerror</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-onerror">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`navigator.serviceWorker.onerror` is the [event handler][1] that must be
-supported as attribute by the `[ServiceWorkerContainer][2]` object. An event
-named `error` using the `[ErrorEvent][3]` interface is dispatched on
-`[ServiceWorkerContainer][2]` object for any error from the associated
-`[ServiceWorker][4]` objects.
-
-[1]: http://goo.gl/rBfiz0
-[2]: #service-worker-container-interface
-[3]: http://goo.gl/FKuWgu
-[4]: #service-worker-interface
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section onerror so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.2-navigator-service-worker-waiting.html b/third_party/web_platform_tests/service-workers/stub-3.2.2-navigator-service-worker-waiting.html
deleted file mode 100644
index 2852d97..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.2-navigator-service-worker-waiting.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: waiting</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-waiting">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`navigator.serviceWorker.waiting` must return a [ServiceWorker][1] object
-representing the waiting Service Worker that is considered the [worker in
-waiting][2] for the document. `navigator.serviceWorker.waiting` returns `null`
-if there is no [worker in waiting][2] for the document.
-
-[1]: #service-worker-interface
-[2]: #worker-in-waiting
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section waiting so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.3-navigator-service-worker-active.html b/third_party/web_platform_tests/service-workers/stub-3.2.3-navigator-service-worker-active.html
deleted file mode 100644
index 3ac45d1..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.3-navigator-service-worker-active.html
+++ /dev/null
@@ -1,40 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: active</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-active">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`navigator.serviceWorker.active` must return a [ServiceWorker][1] object
-representing the [active worker][2] that is currently undergoing or completed
-the activation process (from step 4 to step 9 of the [_Activation
-algorithm][3]) for the given [URL scope][4] in which the document is controlled
-(or to be controlled). `navigator.serviceWorker.active` returns `null` if no
-Service Worker [registration][5] is in the [activation process][6].
-
-[1]: #service-worker-interface
-[2]: #active-worker
-[3]: #activation-algorithm
-[4]: #url-scope
-[5]: #service-worker-registration-internal-interface
-[6]: #activation-process
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section active so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.4-navigator-service-worker-controller.html b/third_party/web_platform_tests/service-workers/stub-3.2.4-navigator-service-worker-controller.html
deleted file mode 100644
index 90378dd..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.4-navigator-service-worker-controller.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: controller</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-controller">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`navigator.serviceWorker.controller` must return a [ServiceWorker][1] object
-representing the [active worker][2] that currently handles resource requests
-for the document. `navigator.serviceWorker.controller` returns `null` if the
-current document was not [created under a Service Worker][3] (See step 6-1 of
-[_OnFetchRequest][3] algorithm) or the request is a force refresh
-(shift+refresh).
-
-[1]: #service-worker-interface
-[2]: #active-worker
-[3]: #on-fetch-request-algorithm
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section controller so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.5-navigator-service-worker-ready.html b/third_party/web_platform_tests/service-workers/stub-3.2.5-navigator-service-worker-ready.html
deleted file mode 100644
index f3b1ca7..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.5-navigator-service-worker-ready.html
+++ /dev/null
@@ -1,67 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: ready</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-ready">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`navigator.serviceWorker.ready` attribute must return the result of running
-these steps:
-
-1. Let `promise` be a newly-created [promise][1].
-2. Return `promise`.
-3. Run the following steps asynchronously:
- 1. Let `registration` be the result of running [_ScopeMatch
- algorithm][2] with document's url as its argument.
- 2. If `registration` is null, then:
- 1. Wait for the document to have a matching [registration][3].
- 3. If the [registration][3], represented by `registration`, for the
- document has an [active worker][4], then:
- 1. Resolve `promise` with the [ServiceWorker][5] object associated
- with the [active worker][4].
- 2. Abort these steps.
- 4. If the [registration][3], represented by `registration`, for the
- document has a [worker in waiting][6], then:
- 1. Resolve `promise` with the [ServiceWorker][5] object associated
- with the [worker in waiting][6].
- 2. Abort these steps.
- 5. Wait until the [registration][3], represented by `registration`,
- for the document acquires a [worker in waiting][6] through a new
- [installation process][7].
- 6. Resolve `promise` with the [ServiceWorker][5] object associated
- with the [worker in waiting][6].
-Note that `ready` attribute is desinged in a way that the returned [promise][1]
-will never reject. Instead, it waits until the [promise][1] resolves with a
-newly installed [worker in waiting][6]. Hence, the `state` of the acquired
-[`ServiceWorker`][8] object is either `installed`, `activating` or `activated`.
-
-
-
-[1]: http://goo.gl/3TobQS
-[2]: #scope-match-algorithm
-[3]: #registration
-[4]: #active-worker
-[5]: #service-worker-interface
-[6]: #worker-in-waiting
-[7]: #installation-process
-[8]: #service-worker
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section ready so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.6-navigator-service-worker-getAll.html b/third_party/web_platform_tests/service-workers/stub-3.2.6-navigator-service-worker-getAll.html
deleted file mode 100644
index 18180b9..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.6-navigator-service-worker-getAll.html
+++ /dev/null
@@ -1,30 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: getAll()</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-getAll">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`navigator.serviceWorker.getAll()` method must return a promise that resolves
-with the array of the ServiceWorker objects in `installing`, `installed`,
-`activating` and `activated` states.
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section getAll() so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.7-navigator-service-worker-register.html b/third_party/web_platform_tests/service-workers/stub-3.2.7-navigator-service-worker-register.html
deleted file mode 100644
index c9253dd..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.7-navigator-service-worker-register.html
+++ /dev/null
@@ -1,32 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: register()</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-register">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`navigator.serviceWorker.register(url, options)` method must run the
-[Registration algorithm][1] passing `url` and `options`.`scope` as the
-arguments.
-
-[1]: #registration-algorithm
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section register() so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.8-navigator-service-worker-unregister.html b/third_party/web_platform_tests/service-workers/stub-3.2.8-navigator-service-worker-unregister.html
deleted file mode 100644
index c4c0c24..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.8-navigator-service-worker-unregister.html
+++ /dev/null
@@ -1,31 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: unregister()</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-unregister">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`navigator.serviceWorker.unregister(scope)` method must run the [Unregistration
-algorithm][1] passing `scope` as the argument.
-
-[1]: #unregistration-algorithm
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section unregister() so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-3.2.9-navigator-service-worker-onupdatefound.html b/third_party/web_platform_tests/service-workers/stub-3.2.9-navigator-service-worker-onupdatefound.html
deleted file mode 100644
index 4502b2e..0000000
--- a/third_party/web_platform_tests/service-workers/stub-3.2.9-navigator-service-worker-onupdatefound.html
+++ /dev/null
@@ -1,42 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: onupdatefound</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-onupdatefound">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`navigator.serviceWorker.onupdatefound` is the [event handler][1] that must be
-supported as attribute by the `[ServiceWorkerContainer][2]` object. An
-`updatefound` event using the `[Event][3]` interface is dispatched on
-`[ServiceWorkerContainer][2]` object (See step 4 of the [_Installation
-algorithm][4]) when the associated Service Worker [registration][5] for the
-document enters the [installation process][6] such that
-`navigator.serviceWorker.installing` becomes the new [installing worker][7].
-
-[1]: http://goo.gl/rBfiz0
-[2]: #service-worker-container-interface
-[3]: http://goo.gl/Mzv7Dv
-[4]: #installation-algorithm
-[5]: #registration
-[6]: #installation-process
-[7]: #installing-worker
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section onupdatefound so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.1-service-worker-global-scope.html b/third_party/web_platform_tests/service-workers/stub-4.1-service-worker-global-scope.html
deleted file mode 100644
index ce6a045..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.1-service-worker-global-scope.html
+++ /dev/null
@@ -1,75 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: ServiceWorkerGlobalScope</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-global-scope">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- <script src=/resources/WebIDLParser.js></script>
- <script src=/resources/idlharness.js></script>
-
- </head>
- <body>
-
-<script type=text/plain id="idl_0">
-[Global]
-interface ServiceWorkerGlobalScope : WorkerGlobalScope {
- readonly attribute CacheStorage caches;
- // A container for a list of window objects, identifiable by ID, that
- // correspond to windows (or workers) that are "controlled" by this SW
- readonly attribute ServiceWorkerClients clients;
- [Unforgeable] readonly attribute DOMString scope;
-
- Promise<any> fetch((Request or ScalarValueString) request);
-
- void update();
- void unregister();
-
- attribute EventHandler oninstall;
- attribute EventHandler onactivate;
- attribute EventHandler onfetch;
- attribute EventHandler onbeforeevicted;
- attribute EventHandler onevicted;
-
- // The event.source of these MessageEvents are instances of Client
- attribute EventHandler onmessage;
-
- // close() method inherited from WorkerGlobalScope is not exposed.
-};
-</pre>
-
-<!--
-The `ServiceWorkerGlobalScope` interface represents the global execution
-context of a Service Worker. `ServiceWorkerGlobalScope` object provides
-generic, event-driven, time-limited script execution contexts that run at an
-origin. Once successfully [registered][1], a Service Worker is started, kept
-alive and killed by their relationship to events, not documents. Any type of
-synchronous requests MUST NOT be initiated inside of a Service Worker.
-
-[1]: #navigator-service-worker-register
--->
-
-
- <script type=text/plain id="untested_idls">
- interface CacheStorage {};
- interface ServiceWorkerClients {};
- interface Request {};
- interface ScalarValueString {};
- interface EventHandler {};
- interface WorkerGlobalScope {};
- </pre>
-
- <script>
- var idl_array = new IdlArray();
- idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
- idl_array.add_idls(document.getElementById("idl_0").textContent);
- idl_array.add_objects({
- ServiceWorkerGlobalScope: ["throw new Error ('No object defined for the ServiceWorkerGlobalScope interface')"]
- });
- idl_array.test();
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.1.1-service-worker-global-scope-caches.html b/third_party/web_platform_tests/service-workers/stub-4.1.1-service-worker-global-scope-caches.html
deleted file mode 100644
index 4e68cc2..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.1.1-service-worker-global-scope-caches.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: caches</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-global-scope-caches">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`self.caches` must return the `[CacheStorage][1]` object that is the global
-asynchronous map object for the `[ServiceWorkerGlobalScope][2]` execution
-context containing the cache objects keyed by the name of the caches. Caches
-are always enumerable via `self.caches` in insertion order (per [ECMAScript 6
-Map objects][3].)
-
-[1]: #cache-storage-interface
-[2]: #service-worker-global-scope-interface
-[3]: http://goo.gl/gNnDPO
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section caches so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.1.2-service-worker-global-scope-clients.html b/third_party/web_platform_tests/service-workers/stub-4.1.2-service-worker-global-scope-clients.html
deleted file mode 100644
index 8499c71..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.1.2-service-worker-global-scope-clients.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: clients</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-global-scope-clients">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`self.clients` must return the `[ServiceWorkerClients][1]` object containing a
-list of client objects, identifiable by ID, that correspond to windows or
-workers that are [controlled][2] by this Service Worker.
-
-[1]: #service-worker-clients-interface
-[2]: #document-control
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section clients so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.1.3-service-worker-global-scope-scope.html b/third_party/web_platform_tests/service-workers/stub-4.1.3-service-worker-global-scope-scope.html
deleted file mode 100644
index 3784e1e..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.1.3-service-worker-global-scope-scope.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: scope</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-global-scope-scope">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-The `scope` attribute of a [ServiceWorkerGlobalScope][1] object reflects the
-[URL scope][2] of the associated Service Worker [registration][3]. The `scope`
-attribute must return the [serialization][4] of the URL representing the [URL
-scope][2] of the associated Service Worker [registration][3].
-
-[1]: #service-worker-global-scope-interface
-[2]: #url-scope
-[3]: #registration
-[4]: http://url.spec.whatwg.org/#concept-url-serializer
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section scope so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.1.4-service-worker-global-scope-fetch.html b/third_party/web_platform_tests/service-workers/stub-4.1.4-service-worker-global-scope-fetch.html
deleted file mode 100644
index 29548a7..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.1.4-service-worker-global-scope-fetch.html
+++ /dev/null
@@ -1,55 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: fetch(request)</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-global-scope-fetch">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`self.fetch(request)` method must run these steps:
-
-1. Let `request` be a [request][1] represented by `request`.
-2. Set [`client`][2] of `request` to the [JavaScript global
- environment][3] represented by `self` object.
-3. Let `promise` be a newly-created [promise][4].
-4. Return `promise.`
-5. Run the following steps asynchronously:
- 1. Let `response` be the result of running [fetch algorithm][5] with
- `request` as its argument.
- 2. If `response` is a [network error][6], then:
- 1. Reject `promise` with a new [DOMException][7] whose name is
- "[NetworkError][8]".
- 3. Else,
- 1. Resolve `promise` with a new [Response][9] object associated
- with `response`.
-
-
-
-[1]: http://goo.gl/ucOuXl
-[2]: http://goo.gl/Oxj4xQ
-[3]: http://goo.gl/ifwwCC
-[4]: http://goo.gl/3TobQS
-[5]: http://goo.gl/fGMifs
-[6]: http://goo.gl/jprjjc
-[7]: http://goo.gl/A0U8qC
-[8]: http://goo.gl/lud5HB
-[9]: http://goo.gl/Deazjv
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section fetch(request) so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.1.5-service-worker-global-scope-update.html b/third_party/web_platform_tests/service-workers/stub-4.1.5-service-worker-global-scope-update.html
deleted file mode 100644
index ee9552b..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.1.5-service-worker-global-scope-update.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: update()</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-global-scope-update">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`update()` pings the server for an updated version of this script without
-consulting caches. `self.update()` method must run the [_SoftUpdate
-algorithm][1] passing its serviceWorkerRegistration object as the argument
-which is the result of running the [_GetRegistration algorithm][2] with
-`self.scope` as the argument. (This is conceptually the same operation that UA
-does maximum once per every 24 hours.)
-
-[1]: #soft-update-algorithm
-[2]: #get-registration-algorithm
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section update() so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.1.6-service-worker-global-scope-unregister.html b/third_party/web_platform_tests/service-workers/stub-4.1.6-service-worker-global-scope-unregister.html
deleted file mode 100644
index 9f76ee3..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.1.6-service-worker-global-scope-unregister.html
+++ /dev/null
@@ -1,31 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: unregister()</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-global-scope-unregister">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`self.unregister()` method must run the [Unregistration algorithm][1]
-implicitly passing `self.scope` as the argument.
-
-[1]: #unregistration-algorithm
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section unregister() so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.1.7-service-worker-global-scope-onmessage.html b/third_party/web_platform_tests/service-workers/stub-4.1.7-service-worker-global-scope-onmessage.html
deleted file mode 100644
index d536a2c..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.1.7-service-worker-global-scope-onmessage.html
+++ /dev/null
@@ -1,45 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: onmessage</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-global-scope-onmessage">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`self.onmessage` is the [event handler][1] that must be supported as attribute
-by the `ServiceWorkerGlobalScope` object. `ServiceWorkerGlobalScope` objects
-act as if they had an implicit `[MessagePort][2]` associated with them. This
-port is part of a channel that is set up when the worker is created, but it is
-not exposed. This object must never be garbage collected before the
-`ServiceWorkerGlobalScope` object.
-
-All messages received by that port must immediately be retargeted at the
-`ServiceWorkerGlobalScope` object. That is, an event named `message` using the
-`[MessageEvent][3]` interface is dispatched on ServiceWorkerGlobalScope object.
-The `event.source` of these `[MessageEvent][3]`s are instances of `[Client][4]`.
-
-
-
-[1]: http://goo.gl/rBfiz0
-[2]: http://goo.gl/tHBrI6
-[3]: http://goo.gl/S5e0b6
-[4]: #client-interface
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section onmessage so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.2-client.html b/third_party/web_platform_tests/service-workers/stub-4.2-client.html
deleted file mode 100644
index 96976c1..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.2-client.html
+++ /dev/null
@@ -1,61 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: Client</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#client">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- <script src=/resources/WebIDLParser.js></script>
- <script src=/resources/idlharness.js></script>
-
- </head>
- <body>
-
-<script type=text/plain id="idl_0">
-[Constructor()] // no-op constructor
-interface Client {
- readonly attribute unsigned long id;
- void postMessage(any message, DOMString targetOrigin,
- optional sequence<Transferable> transfer);
-};
-</pre>
-
-<!--
-The `Client` interface represents the window or the worker (defined as client)
-that is [controlled][1] by the Service Worker. This object provides a no-op
-constructor. Callers should note that only `Client` objects created by the user
-agent (see [`this.clients.getServiced()`][2]) will provide meaningful
-functionality.
-
-The `id` of a `Client` identifies the specific client object from the list of
-client objects serviced by the Service Worker. The `postMessage(message,
-targetOrigin, transfer)` method of a `[Client][3]`, when called, causes a
-`[MessageEvent][4]` to be dispatched at the client object.
-
-
-
-[1]: #document-control
-[2]: #get-serviced-method
-[3]: #client-interface
-[4]: http://goo.gl/4SLWiH
--->
-
-
- <script type=text/plain id="untested_idls">
- interface Transferable {};
- </pre>
-
- <script>
- var idl_array = new IdlArray();
- idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
- idl_array.add_idls(document.getElementById("idl_0").textContent);
- idl_array.add_objects({
- Client: ["throw new Error ('No object defined for the Client interface')"]
- });
- idl_array.test();
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.3-service-worker-clients.html b/third_party/web_platform_tests/service-workers/stub-4.3-service-worker-clients.html
deleted file mode 100644
index beb5d59..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.3-service-worker-clients.html
+++ /dev/null
@@ -1,48 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: ServiceWorkerClients</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-clients">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- <script src=/resources/WebIDLParser.js></script>
- <script src=/resources/idlharness.js></script>
-
- </head>
- <body>
-
-<script type=text/plain id="idl_0">
-interface ServiceWorkerClients {
- // A list of client objects, identifiable by ID, that correspond to windows
- // (or workers) that are "controlled" by this SW
- Promise<sequence<Client>?> getServiced();
- Promise<any> reloadAll();
-};
-</pre>
-
-<!--
-The `ServiceWorkerClients` interface represents a container for a list of
-`[Client][1]` objects.
-
-[1]: #client-interface
--->
-
-
- <script type=text/plain id="untested_idls">
- interface Client {};
- </pre>
-
- <script>
- var idl_array = new IdlArray();
- idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
- idl_array.add_idls(document.getElementById("idl_0").textContent);
- idl_array.add_objects({
- ServiceWorkerClients: ["throw new Error ('No object defined for the ServiceWorkerClients interface')"]
- });
- idl_array.test();
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.3.1-get-serviced-method.html b/third_party/web_platform_tests/service-workers/stub-4.3.1-get-serviced-method.html
deleted file mode 100644
index 8543bd4..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.3.1-get-serviced-method.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: getServiced()</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#get-serviced-method">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-The `getServiced()` method of a `ServiceWorkerClients`, when called, returns a
-[Promise][1] that will resolve with a list of `[Client][2]` objects that are
-[controlled][3] by this Service Worker.
-
-[1]: http://goo.gl/3TobQS
-[2]: #client-interface
-[3]: #document-control
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section getServiced() so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.3.2-reloadall-method.html b/third_party/web_platform_tests/service-workers/stub-4.3.2-reloadall-method.html
deleted file mode 100644
index dd79a91..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.3.2-reloadall-method.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: reloadAll()</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#reloadall-method">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`reloadAll()` provides a mechanism for the worker to request synchronized
-re-fetch of all documents whose URLs match the registration's [URL scope][1].
-An event named `reloadpage` is dispatched on the `navigator.serviceWorker`
-object of each document. The in-document handlers may allow the event to
-continue, request an extension (via [`e.waitUntil()`][2]), or cancel the
-collective reload by calling [`e.preventDefault()`][3].
-
-[1]: #url-scope
-[2]: #wait-until-method
-[3]: http://goo.gl/2zH6ie
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section reloadAll() so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.4-request-objects.html b/third_party/web_platform_tests/service-workers/stub-4.4-request-objects.html
deleted file mode 100644
index aa3502b..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.4-request-objects.html
+++ /dev/null
@@ -1,72 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: Request Objects</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#request-objects">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- <script src=/resources/WebIDLParser.js></script>
- <script src=/resources/idlharness.js></script>
-
- </head>
- <body>
-
-<script type=text/plain id="idl_0">
-[Constructor(optional RequestInit init)]
-interface Request {
- attribute unsigned long timeout;
- attribute DOMString url;
- attribute ByteString method;
- readonly attribute DOMString origin;
- readonly attribute Mode mode;
- attribute boolean synchronous;
- attribute boolean forcePreflight;
- attribute boolean omitCredentials;
- readonly attribute DOMString referrer;
- readonly attribute HeaderMap headers; // alternative: sequence<Header> headers;
- attribute any body;
-};
-
-dictionary RequestInit {
- unsigned long timeout = 0;
- DOMString url;
- boolean synchronous = false;
- boolean forcePreflight = false;
- boolean omitCredentials = false;
- ByteString method = "GET";
- HeaderMap headers;
- any body;
-};
-
-enum Mode {
- "same origin",
- "tainted cross-origin",
- "CORS",
- "CORS-with-forced-preflight"
-};
-
-[MapClass(DOMString, DOMString)]
-interface HeaderMap {
-};
-</pre>
-
-
-
-
- <script>
- var idl_array = new IdlArray();
- idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
- idl_array.add_idls(document.getElementById("idl_0").textContent);
- idl_array.add_objects({
- Request: ["throw new Error ('No object defined for the Request interface')"],
- RequestInit: ["throw new Error ('No object defined for the RequestInit dictionary')"],
- Mode: ["throw new Error ('No object defined for the Mode enum')"],
- HeaderMap: ["throw new Error ('No object defined for the HeaderMap interface')"]
- });
- idl_array.test();
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.5-response-objects.html b/third_party/web_platform_tests/service-workers/stub-4.5-response-objects.html
deleted file mode 100644
index a334586..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.5-response-objects.html
+++ /dev/null
@@ -1,75 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: Response Objects</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#response-objects">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- <script src=/resources/WebIDLParser.js></script>
- <script src=/resources/idlharness.js></script>
-
- </head>
- <body>
-
-<!--
-`Response` objects model HTTP responses.
--->
-<script type=text/plain id="idl_0">
-[Constructor]
-interface AbstractResponse {
-};
-
-interface OpaqueResponse : AbstractResponse {
- readonly attribute unsigned short status;
- readonly attribute ByteString statusText;
- // Returns a filtered list of headers. See prose for details.
- readonly attribute HeaderMap headers;
- // No setter for headers
- readonly attribute DOMString url;
-};
-
-interface CORSResponse : Response {
- readonly attribute HeaderMap headers;
-};
-
-[Constructor(optional ResponseInit responseInitDict)]
-interface Response : AbstractResponse {
- attribute unsigned short status;
- attribute ByteString statusText;
- readonly attribute HeaderMap headers;
- attribute DOMString url;
- Promise<Blob> toBlob();
-};
-
-dictionary ResponseInit {
- unsigned short status = 200;
- ByteString statusText = "OK";
- HeaderMap headers;
-};
-</pre>
-
-
-
- <script type=text/plain id="untested_idls">
- interface HeaderMap {};
- interface Blob {};
- </pre>
-
- <script>
- var idl_array = new IdlArray();
- idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
- idl_array.add_idls(document.getElementById("idl_0").textContent);
- idl_array.add_objects({
- AbstractResponse: ["throw new Error ('No object defined for the AbstractResponse interface')"],
- OpaqueResponse: ["throw new Error ('No object defined for the OpaqueResponse interface')"],
- CORSResponse: ["throw new Error ('No object defined for the CORSResponse interface')"],
- Response: ["throw new Error ('No object defined for the Response interface')"],
- ResponseInit: ["throw new Error ('No object defined for the ResponseInit dictionary')"]
- });
- idl_array.test();
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.5.2-response.html b/third_party/web_platform_tests/service-workers/stub-4.5.2-response.html
deleted file mode 100644
index 0a8715c..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.5.2-response.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: Response</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#response">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`Response` objects are mutable and constructable. They model HTTP responses.
-The `fetch()` API returns this type for same-origin responses.
-
-It may be possible to set the `Location` header of a `Response` object to
-someplace not in the current origin but this is not a security issue.
-Cross-origin response bodies are opaque to script, and since only same-origin
-documents will encounter these responses, the only systems the Service Worker
-can "lie to" are same-origin (and therefore safe from the perspective of other
-origins).
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section Response so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.5.4-opaque-response.html b/third_party/web_platform_tests/service-workers/stub-4.5.4-opaque-response.html
deleted file mode 100644
index 1698558..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.5.4-opaque-response.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: OpaqueResponse</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#opaque-response">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`OpaqueResponse` objects are immutable but constructable. The `fetch()` API
-returns this type for cross-origin responses.
-
-Their role is to encapsulate the security properties of the web platform. As
-such, their `body` attribute will always be `undefined` and the list of
-readable `headers` is heavily filtered.
-
-`OpaqueResponse` objects may be forwarded on to rendering documents in exactly
-the same way as mutable `Response` objects.
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section OpaqueResponse so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.6-cache-objects.html b/third_party/web_platform_tests/service-workers/stub-4.6-cache-objects.html
deleted file mode 100644
index 3bb47b2..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.6-cache-objects.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: Caches</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-objects">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-To allow authors to fully manage their content caches for offline use, the
-`[ServiceWorkerGlobalScope][1]` execution context provides the caching methods
-largely conforming to [ECMAScript 6 Map objects][2] with additional convenience
-methods. A domain can have multiple, named `[Cache][3]` objects, whose contents
-are entirely under the control of scripts. Caches are not shared across
-domains, and they are completely isolated from the browser's HTTP cache.
-
-[1]: #service-worker-global-scope-interface
-[2]: http://goo.gl/gNnDPO
-[3]: #cache-interface
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section Caches so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.6.1-cache-lifetimes.html b/third_party/web_platform_tests/service-workers/stub-4.6.1-cache-lifetimes.html
deleted file mode 100644
index 9068d0c..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.6.1-cache-lifetimes.html
+++ /dev/null
@@ -1,38 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: Understanding Cache Lifetimes</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-lifetimes">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-The `[Cache][1]` instances are not part of the browser's HTTP cache. The
-`[Cache][1]` objects are exactly what authors have to manage themselves. The
-`[Cache][1]` objects do not get updated unless authors explicitly request them
-to be. The `[Cache][1]` objects do not expire unless authors delete the
-entries. The `[Cache][1]` objects do not disappear just because the Service
-Worker script is updated. That is, caches are not updated automatically.
-Updates must be manually managed. This implies that authors should version
-their caches by name and make sure to use the caches only from the version of
-the ServiceWorker that can safely operate on.
-
-[1]: #cache-interface
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section Understanding Cache Lifetimes so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.6.2-cache.html b/third_party/web_platform_tests/service-workers/stub-4.6.2-cache.html
deleted file mode 100644
index faee336..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.6.2-cache.html
+++ /dev/null
@@ -1,64 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: Cache</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- <script src=/resources/WebIDLParser.js></script>
- <script src=/resources/idlharness.js></script>
-
- </head>
- <body>
-
-<script type=text/plain id="idl_0">
-[Exposed=(Window,Worker)]
-interface Cache {
- Promise<Response> match(RequestInfo request, optional CacheQueryOptions options);
- Promise<sequence<Response>> matchAll(optional RequestInfo request, optional CacheQueryOptions options);
- Promise<void> add(RequestInfo request);
- Promise<void> addAll(sequence<RequestInfo> requests);
- Promise<void> put(RequestInfo request, Response response);
- Promise<boolean> delete(RequestInfo request, optional CacheQueryOptions options);
- Promise<sequence<Request>> keys(optional RequestInfo request, optional CacheQueryOptions options);
-};
-
-dictionary CacheQueryOptions {
- boolean ignoreSearch = false;
- boolean ignoreMethod = false;
- boolean ignoreVary = false;
- DOMString cacheName;
-};
-
-dictionary CacheBatchOperation {
- DOMString type;
- Request request;
- Response response;
- CacheQueryOptions options;
-};
-</pre>
-
-
-
- <script type=text/plain id="untested_idls">
- interface AbstractResponse {};
- interface Request {};
- interface ScalarValueString {};
- </pre>
-
- <script>
- var idl_array = new IdlArray();
- idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
- idl_array.add_idls(document.getElementById("idl_0").textContent);
- idl_array.add_objects({
- Cache: ["throw new Error ('No object defined for the Cache interface')"],
- QueryParams: ["throw new Error ('No object defined for the QueryParams dictionary')"],
- CacheIterationCallback: ["throw new Error ('No object defined for the CacheIterationCallback callback')"]
- });
- idl_array.test();
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.6.3-cache-storage.html b/third_party/web_platform_tests/service-workers/stub-4.6.3-cache-storage.html
deleted file mode 100644
index 875220e..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.6.3-cache-storage.html
+++ /dev/null
@@ -1,62 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: CacheStorage</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cache-storage">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- <script src=/resources/WebIDLParser.js></script>
- <script src=/resources/idlharness.js></script>
-
- </head>
- <body>
-
-<script type=text/plain id="idl_0">
-[Constructor(sequence<any> iterable)]
-interface CacheStorage {
- Promise<any> match(ScalarValueString url, optional DOMString cacheName);
- Promise<Cache> get(DOMString key);
- Promise<boolean> has(DOMString key);
- Promise<any> set(DOMString key, Cache val);
- Promise<any> clear();
- Promise<any> delete(DOMString key);
- void forEach(CacheStorageIterationCallback callback, optional object thisArg);
- Promise<sequence<any>> entries();
- Promise<sequence<DOMString>> keys();
- Promise<sequence<Cache>> values();
- Promise<unsigned long> size();
-};
-
-callback CacheStorageIterationCallback = void (Cache value, DOMString key, CacheStorage map);
-</pre>
-
-<!--
-**Note**:[CacheStorage][1]interface is designed to largely conform
-to[ECMAScript 6 Map objects][2]but entirely async, and with additional
-convenience methods.
-
-[1]: #cache-storage-interface
-[2]: http://goo.gl/gNnDPO
--->
-
-
- <script type=text/plain id="untested_idls">
- interface ScalarValueString {};
- interface Cache {};
- </pre>
-
- <script>
- var idl_array = new IdlArray();
- idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
- idl_array.add_idls(document.getElementById("idl_0").textContent);
- idl_array.add_objects({
- CacheStorage: ["throw new Error ('No object defined for the CacheStorage interface')"],
- CacheStorageIterationCallback: ["throw new Error ('No object defined for the CacheStorageIterationCallback callback')"]
- });
- idl_array.test();
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.7.1-install-phase-event.html b/third_party/web_platform_tests/service-workers/stub-4.7.1-install-phase-event.html
deleted file mode 100644
index 195c38d..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.7.1-install-phase-event.html
+++ /dev/null
@@ -1,51 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: InstallPhaseEvent</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#install-phase-event">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- <script src=/resources/WebIDLParser.js></script>
- <script src=/resources/idlharness.js></script>
-
- </head>
- <body>
-
-<script type=text/plain id="idl_0">
-interface InstallPhaseEvent : Event {
- Promise<any> waitUntil(Promise<any> f);
-};
-</pre>
-
-<!--
-Service Workers have two [Lifecycle events][1], `[install][2]` and
-`[activate][3]`. Service Workers use the `[InstallPhaseEvent][4]` interface for
-`[activate][3]` event and the `[InstallEvent][5]` interface, which inherits
-from the `[InstallPhaseEvent][4]` interface, for `[install][2]` event.
-
-[1]: #lifecycle-events
-[2]: #install-event
-[3]: #activate-event
-[4]: #install-phase-event-interface
-[5]: #install-event-interface
--->
-
-
- <script type=text/plain id="untested_idls">
- interface Event {};
- </pre>
-
- <script>
- var idl_array = new IdlArray();
- idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
- idl_array.add_idls(document.getElementById("idl_0").textContent);
- idl_array.add_objects({
- InstallPhaseEvent: ["throw new Error ('No object defined for the InstallPhaseEvent interface')"]
- });
- idl_array.test();
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.7.1.1-wait-until-method.html b/third_party/web_platform_tests/service-workers/stub-4.7.1.1-wait-until-method.html
deleted file mode 100644
index 84b730f..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.7.1.1-wait-until-method.html
+++ /dev/null
@@ -1,39 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: event.waitUntil(f)</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#wait-until-method">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`event.waitUntil(f)` method, when called in `oninstall` or `onactivate`,
-extends the lifetime of the event. When called in `oninstall`, it delays
-treating the installing worker until the passed [Promise][1] resolves
-successfully. This is primarily used to ensure that a `ServiceWorker` is not
-active until all of the core caches it depends on are populated. When called in
-`onactivate`, it delays treating the activating worker until the passed
-[Promise][1] resolves successfully. This is primarily used to ensure that any
-[Functional events][2] are not dispatched to the `ServiceWorker` until it
-upgrades database schemas and deletes the outdated cache entries.
-
-[1]: http://goo.gl/3TobQS
-[2]: #functional-events
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section event.waitUntil(f) so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.7.2-install-event.html b/third_party/web_platform_tests/service-workers/stub-4.7.2-install-event.html
deleted file mode 100644
index a2a5b1d..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.7.2-install-event.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: install Event</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#install-event">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-An event named `[install][1]` using the `[InstallEvent][2]` interface is
-dispatched on `ServiceWorkerGlobalScope` object when the state of the
-associated `ServiceWorker` changes its value to `installing`. (See step 3 of
-[_Installation algorithm][3])
-
-[1]: #install-event
-[2]: #install-event-interface
-[3]: #installation-algorithm
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section install Event so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.7.2.1-install-event-section.html b/third_party/web_platform_tests/service-workers/stub-4.7.2.1-install-event-section.html
deleted file mode 100644
index c305159..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.7.2.1-install-event-section.html
+++ /dev/null
@@ -1,47 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: InstallEvent</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#install-event-section">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- <script src=/resources/WebIDLParser.js></script>
- <script src=/resources/idlharness.js></script>
-
- </head>
- <body>
-
-<script type=text/plain id="idl_0">
-interface InstallEvent : InstallPhaseEvent {
- readonly attribute ServiceWorker? activeWorker;
- void replace();
-};
-</pre>
-
-<!--
-Service Workers use the `[InstallEvent][1]` interface for `[install][2]` event.
-
-[1]: #install-event-interface
-[2]: #install-event
--->
-
-
- <script type=text/plain id="untested_idls">
- interface ServiceWorker {};
- interface InstallPhaseEvent {};
- </pre>
-
- <script>
- var idl_array = new IdlArray();
- idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
- idl_array.add_idls(document.getElementById("idl_0").textContent);
- idl_array.add_objects({
- InstallEvent: ["throw new Error ('No object defined for the InstallEvent interface')"]
- });
- idl_array.test();
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.7.2.2-replace-method.html b/third_party/web_platform_tests/service-workers/stub-4.7.2.2-replace-method.html
deleted file mode 100644
index 78c916f..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.7.2.2-replace-method.html
+++ /dev/null
@@ -1,38 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: event.replace()</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#replace-method">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`replace()` interacts with `waitUntil` method in the following way:
-
-- Successful installation can be delayed by `waitUntil`, perhaps by
- subsequent event handlers.
-- Replacement only happens upon successful installation
-- Therefore, replacement of the [active worker][1] (if any) is not
- immediate, however it may occur as soon as the end of the current turn.
-
-
-
-[1]: #navigator-service-worker-active
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section event.replace() so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.7.3-activate-event.html b/third_party/web_platform_tests/service-workers/stub-4.7.3-activate-event.html
deleted file mode 100644
index 82c049a..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.7.3-activate-event.html
+++ /dev/null
@@ -1,41 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: activate Event</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#activate-event">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-An event named `[activate][1]` using the `[InstallPhaseEvent][2]` interface is
-dispatched on `ServiceWorkerGlobalScope` object when the state of the
-associated `ServiceWorker` changes its value to `activating`. (See step 6 of
-[_Activation algorithm][3])
-
-Service Workers use the `[InstallPhaseEvent][4]` interface for `[activate][1]`
-event.
-
-
-
-[1]: #activate-event
-[2]: #install-phase-event
-[3]: #activation-algorithm
-[4]: #install-phase-event-interface
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section activate Event so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.7.4.1-fetch-event-section.html b/third_party/web_platform_tests/service-workers/stub-4.7.4.1-fetch-event-section.html
deleted file mode 100644
index 8555903..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.7.4.1-fetch-event-section.html
+++ /dev/null
@@ -1,71 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: FetchEvent</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#fetch-event-section">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- <script src=/resources/WebIDLParser.js></script>
- <script src=/resources/idlharness.js></script>
-
- </head>
- <body>
-
-<script type=text/plain id="idl_0">
-[Constructor]
-interface FetchEvent : Event {
- readonly attribute Request request;
- readonly attribute Client client; // The window issuing the request.
- readonly attribute Context context;
- readonly attribute boolean isReload;
-
- void respondWith(Promise<AbstractResponse> r);
- Promise<any> forwardTo(ScalarValueString url);
- Promise<any> default();
-};
-
-enum Context {
- "connect",
- "font",
- "img",
- "object",
- "script",
- "style",
- "worker",
- "popup",
- "child",
- "navigate"
-};
-</pre>
-
-<!--
-Service Workers use the `[FetchEvent][1]` interface for `[fetch][2]` event.
-
-[1]: #fetch-event-interface
-[2]: #fetch-event
--->
-
-
- <script type=text/plain id="untested_idls">
- interface Request {};
- interface Client {};
- interface AbstractResponse {};
- interface ScalarValueString {};
- interface Event {};
- </pre>
-
- <script>
- var idl_array = new IdlArray();
- idl_array.add_untested_idls(document.getElementById("untested_idls").textContent);
- idl_array.add_idls(document.getElementById("idl_0").textContent);
- idl_array.add_objects({
- FetchEvent: ["throw new Error ('No object defined for the FetchEvent interface')"],
- Context: ["throw new Error ('No object defined for the Context enum')"]
- });
- idl_array.test();
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.7.4.2-respond-with-method.html b/third_party/web_platform_tests/service-workers/stub-4.7.4.2-respond-with-method.html
deleted file mode 100644
index f178a50..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.7.4.2-respond-with-method.html
+++ /dev/null
@@ -1,46 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: event.respondWith(r)</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#respond-with-method">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`event.respondWith(r)` method must run the steps, from step 10 to step 15,
-defined in the [_OnFetchRequest algorithm][1].
-
-The `r` argument must resolve with a [AbstractResponse][2], else a
-[NetworkError][3] is thrown. If the request is a top-level navigation and the
-return value is a [OpaqueResponse][4] (an opaque response body), a
-[NetworkError][3] is thrown. The final URL of all successful (non
-network-error) responses is the [requested][5] URL. Renderer-side security
-checks about tainting for cross-origin content are tied to the transparency (or
-opacity) of the [Response][6] body, not URLs.
-
-
-
-[1]: #on-fetch-request-algorithm
-[2]: #abstract-response-interface
-[3]: http://w3c.github.io/dom/#networkerror
-[4]: #opaque-response-interface
-[5]: #request-objects
-[6]: #response-interface
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section event.respondWith(r) so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.7.4.3-default-method.html b/third_party/web_platform_tests/service-workers/stub-4.7.4.3-default-method.html
deleted file mode 100644
index 52a8dbd..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.7.4.3-default-method.html
+++ /dev/null
@@ -1,52 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: event.default()</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#default-method">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-`event.default()` method must run these steps:
-
-1. Let `promise` be a newly-created [promise][1].
-2. Return `promise.`
-3. Run the following steps asynchronously:
- 1. Let `request` be `event`'s `request`.
- 2. Set `request`'s [skip service worker flag][2].
- 3. Let `response` be the result of running [fetch algorithm][3] with
- `request` as its argument.
- 4. If `response` is a [network error][4], then:
- 1. Reject `promise` with a new [DOMException][5] whose name is
- "[NetworkError][6]".
- 5. Else,
- 1. Resolve `promise` with a new [Response][7] object associated
- with `response`.
-
-
-
-[1]: http://goo.gl/3TobQS
-[2]: http://goo.gl/gP7IWW
-[3]: http://goo.gl/fGMifs
-[4]: http://goo.gl/jprjjc
-[5]: http://goo.gl/A0U8qC
-[6]: http://goo.gl/lud5HB
-[7]: http://goo.gl/Deazjv
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section event.default() so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-4.7.4.4-is-reload-attribute.html b/third_party/web_platform_tests/service-workers/stub-4.7.4.4-is-reload-attribute.html
deleted file mode 100644
index f116b68..0000000
--- a/third_party/web_platform_tests/service-workers/stub-4.7.4.4-is-reload-attribute.html
+++ /dev/null
@@ -1,32 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: event.isReload</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#is-reload-attribute">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-Returns true if `event` was dispatched with the user's intention for the page
-reload, and false otherwise. Pressing the refresh button should be considered a
-reload while clicking a link and pressing the back button should not. The
-behavior of the `Ctrl+l enter` is left to the implementations of the user
-agents.
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section event.isReload so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-5.1-origin-relativity.html b/third_party/web_platform_tests/service-workers/stub-5.1-origin-relativity.html
deleted file mode 100644
index e885de6..0000000
--- a/third_party/web_platform_tests/service-workers/stub-5.1-origin-relativity.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: Origin Relativity</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#origin-relativity">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-One of the advanced concerns that major applications would encounter is whether
-they can be hosted from a CDN. By definition, these are servers in other
-places, often on other domains. Therefore, Service Workers cannot be hosted on
-CDNs. But they can include resources via [importScripts()][1]. The reason for
-this restriction is that Service Workers create the opportunity for a bad actor
-to turn a bad day into a bad eternity.
-
-[1]: http://goo.gl/Owcfs2
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section Origin Relativity so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/stub-5.2-cross-origin-resources.html b/third_party/web_platform_tests/service-workers/stub-5.2-cross-origin-resources.html
deleted file mode 100644
index 3a10c9e..0000000
--- a/third_party/web_platform_tests/service-workers/stub-5.2-cross-origin-resources.html
+++ /dev/null
@@ -1,48 +0,0 @@
-<!DOCTYPE html>
-<html>
-<title>Service Workers: Cross-Origin Resources & CORS</title>
- <head>
- <link rel="help" href="https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#cross-origin-resources">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-
- </head>
- <body>
-
-<!--
-
-Applications tend to cache items that come from a CDN or other domain. It is
-possible to request many of them directly using <script>, <img>, <video> and
-<link> elements. It would be hugely limiting if this sort of runtime
-collaboration broke when offline. Similarly, it is possible to XHR many sorts
-of off-domain resources when appropriate CORS headers are set.
-
-ServiceWorkers enable this by allowing `Cache`s to fetch and cache off-origin
-items. Some restrictions apply, however. First, unlike same-origin resources
-which are managed in the `Cache` as `[Promise][1]`s for `Response` instances,
-the objects stored are `[Promise][1]`s for `OpaqueResponse` instances.
-`OpaqueResponse` provides a much less expressive API than `Response`; the
-bodies and headers cannot be read or set, nor many of the other aspects of
-their content inspected. They can be passed to `respondWith()` and
-`forwardTo()` in the same manner as `Response`s, but cannot be meaningfully
-created programmatically. These limitations are necessary to preserve the
-security invariants of the platform. Allowing `Cache`s to store them allows
-applications to avoid re-architecting in most cases.
-
-
-
-[1]: http://goo.gl/3TobQS
-
--->
-
-
-
- <script>
- test(function() {
- // not_implemented();
- }, "There are no tests for section Cross-Origin Resources & CORS so far.");
- </script>
-
- </body>
-</html>
-
diff --git a/third_party/web_platform_tests/service-workers/tools/blink-import.py b/third_party/web_platform_tests/service-workers/tools/blink-import.py
deleted file mode 100644
index 84c958c..0000000
--- a/third_party/web_platform_tests/service-workers/tools/blink-import.py
+++ /dev/null
@@ -1,204 +0,0 @@
-import os
-import re
-import shutil
-import glob
-import tempfile
-import sys
-from collections import defaultdict
-
-here = os.path.abspath(os.path.split(__file__)[0])
-
-def get_extra_files(chromium_root):
- return [(os.path.join(chromium_root, "LayoutTests", "http", "tests", "resources", "testharness-helpers.js"),
- os.path.join("resources", "testharness-helpers.js"))]
-
-resources_re = re.compile("/?(?:\.\./)*resources/(testharness(?:report)?)\.js")
-
-def resources_path(line, depth):
- return False, resources_re.sub(r"/resources/\1.js", line)
-
-php_re = re.compile("\.php")
-
-def python_to_php(line, depth):
- return False, php_re.sub(".py", line)
-
-abs_testharness_helpers_re = re.compile("([\"'])/resources/testharness-helpers.js")
-testharness_helpers_re = re.compile("\.\./((?:\.\./)*)resources/testharness-helpers.js")
-
-def testharness_helpers(line, depth):
- if abs_testharness_helpers_re.findall(line):
- return False, abs_testharness_helpers_re.sub(r"\1%sresources/testharness-helpers.js" % ("../" * (depth - 1)), line)
- return False, testharness_helpers_re.sub(r"\1resources/testharness-helpers.js", line)
-
-serviceworker_path_re = re.compile("/serviceworker/")
-def service_worker_path(line, depth):
- return False, serviceworker_path_re.sub("/service-workers/", line)
-
-localhost_re = re.compile("localhost")
-alt_host_re = re.compile("127\.0\.0\.1")
-port_http_re = re.compile("8000")
-port_https_re = re.compile("8000")
-
-
-def server_names(line, depth):
- line, count_0 = localhost_re.subn("{{host}}", line)
- line, count_1 = alt_host_re.subn("{{domains[www]}}", line)
- line, count_2 = port_http_re.subn("{{ports[http][0]}}", line)
- line, count_3 = port_https_re.subn("{{ports[https][0]}}", line)
-
- count = count_0 + count_1 + count_2 + count_3
-
- return bool(count), line
-
-
-def source_paths(chromium_root):
- for dirpath, dirnames, filenames in os.walk(chromium_root):
- if "chromium" in dirnames:
- dirnames.remove("chromium")
- for filename in filenames:
- if filename.endswith("-expected.txt") or filename.endswith(".php"):
- continue
- yield os.path.relpath(os.path.join(dirpath, filename), chromium_root)
-
-
-def do_subs(path, line):
- depth = len(os.path.split(os.path.sep))
- subs = [resources_path, python_to_php, testharness_helpers, service_worker_path, server_names]
- file_is_template = False
- for sub in subs:
- added_template, line = sub(line, depth)
- if added_template:
- file_is_template = True
- return file_is_template, line
-
-def get_head(git):
- return git("rev-parse", "HEAD")
-
-def get_changes(git, path, old, new):
- data = git("diff", "--name-status", "-z", "--no-renames", "%s..%s" % (old, new), "--", path)
- items = data.split("\0")
- rv = defaultdict(list)
- for status, path in items:
- rv[status].append(path)
-
- return rv
-
-def copy(src_path, out_dir, rel_path):
- dest = os.path.normpath(os.path.join(out_dir, rel_path))
- dest_dir = os.path.split(dest)[0]
- if not os.path.exists(dest_dir):
- os.makedirs(dest_dir)
- shutil.copy2(src_path, dest)
-
-def copy_local_files(local_files, out_root, tmp_dir):
- for path in local_files:
- rel_path = os.path.relpath(path, out_root)
- copy(path, tmp_dir, rel_path)
-
-def copy_extra_files(chromium_root, tmp_dir):
- for in_path, rel_path in get_extra_files(chromium_root):
- copy(in_path, tmp_dir, rel_path)
-
-def sub_changed_filenames(filename_changes, f):
- rv = []
- for line in f:
- for in_name, out_name in filename_changes.items():
- line = line.replace(in_name, out_name)
- rv.append(line)
- return "".join(rv)
-
-testharness_re = re.compile("<script[^>]*src=[\"']?/resources/testharness.js[\"' ][^>]*>")
-
-def is_top_level_test(path, data):
- if os.path.splitext(path)[1] != ".html":
- return False
- for line in data:
- if testharness_re.findall(line):
- return True
- return False
-
-def add_suffix(path, suffix):
- root, ext = os.path.splitext(path)
- return root + ".%s" % suffix + ext
-
-def main():
- if "--cache-tests" in sys.argv:
- sw_path = os.path.join("LayoutTests", "http", "tests", "cachestorage")
- out_root = os.path.abspath(os.path.join(here, "..", "cache-storage"))
- elif "--sw-tests" in sys.argv:
- sw_path = os.path.join("LayoutTests", "http", "tests", "serviceworkers")
- out_root = os.path.abspath(os.path.join(here, "..", "service-worker"))
- else:
- raise ValueError("Must supply either --cache-tests or --sw-tests")
-
- chromium_root = os.path.abspath(sys.argv[1])
-
- work_path = tempfile.mkdtemp()
-
- test_path = os.path.join(chromium_root, sw_path)
-
- local_files = glob.glob(os.path.normpath(os.path.join(here, "..", "resources", "*.py")))
-
- if not os.path.exists(out_root):
- os.mkdir(out_root)
-
- copy_local_files(local_files, out_root, work_path)
- copy_extra_files(chromium_root, work_path)
-
- path_changes = {}
-
- for path in source_paths(test_path):
- out_path = os.path.join(work_path, path)
- out_dir = os.path.dirname(out_path)
- if not os.path.exists(out_dir):
- os.makedirs(out_dir)
- with open(os.path.join(test_path, path)) as in_f:
- data = []
- sub = False
- for line in in_f:
- sub_flag, output_line = do_subs(path, line)
- data.append(output_line)
- if sub_flag:
- sub = True
- is_test = is_top_level_test(out_path, data)
-
- initial_path = out_path
-
- if is_test:
- path_1 = add_suffix(out_path, "https")
- else:
- path_1 = out_path
-
- if sub:
- path_2 = add_suffix(out_path, "sub")
- else:
- path_2 = path_1
-
- if path_2 != initial_path:
- path_changes[initial_path] = path_2
-
- with open(path_2, "w") as out_f:
- out_f.write("".join(data))
-
- filename_changes = {}
-
- for k, v in path_changes.items():
- if os.path.basename(k) in filename_changes:
- print "Got duplicate name:" + os.path.basename(k)
- filename_changes[os.path.basename(k)] = os.path.basename(v)
-
- for path in source_paths(work_path):
- full_path = os.path.join(work_path, path)
- with open(full_path) as f:
- data = sub_changed_filenames(filename_changes, f)
- with open(full_path, "w") as f:
- f.write(data)
-
- for dirpath, dirnames, filenames in os.walk(work_path):
- for filename in filenames:
- in_path = os.path.join(dirpath, filename)
- rel_path = os.path.relpath(in_path, work_path)
- copy(in_path, out_root, rel_path)
-
-if __name__ == "__main__":
- main()
diff --git a/third_party/web_platform_tests/tools/wptserve/wptserve/handlers.py b/third_party/web_platform_tests/tools/wptserve/wptserve/handlers.py
index 0da9e31..9463a15 100644
--- a/third_party/web_platform_tests/tools/wptserve/wptserve/handlers.py
+++ b/third_party/web_platform_tests/tools/wptserve/wptserve/handlers.py
@@ -1,4 +1,4 @@
-import cgi
+import html
import json
import os
import traceback
@@ -72,7 +72,7 @@
<ul>
%(items)s
</ul>
-""" % {"path": cgi.escape(url_path),
+""" % {"path": html.escape(url_path),
"items": "\n".join(self.list_items(url_path, path))} # flake8: noqa
def list_items(self, base_path, path):
@@ -89,14 +89,14 @@
yield ("""<li class="dir"><a href="%(link)s">%(name)s</a></li>""" %
{"link": link, "name": ".."})
for item in sorted(os.listdir(path)):
- link = cgi.escape(quote(item))
+ link = html.escape(quote(item))
if os.path.isdir(os.path.join(path, item)):
link += "/"
class_ = "dir"
else:
class_ = "file"
yield ("""<li class="%(class)s"><a href="%(link)s">%(name)s</a></li>""" %
- {"link": link, "name": cgi.escape(item), "class": class_})
+ {"link": link, "name": html.escape(item), "class": class_})
def wrap_pipeline(path, request, response):
diff --git a/tools/on_device_tests_gateway.proto b/tools/on_device_tests_gateway.proto
new file mode 100644
index 0000000..3562265
--- /dev/null
+++ b/tools/on_device_tests_gateway.proto
@@ -0,0 +1,66 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package on_device_tests_gateway;
+
+// Interface exported by the server.
+service on_device_tests_gateway {
+ // A dumb proxy RPC service that passes user defined command line options
+ // to the on-device tests gateway and streams back output in real time.
+ rpc exec_command (OnDeviceTestsCommand) returns (stream OnDeviceTestsResponse) {
+ }
+
+ rpc exec_watch_command (OnDeviceTestsWatchCommand) returns (stream OnDeviceTestsResponse) {
+ }
+}
+
+// Working directory and command line arguments to be passed to the gateway.
+message OnDeviceTestsCommand {
+ enum Config {
+ devel = 0;
+ }
+
+ string workdir = 1;
+ string token = 2;
+ string test_type = 3;
+ string platform = 4;
+ string archive_path = 5;
+ string config = 6;
+ string tag = 7;
+ repeated string labels = 8;
+ string builder_name = 9;
+ string change_id = 10;
+ string build_number = 11;
+ string loader_platform = 12;
+ string loader_config = 13;
+ string version = 14;
+ optional bool dry_run = 15;
+ repeated string dimension = 16;
+}
+
+// Working directory and command line arguments to be passed to the gateway.
+message OnDeviceTestsWatchCommand {
+ string workdir = 1;
+ string token = 2;
+ string session_id = 3;
+ string change_id = 4;
+ optional bool dry_run = 5;
+}
+
+// Response from the on-device tests.
+message OnDeviceTestsResponse {
+ string response = 1;
+}
diff --git a/tools/on_device_tests_gateway_client.py b/tools/on_device_tests_gateway_client.py
new file mode 100644
index 0000000..6a799bb
--- /dev/null
+++ b/tools/on_device_tests_gateway_client.py
@@ -0,0 +1,241 @@
+#!/usr/bin/env python3
+#
+# Copyright 2022 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""gRPC On-device Tests Gateway client."""
+
+import argparse
+import logging
+import sys
+
+import grpc
+
+import on_device_tests_gateway_pb2
+import on_device_tests_gateway_pb2_grpc
+
+# All tests On-Device tests support
+_TEST_TYPES = [
+ 'black_box_test',
+ 'evergreen_test',
+ 'unit_test',
+]
+
+# All test configs On-Device tests support
+_TEST_CONFIGS = [
+ 'devel',
+ 'staging',
+ 'production',
+]
+
+_WORK_DIR = '/on_device_tests_gateway'
+_ON_DEVICE_TESTS_GATEWAY_SERVICE_HOST = (
+ 'on-device-tests-gateway-service.on-device-tests.svc.cluster.local')
+_ON_DEVICE_TESTS_GATEWAY_SERVICE_PORT = '50052'
+
+
+class OnDeviceTestsGatewayClient():
+ """On-device tests Gateway Client class."""
+
+ def __init__(self):
+ self.channel = grpc.insecure_channel(
+ target=f'{_ON_DEVICE_TESTS_GATEWAY_SERVICE_HOST}:{_ON_DEVICE_TESTS_GATEWAY_SERVICE_PORT}', # pylint:disable=line-too-long
+ # These options need to match server settings.
+ options=[('grpc.keepalive_time_ms', 10000),
+ ('grpc.keepalive_timeout_ms', 5000),
+ ('grpc.keepalive_permit_without_calls', 1),
+ ('grpc.http2.max_pings_without_data', 0),
+ ('grpc.http2.min_time_between_pings_ms', 10000),
+ ('grpc.http2.min_ping_interval_without_data_ms', 5000)])
+ self.stub = on_device_tests_gateway_pb2_grpc.on_device_tests_gatewayStub(
+ self.channel)
+
+ def run_trigger_command(self, workdir: str, args: argparse.Namespace):
+ """Calls On-Device Tests service and passing given parameters to it.
+
+ Args:
+ workdir (str): Current script workdir.
+ args (Namespace): Arguments passed in command line.
+ """
+ for response_line in self.stub.exec_command(
+ on_device_tests_gateway_pb2.OnDeviceTestsCommand(
+ workdir=workdir,
+ token=args.token,
+ test_type=args.test_type,
+ platform=args.platform,
+ archive_path=args.archive_path,
+ config=args.config,
+ tag=args.tag,
+ labels=args.label,
+ builder_name=args.builder_name,
+ change_id=args.change_id,
+ build_number=args.build_number,
+ loader_platform=args.loader_platform,
+ loader_config=args.loader_config,
+ version=args.version,
+ dry_run=args.dry_run,
+ dimension=args.dimension or [],
+ )):
+
+ print(response_line.response)
+
+ def run_watch_command(self, workdir: str, args: argparse.Namespace):
+ """Calls On-Device Tests watch service and passing given parameters to it.
+
+ Args:
+ workdir (str): Current script workdir.
+ args (Namespace): Arguments passed in command line.
+ """
+ for response_line in self.stub.exec_watch_command(
+ on_device_tests_gateway_pb2.OnDeviceTestsWatchCommand(
+ workdir=workdir,
+ token=args.token,
+ change_id=args.change_id,
+ session_id=args.session_id,
+ )):
+
+ print(response_line.response)
+
+
+def main():
+ """Main routine."""
+ logging.basicConfig(
+ level=logging.INFO, format='[%(filename)s:%(lineno)s] %(message)s')
+
+ parser = argparse.ArgumentParser(
+ epilog=('Example: ./on_device_tests_gateway_client.py '
+ '--test_type=unit_test '
+ '--remote_archive_path=gs://my_bucket/path/to/artifacts.tar '
+ '--platform=raspi-2 '
+ '--config=devel'),
+ formatter_class=argparse.RawDescriptionHelpFormatter)
+ parser.add_argument(
+ '-t',
+ '--token',
+ type=str,
+ required=True,
+ help='On Device Tests authentication token')
+ parser.add_argument(
+ '--dry_run',
+ action='store_true',
+ help='Specifies to show what would be done without actually doing it.')
+ parser.add_argument(
+ '-i',
+ '--change_id',
+ type=str,
+ help='ChangeId that triggered this test, if any. '
+ 'Saved with performance test results.')
+
+ subparsers = parser.add_subparsers(
+ dest='action', help='On-Device tests commands', required=True)
+ trigger_parser = subparsers.add_parser(
+ 'trigger', help='Trigger On-Device tests')
+
+ trigger_parser.add_argument(
+ '-e',
+ '--test_type',
+ type=str,
+ choices=_TEST_TYPES,
+ required=True,
+ help='Type of test to run.')
+ trigger_parser.add_argument(
+ '-p',
+ '--platform',
+ type=str,
+ required=True,
+ help='Platform this test was built for.')
+ trigger_parser.add_argument(
+ '-c',
+ '--config',
+ type=str,
+ choices=_TEST_CONFIGS,
+ required=True,
+ help='Cobalt config being tested.')
+ trigger_parser.add_argument(
+ '-a',
+ '--archive_path',
+ type=str,
+ required=True,
+ help='Path to Cobalt archive to be tested. Must be on gcs.')
+ trigger_parser.add_argument(
+ '-g',
+ '--tag',
+ type=str,
+ help='Value saved with performance results. '
+ 'Indicates why this test was triggered.')
+ trigger_parser.add_argument(
+ '-l',
+ '--label',
+ type=str,
+ default=[],
+ action='append',
+ help='Additional labels to assign to the test.')
+ trigger_parser.add_argument(
+ '-b',
+ '--builder_name',
+ type=str,
+ help='Name of the builder that built the artifacts, '
+ 'if any. Saved with performance test results')
+ trigger_parser.add_argument(
+ '-n',
+ '--build_number',
+ type=str,
+ help='Build number associated with the build, if any. '
+ 'Saved with performance test results.')
+ trigger_parser.add_argument(
+ '--loader_platform',
+ type=str,
+ help='Platform of the loader to run the test. Only '
+ 'applicable in Evergreen mode.')
+ trigger_parser.add_argument(
+ '--loader_config',
+ type=str,
+ help='Cobalt config of the loader to run the test. Only '
+ 'applicable in Evergreen mode.')
+ trigger_parser.add_argument(
+ '--dimension',
+ type=str,
+ action='append',
+ help='On-Device Tests dimension used to select a device. '
+ 'Must have the following form: <dimension>=<value>.'
+ ' E.G. "release_version=regex:10.*')
+ trigger_parser.add_argument(
+ '--version',
+ type=str,
+ default='COBALT',
+ help='Cobalt version being tested.')
+
+ watch_parser = subparsers.add_parser('watch', help='Trigger On-Device tests')
+ watch_parser.add_argument(
+ 'session_id',
+ type=str,
+ help='Session id of a previously triggered mobile '
+ 'harness test. If passed, the test will not be '
+ 'triggered, but will be watched until the exit '
+ 'status is reached.')
+
+ args = parser.parse_args()
+
+ client = OnDeviceTestsGatewayClient()
+ try:
+ if args.action == 'trigger':
+ client.run_trigger_command(workdir=_WORK_DIR, args=args)
+ else:
+ client.run_watch_command(workdir=_WORK_DIR, args=args)
+ except grpc.RpcError as e:
+ print(e)
+ return e.code().value
+
+
+if __name__ == '__main__':
+ sys.exit(main())