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
61fn 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 fn limpar_env_paths() {
96 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 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 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 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 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 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}