1use std::ffi::OsString;
4use std::net::{Ipv4Addr, SocketAddr};
5use std::path::{Path, PathBuf};
6
7use eyre::{Context, bail};
8use futures_util::FutureExt as _;
9use smol::{
10 channel::{Sender, bounded},
11 fs,
12 io::{AsyncReadExt, AsyncWriteExt},
13 net::TcpListener,
14};
15use target_lexicon::Triple;
16use tracing::info;
17
18use crate::{
19 assets, browser_runtime,
20 build::{BuildOptions, RustBuild, RustDynamicLibraries, RustLinkage},
21 device::Artifact,
22 hydrolysis::backend::HydrolysisBackend,
23 platform::{PackageOptions, TargetPlatform},
24 project::Project,
25 toolchain::{ToolchainError, windows_arm64_llvm::WindowsArm64LlvmToolchain},
26 utils::{command, run_command_os, which},
27};
28#[cfg(target_os = "macos")]
29use crate::{
30 macos_bundle::{
31 MacOsUsageDescription, package_binary_as_app, package_cef_helper_app,
32 sign_macos_app as sign_app,
33 },
34 project::BrowserRuntimePlan,
35};
36
37#[cfg(target_os = "macos")]
38const HYDROLYSIS_INIT_HINT: &str = "water run --platform macos --backend hydrolysis";
39#[cfg(target_os = "linux")]
40const HYDROLYSIS_INIT_HINT: &str = "water run --platform linux --backend hydrolysis";
41#[cfg(target_os = "windows")]
42const HYDROLYSIS_INIT_HINT: &str = "water run --platform windows --backend hydrolysis";
43#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
44const HYDROLYSIS_INIT_HINT: &str = "initialize hydrolysis backend on macOS, Linux, or Windows";
45
46async fn hydrolysis_profile_dir(
48 project: &Project,
49 platform: TargetPlatform,
50 profile: &str,
51 linkage: RustLinkage,
52) -> eyre::Result<PathBuf> {
53 Ok(project
54 .water_target_dir(linkage)
55 .await?
56 .join(platform.triple().to_string())
57 .join(profile))
58}
59
60const fn hydrolysis_loader_search_path(platform: TargetPlatform) -> Option<&'static str> {
62 match platform {
63 TargetPlatform::MacOS => Some("@executable_path/../Frameworks"),
64 TargetPlatform::Linux => Some("$ORIGIN"),
65 _ => None,
66 }
67}
68
69pub async fn build_hydrolysis(
74 project: &Project,
75 platform: TargetPlatform,
76 options: BuildOptions,
77) -> eyre::Result<PathBuf> {
78 build_hydrolysis_with_envs_and_features(project, platform, options, &[], &[]).await
79}
80
81pub async fn build_hydrolysis_with_envs(
86 project: &Project,
87 platform: TargetPlatform,
88 options: BuildOptions,
89 extra_envs: &[(String, OsString)],
90) -> eyre::Result<PathBuf> {
91 build_hydrolysis_with_envs_and_features(project, platform, options, extra_envs, &[]).await
92}
93
94pub async fn build_hydrolysis_with_envs_and_features(
99 project: &Project,
100 platform: TargetPlatform,
101 options: BuildOptions,
102 extra_envs: &[(String, OsString)],
103 extra_features: &[&str],
104) -> eyre::Result<PathBuf> {
105 if !is_hydrolysis_native_platform(platform) {
106 bail!("Hydrolysis backend is only supported on macOS, Linux, and Windows");
107 }
108
109 let backend_path = project.backend_path::<HydrolysisBackend>();
110 let cargo_toml = backend_path.join("Cargo.toml");
111
112 if !cargo_toml.exists() {
113 bail!(
114 "Hydrolysis backend not found at {}. Run `{HYDROLYSIS_INIT_HINT}` to initialize it.",
115 backend_path.display(),
116 );
117 }
118
119 copy_assets_and_fonts(
122 project,
123 &backend_path,
124 options.sccache_path(),
125 options.uses_dev_server(),
126 )
127 .await?;
128
129 let llvm_envs = WindowsArm64LlvmToolchain
130 .cargo_envs(&crate::toolchain::Host::current())
131 .await
132 .map_err(|error| match error {
133 ToolchainError::Fixable(_) => eyre::eyre!(
134 "Windows ARM64 LLVM toolchain is missing. Run `water doctor --fix` to install it automatically."
135 ),
136 ToolchainError::Unfixable(unfixable) => {
137 eyre::eyre!("Windows ARM64 LLVM toolchain check failed: {unfixable}")
138 }
139 })?;
140
141 let mut build = RustBuild::new(&backend_path, platform.triple())
142 .with_project(project)
143 .with_target_dir(project.water_target_dir(options.linkage()).await?)
144 .with_features(extra_features.iter().copied())
145 .with_linkage(
146 options.linkage(),
147 &format!("{}/dev", project.crate_name()),
148 hydrolysis_loader_search_path(platform),
149 )
150 .with_envs(llvm_envs)
151 .with_envs(options.cargo_envs().iter().cloned())
152 .with_envs(extra_envs.iter().cloned());
153 if let Some(sccache_path) = options.sccache_path() {
154 build = build.with_sccache(sccache_path.to_path_buf());
155 }
156 build
157 .build_binary(
158 project.hydrolysis_backend_crate_name().as_str(),
159 options.is_release(),
160 )
161 .await
162 .wrap_err("Failed to build hydrolysis backend with cargo")?;
163
164 build
165 .lib_output_dir(options.is_release())
166 .await
167 .map_err(Into::into)
168}
169
170pub async fn built_hydrolysis_binary_path(
175 project: &Project,
176 platform: TargetPlatform,
177 profile: &str,
178 linkage: RustLinkage,
179) -> eyre::Result<PathBuf> {
180 let target_dir = hydrolysis_profile_dir(project, platform, profile, linkage).await?;
181 let binary_name = project.hydrolysis_backend_crate_name();
182 let binary_path = if cfg!(windows) {
183 target_dir.join(format!("{binary_name}.exe"))
184 } else {
185 target_dir.join(binary_name.as_ref())
186 };
187 if binary_path.exists() {
188 return Ok(binary_path);
189 }
190
191 let alt_binary_name = binary_name.replace('-', "_");
192 let underscored_path = if cfg!(windows) {
193 target_dir.join(format!("{alt_binary_name}.exe"))
194 } else {
195 target_dir.join(alt_binary_name)
196 };
197 if underscored_path.exists() {
198 return Ok(underscored_path);
199 }
200
201 bail!(
202 "Built hydrolysis binary not found at {} or {}",
203 binary_path.display(),
204 underscored_path.display()
205 );
206}
207
208pub(crate) async fn stage_hydrolysis_shared_runtime(
219 binary_path: &Path,
220 platform: TargetPlatform,
221) -> eyre::Result<()> {
222 if !is_hydrolysis_native_platform(platform) {
223 bail!("Hydrolysis shared runtime can only be staged for macOS, Linux, and Windows");
224 }
225 let runtime_dir = binary_path.parent().ok_or_else(|| {
226 eyre::eyre!(
227 "Hydrolysis binary path has no output directory: {}",
228 binary_path.display()
229 )
230 })?;
231 let libraries = RustDynamicLibraries::resolve(runtime_dir, &platform.triple()).await?;
232 synchronize_shared_runtime(runtime_dir, Some(&libraries), &platform.triple()).await
233}
234
235pub async fn clean_hydrolysis(project: &Project) -> eyre::Result<()> {
240 let backend_path = project.backend_path::<HydrolysisBackend>();
241 let cargo_toml = backend_path.join("Cargo.toml");
242
243 if !cargo_toml.exists() {
244 return Ok(());
245 }
246
247 for linkage in [RustLinkage::SharedRuntime, RustLinkage::Static] {
251 let shared_target_dir = project.water_target_dir(linkage).await?;
252 if !shared_target_dir.exists() {
253 continue;
254 }
255 let clean_args: Vec<OsString> = vec![
256 "clean".into(),
257 "--manifest-path".into(),
258 cargo_toml.as_os_str().to_owned(),
259 "--target-dir".into(),
260 shared_target_dir.as_os_str().to_owned(),
261 "--package".into(),
262 project.hydrolysis_backend_crate_name().as_str().into(),
263 ];
264 run_command_os("cargo", clean_args).await?;
265 }
266 let dist_web = backend_path.join("dist/web");
267 if dist_web.exists() {
268 fs::remove_dir_all(&dist_web).await?;
269 }
270 let dist_web_dev = backend_path.join("dist/web-dev");
271 if dist_web_dev.exists() {
272 fs::remove_dir_all(&dist_web_dev).await?;
273 }
274 Ok(())
275}
276
277pub async fn package_hydrolysis(
285 project: &Project,
286 platform: TargetPlatform,
287 options: PackageOptions,
288) -> eyre::Result<Artifact> {
289 if platform == TargetPlatform::Web {
290 let site_root = package_hydrolysis_web_site(project, options.is_debug(), false).await?;
291 return Ok(Artifact::new(project.bundle_identifier(), site_root));
292 }
293
294 if !is_hydrolysis_native_platform(platform) {
295 bail!(
296 "Hydrolysis backend is only supported on macOS, Linux, and Windows for binary packaging"
297 );
298 }
299
300 let profile = if options.is_debug() {
301 "debug"
302 } else {
303 "release"
304 };
305 let backend_path = project.backend_path::<HydrolysisBackend>();
306 copy_assets_and_fonts(project, &backend_path, None, options.uses_dev_server()).await?;
307
308 let linkage = if options.uses_shared_rust_runtime() {
309 RustLinkage::SharedRuntime
310 } else {
311 RustLinkage::Static
312 };
313 let final_binary_path =
314 built_hydrolysis_binary_path(project, platform, profile, linkage).await?;
315 let profile_directory = final_binary_path.parent().ok_or_else(|| {
316 eyre::eyre!(
317 "Hydrolysis binary path has no profile directory: {}",
318 final_binary_path.display()
319 )
320 })?;
321 let runtime_plan = project
322 .browser_runtime_plan(platform, crate::platform::TargetBackend::Hydrolysis)
323 .await?;
324 let shared_libraries = if options.uses_shared_rust_runtime() {
325 let lib_dir = final_binary_path.parent().ok_or_else(|| {
326 eyre::eyre!(
327 "Hydrolysis binary path has no output directory: {}",
328 final_binary_path.display()
329 )
330 })?;
331 Some(RustDynamicLibraries::resolve(lib_dir, &platform.triple()).await?)
332 } else {
333 None
334 };
335
336 #[cfg(target_os = "macos")]
337 {
338 if platform == TargetPlatform::MacOS {
339 return package_hydrolysis_macos(
340 project,
341 platform,
342 &backend_path,
343 &final_binary_path,
344 profile_directory,
345 runtime_plan,
346 shared_libraries.as_ref(),
347 )
348 .await;
349 }
350 }
351
352 let runtime_dir = final_binary_path.parent().ok_or_else(|| {
353 eyre::eyre!(
354 "Hydrolysis binary path has no output directory: {}",
355 final_binary_path.display()
356 )
357 })?;
358 synchronize_shared_runtime(runtime_dir, shared_libraries.as_ref(), &platform.triple()).await?;
359 browser_runtime::stage(runtime_plan, platform, profile_directory, runtime_dir).await?;
360
361 if platform == TargetPlatform::Linux {
362 let executable_name = final_binary_path
363 .file_name()
364 .and_then(std::ffi::OsStr::to_str)
365 .ok_or_else(|| eyre::eyre!("Hydrolysis binary has no valid executable name"))?;
366 crate::platforming::linux_share::write_linux_share_material(
367 project,
368 &project.manifest().package.name,
369 executable_name,
370 &backend_path.join("dist/share"),
371 )
372 .await?;
373 }
374
375 Ok(Artifact::new(
376 project.bundle_identifier(),
377 final_binary_path,
378 ))
379}
380
381#[cfg(target_os = "macos")]
382async fn package_hydrolysis_macos(
383 project: &Project,
384 platform: TargetPlatform,
385 backend_path: &Path,
386 binary_path: &Path,
387 profile_directory: &Path,
388 runtime_plan: BrowserRuntimePlan,
389 shared_libraries: Option<&RustDynamicLibraries>,
390) -> eyre::Result<Artifact> {
391 let app_name = project
392 .manifest()
393 .package
394 .name
395 .chars()
396 .filter(|character| character.is_alphanumeric() || *character == ' ')
397 .collect::<String>();
398 let app_name = if app_name.is_empty() {
399 "WaterUIHydrolysis".to_string()
400 } else {
401 app_name
402 };
403 let dist_dir = backend_path.join("dist");
404 fs::create_dir_all(&dist_dir).await?;
405 let usage_descriptions = project
406 .manifest()
407 .permissions
408 .iter()
409 .filter(|(_, entry)| entry.is_enabled())
410 .flat_map(|(key, entry)| {
411 key.macos_usage_description_keys()
412 .iter()
413 .map(|&plist_key| MacOsUsageDescription {
414 plist_key,
415 description: entry.description().to_string(),
416 })
417 })
418 .collect::<Vec<_>>();
419 let icns = assets::project_macos_icns(project)?;
420 let app_path = package_binary_as_app(
421 binary_path,
422 project.bundle_identifier(),
423 &app_name,
424 &usage_descriptions,
425 Some(&backend_path.join("resources")),
426 &icns,
427 &dist_dir,
428 )
429 .await?;
430 synchronize_shared_runtime(
431 &app_path.join("Contents/Frameworks"),
432 shared_libraries,
433 &platform.triple(),
434 )
435 .await?;
436 browser_runtime::stage_macos_app(runtime_plan, profile_directory, &app_path.join("Contents"))
437 .await?;
438 if runtime_plan.requires_cef() {
439 let helper_binary = binary_path
440 .parent()
441 .expect("Hydrolysis application binary must have a profile directory")
442 .join("waterui-cef-helper");
443 let _helper_apps = package_cef_helper_app(
444 &app_path,
445 binary_path,
446 &helper_binary,
447 project.bundle_identifier(),
448 )
449 .await?;
450 }
451 sign_app(
452 &app_path,
453 project.bundle_identifier(),
454 !usage_descriptions.is_empty(),
455 )
456 .await?;
457 Ok(Artifact::new(project.bundle_identifier(), app_path))
458}
459
460async fn synchronize_shared_runtime(
461 destination: &Path,
462 libraries: Option<&RustDynamicLibraries>,
463 triple: &Triple,
464) -> eyre::Result<()> {
465 if let Some(libraries) = libraries {
466 libraries.stage(destination).await
467 } else {
468 RustDynamicLibraries::remove_staged(destination, triple).await
469 }
470}
471
472#[must_use]
474pub const fn is_hydrolysis_platform(platform: TargetPlatform) -> bool {
475 matches!(
476 platform,
477 TargetPlatform::Linux
478 | TargetPlatform::MacOS
479 | TargetPlatform::Windows
480 | TargetPlatform::Web
481 )
482}
483
484const fn is_hydrolysis_native_platform(platform: TargetPlatform) -> bool {
485 matches!(
486 platform,
487 TargetPlatform::Linux | TargetPlatform::MacOS | TargetPlatform::Windows
488 )
489}
490
491async fn copy_assets_and_fonts(
492 project: &Project,
493 backend_path: &Path,
494 sccache_path: Option<&Path>,
495 dev_server: bool,
496) -> eyre::Result<()> {
497 let resources_dir = backend_path.join("resources");
498 fs::create_dir_all(&resources_dir).await?;
499 let manifest =
500 assets::stage_project_assets_for_gtk(project, &resources_dir, sccache_path, dev_server)
501 .await?;
502
503 fs::write(
506 backend_path.join("app-icon.ico"),
507 assets::project_windows_ico(project)?,
508 )
509 .await?;
510
511 let mut font_declarations = assets::scan_fonts(project).await?;
512 font_declarations.extend(assets::hydrolysis_default_font_declarations());
513 let mut resolved_fonts = assets::resolve_fonts(font_declarations).await?;
514 resolved_fonts.extend(assets::scan_project_font_assets(&manifest)?);
515 if !resolved_fonts.is_empty() {
516 let fonts_dest = resources_dir.join("fonts");
517 assets::copy_fonts(&resolved_fonts, &fonts_dest).await?;
518 info!(
519 "Copied {} fonts to hydrolysis resources",
520 resolved_fonts.len()
521 );
522 }
523 Ok(())
524}
525
526pub async fn prepare_hydrolysis_web_dev_site(project: &Project) -> eyre::Result<PathBuf> {
531 package_hydrolysis_web_site(project, true, true).await
532}
533
534#[derive(Debug)]
536pub struct HydrolysisWebDevServer {
537 address: SocketAddr,
538 shutdown_tx: Option<Sender<()>>,
539 _task: smol::Task<()>,
540}
541
542impl HydrolysisWebDevServer {
543 pub async fn start(site_root: PathBuf) -> eyre::Result<Self> {
548 let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))
549 .await
550 .wrap_err("Failed to bind Hydrolysis web dev server")?;
551 let address = listener
552 .local_addr()
553 .wrap_err("Failed to resolve Hydrolysis web dev server address")?;
554 let (shutdown_tx, shutdown_rx) = bounded::<()>(1);
555
556 let task = smol::spawn(async move {
557 let shutdown = shutdown_rx.recv().fuse();
558 futures_util::pin_mut!(shutdown);
559
560 loop {
561 let accept = listener.accept().fuse();
562 futures_util::pin_mut!(accept);
563
564 match futures_util::future::select(accept, shutdown.as_mut()).await {
565 futures_util::future::Either::Left((Ok((mut stream, _peer)), _)) => {
566 let site_root = site_root.clone();
567 smol::spawn(async move {
568 if let Err(error) = serve_http_request(&mut stream, &site_root).await {
569 tracing::warn!(
570 target: "waterui::hydrolysis::web",
571 error = %error,
572 "Hydrolysis web dev server request failed"
573 );
574 }
575 })
576 .detach();
577 }
578 futures_util::future::Either::Left((Err(error), _)) => {
579 tracing::warn!(
580 target: "waterui::hydrolysis::web",
581 error = %error,
582 "Hydrolysis web dev server accept failed"
583 );
584 break;
585 }
586 futures_util::future::Either::Right((_shutdown, _)) => break,
587 }
588 }
589 });
590
591 Ok(Self {
592 address,
593 shutdown_tx: Some(shutdown_tx),
594 _task: task,
595 })
596 }
597
598 #[must_use]
600 pub const fn address(&self) -> SocketAddr {
601 self.address
602 }
603}
604
605impl Drop for HydrolysisWebDevServer {
606 fn drop(&mut self) {
607 if let Some(tx) = self.shutdown_tx.take() {
608 let _ = tx.try_send(());
609 }
610 }
611}
612
613async fn package_hydrolysis_web_site(
614 project: &Project,
615 debug: bool,
616 dev_site: bool,
617) -> eyre::Result<PathBuf> {
618 let backend_path = project.backend_path::<HydrolysisBackend>();
619 let cargo_toml = backend_path.join("Cargo.toml");
620 let lib_rs = backend_path.join("src/lib.rs");
621 if !cargo_toml.exists() {
622 bail!(
623 "Hydrolysis backend not found at {}. Run `water backend add hydrolysis` first.",
624 backend_path.display(),
625 );
626 }
627 if !lib_rs.exists() {
628 bail!(
629 "Hydrolysis backend at {} is missing src/lib.rs for web packaging. Re-scaffold the backend and try again.",
630 backend_path.display(),
631 );
632 }
633
634 let site_root = backend_path.join(if dev_site { "dist/web-dev" } else { "dist/web" });
635 if site_root.exists() {
636 fs::remove_dir_all(&site_root).await?;
637 }
638 fs::create_dir_all(&site_root).await?;
639
640 build_hydrolysis_web_bundle(&backend_path, &site_root, debug).await?;
642 super::web_launch::write_web_shell(project, &site_root).await?;
643 copy_web_assets_and_fonts(project, &site_root).await?;
644
645 Ok(site_root)
646}
647
648async fn build_hydrolysis_web_bundle(
649 backend_path: &Path,
650 site_root: &Path,
651 debug: bool,
652) -> eyre::Result<()> {
653 let wasm_pack = which("wasm-pack")
654 .await
655 .wrap_err("wasm-pack is required to build Hydrolysis web bundles")?;
656 let pkg_dir = site_root.join("pkg");
657 fs::create_dir_all(&pkg_dir).await?;
658
659 let mut wasm_pack_cmd = smol::process::Command::new(wasm_pack);
660 let wasm_pack_cmd = command(&mut wasm_pack_cmd);
661 wasm_pack_cmd
662 .current_dir(backend_path)
663 .arg("build")
664 .arg("--target")
665 .arg("web")
666 .arg("--out-dir")
667 .arg(&pkg_dir)
668 .arg("--out-name")
669 .arg("app");
670 if debug {
671 wasm_pack_cmd.arg("--dev");
672 } else {
673 wasm_pack_cmd.arg("--release");
674 }
675
676 let output = wasm_pack_cmd.output().await?;
677 if !output.status.success() {
678 let stderr = String::from_utf8_lossy(&output.stderr);
679 let stdout = String::from_utf8_lossy(&output.stdout);
680 let details = if stderr.trim().is_empty() {
681 stdout.to_string()
682 } else {
683 stderr.to_string()
684 };
685 bail!(
686 "Failed to build Hydrolysis web bundle with wasm-pack (status {}):\n{}",
687 output.status,
688 details
689 );
690 }
691
692 Ok(())
693}
694
695async fn copy_web_assets_and_fonts(project: &Project, site_root: &Path) -> eyre::Result<()> {
696 assets::stage_project_assets_for_web(project, site_root).await?;
697 assets::stage_hydrolysis_web_fonts(project, site_root).await?;
698 Ok(())
699}
700
701async fn serve_http_request(
702 stream: &mut smol::net::TcpStream,
703 site_root: &Path,
704) -> eyre::Result<()> {
705 let mut buffer = vec![0u8; 8192];
706 let bytes_read = stream
707 .read(&mut buffer)
708 .await
709 .wrap_err("Failed to read HTTP request")?;
710 if bytes_read == 0 {
711 return Ok(());
712 }
713
714 let request = std::str::from_utf8(&buffer[..bytes_read])
715 .wrap_err("Hydrolysis web dev server received non-UTF-8 request head")?;
716 let request_line = request
717 .lines()
718 .next()
719 .ok_or_else(|| eyre::eyre!("Hydrolysis web dev server received an empty request"))?;
720 let mut parts = request_line.split_whitespace();
721 let method = parts.next().unwrap_or_default();
722 let path = parts.next().unwrap_or("/");
723 if method != "GET" && method != "HEAD" {
724 write_response(
725 stream,
726 405,
727 "text/plain; charset=utf-8",
728 b"Method Not Allowed",
729 )
730 .await?;
731 return Ok(());
732 }
733
734 let Ok(file_path) = resolve_site_path(site_root, path) else {
735 write_response(stream, 404, "text/plain; charset=utf-8", b"Not Found").await?;
736 return Ok(());
737 };
738 let body = match fs::read(&file_path).await {
739 Ok(body) => body,
740 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
741 write_response(stream, 404, "text/plain; charset=utf-8", b"Not Found").await?;
742 return Ok(());
743 }
744 Err(error) => {
745 return Err(error).wrap_err_with(|| format!("Failed to read {}", file_path.display()));
746 }
747 };
748 let mime = mime_type_for_path(&file_path);
749 if method == "HEAD" {
750 write_headers(stream, 200, mime, body.len()).await?;
751 return Ok(());
752 }
753 write_response(stream, 200, mime, &body).await
754}
755
756fn resolve_site_path(site_root: &Path, request_path: &str) -> eyre::Result<PathBuf> {
757 let request_path = request_path.split('?').next().unwrap_or("/");
758 let trimmed = request_path.trim_start_matches('/');
759 let relative = if trimmed.is_empty() {
760 "index.html"
761 } else {
762 trimmed
763 };
764
765 let mut resolved = site_root.to_path_buf();
766 for segment in relative.split('/') {
767 if segment.is_empty() || segment == "." {
768 continue;
769 }
770 if segment == ".." || segment.contains('\\') {
771 bail!("Hydrolysis web dev server rejected invalid path {request_path}");
772 }
773 resolved.push(segment);
774 }
775
776 if resolved.is_dir() {
777 resolved.push("index.html");
778 }
779 if !resolved.exists() {
780 bail!("Hydrolysis web dev server could not resolve {request_path}");
781 }
782 Ok(resolved)
783}
784
785async fn write_headers(
786 stream: &mut smol::net::TcpStream,
787 status_code: u16,
788 content_type: &str,
789 content_length: usize,
790) -> eyre::Result<()> {
791 let status_text = match status_code {
792 200 => "OK",
793 404 => "Not Found",
794 405 => "Method Not Allowed",
795 _ => "Internal Server Error",
796 };
797 let headers = format!(
798 "HTTP/1.1 {status_code} {status_text}\r\nContent-Type: {content_type}\r\nContent-Length: {content_length}\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n"
799 );
800 stream.write_all(headers.as_bytes()).await?;
801 Ok(())
802}
803
804async fn write_response(
805 stream: &mut smol::net::TcpStream,
806 status_code: u16,
807 content_type: &str,
808 body: &[u8],
809) -> eyre::Result<()> {
810 write_headers(stream, status_code, content_type, body.len()).await?;
811 stream.write_all(body).await?;
812 stream.flush().await?;
813 Ok(())
814}
815
816fn mime_type_for_path(path: &Path) -> &'static str {
817 match path.extension().and_then(std::ffi::OsStr::to_str) {
818 Some("html") => "text/html; charset=utf-8",
819 Some("js") => "text/javascript; charset=utf-8",
820 Some("css") => "text/css; charset=utf-8",
821 Some("json") => "application/json",
822 Some("wasm") => "application/wasm",
823 Some("svg") => "image/svg+xml",
824 Some("png") => "image/png",
825 Some("jpg" | "jpeg") => "image/jpeg",
826 Some("gif") => "image/gif",
827 Some("webp") => "image/webp",
828 Some("avif") => "image/avif",
829 Some("ico") => "image/x-icon",
830 Some("txt") => "text/plain; charset=utf-8",
831 _ => "application/octet-stream",
832 }
833}