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}