1#![allow(dead_code)]
2use std::env;
11use std::fs;
12use std::path::{Path, PathBuf};
13use std::sync::OnceLock;
14
15pub const MUX_CONFIG_HOME_ENV: &str = "AGENTMUX_CONFIG_HOME";
18pub const MUX_DATA_HOME_ENV: &str = "AGENTMUX_DATA_HOME";
19pub const MUX_APP_PATH_ENV: &str = "AGENTMUX_APP_PATH";
20pub const MUX_DEV_ENV: &str = "AGENTMUX_DEV";
21pub const MUX_DEV_VITE_ENV: &str = "AGENTMUX_DEV_VITE";
22pub const MUX_JWT_TOKEN_ENV: &str = "AGENTMUX_JWT";
23pub const MUX_SWAP_TOKEN_ENV: &str = "AGENTMUX_SWAPTOKEN";
24
25pub const MUX_LOCK_FILE: &str = "wave.lock";
28pub const DOMAIN_SOCKET_BASE_NAME: &str = "wave.sock";
29pub const REMOTE_DOMAIN_SOCKET_BASE_NAME: &str = "wave-remote.sock";
30pub const MUX_DB_DIR: &str = "db";
31pub const CONFIG_DIR: &str = "config";
32pub const REMOTE_MUX_HOME_DIR_NAME: &str = ".agentmux";
33pub const REMOTE_FULL_DOMAIN_SOCKET_PATH: &str = "~/.agentmux/wave-remote.sock";
34
35static MUX_VERSION: OnceLock<String> = OnceLock::new();
38static BUILD_TIME: OnceLock<String> = OnceLock::new();
39
40pub fn set_version(version: &str) {
42 let _ = MUX_VERSION.set(version.to_string());
43}
44
45pub fn set_build_time(time: &str) {
47 let _ = BUILD_TIME.set(time.to_string());
48}
49
50pub fn get_version() -> &'static str {
52 MUX_VERSION.get().map_or("0.0.0", |v| v.as_str())
53}
54
55pub fn get_build_time() -> &'static str {
57 BUILD_TIME.get().map_or("0", |v| v.as_str())
58}
59
60pub fn get_home_dir() -> PathBuf {
64 dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"))
65}
66
67pub fn get_mux_data_dir() -> PathBuf {
85 match agentmux_common::data_paths::agentmux_root() {
86 Ok(p) => p,
87 Err(e) => {
88 tracing::error!(
89 "cannot resolve the AgentMux root ({e}); falling back to a relative .agentmux rather than /"
90 );
91 PathBuf::from(".agentmux")
92 }
93 }
94}
95
96pub fn migrate_legacy_data_dir() {
99 let new_dir = get_mux_data_dir();
100 if new_dir.exists() {
101 return; }
103 let old_dir = get_home_dir().join(".waveterm");
104 if !old_dir.exists() {
105 return; }
107 tracing::info!(
108 "Migrating data directory from {} to {}",
109 old_dir.display(),
110 new_dir.display()
111 );
112 if let Err(e) = copy_dir_all(&old_dir, &new_dir) {
113 tracing::warn!("Data migration failed (continuing with empty dir): {}", e);
114 } else {
115 tracing::info!("Migration complete");
116 }
117}
118
119fn copy_dir_all(src: &Path, dst: &Path) -> Result<(), String> {
121 fs::create_dir_all(dst)
122 .map_err(|e| format!("cannot create {}: {}", dst.display(), e))?;
123 for entry in fs::read_dir(src)
124 .map_err(|e| format!("cannot read {}: {}", src.display(), e))?
125 {
126 let entry = entry.map_err(|e| format!("read_dir entry error: {}", e))?;
127 let src_path = entry.path();
128 let dst_path = dst.join(entry.file_name());
129 if src_path.is_dir() {
130 copy_dir_all(&src_path, &dst_path)?;
131 } else {
132 fs::copy(&src_path, &dst_path)
133 .map_err(|e| format!("copy {} → {}: {}", src_path.display(), dst_path.display(), e))?;
134 }
135 }
136 Ok(())
137}
138
139pub fn get_mux_config_dir() -> PathBuf {
142 if let Some(dir) = env::var_os(MUX_CONFIG_HOME_ENV) {
143 if !dir.is_empty() {
144 return PathBuf::from(dir);
145 }
146 }
147 get_mux_data_dir().join(CONFIG_DIR)
148}
149
150pub fn get_mux_db_dir() -> PathBuf {
152 get_mux_data_dir().join(MUX_DB_DIR)
153}
154
155pub fn get_mux_app_path() -> Option<PathBuf> {
157 env::var(MUX_APP_PATH_ENV).ok().map(PathBuf::from)
158}
159
160pub fn get_mux_app_bin_path() -> Option<PathBuf> {
162 get_mux_app_path().map(|p| p.join("bin"))
163}
164
165pub fn get_domain_socket_name() -> PathBuf {
167 get_mux_data_dir().join(DOMAIN_SOCKET_BASE_NAME)
168}
169
170pub fn get_mux_lock_file() -> PathBuf {
172 get_mux_data_dir().join(MUX_LOCK_FILE)
173}
174
175pub fn ensure_dir(dir: &Path) -> Result<(), String> {
179 if dir.exists() {
180 return Ok(());
181 }
182 fs::create_dir_all(dir).map_err(|e| format!("cannot create directory {}: {}", dir.display(), e))
183}
184
185pub fn ensure_mux_data_dir() -> Result<(), String> {
187 ensure_dir(&get_mux_data_dir())
188}
189
190pub fn ensure_mux_db_dir() -> Result<(), String> {
192 ensure_dir(&get_mux_db_dir())
193}
194
195pub fn ensure_mux_config_dir() -> Result<(), String> {
197 ensure_dir(&get_mux_config_dir())
198}
199
200pub fn ensure_mux_presets_dir() -> Result<(), String> {
202 ensure_dir(&get_mux_config_dir().join("presets"))
203}
204
205pub struct MuxLock {
209 #[allow(dead_code)]
210 file: fs::File,
211}
212
213impl MuxLock {
214 pub fn acquire() -> Result<Self, String> {
217 Self::acquire_at(&get_mux_lock_file())
218 }
219
220 #[cfg(unix)]
222 pub fn acquire_at(lock_path: &Path) -> Result<Self, String> {
223 use std::os::unix::io::AsRawFd;
224
225 ensure_dir(lock_path.parent().unwrap_or(Path::new("/")))?;
226
227 let file = fs::OpenOptions::new()
228 .create(true)
229 .write(true)
230 .truncate(false)
231 .open(lock_path)
232 .map_err(|e| format!("cannot open lock file {}: {}", lock_path.display(), e))?;
233
234 let fd = file.as_raw_fd();
235 let result = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
236 if result != 0 {
237 return Err("another AgentMux instance is already running".to_string());
238 }
239
240 Ok(MuxLock { file })
241 }
242
243 #[cfg(not(unix))]
245 pub fn acquire_at(lock_path: &Path) -> Result<Self, String> {
246 ensure_dir(lock_path.parent().unwrap_or(Path::new("/")))?;
247
248 let file = fs::OpenOptions::new()
249 .create(true)
250 .write(true)
251 .truncate(true)
252 .open(lock_path)
253 .map_err(|e| format!("cannot open lock file {}: {}", lock_path.display(), e))?;
254
255 Ok(MuxLock { file })
256 }
257}
258
259pub const SRV_DATA_LOCK_FILE: &str = "srv.lock";
261
262static DATA_DIR_LOCKS: std::sync::Mutex<Vec<(PathBuf, MuxLock)>> = std::sync::Mutex::new(Vec::new());
264
265pub fn acquire_data_dir_lock(data_dir: &Path) -> Result<(), String> {
271 let mut held = DATA_DIR_LOCKS.lock().unwrap_or_else(|e| e.into_inner());
272 if held.iter().any(|(p, _)| p == data_dir) {
273 return Ok(());
274 }
275 let lock_path = data_dir.join(SRV_DATA_LOCK_FILE);
276 let mut lock = MuxLock::acquire_at(&lock_path).map_err(|_| {
277 format!(
278 "another agentmux-srv is already using {} (lock {})",
279 data_dir.display(),
280 lock_path.display()
281 )
282 })?;
283 {
288 use std::io::{Seek, Write};
289 let _ = lock.file.set_len(0);
290 let _ = lock.file.seek(std::io::SeekFrom::Start(0));
291 let _ = write!(lock.file, "{}", std::process::id());
292 }
293 held.push((data_dir.to_path_buf(), lock));
294 Ok(())
295}
296
297pub fn is_dev_mode() -> bool {
301 env::var(MUX_DEV_ENV)
302 .map(|v| !v.is_empty())
303 .unwrap_or(false)
304}
305
306pub fn expand_home_dir(path: &str) -> Result<PathBuf, String> {
309 if path.split(['/', '\\']).any(|c| c == "..") {
311 return Err(format!("cannot expand path: '..' traversal in {}", path));
312 }
313
314 if let Some(rest) = path.strip_prefix('~') {
315 let home = get_home_dir();
316 if rest.is_empty() || rest.starts_with('/') || rest.starts_with('\\') {
317 Ok(home.join(rest.trim_start_matches(['/', '\\'])))
318 } else {
319 Err(format!("cannot expand ~: invalid ~user in {}", path))
320 }
321 } else {
322 Ok(PathBuf::from(path))
323 }
324}
325
326pub fn expand_home_dir_safe(path: &str) -> PathBuf {
328 expand_home_dir(path).unwrap_or_else(|_| PathBuf::from(path))
329}
330
331pub fn msys_to_windows_path(path: &str) -> String {
347 #[cfg(windows)]
348 {
349 let stripped = path.strip_prefix("/mnt").unwrap_or(path);
352 let bytes = stripped.as_bytes();
353 let is_drive_path = bytes.len() >= 2
354 && bytes[0] == b'/'
355 && (bytes[1] as char).is_ascii_alphabetic()
356 && (bytes.len() == 2 || bytes[2] == b'/');
357 if is_drive_path {
358 let drive = (bytes[1] as char).to_ascii_uppercase();
359 let rest = stripped[2..].replace('/', "\\"); return format!("{drive}:{}", if rest.is_empty() { "\\" } else { &rest });
361 }
362 }
363 let _ = path; path.to_string()
365}
366
367pub fn normalize_working_dir(raw: &str) -> Option<String> {
372 let raw = raw.trim();
373 if raw.is_empty() {
374 return None;
375 }
376 let win = msys_to_windows_path(raw);
377 Some(expand_home_dir_safe(&win).to_string_lossy().into_owned())
378}
379
380pub fn verify_no_symlink_escape(
400 final_path: &Path,
401 base_canonical: &Path,
402) -> Result<(), String> {
403 let mut p = final_path;
404 loop {
405 match p.canonicalize() {
406 Ok(canonical) => {
407 if !canonical.starts_with(base_canonical) {
408 return Err(format!(
409 "symlinked ancestor escapes working dir: {} → {}",
410 p.display(),
411 canonical.display()
412 ));
413 }
414 return Ok(());
415 }
416 Err(_) => match p.parent() {
417 Some(parent) if !parent.as_os_str().is_empty() => p = parent,
418 _ => {
419 return Err(format!(
423 "no existing ancestor found under base: {}",
424 final_path.display()
425 ));
426 }
427 },
428 }
429 }
430}
431
432fn has_drive_letter_prefix(s: &str) -> bool {
437 let mut chars = s.chars();
438 matches!(
439 (chars.next(), chars.next()),
440 (Some(c), Some(':')) if c.is_ascii_alphabetic()
441 )
442}
443
444pub fn safe_join_within_base(base: &Path, relative: &str) -> Result<PathBuf, String> {
459 if relative.is_empty() {
460 return Err("safe_join_within_base: empty relative path".into());
461 }
462 let rel_path = Path::new(relative);
463 if rel_path.is_absolute() {
464 return Err(format!("safe_join_within_base: absolute path not allowed: {relative}"));
465 }
466 if matches!(relative.chars().next(), Some('/') | Some('\\')) {
471 return Err(format!("safe_join_within_base: rooted path not allowed: {relative}"));
472 }
473 if has_drive_letter_prefix(relative) {
479 return Err(format!(
480 "safe_join_within_base: drive-letter prefix not allowed: {relative}"
481 ));
482 }
483 let mut cleaned = PathBuf::new();
484 for segment in relative.split(['/', '\\']) {
485 match segment {
486 "" | "." => continue,
487 ".." => {
488 return Err(format!(
489 "safe_join_within_base: '..' traversal not allowed: {relative}"
490 ));
491 }
492 other if has_drive_letter_prefix(other) => {
498 return Err(format!(
499 "safe_join_within_base: drive-letter prefix in segment {other:?}: {relative}"
500 ));
501 }
502 other => cleaned.push(other),
503 }
504 }
505 if cleaned.as_os_str().is_empty() {
506 return Err(format!(
507 "safe_join_within_base: relative path resolves to empty: {relative}"
508 ));
509 }
510 Ok(base.join(cleaned))
511}
512
513pub fn replace_home_dir(path: &str) -> String {
515 let home = get_home_dir();
516 let home_str = home.to_string_lossy();
517 if path.starts_with(home_str.as_ref()) {
518 format!("~{}", &path[home_str.len()..])
519 } else {
520 path.to_string()
521 }
522}
523
524pub fn client_arch() -> String {
528 format!("{}/{}", std::env::consts::OS, std::env::consts::ARCH)
529}
530
531pub fn get_system_summary() -> String {
533 format!(
534 "{} {} ({})",
535 std::env::consts::OS,
536 std::env::consts::ARCH,
537 whoami::distro()
538 )
539}
540
541pub fn determine_lang() -> String {
543 env::var("LANG")
544 .or_else(|_| env::var("LC_ALL"))
545 .or_else(|_| env::var("LANGUAGE"))
546 .unwrap_or_else(|_| "en_US".to_string())
547}
548
549#[cfg(test)]
552mod tests {
553 use super::*;
554
555 #[test]
556 fn test_env_var_constants() {
557 assert_eq!(MUX_CONFIG_HOME_ENV, "AGENTMUX_CONFIG_HOME");
558 assert_eq!(MUX_DATA_HOME_ENV, "AGENTMUX_DATA_HOME");
559 assert_eq!(MUX_DEV_ENV, "AGENTMUX_DEV");
560 }
561
562 #[test]
563 fn test_file_constants() {
564 assert_eq!(MUX_LOCK_FILE, "wave.lock");
565 assert_eq!(DOMAIN_SOCKET_BASE_NAME, "wave.sock");
566 assert_eq!(MUX_DB_DIR, "db");
567 assert_eq!(CONFIG_DIR, "config");
568 }
569
570 #[test]
571 fn test_version_management() {
572 assert!(!get_version().is_empty());
575 }
576
577 #[test]
578 fn test_mux_data_dir_default() {
579 let _guard = crate::test_support::ISOLATED_AUTH_ENV_LOCK
587 .lock()
588 .unwrap_or_else(|e| e.into_inner());
589 let dir = get_mux_data_dir();
591 assert!(dir.to_string_lossy().contains(".agentmux") || dir.to_string_lossy().contains("AGENTMUX"));
592 }
593
594 #[test]
595 fn test_mux_db_dir() {
596 let db_dir = get_mux_db_dir();
597 assert!(db_dir.to_string_lossy().ends_with("db"));
598 }
599
600 #[test]
601 fn test_mux_config_dir() {
602 let config_dir = get_mux_config_dir();
603 assert!(config_dir.to_string_lossy().contains("config") || config_dir.to_string_lossy().contains("AGENTMUX"));
604 }
605
606 #[test]
607 fn test_domain_socket_name() {
608 let sock = get_domain_socket_name();
609 assert!(sock.to_string_lossy().ends_with("wave.sock"));
610 }
611
612 #[test]
613 fn test_lock_file_path() {
614 let lock = get_mux_lock_file();
615 assert!(lock.to_string_lossy().ends_with("wave.lock"));
616 }
617
618 #[test]
619 fn test_expand_home_dir_tilde() {
620 let path = expand_home_dir("~/docs").unwrap();
621 assert!(path.to_string_lossy().contains("docs"));
622 assert!(!path.to_string_lossy().starts_with('~'));
623 }
624
625 #[test]
626 fn test_expand_home_dir_tilde_only() {
627 let path = expand_home_dir("~").unwrap();
628 assert!(!path.to_string_lossy().starts_with('~'));
629 }
630
631 #[test]
632 fn test_expand_home_dir_no_tilde() {
633 let path = expand_home_dir("/etc/hosts").unwrap();
634 assert_eq!(path, PathBuf::from("/etc/hosts"));
635 }
636
637 #[test]
638 fn test_expand_home_dir_traversal() {
639 let result = expand_home_dir("~otheruser/docs");
641 assert!(result.is_err());
642 }
643
644 #[test]
645 fn test_expand_home_dir_dotdot_traversal() {
646 assert!(expand_home_dir("~/../../etc/passwd").is_err());
648 assert!(expand_home_dir("../../../root").is_err());
649 assert!(expand_home_dir("/tmp/../etc/shadow").is_err());
650 assert!(expand_home_dir("~/.config").is_ok());
652 assert!(expand_home_dir("/tmp/.hidden").is_ok());
653 }
654
655 #[test]
656 fn test_safe_join_within_base_simple() {
657 let base = PathBuf::from("/home/user/agent");
658 let p = safe_join_within_base(&base, "CLAUDE.md").unwrap();
659 assert_eq!(p, PathBuf::from("/home/user/agent/CLAUDE.md"));
660 }
661
662 #[test]
663 fn test_safe_join_within_base_nested() {
664 let base = PathBuf::from("/home/user/agent");
665 let p = safe_join_within_base(&base, ".claude/commands/startup.md").unwrap();
666 assert_eq!(p, PathBuf::from("/home/user/agent/.claude/commands/startup.md"));
667 }
668
669 #[test]
670 fn test_safe_join_within_base_backslash_separator() {
671 let base = PathBuf::from("C:\\agent");
673 let p = safe_join_within_base(&base, ".claude\\commands\\startup.md").unwrap();
674 let s = p.to_string_lossy();
677 assert!(s.contains(".claude"));
678 assert!(s.contains("commands"));
679 assert!(s.contains("startup.md"));
680 }
681
682 #[test]
683 fn test_safe_join_within_base_dot_segments_skipped() {
684 let base = PathBuf::from("/base");
685 let p = safe_join_within_base(&base, "./a/./b").unwrap();
686 assert_eq!(p, PathBuf::from("/base/a/b"));
687 }
688
689 #[test]
690 fn test_safe_join_within_base_rejects_dotdot() {
691 let base = PathBuf::from("/base");
692 assert!(safe_join_within_base(&base, "..").is_err());
693 assert!(safe_join_within_base(&base, "../etc").is_err());
694 assert!(safe_join_within_base(&base, "a/../b").is_err());
695 assert!(safe_join_within_base(&base, "a/b/..").is_err());
696 }
697
698 #[test]
699 fn test_safe_join_within_base_rejects_absolute() {
700 let base = PathBuf::from("/base");
701 assert!(safe_join_within_base(&base, "/etc/passwd").is_err());
702 assert!(safe_join_within_base(&base, "\\Windows\\System32").is_err());
703 assert!(safe_join_within_base(&base, "/foo").is_err());
705 #[cfg(windows)]
706 assert!(safe_join_within_base(&base, "C:\\Windows").is_err());
707 }
708
709 #[test]
710 fn test_safe_join_within_base_rejects_drive_letter_prefix() {
711 let base = PathBuf::from("/base");
716 assert!(safe_join_within_base(&base, "C:foo").is_err());
717 assert!(safe_join_within_base(&base, "D:payload.txt").is_err());
718 assert!(safe_join_within_base(&base, "z:relative\\file").is_err());
719 assert!(safe_join_within_base(&base, "C:\\Windows\\System32").is_err());
722 }
723
724 #[test]
725 fn test_safe_join_within_base_rejects_drive_letter_in_inner_segment() {
726 let base = PathBuf::from("/base");
732 assert!(safe_join_within_base(&base, "safe/C:payload.txt").is_err());
733 assert!(safe_join_within_base(&base, "a/b/D:malicious").is_err());
734 assert!(safe_join_within_base(&base, "subdir\\E:nested").is_err());
735 assert!(safe_join_within_base(&base, "C:foo/bar").is_err());
738 }
739
740 #[test]
741 fn test_verify_no_symlink_escape_existing_inside_base() {
742 let base = std::env::temp_dir();
746 let canonical_base = base.canonicalize().unwrap();
747 let target = base.join("nonexistent-subdir").join("file.md");
748 assert!(verify_no_symlink_escape(&target, &canonical_base).is_ok());
749 }
750
751 #[test]
752 #[cfg(unix)]
753 fn test_verify_no_symlink_escape_rejects_symlinked_ancestor() {
754 use std::os::unix::fs::symlink;
759 let tmp = std::env::temp_dir();
760 let base = tmp.join("agentmux-symlink-test-base");
761 let outside = tmp.join("agentmux-symlink-test-outside");
762 let _ = std::fs::remove_dir_all(&base);
763 let _ = std::fs::remove_dir_all(&outside);
764 std::fs::create_dir_all(&base).unwrap();
765 std::fs::create_dir_all(&outside).unwrap();
766 let canonical_base = base.canonicalize().unwrap();
767 symlink(&outside, base.join(".claude")).unwrap();
768 let target = base.join(".claude").join("commands").join("startup.md");
769 assert!(verify_no_symlink_escape(&target, &canonical_base).is_err());
770 let _ = std::fs::remove_dir_all(&base);
772 let _ = std::fs::remove_dir_all(&outside);
773 }
774
775 #[test]
776 fn test_has_drive_letter_prefix() {
777 assert!(has_drive_letter_prefix("C:foo"));
778 assert!(has_drive_letter_prefix("c:bar"));
779 assert!(has_drive_letter_prefix("Z:\\baz"));
780 assert!(!has_drive_letter_prefix(":foo"));
781 assert!(!has_drive_letter_prefix("foo"));
782 assert!(!has_drive_letter_prefix("12:not-a-drive"));
783 assert!(!has_drive_letter_prefix(""));
784 assert!(!has_drive_letter_prefix("Ω:foo"));
788 }
789
790 #[test]
791 #[cfg(windows)]
792 fn test_msys_to_windows_path() {
793 assert_eq!(msys_to_windows_path("/c/Users/user/p"), "C:\\Users\\user\\p");
794 assert_eq!(msys_to_windows_path("/mnt/c/Users/user/p"), "C:\\Users\\user\\p");
795 assert_eq!(msys_to_windows_path("/d/work"), "D:\\work");
796 assert_eq!(msys_to_windows_path("/c"), "C:\\");
797 assert_eq!(msys_to_windows_path("C:\\Users\\user"), "C:\\Users\\user");
799 assert_eq!(msys_to_windows_path("/usr/bin"), "/usr/bin");
801 assert_eq!(msys_to_windows_path("/ab/c"), "/ab/c");
803 }
804
805 #[test]
806 fn test_normalize_working_dir_empty() {
807 assert_eq!(normalize_working_dir(""), None);
808 assert_eq!(normalize_working_dir(" "), None);
809 }
810
811 #[test]
812 fn test_safe_join_within_base_rejects_empty() {
813 let base = PathBuf::from("/base");
814 assert!(safe_join_within_base(&base, "").is_err());
815 assert!(safe_join_within_base(&base, "./.").is_err());
817 }
818
819 #[test]
820 fn test_expand_home_dir_safe() {
821 let path = expand_home_dir_safe("~/test");
822 assert!(!path.to_string_lossy().starts_with('~'));
823
824 let path = expand_home_dir_safe("~otheruser/test");
826 assert_eq!(path, PathBuf::from("~otheruser/test"));
827 }
828
829 #[test]
830 fn test_replace_home_dir() {
831 let home = get_home_dir();
832 let path = format!("{}/documents/file.txt", home.display());
833 let replaced = replace_home_dir(&path);
834 assert!(replaced.starts_with('~'));
835 assert!(replaced.contains("documents/file.txt"));
836 }
837
838 #[test]
839 fn test_replace_home_dir_no_match() {
840 let result = replace_home_dir("/etc/hosts");
841 assert_eq!(result, "/etc/hosts");
842 }
843
844 #[test]
845 fn test_client_arch() {
846 let arch = client_arch();
847 assert!(arch.contains('/'));
848 }
849
850 #[test]
851 fn test_system_summary() {
852 let summary = get_system_summary();
853 assert!(!summary.is_empty());
854 }
855
856 #[test]
857 fn test_determine_lang() {
858 let lang = determine_lang();
859 assert!(!lang.is_empty());
860 }
861
862 #[test]
863 fn test_is_dev_mode() {
864 let _ = is_dev_mode(); }
867
868 #[test]
869 fn test_ensure_dir_existing() {
870 assert!(ensure_dir(Path::new("/tmp")).is_ok());
872 }
873}