Skip to main content

mixtape_tools/filesystem/
create_directory.rs

1use crate::filesystem::validate_path;
2use crate::prelude::*;
3use std::path::PathBuf;
4use tokio::fs;
5
6/// Input for creating a directory
7#[derive(Debug, Deserialize, JsonSchema)]
8pub struct CreateDirectoryInput {
9    /// Path to the directory to create (relative to base path)
10    pub path: PathBuf,
11}
12
13/// Tool for creating directories
14pub struct CreateDirectoryTool {
15    base_path: PathBuf,
16}
17
18impl Default for CreateDirectoryTool {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl CreateDirectoryTool {
25    /// Creates a new tool using the current working directory as the base path.
26    ///
27    /// Equivalent to `Default::default()`.
28    ///
29    /// # Panics
30    ///
31    /// Panics if the current working directory cannot be determined.
32    /// Use [`try_new`](Self::try_new) or [`with_base_path`](Self::with_base_path) instead.
33    pub fn new() -> Self {
34        Self {
35            base_path: std::env::current_dir().expect("Failed to get current working directory"),
36        }
37    }
38
39    /// Creates a new tool using the current working directory as the base path.
40    ///
41    /// Returns an error if the current working directory cannot be determined.
42    pub fn try_new() -> std::io::Result<Self> {
43        Ok(Self {
44            base_path: std::env::current_dir()?,
45        })
46    }
47
48    /// Creates a tool with a custom base directory.
49    ///
50    /// All file operations will be constrained to this directory.
51    pub fn with_base_path(base_path: PathBuf) -> Self {
52        Self { base_path }
53    }
54}
55
56impl Tool for CreateDirectoryTool {
57    type Input = CreateDirectoryInput;
58
59    fn name(&self) -> &str {
60        "create_directory"
61    }
62
63    fn description(&self) -> &str {
64        "Create a new directory. Parent directories will be created automatically if they don't exist."
65    }
66
67    async fn execute(&self, input: Self::Input) -> std::result::Result<ToolResult, ToolError> {
68        // Validate path is within base directory before creation
69        let validated_path = validate_path(&self.base_path, &input.path)?;
70
71        // Create the directory (and any missing parents)
72        fs::create_dir_all(&validated_path)
73            .await
74            .map_err(|e| ToolError::from(format!("Failed to create directory: {}", e)))?;
75
76        Ok(format!("Successfully created directory: {}", input.path.display()).into())
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use tempfile::TempDir;
84
85    #[test]
86    fn test_tool_metadata() {
87        let tool: CreateDirectoryTool = Default::default();
88        assert_eq!(tool.name(), "create_directory");
89        assert!(!tool.description().is_empty());
90
91        let tool2 = CreateDirectoryTool::new();
92        assert_eq!(tool2.name(), "create_directory");
93    }
94
95    #[test]
96    fn test_try_new() {
97        let tool = CreateDirectoryTool::try_new();
98        assert!(tool.is_ok());
99    }
100
101    #[test]
102    fn test_format_methods() {
103        let tool = CreateDirectoryTool::new();
104        let params = serde_json::json!({"path": "new_dir"});
105
106        assert!(!tool.format_input_plain(&params).is_empty());
107        assert!(!tool.format_input_ansi(&params).is_empty());
108        assert!(!tool.format_input_markdown(&params).is_empty());
109
110        let result = ToolResult::from("Created directory");
111        assert!(!tool.format_output_plain(&result).is_empty());
112        assert!(!tool.format_output_ansi(&result).is_empty());
113        assert!(!tool.format_output_markdown(&result).is_empty());
114    }
115
116    #[tokio::test]
117    async fn test_create_directory_rejects_absolute_path_without_side_effects() {
118        // SECURITY TEST: Attempting to create a directory outside base_path
119        // using an absolute path should fail WITHOUT creating the directory first.
120        let temp_dir = TempDir::new().unwrap();
121        let evil_target = TempDir::new().unwrap();
122        let evil_dir = evil_target.path().join("should_not_exist");
123
124        let tool = CreateDirectoryTool::with_base_path(temp_dir.path().to_path_buf());
125
126        // Try to create a directory using an absolute path outside base_path
127        let input = CreateDirectoryInput {
128            path: evil_dir.clone(),
129        };
130
131        let result = tool.execute(input).await;
132
133        // The operation should fail
134        assert!(
135            result.is_err(),
136            "Absolute path outside base should be rejected"
137        );
138
139        // CRITICAL: The directory should NOT have been created
140        assert!(
141            !evil_dir.exists(),
142            "Security bug: directory was created before validation! Path: {:?}",
143            evil_dir
144        );
145    }
146
147    #[tokio::test]
148    async fn test_create_directory() {
149        let temp_dir = TempDir::new().unwrap();
150        let tool = CreateDirectoryTool::with_base_path(temp_dir.path().to_path_buf());
151
152        let input = CreateDirectoryInput {
153            path: PathBuf::from("test_dir"),
154        };
155
156        let result = tool.execute(input).await.unwrap();
157        assert!(result.as_text().contains("Successfully created"));
158        assert!(temp_dir.path().join("test_dir").exists());
159    }
160
161    #[tokio::test]
162    async fn test_create_nested_directory() {
163        let temp_dir = TempDir::new().unwrap();
164        let tool = CreateDirectoryTool::with_base_path(temp_dir.path().to_path_buf());
165
166        let input = CreateDirectoryInput {
167            path: PathBuf::from("parent/child/grandchild"),
168        };
169
170        let result = tool.execute(input).await.unwrap();
171        assert!(result.as_text().contains("Successfully created"));
172        assert!(temp_dir.path().join("parent/child/grandchild").exists());
173    }
174}