1#![allow(dead_code, unused_imports, unused_qualifications, unreachable_patterns)]
9
10use super::detect::{detect_distros, WslDistro};
11use super::shell_config::{install_block, uninstall_block, ShellBlockConfig};
12use std::path::{Path, PathBuf};
13#[cfg(target_os = "windows")]
14use std::time::Duration;
15
16#[cfg(target_os = "windows")]
21const WSL_DEP_INSTALL_TIMEOUT: Duration = Duration::from_secs(300);
22
23#[cfg(target_os = "windows")]
25const WSL_QUICK_CMD_TIMEOUT: Duration = Duration::from_secs(15);
26
27pub use super::detect::decode_wsl_output;
28
29#[derive(Debug, Clone)]
44pub struct LinuxReleaseSpec {
45 pub repo: String,
47 pub tag: String,
51 pub asset_gnu: String,
55 pub asset_musl: String,
59 pub binaries: Vec<String>,
62}
63
64#[derive(Debug, Clone)]
66pub struct WslInstallConfig {
67 pub app_name: String,
69 pub shell_block: String,
71 pub linux_binary_path: Option<PathBuf>,
73 pub linux_binary_target: Option<String>,
76 pub auto_install_linux_release: Option<LinuxReleaseSpec>,
80 pub linux_binaries_to_remove: Vec<String>,
86}
87
88#[derive(Debug)]
90pub struct DistroResult {
91 pub distro_name: String,
93 pub outcome: Result<Vec<String>, String>,
95}
96
97pub fn configure_all_distros(config: &WslInstallConfig) -> Vec<DistroResult> {
111 let distros = detect_distros();
112 distros
113 .into_iter()
114 .map(|distro| {
115 let name = distro.name.clone();
116 let outcome = configure_distro(&distro, config);
117 DistroResult {
118 distro_name: name,
119 outcome,
120 }
121 })
122 .collect()
123}
124
125pub fn unconfigure_all_distros(config: &WslInstallConfig) -> Vec<DistroResult> {
133 let distros = detect_distros();
134 distros
135 .into_iter()
136 .map(|distro| {
137 let name = distro.name.clone();
138 let outcome = unconfigure_distro(&distro, config);
139 DistroResult {
140 distro_name: name,
141 outcome,
142 }
143 })
144 .collect()
145}
146
147#[cfg(target_os = "windows")]
151pub fn find_wsl_home(distro: &str) -> Option<PathBuf> {
152 let linux_home = find_linux_home(distro)?;
153 if linux_home.is_empty() {
154 return None;
155 }
156
157 for prefix in &[r"\\wsl$", r"\\wsl.localhost"] {
158 let win_path = format!(r"{}\{}{}", prefix, distro, linux_home.replace('/', r"\"));
159 let path = PathBuf::from(&win_path);
160 if path.exists() {
161 return Some(path);
162 }
163 }
164
165 None
166}
167
168#[cfg(not(target_os = "windows"))]
170pub fn find_wsl_home(_distro: &str) -> Option<PathBuf> {
171 None
172}
173
174#[cfg(target_os = "windows")]
176fn find_linux_home(distro: &str) -> Option<String> {
177 crate::internal::wsl::detect::linux_home(distro)
178}
179
180fn configure_distro(distro: &WslDistro, config: &WslInstallConfig) -> Result<Vec<String>, String> {
182 let home_path = distro
183 .home_path
184 .as_ref()
185 .ok_or_else(|| format!("could not find home directory for {}", distro.name))?;
186
187 let mut actions = Vec::new();
188
189 #[cfg(target_os = "windows")]
191 if let (Some(src), Some(target)) = (&config.linux_binary_path, &config.linux_binary_target) {
192 copy_linux_binary(home_path, src, target, &distro.name, &mut actions)?;
193 }
194
195 let block_config = ShellBlockConfig::new(&config.app_name, &config.shell_block);
197 inject_shell_configs(home_path, &block_config, &mut actions)?;
198
199 #[cfg(target_os = "windows")]
202 if let Some(release) = config.auto_install_linux_release.as_ref() {
203 install_linux_release(&distro.name, release, &mut actions)?;
204 }
205
206 Ok(actions)
207}
208
209fn unconfigure_distro(
211 distro: &WslDistro,
212 config: &WslInstallConfig,
213) -> Result<Vec<String>, String> {
214 let home_path = distro
215 .home_path
216 .as_ref()
217 .ok_or_else(|| format!("could not find home directory for {}", distro.name))?;
218
219 let mut actions = Vec::new();
220
221 let block_config = ShellBlockConfig::new(&config.app_name, &config.shell_block);
223 for name in &[".bashrc", ".zshrc", ".profile"] {
224 let path = home_path.join(name);
225 if path.exists() {
226 match uninstall_block(&path, &block_config) {
227 Ok(crate::internal::wsl::shell_config::UninstallResult::Removed) => {
228 actions.push(format!("Removed block from {name}"));
229 }
230 Ok(crate::internal::wsl::shell_config::UninstallResult::NotPresent) => {}
231 Err(e) => {
232 return Err(format!("{name}: {e}"));
233 }
234 }
235 }
236 }
237
238 if let Some(target) = &config.linux_binary_target {
240 let binary_path = home_path.join(target);
241 if binary_path.exists() {
242 std::fs::remove_file(&binary_path).map_err(|e| format!("remove binary: {e}"))?;
243 actions.push(format!("Removed ~/{target}"));
244 }
245 }
246
247 #[cfg(target_os = "windows")]
249 if !config.linux_binaries_to_remove.is_empty() {
250 remove_linux_release_binaries(&distro.name, &config.linux_binaries_to_remove, &mut actions);
251 }
252
253 #[cfg(target_os = "windows")]
257 {
258 let app = &config.app_name;
259 let runtime_dir = home_path.join(format!(".{app}"));
260 if runtime_dir.exists() {
261 remove_runtime_files_in_distro(&distro.name, &runtime_dir, app, &mut actions);
262 }
263 }
264
265 Ok(actions)
266}
267
268#[cfg(target_os = "windows")]
272fn remove_linux_release_binaries(
273 distro_name: &str,
274 binaries: &[String],
275 actions: &mut Vec<String>,
276) {
277 let rm_args: Vec<String> = binaries
278 .iter()
279 .map(|b| format!("/usr/local/bin/{b}"))
280 .collect();
281 let script = format!(
282 "set -e\nfor b in {}; do sudo rm -f \"$b\" 2>/dev/null || true; done",
283 rm_args
284 .iter()
285 .map(|p| format!("'{p}'"))
286 .collect::<Vec<_>>()
287 .join(" ")
288 );
289 let mut cmd = std::process::Command::new("wsl");
290 cmd.args(["-d", distro_name, "-e", "bash", "-c", &script]);
291 match crate::internal::core::timeout::run_with_timeout(cmd, WSL_QUICK_CMD_TIMEOUT) {
292 Ok(crate::internal::core::timeout::TimeoutResult::Completed(output))
293 if output.status.success() =>
294 {
295 actions.push(format!(
296 "Removed {} from /usr/local/bin/",
297 binaries.join(", ")
298 ));
299 }
300 Ok(crate::internal::core::timeout::TimeoutResult::Completed(output)) => {
301 let stderr = String::from_utf8_lossy(&output.stderr);
302 actions.push(format!(
303 "Warning: could not remove binaries from /usr/local/bin/ ({})",
304 stderr.lines().next().unwrap_or("unknown error")
305 ));
306 }
307 Ok(crate::internal::core::timeout::TimeoutResult::TimedOut) => {
308 actions.push("Warning: timed out removing binaries from /usr/local/bin/".to_string());
309 }
310 Err(e) => {
311 actions.push(format!(
312 "Warning: could not launch wsl to remove binaries: {e}"
313 ));
314 }
315 }
316}
317
318#[cfg(target_os = "windows")]
321fn remove_runtime_files_in_distro(
322 distro_name: &str,
323 runtime_dir: &Path,
324 app_name: &str,
325 actions: &mut Vec<String>,
326) {
327 let dir = runtime_dir.display().to_string();
328 let script = format!(
332 "set -e\n\
333 pkill -TERM -x {app_name}-agent 2>/dev/null || true\n\
334 sleep 0.3\n\
335 rm -f '{dir}/agent.sock' '{dir}/agent.pid'\n\
336 rmdir '{dir}' 2>/dev/null || true"
337 );
338 let mut cmd = std::process::Command::new("wsl");
339 cmd.args(["-d", distro_name, "-e", "bash", "-c", &script]);
340 drop(crate::internal::core::timeout::run_with_timeout(
341 cmd,
342 WSL_QUICK_CMD_TIMEOUT,
343 ));
344 actions.push(format!("Cleaned up runtime files in ~/.{app_name}/"));
345}
346
347fn inject_shell_configs(
349 home_path: &Path,
350 block_config: &ShellBlockConfig,
351 actions: &mut Vec<String>,
352) -> Result<(), String> {
353 let mut configured = false;
354
355 let bashrc = home_path.join(".bashrc");
357 if bashrc.exists() {
358 match install_block(&bashrc, block_config) {
359 Ok(crate::internal::wsl::shell_config::InstallResult::Installed) => {
360 actions.push("Updated .bashrc".to_string());
361 configured = true;
362 }
363 Ok(crate::internal::wsl::shell_config::InstallResult::AlreadyPresent) => {
364 configured = true;
365 }
366 Err(e) => return Err(format!(".bashrc: {e}")),
367 }
368 }
369
370 let zshrc = home_path.join(".zshrc");
372 if zshrc.exists() {
373 match install_block(&zshrc, block_config) {
374 Ok(crate::internal::wsl::shell_config::InstallResult::Installed) => {
375 actions.push("Updated .zshrc".to_string());
376 configured = true;
377 }
378 Ok(crate::internal::wsl::shell_config::InstallResult::AlreadyPresent) => {
379 configured = true;
380 }
381 Err(e) => return Err(format!(".zshrc: {e}")),
382 }
383 }
384
385 if !configured {
387 let profile = home_path.join(".profile");
388 if profile.exists() {
389 match install_block(&profile, block_config) {
390 Ok(crate::internal::wsl::shell_config::InstallResult::Installed) => {
391 actions.push("Updated .profile".to_string());
392 }
393 Ok(crate::internal::wsl::shell_config::InstallResult::AlreadyPresent) => {}
394 Err(e) => return Err(format!(".profile: {e}")),
395 }
396 } else {
397 match install_block(&bashrc, block_config) {
399 Ok(_) => {
400 actions.push("Created .bashrc".to_string());
401 }
402 Err(e) => return Err(format!("create .bashrc: {e}")),
403 }
404 }
405 }
406
407 Ok(())
408}
409
410#[cfg(target_os = "windows")]
412fn copy_linux_binary(
413 home_path: &Path,
414 src: &Path,
415 target: &str,
416 distro_name: &str,
417 actions: &mut Vec<String>,
418) -> Result<(), String> {
419 let dest = home_path.join(target);
420 if let Some(parent) = dest.parent() {
421 std::fs::create_dir_all(parent)
422 .map_err(|e| format!("create directory {}: {e}", parent.display()))?;
423 }
424 std::fs::copy(src, &dest).map_err(|e| format!("copy binary: {e}"))?;
425
426 if let Some(linux_home) = find_linux_home(distro_name) {
428 let linux_path = format!("{linux_home}/{target}");
429 let mut cmd = std::process::Command::new("wsl");
430 cmd.args(["-d", distro_name, "-e", "chmod", "+x", &linux_path]);
431 drop(crate::internal::core::timeout::run_status_with_timeout(
432 cmd,
433 WSL_QUICK_CMD_TIMEOUT,
434 ));
435 }
436
437 actions.push(format!("Installed binary to ~/{target}"));
438 Ok(())
439}
440
441#[cfg(target_os = "windows")]
448fn distro_is_glibc(distro_name: &str) -> bool {
449 let mut wsl = std::process::Command::new("wsl");
450 wsl.args(["-d", distro_name, "-e", "ldd", "--version"]);
451 match crate::internal::core::timeout::run_with_timeout(wsl, WSL_QUICK_CMD_TIMEOUT) {
452 Ok(crate::internal::core::timeout::TimeoutResult::Completed(o)) => {
453 let stdout = String::from_utf8_lossy(&o.stdout);
454 let stderr = String::from_utf8_lossy(&o.stderr);
455 let combined = format!("{stdout}{stderr}");
456 combined.contains("GNU libc") || combined.contains("Free Software Foundation")
460 }
461 _ => false, }
463}
464
465#[cfg(target_os = "windows")]
472fn install_linux_release(
473 distro_name: &str,
474 spec: &LinuxReleaseSpec,
475 actions: &mut Vec<String>,
476) -> Result<(), String> {
477 let asset = if distro_is_glibc(distro_name) {
478 &spec.asset_gnu
479 } else {
480 &spec.asset_musl
481 };
482 let url = format!(
483 "https://github.com/{}/releases/download/{}/{}",
484 spec.repo, spec.tag, asset
485 );
486
487 let bins = spec.binaries.join(" ");
525 let script = format!(
526 "set -e\n\
527 work=/tmp/sshenc-install-$$\n\
528 rm -rf \"$work\"\n\
529 mkdir -p \"$work\"\n\
530 cd \"$work\"\n\
531 trap 'rm -rf \"$work\"' EXIT\n\
532 curl -fsSL '{url}' -o release.tar.gz\n\
533 tar xzf release.tar.gz\n\
534 for b in {bins}; do\n\
535 sudo cp \"$b\" \"/usr/local/bin/$b.new\"\n\
536 sudo chmod +x \"/usr/local/bin/$b.new\"\n\
537 sudo mv \"/usr/local/bin/$b.new\" \"/usr/local/bin/$b\"\n\
538 done\n\
539 pkill -KILL -x sshenc-agent 2>/dev/null || true\n"
540 );
541 let mut cmd = std::process::Command::new("wsl");
554 cmd.args(["-d", distro_name, "-e", "bash", "-c", &script]);
555 match crate::internal::core::timeout::run_with_timeout(cmd, WSL_DEP_INSTALL_TIMEOUT) {
556 Ok(crate::internal::core::timeout::TimeoutResult::Completed(output))
557 if output.status.success() =>
558 {
559 actions.push(format!(
560 "Installed {} from {} {}",
561 spec.binaries.join(", "),
562 spec.repo,
563 spec.tag
564 ));
565 Ok(())
566 }
567 Ok(crate::internal::core::timeout::TimeoutResult::TimedOut) => {
568 actions.push(format!(
569 "Warning: {} install timed out after {}s",
570 spec.repo,
571 WSL_DEP_INSTALL_TIMEOUT.as_secs()
572 ));
573 Ok(())
574 }
575 Ok(crate::internal::core::timeout::TimeoutResult::Completed(output)) => {
576 let stderr = String::from_utf8_lossy(&output.stderr);
581 let tail: Vec<&str> = stderr
582 .lines()
583 .rev()
584 .filter(|l| !l.trim().is_empty())
585 .take(3)
586 .collect();
587 let detail: String = tail.into_iter().rev().collect::<Vec<_>>().join(" / ");
588 let exit = output
589 .status
590 .code()
591 .map(|c| format!("exit {c}"))
592 .unwrap_or_else(|| "signaled".to_string());
593 actions.push(format!(
594 "Warning: failed to install {} from {} ({}: {})",
595 spec.binaries.join(", "),
596 url,
597 exit,
598 if detail.is_empty() {
599 "no stderr"
600 } else {
601 detail.as_str()
602 }
603 ));
604 Ok(())
605 }
606 Err(e) => {
607 actions.push(format!(
608 "Warning: failed to launch wsl install for {} ({e})",
609 spec.binaries.join(", "),
610 ));
611 Ok(())
612 }
613 }
614}
615
616#[cfg(test)]
623#[allow(clippy::unwrap_used, clippy::panic, let_underscore_drop)]
624mod tests {
625 use super::*;
626 use std::sync::atomic::{AtomicU64, Ordering};
627
628 static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
629
630 fn test_dir(name: &str) -> PathBuf {
631 let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
632 let pid = std::process::id();
633 let dir =
634 std::env::temp_dir().join(format!("enclaveapp-wsl-install-test-{pid}-{id}-{name}"));
635 std::fs::remove_dir_all(&dir).ok();
636 std::fs::create_dir_all(&dir).unwrap();
637 dir
638 }
639
640 #[test]
641 fn test_inject_shell_configs_bashrc() {
642 let dir = test_dir("inject-bashrc");
643 std::fs::write(dir.join(".bashrc"), "# existing\n").unwrap();
644
645 let config = ShellBlockConfig::new("testapp", "export FOO=bar");
646 let mut actions = Vec::new();
647 inject_shell_configs(&dir, &config, &mut actions).unwrap();
648
649 assert!(!actions.is_empty());
650 let content = std::fs::read_to_string(dir.join(".bashrc")).unwrap();
651 assert!(content.contains("BEGIN testapp managed block"));
652 assert!(content.contains("export FOO=bar"));
653
654 std::fs::remove_dir_all(&dir).unwrap();
655 }
656
657 #[test]
658 fn test_inject_shell_configs_zshrc() {
659 let dir = test_dir("inject-zshrc");
660 std::fs::write(dir.join(".zshrc"), "# zsh config\n").unwrap();
661
662 let config = ShellBlockConfig::new("testapp", "export BAR=baz");
663 let mut actions = Vec::new();
664 inject_shell_configs(&dir, &config, &mut actions).unwrap();
665
666 assert!(actions.iter().any(|a| a.contains(".zshrc")));
667 let content = std::fs::read_to_string(dir.join(".zshrc")).unwrap();
668 assert!(content.contains("export BAR=baz"));
669
670 std::fs::remove_dir_all(&dir).unwrap();
671 }
672
673 #[test]
674 fn test_inject_shell_configs_both() {
675 let dir = test_dir("inject-both");
676 std::fs::write(dir.join(".bashrc"), "# bash\n").unwrap();
677 std::fs::write(dir.join(".zshrc"), "# zsh\n").unwrap();
678
679 let config = ShellBlockConfig::new("testapp", "export X=1");
680 let mut actions = Vec::new();
681 inject_shell_configs(&dir, &config, &mut actions).unwrap();
682
683 assert!(actions.iter().any(|a| a.contains(".bashrc")));
684 assert!(actions.iter().any(|a| a.contains(".zshrc")));
685
686 std::fs::remove_dir_all(&dir).unwrap();
687 }
688
689 #[test]
690 fn test_inject_shell_configs_fallback_profile() {
691 let dir = test_dir("inject-profile");
692 std::fs::write(dir.join(".profile"), "# profile\n").unwrap();
694
695 let config = ShellBlockConfig::new("testapp", "export Y=2");
696 let mut actions = Vec::new();
697 inject_shell_configs(&dir, &config, &mut actions).unwrap();
698
699 assert!(actions.iter().any(|a| a.contains(".profile")));
700 let content = std::fs::read_to_string(dir.join(".profile")).unwrap();
701 assert!(content.contains("export Y=2"));
702
703 std::fs::remove_dir_all(&dir).unwrap();
704 }
705
706 #[test]
707 fn test_inject_shell_configs_creates_bashrc() {
708 let dir = test_dir("inject-create");
709 let config = ShellBlockConfig::new("testapp", "export Z=3");
712 let mut actions = Vec::new();
713 inject_shell_configs(&dir, &config, &mut actions).unwrap();
714
715 assert!(actions.iter().any(|a| a.contains(".bashrc")));
716 let content = std::fs::read_to_string(dir.join(".bashrc")).unwrap();
717 assert!(content.contains("export Z=3"));
718
719 std::fs::remove_dir_all(&dir).unwrap();
720 }
721
722 #[test]
723 fn test_inject_idempotent() {
724 let dir = test_dir("inject-idempotent");
725 std::fs::write(dir.join(".bashrc"), "# existing\n").unwrap();
726
727 let config = ShellBlockConfig::new("testapp", "export A=1");
728 let mut actions1 = Vec::new();
729 inject_shell_configs(&dir, &config, &mut actions1).unwrap();
730 let content1 = std::fs::read_to_string(dir.join(".bashrc")).unwrap();
731
732 let mut actions2 = Vec::new();
733 inject_shell_configs(&dir, &config, &mut actions2).unwrap();
734 let content2 = std::fs::read_to_string(dir.join(".bashrc")).unwrap();
735
736 assert_eq!(content1, content2);
737
738 std::fs::remove_dir_all(&dir).unwrap();
739 }
740
741 #[test]
742 fn test_unconfigure_distro_removes_blocks() {
743 let dir = test_dir("unconfigure");
744 std::fs::write(dir.join(".bashrc"), "# before\n").unwrap();
745
746 let block_config = ShellBlockConfig::new("testapp", "export Q=1");
747 install_block(dir.join(".bashrc").as_path(), &block_config).unwrap();
748
749 let distro = WslDistro {
750 name: "TestDistro".to_string(),
751 home_path: Some(dir.clone()),
752 };
753 let config = WslInstallConfig {
754 app_name: "testapp".to_string(),
755 shell_block: "export Q=1".to_string(),
756 auto_install_linux_release: None,
757 linux_binary_path: None,
758 linux_binary_target: None,
759 linux_binaries_to_remove: vec![],
760 };
761 let result = unconfigure_distro(&distro, &config).unwrap();
762 assert!(result.iter().any(|a| a.contains("Removed")));
763
764 let content = std::fs::read_to_string(dir.join(".bashrc")).unwrap();
765 assert!(!content.contains("BEGIN testapp managed block"));
766
767 std::fs::remove_dir_all(&dir).unwrap();
768 }
769
770 #[test]
771 fn test_decode_wsl_output_utf8() {
772 let input = b"Ubuntu\nDebian\n";
773 let result = decode_wsl_output(input);
774 assert_eq!(result, "Ubuntu\nDebian\n");
775 }
776
777 #[test]
778 fn test_decode_wsl_output_utf16le_bom() {
779 let mut bytes = vec![0xFF_u8, 0xFE]; for ch in "Hi\n".encode_utf16() {
782 bytes.extend_from_slice(&ch.to_le_bytes());
783 }
784 let result = decode_wsl_output(&bytes);
785 assert_eq!(result, "Hi\n");
786 }
787
788 #[test]
789 fn test_find_wsl_home_non_windows() {
790 #[cfg(not(target_os = "windows"))]
792 assert!(find_wsl_home("Ubuntu").is_none());
793 }
794
795 #[test]
796 fn test_distro_result_debug() {
797 let result = DistroResult {
798 distro_name: "Ubuntu".to_string(),
799 outcome: Ok(vec!["Updated .bashrc".to_string()]),
800 };
801 let debug_str = format!("{result:?}");
802 assert!(debug_str.contains("Ubuntu"));
803 }
804
805 #[test]
806 fn test_wsl_install_config_clone() {
807 let config = WslInstallConfig {
808 app_name: "test".to_string(),
809 shell_block: "# test".to_string(),
810 auto_install_linux_release: None,
811 linux_binary_path: None,
812 linux_binary_target: None,
813 linux_binaries_to_remove: vec![],
814 };
815 let cloned = config.clone();
816 assert_eq!(cloned.app_name, config.app_name);
817 assert_eq!(cloned.shell_block, config.shell_block);
818 }
819
820 #[test]
821 fn test_decode_wsl_output_real_utf16le_bom() {
822 let text = "Ubuntu\r\n";
824 let mut bytes = vec![0xFF_u8, 0xFE]; for ch in text.encode_utf16() {
826 bytes.extend_from_slice(&ch.to_le_bytes());
827 }
828 let result = decode_wsl_output(&bytes);
829 assert_eq!(result, "Ubuntu\r\n");
830 }
831
832 #[test]
833 fn test_decode_wsl_output_plain_utf8() {
834 let input = b"Debian GNU/Linux\n";
835 let result = decode_wsl_output(input);
836 assert_eq!(result, "Debian GNU/Linux\n");
837 }
838
839 #[test]
840 fn test_configure_distro_creates_backup_like_file() {
841 let dir = test_dir("configure-backup");
843 let bashrc = dir.join(".bashrc");
844 std::fs::write(&bashrc, "# original content\nexport PATH=/usr/bin\n").unwrap();
845 let original_content = std::fs::read_to_string(&bashrc).unwrap();
846
847 let distro = WslDistro {
848 name: "TestDistro".to_string(),
849 home_path: Some(dir.clone()),
850 };
851 let config = WslInstallConfig {
852 app_name: "testapp".to_string(),
853 shell_block: "export TEST=1".to_string(),
854 auto_install_linux_release: None,
855 linux_binary_path: None,
856 linux_binary_target: None,
857 linux_binaries_to_remove: vec![],
858 };
859
860 let result = configure_distro(&distro, &config).unwrap();
861 assert!(!result.is_empty());
862
863 let new_content = std::fs::read_to_string(&bashrc).unwrap();
865 assert!(new_content.contains("BEGIN testapp managed block"));
866 assert!(new_content.contains(&original_content.trim_end().to_string()));
868
869 std::fs::remove_dir_all(&dir).unwrap();
870 }
871
872 #[test]
873 fn test_unconfigure_distro_removes_block_but_keeps_content() {
874 let dir = test_dir("unconfigure-keep");
875 let bashrc = dir.join(".bashrc");
876 std::fs::write(&bashrc, "# my config\nexport FOO=bar\n").unwrap();
877
878 let block_config = ShellBlockConfig::new("testapp", "export Q=1");
879 install_block(bashrc.as_path(), &block_config).unwrap();
880
881 let content = std::fs::read_to_string(&bashrc).unwrap();
883 assert!(content.contains("BEGIN testapp managed block"));
884
885 let distro = WslDistro {
886 name: "TestDistro".to_string(),
887 home_path: Some(dir.clone()),
888 };
889 let config = WslInstallConfig {
890 app_name: "testapp".to_string(),
891 shell_block: "export Q=1".to_string(),
892 auto_install_linux_release: None,
893 linux_binary_path: None,
894 linux_binary_target: None,
895 linux_binaries_to_remove: vec![],
896 };
897 let result = unconfigure_distro(&distro, &config).unwrap();
898 assert!(result.iter().any(|a| a.contains("Removed")));
899
900 let final_content = std::fs::read_to_string(&bashrc).unwrap();
901 assert!(!final_content.contains("BEGIN testapp managed block"));
902 assert!(final_content.contains("# my config"));
903 assert!(final_content.contains("export FOO=bar"));
904
905 std::fs::remove_dir_all(&dir).unwrap();
906 }
907
908 #[test]
909 fn test_decode_wsl_output_utf16le_multiple_lines() {
910 let text = "Ubuntu\nDebian\n";
912 let mut bytes = vec![0xFF_u8, 0xFE];
913 for ch in text.encode_utf16() {
914 bytes.extend_from_slice(&ch.to_le_bytes());
915 }
916 let result = decode_wsl_output(&bytes);
917 assert_eq!(result, "Ubuntu\nDebian\n");
918 }
919
920 #[test]
921 fn test_decode_wsl_output_empty_utf8() {
922 let result = decode_wsl_output(b"");
923 assert_eq!(result, "");
924 }
925}