1use std::{collections::HashMap, sync::RwLock};
36
37use tauri::http::{
38 Method,
39 request::Request,
40 response::{Builder, Response},
41};
42
43use super::ServiceRegistry::ServiceRegistry;
44use crate::dev_log;
45
46static SERVICE_REGISTRY:RwLock<Option<ServiceRegistry>> = RwLock::new(None);
48
49pub fn init_service_registry(registry:ServiceRegistry) {
54 let mut registry_lock = SERVICE_REGISTRY.write().unwrap();
55 *registry_lock = Some(registry);
56}
57
58fn get_service_registry() -> Option<ServiceRegistry> {
70 let guard = SERVICE_REGISTRY.read().ok()?;
71 guard.clone()
72}
73
74#[derive(Clone, Debug)]
79pub struct DnsPort(pub u16);
80
81#[derive(Clone)]
83struct CacheEntry {
84 body:Vec<u8>,
86 content_type:String,
88 cache_control:String,
90 etag:Option<String>,
92 last_modified:Option<String>,
94}
95
96static CACHE:RwLock<Option<HashMap<String, CacheEntry>>> = RwLock::new(None);
104
105fn init_cache() {
107 let mut cache = CACHE.write().unwrap();
108 if cache.is_none() {
109 *cache = Some(HashMap::new());
110 }
111}
112
113fn get_cached(path:&str) -> Option<CacheEntry> {
115 let cache = CACHE.read().unwrap();
116 cache.as_ref()?.get(path).cloned()
117}
118
119fn set_cached(path:&str, entry:CacheEntry) {
121 let mut cache = CACHE.write().unwrap();
122 if let Some(cache) = cache.as_mut() {
123 cache.insert(path.to_string(), entry);
124 }
125}
126
127fn should_cache(path:&str) -> bool {
131 let path_lower = path.to_lowercase();
132 path_lower.ends_with(".css")
133 || path_lower.ends_with(".js")
134 || path_lower.ends_with(".png")
135 || path_lower.ends_with(".jpg")
136 || path_lower.ends_with(".jpeg")
137 || path_lower.ends_with(".gif")
138 || path_lower.ends_with(".svg")
139 || path_lower.ends_with(".woff")
140 || path_lower.ends_with(".woff2")
141 || path_lower.ends_with(".ttf")
142 || path_lower.ends_with(".eot")
143 || path_lower.ends_with(".ico")
144}
145
146fn parse_land_uri(uri:&str) -> Result<(String, String), String> {
166 let without_scheme = uri
168 .strip_prefix("land://")
169 .ok_or_else(|| format!("Invalid land:// URI: {}", uri))?;
170
171 let parts:Vec<&str> = without_scheme.splitn(2, '/').collect();
173
174 let domain = parts.get(0).ok_or_else(|| format!("No domain in URI: {}", uri))?.to_string();
175
176 let path = if parts.len() > 1 { format!("/{}", parts[1]) } else { "/".to_string() };
177
178 dev_log!("lifecycle", "[Scheme] Parsed URI: {} -> domain={}, path={}", uri, domain, path);
179 Ok((domain, path))
180}
181
182fn forward_http_request(
194 url:&str,
195 request:&Request<Vec<u8>>,
196 method:Method,
197) -> Result<(u16, Vec<u8>, HashMap<String, String>), String> {
198 let parsed_url = url.parse::<http::uri::Uri>().map_err(|e| format!("Invalid URL: {}", e))?;
200
201 let host = parsed_url.host().ok_or("No host in URL")?.to_string();
203 let port = parsed_url.port_u16().unwrap_or(80);
204 let path = parsed_url
205 .path_and_query()
206 .map(|p| p.as_str().to_string())
207 .unwrap_or_else(|| "/".to_string());
208
209 let addr = format!("{}:{}", host, port);
210
211 dev_log!("lifecycle", "[Scheme] Connecting to {} at {}", url, addr);
212
213 let body = request.body().clone();
215 let headers:Vec<(String, String)> = request
216 .headers()
217 .iter()
218 .filter_map(|(name, value)| {
219 let header_name = name.as_str().to_lowercase();
220 let hop_by_hop_headers = [
221 "connection",
222 "keep-alive",
223 "proxy-authenticate",
224 "proxy-authorization",
225 "te",
226 "trailers",
227 "transfer-encoding",
228 "upgrade",
229 ];
230 if !hop_by_hop_headers.contains(&header_name.as_str()) {
231 value.to_str().ok().map(|v| (name.as_str().to_string(), v.to_string()))
232 } else {
233 None
234 }
235 })
236 .collect();
237
238 let result = std::thread::spawn(move || {
240 let rt = tokio::runtime::Runtime::new().map_err(|e| format!("Failed to create runtime: {}", e))?;
241
242 rt.block_on(async {
243 use tokio::{
244 io::{AsyncReadExt, AsyncWriteExt},
245 net::TcpStream,
246 };
247
248 let mut stream = TcpStream::connect(&addr)
250 .await
251 .map_err(|e| format!("Failed to connect: {}", e))?;
252
253 let mut request_str = format!("{} {} HTTP/1.1\r\nHost: {}\r\n", method.as_str(), path, host);
255
256 for (name, value) in &headers {
258 request_str.push_str(&format!("{}: {}\r\n", name, value));
259 }
260
261 if !body.is_empty() {
263 request_str.push_str(&format!("Content-Length: {}\r\n", body.len()));
264 }
265
266 request_str.push_str("\r\n");
267
268 stream
270 .write_all(request_str.as_bytes())
271 .await
272 .map_err(|e| format!("Failed to write request: {}", e))?;
273
274 if !body.is_empty() {
275 stream
276 .write_all(&body)
277 .await
278 .map_err(|e| format!("Failed to write body: {}", e))?;
279 }
280
281 let mut buffer = Vec::new();
283 let mut temp_buf = [0u8; 8192];
284
285 loop {
286 let n = stream
287 .read(&mut temp_buf)
288 .await
289 .map_err(|e| format!("Failed to read response: {}", e))?;
290
291 if n == 0 {
292 break;
293 }
294
295 buffer.extend_from_slice(&temp_buf[..n]);
296
297 if buffer.len() > 1024 * 1024 {
300 dev_log!("lifecycle", "warn: [Scheme] Response too large, truncating");
302 break;
303 }
304
305 if let Some(headers_end) = buffer.windows(4).position(|w| w == b"\r\n\r\n") {
308 let headers = String::from_utf8_lossy(&buffer[..headers_end]);
309 if let Some(cl_line) = headers.lines().find(|l| l.to_lowercase().starts_with("content-length:")) {
310 if let Ok(cl) = cl_line.trim_start_matches("content-length:").trim().parse::<usize>() {
311 let body_expected = headers_end + 4 + cl;
312 if buffer.len() >= body_expected {
313 break;
314 }
315 }
316 } else if !headers.contains("Transfer-Encoding: chunked") {
317 continue;
319 }
320 }
321 }
322
323 let response_str = String::from_utf8_lossy(&buffer);
325 parse_http_response(&response_str)
326 })
327 })
328 .join()
329 .map_err(|e| format!("Thread panicked: {:?}", e))?;
330
331 result
332}
333
334fn parse_http_response(response:&str) -> Result<(u16, Vec<u8>, HashMap<String, String>), String> {
336 let headers_end = response
338 .find("\r\n\r\n")
339 .ok_or("Invalid HTTP response: no headers/body separator")?;
340
341 let headers_str = &response[..headers_end];
342 let body = response[headers_end + 4..].as_bytes().to_vec();
343
344 let mut lines = headers_str.lines();
346 let status_line = lines.next().ok_or("Invalid HTTP response: no status line")?;
347
348 let status = status_line
350 .split_whitespace()
351 .nth(1)
352 .and_then(|s| s.parse::<u16>().ok())
353 .ok_or_else(|| format!("Invalid status line: {}", status_line))?;
354
355 let mut headers = HashMap::new();
357 for line in lines {
358 if let Some((name, value)) = line.split_once(':') {
359 headers.insert(name.trim().to_lowercase(), value.trim().to_string());
360 }
361 }
362
363 Ok((status, body, headers))
364}
365
366pub fn land_scheme_handler(request:&Request<Vec<u8>>) -> Response<Vec<u8>> {
406 init_cache();
408
409 let uri = request.uri().to_string();
411 dev_log!("lifecycle", "[Scheme] Handling land:// request: {}", uri);
412
413 let (domain, path) = match parse_land_uri(&uri) {
415 Ok(result) => result,
416 Err(e) => {
417 dev_log!("lifecycle", "error: [Scheme] Failed to parse URI: {}", e);
418 return build_error_response(400, &format!("Bad Request: {}", e));
419 },
420 };
421
422 if request.method() == Method::OPTIONS {
424 dev_log!("lifecycle", "[Scheme] Handling CORS preflight request");
425 return build_cors_preflight_response();
426 }
427
428 if should_cache(&path) {
430 if let Some(cached) = get_cached(&path) {
431 dev_log!("lifecycle", "[Scheme] Cache hit for: {}", path);
432 return build_cached_response(cached);
433 }
434 }
435
436 let registry = match get_service_registry() {
438 Some(r) => r,
439 None => {
440 dev_log!("lifecycle", "error: [Scheme] Service registry not initialized");
441 return build_error_response(503, "Service Unavailable: Registry not initialized");
442 },
443 };
444
445 let service = match registry.lookup(&domain) {
446 Some(s) => s,
447 None => {
448 dev_log!("lifecycle", "warn: [Scheme] Service not found: {}", domain);
449 return build_error_response(404, &format!("Not Found: Service {} not registered", domain));
450 },
451 };
452
453 let local_url = format!("http://127.0.0.1:{}{}", service.port, path);
455
456 dev_log!(
457 "lifecycle",
458 "[Scheme] Routing {} {} to local service at {}",
459 request.method(),
460 uri,
461 local_url
462 );
463
464 let result = forward_http_request(&local_url, request, request.method().clone());
466
467 match result {
468 Ok((status, body, headers)) => {
469 let body_bytes = body.clone();
471
472 let LowerPath = path.to_ascii_lowercase();
483 let IsAssetRequest = LowerPath.ends_with(".js")
484 || LowerPath.ends_with(".mjs")
485 || LowerPath.ends_with(".cjs")
486 || LowerPath.ends_with(".json")
487 || LowerPath.ends_with(".map")
488 || LowerPath.ends_with(".css")
489 || LowerPath.ends_with(".wasm")
490 || LowerPath.ends_with(".svg")
491 || LowerPath.ends_with(".png")
492 || LowerPath.ends_with(".woff")
493 || LowerPath.ends_with(".woff2")
494 || LowerPath.ends_with(".ttf")
495 || LowerPath.ends_with(".otf");
496 let UpstreamSaysHtml = headers
497 .get("content-type")
498 .map(|V| V.to_ascii_lowercase().contains("text/html"))
499 .unwrap_or(false);
500 if IsAssetRequest && (status == 404 || (status >= 400 && UpstreamSaysHtml)) {
501 dev_log!(
502 "scheme-assets",
503 "[LandFix:Mime] swap HTML 404 → text/plain empty for asset path={} status={}",
504 path,
505 status
506 );
507 return Builder::new()
508 .status(404)
509 .header("Content-Type", "text/plain; charset=utf-8")
510 .header("Access-Control-Allow-Origin", "land://code.editor.land")
511 .body(Vec::<u8>::new())
512 .unwrap_or_else(|_| build_error_response(500, "Failed to build 404 response"));
513 }
514
515 let mut response_builder = Builder::new()
517 .status(status)
518 .header("Access-Control-Allow-Origin", "land://code.editor.land")
519 .header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
520 .header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
521
522 let important_headers = [
524 "content-type",
525 "content-length",
526 "etag",
527 "last-modified",
528 "cache-control",
529 "expires",
530 "content-encoding",
531 "content-disposition",
532 "location",
533 ];
534
535 for header_name in &important_headers {
536 if let Some(value) = headers.get(*header_name) {
537 response_builder = response_builder.header(*header_name, value);
538 }
539 }
540
541 let response = response_builder.body(body_bytes);
542
543 if status == 200 && should_cache(&path) {
545 let content_type = headers
546 .get("content-type")
547 .unwrap_or(&"application/octet-stream".to_string())
548 .clone();
549 let cache_control = headers
550 .get("cache-control")
551 .unwrap_or(&"public, max-age=3600".to_string())
552 .clone();
553 let etag = headers.get("etag").cloned();
554 let last_modified = headers.get("last-modified").cloned();
555
556 let entry = CacheEntry { body, content_type, cache_control, etag, last_modified };
557 set_cached(&path, entry);
558 dev_log!("lifecycle", "[Scheme] Cached response for: {}", path);
559 }
560
561 response.unwrap_or_else(|_| build_error_response(500, "Internal Server Error"))
562 },
563 Err(e) => {
564 dev_log!("lifecycle", "error: [Scheme] Failed to forward request: {}", e);
565 build_error_response(503, &format!("Service Unavailable: {}", e))
566 },
567 }
568}
569
570fn build_error_response(status:u16, message:&str) -> Response<Vec<u8>> {
572 let body = serde_json::json!({
573 "error": message,
574 "status": status
575 });
576
577 Builder::new()
578 .status(status)
579 .header("Content-Type", "application/json")
580 .header("Access-Control-Allow-Origin", "land://code.editor.land")
581 .body(serde_json::to_vec(&body).unwrap_or_default())
582 .unwrap_or_else(|_| Builder::new().status(500).body(Vec::new()).unwrap())
583}
584
585fn build_cors_preflight_response() -> Response<Vec<u8>> {
587 Builder::new()
588 .status(204)
589 .header("Access-Control-Allow-Origin", "land://code.editor.land")
590 .header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
591 .header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
592 .header("Access-Control-Max-Age", "86400")
593 .body(Vec::new())
594 .unwrap()
595}
596
597fn build_cached_response(entry:CacheEntry) -> Response<Vec<u8>> {
599 let mut builder = Builder::new()
600 .status(200)
601 .header("Content-Type", &entry.content_type)
602 .header("Access-Control-Allow-Origin", "land://code.editor.land")
603 .header("Cache-Control", &entry.cache_control);
604
605 if let Some(etag) = &entry.etag {
606 builder = builder.header("ETag", etag);
607 }
608
609 if let Some(last_modified) = &entry.last_modified {
610 builder = builder.header("Last-Modified", last_modified);
611 }
612
613 builder
614 .body(entry.body)
615 .unwrap_or_else(|_| build_error_response(500, "Internal Server Error"))
616}
617
618pub fn register_land_service(name:&str, port:u16) {
627 let registry = get_service_registry().expect("Service registry not initialized. Call init_service_registry first.");
628 registry.register(name.to_string(), port, Some("/health".to_string()));
629 dev_log!("lifecycle", "[Scheme] Registered service: {} -> {}", name, port);
630}
631
632pub fn get_land_port(name:&str) -> Option<u16> {
643 let registry = get_service_registry()?;
644 registry.lookup(name).map(|s| s.port)
645}
646
647pub fn land_scheme_handler_async<R:tauri::Runtime>(
680 _ctx:tauri::UriSchemeContext<'_, R>,
681 request:tauri::http::request::Request<Vec<u8>>,
682 responder:tauri::UriSchemeResponder,
683) {
684 std::thread::spawn(move || {
686 let response = land_scheme_handler(&request);
687 responder.respond(response);
688 });
689}
690
691#[allow(dead_code)]
700fn get_cors_origins() -> &'static str {
701 "land://localhost, http://land.localhost, land://code.editor.land"
703}
704
705#[inline]
710pub fn Scheme() {}
711
712fn MimeFromExtension(Path:&str) -> &'static str {
718 if Path.ends_with(".js") || Path.ends_with(".mjs") {
719 "application/javascript"
720 } else if Path.ends_with(".css") {
721 "text/css"
722 } else if Path.ends_with(".html") || Path.ends_with(".htm") {
723 "text/html"
724 } else if Path.ends_with(".json") {
725 "application/json"
726 } else if Path.ends_with(".svg") {
727 "image/svg+xml"
728 } else if Path.ends_with(".png") {
729 "image/png"
730 } else if Path.ends_with(".jpg") || Path.ends_with(".jpeg") {
731 "image/jpeg"
732 } else if Path.ends_with(".gif") {
733 "image/gif"
734 } else if Path.ends_with(".woff") {
735 "font/woff"
736 } else if Path.ends_with(".woff2") {
737 "font/woff2"
738 } else if Path.ends_with(".ttf") {
739 "font/ttf"
740 } else if Path.ends_with(".wasm") {
741 "application/wasm"
742 } else if Path.ends_with(".map") {
743 "application/json"
744 } else if Path.ends_with(".txt") || Path.ends_with(".md") {
745 "text/plain"
746 } else if Path.ends_with(".xml") {
747 "application/xml"
748 } else {
749 "application/octet-stream"
750 }
751}
752
753pub fn VscodeFileSchemeHandler<R:tauri::Runtime>(
782 AppHandle:&tauri::AppHandle<R>,
783 Request:&tauri::http::request::Request<Vec<u8>>,
784) -> Response<Vec<u8>> {
785 let Uri = Request.uri().to_string();
786 dev_log!("scheme-assets", "[LandFix:VscodeFile] Request: {}", Uri);
792 dev_log!("scheme-assets", "[SchemeAssets] request uri={}", Uri);
793
794 let FilePath = Uri
809 .strip_prefix("vscode-file://vscode-app/")
810 .or_else(|| Uri.strip_prefix("vscode-file://vscode-app"))
811 .or_else(|| {
812 let After = Uri.strip_prefix("vscode-file://")?;
815 let SlashIdx = After.find('/')?;
816 Some(&After[SlashIdx + 1..])
817 })
818 .unwrap_or("");
819
820 let CleanPath = if FilePath.starts_with("Static/Application//out/") {
823 FilePath.replacen("Static/Application//out/", "Static/Application/", 1)
824 } else if FilePath.starts_with("Static/Application/out/") {
825 FilePath.replacen("Static/Application/out/", "Static/Application/", 1)
826 } else {
827 FilePath.to_string()
828 };
829
830 let CleanPath = if CleanPath.starts_with("Static/node_modules/") {
834 CleanPath.replacen("Static/node_modules/", "Static/Application/node_modules/", 1)
835 } else {
836 CleanPath
837 };
838
839 if CleanPath.ends_with(".map") {
847 return Builder::new()
848 .status(204)
849 .header("Access-Control-Allow-Origin", "*")
850 .body(Vec::new())
851 .unwrap_or_else(|_| build_error_response(500, "Failed to build response"));
852 }
853
854 if CleanPath.ends_with(".css") {
866 let LocalPath = format!("/Static/Application/{}", CleanPath.trim_start_matches("Static/Application/"));
867 let Body = format!("globalThis._LOAD_CSS_WORKER?.({:?}); export default {{}};", LocalPath);
868 dev_log!(
869 "scheme-assets",
870 "[LandFix:VscodeFile] css-shim {} -> _LOAD_CSS_WORKER({})",
871 CleanPath,
872 LocalPath
873 );
874 return Builder::new()
875 .status(200)
876 .header("Content-Type", "application/javascript; charset=utf-8")
877 .header("Access-Control-Allow-Origin", "*")
878 .header("Cache-Control", "public, max-age=31536000, immutable")
879 .body(Body.into_bytes())
880 .unwrap_or_else(|_| build_error_response(500, "Failed to build response"));
881 }
882
883 let IsAbsoluteOSPath = [
891 "Volumes/",
892 "Users/",
893 "Library/",
894 "System/",
895 "Applications/",
896 "private/",
897 "tmp/",
898 "var/",
899 "etc/",
900 "opt/",
901 "home/",
902 "usr/",
903 "srv/",
904 "mnt/",
905 "root/",
906 ]
907 .iter()
908 .any(|Prefix| CleanPath.starts_with(Prefix));
909
910 if IsAbsoluteOSPath {
911 let AbsolutePath = format!("/{}", CleanPath);
912 let FilesystemPath = std::path::Path::new(&AbsolutePath);
913 dev_log!(
914 "scheme-assets",
915 "[LandFix:VscodeFile] os-abs candidate {} (exists={}, is_file={})",
916 AbsolutePath,
917 FilesystemPath.exists(),
918 FilesystemPath.is_file()
919 );
920 if FilesystemPath.exists() && FilesystemPath.is_file() {
921 match crate::Cache::AssetMemoryMap::LoadOrInsert::Fn(FilesystemPath) {
927 Ok(Entry) => {
928 let AcceptsBrotli = Request
929 .headers()
930 .get("accept-encoding")
931 .and_then(|V| V.to_str().ok())
932 .map(|S| S.contains("br"))
933 .unwrap_or(false);
934 let (Body, Encoding):(Vec<u8>, Option<&str>) = if AcceptsBrotli {
935 match Entry.AsBrotliSlice() {
936 Some(Slice) => (Slice.to_vec(), Some("br")),
937 None => (Entry.AsSlice().to_vec(), None),
938 }
939 } else {
940 (Entry.AsSlice().to_vec(), None)
941 };
942 dev_log!(
943 "scheme-assets",
944 "[LandFix:VscodeFile] os-abs served {} ({}, {} bytes, encoding={:?})",
945 AbsolutePath,
946 Entry.Mime,
947 Body.len(),
948 Encoding
949 );
950 let mut B = Builder::new()
951 .status(200)
952 .header("Content-Type", Entry.Mime)
953 .header("Access-Control-Allow-Origin", "*")
954 .header("Cache-Control", "public, max-age=3600");
955 if let Some(Enc) = Encoding {
956 B = B.header("Content-Encoding", Enc);
957 }
958 return B
959 .body(Body)
960 .unwrap_or_else(|_| build_error_response(500, "Failed to build response"));
961 },
962 Err(Error) => {
963 dev_log!(
964 "lifecycle",
965 "warn: [LandFix:VscodeFile] os-abs mmap failure {}: {}",
966 AbsolutePath,
967 Error
968 );
969 },
970 }
971 } else {
972 dev_log!("lifecycle", "warn: [LandFix:VscodeFile] os-abs not on disk: {}", AbsolutePath);
973 }
974 }
975
976 dev_log!("lifecycle", "[LandFix:VscodeFile] Resolved path: {}", CleanPath);
977
978 let AssetResult = AppHandle.asset_resolver().get(CleanPath.clone());
982
983 if let Some(Asset) = AssetResult {
984 let Mime = MimeFromExtension(&CleanPath);
985
986 dev_log!(
987 "lifecycle",
988 "[LandFix:VscodeFile] Serving (embedded) {} ({}, {} bytes)",
989 CleanPath,
990 Mime,
991 Asset.bytes.len()
992 );
993 dev_log!(
994 "scheme-assets",
995 "[SchemeAssets] serve source=embedded path={} mime={} bytes={}",
996 CleanPath,
997 Mime,
998 Asset.bytes.len()
999 );
1000
1001 return Builder::new()
1002 .status(200)
1003 .header("Content-Type", Mime)
1004 .header("Access-Control-Allow-Origin", "*")
1005 .header("Cache-Control", "public, max-age=31536000, immutable")
1006 .body(Asset.bytes.to_vec())
1007 .unwrap_or_else(|_| build_error_response(500, "Failed to build response"));
1008 }
1009
1010 let StaticRoot = crate::IPC::WindServiceHandlers::Utilities::ApplicationRoot::get_static_application_root();
1012
1013 if let Some(Root) = StaticRoot {
1014 let FilesystemPath = std::path::Path::new(&Root).join(&CleanPath);
1015
1016 if FilesystemPath.exists() && FilesystemPath.is_file() {
1017 match crate::Cache::AssetMemoryMap::LoadOrInsert::Fn(&FilesystemPath) {
1021 Ok(Entry) => {
1022 let AcceptsBrotli = Request
1023 .headers()
1024 .get("accept-encoding")
1025 .and_then(|V| V.to_str().ok())
1026 .map(|S| S.contains("br"))
1027 .unwrap_or(false);
1028 let (Body, Encoding):(Vec<u8>, Option<&str>) = if AcceptsBrotli {
1029 match Entry.AsBrotliSlice() {
1030 Some(Slice) => (Slice.to_vec(), Some("br")),
1031 None => (Entry.AsSlice().to_vec(), None),
1032 }
1033 } else {
1034 (Entry.AsSlice().to_vec(), None)
1035 };
1036 dev_log!(
1037 "lifecycle",
1038 "[LandFix:VscodeFile] Serving (fs-mmap) {} ({}, {} bytes, encoding={:?})",
1039 CleanPath,
1040 Entry.Mime,
1041 Body.len(),
1042 Encoding
1043 );
1044 let mut B = Builder::new()
1045 .status(200)
1046 .header("Content-Type", Entry.Mime)
1047 .header("Access-Control-Allow-Origin", "*")
1048 .header("Cache-Control", "public, max-age=3600");
1049 if let Some(Enc) = Encoding {
1050 B = B.header("Content-Encoding", Enc);
1051 }
1052 return B
1053 .body(Body)
1054 .unwrap_or_else(|_| build_error_response(500, "Failed to build response"));
1055 },
1056 Err(Error) => {
1057 dev_log!(
1058 "lifecycle",
1059 "warn: [LandFix:VscodeFile] Failed to read {}: {}",
1060 FilesystemPath.display(),
1061 Error
1062 );
1063 },
1064 }
1065 }
1066 }
1067
1068 dev_log!(
1069 "lifecycle",
1070 "warn: [LandFix:VscodeFile] Not found: {} (resolved: {})",
1071 Uri,
1072 CleanPath
1073 );
1074 build_error_response(404, &format!("Not Found: {}", CleanPath))
1075}
1076
1077pub fn VscodeWebviewSchemeHandler<R:tauri::Runtime>(
1130 AppHandle:&tauri::AppHandle<R>,
1131 Request:&tauri::http::request::Request<Vec<u8>>,
1132) -> Response<Vec<u8>> {
1133 let Uri = Request.uri().to_string();
1134 dev_log!("scheme-assets", "[LandFix:VscodeWebview] Request: {}", Uri);
1135
1136 let After = match Uri.strip_prefix("vscode-webview://") {
1141 Some(Rest) => Rest,
1142 None => {
1143 return build_error_response(400, "vscode-webview scheme without prefix");
1144 },
1145 };
1146 let PathStart = match After.find('/') {
1147 Some(Index) => Index + 1,
1148 None => {
1149 return build_error_response(400, "vscode-webview URI missing path component");
1150 },
1151 };
1152 let PathPlusQuery = &After[PathStart..];
1153 let CleanPath:&str = PathPlusQuery
1155 .split_once(|C:char| C == '?' || C == '#')
1156 .map(|(Path, _)| Path)
1157 .unwrap_or(PathPlusQuery);
1158 if CleanPath.is_empty() || CleanPath.contains("..") {
1162 return build_error_response(404, "vscode-webview path empty or traversal");
1163 }
1164
1165 let ResolvedPath = format!("Static/Application/vs/workbench/contrib/webview/browser/pre/{}", CleanPath);
1166 dev_log!(
1167 "scheme-assets",
1168 "[LandFix:VscodeWebview] resolve {} -> {}",
1169 CleanPath,
1170 ResolvedPath
1171 );
1172
1173 if let Some(Asset) = AppHandle.asset_resolver().get(ResolvedPath.clone()) {
1178 let Mime = MimeFromExtension(&ResolvedPath);
1179 dev_log!(
1180 "scheme-assets",
1181 "[LandFix:VscodeWebview] serve embedded {} ({}, {} bytes)",
1182 ResolvedPath,
1183 Mime,
1184 Asset.bytes.len()
1185 );
1186 return Builder::new()
1187 .status(200)
1188 .header("Content-Type", Mime)
1189 .header("Access-Control-Allow-Origin", "*")
1190 .header("Cross-Origin-Embedder-Policy", "require-corp")
1191 .header("Cross-Origin-Resource-Policy", "cross-origin")
1192 .header("Cache-Control", "no-cache")
1193 .body(Asset.bytes.to_vec())
1194 .unwrap_or_else(|_| build_error_response(500, "Failed to build response"));
1195 }
1196
1197 let StaticRoot = crate::IPC::WindServiceHandlers::Utilities::ApplicationRoot::get_static_application_root();
1202 if let Some(Root) = StaticRoot {
1203 let FilesystemPath = std::path::Path::new(&Root).join(&ResolvedPath);
1204 if FilesystemPath.exists() && FilesystemPath.is_file() {
1205 match std::fs::read(&FilesystemPath) {
1206 Ok(Bytes) => {
1207 let Mime = MimeFromExtension(&ResolvedPath);
1208 dev_log!(
1209 "scheme-assets",
1210 "[LandFix:VscodeWebview] serve filesystem {} ({}, {} bytes)",
1211 FilesystemPath.display(),
1212 Mime,
1213 Bytes.len()
1214 );
1215 return Builder::new()
1216 .status(200)
1217 .header("Content-Type", Mime)
1218 .header("Access-Control-Allow-Origin", "*")
1219 .header("Cross-Origin-Embedder-Policy", "require-corp")
1220 .header("Cross-Origin-Resource-Policy", "cross-origin")
1221 .header("Cache-Control", "no-cache")
1222 .body(Bytes)
1223 .unwrap_or_else(|_| build_error_response(500, "Failed to build response"));
1224 },
1225 Err(Error) => {
1226 dev_log!(
1227 "lifecycle",
1228 "warn: [LandFix:VscodeWebview] Failed to read {}: {}",
1229 FilesystemPath.display(),
1230 Error
1231 );
1232 },
1233 }
1234 }
1235 }
1236
1237 dev_log!(
1238 "lifecycle",
1239 "warn: [LandFix:VscodeWebview] Not found: {} (resolved: {})",
1240 Uri,
1241 ResolvedPath
1242 );
1243 build_error_response(404, &format!("Not Found: {}", ResolvedPath))
1244}