1//! The pictures that go with what Whiskers remembers. A picture's name is the same on every device 2//! (made from the time and a counter), so the same name is the same picture and is never replaced. 3//! Names arrive from outside, so a name that could touch a path or a key it should not is not 4//! representable here: a [`SafePictureId`] can only be made by passing [`PictureId::is_safe`]. 5 6use serde::Serialize; 7use whiskers_core::PictureId; 8 9use crate::error::StoreError; 10 11/// A picture name that has been checked: a bare file name of the kind pictures are given, with no 12/// path, no odd characters and a picture extension. 13#[derive(Clone, Debug, PartialEq, Eq, Serialize)] 14pub struct SafePictureId(PictureId); 15 16#[derive(Clone, Copy, Debug, PartialEq, Eq)] 17pub struct UnsafePictureId; 18 19impl SafePictureId { 20 pub fn new(id: PictureId) -> Result<Self, UnsafePictureId> { 21 if id.is_safe() { Ok(Self(id)) } else { Err(UnsafePictureId) } 22 } 23 24 pub fn parse(name: &str) -> Result<Self, UnsafePictureId> { 25 Self::new(PictureId(name.to_owned())) 26 } 27 28 pub fn as_str(&self) -> &str { 29 &self.0.0 30 } 31} 32 33/// What a `put` did. 34#[derive(Clone, Copy, Debug, PartialEq, Eq)] 35pub enum Put { 36 Stored, 37 /// A picture was already kept under this name. It is left exactly as it was. 38 AlreadyKept, 39} 40 41/// The shelf of pictures. 42/// 43/// Contract, for every adapter: 44/// 45/// - **Never replaces.** `put` under a name already kept changes nothing and says so. 46/// - **Exactly what was put.** `get` returns the bytes `put` stored, byte for byte. 47/// - **Atomic.** A `get` never sees half of a `put`; of two `put`s of one name, one wins whole. 48/// - **Durable once `put` returns `Ok`.** 49#[expect(async_fn_in_trait, reason = "a Worker's futures hold JavaScript values and cannot be Send, so no Send bound may be required here")] 50pub trait PictureShelf { 51 async fn has(&self, id: &SafePictureId) -> Result<bool, StoreError>; 52 53 async fn put(&self, id: &SafePictureId, bytes: &[u8]) -> Result<Put, StoreError>; 54 55 async fn get(&self, id: &SafePictureId) -> Result<Option<Vec<u8>>, StoreError>; 56} 57 58#[cfg(test)] 59mod tests { 60 use super::*; 61 62 #[test] 63 fn a_name_that_could_reach_a_path_is_not_a_picture_id() { 64 assert!(SafePictureId::parse("0000000000000000-0000.jpg").is_ok()); 65 for bad in ["../x.jpg", "a/b.jpg", ".hidden.jpg", "a..b.jpg", "noextension", "x.exe", "", &"a".repeat(70)] { 66 assert!(SafePictureId::parse(bad).is_err(), "{bad:?}"); 67 } 68 } 69}