mirror of
https://github.com/servo/servo
synced 2026-04-25 17:15:48 +02:00
Implement StorageManager API (#43976)
Add the Storage Standard WebIDL for NavigatorStorage and StorageManager, wire navigator.storage on Window and Worker, and implement persisted(), persist(), and estimate(). Testing: covered by WP test. part of #39100 fixes #39101 --------- Signed-off-by: Taym Haddadi <haddadi.taym@gmail.com>
This commit is contained in:
@@ -176,6 +176,8 @@ pub struct Preferences {
|
|||||||
// feature: Sanitizer API | #43948 | Web/API/HTML_Sanitizer_API
|
// feature: Sanitizer API | #43948 | Web/API/HTML_Sanitizer_API
|
||||||
pub dom_sanitizer_enabled: bool,
|
pub dom_sanitizer_enabled: bool,
|
||||||
pub dom_script_asynch: bool,
|
pub dom_script_asynch: bool,
|
||||||
|
// feature: Storage API | #43976 | Web/API/Storage_API
|
||||||
|
pub dom_storage_manager_api_enabled: bool,
|
||||||
// feature: ServiceWorker | #36538 | Web/API/Service_Worker_API
|
// feature: ServiceWorker | #36538 | Web/API/Service_Worker_API
|
||||||
pub dom_serviceworker_enabled: bool,
|
pub dom_serviceworker_enabled: bool,
|
||||||
pub dom_serviceworker_timeout_seconds: i64,
|
pub dom_serviceworker_timeout_seconds: i64,
|
||||||
@@ -392,6 +394,7 @@ impl Preferences {
|
|||||||
dom_resize_observer_enabled: true,
|
dom_resize_observer_enabled: true,
|
||||||
dom_sanitizer_enabled: false,
|
dom_sanitizer_enabled: false,
|
||||||
dom_script_asynch: true,
|
dom_script_asynch: true,
|
||||||
|
dom_storage_manager_api_enabled: false,
|
||||||
dom_serviceworker_enabled: false,
|
dom_serviceworker_enabled: false,
|
||||||
dom_serviceworker_timeout_seconds: 60,
|
dom_serviceworker_timeout_seconds: 60,
|
||||||
dom_servo_helpers_enabled: false,
|
dom_servo_helpers_enabled: false,
|
||||||
|
|||||||
@@ -350,6 +350,7 @@ pub(crate) mod servoparser;
|
|||||||
pub(crate) mod shadowroot;
|
pub(crate) mod shadowroot;
|
||||||
pub(crate) mod staticrange;
|
pub(crate) mod staticrange;
|
||||||
pub(crate) mod storage;
|
pub(crate) mod storage;
|
||||||
|
pub(crate) mod storagemanager;
|
||||||
pub(crate) mod stream;
|
pub(crate) mod stream;
|
||||||
pub(crate) use self::stream::*;
|
pub(crate) use self::stream::*;
|
||||||
pub(crate) mod svg;
|
pub(crate) mod svg;
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ use crate::dom::permissions::Permissions;
|
|||||||
use crate::dom::pluginarray::PluginArray;
|
use crate::dom::pluginarray::PluginArray;
|
||||||
use crate::dom::serviceworkercontainer::ServiceWorkerContainer;
|
use crate::dom::serviceworkercontainer::ServiceWorkerContainer;
|
||||||
use crate::dom::servointernals::ServoInternals;
|
use crate::dom::servointernals::ServoInternals;
|
||||||
|
use crate::dom::storagemanager::StorageManager;
|
||||||
use crate::dom::types::UserActivation;
|
use crate::dom::types::UserActivation;
|
||||||
use crate::dom::wakelock::WakeLock;
|
use crate::dom::wakelock::WakeLock;
|
||||||
#[cfg(feature = "webgpu")]
|
#[cfg(feature = "webgpu")]
|
||||||
@@ -127,6 +128,7 @@ pub(crate) struct Navigator {
|
|||||||
permissions: MutNullableDom<Permissions>,
|
permissions: MutNullableDom<Permissions>,
|
||||||
mediasession: MutNullableDom<MediaSession>,
|
mediasession: MutNullableDom<MediaSession>,
|
||||||
clipboard: MutNullableDom<Clipboard>,
|
clipboard: MutNullableDom<Clipboard>,
|
||||||
|
storage: MutNullableDom<StorageManager>,
|
||||||
#[cfg(feature = "webgpu")]
|
#[cfg(feature = "webgpu")]
|
||||||
gpu: MutNullableDom<GPU>,
|
gpu: MutNullableDom<GPU>,
|
||||||
/// <https://www.w3.org/TR/gamepad/#dfn-hasgamepadgesture>
|
/// <https://www.w3.org/TR/gamepad/#dfn-hasgamepadgesture>
|
||||||
@@ -155,6 +157,7 @@ impl Navigator {
|
|||||||
permissions: Default::default(),
|
permissions: Default::default(),
|
||||||
mediasession: Default::default(),
|
mediasession: Default::default(),
|
||||||
clipboard: Default::default(),
|
clipboard: Default::default(),
|
||||||
|
storage: Default::default(),
|
||||||
#[cfg(feature = "webgpu")]
|
#[cfg(feature = "webgpu")]
|
||||||
gpu: Default::default(),
|
gpu: Default::default(),
|
||||||
#[cfg(feature = "gamepad")]
|
#[cfg(feature = "gamepad")]
|
||||||
@@ -478,6 +481,12 @@ impl NavigatorMethods<crate::DomTypeHolder> for Navigator {
|
|||||||
.or_init(|| Clipboard::new(cx, &self.global()))
|
.or_init(|| Clipboard::new(cx, &self.global()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <https://storage.spec.whatwg.org/#api>
|
||||||
|
fn Storage(&self, cx: &mut js::context::JSContext) -> DomRoot<StorageManager> {
|
||||||
|
self.storage
|
||||||
|
.or_init(|| StorageManager::new(&self.global(), CanGc::from_cx(cx)))
|
||||||
|
}
|
||||||
|
|
||||||
/// <https://w3c.github.io/beacon/#sec-processing-model>
|
/// <https://w3c.github.io/beacon/#sec-processing-model>
|
||||||
fn SendBeacon(&self, url: USVString, data: Option<BodyInit>, can_gc: CanGc) -> Fallible<bool> {
|
fn SendBeacon(&self, url: USVString, data: Option<BodyInit>, can_gc: CanGc) -> Fallible<bool> {
|
||||||
let global = self.global();
|
let global = self.global();
|
||||||
|
|||||||
@@ -276,14 +276,9 @@ impl PermissionAlgorithm for Permissions {
|
|||||||
match status.State() {
|
match status.State() {
|
||||||
// Step 3.
|
// Step 3.
|
||||||
PermissionState::Prompt => {
|
PermissionState::Prompt => {
|
||||||
// https://w3c.github.io/permissions/#request-permission-to-use (Step 3 - 4)
|
|
||||||
let permission_name = status.get_query();
|
let permission_name = status.get_query();
|
||||||
let globalscope = GlobalScope::current().expect("No current global object");
|
let globalscope = GlobalScope::current().expect("No current global object");
|
||||||
let state = prompt_user_from_embedder(permission_name, &globalscope);
|
request_permission_to_use(permission_name, &globalscope);
|
||||||
globalscope
|
|
||||||
.permission_state_invocation_results()
|
|
||||||
.borrow_mut()
|
|
||||||
.insert(permission_name, state);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Step 2.
|
// Step 2.
|
||||||
@@ -354,6 +349,24 @@ pub(crate) fn descriptor_permission_state(
|
|||||||
PermissionState::Prompt
|
PermissionState::Prompt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <https://w3c.github.io/permissions/#request-permission-to-use>
|
||||||
|
pub(crate) fn request_permission_to_use(
|
||||||
|
name: PermissionName,
|
||||||
|
global_scope: &GlobalScope,
|
||||||
|
) -> PermissionState {
|
||||||
|
let state = descriptor_permission_state(name, Some(global_scope));
|
||||||
|
if state != PermissionState::Prompt {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = prompt_user_from_embedder(name, global_scope);
|
||||||
|
global_scope
|
||||||
|
.permission_state_invocation_results()
|
||||||
|
.borrow_mut()
|
||||||
|
.insert(name, state);
|
||||||
|
descriptor_permission_state(name, Some(global_scope))
|
||||||
|
}
|
||||||
|
|
||||||
fn prompt_user_from_embedder(name: PermissionName, global_scope: &GlobalScope) -> PermissionState {
|
fn prompt_user_from_embedder(name: PermissionName, global_scope: &GlobalScope) -> PermissionState {
|
||||||
let Some(webview_id) = global_scope.webview_id() else {
|
let Some(webview_id) = global_scope.webview_id() else {
|
||||||
warn!("Requesting permissions from non-webview-associated global scope");
|
warn!("Requesting permissions from non-webview-associated global scope");
|
||||||
|
|||||||
279
components/script/dom/storagemanager.rs
Normal file
279
components/script/dom/storagemanager.rs
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use dom_struct::dom_struct;
|
||||||
|
use servo_base::generic_channel::GenericCallback;
|
||||||
|
|
||||||
|
use crate::dom::bindings::codegen::Bindings::PermissionStatusBinding::{
|
||||||
|
PermissionName, PermissionState,
|
||||||
|
};
|
||||||
|
use crate::dom::bindings::codegen::Bindings::StorageManagerBinding::{
|
||||||
|
StorageEstimate, StorageManagerMethods,
|
||||||
|
};
|
||||||
|
use crate::dom::bindings::error::Error;
|
||||||
|
use crate::dom::bindings::refcounted::TrustedPromise;
|
||||||
|
use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
|
||||||
|
use crate::dom::bindings::root::DomRoot;
|
||||||
|
use crate::dom::globalscope::GlobalScope;
|
||||||
|
use crate::dom::permissions::request_permission_to_use;
|
||||||
|
use crate::dom::promise::Promise;
|
||||||
|
use crate::realms::InRealm;
|
||||||
|
use crate::script_runtime::CanGc;
|
||||||
|
use crate::task_source::SendableTaskSource;
|
||||||
|
|
||||||
|
#[dom_struct]
|
||||||
|
pub(crate) struct StorageManager {
|
||||||
|
reflector_: Reflector,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StorageManager {
|
||||||
|
fn new_inherited() -> StorageManager {
|
||||||
|
StorageManager {
|
||||||
|
reflector_: Reflector::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot<StorageManager> {
|
||||||
|
reflect_dom_object(Box::new(StorageManager::new_inherited()), global, can_gc)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn origin_cannot_obtain_local_storage_shelf(&self) -> bool {
|
||||||
|
!self.global().origin().is_tuple()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn type_error_from_string(message: String) -> Error {
|
||||||
|
let message = std::ffi::CString::new(message)
|
||||||
|
.unwrap_or_else(|_| c"Storage operation failed".to_owned());
|
||||||
|
Error::Type(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StorageManagerBooleanResponseHandler {
|
||||||
|
trusted_promise: Option<TrustedPromise>,
|
||||||
|
task_source: SendableTaskSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StorageManagerBooleanResponseHandler {
|
||||||
|
fn new(trusted_promise: TrustedPromise, task_source: SendableTaskSource) -> Self {
|
||||||
|
Self {
|
||||||
|
trusted_promise: Some(trusted_promise),
|
||||||
|
task_source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle(&mut self, result: Result<bool, String>) {
|
||||||
|
let Some(trusted_promise) = self.trusted_promise.take() else {
|
||||||
|
error!("StorageManager callback called twice.");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.task_source
|
||||||
|
.queue(task!(storage_manager_boolean_response: move |cx| {
|
||||||
|
let promise = trusted_promise.root();
|
||||||
|
match result {
|
||||||
|
Ok(value) => promise.resolve_native(&value, CanGc::from_cx(cx)),
|
||||||
|
Err(message) => promise.reject_error(
|
||||||
|
StorageManager::type_error_from_string(message),
|
||||||
|
CanGc::from_cx(cx),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StorageManagerEstimateResponseHandler {
|
||||||
|
trusted_promise: Option<TrustedPromise>,
|
||||||
|
task_source: SendableTaskSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StorageManagerEstimateResponseHandler {
|
||||||
|
fn new(trusted_promise: TrustedPromise, task_source: SendableTaskSource) -> Self {
|
||||||
|
Self {
|
||||||
|
trusted_promise: Some(trusted_promise),
|
||||||
|
task_source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle(&mut self, result: Result<(u64, u64), String>) {
|
||||||
|
let Some(trusted_promise) = self.trusted_promise.take() else {
|
||||||
|
error!("StorageManager callback called twice.");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.task_source
|
||||||
|
.queue(task!(storage_manager_estimate_response: move |cx| {
|
||||||
|
let promise = trusted_promise.root();
|
||||||
|
match result {
|
||||||
|
Ok((usage, quota)) => {
|
||||||
|
let mut estimate = StorageEstimate::empty();
|
||||||
|
estimate.usage = Some(usage);
|
||||||
|
estimate.quota = Some(quota);
|
||||||
|
promise.resolve_native(&estimate, CanGc::from_cx(cx));
|
||||||
|
},
|
||||||
|
Err(message) => {
|
||||||
|
promise.reject_error(
|
||||||
|
StorageManager::type_error_from_string(message),
|
||||||
|
CanGc::from_cx(cx),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StorageManagerMethods<crate::DomTypeHolder> for StorageManager {
|
||||||
|
/// <https://storage.spec.whatwg.org/#dom-storagemanager-persisted>
|
||||||
|
fn Persisted(&self, comp: InRealm, can_gc: CanGc) -> Rc<Promise> {
|
||||||
|
// Step 1. Let promise be a new promise.
|
||||||
|
let promise = Promise::new_in_current_realm(comp, can_gc);
|
||||||
|
// Step 2. Let global be this’s relevant global object.
|
||||||
|
let global = self.global();
|
||||||
|
|
||||||
|
// Step 3. Let shelf be the result of running obtain a local storage shelf with this’s relevant
|
||||||
|
// settings object.
|
||||||
|
// Step 4. If shelf is failure, then reject promise with a TypeError.
|
||||||
|
if self.origin_cannot_obtain_local_storage_shelf() {
|
||||||
|
promise.reject_error(
|
||||||
|
Error::Type(c"Storage is unavailable for opaque origins".to_owned()),
|
||||||
|
can_gc,
|
||||||
|
);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5. Otherwise, run these steps in parallel:
|
||||||
|
// Step 5.1. Let persisted be true if shelf’s bucket map["default"]'s mode is "persistent";
|
||||||
|
// otherwise false.
|
||||||
|
// It will be false when there’s an internal error.
|
||||||
|
// Step 5.2. Queue a storage task with global to resolve promise with persisted.
|
||||||
|
let mut handler = StorageManagerBooleanResponseHandler::new(
|
||||||
|
TrustedPromise::new(promise.clone()),
|
||||||
|
global.task_manager().storage_task_source().to_sendable(),
|
||||||
|
);
|
||||||
|
let callback = GenericCallback::new(move |message| {
|
||||||
|
handler.handle(message.unwrap_or_else(|error| Err(error.to_string())));
|
||||||
|
})
|
||||||
|
.expect("Could not create StorageManager persisted callback");
|
||||||
|
|
||||||
|
if global
|
||||||
|
.storage_threads()
|
||||||
|
.persisted(global.origin().immutable().clone(), callback.clone())
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
if let Err(error) = callback.send(Err("Failed to queue storage task".to_owned())) {
|
||||||
|
error!("Failed to deliver StorageManager persisted error: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 6. Return promise.
|
||||||
|
promise
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <https://storage.spec.whatwg.org/#dom-storagemanager-persist>
|
||||||
|
fn Persist(&self, comp: InRealm, can_gc: CanGc) -> Rc<Promise> {
|
||||||
|
// Step 1. Let promise be a new promise.
|
||||||
|
let promise = Promise::new_in_current_realm(comp, can_gc);
|
||||||
|
// Step 2. Let global be this’s relevant global object.
|
||||||
|
let global = self.global();
|
||||||
|
|
||||||
|
// Step 3. Let shelf be the result of running obtain a local storage shelf with this’s relevant
|
||||||
|
// settings object.
|
||||||
|
// Step 4. If shelf is failure, then reject promise with a TypeError.
|
||||||
|
if self.origin_cannot_obtain_local_storage_shelf() {
|
||||||
|
promise.reject_error(
|
||||||
|
Error::Type(c"Storage is unavailable for opaque origins".to_owned()),
|
||||||
|
can_gc,
|
||||||
|
);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5. Otherwise, run these steps in parallel:
|
||||||
|
// Step 5.1. Let permission be the result of requesting permission to use
|
||||||
|
// "persistent-storage".
|
||||||
|
let permission = request_permission_to_use(PermissionName::Persistent_storage, &global);
|
||||||
|
|
||||||
|
// Step 5.2. Let bucket be shelf’s bucket map["default"].
|
||||||
|
// Step 5.3. Let persisted be true if bucket’s mode is "persistent"; otherwise false.
|
||||||
|
// It will be false when there’s an internal error.
|
||||||
|
// Step 5.4. If persisted is false and permission is "granted", then:
|
||||||
|
// Step 5.4.1. Set bucket’s mode to "persistent".
|
||||||
|
// Step 5.4.2. If there was no internal error, then set persisted to true.
|
||||||
|
// Step 5.5. Queue a storage task with global to resolve promise with persisted.
|
||||||
|
let mut handler = StorageManagerBooleanResponseHandler::new(
|
||||||
|
TrustedPromise::new(promise.clone()),
|
||||||
|
global.task_manager().storage_task_source().to_sendable(),
|
||||||
|
);
|
||||||
|
let callback = GenericCallback::new(move |message| {
|
||||||
|
handler.handle(message.unwrap_or_else(|error| Err(error.to_string())));
|
||||||
|
})
|
||||||
|
.expect("Could not create StorageManager persist callback");
|
||||||
|
|
||||||
|
if global
|
||||||
|
.storage_threads()
|
||||||
|
.persist(
|
||||||
|
global.origin().immutable().clone(),
|
||||||
|
permission == PermissionState::Granted,
|
||||||
|
callback.clone(),
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
if let Err(error) = callback.send(Err("Failed to queue storage task".to_owned())) {
|
||||||
|
error!("Failed to deliver StorageManager persist error: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 6. Return promise.
|
||||||
|
promise
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <https://storage.spec.whatwg.org/#dom-storagemanager-estimate>
|
||||||
|
fn Estimate(&self, comp: InRealm, can_gc: CanGc) -> Rc<Promise> {
|
||||||
|
// Step 1. Let promise be a new promise.
|
||||||
|
let promise = Promise::new_in_current_realm(comp, can_gc);
|
||||||
|
// Step 2. Let global be this’s relevant global object.
|
||||||
|
let global = self.global();
|
||||||
|
|
||||||
|
// Step 3. Let shelf be the result of running obtain a local storage shelf with this’s relevant
|
||||||
|
// settings object.
|
||||||
|
// Step 4. If shelf is failure, then reject promise with a TypeError.
|
||||||
|
if self.origin_cannot_obtain_local_storage_shelf() {
|
||||||
|
promise.reject_error(
|
||||||
|
Error::Type(c"Storage is unavailable for opaque origins".to_owned()),
|
||||||
|
can_gc,
|
||||||
|
);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5. Otherwise, run these steps in parallel:
|
||||||
|
// Step 5.1. Let usage be storage usage for shelf.
|
||||||
|
// Step 5.2. Let quota be storage quota for shelf.
|
||||||
|
// Step 5.3. Let dictionary be a new StorageEstimate dictionary whose usage member is usage and quota
|
||||||
|
// member is quota.
|
||||||
|
// Step 5.4. If there was an internal error while obtaining usage and quota, then queue a storage
|
||||||
|
// task with global to reject promise with a TypeError.
|
||||||
|
// Step 5.5. Otherwise, queue a storage task with global to resolve promise with dictionary.
|
||||||
|
let mut handler = StorageManagerEstimateResponseHandler::new(
|
||||||
|
TrustedPromise::new(promise.clone()),
|
||||||
|
global.task_manager().storage_task_source().to_sendable(),
|
||||||
|
);
|
||||||
|
let callback = GenericCallback::new(move |message| {
|
||||||
|
handler.handle(message.unwrap_or_else(|error| Err(error.to_string())));
|
||||||
|
})
|
||||||
|
.expect("Could not create StorageManager estimate callback");
|
||||||
|
|
||||||
|
if global
|
||||||
|
.storage_threads()
|
||||||
|
.estimate(global.origin().immutable().clone(), callback.clone())
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
if let Err(error) = callback.send(Err("Failed to queue storage task".to_owned())) {
|
||||||
|
error!("Failed to deliver StorageManager estimate error: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 6. Return promise.
|
||||||
|
promise
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ use crate::dom::bindings::utils::to_frozen_array;
|
|||||||
use crate::dom::navigator::hardware_concurrency;
|
use crate::dom::navigator::hardware_concurrency;
|
||||||
use crate::dom::navigatorinfo;
|
use crate::dom::navigatorinfo;
|
||||||
use crate::dom::permissions::Permissions;
|
use crate::dom::permissions::Permissions;
|
||||||
|
use crate::dom::storagemanager::StorageManager;
|
||||||
#[cfg(feature = "webgpu")]
|
#[cfg(feature = "webgpu")]
|
||||||
use crate::dom::webgpu::gpu::GPU;
|
use crate::dom::webgpu::gpu::GPU;
|
||||||
use crate::dom::workerglobalscope::WorkerGlobalScope;
|
use crate::dom::workerglobalscope::WorkerGlobalScope;
|
||||||
@@ -24,6 +25,7 @@ use crate::script_runtime::{CanGc, JSContext};
|
|||||||
pub(crate) struct WorkerNavigator {
|
pub(crate) struct WorkerNavigator {
|
||||||
reflector_: Reflector,
|
reflector_: Reflector,
|
||||||
permissions: MutNullableDom<Permissions>,
|
permissions: MutNullableDom<Permissions>,
|
||||||
|
storage: MutNullableDom<StorageManager>,
|
||||||
#[cfg(feature = "webgpu")]
|
#[cfg(feature = "webgpu")]
|
||||||
gpu: MutNullableDom<GPU>,
|
gpu: MutNullableDom<GPU>,
|
||||||
}
|
}
|
||||||
@@ -33,6 +35,7 @@ impl WorkerNavigator {
|
|||||||
WorkerNavigator {
|
WorkerNavigator {
|
||||||
reflector_: Reflector::new(),
|
reflector_: Reflector::new(),
|
||||||
permissions: Default::default(),
|
permissions: Default::default(),
|
||||||
|
storage: Default::default(),
|
||||||
#[cfg(feature = "webgpu")]
|
#[cfg(feature = "webgpu")]
|
||||||
gpu: Default::default(),
|
gpu: Default::default(),
|
||||||
}
|
}
|
||||||
@@ -115,6 +118,12 @@ impl WorkerNavigatorMethods<crate::DomTypeHolder> for WorkerNavigator {
|
|||||||
.or_init(|| Permissions::new(&self.global(), CanGc::deprecated_note()))
|
.or_init(|| Permissions::new(&self.global(), CanGc::deprecated_note()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <https://storage.spec.whatwg.org/#api>
|
||||||
|
fn Storage(&self, cx: &mut js::context::JSContext) -> DomRoot<StorageManager> {
|
||||||
|
self.storage
|
||||||
|
.or_init(|| StorageManager::new(&self.global(), CanGc::from_cx(cx)))
|
||||||
|
}
|
||||||
|
|
||||||
// https://gpuweb.github.io/gpuweb/#dom-navigator-gpu
|
// https://gpuweb.github.io/gpuweb/#dom-navigator-gpu
|
||||||
#[cfg(feature = "webgpu")]
|
#[cfg(feature = "webgpu")]
|
||||||
fn Gpu(&self) -> DomRoot<GPU> {
|
fn Gpu(&self) -> DomRoot<GPU> {
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ impl TaskManager {
|
|||||||
intersection_observer_task_source,
|
intersection_observer_task_source,
|
||||||
IntersectionObserver
|
IntersectionObserver
|
||||||
);
|
);
|
||||||
|
task_source_functions!(self, storage_task_source, Storage);
|
||||||
#[cfg(feature = "webgpu")]
|
#[cfg(feature = "webgpu")]
|
||||||
task_source_functions!(self, webgpu_task_source, WebGPU);
|
task_source_functions!(self, webgpu_task_source, WebGPU);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ pub(crate) enum TaskSourceName {
|
|||||||
Geolocation,
|
Geolocation,
|
||||||
/// <https://w3c.github.io/IntersectionObserver/#intersectionobserver-task-source>
|
/// <https://w3c.github.io/IntersectionObserver/#intersectionobserver-task-source>
|
||||||
IntersectionObserver,
|
IntersectionObserver,
|
||||||
|
/// <https://storage.spec.whatwg.org/#storage-task-source>
|
||||||
|
Storage,
|
||||||
/// <https://www.w3.org/TR/webgpu/#-webgpu-task-source>
|
/// <https://www.w3.org/TR/webgpu/#-webgpu-task-source>
|
||||||
WebGPU,
|
WebGPU,
|
||||||
}
|
}
|
||||||
@@ -85,6 +87,7 @@ impl From<TaskSourceName> for ScriptThreadEventCategory {
|
|||||||
TaskSourceName::Timer => ScriptThreadEventCategory::TimerEvent,
|
TaskSourceName::Timer => ScriptThreadEventCategory::TimerEvent,
|
||||||
TaskSourceName::Gamepad => ScriptThreadEventCategory::InputEvent,
|
TaskSourceName::Gamepad => ScriptThreadEventCategory::InputEvent,
|
||||||
TaskSourceName::IntersectionObserver => ScriptThreadEventCategory::ScriptEvent,
|
TaskSourceName::IntersectionObserver => ScriptThreadEventCategory::ScriptEvent,
|
||||||
|
TaskSourceName::Storage => ScriptThreadEventCategory::ScriptEvent,
|
||||||
TaskSourceName::WebGPU => ScriptThreadEventCategory::ScriptEvent,
|
TaskSourceName::WebGPU => ScriptThreadEventCategory::ScriptEvent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -710,7 +710,7 @@ DOMInterfaces = {
|
|||||||
'Navigator': {
|
'Navigator': {
|
||||||
'inRealms': ['GetVRDisplays'],
|
'inRealms': ['GetVRDisplays'],
|
||||||
'canGc': ['Languages', 'SendBeacon', 'UserActivation'],
|
'canGc': ['Languages', 'SendBeacon', 'UserActivation'],
|
||||||
'cx': ['Clipboard', 'WakeLock'],
|
'cx': ['Clipboard', 'Storage', 'WakeLock'],
|
||||||
},
|
},
|
||||||
|
|
||||||
'WakeLock': {
|
'WakeLock': {
|
||||||
@@ -829,6 +829,11 @@ DOMInterfaces = {
|
|||||||
'weakReferenceable': True,
|
'weakReferenceable': True,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'StorageManager': {
|
||||||
|
'inRealms': ['Persisted', 'Persist', 'Estimate'],
|
||||||
|
'canGc': ['Persisted', 'Persist', 'Estimate'],
|
||||||
|
},
|
||||||
|
|
||||||
'SubtleCrypto': {
|
'SubtleCrypto': {
|
||||||
'realm': ['Encrypt', 'Decrypt', 'Sign', 'Verify', 'GenerateKey', 'DeriveKey', 'DeriveBits', 'Digest', 'ImportKey', 'ExportKey', 'WrapKey', 'UnwrapKey', 'EncapsulateKey', 'EncapsulateBits', 'DecapsulateKey', 'DecapsulateBits', 'GetPublicKey'],
|
'realm': ['Encrypt', 'Decrypt', 'Sign', 'Verify', 'GenerateKey', 'DeriveKey', 'DeriveBits', 'Digest', 'ImportKey', 'ExportKey', 'WrapKey', 'UnwrapKey', 'EncapsulateKey', 'EncapsulateBits', 'DecapsulateKey', 'DecapsulateBits', 'GetPublicKey'],
|
||||||
'cx': ['Supports', 'Supports_'],
|
'cx': ['Supports', 'Supports_'],
|
||||||
@@ -1067,6 +1072,7 @@ DOMInterfaces = {
|
|||||||
|
|
||||||
'WorkerNavigator': {
|
'WorkerNavigator': {
|
||||||
'canGc': ['Languages'],
|
'canGc': ['Languages'],
|
||||||
|
'cx': ['Storage'],
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
29
components/script_bindings/webidls/StorageManager.webidl
Normal file
29
components/script_bindings/webidls/StorageManager.webidl
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
// https://storage.spec.whatwg.org/#api
|
||||||
|
|
||||||
|
[SecureContext]
|
||||||
|
interface mixin NavigatorStorage {
|
||||||
|
[SameObject, Pref="dom_storage_manager_api_enabled"] readonly attribute StorageManager storage;
|
||||||
|
};
|
||||||
|
Navigator includes NavigatorStorage;
|
||||||
|
WorkerNavigator includes NavigatorStorage;
|
||||||
|
|
||||||
|
[SecureContext, Exposed=(Window,Worker)]
|
||||||
|
interface StorageManager {
|
||||||
|
[NewObject]
|
||||||
|
Promise<boolean> persisted();
|
||||||
|
|
||||||
|
[Exposed=Window, NewObject]
|
||||||
|
Promise<boolean> persist();
|
||||||
|
|
||||||
|
[NewObject]
|
||||||
|
Promise<StorageEstimate> estimate();
|
||||||
|
};
|
||||||
|
|
||||||
|
dictionary StorageEstimate {
|
||||||
|
unsigned long long usage;
|
||||||
|
unsigned long long quota;
|
||||||
|
};
|
||||||
@@ -3,9 +3,12 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use servo_base::generic_channel::{self, GenericReceiver, GenericSender};
|
use servo_base::generic_channel::{
|
||||||
|
self, GenericCallback, GenericReceiver, GenericSender, SendResult,
|
||||||
|
};
|
||||||
use servo_base::id::WebViewId;
|
use servo_base::id::WebViewId;
|
||||||
use servo_url::ImmutableOrigin;
|
use servo_url::ImmutableOrigin;
|
||||||
|
|
||||||
@@ -67,6 +70,37 @@ impl ClientStorageThreadHandle {
|
|||||||
self.sender.send(message).unwrap();
|
self.sender.send(message).unwrap();
|
||||||
receiver
|
receiver
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn persisted(
|
||||||
|
&self,
|
||||||
|
origin: ImmutableOrigin,
|
||||||
|
sender: GenericCallback<Result<bool, String>>,
|
||||||
|
) -> SendResult {
|
||||||
|
self.sender
|
||||||
|
.send(ClientStorageThreadMessage::Persisted { origin, sender })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn persist(
|
||||||
|
&self,
|
||||||
|
origin: ImmutableOrigin,
|
||||||
|
permission_granted: bool,
|
||||||
|
sender: GenericCallback<Result<bool, String>>,
|
||||||
|
) -> SendResult {
|
||||||
|
self.sender.send(ClientStorageThreadMessage::Persist {
|
||||||
|
origin,
|
||||||
|
permission_granted,
|
||||||
|
sender,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn estimate(
|
||||||
|
&self,
|
||||||
|
origin: ImmutableOrigin,
|
||||||
|
sender: GenericCallback<Result<(u64, u64), String>>,
|
||||||
|
) -> SendResult {
|
||||||
|
self.sender
|
||||||
|
.send(ClientStorageThreadMessage::Estimate { origin, sender })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ClientStorageThreadHandle> for GenericSender<ClientStorageThreadMessage> {
|
impl From<ClientStorageThreadHandle> for GenericSender<ClientStorageThreadMessage> {
|
||||||
@@ -106,7 +140,7 @@ impl StorageType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <https://storage.spec.whatwg.org/#bucket-mode>
|
/// <https://storage.spec.whatwg.org/#bucket-mode>
|
||||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
/// It is initially "best-effort".
|
/// It is initially "best-effort".
|
||||||
#[default]
|
#[default]
|
||||||
@@ -123,6 +157,19 @@ impl Mode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromStr for Mode {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
/// <https://storage.spec.whatwg.org/#bucket-mode>
|
||||||
|
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||||
|
match value {
|
||||||
|
"best-effort" => Ok(Mode::BestEffort),
|
||||||
|
"persistent" => Ok(Mode::Persistent),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <https://storage.spec.whatwg.org/#storage-identifier>
|
/// <https://storage.spec.whatwg.org/#storage-identifier>
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub enum StorageIdentifier {
|
pub enum StorageIdentifier {
|
||||||
@@ -200,6 +247,18 @@ pub enum ClientStorageThreadMessage {
|
|||||||
name: String,
|
name: String,
|
||||||
sender: GenericSender<Result<(), String>>,
|
sender: GenericSender<Result<(), String>>,
|
||||||
},
|
},
|
||||||
/// Send a reply when done cleaning up thread resources and then shut it down
|
Persisted {
|
||||||
|
origin: ImmutableOrigin,
|
||||||
|
sender: GenericCallback<Result<bool, String>>,
|
||||||
|
},
|
||||||
|
Persist {
|
||||||
|
origin: ImmutableOrigin,
|
||||||
|
permission_granted: bool,
|
||||||
|
sender: GenericCallback<Result<bool, String>>,
|
||||||
|
},
|
||||||
|
Estimate {
|
||||||
|
origin: ImmutableOrigin,
|
||||||
|
sender: GenericCallback<Result<(u64, u64), String>>,
|
||||||
|
},
|
||||||
Exit(GenericSender<()>),
|
Exit(GenericSender<()>),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
|
|
||||||
use malloc_size_of::malloc_size_of_is_0;
|
use malloc_size_of::malloc_size_of_is_0;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use servo_base::generic_channel::{self, GenericSend, GenericSender, SendResult};
|
use servo_base::generic_channel::{self, GenericCallback, GenericSend, GenericSender, SendResult};
|
||||||
|
use servo_url::ImmutableOrigin;
|
||||||
|
|
||||||
use crate::client_storage::ClientStorageThreadMessage;
|
use crate::client_storage::ClientStorageThreadMessage;
|
||||||
use crate::indexeddb::IndexedDBThreadMsg;
|
use crate::indexeddb::IndexedDBThreadMsg;
|
||||||
@@ -34,6 +35,38 @@ impl StorageThreads {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn persisted(
|
||||||
|
&self,
|
||||||
|
origin: ImmutableOrigin,
|
||||||
|
sender: GenericCallback<Result<bool, String>>,
|
||||||
|
) -> SendResult {
|
||||||
|
self.client_storage_thread
|
||||||
|
.send(ClientStorageThreadMessage::Persisted { origin, sender })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn persist(
|
||||||
|
&self,
|
||||||
|
origin: ImmutableOrigin,
|
||||||
|
permission_granted: bool,
|
||||||
|
sender: GenericCallback<Result<bool, String>>,
|
||||||
|
) -> SendResult {
|
||||||
|
self.client_storage_thread
|
||||||
|
.send(ClientStorageThreadMessage::Persist {
|
||||||
|
origin,
|
||||||
|
permission_granted,
|
||||||
|
sender,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn estimate(
|
||||||
|
&self,
|
||||||
|
origin: ImmutableOrigin,
|
||||||
|
sender: GenericCallback<Result<(u64, u64), String>>,
|
||||||
|
) -> SendResult {
|
||||||
|
self.client_storage_thread
|
||||||
|
.send(ClientStorageThreadMessage::Estimate { origin, sender })
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Consider changing to `webstorage_sites`
|
// TODO: Consider changing to `webstorage_sites`
|
||||||
pub fn webstorage_origins(&self, storage_type: WebStorageType) -> Vec<OriginDescriptor> {
|
pub fn webstorage_origins(&self, storage_type: WebStorageType) -> Vec<OriginDescriptor> {
|
||||||
let (sender, receiver) = generic_channel::channel().unwrap();
|
let (sender, receiver) = generic_channel::channel().unwrap();
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::thread;
|
use std::str::FromStr;
|
||||||
|
use std::{fs, thread};
|
||||||
|
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use rusqlite::{Connection, Transaction};
|
use rusqlite::{Connection, Transaction};
|
||||||
@@ -16,6 +17,12 @@ use storage_traits::client_storage::{
|
|||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// <https://storage.spec.whatwg.org/#storage-quota>
|
||||||
|
/// The storage quota of a storage shelf is an implementation-defined conservative estimate of the
|
||||||
|
/// total amount of byttes it can hold. We use 10 GiB per shelf, matching Firefox's documented
|
||||||
|
/// limit (<https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria>).
|
||||||
|
const STORAGE_SHELF_QUOTA_BYTES: u64 = 10 * 1024 * 1024 * 1024;
|
||||||
|
|
||||||
trait RegistryEngine {
|
trait RegistryEngine {
|
||||||
type Error: Debug;
|
type Error: Debug;
|
||||||
fn create_database(
|
fn create_database(
|
||||||
@@ -36,6 +43,13 @@ trait RegistryEngine {
|
|||||||
origin: ImmutableOrigin,
|
origin: ImmutableOrigin,
|
||||||
sender: &GenericSender<ClientStorageThreadMessage>,
|
sender: &GenericSender<ClientStorageThreadMessage>,
|
||||||
) -> Result<StorageProxyMap, ClientStorageErrorr<Self::Error>>;
|
) -> Result<StorageProxyMap, ClientStorageErrorr<Self::Error>>;
|
||||||
|
fn persisted(&mut self, origin: ImmutableOrigin) -> Result<bool, String>;
|
||||||
|
fn persist(
|
||||||
|
&mut self,
|
||||||
|
origin: ImmutableOrigin,
|
||||||
|
permission_granted: bool,
|
||||||
|
) -> Result<bool, String>;
|
||||||
|
fn estimate(&mut self, origin: ImmutableOrigin) -> Result<(u64, u64), String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SqliteEngine {
|
struct SqliteEngine {
|
||||||
@@ -203,8 +217,8 @@ fn create_a_storage_bucket(
|
|||||||
storage_type: StorageType,
|
storage_type: StorageType,
|
||||||
tx: &Transaction,
|
tx: &Transaction,
|
||||||
) -> rusqlite::Result<i64> {
|
) -> rusqlite::Result<i64> {
|
||||||
// Step 1: Let bucket be null.
|
// Step 1. Let bucket be null.
|
||||||
// Step 2: If type is "local", then set bucket to a new local storage bucket.
|
// Step 2. If type is "local", then set bucket to a new local storage bucket.
|
||||||
let bucket_id: i64 = if let StorageType::Local = storage_type {
|
let bucket_id: i64 = if let StorageType::Local = storage_type {
|
||||||
tx.query_row(
|
tx.query_row(
|
||||||
"INSERT INTO buckets (mode, shelf_id) VALUES (?1, ?2)
|
"INSERT INTO buckets (mode, shelf_id) VALUES (?1, ?2)
|
||||||
@@ -214,9 +228,9 @@ fn create_a_storage_bucket(
|
|||||||
|row| row.get(0),
|
|row| row.get(0),
|
||||||
)?
|
)?
|
||||||
} else {
|
} else {
|
||||||
// Step 3: Otherwise:
|
// Step 3. Otherwise:
|
||||||
// Step 3.1: Assert: type is "session".
|
// Step 3.1. Assert: type is "session".
|
||||||
// Step 3.2: Set bucket to a new session storage bucket.
|
// Step 3.2. Set bucket to a new session storage bucket.
|
||||||
tx.query_row(
|
tx.query_row(
|
||||||
"INSERT INTO buckets (shelf_id) VALUES (?1)
|
"INSERT INTO buckets (shelf_id) VALUES (?1)
|
||||||
ON CONFLICT(shelf_id) DO UPDATE SET shelf_id = excluded.shelf_id
|
ON CONFLICT(shelf_id) DO UPDATE SET shelf_id = excluded.shelf_id
|
||||||
@@ -226,7 +240,7 @@ fn create_a_storage_bucket(
|
|||||||
)?
|
)?
|
||||||
};
|
};
|
||||||
|
|
||||||
// Step 4: For each endpoint of registered storage endpoints whose types contain type,
|
// Step 4. For each endpoint of registered storage endpoints whose types contain type,
|
||||||
// set bucket’s bottle map[endpoint’s identifier] to
|
// set bucket’s bottle map[endpoint’s identifier] to
|
||||||
// a new storage bottle whose quota is endpoint’s quota.
|
// a new storage bottle whose quota is endpoint’s quota.
|
||||||
|
|
||||||
@@ -249,7 +263,7 @@ fn create_a_storage_bucket(
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 5: Return bucket.
|
// Step 5. Return bucket.
|
||||||
Ok(bucket_id)
|
Ok(bucket_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,8 +273,10 @@ fn create_a_storage_shelf(
|
|||||||
origin: &ImmutableOrigin,
|
origin: &ImmutableOrigin,
|
||||||
storage_type: StorageType,
|
storage_type: StorageType,
|
||||||
tx: &Transaction,
|
tx: &Transaction,
|
||||||
) -> rusqlite::Result<i64> {
|
) -> rusqlite::Result<StorageShelf> {
|
||||||
// Step 1: Let shelf be a new storage shelf.
|
// To create a storage shelf, given a storage type type, run these steps:
|
||||||
|
// Step 1. Let shelf be a new storage shelf.
|
||||||
|
// Step 2. Set shelf’s bucket map["default"] to the result of running create a storage bucket with type.
|
||||||
let shelf_id: i64 = tx.query_row(
|
let shelf_id: i64 = tx.query_row(
|
||||||
"INSERT INTO shelves (shed_id, origin) VALUES (?1, ?2)
|
"INSERT INTO shelves (shed_id, origin) VALUES (?1, ?2)
|
||||||
ON CONFLICT(shed_id, origin) DO UPDATE SET origin = excluded.origin
|
ON CONFLICT(shed_id, origin) DO UPDATE SET origin = excluded.origin
|
||||||
@@ -269,9 +285,10 @@ fn create_a_storage_shelf(
|
|||||||
|row| row.get(0),
|
|row| row.get(0),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Step 2: Set shelf’s bucket map["default"] to the result of running create a storage bucket with type.
|
// Step 3. Return shelf.
|
||||||
// Note: returning `shelf’s bucket map["default"]`, which is the `bucket_id`.
|
Ok(StorageShelf {
|
||||||
create_a_storage_bucket(shelf_id, storage_type, tx)
|
default_bucket_id: create_a_storage_bucket(shelf_id, storage_type, tx)?,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <https://storage.spec.whatwg.org/#obtain-a-storage-shelf>
|
/// <https://storage.spec.whatwg.org/#obtain-a-storage-shelf>
|
||||||
@@ -280,18 +297,124 @@ fn obtain_a_storage_shelf(
|
|||||||
origin: &ImmutableOrigin,
|
origin: &ImmutableOrigin,
|
||||||
storage_type: StorageType,
|
storage_type: StorageType,
|
||||||
tx: &Transaction,
|
tx: &Transaction,
|
||||||
) -> rusqlite::Result<i64> {
|
) -> rusqlite::Result<StorageShelf> {
|
||||||
// Step 1: Let key be the result of running obtain a storage key with environment.
|
create_a_storage_shelf(shed, origin, storage_type, tx)
|
||||||
// Step 2: If key is failure, then return failure.
|
}
|
||||||
|
|
||||||
// Step 3: If shed[key] does not exist,
|
/// <https://storage.spec.whatwg.org/#storage-shelf>
|
||||||
// then set shed[key] to the result of running create a storage shelf with type.
|
///
|
||||||
// Note: method internally conditions on shed[key] not existing.
|
/// A storage shelf exists for each storage key within a storage shed. It holds a bucket map, which
|
||||||
let bucket_id = create_a_storage_shelf(shed, origin, storage_type, tx)?;
|
/// is a map of strings to storage buckets.
|
||||||
|
struct StorageShelf {
|
||||||
|
default_bucket_id: i64,
|
||||||
|
}
|
||||||
|
|
||||||
// Step 4: Return shed[key].
|
/// <https://storage.spec.whatwg.org/#obtain-a-local-storage-shelf>
|
||||||
// Note: returning `shed[key]["default"]`, which is `bucket_id`.
|
///
|
||||||
Ok(bucket_id)
|
/// To obtain a local storage shelf, given an environment settings object environment, return the
|
||||||
|
/// result of running obtain a storage shelf with the user agent’s storage shed, environment, and
|
||||||
|
/// "local".
|
||||||
|
fn obtain_a_local_storage_shelf(
|
||||||
|
origin: &ImmutableOrigin,
|
||||||
|
tx: &Transaction,
|
||||||
|
) -> Result<StorageShelf, String> {
|
||||||
|
if !origin.is_tuple() {
|
||||||
|
return Err("Storage is unavailable for opaque origins".to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
let shed =
|
||||||
|
ensure_storage_shed(&StorageType::Local, None, tx).map_err(|error| error.to_string())?;
|
||||||
|
obtain_a_storage_shelf(shed, origin, StorageType::Local, tx).map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <https://storage.spec.whatwg.org/#bucket-mode>
|
||||||
|
///
|
||||||
|
/// A local storage bucket has a mode, which is "best-effort" or "persistent". It is initially
|
||||||
|
/// "best-effort".
|
||||||
|
fn bucket_mode(bucket_id: i64, tx: &Transaction) -> rusqlite::Result<Mode> {
|
||||||
|
let mode: String = tx.query_row(
|
||||||
|
"SELECT mode FROM buckets WHERE id = ?1;",
|
||||||
|
[bucket_id],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
Ok(Mode::from_str(&mode).unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <https://storage.spec.whatwg.org/#dom-storagemanager-persist>
|
||||||
|
///
|
||||||
|
/// Set bucket’s mode to "persistent".
|
||||||
|
fn set_bucket_mode(bucket_id: i64, mode: Mode, tx: &Transaction) -> rusqlite::Result<()> {
|
||||||
|
tx.execute(
|
||||||
|
"UPDATE buckets SET mode = ?1, persisted = ?2 WHERE id = ?3;",
|
||||||
|
(mode.as_str(), matches!(mode, Mode::Persistent), bucket_id),
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <https://storage.spec.whatwg.org/#storage-usage>
|
||||||
|
///
|
||||||
|
/// The storage usage of a storage shelf is an implementation-defined rough estimate of the amount
|
||||||
|
/// of bytes used by it.
|
||||||
|
///
|
||||||
|
/// This cannot be an exact amount as user agents might, and are encouraged to, use deduplication,
|
||||||
|
/// compression, and other techniques that obscure exactly how much bytes a storage shelf uses.
|
||||||
|
fn storage_usage_for_bucket(bucket_id: i64, tx: &Transaction) -> Result<u64, String> {
|
||||||
|
let mut stmt = tx
|
||||||
|
.prepare(
|
||||||
|
"SELECT directories.path
|
||||||
|
FROM directories
|
||||||
|
JOIN databases ON directories.database_id = databases.id
|
||||||
|
JOIN bottles ON databases.bottle_id = bottles.id
|
||||||
|
WHERE bottles.bucket_id = ?1;",
|
||||||
|
)
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
|
||||||
|
let rows = stmt
|
||||||
|
.query_map([bucket_id], |row| row.get::<_, String>(0))
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
|
||||||
|
let mut usage = 0_u64;
|
||||||
|
for path in rows {
|
||||||
|
usage += directory_size(&PathBuf::from(path.map_err(|error| error.to_string())?))?;
|
||||||
|
}
|
||||||
|
Ok(usage)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <https://storage.spec.whatwg.org/#storage-quota>
|
||||||
|
///
|
||||||
|
/// The storage quota of a storage shelf is an implementation-defined conservative estimate of the
|
||||||
|
/// total amount of bytes it can hold. This amount should be less than the total storage space on
|
||||||
|
/// the device. It must not be a function of the available storage space on the device.
|
||||||
|
///
|
||||||
|
/// User agents are strongly encouraged to consider navigation frequency, recency of visits,
|
||||||
|
/// bookmarking, and permission for "persistent-storage" when determining quotas.
|
||||||
|
///
|
||||||
|
/// Directly or indirectly revealing available storage space can lead to fingerprinting and leaking
|
||||||
|
/// information outside the scope of the origin involved.
|
||||||
|
fn storage_quota_for_bucket(_bucket_id: i64, _tx: &Transaction) -> Result<u64, String> {
|
||||||
|
Ok(STORAGE_SHELF_QUOTA_BYTES)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <https://storage.spec.whatwg.org/#storage-usage>
|
||||||
|
///
|
||||||
|
/// The storage usage of a storage shelf is an implementation-defined rough estimate of the amount
|
||||||
|
/// of bytes used by it.
|
||||||
|
fn directory_size(path: &PathBuf) -> Result<u64, String> {
|
||||||
|
let metadata = fs::metadata(path).map_err(|error| error.to_string())?;
|
||||||
|
if metadata.is_file() {
|
||||||
|
return Ok(metadata.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !metadata.is_dir() {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut size = 0_u64;
|
||||||
|
for entry in fs::read_dir(path).map_err(|error| error.to_string())? {
|
||||||
|
let entry = entry.map_err(|error| error.to_string())?;
|
||||||
|
size += directory_size(&entry.path())?;
|
||||||
|
}
|
||||||
|
Ok(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegistryEngine for SqliteEngine {
|
impl RegistryEngine for SqliteEngine {
|
||||||
@@ -393,18 +516,17 @@ impl RegistryEngine for SqliteEngine {
|
|||||||
) -> Result<StorageProxyMap, ClientStorageErrorr<Self::Error>> {
|
) -> Result<StorageProxyMap, ClientStorageErrorr<Self::Error>> {
|
||||||
let tx = self.connection.transaction()?;
|
let tx = self.connection.transaction()?;
|
||||||
|
|
||||||
// Step 1: Let shed be null.
|
// Step 1. Let shed be null.
|
||||||
let shed_id: i64 = match storage_type {
|
let shed_id: i64 = match storage_type {
|
||||||
StorageType::Local => {
|
StorageType::Local => {
|
||||||
// Step 2: If type is "local", then set shed to the user agent’s storage shed.
|
// Step 2. If type is "local", then set shed to the user agent’s storage shed.
|
||||||
ensure_storage_shed(&storage_type, None, &tx)?
|
ensure_storage_shed(&storage_type, None, &tx)?
|
||||||
},
|
},
|
||||||
StorageType::Session => {
|
StorageType::Session => {
|
||||||
// Step 3: Otherwise:
|
// Step 3. Otherwise:
|
||||||
// Step 3.1: Assert: type is "session".
|
// Step 3.1. Assert: type is "session".
|
||||||
// Step 3.2: Set shed to environment’s global object’s associated Document’s
|
// Step 3.2. Set shed to environment’s global object’s associated Document’s node
|
||||||
// node navigable’s traversable navigable’s storage shed.
|
// navigable’s traversable navigable’s storage shed.
|
||||||
// Note: using the browsing context of the webview as the traversable navigable.
|
|
||||||
ensure_storage_shed(
|
ensure_storage_shed(
|
||||||
&storage_type,
|
&storage_type,
|
||||||
Some(Into::<BrowsingContextId>::into(webview).to_string()),
|
Some(Into::<BrowsingContextId>::into(webview).to_string()),
|
||||||
@@ -413,13 +535,13 @@ impl RegistryEngine for SqliteEngine {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Step 4: Let shelf be the result of running obtain a storage shelf,
|
// Step 4. Let shelf be the result of running obtain a storage shelf, with shed,
|
||||||
// with shed, environment, and type.
|
// environment, and type.
|
||||||
// Step 5: If shelf is failure, then return failure.
|
// Step 5. If shelf is failure, then return failure.
|
||||||
let bucket_id = obtain_a_storage_shelf(shed_id, &origin, storage_type, &tx)?;
|
let shelf = obtain_a_storage_shelf(shed_id, &origin, storage_type, &tx)?;
|
||||||
|
|
||||||
// Step 6: Let bucket be shelf’s bucket map["default"].
|
// Step 6. Let bucket be shelf’s bucket map["default"].
|
||||||
// Done above with `bucket_id`.
|
let bucket_id = shelf.default_bucket_id;
|
||||||
|
|
||||||
let bottle_id: i64 = tx.query_row(
|
let bottle_id: i64 = tx.query_row(
|
||||||
"SELECT id FROM bottles WHERE bucket_id = ?1 AND identifier = ?2;",
|
"SELECT id FROM bottles WHERE bucket_id = ?1 AND identifier = ?2;",
|
||||||
@@ -429,19 +551,94 @@ impl RegistryEngine for SqliteEngine {
|
|||||||
|
|
||||||
tx.commit()?;
|
tx.commit()?;
|
||||||
|
|
||||||
// Step 7: Let bottle be bucket’s bottle map[identifier].
|
// Step 7. Let bottle be bucket’s bottle map[identifier].
|
||||||
// Note: done with `bucket_id`.
|
|
||||||
|
|
||||||
// Step 8: Let proxyMap be a new storage proxy map whose backing map is bottle’s map.
|
// Step 8. Let proxyMap be a new storage proxy map whose backing map is bottle’s map.
|
||||||
// Step 9: Append proxyMap to bottle’s proxy map reference set.
|
// Step 9. Append proxyMap to bottle’s proxy map reference set.
|
||||||
// Note: not doing the reference set part for now, not sure what it is useful for.
|
// Step 10. Return proxyMap.
|
||||||
|
|
||||||
// Step 10: Return proxyMap.
|
|
||||||
Ok(StorageProxyMap {
|
Ok(StorageProxyMap {
|
||||||
bottle_id,
|
bottle_id,
|
||||||
handle: ClientStorageThreadHandle::new(sender.clone()),
|
handle: ClientStorageThreadHandle::new(sender.clone()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn persisted(&mut self, origin: ImmutableOrigin) -> Result<bool, String> {
|
||||||
|
let tx = self
|
||||||
|
.connection
|
||||||
|
.transaction()
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
|
||||||
|
// <https://storage.spec.whatwg.org/#dom-storagemanager-persisted>
|
||||||
|
// Let shelf be the result of running obtain a local storage shelf with this’s relevant
|
||||||
|
// settings object.
|
||||||
|
let shelf = obtain_a_local_storage_shelf(&origin, &tx)?;
|
||||||
|
|
||||||
|
// Let persisted be true if shelf’s bucket map["default"]'s mode is "persistent";
|
||||||
|
// otherwise false.
|
||||||
|
// It will be false when there’s an internal error.
|
||||||
|
let persisted = bucket_mode(shelf.default_bucket_id, &tx)
|
||||||
|
.is_ok_and(|mode| mode == Mode::Persistent) &&
|
||||||
|
tx.commit().is_ok();
|
||||||
|
|
||||||
|
Ok(persisted)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn persist(
|
||||||
|
&mut self,
|
||||||
|
origin: ImmutableOrigin,
|
||||||
|
permission_granted: bool,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
let tx = self
|
||||||
|
.connection
|
||||||
|
.transaction()
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
|
||||||
|
// <https://storage.spec.whatwg.org/#dom-storagemanager-persist>
|
||||||
|
// Let shelf be the result of running obtain a local storage shelf with this’s relevant
|
||||||
|
// settings object.
|
||||||
|
let shelf = obtain_a_local_storage_shelf(&origin, &tx)?;
|
||||||
|
|
||||||
|
// Let bucket be shelf’s bucket map["default"].
|
||||||
|
let bucket_id = shelf.default_bucket_id;
|
||||||
|
|
||||||
|
// Let persisted be true if bucket’s mode is "persistent"; otherwise false.
|
||||||
|
// It will be false when there’s an internal error.
|
||||||
|
let mut persisted = bucket_mode(bucket_id, &tx).is_ok_and(|mode| mode == Mode::Persistent);
|
||||||
|
|
||||||
|
// If persisted is false and permission is "granted", then:
|
||||||
|
// Set bucket’s mode to "persistent".
|
||||||
|
// If there was no internal error, then set persisted to true.
|
||||||
|
if !persisted && permission_granted {
|
||||||
|
persisted = set_bucket_mode(bucket_id, Mode::Persistent, &tx).is_ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
if tx.commit().is_err() {
|
||||||
|
persisted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(persisted)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimate(&mut self, origin: ImmutableOrigin) -> Result<(u64, u64), String> {
|
||||||
|
let tx = self
|
||||||
|
.connection
|
||||||
|
.transaction()
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
|
||||||
|
// <https://storage.spec.whatwg.org/#dom-storagemanager-estimate>
|
||||||
|
// Let shelf be the result of running obtain a local storage shelf with this’s relevant
|
||||||
|
// settings object.
|
||||||
|
let shelf = obtain_a_local_storage_shelf(&origin, &tx)?;
|
||||||
|
|
||||||
|
// Let usage be storage usage for shelf.
|
||||||
|
let usage = storage_usage_for_bucket(shelf.default_bucket_id, &tx)?;
|
||||||
|
// Let quota be storage quota for shelf.
|
||||||
|
let quota = storage_quota_for_bucket(shelf.default_bucket_id, &tx)?;
|
||||||
|
|
||||||
|
tx.commit().map_err(|error| error.to_string())?;
|
||||||
|
|
||||||
|
Ok((usage, quota))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ClientStorageThreadFactory {
|
pub trait ClientStorageThreadFactory {
|
||||||
@@ -533,6 +730,19 @@ where
|
|||||||
let result = self.engine.delete_database(bottle_id, name);
|
let result = self.engine.delete_database(bottle_id, name);
|
||||||
let _ = sender.send(result.map_err(|e| format!("{:?}", e)));
|
let _ = sender.send(result.map_err(|e| format!("{:?}", e)));
|
||||||
},
|
},
|
||||||
|
ClientStorageThreadMessage::Persisted { origin, sender } => {
|
||||||
|
let _ = sender.send(self.engine.persisted(origin));
|
||||||
|
},
|
||||||
|
ClientStorageThreadMessage::Persist {
|
||||||
|
origin,
|
||||||
|
permission_granted,
|
||||||
|
sender,
|
||||||
|
} => {
|
||||||
|
let _ = sender.send(self.engine.persist(origin, permission_granted));
|
||||||
|
},
|
||||||
|
ClientStorageThreadMessage::Estimate { origin, sender } => {
|
||||||
|
let _ = sender.send(self.engine.estimate(origin));
|
||||||
|
},
|
||||||
ClientStorageThreadMessage::Exit(sender) => {
|
ClientStorageThreadMessage::Exit(sender) => {
|
||||||
let _ = sender.send(());
|
let _ = sender.send(());
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use servo_base::generic_channel;
|
use servo_base::generic_channel::{self, GenericCallback};
|
||||||
use servo_base::id::{BrowsingContextId, PipelineNamespace, PipelineNamespaceId, WebViewId};
|
use servo_base::id::{BrowsingContextId, PipelineNamespace, PipelineNamespaceId, WebViewId};
|
||||||
use servo_url::ServoUrl;
|
use servo_url::ServoUrl;
|
||||||
use storage::ClientStorageThreadFactory;
|
use storage::ClientStorageThreadFactory;
|
||||||
@@ -229,3 +229,77 @@ fn test_repeated_session_obtain_reuses_same_logical_rows() {
|
|||||||
assert_eq!(bucket_count, 1);
|
assert_eq!(bucket_count, 1);
|
||||||
assert_eq!(bottle_count, 1);
|
assert_eq!(bottle_count, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_local_persistence_and_estimate() {
|
||||||
|
install_test_namespace();
|
||||||
|
let tmp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let handle: ClientStorageThreadHandle =
|
||||||
|
ClientStorageThreadFactory::new(Some(tmp_dir.path().to_path_buf()));
|
||||||
|
|
||||||
|
let origin = ServoUrl::parse("https://example.com").unwrap().origin();
|
||||||
|
let webview = WebViewId::new(servo_base::id::TEST_PAINTER_ID);
|
||||||
|
let storage_proxy_map = obtain_bottle_map(
|
||||||
|
&handle,
|
||||||
|
StorageType::Local,
|
||||||
|
webview,
|
||||||
|
StorageIdentifier::IndexedDB,
|
||||||
|
origin.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let (cb, rx) = GenericCallback::new_blocking().unwrap();
|
||||||
|
handle.persisted(origin.clone(), cb).unwrap();
|
||||||
|
assert!(!rx.recv().unwrap().unwrap());
|
||||||
|
|
||||||
|
let (cb, rx) = GenericCallback::new_blocking().unwrap();
|
||||||
|
handle.persist(origin.clone(), false, cb).unwrap();
|
||||||
|
assert!(!rx.recv().unwrap().unwrap());
|
||||||
|
|
||||||
|
let (cb, rx) = GenericCallback::new_blocking().unwrap();
|
||||||
|
handle.persisted(origin.clone(), cb).unwrap();
|
||||||
|
assert!(!rx.recv().unwrap().unwrap());
|
||||||
|
|
||||||
|
let (cb, rx) = GenericCallback::new_blocking().unwrap();
|
||||||
|
handle.persist(origin.clone(), true, cb).unwrap();
|
||||||
|
assert!(rx.recv().unwrap().unwrap());
|
||||||
|
|
||||||
|
let (cb, rx) = GenericCallback::new_blocking().unwrap();
|
||||||
|
handle.persisted(origin.clone(), cb).unwrap();
|
||||||
|
assert!(rx.recv().unwrap().unwrap());
|
||||||
|
|
||||||
|
let path = handle
|
||||||
|
.create_database(storage_proxy_map.bottle_id, "estimate".to_string())
|
||||||
|
.recv()
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
let payload = vec![0x5a; 8192];
|
||||||
|
std::fs::write(path.join("payload.bin"), &payload).unwrap();
|
||||||
|
|
||||||
|
let (cb, rx) = GenericCallback::new_blocking().unwrap();
|
||||||
|
handle.estimate(origin, cb).unwrap();
|
||||||
|
let (usage, quota) = rx.recv().unwrap().unwrap();
|
||||||
|
assert!(usage >= payload.len() as u64);
|
||||||
|
assert!(quota > usage);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_storage_manager_operations_fail_for_opaque_origins() {
|
||||||
|
install_test_namespace();
|
||||||
|
let tmp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let handle: ClientStorageThreadHandle =
|
||||||
|
ClientStorageThreadFactory::new(Some(tmp_dir.path().to_path_buf()));
|
||||||
|
|
||||||
|
let origin = ServoUrl::parse("data:text/plain,hello").unwrap().origin();
|
||||||
|
|
||||||
|
let (cb, rx) = GenericCallback::new_blocking().unwrap();
|
||||||
|
handle.persisted(origin.clone(), cb).unwrap();
|
||||||
|
assert!(rx.recv().unwrap().is_err());
|
||||||
|
|
||||||
|
let (cb, rx) = GenericCallback::new_blocking().unwrap();
|
||||||
|
handle.persist(origin.clone(), true, cb).unwrap();
|
||||||
|
assert!(rx.recv().unwrap().is_err());
|
||||||
|
|
||||||
|
let (cb, rx) = GenericCallback::new_blocking().unwrap();
|
||||||
|
handle.estimate(origin, cb).unwrap();
|
||||||
|
assert!(rx.recv().unwrap().is_err());
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ pub(crate) static EXPERIMENTAL_PREFS: &[&str] = &[
|
|||||||
"dom_notification_enabled",
|
"dom_notification_enabled",
|
||||||
"dom_offscreen_canvas_enabled",
|
"dom_offscreen_canvas_enabled",
|
||||||
"dom_permissions_enabled",
|
"dom_permissions_enabled",
|
||||||
|
"dom_storage_manager_api_enabled",
|
||||||
"dom_webgl2_enabled",
|
"dom_webgl2_enabled",
|
||||||
"dom_webgpu_enabled",
|
"dom_webgpu_enabled",
|
||||||
"layout_columns_enabled",
|
"layout_columns_enabled",
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ WEBIDL_STANDARDS = [
|
|||||||
b"//fetch.spec.whatwg.org",
|
b"//fetch.spec.whatwg.org",
|
||||||
b"//html.spec.whatwg.org",
|
b"//html.spec.whatwg.org",
|
||||||
b"//streams.spec.whatwg.org",
|
b"//streams.spec.whatwg.org",
|
||||||
|
b"//storage.spec.whatwg.org",
|
||||||
b"//url.spec.whatwg.org",
|
b"//url.spec.whatwg.org",
|
||||||
b"//urlpattern.spec.whatwg.org",
|
b"//urlpattern.spec.whatwg.org",
|
||||||
b"//xhr.spec.whatwg.org",
|
b"//xhr.spec.whatwg.org",
|
||||||
|
|||||||
2
tests/wpt/mozilla/meta/MANIFEST.json
vendored
2
tests/wpt/mozilla/meta/MANIFEST.json
vendored
@@ -14323,7 +14323,7 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"interfaces.https.html": [
|
"interfaces.https.html": [
|
||||||
"090fde1d7537506349e9b47f6e7f75b12243525c",
|
"566deaac42e0f430fadcbd28bf4dba751c7ba39a",
|
||||||
[
|
[
|
||||||
null,
|
null,
|
||||||
{}
|
{}
|
||||||
|
|||||||
@@ -335,6 +335,7 @@ test_interfaces([
|
|||||||
"StaticRange",
|
"StaticRange",
|
||||||
"StereoPannerNode",
|
"StereoPannerNode",
|
||||||
"Storage",
|
"Storage",
|
||||||
|
"StorageManager",
|
||||||
"StorageEvent",
|
"StorageEvent",
|
||||||
"StyleSheet",
|
"StyleSheet",
|
||||||
"StyleSheetList",
|
"StyleSheetList",
|
||||||
|
|||||||
Reference in New Issue
Block a user