1use 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
50static SERVICE_REGISTRY:RwLock<Option<ServiceRegistry>> = RwLock::new(None);
52
53pub fn init_service_registry(registry:ServiceRegistry) {
58 let mut registry_lock = SERVICE_REGISTRY.write().unwrap();
59
60 *registry_lock = Some(registry);
61}
62
63fn get_service_registry() -> Option<ServiceRegistry> {
75 let guard = SERVICE_REGISTRY.read().ok()?;
76
77 guard.clone()
78}
79
80#[derive(Clone, Debug)]
85pub struct DnsPort(pub u16);
86
87#[derive(Clone)]
89struct CacheEntry {
90 body:Vec<u8>,
92
93 content_type:String,
95
96 cache_control:String,
98
99 etag:Option<String>,
101
102 last_modified:Option<String>,
104}
105
106static CACHE:RwLock<Option<HashMap<String, CacheEntry>>> = RwLock::new(None);
114
115fn init_cache() {
117 let mut cache = CACHE.write().unwrap();
118
119 if cache.is_none() {
120 *cache = Some(HashMap::new());
121 }
122}
123
124fn get_cached(path:&str) -> Option<CacheEntry> {
126 let cache = CACHE.read().unwrap();
127
128 cache.as_ref()?.get(path).cloned()
129}
130
131fn 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
140fn 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
160fn parse_land_uri(uri:&str) -> Result<(String, String), String> {
180 let without_scheme = uri
182 .strip_prefix("land://")
183 .ok_or_else(|| format!("Invalid land:// URI: {}", uri))?;
184
185 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
197fn 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 let parsed_url = url.parse::<http::uri::Uri>().map_err(|e| format!("Invalid URL: {}", e))?;
217
218 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 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 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 let mut stream = TcpStream::connect(&addr)
270 .await
271 .map_err(|e| format!("Failed to connect: {}", e))?;
272
273 let mut request_str = format!("{} {} HTTP/1.1\r\nHost: {}\r\n", method.as_str(), path, host);
275
276 for (name, value) in &headers {
278 request_str.push_str(&format!("{}: {}\r\n", name, value));
279 }
280
281 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 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 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 if buffer.len() > 1024 * 1024 {
320 dev_log!("lifecycle", "warn: [Scheme] Response too large, truncating");
322 break;
323 }
324
325 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 continue;
339 }
340 }
341 }
342
343 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
354fn parse_http_response(response:&str) -> Result<(u16, Vec<u8>, HashMap<String, String>), String> {
356 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 let mut lines = headers_str.lines();
367
368 let status_line = lines.next().ok_or("Invalid HTTP response: no status line")?;
369
370 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 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
389pub fn land_scheme_handler(request:&Request<Vec<u8>>) -> Response<Vec<u8>> {
429 init_cache();
431
432 let uri = request.uri().to_string();
434
435 dev_log!("lifecycle", "[Scheme] Handling land:// request: {}", uri);
436
437 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 if request.method() == Method::OPTIONS {
450 dev_log!("lifecycle", "[Scheme] Handling CORS preflight request");
451
452 return build_cors_preflight_response();
453 }
454
455 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 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 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 let result = forward_http_request(&local_url, request, request.method().clone());
498
499 match result {
500 Ok((status, body, headers)) => {
501 let body_bytes = body.clone();
503
504 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 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 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 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
613fn 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
628fn 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
640fn 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
661pub 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
677pub 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
693pub 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 std::thread::spawn(move || {
734 let response = land_scheme_handler(&request);
735 responder.respond(response);
736 });
737}
738
739#[allow(dead_code)]
748fn get_cors_origins() -> &'static str {
749 "land://localhost, http://land.localhost, land://code.editor.land"
751}
752
753#[inline]
758pub fn Scheme() {}
759
760fn 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
801pub fn VscodeFileSchemeHandler<R:tauri::Runtime>(
830 AppHandle:&tauri::AppHandle<R>,
831
832 Request:&tauri::http::request::Request<Vec<u8>>,
833) -> Response<Vec<u8>> {
834 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 dev_log!("scheme-assets", "[LandFix:VscodeFile] Request: {}", Uri);
877
878 dev_log!("scheme-assets", "[SchemeAssets] request uri={}", Uri);
879
880 let FilePath = Uri
895 .strip_prefix("vscode-file://vscode-app/")
896 .or_else(|| Uri.strip_prefix("vscode-file://vscode-app"))
897 .or_else(|| {
898 let After = Uri.strip_prefix("vscode-file://")?;
901 let SlashIdx = After.find('/')?;
902 Some(&After[SlashIdx + 1..])
903 })
904 .unwrap_or("");
905
906 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 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 let CleanPath = match CleanPath.split_once(['?', '#']) {
938 Some((Before, _)) => Before.to_string(),
939
940 None => CleanPath,
941 };
942
943 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 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 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 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 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 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 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 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 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
1246pub 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 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 let CleanPath:&str = PathPlusQuery
1361 .split_once(|C:char| C == '?' || C == '#')
1362 .map(|(Path, _)| Path)
1363 .unwrap_or(PathPlusQuery);
1364
1365 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 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 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}