Skip to main content

sqlite_graphrag/
paths.rs

1//! XDG/cwd path resolution and traversal-safe overrides.
2//!
3//! Resolves data directories via [`directories::ProjectDirs`] and validates
4//! that user-supplied paths cannot escape the project root.
5
6use crate::errors::AppError;
7use crate::i18n::validation;
8use directories::ProjectDirs;
9use std::path::{Path, PathBuf};
10
11#[derive(Debug)]
12pub struct AppPaths {
13    pub db: PathBuf,
14    pub models: PathBuf,
15}
16
17impl AppPaths {
18    pub fn resolve(db_override: Option<&str>) -> Result<Self, AppError> {
19        let proj = ProjectDirs::from("", "", "sqlite-graphrag").ok_or_else(|| {
20            AppError::Io(std::io::Error::other(
21                "não foi possível determinar o diretório home",
22            ))
23        })?;
24
25        let cache_root = if let Some(override_dir) = std::env::var_os("SQLITE_GRAPHRAG_CACHE_DIR") {
26            PathBuf::from(override_dir)
27        } else {
28            proj.cache_dir().to_path_buf()
29        };
30
31        let db = if let Some(p) = db_override {
32            validate_path(p)?;
33            PathBuf::from(p)
34        } else if let Ok(env_path) = std::env::var("SQLITE_GRAPHRAG_DB_PATH") {
35            validate_path(&env_path)?;
36            PathBuf::from(env_path)
37        } else if let Some(home_dir) = home_env_dir()? {
38            home_dir.join("graphrag.sqlite")
39        } else {
40            std::env::current_dir()
41                .map_err(AppError::Io)?
42                .join("graphrag.sqlite")
43        };
44
45        Ok(Self {
46            db,
47            models: cache_root.join("models"),
48        })
49    }
50
51    pub fn ensure_dirs(&self) -> Result<(), AppError> {
52        for dir in [parent_or_err(&self.db)?, self.models.as_path()] {
53            std::fs::create_dir_all(dir)?;
54        }
55        Ok(())
56    }
57}
58
59fn validate_path(p: &str) -> Result<(), AppError> {
60    if p.contains("..") {
61        return Err(AppError::Validation(validation::path_traversal(p)));
62    }
63    Ok(())
64}
65
66/// Resolves `SQLITE_GRAPHRAG_HOME` as the root directory for the default database.
67///
68/// Returns `Ok(Some(dir))` when the env var is set and valid,
69/// `Ok(None)` when absent or empty (falls back to `current_dir`),
70/// and `Err(...)` when the value contains traversal components.
71fn home_env_dir() -> Result<Option<PathBuf>, AppError> {
72    let raw = match std::env::var("SQLITE_GRAPHRAG_HOME") {
73        Ok(v) => v,
74        Err(_) => return Ok(None),
75    };
76    if raw.is_empty() {
77        return Ok(None);
78    }
79    validate_path(&raw)?;
80    Ok(Some(PathBuf::from(raw)))
81}
82
83pub(crate) fn parent_or_err(path: &Path) -> Result<&Path, AppError> {
84    path.parent().ok_or_else(|| {
85        AppError::Validation(format!(
86            "caminho '{}' não possui componente pai válido",
87            path.display()
88        ))
89    })
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use serial_test::serial;
96    use tempfile::TempDir;
97
98    /// Clears all variables that affect `AppPaths::resolve` to isolate the
99    /// test from the developer/CI environment.
100    fn limpar_env_paths() {
101        // SAFETY: testes marcados com #[serial] garantem ausência de concorrência.
102        unsafe {
103            std::env::remove_var("SQLITE_GRAPHRAG_HOME");
104            std::env::remove_var("SQLITE_GRAPHRAG_DB_PATH");
105            std::env::remove_var("SQLITE_GRAPHRAG_CACHE_DIR");
106        }
107    }
108
109    #[test]
110    #[serial]
111    fn home_env_resolve_db_em_subdir() {
112        limpar_env_paths();
113        let tmp = TempDir::new().expect("tempdir");
114        // SAFETY: serial.
115        unsafe {
116            std::env::set_var("SQLITE_GRAPHRAG_HOME", tmp.path());
117        }
118
119        let paths = AppPaths::resolve(None).expect("resolve com HOME valido");
120        assert_eq!(paths.db, tmp.path().join("graphrag.sqlite"));
121
122        limpar_env_paths();
123    }
124
125    #[test]
126    #[serial]
127    fn home_env_traversal_rejeitado() {
128        limpar_env_paths();
129        // SAFETY: serial.
130        unsafe {
131            std::env::set_var("SQLITE_GRAPHRAG_HOME", "/tmp/../etc");
132        }
133
134        let resultado = AppPaths::resolve(None);
135        assert!(
136            matches!(resultado, Err(AppError::Validation(_))),
137            "traversal em SQLITE_GRAPHRAG_HOME deve falhar como Validation, obteve {resultado:?}"
138        );
139
140        limpar_env_paths();
141    }
142
143    #[test]
144    #[serial]
145    fn db_path_vence_home() {
146        limpar_env_paths();
147        let tmp_home = TempDir::new().expect("tempdir home");
148        let tmp_db = TempDir::new().expect("tempdir db");
149        let db_explicito = tmp_db.path().join("explicito.sqlite");
150        // SAFETY: serial.
151        unsafe {
152            std::env::set_var("SQLITE_GRAPHRAG_HOME", tmp_home.path());
153            std::env::set_var("SQLITE_GRAPHRAG_DB_PATH", &db_explicito);
154        }
155
156        let paths = AppPaths::resolve(None).expect("resolve com DB_PATH e HOME");
157        assert_eq!(paths.db, db_explicito);
158
159        limpar_env_paths();
160    }
161
162    #[test]
163    #[serial]
164    fn flag_vence_home() {
165        limpar_env_paths();
166        let tmp_home = TempDir::new().expect("tempdir home");
167        let tmp_flag = TempDir::new().expect("tempdir flag");
168        let db_flag = tmp_flag.path().join("via-flag.sqlite");
169        // SAFETY: serial.
170        unsafe {
171            std::env::set_var("SQLITE_GRAPHRAG_HOME", tmp_home.path());
172        }
173
174        let paths = AppPaths::resolve(Some(db_flag.to_str().expect("utf8")))
175            .expect("resolve com flag e HOME");
176        assert_eq!(paths.db, db_flag);
177
178        limpar_env_paths();
179    }
180
181    #[test]
182    #[serial]
183    fn home_env_vazio_cai_para_cwd() {
184        limpar_env_paths();
185        // SAFETY: serial.
186        unsafe {
187            std::env::set_var("SQLITE_GRAPHRAG_HOME", "");
188        }
189
190        let paths = AppPaths::resolve(None).expect("resolve com HOME vazio");
191        let esperado = std::env::current_dir()
192            .expect("cwd")
193            .join("graphrag.sqlite");
194        assert_eq!(paths.db, esperado);
195
196        limpar_env_paths();
197    }
198
199    #[test]
200    fn parent_or_err_aceita_path_normal() {
201        let p = PathBuf::from("/home/usuario/db.sqlite");
202        let pai = parent_or_err(&p).expect("parent valido");
203        assert_eq!(pai, Path::new("/home/usuario"));
204    }
205
206    #[test]
207    fn parent_or_err_aceita_path_relativo() {
208        let p = PathBuf::from("subpasta/arquivo.sqlite");
209        let pai = parent_or_err(&p).expect("parent relativo");
210        assert_eq!(pai, Path::new("subpasta"));
211    }
212
213    #[test]
214    fn parent_or_err_rejeita_raiz_unix() {
215        let p = PathBuf::from("/");
216        let resultado = parent_or_err(&p);
217        assert!(matches!(resultado, Err(AppError::Validation(_))));
218    }
219
220    #[test]
221    fn parent_or_err_rejeita_path_vazio() {
222        let p = PathBuf::from("");
223        let resultado = parent_or_err(&p);
224        assert!(matches!(resultado, Err(AppError::Validation(_))));
225    }
226}