mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 16:07:24 +00:00
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:
@@ -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"))
|
||||
|
||||
Reference in New Issue
Block a user