The pictures that go with what Whiskers remembers. A picture's name is the same on every device (made from the time and a counter), so the same name is the same picture and is never replaced. Names arrive from outside, so a name that could touch a path or a key it should not is not representable here: a [SafePictureId] can only be made by passing [PictureId::is_safe].

6use serde::Serialize;
7use whiskers_core::PictureId;
9use crate::error::StoreError;

A picture name that has been checked: a bare file name of the kind pictures are given, with no path, no odd characters and a picture extension.

13#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
14pub struct SafePictureId(PictureId);
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}

What a put did.

34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum Put {
36    Stored,

A picture was already kept under this name. It is left exactly as it was.

38    AlreadyKept,
39}

The shelf of pictures.

Contract, for every adapter:

  • Never replaces. put under a name already kept changes nothing and says so.
  • Exactly what was put. get returns the bytes put stored, byte for byte.
  • Atomic. A get never sees half of a put; of two puts of one name, one wins whole.
  • 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}
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}