Skip to main content

vv_agent/workspace/
memory.rs

1use std::any::Any;
2use std::collections::{BTreeMap, BTreeSet};
3use std::sync::{Arc, Mutex};
4
5use super::{
6    artifacts::is_reserved_artifact_path, current_utc_isoformat, exclusive_workspace_path,
7    glob_match, insert_parent_dirs, normalize_workspace_path, normalized_glob_pattern, not_found,
8    suffix_with_dot, FileInfo, WorkspaceBackend,
9};
10
11#[derive(Debug, Clone)]
12pub struct MemoryWorkspaceBackend {
13    files: Arc<Mutex<BTreeMap<String, Vec<u8>>>>,
14    dirs: Arc<Mutex<BTreeSet<String>>>,
15}
16
17impl Default for MemoryWorkspaceBackend {
18    fn default() -> Self {
19        let mut dirs = BTreeSet::new();
20        dirs.insert(String::new());
21        Self {
22            files: Arc::new(Mutex::new(BTreeMap::new())),
23            dirs: Arc::new(Mutex::new(dirs)),
24        }
25    }
26}
27
28impl WorkspaceBackend for MemoryWorkspaceBackend {
29    fn as_any(&self) -> &dyn Any {
30        self
31    }
32
33    fn list_files(&self, base: &str, glob: &str) -> std::io::Result<Vec<String>> {
34        let base = normalize_workspace_path(base);
35        if is_reserved_artifact_path(&base) {
36            return Ok(Vec::new());
37        }
38        let glob = normalized_glob_pattern(glob);
39        let pattern = if base.is_empty() {
40            glob
41        } else {
42            format!("{base}/{glob}")
43        };
44        let files = self.files.lock().expect("memory workspace poisoned");
45        let mut matches = files
46            .keys()
47            .filter(|path| !is_reserved_artifact_path(path))
48            .filter(|path| glob_match(path, &pattern))
49            .cloned()
50            .collect::<Vec<_>>();
51        matches.sort();
52        Ok(matches)
53    }
54
55    fn read_text(&self, path: &str) -> std::io::Result<String> {
56        let bytes = self.read_bytes(path)?;
57        Ok(String::from_utf8_lossy(&bytes).to_string())
58    }
59
60    fn read_bytes(&self, path: &str) -> std::io::Result<Vec<u8>> {
61        let key = normalize_workspace_path(path);
62        let files = self.files.lock().expect("memory workspace poisoned");
63        files.get(&key).cloned().ok_or_else(|| not_found(path))
64    }
65
66    fn write_text(&self, path: &str, content: &str, append: bool) -> std::io::Result<usize> {
67        if is_reserved_artifact_path(path) {
68            return Err(std::io::Error::new(
69                std::io::ErrorKind::PermissionDenied,
70                "artifact paths are immutable",
71            ));
72        }
73        let key = normalize_workspace_path(path);
74        let mut files = self.files.lock().expect("memory workspace poisoned");
75        let entry = files.entry(key.clone()).or_default();
76        if append {
77            entry.extend_from_slice(content.as_bytes());
78        } else {
79            *entry = content.as_bytes().to_vec();
80        }
81        drop(files);
82        self.ensure_parent_dirs(&key);
83        Ok(content.len())
84    }
85
86    fn write_text_exclusive(&self, path: &str, content: &str) -> std::io::Result<usize> {
87        let mut chunks = std::iter::once(Ok(content.to_string()));
88        self.write_text_chunks_exclusive(path, &mut chunks)
89    }
90
91    fn write_text_chunks_exclusive(
92        &self,
93        path: &str,
94        chunks: &mut dyn Iterator<Item = std::io::Result<String>>,
95    ) -> std::io::Result<usize> {
96        let key = exclusive_workspace_path(path)?;
97        let mut data = Vec::new();
98        for chunk in chunks {
99            data.extend_from_slice(chunk?.as_bytes());
100        }
101        let size = data.len();
102        let mut files = self.files.lock().expect("memory workspace poisoned");
103        let dirs = self.dirs.lock().expect("memory workspace poisoned");
104        if files.contains_key(&key) || dirs.contains(&key) {
105            return Err(std::io::Error::new(
106                std::io::ErrorKind::AlreadyExists,
107                format!("path already exists: {path}"),
108            ));
109        }
110        for parent in key.split('/').scan(String::new(), |current, segment| {
111            if !current.is_empty() {
112                current.push('/');
113            }
114            current.push_str(segment);
115            Some(current.clone())
116        }) {
117            if parent != key && files.contains_key(&parent) {
118                return Err(std::io::Error::new(
119                    std::io::ErrorKind::NotADirectory,
120                    format!("exclusive path parent is not a directory: {parent}"),
121                ));
122            }
123        }
124        files.insert(key.clone(), data);
125        drop(dirs);
126        drop(files);
127        self.ensure_parent_dirs(&key);
128        Ok(size)
129    }
130
131    fn file_info(&self, path: &str) -> std::io::Result<Option<FileInfo>> {
132        let key = normalize_workspace_path(path);
133        let files = self.files.lock().expect("memory workspace poisoned");
134        if let Some(content) = files.get(&key) {
135            return Ok(Some(FileInfo {
136                path: key,
137                is_file: true,
138                is_dir: false,
139                size: content.len() as u64,
140                modified_at: current_utc_isoformat(),
141                suffix: suffix_with_dot(path),
142            }));
143        }
144        drop(files);
145        let dirs = self.dirs.lock().expect("memory workspace poisoned");
146        Ok(dirs.get(&key).map(|path| FileInfo {
147            path: if path.is_empty() {
148                ".".to_string()
149            } else {
150                path.clone()
151            },
152            is_file: false,
153            is_dir: true,
154            size: 0,
155            modified_at: current_utc_isoformat(),
156            suffix: String::new(),
157        }))
158    }
159
160    fn exists(&self, path: &str) -> bool {
161        let key = normalize_workspace_path(path);
162        self.files
163            .lock()
164            .expect("memory workspace poisoned")
165            .contains_key(&key)
166            || self
167                .dirs
168                .lock()
169                .expect("memory workspace poisoned")
170                .contains(&key)
171    }
172
173    fn is_file(&self, path: &str) -> bool {
174        let key = normalize_workspace_path(path);
175        self.files
176            .lock()
177            .expect("memory workspace poisoned")
178            .contains_key(&key)
179    }
180
181    fn mkdir(&self, path: &str) -> std::io::Result<()> {
182        let key = normalize_workspace_path(path);
183        let mut dirs = self.dirs.lock().expect("memory workspace poisoned");
184        dirs.insert(key.clone());
185        insert_parent_dirs(&mut dirs, &key);
186        Ok(())
187    }
188}
189
190impl MemoryWorkspaceBackend {
191    fn ensure_parent_dirs(&self, key: &str) {
192        let mut dirs = self.dirs.lock().expect("memory workspace poisoned");
193        insert_parent_dirs(&mut dirs, key);
194    }
195}