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}