Skip to main content

pg_embedded_setup_unpriv/cache/
config.rs

1//! Configuration for the shared binary cache.
2//!
3//! Resolves the cache directory from environment variables with XDG-compliant
4//! fallback paths.
5
6use camino::Utf8PathBuf;
7use std::path::PathBuf;
8
9/// Subdirectory path within the XDG cache home.
10const CACHE_SUBDIR: &str = "pg-embedded/binaries";
11
12/// Configuration for the shared binary cache.
13#[derive(Debug, Clone)]
14pub struct BinaryCacheConfig {
15    /// Root directory for cached `PostgreSQL` binaries.
16    pub cache_dir: Utf8PathBuf,
17}
18
19impl BinaryCacheConfig {
20    /// Creates a new cache configuration using the resolved cache directory.
21    #[must_use]
22    pub fn new() -> Self {
23        Self {
24            cache_dir: resolve_cache_dir(),
25        }
26    }
27
28    /// Creates a cache configuration with a custom directory.
29    #[must_use]
30    pub const fn with_dir(cache_dir: Utf8PathBuf) -> Self {
31        Self { cache_dir }
32    }
33}
34
35impl Default for BinaryCacheConfig {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41/// Resolves the binary cache directory from environment and XDG conventions.
42///
43/// The resolution order is:
44///
45/// 1. `PG_BINARY_CACHE_DIR` environment variable if set and valid UTF-8
46/// 2. `$XDG_CACHE_HOME/pg-embedded/binaries` if `XDG_CACHE_HOME` is set
47/// 3. `~/.cache/pg-embedded/binaries` as fallback
48/// 4. `std::env::temp_dir()/pg-embedded/binaries` as last resort (platform-dependent)
49///
50/// # Examples
51///
52/// ```
53/// use pg_embedded_setup_unpriv::cache::resolve_cache_dir;
54///
55/// let cache_dir = resolve_cache_dir();
56/// assert!(!cache_dir.as_str().is_empty());
57/// ```
58#[must_use]
59pub fn resolve_cache_dir() -> Utf8PathBuf {
60    // Check explicit environment variable first
61    if let Some(dir) = resolve_from_env() {
62        return dir;
63    }
64
65    // Try XDG cache home
66    if let Some(dir) = resolve_from_xdg_cache() {
67        return dir;
68    }
69
70    // Fall back to home directory
71    if let Some(dir) = resolve_from_home() {
72        return dir;
73    }
74
75    // Last resort: temp directory (portable across platforms)
76    let temp_path = std::env::temp_dir().join("pg-embedded").join("binaries");
77    Utf8PathBuf::from_path_buf(temp_path).unwrap_or_else(|path| {
78        // If temp_dir is not valid UTF-8, use a hardcoded fallback
79        Utf8PathBuf::from(path.to_string_lossy().into_owned())
80    })
81}
82
83/// Attempts to resolve cache directory from `PG_BINARY_CACHE_DIR` environment variable.
84fn resolve_from_env() -> Option<Utf8PathBuf> {
85    let raw = std::env::var("PG_BINARY_CACHE_DIR").ok()?;
86    let trimmed = raw.trim();
87    if trimmed.is_empty() {
88        return None;
89    }
90    Utf8PathBuf::from_path_buf(PathBuf::from(trimmed)).ok()
91}
92
93/// Attempts to resolve cache directory from `XDG_CACHE_HOME`.
94fn resolve_from_xdg_cache() -> Option<Utf8PathBuf> {
95    let raw = std::env::var("XDG_CACHE_HOME").ok()?;
96    let trimmed = raw.trim();
97    if trimmed.is_empty() {
98        return None;
99    }
100    let path = Utf8PathBuf::from_path_buf(PathBuf::from(trimmed)).ok()?;
101    Some(path.join(CACHE_SUBDIR))
102}
103
104/// Attempts to resolve cache directory from home directory.
105fn resolve_from_home() -> Option<Utf8PathBuf> {
106    let home = dirs::home_dir()?;
107    let path = Utf8PathBuf::from_path_buf(home).ok()?;
108    Some(path.join(".cache").join(CACHE_SUBDIR))
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::test_support::scoped_env;
115    use rstest::rstest;
116    use std::ffi::OsString;
117
118    /// Consolidated test for `resolve_cache_dir` with various environment configurations.
119    #[rstest]
120    #[case::explicit_env_var(Some("/custom/cache/path"), None, "/custom/cache/path")]
121    #[case::xdg_fallback(
122        None,
123        Some("/home/testuser/.cache"),
124        &format!("/home/testuser/.cache/{CACHE_SUBDIR}")
125    )]
126    #[case::empty_env_var_uses_xdg(
127        Some(""),
128        Some("/home/testuser/.cache"),
129        &format!("/home/testuser/.cache/{CACHE_SUBDIR}")
130    )]
131    #[case::whitespace_only_uses_xdg(
132        Some("   "),
133        Some("/home/testuser/.cache"),
134        &format!("/home/testuser/.cache/{CACHE_SUBDIR}")
135    )]
136    fn resolve_cache_dir_respects_env_priority(
137        #[case] pg_cache_dir: Option<&str>,
138        #[case] xdg_cache_home: Option<&str>,
139        #[case] expected: &str,
140    ) {
141        let env_vars = vec![
142            (
143                OsString::from("PG_BINARY_CACHE_DIR"),
144                pg_cache_dir.map(OsString::from),
145            ),
146            (
147                OsString::from("XDG_CACHE_HOME"),
148                xdg_cache_home.map(OsString::from),
149            ),
150        ];
151
152        let _guard = scoped_env(env_vars);
153        let result = resolve_cache_dir();
154        assert_eq!(result.as_str(), expected);
155    }
156
157    #[test]
158    fn binary_cache_config_default_uses_resolved_dir() {
159        let _guard = scoped_env([
160            (OsString::from("PG_BINARY_CACHE_DIR"), None),
161            (OsString::from("XDG_CACHE_HOME"), None),
162        ]);
163        let config = BinaryCacheConfig::default();
164        assert!(config.cache_dir.as_str().contains("pg-embedded"));
165    }
166
167    #[test]
168    fn binary_cache_config_with_dir_uses_provided_path() {
169        let custom_path = Utf8PathBuf::from("/custom/path");
170        let config = BinaryCacheConfig::with_dir(custom_path.clone());
171        assert_eq!(config.cache_dir, custom_path);
172    }
173}