fix(storage): exclude Web Storage from quota-managed usage

Align StorageManager.estimate and CDP Storage.getUsageAndQuota with Chromium's
quota clients. Report IndexedDB, Cache Storage, and OPFS by backend, including
named and implicit-default bucket data, without touching local/sessionStorage.
Keep Web Storage's independent per-area quota unchanged.

Cover Web Storage exclusion, an unopened or closed SQLite localStorage backend,
default and named OPFS data, bucket attribution, and command-session isolation.
This commit is contained in:
ldm0
2026-11-01 04:03:38 +08:00
committed by Donough Liu
parent 1e09077598
commit 43feec65be
9 changed files with 207 additions and 123 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ pub use moli_renderer_v8::{
};
pub use moli_storage_service::{
DEFAULT_ORIGIN_STORAGE_QUOTA_BYTES, SharedStorageBucketStore, StorageBucketIdentity,
complete_storage_bucket_deletion, new_shared_json_storage_bucket_store,
StorageBucketLocator, complete_storage_bucket_deletion, new_shared_json_storage_bucket_store,
new_shared_json_storage_bucket_store_with_cache_root,
new_shared_json_storage_bucket_store_with_cache_root_and_indexed_db_manager,
new_shared_storage_bucket_store, new_shared_storage_bucket_store_with_indexed_db_manager,
+37 -34
View File
@@ -136,9 +136,9 @@ pub(crate) struct BrowserContextPageStorageHandles {
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub(crate) struct BrowserContextOriginStorageUsage {
pub(crate) local_storage_usage: u64,
pub(crate) indexed_db_usage: u64,
pub(crate) storage_buckets_usage: u64,
pub(crate) cache_storage_usage: u64,
pub(crate) file_system_usage: u64,
pub(crate) total_usage: u64,
}
@@ -367,10 +367,6 @@ impl std::fmt::Debug for BrowserContext {
}
}
fn usize_to_u64_saturating(value: usize) -> u64 {
u64::try_from(value).unwrap_or(u64::MAX)
}
impl BrowserContext {
pub(crate) fn active_page_target(&self) -> &PageTargetHost {
self.page_targets
@@ -771,52 +767,59 @@ impl BrowserContext {
&self,
serialized_origin: &str,
) -> Result<BrowserContextOriginStorageUsage, String> {
let local_storage_usage = {
let store = self.storage_partition.web_storage_store();
usize_to_u64_saturating(
store
.try_usage_bytes_for_origin_areas(serialized_origin)
.map_err(|error| error.to_string())?,
)
};
let indexed_db_usage = moli_core::storage::indexed_db_origins_with_prefix_usage_bytes(
// Web Storage has its own per-area limit and is not a QuotaManager client.
let mut indexed_db_usage = moli_core::storage::indexed_db_origins_with_prefix_usage_bytes(
self.storage_partition.indexed_db_manager(),
&moli_storage_key::storage_key_prefix_for_origin(serialized_origin),
)?;
let storage_buckets_usage = self.storage_bucket_usage_for_origin(serialized_origin)?;
Ok(BrowserContextOriginStorageUsage {
local_storage_usage,
indexed_db_usage,
storage_buckets_usage,
total_usage: local_storage_usage
.saturating_add(indexed_db_usage)
.saturating_add(storage_buckets_usage),
})
}
fn storage_bucket_usage_for_origin(&self, serialized_origin: &str) -> Result<u64, String> {
let (bucket_identities, cache_usage, storage_service) = {
let (bucket_identities, default_keys, cache_storage_usage, storage_service) = {
let store = self.storage_partition.storage_bucket_store().lock();
(
store.bucket_identities_for_origin_areas(serialized_origin),
store.keys_for_origin_areas(serialized_origin),
store.cache_usage_for_origin_areas(serialized_origin),
store.storage_service(),
)
};
let mut usage = cache_usage;
let mut default_keys = default_keys
.into_iter()
.map(|(key, _)| key)
.collect::<BTreeSet<_>>();
// OPFS can use the implicit default bucket before any CacheStorage or
// named-bucket metadata exists. Do not make its usage depend on that registry.
let url = url::Url::parse(serialized_origin).map_err(|error| error.to_string())?;
default_keys.insert(
moli_storage_key::MoliStorageKey::first_party_from_url(&url, None)
.serialized_storage_key(),
);
let mut file_system_usage = 0u64;
for key in default_keys {
let locator = moli_core::storage::StorageBucketLocator::default_bucket(key);
file_system_usage = file_system_usage.saturating_add(
storage_service
.opfs_usage(&locator)
.map_err(|error| format!("FailedToReadStorageBucketOpfsUsage: {error}"))?,
);
}
for identity in bucket_identities {
let indexed_db_usage = moli_core::storage::indexed_db_origin_usage_bytes(
let bucket_indexed_db_usage = moli_core::storage::indexed_db_origin_usage_bytes(
self.storage_partition.indexed_db_manager(),
&identity.indexed_db_storage_key(),
)?;
let opfs_usage = storage_service
.opfs_usage(&identity.locator())
.map_err(|error| format!("FailedToReadStorageBucketOpfsUsage: {error}"))?;
usage = usage
.saturating_add(indexed_db_usage)
.saturating_add(opfs_usage);
indexed_db_usage = indexed_db_usage.saturating_add(bucket_indexed_db_usage);
file_system_usage = file_system_usage.saturating_add(opfs_usage);
}
Ok(usage)
Ok(BrowserContextOriginStorageUsage {
indexed_db_usage,
cache_storage_usage,
file_system_usage,
total_usage: indexed_db_usage
.saturating_add(cache_storage_usage)
.saturating_add(file_system_usage),
})
}
fn complete_storage_bucket_deletions(
@@ -450,7 +450,7 @@ fn browser_context_cookie_snapshot_and_delete_use_storage_partition_cookie_handl
}
#[test]
fn browser_context_storage_usage_reads_storage_partition_owner() {
fn browser_context_quota_usage_excludes_web_storage() {
let browser_context = BrowserContext::new("BID-storage-usage".to_owned());
let origin = Url::parse("https://usage.example/app")
.unwrap()
@@ -468,10 +468,10 @@ fn browser_context_storage_usage_reads_storage_partition_owner() {
.storage_usage_for_origin(&origin)
.expect("storage usage should be readable");
assert_eq!(usage.local_storage_usage, "owner".len() as u64);
assert_eq!(usage.indexed_db_usage, 0);
assert_eq!(usage.storage_buckets_usage, 0);
assert_eq!(usage.total_usage, usage.local_storage_usage);
assert_eq!(usage.cache_storage_usage, 0);
assert_eq!(usage.file_system_usage, 0);
assert_eq!(usage.total_usage, 0);
}
#[test]
+8 -4
View File
@@ -422,16 +422,20 @@ fn get_usage_and_quota_command_output_plan(
"overrideActive": override_active,
"usageBreakdown": [
{
"storageType": "local_storage",
"usage": usage.local_storage_usage as f64,
"storageType": "file_systems",
"usage": usage.file_system_usage as f64,
},
{
"storageType": "indexeddb",
"usage": usage.indexed_db_usage as f64,
},
{
"storageType": "storage_buckets",
"usage": usage.storage_buckets_usage as f64,
"storageType": "cache_storage",
"usage": usage.cache_storage_usage as f64,
},
{
"storageType": "service_workers",
"usage": 0,
}
]
}))
+94 -29
View File
@@ -2510,7 +2510,7 @@ async fn storage_clear_data_for_partitioned_storage_key_clears_exact_partition_o
}
#[tokio::test(flavor = "multi_thread")]
async fn storage_get_usage_and_quota_reports_local_storage_usage_for_origin() {
async fn storage_get_usage_and_quota_excludes_web_storage_for_origin() {
let mut ctx = TestContext::new();
{
let fixture_value = Some(BrowserContext::new_with_page_for_test(
@@ -2559,22 +2559,29 @@ async fn storage_get_usage_and_quota_reports_local_storage_usage_for_origin() {
.await;
let result = take_result_by_id(&mut ctx, 12_528);
assert_eq!(json_number_as_u64(&result["usage"]), 5);
assert_eq!(json_number_as_u64(&result["usage"]), 0);
assert_eq!(
json_number_as_u64(&result["quota"]),
moli_core::storage::DEFAULT_ORIGIN_STORAGE_QUOTA_BYTES
);
assert_eq!(result["overrideActive"], json!(false));
assert_eq!(usage_breakdown_value(&result, "local_storage"), 5);
assert_eq!(usage_breakdown_value(&result, "indexeddb"), 0);
assert_eq!(usage_breakdown_value(&result, "storage_buckets"), 0);
assert!(
assert_eq!(usage_breakdown_value(&result, "cache_storage"), 0);
assert_eq!(usage_breakdown_value(&result, "file_systems"), 0);
assert_eq!(usage_breakdown_value(&result, "service_workers"), 0);
assert_eq!(
result["usageBreakdown"]
.as_array()
.unwrap()
.iter()
.all(|entry| entry["storageType"].as_str() != Some("session_storage")),
"CDP StorageType has no session_storage token"
.map(|entry| entry["storageType"].as_str().unwrap())
.collect::<Vec<_>>(),
[
"file_systems",
"indexeddb",
"cache_storage",
"service_workers"
]
);
}
@@ -2649,7 +2656,8 @@ async fn storage_get_usage_and_quota_reports_indexed_db_usage_for_origin() {
indexed_db_usage > 0,
"IndexedDB metadata and record bytes should be reported"
);
assert_eq!(usage_breakdown_value(&result, "local_storage"), 0);
assert_eq!(usage_breakdown_value(&result, "cache_storage"), 0);
assert_eq!(usage_breakdown_value(&result, "file_systems"), 0);
assert_eq!(json_number_as_u64(&result["usage"]), indexed_db_usage);
}
@@ -2764,21 +2772,28 @@ async fn storage_get_usage_and_quota_reports_storage_bucket_usage_for_origin() {
};
let opfs_usage = {
let storage_service = storage_bucket_store.lock().storage_service();
let locator = bucket_a_identity.locator();
let bucket_key = moli_storage_service::StorageService::opfs_bucket_key(&locator)
.expect("bucket OPFS key should derive");
let root = storage_service
.ensure_opfs_root(&locator)
.expect("bucket OPFS root should open");
let file = storage_service
.with_opfs(|opfs| opfs.get_file(&bucket_key, &root, "usage.txt", true))
.expect("bucket OPFS file should open");
storage_service
.with_opfs(|opfs| opfs.write_file(&bucket_key, &file, b"opfs usage", None))
.expect("bucket OPFS file should write");
storage_service
.opfs_usage(&locator)
.expect("bucket OPFS usage should load")
[
bucket_a_identity.locator(),
moli_storage_service::StorageBucketLocator::default_bucket(&storage_key),
]
.iter()
.map(|locator| {
let bucket_key = moli_storage_service::StorageService::opfs_bucket_key(locator)
.expect("bucket OPFS key should derive");
let root = storage_service
.ensure_opfs_root(locator)
.expect("bucket OPFS root should open");
let file = storage_service
.with_opfs(|opfs| opfs.get_file(&bucket_key, &root, "usage.txt", true))
.expect("bucket OPFS file should open");
storage_service
.with_opfs(|opfs| opfs.write_file(&bucket_key, &file, b"opfs usage", None))
.expect("bucket OPFS file should write");
storage_service
.opfs_usage(locator)
.expect("bucket OPFS usage should load")
})
.sum::<u64>()
};
{
@@ -2801,7 +2816,6 @@ async fn storage_get_usage_and_quota_reports_storage_bucket_usage_for_origin() {
.await;
let result = take_result_by_id(&mut ctx, 12_533);
let bucket_usage = usage_breakdown_value(&result, "storage_buckets");
let (bucket_a_usage, bucket_b_usage) = ctx.with_fixture(|conn| {
let bc = conn.browser_context.as_ref().unwrap();
let bucket_a_usage = moli_core::storage::indexed_db_origin_usage_bytes(
@@ -2817,15 +2831,59 @@ async fn storage_get_usage_and_quota_reports_storage_bucket_usage_for_origin() {
(bucket_a_usage, bucket_b_usage)
});
assert_eq!(
bucket_usage,
json_number_as_u64(&result["usage"]),
bucket_a_usage
.saturating_add(bucket_b_usage)
.saturating_add(cache_usage)
.saturating_add(opfs_usage)
);
assert_eq!(usage_breakdown_value(&result, "local_storage"), 0);
assert_eq!(
usage_breakdown_value(&result, "indexeddb"),
bucket_a_usage.saturating_add(bucket_b_usage)
);
assert_eq!(usage_breakdown_value(&result, "cache_storage"), cache_usage);
assert_eq!(usage_breakdown_value(&result, "file_systems"), opfs_usage);
}
#[tokio::test(flavor = "multi_thread")]
async fn storage_get_usage_and_quota_reports_default_opfs_without_bucket_registry_entries() {
let mut ctx = TestContext::new();
let origin = "https://opfs-only-usage.test";
let key = first_party_storage_key_for_origin(origin);
let expected = ctx.with_fixture(|conn| {
conn.browser_context = Some(BrowserContext::new("BID-usage-opfs-only".into()));
let store = conn
.browser_context
.as_ref()
.unwrap()
.storage_bucket_store_for_test()
.lock();
assert!(store.keys_for_origin_areas(origin).is_empty());
let service = store.storage_service();
drop(store);
let locator = moli_core::storage::StorageBucketLocator::default_bucket(key);
let bucket_key = moli_storage_service::StorageService::opfs_bucket_key(&locator).unwrap();
let root = service.ensure_opfs_root(&locator).unwrap();
let file = service
.with_opfs(|opfs| opfs.get_file(&bucket_key, &root, "only-opfs.txt", true))
.unwrap();
service
.with_opfs(|opfs| opfs.write_file(&bucket_key, &file, b"quota-managed bytes", None))
.unwrap();
service.opfs_usage(&locator).unwrap()
});
assert!(expected > 0);
ctx.process_async(json!({
"id": 12_534,
"method": "Storage.getUsageAndQuota",
"params": {"origin": origin}
}))
.await;
let result = take_result_by_id(&mut ctx, 12_534);
assert_eq!(usage_breakdown_value(&result, "file_systems"), expected);
assert_eq!(usage_breakdown_value(&result, "cache_storage"), 0);
assert_eq!(usage_breakdown_value(&result, "indexeddb"), 0);
assert_eq!(json_number_as_u64(&result["usage"]), bucket_usage);
assert_eq!(json_number_as_u64(&result["usage"]), expected);
}
#[tokio::test(flavor = "multi_thread")]
@@ -2851,6 +2909,13 @@ async fn storage_get_usage_and_quota_targets_command_session_browser_context() {
let store = inactive.web_storage_store_for_test();
assert!(store.set_item(&storage_key, "local", "bbbb"));
}
seed_indexed_db_usage(inactive.indexed_db_manager_for_test(), &storage_key);
let expected_usage = moli_core::storage::indexed_db_origin_usage_bytes(
inactive.indexed_db_manager_for_test(),
&storage_key,
)
.expect("inactive context IndexedDB usage");
assert!(expected_usage > 0);
ctx.with_fixture(|conn| conn.install_browser_context_fixture_for_test(active));
ctx.with_fixture(|conn| conn.push_inactive_browser_context_fixture_for_test(inactive));
@@ -2870,8 +2935,8 @@ async fn storage_get_usage_and_quota_targets_command_session_browser_context() {
let result = response
.get("result")
.expect("response should include result");
assert_eq!(json_number_as_u64(&result["usage"]), 4);
assert_eq!(usage_breakdown_value(result, "local_storage"), 4);
assert_eq!(json_number_as_u64(&result["usage"]), expected_usage);
assert_eq!(usage_breakdown_value(result, "indexeddb"), expected_usage);
}
#[tokio::test(flavor = "multi_thread")]
@@ -483,9 +483,6 @@ enum StorageBucketCachedResponseMaterialization<'scope> {
struct StorageManagerOwnerContext {
storage_key: String,
area_key: Option<String>,
host_ptr: Option<*mut crate::native_bridge::JsContextHost>,
popup_id: Option<u64>,
}
#[derive(webidl::WebIdlArgs)]
@@ -706,18 +703,7 @@ pub(in crate::context_bootstrap) fn navigator_storage_estimate_callback<'s>(
else {
return;
};
let usage = match storage_manager_usage_bytes(scope, &context) {
Ok(usage) => usage,
Err(error) => {
let error = crate::context_bootstrap::new_dom_exception_value(
scope,
&error.to_string(),
"UnknownError",
);
let _ = resolver.reject(scope, error);
return;
}
};
let usage = storage_manager_usage_bytes(scope, &context);
let estimate = build_storage_estimate_object_with_quota(
scope,
usage.total,
@@ -830,9 +816,6 @@ fn storage_manager_owner_context<'s>(
let storage_context = host.storage_context_for_lightweight_popup(popup_id)?;
return Some(StorageManagerOwnerContext {
storage_key: storage_context.storage_key().serialized_storage_key(),
area_key: Some(storage_context.web_storage_area_key().to_owned()),
host_ptr: Some(host_ptr),
popup_id: Some(popup_id),
});
} else {
host.top_document_storage_context()
@@ -840,19 +823,11 @@ fn storage_manager_owner_context<'s>(
};
return Some(StorageManagerOwnerContext {
storage_key: context.storage_key().serialized_storage_key(),
area_key: Some(context.web_storage_area_key().to_owned()),
host_ptr: Some(host_ptr),
popup_id: None,
});
}
let storage_key = crate::worker::worker_storage_key(scope)?;
let storage_key = storage_key.serialized_storage_key();
Some(StorageManagerOwnerContext {
storage_key,
area_key: None,
host_ptr: None,
popup_id: None,
})
Some(StorageManagerOwnerContext { storage_key })
}
fn storage_manager_child_handle<'s>(
@@ -875,23 +850,7 @@ fn storage_manager_popup_id<'s>(
fn storage_manager_usage_bytes(
scope: &mut v8::PinScope<'_, '_>,
context: &StorageManagerOwnerContext,
) -> Result<StorageUsageSnapshot, moli_web_storage::WebStorageMutationError> {
let web_storage_usage =
if let Some((host_ptr, area_key)) = context.host_ptr.zip(context.area_key.as_deref()) {
let host = unsafe { &*host_ptr };
let local = host.web_storage_store().try_usage_bytes(area_key)?;
let session = if let Some(popup_id) = context.popup_id {
host.lightweight_popup_session_storage_store(popup_id)
.map(|store| store.try_usage_bytes(area_key))
.transpose()?
.unwrap_or(0)
} else {
host.session_storage_store().try_usage_bytes(area_key)?
};
local.saturating_add(session) as u64
} else {
0
};
) -> StorageUsageSnapshot {
let locator = moli_storage_service::StorageBucketLocator::default_bucket(&context.storage_key);
let bucket_usage = storage_bucket_quota_owner_for_locator(scope, &locator)
.and_then(|owner| owner.usage_snapshot().ok());
@@ -912,16 +871,15 @@ fn storage_manager_usage_bytes(
0,
)
});
Ok(StorageUsageSnapshot {
total: web_storage_usage
.saturating_add(indexed_db_usage)
StorageUsageSnapshot {
total: indexed_db_usage
.saturating_add(cache_storage_usage)
.saturating_add(opfs_usage),
quota,
indexed_db: indexed_db_usage,
cache_storage: cache_storage_usage,
opfs: opfs_usage,
})
}
}
pub(in crate::context_bootstrap) fn storage_bucket_manager_open_callback<'s>(
@@ -140,6 +140,29 @@ async fn retained_popup_storage_managers_use_bound_popup_owner() {
"https://popup-storage-owner.test/page.html",
&loader,
);
// Seed quota-managed bytes for the popup's origin independently of the
// retained JS receiver. Reading through the opener's owner would report 0.
{
let host = vm.context_host_weak_for_test().upgrade().unwrap();
let store = host.borrow().storage_bucket_store();
let service = store.lock().storage_service();
let key = moli_storage_key::MoliStorageKey::first_party_from_url(
&url::Url::parse("https://popup-storage-owner-child.test").unwrap(),
None,
)
.serialized_storage_key();
let locator = moli_storage_service::StorageBucketLocator::default_bucket(key);
let bucket_key = moli_storage_service::StorageService::opfs_bucket_key(&locator).unwrap();
let root = service.ensure_opfs_root(&locator).unwrap();
let file = service
.with_opfs(|opfs| opfs.get_file(&bucket_key, &root, "popup-usage.txt", true))
.unwrap();
service
.with_opfs(|opfs| {
opfs.write_file(&bucket_key, &file, b"popup quota-managed data", None)
})
.unwrap();
}
vm.exec(
r#"
@@ -647,7 +670,7 @@ fn window_open_lightweight_popup_clones_and_isolates_session_storage() {
);
}
#[tokio::test]
async fn storage_manager_estimate_uses_lightweight_popup_session_storage_owner() {
async fn storage_manager_estimate_excludes_lightweight_popup_session_storage() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
let mut vm = new_page_task_executor_test_vm_with_loader("https://example.com/", &loader);
@@ -698,7 +721,7 @@ async fn storage_manager_estimate_uses_lightweight_popup_session_storage_owner()
assert_eq!(
vm.eval("__popupStorageEstimateMessages[0]")
.expect("popup StorageManager estimate message should evaluate"),
r#"{"usage":16,"indexedDB":null,"topValue":"AAAAA","popupValue":"BBBBBBBBBBB"}"#
r#"{"usage":0,"indexedDB":null,"topValue":"AAAAA","popupValue":"BBBBBBBBBBB"}"#
);
}
#[test]
@@ -2740,7 +2740,7 @@ fn navigator_storage_estimate_usage_tracks_current_origin_storage() {
assert_eq!(
result,
"0||7||true|number|true|true|true|true|true|true|indexedDB,caches,fileSystem|1073741824"
"0||0||true|number|true|true|true|true|true|true|indexedDB,caches,fileSystem|1073741824"
);
let other_page_url = format!("https://storage-estimate-other-{nonce}.test/");
@@ -1,5 +1,36 @@
use super::*;
#[test]
fn storage_estimate_does_not_open_local_storage_or_depend_on_its_connection() {
for closed in [false, true] {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("unused/localstorage.sqlite");
let runtime = moli_sqlite::Runtime::new().unwrap();
let (local, owner) = moli_web_storage::new_lazy_sqlite_web_storage_store(
path.clone(),
moli_sqlite::OpenMode::Create,
runtime.handle(),
)
.unwrap();
let handles =
crate::RendererWebStorageHandles::new(local, crate::new_shared_web_storage_store());
let mut vm =
new_storage_page_task_executor_test_vm("https://quota-without-webstorage.test/");
vm.set_web_storage_handles(&handles);
if closed {
owner.close().unwrap();
}
vm.eval("globalThis.estimateResult = 'pending'; navigator.storage.estimate().then(value => {estimateResult = JSON.stringify([value.usage, typeof value.quota]);}, error => {estimateResult = error.name;});").unwrap();
assert_eq!(
vm.eval_after_selected_page_tasks("estimateResult").unwrap(),
"[0,\"number\"]"
);
assert!(!path.parent().unwrap().exists());
drop(vm);
owner.close().unwrap();
}
}
#[test]
fn sqlite_storage_wrappers_share_utf16_data_without_sharing_session_storage() {
let directory = tempfile::tempdir().unwrap();