Skip to main content

Mountain/IPC/WindServiceHandlers/Utilities/
PathExtraction.rs

1#![allow(non_snake_case, unused_variables, dead_code, unused_imports)]
2
3//! Converts VS Code `Uri`-shaped arguments to platform-native paths.
4//! Co-locates percent-decoding, userdata remapping, and `/Static/Application`
5//! rewriting because each is a private helper of `extract_path_from_arg`.
6//! Percent-decoding is also re-exported for callers outside the VFS path
7//! (configuration loaders, etc.).
8
9use serde_json::Value;
10
11use super::{ApplicationRoot::get_static_application_root, UserdataDir::get_userdata_base_dir};
12use crate::dev_log;
13
14/// Extract a filesystem path from a VS Code argument.
15/// VS Code sends URI objects `{ scheme: "file", path: "/C:/foo", fsPath:
16/// "C:\\foo" }` but Mountain handlers expect platform-native path strings.
17///
18/// Windows URI paths have a leading slash: `/C:/Users/...` → strip it.
19/// Unix paths start with `/` normally.
20pub fn extract_path_from_arg(Arg:&Value) -> Result<String, String> {
21	if let Some(Path) = Arg.as_str() {
22		return Ok(normalize_uri_path(Path));
23	}
24
25	if let Some(Object) = Arg.as_object() {
26		if let Some(FsPath) = Object.get("fsPath").and_then(|V| V.as_str()) {
27			if !FsPath.is_empty() {
28				return Ok(FsPath.to_string());
29			}
30		}
31
32		if let Some(Path) = Object.get("path").and_then(|V| V.as_str()) {
33			if !Path.is_empty() {
34				return Ok(normalize_uri_path(Path));
35			}
36		}
37
38		if let Some(External) = Object.get("external").and_then(|V| V.as_str()) {
39			if External.starts_with("file://") {
40				let Stripped = External.trim_start_matches("file://");
41
42				return Ok(normalize_uri_path(Stripped));
43			}
44		}
45	}
46
47	Err("File path must be a string or URI object with path/fsPath field".to_string())
48}
49
50fn normalize_uri_path(Path:&str) -> String {
51	let Decoded = percent_decode(Path);
52
53	let Resolved = resolve_userdata_path(&Decoded);
54
55	let Resolved = resolve_static_application_path(&Resolved);
56
57	#[cfg(target_os = "windows")]
58	{
59		let Trimmed = if Resolved.len() >= 3 && Resolved.starts_with('/') && Resolved.as_bytes().get(2) == Some(&b':') {
60			Resolved[1..].to_string()
61		} else {
62			Resolved
63		};
64
65		Trimmed.replace('/', "\\")
66	}
67
68	#[cfg(not(target_os = "windows"))]
69	{
70		Resolved
71	}
72}
73
74fn resolve_userdata_path(Path:&str) -> String {
75	if !Path.starts_with("/User/") && Path != "/User" {
76		return Path.to_string();
77	}
78
79	let UserDataBase = get_userdata_base_dir();
80
81	let Resolved = format!("{}{}", UserDataBase, Path);
82
83	dev_log!("vfs", "resolve_userdata: {} -> {}", Path, Resolved);
84
85	Resolved
86}
87
88/// Map paths starting with /Static/Application/ to the real Sky Target
89/// directory. Also accepts the leading-slash-less form - the WASM loader
90/// (`vscode-oniguruma` → `onig.wasm`) resolves asset URLs relative to the
91/// current document, which strips the leading slash before the path
92/// reaches `file:read`. Without this branch, `tokio::fs::read` would be
93/// called with a relative path and fail with ENOENT, breaking TextMate
94/// syntax highlighting.
95fn resolve_static_application_path(Path:&str) -> String {
96	let Normalized = if Path.starts_with("/Static/Application/") || Path == "/Static/Application" {
97		Path.to_string()
98	} else if Path.starts_with("Static/Application/") || Path == "Static/Application" {
99		format!("/{}", Path)
100	} else {
101		return Path.to_string();
102	};
103
104	if let Some(Root) = get_static_application_root() {
105		let Relative = Normalized.strip_prefix("/Static/Application").unwrap_or("");
106
107		let Resolved = format!("{}/Static/Application{}", Root, Relative);
108
109		dev_log!("vfs", "resolve_static: {} -> {}", Path, Resolved);
110
111		Resolved
112	} else {
113		Path.to_string()
114	}
115}
116
117/// Decode percent-encoded characters in URI paths.
118/// Handles: %20 (space), %23 (#), %25 (%), %5B ([), %5D (]), etc.
119pub fn percent_decode(Input:&str) -> String {
120	let mut Result = String::with_capacity(Input.len());
121
122	let Bytes = Input.as_bytes();
123
124	let mut I = 0;
125
126	while I < Bytes.len() {
127		if Bytes[I] == b'%' && I + 2 < Bytes.len() {
128			let High = hex_digit(Bytes[I + 1]);
129
130			let Low = hex_digit(Bytes[I + 2]);
131
132			if let (Some(H), Some(L)) = (High, Low) {
133				Result.push((H * 16 + L) as char);
134
135				I += 3;
136
137				continue;
138			}
139		}
140
141		Result.push(Bytes[I] as char);
142
143		I += 1;
144	}
145
146	Result
147}
148
149pub fn hex_digit(Byte:u8) -> Option<u8> {
150	match Byte {
151		b'0'..=b'9' => Some(Byte - b'0'),
152
153		b'a'..=b'f' => Some(Byte - b'a' + 10),
154
155		b'A'..=b'F' => Some(Byte - b'A' + 10),
156
157		_ => None,
158	}
159}