mcp_execution_files/filesystem.rs
1//! In-memory filesystem and export functionality.
2//!
3//! Provides an in-memory filesystem for MCP tool definitions with
4//! high-performance export to real filesystem.
5//!
6//! # Core Features
7//!
8//! - **In-memory storage**: Files stored in `HashMap` for O(1) lookup
9//! - **Filesystem export**: Sequential and parallel export modes
10//! - **Atomic writes**: Optional atomic file operations
11//! - **Thread-safe**: All types are `Send + Sync`
12//!
13//! # Performance Optimizations
14//!
15//! 1. **Directory Pre-creation**: Creates all directories first in single pass
16//! 2. **Parallel Writes**: Uses rayon for parallel file writing (opt-in)
17//! 3. **Atomic Operations**: Writes to temp file then renames
18//! 4. **Minimal Allocations**: Reuses path buffers, caches canonicalized base
19//!
20//! # Examples
21//!
22//! ## Basic usage
23//!
24//! ```
25//! use mcp_execution_files::FileSystem;
26//!
27//! let mut fs = FileSystem::new();
28//! fs.add_file("/mcp-tools/test.ts", "export const VERSION = '1.0';").unwrap();
29//!
30//! let content = fs.read_file("/mcp-tools/test.ts").unwrap();
31//! assert_eq!(content, "export const VERSION = '1.0';");
32//! ```
33//!
34//! ## Export to filesystem
35//!
36//! ```
37//! use mcp_execution_files::FilesBuilder;
38//! # use tempfile::TempDir;
39//!
40//! # let temp_dir = TempDir::new().unwrap();
41//! # let output_dir = temp_dir.path();
42//! let fs = FilesBuilder::new()
43//! .add_file("/tools/create.ts", "export function create() {}")
44//! .add_file("/tools/update.ts", "export function update() {}")
45//! .build()
46//! .unwrap();
47//!
48//! // Export to filesystem
49//! fs.export_to_filesystem(output_dir).unwrap();
50//!
51//! assert!(output_dir.join("tools/create.ts").exists());
52//! # Ok::<(), Box<dyn std::error::Error>>(())
53//! ```
54
55use crate::types::{FileEntry, FilePath, FilesError, Result};
56use std::collections::{HashMap, HashSet};
57use std::fs;
58use std::io::Write;
59use std::path::{Path, PathBuf};
60
61/// An in-memory virtual filesystem for MCP tool definitions.
62///
63/// `FileSystem` provides a read-only filesystem structure that stores generated
64/// TypeScript files in memory. Files are organized in a hierarchical structure
65/// like `/mcp-tools/servers/<server-id>/...`.
66///
67/// # Thread Safety
68///
69/// This type is `Send` and `Sync`, making it safe to use across threads.
70///
71/// # Examples
72///
73/// ```
74/// use mcp_execution_files::FileSystem;
75///
76/// let mut vfs = FileSystem::new();
77/// vfs.add_file("/mcp-tools/manifest.json", "{}").unwrap();
78///
79/// assert!(vfs.exists("/mcp-tools/manifest.json"));
80/// assert_eq!(vfs.file_count(), 1);
81/// ```
82#[derive(Debug, Clone)]
83pub struct FileSystem {
84 files: HashMap<FilePath, FileEntry>,
85}
86
87impl FileSystem {
88 /// Creates a new empty virtual filesystem.
89 ///
90 /// # Examples
91 ///
92 /// ```
93 /// use mcp_execution_files::FileSystem;
94 ///
95 /// let vfs = FileSystem::new();
96 /// assert_eq!(vfs.file_count(), 0);
97 /// ```
98 #[must_use]
99 pub fn new() -> Self {
100 Self {
101 files: HashMap::new(),
102 }
103 }
104
105 /// Adds a file to the virtual filesystem.
106 ///
107 /// If a file already exists at the path, it will be replaced.
108 ///
109 /// # Errors
110 ///
111 /// Returns an error if the path is invalid (not absolute, contains '..', etc.).
112 ///
113 /// # Examples
114 ///
115 /// ```
116 /// use mcp_execution_files::FileSystem;
117 ///
118 /// let mut vfs = FileSystem::new();
119 /// vfs.add_file("/mcp-tools/test.ts", "console.log('hello');").unwrap();
120 ///
121 /// assert!(vfs.exists("/mcp-tools/test.ts"));
122 /// # Ok::<(), mcp_execution_files::FilesError>(())
123 /// ```
124 pub fn add_file(&mut self, path: impl AsRef<Path>, content: impl Into<String>) -> Result<()> {
125 let vfs_path = FilePath::new(path)?;
126 let file = FileEntry::new(content);
127 self.files.insert(vfs_path, file);
128 Ok(())
129 }
130
131 /// Reads the content of a file.
132 ///
133 /// # Errors
134 ///
135 /// Returns `FilesError::FileNotFound` if the file does not exist.
136 /// Returns `FilesError::InvalidPath` if the path is invalid.
137 ///
138 /// # Examples
139 ///
140 /// ```
141 /// use mcp_execution_files::FileSystem;
142 ///
143 /// let mut vfs = FileSystem::new();
144 /// vfs.add_file("/test.ts", "export {}").unwrap();
145 ///
146 /// let content = vfs.read_file("/test.ts").unwrap();
147 /// assert_eq!(content, "export {}");
148 /// # Ok::<(), mcp_execution_files::FilesError>(())
149 /// ```
150 pub fn read_file(&self, path: impl AsRef<Path>) -> Result<&str> {
151 let vfs_path = FilePath::new(path)?;
152 self.files
153 .get(&vfs_path)
154 .map(FileEntry::content)
155 .ok_or_else(|| FilesError::FileNotFound {
156 path: vfs_path.as_str().to_string(),
157 })
158 }
159
160 /// Checks if a file exists at the given path.
161 ///
162 /// Returns `false` if the path is invalid.
163 ///
164 /// # Examples
165 ///
166 /// ```
167 /// use mcp_execution_files::FileSystem;
168 ///
169 /// let mut vfs = FileSystem::new();
170 /// vfs.add_file("/exists.ts", "").unwrap();
171 ///
172 /// assert!(vfs.exists("/exists.ts"));
173 /// assert!(!vfs.exists("/missing.ts"));
174 /// ```
175 #[must_use]
176 pub fn exists(&self, path: impl AsRef<Path>) -> bool {
177 FilePath::new(path)
178 .ok()
179 .and_then(|p| self.files.get(&p))
180 .is_some()
181 }
182
183 /// Lists all files and directories in a directory.
184 ///
185 /// Returns an empty vector if the directory is empty or does not exist.
186 ///
187 /// # Errors
188 ///
189 /// Returns `FilesError::InvalidPath` if the path is invalid.
190 /// Returns `FilesError::NotADirectory` if the path points to a file.
191 ///
192 /// # Examples
193 ///
194 /// ```
195 /// use mcp_execution_files::FileSystem;
196 ///
197 /// let mut vfs = FileSystem::new();
198 /// vfs.add_file("/mcp-tools/servers/test1.ts", "").unwrap();
199 /// vfs.add_file("/mcp-tools/servers/test2.ts", "").unwrap();
200 ///
201 /// let entries = vfs.list_dir("/mcp-tools/servers").unwrap();
202 /// assert_eq!(entries.len(), 2);
203 /// # Ok::<(), mcp_execution_files::FilesError>(())
204 /// ```
205 pub fn list_dir(&self, path: impl AsRef<Path>) -> Result<Vec<FilePath>> {
206 let vfs_path = FilePath::new(path)?;
207 let path_str = vfs_path.as_str();
208
209 // Check if the path itself is a file
210 if self.files.contains_key(&vfs_path) {
211 return Err(FilesError::NotADirectory {
212 path: path_str.to_string(),
213 });
214 }
215
216 // Collect all direct children
217 let mut children = Vec::new();
218 let normalized_dir = if path_str.ends_with('/') {
219 path_str.to_string()
220 } else {
221 format!("{path_str}/")
222 };
223
224 for file_path in self.files.keys() {
225 let file_str = file_path.as_str();
226
227 // Check if this file is under the directory
228 if file_str.starts_with(&normalized_dir) {
229 let relative = &file_str[normalized_dir.len()..];
230
231 // Only include direct children (no subdirectories)
232 if !relative.contains('/') && !relative.is_empty() {
233 children.push(file_path.clone());
234 } else if let Some(idx) = relative.find('/') {
235 // This is a subdirectory, add the directory path
236 let subdir = format!("{}{}", normalized_dir, &relative[..idx]);
237 if let Ok(subdir_path) = FilePath::new(subdir)
238 && !children.contains(&subdir_path)
239 {
240 children.push(subdir_path);
241 }
242 }
243 }
244 }
245
246 children.sort_by(|a, b| a.as_str().cmp(b.as_str()));
247 Ok(children)
248 }
249
250 /// Returns the total number of files in the VFS.
251 ///
252 /// # Examples
253 ///
254 /// ```
255 /// use mcp_execution_files::FileSystem;
256 ///
257 /// let mut vfs = FileSystem::new();
258 /// assert_eq!(vfs.file_count(), 0);
259 ///
260 /// vfs.add_file("/test1.ts", "").unwrap();
261 /// vfs.add_file("/test2.ts", "").unwrap();
262 /// assert_eq!(vfs.file_count(), 2);
263 /// ```
264 #[must_use]
265 pub fn file_count(&self) -> usize {
266 self.files.len()
267 }
268
269 /// Returns all file paths in the VFS.
270 ///
271 /// The paths are returned in sorted order.
272 ///
273 /// # Examples
274 ///
275 /// ```
276 /// use mcp_execution_files::FileSystem;
277 ///
278 /// let mut vfs = FileSystem::new();
279 /// vfs.add_file("/a.ts", "").unwrap();
280 /// vfs.add_file("/b.ts", "").unwrap();
281 ///
282 /// let paths = vfs.all_paths();
283 /// assert_eq!(paths.len(), 2);
284 /// ```
285 #[must_use]
286 pub fn all_paths(&self) -> Vec<&FilePath> {
287 let mut paths: Vec<_> = self.files.keys().collect();
288 paths.sort_by(|a, b| a.as_str().cmp(b.as_str()));
289 paths
290 }
291
292 /// Returns an iterator over all files in the VFS.
293 ///
294 /// Each item is a tuple of `(&FilePath, &FileEntry)`.
295 ///
296 /// # Examples
297 ///
298 /// ```
299 /// use mcp_execution_files::FileSystem;
300 ///
301 /// let mut vfs = FileSystem::new();
302 /// vfs.add_file("/a.ts", "content a").unwrap();
303 /// vfs.add_file("/b.ts", "content b").unwrap();
304 ///
305 /// let files: Vec<_> = vfs.files().collect();
306 /// assert_eq!(files.len(), 2);
307 /// ```
308 pub fn files(&self) -> impl Iterator<Item = (&FilePath, &FileEntry)> {
309 self.files.iter()
310 }
311
312 /// Removes all files from the VFS.
313 ///
314 /// # Examples
315 ///
316 /// ```
317 /// use mcp_execution_files::FileSystem;
318 ///
319 /// let mut vfs = FileSystem::new();
320 /// vfs.add_file("/test.ts", "").unwrap();
321 /// assert_eq!(vfs.file_count(), 1);
322 ///
323 /// vfs.clear();
324 /// assert_eq!(vfs.file_count(), 0);
325 /// ```
326 pub fn clear(&mut self) {
327 self.files.clear();
328 }
329
330 /// Exports VFS contents to real filesystem.
331 ///
332 /// This is a high-performance implementation optimized for the progressive
333 /// loading pattern. It pre-creates all directories and writes files sequentially.
334 ///
335 /// # Performance
336 ///
337 /// Target: <50ms for 30 files (GitHub server typical case)
338 ///
339 /// Optimizations:
340 /// - Single pass directory creation
341 /// - Cached canonicalized base path
342 /// - Minimal allocations
343 ///
344 /// # Errors
345 ///
346 /// Returns error if:
347 /// - Base path doesn't exist or isn't a directory
348 /// - Permission denied
349 /// - I/O error during write
350 ///
351 /// # Examples
352 ///
353 /// ```
354 /// use mcp_execution_files::FilesBuilder;
355 /// # use tempfile::TempDir;
356 ///
357 /// # let temp = TempDir::new().unwrap();
358 /// # let base = temp.path();
359 /// let vfs = FilesBuilder::new()
360 /// .add_file("/manifest.json", "{}")
361 /// .build()
362 /// .unwrap();
363 ///
364 /// vfs.export_to_filesystem(base).unwrap();
365 /// assert!(base.join("manifest.json").exists());
366 /// # Ok::<(), Box<dyn std::error::Error>>(())
367 /// ```
368 pub fn export_to_filesystem(&self, base_path: impl AsRef<Path>) -> Result<()> {
369 self.export_to_filesystem_with_options(base_path, &ExportOptions::default())
370 }
371
372 /// Exports VFS contents with custom options.
373 ///
374 /// # Errors
375 ///
376 /// Returns an error if:
377 /// - Base path does not exist
378 /// - Base path cannot be canonicalized
379 /// - I/O operations fail during directory creation or file writing
380 ///
381 /// # Examples
382 ///
383 /// ```
384 /// use mcp_execution_files::{FilesBuilder, ExportOptions};
385 /// # use tempfile::TempDir;
386 ///
387 /// # let temp = TempDir::new().unwrap();
388 /// # let base = temp.path();
389 /// let vfs = FilesBuilder::new()
390 /// .add_file("/test.ts", "export {}")
391 /// .build()
392 /// .unwrap();
393 ///
394 /// let options = ExportOptions::default().with_atomic_writes(false);
395 /// vfs.export_to_filesystem_with_options(base, &options).unwrap();
396 /// # Ok::<(), Box<dyn std::error::Error>>(())
397 /// ```
398 pub fn export_to_filesystem_with_options(
399 &self,
400 base_path: impl AsRef<Path>,
401 options: &ExportOptions,
402 ) -> Result<()> {
403 let base = base_path.as_ref();
404
405 // Validate base path exists
406 if !base.exists() {
407 return Err(FilesError::FileNotFound {
408 path: base.display().to_string(),
409 });
410 }
411
412 // Canonicalize base path once (performance optimization)
413 let canonical_base = base.canonicalize().map_err(|e| FilesError::InvalidPath {
414 path: format!("Failed to canonicalize {}: {}", base.display(), e),
415 })?;
416
417 // Phase 1: Collect all unique directories
418 let dirs = self.collect_directories(&canonical_base);
419
420 // Phase 2: Create all directories in one pass
421 Self::create_directories(&dirs)?;
422
423 // Phase 3: Write all files
424 self.write_files(&canonical_base, options)?;
425
426 Ok(())
427 }
428
429 /// Exports VFS contents using parallel writes (requires 'parallel' feature).
430 ///
431 /// Faster for large numbers of files (>50), but may not preserve write order.
432 ///
433 /// # Errors
434 ///
435 /// Returns error if:
436 /// - Base path doesn't exist or isn't a directory
437 /// - Permission denied during directory creation or file write
438 /// - I/O error during parallel write operations
439 ///
440 /// # Examples
441 ///
442 /// ```
443 /// use mcp_execution_files::FilesBuilder;
444 /// # use tempfile::TempDir;
445 ///
446 /// # let temp = TempDir::new().unwrap();
447 /// # let base = temp.path();
448 /// let vfs = FilesBuilder::new()
449 /// .add_file("/tool1.ts", "export {}")
450 /// .add_file("/tool2.ts", "export {}")
451 /// .build()
452 /// .unwrap();
453 ///
454 /// #[cfg(feature = "parallel")]
455 /// vfs.export_to_filesystem_parallel(base).unwrap();
456 /// # Ok::<(), Box<dyn std::error::Error>>(())
457 /// ```
458 #[cfg(feature = "parallel")]
459 pub fn export_to_filesystem_parallel(&self, base_path: impl AsRef<Path>) -> Result<()> {
460 use rayon::prelude::*;
461
462 let base = base_path.as_ref();
463 let canonical_base = base.canonicalize().map_err(|e| FilesError::InvalidPath {
464 path: format!("Failed to canonicalize {}: {}", base.display(), e),
465 })?;
466
467 // Phase 1: Collect and create directories (must be sequential)
468 let dirs = self.collect_directories(&canonical_base);
469 Self::create_directories(&dirs)?;
470
471 // Phase 2: Write files in parallel
472 let files: Vec<_> = self.files().collect();
473 let options = ExportOptions::default();
474
475 files
476 .par_iter()
477 .try_for_each(|(vfs_path, file)| -> Result<()> {
478 let disk_path = Self::vfs_to_disk_path(vfs_path.as_str(), &canonical_base);
479 write_file_atomic(&disk_path, file.content(), &options)
480 })?;
481
482 Ok(())
483 }
484
485 /// Collects all unique directory paths needed for export.
486 ///
487 /// This is done in a single pass to minimize allocations.
488 fn collect_directories(&self, base: &Path) -> HashSet<PathBuf> {
489 let mut dirs = HashSet::new();
490
491 for (vfs_path, _) in self.files() {
492 let disk_path = Self::vfs_to_disk_path(vfs_path.as_str(), base);
493
494 // Add all parent directories
495 if let Some(parent) = disk_path.parent() {
496 // Insert parent and all ancestors
497 let mut current = parent;
498 while current != base && dirs.insert(current.to_path_buf()) {
499 if let Some(p) = current.parent() {
500 current = p;
501 } else {
502 break;
503 }
504 }
505 }
506 }
507
508 dirs
509 }
510
511 /// Creates all directories in one pass.
512 ///
513 /// Uses `fs::create_dir_all` which is efficient for creating directory trees.
514 fn create_directories(dirs: &HashSet<PathBuf>) -> Result<()> {
515 for dir in dirs {
516 fs::create_dir_all(dir).map_err(|e| FilesError::InvalidPath {
517 path: format!("Failed to create directory {}: {}", dir.display(), e),
518 })?;
519 }
520 Ok(())
521 }
522
523 /// Writes all files to disk.
524 fn write_files(&self, base: &Path, options: &ExportOptions) -> Result<()> {
525 for (vfs_path, file) in self.files() {
526 let disk_path = Self::vfs_to_disk_path(vfs_path.as_str(), base);
527 write_file_atomic(&disk_path, file.content(), options)?;
528 }
529 Ok(())
530 }
531
532 /// Converts VFS path to disk path.
533 ///
534 /// Strips leading '/' and joins with base path.
535 ///
536 /// # Panics
537 ///
538 /// Panics if path contains `..` (path traversal attempt).
539 /// This is defense-in-depth since `FilePath::new()` also validates.
540 fn vfs_to_disk_path(vfs_path: &str, base: &Path) -> PathBuf {
541 // Strip leading '/' from VFS path
542 let relative = vfs_path.strip_prefix('/').unwrap_or(vfs_path);
543
544 // Defense-in-depth: reject path traversal attempts
545 // Primary validation is in FilePath::new(), this is a safety net
546 assert!(
547 !relative.contains(".."),
548 "SECURITY: Path traversal attempt detected in VFS path: {vfs_path}"
549 );
550
551 // Convert forward slashes to platform-specific separators
552 let relative_path = if cfg!(target_os = "windows") {
553 PathBuf::from(relative.replace('/', "\\"))
554 } else {
555 PathBuf::from(relative)
556 };
557
558 base.join(relative_path)
559 }
560}
561
562impl Default for FileSystem {
563 fn default() -> Self {
564 Self::new()
565 }
566}
567
568/// Options for filesystem export operations.
569///
570/// # Examples
571///
572/// ```
573/// use mcp_execution_files::ExportOptions;
574///
575/// let options = ExportOptions::default()
576/// .with_atomic_writes(true)
577/// .with_overwrite(true);
578/// ```
579#[derive(Debug, Clone)]
580pub struct ExportOptions {
581 /// Use atomic writes (write to temp file, then rename)
582 pub atomic: bool,
583 /// Overwrite existing files
584 pub overwrite: bool,
585}
586
587impl ExportOptions {
588 /// Creates new export options with defaults.
589 ///
590 /// Defaults:
591 /// - atomic: true (safer)
592 /// - overwrite: true (common case)
593 #[must_use]
594 pub const fn new() -> Self {
595 Self {
596 atomic: true,
597 overwrite: true,
598 }
599 }
600
601 /// Sets whether to use atomic writes.
602 #[must_use]
603 pub const fn with_atomic_writes(mut self, atomic: bool) -> Self {
604 self.atomic = atomic;
605 self
606 }
607
608 /// Sets whether to overwrite existing files.
609 #[must_use]
610 pub const fn with_overwrite(mut self, overwrite: bool) -> Self {
611 self.overwrite = overwrite;
612 self
613 }
614}
615
616impl Default for ExportOptions {
617 fn default() -> Self {
618 Self::new()
619 }
620}
621
622/// Writes file content to disk atomically.
623///
624/// If atomic mode is enabled, writes to temp file then renames.
625/// Otherwise, writes directly.
626fn write_file_atomic(path: &Path, content: &str, options: &ExportOptions) -> Result<()> {
627 // Check if file exists and we shouldn't overwrite
628 if !options.overwrite && path.exists() {
629 return Ok(());
630 }
631
632 if options.atomic {
633 // Atomic write: temp file + rename
634 let temp_path = path.with_extension("tmp");
635
636 // Write to temp file
637 let mut file = fs::File::create(&temp_path).map_err(|e| FilesError::InvalidPath {
638 path: format!("Failed to create temp file {}: {}", temp_path.display(), e),
639 })?;
640
641 file.write_all(content.as_bytes())
642 .map_err(|e| FilesError::InvalidPath {
643 path: format!("Failed to write to {}: {}", temp_path.display(), e),
644 })?;
645
646 file.sync_all().map_err(|e| FilesError::InvalidPath {
647 path: format!("Failed to sync {}: {}", temp_path.display(), e),
648 })?;
649
650 // Rename to final location
651 fs::rename(&temp_path, path).map_err(|e| FilesError::InvalidPath {
652 path: format!(
653 "Failed to rename {} to {}: {}",
654 temp_path.display(),
655 path.display(),
656 e
657 ),
658 })?;
659 } else {
660 // Direct write (faster, but not atomic)
661 fs::write(path, content).map_err(|e| FilesError::InvalidPath {
662 path: format!("Failed to write {}: {}", path.display(), e),
663 })?;
664 }
665
666 Ok(())
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672 use crate::FilesBuilder;
673 use tempfile::TempDir;
674
675 // FileSystem core tests
676 #[test]
677 fn test_vfs_new() {
678 let vfs = FileSystem::new();
679 assert_eq!(vfs.file_count(), 0);
680 }
681
682 #[test]
683 fn test_vfs_default() {
684 let vfs = FileSystem::default();
685 assert_eq!(vfs.file_count(), 0);
686 }
687
688 #[test]
689 fn test_add_file() {
690 let mut vfs = FileSystem::new();
691 vfs.add_file("/test.ts", "content").unwrap();
692 assert_eq!(vfs.file_count(), 1);
693 }
694
695 #[test]
696 fn test_add_file_invalid_path() {
697 let mut vfs = FileSystem::new();
698 let result = vfs.add_file("relative/path", "content");
699 assert!(result.is_err());
700 }
701
702 #[test]
703 fn test_read_file() {
704 let mut vfs = FileSystem::new();
705 vfs.add_file("/test.ts", "hello world").unwrap();
706
707 let content = vfs.read_file("/test.ts").unwrap();
708 assert_eq!(content, "hello world");
709 }
710
711 #[test]
712 fn test_read_file_not_found() {
713 let vfs = FileSystem::new();
714 let result = vfs.read_file("/missing.ts");
715 assert!(result.is_err());
716 assert!(result.unwrap_err().is_not_found());
717 }
718
719 #[test]
720 fn test_exists() {
721 let mut vfs = FileSystem::new();
722 vfs.add_file("/exists.ts", "").unwrap();
723
724 assert!(vfs.exists("/exists.ts"));
725 assert!(!vfs.exists("/missing.ts"));
726 }
727
728 #[test]
729 fn test_exists_invalid_path() {
730 let vfs = FileSystem::new();
731 assert!(!vfs.exists("relative/path"));
732 }
733
734 #[test]
735 fn test_list_dir() {
736 let mut vfs = FileSystem::new();
737 vfs.add_file("/mcp-tools/servers/test1.ts", "").unwrap();
738 vfs.add_file("/mcp-tools/servers/test2.ts", "").unwrap();
739
740 let entries = vfs.list_dir("/mcp-tools/servers").unwrap();
741 assert_eq!(entries.len(), 2);
742 }
743
744 #[test]
745 fn test_list_dir_empty() {
746 let vfs = FileSystem::new();
747 let entries = vfs.list_dir("/empty").unwrap();
748 assert_eq!(entries.len(), 0);
749 }
750
751 #[test]
752 fn test_list_dir_not_a_directory() {
753 let mut vfs = FileSystem::new();
754 vfs.add_file("/file.ts", "").unwrap();
755
756 let result = vfs.list_dir("/file.ts");
757 assert!(result.is_err());
758 assert!(result.unwrap_err().is_not_directory());
759 }
760
761 #[test]
762 fn test_list_dir_subdirectories() {
763 let mut vfs = FileSystem::new();
764 vfs.add_file("/mcp-tools/servers/test/file1.ts", "")
765 .unwrap();
766 vfs.add_file("/mcp-tools/servers/test/file2.ts", "")
767 .unwrap();
768 vfs.add_file("/mcp-tools/servers/other.ts", "").unwrap();
769
770 let entries = vfs.list_dir("/mcp-tools/servers").unwrap();
771 // Should include 'test' directory and 'other.ts' file
772 assert_eq!(entries.len(), 2);
773 }
774
775 #[test]
776 fn test_file_count() {
777 let mut vfs = FileSystem::new();
778 assert_eq!(vfs.file_count(), 0);
779
780 vfs.add_file("/test1.ts", "").unwrap();
781 assert_eq!(vfs.file_count(), 1);
782
783 vfs.add_file("/test2.ts", "").unwrap();
784 assert_eq!(vfs.file_count(), 2);
785 }
786
787 #[test]
788 fn test_all_paths() {
789 let mut vfs = FileSystem::new();
790 vfs.add_file("/b.ts", "").unwrap();
791 vfs.add_file("/a.ts", "").unwrap();
792
793 let paths = vfs.all_paths();
794 assert_eq!(paths.len(), 2);
795 // Should be sorted
796 assert_eq!(paths[0].as_str(), "/a.ts");
797 assert_eq!(paths[1].as_str(), "/b.ts");
798 }
799
800 #[test]
801 fn test_clear() {
802 let mut vfs = FileSystem::new();
803 vfs.add_file("/test1.ts", "").unwrap();
804 vfs.add_file("/test2.ts", "").unwrap();
805 assert_eq!(vfs.file_count(), 2);
806
807 vfs.clear();
808 assert_eq!(vfs.file_count(), 0);
809 }
810
811 #[test]
812 fn test_replace_file() {
813 let mut vfs = FileSystem::new();
814 vfs.add_file("/test.ts", "original").unwrap();
815 assert_eq!(vfs.read_file("/test.ts").unwrap(), "original");
816
817 vfs.add_file("/test.ts", "updated").unwrap();
818 assert_eq!(vfs.read_file("/test.ts").unwrap(), "updated");
819 assert_eq!(vfs.file_count(), 1);
820 }
821
822 #[test]
823 fn test_vfs_is_send_sync() {
824 fn assert_send<T: Send>() {}
825 fn assert_sync<T: Sync>() {}
826
827 assert_send::<FileSystem>();
828 assert_sync::<FileSystem>();
829 }
830
831 // Export tests
832 #[test]
833 fn test_export_single_file() {
834 let temp = TempDir::new().unwrap();
835 let vfs = FilesBuilder::new()
836 .add_file("/test.ts", "export const VERSION = '1.0';")
837 .build()
838 .unwrap();
839
840 vfs.export_to_filesystem(temp.path()).unwrap();
841
842 let exported = temp.path().join("test.ts");
843 assert!(exported.exists());
844 assert_eq!(
845 fs::read_to_string(exported).unwrap(),
846 "export const VERSION = '1.0';"
847 );
848 }
849
850 #[test]
851 fn test_export_nested_files() {
852 let temp = TempDir::new().unwrap();
853 let vfs = FilesBuilder::new()
854 .add_file("/tools/create.ts", "export function create() {}")
855 .add_file("/tools/update.ts", "export function update() {}")
856 .add_file("/manifest.json", "{}")
857 .build()
858 .unwrap();
859
860 vfs.export_to_filesystem(temp.path()).unwrap();
861
862 assert!(temp.path().join("tools/create.ts").exists());
863 assert!(temp.path().join("tools/update.ts").exists());
864 assert!(temp.path().join("manifest.json").exists());
865 }
866
867 #[test]
868 fn test_export_overwrite() {
869 let temp = TempDir::new().unwrap();
870 let path = temp.path().join("test.ts");
871
872 // Write initial file
873 fs::write(&path, "old content").unwrap();
874
875 let vfs = FilesBuilder::new()
876 .add_file("/test.ts", "new content")
877 .build()
878 .unwrap();
879
880 vfs.export_to_filesystem(temp.path()).unwrap();
881
882 assert_eq!(fs::read_to_string(path).unwrap(), "new content");
883 }
884
885 #[test]
886 fn test_export_no_overwrite() {
887 let temp = TempDir::new().unwrap();
888 let path = temp.path().join("test.ts");
889
890 // Write initial file
891 fs::write(&path, "old content").unwrap();
892
893 let vfs = FilesBuilder::new()
894 .add_file("/test.ts", "new content")
895 .build()
896 .unwrap();
897
898 let options = ExportOptions::default().with_overwrite(false);
899 vfs.export_to_filesystem_with_options(temp.path(), &options)
900 .unwrap();
901
902 // Should not overwrite
903 assert_eq!(fs::read_to_string(path).unwrap(), "old content");
904 }
905
906 #[test]
907 fn test_export_atomic_writes() {
908 let temp = TempDir::new().unwrap();
909 let vfs = FilesBuilder::new()
910 .add_file("/test.ts", "atomic content")
911 .build()
912 .unwrap();
913
914 let options = ExportOptions::default().with_atomic_writes(true);
915 vfs.export_to_filesystem_with_options(temp.path(), &options)
916 .unwrap();
917
918 let path = temp.path().join("test.ts");
919 assert!(path.exists());
920 assert_eq!(fs::read_to_string(path).unwrap(), "atomic content");
921
922 // Temp file should be cleaned up
923 let temp_path = temp.path().join("test.tmp");
924 assert!(!temp_path.exists());
925 }
926
927 #[test]
928 fn test_export_non_atomic_writes() {
929 let temp = TempDir::new().unwrap();
930 let vfs = FilesBuilder::new()
931 .add_file("/test.ts", "direct content")
932 .build()
933 .unwrap();
934
935 let options = ExportOptions::default().with_atomic_writes(false);
936 vfs.export_to_filesystem_with_options(temp.path(), &options)
937 .unwrap();
938
939 let path = temp.path().join("test.ts");
940 assert_eq!(fs::read_to_string(path).unwrap(), "direct content");
941 }
942
943 #[test]
944 fn test_export_invalid_base_path() {
945 let vfs = FilesBuilder::new()
946 .add_file("/test.ts", "")
947 .build()
948 .unwrap();
949
950 let result = vfs.export_to_filesystem("/nonexistent/path/that/does/not/exist");
951 assert!(result.is_err());
952 }
953
954 #[test]
955 fn test_export_many_files() {
956 let temp = TempDir::new().unwrap();
957 let mut builder = FilesBuilder::new();
958
959 // Add 30 files (GitHub server typical case)
960 for i in 0..30 {
961 builder = builder.add_file(
962 format!("/tools/tool{i}.ts"),
963 format!("export function tool{i}() {{}}"),
964 );
965 }
966
967 let vfs = builder.build().unwrap();
968 vfs.export_to_filesystem(temp.path()).unwrap();
969
970 // Verify all files exist
971 for i in 0..30 {
972 assert!(temp.path().join(format!("tools/tool{i}.ts")).exists());
973 }
974 }
975
976 #[test]
977 fn test_export_deep_nesting() {
978 let temp = TempDir::new().unwrap();
979 let vfs = FilesBuilder::new()
980 .add_file("/a/b/c/d/e/deep.ts", "export {}")
981 .build()
982 .unwrap();
983
984 vfs.export_to_filesystem(temp.path()).unwrap();
985
986 assert!(temp.path().join("a/b/c/d/e/deep.ts").exists());
987 }
988
989 #[test]
990 #[cfg(feature = "parallel")]
991 fn test_export_parallel() {
992 let temp = TempDir::new().unwrap();
993 let mut builder = FilesBuilder::new();
994
995 for i in 0..100 {
996 builder = builder.add_file(format!("/file{i}.ts"), format!("export const N = {i};"));
997 }
998
999 let vfs = builder.build().unwrap();
1000 vfs.export_to_filesystem_parallel(temp.path()).unwrap();
1001
1002 // Verify all files exist
1003 for i in 0..100 {
1004 let path = temp.path().join(format!("file{i}.ts"));
1005 assert!(path.exists());
1006 }
1007 }
1008
1009 #[test]
1010 fn test_export_options_default() {
1011 let options = ExportOptions::default();
1012 assert!(options.atomic);
1013 assert!(options.overwrite);
1014 }
1015
1016 #[test]
1017 fn test_export_options_builder() {
1018 let options = ExportOptions::new()
1019 .with_atomic_writes(false)
1020 .with_overwrite(false);
1021
1022 assert!(!options.atomic);
1023 assert!(!options.overwrite);
1024 }
1025}