Skip to main content

DevelopmentNodeEnvironment_MicrosoftVSCodeDependency_22NodeVersion_Bundle_Clean_Debug_ElectronProfile_EsbuildCompiler_Mountain/IPC/WindServiceHandlers/Utilities/
PathExtraction.rs

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