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.
This commit is contained in:
ldm0
2026-10-11 18:06:48 +08:00
parent 20e92e7ede
commit c0d6ca4fdc
+71 -31
View File
@@ -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<AccessKey> {
pub bytes: Arc<[u8]>,
pub mime_type: String,
pub access_key: Option<AccessKey>,
}
/// 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(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<AccessKey, MediaSource = std::convert::Infallible> {
pub data: ObjectUrlData<MediaSource>,
pub access_key: Option<AccessKey>,
}
#[derive(Clone, Debug)]
struct BlobState<OwnerId, PartitionId> {
owner_id: Option<OwnerId>,
@@ -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<ObjectUrlEntry<AccessKey>>
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<ObjectUrlEntry<AccessKey, MediaSource>>
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<MediaSource>,
) -> Option<ObjectUrlData<MediaSource>>
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::<u64, u64, String>::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::<u64, u64, String, String, Arc<String>>::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"))