1use crate::LoadError;
8use std::collections::HashMap;
9use std::fs;
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12
13pub trait FileSystem: Send + Sync + std::fmt::Debug {
19 fn read(&self, path: &Path) -> Result<Arc<str>, LoadError>;
25
26 fn exists(&self, path: &Path) -> bool;
28
29 fn is_encrypted(&self, path: &Path) -> bool;
34
35 fn normalize(&self, path: &Path) -> PathBuf;
40
41 fn glob(&self, pattern: &str) -> Result<Vec<PathBuf>, String> {
47 let _ = pattern;
48 Err("glob is not supported by this filesystem".to_string())
49 }
50}
51
52#[derive(Debug, Default, Clone)]
57pub struct DiskFileSystem;
58
59impl FileSystem for DiskFileSystem {
60 fn read(&self, path: &Path) -> Result<Arc<str>, LoadError> {
61 let bytes = fs::read(path).map_err(|e| LoadError::Io {
62 path: path.to_path_buf(),
63 source: e,
64 })?;
65
66 let content = match String::from_utf8(bytes) {
68 Ok(s) => s,
69 Err(e) => String::from_utf8_lossy(e.as_bytes()).into_owned(),
70 };
71
72 Ok(content.into())
73 }
74
75 fn exists(&self, path: &Path) -> bool {
76 path.exists()
77 }
78
79 fn is_encrypted(&self, path: &Path) -> bool {
80 match path.extension().and_then(|e| e.to_str()) {
81 Some("gpg") => true,
82 Some("asc") => {
83 if let Ok(content) = fs::read_to_string(path) {
85 let check_len = 1024.min(content.len());
86 content[..check_len].contains("-----BEGIN PGP MESSAGE-----")
87 } else {
88 false
89 }
90 }
91 _ => false,
92 }
93 }
94
95 fn normalize(&self, path: &Path) -> PathBuf {
96 if let Ok(canonical) = path.canonicalize() {
98 return canonical;
99 }
100
101 if path.is_absolute() {
103 path.to_path_buf()
104 } else if let Ok(cwd) = std::env::current_dir() {
105 let mut result = cwd;
107 for component in path.components() {
108 match component {
109 std::path::Component::ParentDir => {
110 result.pop();
111 }
112 std::path::Component::Normal(s) => {
113 result.push(s);
114 }
115 std::path::Component::CurDir => {}
116 std::path::Component::RootDir => {
117 result = PathBuf::from("/");
118 }
119 std::path::Component::Prefix(p) => {
120 result = PathBuf::from(p.as_os_str());
121 }
122 }
123 }
124 result
125 } else {
126 path.to_path_buf()
128 }
129 }
130
131 fn glob(&self, pattern: &str) -> Result<Vec<PathBuf>, String> {
132 let entries = glob::glob(pattern).map_err(|e| e.to_string())?;
133 let mut matched: Vec<PathBuf> = entries.filter_map(Result::ok).collect();
137 matched.sort();
138 Ok(matched)
139 }
140}
141
142#[derive(Debug, Default, Clone)]
159pub struct VirtualFileSystem {
160 files: HashMap<PathBuf, Arc<str>>,
161}
162
163impl VirtualFileSystem {
164 #[must_use]
166 pub fn new() -> Self {
167 Self::default()
168 }
169
170 pub fn add_file(&mut self, path: impl AsRef<Path>, content: impl Into<String>) {
175 let normalized = normalize_vfs_path(path.as_ref());
176 self.files.insert(normalized, content.into().into());
177 }
178
179 pub fn add_files(
183 &mut self,
184 files: impl IntoIterator<Item = (impl AsRef<Path>, impl Into<String>)>,
185 ) {
186 for (path, content) in files {
187 self.add_file(path, content);
188 }
189 }
190
191 #[must_use]
193 pub fn from_files(
194 files: impl IntoIterator<Item = (impl AsRef<Path>, impl Into<String>)>,
195 ) -> Self {
196 let mut vfs = Self::new();
197 vfs.add_files(files);
198 vfs
199 }
200
201 #[must_use]
203 pub fn len(&self) -> usize {
204 self.files.len()
205 }
206
207 #[must_use]
209 pub fn is_empty(&self) -> bool {
210 self.files.is_empty()
211 }
212}
213
214impl FileSystem for VirtualFileSystem {
215 fn read(&self, path: &Path) -> Result<Arc<str>, LoadError> {
216 let normalized = normalize_vfs_path(path);
217
218 self.files
219 .get(&normalized)
220 .cloned()
221 .ok_or_else(|| LoadError::Io {
222 path: path.to_path_buf(),
223 source: std::io::Error::new(
224 std::io::ErrorKind::NotFound,
225 format!("file not found in virtual filesystem: {}", path.display()),
226 ),
227 })
228 }
229
230 fn exists(&self, path: &Path) -> bool {
231 let normalized = normalize_vfs_path(path);
232 self.files.contains_key(&normalized)
233 }
234
235 fn is_encrypted(&self, _path: &Path) -> bool {
236 false
239 }
240
241 fn normalize(&self, path: &Path) -> PathBuf {
242 normalize_vfs_path(path)
244 }
245
246 fn glob(&self, pattern: &str) -> Result<Vec<PathBuf>, String> {
247 let normalized = pattern.replace('\\', "/");
250 let normalized = normalized.strip_prefix("./").unwrap_or(&normalized);
251 let glob_pattern = glob::Pattern::new(normalized).map_err(|e| e.to_string())?;
252 let mut matched: Vec<PathBuf> = self
253 .files
254 .keys()
255 .filter(|path| glob_pattern.matches_path(path))
256 .cloned()
257 .collect();
258 matched.sort();
259 Ok(matched)
260 }
261}
262
263fn normalize_vfs_path(path: &Path) -> PathBuf {
270 let path_str = path.to_string_lossy();
271
272 let normalized = path_str.replace('\\', "/");
274
275 let normalized = normalized.strip_prefix("./").unwrap_or(&normalized);
277
278 let mut components = Vec::new();
280 for part in normalized.split('/') {
281 match part {
282 "" | "." => {}
283 ".." => {
284 if !components.is_empty() && components.last() != Some(&"..") {
286 components.pop();
287 } else {
288 components.push("..");
289 }
290 }
291 _ => components.push(part),
292 }
293 }
294
295 if components.is_empty() {
296 PathBuf::from(".")
297 } else {
298 PathBuf::from(components.join("/"))
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn test_normalize_vfs_path() {
308 assert_eq!(
309 normalize_vfs_path(Path::new("foo/bar")),
310 PathBuf::from("foo/bar")
311 );
312 assert_eq!(
313 normalize_vfs_path(Path::new("./foo/bar")),
314 PathBuf::from("foo/bar")
315 );
316 assert_eq!(
317 normalize_vfs_path(Path::new("foo/../bar")),
318 PathBuf::from("bar")
319 );
320 assert_eq!(
321 normalize_vfs_path(Path::new("foo/./bar")),
322 PathBuf::from("foo/bar")
323 );
324 assert_eq!(
325 normalize_vfs_path(Path::new("foo\\bar")),
326 PathBuf::from("foo/bar")
327 );
328 }
329
330 #[test]
331 fn test_virtual_filesystem_basic() {
332 let mut vfs = VirtualFileSystem::new();
333 vfs.add_file("test.beancount", "2024-01-01 open Assets:Bank USD");
334
335 assert!(vfs.exists(Path::new("test.beancount")));
336 assert!(!vfs.exists(Path::new("nonexistent.beancount")));
337
338 let content = vfs.read(Path::new("test.beancount")).unwrap();
339 assert_eq!(&*content, "2024-01-01 open Assets:Bank USD");
340 }
341
342 #[test]
343 fn test_virtual_filesystem_path_normalization() {
344 let mut vfs = VirtualFileSystem::new();
345 vfs.add_file("foo/bar.beancount", "content");
346
347 assert!(vfs.exists(Path::new("foo/bar.beancount")));
349 assert!(vfs.exists(Path::new("./foo/bar.beancount")));
350
351 let content = vfs.read(Path::new("./foo/bar.beancount")).unwrap();
353 assert_eq!(&*content, "content");
354 }
355
356 #[test]
357 fn test_virtual_filesystem_not_encrypted() {
358 let vfs = VirtualFileSystem::new();
359
360 assert!(!vfs.is_encrypted(Path::new("test.gpg")));
362 assert!(!vfs.is_encrypted(Path::new("test.asc")));
363 }
364
365 #[test]
366 fn test_virtual_filesystem_from_files() {
367 let vfs = VirtualFileSystem::from_files([
368 ("a.beancount", "content a"),
369 ("b.beancount", "content b"),
370 ]);
371
372 assert_eq!(vfs.len(), 2);
373 assert!(vfs.exists(Path::new("a.beancount")));
374 assert!(vfs.exists(Path::new("b.beancount")));
375 }
376}