use std::path::Path;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::error::AppError;
pub mod local;
#[cfg(any(feature = "blob-s3", feature = "blob-gcs"))]
pub mod object;
#[cfg(any(test, feature = "test-support"))]
pub mod conformance;
pub const MAX_BLOB_BYTES: u64 = 4_096 * 262_144;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct BlobKey(String);
impl BlobKey {
pub fn for_room(room_id: &str, blob_ref: &str) -> Result<Self, AppError> {
validate_blob_ref(blob_ref)?;
Ok(Self(format!("{}{blob_ref}", room_prefix(room_id))))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
pub fn room_prefix(room_id: &str) -> String {
let digest = Sha256::digest(room_id.as_bytes());
let mut hex = String::with_capacity(32);
for b in &digest[..16] {
hex.push_str(&format!("{b:02x}"));
}
format!("rooms/{hex}/")
}
pub fn validate_blob_ref(blob_ref: &str) -> Result<(), AppError> {
let well_formed = blob_ref.len() <= 128
&& match blob_ref.as_bytes().first() {
Some(b'z') => blob_ref[1..]
.bytes()
.all(|c| c.is_ascii_alphanumeric() && !matches!(c, b'0' | b'O' | b'I' | b'l')),
Some(b'u') => blob_ref[1..]
.bytes()
.all(|c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_'),
_ => false,
};
if !well_formed || sha256_from_digest_multibase(blob_ref).is_none() {
return Err(AppError::Validation(format!(
"`{blob_ref}` is not a sha2-256 DigestMultibase"
)));
}
Ok(())
}
pub fn blob_ref_of_manifest(manifest: &serde_json::Value) -> Result<String, AppError> {
let canonical = serde_json_canonicalizer::to_vec(manifest)
.map_err(|e| AppError::Validation(format!("manifest cannot be canonicalised: {e}")))?;
Ok(sha256_digest_multibase(&Sha256::digest(&canonical).into()))
}
pub fn sha256_digest_multibase(digest: &[u8; 32]) -> String {
vta_sdk::protocols::backup_management::chunked::sha256_digest_multibase(digest)
}
pub fn sha256_from_digest_multibase(value: &str) -> Option<[u8; 32]> {
vta_sdk::protocols::backup_management::chunked::sha256_from_digest_multibase(value)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BackendRef {
pub kind: String,
pub key: BlobKey,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Deletion {
Deleted,
Lapses { at: chrono::DateTime<chrono::Utc> },
Unsupported,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Health {
pub ok: bool,
pub detail: Option<String>,
}
#[async_trait]
pub trait BlobStore: Send + Sync {
fn kind(&self) -> &'static str;
async fn put(&self, key: &BlobKey, staged: &Path, size: u64) -> Result<BackendRef, AppError>;
async fn get_range(&self, at: &BackendRef, start: u64, end: u64) -> Result<Vec<u8>, AppError>;
async fn delete(&self, at: &BackendRef) -> Result<Deletion, AppError>;
async fn health(&self) -> Health;
}
pub(crate) fn check_kind(store: &dyn BlobStore, at: &BackendRef) -> Result<(), AppError> {
if at.kind != store.kind() {
return Err(AppError::Internal(format!(
"blob `{}` was stored by a `{}` store, not this `{}` one",
at.key.as_str(),
at.kind,
store.kind()
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn a_ref() -> String {
sha256_digest_multibase(&[7u8; 32])
}
#[test]
fn room_prefix_hides_the_room_and_is_stable() {
let p = room_prefix("did:webvh:QmRoom:example.com:room");
assert!(p.starts_with("rooms/") && p.ends_with('/'));
assert_eq!(p.len(), "rooms/".len() + 32 + 1);
assert!(!p.contains("example"));
assert_eq!(p, room_prefix("did:webvh:QmRoom:example.com:room"));
assert_ne!(p, room_prefix("did:webvh:QmOther:example.com:room"));
}
#[test]
fn a_key_is_built_only_from_a_real_digest() {
let key = BlobKey::for_room("did:key:zRoom", &a_ref()).unwrap();
assert!(key.as_str().ends_with(&a_ref()));
for bad in [
"",
"../etc/passwd",
"zQm/../x",
"sha256:abcd",
"z0OIl",
"uAAAA",
] {
assert!(BlobKey::for_room("did:key:zRoom", bad).is_err(), "{bad}");
}
}
#[test]
fn blob_ref_is_over_the_canonical_form() {
let a = serde_json::json!({"size": 5, "digest": "zX", "chunks": {"chunkSize": 16384}});
let b: serde_json::Value =
serde_json::from_str(r#"{"chunks":{"chunkSize":16384},"digest":"zX","size":5}"#)
.unwrap();
assert_eq!(
blob_ref_of_manifest(&a).unwrap(),
blob_ref_of_manifest(&b).unwrap()
);
validate_blob_ref(&blob_ref_of_manifest(&a).unwrap()).unwrap();
}
}