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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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/. */
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<()>),
}

View File

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

View File

@@ -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 buckets bottle map[endpoints identifier] to
// 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)
}
@@ -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 shelfs 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 shelfs bucket map["default"] to the result of running create a storage bucket with type.
// Note: returning `shelfs 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 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 {
@@ -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 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)?
},
StorageType::Session => {
// Step 3: Otherwise:
// Step 3.1: Assert: type is "session".
// Step 3.2: Set shed to environments global objects associated Documents
// node navigables traversable navigables 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 environments global objects associated Documents node
// navigables traversable navigables 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 shelfs bucket map["default"].
// Done above with `bucket_id`.
// Step 6. Let bucket be shelfs 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 buckets bottle map[identifier].
// Note: done with `bucket_id`.
// Step 7. Let bottle be buckets bottle map[identifier].
// 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.
// 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 bottles map.
// Step 9. Append proxyMap to bottles 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 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 {
@@ -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;

View File

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

View File

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

View File

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

View File

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

View File

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