diff --git a/crates/manifest/src/paths.rs b/crates/manifest/src/paths.rs index 178e1dc6..22db9351 100644 --- a/crates/manifest/src/paths.rs +++ b/crates/manifest/src/paths.rs @@ -31,43 +31,33 @@ pub const RESOURCE_DIR_ENV: &str = "INSOMNIA_RESOURCE_DIR"; /// 設定ディレクトリ。`profiles.toml`, `providers.toml`, `models.toml`, /// `prompts/` などが置かれる。 pub fn config_dir() -> Option { - if let Some(p) = env_path("INSOMNIA_CONFIG_DIR") { - return Some(p); - } - if let Some(p) = env_path("INSOMNIA_HOME") { - return Some(p.join("config")); - } - if let Some(p) = env_path("XDG_CONFIG_HOME") { - return Some(p.join("insomnia")); - } - Some(env_path("HOME")?.join(".config").join("insomnia")) + resolve_config_dir_from_parts( + env_path("INSOMNIA_CONFIG_DIR"), + env_path("INSOMNIA_HOME"), + env_path("XDG_CONFIG_HOME"), + env_path("HOME"), + ) } /// データディレクトリ。`sessions/` などプログラムが書く永続データの /// 置き場。 pub fn data_dir() -> Option { - if let Some(p) = env_path("INSOMNIA_DATA_DIR") { - return Some(p); - } - if let Some(p) = env_path("INSOMNIA_HOME") { - return Some(p); - } - Some(env_path("HOME")?.join(".insomnia")) + resolve_data_dir_from_parts( + env_path("INSOMNIA_DATA_DIR"), + env_path("INSOMNIA_HOME"), + env_path("HOME"), + ) } /// ランタイムディレクトリ。socket, `pods.json`, Pod ごとの `pid` / /// `status.json` 等が置かれる。再起動で消えて構わない。 pub fn runtime_dir() -> Option { - if let Some(p) = env_path("INSOMNIA_RUNTIME_DIR") { - return Some(p); - } - if let Some(p) = env_path("INSOMNIA_HOME") { - return Some(p.join("run")); - } - if let Some(p) = env_path("XDG_RUNTIME_DIR") { - return Some(p.join("insomnia")); - } - Some(env_path("HOME")?.join(".insomnia").join("run")) + resolve_runtime_dir_from_parts( + env_path("INSOMNIA_RUNTIME_DIR"), + env_path("INSOMNIA_HOME"), + env_path("XDG_RUNTIME_DIR"), + env_path("HOME"), + ) } // ---- well-known file getters ------------------------------------------------ @@ -77,12 +67,12 @@ pub fn runtime_dir() -> Option { /// This is application/profile selection configuration, not a Pod manifest /// layer. pub fn user_profiles_path() -> Option { - Some(config_dir()?.join("profiles.toml")) + user_profiles_path_from_config_dir(config_dir()) } /// `/prompts/` — user prompts ライブラリ。 pub fn user_prompts_dir() -> Option { - Some(config_dir()?.join("prompts")) + user_prompts_dir_from_config_dir(config_dir()) } /// Root resource directory used for bundled prompts, profiles, catalogs, and docs. @@ -113,28 +103,28 @@ pub fn builtin_profiles_dir() -> Option { /// `/prompts.toml` — user prompt pack。 pub fn user_pack_file() -> Option { - Some(config_dir()?.join("prompts.toml")) + user_pack_file_from_config_dir(config_dir()) } /// `/` — providers.toml / models.toml 等の /// user override ファイル。 pub fn user_catalog_override(file_name: &str) -> Option { - Some(config_dir()?.join(file_name)) + user_catalog_override_from_config_dir(config_dir(), file_name) } /// `/sessions/` — session store のデフォルト位置。 pub fn sessions_dir() -> Option { - Some(data_dir()?.join("sessions")) + sessions_dir_from_data_dir(data_dir()) } /// `/pods.json` — machine-wide Pod allocation registry。 pub fn pod_registry_path() -> Option { - Some(runtime_dir()?.join("pods.json")) + pod_registry_path_from_runtime_dir(runtime_dir()) } /// `//` — Pod ごとのランタイムディレクトリ。 pub fn pod_runtime_dir(pod_name: &str) -> Option { - Some(runtime_dir()?.join(pod_name)) + pod_runtime_dir_from_runtime_dir(runtime_dir(), pod_name) } /// `//sock` — Pod の Unix socket パス。 @@ -144,206 +134,306 @@ pub fn pod_runtime_dir(pod_name: &str) -> Option { /// attach フロー等) からの**予測**はこの関数で行う。両者は同じパス /// を返すことが期待される。 pub fn pod_socket_path(pod_name: &str) -> Option { - Some(pod_runtime_dir(pod_name)?.join("sock")) + pod_socket_path_from_runtime_dir(runtime_dir(), pod_name) } // ---- internals -------------------------------------------------------------- +fn resolve_config_dir_from_parts( + insomnia_config_dir: Option, + insomnia_home: Option, + xdg_config_home: Option, + home: Option, +) -> Option { + if let Some(p) = insomnia_config_dir { + return Some(p); + } + if let Some(p) = insomnia_home { + return Some(p.join("config")); + } + if let Some(p) = xdg_config_home { + return Some(p.join("insomnia")); + } + Some(home?.join(".config").join("insomnia")) +} + +fn resolve_data_dir_from_parts( + insomnia_data_dir: Option, + insomnia_home: Option, + home: Option, +) -> Option { + if let Some(p) = insomnia_data_dir { + return Some(p); + } + if let Some(p) = insomnia_home { + return Some(p); + } + Some(home?.join(".insomnia")) +} + +fn resolve_runtime_dir_from_parts( + insomnia_runtime_dir: Option, + insomnia_home: Option, + xdg_runtime_dir: Option, + home: Option, +) -> Option { + if let Some(p) = insomnia_runtime_dir { + return Some(p); + } + if let Some(p) = insomnia_home { + return Some(p.join("run")); + } + if let Some(p) = xdg_runtime_dir { + return Some(p.join("insomnia")); + } + Some(home?.join(".insomnia").join("run")) +} + +fn user_profiles_path_from_config_dir(config_dir: Option) -> Option { + Some(config_dir?.join("profiles.toml")) +} + +fn user_prompts_dir_from_config_dir(config_dir: Option) -> Option { + Some(config_dir?.join("prompts")) +} + +fn user_pack_file_from_config_dir(config_dir: Option) -> Option { + Some(config_dir?.join("prompts.toml")) +} + +fn user_catalog_override_from_config_dir( + config_dir: Option, + file_name: &str, +) -> Option { + Some(config_dir?.join(file_name)) +} + +fn sessions_dir_from_data_dir(data_dir: Option) -> Option { + Some(data_dir?.join("sessions")) +} + +fn pod_registry_path_from_runtime_dir(runtime_dir: Option) -> Option { + Some(runtime_dir?.join("pods.json")) +} + +fn pod_runtime_dir_from_runtime_dir( + runtime_dir: Option, + pod_name: &str, +) -> Option { + Some(runtime_dir?.join(pod_name)) +} + +fn pod_socket_path_from_runtime_dir( + runtime_dir: Option, + pod_name: &str, +) -> Option { + Some(pod_runtime_dir_from_runtime_dir(runtime_dir, pod_name)?.join("sock")) +} + /// 空文字列の env は未設定として扱う。`std::env::var` は `Ok("")` と /// `Err(NotPresent)` を区別するが、パス解決においては両者を未設定と /// 同等に扱うのが直感的。 fn env_path(name: &str) -> Option { - std::env::var(name) - .ok() - .filter(|s| !s.is_empty()) - .map(PathBuf::from) + let value = std::env::var(name).ok()?; + path_from_env_value(Some(value.as_str())) +} + +fn path_from_env_value(value: Option<&str>) -> Option { + value.filter(|s| !s.is_empty()).map(PathBuf::from) } #[cfg(test)] mod tests { use super::*; - use std::sync::{Mutex, MutexGuard, OnceLock}; - - /// プロセス全体で env を弄るテスト同士が並行に走らないように保護 - /// する。Cargo の test harness はファイル単位で別プロセスにせず - /// マルチスレッドで実行するため、env を読む全テストはこの lock を - /// 取ってから操作する。 - fn env_lock() -> MutexGuard<'static, ()> { - static LOCK: OnceLock> = OnceLock::new(); - LOCK.get_or_init(|| Mutex::new(())) - .lock() - .unwrap_or_else(|e| e.into_inner()) - } - - /// テスト中だけ env を上書きし、drop 時に元の値に戻す RAII guard。 - struct EnvGuard { - vars: Vec<(&'static str, Option)>, - _lock: MutexGuard<'static, ()>, - } - - impl EnvGuard { - fn new(overrides: &[(&'static str, Option<&str>)]) -> Self { - let lock = env_lock(); - let names = [ - "INSOMNIA_CONFIG_DIR", - "INSOMNIA_DATA_DIR", - "INSOMNIA_RUNTIME_DIR", - "INSOMNIA_RESOURCE_DIR", - "INSOMNIA_HOME", - "XDG_CONFIG_HOME", - "XDG_RUNTIME_DIR", - "HOME", - ]; - let saved: Vec<_> = names.iter().map(|n| (*n, std::env::var(n).ok())).collect(); - // SAFETY: env_lock() 取得済みなので env への並行アクセスは - // この test バイナリ内では発生しない。 - unsafe { - for (n, _) in &saved { - std::env::remove_var(n); - } - for (n, v) in overrides { - if let Some(v) = v { - std::env::set_var(n, v); - } - } - } - Self { - vars: saved, - _lock: lock, - } - } - } - - impl Drop for EnvGuard { - fn drop(&mut self) { - // SAFETY: lock を握ったまま元に戻す。 - unsafe { - for (n, v) in &self.vars { - match v { - Some(v) => std::env::set_var(n, v), - None => std::env::remove_var(n), - } - } - } - } - } #[test] fn config_dir_falls_back_to_home_dot_config() { - let _g = EnvGuard::new(&[("HOME", Some("/h"))]); - assert_eq!(config_dir().unwrap(), PathBuf::from("/h/.config/insomnia")); + assert_eq!( + resolve_config_dir_from_parts(None, None, None, Some(PathBuf::from("/h"))).unwrap(), + PathBuf::from("/h/.config/insomnia") + ); } #[test] fn config_dir_uses_xdg_when_set() { - let _g = EnvGuard::new(&[("HOME", Some("/h")), ("XDG_CONFIG_HOME", Some("/x"))]); - assert_eq!(config_dir().unwrap(), PathBuf::from("/x/insomnia")); + assert_eq!( + resolve_config_dir_from_parts( + None, + None, + Some(PathBuf::from("/x")), + Some(PathBuf::from("/h")), + ) + .unwrap(), + PathBuf::from("/x/insomnia") + ); } #[test] fn config_dir_insomnia_home_outranks_xdg() { - let _g = EnvGuard::new(&[ - ("HOME", Some("/h")), - ("XDG_CONFIG_HOME", Some("/x")), - ("INSOMNIA_HOME", Some("/sand")), - ]); - assert_eq!(config_dir().unwrap(), PathBuf::from("/sand/config")); + assert_eq!( + resolve_config_dir_from_parts( + None, + Some(PathBuf::from("/sand")), + Some(PathBuf::from("/x")), + Some(PathBuf::from("/h")), + ) + .unwrap(), + PathBuf::from("/sand/config") + ); } #[test] fn config_dir_explicit_wins_over_insomnia_home() { - let _g = EnvGuard::new(&[ - ("HOME", Some("/h")), - ("INSOMNIA_HOME", Some("/sand")), - ("INSOMNIA_CONFIG_DIR", Some("/explicit-cfg")), - ]); - assert_eq!(config_dir().unwrap(), PathBuf::from("/explicit-cfg")); + assert_eq!( + resolve_config_dir_from_parts( + Some(PathBuf::from("/explicit-cfg")), + Some(PathBuf::from("/sand")), + None, + Some(PathBuf::from("/h")), + ) + .unwrap(), + PathBuf::from("/explicit-cfg") + ); } #[test] fn data_dir_default_is_dot_insomnia() { - let _g = EnvGuard::new(&[("HOME", Some("/h"))]); - assert_eq!(data_dir().unwrap(), PathBuf::from("/h/.insomnia")); + assert_eq!( + resolve_data_dir_from_parts(None, None, Some(PathBuf::from("/h"))).unwrap(), + PathBuf::from("/h/.insomnia") + ); } #[test] fn data_dir_insomnia_home_is_data_dir_itself() { - let _g = EnvGuard::new(&[("HOME", Some("/h")), ("INSOMNIA_HOME", Some("/sand"))]); - assert_eq!(data_dir().unwrap(), PathBuf::from("/sand")); + assert_eq!( + resolve_data_dir_from_parts( + None, + Some(PathBuf::from("/sand")), + Some(PathBuf::from("/h")) + ) + .unwrap(), + PathBuf::from("/sand") + ); + } + + #[test] + fn data_dir_explicit_wins_over_insomnia_home() { + assert_eq!( + resolve_data_dir_from_parts( + Some(PathBuf::from("/explicit-data")), + Some(PathBuf::from("/sand")), + Some(PathBuf::from("/h")), + ) + .unwrap(), + PathBuf::from("/explicit-data") + ); } #[test] fn runtime_dir_prefers_xdg_runtime_dir() { - let _g = EnvGuard::new(&[ - ("HOME", Some("/h")), - ("XDG_RUNTIME_DIR", Some("/xdg-runtime")), - ]); assert_eq!( - runtime_dir().unwrap(), + resolve_runtime_dir_from_parts( + None, + None, + Some(PathBuf::from("/xdg-runtime")), + Some(PathBuf::from("/h")), + ) + .unwrap(), PathBuf::from("/xdg-runtime/insomnia") ); } #[test] fn runtime_dir_falls_back_to_dot_insomnia_run() { - let _g = EnvGuard::new(&[("HOME", Some("/h"))]); - assert_eq!(runtime_dir().unwrap(), PathBuf::from("/h/.insomnia/run")); + assert_eq!( + resolve_runtime_dir_from_parts(None, None, None, Some(PathBuf::from("/h"))).unwrap(), + PathBuf::from("/h/.insomnia/run") + ); } #[test] fn runtime_dir_insomnia_home_is_run_subdir() { - let _g = EnvGuard::new(&[ - ("HOME", Some("/h")), - ("XDG_RUNTIME_DIR", Some("/run/user/1000")), - ("INSOMNIA_HOME", Some("/sand")), - ]); - assert_eq!(runtime_dir().unwrap(), PathBuf::from("/sand/run")); + assert_eq!( + resolve_runtime_dir_from_parts( + None, + Some(PathBuf::from("/sand")), + Some(PathBuf::from("/run/user/1000")), + Some(PathBuf::from("/h")), + ) + .unwrap(), + PathBuf::from("/sand/run") + ); } #[test] - fn empty_env_treated_as_unset() { - let _g = EnvGuard::new(&[("HOME", Some("/h")), ("XDG_CONFIG_HOME", Some(""))]); - assert_eq!(config_dir().unwrap(), PathBuf::from("/h/.config/insomnia")); + fn runtime_dir_explicit_wins_over_insomnia_home() { + assert_eq!( + resolve_runtime_dir_from_parts( + Some(PathBuf::from("/explicit-run")), + Some(PathBuf::from("/sand")), + Some(PathBuf::from("/run/user/1000")), + Some(PathBuf::from("/h")), + ) + .unwrap(), + PathBuf::from("/explicit-run") + ); + } + + #[test] + fn empty_env_value_treated_as_unset_before_path_resolution() { + let xdg_config_home = path_from_env_value(Some("")); + + assert_eq!( + resolve_config_dir_from_parts(None, None, xdg_config_home, Some(PathBuf::from("/h"))) + .unwrap(), + PathBuf::from("/h/.config/insomnia") + ); } #[test] fn returns_none_when_nothing_set() { - let _g = EnvGuard::new(&[]); - assert!(config_dir().is_none()); - assert!(data_dir().is_none()); - assert!(runtime_dir().is_none()); + assert!(resolve_config_dir_from_parts(None, None, None, None).is_none()); + assert!(resolve_data_dir_from_parts(None, None, None).is_none()); + assert!(resolve_runtime_dir_from_parts(None, None, None, None).is_none()); } #[test] fn well_known_files_compose_off_base_dirs() { - let _g = EnvGuard::new(&[("INSOMNIA_HOME", Some("/sand"))]); + let config_dir = Some(PathBuf::from("/sand/config")); + let data_dir = Some(PathBuf::from("/sand")); + let runtime_dir = Some(PathBuf::from("/sand/run")); + assert_eq!( - user_profiles_path().unwrap(), + user_profiles_path_from_config_dir(config_dir.clone()).unwrap(), PathBuf::from("/sand/config/profiles.toml") ); assert_eq!( - user_prompts_dir().unwrap(), + user_prompts_dir_from_config_dir(config_dir.clone()).unwrap(), PathBuf::from("/sand/config/prompts") ); assert_eq!( - user_pack_file().unwrap(), + user_pack_file_from_config_dir(config_dir.clone()).unwrap(), PathBuf::from("/sand/config/prompts.toml") ); assert_eq!( - user_catalog_override("providers.toml").unwrap(), + user_catalog_override_from_config_dir(config_dir, "providers.toml").unwrap(), PathBuf::from("/sand/config/providers.toml") ); - assert_eq!(sessions_dir().unwrap(), PathBuf::from("/sand/sessions")); assert_eq!( - pod_registry_path().unwrap(), + sessions_dir_from_data_dir(data_dir).unwrap(), + PathBuf::from("/sand/sessions") + ); + assert_eq!( + pod_registry_path_from_runtime_dir(runtime_dir.clone()).unwrap(), PathBuf::from("/sand/run/pods.json") ); assert_eq!( - pod_runtime_dir("foo").unwrap(), + pod_runtime_dir_from_runtime_dir(runtime_dir.clone(), "foo").unwrap(), PathBuf::from("/sand/run/foo") ); assert_eq!( - pod_socket_path("foo").unwrap(), + pod_socket_path_from_runtime_dir(runtime_dir, "foo").unwrap(), PathBuf::from("/sand/run/foo/sock") ); }