From c0d6ca4fdcf604e5df402b684dd1aa8cc52d92d2 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 6 Oct 2026 15:19:37 +0800 Subject: [PATCH] feat(file-api): capture blob URL creator access keys Snapshot Blob or MediaSource data together with the creator access key under the store lock. Preserve the data-only API and retain parsed entries independently of revocation. --- moli-file-api/src/blob_store.rs | 102 ++++++++++++++++++++++---------- 1 file changed, 71 insertions(+), 31 deletions(-) diff --git a/moli-file-api/src/blob_store.rs b/moli-file-api/src/blob_store.rs index 61b582bc86..695a832eb5 100644 --- a/moli-file-api/src/blob_store.rs +++ b/moli-file-api/src/blob_store.rs @@ -13,15 +13,6 @@ use uuid::Builder as UuidBuilder; /// Runtime id for one Blob backing-store entry. pub type BlobId = u64; -/// A parsed Blob URL retains immutable data and the URL creator's access key. -/// Consumers check the key against the fetching environment separately. -#[derive(Clone, Debug)] -pub struct ObjectUrlEntry { - pub bytes: Arc<[u8]>, - pub mime_type: String, - pub access_key: Option, -} - /// The object associated with an object URL. MediaSource is supplied by the /// embedding, so the store can retain its identity without treating it as bytes. #[derive(Clone, Debug)] @@ -46,6 +37,15 @@ pub enum ObjectUrlData { MediaSource(MediaSource), } +/// A parsed blob URL retains both its object and the creator's access key. +/// The embedding checks this key against the fetching environment, even when +/// revocation has already removed the URL from the store. +#[derive(Clone, Debug)] +pub struct ObjectUrlEntry { + pub data: ObjectUrlData, + pub access_key: Option, +} + #[derive(Clone, Debug)] struct BlobState { owner_id: Option, @@ -369,23 +369,6 @@ where self.object_url_blob_data(object_urls.get(url)?.target.blob_id()?) } - /// Atomically capture the Blob backing and the URL creator's access key. - /// Fragments are excluded from lookup; the snapshot survives revocation. - pub fn object_url_entry(&self, url: &str) -> Option> - where - AccessKey: Clone, - { - let url = url.split_once('#').map_or(url, |(url, _)| url); - let object_urls = self.object_urls.lock(); - let state = object_urls.get(url)?; - let (bytes, mime_type) = self.object_url_blob_data(state.target.blob_id()?)?; - Some(ObjectUrlEntry { - bytes, - mime_type, - access_key: state.access_key.clone(), - }) - } - fn object_url_blob_data(&self, blob_id: BlobId) -> Option<(Arc<[u8]>, String)> { let blobs = self.blobs.lock(); let blob = blobs.by_id.get(&blob_id)?; @@ -400,7 +383,33 @@ where { let url = url.split_once('#').map_or(url, |(url, _)| url); let object_urls = self.object_urls.lock(); - match &object_urls.get(url)?.target { + self.object_url_target_data(&object_urls.get(url)?.target) + } + + /// Atomically snapshot the object and its creator key. Fragment handling + /// and backing-store ownership are the same as for `object_url_data`. + pub fn object_url_entry(&self, url: &str) -> Option> + where + AccessKey: Clone, + MediaSource: Clone, + { + let url = url.split_once('#').map_or(url, |(url, _)| url); + let object_urls = self.object_urls.lock(); + let state = object_urls.get(url)?; + Some(ObjectUrlEntry { + data: self.object_url_target_data(&state.target)?, + access_key: state.access_key.clone(), + }) + } + + fn object_url_target_data( + &self, + target: &ObjectUrlTarget, + ) -> Option> + where + MediaSource: Clone, + { + match target { ObjectUrlTarget::Blob(id) => { let (bytes, mime_type) = self.object_url_blob_data(*id)?; Some(ObjectUrlData::Blob { @@ -583,7 +592,9 @@ mod tests { .object_url_entry(&format!("{first}#fragment")) .unwrap(); let other = store.object_url_entry(&second).unwrap(); - assert!(Arc::ptr_eq(&captured.bytes, &other.bytes)); + let ObjectUrlData::Blob { bytes: captured_bytes, mime_type } = &captured.data; + let ObjectUrlData::Blob { bytes: other_bytes, .. } = &other.data; + assert!(Arc::ptr_eq(captured_bytes, other_bytes)); assert_eq!(captured.access_key.as_ref(), Some(&first_key)); assert_eq!(other.access_key.as_ref(), Some(&second_key)); assert_eq!( @@ -595,8 +606,8 @@ mod tests { assert!(store.revoke_object_url_with_access_key(&second, &second_key)); assert!(store.object_url_entry(&first).is_none()); assert!(store.blob_bytes(blob).is_none()); - assert_eq!(&*captured.bytes, b"private"); - assert_eq!(captured.mime_type, "text/plain"); + assert_eq!(&**captured_bytes, b"private"); + assert_eq!(mime_type, "text/plain"); assert_eq!(captured.access_key.as_ref(), Some(&first_key)); } @@ -610,6 +621,36 @@ mod tests { assert!(store.object_url_entry(&format!("{url}?query")).is_none()); } + #[test] + fn captured_object_url_entries_retain_the_creator_key_after_revocation() { + let store = BlobStore::::default(); + let blob = store.create_blob( + Some(1), + Some(2), + b"private".to_vec(), + "text/plain".to_owned(), + ); + let key = "creator storage partition".to_owned(); + let url = store + .create_object_url_with_lifetime_and_access_key( + Some(1), + Some(3), + blob, + "null", + Some(key.clone()), + ) + .unwrap(); + let captured = store.object_url_entry(&format!("{url}#fragment")).unwrap(); + store.release_blob_wrapper_ref(blob); + assert!(store.revoke_object_url_with_access_key(&url, &key)); + assert!(store.object_url_entry(&url).is_none()); + assert!(store.blob_bytes(blob).is_none()); + assert_eq!(captured.access_key.as_ref(), Some(&key)); + let ObjectUrlData::Blob { bytes, mime_type } = captured.data; + assert_eq!(&*bytes, b"private"); + assert_eq!(mime_type, "text/plain"); + } + #[test] fn media_source_urls_preserve_type_identity_and_creator_lifetimes() { let store = BlobStore::>::default(); @@ -638,7 +679,6 @@ mod tests { .unwrap(); assert_ne!(first, second); assert!(store.object_url_shared_bytes_and_type(&first).is_none()); - assert!(store.object_url_entry(&first).is_none()); assert_eq!(store.object_url_metadata(&first).as_deref(), Some("policy")); let Some(ObjectUrlData::MediaSource(captured)) = store.object_url_data(&format!("{first}#fragment"))