Skip to main content

solid_pod_rs/storage/
memory.rs

1//! In-memory storage backend.
2//!
3//! Designed for tests. The state lives in a single
4//! `Arc<RwLock<HashMap<String, (Bytes, ResourceMeta)>>>`. Change
5//! events are broadcast to all registered watchers; a watcher only
6//! receives events for paths that are equal to, or descend from, the
7//! path it was registered with.
8
9use std::collections::HashMap;
10use std::path::{Component, Path};
11use std::sync::Arc;
12
13use async_trait::async_trait;
14use bytes::Bytes;
15use sha2::{Digest, Sha256};
16use tokio::sync::{broadcast, mpsc, RwLock};
17
18use crate::error::PodError;
19use crate::storage::{ResourceMeta, Storage, StorageEvent};
20
21/// In-memory `Storage` implementation.
22#[derive(Clone)]
23pub struct MemoryBackend {
24    inner: Arc<Inner>,
25}
26
27struct Inner {
28    data: RwLock<HashMap<String, Entry>>,
29    events: broadcast::Sender<StorageEvent>,
30}
31
32#[derive(Clone)]
33struct Entry {
34    body: Bytes,
35    meta: ResourceMeta,
36}
37
38impl Default for MemoryBackend {
39    fn default() -> Self {
40        Self::new()
41    }
42}
43
44impl MemoryBackend {
45    /// Create a new empty backend.
46    pub fn new() -> Self {
47        let (events, _) = broadcast::channel(256);
48        Self {
49            inner: Arc::new(Inner {
50                data: RwLock::new(HashMap::new()),
51                events,
52            }),
53        }
54    }
55
56    fn compute_etag(body: &[u8]) -> String {
57        hex::encode(Sha256::digest(body))
58    }
59
60    /// Normalise and validate a storage path.
61    ///
62    /// Mirrors `FsBackend::normalize` so both shipped backends reject
63    /// the same malformed paths (NUL bytes and `..` / root / prefix
64    /// path segments) with `PodError::InvalidPath`. Previously this
65    /// backend stored such paths as literal keys while the filesystem
66    /// backend rejected them, so the same request succeeded or failed
67    /// depending on which backend a pod used.
68    fn normalize(path: &str) -> Result<String, PodError> {
69        let p = if path.is_empty() {
70            "/".to_string()
71        } else if path.starts_with('/') {
72            path.to_string()
73        } else {
74            format!("/{path}")
75        };
76        if p.contains('\0') {
77            return Err(PodError::InvalidPath(p));
78        }
79        let rel = p.trim_start_matches('/');
80        if Path::new(rel).components().any(|component| {
81            matches!(
82                component,
83                Component::ParentDir | Component::RootDir | Component::Prefix(_)
84            )
85        }) {
86            return Err(PodError::InvalidPath(p));
87        }
88        Ok(p)
89    }
90
91    fn is_under(child: &str, container: &str) -> bool {
92        if container == "/" {
93            return child != "/";
94        }
95        let c = container.trim_end_matches('/');
96        child == c || child.starts_with(&format!("{c}/"))
97    }
98}
99
100#[async_trait]
101impl Storage for MemoryBackend {
102    async fn get(&self, path: &str) -> Result<(Bytes, ResourceMeta), PodError> {
103        let path = Self::normalize(path)?;
104        let guard = self.inner.data.read().await;
105        guard
106            .get(&path)
107            .map(|e| (e.body.clone(), e.meta.clone()))
108            .ok_or(PodError::NotFound(path))
109    }
110
111    async fn put(
112        &self,
113        path: &str,
114        body: Bytes,
115        content_type: &str,
116    ) -> Result<ResourceMeta, PodError> {
117        let path = Self::normalize(path)?;
118        let etag = Self::compute_etag(&body);
119        let meta = ResourceMeta {
120            etag,
121            modified: chrono::Utc::now(),
122            size: body.len() as u64,
123            content_type: content_type.to_string(),
124            links: Vec::new(),
125        };
126        let mut guard = self.inner.data.write().await;
127        let existed = guard.contains_key(&path);
128        guard.insert(
129            path.clone(),
130            Entry {
131                body,
132                meta: meta.clone(),
133            },
134        );
135        drop(guard);
136        let event = if existed {
137            StorageEvent::Updated(path)
138        } else {
139            StorageEvent::Created(path)
140        };
141        let _ = self.inner.events.send(event);
142        Ok(meta)
143    }
144
145    async fn delete(&self, path: &str) -> Result<(), PodError> {
146        let path = Self::normalize(path)?;
147        let mut guard = self.inner.data.write().await;
148        match guard.remove(&path) {
149            Some(_) => {
150                drop(guard);
151                let _ = self.inner.events.send(StorageEvent::Deleted(path));
152                Ok(())
153            }
154            None => Err(PodError::NotFound(path)),
155        }
156    }
157
158    async fn list(&self, container: &str) -> Result<Vec<String>, PodError> {
159        let container = Self::normalize(container)?;
160        let container = if container.ends_with('/') {
161            container
162        } else {
163            format!("{container}/")
164        };
165        let guard = self.inner.data.read().await;
166        let mut seen = std::collections::BTreeSet::new();
167        for key in guard.keys() {
168            if !key.starts_with(&container) {
169                continue;
170            }
171            let remainder = &key[container.len()..];
172            if remainder.is_empty() {
173                continue;
174            }
175            let name = match remainder.find('/') {
176                Some(pos) => &remainder[..=pos],
177                None => remainder,
178            };
179            seen.insert(name.to_string());
180        }
181        Ok(seen.into_iter().collect())
182    }
183
184    async fn head(&self, path: &str) -> Result<ResourceMeta, PodError> {
185        let path = Self::normalize(path)?;
186        let guard = self.inner.data.read().await;
187        guard
188            .get(&path)
189            .map(|e| e.meta.clone())
190            .ok_or(PodError::NotFound(path))
191    }
192
193    async fn exists(&self, path: &str) -> Result<bool, PodError> {
194        let path = Self::normalize(path)?;
195        let guard = self.inner.data.read().await;
196        Ok(guard.contains_key(&path))
197    }
198
199    async fn create_container(&self, path: &str) -> Result<ResourceMeta, PodError> {
200        let container = Self::normalize(path)?;
201        let container = if container.ends_with('/') {
202            container
203        } else {
204            format!("{container}/")
205        };
206        let meta = ResourceMeta::new("container", 0, "application/ld+json");
207        let mut guard = self.inner.data.write().await;
208        guard.insert(
209            container.clone(),
210            Entry {
211                body: Bytes::new(),
212                meta: meta.clone(),
213            },
214        );
215        drop(guard);
216        let _ = self.inner.events.send(StorageEvent::Created(container));
217        Ok(meta)
218    }
219
220    async fn watch(&self, path: &str) -> Result<mpsc::Receiver<StorageEvent>, PodError> {
221        let filter_path = Self::normalize(path)?;
222        let mut rx = self.inner.events.subscribe();
223        let (tx, out_rx) = mpsc::channel(64);
224        tokio::spawn(async move {
225            while let Ok(event) = rx.recv().await {
226                let target = match &event {
227                    StorageEvent::Created(p)
228                    | StorageEvent::Updated(p)
229                    | StorageEvent::Deleted(p) => p.clone(),
230                };
231                if MemoryBackend::is_under(&target, &filter_path) && tx.send(event).await.is_err() {
232                    return;
233                }
234            }
235        });
236        Ok(out_rx)
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[tokio::test]
245    async fn put_get_roundtrip() {
246        let m = MemoryBackend::new();
247        m.put("/foo", Bytes::from_static(b"hello"), "text/plain")
248            .await
249            .unwrap();
250        let (body, meta) = m.get("/foo").await.unwrap();
251        assert_eq!(&body[..], b"hello");
252        assert_eq!(meta.size, 5);
253        assert_eq!(meta.content_type, "text/plain");
254    }
255
256    #[tokio::test]
257    async fn list_direct_children_only() {
258        let m = MemoryBackend::new();
259        m.put("/a/b", Bytes::from_static(b""), "text/plain")
260            .await
261            .unwrap();
262        m.put("/a/c/d", Bytes::from_static(b""), "text/plain")
263            .await
264            .unwrap();
265        let mut items = m.list("/a").await.unwrap();
266        items.sort();
267        assert_eq!(items, vec!["b".to_string(), "c/".to_string()]);
268    }
269
270    #[tokio::test]
271    async fn rejects_traversal_and_nul_paths_like_fs_backend() {
272        // Parity with FsBackend::normalize (see rejects_path_traversal
273        // in fs.rs): the in-memory backend must refuse the same
274        // malformed paths instead of storing them as literal keys.
275        let m = MemoryBackend::new();
276        for bad in ["/../escape", "/a/../b", "/a\0b"] {
277            let err = m
278                .put(bad, Bytes::from_static(b""), "text/plain")
279                .await
280                .unwrap_err();
281            assert!(matches!(err, PodError::InvalidPath(_)), "put({bad:?})");
282            assert!(
283                matches!(m.get(bad).await, Err(PodError::InvalidPath(_))),
284                "get({bad:?})"
285            );
286        }
287        // Clean paths, including one without a leading slash, still work.
288        m.put("ok.txt", Bytes::from_static(b"x"), "text/plain")
289            .await
290            .unwrap();
291        assert_eq!(&m.get("/ok.txt").await.unwrap().0[..], b"x");
292    }
293
294    #[tokio::test]
295    async fn watch_receives_created_event() {
296        let m = MemoryBackend::new();
297        let mut rx = m.watch("/").await.unwrap();
298        m.put("/x", Bytes::from_static(b"hi"), "text/plain")
299            .await
300            .unwrap();
301        let event = tokio::time::timeout(std::time::Duration::from_secs(1), rx.recv())
302            .await
303            .unwrap()
304            .unwrap();
305        assert_eq!(event, StorageEvent::Created("/x".into()));
306    }
307}