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:
Taym Haddadi
2026-04-21 15:41:58 +02:00
committed by GitHub
parent d1e3e8080c
commit 5ac8bf4db3
18 changed files with 788 additions and 56 deletions

View File

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

View File

@@ -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;

View File

@@ -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();

View File

@@ -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");

View 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 thiss relevant global object.
let global = self.global();
// Step 3. Let shelf be the result of running obtain a local storage shelf with thiss 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 shelfs bucket map["default"]'s mode is "persistent";
// otherwise false.
// It will be false when theres 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 thiss relevant global object.
let global = self.global();
// Step 3. Let shelf be the result of running obtain a local storage shelf with thiss 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 shelfs bucket map["default"].
// Step 5.3. Let persisted be true if buckets mode is "persistent"; otherwise false.
// It will be false when theres an internal error.
// Step 5.4. If persisted is false and permission is "granted", then:
// Step 5.4.1. Set buckets 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 thiss relevant global object.
let global = self.global();
// Step 3. Let shelf be the result of running obtain a local storage shelf with thiss 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
}
}

View File

@@ -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> {

View File

@@ -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);
} }

View File

@@ -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,
} }
} }

View File

@@ -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'],
}, },
} }

View 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;
};

View File

@@ -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<()>),
} }

View File

@@ -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();

View File

@@ -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 buckets bottle map[endpoints identifier] to // set buckets bottle map[endpoints identifier] to
// a new storage bottle whose quota is endpoints quota. // a new storage bottle whose quota is endpoints 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 shelfs 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 shelfs bucket map["default"] to the result of running create a storage bucket with type. // Step 3. Return shelf.
// Note: returning `shelfs 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 agents 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 buckets 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 agents storage shed. // Step 2. If type is "local", then set shed to the user agents 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 environments global objects associated Documents // Step 3.2. Set shed to environments global objects associated Documents node
// node navigables traversable navigables storage shed. // navigables traversable navigables 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 shelfs bucket map["default"]. // Step 6. Let bucket be shelfs 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 buckets bottle map[identifier]. // Step 7. Let bottle be buckets bottle map[identifier].
// Note: done with `bucket_id`.
// Step 8: Let proxyMap be a new storage proxy map whose backing map is bottles map. // Step 8. Let proxyMap be a new storage proxy map whose backing map is bottles map.
// Step 9: Append proxyMap to bottles proxy map reference set. // Step 9. Append proxyMap to bottles 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 thiss relevant
// settings object.
let shelf = obtain_a_local_storage_shelf(&origin, &tx)?;
// Let persisted be true if shelfs bucket map["default"]'s mode is "persistent";
// otherwise false.
// It will be false when theres 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 thiss relevant
// settings object.
let shelf = obtain_a_local_storage_shelf(&origin, &tx)?;
// Let bucket be shelfs bucket map["default"].
let bucket_id = shelf.default_bucket_id;
// Let persisted be true if buckets mode is "persistent"; otherwise false.
// It will be false when theres 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 buckets 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 thiss 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;

View File

@@ -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());
}

View File

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

View File

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

View File

@@ -14323,7 +14323,7 @@
] ]
], ],
"interfaces.https.html": [ "interfaces.https.html": [
"090fde1d7537506349e9b47f6e7f75b12243525c", "566deaac42e0f430fadcbd28bf4dba751c7ba39a",
[ [
null, null,
{} {}

View File

@@ -335,6 +335,7 @@ test_interfaces([
"StaticRange", "StaticRange",
"StereoPannerNode", "StereoPannerNode",
"Storage", "Storage",
"StorageManager",
"StorageEvent", "StorageEvent",
"StyleSheet", "StyleSheet",
"StyleSheetList", "StyleSheetList",