vv_agent/memory/session/
storage.rs1use std::path::{Path, PathBuf};
2
3use super::SessionMemory;
4
5const SESSION_MEMORY_FILENAME: &str = "session_memory.json";
6
7impl SessionMemory {
8 pub fn storage_path(&self) -> Option<PathBuf> {
9 let workspace = self.workspace.as_ref()?;
10 let workspace_root = workspace.canonicalize().ok()?;
11 let storage_dir = if self.config.storage_dir.is_absolute() {
12 self.config.storage_dir.clone()
13 } else {
14 workspace.join(&self.config.storage_dir)
15 };
16 let scoped_dir = match self
17 .storage_scope
18 .as_deref()
19 .filter(|scope| !scope.is_empty())
20 {
21 Some(scope) => storage_dir.join(sanitize_storage_scope(scope)?),
22 None => storage_dir,
23 };
24 let resolved = scoped_dir.join(SESSION_MEMORY_FILENAME);
25 let parent = resolved.parent()?;
26 let canonical_parent = if parent.exists() {
27 parent.canonicalize().ok()?
28 } else {
29 canonicalize_existing_ancestor(parent)?
30 };
31 if !canonical_parent.starts_with(&workspace_root) {
32 return None;
33 }
34 Some(resolved)
35 }
36}
37
38fn sanitize_storage_scope(raw: &str) -> Option<String> {
39 let safe = raw
40 .trim()
41 .chars()
42 .map(|ch| {
43 if ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '-') {
44 ch
45 } else {
46 '_'
47 }
48 })
49 .collect::<String>()
50 .trim_matches(['.', '_', '-'])
51 .to_string();
52 (!safe.is_empty()).then_some(safe)
53}
54
55fn canonicalize_existing_ancestor(path: &Path) -> Option<PathBuf> {
56 let mut current = path;
57 while !current.exists() {
58 current = current.parent()?;
59 }
60 current.canonicalize().ok()
61}