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