Skip to main content

DevelopmentNodeEnvironment_MicrosoftVSCodeDependency_22NodeVersion_Bundle_Clean_Debug_ElectronProfile_EsbuildCompiler_Mountain/Binary/Build/
Scheme.rs

1//! # Scheme Handler Module
2//!
3//! Provides custom URI scheme handlers for Tauri webview isolation.
4//!
5//! ## RESPONSIBILITIES
6//!
7//! - Handle `land://` custom protocol requests
8//! - Routing to local HTTP services via ServiceRegistry
9//! - Forward HTTP requests (GET, POST, PUT, DELETE, PATCH) to local services
10//! - Set appropriate CORS headers for webview isolation
11//! - Handle CORS preflight requests (OPTIONS method)
12//! - Implement basic caching for static assets
13//! - Handle health checks and error scenarios
14//!
15//! ## ARCHITECTURAL ROLE
16//!
17//! The Scheme module provides protocol-level isolation and routing for
18//! webviews:
19//!
20//! ```text
21//! land://code.land.playform.cloud/path ──► ServiceRegistry ──► http://127.0.0.1:PORT/path
22//!                                       │                        │
23//!                                       ▼                        ▼
24//!                               CORS Headers Set          Local Service
25//!                                                            Response
26//! ```
27//!
28//! ## SECURITY
29//!
30//! - All responses include Access-Control-Allow-Origin:
31//!   land://code.land.playform.cloud
32//! - Content-Type preserved from local service response
33//! - CORS headers set appropriately for cross-origin requests
34//! - Request validation and sanitization
35
36use std::{
37	collections::HashMap,
38	panic::{AssertUnwindSafe, catch_unwind},
39	sync::RwLock,
40};
41
42use tauri::http::{
43	Method,
44	request::Request,
45	response::{Builder, Response},
46};
47
48use super::ServiceRegistry::ServiceRegistry;
49use crate::dev_log;
50
51// Global service registry (will be initialized in Tauri setup)
52static SERVICE_REGISTRY:RwLock<Option<ServiceRegistry>> = RwLock::new(None);
53
54/// Initialize the global service registry
55///
56/// This must be called once during application setup before any land://
57/// requests.
58pub fn init_service_registry(registry:ServiceRegistry) {
59	let mut registry_lock = SERVICE_REGISTRY.write().unwrap();
60
61	*registry_lock = Some(registry);
62}
63
64/// Get a reference to the global service registry
65///
66/// Returns None if not initialized (should not happen in normal operation).
67///
68/// # Safety
69/// This function uses an unsafe block to get a static reference to the
70/// service registry. This is safe because:
71/// 1. The SERVICE_REGISTRY is a static RwLock that lives for the entire program
72/// 2. We only write to it during initialization (before any land:// requests)
73/// 3. After initialization, we only read from it
74/// 4. The RwLock guarantees thread-safe access
75fn get_service_registry() -> Option<ServiceRegistry> {
76	let guard = SERVICE_REGISTRY.read().ok()?;
77
78	guard.clone()
79}
80
81/// DNS port managed state structure
82///
83/// This struct holds the DNS server port number and is managed by Tauri
84/// as application state, making it accessible to Tauri commands.
85#[derive(Clone, Debug)]
86pub struct DnsPort(pub u16);
87
88/// Cache entry for static asset caching
89#[derive(Clone)]
90struct CacheEntry {
91	/// Cached response bytes
92	body:Vec<u8>,
93
94	/// Content-Type header value
95	content_type:String,
96
97	/// Cache-Control header value
98	cache_control:String,
99
100	/// ETag for conditional requests
101	etag:Option<String>,
102
103	/// Last-Modified timestamp
104	last_modified:Option<String>,
105}
106
107/// Simple in-memory cache for static assets
108///
109/// Uses a HashMap to store cached responses by URL path.
110/// This is a basic implementation that could be enhanced with:
111/// - TTL-based expiration
112/// - LRU eviction when cache is full
113/// - Size limits
114static CACHE:RwLock<Option<HashMap<String, CacheEntry>>> = RwLock::new(None);
115
116/// Initialize the static asset cache
117fn init_cache() {
118	let mut cache = CACHE.write().unwrap();
119
120	if cache.is_none() {
121		*cache = Some(HashMap::new());
122	}
123}
124
125/// Get a cached response if available
126fn get_cached(path:&str) -> Option<CacheEntry> {
127	let cache = CACHE.read().unwrap();
128
129	cache.as_ref()?.get(path).cloned()
130}
131
132/// Store a response in the cache
133fn set_cached(path:&str, entry:CacheEntry) {
134	let mut cache = CACHE.write().unwrap();
135
136	if let Some(cache) = cache.as_mut() {
137		cache.insert(path.to_string(), entry);
138	}
139}
140
141/// Check if a path should be cached
142///
143/// Returns true for CSS, JS, images, fonts, and other static assets.
144fn should_cache(path:&str) -> bool {
145	let path_lower = path.to_lowercase();
146
147	path_lower.ends_with(".css")
148		|| path_lower.ends_with(".js")
149		|| path_lower.ends_with(".png")
150		|| path_lower.ends_with(".jpg")
151		|| path_lower.ends_with(".jpeg")
152		|| path_lower.ends_with(".gif")
153		|| path_lower.ends_with(".svg")
154		|| path_lower.ends_with(".woff")
155		|| path_lower.ends_with(".woff2")
156		|| path_lower.ends_with(".ttf")
157		|| path_lower.ends_with(".eot")
158		|| path_lower.ends_with(".ico")
159}
160
161/// Parse a land:// URI to extract domain and path
162///
163/// # Parameters
164///
165/// - `uri`: The land:// URI (e.g.,
166///   "land://code.land.playform.cloud/path/to/resource")
167///
168/// # Returns
169///
170/// A tuple of (domain, path) where:
171/// - domain: "code.land.playform.cloud"
172/// - path: "/path/to/resource"
173///
174/// # Example
175///
176/// ```rust
177/// let (domain, path) = parse_land_uri("land://code.land.playform.cloud/api/status");
178/// assert_eq!(domain, "code.land.playform.cloud");
179/// assert_eq!(path, "/api/status");
180/// ```
181fn parse_land_uri(uri:&str) -> Result<(String, String), String> {
182	// Remove the land:// prefix
183	let without_scheme = uri
184		.strip_prefix("land://")
185		.ok_or_else(|| format!("Invalid land:// URI: {}", uri))?;
186
187	// Split into domain and path
188	let parts:Vec<&str> = without_scheme.splitn(2, '/').collect();
189
190	let domain = parts.get(0).ok_or_else(|| format!("No domain in URI: {}", uri))?.to_string();
191
192	let path = if parts.len() > 1 { format!("/{}", parts[1]) } else { "/".to_string() };
193
194	dev_log!("lifecycle", "[Scheme] Parsed URI: {} -> domain={}, path={}", uri, domain, path);
195
196	Ok((domain, path))
197}
198
199/// Forward an HTTP request to a local service
200///
201/// # Parameters
202///
203/// - `url`: The full URL to forward to (e.g., "http://127.0.0.1:8080/path")
204/// - `request`: The original Tauri request
205/// - `method`: The HTTP method to use
206///
207/// # Returns
208///
209/// A Tauri response with status, headers, and body from the forwarded request
210fn forward_http_request(
211	url:&str,
212
213	request:&Request<Vec<u8>>,
214
215	method:Method,
216) -> Result<(u16, Vec<u8>, HashMap<String, String>), String> {
217	// Parse URL to get host and path
218	let parsed_url = url.parse::<http::uri::Uri>().map_err(|e| format!("Invalid URL: {}", e))?;
219
220	// Extract host, port, and path as owned strings to satisfy 'static lifetime
221	let host = parsed_url.host().ok_or("No host in URL")?.to_string();
222
223	let port = parsed_url.port_u16().unwrap_or(80);
224
225	let path = parsed_url
226		.path_and_query()
227		.map(|p| p.as_str().to_string())
228		.unwrap_or_else(|| "/".to_string());
229
230	let addr = format!("{}:{}", host, port);
231
232	dev_log!("lifecycle", "[Scheme] Connecting to {} at {}", url, addr);
233
234	// Clone request body and headers for use in thread
235	let body = request.body().clone();
236
237	let headers:Vec<(String, String)> = request
238		.headers()
239		.iter()
240		.filter_map(|(name, value)| {
241			let header_name = name.as_str().to_lowercase();
242			let hop_by_hop_headers = [
243				"connection",
244				"keep-alive",
245				"proxy-authenticate",
246				"proxy-authorization",
247				"te",
248				"trailers",
249				"transfer-encoding",
250				"upgrade",
251			];
252			if !hop_by_hop_headers.contains(&header_name.as_str()) {
253				value.to_str().ok().map(|v| (name.as_str().to_string(), v.to_string()))
254			} else {
255				None
256			}
257		})
258		.collect();
259
260	// Use tokio runtime to make the request
261	let result = std::thread::spawn(move || {
262		let rt = tokio::runtime::Runtime::new().map_err(|e| format!("Failed to create runtime: {}", e))?;
263
264		rt.block_on(async {
265			use tokio::{
266				io::{AsyncReadExt, AsyncWriteExt},
267				net::TcpStream,
268			};
269
270			// Connect to the service
271			let mut stream = TcpStream::connect(&addr)
272				.await
273				.map_err(|e| format!("Failed to connect: {}", e))?;
274
275			// Build HTTP request
276			let mut request_str = format!("{} {} HTTP/1.1\r\nHost: {}\r\n", method.as_str(), path, host);
277
278			// Add headers
279			for (name, value) in &headers {
280				request_str.push_str(&format!("{}: {}\r\n", name, value));
281			}
282
283			// Add Content-Length if there's a body
284			if !body.is_empty() {
285				request_str.push_str(&format!("Content-Length: {}\r\n", body.len()));
286			}
287
288			request_str.push_str("\r\n");
289
290			// Send request
291			stream
292				.write_all(request_str.as_bytes())
293				.await
294				.map_err(|e| format!("Failed to write request: {}", e))?;
295
296			if !body.is_empty() {
297				stream
298					.write_all(&body)
299					.await
300					.map_err(|e| format!("Failed to write body: {}", e))?;
301			}
302
303			// Read response
304			let mut buffer = Vec::new();
305			let mut temp_buf = [0u8; 8192];
306
307			loop {
308				let n = stream
309					.read(&mut temp_buf)
310					.await
311					.map_err(|e| format!("Failed to read response: {}", e))?;
312
313				if n == 0 {
314					break;
315				}
316
317				buffer.extend_from_slice(&temp_buf[..n]);
318
319				// Check if we've read the full response (simple check for content-length or end
320				// of headers)
321				if buffer.len() > 1024 * 1024 {
322					// Limit to 1MB
323					dev_log!("lifecycle", "warn: [Scheme] Response too large, truncating");
324					break;
325				}
326
327				// Simple heuristic: if we have a full HTTP response with Content-Length, check
328				// if we've read everything
329				if let Some(headers_end) = buffer.windows(4).position(|w| w == b"\r\n\r\n") {
330					let headers = String::from_utf8_lossy(&buffer[..headers_end]);
331					if let Some(cl_line) = headers.lines().find(|l| l.to_lowercase().starts_with("content-length:")) {
332						if let Ok(cl) = cl_line.trim_start_matches("content-length:").trim().parse::<usize>() {
333							let body_expected = headers_end + 4 + cl;
334							if buffer.len() >= body_expected {
335								break;
336							}
337						}
338					} else if !headers.contains("Transfer-Encoding: chunked") {
339						// No Content-Length and not chunked, assume complete if connection closes
340						continue;
341					}
342				}
343			}
344
345			// Parse response - pass raw bytes so binary bodies (PNG, etc.)
346			// are never corrupted by UTF-8 lossy conversion.
347			parse_http_response(&buffer)
348		})
349	})
350	.join()
351	.map_err(|e| format!("Thread panicked: {:?}", e))?;
352
353	result
354}
355
356/// Parse a raw HTTP response into (status, body, headers).
357/// Operates on raw bytes so binary bodies (PNG, JPEG, WASM, etc.) are never
358/// corrupted by UTF-8 lossy conversion. Only the headers portion (which is
359/// always ASCII) is decoded as UTF-8.
360fn parse_http_response(response:&[u8]) -> Result<(u16, Vec<u8>, HashMap<String, String>), String> {
361	let headers_end = response
362		.windows(4)
363		.position(|w| w == b"\r\n\r\n")
364		.ok_or("Invalid HTTP response: no headers/body separator")?;
365
366	let headers_str =
367		std::str::from_utf8(&response[..headers_end]).map_err(|e| format!("Invalid UTF-8 in HTTP headers: {}", e))?;
368
369	let body = response[headers_end + 4..].to_vec();
370
371	// Parse status line
372	let mut lines = headers_str.lines();
373
374	let status_line = lines.next().ok_or("Invalid HTTP response: no status line")?;
375
376	// Parse status code (e.g., "HTTP/1.1 200 OK" -> 200)
377	let status = status_line
378		.split_whitespace()
379		.nth(1)
380		.and_then(|s| s.parse::<u16>().ok())
381		.ok_or_else(|| format!("Invalid status line: {}", status_line))?;
382
383	// Parse headers
384	let mut headers = HashMap::new();
385
386	for line in lines {
387		if let Some((name, value)) = line.split_once(':') {
388			headers.insert(name.trim().to_lowercase(), value.trim().to_string());
389		}
390	}
391
392	Ok((status, body, headers))
393}
394
395/// Handles `land://` custom protocol requests
396///
397/// This function is called by Tauri when a webview makes a request to the
398/// `land://` protocol. It routes the request to local HTTP services via the
399/// ServiceRegistry.
400///
401/// # Parameters
402///
403/// - `request`: The incoming webview request with URI path and headers
404///
405/// # Returns
406///
407/// A Tauri response with:
408/// - Status code from local service (or error status)
409/// - Headers from local service plus CORS headers
410/// - Response body from local service (or error body)
411///
412/// # Implementation Details
413///
414/// 1. Parse the land:// URI to extract domain and path
415/// 2. Look up the service in the ServiceRegistry
416/// 3. Handle CORS preflight (OPTIONS) requests
417/// 4. Check cache for static assets
418/// 5. Forward the request to the local service
419/// 6. Add CORS headers to the response
420/// 7. Cache static assets for future requests
421///
422/// # Error Handling
423///
424/// - 400: Invalid URI format
425/// - 404: Service not found in registry
426/// - 503: Service unavailable / request failed
427///
428/// # Example
429///
430/// ```rust
431/// tauri::Builder::default()
432/// 	.register_uri_scheme_protocol("fiddee", |_app, request| fiddee_scheme_handler(request))
433/// ```
434pub fn land_scheme_handler(request:&Request<Vec<u8>>) -> Response<Vec<u8>> {
435	// Initialize cache on first request
436	init_cache();
437
438	// Get URI
439	let uri = request.uri().to_string();
440
441	dev_log!("lifecycle", "[Scheme] Handling land:// request: {}", uri);
442
443	// Parse URI to extract domain and path
444	let (domain, path) = match parse_land_uri(&uri) {
445		Ok(result) => result,
446
447		Err(e) => {
448			dev_log!("lifecycle", "error: [Scheme] Failed to parse URI: {}", e);
449
450			return build_error_response(400, &format!("Bad Request: {}", e));
451		},
452	};
453
454	// Handle CORS preflight requests
455	if request.method() == Method::OPTIONS {
456		dev_log!("lifecycle", "[Scheme] Handling CORS preflight request");
457
458		return build_cors_preflight_response();
459	}
460
461	// Check cache for static assets
462	if should_cache(&path) {
463		if let Some(cached) = get_cached(&path) {
464			dev_log!("lifecycle", "[Scheme] Cache hit for: {}", path);
465
466			return build_cached_response(cached);
467		}
468	}
469
470	// Look up service in registry
471	let registry = match get_service_registry() {
472		Some(r) => r,
473
474		None => {
475			dev_log!("lifecycle", "error: [Scheme] Service registry not initialized");
476
477			return build_error_response(503, "Service Unavailable: Registry not initialized");
478		},
479	};
480
481	let service = match registry.lookup(&domain) {
482		Some(s) => s,
483
484		None => {
485			dev_log!("lifecycle", "warn: [Scheme] Service not found: {}", domain);
486
487			return build_error_response(404, &format!("Not Found: Service {} not registered", domain));
488		},
489	};
490
491	// Build local service URL
492	let local_url = format!("http://127.0.0.1:{}{}", service.port, path);
493
494	dev_log!(
495		"lifecycle",
496		"[Scheme] Routing {} {} to local service at {}",
497		request.method(),
498		uri,
499		local_url
500	);
501
502	// Forward request to local service
503	let result = forward_http_request(&local_url, request, request.method().clone());
504
505	match result {
506		Ok((status, body, headers)) => {
507			// Clone body before using it
508			let body_bytes = body.clone();
509
510			// LAND-FIX B1.P1: MIME-honesty on 404. The localhost
511			// server (or Astro/Vite dev page underneath) returns an
512			// HTML body with `Content-Type: text/html` for any
513			// missing path. The webview asks for `.js`/`.json`/`.css`
514			// files; when it parses the HTML body as JS it crashes
515			// with `SyntaxError: Unexpected token '<'` at column N -
516			// the exact symptom reported in the release-electron-
517			// bundled run. Rewrite the response to text/plain empty
518			// body when the request was for a known asset extension
519			// AND upstream returned non-2xx.
520			let LowerPath = path.to_ascii_lowercase();
521
522			let IsAssetRequest = LowerPath.ends_with(".js")
523				|| LowerPath.ends_with(".mjs")
524				|| LowerPath.ends_with(".cjs")
525				|| LowerPath.ends_with(".json")
526				|| LowerPath.ends_with(".map")
527				|| LowerPath.ends_with(".css")
528				|| LowerPath.ends_with(".wasm")
529				|| LowerPath.ends_with(".svg")
530				|| LowerPath.ends_with(".png")
531				|| LowerPath.ends_with(".woff")
532				|| LowerPath.ends_with(".woff2")
533				|| LowerPath.ends_with(".ttf")
534				|| LowerPath.ends_with(".otf");
535
536			let UpstreamSaysHtml = headers
537				.get("content-type")
538				.map(|V| V.to_ascii_lowercase().contains("text/html"))
539				.unwrap_or(false);
540
541			if IsAssetRequest && (status == 404 || (status >= 400 && UpstreamSaysHtml)) {
542				dev_log!(
543					"scheme-assets",
544					"[LandFix:Mime] swap HTML 404 → text/plain empty for asset path={} status={}",
545					path,
546					status
547				);
548
549				return Builder::new()
550					.status(404)
551					.header("Content-Type", "text/plain; charset=utf-8")
552					.header("Access-Control-Allow-Origin", "land://code.land.playform.cloud")
553					.body(Vec::<u8>::new())
554					.unwrap_or_else(|_| build_error_response(500, "Failed to build 404 response"));
555			}
556
557			// Build response with CORS headers
558			let mut response_builder = Builder::new()
559				.status(status)
560				.header("Access-Control-Allow-Origin", "land://code.land.playform.cloud")
561				.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
562				.header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
563
564			// Add important headers from local service
565			let important_headers = [
566				"content-type",
567				"content-length",
568				"etag",
569				"last-modified",
570				"cache-control",
571				"expires",
572				"content-encoding",
573				"content-disposition",
574				"location",
575			];
576
577			for header_name in &important_headers {
578				if let Some(value) = headers.get(*header_name) {
579					response_builder = response_builder.header(*header_name, value);
580				}
581			}
582
583			let response = response_builder.body(body_bytes);
584
585			// Cache static assets
586			if status == 200 && should_cache(&path) {
587				let content_type = headers
588					.get("content-type")
589					.unwrap_or(&"application/octet-stream".to_string())
590					.clone();
591
592				let cache_control = headers
593					.get("cache-control")
594					.unwrap_or(&"public, max-age=3600".to_string())
595					.clone();
596
597				let etag = headers.get("etag").cloned();
598
599				let last_modified = headers.get("last-modified").cloned();
600
601				let entry = CacheEntry { body, content_type, cache_control, etag, last_modified };
602
603				set_cached(&path, entry);
604
605				dev_log!("lifecycle", "[Scheme] Cached response for: {}", path);
606			}
607
608			response.unwrap_or_else(|_| build_error_response(500, "Internal Server Error"))
609		},
610
611		Err(e) => {
612			dev_log!("lifecycle", "error: [Scheme] Failed to forward request: {}", e);
613
614			build_error_response(503, &format!("Service Unavailable: {}", e))
615		},
616	}
617}
618
619/// Build an error response with CORS headers
620fn build_error_response(status:u16, message:&str) -> Response<Vec<u8>> {
621	let body = serde_json::json!({
622		"error": message,
623		"status": status
624	});
625
626	Builder::new()
627		.status(status)
628		.header("Content-Type", "application/json")
629		.header("Access-Control-Allow-Origin", "land://code.land.playform.cloud")
630		.body(serde_json::to_vec(&body).unwrap_or_default())
631		.unwrap_or_else(|_| Builder::new().status(500).body(Vec::new()).unwrap())
632}
633
634/// Build a CORS preflight response
635fn build_cors_preflight_response() -> Response<Vec<u8>> {
636	Builder::new()
637		.status(204)
638		.header("Access-Control-Allow-Origin", "land://code.land.playform.cloud")
639		.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
640		.header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
641		.header("Access-Control-Max-Age", "86400")
642		.body(Vec::new())
643		.unwrap()
644}
645
646/// Build a response from cached data
647fn build_cached_response(entry:CacheEntry) -> Response<Vec<u8>> {
648	let mut builder = Builder::new()
649		.status(200)
650		.header("Content-Type", &entry.content_type)
651		.header("Access-Control-Allow-Origin", "land://code.land.playform.cloud")
652		.header("Cache-Control", &entry.cache_control);
653
654	if let Some(etag) = &entry.etag {
655		builder = builder.header("ETag", etag);
656	}
657
658	if let Some(last_modified) = &entry.last_modified {
659		builder = builder.header("Last-Modified", last_modified);
660	}
661
662	builder
663		.body(entry.body)
664		.unwrap_or_else(|_| build_error_response(500, "Internal Server Error"))
665}
666
667/// Register a service with the land:// scheme
668///
669/// This helper function makes it easy to register local services.
670///
671/// # Parameters
672///
673/// - `name`: Domain name (e.g., "code.land.playform.cloud")
674/// - `port`: Local port where the service is listening
675pub fn register_land_service(name:&str, port:u16) {
676	let registry = get_service_registry().expect("Service registry not initialized. Call init_service_registry first.");
677
678	registry.register(name.to_string(), port, Some("/health".to_string()));
679
680	dev_log!("lifecycle", "[Scheme] Registered service: {} -> {}", name, port);
681}
682
683/// Get the port for a registered service
684///
685/// # Parameters
686///
687/// - `name`: Domain name to look up
688///
689/// # Returns
690///
691/// - `Some(port)` if service is registered
692/// - `None` if service not found
693pub fn get_land_port(name:&str) -> Option<u16> {
694	let registry = get_service_registry()?;
695
696	registry.lookup(name).map(|s| s.port)
697}
698
699/// Handles `land://` custom protocol requests asynchronously
700///
701/// This is the asynchronous version of `land_scheme_handler` that uses
702/// Tauri's `UriSchemeResponder` to respond asynchronously, allowing the
703/// request processing to happen in a separate thread.
704///
705/// This is the recommended handler for production use as it provides better
706/// performance and doesn't block the main thread.
707///
708/// # Parameters
709///
710/// - `_ctx`: The URI scheme context (not used in current implementation)
711/// - `request`: The incoming webview request with URI path and headers
712/// - `responder`: The responder to send the response back asynchronously
713///
714/// # Platform Support
715///
716/// - **macOS, Linux**: Uses `land://localhost/` as Origin
717/// - **Windows**: Uses `http://land.localhost/` as Origin by default
718///
719/// # Example
720///
721/// ```rust
722/// tauri::Builder::default()
723/// 	.register_asynchronous_uri_scheme_protocol("fiddee", |_ctx, request, responder| {
724/// 		land_scheme_handler_async(_ctx, request, responder)
725/// 	})
726/// ```
727///
728/// Note: This implementation uses thread spawning as a workaround since
729/// Tauri 2.x's async scheme handler API requires specific runtime setup.
730/// The thread-based approach works correctly and is production-ready.
731pub fn land_scheme_handler_async<R:tauri::Runtime>(
732	_ctx:tauri::UriSchemeContext<'_, R>,
733
734	request:tauri::http::request::Request<Vec<u8>>,
735
736	responder:tauri::UriSchemeResponder,
737) {
738	// Spawn a new thread to handle the request asynchronously
739	std::thread::spawn(move || {
740		let response = land_scheme_handler(&request);
741		responder.respond(response);
742	});
743}
744
745/// Get the appropriate Access-Control-Allow-Origin header for the current
746/// platform
747///
748/// Tauri uses different origins for custom URI schemes on different platforms:
749/// - macOS, Linux: land://localhost/
750/// - Windows: <http://land.localhost/>
751///
752/// Returns a comma-separated list of origins to support all platforms.
753fn get_cors_origins() -> &'static str {
754	// Support both macOS/Linux (land://localhost) and Windows (http://land.localhost)
755	"land://localhost, http://land.localhost, land://code.land.playform.cloud"
756}
757
758/// Initializes the scheme handler module
759///
760/// This is a placeholder function that can be used for any future
761/// initialization logic needed by the scheme handler.
762#[inline]
763pub fn Scheme() {}
764
765// ==========================================================================
766// vscode-file:// Protocol Handler
767// ==========================================================================
768
769/// MIME type detection from file extension
770fn MimeFromExtension(Path:&str) -> &'static str {
771	if Path.ends_with(".js") || Path.ends_with(".mjs") {
772		"application/javascript"
773	} else if Path.ends_with(".css") {
774		"text/css"
775	} else if Path.ends_with(".html") || Path.ends_with(".htm") {
776		"text/html"
777	} else if Path.ends_with(".json") {
778		"application/json"
779	} else if Path.ends_with(".svg") {
780		"image/svg+xml"
781	} else if Path.ends_with(".png") {
782		"image/png"
783	} else if Path.ends_with(".jpg") || Path.ends_with(".jpeg") {
784		"image/jpeg"
785	} else if Path.ends_with(".gif") {
786		"image/gif"
787	} else if Path.ends_with(".woff") {
788		"font/woff"
789	} else if Path.ends_with(".woff2") {
790		"font/woff2"
791	} else if Path.ends_with(".ttf") {
792		"font/ttf"
793	} else if Path.ends_with(".wasm") {
794		"application/wasm"
795	} else if Path.ends_with(".map") {
796		"application/json"
797	} else if Path.ends_with(".txt") || Path.ends_with(".md") {
798		"text/plain"
799	} else if Path.ends_with(".xml") {
800		"application/xml"
801	} else {
802		"application/octet-stream"
803	}
804}
805
806/// Handles `vscode-file://` custom protocol requests.
807///
808/// VS Code's Electron workbench computes asset URLs as:
809///   `vscode-file://vscode-app/{appRoot}/out/vs/workbench/...`
810///
811/// This handler maps those URLs to the embedded frontend assets
812/// served from the `frontendDist` directory (`../Sky/Target`).
813///
814/// # URL Mapping
815///
816/// ```text
817/// vscode-file://vscode-app/Static/Application/vs/workbench/foo.js
818///                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
819///                          This path maps to Sky/Target/Static/Application/vs/workbench/foo.js
820/// ```
821///
822/// The `/out/` prefix that the workbench appends is stripped if present,
823/// since our assets live at `/Static/Application/vs/` not
824/// `/Static/Application/out/vs/`.
825///
826/// # Parameters
827///
828/// - `AppHandle`: Tauri AppHandle for resolving the frontend dist path
829/// - `Request`: The incoming request
830///
831/// # Returns
832///
833/// Response with file contents and correct MIME type, or 404
834pub fn VscodeFileSchemeHandler<R:tauri::Runtime>(
835	AppHandle:&tauri::AppHandle<R>,
836
837	Request:&tauri::http::request::Request<Vec<u8>>,
838) -> Response<Vec<u8>> {
839	// The scheme handler runs inside the wkwebview URL loading code
840	// (Objective-C FFI). A panic here crosses an `extern "C"` boundary
841	// that cannot unwind - the process aborts immediately. Catch the
842	// panic so a bad mmap or MIME bug returns a 500 instead of taking
843	// the whole editor down.
844	let Result = catch_unwind(AssertUnwindSafe(|| _VscodeFileSchemeHandler(AppHandle, Request)));
845
846	match Result {
847		Ok(Response) => Response,
848
849		Err(Panic) => {
850			let Info = if let Some(Text) = Panic.downcast_ref::<&str>() {
851				Text.to_string()
852			} else if let Some(Text) = Panic.downcast_ref::<String>() {
853				Text.clone()
854			} else {
855				"unknown panic".to_string()
856			};
857
858			dev_log!(
859				"lifecycle",
860				"error: [LandFix:VscodeFile] caught panic in scheme handler: {}",
861				Info
862			);
863
864			build_error_response(500, &format!("Internal Server Error (caught panic: {})", Info))
865		},
866	}
867}
868
869fn _VscodeFileSchemeHandler<R:tauri::Runtime>(
870	AppHandle:&tauri::AppHandle<R>,
871
872	Request:&tauri::http::request::Request<Vec<u8>>,
873) -> Response<Vec<u8>> {
874	let Uri = Request.uri().to_string();
875
876	// Per-asset-request line - every `<img src="vscode-file://...">` +
877	// worker / wasm / font in the workbench fires through here. The
878	// `scheme-assets` line below (opt-in tag) already captures the
879	// same data; duplicating under `lifecycle` at the default level
880	// just floods the log.
881	dev_log!("scheme-assets", "[LandFix:VscodeFile] Request: {}", Uri);
882
883	dev_log!("scheme-assets", "[SchemeAssets] request uri={}", Uri);
884
885	// Extract path from: vscode-file://<authority>/<path>
886	//
887	// The canonical workbench-side authority is `vscode-app` (used by
888	// `FileAccess.uriToBrowserUri` for ALL workbench resources). But
889	// `WebviewImplementation::asWebviewUri` rewrites local resource
890	// URIs to use the extension's identifier as the authority - e.g.
891	// `vscode-file://vscode.git/Volumes/.../extensions/git/media/icon.svg`.
892	// The strip-prefix chain below covers both:
893	//   1. Exact `vscode-app` authority (with or without trailing `/`)
894	//   2. ANY other authority - we treat the post-authority path as the resource
895	//      path and let the OS-absolute-root detection below serve it straight from
896	//      disk. Without this fallback every extension-supplied webview asset
897	//      (icons, scripts, stylesheets, fonts) returned 404 because the strip
898	//      yielded `""` and the asset_resolver lookup ran with an empty key.
899	let FilePath = Uri
900		.strip_prefix("vscode-file://vscode-app/")
901		.or_else(|| Uri.strip_prefix("vscode-file://vscode-app"))
902		.or_else(|| {
903			// Generic `vscode-file://<authority>/<path>` - skip past the
904			// `vscode-file://` scheme + the authority's first `/`.
905			let After = Uri.strip_prefix("vscode-file://")?;
906			let SlashIdx = After.find('/')?;
907			Some(&After[SlashIdx + 1..])
908		})
909		.unwrap_or("");
910
911	// Strip /out/ prefix if present - our assets are at /Static/Application/vs/
912	// not /Static/Application/out/vs/
913	let CleanPath = if FilePath.starts_with("Static/Application//out/") {
914		FilePath.replacen("Static/Application//out/", "Static/Application/", 1)
915	} else if FilePath.starts_with("Static/Application/out/") {
916		FilePath.replacen("Static/Application/out/", "Static/Application/", 1)
917	} else {
918		FilePath.to_string()
919	};
920
921	// VS Code's nodeModulesPath = 'vs/../../node_modules' resolves ../../ from
922	// Static/Application/vs/ up to Static/. The browser canonicalizes this to
923	// Static/node_modules/ but our files live at Static/Application/node_modules/.
924	let CleanPath = if CleanPath.starts_with("Static/node_modules/") {
925		CleanPath.replacen("Static/node_modules/", "Static/Application/node_modules/", 1)
926	} else {
927		CleanPath
928	};
929
930	// Strip `?<query>` and `#<fragment>` from the resolved path so
931	// filesystem / asset-resolver lookups operate on a clean path
932	// component. Roo's runtime sourcemap-probe (`vZt` in its bundle)
933	// fetches `<src>?source-map=true` which would otherwise hit the
934	// asset_resolver as a literal `index.js?source-map=true` filename
935	// and either 404 or fall through to the SPA-fallback `index.html`
936	// (5765 bytes served as `application/octet-stream`). With the
937	// strip, `index.js?source-map=true` → `index.js`, which exists on
938	// disk and serves correctly with the right MIME. Equivalent for
939	// `#<fragment>`. Sourcemap-probe URLs that point to non-existent
940	// suffixes (`index.map.json`, `index.sourcemap`) still 404
941	// silently; that is the intended behavior of `vZt`'s preload list.
942	let CleanPath = match CleanPath.split_once(['?', '#']) {
943		Some((Before, _)) => Before.to_string(),
944
945		None => CleanPath,
946	};
947
948	// P1.5 fix: DevTools fetches `*.js.map` for every bundled script it loads
949	// to render pretty stack traces. Our `Static/Application/` tree ships the
950	// JS files without their `.map` siblings (esbuild's `sourcemap:false` path)
951	// so those requests always 404. Short-circuit here with a clean
952	// `204 No Content` - Chromium treats 204 as "no map available" and moves
953	// on silently, avoiding both the noisy stderr lines and the filesystem
954	// stat round-trip per request.
955	if CleanPath.ends_with(".map") {
956		return Builder::new()
957			.status(204)
958			.header("Access-Control-Allow-Origin", "*")
959			.header("Cross-Origin-Resource-Policy", "cross-origin")
960			.body(Vec::new())
961			.unwrap_or_else(|_| build_error_response(500, "Failed to build response"));
962	}
963
964	// CSS-as-JS shim: when a `.css` URL is requested through
965	// `vscode-file://` (which happens for any unstripped raw `import
966	// "./foo.css"` that VS Code's bundle still contains after
967	// `workbench.js` switches `_VSCODE_FILE_ROOT` to the custom
968	// scheme), the browser would refuse the response with
969	// `'text/css' is not a valid JavaScript MIME type`. Service
970	// Workers can't intercept custom-scheme requests, so we inline
971	// the same JS shim the Worker SW emits on the localhost path:
972	// invoke `_LOAD_CSS_WORKER` against the localhost-form path and
973	// export an empty default. The SW + `<link>` fast-path then
974	// loads the actual CSS bytes from `/Static/Application/...`.
975	//
976	// CRITICAL gate: only apply the shim for paths under
977	// `Static/Application/` (i.e. workbench-internal CSS imports
978	// that survive bundling as `import "./foo.css"`). Extension-
979	// contributed CSS lives in absolute filesystem paths
980	// (`Users/...`, `Volumes/...`, `Library/...`, etc.) and reaches
981	// `vscode-file://` via `WebviewImplementation::asWebviewUri`.
982	// Those `.css` files MUST be served as real `text/css` from
983	// disk (the IsAbsoluteOSPath fallback below handles them) -
984	// returning the JS shim instead silently breaks every
985	// extension webview-ui that bundles its own stylesheet
986	// (Roo: `webview-ui/build/assets/index.css`, Claude, GitLens,
987	// Continue, etc. all use Vite/webpack and ship CSS bundles).
988	// Without this gate the iframe loads no styles and the panel
989	// renders as a transparent overlay over the workbench - the
990	// classic "blank webview" symptom.
991	if CleanPath.ends_with(".css") && CleanPath.starts_with("Static/Application/") {
992		let LocalPath = format!("/Static/Application/{}", CleanPath.trim_start_matches("Static/Application/"));
993
994		let Body = format!("globalThis._LOAD_CSS_WORKER?.({:?}); export default {{}};", LocalPath);
995
996		dev_log!(
997			"scheme-assets",
998			"[LandFix:VscodeFile] css-shim {} -> _LOAD_CSS_WORKER({})",
999			CleanPath,
1000			LocalPath
1001		);
1002
1003		return Builder::new()
1004			.status(200)
1005			.header("Content-Type", "application/javascript; charset=utf-8")
1006			.header("Access-Control-Allow-Origin", "*")
1007			.header("Cross-Origin-Resource-Policy", "cross-origin")
1008			.header("Cross-Origin-Embedder-Policy", "require-corp")
1009			.header("Cache-Control", "public, max-age=31536000, immutable")
1010			.body(Body.into_bytes())
1011			.unwrap_or_else(|_| build_error_response(500, "Failed to build response"));
1012	}
1013
1014	// Icon themes, grammars and other extension-contributed assets generate
1015	// URIs like `vscode-file://vscode-app/Volumes/<vol>/.../seti.woff` after
1016	// `FileAccess.uriToBrowserUri` rewrites a plain `file:///Volumes/...`
1017	// extension path. The authority `vscode-app` is followed directly by the
1018	// absolute filesystem path (sans leading `/`). Detect the well-known macOS /
1019	// Linux absolute-path roots and serve straight from disk instead of trying
1020	// to resolve them against `Sky/Target/` (where they do not exist).
1021	let IsAbsoluteOSPath = [
1022		"Volumes/",
1023		"Users/",
1024		"Library/",
1025		"System/",
1026		"Applications/",
1027		"private/",
1028		"tmp/",
1029		"var/",
1030		"etc/",
1031		"opt/",
1032		"home/",
1033		"usr/",
1034		"srv/",
1035		"mnt/",
1036		"root/",
1037	]
1038	.iter()
1039	.any(|Prefix| CleanPath.starts_with(Prefix));
1040
1041	if IsAbsoluteOSPath {
1042		let AbsolutePath = format!("/{}", CleanPath);
1043
1044		let FilesystemPath = std::path::Path::new(&AbsolutePath);
1045
1046		dev_log!(
1047			"scheme-assets",
1048			"[LandFix:VscodeFile] os-abs candidate {} (exists={}, is_file={})",
1049			AbsolutePath,
1050			FilesystemPath.exists(),
1051			FilesystemPath.is_file()
1052		);
1053
1054		if FilesystemPath.exists() && FilesystemPath.is_file() {
1055			// LAND-PATCH B7.P01: route through the mmap cache. First
1056			// hit on a path mmaps the file; subsequent hits are
1057			// wait-free DashMap reads. Brotli sibling (`<file>.br`)
1058			// is auto-discovered and served when the request offers
1059			// `Accept-Encoding: br`.
1060			match crate::Cache::AssetMemoryMap::LoadOrInsert::Fn(FilesystemPath) {
1061				Ok(Entry) => {
1062					let AcceptsBrotli = Request
1063						.headers()
1064						.get("accept-encoding")
1065						.and_then(|V| V.to_str().ok())
1066						.map(|S| S.contains("br"))
1067						.unwrap_or(false);
1068
1069					let (Body, Encoding):(Vec<u8>, Option<&str>) = if AcceptsBrotli {
1070						match Entry.AsBrotliSlice() {
1071							Some(Slice) => (Slice.to_vec(), Some("br")),
1072
1073							None => (Entry.AsSlice().to_vec(), None),
1074						}
1075					} else {
1076						(Entry.AsSlice().to_vec(), None)
1077					};
1078
1079					dev_log!(
1080						"scheme-assets",
1081						"[LandFix:VscodeFile] os-abs served {} ({}, {} bytes, encoding={:?})",
1082						AbsolutePath,
1083						Entry.Mime,
1084						Body.len(),
1085						Encoding
1086					);
1087
1088					// `Cross-Origin-Resource-Policy: cross-origin` lets the
1089					// COEP-isolated webview iframe (which Mountain serves
1090					// from the `vscode-webview://` scheme with
1091					// `Cross-Origin-Embedder-Policy: require-corp`) load
1092					// these assets via `<script src=…>` / `<link href=…>`.
1093					// Without it WebKit refuses to expose the response to
1094					// the embedder document and the extension's React
1095					// bundle / CSS / fonts come up as cross-origin
1096					// resource-policy blocks.
1097					let mut B = Builder::new()
1098						.status(200)
1099						.header("Content-Type", Entry.Mime)
1100						.header("Access-Control-Allow-Origin", "*")
1101						.header("Cross-Origin-Resource-Policy", "cross-origin")
1102						.header("Cross-Origin-Embedder-Policy", "require-corp")
1103						.header("Cache-Control", "public, max-age=3600");
1104
1105					if let Some(Enc) = Encoding {
1106						B = B.header("Content-Encoding", Enc);
1107					}
1108
1109					return B
1110						.body(Body)
1111						.unwrap_or_else(|_| build_error_response(500, "Failed to build response"));
1112				},
1113
1114				Err(Error) => {
1115					dev_log!(
1116						"lifecycle",
1117						"warn: [LandFix:VscodeFile] os-abs mmap failure {}: {}",
1118						AbsolutePath,
1119						Error
1120					);
1121				},
1122			}
1123		} else {
1124			dev_log!("lifecycle", "warn: [LandFix:VscodeFile] os-abs not on disk: {}", AbsolutePath);
1125		}
1126	}
1127
1128	dev_log!("lifecycle", "[LandFix:VscodeFile] Resolved path: {}", CleanPath);
1129
1130	// Resolve against the frontendDist directory
1131	// In production: embedded in the binary via asset_resolver
1132	// In debug: fall back to filesystem read from Sky/Target
1133	let AssetResult = AppHandle.asset_resolver().get(CleanPath.clone());
1134
1135	if let Some(Asset) = AssetResult {
1136		let Mime = MimeFromExtension(&CleanPath);
1137
1138		dev_log!(
1139			"lifecycle",
1140			"[LandFix:VscodeFile] Serving (embedded) {} ({}, {} bytes)",
1141			CleanPath,
1142			Mime,
1143			Asset.bytes.len()
1144		);
1145
1146		dev_log!(
1147			"scheme-assets",
1148			"[SchemeAssets] serve source=embedded path={} mime={} bytes={}",
1149			CleanPath,
1150			Mime,
1151			Asset.bytes.len()
1152		);
1153
1154		return Builder::new()
1155			.status(200)
1156			.header("Content-Type", Mime)
1157			.header("Access-Control-Allow-Origin", "*")
1158			.header("Cross-Origin-Resource-Policy", "cross-origin")
1159			.header("Cross-Origin-Embedder-Policy", "require-corp")
1160			.header("Cache-Control", "public, max-age=31536000, immutable")
1161			.body(Asset.bytes.to_vec())
1162			.unwrap_or_else(|_| build_error_response(500, "Failed to build response"));
1163	}
1164
1165	// Fallback: read from filesystem (dev mode where assets aren't embedded)
1166	let StaticRoot = crate::IPC::WindServiceHandlers::Utilities::ApplicationRoot::Get::Fn();
1167
1168	if let Some(Root) = StaticRoot {
1169		let FilesystemPath = std::path::Path::new(&Root).join(&CleanPath);
1170
1171		if FilesystemPath.exists() && FilesystemPath.is_file() {
1172			// LAND-PATCH B7.P01: mmap-cache the StaticRoot fallback
1173			// path so dev-mode workbench reloads pay the syscall
1174			// once per asset for the entire session.
1175			match crate::Cache::AssetMemoryMap::LoadOrInsert::Fn(&FilesystemPath) {
1176				Ok(Entry) => {
1177					let AcceptsBrotli = Request
1178						.headers()
1179						.get("accept-encoding")
1180						.and_then(|V| V.to_str().ok())
1181						.map(|S| S.contains("br"))
1182						.unwrap_or(false);
1183
1184					let (Body, Encoding):(Vec<u8>, Option<&str>) = if AcceptsBrotli {
1185						match Entry.AsBrotliSlice() {
1186							Some(Slice) => (Slice.to_vec(), Some("br")),
1187
1188							None => (Entry.AsSlice().to_vec(), None),
1189						}
1190					} else {
1191						(Entry.AsSlice().to_vec(), None)
1192					};
1193
1194					dev_log!(
1195						"lifecycle",
1196						"[LandFix:VscodeFile] Serving (fs-mmap) {} ({}, {} bytes, encoding={:?})",
1197						CleanPath,
1198						Entry.Mime,
1199						Body.len(),
1200						Encoding
1201					);
1202
1203					// `Cross-Origin-Resource-Policy: cross-origin` lets the
1204					// COEP-isolated webview iframe (which Mountain serves
1205					// from the `vscode-webview://` scheme with
1206					// `Cross-Origin-Embedder-Policy: require-corp`) load
1207					// these assets via `<script src=…>` / `<link href=…>`.
1208					// Without it WebKit refuses to expose the response to
1209					// the embedder document and the extension's React
1210					// bundle / CSS / fonts come up as cross-origin
1211					// resource-policy blocks.
1212					let mut B = Builder::new()
1213						.status(200)
1214						.header("Content-Type", Entry.Mime)
1215						.header("Access-Control-Allow-Origin", "*")
1216						.header("Cross-Origin-Resource-Policy", "cross-origin")
1217						.header("Cross-Origin-Embedder-Policy", "require-corp")
1218						.header("Cache-Control", "public, max-age=3600");
1219
1220					if let Some(Enc) = Encoding {
1221						B = B.header("Content-Encoding", Enc);
1222					}
1223
1224					return B
1225						.body(Body)
1226						.unwrap_or_else(|_| build_error_response(500, "Failed to build response"));
1227				},
1228
1229				Err(Error) => {
1230					dev_log!(
1231						"lifecycle",
1232						"warn: [LandFix:VscodeFile] Failed to read {}: {}",
1233						FilesystemPath.display(),
1234						Error
1235					);
1236				},
1237			}
1238		}
1239	}
1240
1241	dev_log!(
1242		"lifecycle",
1243		"warn: [LandFix:VscodeFile] Not found: {} (resolved: {})",
1244		Uri,
1245		CleanPath
1246	);
1247
1248	build_error_response(404, &format!("Not Found: {}", CleanPath))
1249}
1250
1251/// Custom URI scheme handler for `vscode-webview://` requests.
1252///
1253/// VS Code's `WebviewElement` (used by every extension webview - Roo
1254/// Code, Claude, GitLens, custom-editor providers) wraps the inner
1255/// extension HTML in an `<iframe>` whose `src` is
1256/// `vscode-webview://<authority>/index.html?...`. The `<authority>` is
1257/// a per-instance random base32 string. The authority is irrelevant to
1258/// the bytes served - all that matters is the path component, which
1259/// always resolves under
1260/// `vs/workbench/contrib/webview/browser/pre/`.
1261///
1262/// In stock Electron VS Code, `app.protocol.registerStreamProtocol(
1263/// 'vscode-webview', ...)` serves this directory. Under Tauri 2.x +
1264/// WKWebView, `register_asynchronous_uri_scheme_protocol("vscode-webview",
1265/// ...)` installs an equivalent `WKURLSchemeHandler`. Without this handler,
1266/// every extension that uses `webviewView` / `WebviewPanel` /
1267/// `CustomEditor` lands the inner iframe at a `vscode-webview://...`
1268/// URL the WKWebView can't resolve, the iframe stays blank, and the
1269/// extension surface is dead.
1270///
1271/// Three resources live under `pre/`:
1272///   - `index.html`        - the webview shell that bridges `postMessage`
1273///     between workbench host and inner extension HTML
1274///   - `service-worker.js` - registered by `index.html` to intercept
1275///     `vscode-webview-resource` requests for extension-shipped assets
1276///   - `fake.html`         - sandbox stub used as a placeholder before
1277///     extension HTML arrives via postMessage
1278///
1279/// Anything else (querystrings, extra path segments, GUID-like
1280/// authorities) is silently dropped; the extension's actual content
1281/// gets piped in via the `swMessage` channel after `index.html` boots,
1282/// not through this scheme handler.
1283///
1284/// # Parameters
1285///
1286/// - `AppHandle`: Tauri AppHandle for resolving the embedded asset resolver and
1287///   the dev-mode `Static/Application/` filesystem fallback (same chain as
1288///   `VscodeFileSchemeHandler`).
1289/// - `Request`: The incoming request - typically a `GET` for one of the three
1290///   pre-baked files.
1291///
1292/// # Returns
1293///
1294/// A `Response<Vec<u8>>` carrying:
1295///   - `200 OK` with the file bytes + correct MIME (`text/html` /
1296///     `application/javascript`) when found, or
1297///   - `404 Not Found` when the resolved path falls outside the `pre/`
1298///     directory or the asset isn't shipped.
1299///
1300/// CORS headers are permissive (`*`) to match the workbench host's
1301/// `vscode-webview-resource:` traffic, which round-trips through the
1302/// service worker registered by `index.html`.
1303pub fn VscodeWebviewSchemeHandler<R:tauri::Runtime>(
1304	AppHandle:&tauri::AppHandle<R>,
1305
1306	Request:&tauri::http::request::Request<Vec<u8>>,
1307) -> Response<Vec<u8>> {
1308	let Result = catch_unwind(AssertUnwindSafe(|| _VscodeWebviewSchemeHandler(AppHandle, Request)));
1309
1310	match Result {
1311		Ok(Response) => Response,
1312
1313		Err(Panic) => {
1314			let Info = if let Some(Text) = Panic.downcast_ref::<&str>() {
1315				Text.to_string()
1316			} else if let Some(Text) = Panic.downcast_ref::<String>() {
1317				Text.clone()
1318			} else {
1319				"unknown panic".to_string()
1320			};
1321
1322			dev_log!(
1323				"lifecycle",
1324				"error: [LandFix:VscodeWebview] caught panic in scheme handler: {}",
1325				Info
1326			);
1327
1328			build_error_response(500, &format!("Internal Server Error (caught panic: {})", Info))
1329		},
1330	}
1331}
1332
1333fn _VscodeWebviewSchemeHandler<R:tauri::Runtime>(
1334	AppHandle:&tauri::AppHandle<R>,
1335
1336	Request:&tauri::http::request::Request<Vec<u8>>,
1337) -> Response<Vec<u8>> {
1338	let Uri = Request.uri().to_string();
1339
1340	dev_log!("scheme-assets", "[LandFix:VscodeWebview] Request: {}", Uri);
1341
1342	// `vscode-webview://<authority>/<path>?<query>`. We only care about
1343	// `<path>` - authority is per-instance noise, querystring is the
1344	// `id`/`parentId`/`extensionId`/etc that `index.html` reads via
1345	// `URLSearchParams` (we don't touch it).
1346	let After = match Uri.strip_prefix("vscode-webview://") {
1347		Some(Rest) => Rest,
1348
1349		None => {
1350			return build_error_response(400, "vscode-webview scheme without prefix");
1351		},
1352	};
1353
1354	let PathStart = match After.find('/') {
1355		Some(Index) => Index + 1,
1356
1357		None => {
1358			return build_error_response(400, "vscode-webview URI missing path component");
1359		},
1360	};
1361
1362	let PathPlusQuery = &After[PathStart..];
1363
1364	// Trim the querystring + fragment - filesystem doesn't care.
1365	let CleanPath:&str = PathPlusQuery
1366		.split_once(|C:char| C == '?' || C == '#')
1367		.map(|(Path, _)| Path)
1368		.unwrap_or(PathPlusQuery);
1369
1370	// Reject path-traversal attempts. The webview shell is a static
1371	// three-file directory; anything containing `..` or hitting
1372	// outside `pre/` is hostile or a bug.
1373	if CleanPath.is_empty() || CleanPath.contains("..") {
1374		return build_error_response(404, "vscode-webview path empty or traversal");
1375	}
1376
1377	let ResolvedPath = format!("Static/Application/vs/workbench/contrib/webview/browser/pre/{}", CleanPath);
1378
1379	dev_log!(
1380		"scheme-assets",
1381		"[LandFix:VscodeWebview] resolve {} -> {}",
1382		CleanPath,
1383		ResolvedPath
1384	);
1385
1386	// Try the embedded asset resolver first (release / packaged builds
1387	// where `Sky/Target/Static/Application/` is bundled into Mountain's
1388	// binary). Falls through to the filesystem fallback below for
1389	// debug-electron-bundled, where assets ship next to Mountain.
1390	if let Some(Asset) = AppHandle.asset_resolver().get(ResolvedPath.clone()) {
1391		let Mime = MimeFromExtension(&ResolvedPath);
1392
1393		dev_log!(
1394			"scheme-assets",
1395			"[LandFix:VscodeWebview] serve embedded {} ({}, {} bytes)",
1396			ResolvedPath,
1397			Mime,
1398			Asset.bytes.len()
1399		);
1400
1401		return Builder::new()
1402			.status(200)
1403			.header("Content-Type", Mime)
1404			.header("Access-Control-Allow-Origin", "*")
1405			.header("Cross-Origin-Embedder-Policy", "require-corp")
1406			.header("Cross-Origin-Resource-Policy", "cross-origin")
1407			.header("Cache-Control", "no-cache")
1408			.body(Asset.bytes.to_vec())
1409			.unwrap_or_else(|_| build_error_response(500, "Failed to build response"));
1410	}
1411
1412	// Filesystem fallback for dev mode. `ApplicationRoot` is set by
1413	// `Binary/Main/AppLifecycle.rs` to the resolved `Sky/Target/`
1414	// directory at startup so we can read the same `pre/` files the
1415	// embedded resolver would have served.
1416	let StaticRoot = crate::IPC::WindServiceHandlers::Utilities::ApplicationRoot::Get::Fn();
1417
1418	if let Some(Root) = StaticRoot {
1419		let FilesystemPath = std::path::Path::new(&Root).join(&ResolvedPath);
1420
1421		if FilesystemPath.exists() && FilesystemPath.is_file() {
1422			match std::fs::read(&FilesystemPath) {
1423				Ok(Bytes) => {
1424					let Mime = MimeFromExtension(&ResolvedPath);
1425
1426					dev_log!(
1427						"scheme-assets",
1428						"[LandFix:VscodeWebview] serve filesystem {} ({}, {} bytes)",
1429						FilesystemPath.display(),
1430						Mime,
1431						Bytes.len()
1432					);
1433
1434					return Builder::new()
1435						.status(200)
1436						.header("Content-Type", Mime)
1437						.header("Access-Control-Allow-Origin", "*")
1438						.header("Cross-Origin-Embedder-Policy", "require-corp")
1439						.header("Cross-Origin-Resource-Policy", "cross-origin")
1440						.header("Cache-Control", "no-cache")
1441						.body(Bytes)
1442						.unwrap_or_else(|_| build_error_response(500, "Failed to build response"));
1443				},
1444
1445				Err(Error) => {
1446					dev_log!(
1447						"lifecycle",
1448						"warn: [LandFix:VscodeWebview] Failed to read {}: {}",
1449						FilesystemPath.display(),
1450						Error
1451					);
1452				},
1453			}
1454		}
1455	}
1456
1457	dev_log!(
1458		"lifecycle",
1459		"warn: [LandFix:VscodeWebview] Not found: {} (resolved: {})",
1460		Uri,
1461		ResolvedPath
1462	);
1463
1464	build_error_response(404, &format!("Not Found: {}", ResolvedPath))
1465}