Skip to main content

Mountain/Track/Effect/CreateEffectForRequest/
FileSystem.rs

1#![allow(non_snake_case, unused_variables, dead_code, unused_imports)]
2
3//! # FileSystem Effect (CreateEffectForRequest)
4//!
5//! Effect constructors for the `FileSystem.*` RPC family. Each handler
6//! delegates to the `FileSystemReader` or `FileSystemWriter` provider trait on
7//! `MountainEnvironment`. All methods accept `file://` URIs from Cocoon and
8//! strip the scheme before passing a native `PathBuf` to the provider.
9//!
10//! ## Methods handled
11//!
12//! | Method | Provider | Description |
13//! |---|---|---|
14//! | `FileSystem.ReadFile` | `FileSystemReader` | Read raw bytes from a file |
15//! | `FileSystem.WriteFile` | `FileSystemWriter` | Write bytes to a file |
16//! | `FileSystem.ReadDirectory` | `FileSystemReader` | List directory entries |
17//! | `FileSystem.Stat` | `FileSystemReader` | Get file metadata |
18//! | `FileSystem.CreateDirectory` | `FileSystemWriter` | Create a directory (optionally recursive) |
19//! | `FileSystem.Delete` | `FileSystemWriter` | Delete a file or directory |
20//! | `FileSystem.Rename` | `FileSystemWriter` | Rename/move a file or directory |
21//! | `FileSystem.Copy` | `FileSystemWriter` | Copy a file or directory tree |
22//!
23//! ## VS Code reference
24//!
25//! `vs/platform/files/common/fileService.ts`,
26//! `vs/base/parts/ipc/common/ipc.net.ts`
27
28use std::{future::Future, pin::Pin, sync::Arc};
29
30use base64::{Engine as _, engine::general_purpose::STANDARD};
31use CommonLibrary::{
32	Environment::Requires::Requires,
33	FileSystem::{FileSystemReader::FileSystemReader, FileSystemWriter::FileSystemWriter},
34};
35use serde_json::{Value, json};
36use tauri::Runtime;
37
38use crate::{RunTime::ApplicationRunTime::ApplicationRunTime, Track::Effect::MappedEffectType::MappedEffect};
39
40/// Strip a leading `file://` (or `file:///`) scheme from the incoming path.
41/// Cocoon sends full URIs like `file:///<home>/.land/extensions/...`
42/// through `FileSystem.ReadFile`/`WriteFile`/`ReadDirectory`; `PathBuf` from
43/// such a string treats the scheme literally and every read 404s. Without
44/// this the redhat.java activation (and any other extension that uses the
45/// gRPC fs.readFile path for its own package.json) fails with "Resource not
46/// found: file:///...".
47fn StripFileUriScheme(Input:&str) -> &str {
48	if let Some(Rest) = Input.strip_prefix("file://") {
49		// `file:///Users/...` - the third slash is part of the path, keep it.
50		if Rest.starts_with('/') {
51			return Rest;
52		}
53
54		// `file://localhost/Users/...` - rarely used, but normalise by
55		// stripping host-up-to-first-slash. Fall through on failure.
56		if let Some(Idx) = Rest.find('/') {
57			return &Rest[Idx..];
58		}
59	}
60
61	Input
62}
63
64pub fn CreateEffect<R:Runtime>(MethodName:&str, Parameters:Value) -> Option<Result<MappedEffect, String>> {
65	match MethodName {
66		"FileSystem.ReadFile" => {
67			let effect =
68				move |run_time:Arc<ApplicationRunTime>| -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> {
69					Box::pin(async move {
70						let path_str = Parameters.get(0).and_then(Value::as_str).unwrap_or("");
71						// Empty-path guard: extensions occasionally
72						// pass `""` to `vscode.workspace.fs.readFile`
73						// when probing optional config files. Stock VS
74						// Code's FileSystemProvider would return
75						// `FileNotFound`; replicating that contract
76						// here avoids a panic in `PathBuf::from("")`-
77						// rooted FS calls (which can confuse Mountain's
78						// path-security guard into emitting a "path
79						// outside workspace" rejection that trips the
80						// breaker cascade).
81						if path_str.is_empty() {
82							return Err("FileSystem.ReadFile: empty path (resource not found)".to_string());
83						}
84						if path_str.starts_with("vscode://schemas-associations/") {
85							let payload = serde_json::to_vec(&json!({ "schemas": [] }))
86								.unwrap_or_else(|_| b"{\"schemas\":[]}".to_vec());
87							return Ok(json!(payload));
88						}
89						let fs_reader:Arc<dyn FileSystemReader> = run_time.Environment.Require();
90						let path = std::path::PathBuf::from(StripFileUriScheme(path_str));
91						fs_reader
92							.ReadFile(&path)
93							.await
94							.map(|bytes| json!(bytes))
95							.map_err(|e| e.to_string())
96					})
97				};
98
99			Some(Ok(Box::new(effect)))
100		},
101
102		"FileSystem.WriteFile" => {
103			let effect =
104				move |run_time:Arc<ApplicationRunTime>| -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> {
105					Box::pin(async move {
106						let fs_writer:Arc<dyn FileSystemWriter> = run_time.Environment.Require();
107						let path_str = Parameters.get(0).and_then(Value::as_str).unwrap_or("");
108						if path_str.is_empty() {
109							return Err("FileSystem.WriteFile: empty path (resource not found)".to_string());
110						}
111						let path = std::path::PathBuf::from(StripFileUriScheme(path_str));
112						let content = Parameters.get(1).cloned();
113						let content_bytes = match content {
114							Some(Value::Array(arr)) => {
115								arr.into_iter().filter_map(|v| v.as_u64().map(|n| n as u8)).collect()
116							},
117							Some(Value::String(s)) => STANDARD.decode(&s).unwrap_or_default(),
118							_ => vec![],
119						};
120						fs_writer
121							.WriteFile(&path, content_bytes, true, true)
122							.await
123							.map(|_| json!(null))
124							.map_err(|e| e.to_string())
125					})
126				};
127
128			Some(Ok(Box::new(effect)))
129		},
130
131		"FileSystem.ReadDirectory" => {
132			let effect =
133				move |run_time:Arc<ApplicationRunTime>| -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> {
134					Box::pin(async move {
135						let path_str = Parameters.get(0).and_then(Value::as_str).unwrap_or("");
136						// Empty-path guard: same contract as ReadFile and
137						// Stat. An empty string from an extension probe
138						// must return "resource not found" so the
139						// LooksLike404 classifier in
140						// MountainVinegRPCService downgrades the log level
141						// and uses error code -32004 instead of tripping
142						// the circuit breaker with a -32000.
143						if path_str.is_empty() {
144							return Err("FileSystem.ReadDirectory: empty path (resource not found)".to_string());
145						}
146						let fs_reader:Arc<dyn FileSystemReader> = run_time.Environment.Require();
147						let path = std::path::PathBuf::from(StripFileUriScheme(path_str));
148						fs_reader
149							.ReadDirectory(&path)
150							.await
151							.map(|entries| json!(entries))
152							.map_err(|e| e.to_string())
153					})
154				};
155
156			Some(Ok(Box::new(effect)))
157		},
158
159		"FileSystem.Stat" => {
160			let effect =
161				move |run_time:Arc<ApplicationRunTime>| -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> {
162					Box::pin(async move {
163						let fs_reader:Arc<dyn FileSystemReader> = run_time.Environment.Require();
164						let path_str = Parameters.get(0).and_then(Value::as_str).unwrap_or("");
165						// Empty-path guard: same rationale as
166						// `FileSystem.ReadFile` above. Returning
167						// `not found` matches VS Code's
168						// `FileSystemProvider.stat()` contract for
169						// probes of paths the extension hasn't
170						// validated upstream.
171						if path_str.is_empty() {
172							return Err("FileSystem.Stat: empty path (resource not found)".to_string());
173						}
174						let path = std::path::PathBuf::from(StripFileUriScheme(path_str));
175						fs_reader
176							.StatFile(&path)
177							.await
178							.map(|stat| json!(stat))
179							.map_err(|e| e.to_string())
180					})
181				};
182
183			Some(Ok(Box::new(effect)))
184		},
185
186		"FileSystem.CreateDirectory" => {
187			let effect =
188				move |run_time:Arc<ApplicationRunTime>| -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> {
189					Box::pin(async move {
190						let fs_writer:Arc<dyn FileSystemWriter> = run_time.Environment.Require();
191						let path_str = Parameters.get(0).and_then(Value::as_str).unwrap_or("");
192						let path = std::path::PathBuf::from(StripFileUriScheme(path_str));
193						fs_writer
194							.CreateDirectory(&path, true)
195							.await
196							.map(|_| json!(null))
197							.map_err(|e| e.to_string())
198					})
199				};
200
201			Some(Ok(Box::new(effect)))
202		},
203
204		"FileSystem.Delete" => {
205			let effect =
206				move |run_time:Arc<ApplicationRunTime>| -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> {
207					Box::pin(async move {
208						let fs_writer:Arc<dyn FileSystemWriter> = run_time.Environment.Require();
209						let path_str = Parameters.get(0).and_then(Value::as_str).unwrap_or("");
210						let path = std::path::PathBuf::from(StripFileUriScheme(path_str));
211						let recursive = Parameters.get(1).and_then(Value::as_bool).unwrap_or(false);
212						fs_writer
213							.Delete(&path, recursive, false)
214							.await
215							.map(|_| json!(null))
216							.map_err(|e| e.to_string())
217					})
218				};
219
220			Some(Ok(Box::new(effect)))
221		},
222
223		"FileSystem.Rename" => {
224			let effect =
225				move |run_time:Arc<ApplicationRunTime>| -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> {
226					Box::pin(async move {
227						let fs_writer:Arc<dyn FileSystemWriter> = run_time.Environment.Require();
228						let source = Parameters.get(0).and_then(Value::as_str).unwrap_or("");
229						let target = Parameters.get(1).and_then(Value::as_str).unwrap_or("");
230						fs_writer
231							.Rename(
232								&std::path::PathBuf::from(StripFileUriScheme(source)),
233								&std::path::PathBuf::from(StripFileUriScheme(target)),
234								true,
235							)
236							.await
237							.map(|_| json!(null))
238							.map_err(|e| e.to_string())
239					})
240				};
241
242			Some(Ok(Box::new(effect)))
243		},
244
245		"FileSystem.Copy" => {
246			let effect =
247				move |run_time:Arc<ApplicationRunTime>| -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> {
248					Box::pin(async move {
249						let fs_writer:Arc<dyn FileSystemWriter> = run_time.Environment.Require();
250						let source = Parameters.get(0).and_then(Value::as_str).unwrap_or("");
251						let target = Parameters.get(1).and_then(Value::as_str).unwrap_or("");
252						fs_writer
253							.Copy(
254								&std::path::PathBuf::from(StripFileUriScheme(source)),
255								&std::path::PathBuf::from(StripFileUriScheme(target)),
256								true,
257							)
258							.await
259							.map(|_| json!(null))
260							.map_err(|e| e.to_string())
261					})
262				};
263
264			Some(Ok(Box::new(effect)))
265		},
266
267		_ => None,
268	}
269}