Skip to main content

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