Skip to main content

sqlite_graphrag/
paths.rs

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