gemini_memory_rs/okf/
store.rs1use async_trait::async_trait;
9use std::collections::BTreeMap;
10use std::path::{Component, Path, PathBuf};
11
12use crate::core::MemoryError;
13
14#[async_trait]
16pub trait OkfStore: Send + Sync {
17 async fn read(&self, path: &str) -> Result<Option<String>, MemoryError>;
19
20 async fn write(&self, path: &str, contents: &str) -> Result<(), MemoryError>;
22
23 async fn remove(&self, path: &str) -> Result<(), MemoryError>;
25
26 async fn list(&self, prefix: &str) -> Result<Vec<String>, MemoryError>;
28}
29
30#[derive(Debug, Default)]
32pub struct MemoryStore {
33 files: parking_lot::RwLock<BTreeMap<String, String>>,
34}
35
36impl MemoryStore {
37 pub fn new() -> Self {
39 Self::default()
40 }
41
42 pub fn paths(&self) -> Vec<String> {
44 self.files.read().keys().cloned().collect()
45 }
46}
47
48#[async_trait]
49impl OkfStore for MemoryStore {
50 async fn read(&self, path: &str) -> Result<Option<String>, MemoryError> {
51 Ok(self.files.read().get(path).cloned())
52 }
53
54 async fn write(&self, path: &str, contents: &str) -> Result<(), MemoryError> {
55 reject_traversal(path)?;
56 self.files
57 .write()
58 .insert(path.to_string(), contents.to_string());
59 Ok(())
60 }
61
62 async fn remove(&self, path: &str) -> Result<(), MemoryError> {
63 self.files.write().remove(path);
64 Ok(())
65 }
66
67 async fn list(&self, prefix: &str) -> Result<Vec<String>, MemoryError> {
68 Ok(self
69 .files
70 .read()
71 .keys()
72 .filter(|k| k.starts_with(prefix))
73 .cloned()
74 .collect())
75 }
76}
77
78#[derive(Debug, Clone)]
84pub struct FsStore {
85 root: PathBuf,
86}
87
88impl FsStore {
89 pub fn new(root: impl Into<PathBuf>) -> Self {
91 Self { root: root.into() }
92 }
93
94 pub fn root(&self) -> &Path {
96 &self.root
97 }
98
99 fn resolve(&self, path: &str) -> Result<PathBuf, MemoryError> {
100 reject_traversal(path)?;
101 Ok(self.root.join(path))
102 }
103}
104
105#[async_trait]
106impl OkfStore for FsStore {
107 async fn read(&self, path: &str) -> Result<Option<String>, MemoryError> {
108 let full = self.resolve(path)?;
109 match tokio::fs::read_to_string(&full).await {
110 Ok(contents) => Ok(Some(contents)),
111 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
112 Err(e) => Err(MemoryError::Storage(format!("{}: {e}", full.display()))),
113 }
114 }
115
116 async fn write(&self, path: &str, contents: &str) -> Result<(), MemoryError> {
117 let full = self.resolve(path)?;
118 if let Some(parent) = full.parent() {
119 tokio::fs::create_dir_all(parent)
120 .await
121 .map_err(|e| MemoryError::Storage(format!("{}: {e}", parent.display())))?;
122 }
123 let temp = full.with_extension("tmp");
126 tokio::fs::write(&temp, contents)
127 .await
128 .map_err(|e| MemoryError::Storage(format!("{}: {e}", temp.display())))?;
129 tokio::fs::rename(&temp, &full)
130 .await
131 .map_err(|e| MemoryError::Storage(format!("{}: {e}", full.display())))?;
132 Ok(())
133 }
134
135 async fn remove(&self, path: &str) -> Result<(), MemoryError> {
136 let full = self.resolve(path)?;
137 match tokio::fs::remove_file(&full).await {
138 Ok(()) => Ok(()),
139 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
140 Err(e) => Err(MemoryError::Storage(format!("{}: {e}", full.display()))),
141 }
142 }
143
144 async fn list(&self, prefix: &str) -> Result<Vec<String>, MemoryError> {
145 reject_traversal(prefix)?;
146 let mut out = Vec::new();
147 let mut stack = vec![self.root.clone()];
148 while let Some(dir) = stack.pop() {
149 let mut entries = match tokio::fs::read_dir(&dir).await {
150 Ok(entries) => entries,
151 Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
152 Err(e) => return Err(MemoryError::Storage(format!("{}: {e}", dir.display()))),
153 };
154 while let Some(entry) = entries
155 .next_entry()
156 .await
157 .map_err(|e| MemoryError::Storage(e.to_string()))?
158 {
159 let path = entry.path();
160 if path.is_dir() {
161 stack.push(path);
162 continue;
163 }
164 if let Ok(relative) = path.strip_prefix(&self.root) {
165 let relative = relative.to_string_lossy().replace('\\', "/");
166 if relative.starts_with(prefix) {
167 out.push(relative);
168 }
169 }
170 }
171 }
172 out.sort();
173 Ok(out)
174 }
175}
176
177fn reject_traversal(path: &str) -> Result<(), MemoryError> {
179 let candidate = Path::new(path);
180 if candidate.is_absolute() {
181 return Err(MemoryError::PolicyRefused(format!(
182 "absolute memory path `{path}` refused"
183 )));
184 }
185 for component in candidate.components() {
186 if matches!(component, Component::ParentDir | Component::RootDir) {
187 return Err(MemoryError::PolicyRefused(format!(
188 "memory path `{path}` escapes its namespace"
189 )));
190 }
191 }
192 Ok(())
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[tokio::test]
200 async fn memory_store_round_trips_and_lists_by_prefix() {
201 let store = MemoryStore::new();
202 store.write("users/a/profile.md", "one").await.unwrap();
203 store.write("users/b/profile.md", "two").await.unwrap();
204
205 assert_eq!(
206 store.read("users/a/profile.md").await.unwrap().as_deref(),
207 Some("one")
208 );
209 assert_eq!(store.list("users/a/").await.unwrap().len(), 1);
210
211 store.remove("users/a/profile.md").await.unwrap();
212 assert!(store.read("users/a/profile.md").await.unwrap().is_none());
213 store.remove("users/a/profile.md").await.unwrap();
215 }
216
217 #[tokio::test]
218 async fn traversal_is_refused_before_it_reaches_the_filesystem() {
219 let store = MemoryStore::new();
220 let err = store.write("../../etc/passwd", "x").await.unwrap_err();
221 assert!(matches!(err, MemoryError::PolicyRefused(_)));
222 assert!(store.write("/etc/passwd", "x").await.is_err());
223 }
224
225 #[tokio::test]
226 async fn fs_store_round_trips_through_a_real_directory() {
227 let root = std::env::temp_dir().join(format!("okf-test-{}", uuid::Uuid::new_v4()));
228 let store = FsStore::new(&root);
229
230 store
231 .write("users/usr_1/preferences.md", "hello")
232 .await
233 .unwrap();
234 assert_eq!(
235 store
236 .read("users/usr_1/preferences.md")
237 .await
238 .unwrap()
239 .as_deref(),
240 Some("hello")
241 );
242 assert_eq!(
243 store.list("users/").await.unwrap(),
244 vec!["users/usr_1/preferences.md"]
245 );
246 assert!(
247 store
248 .read("users/usr_1/missing.md")
249 .await
250 .unwrap()
251 .is_none()
252 );
253
254 store.remove("users/usr_1/preferences.md").await.unwrap();
255 assert!(store.list("users/").await.unwrap().is_empty());
256
257 let _ = tokio::fs::remove_dir_all(&root).await;
258 }
259}