1use 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
66fn 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 fn limpar_env_paths() {
101 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 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 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 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 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 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}