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