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