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
|
||||
pub dom_sanitizer_enabled: 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
|
||||
pub dom_serviceworker_enabled: bool,
|
||||
pub dom_serviceworker_timeout_seconds: i64,
|
||||
@@ -392,6 +394,7 @@ impl Preferences {
|
||||
dom_resize_observer_enabled: true,
|
||||
dom_sanitizer_enabled: false,
|
||||
dom_script_asynch: true,
|
||||
dom_storage_manager_api_enabled: false,
|
||||
dom_serviceworker_enabled: false,
|
||||
dom_serviceworker_timeout_seconds: 60,
|
||||
dom_servo_helpers_enabled: false,
|
||||
|
||||
@@ -350,6 +350,7 @@ pub(crate) mod servoparser;
|
||||
pub(crate) mod shadowroot;
|
||||
pub(crate) mod staticrange;
|
||||
pub(crate) mod storage;
|
||||
pub(crate) mod storagemanager;
|
||||
pub(crate) mod stream;
|
||||
pub(crate) use self::stream::*;
|
||||
pub(crate) mod svg;
|
||||
|
||||
@@ -56,6 +56,7 @@ use crate::dom::permissions::Permissions;
|
||||
use crate::dom::pluginarray::PluginArray;
|
||||
use crate::dom::serviceworkercontainer::ServiceWorkerContainer;
|
||||
use crate::dom::servointernals::ServoInternals;
|
||||
use crate::dom::storagemanager::StorageManager;
|
||||
use crate::dom::types::UserActivation;
|
||||
use crate::dom::wakelock::WakeLock;
|
||||
#[cfg(feature = "webgpu")]
|
||||
@@ -127,6 +128,7 @@ pub(crate) struct Navigator {
|
||||
permissions: MutNullableDom<Permissions>,
|
||||
mediasession: MutNullableDom<MediaSession>,
|
||||
clipboard: MutNullableDom<Clipboard>,
|
||||
storage: MutNullableDom<StorageManager>,
|
||||
#[cfg(feature = "webgpu")]
|
||||
gpu: MutNullableDom<GPU>,
|
||||
/// <https://www.w3.org/TR/gamepad/#dfn-hasgamepadgesture>
|
||||
@@ -155,6 +157,7 @@ impl Navigator {
|
||||
permissions: Default::default(),
|
||||
mediasession: Default::default(),
|
||||
clipboard: Default::default(),
|
||||
storage: Default::default(),
|
||||
#[cfg(feature = "webgpu")]
|
||||
gpu: Default::default(),
|
||||
#[cfg(feature = "gamepad")]
|
||||
@@ -478,6 +481,12 @@ impl NavigatorMethods<crate::DomTypeHolder> for Navigator {
|
||||
.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>
|
||||
fn SendBeacon(&self, url: USVString, data: Option<BodyInit>, can_gc: CanGc) -> Fallible<bool> {
|
||||
let global = self.global();
|
||||
|
||||
@@ -276,14 +276,9 @@ impl PermissionAlgorithm for Permissions {
|
||||
match status.State() {
|
||||
// Step 3.
|
||||
PermissionState::Prompt => {
|
||||
// https://w3c.github.io/permissions/#request-permission-to-use (Step 3 - 4)
|
||||
let permission_name = status.get_query();
|
||||
let globalscope = GlobalScope::current().expect("No current global object");
|
||||
let state = prompt_user_from_embedder(permission_name, &globalscope);
|
||||
globalscope
|
||||
.permission_state_invocation_results()
|
||||
.borrow_mut()
|
||||
.insert(permission_name, state);
|
||||
request_permission_to_use(permission_name, &globalscope);
|
||||
},
|
||||
|
||||
// Step 2.
|
||||
@@ -354,6 +349,24 @@ pub(crate) fn descriptor_permission_state(
|
||||
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 {
|
||||
let Some(webview_id) = global_scope.webview_id() else {
|
||||
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::navigatorinfo;
|
||||
use crate::dom::permissions::Permissions;
|
||||
use crate::dom::storagemanager::StorageManager;
|
||||
#[cfg(feature = "webgpu")]
|
||||
use crate::dom::webgpu::gpu::GPU;
|
||||
use crate::dom::workerglobalscope::WorkerGlobalScope;
|
||||
@@ -24,6 +25,7 @@ use crate::script_runtime::{CanGc, JSContext};
|
||||
pub(crate) struct WorkerNavigator {
|
||||
reflector_: Reflector,
|
||||
permissions: MutNullableDom<Permissions>,
|
||||
storage: MutNullableDom<StorageManager>,
|
||||
#[cfg(feature = "webgpu")]
|
||||
gpu: MutNullableDom<GPU>,
|
||||
}
|
||||
@@ -33,6 +35,7 @@ impl WorkerNavigator {
|
||||
WorkerNavigator {
|
||||
reflector_: Reflector::new(),
|
||||
permissions: Default::default(),
|
||||
storage: Default::default(),
|
||||
#[cfg(feature = "webgpu")]
|
||||
gpu: Default::default(),
|
||||
}
|
||||
@@ -115,6 +118,12 @@ impl WorkerNavigatorMethods<crate::DomTypeHolder> for WorkerNavigator {
|
||||
.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
|
||||
#[cfg(feature = "webgpu")]
|
||||
fn Gpu(&self) -> DomRoot<GPU> {
|
||||
|
||||
@@ -162,6 +162,7 @@ impl TaskManager {
|
||||
intersection_observer_task_source,
|
||||
IntersectionObserver
|
||||
);
|
||||
task_source_functions!(self, storage_task_source, Storage);
|
||||
#[cfg(feature = "webgpu")]
|
||||
task_source_functions!(self, webgpu_task_source, WebGPU);
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ pub(crate) enum TaskSourceName {
|
||||
Geolocation,
|
||||
/// <https://w3c.github.io/IntersectionObserver/#intersectionobserver-task-source>
|
||||
IntersectionObserver,
|
||||
/// <https://storage.spec.whatwg.org/#storage-task-source>
|
||||
Storage,
|
||||
/// <https://www.w3.org/TR/webgpu/#-webgpu-task-source>
|
||||
WebGPU,
|
||||
}
|
||||
@@ -85,6 +87,7 @@ impl From<TaskSourceName> for ScriptThreadEventCategory {
|
||||
TaskSourceName::Timer => ScriptThreadEventCategory::TimerEvent,
|
||||
TaskSourceName::Gamepad => ScriptThreadEventCategory::InputEvent,
|
||||
TaskSourceName::IntersectionObserver => ScriptThreadEventCategory::ScriptEvent,
|
||||
TaskSourceName::Storage => ScriptThreadEventCategory::ScriptEvent,
|
||||
TaskSourceName::WebGPU => ScriptThreadEventCategory::ScriptEvent,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -710,7 +710,7 @@ DOMInterfaces = {
|
||||
'Navigator': {
|
||||
'inRealms': ['GetVRDisplays'],
|
||||
'canGc': ['Languages', 'SendBeacon', 'UserActivation'],
|
||||
'cx': ['Clipboard', 'WakeLock'],
|
||||
'cx': ['Clipboard', 'Storage', 'WakeLock'],
|
||||
},
|
||||
|
||||
'WakeLock': {
|
||||
@@ -829,6 +829,11 @@ DOMInterfaces = {
|
||||
'weakReferenceable': True,
|
||||
},
|
||||
|
||||
'StorageManager': {
|
||||
'inRealms': ['Persisted', 'Persist', 'Estimate'],
|
||||
'canGc': ['Persisted', 'Persist', 'Estimate'],
|
||||
},
|
||||
|
||||
'SubtleCrypto': {
|
||||
'realm': ['Encrypt', 'Decrypt', 'Sign', 'Verify', 'GenerateKey', 'DeriveKey', 'DeriveBits', 'Digest', 'ImportKey', 'ExportKey', 'WrapKey', 'UnwrapKey', 'EncapsulateKey', 'EncapsulateBits', 'DecapsulateKey', 'DecapsulateBits', 'GetPublicKey'],
|
||||
'cx': ['Supports', 'Supports_'],
|
||||
@@ -1067,6 +1072,7 @@ DOMInterfaces = {
|
||||
|
||||
'WorkerNavigator': {
|
||||
'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/. */
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
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_url::ImmutableOrigin;
|
||||
|
||||
@@ -67,6 +70,37 @@ impl ClientStorageThreadHandle {
|
||||
self.sender.send(message).unwrap();
|
||||
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> {
|
||||
@@ -106,7 +140,7 @@ impl StorageType {
|
||||
}
|
||||
|
||||
/// <https://storage.spec.whatwg.org/#bucket-mode>
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum Mode {
|
||||
/// It is initially "best-effort".
|
||||
#[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>
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub enum StorageIdentifier {
|
||||
@@ -200,6 +247,18 @@ pub enum ClientStorageThreadMessage {
|
||||
name: 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<()>),
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
use malloc_size_of::malloc_size_of_is_0;
|
||||
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::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`
|
||||
pub fn webstorage_origins(&self, storage_type: WebStorageType) -> Vec<OriginDescriptor> {
|
||||
let (sender, receiver) = generic_channel::channel().unwrap();
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
use std::fmt::Debug;
|
||||
use std::path::PathBuf;
|
||||
use std::thread;
|
||||
use std::str::FromStr;
|
||||
use std::{fs, thread};
|
||||
|
||||
use log::warn;
|
||||
use rusqlite::{Connection, Transaction};
|
||||
@@ -16,6 +17,12 @@ use storage_traits::client_storage::{
|
||||
};
|
||||
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 {
|
||||
type Error: Debug;
|
||||
fn create_database(
|
||||
@@ -36,6 +43,13 @@ trait RegistryEngine {
|
||||
origin: ImmutableOrigin,
|
||||
sender: &GenericSender<ClientStorageThreadMessage>,
|
||||
) -> 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 {
|
||||
@@ -203,8 +217,8 @@ fn create_a_storage_bucket(
|
||||
storage_type: StorageType,
|
||||
tx: &Transaction,
|
||||
) -> rusqlite::Result<i64> {
|
||||
// Step 1: Let bucket be null.
|
||||
// Step 2: If type is "local", then set bucket to a new local storage bucket.
|
||||
// Step 1. Let bucket be null.
|
||||
// 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 {
|
||||
tx.query_row(
|
||||
"INSERT INTO buckets (mode, shelf_id) VALUES (?1, ?2)
|
||||
@@ -214,9 +228,9 @@ fn create_a_storage_bucket(
|
||||
|row| row.get(0),
|
||||
)?
|
||||
} else {
|
||||
// Step 3: Otherwise:
|
||||
// Step 3.1: Assert: type is "session".
|
||||
// Step 3.2: Set bucket to a new session storage bucket.
|
||||
// Step 3. Otherwise:
|
||||
// Step 3.1. Assert: type is "session".
|
||||
// Step 3.2. Set bucket to a new session storage bucket.
|
||||
tx.query_row(
|
||||
"INSERT INTO buckets (shelf_id) VALUES (?1)
|
||||
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
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -259,8 +273,10 @@ fn create_a_storage_shelf(
|
||||
origin: &ImmutableOrigin,
|
||||
storage_type: StorageType,
|
||||
tx: &Transaction,
|
||||
) -> rusqlite::Result<i64> {
|
||||
// Step 1: Let shelf be a new storage shelf.
|
||||
) -> rusqlite::Result<StorageShelf> {
|
||||
// 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(
|
||||
"INSERT INTO shelves (shed_id, origin) VALUES (?1, ?2)
|
||||
ON CONFLICT(shed_id, origin) DO UPDATE SET origin = excluded.origin
|
||||
@@ -269,9 +285,10 @@ fn create_a_storage_shelf(
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
// Step 2: Set shelf’s bucket map["default"] to the result of running create a storage bucket with type.
|
||||
// Note: returning `shelf’s bucket map["default"]`, which is the `bucket_id`.
|
||||
create_a_storage_bucket(shelf_id, storage_type, tx)
|
||||
// Step 3. Return shelf.
|
||||
Ok(StorageShelf {
|
||||
default_bucket_id: create_a_storage_bucket(shelf_id, storage_type, tx)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// <https://storage.spec.whatwg.org/#obtain-a-storage-shelf>
|
||||
@@ -280,18 +297,124 @@ fn obtain_a_storage_shelf(
|
||||
origin: &ImmutableOrigin,
|
||||
storage_type: StorageType,
|
||||
tx: &Transaction,
|
||||
) -> rusqlite::Result<i64> {
|
||||
// Step 1: Let key be the result of running obtain a storage key with environment.
|
||||
// Step 2: If key is failure, then return failure.
|
||||
) -> rusqlite::Result<StorageShelf> {
|
||||
create_a_storage_shelf(shed, origin, storage_type, tx)
|
||||
}
|
||||
|
||||
// Step 3: If shed[key] does not exist,
|
||||
// then set shed[key] to the result of running create a storage shelf with type.
|
||||
// Note: method internally conditions on shed[key] not existing.
|
||||
let bucket_id = create_a_storage_shelf(shed, origin, storage_type, tx)?;
|
||||
/// <https://storage.spec.whatwg.org/#storage-shelf>
|
||||
///
|
||||
/// A storage shelf exists for each storage key within a storage shed. It holds a bucket map, which
|
||||
/// is a map of strings to storage buckets.
|
||||
struct StorageShelf {
|
||||
default_bucket_id: i64,
|
||||
}
|
||||
|
||||
// Step 4: Return shed[key].
|
||||
// Note: returning `shed[key]["default"]`, which is `bucket_id`.
|
||||
Ok(bucket_id)
|
||||
/// <https://storage.spec.whatwg.org/#obtain-a-local-storage-shelf>
|
||||
///
|
||||
/// 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 {
|
||||
@@ -393,18 +516,17 @@ impl RegistryEngine for SqliteEngine {
|
||||
) -> Result<StorageProxyMap, ClientStorageErrorr<Self::Error>> {
|
||||
let tx = self.connection.transaction()?;
|
||||
|
||||
// Step 1: Let shed be null.
|
||||
// Step 1. Let shed be null.
|
||||
let shed_id: i64 = match storage_type {
|
||||
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)?
|
||||
},
|
||||
StorageType::Session => {
|
||||
// Step 3: Otherwise:
|
||||
// Step 3.1: Assert: type is "session".
|
||||
// Step 3.2: Set shed to environment’s global object’s associated Document’s
|
||||
// node navigable’s traversable navigable’s storage shed.
|
||||
// Note: using the browsing context of the webview as the traversable navigable.
|
||||
// Step 3. Otherwise:
|
||||
// Step 3.1. Assert: type is "session".
|
||||
// Step 3.2. Set shed to environment’s global object’s associated Document’s node
|
||||
// navigable’s traversable navigable’s storage shed.
|
||||
ensure_storage_shed(
|
||||
&storage_type,
|
||||
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,
|
||||
// with shed, environment, and type.
|
||||
// Step 5: If shelf is failure, then return failure.
|
||||
let bucket_id = obtain_a_storage_shelf(shed_id, &origin, storage_type, &tx)?;
|
||||
// Step 4. Let shelf be the result of running obtain a storage shelf, with shed,
|
||||
// environment, and type.
|
||||
// Step 5. If shelf is failure, then return failure.
|
||||
let shelf = obtain_a_storage_shelf(shed_id, &origin, storage_type, &tx)?;
|
||||
|
||||
// Step 6: Let bucket be shelf’s bucket map["default"].
|
||||
// Done above with `bucket_id`.
|
||||
// Step 6. Let bucket be shelf’s bucket map["default"].
|
||||
let bucket_id = shelf.default_bucket_id;
|
||||
|
||||
let bottle_id: i64 = tx.query_row(
|
||||
"SELECT id FROM bottles WHERE bucket_id = ?1 AND identifier = ?2;",
|
||||
@@ -429,19 +551,94 @@ impl RegistryEngine for SqliteEngine {
|
||||
|
||||
tx.commit()?;
|
||||
|
||||
// Step 7: Let bottle be bucket’s bottle map[identifier].
|
||||
// Note: done with `bucket_id`.
|
||||
// Step 7. Let bottle be bucket’s bottle map[identifier].
|
||||
|
||||
// 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.
|
||||
// Note: not doing the reference set part for now, not sure what it is useful for.
|
||||
|
||||
// Step 10: Return proxyMap.
|
||||
// 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 10. Return proxyMap.
|
||||
Ok(StorageProxyMap {
|
||||
bottle_id,
|
||||
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 {
|
||||
@@ -533,6 +730,19 @@ where
|
||||
let result = self.engine.delete_database(bottle_id, name);
|
||||
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) => {
|
||||
let _ = sender.send(());
|
||||
break;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
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_url::ServoUrl;
|
||||
use storage::ClientStorageThreadFactory;
|
||||
@@ -229,3 +229,77 @@ fn test_repeated_session_obtain_reuses_same_logical_rows() {
|
||||
assert_eq!(bucket_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_offscreen_canvas_enabled",
|
||||
"dom_permissions_enabled",
|
||||
"dom_storage_manager_api_enabled",
|
||||
"dom_webgl2_enabled",
|
||||
"dom_webgpu_enabled",
|
||||
"layout_columns_enabled",
|
||||
|
||||
@@ -126,6 +126,7 @@ WEBIDL_STANDARDS = [
|
||||
b"//fetch.spec.whatwg.org",
|
||||
b"//html.spec.whatwg.org",
|
||||
b"//streams.spec.whatwg.org",
|
||||
b"//storage.spec.whatwg.org",
|
||||
b"//url.spec.whatwg.org",
|
||||
b"//urlpattern.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": [
|
||||
"090fde1d7537506349e9b47f6e7f75b12243525c",
|
||||
"566deaac42e0f430fadcbd28bf4dba751c7ba39a",
|
||||
[
|
||||
null,
|
||||
{}
|
||||
|
||||
@@ -335,6 +335,7 @@ test_interfaces([
|
||||
"StaticRange",
|
||||
"StereoPannerNode",
|
||||
"Storage",
|
||||
"StorageManager",
|
||||
"StorageEvent",
|
||||
"StyleSheet",
|
||||
"StyleSheetList",
|
||||
|
||||
Reference in New Issue
Block a user