Skip to main content

xianyun_executor/
datadir.rs

1//! Data directory resolution for persistent caproni state.
2//!
3//! Ported from `internal/datadir/datadir.go`.
4
5use std::path::{Path, PathBuf};
6
7const BASE_PATH: &str = ".local/share/xianyun";
8/// Existing Go-caproni (and earlier Rust-port) installs persisted PKI/
9/// cert state here. Recognized as a fallback read/write location so an
10/// existing profile's state isn't orphaned by this rename -- see
11/// PORT_NOTES.md's naming-migration notes.
12const LEGACY_BASE_PATH: &str = ".local/share/caproni";
13
14/// Ported from `Dir`, with the home directory taken as an explicit
15/// parameter rather than read from `$HOME`/`os.UserHomeDir()` internally
16/// (see `PORT_NOTES.md`'s "Design notes" section). There is no zero-argument
17/// wrapper in this crate; production callers (e.g.
18/// `xianyun_cli::commands::up`) resolve `$HOME` themselves and pass it in,
19/// keeping the ambient-environment read at the CLI's edge rather than
20/// buried in a library function.
21///
22/// Creates the directory if it doesn't exist. The path is `<home>/
23/// .local/share/xianyun/<profile>/`, unless that doesn't exist yet AND
24/// `<home>/.local/share/caproni/<profile>/` (the pre-rename path) does,
25/// in which case the existing caproni-path profile directory is used
26/// instead of creating a fresh, empty one and orphaning the existing
27/// state.
28pub fn dir_in(home: &Path, profile: &str) -> Result<PathBuf, std::io::Error> {
29    let primary = home.join(BASE_PATH).join(profile);
30    if primary.is_dir() {
31        return Ok(primary);
32    }
33
34    let legacy = home.join(LEGACY_BASE_PATH).join(profile);
35    if legacy.is_dir() {
36        return Ok(legacy);
37    }
38
39    std::fs::create_dir_all(&primary)?;
40    Ok(primary)
41}
42
43/// Ported from `PKIDir`.
44pub fn pki_dir_in(home: &Path, profile: &str) -> Result<PathBuf, std::io::Error> {
45    get_or_create_sub_dir(home, profile, "pki")
46}
47
48/// Ported from `CertsDir`.
49pub fn certs_dir_in(home: &Path, profile: &str) -> Result<PathBuf, std::io::Error> {
50    get_or_create_sub_dir(home, profile, "certs.d")
51}
52
53/// Ported from `K3dRegistriesPath`.
54pub fn k3d_registries_path_in(home: &Path, profile: &str) -> Result<PathBuf, std::io::Error> {
55    let dir = dir_in(home, profile)?;
56    Ok(dir.join("k3d-registries.yaml"))
57}
58
59fn get_or_create_sub_dir(
60    home: &Path,
61    profile: &str,
62    subdir: &str,
63) -> Result<PathBuf, std::io::Error> {
64    let dir = dir_in(home, profile)?.join(subdir);
65    std::fs::create_dir_all(&dir)?;
66    Ok(dir)
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    // NOTE: every ported test here relies on Go's t.Setenv("HOME", ...)
74    // isolation. Rust's std::env::set_var is process-global and unsafe as
75    // of the 2024 edition; running these under `cargo test`'s default
76    // multi-threaded runner would race across tests that also read $HOME.
77    // Phase 4 should thread the home directory in explicitly (e.g. `dir(profile:
78    // &str, home: &Path)`) instead of reading the environment internally,
79    // matching the same design question flagged for `find_nss_db_paths`
80    // in xianyun-pki's PORT_NOTES.md. Ported faithfully below and left
81    // `#[ignore]`d for both reasons (stub + env isolation).
82
83    #[test]
84    fn dir_creates_directory_under_local_share_xianyun() {
85        let tmp_dir = tempfile::tempdir().unwrap();
86
87        let d = dir_in(tmp_dir.path(), "myprofile").unwrap();
88        assert_eq!(d, tmp_dir.path().join(".local/share/xianyun/myprofile"));
89        assert!(d.is_dir());
90    }
91
92    // Existing Go-caproni installs persisted PKI/cert state under
93    // .local/share/caproni/<profile>/; this port must keep reading it
94    // rather than silently starting a new, empty .local/share/xianyun/
95    // directory and orphaning that state (which would look like the dev
96    // CA vanished). See PORT_NOTES.md's naming-migration notes.
97    #[test]
98    fn dir_prefers_xianyun_path_when_it_already_exists() {
99        let tmp_dir = tempfile::tempdir().unwrap();
100        std::fs::create_dir_all(tmp_dir.path().join(".local/share/xianyun/myprofile")).unwrap();
101        std::fs::create_dir_all(tmp_dir.path().join(".local/share/caproni/myprofile")).unwrap();
102
103        let d = dir_in(tmp_dir.path(), "myprofile").unwrap();
104        assert_eq!(d, tmp_dir.path().join(".local/share/xianyun/myprofile"));
105    }
106
107    #[test]
108    fn dir_falls_back_to_existing_caproni_path() {
109        let tmp_dir = tempfile::tempdir().unwrap();
110        std::fs::create_dir_all(tmp_dir.path().join(".local/share/caproni/myprofile")).unwrap();
111
112        let d = dir_in(tmp_dir.path(), "myprofile").unwrap();
113        assert_eq!(d, tmp_dir.path().join(".local/share/caproni/myprofile"));
114    }
115
116    #[test]
117    fn dir_creates_under_xianyun_when_neither_exists() {
118        let tmp_dir = tempfile::tempdir().unwrap();
119
120        let d = dir_in(tmp_dir.path(), "myprofile").unwrap();
121        assert_eq!(d, tmp_dir.path().join(".local/share/xianyun/myprofile"));
122    }
123
124    #[test]
125    fn dir_profile_scoping() {
126        let tmp_dir = tempfile::tempdir().unwrap();
127
128        let d = dir_in(tmp_dir.path(), "xianyun").unwrap();
129        assert_eq!(d, tmp_dir.path().join(".local/share/xianyun/xianyun"));
130    }
131
132    #[test]
133    fn dir_idempotent() {
134        let tmp_dir = tempfile::tempdir().unwrap();
135        let d1 = dir_in(tmp_dir.path(), "test").unwrap();
136        let d2 = dir_in(tmp_dir.path(), "test").unwrap();
137        assert_eq!(d1, d2);
138    }
139
140    #[test]
141    fn pki_dir_creates_subdirectory() {
142        let tmp_dir = tempfile::tempdir().unwrap();
143
144        let d = pki_dir_in(tmp_dir.path(), "xianyun").unwrap();
145        assert_eq!(d, tmp_dir.path().join(".local/share/xianyun/xianyun/pki"));
146        assert!(d.is_dir());
147    }
148
149    #[test]
150    fn certs_dir_creates_subdirectory() {
151        let tmp_dir = tempfile::tempdir().unwrap();
152
153        let d = certs_dir_in(tmp_dir.path(), "myprofile").unwrap();
154        assert_eq!(
155            d,
156            tmp_dir
157                .path()
158                .join(".local/share/xianyun/myprofile/certs.d")
159        );
160        assert!(d.is_dir());
161    }
162}