mixtape_tools/filesystem/
create_directory.rs1use crate::filesystem::validate_path;
2use crate::prelude::*;
3use std::path::PathBuf;
4use tokio::fs;
5
6#[derive(Debug, Deserialize, JsonSchema)]
8pub struct CreateDirectoryInput {
9 pub path: PathBuf,
11}
12
13pub 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 pub fn new() -> Self {
34 Self {
35 base_path: std::env::current_dir().expect("Failed to get current working directory"),
36 }
37 }
38
39 pub fn try_new() -> std::io::Result<Self> {
43 Ok(Self {
44 base_path: std::env::current_dir()?,
45 })
46 }
47
48 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 let validated_path = validate_path(&self.base_path, &input.path)?;
70
71 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(¶ms).is_empty());
107 assert!(!tool.format_input_ansi(¶ms).is_empty());
108 assert!(!tool.format_input_markdown(¶ms).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 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 let input = CreateDirectoryInput {
128 path: evil_dir.clone(),
129 };
130
131 let result = tool.execute(input).await;
132
133 assert!(
135 result.is_err(),
136 "Absolute path outside base should be rejected"
137 );
138
139 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}