1use 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#[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 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 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 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 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}