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	if let Some(Object) = Arg.as_object() {
25		if let Some(FsPath) = Object.get("fsPath").and_then(|V| V.as_str()) {
26			if !FsPath.is_empty() {
27				return Ok(FsPath.to_string());
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		if let Some(External) = Object.get("external").and_then(|V| V.as_str()) {
36			if External.starts_with("file://") {
37				let Stripped = External.trim_start_matches("file://");
38				return Ok(normalize_uri_path(Stripped));
39			}
40		}
41	}
42	Err("File path must be a string or URI object with path/fsPath field".to_string())
43}
44
45fn normalize_uri_path(Path:&str) -> String {
46	let Decoded = percent_decode(Path);
47	let Resolved = resolve_userdata_path(&Decoded);
48	let Resolved = resolve_static_application_path(&Resolved);
49
50	#[cfg(target_os = "windows")]
51	{
52		let Trimmed = if Resolved.len() >= 3 && Resolved.starts_with('/') && Resolved.as_bytes().get(2) == Some(&b':') {
53			Resolved[1..].to_string()
54		} else {
55			Resolved
56		};
57		Trimmed.replace('/', "\\")
58	}
59
60	#[cfg(not(target_os = "windows"))]
61	{
62		Resolved
63	}
64}
65
66fn resolve_userdata_path(Path:&str) -> String {
67	if !Path.starts_with("/User/") && Path != "/User" {
68		return Path.to_string();
69	}
70
71	let UserDataBase = get_userdata_base_dir();
72	let Resolved = format!("{}{}", UserDataBase, Path);
73	dev_log!("vfs", "resolve_userdata: {} -> {}", Path, Resolved);
74	Resolved
75}
76
77/// Map paths starting with /Static/Application/ to the real Sky Target
78/// directory. Also accepts the leading-slash-less form - the WASM loader
79/// (`vscode-oniguruma` → `onig.wasm`) resolves asset URLs relative to the
80/// current document, which strips the leading slash before the path
81/// reaches `file:read`. Without this branch, `tokio::fs::read` would be
82/// called with a relative path and fail with ENOENT, breaking TextMate
83/// syntax highlighting.
84fn resolve_static_application_path(Path:&str) -> String {
85	let Normalized = if Path.starts_with("/Static/Application/") || Path == "/Static/Application" {
86		Path.to_string()
87	} else if Path.starts_with("Static/Application/") || Path == "Static/Application" {
88		format!("/{}", Path)
89	} else {
90		return Path.to_string();
91	};
92
93	if let Some(Root) = get_static_application_root() {
94		let Relative = Normalized.strip_prefix("/Static/Application").unwrap_or("");
95		let Resolved = format!("{}/Static/Application{}", Root, Relative);
96		dev_log!("vfs", "resolve_static: {} -> {}", Path, Resolved);
97		Resolved
98	} else {
99		Path.to_string()
100	}
101}
102
103/// Decode percent-encoded characters in URI paths.
104/// Handles: %20 (space), %23 (#), %25 (%), %5B ([), %5D (]), etc.
105pub fn percent_decode(Input:&str) -> String {
106	let mut Result = String::with_capacity(Input.len());
107	let Bytes = Input.as_bytes();
108	let mut I = 0;
109
110	while I < Bytes.len() {
111		if Bytes[I] == b'%' && I + 2 < Bytes.len() {
112			let High = hex_digit(Bytes[I + 1]);
113			let Low = hex_digit(Bytes[I + 2]);
114			if let (Some(H), Some(L)) = (High, Low) {
115				Result.push((H * 16 + L) as char);
116				I += 3;
117				continue;
118			}
119		}
120		Result.push(Bytes[I] as char);
121		I += 1;
122	}
123
124	Result
125}
126
127pub fn hex_digit(Byte:u8) -> Option<u8> {
128	match Byte {
129		b'0'..=b'9' => Some(Byte - b'0'),
130		b'a'..=b'f' => Some(Byte - b'a' + 10),
131		b'A'..=b'F' => Some(Byte - b'A' + 10),
132		_ => None,
133	}
134}