From a90997d792cb98c58246dd37ee40bbbd9850c88d Mon Sep 17 00:00:00 2001 From: ldm0 Date: Fri, 11 Sep 2026 04:34:50 +0800 Subject: [PATCH] fix(blob): ignore URL fragments during resource lookup Resolve Blob URL bytes and text using the serialized URL without its fragment. Keep query strings, appended paths, and percent-encoded hash characters in the lookup key, and preserve exact-match revocation. Cover fragment lookup, non-fragment suffix rejection, and reference release after revoking the original URL. --- moli-file-api/src/blob_store.rs | 43 ++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/moli-file-api/src/blob_store.rs b/moli-file-api/src/blob_store.rs index 709ed55056..cef89a0c8e 100644 --- a/moli-file-api/src/blob_store.rs +++ b/moli-file-api/src/blob_store.rs @@ -205,8 +205,9 @@ where true } - /// Return object URL bytes and MIME type. + /// Return object URL bytes and MIME type, excluding its fragment. pub fn object_url_bytes_and_type(&self, url: &str) -> Option<(Vec, String)> { + let url = url.split_once('#').map_or(url, |(url, _)| url); let blob_id = self .object_urls .lock() @@ -381,6 +382,46 @@ mod tests { assert!(store.blob_bytes(blob_id).is_none()); } + #[test] + fn object_url_lookup_ignores_fragment_but_revocation_requires_exact_url() { + let store = BlobStore::::default(); + let blob = store.create_blob(Some(1), None, b"hello".to_vec(), "text/plain".to_owned()); + let url = store + .create_object_url(Some(1), blob, "https://example.test") + .expect("object URL"); + store.release_blob_wrapper_ref(blob); + + for suffix in ["", "#", "#fragment", "#fragment#tail"] { + assert_eq!( + store.object_url_bytes_and_type(&format!("{url}{suffix}")), + Some((b"hello".to_vec(), "text/plain".to_owned())), + "lookup should ignore the fragment: {suffix}" + ); + } + for suffix in ["?query", "?query#fragment", "/path", "%23fragment"] { + assert!( + store + .object_url_bytes_and_type(&format!("{url}{suffix}")) + .is_none(), + "lookup must preserve the non-fragment suffix: {suffix}" + ); + } + assert!(!store.revoke_object_url(&format!("{url}#fragment"))); + assert!(!store.revoke_object_url(&format!("{url}#"))); + assert_eq!( + store.object_url_body_and_type(&format!("{url}#fragment")), + Some(("hello".to_owned(), "text/plain".to_owned())) + ); + + assert!(store.revoke_object_url(&url)); + assert!( + store + .object_url_bytes_and_type(&format!("{url}#fragment")) + .is_none() + ); + assert!(store.blob_bytes(blob).is_none()); + } + #[test] fn devtools_uuid_is_stable_distinct_and_resolves_bytes() { let store = BlobStore::::default();