Skip to main content

codewhale_paths/
lib.rs

1//! Canonical user-scoped runtime path resolution for Codewhale.
2//!
3//! This leaf crate owns only the environment and platform-home decision. File
4//! migration and per-subsystem fallback remain with the crate that owns those
5//! files.
6#![deny(missing_docs)]
7
8use std::ffi::OsString;
9use std::path::PathBuf;
10
11/// Canonical Codewhale app directory name under the user home.
12pub const CODEWHALE_APP_DIR: &str = ".codewhale";
13
14/// Legacy DeepSeek-branded directory retained for compatibility reads.
15pub const LEGACY_APP_DIR: &str = ".deepseek";
16
17/// Return the explicit Codewhale home override, if one is configured.
18///
19/// Unicode values are trimmed so whitespace-only values are treated as unset,
20/// matching the existing config and secret-store contract. Non-Unicode path
21/// values are preserved on platforms that support them instead of silently
22/// dropping an otherwise valid filesystem path.
23#[must_use]
24pub fn codewhale_home_override() -> Option<PathBuf> {
25    path_env("CODEWHALE_HOME")
26}
27
28/// Whether `CODEWHALE_HOME` establishes an explicit isolation boundary.
29#[must_use]
30pub fn codewhale_home_is_explicit() -> bool {
31    codewhale_home_override().is_some()
32}
33
34/// Return the legacy `DEEPSEEK_HOME` compatibility override, if configured.
35///
36/// New state must use [`codewhale_home`]. This resolver exists only for readers
37/// whose persisted format still explicitly supports the legacy environment
38/// alias.
39#[must_use]
40pub fn legacy_deepseek_home_override() -> Option<PathBuf> {
41    path_env("DEEPSEEK_HOME")
42}
43
44/// Resolve the user's platform home, preferring `HOME` before `USERPROFILE`.
45///
46/// The explicit environment order makes CLI, state, config, and secret paths
47/// deterministic in hermetic shells. On Windows, `HOMEDRIVE` plus `HOMEPATH`
48/// remains a compatibility fallback before the platform resolver. The platform
49/// resolver remains last for ordinary desktop launches without those variables.
50#[must_use]
51pub fn user_home() -> Option<PathBuf> {
52    path_env("HOME")
53        .or_else(|| path_env("USERPROFILE"))
54        .or_else(windows_home_from_environment)
55        .or_else(dirs::home_dir)
56}
57
58#[cfg(windows)]
59fn windows_home_from_environment() -> Option<PathBuf> {
60    let mut path = path_env("HOMEDRIVE")?;
61    path.push(path_env("HOMEPATH")?);
62    (!path.as_os_str().is_empty()).then_some(path)
63}
64
65#[cfg(not(windows))]
66fn windows_home_from_environment() -> Option<PathBuf> {
67    None
68}
69
70/// Resolve the canonical Codewhale runtime home.
71///
72/// An explicit `CODEWHALE_HOME` is returned verbatim. Otherwise this is
73/// `<user home>/.codewhale`.
74#[must_use]
75pub fn codewhale_home() -> Option<PathBuf> {
76    codewhale_home_override().or_else(|| user_home().map(|home| home.join(CODEWHALE_APP_DIR)))
77}
78
79/// Resolve the ambient legacy DeepSeek home used for compatibility reads.
80///
81/// This never follows `CODEWHALE_HOME`: callers must suppress legacy fallback
82/// whenever [`codewhale_home_is_explicit`] is true.
83#[must_use]
84pub fn legacy_deepseek_home() -> Option<PathBuf> {
85    user_home().map(|home| home.join(LEGACY_APP_DIR))
86}
87
88fn path_env(name: &str) -> Option<PathBuf> {
89    std::env::var_os(name).and_then(normalize_path_value)
90}
91
92fn normalize_path_value(value: OsString) -> Option<PathBuf> {
93    if value.is_empty() {
94        return None;
95    }
96    match value.to_str() {
97        Some(value) => {
98            let value = value.trim();
99            (!value.is_empty()).then(|| PathBuf::from(value))
100        }
101        None => Some(PathBuf::from(value)),
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn unicode_path_values_are_trimmed_and_whitespace_is_unset() {
111        assert_eq!(
112            normalize_path_value(OsString::from("  /tmp/codewhale  ")),
113            Some(PathBuf::from("/tmp/codewhale"))
114        );
115        assert_eq!(normalize_path_value(OsString::from(" \t\n ")), None);
116        assert_eq!(normalize_path_value(OsString::new()), None);
117    }
118
119    #[cfg(unix)]
120    #[test]
121    fn unix_non_unicode_path_values_are_preserved() {
122        use std::os::unix::ffi::OsStringExt;
123
124        let value = OsString::from_vec(b"codewhale-\xff-home".to_vec());
125        assert_eq!(
126            normalize_path_value(value.clone()),
127            Some(PathBuf::from(value))
128        );
129    }
130
131    #[cfg(windows)]
132    #[test]
133    fn windows_non_unicode_path_values_are_preserved() {
134        use std::os::windows::ffi::OsStringExt;
135
136        let value = OsString::from_wide(&[b'C' as u16, b':' as u16, b'\\' as u16, 0xd800]);
137        assert_eq!(
138            normalize_path_value(value.clone()),
139            Some(PathBuf::from(value))
140        );
141    }
142}