hardware_enclave/internal/wsl/
shell_config.rs1#![allow(dead_code, unused_imports, unused_qualifications, unreachable_patterns)]
10
11use crate::internal::core::Result;
12use std::path::Path;
13
14#[derive(Debug)]
16pub struct ShellBlockConfig {
17 pub app_name: String,
19 pub block_content: String,
21}
22
23impl ShellBlockConfig {
24 pub fn new(app_name: &str, block_content: &str) -> Self {
25 ShellBlockConfig {
26 app_name: app_name.to_string(),
27 block_content: block_content.to_string(),
28 }
29 }
30
31 fn begin_marker(&self) -> String {
32 format!("# BEGIN {} managed block -- do not edit", self.app_name)
33 }
34
35 fn end_marker(&self) -> String {
36 format!("# END {} managed block", self.app_name)
37 }
38
39 fn full_block(&self) -> String {
40 format!(
41 "{}\n{}\n{}",
42 self.begin_marker(),
43 self.block_content,
44 self.end_marker()
45 )
46 }
47}
48
49#[derive(Debug, PartialEq, Eq)]
51pub enum InstallResult {
52 Installed,
53 AlreadyPresent,
54}
55
56#[derive(Debug, PartialEq, Eq)]
58pub enum UninstallResult {
59 Removed,
60 NotPresent,
61}
62
63pub fn is_installed(path: &Path, config: &ShellBlockConfig) -> Result<bool> {
65 if !path.exists() {
66 return Ok(false);
67 }
68 let content = std::fs::read_to_string(path)?;
69 Ok(content.contains(&config.begin_marker()))
70}
71
72pub fn install_block(path: &Path, config: &ShellBlockConfig) -> Result<InstallResult> {
92 let content = if path.exists() {
93 std::fs::read_to_string(path)?
94 } else {
95 String::new()
96 };
97
98 let content = content.replace("\r\n", "\n");
100 let begin = config.begin_marker();
101 let end = config.end_marker();
102 let new_block = config.full_block();
103
104 if let Some(begin_idx) = content.find(&begin) {
107 if let Some(rel_end_idx) = content[begin_idx..].find(&end) {
108 let block_end = begin_idx + rel_end_idx + end.len();
109 let existing = &content[begin_idx..block_end];
110 if existing == new_block {
111 return Ok(InstallResult::AlreadyPresent);
112 }
113 let mut output = String::with_capacity(content.len() + new_block.len());
116 output.push_str(&content[..begin_idx]);
117 output.push_str(&new_block);
118 output.push_str(&content[block_end..]);
119 std::fs::write(path, &output)?;
120 return Ok(InstallResult::Installed);
121 }
122 }
128
129 if let Some(parent) = path.parent() {
131 std::fs::create_dir_all(parent)?;
132 }
133
134 let mut output = content;
135 if !output.is_empty() && !output.ends_with('\n') {
137 output.push('\n');
138 }
139 if !output.is_empty() {
141 output.push('\n');
142 }
143 output.push_str(&new_block);
144 output.push('\n');
145
146 std::fs::write(path, &output)?;
147 Ok(InstallResult::Installed)
148}
149
150pub fn uninstall_block(path: &Path, config: &ShellBlockConfig) -> Result<UninstallResult> {
155 if !path.exists() {
156 return Ok(UninstallResult::NotPresent);
157 }
158
159 let content = std::fs::read_to_string(path)?;
160 let content = content.replace("\r\n", "\n");
161
162 if !content.contains(&config.begin_marker()) {
163 return Ok(UninstallResult::NotPresent);
164 }
165
166 let begin = &config.begin_marker();
167 let end = &config.end_marker();
168
169 let lines: Vec<&str> = content.lines().collect();
170 let mut new_lines: Vec<&str> = Vec::new();
171 let mut in_block = false;
172
173 for line in &lines {
174 if line.contains(begin.as_str()) {
175 in_block = true;
176 if let Some(last) = new_lines.last() {
178 if last.is_empty() {
179 new_lines.pop();
180 }
181 }
182 continue;
183 }
184 if in_block {
185 if line.contains(end.as_str()) {
186 in_block = false;
187 }
188 continue;
189 }
190 new_lines.push(line);
191 }
192
193 let mut result = new_lines.join("\n");
195 let trimmed = result.trim_end().to_string();
197 result = if trimmed.is_empty() {
198 trimmed
199 } else {
200 trimmed + "\n"
201 };
202
203 std::fs::write(path, &result)?;
204 Ok(UninstallResult::Removed)
205}
206
207pub fn validate_shell_syntax(path: &Path, shell: &str) -> Result<()> {
212 use crate::internal::core::timeout::{run_with_timeout, TimeoutResult};
213 use std::time::Duration;
214 let mut cmd = std::process::Command::new(shell);
215 cmd.arg("-n").arg(path);
216 let output = run_with_timeout(cmd, Duration::from_secs(10));
220
221 match output {
222 Ok(TimeoutResult::Completed(o)) if o.status.success() => Ok(()),
223 Ok(TimeoutResult::TimedOut) => Err(crate::internal::core::Error::Config(format!(
224 "{shell} syntax check timed out after 10s"
225 ))),
226 Ok(TimeoutResult::Completed(o)) => {
227 let stderr = String::from_utf8_lossy(&o.stderr);
228 Err(crate::internal::core::Error::Config(format!(
229 "{shell} syntax check failed: {stderr}"
230 )))
231 }
232 Err(_) => {
233 Ok(())
235 }
236 }
237}
238
239pub fn shell_config_paths(home: &Path) -> Vec<(&'static str, std::path::PathBuf)> {
241 vec![
242 ("bash", home.join(".bashrc")),
243 ("zsh", home.join(".zshrc")),
244 ("bash", home.join(".profile")),
245 ]
246}
247
248#[cfg(test)]
249#[allow(clippy::unwrap_used, clippy::panic, let_underscore_drop)]
250mod tests {
251 use super::*;
252 use std::path::PathBuf;
253 use std::sync::atomic::{AtomicU64, Ordering};
254
255 static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
256
257 fn test_dir(name: &str) -> PathBuf {
258 let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
259 let pid = std::process::id();
260 let dir = std::env::temp_dir().join(format!("enclaveapp-wsl-test-{pid}-{id}-{name}"));
261 let _ = std::fs::remove_dir_all(&dir);
262 std::fs::create_dir_all(&dir).unwrap();
263 dir
264 }
265
266 fn test_config() -> ShellBlockConfig {
267 ShellBlockConfig::new(
268 "sshenc",
269 "export SSH_AUTH_SOCK=\"$HOME/.sshenc/agent.sock\"",
270 )
271 }
272
273 #[test]
274 fn test_install_new_file() {
275 let dir = test_dir("install-new");
276 let path = dir.join(".bashrc");
277 let config = test_config();
278
279 let result = install_block(&path, &config).unwrap();
280 assert_eq!(result, InstallResult::Installed);
281
282 let content = std::fs::read_to_string(&path).unwrap();
283 assert!(content.contains(&config.begin_marker()));
284 assert!(content.contains("SSH_AUTH_SOCK"));
285 assert!(content.contains(&config.end_marker()));
286 assert!(content.ends_with('\n'));
288
289 std::fs::remove_dir_all(&dir).unwrap();
290 }
291
292 #[test]
293 fn test_install_existing_file() {
294 let dir = test_dir("install-existing");
295 let path = dir.join(".bashrc");
296 let config = test_config();
297
298 std::fs::write(&path, "# existing config\nexport PATH=/usr/bin\n").unwrap();
299
300 let result = install_block(&path, &config).unwrap();
301 assert_eq!(result, InstallResult::Installed);
302
303 let content = std::fs::read_to_string(&path).unwrap();
304 assert!(content.starts_with("# existing config"));
305 assert!(content.contains(&config.begin_marker()));
306 assert!(content.contains("PATH=/usr/bin\n\n"));
308
309 std::fs::remove_dir_all(&dir).unwrap();
310 }
311
312 #[test]
313 fn test_install_idempotent() {
314 let dir = test_dir("install-idempotent");
315 let path = dir.join(".bashrc");
316 let config = test_config();
317
318 let result1 = install_block(&path, &config).unwrap();
319 assert_eq!(result1, InstallResult::Installed);
320 let content_first = std::fs::read_to_string(&path).unwrap();
321
322 let result2 = install_block(&path, &config).unwrap();
323 assert_eq!(result2, InstallResult::AlreadyPresent);
324 let content_second = std::fs::read_to_string(&path).unwrap();
325
326 assert_eq!(content_first, content_second);
327
328 std::fs::remove_dir_all(&dir).unwrap();
329 }
330
331 #[test]
332 fn test_install_replaces_outdated_block() {
333 let dir = test_dir("install-replaces-outdated");
339 let path = dir.join(".bashrc");
340
341 let old_config = ShellBlockConfig::new("testapp", "export OLD=1");
344 std::fs::write(
345 &path,
346 format!(
347 "# user pre-content\nexport USER_VAR=keep\n\n{}\n\n# user post-content\nalias ll='ls -la'\n",
348 old_config.full_block()
349 ),
350 )
351 .unwrap();
352
353 let new_config = ShellBlockConfig::new("testapp", "export NEW=v2");
355 let result = install_block(&path, &new_config).unwrap();
356 assert_eq!(result, InstallResult::Installed);
357
358 let content = std::fs::read_to_string(&path).unwrap();
359 assert!(content.contains("export NEW=v2"), "new body missing");
361 assert!(!content.contains("export OLD=1"), "old body still present");
362 assert!(content.contains("export USER_VAR=keep"));
364 assert!(content.contains("alias ll='ls -la'"));
365 let block_count = content.matches(&new_config.begin_marker()).count();
367 assert_eq!(
368 block_count, 1,
369 "expected exactly one block, got {block_count}"
370 );
371
372 std::fs::remove_dir_all(&dir).unwrap();
373 }
374
375 #[test]
376 fn test_uninstall_removes_block() {
377 let dir = test_dir("uninstall-removes");
378 let path = dir.join(".bashrc");
379 let config = test_config();
380
381 install_block(&path, &config).unwrap();
382
383 let result = uninstall_block(&path, &config).unwrap();
384 assert_eq!(result, UninstallResult::Removed);
385
386 let content = std::fs::read_to_string(&path).unwrap();
387 assert!(!content.contains(&config.begin_marker()));
388 assert!(!content.contains(&config.end_marker()));
389 assert!(!content.contains("SSH_AUTH_SOCK"));
390
391 std::fs::remove_dir_all(&dir).unwrap();
392 }
393
394 #[test]
395 fn test_uninstall_not_present() {
396 let dir = test_dir("uninstall-not-present");
397 let path = dir.join(".bashrc");
398 let config = test_config();
399
400 std::fs::write(&path, "# just a comment\n").unwrap();
401
402 let result = uninstall_block(&path, &config).unwrap();
403 assert_eq!(result, UninstallResult::NotPresent);
404
405 std::fs::remove_dir_all(&dir).unwrap();
406 }
407
408 #[test]
409 fn test_uninstall_missing_file() {
410 let config = test_config();
411 let result = uninstall_block(Path::new("/nonexistent/path/.bashrc"), &config).unwrap();
412 assert_eq!(result, UninstallResult::NotPresent);
413 }
414
415 #[test]
416 fn test_is_installed_true() {
417 let dir = test_dir("is-installed-true");
418 let path = dir.join(".bashrc");
419 let config = test_config();
420
421 install_block(&path, &config).unwrap();
422 assert!(is_installed(&path, &config).unwrap());
423
424 std::fs::remove_dir_all(&dir).unwrap();
425 }
426
427 #[test]
428 fn test_is_installed_false() {
429 let dir = test_dir("is-installed-false");
430 let path = dir.join(".bashrc");
431 let config = test_config();
432
433 std::fs::write(&path, "").unwrap();
434 assert!(!is_installed(&path, &config).unwrap());
435
436 std::fs::remove_dir_all(&dir).unwrap();
437 }
438
439 #[test]
440 fn test_uninstall_preserves_other_content() {
441 let dir = test_dir("uninstall-preserves");
442 let path = dir.join(".bashrc");
443 let config = test_config();
444
445 std::fs::write(&path, "# before\nexport FOO=bar\n").unwrap();
446 install_block(&path, &config).unwrap();
447
448 uninstall_block(&path, &config).unwrap();
449
450 let content = std::fs::read_to_string(&path).unwrap();
451 assert!(content.contains("# before"));
452 assert!(content.contains("export FOO=bar"));
453 assert!(!content.contains(&config.begin_marker()));
454 assert!(!content.contains("SSH_AUTH_SOCK"));
455
456 std::fs::remove_dir_all(&dir).unwrap();
457 }
458
459 #[test]
460 fn test_install_block_content() {
461 let dir = test_dir("block-content");
462 let path = dir.join(".bashrc");
463 let config = test_config();
464
465 install_block(&path, &config).unwrap();
466
467 let content = std::fs::read_to_string(&path).unwrap();
468 let begin = content.find(&config.begin_marker()).unwrap();
470 let end = content.find(&config.end_marker()).unwrap();
471 assert!(begin < end);
472
473 let block_body = &content[begin + config.begin_marker().len()..end];
474 assert!(block_body.contains("SSH_AUTH_SOCK"));
475
476 std::fs::remove_dir_all(&dir).unwrap();
477 }
478
479 #[test]
480 fn test_custom_app_name() {
481 let sshenc = ShellBlockConfig::new("sshenc", "# sshenc stuff");
482 let awsenc = ShellBlockConfig::new("awsenc", "# awsenc stuff");
483
484 assert_ne!(sshenc.begin_marker(), awsenc.begin_marker());
485 assert_ne!(sshenc.end_marker(), awsenc.end_marker());
486
487 assert!(sshenc.begin_marker().contains("sshenc"));
488 assert!(awsenc.begin_marker().contains("awsenc"));
489 }
490
491 #[test]
492 fn test_crlf_normalization() {
493 let dir = test_dir("crlf");
494 let path = dir.join(".bashrc");
495 let config = test_config();
496
497 std::fs::write(&path, "# existing\r\nexport FOO=bar\r\n").unwrap();
499
500 let result = install_block(&path, &config).unwrap();
501 assert_eq!(result, InstallResult::Installed);
502
503 let content = std::fs::read_to_string(&path).unwrap();
504 assert!(!content.contains("\r\n"));
506 assert!(content.contains("# existing"));
507 assert!(content.contains(&config.begin_marker()));
508
509 std::fs::remove_dir_all(&dir).unwrap();
510 }
511
512 #[test]
513 fn test_shell_config_paths() {
514 let home = PathBuf::from("/home/testuser");
515 let paths = shell_config_paths(&home);
516
517 assert_eq!(paths.len(), 3);
518 assert_eq!(paths[0].0, "bash");
519 assert_eq!(paths[0].1, home.join(".bashrc"));
520 assert_eq!(paths[1].0, "zsh");
521 assert_eq!(paths[1].1, home.join(".zshrc"));
522 assert_eq!(paths[2].0, "bash");
523 assert_eq!(paths[2].1, home.join(".profile"));
524 }
525
526 #[test]
527 fn test_install_block_special_characters() {
528 let dir = test_dir("special-chars");
529 let path = dir.join(".bashrc");
530 let config = ShellBlockConfig::new(
531 "sshenc",
532 r#"export FOO="$HOME/.sshenc/agent.sock"
533export BAR=`whoami`
534export BAZ=\\escaped"#,
535 );
536
537 let result = install_block(&path, &config).unwrap();
538 assert_eq!(result, InstallResult::Installed);
539
540 let content = std::fs::read_to_string(&path).unwrap();
541 assert!(content.contains("$HOME"));
542 assert!(content.contains("`whoami`"));
543 assert!(content.contains("\\\\escaped"));
544 assert!(content.contains(&config.begin_marker()));
545 assert!(content.contains(&config.end_marker()));
546
547 std::fs::remove_dir_all(&dir).unwrap();
548 }
549
550 #[test]
551 fn test_uninstall_preserves_content_before_and_after_exactly() {
552 let dir = test_dir("preserve-exact");
553 let path = dir.join(".bashrc");
554 let config = ShellBlockConfig::new("sshenc", "export X=1");
555
556 let before = "# line one\nexport PATH=/usr/bin\n";
557 let after = "# line three\nexport Y=2\n";
558 std::fs::write(&path, before).unwrap();
560 install_block(&path, &config).unwrap();
561 let mut content = std::fs::read_to_string(&path).unwrap();
563 content.push_str(after);
564 std::fs::write(&path, &content).unwrap();
565
566 uninstall_block(&path, &config).unwrap();
567
568 let result = std::fs::read_to_string(&path).unwrap();
569 assert!(result.contains("# line one"));
570 assert!(result.contains("export PATH=/usr/bin"));
571 assert!(result.contains("# line three"));
572 assert!(result.contains("export Y=2"));
573 assert!(!result.contains(&config.begin_marker()));
574 assert!(!result.contains("export X=1"));
575
576 std::fs::remove_dir_all(&dir).unwrap();
577 }
578
579 #[test]
580 fn test_multiple_different_app_blocks_coexist() {
581 let dir = test_dir("multi-blocks");
582 let path = dir.join(".bashrc");
583 let sshenc_config =
584 ShellBlockConfig::new("sshenc", "export SSH_AUTH_SOCK=/tmp/sshenc.sock");
585 let awsenc_config = ShellBlockConfig::new("awsenc", "export AWS_PROFILE=default");
586
587 install_block(&path, &sshenc_config).unwrap();
588 install_block(&path, &awsenc_config).unwrap();
589
590 let content = std::fs::read_to_string(&path).unwrap();
591 assert!(content.contains(&sshenc_config.begin_marker()));
592 assert!(content.contains(&sshenc_config.end_marker()));
593 assert!(content.contains(&awsenc_config.begin_marker()));
594 assert!(content.contains(&awsenc_config.end_marker()));
595 assert!(content.contains("SSH_AUTH_SOCK"));
596 assert!(content.contains("AWS_PROFILE"));
597
598 uninstall_block(&path, &sshenc_config).unwrap();
600 let content = std::fs::read_to_string(&path).unwrap();
601 assert!(!content.contains(&sshenc_config.begin_marker()));
602 assert!(content.contains(&awsenc_config.begin_marker()));
603 assert!(content.contains("AWS_PROFILE"));
604
605 std::fs::remove_dir_all(&dir).unwrap();
606 }
607
608 #[test]
609 fn test_install_then_update_pattern() {
610 let dir = test_dir("install-update");
611 let path = dir.join(".bashrc");
612 let config_v1 = ShellBlockConfig::new("sshenc", "export VERSION=1");
613
614 install_block(&path, &config_v1).unwrap();
615 let content_v1 = std::fs::read_to_string(&path).unwrap();
616 assert!(content_v1.contains("VERSION=1"));
617
618 uninstall_block(&path, &config_v1).unwrap();
620 let config_v2 = ShellBlockConfig::new("sshenc", "export VERSION=2");
621 install_block(&path, &config_v2).unwrap();
622
623 let content_v2 = std::fs::read_to_string(&path).unwrap();
624 assert!(!content_v2.contains("VERSION=1"));
625 assert!(content_v2.contains("VERSION=2"));
626 assert!(content_v2.contains(&config_v2.begin_marker()));
627
628 std::fs::remove_dir_all(&dir).unwrap();
629 }
630
631 #[test]
634 fn begin_marker_contains_app_name() {
635 let config = ShellBlockConfig::new("myapp", "content");
636 assert!(config.begin_marker().contains("myapp"));
637 }
638
639 #[test]
640 fn begin_marker_format_is_correct() {
641 let config = ShellBlockConfig::new("sshenc", "content");
642 assert_eq!(
643 config.begin_marker(),
644 "# BEGIN sshenc managed block -- do not edit"
645 );
646 }
647
648 #[test]
649 fn end_marker_contains_app_name() {
650 let config = ShellBlockConfig::new("myapp", "content");
651 assert!(config.end_marker().contains("myapp"));
652 }
653
654 #[test]
655 fn end_marker_format_is_correct() {
656 let config = ShellBlockConfig::new("sshenc", "content");
657 assert_eq!(config.end_marker(), "# END sshenc managed block");
658 }
659
660 #[test]
661 fn full_block_contains_begin_and_end_markers() {
662 let config = ShellBlockConfig::new("sshenc", "export SSH_AUTH_SOCK=test");
663 let block = config.full_block();
664 assert!(block.contains(&config.begin_marker()));
665 assert!(block.contains(&config.end_marker()));
666 }
667
668 #[test]
669 fn full_block_contains_block_content() {
670 let config = ShellBlockConfig::new("sshenc", "export SSH_AUTH_SOCK=test");
671 let block = config.full_block();
672 assert!(block.contains("export SSH_AUTH_SOCK=test"));
673 }
674
675 #[test]
676 fn full_block_structure_is_begin_content_end() {
677 let config = ShellBlockConfig::new("app", "body_line");
678 let block = config.full_block();
679 let begin_pos = block.find(&config.begin_marker()).unwrap();
680 let content_pos = block.find("body_line").unwrap();
681 let end_pos = block.find(&config.end_marker()).unwrap();
682 assert!(begin_pos < content_pos);
683 assert!(content_pos < end_pos);
684 }
685
686 #[test]
687 fn shell_config_paths_contains_bashrc() {
688 let home = Path::new("/home/user");
689 let paths = shell_config_paths(home);
690 assert!(paths.iter().any(|(_, p)| p.ends_with(".bashrc")));
691 }
692
693 #[test]
694 fn shell_config_paths_contains_zshrc() {
695 let home = Path::new("/home/user");
696 let paths = shell_config_paths(home);
697 assert!(paths.iter().any(|(_, p)| p.ends_with(".zshrc")));
698 }
699
700 #[test]
701 fn shell_config_paths_contains_profile() {
702 let home = Path::new("/home/user");
703 let paths = shell_config_paths(home);
704 assert!(paths.iter().any(|(_, p)| p.ends_with(".profile")));
705 }
706
707 #[test]
708 fn shell_config_paths_all_under_home() {
709 let home = Path::new("/home/testuser");
710 let paths = shell_config_paths(home);
711 assert!(!paths.is_empty());
712 for (_, path) in &paths {
713 assert!(
714 path.starts_with(home),
715 "path {path:?} should be under home dir"
716 );
717 }
718 }
719}